Merge pull request 'BugFix manuelle Eingabe TRainings' (#105) from develop into main
Reviewed-on: #105
This commit is contained in:
commit
67b508f09b
|
|
@ -12,7 +12,8 @@ Dieser Ordner ist der **primäre Orientierungspunkt** für Claude Code / Cursor-
|
||||||
| 2 | **`rules/DOCUMENTATION.md`** – Ablage- und Dokumentationsregeln |
|
| 2 | **`rules/DOCUMENTATION.md`** – Ablage- und Dokumentationsregeln |
|
||||||
| 3 | `rules/ARCHITECTURE.md`, `rules/CODING_RULES.md`, `rules/LESSONS_LEARNED.md` |
|
| 3 | `rules/ARCHITECTURE.md`, `rules/CODING_RULES.md`, `rules/LESSONS_LEARNED.md` |
|
||||||
| 4 | Issue-Landkarte: **`.claude/docs/GITEA_ISSUES_INDEX.md`** |
|
| 4 | Issue-Landkarte: **`.claude/docs/GITEA_ISSUES_INDEX.md`** |
|
||||||
| 5 | **Universal CSV Import** (Modul/Executor/Vorlagen): **`docs/technical/UNIVERSAL_CSV_IMPORT_AGENT_GUIDE.md`** (unter `.claude/`) |
|
| 5 | **Jinkendo Foundation** (Designprinzipien 1–9): **`docs/jinkendo-foundation/design-principles/README.md`** |
|
||||||
|
| 6 | **Universal CSV Import** (Modul/Executor/Vorlagen): **`docs/technical/UNIVERSAL_CSV_IMPORT_AGENT_GUIDE.md`** (unter `.claude/`) |
|
||||||
|
|
||||||
Themen mit UI/Nav/PWA: siehe `../docs/issues/GUI_IA_ADMIN_NAV_2026-04-05.md` (im **Projekt**-`docs/`, nicht hier).
|
Themen mit UI/Nav/PWA: siehe `../docs/issues/GUI_IA_ADMIN_NAV_2026-04-05.md` (im **Projekt**-`docs/`, nicht hier).
|
||||||
|
|
||||||
|
|
@ -25,6 +26,7 @@ Themen mit UI/Nav/PWA: siehe `../docs/issues/GUI_IA_ADMIN_NAV_2026-04-05.md` (im
|
||||||
├── README.md ← Diese Datei
|
├── README.md ← Diese Datei
|
||||||
├── rules/ ← Verbindliche Regeln (versioniert, wenn konfiguriert)
|
├── rules/ ← Verbindliche Regeln (versioniert, wenn konfiguriert)
|
||||||
├── docs/ ← Spezifikationen + Arbeitspapiere
|
├── docs/ ← Spezifikationen + Arbeitspapiere
|
||||||
|
│ ├── jinkendo-foundation/ ← Foundation Produktfamilie (Designprinzipien)
|
||||||
│ ├── functional/ ← Fachlich (WAS)
|
│ ├── functional/ ← Fachlich (WAS)
|
||||||
│ ├── technical/ ← Technisch (WIE)
|
│ ├── technical/ ← Technisch (WIE)
|
||||||
│ ├── architecture/ ← Querschnitt
|
│ ├── architecture/ ← Querschnitt
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ _Dieser Ordner `.claude/docs/` ist per `.gitignore`-Ausnahme **versioniert** (Sp
|
||||||
├── ROADMAP.md ← Strategische Phasen (0–3)
|
├── ROADMAP.md ← Strategische Phasen (0–3)
|
||||||
├── CLEANUP_PLAN.md ← Historie Bereinigung März 2026
|
├── CLEANUP_PLAN.md ← Historie Bereinigung März 2026
|
||||||
├── prompts/ ← Exportierte Prompt-Artefakte (JSON)
|
├── prompts/ ← Exportierte Prompt-Artefakte (JSON)
|
||||||
|
├── jinkendo-foundation/ ← Foundation Produktfamilie (Designprinzipien)
|
||||||
├── functional/ ← Fachliche Spezifikationen (WAS)
|
├── functional/ ← Fachliche Spezifikationen (WAS)
|
||||||
├── technical/ ← Technische Spezifikationen & Referenz (WIE)
|
├── technical/ ← Technische Spezifikationen & Referenz (WIE)
|
||||||
├── working/ ← Arbeitspapiere, Analysen, Session-Snapshots
|
├── working/ ← Arbeitspapiere, Analysen, Session-Snapshots
|
||||||
|
|
@ -55,6 +56,7 @@ _Dieser Ordner `.claude/docs/` ist per `.gitignore`-Ausnahme **versioniert** (Sp
|
||||||
| Dashboard-Widgets | `technical/DASHBOARD_WIDGETS_AGENT_GUIDE.md` | Widget-Katalog + Registrierung (siehe Guide) |
|
| Dashboard-Widgets | `technical/DASHBOARD_WIDGETS_AGENT_GUIDE.md` | Widget-Katalog + Registrierung (siehe Guide) |
|
||||||
| Training Profiler / Resolver | `technical/TRAINING_PROFILE_RESOLVER_LAYER1.md`, `functional/TRAINING_TYPE_PROFILES.md` | Resolver-Module wie im Guide genannt |
|
| Training Profiler / Resolver | `technical/TRAINING_PROFILE_RESOLVER_LAYER1.md`, `functional/TRAINING_TYPE_PROFILES.md` | Resolver-Module wie im Guide genannt |
|
||||||
| Universal CSV Import | `technical/UNIVERSAL_CSV_IMPORT_AGENT_GUIDE.md` | `backend/csv_parser/`, `routers/csv_import.py`, `routers/admin_csv_templates.py` |
|
| Universal CSV Import | `technical/UNIVERSAL_CSV_IMPORT_AGENT_GUIDE.md` | `backend/csv_parser/`, `routers/csv_import.py`, `routers/admin_csv_templates.py` |
|
||||||
|
| **Designprinzipien (Produktfamilie)** | **`jinkendo-foundation/design-principles/README.md`** | Querschnittsmuster; Foundation für Schwester-Apps |
|
||||||
| Aktivität Produktionsreife | `technical/ACTIVITY_PRODUCTION_ARCHITECTURE_AND_PHASES.md` (+ EAV-Guide) | `backend/data_layer/activity_session_metrics.py`, `activity_metrics.py`, CSV-Orchestrierung |
|
| Aktivität Produktionsreife | `technical/ACTIVITY_PRODUCTION_ARCHITECTURE_AND_PHASES.md` (+ EAV-Guide) | `backend/data_layer/activity_session_metrics.py`, `activity_metrics.py`, CSV-Orchestrierung |
|
||||||
| Mitgliedschaft / Features | `technical/MEMBERSHIP_SYSTEM.md`, `architecture/FEATURE_ENFORCEMENT.md` | `backend/auth.py`, Feature-Logging, Router mit Enforcement |
|
| Mitgliedschaft / Features | `technical/MEMBERSHIP_SYSTEM.md`, `architecture/FEATURE_ENFORCEMENT.md` | `backend/auth.py`, Feature-Logging, Router mit Enforcement |
|
||||||
|
|
||||||
|
|
@ -92,6 +94,16 @@ _Dieser Ordner `.claude/docs/` ist per `.gitignore`-Ausnahme **versioniert** (Sp
|
||||||
|
|
||||||
## Technische Spezifikationen (`technical/`)
|
## Technische Spezifikationen (`technical/`)
|
||||||
|
|
||||||
|
### Jinkendo Foundation (Produktfamilie)
|
||||||
|
|
||||||
|
| Dokument | Inhalt |
|
||||||
|
|----------|--------|
|
||||||
|
| **[jinkendo-foundation/README.md](jinkendo-foundation/README.md)** | Einstieg Foundation-Ordner |
|
||||||
|
| **[design-principles/README.md](jinkendo-foundation/design-principles/README.md)** | Index: 9 Designprinzipien, Lesereihenfolge, Übergabe-Checkliste |
|
||||||
|
| `design-principles/*_DESIGN_PRINCIPLES.md` | Einzeldokumente (#1–#9) |
|
||||||
|
|
||||||
|
### Referenz & Agent-Guides (`technical/`)
|
||||||
|
|
||||||
| Dokument | Thema |
|
| Dokument | Thema |
|
||||||
|----------|--------|
|
|----------|--------|
|
||||||
| `AGGREGATION_METHODS.md` | Aggregation |
|
| `AGGREGATION_METHODS.md` | Aggregation |
|
||||||
|
|
@ -183,4 +195,4 @@ Siehe [`audit/README.md`](./audit/README.md).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Letzte Aktualisierung:** 9. April 2026 (Universal CSV Agent-Guide, Abgleich-Tabelle)
|
**Letzte Aktualisierung:** 4. Juli 2026 (Jinkendo Foundation, Designprinzipien nach `jinkendo-foundation/`)
|
||||||
|
|
|
||||||
46
.claude/docs/jinkendo-foundation/README.md
Normal file
46
.claude/docs/jinkendo-foundation/README.md
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
# Jinkendo Foundation
|
||||||
|
|
||||||
|
**Stand:** 2026-07-04
|
||||||
|
**Zweck:** Geteilte **Architektur-Foundation** für die Jinkendo-Produktfamilie (Mitai, Miken, Ikigai, Shinkan, …) — unabhängig von app-spezifischer Domänenlogik.
|
||||||
|
|
||||||
|
Dieser Ordner enthält **keine** Mitai-Fachspecs und **keine** operativen Agent-Guides zu einzelnen Features. Die Referenzimplementierung bleibt in **Mitai Jinkendo**; die Foundation beschreibt **übertragbare Muster**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Inhalt
|
||||||
|
|
||||||
|
| Pfad | Beschreibung |
|
||||||
|
|------|--------------|
|
||||||
|
| **[design-principles/README.md](./design-principles/README.md)** | **Einstieg:** Index aller 9 Designprinzipien-Dokumente, Lesereihenfolge, Übergabe-Checkliste |
|
||||||
|
| `design-principles/*_DESIGN_PRINCIPLES.md` | Einzeldokumente (#1 Prompt Engine … #9 Migration & Deploy) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Mitai-spezifische Implementierung
|
||||||
|
|
||||||
|
Agent-Guides, API-Referenz und Domänen-Specs liegen weiterhin unter:
|
||||||
|
|
||||||
|
- `.claude/docs/technical/` — WIE (Implementierung)
|
||||||
|
- `.claude/docs/functional/` — WAS (Fachlich)
|
||||||
|
- `docs/issues/` — Issue-Epics
|
||||||
|
|
||||||
|
Die Designprinzipien verlinken dorthin, wo Mitai als **Beispiel** dient.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Apps der Familie
|
||||||
|
|
||||||
|
| Domain | App |
|
||||||
|
|--------|-----|
|
||||||
|
| mitai.jinkendo.de | Körper-Tracking (Referenz) |
|
||||||
|
| miken.jinkendo.de | Meditation |
|
||||||
|
| ikigai.jinkendo.de | Lebenssinn |
|
||||||
|
| shinkan.jinkendo.de | Kampfsport |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Pflege
|
||||||
|
|
||||||
|
Neue Querschnittsmuster → neues Dokument unter `design-principles/` + Eintrag im [Index](./design-principles/README.md).
|
||||||
|
|
||||||
|
Regeln zur Ablage: [DOCUMENTATION.md](../../rules/DOCUMENTATION.md)
|
||||||
|
|
@ -0,0 +1,326 @@
|
||||||
|
# Auth & Session – Designprinzipien (Extraktion)
|
||||||
|
|
||||||
|
**Status:** Analyse / Arbeitspapier
|
||||||
|
**Stand:** 2026-07-04
|
||||||
|
**Geltungsbereich:** Authentifizierung, Session-Management, rollenbasierte API-Zugriffe — kein Mandanten-/SSO-System, keine Zahlungs-Auth
|
||||||
|
|
||||||
|
**Serie:** Designprinzipien für Produktfamilie · Dokument 5 von n
|
||||||
|
**Vorgänger:** [REGISTRY_PLUGIN_DESIGN_PRINCIPLES.md](./REGISTRY_PLUGIN_DESIGN_PRINCIPLES.md)
|
||||||
|
|
||||||
|
**Kernkomponenten:**
|
||||||
|
|
||||||
|
| Bereich | Pfade |
|
||||||
|
|---------|-------|
|
||||||
|
| Auth-Kern | `backend/auth.py` |
|
||||||
|
| Auth-Endpoints | `backend/routers/auth.py` |
|
||||||
|
| Profile | `backend/routers/profiles.py` |
|
||||||
|
| Frontend | `frontend/src/context/AuthContext.jsx`, `ProfileContext.jsx`, `utils/api.js` |
|
||||||
|
| DB | `profiles`, `sessions` |
|
||||||
|
| Architektur-Regeln | `.claude/rules/ARCHITECTURE.md`, `CLAUDE.md` § Auth |
|
||||||
|
| Vision (nicht implementiert) | `CENTRAL_SUBSCRIPTION_SYSTEM.md` (SSO/JWT) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Modul
|
||||||
|
|
||||||
|
**Auth & Session**
|
||||||
|
|
||||||
|
Server-seitige, token-basierte Authentifizierung mit FastAPI-Dependencies — **Identität und Rolle**, getrennt von Feature-Entitlements und fachlicher Logik.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Fachliche Verantwortung
|
||||||
|
|
||||||
|
Das Modul übernimmt:
|
||||||
|
|
||||||
|
1. **Identität** — Wer ist eingeloggt? (`profiles` + Passwort/bcrypt)
|
||||||
|
2. **Session** — Opaque Token in `sessions`, Ablaufzeit, Logout
|
||||||
|
3. **API-Gate** — `require_auth`, `require_admin`, `require_auth_flexible`
|
||||||
|
4. **Passwort-Lifecycle** — Hash, Verify, Reset, Registrierung, E-Mail-Verifizierung
|
||||||
|
5. **Rollen** — `profiles.role`: `user` \| `admin` (grobbinsenartig)
|
||||||
|
|
||||||
|
Es übernimmt **nicht**:
|
||||||
|
|
||||||
|
- Feature-Limits / Tier (→ Feature & Entitlement System, gleiche `auth.py`-Datei aber logisch getrennt)
|
||||||
|
- Mandanten-Isolation / Org-Workspaces
|
||||||
|
- OAuth/SSO/JWT (nur Vision)
|
||||||
|
- Authorization auf Datensatzebene (Row-Level Security)
|
||||||
|
|
||||||
|
### Was Mitai **ist** vs. **nicht ist**
|
||||||
|
|
||||||
|
| Mitai | Produktfamilien-Muster |
|
||||||
|
|-------|------------------------|
|
||||||
|
| 1 Login = 1 Profil (E-Mail) | ✅ Account-Modell |
|
||||||
|
| Historisch Multi-Profil auf einer Instanz | ⚠️ Legacy (`/profiles`, `X-Profile-Id`) |
|
||||||
|
| Self-hosted Einzelinstanz | ✅ Kein Multi-Tenant-SaaS |
|
||||||
|
| Session-Token in DB | ✅ Server-side Session Store |
|
||||||
|
| Zentrale Jinkendo-Auth (Vision) | ❌ nicht gebaut |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Administrierte vs. code-definierte Konfiguration
|
||||||
|
|
||||||
|
| Konfiguration | Speicherort | Administrierbar? |
|
||||||
|
|---------------|-------------|------------------|
|
||||||
|
| Nutzer-Stammdaten, Rolle | `profiles` | Admin (User-Verwaltung) / Self-Service |
|
||||||
|
| Session-Laufzeit | `profiles.session_days` (Default 30) | Profil/Admin |
|
||||||
|
| Passwort-Hash | `profiles.pin_hash` | Nutzer (change pin) |
|
||||||
|
| E-Mail-Verifizierung | `email_verified`, Token-Felder | System |
|
||||||
|
| Trial-Ende | `trial_ends_at` | System bei Registrierung |
|
||||||
|
| SMTP | Env (`SMTP_*`, `APP_URL`) | Deploy |
|
||||||
|
| Rate Limits Login/Register | Code (`5/min`, `3/hour`) | Code |
|
||||||
|
|
||||||
|
**Hardcodiert:** bcrypt, Token-Länge (`secrets.token_urlsafe(32)`), Rollen-Enum (`user`/`admin`), Header-Name `X-Auth-Token`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Session- und Auth-Flow
|
||||||
|
|
||||||
|
```
|
||||||
|
Login (email + password)
|
||||||
|
→ verify_pin (bcrypt | legacy SHA256)
|
||||||
|
→ optional bcrypt upgrade
|
||||||
|
→ INSERT sessions (token, profile_id, expires_at)
|
||||||
|
→ Client: localStorage bodytrack_token
|
||||||
|
|
||||||
|
Request
|
||||||
|
→ Header X-Auth-Token (api.js / AuthContext)
|
||||||
|
→ get_session(token) JOIN profiles
|
||||||
|
→ require_auth → session dict (profile_id, role, …)
|
||||||
|
|
||||||
|
Logout
|
||||||
|
→ DELETE sessions WHERE token=…
|
||||||
|
→ Client: localStorage clear
|
||||||
|
```
|
||||||
|
|
||||||
|
**Sonderfall:** `require_auth_flexible` — Token via Header **oder** Query `ssetoken` (SSE, `<img>`, Downloads).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Rollen
|
||||||
|
|
||||||
|
| Rolle | Mechanismus | Typische Rechte |
|
||||||
|
|-------|-------------|-----------------|
|
||||||
|
| **user** | `profiles.role = 'user'` | Eigene Daten, Features nach Tier |
|
||||||
|
| **admin** | `require_admin` | Admin-Shell, Prompts, User, System |
|
||||||
|
|
||||||
|
Kein feingranulares RBAC (keine Permission-Matrix). Admin ist Binär-Schalter.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Designprinzipien
|
||||||
|
|
||||||
|
### 1. FastAPI-Dependencies als Auth-Gate
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | Jeder geschützte Endpoint nutzt `session: dict = Depends(require_auth)` als **separaten** Parameter — nie in `Header()` eingebettet. |
|
||||||
|
| **Begründung** | Verhindert ungeschützte Endpoints durch falsche Parameter-Signatur. |
|
||||||
|
| **Quelle** | `CLAUDE.md` § Kritische Regeln; `auth.py` |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | Nicht linter-erzwungen; Legacy-Endpoints existieren. |
|
||||||
|
|
||||||
|
### 2. Server-side opaque Sessions
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | Token ist zufällig, in DB gespeichert; Validierung über `sessions` + Ablauf — kein JWT mit Client-Claims. |
|
||||||
|
| **Begründung** | Revocation (Logout), kein Trust in Client-Payload; einfaches Modell für Self-Hosted. |
|
||||||
|
| **Quelle** | `sessions` Tabelle; `make_token()`, `get_session()` |
|
||||||
|
| **Tragfähigkeit** | **hoch** (Single-App, Self-Hosted) |
|
||||||
|
| **Einschränkung** | Skalierung multi-node braucht shared session store; SSO-Familie braucht anderes Modell. |
|
||||||
|
|
||||||
|
### 3. profile_id aus Session, nicht aus Client
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | Autoritative Identität für neue Endpoints: `session['profile_id']` — Client darf Profil nicht wählen. |
|
||||||
|
| **Begründung** | Verhindert IDOR (Zugriff auf fremde Profile). |
|
||||||
|
| **Quelle** | `routers/goals.py`, `routers/prompts.py`; Architektur-Intent in `CLAUDE.md` |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | Legacy `get_pid(x_profile_id)` akzeptiert `X-Profile-Id` **ohne** Session-Abgleich — siehe Nicht übernehmen. |
|
||||||
|
|
||||||
|
### 4. bcrypt mit Legacy-Migration
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | Passwörter mit bcrypt; SHA256-Legacy beim Login erkannt und transparent auf bcrypt upgraded. |
|
||||||
|
| **Begründung** | Kein Big-Bang-Migration; sichere Hashes ohne Nutzer-Zwangs-Reset. |
|
||||||
|
| **Quelle** | `verify_pin()`, Login in `routers/auth.py` |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | Upgrade nur bei erfolgreichem Login. |
|
||||||
|
|
||||||
|
### 5. Rate Limiting auf Auth-Endpoints
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | Login, Register, Forgot-Password, Resend-Verification mit `slowapi`-Limits (IP-basiert). |
|
||||||
|
| **Begründung** | Brute-Force- und Abuse-Schutz. |
|
||||||
|
| **Quelle** | `routers/auth.py` (`5/minute`, `3/hour`); `main.py` Limiter |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | IP-only; kein account-based lockout. |
|
||||||
|
|
||||||
|
### 6. Keine E-Mail-Enumeration bei sensiblen Flows
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | Forgot-Password und Resend-Verification liefern generische Erfolgsmeldung, auch wenn E-Mail unbekannt. |
|
||||||
|
| **Begründung** | Privacy; erschwert Account-Scraping. |
|
||||||
|
| **Quelle** | `password_reset_request`, `resend_verification` |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | Register sagt „E-Mail bereits registriert“ (Enumeration möglich). |
|
||||||
|
|
||||||
|
### 7. E-Mail-Verifizierung vor voller Nutzung
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | Self-Register setzt `email_verified=FALSE`; Verify-Endpoint aktiviert + Auto-Login-Session. |
|
||||||
|
| **Begründung** | Valide Kontaktadresse; Spam-Reduktion. |
|
||||||
|
| **Quelle** | `register`, `verify_email` in `routers/auth.py` |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | Nicht überall im Backend erzwungen (Login ohne verified check?). |
|
||||||
|
|
||||||
|
### 8. Flexible Auth für technische Clients
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | `require_auth_flexible`: gleiche Session-Validierung via Header oder `?ssetoken=` für SSE/Bilder. |
|
||||||
|
| **Begründung** | Browser-APIs ohne Custom Headers. |
|
||||||
|
| **Quelle** | `auth.py`; Prompt SSE `/execute-stream` |
|
||||||
|
| **Tragfähigkeit** | **mittel–hoch** |
|
||||||
|
| **Einschränkung** | Token in URL kann in Logs/Referrer leaken — kurze Sessions / HTTPS Pflicht. |
|
||||||
|
|
||||||
|
### 9. Zentraler API-Client mit Token-Injektion
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | Frontend: `api.js` injiziert `X-Auth-Token` automatisch — kein scattered `fetch` ohne Auth. |
|
||||||
|
| **Begründung** | Konsistenz; eine Stelle für Token-Handling. |
|
||||||
|
| **Quelle** | `utils/api.js` → `hdrs()`; `getToken()` aus AuthContext |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | Einzelne Komponenten umgehen noch `api.js` (SettingsPage, EmailSettings). |
|
||||||
|
|
||||||
|
### 10. Auth getrennt von Authorization (Features)
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | `require_auth` = identifiziert; `check_feature_access` = berechtigt für Aktion — nacheinander im Router. |
|
||||||
|
| **Begründung** | Klare Schichten; Auth-Modul nicht mit Tier-Logik vermischen (auch wenn Datei `auth.py` beides enthält). |
|
||||||
|
| **Quelle** | `FEATURE_ENTITLEMENT_DESIGN_PRINCIPLES.md`; Router-Muster |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | Legacy Profil-Flags `ai_enabled`, `export_enabled` parallel zum Feature-System. |
|
||||||
|
|
||||||
|
### 11. Admin-Gate im Frontend und Backend
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | Backend: `require_admin`; Frontend: `RequireAdmin` + `isAdmin` aus Session-Rolle. |
|
||||||
|
| **Begründung** | UX-Navigation + API-Sicherheit (Frontend allein reicht nicht). |
|
||||||
|
| **Quelle** | `RequireAdmin.jsx`; `require_admin()` |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | Einzelne Routen (Workflow-Editor) ohne Frontend-Admin-Gate. |
|
||||||
|
|
||||||
|
### 12. Session-Kontext im Frontend
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | `AuthProvider` hält `{ token, profile_id, role, profile }`; App setzt `setProfileId(session.profile_id)` für API. |
|
||||||
|
| **Begründung** | Single React-Tree für Login-State; Re-Validate via `/auth/me` beim Start. |
|
||||||
|
| **Quelle** | `AuthContext.jsx`; `App.jsx` |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | `ProfileContext` lädt alle Profile — Multi-Profil-UX Rest; Session-Profil ist Kanon. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Nicht übernehmen
|
||||||
|
|
||||||
|
1. **`get_pid(X-Profile-Id)` ohne Session-Bindung** — Client kann fremde `profile_id` senden; IDOR-Risiko. Kanon: immer `session['profile_id']` oder explizite Admin-Impersonation mit Audit.
|
||||||
|
|
||||||
|
2. **Profile-CRUD nur mit `require_auth`** — `/profiles` listet alle Nutzer für jeden Authentifizierten (Kommentar „admin“, kein `require_admin`). Für Familien-Architektur: strikte Admin-Gates.
|
||||||
|
|
||||||
|
3. **Dual-System Profil-Flags vs. Features** — `ai_enabled`, `export_enabled`, `ai_limit_day` in Session-Query neben v9c Feature-Registry.
|
||||||
|
|
||||||
|
4. **localStorage-Key-Inkonsistenz** — `bodytrack_token` vs. `mitai-jinkendo_active_profile` (historischer App-Name).
|
||||||
|
|
||||||
|
5. **Direktes `fetch` ohne `api.js`** — umgeht Token-/Error-Konvention.
|
||||||
|
|
||||||
|
6. **Reset-Token in `sessions`-Tabelle** — `reset_{token}` mischt Session-Typen in einer Tabelle; OK für MVP, für Familie: getrennte Token-Typen/Tabellen.
|
||||||
|
|
||||||
|
7. **Kein JWT/SSO trotz Produktfamilien-Vision** — `CENTRAL_SUBSCRIPTION_SYSTEM.md` beschreibt `auth.jinkendo.de` — Mitai-Implementierung ist **nicht** das Zielbild für Cross-App-SSO.
|
||||||
|
|
||||||
|
8. **Multi-Profil-Haushalt ohne klares Modell** — Legacy Multi-Profile auf einer Instanz vs. 1 Account = 1 Profil; für neue Apps Modell explizit wählen.
|
||||||
|
|
||||||
|
9. **Role als einziges RBAC** — reicht für Admin/User, nicht für feingranulare Permissions.
|
||||||
|
|
||||||
|
10. **Session-Query mit veralteten Profil-Spalten** — `get_session` SELECT enthält Legacy-Felder statt nur Identität + Rolle.
|
||||||
|
|
||||||
|
11. **Fehlende erzwungene E-Mail-Verified-Prüfung** — Registrierung setzt Flag, Login prüft es nicht offensichtlich.
|
||||||
|
|
||||||
|
12. **Debug-Print in Auth-Modul** — `print("[AUTH.PY] Module loaded…")` in Produktionscode.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Abgrenzung zu anderen Serien-Dokumenten
|
||||||
|
|
||||||
|
| Thema | Dokument |
|
||||||
|
|-------|----------|
|
||||||
|
| Tier, Limits, Quotas | [FEATURE_ENTITLEMENT_DESIGN_PRINCIPLES.md](./FEATURE_ENTITLEMENT_DESIGN_PRINCIPLES.md) |
|
||||||
|
| Zentrale SSO/Abo-Vision | [CENTRAL_SUBSCRIPTION_SYSTEM.md](../../technical/CENTRAL_SUBSCRIPTION_SYSTEM.md) |
|
||||||
|
| API-First / Router | `ARCHITECTURE.md` §1 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Modul-Inventar (Ist-Stand)
|
||||||
|
|
||||||
|
```
|
||||||
|
backend/
|
||||||
|
├── auth.py # Session, require_*, Feature-Access (v9c)
|
||||||
|
└── routers/
|
||||||
|
├── auth.py # login, logout, register, verify, reset
|
||||||
|
└── profiles.py # CRUD, get_pid (Legacy)
|
||||||
|
|
||||||
|
frontend/src/
|
||||||
|
├── context/AuthContext.jsx
|
||||||
|
├── context/ProfileContext.jsx
|
||||||
|
├── layouts/RequireAdmin.jsx
|
||||||
|
└── utils/api.js # Token-Injektion
|
||||||
|
|
||||||
|
DB:
|
||||||
|
├── profiles # Identität, Rolle, Hash, Tier, Trial
|
||||||
|
└── sessions # token → profile_id, expires_at
|
||||||
|
```
|
||||||
|
|
||||||
|
**Endpoints (Auswahl):**
|
||||||
|
|
||||||
|
| Endpoint | Auth |
|
||||||
|
|----------|------|
|
||||||
|
| `POST /api/auth/login` | Public + Rate limit |
|
||||||
|
| `POST /api/auth/logout` | Token optional |
|
||||||
|
| `GET /api/auth/me` | require_auth |
|
||||||
|
| `POST /api/auth/register` | Public + Rate limit |
|
||||||
|
| `GET /api/auth/verify/{token}` | Public |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Verwandte Dokumentation
|
||||||
|
|
||||||
|
- Feature-Entitlements: [FEATURE_ENTITLEMENT_DESIGN_PRINCIPLES.md](./FEATURE_ENTITLEMENT_DESIGN_PRINCIPLES.md)
|
||||||
|
- Architektur-Regeln Auth: `CLAUDE.md`, `.claude/rules/ARCHITECTURE.md`
|
||||||
|
- GUI Admin-Guard: `docs/issues/GUI_IA_ADMIN_NAV_2026-04-05.md`
|
||||||
|
- SSO-Vision: [CENTRAL_SUBSCRIPTION_SYSTEM.md](../../technical/CENTRAL_SUBSCRIPTION_SYSTEM.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Geplante Folgedokumente (Serie)
|
||||||
|
|
||||||
|
| # | Modul | Status |
|
||||||
|
|---|-------|--------|
|
||||||
|
| 1 | Prompt Engine | ✅ |
|
||||||
|
| 2 | Data Layer | ✅ |
|
||||||
|
| 3 | Feature & Entitlement | ✅ |
|
||||||
|
| 4 | Registry-/Plugin-Muster | ✅ |
|
||||||
|
| 5 | Auth & Session | ✅ dieses Dokument |
|
||||||
|
| 6 | Universal Import | ✅ `UNIVERSAL_IMPORT_DESIGN_PRINCIPLES.md` |
|
||||||
|
| 7 | Dashboard Widgets | ✅ |
|
||||||
|
| 8 | Navigation / IA | ✅ `NAVIGATION_IA_DESIGN_PRINCIPLES.md` |
|
||||||
|
| 9 | Migration & Deploy | ✅ |
|
||||||
|
|
@ -0,0 +1,363 @@
|
||||||
|
# Dashboard Widgets – Designprinzipien (Extraktion)
|
||||||
|
|
||||||
|
**Status:** Analyse / Arbeitspapier
|
||||||
|
**Stand:** 2026-07-04
|
||||||
|
**Geltungsbereich:** Konfigurierbare Übersicht (Widget-Katalog, Layout, Entitlements, Frontend-Registry) — keine Chart-/Metrik-Berechnung
|
||||||
|
|
||||||
|
**Serie:** Designprinzipien für Produktfamilie · Dokument 7 von n
|
||||||
|
**Vorgänger:** [UNIVERSAL_IMPORT_DESIGN_PRINCIPLES.md](./UNIVERSAL_IMPORT_DESIGN_PRINCIPLES.md)
|
||||||
|
|
||||||
|
**Kernkomponenten:**
|
||||||
|
|
||||||
|
| Bereich | Pfade |
|
||||||
|
|---------|-------|
|
||||||
|
| Katalog (SSoT) | `backend/widget_catalog.py` |
|
||||||
|
| Layout-Schema | `backend/dashboard_layout_schema.py` |
|
||||||
|
| Config-Validierung | `backend/dashboard_widget_config.py` |
|
||||||
|
| Entitlements | `backend/dashboard_widget_entitlements.py`, `widget_feature_requirements_db.py` |
|
||||||
|
| Produkt-Standard | `backend/system_dashboard_product_default.py` |
|
||||||
|
| HTTP | `backend/routers/app_dashboard.py` |
|
||||||
|
| Frontend-Registry | `frontend/src/widgetSystem/dashboardWidgetRegistry.jsx` |
|
||||||
|
| Registrierung | `frontend/src/widgetSystem/registerDashboardWidgets.js` |
|
||||||
|
| Layout-Editor | `frontend/src/pages/DashboardConfigurePage.jsx` |
|
||||||
|
| Fehler-Isolation | `frontend/src/widgetSystem/WidgetErrorBoundary.jsx` |
|
||||||
|
| Leitfaden | `DASHBOARD_WIDGETS_AGENT_GUIDE.md` |
|
||||||
|
| Registry-Meta | [REGISTRY_PLUGIN_DESIGN_PRINCIPLES.md](./REGISTRY_PLUGIN_DESIGN_PRINCIPLES.md) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Modul
|
||||||
|
|
||||||
|
**Dashboard Widgets**
|
||||||
|
|
||||||
|
Erweiterbares System für **konfigurierbare Startübersicht**: Backend-Katalog definiert erlaubte Widget-IDs; Nutzer speichern Reihenfolge, Ein/Aus und optionale `config` pro Profil; Frontend rendert über eine lokale Komponenten-Registry.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Fachliche Verantwortung
|
||||||
|
|
||||||
|
Das Modul übernimmt:
|
||||||
|
|
||||||
|
1. **Widget-Katalog** — IDs, Titel, Beschreibung, optionale Feature-Anforderung (`requires_feature`).
|
||||||
|
2. **Layout-Persistenz** — `profiles.dashboard_layout` (JSON v1: `{ version, widgets[] }`).
|
||||||
|
3. **Validierung** — Erlaubte IDs, keine Duplikate, max. 32 Widgets, mindestens eines aktiv.
|
||||||
|
4. **Pro-Widget-Config** — Whitelist pro Widget-ID; Normalisierung beim Speichern.
|
||||||
|
5. **Standard-Layouts** — Code-Fallback (`DEFAULT_PRODUCT_DASHBOARD_WIDGET_IDS`), Admin-Override (`system_config`), Lab-Template (`DEFAULT_LAB_WIDGET_IDS`).
|
||||||
|
6. **Entitlements** — `allowed` im Katalog; Layout bereinigt bei fehlender Berechtigung.
|
||||||
|
7. **Frontend-Rendering** — Registry mappt Katalog-ID → React-Komponente + Props aus `layoutEntry.config`.
|
||||||
|
8. **Nutzer-Konfigurator** — „Übersicht anpassen“ (Sortieren, Toggle, Config-Editoren).
|
||||||
|
|
||||||
|
Es übernimmt **nicht**:
|
||||||
|
|
||||||
|
- Berechnung von KPIs, Charts, Scores (→ Data Layer + Chart-Endpoints, siehe [DATA_LAYER_DESIGN_PRINCIPLES.md](./DATA_LAYER_DESIGN_PRINCIPLES.md))
|
||||||
|
- Tier-/Subscription-Logik in Widgets (→ Feature System, siehe [FEATURE_ENTITLEMENT_DESIGN_PRINCIPLES.md](./FEATURE_ENTITLEMENT_DESIGN_PRINCIPLES.md))
|
||||||
|
- Prompt-/KI-Ausführung (Widget zeigt nur UI; Pipeline läuft über eigene API)
|
||||||
|
|
||||||
|
### Datenfluss (Happy Path)
|
||||||
|
|
||||||
|
```
|
||||||
|
WIDGET_CATALOG (Backend)
|
||||||
|
→ GET /api/app/widgets/catalog (+ allowed via check_feature_access)
|
||||||
|
→ GET /api/app/dashboard-layout
|
||||||
|
→ coalesce_effective_layout (Profil oder Standard)
|
||||||
|
→ merge_missing_catalog_widgets (neue IDs anhängen)
|
||||||
|
→ apply_entitlements_to_layout_dict
|
||||||
|
→ Frontend: ensureDashboardWidgetsRegistered()
|
||||||
|
→ WidgetRenderer: enabled widgets → mapProps(layoutEntry.config) → Component
|
||||||
|
→ PUT /api/app/dashboard-layout (Pydantic + Entitlements + speichern)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Layout-Eintrag (Struktur)
|
||||||
|
|
||||||
|
| Feld | Bedeutung |
|
||||||
|
|------|-----------|
|
||||||
|
| `id` | Muss in `WIDGET_CATALOG` existieren |
|
||||||
|
| `enabled` | Sichtbar auf der Übersicht |
|
||||||
|
| `config` | Optional; nur für whitelisted Widgets mit Inhalt erlaubt |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Administrierte vs. code-definierte Konfiguration
|
||||||
|
|
||||||
|
| Konfiguration | Speicherort | Wer pflegt? |
|
||||||
|
|---------------|-------------|-------------|
|
||||||
|
| Widget-IDs, Metadaten, Default-Aktivierung | `widget_catalog.py` | Entwickler |
|
||||||
|
| Produkt-Standard-Layout (live) | `system_config.dashboard_product_default` | Admin |
|
||||||
|
| Produkt-Standard (Fallback) | `DEFAULT_PRODUCT_DASHBOARD_WIDGET_IDS` | Entwickler |
|
||||||
|
| Lab-/Editor-Standard | `DEFAULT_LAB_WIDGET_IDS` | Entwickler |
|
||||||
|
| Nutzer-Layout | `profiles.dashboard_layout` | Nutzer |
|
||||||
|
| Feature-Gate (Katalog) | `requires_feature` pro Eintrag | Entwickler |
|
||||||
|
| Feature-Gate (Override) | `widget_feature_requirements` + Marker | Admin |
|
||||||
|
| Config-Schema pro Widget | `dashboard_widget_config.py` | Entwickler |
|
||||||
|
| React-Komponente | `registerDashboardWidgets.js` | Entwickler |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Designprinzipien
|
||||||
|
|
||||||
|
### 1. Backend-Katalog als Single Source of Truth für IDs
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | `WIDGET_CATALOG` ist die einzige autoritative Liste erlaubter Widget-IDs; `ALLOWED_WIDGET_IDS` wird daraus abgeleitet — nicht manuell duplizieren. |
|
||||||
|
| **Begründung** | Layout-Validator, API und Default-Layouts bleiben synchron; unbekannte IDs werden beim PUT abgewiesen. |
|
||||||
|
| **Quelle** | `widget_catalog.py`; Agent-Guide §4 |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | Frontend-Registry ist zweite manuelle Bindung (kein Build-Time-Gate). |
|
||||||
|
|
||||||
|
### 2. Dual Registry: Backend-Kanon + Frontend-Komponentenbindung
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | Jede Katalog-ID braucht einen Eintrag in `registerDashboardWidget({ id, Component, mapProps })`; idempotent via `ensureDashboardWidgetsRegistered()`. |
|
||||||
|
| **Begründung** | React-Komponenten können nicht im Python-Katalog leben; explizite Zuordnung hält Bundle tree-shakeable. |
|
||||||
|
| **Quelle** | `registerDashboardWidgets.js`, `dashboardWidgetRegistry.jsx` |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | Fehlende Registrierung → Laufzeit „Unbekanntes Widget“, kein CI-Fail. |
|
||||||
|
|
||||||
|
### 3. Layout als versioniertes Profil-JSON
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | Nutzer-Layout in `profiles.dashboard_layout`; Schema `version: 1`, Liste `{ id, enabled, config? }`. |
|
||||||
|
| **Begründung** | Pro Profil anpassbar; Reset auf NULL → System-Standard. |
|
||||||
|
| **Quelle** | `DashboardLayoutPayload`, `app_dashboard.py` |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | Nur v1; Schema-Evolution braucht Migrationspfad. |
|
||||||
|
|
||||||
|
### 4. Validierung an der API-Grenze (Pydantic)
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | Jeder GET/PUT-Pfad normalisiert über `DashboardLayoutPayload`: Duplikat-IDs, unbekannte IDs, leeres Layout (kein enabled) → Fehler. |
|
||||||
|
| **Begründung** | Keine korrupten Layouts in der DB; Frontend kann auf gültige Struktur vertrauen. |
|
||||||
|
| **Quelle** | `dashboard_layout_schema.py` |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | Ungültiges gespeichertes Layout → Fallback auf Standard (`coalesce_effective_layout`). |
|
||||||
|
|
||||||
|
### 5. Config nur für explizit whitelisted Widgets
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | `WIDGETS_ALLOWING_CONFIG`: Widgets **ohne** Eintrag dürfen nur leere `config` haben; sonst Validierungsfehler. |
|
||||||
|
| **Begründung** | Verhindert unkontrollierte JSON-Blobs und stille Ignorierung unbekannter Keys. |
|
||||||
|
| **Quelle** | `dashboard_widget_config.py` |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | Pro Widget heterogene Schemas (chart_days vs. KPI-Tiles vs. show_*-Booleans). |
|
||||||
|
|
||||||
|
### 6. Strikte Config-Keys (Whitelist, Normalisierung)
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | Unbekannte Keys in `config` werden abgelehnt; bekannte Keys typgeprüft und normalisiert (z. B. `chart_days` 7–90, KPI max. 9 Kacheln). |
|
||||||
|
| **Begründung** | Vorhersagbares Verhalten; Editor und Backend stimmen überein. |
|
||||||
|
| **Quelle** | `_validate_chart_days_only`, `_validate_kpi_board_config`, History-Viz-Defaults |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | Frontend-Normalizer (`bodyChartDays.js`, `*VizConfig.js`) teils parallel — Abweichungsrisiko. |
|
||||||
|
|
||||||
|
### 7. Config-Größenlimit
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | `MAX_WIDGET_CONFIG_JSON_BYTES` (3072) — keine großen Blobs in Layout-JSON. |
|
||||||
|
| **Begründung** | DB-Spalte und API-Payload bleiben schlank; Config = Präferenzen, nicht Datenspeicher. |
|
||||||
|
| **Quelle** | `dashboard_widget_config.py` |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | — |
|
||||||
|
|
||||||
|
### 8. Katalog-Erweiterung ohne Layout-Reset
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | `merge_missing_catalog_widgets` hängt neue Katalog-IDs ans bestehende Layout an (`enabled: false`). |
|
||||||
|
| **Begründung** | Nutzer müssen nach Deploy nicht resetten; „Übersicht anpassen“ zeigt neue Optionen. |
|
||||||
|
| **Quelle** | `dashboard_layout_schema.py`; Agent-Guide |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | Reihenfolge neuer Widgets immer am Ende. |
|
||||||
|
|
||||||
|
### 9. Mehrere Standard-Layouts (Produkt vs. Lab vs. Admin)
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | **Produkt:** `get_product_default_base_dict` (DB-Override oder `DEFAULT_PRODUCT_DASHBOARD_WIDGET_IDS`). **Lab:** `lab_default_layout_dict` für Editor/Reset. **Nutzer:** eigenes JSON oder NULL. |
|
||||||
|
| **Begründung** | Onboarding-Default getrennt von Entwickler-/Lab-Template; Admin kann Produkt-Standard ohne Deploy ändern. |
|
||||||
|
| **Quelle** | `system_dashboard_product_default.py`, `widget_catalog.py` |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | Feldname `lab_default_layout` historisch irreführend (Servertemplate, nicht nur Lab). |
|
||||||
|
|
||||||
|
### 10. Entitlements zentral, Widgets konsumieren nur `allowed`
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | Sichtbarkeit über `check_feature_access` in `widget_id_allowed`; Katalog liefert `allowed` pro Zeile. Widgets/React duplizieren **keine** Tier-Logik. |
|
||||||
|
| **Begründung** | Eine Wahrheit für „darf angezeigt werden“; spätere Feature-Cluster ohne Widget-Refactor. |
|
||||||
|
| **Quelle** | `dashboard_widget_entitlements.py`; Agent-Guide §0 |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | Inhalts-Endpoints (Charts, KI) brauchen **eigenes** Feature-Gate (Defense in Depth). |
|
||||||
|
|
||||||
|
### 11. Layout-Persistenz bereinigt nicht erlaubte Widgets
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | `apply_entitlements_to_layout_dict`: bei fehlender Berechtigung `enabled: false`; mindestens `welcome` bleibt aktiv. GET und PUT wenden an. |
|
||||||
|
| **Begründung** | Keine „gespeichert aber nie sichtbar“-Zombies; Downgrade/Tier-Wechsel degradieren gracefully. |
|
||||||
|
| **Quelle** | `dashboard_widget_entitlements.py`, `app_dashboard.py` |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | Policy ist deaktivieren, nicht entfernen — IDs bleiben im JSON. |
|
||||||
|
|
||||||
|
### 12. DB-Override für Widget-Feature-Anforderungen
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | Katalog-`requires_feature` ist Default; Admin kann per `dashboard_widget_requirement_custom` + `widget_feature_requirements` überschreiben (AND-Semantik). |
|
||||||
|
| **Begründung** | Runtime-Anpassung ohne Code-Deploy; Marker-Zeile trennt Custom von Fallback. |
|
||||||
|
| **Quelle** | `widget_feature_requirements_db.py`, Migration 041 |
|
||||||
|
| **Tragfähigkeit** | **mittel–hoch** |
|
||||||
|
| **Einschränkung** | Zwei Quellen (Code + DB) — Dokumentation und Admin-UI nötig. |
|
||||||
|
|
||||||
|
### 13. mapProps: Layout-Config → Komponenten-Props
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | Registry-Eintrag mappt `ctx.layoutEntry.config` auf typisierte Props (`chartDays`, `kpiConfig`, `bodyHistoryVizConfig`, …). |
|
||||||
|
| **Begründung** | Widget-Komponenten bleiben layout-agnostisch; Normalisierung an einer Stelle pro ID. |
|
||||||
|
| **Quelle** | `registerDashboardWidgets.js` |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | Teilweise Normalisierung in Widget statt in `mapProps` (inkonsistent, aber dokumentiert). |
|
||||||
|
|
||||||
|
### 14. Refresh-Koordination über Context
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | `refreshTick` + `requestRefresh()` im Render-Context; Widgets laden Daten bei Tick-Änderung neu; Aktionen (z. B. Schnelleingabe) rufen `requestRefresh`. |
|
||||||
|
| **Begründung** | Kein globales State-Monster; gezielte Invalidierung nach Capture. |
|
||||||
|
| **Quelle** | `dashboardWidgetRegistry.jsx`, Widget-Implementierungen |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | Kein feingranulares Cache pro Widget. |
|
||||||
|
|
||||||
|
### 15. Fehler-Isolation pro Widget
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | `WidgetErrorBoundary` um jede Instanz — Render-Fehler crashen nicht die ganze Übersicht. |
|
||||||
|
| **Begründung** | Robuste PWA; ein defektes Chart blockiert nicht Gewicht-Eingabe. |
|
||||||
|
| **Quelle** | `WidgetErrorBoundary.jsx` |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | Kein automatisches Retry/Reporting. |
|
||||||
|
|
||||||
|
### 16. Konfigurator filtert nach `allowed`
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | `DashboardConfigurePage` blendet Widgets mit `allowed === false` aus der bearbeitbaren Liste aus. |
|
||||||
|
| **Begründung** | Nutzer sehen keine Optionen, die sie nicht nutzen dürfen (Agent-Guide A2). |
|
||||||
|
| **Quelle** | `DashboardConfigurePage.jsx` |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | Bereits gespeicherte disabled Einträge können im JSON verbleiben. |
|
||||||
|
|
||||||
|
### 17. Widgets konsumieren Data Layer, duplizieren keine Logik
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | Chart-/KPI-Widgets rufen Chart-Endpoints bzw. API-Fassaden auf; Berechnungen leben in `data_layer/`, nicht in Widget-JS. |
|
||||||
|
| **Begründung** | Gleiche Zahlen wie Verlauf, KI-Platzhalter und Export. |
|
||||||
|
| **Quelle** | Layer-2b `*_history_viz`-Widgets; [DATA_LAYER_DESIGN_PRINCIPLES.md](./DATA_LAYER_DESIGN_PRINCIPLES.md) |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | Legacy-Widgets unter `dashboard-widgets-legacy/` teils ältere Fetch-Pfade. |
|
||||||
|
|
||||||
|
### 18. Dedizierte Config-Editoren für komplexe Widgets
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | Einfache `chart_days`: Set `CHART_DAYS_WIDGET_IDS` im Layout-Editor; komplexe Config: eigene Editor-Komponenten (`KpiBoardConfigEditor`, `*VizConfigEditor`). |
|
||||||
|
| **Begründung** | UX skaliert mit Config-Komplexität; Backend-Schema und Editor bleiben parallel pflegbar. |
|
||||||
|
| **Quelle** | `widgetSystem/*ConfigEditor.jsx`, Agent-Guide §3.4 |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | Jedes neue komplexe Widget = Editor + Validator + Tests. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Nicht übernehmen
|
||||||
|
|
||||||
|
1. **Tier-Logik in React-Widgets** — nur `allowed` aus API; keine hardcodierten Plan-Namen.
|
||||||
|
|
||||||
|
2. **`ALLOWED_WIDGET_IDS` manuell pflegen** — immer aus Katalog ableiten.
|
||||||
|
|
||||||
|
3. **Config ohne Backend-Whitelist** — stille Ignorierung unbekannter Keys in Widgets.
|
||||||
|
|
||||||
|
4. **Nur UI-Gating ohne API-Absicherung** — Chart-/KI-/Export-Endpoints weiterhin `check_feature_access` (403).
|
||||||
|
|
||||||
|
5. **Frontend-Registry vergessen** — Katalog-Eintrag ohne `registerDashboardWidget` → Laufzeitfehler statt Build-Fail.
|
||||||
|
|
||||||
|
6. **Große Daten in `config`** — Layout ist Präferenzspeicher, kein Blob-Store (>3072 Bytes).
|
||||||
|
|
||||||
|
7. **Doppelte Widget-IDs im Layout** — Validator verbietet; Editor muss dasselbe erzwingen.
|
||||||
|
|
||||||
|
8. **Neue Katalog-IDs ohne `merge_missing_catalog_widgets`-Pfad** — Nutzer-Layouts veralten unsichtbar.
|
||||||
|
|
||||||
|
9. **Kompletter Katalog nur in DB** — schwer testbar; Code-Katalog + optionale Feature-Overrides ist das Muster.
|
||||||
|
|
||||||
|
10. **Evidence-Pflicht à la Placeholder-Registry** — 22 Metadatenfelder pro Widget wären Overkill; Tiefe an Risiko anpassen ([REGISTRY_PLUGIN_DESIGN_PRINCIPLES.md](./REGISTRY_PLUGIN_DESIGN_PRINCIPLES.md)).
|
||||||
|
|
||||||
|
11. **Ein Default für alles** — Produkt-Onboarding, Lab-Template und Admin-Override haben unterschiedliche Zwecke; nicht vermischen.
|
||||||
|
|
||||||
|
12. **Fehlender Cross-Check Backend ↔ Frontend IDs** — empfohener Test/Gate fehlt im Ist-Stand; nicht als „optional“ ignorieren.
|
||||||
|
|
||||||
|
13. **Berechnungslogik im Widget** — KPIs/Scores gehören in Data Layer, nicht in `useEffect`-Mathe.
|
||||||
|
|
||||||
|
14. **Entitlements beim Speichern ablehnen statt deaktivieren** — Mitai wählt deaktivieren; Policy bewusst festlegen und dokumentieren.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Modul-Inventar (Ist-Stand)
|
||||||
|
|
||||||
|
```
|
||||||
|
backend/
|
||||||
|
├── widget_catalog.py # WIDGET_CATALOG, DEFAULT_*_IDS
|
||||||
|
├── dashboard_layout_schema.py # Pydantic, merge_missing, defaults
|
||||||
|
├── dashboard_widget_config.py # WIDGETS_ALLOWING_CONFIG, Validatoren
|
||||||
|
├── dashboard_widget_entitlements.py # allowed, layout cleanup
|
||||||
|
├── widget_feature_requirements_db.py # Admin-Override
|
||||||
|
├── system_dashboard_product_default.py
|
||||||
|
└── routers/app_dashboard.py
|
||||||
|
|
||||||
|
frontend/src/
|
||||||
|
├── widgetSystem/
|
||||||
|
│ ├── dashboardWidgetRegistry.jsx
|
||||||
|
│ ├── registerDashboardWidgets.js
|
||||||
|
│ ├── layoutEditor.js
|
||||||
|
│ ├── bodyChartDays.js, *VizConfig.js
|
||||||
|
│ └── *ConfigEditor.jsx
|
||||||
|
├── components/dashboard-widgets/ # Produkt-Widgets
|
||||||
|
├── components/dashboard-widgets-legacy/ # ältere Kern-Widgets
|
||||||
|
└── pages/DashboardConfigurePage.jsx
|
||||||
|
|
||||||
|
DB:
|
||||||
|
├── profiles.dashboard_layout
|
||||||
|
├── system_config.dashboard_product_default
|
||||||
|
├── dashboard_widget_requirement_custom
|
||||||
|
└── widget_feature_requirements
|
||||||
|
```
|
||||||
|
|
||||||
|
**Katalog-Umfang:** ~24 Widget-IDs (Stand `widget_catalog.py`); ~13 mit konfigurierbarer `config`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Verwandte Dokumentation
|
||||||
|
|
||||||
|
- Agent-Guide (normativ): [DASHBOARD_WIDGETS_AGENT_GUIDE.md](../../technical/DASHBOARD_WIDGETS_AGENT_GUIDE.md)
|
||||||
|
- Registry-Meta: [REGISTRY_PLUGIN_DESIGN_PRINCIPLES.md](./REGISTRY_PLUGIN_DESIGN_PRINCIPLES.md)
|
||||||
|
- Feature-Gates: [FEATURE_ENTITLEMENT_DESIGN_PRINCIPLES.md](./FEATURE_ENTITLEMENT_DESIGN_PRINCIPLES.md)
|
||||||
|
- Datenberechnung: [DATA_LAYER_DESIGN_PRINCIPLES.md](./DATA_LAYER_DESIGN_PRINCIPLES.md)
|
||||||
|
- Architektur §9: `.claude/rules/ARCHITECTURE.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Geplante Folgedokumente (Serie)
|
||||||
|
|
||||||
|
| # | Modul | Status |
|
||||||
|
|---|-------|--------|
|
||||||
|
| 1–6 | … | ✅ |
|
||||||
|
| 7 | Dashboard Widgets | ✅ dieses Dokument |
|
||||||
|
| 8 | Navigation / IA | ✅ |
|
||||||
|
| 9 | Migration & Deploy | ✅ |
|
||||||
|
|
@ -0,0 +1,359 @@
|
||||||
|
# Data Layer – Designprinzipien (Extraktion)
|
||||||
|
|
||||||
|
**Status:** Analyse / Arbeitspapier
|
||||||
|
**Stand:** 2026-07-04
|
||||||
|
**Geltungsbereich:** Multi-Layer Data Architecture (Phase 0c, Issue #53) — keine Mitai-Gesamtarchitektur, keine konkrete Gesundheits-/Ernährungsfachlogik als Produktinhalt
|
||||||
|
|
||||||
|
**Serie:** Designprinzipien für Produktfamilie · Dokument 2 von n
|
||||||
|
**Vorgänger:** [PROMPT_ENGINE_DESIGN_PRINCIPLES.md](./PROMPT_ENGINE_DESIGN_PRINCIPLES.md)
|
||||||
|
|
||||||
|
**Kernkomponenten:**
|
||||||
|
|
||||||
|
| Bereich | Pfade |
|
||||||
|
|---------|-------|
|
||||||
|
| Metriken (Layer 1) | `backend/data_layer/*_metrics.py`, `scores.py`, `correlations.py` |
|
||||||
|
| Utilities | `backend/data_layer/utils.py` |
|
||||||
|
| Visualisierung (Layer 2b) | `*_chart_payloads.py`, `*_viz.py` |
|
||||||
|
| KI-Formatierung (Layer 2a-Hilfe) | `prompt_output_compact.py` |
|
||||||
|
| Persistenz-Orchestrierung | `activity_persistence_orchestrator.py`, `activity_session_metrics.py` |
|
||||||
|
| Konsumenten | `routers/charts.py`, `placeholder_resolver.py`, `routers/exportdata.py` |
|
||||||
|
| Leitfäden | `DATA_LAYER_EXTENSION_GUIDE.md`, `docs/issues/issue-53-phase-0c-multi-layer-architecture.md` |
|
||||||
|
| Architektur-Regel Import-Grenze | `.claude/rules/ARCHITECTURE.md` §8 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Modul
|
||||||
|
|
||||||
|
**Data Layer** (Phase 0c Multi-Layer Architecture, Issue #53)
|
||||||
|
|
||||||
|
Zentrale Schicht für **Datenabruf, Berechnung und strukturierte Aufbereitung** — ohne UI-Formatierung, ohne Prompt-Texte, ohne Chart.js-spezifische Ausgabe in den Kern-Metrik-Modulen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Fachliche Verantwortung
|
||||||
|
|
||||||
|
Der Data Layer ist die **Single Source of Truth für alle abgeleiteten Messwerte und Metriken**. Er übernimmt:
|
||||||
|
|
||||||
|
1. **Datenabruf** — Lesen aus PostgreSQL (profile-scoped), optional mit Quality-Filter.
|
||||||
|
2. **Berechnung** — Trends, Scores, Korrelationen, Aggregationen, Projektionen.
|
||||||
|
3. **Strukturierte Rückgabe** — Dicts/Listen mit numerischen Werten, Datumsfeldern, Metadaten (`confidence`, `data_points`).
|
||||||
|
4. **Konsumenten-Bereitstellung** — Charts (Layer 2b), KI-Platzhalter (Layer 2a via Resolver), Export, Router-Anreicherung.
|
||||||
|
|
||||||
|
Er übernimmt **nicht**:
|
||||||
|
|
||||||
|
- CSV-Parsing und Feld-Mapping (Import-Schicht)
|
||||||
|
- Prompt-Template-Auflösung (Prompt Engine)
|
||||||
|
- React-Rendering oder Frontend-Berechnungen
|
||||||
|
- Autorisierung / Feature-Limits (Auth-Schicht)
|
||||||
|
|
||||||
|
### Schichtenmodell (Multi-Layer)
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────┐
|
||||||
|
│ Layer 0: Persistenz (PostgreSQL) │
|
||||||
|
│ weight_log, nutrition_log, activity_log, sleep_log, … │
|
||||||
|
└──────────────────────────┬──────────────────────────────┘
|
||||||
|
│
|
||||||
|
┌──────────────────────────▼──────────────────────────────┐
|
||||||
|
│ Layer 1: DATA LAYER (Metriken) │
|
||||||
|
│ Strukturierte Daten · confidence · data_points │
|
||||||
|
│ KEINE formatierten Strings · KEINE Chart.js-Objekte │
|
||||||
|
└──────────────┬───────────────────────┬──────────────────┘
|
||||||
|
│ │
|
||||||
|
▼ ▼
|
||||||
|
┌──────────────────────────┐ ┌────────────────────────────┐
|
||||||
|
│ Layer 2a: KI / Prompts │ │ Layer 2b: Visualisierung │
|
||||||
|
│ placeholder_resolver │ │ *_chart_payloads, *_viz │
|
||||||
|
│ prompt_output_compact │ │ routers/charts.py │
|
||||||
|
└──────────────────────────┘ └────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Administrierte vs. code-definierte Konfiguration
|
||||||
|
|
||||||
|
| Was | Wo | Administrierbar? |
|
||||||
|
|-----|-----|------------------|
|
||||||
|
| Berechnungslogik (Formeln, Fenster) | `data_layer/*.py` | ❌ Code + Review |
|
||||||
|
| Confidence-Schwellen | `data_layer/utils.py` | ❌ Code |
|
||||||
|
| Goal Mode / Focus Weights | DB (`profiles`, `user_focus_area_weights`) | ✅ Nutzer/Admin |
|
||||||
|
| Quality Filter (Profil) | DB (`profiles`) | ✅ Admin |
|
||||||
|
| Chart-Zeitfenster | Query-Parameter an API | ✅ Request |
|
||||||
|
| Referenzwerte (persönlich) | DB + `reference_values.py` | ✅ Nutzer |
|
||||||
|
| EAV Session Metrics | DB (`training_*_parameter`) | ✅ Admin |
|
||||||
|
|
||||||
|
**Bewusst nicht hardcodiert in Routern:** Metrik-Berechnungen — Router delegieren an Data Layer.
|
||||||
|
|
||||||
|
**Hardcodiert (Code):** Domänen-Module, Confidence-Regeln, Schwellen pro Metrik-Typ, TDEE-Fallback-Logik, Chart-Payload-Struktur.
|
||||||
|
|
||||||
|
### Trennung: Metriken · Chart-Payloads · KI-Formatierung · Persistenz
|
||||||
|
|
||||||
|
| Schicht | Module | Verantwortung |
|
||||||
|
|---------|--------|---------------|
|
||||||
|
| **Metriken** | `body_metrics.py`, `nutrition_metrics.py`, … | Reine Berechnung, strukturierte Dicts |
|
||||||
|
| **Chart-Payloads** | `nutrition_chart_payloads.py`, `correlation_chart_payloads.py`, … | Chart.js-kompatible `{ labels, datasets, metadata }` aus Layer-1-Daten |
|
||||||
|
| **Viz-Bundles** | `body_viz.py`, `fitness_viz.py`, … | Zusammengesetzte Dashboard-/History-Pakete für Frontend |
|
||||||
|
| **KI-Kompaktierung** | `prompt_output_compact.py` | Token-sparende Zahlen/JSON für Platzhalter |
|
||||||
|
| **Interpretation** | `*_interpretation.py`, `vital_signs_assessment.py` | Textliche Einordnung (WHO-Klassen etc.) — Grenze zu Layer 2a |
|
||||||
|
| **Persistenz-Orchestrator** | `activity_persistence_orchestrator.py` | Schreibpfade REST/CSV → DB + Nebenwirkungen (EAV, Eval) |
|
||||||
|
|
||||||
|
### Konsumenten (wer ruft den Data Layer auf?)
|
||||||
|
|
||||||
|
| Konsument | Muster |
|
||||||
|
|-----------|--------|
|
||||||
|
| `routers/charts.py` | Layer-1-Funktion + Chart-Payload-Builder |
|
||||||
|
| `placeholder_resolver.py` | Layer-1 → Formatierung/JSON für `{{placeholders}}` |
|
||||||
|
| `routers/exportdata.py` | `enrich_sessions_with_metrics`, `serialize_dates` |
|
||||||
|
| `routers/activity.py`, `csv_import.py` | `activity_persistence_orchestrator` (Schreiben) |
|
||||||
|
| `prompt_executor.execute_prompt_with_data` | ⚠️ teils Roh-SQL parallel zum Data Layer (Legacy) |
|
||||||
|
|
||||||
|
### Rollen
|
||||||
|
|
||||||
|
Der Data Layer hat **keine eigene Admin-UI**. Konfiguration erfolgt indirekt:
|
||||||
|
|
||||||
|
- **Admin:** Training-Parameter, Attributprofile, Referenzwert-Typen, Quality-Filter
|
||||||
|
- **Nutzer:** Profildaten, Referenzwerte, Focus-Area-Gewichte (beeinflussen Scores)
|
||||||
|
- **Entwickler:** Neue Funktionen in `data_layer/` nach Extension Guide
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Designprinzipien
|
||||||
|
|
||||||
|
### 1. Single Source of Truth für Berechnungen
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | Jede Metrik wird **einmal** in `data_layer/` berechnet; Charts, KI und Export konsumieren dieselbe Funktion. |
|
||||||
|
| **Begründung** | Verhindert divergierende Zahlen zwischen Dashboard, Analyse und KI-Ausgabe. |
|
||||||
|
| **Quelle** | Issue #53 Executive Summary; `nutrition_chart_payloads.py` Kommentar „identisch zu GET /api/charts/energy-balance“ |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | Nicht alle Pfade migriert (`insights._prepare_template_vars`, `execute_prompt_with_data` Roh-SQL). |
|
||||||
|
|
||||||
|
### 2. Layer 1 liefert strukturierte Daten, keine formatierten Strings
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | Kern-Metrik-Funktionen geben Dicts mit `float`/`int`/`date` zurück — **keine** Strings mit Einheiten („86,1 kg“). |
|
||||||
|
| **Begründung** | Formatierung ist konsumentenspezifisch (DE-Locale, Chart-Achsen, KI-Token). |
|
||||||
|
| **Quelle** | `data_layer/__init__.py` Docstring: „NO FORMATTING. NO STRINGS WITH UNITS.“ |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | `placeholder_resolver` und `*_interpretation` Module formatieren teils direkt — Grenze Layer 1/2a nicht überall scharf. |
|
||||||
|
|
||||||
|
### 3. Pflicht-Metadaten: confidence + data_points
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | Jede Metrik-Funktion liefert mindestens `confidence` (`high`\|`medium`\|`low`\|`insufficient`) und `data_points`. |
|
||||||
|
| **Begründung** | UI/KI können Datenqualität kommunizieren; Debugging und Monitoring vereinfacht. |
|
||||||
|
| **Quelle** | `DATA_LAYER_EXTENSION_GUIDE.md` § Pflicht-Felder; `calculate_confidence()` in `utils.py` |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | Nicht runtime-validiert; Disziplin per Code-Review. |
|
||||||
|
|
||||||
|
### 4. Confidence nach Metrik-Typ und Zeitfenster
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | Schwellen unterscheiden `general`, `correlation`, `trend` und Fensterlänge (7d / 28d / 90d). |
|
||||||
|
| **Begründung** | Korrelationen brauchen mehr Paare; Trends messen Abdeckung (% der Tage). |
|
||||||
|
| **Quelle** | `data_layer/utils.py` → `calculate_confidence()` |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | Schwellen global hardcodiert, nicht pro Metrik konfigurierbar. |
|
||||||
|
|
||||||
|
### 5. Domänen-Module statt Monolith
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | Ein Python-Modul pro fachlichem Bereich (`body_metrics`, `nutrition_metrics`, …), max. ~500 Zeilen, dann Split. |
|
||||||
|
| **Begründung** | Wartbarkeit, klare Ownership, parallele Entwicklung. |
|
||||||
|
| **Quelle** | `DATA_LAYER_EXTENSION_GUIDE.md` § Modul-Struktur |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | Einige Module deutlich >500 Zeilen (Phase-0c-Wachstum). |
|
||||||
|
|
||||||
|
### 6. Layer 2b: Chart-Payloads als Adapter
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | Chart.js-Strukturen leben in dedizierten `*_chart_payloads.py` / `*_viz.py`, nicht in Metrik-Modulen. |
|
||||||
|
| **Begründung** | Gleiche Metrik, verschiedene Visualisierungen; API-Endpoints bleiben dünn. |
|
||||||
|
| **Quelle** | `nutrition_chart_payloads.py`; `routers/charts.py` Imports |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | Teilweise noch SQL-Duplikation in Payload-Buildern neben Layer-1-Aufruf. |
|
||||||
|
|
||||||
|
### 7. Layer 2a-Hilfe: KI-spezifische Kompaktierung getrennt
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | Token-Reduktion für LLM-Kontext (`compact_float_for_prompt`, `compact_json_payload_for_prompts`) ist eigenes Modul, nicht in Metrik-Kern. |
|
||||||
|
| **Begründung** | KI hat andere Anforderungen als Charts (Präzision vs. Token-Kosten). |
|
||||||
|
| **Quelle** | `prompt_output_compact.py`; Tests in `tests/test_prompt_output_compact.py` |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | Nur für KI-Pfad; Charts nutzen eigene Rundung. |
|
||||||
|
|
||||||
|
### 8. Import-Grenze: Ingest vs. Interpretation
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | CSV-Import macht Mapping + Typkonvertierung + Duplikatlogik — **keine** fachliche Auswertung beim Insert. |
|
||||||
|
| **Begründung** | Semantik gehört in Layer 1+, sonst versteckte Business-Logik in Import-Adaptern. |
|
||||||
|
| **Quelle** | `ARCHITECTURE.md` §8; Issue #53 |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | Legacy-Adapter (Apple-Schlaf-Aggregat, dedizierte Import-Endpoints) noch aktiv. |
|
||||||
|
|
||||||
|
### 9. Persistenz-Orchestrator für Schreibpfade
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | Alle Schreibwege eines Domänenobjekts (REST, CSV, Legacy) laufen durch **einen** Orchestrator mit Nebenwirkungen (EAV, Evaluation). |
|
||||||
|
| **Begründung** | Konsistente Duplikat-Erkennung, Registry-Felder, keine divergierenden Insert-Logiken. |
|
||||||
|
| **Quelle** | `activity_persistence_orchestrator.py` |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | Bisher vor allem Aktivität; andere Domänen noch direkt in Routern. |
|
||||||
|
|
||||||
|
### 10. Registry als Feld-Kanon (Activity)
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | Erlaubte persistierbare Felder für CSV/REST leiten sich aus `module_registry` ab, nicht aus Router-Hardcoding. |
|
||||||
|
| **Begründung** | Single Source of Truth für Import-Mappings und DB-Updates. |
|
||||||
|
| **Quelle** | `activity_data_canon.py`, `activity_persistence_orchestrator.py` |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | Nur Activity vollständig; andere Module noch klassische Spalten-CRUD. |
|
||||||
|
|
||||||
|
### 11. EAV-Anreicherung als Read-Layer
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | Session-Metriken (EAV) werden beim **Lesen** angereichert (`enrich_sessions_with_metrics`), nicht pro Consumer dupliziert. |
|
||||||
|
| **Begründung** | Ein Merge-Kanon für Liste, Detail, Export, Platzhalter. |
|
||||||
|
| **Quelle** | `activity_session_metrics.py`; `ACTIVITY_SESSION_METRICS_EAV_AGENT_GUIDE.md` |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | Domänenspezifisch (Training); Muster übertragbar. |
|
||||||
|
|
||||||
|
### 12. Scores als composable Layer
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | Composite Scores (`scores.py`) kombinieren Domänen-Metriken mit nutzer-spezifischen Focus Weights — keine Score-Logik in Routern. |
|
||||||
|
| **Begründung** | Goal-Mode-/Focus-abhängige Gewichtung zentral, für KI und Dashboard gleich. |
|
||||||
|
| **Quelle** | `data_layer/scores.py`; Phase-0b-Fokus-System |
|
||||||
|
| **Tragfähigkeit** | **mittel–hoch** |
|
||||||
|
| **Einschränkung** | Eng an Mitai-Zielsystem gekoppelt; Muster „gewichtete Composite Scores“ ist generisch. |
|
||||||
|
|
||||||
|
### 13. API-First: Router delegieren, rechnen nicht
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | `routers/charts.py` und ähnliche Endpoints rufen Data-Layer-Funktionen auf und mappen auf HTTP — keine Trend-Berechnung im Router. |
|
||||||
|
| **Begründung** | Testbarkeit; Frontend ohne Business-Logik. |
|
||||||
|
| **Quelle** | `ARCHITECTURE.md` §1.2 API-First |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | `charts.py` ist groß (2246+ Zeilen) — viel Adapter-Code, aber Berechnung delegiert. |
|
||||||
|
|
||||||
|
### 14. serialize_dates / safe_float als Querschnitt
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | JSON/API-Serialisierung (Dates, Decimal) zentral in `utils.py`, nicht pro Modul neu erfunden. |
|
||||||
|
| **Begründung** | PostgreSQL-Typen (DATE, DECIMAL) konsistent für API und Export. |
|
||||||
|
| **Quelle** | `data_layer/utils.py` |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | — |
|
||||||
|
|
||||||
|
### 15. Extension Guide als verbindlicher Entwicklungsvertrag
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | Neue Metriken folgen Template (Retrieve → Confidence → Early Return → Calculate → Return) und werden in `__init__.py` exportiert. |
|
||||||
|
| **Begründung** | Einheitliche Struktur für 97+ Funktionen und wachsende Codebase. |
|
||||||
|
| **Quelle** | `DATA_LAYER_EXTENSION_GUIDE.md` |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | Guide und Ist-Code divergieren teils (Modulgröße, `goals.py` noch nicht in `__init__`). |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Nicht übernehmen
|
||||||
|
|
||||||
|
Muster, die sich nicht bewährt haben oder zu produktspezifisch sind:
|
||||||
|
|
||||||
|
1. **Berechnungslogik in `placeholder_resolver.py`** — Phase-0b-Legacy; Resolver soll nur formatieren/aggregieren, nicht rechnen.
|
||||||
|
|
||||||
|
2. **Paralleler Roh-SQL-Kontext in `prompt_executor.execute_prompt_with_data`** — lädt Modul-Rohdaten per SQL, obwohl Layer 1 existiert; zweite Wahrheit.
|
||||||
|
|
||||||
|
3. **Legacy Insights-Pfad (`insights._prepare_template_vars`)** — eigene Variablen-Vorbereitung ohne Data Layer.
|
||||||
|
|
||||||
|
4. **Import mit fachlicher Interpretation** — Apple-Schlaf-Aggregat und ähnliche Adapter verstecken Semantik im Ingest (Gitea #69).
|
||||||
|
|
||||||
|
5. **Monolithische Router mit Inline-Berechnung** — vor Phase 0c; gelegentlich noch Reste in nicht migrierten Pfaden.
|
||||||
|
|
||||||
|
6. **Interpretation vermischt mit Layer 1** — `*_interpretation.py` liefert teils fertige Texte; für Familien-Architektur klar als Layer 2a/2b markieren oder auslagern.
|
||||||
|
|
||||||
|
7. **SQL-Duplikation in Chart-Payloads** — manche Payload-Builder führen eigene Queries statt ausschließlich Layer-1-Ergebnisse zu visualisieren.
|
||||||
|
|
||||||
|
8. **Hardcodierte Confidence global** — funktioniert, aber nicht pro Metrik/Domäne konfigurierbar; Skalierung in Multi-Tenant-Produktfamilie prüfen.
|
||||||
|
|
||||||
|
9. **Domänen-Module als Produktinhalt** — `body_metrics`, TDEE, WHR etc. sind Mitai-spezifisch; **Schichtenmodell** übernehmen, **Formeln** nicht blind kopieren.
|
||||||
|
|
||||||
|
10. **Fehlende runtime-Validierung des Return-Schemas** — `confidence`/`data_points` per Konvention, nicht per TypedDict/Pydantic erzwungen.
|
||||||
|
|
||||||
|
11. **Uneinheitliche Schreib-Orchestrierung** — nur Activity hat `persistence_orchestrator`; andere Domänen noch fragmentiert.
|
||||||
|
|
||||||
|
12. **Riesige Einzeldateien** — einige Metrik-Module >>500 Zeilen widersprechen eigenem Extension Guide.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Modul-Inventar (Ist-Stand)
|
||||||
|
|
||||||
|
```
|
||||||
|
backend/data_layer/
|
||||||
|
├── Kern-Metriken (Layer 1)
|
||||||
|
│ ├── body_metrics.py
|
||||||
|
│ ├── nutrition_metrics.py
|
||||||
|
│ ├── activity_metrics.py
|
||||||
|
│ ├── recovery_metrics.py
|
||||||
|
│ ├── health_metrics.py
|
||||||
|
│ ├── scores.py
|
||||||
|
│ └── correlations.py
|
||||||
|
├── Visualisierung (Layer 2b)
|
||||||
|
│ ├── *_chart_payloads.py (nutrition, recovery, correlation)
|
||||||
|
│ └── *_viz.py (body, nutrition, fitness, recovery, history_overview)
|
||||||
|
├── KI / Format (Layer 2a-Nähe)
|
||||||
|
│ ├── prompt_output_compact.py
|
||||||
|
│ └── *_interpretation.py
|
||||||
|
├── Persistenz / EAV
|
||||||
|
│ ├── activity_persistence_orchestrator.py
|
||||||
|
│ ├── activity_session_metrics.py
|
||||||
|
│ └── activity_data_canon.py
|
||||||
|
├── Querschnitt
|
||||||
|
│ ├── utils.py
|
||||||
|
│ ├── reference_values.py
|
||||||
|
│ └── nutrition_body_merge.py
|
||||||
|
└── __init__.py (Exports)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Konsumenten-Endpoints (Auswahl):** 20+ Chart-Endpoints in `routers/charts.py` (E1–E5, A1–A8, R1–R5, C1–C4).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Verwandte Dokumentation
|
||||||
|
|
||||||
|
- Issue #53 Abschluss: [issue-53-phase-0c-multi-layer-architecture.md](../../../../docs/issues/issue-53-phase-0c-multi-layer-architecture.md)
|
||||||
|
- Extension Guide: [DATA_LAYER_EXTENSION_GUIDE.md](../../technical/DATA_LAYER_EXTENSION_GUIDE.md)
|
||||||
|
- Fachliche Datenarchitektur: [DATA_ARCHITECTURE.md](../../functional/DATA_ARCHITECTURE.md)
|
||||||
|
- Import-Grenze: [ARCHITECTURE.md](../../../rules/ARCHITECTURE.md) §8
|
||||||
|
- Platzhalter-Anbindung: [PLACEHOLDER_REGISTRY_FRAMEWORK.md](../../technical/PLACEHOLDER_REGISTRY_FRAMEWORK.md)
|
||||||
|
- Prompt Engine (Konsument Layer 2a): [PROMPT_ENGINE_DESIGN_PRINCIPLES.md](./PROMPT_ENGINE_DESIGN_PRINCIPLES.md)
|
||||||
|
- Feature & Entitlement: [FEATURE_ENTITLEMENT_DESIGN_PRINCIPLES.md](./FEATURE_ENTITLEMENT_DESIGN_PRINCIPLES.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Geplante Folgedokumente (Serie)
|
||||||
|
|
||||||
|
| # | Modul | Datei (geplant) |
|
||||||
|
|---|-------|-----------------|
|
||||||
|
| 1 | Prompt Engine | ✅ `PROMPT_ENGINE_DESIGN_PRINCIPLES.md` |
|
||||||
|
| 2 | Data Layer | ✅ dieses Dokument |
|
||||||
|
| 3 | Feature & Entitlement System | ✅ `FEATURE_ENTITLEMENT_DESIGN_PRINCIPLES.md` |
|
||||||
|
| 4 | Registry-/Plugin-Muster | ✅ `REGISTRY_PLUGIN_DESIGN_PRINCIPLES.md` |
|
||||||
|
| 5 | Auth & Session | ✅ `AUTH_SESSION_DESIGN_PRINCIPLES.md` |
|
||||||
|
| 6 | Universal Import | ✅ `UNIVERSAL_IMPORT_DESIGN_PRINCIPLES.md` |
|
||||||
|
| 7 | Dashboard Widgets | ✅ `DASHBOARD_WIDGETS_DESIGN_PRINCIPLES.md` |
|
||||||
|
| 8 | Navigation / IA | ✅ `NAVIGATION_IA_DESIGN_PRINCIPLES.md` |
|
||||||
|
| 9 | Migration & Deploy | ✅ `MIGRATION_DEPLOY_DESIGN_PRINCIPLES.md` |
|
||||||
|
|
@ -0,0 +1,370 @@
|
||||||
|
# Feature & Entitlement System – Designprinzipien (Extraktion)
|
||||||
|
|
||||||
|
**Status:** Analyse / Arbeitspapier
|
||||||
|
**Stand:** 2026-07-04
|
||||||
|
**Geltungsbereich:** Membership-, Tier- und Feature-Limit-System (v9c) — keine Mitai-Domänenlogik, kein zentrales SSO/Stripe (Vision)
|
||||||
|
|
||||||
|
**Serie:** Designprinzipien für Produktfamilie · Dokument 3 von n
|
||||||
|
**Vorgänger:** [DATA_LAYER_DESIGN_PRINCIPLES.md](./DATA_LAYER_DESIGN_PRINCIPLES.md)
|
||||||
|
|
||||||
|
**Kernkomponenten:**
|
||||||
|
|
||||||
|
| Bereich | Pfade |
|
||||||
|
|---------|-------|
|
||||||
|
| Entitlement-Auflösung | `backend/auth.py` (`get_effective_tier`, `check_feature_access`, `increment_feature_usage`) |
|
||||||
|
| Monitoring | `backend/feature_logger.py` |
|
||||||
|
| Nutzer-API | `backend/routers/subscription.py`, `backend/routers/features.py` |
|
||||||
|
| Admin | `routers/tiers_mgmt.py`, `tier_limits.py`, `coupons.py`, `access_grants.py`, `user_restrictions.py` |
|
||||||
|
| Widget-Gating | `backend/dashboard_widget_entitlements.py`, `widget_feature_requirements_db.py` |
|
||||||
|
| Frontend | `UsageBadge.jsx`, Feature-Usage in Seiten (z. B. `Analysis.jsx`, `WeightPage`) |
|
||||||
|
| Doku | `MEMBERSHIP_SYSTEM.md`, `FEATURE_ENFORCEMENT.md`, `CENTRAL_SUBSCRIPTION_SYSTEM.md` (Vision) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Modul
|
||||||
|
|
||||||
|
**Feature & Entitlement System** (Membership v9c)
|
||||||
|
|
||||||
|
Zentrale Schicht für **„Darf dieser Nutzer diese Funktion wie oft nutzen?“** — unabhängig von Auth (Identität) und unabhängig von fachlicher Business-Logik in Routern.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Fachliche Verantwortung
|
||||||
|
|
||||||
|
Das System übernimmt:
|
||||||
|
|
||||||
|
1. **Feature-Registry** — Deklarative Liste aller limitierbaren Produktfunktionen mit Metadaten.
|
||||||
|
2. **Tier-Auflösung** — Effektiver Tarif eines Profils (Basis-Tier + zeitliche Grants).
|
||||||
|
3. **Limit-Auflösung** — Pro Feature: Override → Tier-Limit → Feature-Default.
|
||||||
|
4. **Usage-Tracking** — Zähler für Count-Features mit optionalem Reset (daily/monthly/never).
|
||||||
|
5. **Enforcement** — HTTP 403 bei Überschreitung; Frontend-Vorschaum via Badges.
|
||||||
|
6. **Beobachtbarkeit** — Strukturiertes JSON-Logging aller Access-Checks.
|
||||||
|
7. **Promotionen** — Coupons → Access Grants (temporäre Tier-Elevation, Pause/Resume).
|
||||||
|
|
||||||
|
Es übernimmt **nicht**:
|
||||||
|
|
||||||
|
- Login, Session, Passwort (Auth-Modul)
|
||||||
|
- Zahlungsabwicklung / Stripe (geplant, `CENTRAL_SUBSCRIPTION_SYSTEM.md`)
|
||||||
|
- Mandanten-Isolation (Org/Workspace) — Entitlements sind **profile-scoped**
|
||||||
|
- Inhaltliche Berechtigung pro Datensatz (nur Feature-Gates)
|
||||||
|
|
||||||
|
### Zwei Entscheidungsebenen
|
||||||
|
|
||||||
|
| Ebene | Frage | Funktion |
|
||||||
|
|-------|--------|----------|
|
||||||
|
| **Tier** | Welcher Tarif gilt? | `get_effective_tier()` |
|
||||||
|
| **Feature** | Darf Feature X genutzt werden (wie oft)? | `check_feature_access()` |
|
||||||
|
|
||||||
|
Tier beeinflusst Feature-Limits über `tier_limits`; User-Overrides können Limits unabhängig vom Tier setzen.
|
||||||
|
|
||||||
|
### Administrierte Konfigurationen
|
||||||
|
|
||||||
|
| Konfiguration | Speicherort | Admin-UI |
|
||||||
|
|---------------|-------------|----------|
|
||||||
|
| Feature-Definitionen | `features` | Admin Features |
|
||||||
|
| Tier-Stufen | `tiers` | Admin Tiers |
|
||||||
|
| Tier × Feature Matrix | `tier_limits` | Admin Tier Limits |
|
||||||
|
| User-Overrides | `user_feature_restrictions` | Admin User Restrictions |
|
||||||
|
| Coupons | `coupons`, `coupon_redemptions` | Admin Coupons |
|
||||||
|
| Temporäre Tier-Grants | `access_grants` | (via Coupon/Admin) |
|
||||||
|
| Widget → Feature Mapping | `widget_feature_requirements`, Katalog | Admin Widget Features |
|
||||||
|
| Usage-Zähler | `user_feature_usage` | (automatisch) |
|
||||||
|
|
||||||
|
**Nicht hardcodiert:** Limits pro Tier, Feature-Metadaten, Coupon-Parameter, User-Overrides.
|
||||||
|
|
||||||
|
**Hardcodiert (Code):** Feature-IDs in Routern (`'ai_calls'`, `'weight_entries'`, …), Reset-Berechnung, 4-Phasen-Muster, 11 initial registrierte Features.
|
||||||
|
|
||||||
|
### Auflösungs-Hierarchien
|
||||||
|
|
||||||
|
**Effektiver Tier** (`get_effective_tier`):
|
||||||
|
|
||||||
|
1. Aktiver `access_grants`-Eintrag (`is_active`, `valid_from`/`valid_until`)
|
||||||
|
2. Fallback: `profiles.tier`
|
||||||
|
|
||||||
|
**Feature-Limit** (`check_feature_access` → `_check_impl`):
|
||||||
|
|
||||||
|
1. `user_feature_restrictions.limit_value` (höchste Priorität)
|
||||||
|
2. `tier_limits` für effektiven Tier
|
||||||
|
3. `features.default_limit`
|
||||||
|
|
||||||
|
**Limit-Semantik:**
|
||||||
|
|
||||||
|
| `limit_type` | Bedeutung |
|
||||||
|
|--------------|-----------|
|
||||||
|
| `count` | Zählbares Kontingent; `used < limit` |
|
||||||
|
| `boolean` | An/Aus; `limit == 1` erlaubt, `0` gesperrt |
|
||||||
|
|
||||||
|
| `limit_value` | Bedeutung |
|
||||||
|
|---------------|-----------|
|
||||||
|
| `NULL` | Unbegrenzt |
|
||||||
|
| `0` | Deaktiviert |
|
||||||
|
| `> 0` | Kontingent oder Boolean „an“ |
|
||||||
|
|
||||||
|
### Rollen
|
||||||
|
|
||||||
|
| Rolle | Darf |
|
||||||
|
|-------|------|
|
||||||
|
| **Admin** | Features/Tiers/Limits/Coupons/Restrictions CRUD; alle Nutzer-Overrides |
|
||||||
|
| **Nutzer** | Eigene Subscription/Usage lesen (`/subscription/me`, `/features/usage`); keine Limit-Änderung |
|
||||||
|
|
||||||
|
Enforcement gilt für alle authentifizierten Nutzer gleich — Admins haben keine automatische Bypass-Logik in `check_feature_access`.
|
||||||
|
|
||||||
|
### Versionierung, Freigabe, Test
|
||||||
|
|
||||||
|
| Mechanismus | Status |
|
||||||
|
|-------------|--------|
|
||||||
|
| 4-Phasen-Rollout (Monitor → UI → Enforce) | ✅ dokumentiert & angewendet |
|
||||||
|
| JSON-Log `feature-usage.log` | ✅ Phase 2 Monitoring |
|
||||||
|
| DB-Migration v9c für Schema | ✅ |
|
||||||
|
| Automatisierte Enforcement-Tests pro Router | ⚠️ teilweise (Widgets getestet) |
|
||||||
|
| Zentrale Policy „jeder Endpoint muss checken“ | ❌ nicht erzwungen |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Designprinzipien
|
||||||
|
|
||||||
|
### 1. Feature-Registry statt hardcodierter Limits
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | Jedes limitierbare Produkt-Feature ist Zeile in `features` — neue Features ohne Schema-Migration für Limits. |
|
||||||
|
| **Begründung** | Admin-UI, Usage-API und Backend-Checks teilen dieselbe ID und Metadaten. |
|
||||||
|
| **Quelle** | `MEMBERSHIP_SYSTEM.md` § Feature-Registry; `routers/features.py` |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | Feature-IDs müssen trotzdem in Router-Code referenziert werden. |
|
||||||
|
|
||||||
|
### 2. Eine Auflösungsfunktion für Entitlements
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | Alle Backend- und Widget-Checks rufen `check_feature_access(profile_id, feature_id)` auf. |
|
||||||
|
| **Begründung** | Keine duplizierte Tier/Limit-Logik in Routern, Widgets oder Frontend. |
|
||||||
|
| **Quelle** | `auth.py`; `dashboard_widget_entitlements.py` |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | Nicht alle Endpoints nutzen es (z. B. `/prompts/execute` fehlt). |
|
||||||
|
|
||||||
|
### 3. Getrennte Tier- und Feature-Auflösung
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | `get_effective_tier()` für Tarif; `check_feature_access()` für konkretes Feature — Tier ist Input, nicht Output der Feature-Prüfung. |
|
||||||
|
| **Begründung** | Temporäre Grants heben Tier an; User-Override kann einzelnes Feature unabhängig anpassen. |
|
||||||
|
| **Quelle** | `auth.py` |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | `get_effective_tier` im Code einfacher als in `MEMBERSHIP_SYSTEM.md` (kein `tier_locked`, Trial nicht in Tier-Funktion). |
|
||||||
|
|
||||||
|
### 4. Prioritäts-Kette für Limits
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | User-Override > Tier-Limit > Feature-Default — explizit und dokumentiert. |
|
||||||
|
| **Begründung** | Support/Beta-Fälle ohne Tier-Wechsel; vorhersehbares Verhalten. |
|
||||||
|
| **Quelle** | `_check_impl()` in `auth.py`; `MEMBERSHIP_SYSTEM.md` § Zugriffs-Hierarchie |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | `user_feature_restrictions.enabled` im Schema, aber nicht in `_check_impl` ausgewertet. |
|
||||||
|
|
||||||
|
### 5. Count vs. Boolean als zwei Feature-Klassen
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | Zählbare Aktionen (`count` + Usage) vs. Schalter-Features (`boolean`, kein Counter). |
|
||||||
|
| **Begründung** | Pipeline-An/Aus vs. monatliche KI-Calls — unterschiedliche UX und Backend-Logik. |
|
||||||
|
| **Quelle** | `features.limit_type`; `FEATURE_ENFORCEMENT.md` |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | Boolean-Features nutzen `limit_value` 0/1 — leicht mit Count zu verwechseln. |
|
||||||
|
|
||||||
|
### 6. Reset-Perioden für Count-Features
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | `reset_period`: `never` \| `daily` \| `monthly` — Counter-Reset in `check_feature_access` bei abgelaufenem `reset_at`. |
|
||||||
|
| **Begründung** | Monats-Kontingente vs. Lifetime-Limits in einem Modell. |
|
||||||
|
| **Quelle** | `auth.py` → `_calculate_next_reset()` |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | Reset beim Check, nicht per Cron — Edge Cases bei seltenem Zugriff. |
|
||||||
|
|
||||||
|
### 7. Usage nur bei neuen Entitäten incrementieren
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | `increment_feature_usage()` nur nach **INSERT**, nicht nach UPDATE/Upsert-Deduplikat. |
|
||||||
|
| **Begründung** | Limits messen „neue Nutzung“, nicht Bearbeitung bestehender Daten. |
|
||||||
|
| **Quelle** | `FEATURE_ENFORCEMENT.md` § Wichtige Regeln |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | Bulk-Import muss explizit zählen; Fehler anfällig. |
|
||||||
|
|
||||||
|
### 8. Vier-Phasen-Rollout (Observe before Enforce)
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | Phase 1 Cleanup → 2 Logging → 3 Frontend-Badges → 4 HTTP 403. |
|
||||||
|
| **Begründung** | Limits einführen ohne blind Nutzer zu blockieren; Daten für Limit-Kalibrierung. |
|
||||||
|
| **Quelle** | `FEATURE_ENFORCEMENT.md` |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | Disziplin pro Feature; kein zentraler Feature-Flag pro Endpoint-Phase. |
|
||||||
|
|
||||||
|
### 9. Strukturiertes Feature-Logging
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | Jeder Check: `log_feature_usage(profile_id, feature_id, access, action)` → JSON in `feature-usage.log`. |
|
||||||
|
| **Begründung** | Audit, Debugging, Kalibrierung — auch wenn noch nicht enforced. |
|
||||||
|
| **Quelle** | `feature_logger.py` |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | Log-Pfad `/app/logs` container-spezifisch. |
|
||||||
|
|
||||||
|
### 10. Defense in Depth: API 403 + Frontend-Gate
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | Backend blockiert autoritativ; Frontend zeigt `UsageBadge`, deaktiviert Buttons, Tooltip bei Limit. |
|
||||||
|
| **Begründung** | UX (frühes Feedback) + Sicherheit (API nicht umgehbar via curl). |
|
||||||
|
| **Quelle** | `FEATURE_ENFORCEMENT.md`; `UsageBadge.jsx` |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | Frontend-Gate optional pro Seite; nicht generisch erzwungen. |
|
||||||
|
|
||||||
|
### 11. Nutzer-Usage-API ohne Code-Änderung bei neuen Features
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | `GET /features/usage` iteriert alle aktiven `features` und ruft `check_feature_access` pro Zeile. |
|
||||||
|
| **Begründung** | Neues DB-Feature erscheint automatisch in Quota-Übersicht. |
|
||||||
|
| **Quelle** | `routers/features.py` → `get_feature_usage()` |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | Frontend muss Feature-ID kennen, um Badge zu binden. |
|
||||||
|
|
||||||
|
### 12. Access Grants für temporäre Tier-Elevation
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | Coupons/Admin erzeugen `access_grants`; effektiver Tier steigt zeitlich begrenzt. |
|
||||||
|
| **Begründung** | Promotions, Partner (Wellpass), Trials ohne permanente Tier-Änderung. |
|
||||||
|
| **Quelle** | `access_grants`; `routers/coupons.py` (Pause/Resume) |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | Coupon-Stacking-Logik komplex; dokumentiert vs. Code prüfen bei Neuentwicklung. |
|
||||||
|
|
||||||
|
### 13. Entitlements als Querschnitt für UI-Module (Widgets)
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | Dashboard-Widgets mappen auf `features.id`; Katalog liefert `allowed` pro Profil. |
|
||||||
|
| **Begründung** | Tier-Logik nicht in React-Widgets duplizieren (`DASHBOARD_WIDGETS_AGENT_GUIDE` §0). |
|
||||||
|
| **Quelle** | `dashboard_widget_entitlements.py`; `ARCHITECTURE.md` §9 |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | Widget-Sichtbarkeit ≠ API-Schutz — Chart-Endpoints brauchen eigenes Gating (A4). |
|
||||||
|
|
||||||
|
### 14. Admin-konfigurierbare Tier × Feature Matrix
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | `tier_limits` trennt Tier-Definition von Limits; Tiers ohne hardcodierte Spalten pro Feature. |
|
||||||
|
| **Begründung** | Neue Tiers/Preise ohne Code-Deploy der Limit-Logik. |
|
||||||
|
| **Quelle** | `MEMBERSHIP_SYSTEM.md` § Tier-System; `tier_limits` Tabelle |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | Tier-Namen in Seed-Daten (`free`, `premium`, …) — erweiterbar, aber Konvention. |
|
||||||
|
|
||||||
|
### 15. NULL = unlimited, 0 = disabled
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | Einheitliche Semantik für Limit-Werte in allen Schichten. |
|
||||||
|
| **Begründung** | Vermeidet Sonderfälle „-1 means unlimited“; klare Admin-UI. |
|
||||||
|
| **Quelle** | `_check_impl()` in `auth.py` |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | SQL NULL vs. Python None — konsistent, aber in UI erklärungsbedürftig. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Registrierte Features (Referenz)
|
||||||
|
|
||||||
|
| Feature ID | Typ | Reset | Typische Aktion |
|
||||||
|
|------------|-----|-------|-----------------|
|
||||||
|
| `weight_entries` | count | never | Gewicht anlegen |
|
||||||
|
| `circumference_entries` | count | never | Umfang anlegen |
|
||||||
|
| `caliper_entries` | count | never | Caliper anlegen |
|
||||||
|
| `activity_entries` | count | monthly | Training anlegen/import |
|
||||||
|
| `nutrition_entries` | count | monthly | Ernährung anlegen/import |
|
||||||
|
| `photos` | count | monthly | Foto hochladen |
|
||||||
|
| `ai_calls` | count | monthly | KI-Analyse |
|
||||||
|
| `ai_pipeline` | boolean | — | Pipeline-Analyse |
|
||||||
|
| `data_export` | count | monthly | Export/PDF |
|
||||||
|
| `data_import` | count | monthly | ZIP/Universal-Import |
|
||||||
|
|
||||||
|
**Enforcement-Lücken (Ist):** `routers/prompts.py` (`/execute`, `/execute-stream`) ohne `check_feature_access` — Legacy `insights.py` hat Enforcement für `ai_calls`/`ai_pipeline`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Nicht übernehmen
|
||||||
|
|
||||||
|
1. **Dokumentations-Drift** — `MEMBERSHIP_SYSTEM.md` („Enforcement deaktiviert“) vs. `FEATURE_ENFORCEMENT.md` (Phase 4 komplett) vs. Ist-Code; bei Neuentwicklung einen Kanon festlegen.
|
||||||
|
|
||||||
|
2. **Unvollständige Tier-Auflösung** — Doku beschreibt `tier_locked`, Trial-in-Tier; Code nutzt primär Grants + `profiles.tier`. Trial (`trial_ends_at`) eher UI-Banner als Tier-Engine.
|
||||||
|
|
||||||
|
3. **Feature-IDs in Routern verstreut** — kein zentraler Endpoint-Registry-Eintrag „welcher Router prüft welches Feature“.
|
||||||
|
|
||||||
|
4. **Check und Increment nicht atomar** — Race bei parallelen Requests möglich; kein DB-Level Locking.
|
||||||
|
|
||||||
|
5. **Legacy Profil-Spalten parallel** — `ai_enabled`, `ai_limit_day`, `export_enabled` in Sessions-Query neben Feature-System.
|
||||||
|
|
||||||
|
6. **Frontend ohne Backend-Gate** — reine UI-Deaktivierung ohne 403 ist unsicher (manche Seiten nur teilweise umgesetzt).
|
||||||
|
|
||||||
|
7. **Boolean via limit_value 0/1** — funktioniert, aber für Familien-Architektur explizites `enabled`-Flag oder Capability-Tokens erwägen.
|
||||||
|
|
||||||
|
8. **Unused Schema-Felder** — `user_feature_restrictions.enabled` nicht in Auflösung eingebunden.
|
||||||
|
|
||||||
|
9. **App-lokales Abo ohne Zahlungsanbindung** — Stripe/SSO nur Vision (`CENTRAL_SUBSCRIPTION_SYSTEM.md`); nicht als fertiges Familien-Muster übernehmen.
|
||||||
|
|
||||||
|
10. **Profile als Entitlement-Subject** — kein Org/Mandant; Multi-App-Familie braucht separates Identity/Subscription-Boundary.
|
||||||
|
|
||||||
|
11. **Self-hosted Tier als Sonderfall** — `selfhosted` ist Deploy-Modell, kein generisches SaaS-Tier-Muster.
|
||||||
|
|
||||||
|
12. **Increment-Schleifen bei Bulk** — `for _ in range(new_entries): increment_feature_usage()` — ineffizient; batch-Inkrement besser.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Modul-Inventar (Ist-Stand)
|
||||||
|
|
||||||
|
```
|
||||||
|
backend/
|
||||||
|
├── auth.py # get_effective_tier, check_feature_access, increment_feature_usage
|
||||||
|
├── feature_logger.py # JSON-Logging
|
||||||
|
├── dashboard_widget_entitlements.py # Widget allowed + Layout-Sanitisierung
|
||||||
|
├── widget_feature_requirements_db.py
|
||||||
|
└── routers/
|
||||||
|
├── subscription.py # /me, /usage, /limits (Nutzer)
|
||||||
|
├── features.py # Admin CRUD + /usage, /check-access
|
||||||
|
├── tiers_mgmt.py, tier_limits.py
|
||||||
|
├── coupons.py, access_grants.py
|
||||||
|
└── user_restrictions.py
|
||||||
|
|
||||||
|
frontend/src/components/
|
||||||
|
└── UsageBadge.jsx # Quota-Anzeige (Phase 3)
|
||||||
|
```
|
||||||
|
|
||||||
|
**DB (v9c):** `features`, `tiers`, `tier_limits`, `user_feature_restrictions`, `user_feature_usage`, `coupons`, `coupon_redemptions`, `access_grants`, `user_activity_log`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Verwandte Dokumentation
|
||||||
|
|
||||||
|
- Membership-Detail: [MEMBERSHIP_SYSTEM.md](../../technical/MEMBERSHIP_SYSTEM.md)
|
||||||
|
- Enforcement-Howto: [FEATURE_ENFORCEMENT.md](../../architecture/FEATURE_ENFORCEMENT.md)
|
||||||
|
- Vision Produktfamilie: [CENTRAL_SUBSCRIPTION_SYSTEM.md](../../technical/CENTRAL_SUBSCRIPTION_SYSTEM.md)
|
||||||
|
- Widget-Gating: [DASHBOARD_WIDGETS_AGENT_GUIDE.md](../../technical/DASHBOARD_WIDGETS_AGENT_GUIDE.md) §0
|
||||||
|
- Prompt Engine (Enforcement-Lücke): [PROMPT_ENGINE_DESIGN_PRINCIPLES.md](./PROMPT_ENGINE_DESIGN_PRINCIPLES.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Geplante Folgedokumente (Serie)
|
||||||
|
|
||||||
|
| # | Modul | Status |
|
||||||
|
|---|-------|--------|
|
||||||
|
| 1 | Prompt Engine | ✅ |
|
||||||
|
| 2 | Data Layer | ✅ |
|
||||||
|
| 3 | Feature & Entitlement | ✅ dieses Dokument |
|
||||||
|
| 4 | Registry-/Plugin-Muster | ✅ |
|
||||||
|
| 5 | Auth & Session | ✅ |
|
||||||
|
| 6 | Universal Import | ✅ |
|
||||||
|
| 7 | Dashboard Widgets | ✅ |
|
||||||
|
| 8 | Navigation / IA | ✅ |
|
||||||
|
| 9 | Migration & Deploy | ✅ `MIGRATION_DEPLOY_DESIGN_PRINCIPLES.md` |
|
||||||
|
|
@ -0,0 +1,369 @@
|
||||||
|
# Migration & Deploy – Designprinzipien (Extraktion)
|
||||||
|
|
||||||
|
**Status:** Analyse / Arbeitspapier
|
||||||
|
**Stand:** 2026-07-04
|
||||||
|
**Geltungsbereich:** DB-Migrationen, Container-Startup, CI/CD-Deploy — keine Anwendungsdomäne
|
||||||
|
|
||||||
|
**Serie:** Designprinzipien für Produktfamilie · Dokument 9 von n (Abschluss)
|
||||||
|
**Vorgänger:** [NAVIGATION_IA_DESIGN_PRINCIPLES.md](./NAVIGATION_IA_DESIGN_PRINCIPLES.md)
|
||||||
|
|
||||||
|
**Kernkomponenten:**
|
||||||
|
|
||||||
|
| Bereich | Pfade |
|
||||||
|
|---------|-------|
|
||||||
|
| DB-Init & Migrationen | `backend/db_init.py`, `backend/startup.sh` |
|
||||||
|
| Migrationen | `backend/migrations/XXX_*.sql` |
|
||||||
|
| Basis-Schema (Greenfield) | `backend/schema.sql` |
|
||||||
|
| Tracking | Tabelle `schema_migrations` |
|
||||||
|
| Compose Prod/Dev | `docker-compose.yml`, `docker-compose.dev-env.yml` |
|
||||||
|
| CI/CD | `.gitea/workflows/deploy-dev.yml`, `deploy-prod.yml`, `test.yml` |
|
||||||
|
| Versionierung | `backend/version.py` (`APP_VERSION`, `DB_SCHEMA_VERSION`, `MODULE_VERSIONS`) |
|
||||||
|
| Doku (operativ) | `MIGRATIONS.md` |
|
||||||
|
| Architektur-Regeln | `.claude/rules/ARCHITECTURE.md` §2, §7 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Modul
|
||||||
|
|
||||||
|
**Migration & Deploy**
|
||||||
|
|
||||||
|
Automatische **PostgreSQL-Schema-Evolution** beim Container-Start plus **Git-getriebene Deploy-Pipeline** (develop → Dev, main → Prod) auf selbst-gehosteter Infrastruktur (Docker auf Raspberry Pi).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Fachliche Verantwortung
|
||||||
|
|
||||||
|
Das Modul übernimmt:
|
||||||
|
|
||||||
|
1. **Schema-Migrationen** — Nummerierte SQL-Dateien, idempotent wo möglich, getrackt in `schema_migrations`.
|
||||||
|
2. **Startup-Orchestrierung** — Postgres ready → Schema/Migrationen → optional SQLite-Import → Uvicorn.
|
||||||
|
3. **Umgebungstrennung** — Dev (`3099`/`8099`) vs. Prod (`3002`/`8002`), getrennte DBs/Volumes.
|
||||||
|
4. **Deploy-Automatisierung** — Push auf Branch → Runner → `git reset --hard` → `docker compose build --no-cache` → Health-Check.
|
||||||
|
5. **Post-Deploy-Tests** — Pytest/Lint/Frontend-Build gegen **deployed** Container auf dem Runner.
|
||||||
|
6. **Versions-Metadaten** — App-/Modul-Version und dokumentierte `DB_SCHEMA_VERSION`.
|
||||||
|
|
||||||
|
Es übernimmt **nicht**:
|
||||||
|
|
||||||
|
- Fachliche Datenberechnungen (→ Data Layer)
|
||||||
|
- Automatisches Downgrade/Rollback von Schema
|
||||||
|
- Blue-Green oder Multi-Region-Deploy
|
||||||
|
|
||||||
|
### Deploy-Pipeline (Happy Path)
|
||||||
|
|
||||||
|
```
|
||||||
|
Entwickler: commit → push develop
|
||||||
|
→ Gitea Runner: deploy-dev.yml
|
||||||
|
→ cd /home/lars/docker/bodytrack-dev
|
||||||
|
→ git fetch + reset --hard origin/develop
|
||||||
|
→ docker compose -f docker-compose.dev-env.yml build --no-cache && up -d
|
||||||
|
→ backend startup.sh → db_init.py (Migrationen)
|
||||||
|
→ curl localhost:8099/api/auth/status
|
||||||
|
→ test.yml (push + nach Deploy): pytest im Container, py_compile, npm run build
|
||||||
|
|
||||||
|
Prod: PR develop → main → deploy-prod.yml (Port 8002, bodytrack/)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Administrierte vs. code-definierte Konfiguration
|
||||||
|
|
||||||
|
| Konfiguration | Speicherort | Wer pflegt? |
|
||||||
|
|---------------|-------------|-------------|
|
||||||
|
| Migration-SQL | `backend/migrations/` | Entwickler |
|
||||||
|
| Welche Migrationen angewendet | `schema_migrations` (DB) | Automatisch |
|
||||||
|
| Greenfield-Basis | `schema.sql` | Entwickler (selten) |
|
||||||
|
| Compose/Ports/Env | `docker-compose*.yml`, `.env` auf Server | Betrieb |
|
||||||
|
| Deploy-Workflow | `.gitea/workflows/*.yml` | Entwickler |
|
||||||
|
| App-Version / Changelog | `backend/version.py` | Entwickler (pro Release) |
|
||||||
|
| Prod-Geheimnisse | Server-`.env`, nicht im Repo | Betrieb |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Designprinzipien
|
||||||
|
|
||||||
|
### 1. Migrationen beim Container-Start ( nicht manuell in Prod)
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | `startup.sh` ruft `db_init.py` auf **bevor** Uvicorn startet; pending Migrationen werden automatisch angewendet. |
|
||||||
|
| **Begründung** | Kein vergessenes Schema-Update; Deploy und DB-Stand bleiben gekoppelt. |
|
||||||
|
| **Quelle** | `startup.sh`, `db_init.py` |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | Fehlgeschlagene Migration blockiert API-Start (`sys.exit(1)`). |
|
||||||
|
|
||||||
|
### 2. Nummeriertes Datei-Pattern als Gate
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | Nur `\d{3}_*.sql` wird ausgeführt (z. B. `054_activity_session_metrics_eav.sql`); alles andere wird ignoriert. |
|
||||||
|
| **Begründung** | Sortierbare Reihenfolge; Ad-hoc-Skripte (`check_features.sql`, `v9c_*.sql`) verunreinigen nicht den Lauf. |
|
||||||
|
| **Quelle** | `run_migrations()` Regex |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | Legacy-Dateien ohne Nummer liegen noch im Ordner (historischer Ballast). |
|
||||||
|
|
||||||
|
### 3. Tracking-Tabelle als Single Source of „applied“
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | `schema_migrations(filename)` — jede erfolgreiche Datei genau einmal eingetragen; Pending = Dateien minus Applied. |
|
||||||
|
| **Begründung** | Idempotenter Startup; wiederholter Container-Start wendet nichts doppelt an. |
|
||||||
|
| **Quelle** | `ensure_migration_table`, `apply_migration` |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | Kein checksum — geänderte Datei nach Apply wird nicht erneut ausgeführt. |
|
||||||
|
|
||||||
|
### 4. Alphabetische Reihenfolge = Migrations-Reihenfolge
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | `sorted(glob)` — dreistellige Präfixe (`001`, `054`, `061`) definieren die Apply-Order. |
|
||||||
|
| **Begründung** | Einfach, git-freundlich, keine separate Migrations-Registry. |
|
||||||
|
| **Quelle** | `run_migrations()` |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | Nummern-Kollisionen oder nachträgliches Einfügen erfordern Disziplin (immer nächste freie Nummer). |
|
||||||
|
|
||||||
|
### 5. Fail-Fast bei Migrationsfehler
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | Schlägt eine Migration fehl → kein Commit in Tracking (bei Exception vor INSERT), Prozess exit 1, Container unhealthy. |
|
||||||
|
| **Begründung** | API läuft nicht mit halb angewendetem Schema. |
|
||||||
|
| **Quelle** | `apply_migration`, `main` in `db_init.py` |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | Manueller Recovery-Prozess nötig (siehe MIGRATIONS.md Rollback). |
|
||||||
|
|
||||||
|
### 6. Greenfield: schema.sql, Bestand: nur Migrationen
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | Existiert `profiles` nicht → einmalig `schema.sql` laden; danach nur noch nummerierte Migrationen. |
|
||||||
|
| **Begründung** | Frische Instanz schnell bootstrapped; langlebige DBs evolvieren incremental. |
|
||||||
|
| **Quelle** | `check_table_exists`, `load_schema` |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | `schema.sql` kann hinter Migrationen zurückfallen wenn nicht gepflegt. |
|
||||||
|
|
||||||
|
### 7. Idempotente DDL bevorzugen
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | `ADD COLUMN IF NOT EXISTS`, `CREATE INDEX IF NOT EXISTS`, defensive UPDATEs — Migration soll mehrfach ausführbar sein ohne Schaden. |
|
||||||
|
| **Begründung** | Recovery nach partiellem Apply; manuelles Re-Run sicherer. |
|
||||||
|
| **Quelle** | `MIGRATIONS.md` Best Practices |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | Nicht alle Änderungen sind idempotent (DROP, irreversible Datenmigration). |
|
||||||
|
|
||||||
|
### 8. Kein psql-Meta in Migrationsdateien
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | Nur SQL — kein `\echo`, `\i`, `\connect`; Ausführung via psycopg2, nicht interaktiv. |
|
||||||
|
| **Begründung** | Parser/Runner versteht nur SQL-Statements. |
|
||||||
|
| **Quelle** | `MIGRATIONS.md`, `apply_migration` |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | — |
|
||||||
|
|
||||||
|
### 9. Schema-Änderung = nummerierte Migration (nie ad-hoc in Prod)
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | Neue Tabellen/Spalten **nur** via `backend/migrations/XXX_*.sql`; nicht direkt in laufender Prod-DB editieren. |
|
||||||
|
| **Begründung** | Reproduzierbarkeit Dev→Prod; Review im Git-Diff. |
|
||||||
|
| **Quelle** | ARCHITECTURE.md, CLAUDE.md |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | Agent-Regel — technisch nicht erzwungen. |
|
||||||
|
|
||||||
|
### 10. DB_SCHEMA_VERSION als dokumentierter Marker
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | `backend/version.py` → `DB_SCHEMA_VERSION` bei Schema-Änderung manuell bumpen (Format z. B. `YYYYMMDD` + Suffix). |
|
||||||
|
| **Begründung** | API `/api/version` und Changelog zeigen Schema-Stand unabhängig von App-Minor. |
|
||||||
|
| **Quelle** | ARCHITECTURE.md §2.6 |
|
||||||
|
| **Tragfähigkeit** | **mittel–hoch** |
|
||||||
|
| **Einschränkung** | Nicht automatisch aus `schema_migrations` abgeleitet — Drift möglich. |
|
||||||
|
|
||||||
|
### 11. Branch → Umgebung (develop / main)
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | `develop` → Dev-Deploy automatisch; `main` → Prod-Deploy automatisch; Prod nur nach expliziter Freigabe/Merge. |
|
||||||
|
| **Begründung** | Klare Promotion; Dev als Integrationsumgebung. |
|
||||||
|
| **Quelle** | Workflows, CLAUDE.md Deployment |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | Kein Staging-Branch zwischen Dev und Prod. |
|
||||||
|
|
||||||
|
### 12. Deploy-Arbeitskopie = exakt Remote-Branch
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | Runner: `git fetch` + `git reset --hard origin/<branch>` — keine `pull`-Merge-Konflikte, kein schmutziger `package-lock` auf dem Pi. |
|
||||||
|
| **Begründung** | Reproduzierbarer Deploy-Baum; Fix aus GUI-IA-Abnahme 2026-04-05. |
|
||||||
|
| **Quelle** | `deploy-prod.yml`, `deploy-dev.yml` |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | Lokale Hotfixes auf dem Server werden beim Deploy überschrieben. |
|
||||||
|
|
||||||
|
### 13. Immutabler Build pro Deploy (`--no-cache`)
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | `docker compose build --no-cache` bei jedem Deploy — frisches Image aus Dockerfile + Repo-Stand. |
|
||||||
|
| **Begründung** | Keine veralteten Layer; Migrationen und Code garantiert im Image. |
|
||||||
|
| **Quelle** | Deploy-Workflows |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | Langsamere Deploys; kein Registry-basiertes Image-Promotion. |
|
||||||
|
|
||||||
|
### 14. Health-Check nach Deploy
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | Nach `up -d`: kurz warten, dann `curl -sf …/api/auth/status` (8099 Dev / 8002 Prod). |
|
||||||
|
| **Begründung** | Minimale Smoke-Verification dass API antwortet (inkl. DB-Init durchlaufen). |
|
||||||
|
| **Quelle** | Deploy-Workflows |
|
||||||
|
| **Tragfähigkeit** | **mittel–hoch** |
|
||||||
|
| **Einschränkung** | Prüft nicht fachliche Endpoints oder Migration-Inhalt. |
|
||||||
|
|
||||||
|
### 15. Persistente Volumes für Daten und Fotos
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | Postgres-Daten, `/app/data`, `/app/photos` in benannten/external Volumes — überleben Container-Rebuild. |
|
||||||
|
| **Begründung** | Deploy = neues Image, nicht Datenverlust. |
|
||||||
|
| **Quelle** | `docker-compose*.yml` |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | Volume-Backup/Restore ist Betriebsaufgabe außerhalb Repo. |
|
||||||
|
|
||||||
|
### 16. Postgres Healthcheck vor Backend-Start
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | `depends_on: condition: service_healthy` — Backend startet erst wenn DB `pg_isready`. |
|
||||||
|
| **Begründung** | `wait_for_postgres` in db_init ist zweite Absicherung; reduziert Race beim ersten Start. |
|
||||||
|
| **Quelle** | Compose-Files |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | — |
|
||||||
|
|
||||||
|
### 17. Tests gegen deployed Stack (Self-Hosted Runner)
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | `test.yml` führt pytest **im laufenden Backend-Container** auf dem Pi aus, nicht in isolierter GitHub-Cloud. |
|
||||||
|
| **Begründung** | Tests laufen gegen echte Dev/Prod-Compose-Umgebung des Projekts. |
|
||||||
|
| **Quelle** | `.gitea/workflows/test.yml` |
|
||||||
|
| **Tragfähigkeit** | **mittel** |
|
||||||
|
| **Einschränkung** | Prod-Deploy triggert Tests auf Prod-Pfad — Risiko wenn Tests schreibend; `-m 'not slow'` begrenzt Laufzeit. |
|
||||||
|
|
||||||
|
### 18. Feste Ports pro Umgebung
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | Dev `3099/8099`, Prod `3002/8002` — nicht ändern (Reverse Proxy/Fritz!Box hängen daran). |
|
||||||
|
| **Begründung** | Externe URLs (`dev.mitai.jinkendo.de`, `mitai.jinkendo.de`) stabil. |
|
||||||
|
| **Quelle** | CLAUDE.md, Compose |
|
||||||
|
| **Tragfähigkeit** | **hoch** (betriebsspezifisch) |
|
||||||
|
| **Einschränkung** | Andere Projekte brauchen eigene Port-Matrix. |
|
||||||
|
|
||||||
|
### 19. Prod-Schutz: Deploy nur über Git
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | Keine direkten Prod-Container-/DB-Schreibzugriffe für Automation; Prod-Änderung = Merge `main` → Workflow. |
|
||||||
|
| **Begründung** | Audit-Trail, Review, keine Drift. |
|
||||||
|
| **Quelle** | ARCHITECTURE.md §7.1, `/deploy` Command |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | Menschlicher SSH-Zugriff bleibt möglich — Prozess, nicht Technik. |
|
||||||
|
|
||||||
|
### 20. Versions-Bump als Release-Disziplin
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | Jede lieferbare Änderung: `APP_VERSION`, betroffene `MODULE_VERSIONS`, `CHANGELOG` in `version.py`; bei Schema auch `DB_SCHEMA_VERSION`. |
|
||||||
|
| **Begründung** | `/api/version`, Support, Korrelation Deploy ↔ Code. |
|
||||||
|
| **Quelle** | ARCHITECTURE.md §2.5, `deploy.md` |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | Frontend-`version.js` in Spec erwähnt, im Repo teils nicht vorhanden — Dual-Bump unvollständig. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Nicht übernehmen
|
||||||
|
|
||||||
|
1. **Unnummerierte Migrationsdateien** — `v9c_*.sql`, `check_*.sql` werden nicht auto-applied; nicht als Vorbild.
|
||||||
|
|
||||||
|
2. **Migration-Datei nach Apply ändern** — Tracking verhindert Re-Run; neue Nummer statt Edit.
|
||||||
|
|
||||||
|
3. **Automatischer Downgrade** — nicht implementiert; Rollback manuell + Tracking-Eintrag löschen.
|
||||||
|
|
||||||
|
4. **Direktes Schema in Prod** — immer Git-Migration + Deploy.
|
||||||
|
|
||||||
|
5. **Breaking DROP ohne Koordination** — App-Code und Migration in einem Release.
|
||||||
|
|
||||||
|
6. **psql-Metacommands in `.sql`** — bricht Python-Runner.
|
||||||
|
|
||||||
|
7. **`git pull` auf Deploy-Server** — Merge-Schmutz; `reset --hard` ist das Muster.
|
||||||
|
|
||||||
|
8. **Prod-Deploy ohne Dev-Validierung** — develop-First ist implizite Policy.
|
||||||
|
|
||||||
|
9. **Schema-Drift ohne `DB_SCHEMA_VERSION`-Bump** — dokumentarische Lücke.
|
||||||
|
|
||||||
|
10. **Cached Docker-Build als Default** — Mitai wählt Reproduzierbarkeit über Geschwindigkeit.
|
||||||
|
|
||||||
|
11. **Migrationen außerhalb Container-Start vergessen** — manuelles psql in Prod als Normalfall.
|
||||||
|
|
||||||
|
12. **Hardcoded Seed-Daten in Migration** — produktive User/Secrets nicht in SQL.
|
||||||
|
|
||||||
|
13. **Port-Änderung „nebenbei“** — Infrastruktur-Kopplung.
|
||||||
|
|
||||||
|
14. **Tests nur lokal, nie auf Runner-Stack** — Mitai testet bewusst post-deploy im Pi-Container (Trade-off verstehen).
|
||||||
|
|
||||||
|
15. **Transaktionssteuerung in SQL-Datei** — Runner committet pro Datei; komplexe multi-step Rollbacks nicht eingebaut.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Modul-Inventar (Ist-Stand)
|
||||||
|
|
||||||
|
```
|
||||||
|
backend/
|
||||||
|
├── db_init.py # wait, schema, run_migrations, sqlite import
|
||||||
|
├── startup.sh # db_init → uvicorn
|
||||||
|
├── schema.sql # Greenfield
|
||||||
|
├── migrations/ # 001–061+ nummeriert (+ Legacy ohne Nummer)
|
||||||
|
└── version.py # APP_VERSION, DB_SCHEMA_VERSION, MODULE_VERSIONS
|
||||||
|
|
||||||
|
docker-compose.yml # Prod: 3002/8002
|
||||||
|
docker-compose.dev-env.yml # Dev: 3099/8099
|
||||||
|
|
||||||
|
.gitea/workflows/
|
||||||
|
├── deploy-dev.yml # push develop
|
||||||
|
├── deploy-prod.yml # push main
|
||||||
|
└── test.yml # pytest, lint, npm build on Pi
|
||||||
|
|
||||||
|
Server (Pi):
|
||||||
|
/home/lars/docker/bodytrack-dev/ # develop
|
||||||
|
/home/lars/docker/bodytrack/ # main
|
||||||
|
```
|
||||||
|
|
||||||
|
**Migrationen (Stand):** 60+ nummerierte Dateien (`001` … `061`); höchste Nummer im Repo prüfen vor neuer Migration.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Verwandte Dokumentation
|
||||||
|
|
||||||
|
- Operativ: [MIGRATIONS.md](../../technical/MIGRATIONS.md)
|
||||||
|
- Architektur: `.claude/rules/ARCHITECTURE.md` §2 (Versionierung), §7 (Prod-Schutz)
|
||||||
|
- Deploy-Command: `.claude/commands/deploy.md`, `merge-to-prod.md`
|
||||||
|
- Import/Migration-Grenze: [UNIVERSAL_IMPORT_DESIGN_PRINCIPLES.md](./UNIVERSAL_IMPORT_DESIGN_PRINCIPLES.md)
|
||||||
|
- Auth auf Prod: [AUTH_SESSION_DESIGN_PRINCIPLES.md](./AUTH_SESSION_DESIGN_PRINCIPLES.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Serie – Übersicht (abgeschlossen)
|
||||||
|
|
||||||
|
| # | Modul | Status |
|
||||||
|
|---|-------|--------|
|
||||||
|
| 1 | Prompt Engine | ✅ `PROMPT_ENGINE_DESIGN_PRINCIPLES.md` |
|
||||||
|
| 2 | Data Layer | ✅ `DATA_LAYER_DESIGN_PRINCIPLES.md` |
|
||||||
|
| 3 | Feature & Entitlement | ✅ `FEATURE_ENTITLEMENT_DESIGN_PRINCIPLES.md` |
|
||||||
|
| 4 | Registry / Plugin (Meta) | ✅ `REGISTRY_PLUGIN_DESIGN_PRINCIPLES.md` |
|
||||||
|
| 5 | Auth & Session | ✅ `AUTH_SESSION_DESIGN_PRINCIPLES.md` |
|
||||||
|
| 6 | Universal Import | ✅ `UNIVERSAL_IMPORT_DESIGN_PRINCIPLES.md` |
|
||||||
|
| 7 | Dashboard Widgets | ✅ `DASHBOARD_WIDGETS_DESIGN_PRINCIPLES.md` |
|
||||||
|
| 8 | Navigation / IA | ✅ `NAVIGATION_IA_DESIGN_PRINCIPLES.md` |
|
||||||
|
| 9 | Migration & Deploy | ✅ dieses Dokument |
|
||||||
|
|
@ -0,0 +1,346 @@
|
||||||
|
# Navigation & Informationsarchitektur – Designprinzipien (Extraktion)
|
||||||
|
|
||||||
|
**Status:** Analyse / Arbeitspapier
|
||||||
|
**Stand:** 2026-07-04
|
||||||
|
**Geltungsbereich:** App-Navigation, Bereichs-Shells, Admin-IA, Responsive Shell — keine Seiteninhalte oder Domänenlogik
|
||||||
|
|
||||||
|
**Serie:** Designprinzipien für Produktfamilie · Dokument 8 von n
|
||||||
|
**Vorgänger:** [DASHBOARD_WIDGETS_DESIGN_PRINCIPLES.md](./DASHBOARD_WIDGETS_DESIGN_PRINCIPLES.md)
|
||||||
|
|
||||||
|
**Kernkomponenten:**
|
||||||
|
|
||||||
|
| Bereich | Pfade |
|
||||||
|
|---------|-------|
|
||||||
|
| Hauptnavigation | `frontend/src/config/appNav.js` |
|
||||||
|
| Erfassung | `frontend/src/config/captureNav.js`, `layouts/CaptureShell.jsx` |
|
||||||
|
| Einstellungen | `frontend/src/config/settingsNav.js`, `layouts/SettingsShell.jsx` |
|
||||||
|
| Admin | `frontend/src/config/adminNav.js`, `layouts/AdminShell.jsx`, `RequireAdmin.jsx` |
|
||||||
|
| KI-Analyse (Kategorien) | `frontend/src/config/analysisCategories.js`, `pages/Analysis.jsx` |
|
||||||
|
| Routing | `frontend/src/App.jsx` |
|
||||||
|
| Desktop-Sidebar | `frontend/src/components/DesktopSidebar.jsx` |
|
||||||
|
| Responsive CSS | `frontend/src/app.css` (`--nav-h`, `.bottom-nav`, `.analysis-split`, `.desktop-sidebar`) |
|
||||||
|
| Abnahme-Doku | `docs/issues/GUI_IA_ADMIN_NAV_2026-04-05.md` |
|
||||||
|
| Responsive-Spec | `.claude/docs/functional/RESPONSIVE_UI.md` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Modul
|
||||||
|
|
||||||
|
**Navigation & Informationsarchitektur (IA)**
|
||||||
|
|
||||||
|
Schichtenmodell für die PWA: **eine primäre Hauptnavigation** (6–7 Bereiche), darunter **Bereichs-Shells** mit eigener Sub-Navigation, getrennte **Admin-Realm**, **Auth-Gates** und **ein Breakpoint** für Mobile vs. Desktop.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Fachliche Verantwortung
|
||||||
|
|
||||||
|
Das Modul übernimmt:
|
||||||
|
|
||||||
|
1. **Hauptnav-SSoT** — Reihenfolge, Labels, Icons, Admin-Sichtbarkeit (`getMainNavItems`).
|
||||||
|
2. **Routing-Struktur** — Welche URL gehört zu welchem Bereich (Übersicht, Erfassen, Verlauf, Ziele, Analyse, Einstellungen, Admin).
|
||||||
|
3. **Sub-Navigation pro Bereich** — Capture-Hub, Settings-Tabs, Admin-Gruppen, Analyse-Kategorien.
|
||||||
|
4. **Layout-Muster** — Bottom-Nav (mobil), Sidebar (Desktop), `analysis-split` für tiefe Bereiche.
|
||||||
|
5. **Zugriffskontrolle (UI)** — `RequireAdmin`, Admin-Link nur bei `role === 'admin'`.
|
||||||
|
6. **Active-State** — Nested Routes (Erfassung unter `/capture`, Admin unter `/admin/*`).
|
||||||
|
7. **PWA-Tauglichkeit** — Safe Area, Scrollbare Bottom-Nav, Content-Padding.
|
||||||
|
|
||||||
|
Es übernimmt **nicht**:
|
||||||
|
|
||||||
|
- Backend-Autorisierung (→ [AUTH_SESSION_DESIGN_PRINCIPLES.md](./AUTH_SESSION_DESIGN_PRINCIPLES.md))
|
||||||
|
- Feature-Entitlements in der Nav (Tier-Gates an Endpoints/Widgets, nicht an jedem NavLink)
|
||||||
|
- Inhaltliche Tab-Logik innerhalb von Verlauf/Analyse (Seiten concern)
|
||||||
|
|
||||||
|
### IA-Modell (Nutzerperspektive)
|
||||||
|
|
||||||
|
| Ebene | Mental Model | Beispiel-Routen |
|
||||||
|
|-------|--------------|-----------------|
|
||||||
|
| **Primär** | Wo bin ich in der App? | `/`, `/capture`, `/history`, `/goals`, `/analysis`, `/settings` |
|
||||||
|
| **Sekundär (Shell)** | Was mache ich in diesem Bereich? | `/weight`, `/admin/g/features`, `/settings/dashboard-layout` |
|
||||||
|
| **Tertiär (Seite)** | Tabs/Filter innerhalb einer Maske | Verlauf-Tabs, Analyse-Kategorien |
|
||||||
|
|
||||||
|
### Strategisch vs. taktisch (Ziele)
|
||||||
|
|
||||||
|
| Ebene | Ort | Zweck |
|
||||||
|
|-------|-----|-------|
|
||||||
|
| **Strategisch** | `/goals` (Hauptnav) | Ziele definieren, Prioritäten, Focus Areas |
|
||||||
|
| **Taktisch** | `/custom-goals` (Erfassung) | Tägliche Ist-Werte für eigene Ziele |
|
||||||
|
| **Auswertung** | `/history` | Trends, Charts, Vergleiche |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Administrierte vs. code-definierte Konfiguration
|
||||||
|
|
||||||
|
| Konfiguration | Speicherort | Wer pflegt? |
|
||||||
|
|---------------|-------------|-------------|
|
||||||
|
| Hauptnav-Reihenfolge & Labels | `appNav.js` | Entwickler |
|
||||||
|
| Erfassungs-Kacheln & Shell-Nav | `captureNav.js` | Entwickler |
|
||||||
|
| Admin-Gruppen & Hub-Karten | `adminNav.js` | Entwickler |
|
||||||
|
| Settings-Subnav | `settingsNav.js` | Entwickler |
|
||||||
|
| Analyse-Kategorie-Reihenfolge | `analysisCategories.js` | Entwickler |
|
||||||
|
| KI-Prompt-Kategorien (Runtime) | DB `ai_prompts.category` | Admin (Prompts) |
|
||||||
|
| React-Routes | `App.jsx` | Entwickler (muss zu Nav-Configs passen) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Designprinzipien
|
||||||
|
|
||||||
|
### 1. Eine Quelle für die Hauptnavigation
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | `getMainNavItems(isAdmin)` liefert dieselbe Item-Liste für **Bottom-Nav** und **Desktop-Sidebar** — keine parallelen Hardcodings. |
|
||||||
|
| **Begründung** | Reihenfolge und Labels bleiben synchron; Admin-Conditional an einer Stelle. |
|
||||||
|
| **Quelle** | `appNav.js`, `App.jsx`, `DesktopSidebar.jsx` |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | Active-State-Logik ist in zwei Dateien dupliziert (`navItemActive` / `sidebarLinkActive`). |
|
||||||
|
|
||||||
|
### 2. Feste primäre IA-Reihenfolge (Produkt-Story)
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | Übersicht → Erfassen → Verlauf → **Ziele** → Analyse → Einstellungen → [Admin] — spiegelt Nutzerfluss: sehen → eingeben → auswerten → steuern → interpretieren → konfigurieren. |
|
||||||
|
| **Begründung** | Ziele als eigener Hauptpunkt (nicht unter Analyse versteckt); klare Trennung Capture vs. History vs. Analysis. |
|
||||||
|
| **Quelle** | `GUI_IA_ADMIN_NAV_2026-04-05.md`; `appNav.js` |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | Produkt-spezifisch; andere Apps können andere Reihenfolge brauchen. |
|
||||||
|
|
||||||
|
### 3. Config-Dateien pro Bereich (Nav-as-Data)
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | Sub-Navigation lebt in dedizierten Config-Modulen (`captureNav`, `adminNav`, `settingsNav`, `analysisCategories`) — Shell-Komponenten iterieren nur. |
|
||||||
|
| **Begründung** | Neue Erfassungsmaske = Eintrag in Config + Route; kein Nav-HTML in jeder Page. |
|
||||||
|
| **Quelle** | `captureNav.js` Kommentar „Pfade müssen mit Routes übereinstimmen“ |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | Kein Build-Time-Check Config ↔ Routes. |
|
||||||
|
|
||||||
|
### 4. Bereichs-Shells für tiefe Navigation
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | Capture, Settings und Admin nutzen **Shell-Layouts** mit `<Outlet />`; Nutzer wechselt Sub-Bereiche ohne Hauptnav zu verlassen. |
|
||||||
|
| **Begründung** | Erfassung hat 12+ Masken — wären als Hauptnav-Einträge unbrauchbar. |
|
||||||
|
| **Quelle** | `CaptureShell`, `SettingsShell`, `AdminShell` |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | Verlauf und Analyse haben eigene Tab-Muster (kein gemeinsames Shell-Config). |
|
||||||
|
|
||||||
|
### 5. Wiederverwendbares `analysis-split`-Layout
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | Admin, Settings und KI-Analyse teilen CSS-Muster: mobil horizontale Chips, Desktop linke Spalte + `__main` für Inhalt. |
|
||||||
|
| **Begründung** | Ein visuelles Muster für „Kategorie links, Arbeit rechts“; weniger UI-Drift. |
|
||||||
|
| **Quelle** | `AdminShell.jsx`, `SettingsShell.jsx`, `Analysis.jsx`, `app.css` |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | Capture nutzt eigenes `capture-shell` (Emoji-Icons, Hub-Kacheln). |
|
||||||
|
|
||||||
|
### 6. Admin: Gruppen in der Shell, Seiten über Hub
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | Shell-Nav zeigt nur **Admin-Gruppen** (+ Übersicht); konkrete Seiten als Karten auf `/admin/g/:groupId`. |
|
||||||
|
| **Begründung** | Skaliert bei wachsender Admin-Oberfläche; keine 20er-Sidebar. |
|
||||||
|
| **Quelle** | `adminNav.js` (`ADMIN_GROUPS`, `getAdminShellNavEntries`) |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | Ein Klick mehr als flache Nav; bewusster Trade-off. |
|
||||||
|
|
||||||
|
### 7. Admin als eigener Realm
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | `/admin/*` hinter `RequireAdmin`; kein Admin-Block mehr in Einstellungen; Profil-Anlage nur Admin → Benutzerverwaltung. |
|
||||||
|
| **Begründung** | Trennung Nutzer- vs. Betreiber-Kontext; weniger Verwechslung. |
|
||||||
|
| **Quelle** | `RequireAdmin.jsx`, `GUI_IA_ADMIN_NAV_2026-04-05.md` |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | UI-Guard ersetzt nicht Backend-`require_admin` auf APIs. |
|
||||||
|
|
||||||
|
### 8. Route-Guard mit Nutzer-Feedback
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | Nicht-Admin auf `/admin` → Redirect `/` mit `state.adminDenied`; Dashboard zeigt Hinweis. |
|
||||||
|
| **Begründung** | Stilles Scheitern vermeiden; klare Erwartung. |
|
||||||
|
| **Quelle** | `RequireAdmin.jsx`, `Dashboard.jsx` |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | — |
|
||||||
|
|
||||||
|
### 9. Nested Active-State für Section-Prefixes
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | Custom Active-Logik: `/capture` aktiv bei allen Erfassungs-Pfaden; `/admin` bei gesamten Admin-Baum; `/goals` mit `end: true` (exakt). |
|
||||||
|
| **Begründung** | React-Router `end` allein reicht für Section-Gruppen nicht. |
|
||||||
|
| **Quelle** | `navItemActive`, `adminShellEntryIsActive` |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | Neue Section-Prefixes brauchen explizite Regel. |
|
||||||
|
|
||||||
|
### 10. Erfassungs-Hub + direkte Deep-Links
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | `/capture` = Kachel-Hub; jede Maske auch direkt erreichbar (`/weight`, …); Shell-Nav immer sichtbar. |
|
||||||
|
| **Begründung** | Onboarding über Hub; Power-User/Dashboard-Links springen direkt. |
|
||||||
|
| **Quelle** | `CaptureHub`, `CAPTURE_HUB_TILES` |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | Hub und Shell-Nav listen dieselben Ziele (Doppelpflege). |
|
||||||
|
|
||||||
|
### 11. Einstellungen: nur aktives Profil
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | Settings = Self-Service für **aktives** Profil (Name, E-Mail, Avatar, Quality-Filter); keine Profil-Liste für Endnutzer. |
|
||||||
|
| **Begründung** | Multi-Profil-Verwaltung ist Admin-Aufgabe; reduziert Komplexität. |
|
||||||
|
| **Quelle** | `SettingsPage.jsx`, IA-Doku |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | Session-bound Profile-Id-Schwäche bleibt Backend-Thema. |
|
||||||
|
|
||||||
|
### 12. Settings-Subnav für Layout & Export
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | Konfiguration schwerer Features (Dashboard-Layout, PDF-Berichte, Referenzwerte) als eigene Settings-Routen unter Shell — nicht in „Allgemein“ verstecken. |
|
||||||
|
| **Begründung** | Entspricht [DASHBOARD_WIDGETS_DESIGN_PRINCIPLES.md](./DASHBOARD_WIDGETS_DESIGN_PRINCIPLES.md) (Nutzer-Konfigurator). |
|
||||||
|
| **Quelle** | `settingsNav.js` |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | Admin-Dashboard-Default liegt unter `/admin/...` (getrennte Rolle). |
|
||||||
|
|
||||||
|
### 13. KI-Analyse: Ergebnis im Hauptspalt
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | Neue Analyse-Ergebnisse rendern in `analysis-split__main`, nicht in der Kategorie-Nav — Nav bleibt wählbar. |
|
||||||
|
| **Begründung** | Lange Ergebnisse verdrängen sonst die Prompt-Auswahl (Mobile). |
|
||||||
|
| **Quelle** | `GUI_IA_ADMIN_NAV_2026-04-05.md`, `Analysis.jsx` |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | — |
|
||||||
|
|
||||||
|
### 14. Ein Breakpoint Mobile / Desktop (1024px)
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | `< 1024px`: Bottom-Nav + Mobile-Header; `≥ 1024px`: Desktop-Sidebar, Bottom-Nav ausgeblendet, breiterer Content. **Kein** separates Tablet-Layout. |
|
||||||
|
| **Begründung** | Einfache Spec, PWA-first; iPad im Portrait = Mobile-Verhalten. |
|
||||||
|
| **Quelle** | `RESPONSIVE_UI.md`, `app.css` |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | Große Phones und kleine Tablets identisch behandelt. |
|
||||||
|
|
||||||
|
### 15. PWA Safe Area für Bottom-Navigation
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | `--nav-h`, `--nav-pad-top`, `env(safe-area-inset-bottom)` auf `.bottom-nav`; Content-`padding-bottom` inkl. Nav-Höhe; horizontal scrollbare Nav bei vielen Items. |
|
||||||
|
| **Begründung** | iPhone Home-Indicator und Notch — kein Clipping, kein verdeckter Content. |
|
||||||
|
| **Quelle** | `app.css`, IA-Doku |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | Safe Area nur auf Nav/Content-Padding, nicht global überall. |
|
||||||
|
|
||||||
|
### 16. Auth-Routen außerhalb der App-Shell
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | Login, Register, Verify, Reset-Password rendern **ohne** Bottom-Nav/Sidebar — minimale Vollbild-Cards. |
|
||||||
|
| **Begründung** | Keine Navigation ohne Session; klarer Fokus. |
|
||||||
|
| **Quelle** | `App.jsx` (early returns vor `AppShell`) |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | Public Routes nicht zentral in einer Route-Config. |
|
||||||
|
|
||||||
|
### 17. Rollen-sichtbare Nav-Einträge (UI only)
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | Admin-Link erscheint nur wenn `isAdmin`; Backend schützt APIs separat. |
|
||||||
|
| **Begründung** | Progressive disclosure; normale Nutzer sehen keinen toten Link. |
|
||||||
|
| **Quelle** | `getMainNavItems(isAdmin)` |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | Security nicht durch Ausblenden ersetzt. |
|
||||||
|
|
||||||
|
### 18. Deep-Link-State für Verlauf
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | Nav zu `/history` setzt optional `state: { tab: 'overview' }` — konsistenter Einstieg von Hauptnav. |
|
||||||
|
| **Begründung** | Verlauf merkt sich Tabs; Hauptnav soll nicht zufälligen alten Tab öffnen. |
|
||||||
|
| **Quelle** | `App.jsx`, `DesktopSidebar.jsx`, `History.jsx` |
|
||||||
|
| **Tragfähigkeit** | **mittel** |
|
||||||
|
| **Einschränkung** | Nur für History implementiert, nicht app-weit. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Nicht übernehmen
|
||||||
|
|
||||||
|
1. **Hauptnav an mehreren Stellen hardcoden** — immer `appNav.js`.
|
||||||
|
|
||||||
|
2. **Admin-Funktionen in Einstellungen** — eigener `/admin`-Bereich.
|
||||||
|
|
||||||
|
3. **Alle Erfassungsmasken in die Bottom-Nav** — Shell + Hub skaliert.
|
||||||
|
|
||||||
|
4. **Alle Admin-Seiten in der Shell-Sidebar** — Hub-Gruppen-Muster beibehalten.
|
||||||
|
|
||||||
|
5. **Nav-Config ohne Route-Pflege** — jeder neue Pfad: Config + `App.jsx` + ggf. Active-State.
|
||||||
|
|
||||||
|
6. **UI-Admin-Guard ohne Backend-Guard** — `RequireAdmin` ist UX, APIs brauchen `require_admin`.
|
||||||
|
|
||||||
|
7. **Zwei Tablet-/Desktop-Breakpoints** — Mitai: ein Cut bei 1024px.
|
||||||
|
|
||||||
|
8. **Safe Area ignorieren** — PWA auf iOS bricht sonst an Bottom-Nav.
|
||||||
|
|
||||||
|
9. **Profil-Liste für Endnutzer in Settings** — Multi-Profil = Admin.
|
||||||
|
|
||||||
|
10. **Feature-Tier-Logik in Nav-Komponenten** — Entitlements an Widgets/APIs ([FEATURE_ENTITLEMENT_DESIGN_PRINCIPLES.md](./FEATURE_ENTITLEMENT_DESIGN_PRINCIPLES.md)).
|
||||||
|
|
||||||
|
11. **Inkonsistente Layout-Muster pro Bereich** — wo `analysis-split` passt, nicht neues Ad-hoc-Layout erfinden.
|
||||||
|
|
||||||
|
12. **Orphan-Routes ohne Nav-Ergänzung** — z. B. `/subscription`, `/workflow-editor/:id` existieren außerhalb Haupt-IA; bewusst dokumentieren, nicht unkontrolliert multiply.
|
||||||
|
|
||||||
|
13. **Active-State nur per Router-Default** — Section-Prefixes (`/capture/*`, `/admin/*`) brauchen explizite Regeln.
|
||||||
|
|
||||||
|
14. **Analyse-Ergebnisse in der Nav-Spalte** — verdrängt Prompt-Auswahl auf Mobile.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Modul-Inventar (Ist-Stand)
|
||||||
|
|
||||||
|
```
|
||||||
|
frontend/src/config/
|
||||||
|
├── appNav.js # Hauptnav (6 + Admin)
|
||||||
|
├── captureNav.js # Erfassungs-Hub + Shell
|
||||||
|
├── settingsNav.js # Settings-Subnav
|
||||||
|
├── adminNav.js # ADMIN_GROUPS, Shell-Entries
|
||||||
|
└── analysisCategories.js # KI-Analyse-Gruppen
|
||||||
|
|
||||||
|
frontend/src/layouts/
|
||||||
|
├── CaptureShell.jsx
|
||||||
|
├── SettingsShell.jsx
|
||||||
|
├── AdminShell.jsx
|
||||||
|
└── RequireAdmin.jsx
|
||||||
|
|
||||||
|
frontend/src/components/
|
||||||
|
└── DesktopSidebar.jsx
|
||||||
|
|
||||||
|
frontend/src/App.jsx # Routes + Bottom-Nav + Auth-Gates
|
||||||
|
frontend/src/app.css # Shell, split, safe-area, 1024px breakpoint
|
||||||
|
```
|
||||||
|
|
||||||
|
**Hauptnav (7 Einträge mit Admin):** Übersicht · Erfassen · Verlauf · Ziele · Analyse · Einstellungen · Admin
|
||||||
|
|
||||||
|
**Admin-Gruppen (8):** users · features · subscription · training · goals · prompts · system
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Verwandte Dokumentation
|
||||||
|
|
||||||
|
- Abnahme-Stand: [GUI_IA_ADMIN_NAV_2026-04-05.md](../../../../docs/issues/GUI_IA_ADMIN_NAV_2026-04-05.md)
|
||||||
|
- Responsive-Spec: [RESPONSIVE_UI.md](../../functional/RESPONSIVE_UI.md)
|
||||||
|
- Auth/Session: [AUTH_SESSION_DESIGN_PRINCIPLES.md](./AUTH_SESSION_DESIGN_PRINCIPLES.md)
|
||||||
|
- Dashboard-Konfigurator: [DASHBOARD_WIDGETS_DESIGN_PRINCIPLES.md](./DASHBOARD_WIDGETS_DESIGN_PRINCIPLES.md)
|
||||||
|
- Gitea #30 (Responsive UI, teilweise erledigt)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Geplante Folgedokumente (Serie)
|
||||||
|
|
||||||
|
| # | Modul | Status |
|
||||||
|
|---|-------|--------|
|
||||||
|
| 1–7 | … | ✅ |
|
||||||
|
| 8 | Navigation / IA | ✅ dieses Dokument |
|
||||||
|
| 9 | Migration & Deploy | ✅ |
|
||||||
|
|
@ -0,0 +1,305 @@
|
||||||
|
# Prompt Engine – Designprinzipien (Extraktion)
|
||||||
|
|
||||||
|
**Status:** Analyse / Arbeitspapier
|
||||||
|
**Stand:** 2026-07-04
|
||||||
|
**Geltungsbereich:** Modul „Prompt Engine“ (Unified Prompt System, Issue #28) — keine Mitai-Gesamtarchitektur, keine Domänenlogik (Gesundheit, Ernährung, Messwerte)
|
||||||
|
|
||||||
|
**Serie:** Designprinzipien für Produktfamilie · Dokument 1 von n
|
||||||
|
|
||||||
|
**Kernkomponenten:**
|
||||||
|
|
||||||
|
| Bereich | Pfade |
|
||||||
|
|---------|-------|
|
||||||
|
| Executor | `backend/prompt_executor.py`, `backend/workflow_executor.py` |
|
||||||
|
| Platzhalter | `backend/placeholder_resolver.py`, `backend/placeholder_registry.py`, `backend/placeholder_registrations/` |
|
||||||
|
| API | `backend/routers/prompts.py`, `backend/routers/workflows.py` |
|
||||||
|
| Admin-UI | `frontend/src/pages/AdminPromptsPage.jsx`, `UnifiedPromptModal.jsx`, `WorkflowEditorPage.jsx` |
|
||||||
|
| Fachliche Spec | `.claude/docs/functional/AI_PROMPTS.md` |
|
||||||
|
| Platzhalter-Governance | `.claude/docs/technical/PLACEHOLDER_REGISTRY_FRAMEWORK.md`, `docs/PLACEHOLDER_GOVERNANCE.md` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Modul
|
||||||
|
|
||||||
|
**Prompt Engine** (Unified Prompt System, Issue #28)
|
||||||
|
|
||||||
|
Backend-Kern: `prompt_executor.py`, `placeholder_resolver.py`, `placeholder_registry` / `placeholder_registrations/`, `workflow_executor.py`
|
||||||
|
API: `routers/prompts.py`
|
||||||
|
Admin-UI: `AdminPromptsPage`, `UnifiedPromptModal`, `WorkflowEditorPage`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Fachliche Verantwortung
|
||||||
|
|
||||||
|
Die Prompt Engine ist die **zentrale Ausführungs- und Konfigurationsschicht für KI-Analysen**. Sie übernimmt:
|
||||||
|
|
||||||
|
1. **Prompt-Orchestrierung** — Laden aktiver Prompt-Definitionen aus der DB und Ausführung als `base` (Einzelprompt), `pipeline` (mehrstufig) oder `workflow` (Graph).
|
||||||
|
2. **Kontextaufbereitung** — Befüllen von Platzhaltern mit profilbezogenen Daten (Resolver → Data Layer).
|
||||||
|
3. **LLM-Aufruf** — Einheitlicher OpenRouter-Call über injizierte Callback-Funktion.
|
||||||
|
4. **Ergebnisbehandlung** — JSON-Validierung, strukturierte Container (Fragenergänzungen), Debug-Metadaten, optionales Speichern in `ai_insights`.
|
||||||
|
5. **Admin-Konfiguration** — CRUD für Prompts/Workflows, Import/Export, Vorschau und Test ohne Produktions-Ausführung.
|
||||||
|
|
||||||
|
### Administrierte Konfigurationen
|
||||||
|
|
||||||
|
| Konfiguration | Speicherort | Inhalt |
|
||||||
|
|---------------|-------------|--------|
|
||||||
|
| Prompt-Metadaten | `ai_prompts` | `name`, `slug`, `category`, `active`, `sort_order`, `display_name` |
|
||||||
|
| Templates | `ai_prompts` | `template` |
|
||||||
|
| Pipeline-Stages | `ai_prompts.stages` (JSONB) | Stages mit `inline` / `reference` |
|
||||||
|
| Workflow-Graphen | `ai_prompts.graph_data` | Knoten, Kanten, Metadaten |
|
||||||
|
| Output-Regeln | `ai_prompts` | `output_format`, `output_schema` |
|
||||||
|
| Fragenergänzungen | `ai_prompts.question_augmentations` | Optionale Standard-Fragen (Hybridmodell: Knoten > Prompt) |
|
||||||
|
| System-Reset | `ai_prompts` | `is_system_default`, `default_template` |
|
||||||
|
| Legacy-Pipeline-Configs | `pipeline_configs` | Module, Zeiträume, Stage-Slugs (parallel zum Unified System) |
|
||||||
|
| Workflow-Fragenkatalog | `workflow_question_catalog` | Fragetypen, Templates, Normalisierung |
|
||||||
|
|
||||||
|
### Bewusst nicht hardcodiert
|
||||||
|
|
||||||
|
- Prompt-Texte, Pipeline-Zusammensetzung, Workflow-Topologie
|
||||||
|
- Kategorie, Sichtbarkeit (`active`), Sortierung
|
||||||
|
- Output-Format und Schema pro Prompt
|
||||||
|
- Referenz vs. Inline in Pipeline-Stages
|
||||||
|
|
||||||
|
### Hardcodiert (Code / Env)
|
||||||
|
|
||||||
|
- Platzhalter-Definitionen und Resolver (`PLACEHOLDER_MAP`, Registry)
|
||||||
|
- LLM-Modell (`OPENROUTER_MODEL`)
|
||||||
|
- Default-Module und -Zeiträume in `/prompts/execute`
|
||||||
|
- Domänen-Kategorien im Frontend (`analysisCategories.js`)
|
||||||
|
- Meta-Prompts für Generate/Optimize (Admin-Tooling)
|
||||||
|
|
||||||
|
### Trennung: Template · Platzhalter · Kontext · Workflow
|
||||||
|
|
||||||
|
| Schicht | Ort | Rolle |
|
||||||
|
|---------|-----|-------|
|
||||||
|
| **Templates** | `ai_prompts.template`, `stages`, Knoten-Templates im Graph | Was an die KI geht |
|
||||||
|
| **Platzhalter** | `placeholder_resolver` + Registry | Semantische API-Keys `{{key}}`, Resolver-Funktionen |
|
||||||
|
| **Kontextdaten** | `execute_prompt_with_data` + Resolver → `data_layer/` | Werte für Platzhalter |
|
||||||
|
| **Workflows** | `graph_data` + `workflow_executor` | Ausführungsgraph, Verzweigung, Join, Aggregation |
|
||||||
|
|
||||||
|
### Durchsetzung: keine Sonderlogik außerhalb der Engine
|
||||||
|
|
||||||
|
**Konzeptionell:** Ein Executor (`execute_prompt` → `execute_prompt_with_data`) als Single Entry Point.
|
||||||
|
|
||||||
|
**Praktisch unvollständig:** Legacy-Pfade in `insights.py` (`/insights/run`, `/insights/pipeline`) mit eigener Template-Logik (`_prepare_template_vars`, `_render_template`) und direkten LLM-Calls; `History.jsx` nutzt noch `runInsight`. Kein technischer Guard (Lint/Policy), nur Konvention.
|
||||||
|
|
||||||
|
### Rollen und Berechtigungen
|
||||||
|
|
||||||
|
| Rolle | Darf |
|
||||||
|
|-------|------|
|
||||||
|
| **Admin** (`require_admin`) | Prompts/Workflows/Pipeline-Configs CRUD, Import/Export, Reset-to-default, Generate/Optimize, Platzhalter-Metadaten-ZIP |
|
||||||
|
| **Nutzer** (`require_auth`) | Aktive Prompts listen (ohne Pipeline-Slugs), ausführen (`/prompts/execute`), Preview, Platzhalter-Katalog, eigene Werte exportieren |
|
||||||
|
|
||||||
|
Workflow-Editor-Route (`/workflow-editor/:id`) ist nicht hinter `RequireAdmin`; Schreib-APIs sind admin-geschützt.
|
||||||
|
|
||||||
|
### Versionierung, Freigabe, Test
|
||||||
|
|
||||||
|
| Mechanismus | Status |
|
||||||
|
|-------------|--------|
|
||||||
|
| Prompt-Versionsverlauf in DB | ❌ Overwrite |
|
||||||
|
| Reset-to-default für System-Prompts | ✅ `is_system_default` + `default_template` |
|
||||||
|
| JSON Import/Export (Dev→Prod) | ✅ `/export-all`, `/import` |
|
||||||
|
| Admin-Test mit Debug | ✅ `debug=true`, UnifiedPromptModal |
|
||||||
|
| Preview ohne LLM | ✅ `POST /preview` |
|
||||||
|
| Platzhalter-Deprecation-Prozess | 📄 dokumentiert, nicht runtime-erzwungen |
|
||||||
|
| Formales Freigabe-Workflow | ❌ |
|
||||||
|
| Executor-E2E-Tests | ⚠️ punktuell (Modifier, Output-Compact) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Designprinzipien
|
||||||
|
|
||||||
|
### 1. Single Executor für Prompt-Ausführung
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | Alle KI-Analysen laufen über `execute_prompt` / `execute_prompt_with_data`. |
|
||||||
|
| **Begründung** | Einheitliche Platzhalter-Auflösung, Debug, JSON-Validierung, Speicher-Metadaten. |
|
||||||
|
| **Quelle** | `backend/prompt_executor.py`; `POST /api/prompts/execute`; `Analysis.jsx` → `executeUnifiedPromptStream` |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | Legacy `insights.py` und teils `History.jsx` umgehen den Executor noch. |
|
||||||
|
|
||||||
|
### 2. Konfigurierbare Prompt-Bibliothek statt fest verdrahteter Texte
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | Prompt-Inhalte und Workflows liegen in `ai_prompts`, nicht im Anwendungscode. |
|
||||||
|
| **Begründung** | Admins können Analysen anpassen, duplizieren, deaktivieren, ohne Deploy. |
|
||||||
|
| **Quelle** | Migration 020; `UnifiedPromptCreate`/`Update` in `models.py`; `AdminPromptsPage.jsx` |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | Parallel existieren noch `pipeline_configs` und hardcodierte Default-Module/Zeiträume. |
|
||||||
|
|
||||||
|
### 3. Drei Prompt-Typen mit klarer Verantwortung
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | `base` = wiederverwendbarer Baustein; `pipeline` = sequenzielle Stages; `workflow` = Graph mit Verzweigung. |
|
||||||
|
| **Begründung** | Komposition ohne Copy-Paste; Reference-Prompts in Pipelines (`source: 'reference'`). |
|
||||||
|
| **Quelle** | `execute_prompt()` Typ-Verzweigung; `StagePromptCreate` in `models.py` |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | Pipeline-Stages laufen sequentiell, obwohl konzeptionell „parallel“; `workflow_definitions` und `ai_prompts.graph_data` doppelt. |
|
||||||
|
|
||||||
|
### 4. Platzhalter als API-Verträge (Registry)
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | Platzhalter sind registrierte, dokumentierte Verträge — keine freien Prompt-Hilfsvariablen. |
|
||||||
|
| **Begründung** | Konsistenz für Injektion, GUI-Picker, Export, Validierung. |
|
||||||
|
| **Quelle** | `PLACEHOLDER_REGISTRY_FRAMEWORK.md`; `docs/PLACEHOLDER_GOVERNANCE.md`; `import placeholder_registrations` in `main.py` |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | Duplikat `PLACEHOLDER_MAP` in `placeholder_resolver.py` neben Registry; Metadaten teils noch Legacy. |
|
||||||
|
|
||||||
|
### 5. Trennung Template (Was) vs. Resolver (Daten)
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | Templates enthalten nur `{{keys}}`; Berechnung liegt in Resolver/Data Layer. |
|
||||||
|
| **Begründung** | Prompt-Autoren ändern Text, nicht Berechnungslogik. |
|
||||||
|
| **Quelle** | `resolve_placeholders()` in `prompt_executor.py`; Registry-Felder `resolver_function`, `data_layer_function` |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | `execute_prompt_with_data` lädt zusätzlich Roh-SQL pro Modul — zweite Kontext-Schicht. |
|
||||||
|
|
||||||
|
### 6. Layer-1-Daten vs. Layer-2a-Prompt-Injektion
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | Berechnungen in `data_layer/`; Prompt Engine konsumiert nur formatierte Werte. |
|
||||||
|
| **Begründung** | Single Source of Truth für Charts, Platzhalter, KI. |
|
||||||
|
| **Quelle** | Phase-0c-Architektur; Registry-Felder `data_layer_module` / `layer_1_decision` |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | Legacy `_prepare_template_vars` in `insights.py` umgeht Data Layer. |
|
||||||
|
|
||||||
|
### 7. Transparenz durch Debug- und Preview-Modus
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | Aufgelöste/unaufgelöste Platzhalter, Final-Prompt und Stage-Outputs sind inspizierbar; Preview ohne LLM. |
|
||||||
|
| **Begründung** | Admin kann Prompts testen und Wertetabelle/Expertenmodus speisen. |
|
||||||
|
| **Quelle** | `debug`-Parameter; `/preview`; `UnifiedPromptModal` Test-Button; `ai_insights.metadata` |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | Debug-Daten in Responses können groß/sensibel sein; kein separates Staging. |
|
||||||
|
|
||||||
|
### 8. Wiederverwendbare Base-Prompts via Reference
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | Pipeline-Stages referenzieren Slugs statt Templates zu duplizieren. |
|
||||||
|
| **Begründung** | Ein Baustein, mehrere Workflows; zentral wartbar. |
|
||||||
|
| **Quelle** | `source == 'reference'` in `execute_pipeline_prompt()` |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | Keine Referenz-Versionierung; Änderung am Base-Prompt wirkt sofort auf alle Referenzen. |
|
||||||
|
|
||||||
|
### 9. Strukturierte LLM-Ausgaben per Output-Format
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | Pro Prompt/Prompt-Def: `output_format: text\|json`, optional `output_schema`; Pipeline-Outputs als Stage-Keys im Kontext. |
|
||||||
|
| **Begründung** | Maschinenlesbare Zwischenergebnisse für Multi-Stage und Wertetabelle. |
|
||||||
|
| **Quelle** | `validate_json_output()`; Stage `output_key` in Pipeline |
|
||||||
|
| **Tragfähigkeit** | **mittel** |
|
||||||
|
| **Einschränkung** | JSON-Schema-Validierung ist TODO (`jsonschema`); Markdown-Unwrap als Heuristik. |
|
||||||
|
|
||||||
|
### 10. Admin-only Konfiguration, User-only Ausführung
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | Schreibende Prompt-/Workflow-Operationen nur mit `require_admin`. |
|
||||||
|
| **Begründung** | Produktions-Prompts sind Systemkonfiguration, nicht Nutzerdaten. |
|
||||||
|
| **Quelle** | `require_admin` in `routers/prompts.py`; `RequireAdmin` für `/admin/prompts` |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | `/workflow-editor/:id` ohne Frontend-Admin-Gate; `/prompts/execute` ohne `check_feature_access` (Legacy-Pfad in `insights.py` hat Enforcement). |
|
||||||
|
|
||||||
|
### 11. Import/Export als Umgebungs-Sync
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | Prompt-Sätze als JSON exportierbar/importierbar (Dev→Prod). |
|
||||||
|
| **Begründung** | Konfiguration versionierbar in Git, nicht in der App-DB. |
|
||||||
|
| **Quelle** | `GET /export-all`, `POST /import` in `routers/prompts.py`; Admin-UI Buttons |
|
||||||
|
| **Tragfähigkeit** | **mittel** |
|
||||||
|
| **Einschränkung** | Kein Diff, keine Merge-Strategie, kein Rollback; Overwrite-Flag manuell. |
|
||||||
|
|
||||||
|
### 12. Workflow-Erweiterung: Graph + Fragenergänzungen + Signale
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | Workflows als Knoten/Kanten-Graph; optionale Fragen am Knoten; Normalisierung/Logic/Join als Engine-Schicht. |
|
||||||
|
| **Begründung** | Bedingte, verzweigte Analysen jenseits linearer Pipelines. |
|
||||||
|
| **Quelle** | `workflow_executor.py`; Migration 034; `question_augmenter.py` |
|
||||||
|
| **Tragfähigkeit** | **mittel** |
|
||||||
|
| **Einschränkung** | Hohe Komplexität; zwei Speicherorte (`graph_data` vs. `workflow_definitions`); Jinja2 im Workflow-Pfad zusätzlich zu `{{}}`-Resolver. |
|
||||||
|
|
||||||
|
### 13. Platzhalter-Modifier für KI-Kontext
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | `{{key\|d}}` (Wert + Beschreibung), `{{key\|x}}` (Erklärung ohne Zahl) über Katalog-Metadaten. |
|
||||||
|
| **Begründung** | Prompts können Kontext für das Modell reichhaltiger machen ohne Template-Duplikate. |
|
||||||
|
| **Quelle** | `resolve_placeholders()` Modifier-Logik; `get_placeholder_catalog()` |
|
||||||
|
| **Tragfähigkeit** | **mittel** |
|
||||||
|
| **Einschränkung** | Modifier-Syntax ad hoc; Katalog-Pflicht für sinnvolle `\|x`-Nutzung. |
|
||||||
|
|
||||||
|
### 14. System-Prompt-Reset statt DB-Versionierung
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | Shipped Prompts mit `is_system_default` + `default_template`; Admin-Reset auf Original. |
|
||||||
|
| **Begründung** | Schutz vor irreversiblen Fehlkonfigurationen ohne vollständiges Versionsmodell. |
|
||||||
|
| **Quelle** | Migration 019; `POST /{prompt_id}/reset-to-default` |
|
||||||
|
| **Tragfähigkeit** | **mittel** |
|
||||||
|
| **Einschränkung** | Nur ein Default-Snapshot; keine Historie benutzerdefinierter Änderungen. |
|
||||||
|
|
||||||
|
### 15. Governance für Platzhalter-Änderungen
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | Breaking Changes nur über Deprecation + Replacement; semantische Verträge dokumentiert. |
|
||||||
|
| **Begründung** | Prompts in Produktion brechen nicht still. |
|
||||||
|
| **Quelle** | `docs/PLACEHOLDER_GOVERNANCE.md` §4 |
|
||||||
|
| **Tragfähigkeit** | **mittel** (prozessual) |
|
||||||
|
| **Einschränkung** | Prozess in Doku, nicht im Runtime erzwungen; Checkliste verweist noch auf Legacy-Dateien. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Nicht übernehmen
|
||||||
|
|
||||||
|
Muster, die sich nicht bewährt haben oder zu produktspezifisch sind — bei Neuentwicklung vermeiden:
|
||||||
|
|
||||||
|
1. **Parallele Ausführungspfade** — Legacy `insights.py` (`/insights/run`, `/insights/pipeline`) mit eigener Template-Engine und LLM-Calls neben `prompt_executor`; Frontend-Split (`Analysis` vs. `History`).
|
||||||
|
|
||||||
|
2. **Doppelte Metadaten für Platzhalter** — `PLACEHOLDER_MAP`, Registry, `placeholder_metadata_complete.py` und Katalog-Fallbacks parallel; erzeugt Sync-Risiko (114 Keys müssen deckungsgleich bleiben).
|
||||||
|
|
||||||
|
3. **Zwei Pipeline-Modelle gleichzeitig** — `pipeline_configs` (3 fixe Stages) und Unified `type=pipeline` in `ai_prompts`; Migration 020 migriert, Tabelle bleibt aktiv.
|
||||||
|
|
||||||
|
4. **Zwei Workflow-Speicher** — `workflow_definitions.graph` und `ai_prompts.graph_data`; unklare Single Source of Truth.
|
||||||
|
|
||||||
|
5. **Roh-SQL-Kontextladung im Executor** — `execute_prompt_with_data` lädt Modul-Rohdaten per SQL, obwohl Resolver/Data Layer existieren; Domänenwissen in der Engine.
|
||||||
|
|
||||||
|
6. **Hardcodierte Execute-Defaults** — Module/Zeiträume in `/execute` fest verdrahtet statt aus Prompt-/Pipeline-Konfiguration.
|
||||||
|
|
||||||
|
7. **Fehlende Feature-Enforcement-Konsistenz** — `check_feature_access` auf Legacy-Insights, nicht auf `/prompts/execute`.
|
||||||
|
|
||||||
|
8. **„Parallel“ als sequentiell implementiert** — Pipeline-Stages kommentiert als parallel, Code sequentiell; irreführendes Modell.
|
||||||
|
|
||||||
|
9. **Unvollständige Output-Validierung** — JSON-Parse + Markdown-Unwrap, Schema-Check auskommentiert/TODO.
|
||||||
|
|
||||||
|
10. **Workflow-Editor ohne klares Admin-Gate in Routing** — Schreib-API geschützt, UI-Route für alle Authentifizierten erreichbar.
|
||||||
|
|
||||||
|
11. **Domänen-spezifische Hardcodings in der Engine** — Kategorien (`körper`, `ernährung`, …), deutsche Meta-Prompts für Generate/Optimize, Fitness-Kontext in Prompt-Generator.
|
||||||
|
|
||||||
|
12. **Kein integriertes Prompt-Versions- und Freigabemodell** — Overwrite + JSON-Export ersetzt keine Revision/Review/Publish-Pipeline.
|
||||||
|
|
||||||
|
13. **Issue #51 (Seitenzuordnung) nicht umgesetzt** — Prompt-Verfügbarkeit kontextuell nicht konfigurierbar; alles über zentrale Analyse-Seite.
|
||||||
|
|
||||||
|
14. **Globales LLM-Modell per Env** — `workflow_executor` übergibt Modell pro Call, `call_openrouter` ignoriert es; keine prompt-spezifische Modellwahl trotz API-Shape.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Verwandte Dokumentation
|
||||||
|
|
||||||
|
- Fachliche Spec: [AI_PROMPTS.md](../../functional/AI_PROMPTS.md)
|
||||||
|
- Platzhalter-Registry: [PLACEHOLDER_REGISTRY_FRAMEWORK.md](../../technical/PLACEHOLDER_REGISTRY_FRAMEWORK.md)
|
||||||
|
- Platzhalter-Governance: [PLACEHOLDER_GOVERNANCE.md](../../../../docs/PLACEHOLDER_GOVERNANCE.md)
|
||||||
|
- Issue #28 (Unified Prompt System): abgeschlossen, siehe `CLAUDE.md`
|
||||||
|
- Issue #51 (Prompt-Seitenzuordnung): [issue-51-prompt-page-assignment.md](../../../../docs/issues/issue-51-prompt-page-assignment.md)
|
||||||
|
- **Serie (abgeschlossen):** [Index](./README.md) · [Jinkendo Foundation](../README.md) · Dokumente #1–#9
|
||||||
136
.claude/docs/jinkendo-foundation/design-principles/README.md
Normal file
136
.claude/docs/jinkendo-foundation/design-principles/README.md
Normal file
|
|
@ -0,0 +1,136 @@
|
||||||
|
# Designprinzipien – Index (Jinkendo Foundation)
|
||||||
|
|
||||||
|
**Teil von:** [Jinkendo Foundation](../README.md)
|
||||||
|
|
||||||
|
**Status:** Arbeitspapier / Übergabe
|
||||||
|
**Stand:** 2026-07-04
|
||||||
|
**Zweck:** Zentraler Einstieg für die aus Mitai extrahierte Serie **tragfähiger Designprinzipien** — für neue Apps der Jinkendo-Produktfamilie oder vergleichbare Self-Hosted-PWAs.
|
||||||
|
|
||||||
|
**Nicht enthalten:** Mitai-Gesamtarchitektur, Domänenlogik (Gesundheit/Ernährung), Kairo-/Framework-Empfehlungen.
|
||||||
|
|
||||||
|
**Ablage:** `.claude/docs/jinkendo-foundation/design-principles/` · Regeln: [DOCUMENTATION.md](../../../rules/DOCUMENTATION.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Wofür diese Serie?
|
||||||
|
|
||||||
|
Mitai Jinkendo implementiert wiederkehrende **Querschnittsmuster** (Prompt-Ausführung, Data Layer, Entitlements, Registries, Auth, Import, Dashboard, Navigation, Deploy). Die neun Dokumente destillieren daraus:
|
||||||
|
|
||||||
|
- **Was** übertragbar ist (Prinzip + Begründung + Tragfähigkeit)
|
||||||
|
- **Was** bewusst nicht kopiert werden soll („Nicht übernehmen“)
|
||||||
|
- **Wo** im Code nachgeschaut werden kann (Pfade, Agent-Guides)
|
||||||
|
|
||||||
|
Jedes Dokument ist **eigenständig lesbar**; dieser Index ordnet Abhängigkeiten und Lese-Reihenfolge.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Dokumente (9/9)
|
||||||
|
|
||||||
|
| # | Modul | Datei | Kernidee (1 Satz) |
|
||||||
|
|---|-------|-------|-------------------|
|
||||||
|
| 1 | Prompt Engine | [PROMPT_ENGINE_DESIGN_PRINCIPLES.md](./PROMPT_ENGINE_DESIGN_PRINCIPLES.md) | Ein Executor, Pipeline/Workflow-Typen, Platzhalter über Registry — keine Raw-Template-Ausführung. |
|
||||||
|
| 2 | Data Layer | [DATA_LAYER_DESIGN_PRINCIPLES.md](./DATA_LAYER_DESIGN_PRINCIPLES.md) | Layer 0→1→2: Single Source of Truth für Berechnungen; Charts und KI nutzen dieselbe Schicht. |
|
||||||
|
| 3 | Feature & Entitlement | [FEATURE_ENTITLEMENT_DESIGN_PRINCIPLES.md](./FEATURE_ENTITLEMENT_DESIGN_PRINCIPLES.md) | Zentrale `check_feature_access`, DB-Registry, 4-Phasen-Rollout — Enforcement an der API. |
|
||||||
|
| 4 | Registry / Plugin (Meta) | [REGISTRY_PLUGIN_DESIGN_PRINCIPLES.md](./REGISTRY_PLUGIN_DESIGN_PRINCIPLES.md) | Drei Registry-Muster (Platzhalter, Widgets, CSV): SSoT, Validierung an der Grenze, Runtime-Registrierung. |
|
||||||
|
| 5 | Auth & Session | [AUTH_SESSION_DESIGN_PRINCIPLES.md](./AUTH_SESSION_DESIGN_PRINCIPLES.md) | Server-Sessions, `require_auth` als Depends — bekannte Schwäche: Profile-Header ohne Session-Bindung. |
|
||||||
|
| 6 | Universal Import | [UNIVERSAL_IMPORT_DESIGN_PRINCIPLES.md](./UNIVERSAL_IMPORT_DESIGN_PRINCIPLES.md) | Modul-Registry + Executor + Vorlagen; Ingest ≠ Interpretation; SAVEPOINT pro Zeile. |
|
||||||
|
| 7 | Dashboard Widgets | [DASHBOARD_WIDGETS_DESIGN_PRINCIPLES.md](./DASHBOARD_WIDGETS_DESIGN_PRINCIPLES.md) | Backend-Katalog, Profil-Layout, Config-Whitelist, Entitlements — Dual Registry mit Frontend. |
|
||||||
|
| 8 | Navigation / IA | [NAVIGATION_IA_DESIGN_PRINCIPLES.md](./NAVIGATION_IA_DESIGN_PRINCIPLES.md) | `appNav` als SSoT, Shells für tiefe Bereiche, Admin-Hub, ein Breakpoint 1024px. |
|
||||||
|
| 9 | Migration & Deploy | [MIGRATION_DEPLOY_DESIGN_PRINCIPLES.md](./MIGRATION_DEPLOY_DESIGN_PRINCIPLES.md) | `XXX_*.sql` beim Container-Start; develop/main → Dev/Prod; fail-fast, kein Auto-Rollback. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Empfohlene Lesereihenfolge
|
||||||
|
|
||||||
|
### Schnellüberblick (30 Min)
|
||||||
|
|
||||||
|
1. [REGISTRY_PLUGIN_DESIGN_PRINCIPLES.md](./REGISTRY_PLUGIN_DESIGN_PRINCIPLES.md) — Meta-Muster für viele Module
|
||||||
|
2. [DATA_LAYER_DESIGN_PRINCIPLES.md](./DATA_LAYER_DESIGN_PRINCIPLES.md) — Daten vs. Darstellung
|
||||||
|
3. [MIGRATION_DEPLOY_DESIGN_PRINCIPLES.md](./MIGRATION_DEPLOY_DESIGN_PRINCIPLES.md) — Betrieb & Schema-Evolution
|
||||||
|
|
||||||
|
### Vollständige Implementierung (neues Produkt)
|
||||||
|
|
||||||
|
```
|
||||||
|
Foundation: (9) Migration & Deploy → (5) Auth → (3) Feature & Entitlement
|
||||||
|
Daten: (2) Data Layer → (6) Universal Import
|
||||||
|
Erweiterung: (4) Registry Meta → (1) Prompt Engine → (7) Dashboard Widgets
|
||||||
|
Oberfläche: (8) Navigation / IA
|
||||||
|
```
|
||||||
|
|
||||||
|
### Nur ein Modul nachbauen
|
||||||
|
|
||||||
|
| Ziel | Lese zuerst | Dann |
|
||||||
|
|------|-------------|------|
|
||||||
|
| KI-Analysen | #1 Prompt Engine | #2 Data Layer, #4 Registry |
|
||||||
|
| Charts & KPIs | #2 Data Layer | #7 Dashboard Widgets |
|
||||||
|
| Freemium / Limits | #3 Feature & Entitlement | #7 Widgets (Gating) |
|
||||||
|
| CSV-Import | #6 Universal Import | #2 Data Layer, #4 Registry |
|
||||||
|
| Admin-PWA | #8 Navigation / IA | #3, #5 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Querschnittsthemen (über alle Docs)
|
||||||
|
|
||||||
|
| Thema | Primär | Ergänzend |
|
||||||
|
|-------|--------|-----------|
|
||||||
|
| Single Source of Truth | #2 Data Layer | #4 Registry, #7 Widget-Katalog, #6 Modul-Registry |
|
||||||
|
| Validierung an der Grenze | #4 Registry | #7 Layout-Pydantic, #6 Template-Validator |
|
||||||
|
| Feature-Gating | #3 Entitlement | #7 Widget `allowed`, #6 Import-Limits |
|
||||||
|
| Dual Registry (Backend + Frontend) | #4 Registry | #7 `registerDashboardWidgets`, Platzhalter-UI |
|
||||||
|
| Idempotenz / Recovery | #9 Migration | #6 SAVEPOINT, Migration `IF NOT EXISTS` |
|
||||||
|
| Bekannte Lücken dokumentieren | alle „Nicht übernehmen“ | z. B. IDOR Profile-Header (#5), fehlender Cross-Check Widget-IDs (#7) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Verwandte normative Docs (Mitai-spezifisch)
|
||||||
|
|
||||||
|
Diese Agent-Guides sind **Implementierungsdetail**; die Designprinzipien-Serie ist **extrahiertes Muster**:
|
||||||
|
|
||||||
|
| Thema | Agent-Guide / Spec |
|
||||||
|
|-------|-------------------|
|
||||||
|
| Platzhalter | [PLACEHOLDER_REGISTRY_FRAMEWORK.md](../../technical/PLACEHOLDER_REGISTRY_FRAMEWORK.md) |
|
||||||
|
| Data Layer erweitern | [DATA_LAYER_EXTENSION_GUIDE.md](../../technical/DATA_LAYER_EXTENSION_GUIDE.md) |
|
||||||
|
| CSV Import | [UNIVERSAL_CSV_IMPORT_AGENT_GUIDE.md](../../technical/UNIVERSAL_CSV_IMPORT_AGENT_GUIDE.md) |
|
||||||
|
| Dashboard Widgets | [DASHBOARD_WIDGETS_AGENT_GUIDE.md](../../technical/DASHBOARD_WIDGETS_AGENT_GUIDE.md) |
|
||||||
|
| GUI / Admin / Nav | [GUI_IA_ADMIN_NAV_2026-04-05.md](../../../../docs/issues/GUI_IA_ADMIN_NAV_2026-04-05.md) |
|
||||||
|
| Migrationen (operativ) | [MIGRATIONS.md](../../technical/MIGRATIONS.md) |
|
||||||
|
| Membership | [MEMBERSHIP_SYSTEM.md](../../technical/MEMBERSHIP_SYSTEM.md), [FEATURE_ENFORCEMENT.md](../../architecture/FEATURE_ENFORCEMENT.md) |
|
||||||
|
| Architektur-Regeln | [ARCHITECTURE.md](../../../rules/ARCHITECTURE.md) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Übergabe-Checkliste (neues Produkt in der Familie)
|
||||||
|
|
||||||
|
Nutze diese Liste beim Start eines Schwester-Projekts — **Prinzipien ja, Mitai-Pfade nein**:
|
||||||
|
|
||||||
|
```
|
||||||
|
[ ] Deploy: nummerierte SQL-Migrationen + Tracking-Tabelle + Startup vor App
|
||||||
|
[ ] Auth: Session server-side; Depends(require_auth); Admin-Route-Guard
|
||||||
|
[ ] Entitlements: eine check_feature_access-Funktion; keine Tier-Logik in UI-Widgets
|
||||||
|
[ ] Data Layer: Berechnungen nicht in Router/React duplizieren
|
||||||
|
[ ] Registry: neue erweiterbare IDs nur über zentralen Katalog + Validierung
|
||||||
|
[ ] Import (falls CSV): Modul-Registry + Vorlagen + Import-Grenze (keine Scores beim Insert)
|
||||||
|
[ ] Dashboard (falls konfigurierbar): Backend-Katalog + Layout-JSON + allowed-Flag
|
||||||
|
[ ] Navigation: eine appNav-SSoT; Shells für >6 Top-Level-Bereiche
|
||||||
|
[ ] Prompt/KI (falls): ein Executor; Platzhalter-Registry; kein Raw-Template an LLM
|
||||||
|
[ ] Dokumentieren: pro Modul „Nicht übernehmen“ aus Mitai-Lücken mitnehmen
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Pflege
|
||||||
|
|
||||||
|
| Aktion | Wo |
|
||||||
|
|--------|-----|
|
||||||
|
| Neues Querschnittsmodul extrahiert | Neues `*_DESIGN_PRINCIPLES.md` + Zeile in Tabelle oben |
|
||||||
|
| Mitai-Implementierung ändert Muster | Betroffenes Einzeldokument + ggf. Querschnittstabelle hier |
|
||||||
|
| Nur Mitai-Bugfix ohne Muster-Änderung | Agent-Guides / Code; Designprinzipien optional |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Changelog Index
|
||||||
|
|
||||||
|
| Datum | Änderung |
|
||||||
|
|-------|----------|
|
||||||
|
| 2026-07-04 | Index angelegt; Serie 1–9 abgeschlossen |
|
||||||
|
| 2026-07-04 | Nach `jinkendo-foundation/design-principles/` verschoben |
|
||||||
|
|
@ -0,0 +1,315 @@
|
||||||
|
# Registry- & Plugin-Muster – Designprinzipien (Extraktion)
|
||||||
|
|
||||||
|
**Status:** Analyse / Arbeitspapier
|
||||||
|
**Stand:** 2026-07-04
|
||||||
|
**Geltungsbereich:** Querschnittsmuster für erweiterbare Registries — drei Implementierungen in Mitai (Platzhalter, Dashboard-Widgets, CSV-Import)
|
||||||
|
|
||||||
|
**Serie:** Designprinzipien für Produktfamilie · Dokument 4 von n
|
||||||
|
**Vorgänger:** [FEATURE_ENTITLEMENT_DESIGN_PRINCIPLES.md](./FEATURE_ENTITLEMENT_DESIGN_PRINCIPLES.md)
|
||||||
|
|
||||||
|
**Die drei Registries:**
|
||||||
|
|
||||||
|
| Registry | Backend-Kanon | Frontend-/Runtime-Registry | Leitfaden |
|
||||||
|
|----------|---------------|----------------------------|-----------|
|
||||||
|
| **Platzhalter** | `placeholder_registry.py` + `placeholder_registrations/` | `PLACEHOLDER_MAP` in `placeholder_resolver.py` | `PLACEHOLDER_REGISTRY_FRAMEWORK.md` |
|
||||||
|
| **Dashboard-Widgets** | `widget_catalog.py` | `registerDashboardWidgets.js` → `dashboardWidgetRegistry.jsx` | `DASHBOARD_WIDGETS_AGENT_GUIDE.md` |
|
||||||
|
| **CSV-Import-Module** | `csv_parser/module_registry.py` | — (Executor + Admin-UI konsumieren API) | `UNIVERSAL_CSV_IMPORT_AGENT_GUIDE.md` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Modul
|
||||||
|
|
||||||
|
**Registry- & Plugin-Muster** (Meta-Schicht)
|
||||||
|
|
||||||
|
Wiederkehrendes Architekturmuster: **Zentral registrierte, ID-basierte Erweiterungspunkte** mit Metadaten, Validierung und getrennten Konsumenten (GUI, API, Batch). Kein einzelnes Runtime-Modul — ein **Familien-Designpattern**, das in Mitai dreimal konkret umgesetzt ist.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Fachliche Verantwortung
|
||||||
|
|
||||||
|
Registries übernehmen:
|
||||||
|
|
||||||
|
1. **Autoritative ID-Liste** — Was existiert, was ist erlaubt, in welcher Reihenfolge (optional).
|
||||||
|
2. **Metadaten für Mensch & Maschine** — Titel, Beschreibung, Typen, Abhängigkeiten, semantische Verträge.
|
||||||
|
3. **Validierung** — Unbekannte IDs werden abgelehnt; Konfigurationen gegen Kanon geprüft.
|
||||||
|
4. **Entkopplung** — Implementierung (Resolver, React-Komponente, Import-Executor) registriert sich an den Kanon, nicht umgekehrt.
|
||||||
|
5. **Erweiterbarkeit ohne Schema-Explosion** — Neue Einträge über Code-Registrierung (+ ggf. DB-Overrides), nicht über neue DB-Spalten pro Feature.
|
||||||
|
|
||||||
|
Registries übernehmen **nicht**:
|
||||||
|
|
||||||
|
- Fachliche Berechnung (→ Data Layer)
|
||||||
|
- Entitlement-Auflösung (→ Feature System; Widgets *referenzieren* Features)
|
||||||
|
- Auth / Mandanten
|
||||||
|
|
||||||
|
### Gemeinsames Strukturschema
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────┐
|
||||||
|
│ REGISTRY (Kanon) │
|
||||||
|
│ ID + Metadaten + optionale Policies/Abhängigkeiten │
|
||||||
|
└────────────────────┬────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
┌───────────────┼───────────────┐
|
||||||
|
▼ ▼ ▼
|
||||||
|
Implementierung Validierung Konsumenten
|
||||||
|
(Resolver/ (Schema/ (GUI-Picker,
|
||||||
|
Component/ Tests) API, Executor)
|
||||||
|
Executor)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Vergleich der drei Implementierungen
|
||||||
|
|
||||||
|
| Aspekt | Platzhalter | Dashboard-Widgets | CSV-Import |
|
||||||
|
|--------|-------------|-------------------|------------|
|
||||||
|
| **Primär-ID** | `key` (snake_case) | `id` (snake_case) | Modul-Name (`nutrition`, `activity`, …) |
|
||||||
|
| **Kanon-Speicher** | Python Singleton + Cluster-Module | Python-Liste `WIDGET_CATALOG` | Python-Dict `MODULE_DEFINITIONS` |
|
||||||
|
| **Metadaten-Tiefe** | Sehr hoch (22+ Felder, Evidence) | Mittel (title, description, requires_feature) | Hoch (fields, types, duplicate_key, aggregates) |
|
||||||
|
| **Runtime-Registry** | `register_placeholder()` beim Import | `registerDashboardWidget()` idempotent | Keine — Executor liest Dict |
|
||||||
|
| **Frontend-Spiegel** | PlaceholderPicker, Admin-Prompt-Modal | `ensureDashboardWidgetsRegistered()` | Admin CSV Template Editor |
|
||||||
|
| **Validierung** | `metadata.validate()`, Governance-Docs | Pydantic Layout + `validate_widget_entry_config` | `validate_field_mappings`, `validate_csv_template` |
|
||||||
|
| **Tests** | `test_placeholder_metadata.py`, … | `test_widget_catalog.py` | `test_template_validator.py`, … |
|
||||||
|
| **DB-Override** | Nein (nur Code) | Ja (`widget_feature_requirements`, Layout pro Profil) | Ja (Vorlagen, Nutzer-Mappings) |
|
||||||
|
| **Entitlements** | Indirekt (Data/Features) | `requires_feature` → `check_feature_access` | Feature-Limits beim Import |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Designprinzipien (übergreifend)
|
||||||
|
|
||||||
|
### 1. Single Source of Truth für erlaubte IDs
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | Jede erweiterbare Einheit hat **eine** autoritative ID-Liste; Router und UI duplizieren keine Feld-/Widget-/Platzhalter-Listen. |
|
||||||
|
| **Begründung** | Verhindert „funktioniert in der UI, scheitert in der API“ und umgekehrt. |
|
||||||
|
| **Quelle** | `module_registry.py` Kommentar; `widget_catalog.py`; `PLACEHOLDER_REGISTRY_FRAMEWORK.md` §2.3 |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | Platzhalter: paralleles `PLACEHOLDER_MAP` neben Registry. |
|
||||||
|
|
||||||
|
### 2. ID-Stabilität als Vertrag
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | IDs/Keys sind stabile API-Verträge; Umbenennung = neuer Key + Deprecation, nicht stilles Rename. |
|
||||||
|
| **Begründung** | Prompts, Layouts und CSV-Vorlagen referenzieren IDs persistent. |
|
||||||
|
| **Quelle** | `docs/PLACEHOLDER_GOVERNANCE.md` §4.2–4.3; Widget-Layout in `profiles.dashboard_layout` |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | Nicht überall runtime-erzwungen (Platzhalter-Governance prozessual). |
|
||||||
|
|
||||||
|
### 3. Metadaten getrennt von Implementierung
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | Registry speichert **Was** (ID, Beschreibung, Typ, Policies); Implementierung lebt in separaten Modulen. |
|
||||||
|
| **Begründung** | GUI, Export, Validierung und Docs können Metadaten nutzen ohne Resolver/Component zu laden. |
|
||||||
|
| **Quelle** | `PlaceholderMetadata` Dataclass; `WidgetCatalogEntry`; `MODULE_DEFINITIONS.fields` |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | Platzhalter bindet `_resolver_func` optional an Metadata-Objekt. |
|
||||||
|
|
||||||
|
### 4. Zwei-Phasen-Registrierung (Backend-Kanon + Runtime-Binding)
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | Phase A: Kanon definiert IDs und Metadaten. Phase B: Implementierung registriert sich (Platzhalter-Cluster-Import, `registerDashboardWidget`, Executor nutzt Modul-Def). |
|
||||||
|
| **Begründung** | Backend bleibt autoritativ; Frontend/plugins können nachziehen, Tests können Lücken finden. |
|
||||||
|
| **Quelle** | `import placeholder_registrations` in `main.py`; `ensureDashboardWidgetsRegistered()` |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | Fehlende Frontend-Registrierung zeigt „Unbekanntes Widget“ — kein Build-Time-Fail. |
|
||||||
|
|
||||||
|
### 5. Auto-Registration via Package-Import
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | Side-Effect-Import eines Packages triggert vollständige Registrierung (`placeholder_registrations/__init__.py`). |
|
||||||
|
| **Begründung** | Keine vergessene manuelle Registrierungsliste in `main.py` pro Eintrag. |
|
||||||
|
| **Quelle** | `placeholder_registrations/__init__.py`; `main.py` |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | Import-Reihenfolge und zirkuläre Imports beachten. |
|
||||||
|
|
||||||
|
### 6. Validierung am Registry-Rand
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | Unbekannte Keys/Widget-IDs/Feld-Mappings werden an Registry-Grenzen abgewiesen, nicht erst in der Business-Logik. |
|
||||||
|
| **Begründung** | Frühes, klares Fehlerbild für Admins und Entwickler. |
|
||||||
|
| **Quelle** | `DashboardWidgetEntry` + `ALLOWED_WIDGET_IDS`; `validate_field_mappings()`; `get_unknown_placeholders()` |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | Prompt-Templates können unbekannte Platzhalter erst zur Laufzeit offenbaren. |
|
||||||
|
|
||||||
|
### 7. Konsumenten-Agnostik
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | Dieselbe Registry bedient mehrere Konsumenten (KI, Charts, Export, Admin-Browser) ohne duplizierte Metadaten. |
|
||||||
|
| **Begründung** | DRY für Beschreibungen, Kategorien, Beispielwerte. |
|
||||||
|
| **Quelle** | `PLACEHOLDER_REGISTRY_FRAMEWORK.md` §2.2; `get_placeholder_catalog()` |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | Legacy-Export-Pfade mergen noch „Registry + Legacy“. |
|
||||||
|
|
||||||
|
### 8. Erweiterungs-Checkliste statt Ad-hoc
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | Jedes Registry hat dokumentierte Schritte A→G (Katalog-Eintrag, Validierung, Tests, Version-Bump). |
|
||||||
|
| **Begründung** | Agenten und Menschen erweitern konsistent; Review an Checkliste. |
|
||||||
|
| **Quelle** | `DASHBOARD_WIDGETS_AGENT_GUIDE.md` §2; `UNIVERSAL_CSV_IMPORT_AGENT_GUIDE.md` §2; `PLACEHOLDER_DEVELOPMENT_GUIDE.md` |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | Drei separate Guides — kein unified „Registry Agent Guide“. |
|
||||||
|
|
||||||
|
### 9. Tests auf Katalog-Konsistenz
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | Automatisierte Tests prüfen Eindeutigkeit, Reihenfolge, Payload-Shape, ID-Abgleich Kanon ↔ abgeleitete Sets. |
|
||||||
|
| **Begründung** | Regression wenn Katalog wächst but Registry/Layout nicht mitzieht. |
|
||||||
|
| **Quelle** | `test_widget_catalog.py`; Placeholder-Metadata-Tests |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | Kein Cross-Registry-Test „Frontend widget IDs == backend catalog“. |
|
||||||
|
|
||||||
|
### 10. Optionale Entitlement-Referenz im Kanon
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | Registry-Einträge **referenzieren** Feature-IDs (`requires_feature`), lösen Entitlements aber nicht selbst auf. |
|
||||||
|
| **Begründung** | Tier-Logik bleibt in `check_feature_access`; Katalog bleibt deklarativ. |
|
||||||
|
| **Quelle** | `WidgetCatalogEntry.requires_feature`; `dashboard_widget_entitlements.py` |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | Platzhalter haben kein direktes `requires_feature` — Gating nur indirekt. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Designprinzipien (spezifisch pro Registry)
|
||||||
|
|
||||||
|
### Platzhalter-Registry
|
||||||
|
|
||||||
|
| # | Prinzip | Tragfähigkeit | Schwäche |
|
||||||
|
|---|---------|---------------|----------|
|
||||||
|
| P1 | **Semantischer Vertrag** (`semantic_contract`) pro Key — Platzhalter sind API, nicht Prompt-Hilfe | hoch | Viele Legacy-Keys mit schwachem Vertrag |
|
||||||
|
| P2 | **Evidence-Tagging** — Herkunft jedes Metadatenfelds nachvollziehbar | mittel | Pflegeaufwand |
|
||||||
|
| P3 | **Cluster-Module** — Registrierung nach Domäne (`nutrition_part_a`, `body_metrics`, …) | hoch | 114 Keys Sync mit `PLACEHOLDER_MAP` |
|
||||||
|
| P4 | **Data-Layer-Referenz** in Metadata (`data_layer_function`) — Bindung an Layer 1 | hoch | Nicht alle Keys vollständig verknüpft |
|
||||||
|
| P5 | **Singleton** `get_registry()` — globaler Kanon | hoch | Test-Isolation braucht Disziplin |
|
||||||
|
|
||||||
|
### Dashboard-Widget-Registry
|
||||||
|
|
||||||
|
| # | Prinzip | Tragfähigkeit | Schwäche |
|
||||||
|
|---|---------|---------------|----------|
|
||||||
|
| W1 | **Backend-Katalog = ALLOWED_WIDGET_IDS** — Layout-Schema leitet ab | hoch | Frontend-Registry manuell parallel |
|
||||||
|
| W2 | **`merge_missing_catalog_widgets`** — neue Katalog-IDs erscheinen im Layout ohne User-Reset | hoch | — |
|
||||||
|
| W3 | **Strikte `config`-Validierung** nur für whitelisted Widgets (`WIDGETS_ALLOWING_CONFIG`) | hoch | Config-Schemas pro Widget heterogen |
|
||||||
|
| W4 | **Graceful Degradation** — unregistrierte ID → Fehler-Karte, nicht Crash | mittel | Maskiert Deploy-Fehler |
|
||||||
|
| W5 | **WidgetErrorBoundary** pro Instanz | hoch | — |
|
||||||
|
|
||||||
|
### CSV-Modul-Registry
|
||||||
|
|
||||||
|
| # | Prinzip | Tragfähigkeit | Schwäche |
|
||||||
|
|---|---------|---------------|----------|
|
||||||
|
| C1 | **`MODULE_DEFINITIONS` = einzige Feldliste** — Router duplizieren nicht | hoch | Activity erweitert dynamisch um `training_parameters` |
|
||||||
|
| C2 | **Deklarative Duplikat-Strategie** (`duplicate_key`, `update`/`skip`) | hoch | Modul-spezifische Executor-Sonderfälle |
|
||||||
|
| C3 | **`import_row_processing`** — Aggregation in Registry, nicht im Router | hoch | Legacy-Defaults in Modul-Def |
|
||||||
|
| C4 | **`validate_field_mappings`** vor Persistenz | hoch | Nutzer-Mappings teils ohne volle Validator-Parität (#71) |
|
||||||
|
| C5 | **Persistenz-Orchestrator** liest Registry-Felder (`activity_persistence_orchestrator`) | hoch | Nur Activity voll ausgebaut |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Anti-Pattern: Doppel-Registry
|
||||||
|
|
||||||
|
Mitai zeigt an **Platzhaltern** das Risiko explizit:
|
||||||
|
|
||||||
|
```
|
||||||
|
placeholder_registrations/ ──register──► PlaceholderRegistry (Metadata)
|
||||||
|
│ ▲
|
||||||
|
└── resolver in code ──► PLACEHOLDER_MAP (Runtime, 114 Keys)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Regel für Produktfamilie:** Runtime-Auflösung soll Metadata-Registry **lesen**, nicht spiegeln.
|
||||||
|
|
||||||
|
Widgets sind näher am Ideal: Backend `WIDGET_CATALOG` ist Kanon; Frontend muss IDs manuell in `registerDashboardWidgets.js` binden — akzeptabel, aber testbar machen (Cross-Check).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Nicht übernehmen
|
||||||
|
|
||||||
|
1. **Parallele Runtime-Maps** — `PLACEHOLDER_MAP` + Registry; eine Quelle für Keys und Resolver-Pfad.
|
||||||
|
|
||||||
|
2. **Frontend-Registry ohne Build-/Test-Gate** — fehlende `registerDashboardWidget`-Einträge erst zur Laufzeit sichtbar.
|
||||||
|
|
||||||
|
3. **Metadaten-Duplikation in Export-Code** — hardcodierte Beschreibungen außerhalb der Registry.
|
||||||
|
|
||||||
|
4. **Registry-Einträge ohne Validierungs-Tests** — besonders bei 100+ Platzhaltern.
|
||||||
|
|
||||||
|
5. **Import-Feldlisten in Routern** — alles über `module_registry` (Mitai-Zielbild, noch nicht überall).
|
||||||
|
|
||||||
|
6. **Evidence-/Metadata-Pflicht für einfache Plugins** — 22 Felder für Widgets wären Overkill; **Metadaten-Tiefe an Risiko anpassen**.
|
||||||
|
|
||||||
|
7. **Dynamische Registry aus DB ohne Versionierung** — Widget-Feature-Overrides OK; kompletter Kanon nur in DB wäre schwer testbar.
|
||||||
|
|
||||||
|
8. **Registry ohne Deprecation-Pfad** — Breaking Key-Changes still (Platzhalter-Governance existiert, durchsetzen).
|
||||||
|
|
||||||
|
9. **Entitlements in Registry auflösen** — Widgets richtig: referenzieren; nicht Tier-Logik im Katalog.
|
||||||
|
|
||||||
|
10. **Schlaf-Modul leeres `fields: {}`** — Sondermodus (`import_mode`) statt sauberem Registry-Eintrag; technische Schuld.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Entscheidungsmatrix: Welche Registry-Tiefe?
|
||||||
|
|
||||||
|
| Wenn … | dann Metadaten-Tiefe … | Beispiel |
|
||||||
|
|--------|------------------------|----------|
|
||||||
|
| Externe Verträge / KI / Compliance | Hoch (Vertrag, Evidence, Missing-Policy) | Platzhalter |
|
||||||
|
| UI-Plugin mit optionaler Config | Mittel (ID, title, config schema, feature ref) | Widgets |
|
||||||
|
| Daten-Ingest / Schema-Mapping | Hoch (Typen, keys, constraints) | CSV-Module |
|
||||||
|
| Internes Hilfsmodul | Minimal (ID + Handler-Ref) | — |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Modul-Inventar (Querschnitt)
|
||||||
|
|
||||||
|
```
|
||||||
|
backend/
|
||||||
|
├── placeholder_registry.py
|
||||||
|
├── placeholder_registrations/ # Auto-import Cluster
|
||||||
|
├── placeholder_resolver.py # PLACEHOLDER_MAP (Legacy-Spiegel)
|
||||||
|
├── placeholder_registry_export.py
|
||||||
|
├── widget_catalog.py
|
||||||
|
├── dashboard_layout_schema.py
|
||||||
|
├── dashboard_widget_config.py
|
||||||
|
├── dashboard_widget_entitlements.py
|
||||||
|
├── widget_feature_requirements_db.py
|
||||||
|
└── csv_parser/
|
||||||
|
└── module_registry.py
|
||||||
|
|
||||||
|
frontend/src/
|
||||||
|
├── widgetSystem/
|
||||||
|
│ ├── registerDashboardWidgets.js
|
||||||
|
│ └── dashboardWidgetRegistry.jsx
|
||||||
|
└── components/workflow/panels/PlaceholderPicker.jsx
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Verwandte Dokumentation
|
||||||
|
|
||||||
|
- Platzhalter: [PLACEHOLDER_REGISTRY_FRAMEWORK.md](../../technical/PLACEHOLDER_REGISTRY_FRAMEWORK.md), [PLACEHOLDER_GOVERNANCE.md](../../../../docs/PLACEHOLDER_GOVERNANCE.md)
|
||||||
|
- Widgets: [DASHBOARD_WIDGETS_AGENT_GUIDE.md](../../technical/DASHBOARD_WIDGETS_AGENT_GUIDE.md)
|
||||||
|
- Import: [UNIVERSAL_CSV_IMPORT_AGENT_GUIDE.md](../../technical/UNIVERSAL_CSV_IMPORT_AGENT_GUIDE.md)
|
||||||
|
- Entitlements (Widget-Gating): [FEATURE_ENTITLEMENT_DESIGN_PRINCIPLES.md](./FEATURE_ENTITLEMENT_DESIGN_PRINCIPLES.md)
|
||||||
|
- Data Layer (Platzhalter-Berechnung): [DATA_LAYER_DESIGN_PRINCIPLES.md](./DATA_LAYER_DESIGN_PRINCIPLES.md)
|
||||||
|
- Prompt Engine (Platzhalter-Konsument): [PROMPT_ENGINE_DESIGN_PRINCIPLES.md](./PROMPT_ENGINE_DESIGN_PRINCIPLES.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Geplante Folgedokumente (Serie)
|
||||||
|
|
||||||
|
| # | Modul | Status |
|
||||||
|
|---|-------|--------|
|
||||||
|
| 1 | Prompt Engine | ✅ |
|
||||||
|
| 2 | Data Layer | ✅ |
|
||||||
|
| 3 | Feature & Entitlement | ✅ |
|
||||||
|
| 4 | Registry-/Plugin-Muster | ✅ dieses Dokument |
|
||||||
|
| 5 | Auth & Session | ✅ `AUTH_SESSION_DESIGN_PRINCIPLES.md` |
|
||||||
|
| 6 | Universal Import | ✅ |
|
||||||
|
| 7 | Dashboard Widgets | ✅ |
|
||||||
|
| 8 | Navigation / IA | ✅ |
|
||||||
|
| 9 | Migration & Deploy | ✅ |
|
||||||
|
|
||||||
|
*Hinweis:* Dokumente 6 und 7 vertiefen Einzel-Registries; dieses Meta-Dokument ist die übergreifende Extraktion.
|
||||||
|
|
@ -0,0 +1,325 @@
|
||||||
|
# Universal CSV Import – Designprinzipien (Extraktion)
|
||||||
|
|
||||||
|
**Status:** Analyse / Arbeitspapier
|
||||||
|
**Stand:** 2026-07-04
|
||||||
|
**Geltungsbereich:** Universal CSV Import (Issue #21) — Ingest/Mapping/Persistenz, keine Auswertungslogik
|
||||||
|
|
||||||
|
**Serie:** Designprinzipien für Produktfamilie · Dokument 6 von n
|
||||||
|
**Vorgänger:** [AUTH_SESSION_DESIGN_PRINCIPLES.md](./AUTH_SESSION_DESIGN_PRINCIPLES.md)
|
||||||
|
|
||||||
|
**Kernkomponenten:**
|
||||||
|
|
||||||
|
| Bereich | Pfade |
|
||||||
|
|---------|-------|
|
||||||
|
| Modul-Kanon | `backend/csv_parser/module_registry.py` |
|
||||||
|
| Ausführung | `backend/csv_parser/executor.py` |
|
||||||
|
| Parsing/Typen | `core.py`, `type_converter.py`, `field_units.py` |
|
||||||
|
| Aggregation | `import_row_processing.py` |
|
||||||
|
| Validierung | `template_validator.py` |
|
||||||
|
| Fehler-Hints | `import_errors.py` |
|
||||||
|
| Mapping-Vorschläge | `mapping_suggest.py` |
|
||||||
|
| Nutzer-API | `backend/routers/csv_import.py` |
|
||||||
|
| Admin-Vorlagen | `backend/routers/admin_csv_templates.py` |
|
||||||
|
| Persistenz-Orchestrator | `data_layer/activity_persistence_orchestrator.py` |
|
||||||
|
| Leitfaden | `UNIVERSAL_CSV_IMPORT_AGENT_GUIDE.md` |
|
||||||
|
| Import-Grenze | `.claude/rules/ARCHITECTURE.md` §8 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Modul
|
||||||
|
|
||||||
|
**Universal CSV Import**
|
||||||
|
|
||||||
|
Konfigurierbare Pipeline: **CSV-Datei → Feld-Mapping → Typkonvertierung → (optional Aggregation) → DB-Upsert** — mit Vorlagen, Audit-Log und row-level Fehlertoleranz.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Fachliche Verantwortung
|
||||||
|
|
||||||
|
Das Modul übernimmt:
|
||||||
|
|
||||||
|
1. **Modul-Registry** — Welche Zieltabellen/Felder importierbar sind (Typen, Duplikat-Keys, Strategien).
|
||||||
|
2. **Vorlagen (Mappings)** — System-Templates (Admin) + Nutzer-Kopien (`csv_field_mappings`).
|
||||||
|
3. **Analyse** — Delimiter-Erkennung, Spalten-Signatur, Mapping-Vorschläge, Diagnose einzelner Zeilen.
|
||||||
|
4. **Ausführung** — Upsert pro Modul, `source=csv`, Statistik, `affected_ids`.
|
||||||
|
5. **Fehlertransparenz** — Row-level Errors mit `code`/`hint`; kein Silent-Fail der ganzen Transaktion.
|
||||||
|
6. **Audit** — `csv_import_log` mit Status, Counts, betroffenen IDs.
|
||||||
|
7. **Limits** — Dateigröße/Zeilen aus `system_config`; Feature-Entitlements pro Modul.
|
||||||
|
|
||||||
|
Es übernimmt **nicht**:
|
||||||
|
|
||||||
|
- Fachliche Metriken / Scores (→ Data Layer, siehe [DATA_LAYER_DESIGN_PRINCIPLES.md](./DATA_LAYER_DESIGN_PRINCIPLES.md))
|
||||||
|
- Prompt-/KI-Logik
|
||||||
|
- Vollständiger Ersatz aller Legacy-Import-Endpoints (noch parallel)
|
||||||
|
|
||||||
|
### Pipeline (Happy Path)
|
||||||
|
|
||||||
|
```
|
||||||
|
Upload CSV
|
||||||
|
→ decode_raw_bytes + resolve_effective_csv_delimiter
|
||||||
|
→ Vorlage laden (csv_field_mappings)
|
||||||
|
→ validate (optional Admin) / feature check
|
||||||
|
→ run_universal_csv_import(cur, …) // eine Transaktion
|
||||||
|
→ build_row_after_mapping (type_converter)
|
||||||
|
→ aggregate_mapped_rows (import_row_processing)
|
||||||
|
→ UPSERT / activity_persistence_orchestrator
|
||||||
|
→ csv_import_log UPDATE + increment_feature_usage
|
||||||
|
```
|
||||||
|
|
||||||
|
### Unterstützte Module (Registry)
|
||||||
|
|
||||||
|
| Modul | Zieltabelle | Besonderheit |
|
||||||
|
|-------|-------------|--------------|
|
||||||
|
| `nutrition` | `nutrition_log` | Tages-Aggregation |
|
||||||
|
| `weight` | `weight_log` | Duplikat: profile + date |
|
||||||
|
| `activity` | `activity_log` | SAVEPOINT pro Zeile; EAV via Orchestrator |
|
||||||
|
| `vitals_baseline` | `vitals_baseline` | Tages-Aggregation |
|
||||||
|
| `blood_pressure` | `blood_pressure_log` | Composite `measured_at` |
|
||||||
|
| `sleep` | `sleep_log` | Legacy-Adapter `import_mode: apple_sleep_aggregate` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Administrierte vs. code-definierte Konfiguration
|
||||||
|
|
||||||
|
| Konfiguration | Speicherort | Wer pflegt? |
|
||||||
|
|---------------|-------------|-------------|
|
||||||
|
| Zielfelder, Typen, Duplikat-Keys | `MODULE_DEFINITIONS` | Entwickler (Code) |
|
||||||
|
| System-Vorlagen | `csv_field_mappings` (`is_system=true`) | Admin (+ Migration Seeds) |
|
||||||
|
| Nutzer-Mappings | `csv_field_mappings` (`profile_id`) | Nutzer (Kopie/Anpassung) |
|
||||||
|
| `field_mappings`, `type_conversions`, `import_row_processing` | JSONB in Vorlage | Admin/Nutzer |
|
||||||
|
| Import-Limits | `system_config.csv_import` | Admin |
|
||||||
|
| Delimiter-Sniffing-Heuristik | `core.py` | Code |
|
||||||
|
| Header-Aliases (Vorschläge) | `mapping_suggest.py` | Code |
|
||||||
|
|
||||||
|
**Bewusst nicht in Routern hardcodiert:** Feldlisten, Duplikat-Logik — nur Registry + Executor.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Designprinzipien
|
||||||
|
|
||||||
|
### 1. Module Registry als Single Source of Truth
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | Alle erlaubten Zielfelder, Typen und Duplikat-Keys leben in `MODULE_DEFINITIONS` — Router duplizieren nicht. |
|
||||||
|
| **Begründung** | Admin-UI, Validator, Executor und `/api/csv/modules` bleiben synchron. |
|
||||||
|
| **Quelle** | `module_registry.py`; Agent-Guide §1 |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | Activity erweitert Felder dynamisch aus `training_parameters` (DB). |
|
||||||
|
|
||||||
|
### 2. Ingest vs. Interpretation (Import-Grenze)
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | Import: Mapping + Typ/Einheit + Duplikat/Upsert. **Keine** fachliche Auswertung beim Insert. |
|
||||||
|
| **Begründung** | Semantik gehört in Data Layer; Import bleibt austauschbar und testbar. |
|
||||||
|
| **Quelle** | `ARCHITECTURE.md` §8; [DATA_LAYER_DESIGN_PRINCIPLES.md](./DATA_LAYER_DESIGN_PRINCIPLES.md) |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | `sleep_apple_import.py` ist Legacy-Adapter mit quellenspezifischer Logik. |
|
||||||
|
|
||||||
|
### 3. Vorlagen trennen Struktur von Datei
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | `csv_field_mappings` speichert Modul, Delimiter, Header-Flag, Mappings, Conversions, Row-Processing — unabhängig vom Upload. |
|
||||||
|
| **Begründung** | Wiederverwendung (Apple Health, Omron, …); Nutzer wählt Vorlage statt jedes Mal neu zu mappen. |
|
||||||
|
| **Quelle** | Migration 042; Admin + User APIs |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | Nutzer-Kopien nicht immer durch `validate_csv_template` (#71). |
|
||||||
|
|
||||||
|
### 4. Effektives Trennzeichen aus Datei, nicht blind aus Vorlage
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | `resolve_effective_csv_delimiter` — DE-Export (`;`) vs. EN-Vorlage (`,`) wird aus Header-Feldanzahl erkannt. |
|
||||||
|
| **Begründung** | Regionale CSV-Exporte brechen sonst das gesamte Mapping (eine Spalte). |
|
||||||
|
| **Quelle** | `core.py` |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | Heuristik, kein 100%-Garant für exotische Formate. |
|
||||||
|
|
||||||
|
### 5. Typ- und Einheiten-Konvertierung deklarativ
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | `type_conversions` + `source_unit` in Vorlage; Logik in `type_converter` / `field_units`. |
|
||||||
|
| **Begründung** | kJ→kcal, Datumsformate, Dezimal-Komma ohne Code pro Quelle. |
|
||||||
|
| **Quelle** | `type_converter.py`, `field_units.py` |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | Falsche `source_unit` → DB-Overflow; `enrich_row_error` hilft nachträglich. |
|
||||||
|
|
||||||
|
### 6. Zeilen-Aggregation vor Upsert
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | `import_row_processing` (group_by + aggregates) fasst mehrere CSV-Zeilen pro logischem Tag/Datensatz zusammen. |
|
||||||
|
| **Begründung** | Ernährung/Vitals: viele Rohzeilen → ein Tageseintrag. |
|
||||||
|
| **Quelle** | `import_row_processing.py` |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | Modul-Default als Legacy-Fallback wenn Vorlage leer; Admin „Format prüfen“ kann Processing auslassen. |
|
||||||
|
|
||||||
|
### 7. Ein Cursor, eine Transaktion, SAVEPOINT pro Zeile
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | `run_universal_csv_import(cur, …)` nutzt **bestehenden** Cursor; bei Row-Fehlern SAVEPOINT + ROLLBACK TO, nicht ganze Xact abbrechen. |
|
||||||
|
| **Begründung** | PostgreSQL „transaction aborted“; partielle Imports mit Fehlerliste. |
|
||||||
|
| **Quelle** | `executor.py` (activity, vitals); `csv_import.py` SAVEPOINT `csv_import_exec` |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | Nicht alle Module gleich implementiert; Disziplin pro Modul. |
|
||||||
|
|
||||||
|
### 8. Kein verschachteltes get_db im Importpfad
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | FK-Auflösung (z. B. Trainingstyp) und Activity-Persistenz mit **demselben** `cur` wie der Import. |
|
||||||
|
| **Begründung** | Pool-Deadlocks, konsistente Transaktion. |
|
||||||
|
| **Quelle** | `_resolve_training_type_for_activity`; Agent-Guide §2 |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | Lazy-Import aus Router in Executor (Kopplung). |
|
||||||
|
|
||||||
|
### 9. Strukturierte Fehler mit Hints
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | `enrich_row_error()` mappt DB-/Parse-Fehler auf `code` + menschenlesbaren `hint`. |
|
||||||
|
| **Begründung** | Nutzer/Admin können Vorlagen korrigieren ohne PostgreSQL-Kenntnis. |
|
||||||
|
| **Quelle** | `import_errors.py`; Import-Response `error_details` |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | Heuristische String-Matches, nicht vollständig. |
|
||||||
|
|
||||||
|
### 10. Vorlagen-Validierung vor Persistenz (Admin)
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | `validate_csv_template` → `{ valid, errors[], warnings[] }`; Admin Create/Update → HTTP 422 bei Fehlern. |
|
||||||
|
| **Begründung** | Fehler früh, nicht erst beim Nutzer-Import. |
|
||||||
|
| **Quelle** | `template_validator.py`; `admin_csv_templates.py` |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | Dry-Run / User-Mappings Lücken (#71). |
|
||||||
|
|
||||||
|
### 11. System- vs. User-Mappings (Permissions)
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | `is_system=true`: nur Admin editierbar; Nutzer kopiert und passt eigene Zeile an. |
|
||||||
|
| **Begründung** | Shipped Templates schützen; Individualisierung erlauben. |
|
||||||
|
| **Quelle** | `permissions.py`; DB CHECK + Unique Indexes |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | — |
|
||||||
|
|
||||||
|
### 12. Import-Audit und Rollback-Vorbereitung
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | Jeder Lauf schreibt `csv_import_log` mit Counts, `error_details`, `affected_ids` (PKs pro Tabelle). |
|
||||||
|
| **Begründung** | Nachvollziehbarkeit, spätere Bereinigung/Rollback, Erfolgsrate pro Vorlage. |
|
||||||
|
| **Quelle** | Migration 042; `csv_import_execute` |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | Automatischer Rollback-Button nicht überall umgesetzt. |
|
||||||
|
|
||||||
|
### 13. Feature-Entitlements an Import gebunden
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | `data_import` global + modulspezifisch (`nutrition_entries`, …); Increment nur für **neue** Zeilen. |
|
||||||
|
| **Begründung** | Konsistent mit Membership-System. |
|
||||||
|
| **Quelle** | `csv_import.py` `_check_module_feature_access`, `increment_feature_usage` |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | Bulk-Increment-Schleife ineffizient (wie Feature-Doc). |
|
||||||
|
|
||||||
|
### 14. Mapping-Vorschläge (Heuristik, nicht Autorität)
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | `mapping_suggest.py` schlägt Spalten-Zuordnung aus Header-Aliases vor — Admin bestätigt. |
|
||||||
|
| **Begründung** | Schneller Editor-Start; Kanon bleibt menschlich/administrativ freigegeben. |
|
||||||
|
| **Quelle** | `_MODULE_HEADER_ALIASES` |
|
||||||
|
| **Tragfähigkeit** | **mittel–hoch** |
|
||||||
|
| **Einschränkung** | Domänenspezifische Aliases hardcodiert (DE/EN). |
|
||||||
|
|
||||||
|
### 15. Persistenz-Orchestrator für komplexe Domänen
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Prinzip** | Activity: nach Registry-Mapping → `activity_persistence_orchestrator` (Upsert + EAV + Eval-Hook). |
|
||||||
|
| **Begründung** | Gleiche Schreiblogik wie REST-API; kein divergierender CSV-Pfad. |
|
||||||
|
| **Quelle** | `activity_persistence_orchestrator.py`; [DATA_LAYER_DESIGN_PRINCIPLES.md](./DATA_LAYER_DESIGN_PRINCIPLES.md) §9 |
|
||||||
|
| **Tragfähigkeit** | **hoch** |
|
||||||
|
| **Einschränkung** | Nur Activity vollständig; andere Module direkt im Executor. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Nicht übernehmen
|
||||||
|
|
||||||
|
1. **Parallele Legacy-Import-Endpoints** — `/api/nutrition/import-csv`, `/api/activity/import-csv` neben Universal-Pfad; neue Quellen nur über Universal + Vorlage (ARCHITECTURE §8.2).
|
||||||
|
|
||||||
|
2. **Quellenspezifische Aggregat-Logik im Import** — `sleep_apple_import` als Dauerlösung; Ziel: mapping-nah + Layer 1 (Gitea #69).
|
||||||
|
|
||||||
|
3. **Feldlisten in Routern** — jede neue Spalte nur via `module_registry` + Migration.
|
||||||
|
|
||||||
|
4. **Verschachtelte DB-Connections im Executor** — Pool-Risiko; immer Caller-`cur` durchreichen.
|
||||||
|
|
||||||
|
5. **Transaktion ohne SAVEPOINT bei Multi-Row-Import** — ein Fehler killt gesamten Import + opaque „transaction aborted“.
|
||||||
|
|
||||||
|
6. **Blindes Vorlagen-Delimiter** — regionaler Export bricht Mapping.
|
||||||
|
|
||||||
|
7. **Nutzer-Mappings ohne Validierung** — #71; Copy-from-System muss durch Validator.
|
||||||
|
|
||||||
|
8. **Dry-Run ohne `import_row_processing`** — Admin „Format prüfen“ unvollständig vs. echter Import.
|
||||||
|
|
||||||
|
9. **`source`-CHECK in DB vergessen** — Import setzt `csv`, Constraint muss Migration sein.
|
||||||
|
|
||||||
|
10. **NUMERIC-Overflow durch falsche Einheit** — Schema + `source_unit` + Migrationbreite gemeinsam planen.
|
||||||
|
|
||||||
|
11. **Interpretation/Auswertung beim Import** — Scores, TDEE, Training-Quality nicht in `executor.py`.
|
||||||
|
|
||||||
|
12. **Executor-Monolith ohne Modul-Split** — `executor.py` wächst pro Modul; langfristig Executor-Strategie pro Registry-Key.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Modul-Inventar (Ist-Stand)
|
||||||
|
|
||||||
|
```
|
||||||
|
backend/csv_parser/
|
||||||
|
├── module_registry.py # MODULE_DEFINITIONS
|
||||||
|
├── executor.py # run_universal_csv_import
|
||||||
|
├── core.py # decode, delimiter, limits
|
||||||
|
├── type_converter.py
|
||||||
|
├── field_units.py
|
||||||
|
├── import_row_processing.py
|
||||||
|
├── template_validator.py
|
||||||
|
├── import_errors.py
|
||||||
|
├── mapping_suggest.py
|
||||||
|
├── permissions.py
|
||||||
|
└── sleep_apple_import.py # Legacy-Adapter
|
||||||
|
|
||||||
|
backend/routers/
|
||||||
|
├── csv_import.py # Nutzer: modules, analyze, import, mappings
|
||||||
|
└── admin_csv_templates.py # Admin: CRUD + validate
|
||||||
|
|
||||||
|
DB:
|
||||||
|
├── csv_field_mappings
|
||||||
|
└── csv_import_log
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Verwandte Dokumentation
|
||||||
|
|
||||||
|
- Agent-Guide (normativ): [UNIVERSAL_CSV_IMPORT_AGENT_GUIDE.md](../../technical/UNIVERSAL_CSV_IMPORT_AGENT_GUIDE.md)
|
||||||
|
- Registry-Meta: [REGISTRY_PLUGIN_DESIGN_PRINCIPLES.md](./REGISTRY_PLUGIN_DESIGN_PRINCIPLES.md)
|
||||||
|
- Import-Grenze: [ARCHITECTURE.md](../../../rules/ARCHITECTURE.md) §8
|
||||||
|
- Feature-Limits: [FEATURE_ENTITLEMENT_DESIGN_PRINCIPLES.md](./FEATURE_ENTITLEMENT_DESIGN_PRINCIPLES.md)
|
||||||
|
- Gitea #71: Dry-Run, User-Mapping-Validierung
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Geplante Folgedokumente (Serie)
|
||||||
|
|
||||||
|
| # | Modul | Status |
|
||||||
|
|---|-------|--------|
|
||||||
|
| 1–5 | … | ✅ |
|
||||||
|
| 6 | Universal Import | ✅ dieses Dokument |
|
||||||
|
| 7 | Dashboard Widgets | ✅ `DASHBOARD_WIDGETS_DESIGN_PRINCIPLES.md` |
|
||||||
|
| 8 | Navigation / IA | ✅ |
|
||||||
|
| 9 | Migration & Deploy | ✅ |
|
||||||
|
|
@ -20,6 +20,7 @@
|
||||||
|-----|------|----------------|
|
|-----|------|----------------|
|
||||||
| **Fachliche Spec (WAS)** | `.claude/docs/functional/` | Domäne, Use Cases, UX-Ziele, fachliche Datenarchitektur. **Keine** reine API-Parameterliste (→ technical). |
|
| **Fachliche Spec (WAS)** | `.claude/docs/functional/` | Domäne, Use Cases, UX-Ziele, fachliche Datenarchitektur. **Keine** reine API-Parameterliste (→ technical). |
|
||||||
| **Technische Spec (WIE)** | `.claude/docs/technical/` | API-, DB-, Implementierungsmuster, Agent-Guides, Migrationen. |
|
| **Technische Spec (WIE)** | `.claude/docs/technical/` | API-, DB-, Implementierungsmuster, Agent-Guides, Migrationen. |
|
||||||
|
| **Produktfamilie / Foundation** | `.claude/docs/jinkendo-foundation/` | Übertragbare Designprinzipien (nicht Mitai-Domäne); Einstieg `design-principles/README.md`. |
|
||||||
| **Architektur-Querschnitt** | `.claude/docs/architecture/` | Kurze Überblicke (z. B. Frontend-Baum), ergänzend zu technical. |
|
| **Architektur-Querschnitt** | `.claude/docs/architecture/` | Kurze Überblicke (z. B. Frontend-Baum), ergänzend zu technical. |
|
||||||
| **Arbeitspapier / Zwischenstand** | `.claude/docs/working/` | Analysen, Sessions, Migration-Notizen, **keine** langfristige Norm. Kann veraltet sein → Datum im Dokument. **Nicht** als alleinige „Wahrheit“ für Produkt zitieren. |
|
| **Arbeitspapier / Zwischenstand** | `.claude/docs/working/` | Analysen, Sessions, Migration-Notizen, **keine** langfristige Norm. Kann veraltet sein → Datum im Dokument. **Nicht** als alleinige „Wahrheit“ für Produkt zitieren. |
|
||||||
| **Audits & Matrizen** | `.claude/docs/audit/` | Zeitlich begrenzte Reviews, Reconciliation, Gitea-Vorlagen. |
|
| **Audits & Matrizen** | `.claude/docs/audit/` | Zeitlich begrenzte Reviews, Reconciliation, Gitea-Vorlagen. |
|
||||||
|
|
@ -63,6 +64,7 @@ Diese Ordner sind **kein** Ersatz für `working/` oder `docs/issues/`, wenn das
|
||||||
```
|
```
|
||||||
.claude/README.md ← Einstieg Agent/Human
|
.claude/README.md ← Einstieg Agent/Human
|
||||||
.claude/docs/README.md ← Spec-Katalog
|
.claude/docs/README.md ← Spec-Katalog
|
||||||
|
.claude/docs/jinkendo-foundation/ ← Foundation Produktfamilie (Designprinzipien)
|
||||||
.claude/docs/functional/ ← WAS
|
.claude/docs/functional/ ← WAS
|
||||||
.claude/docs/technical/ ← WIE
|
.claude/docs/technical/ ← WIE
|
||||||
.claude/docs/working/ ← Arbeitspapiere / Analysen
|
.claude/docs/working/ ← Arbeitspapiere / Analysen
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@
|
||||||
> | **Universal CSV Import** (neues Modul / Executor / Vorlagen) | **`.claude/docs/technical/UNIVERSAL_CSV_IMPORT_AGENT_GUIDE.md`** |
|
> | **Universal CSV Import** (neues Modul / Executor / Vorlagen) | **`.claude/docs/technical/UNIVERSAL_CSV_IMPORT_AGENT_GUIDE.md`** |
|
||||||
> | **GUI / IA / Admin / Nav / PWA-Leiste** | **`docs/issues/GUI_IA_ADMIN_NAV_2026-04-05.md`** |
|
> | **GUI / IA / Admin / Nav / PWA-Leiste** | **`docs/issues/GUI_IA_ADMIN_NAV_2026-04-05.md`** |
|
||||||
> | **Dashboard-Widgets** (Katalog, Registrierung, `config`) | **`.claude/docs/technical/DASHBOARD_WIDGETS_AGENT_GUIDE.md`** |
|
> | **Dashboard-Widgets** (Katalog, Registrierung, `config`) | **`.claude/docs/technical/DASHBOARD_WIDGETS_AGENT_GUIDE.md`** |
|
||||||
|
> | **Designprinzipien Produktfamilie** (Serie 1–9, Foundation) | **`.claude/docs/jinkendo-foundation/design-principles/README.md`** |
|
||||||
> | **Agent-Einstieg** | **`.claude/README.md`** |
|
> | **Agent-Einstieg** | **`.claude/README.md`** |
|
||||||
> | **Activity Session Metrics (EAV, Attributprofile)** | **`.claude/docs/technical/ACTIVITY_SESSION_METRICS_EAV_AGENT_GUIDE.md`** |
|
> | **Activity Session Metrics (EAV, Attributprofile)** | **`.claude/docs/technical/ACTIVITY_SESSION_METRICS_EAV_AGENT_GUIDE.md`** |
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -135,7 +135,7 @@ def insert_activity_from_entry(cur, profile_id: str, eid: str, e: ActivityEntry)
|
||||||
hr_avg,hr_max,hr_min,distance_km,pace_min_per_km,cadence,avg_power,elevation_gain,
|
hr_avg,hr_max,hr_min,distance_km,pace_min_per_km,cadence,avg_power,elevation_gain,
|
||||||
temperature_celsius,humidity_percent,avg_hr_percent,kcal_per_km,rpe,source,notes,
|
temperature_celsius,humidity_percent,avg_hr_percent,kcal_per_km,rpe,source,notes,
|
||||||
training_type_id,training_category,training_subcategory,created)
|
training_type_id,training_category,training_subcategory,created)
|
||||||
VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,CURRENT_TIMESTAMP)""",
|
VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,CURRENT_TIMESTAMP)""",
|
||||||
(
|
(
|
||||||
eid,
|
eid,
|
||||||
profile_id,
|
profile_id,
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,24 @@ matplotlib.use("Agg")
|
||||||
import matplotlib.pyplot as plt
|
import matplotlib.pyplot as plt
|
||||||
|
|
||||||
|
|
||||||
|
def _safe_float_series(raw: list, n: int) -> list[float]:
|
||||||
|
out: list[float] = []
|
||||||
|
for i in range(n):
|
||||||
|
if i >= len(raw):
|
||||||
|
out.append(0.0)
|
||||||
|
continue
|
||||||
|
v = raw[i]
|
||||||
|
try:
|
||||||
|
out.append(float(v) if v is not None else 0.0)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
out.append(0.0)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
def _color_to_rgb(hex_or_rgba: str) -> tuple[float, float, float]:
|
def _color_to_rgb(hex_or_rgba: str) -> tuple[float, float, float]:
|
||||||
s = (hex_or_rgba or "#333333").strip()
|
s = (hex_or_rgba or "#333333").strip()
|
||||||
|
if s.startswith("#") and len(s) >= 9:
|
||||||
|
s = s[:7]
|
||||||
if s.startswith("#") and len(s) >= 7:
|
if s.startswith("#") and len(s) >= 7:
|
||||||
try:
|
try:
|
||||||
r = int(s[1:3], 16) / 255.0
|
r = int(s[1:3], 16) / 255.0
|
||||||
|
|
@ -43,7 +59,87 @@ def chart_payload_to_png(payload: dict[str, Any], fig_width_in: float = 6.2, fig
|
||||||
else:
|
else:
|
||||||
ax.text(0.5, 0.5, "Keine Daten", ha="center", va="center", transform=ax.transAxes)
|
ax.text(0.5, 0.5, "Keine Daten", ha="center", va="center", transform=ax.transAxes)
|
||||||
|
|
||||||
elif chart_type in ("line", "bar", "scatter") and datasets:
|
elif chart_type == "dual_axis_line" and datasets and labels:
|
||||||
|
ax2 = ax.twinx()
|
||||||
|
x = list(range(len(labels)))
|
||||||
|
step = max(1, len(labels) // 8)
|
||||||
|
h1, l1 = [], []
|
||||||
|
h2, l2 = [], []
|
||||||
|
for i, ds in enumerate(datasets):
|
||||||
|
y_raw = ds.get("data") or []
|
||||||
|
if not y_raw:
|
||||||
|
continue
|
||||||
|
y = _safe_float_series(y_raw, len(labels))
|
||||||
|
lab = ds.get("label") or f"Serie {i + 1}"
|
||||||
|
col = _color_to_rgb(str(ds.get("borderColor") or ds.get("backgroundColor") or "#333333"))
|
||||||
|
y_axis = str(ds.get("yAxisID") or "").lower()
|
||||||
|
use_right = y_axis in ("right", "weight", "y1")
|
||||||
|
use_left = not use_right
|
||||||
|
target = ax if use_left else ax2
|
||||||
|
linestyle = "--" if ds.get("borderDash") else "-"
|
||||||
|
(ln,) = target.plot(
|
||||||
|
x[: len(y)],
|
||||||
|
y,
|
||||||
|
label=lab,
|
||||||
|
color=col,
|
||||||
|
linewidth=1.8,
|
||||||
|
linestyle=linestyle,
|
||||||
|
marker="o",
|
||||||
|
markersize=2 if linestyle == "-" else 0,
|
||||||
|
)
|
||||||
|
if use_left:
|
||||||
|
h1.append(ln)
|
||||||
|
l1.append(lab)
|
||||||
|
else:
|
||||||
|
h2.append(ln)
|
||||||
|
l2.append(lab)
|
||||||
|
ax.set_xticks(x[::step])
|
||||||
|
ax.set_xticklabels([labels[j] for j in range(0, len(labels), step)], rotation=25, fontsize=7)
|
||||||
|
ax.grid(True, alpha=0.25)
|
||||||
|
ax.set_xmargin(0.02)
|
||||||
|
leg = h1 + h2
|
||||||
|
leg_l = l1 + l2
|
||||||
|
if leg:
|
||||||
|
ax.legend(leg, leg_l, loc="upper right", fontsize=7)
|
||||||
|
|
||||||
|
elif chart_type == "bar" and datasets and labels:
|
||||||
|
n = len(labels)
|
||||||
|
x = list(range(n))
|
||||||
|
stack_keys_nonnull = [ds.get("stack") for ds in datasets if (ds.get("data") or []) != []]
|
||||||
|
use_stacked = (
|
||||||
|
len(datasets) >= 2
|
||||||
|
and len(stack_keys_nonnull) == len(datasets)
|
||||||
|
and all(sk for sk in stack_keys_nonnull)
|
||||||
|
and len(set(stack_keys_nonnull)) == 1
|
||||||
|
)
|
||||||
|
if use_stacked:
|
||||||
|
bottom = [0.0] * n
|
||||||
|
for ds in datasets:
|
||||||
|
y_raw = ds.get("data") or []
|
||||||
|
yv = _safe_float_series(y_raw, n)
|
||||||
|
lab = ds.get("label") or "Serie"
|
||||||
|
col = _color_to_rgb(str(ds.get("backgroundColor") or ds.get("borderColor") or "#1D9E75"))
|
||||||
|
ax.bar(x, yv, bottom=bottom, label=lab, color=col, alpha=0.9, width=0.72)
|
||||||
|
bottom = [bottom[i] + yv[i] for i in range(n)]
|
||||||
|
ax.set_xticks(x)
|
||||||
|
ax.set_xticklabels(labels, rotation=30, fontsize=7)
|
||||||
|
else:
|
||||||
|
width = 0.8 / max(len(datasets), 1)
|
||||||
|
for i, ds in enumerate(datasets):
|
||||||
|
y_raw = ds.get("data") or []
|
||||||
|
yv = _safe_float_series(y_raw, n)
|
||||||
|
offset = (i - len(datasets) / 2 + 0.5) * width
|
||||||
|
xpos = [xj + offset for xj in x]
|
||||||
|
lab = ds.get("label") or f"Serie {i + 1}"
|
||||||
|
col = _color_to_rgb(str(ds.get("backgroundColor") or ds.get("borderColor") or "#1D9E75"))
|
||||||
|
ax.bar(xpos, yv, width=width * 0.9, label=lab, color=col, alpha=0.88)
|
||||||
|
ax.set_xticks(x)
|
||||||
|
ax.set_xticklabels(labels, rotation=30, fontsize=7)
|
||||||
|
ax.legend(loc="upper right", fontsize=7)
|
||||||
|
ax.grid(True, axis="y", alpha=0.25)
|
||||||
|
ax.set_xmargin(0.02)
|
||||||
|
|
||||||
|
elif chart_type in ("line", "scatter") and datasets:
|
||||||
x = range(len(labels)) if labels else []
|
x = range(len(labels)) if labels else []
|
||||||
for i, ds in enumerate(datasets):
|
for i, ds in enumerate(datasets):
|
||||||
y = ds.get("data") or []
|
y = ds.get("data") or []
|
||||||
|
|
@ -51,31 +147,21 @@ def chart_payload_to_png(payload: dict[str, Any], fig_width_in: float = 6.2, fig
|
||||||
continue
|
continue
|
||||||
lab = ds.get("label") or f"Serie {i + 1}"
|
lab = ds.get("label") or f"Serie {i + 1}"
|
||||||
col = _color_to_rgb(str(ds.get("borderColor") or ds.get("backgroundColor") or "#1D9E75"))
|
col = _color_to_rgb(str(ds.get("borderColor") or ds.get("backgroundColor") or "#1D9E75"))
|
||||||
if chart_type == "bar":
|
ls = "--" if ds.get("borderDash") else "-"
|
||||||
yv = y[: len(labels)] if labels else y
|
ax.plot(
|
||||||
bg = ds.get("backgroundColor")
|
list(x)[: len(y)],
|
||||||
if isinstance(bg, list):
|
y,
|
||||||
cols = [_color_to_rgb(str(c)) for c in bg[: len(yv)]]
|
label=lab,
|
||||||
else:
|
color=col,
|
||||||
cols = [_color_to_rgb(str(bg or "#1D9E75"))] * len(yv)
|
linewidth=1.6,
|
||||||
ax.bar(list(range(len(yv))), yv, label=lab, color=cols[: len(yv)], alpha=0.88)
|
linestyle=ls,
|
||||||
else:
|
marker="o",
|
||||||
ax.plot(
|
markersize=2,
|
||||||
list(x)[: len(y)],
|
)
|
||||||
y,
|
if labels:
|
||||||
label=lab,
|
|
||||||
color=col,
|
|
||||||
linewidth=1.6,
|
|
||||||
marker="o",
|
|
||||||
markersize=2,
|
|
||||||
)
|
|
||||||
if labels and chart_type != "bar":
|
|
||||||
step = max(1, len(labels) // 8)
|
step = max(1, len(labels) // 8)
|
||||||
ax.set_xticks(list(x)[::step])
|
ax.set_xticks(list(x)[::step])
|
||||||
ax.set_xticklabels([labels[j] for j in range(0, len(labels), step)], rotation=25, fontsize=7)
|
ax.set_xticklabels([labels[j] for j in range(0, len(labels), step)], rotation=25, fontsize=7)
|
||||||
elif labels and chart_type == "bar":
|
|
||||||
ax.set_xticks(list(x))
|
|
||||||
ax.set_xticklabels(labels, rotation=30, fontsize=7)
|
|
||||||
ax.legend(loc="upper right", fontsize=7)
|
ax.legend(loc="upper right", fontsize=7)
|
||||||
ax.grid(True, alpha=0.25)
|
ax.grid(True, alpha=0.25)
|
||||||
ax.set_xmargin(0.02)
|
ax.set_xmargin(0.02)
|
||||||
|
|
|
||||||
|
|
@ -122,6 +122,15 @@ def _weight_series_payload(bundle_weight: dict[str, Any]) -> dict[str, Any] | No
|
||||||
"borderColor": "#378ADD",
|
"borderColor": "#378ADD",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
if any(p.get("avg14") is not None for p in series):
|
||||||
|
datasets.append(
|
||||||
|
{
|
||||||
|
"label": "Ø 14T",
|
||||||
|
"data": [safe_float(p.get("avg14")) for p in series],
|
||||||
|
"borderColor": "#085041",
|
||||||
|
"borderDash": [6, 3],
|
||||||
|
}
|
||||||
|
)
|
||||||
return {"chart_type": "line", "data": {"labels": labels, "datasets": datasets}, "metadata": {"confidence": "high"}}
|
return {"chart_type": "line", "data": {"labels": labels, "datasets": datasets}, "metadata": {"confidence": "high"}}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -145,6 +154,97 @@ def _line_payload_from_points(
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _donut_avg_to_pie_payload(donut: list[dict[str, Any]] | None) -> dict[str, Any] | None:
|
||||||
|
if not donut:
|
||||||
|
return None
|
||||||
|
labels = [str(x.get("name") or "") for x in donut]
|
||||||
|
values = [safe_float(x.get("value")) for x in donut]
|
||||||
|
colors = [str(x.get("color") or "#666666") for x in donut]
|
||||||
|
if not labels or not values or len(labels) != len(values):
|
||||||
|
return None
|
||||||
|
return {
|
||||||
|
"chart_type": "pie",
|
||||||
|
"data": {
|
||||||
|
"labels": labels,
|
||||||
|
"datasets": [{"data": values, "backgroundColor": colors}],
|
||||||
|
},
|
||||||
|
"metadata": {"confidence": "high"},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _daily_macros_stacked_bar_payload(daily_macros: list[dict[str, Any]]) -> dict[str, Any] | None:
|
||||||
|
if len(daily_macros) < 2:
|
||||||
|
return None
|
||||||
|
labels = [str(d.get("date") or "") for d in daily_macros]
|
||||||
|
return {
|
||||||
|
"chart_type": "bar",
|
||||||
|
"data": {
|
||||||
|
"labels": labels,
|
||||||
|
"datasets": [
|
||||||
|
{
|
||||||
|
"label": "Protein (g)",
|
||||||
|
"data": [safe_float(d.get("Protein")) for d in daily_macros],
|
||||||
|
"backgroundColor": "#4a8f72",
|
||||||
|
"stack": "daily",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Fett (g)",
|
||||||
|
"data": [safe_float(d.get("Fett")) for d in daily_macros],
|
||||||
|
"backgroundColor": "#6e8eb8",
|
||||||
|
"stack": "daily",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "KH (g)",
|
||||||
|
"data": [safe_float(d.get("KH")) for d in daily_macros],
|
||||||
|
"backgroundColor": "#c17d45",
|
||||||
|
"stack": "daily",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"metadata": {"confidence": "high"},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _kcal_vs_weight_dual_payload(kw: dict[str, Any]) -> dict[str, Any] | None:
|
||||||
|
pts = kw.get("points") or []
|
||||||
|
if len(pts) < 2:
|
||||||
|
return None
|
||||||
|
labels = [str(p.get("date") or "") for p in pts]
|
||||||
|
tdee = kw.get("tdee_reference_kcal")
|
||||||
|
datasets: list[dict[str, Any]] = [
|
||||||
|
{
|
||||||
|
"label": "Ø Kalorien (7T)",
|
||||||
|
"data": [safe_float(p.get("kcal_avg")) for p in pts],
|
||||||
|
"borderColor": "#EA580C",
|
||||||
|
"yAxisID": "kcal",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
if tdee is not None and float(safe_float(tdee) or 0) > 0:
|
||||||
|
td = float(tdee)
|
||||||
|
datasets.append(
|
||||||
|
{
|
||||||
|
"label": "TDEE (geschätzt)",
|
||||||
|
"data": [td] * len(labels),
|
||||||
|
"borderColor": "#888888",
|
||||||
|
"yAxisID": "kcal",
|
||||||
|
"borderDash": [5, 5],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
datasets.append(
|
||||||
|
{
|
||||||
|
"label": "Gewicht (kg)",
|
||||||
|
"data": [safe_float(p.get("weight")) for p in pts],
|
||||||
|
"borderColor": "#2563EB",
|
||||||
|
"yAxisID": "weight",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"chart_type": "dual_axis_line",
|
||||||
|
"data": {"labels": labels, "datasets": datasets},
|
||||||
|
"metadata": {"confidence": "high"},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def _append_body_bundle(story: list, styles: dict, profile_id: str, cfg: dict[str, Any]) -> None:
|
def _append_body_bundle(story: list, styles: dict, profile_id: str, cfg: dict[str, Any]) -> None:
|
||||||
days = int(cfg.get("chart_days") or 30)
|
days = int(cfg.get("chart_days") or 30)
|
||||||
bundle = get_body_history_viz_bundle(profile_id, days)
|
bundle = get_body_history_viz_bundle(profile_id, days)
|
||||||
|
|
@ -235,17 +335,27 @@ def _append_nutrition_bundle(story: list, styles: dict, profile_id: str, cfg: di
|
||||||
pl2 = charts.get("nutrition_adherence")
|
pl2 = charts.get("nutrition_adherence")
|
||||||
if pl2:
|
if pl2:
|
||||||
_add_chart_to_story(story, styles, pl2, "Ernährungs-Adherence")
|
_add_chart_to_story(story, styles, pl2, "Ernährungs-Adherence")
|
||||||
if cfg.get("show_macro_distribution_pair", False) or cfg.get("show_macro_daily_bars", False):
|
if cfg.get("show_macro_daily_bars", False):
|
||||||
|
dm = bundle.get("daily_macros") or []
|
||||||
|
pl_dm = _daily_macros_stacked_bar_payload(dm)
|
||||||
|
if pl_dm:
|
||||||
|
_add_chart_to_story(story, styles, pl_dm, "Makroverteilung täglich (g) · Fokus Protein")
|
||||||
|
|
||||||
|
if cfg.get("show_macro_distribution_pair", False):
|
||||||
|
pie_pl = _donut_avg_to_pie_payload(bundle.get("donut_avg_pct"))
|
||||||
|
if pie_pl:
|
||||||
|
_add_chart_to_story(story, styles, pie_pl, "Ø Makro-Quote (% der Makro-kcal)")
|
||||||
wm = bundle.get("weekly_macro_chart")
|
wm = bundle.get("weekly_macro_chart")
|
||||||
if isinstance(wm, dict) and wm.get("chart_type"):
|
if isinstance(wm, dict) and wm.get("chart_type"):
|
||||||
_add_chart_to_story(story, styles, wm, "Makros (wöchentlich)")
|
meta_wm = wm.get("metadata") or {}
|
||||||
|
if meta_wm.get("confidence") != "insufficient":
|
||||||
|
_add_chart_to_story(story, styles, wm, "Wöch. Makro-Verteilung (Anteil %)")
|
||||||
|
|
||||||
kw = bundle.get("kcal_vs_weight") or {}
|
kw = bundle.get("kcal_vs_weight") or {}
|
||||||
if cfg.get("show_kcal_vs_weight", False) and kw.get("points"):
|
if cfg.get("show_kcal_vs_weight", False) and kw.get("points"):
|
||||||
pts = kw["points"]
|
pl_kw = _kcal_vs_weight_dual_payload(kw)
|
||||||
if pts:
|
if pl_kw:
|
||||||
pl = _line_payload_from_points(pts, "date", "kcal", "kcal")
|
_add_chart_to_story(story, styles, pl_kw, "Kalorien (Ø 7 Tage) vs. Gewicht")
|
||||||
if pl:
|
|
||||||
_add_chart_to_story(story, styles, pl, "Kalorien vs. Zeit")
|
|
||||||
story.append(Spacer(1, 2 * mm))
|
story.append(Spacer(1, 2 * mm))
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
88
backend/scripts/audit_insert_placeholders.py
Normal file
88
backend/scripts/audit_insert_placeholders.py
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
"""Audit: INSERT %s-Platzhalter vs. Parameter-Anzahl in backend/."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import ast
|
||||||
|
import re
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
|
||||||
|
|
||||||
|
def _count_tuple_elements(node: ast.AST) -> int | None:
|
||||||
|
if isinstance(node, ast.Tuple):
|
||||||
|
return len(node.elts)
|
||||||
|
if isinstance(node, ast.List):
|
||||||
|
return len(node.elts)
|
||||||
|
if isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute):
|
||||||
|
if node.func.attr in {"values", "keys"}:
|
||||||
|
return None
|
||||||
|
if isinstance(node, ast.BinOp) and isinstance(node.op, ast.Add):
|
||||||
|
left = _count_tuple_elements(node.left)
|
||||||
|
right = _count_tuple_elements(node.right)
|
||||||
|
if left is not None and right is not None:
|
||||||
|
return left + right
|
||||||
|
if isinstance(node, ast.ListComp):
|
||||||
|
return None
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def audit_file(path: Path) -> list[str]:
|
||||||
|
issues: list[str] = []
|
||||||
|
try:
|
||||||
|
tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
|
||||||
|
except SyntaxError:
|
||||||
|
return issues
|
||||||
|
|
||||||
|
for node in ast.walk(tree):
|
||||||
|
if not isinstance(node, ast.Call):
|
||||||
|
continue
|
||||||
|
func = node.func
|
||||||
|
if not (
|
||||||
|
isinstance(func, ast.Attribute)
|
||||||
|
and func.attr == "execute"
|
||||||
|
and isinstance(func.value, ast.Name)
|
||||||
|
and func.value.id == "cur"
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
if len(node.args) < 2:
|
||||||
|
continue
|
||||||
|
sql_node, params_node = node.args[0], node.args[1]
|
||||||
|
sql = None
|
||||||
|
if isinstance(sql_node, ast.Constant) and isinstance(sql_node.value, str):
|
||||||
|
sql = sql_node.value
|
||||||
|
elif isinstance(sql_node, ast.JoinedStr):
|
||||||
|
continue
|
||||||
|
if not sql or "INSERT" not in sql.upper():
|
||||||
|
continue
|
||||||
|
if "VALUES" not in sql.upper():
|
||||||
|
continue
|
||||||
|
ph = sql.count("%s")
|
||||||
|
param_count = _count_tuple_elements(params_node)
|
||||||
|
if param_count is None:
|
||||||
|
continue
|
||||||
|
if ph != param_count:
|
||||||
|
rel = path.relative_to(ROOT)
|
||||||
|
issues.append(
|
||||||
|
f"{rel}:{node.lineno} INSERT placeholders={ph} params={param_count}"
|
||||||
|
)
|
||||||
|
return issues
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
issues: list[str] = []
|
||||||
|
for path in ROOT.rglob("*.py"):
|
||||||
|
if "venv" in path.parts or "__pycache__" in path.parts:
|
||||||
|
continue
|
||||||
|
issues.extend(audit_file(path))
|
||||||
|
if issues:
|
||||||
|
print("MISMATCHES:")
|
||||||
|
for issue in sorted(issues):
|
||||||
|
print(issue)
|
||||||
|
return 1
|
||||||
|
print("OK: keine INSERT-Platzhalter-Mismatches gefunden (statisch prüfbar).")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
49
backend/tests/test_activity_insert_sql.py
Normal file
49
backend/tests/test_activity_insert_sql.py
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
"""Regression: INSERT-Platzhalter müssen zur Wert-Tuple passen."""
|
||||||
|
|
||||||
|
from data_layer.activity_persistence_orchestrator import (
|
||||||
|
insert_activity_csv_minimal,
|
||||||
|
insert_activity_from_entry,
|
||||||
|
)
|
||||||
|
from models import ActivityEntry
|
||||||
|
|
||||||
|
|
||||||
|
class _CaptureCursor:
|
||||||
|
def __init__(self):
|
||||||
|
self.captured = {}
|
||||||
|
|
||||||
|
def execute(self, sql, params):
|
||||||
|
self.captured["sql"] = sql
|
||||||
|
self.captured["params"] = params
|
||||||
|
|
||||||
|
|
||||||
|
def test_insert_activity_from_entry_smoke_with_mock_cursor():
|
||||||
|
cur = _CaptureCursor()
|
||||||
|
entry = ActivityEntry(date="2026-07-22", activity_type="Laufen")
|
||||||
|
insert_activity_from_entry(cur, "profile-1", "entry-1", entry)
|
||||||
|
assert cur.captured["params"] is not None
|
||||||
|
assert cur.captured["sql"].count("%s") == len(cur.captured["params"])
|
||||||
|
|
||||||
|
|
||||||
|
def test_insert_activity_csv_minimal_smoke_with_mock_cursor():
|
||||||
|
cur = _CaptureCursor()
|
||||||
|
insert_activity_csv_minimal(
|
||||||
|
cur,
|
||||||
|
"profile-1",
|
||||||
|
"entry-1",
|
||||||
|
date_iso="2026-07-22",
|
||||||
|
start_time=None,
|
||||||
|
end_time=None,
|
||||||
|
activity_type="Laufen",
|
||||||
|
duration_min=45,
|
||||||
|
kcal_active=None,
|
||||||
|
kcal_resting=None,
|
||||||
|
hr_avg=None,
|
||||||
|
hr_max=None,
|
||||||
|
distance_km=None,
|
||||||
|
training_type_id=1,
|
||||||
|
training_category="cardio",
|
||||||
|
training_subcategory=None,
|
||||||
|
source="csv",
|
||||||
|
)
|
||||||
|
assert cur.captured["params"] is not None
|
||||||
|
assert cur.captured["sql"].count("%s") == len(cur.captured["params"])
|
||||||
Loading…
Reference in New Issue
Block a user