# AHK-Ehrenamtsmanagement — Entscheidungsprotokoll und bisheriger Aufbau > **Stand:** 2026-09-16 > **Projekt:** AHK-Ehrenamtsmanagement > **Technischer Name:** `ahk-ehrenamtsmanagement` > **Status:** Greenfield-Neustart; Phase 0 vollständig geplant, Phase 1 vollständig entschieden, Phase 2 begonnen > **Verbindliche Grundlage:** `PROJECT_DESIGN.md` > **Visuelle Referenz:** `PROJECT_DESIGN_VOR_ORT_MOCKUP.png` > **Arbeitsprinzip:** Wenn `PROJECT_DESIGN.md` etwas nicht entscheidet, wird nicht geraten, sondern bewusst gemeinsam entschieden. --- # 1. Grundsatzentscheidungen ## 1.1 Greenfield statt Legacy-Umbau Der bestehende Prototyp wird **nicht migriert**. - kein erhaltenswerter Produktivdatenbestand - nur ein einzelner Testnutzer ohne relevante Inhalte - Ziel ist ausdrücklich, den Aufbau der App von Grund auf zu lernen - kein Legacy-Code wird nur deshalb übernommen, weil er bereits existiert - keine Legacy-Datenmigration - keine Kompatibilitätsschicht - keine Datenrettung aus der alten SQLite-Datenbank Falls alte Implementierungen später als Referenz dienen, werden sie zuerst gegen `PROJECT_DESIGN.md` geprüft. ## 1.2 Projektname Sichtbarer Produktname: ```text AHK-Ehrenamtsmanagement ``` Technischer Name: ```text ahk-ehrenamtsmanagement ``` Der technische Name soll konsistent für Repository, App-Verzeichnis, Stack-Verzeichnis und Container verwendet werden. ## 1.3 Entwicklungsumgebung ```text Betriebssystem: EndeavourOS / Arch Linux Shell: Bash Python-Verwaltung: pyenv Virtuelle Umgebungen: pyenv-virtualenv Python-Serie: 3.14 ``` Fish ist nicht mehr Standardshell. ## 1.4 Python-Environment Eigenes pyenv-virtualenv: ```text ahk-ehrenamtsmanagement ``` Projektbindung: ```bash pyenv local ahk-ehrenamtsmanagement ``` `.python-version` wird versioniert. Das eigentliche Virtualenv liegt außerhalb des Repositories in der pyenv-Verwaltung. --- # 2. Phase 0 — Greenfield-Projektbasis ## Ziel Am Ende von Phase 0 existiert eine minimale, lauffähige und getestete Flask-Anwendung. Noch nicht enthalten: - Datenbankmodelle - Auth - Organisationen - Teams - Rollen - Templates - Branding - Docker - Fachmodule ## 2.1 Git-Grundlage Initiale Struktur: ```text ahk-ehrenamtsmanagement/ ├── PROJECT_DESIGN.md ├── PROJECT_DESIGN_VOR_ORT_MOCKUP.png └── .gitignore ``` Initialisierung: ```bash mkdir ahk-ehrenamtsmanagement cd ahk-ehrenamtsmanagement git init ``` Minimale `.gitignore`: ```gitignore # Python __pycache__/ *.py[cod] # Optional local virtual environments .venv/ venv/ # Local application data instance/ # Environment / secrets .env # OS / editor noise .DS_Store ``` Baseline-Commit: ```bash git add . git status git commit -m "Initialize AHK-Ehrenamtsmanagement project" ``` Ein Remote ist in Phase 0 nicht Pflicht. ## 2.2 Abhängigkeiten Runtime und Development werden getrennt. `requirements.txt`: ```text Flask ``` `requirements-dev.txt`: ```text -r requirements.txt pytest ``` Installation: ```bash python -m pip install -r requirements-dev.txt ``` Bevorzugte Form: ```text python -m pip ... ``` ## 2.3 Erste Flask-Struktur Verbindlich: - Flask App Factory - Blueprints - eigener Core-Bereich für `/health` Struktur: ```text app/ ├── __init__.py └── core/ ├── __init__.py └── health/ ├── __init__.py └── routes.py ``` `app/core/health/__init__.py`: ```python from flask import Blueprint bp = Blueprint("health", __name__) from app.core.health import routes ``` `app/core/health/routes.py`: ```python from app.core.health import bp @bp.get("/health") def health(): return {"status": "ok"}, 200 ``` Initiale `app/__init__.py`: ```python from flask import Flask def create_app(): app = Flask(__name__) from app.core.health import bp as health_bp app.register_blueprint(health_bp) return app ``` Start: ```bash flask --app 'app:create_app' run --debug ``` Healthcheck: ```bash curl -i http://127.0.0.1:5000/health ``` Erwartung: ```json {"status": "ok"} ``` mit HTTP 200. ## 2.4 Erste Testbasis `tests/test_health.py`: ```python from app import create_app def test_health_endpoint(): app = create_app() client = app.test_client() response = client.get("/health") assert response.status_code == 200 assert response.get_json() == {"status": "ok"} ``` Ausführen: ```bash python -m pytest ``` --- # 3. Phase 1 — Technisches App-Fundament ## Reihenfolge ```text 1.1 Architektur-Gerüst 1.2 Konfigurationssystem 1.3 Flask Extensions 1.4 SQLAlchemy + SQLite 1.5 Flask-Migrate / Alembic 1.6 Teststruktur / Fixtures 1.7 Foundation-Tests 1.8 Checkpoint ``` ## 3.1 Architektur-Gerüst Die vollständige Ordnerstruktur wird als Gerüst angelegt. Wichtig: - nur Ordner/Pakete vorbereiten - nicht vorsorglich überall `models.py`, `routes.py`, `services.py`, `permissions.py` oder `forms.py` anlegen - solche Dateien entstehen erst bei echter Implementierung ```text app/ ├── __init__.py ├── core/ │ ├── __init__.py │ ├── health/ │ ├── auth/ │ ├── accounts/ │ ├── organizations/ │ ├── teams/ │ ├── memberships/ │ ├── invitations/ │ ├── permissions/ │ ├── branding/ │ ├── storage/ │ ├── notifications/ │ ├── dashboard/ │ ├── profiles/ │ └── audit/ ├── modules/ │ ├── __init__.py │ ├── scheduling/ │ ├── documents/ │ └── chat/ ├── templates/ │ ├── core/ │ └── components/ └── static/ ├── css/ ├── js/ └── avatars/ tests/ instance/ ``` Git speichert keine leeren Ordner. Keine künstliche `.gitkeep`-Orgie. ## 3.2 `config.py` Liegt im **Projektroot**. Zunächst: ```python class Config: pass class DevelopmentConfig(Config): DEBUG = True class TestingConfig(Config): TESTING = True ``` Noch nicht vorsorglich anlegen: - `ProductionConfig` - `SECRET_KEY` - SMTP - Uploadlimits - Auth-Zeiten - Polling-Intervalle ## 3.3 App-Factory-Interface Festgelegt: ```python def create_app( config_class=DevelopmentConfig, config_overrides=None, ): ... ``` Reihenfolge: ```text Factory starten → Config-Klasse laden → optionale Overrides → Extensions initialisieren → Blueprints registrieren → später Module registrieren ``` Sinngemäß: ```python from flask import Flask from config import DevelopmentConfig def create_app( config_class=DevelopmentConfig, config_overrides=None, ): app = Flask(__name__) app.config.from_object(config_class) if config_overrides: app.config.from_mapping(config_overrides) return app ``` ## 3.4 Zentrale Flask-Extensions Datei: ```text app/extensions.py ``` Nur Extension-Instanzen, keine Models, keine Businesslogik, keine Konfiguration. Zunächst: ```python from flask_sqlalchemy import SQLAlchemy db = SQLAlchemy() ``` Später: ```python from flask_migrate import Migrate from flask_sqlalchemy import SQLAlchemy db = SQLAlchemy() migrate = Migrate() ``` In der Factory: ```python db.init_app(app) migrate.init_app(app, db) ``` ## 3.5 SQLite-Strategie Development: ```text instance/app.db ``` Sinngemäß: ```python SQLALCHEMY_DATABASE_URI = "sqlite:///app.db" ``` Tests: - pro Test eigene temporäre echte SQLite-Datei - keine In-Memory-Datenbank - keine gemeinsam genutzte Testdatenbank - nie `instance/app.db` ```text DevelopmentConfig → instance/app.db TestingConfig → temporäre SQLite-Datei pro Test Production später → /app/instance/app.db ``` ## 3.6 Flask-Migrate / Alembic Runtime-Abhängigkeiten: ```text Flask Flask-SQLAlchemy Flask-Migrate ``` Initialisierung bereits in Phase 1: ```bash flask --app 'app:create_app' db init ``` Ergebnis: ```text migrations/ ├── alembic.ini ├── env.py ├── script.py.mako └── versions/ ``` Festgelegt: - `migrations/` wird versioniert - keine künstliche leere erste Migration - erste echte Migration erst mit erstem Domain-Model in Phase 2 - `db.create_all()` ist keine Produktionsstrategie ## 3.7 pytest-Fixtures ```text tests/ ├── conftest.py └── test_health.py ``` In Phase 1 nur: - `app` - `client` - temporäre SQLite-Testdatenbank Noch keine Domain-Fixtures. ## 3.8 Foundation-Tests Mindestens: 1. `/health` liefert 200 und `{"status": "ok"}` 2. `TestingConfig` ist aktiv 3. `config_overrides` überschreibt Config-Werte 4. SQLAlchemy erreicht die temporäre SQLite-DB (`SELECT 1`) 5. Tests verwenden niemals `instance/app.db` ## 3.9 Phase-1-Checkpoint Phase 1 ist fertig, wenn: - Architektur-Gerüst vorhanden - Root-`config.py` vorhanden - Factory-Interface umgesetzt - `app/extensions.py` vorhanden - SQLAlchemy integriert - Development-SQLite auf `instance/app.db` - Tests je eigene temporäre SQLite-Datei - Flask-Migrate/Alembic initialisiert - keine künstliche Migration erzeugt - `/health` funktioniert - alle Foundation-Tests grün - keine Secrets oder lokalen DB-Daten versioniert - sauber committed Beispiel: ```bash git add . git status git commit -m "Establish application foundation" ``` --- # 4. Phase 2 — Plattform-Core / Datenmodell ## Reihenfolge ```text 2.1 Organization 2.2 Account 2.3 BrandProfile 2.4 Team 2.5 OrganizationMembership 2.6 TeamMembership 2.7 Permission-Grundlagen 2.8 TeamInvitation 2.9 OrganizationAdminInvitation 2.10 TeamModule 2.11 AuditLog 2.12 Core-Integritätstests 2.13 Checkpoint ``` `BrandProfile` wird bewusst **vor `Team`** gebaut, damit `Team.brand_profile_id` von Anfang an auf ein reales Modell zeigt. --- # 5. Globale Model-Konventionen ## 5.1 Primary Keys Normale Domain-Models verwenden: ```text Integer Primary Keys ``` Opaque/kryptographische Werte werden dort verwendet, wo sie tatsächlich nötig sind, z. B. Auth-Tokens oder Storage Keys. ## 5.2 Statusfelder Begrenzte Statusfelder: - Python-Enums - klare Stringwerte in der Datenbank Beispiele: ```text active archived disabled ``` ## 5.3 Zeitstempel Global: - intern UTC - Python-Seite timezone-aware - lokale Zeitzone erst bei Darstellung - keine lokale Serverzeit als Domain-Wahrheit --- # 6. `Organization` Beschlossen: ```text Organization ├── id Integer PK ├── name required ├── slug required, unique, stabil ├── status active | archived └── created_at UTC timestamp ``` ## Slug Ergänzt, weil Organisations-URLs Slugs verwenden: ```text Aidshilfe Köln e.V. → aidshilfe-koeln ``` URL: ```text /org/aidshilfe-koeln/manage/ ``` Der Slug ist eine **stabile technische Kennung** und folgt Namensänderungen nicht automatisch. ## Betriebsrealität Die Architektur behält `Organization`, aber der reale Betrieb ist auf genau eine Organisation ausgerichtet: ```text Aidshilfe Köln e.V. ``` Es ist nicht geplant, die App anderen Aidshilfen oder externen Organisationen bereitzustellen. --- # 7. `Account` Beschlossen: ```text Account ├── id Integer PK ├── email required, unique, normalized ├── display_name optional ├── status │ ├── active │ └── disabled ├── created_at UTC └── last_login_at optional, UTC ``` ## E-Mail-Normalisierung Beispiel: ```text " Patsy@Example.DE " → "patsy@example.de" ``` Regeln: - Whitespace entfernen - Groß-/Kleinschreibung für Account-Identität nicht unterscheiden - gleiche Normalisierung vor Suche und Speicherung - zentrale Normalisierungsfunktion ## Status Neue verifizierte/eingeladene Accounts starten als: ```text active ``` Kein zusätzlicher `pending`-Accountstatus. ## `display_name` Optional, weil Account vor abgeschlossenem Onboarding existieren kann. ## `last_login_at` Initial `NULL`. Nur nach erfolgreicher Authentifizierung aktualisieren, nicht bei jedem Seitenaufruf oder bei normaler Nutzung einer gültigen Device Session. ## `disabled` Wirkt sofort: ```text Account → disabled → alle aktiven DeviceSessions widerrufen → bestehende Sitzungen verlieren Zugriff → neue Logins nicht zugelassen ``` Bei späterer Reaktivierung: - alte Sessions bleiben widerrufen - neue Authentifizierung nötig - neue Device Session wird erzeugt --- # 8. `BrandProfile` Aktueller Stand: ```text BrandProfile ├── id ├── organization_id ├── name ├── logo_storage_key optional ├── primary_color ├── primary_strong ├── accent_color ├── accent_strong ├── heading_font └── body_font ``` ## 8.1 Name Da die App real nur für eine Organisation betrieben wird: ```text name ``` wird global eindeutig gehalten. `organization_id` bleibt trotzdem Teil des Architekturmodells. ## 8.2 Logo Logo ist optional. Gespeichert wird: ```text logo_storage_key ``` Kein Dateipfad, kein Originaldateiname. Der zentrale Storage-Service löst den opaque Key später auf. ## 8.3 Farben Validierte und normalisierte HEX-Strings: ```text #RRGGBB ``` Beispiele: ```text #00858A #005F61 #52E0CF #2596BE ``` Nicht als freie Brandwerte: - `rgb(...)` - `rgba(...)` - `hsl(...)` - `#RRGGBBAA` - beliebige CSS-Ausdrücke Semantische Ableitungen und Transparenzen entstehen im zentralen Designsystem. ## 8.4 Fonts Font-Auswahl über Code-Registry. Die Datenbank speichert nur stabile Keys: ```text heading_font = "big-shoulders-display" body_font = "noto-sans" ``` Keine freien Fontnamen, URLs oder CSS-Fragmente. ## 8.5 Font-Auslieferung Fonts werden lokal mit der Anwendung ausgeliefert. Keine Pflichtabhängigkeit von Google Fonts, CDN oder anderen externen Font-Diensten. Vor Nutzung einer Schrift wird geprüft, ob die Lizenz lokales Webhosting erlaubt. ## 8.6 Offene Entscheidung Noch nicht entschieden: ```text created_at updated_at ``` für `BrandProfile`. Variante A: - beide ergänzen Variante B: - keine Zeitstempel - Änderungsnachvollziehbarkeit nur über `AuditLog` Dies ist der aktuelle nächste offene Punkt. --- # 9. Noch nicht im Detail entschieden - `Team` - `OrganizationMembership` - `TeamMembership` - Permission-Helper - Einladungsmodelle - `TeamModule` - `AuditLog` - `AuthChallenge` - `DeviceSession` - Passwordless-Flow - BrandProfile-Seed für Vor-Ort - konkrete Font-Registry-Implementierung - Core Storage - UI-Komponenten - Dashboard - Scheduling - Documents - Chat - Deployment - SMTP - Docker - Caddy - Backup/Restore Alle diese Punkte bleiben an `PROJECT_DESIGN.md` gebunden und werden schrittweise entschieden. --- # 10. Arbeitsmethode Für jede Phase: ```text 1. PROJECT_DESIGN.md lesen 2. bereits Festgelegtes übernehmen 3. echte offene Entscheidungen identifizieren 4. gemeinsam entscheiden 5. erst danach Tasks ableiten 6. implementieren 7. testen 8. Checkpoint / Commit ``` Regel: > `TASKS.md` darf keine neue Architektur erfinden. --- # 11. Aktueller nächster Schritt Aktueller Stand: ```text Phase 2.3 — BrandProfile ``` Nächste Entscheidung: ```text Soll BrandProfile created_at und updated_at erhalten? ``` Danach wird `BrandProfile` abgeschlossen und Phase 2.4 `Team` begonnen.