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

897 lines
15 KiB
Markdown

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