ehrenamt/notes/2026-09-16_AHK-Ehrenamtsmanagement_Entscheidungsprotokoll-bisher.md
2026-09-17 00:11:23 +02:00

15 KiB

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:

AHK-Ehrenamtsmanagement

Technischer Name:

ahk-ehrenamtsmanagement

Der technische Name soll konsistent für Repository, App-Verzeichnis, Stack-Verzeichnis und Container verwendet werden.

1.3 Entwicklungsumgebung

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:

ahk-ehrenamtsmanagement

Projektbindung:

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:

ahk-ehrenamtsmanagement/
├── PROJECT_DESIGN.md
├── PROJECT_DESIGN_VOR_ORT_MOCKUP.png
└── .gitignore

Initialisierung:

mkdir ahk-ehrenamtsmanagement
cd ahk-ehrenamtsmanagement
git init

Minimale .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:

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:

Flask

requirements-dev.txt:

-r requirements.txt

pytest

Installation:

python -m pip install -r requirements-dev.txt

Bevorzugte Form:

python -m pip ...

2.3 Erste Flask-Struktur

Verbindlich:

  • Flask App Factory
  • Blueprints
  • eigener Core-Bereich für /health

Struktur:

app/
├── __init__.py
└── core/
    ├── __init__.py
    └── health/
        ├── __init__.py
        └── routes.py

app/core/health/__init__.py:

from flask import Blueprint

bp = Blueprint("health", __name__)

from app.core.health import routes

app/core/health/routes.py:

from app.core.health import bp

@bp.get("/health")
def health():
    return {"status": "ok"}, 200

Initiale app/__init__.py:

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:

flask --app 'app:create_app' run --debug

Healthcheck:

curl -i http://127.0.0.1:5000/health

Erwartung:

{"status": "ok"}

mit HTTP 200.

2.4 Erste Testbasis

tests/test_health.py:

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:

python -m pytest

3. Phase 1 — Technisches App-Fundament

Reihenfolge

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
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:

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:

def create_app(
    config_class=DevelopmentConfig,
    config_overrides=None,
):
    ...

Reihenfolge:

Factory starten
→ Config-Klasse laden
→ optionale Overrides
→ Extensions initialisieren
→ Blueprints registrieren
→ später Module registrieren

Sinngemäß:

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:

app/extensions.py

Nur Extension-Instanzen, keine Models, keine Businesslogik, keine Konfiguration.

Zunächst:

from flask_sqlalchemy import SQLAlchemy

db = SQLAlchemy()

Später:

from flask_migrate import Migrate
from flask_sqlalchemy import SQLAlchemy

db = SQLAlchemy()
migrate = Migrate()

In der Factory:

db.init_app(app)
migrate.init_app(app, db)

3.5 SQLite-Strategie

Development:

instance/app.db

Sinngemäß:

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
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:

Flask
Flask-SQLAlchemy
Flask-Migrate

Initialisierung bereits in Phase 1:

flask --app 'app:create_app' db init

Ergebnis:

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

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:

git add .
git status
git commit -m "Establish application foundation"

4. Phase 2 — Plattform-Core / Datenmodell

Reihenfolge

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:

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:

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:

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:

Aidshilfe Köln e.V.
→ aidshilfe-koeln

URL:

/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:

Aidshilfe Köln e.V.

Es ist nicht geplant, die App anderen Aidshilfen oder externen Organisationen bereitzustellen.


7. Account

Beschlossen:

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:

"  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:

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:

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:

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:

name

wird global eindeutig gehalten.

organization_id bleibt trotzdem Teil des Architekturmodells.

Logo ist optional.

Gespeichert wird:

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:

#RRGGBB

Beispiele:

#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:

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:

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:

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:

Phase 2.3 — BrandProfile

Nächste Entscheidung:

Soll BrandProfile created_at und updated_at erhalten?

Danach wird BrandProfile abgeschlossen und Phase 2.4 Team begonnen.