Compare commits
370 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ea7de64061 | |||
| 7265cd5a01 | |||
| 5e5f4ca8d4 | |||
| f0e581a9f5 | |||
| cd457e3ea0 | |||
| e9bf5bd1a5 | |||
| 3468b2066e | |||
| a1e4ad66df | |||
| 85fccdd093 | |||
| 19bbcdaf50 | |||
| cec96ae473 | |||
| 53f1c7161f | |||
| 89c6780294 | |||
| 3f130aa8ad | |||
| 69ce3f6975 | |||
| dccb065181 | |||
| e828a5da32 | |||
| 5bca5ef9eb | |||
| 5ed06002d9 | |||
| b8f65e04c5 | |||
| f3710ac0a1 | |||
| dbc2dfacb9 | |||
| 6ab2f20f08 | |||
| a4e73c830f | |||
| 63c99b0ec5 | |||
| d448c3191f | |||
| 8a4be795f4 | |||
| a49987408b | |||
| f36a747efa | |||
| de9fdf3ac0 | |||
| 9b4d091637 | |||
| df93da9a03 | |||
| de939481ba | |||
| 6d130a7e09 | |||
| b2fbf6b4af | |||
| ca2adbd55e | |||
| ad051c015f | |||
| b464047c3a | |||
| 7203c871fc | |||
| 480890d0c6 | |||
| 8f1dad53ab | |||
| 044ce2ee60 | |||
| f63b09fc9c | |||
| 713a344d17 | |||
| 1d94c2ebf1 | |||
| a152218c45 | |||
| 4ef3f00e6b | |||
| 3c12363b8f | |||
| 07e147bc76 | |||
| 18547613ea | |||
| 48d51c07c5 | |||
| 3b483346de | |||
| e0ddfa6ce5 | |||
| ee22b22970 | |||
| c1bf9279ad | |||
| 97efe66306 | |||
| 8d5f0b533c | |||
| 800189ff8f | |||
| 3be7606d90 | |||
| ca3a9c6fa4 | |||
| 5692931d07 | |||
| 98b279fa89 | |||
| 1e7941f57b | |||
| 0adf20c9e1 | |||
| 4724da28b1 | |||
| d4b1780193 | |||
| f2650dac57 | |||
| fad1058d54 | |||
| 9dd44ce3ca | |||
| 87f258be38 | |||
| 779e2477ba | |||
| f074a8bef0 | |||
| 0677663268 | |||
| d4e9bded23 | |||
| 7411543a97 | |||
| dd0fae4bf5 | |||
| a9a6153ed5 | |||
| 4130a63dfe | |||
| 9d52aeab67 | |||
| b68185842e | |||
| 40641594ac | |||
| e4cb491d46 | |||
| 8404a42b6c | |||
| fa10450315 | |||
| 37785135b1 | |||
| 8ee8f52e0f | |||
| 8718cf5c70 | |||
| 91dae7b614 | |||
| 20927a5969 | |||
| 7db77f4738 | |||
| 3e87f7515a | |||
| a2f60d3f46 | |||
| 30dc30c7aa | |||
| 7cfbca40bb | |||
| c294c27de8 | |||
| 50c9beb4b3 | |||
| bd5a409fa7 | |||
| 3450a9296a | |||
| 29a5db63e0 | |||
| 8d1dd59c3c | |||
| 5b73d1a1f5 | |||
| c2c736dafc | |||
| c6b8c396ad | |||
| a19ed02300 | |||
| 6db31e7312 | |||
| a34e748be5 | |||
| 16187fbbd0 | |||
| b2157d8a40 | |||
| 50aff849d8 | |||
| a0a891e550 | |||
| 9ba35dc022 | |||
| 46fae3da33 | |||
| f4196c3580 | |||
| d1d8539b42 | |||
| a8633235f2 | |||
| 5c882985e0 | |||
| 04cc77d501 | |||
| 8e68261bc1 | |||
| b0611b9f7f | |||
| 614c2dcfaa | |||
| f5c886fc13 | |||
| d019c20338 | |||
| 905bce198f | |||
| 45e3b5f4f6 | |||
| 207817376d | |||
| 128a9d752e | |||
| d7d45a8927 | |||
| fc5748bef1 | |||
| 9d880e2346 | |||
| c816e50c68 | |||
| 294740b780 | |||
| 675cfa85f0 | |||
| 4725eaa90b | |||
| 9f4678f418 | |||
| 5331eab39c | |||
| 93b8d09d05 | |||
| 0551bb3d80 | |||
| 3bf012a8f4 | |||
| e22266a18c | |||
| d58db3d5dd | |||
| cdeddc7cec | |||
| 2148d0aa7f | |||
| 69f238d9b8 | |||
| f9e295bce0 | |||
| 888d0bd009 | |||
| 1942585546 | |||
| a28a9d399a | |||
| 9be69ace5c | |||
| 286c36e9d7 | |||
| 294b09a5d9 | |||
| e5291256d0 | |||
| 4d36bbf634 | |||
| e4451e1362 | |||
| a1b85cd865 | |||
| 7245bbb1da | |||
| 5f67c01cef | |||
| 4720d70af0 | |||
| 13a1d3a060 | |||
| 7f62b6ceee | |||
| 9b3f594007 | |||
| 5d308b20ba | |||
| 1d698e4b0a | |||
| 57c464c9f6 | |||
| a7a428745f | |||
| 2d187447bb | |||
| 2de4c0b7c9 | |||
| 34966b9e84 | |||
| 9a0cf7f823 | |||
| 78c6c51520 | |||
| 5200895a73 | |||
| 8f8bdf6d8b | |||
| f67bf280c3 | |||
| 732b322c52 | |||
| d42eb3ac52 | |||
| e382b6ed35 | |||
| a4548f5587 | |||
| 9d122d4808 | |||
| 9c3494a7ea | |||
| 9353909fda | |||
| 5a8a212f40 | |||
| f9df2d31db | |||
| ab612a5335 | |||
| b2f77ca627 | |||
| 39b1fd04f0 | |||
| 9020e5eb16 | |||
| 46feb4c867 | |||
| 3067b2e6a8 | |||
| 1e2fdeeb0f | |||
| 728b37ad5f | |||
| 8afdd811db | |||
| 4588ef4c7e | |||
| 6e6270b717 | |||
| 14b005e9b8 | |||
| ef4dd93324 | |||
| 7450c269a5 | |||
| e50c18f92e | |||
| d19a1061d8 | |||
| 99a5fccaa5 | |||
| cb868373f4 | |||
| 472cf1afdb | |||
| 0cb0e81d27 | |||
| 6a9351874f | |||
| 734d943d73 | |||
| 16eaf839e7 | |||
| 295c7e7efc | |||
| c9175bd2fd | |||
| f15aa7c415 | |||
| 1684892bcb | |||
| 4fee5a2b47 | |||
| 82705f0c3e | |||
| a51f794945 | |||
| 7693139242 | |||
| 55d87d8d17 | |||
| 623af621b4 | |||
| 949a77fe38 | |||
| 0275f76432 | |||
| bc1790bd82 | |||
| b35a5ae216 | |||
| 8c07cf36ee | |||
| 7d2661a8e8 | |||
| 0fdee610ed | |||
| f1c470a8a3 | |||
| 736656bde8 | |||
| e441f59bff | |||
| c3eb5a62c4 | |||
| 79e748b470 | |||
| 88c4201f80 | |||
| 6e1cc62065 | |||
| 76cc81a385 | |||
| bd9cfaa6e4 | |||
| a4f11a8225 | |||
| 5e5350d5ac | |||
| 73ac2218c7 | |||
| 352237bbb9 | |||
| 4cf7133bce | |||
| c182ced7cd | |||
| 5338871f36 | |||
| 3005f1cb3e | |||
| 72e8f31cff | |||
| 73975d3402 | |||
| 4902771772 | |||
| c2efbee4ee | |||
| 514b64682c | |||
| a0a0be8bef | |||
| 613fedfaff | |||
| 2e761161ef | |||
| 0a203aaf75 | |||
| f50e9db523 | |||
| 749c185e3d | |||
| 214f90d39b | |||
| 0d34c0a73d | |||
| ae51d201bc | |||
| 220a16429c | |||
| e759076a6c | |||
| 8175e239b4 | |||
| 8f5af49a6f | |||
| e7dc6a6cd3 | |||
| e09a2284e9 | |||
| b0faa4bfab | |||
| a1a3f2e0a1 | |||
| 45bc049c0d | |||
| e4e362b0a9 | |||
| 300d916fad | |||
| 1631bd2e02 | |||
| 639392e133 | |||
| 2e105a99b8 | |||
| 4235246cd7 | |||
| 57a8957c93 | |||
| d153a22545 | |||
| 930a786315 | |||
| 9da29a2231 | |||
| b06d026dd0 | |||
| 32ba008660 | |||
| 657fcc241a | |||
| c69edc6952 | |||
| 789b640ad0 | |||
| 14cf8a1a53 | |||
| ea4c1f87f6 | |||
| 2fa1db55fd | |||
| 75ddd06d6a | |||
| 597486bef1 | |||
| ebad8025f4 | |||
| c7650cac2f | |||
| 4b2848c7c3 | |||
| 255fa45e90 | |||
| 7043addd15 | |||
| 1c268555f6 | |||
| 81d1e9bdfd | |||
| 85163ad440 | |||
| 502dddd3b3 | |||
| 00edc7a93d | |||
| d50bed428b | |||
| 49adb395dd | |||
| 3214055531 | |||
| 81fd7d9b3b | |||
| 3898e8bc2c | |||
| d3ddc52118 | |||
| 79dabbca5a | |||
| ed15f73727 | |||
| a8942a9e4e | |||
| 5dc93d9a8c | |||
| 805ad3c5a5 | |||
| 13efce6e36 | |||
| cf9932990e | |||
| 38d84ecdf6 | |||
| ce63d46cf4 | |||
| 435da7f17a | |||
| 4e654e50c0 | |||
| 12fd3926b2 | |||
| 919910d52a | |||
| 3dc4c9c79e | |||
| 8a9f9f960f | |||
| f4f5642c21 | |||
| 05042ee9ec | |||
| e96951728d | |||
| bfaf532ab2 | |||
| e41908af73 | |||
| c1243651bb | |||
| 59d53d6154 | |||
| 98edb282ed | |||
| 81b9e8f601 | |||
| 4c974620d8 | |||
| 04663e090a | |||
| 56fc6d853d | |||
| 5cf61289ec | |||
| 9af28faa35 | |||
| 34e93101f1 | |||
| bb4d927090 | |||
| 8dd748e7d9 | |||
| bacba311ae | |||
| 24bf3f7035 | |||
| 0dbcd4175c | |||
| 2f7e1e50ad | |||
| 60709df615 | |||
| 3c0e63757c | |||
| ee54f8380f | |||
| f79f83e8f9 | |||
| fb8837574e | |||
| 1ce6d929ce | |||
| 1640fe6045 | |||
| 61baf26da6 | |||
| 0edc86e05a | |||
| b2b7bd423d | |||
| 56e952f084 | |||
| 6586d3b68b | |||
| fff30b49e1 | |||
| f544975a6c | |||
| 4bc24b4caf | |||
| 42aec79ad1 | |||
| 9ac8200b41 | |||
| 000e78e976 | |||
| 34235ef46d | |||
| 8cda5c27ec | |||
| aff3020b13 | |||
| 456ead72b6 | |||
| 030eb41429 | |||
| 5db8f8588c | |||
| 8992c300f1 | |||
| b9adf6da84 | |||
| 80936b226d | |||
| 8bed0199b6 | |||
| cfab5c2d69 | |||
| 8261fa4420 | |||
| 75d6a40817 | |||
| d7ed0c0e9b | |||
| d73ed13f87 | |||
| 6abc911e94 | |||
| 28ca64b5b4 | |||
| fc33bfbdeb | |||
| be0385922d |
|
|
@ -1,21 +1,25 @@
|
|||
# Shinkan Jinkendo - Projekt-Status
|
||||
|
||||
**Stand:** 2026-05-08
|
||||
**Version (Code):** 0.8.64 (`backend/version.py`, APP_VERSION)
|
||||
**DB-Schema-Version:** `20260508049` (`backend/version.py`, DB_SCHEMA_VERSION)
|
||||
**Stand:** 2026-05-14
|
||||
**Version (Code):** 0.8.140 (`backend/version.py`, APP_VERSION)
|
||||
**DB-Schema-Version:** `20260515063` (`backend/version.py`, DB_SCHEMA_VERSION)
|
||||
**Branch:** develop
|
||||
|
||||
---
|
||||
|
||||
## Executive Summary
|
||||
|
||||
**Aktueller Meilenstein (Medien):** Das **Medien-Archiv** (`media_assets` + `exercise_media.media_asset_id`) ist **produktiv nutzbar**: zentrale Bibliothek **`/media`** (Kacheln/Liste, Filter inkl. Lifecycle, Suche/Tags, Copyright, Bulk-Lifecycle und Bulk-PATCH), **Verknüpfung aus dem Archiv** in der Übungsbearbeitung (`POST …/media/from-asset`), **deduplizierter Speicher** unter **`library/…`** (Vereinsordner aus Name, Medienkind-Unterordner, Governance-Umzug bei Sichtbarkeitsänderung), **Papierkorb & Lifecycle** (Reaktivierung, Soft-Trash, Superadmin-Purge), plus **Inline-Medien im Rich-Text** (Modal-Picker, Größenwahl, Drag&Drop mit Auto-Scroll, Vorschau-/Rückweg-UX). **Governance:** Sichtbarkeit **`official`** nur noch **Superadmin** (Übungen und Medien); Plattform-Admin wie Trainer für Vereins-/Private-Inhalte. **Vereinsübungen** mit Datei-Assets: **Copyright-Pflicht** (API/UI). **Aktiver Verein:** Dropdown, Profilfeld `active_club_id`, Header `X-Active-Club-Id` und `effective_club_id` sind nach **0.8.59** synchronisiert (inkl. Plattform-Admin ohne Header beim ersten Request).
|
||||
**Aktueller Meilenstein (Medien):** Das **Medien-Archiv** (`media_assets` + `exercise_media.media_asset_id`) ist **produktiv nutzbar**: zentrale Bibliothek **`/media`** (Kacheln/Liste, Filter inkl. Lifecycle, Suche/Tags, Copyright, Bulk-Lifecycle und Bulk-PATCH), **Verknüpfung aus dem Archiv** in der Übungsbearbeitung (`POST …/media/from-asset`), **deduplizierter Speicher** unter **`library/…`**, **Papierkorb & Lifecycle**, plus **Inline-Medien im Rich-Text** (Modal-Picker, Größenwahl, Drag-and-Drop mit Auto-Scroll). **Governance:** Sichtbarkeit **`official`** nur **Superadmin** (Übungen und Medien). **Vereinsübungen** mit Datei-Assets: **Copyright-Pflicht** (API/UI). **Aktiver Verein:** Dropdown, Profilfeld `active_club_id`, Header `X-Active-Club-Id` und `effective_club_id` sind nach **0.8.59** synchronisiert.
|
||||
|
||||
**Parallel weiter relevant:** **Trainingsrahmenprogramm** (036–037), **Progressionsgraph** (032–034) — siehe **`TRAINING_FRAMEWORK_SPEC.md`**.
|
||||
**Melde- und Transparenzpfad (P-13, seit 0.8.87 ff.):** **Inhaltsmeldungen** mit Workflow im Posteingang, Club-Admin-Beteiligung für Vereinsmedien, Legal-Hold-Anbindung, Badges in der Medienbibliothek; Folgepakete P-14–P-16 bewusst offen (siehe `docs/HANDOVER.md`).
|
||||
|
||||
**Referenz:** [`library/FEATURES_DELIVERED_2026-Q2.md`](library/FEATURES_DELIVERED_2026-Q2.md) §12 · Medien-Norm: [`technical/MEDIA_ASSETS_AND_ARCHIVE_SPEC.md`](technical/MEDIA_ASSETS_AND_ARCHIVE_SPEC.md) (inkl. **§11 Inline-Medien**, umgesetzt)
|
||||
**Plattform-Rechtstexte (P-01, 0.8.95–0.8.96):** Admin-Editor mit **Abschnitts- und Vollvorschau** (Markdown); fortlaufende Abschnittsnummerierung in der Anzeige/PDF (Darstellung, nicht DB-persistent).
|
||||
|
||||
**Nächste Schritte — Medien & Archiv** (Stand 2026-05-08, für **neue Session**):
|
||||
**Parallel weiter relevant:** **Trainingsplan Phasen & Streams** (Migration **063**, Coach + Planung **0.8.137–0.8.140**; Handover **`docs/HANDOVER.md`** §3); **Trainingsrahmenprogramm** (036–037), **Progressionsgraph** (032–034) — siehe **`TRAINING_FRAMEWORK_SPEC.md`**. **Planungs-KI Progressionsgraph** (Roadmap-first, Auto-Optimierung, Katalog-Kontext **0.8.233**): Ist-Doku **`docs/architecture/PLANNING_PROGRESSION_GRAPH_KI.md`**, Handover **`docs/HANDOVER.md`** §2.8.
|
||||
|
||||
**Referenz:** [`library/FEATURES_DELIVERED_2026-Q2.md`](library/FEATURES_DELIVERED_2026-Q2.md) Abschnitt 12 · Medien-Norm: [`technical/MEDIA_ASSETS_AND_ARCHIVE_SPEC.md`](technical/MEDIA_ASSETS_AND_ARCHIVE_SPEC.md) (inkl. **Abschnitt 11 Inline-Medien**, umgesetzt) · **Fachlicher Nutzerüberblick:** [`../../docs/FACHLICHE_NUTZERFUNKTIONEN.md`](../../docs/FACHLICHE_NUTZERFUNKTIONEN.md)
|
||||
|
||||
**Nächste Schritte — Medien & Archiv** (Stand 2026-05-12, für **neue Session**):
|
||||
|
||||
1. ~~**Übung → `official` Promotion** inkl. Medien-Anhebung + **Copyright-Pflicht** bei `official` (Spec §4.2)~~ — umgesetzt (0.8.47).
|
||||
2. ~~**Eigenständige Medienmanager-Seite**~~ — **Basis umgesetzt** (`/media`); Ausbau nach Bedarf: Quotas, feinere Bulk-Workflows, Sichtbarkeits-PATCH in der UI vereinheitlichen.
|
||||
|
|
@ -23,15 +27,17 @@
|
|||
4. **S3 / externes Backend** hinter Speicher-Abstraktion (Spec §7) — nach stabiler Nutzung lokaler/NAS-Pfade.
|
||||
5. **Inline-Medien im Fließtext (Spec §11)** — **Basis umgesetzt (0.8.60–0.8.64)**: Platzhalter-Syntax, zentraler Renderer, Modal-Picker, Drag&Drop + Auto-Scroll; offen: weitere UX-Politur und ggf. strategischer Umbau auf reine Asset-Referenz (separat zu entscheiden).
|
||||
|
||||
**Inline:** verbindliche Leitplanken **`MEDIA_ASSETS_AND_ARCHIVE_SPEC.md` §11**; Umsetzung aktiv im Produktpfad (RTE + Anzeige).
|
||||
**Inline:** verbindliche Leitplanken **`MEDIA_ASSETS_AND_ARCHIVE_SPEC.md`** Abschnitt 11; Umsetzung aktiv im Produktpfad (RTE + Anzeige).
|
||||
|
||||
---
|
||||
|
||||
**Nächste Schritte (Auszug — Planung/Rahmen):**
|
||||
**Nächste Schritte (Auszug — Planung/Rahmen & Kombination):**
|
||||
|
||||
1. Kalender‑UI: „Aus Rahmen übernehmen“ an **`from-framework-slot`** anbinden; ggf. Bulk.
|
||||
2. Governance: Sichtbarkeit **club/official** für Rahmen so ausprägen, dass andere Trainer kopieren dürfen (Policy + API).
|
||||
3. Optional Backlog Graph: Alternativgruppen / bessere Visualisierung (**§4**).
|
||||
4. **Breakout / Coaching (Arbeitspaket):** Backend-Konsistenz `phases`↔`sections`, Run-UI vs. Spec (Stream-Tabs), Vorlagen phasenfähig, E2E-Smoke — siehe **`docs/HANDOVER.md`** (Tabelle „Coaching & Breakout“).
|
||||
5. **Kombinationsübungen / Coach (Fachspez § 10.6):** Coach **Stufe B/C** (archetypgesteuerte Durchführung); **Archetyp-Verwaltung** jenseits Code-Konstanten; **Massen-Vorbelegung** aller Slot-Zeit/Anzahl-Felder; **serverseitige** Validierung Profil ↔ Archetyp — siehe `TRAINING_MODULES_IMPLEMENTATION_PLAN.md` (Pakete **4e–4g**) und `COMBINATION_TIMING_PROFILE_PLAN.md`.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -77,7 +83,9 @@ Die exakten Zahlen hängen von der Umgebung ab (siehe Admin/DB). Die Skills/Übu
|
|||
- [x] **Varianten** (CRUD, Reorder, Voraussetzung) + Anzeige im Detail
|
||||
- [x] **Progressionsgraph zwischen Übungen** (Bibliotheks-Container, Kanten, Sequenz-Bulk, Varianten-Knoten — Zwischenstand, siehe TRAINING_FRAMEWORK_SPEC §4)
|
||||
- [x] Medien (Upload/Embed, rollenabhängige Größenlimits)
|
||||
- [x] Suche & Filter (Multi-Filter, Chips, Fokus beim Suchen)
|
||||
- [x] Suche & Filter (Multi-Filter, Chips, Fokus beim Suchen; **Freigabelevel** als UI-Begriff für `visibility`)
|
||||
- [x] **Übungsformular:** Registerkarten (Stammdaten … Medien & Mehr), kompakte Chip-Editoren, Varianten-Speichern über Aktionsleiste
|
||||
- [x] **Fähigkeiten-Intensität** ohne Primär-Flag (`niedrig`/`mittel`/`hoch`; Backend `is_primary` immer false)
|
||||
- [x] Exercise Blocks (Bausteine)
|
||||
- [x] Saved Searches (wo implementiert)
|
||||
|
||||
|
|
@ -87,6 +95,8 @@ Die exakten Zahlen hängen von der Umgebung ab (siehe Admin/DB). Die Skills/Übu
|
|||
- [x] **Optionale Zuordnung einer Übungsvariante** pro Eintrag (`exercise_variant_id`)
|
||||
- [x] **Trainingsrahmenprogramm Bibliothek** (Ziele, Slots, Kontext) + **Slot‑Blueprints** in `training_units` (036–037)
|
||||
- [x] **Materialisierung** aus Rahmen‑Slot (`POST …/training-units/from-framework-slot`; UI‑Anbindung optional)
|
||||
- [x] **Phasenmodell & parallele Streams** pro Einheit (Migration **063**): `training_unit_phases`, `training_unit_parallel_streams`; GET mit **`phases`** + flachen **`sections`**; PUT mit **`phases`** (App **0.8.137–0.8.140**)
|
||||
- [x] **Coaching-Modus** für Breakout: Timeline mit Split-Wahl, Rejoin vor Ganzgruppe/nächstem Split, Nachbereitung speichern → Plan & Ablauf (`TrainingCoachPage`, `trainingPlanUtils.js`)
|
||||
- [ ] Kalender-View / erweiterte Roadmap (Backlog)
|
||||
|
||||
**MediaWiki Import:**
|
||||
|
|
@ -96,6 +106,7 @@ Die exakten Zahlen hängen von der Umgebung ab (siehe Admin/DB). Die Skills/Übu
|
|||
**Skills-System:**
|
||||
|
||||
- [x] Hierarchisches Schema, Fokusbereich-Zuordnung, Exercise-Skill mit Levels
|
||||
- [x] **Gewichtetes Fähigkeiten-Profil (Phase 3):** Module, Rahmenprogramme, Regressionspfade; Peer-Kontext getrennt; Listen-Filter + Discovery — **`technical/SKILL_SCORING_SPEC.md`**
|
||||
|
||||
**Admin-UI:**
|
||||
|
||||
|
|
@ -150,17 +161,19 @@ Deployment der oben genannten Migrationen und Datenabgleich nach internem Prozes
|
|||
|
||||
| Dokument | Pfad | Stand | Status |
|
||||
|----------|------|-------|--------|
|
||||
| Lieferliste Q2 2026 | `library/FEATURES_DELIVERED_2026-Q2.md` | 2026-05-08 | ✅ Aktualisiert (§12 Medien inkl. Inline 0.8.60–0.8.64) |
|
||||
| Fachliche Nutzerfunktionen (Design/Product) | `docs/FACHLICHE_NUTZERFUNKTIONEN.md` | 2026-05-14 | Phasen/Coach/Rejoin |
|
||||
| Lieferliste Q2 2026 | `library/FEATURES_DELIVERED_2026-Q2.md` | 2026-05-14 | §11a Breakout |
|
||||
| Trainingsrahmen + Graph | `technical/TRAINING_FRAMEWORK_SPEC.md` | 2026-05-05 | ✅ §2 Blueprint |
|
||||
| Anforderungen (Index) | `functional/SHINKAN_REQUIREMENTS.md` | 2026-04-27 | ✅ Neu |
|
||||
| Anforderungen (Index) | `functional/SHINKAN_REQUIREMENTS.md` | 2026-05-12 | Verweis Nutzerüberblick |
|
||||
| Database Schema | `technical/DATABASE_SCHEMA.md` | 2026-05-07 | ✅ Hinweis 040–046 Medien (Kurz) |
|
||||
| Domain Model | `functional/DOMAIN_MODEL.md` | 2026-05-07 | ✅ Abschnitt Medien-Archiv |
|
||||
| Domain Model | `functional/DOMAIN_MODEL.md` | 2026-05-14 | Parallele Streams Ist 063 |
|
||||
| API Übungen | `technical/EXERCISES_API_SPEC.md` | 2026-05-08 | ✅ Medien/Inline-Workflow ergänzt |
|
||||
| Frontend Routing | `technical/EXERCISES_FRONTEND_ROUTING.md` | 2026-04-30 | ✅ Ergänzt UI-Hinweise |
|
||||
| Search & Filter | `technical/SEARCH_FILTER_SPEC.md` | 2026-04-27 | ✅ Aktualisiert (Liste UX) |
|
||||
| Media Upload | `technical/MEDIA_UPLOAD_SPEC.md` | 2026-05-07 | ✅ Verweis Archiv/Inline |
|
||||
| Medien-Archiv & Lifecycle | `technical/MEDIA_ASSETS_AND_ARCHIVE_SPEC.md` | 2026-05-08 | ✅ Ist-Changelog + §11 Inline erweitert |
|
||||
| Projektstatus | `PROJECT_STATUS.md` | 2026-05-08 | ✅ auf 0.8.64 aktualisiert |
|
||||
| Parallele Streams (Fach/Technik) | `functional/PARALLEL_TRAINING_STREAMS_CONCEPT.md`, `technical/PARALLEL_TRAINING_STREAMS_SPEC.md` | 2026-05-14 | Ist-Stand P1 teils |
|
||||
| Projektstatus | `PROJECT_STATUS.md` | 2026-05-14 | Keyset, KPIs, Breakout/Coach Kurzverweis |
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -171,4 +184,4 @@ Deployment der oben genannten Migrationen und Datenabgleich nach internem Prozes
|
|||
|
||||
---
|
||||
|
||||
**Letzte Aktualisierung:** 2026-05-08 (Inline-/Medien-Workflow 0.8.60–0.8.64 konsolidiert)
|
||||
**Letzte Aktualisierung:** 2026-05-14 (Version 0.8.140, DB 063, Handover Coaching/Breakout)
|
||||
|
|
|
|||
100
.claude/docs/functional/AI_EXERCISE_ASSISTANT_VISION.md
Normal file
100
.claude/docs/functional/AI_EXERCISE_ASSISTANT_VISION.md
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
# KI-Unterstützung bei Übungen – Produkt-Vision
|
||||
|
||||
**Version:** 0.1
|
||||
**Datum:** 2026-05-22
|
||||
**Status:** Zielbild / Anforderungsgrundlage (nicht gleich Ist-Spec – technische Schnittstellen: **`technical/KI_FEATURES_SPEC.md`**, **`technical/AI_PROMPT_SYSTEM_SPEC.md`**, **`technical/AI_TRAINING_PLANNING_CONCEPT.md` §1.1**)
|
||||
**Zielgruppe:** Product, Trainer-UX, später Admin-Werkzeuge
|
||||
|
||||
---
|
||||
|
||||
## 1. Übergeordnete Prinzipien
|
||||
|
||||
1. **Immer Vorschlag, nie blind überschreiben**
|
||||
Die KI liefert **Vorschläge** (Änderungen, Ergänzungen, Strukturen). Bestehende Inhalte werden **nicht** still ersetzt. Übernahme erfolgt durch den Nutzer: **teilweise** (Felder/Stellen/Blöcke) oder **komplett** („Vorschlag gesamt akzeptieren“).
|
||||
|
||||
2. **Granulare Anforderung im Editor**
|
||||
Innerhalb einer Übung soll KI-Unterstützung **feldbasiert oder bereichsbasiert** auslösbar sein (z. B. nur „Anleitung schärfen“, nur „Fähigkeiten“, nur „Variantenrahmen“) **oder** als **Komplettüberarbeitung** mit klarem Warnhinweis (Umfang/transparenter Diff).
|
||||
|
||||
3. **Nachweisliche Herkunft**
|
||||
Übernommene KI-Inhalte werden technisch dort abgebildet, wo bereits vorgesehen (z. B. **`summary_ai_generated`**, **`exercise_skills.ai_suggested`**) und um analogen Hinweis für weitergehende Textfelder/Varianten **erweitert**, sobald Implementierung konkret wird.
|
||||
|
||||
---
|
||||
|
||||
## 2. Funktionsbereiche (Vision)
|
||||
|
||||
### 2.1 Von der Idee zur kompletten Übung („Zielausbau“)
|
||||
|
||||
**Einstieg minimal:** Kurzbeschreibung oder Stichwort, **Ziel** („was soll erreicht werden?“), wenige **Rahmenparameter** (z. B. Fokusbereich, Trainingszeit, Teilnehmerzahl, Alter, Platzausstattung, Sicherheitshinweise – konkrete Dropdowns/Freifelder in UX später festlegen).
|
||||
|
||||
**KI-Aufgabe:** aus diesem dünnen Kontext einen **übernehmbaren Entwurf** einer **ganzen Übung** erzeugen: Titel‑Vorschlag, Ziel-/Durchführungstext, Sicherheit/Organisation, ggf. Trainerhinweise – **immer als Vorschlagspaket**, nicht als Speicher ohne Bestätigung.
|
||||
|
||||
**Abgrenzung:** Kombinationsübungen / komplexe Methodenprofile können **phasenweise** später einbezogen werden (Verweis Fachspez Trainingsmodule).
|
||||
|
||||
### 2.2 Anleitung (Durchführung / „Ausführung“) maximal hilfreich
|
||||
|
||||
**Ziel:** Die **Ausführungs-/Anleitungsbereiche** sollen sich **didaktisch klar**, **teilbar** und **wieder verwendbar** lesen – ohne den Trainer zu entmindigen.
|
||||
|
||||
**KI-Aufgabe:** Überarbeitungsvorschlag für Struktur (nummerierte Schritte, Zeiten pro Block, häufige Fehler, Progressionshinweise innerhalb der Übung wo sinnvoll). **Selektiver** Aufruf: nur diese Felder oder nur ein markierter Abschnitt (wenn UX Textauswahl unterstützt).
|
||||
|
||||
### 2.3 Kurzbeschreibung (`summary`)
|
||||
|
||||
**KI-Aufgabe:** Aus den **relevanten Übungstexten** eine **Liste-/Karte-taugliche** Kurzfassung generieren — wie in **`KI_FEATURES_SPEC.md`** beschrieben, mit **Ablehnen / Bearbeiten / Übernehmen**.
|
||||
|
||||
### 2.4 Einordnung – primär **Fähigkeiten**
|
||||
|
||||
**KI-Aufgabe:** automatische Erkennung und **Zuordnung** zum **globale Skills-Katalog** inklusive:
|
||||
|
||||
- **Intensität** (`exercise_skills`)
|
||||
- **Skill-Level**: `required_level` / `target_level` nach **kanonischen Slugs** (Backend-konform)
|
||||
- **`is_primary`** / Priorisierung wo fachlich sinnvoll
|
||||
|
||||
**Prompt-Kontext für Qualität:** Stammfelder wie `skills.description`, **`karate_relevance`**, **`relevance_level`**, **`focus_areas`**, optional **`skill_level_definitions`** nur für eine **kurze Kandidatenliste** (zweite Runde möglich) – keine vollständigen Romane für den gesamten Katalog auf einmal.
|
||||
|
||||
### 2.5 Varianten (optional, später prioritär erwägenswert)
|
||||
|
||||
**Vision:** Aus Ziel-/Durchführungstext **mehrere sinnvolle Ausprägungen** als **Übungsvarianten** vorschlagen oder einzelne erzeugen (**progression**, **Schwierigkeit**, andere Paararbeit, Gerätevariation) mit **übernehmbarem** Datenmodell gleich dem bestehenden `exercise_variants`.
|
||||
|
||||
**Randbedingungen:** Validierung gegen Übungstyp (Kombinationsübungen ohne Varianten laut Produktstand), keine Halluzination fremder IDs.
|
||||
|
||||
---
|
||||
|
||||
## 3. Kontextbezug später: Nachbearbeitung aus der Trainingsplanung
|
||||
|
||||
**Vision:** Hinweise aus der **Nachbearbeitung** einer Trainingseinheit (Ist‑Minuten, Trainer-Notizen, Abweichungen „was lief nicht?“ – je nach Datenmodell) fließen **optional** als Kontext in eine **erneute KI-Überarbeitung der betroffenen Übung** ein („Übung aus den Erfahrungen der Gruppe verbessern“).
|
||||
|
||||
**Konsequenz technisch später:** Zugriffsrechte, Mandant, keine unzulässige Verknüpfung personenbezogener Sportlerdaten; Aggregation auf **Einheit-/Gruppe** und **bereits dokumentierte Trainer-Insights**.
|
||||
|
||||
---
|
||||
|
||||
## 4. Admin: Massenverarbeitung und Analyse
|
||||
|
||||
**Vision für Plattform-/Vereins-Admins:**
|
||||
|
||||
| Thema | Richtungsziel |
|
||||
|-------|----------------|
|
||||
| **Massenverarbeitung** | Batch: z. B. Zusammenfassungen nachziehen, fehlende Skills vorschlagen, einheitlicher Stil bei importiertem Bestand — immer mit **Review-Queue**, nicht ohne menschliche Freigabe skalierungskritisch. |
|
||||
| **Analyse / Qualität** | Werkzeugkasten oder Berichte: **welche Übungen** sollten überarbeitet werden? z. B. leere/kurze `summary`, fehlende `goal`/`execution`, **fehlende oder widersprüchliche Skill-Zuordnung**, Import-Herkunft ohne Plausibilität, Kombi-Slots unvollständig, sehr alte Imports. |
|
||||
| **Lückenkarten** | Z. B. Abgleich gegen **Skill-Discovery**/Profil-Analysen („keine Übung deckt Fähigkeit X ab“ auf gewähltem Korpus); Verbindung zu **`skill-discovery`** entscheidend später im Detail (kein automatischer Rewrite ohne Policy). |
|
||||
|
||||
**Governance:** Sichtbarkeit (`official`, Verein), Rechte (**Superadmin** vs. Vereinsinhalt), Audit der KI-Anwendung bei Massenjobs.
|
||||
|
||||
---
|
||||
|
||||
## 5. Phasierung (überarbeitungsfähig)
|
||||
|
||||
| Phase | Inhalt |
|
||||
|-------|--------|
|
||||
| **P0** | KI-Service + Prompts aus DB + **Suggestion-only** UX; Kern: **Summary** + **Skills** (wie Spec-Minimum), **ein Feld / Komplettpaket mit Diff** nach UX. |
|
||||
| **P1** | **Anleitung überarbeiten** + **„von Idee zur Übung“** (Zielausbau) mit Rahmenparameter-Form |
|
||||
| **P2** | **Variantenvorschläge** mit strenger Validation |
|
||||
| **P3** | **Planungs-/Nachbereitungskontext** |
|
||||
| **P4** | **Admin** Massen-/Analyse (Queue + Reports + Governance) |
|
||||
|
||||
---
|
||||
|
||||
## 6. Offene Produkt-/Fachfragen
|
||||
|
||||
- Minimaler **Parameterbau** beim Zielausbau (Pflicht vs. optional).
|
||||
- Umgang mit **Medien**/Inline-Verweisen beim KI-Text – nichts zerstören, Platzhalter erhalten (siehe Medien-Spec §11).
|
||||
- **Kombinationsübungen:** welche Teilaspekte dürfen KI anfassen?
|
||||
- Limits: **Tokens**, **Rate-Limits**, Kostenüberwachung pro Verein/global.
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
# Shinkan Jinkendo - Fachliches Domänenmodell
|
||||
|
||||
**Version:** 0.4.4
|
||||
**Stand:** 2026-05-08 (Medien-Archiv **`media_assets`** / Bibliothek **`/media`** im Ist; **Inline-Medien** im Fließtext umgesetzt — `MEDIA_ASSETS_AND_ARCHIVE_SPEC.md` §11)
|
||||
**Version:** 0.4.6
|
||||
**Stand:** 2026-05-14 (Fachlicher Nutzerüberblick: `docs/FACHLICHE_NUTZERFUNKTIONEN.md`)
|
||||
**Basis:** `shinkan_anforderungsdokument_entwurf.md` + Fähigkeitsmatrix
|
||||
|
||||
---
|
||||
|
|
@ -57,7 +57,7 @@ Haupt-Kategorie (KARATE / ALLGEMEINE)
|
|||
- Selbstverteidigung ✓
|
||||
- Gewaltschutz ✓
|
||||
|
||||
**Technische Umsetzung:** M:N Beziehungen mit `is_primary` Flag.
|
||||
**Technische Umsetzung:** M:N-Beziehungen mit optionalem `is_primary`-Flag bei **Fokusbereichen, Stilrichtungen, Trainingsstilen und Zielgruppen** — nicht bei `exercise_skills` (dort nur Intensität `niedrig|mittel|hoch`).
|
||||
|
||||
### 3. Hierarchischer Kontext (§8.1)
|
||||
|
||||
|
|
@ -407,10 +407,9 @@ skill_level_definitions (
|
|||
- Reaktion (Koordination, target_level: 2, intensity: mittel)
|
||||
|
||||
**Attribute pro Fähigkeitsbezug:**
|
||||
- is_primary (Haupt- oder Nebenfähigkeit)
|
||||
- intensity (niedrig/mittel/hoch)
|
||||
- required_level (Voraussetzung, 1-5)
|
||||
- target_level (Ziel-Level, 1-5)
|
||||
- `intensity` — Nutzeneinschätzung: **niedrig | mittel | hoch** (Standard **mittel**)
|
||||
- `required_level` / `target_level` — Stufen-Spanne (kanonische Slugs basis … optimierung)
|
||||
- `is_primary` — Legacy-Feld; **nicht mehr in der UI**, beim Speichern immer false; Scoring ignoriert es
|
||||
|
||||
**🆕 Fokusbereich-Filterung:**
|
||||
- Bei Übungen mit Fokusbereich "Karate" sollten primär KARATE-Fähigkeiten zugeordnet werden
|
||||
|
|
@ -466,6 +465,8 @@ skill_level_definitions (
|
|||
|
||||
**Fachliche Grenze aktuell:** Mehrere gleichwertige „Pakete“ paralleler Alternativen sind **modellierbar** (mehrere ausgehende Kanten), aber noch **nicht** über eine dedizierte „Alternativgruppe“ in der UI trivial pflegbar; siehe `technical/TRAINING_FRAMEWORK_SPEC.md` §4.
|
||||
|
||||
**KI-Planung (Workbench, Stand 0.8.233):** Am Graph können Trainer neben Kanten ein **`planning_roadmap`**-Artefakt (Curriculum-Stufen) und **`planning_catalog_context`** (Primärfokus, Stilrichtung, Trainingsstil, Zielgruppe aus den Katalog-Dimensionen §1) pflegen. Die Roadmap-first-Pipeline matcht Übungen pro Stufe; Didaktik und Reihenfolge kommen aus Roadmap + QS, nicht aus Technik-Hardcoding. **Geplant (H1):** Katalog-Dimensionen zusätzlich als **Prompt-Snippets** in LLM-Aufrufen (Priorität Primärfokus → Trainingsstil → Zielgruppe → Stilrichtung) — **`docs/architecture/PLANNING_CATALOG_PROMPT_SNIPPETS.md`**. Technische Details: **`docs/architecture/PLANNING_PROGRESSION_GRAPH_KI.md`**. Für **Trainingsplanung** (Einheit, Abschnitt, Rahmen-Slot) gelten dieselben Katalog- und Retrieval-Bausteine mit anderen Scopes — Phase G, siehe Roadmap **`PLANNING_KI_ROADMAP.md`**.
|
||||
|
||||
### Trainingsrahmen‑Vorlage (Rahmenprogramm, CURR‑002 Stufe 2 / CURR‑009)
|
||||
|
||||
**Abgrenzung:** Eine **einzeilige** Trainingsplan‑Mikrovorlage (`training_plan_template`) strukturiert **eine** Einheit; das **Rahmenprogramm** ist eine **eigene Bibliotheksentität** mit **sortierten Session‑Slots**, **mindestens einem** formulierten **Entwicklungsziel** (Zielliste, **CURR‑011**) und einem **vollständigen Ablauf** pro Slot (**`training_unit_sections` + `training_unit_section_items`** wie bei geplanten Einheiten — **CURR‑010** inhaltlich, technisch seit **037** identisch zur Planungsstruktur). Der persistierte **Progressionsgraph** zwischen Übungen bleibt **optional** (**CURR‑013**).
|
||||
|
|
@ -474,7 +475,43 @@ skill_level_definitions (
|
|||
|
||||
**Konkretisierung (037/API):** `POST /api/training-units/from-framework-slot` legt eine geplante Einheit aus dem Slot‑Blueprint an; **`origin_framework_slot_id`** dient als Herkunftsreferenz (**Lineage light**; weiteres Feedback/Lineage‑Konzept: Konzeptpapier Schritt **E**).
|
||||
|
||||
---
|
||||
### Trainingsmodul (Bibliothek)
|
||||
|
||||
**Abgrenzung:** Wiederverwendbare **Übungsfolge** (`training_modules` + `training_module_items`) — kein Kalendertermin, kein Rahmen-Slot. Übernahme in geplante Einheiten über Planung (`apply-training-module`).
|
||||
|
||||
**Governance:** wie andere Bibliotheksartefakte (`visibility`, `club_id`, `library_content_visibility_sql`).
|
||||
|
||||
### Gewichtetes Fähigkeiten-Profil (Planungs-Bausteine, Phase 3)
|
||||
|
||||
**Zweck:** Aus den verknüpften Übungen eines Planungsartefakts wird ein **Fähigkeiten-Profil** berechnet (Trainingsgewicht je Fähigkeit). Trainer vergleichen Bausteine **innerhalb desselben Typs**, um z. B. das passendste Modul für eine Ziel-Fähigkeit zu finden.
|
||||
|
||||
**Artefakttypen (getrennte Peer-Kontexte):**
|
||||
|
||||
| Typ | Vergleich |
|
||||
|-----|-----------|
|
||||
| `training_module` | nur sichtbare **Module** |
|
||||
| `framework_program` | nur sichtbare **Rahmenprogramme** |
|
||||
| `progression_graph` | nur sichtbare **Regressionspfade** |
|
||||
|
||||
**Metriken (Nutzer):**
|
||||
|
||||
- **Score / Gewicht** — absolut (Dauer × Häufigkeit × Intensität × Stufen-Spanne)
|
||||
- **Prozent** — Anteil am stärksten sichtbaren Peer **desselben Typs** für diese Fähigkeit (max. 100 %)
|
||||
- **★** — stärkster Peer in diesem Kontext
|
||||
|
||||
**UI:** Profile in Editoren; KPI-Kacheln und Filter in Listen (`/planning/framework-programs`, `/planning/training-modules`); Discovery auf der Fähigkeiten-Seite.
|
||||
|
||||
**Technik:** `backend/skill_scoring.py`, `routers/skill_profiles.py` — Spec **`technical/SKILL_SCORING_SPEC.md`**.
|
||||
|
||||
### Parallele Trainingsstreams (Breakout)
|
||||
|
||||
**Fachlich:** Eine Kalender‑**Einheit** kann aus **Phasen** bestehen — z. B. gemeinsamer Block, dann **beliebig viele parallele** „Teilstrecken“ (**Streams**) mit je eigenem Miniplan (Abschnitte/Übungen), erneut gemeinsamer Block. Das ist **nicht** dasselbe wie ein **Rahmenprogramm‑Slot** (Serien‑Session über Wochen): Slots strukturieren **mehrere Einheiten** in einem Programm; **Streams** strukturieren **gleichzeitige** Abläufe **innerhalb einer** Einheit.
|
||||
|
||||
**Sonderfall Stationen:** Rotation kann **innerhalb** einer Stream‑Planung über **Kombinationsübungen** (Methodenprofil/Archetyp) abgebildet werden; hallenweit **synchron** getaktete Rotation ist eine **erweiterte** Ausbaustufe (siehe Fachkonzept).
|
||||
|
||||
**Umsetzung (2026-05, Migration 063, App 0.8.137 ff.):** Tabellen **`training_unit_phases`** und **`training_unit_parallel_streams`**; **`training_unit_sections`** mit **`phase_id`** und **`parallel_stream_id`** (exakt eine Zuordnung pro Sektion). **`GET /api/training-units/:id`** liefert **`phases`** (verschachtelt) und flache **`sections`**. **Coaching** und **Durchführung** nutzen dieselbe Phasenlogik im Frontend (`trainingPlanUtils.js`).
|
||||
|
||||
**Dokumentation:** `functional/PARALLEL_TRAINING_STREAMS_CONCEPT.md`, Umsetzung `technical/PARALLEL_TRAINING_STREAMS_SPEC.md`.
|
||||
|
||||
## Medien-Archiv & Übungs-Anhänge (Stand 2026-05-07)
|
||||
|
||||
|
|
@ -482,7 +519,7 @@ skill_level_definitions (
|
|||
- **`exercise_media`:** Verknüpfung **Übung ↔ Asset** (`media_asset_id`) oder **Embed** ohne Asset; Felder wie `context` (`ablauf` \| `detail` \| `trainer_hint`), Sortierung, Primär-Medium.
|
||||
- **`platform_media_storage`:** Konfiguration effektiver Medienwurzel (Superadmin, relativ zu `MEDIA_ROOT`).
|
||||
- **Produkt:** Medienbibliothek **`/media`**; in der Übungsbearbeitung Upload, Entfernen der Verknüpfung, **Aus Archiv verknüpfen**; Governance **`official`** und Copyright-Regeln wie in der Norm beschrieben.
|
||||
- **Geplant:** **Inline-Verweise** in Fließtextfeldern auf dieselbe Verknüpfung (`exercise_media.id`) — **`MEDIA_ASSETS_AND_ARCHIVE_SPEC.md` §11**, **`docs/HANDOVER.md`** §5.
|
||||
- **Inline-Verweise** in Fließtextfeldern: **`MEDIA_ASSETS_AND_ARCHIVE_SPEC.md` §11**, **`docs/HANDOVER.md`** §5.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -640,12 +677,13 @@ skill_level_definitions (
|
|||
- [ ] Level-Definitionen aus Fähigkeitsmatrix extrahieren (optional)
|
||||
- [ ] Skills-Beschreibungen aus Wiki importieren (Migration 024)
|
||||
- [ ] Admin-UI für Fähigkeiten-Kategorien (CRUD)
|
||||
- [ ] Skill-Filter in Übungssuche integrieren
|
||||
- [x] Skill-Filter in Übungssuche (SkillTreeMultiSelect + Stufen)
|
||||
- [x] Gewichtetes Fähigkeiten-Profil für Planungs-Bausteine (Module, Rahmen, Pfade) — siehe `technical/SKILL_SCORING_SPEC.md`
|
||||
- [ ] Reifegradmodelle definieren (Kombination Fokusbereich + Stil + Zielgruppe)
|
||||
- [ ] KI-Unterstützung für Trainingsplanung (basierend auf Fähigkeiten-Level)
|
||||
|
||||
---
|
||||
|
||||
**Letzte Aktualisierung:** 2026-04-27
|
||||
**Letzte Aktualisierung:** 2026-05-20
|
||||
**Verantwortlich:** Claude Code
|
||||
**Review:** Pending
|
||||
|
|
|
|||
114
.claude/docs/functional/PARALLEL_TRAINING_STREAMS_CONCEPT.md
Normal file
114
.claude/docs/functional/PARALLEL_TRAINING_STREAMS_CONCEPT.md
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
# Parallele Trainingsstreams (Breakout) — Fachkonzept
|
||||
|
||||
**Status:** MVP-Umsetzung **teilweise** (Code) · **Stand:** 2026-05-14
|
||||
**Ziel:** Planung und Durchführung von Training mit **phasenweise gemeinsamem** Ablauf und **beliebig vielen parallelen Teilstrecken** (Breakout-Sessions), inkl. Sonderfall **rotierende Stationen**.
|
||||
|
||||
**Technische Ausarbeitung:** `.claude/docs/technical/PARALLEL_TRAINING_STREAMS_SPEC.md`
|
||||
**Domänenbegriffe (Überblick):** `.claude/docs/functional/DOMAIN_MODEL.md` (Abschnitt Parallele Streams)
|
||||
|
||||
---
|
||||
|
||||
## 1. Ausgangslage und Problem
|
||||
|
||||
In Kinder- und Breitensport-Training ist ein typischer Ablauf:
|
||||
|
||||
1. **Gemeinsam:** Aufwärmen, Koordination, Ansagen.
|
||||
2. **Getrennt:** Kinder in mehrere Gruppen teilen; **Co-Trainer** leiten jeweils eigene Inhalte **gleichzeitig**.
|
||||
3. **Gemeinsam:** Abschluss, gemeinsame Übungen, Verabschiedung.
|
||||
|
||||
Die aktuelle Shinkan-Planung modelliert pro Termin **eine lineare Folge von Abschnitten und Übungen** pro Einheit. Das genügt nicht, wenn **mehrere gleichzeitige „Unter-Sessions“** mit unterschiedlichen Plänen dokumentiert und auf der Matte geführt werden sollen.
|
||||
|
||||
---
|
||||
|
||||
## 2. Ziele (fachlich)
|
||||
|
||||
| ID | Ziel |
|
||||
|----|------|
|
||||
| PT‑01 | Eine **Kalender-Einheit** bleibt **ein** Termin (eine Halle, eine Gruppe, ein Datum) — kein Splitten in künstlich mehrere Kalendereinträge nur für Parallelität. |
|
||||
| PT‑02 | **Unbegrenzte** Anzahl paralleler **Streams** (Teilstrecken) in einer oder mehreren **Parallelphasen**. |
|
||||
| PT‑03 | **Phasenmodell:** klar erkennbar **Gemeinsam** vs. **Parallel** vs. wieder **Gemeinsam** (auch mehrfach hintereinander möglich). |
|
||||
| PT‑04 | **Rollen:** Leitung (Haupttrainer) und Co-Trainer; Zuordnung der Co-Trainer **soll** an konkrete Streams anschließbar sein (heute: nur flache Liste pro Einheit — siehe technische Spec). |
|
||||
| PT‑05 | **Sonderfall Stationen:** rotierender Ablauf (z. B. Wechsel alle 20 Min.) **inhaltlich** unterscheiden zwischen (a) Rotation **innerhalb** einer Teilstrecke und (b) **synchron** getakteter Hallen-Rotation — siehe §5. |
|
||||
| PT‑06 | **Durchführung:** Trainer können „ihre“ Spur auf dem Gerät abarbeiten; Fortschritt pro Spur nachvollziehbar. |
|
||||
|
||||
**Nicht-Ziel (frühe Stufen):** Echtzeit-Synchronisation mehrerer Geräte; individuelles Athleten-Tracking; automatische Raumbelegung.
|
||||
|
||||
---
|
||||
|
||||
## 3. Begriffe
|
||||
|
||||
| Begriff | Definition |
|
||||
|---------|------------|
|
||||
| **Einheit / Termin** | Geplante `training_unit` für Gruppe und Datum — übergeordneter Rahmen des Abends. |
|
||||
| **Phase** | Organisatorischer Block innerhalb der Einheit: entweder **ganze Gruppe** oder **parallel**. |
|
||||
| **Stream / Teilstrecke** | Innerhalb einer Parallelphase: eine von N **gleichzeitig** stattfindenden Unter-Abläufen mit **eigenem** Miniplan (Abschnitte, Übungen, Notizen — analog heutiger Planung). |
|
||||
| **Synchronisationspunkt** | Fachlich: alle treffen sich wieder (Beginn einer **Gemeinschaftsphase** nach Parallelität). |
|
||||
| **Station (Rotation)** | Inhaltlicher Fokus oder Platz, den Teilnehmer **wechselnd** anlaufen; kann als Kombinations-/Zirkellogik oder als koordinierter Hallenrhythmus modelliert werden (§5). |
|
||||
|
||||
**Abgrenzung „Rahmenprogramm-Slot“:** Ein Slot im **Rahmenprogramm** ist eine **Session in einer Serie** (z. B. Woche 1 vs. Woche 2), **nicht** „Teilgruppe A gleichzeitig mit Teilgruppe B in derselben Stunde“. Parallele Streams sind **innerhalb einer Einheit**, orthogonal zum Rahmen-Slot.
|
||||
|
||||
**Abgrenzung **Kombinationsübung**:** Eine Kombi-Übung bündelt **mehrere Einzelübungen** mit Methodenprofil (Archetyp, ggf. Rotation) **in einem Plan-Item**. Sie ersetzt **nicht** mehrere Trainer mit **jeweils eigenem Gesamtablauf**, kann aber **pro Stream** für Stationslogik genutzt werden.
|
||||
|
||||
---
|
||||
|
||||
## 4. Szenarien
|
||||
|
||||
### 4.1 Klassischer Breakout
|
||||
|
||||
30 Min. gemeinsam → 25 Min. drei parallele Streams (Gruppe an Matte / an Schlagsack / Fußarbeit) → 15 Min. gemeinsam.
|
||||
|
||||
### 4.2 Viele Kinder, mehrere Co-Trainer
|
||||
|
||||
Haupttrainer plant die Gesamtstruktur; jeder Co-Trainer sieht in der Durchführung primär die zugewiesene Teilstrecke.
|
||||
|
||||
### 4.3 Rollierendes Stationssystem
|
||||
|
||||
Alle Gruppen arbeiten an **verschiedenen Schwerpunkten** und **wechseln** nach festem Intervall die Station — entweder **nur innerhalb einer Spur** oder **hallenweit synchron** (offene fachliche Präzisierung in MVP vs. später, §5).
|
||||
|
||||
---
|
||||
|
||||
## 5. Sonderfall: Stationen und Kombinationsübungen
|
||||
|
||||
### 5.1 Variante A — Rotation innerhalb einer Teilstrecke
|
||||
|
||||
Eine Teilgruppe rotiert durch mehrere Übungen (Zeit oder Runden). Das liegt nah an einer **Kombinationsübung** mit Archetyp z. B. „Zirkel / zeitgesteuerte Rotation“ und Parametern (Wechselintervall). **Empfehlung:** Diese Variante über **bestehendes** Kombinationsübungs-Konzept in der jeweiligen **Stream-Planung** abbilden (`planning_method_profile`).
|
||||
|
||||
### 5.2 Variante B — Synchron getaktete Hallen-Rotation
|
||||
|
||||
Alle Streams (oder alle Kinder insgesamt) **wechseln gleichzeitig** zur nächsten Station; Startstation kann pro Teilgruppe **versetzt** sein. Das ist **organisatorisch** schwerer: es braucht entweder **Phasen-Metadaten** (globaler Takt) oder eine explizite **Rot/Matrix**. **Empfehlung:** In einer **zweiten Ausbaustufe** abbilden; MVP kann bei Variante A starten, sofern fachlich ausreichend.
|
||||
|
||||
---
|
||||
|
||||
## 6. Rollen und Verantwortlichkeiten
|
||||
|
||||
- **Leitungstrainer:** Hält den Faden, startet Gemeinschaftsphasen, koordiniert Parallelbeginn/-ende (fachlich; ggf. später UI-Hinweise).
|
||||
- **Co-Trainer:** Verantwortlich für **zugeteilte** Streams; Zuordnung soll **pro Stream** möglich werden (Erweiterung gegenüber reiner Einheits-Co-Trainer-Liste).
|
||||
|
||||
---
|
||||
|
||||
## 7. Offene fachliche Entscheidungen
|
||||
|
||||
1. **MVP Umfang:** Reicht **freie Parallelität** ohne **synchronen** Hallenwechsel (Variante B)?
|
||||
2. **Dauer:** Sollen Phasen oder Streams **Soll-Minuten** tragen (nur Anzeige vs. später Timer)?
|
||||
3. **Vorlagen:** Müssen `training_plan_templates` parallel-fähig werden **vor** oder **mit** der ersten Implementierung?
|
||||
4. **Sichtbarkeit:** Dürfen alle Co-Trainer alle Streams sehen, oder „nur meine Spur“?
|
||||
|
||||
---
|
||||
|
||||
## 9. Umsetzungsstand (kurz, 2026-05-14)
|
||||
|
||||
- **Erreicht:** Datenmodell Phasen/Streams (**063**), API **GET/PUT** mit **`phases`**, Planungs-Breakout-UI, Durchführung und Coach nutzen dieselbe Phasen-/Stream-Logik im Frontend (`trainingPlanUtils.js`). **Synchronisationspunkt** fachlich umgesetzt: vor nächster Ganzgruppenphase oder nächstem Split erscheint im Coach die **Rejoin-Karte** (mehrere Streams), sofern nicht am absoluten Planende.
|
||||
- **Noch offen:** vollständige **Persistenz-Konsistenz** bei nachträglich geänderten Sektionen, **Vorlagen** mit Phasen, **Trainer pro Stream** in der UI, ggf. **Stream-Tabs** in der Durchführungsansicht wie in §5.2 skizziert — siehe **`docs/HANDOVER.md`** (Arbeitspaket-Tabelle).
|
||||
|
||||
---
|
||||
|
||||
## 10. Verwandte Dokumente
|
||||
|
||||
| Dokument | Bezug |
|
||||
|----------|--------|
|
||||
| `technical/TRAINING_FRAMEWORK_SPEC.md` | Rahmen-Slots = Serien-Sessions, **nicht** Intra-Einheit-Parallelität |
|
||||
| `technical/TRAINING_MODULES_AND_COMBINATION_EXERCISES_SPEC.md` | Kombinationsübungen, Archetypen, Stationslogik **im Item** |
|
||||
| `functional/Shinkan Trainingsmodule Kombinationsuebungen Spezifikation V2.md` | Fachliche Tiefe Kombi |
|
||||
| `docs/FACHLICHE_NUTZERFUNKTIONEN.md` | Nutzerüberblick |
|
||||
| `docs/HANDOVER.md` | Ist-Stand Coach, offene Breakout-Punkte |
|
||||
| `technical/DATABASE_SCHEMA.md` | Aktueller Stand Tabellen |
|
||||
|
|
@ -4,11 +4,17 @@ Ausführliche fachliche Inhalte:
|
|||
|
||||
| Dokument | Inhalt |
|
||||
|----------|--------|
|
||||
| [**Fachliche Nutzerfunktionen (Ist, Überblick)**](../../../docs/FACHLICHE_NUTZERFUNKTIONEN.md) | Kompakte **Nutzer-/Rollen-Perspektive** zur Übergabe an Design & Product (ohne Implementierungsdetail) |
|
||||
| [shinkan_anforderungsdokument_entwurf.md](./shinkan_anforderungsdokument_entwurf.md) | Gesamtentwurf Anforderungen |
|
||||
| [DOMAIN_MODEL.md](./DOMAIN_MODEL.md) | Domänenmodell, Variantenlogik (§11.2), **Medien-Archiv** (Abschnitt 2026-05) |
|
||||
| [MEDIA_ASSETS_AND_ARCHIVE_SPEC.md](../technical/MEDIA_ASSETS_AND_ARCHIVE_SPEC.md) | Medien-Archiv, Lifecycle, **geplante Inline-Medien §11** |
|
||||
| [DOMAIN_MODEL.md](./DOMAIN_MODEL.md) | Domänenmodell, Variantenlogik (Abschnitt 11.2), **Medien-Archiv** (Abschnitt 2026-05) |
|
||||
| [MEDIA_ASSETS_AND_ARCHIVE_SPEC.md](../technical/MEDIA_ASSETS_AND_ARCHIVE_SPEC.md) | Medien-Archiv, Lifecycle, **Inline-Medien** (Spec Abschnitt 11, umgesetzt) |
|
||||
| [MULTI_TENANCY_RBAC_ARCHITECTURE.md](../technical/MULTI_TENANCY_RBAC_ARCHITECTURE.md) | Zielarchitektur Mandanten/Rollen/Membership & Umsetzungsplan |
|
||||
| [**Trainingsmodule & Kombinationsübungen (Fachspez V3)**](./Shinkan%20Trainingsmodule%20Kombinationsuebungen%20Spezifikation%20V2.md) | Produktlogik Module/Kombinationen, **Methoden-Archetypen**, **Coaching-Stufen (§ 10.4)**, kanonische Archetyp-IDs **§ 10.2.1**, **Anhang A** Implementierungsabgleich |
|
||||
| [**Umsetzungsplan Trainingsmodule & Kombination**](../working/TRAINING_MODULES_IMPLEMENTATION_PLAN.md) | Phase 1–5, Coaching-Pakete 4a–4d, Verweis auf Code-Stand |
|
||||
| [**Technischer Entwurf Module/Kombination**](../technical/TRAINING_MODULES_AND_COMBINATION_EXERCISES_SPEC.md) | API/Daten-Ideen; aktueller Coach-/Archetyp-Abgleich im Kopfabschnitt |
|
||||
| [**KI-Unterstützung Übungen (Vision)**](./AI_EXERCISE_ASSISTANT_VISION.md) | Zielbild Zielausbau, Vorschlags-UX (teilweise/komplett), Skills/Varianten, später Planungskontext, Admin-Masse/Qualität |
|
||||
| [**KI Übungen – Umsetzungsplan**](../working/AI_EXERCISE_IMPLEMENTATION_PLAN.md) | Stufen S0–S6, Driftschutz-Regeln, Checkliste gegen Specs |
|
||||
|
||||
**Lieferstand & Umsetzung (Stand Code):** [`../PROJECT_STATUS.md`](../PROJECT_STATUS.md), [`../library/FEATURES_DELIVERED_2026-Q2.md`](../library/FEATURES_DELIVERED_2026-Q2.md) §12, Repo-Root **`docs/HANDOVER.md`**.
|
||||
**Lieferstand & Umsetzung (Stand Code):** [`../PROJECT_STATUS.md`](../PROJECT_STATUS.md), [`../library/FEATURES_DELIVERED_2026-Q2.md`](../library/FEATURES_DELIVERED_2026-Q2.md) (Abschnitt 12), Repo-Root **`docs/HANDOVER.md`**, **`docs/FACHLICHE_NUTZERFUNKTIONEN.md`**.
|
||||
|
||||
`CLAUDE.md` (Repo-Root) verweist hierher als Einstieg.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,829 @@
|
|||
# Trainingsmodule und Kombinationsübungen — fachliche Spezifikation V3
|
||||
|
||||
**Status:** fachlicher Spezifikationsentwurf
|
||||
**Stand:** 2026-05-12 (Anhang A App **0.8.110**; Zeit‑Pfad **`COMBINATION_TIMING_PROFILE_PLAN.md`**) · **Coaching/Archetypen:** § 10.2.1, § 10.4–10.6, **§ 5.4/§ 6.3** Methoden/Archetypen/Zeitschicht · **Anhang A**
|
||||
**Zweck:** Produkt- und Fachspezifikation für Trainingsmodule, Kombinationsübungen, Trainingsmethodenbezug, Planungsintegration und Coaching-Modus in Shinkan.
|
||||
|
||||
**Wichtige Leitlinie dieser Version:**
|
||||
Diese Spezifikation beschreibt bewusst **keine verbindlichen Tabellen, API-Pfade, Spaltennamen oder konkrete Implementierungsdetails**. Die technische Umsetzung soll durch den Coding Agent auf Basis der bestehenden Codebasis geplant werden. Ziel dieses Dokuments ist es, die fachliche Zielarchitektur, Nutzerlogik, Datenbedeutung und Produktentscheidungen so klar zu beschreiben, dass spätere große Refactorings vermieden werden, ohne die bestehende Anwendung durch zu frühe technische Festlegungen zu destabilisieren.
|
||||
|
||||
---
|
||||
|
||||
## 1. Ausgangslage
|
||||
|
||||
Shinkan ist eine trainerzentrierte App für Übungsverwaltung, Trainingsplanung, Rahmenprogramme und Durchführung. Die bestehende Planung arbeitet fachlich mit Trainingseinheiten, Trainingsabschnitten und Einträgen wie Übungen oder Notizen.
|
||||
|
||||
Für die nächste Ausbaustufe werden zwei zusätzliche fachliche Bausteine benötigt:
|
||||
|
||||
1. **Kombinationsübungen**
|
||||
Strukturierte Übungsformen, bei denen mehrere Einzelübungen, Stationen, Rollen oder Schritte methodisch zusammenwirken.
|
||||
|
||||
2. **Trainingsmodule**
|
||||
Wiederverwendbare Planungsbausteine, also gespeicherte Übungsfolgen oder Trainingsblöcke, die in konkrete Trainings oder Rahmenprogramme übernommen werden können.
|
||||
|
||||
Zusätzlich muss geklärt werden, wie **Trainingsmethoden**, **Methoden-Archetypen** und **konkrete Ablaufprofile** fachlich voneinander getrennt werden.
|
||||
|
||||
---
|
||||
|
||||
## 2. Fachliche Grundentscheidungen
|
||||
|
||||
### 2.1 Trainingsabschnitte bleiben Makrostruktur
|
||||
|
||||
Trainingsabschnitte beschreiben die grobe Struktur einer Trainingseinheit, z. B.:
|
||||
|
||||
* Aufwärmen,
|
||||
* Hauptteil,
|
||||
* Kumite,
|
||||
* Kata,
|
||||
* Selbstschutz,
|
||||
* Abschluss.
|
||||
|
||||
Ein Abschnitt ist damit ein Gliederungselement der gesamten Trainingseinheit.
|
||||
|
||||
### 2.2 Kombinationsübungen sind nicht an genau einen Abschnitt gebunden
|
||||
|
||||
Eine Kombinationsübung darf nicht fachlich oder technisch auf genau einen Trainingsabschnitt reduziert werden.
|
||||
|
||||
Sie kann:
|
||||
|
||||
* innerhalb eines Abschnitts verwendet werden,
|
||||
* einen Abschnitt faktisch ausfüllen,
|
||||
* zwischen zwei Abschnitten stehen,
|
||||
* als zentraler Block der Einheit auf Trainingsebene liegen,
|
||||
* Bestandteil eines Trainingsmoduls sein,
|
||||
* Bestandteil eines Rahmenprogramms oder Rahmen-Slots sein.
|
||||
|
||||
Der Abschnitt kann ein sinnvoller Anzeige- oder Planungskontext sein, ist aber nicht die fachliche Heimat der Kombinationsübung.
|
||||
|
||||
### 2.3 Kombinationsübungen gehören fachlich zum Übungsbereich
|
||||
|
||||
Eine Kombinationsübung ist eine Sonderform einer Übung. Sie besitzt daher die typischen Eigenschaften einer Übung:
|
||||
|
||||
* Titel,
|
||||
* Ziel,
|
||||
* Durchführung,
|
||||
* Trainerhinweise,
|
||||
* Vorbereitung,
|
||||
* Hilfsmittel,
|
||||
* Dauer,
|
||||
* Zielgruppe,
|
||||
* Fähigkeiten,
|
||||
* Methodenbezug,
|
||||
* Medien,
|
||||
* Sichtbarkeit,
|
||||
* Freigabestatus.
|
||||
|
||||
Zusätzlich besitzt sie eine interne Struktur:
|
||||
|
||||
* Slots,
|
||||
* Stationen,
|
||||
* Rollen,
|
||||
* Schritte,
|
||||
* Übungspools,
|
||||
* Methoden-Archetyp,
|
||||
* Ablaufprofil für Planung und Coaching.
|
||||
|
||||
### 2.4 Trainingsmodule gehören fachlich zur Planung
|
||||
|
||||
Trainingsmodule sind keine Übungen, sondern wiederverwendbare Planungsbausteine.
|
||||
|
||||
Ein Trainingsmodul kann enthalten:
|
||||
|
||||
* einzelne Übungen,
|
||||
* Kombinationsübungen,
|
||||
* Notizen,
|
||||
* methodische Hinweise,
|
||||
* kurze wiederverwendbare Übungsfolgen,
|
||||
* größere Blöcke innerhalb einer Einheit.
|
||||
|
||||
Trainingsmodule sollten deshalb fachlich unter **Planung / Bibliothek / Module** verortet werden.
|
||||
|
||||
### 2.5 Einfügen bedeutet Kopie mit Herkunft, nicht Live-Verknüpfung
|
||||
|
||||
Wenn ein Trainingsmodul oder eine Kombinationsübung in eine konkrete Trainingseinheit übernommen wird, entsteht eine bearbeitbare Planungsinstanz.
|
||||
|
||||
Grundsatz:
|
||||
|
||||
> Bibliothek = Vorlage.
|
||||
> Planung = lokal bearbeitbare Übernahme.
|
||||
> Durchführung = tatsächliche Nutzung im Training.
|
||||
|
||||
Spätere Änderungen an der Vorlage dürfen bereits geplante oder historische Einheiten nicht ungefragt verändern.
|
||||
|
||||
---
|
||||
|
||||
## 3. Zentrale Begriffe
|
||||
|
||||
| Begriff | Fachliche Bedeutung |
|
||||
| ---------------------- | ------------------------------------------------------------------------------------------------------------------ |
|
||||
| **Trainingseinheit** | Konkretes geplantes oder durchgeführtes Training. |
|
||||
| **Trainingsabschnitt** | Makrostruktur der Einheit, z. B. Aufwärmen oder Hauptteil. |
|
||||
| **Planungsblock** | Zusammenhängender Inhalt innerhalb einer Einheit, z. B. Modul, Kombinationsübung oder manuell gruppierter Block. |
|
||||
| **Kombinationsübung** | Sonderform einer Übung mit interner Struktur aus Slots, Stationen, Rollen oder Schritten. |
|
||||
| **Trainingsmodul** | Wiederverwendbarer Planungsbaustein aus Übungen, Kombinationsübungen und Notizen. |
|
||||
| **Trainingsmethode** | Fachlicher Katalogeintrag, der beschreibt, wie eine Trainingsform didaktisch oder sportmethodisch einzuordnen ist. |
|
||||
| **Methoden-Archetyp** | Ablaufmuster für Planung und Coaching, z. B. rotierender Zirkel oder lineare Sequenz. |
|
||||
| **Ablaufprofil** | Konkrete Ausprägung eines Archetyps, z. B. Arbeitszeit, Wechselzeit, Runden oder Erklärphase. |
|
||||
| **Slot** | Platzhalter innerhalb einer Kombinationsübung, z. B. Station 1, Rolle A oder Schritt 2. |
|
||||
| **Slot-Pool** | Menge möglicher Übungen für einen Slot, aus denen bei der Planung eine konkrete Auswahl getroffen werden kann. |
|
||||
|
||||
---
|
||||
|
||||
## 4. Trainingsmethoden: Ablage und Beschreibung
|
||||
|
||||
### 4.1 Rolle des Methodenkatalogs
|
||||
|
||||
Trainingsmethoden sollen als eigenständige fachliche Katalogobjekte geführt werden.
|
||||
|
||||
Sie beschreiben nicht eine konkrete Übung, sondern die methodische Qualität einer Trainingsform.
|
||||
|
||||
Beispiele:
|
||||
|
||||
* Zirkeltraining,
|
||||
* Rollenspiel,
|
||||
* strukturierte Übung,
|
||||
* Koordinationstraining,
|
||||
* plyometrisches Training,
|
||||
* Dauermethode,
|
||||
* extensive Intervallmethode,
|
||||
* Partnerübung,
|
||||
* freie Anwendung,
|
||||
* Reflexionsformat.
|
||||
|
||||
Der Methodenkatalog dient:
|
||||
|
||||
* der Übungsbeschreibung,
|
||||
* der Suche und Filterung,
|
||||
* der Trainingsplanung,
|
||||
* der fachlichen Standardisierung im Verein,
|
||||
* der späteren KI- oder Assistenzunterstützung,
|
||||
* der Qualitätssicherung bei offiziellen Inhalten.
|
||||
|
||||
### 4.2 Abgrenzung: Methode, Archetyp, Ablaufprofil
|
||||
|
||||
Die drei Begriffe müssen getrennt bleiben.
|
||||
|
||||
| Ebene | Frage | Beispiel |
|
||||
| --------------------- | ---------------------------------------------- | --------------------------------------------------------- |
|
||||
| **Trainingsmethode** | Welche fachliche Methode wird verwendet? | Zirkeltraining, Rollenspiel, Intervalltraining. |
|
||||
| **Methoden-Archetyp** | Nach welchem Ablaufmuster wird gesteuert? | rotierender Zirkel, parallele Stationen, lineare Sequenz. |
|
||||
| **Ablaufprofil** | Wie ist die konkrete Durchführung eingestellt? | 45 Sekunden Arbeit, 15 Sekunden Wechsel, 3 Runden. |
|
||||
|
||||
Wichtig:
|
||||
|
||||
> Der Methoden-Archetyp ersetzt nicht die Trainingsmethode. Er ergänzt sie nur dort, wo der Ablauf für Planung oder Coaching maschinenlesbar interpretiert werden muss.
|
||||
|
||||
### 4.3 Fachliche Beschreibung einer Trainingsmethode
|
||||
|
||||
Eine Trainingsmethode sollte aus Trainersicht so beschrieben werden, dass sie zuverlässig angewendet, gesucht und von anderen Methoden unterschieden werden kann.
|
||||
|
||||
Empfohlene fachliche Beschreibungsfelder:
|
||||
|
||||
| Feld | Zweck |
|
||||
| ---------------------------------- | -------------------------------------------------------------------------- |
|
||||
| **Name** | Eindeutige Bezeichnung der Methode. |
|
||||
| **Kurzbeschreibung** | Schnelle Orientierung in Listen und Auswahlfeldern. |
|
||||
| **Langbeschreibung** | Fachliche Erklärung der Methode. |
|
||||
| **Ziel / Nutzen** | Wofür diese Methode besonders geeignet ist. |
|
||||
| **Typische Einsatzsituationen** | Wann die Methode sinnvoll eingesetzt wird. |
|
||||
| **Geeignete Zielgruppen** | Altersgruppen, Leistungsgruppen oder Trainingskontexte. |
|
||||
| **Organisationsform** | Einzelarbeit, Partnerarbeit, Gruppe, Stationen, Kreis, freie Fläche usw. |
|
||||
| **Belastungscharakter** | locker, technisch, koordinativ, intensiv, intervallartig, spielerisch usw. |
|
||||
| **Typische Dauer** | Orientierung für Planung und Zeitmanagement. |
|
||||
| **Benötigte Rahmenbedingungen** | Platz, Material, Gruppengröße, Sicherheitsabstände. |
|
||||
| **Trainerhinweise** | Wichtige Hinweise für Anleitung und Steuerung. |
|
||||
| **Risiken / typische Fehler** | Was bei falscher Anwendung problematisch sein kann. |
|
||||
| **Geeignete Fähigkeiten** | Fähigkeiten, die mit der Methode häufig adressiert werden. |
|
||||
| **Verwandte Methoden** | Ähnliche oder kombinierbare Methoden. |
|
||||
| **Abgrenzung zu anderen Methoden** | Wann eine andere Methode passender wäre. |
|
||||
| **Optionale Standard-Archetypen** | Falls die Methode häufig mit bestimmten Ablaufmustern genutzt wird. |
|
||||
| **Status und Sichtbarkeit** | Entwurf, freigegeben, offiziell, vereinsintern usw. |
|
||||
|
||||
Diese Felder sind fachliche Anforderungen. Die konkrete technische Ablage soll der Coding Agent anhand der bestehenden Methodendomäne planen.
|
||||
|
||||
### 4.4 Haupt- und Nebenmethoden
|
||||
|
||||
Eine Übung sollte fachlich mindestens eine Hauptmethode haben können.
|
||||
|
||||
Zusätzlich können Nebenmethoden sinnvoll sein, weil eine Übung aus mehreren methodischen Perspektiven beschrieben werden kann.
|
||||
|
||||
Beispiel:
|
||||
|
||||
* Hauptmethode: Zirkeltraining
|
||||
* Nebenmethode: plyometrisches Training
|
||||
* weitere Nebenmethode: Koordinationstraining
|
||||
|
||||
Produktentscheidung:
|
||||
|
||||
> Übungen und Kombinationsübungen sollen eine Hauptmethode und optional weitere Nebenmethoden unterstützen.
|
||||
|
||||
Perspektivisch kann zusätzlich unterschieden werden zwischen:
|
||||
|
||||
* sportmethodischer Methode,
|
||||
* didaktischer Vermittlungsmethode,
|
||||
* organisatorischer Durchführungsform.
|
||||
|
||||
Diese Unterscheidung sollte aber im MVP nicht übermodelliert werden.
|
||||
|
||||
### 4.5 Trainingsmethoden in Kombinationsübungen
|
||||
|
||||
Bei Kombinationsübungen ist der Methodenbezug besonders wichtig.
|
||||
|
||||
Eine Kombinationsübung sollte daher fachlich drei Dinge besitzen:
|
||||
|
||||
1. **Methode**
|
||||
Beispiel: Zirkeltraining.
|
||||
|
||||
2. **Archetyp**
|
||||
Beispiel: rotierender Zeit-Zirkel.
|
||||
|
||||
3. **Ablaufprofil**
|
||||
Beispiel: 6 Stationen, 45 Sekunden Arbeit, 15 Sekunden Wechsel, 3 Runden.
|
||||
|
||||
So kann dieselbe Methode unterschiedlich angewendet werden:
|
||||
|
||||
| Methode | Archetyp | Beispiel |
|
||||
| ------------------- | ------------------- | --------------------------------------------------- |
|
||||
| Zirkeltraining | rotierender Zirkel | Alle Gruppen wechseln gemeinsam weiter. |
|
||||
| Zirkeltraining | parallele Stationen | Stationen laufen parallel, kein gemeinsamer Umlauf. |
|
||||
| Intervalltraining | Intervallblock | Gemeinsame Zeitdomäne ohne Stationen. |
|
||||
| Strukturierte Übung | lineare Sequenz | Schritt 1, Schritt 2, Schritt 3. |
|
||||
|
||||
### 4.6 Methoden in Trainingsmodulen
|
||||
|
||||
Trainingsmodule können ebenfalls einen Methodenbezug besitzen, aber anders als Übungen.
|
||||
|
||||
Ein Modul kann:
|
||||
|
||||
* eine dominante Methode haben,
|
||||
* mehrere Methoden enthalten,
|
||||
* methodisch neutral sein,
|
||||
* nur aus einzelnen Übungen bestehen, die selbst Methoden besitzen.
|
||||
|
||||
Empfehlung:
|
||||
|
||||
> Ein Trainingsmodul darf optional eine primäre methodische Ausrichtung besitzen, sollte aber nicht zwingend eine Methode erzwingen.
|
||||
|
||||
Beispiel:
|
||||
|
||||
* Modul: „Aktivierung und Reaktion“
|
||||
* Primäre methodische Ausrichtung: Koordinationstraining
|
||||
* Enthaltene Übungen: Reaktionsspiel, Sprintsignal, Partneraufgabe
|
||||
|
||||
### 4.7 Methoden in der Suche und Planung
|
||||
|
||||
Der Methodenkatalog soll in der Nutzung sichtbar werden.
|
||||
|
||||
Benötigte Such- und Planungsfunktionen:
|
||||
|
||||
* Übungen nach Methode filtern,
|
||||
* Kombinationsübungen nach Methode und Archetyp filtern,
|
||||
* Trainingsmodule nach methodischer Ausrichtung filtern,
|
||||
* in der Planung passende Methoden für ein Trainingsziel finden,
|
||||
* Methoden als Qualitätsmerkmal offizieller Vereinsinhalte nutzen,
|
||||
* bei der Auswahl einer Kombinationsübung passende Ablaufmuster vorschlagen.
|
||||
|
||||
Beispiel aus Trainersicht:
|
||||
|
||||
> „Ich suche eine Übung für Kumite, Jugendliche, Schwerpunkt Beinarbeit, Methode Zirkeltraining oder Koordinationstraining, Dauer maximal 15 Minuten.“
|
||||
|
||||
### 4.8 Governance des Methodenkatalogs
|
||||
|
||||
Trainingsmethoden sind fachliche Standardobjekte. Daher sollten sie stärker kontrolliert werden als private Trainingsnotizen.
|
||||
|
||||
Empfehlung:
|
||||
|
||||
* offizielle Methoden werden durch Administratoren oder Inhaltsverantwortliche gepflegt,
|
||||
* Vereine können eigene Ergänzungen oder Spezialisierungen anlegen,
|
||||
* Trainer können Vorschläge oder private methodische Hinweise erfassen,
|
||||
* Änderungen an offiziellen Methoden sollten nicht ungeprüft globale Inhalte verändern.
|
||||
|
||||
---
|
||||
|
||||
## 5. Methoden-Archetypen für Kombinationsübungen
|
||||
|
||||
### 5.1 Zweck
|
||||
|
||||
Archetypen beschreiben wiederkehrende Ablaufmuster, die für Planung und Coaching relevant sind.
|
||||
|
||||
Sie beantworten nicht die Frage „Welche Methode ist das?“, sondern:
|
||||
|
||||
> Wie soll dieser Block im Training durchlaufen oder angezeigt werden?
|
||||
|
||||
### 5.2 Empfohlene Start-Archetypen
|
||||
|
||||
| Archetyp | Fachliche Bedeutung | Coaching-Idee |
|
||||
| ------------------------ | -------------------------------------------------------------------- | -------------------------------------------------- |
|
||||
| **Lineare Sequenz** | Übungen bauen nacheinander aufeinander auf. | Schrittfolge mit optionalem Timer. |
|
||||
| **Rotierender Zirkel** | Mehrere Stationen, Gruppen wechseln nach Zeit weiter. | Gemeinsamer Timer, Wechselhinweis, Rundenzähler. |
|
||||
| **Parallele Stationen** | Mehrere Stationen laufen gleichzeitig, aber ohne zwingende Rotation. | Vorher erklären, dann paralleler Betrieb. |
|
||||
| **Parcours** | Stationen oder Aufgaben entlang eines Wegs oder Ablaufs. | Navigation, Abhaken, flexible Reihenfolge möglich. |
|
||||
| **Partner-/Paarwechsel** | Rollen oder Aufgaben wechseln gekoppelt. | A/B-Logik, Rollenhinweise, Wechselimpulse. |
|
||||
| **Intervallblock** | Gemeinsame Zeitdomäne mit wiederholten Belastungsphasen. | Globale Uhr, Intervallanzeige. |
|
||||
| **Freier Methodenblock** | Methodischer Zusammenhang ohne harte Steuerungslogik. | Kompakte Anzeige, manuelles Abhaken. |
|
||||
|
||||
### 5.3 Mindestanforderung an Archetypen
|
||||
|
||||
Für jeden Archetyp muss fachlich beschrieben sein:
|
||||
|
||||
* wann er verwendet wird,
|
||||
* welche Informationen der Trainer bei der Planung benötigt,
|
||||
* welche Informationen im Coaching-Modus angezeigt werden,
|
||||
* welche Angaben verpflichtend sind,
|
||||
* welche Angaben optional sind,
|
||||
* wann ein anderer Archetyp besser geeignet wäre.
|
||||
|
||||
Die technische Validierung und konkrete Ablage dieser Angaben soll der Coding Agent planen.
|
||||
|
||||
### 5.4 Einordnung: Trainingsformen wie HIIT, Dauer, plyometrisch ↔ Archetypen
|
||||
|
||||
**Wichtige Trennung (bleibt fachlich zwingend):**
|
||||
|
||||
* **Trainingsmethode im Methodenkatalog** (z. B. HIIT, extensive Intervallmethode, Dauermethode, plyometrisches Training) beschreibt primär den **didaktisch/belastungsmethodischen Kontext („was für eine Trainingsqualität ist das?“)**.
|
||||
* **`method_archetype`** beschreibt **das Ablaufmuster („wie soll der Trainer den Block strukturieren und im Coach geführt werden?“)** — insbesondere **Parallelität, Rotation, Sequenz, Zeitdomänen**.
|
||||
|
||||
Dieselbe Methode kann in der Praxis mit **mehreren** Archetypen sinnvoll kombiniert sein; das ist **kein Widerspruch**.
|
||||
|
||||
| Beispiel (Methoden-/Belastungsbegriff) | Typischer Archetyp (Orientierung), nicht Pflicht |
|
||||
| -------------------------------------- | ---------------------------------------------- |
|
||||
| **HIIT**, Tabata-ähnlich, Kurzintervalle oft mit hoher Intention | sehr oft **`time_domain_interval`** oder innerhalb eines Zirkels **`circuit_rotate_time`** mit kurzen Arbeitsphasen; Partnerformen zusätzlich **`pair_superset`**. |
|
||||
| **Klassisches Intervalltraining** (längere Arbeit, definierte Erholung, N Wiederholungen) | überwiegend **`time_domain_interval`** oder **`circuit_rotate_time`**, wenn die „Intervallschicht“ an Stationen gebunden ist. |
|
||||
| **Dauermethode** (überwiegend durchgehend ohne harte Arbeit-Erholung-Takte) | eher **`free_method_block`** oder **`sequence_linear`** mit optionalen Hinweiten; **weniger** `time_domain_interval`, sofern kein geregeltes Intervallschema gemeint ist. |
|
||||
| **Plyometrisch**, Explosivblöcke, Technik-Schichtung | häufig **`sequence_linear`** (Progression vor Ort) oder **`circuit_rotate_time`** / **`time_domain_interval`**, wenn klar Zeitfenster oder Wiederholungsblöcke vorgegeben sind. |
|
||||
| Rein **organisatorisches** Stationslaufen ohne gemeinsamen Intervalltakt | **`circuit_rotate_time`** oder **`station_parcours`**, **`circuit_all_parallel`**, je nach ob rotiert wird oder parallel aktiv ist. |
|
||||
|
||||
**Shinkan-Zielrichtung bleibt trainerzentriert:** Es geht **nicht** um individuelle Pulsonomie eines Sportlers, sondern darum, dass der **Trainer Belastungs- und Erholungsphasen, Durchläufe und ggf. Umlauf-/Parallellogik** vorgibt und der **Coach** diese Vorgaben **sichtbar und später steuerbar** macht
|
||||
(siehe **§ 6.3** zu Phasen jenseits „nur Gesamtminuten auf dem Planungsitem“).
|
||||
|
||||
### 5.5 Erweiterbarkeit von Archetypen (aktuell zurückgestellt)
|
||||
|
||||
Die Idee einer **von Superadmins zur Laufzeit editierbare Archetyp-Registry**, die den Coaching-Modus **völlig frei parametrierbar** macht, wird **zurückgestellt**. Vorerst reicht die **festgelegte, versionierte Liste** kanonischer Archetyp-IDs (**§ 10.2.1**); **weitere Archetypen** können später **wie bisher durch Produkt-/Release entschieden** ergänzt werden (Code oder kuratierter Import), ohne freies „Beliebig-Neuanlegen“ ohne definierten Coach-Verhaltens-Anker.
|
||||
|
||||
---
|
||||
|
||||
## 6. Kombinationsübungen
|
||||
|
||||
### 6.1 Fachliche Beschreibung
|
||||
|
||||
Eine Kombinationsübung ist eine wiederverwendbare Übungsform mit interner Struktur.
|
||||
|
||||
Beispiele:
|
||||
|
||||
* Kumite-Zirkel mit fünf Stationen,
|
||||
* Koordinationsparcours,
|
||||
* Selbstschutz-Parcours,
|
||||
* Partnerwechselübung,
|
||||
* methodische Sequenz zur Distanzkontrolle,
|
||||
* Reaktions- und Explosivitätsblock,
|
||||
* Aufwärmparcours für Kinder.
|
||||
|
||||
### 6.2 Bestandteile
|
||||
|
||||
Eine Kombinationsübung sollte fachlich enthalten:
|
||||
|
||||
* allgemeine Übungsbeschreibung,
|
||||
* Ziel,
|
||||
* Durchführung,
|
||||
* Trainerhinweise,
|
||||
* Vorbereitung,
|
||||
* Hilfsmittel,
|
||||
* Zielgruppe,
|
||||
* Fähigkeiten,
|
||||
* Hauptmethode,
|
||||
* optionale Nebenmethoden,
|
||||
* Archetyp,
|
||||
* Slots / Stationen / Rollen / Schritte,
|
||||
* mögliche Übungen je Slot,
|
||||
* strukturierte **Zeitphasen und Belastungs-/Erholungsvorgaben** innerhalb der Kombination (**`method_profile`**, Überblick § 6.3; Details § 10.5) — **zusätzlich** zu allenfalls geplanten **Gesamtminuten am Planungseintrag**,
|
||||
* optionale klassische Hinweise zu Dauer, Runden oder Wechsel aus der Übung heraus,
|
||||
* Hinweise für den Coaching-Modus.
|
||||
|
||||
### 6.3 Zeitschicht: Phasen innerhalb der Kombination (Bibliothek) und Anpassungen in der Planung
|
||||
|
||||
Ein **einzelnes** Feld „Geplante Minuten für diesen Eintrag in der Einheit“ kann die **innenliegende zeitliche Logik** einer Kombinationsübung **nicht** ersetzen. Für Trainersteuerung (und später für Coaching **Stufe C**) soll die Kombination in der Bibliotheksbeschreibung vorsehen können:
|
||||
|
||||
**A) Kombinationseinheit („global“ über die Slots)**
|
||||
|
||||
* **Arbeits-** und **Erholungszeiten** (Sekunden/Minuten) und **Anzahl der Durchläufe** oder **Intervalle**,
|
||||
* ggf. **gemeinsamer Takt** für alle Teilnehmenden (z. B. rotierender Zirkel oder eine gemeinsame Intervalluhr),
|
||||
* **Erklär- oder Aufbauzeit** vor dem eigentlichen Start,
|
||||
* dort, wo der Archetyp passt: **Runden-/Umlaufzahl** oder vergleichbare Strukturen.
|
||||
|
||||
Alle diese Angaben sind **Anweisungen an den Trainer** und **Coach‑Assistenz**, **keine** individuelle Pulssonde oder ähnliche Personenmessung.
|
||||
|
||||
**B) Optional pro Slot oder Schritt**
|
||||
|
||||
* wenn fachlich sinnvoll: **von Station zu Station variierende Arbeitsphasen** oder Mini‑Sequenzen innerhalb eines Slots — technisch z. B. als strukturierte Liste in `method_profile` mit Bezug zum `slot_index` (Ausarbeitung Coding Agent).
|
||||
|
||||
**Nach Einplanung in eine konkrete Trainingseinheit** muss diese Zeitschicht (oder ihr Abgleich mit der Einheitsposition) für den Trainer **bearbeitbar** bleiben, **ohne** die Bibliotheksvorlage still zu überschreiben (kopier-/instanzbasierte Anpassungen — siehe bereits § 2.5 und § 8.3).
|
||||
|
||||
**Umsetzung in der App (Stand 0.8.110):** Pro Übungszeile in einer Trainingseinheit kann optional ein **JSON-Snapshot** des Ablaufprofils gespeichert werden (`planning_method_profile` in der DB, Migration **057**). **`null`** oder fehlender Key: für **Anzeige und Editor** wirkt das **Zusammenführen** aus **Katalog** (`exercises.method_profile` bzw. Join `catalog_method_profile`) **+** Snapshot — der Katalog wird **nicht** durch ein leeres Planungsobjekt verworfen; fehlende bzw. JSON-`null`-Werte im Snapshot **überschreiben** keine Katalogfelder; `slot_profiles_v1` wird **je `slot_index`** zusammengeführt (inkl. konsistenter Steuerungslogik Zeit vs. Ziel‑Wdh.). Persistenz: der Snapshot speichert nur die vom Trainer **gesetzten** Planungsdaten (nach STZ-„Runde“ können leere Objekte als `null` normalisiert werden). **Konkrete Logik:** Frontend `effectiveComboMethodProfile` / `merge` in `frontend/src/utils/comboPlanningMethodProfile.js` (Coach, Planungseditor, Druck/Vorschau konsistent). Bearbeitung in der Planungs-UI: Modal **„Ablauf bearbeiten…“** mit `CombinationMethodProfileEditor` + Vorschau `CombinationPlanBracket`.
|
||||
|
||||
**Coach:** soll die wirksamen Werte nach **Übernahme** und **Einheitsübersteuerungen** konsistent nachvollziehen (**§ 10.4**).
|
||||
|
||||
**Geplantes kanonisches Zeitmodell:** Globale Eckwerte (z. B. Anzahl der Durchläufe / Runden, optionale Gesamt-/Einführungszeit als Ziel oder Rechenhilfe) und **pro Platz (Slot)** die Dimensionen „Belastung“, „wie viele gleiche Übung hintereinander“, „kurze Pause dazwischen“, „Übergangszeit zur nächsten Übung/arbeitstation“ — dokumentiert für die technische Angleichung in **`.claude/docs/working/COMBINATION_TIMING_PROFILE_PLAN.md`** (Felder **`slot_profiles_v1`**, `timing_schema`). Archetypen können **Strukturen und typische Schnellwahlen** vorgeben (z. B. Zirkel: Relation Belastungszeit = Übergangszeit oder Erholungsanteil ≈ 2/3 der Belastung); der Archetyp **Freier Methodenblock** bildet den **Maximal‑Pfad** ohne stärkere stille Annahmen. **Pyramidale/abhängige Pausen** (Pause abhängig von vorheriger Belastung) sind **nicht Teil des aktuellen Umsetzungspfads**, können später als eigener Untertyp ergänzt werden.
|
||||
|
||||
**Fortschritt pro Slot (Stand 0.8.109):** optional **`advance_mode`** je Eintrag in **`slot_profiles_v1`**: `timed` — Standard (`load_sec` = geplante Arbeitsdauer für Timer im Coach; fehlende Angabe entspricht `timed` ohne Sekundenfeld), **`rep`** — mengenorientiert (Zielzahl über **`consecutive_reps`**; keine verbindliche Arbeitsuhr), **`manual`** — coachgeführt (Fortschritt bewusst per Schritt später im Coach, optional Richtwert über **`consecutive_reps`**). Optional **`rep_series_count`**: Standard **1** (wird im Formular/API explizit geführt); Ausnahmen nur, wenn der **Methoden‑Archetyp** in `ARCHETYPE_DEFAULT_REP_SERIES_COUNT` eine andere Vorgabe definiert oder der Nutzer eine andere Zahl setzt. ≥ 2 ermöglicht Pause **zwischen Serien** (`intra_rep_rest_sec`). Bei nur **einer** Serie: kein **`intra_rep_rest_sec`** in UI und Payload; **`transition_after_sec`** = Wechsel zur nächsten Station.
|
||||
|
||||
### 6.4 Slot- und Pool-Logik
|
||||
|
||||
Slots können fest oder variabel sein.
|
||||
|
||||
Beispiel fest:
|
||||
|
||||
* Station 1 = Seilspringen
|
||||
* Station 2 = Liegestütz
|
||||
* Station 3 = Beinarbeit
|
||||
|
||||
Beispiel variabel:
|
||||
|
||||
* Station 1 = eine Übung aus Pool „Beinarbeit“
|
||||
* Station 2 = eine Übung aus Pool „Reaktion“
|
||||
* Station 3 = eine Übung aus Pool „Konter“
|
||||
|
||||
Die konkrete Auswahl kann bei der Planung angepasst werden, ohne die Bibliotheksvorlage zu ändern.
|
||||
|
||||
---
|
||||
|
||||
## 7. Trainingsmodule
|
||||
|
||||
### 7.1 Fachliche Beschreibung
|
||||
|
||||
Ein Trainingsmodul ist ein wiederverwendbarer Planungsbaustein.
|
||||
|
||||
Beispiele:
|
||||
|
||||
* Standard-Aufwärmen für Kinder,
|
||||
* Mobilisation und Aktivierung,
|
||||
* Kumite-Beinarbeit 20 Minuten,
|
||||
* SV-Einstieg Wahrnehmung und Distanz,
|
||||
* Abschlussritual mit Reflexion,
|
||||
* prüfungsnaher Kihon-Block.
|
||||
|
||||
### 7.2 Bestandteile
|
||||
|
||||
Ein Trainingsmodul sollte fachlich enthalten:
|
||||
|
||||
* Titel,
|
||||
* Kurzbeschreibung,
|
||||
* Ziel,
|
||||
* empfohlene Dauer,
|
||||
* empfohlene Zielgruppe,
|
||||
* optional empfohlener Einsatzbereich,
|
||||
* optionale methodische Ausrichtung,
|
||||
* enthaltene Übungen,
|
||||
* enthaltene Kombinationsübungen,
|
||||
* Notizen oder Trainerhinweise,
|
||||
* Sichtbarkeit,
|
||||
* Freigabestatus.
|
||||
|
||||
### 7.3 Keine harte Abschnittsbindung
|
||||
|
||||
Ein Modul kann für einen Abschnitt empfohlen sein, z. B. „Aufwärmen“, darf aber nicht technisch darauf beschränkt werden.
|
||||
|
||||
Ein Modul kann:
|
||||
|
||||
* in einen Abschnitt eingefügt werden,
|
||||
* als eigener Block auf Einheitsebene eingefügt werden,
|
||||
* zwischen Abschnitten eingefügt werden,
|
||||
* in ein Rahmenprogramm übernommen werden.
|
||||
|
||||
---
|
||||
|
||||
## 8. Planungslogik
|
||||
|
||||
### 8.1 Planungsblöcke
|
||||
|
||||
Für die Produktlogik braucht Shinkan den Begriff des Planungsblocks.
|
||||
|
||||
Ein Planungsblock ist ein zusammengehöriger Inhalt in einer Trainingseinheit.
|
||||
|
||||
Planungsblöcke können sein:
|
||||
|
||||
* eingefügtes Trainingsmodul,
|
||||
* eingefügte Kombinationsübung,
|
||||
* manuell gruppierter Block,
|
||||
* später ggf. weitere Blocktypen.
|
||||
|
||||
### 8.2 Verhältnis zu Abschnitten
|
||||
|
||||
Ein Planungsblock kann einem Abschnitt zugeordnet sein, muss aber nicht vollständig in einem Abschnitt aufgehen.
|
||||
|
||||
Produktregel:
|
||||
|
||||
> Abschnitte gliedern die Einheit. Planungsblöcke gliedern den konkreten Trainingsinhalt.
|
||||
|
||||
### 8.3 Lokale Anpassbarkeit
|
||||
|
||||
Nach dem Einfügen muss ein Planungsblock lokal angepasst werden können:
|
||||
|
||||
* Dauer ändern,
|
||||
* **bei Kombinationsübungen:** Ablaufprofil **optional nur für diese Platzierung** überschreiben (aktuell: Snapshot parallel zum Katalog-`method_profile`, z. B. Arbeit-, Erholungs- und Runden-/Intervallangaben über die gleichen strukturierten Felder wie im Übungskatalog) — zusätzlich zu den **Gepl.-Min.** am Eintrag; **Stations-/Slot-Austausch** am konkreten Vorkommen weiter über die bestehende Übungs-/Planungslogik, nicht gesondert als „Kombi-Programmierung“ je Zeile,
|
||||
* Übung austauschen,
|
||||
* Station ergänzen,
|
||||
* Hinweise anpassen,
|
||||
* Reihenfolge ändern,
|
||||
* Block auflösen,
|
||||
* Block duplizieren,
|
||||
* Block als neues Modul speichern.
|
||||
|
||||
Diese Änderungen betreffen nur die konkrete Einheit oder den konkreten Rahmen-Slot, nicht automatisch das Bibliotheksexemplar.
|
||||
|
||||
---
|
||||
|
||||
## 9. UX-Anforderungen
|
||||
|
||||
### 9.1 Inhalt hinzufügen
|
||||
|
||||
Im Planungseditor sollte der Trainer fachlich klar wählen können:
|
||||
|
||||
* Übung hinzufügen,
|
||||
* Kombinationsübung hinzufügen,
|
||||
* Trainingsmodul hinzufügen,
|
||||
* Notiz hinzufügen,
|
||||
* manuellen Block erstellen.
|
||||
|
||||
### 9.2 Modul erstellen
|
||||
|
||||
Ein Modul sollte auf mehreren Wegen entstehen können:
|
||||
|
||||
* leer anlegen,
|
||||
* aus bestehendem Abschnitt speichern,
|
||||
* aus markierten Übungen speichern,
|
||||
* aus einem Teil eines alten Trainings speichern.
|
||||
|
||||
### 9.3 Kombinationsübung erstellen
|
||||
|
||||
Eine Kombinationsübung sollte geführt angelegt werden:
|
||||
|
||||
1. Grunddaten erfassen,
|
||||
2. Methode wählen,
|
||||
3. Archetyp wählen,
|
||||
4. Slots / Stationen / Rollen definieren,
|
||||
5. Übungen oder Pools zuordnen,
|
||||
6. Ablaufprofil festlegen,
|
||||
7. Coaching-Vorschau prüfen,
|
||||
8. speichern.
|
||||
|
||||
### 9.4 Methoden auswählen
|
||||
|
||||
Die Methodenauswahl sollte Trainer unterstützen, nicht belasten.
|
||||
|
||||
Empfohlene UX:
|
||||
|
||||
* Hauptmethode prominent,
|
||||
* Nebenmethoden optional,
|
||||
* passende Methoden vorschlagen,
|
||||
* Methoden kurz erklären,
|
||||
* bei Kombinationsübungen passende Archetypen vorschlagen,
|
||||
* keine Pflicht zur Überklassifizierung bei einfachen Übungen.
|
||||
|
||||
---
|
||||
|
||||
## 10. Coaching- und Assistenzmodus
|
||||
|
||||
### 10.1 Ziel
|
||||
|
||||
Der Coaching-Modus soll die Durchführung unterstützen, ohne den Trainer zu zwingen, exakt dem Plan zu folgen.
|
||||
|
||||
Grundsatz:
|
||||
|
||||
> Der Coaching-Modus gibt Orientierung, Zeitstruktur und Ablaufhilfe, bleibt aber in der Praxis flexibel.
|
||||
|
||||
### 10.2 Unterschiedliche Anzeige je Archetyp
|
||||
|
||||
| Archetyp | Coaching-Anzeige |
|
||||
| -------------------- | -------------------------------------------- |
|
||||
| Lineare Sequenz | Schrittfolge mit Weiter/Zurück. |
|
||||
| Rotierender Zirkel | Stationen, Arbeitszeit, Wechselzeit, Runden. |
|
||||
| Parallele Stationen | Erst Erklärübersicht, dann Parallelbetrieb. |
|
||||
| Parcours | Stationen oder Wegpunkte zum Abhaken. |
|
||||
| Partner-/Paarwechsel | Rollen, Aufgaben und Wechselhinweise. |
|
||||
| Intervallblock | Globale Zeit, Intervallzähler, Aufgaben. |
|
||||
| Freier Methodenblock | Kompakte Übersicht und manuelle Steuerung. |
|
||||
|
||||
#### 10.2.1 Kanonische Archetyp-IDs (Abgleich Fachbegriff, UI und API)
|
||||
|
||||
Damit Produktbeschreibung, Formularfelder (`method_archetype`), Trainingscoach und Backend‑Validierung **dieselben Werte** nutzen und es keinen dokumentationsbedingten Drift gibt, gelten diese **festen Schlüssel** (Maschinen‑IDs):
|
||||
|
||||
| Archetyp (fachlicher Name in § 5.2) | Schlüssel `method_archetype` (`exercises.method_archetype`) |
|
||||
| ----------------------------------- | ----------------------------------------------------------- |
|
||||
| Lineare Sequenz | `sequence_linear` |
|
||||
| Rotierender Zirkel (Zeit) | `circuit_rotate_time` |
|
||||
| Parallele Stationen | `circuit_all_parallel` |
|
||||
| Parcours | `station_parcour` |
|
||||
| Partner- / Paarwechsel | `pair_superset` |
|
||||
| Intervallblock (Zeitdomäne) | `time_domain_interval` |
|
||||
| Freier Methodenblock | `free_method_block` |
|
||||
|
||||
Änderungen an dieser Zuordnung nur **gemeinsam** (Produkt, Backend‑Enum und UI‑Konstanten); siehe Implementierungsanhang weiter unten.
|
||||
|
||||
### 10.3 Durchführungsdokumentation
|
||||
|
||||
Perspektivisch sollte dokumentierbar sein:
|
||||
|
||||
* was durchgeführt wurde,
|
||||
* was übersprungen wurde,
|
||||
* was verändert wurde,
|
||||
* tatsächliche Dauer,
|
||||
* Trainerhinweise,
|
||||
* Reflexion,
|
||||
* Vorschläge zur Verbesserung einer Übung oder eines Moduls.
|
||||
|
||||
Die konkrete technische Umsetzung wird nicht in dieser Spezifikation festgelegt.
|
||||
|
||||
### 10.4 Coaching-Reifegrade (Normierung ohne technisches Pflichtenheft)
|
||||
|
||||
Archetyp-spezifisches Coaching soll **nicht** als ein einziges UX-„Monolith“ gebaut werden, sondern in **nachvollziehbaren Stufen**, damit frühere Umsetzungen nicht überschrieben wirken und der Fortschritt in Doku/Umsetzungsplan nachverfolgt werden kann:
|
||||
|
||||
| Stufe | Bezeichnung (Arbeitstitel) | Inhalt aus Trainersicht | Abgrenzung |
|
||||
| ----- | ---------------------------- | ------------------------ | ----------- |
|
||||
| **A** | **Informations-/Struktursicht** | Pro Kombinationsübung: Kopf‑Kontext aus Katalog **plus** strukturierte Darstellung der **Slots** und der **einzelnen Kandidatenübungen** (Titel, Kurztext, Detail aufklappbar); **ein zeitlicher Schritt im Coach** entspricht weiter **einem** Planungseintrag (ein Item in der Einheit). | Kein eigener Rundenzähler, kein eigener Stations‑Timer‑State pro Archetyp. |
|
||||
| **B** | **Archetyp-Steuerung in der bestehenden Zeitleiste** | Optionale Aufspaltung: z. B. bei **`sequence_linear`** pro Slot **ein Coach‑Schritt** (Weiter/Zurück pro Station), ohne die Datenbank-Semantik der Einheit zu zerstückeln (Virtuelle Schritte oder materialisierte Hilfs‑Einträge – technische Variante dokumentieren). | Bewusste Produkt-/Architekturentscheidung nötig, damit IST‑Zeiten und Abschluss‑PUT konsistent bleiben. |
|
||||
| **C** | **Interaktive Assistenz je Archetyp** | Gemeinschafts-/Stations‑Timer, Wechselimpulse (**`circuit_rotate_time`**), Vorab‑„Erklärphase“‑Flag (**`circuit_all_parallel`**), Abhaken (**`station_parcour`**), gekoppelte A/B‑Ansicht (**`pair_superset`**), globale Intervalluhr (**`time_domain_interval`**) — jeweils an Parameter aus **`method_profile`** angebunden, wo diese in Stufe A/B bereits sichtbar gepflegt werden. | Keine verpflichtende KI‑Steuerung; Trainer kann überspringen (Grundsatz § 10.1). |
|
||||
|
||||
**Aktuelle Zielrichtung:** Stufe **A** soll für **alle** in § 10.2.1 genannten Archetypen **inhaltsgleich** die Slot‑ und Kandidateninformation liefern; **unterschiedliche Kopf-/Hilfstexte und UI-Mikrolayouts** nach Archetyp sind Teil von A und sollten gemeinsam mit Stufe B/C wachsen (kein „still“ abweichendes Verhalten ohne Doku‑Update).
|
||||
|
||||
### 10.5 Fachliche Mindestinfos im **Ablaufprofil** (`method_profile`) pro Archetyp
|
||||
|
||||
`method_profile` ist das **konkretisierende** JSON (o. ä.) zum gewählten Archetyp: Zeiten, Runden, Schalter. Technische Pflichtfelder und Validierung regelt die technische Umsetzung — **fachlich** gilt folgende Minimal-Erwartung, damit Stufe B/C sinnvoll nutzbar ist:
|
||||
|
||||
| Archetyp-Schlüssel | Mindest-Parameter (fachlich sinnvoll; Benennung in der Umsetzung kanonisch festlegen). Typische Zuordnung methodischer Überbegriffe: **§ 5.4** |
|
||||
| ------------------ | ------------------------------------------------------------------------------------- |
|
||||
| `sequence_linear` | Orientierungs-Arbeits-/Pausenhinweise je Schritt oder global; Reihenfolge = Slotreihenfolge — u. a. für **Skillschichtungen**, Aufwärmserien ohne festen Rotationstakt oder **Ausdauer-/Technikketten ohne Intervalltakt**. |
|
||||
| `circuit_rotate_time` | **Arbeit** je Station oder Umlauf, **optional Wechsel/Transition**, **optional Erholung zwischen Runden**, **optional Rundenanzahl**; Kern für rotierenden Zirkel inkl. vieler HIIT-/Zirkelmischformen über Stationen hinweg (**§ 5.4**). |
|
||||
| `circuit_all_parallel` | „Erst gemeinsame Erklärung, dann gleichzeitiger Betrieb aller Stationen“; Zeitfenster Vorab‑Erklärung optional — z. B. wenn keine Rotation, aber gemeinsamer Startzeitpunkt gewünscht ist. |
|
||||
| `station_parcour` | Fokus Stationsbeschreibung; optional freie Besuchsreihenfolge (Profil/Archetyp); weniger zentral **feste Arbeit/Erholung-Takte**, mehr Navigation/Abhaken (später Stufe C). |
|
||||
| `pair_superset` | Arbeit und Wechsel bei **gekoppelten** Rollen; typisch wenn zwei Linien oder Partnerblöcke im Takt gewechselt werden. |
|
||||
| `time_domain_interval` | Klare Zeitdomäne: **Belastungs-, Erholungsblöcke** und **Anzahl Wiederholungen** der Domäne bzw. **Gesamtblockbegrenzung** — zentrale Schicht für viele Formen aus **„Intervall/HIIT/Zeitschachtelungs“‑**Methodenkatalog ohne individuelle Messung (**§ 5.4**). |
|
||||
| `free_method_block` | Keine zusätzlichen Pflichtparameter; **unterstützt** etwa **reibungsarmere Dauer- oder Spielformen**, wo der Trainer keine starke Taktuhr braucht, aber Stationsideen strukturiert bündeln will. |
|
||||
|
||||
#### 10.5.1 Mehrschichtiges Planen (Überblick)
|
||||
|
||||
| Ebene | Inhalt zeitlicher Art |
|
||||
| ----- | --------------------- |
|
||||
| **Einheit / Planungsitem** | z. B. geplante **Gesamtminuten** dieser Platzierung („der Block soll heute etwa 25 Min einnehmen“). |
|
||||
| **Kombinationsübung (Bibliothek)** | strukturierte **Phasen in `method_profile`** (arbeiten, pausieren, Runden…) — § 6.3. |
|
||||
| **Einheitliche Planungsinstanz** | optionale Abweiche vom Bibliotheksprofil **nur für dieses Training** (§ 8.3). |
|
||||
| **Coach** | liest wirksamen Stand (Bibliothek + Overrides) zur **Orientierung**, später automatisierte Taktassistenz (**§ 10.4**). |
|
||||
|
||||
Solange diese Mindestinfos in der Datenpflege noch **nicht** validiert oder nicht geführt erfasst werden, bleibt Coaching bei **Informations-Schicht und manuellen Timern des bestehenden Coach-Dialogs** die fachlich ehrliche Darstellung (siehe Anhang A).
|
||||
|
||||
### 10.6 Offene und geplante Erweiterungen (Produkt-Backlog, Stand 2026-05-12)
|
||||
|
||||
Die folgenden Punkte stammen aus **Session-/Chat-Arbeit** an Planung, Klammerdarstellung und Coach **Stufe A**; sie sind **noch nicht** als vollständige Produktfunktion abgeschlossen bzw. bewusst zurückgestellt:
|
||||
|
||||
| Thema | Kurzbeschreibung | Status |
|
||||
| ----- | ---------------- | ------ |
|
||||
| **Coaching Stufe B/C (individuelle Archetyp-Steuerung)** | Über **Stufe A** (lesend: Slots, Zeiten, Archetyp-Hinweis, Kandidaten-Texte) hinaus: **pro Archetyp** gesteuerte Durchführung (z. B. Substeps bei Sequenz, Stations-/Rotations-Timer beim Zirkel, Erklärphase bei parallelen Stationen, Abhaken Parcours, Intervalluhr). § 10.4 Stufe **B** (Zeitleiste) und **C** (Assistenz). | **Offen** — aktuell nur informativ/Orientierung; kein archetypspezifischer Zustand im Coach. |
|
||||
| **Administrierbarkeit der Archetypen** | Archetypen sind **fest** im Code (`COMBINATION_ARCHETYPE_IDS` Backend, `COMBINATION_ARCHETYPE_OPTIONS` Frontend); **keine** DB-/Admin-Oberfläche für Labels, Defaults, Sichtbarkeit oder club-spezifische Erweiterungen. | **Offen** — Änderungen nur per Release/Code-Review. |
|
||||
| **Einfache Vorbelegung aller Zeit- und Anzahlfelder** | Teilweise: Schnellwahlen (**Zirkel**, **Intervall**), Serien-Default **1**, Archetyp-Map `ARCHETYPE_DEFAULT_REP_SERIES_COUNT`. **Fehlt:** ein Klick „alle Stationen aus globalen Eckwerten / Archetyp-Muster füllen“, Profil-weite **Reset/Übernehmen**-Presets über alle Slots. | **Teilweise** — Ausbau siehe `COMBINATION_TIMING_PROFILE_PLAN.md` § 1 („Archetyp = Struktur + Defaults“). |
|
||||
| **Archetypbedingte Restriktionen & Server-Validierung** | Client führt geführte Felder; **keine** verbindliche Backend-Prüfung „Profil passt zu Archetyp“ (Pflichtschlüssel, Wertebereiche, unzulässige Slot-Kombinationen). | **Offen** — erhöht Datenqualität und Coach-Verlässlichkeit vor Stufe C. |
|
||||
| **Governance Archetyp ↔ offizielle Inhalte** | Noch keine getrennte Policy „nur Superadmin darf neue Archetyp-IDs einführen“ (derzeit ohnehin nur Code). | **Offen** — relevant sobald Archetypen konfigurierbar werden. |
|
||||
|
||||
**Hinweis:** § 13.1 nennt Stufe **A** als MVP-Pflicht und **B/C** als Ausbauschritte — die Tabelle oben präzisiert die **noch offenen** Arbeitspakete aus der Umsetzungspraxis.
|
||||
|
||||
---
|
||||
|
||||
## 11. Rahmenprogramm-Integration
|
||||
|
||||
Trainingsmodule und Kombinationsübungen müssen auch in Rahmenprogrammen nutzbar sein.
|
||||
|
||||
Regel:
|
||||
|
||||
> Was in einer konkreten Trainingseinheit geplant werden kann, sollte grundsätzlich auch in einem Rahmenprogramm oder Rahmen-Slot vorbereitet werden können, sofern es keine echte Durchführung voraussetzt.
|
||||
|
||||
Das betrifft insbesondere:
|
||||
|
||||
* Modul einfügen,
|
||||
* Kombinationsübung einfügen,
|
||||
* methodische Ausrichtung übernehmen,
|
||||
* Slot-Pools vorbelegen,
|
||||
* Dauer anpassen,
|
||||
* später konkrete Einheit daraus ableiten.
|
||||
|
||||
Nicht in den Rahmen gehört:
|
||||
|
||||
* echte Durchführung,
|
||||
* tatsächliche Dauer,
|
||||
* spontane Trainingsnotizen,
|
||||
* Nachbereitungsreflexion.
|
||||
|
||||
---
|
||||
|
||||
## 12. Governance
|
||||
|
||||
Für Methoden, Übungen, Kombinationsübungen und Module gelten abgestufte Sichtbarkeiten und Verantwortlichkeiten.
|
||||
|
||||
Empfohlene fachliche Ebenen:
|
||||
|
||||
* privat,
|
||||
* Verein,
|
||||
* offiziell,
|
||||
* archiviert,
|
||||
* Entwurf,
|
||||
* freigegeben.
|
||||
|
||||
Normale Trainer sollen Inhalte nutzen und lokal anpassen können. Offizielle oder vereinsweite Vorlagen sollen nicht ungeprüft überschrieben werden.
|
||||
|
||||
Für Methoden ist eine besondere Qualitätskontrolle sinnvoll, weil sie als fachlicher Katalog für viele Übungen und Planungen wirken.
|
||||
|
||||
---
|
||||
|
||||
## 13. MVP-Empfehlung
|
||||
|
||||
### 13.1 Muss enthalten sein
|
||||
|
||||
* Trainingsmodule anlegen und wiederverwenden,
|
||||
* Kombinationsübungen als fachliche Sonderform von Übungen,
|
||||
* Methodenbezug mit Hauptmethode und optionalen Nebenmethoden,
|
||||
* klare Trennung zwischen Methode, Archetyp und Ablaufprofil,
|
||||
* mindestens folgende Archetypen:
|
||||
|
||||
* lineare Sequenz,
|
||||
* rotierender Zirkel,
|
||||
* freier Methodenblock,
|
||||
* Planungsblöcke als fachliches Konzept,
|
||||
* lokale Anpassbarkeit nach Einfügen,
|
||||
* Coaching: mindestens **Stufe A** nach § 10.4 für alle Archetypen aus § 10.2.1 (strukturierte Slot-/Kombi-Darstellung; Archetyp-Hilfstexte); **zeitliche/mechanische Archetyp-Steuerung (Stufen B/C)** ausdrücklich als Ausbauschritte.
|
||||
|
||||
### 13.2 Sollte vorbereitet werden
|
||||
|
||||
* parallele Stationen,
|
||||
* Parcours,
|
||||
* Partner-/Paarwechsel,
|
||||
* Intervallblock,
|
||||
* Durchführungsdokumentation,
|
||||
* Rückfluss von Erfahrungswissen,
|
||||
* Offline-/PWA-Nutzung,
|
||||
* stärkere Suche nach Methoden und Archetypen.
|
||||
|
||||
### 13.3 Nicht im MVP
|
||||
|
||||
* vollständige technische Event-Historie jeder Planänderung,
|
||||
* automatische Synchronisation alter Einheiten bei Vorlagenänderung,
|
||||
* komplexe Verschachtelung von Modulen in Modulen,
|
||||
* individuelles Athleten-Tracking,
|
||||
* KI-generierte Trainingsplanung,
|
||||
* verbindliche technische Tabellen- oder API-Architektur.
|
||||
|
||||
---
|
||||
|
||||
## 14. Arbeitsauftrag an den Coding Agent — fachliche Leitplanken
|
||||
|
||||
Der Coding Agent soll die bestehende Codebasis prüfen und auf dieser Grundlage eine technische Umsetzungsplanung erstellen.
|
||||
|
||||
Dabei soll er ausdrücklich:
|
||||
|
||||
1. bestehende Strukturen wiederverwenden, soweit sinnvoll,
|
||||
2. keine unnötigen Refactorings auslösen,
|
||||
3. bestehende Trainingsplanung nicht destabilisieren,
|
||||
4. Migrationen schrittweise und rückwärtskompatibel planen,
|
||||
5. vorhandene Methodendomäne berücksichtigen,
|
||||
6. die Trennung zwischen Trainingsmethode, Archetyp und Ablaufprofil fachlich erhalten,
|
||||
7. technische Alternativen mit Vor- und Nachteilen darstellen,
|
||||
8. erst danach konkrete Tabellen, APIs und UI-Komponenten vorschlagen.
|
||||
|
||||
Die Spezifikation ist daher kein technisches Pflichtenheft, sondern ein fachlicher Rahmen.
|
||||
|
||||
---
|
||||
|
||||
## 15. Zusammenfassung der verbindlichen Produktlogik
|
||||
|
||||
1. Trainingsabschnitte sind die Makrostruktur der Einheit.
|
||||
2. Kombinationsübungen sind keine Abschnitte.
|
||||
3. Kombinationsübungen sind Sonderformen von Übungen.
|
||||
4. Trainingsmodule sind Planungsbausteine.
|
||||
5. Trainingsmethoden sind eigenständige fachliche Katalogobjekte.
|
||||
6. Eine Übung hat eine Hauptmethode und optional Nebenmethoden.
|
||||
7. Methoden-Archetypen beschreiben Ablaufmuster, nicht die Methode selbst.
|
||||
8. Ablaufprofile konkretisieren den Archetyp für Planung und Coaching (siehe § 10.5).
|
||||
9. Einfügen aus Bibliotheken erzeugt lokal bearbeitbare Planungsinhalte.
|
||||
10. Vorlagenänderungen verändern historische oder konkrete Planungen nicht automatisch.
|
||||
11. Rahmenprogramme sollen dieselbe Planungslogik nutzen wie konkrete Einheiten.
|
||||
12. Der Coding Agent entscheidet die technische Umsetzung anhand der bestehenden Codebasis.
|
||||
13. Archetyp-IDs und Coaching-Stufen (§ 10.2.1, § 10.4) sind die **Referenz gegen Code-Drift**; Änderungen nur mit Anhang A und technischer Doku.
|
||||
14. **Zeitliche Phasen** einer Kombination liegen vorrangig in **`method_profile`** und **Gesamtzeit am Planungseintrag**; **Übersteuerungen nur in der Planungsinstanz**, nicht still in der Bibliothek (§ 6.3, § 8.3, § 10.5.1).
|
||||
|
||||
---
|
||||
|
||||
## Anhang A — Implementierungsabgleich (Stand Code: App **0.8.110**)
|
||||
|
||||
Zweck: dieselbe Tabelle für **Produkt / Architekt / Agent** beim nächsten Schritt; verhindert „wir haben X gebaut, die Spec sagt aber Y“ ohne dass es dokumentiert wird.
|
||||
|
||||
| Thema (fachliche Headline aus dieser Spez) | Kurz beschrieben | Stand Code / UX (Referenz nur) | Lücke / nächste sinnvolle Schritte |
|
||||
|--------------------------------------------|-----------------|---------------------------------|-------------------------------------|
|
||||
| **Trainingsmodule (Bibliothek)** | Wiederverwendbare Blöcke, Kopier-Einfügen in Einheit | Bibliothek, API, Übernahme-Modal, Lineage-Spalte | **Phase 3** des Umsetzungsplans: erweiterter Übernahmemodus |
|
||||
| **Kombinationsübung im Katalog** | `exercise_kind=combination`, Slots, Pools (Kandidaten) | Migration 056, CRUD Übung mit `combination_slots`, GET liefert Slots + Kandidatentitel | Fachbezug Haupt-/Nebenmethoden aus § 4/§ 6 dort umsetzen, wo die Domäne es noch nicht abdeckt |
|
||||
| **Archetyp + Ablaufprofil am Katalogobjekt** | `method_archetype`, JSON `method_profile` + **`slot_profiles_v1`** | Geführtes Profil (`CombinationMethodProfileEditor`), `advance_mode` je Slot (Zeit / Ziel‑Wdh. / Coach), API-Build aus `ExerciseFormPage` | **Admin-UI für Archetypen** fehlt (nur Code-Konstanten); **serverseitige Validierung** Profil↔Archetyp offen; **volle Vorbelegung** aller Slots aus Preset/Archetyp nur teilweise (Schnellwahl) |
|
||||
| **Einplanbarkeit (normale Planung)** | Kombi in Sektionen; Overrides § 8.3 | `planning_method_profile` JSONB; Modal **„Ablauf bearbeiten“**; **Merge** Katalog+Planung im Frontend (`effectiveComboMethodProfile`); Payload-Sanitisierung; Backend `Json()` beim Insert | Planungsblöcke Phase 3; **serverseitige** Zusammenführung/Validierung optional (aktuell Merge nur Client) |
|
||||
| **Darstellung Planung / Lauf / Druck** | Konsistente Zeiten & Wdh. | `CombinationPlanBracket`, `effectiveStationTimingSummary`, Belastungs-Badge je Station; kompakte Kombi-Zeile in `TrainingUnitSectionsEditor` | Feintuning nach Nutzerfeedback |
|
||||
| **Zeitphasen (global / pro Slot)** | § 6.3 | `slot_profiles_v1`, globale Archetyp-Felder, `inferAdvanceModeFromStoredSlotRow` für Legacy-Zeilen | `timing_schema`-Konvergenz laut `COMBINATION_TIMING_PROFILE_PLAN.md` |
|
||||
| **Coaching Stufe A** | Slots + Kandidaten, Archetyp, Profil lesbar | `ExerciseFullContent` + `CombinationCoachSlots`: Merge Katalog+Planung; **globale Eckdaten mit fachlichen Labels** (`describeGlobalComboProfile`); Stationstexte inkl. „Wdh. ohne Wechsel zur nächsten Station“ / Pausen-Hinweis | Stufe **B/C** weiterhin **offen** (§ 10.6) |
|
||||
| **Coaching Stufe B** | Zeitleiste archetypnah | **Nein** — ein Coach‑Schritt = ein Planungsitem | Designentscheid: virtuelle Substeps vs. DB‑Materialisierung; Auswirkung auf Ist‑Zeit pro Item |
|
||||
| **Coaching Stufe C** | Timer/Wechsel/Abhaken nach Archetyp | Nur **generischer** Minuten-/Ist-Input pro Item; **kein** Stations-Timer-State | Pro Archetyp UI + `method_profile` — Haupt-Backlog |
|
||||
| **Rahmenprogramm** | Gleiche Inhalte wie Einheit | Slot‑Blueprint, `from-framework-slot` | Modul-/Kombi‑UX in Rahmen wie in Einheit konsolidieren (Phase 5) |
|
||||
| **Coaching-Vorschau im Editor** | § 9.3 Schritt 7 | **Peek** / Run nutzen `CombinationPlanBracket`; kein eigener „Coach-Sim“-Modus im Übungseditor | Optional: eingebettete read-only Coach-Ansicht |
|
||||
|
||||
**Pflege:** Bei jeder relevanten Codeänderung diese Tabelle **in demselben PR / derselben Session** anpassen (kein stiller Drift).
|
||||
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
# Gelieferte Features & technische Basis (Q2 2026)
|
||||
|
||||
**Stand:** 2026-05-08
|
||||
**Referenz:** `backend/version.py` — **APP_VERSION 0.8.64**, **DB_SCHEMA_VERSION** siehe dort
|
||||
**Stand:** 2026-05-20
|
||||
**Referenz:** `backend/version.py` — aktuelle **APP_VERSION** / **DB_SCHEMA_VERSION** (Stand Code u. a. **0.8.96**)
|
||||
|
||||
Dieses Dokument bündelt die in der Entwicklungsphase erreichten **lieferbaren** Funktionen und die zugehörigen **technischen Artefakte**. Trainingsrahmen‑Bibliothek + Slot‑Blueprint: **`technical/TRAINING_FRAMEWORK_SPEC.md`** §2. **Progressionsgraph zwischen Übungen** (Zwischenstand, Grenzen): **§§3–4**. **Medien-Archiv & Bibliothek:** Abschnitt **12** unten + **`MEDIA_ASSETS_AND_ARCHIVE_SPEC.md`**. Detail-Spezifikationen bleiben in den verlinkten Pfaden unter `.claude/docs/technical/` und `.claude/docs/functional/`.
|
||||
|
||||
|
|
@ -68,7 +68,7 @@ Logik: `_upload_limit_bytes(session)` vor `read()`-Prüfung.
|
|||
## 5. Frontend – Übungsliste (`ExercisesListPage.jsx`)
|
||||
|
||||
- Tabs **Liste** · **Progressionsgraphen** (`ExerciseProgressionGraphPanel`): Graphen anlegen/bearbeiten, Kanten inkl. Sequenz-Bulk und Tabellenansicht.
|
||||
- **Filter-Modal** (Fokus, Stilrichtung, Trainingsstil, Zielgruppe, Fähigkeit + Stufen von/bis, Sichtbarkeit, Status).
|
||||
- **Filter-Modal** (Fokus, Stilrichtung, Trainingsstil, Zielgruppe, Fähigkeit + Stufen von/bis, **Freigabelevel**, Status).
|
||||
- **Filter-Chips** unter der Suchleiste; Klick entfernt einen Filter; Badge am Filter-Button = Anzahl Chips.
|
||||
- **Kein Vollbild-Spinner** bei jeder Suche: nur noch **`listFetching`** — Suchfelder bleiben im DOM (**Fokus/Cursor** bleiben erhalten); Liste zeigt optional „Aktualisiere Treffer…“.
|
||||
- **`<datalist>`** mit Titeln der aktuellen Treffer; **`autoComplete="on"`** für Browser-Vorschläge.
|
||||
|
|
@ -76,14 +76,47 @@ Logik: `_upload_limit_bytes(session)` vor `read()`-Prüfung.
|
|||
|
||||
---
|
||||
|
||||
## 6. Frontend – Übung bearbeiten (`ExerciseFormPage.jsx`)
|
||||
## 6. Frontend – Übung bearbeiten (`ExerciseFormPageRoot.jsx`)
|
||||
|
||||
**Routing:** `/exercises/new`, `/exercises/:id/edit` — keine separaten Varianten-Routen.
|
||||
|
||||
### 6.1 Tab-Navigation (Registerkarten)
|
||||
|
||||
Horizontale **`PageSectionNav`** über **`ExerciseFormTabBar`** / **`ExerciseFormPanel`** (`ExerciseFormLayout.jsx`); farbige linke Panel-Ränder (CSS `.exercise-form-edit`, `.exercise-form-panel--*`).
|
||||
|
||||
| Tab | Inhalt |
|
||||
|-----|--------|
|
||||
| **Stammdaten** | Titel, Kurztext, Dauer/Gruppe, Equipment, **Freigabelevel** (`visibility`), Status, Verein |
|
||||
| **Anleitung** | Ziel, Durchführung, Vorbereitung, Trainerhinweise (Rich-Text inkl. Inline-Medien) |
|
||||
| **Einordnung** | Fokusbereiche, Stilrichtungen, Trainingsstile, Zielgruppen, Altersgruppen, **Fähigkeiten** (kompakte Chip-Editoren) |
|
||||
| **Kombination** | nur bei `exercise_kind=combination`: Slots, Archetyp, `method_profile` |
|
||||
| **Varianten** | nur nach erstem Speichern; **nicht** bei Kombinationsübungen |
|
||||
| **Medien & Mehr** | Medien, Progressionsgraph, KI-Hilfen, Löschen — nach erstem Speichern |
|
||||
|
||||
Neue Übungen: Tabs **Varianten** und **Medien & Mehr** deaktiviert bis zur ersten Speicherung.
|
||||
|
||||
### 6.2 Freigabelevel (UI-Begriff)
|
||||
|
||||
Feld **`exercises.visibility`** heißt in der UI durchgängig **Freigabelevel** (`frontend/src/constants/exerciseGovernanceLabels.js`) — Liste, Filter, Bulk, Picker, Formular. API/DB-Feldname **`visibility`** unverändert.
|
||||
|
||||
### 6.3 Fähigkeiten am Übungsobjekt
|
||||
|
||||
- Intensität je Fähigkeit: **`niedrig` \| `mittel` \| `hoch`**, Standard **`mittel`** (`exerciseSkillIntensity.js`).
|
||||
- Kein „Primär“-Schalter mehr in der UI; **`is_primary`** bei `exercise_skills` ist Legacy — Backend speichert immer **`false`**, Scoring ignoriert das Feld.
|
||||
- Kompakte **Chip-Editoren** für Katalog-Zuordnungen und Fähigkeiten (`ExerciseCatalogAssocEditor`, `ExerciseSkillsEditor`).
|
||||
|
||||
### 6.4 Varianten-Editor
|
||||
|
||||
- Tab **Varianten**: **eine Variante zur Zeit** (Dropdown oder „Erste Variante anlegen“); Felder über **`ExerciseVariantFields`**; Reihenfolge Nach oben/unten; Löschen pro Variante.
|
||||
- **Speichern über Aktionsleiste:** `performSaveAttempt` ruft zuerst **`persistPendingVariantChanges()`** auf (geänderte Varianten per PUT, danach optional Entwurf **`createVariantFromDraft()`**).
|
||||
- Button **„Variante anlegen“** (`type="button"`, kein verschachteltes `<form>`): legt Entwurf sofort per API an; alternativ mitgesichert über **Speichern** in der Aktionsleiste.
|
||||
- Snapshot **`variantsSavedSnapshotRef`** für Dirty-Erkennung; Hinweis im Panel: Änderungen werden mit Speichern in der Aktionsleiste mitgesichert.
|
||||
|
||||
### 6.5 Medien & Progressionsgraph
|
||||
|
||||
- **Varianten-Editor**: eingeklappter Bereich (`<details>`), **eine Variante zur Zeit** über Dropdown oder „Neue Variante“; Felder über **`ExerciseVariantFields`**; Reihenfolge Nach oben/unten; Speichern/Löschen pro Variante.
|
||||
- **Medien:** Upload/Embed, **Archiv verknüpfen** (`from-asset`), Medienliste mit Vorschau, Reaktivierung bei Archiv-Konflikt — Details **§12**.
|
||||
- Block **Progressionsgraph** (Edit): Kanten mit Bezug zur aktuellen Übung.
|
||||
|
||||
Hinweis: Es gibt **keine** separaten Routen `/exercises/:id/variants/...` — Bearbeitung erfolgt unter **`/exercises/:id/edit`** (Routing-Doku ggf. anpassen).
|
||||
|
||||
---
|
||||
|
||||
## 7. Frontend – Übung Detail (`ExerciseDetailPage.jsx`)
|
||||
|
|
@ -123,7 +156,16 @@ Hinweis: Es gibt **keine** separaten Routen `/exercises/:id/variants/...` — Be
|
|||
|
||||
---
|
||||
|
||||
## 12. Medien-Archiv & Medienbibliothek (Migration **045** ff., App ca. **0.8.41–0.8.64**)
|
||||
## 12. Trainingsplan: Phasen & parallele Streams (DB **063**, App **0.8.137–0.8.140**)
|
||||
|
||||
- **063:** `training_unit_phases`, `training_unit_parallel_streams`; Sektionen mit `phase_id` / `parallel_stream_id`; Default-Ganzgruppenphase für Bestand.
|
||||
- **API:** `GET /api/training-units/:id` mit **`phases`** + **`sections`**; `PUT`/`POST` mit **`phases`** für Breakout-Einheiten (**0.8.138**); Rahmen-Slot-Materialisierung kopiert Phasen (**0.8.138**).
|
||||
- **Frontend:** Planung Breakout-Panel (**0.8.139–0.8.140**); **`trainingPlanUtils.js`** — `sectionsWithPlanLocForDisplay`, `flattenPlanTimeline`, `buildCoachSavePlanPayload`, Split-Rejoin (`coachShouldPromptSplitRejoinTransition`); **`TrainingCoachPage`**, **`TrainingUnitRunPage`**.
|
||||
- **Doku:** `.claude/docs/technical/PARALLEL_TRAINING_STREAMS_SPEC.md`, `docs/HANDOVER.md` §3, Arbeitspaket „offen“.
|
||||
|
||||
---
|
||||
|
||||
## 13. Medien-Archiv & Medienbibliothek (Migration **045** ff., App ca. **0.8.41–0.8.64**)
|
||||
|
||||
Einzelnorm: **`technical/MEDIA_ASSETS_AND_ARCHIVE_SPEC.md`**. Kurzüberblick geliefert:
|
||||
|
||||
|
|
@ -150,7 +192,7 @@ Einzelnorm: **`technical/MEDIA_ASSETS_AND_ARCHIVE_SPEC.md`**. Kurzüberblick gel
|
|||
|
||||
---
|
||||
|
||||
## 13. Nächste sinnvolle Schritte (nicht Lieferstand)
|
||||
## 14. Nächste sinnvolle Schritte (nicht Lieferstand)
|
||||
|
||||
- Trainingsplanung: Kalender‑UI‑Anbindung **„aus Rahmen übernehmen“**; Visibility/Policies für geteilte Rahmen (**CURR‑004** später).
|
||||
- Progressions-Serien als **Blöcke** (angekündigt; Voraussetzung: `prerequisite_variant_id` / `progression_level` vorhanden).
|
||||
|
|
@ -160,14 +202,55 @@ Einzelnorm: **`technical/MEDIA_ASSETS_AND_ARCHIVE_SPEC.md`**. Kurzüberblick gel
|
|||
|
||||
---
|
||||
|
||||
## 14. Verweise
|
||||
## 15. Gewichtetes Fähigkeiten-Scoring (Phase 3, Stand 2026-05-20)
|
||||
|
||||
Norm: **`technical/SKILL_SCORING_SPEC.md`**.
|
||||
|
||||
### 15.1 Backend
|
||||
|
||||
- **`skill_scoring.py`:** Gewichtung (Dauer × Vorkommen × Intensität × Stufen); `compute_planning_corpus_by_type()` mit getrennten Corpora; `universal_percent` capped auf 100 %
|
||||
- **`routers/skill_profiles.py`:** Profile-GET pro Artefakt; `POST /api/skill-profiles/batch-summaries`; `GET /api/skill-discovery/suggestions`
|
||||
- Sichtbarkeit: **`library_content_visibility_sql`** (Planungs-Bibliothek, nicht „nur Verein club“)
|
||||
|
||||
### 15.2 Frontend
|
||||
|
||||
- **Listen:** Rahmenprogramme + Trainingsmodule — Filter-Modal (wie Übungen), Chips, `SkillTreeMultiSelect` (Portal-Dropdown)
|
||||
- **KPI:** `SkillProfileCompact` — Top je Unterkategorie, Score + Peer-%
|
||||
- **Editoren + Modal:** `SkillProfilePanel`, `SkillProfileFullModal`
|
||||
- **Discovery:** `SkillDiscoveryPanel` auf Fähigkeiten-Seite
|
||||
|
||||
### 15.3 Offen
|
||||
|
||||
- Corpus-Caching; pytest für Typ-Trennung; Filter-Persistenz; Skill-Filter Import-Dialog „Rahmen übernehmen“
|
||||
|
||||
---
|
||||
|
||||
## 16. Übungen – Governance & Berechtigungen (Ist, Stand 2026-05-20)
|
||||
|
||||
**Owner:** `exercises.created_by` (Ersteller). **Varianten** haben kein eigenes `created_by` — Rechte leiten sich von der Eltern-Übung ab.
|
||||
|
||||
| Aktion | `private` | `club` | `official` |
|
||||
|--------|-----------|--------|------------|
|
||||
| **Lesen** | Ersteller; Plattform-Admin | Aktive Vereinsmitglieder des Objekt-`club_id`; Plattform-Admin ohne Mitgliedschaft (Audit) | Plattform-weit |
|
||||
| **Bearbeiten** (Übung inkl. Varianten/Medien) | Ersteller; Plattform-Admin | Ersteller; Plattform-Admin; **`can_plan_in_club`** im Objekt-Verein (`trainer`, `content_editor`, `division_lead`, `club_admin`) | Plattform-Admin |
|
||||
| **Löschen** | Ersteller; Vereins-Admin gemeinsamer Vereine mit Ersteller | Nur **`club_admin`** im Objekt-Verein | Nur Plattform-Admin |
|
||||
|
||||
**Code:** `backend/club_tenancy.py` (`exercise_visible_to_profile`, `can_plan_in_club`), `backend/routers/exercises.py` (`_assert_can_edit_exercise`, `_assert_can_delete_exercise`).
|
||||
|
||||
---
|
||||
|
||||
## 17. Verweise
|
||||
|
||||
| Thema | Dokument |
|
||||
|--------|----------|
|
||||
| Rahmenprogramm / Progressionsgraph | `technical/TRAINING_FRAMEWORK_SPEC.md` |
|
||||
| Fähigkeiten-Scoring Planung | `technical/SKILL_SCORING_SPEC.md` |
|
||||
| API Übungen | `technical/EXERCISES_API_SPEC.md` |
|
||||
| Domänenmodell | `functional/DOMAIN_MODEL.md` |
|
||||
| Datenbank Überblick | `technical/DATABASE_SCHEMA.md` |
|
||||
| Medien Upload (Limits, MIME) | `technical/MEDIA_UPLOAD_SPEC.md` |
|
||||
| Medien-Archiv & Lifecycle | `technical/MEDIA_ASSETS_AND_ARCHIVE_SPEC.md` |
|
||||
| Parallele Phasen/Streams | `functional/PARALLEL_TRAINING_STREAMS_CONCEPT.md`, `technical/PARALLEL_TRAINING_STREAMS_SPEC.md` |
|
||||
| Coaching/Breakout-Handover | `docs/HANDOVER.md` |
|
||||
| Fachlicher Nutzerüberblick | `docs/FACHLICHE_NUTZERFUNKTIONEN.md` (Repo-Root) |
|
||||
| Projektstatus-Kachel | `../PROJECT_STATUS.md` |
|
||||
|
|
|
|||
|
|
@ -79,16 +79,18 @@ Ausgangslage im Code: `private` \| `club` \| `official` (siehe `club_tenancy`).
|
|||
|
||||
### Stufe E – Capabilities dokumentieren (ohne UI für Custom Roles)
|
||||
|
||||
- Markdown-Tabelle **Capability-Fingerprint**: Kennungen wie `content.share_club`, `planning.edit_unit`, `org.manage_members`, … mit Zuordnung zu den **heutigen** festen Vereinsrollen.
|
||||
- **Verbindliche Spez v1:** `CAPABILITY_CATALOG.v1.md` — Capability-IDs, Account-Lifecycle, Rollen-Matrix, Endpoint-Mapping.
|
||||
- Markdown-Tabelle **Capability-Fingerprint**: Kennungen wie `exercises.ai.suggest`, `org.members.manage`, … mit Zuordnung zu den **heutigen** festen Vereinsrollen (siehe Katalog §5–6).
|
||||
- Ziel: später `club_custom_roles` nur noch andere Kombination derselben Kennungen – keine zweite Philosophie.
|
||||
|
||||
### Stufe F – Community (eigenes Epic)
|
||||
|
||||
- Konzept: Freigabe **additiv** (Flag oder Enum), Moderation, Sichtbarkeit „öffentlich außerhalb meines Vereins“ ohne bestehende `club`-Isolation zu brechen.
|
||||
|
||||
### Zurückgestellt – Vereinsabo / Limits
|
||||
### Zurückgestellt – Vereinsabo / Limits (Konzept liegt vor)
|
||||
|
||||
- Wiederöffnen wenn ACCESS_LAYER Stufe C/D stabil; dann Enforcement vor ausgewählten Writes an einen Billing-Stripe binden.
|
||||
- **Spez v1:** `CLUB_MEMBERSHIP_AND_FEATURES.v1.md` — Feature-Registry (Mitai-v9c-Pattern), `club_plans`/`club_subscriptions`, Kontingente an `club_id`.
|
||||
- Implementierung/Billing (Stripe) weiter zurückgestellt; Schema- und Enforcement-Hooks gemäß 4-Phasen-Rollout (Mitai-Vorbild) vorbereiten, sobald Stufe C/D stabil.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -117,10 +119,28 @@ Ausgangslage im Code: `private` \| `club` \| `official` (siehe `club_tenancy`).
|
|||
|
||||
## 7. Referenzen
|
||||
|
||||
- **`CAPABILITY_CATALOG.v1.md`** – Rollen, Capabilities, CRUD-Mapping, `GET /api/me/entitlements`.
|
||||
- **`CLUB_MEMBERSHIP_AND_FEATURES.v1.md`** – Vereinsabo, Feature-Limits, Mitai-Mapping, Ziel-Schema.
|
||||
- `.claude/docs/technical/MULTI_TENANCY_RBAC_ARCHITECTURE.md` – übergeordnetes Zielbild & Begriffe.
|
||||
- `.claude/docs/technical/MEDIA_ASSETS_AND_ARCHIVE_SPEC.md` – verbindliche Domänenregeln für **Medien-Assets** (gleiche Sichtbarkeit wie Übungen, Promotion-Kopplung, Copyright, Papierkorb/Lebenszyklus, externer Speicher). Bei Widerspruch zur Sichtbarkeits-Tabelle in §3 dieses Dokuments: §3 für Enums/`library_content_*`-Semantik, Medien-Spez für Asset-spezifische Zusatzregeln.
|
||||
- `backend/club_tenancy.py` – bestehende Bausteine (`assert_club_member`, `exercise_visible_to_profile`, …); Ziel ist Deren schrittweise Zusammenführung unter die neue Zugriffsschicht ohne Big-Bang.
|
||||
- `backend/club_tenancy.py` – bestehende Bausteine (`assert_club_member`, `exercise_visible_to_profile`, `can_plan_in_club`, …); Ziel ist Deren schrittweise Zusammenführung unter die neue Zugriffsschicht ohne Big-Bang.
|
||||
|
||||
---
|
||||
|
||||
**Letzte Aktualisierung:** 2026-05-07
|
||||
## 8. Anhang – Übungen (Ist-Implementierung, Referenz)
|
||||
|
||||
**Stand:** 2026-05-20 · **Detail:** `EXERCISES_API_SPEC.md` Permissions, `FEATURES_DELIVERED_2026-Q2.md` §16
|
||||
|
||||
| Feld / Konzept | Semantik |
|
||||
|----------------|----------|
|
||||
| `created_by` | Owner der Übung; Varianten erben Rechte |
|
||||
| `visibility` | UI: **Freigabelevel** — `private` \| `club` \| `official` |
|
||||
| Lesen | `exercise_visible_to_profile` — `official` global; `private` Ersteller + Plattform-Admin; `club` aktive Mitglieder (+ Plattform-Admin Audit) |
|
||||
| Bearbeiten | Ersteller; Plattform-Admin; bei `club` zusätzlich `can_plan_in_club` (Trainer, Content-Editor, Spartenleitung, Vereins-Admin) |
|
||||
| Löschen | `official` → Plattform-Admin; `club` → `club_admin` im Objekt-Verein; `private` → Ersteller oder Vereins-Admin mit gemeinsamem Verein |
|
||||
|
||||
**Hinweis:** Dieser Anhang dokumentiert den **produktiven Code-Pfad** in `exercises.py`; die Roadmap in §4 bleibt für die langfristige Vereinheitlichung aller Bibliotheksartefakte maßgeblich.
|
||||
|
||||
---
|
||||
|
||||
**Letzte Aktualisierung:** 2026-05-20
|
||||
|
|
|
|||
|
|
@ -1,11 +1,20 @@
|
|||
# KI-Prompt-System – Universelle Admin-Konfiguration
|
||||
|
||||
**Version:** 1.0
|
||||
**Datum:** 2026-04-24
|
||||
**Status:** DRAFT
|
||||
**Version:** 1.1
|
||||
**Datum:** 2026-05-30
|
||||
**Status:** Kern umgesetzt (`ai_prompts`, `prompt_resolver`, Superadmin-HTTP-API); Kaskaden geplant (Abschnitt 8)
|
||||
|
||||
**Zielbild (Roadmap):** `.claude/docs/technical/AI_PROMPT_TARGET_ARCHITECTURE.md` — Kontext-Arten, Composition, Planung/Rahmen, Phasenplan.
|
||||
|
||||
**Ist-Stand API (Superadmin):**
|
||||
- `GET /api/admin/ai-prompts`, `GET /api/admin/ai-prompts/{id}`, `PUT …`, `POST …/preview`, `POST …/reset-template`, `GET /api/admin/ai-prompts/catalog/placeholders`
|
||||
- Spalte **`openrouter_model`** (Migration **070**): Optional pro Prompt-Zeile; OpenRouter **`model`**-Parameter; **`NULL`/leer ⇒ `OPENROUTER_MODEL`** aus der Umgebung.
|
||||
|
||||
**Autor:** Claude Code
|
||||
**Vorbild:** Mitai Jinkendo Issue #53 + `backend/routers/prompts.py` + Placeholder-System
|
||||
|
||||
**Verwandt (Skill-Katalog in Übungs-KI):** `working/AI_SKILL_RETRIEVAL_PROFILES_SPEC.md` — Tabelle **`ai_skill_retrieval_profiles`** (`config`-JSON ergänzt inhaltliche Prompt-/Katalog-Steuerung neben Platzhaltern).
|
||||
|
||||
---
|
||||
|
||||
## 1. Konzept
|
||||
|
|
@ -28,6 +37,7 @@ steuerbar. Kein KI-Aufruf ist fest im Code verdrahtet.
|
|||
|-------------|-----------|
|
||||
| `exercise_summary` | Generiert `exercises.summary` aus goal + execution |
|
||||
| `exercise_skill_suggestions` | Empfiehlt Skills + Stufen für eine Übung |
|
||||
| `exercise_instruction_rewrite` | Überarbeitet Anleitung: goal, execution, preparation, trainer_notes (JSON, prägnantes HTML) |
|
||||
| `exercise_category_suggestions` | Empfiehlt Fokusbereich, Stil, Zielgruppe |
|
||||
| `model_skill_level_description` | Generiert Stufen-Beschreibung in der Fähigkeitsmatrix |
|
||||
| `training_plan_notes` | Erzeugt Trainer-Notizen für Trainingseinheiten |
|
||||
|
|
@ -174,10 +184,9 @@ Wähle maximal 5 passende Fähigkeiten. Für jede gib an:
|
|||
- required_level: Voraussetzung (einsteiger|grundlagen|aufbau|fortgeschritten|experte)
|
||||
- target_level: Ziel nach regelmäßigem Training (gleiche Werte)
|
||||
- intensity: Trainingsintensität (niedrig|mittel|hoch)
|
||||
- is_primary: true wenn Hauptfähigkeit
|
||||
|
||||
Antworte NUR als JSON-Array:
|
||||
[{"skill_id": 1, "required_level": "grundlagen", "target_level": "aufbau", "intensity": "hoch", "is_primary": true}]
|
||||
[{"skill_id": 1, "required_level": "grundlagen", "target_level": "aufbau", "intensity": "hoch"}]
|
||||
|
||||
Wenn keine Fähigkeit passt, antworte mit [].$$,
|
||||
'exercise', 'json', true, NULL, 2),
|
||||
|
|
@ -597,6 +606,19 @@ AI_PROMPT_SYSTEM_SPEC: ai_service.run_ai_prompt("exercise_summary", ...)
|
|||
|
||||
---
|
||||
|
||||
**Version:** 1.0
|
||||
**Datum:** 2026-04-24
|
||||
**Status:** DRAFT
|
||||
## 8. Prompt-Kaskaden (geplant — nicht implementiert)
|
||||
|
||||
**Ziel:** Vorlagen, die andere Prompts einbinden oder in feste Stufen (System → Fach → Ausgabeformat) zerlegt werden — ohne die DB-Templates mit duplizierten Fliesstexten zu zersplittern.
|
||||
|
||||
**Konzeptskizze:**
|
||||
- Optional neues Feld `base_slug` oder eigene Tabelle `ai_prompt_composition` (Reihenfolge, Rolle: `system|user|prepend`).
|
||||
- Platzhaltersyntax z. B. `{{include_prompt:slug}}` mit **maximaler Verschachtelungstiefe** und Zykluserkennung.
|
||||
- Auflösungsreihenfolge: (1) eingebundene Slugs expandieren, (2) Kontext-Variablen wie heute ersetzen.
|
||||
|
||||
Bis zur Umsetzung bleiben zusammengesetzte Anweisungen im **einen** Template pro Slug (wie `exercise_skill_suggestions` mit `{{skills_catalog}}`).
|
||||
|
||||
---
|
||||
|
||||
**Version:** 1.1
|
||||
**Datum:** 2026-05-30
|
||||
**Status:** Teile umgesetzt (DB 067/069, Resolver, Superadmin-API + UI); Kaskaden offen
|
||||
|
|
|
|||
166
.claude/docs/technical/AI_PROMPT_TARGET_ARCHITECTURE.md
Normal file
166
.claude/docs/technical/AI_PROMPT_TARGET_ARCHITECTURE.md
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
# KI-Prompt-System — Zielarchitektur (Shinkan Jinkendo)
|
||||
|
||||
**Version:** 1.0
|
||||
**Datum:** 2026-05-30
|
||||
**Status:** VERBINDLICHE ZIELRICHTUNG (Roadmap — nicht alles bereits umgesetzt)
|
||||
**Ergänzt:** `AI_PROMPT_SYSTEM_SPEC.md` (aktueller Ist-Stand APIs/DB/UI), Mitai-Anleihen aus gleichnamigen Konzepten (Admin-Prompts, Platzhalter)
|
||||
|
||||
---
|
||||
|
||||
## 1. Zweck
|
||||
|
||||
Dieses Dokument beschreibt das **Zielbild**, damit spätere Arbeiten (**Trainingsplanung**, **mehrstufige Rahmenprogramme**, **Phasen/Streams**, weitere KI-Artefakte) **nicht** zu wiederholten Refaktoren von Übungs-KI oder OpenRouter-Anbindung zwingen.
|
||||
|
||||
**Leitkriterien:** wenige stabile Schnittflächen, Kontext pro Domäne, komponierbare Prompts, gültige Ausgaben, Betrieb ohne Code-Deploy für kleine Tweaks.
|
||||
|
||||
---
|
||||
|
||||
## 2. Leitprinzipien
|
||||
|
||||
### 2.1 Eine stabile Ausführungsschicht
|
||||
|
||||
Alle produktiven KI-Aufrufe sollten mittelfristig über eine **einheitliche Fassade** laufen:
|
||||
|
||||
- **Eingabe:** `slug` (+ optional Kontext-Arten-Enum), **serialisierter Domän-Kontext** (Pydantic pro Kind), Konfiguration (Modell, Temperatur, … aus Env/DB).
|
||||
- **Ausgabe:** Text oder validiertes JSON, Metadaten (`model`, `slug`, ggf. `prompt_version`/Hash), strukturierte Fehler.
|
||||
|
||||
Router und Frontend rufen diese Schicht oder schmale Orchestratoren — **nicht** direkt `httpx`/OpenRouter an jeder Ecke verteilt.
|
||||
|
||||
**Frühere Konkretisierung (Umsetzung gestartet):** Modul `backend/ai_prompt_runtime.py` (`load_ai_prompt_row`, `load_and_render_ai_prompt`, Kontext-Arten) sowie `backend/ai_prompt_job.py` (Pydantic `ExerciseFormAiPromptContext` fuer Uebungs-Prompts — Admin-Vorschau + erweiterbare Router-Nutzung); `exercise_ai` orchestriert OpenRouter nach dem Rendern.
|
||||
|
||||
### 2.2 Trennung: Semantik vs. Transport
|
||||
|
||||
- **Semantik:** Was soll das Modell liefern? Das hängt an **Prompt-Definition**, **Ausgabeformat** (`text`/`json`) und nachvollziehbarer Validierung — nicht am HTTP-Client.
|
||||
- **Transport:** OpenRouter, Modellwahl, Retry, Timeouts bleiben in einem oder wenigen Hilfsmodulen.
|
||||
|
||||
### 2.3 Kontext-Namespaces für Platzhalter
|
||||
|
||||
Platzhalter und erlaubte Keys sind **pro logischer Kontext-Art** definiert, z. B.:
|
||||
|
||||
- `exercise_form_ai` — heute: Übungsformular-Vorschläge.
|
||||
- später: `training_unit`, `framework_program_slot`, `import_wiki`, …
|
||||
|
||||
Damit kann der Katalog wachsen, ohne dass alle Keys in einen globalen Soup-Namespace müssen (`exercise_*` vs. `framework_*` ohne Kollisionen). Optional später **präfixierte** Keys (`exercise.title`, `slot.index`).
|
||||
|
||||
### 2.4 Komposition / Kaskade explizit
|
||||
|
||||
**Ziel:** Mehrteilige Prompts („System“–„Nutzer“–Anhänge) und **Einbindung anderer Vorlagen** als **Daten** (Kompositionsmodell), nicht nur als unbearbeiteter Freitext mit `{{include}}`.
|
||||
|
||||
Skizzen (noch nicht vollständig umgesetzt):
|
||||
|
||||
- Tabelle oder JSON-Spalte `composition`/`ai_prompt_segments`: geordnete Segmente mit `role` (`system` \| `user` \| äquivalent zum jeweiligen API-Shape), Quelle (`inline`, `ref_slug`), optional `ref_slug`, Schema-Version.
|
||||
- Einbindungen mit **Maximaltiefe** und **Zykluserkennung** — keine unbegrenzten Makro-Ketten.
|
||||
|
||||
Bis dahin bleiben zusammenhängende Anweisungen in **einem** DB-Template pro Slug tragbar (`exercise_skill_suggestions` + `{{skills_catalog}}` bleiben gültig).
|
||||
|
||||
---
|
||||
|
||||
## 3. Zieldatenmodell (Schichten)
|
||||
|
||||
### 3.1 Definition (`ai_prompts` — bereits vorhanden, evolviert)
|
||||
|
||||
| Konzept | Bedeutung |
|
||||
|--------|-----------|
|
||||
| `slug`, `category`, `output_format`, `active` | Adressierung & Schalter |
|
||||
| `template` | aktueller Inhalt |
|
||||
| `default_template` | Referenz zum Zurücksetzen (Migration **069**) |
|
||||
| `output_schema` (JSONB) | optional: JSON-Outputs validieren |
|
||||
|
||||
**Ausbaustufen:**
|
||||
|
||||
1. Nur `template`-Text (**heute**, plus Mustache über `prompt_resolver`).
|
||||
2. Zusätzlich **Versionierung**: Historie oder `template_version`/Audit (wer hat wann geändert).
|
||||
3. **Segmentierte Composition** wie in Abschnitt 2.4.
|
||||
|
||||
### 3.2 Kontext-Builder pro Domäne
|
||||
|
||||
Pro **Kontext-Art** eine klar genannte Routine (Pattern: registrierbare Builder):
|
||||
|
||||
| Kontext-Art | Beispiel-Input aus der App | Beispiel-Platzhalter / Daten |
|
||||
|-------------|----------------------------|------------------------------|
|
||||
| `exercise_form_ai` | Titel, Ziel/Durchführung (HTML→Plain), Fokuskontext, Retrieval-Profil-Influenza | `exercise_*`, `skills_catalog` |
|
||||
| `training_unit` (geplant) | Sektionen, Zeiten, Phasen/Streams, verknüpfte Übungs-IDs | `unit_*`, `sections_summary_*` |
|
||||
| `framework_program` (geplant) | Ziele pro Woche/Schicht, Slots, bereits geplante Einheiten, Skill-Scores | `framework_*`, `slot_*`, aggregierte KPIs |
|
||||
|
||||
**Regel:** Planungs-UI baut keine Prompt-Strings; sie liefert **Domän-DTOs** → Builder erzeugen **Platzhalter-Map + ggf. Anhänge**.
|
||||
|
||||
### 3.3 Skill-Retrieval und Prompts
|
||||
|
||||
`ai_skill_retrieval_profiles` steuert **Katalog‑Zusammenstellung** vor dem Platzhalter `{{skills_catalog}}` — das bleibt **orthogonal** zur Prompt-Verwaltung: Prompt ändert *Anweisung*, Profil ändert *welche Skills im Kontextfenster sind*.
|
||||
|
||||
---
|
||||
|
||||
## 4. Trainingsplanung & Rahmen — erwartete Komplexität
|
||||
|
||||
Risiken: sehr große Kontexte (viele Slots, Streams, Bibliotheken), wiederholte KI-Anfragen, Token-Limits.
|
||||
|
||||
**Vorbereitende Strategien:**
|
||||
|
||||
1. **Gestufte Kontexte:** Rohdaten → interne Kurzfassungen (optional zweiter Prompt oder heuristisch) → finale Generator-Prompt nur mit komprimierten Summaries.
|
||||
2. **Slug-Pro-Use-Case:** z. B. `training_unit_trainer_notes`, `framework_slot_coach_hint` — jeweils schmaler Vertrag statt „ein Prompt für alles“.
|
||||
3. **Output-Verträge:** JSON-Schema + Server-Validierung vor UI; Fehlermeldungen mit Referenz auf Slug/Version.
|
||||
4. **Feature-Flags / Modell-Overrides** pro Slug (optional in DB oder Env) für Dev/Prod ohne große Codepfade.
|
||||
|
||||
---
|
||||
|
||||
## 5. Mitai (Jinkendo)
|
||||
|
||||
Konzeptionell **gleiche Bausteine** (admin-konfigurierbare Prompts, Platzhalter, Preview), **andere** Kontext-Builder und ggf. andere Mandanten/Overlays. Eine gemeinsame **Resolver-/Mustache-Ebene** ist wünschenswert; **Shinkan-spezifische** Planungs- und Rahmenkontexte bleiben in Shinkan gekapselt.
|
||||
|
||||
---
|
||||
|
||||
## 6. Betrieb, Sicherheit, Observability
|
||||
|
||||
- **Audit:** `updated_by` / Änderungshistorie für Templates (Backlog), heute: Timestamps.
|
||||
- **Prompt-Injection:** System-/User-Segmente trennen; sensible Regeln in `system`/`developer`-äquivalenten Blöcken (wenn API das hergibt).
|
||||
- **Logging:** weiter `SHINKAN_AI_DEBUG`; langfristig Hash/Länge des **aufgelösten** Prompts pro Request (ohne Secrets).
|
||||
- **Kosten/Latenz:** Timeouts, max. Token-Hinweise pro Slug-Konfiguration.
|
||||
|
||||
---
|
||||
|
||||
## 7. Phasenplan (empfohlen, ohne Big-Bang)
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph laufzeit
|
||||
A[ai_prompts DB]
|
||||
B[prompt_resolver Mustache]
|
||||
C[ai_prompt_runtime]
|
||||
J[ai_prompt_job Pydantic]
|
||||
D[exercise_ai OpenRouter]
|
||||
end
|
||||
A --> C
|
||||
C --> B
|
||||
J --> D
|
||||
C --> D
|
||||
B --> D
|
||||
```
|
||||
|
||||
| Phase | Inhalt |
|
||||
|-------|--------|
|
||||
| **P0** | `AiPromptContextKind`, `load_ai_prompt_row` zentral; Übungs-KI über Laufzeit. |
|
||||
| **P1** | `load_and_render_ai_prompt`, `AiPromptUnavailableError`, `render_ai_prompt_template_for_row`; **`ExerciseFormAiPromptContext`** in `ai_prompt_context.py`; **`run_exercise_form_ai_suggestion`**; Übungs-API und Admin-Vorschau nutzen denselben Kontext. |
|
||||
| **P2** | Versionierung oder Audit-Spalten; **teilweise:** optionales OpenRouter-Modell pro Zeile (`openrouter_model`, Migration 070, Fallback `OPENROUTER_MODEL`); weitere Overrides (Temperatur) offen. |
|
||||
| **P3** | Composition/Segmente (JSON Schema Version 1) + UI nur für komplexe Slugs. |
|
||||
| **P4** | Erste Planungs-/Rahmen-Slugs mit dedizierten Buildern und Token-Budget-Strategien. |
|
||||
|
||||
---
|
||||
|
||||
## 8. Was bewusst vermieden werden soll
|
||||
|
||||
- Vollständige „Workflow-Engine“ mit beliebigen Graphen, bevor 2–3 konkrete Planungs-Anwendungsfälle live sind.
|
||||
- Pro-Verein-Prompt-Kopien vor klar definierter Produkt-Anforderung (sonst Daten- und Pflege-Spirale).
|
||||
- Unbegrenzte `include`-rekursive Textmakros ohne Tiefenschutz.
|
||||
|
||||
---
|
||||
|
||||
## 9. Querverweise
|
||||
|
||||
- Ist-Implementierung Prompts/UI: `AI_PROMPT_SYSTEM_SPEC.md`
|
||||
- Zugriffsrecht Admin-Prompts: `ACCESS_LAYER_ENDPOINT_AUDIT.md`
|
||||
- Retrieval-Profile: `.claude/docs/working/AI_SKILL_RETRIEVAL_PROFILES_SPEC.md`
|
||||
- Übungs-KI-Codepfad: `backend/exercise_ai.py`, `backend/prompt_resolver.py`, `backend/ai_prompt_runtime.py`, `backend/ai_prompt_context.py`, `backend/ai_prompt_job.py`
|
||||
|
||||
---
|
||||
|
||||
**Version:** 1.0 · **Datum:** 2026-05-30
|
||||
202
.claude/docs/technical/AI_TRAINING_PLANNING_CONCEPT.md
Normal file
202
.claude/docs/technical/AI_TRAINING_PLANNING_CONCEPT.md
Normal file
|
|
@ -0,0 +1,202 @@
|
|||
# KI-gestützte Trainingsplanung – Zentrales Konzept
|
||||
|
||||
**Version:** 0.3
|
||||
**Datum:** 2026-05-22
|
||||
**Status:** Arbeitsdokument (Verfeinerung durch fachliche Konzept-Agentur vorgesehen)
|
||||
**Ziel:** Einheitlicher Rahmen für **stufenweise** KI-Unterstützung – zuerst **Übungsanlage** (Zusammenfassung, Fähigkeiten, Texte), später **Planung** (Abschnitte, Einheiten, Rahmen) – ohne vollständigen Übungskatalog im Prompt.
|
||||
|
||||
**Maßgebende Version zum Abgleich:** `backend/version.py` (`APP_VERSION`, `DB_SCHEMA_VERSION`, relevante Einträge in `MODULE_VERSIONS`).
|
||||
|
||||
**Verwandte Dokumente:**
|
||||
`functional/DOMAIN_MODEL.md` · **`functional/AI_EXERCISE_ASSISTANT_VISION.md`** (Übungs-KI: Zielbild vor Planungs-KI) · `functional/TRAINING_CURRICULUM_AND_GOVERNANCE_CONCEPT.md` (u. a. CURR-003 zu Progressions-/KI-Automatik) · **`working/AI_PLANNING_KI_MULTISTAGE_FORECAST.md`** (mehrstufige Planungs-KI: Daten-„Graph“, Pipeline-Stufen, Code-Schnitte – Vorschau gegen späteres Refactoring) · `technical/TRAINING_FRAMEWORK_SPEC.md` · **`technical/SKILL_SCORING_SPEC.md`** (Fähigkeits-Profilierung, Discovery) · `technical/KI_FEATURES_SPEC.md` · `technical/AI_PROMPT_SYSTEM_SPEC.md` · `technical/SKILLS_MATRIX_SPEC.md` · `docs/FACHLICHE_NUTZERFUNKTIONEN.md` · `docs/HANDOVER.md`
|
||||
|
||||
---
|
||||
|
||||
## 1. Produktliche Leitlinien
|
||||
|
||||
- **Nutzer:** Trainer/Vereinskontext, **Gruppenplanung** – keine Pflicht zur individuellen Sportler-Verfolgung; Kontext soll primär aus **Gruppe**, **bereits geplanten/durchgeführten Einheiten**, **Rahmen-/Zielen** und **berechtigtem Übungskorpus** bestehen.
|
||||
- **Human-in-the-loop:** KI liefert **Vorschläge** (Liste, Reihenfolge, Begründung); schreibende Übernahme in Pläne nur nach **Trainer-Bestätigung** oder expliziter Aktion (analog „Manual First“ in `KI_FEATURES_SPEC.md`).
|
||||
- **Governance-first:** Nur Übungen, die die API bereits für den Mandanten/Kontext **sichtbar** freigibt, dürfen in Kandidatenlisten landen – **vor** Retrieval und **vor** jedem Prompt.
|
||||
|
||||
### 1.1 Abgleich: aktueller Code- und Schema-Stand (Stand Review 2026-05-22)
|
||||
|
||||
| Thema | Ist im Repo | Konsequenz für dieses Konzept |
|
||||
|--------|-------------|-------------------------------|
|
||||
| **OpenRouter / LLM im Backend** | Produktiver Aufruf für Übungs‑Suggest in `openrouter_chat.py`, `exercise_ai.py`; Endpunkte **`POST …/exercises/ai/suggest`** und **`POST …/{id}/ai/regenerate`**; Migration **067** (`ai_prompts`, `summary_ai_generated`). **`db.py`**-Bootstrap nutzt **`display_name`**. | **Übungs-Assistent (P0)** vorhanden; generalisierter Service + **Planungs-KI** folgen. |
|
||||
| **Übungs-KI laut Spec** | P0: Kurzfassung + Skill‑Vorschläge (`include_summary` / `include_skills`); **kein** Auto-KI beim Speichern (S5 im Umsetzungsplan). | Feinspez: `summary_ai_generated` bei manueller Kurzfassung zurücksetzen; Rate-Limits; Prompt-Admin-UI. |
|
||||
| **Fähigkeiten-Stammdaten** | Migration **`065_skills_wiki_karate_relevance`:** `skills.karate_relevance` (Text), `skills.relevance_level` (1–3, optional); dazu weiterhin `description`, `focus_areas`, Kategorien, `skill_level_definitions` (Level 1–5 je Skill). | Diese Felder sind **expliziter Prompt-Kontext** für Skill-Vorschläge (Disambiguierung Karate vs. universal) – siehe §6. |
|
||||
| **Skill-Scoring & Discovery (ohne LLM)** | Router `skill_profiles.py` + Modul `skill_scoring.py`: u. a. `GET …/skill-profile` für **Rahmenprogramm**, **Trainingsmodul**, **Progressionsgraph**; `POST /skill-profiles/batch-summaries`; **`GET /api/skill-discovery/suggestions`** (Match Bibliotheksartefakte ⇄ `skill_ids`, mit `library_content_visibility_sql`). | Ergänzt §3 **Stufe 3**: deterministische **Skill-Abdeckung / Artefakt-Discovery** ist **bereits vorhanden** und kann später die **Planungs-KI** speisen (Ziel-Skill-Mengen, Vergleich „Profil des Rahmens“) – ersetzt aber **nicht** die Top‑K-Selektion aus dem **Übungskatalog** für eine konkrete Session. |
|
||||
| **Profil / Planungs-Präferenzen** | `profiles.training_planning_prefs` (JSONB, vgl. `MODULE_VERSIONS` → `profiles`), Planungsmodul mit u. a. **Vorlagen inkl. Split-Sessions** (`planning`), `training_units` mit **Publish in Rahmen-Slot-Blueprint**. | Zukünftige KI-Planung kann **Prefs** und **Vorlagen-Struktur** als weiche Constraints einbeziehen; Rahmen↔Einheit-Fluss ist produktiv erweitert – für KI nur relevant, sobald Planungs-Endpunkte angebunden werden. |
|
||||
| **Übungsliste API** | Keyset-Pagination u. a. `cursor_updated_at` + Tie-break `id` (`exercises`-Modul laut `MODULE_VERSIONS`). | Retrieval-Pipelines sollten **cursorbasiert** paginieren, nicht „alle IDs auf einmal“ laden. |
|
||||
|
||||
**Nächster produktiver Fokus:** Prompt-/Admin‑UI zur Pflege von `ai_prompts`, **Rate-Limits**, optional **Auto-KI beim Speichern**; danach Übergang zur **Planungs-KI** laut diesem Dokument.
|
||||
|
||||
**Architektur-Vorschau (Planungs-KI):** Damit die **kleinere, starre** Übungs-Pipeline nicht zur stillen Vorlage für Planung wird, sind **eigenes Modul**, **stufenweise Outputs mit Validierung** und ein **kompaktes Kontext-DTO** vorgesehen — siehe **`working/AI_PLANNING_KI_MULTISTAGE_FORECAST.md`**.
|
||||
|
||||
---
|
||||
|
||||
## 2. Kernproblem: Skalierung des Kontextes
|
||||
|
||||
Aus einer **großen Übungssammlung** („>1000 Übungen“) können weder alle **Felder** (Ziele, Ablauf, Skills, Varianten …) noch alle **Zeilen** sinnvoll in einen LLM-Prompt.
|
||||
|
||||
**Abgrenzung Übungsanlage (aktueller Prioritätspfad):** Hier geht der Prompt typischerweise von **einzelnen** Freitexten (`title`, `goal`, `execution`, …) und einem **Skills-Katalog-Auszug** aus – nicht vom gesamten Übungsbestand. Trotzdem gilt: Aktive Skills **paginieren** oder **stufig** laden (Subset + zweite Runde nur für Kurzliste), keine vollständigen Romane aus `skill_level_definitions` für hunderte Fähigkeiten auf einmal.
|
||||
|
||||
**Festlegung (Planungs-KI):** Der LLM-Prompt erhält immer nur ein **begrenztes Kontext-Paket** mit:
|
||||
|
||||
| Paketteil | Zweck |
|
||||
|-----------|--------|
|
||||
| **Auftrag** | z. B. Sektionstyp, Dauerbudget, Schwierigkeit, erlaubte Phasen/Streams |
|
||||
| **Hard Constraints** | Gruppe, Termin/Zeitraum, Governance-Filter bereits angewendet |
|
||||
| **Komprimierte Historie** | Letzte *N* Einheiten als **Liste von Übungs-IDs + Kurzlabels** (+ optional Haupt-Skills), keine vollen Fließtexte |
|
||||
| **Ziele / Rahmen** | Kurztexte aus Rahmen-Slot/Zielblöcken oder Trainer-Prompt |
|
||||
| **Kandidaten-Top‑K** | z. B. 30–120 Übungen, **je Zeile gekürzt** (Titel, `summary`, 2–5 Skill-Namen/Stufen); **nie** der gesamte Katalog |
|
||||
| **Strukturierte Kanten optional** | Kleine Mengen Kanten aus Progressionsgraph: „Nachbarn von zuletzt genutzten Übungen“ |
|
||||
|
||||
**Zahlen‑Richtwerte (überarbeitungsfähig):**
|
||||
Kandidaten **vor** dem LLM typischerweise **50–150** Einträge; im Prompt durch Token-Limit weiter **truncate** oder **zweistufig** (grober Ranking-Schritt ohne LLM, dann finer mit LLM auf Top‑40).
|
||||
|
||||
---
|
||||
|
||||
## 3. Pipeline: „Selektion vor dem Prompt“
|
||||
|
||||
Die **„optimale“** Auswahl entsteht **nicht**, indem das Modell den Katalog „im Kopf“ hält, sondern über eine **mehrstufige Pipeline**:
|
||||
|
||||
### Stufe 1 – Harte Filter (deterministisch, DB)
|
||||
|
||||
Synchron zur bestehenden Suche/List-API-Logik, z. B.:
|
||||
|
||||
- Sichtbarkeit / Verein / `official`‑Regeln
|
||||
- Aktivitäts-/Archiv-Status der Übung
|
||||
- Fokusbereich, Stil, Zielgruppe (wenn Trainings-/Gruppenkontext das vorgibt)
|
||||
- Ausschluss bereits in **dieser Einheit** fester Übernutzung (optional)
|
||||
|
||||
Ergebnis: Menge \(M\) – kann noch sehr groß sein.
|
||||
|
||||
### Stufe 2 – Kontext-Verankerung (deterministisch + Graph)
|
||||
|
||||
- **Historie:** aus letzten *N* Gruppeneinheiten extrahierte `exercise_id`s (optional Variant).
|
||||
- **Progressionsgraph:** ausgehend davon Nachbarn (eingehend/ausgehend begrenzte Tiefe) – bereits im Produkt als **unterstützend** modelliert (**CURR‑013**).
|
||||
- **Rahmen/Slot-Ziele:** Überlapp mit Skill-Tags oder Stichwortliste (falls formalisiert).
|
||||
- **Variantenketten:** `prerequisite_variant_id` / `progression_level` nur innerhalb bereits gewählter Übung prüfen oder als Hint an den LLM-Block durchreichen.
|
||||
|
||||
Ergebnis: **„Anker‑Menge“** + **„Graph‑Nachbarschaft“** → priorisierte Kandidaten.
|
||||
|
||||
### Stufe 3 – Weiches Ranking / Retrieval (halb-strukturiert)
|
||||
|
||||
Mindestens **eine** der folgenden Optionen – kombinierbar:
|
||||
|
||||
1. **Skill-/Facet-Overlap:** Punktezahl, wenn Übungs-Skills mit Ziel-/Matrix-Schwerpunkten übereinstimmen (bereits Daten in `exercise_skills`).
|
||||
2. **Diversitäts-/Wiederholungsstrafe:** häufig in letzten Wochen geübte Übungen abwerten.
|
||||
3. **Textsuche:** PostgreSQL **`tsvector`/Volltext** auf `title`, `summary`, ggf. gekürzte `goal` – für Trainer-Stichwort „Koordination Sprung“.
|
||||
4. **Semantische Suche:** Embeddings + **Ähnlichkeitsuche** auf Kurztexte (siehe §5).
|
||||
5. **Skill-Discovery über Planungs-Artefakte (bereits implementiert):** `GET /api/skill-discovery/suggestions` matching **Bibliotheksartefakte** (u. a. Rahmenprogramm, Trainingsmodul, Progressionsgraph) gegen gegebene `skill_ids`; `GET …/skill-profile` liefert **gewichtete Fähigkeitsprofile** aus den dort verknüpften Übungen (siehe `SKILL_SCORING_SPEC.md`). Das ist ein **deterministischer** Baustein für „welche Artefakte passen zu diesen Skills?“, **nicht** der Ersatz für **Top‑K-Übung**-Auswahl in einer konkreten Session – dort weiter Stufen 1–2 + Punkte 1–4/LLM.
|
||||
|
||||
Ergebnis: sortierte Liste, **Top‑K** für den Prompt.
|
||||
|
||||
### Stufe 4 – LLM (optional zweistufig)
|
||||
|
||||
- **Optional 1:** LLM nur **sortiert/rankted** bereits vorgegebene IDs (Ranking mit kurzer Begründung).
|
||||
- **Optional 2:** Zwei Calls: erst „welche drei Schwerpunkte“ / „Welche Constraints habe ich übersehen?“, zweiter Call nur mit gekürztem Top‑K – nur wenn UX den Mehraufwand rechtfertigt.
|
||||
|
||||
**Ausgabe-Contract:** Zurückkommen dürfen **nur gültige `exercise_id`s** aus der übergebenen Kandidatenliste (Server validiert gegen Set); **Halluzinationsrisiko damit entschärft**.
|
||||
|
||||
---
|
||||
|
||||
## 4. Antwort auf die konkrete Frage: „Alle Übungen in den Prompt?“
|
||||
|
||||
**Nein.** Workflow:
|
||||
|
||||
1. **DB + Regeln + Graph + Historie** reduzieren auf **einige Hundert bis wenige tausend** Rohzeilen höchstens **intern** – aber
|
||||
2. in den **LLM-Prompt** gehen nur **Top‑K kompakte Artefakte** (siehe §2).
|
||||
|
||||
Das LLM löst dann **Ranking, Reihenfolge, Timing-Hinweise, Trainer-sprachliche Kurzkommentare** – nicht die Frage „gibt es diese Übung überhaupt im System?“.
|
||||
|
||||
---
|
||||
|
||||
## 5. Vector DB (z. B. Qdrant) – wann nötig, wann nicht?
|
||||
|
||||
### 5.1 Ziel embeddings
|
||||
|
||||
Semantic Retrieval: „wie springt Coordinative Belastung ohne das Wort ‚Koordination‘ im Titel zu stehen.“ Das ist **über** reine Filtersuche und oft **über** stumpfe Volltextsuche erreichbar.
|
||||
|
||||
### 5.2 Option A – ohne separate Vector DB
|
||||
|
||||
- **PostgreSQL + `pgvector`** (Extension): Embeddings-Spalte an `exercises` (oder an „Search Document“-Zeilen), Indices, Abfrage zusammen mit SQL-Filtern in **einer Transaktions-DB**.
|
||||
- **Größenordnung** einige 10 k–100 k Zeilen für Übungen: für Shinkan **oft ausreichend**.
|
||||
- Vorteile: ein Betriebspfad, Mandanten-/Governance weiter in SQL lösen; Backup wie heute.
|
||||
|
||||
### 5.3 Option B – Qdrant (oder anderer Dediz-Vektorstore)
|
||||
|
||||
Sinnvoller zeitlicher Punkt oder technische Auslöser:
|
||||
|
||||
- sehr hohe Latenz-Anforderung mit **Hybrid-Filter** über viele kombinierte Metadaten in nahezu Echtzeit,
|
||||
- Entkopplung: Embedding-Pipeline läuft asynchron und soll die **Operational DB** nicht beschweren,
|
||||
- später **mehrsprachig** oder **mehrere Embedding-Versionen**/Re-Index ohne großen Migrationstress,
|
||||
- Team bevorzugt **Dedicated** Vector-Ops gegenüber Postgres-Ops.
|
||||
|
||||
### 5.4 Empfehlung für diese Codebasis (überarbeitungsfähig)
|
||||
|
||||
1. **Phase 1:** Harte Filter + Graph + Historie + **PostgreSQL-Volltext** + Top‑K; LLM erst auf komprimierten Kandidaten → hoher Nutzen ohne neuen Infrastructure-Typen.
|
||||
2. **Phase 2:** Bei nachweisbaren „Recall-Lücken“ (Trainer findet nichts Passendes trotz großem Korpus) **`pgvector` in Postgres** evaluieren – **vor** zusätzlicher Infrastruktur wie Qdrant.
|
||||
3. **Phase 3:** Qdrant (o. ä.) wenn Größenordnung, Betrieb oder Produkt-Anforderungen **pgvector** sprengen oder klar eine **embedding-first** Produktstraße geplant wird.
|
||||
|
||||
**Fazit:** Eine dedizierte **Vector DB ist nicht zwingende Voraussetzung** für vernünftige Selektion; sie ist eine **Ausbaustufe**, wenn **semantische Lücke** und Skalierung es rechtfertigen. **Selektion** ist immer **„Filter + Ranking + kleines Top‑K“**, unabhängig vom Speicherort der Vektoren.
|
||||
|
||||
---
|
||||
|
||||
## 6. Datenpflege für gutes Retrieval (fachlicher Hebel)
|
||||
|
||||
Retrieval‑Qualität hängt stärker an **Metadaten** als an der Embedding-Technologie allein:
|
||||
|
||||
- verlässliche **Skills** (`exercise_skills`, ggf. KI-geholfen bereits laut Spez beim Übungs-Anlegen); `exercise_skills.ai_suggested` und kanonische Stufen (`required_level` / `target_level` als Slugs) für Nachvollziehbarkeit.
|
||||
- **`skills`-Stamm:** `description`, **`karate_relevance`**, **`relevance_level` (1–3)**, **`focus_areas`**, Kategorien/Keywords für **Prompt-Kontext** beim Skill-Mapping bei der Übungsanlage; optional **`skill_level_definitions`** für Stufen 1–5 **gezielt** in die zweite Prompt-Runde (nur Kurzliste Kandidaten).
|
||||
- sinnvolle **`summary`**-Felder für Karten/Liste/KI-Pack;
|
||||
- **Progressionsgraph** dort, wo pädagogische Ketten gefestigt sind;
|
||||
- konsistente **Fokusbereich/Stil**-Zuordnung.
|
||||
|
||||
Das fachliche Konzept sollte entscheiden: **wie viel automatische Pflege vs. Trainer-Pflichtfelder**.
|
||||
|
||||
---
|
||||
|
||||
## 7. Produkt-/Release-Stufen (Anknüpfung)
|
||||
|
||||
Priorität **jetzt**: **Übungsanlage**, danach **Planung**.
|
||||
|
||||
| Stufe | Nutzen | Technik-Schwerpunkt |
|
||||
|-------|--------|---------------------|
|
||||
| **A0** | **Zentraler KI-Service** (ein Modul/Hilfslayer), Prompts aus `ai_prompts` | OpenRouter oder vereinbarter Provider, Timeouts, `503` ohne Key, Parsing/Validation |
|
||||
| **A1** | **Übungsanlage** (vgl. `KI_FEATURES_SPEC`): `summary`, Skill-Vorschläge inkl. Stufen/Intensität, optional Textglättung | `POST /api/exercises/ai/suggest`, `POST /api/exercises/{id}/ai/regenerate`; Prompt-Kontext: Skills mit `description`, `karate_relevance`, `relevance_level`, optional `skill_level_definitions` für Kurzliste; DB: `summary_ai_generated`, `exercise_skills.ai_suggested` |
|
||||
| B | „Übungen für Abschnitt vorschlagen“ | Pipeline §3 Stufen 1–3 + Prompt mit Top‑K (Übungsliste **keyset-pagination** beachten) |
|
||||
| C | Reihenfolge / Zeitslots innerhalb bestehender Sektion | Graph + LLM Ranking |
|
||||
| D | Ganze Einheit (inkl. Phasen/Streams vereinfacht) | strukturiertes JSON + strikte Schema-Validation gegen bestehende `PUT`-Payloads |
|
||||
| E | Mehreinheiten / Rahmen‑Alignment | Ziele aus Rahmenprogramm, Serie von Slots; **Skill-Profile** (`…/skill-profile`) als Kontextuelle Verstärker |
|
||||
|
||||
Die **Selektions‑Pipeline §3** bleibt für **Planungs**-KI konsistent und wird parametrierbar erweitert; **§1.1** spiegelt den **aktuellen Implementierungs**-Vorsprung (Skill-Scoring ohne LLM) wider.
|
||||
|
||||
---
|
||||
|
||||
## 8. Compliance & Datenschutz (Kurzhinweis)
|
||||
|
||||
- Datenminimierung: **keine Teilnehmerliste** ohne expliziten Scope; Kontext über **Einheiten-Metadaten** und Übungen.
|
||||
- **OpenRouter**/Modellwahl: Organisation intern klären (AV/Verarbeitungsvertrag, Datenflüsse außerhalb EU – siehe Repo-Compliance-Dokumente).
|
||||
- **Logging:** Prompts keine unnötigen personenbezogenen Daten; wenn Debug: Retention definieren.
|
||||
|
||||
---
|
||||
|
||||
## 9. Offene Punkte für die fachliche Verfeinerung
|
||||
|
||||
- Gewichtung „**Wiederholung** vs. **Progression** vs. **Motivation**“ (domänenspezifisch).
|
||||
- Umgang mit **Kombinationsübungen** und **Coach-Stufen B/C** in der Datenübergabe.
|
||||
- Soll das System **„Lücken“** aus der **Matrix-Auflösung** aktiv quantifizieren oder nur Narrative verwenden?
|
||||
- Akzeptierte **Übersetzungen**: nur Deutsch oder mehrsprachige Embeddings erforderlich?
|
||||
|
||||
---
|
||||
|
||||
## 10. Glossar
|
||||
|
||||
| Begriff | Bedeutung |
|
||||
|---------|-----------|
|
||||
| **Top‑K** | Feste kleine Obergrenze Übungen pro LLM-Anfrage |
|
||||
| **Hard filter** | Deterministische SQL-/Policy-Einschränkung vor KI |
|
||||
| **Kontext-Paket** | Zusammengesetztes, tokenlimitiertes Eingabeobjekt für den Prompt |
|
||||
| **Retrieval** | algorithmischer Schritt ohne Generierung („wer kommt überhaupt in Frage“) |
|
||||
331
.claude/docs/technical/CAPABILITY_CATALOG.v1.md
Normal file
331
.claude/docs/technical/CAPABILITY_CATALOG.v1.md
Normal file
|
|
@ -0,0 +1,331 @@
|
|||
# Capability-Katalog Shinkan v1
|
||||
|
||||
**Status:** Konzept (verbindliche Zieldefinition; M3 teilweise umgesetzt)
|
||||
**Stand:** 2026-06-06
|
||||
**Bezüge:** `ACCESS_LAYER_AND_GOVERNANCE_PLAN.md` (Stufe E), `MULTI_TENANCY_RBAC_ARCHITECTURE.md`, `CLUB_MEMBERSHIP_AND_FEATURES.v1.md`, **`MEMBERSHIP_RBAC_DECISIONS_2026-06.md`** (Produktentscheidungen)
|
||||
|
||||
---
|
||||
|
||||
## 1. Zweck
|
||||
|
||||
Dieses Dokument definiert **benannte Capabilities** (Wer darf welche **Funktion** ausführen?) — getrennt von:
|
||||
|
||||
- **Governance** (Darf ich *dieses Objekt* lesen/ändern? → `visibility`, `club_id`, `created_by`)
|
||||
- **Feature-Limits** (Wie viel darf der **Verein**? → `CLUB_MEMBERSHIP_AND_FEATURES.v1.md`)
|
||||
|
||||
Capabilities beantworten: *„Darf ein Trainer mit Rolle X die Funktion Y im Verein Z überhaupt nutzen?“*
|
||||
|
||||
---
|
||||
|
||||
## 2. Namenskonvention
|
||||
|
||||
```
|
||||
{domain}.{action}[.{qualifier}]
|
||||
```
|
||||
|
||||
| Segment | Beispiele |
|
||||
|---------|-----------|
|
||||
| `domain` | `exercises`, `media`, `planning`, `org`, `platform` |
|
||||
| `action` | `read`, `create`, `update`, `delete`, `manage`, `execute` |
|
||||
| `qualifier` | `ai.suggest`, `join_request`, `inbox.review` |
|
||||
|
||||
**CRUD-Mapping:**
|
||||
|
||||
| Aktion | Capability-Suffix | Bedeutung |
|
||||
|--------|-------------------|-----------|
|
||||
| Lesen (Listen/Detail) | `.read` | Navigation + API-Lesen erlaubt |
|
||||
| Anlegen | `.create` | POST/INSERT |
|
||||
| Bearbeiten | `.update` | PUT/PATCH (eigenes + berechtigtes Fremdes) |
|
||||
| Löschen | `.delete` | DELETE (strenger als update) |
|
||||
| Verwalten | `.manage` | Org-Funktionen, Freigaben, Mitglieder |
|
||||
| Ausführen (ohne Persistenz) | `.execute` | z. B. KI-Vorschau, Coach-Lauf |
|
||||
|
||||
Objektbezogene Feinheiten (nur Ersteller, nur Vereinsadmin des Objekt-Vereins) bleiben in **Governance** — Capabilities sind das **Tür-Schloss** davor.
|
||||
|
||||
---
|
||||
|
||||
## 3. Account-Lifecycle (Voraussetzung für Capabilities)
|
||||
|
||||
| `account_state` | Bedingung | Typische Capabilities |
|
||||
|-----------------|-----------|------------------------|
|
||||
| `anonymous` | Keine Session | nur öffentliche Routen (`/login`, Rechtstexte, `clubs/public-directory`) |
|
||||
| `unverified` | Session, `email_verified=false` | `account.resend_verification`, `account.logout` |
|
||||
| `verified_pending_club` | Verifiziert, keine aktive `club_members` | `club.join_request`, `club.creation_request` (M7), `account.settings` — **kein** Lesezugriff auf Domänen-Inhalte (siehe Entscheidungs-Doc §1.1) |
|
||||
| `active_member` | Mind. eine aktive Vereinsmitgliedschaft | Domänen-Capabilities gemäß Vereinsrolle |
|
||||
| `platform_admin` | `role` ∈ `admin`, `superadmin` | `platform.*` zusätzlich |
|
||||
|
||||
**Regel:** Domänen-Capabilities (`exercises.*`, `planning.*`, …) erfordern mindestens `active_member`, sofern nicht `platform_admin`.
|
||||
|
||||
---
|
||||
|
||||
## 4. Rollen-Scopes
|
||||
|
||||
### 4.1 Portal-Rollen (`profiles.role`)
|
||||
|
||||
| Rolle | Scope | Kurz |
|
||||
|-------|-------|------|
|
||||
| `user` | Portal | Standard nach Registrierung (Zielbild; heute oft `trainer` Legacy) |
|
||||
| `trainer` | Portal | Legacy — mittelfristig durch `user` + Vereinsrollen ersetzen |
|
||||
| `admin` | Portal | Plattform-Admin (Vereine anlegen, erweiterte Ops) |
|
||||
| `superadmin` | Portal | Vollzugriff Plattform + Superadmin-Werkzeuge |
|
||||
|
||||
### 4.2 Vereinsrollen (`club_member_roles.role_code`)
|
||||
|
||||
| Rolle | Fachlich |
|
||||
|-------|----------|
|
||||
| `club_admin` | Vereinsorganisation, Mitglieder, Struktur |
|
||||
| `trainer` | Planung, Übungen, Durchführung |
|
||||
| `content_editor` | Inhalte pflegen (Bibliothek) |
|
||||
| `division_lead` | Spartenleitung (später division-scope) |
|
||||
|
||||
Mehrfachrollen pro Mitgliedschaft sind möglich (OR-Verknüpfung der Capabilities).
|
||||
|
||||
### 4.3 Mapping heutiger Helfer → Capabilities
|
||||
|
||||
| Heutiger Code (`club_tenancy.py`) | Ziel-Capability-Cluster |
|
||||
|-----------------------------------|-------------------------|
|
||||
| `can_manage_club_org` | `org.structure.manage`, `org.members.manage`, `org.inbox.review` |
|
||||
| `can_plan_in_club` | `planning.*`, `exercises.create/update`, `modules.*`, `framework.*` |
|
||||
| `is_platform_admin` | `platform.*` (Bypass Mandant, Audit-Pflicht) |
|
||||
| `is_superadmin` | `platform.superadmin.*` |
|
||||
|
||||
---
|
||||
|
||||
## 5. Capability-Katalog (v1)
|
||||
|
||||
Legende Spalten:
|
||||
|
||||
- **Min. Account:** `verified_pending_club` | `active_member` | `platform_admin`
|
||||
- **Vereinsrollen:** leer = alle aktiven Mitglieder; sonst mindestens eine Rolle
|
||||
- **Feature-ID:** optionales Kontingent (siehe Club-Membership-Doc); leer = kein Limit
|
||||
- **Governance:** zusätzliche Objektprüfung ja/nein
|
||||
|
||||
### 5.1 Account & Onboarding
|
||||
|
||||
| Capability-ID | Min. Account | Vereinsrollen | Feature-ID | Endpoints / UI |
|
||||
|---------------|--------------|---------------|------------|----------------|
|
||||
| `account.settings.read` | `unverified` | — | — | `GET /profiles/me`, Einstellungen |
|
||||
| `account.settings.update` | `unverified` | — | — | `PUT /profiles/{id}` (eigenes Profil) |
|
||||
| `account.password.change` | `unverified` | — | — | `PUT /api/auth/pin` |
|
||||
| `account.resend_verification` | `unverified` | — | — | `POST /api/auth/resend-verification` |
|
||||
| `club.directory.read` | `verified_pending_club` | — | — | `GET /clubs/public-directory` |
|
||||
| `club.join_request.create` | `verified_pending_club` | — | — | `POST /me/club-join-requests`, Registrierung mit `requested_club_id` |
|
||||
| `club.join_request.withdraw` | `verified_pending_club` | — | — | `DELETE /me/club-join-requests/{id}` |
|
||||
| `club.join_request.read_own` | `verified_pending_club` | — | — | `GET /me/club-join-requests` |
|
||||
|
||||
### 5.2 Organisation (Verein)
|
||||
|
||||
| Capability-ID | Min. Account | Vereinsrollen | Feature-ID | Endpoints / UI |
|
||||
|---------------|--------------|---------------|------------|----------------|
|
||||
| `org.club.read` | `active_member` | * | — | `GET /clubs`, `GET /clubs/{id}` (eigene Vereine) |
|
||||
| `org.club.create` | `platform_admin` | — | — | `POST /clubs` |
|
||||
| `org.club.update` | `platform_admin` | `club_admin` | — | `PUT /clubs/{id}` |
|
||||
| `org.club.delete` | `platform_admin` | — | — | `DELETE /clubs/{id}` |
|
||||
| `org.structure.manage` | `active_member` | `club_admin` | `training_groups` | Sparten, Gruppen CRUD |
|
||||
| `org.members.read` | `active_member` | `club_admin` | — | `GET /clubs/{id}/members` |
|
||||
| `org.members.manage` | `active_member` | `club_admin` | `active_members` | POST/PUT/DELETE Mitglieder |
|
||||
| `org.members.directory` | `active_member` | * | — | `GET /clubs/{id}/members/directory` (ohne E-Mail für Nicht-Admins) |
|
||||
| `org.join_request.review` | `active_member` | `club_admin` | — | Join-Request accept/reject, Inbox |
|
||||
| `org.inbox.read` | `active_member` | `club_admin` | — | Posteingang Join + Content-Reports |
|
||||
|
||||
### 5.3 Übungen
|
||||
|
||||
| Capability-ID | Min. Account | Vereinsrollen | Feature-ID | Governance |
|
||||
|---------------|--------------|---------------|------------|------------|
|
||||
| `exercises.read` | `active_member` | * | — | ja (visibility) |
|
||||
| `exercises.create` | `active_member` | `trainer`, `content_editor`, `club_admin`, `division_lead` | `exercises` | — |
|
||||
| `exercises.update` | `active_member` | `trainer`, `content_editor`, `club_admin`, `division_lead` | — | ja |
|
||||
| `exercises.delete` | `active_member` | `club_admin` (+ Ersteller privat) | — | ja |
|
||||
| `exercises.bulk_metadata` | `active_member` | `content_editor`, `club_admin` | — | ja |
|
||||
| `exercises.ai.suggest` | `active_member` | `trainer`, `content_editor`, `club_admin` | `ai_calls` | — |
|
||||
| `exercises.ai.regenerate` | `active_member` | `trainer`, `content_editor`, `club_admin` | `ai_calls` | ja (edit) |
|
||||
| `exercises.media.read` | `active_member` | * | — | ja |
|
||||
| `exercises.media.upload` | `active_member` | `trainer`, `content_editor`, `club_admin` | `exercise_media` | ja |
|
||||
| `exercises.variants.manage` | `active_member` | `trainer`, `content_editor`, `club_admin` | — | ja |
|
||||
|
||||
**Representative Endpoints:** `/api/exercises*`, `/api/exercises/ai/*`, Medien-Datei-Download.
|
||||
|
||||
### 5.4 Medien-Bibliothek (Archiv)
|
||||
|
||||
| Capability-ID | Min. Account | Vereinsrollen | Feature-ID | Governance |
|
||||
|---------------|--------------|---------------|------------|------------|
|
||||
| `media.library.read` | `active_member` | * | — | ja |
|
||||
| `media.library.upload` | `active_member` | `trainer`, `content_editor`, `club_admin` | `exercise_media` | ja |
|
||||
| `media.library.update` | `active_member` | `trainer`, `content_editor`, `club_admin` | — | ja |
|
||||
| `media.library.lifecycle` | `active_member` | `trainer`, `club_admin` | — | ja |
|
||||
| `media.rights.declare` | `active_member` | `trainer`, `club_admin` | — | ja |
|
||||
| `media.admin.rights_review` | `platform_admin` | — | — | Plattform-Admin Legacy-Review |
|
||||
|
||||
### 5.5 Trainingsmodule & Rahmenprogramme
|
||||
|
||||
| Capability-ID | Min. Account | Vereinsrollen | Feature-ID | Governance |
|
||||
|---------------|--------------|---------------|------------|------------|
|
||||
| `modules.read` | `active_member` | * | — | ja |
|
||||
| `modules.create` | `active_member` | `trainer`, `content_editor`, `club_admin` | `training_programs` | — |
|
||||
| `modules.update` | `active_member` | `trainer`, `content_editor`, `club_admin` | — | ja |
|
||||
| `modules.delete` | `active_member` | `club_admin` (+ Ersteller) | — | ja |
|
||||
| `framework.read` | `active_member` | * | — | ja |
|
||||
| `framework.create` | `active_member` | `trainer`, `club_admin` | `training_programs` | — |
|
||||
| `framework.update` | `active_member` | `trainer`, `club_admin` | — | ja |
|
||||
| `framework.delete` | `active_member` | `club_admin` (+ Ersteller) | — | ja |
|
||||
| `plan_templates.read` | `active_member` | * | — | ja |
|
||||
| `plan_templates.manage` | `active_member` | `trainer`, `club_admin` | — | ja |
|
||||
|
||||
### 5.6 Progressionspfade
|
||||
|
||||
| Capability-ID | Min. Account | Vereinsrollen | Feature-ID | Governance |
|
||||
|---------------|--------------|---------------|------------|------------|
|
||||
| `progression.read` | `active_member` | * | — | ja |
|
||||
| `progression.manage` | `active_member` | `trainer`, `content_editor`, `club_admin` | — | ja |
|
||||
|
||||
### 5.7 Planung & Durchführung
|
||||
|
||||
| Capability-ID | Min. Account | Vereinsrollen | Feature-ID | Governance |
|
||||
|---------------|--------------|---------------|------------|------------|
|
||||
| `planning.calendar.read` | `active_member` | * | — | ja (Gruppe/Verein) |
|
||||
| `planning.units.create` | `active_member` | `trainer`, `club_admin`, `division_lead` | `training_units` | ja |
|
||||
| `planning.units.update` | `active_member` | `trainer`, `club_admin`, `division_lead` | — | ja |
|
||||
| `planning.units.delete` | `active_member` | `club_admin`, `trainer` (eigene) | — | ja |
|
||||
| `planning.units.run` | `active_member` | `trainer`, `club_admin`, `division_lead` | — | ja |
|
||||
| `planning.coach.execute` | `active_member` | `trainer`, `club_admin` | — | ja |
|
||||
| `planning.ai.suggest` | `active_member` | `trainer`, `club_admin` | `ai_calls` | — |
|
||||
| `planning.ai.progression_path` | `active_member` | `trainer`, `club_admin` | `ai_calls` | — |
|
||||
|
||||
### 5.8 Fähigkeiten & Scoring
|
||||
|
||||
| Capability-ID | Min. Account | Vereinsrollen | Feature-ID | Governance |
|
||||
|---------------|--------------|---------------|------------|------------|
|
||||
| `skills.catalog.read` | `active_member` | * | — | globaler Katalog |
|
||||
| `skills.discovery.read` | `active_member` | `trainer`, `content_editor` | — | — |
|
||||
| `skill_profiles.read` | `active_member` | * | — | ja (Artefakt) |
|
||||
|
||||
### 5.9 Governance & Meldungen
|
||||
|
||||
| Capability-ID | Min. Account | Vereinsrollen | Feature-ID |
|
||||
|---------------|--------------|---------------|------------|
|
||||
| `governance.content_report.create` | `active_member` | * | — |
|
||||
| `governance.content_report.review` | `active_member` | `club_admin` | — |
|
||||
| `governance.change_request.*` | `active_member` | `content_editor`, `club_admin` | — |
|
||||
|
||||
### 5.10 Plattform (nur Portal-Admin / Superadmin)
|
||||
|
||||
| Capability-ID | Min. Account | Portal-Rolle | Feature-ID |
|
||||
|---------------|--------------|--------------|------------|
|
||||
| `platform.admin.access` | `platform_admin` | `admin`, `superadmin` | — |
|
||||
| `platform.users.manage` | `platform_admin` | `superadmin` | — |
|
||||
| `platform.catalogs.manage` | `platform_admin` | `superadmin` | — |
|
||||
| `platform.maturity_models.manage` | `platform_admin` | `superadmin` | — |
|
||||
| `platform.wiki_import.execute` | `platform_admin` | `superadmin` | `wiki_import` |
|
||||
| `platform.ai_prompts.manage` | `platform_admin` | `superadmin` | — |
|
||||
| `platform.exercise_enrichment.execute` | `platform_admin` | `superadmin` | `ai_calls` |
|
||||
| `platform.user_content.moderate` | `platform_admin` | `superadmin` | — |
|
||||
| `platform.legal_documents.manage` | `platform_admin` | `superadmin` | — |
|
||||
| `platform.media_storage.manage` | `platform_admin` | `superadmin` | — |
|
||||
| `platform.club_creation.approve` | `platform_admin` | `superadmin` | — |
|
||||
|
||||
*Geplant:* `club.creation_request.submit` → `verified_pending_club`; Freigabe über `platform.club_creation.approve`.
|
||||
|
||||
---
|
||||
|
||||
## 6. Standard-Zuordnung Vereinsrolle → Capabilities (v1, fest)
|
||||
|
||||
Diese Tabelle ist die **initiale** Grant-Matrix (`club_role_capability_grants`). Später durch Custom Roles ersetzbar — gleiche Capability-IDs.
|
||||
|
||||
| Capability-Cluster | `club_admin` | `trainer` | `content_editor` | `division_lead` |
|
||||
|--------------------|:------------:|:---------:|:----------------:|:---------------:|
|
||||
| `org.structure.manage` | ✓ | — | — | ✓ (eigene Sparte, später) |
|
||||
| `org.members.manage` | ✓ | — | — | — |
|
||||
| `org.join_request.review` | ✓ | — | — | — |
|
||||
| `exercises.read` | ✓ | ✓ | ✓ | ✓ |
|
||||
| `exercises.create/update` | ✓ | ✓ | ✓ | ✓ |
|
||||
| `exercises.delete` | ✓ | — | — | — |
|
||||
| `exercises.ai.*` | ✓ | ✓ | ✓ | ✓ |
|
||||
| `media.library.*` | ✓ | ✓ | ✓ | ✓ |
|
||||
| `modules.*` / `framework.*` | ✓ | ✓ | ✓ | ✓ |
|
||||
| `planning.*` | ✓ | ✓ | — | ✓ |
|
||||
| `planning.coach.execute` | ✓ | ✓ | — | ✓ |
|
||||
| `governance.content_report.review` | ✓ | — | — | — |
|
||||
|
||||
---
|
||||
|
||||
## 7. API-Vertrag (Ziel)
|
||||
|
||||
### 7.1 Effektive Rechte für Frontend
|
||||
|
||||
```
|
||||
GET /api/me/entitlements?club_id={optional}
|
||||
```
|
||||
|
||||
Antwort (Ausschnitt):
|
||||
|
||||
```json
|
||||
{
|
||||
"account_state": "active_member",
|
||||
"portal_role": "user",
|
||||
"club_id": 12,
|
||||
"club_roles": ["trainer"],
|
||||
"capabilities": {
|
||||
"exercises.read": true,
|
||||
"exercises.ai.suggest": true,
|
||||
"org.members.manage": false
|
||||
},
|
||||
"features": {
|
||||
"ai_calls": { "allowed": true, "used": 4, "limit": 50, "remaining": 46, "reset_at": "2026-07-01T00:00:00Z" }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Frontend: Navigation und Buttons nur aus dieser Antwort — **keine** duplizierten Rollen-Checks in JSX (Ausnahme: rein kosmetische Labels).
|
||||
|
||||
### 7.2 Backend-Enforcement
|
||||
|
||||
Zentral (Zielmodul `authorization/capabilities.py` oder Erweiterung `club_tenancy.py`):
|
||||
|
||||
```python
|
||||
assert_capability(tenant, "exercises.ai.suggest", club_id=tenant.effective_club_id)
|
||||
assert_club_feature(tenant, "ai_calls", club_id=tenant.effective_club_id) # siehe Club-Membership-Doc
|
||||
# + bestehende Governance auf Objekt-Ebene
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Implementierungsreihenfolge (Capabilities)
|
||||
|
||||
| Phase | Inhalt |
|
||||
|-------|--------|
|
||||
| C0 | Account-Gates (`unverified`, `verified_pending_club`) — ohne Capability-DB |
|
||||
| C1 | `capabilities` + `club_role_capability_grants` seed aus §5–6 |
|
||||
| C2 | `GET /api/me/entitlements` + Frontend-Nav |
|
||||
| C3 | Enforcement: KI-Endpoints, `exercises.create`, `planning.*` |
|
||||
| C4 | Restliche Router schrittweise; Audit in `ACCESS_LAYER_ENDPOINT_AUDIT.md` |
|
||||
| C5 | Custom Roles (optional) — gleiche IDs |
|
||||
|
||||
---
|
||||
|
||||
## 9. Abgrenzung & Drift-Schutz
|
||||
|
||||
1. **Neue Nutzerfunktion** → `register_capability()` in `rights_registrations/<modul>.py`, dann Endpoint mit `probe_capability`. Namenskonvention hier dokumentieren — **kein** Bulk-Seed in Migrationen.
|
||||
2. **Kontingent** → `register_feature()` im selben Modul; Consume über `consume_club_feature_with_usage`.
|
||||
3. **Kein** paralleles `if (user.role === 'trainer')` für Sicherheit — nur UX-Fallback.
|
||||
4. Capability ≠ Feature: `exercises.ai.suggest` (darf ich?) vs. `ai_calls` (wie viel übrig?).
|
||||
5. Plattform-Admin-Bypass dokumentieren und auditieren (`platform_admin` sieht Mandant, nicht automatisch alle Quotas).
|
||||
|
||||
Siehe **`docs/working/RIGHTS_AND_FEATURES_REGISTRY.md`** (Registry-first, ersetzt Katalog-first aus 079).
|
||||
|
||||
---
|
||||
|
||||
## 10. Referenzen
|
||||
|
||||
| Dokument | Inhalt |
|
||||
|----------|--------|
|
||||
| `CLUB_MEMBERSHIP_AND_FEATURES.v1.md` | Vereinsabo, Feature-Registry, Kontingente |
|
||||
| `ACCESS_LAYER_AND_GOVERNANCE_PLAN.md` | TenantContext, Governance, Stufe E |
|
||||
| `MULTI_TENANCY_RBAC_ARCHITECTURE.md` | §4.6 Vereinsabo-Zielbild |
|
||||
| `ACCESS_LAYER_ENDPOINT_AUDIT.md` | Endpoint-Pflege |
|
||||
| Mitai `FEATURE_ENFORCEMENT.md` | 4-Phasen-Rollout-Vorbild |
|
||||
|
||||
---
|
||||
|
||||
**Changelog**
|
||||
|
||||
- 2026-06-06: v1 — Initial-Katalog aus Ist-Code (`club_tenancy`, Router-Inventar) + Ziel-Onboarding.
|
||||
478
.claude/docs/technical/CLUB_MEMBERSHIP_AND_FEATURES.v1.md
Normal file
478
.claude/docs/technical/CLUB_MEMBERSHIP_AND_FEATURES.v1.md
Normal file
|
|
@ -0,0 +1,478 @@
|
|||
# Vereins-Membership & Feature-System Shinkan v1
|
||||
|
||||
**Status:** Konzept + M1–M3 teilweise produktiv (siehe Entscheidungs-Doc §2)
|
||||
**Stand:** 2026-06-06
|
||||
**Bezüge:** Schwesterprojekt Mitai (`v9c_subscription_system.sql`, `FEATURE_ENFORCEMENT.md`), `CAPABILITY_CATALOG.v1.md`, `MULTI_TENANCY_RBAC_ARCHITECTURE.md` §4.6, `ACCESS_LAYER_AND_GOVERNANCE_PLAN.md`, **`MEMBERSHIP_RBAC_DECISIONS_2026-06.md`**
|
||||
|
||||
---
|
||||
|
||||
## 1. Zweck
|
||||
|
||||
Shinkan verkauft und limitiert **nicht Einzelpersonen** (wie Mitai), sondern **Vereine**. Dieses Dokument definiert:
|
||||
|
||||
- das **Feature-Registry**-Muster (limitierbare Funktionen),
|
||||
- das **Vereins-Abo** (`club_plans`, `club_subscriptions`),
|
||||
- **Kontingente** und Enforcement,
|
||||
- die **Abbildung von Mitai** und **Vermeidung von Refactoring-Schulden**.
|
||||
|
||||
Capabilities (Rollen: *darf ich die Funktion?*) → `CAPABILITY_CATALOG.v1.md`.
|
||||
|
||||
---
|
||||
|
||||
## 2. Grundprinzip: Zwei Achsen
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph cap [Achse 1 — Capabilities]
|
||||
CR[club_role_capability_grants]
|
||||
PR[portal_role_capability_grants]
|
||||
end
|
||||
|
||||
subgraph feat [Achse 2 — Features / Kontingente]
|
||||
FP[club_plans]
|
||||
FPL[club_plan_limits]
|
||||
FS[club_subscriptions]
|
||||
FU[club_feature_usage]
|
||||
end
|
||||
|
||||
subgraph gov [Achse 3 — Governance]
|
||||
GV[visibility / club_id / created_by]
|
||||
end
|
||||
|
||||
REQ[HTTP Request] --> ACCT[Account-Lifecycle]
|
||||
ACCT --> cap
|
||||
cap --> gov
|
||||
gov --> feat
|
||||
feat --> EXEC[Ausführung + increment]
|
||||
```
|
||||
|
||||
| Frage | System | Subjekt |
|
||||
|-------|--------|---------|
|
||||
| Darf Trainer X KI nutzen? | Capability `exercises.ai.suggest` | `profile_id` + `club_role` |
|
||||
| Wie viele KI-Aufrufe hat Verein Y? | Feature `ai_calls` | **`club_id`** |
|
||||
| Darf ich diese Übung ändern? | Governance | Objekt + Mitgliedschaft |
|
||||
|
||||
**Beide Achsen müssen erfüllt sein** (AND), außer dokumentierte Plattform-Ausnahmen.
|
||||
|
||||
---
|
||||
|
||||
## 3. Mitai-Mapping (was übernehmen, was nicht)
|
||||
|
||||
### 3.1 Übernehmen (Pattern)
|
||||
|
||||
| Mitai (Person) | Shinkan (Verein) | Anmerkung |
|
||||
|----------------|------------------|-----------|
|
||||
| `features` (TEXT-PK, Registry) | `features` (`app='shinkan'`) | Gemeinsames Muster, ggf. später Jinkendo-weit |
|
||||
| `tiers` | `club_plans` | Produktdefinition |
|
||||
| `tier_limits` | `club_plan_limits` | Matrix Plan × Feature |
|
||||
| `user_feature_restrictions` | `club_feature_overrides` | Admin-Override pro Verein |
|
||||
| `user_feature_usage` | `club_feature_usage` | Verbrauch pro Verein |
|
||||
| `access_grants` | `club_access_grants` | Trial, Promo, manuelle Freischaltung |
|
||||
| `check_feature_access()` | `check_club_feature_access()` | Subjekt `club_id` |
|
||||
| `increment_feature_usage()` | `increment_club_feature_usage()` | Nur bei INSERT / KI-Call |
|
||||
| 4-Phasen-Rollout | identisch | Log → UI → Hard-Block |
|
||||
| `GET /api/features/usage` | `GET /api/clubs/{id}/entitlements` | siehe Capability-Doc §7 |
|
||||
|
||||
### 3.2 Nicht übernehmen
|
||||
|
||||
| Mitai | Shinkan-Grund |
|
||||
|-------|---------------|
|
||||
| `profiles.tier` als Haupt-Abo | Verein zahlt, nicht Einzeltrainer |
|
||||
| `subscriptions` (Shinkan `001`, INT-Features) | Ungenutzt, Schema-Drift |
|
||||
| `get_effective_tier(profile_id)` für Shinkan-Limits | Ersetzen durch `get_effective_club_plan(club_id)` |
|
||||
| Profil-zentrierte Enforcement-Hooks allein | Primär `club_id`; Profil nur für Attribution |
|
||||
|
||||
### 3.3 Parallelität Jinkendo-Familie (später)
|
||||
|
||||
`CENTRAL_SUBSCRIPTION_SYSTEM.md` (Mitai): zentrales Personen-Abo über Apps.
|
||||
|
||||
**Zielbild ohne Refactoring:**
|
||||
|
||||
```
|
||||
features.enforcement_subject ∈ { 'club', 'profile', 'portal' }
|
||||
|
||||
effektives_limit(feature) = merge(
|
||||
club_plan_limit(club_id, feature), # Shinkan-Hauptquelle
|
||||
profile_grant_limit(profile_id, feature) # optional Jinkendo-Bonus
|
||||
)
|
||||
```
|
||||
|
||||
Merge-Regel (Vorschlag): **Maximum** der erlaubten Kontingente, boolean = OR. Details vor Stripe festlegen.
|
||||
|
||||
---
|
||||
|
||||
## 4. Ist-Zustand Shinkan (Drift — zuerst bereinigen)
|
||||
|
||||
| Artefakt | Problem |
|
||||
|----------|---------|
|
||||
| `backend/migrations/001_auth_membership.sql` | `features.id SERIAL`, `tier_limits.tier VARCHAR` |
|
||||
| `backend/auth.py` `check_feature_access()` | Erwartet Mitai-v9c-Schema (`features.id TEXT`, `tier_id`, `limit_type`, …) |
|
||||
| Kein Router | Ruft `check_feature_access` auf |
|
||||
| `profiles.tier` | Existiert, ohne Shinkan-Enforcement |
|
||||
|
||||
**Pflicht vor Phase 3 (Enforcement):** Migration `0XX_club_features_v1.sql` — v9c-kompatibles Feature-Schema + Vereins-Tabellen; alte `001`-Feature-Zeilen migrieren oder deprecaten.
|
||||
|
||||
---
|
||||
|
||||
## 5. Ziel-Schema (v1)
|
||||
|
||||
### 5.1 Feature-Registry (app-weit, Mitai-kompatibel)
|
||||
|
||||
```sql
|
||||
-- Konzept — Implementierung als nummerierte Migration
|
||||
CREATE TABLE features (
|
||||
id TEXT PRIMARY KEY, -- z.B. 'ai_calls'
|
||||
app TEXT NOT NULL DEFAULT 'shinkan',
|
||||
name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
category TEXT NOT NULL, -- 'content'|'planning'|'ai'|'org'|'integration'|'platform'
|
||||
limit_type TEXT NOT NULL DEFAULT 'count', -- 'count' | 'boolean'
|
||||
reset_period TEXT NOT NULL DEFAULT 'never', -- 'never' | 'daily' | 'monthly'
|
||||
default_limit INTEGER, -- NULL=∞, 0=aus
|
||||
enforcement_subject TEXT NOT NULL DEFAULT 'club', -- 'club'|'profile'|'portal'
|
||||
active BOOLEAN NOT NULL DEFAULT true,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
```
|
||||
|
||||
### 5.2 Vereins-Produkte & Abo
|
||||
|
||||
```sql
|
||||
CREATE TABLE club_plans (
|
||||
id TEXT PRIMARY KEY, -- 'free', 'verein_starter', 'verein_pro'
|
||||
name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
price_monthly_cents INTEGER,
|
||||
price_yearly_cents INTEGER,
|
||||
stripe_price_id_monthly TEXT,
|
||||
stripe_price_id_yearly TEXT,
|
||||
active BOOLEAN NOT NULL DEFAULT true,
|
||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE club_subscriptions (
|
||||
id SERIAL PRIMARY KEY,
|
||||
club_id INT NOT NULL REFERENCES clubs(id) ON DELETE CASCADE,
|
||||
plan_id TEXT NOT NULL REFERENCES club_plans(id),
|
||||
status TEXT NOT NULL DEFAULT 'active', -- active|trial|past_due|cancelled
|
||||
started_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
ends_at TIMESTAMPTZ,
|
||||
trial_ends_at TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
UNIQUE (club_id) -- ein aktiver Plan pro Verein (v1)
|
||||
);
|
||||
|
||||
CREATE TABLE club_plan_limits (
|
||||
id SERIAL PRIMARY KEY,
|
||||
plan_id TEXT NOT NULL REFERENCES club_plans(id) ON DELETE CASCADE,
|
||||
feature_id TEXT NOT NULL REFERENCES features(id) ON DELETE CASCADE,
|
||||
limit_value INTEGER, -- NULL=∞, 0=deaktiviert
|
||||
UNIQUE (plan_id, feature_id)
|
||||
);
|
||||
```
|
||||
|
||||
### 5.3 Overrides, Grants, Verbrauch
|
||||
|
||||
```sql
|
||||
CREATE TABLE club_feature_overrides (
|
||||
id SERIAL PRIMARY KEY,
|
||||
club_id INT NOT NULL REFERENCES clubs(id) ON DELETE CASCADE,
|
||||
feature_id TEXT NOT NULL REFERENCES features(id) ON DELETE CASCADE,
|
||||
limit_value INTEGER NOT NULL,
|
||||
reason TEXT,
|
||||
set_by_profile_id INT REFERENCES profiles(id) ON DELETE SET NULL,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
UNIQUE (club_id, feature_id)
|
||||
);
|
||||
|
||||
CREATE TABLE club_access_grants (
|
||||
id SERIAL PRIMARY KEY,
|
||||
club_id INT NOT NULL REFERENCES clubs(id) ON DELETE CASCADE,
|
||||
plan_id TEXT REFERENCES club_plans(id),
|
||||
feature_id TEXT REFERENCES features(id), -- optional Einzel-Feature
|
||||
grant_limit INTEGER,
|
||||
starts_at TIMESTAMPTZ NOT NULL,
|
||||
ends_at TIMESTAMPTZ NOT NULL,
|
||||
reason TEXT,
|
||||
created_by_profile_id INT REFERENCES profiles(id) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
CREATE TABLE club_feature_usage (
|
||||
id SERIAL PRIMARY KEY,
|
||||
club_id INT NOT NULL REFERENCES clubs(id) ON DELETE CASCADE,
|
||||
feature_id TEXT NOT NULL REFERENCES features(id) ON DELETE CASCADE,
|
||||
usage_count INTEGER NOT NULL DEFAULT 0,
|
||||
reset_at TIMESTAMPTZ,
|
||||
last_used_at TIMESTAMPTZ,
|
||||
UNIQUE (club_id, feature_id)
|
||||
);
|
||||
|
||||
-- Optional: Attribution / Fairness / Audit
|
||||
CREATE TABLE club_feature_usage_events (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
club_id INT NOT NULL,
|
||||
feature_id TEXT NOT NULL,
|
||||
profile_id INT REFERENCES profiles(id) ON DELETE SET NULL,
|
||||
action TEXT NOT NULL, -- 'ai_suggest', 'exercise_create', ...
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
```
|
||||
|
||||
### 5.4 Capabilities (Rollen — Kurzreferenz)
|
||||
|
||||
Siehe `CAPABILITY_CATALOG.v1.md` für IDs. Tabellen:
|
||||
|
||||
```sql
|
||||
CREATE TABLE capabilities (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
domain TEXT NOT NULL,
|
||||
min_account_state TEXT NOT NULL DEFAULT 'active_member',
|
||||
linked_feature_id TEXT REFERENCES features(id), -- optional Kontingent
|
||||
active BOOLEAN NOT NULL DEFAULT true
|
||||
);
|
||||
|
||||
CREATE TABLE club_role_capability_grants (
|
||||
role_code TEXT NOT NULL, -- club_admin, trainer, ...
|
||||
capability_id TEXT NOT NULL REFERENCES capabilities(id) ON DELETE CASCADE,
|
||||
PRIMARY KEY (role_code, capability_id)
|
||||
);
|
||||
|
||||
CREATE TABLE portal_role_capability_grants (
|
||||
portal_role TEXT NOT NULL, -- admin, superadmin
|
||||
capability_id TEXT NOT NULL REFERENCES capabilities(id) ON DELETE CASCADE,
|
||||
PRIMARY KEY (portal_role, capability_id)
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Shinkan Feature-Katalog (Seed v1)
|
||||
|
||||
Übernahme aus `001_auth_membership.sql` + Ist-Endpoints, angereichert:
|
||||
|
||||
| feature_id | category | limit_type | reset_period | enforcement_subject | Default Free | Beschreibung |
|
||||
|------------|----------|------------|--------------|---------------------|--------------|--------------|
|
||||
| `exercises` | content | count | never | club | 100 | Anzahl Übungen im Verein (Bestand) |
|
||||
| `exercise_media` | content | count | monthly | club | 20 | Medien-Uploads / Monat |
|
||||
| `training_units` | planning | count | monthly | club | 40 | Geplante/durchgeführte Einheiten |
|
||||
| `training_programs` | planning | count | never | club | 5 | Module + Rahmenprogramme (kombiniert v1) |
|
||||
| `training_groups` | org | count | never | club | 10 | Trainingsgruppen |
|
||||
| `active_members` | org | count | never | club | 25 | Aktive Mitglieder |
|
||||
| `ai_calls` | ai | count | monthly | club | 0 | KI-Aufrufe (Suggest, Regenerate, Planung) |
|
||||
| `ai_pipeline` | ai | boolean | never | club | 0 | Erweiterte KI-Pipelines (Batch, später) |
|
||||
| `wiki_import` | integration | boolean | never | portal | 0 | MediaWiki-Import (Superadmin) |
|
||||
| `data_export` | integration | boolean | never | club | 0 | Export-Funktionen (wenn eingeführt) |
|
||||
|
||||
**Hinweis:** Free-Defaults sind Produktentscheidung — Tabelle dient Implementierung.
|
||||
|
||||
### 6.1 Beispiel-Pläne (Seed)
|
||||
|
||||
| plan_id | ai_calls/Monat | exercises | active_members |
|
||||
|---------|----------------|-----------|----------------|
|
||||
| `free` | 0 | 100 | 25 |
|
||||
| `verein_starter` | 30 | 500 | 80 |
|
||||
| `verein_pro` | 200 | NULL (∞) | NULL |
|
||||
| `pilot` | 100 | NULL | NULL |
|
||||
|
||||
Jeder Verein erhält bei Anlage durch Superadmin initial `club_subscriptions.plan_id = 'free'` (oder `pilot`).
|
||||
|
||||
---
|
||||
|
||||
## 7. Auflösungslogik
|
||||
|
||||
### 7.1 Effektiver Vereinsplan
|
||||
|
||||
```python
|
||||
def get_effective_club_plan(cur, club_id: int) -> str:
|
||||
"""
|
||||
1. Aktiver club_access_grants mit plan_id (höchste Priorität, Zeitfenster)
|
||||
2. club_subscriptions.status == 'active' → plan_id
|
||||
3. Fallback 'free'
|
||||
"""
|
||||
```
|
||||
|
||||
### 7.2 Feature-Limit (analog Mitai `check_feature_access`)
|
||||
|
||||
```python
|
||||
def check_club_feature_access(
|
||||
cur,
|
||||
club_id: int,
|
||||
feature_id: str,
|
||||
*,
|
||||
profile_id: int | None = None, # nur für Logging / optionale Profil-Boni später
|
||||
) -> dict:
|
||||
"""
|
||||
Priorität:
|
||||
1. club_feature_overrides (club_id, feature_id)
|
||||
2. club_plan_limits für get_effective_club_plan(club_id)
|
||||
3. features.default_limit
|
||||
|
||||
Auswertung:
|
||||
- limit_type boolean: limit_value == 1
|
||||
- limit_type count: used < limit (club_feature_usage, reset beachten)
|
||||
|
||||
Returns: { allowed, limit, used, remaining, reason, reset_at }
|
||||
"""
|
||||
```
|
||||
|
||||
### 7.3 Vollständige Request-Kette
|
||||
|
||||
```
|
||||
1. require_auth
|
||||
2. assert_account_state(min_state) # unverified / verified_pending_club / active_member
|
||||
3. get_tenant_context
|
||||
4. assert_capability(tenant, cap_id) # Rollen-Achse
|
||||
5. assert_content_governance(...) # nur bei Objekt-Endpoints
|
||||
6. check_club_feature_access(club_id, feature_id)
|
||||
7. … Business-Logik …
|
||||
8. consume_club_feature_with_usage(…) + merge_feature_usage_into_response(payload, usage)
|
||||
# Standard: zählen, JSON-Log phase=consume, feature_usage in Response
|
||||
9. optional: club_feature_usage_events (profile_id, action)
|
||||
```
|
||||
|
||||
**Response-Standard (alle Consume-Endpoints):** JSON-Feld `feature_usage` — Map `feature_id → { allowed, used, limit, remaining, reason, … }` wie `GET /me/entitlements`. Frontend: `request()` synchronisiert Entitlements automatisch (`featureUsageSync.js`); UI-Komponenten brauchen keinen Einzelcode.
|
||||
|
||||
### 7.4 Wer zählt als Verbrauch?
|
||||
|
||||
| Aktion | increment | Subjekt |
|
||||
|--------|-----------|---------|
|
||||
| `POST /exercises` (neu) | `exercises` | `club_id` des Objekts oder `effective_club_id` |
|
||||
| Medien-Upload | `exercise_media` | Verein des Mediums |
|
||||
| KI Suggest/Regenerate | `ai_calls` | `effective_club_id` |
|
||||
| Mitglied hinzufügen | `active_members` | Ziel-`club_id` |
|
||||
| Trainingsgruppe anlegen | `training_groups` | `club_id` |
|
||||
|
||||
**Mitai-Regel:** Counter **nicht** bei UPDATE/DELETE erhöhen.
|
||||
|
||||
---
|
||||
|
||||
## 8. API-Oberfläche
|
||||
|
||||
### 8.1 Nutzer / Vereinsadmin
|
||||
|
||||
```
|
||||
GET /api/clubs/{club_id}/entitlements
|
||||
```
|
||||
|
||||
Kombiniert Capabilities + Feature-Kontingente (siehe `CAPABILITY_CATALOG.v1.md` §7.1).
|
||||
|
||||
```
|
||||
GET /api/me/entitlements?club_id=12
|
||||
```
|
||||
|
||||
Bequemer Alias für aktiven Verein.
|
||||
|
||||
### 8.2 Superadmin / Plattform
|
||||
|
||||
| Endpoint | Zweck |
|
||||
|----------|-------|
|
||||
| `GET/PUT /api/admin/club-plans` | Plan-CRUD |
|
||||
| `GET/PUT /api/admin/club-plan-limits` | Matrix |
|
||||
| `GET/PUT /api/admin/clubs/{id}/subscription` | Verein-Abo |
|
||||
| `GET/PUT /api/admin/clubs/{id}/feature-overrides` | Sonderkontingente |
|
||||
| `POST /api/admin/clubs/{id}/access-grants` | Trial/Promo |
|
||||
|
||||
Vorbild UI: Mitai `AdminTierLimitsPage.jsx`, `AdminUserRestrictionsPage.jsx` → Vereins-Kontext.
|
||||
|
||||
### 8.3 Geplant: Vereinsgründung
|
||||
|
||||
```
|
||||
POST /api/club-creation-requests # Nutzer (verified_pending_club)
|
||||
GET /api/admin/club-creation-requests
|
||||
POST /api/admin/club-creation-requests/{id}/approve # legt club + subscription an
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. Vier-Phasen-Rollout (aus Mitai)
|
||||
|
||||
| Phase | Shinkan-Aktivität | Nutzer sichtbar? |
|
||||
|-------|-------------------|------------------|
|
||||
| **0** | Schema-Migration, Seed `features` + `club_plans`, Drift `001` bereinigen | Nein |
|
||||
| **1** | Account-Gates + Capability-Grants (ohne Limits) | Onboarding-Hinweise |
|
||||
| **2** | `check_club_feature_access` — **nur JSON-Log** (`feature_logger` analog Mitai) | Nein |
|
||||
| **3** | `GET …/entitlements` + UsageBadge im UI | Ja (Kontingent-Anzeige) |
|
||||
| **4** | HTTP 403 bei Limit + `increment` | Ja (Hard-Block) |
|
||||
|
||||
**Reihenfolge innerhalb Phase 4:** zuerst `ai_calls`, dann `exercise_media`, dann Bestands-Limits (`exercises`, `active_members`).
|
||||
|
||||
---
|
||||
|
||||
## 10. CI / Test-Isolation (Betrieb)
|
||||
|
||||
Unabhängig vom Membership-System — **Pflicht** wegen Prod-Vorfälle (`access_layer_it_*@test.local`):
|
||||
|
||||
| Regel | Umsetzung |
|
||||
|-------|-----------|
|
||||
| Integrationstests nie gegen Prod-DB | Eigene Test-DB oder Job-Postgres in Gitea |
|
||||
| `ENVIRONMENT=production` + `ALLOW_INTEGRATION_TESTS` | Default `0`, Tests abbrechen |
|
||||
| Test-Accounts | E-Mail `@test.local` oder `profiles.is_test_account` |
|
||||
| Cleanup | Fixture-`finally` + Nightly-Job löscht Leichen |
|
||||
|
||||
`.gitea/workflows/test.yml`: pytest-backend gegen Deploy-DB **ersetzen** durch isolierte DB (eigenes Epic, parallel zu Membership).
|
||||
|
||||
---
|
||||
|
||||
## 11. Implementierungs-Roadmap (gesamt)
|
||||
|
||||
| Schritt | Deliverable | Membership-relevant |
|
||||
|---------|-------------|-------------------|
|
||||
| M0 | CI-Isolation + Prod-Cleanup-Runbook | Nein |
|
||||
| M1 | Migration Feature-Schema v9c + `club_plans`/`club_subscriptions` (leer nutzbar) | **Ja** |
|
||||
| M2 | `check_club_feature_access` + Seed Pläne | **Ja** |
|
||||
| M3 | Account-Lifecycle + Capability-Grants | Capabilities |
|
||||
| M4 | `GET /me/entitlements` | **Ja** |
|
||||
| M5 | Enforcement `ai_calls` (Phase 4) | **Ja** |
|
||||
| M6 | Admin Plan-Matrix UI | **Ja** |
|
||||
| M7 | `club_creation_requests` | Prozess |
|
||||
| M8 | Stripe / Rechnung | Später |
|
||||
|
||||
**Nach Produktentscheidungen 2026-06-06** (Details `MEMBERSHIP_RBAC_DECISIONS_2026-06.md` §4):
|
||||
|
||||
| Phase | Paket | Priorität |
|
||||
|-------|--------|-----------|
|
||||
| A | Onboarding-Gates vollständig (`verified_pending_club`) | **Als Nächstes** |
|
||||
| B | M7 Vereinsgründung beantragen | hoch |
|
||||
| C | M5 Hard-Block `ai_calls` | danach |
|
||||
| D | M6 Superadmin-UI | danach |
|
||||
| E | Systemrolle `co_trainer` + Frontend-Entitlements | v1 Rollen |
|
||||
| F | Trainer-Member-Budgets (v2) | später |
|
||||
|
||||
---
|
||||
|
||||
## 12. Offene Produktentscheidungen
|
||||
|
||||
Vor M6 festlegen:
|
||||
|
||||
1. **Zählen `active_members`:** alle Mitglieder oder nur Rollen mit Planungsrecht?
|
||||
2. **Soft-Limit vs. Hard-Stop:** Warnung bei 80 % oder sofort 403?
|
||||
3. **Pilotverein:** eigener Plan `pilot` mit hohen Limits?
|
||||
4. **KI-Fairness:** nur Vereinslimit oder zusätzlich Max pro Trainer/Monat?
|
||||
5. **Offizielle Inhalte:** für `verified_pending_club` sichtbar oder gesperrt? → **entschieden: gesperrt** (`MEMBERSHIP_RBAC_DECISIONS_2026-06.md` §1.1)
|
||||
6. **Portal `admin` vs. `superadmin`:** Wer darf Vereine anlegen? (Ziel: nur `superadmin` für Freigabe)
|
||||
|
||||
---
|
||||
|
||||
## 13. Referenzen
|
||||
|
||||
| Pfad | Inhalt |
|
||||
|------|--------|
|
||||
| `c:/dev/mitai-jinkendo/backend/migrations/v9c_subscription_system.sql` | Mitai-Schema-Vorlage |
|
||||
| `c:/dev/mitai-jinkendo/.claude/docs/architecture/FEATURE_ENFORCEMENT.md` | 4-Phasen-Modell |
|
||||
| `c:/dev/mitai-jinkendo/.claude/docs/technical/MEMBERSHIP_SYSTEM.md` | Mitai-Hauptdoku |
|
||||
| `c:/dev/mitai-jinkendo/.claude/docs/technical/CENTRAL_SUBSCRIPTION_SYSTEM.md` | Jinkendo-Familie später |
|
||||
| `CAPABILITY_CATALOG.v1.md` | Rollen & Capabilities |
|
||||
| `MULTI_TENANCY_RBAC_ARCHITECTURE.md` §4.6 | Ursprüngliches Vereinsabo-Zielbild |
|
||||
| `ACCESS_LAYER_AND_GOVERNANCE_PLAN.md` | Stufe E/F |
|
||||
|
||||
---
|
||||
|
||||
**Changelog**
|
||||
|
||||
- 2026-06-06: v1 — Mitai-Mapping, Ziel-Schema, Feature-Seed, Auflösungslogik, Rollout.
|
||||
|
|
@ -1,11 +1,12 @@
|
|||
# Exercises API Specification
|
||||
|
||||
**Version:** 1.5
|
||||
**Datum:** 2026-05-08
|
||||
**Version:** 1.6
|
||||
**Datum:** 2026-05-20
|
||||
**Status:** Teilweise implementiert (Liste mit Filtern + Varianten + Medienlimits + Progressionsgraphen siehe Code)
|
||||
**Autor:** Claude Code
|
||||
**Änderungen v1.4:** Endpoints **`/exercise-progression-graphs`** inkl. Kanten, **`POST …/edges/sequence`**, **`POST …/edges/delete-batch`** — Detailtabellen siehe **`TRAINING_FRAMEWORK_SPEC.md`** §3.3
|
||||
**Änderungen v1.6:** Freigabelevel-UI-Hinweis; `exercise_skills` ohne `is_primary` in Requests (Legacy-Feld wird ignoriert/forciert false); Permissions-Bereich an Ist-Code angeglichen; Intensität kanonisch `niedrig|mittel|hoch`
|
||||
**Änderungen v1.5:** Medien-/Inline-Workflow aktualisiert (Modal-Picker, Drag&Drop UX im Frontend), Klarstellung zu `context` (legacy/optional), Hinweise zu Platzhaltern in Rich-Text-Feldern.
|
||||
**Änderungen v1.4:** Endpoints **`/exercise-progression-graphs`** inkl. Kanten, **`POST …/edges/sequence`**, **`POST …/edges/delete-batch`** — Detailtabellen siehe **`TRAINING_FRAMEWORK_SPEC.md`** §3.3
|
||||
**Änderungen v1.3:** `GET /exercises` erweiterte Query-Parameter (`include_variants`, Multi-Filter, `ai_search`-Platzhalter); Dokumentation angepasst
|
||||
**Änderungen v1.2:** KI-Assistenz Endpoints, Skill-Level-System (benannte Stufen), intensity als low/medium/high
|
||||
**Änderungen v1.1:** Exercise Blocks Endpoints, Permissions dokumentiert, age_groups korrigiert
|
||||
|
|
@ -185,11 +186,11 @@ Lightweight-Liste; bei `include_variants=true` zusätzlich z. B.:
|
|||
"skill_id": 10,
|
||||
"skill_name": "Distanzgefühl",
|
||||
"skill_category": "Kumite",
|
||||
"is_primary": true,
|
||||
"intensity": "hoch",
|
||||
"required_level": "grundlagen",
|
||||
"target_level": "aufbau",
|
||||
"ai_suggested": false
|
||||
"ai_suggested": false,
|
||||
"is_primary": false
|
||||
}
|
||||
],
|
||||
|
||||
|
|
@ -307,7 +308,6 @@ Lightweight-Liste; bei `include_variants=true` zusätzlich z. B.:
|
|||
"skills": [
|
||||
{
|
||||
"skill_id": 10,
|
||||
"is_primary": true,
|
||||
"intensity": "hoch",
|
||||
"required_level": "grundlagen",
|
||||
"target_level": "aufbau"
|
||||
|
|
@ -578,7 +578,6 @@ Wird beim Klick auf „✨ KI-Vorschlag" im Formular aufgerufen.
|
|||
"required_level": "grundlagen",
|
||||
"target_level": "aufbau",
|
||||
"intensity": "hoch",
|
||||
"is_primary": true,
|
||||
"confidence": 0.92
|
||||
},
|
||||
{
|
||||
|
|
@ -588,7 +587,6 @@ Wird beim Klick auf „✨ KI-Vorschlag" im Formular aufgerufen.
|
|||
"required_level": "einsteiger",
|
||||
"target_level": "grundlagen",
|
||||
"intensity": "mittel",
|
||||
"is_primary": false,
|
||||
"confidence": 0.74
|
||||
}
|
||||
]
|
||||
|
|
@ -621,6 +619,38 @@ Trainer muss im Frontend aktiv übernehmen.
|
|||
|
||||
## Permissions
|
||||
|
||||
**UI-Hinweis:** Das Feld `visibility` heißt in der Oberfläche **Freigabelevel** (`exerciseGovernanceLabels.js`).
|
||||
|
||||
### Lesen (`GET /exercises`, `GET /exercises/{id}`)
|
||||
|
||||
| `visibility` | Wer darf lesen? |
|
||||
|--------------|-----------------|
|
||||
| `official` | Plattform-weit |
|
||||
| `private` | Ersteller (`created_by`); Plattform-Admin |
|
||||
| `club` | Aktive Mitglieder des Objekt-`club_id`; Plattform-Admin ohne Mitgliedschaft (Audit-Zugang) |
|
||||
|
||||
Implementierung: `library_content_visible_to_profile` / `exercise_visible_to_profile` in `club_tenancy.py`.
|
||||
|
||||
### Bearbeiten (`PUT`, Varianten-CRUD, Medien an Übung)
|
||||
|
||||
| Bedingung | Wer darf bearbeiten? |
|
||||
|-----------|----------------------|
|
||||
| Ersteller | Immer (eigene Übung) |
|
||||
| Plattform-Admin | Immer |
|
||||
| `visibility=club` | Zusätzlich **`can_plan_in_club`** im Objekt-Verein: `club_admin`, `trainer`, `content_editor`, `division_lead` |
|
||||
|
||||
Implementierung: `_assert_can_edit_exercise` in `exercises.py`. **Varianten** haben kein eigenes Owner-Feld — gleiche Prüfung wie Eltern-Übung.
|
||||
|
||||
### Löschen (`DELETE /exercises/{id}`)
|
||||
|
||||
| `visibility` | Wer darf löschen? |
|
||||
|--------------|-------------------|
|
||||
| `official` | Nur Plattform-Admin |
|
||||
| `club` | Nur **`club_admin`** im Objekt-Verein |
|
||||
| `private` | Ersteller; oder Vereins-Admin, der mit dem Ersteller einen gemeinsamen Verein teilt |
|
||||
|
||||
Implementierung: `_assert_can_delete_exercise` in `exercises.py`.
|
||||
|
||||
### Sichtbarkeits-Workflow
|
||||
|
||||
| Von → Nach | Wer darf das? |
|
||||
|
|
@ -638,11 +668,12 @@ Trainer muss im Frontend aktiv übernehmen.
|
|||
| `club → official` | Club-Admin, Super-Admin |
|
||||
| `official → club` | Super-Admin |
|
||||
|
||||
### Owner-Checks
|
||||
### Owner-Checks (veraltet — siehe Tabellen oben)
|
||||
|
||||
- **Bearbeiten** (PUT): Nur Ersteller oder Club-Admin
|
||||
- **Löschen** (DELETE): Nur Ersteller oder Super-Admin
|
||||
- **Lesen** (`private`): Nur Ersteller
|
||||
Die folgenden Kurzregeln sind durch die Ist-Implementierung ersetzt; nur zur historischen Einordnung:
|
||||
|
||||
- ~~Bearbeiten (PUT): Nur Ersteller oder Club-Admin~~ → siehe **Bearbeiten**-Tabelle (`can_plan_in_club`)
|
||||
- ~~Löschen (DELETE): Nur Ersteller oder Super-Admin~~ → siehe **Löschen**-Tabelle
|
||||
|
||||
**403 Fehler-Beispiel:**
|
||||
```json
|
||||
|
|
@ -904,7 +935,8 @@ Trainer muss im Frontend aktiv übernehmen.
|
|||
### Exercise Skills
|
||||
- `required_level`: enum – `einsteiger | grundlagen | aufbau | fortgeschritten | experte` (optional/nullable)
|
||||
- `target_level`: enum – gleiche Werte (optional/nullable)
|
||||
- `intensity`: enum – `niedrig | mittel | hoch` (optional/nullable)
|
||||
- `intensity`: enum – **`niedrig | mittel | hoch`** (optional/nullable; Default beim Speichern **`mittel`**)
|
||||
- `is_primary`: **Legacy** — Spalte existiert in DB, wird bei POST/PUT **nicht ausgewertet** (immer `false` gespeichert); UI liefert/speichert kein Primär-Flag mehr; Scoring ignoriert das Feld
|
||||
- `target_level` sollte >= `required_level` sein (Warnung, kein Fehler)
|
||||
|
||||
### Exercise Block Item
|
||||
|
|
|
|||
|
|
@ -99,20 +99,21 @@ Exercise Block ──── (N) Block Items ──── (1) Exercise
|
|||
|
||||
### 1.3 M:N Beziehungen (Primary/Secondary Pattern)
|
||||
|
||||
**Regel:** Alle Katalog-Zuordnungen nutzen M:N mit `is_primary` Flag.
|
||||
**Regel:** Katalog-Zuordnungen (Fokus, Stil, Zielgruppe, …) nutzen M:N mit optionalem `is_primary`-Flag.
|
||||
|
||||
**Betroffene Relationen:**
|
||||
**Betroffene Relationen (mit `is_primary`):**
|
||||
- `exercise_focus_areas` (Übung ↔ Fokusbereiche)
|
||||
- `exercise_styles` (Übung ↔ Trainingsstile)
|
||||
- `exercise_styles` / `exercise_style_directions` (Übung ↔ Stilrichtungen)
|
||||
- `exercise_training_types` (Übung ↔ Trainingsstile)
|
||||
- `exercise_target_groups` (Übung ↔ Zielgruppen)
|
||||
- `exercise_training_characters` (Übung ↔ Trainingscharaktere)
|
||||
- `exercise_skills` (Übung ↔ Fähigkeiten)
|
||||
|
||||
**Primary/Secondary Semantik:**
|
||||
**Ausnahme — `exercise_skills`:** Kein Primär-Flag in UI/API mehr; stattdessen **`intensity`** (`niedrig` \| `mittel` \| `hoch`, Default `mittel`). Spalte `is_primary` bleibt Legacy (Backend speichert immer `false`).
|
||||
|
||||
**Primary/Secondary Semantik (Katalog-Dimensionen):**
|
||||
- **Primary:** Hauptzuordnung, entscheidend für Filter/Suche
|
||||
- **Secondary:** Nebenzuordnung, zusätzlicher Kontext
|
||||
- **Regel:** Genau EINE Primary-Zuordnung pro Dimension
|
||||
- **UI:** Primary wird visuell hervorgehoben (z.B. fett, farbig)
|
||||
- **Regel:** Genau EINE Primary-Zuordnung pro Dimension (wo UI das noch anbietet)
|
||||
- **UI:** Primary wird visuell hervorgehoben (z. B. fett, farbig) — Fähigkeiten: Intensitäts-Segmente statt Primary
|
||||
|
||||
**Legacy-Felder (DEPRECATED):**
|
||||
- `exercises.focus_area` → Ignorieren, nutze `exercise_focus_areas`
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
# Frontend Routing & Navigation Specification
|
||||
|
||||
**Version:** 1.2
|
||||
**Datum:** 2026-04-30
|
||||
**Version:** 1.3
|
||||
**Datum:** 2026-05-20
|
||||
**Status:** DRAFT - Awaiting Review
|
||||
**Autor:** Claude Code
|
||||
**Änderungen v1.3:** Übungsformular Tab-Navigation unter `/exercises/:id/edit` (Stammdaten … Medien & Mehr); Freigabelevel als UI-Begriff
|
||||
**Änderungen v1.2:** Übersicht **Übungen**: Tabs Liste \| Progressionsgraphen auf `/exercises`; Progressions-Editor ohne neue Routen (Panel + Formularblock unter `/exercises/:id/edit`)
|
||||
**Änderungen v1.1:** Übungsvarianten-Bearbeitung nur unter `/exercises/:id/edit` (keine VariantFormPage-Routen)
|
||||
|
||||
|
|
@ -17,7 +18,7 @@
|
|||
/exercises → ExercisesListPage — Tabs: **Liste** \| **Progressionsgraphen** (`ExerciseProgressionGraphPanel`)
|
||||
/exercises/new → ExerciseFormPage (Create)
|
||||
/exercises/{id} → ExerciseDetailPage (Accordion-Layout)
|
||||
/exercises/{id}/edit → ExerciseFormPage (Edit inkl. Varianten-Editor inline + Block Progressionsgraph)
|
||||
/exercises/{id}/edit → ExerciseFormPage (Edit: Registerkarten + Varianten inline + Progressionsgraph)
|
||||
|
||||
/exercise-blocks → ExerciseBlocksListPage (Meine Blocks)
|
||||
/exercise-blocks/new → ExerciseBlockFormPage (Create)
|
||||
|
|
@ -35,6 +36,25 @@
|
|||
- Pagination: `/exercises?limit=50&offset=100`
|
||||
- Sortierung: `/exercises?sort=created_at&order=desc`
|
||||
|
||||
### 1.2 Übungsformular – Registerkarten (`/exercises/new`, `/exercises/:id/edit`)
|
||||
|
||||
**Implementierung:** `ExerciseFormPageRoot.jsx` + `ExerciseFormLayout.jsx` (`ExerciseFormTabBar`, `ExerciseFormPanel`).
|
||||
|
||||
| Tab-ID | Label | Verfügbarkeit |
|
||||
|--------|-------|---------------|
|
||||
| `stammdaten` | Stammdaten | immer |
|
||||
| `anleitung` | Anleitung | immer |
|
||||
| `einordnung` | Einordnung | immer |
|
||||
| `kombination` | Kombination | nur `exercise_kind=combination` |
|
||||
| `varianten` | Varianten | Edit-Modus; nicht bei Kombination; disabled bei Neuanlage |
|
||||
| `medien` | Medien & Mehr | Edit-Modus; disabled bei Neuanlage |
|
||||
|
||||
**UX-Regeln:**
|
||||
- Nur ein Panel sichtbar (`activeFormTab`); Navigation über `PageSectionNav`.
|
||||
- **Freigabelevel** (Feld `visibility`) in Stammdaten — Konstante `EXERCISE_VISIBILITY_FIELD_LABEL`.
|
||||
- Varianten-Änderungen werden mit **Speichern** in der Aktionsleiste persistiert (`persistPendingVariantChanges`); Button „Variante anlegen“ optional sofort.
|
||||
- Kein URL-Hash pro Tab (Tab-State nur lokal).
|
||||
|
||||
---
|
||||
|
||||
## 2. Navigation-Patterns
|
||||
|
|
@ -673,7 +693,7 @@ function App() {
|
|||
|
||||
---
|
||||
|
||||
**Version:** 1.2
|
||||
**Letzte Änderung:** 2026-04-30
|
||||
**Version:** 1.3
|
||||
**Letzte Änderung:** 2026-05-20
|
||||
**Status:** REVIEWED - Pending Implementation
|
||||
**Review-Änderungen:** Progressionsgraphen-UI (Tabs, Formularblock); Exercise Blocks Routes + Navigation (früher)
|
||||
**Review-Änderungen:** Formular-Registerkarten; Progressionsgraphen-UI (Tabs, Formularblock); Exercise Blocks Routes + Navigation (früher)
|
||||
|
|
|
|||
|
|
@ -7,11 +7,16 @@
|
|||
**Änderungen v1.1:** Prompts sind nicht hardcoded – sie werden aus der DB geladen (AI_PROMPT_SYSTEM_SPEC.md)
|
||||
**Verwandte Specs:** AI_PROMPT_SYSTEM_SPEC.md (Prompt-DB + Platzhalter), SKILLS_MATRIX_SPEC.md (Fähigkeitsmatrix)
|
||||
|
||||
**Übergeordnete Produkt-Vision** (breiter Scope: Zielausbau, bereichsweise vs. Gesamtüberarbeitung, Varianten, Planungs-/Nachbereitungskontext, Admin-Masse):
|
||||
`functional/AI_EXERCISE_ASSISTANT_VISION.md`
|
||||
|
||||
---
|
||||
|
||||
## 1. Übersicht
|
||||
|
||||
Zwei KI-gestützte Assistenzfunktionen beim Anlegen und Bearbeiten von Übungen:
|
||||
KI-gestützte Assistenzfunktionen beim Anlegen und Bearbeiten von Übungen (Mindestpaket dieser Spec):
|
||||
|
||||
**Hinweis:** Die beiden folgenden Zeilen entsprechen **P0** der Phasierung in **`AI_EXERCISE_ASSISTANT_VISION.md`**; spätere Funkteile sind dort beschrieben.
|
||||
|
||||
| Funktion | Ziel |
|
||||
|---------|------|
|
||||
|
|
@ -155,7 +160,38 @@ KI gibt Vorschläge
|
|||
Liefert KI-Vorschläge auf Basis von Eingabe-Text, **bevor** die Übung gespeichert wurde.
|
||||
Wird beim Klick auf „KI-Vorschlag" im Formular aufgerufen.
|
||||
|
||||
**Request Body:**
|
||||
**Required Fields:** mindestens `goal` ODER `execution`
|
||||
|
||||
**Optional – Skill-Katalogpriorisierung (Stand 068):**
|
||||
|
||||
```json
|
||||
{
|
||||
"focus_areas_context": [
|
||||
{ "focus_area_id": 3, "is_primary": true },
|
||||
{ "focus_area_id": 1, "is_primary": false }
|
||||
],
|
||||
"focus_area_hint": "Karate, Kumite…"
|
||||
}
|
||||
```
|
||||
|
||||
- `focus_areas_context`: IDs aus Stammdatum **Fokusbereiche**; Primär soll zuerst stehen (`is_primary`). Ohne Feld oder leere Liste gilt das DB-Profil **`is_default`** (`ai_skill_retrieval_profiles`).
|
||||
- `focus_area_hint`: bleibt lesbarer Text für den Prompt (bestehende Prompts).
|
||||
|
||||
|
||||
**Minimal-Beispiel (Mit Fokus für Retrieval):**
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "Maai - Distanzübung",
|
||||
"goal": "…",
|
||||
"execution": "…",
|
||||
"focus_areas_context": [ { "focus_area_id": 1, "is_primary": true } ]
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
**Minimal-Beispiel ( ohne Fokus — nur Texts):**
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "Maai - Distanzübung",
|
||||
|
|
@ -164,8 +200,6 @@ Wird beim Klick auf „KI-Vorschlag" im Formular aufgerufen.
|
|||
}
|
||||
```
|
||||
|
||||
**Required Fields:** mindestens `goal` ODER `execution` (je länger, desto besser)
|
||||
|
||||
**Response:** `200 OK`
|
||||
```json
|
||||
{
|
||||
|
|
@ -182,7 +216,6 @@ Wird beim Klick auf „KI-Vorschlag" im Formular aufgerufen.
|
|||
"required_level": "grundlagen",
|
||||
"target_level": "aufbau",
|
||||
"intensity": "hoch",
|
||||
"is_primary": true,
|
||||
"confidence": 0.92
|
||||
},
|
||||
{
|
||||
|
|
@ -192,7 +225,6 @@ Wird beim Klick auf „KI-Vorschlag" im Formular aufgerufen.
|
|||
"required_level": "einsteiger",
|
||||
"target_level": "grundlagen",
|
||||
"intensity": "mittel",
|
||||
"is_primary": false,
|
||||
"confidence": 0.74
|
||||
}
|
||||
]
|
||||
|
|
|
|||
243
.claude/docs/technical/MEMBERSHIP_RBAC_DECISIONS_2026-06.md
Normal file
243
.claude/docs/technical/MEMBERSHIP_RBAC_DECISIONS_2026-06.md
Normal file
|
|
@ -0,0 +1,243 @@
|
|||
# Membership, RBAC & Kontingente — Produktentscheidungen
|
||||
|
||||
**Status:** Verbindlich (Zielbild & Roadmap-Priorisierung)
|
||||
**Stand:** 2026-06-06
|
||||
**Bezüge:** `CAPABILITY_CATALOG.v1.md`, `CLUB_MEMBERSHIP_AND_FEATURES.v1.md`, `ACCESS_LAYER_AND_GOVERNANCE_PLAN.md`
|
||||
|
||||
Dieses Dokument hält **getroffene Produktentscheidungen** fest (Session 2026-06-06) und ergänzt die v1-Konzept-Specs um Umsetzungsrichtung. Technischer Implementierungsstand: Abschnitt 2.
|
||||
|
||||
---
|
||||
|
||||
## 1. Getroffene Entscheidungen
|
||||
|
||||
### 1.1 Onboarding: `verified_pending_club`
|
||||
|
||||
Nutzer **ohne aktive Vereinsmitgliedschaft** (E-Mail verifiziert) dürfen **nur**:
|
||||
|
||||
| Erlaubt | Nicht erlaubt (Zielbild) |
|
||||
|---------|---------------------------|
|
||||
| Konto / Einstellungen | Übungen, Planung, KI, Medien |
|
||||
| Vereinsverzeichnis lesen | Vereinsinterne Inhalte (`club`), private Fremdinhalte |
|
||||
| **Beitrittsantrag** an bestehenden Verein | Vollzugriff auf Bibliothek / offizielle Inhalte (Lesen) — **bewusst gesperrt** bis Mitgliedschaft |
|
||||
| **Vereinsgründung beantragen** (Prozess M7, Superadmin-Freigabe) | |
|
||||
|
||||
**Kein** „Bibliothek durchstöbern“ für Bewerber — reduziert Datenexposition und vereinfacht UX („erst Verein, dann Arbeit“).
|
||||
|
||||
Technischer Zustand: `account_state = verified_pending_club` (siehe `CAPABILITY_CATALOG.v1.md` §3).
|
||||
|
||||
---
|
||||
|
||||
### 1.2 Rollenmodell: Risikoarm statt Big-Bang
|
||||
|
||||
**Zielbild (langfristig):**
|
||||
|
||||
- **Fest:** nur `superadmin` (Plattform) als nicht konfigurierbare Systemrolle.
|
||||
- **Dynamisch konfigurierbar:** alle Vereinsrollen und deren Capability-Bundles (später `club_custom_roles`).
|
||||
- Optional: `admin` (Plattform) als abgeschwächter Portal-Admin bleibt vorerst bestehen (Ist-Code).
|
||||
|
||||
**Entscheidung v1 (risikoarm):**
|
||||
|
||||
| Maßnahme | Jetzt | Später |
|
||||
|----------|-------|--------|
|
||||
| Alte Helfer (`can_plan_in_club`, `if (club_admin)` in JSX) | **Behalten** — weiter produktiv | Schrittweise durch `entitlements` ersetzen |
|
||||
| Neue Endpoints / Features | Nur über **Capability-IDs** + Audit | — |
|
||||
| Neue Vereinsrollen | Als **Systemrollen** ergänzen (z. B. `co_trainer`) | Custom Roles UI |
|
||||
| `club_custom_roles` | **Nicht** in v1 | v2 Epic |
|
||||
|
||||
**Begründung:** Backend und Frontend haben hunderte Verdrahtungen auf `trainer` / `club_admin` / Plattform-Rollen. Parallelbetrieb Capability-System + Legacy-Helfer ist sicherer als einmaliges Aufbrechen.
|
||||
|
||||
**Co-Trainer (geplant als Systemrolle):** weniger Capabilities als `trainer` (z. B. kein `planning.*`, kein `exercises.create`) — Umsetzung nach Onboarding-Gates + Entitlements-Rollout, nicht vorher.
|
||||
|
||||
---
|
||||
|
||||
### 1.3 Vereins-Kontingente (Membership-Pakete)
|
||||
|
||||
**Jetzt:** Schema und Anzeige vorbereiten; **keine** detaillierte Paket-Logik (z. B. „3 Trainer + 10 Co-Trainer“) implementieren.
|
||||
|
||||
| Vorbereitet (DB/Module) | Bewusst zurückgestellt |
|
||||
|-------------------------|-------------------------|
|
||||
| `features`, `club_plans`, `club_subscriptions` | Eigene Feature-IDs `trainer_seats` / `co_trainer_seats` |
|
||||
| Bestands-Limits (`exercises`, `training_groups`, `ai_calls`, …) | Zählregel „nur planungsberechtigte Mitglieder“ vs. alle Mitglieder |
|
||||
| `GET /me/entitlements` Feature-Teil | Stripe / Rechnung (M8) |
|
||||
|
||||
**Prinzip:** Neue Kontingent-Typen = neue `features`-Zeile + Plan-Limits + optional Capability-`linked_feature_id` — ohne Schema-Bruch.
|
||||
|
||||
---
|
||||
|
||||
### 1.4 Trainer-Budget innerhalb Vereins-Kontingent (v2)
|
||||
|
||||
**Anforderung:** Vereins-KI-Kontingent liegt beim Verein; **Vereinsadmin** kann pro Trainer ein **Sub-Budget** vergeben (Fairness, „Kontingent-Fresser“).
|
||||
|
||||
**Entscheidung:**
|
||||
|
||||
- v1: nur **Vereins-Ebene** (`club_plan_limits`, `club_feature_usage`).
|
||||
- v2: neue Tabellen (Skizze):
|
||||
|
||||
```sql
|
||||
-- Skizze — noch nicht migriert
|
||||
club_member_feature_budgets (club_id, profile_id, feature_id, limit_value, …)
|
||||
club_member_feature_usage (club_id, profile_id, feature_id, usage_count, reset_at, …)
|
||||
```
|
||||
|
||||
**Prüf-Kette v2:** Capability → Mitglieds-Budget (falls gesetzt, `profile_id` aus Session) → Vereins-Kontingent.
|
||||
|
||||
**Fairness-Modell (offen, Tendenz):** harte Sub-Budgets (Modell A) — Trainer darf sein Budget nicht überschreiten, auch wenn Verein noch Rest hat.
|
||||
|
||||
**Roadmap:** Phase 5b / Meilenstein **M9** in `docs/working/RBAC_ENFORCEMENT_ROADMAP.md` — Vereinsadmin-UI zur Verteilung, Entitlements mit persönlichem + Vereins-Rest, Auswertung je Person.
|
||||
|
||||
---
|
||||
|
||||
### 1.5 Enforcement-Phasen (unverändert, bestätigt)
|
||||
|
||||
| Phase | Verhalten | Nutzer sichtbar |
|
||||
|-------|-----------|-----------------|
|
||||
| 2 (M2/M3) | JSON-Log, kein Block | Nein (außer Logs) |
|
||||
| 3 (M4) | `GET /me/entitlements` + Badge | Kontingent-Anzeige |
|
||||
| 4 (M5+) | HTTP 403 + `increment` | Hard-Block |
|
||||
|
||||
Env-Schalter: `ACCOUNT_GATE_ENFORCE` (Default `1`, Endpoint-Helfer), `ACCOUNT_GATE_API_ENFORCE` (Default `1`, API-Middleware Phase A), `CAPABILITY_ENFORCE` / `CLUB_FEATURE_ENFORCE` (Default `0`).
|
||||
|
||||
---
|
||||
|
||||
## 2. Implementierungsstand (Ist, Codebase)
|
||||
|
||||
**DB-Schema:** `20260606083` · App **0.8.199** (`backend/version.py`)
|
||||
**Roadmap (detailliert):** `docs/working/RBAC_ENFORCEMENT_ROADMAP.md`
|
||||
|
||||
### M1 — Feature-Schema v9c ✅
|
||||
|
||||
| Deliverable | Status |
|
||||
|-------------|--------|
|
||||
| Migration `078_club_features_and_plans.sql` | ✅ |
|
||||
| Legacy `001` archiviert | ✅ |
|
||||
| `club_plans`, `club_subscriptions`, Usage-Tabellen | ✅ |
|
||||
| Seed Features + Pläne (`free`, …) | ✅ |
|
||||
| `club_features.py`: `check_club_feature_access`, `get_effective_club_plan` | ✅ |
|
||||
| Backfill Vereine → Plan `free` | ✅ |
|
||||
|
||||
### M2 — Feature-Probe (Log only) ✅
|
||||
|
||||
| Deliverable | Status |
|
||||
|-------------|--------|
|
||||
| `club_feature_logger.py` → `club-feature-usage.log` | ✅ |
|
||||
| `probe_club_feature_access()` | ✅ |
|
||||
| Hooks: KI-Endpoints, `POST /exercises`, Medien-Upload, Planungs-KI | ✅ |
|
||||
| Consume-Standard + `feature_usage` in Response (`ai_calls`) | ✅ |
|
||||
| `CLUB_FEATURE_ENFORCE=0` (Default) | ✅ |
|
||||
|
||||
### M3 — Account-Lifecycle + Capability-Grants ⚠️ teilweise
|
||||
|
||||
| Deliverable | Status | Lücke |
|
||||
|-------------|--------|-------|
|
||||
| Migration `079_capabilities.sql` + Seed | ✅ | — |
|
||||
| `account_lifecycle.py`, `resolve_account_state` | ✅ | — |
|
||||
| `capabilities.py`, `check_capability`, `probe_capability` | ✅ | — |
|
||||
| `TenantContext.account_state` | ✅ | — |
|
||||
| `GET /profiles/me` → `account_state`, `club_roles` | ✅ | — |
|
||||
| Account-Gates auf **Schreib-/KI-Endpoints** | ✅ | Lesepfade für Bewerber noch offen |
|
||||
| `CAPABILITY_ENFORCE=0` (nur Log) | ✅ | — |
|
||||
| Onboarding UX: nur Bewerbung/Gründung | ✅ | Phase A: API-Middleware + `/onboarding` + reduzierte Nav |
|
||||
| `club_creation_requests` (M7) | ✅ Basis | Capabilities + Admin-Freigabe |
|
||||
| Quota-Bypass via Capability-Grants (083) | ✅ | kein paralleles Exemption-Schema |
|
||||
| Custom Roles / Co-Trainer | ❌ | bewusst v2 |
|
||||
| Legacy-Helfer entfernt | ❌ | bewusst parallel |
|
||||
|
||||
### M4 — Anzeige ✅ teilweise
|
||||
|
||||
| Deliverable | Status |
|
||||
|-------------|--------|
|
||||
| `GET /api/me/entitlements` | ✅ |
|
||||
| `EntitlementsContext`, `hasCapability()` | ✅ (UI nutzt noch kaum) |
|
||||
| `FeatureUsageBadge` | ✅ nur KI im Übungsformular |
|
||||
| `featureUsageSync` in `request()` | ✅ |
|
||||
|
||||
### M5 — Hard-Block + vollständiger Verbrauch ⚠️
|
||||
|
||||
| Deliverable | Status |
|
||||
|-------------|--------|
|
||||
| `consume_club_feature_with_usage` Standard | ✅ `ai_calls` |
|
||||
| `CLUB_FEATURE_ENFORCE=1` produktiv | ❌ Default 0 |
|
||||
| Consume `exercises`, `exercise_media`, … | ❌ |
|
||||
|
||||
### M6 — Admin UI Rollen & Rechte ⚠️
|
||||
|
||||
| Deliverable | Status |
|
||||
|-------------|--------|
|
||||
| `/admin/rights` Capability-Matrix (Portal + Verein) | ✅ |
|
||||
| Klartext zuerst, Enforcement-Badge | ✅ 2026-06-07 |
|
||||
| Kontingent-Bypass + Vereinspläne (Seed) | ✅ |
|
||||
| Neue Pläne / Rollen anlegen (CRUD) | ❌ |
|
||||
|
||||
### Bewusst zurückgestellt
|
||||
|
||||
| ID | Inhalt |
|
||||
|----|--------|
|
||||
| M0 | CI-Isolation / Test-DB |
|
||||
| M8 | Stripe |
|
||||
| v2 | Trainer-Budgets, Custom Roles |
|
||||
|
||||
---
|
||||
|
||||
## 3. Architektur-Zielbild (kompakt)
|
||||
|
||||
```
|
||||
Request
|
||||
→ require_auth
|
||||
→ account_state (Gate)
|
||||
→ TenantContext
|
||||
→ assert_capability (Rolle / Funktion)
|
||||
→ check_club_feature_access (Vereins-Kontingent)
|
||||
→ [v2] member_feature_budget (Trainer-Budget)
|
||||
→ Governance (Objekt)
|
||||
```
|
||||
|
||||
**Drei Achsen:** Account-Lifecycle · Capabilities · Features (Kontingente). Governance bleibt vierte Prüfung.
|
||||
|
||||
---
|
||||
|
||||
## 4. Empfohlene Roadmap (nach Entscheidungen)
|
||||
|
||||
| Phase | Paket | Warum zuerst |
|
||||
|-------|--------|--------------|
|
||||
| **A** | **Onboarding-Gates vollständig** | ✅ umgesetzt (API + Frontend `/onboarding`) |
|
||||
| **B** | **M7 Vereinsgründung beantragen** | **Als Nächstes** — zweiter Pfad für `verified_pending_club` |
|
||||
| **C** | **M5 Hard-Block `ai_calls`** | Free-Plan `0` wird real; Badge (M4) liefert Erklärung |
|
||||
| **D** | **M6 voll** | Pläne-CRUD, Rollen-CRUD | ⚠️ Matrix da |
|
||||
| **E** | Entitlements im Frontend (`hasCapability`) | Entscheidung 1.2 risikoarm |
|
||||
| **F** | **M9 Kontingent-Verteilung** — Vereinsadmin vergibt Sub-Budgets pro Person (`profile_id`); Prüfung + Consume personenbezogen; UI Vereinsorga | Entscheidung 1.4, Roadmap Phase 5b |
|
||||
| **G** | `co_trainer` + Custom Roles (v2) | Entscheidung 1.2 |
|
||||
|
||||
M0 parallel, nicht blockierend.
|
||||
|
||||
---
|
||||
|
||||
## 5. Offene Punkte (vor M6 / v2)
|
||||
|
||||
1. Fairness Modell A/B/C für Trainer-Budget (Tendenz: A).
|
||||
2. Ob `admin` (Portal) langfristig neben `superadmin` bleibt.
|
||||
3. Ob offizielle Inhalte für Bewerber **nie** lesbar bleiben (aktuell: ja).
|
||||
|
||||
---
|
||||
|
||||
## 6. Referenzen
|
||||
|
||||
| Pfad | Inhalt |
|
||||
|------|--------|
|
||||
| `CAPABILITY_CATALOG.v1.md` | Capability-IDs, Account-States |
|
||||
| `CLUB_MEMBERSHIP_AND_FEATURES.v1.md` | Feature-Registry, Kontingente |
|
||||
| `backend/club_features.py` | Vereins-Features |
|
||||
| `backend/capabilities.py` | Capability-Auflösung |
|
||||
| `backend/account_lifecycle.py` | Account-Gates |
|
||||
|
||||
## 7. Superadmin im Verein (FAQ)
|
||||
|
||||
Siehe **`docs/working/RBAC_ENFORCEMENT_ROADMAP.md` §4**: Plattform-Admin (`admin`, `superadmin`) erhält **Capability-Bypass** für Vereins-Funktionen ohne `club_admin`-Mitgliedschaft. Mandant über aktiven Verein wählen; Kontingente via Bypass. Einzelne Legacy-Pfade (z. B. Löschen `visibility=club`) sind noch nicht vereinheitlicht — Ziel Phase 3.
|
||||
|
||||
---
|
||||
|
||||
**Changelog**
|
||||
|
||||
- 2026-06-06: Initial — Entscheidungen Onboarding, Rollen-Risiko, Kontingente, Trainer-Budget v2; Ist-Stand M1–M3; Roadmap A–F.
|
||||
- 2026-06-06: Phase A — `account_onboarding_gate.py`, Frontend `/onboarding`, reduzierte Navigation.
|
||||
- 2026-06-07: M4–M6 Ist-Stand, Roadmap-Verweis, Superadmin-FAQ; Admin-Matrix UX + Enforcement-Audit.
|
||||
- 2026-06-08: Roadmap Phase 5b / M9 — Vereinsadmin-Kontingentverteilung pro Person; Enforce Dev verifiziert (0.8.202).
|
||||
|
|
@ -227,7 +227,9 @@ Ziel: **vereinszentrierte** Vertrags- und Limitlogik, analog zur bestehenden Tie
|
|||
## 8. Verwandtes Dokument
|
||||
|
||||
- **`ACCESS_LAYER_AND_GOVERNANCE_PLAN.md`** – verbindliche Umsetzungsstufen A–F, einheitliche Zugriffsschicht, Scope-Erweiterung (`division`, später Community), Capability-Vorbereitung ohne Custom-Rollen-UI; Vereinsabo explizit zurückgestellt.
|
||||
- **`CAPABILITY_CATALOG.v1.md`** – Rollen, Capability-IDs, Account-Lifecycle, Endpoint-Mapping.
|
||||
- **`CLUB_MEMBERSHIP_AND_FEATURES.v1.md`** – Vereinsabo, Feature-Registry (Mitai-v9c-Pattern), Kontingente.
|
||||
|
||||
---
|
||||
|
||||
**Letzte Aktualisierung:** 2026-05-05
|
||||
**Letzte Aktualisierung:** 2026-06-06
|
||||
|
|
|
|||
144
.claude/docs/technical/NAV_RETURN_CONTEXT_SPEC.md
Normal file
144
.claude/docs/technical/NAV_RETURN_CONTEXT_SPEC.md
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
# Navigation — Return-Kontext (Rücksprung)
|
||||
|
||||
**Stand:** 2026-05-20
|
||||
**Status:** Spezifikation + Phase 1–2 umgesetzt
|
||||
**Ziel:** In der PWA (ohne Browser-Back) zuverlässig an den fachlichen Ausgangspunkt zurückkehren — inkl. sinnvollem Label und optional UI-State.
|
||||
|
||||
---
|
||||
|
||||
## Problem
|
||||
|
||||
Viele Flows navigieren von Kontext A zu Editor/Detail B (z. B. Übungsliste → Modulbearbeitung). Die Zielseite kennt A nicht und bietet nur einen **fest verdrahteten** Zurück-Link (z. B. immer „Modul-Bibliothek“). In der installierten PWA fehlt zusätzlich die Browser-Chrome.
|
||||
|
||||
Betroffen u. a.:
|
||||
|
||||
- Übungsliste → Modul anlegen/bearbeiten
|
||||
- Planung → Einheiten-Editor (teilweise gelöst via `planningReturn`)
|
||||
- Modals mit Speichern + Redirect auf Vollseite
|
||||
|
||||
---
|
||||
|
||||
## Strategie (Hybrid)
|
||||
|
||||
| Mechanismus | Wann |
|
||||
|-------------|------|
|
||||
| **Expliziter Return-Kontext** (`appReturn` in Router-State) | Seitenwechsel, bei denen das Ziel einen fachlichen Rücksprung anbieten soll |
|
||||
| **History-Back** (`navigate(-1)`) | Fallback, wenn kein Kontext gesetzt ist und History-Eintrag existiert |
|
||||
| **Default-Pfad** | Fallback der Zielseite (z. B. Modul-Bibliothek) |
|
||||
| **Modal schließen** | Overlays/Peek — kein Routing-Return |
|
||||
|
||||
**Nicht** als alleinige Lösung: reines Browser-Back (History durch `replace`, Deep Links, Reload unzuverlässig).
|
||||
|
||||
---
|
||||
|
||||
## Datenmodell
|
||||
|
||||
Router-State-Schlüssel: **`appReturn`**
|
||||
|
||||
```javascript
|
||||
{
|
||||
v: 1, // Schema-Version
|
||||
path: '/exercises', // Ziel-URL (inkl. Query, falls nötig)
|
||||
label: 'Zurück zur Übungsliste', // Anzeige im UI (vollständiger Satz)
|
||||
kind: 'exerciseList', // optional: Typ für erweiterte Wiederherstellung
|
||||
payload: { ... } // optional: kind-spezifische Daten
|
||||
}
|
||||
```
|
||||
|
||||
### `kind`-Werte (erweiterbar)
|
||||
|
||||
| kind | payload | path-Ableitung |
|
||||
|------|---------|----------------|
|
||||
| `exerciseList` | — | `/exercises` (Filter/Auswahl via sessionStorage) |
|
||||
| `planningHub` | `buildPlanningHubReturnState(...)` | `planningHubPathFromReturnState(payload)` |
|
||||
| `trainingModulesList` | — | `/planning/training-modules` |
|
||||
| `planTemplatesList` | — | `/planning/plan-templates` |
|
||||
| `frameworkProgramsList` | — | `/planning/framework-programs` |
|
||||
| `settings` | — | `/settings` |
|
||||
| `dashboard` | — | `/` |
|
||||
| `mediaLibrary` | — | `/media` |
|
||||
| `trainingRun` | `{ unitId }` | `/planning/run/:unitId` |
|
||||
| `currentLocation` | — | aktuelle Route (z. B. Einheiten-Editor) |
|
||||
| (frei) | — | `path` direkt gesetzt |
|
||||
|
||||
### Legacy-Kompatibilität
|
||||
|
||||
Bestehendes Feld **`planningReturn`** (Planung ↔ Einheiten-Editor) wird beim Lesen in `appReturn` **bridged** — keine Big-Bang-Migration nötig.
|
||||
|
||||
---
|
||||
|
||||
## API (Frontend)
|
||||
|
||||
Zentrale Datei: `frontend/src/utils/navReturnContext.js`
|
||||
|
||||
| Funktion | Zweck |
|
||||
|----------|--------|
|
||||
| `buildNavReturnContext({ path, label, kind?, payload? })` | Kontext-Objekt erzeugen |
|
||||
| `buildExercisesListReturnContext()` | Standard-Rückkehr Übungsliste |
|
||||
| `buildPlanningHubReturnContext(hubState)` | Planungs-Hub inkl. Filter-Query |
|
||||
| `buildTrainingModulesListReturnContext()` | Modul-Bibliothek |
|
||||
| `readNavReturnFromLocation(location)` | Kontext aus `location.state` (+ Legacy) |
|
||||
| `resolveNavReturnTarget(location, fallback)` | `{ path, label }` für UI |
|
||||
| `goNavReturn(navigate, location, fallback?)` | Programmatischer Rücksprung (priorisiert: Kontext → History → Fallback) |
|
||||
| `navigateWithAppReturn(navigate, to, returnContext, options?)` | Navigation mit gesetztem `appReturn` |
|
||||
| `preserveAppReturnOnNavigate(navigate, location, to, options?)` | Weiterleiten, bestehenden Kontext behalten (z. B. nach `replace`) |
|
||||
|
||||
UI-Komponente: **`PageReturnButton`** — app-typischer Zurück-Schalter (Button mit Pfeil, kein Router-Link).
|
||||
Links **zum** Ziel: **`NavStateLink`** mit `returnContext` der Quellseite.
|
||||
|
||||
### Editor-Aktionen
|
||||
|
||||
Auf Vollseiten-Editoren mit **`FormActionBar`** (`placement="bottom"`) oder **`PageFormEditorChrome`**:
|
||||
|
||||
- **Kein** separater Zurück-Link/Button oben (wirkt in der App redundant)
|
||||
- **Abbrechen** → `goBack()` / `goNavReturn(...)` (Einsprungspunkt)
|
||||
- **Speichern & Schließen** → nach erfolgreichem Save ebenfalls `goBack()`
|
||||
- Sticky Action Bar unten nutzen
|
||||
|
||||
**PageReturnButton** nur auf **Leseseiten** ohne Editor-Leiste (z. B. Übungsdetail, Einstellungen-Unterseiten, Trainingsablauf).
|
||||
|
||||
---
|
||||
|
||||
## Regeln für Entwickler
|
||||
|
||||
1. **Jede Navigation** von Kontext A zu Editor B, wo der Nutzer „weitermachen“ soll, setzt `appReturn` (oder nutzt `navigateWithAppReturn`).
|
||||
2. **Zielseite** zeigt `PageReturnButton` mit sinnvollem **Default-Fallback** (Bibliothek/Hub).
|
||||
3. **Nach Create + `replace: true`:** Return-Kontext mit `preserveAppReturnOnNavigate` erhalten.
|
||||
4. **Modals:** Schließen reicht; Redirect nach Speichern = Seiten-Navigation → Return setzen.
|
||||
5. **Kein Return-Kontext** in `location.state` für interne Bibliothek → Detail → Bearbeiten, wenn Herkunft = offensichtliche Elternliste (Default-Fallback genügt).
|
||||
6. **UI-State** (Filter, Auswahl): weiter über bestehende Session-Mechanismen (z. B. `exerciseListSessionState`), nicht im Return-Payload duplizieren, außer kind erfordert Query-Reconstruction (Planung).
|
||||
|
||||
---
|
||||
|
||||
## Umsetzungsstand
|
||||
|
||||
### Phase 1 (Pilot)
|
||||
|
||||
- [x] Spec + Utility + Tests
|
||||
- [x] `PageReturnButton` (ersetzt Link-Variante)
|
||||
- [x] Übungsliste → Modul speichern → Modul-Editor
|
||||
- [x] Planung: `SaveExercisesAsModuleModal` leitet Return-Kontext weiter
|
||||
- [x] `TrainingUnitEditPage`: `goBack` über `goNavReturn` (Legacy-bridge)
|
||||
|
||||
### Phase 2 (Flows verbinden)
|
||||
|
||||
- [x] Listen → Editoren: Übungen, Module, Vorlagen, Rahmenprogramme
|
||||
- [x] Dashboard → Übung bearbeiten / Trainingsablauf / Einheit bearbeiten
|
||||
- [x] Einstellungen-Unterseiten (Rechtliches, Systeminfo)
|
||||
- [x] Trainingsablauf + Coach-Modus (`trainingRun`, Planungs-Fallback)
|
||||
- [x] Medienbibliothek → verknüpfte Übungen/Einheiten
|
||||
- [x] `ExercisePeekModal` → Vollseite mit Return
|
||||
- [x] Editoren: Abbrechen + Speichern & Schließen → Einsprungspunkt
|
||||
|
||||
### Optional (später)
|
||||
|
||||
- Globaler Zurück-Button in App-Chrome (Mobile)
|
||||
- Nach Speichern: explizite Aktion „Zurück zum Ausgang“ im Toast
|
||||
|
||||
---
|
||||
|
||||
## Referenzen
|
||||
|
||||
- Bestehend: `frontend/src/utils/planningUnitRoutes.js` (`planningReturn`)
|
||||
- Session Übungsliste: `frontend/src/utils/exerciseListSessionState.js`
|
||||
- PWA-Kontext: `docs/FACHLICHE_NUTZERFUNKTIONEN.md`, App-Shell in `App.jsx`
|
||||
136
.claude/docs/technical/PARALLEL_TRAINING_STREAMS_SPEC.md
Normal file
136
.claude/docs/technical/PARALLEL_TRAINING_STREAMS_SPEC.md
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
# Parallele Trainingsstreams — Technische Spezifikation (Umsetzung)
|
||||
|
||||
**Status:** Umsetzung **Phase 1 (teils)** · **Stand:** 2026-05-14
|
||||
**Fachgrundlage:** `.claude/docs/functional/PARALLEL_TRAINING_STREAMS_CONCEPT.md`
|
||||
|
||||
Dieses Dokument beschreibt die **Umsetzung** auf Basis der **aktuellen Codebasis** (Stand 2026-05-14): **`training_unit_phases` / `training_unit_parallel_streams`** (Migration **063**) und **`training_unit_sections`** mit Phasen-/Stream-Bezug; **`training_unit_section_items`** (Übung/Notiz, optional `planning_method_profile` für Kombinationsübungen, Migration **057**); Rahmen-**Blueprint**-Einheiten mit `framework_slot_id` (**037**); Leitung **`lead_trainer_profile_id`** (**038**); Co-Trainer **`assistant_trainer_profile_ids`** JSONB (**042**); Durchführung und Coaching über **`TrainingUnitRunPage`**, **`TrainingCoachPage`** und **`trainingPlanUtils.js`**.
|
||||
|
||||
---
|
||||
|
||||
## 1. Ist-Stand (Code, 2026-05-14)
|
||||
|
||||
| Bereich | Aktuell |
|
||||
|---------|---------|
|
||||
| **Schema** | Migration **063:** `training_unit_phases`, `training_unit_parallel_streams`; Sektionen mit `phase_id` **oder** `parallel_stream_id`. |
|
||||
| **API** | `GET /api/training-units/:id` — **`phases`** (verschachtelt) + flache **`sections`**. `PUT/POST` mit **`phases`** für Breakout-Einheiten (**0.8.138**); höchstens eines von `phases`, `sections`, `exercises` pro Request (Planning-Router). Legacy-PUT mit nur `sections` erzeugt/ergänzt Ganzgruppen-Phase. |
|
||||
| **Planung (UI)** | Breakout-Panel: Ganzgruppen-/parallele Phasen, Streams; Speichern phasenbasiert (`trainingUnitSectionsForm.js`, `TrainingPlanningPage`). |
|
||||
| **Durchführung** | `TrainingUnitRunPage.jsx` + `trainingPlanUtils.js` (`sectionsWithPlanLocForDisplay`, `buildPlanRunViewModelFromSections`) — Phasenfolge in „Plan & Ablauf“. |
|
||||
| **Coaching** | `TrainingCoachPage.jsx` + `flattenPlanTimeline`, Stream-Picks, Rejoin vor Ganzgruppe/nächstem Split (`coachShouldPromptSplitRejoinTransition`), Nachbereitung mit `buildCoachSavePlanPayload`, danach Navigation zu `/planning/run/:id`. |
|
||||
| **Kombinationsübung** | Unverändert je Item; `planning_method_profile`, Coach-Kombi-Stufe A. |
|
||||
| **Trainer-Zuweisung** | `lead_trainer_profile_id`, `assistant_trainer_profile_ids` am Einheitskopf; **Stream-**`assigned_trainer_profile_ids` im Schema — UI/Policy noch nicht vollständig (siehe **§8 offen**). |
|
||||
| **Rahmenprogramm** | Blueprint-`training_units` können dieselbe Phasenstruktur tragen; Kopie aus Slot (`from-framework-slot`, **0.8.138**). |
|
||||
|
||||
**Hinweis:** Die frühere Planungsvariante „nur lineare `training_unit_sections` ohne Phasen“ gilt weiter für Alt-Daten; Migration **063** ordnet Bestand einer Default-Ganzgruppenphase zu.
|
||||
|
||||
---
|
||||
|
||||
## 2. Zielarchitektur (logisch)
|
||||
|
||||
```
|
||||
training_unit (Kalender-Einheit)
|
||||
├── phase (order, kind: whole_group | parallel, optional Metadaten)
|
||||
│ ├── [whole_group] → sections[] → items[] (wie heute)
|
||||
│ └── [parallel] → stream (order, label, optional trainer_ids[])
|
||||
│ └── sections[] → items[]
|
||||
```
|
||||
|
||||
**Abwärtskompatibilität:** Einheiten **ohne** explizite Phasen/Streams verhalten sich wie heute: **implizit** eine einzige „Gemeinschaftsphase“ mit den vorhandenen Sektionen (Migration: alle bestehenden Sektionen an diese Default-Hülle hängen).
|
||||
|
||||
---
|
||||
|
||||
## 3. Datenmodell — Optionen
|
||||
|
||||
**Ist (063):** Die unten skizzierte **empfohlene** Normalform ist unter den genannten Tabellennamen produktiv; die Abschnitte 3.1/3.2 bleiben zur Einordnung erhalten.
|
||||
|
||||
### 3.1 Empfohlen: explizite Phasen + Streams (normalisiert)
|
||||
|
||||
Die Tabellen sind **umgesetzt** (Namen final):
|
||||
|
||||
| Tabelle | Zweck |
|
||||
|---------|--------|
|
||||
| `training_unit_phases` | `training_unit_id`, `order_index`, `phase_kind` (`whole_group` \| `parallel`), optional `title`, `guidance_notes`, optional `planned_duration_min` |
|
||||
| `training_unit_parallel_streams` | `phase_id` (FK, nur wenn parent parallel), `order_index`, `title`/`label`, optional `notes`, optional `assigned_trainer_profile_ids` JSONB (oder 1:n-Hilfstabelle) |
|
||||
|
||||
**Anpassung `training_unit_sections`:** Zusätzliche FK-Spalte(n), z. B.:
|
||||
|
||||
- `phase_id` **NULL** und `parallel_stream_id` **NULL** → **Legacy / Default-Einheitsphase** (Migration setzt Default-Phase); oder
|
||||
- genau einer von `phase_id` (whole group) oder `parallel_stream_id` gesetzt.
|
||||
|
||||
**Constraints:** CHECK: nicht beide gesetzt; bei `phase_kind = parallel` Sektionen nur unter `parallel_stream_id`; bei `whole_group` nur unter `phase_id`.
|
||||
|
||||
**Vorteil:** Klare Semantik, Reporting, API-Shape konsistent.
|
||||
|
||||
### 3.2 Minimalvariante (nicht ideal fachlich)
|
||||
|
||||
Nur **`training_unit_parallel_streams`** + `parallel_stream_id` auf Sektionen; Phasen implizit durch „Marker“-Sektionen oder Konvention. **Nicht empfohlen**, erschwert UI und Erklärbarkeit.
|
||||
|
||||
---
|
||||
|
||||
## 4. API
|
||||
|
||||
- **`GET /api/training-units/:id`** (und Listen-Payloads wo vollständiger Plan nötig): verschachtelte Struktur **Phasen → Streams → sections → items** oder flache `sections` mit ausgefüllten `phase_id` / `parallel_stream_id` (Frontend kann normalisieren).
|
||||
- **`PUT/PATCH`:** Atomares Ersetzen der Phasen/Streams/Sektionen analog zu bestehendem `_replace_unit_sections`-Muster; **Validierung** der CHECK-Regeln serverseitig.
|
||||
- **Blueprint / Rahmen:** Blueprint-`training_units` dürfen dieselbe Struktur tragen; `GET` Kalenderliste blendet Blueprints weiter aus (`framework_slot_id IS NOT NULL`).
|
||||
|
||||
**Governance / Mandant:** Unverändert über Einheit → `group_id`; keine neuen Mandanten-Entitäten.
|
||||
|
||||
---
|
||||
|
||||
## 5. Frontend
|
||||
|
||||
### 5.1 Planung (`TrainingPlanningPage`)
|
||||
|
||||
- Darstellung als **vertikale Phasen**: Gemeinschaftsblöcke + Parallelphase mit **N Spalten** (Streams).
|
||||
- **Wiederverwendung:** `TrainingUnitSectionsEditor` **pro Stream** und pro Gemeinschaftsphase — analog zur Wiederverwendung **pro Rahmen-Slot** in `TrainingFrameworkProgramEditPage`.
|
||||
- **Co-Trainer:** UI pro Stream (`assigned_trainer_profile_ids`); Regel zur **Kopfliste** `assistant_trainer_profile_ids` festlegen (z. B. Union aller Stream-Zuweisungen für „Wer ist heute dabei“ + Rückwärtskompatibilität wenn Stream-Felder leer).
|
||||
|
||||
### 5.2 Durchführung (`TrainingUnitRunPage`)
|
||||
|
||||
- Gemeinschaftsphasen: heutiges **lineares** Verhalten.
|
||||
- Parallelphase: **Tabs, Akkordeon oder Swipe** zwischen Streams; Fortschritt **pro Stream** (Storage-Key z. B. `${unitId}:${streamId}`).
|
||||
- Kombi-Items: unverändert `CombinationPlanBracket` / `effectiveComboMethodProfile`.
|
||||
- Optional später: Filter „nur meine Spur“ anhand Session-Profil vs. Stream-Zuweisung.
|
||||
|
||||
### 5.3 Vorlagen (`training_plan_templates`)
|
||||
|
||||
- Erweiterung um **dieselbe** Phasen/Streams-Semantik (Kindtabellen oder serialisiertes JSON — Abgleich mit Kopierlogik aus Vorlage in Einheit).
|
||||
- **Kein** Live-Spiegel: weiterhin Materialisierung beim Anwenden.
|
||||
|
||||
---
|
||||
|
||||
## 6. Bezug Kombinationsübungen
|
||||
|
||||
- **Variante A** (Rotation innerhalb einer Teilstrecke): ein oder mehrere **Items** vom Typ Kombi im jeweiligen Stream; Archetyp und Parameter wie in `TRAINING_MODULES_AND_COMBINATION_EXERCISES_SPEC.md`.
|
||||
- **Variante B** (synchron Hallenweit): erweiterte **Phasen-** oder **Stream-übergreifende** Metadaten — **nicht** in MVP-Zwang; eigenes Teilpaket nach fachlicher Freigabe (`PARALLEL_TRAINING_STREAMS_CONCEPT.md` §5.2).
|
||||
|
||||
---
|
||||
|
||||
## 7. Migration und Risiken
|
||||
|
||||
1. **Datenmigration:** Alle existierenden `training_unit_sections` einer Einheit einer **Default-Phase** `whole_group` zuordnen.
|
||||
2. **API-Versionierung:** Clients, die nur flache `sections` erwarten, müssen angepasst werden (oder Server liefert **beides** kurzzeitig — nur wenn nötig).
|
||||
3. **Performance:** Tiefe Kopien (Rahmen-Slot, Duplikat Einheit) müssen rekursiv Phasen/Streams mitsamt Sektionen/Items kopieren.
|
||||
4. **Tests:** pytest für PUT/GET mit gemischten Phasen; ggf. Playwright-Smoke für Planung/Run.
|
||||
|
||||
---
|
||||
|
||||
## 8. Implementierungsphasen (Abgleich)
|
||||
|
||||
| Phase | Inhalt | Stand 2026-05-14 |
|
||||
|-------|--------|------------------|
|
||||
| **P1** | Schema Phasen + Streams; Migration **063**; GET/PUT verschachtelt; Planungs-UI; Run + Coach phasenbasiert | **Teilweise erledigt** — Run-UI nutzt Phasen-Timeline in der Anzeige; **Stream-Tabs** optional noch zu vereinheitlichen (§5.2) |
|
||||
| **P2** | Trainer-Zuordnung pro Stream + effektive Anzeige; Vorlagen erweitert | **Offen** |
|
||||
| **P3** | Synchroner Hallen-Takt / Rotationsmatrix (falls fachlich freigegeben) | **Offen** |
|
||||
|
||||
**Offene Punkte (kurz):** siehe **`docs/HANDOVER.md`** Tabelle „Coaching & Breakout“.
|
||||
|
||||
## 9. Verwandte Dokumente
|
||||
|
||||
| Dokument | Bezug |
|
||||
|----------|--------|
|
||||
| `functional/PARALLEL_TRAINING_STREAMS_CONCEPT.md` | Fachziele, Begriffe, Entscheidungsfragen |
|
||||
| `technical/TRAINING_FRAMEWORK_SPEC.md` | Rahmen-Slot vs. Parallelität |
|
||||
| `technical/TRAINING_MODULES_AND_COMBINATION_EXERCISES_SPEC.md` | Kombi, `planning_method_profile` |
|
||||
| `technical/DATABASE_SCHEMA.md`, `backend/migrations/` | DDL-Historie |
|
||||
| `TrainingPlanningPage.jsx`, `TrainingUnitRunPage.jsx`, `TrainingFrameworkProgramEditPage.jsx` | Planung, Durchführung, Rahmen |
|
||||
| `frontend/src/utils/trainingPlanUtils.js`, `TrainingCoachPage.jsx` | Phasen-Timeline, Rejoin, Coach-Speichern |
|
||||
184
.claude/docs/technical/SKILL_SCORING_SPEC.md
Normal file
184
.claude/docs/technical/SKILL_SCORING_SPEC.md
Normal file
|
|
@ -0,0 +1,184 @@
|
|||
# Gewichtetes Fähigkeiten-Scoring (Phase 3)
|
||||
|
||||
**Stand:** 2026-05-20
|
||||
**Status:** Variante A (regelbasiert) umgesetzt — **v1.3** (Peer-Kontext getrennt + Listen-Filter)
|
||||
**Modul:** `backend/skill_scoring.py`, Router `backend/routers/skill_profiles.py`
|
||||
|
||||
## Ziel
|
||||
|
||||
Trainer wählen **Schwerpunkt-Fähigkeiten** und finden passende **Bausteine** für die Trainingsplanung:
|
||||
|
||||
- **Trainingsmodule** — wiederverwendbare Übungsfolgen
|
||||
- **Rahmenprogramme** — Programme mit Zielen und Session-Slots
|
||||
- **Regressionspfade** (Progressionsgraphen) — Übungsketten
|
||||
|
||||
Das Scoring beantwortet: *Wie stark trainiert dieser Baustein eine Fähigkeit?* und *Wie stark ist er im Vergleich zu anderen **sichtbaren** Bausteinen **desselben Typs**?*
|
||||
|
||||
## Fachliche Kernregel: Peer-Kontext (nicht vermischen)
|
||||
|
||||
| Planungs-Artefakt | Vergleichsgruppe (`universal_percent`) |
|
||||
|-------------------|----------------------------------------|
|
||||
| Trainingsmodul | nur andere **sichtbare Module** |
|
||||
| Rahmenprogramm | nur andere **sichtbare Rahmenprogramme** |
|
||||
| Regressionspfad | nur andere **sichtbare Pfade** |
|
||||
|
||||
**Nicht** verglichen werden:
|
||||
|
||||
- Module vs. Rahmenprogramme vs. Pfade (kein Mix)
|
||||
- Artefakte anderer Vereine, auf die der Nutzer keinen Planungszugriff hat
|
||||
|
||||
**Sichtbarkeit:** `library_content_visibility_sql` — private, vereinsinterne und offizielle Inhalte gemäß Mandant/Rolle, analog zu anderen Bibliothekslisten.
|
||||
|
||||
## Datenquellen
|
||||
|
||||
| Artefakt | Übungen aus |
|
||||
|----------|-------------|
|
||||
| Rahmenprogramm (gesamt) | Alle Blueprint-`training_units` der Slots → `training_unit_section_items` |
|
||||
| Rahmenprogramm (pro Slot) | Blueprint einer Session |
|
||||
| Trainingsmodul | `training_module_items` (nur `item_type = exercise`) |
|
||||
| Progressionsgraph | `from_exercise_id` + `to_exercise_id` je Kante (Vorkommen zählt) |
|
||||
|
||||
Fähigkeiten je Übung: `exercise_skills` → `skills` (nur `status = active`).
|
||||
|
||||
## Gewichtungsformel (v1.1 / v1.2)
|
||||
|
||||
Pro **Übungsvorkommen** (eine Zeile im Ablauf / Modul / Kanten-Endpunkt):
|
||||
|
||||
1. **Basis-Minuten** = `planned_duration_min` der Position, sonst Default (Einheit/Modul: 8 Min, Graph: 10 Min).
|
||||
2. Pro verknüpfte Fähigkeit der Übung:
|
||||
- `Beitrag = Basis-Minuten × Anzahl Vorkommen × Link-Faktor`
|
||||
- **Link-Faktor** = Intensität × Stufen-Faktor
|
||||
|
||||
### Intensität (Nutzeneinschätzung, UI-Feld)
|
||||
|
||||
| Wert | Faktor |
|
||||
|------|--------|
|
||||
| niedrig | 0,85 |
|
||||
| mittel / leer | 1,0 |
|
||||
| hoch | 1,2 |
|
||||
|
||||
### Stufen-Spanne (`required_level` → `target_level`, UI „von/bis“)
|
||||
|
||||
Kanonische Slugs: basis … optimierung (1–5). Fehlen beide: Faktor 1,0.
|
||||
|
||||
- **Spanne** = Anzahl Stufen von „von“ bis „bis“ (1–5)
|
||||
- **Mittelpunkt** = durchschnittliche Stufe
|
||||
- Faktor ≈ `(0,92 + 0,04 × Spanne) × (0,95 + 0,025 × Mittelpunkt)` → typisch 0,96–1,20
|
||||
|
||||
### Bewusst nicht im Scoring
|
||||
|
||||
| Feld | Grund |
|
||||
|------|--------|
|
||||
| `is_primary` | Perspektivabhängig; bleibt in Übungs-UI, fließt nicht ins Profil ein |
|
||||
| `development_contribution` | Legacy-DB-Feld, in UI nicht gepflegt |
|
||||
|
||||
## Aggregierte Metriken
|
||||
|
||||
| Feld | Bedeutung |
|
||||
|------|-----------|
|
||||
| `weight` / `score` | Absolutes **Trainingsgewicht** (gewichtete Minuten) — über alle Fähigkeiten eines Artefakts vergleichbar |
|
||||
| `share_percent` | Anteil am `total_weight` **innerhalb dieses Artefakts** (summiert 100 %) — sekundär |
|
||||
| `by_main_category[]` | Je Unterkategorie `top_skill` (stärkste Fähigkeit nach Gewicht) |
|
||||
| `universal_percent` | Anteil am **Maximum derselben Fähigkeit im Peer-Kontext** (max. 100 %) |
|
||||
| `is_club_best_for_skill` | Stärkster sichtbarer Peer für diese Fähigkeit (★ in UI) |
|
||||
| `club_best` | Referenz-Peer (Titel, Typ, Gewicht) — **Legacy-Name**, fachlich Peer-Best |
|
||||
|
||||
### Berechnung `universal_percent`
|
||||
|
||||
```
|
||||
effective_ref = max(max_weight_in_peer_corpus(skill_id), eigenes_gewicht)
|
||||
universal_percent = min(100, weight / effective_ref × 100)
|
||||
```
|
||||
|
||||
Corpus je Typ: `compute_planning_corpus_by_type()` scannt sichtbare Artefakte getrennt nach `framework_program`, `training_module`, `progression_graph`.
|
||||
|
||||
Discovery-Sortierung nutzt **`match_score`** (= Summe absoluter Gewichte der gewählten Fähigkeiten), nicht den Plan-internen Anteil. Discovery verwendet typ-getrennte Referenz (`fw_ref`, `mod_ref`, `graph_ref`).
|
||||
|
||||
## API
|
||||
|
||||
| Methode | Pfad | Beschreibung |
|
||||
|---------|------|--------------|
|
||||
| GET | `/api/training-framework-programs/{id}/skill-profile` | `overall` + `slots[]` mit je `profile`; `reference_scale` (Peer-Kontext Rahmenprogramme) |
|
||||
| GET | `/api/training-modules/{id}/skill-profile` | `overall`; `reference_scale` (Peer-Kontext Module) |
|
||||
| GET | `/api/exercise-progression-graphs/{id}/skill-profile` | `overall`; `reference_scale` (Peer-Kontext Pfade) |
|
||||
| POST | `/api/skill-profiles/batch-summaries` | Kompakte Profile für Listen; Body: `frameworkProgramIds`, `trainingModuleIds`, …; Response: `summaries`, `reference_scale_by_type`, `club_best_by_skill` |
|
||||
| GET | `/api/skill-discovery/suggestions?skill_ids=1,2,3` | Ranking sichtbarer Artefakte; Query `types`, `limit` |
|
||||
|
||||
Zugriff: `get_tenant_context` + `library_content_visibility_sql` wie Parent-Artefakt.
|
||||
|
||||
### `reference_scale` / `reference_scale_by_type`
|
||||
|
||||
```json
|
||||
{
|
||||
"scope": "planning_peer",
|
||||
"artifact_type": "training_module",
|
||||
"artifacts_scanned": 12,
|
||||
"skills_in_corpus": 34,
|
||||
"description": "Prozent = Anteil am stärksten sichtbaren Eintrag unter Trainingsmodulen …"
|
||||
}
|
||||
```
|
||||
|
||||
## UI
|
||||
|
||||
### Bearbeitung (Vollprofil)
|
||||
|
||||
| Ort | Panel | `artifactType` |
|
||||
|-----|-------|----------------|
|
||||
| Rahmenprogramm bearbeiten | Fähigkeiten-Schwerpunkte (+ Sessions) | `framework_program` |
|
||||
| Trainingsmodul bearbeiten | Fähigkeiten im Modul | `training_module` |
|
||||
| Progressionsgraph | Fähigkeiten entlang des Pfads | `progression_graph` |
|
||||
|
||||
Anzeige: Top je Kategorie (Editor) oder alle Fähigkeiten (Modal). Hinweise nennen Peer-Kontext explizit (z. B. „72 % Rahmenpr.“).
|
||||
|
||||
### Listen & Filter (UX wie Übungsliste)
|
||||
|
||||
| Liste | Filter |
|
||||
|-------|--------|
|
||||
| Rahmenprogramme (`/planning/framework-programs`) | Suche, Katalog (Fokus/Trainingsart/Zielgruppe), Session-Dauer, **Fähigkeiten** (`SkillTreeMultiSelect`), Mindest-% im Peer-Kontext, Sortierung nach Stärke |
|
||||
| Trainingsmodule (`/planning/training-modules`) | Suche, **Fähigkeiten** (+ Min-%, Sortierung) |
|
||||
|
||||
- **Filter-Button** mit Badge, entfernbare **Chips**, Einstellungen im **Modal** (`PlanningArtifactFilterModal`)
|
||||
- KPI-Kacheln: Top-Fähigkeit **je Unterkategorie** mit Score + Peer-%
|
||||
- Vollprofil-Modal: `SkillProfileFullModal` mit `displayMode=full`
|
||||
|
||||
Profil wird nach Speichern neu geladen (`skillProfileTick` in Editoren).
|
||||
|
||||
### Discovery
|
||||
|
||||
**Fähigkeiten-Seite → Planungs-Vorschläge:** Multi-Select + API `/api/skill-discovery/suggestions` (optional Filter `types`).
|
||||
|
||||
## Frontend-Module (Auswahl)
|
||||
|
||||
| Pfad | Rolle |
|
||||
|------|--------|
|
||||
| `frontend/src/components/planning/PlanningArtifactFilterModal.jsx` | Filter-Modal |
|
||||
| `frontend/src/components/planning/PlanningSkillFilterSection.jsx` | Fähigkeiten-Block im Modal |
|
||||
| `frontend/src/utils/planningArtifactFilterChips.js` | Chip-Labels + Entfernen |
|
||||
| `frontend/src/utils/frameworkProgramListHelpers.js` | Client-Filter Rahmenprogramme |
|
||||
| `frontend/src/utils/trainingModuleListHelpers.js` | Client-Filter Module |
|
||||
| `frontend/src/components/skills/SkillProfileCompact.jsx` | KPI-Kacheln in Listen |
|
||||
| `frontend/src/components/SkillTreeMultiSelect.jsx` | Baumauswahl (Portal-Dropdown in Modals) |
|
||||
|
||||
## Grenzen / später
|
||||
|
||||
- Kein DB-Cache (`skill_profile_json`) — on-the-fly; bei >50 Artefakten pro Typ serverseitiger Index/Caching
|
||||
- Entwicklungsziele am Rahmenkopf bleiben Freitext (kein Scoring)
|
||||
- KI-Zusammenfassung (Variante B) nicht Teil von v1.0
|
||||
- Trainings**einheiten** (Kalender) optional als nächste Erweiterung
|
||||
- Filter-Persistenz („Als Standard speichern“) wie bei Übungen — noch nicht für Planungslisten
|
||||
- Fähigkeiten-Filter im Dialog **Planung → Rahmen übernehmen** — Katalog ja, Skill-Filter optional nachziehen
|
||||
- API-Feldnamen `club_*` / `skillMinClubPercent` — technische Altlast, semantisch Peer-Kontext
|
||||
|
||||
## Tests
|
||||
|
||||
- `backend/tests/test_skill_scoring.py` — Multiplikator, Aggregation, Match-Score, Cap 100 %
|
||||
- **Offen:** dedizierte Tests für `compute_planning_corpus_by_type` (Typ-Trennung)
|
||||
|
||||
## Verweise
|
||||
|
||||
| Dokument | Inhalt |
|
||||
|----------|--------|
|
||||
| `functional/DOMAIN_MODEL.md` | Domänenabschnitt Planungs-Fähigkeiten-Profil |
|
||||
| `docs/FACHLICHE_NUTZERFUNKTIONEN.md` | Nutzerüberblick Listen/Filter |
|
||||
| `docs/HANDOVER.md` | Handover-Abschnitt Phase 3 |
|
||||
| `technical/ACCESS_LAYER_AND_GOVERNANCE_PLAN.md` | Sichtbarkeit / Mandant |
|
||||
|
|
@ -15,6 +15,8 @@
|
|||
| `DATABASE_SCHEMA.md` | **Nachgeordnete** Übersicht: Migrationshistorie und Tabellenliste; Detail-DDL primär **hier §2–§3** + SQL unter `backend/migrations/`. |
|
||||
| `functional/DOMAIN_MODEL.md` | Fachliche Begriffe; Kurzverweis auf Progressionsgraph ergänzt. |
|
||||
| `TRAINING_CURRICULUM_AND_GOVERNANCE_CONCEPT.md` | **Was** und **warum** (Bibliothek vs. Instanz, Governance, CURR‑Tabelle). |
|
||||
| `functional/PARALLEL_TRAINING_STREAMS_CONCEPT.md`, `technical/PARALLEL_TRAINING_STREAMS_SPEC.md` | **Parallele Streams / Breakout innerhalb einer Einheit** — orthogonale Domäne zu **Rahmen‑Slots** (Serien‑Sessions). |
|
||||
| `technical/SKILL_SCORING_SPEC.md` | **Fähigkeiten-Profil** der Rahmen‑Slots / Module / Pfade; Listen-Filter und Peer‑Vergleich (nur gleicher Artefakttyp). |
|
||||
|
||||
**Konsequenz:** Diese Datei bleibt der **technische Arbeitspool** für Rahmenprogramm Stufe 1–2. Abschnitt **§4** beschreibt explizit den **aktuellen Produktfreigabe-Umfang** und **bekannte Lücken** (damit Trainingsplanung weiter gebaut werden kann ohne falscher Erwartung an „Alternative‑Pakete“ in der UI).
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,205 @@
|
|||
# Trainingsmodule und Kombinationsübungen — Spezifikation (Entwurf)
|
||||
|
||||
**Status:** Entwurf zur fachlichen und technischen Abstimmung · **Stand:** 2026-05-12 (Code **0.8.110**, siehe `backend/version.py`)
|
||||
**Zweck:** Rahmen für Umsetzung, Integration in Planung/Rahmenprogramm und Durchführung im assistierten Training (Coaching-Modus). Dieses Dokument ist **nicht** implementierungsbindend, bis die markierten **offenen Entscheidungen** geschlossen und der Status angehoben wurde.
|
||||
|
||||
**Abgleich mit Code (Drift vermeiden):**
|
||||
|
||||
- **Kanonische Archetyp-IDs:** fest in `backend/routers/exercises.py` (`COMBINATION_ARCHETYPE_IDS`); fachliche Tabelle und UI-Labels in `frontend/src/constants/combinationArchetypes.js` — die **fachliche Master-Zuordnung** Name↔ID steht in `functional/Shinkan Trainingsmodule Kombinationsuebungen Spezifikation V2.md` § 10.2.1. **Administrierbare Archetypen** (DB/UI) gibt es **nicht**; Erweiterungen nur per Code-Release — Fachspez **§ 10.6**.
|
||||
- **Planungs-Override:** `planning_method_profile` (Migration **057**) speichert einen **Snapshot**; **Merge** mit Katalogprofil erfolgt im Frontend (`frontend/src/utils/comboPlanningMethodProfile.js` — `effectiveComboMethodProfile`), nicht als serverseitiger Join. Payload-Sanitisierung vor PUT; Backend speichert JSONB zuverlässig (`Json()`).
|
||||
- **Coaching:** Stufe **A** — Slots, Kandidaten, Archetyp-Hilfstext, **Label** für globale Eckdaten (`describeGlobalComboProfile` in `combinationMethodProfileUi.js`), visuelle Klammer (`CombinationPlanBracket`) in Peek/Run; Stufen **B/C** (archetypgesteuerte Zeitleiste/Takt) **offen** — Fachspez § 10.4, **§ 10.6**, **Anhang A**.
|
||||
- **Umsetzungsplan:** `working/TRAINING_MODULES_IMPLEMENTATION_PLAN.md` (Phase 2/4 „teilweise“; Pakete **4e–4g** für Admin, Vorbelegung, Validierung).
|
||||
|
||||
**Verwandte Dokumente:**
|
||||
|
||||
| Dokument | Bezug |
|
||||
|----------|--------|
|
||||
| `TRAINING_FRAMEWORK_SPEC.md` | Rahmen-Bibliothek, Slot-Blueprint, Kopiersemantik (`from-framework-slot`) |
|
||||
| `functional/PARALLEL_TRAINING_STREAMS_CONCEPT.md`, `technical/PARALLEL_TRAINING_STREAMS_SPEC.md` | Parallele Teilstrecken **innerhalb einer Einheit**; Kombi-Übungen weiter nutzbar **pro Stream** für Stationsrotation |
|
||||
| `DATABASE_SCHEMA.md` | Aktueller Stand `training_units`, Sektionen, Items |
|
||||
| `functional/DOMAIN_MODEL.md` | Domänenbegriffe (bei Bedarf zu erweitern) |
|
||||
| `EXERCISES_*` (Katalog) | Einzelübungen, Varianten |
|
||||
| `ACCESS_LAYER_AND_GOVERNANCE_PLAN.md` | Sichtbarkeit, Mandant, Rollen bei neuen Bibliotheks-Entitäten |
|
||||
|
||||
---
|
||||
|
||||
## 1. Zielbild und Abgrenzung
|
||||
|
||||
### 1.1 Problem
|
||||
|
||||
Die Trainingsplanung unterstützt Einheiten mit Sektionen und einzelnen Übungen (inkl. Notizen) sowie Rahmenprogramme mit Blueprint-Einheiten pro Slot. Es fehlen:
|
||||
|
||||
- **Wiederverwendbare Übungsfolgen** („Trainingsmodule“), die sich wie Bausteine in eine Einheit einfügen lassen (ganze Sektion oder Block innerhalb einer Sektion), inkl. kopierbasierter Integration analog zum Rahmen.
|
||||
- **Strukturierte Kombinationsformen** (z. B. Zirkel mit Stationstausch, Parcour), bei denen **mehrere Einzelübungen** Slots oder Rollen einnehmen und die **Trainingsmethode** den Ablauf (Rotation, parallele Stationen, Zeitmodell) bestimmt.
|
||||
- Ein durchgängiges Konzept für den **Coaching- bzw. Assistenzmodus**, in dem derselbe Plan je nach Archetyp **unterschiedlich gesteuert** wird (Beispiel Zirkel: Erklärphase vs. parallele Nutzung aller Stationen).
|
||||
|
||||
### 1.2 Nicht-Ziele (für erste Ausbaustufe)
|
||||
|
||||
- **Individuelles Athleten-Tracking** oder Leistungsmessung pro Person (außerhalb Shinkan-MVP, siehe Produktabgrenzung).
|
||||
- Automatische **Synchronisation** zwischen Bibliotheksexemplar und bereits geplanten historischen Einheiten (bewusst: **Kopie** statt Live-Spiegel, konsistent mit Rahmen-Konzept).
|
||||
|
||||
### 1.3 Zwei Bausteine (fachliche Trennung)
|
||||
|
||||
| Baustein | Kurzname | Einordnung | Kurzbeschreibung |
|
||||
|----------|-----------|------------|------------------|
|
||||
| **Typ 1** | **Kombinationsübung** | Übungskatalog (Sonderform einer **Übung**) | Eine logische Übung mit **1–n Slots**; Slots können einzelne Übungen oder **Pools** auswählbarer Übungen tragen; **Methodenprofil / Archetyp** steuert später den Durchlauf. |
|
||||
| **Typ 2** | **Trainingsmodul** | Planung / Bibliothek | Gespeicherte, wiederverwendbare **Sequenz** von Elementen (Einzelübungen, optional Kombinationsübungen, Notizen); Einbindung per **Kopie** in konkrete `training_units` oder in Rahmen-Slot-Blueprints. |
|
||||
|
||||
**Abgrenzung Rahmenprogramm:** Das Rahmenprogramm strukturiert **mehrere Einheiten** (Slots) auf Programm-Ebene. Ein Trainingsmodul strukturiert typischerweise **Inhalt einer Einheit** oder eines Teils davon, nicht den Wochen-/Periodenrahmen.
|
||||
|
||||
---
|
||||
|
||||
## 2. Begriffe
|
||||
|
||||
| Begriff | Definition |
|
||||
|---------|------------|
|
||||
| **Bibliotheksexemplar** | Gespeicherte Vorlage (Modul oder Kombinationsübung-Definition) mit Governance (z. B. global, Verein, privat). |
|
||||
| **Instanz in der Planung** | In `training_unit_section_items` (und ggf. ergänzende Tabellen) materialisierter Ablauf für einen **konkreten Termin** bzw. eine **geplante Einheit**. |
|
||||
| **Slot (Typ 1)** | Position innerhalb einer Kombinationsübung; kann genau eine gewählte Übung oder einen **Pool** (mehrere Kandidaten) referenzieren. |
|
||||
| **Methodenprofil / Archetyp** | Maschinenlesbare Semantik **wie** trainiert wird (Zeit, Rotation, Parallelität), ergänzend zum bestehenden Katalog `training_methods` (Beschreibung **was** für eine Didaktik/Kondition gilt). |
|
||||
| **Coaching-Modus** | UI- und Zustandslogik zur Durchführung einer geplanten Einheit (Timer, Phasen, Stationen). |
|
||||
|
||||
---
|
||||
|
||||
## 3. Trainingsmethoden und Archetypen (Typ 1)
|
||||
|
||||
### 3.1 Bestehende Basis
|
||||
|
||||
Der Katalog `training_methods` (Migration 003) enthält u. a. **Zirkeltraining** (`category` u. a. `zirkel`, `kondition`). Er beschreibt die Methode **inhaltlich**, nicht aber Parameter wie Wechselintervalle oder parallele vs. rotierende Nutzung.
|
||||
|
||||
### 3.2 Erweiterung: Archetyp
|
||||
|
||||
Jede Kombinationsübung (und optional der Methodendatensatz als Default) erhält ein Feld **`method_archetype`** (Enum/Wertliste). Der Archetyp legt fest, welche **Parameter** am Methodenprofil relevant sind und wie der **Coaching-Modus** den Ablauf interpretiert.
|
||||
|
||||
**Vorschlagsliste (erweiterbar, zu verbindlich machen):**
|
||||
|
||||
| Archetyp-ID (Vorschlag) | Beschreibung Planungslogik | Coaching (Intent) |
|
||||
|-------------------------|----------------------------|---------------------|
|
||||
| `circuit_rotate_time` | n Stationen; Wechsel nach Ablaufzeit, optional globale Rundenanzahl | Rotierender oder gemeinsamer Takt; Umlauf zur nächsten Station |
|
||||
| `circuit_all_parallel` | n Stationen; **kein** Umlauf als fachlicher Kern, alle Stationen gleichzeitig aktiv | Erklärphase (alle Inhalte vorher), dann **parallel** alle Stationen |
|
||||
| `sequence_linear` | feste Reihenfolge; Aufbau, keine Kreisrotation | Schrittliste / Timer optional pro Schritt |
|
||||
| `station_parcour` | Stationsbezogener Pfad, Reihenfolge kann variieren | Navigation / Abhaken eher als ein globaler Umlauf-Takt |
|
||||
| `pair_superset` | zwei (oder wenige) Blöcke im Wechsel | Partnerlogik, gekoppelte Timer |
|
||||
| `time_domain_interval` | AMRAP/EMOM-ähnliche Zeitdomäne | Globale Uhr, Runden-/Intervallzähler |
|
||||
|
||||
### 3.3 Parameter des Methodenprofils
|
||||
|
||||
Zu präzisieren (JSON-Dokument vs. normalisierte Spalten):
|
||||
|
||||
- Zeit: `work_seconds`, `rest_seconds`, `transition_seconds`, `rounds`
|
||||
- Organisation: `station_count`, `rotation_direction`, Flags `explain_all_before_start`, `stations_operate_simultaneously`
|
||||
- ggf. `intensity_profile` (skalar oder Enum), nur wenn für MVP nötig
|
||||
|
||||
**Offen:** Welche Parameter sind **Pflicht pro Archetyp** (Validierung).
|
||||
|
||||
---
|
||||
|
||||
## 4. Datenmodell (Zielarchitektur, Entwurf)
|
||||
|
||||
### 4.1 Typ 2 — Trainingsmodule
|
||||
|
||||
**Entwurfstabellen (Namen können bei Implementierung angeglichen werden):**
|
||||
|
||||
- `training_modules` — Kopf: Titel, Beschreibung, Metadaten, `visibility`, `club_id`, `created_by`, Timestamps
|
||||
- optional `training_module_sections` — falls ein Modul mehrere semantische Blöcke abbilden soll
|
||||
- `training_module_items` — Reihenfolge, Verweis auf:
|
||||
- Einzelübung (`exercise_id`, `exercise_variant_id`)
|
||||
- Kombinationsübung (`combination_exercise_id` / `exercise_id` mit `kind=combination`)
|
||||
- Freitext-Notiz (analog `note` bei Einheiten)
|
||||
|
||||
Semantik: **Bibliotheksbaum**, keine Bindung an Kalender oder Gruppe.
|
||||
|
||||
### 4.2 Typ 1 — Kombinationsübungen
|
||||
|
||||
**Option A (Embedding in `exercises`):** Spalte `exercise_kind` = `simple` | `combination` und Kindtabellen für Slots/Pools.
|
||||
|
||||
**Option B (Separate Kopf-Tabelle):** 1:1-Beziehung zwischen `exercises` und `combination_exercises`.
|
||||
|
||||
**Slot-Pools:** mindestens M:N **Pool-Kandidat** pro Slot; die **konkret geplante Auswahl** gehört zur **Instanz** (geplante Einheit), nicht zwingend zum Bibliotheksexemplar.
|
||||
|
||||
### 4.3 Integration in geplante Einheiten
|
||||
|
||||
Heute: `training_unit_section_items` mit `item_type` in (`exercise`, `note`).
|
||||
|
||||
**Erweiterungsoptionen (Entscheidung offen):**
|
||||
|
||||
1. **Expansion beim Einfügen:** Modul wird in Items „aufgeklappt“; optional `source_module_id` an Items für Herkunft (Lineage-Light).
|
||||
2. **Block-Item:** neuer `item_type` `module_reference` oder `combination` mit ID und eingebetteter Bearbeitungssemantik (komplexer, aber „Modul als Einheit“ editierbar).
|
||||
|
||||
Empfehlung zur Abstimmung: MVP oft mit **Expansion** + optionaler Markierung; später Block-Knoten.
|
||||
|
||||
**Rahmenprogramm:** Blueprint-`training_units` pro Slot nutzen dieselbe Sektions-/Item-Struktur — Module müssen **dort** ebenfalls einfügbar sein, wenn Rahmen und konkrete Planung konsistent bleiben sollen.
|
||||
|
||||
---
|
||||
|
||||
## 5. API (Skizze)
|
||||
|
||||
Verbindliche Pfade und Payloads folgen nach Freigabe dieses Dokuments.
|
||||
|
||||
| Richtung | Beispielpfad / Funktion | Zweck |
|
||||
|----------|-------------------------|--------|
|
||||
| CRUD | `GET/POST/PUT/DELETE …/training-modules` | Bibliothek Trainingsmodule |
|
||||
| Anwendung | `POST …/training-units/{id}/apply-module` | Modulinhalt in Sektion kopieren (tiefe Kopie) |
|
||||
| Übungen | Erweiterung `GET/POST/PUT …/exercises` oder Unterressource `…/exercises/{id}/combination` | Kombinationsübung inkl. Slots |
|
||||
| optional | `POST …/training-units/from-module` | Neue Einheit aus Modul (falls produktrelevant) |
|
||||
|
||||
**AuthZ:** analog andere Bibliotheks- und Planungsobjekte; Abgleich mit `ACCESS_LAYER_AND_GOVERNANCE_PLAN.md` und Endpoint-Audit.
|
||||
|
||||
---
|
||||
|
||||
## 6. Frontend
|
||||
|
||||
- **Bibliothek:** Verwaltung Trainingsmodule (Liste, Editor, Sortierung, Vorschau).
|
||||
- **Übungsbereich:** Editor für Kombinationsübungen (Slots, Pools, Methodenprofil/Archetyp).
|
||||
- **Planungs-UI:** Aktion „Modul einfügen“, Ziel-Sektion und Position; Hinweis **Kopie** und Editierbarkeit pro Termin.
|
||||
|
||||
---
|
||||
|
||||
## 7. Coaching- / Assistenzmodus (Durchlauf)
|
||||
|
||||
### 7.1 Phasenmodell (konzeptionell)
|
||||
|
||||
- **Briefing / Erklärung:** insbesondere für `circuit_all_parallel` und Varianten mit `explain_all_before_start`
|
||||
- **Arbeitsphase(n):** timer- und stationsgetrieben
|
||||
- **Übergänge:** Pausen, Wechsel, Rundenzähler
|
||||
|
||||
### 7.2 Persistenz während Durchführung
|
||||
|
||||
**Offen:** Ob ein **`training_session_run`** (Snapshot der aufgelösten Einheit zum Startzeitpunkt) für Nachvollziehbarkeit und Offline-Fähigkeit nötig ist.
|
||||
|
||||
### 7.3 Ausbaustufen
|
||||
|
||||
1. Read-only **Durchführungsansicht** (Archetyp + Zeiten, keine komplexe State Machine)
|
||||
2. **Aktiver Modus** mit State Machine und Archetyp-spezifischer UI
|
||||
3. Optional: Offline/PWA-Verhalten
|
||||
|
||||
---
|
||||
|
||||
## 8. Umsetzungsphasen (Vorschlag)
|
||||
|
||||
| Phase | Inhalt |
|
||||
|-------|--------|
|
||||
| **A** | Dieses Dokument verbindlich machen; Archetypen und Parameter final; Governance-Regeln |
|
||||
| **B** | Typ 2: `training_modules` + API + „Modul in Einheit einfügen“ (Expansion) |
|
||||
| **C** | Typ 1: Kombinationsübung im Katalog + Slots/Pools + Methodenprofil |
|
||||
| **D** | Einbindung in Rahmen-Slot-Blueprints (Editor-Flow) |
|
||||
| **E** | Coaching-Modus gemäß Archetyp |
|
||||
|
||||
---
|
||||
|
||||
## 9. Offene Entscheidungen (Checkliste)
|
||||
|
||||
- [ ] Modul-Einfügung: nur **Expansion** vs. **Block-Knoten** vs. beides
|
||||
- [ ] Normalisierung vs. JSON für **Methodenprofil-Parameter**
|
||||
- [ ] Globale vs. vereinsbezogene vs. private **Trainingsmodule** (Governance-Matrix)
|
||||
- [ ] Pflichtbinding: muss jede Kombinationsübung einen **Default-Archetyp** aus `training_methods` erben dürfen?
|
||||
- [ ] Coaching: Mindestumfang MVP (nur Ansicht vs. interaktive Timer)
|
||||
- [ ] Verweise in `DOMAIN_MODEL.md` und `DATABASE_SCHEMA.md` nach Implementierung pflegen
|
||||
|
||||
---
|
||||
|
||||
## 10. Changelog
|
||||
|
||||
| Datum | Änderung |
|
||||
|-------|----------|
|
||||
| 2026-05-12 | Erstversion (Entwurf) angelegt |
|
||||
|
|
@ -13,8 +13,11 @@ Fortlaufend gemäß `ACCESS_LAYER_AND_GOVERNANCE_PLAN.md` Stufe A–C.
|
|||
| exercises | `PATCH /api/exercises/bulk-metadata` | ja | `get_tenant_context` | ja | Liste: UI-Mehrfachwahl; bis 500 IDs; nur Ersteller oder Plattform-Admin |
|
||||
| exercises | `GET .../media/{mid}/file` | ja | `get_tenant_context_flexible` | ja (wie Übung lesen) | Datei oder `?ssetoken`; kein anonymes `/media/` ohne ALLOW_PUBLIC_MEDIA_STATIC |
|
||||
| exercises | übrige geschützte `/api/exercises*` | ja | `get_tenant_context` | ja | PUT Einzelübung: bei Sichtbarkeit `official` Medien-§4.2 (422: Lifecycle/Promotion/Copyright) |
|
||||
| exercises | POST `/api/exercises/ai/suggest`, POST `/api/exercises/{id}/ai/regenerate` | ja | `get_tenant_context` | nein | Nur Vorschlags-JSON; keine DB-Schreibung; OpenRouter — suggest optional `focus_areas_context` für Retrieval-Profile |
|
||||
| exercise_progression_graphs | `/api/exercise-progression-graphs*` | ja | `get_tenant_context` | Liste wie Bibliothek; Schreiben Ersteller/Plattform-Admin | Kanten: Lesen wenn Graph lesbar |
|
||||
| training_planning | alle geschützten Endpoints | ja | `get_tenant_context` | ja | Vorlagen-Liste wie Übungen; POST Vorlage Default club_id |
|
||||
| dashboard | `GET /api/dashboard/kpis` | ja | `get_tenant_context` | wie `GET /api/exercises` + `GET /api/training-units` | Aggregat für Dashboard-Kurzüberblick (ein Roundtrip) |
|
||||
| training_modules | `/api/training-modules*` | ja | `get_tenant_context` | ja | Bibliotheks-Module wie Vorlagen/Rahmen; POST Default `club_id` bei `visibility=club` |
|
||||
| training_framework_programs | alle geschützten Endpoints | ja | `get_tenant_context` | ja | Liste + POST Default club_id |
|
||||
| admin_users | `GET /api/admin/users` | Plattform | `require_auth` | Admin-Rolle | EXEMPT `check_access_layer_hints.py` |
|
||||
| platform_media_storage | `GET/PUT /api/admin/platform-media-storage` | Plattform | `require_auth` | GET: `is_platform_admin`; PUT: nur `superadmin` | Relativer Pfad unter `MEDIA_ROOT`; keine Secrets; EXEMPT wie admin_users |
|
||||
|
|
@ -30,18 +33,29 @@ Fortlaufend gemäß `ACCESS_LAYER_AND_GOVERNANCE_PLAN.md` Stufe A–C.
|
|||
| skills | `/api/skills*` | nein (global) | `require_auth` | je Endpoint | EXEMPT |
|
||||
| maturity_models | Admin-Matrix | nein (global) | `require_auth` | Admin für Schreiben; `GET …/{id}` nur Portal-Admin | EXEMPT |
|
||||
| matrix_stack_bundle | Export/Import Bundles | Plattform/Test | `require_auth` | Admin | EXEMPT |
|
||||
| matrix_editor | `/api/admin/matrix-editor/*` (Export/Import Editor-Bundle) | Plattform | `require_auth` | nur `superadmin` | EXEMPT; globale Fähigkeitsmatrix ohne Mandantenkontext |
|
||||
| import_wiki / import_wiki_admin | Wiki-Import | Werkzeug | `require_auth`/Admin | Admin | EXEMPT |
|
||||
| ai_skill_retrieval_admin | `/api/admin/ai-skill-retrieval-profiles*` (CRUD) | Plattform | `require_auth` | nur `superadmin`; JSON `config` | EXEMPT wie `admin_users`; kein Vereinsbezug |
|
||||
| ai_prompts_admin | `/api/admin/ai-prompts*` (Liste, Detail, PUT, Preview, Reset) | Plattform | `require_auth` | nur `superadmin` | EXEMPT; globale `ai_prompts` ohne Mandantenkontext |
|
||||
| exercise_enrichment_admin | `/api/admin/exercise-enrichment/*` (Kandidaten, Preview, Apply) | Plattform | `require_auth` | nur `superadmin` | EXEMPT; plattformweite Übungsliste + Skill-Schreibung; kein TenantContext |
|
||||
| admin_user_content | `/api/admin/user-content/*` (Meta, Nutzer-Summary, Items, PATCH, DELETE) | Plattform | `require_auth` | nur `superadmin` | EXEMPT; Moderation nutzerangelegter Inhalte inkl. privat; kein TenantContext |
|
||||
|
||||
**Legende:** Router auf der EXEMPT-Liste des Scripts sind globale oder Auth-only-Pfade; sobald ein Router Vereinsdaten oder Bibliotheks-Sichtbarkeit erhält, EXEMPT entfernen und `get_tenant_context` einführen.
|
||||
|
||||
**Pflege / Drift:** Änderungen an Mandanten, Governance (`visibility`/`club_id`) oder neuen inhaltsbezogenen Endpoints → eine Zeile in dieser Tabelle anpassen und `PRODUCTION_READINESS_AUDIT_2026-05.md` prüfen.
|
||||
|
||||
Letzte Änderung: 2026-05-07 — Upload-Dedupe Papierkorb 409 + `reactivate`; DELETE …/media nur Verknüpfung.
|
||||
Letzte Änderung: 2026-06-06 — Superadmin `/api/admin/user-content/*` (Nutzer-Inhalte Moderation).
|
||||
|
||||
---
|
||||
|
||||
### Changelog (Fortführung)
|
||||
|
||||
- **2026-05-23:** Superadmin-API `exercise_enrichment_admin` (Batch-Übungs-Anreicherung KI) dokumentiert.
|
||||
- **2026-05-30:** Superadmin-API `ai_prompts_admin` (`/api/admin/ai-prompts*`) dokumentiert.
|
||||
- **2026-05-29:** Superadmin-API `ai_skill_retrieval_admin` (Retrieval-Profile) dokumentiert.
|
||||
- **2026-05-22:** Übungs-KI-Endpunkte (Suggest/Regenerate) dokumentiert.
|
||||
|
||||
- **2026-05-13:** Dashboard-KPI-Endpunkt dokumentiert.
|
||||
- **2026-05-07:** Legacy `GET/PUT /api/profile` auf Session-Profil gehärtet; OpenAPI/Health-Ready Produktionsdefaults; Security-Release-Tests + CI-Schritt `security_release_checks.py` — siehe `PRODUCTION_READINESS_AUDIT_2026-05.md`.
|
||||
- **2026-05-07 (Phase 3):** CSP SPA (nginx); API `nosniff`-Middleware — siehe `PRODUCTION_READINESS_AUDIT_2026-05.md`.
|
||||
|
||||
|
|
|
|||
67
.claude/docs/working/AI_EXERCISE_IMPLEMENTATION_PLAN.md
Normal file
67
.claude/docs/working/AI_EXERCISE_IMPLEMENTATION_PLAN.md
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
# Umsetzungsplan – KI bei Übungen (stufenweise, Driftschutz)
|
||||
|
||||
**Version:** 0.2
|
||||
**Datum:** 2026-05-29
|
||||
**Bezüge:** `functional/AI_EXERCISE_ASSISTANT_VISION.md` · **`working/AI_SKILL_RETRIEVAL_PROFILES_SPEC.md`** · `technical/KI_FEATURES_SPEC.md` · `technical/AI_PROMPT_SYSTEM_SPEC.md` · `technical/AI_TRAINING_PLANNING_CONCEPT.md` (§1.1 Ist-Stand)
|
||||
|
||||
---
|
||||
|
||||
## 1. Drift vermeiden – verbindliche Regeln
|
||||
|
||||
1. **Spec vor Code:** Request/Response-Felder und Statuscodes an `KI_FEATURES_SPEC.md` ausrichten; Abweichungen zuerst Spec oder dieses Dokument anpassen.
|
||||
2. **Prompts in der DB:** Keine produktionskritischen Prompt-Langtexte nur im Code; Defaults per **Migration** in `ai_prompts`, Anpassung durch Admins über vorgesehene Oberfläche (später) oder SQL.
|
||||
3. **Skill-Retrieval-Profile:** Gewichte/Quotes in **`ai_skill_retrieval_profiles.config`** — Spezifikation `working/AI_SKILL_RETRIEVAL_PROFILES_SPEC.md`; kein zweites gleichzeitiges Truth-Repo im Sourcecode außer defensiver Fallback `_FALLBACK_RETRIEVAL_CONFIG` in `exercise_ai.py`.
|
||||
4. **Stufen-Slugs & Intensität:** Nur **kanonische** Werte wie in `exercises.py` (`basis` … `optimierung`, `niedrig|mittel|hoch`); LLM-Ausgaben **normalisieren**, ungültige `skill_id` verwerfen.
|
||||
5. **Kein stiller DB-Write:** KI liefert **Vorschläge**; Persistenz nur über bestehende **PUT/POST exercises** inkl. Trainer-Aktion (und optional `summary_ai_generated` / `ai_suggested` wie Spec).
|
||||
6. **Mandant:** Übungsbezogene KI-Endpunkte nutzen `Depends(get_tenant_context)`; keine Ausnahme ohne Eintrag in `ACCESS_LAYER_ENDPOINT_AUDIT.md`.
|
||||
7. **Schema:** Neue DB-Objekte nur nummerierte Migration **`backend/migrations/`** (aktuell bis **068**) und `DB_SCHEMA_VERSION` anheben.
|
||||
|
||||
---
|
||||
|
||||
## 2. Stufen (Releases)
|
||||
|
||||
| Stufe | Inhalt | Exit-Kriterium |
|
||||
|-------|--------|------------------|
|
||||
| **S0** | Dieses Dokument + Verweise konsistent | Review abgehakt |
|
||||
| **S1** | Migration `ai_prompts` + Defaults `exercise_summary`, `exercise_skill_suggestions`; `exercises.summary_ai_generated` | Migrierte DB, App startet |
|
||||
| **S2** | `httpx`-Client OpenRouter; Modul lädt Prompt, ersetzt Platzhalter, parst Antwort | Unit-/Smoke: 503 ohne Key |
|
||||
| **S3** | `POST /api/exercises/ai/suggest`, `POST /api/exercises/{id}/ai/regenerate` | OpenAPI/Handtest mit Key |
|
||||
| **S4** | Frontend: KI-Vorschlag, **Änderungsdialog** (Vorschau, Kurzfassung wählbar, Fähigkeiten pro Zeile an-/abwählbar), dann Übernahme ins Formular | Manuelle UX-Prüfung |
|
||||
| **S4b** | **Skill-Retrieval:** Migration **`ai_skill_retrieval_profiles`**, `focus_areas_context` am **`POST …/ai/suggest`**, `exercise_ai` kontextbezogener Katalog (Gewichte, Caps, Keyword-Patches) | Migration 068 angelegt; Smoke mit Gewaltschutz / ohne Fokus |
|
||||
| **S5** | (später) Auto-Fallback beim Speichern laut `KI_FEATURES_SPEC` §7 | Feature-Flag / Config |
|
||||
| **S6** | (später) Zielausbau, Anleitung-only, Varianten, Admin-Masse laut Vision | Separate Epics |
|
||||
|
||||
**Aktueller Implementierungsstand:** **S4 + S4b** im Code (`exercise_ai` + Formular übermittelt `focus_areas_context`).
|
||||
|
||||
---
|
||||
|
||||
## 3. Implementierungs-Checkliste (Technik)
|
||||
|
||||
- [ ] `OPENROUTER_API_KEY` / `OPENROUTER_MODEL` in `.env.example` dokumentiert (bereits teils vorhanden – prüfen).
|
||||
- [ ] Fehlerbilder: `400` zu wenig Inhalt, `503` KI nicht konfiguriert, `502` Upstream-Fehler mit kurzer Message.
|
||||
- [ ] Logging: **keine** vollständigen Prompts mit personenbezogenen Daten in Prod-Logs (optional DEBUG).
|
||||
- [ ] Optional: Rate-Limit KI-Endpunkte (`slowapi`) – nach Bedarf.
|
||||
- [ ] `MODULE_VERSIONS["exercises"]` / Changelog bei API-Erweiterung setzen.
|
||||
|
||||
---
|
||||
|
||||
## 4. Changelog dieses Plans
|
||||
|
||||
- **2026-05-22:** Initial; S1–S4 als erster Umsetzungspfad.
|
||||
- **2026-05-22:** S1–S4 im Code umgesetzt (Migration 067, `exercise_ai` + Router, Übungsformular); S5 weiter offen.
|
||||
- **2026-05-29:** **S4b:** Migration **068**, `ai_skill_retrieval_profiles`; suggest `focus_areas_context`; Frontend sendet gesetzte Fokusbereiche; Spec `working/AI_SKILL_RETRIEVAL_PROFILES_SPEC.md`.
|
||||
|
||||
---
|
||||
|
||||
## 5. Umsetzungsstand (Zwischencheckpoint)
|
||||
|
||||
**Erledigt (2026-05-22):** Migration **`067_ai_prompts_exercise_assistant`**, **`openrouter_chat`**, **`exercise_ai`**, **`POST /api/exercises/ai/suggest`** und **`POST /api/exercises/{id}/ai/regenerate`**, Formular-Schaltflächen (Kurzfassung / Fähigkeiten / kombiniert).
|
||||
|
||||
**Erledigt (2026-05-29):** Migration **`068`** / Profil **`ai_skill_retrieval_profiles`** (Standard + Profil Gewaltschutz wenn `focus_areas.name` vorhanden); **`exercise_ai`** — Score/Kategorie-Zapfen/Text-Overlap/Keyword-Zuschläge; **API:** `ExerciseAiSuggestBody.focus_areas_context`; **Regenerate** nutzt DB-Fokuszeilen.
|
||||
|
||||
**Nacharbeit S4 UX:** Übernahmedialog **`ExerciseFormPageRoot`**: keine sofortige Überschreibung; Kurzfassung mit Vergleich + Checkbox; Fähigkeiten mit Neu/Aktualisierung, Checkboxen, „Alle auswählen/abwählen“; **`Escape`** schließt; KI-Schaltflächen blockiert solange Dialog offen.
|
||||
|
||||
**Offen nächste Schritte Pflege/Umsetzung:** weitere Retrieval-Profile (z. B. Karate-/Fitness-Schwerpunkt) per SQL später Admin-UI; optionales Feld **`skills.ai_context`** Kurzbeschreibung für KI; automatische KI beim Speichern (**S5**); Prompt-/Profil-Admin-UI ohne SQL; Rate-Limits.
|
||||
|
||||
**Bewusst noch nicht (`summary_ai_generated`):** zurücksetzen bei manueller Kurzfassung im UI; Admin-Pflege `ai_skill_retrieval_profiles`.
|
||||
|
||||
124
.claude/docs/working/AI_PLANNING_KI_MULTISTAGE_FORECAST.md
Normal file
124
.claude/docs/working/AI_PLANNING_KI_MULTISTAGE_FORECAST.md
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
# Mehrstufige KI für Trainingsplanung – Architektur-Vorschau (Anti-Refactoring)
|
||||
|
||||
**Version:** 0.1
|
||||
**Datum:** 2026-05-22
|
||||
**Status:** Planungs-/Architektur-Arbeitspapier (keine Implementierungspflicht)
|
||||
**Ziel:** Für die **spätere** Planungs-KI bereits **Schnittstellen und Schichten** vorzeichnen, damit die **kleinere, starre** Übungs-KI nicht zur impliziten Vorlage für einen viel größeren Kopf wird — **ohne** jetzt eine Mitai-artige Workflow-Engine zu bauen.
|
||||
|
||||
**Update 2026-06-07:** Progressionsgraph startet **Phase F** (`planning_progression_roadmap.py`) — Roadmap-first, Workflow-lite. Siehe **`PLANNING_PROGRESSION_ROADMAP_SPEC.md`** und **`docs/architecture/PLANNING_KI_ROADMAP.md`**. Gruppenanalyse bleibt in der **Trainingsplanungs-Pipeline** (§3 S0–S4), nicht im Graphen.
|
||||
|
||||
**Bezüge:** `technical/AI_TRAINING_PLANNING_CONCEPT.md` · `functional/AI_EXERCISE_ASSISTANT_VISION.md` · `technical/SKILL_SCORING_SPEC.md` · `functional/TRAINING_CURRICULUM_AND_GOVERNANCE_CONCEPT.md` (CURR-003) · Schwesterprojekt Mitai: `c:/dev/mitai-jinkendo` (Referenz: `prompt_executor`, `placeholder_resolver`, `workflow_*` — **nicht** Pflicht-Port).
|
||||
|
||||
---
|
||||
|
||||
## 1. Zwei getrennte Produktlinien (bewusst entkoppelt)
|
||||
|
||||
| Linie | Rolle | Orchestrator |
|
||||
|--------|--------|----------------|
|
||||
| **Übungs-KI** | wenige Eingaben → Kurzfassung / Skills; **starrer** Ablauf (1–2 Calls), kleines Kontextfenster | z. B. `exercise_ai.py` (heute) |
|
||||
| **Planungs-KI** | Gruppe, Zeit, Ziele, Historie, Katalogausschnitt, Phasen/Streams → **strukturierte Planelemente** | **eigenes** Modul + **mehrstufig** (siehe §3) |
|
||||
|
||||
**Regel:** Shared Library nur auf **niedriger Ebene** (`openrouter_chat`-Art: HTTP, Timeouts, Modellname, Fehler-Mapping) und **gemeinsame Prompt-Tabelle** `ai_prompts`. **Keine** Vermischung der Geschäftslogik „Übung erstellen“ mit „Einheit füllen“, um später keine Abhängigkeiten reißen zu müssen.
|
||||
|
||||
---
|
||||
|
||||
## 2. Konzeptioneller „Planungs-Graph“ (Daten, nicht zwingend Graph-DB)
|
||||
|
||||
Für die Planungs-KI ist ein **Graph als Denkmodell** hilfreich — technisch reicht meist **PostgreSQL + bestehende FKs** (+ optional `exercise_progression_graphs`):
|
||||
|
||||
**Knoten-Typen (Auszug):** `training_groups`, `training_units`, `training_unit_sections` / Items, `exercises`, `skills`, `training_framework_programs` / Slots / Goals, ggf. Nachbearbeitungs-/Debrief-Metadaten.
|
||||
|
||||
**Kanten-Typen (Auszug):**
|
||||
|
||||
- **Zeitliche Folge:** Einheiten einer Gruppe nach `planned_date` / Reihenfolge
|
||||
- **Inhalt:** Section-Item → `exercise_id` (± Variante)
|
||||
- **Ziele:** Slot-/Framework-Ziele, Kopf-Notizen, Trainer-Zieltexte
|
||||
- **Progression:** Kanten aus `exercise_progression_graphs` (optional erweitern um „empfohlene Folge im Gruppenkontext“, bleibt Spekulationsfeld)
|
||||
- **Skills:** bereits über `exercise_skills`; aggregiert über `skill_scoring`-Pfad
|
||||
|
||||
**Wichtig:** Für KI **nicht** einen Riesen-Graphen serialisieren, sondern **Projektionen** („letzte *N* Einheiten“, „Nachbarn im Progressionsgraph zu zuletzt verwendeten Übungen“, „Skill-Gap Heuristik“).
|
||||
|
||||
---
|
||||
|
||||
## 3. Mehrstufiger Prozess (Pflichtidee für Planungs-KI)
|
||||
|
||||
Statt einem Prompt „mach den ganzen Plan“ mehrere **Schritte mit kleinen, validierbaren Outputs**:
|
||||
|
||||
| Stufe | Beispiel-Aufgabe | Deterministisch möglich? | Typischer LLM-Einsatz |
|
||||
|-------|-------------------|--------------------------|------------------------|
|
||||
| **S0** | Governance + Filter + Historie + Slot-Ziele zusammenstellen | Ja (SQL/API) | Nein |
|
||||
| **S1** | Kandidaten-Übungen auf Top‑K schrumpfen (Skills, Volltext, Score, Wiederholungsstrafe) | Teilweise | Optional Ranking |
|
||||
| **S2** | Reihenfolge je Section / Phase unter Constraints (Aufwärmen, Graphen-Nachbarn) | Teilweise | Ja (auf kleiner Liste) |
|
||||
| **S3** | Zeiten auf Section/Item vorschlagen oder Plausibilisieren | Teilweise | Ja |
|
||||
| **S4** | Trainer-sprachliche Kurzbegründung / Alternativen | Nein | Ja |
|
||||
|
||||
**Zwischen jeder Stufe:** starkes **Schema / Validierung** (z. B. nur erlaubte `exercise_id`s, nur erlaubte Slot-Struktur zu Phasen/Streams). So bleibt das System auch bei Modell-Fehlern stabil.
|
||||
|
||||
---
|
||||
|
||||
## 4. Schnittstellen-Vorsorge im Code (ohne Big-Bang)
|
||||
|
||||
Minimal-Ausbaustufe später, die Refactoring vermeidet:
|
||||
|
||||
1. **`PlanningContextPack` (internes DTO)** — reines Python-`dict`/`dataclass` oder Pydantic: aggregierte, **tokenbewusst gekürzte** Ansicht (Gruppe, nächste Einheit-Ziele, Historie-IDs, Top‑K-Kandidaten, Constraints).
|
||||
2. **`planning_ai_steps` als rein **funktionale** Pipeline** — jede Stufe `(context) → context` oder `(context) → partial_suggestion`; keine globale „Prompt-String-Bastelei“ überall im Router.
|
||||
3. **Prompt-Slugs pro Stufe** in `ai_prompts` (analog Übung), z. B. `planning_rank_section_items`, `planning_explain_sequence`, mit **eigenem** Platzhalter-Katalog (nicht `{{skills_catalog}}` aus Übungen recyclen).
|
||||
4. **Router** `training_planning.py` (oder neuer `planning_ai.py`): nur **dünne** HTTP-Schicht, ruft Orchestrator.
|
||||
|
||||
Optional **später**, wenn nötig: zweite Tabelle `ai_prompt_chains` oder externe Workflow-Definition — **erst** wenn 3–4 feste Stufen nicht mehr reichen. Mitai-Workflow-Engine dann **bewusste** Option, kein Default.
|
||||
|
||||
---
|
||||
|
||||
## 5. Kontextfenster und „Kaskade“
|
||||
|
||||
**Kerngedanke:** Je Stufe nur **neue** Information hinzufügen, die vorherige Stufen **ersetzen** oder **verdichten**, nicht duplizieren.
|
||||
|
||||
Beispiel:
|
||||
|
||||
- Stufe A (LLM oder Heuristik): „Priorisierte Skill-Ziele für diese Session“ (kurz)
|
||||
- Stufe B: Top‑40 Übungen mit **einer** Zeile pro Übung
|
||||
- Stufe C: Reihenfolge für 8 IDs + 2-Satz-Begründung
|
||||
|
||||
So bleibt dieselbe fachliche Tiefe erreichbar ohne Kontext-Explosion.
|
||||
|
||||
---
|
||||
|
||||
## 6. Schnittstellen zu bereits vorhandenen Bausteinen
|
||||
|
||||
- **`skill_profiles` / `skill-discovery`:** liefern **deterministische** Ziel-/Profil-Signale für S0/S1 (`SKILL_SCORING_SPEC.md`).
|
||||
- **`training_planning_prefs`:** weiche Constraints (Tone, Dauer, Split-Vorlieben).
|
||||
- **`exercise_progression_graphs`:** lokale Nachbarschaft um „zuletzt verwendet“.
|
||||
- **Mitai-Referenz:** Platzhalter-Katalog + Preview-API als **Inspiration** für Admin-UX; Workflow-Graph nur wenn Shinkan **wirklich** viele verzweigte Pipelines braucht.
|
||||
|
||||
---
|
||||
|
||||
## 7. Was wir **nicht** jetzt tun müssen
|
||||
|
||||
- Keine zweite Graph-Datenbank nur für KI.
|
||||
- Keine Workflow-UI-Kopie aus Mitai.
|
||||
- Keine Vereinheitlichung der Übungs-KI mit Planungs-KI über einen „Mega-Orchestrator“.
|
||||
|
||||
---
|
||||
|
||||
## 8. Kurz-Checkliste „Refactoring vermeiden“ vor erster Planungs-KI-Zeile Code
|
||||
|
||||
- [ ] Eigenes Modulbaum-„Root“ für Planung (nicht `exercise_ai` erweitern).
|
||||
- [ ] Prompt-Slugs mit **Planungs-**Präfix und **eigenem** Platzhalter-Set dokumentieren.
|
||||
- [ ] Outputs pro Stufe **JSON-Schema** oder Pydantic validieren.
|
||||
- [ ] Kandidatenlisten **immer** serverseitig auf erlaubte IDs begrenzen.
|
||||
|
||||
---
|
||||
|
||||
## 9. Progressionsgraph vs. Trainingsplanung (2026-06-07)
|
||||
|
||||
| Pipeline | Kontext | Orchestrator |
|
||||
|----------|---------|--------------|
|
||||
| **Progressionsgraph (F)** | Zieltext, N Steps, Semantic Brief | `planning_progression_roadmap.py` |
|
||||
| **Trainingsplanung (G, später)** | Gruppe, Historie, Rahmen, Zeit | `planning_ai_steps` + ggf. Mitai Workflow |
|
||||
|
||||
---
|
||||
|
||||
## 10. Changelog
|
||||
|
||||
- **2026-06-07:** Verweis Phase F Roadmap-first; Abgrenzung Graphen/Planung.
|
||||
- **2026-05-22:** Erstfassung als Vorschau-Dokument für mehrstufige Planungs-KI.
|
||||
121
.claude/docs/working/AI_SKILL_RETRIEVAL_PROFILES_SPEC.md
Normal file
121
.claude/docs/working/AI_SKILL_RETRIEVAL_PROFILES_SPEC.md
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
# KI Skill-Retrieval-Profile (`ai_skill_retrieval_profiles`)
|
||||
|
||||
**Version:** 0.1
|
||||
**Datum:** 2026-05-29
|
||||
**Status:** Umsetzung gestartet (Migration **068**)
|
||||
**Ziel:** Für `POST /api/exercises/ai/suggest` (Skill-Katalogauszug) **Gewichte und Quoten** steuerbar machen:
|
||||
|
||||
- gebunden an **Übungs-Fokusbereich** (`focus_areas.id`),
|
||||
- ein **Standardprofil** ohne Fokus,
|
||||
- **optional zusammengeführte** Profile bei mehreren Fokusbereichen,
|
||||
- **optional Keyword-Übersteuerungen** aus Ziel/Durchführung (z. B. Rollenspiel vs. Befreiung).
|
||||
|
||||
**Technische Basis:** Skills mit `skills.main_category_id` → `skill_main_categories.slug` (`karate` | `allgemeine`) und `skills.category_id` → `skill_categories.slug` (`kondition`, `selbstverteidigung`, …).
|
||||
|
||||
**Bezüge:** `.claude/docs/working/AI_EXERCISE_IMPLEMENTATION_PLAN.md` · `backend/exercise_ai.py`
|
||||
|
||||
---
|
||||
|
||||
## 1. Datenmodell
|
||||
|
||||
### Tabelle `ai_skill_retrieval_profiles`
|
||||
|
||||
| Spalte | Typ | Beschreibung |
|
||||
|--------|-----|--------------|
|
||||
| `id` | serial | Primärschlüssel |
|
||||
| `focus_area_id` | int NULL FK → `focus_areas(id)` ON DELETE SET NULL | **`NULL`** nur für Standardeintrag möglich (siehe `is_default`) |
|
||||
| `is_default` | boolean | Genau **eine** Zeile mit `true` |
|
||||
| `name` | varchar | Kurzer Name (Admin später) |
|
||||
| `description` | text | Hinweise für Pflege |
|
||||
| `active` | boolean | Nur aktive werden geladen |
|
||||
| `config` | jsonb | Siehe §2 |
|
||||
|
||||
**Constraints / Indizes**
|
||||
|
||||
- Eindeutig: `(focus_area_id)` WHERE `focus_area_id IS NOT NULL`
|
||||
- Eindeutig: `(is_default)` WHERE `is_default = true`
|
||||
|
||||
---
|
||||
|
||||
## 2. JSON-Konfiguration `config.version = 1`
|
||||
|
||||
Alle Schlüssel **optional**; fehlende Werte fallen auf **einprogrammierten Fallback** in `exercise_ai.py` zurück (entspricht bisher grob „neutral“).
|
||||
|
||||
### 2.1 Gewichtungen (Ranking)
|
||||
|
||||
| Schlüssel | Typ | Bedeutung |
|
||||
|-----------|-----|------------|
|
||||
| `main_slug_weights` | `object[str, float]` | Multiplikator pro Hauptkategorie-Slug (`karate`, `allgemeine`) |
|
||||
| `category_slug_weights` | `object[str, float]` | Multiplikator pro `skill_categories.slug` |
|
||||
|
||||
Basis-Score (vereinfacht):
|
||||
`(importance oder 3) × main_w × cat_w × text_overlap_bonus × importance_multiplier`
|
||||
|
||||
### 2.2 Kapazitätsbegrenzung (Liste)
|
||||
|
||||
`_MAX_SKILLS_CATALOG_LINES` (aktuell **240**) Zeilen Gesamt:
|
||||
|
||||
| Schlüssel | Typ | Bedeutung |
|
||||
|-----------|-----|------------|
|
||||
| `category_max_share` | `object[str, float]` | Max. Anteil dieser **Unterkategorie** am Endergebnis (0–1), z. B. `{ "kondition": 0.25 }` |
|
||||
| `main_min_share` | `object[str, float]` | Mindest-Zielanteil Hauptkategorie beim **Auswahl-Greedy** (weich; Rest nach Score aufgefüllt) |
|
||||
|
||||
### 2.3 Text / Token-Sparen
|
||||
|
||||
| Schlüssel | Typ | Standard | Bedeutung |
|
||||
|-----------|-----|----------|------------|
|
||||
| `description_plain_max_len` | int | 160 | Gekürzte Beschreibung pro Zeile |
|
||||
| `karate_relevance_max_len` | int | **0** oder 80 | **`0`** = Feld `karate_relevance`/`relevance_level` in der Promptzeile **weglassen** |
|
||||
|
||||
### 2.4 Keyword-Overrides (optional)
|
||||
|
||||
Liste `keyword_overrides`: jedes Element:
|
||||
|
||||
```json
|
||||
{
|
||||
"keywords_any": ["befreiung", "haltegriff"],
|
||||
"case_insensitive": true,
|
||||
"patch": {
|
||||
"category_slug_weights": { "selbstverteidigung": 2.5 },
|
||||
"category_max_share": { "koordination": 0.1 }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Textsuche in verkettetem Korpus **Titel, Ziel, Durchführung, Focus-Hint** (bereits plaintext). Reihenfolge: erst Basis-Profile zusammenmergen, dann **alle treffenden Overrides**‑`patch`‑Objekte **flach zusammenführen** (Gewichte multiplikativ übereinander, Caps den strengsten Wert nehmen – aktuelle Implementierung im Code dokumentiert).
|
||||
|
||||
---
|
||||
|
||||
## 3. Mehrere Fokusbereiche auf der Übung
|
||||
|
||||
Request-Body: `focus_areas_context: [{ "focus_area_id": n, "is_primary": bool }, …]`
|
||||
|
||||
**Aktuelle Merge-Strategie (v1):** Profile laden → **gleichgewichtete Mittelwert-Bildung** der numerischen Gewichte / Caps (implementiert für `main_slug_weights`, `category_slug_weights`, `category_max_share`, `main_min_share`, `*_max_len`). Anschließend **Keyword-Overrides** anwenden.
|
||||
|
||||
**Primär-Fokus:** Im Frontend soll die **primäre** Zeile aus `focus_areas_multi` **zuerst** in der Liste stehen; die Merge-Strategie kann später zu „Primär dominate“ erweitert werden.
|
||||
|
||||
Ohne Kontext oder ohne Treffer auf aktive Profile: **nur Standardprofil** (`is_default`).
|
||||
|
||||
---
|
||||
|
||||
## 4. Seed-Daten (Migration)
|
||||
|
||||
- **`is_default=true`:** ausgewogene Standard-Gewichte, moderate Caps auf `kondition`/`koordination`, Karate-Relevanz gekürzt.
|
||||
- **`Gewaltschutz`:** `focus_area_id` per `(SELECT id FROM focus_areas WHERE name = 'Gewaltschutz' LIMIT 1)` — höhere Gewichte für `kognition`, `psychische_faehigkeiten`, `soziale_faehigkeiten`, `selbstverteidigung`; gedrosseltes `kondition`/`koordination`; `karate_relevance_max_len`: 0; Keyword-Patches wie oben können nachgeschärft werden.
|
||||
|
||||
Weitere Profile (Karate-Schwerpunkt etc.) später per Admin-SQL oder UI.
|
||||
|
||||
---
|
||||
|
||||
## 5. API
|
||||
|
||||
`ExerciseAiSuggestBody` erweitert um **`focus_areas_context`** (Liste). Feld **`focus_area_hint`** bleibt für den **Prompt-Kontext** (bestehende Prompts).
|
||||
|
||||
`POST …/ai/regenerate` nutzt gespeicherte `exercise_focus_areas` zur gleichen Retrieval-Logik wie Suggest.
|
||||
|
||||
**Pflege der Profile:** Superadmin ohne Mandantenwahl — **`GET|POST /api/admin/ai-skill-retrieval-profiles`**, **`GET|PUT|DELETE /api/admin/ai-skill-retrieval-profiles/{id}`** (`routers/ai_skill_retrieval_admin.py`); Web-UI Superadmin unter **`/admin/ai-skill-retrieval`**.
|
||||
|
||||
## 6. Changelog
|
||||
|
||||
- **2026-05-29:** Superadmin-Pflege-Endpoints + UI‑Route dokumentiert (`/admin/ai-skill-retrieval`).
|
||||
- **2026-05-29:** Erstellt; gekoppelt an Migration **068** und erste `exercise_ai`-Integration.
|
||||
106
.claude/docs/working/COMBINATION_TIMING_PROFILE_PLAN.md
Normal file
106
.claude/docs/working/COMBINATION_TIMING_PROFILE_PLAN.md
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
# Kombinations‑Ablaufprofil — Zeitmodell, Archetyp‑Vorgaben, Umsetzung
|
||||
|
||||
**Zweck:** Fach-/Technik-Brücke zwischen Wunschbild („kein Nutzer‑JSON“, globale und slotbezogene Eckwerte, Archetyp‑Strukturen) und bestehendem Speicher **`method_profile` (JSON)** + **`planning_method_profile`** auf Planungszeilen.
|
||||
|
||||
**Bezüge:** `.claude/docs/functional/Shinkan Trainingsmodule Kombinationsuebungen Spezifikation V2.md` (§ 6.3 / § 8.3); Frontend `CombinationMethodProfileEditor`, `combinationMethodProfileUi.js`; Archetyp‑IDs siehe Backend `COMBINATION_ARCHETYPE_IDS` / Frontend `COMBINATION_ARCHETYPE_OPTIONS`.
|
||||
|
||||
---
|
||||
|
||||
## 1. Grundprinzipien
|
||||
|
||||
| Prinzip | Beschreibung |
|
||||
|--------|--------------|
|
||||
| **Kein Pflicht‑JSON für Trainer** | Alle trainertypischen Pflegepfade nur über geführte Felder + Archetyp‑Vorschlagsknöpfe. |
|
||||
| **JSON bleibt Transport** | Persistenz geschieht weiter in `method_profile` / Kopie in `planning_method_profile`; **kanonische Schlüssel** werden hier und in Codekommentaren festgehalten. |
|
||||
| **Archetyp = Struktur + Defaults** | Wechsel des Archetyps soll (optional/togglebar) Grundwerte oder typische Relationen vorbelegen können — keine stillen Überschreibungen ohne Hinweis. |
|
||||
| **`free_method_block` = Maximale Freiheit** | Entspricht „maximaler Konfiguration“: alle relevanten Timing‑Dimensionen über UI, insbesondere **pro Slot**; keine impliziten stationären Constraints. |
|
||||
|
||||
---
|
||||
|
||||
## 2. Kanonisches Zeit‑Schema (`timing_schema`)
|
||||
|
||||
**Empfohlene Versionierung im Objekt:**
|
||||
|
||||
- **`timing_schema: 1`** — sobald neue globale/strukturierte Felder aktiv genutzt werden (Pilot; UI kann ohne Migration starten durch parallele Schlüssel).
|
||||
|
||||
### 2.1 Globalebene (`method_profile`)
|
||||
|
||||
| Feld (Pilot) | Semantik |
|
||||
|----------------|----------|
|
||||
| `timing_schema` | `1` wenn Block unten aktiv |
|
||||
| `intro_sec` oder bestehend `block_intro_sec` | einmalige Einführung/Demo am Block |
|
||||
| `rounds` (bzw. bei Intervallen `interval_rounds` — Angleich später) | komplette Durchläufe des Musters |
|
||||
| *Planned totals* nur **berechnete Anzeige** in UI, optional persistiert z. B. `planned_total_duration_min_hint` später |
|
||||
|
||||
Relationen **Zwischen Arbeit und Pause** können als Schnellwahl gesetzt werden (kein eigener Persist‑Erzwing‑Typ nötig), indem konkrete Sekunden geschrieben werden.
|
||||
|
||||
### 2.2 Slots (`slot_profiles_v1`)
|
||||
|
||||
Array synchron zu `slot_index`; fehlende Einträge = „nicht gefüllt / aus globalen Eckdaten ableiten wo sinnvoll“.
|
||||
|
||||
Objekt‑Shape (Sekunden, ganze Zahlen ≥ 0):
|
||||
|
||||
```json
|
||||
{
|
||||
"slot_index": 0,
|
||||
"load_sec": 40,
|
||||
"consecutive_reps": 1,
|
||||
"intra_rep_rest_sec": 10,
|
||||
"transition_after_sec": 15
|
||||
}
|
||||
```
|
||||
|
||||
| Feld | Bedeutung |
|
||||
|------|------------|
|
||||
| `load_sec` | Belastungsdauer „an der Station“. |
|
||||
| `consecutive_reps` | Wiederholungen pro „Serie“ bzw. ohne Wechsel zu **neuem** Stationsinhalt (oft 1). |
|
||||
| `rep_series_count` | Anzahl Serien à `consecutive_reps` bei rep/manual; Standard **1**, Archetyp‑Vorgabe möglich (**`ARCHETYPE_DEFAULT_REP_SERIES_COUNT`**). Persistiert für rep/manual ab 1. |
|
||||
| `intra_rep_rest_sec` | Pause zwischen den Folge‑Wiederholungen bzw. **zwischen Serien** (nur sinnvoll, wenn `rep_series_count` ≥ 2 im Modus `rep`/`manual`; sonst Wechselzeit `transition_after_sec` nutzen). |
|
||||
| `transition_after_sec` | Pause / Wechsel **zur nächsten** Station oder zum nächsten logischen Block. |
|
||||
|
||||
**Hinweis:** Bestehende Archetyp‑„flachen“ Schlüssel (`work_seconds`, `transition_seconds`, …) werden schrittweise **nicht zerstört**, sondern Slots ergänzen; Konvergenz (eine Darstellung zu v1) kann Phase 4 sein.
|
||||
|
||||
---
|
||||
|
||||
## 3. Archetyp → typische Schnellwahl (Überblicks‑Matrix)
|
||||
|
||||
| Archetyp | Globale Schnellwahl (Beispiele) | Slots |
|
||||
|----------|---------------------------------|-------|
|
||||
| `circuit_rotate_time` | Arbeit; Rotation „≈ Arbeit“ oder „Pause 2/3 Arbeit“ bezogen auf Rund‑Pausen/Rotation wo im UI dokumentiert | sinnvoll ab **timing_schema** geführt |
|
||||
| `time_domain_interval` | Pause = Arbeit; Pause = 2/3 Arbeit (auf `rest_seconds`↔`work_seconds`) | optional |
|
||||
| `sequence_linear` | Einführung + grobe Sek./Station | **slot_profiles_v1** priorisiert |
|
||||
| `circuit_all_parallel` | Erklärzeit, gemeinsamer Start | Slots optional |
|
||||
| `pair_superset` | Wechsel A↔B, Arbeit je Seite (+ später erweiterbar) | 2‑Slot‑Fokus |
|
||||
| `free_method_block` | alle globalen Slots optional | **Pfad für maximale Flex** |
|
||||
| `station_parcour` | Reihenfolge frei‑Flag bestehend | pro Station Verweilen sinnvoll |
|
||||
|
||||
**Pyramidal (später):** neue Archetyp‑ID **`pyramid_interval`** o. ä. oder Flag `pyramid_recovery_rule` mit Regelwerk „Pause abhängig von letzter Belastung“ — **explizit out of scope** bis Regeln feststehen.
|
||||
|
||||
---
|
||||
|
||||
## 4. UX‑Normen
|
||||
|
||||
- **Trainingsplanung** (`plannerMode`): **keine** Roh‑JSON‑Oberfläche.
|
||||
- **Übungsformular**: Roh‑JSON nur wenn `allowExpertJson === true` (Default false; später z. B. Superadmin/Dev).
|
||||
- **Coaching‑Ansicht**: nur **wirksame** Zahlen aus Snapshot/Katalog (Merge wie in `comboPlanningMethodProfile.js`); **globale** Profilwerte mit **fachlichen Labels** (`describeGlobalComboProfile`), nicht nur Rohschlüsseln.
|
||||
|
||||
### 4.1 Stand Umsetzung (App **0.8.110**, Kurz)
|
||||
|
||||
- **`slot_profiles_v1`** und Schnellwahlen Zirkel/Intervall im geführten Editor umgesetzt; **`advance_mode`** je Slot (Zeit / Ziel‑Wdh. / Coach).
|
||||
- **Phase 2** dieses Plans (Modal „Archetyp‑Vorlage anwenden?“, nicht‑destruktives Merge über alle Slots) — **noch offen** (Fachspez § 10.6, Umsetzungsplan Paket **4f**).
|
||||
|
||||
---
|
||||
|
||||
## 5. Phasen (Implementierung)
|
||||
|
||||
| Phase | Inhalt |
|
||||
|-------|--------|
|
||||
| **1 (jetzt)** | Slot‑Zeilen‑UI über `slot_profiles_v1`; Schnellwahl‑Ratios für `circuit_rotate_time` + `time_domain_interval`; `plannerMode` ohne JSON; `allowExpertJson` default false |
|
||||
| **2** | Beim Archetypwechsel **optionales** Modal „Archetyp‑Vorlage anwenden?“ mit nicht‑destruktivem Merge |
|
||||
| **3** | Geplante **Gesamtzeit** konsistent rechnerisch (Summe Slots × Runden + Global) mit Transparenz in UI |
|
||||
| **4** | Konsolidierung flacher Schlüssel → **`timing_schema`** v1‑only im Editor |
|
||||
| **5** | Pyramide / adaptive Recovery |
|
||||
|
||||
---
|
||||
|
||||
**Pflege:** Änderungen an Schlüsseln oder Phasen hier und in Anhang A der Fachspez mitziehen.
|
||||
68
.claude/docs/working/EXERCISE_ENRICHMENT_ADMIN.md
Normal file
68
.claude/docs/working/EXERCISE_ENRICHMENT_ADMIN.md
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
# Superadmin: Übungs-Anreicherung per KI
|
||||
|
||||
Stand: 2026-05-23 · App 0.8.178
|
||||
|
||||
## Zweck
|
||||
|
||||
Plattform-weites Werkzeug für Superadmins, um Übungen (typisch `draft`, ohne Skills) **batchweise** per KI mit Fähigkeiten anzureichern und kontrolliert auf `in_review` zu setzen.
|
||||
|
||||
Verbessert indirekt die Planungs-KI (`POST /api/planning/exercise-suggest`), die gegen Skill-Profile rankt — unvollständige `exercise_skills` führen dort zu Volltext-dominiertem Ranking.
|
||||
|
||||
## UI
|
||||
|
||||
- Route: `/admin/exercise-enrichment` (nur Superadmin)
|
||||
- Admin-Menü: „Übungs-Anreicherung“
|
||||
|
||||
## API
|
||||
|
||||
Prefix: `/api/admin/exercise-enrichment`
|
||||
|
||||
| Methode | Pfad | Beschreibung |
|
||||
|---------|------|--------------|
|
||||
| GET | `/candidates` | Paginierte Kandidaten (Filter: status, visibility, focus_area, without_skills, with_ai_suggested_skills, include_club, search) |
|
||||
| POST | `/preview` | Dry-Run — `{ exercise_ids[], modes: { skills, summary }, merge_mode }` |
|
||||
| POST | `/apply` | `{ items: [{ exercise_id, merged_skills }], merge_mode, set_status }` |
|
||||
|
||||
Auth: `require_auth` + `is_superadmin` — **kein** `TenantContext` (EXEMPT, siehe ACCESS_LAYER_ENDPOINT_AUDIT.md).
|
||||
|
||||
## KI
|
||||
|
||||
Wiederverwendet `run_exercise_form_ai_suggestion` → Prompts `exercise_skill_suggestions` (MVP Pflicht), optional `exercise_summary`. Skill-Katalog via `build_contextual_skills_catalog_block` / `ai_skill_retrieval_profiles`.
|
||||
|
||||
## Merge-Modi (Skills)
|
||||
|
||||
- `additive` (Default): manuelle Skills bleiben; KI ergänzt neue; bestehende `ai_suggested`-Links werden aktualisiert
|
||||
- `replace_ai_only`: nur `ai_suggested=true` entfernen, dann KI-Set anwenden
|
||||
- `replace_all`: alle Skills ersetzen (explizit)
|
||||
|
||||
## Defaults
|
||||
|
||||
- Kandidaten: **Status** primär (Default `draft`); Sichtbarkeit Default **`private`**, wählbar bis „Alle“
|
||||
- Skill-Merge Default: **`replace_all`** (alle Skills KI-neu, `ai_suggested=true` — unterscheidbar von manuell)
|
||||
- Nach Apply: `set_status=in_review` (nie automatisch `approved`)
|
||||
- Batch: keine Gesamtgrenze (bis 10.000 IDs); **Analyze** + explizite Nutzerbestätigung
|
||||
- **Preview:** max. **3 Übungen/HTTP-Request** (parallel LLM), Frontend chunked — vermeidet Gateway-504 (~60s Fritz!Box)
|
||||
- **Apply:** HTTP-Chunks à 25 (nur DB, kein LLM)
|
||||
|
||||
## Inhalte (modular)
|
||||
|
||||
| Modus | Prompt | Apply-Felder |
|
||||
|-------|--------|--------------|
|
||||
| Skills | `exercise_skill_suggestions` | `exercise_skills` inkl. Intensität, required/target_level, `ai_suggested` |
|
||||
| Summary | `exercise_summary` | `summary`, `summary_ai_generated=true` |
|
||||
| Anleitung | `exercise_instruction_rewrite` | `goal`, `execution`, `preparation`, `trainer_notes` |
|
||||
|
||||
## API (ergänzt)
|
||||
|
||||
| Methode | Pfad | Beschreibung |
|
||||
|---------|------|--------------|
|
||||
| GET | `/candidate-ids` | Alle IDs zum Filter (Select-all) |
|
||||
| POST | `/analyze` | `{ exercise_ids[], modes }` → Kosten-Schätzung vor Start |
|
||||
|
||||
## Keine Migration
|
||||
|
||||
Bestehende Spalte `exercise_skills.ai_suggested` reicht; kein Enrichment-Log in MVP.
|
||||
|
||||
## Tests
|
||||
|
||||
`backend/tests/test_exercise_enrichment_admin.py` — 403, Merge-Logik, Status draft→in_review.
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
# Rahmenprogramm: Filter, Dauer, Fähigkeiten-Schwerpunkte (Roadmap)
|
||||
|
||||
**Stand:** 2026-05-20
|
||||
**Status:** Phase 1 umgesetzt; Phase 3 v1.0 umgesetzt (regelbasiert); Phase 2 teilweise offen
|
||||
|
||||
## Phase 1 (umgesetzt)
|
||||
|
||||
### Listen-Anzeige Session-Dauer
|
||||
|
||||
- **GET `/api/training-framework-programs`:** `session_duration_min`, `session_duration_max` (aus Blueprint-`training_units.planned_duration_min`), `goal_titles_agg`, ID-Arrays für Katalog-M:N.
|
||||
- **UI:** Rahmenprogramm-Liste, Trainingsplanung (Einheiten-Liste/Kalender), Import-Dialog (Programm + pro Slot).
|
||||
|
||||
### Import-Filter (clientseitig)
|
||||
|
||||
- Textsuche (Titel, Beschreibung, Ziele, Katalog-Namen)
|
||||
- Fokusbereich, Trainingsart, Zielgruppe (Checkboxen, Katalog-API)
|
||||
- Ziel-Session-Dauer in Minuten (±10 Min Toleranz gegen Min/Max der Slots)
|
||||
|
||||
**Grenze:** Entwicklungsziele sind **freie Texte** pro Rahmen (`training_framework_goals.title`), keine kontrollierte Taxonomie → Filter nur Volltext, keine homogene „Ziel-Tags“-Liste.
|
||||
|
||||
## Phase 2 (empfohlen, ohne KI)
|
||||
|
||||
| Kriterium | Datenquelle heute | Verbesserung |
|
||||
|-----------|-------------------|--------------|
|
||||
| Fokusbereich / Stil / Trainingsart / Zielgruppe | M:N am Rahmenkopf | bereits filterbar |
|
||||
| Entwicklungsziele | Freitext-Ziele | Optional: Ziel-Vorlagen-Katalog oder Tags (Migration) |
|
||||
| Session-Dauer | `planned_duration_min` pro Slot | erledigt |
|
||||
| Fähigkeiten-Schwerpunkt | noch nicht | siehe Phase 3 |
|
||||
|
||||
**API-Erweiterung (optional):** `GET /api/training-framework-programs?focus_area_id=&training_type_id=&duration_min=` serverseitig — sinnvoll ab >50 Rahmen in der Bibliothek.
|
||||
|
||||
## Phase 3 — Fähigkeiten aus Übungen (umgesetzt v1.0)
|
||||
|
||||
**Spec:** `.claude/docs/technical/SKILL_SCORING_SPEC.md`
|
||||
|
||||
- Gewichtetes Profil: Rahmenprogramm (gesamt + pro Slot), Trainingsmodul, Progressionsgraph
|
||||
- `GET /api/skill-discovery/suggestions?skill_ids=…` für Bibliotheks-Vorschläge
|
||||
- UI: Profil-Panels in Editoren + Tab „Planungs-Vorschläge“ auf der Fähigkeiten-Seite
|
||||
- **Kein** automatisches Überschreiben der Stammdaten-Fokusbereiche
|
||||
|
||||
### Variante B — KI-Zusammenfassung (OpenRouter, optional, offen)
|
||||
|
||||
1. Input: Titel Rahmen, Ziele (Text), Liste Übungstitel + Dauer + vorhandene Skill-Namen.
|
||||
2. Prompt: strukturiertes JSON (`suggested_focus_areas[]`, `skill_emphasis[]`, `rationale_de`).
|
||||
3. Speichern als `ai_context_summary` (Version, Modell, Timestamp) — **nur Vorschlag**, manuelle Bestätigung vor Übernahme in Stammdaten.
|
||||
|
||||
**Vorteil:** natürliche Schwerpunkte auch bei unvollständigen Skill-Links.
|
||||
**Risiko:** Halluzination, Kosten, Datenschutz (Vereinsdaten in Prompt).
|
||||
|
||||
### Empfehlung
|
||||
|
||||
Zuerst **Variante A** für Listen/Filter und Abgleich mit manuell gesetzten Fokusbereichen; KI nur als **„Vorschlag generieren“-Button** im Rahmen-Editor, wenn Regelwerk und Katalog-Zuordnung zu dünn sind.
|
||||
|
||||
## Offene Produktfragen
|
||||
|
||||
1. Soll Filter **UND** (alle Kriterien) oder **ODER** (mindestens eines) sein? — Import aktuell **UND**.
|
||||
2. Rahmen mit **unterschiedlichen** Slot-Dauern: Liste zeigt Min–Max; Filter „90 Min“ trifft Range.
|
||||
3. Sollen homogenisierte **Entwicklungsziel-Tags** ein eigener Katalog werden (Admin), analog `target_groups`?
|
||||
|
|
@ -0,0 +1,125 @@
|
|||
# Parallele Trainingsstreams — Ist-Analyse und risikoarmer Umsetzungsplan
|
||||
|
||||
**Status:** Stufe A (Analyse/Plan, ohne produktive Umsetzung in jener Session)
|
||||
**Stand:** 2026-05-14
|
||||
**Verbindliche fachliche Basis:** `.claude/docs/functional/PARALLEL_TRAINING_STREAMS_CONCEPT.md`, `.claude/docs/technical/PARALLEL_TRAINING_STREAMS_SPEC.md`
|
||||
|
||||
Dieses Dokument **persistiert** die strukturierte Prüfung der realen Codebasis (`training_planning.py`, `training_framework_programs.py`, `training_unit_sections`/`items`, Frontend Planung/Run/Coach) und den empfohlenen Implementierungspfad.
|
||||
|
||||
---
|
||||
|
||||
## 1. Zusammenfassung
|
||||
|
||||
- Plan-Inhalt pro Einheit ist heute **eine flache Liste** `training_unit_sections` mit **`UNIQUE (training_unit_id, order_index)`** (Migration 031) und `training_unit_section_items`; zentral: **`_fetch_sections`**, **`_replace_unit_sections`**, **`_hydrate_training_unit_payload`** in `backend/routers/training_planning.py`.
|
||||
- Parallele Phasen/Streams **passen** zu den Produktregeln (ein Kalendertermin, N Streams, je Miniplan), sind im Schema aber **nicht** abbildbar ohne Erweiterung und **ohne Auflösung** des globalen `order_index`-Modells.
|
||||
- **Empfehlung:** **Normalisierte** Tabellen `training_unit_phases`, `training_unit_parallel_streams`, erweiterte `training_unit_sections` mit FK auf Phase bzw. Stream, **partielle Unique-Indizes** statt `UNIQUE (training_unit_id, order_index)` für alle Sektionen.
|
||||
- **Blocker im Code:** u. a. `POST /api/training-units/{id}/apply-training-module` mit **`section_order_index` global pro Einheit** (`_resolve_training_unit_section_id`).
|
||||
- **Nicht persistiert an anderer Stelle:** Erste Fassung existierte nur als Chat-Antwort; dieses File ist die **kanonische** Arbeitskopie im Repo.
|
||||
|
||||
---
|
||||
|
||||
## 2. Ist-Analyse (kurz)
|
||||
|
||||
### Datenbank
|
||||
|
||||
- `training_unit_sections`: u. a. `training_unit_id`, `order_index`, `UNIQUE (training_unit_id, order_index)`.
|
||||
- `training_unit_section_items`: Übung/Notiz, `planning_method_profile` (Kombi), `source_training_module_id`.
|
||||
|
||||
### Backend (`training_planning.py`)
|
||||
|
||||
- `_replace_unit_sections`: DELETE aller Sektionen der Einheit + INSERT (vollständiger Ersetzungsbaum).
|
||||
- `_sections_clone_payload` + `_copy_blueprint_into_scheduled_unit`: tiefe Kopie für `from-framework-slot`.
|
||||
- `_flatten_exercises_from_sections`: flaches `exercises` am Unit-Payload.
|
||||
- `apply_training_module_to_training_unit`: Sektion per **`section_order_index`** global.
|
||||
|
||||
### Rahmen (`training_framework_programs.py`)
|
||||
|
||||
- Blueprint-`training_units` pro Slot; gleiche `_replace_unit_sections`-Semantik.
|
||||
|
||||
### Frontend
|
||||
|
||||
- Planung: `TrainingPlanningPageRoot.jsx`, `TrainingUnitSectionsEditor`, `buildSectionsPayload` / `normalizeUnitToForm`.
|
||||
- Run: `TrainingUnitRunPage.jsx` — Fortschritt `sessionStorage` Key `sj_training_run_checked_${unitId}`.
|
||||
- Coach: `TrainingCoachPage.jsx` — `flattenPlanTimeline` (linearer Ablauf).
|
||||
|
||||
### Tests
|
||||
|
||||
- Kaum Abdeckung für Plan-Inhalt; vorhanden u. a. `test_training_unit_assignments.py` (Merge Co-Trainer, ohne DB), `test_training_units_list_keyset.py` (Keyset-Validierung).
|
||||
|
||||
---
|
||||
|
||||
## 3. Technische Optionen und Empfehlung
|
||||
|
||||
| Option | Kurz |
|
||||
|--------|------|
|
||||
| A JSONB nur auf `training_units` | Niedriges DDL-Risiko, hohes Drift-/Wartungsrisiko — **nicht empfohlen** |
|
||||
| B Normalisiert Phasen/Streams | **Empfohlen** — eine Wahrheit, saubere Kopie, Rahmen kompatibel |
|
||||
| C Nur UI-Konvention ohne DB | Widerspricht Produkt — **abgelehnt** |
|
||||
|
||||
---
|
||||
|
||||
## 4. Migrations- und Kompatibilitätsstrategie
|
||||
|
||||
- Default **`whole_group`‑Phase** für alle bestehenden Einheiten; alle bisherigen Sektionen erhalten `phase_id`.
|
||||
- Unique-Regel: **pro Phase** bzw. **pro Stream** `order_index` eindeutig (partielle Unique-Indizes).
|
||||
- API optional: zusätzlich abgeleitetes flaches `sections` für Übergang — Entscheidung je nach Consumer (praktisch nur dieses Frontend).
|
||||
|
||||
---
|
||||
|
||||
## 5. API- / Frontend-Hotspots
|
||||
|
||||
- `GET`/`PUT` `/api/training-units/{id}`: verschachtelte `phases` / `streams` / `sections` / `items`.
|
||||
- `POST .../apply-training-module`: Kontext **Phase/Stream + Sektionsindex im Träger**.
|
||||
- Run/Coach: stream-spezifischer Fortschritt; `flattenPlanTimeline` phase-aware oder pro Stream.
|
||||
|
||||
---
|
||||
|
||||
## 6. Implementierungspakete (Überblick)
|
||||
|
||||
0. Spike DDL + Contract-Doku
|
||||
1. **Erledigt (2026-05-14):** Migration **063** + `training_planning`: Phasen/Streams-Schema, Backfill whole_group, `GET` mit `phases`, Legacy-`sections`-PUT unverändert (eine whole_group-Phase).
|
||||
2. PUT mit echten Parallelphasen / Streams, `apply-training-module` mit Stream-Kontext, `from-framework-slot`-Kopie
|
||||
3. Planung UI
|
||||
4. Run + Coach
|
||||
5. Co-Trainer pro Stream
|
||||
6. MVP+ (Duplizieren, Verschieben, „nur meine Spur“)
|
||||
|
||||
---
|
||||
|
||||
## 7. Risiken
|
||||
|
||||
- Migration Unique-Constraint / bestehende Daten.
|
||||
- Regression Run/Coach / Dashboard-Joins (meist unkritisch, solange `training_unit_id` auf Sektionen bleibt).
|
||||
- Rahmen-Blueprints: gleiche Struktur wie Kalender-Einheiten anstreben (oder bewusst zweite Phase nur Kalender).
|
||||
|
||||
---
|
||||
|
||||
## 8. Offene Produkt-/Technikfragen
|
||||
|
||||
- Rahmen-Blueprint parallel im MVP oder erst nach Kalender-Einheit?
|
||||
- Semantik `exercises`-Flatlist bei Parallelität.
|
||||
- Merge-Regel `assistant_trainer_profile_ids` Kopf vs. Stream-Zuweisungen.
|
||||
|
||||
---
|
||||
|
||||
## 9. Verweise
|
||||
|
||||
- Fachkonzept: `.claude/docs/functional/PARALLEL_TRAINING_STREAMS_CONCEPT.md`
|
||||
- Technische Spec (Entwurf): `.claude/docs/technical/PARALLEL_TRAINING_STREAMS_SPEC.md`
|
||||
- Domänenüberblick: `.claude/docs/functional/DOMAIN_MODEL.md` (Abschnitt Parallele Streams)
|
||||
- `./PARALLEL_TRAINING_STREAMS_PREREQ_PROMPT.md` — **Prompt** für Folgesession (Performance/Wartung/Vorbereitung)
|
||||
|
||||
---
|
||||
|
||||
## 10. Vorbereitende Arbeiten (Session 2026-05-13)
|
||||
|
||||
Ohne produktives Parallel-Feature, nur Risikoabbau und Transparenz:
|
||||
|
||||
- **`training_planning.py`:** Lesepfad `_fetch_sections` in SQL-Konstanten + `_fetch_section_items_for_section` / `_hydrate_section_item_combination_slots` strukturiert; `_replace_unit_sections` delegiert an `_insert_one_replacement_section`; `_hydrate_training_unit_payload` dokumentiert.
|
||||
- **Tests:** `tests/test_training_planning_sections_pure.py` (flatten, ohne DB); `tests/test_training_planning_sections_integration.py` (Roundtrip replace↔fetch bei `TRAINING_PLANNING_INTEGRATION=1`).
|
||||
- **Frontend:** Kurzkommentare an Planung (`TrainingPlanningPageRoot`, `buildSectionsPayload`), Run, Coach, `flattenPlanTimeline` — Anbindungspunkte für spätere Phase/Stream-Logik.
|
||||
- **DOMAIN_MODEL:** UNIQUE-Hinweis und „keine Migration ohne Freigabe“.
|
||||
|
||||
**Empfohlene nächste Schritte:** Pakete **0** (DDL/Contract festzurren) und **1** (Schema + Migration + hydrate/replace laut Plan Abschnitt 4–6) in einer dedizierten Feature-Session; danach Paket **2** (PUT/Module/Clone).
|
||||
|
||||
---
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
# Prompt: Vorbereitungs- / Vorarbeit-Session (Performance & Wartung) für „Parallele Trainingsstreams“
|
||||
|
||||
**Kontext:** Du arbeitest in **Shinkan Jinkendo** (`c:\Dev\shinkan-jinkendo`). Das Feature **Parallele Trainingsstreams / Breakout** ist **inhaltlich** spezifiziert; eine **Ist-Analyse und ein risikoarmer Umsetzungsplan** liegen **persistiert** in:
|
||||
|
||||
- `.claude/docs/working/PARALLEL_TRAINING_STREAMS_ANALYSIS_AND_IMPLEMENTATION_PLAN.md`
|
||||
- Fachlich: `.claude/docs/functional/PARALLEL_TRAINING_STREAMS_CONCEPT.md`
|
||||
- Technik-Entwurf: `.claude/docs/technical/PARALLEL_TRAINING_STREAMS_SPEC.md`
|
||||
|
||||
**Deine Rolle:** Du hast bereits **Refaktorierungs- und Wartungsaufgaben** mit Fokus **Performance, Lesbarkeit und Testbarkeit** durchgeführt. In **dieser** Session geht es **nicht** darum, das komplette Parallel-Feature zu bauen, sondern um **Vorarbeiten („Prerequisites“)**, die die geplante Komplexitätsauflösung **sicherer und billiger** machen.
|
||||
|
||||
## Ziele
|
||||
|
||||
1. **Lesepfad Planung vereinheitlichen:** `backend/routers/training_planning.py` ist zentral für `_fetch_sections`, `_replace_unit_sections`, `_hydrate_training_unit_payload`, Klonen, Blueprint-Kopie, `apply-training-module`. Prüfe, ob klar abgegrenzte Hilfsfunktionen (ohne Verhaltensänderung) die **nächste** große Änderung erleichtern — **keine** Feature-Logik für Phasen/Streams hinzufügen, nur Struktur/Tests/Docs wenn nötig.
|
||||
|
||||
2. **Test-Lücken schließen (minimal, hoher Nutzen):** Heute fehlen **DB/API-Tests** für kritische Pfade (`_replace_unit_sections` Roundtrip, `from-framework-slot` Struktur-Kopie, optional `apply-training-module`). Ergänze **kleine, deterministische** Tests (pytest mit DB, falls im Projekt üblich), ohne riesige Fixtures.
|
||||
|
||||
3. **Frontend-Schneidstellen markieren:** kurze Kommentare oder ein **Working-Doc-Update**, wo `TrainingPlanningPageRoot`, `buildSectionsPayload`, `TrainingUnitRunPage`, `TrainingCoachPage` + `trainingPlanUtils.flattenPlanTimeline` später angebunden werden — **kein** großes UI-Rewrite.
|
||||
|
||||
4. **Migrations-Sicherheit:** Dokumentiere in **einem Absatz** im `ANALYSIS`-Dokument oder hier, welche **Unique-Constraints** (`training_unit_sections`: `UNIQUE (training_unit_id, order_index)`) die Parallelität blockieren — **ohne** sie schon zu ändern, außer es ist Teil einer **explizit** freigegebenen ersten Migration.
|
||||
|
||||
5. **Performance nur berührensensible Stellen:** Einzelabruf `GET /api/training-units/{id}` wird mit mehr JOINs kommen. Falls du **jetzt** N+1 oder redundante Arbeit in `_fetch_sections` siehst und das **risikoarm** verbesserbar ist, nur mit **Messpunkt/Messvorstellung** (kein unnötiger Micro-Optimismus).
|
||||
|
||||
## Leitplanken
|
||||
|
||||
- **Stabilität vor Geschwindigkeit:** Keine Änderung, die bestehende Einheiten, Rahmen-Blueprints oder Run-Modus bricht.
|
||||
- **Keine pauschalen Refactors:** Nur Änderungen mit **klarem** Träger für das Parallel-Feature oder mit **Test-Regression-Schutz**.
|
||||
- **Regeln:** `.claude/rules/ARCHITECTURE.md`, `CODING_RULES.md`, Zugriffsschicht wo relevant.
|
||||
|
||||
## Erwartete Ausgabe
|
||||
|
||||
1. Kurze **Liste erledigter Vorarbeiten** (Dateien, was warum).
|
||||
2. **Empfohlene Reihenfolge** für die **nächste** Session, die Phasen/Streams **implementiert** (verweis auf `PARALLEL_TRAINING_STREAMS_ANALYSIS_AND_IMPLEMENTATION_PLAN.md` Pakete 0–2).
|
||||
3. Falls nichts Sinnvolles ohne Feature-Branch riskiert werden kann: **explizit** „keine Code-Änderung“, nur Risiko-Notiz.
|
||||
|
||||
## Optionaler Startbefehl
|
||||
|
||||
```
|
||||
Lies zuerst:
|
||||
.claude/docs/working/PARALLEL_TRAINING_STREAMS_ANALYSIS_AND_IMPLEMENTATION_PLAN.md
|
||||
dann backend/routers/training_planning.py (Abschnitte um _fetch_sections, _replace_unit_sections).
|
||||
```
|
||||
529
.claude/docs/working/PLANNING_EXERCISE_SUGGEST_CONTEXT.md
Normal file
529
.claude/docs/working/PLANNING_EXERCISE_SUGGEST_CONTEXT.md
Normal file
|
|
@ -0,0 +1,529 @@
|
|||
# Planungs-KI: Übungssuche & Kontext für Neu-Anlage
|
||||
|
||||
**Version:** 0.2
|
||||
**Datum:** 2026-05-23
|
||||
**Status:** P0–P2 ✅ · Phase A/B/B2 ✅ · **Phase C1–C3 ✅** · **Phase E ✅** (Semantik + Pfad-QA)
|
||||
**Bezüge:** `AI_PLANNING_KI_MULTISTAGE_FORECAST.md` · `AI_PROMPT_TARGET_ARCHITECTURE.md` · `SKILL_SCORING_SPEC.md` · `TRAINING_FRAMEWORK_SPEC.md` §3 (Progressionsgraph)
|
||||
|
||||
---
|
||||
|
||||
## 1. Ziel
|
||||
|
||||
Trainer in der **Trainingsplanung** sollen Übungen finden oder anlegen können mit natürlichen Anfragen wie:
|
||||
|
||||
- „Vertiefung zu Übung XY“
|
||||
- „Nächste sinnvolle Übung im Progressionsgraph Z“
|
||||
- „Baut auf der bisherigen Planung auf — Reaktionsschnelligkeit mit Partnern“
|
||||
- **Preset:** „Schlage mir die nächste Übung vor“
|
||||
|
||||
**Suche** (Bibliothek) und **Neu mit KI-Assistent** (Anlage) nutzen dasselbe **`PlanningExerciseContextPack`** — unterschiedliches Ergebnis (Treffer vs. Entwurf).
|
||||
|
||||
---
|
||||
|
||||
## 2. Architektur (Mehrstufig)
|
||||
|
||||
| Stufe | Name | Technik | P0 |
|
||||
|-------|------|---------|-----|
|
||||
| **S0** | Kontext-Pack | SQL/API, deterministisch | ✅ |
|
||||
| **S1a** | Intent strukturieren | LLM `planning_exercise_search_intent` (Szenario-Pipeline) | ✅ P1 |
|
||||
| **S1b** | Hybrid-Retrieval | Score: Volltext + Graph + Skills + Plan + **Profil** | ✅ |
|
||||
| **S1b+** | Profil-Vorselektion | `ExerciseMatchProfile` × `PlanningTargetProfile` | ✅ `profile_v1` |
|
||||
| **S1c** | Rerank + Begründung | Optional LLM `planning_exercise_search_rank` | Regelbasierte `reasons[]` |
|
||||
| **S2** | Neu-Anlage | Bestehende `suggestExerciseAi` + Pack als Zusatzkontext | Später |
|
||||
|
||||
Zwischen jeder Stufe: **nur erlaubte `exercise_id`s** (Governance / Sichtbarkeit).
|
||||
|
||||
---
|
||||
|
||||
## 3. Intent-Typen
|
||||
|
||||
| `intent_hint` | Bedeutung | Retrieval-Gewichtung (P0) |
|
||||
|---------------|-----------|---------------------------|
|
||||
| `suggest_next` | Nächste Übung (Default bei leerer/kurzer Query) | Progression + Skill-Overlap + Plan-Kontinuität |
|
||||
| `progression_next` | Explizit Graph-Folge | Progression hoch |
|
||||
| `deepen_exercise` | Vertiefung zu Anker-Übung | Skill-Overlap hoch, ähnlicher Fokus |
|
||||
| `continue_plan_goal` | Auf bisherigen Plan aufbauen | Plan-Kontinuität, Wiederholungsstrafe |
|
||||
| `free_search` | Freitext / Stichwort | Volltext hoch |
|
||||
|
||||
**S1a (später):** Freitext → JSON `{ intent, skill_hints[], requires_partner, level_hint, … }` validiert per Pydantic.
|
||||
|
||||
**P0:** `intent_hint` vom Client oder Keyword-Heuristik auf `query`.
|
||||
|
||||
---
|
||||
|
||||
## 4. PlanningExerciseContextPack (S0)
|
||||
|
||||
Serverseitig aus Request + DB (tokenbewusst für spätere LLM-Stufen):
|
||||
|
||||
| Feld | Quelle | UI-Chip |
|
||||
|------|--------|---------|
|
||||
| `unit_id`, Titel, `group_id`, Gruppenname | `training_units` + `training_groups` | Gruppe · Einheit |
|
||||
| `section_order_index`, Abschnittstitel | `training_unit_sections` | Abschnitt |
|
||||
| `planned_exercise_ids[]` | Items der Einheit (Reihenfolge) | „N Übungen im Plan“ |
|
||||
| `anchor_exercise_id`, Titel | Request oder letzte Übung vor Einfügepunkt | Anker |
|
||||
| `anchor_skill_ids[]` | `exercise_skills` | (intern) |
|
||||
| `progression_graph_id` | Request oder **Auto-Match** vom Anker (sichtbarer Graph mit passenden Ausgangskanten) | Graph |
|
||||
| `progression_graph_name`, `progression_graph_auto_resolved` | Response `context_summary` | Graph (auto) |
|
||||
| `anchor_exercise_variant_id` | Request / Abschnitt-Item / DB | (intern) |
|
||||
| `progression_successor_ids[]` | `exercise_progression_edges` ab Anker (variantenbewusst, Migration **034**) | (intern) |
|
||||
| `progression_successor_variants` | `to_exercise_variant_id` pro Nachfolger | (intern) |
|
||||
| `group_recent_exercise_ids[]` | Letzte Einheiten derselben Gruppe | Wiederholungsstrafe |
|
||||
| `framework_slot_notes` | Rahmen-Slot falls `framework_slot_id` | (später) |
|
||||
|
||||
**Berechtigung:** `get_tenant_context` + `_assert_training_unit_permission` wie `GET /training-units/{id}`.
|
||||
|
||||
---
|
||||
|
||||
## 5. Hybrid-Retrieval (S1b, P0)
|
||||
|
||||
Kandidaten: sichtbare Übungen (`library_content_visibility_sql`), ohne `archived`, max. ~400 (recent).
|
||||
|
||||
**Score** (0–1, gewichtet nach Intent):
|
||||
|
||||
```
|
||||
score = w_ft * fulltext_rank
|
||||
+ w_prog * progression_hit
|
||||
+ w_skill * skill_jaccard(anchor, candidate)
|
||||
+ w_plan * plan_affinity
|
||||
+ w_profile * profile_match(exercise, target)
|
||||
+ w_repeat * (candidate in unit_plan ? -1 : 0)
|
||||
+ w_group_repeat * (candidate in group_recent ? -0.5 : 0)
|
||||
```
|
||||
|
||||
**`profile_match`** (0–1): siehe §12–§13 — Katalog-Dimensionen + Skill-Gewichte + Skill-Gap.
|
||||
|
||||
**`reasons[]`** (regelbasiert, Deutsch): z. B. „Nachfolger im Progressionsgraph“, „Fähigkeiten passen zur Anker-Übung“, „Fokusbereich passend zum Planungsziel“, „Deckt Skill-Lücke im bisherigen Plan“, „Volltext-Treffer“.
|
||||
|
||||
---
|
||||
|
||||
## 6. API
|
||||
|
||||
### `POST /api/planning/exercise-suggest`
|
||||
|
||||
**Body:**
|
||||
|
||||
```json
|
||||
{
|
||||
"unit_id": 123,
|
||||
"section_order_index": 0,
|
||||
"phase_order_index": null,
|
||||
"parallel_stream_order_index": null,
|
||||
"anchor_exercise_id": 456,
|
||||
"anchor_exercise_variant_id": 12,
|
||||
"progression_graph_id": 7,
|
||||
"query": "Schlage mir die nächste Übung vor",
|
||||
"intent_hint": "suggest_next",
|
||||
"limit": 20,
|
||||
"exercise_kind_any": ["simple"]
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"context_summary": {
|
||||
"unit_title": "…",
|
||||
"group_name": "…",
|
||||
"section_title": "Hauptteil",
|
||||
"planned_count": 4,
|
||||
"anchor_title": "Partner-Fangspiel"
|
||||
},
|
||||
"target_profile_summary": {
|
||||
"sources": ["framework_catalog", "current_unit_plan", "anchor_exercise"],
|
||||
"focus_areas": ["Reaktion & Abwehr"],
|
||||
"top_skills": [{ "skill_id": 12, "name": "Reaktionsgeschwindigkeit", "weight": 1.0 }],
|
||||
"has_skill_gap": true
|
||||
},
|
||||
"retrieval_phase": "profile_v1",
|
||||
"intent_resolved": "suggest_next",
|
||||
"hits": [
|
||||
{
|
||||
"id": 99,
|
||||
"title": "…",
|
||||
"summary": "…",
|
||||
"score": 0.78,
|
||||
"reasons": ["Nachfolger im Progressionsgraph", "Fokusbereich passend zum Planungsziel"],
|
||||
"focus_area": "…"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Modul:** `backend/planning_exercise_suggest.py` · `backend/planning_exercise_profiles.py` · Router `backend/routers/planning_exercise_suggest.py`
|
||||
|
||||
---
|
||||
|
||||
## 7. Frontend
|
||||
|
||||
| Ort | Verhalten |
|
||||
|-----|-----------|
|
||||
| `ExercisePickerModal` | Prop `planningContext` → Planungs-API statt reiner `listExercises`; Kontext-Chips; `reasons` unter Treffer |
|
||||
| `TrainingUnitEditPage` | `planningContext` aus Einheit + Picker-Ziel (Anker = letzte Übung im Abschnitt) |
|
||||
| **`ExercisesListPageRoot`** | Schalter **„Neu mit KI-Assistent“**: Planungs-KI-Suche (frei, ohne `unit_id`) + Neuanlage im Modal; **„+ Neu“** ausgeblendet |
|
||||
| Rahmen / Kombi-Formular | analog, sobald `unit_id` / Slot-Blueprint bekannt |
|
||||
| Übungsliste (ohne KI-Schalter) | weiter Volltext |
|
||||
|
||||
**Zweites Suchfeld** im Picker: Query = Volltext + ergänzender Begriff (ODER in P0 als Konkatenation an Backend).
|
||||
|
||||
---
|
||||
|
||||
## 8. Neu-Anlage (Anbindung, Phase P1)
|
||||
|
||||
Wenn `hits` leer oder Trainer wählt „Mit KI anlegen“:
|
||||
|
||||
- `planning_context` im Request-Body → `planning_context_json` in Übungs-Prompts (Migration **085**); Pfad-Builder + Picker ✅ **0.8.208**
|
||||
- Kurzbeschreibung optional leer (freier Vorschlag) oder aus Intent/Skizze
|
||||
|
||||
---
|
||||
|
||||
## 9. Phasen-Roadmap
|
||||
|
||||
| Phase | Inhalt | Status |
|
||||
|-------|--------|--------|
|
||||
| **P0** | Context-Pack, Hybrid-Score, API, Picker in Planung | ✅ |
|
||||
| **P0.1** | `ExerciseMatchProfile` / `PlanningTargetProfile`, `profile_v1` | ✅ |
|
||||
| **P1** | Szenario-Pipeline + LLM Query-Intent → Erwartungsprofil | ✅ |
|
||||
| **P2 / B2** | LLM-Rerank bei engem Top-Feld (max. 2 Calls) | ✅ |
|
||||
| **P3** | Skill-Discovery / Framework-Ziele im Pack | 🔲 |
|
||||
| **A** | Voll-Library Hybrid-Ranking | ✅ **0.8.177** |
|
||||
| **B** | Text-Signale guidance/Rahmen-Ziele | ✅ **0.8.181** |
|
||||
| **C1** | Graph auto-match + variantenbewusste Nachfolger | ✅ **0.8.183** |
|
||||
| **C2** | Varianten in Trefferliste / Picker | ✅ **0.8.184** |
|
||||
| **C3** | Graph-Builder (Ziel → Pfad → speichern) | ✅ **0.8.185** |
|
||||
| **E** | Semantik-Schicht + Pfad-QA (Lücken/Brücken/LLM-QS) | ✅ **0.8.186** |
|
||||
| **E2** | Pfad-Neuordnung + KI-Lückenfüller | ✅ **0.8.187** |
|
||||
| **D** | Neu-Anlage: `planning_context` an `suggestExerciseAi` (Migration **085**) | ✅ **0.8.208** |
|
||||
|
||||
---
|
||||
|
||||
## 10. Changelog
|
||||
|
||||
- **2026-05-23:** Phase C1 — Graph auto-match, variantenbewusste Nachfolger (`planning_exercise_progression.py`).
|
||||
- **2026-05-23:** Phase B2 — Rerank bei engem Top-Feld; Phase B — Text-Signale; Phase A — Voll-Library (siehe §17–§19).
|
||||
- **2026-05-22:** Erstfassung; P0 API + Planungs-Picker.
|
||||
- **2026-05-22:** P0 implementiert (`planning_exercise_suggest.py`, Router, Picker); unsaved Formular-Plan noch nicht an API (nur persistierte Einheit).
|
||||
- **2026-05-22:** P0.1 — `planning_exercise_profiles.py`, Profil-Score in Hybrid-Retrieval, `retrieval_phase: profile_v1`, `target_profile_summary`.
|
||||
- **2026-05-22:** P2 — LLM-Rerank optional (`include_llm_rank`); Client `planned_exercise_ids[]`; Prompt Migration 072.
|
||||
|
||||
---
|
||||
|
||||
## 11. Bekannte Lücken & Backlog
|
||||
|
||||
- **Ungespeicherte Plan-Änderungen:** ✅ Client übergibt `planned_exercise_ids[]` aus Formular (TrainingUnitEditPage).
|
||||
- **Progressionsgraph-ID:** ✅ Auto-Match vom Anker (**C1**); manuelle Auswahl in UI noch offen.
|
||||
- **Anker-Variante:** ✅ Client + DB (**C1**); Picker wählt Variante bei Treffer (**C2** — Dropdown + Graph-Vorschlag).
|
||||
- **Graph-Builder (C3):** Ziel → Pfad vorschlagen → in Graph speichern — ✅ **0.8.185**
|
||||
- **Varianten-Suche:** Library-Picker nutzt `include_variants`; Planungs-KI rankt primär **Übungsebene** — Varianten-Expansion nur gezielt (**C2**).
|
||||
- **Enrichment:** Superadmin-Tool für Skills; Datenqualität der Bibliothek entscheidend für Profil-Score.
|
||||
- **LLM-Intent:** ✅ P1 Szenario-Pipeline + `planning_exercise_search_intent` (Migration 073).
|
||||
- **Preset + LLM:** ✅ Erwartungs-LLM (074) bei Planungsbezug; Preset ohne Plan = kein Erwartungs-LLM.
|
||||
|
||||
---
|
||||
|
||||
## 16. Szenario-Pipeline & Query-Erwartungsprofil (P1)
|
||||
|
||||
Komplexe Planungsanfragen brauchen **Schritte vor** dem Profil-Match — nicht jede Query ist gleich.
|
||||
|
||||
### 16.1 Szenario-Klassen
|
||||
|
||||
| `scenario_kind` | Typische Anfrage | LLM Intent? |
|
||||
|-----------------|------------------|-------------|
|
||||
| `preset_next` | „Nächste Übung vorschlagen“ (Preset) | Erwartungs-LLM (074) wenn Planungsbezug |
|
||||
| `progression` | Progressionsgraph / Pfad | Ja (wenn Freitext) |
|
||||
| `deepen` | Vertiefung Anker | Ja |
|
||||
| `continue_plan` | Auf bisherigen Plan aufbauen | Ja |
|
||||
| `additive_constraint` | Plan **+** Zusatz (z. B. Schnellkraft) | Ja |
|
||||
| `free_search` | Offene Stichwortsuche | Ja |
|
||||
|
||||
**Routing:** `planning_exercise_target_pipeline.classify_planning_scenario()` → `should_run_llm_intent_pipeline()`.
|
||||
|
||||
### 16.2 Pipeline (Reihenfolge)
|
||||
|
||||
```
|
||||
S0 Kontext-Pack
|
||||
→ Heuristik-Intent + Szenario
|
||||
→ [optional] LLM planning_exercise_search_intent
|
||||
→ Basis PlanningTargetProfile (Rahmen, Plan, Anker, Gap)
|
||||
→ Merge Query-Overlay (Katalog-IDs aus Hints)
|
||||
→ Hybrid-Retrieval + Profil-Score
|
||||
→ [optional] LLM-Rerank
|
||||
```
|
||||
|
||||
Module: `planning_exercise_target_pipeline.py` · `planning_exercise_intent.py`
|
||||
|
||||
### 16.3 API (Erweiterung)
|
||||
|
||||
| Request | Default | Bedeutung |
|
||||
|---------|---------|-----------|
|
||||
| `include_llm_intent` | `true` | LLM nur wenn Szenario ≠ preset_next und Query nicht leer |
|
||||
|
||||
| Response | Bedeutung |
|
||||
|----------|-----------|
|
||||
| `scenario_kind` | Szenario-Klasse |
|
||||
| `query_intent_summary` | intent, llm_applied, rationale, skill_hints_resolved |
|
||||
| `intent_heuristic` | Heuristik vor LLM |
|
||||
| `retrieval_phase` | z. B. `profile_v1+query_intent+llm_rank` |
|
||||
|
||||
**Prompt 073:** `planning_exercise_search_intent` — Ausgabe JSON mit `skill_hints`, `focus_hints`, `emphasis` (`additive`|`replace`).
|
||||
|
||||
---
|
||||
|
||||
## 15. LLM-Rerank (P2)
|
||||
|
||||
**Request:**
|
||||
|
||||
| Feld | Typ | Default | Bedeutung |
|
||||
|------|-----|---------|-----------|
|
||||
| `planned_exercise_ids` | `int[]` | — | Optional: Reihenfolge aus Formular (überschreibt DB-Plan) |
|
||||
| `include_llm_rank` | `bool` | `true` (Client) | Backend gated (B2): Rerank nur bei engem Top-Feld, max. 2 LLM-Calls |
|
||||
|
||||
**Response:**
|
||||
|
||||
| Feld | Wert |
|
||||
|------|------|
|
||||
| `retrieval_phase` | `profile_v1` oder `profile_v1+llm_rank` |
|
||||
| `llm_rank_applied` | `true` wenn LLM erfolgreich sortiert hat |
|
||||
| `hits[].llm_rank` | optional: Position nach LLM (1…n) |
|
||||
|
||||
**Fallback:** Kein API-Key, inaktiver Prompt oder Parse-Fehler → Hybrid-Reihenfolge unverändert, `llm_rank_applied: false`.
|
||||
|
||||
**Prompt:** Migration **072**, Slug `planning_exercise_search_rank` — Kandidaten als JSON mit Titel, summary, goal (Plaintext), skills; Ausgabe `{ ranked_ids, reasons }`.
|
||||
|
||||
---
|
||||
|
||||
## 12. ExerciseMatchProfile & PlanningTargetProfile (Phase 1)
|
||||
|
||||
Ziel: deterministische Vorselektion über **Profil-Dimensionen** statt nur Titel/Jaccard.
|
||||
|
||||
### 12.1 ExerciseMatchProfile (pro Übung)
|
||||
|
||||
| Feld | Quelle |
|
||||
|------|--------|
|
||||
| `focus_area_ids` | `exercise_focus_areas` (Primary = 1.0, sonst 0.85) |
|
||||
| `style_direction_ids` | `exercise_style_directions` |
|
||||
| `training_type_ids` | `exercise_training_types` |
|
||||
| `target_group_ids` | `exercise_target_groups` |
|
||||
| `skill_weights` | `exercise_skills` × Intensitäts-Multiplikator (`skill_scoring._skill_link_multiplier`) |
|
||||
|
||||
Bulk-Lader: `load_exercise_match_profiles_bulk(cur, exercise_ids)`.
|
||||
|
||||
### 12.2 PlanningTargetProfile (Planungsziel)
|
||||
|
||||
Zusammensetzung aus mehreren Quellen (`sources[]`):
|
||||
|
||||
| Quelle | Inhalt |
|
||||
|--------|--------|
|
||||
| `framework_catalog` | Fokus/Stil/Trainingsstil/Zielgruppe aus `training_framework_program_*` |
|
||||
| `framework_slot_skill_profile` | Skill-Profil des Slot-Blueprints (`profile_for_occurrences`) |
|
||||
| `framework_overall_skill_profile` | Fallback: alle Blueprint-Einheiten des Rahmens |
|
||||
| `current_unit_plan` | Skill-Profil der bereits eingeplanten Übungen dieser Einheit |
|
||||
| `anchor_exercise` | Katalog + Skills der Anker-Übung (Intent-abhängig) |
|
||||
| `skill_gap_vs_plan` | `target_skills − plan_skills` (normalisiert, Schwelle > 0.08) |
|
||||
|
||||
Builder: `build_planning_target_profile(cur, unit=…, planned_exercise_ids=…, anchor_exercise_id=…, intent=…)`.
|
||||
|
||||
Rahmen-Anbindung über `unit.framework_slot_id` oder `origin_framework_slot_id`.
|
||||
|
||||
---
|
||||
|
||||
## 13. Profil-Score (Formeln)
|
||||
|
||||
**Gewichtete Überlappung** (Katalog + Skills):
|
||||
|
||||
```
|
||||
overlap(a, b) = Σ min(a[k], b[k]) / Σ max(a[k], b[k])
|
||||
```
|
||||
|
||||
**Skill-Gap-Abdeckung:**
|
||||
|
||||
```
|
||||
gap_coverage(gap, candidate) = Σ min(gap[k], candidate[k]) / Σ gap[k]
|
||||
```
|
||||
|
||||
**Profil-Score** (intent-gewichtet, Summe Dimensionen = 1.0):
|
||||
|
||||
```
|
||||
profile_score = w_focus * overlap(focus)
|
||||
+ w_style * overlap(style)
|
||||
+ w_tt * overlap(training_type)
|
||||
+ w_tg * overlap(target_group)
|
||||
+ w_skill * overlap(skill_weights)
|
||||
+ w_gap * gap_coverage(skill_gap)
|
||||
```
|
||||
|
||||
Intent-Gewichte (Auszug): `deepen_exercise` → Skill hoch; `continue_plan_goal` → Gap hoch; `free_search` → Gap + Skill moderat.
|
||||
|
||||
Scorer: `score_exercise_against_target(exercise_profile, target_profile, intent=…) → (score, reasons[])`.
|
||||
|
||||
---
|
||||
|
||||
## 14. Hybrid + Profil (P0.1)
|
||||
|
||||
Im Hybrid-Score kommt **`w_profile * profile_score`** hinzu (Intent-abhängig ~0.15–0.35). Jaccard auf Anker-Skills bleibt parallel (schneller Anker-Fokus).
|
||||
|
||||
**Response-Felder:**
|
||||
|
||||
| Feld | Bedeutung |
|
||||
|------|-----------|
|
||||
| `retrieval_phase` | `"profile_v1"` — Phase-1 aktiv, kein LLM-Rerank |
|
||||
| `target_profile_summary` | Lesbare Kurzinfo für UI-Chips (Fokus, Top-Skills, Quellen) |
|
||||
|
||||
**Phase 2 (P2 / B2):** siehe §15 und §18 — `include_llm_rank: true` vom Client, Backend entscheidet.
|
||||
|
||||
---
|
||||
|
||||
## 17. Phase A — Voll-Library-Ranking (0.8.177)
|
||||
|
||||
- Kein OR-Profil-Pool (~500 Übungen) mehr.
|
||||
- Alle sichtbaren Übungen (bis 8000) werden hybrid gescored (`fetch_all_visible_exercise_rows` + `rank_visible_library_hits`).
|
||||
- API: `full_library_ranked: true`, `retrieval_phase` enthält `+full_library+`.
|
||||
|
||||
---
|
||||
|
||||
## 18. Phase B / B2 — Text-Signale & Rerank-Gates (0.8.181–0.8.182)
|
||||
|
||||
**B — Text-Signale (`planning_exercise_text_signals.py`):**
|
||||
|
||||
- `section_guidance_notes`, Rahmen-Ziele/Notizen → Skill-/Katalog-Gewichte ohne LLM.
|
||||
- `requires_partner` aus Intent filtert Kandidaten.
|
||||
- `retrieval_phase +text_signals`.
|
||||
|
||||
**B2 — Rerank bei unklarem Ranking:**
|
||||
|
||||
- `hybrid_ranking_ambiguous(hits)` (Top-4-/Top-10-Gap).
|
||||
- Rerank auch nach Erwartungs-/Intent-LLM, wenn Scores eng beieinander.
|
||||
- Budget: max. **2** LLM-Calls (Profil + optional Rerank).
|
||||
|
||||
---
|
||||
|
||||
## 19. Phase C1 — Progressionsgraph im Planungskontext (0.8.183)
|
||||
|
||||
**Modul:** `planning_exercise_progression.py`
|
||||
|
||||
### Auto-Match Graph
|
||||
|
||||
Wenn `progression_graph_id` fehlt und Anker-Übung gesetzt: sichtbarer Graph mit passender `next_exercise`-Kante vom Anker (variantenbewusst). Bevorzugung: variantenspezifische Kanten > Anzahl Kanten.
|
||||
|
||||
### Variantenbewusste Nachfolger (Migration 034)
|
||||
|
||||
Generische Kante (`from_exercise_variant_id IS NULL`) gilt für jeden Anker; variantenspezifische Kante nur bei passender Anker-Variante.
|
||||
|
||||
Treffer: optional `hits[].suggested_variant_id`.
|
||||
|
||||
### Request / Response
|
||||
|
||||
| Feld | Bedeutung |
|
||||
|------|-----------|
|
||||
| `anchor_exercise_variant_id` | Request — Variante der Anker-Übung |
|
||||
| `progression_graph_name` | Response — Name des (auto-)Graphs |
|
||||
| `progression_graph_auto_resolved` | Response — Auto-Match aktiv |
|
||||
|
||||
---
|
||||
|
||||
## 20. Phase C2 — Varianten in Treffern (0.8.184) ✅
|
||||
|
||||
- API: `variants[]`, `suggested_variant_name` pro Treffer (Batch aus `exercise_variants`).
|
||||
- **`ExercisePickerModal`:** Dropdown pro Treffer; Graph-`suggested_variant_id` vorausgewählt; Übernahme setzt `exercise_variant_id`.
|
||||
- **`hydrateExercisePlanningRow`:** übernimmt `exercise_variant_id` / `suggested_variant_id` in die Planungszeile.
|
||||
|
||||
---
|
||||
|
||||
## 21. Phase C3 — Graph-Builder (0.8.185) ✅
|
||||
|
||||
**API:** `POST /api/planning/progression-path-suggest`
|
||||
|
||||
| Feld | Bedeutung |
|
||||
|------|-----------|
|
||||
| `query` | Ziel / Entwicklungsrichtung (Freitext, min. 3 Zeichen) |
|
||||
| `max_steps` | 2–10, Default 5 |
|
||||
| `progression_graph_id` | optional — Graph-Kontext für Nachfolger ab Schritt 2 |
|
||||
| `include_llm_intent` | LLM nur Schritt 1 (Budget) |
|
||||
|
||||
**Response:** `steps[]` mit `exercise_id`, `variant_id`, `title`, `reasons`, `variants`; `retrieval_phase: …+path_builder`.
|
||||
|
||||
**Algorithmus:** Iterativ Hybrid-Ranking — Schritt 1 aus Zielprofil, Folgeschritte mit Anker = letzte Übung, ohne Duplikate.
|
||||
|
||||
**UI:** `ExerciseProgressionPathBuilder` im Progressionsgraph-Panel — Review, Varianten, `POST …/edges/sequence`.
|
||||
|
||||
---
|
||||
|
||||
## 22. Phase E — Semantik-Schicht + Pfad-QA (0.8.186) ✅
|
||||
|
||||
### Semantic Brief (`planning_exercise_semantics.py`)
|
||||
|
||||
Parallel zum Katalog-Overlay — **nicht ersetzend**:
|
||||
|
||||
| Feld | Bedeutung |
|
||||
|------|-----------|
|
||||
| `primary_topic` | z. B. `mae geri` |
|
||||
| `must_phrases` / `exclude_phrases` | Phrasen-Match in Titel/Ziel/Varianten |
|
||||
| `development_arc` | einstieg → … → perfektion |
|
||||
| `semantic_strength` | 0–1 — steuert dynamisches Blend im Hybrid-Score |
|
||||
| `retrieval_query` | fokussierte Volltext-Query (nicht ganzer Satz) |
|
||||
|
||||
Optional LLM: Prompt `planning_exercise_query_semantics` (Migration **075**).
|
||||
|
||||
**Hybrid-Score:** neuer Term `w_semantic * semantic_score` — Profil/Volltext werden bei hoher `semantic_strength` relativ abgeschwächt.
|
||||
|
||||
### Pfad-QA (`planning_exercise_path_qa.py`)
|
||||
|
||||
Nach Pfad-Bildung:
|
||||
|
||||
1. **Lücken-Messung** zwischen benachbarten Schritten (Skill-Jaccard + Semantik zum erwarteten Phasen-Segment)
|
||||
2. **Brücken-Übungen** bei großen Lücken (zusätzliche Schritte, markiert `is_bridge`)
|
||||
3. **LLM-QS** (Prompt `planning_exercise_path_qa`): Reihenfolge, Themen-Abdeckung, Empfehlungen
|
||||
|
||||
**API-Erweiterung** `progression-path-suggest`: `include_path_qa`, `include_llm_path_qa` · Response: `semantic_brief_summary`, `path_qa`.
|
||||
|
||||
**Pfad-Schritte:** Semantic Brief + Entwicklungsphase in **allen** Schritten (nicht nur Schritt 1).
|
||||
|
||||
### Phase E2 (0.8.187)
|
||||
|
||||
- **LLM-QS → Neuordnung:** `ordered_step_indices` im Prompt `planning_exercise_path_qa` (Migration **076**)
|
||||
- **KI-Lückenfüller:** `planning_exercise_path_ai_fill.py` — `is_ai_proposal` wenn Bibliothek keine Brücke liefert
|
||||
- Request: `include_path_reorder`, `include_ai_gap_fill`
|
||||
|
||||
---
|
||||
|
||||
## 23. Phase E3 (0.8.203) ✅
|
||||
|
||||
- Off-Topic aus Pfad entfernen; `gap_fill_offers` mit `goal_for_ai`; voller KI-Call im UI (kein Pre-Vorschlag)
|
||||
- Migration **077** `suggested_new_exercises` im Pfad-QS-Prompt
|
||||
|
||||
---
|
||||
|
||||
## 24. Phase F — Roadmap-first Progressionsgraph (0.8.204–217) ✅
|
||||
|
||||
**Entscheidung:** Progressionsgraph plant **vom Ziel rückwärts** (Roadmap → Stufenspezifikation → Bibliothek/KI). **Keine Gruppenanalyse** — die gehört zur Trainingsplanung.
|
||||
|
||||
**Ist-Stand (vollständig):** `docs/architecture/PLANNING_PROGRESSION_GRAPH_KI.md`
|
||||
**Spec:** `working/PLANNING_PROGRESSION_ROADMAP_SPEC.md` · **Roadmap:** `docs/architecture/PLANNING_KI_ROADMAP.md`
|
||||
|
||||
| Teil | Modul / API |
|
||||
|------|-------------|
|
||||
| Pipeline | `planning_progression_roadmap.py` (Workflow-lite) |
|
||||
| Match | `planning_exercise_path_builder.py` — `roadmap_first`, `roadmap_override` |
|
||||
| Skills | `planning_skill_expectations.py` — pro Stufe + Pfad |
|
||||
| Gap-KI | `planning_exercise_form_context.py`, `planning_exercise_path_ai_fill.py` |
|
||||
| Persistenz | `planning_roadmap` JSONB (Migration **088**) |
|
||||
| API | `progression-path-suggest`, `PUT` Graph, `POST …/edges/sequence` |
|
||||
| Prompts | **078/079/087** — Slugs nur in `ai_prompts` |
|
||||
| UI | `ExerciseProgressionPathBuilder`, `ExerciseGapFillPrepModal` |
|
||||
|
||||
**Graph-Bias:** `progression_graph_id` bevorzugt **bestehende Nachfolger** ab Schritt 2 (Gewicht ~4–10 %), baut aber **keinen** Pfad aus vorhandenen Knoten — siehe Ist-Doku §5.
|
||||
|
||||
**Mitai Workflow-Engine:** bewusst **nicht** jetzt — Pipeline workflow-ready für spätere Anbindung.
|
||||
|
||||
---
|
||||
|
||||
## 25. Backlog (offen)
|
||||
|
||||
Siehe priorisierte Liste in **`docs/architecture/PLANNING_PROGRESSION_GRAPH_KI.md`** §10:
|
||||
|
||||
1. UI-Wizard (Progressionsgraph) — separater Chat
|
||||
2. Graph-Erweiterungsmodus (Start ab Knoten)
|
||||
3. Trainingsplanung Phase G (Gruppenkontext, `planning_skill_expectations`)
|
||||
4. Kontext auf allen Pfad-Schritten in der UI
|
||||
5. Enrichment / Prompt-Feintuning
|
||||
6. Mitai Workflow-Engine (langfristig)
|
||||
209
.claude/docs/working/PLANNING_PROGRESSION_ROADMAP_SPEC.md
Normal file
209
.claude/docs/working/PLANNING_PROGRESSION_ROADMAP_SPEC.md
Normal file
|
|
@ -0,0 +1,209 @@
|
|||
# Planungs-KI — Progressions-Roadmap (Phase F)
|
||||
|
||||
**Version:** 0.1
|
||||
**Datum:** 2026-06-07
|
||||
**Status:** VERBINDLICHE ZIELARCHITEKTUR — **F0–F9 umgesetzt** (0.8.217)
|
||||
**Geltungsbereich:** **Progressionsgraph** (`exercise_progression_graphs`) — **ohne** Gruppenanalyse
|
||||
|
||||
**Ist-Stand (Module, API, Graph-Verhalten, Persistenz):** `docs/architecture/PLANNING_PROGRESSION_GRAPH_KI.md`
|
||||
|
||||
**Bezüge:**
|
||||
`working/PLANNING_EXERCISE_SUGGEST_CONTEXT.md` · `working/AI_PLANNING_KI_MULTISTAGE_FORECAST.md` · `technical/AI_TRAINING_PLANNING_CONCEPT.md` · `technical/AI_PROMPT_TARGET_ARCHITECTURE.md` · `docs/architecture/PLANNING_KI_ROADMAP.md` · `docs/HANDOVER.md`
|
||||
|
||||
---
|
||||
|
||||
## 1. Entscheidung (2026-06-07)
|
||||
|
||||
### 1.1 Problem
|
||||
|
||||
Der Pfad-Builder (Phase C3/E) ist **retrieval-first**: Zieltext → N Übungen aus der Bibliothek → QS nachbessern. Das entspricht nicht der menschlichen Planung (Ziel → Roadmap → Stufenspezifikation → Übung).
|
||||
|
||||
### 1.2 Festlegung
|
||||
|
||||
| Thema | Entscheidung |
|
||||
|--------|----------------|
|
||||
| **Progressionsgraph** | **Roadmap-first** — Phasen A→B→C, dann Bibliothek (D), dann Feinausplanung (E) |
|
||||
| **Gruppenanalyse** | **Nicht** in der Graphen-Pipeline — erst bei **Trainingsplanung** (Einheit/Rahmen) |
|
||||
| **Mitai Workflow-Engine** | **Nicht** jetzt portieren — **Workflow-lite** (`PlanningProgressionPipeline`), später workflow-ready |
|
||||
| **Ein Mega-Prompt** | **Verboten** — validierte Artefakte pro Phase |
|
||||
|
||||
### 1.3 Abgrenzung Trainingsplanung
|
||||
|
||||
```
|
||||
Progressionsgraph-Pipeline Trainingsplanungs-Pipeline (später)
|
||||
───────────────────────── ───────────────────────────────────
|
||||
Ziel + N Major Steps Gruppe + Historie + Termin + Rahmen
|
||||
Kein Gruppenkontext Kontext-Pack S0 (AI_PLANNING_KI_MULTISTAGE_FORECAST)
|
||||
Curriculum / Technikpfad Session-Füllung / Reihenfolge / Zeiten
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Menschliches Vorbild → Phasen
|
||||
|
||||
| Mensch | Phase | Output-Artefakt | LLM |
|
||||
|--------|-------|-----------------|-----|
|
||||
| Startpunkt + Zielzustand | **A** Zielanalyse | `goal_analysis` | Optional (klein) |
|
||||
| Zwischenziele, gewichten, auf N reduzieren | **B** Roadmap | `roadmap` (`micro_objectives[]`, `major_steps[N]`) | Ja |
|
||||
| Belastung, Übungstyp, Lernziel je Stufe | **C** Stufenspezifikation | `stage_specs[]` | Teilweise |
|
||||
| Bibliothek / Brücke | **D** Match | `step_matches[]` oder `gaps[]` | Nein (Retrieval) |
|
||||
| Skizze + Feinplan | **E** Übungsentwurf | bestehend `suggestExerciseAi` | On-demand |
|
||||
|
||||
**Phase B** = Kern: 8–12 `micro_objectives` → Konsolidierung → exakt `max_steps` `major_steps`.
|
||||
|
||||
---
|
||||
|
||||
## 3. Pipeline-Orchestrator (Workflow-lite)
|
||||
|
||||
Modul: **`backend/planning_progression_roadmap.py`**
|
||||
|
||||
```python
|
||||
ctx = ProgressionRoadmapContext(goal_query=..., max_steps=N, semantic_brief=...)
|
||||
ctx = phase_a_goal_analysis(ctx) # deterministisch + optional LLM
|
||||
ctx = phase_b_roadmap(ctx) # micro → major
|
||||
ctx = phase_c_stage_specs(ctx) # je major_step
|
||||
# Phase D/E: bestehende path_builder / retrieval / ai_fill — speisen von ctx.major_steps
|
||||
```
|
||||
|
||||
Jede Phase: `(ctx) → ctx`, Zwischenergebnisse in API-Response für **Human-in-the-loop** (Roadmap-Review vor Übungs-Match).
|
||||
|
||||
**Später:** jede Phase = Workflow-Knoten (Mitai-kompatibel), keine API-Änderung an Artefakten.
|
||||
|
||||
---
|
||||
|
||||
## 4. JSON-Artefakte (Pydantic)
|
||||
|
||||
### 4.1 `goal_analysis` (Phase A)
|
||||
|
||||
```json
|
||||
{
|
||||
"primary_topic": "Mae Geri",
|
||||
"start_assumption": "Grundkenntnisse der Standführung, keine Perfektion",
|
||||
"target_state": "Sicherer, präziser Mae Geri unter Belastung und in Anwendung",
|
||||
"success_criteria": ["saubere Kammerhaltung", "Hüftführung", "Kime am Zielpunkt"],
|
||||
"constraints": { "partner_required": false, "equipment": [] }
|
||||
}
|
||||
```
|
||||
|
||||
### 4.2 `roadmap` (Phase B)
|
||||
|
||||
```json
|
||||
{
|
||||
"micro_objectives": [
|
||||
{ "id": "m1", "phase": "grundlage", "title": "Stellung und Kammerhaltung", "weight": 0.9, "depends_on": [] },
|
||||
{ "id": "m2", "phase": "vertiefung", "title": "Hüft- und Kniekoordination", "weight": 0.85, "depends_on": ["m1"] }
|
||||
],
|
||||
"major_steps": [
|
||||
{
|
||||
"index": 0,
|
||||
"phase": "grundlage",
|
||||
"learning_goal": "Stabile Mae-Geri-Grundstellung",
|
||||
"consolidates": ["m1"],
|
||||
"rationale": "Einstieg ohne Perfektionsdruck"
|
||||
}
|
||||
],
|
||||
"consolidation_notes": ["Perfektion mit Anwendung zusammengeführt"]
|
||||
}
|
||||
```
|
||||
|
||||
### 4.3 `stage_spec` (Phase C, je Major Step)
|
||||
|
||||
```json
|
||||
{
|
||||
"major_step_index": 2,
|
||||
"learning_goal": "…",
|
||||
"load_profile": ["präzision", "koordination"],
|
||||
"exercise_type": "kihon_einzel",
|
||||
"success_criteria": ["…"],
|
||||
"anti_patterns": ["reine Kraftübung ohne Technikbezug"]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. API (schrittweise)
|
||||
|
||||
### 5.1 Erweiterung `POST /api/planning/progression-path-suggest`
|
||||
|
||||
| Feld (neu) | Default | Bedeutung |
|
||||
|------------|---------|-----------|
|
||||
| `roadmap_first` | `false` → später `true` | Roadmap-Pipeline vor Retrieval |
|
||||
| `include_roadmap_preview` | `true` wenn `roadmap_first` | Artefakte A/B/C in Response |
|
||||
|
||||
**Response (neu):**
|
||||
|
||||
```json
|
||||
{
|
||||
"progression_roadmap": {
|
||||
"goal_analysis": { },
|
||||
"roadmap": { },
|
||||
"stage_specs": [ ],
|
||||
"pipeline_phase": "roadmap_v1"
|
||||
},
|
||||
"steps": [ ]
|
||||
}
|
||||
```
|
||||
|
||||
**Übergangsphase (0.8.204):** `include_roadmap_preview=true` liefert Roadmap **parallel** zum bestehenden retrieval-first Pfad — UI kann Roadmap reviewen, Schritte bleiben vorerst retrieval-basiert.
|
||||
|
||||
**Zielphase (F2):** `roadmap_first=true` — Retrieval pro Major Step aus `stage_specs`, nicht mehr iterativ „beste nächste Übung“.
|
||||
|
||||
### 5.2 Prompt-Slugs — nur in `ai_prompts`, nie im Code
|
||||
|
||||
**Regel:** Prompt-**Texte** leben ausschließlich in der Tabelle `ai_prompts` (Superadmin bearbeitbar, Vorschau, `openrouter_model` pro Zeile). Python referenziert nur **Slugs** (`PROMPT_SLUG_*` in `planning_progression_roadmap.py`). Kein verstecktes Hardcoding von Templates.
|
||||
|
||||
| Slug | Phase | Migration |
|
||||
|------|-------|-----------|
|
||||
| `planning_progression_start_target` | Start/Ziel | **087** |
|
||||
| `planning_progression_goal_analysis` | A | **078** |
|
||||
| `planning_progression_roadmap` | B | **078** |
|
||||
| `planning_progression_stage_spec` | C | **079** |
|
||||
|
||||
**API:** `include_llm_roadmap` (Default `true`) — lädt Prompts via `load_and_render_ai_prompt`. Bei Fehler/kein OpenRouter: **deterministischer Fallback** (kein stilles Versagen).
|
||||
|
||||
**Response:** `prompt_slugs` (genutzte Slugs), `prompt_slug_catalog` (Referenz), `llm_*_applied` Flags.
|
||||
|
||||
**Admin:** Templates unter Kategorie `training` pflegen — siehe `AI_PROMPT_SYSTEM_SPEC.md`.
|
||||
|
||||
---
|
||||
|
||||
## 6. UI-Roadmap
|
||||
|
||||
1. **F1:** Roadmap-Box unter Ziel-Eingabe (Major Steps als Karten, editierbar) — vor Übungsliste
|
||||
2. **F2:** Match-Ergebnis pro Major Step (Bibliothek / Lücke / KI anlegen)
|
||||
3. **F3:** `roadmap_first` als Default im Graph-Builder
|
||||
|
||||
---
|
||||
|
||||
## 7. Was bewusst nicht in Phase F
|
||||
|
||||
- Gruppen-Historie, Belastungssteuerung der Gruppe
|
||||
- Mitai `workflow_engine` Port
|
||||
- Vollautomatisches Speichern ohne Trainer-Review
|
||||
|
||||
---
|
||||
|
||||
## 8. Implementierungsstände
|
||||
|
||||
| ID | Inhalt | Status |
|
||||
|----|--------|--------|
|
||||
| **F0** | Spec + Doku + `planning_progression_roadmap.py` Scaffold | ✅ 0.8.204 |
|
||||
| **F1** | `include_roadmap_preview` in API + deterministische A/B | ✅ 0.8.204 |
|
||||
| **F2** | LLM Phase A/B/C über `ai_prompts` (078/079), `include_llm_roadmap` | ✅ 0.8.205 |
|
||||
| **F3** | Retrieval aus `stage_specs` (roadmap_first) | ✅ 0.8.206–209 |
|
||||
| **F4** | UI Roadmap-Review + `roadmap_override` | ✅ 0.8.207 |
|
||||
| **F5** | Start/Ziel strukturiert + Prompt **087** + Zwei-Schritt-UI | ✅ 0.8.210–214 |
|
||||
| **F6** | Gap-Prep + `planning_context` an Übungs-KI | ✅ 0.8.212–214 |
|
||||
| **F7** | `planning_skill_expectations` | ✅ 0.8.215–216 |
|
||||
| **F8** | Editierbare `stage_specs` in UI | ✅ 0.8.216 |
|
||||
| **F9** | `planning_roadmap` JSONB (Migration **088**) | ✅ 0.8.217 |
|
||||
| **G** | Trainingsplanung: eigene Pipeline + Workflow-Engine | 🔲 |
|
||||
|
||||
Details: `docs/architecture/PLANNING_PROGRESSION_GRAPH_KI.md`
|
||||
|
||||
---
|
||||
|
||||
## 9. Changelog
|
||||
|
||||
- **2026-05-22:** Ist-Stand F5–F9 dokumentiert; Verweis auf `PLANNING_PROGRESSION_GRAPH_KI.md`.
|
||||
- **2026-06-07:** Erstfassung — Roadmap-first Entscheidung, Abgrenzung Graphen vs. Planung, Workflow-lite.
|
||||
81
.claude/docs/working/PROGRESSION_GRAPH_SLOT_EDITOR_SPEC.md
Normal file
81
.claude/docs/working/PROGRESSION_GRAPH_SLOT_EDITOR_SPEC.md
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
# Progressionsgraph — Slot-Editor (Phase B)
|
||||
|
||||
**Stand:** 2026-06-10 · **Status:** In Umsetzung
|
||||
|
||||
## Ziel
|
||||
|
||||
Ein Progressionsgraph = **ein linearer Hauptpfad** (Roadmap = strukturgebend). Jeder **Major Step** ist ein **Slot** mit:
|
||||
|
||||
- **primary** — Hauptübung des Slots (Pfadknoten)
|
||||
- **siblings** — 0..n Schwestern (gleiche Stufe, `edge_type: sibling`)
|
||||
|
||||
KI-Entwürfe und Bibliotheksübungen leben **im selben Slot-Modell**, ohne sofortige Übungsanlage.
|
||||
|
||||
## Slot-Zustände (`kind`)
|
||||
|
||||
| kind | Bedeutung |
|
||||
|------|-----------|
|
||||
| `empty` | Noch keine Übung |
|
||||
| `library` | `exercise_id` (+ optional `variant_id`) |
|
||||
| `proposal` | KI-Entwurf (`ai_suggestion`, kein `exercise_id`) |
|
||||
|
||||
## Kanten
|
||||
|
||||
- `primary(n) → primary(n+1)` — `next_exercise` (nur befüllte Primärkette, lückenlos verbunden)
|
||||
- `primary ↔ sibling` — `sibling` (pro Slot)
|
||||
|
||||
Leere Slots in der Roadmap sind erlaubt; Kanten nur zwischen aufeinanderfolgenden befüllten Primär-Slots.
|
||||
|
||||
## Editor-Zustand (`ProgressionGraphDraft`)
|
||||
|
||||
```ts
|
||||
{
|
||||
goalQuery, startSituation, targetState, roadmapNotes, maxSteps,
|
||||
majorSteps: MajorStep[],
|
||||
slots: Slot[], // index = major_step_index
|
||||
pathSkillExpectations?,
|
||||
lastFindings?, // path_qa-Snapshot
|
||||
dirty: boolean,
|
||||
}
|
||||
```
|
||||
|
||||
**Hydration:** `planning_roadmap` + Kanten → Slots; `slot_contents[]` für Entwürfe; Primärkette aus `next_exercise`.
|
||||
|
||||
**Speichern:** Batch-Delete bestehender Pfad-/Schwester-Kanten → `edges/sequence` (Primärkette) → einzelne `sibling`-Kanten → `PUT`/`sequence` mit Artefakt inkl. `slot_contents`, optional `last_findings`.
|
||||
|
||||
## Findings-Panel
|
||||
|
||||
Nutzt `path_qa` (`overall_ok`, `quality_score`, `issues`, `recommendations`, `gap_fill_offers`, …).
|
||||
|
||||
**API:** `POST /api/planning/progression-path-suggest` mit `evaluate_only: true` und `evaluate_steps[]` — QA ohne Re-Match.
|
||||
|
||||
Persistenz: `planning_roadmap.last_findings`.
|
||||
|
||||
## Artefakt-Erweiterung (`GraphPlanningRoadmapArtifact`)
|
||||
|
||||
Zusätzlich optional:
|
||||
|
||||
- `slot_contents[]` — `{ major_step_index, primary, siblings[] }`
|
||||
- `last_findings` — letzter `path_qa`-Snapshot
|
||||
|
||||
## UI (konsolidiert)
|
||||
|
||||
- **Eine Oberfläche:** `ExerciseProgressionGraphPanel` embeddet `ProgressionGraphEditor` (Slots + Findings)
|
||||
- Kein separater Slot-Editor, kein 4-Schritt-KI-Wizard, kein `ProgressionChainEditor` im Panel
|
||||
- Route `/progression-graphs/:id` → Redirect nach `/exercises` (Deep-Link wählt Graph)
|
||||
- **Phase C:** Übersicht mit Kacheln (Name, Start, Ziel)
|
||||
|
||||
## Ersetzt (Legacy, nicht mehr im Panel)
|
||||
|
||||
- `ExerciseProgressionPathBuilder` · `ProgressionChainEditor` — Code bleibt vorerst, nicht eingebunden
|
||||
|
||||
## Implementierungsreihenfolge
|
||||
|
||||
| ID | Inhalt |
|
||||
|----|--------|
|
||||
| B.0 | Draft + Laden/Speichern Slots ↔ Kanten |
|
||||
| B.1 | Slot-Karten, Bibliothek + Entwurf |
|
||||
| B.2 | Findings-Panel + `evaluate_only` |
|
||||
| B.3 | Entwürfe im Artefakt + „Übung anlegen“ |
|
||||
| B.4 | Route + Panel vereinfachen |
|
||||
| B.5 | `last_findings` + Phase-C-Vorbereitung |
|
||||
43
.claude/docs/working/TRAINING_MODULES_IMPLEMENTATION_PLAN.md
Normal file
43
.claude/docs/working/TRAINING_MODULES_IMPLEMENTATION_PLAN.md
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
# Umsetzungsplan: Trainingsmodule & Kombinationsübungen
|
||||
|
||||
**Bezug:** `functional/Shinkan Trainingsmodule Kombinationsuebungen Spezifikation V2.md` (Kopf „V3“, inkl. **§ 10.2.1**, **§ 10.4 Coaching-Stufen**, **Anhang A** Implementierungsabgleich — Drift-Schutz)
|
||||
**Technische Entwurfsspezifikation:** `technical/TRAINING_MODULES_AND_COMBINATION_EXERCISES_SPEC.md`
|
||||
**Stand dieses Dokuments:** 2026-05-20 (Abgleich mit Code, siehe `backend/version.py`)
|
||||
|
||||
## Ziele
|
||||
|
||||
Umsetzung der MVP-Punkte aus der Fachspezifikation ohne die bestehende Planung zu destabilisieren: schrittweise Migrationen, bestehende Sektions-/Item-Struktur (`training_unit_sections`, `training_unit_section_items`) beibehalten, Kopiersemantik bei Übernahmen.
|
||||
|
||||
## Phasenüberblick
|
||||
|
||||
| Phase | Inhalt | Status |
|
||||
|-------|--------|--------|
|
||||
| **1** | **Trainingsmodule (Bibliothek):** Tabellen `training_modules`, `training_module_items`; REST CRUD mit Governance wie andere Bibliotheken; Übernahme in eine bestehende Einheit per `POST /api/training-units/{id}/apply-training-module` (Anfügen ans Ende eines Abschnitts via `section_order_index`); optionale Lineage-Spalte `source_training_module_id` auf Planungsitems; UI: Liste/Editor unter `/planning/training-modules`, Link von der Planung, Modal „Modul übernehmen“; **Ergänzung 2026-05-20:** Fähigkeiten-Profil + Listen-Filter (Peer-Vergleich nur unter Modulen) — `technical/SKILL_SCORING_SPEC.md` | **umgesetzt (MVP Schritt 1)** |
|
||||
| **2** | Kombinationsübungen: `exercise_kind`/`combination_*`, Slots, Pools, `method_archetype`, `method_profile` (JSON) | **teilweise** — wie links; zusätzlich **057** `planning_method_profile`; Planungs-Merge Client (`effectiveComboMethodProfile`); Archetypen weiterhin **nur Code-Konstanten** (kein Admin) | **Offen:** Archetyp-Admin-UI; Profil↔Archetyp-Validierung Backend; „alle Slots vorbelegen“ / Presets (siehe Fachspez **§ 10.6**); Haupt-/Nebenmethoden an Kombi wo Spec es verlangt |
|
||||
| **3** | Planungsblöcke: Gruppierung, Auflösen, „als Modul speichern“, erweiterter Übernahmemodus (Zwischenposition) | geplant |
|
||||
| **4** | Coaching: Archetyp-Support | **teilweise:** **Stufe A** — Merge Katalog+Planung; `CombinationPlanBracket` in Peek/Run; globale Profilzahlen mit Labels (`describeGlobalComboProfile`); Stations-/Timing-Zusammenfassung inkl. Wdh.-Hinweise. **Stufe B/C** — **offen** (§ 10.6, Anhang A) |
|
||||
| **5** | Rahmenprogramm: Modulübernahme UX in Slot-Blueprint-Editor konsolidieren | geplant |
|
||||
|
||||
## Coaching — verbindliche Arbeitspakete (gegen Spec-Drift)
|
||||
|
||||
| Paket | Spec-Referenz | Kurzinhalt |
|
||||
|-------|----------------|-----------|
|
||||
| **4a (Ist/Ziel)** | § 10.2.1 | Archetyp-Schlüssel bleiben identisch zu `backend/routers/exercises.py` (`COMBINATION_ARCHETYPE_IDS`) und `frontend/src/constants/combinationArchetypes.js`. |
|
||||
| **4b** | § 10.4 Stufe A | **Erreicht (0.8.110):** Slots + Kandidaten; Archetyp-Hilfstext; wirksames Profil lesend mit **fachlichen Labels**; Klammerdarstellung konsistent (`CombinationPlanBracket`, `comboPlanningMethodProfile.js`). |
|
||||
| **4c** | § 10.4 Stufe B | Entscheidung: virtuelle Substeps vs. persistierte Items; Konsistenz `sectionsToPutPayload`/Ist-Zeit. |
|
||||
| **4d** | § 10.4 Stufe C | Archetyp-spezifische Timer/Wechsel/Abhaken an `method_profile` — nach 4c. |
|
||||
| **4e** | § 10.6 | **Archetyp-Verwaltung:** DB/UI oder Konfiguration statt nur Release — Labels, Defaults, ggf. Vereins-/Rollen-Sichtbarkeit. |
|
||||
| **4f** | § 10.6 · `COMBINATION_TIMING_PROFILE_PLAN.md` | **Massen-Vorbelegung:** ein Klick alle Slot-Zeiten/Anzahlen aus Archetyp/Global; Modal „Archetyp-Vorlage anwenden?“ (Phase 2 des Timing-Plans). |
|
||||
| **4g** | § 10.6 | **Backend-Validierung:** Pflichtfelder/Wertebereiche je `method_archetype`; optional serverseitiger Merge mit Katalog (aktuell nur Client). |
|
||||
|
||||
## Phase 1 (technische Notizen)
|
||||
|
||||
- **Governance:** `visibility`/`club_id`/`created_by` analog `training_plan_templates`; Listenfilter `library_content_visibility_sql`.
|
||||
- **Übernahme:** Keine Live-Verknüpfung; Items werden kopiert; `source_training_module_id` dokumentiert Herkunft.
|
||||
- **Schnittstelle Übernahme:** `section_order_index` entspricht der Reihenfolge der Abschnitte in der gespeicherten Einheit (0-basiert), konsistent zur Planungs-API.
|
||||
|
||||
## Pflege nach Merge
|
||||
|
||||
- `DATABASE_SCHEMA.md` bei größeren Schema-Erweiterungen ergänzen.
|
||||
- `ACCESS_LAYER_ENDPOINT_AUDIT.md` bei neuen mandantenbezogenen Endpunkten fortpflegen.
|
||||
- **Nach jeder Kombi-/Coach-Änderung:** `functional/… Spezifikation V2.md` **Anhang A** und diese Tabelle Phasen 2/4 abstimmen.
|
||||
10
.env.example
10
.env.example
|
|
@ -35,6 +35,16 @@ DB_PASSWORD=CHANGE_ME_SECURE_PASSWORD
|
|||
OPENROUTER_API_KEY=your_api_key_here
|
||||
OPENROUTER_MODEL=anthropic/claude-sonnet-4
|
||||
|
||||
# Vereins-Kontingente hart blockieren (KI-Kosten!). Nur 1, true oder yes aktivieren.
|
||||
# Nach Änderung: docker compose -f docker-compose.dev-env.yml up -d backend
|
||||
CLUB_FEATURE_ENFORCE=1
|
||||
# Standard-OpenRouter-Modell (alle Aufrufe). Optional pro Prompt in ai_prompts.openrouter_model
|
||||
# ueberschreibbar (Migration 070, Superadmin unter „KI Prompts“).
|
||||
|
||||
# Übungs-KI (Docker): ohne Eintrag im compose „environment:“ landet keine .env-Zeile im Container.
|
||||
# Hier ist SHINKAN_AI_DEBUG in docker-compose*.yml angebunden — 1 = ausführliche WARN-Logs (exercise_ai, openrouter).
|
||||
# SHINKAN_AI_DEBUG=1
|
||||
|
||||
SMTP_HOST=smtp.example.com
|
||||
SMTP_PORT=587
|
||||
SMTP_USER=noreply@jinkendo.de
|
||||
|
|
|
|||
|
|
@ -18,6 +18,11 @@ jobs:
|
|||
docker compose -f docker-compose.dev-env.yml build --no-cache
|
||||
docker compose -f docker-compose.dev-env.yml up -d
|
||||
sleep 5
|
||||
curl -sf http://localhost:8098/api/version && echo "✓ DEV API healthy"
|
||||
if ! curl -sf http://localhost:8098/api/version; then
|
||||
echo "✗ DEV API nicht erreichbar — Backend-Logs (Migration/Startup):"
|
||||
docker compose -f docker-compose.dev-env.yml logs backend --tail 120 || true
|
||||
exit 1
|
||||
fi
|
||||
echo "✓ DEV API healthy"
|
||||
curl -sf http://localhost:3098/api/version && echo "✓ DEV über Frontend-Nginx (wie Browser) healthy"
|
||||
echo "=== Shinkan DEV Deploy complete ==="
|
||||
|
|
|
|||
|
|
@ -1,24 +1,29 @@
|
|||
name: Test Suite
|
||||
|
||||
# develop: push/PR → Tests gegen Dev (parallel oder vor Deploy Development).
|
||||
# main: kein push/PR-Trigger — vermeidet doppelten Dev-Lauf beim Merge develop→main;
|
||||
# Prod-Tests nur via workflow_run nach erfolgreichem Deploy Production.
|
||||
on:
|
||||
push:
|
||||
branches: [main, develop]
|
||||
branches: [develop]
|
||||
pull_request:
|
||||
branches: [main, develop]
|
||||
branches: [develop]
|
||||
workflow_run:
|
||||
workflows: ["Deploy Development", "Deploy Production"]
|
||||
types: [completed]
|
||||
|
||||
jobs:
|
||||
# Wie Mitai-Jinkendo: pytest im laufenden backend-Container (Python aus Image, gleiche DB wie Deploy).
|
||||
# Pytest im laufenden backend-Container; ACCESS_LAYER + TRAINING_PLANNING Integration gegen dieselbe PostgreSQL wie Deploy (Schema via Container-Start migriert).
|
||||
pytest-backend:
|
||||
if: ${{ github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Backend pytest im deployten Container
|
||||
run: |
|
||||
set -e
|
||||
EVENT_NAME="${{ github.event_name }}"
|
||||
REF_NAME="${{ github.ref_name }}"
|
||||
BASE_REF="${{ github.base_ref }}"
|
||||
RUN_WORKFLOW="${{ github.event.workflow_run.name }}"
|
||||
APP_DIR="/home/lars/docker/shinkan"
|
||||
COMPOSE_FILE="docker-compose.yml"
|
||||
|
|
@ -28,18 +33,33 @@ jobs:
|
|||
APP_DIR="/home/lars/docker/shinkan-dev"
|
||||
COMPOSE_FILE="docker-compose.dev-env.yml"
|
||||
fi
|
||||
elif [ "$REF_NAME" = "develop" ]; then
|
||||
elif [ "$REF_NAME" = "develop" ] || [ "$BASE_REF" = "develop" ]; then
|
||||
APP_DIR="/home/lars/docker/shinkan-dev"
|
||||
COMPOSE_FILE="docker-compose.dev-env.yml"
|
||||
fi
|
||||
|
||||
cd "$APP_DIR"
|
||||
echo "Warte auf stabilen backend-Container …"
|
||||
for i in $(seq 1 60); do
|
||||
if docker compose -f "$COMPOSE_FILE" exec -T backend true 2>/dev/null; then
|
||||
echo "Backend bereit (Versuch $i)"
|
||||
break
|
||||
fi
|
||||
if [ "$i" -eq 60 ]; then
|
||||
echo "Timeout: backend-Container nicht bereit"
|
||||
docker compose -f "$COMPOSE_FILE" ps || true
|
||||
docker compose -f "$COMPOSE_FILE" logs backend --tail 80 || true
|
||||
exit 1
|
||||
fi
|
||||
sleep 5
|
||||
done
|
||||
|
||||
docker compose -f "$COMPOSE_FILE" exec -T backend sh -lc "
|
||||
pip install -r /app/requirements-dev.txt &&
|
||||
cd /app &&
|
||||
ACCESS_LAYER_STRICT=1 python scripts/check_access_layer_hints.py &&
|
||||
python scripts/security_release_checks.py &&
|
||||
ACCESS_LAYER_INTEGRATION=1 SKIP_DB_MIGRATE=1 python -m pytest tests -m 'not slow' -ra -vv --tb=short
|
||||
ACCESS_LAYER_INTEGRATION=1 TRAINING_PLANNING_INTEGRATION=1 SKIP_DB_MIGRATE=1 python -m pytest tests -m 'not slow' -ra -vv --tb=short
|
||||
"
|
||||
|
||||
lint-backend:
|
||||
|
|
@ -88,6 +108,90 @@ jobs:
|
|||
npm run build
|
||||
echo "✓ Frontend build OK"
|
||||
|
||||
# Phase-0 Lastsmoke: nur k6 — eigener Job (kein Node/Playwright), klare CI-Zuordnung.
|
||||
k6-health-baseline:
|
||||
name: k6 /health Baseline
|
||||
if: ${{ github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success' }}
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
E2E_TARGET_URL: https://dev.shinkan.jinkendo.de
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: E2E-Ziel wählen (Dev über Proxy vs. Production)
|
||||
id: e2e
|
||||
run: |
|
||||
EVENT="${{ github.event_name }}"
|
||||
WF_NAME="${{ github.event.workflow_run.name }}"
|
||||
DEV_BASE="${{ env.E2E_TARGET_URL }}"
|
||||
if [ "$EVENT" = "workflow_run" ] && [ "$WF_NAME" = "Deploy Production" ]; then
|
||||
echo "mode=prod" >> $GITHUB_OUTPUT
|
||||
echo "base_url=https://shinkan.jinkendo.de" >> $GITHUB_OUTPUT
|
||||
echo "→ k6 gegen Prod-Basis."
|
||||
else
|
||||
echo "mode=dev" >> $GITHUB_OUTPUT
|
||||
echo "base_url=${DEV_BASE}" >> $GITHUB_OUTPUT
|
||||
echo "→ k6 gegen Dev (${DEV_BASE})."
|
||||
fi
|
||||
|
||||
- name: Dev /health abwarten
|
||||
if: ${{ steps.e2e.outputs.mode == 'dev' }}
|
||||
run: |
|
||||
BASE="${{ steps.e2e.outputs.base_url }}"
|
||||
echo "Warte auf $BASE/health …"
|
||||
for i in $(seq 1 90); do
|
||||
if curl -sf "$BASE/health" >/dev/null 2>&1; then
|
||||
echo "Health OK (Versuch $i)"
|
||||
exit 0
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
echo "Timeout: Dev /health nicht erreichbar — Deploy / DNS / Firewall prüfen."
|
||||
curl -v "$BASE/health" || true
|
||||
exit 1
|
||||
|
||||
- name: Prod /health abwarten
|
||||
if: ${{ steps.e2e.outputs.mode == 'prod' }}
|
||||
run: |
|
||||
BASE="${{ steps.e2e.outputs.base_url }}"
|
||||
echo "Warte auf $BASE/health …"
|
||||
for i in $(seq 1 60); do
|
||||
if curl -sf "$BASE/health" >/dev/null 2>&1; then
|
||||
echo "Health OK (Versuch $i)"
|
||||
exit 0
|
||||
fi
|
||||
sleep 5
|
||||
done
|
||||
echo "Timeout: Prod /health nicht erreichbar"
|
||||
curl -v "$BASE/health" || true
|
||||
exit 1
|
||||
|
||||
- name: Install k6
|
||||
run: |
|
||||
set -e
|
||||
K6_VER="v0.55.0"
|
||||
ARCH=$(uname -m)
|
||||
case "$ARCH" in
|
||||
x86_64) K6_ARCH=amd64 ;;
|
||||
aarch64|arm64) K6_ARCH=arm64 ;;
|
||||
*) echo "k6: unbekannte Architektur: $ARCH"; exit 1 ;;
|
||||
esac
|
||||
echo "Installing k6 ${K6_VER} linux-${K6_ARCH}"
|
||||
curl -sSL "https://github.com/grafana/k6/releases/download/${K6_VER}/k6-${K6_VER}-linux-${K6_ARCH}.tar.gz" -o /tmp/k6.tgz
|
||||
tar -xzf /tmp/k6.tgz -C /tmp
|
||||
sudo mv "/tmp/k6-${K6_VER}-linux-${K6_ARCH}/k6" /usr/local/bin/k6
|
||||
k6 version
|
||||
|
||||
- name: k6 Health-Baseline (parallele /health)
|
||||
env:
|
||||
BASE_URL: ${{ steps.e2e.outputs.base_url }}
|
||||
run: |
|
||||
set -e
|
||||
echo "k6 gegen BASE_URL=$BASE_URL"
|
||||
k6 run scripts/load/k6-health-baseline.js
|
||||
echo "✓ k6 Health-Baseline passed"
|
||||
|
||||
playwright-tests:
|
||||
if: ${{ github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success' }}
|
||||
runs-on: ubuntu-latest
|
||||
|
|
|
|||
|
|
@ -12,7 +12,13 @@
|
|||
> | Setup-Dokument | `.claude/docs/working/SHINKAN_PROJECT_SETUP.md` |
|
||||
> | Anforderungen | `.claude/docs/functional/SHINKAN_REQUIREMENTS.md` |
|
||||
> | Medien-Archiv, Lifecycle, Inline (Plan §11) | `.claude/docs/technical/MEDIA_ASSETS_AND_ARCHIVE_SPEC.md` |
|
||||
> | Fähigkeiten-Scoring (Planungs-Bausteine) | `.claude/docs/technical/SKILL_SCORING_SPEC.md` |
|
||||
> | Handover / nächste Session | **`docs/HANDOVER.md`** |
|
||||
> | Fachlicher Nutzerüberblick (Design/Product) | **`docs/FACHLICHE_NUTZERFUNKTIONEN.md`** |
|
||||
> | Architektur-Zielbild, Refaktor-Roadmap, verbindliche Shinkan-Regeln | **`docs/architecture/README.md`** |
|
||||
> | Performance-Baseline (Phase 0) | **`docs/architecture/BASELINE_SNAPSHOT.md`** |
|
||||
> | KI-Prompt-System — Zielarchitektur | `.claude/docs/technical/AI_PROMPT_TARGET_ARCHITECTURE.md` |
|
||||
> | Planungs-KI Progressionsgraph (Ist-Stand) | **`docs/architecture/PLANNING_PROGRESSION_GRAPH_KI.md`** · Spec **`.claude/docs/working/PLANNING_PROGRESSION_ROADMAP_SPEC.md`** · Roadmap **`docs/architecture/PLANNING_KI_ROADMAP.md`** |
|
||||
|
||||
## Projekt-Übersicht
|
||||
|
||||
|
|
@ -83,10 +89,11 @@ frontend/src/
|
|||
|
||||
**Siehe:** `backend/version.py` (`APP_VERSION`, `DB_SCHEMA_VERSION`, `MODULE_VERSIONS`) und `.claude/docs/PROJECT_STATUS.md`.
|
||||
|
||||
Kurz (Stand 2026-05-08): App **0.8.64**, DB‑Schema‑Version siehe **`backend/version.py`**; Kern: Übungen, Varianten, **Medien-Archiv & Bibliothek (`/media`)**, **Inline-Medien im Rich-Text** (Modal-Picker, Größenwahl, Drag&Drop + Auto-Scroll), Mandanten-Sync aktiver Verein, Planung mit Sektionen, **Trainingsrahmen Bibliothek + Slot‑Blueprint** (036–037), Progressionsgraph, Reifegrad/Matrix‑Stack — Details `PROJECT_STATUS.md`, `docs/HANDOVER.md`, `MEDIA_ASSETS_AND_ARCHIVE_SPEC.md` (§11 umgesetzt).
|
||||
Kurz (Stand 2026-05-14): App- und DB-Version siehe **`backend/version.py`**; Kern: Übungen, Varianten, **Medien-Archiv & Bibliothek (`/media`)**, **Inline-Medien im Rich-Text**, **Inhaltsmeldungen (P-13)** im Posteingang, Mandanten-Sync aktiver Verein, Planung mit **Phasen & parallelen Streams (Breakout, 063)**, **Trainingsrahmen Bibliothek + Slot‑Blueprint** (036–037), Progressionsgraph, Reifegrad/Matrix‑Stack — Details `PROJECT_STATUS.md`, `docs/HANDOVER.md`, Nutzerüberblick **`docs/FACHLICHE_NUTZERFUNKTIONEN.md`**, `PARALLEL_TRAINING_STREAMS_SPEC.md`, `MEDIA_ASSETS_AND_ARCHIVE_SPEC.md` (Abschnitt 11 umgesetzt).
|
||||
|
||||
### Log (Auszug)
|
||||
|
||||
- 2026-05-20: **Fähigkeiten-Scoring Phase 3** — gewichtete Profile für Module/Rahmen/Pfade; Peer-Vergleich getrennt nach Artefakttyp; Listen-Filter + Discovery — siehe `SKILL_SCORING_SPEC.md`, `docs/HANDOVER.md` §2.6, `FEATURES_DELIVERED_2026-Q2.md` §15.
|
||||
- 2026-05-07: **Medien** — zentrales Archiv (`media_assets`), Bibliothek-UI, Lifecycle/Papierkorb, `from-asset`, Speicherpfade `library/…`, Governance `official`/Copyright; **0.8.59** aktiver Verein UI/API-Sync — siehe `.claude/docs/library/FEATURES_DELIVERED_2026-Q2.md` §12, `docs/HANDOVER.md`.
|
||||
- 2026-05-05: Rahmen nur Bibliothek (**036**), Slot‑Ablauf = `training_units` + Sektionen (**037**), `POST /api/training-units/from-framework-slot`, keine `training_framework_slot_exercises` mehr — siehe `DATABASE_SCHEMA.md` / `FEATURES_DELIVERED_2026-Q2.md`.
|
||||
- 2026-04-27: Übungsvarianten API/UI, Migration 030, Listen-UX-Suche, Admin-Upload-Limits — siehe `PROJECT_STATUS.md` und `docs/library/FEATURES_DELIVERED_2026-Q2.md`.
|
||||
|
|
|
|||
|
|
@ -2,14 +2,16 @@ FROM python:3.12-slim
|
|||
|
||||
WORKDIR /app
|
||||
|
||||
# Install system dependencies
|
||||
# Install system dependencies (tzdata für zoneinfo/ZoneInfo unter Linux)
|
||||
RUN apt-get update && apt-get install -y \
|
||||
postgresql-client \
|
||||
tzdata \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Copy requirements and install dependencies
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
ENV PIP_DEFAULT_TIMEOUT=120
|
||||
RUN pip install --no-cache-dir --retries 5 -r requirements.txt
|
||||
|
||||
# Copy application code
|
||||
COPY . .
|
||||
|
|
|
|||
77
backend/account_lifecycle.py
Normal file
77
backend/account_lifecycle.py
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
"""
|
||||
Account-Lifecycle (CAPABILITY_CATALOG.v1.md §3, M3 C0).
|
||||
|
||||
Zustände: unverified → verified_pending_club → active_member; platform_admin separat.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from fastapi import HTTPException
|
||||
|
||||
from club_tenancy import is_platform_admin
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from tenant_context import TenantContext
|
||||
|
||||
_ACCOUNT_STATE_RANK = {
|
||||
"unverified": 1,
|
||||
"verified_pending_club": 2,
|
||||
"active_member": 3,
|
||||
"platform_admin": 4,
|
||||
}
|
||||
|
||||
|
||||
def resolve_account_state(
|
||||
*,
|
||||
email_verified: bool,
|
||||
global_role: str,
|
||||
has_active_membership: bool,
|
||||
) -> str:
|
||||
"""Ermittelt account_state für ein Profil."""
|
||||
if is_platform_admin(global_role):
|
||||
return "platform_admin"
|
||||
if not email_verified:
|
||||
return "unverified"
|
||||
if not has_active_membership:
|
||||
return "verified_pending_club"
|
||||
return "active_member"
|
||||
|
||||
|
||||
def account_state_satisfies(current: str, required: str) -> bool:
|
||||
"""True wenn current mindestens required ist."""
|
||||
cur = _ACCOUNT_STATE_RANK.get(current, 0)
|
||||
req = _ACCOUNT_STATE_RANK.get(required, 99)
|
||||
if current == "platform_admin":
|
||||
return True
|
||||
return cur >= req
|
||||
|
||||
|
||||
def account_gate_enforcement_enabled() -> bool:
|
||||
"""Account-Gates aktiv (Default an — nur wenige Endpoints in M3)."""
|
||||
return os.getenv("ACCOUNT_GATE_ENFORCE", "1").strip() == "1"
|
||||
|
||||
|
||||
def assert_min_account_state(
|
||||
tenant: "TenantContext",
|
||||
min_state: str,
|
||||
*,
|
||||
endpoint: Optional[str] = None,
|
||||
) -> None:
|
||||
"""
|
||||
Prüft Mindest-Account-Status. Wirft 403 wenn ACCOUNT_GATE_ENFORCE=1 (Default).
|
||||
"""
|
||||
current = getattr(tenant, "account_state", "active_member")
|
||||
ok = account_state_satisfies(current, min_state)
|
||||
if ok:
|
||||
return
|
||||
if not account_gate_enforcement_enabled():
|
||||
return
|
||||
detail = (
|
||||
f"Account-Status „{current}“ reicht nicht für diese Aktion "
|
||||
f"(erforderlich: {min_state})."
|
||||
)
|
||||
if endpoint:
|
||||
detail = f"{detail} ({endpoint})"
|
||||
raise HTTPException(status_code=403, detail=detail)
|
||||
178
backend/account_onboarding_gate.py
Normal file
178
backend/account_onboarding_gate.py
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
"""
|
||||
API-Gates für Onboarding (Phase A — MEMBERSHIP_RBAC_DECISIONS_2026-06.md §1.1).
|
||||
|
||||
Blockiert Domänen-APIs für unverified / verified_pending_club vor dem Router.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import re
|
||||
from typing import Optional, Tuple
|
||||
|
||||
from account_lifecycle import resolve_account_state
|
||||
from club_tenancy import memberships_with_roles
|
||||
|
||||
# Öffentlich ohne Session
|
||||
PUBLIC_API_PREFIXES = (
|
||||
"/api/auth/login",
|
||||
"/api/auth/register",
|
||||
"/api/auth/forgot-password",
|
||||
"/api/auth/reset-password",
|
||||
"/api/auth/verify/",
|
||||
"/api/legal-documents/",
|
||||
"/api/clubs/public-directory",
|
||||
"/api/version",
|
||||
"/api/health/",
|
||||
"/health",
|
||||
)
|
||||
|
||||
# Mit Session, unabhängig vom account_state (Logout, Profil lesen, …)
|
||||
AUTH_INFRA_PREFIXES = (
|
||||
"/api/auth/logout",
|
||||
"/api/auth/me",
|
||||
"/api/auth/status",
|
||||
"/api/auth/pin",
|
||||
"/api/auth/resend-verification",
|
||||
"/api/profiles/me",
|
||||
"/api/me/entitlements",
|
||||
)
|
||||
|
||||
# Zusätzlich für verified_pending_club (Verein bewerben)
|
||||
PENDING_CLUB_PREFIXES = (
|
||||
"/api/me/club-join-requests",
|
||||
"/api/me/club-creation-requests",
|
||||
)
|
||||
|
||||
_PROFILE_MUTATION_RE = re.compile(r"^/api/profiles/(\d+)$")
|
||||
|
||||
|
||||
def api_onboarding_gate_enabled() -> bool:
|
||||
"""Produktions-Gate aktiv (ACCOUNT_GATE_API_ENFORCE=0 zum Abschalten)."""
|
||||
return os.getenv("ACCOUNT_GATE_API_ENFORCE", "1").strip() == "1"
|
||||
|
||||
|
||||
def _middleware_db_lookup_enabled() -> bool:
|
||||
"""
|
||||
Middleware-Session-Lookup nur mit echter DB (nicht in pytest TestClient ohne Postgres).
|
||||
"""
|
||||
if os.getenv("SKIP_DB_MIGRATE", "").strip().lower() in ("1", "true", "yes"):
|
||||
return False
|
||||
if os.getenv("PYTEST_CURRENT_TEST"):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def normalize_api_path(path: str) -> str:
|
||||
p = (path or "").split("?", 1)[0].strip()
|
||||
if not p.startswith("/"):
|
||||
p = "/" + p
|
||||
if len(p) > 1 and p.endswith("/"):
|
||||
p = p[:-1]
|
||||
return p
|
||||
|
||||
|
||||
def is_public_api_path(path: str) -> bool:
|
||||
p = normalize_api_path(path)
|
||||
return any(p == pref or p.startswith(pref) for pref in PUBLIC_API_PREFIXES)
|
||||
|
||||
|
||||
def _path_allowed_for_state(path: str, method: str, account_state: str, profile_id: int) -> bool:
|
||||
p = normalize_api_path(path)
|
||||
m = (method or "GET").upper()
|
||||
|
||||
for pref in AUTH_INFRA_PREFIXES:
|
||||
if p == pref or p.startswith(pref + "/"):
|
||||
return True
|
||||
|
||||
match = _PROFILE_MUTATION_RE.match(p)
|
||||
if match and m in ("PUT", "PATCH") and int(match.group(1)) == int(profile_id):
|
||||
return True
|
||||
|
||||
if account_state == "unverified":
|
||||
return False
|
||||
|
||||
if account_state == "verified_pending_club":
|
||||
for pref in PENDING_CLUB_PREFIXES:
|
||||
if p == pref or p.startswith(pref + "/"):
|
||||
return True
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def resolve_account_state_for_token(cur, session_row: dict) -> str:
|
||||
profile_id = int(session_row["profile_id"])
|
||||
role = (session_row.get("role") or "").lower()
|
||||
cur.execute(
|
||||
"SELECT COALESCE(email_verified, false) AS email_verified FROM profiles WHERE id = %s",
|
||||
(profile_id,),
|
||||
)
|
||||
prof = cur.fetchone()
|
||||
email_verified = bool(prof.get("email_verified")) if prof else False
|
||||
memberships = memberships_with_roles(cur, profile_id, active_only=True)
|
||||
has_active = len(memberships) > 0
|
||||
return resolve_account_state(
|
||||
email_verified=email_verified,
|
||||
global_role=role,
|
||||
has_active_membership=has_active,
|
||||
)
|
||||
|
||||
|
||||
def check_api_onboarding_gate(
|
||||
*,
|
||||
path: str,
|
||||
method: str,
|
||||
profile_id: int,
|
||||
account_state: str,
|
||||
) -> Tuple[bool, Optional[str]]:
|
||||
"""
|
||||
Returns (allowed, reason).
|
||||
active_member / platform_admin → immer erlaubt (Domain).
|
||||
"""
|
||||
if not api_onboarding_gate_enabled():
|
||||
return True, None
|
||||
|
||||
if account_state in ("active_member", "platform_admin"):
|
||||
return True, None
|
||||
|
||||
if _path_allowed_for_state(path, method, account_state, profile_id):
|
||||
return True, None
|
||||
|
||||
return False, f"account_state_{account_state}"
|
||||
|
||||
|
||||
def evaluate_request_gate(token: Optional[str], path: str, method: str) -> Tuple[bool, Optional[str], Optional[str]]:
|
||||
"""
|
||||
Vollständige Prüfung inkl. Session-Lookup.
|
||||
Returns: allowed, reason, account_state (für Logging)
|
||||
"""
|
||||
if not api_onboarding_gate_enabled() or not _middleware_db_lookup_enabled():
|
||||
return True, None, None
|
||||
|
||||
p = normalize_api_path(path)
|
||||
if not p.startswith("/api/"):
|
||||
return True, None, None
|
||||
if is_public_api_path(p):
|
||||
return True, None, None
|
||||
if not token:
|
||||
return True, None, None
|
||||
|
||||
from auth import get_session
|
||||
from db import get_db, get_cursor
|
||||
|
||||
session = get_session(token)
|
||||
if not session:
|
||||
return True, None, None
|
||||
|
||||
profile_id = int(session["profile_id"])
|
||||
with get_db() as conn:
|
||||
cur = get_cursor(conn)
|
||||
account_state = resolve_account_state_for_token(cur, session)
|
||||
|
||||
allowed, reason = check_api_onboarding_gate(
|
||||
path=p,
|
||||
method=method,
|
||||
profile_id=profile_id,
|
||||
account_state=account_state,
|
||||
)
|
||||
return allowed, reason, account_state
|
||||
108
backend/ai_prompt_context.py
Normal file
108
backend/ai_prompt_context.py
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
"""
|
||||
Gemeinsame Pydantic-Modelle fuer Uebungs-KI-Kontext (Formularfelder → Prompt-Platzhalter).
|
||||
|
||||
Keine Imports aus exercise_ai — vermeidet Zirkelimporte mit ai_prompt_job / exercise_ai.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Dict, List, Optional, Sequence, Tuple
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class ExerciseFormAiFocusRow(BaseModel):
|
||||
"""Fokusbereich fuer Skill-Retrieval (ai_skill_retrieval_profiles)."""
|
||||
|
||||
focus_area_id: int = Field(..., ge=1)
|
||||
is_primary: Optional[bool] = False
|
||||
|
||||
|
||||
class ExerciseFormAiPromptContext(BaseModel):
|
||||
"""
|
||||
Inhaltliche Eingabe fuer Uebungs-Prompts (Kurzfassung / Skills / Anleitung).
|
||||
|
||||
Wird genutzt von Admin-Prompt-Vorschau und POST /exercises/ai/suggest (via Mapping).
|
||||
"""
|
||||
|
||||
title: Optional[str] = ""
|
||||
goal: Optional[str] = None
|
||||
execution: Optional[str] = None
|
||||
preparation: Optional[str] = None
|
||||
trainer_notes: Optional[str] = None
|
||||
focus_hint: Optional[str] = None
|
||||
focus_areas_context: Optional[List[ExerciseFormAiFocusRow]] = None
|
||||
planning_context: Optional[Dict[str, Any]] = None
|
||||
|
||||
def focus_area_tuples(self) -> Optional[List[Tuple[int, bool]]]:
|
||||
if not self.focus_areas_context:
|
||||
return None
|
||||
return [(int(x.focus_area_id), bool(x.is_primary)) for x in self.focus_areas_context]
|
||||
|
||||
def has_instruction_source_text(self) -> bool:
|
||||
"""Mindestens ein Anleitungsfeld oder Titel fuer instruction_rewrite."""
|
||||
if (self.title or "").strip():
|
||||
return True
|
||||
for val in (self.goal, self.execution, self.preparation, self.trainer_notes):
|
||||
if val and str(val).strip():
|
||||
return True
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def from_api_suggest(
|
||||
cls,
|
||||
*,
|
||||
title: Optional[str] = None,
|
||||
goal: Optional[str] = None,
|
||||
execution: Optional[str] = None,
|
||||
preparation: Optional[str] = None,
|
||||
trainer_notes: Optional[str] = None,
|
||||
focus_area_hint: Optional[str] = None,
|
||||
focus_areas_context: Optional[Sequence[ExerciseFormAiFocusRow]] = None,
|
||||
planning_context: Optional[Dict[str, Any]] = None,
|
||||
) -> ExerciseFormAiPromptContext:
|
||||
"""Mappt Felder aus POST /exercises/ai/suggest (focus_area_hint → focus_hint)."""
|
||||
hint = (focus_area_hint or "").strip() or None
|
||||
return cls(
|
||||
title=(title or "").strip(),
|
||||
goal=goal,
|
||||
execution=execution,
|
||||
preparation=preparation,
|
||||
trainer_notes=trainer_notes,
|
||||
focus_hint=hint,
|
||||
focus_areas_context=list(focus_areas_context) if focus_areas_context else None,
|
||||
planning_context=dict(planning_context) if planning_context else None,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_focus_tuples(
|
||||
cls,
|
||||
*,
|
||||
title: str = "",
|
||||
goal: Optional[str] = None,
|
||||
execution: Optional[str] = None,
|
||||
preparation: Optional[str] = None,
|
||||
trainer_notes: Optional[str] = None,
|
||||
focus_hint: Optional[str] = None,
|
||||
focus_tuples: Optional[Sequence[Tuple[int, bool]]] = None,
|
||||
) -> ExerciseFormAiPromptContext:
|
||||
rows = None
|
||||
if focus_tuples:
|
||||
rows = [
|
||||
ExerciseFormAiFocusRow(focus_area_id=int(fid), is_primary=bool(prim))
|
||||
for fid, prim in focus_tuples
|
||||
]
|
||||
return cls(
|
||||
title=(title or "").strip(),
|
||||
goal=goal,
|
||||
execution=execution,
|
||||
preparation=preparation,
|
||||
trainer_notes=trainer_notes,
|
||||
focus_hint=(focus_hint or "").strip() or None,
|
||||
focus_areas_context=rows,
|
||||
)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"ExerciseFormAiFocusRow",
|
||||
"ExerciseFormAiPromptContext",
|
||||
]
|
||||
59
backend/ai_prompt_job.py
Normal file
59
backend/ai_prompt_job.py
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
"""
|
||||
KI-Prompt Jobs: Resolver + oeffentliche Fassade fuer Uebungs-KI-Aufrufe.
|
||||
|
||||
Importiert exercise_ai fuer Platzhalter-Builder und OpenRouter-Orchestrierung.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Dict
|
||||
|
||||
from ai_prompt_context import ExerciseFormAiFocusRow, ExerciseFormAiPromptContext
|
||||
from exercise_ai import build_exercise_placeholder_variables
|
||||
|
||||
|
||||
def resolve_exercise_form_variables(cur, slug: str, ctx: ExerciseFormAiPromptContext) -> Dict[str, str]:
|
||||
"""Baut die Mustache-Map fuer exercise_summary / exercise_skill_suggestions."""
|
||||
return build_exercise_placeholder_variables(
|
||||
cur,
|
||||
slug=slug,
|
||||
title=(ctx.title or "").strip(),
|
||||
goal=ctx.goal,
|
||||
execution=ctx.execution,
|
||||
focus_area_hint=ctx.focus_hint,
|
||||
focus_areas_context=ctx.focus_area_tuples(),
|
||||
preparation=ctx.preparation,
|
||||
trainer_notes=ctx.trainer_notes,
|
||||
planning_context=ctx.planning_context,
|
||||
)
|
||||
|
||||
|
||||
def run_exercise_form_ai_suggestion(
|
||||
cur,
|
||||
ctx: ExerciseFormAiPromptContext,
|
||||
*,
|
||||
want_summary: bool,
|
||||
want_skills: bool,
|
||||
want_instructions: bool = False,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Fuehrt Uebungs-KI aus (OpenRouter) — ein Einstieg fuer Router und kuenftige Jobs.
|
||||
|
||||
``ctx`` = Formularinhalt; ``want_*`` = welche Prompt-Slugs angefragt werden.
|
||||
"""
|
||||
from exercise_ai import run_exercise_ai_suggestion
|
||||
|
||||
return run_exercise_ai_suggestion(
|
||||
cur,
|
||||
form_ctx=ctx,
|
||||
want_summary=want_summary,
|
||||
want_skills=want_skills,
|
||||
want_instructions=want_instructions,
|
||||
)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"ExerciseFormAiFocusRow",
|
||||
"ExerciseFormAiPromptContext",
|
||||
"resolve_exercise_form_variables",
|
||||
"run_exercise_form_ai_suggestion",
|
||||
]
|
||||
125
backend/ai_prompt_runtime.py
Normal file
125
backend/ai_prompt_runtime.py
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
"""
|
||||
Gemeinsame KI-Prompt-Laufzeit (Shinkan): DB-Lesezugriff ai_prompts + Kontext-Arten.
|
||||
|
||||
Bleibt ohne Import von exercise_ai (kein Zirkel). Domänen wie exercise_ai nutzen
|
||||
load_ai_prompt_row und die Enum; Platzhalter bauen sie selbst oder über geteilte Builder.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from enum import Enum
|
||||
from typing import Any, Dict, Mapping, Optional, Tuple
|
||||
|
||||
from prompt_resolver import MustacheRenderResult, render_mustache_template
|
||||
|
||||
_PLANNING_AI_SLUGS = frozenset(
|
||||
{
|
||||
"planning_exercise_search_rank",
|
||||
"planning_exercise_search_intent",
|
||||
"planning_exercise_expectation_profile",
|
||||
}
|
||||
)
|
||||
|
||||
_EXERCISE_AI_SLUGS = frozenset(
|
||||
{
|
||||
"exercise_summary",
|
||||
"exercise_skill_suggestions",
|
||||
"exercise_instruction_rewrite",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class AiPromptContextKind(str, Enum):
|
||||
"""
|
||||
Logischer Kontext fuer Platzhalter/Builder — erweiterbar fuer Planung/Rahmen
|
||||
ohne bestehende Slugs zu invalidieren.
|
||||
"""
|
||||
|
||||
PLANNING_EXERCISE_SEARCH = "planning_exercise_search"
|
||||
EXERCISE_FORM_AI = "exercise_form_ai"
|
||||
|
||||
|
||||
def context_kind_for_slug(slug: str) -> Optional[AiPromptContextKind]:
|
||||
"""Ordnet einen DB-Slug einer Kontext-Art zu, sofern registriert."""
|
||||
s = (slug or "").strip().lower()
|
||||
if s in _PLANNING_AI_SLUGS:
|
||||
return AiPromptContextKind.PLANNING_EXERCISE_SEARCH
|
||||
if s in _EXERCISE_AI_SLUGS:
|
||||
return AiPromptContextKind.EXERCISE_FORM_AI
|
||||
return None
|
||||
|
||||
|
||||
def load_ai_prompt_row(cur, slug: str, *, active_only: bool = True) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Laedt eine Zeile ai_prompts fuer Laufzeit-Orchestrierung.
|
||||
|
||||
active_only=True: inaktive Prompts werden wie fehlend behandelt (503 im Aufrufer).
|
||||
"""
|
||||
if active_only:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT slug, display_name, template, output_format, active, openrouter_model
|
||||
FROM ai_prompts
|
||||
WHERE slug = %s AND active = true
|
||||
""",
|
||||
(slug,),
|
||||
)
|
||||
else:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT slug, display_name, template, output_format, active, openrouter_model
|
||||
FROM ai_prompts
|
||||
WHERE slug = %s
|
||||
""",
|
||||
(slug,),
|
||||
)
|
||||
row = cur.fetchone()
|
||||
if not row:
|
||||
return None
|
||||
d = dict(row)
|
||||
if active_only and not d.get("active", True):
|
||||
return None
|
||||
return d
|
||||
|
||||
|
||||
class AiPromptUnavailableError(LookupError):
|
||||
"""Kein aktiver Prompt fuer slug (oder Zeile fehlt)."""
|
||||
|
||||
def __init__(self, slug: str) -> None:
|
||||
self.slug = (slug or "").strip()
|
||||
super().__init__(self.slug)
|
||||
|
||||
|
||||
def render_ai_prompt_template_for_row(
|
||||
row: Mapping[str, Any],
|
||||
variables: Mapping[str, str],
|
||||
) -> MustacheRenderResult:
|
||||
"""Ersetzt Platzhalter anhand einer bereits geladenen ai_prompts-Zeile (z. B. Admin-Vorschauch, inkl. inaktiv)."""
|
||||
return render_mustache_template(str(row.get("template") or ""), variables)
|
||||
|
||||
|
||||
def load_and_render_ai_prompt(
|
||||
cur,
|
||||
slug: str,
|
||||
variables: Mapping[str, str],
|
||||
*,
|
||||
active_only: bool = True,
|
||||
) -> Tuple[Dict[str, Any], MustacheRenderResult]:
|
||||
"""
|
||||
Laedt einen aktiven Prompt und wendet Mustache-Variablen an.
|
||||
Wirft AiPromptUnavailableError, wenn die Zeile fehlt oder (bei active_only) inaktiv ist.
|
||||
"""
|
||||
row = load_ai_prompt_row(cur, slug, active_only=active_only)
|
||||
if not row:
|
||||
raise AiPromptUnavailableError(slug)
|
||||
rr = render_ai_prompt_template_for_row(row, variables)
|
||||
return dict(row), rr
|
||||
|
||||
|
||||
__all__ = [
|
||||
"AiPromptContextKind",
|
||||
"AiPromptUnavailableError",
|
||||
"context_kind_for_slug",
|
||||
"load_ai_prompt_row",
|
||||
"load_and_render_ai_prompt",
|
||||
"render_ai_prompt_template_for_row",
|
||||
]
|
||||
|
|
@ -170,6 +170,10 @@ def get_effective_tier(profile_id: str, conn=None) -> str:
|
|||
|
||||
def check_feature_access(profile_id: str, feature_id: str, conn=None) -> dict:
|
||||
"""
|
||||
DEPRECATED für Shinkan: Mitai-v9c-Profil-Limits — Schema 001 ist archiviert (Migration 078).
|
||||
|
||||
Für Vereins-Kontingente: club_features.check_club_feature_access(club_id, feature_id).
|
||||
|
||||
Check if a profile has access to a feature.
|
||||
|
||||
Access hierarchy:
|
||||
|
|
@ -315,6 +319,8 @@ def _check_impl(profile_id: str, feature_id: str, conn) -> dict:
|
|||
|
||||
def increment_feature_usage(profile_id: str, feature_id: str) -> None:
|
||||
"""
|
||||
DEPRECATED für Shinkan — siehe club_features.increment_club_feature_usage.
|
||||
|
||||
Increment usage counter for a feature.
|
||||
|
||||
Creates usage record if it doesn't exist, with reset_at based on
|
||||
|
|
|
|||
285
backend/capabilities.py
Normal file
285
backend/capabilities.py
Normal file
|
|
@ -0,0 +1,285 @@
|
|||
"""
|
||||
Capability-Auflösung (CAPABILITY_CATALOG.v1.md, M3 C1).
|
||||
|
||||
Phase 2: probe_capability — JSON-Log, kein Block (CAPABILITY_ENFORCE=0).
|
||||
Phase 3+: CAPABILITY_ENFORCE=1 — HTTP 403 bei fehlender Capability.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from typing import Any, Dict, List, Optional, TYPE_CHECKING
|
||||
|
||||
from fastapi import HTTPException
|
||||
|
||||
from account_lifecycle import account_state_satisfies
|
||||
from club_tenancy import is_platform_admin
|
||||
from db import get_db, get_cursor
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from tenant_context import TenantContext
|
||||
|
||||
|
||||
def capability_enforcement_enabled() -> bool:
|
||||
v = os.getenv("CAPABILITY_ENFORCE", "0").strip().lower()
|
||||
return v in ("1", "true", "yes")
|
||||
|
||||
|
||||
def club_roles_in_club(tenant: "TenantContext", club_id: Optional[int]) -> List[str]:
|
||||
if club_id is None:
|
||||
return []
|
||||
for m in tenant.memberships or []:
|
||||
if int(m.get("id") or 0) == int(club_id):
|
||||
roles = m.get("roles") or []
|
||||
if hasattr(roles, "tolist"):
|
||||
roles = roles.tolist()
|
||||
return list(roles)
|
||||
return []
|
||||
|
||||
|
||||
def check_capability(
|
||||
cur,
|
||||
tenant: "TenantContext",
|
||||
capability_id: str,
|
||||
*,
|
||||
club_id: Optional[int] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Prüft eine Capability für Tenant + optionalen Vereinskontext.
|
||||
|
||||
Returns: allowed, reason, account_state, club_roles, linked_feature_id
|
||||
"""
|
||||
account_state = getattr(tenant, "account_state", "active_member")
|
||||
eff_club = club_id if club_id is not None else tenant.effective_club_id
|
||||
club_roles = club_roles_in_club(tenant, eff_club) if eff_club is not None else []
|
||||
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT id, min_account_state, linked_feature_id, active, domain
|
||||
FROM capabilities
|
||||
WHERE id = %s
|
||||
""",
|
||||
(capability_id,),
|
||||
)
|
||||
cap = cur.fetchone()
|
||||
if not cap or not cap.get("active"):
|
||||
return {
|
||||
"allowed": False,
|
||||
"reason": "capability_not_found",
|
||||
"account_state": account_state,
|
||||
"club_roles": club_roles,
|
||||
"linked_feature_id": None,
|
||||
}
|
||||
|
||||
min_state = cap.get("min_account_state") or "active_member"
|
||||
if not account_state_satisfies(account_state, min_state):
|
||||
return {
|
||||
"allowed": False,
|
||||
"reason": "account_state_insufficient",
|
||||
"account_state": account_state,
|
||||
"club_roles": club_roles,
|
||||
"linked_feature_id": cap.get("linked_feature_id"),
|
||||
}
|
||||
|
||||
domain = (cap.get("domain") or "").strip().lower()
|
||||
|
||||
# Kontingent-Bypass (konfigurierbar per portal_role / profile grants, ohne Plattform-Admin-Pflicht)
|
||||
if domain == "quota_bypass":
|
||||
role_lc = (tenant.global_role or "").lower()
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT 1 FROM portal_role_capability_grants
|
||||
WHERE portal_role = %s AND capability_id = %s
|
||||
LIMIT 1
|
||||
""",
|
||||
(role_lc, capability_id),
|
||||
)
|
||||
if cur.fetchone():
|
||||
return {
|
||||
"allowed": True,
|
||||
"reason": "quota_bypass_portal_grant",
|
||||
"account_state": account_state,
|
||||
"club_roles": club_roles,
|
||||
"linked_feature_id": cap.get("linked_feature_id"),
|
||||
}
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT 1 FROM profile_capability_grants
|
||||
WHERE profile_id = %s AND capability_id = %s
|
||||
LIMIT 1
|
||||
""",
|
||||
(tenant.profile_id, capability_id),
|
||||
)
|
||||
if cur.fetchone():
|
||||
return {
|
||||
"allowed": True,
|
||||
"reason": "quota_bypass_profile_grant",
|
||||
"account_state": account_state,
|
||||
"club_roles": club_roles,
|
||||
"linked_feature_id": cap.get("linked_feature_id"),
|
||||
}
|
||||
return {
|
||||
"allowed": False,
|
||||
"reason": "quota_bypass_denied",
|
||||
"account_state": account_state,
|
||||
"club_roles": club_roles,
|
||||
"linked_feature_id": cap.get("linked_feature_id"),
|
||||
}
|
||||
|
||||
# Plattform-Capabilities
|
||||
if domain == "platform" or capability_id.startswith("platform."):
|
||||
role_lc = (tenant.global_role or "").lower()
|
||||
if not is_platform_admin(role_lc):
|
||||
return {
|
||||
"allowed": False,
|
||||
"reason": "portal_role_required",
|
||||
"account_state": account_state,
|
||||
"club_roles": club_roles,
|
||||
"linked_feature_id": cap.get("linked_feature_id"),
|
||||
}
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT 1 FROM portal_role_capability_grants
|
||||
WHERE portal_role = %s AND capability_id = %s
|
||||
LIMIT 1
|
||||
""",
|
||||
(role_lc, capability_id),
|
||||
)
|
||||
if not cur.fetchone():
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT 1 FROM profile_capability_grants
|
||||
WHERE profile_id = %s AND capability_id = %s
|
||||
LIMIT 1
|
||||
""",
|
||||
(tenant.profile_id, capability_id),
|
||||
)
|
||||
if not cur.fetchone():
|
||||
return {
|
||||
"allowed": False,
|
||||
"reason": "portal_capability_denied",
|
||||
"account_state": account_state,
|
||||
"club_roles": club_roles,
|
||||
"linked_feature_id": cap.get("linked_feature_id"),
|
||||
}
|
||||
return {
|
||||
"allowed": True,
|
||||
"reason": "portal_granted",
|
||||
"account_state": account_state,
|
||||
"club_roles": club_roles,
|
||||
"linked_feature_id": cap.get("linked_feature_id"),
|
||||
}
|
||||
|
||||
# Plattform-Admin-Bypass für Mandanten-Funktionen (Audit-Pflicht, s. Katalog §9)
|
||||
if is_platform_admin(tenant.global_role):
|
||||
return {
|
||||
"allowed": True,
|
||||
"reason": "platform_admin_bypass",
|
||||
"account_state": account_state,
|
||||
"club_roles": club_roles,
|
||||
"linked_feature_id": cap.get("linked_feature_id"),
|
||||
}
|
||||
|
||||
# Vereins-Capabilities: aktive Mitgliedschaft im Zielverein
|
||||
if min_state == "active_member":
|
||||
if eff_club is None:
|
||||
return {
|
||||
"allowed": False,
|
||||
"reason": "no_club_context",
|
||||
"account_state": account_state,
|
||||
"club_roles": club_roles,
|
||||
"linked_feature_id": cap.get("linked_feature_id"),
|
||||
}
|
||||
if eff_club not in tenant.club_ids:
|
||||
return {
|
||||
"allowed": False,
|
||||
"reason": "not_club_member",
|
||||
"account_state": account_state,
|
||||
"club_roles": club_roles,
|
||||
"linked_feature_id": cap.get("linked_feature_id"),
|
||||
}
|
||||
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT role_code FROM club_role_capability_grants
|
||||
WHERE capability_id = %s
|
||||
""",
|
||||
(capability_id,),
|
||||
)
|
||||
required_roles = [r["role_code"] for r in cur.fetchall()]
|
||||
|
||||
if required_roles:
|
||||
if not any(r in required_roles for r in club_roles):
|
||||
return {
|
||||
"allowed": False,
|
||||
"reason": "club_role_denied",
|
||||
"account_state": account_state,
|
||||
"club_roles": club_roles,
|
||||
"linked_feature_id": cap.get("linked_feature_id"),
|
||||
}
|
||||
elif min_state == "active_member" and eff_club is not None:
|
||||
# Offene Capability für alle aktiven Mitglieder — Mitgliedschaft reicht
|
||||
pass
|
||||
|
||||
return {
|
||||
"allowed": True,
|
||||
"reason": "granted",
|
||||
"account_state": account_state,
|
||||
"club_roles": club_roles,
|
||||
"linked_feature_id": cap.get("linked_feature_id"),
|
||||
}
|
||||
|
||||
|
||||
def resolve_capabilities_map(
|
||||
cur,
|
||||
tenant: "TenantContext",
|
||||
*,
|
||||
club_id: Optional[int] = None,
|
||||
) -> Dict[str, bool]:
|
||||
"""Alle aktiven Capabilities → bool (für späteres /me/entitlements)."""
|
||||
cur.execute("SELECT id FROM capabilities WHERE active = true ORDER BY id")
|
||||
ids = [r["id"] for r in cur.fetchall()]
|
||||
out: Dict[str, bool] = {}
|
||||
for cid in ids:
|
||||
res = check_capability(cur, tenant, cid, club_id=club_id)
|
||||
out[cid] = bool(res.get("allowed"))
|
||||
return out
|
||||
|
||||
|
||||
def probe_capability(
|
||||
tenant: "TenantContext",
|
||||
capability_id: str,
|
||||
*,
|
||||
action: str,
|
||||
club_id: Optional[int] = None,
|
||||
endpoint: Optional[str] = None,
|
||||
conn=None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Phase 2: Capability prüfen + JSON-Log; blockiert nur bei CAPABILITY_ENFORCE=1."""
|
||||
from capability_logger import log_capability_check
|
||||
|
||||
def _run(c):
|
||||
cur = get_cursor(c)
|
||||
result = check_capability(cur, tenant, capability_id, club_id=club_id)
|
||||
log_capability_check(
|
||||
club_id=club_id if club_id is not None else tenant.effective_club_id,
|
||||
profile_id=tenant.profile_id,
|
||||
capability_id=capability_id,
|
||||
action=action,
|
||||
result=result,
|
||||
endpoint=endpoint,
|
||||
phase="enforce" if capability_enforcement_enabled() else "probe",
|
||||
)
|
||||
if capability_enforcement_enabled() and not result.get("allowed"):
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail=(
|
||||
f"Keine Berechtigung für {capability_id} "
|
||||
f"({result.get('reason', 'denied')})."
|
||||
),
|
||||
)
|
||||
return result
|
||||
|
||||
if conn is not None:
|
||||
return _run(conn)
|
||||
with get_db() as c:
|
||||
return _run(c)
|
||||
94
backend/capability_enforcement_audit.py
Normal file
94
backend/capability_enforcement_audit.py
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
"""
|
||||
Audit: Welche Capabilities sind an Endpoints angebunden?
|
||||
|
||||
Für Admin-Matrix (Rollen & Rechte) und Roadmap — bei neuem probe_capability hier eintragen.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Dict
|
||||
|
||||
# Endpoints rufen probe_capability auf (Log; Block nur bei CAPABILITY_ENFORCE=1)
|
||||
WIRED_PROBE = frozenset(
|
||||
{
|
||||
"exercises.ai.suggest",
|
||||
"exercises.ai.regenerate",
|
||||
"exercises.create",
|
||||
"exercises.media.upload",
|
||||
"planning.ai.suggest",
|
||||
"planning.ai.progression_path",
|
||||
"club.creation_request.read_own",
|
||||
"club.creation_request.create",
|
||||
"club.creation_request.withdraw",
|
||||
"platform.club_creation.approve",
|
||||
}
|
||||
)
|
||||
|
||||
# Kontingent-Verbrauch nach Erfolg (consume_club_feature_with_usage)
|
||||
FEATURE_CONSUME_WIRED = frozenset(
|
||||
{
|
||||
"ai_calls",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def enforcement_status_for_capability(capability_id: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Anzeige-Status für Superadmin-Matrix.
|
||||
|
||||
level: probe | legacy | platform | open | none
|
||||
"""
|
||||
cid = (capability_id or "").strip()
|
||||
if cid in WIRED_PROBE:
|
||||
return {
|
||||
"level": "probe",
|
||||
"label": "API vorbereitet (Log)",
|
||||
"detail": "probe_capability am Endpoint; Hard-Block erst mit CAPABILITY_ENFORCE=1",
|
||||
"implemented": True,
|
||||
}
|
||||
if cid.startswith("platform."):
|
||||
if cid == "platform.admin.access":
|
||||
return {
|
||||
"level": "platform",
|
||||
"label": "Plattform (Router-Guard)",
|
||||
"detail": "RequireAdmin / Superadmin-Checks",
|
||||
"implemented": True,
|
||||
}
|
||||
if cid in WIRED_PROBE:
|
||||
pass
|
||||
return {
|
||||
"level": "platform",
|
||||
"label": "Plattform (teilweise)",
|
||||
"detail": "Meist Router-Guard; Capability-Probe nur wo eingetragen",
|
||||
"implemented": cid in WIRED_PROBE,
|
||||
}
|
||||
if cid.startswith("club."):
|
||||
return {
|
||||
"level": "open",
|
||||
"label": "Onboarding",
|
||||
"detail": "Account-State / eigene Flows",
|
||||
"implemented": cid in WIRED_PROBE,
|
||||
}
|
||||
# Vereins-Capabilities ohne Probe: Legacy club_tenancy (can_plan_in_club, has_club_role, …)
|
||||
return {
|
||||
"level": "legacy",
|
||||
"label": "Nur Legacy-Rollen",
|
||||
"detail": "Noch kein probe_capability — prüft can_plan_in_club / club_admin im Code",
|
||||
"implemented": False,
|
||||
}
|
||||
|
||||
|
||||
def feature_consume_status(feature_id: str) -> Dict[str, Any]:
|
||||
fid = (feature_id or "").strip()
|
||||
if fid in FEATURE_CONSUME_WIRED:
|
||||
return {
|
||||
"level": "consume",
|
||||
"label": "Verbrauch aktiv",
|
||||
"detail": "consume_club_feature_with_usage + feature_usage in Response",
|
||||
"implemented": True,
|
||||
}
|
||||
return {
|
||||
"level": "inventory",
|
||||
"label": "Bestand / Probe",
|
||||
"detail": "Probe oder Live-Zählung; kein Consume nach Aktion",
|
||||
"implemented": False,
|
||||
}
|
||||
64
backend/capability_logger.py
Normal file
64
backend/capability_logger.py
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
"""
|
||||
JSON-Log für Capability-Checks (M3 Phase 2 — analog club_feature_logger).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
|
||||
def _log_dir() -> Path:
|
||||
custom = (os.getenv("CAPABILITY_LOG_DIR") or "").strip()
|
||||
if custom:
|
||||
return Path(custom)
|
||||
return Path("/app/logs")
|
||||
|
||||
|
||||
capability_logger = logging.getLogger("shinkan.capability_usage")
|
||||
capability_logger.setLevel(logging.INFO)
|
||||
capability_logger.propagate = False
|
||||
|
||||
if not capability_logger.handlers:
|
||||
log_dir = _log_dir()
|
||||
try:
|
||||
log_dir.mkdir(parents=True, exist_ok=True)
|
||||
log_file = log_dir / "capability-usage.log"
|
||||
file_handler = logging.FileHandler(log_file, encoding="utf-8")
|
||||
file_handler.setLevel(logging.INFO)
|
||||
file_handler.setFormatter(logging.Formatter("%(message)s"))
|
||||
capability_logger.addHandler(file_handler)
|
||||
except OSError:
|
||||
stream_handler = logging.StreamHandler()
|
||||
stream_handler.setFormatter(logging.Formatter("[capability-usage] %(message)s"))
|
||||
capability_logger.addHandler(stream_handler)
|
||||
|
||||
|
||||
def log_capability_check(
|
||||
*,
|
||||
club_id: Optional[int],
|
||||
profile_id: Optional[int],
|
||||
capability_id: str,
|
||||
action: str,
|
||||
result: Dict[str, Any],
|
||||
endpoint: Optional[str] = None,
|
||||
phase: str = "probe",
|
||||
) -> None:
|
||||
entry = {
|
||||
"timestamp": datetime.now(timezone.utc).isoformat(),
|
||||
"club_id": club_id,
|
||||
"profile_id": profile_id,
|
||||
"capability": capability_id,
|
||||
"action": action,
|
||||
"endpoint": endpoint,
|
||||
"phase": phase,
|
||||
"allowed": result.get("allowed", True),
|
||||
"reason": result.get("reason", "unknown"),
|
||||
"account_state": result.get("account_state"),
|
||||
"club_roles": result.get("club_roles"),
|
||||
"enforcement": os.getenv("CAPABILITY_ENFORCE", "0") == "1",
|
||||
}
|
||||
capability_logger.info(json.dumps(entry, ensure_ascii=False))
|
||||
74
backend/club_feature_logger.py
Normal file
74
backend/club_feature_logger.py
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
"""
|
||||
JSON-Log für Vereins-Feature-Zugriffe (Phase 2: nur Monitoring, kein Block).
|
||||
|
||||
Spez: CLUB_MEMBERSHIP_AND_FEATURES.v1.md §9 Phase 2 — analog Mitai feature_logger.py.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
|
||||
def _log_dir() -> Path:
|
||||
custom = (os.getenv("CLUB_FEATURE_LOG_DIR") or "").strip()
|
||||
if custom:
|
||||
return Path(custom)
|
||||
return Path("/app/logs")
|
||||
|
||||
|
||||
feature_usage_logger = logging.getLogger("shinkan.club_feature_usage")
|
||||
feature_usage_logger.setLevel(logging.INFO)
|
||||
feature_usage_logger.propagate = False
|
||||
|
||||
if not feature_usage_logger.handlers:
|
||||
log_dir = _log_dir()
|
||||
try:
|
||||
log_dir.mkdir(parents=True, exist_ok=True)
|
||||
log_file = log_dir / "club-feature-usage.log"
|
||||
file_handler = logging.FileHandler(log_file, encoding="utf-8")
|
||||
file_handler.setLevel(logging.INFO)
|
||||
file_handler.setFormatter(logging.Formatter("%(message)s"))
|
||||
feature_usage_logger.addHandler(file_handler)
|
||||
except OSError:
|
||||
# Dev ohne /app/logs: Fallback stderr
|
||||
stream_handler = logging.StreamHandler()
|
||||
stream_handler.setFormatter(logging.Formatter("[club-feature-usage] %(message)s"))
|
||||
feature_usage_logger.addHandler(stream_handler)
|
||||
|
||||
|
||||
def log_club_feature_usage(
|
||||
*,
|
||||
club_id: Optional[int],
|
||||
profile_id: Optional[int],
|
||||
feature_id: str,
|
||||
action: str,
|
||||
access: Dict[str, Any],
|
||||
endpoint: Optional[str] = None,
|
||||
phase: str = "probe",
|
||||
) -> None:
|
||||
"""
|
||||
Strukturiertes JSON-Log eines Feature-Checks.
|
||||
|
||||
phase: probe (Phase 2, non-blocking) | enforce (Phase 4, nach Block-Entscheid)
|
||||
"""
|
||||
entry = {
|
||||
"timestamp": datetime.now(timezone.utc).isoformat(),
|
||||
"club_id": club_id,
|
||||
"profile_id": profile_id,
|
||||
"feature": feature_id,
|
||||
"action": action,
|
||||
"endpoint": endpoint,
|
||||
"phase": phase,
|
||||
"plan_id": access.get("plan_id"),
|
||||
"used": access.get("used", 0),
|
||||
"limit": access.get("limit"),
|
||||
"remaining": access.get("remaining"),
|
||||
"allowed": access.get("allowed", True),
|
||||
"reason": access.get("reason", "unknown"),
|
||||
"enforcement": os.getenv("CLUB_FEATURE_ENFORCE", "0") == "1",
|
||||
}
|
||||
feature_usage_logger.info(json.dumps(entry, ensure_ascii=False))
|
||||
713
backend/club_features.py
Normal file
713
backend/club_features.py
Normal file
|
|
@ -0,0 +1,713 @@
|
|||
"""
|
||||
Vereinsbezogene Feature-Limits (Mitai-v9c-Pattern, Subjekt club_id).
|
||||
|
||||
Spez: .claude/docs/technical/CLUB_MEMBERSHIP_AND_FEATURES.v1.md
|
||||
Phase 2 (M2): probe_club_feature_access — JSON-Log, kein HTTP-Block.
|
||||
Phase 4 (M5+): CLUB_FEATURE_ENFORCE=1 — HTTP 403 + increment.
|
||||
|
||||
Verbrauch-Standard für Router:
|
||||
probe_club_feature_access → Business-Logik → consume_club_feature_with_usage → merge_feature_usage_into_response
|
||||
|
||||
Legacy profil-zentriert: auth.check_feature_access (001 / Mitai-Überbleibsel) — nicht für Shinkan-Limits nutzen.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Any, Dict, Optional, TYPE_CHECKING
|
||||
|
||||
from fastapi import HTTPException
|
||||
|
||||
from db import get_db, get_cursor
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from tenant_context import TenantContext
|
||||
|
||||
# Bestands-Features: Verbrauch = Live-Zählung in DB (nicht club_feature_usage)
|
||||
_INVENTORY_FEATURES = frozenset(
|
||||
{"exercises", "training_groups", "active_members", "training_programs"}
|
||||
)
|
||||
|
||||
|
||||
def _calculate_next_reset(reset_period: str, *, now: Optional[datetime] = None) -> Optional[datetime]:
|
||||
"""Nächster Reset-Zeitpunkt; None bei 'never'."""
|
||||
ref = now or datetime.now(timezone.utc)
|
||||
if reset_period == "never":
|
||||
return None
|
||||
if reset_period == "daily":
|
||||
tomorrow = ref.date() + timedelta(days=1)
|
||||
return datetime.combine(tomorrow, datetime.min.time(), tzinfo=timezone.utc)
|
||||
if reset_period == "monthly":
|
||||
if ref.month == 12:
|
||||
return datetime(ref.year + 1, 1, 1, tzinfo=timezone.utc)
|
||||
return datetime(ref.year, ref.month + 1, 1, tzinfo=timezone.utc)
|
||||
return None
|
||||
|
||||
|
||||
def _normalize_limit(raw: Any) -> Optional[int]:
|
||||
"""NULL = unbegrenzt; -1 (Legacy 001) wird als unbegrenzt behandelt."""
|
||||
if raw is None:
|
||||
return None
|
||||
try:
|
||||
v = int(raw)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
if v < 0:
|
||||
return None
|
||||
return v
|
||||
|
||||
|
||||
def get_effective_club_plan(cur, club_id: int) -> str:
|
||||
"""
|
||||
Effektiver Plan für einen Verein.
|
||||
|
||||
1. Aktiver club_access_grants mit plan_id (Zeitfenster, neueste ends_at)
|
||||
2. club_subscriptions.status = 'active' → plan_id
|
||||
3. Fallback 'free'
|
||||
"""
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT plan_id
|
||||
FROM club_access_grants
|
||||
WHERE club_id = %s
|
||||
AND plan_id IS NOT NULL
|
||||
AND starts_at <= NOW()
|
||||
AND ends_at > NOW()
|
||||
ORDER BY ends_at DESC
|
||||
LIMIT 1
|
||||
""",
|
||||
(club_id,),
|
||||
)
|
||||
grant = cur.fetchone()
|
||||
if grant and grant.get("plan_id"):
|
||||
return str(grant["plan_id"])
|
||||
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT plan_id
|
||||
FROM club_subscriptions
|
||||
WHERE club_id = %s AND status = 'active'
|
||||
LIMIT 1
|
||||
""",
|
||||
(club_id,),
|
||||
)
|
||||
sub = cur.fetchone()
|
||||
if sub and sub.get("plan_id"):
|
||||
return str(sub["plan_id"])
|
||||
|
||||
return "free"
|
||||
|
||||
|
||||
def _resolve_club_limit(cur, club_id: int, feature_id: str, feature_row: dict) -> Optional[int]:
|
||||
"""Limit-Wert: Override > Plan > Feature-Default."""
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT limit_value
|
||||
FROM club_feature_overrides
|
||||
WHERE club_id = %s AND feature_id = %s
|
||||
""",
|
||||
(club_id, feature_id),
|
||||
)
|
||||
override = cur.fetchone()
|
||||
if override is not None:
|
||||
return _normalize_limit(override.get("limit_value"))
|
||||
|
||||
plan_id = get_effective_club_plan(cur, club_id)
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT limit_value
|
||||
FROM club_plan_limits
|
||||
WHERE plan_id = %s AND feature_id = %s
|
||||
""",
|
||||
(plan_id, feature_id),
|
||||
)
|
||||
plan_lim = cur.fetchone()
|
||||
if plan_lim is not None:
|
||||
return _normalize_limit(plan_lim.get("limit_value"))
|
||||
|
||||
return _normalize_limit(feature_row.get("default_limit"))
|
||||
|
||||
|
||||
def _live_inventory_count(cur, club_id: int, feature_id: str) -> Optional[int]:
|
||||
"""Aktueller Bestand für reset_period=never Features."""
|
||||
if feature_id == "exercises":
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT COUNT(*)::int AS c
|
||||
FROM exercises
|
||||
WHERE club_id = %s AND status != 'archived'
|
||||
""",
|
||||
(club_id,),
|
||||
)
|
||||
elif feature_id == "training_groups":
|
||||
cur.execute(
|
||||
"SELECT COUNT(*)::int AS c FROM training_groups WHERE club_id = %s",
|
||||
(club_id,),
|
||||
)
|
||||
elif feature_id == "active_members":
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT COUNT(*)::int AS c
|
||||
FROM club_members
|
||||
WHERE club_id = %s AND status = 'active'
|
||||
""",
|
||||
(club_id,),
|
||||
)
|
||||
elif feature_id == "training_programs":
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT COUNT(*)::int AS c FROM (
|
||||
SELECT id FROM training_framework_programs WHERE club_id = %s
|
||||
UNION ALL
|
||||
SELECT id FROM training_modules WHERE club_id = %s
|
||||
) t
|
||||
""",
|
||||
(club_id, club_id),
|
||||
)
|
||||
else:
|
||||
return None
|
||||
|
||||
row = cur.fetchone()
|
||||
return int(row["c"] or 0) if row else 0
|
||||
|
||||
|
||||
def resolve_club_id_for_probe(
|
||||
tenant: "TenantContext",
|
||||
*,
|
||||
object_club_id: Optional[int] = None,
|
||||
) -> Optional[int]:
|
||||
"""Verein für Feature-Probe: explizites Objekt > effective_club_id."""
|
||||
if object_club_id is not None:
|
||||
return int(object_club_id)
|
||||
eff = getattr(tenant, "effective_club_id", None)
|
||||
return int(eff) if eff is not None else None
|
||||
|
||||
|
||||
def _maybe_reset_usage(cur, conn, club_id: int, feature_id: str, feature_row: dict, usage_row: Optional[dict]) -> int:
|
||||
"""Setzt Zähler zurück wenn reset_at überschritten; gibt aktuellen used zurück."""
|
||||
used = int(usage_row.get("usage_count") or 0) if usage_row else 0
|
||||
reset_at = usage_row.get("reset_at") if usage_row else None
|
||||
period = (feature_row.get("reset_period") or "never").strip().lower()
|
||||
|
||||
if not usage_row or not reset_at or period == "never":
|
||||
return used
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
ra = reset_at
|
||||
if hasattr(ra, "tzinfo") and ra.tzinfo is None:
|
||||
ra = ra.replace(tzinfo=timezone.utc)
|
||||
|
||||
if ra and now > ra:
|
||||
next_reset = _calculate_next_reset(period, now=now)
|
||||
cur.execute(
|
||||
"""
|
||||
UPDATE club_feature_usage
|
||||
SET usage_count = 0, reset_at = %s, updated_at = NOW()
|
||||
WHERE club_id = %s AND feature_id = %s
|
||||
""",
|
||||
(next_reset, club_id, feature_id),
|
||||
)
|
||||
conn.commit()
|
||||
return 0
|
||||
|
||||
return used
|
||||
|
||||
|
||||
def check_club_feature_access(
|
||||
club_id: int,
|
||||
feature_id: str,
|
||||
*,
|
||||
conn=None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Prüft Vereins-Kontingent für ein Feature.
|
||||
|
||||
Returns:
|
||||
allowed, limit, used, remaining, reason, plan_id, reset_at (optional)
|
||||
"""
|
||||
if conn is not None:
|
||||
return _check_club_impl(club_id, feature_id, conn)
|
||||
|
||||
with get_db() as c:
|
||||
return _check_club_impl(club_id, feature_id, c)
|
||||
|
||||
|
||||
def _check_club_impl(club_id: int, feature_id: str, conn) -> Dict[str, Any]:
|
||||
cur = get_cursor(conn)
|
||||
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT id, limit_type, reset_period, default_limit, active, enforcement_subject
|
||||
FROM features
|
||||
WHERE id = %s AND app = 'shinkan'
|
||||
""",
|
||||
(feature_id,),
|
||||
)
|
||||
feature = cur.fetchone()
|
||||
if not feature or not feature.get("active"):
|
||||
return {
|
||||
"allowed": False,
|
||||
"limit": None,
|
||||
"used": 0,
|
||||
"remaining": None,
|
||||
"reason": "feature_not_found",
|
||||
"plan_id": get_effective_club_plan(cur, club_id),
|
||||
}
|
||||
|
||||
plan_id = get_effective_club_plan(cur, club_id)
|
||||
limit = _resolve_club_limit(cur, club_id, feature_id, feature)
|
||||
limit_type = (feature.get("limit_type") or "count").strip().lower()
|
||||
|
||||
if limit_type == "boolean":
|
||||
allowed = limit == 1
|
||||
return {
|
||||
"allowed": allowed,
|
||||
"limit": limit,
|
||||
"used": 0,
|
||||
"remaining": None,
|
||||
"reason": "enabled" if allowed else "feature_disabled",
|
||||
"plan_id": plan_id,
|
||||
}
|
||||
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT usage_count, reset_at
|
||||
FROM club_feature_usage
|
||||
WHERE club_id = %s AND feature_id = %s
|
||||
""",
|
||||
(club_id, feature_id),
|
||||
)
|
||||
usage = cur.fetchone()
|
||||
used = _maybe_reset_usage(cur, conn, club_id, feature_id, feature, usage)
|
||||
|
||||
period = (feature.get("reset_period") or "never").strip().lower()
|
||||
if period == "never" and feature_id in _INVENTORY_FEATURES:
|
||||
inv = _live_inventory_count(cur, club_id, feature_id)
|
||||
if inv is not None:
|
||||
used = inv
|
||||
|
||||
if limit is None:
|
||||
return {
|
||||
"allowed": True,
|
||||
"limit": None,
|
||||
"used": used,
|
||||
"remaining": None,
|
||||
"reason": "unlimited",
|
||||
"plan_id": plan_id,
|
||||
"reset_at": usage.get("reset_at") if usage else None,
|
||||
}
|
||||
|
||||
if limit == 0:
|
||||
return {
|
||||
"allowed": False,
|
||||
"limit": 0,
|
||||
"used": used,
|
||||
"remaining": 0,
|
||||
"reason": "feature_disabled",
|
||||
"plan_id": plan_id,
|
||||
"reset_at": usage.get("reset_at") if usage else None,
|
||||
}
|
||||
|
||||
allowed = used < limit
|
||||
return {
|
||||
"allowed": allowed,
|
||||
"limit": limit,
|
||||
"used": used,
|
||||
"remaining": max(0, limit - used),
|
||||
"reason": "within_limit" if allowed else "limit_exceeded",
|
||||
"plan_id": plan_id,
|
||||
"reset_at": usage.get("reset_at") if usage else None,
|
||||
}
|
||||
|
||||
|
||||
def club_feature_enforcement_enabled() -> bool:
|
||||
"""Phase 4: Hard-Block aktiv (Env CLUB_FEATURE_ENFORCE=1|true|yes)."""
|
||||
v = os.getenv("CLUB_FEATURE_ENFORCE", "0").strip().lower()
|
||||
return v in ("1", "true", "yes")
|
||||
|
||||
|
||||
def probe_club_feature_access(
|
||||
*,
|
||||
feature_id: str,
|
||||
action: str,
|
||||
club_id: Optional[int] = None,
|
||||
profile_id: Optional[int] = None,
|
||||
portal_role: Optional[str] = None,
|
||||
endpoint: Optional[str] = None,
|
||||
tenant: Optional["TenantContext"] = None,
|
||||
conn=None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Phase 2: Prüft Vereins-Kontingent, schreibt JSON-Log, blockiert standardmäßig nicht.
|
||||
|
||||
Bei CLUB_FEATURE_ENFORCE=1: HTTP 403 wenn nicht allowed.
|
||||
"""
|
||||
from club_feature_logger import log_club_feature_usage
|
||||
|
||||
if club_id is None:
|
||||
access = {
|
||||
"allowed": not club_feature_enforcement_enabled(),
|
||||
"limit": None,
|
||||
"used": 0,
|
||||
"remaining": None,
|
||||
"reason": "no_club_context",
|
||||
"plan_id": None,
|
||||
}
|
||||
log_club_feature_usage(
|
||||
club_id=None,
|
||||
profile_id=profile_id,
|
||||
feature_id=feature_id,
|
||||
action=action,
|
||||
access=access,
|
||||
endpoint=endpoint,
|
||||
phase="enforce" if club_feature_enforcement_enabled() else "probe",
|
||||
)
|
||||
if club_feature_enforcement_enabled() and not access.get("allowed"):
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail=(
|
||||
f"Kein Vereinskontext für {feature_id} — "
|
||||
"aktiven Verein wählen (X-Active-Club-Id)."
|
||||
),
|
||||
)
|
||||
return access
|
||||
|
||||
def _resolve_access(connection):
|
||||
from club_quota_bypass import is_club_feature_quota_bypassed, quota_bypass_access
|
||||
|
||||
cur = get_cursor(connection)
|
||||
if is_club_feature_quota_bypassed(
|
||||
cur,
|
||||
profile_id=profile_id,
|
||||
portal_role=portal_role,
|
||||
feature_id=feature_id,
|
||||
tenant=tenant,
|
||||
):
|
||||
plan_id = get_effective_club_plan(cur, int(club_id))
|
||||
return quota_bypass_access(
|
||||
feature_id=feature_id,
|
||||
club_id=int(club_id),
|
||||
plan_id=plan_id,
|
||||
)
|
||||
return check_club_feature_access(club_id, feature_id, conn=connection)
|
||||
|
||||
if conn is not None:
|
||||
access = _resolve_access(conn)
|
||||
else:
|
||||
with get_db() as c:
|
||||
access = _resolve_access(c)
|
||||
|
||||
log_club_feature_usage(
|
||||
club_id=club_id,
|
||||
profile_id=profile_id,
|
||||
feature_id=feature_id,
|
||||
action=action,
|
||||
access=access,
|
||||
endpoint=endpoint,
|
||||
phase="enforce" if club_feature_enforcement_enabled() else "probe",
|
||||
)
|
||||
|
||||
if club_feature_enforcement_enabled() and not access.get("allowed"):
|
||||
limit = access.get("limit")
|
||||
used = access.get("used", 0)
|
||||
detail = (
|
||||
f"Kontingent überschritten für {feature_id} "
|
||||
f"({used}/{limit if limit is not None else '∞'}). "
|
||||
f"Grund: {access.get('reason', 'limit_exceeded')}."
|
||||
)
|
||||
raise HTTPException(status_code=403, detail=detail)
|
||||
|
||||
return access
|
||||
|
||||
|
||||
def consume_club_feature(
|
||||
*,
|
||||
feature_id: str,
|
||||
club_id: Optional[int],
|
||||
profile_id: Optional[int] = None,
|
||||
portal_role: Optional[str] = None,
|
||||
action: Optional[str] = None,
|
||||
amount: int = 1,
|
||||
conn=None,
|
||||
) -> None:
|
||||
"""
|
||||
Phase 4 (M5): Zähler nach erfolgreichem Verbrauch erhöhen.
|
||||
Nur wenn club_id gesetzt (Vereins-Kontingent); amount = Anzahl LLM/API-Verbrauchseinheiten.
|
||||
Plattform-Ausnahmen (superadmin, konfigurierte Rollen/Profile) werden nicht gezählt.
|
||||
"""
|
||||
if club_id is None:
|
||||
return
|
||||
|
||||
def _is_exempt(connection) -> bool:
|
||||
from club_quota_bypass import is_club_feature_quota_bypassed
|
||||
|
||||
cur = get_cursor(connection)
|
||||
return is_club_feature_quota_bypassed(
|
||||
cur,
|
||||
profile_id=profile_id,
|
||||
portal_role=portal_role,
|
||||
feature_id=feature_id,
|
||||
)
|
||||
|
||||
if conn is not None:
|
||||
if _is_exempt(conn):
|
||||
return
|
||||
else:
|
||||
with get_db() as c:
|
||||
if _is_exempt(c):
|
||||
return
|
||||
try:
|
||||
n = int(amount)
|
||||
except (TypeError, ValueError):
|
||||
n = 1
|
||||
if n < 1:
|
||||
return
|
||||
for _ in range(n):
|
||||
increment_club_feature_usage(
|
||||
int(club_id),
|
||||
feature_id,
|
||||
profile_id=profile_id,
|
||||
action=action,
|
||||
conn=conn,
|
||||
)
|
||||
|
||||
def _log_consume(connection) -> None:
|
||||
from club_feature_logger import log_club_feature_usage
|
||||
|
||||
access = check_club_feature_access(int(club_id), feature_id, conn=connection)
|
||||
log_club_feature_usage(
|
||||
club_id=int(club_id),
|
||||
profile_id=profile_id,
|
||||
feature_id=feature_id,
|
||||
action=action or "consume",
|
||||
access=access,
|
||||
phase="consume",
|
||||
)
|
||||
|
||||
if conn is not None:
|
||||
_log_consume(conn)
|
||||
else:
|
||||
with get_db() as c:
|
||||
_log_consume(c)
|
||||
|
||||
|
||||
def consume_club_feature_with_usage(
|
||||
*,
|
||||
feature_id: str,
|
||||
club_id: Optional[int],
|
||||
profile_id: Optional[int] = None,
|
||||
portal_role: Optional[str] = None,
|
||||
action: Optional[str] = None,
|
||||
amount: int = 1,
|
||||
cur,
|
||||
tenant: Optional["TenantContext"] = None,
|
||||
conn=None,
|
||||
) -> Optional[Dict[str, Dict[str, Any]]]:
|
||||
"""
|
||||
Standard nach erfolgreichem Verbrauch: zählen, protokollieren, Snapshot für Response.
|
||||
|
||||
Alle Endpoints mit Vereins-Kontingent-Verbrauch nutzen diese Funktion und
|
||||
``merge_feature_usage_into_response`` — kein duplizierter Einzelcode pro Route.
|
||||
"""
|
||||
consume_club_feature(
|
||||
feature_id=feature_id,
|
||||
club_id=club_id,
|
||||
profile_id=profile_id,
|
||||
portal_role=portal_role,
|
||||
action=action,
|
||||
amount=amount,
|
||||
conn=conn,
|
||||
)
|
||||
if club_id is None:
|
||||
return None
|
||||
return {
|
||||
feature_id: club_feature_usage_for_api(
|
||||
cur,
|
||||
club_id=int(club_id),
|
||||
feature_id=feature_id,
|
||||
profile_id=profile_id,
|
||||
portal_role=portal_role,
|
||||
tenant=tenant,
|
||||
conn=conn,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def merge_feature_usage_into_response(
|
||||
payload: Any,
|
||||
feature_usage: Optional[Dict[str, Dict[str, Any]]],
|
||||
) -> Any:
|
||||
"""Standard-Einbettung ``feature_usage`` in JSON-Responses."""
|
||||
if not feature_usage or not isinstance(payload, dict):
|
||||
return payload
|
||||
return {**payload, "feature_usage": feature_usage}
|
||||
|
||||
|
||||
def club_feature_usage_for_api(
|
||||
cur,
|
||||
*,
|
||||
club_id: int,
|
||||
feature_id: str,
|
||||
profile_id: Optional[int] = None,
|
||||
portal_role: Optional[str] = None,
|
||||
tenant: Optional["TenantContext"] = None,
|
||||
conn=None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Feature-Zustand wie GET /me/entitlements → features[feature_id] (nach Verbrauch)."""
|
||||
from club_quota_bypass import is_club_feature_quota_bypassed, quota_bypass_access
|
||||
|
||||
db_conn = conn if conn is not None else cur.connection
|
||||
access = check_club_feature_access(int(club_id), feature_id, conn=db_conn)
|
||||
plan_id = access.get("plan_id") or get_effective_club_plan(cur, int(club_id))
|
||||
|
||||
if is_club_feature_quota_bypassed(
|
||||
cur,
|
||||
profile_id=profile_id,
|
||||
portal_role=portal_role,
|
||||
feature_id=feature_id,
|
||||
tenant=tenant,
|
||||
):
|
||||
ex = quota_bypass_access(
|
||||
feature_id=feature_id,
|
||||
club_id=int(club_id),
|
||||
plan_id=plan_id,
|
||||
)
|
||||
reset_at = access.get("reset_at")
|
||||
return {
|
||||
"allowed": True,
|
||||
"used": access.get("used"),
|
||||
"limit": None,
|
||||
"remaining": None,
|
||||
"reason": ex.get("reason"),
|
||||
"platform_exempt": True,
|
||||
"reset_at": reset_at.isoformat() if hasattr(reset_at, "isoformat") else reset_at,
|
||||
}
|
||||
|
||||
return {
|
||||
"allowed": access.get("allowed"),
|
||||
"used": access.get("used"),
|
||||
"limit": access.get("limit"),
|
||||
"remaining": access.get("remaining"),
|
||||
"reason": access.get("reason"),
|
||||
"platform_exempt": False,
|
||||
"reset_at": access.get("reset_at").isoformat()
|
||||
if access.get("reset_at") is not None and hasattr(access.get("reset_at"), "isoformat")
|
||||
else access.get("reset_at"),
|
||||
}
|
||||
|
||||
|
||||
def increment_club_feature_usage(
|
||||
club_id: int,
|
||||
feature_id: str,
|
||||
*,
|
||||
profile_id: Optional[int] = None,
|
||||
action: Optional[str] = None,
|
||||
conn=None,
|
||||
) -> None:
|
||||
"""Erhöht Vereins-Zähler (nur bei neuem Verbrauch / INSERT-Pfad aufrufen)."""
|
||||
def _run(c):
|
||||
cur = get_cursor(c)
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT reset_period, limit_type
|
||||
FROM features
|
||||
WHERE id = %s AND app = 'shinkan' AND active = true
|
||||
""",
|
||||
(feature_id,),
|
||||
)
|
||||
feature = cur.fetchone()
|
||||
if not feature:
|
||||
return
|
||||
if (feature.get("limit_type") or "count").strip().lower() == "boolean":
|
||||
return
|
||||
|
||||
period = (feature.get("reset_period") or "never").strip().lower()
|
||||
next_reset = _calculate_next_reset(period)
|
||||
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO club_feature_usage (club_id, feature_id, usage_count, reset_at, last_used_at)
|
||||
VALUES (%s, %s, 1, %s, NOW())
|
||||
ON CONFLICT (club_id, feature_id)
|
||||
DO UPDATE SET
|
||||
usage_count = club_feature_usage.usage_count + 1,
|
||||
last_used_at = NOW(),
|
||||
updated_at = NOW()
|
||||
""",
|
||||
(club_id, feature_id, next_reset),
|
||||
)
|
||||
|
||||
if profile_id is not None or action:
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO club_feature_usage_events (club_id, feature_id, profile_id, action)
|
||||
VALUES (%s, %s, %s, %s)
|
||||
""",
|
||||
(club_id, feature_id, profile_id, action or feature_id),
|
||||
)
|
||||
|
||||
if conn is not None:
|
||||
_run(conn)
|
||||
else:
|
||||
with get_db() as c:
|
||||
_run(c)
|
||||
|
||||
|
||||
def list_club_entitlements(cur, club_id: int, *, conn=None) -> Dict[str, Any]:
|
||||
"""Alle aktiven Shinkan-Features mit effektivem Limit und Verbrauch (Liste, intern)."""
|
||||
db_conn = conn if conn is not None else cur.connection
|
||||
plan_id = get_effective_club_plan(cur, club_id)
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT id, name, category, limit_type, reset_period
|
||||
FROM features
|
||||
WHERE app = 'shinkan' AND active = true
|
||||
ORDER BY category, id
|
||||
"""
|
||||
)
|
||||
rows = cur.fetchall()
|
||||
features_out = []
|
||||
for row in rows:
|
||||
fid = row["id"]
|
||||
access = _check_club_impl(club_id, fid, db_conn)
|
||||
features_out.append(
|
||||
{
|
||||
"id": fid,
|
||||
"name": row.get("name"),
|
||||
"category": row.get("category"),
|
||||
"limit_type": row.get("limit_type"),
|
||||
"reset_period": row.get("reset_period"),
|
||||
"allowed": access.get("allowed"),
|
||||
"limit": access.get("limit"),
|
||||
"used": access.get("used"),
|
||||
"remaining": access.get("remaining"),
|
||||
"reason": access.get("reason"),
|
||||
"reset_at": access.get("reset_at"),
|
||||
}
|
||||
)
|
||||
return {"club_id": club_id, "plan_id": plan_id, "features": features_out}
|
||||
|
||||
|
||||
def club_features_map(cur, club_id: int, *, conn=None) -> Dict[str, Any]:
|
||||
"""Feature-Kontingente als Dict feature_id → Zustand (für /me/entitlements)."""
|
||||
raw = list_club_entitlements(cur, club_id, conn=conn)
|
||||
features_dict: Dict[str, Any] = {}
|
||||
for row in raw.get("features") or []:
|
||||
fid = row["id"]
|
||||
features_dict[fid] = {
|
||||
"name": row.get("name"),
|
||||
"category": row.get("category"),
|
||||
"limit_type": row.get("limit_type"),
|
||||
"reset_period": row.get("reset_period"),
|
||||
"allowed": row.get("allowed"),
|
||||
"limit": row.get("limit"),
|
||||
"used": row.get("used"),
|
||||
"remaining": row.get("remaining"),
|
||||
"reason": row.get("reason"),
|
||||
"reset_at": row.get("reset_at"),
|
||||
}
|
||||
return {
|
||||
"club_id": raw.get("club_id"),
|
||||
"plan_id": raw.get("plan_id"),
|
||||
"features": features_dict,
|
||||
}
|
||||
180
backend/club_quota_bypass.py
Normal file
180
backend/club_quota_bypass.py
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
"""
|
||||
Vereins-Kontingent-Bypass über das Capability-System (kein Parallel-Rechtemodell).
|
||||
|
||||
Capabilities:
|
||||
- platform.club_quota.bypass — alle Vereins-Features (Portal-Admin, Grant via portal_role)
|
||||
- platform.club_quota.bypass.{feature_id} — ein Feature (domain quota_bypass, auch für Nicht-Admins per Grant)
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Dict, List, Optional, TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from tenant_context import TenantContext
|
||||
|
||||
QUOTA_BYPASS_ALL = "platform.club_quota.bypass"
|
||||
QUOTA_BYPASS_FEATURE_PREFIX = "platform.club_quota.bypass."
|
||||
|
||||
|
||||
def quota_bypass_capability_id_for_feature(feature_id: str) -> str:
|
||||
return f"{QUOTA_BYPASS_FEATURE_PREFIX}{feature_id}"
|
||||
|
||||
|
||||
def ensure_quota_bypass_capability(cur, feature_id: str) -> str:
|
||||
"""Legt feature-spezifische Bypass-Capability an falls nötig."""
|
||||
cap_id = quota_bypass_capability_id_for_feature(feature_id)
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO capabilities (id, name, domain, min_account_state, linked_feature_id)
|
||||
VALUES (%s, %s, 'quota_bypass', 'active_member', %s)
|
||||
ON CONFLICT (id) DO NOTHING
|
||||
""",
|
||||
(cap_id, f"Vereins-Kontingent umgehen: {feature_id}", feature_id),
|
||||
)
|
||||
return cap_id
|
||||
|
||||
|
||||
def _bypass_capability_ids(cur, feature_id: str) -> List[str]:
|
||||
ids: List[str] = [QUOTA_BYPASS_ALL, quota_bypass_capability_id_for_feature(feature_id)]
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT id FROM capabilities
|
||||
WHERE active = true
|
||||
AND domain = 'quota_bypass'
|
||||
AND linked_feature_id = %s
|
||||
AND id <> %s
|
||||
""",
|
||||
(feature_id, quota_bypass_capability_id_for_feature(feature_id)),
|
||||
)
|
||||
for row in cur.fetchall():
|
||||
cid = row.get("id")
|
||||
if cid and cid not in ids:
|
||||
ids.append(str(cid))
|
||||
return ids
|
||||
|
||||
|
||||
def _portal_role_has_grant(cur, portal_role: str, capability_id: str) -> bool:
|
||||
role = (portal_role or "").strip().lower()
|
||||
if not role:
|
||||
return False
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT 1 FROM portal_role_capability_grants
|
||||
WHERE portal_role = %s AND capability_id = %s
|
||||
LIMIT 1
|
||||
""",
|
||||
(role, capability_id),
|
||||
)
|
||||
return cur.fetchone() is not None
|
||||
|
||||
|
||||
def _profile_has_grant(cur, profile_id: int, capability_id: str) -> bool:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT 1 FROM profile_capability_grants
|
||||
WHERE profile_id = %s AND capability_id = %s
|
||||
LIMIT 1
|
||||
""",
|
||||
(int(profile_id), capability_id),
|
||||
)
|
||||
return cur.fetchone() is not None
|
||||
|
||||
|
||||
def is_club_feature_quota_bypassed(
|
||||
cur,
|
||||
*,
|
||||
profile_id: Optional[int],
|
||||
portal_role: Optional[str],
|
||||
feature_id: str,
|
||||
tenant: Optional["TenantContext"] = None,
|
||||
) -> bool:
|
||||
"""
|
||||
True wenn ein konfigurierter Capability-Grant das Vereins-Kontingent für feature_id umgeht.
|
||||
"""
|
||||
if tenant is not None:
|
||||
from capabilities import check_capability
|
||||
|
||||
for cap_id in _bypass_capability_ids(cur, feature_id):
|
||||
if check_capability(cur, tenant, cap_id).get("allowed"):
|
||||
return True
|
||||
return False
|
||||
|
||||
for cap_id in _bypass_capability_ids(cur, feature_id):
|
||||
if _portal_role_has_grant(cur, portal_role or "", cap_id):
|
||||
return True
|
||||
if profile_id is not None and _profile_has_grant(cur, int(profile_id), cap_id):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def quota_bypass_access(
|
||||
*,
|
||||
feature_id: str,
|
||||
club_id: Optional[int] = None,
|
||||
plan_id: Optional[str] = None,
|
||||
capability_id: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
return {
|
||||
"allowed": True,
|
||||
"limit": None,
|
||||
"used": 0,
|
||||
"remaining": None,
|
||||
"reason": "capability_quota_bypass",
|
||||
"platform_exempt": True,
|
||||
"quota_bypass_capability": capability_id,
|
||||
"plan_id": plan_id,
|
||||
"club_id": club_id,
|
||||
"feature_id": feature_id,
|
||||
}
|
||||
|
||||
|
||||
def list_quota_bypass_grants(cur) -> Dict[str, Any]:
|
||||
"""Admin: alle Grants zu Kontingent-Bypass-Capabilities."""
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT g.portal_role, g.capability_id, c.name AS capability_name,
|
||||
c.linked_feature_id, c.domain
|
||||
FROM portal_role_capability_grants g
|
||||
INNER JOIN capabilities c ON c.id = g.capability_id
|
||||
WHERE g.capability_id = %s
|
||||
OR g.capability_id LIKE %s
|
||||
OR c.domain = 'quota_bypass'
|
||||
ORDER BY g.portal_role, g.capability_id
|
||||
""",
|
||||
(QUOTA_BYPASS_ALL, f"{QUOTA_BYPASS_FEATURE_PREFIX}%"),
|
||||
)
|
||||
portal_grants = [dict(r) for r in cur.fetchall()]
|
||||
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT g.profile_id, p.email, p.name AS profile_name,
|
||||
g.capability_id, c.name AS capability_name, c.linked_feature_id,
|
||||
g.reason, g.granted_by_profile_id, g.created_at
|
||||
FROM profile_capability_grants g
|
||||
INNER JOIN profiles p ON p.id = g.profile_id
|
||||
INNER JOIN capabilities c ON c.id = g.capability_id
|
||||
WHERE g.capability_id = %s
|
||||
OR g.capability_id LIKE %s
|
||||
OR c.domain = 'quota_bypass'
|
||||
ORDER BY g.profile_id, g.capability_id
|
||||
""",
|
||||
(QUOTA_BYPASS_ALL, f"{QUOTA_BYPASS_FEATURE_PREFIX}%"),
|
||||
)
|
||||
profile_grants = [dict(r) for r in cur.fetchall()]
|
||||
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT id, name, domain, linked_feature_id
|
||||
FROM capabilities
|
||||
WHERE id = %s OR id LIKE %s OR domain = 'quota_bypass'
|
||||
ORDER BY id
|
||||
""",
|
||||
(QUOTA_BYPASS_ALL, f"{QUOTA_BYPASS_FEATURE_PREFIX}%"),
|
||||
)
|
||||
capabilities = [dict(r) for r in cur.fetchall()]
|
||||
|
||||
return {
|
||||
"capabilities": capabilities,
|
||||
"portal_role_grants": portal_grants,
|
||||
"profile_grants": profile_grants,
|
||||
}
|
||||
|
|
@ -3,7 +3,7 @@ Vereins-Mandanten: Mitgliedschaften, aktiver Vereinskontext, einfache Berechtigu
|
|||
|
||||
Siehe .claude/docs/technical/MULTI_TENANCY_RBAC_ARCHITECTURE.md
|
||||
"""
|
||||
from typing import Any, Dict, List, Optional, Set
|
||||
from typing import Any, Dict, List, Mapping, Optional, Set, Union
|
||||
|
||||
from fastapi import HTTPException
|
||||
|
||||
|
|
@ -155,6 +155,165 @@ def club_ids_for_profile_with_roles(cur, profile_id: int, *role_codes: str) -> S
|
|||
_GOVERNANCE_VISIBILITY = frozenset({"private", "club", "official"})
|
||||
|
||||
|
||||
def _library_governance_triplet(
|
||||
row: Mapping[str, Any],
|
||||
) -> tuple[str, Optional[int], Optional[int]]:
|
||||
"""visibility, club_id, created_by als normalisierte Werte für Bibliotheks-/Planungsartefakte."""
|
||||
vis = str(row.get("visibility") or "private").strip().lower()
|
||||
if vis not in _GOVERNANCE_VISIBILITY:
|
||||
vis = "private"
|
||||
cid_raw = row.get("club_id")
|
||||
try:
|
||||
ex_cid = int(cid_raw) if cid_raw is not None else None
|
||||
except (TypeError, ValueError):
|
||||
ex_cid = None
|
||||
cr_raw = row.get("created_by")
|
||||
try:
|
||||
creator = int(cr_raw) if cr_raw is not None else None
|
||||
except (TypeError, ValueError):
|
||||
creator = None
|
||||
return vis, ex_cid, creator
|
||||
|
||||
|
||||
def assert_library_content_editable(
|
||||
cur,
|
||||
profile_id: int,
|
||||
role: Optional[str],
|
||||
row: Union[Dict[str, Any], Mapping[str, Any]],
|
||||
) -> None:
|
||||
"""Inhalt bearbeiten: wie Übungen — Ersteller, Plattform-Admin oder Planungsberechtigter im Verein."""
|
||||
pid = int(profile_id)
|
||||
ex_vis, ex_cid, creator = _library_governance_triplet(row)
|
||||
if creator is not None and creator == pid:
|
||||
return
|
||||
if is_platform_admin(role):
|
||||
return
|
||||
if ex_vis == "club" and ex_cid is not None and can_plan_in_club(cur, pid, ex_cid, role):
|
||||
return
|
||||
raise HTTPException(status_code=403, detail="Keine Berechtigung zum Bearbeiten dieses Inhalts")
|
||||
|
||||
|
||||
def assert_library_content_deletable(
|
||||
cur,
|
||||
profile_id: int,
|
||||
role: Optional[str],
|
||||
row: Union[Dict[str, Any], Mapping[str, Any]],
|
||||
) -> None:
|
||||
"""Löschen: wie Übungen — privat Eigentümer/Vereins-Admin-Kontext, Verein nur Vereinsadmin, offiziell nur Plattform."""
|
||||
pid = int(profile_id)
|
||||
if is_platform_admin(role):
|
||||
return
|
||||
vis, cid, creator = _library_governance_triplet(row)
|
||||
try:
|
||||
creator_int = int(creator) if creator is not None else None
|
||||
except (TypeError, ValueError):
|
||||
creator_int = None
|
||||
|
||||
if vis == "official":
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="Offizielle Inhalte dürfen nur von Plattform-Admins gelöscht werden.",
|
||||
)
|
||||
if vis == "club":
|
||||
try:
|
||||
ex_club = int(cid) if cid is not None else None
|
||||
except (TypeError, ValueError):
|
||||
ex_club = None
|
||||
if ex_club is None:
|
||||
raise HTTPException(status_code=400, detail="Vereinsinhalt ohne gültige Vereinszuordnung")
|
||||
if not has_club_role(cur, pid, ex_club, "club_admin"):
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="Nur Vereins-Admins dürfen Vereins-Inhalte löschen.",
|
||||
)
|
||||
return
|
||||
|
||||
if creator_int is not None and creator_int == pid:
|
||||
return
|
||||
if creator_int is not None and club_admin_shares_club_with_creator(cur, pid, creator_int):
|
||||
return
|
||||
raise HTTPException(status_code=403, detail="Keine Berechtigung zum Löschen dieses Inhalts")
|
||||
|
||||
|
||||
def assert_library_content_governance_transition(
|
||||
cur,
|
||||
profile_id: int,
|
||||
role: Optional[str],
|
||||
prev_row: Union[Dict[str, Any], Mapping[str, Any]],
|
||||
next_visibility: str,
|
||||
next_club_id: Optional[int],
|
||||
) -> None:
|
||||
"""
|
||||
Zusätzliche Regeln beim Ändern von visibility/club_id (Zielzustand vor assert_valid_governance_visibility prüfen).
|
||||
|
||||
- Abwahl „official“: nur Plattform-Admin.
|
||||
- private → club: nur Ersteller (oder Plattform-Admin).
|
||||
- club → private: Ersteller, Vereinsadmin im bisherigen Verein oder Plattform-Admin.
|
||||
- club → club mit Wechsel club_id: Vereinsadmin im alten oder neuen Verein oder Plattform-Admin.
|
||||
"""
|
||||
nv = str(next_visibility or "").strip().lower()
|
||||
if nv not in _GOVERNANCE_VISIBILITY:
|
||||
raise HTTPException(status_code=400, detail="Ungültige visibility")
|
||||
|
||||
old_vis, old_cid, creator = _library_governance_triplet(prev_row)
|
||||
new_cid: Optional[int]
|
||||
try:
|
||||
new_cid = int(next_club_id) if next_club_id is not None else None
|
||||
except (TypeError, ValueError):
|
||||
new_cid = None
|
||||
|
||||
pid = int(profile_id)
|
||||
try:
|
||||
creator_int = int(creator) if creator is not None else None
|
||||
except (TypeError, ValueError):
|
||||
creator_int = None
|
||||
|
||||
if old_vis == nv and (nv != "club" or old_cid == new_cid):
|
||||
return
|
||||
|
||||
if old_vis == "official" and nv != "official":
|
||||
if not is_platform_admin(role):
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="Nur Plattform-Admins dürfen offizielle Inhalte auf Verein oder privat setzen.",
|
||||
)
|
||||
|
||||
if nv == "official":
|
||||
return
|
||||
|
||||
if old_vis == "private" and nv == "club":
|
||||
if creator_int is not None and creator_int != pid and not is_platform_admin(role):
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="Nur der Ersteller darf private Inhalte für den Verein freigeben.",
|
||||
)
|
||||
return
|
||||
|
||||
if old_vis == "club" and nv == "private":
|
||||
if is_platform_admin(role):
|
||||
return
|
||||
if creator_int is not None and creator_int == pid:
|
||||
return
|
||||
if old_cid is not None and has_club_role(cur, pid, old_cid, "club_admin"):
|
||||
return
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="Nur Ersteller, Vereins-Admins oder Plattform-Admins dürfen Vereins-Inhalte auf privat setzen.",
|
||||
)
|
||||
|
||||
if old_vis == "club" and nv == "club" and old_cid != new_cid:
|
||||
if is_platform_admin(role):
|
||||
return
|
||||
ok_old = old_cid is not None and has_club_role(cur, pid, old_cid, "club_admin")
|
||||
ok_new = new_cid is not None and has_club_role(cur, pid, new_cid, "club_admin")
|
||||
if ok_old or ok_new:
|
||||
return
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="Nur Vereins-Admins oder Plattform-Admins dürfen die Vereinszuordnung ändern.",
|
||||
)
|
||||
|
||||
|
||||
def assert_valid_governance_visibility(
|
||||
cur,
|
||||
profile_id: int,
|
||||
|
|
|
|||
|
|
@ -180,12 +180,17 @@ def init_db():
|
|||
cur.execute("SELECT COUNT(*) as count FROM ai_prompts WHERE slug='pipeline'")
|
||||
if cur.fetchone()['count'] == 0:
|
||||
cur.execute("""
|
||||
INSERT INTO ai_prompts (slug, name, description, template, active, sort_order)
|
||||
INSERT INTO ai_prompts (
|
||||
slug, display_name, description, template,
|
||||
category, output_format, active, sort_order
|
||||
)
|
||||
VALUES (
|
||||
'pipeline',
|
||||
'Mehrstufige Gesamtanalyse',
|
||||
'Master-Schalter für die gesamte Pipeline. Deaktiviere diese Analyse, um die Pipeline komplett zu verstecken.',
|
||||
'Master-Schalter fuer die gesamte Pipeline. Deaktiviere diese Zeile um die Pipeline zu verstecken.',
|
||||
'PIPELINE_MASTER',
|
||||
'admin',
|
||||
'text',
|
||||
true,
|
||||
-10
|
||||
)
|
||||
|
|
|
|||
113
backend/entitlements.py
Normal file
113
backend/entitlements.py
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
"""
|
||||
Zusammenstellung effektiver Rechte für GET /api/me/entitlements (M4).
|
||||
|
||||
Spez: CAPABILITY_CATALOG.v1.md §7.1, CLUB_MEMBERSHIP_AND_FEATURES.v1.md §8.1
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict, Optional, TYPE_CHECKING
|
||||
|
||||
from fastapi import HTTPException
|
||||
|
||||
from capabilities import club_roles_in_club, resolve_capabilities_map
|
||||
from club_quota_bypass import is_club_feature_quota_bypassed, quota_bypass_access
|
||||
from club_features import club_features_map
|
||||
from club_tenancy import is_platform_admin
|
||||
from tenant_context import _club_exists
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from tenant_context import TenantContext
|
||||
|
||||
|
||||
def _serialize_reset_at(value: Any) -> Optional[str]:
|
||||
if value is None:
|
||||
return None
|
||||
if isinstance(value, datetime):
|
||||
if value.tzinfo is None:
|
||||
return value.replace(tzinfo=None).isoformat() + "Z"
|
||||
return value.isoformat()
|
||||
return str(value)
|
||||
|
||||
|
||||
def _resolve_target_club_id(
|
||||
cur,
|
||||
tenant: "TenantContext",
|
||||
club_id: Optional[int],
|
||||
) -> Optional[int]:
|
||||
"""Effektiver Verein für Entitlements (Query > Tenant)."""
|
||||
target = int(club_id) if club_id is not None else tenant.effective_club_id
|
||||
if target is None:
|
||||
return None
|
||||
|
||||
if is_platform_admin(tenant.global_role):
|
||||
if not _club_exists(cur, target):
|
||||
raise HTTPException(status_code=400, detail="Verein nicht gefunden")
|
||||
return target
|
||||
|
||||
if target not in tenant.club_ids:
|
||||
raise HTTPException(status_code=403, detail="Keine Mitgliedschaft in diesem Verein")
|
||||
return target
|
||||
|
||||
|
||||
def build_me_entitlements(
|
||||
cur,
|
||||
tenant: "TenantContext",
|
||||
*,
|
||||
club_id: Optional[int] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Kombiniert Account-Status, Capabilities und Feature-Kontingente.
|
||||
"""
|
||||
target_club = _resolve_target_club_id(cur, tenant, club_id)
|
||||
club_roles = club_roles_in_club(tenant, target_club) if target_club is not None else []
|
||||
|
||||
capabilities = resolve_capabilities_map(cur, tenant, club_id=target_club)
|
||||
|
||||
features: Dict[str, Any] = {}
|
||||
plan_id = None
|
||||
if target_club is not None:
|
||||
raw = club_features_map(cur, target_club)
|
||||
plan_id = raw.get("plan_id")
|
||||
for fid, row in (raw.get("features") or {}).items():
|
||||
if is_club_feature_quota_bypassed(
|
||||
cur,
|
||||
profile_id=tenant.profile_id,
|
||||
portal_role=tenant.global_role,
|
||||
feature_id=fid,
|
||||
tenant=tenant,
|
||||
):
|
||||
ex = quota_bypass_access(
|
||||
feature_id=fid,
|
||||
club_id=target_club,
|
||||
plan_id=plan_id,
|
||||
)
|
||||
features[fid] = {
|
||||
"allowed": True,
|
||||
"used": row.get("used"),
|
||||
"limit": None,
|
||||
"remaining": None,
|
||||
"reset_at": _serialize_reset_at(row.get("reset_at")),
|
||||
"reason": ex.get("reason"),
|
||||
"platform_exempt": True,
|
||||
}
|
||||
else:
|
||||
features[fid] = {
|
||||
"allowed": row.get("allowed"),
|
||||
"used": row.get("used"),
|
||||
"limit": row.get("limit"),
|
||||
"remaining": row.get("remaining"),
|
||||
"reset_at": _serialize_reset_at(row.get("reset_at")),
|
||||
"reason": row.get("reason"),
|
||||
"platform_exempt": False,
|
||||
}
|
||||
|
||||
return {
|
||||
"account_state": tenant.account_state,
|
||||
"portal_role": tenant.global_role,
|
||||
"club_id": target_club,
|
||||
"plan_id": plan_id,
|
||||
"club_roles": club_roles,
|
||||
"capabilities": capabilities,
|
||||
"features": features,
|
||||
}
|
||||
1122
backend/exercise_ai.py
Normal file
1122
backend/exercise_ai.py
Normal file
File diff suppressed because it is too large
Load Diff
536
backend/exercise_enrichment.py
Normal file
536
backend/exercise_enrichment.py
Normal file
|
|
@ -0,0 +1,536 @@
|
|||
"""
|
||||
Superadmin-Werkzeug: Übungs-Anreicherung per KI (Skills + optional Metadaten).
|
||||
|
||||
Wiederverwendet run_exercise_form_ai_suggestion / exercise_ai — keine neue OpenRouter-Pipeline.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Dict, List, Literal, Optional
|
||||
|
||||
from ai_prompt_context import ExerciseFormAiPromptContext
|
||||
from ai_prompt_job import run_exercise_form_ai_suggestion
|
||||
from exercise_ai import strip_html_to_plain
|
||||
from exercise_rich_text import normalize_inline_exercise_media_markup
|
||||
|
||||
from routers.exercises import (
|
||||
enrich_exercise_detail,
|
||||
normalize_exercise_skill_intensity,
|
||||
normalize_exercise_skill_level,
|
||||
)
|
||||
|
||||
SkillMergeMode = Literal["additive", "replace_ai_only", "replace_all"]
|
||||
|
||||
SKILL_MERGE_MODES = frozenset({"additive", "replace_ai_only", "replace_all"})
|
||||
DEFAULT_SET_STATUS = "in_review"
|
||||
# Max. IDs pro Apply-HTTP-Anfrage (kein LLM).
|
||||
MAX_BATCH_EXERCISES = 50
|
||||
# Preview: pro Request nur wenige Übungen — sonst Gateway-504 (Fritz!Box o.ä. ~60s).
|
||||
MAX_PREVIEW_BATCH_EXERCISES = 3
|
||||
|
||||
_INSTRUCTION_FIELDS = ("goal", "execution", "preparation", "trainer_notes")
|
||||
_SKILL_COMPARE_KEYS = ("intensity", "required_level", "target_level", "is_primary")
|
||||
|
||||
|
||||
def _focus_areas_ai_ctx_from_detail(exercise: Dict[str, Any]) -> list[tuple[int, bool]]:
|
||||
rows: list[tuple[int, bool]] = []
|
||||
for row in exercise.get("focus_areas") or []:
|
||||
if not isinstance(row, dict):
|
||||
continue
|
||||
try:
|
||||
fid = int(row.get("focus_area_id"))
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
if fid < 1:
|
||||
continue
|
||||
rows.append((fid, bool(row.get("is_primary"))))
|
||||
rows.sort(key=lambda x: (not x[1], x[0]))
|
||||
return rows
|
||||
|
||||
|
||||
def _focus_area_hint_from_detail(exercise: Dict[str, Any]) -> str:
|
||||
parts: List[str] = []
|
||||
for row in exercise.get("focus_areas") or []:
|
||||
if isinstance(row, dict):
|
||||
nm = (row.get("name") or "").strip()
|
||||
if nm:
|
||||
parts.append(nm)
|
||||
txt = ", ".join(parts).strip()
|
||||
if len(txt) > 900:
|
||||
return txt[:899] + "…"
|
||||
return txt
|
||||
|
||||
|
||||
def build_form_context_from_exercise(exercise: Dict[str, Any]) -> ExerciseFormAiPromptContext:
|
||||
focus = _focus_area_hint_from_detail(exercise)
|
||||
fctx = _focus_areas_ai_ctx_from_detail(exercise)
|
||||
return ExerciseFormAiPromptContext.from_focus_tuples(
|
||||
title=str(exercise.get("title") or "").strip(),
|
||||
goal=exercise.get("goal"),
|
||||
execution=exercise.get("execution"),
|
||||
preparation=exercise.get("preparation"),
|
||||
trainer_notes=exercise.get("trainer_notes"),
|
||||
focus_hint=focus or None,
|
||||
focus_tuples=fctx or None,
|
||||
)
|
||||
|
||||
|
||||
def validate_exercise_for_enrichment(
|
||||
exercise: Dict[str, Any],
|
||||
*,
|
||||
want_skills: bool = False,
|
||||
want_summary: bool = False,
|
||||
want_instructions: bool = False,
|
||||
) -> Optional[str]:
|
||||
title = str(exercise.get("title") or "").strip()
|
||||
if not title:
|
||||
return "Titel fehlt"
|
||||
|
||||
ctx = build_form_context_from_exercise(exercise)
|
||||
g_plain = strip_html_to_plain(exercise.get("goal"))
|
||||
e_plain = strip_html_to_plain(exercise.get("execution"))
|
||||
|
||||
if want_skills or want_summary:
|
||||
if not (g_plain.strip() or e_plain.strip()):
|
||||
return "Mindestens Ziel oder Durchführung muss Inhalt liefern (für Skills/Kurzfassung)"
|
||||
|
||||
if want_instructions and not ctx.has_instruction_source_text():
|
||||
return "Für Anleitungs-Überarbeitung fehlt Ausgangstext (Titel oder Anleitungsfeld)"
|
||||
|
||||
if not (want_skills or want_summary or want_instructions):
|
||||
return "Kein Anreicherungsmodus aktiv"
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _normalize_skill_row(raw: Dict[str, Any], *, ai_suggested: bool) -> Dict[str, Any]:
|
||||
return {
|
||||
"skill_id": int(raw["skill_id"]),
|
||||
"skill_name": (raw.get("skill_name") or "").strip() or f"Skill #{raw['skill_id']}",
|
||||
"skill_category": raw.get("skill_category"),
|
||||
"is_primary": bool(raw.get("is_primary")),
|
||||
"intensity": normalize_exercise_skill_intensity(raw.get("intensity")),
|
||||
"required_level": normalize_exercise_skill_level(raw.get("required_level")),
|
||||
"target_level": normalize_exercise_skill_level(raw.get("target_level")),
|
||||
"ai_suggested": ai_suggested,
|
||||
}
|
||||
|
||||
|
||||
def _skill_meta_differs(a: Dict[str, Any], b: Dict[str, Any]) -> bool:
|
||||
for k in _SKILL_COMPARE_KEYS:
|
||||
av = a.get(k)
|
||||
bv = b.get(k)
|
||||
if k in ("required_level", "target_level"):
|
||||
av = normalize_exercise_skill_level(av)
|
||||
bv = normalize_exercise_skill_level(bv)
|
||||
elif k == "intensity":
|
||||
av = normalize_exercise_skill_intensity(av)
|
||||
bv = normalize_exercise_skill_intensity(bv)
|
||||
elif k == "is_primary":
|
||||
av = bool(av)
|
||||
bv = bool(bv)
|
||||
if av != bv:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def merge_skills(
|
||||
existing: List[Dict[str, Any]],
|
||||
suggested: List[Dict[str, Any]],
|
||||
mode: SkillMergeMode,
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""Merge-Modi: additive | replace_ai_only | replace_all (alle KI-Skills mit ai_suggested=true)."""
|
||||
existing_norm = [_normalize_skill_row(s, ai_suggested=bool(s.get("ai_suggested"))) for s in existing]
|
||||
suggested_norm = [_normalize_skill_row(s, ai_suggested=True) for s in suggested]
|
||||
|
||||
suggested_by_id = {int(s["skill_id"]): s for s in suggested_norm}
|
||||
|
||||
if mode == "replace_all":
|
||||
return list(suggested_norm)
|
||||
|
||||
if mode == "replace_ai_only":
|
||||
manual = [s for s in existing_norm if not s.get("ai_suggested")]
|
||||
manual_ids = {int(s["skill_id"]) for s in manual}
|
||||
result = list(manual)
|
||||
for s in suggested_norm:
|
||||
sid = int(s["skill_id"])
|
||||
if sid in manual_ids:
|
||||
continue
|
||||
result.append(s)
|
||||
return result
|
||||
|
||||
# additive
|
||||
result: List[Dict[str, Any]] = []
|
||||
seen: set[int] = set()
|
||||
for s in existing_norm:
|
||||
sid = int(s["skill_id"])
|
||||
seen.add(sid)
|
||||
if sid in suggested_by_id and s.get("ai_suggested"):
|
||||
merged = {**s, **suggested_by_id[sid], "ai_suggested": True}
|
||||
result.append(merged)
|
||||
else:
|
||||
result.append(dict(s))
|
||||
for s in suggested_norm:
|
||||
sid = int(s["skill_id"])
|
||||
if sid not in seen:
|
||||
result.append(s)
|
||||
seen.add(sid)
|
||||
return result
|
||||
|
||||
|
||||
def compute_skill_diff(
|
||||
before: List[Dict[str, Any]],
|
||||
after: List[Dict[str, Any]],
|
||||
) -> Dict[str, Any]:
|
||||
before_ids = {int(s["skill_id"]): s for s in before}
|
||||
after_ids = {int(s["skill_id"]): s for s in after}
|
||||
added = [after_ids[i] for i in sorted(after_ids) if i not in before_ids]
|
||||
removed = [before_ids[i] for i in sorted(before_ids) if i not in after_ids]
|
||||
changed: List[Dict[str, Any]] = []
|
||||
for sid in before_ids:
|
||||
if sid in after_ids and _skill_meta_differs(before_ids[sid], after_ids[sid]):
|
||||
changed.append(
|
||||
{
|
||||
"skill_id": sid,
|
||||
"skill_name": after_ids[sid].get("skill_name") or before_ids[sid].get("skill_name"),
|
||||
"before": before_ids[sid],
|
||||
"after": after_ids[sid],
|
||||
}
|
||||
)
|
||||
kept = [
|
||||
before_ids[i]
|
||||
for i in sorted(before_ids)
|
||||
if i in after_ids and i not in {c["skill_id"] for c in changed}
|
||||
]
|
||||
return {"added": added, "removed": removed, "changed": changed, "kept": kept}
|
||||
|
||||
|
||||
def _skills_from_ai_payload(payload: Dict[str, Any]) -> List[Dict[str, Any]]:
|
||||
rows = payload.get("skills")
|
||||
if not isinstance(rows, list):
|
||||
return []
|
||||
return [_normalize_skill_row(r, ai_suggested=True) for r in rows if isinstance(r, dict) and r.get("skill_id")]
|
||||
|
||||
|
||||
def _summary_from_ai_payload(payload: Dict[str, Any]) -> Optional[str]:
|
||||
block = payload.get("summary")
|
||||
if isinstance(block, dict):
|
||||
text = (block.get("text") or "").strip()
|
||||
return text or None
|
||||
if isinstance(block, str) and block.strip():
|
||||
return block.strip()
|
||||
return None
|
||||
|
||||
|
||||
def _instructions_from_ai_payload(payload: Dict[str, Any]) -> Dict[str, str]:
|
||||
block = payload.get("instructions")
|
||||
if not isinstance(block, dict):
|
||||
return {}
|
||||
fields = block.get("fields")
|
||||
if not isinstance(fields, dict):
|
||||
return {}
|
||||
out: Dict[str, str] = {}
|
||||
for key in _INSTRUCTION_FIELDS:
|
||||
val = fields.get(key)
|
||||
if val is not None and str(val).strip():
|
||||
out[key] = str(val).strip()
|
||||
return out
|
||||
|
||||
|
||||
def _instruction_snapshot(exercise: Dict[str, Any]) -> Dict[str, str]:
|
||||
out: Dict[str, str] = {}
|
||||
for key in _INSTRUCTION_FIELDS:
|
||||
raw = exercise.get(key)
|
||||
plain = strip_html_to_plain(raw, max_len=400) if raw else ""
|
||||
if plain.strip():
|
||||
out[key] = plain.strip()
|
||||
return out
|
||||
|
||||
|
||||
def compute_instruction_diff(
|
||||
before: Dict[str, str],
|
||||
after: Dict[str, str],
|
||||
) -> Dict[str, Any]:
|
||||
changed: List[Dict[str, Any]] = []
|
||||
added: List[str] = []
|
||||
for key in _INSTRUCTION_FIELDS:
|
||||
b = (before.get(key) or "").strip()
|
||||
a = (after.get(key) or "").strip()
|
||||
if not a:
|
||||
continue
|
||||
if not b:
|
||||
added.append(key)
|
||||
elif b != strip_html_to_plain(a, max_len=400).strip() and b != a:
|
||||
changed.append({"field": key, "before_plain": b, "after_html": a})
|
||||
return {"changed_fields": changed, "added_fields": added}
|
||||
|
||||
|
||||
def preview_exercise_enrichment(
|
||||
cur,
|
||||
exercise_id: int,
|
||||
*,
|
||||
want_skills: bool = True,
|
||||
want_summary: bool = False,
|
||||
want_instructions: bool = False,
|
||||
merge_mode: SkillMergeMode = "additive",
|
||||
) -> Dict[str, Any]:
|
||||
exercise = enrich_exercise_detail(exercise_id, cur)
|
||||
if not exercise:
|
||||
return {"exercise_id": exercise_id, "ok": False, "error": "Übung nicht gefunden"}
|
||||
|
||||
skip_reason = validate_exercise_for_enrichment(
|
||||
exercise,
|
||||
want_skills=want_skills,
|
||||
want_summary=want_summary,
|
||||
want_instructions=want_instructions,
|
||||
)
|
||||
if skip_reason:
|
||||
return {
|
||||
"exercise_id": exercise_id,
|
||||
"ok": False,
|
||||
"skipped": True,
|
||||
"error": skip_reason,
|
||||
"title": exercise.get("title"),
|
||||
"status": exercise.get("status"),
|
||||
}
|
||||
|
||||
existing = exercise.get("skills") or []
|
||||
suggested: List[Dict[str, Any]] = []
|
||||
ai_meta: Dict[str, Any] = {}
|
||||
payload: Dict[str, Any] = {}
|
||||
suggested_summary: Optional[str] = None
|
||||
suggested_instructions: Dict[str, str] = {}
|
||||
|
||||
if want_skills or want_summary or want_instructions:
|
||||
ctx = build_form_context_from_exercise(exercise)
|
||||
payload = run_exercise_form_ai_suggestion(
|
||||
cur,
|
||||
ctx,
|
||||
want_summary=want_summary,
|
||||
want_skills=want_skills,
|
||||
want_instructions=want_instructions,
|
||||
)
|
||||
if want_skills:
|
||||
suggested = _skills_from_ai_payload(payload)
|
||||
if want_summary:
|
||||
suggested_summary = _summary_from_ai_payload(payload)
|
||||
if want_instructions:
|
||||
suggested_instructions = _instructions_from_ai_payload(payload)
|
||||
ai_meta = {
|
||||
"models": payload.get("models_by_slug") or {},
|
||||
"llm_calls": sum([want_skills, want_summary, want_instructions]),
|
||||
}
|
||||
|
||||
merged = merge_skills(existing, suggested, merge_mode) if want_skills else list(existing)
|
||||
diff = compute_skill_diff(existing, merged) if want_skills else None
|
||||
|
||||
existing_summary = (exercise.get("summary") or "").strip() or None
|
||||
instr_before = _instruction_snapshot(exercise)
|
||||
instr_after_plain = {
|
||||
k: strip_html_to_plain(v, max_len=400) for k, v in suggested_instructions.items()
|
||||
}
|
||||
instruction_diff = (
|
||||
compute_instruction_diff(instr_before, instr_after_plain) if want_instructions else None
|
||||
)
|
||||
|
||||
return {
|
||||
"exercise_id": exercise_id,
|
||||
"ok": True,
|
||||
"title": exercise.get("title"),
|
||||
"status": exercise.get("status"),
|
||||
"visibility": exercise.get("visibility"),
|
||||
"primary_focus_name": _primary_focus_from_exercise(exercise),
|
||||
"existing_skills": existing,
|
||||
"suggested_skills": suggested,
|
||||
"merged_skills": merged,
|
||||
"diff": diff,
|
||||
"existing_summary": existing_summary,
|
||||
"suggested_summary": suggested_summary,
|
||||
"existing_instructions": instr_before,
|
||||
"suggested_instructions": suggested_instructions,
|
||||
"instruction_diff": instruction_diff,
|
||||
"ai_meta": ai_meta,
|
||||
}
|
||||
|
||||
|
||||
def _primary_focus_from_exercise(exercise: Dict[str, Any]) -> Optional[str]:
|
||||
for row in exercise.get("focus_areas") or []:
|
||||
if isinstance(row, dict) and row.get("is_primary"):
|
||||
return (row.get("name") or "").strip() or None
|
||||
for row in exercise.get("focus_areas") or []:
|
||||
if isinstance(row, dict):
|
||||
nm = (row.get("name") or "").strip()
|
||||
if nm:
|
||||
return nm
|
||||
return None
|
||||
|
||||
|
||||
def persist_merged_skills(cur, exercise_id: int, merged: List[Dict[str, Any]], merge_mode: SkillMergeMode) -> None:
|
||||
if merge_mode == "replace_all":
|
||||
cur.execute("DELETE FROM exercise_skills WHERE exercise_id = %s", (exercise_id,))
|
||||
elif merge_mode == "replace_ai_only":
|
||||
cur.execute(
|
||||
"DELETE FROM exercise_skills WHERE exercise_id = %s AND ai_suggested = true",
|
||||
(exercise_id,),
|
||||
)
|
||||
|
||||
for sk in merged:
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO exercise_skills
|
||||
(exercise_id, skill_id, is_primary, intensity, required_level, target_level, ai_suggested)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s)
|
||||
ON CONFLICT (exercise_id, skill_id) DO UPDATE SET
|
||||
intensity = CASE
|
||||
WHEN exercise_skills.ai_suggested = false AND %s = 'additive'
|
||||
THEN exercise_skills.intensity ELSE EXCLUDED.intensity END,
|
||||
required_level = CASE
|
||||
WHEN exercise_skills.ai_suggested = false AND %s = 'additive'
|
||||
THEN exercise_skills.required_level ELSE EXCLUDED.required_level END,
|
||||
target_level = CASE
|
||||
WHEN exercise_skills.ai_suggested = false AND %s = 'additive'
|
||||
THEN exercise_skills.target_level ELSE EXCLUDED.target_level END,
|
||||
is_primary = CASE
|
||||
WHEN exercise_skills.ai_suggested = false AND %s = 'additive'
|
||||
THEN exercise_skills.is_primary ELSE EXCLUDED.is_primary END,
|
||||
ai_suggested = CASE
|
||||
WHEN exercise_skills.ai_suggested = false AND %s = 'additive'
|
||||
THEN exercise_skills.ai_suggested ELSE EXCLUDED.ai_suggested END
|
||||
""",
|
||||
(
|
||||
exercise_id,
|
||||
int(sk["skill_id"]),
|
||||
bool(sk.get("is_primary")),
|
||||
normalize_exercise_skill_intensity(sk.get("intensity")),
|
||||
normalize_exercise_skill_level(sk.get("required_level")),
|
||||
normalize_exercise_skill_level(sk.get("target_level")),
|
||||
bool(sk.get("ai_suggested")),
|
||||
merge_mode,
|
||||
merge_mode,
|
||||
merge_mode,
|
||||
merge_mode,
|
||||
merge_mode,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def _normalize_instruction_fields(fields: Optional[Dict[str, Any]]) -> Dict[str, str]:
|
||||
if not fields:
|
||||
return {}
|
||||
out: Dict[str, str] = {}
|
||||
for key in _INSTRUCTION_FIELDS:
|
||||
if key not in fields:
|
||||
continue
|
||||
raw = fields.get(key)
|
||||
if raw is None or not str(raw).strip():
|
||||
continue
|
||||
out[key] = normalize_inline_exercise_media_markup(str(raw).strip())
|
||||
return out
|
||||
|
||||
|
||||
def apply_exercise_enrichment(
|
||||
cur,
|
||||
exercise_id: int,
|
||||
*,
|
||||
merged_skills: Optional[List[Dict[str, Any]]] = None,
|
||||
merge_mode: SkillMergeMode = "additive",
|
||||
set_status: Optional[str] = DEFAULT_SET_STATUS,
|
||||
apply_skills: bool = False,
|
||||
summary_text: Optional[str] = None,
|
||||
apply_summary: bool = False,
|
||||
instruction_fields: Optional[Dict[str, Any]] = None,
|
||||
apply_instructions: bool = False,
|
||||
) -> Dict[str, Any]:
|
||||
exercise = enrich_exercise_detail(exercise_id, cur)
|
||||
if not exercise:
|
||||
return {"exercise_id": exercise_id, "ok": False, "error": "Übung nicht gefunden"}
|
||||
|
||||
skip_reason = validate_exercise_for_enrichment(
|
||||
exercise,
|
||||
want_skills=apply_skills,
|
||||
want_summary=apply_summary,
|
||||
want_instructions=apply_instructions,
|
||||
)
|
||||
if skip_reason:
|
||||
return {
|
||||
"exercise_id": exercise_id,
|
||||
"ok": False,
|
||||
"skipped": True,
|
||||
"error": skip_reason,
|
||||
}
|
||||
|
||||
skills_list = merged_skills or []
|
||||
if apply_skills:
|
||||
if not skills_list and merge_mode != "replace_all":
|
||||
return {
|
||||
"exercise_id": exercise_id,
|
||||
"ok": False,
|
||||
"error": "Keine Skills zum Anwenden",
|
||||
}
|
||||
persist_merged_skills(cur, exercise_id, skills_list, merge_mode)
|
||||
|
||||
sets: List[str] = []
|
||||
vals: List[Any] = []
|
||||
|
||||
if apply_summary and summary_text is not None:
|
||||
text = str(summary_text).strip()
|
||||
if text:
|
||||
sets.extend(["summary = %s", "summary_ai_generated = true"])
|
||||
vals.append(text[:220])
|
||||
|
||||
if apply_instructions:
|
||||
norm = _normalize_instruction_fields(instruction_fields)
|
||||
for key, val in norm.items():
|
||||
sets.append(f"{key} = %s")
|
||||
vals.append(val)
|
||||
|
||||
new_status = (set_status or "").strip().lower() or None
|
||||
if new_status:
|
||||
if new_status == "approved":
|
||||
return {
|
||||
"exercise_id": exercise_id,
|
||||
"ok": False,
|
||||
"error": "Automatisches Freigeben (approved) ist nicht erlaubt",
|
||||
}
|
||||
if new_status not in ("draft", "in_review", "archived"):
|
||||
return {"exercise_id": exercise_id, "ok": False, "error": "Ungültiger Ziel-Status"}
|
||||
sets.append("status = %s")
|
||||
vals.append(new_status)
|
||||
|
||||
if sets:
|
||||
sets.append("updated_at = NOW()")
|
||||
vals.append(exercise_id)
|
||||
cur.execute(
|
||||
f"UPDATE exercises SET {', '.join(sets)} WHERE id = %s",
|
||||
tuple(vals),
|
||||
)
|
||||
elif not apply_skills:
|
||||
return {"exercise_id": exercise_id, "ok": False, "error": "Nichts anzuwenden"}
|
||||
|
||||
return {
|
||||
"exercise_id": exercise_id,
|
||||
"ok": True,
|
||||
"status": new_status or exercise.get("status"),
|
||||
"skills_applied": len(skills_list) if apply_skills else 0,
|
||||
"summary_applied": apply_summary and bool(summary_text and str(summary_text).strip()),
|
||||
"instructions_applied": apply_instructions and bool(_normalize_instruction_fields(instruction_fields)),
|
||||
}
|
||||
|
||||
|
||||
def estimate_llm_calls(
|
||||
*,
|
||||
exercise_count: int,
|
||||
want_skills: bool,
|
||||
want_summary: bool,
|
||||
want_instructions: bool = False,
|
||||
) -> Dict[str, Any]:
|
||||
per_skills = exercise_count if want_skills else 0
|
||||
per_summary = exercise_count if want_summary else 0
|
||||
per_instructions = exercise_count if want_instructions else 0
|
||||
total = per_skills + per_summary + per_instructions
|
||||
return {
|
||||
"total": total,
|
||||
"per_exercise": sum([want_skills, want_summary, want_instructions]),
|
||||
"skills": per_skills,
|
||||
"summary": per_summary,
|
||||
"instructions": per_instructions,
|
||||
}
|
||||
16
backend/fastapi_param_unwrap.py
Normal file
16
backend/fastapi_param_unwrap.py
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
"""Hilfen für direkte Python-Aufrufe von FastAPI-Route-Handlern (ohne Request-Kontext)."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
|
||||
def unwrap_query_default(value: Any) -> Any:
|
||||
"""
|
||||
Parameter mit Annotation ``= Query(default=…)`` sind im Funktionskörper ``fastapi.params.Query``-Instanzen,
|
||||
solange FastAPI sie nicht durch echte Werte ersetzt hat (interne Aufrufe, Aggregat-Endpunkte).
|
||||
"""
|
||||
try:
|
||||
from fastapi.params import Query
|
||||
except ImportError:
|
||||
return value
|
||||
return value.default if isinstance(value, Query) else value
|
||||
|
|
@ -52,6 +52,28 @@ else:
|
|||
print(f"[FAIL] Migration-Laufzeitfehler: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
# Registry-first: Module → DB (nur registrierte Rechte/Kontingente in Admin-Matrix)
|
||||
if os.getenv("SKIP_DB_MIGRATE", "").strip().lower() not in ("1", "true", "yes"):
|
||||
try:
|
||||
from rights_registry import sync_rights_registry_to_db
|
||||
|
||||
counts = sync_rights_registry_to_db()
|
||||
print(
|
||||
f"[OK] Rights registry sync: {counts['capabilities']} capabilities, "
|
||||
f"{counts['features']} features"
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"[FAIL] Rights registry sync: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
from club_features import club_feature_enforcement_enabled
|
||||
|
||||
_cfe = os.getenv("CLUB_FEATURE_ENFORCE", "0")
|
||||
print(
|
||||
f"[OK] CLUB_FEATURE_ENFORCE raw={_cfe!r} "
|
||||
f"active={club_feature_enforcement_enabled()}"
|
||||
)
|
||||
|
||||
from routers.auth import limiter as auth_rate_limiter
|
||||
|
||||
# OpenAPI: in Produktion standardmäßig aus (Schema nicht öffentlich). Notfall: PUBLIC_OPENAPI=1
|
||||
|
|
@ -82,11 +104,39 @@ app.add_middleware(
|
|||
CORSMiddleware,
|
||||
allow_origins=ALLOWED_ORIGINS,
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
allow_methods=["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
|
||||
allow_headers=["Content-Type", "X-Auth-Token", "X-Active-Club-Id"],
|
||||
)
|
||||
|
||||
|
||||
@app.middleware("http")
|
||||
async def account_onboarding_api_gate(request: Request, call_next):
|
||||
"""
|
||||
Phase A: Domänen-APIs für unverified / verified_pending_club sperren.
|
||||
Siehe account_onboarding_gate.py und MEMBERSHIP_RBAC_DECISIONS_2026-06.md §1.1
|
||||
"""
|
||||
from account_onboarding_gate import evaluate_request_gate
|
||||
|
||||
token = request.headers.get("x-auth-token") or request.headers.get("X-Auth-Token")
|
||||
allowed, reason, _state = evaluate_request_gate(
|
||||
token,
|
||||
request.url.path,
|
||||
request.method,
|
||||
)
|
||||
if not allowed:
|
||||
return JSONResponse(
|
||||
status_code=403,
|
||||
content={
|
||||
"detail": (
|
||||
"Zugriff erst nach E-Mail-Bestätigung und Vereinsmitgliedschaft möglich. "
|
||||
"Du kannst einen Beitrittsantrag stellen oder dein Konto in den Einstellungen verwalten."
|
||||
),
|
||||
"reason": reason,
|
||||
},
|
||||
)
|
||||
return await call_next(request)
|
||||
|
||||
|
||||
@app.middleware("http")
|
||||
async def add_api_security_headers(request: Request, call_next):
|
||||
"""Konsistente Basis-Header auch für rein JSON-Responses (MIME-Sniffing)."""
|
||||
|
|
@ -193,7 +243,7 @@ def read_root():
|
|||
return out
|
||||
|
||||
# Register routers
|
||||
from routers import auth, profiles, exercises, exercise_progression_graphs, clubs, club_memberships, club_join_requests, admin_users, platform_media_storage, media_assets, skills, training_planning, training_framework_programs, catalogs, maturity_models, matrix_stack_bundle, import_wiki, import_wiki_admin
|
||||
from routers import auth, profiles, exercises, exercise_progression_graphs, clubs, club_memberships, club_join_requests, club_creation_requests, admin_users, admin_user_content, admin_rights, me_entitlements, platform_media_storage, media_assets, skills, skill_profiles, training_planning, planning_exercise_suggest, dashboard, training_modules, training_framework_programs, catalogs, maturity_models, matrix_stack_bundle, matrix_editor, import_wiki, import_wiki_admin, legal_documents, content_reports, ai_prompts_admin, ai_skill_retrieval_admin, exercise_enrichment_admin
|
||||
|
||||
app.include_router(auth.router)
|
||||
app.include_router(profiles.router)
|
||||
|
|
@ -202,17 +252,33 @@ app.include_router(exercise_progression_graphs.router)
|
|||
app.include_router(clubs.router)
|
||||
app.include_router(club_memberships.router)
|
||||
app.include_router(club_join_requests.router)
|
||||
app.include_router(club_creation_requests.router)
|
||||
app.include_router(admin_users.router)
|
||||
app.include_router(admin_user_content.router)
|
||||
app.include_router(admin_rights.router)
|
||||
app.include_router(me_entitlements.router)
|
||||
app.include_router(platform_media_storage.router)
|
||||
app.include_router(media_assets.router)
|
||||
app.include_router(media_assets.admin_rights_router)
|
||||
app.include_router(media_assets.admin_legal_hold_router)
|
||||
app.include_router(skills.router)
|
||||
app.include_router(skill_profiles.router)
|
||||
app.include_router(training_planning.router)
|
||||
app.include_router(planning_exercise_suggest.router)
|
||||
app.include_router(dashboard.router)
|
||||
app.include_router(training_modules.router)
|
||||
app.include_router(training_framework_programs.router)
|
||||
app.include_router(catalogs.router)
|
||||
app.include_router(maturity_models.router)
|
||||
app.include_router(matrix_stack_bundle.router)
|
||||
app.include_router(matrix_editor.router)
|
||||
app.include_router(import_wiki.router)
|
||||
app.include_router(import_wiki_admin.router)
|
||||
app.include_router(legal_documents.router)
|
||||
app.include_router(content_reports.router)
|
||||
app.include_router(ai_prompts_admin.router)
|
||||
app.include_router(ai_skill_retrieval_admin.router)
|
||||
app.include_router(exercise_enrichment_admin.router)
|
||||
|
||||
# Lokale Übungs-Medien: standardmäßig nur über geschützten API-Pfad
|
||||
# GET /api/exercises/{id}/media/{mid}/file (?ssetoken für <img>/<video>).
|
||||
|
|
|
|||
247
backend/media_legal_hold.py
Normal file
247
backend/media_legal_hold.py
Normal file
|
|
@ -0,0 +1,247 @@
|
|||
"""P-11: Legal-Hold-Services fuer Medien-Assets.
|
||||
|
||||
Sofortsperrung bei Rechtsverletzungen (Compliance-Paket P-11).
|
||||
|
||||
Klare Trennung:
|
||||
- P-06: Rechteerklaerungs-/Deklarationsstatus (rights_status='declared'/'legacy_unreviewed')
|
||||
- P-11: Legal-Hold-Sperre (legal_hold_active + Metadaten)
|
||||
- P-03: Normaler Papierkorb-Lifecycle (lifecycle_state)
|
||||
- P-13: Meldeverfahren (spaeter; nutzt denselben set_legal_hold-Service)
|
||||
|
||||
Berechtigungen:
|
||||
- Setzen und Aufheben: ausschliesslich Superadmin.
|
||||
- Lesezugriff auf Legal-Hold-Status: Superadmin und Plattform-Admin.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Optional
|
||||
|
||||
from fastapi import HTTPException
|
||||
|
||||
from club_tenancy import is_superadmin
|
||||
from media_rights import write_audit_log_entry
|
||||
|
||||
LEGAL_HOLD_REASON_CODES = {
|
||||
"rights_dispute",
|
||||
"consent_withdrawn",
|
||||
"privacy_complaint",
|
||||
"copyright_complaint",
|
||||
"youth_protection",
|
||||
"illegal_content",
|
||||
"other",
|
||||
}
|
||||
|
||||
|
||||
def assert_superadmin_for_legal_hold(role: Optional[str]) -> None:
|
||||
if not is_superadmin(role):
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="Legal-Hold-Aktionen sind nur fuer Superadmins verfuegbar.",
|
||||
)
|
||||
|
||||
|
||||
def is_media_available_for_normal_use(asset: dict) -> bool:
|
||||
"""True wenn das Medium fuer normale Nutzerpfade verfuegbar ist.
|
||||
|
||||
Ein Medium ist NICHT verfuegbar wenn legal_hold_active = True,
|
||||
unabhaengig vom lifecycle_state.
|
||||
"""
|
||||
return not bool(asset.get("legal_hold_active"))
|
||||
|
||||
|
||||
def assert_not_under_legal_hold(asset: dict) -> None:
|
||||
"""Wirft HTTPException 403 wenn das Medium unter Legal Hold steht."""
|
||||
if bool(asset.get("legal_hold_active")):
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail={
|
||||
"code": "LEGAL_HOLD_ACTIVE",
|
||||
"message": (
|
||||
"Dieses Medium ist durch einen Administrator sofort gesperrt und "
|
||||
"kann nicht verwendet werden."
|
||||
),
|
||||
"asset_id": asset.get("id"),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def set_legal_hold(
|
||||
cur: Any,
|
||||
conn: Any,
|
||||
asset_id: int,
|
||||
acting_profile_id: int,
|
||||
reason_code: str,
|
||||
reason_note: Optional[str],
|
||||
) -> dict:
|
||||
"""Setzt Legal Hold auf ein Medium. Nur Superadmin.
|
||||
|
||||
Wirkung:
|
||||
- legal_hold_active = TRUE
|
||||
- legal_hold_reason_code, legal_hold_reason_note, legal_hold_set_by_profile_id, legal_hold_set_at
|
||||
- rights_status = 'blocked' (Spiegel fuer schnelle Checks)
|
||||
- Audit-Log-Eintrag 'legal_hold_set'
|
||||
|
||||
Gibt das aktualisierte Asset-Dict zurueck.
|
||||
"""
|
||||
if reason_code not in LEGAL_HOLD_REASON_CODES:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Ungueltiger reason_code. Erlaubt: {sorted(LEGAL_HOLD_REASON_CODES)}",
|
||||
)
|
||||
if not reason_note or not reason_note.strip():
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="reason_note (Begruendung) ist Pflicht beim Setzen eines Legal Holds.",
|
||||
)
|
||||
|
||||
cur.execute(
|
||||
"""SELECT id, legal_hold_active, rights_status, lifecycle_state, visibility
|
||||
FROM media_assets WHERE id = %s""",
|
||||
(asset_id,),
|
||||
)
|
||||
from db import r2d
|
||||
row = cur.fetchone()
|
||||
if not row:
|
||||
raise HTTPException(status_code=404, detail="Medium nicht gefunden")
|
||||
asset = r2d(row)
|
||||
|
||||
if bool(asset.get("legal_hold_active")):
|
||||
raise HTTPException(
|
||||
status_code=409,
|
||||
detail="Dieses Medium befindet sich bereits unter Legal Hold.",
|
||||
)
|
||||
|
||||
old_rights_status = asset.get("rights_status") or "legacy_unreviewed"
|
||||
|
||||
cur.execute(
|
||||
"""UPDATE media_assets
|
||||
SET legal_hold_active = TRUE,
|
||||
legal_hold_reason_code = %s,
|
||||
legal_hold_reason_note = %s,
|
||||
legal_hold_set_by_profile_id = %s,
|
||||
legal_hold_set_at = NOW(),
|
||||
legal_hold_released_by_profile_id = NULL,
|
||||
legal_hold_released_at = NULL,
|
||||
legal_hold_release_note = NULL,
|
||||
rights_status = 'blocked',
|
||||
updated_at = NOW()
|
||||
WHERE id = %s
|
||||
RETURNING id, legal_hold_active, legal_hold_reason_code, legal_hold_reason_note,
|
||||
legal_hold_set_by_profile_id, legal_hold_set_at, rights_status,
|
||||
lifecycle_state, visibility""",
|
||||
(reason_code, reason_note.strip()[:2000], acting_profile_id, asset_id),
|
||||
)
|
||||
updated_row = cur.fetchone()
|
||||
updated = r2d(updated_row)
|
||||
|
||||
write_audit_log_entry(
|
||||
cur,
|
||||
asset_id=asset_id,
|
||||
acting_profile_id=acting_profile_id,
|
||||
event_type="legal_hold_set",
|
||||
old_values={"rights_status": old_rights_status, "legal_hold_active": False},
|
||||
new_values={
|
||||
"rights_status": "blocked",
|
||||
"legal_hold_active": True,
|
||||
"reason_code": reason_code,
|
||||
"reason_note": reason_note.strip()[:2000] if reason_note else None,
|
||||
},
|
||||
)
|
||||
|
||||
conn.commit()
|
||||
return updated
|
||||
|
||||
|
||||
def release_legal_hold(
|
||||
cur: Any,
|
||||
conn: Any,
|
||||
asset_id: int,
|
||||
acting_profile_id: int,
|
||||
release_note: Optional[str],
|
||||
) -> dict:
|
||||
"""Hebt Legal Hold auf. Nur Superadmin.
|
||||
|
||||
Wirkung:
|
||||
- legal_hold_active = FALSE
|
||||
- legal_hold_released_by_profile_id, legal_hold_released_at, legal_hold_release_note
|
||||
- rights_status: zurueck auf 'declared' wenn vorher declared war, sonst 'legacy_unreviewed'
|
||||
(Entscheidungslogik: war vor dem Hold ein Deklarations-Eintrag vorhanden?)
|
||||
- Audit-Log-Eintrag 'legal_hold_released'
|
||||
|
||||
Gibt das aktualisierte Asset-Dict zurueck.
|
||||
"""
|
||||
if not release_note or not release_note.strip():
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="release_note (Freigabe-Begruendung) ist Pflicht.",
|
||||
)
|
||||
|
||||
cur.execute(
|
||||
"""SELECT id, legal_hold_active, rights_status, lifecycle_state, visibility,
|
||||
legal_hold_reason_code, legal_hold_reason_note
|
||||
FROM media_assets WHERE id = %s""",
|
||||
(asset_id,),
|
||||
)
|
||||
from db import r2d
|
||||
row = cur.fetchone()
|
||||
if not row:
|
||||
raise HTTPException(status_code=404, detail="Medium nicht gefunden")
|
||||
asset = r2d(row)
|
||||
|
||||
if not bool(asset.get("legal_hold_active")):
|
||||
raise HTTPException(
|
||||
status_code=409,
|
||||
detail="Dieses Medium befindet sich nicht unter Legal Hold.",
|
||||
)
|
||||
|
||||
# Bestimme den rights_status nach Freigabe:
|
||||
# Gibt es eine gueltige Deklaration? → 'declared', sonst 'legacy_unreviewed'
|
||||
cur.execute(
|
||||
"""SELECT COUNT(*) AS cnt FROM media_asset_rights_declarations
|
||||
WHERE media_asset_id = %s
|
||||
AND action_type NOT IN ('correction')
|
||||
AND rights_holder_confirmed = TRUE""",
|
||||
(asset_id,),
|
||||
)
|
||||
decl_row = cur.fetchone()
|
||||
decl_count = int(decl_row[0] if not hasattr(decl_row, "keys") else decl_row["cnt"])
|
||||
restored_rights_status = "declared" if decl_count > 0 else "legacy_unreviewed"
|
||||
|
||||
cur.execute(
|
||||
"""UPDATE media_assets
|
||||
SET legal_hold_active = FALSE,
|
||||
legal_hold_released_by_profile_id = %s,
|
||||
legal_hold_released_at = NOW(),
|
||||
legal_hold_release_note = %s,
|
||||
rights_status = %s,
|
||||
updated_at = NOW()
|
||||
WHERE id = %s
|
||||
RETURNING id, legal_hold_active, rights_status, lifecycle_state, visibility,
|
||||
legal_hold_reason_code, legal_hold_reason_note,
|
||||
legal_hold_set_by_profile_id, legal_hold_set_at,
|
||||
legal_hold_released_by_profile_id, legal_hold_released_at,
|
||||
legal_hold_release_note""",
|
||||
(acting_profile_id, release_note.strip()[:2000], restored_rights_status, asset_id),
|
||||
)
|
||||
updated_row = cur.fetchone()
|
||||
updated = r2d(updated_row)
|
||||
|
||||
write_audit_log_entry(
|
||||
cur,
|
||||
asset_id=asset_id,
|
||||
acting_profile_id=acting_profile_id,
|
||||
event_type="legal_hold_released",
|
||||
old_values={
|
||||
"rights_status": "blocked",
|
||||
"legal_hold_active": True,
|
||||
"reason_code": asset.get("legal_hold_reason_code"),
|
||||
},
|
||||
new_values={
|
||||
"rights_status": restored_rights_status,
|
||||
"legal_hold_active": False,
|
||||
"release_note": release_note.strip()[:2000] if release_note else None,
|
||||
},
|
||||
)
|
||||
|
||||
conn.commit()
|
||||
return updated
|
||||
|
|
@ -21,7 +21,8 @@ LC_TRASH_SOFT = "trash_soft"
|
|||
LC_TRASH_HIDDEN = "trash_hidden"
|
||||
|
||||
SOFT_TO_HIDDEN_DAYS = max(1, int(os.getenv("MEDIA_TRASH_SOFT_TO_HIDDEN_DAYS", "30")))
|
||||
HIDDEN_TO_PURGE_DAYS = max(1, int(os.getenv("MEDIA_TRASH_HIDDEN_TO_PURGE_DAYS", "90")))
|
||||
# P-03b: Default gemaess fachlichem Loeschkonzept (Audit 2026-05-09): 30+30 Tage.
|
||||
HIDDEN_TO_PURGE_DAYS = max(1, int(os.getenv("MEDIA_TRASH_HIDDEN_TO_PURGE_DAYS", "30")))
|
||||
|
||||
|
||||
def assert_can_manage_media_asset_lifecycle(cur: Any, tenant: Any, asset: dict) -> None:
|
||||
|
|
@ -278,6 +279,10 @@ def run_retention_pass(cur: Any, conn: Any) -> dict:
|
|||
"""
|
||||
Automatik: trash_soft älter als SOFT_TO_HIDDEN_DAYS → trash_hidden;
|
||||
trash_hidden mit purge_after_at in der Vergangenheit → purge.
|
||||
|
||||
P-11: Medien unter aktivem Legal Hold werden NICHT gerpurged (legal_hold_active = TRUE).
|
||||
Die Retention verschiebt sie auch nicht automatisch von trash_soft nach trash_hidden —
|
||||
Legal-Hold-Status hat Vorrang vor dem Papierkorb-Lifecycle.
|
||||
"""
|
||||
cutoff_soft = datetime.now(timezone.utc) - timedelta(days=SOFT_TO_HIDDEN_DAYS)
|
||||
cur.execute(
|
||||
|
|
@ -285,6 +290,7 @@ def run_retention_pass(cur: Any, conn: Any) -> dict:
|
|||
SET lifecycle_state = %s, trash_hidden_at = NOW(), updated_at = NOW(),
|
||||
purge_after_at = NOW() + (%s * INTERVAL '1 day')
|
||||
WHERE lifecycle_state = %s AND trash_soft_at IS NOT NULL AND trash_soft_at <= %s
|
||||
AND (legal_hold_active = FALSE OR legal_hold_active IS NULL)
|
||||
RETURNING id""",
|
||||
(LC_TRASH_HIDDEN, HIDDEN_TO_PURGE_DAYS, LC_TRASH_SOFT, cutoff_soft),
|
||||
)
|
||||
|
|
@ -293,7 +299,8 @@ def run_retention_pass(cur: Any, conn: Any) -> dict:
|
|||
|
||||
cur.execute(
|
||||
"""SELECT id FROM media_assets
|
||||
WHERE lifecycle_state = %s AND purge_after_at IS NOT NULL AND purge_after_at <= NOW()""",
|
||||
WHERE lifecycle_state = %s AND purge_after_at IS NOT NULL AND purge_after_at <= NOW()
|
||||
AND (legal_hold_active = FALSE OR legal_hold_active IS NULL)""",
|
||||
(LC_TRASH_HIDDEN,),
|
||||
)
|
||||
purge_ids = [r2d(r)["id"] for r in cur.fetchall()]
|
||||
|
|
|
|||
412
backend/media_rights.py
Normal file
412
backend/media_rights.py
Normal file
|
|
@ -0,0 +1,412 @@
|
|||
"""P-06: Zentrale Rechte-Policy fuer Medien-Uploads und Promotionen.
|
||||
|
||||
Konservative Erstannahmen (p06-v1-conservative):
|
||||
- Alle Uploads (inkl. private) erfordern vollstaendige Erklaerung
|
||||
- Personenfragen bei allen Sichtbarkeiten Pflicht zu beantworten
|
||||
- Promotion zu hoeherem Niveau erfordert neue Erklaerung
|
||||
- Altmedien ('legacy_unreviewed') duerfen nicht promoted werden
|
||||
|
||||
VORLAEUTIG: Juristische Validierung der Felder und Texte steht aus.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json as _json
|
||||
from typing import Any, Optional
|
||||
|
||||
from fastapi import HTTPException
|
||||
|
||||
DECLARATION_VERSION = "p06-v1-conservative"
|
||||
|
||||
# Sichtbarkeits-Hierarchie: private(1) < club(2) < official(3)
|
||||
VISIBILITY_LEVELS: dict[str, int] = {
|
||||
"private": 1,
|
||||
"club": 2,
|
||||
"official": 3,
|
||||
}
|
||||
|
||||
|
||||
def visibility_level(vis: str) -> int:
|
||||
return VISIBILITY_LEVELS.get((vis or "").strip().lower(), 0)
|
||||
|
||||
|
||||
def rights_covers_target(declared_for: Optional[str], target_vis: str) -> bool:
|
||||
"""True wenn die vorhandene Erklaerung die Ziel-Sichtbarkeit abdeckt."""
|
||||
if not declared_for:
|
||||
return False
|
||||
return visibility_level(declared_for) >= visibility_level(target_vis)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Validierung einer eingehenden Erklaerung
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
def validate_rights_declaration(decl: dict[str, Any], target_visibility: str) -> None:
|
||||
"""Pruefen ob alle Pflichtfelder der konservativen Erstannahme vorliegen.
|
||||
|
||||
Wirft HTTPException 400 mit maschinenlesbarem code bei Verstoss.
|
||||
Gilt fuer alle Sichtbarkeiten (private/club/official) identisch.
|
||||
"""
|
||||
# 1. rights_holder_confirmed ist immer Pflicht
|
||||
if not decl.get("rights_holder_confirmed"):
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail={
|
||||
"code": "RIGHTS_DECLARATION_REQUIRED",
|
||||
"message": (
|
||||
"Bitte bestaetigen, dass du die erforderlichen Rechte an diesem Medium besitzt."
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
# 2. contains_identifiable_persons muss explizit beantwortet sein
|
||||
if decl.get("contains_identifiable_persons") is None:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail={
|
||||
"code": "RIGHTS_DECLARATION_REQUIRED",
|
||||
"message": "Bitte angeben, ob erkennbare Personen abgebildet sind.",
|
||||
},
|
||||
)
|
||||
|
||||
# 3. Wenn Personen vorhanden: Einwilligung Pflicht
|
||||
if decl.get("contains_identifiable_persons") is True:
|
||||
if not decl.get("person_consent_confirmed"):
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail={
|
||||
"code": "PERSON_CONSENT_REQUIRED",
|
||||
"message": (
|
||||
"Bitte bestaetigen, dass die Einwilligungen aller erkennbaren Personen vorliegen."
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
# 4. contains_minors muss explizit beantwortet sein
|
||||
if decl.get("contains_minors") is None:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail={
|
||||
"code": "RIGHTS_DECLARATION_REQUIRED",
|
||||
"message": "Bitte angeben, ob Minderjaehrige abgebildet sind.",
|
||||
},
|
||||
)
|
||||
|
||||
# 5. Wenn Minderjaehrige: Elterneinwilligung Pflicht
|
||||
if decl.get("contains_minors") is True:
|
||||
if not decl.get("parental_consent_confirmed"):
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail={
|
||||
"code": "PARENTAL_CONSENT_REQUIRED",
|
||||
"message": (
|
||||
"Bitte bestaetigen, dass die Einwilligungen der Sorgeberechtigten vorliegen."
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
# 6. contains_music muss explizit beantwortet sein
|
||||
if decl.get("contains_music") is None:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail={
|
||||
"code": "RIGHTS_DECLARATION_REQUIRED",
|
||||
"message": "Bitte angeben, ob das Medium Musik enthaelt.",
|
||||
},
|
||||
)
|
||||
|
||||
# 7. Wenn Musik: Musikrechte Pflicht
|
||||
if decl.get("contains_music") is True:
|
||||
if not decl.get("music_rights_confirmed"):
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail={
|
||||
"code": "MUSIC_RIGHTS_REQUIRED",
|
||||
"message": (
|
||||
"Bitte bestaetigen, dass die erforderlichen Musikrechte vorliegen."
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
# 8. contains_third_party_content muss explizit beantwortet sein
|
||||
if decl.get("contains_third_party_content") is None:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail={
|
||||
"code": "RIGHTS_DECLARATION_REQUIRED",
|
||||
"message": "Bitte angeben, ob fremde geschuetzte Inhalte (Logos, Grafiken etc.) enthalten sind.",
|
||||
},
|
||||
)
|
||||
|
||||
# 9. Wenn Fremdmaterial: Rechte Pflicht
|
||||
if decl.get("contains_third_party_content") is True:
|
||||
if not decl.get("third_party_rights_confirmed"):
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail={
|
||||
"code": "THIRD_PARTY_RIGHTS_REQUIRED",
|
||||
"message": (
|
||||
"Bitte bestaetigen, dass die Rechte an allen enthaltenen Fremdmaterialien vorliegen."
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Pruefen ob vorhandene Erklaerung Zielsichtbarkeit abdeckt
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
def check_rights_coverage(cur: Any, asset_id: int, target_visibility: str) -> str:
|
||||
"""Status der Rechteabdeckung fuer ein Asset und eine Zielsichtbarkeit.
|
||||
|
||||
Returns:
|
||||
'ok' - vorhandene Erklaerung reicht aus
|
||||
'legacy' - Altmedium ohne Erklaerung (legacy_unreviewed)
|
||||
'blocked' - durch Admin gesperrt
|
||||
'no_declaration' - neues Medium ohne Erklaerung (sollte nicht vorkommen)
|
||||
|
||||
Hinweis: Eine P-06-Erklaerung beschreibt den Inhalt (Rechteinhaber, Personen, Musik etc.)
|
||||
und ist sichtbarkeitsunabhaengig. rights_status='declared' gilt daher fuer alle
|
||||
Sichtbarkeits-Stufen ohne Levelvergleich.
|
||||
"""
|
||||
cur.execute(
|
||||
"SELECT rights_status FROM media_assets WHERE id = %s",
|
||||
(asset_id,),
|
||||
)
|
||||
row = cur.fetchone()
|
||||
if not row:
|
||||
return "no_declaration"
|
||||
|
||||
rs = (row[0] if not hasattr(row, "keys") else row["rights_status"] or "").strip().lower()
|
||||
|
||||
if rs == "blocked":
|
||||
return "blocked"
|
||||
if rs == "legacy_unreviewed":
|
||||
return "legacy"
|
||||
if rs == "declared":
|
||||
return "ok"
|
||||
return "no_declaration"
|
||||
|
||||
|
||||
def assert_rights_for_promotion(cur: Any, asset_id: int, target_visibility: str) -> None:
|
||||
"""Wirft HTTPException wenn das Asset keine gueltige Erklaerung fuer target_visibility hat."""
|
||||
status = check_rights_coverage(cur, asset_id, target_visibility)
|
||||
if status == "ok":
|
||||
return
|
||||
if status == "legacy":
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail={
|
||||
"code": "LEGACY_REDECLARATION_REQUIRED",
|
||||
"message": (
|
||||
"Dieses Medium wurde vor Einfuehrung der Einwilligungspflicht hochgeladen. "
|
||||
"Bitte eine Rechterklaerung nachreichen, bevor die Sichtbarkeit erhoeht wird."
|
||||
),
|
||||
"asset_id": asset_id,
|
||||
},
|
||||
)
|
||||
if status == "blocked":
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail={
|
||||
"code": "RIGHTS_BLOCKED",
|
||||
"message": "Dieses Medium ist durch einen Administrator gesperrt.",
|
||||
"asset_id": asset_id,
|
||||
},
|
||||
)
|
||||
# no_declaration (neues Medium ohne Erklaerung)
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail={
|
||||
"code": "RIGHTS_DECLARATION_REQUIRED",
|
||||
"message": "Fuer dieses Medium liegt keine Rechterklaerung vor.",
|
||||
"asset_id": asset_id,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def assert_rights_for_exercise_link(cur: Any, asset_id: int, exercise_visibility: str) -> None:
|
||||
"""Pruefen ob das Asset in eine Uebung mit dieser Sichtbarkeit eingebunden werden darf."""
|
||||
status = check_rights_coverage(cur, asset_id, exercise_visibility)
|
||||
if status == "ok":
|
||||
return
|
||||
if status == "legacy" and exercise_visibility == "private":
|
||||
# Altmedien duerfen in private Uebungen eingebunden bleiben (kein Upgrade-Risiko)
|
||||
return
|
||||
if status == "legacy":
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail={
|
||||
"code": "LEGACY_REDECLARATION_REQUIRED",
|
||||
"message": (
|
||||
"Das gewahlte Archiv-Medium hat noch keine Rechterklaerung nach neuem Standard. "
|
||||
"Bitte zuerst eine Erklaerung fuer dieses Medium abgeben."
|
||||
),
|
||||
"asset_id": asset_id,
|
||||
},
|
||||
)
|
||||
if status == "insufficient":
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail={
|
||||
"code": "RIGHTS_SCOPE_INSUFFICIENT",
|
||||
"message": (
|
||||
f"Das Archiv-Medium hat keine Erklaerung fuer Sichtbarkeit '{exercise_visibility}'. "
|
||||
"Bitte zuerst eine neue Erklaerung fuer dieses Medium abgeben."
|
||||
),
|
||||
"asset_id": asset_id,
|
||||
},
|
||||
)
|
||||
if status == "blocked":
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail={
|
||||
"code": "RIGHTS_BLOCKED",
|
||||
"message": "Dieses Medium ist gesperrt und kann nicht verwendet werden.",
|
||||
"asset_id": asset_id,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Declaration-Log schreiben + Schnellfelder aktualisieren
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
def _clean_context(val: Any) -> Optional[str]:
|
||||
"""Leere Strings → None, sonst auf 2000 Zeichen kuerzen."""
|
||||
s = (val or "").strip()
|
||||
return s[:2000] if s else None
|
||||
|
||||
|
||||
def write_rights_declaration(
|
||||
cur: Any,
|
||||
asset_id: int,
|
||||
profile_id: int,
|
||||
action_type: str,
|
||||
target_visibility: str,
|
||||
decl: dict[str, Any],
|
||||
) -> int:
|
||||
"""Schreibt einen neuen Eintrag in media_asset_rights_declarations (append-only).
|
||||
|
||||
Returns: id des neuen Eintrags
|
||||
"""
|
||||
cur.execute(
|
||||
"""INSERT INTO media_asset_rights_declarations (
|
||||
media_asset_id, declared_by_profile_id, action_type, target_visibility,
|
||||
declaration_version,
|
||||
rights_holder_confirmed,
|
||||
contains_identifiable_persons, person_consent_confirmed, person_consent_context,
|
||||
contains_minors, parental_consent_confirmed, parental_consent_context,
|
||||
contains_music, music_rights_confirmed, music_rights_context,
|
||||
contains_third_party_content, third_party_rights_confirmed, third_party_rights_context
|
||||
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||
RETURNING id""",
|
||||
(
|
||||
asset_id,
|
||||
profile_id,
|
||||
action_type,
|
||||
target_visibility,
|
||||
DECLARATION_VERSION,
|
||||
bool(decl.get("rights_holder_confirmed")),
|
||||
decl.get("contains_identifiable_persons"),
|
||||
decl.get("person_consent_confirmed"),
|
||||
_clean_context(decl.get("person_consent_context")),
|
||||
decl.get("contains_minors"),
|
||||
decl.get("parental_consent_confirmed"),
|
||||
_clean_context(decl.get("parental_consent_context")),
|
||||
decl.get("contains_music"),
|
||||
decl.get("music_rights_confirmed"),
|
||||
_clean_context(decl.get("music_rights_context")),
|
||||
decl.get("contains_third_party_content"),
|
||||
decl.get("third_party_rights_confirmed"),
|
||||
_clean_context(decl.get("third_party_rights_context")),
|
||||
),
|
||||
)
|
||||
row = cur.fetchone()
|
||||
if hasattr(row, "keys"):
|
||||
return int(row["id"])
|
||||
return int(row[0])
|
||||
|
||||
|
||||
def write_audit_log_entry(
|
||||
cur: Any,
|
||||
asset_id: int,
|
||||
acting_profile_id: Optional[int],
|
||||
event_type: str,
|
||||
old_values: dict,
|
||||
new_values: dict,
|
||||
) -> None:
|
||||
"""Schreibt einen Eintrag in media_asset_audit_log (append-only)."""
|
||||
cur.execute(
|
||||
"""INSERT INTO media_asset_audit_log
|
||||
(media_asset_id, acting_profile_id, event_type, old_values, new_values)
|
||||
VALUES (%s, %s, %s, %s, %s)""",
|
||||
(
|
||||
asset_id,
|
||||
acting_profile_id,
|
||||
event_type,
|
||||
_json.dumps(old_values, default=str),
|
||||
_json.dumps(new_values, default=str),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def write_rights_correction_declaration(
|
||||
cur: Any,
|
||||
asset_id: int,
|
||||
profile_id: int,
|
||||
target_visibility: str,
|
||||
decl: dict[str, Any],
|
||||
correction_note: Optional[str],
|
||||
) -> int:
|
||||
"""Schreibt eine Korrektur-Deklaration (action_type='correction', append-only)."""
|
||||
cur.execute(
|
||||
"""INSERT INTO media_asset_rights_declarations (
|
||||
media_asset_id, declared_by_profile_id, action_type, target_visibility,
|
||||
declaration_version,
|
||||
rights_holder_confirmed,
|
||||
contains_identifiable_persons, person_consent_confirmed, person_consent_context,
|
||||
contains_minors, parental_consent_confirmed, parental_consent_context,
|
||||
contains_music, music_rights_confirmed, music_rights_context,
|
||||
contains_third_party_content, third_party_rights_confirmed, third_party_rights_context,
|
||||
correction_note
|
||||
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||
RETURNING id""",
|
||||
(
|
||||
asset_id,
|
||||
profile_id,
|
||||
"correction",
|
||||
target_visibility,
|
||||
DECLARATION_VERSION,
|
||||
bool(decl.get("rights_holder_confirmed")),
|
||||
decl.get("contains_identifiable_persons"),
|
||||
decl.get("person_consent_confirmed"),
|
||||
_clean_context(decl.get("person_consent_context")),
|
||||
decl.get("contains_minors"),
|
||||
decl.get("parental_consent_confirmed"),
|
||||
_clean_context(decl.get("parental_consent_context")),
|
||||
decl.get("contains_music"),
|
||||
decl.get("music_rights_confirmed"),
|
||||
_clean_context(decl.get("music_rights_context")),
|
||||
decl.get("contains_third_party_content"),
|
||||
decl.get("third_party_rights_confirmed"),
|
||||
_clean_context(decl.get("third_party_rights_context")),
|
||||
_clean_context(correction_note),
|
||||
),
|
||||
)
|
||||
row = cur.fetchone()
|
||||
if hasattr(row, "keys"):
|
||||
return int(row["id"])
|
||||
return int(row[0])
|
||||
|
||||
|
||||
def update_rights_quick_fields(cur: Any, asset_id: int, target_visibility: str) -> None:
|
||||
"""Setzt die Schnellfelder in media_assets nach erfolgreicher Deklaration."""
|
||||
cur.execute(
|
||||
"""UPDATE media_assets
|
||||
SET rights_status = 'declared',
|
||||
rights_declared_for_visibility = %s,
|
||||
rights_declared_at = NOW(),
|
||||
updated_at = NOW()
|
||||
WHERE id = %s""",
|
||||
(target_visibility, asset_id),
|
||||
)
|
||||
37
backend/migrations/047_legal_documents.sql
Normal file
37
backend/migrations/047_legal_documents.sql
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
-- Migration 047: Admin-konfigurierbare Rechtstexte
|
||||
-- Tabellen: legal_documents (versioniert), legal_document_audit (Änderungslog)
|
||||
-- document_type: impressum | privacy_policy | terms_of_use | media_policy
|
||||
-- status: draft | published | archived
|
||||
-- Partial unique index: nur genau ein published-Dokument pro document_type erlaubt.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS legal_documents (
|
||||
id SERIAL PRIMARY KEY,
|
||||
document_type VARCHAR(50) NOT NULL
|
||||
CHECK (document_type IN ('impressum', 'privacy_policy', 'terms_of_use', 'media_policy')),
|
||||
version INT NOT NULL DEFAULT 1,
|
||||
title VARCHAR(255) NOT NULL,
|
||||
content_sections JSONB NOT NULL DEFAULT '[]',
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'draft'
|
||||
CHECK (status IN ('draft', 'published', 'archived')),
|
||||
change_note TEXT,
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
updated_at TIMESTAMP DEFAULT NOW(),
|
||||
created_by_profile_id INT REFERENCES profiles(id) ON DELETE SET NULL,
|
||||
published_by_profile_id INT REFERENCES profiles(id) ON DELETE SET NULL,
|
||||
published_at TIMESTAMP
|
||||
);
|
||||
|
||||
-- Sicherstellt: pro document_type maximal ein published-Datensatz
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS legal_documents_unique_published
|
||||
ON legal_documents (document_type)
|
||||
WHERE status = 'published';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS legal_document_audit (
|
||||
id SERIAL PRIMARY KEY,
|
||||
legal_document_id INT NOT NULL REFERENCES legal_documents(id) ON DELETE CASCADE,
|
||||
action VARCHAR(50) NOT NULL,
|
||||
changed_by_profile_id INT REFERENCES profiles(id) ON DELETE SET NULL,
|
||||
change_note TEXT,
|
||||
previous_status VARCHAR(20),
|
||||
created_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
75
backend/migrations/048_media_rights_declarations.sql
Normal file
75
backend/migrations/048_media_rights_declarations.sql
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
-- Migration 048: P-06 Upload-Einwilligungsdialog
|
||||
-- Append-only Deklarations-Log + Schnellfelder in media_assets
|
||||
-- Alle bestehenden Medien erhalten rights_status = 'legacy_unreviewed'
|
||||
|
||||
-- Deklarations-Log (append-only, wird nie geaendert oder geloescht)
|
||||
CREATE TABLE IF NOT EXISTS media_asset_rights_declarations (
|
||||
id SERIAL PRIMARY KEY,
|
||||
media_asset_id INT NOT NULL REFERENCES media_assets(id) ON DELETE CASCADE,
|
||||
declared_by_profile_id INT REFERENCES profiles(id) ON DELETE SET NULL,
|
||||
declared_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
|
||||
-- Kontext der Erklaerung
|
||||
action_type VARCHAR(50) NOT NULL
|
||||
CHECK (action_type IN (
|
||||
'upload', -- Erstupload
|
||||
'promote_club', -- Promotion zu club
|
||||
'promote_official', -- Promotion zu official
|
||||
're_declaration', -- Freiwillige Nacherklaerung
|
||||
'legacy_re_declaration' -- Altmedium: erste Erklaerung nachgereicht
|
||||
)),
|
||||
target_visibility VARCHAR(32) NOT NULL
|
||||
CHECK (target_visibility IN ('private', 'club', 'official')),
|
||||
-- Textversion der Erklaerung; 'p06-v1-conservative' = konservative Erstannahmen
|
||||
-- VORLAEUTIG: Texte noch nicht juristisch geprueft
|
||||
declaration_version VARCHAR(40) NOT NULL DEFAULT 'p06-v1-conservative',
|
||||
|
||||
-- Pflichtfeld (alle Sichtbarkeiten, alle Aktionen)
|
||||
rights_holder_confirmed BOOLEAN NOT NULL,
|
||||
|
||||
-- Personen (konservative Annahme: immer abgefragt, auch bei 'private')
|
||||
contains_identifiable_persons BOOLEAN,
|
||||
person_consent_confirmed BOOLEAN, -- Pflicht wenn contains_identifiable_persons = true
|
||||
|
||||
-- Minderjaehrige
|
||||
contains_minors BOOLEAN,
|
||||
parental_consent_confirmed BOOLEAN, -- Pflicht wenn contains_minors = true
|
||||
|
||||
-- Drittmaterial
|
||||
contains_music BOOLEAN,
|
||||
music_rights_confirmed BOOLEAN, -- Pflicht wenn contains_music = true
|
||||
contains_third_party_content BOOLEAN,
|
||||
third_party_rights_confirmed BOOLEAN, -- Pflicht wenn contains_third_party_content = true
|
||||
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_mard_asset
|
||||
ON media_asset_rights_declarations (media_asset_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_mard_profile
|
||||
ON media_asset_rights_declarations (declared_by_profile_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_mard_action_type
|
||||
ON media_asset_rights_declarations (action_type);
|
||||
|
||||
-- Schnellfelder in media_assets (kein Ersatz fuer den Log, nur fuer effiziente Abfragen)
|
||||
ALTER TABLE media_assets
|
||||
ADD COLUMN IF NOT EXISTS rights_status VARCHAR(32)
|
||||
NOT NULL DEFAULT 'legacy_unreviewed'
|
||||
CHECK (rights_status IN ('legacy_unreviewed', 'declared', 'blocked')),
|
||||
ADD COLUMN IF NOT EXISTS rights_declared_for_visibility VARCHAR(32)
|
||||
CHECK (rights_declared_for_visibility IN ('private', 'club', 'official')),
|
||||
ADD COLUMN IF NOT EXISTS rights_declared_at TIMESTAMPTZ;
|
||||
|
||||
-- Bestehende Medien: explicit legacy_unreviewed setzen (redundant zum DEFAULT, zur Klarheit)
|
||||
UPDATE media_assets
|
||||
SET rights_status = 'legacy_unreviewed'
|
||||
WHERE rights_status = 'legacy_unreviewed'; -- no-op, setzt Default explizit
|
||||
|
||||
COMMENT ON TABLE media_asset_rights_declarations IS
|
||||
'P-06: Append-only Erklaerungslog fuer Upload-Einwilligungen. '
|
||||
'Eintraege werden nie geaendert. Juristische Validierung der Felder und Texte steht aus.';
|
||||
|
||||
COMMENT ON COLUMN media_assets.rights_status IS
|
||||
'P-06: legacy_unreviewed = Altbestand ohne P-06-Erklaerung; '
|
||||
'declared = gueltige Erklaerung fuer rights_declared_for_visibility; '
|
||||
'blocked = durch Admin gesperrt (P-11-Schnittstelle).';
|
||||
18
backend/migrations/049_media_rights_consent_context.sql
Normal file
18
backend/migrations/049_media_rights_consent_context.sql
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
-- Migration 049: P-06 Erweiterung – Einwilligungskontext-Felder und Copyright im Upload-Dialog
|
||||
-- Optionale Freitextfelder fuer den Kontext der Einwilligung (z.B. "Schriftliche Einwilligung
|
||||
-- vom 2026-05-01 liegt vor") sowie copyright_notice direkt beim Upload erfassbar.
|
||||
|
||||
ALTER TABLE media_asset_rights_declarations
|
||||
ADD COLUMN IF NOT EXISTS person_consent_context TEXT,
|
||||
ADD COLUMN IF NOT EXISTS parental_consent_context TEXT,
|
||||
ADD COLUMN IF NOT EXISTS music_rights_context TEXT,
|
||||
ADD COLUMN IF NOT EXISTS third_party_rights_context TEXT;
|
||||
|
||||
COMMENT ON COLUMN media_asset_rights_declarations.person_consent_context IS
|
||||
'Optionaler Freitext: In welchem Zusammenhang liegt die Einwilligung der abgebildeten Personen vor?';
|
||||
COMMENT ON COLUMN media_asset_rights_declarations.parental_consent_context IS
|
||||
'Optionaler Freitext: In welchem Zusammenhang liegt die Einwilligung der Sorgeberechtigten vor?';
|
||||
COMMENT ON COLUMN media_asset_rights_declarations.music_rights_context IS
|
||||
'Optionaler Freitext: Welche Lizenz / GEMA-Regelung liegt fuer die Musik vor?';
|
||||
COMMENT ON COLUMN media_asset_rights_declarations.third_party_rights_context IS
|
||||
'Optionaler Freitext: Auf welcher Grundlage duerfen die enthaltenen Fremdinhalte verwendet werden?';
|
||||
47
backend/migrations/050_media_audit_log.sql
Normal file
47
backend/migrations/050_media_audit_log.sql
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
-- Migration 050: Medien-Volljournal – Audit-Log für alle Änderungen + Korrektur-Deklarationen
|
||||
|
||||
-- Vollständiger Audit-Log: Sichtbarkeitsänderungen, Copyright, Metadaten, Lifecycle
|
||||
CREATE TABLE IF NOT EXISTS media_asset_audit_log (
|
||||
id SERIAL PRIMARY KEY,
|
||||
media_asset_id INT NOT NULL REFERENCES media_assets(id) ON DELETE CASCADE,
|
||||
acting_profile_id INT REFERENCES profiles(id) ON DELETE SET NULL,
|
||||
occurred_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
event_type VARCHAR(50) NOT NULL
|
||||
CHECK (event_type IN (
|
||||
'visibility_change',
|
||||
'copyright_change',
|
||||
'metadata_change',
|
||||
'lifecycle_change'
|
||||
)),
|
||||
old_values JSONB,
|
||||
new_values JSONB
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_maal_asset ON media_asset_audit_log (media_asset_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_maal_asset_occurred ON media_asset_audit_log (media_asset_id, occurred_at);
|
||||
|
||||
COMMENT ON TABLE media_asset_audit_log IS
|
||||
'Append-only Protokoll aller Aenderungen an Medien-Assets (Sichtbarkeit, Copyright, Metadaten, Lifecycle). '
|
||||
'Wird nie aktualisiert oder geloescht (ausser ON DELETE CASCADE des Assets).';
|
||||
|
||||
-- Korrektur-Notiz für nachtraegliche Deklarations-Korrekturen
|
||||
ALTER TABLE media_asset_rights_declarations
|
||||
ADD COLUMN IF NOT EXISTS correction_note TEXT;
|
||||
|
||||
COMMENT ON COLUMN media_asset_rights_declarations.correction_note IS
|
||||
'Optionale Begruendung fuer action_type=correction: Warum wurde die Erklaerung korrigiert?';
|
||||
|
||||
-- ''correction'' action_type hinzufuegen (bestehende CHECK-Constraint ersetzen)
|
||||
ALTER TABLE media_asset_rights_declarations
|
||||
DROP CONSTRAINT IF EXISTS media_asset_rights_declarations_action_type_check;
|
||||
|
||||
ALTER TABLE media_asset_rights_declarations
|
||||
ADD CONSTRAINT media_asset_rights_declarations_action_type_check
|
||||
CHECK (action_type IN (
|
||||
'upload',
|
||||
'promote_club',
|
||||
'promote_official',
|
||||
're_declaration',
|
||||
'legacy_re_declaration',
|
||||
'correction'
|
||||
));
|
||||
73
backend/migrations/051_legal_hold.sql
Normal file
73
backend/migrations/051_legal_hold.sql
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
-- Migration 051: P-11 Legal-Hold Lifecycle-Status
|
||||
-- Sofortsperrung fuer rechtlich problematische Medien (Compliance-Paket P-11)
|
||||
--
|
||||
-- Architekturentscheidung:
|
||||
-- rights_status='blocked' bleibt als Spiegel-Schnellstatus (P-06-Kompatibilitaet).
|
||||
-- Primaere Wahrheit: legal_hold_active + dedizierte Metadaten-Felder in media_assets.
|
||||
-- Dies ermoeglicht klare Trennung: P-06 Deklarationsstatus / P-11 Legal Hold / P-03 Lifecycle.
|
||||
-- P-13 kann spaeter denselben set_legal_hold-Service nutzen.
|
||||
|
||||
ALTER TABLE media_assets
|
||||
ADD COLUMN IF NOT EXISTS legal_hold_active BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
ADD COLUMN IF NOT EXISTS legal_hold_reason_code VARCHAR(50)
|
||||
CHECK (legal_hold_reason_code IN (
|
||||
'rights_dispute',
|
||||
'consent_withdrawn',
|
||||
'privacy_complaint',
|
||||
'copyright_complaint',
|
||||
'youth_protection',
|
||||
'illegal_content',
|
||||
'other'
|
||||
)),
|
||||
ADD COLUMN IF NOT EXISTS legal_hold_reason_note TEXT,
|
||||
ADD COLUMN IF NOT EXISTS legal_hold_set_by_profile_id INT REFERENCES profiles(id) ON DELETE SET NULL,
|
||||
ADD COLUMN IF NOT EXISTS legal_hold_set_at TIMESTAMPTZ,
|
||||
ADD COLUMN IF NOT EXISTS legal_hold_released_by_profile_id INT REFERENCES profiles(id) ON DELETE SET NULL,
|
||||
ADD COLUMN IF NOT EXISTS legal_hold_released_at TIMESTAMPTZ,
|
||||
ADD COLUMN IF NOT EXISTS legal_hold_release_note TEXT;
|
||||
|
||||
COMMENT ON COLUMN media_assets.legal_hold_active IS
|
||||
'P-11: TRUE = Medium unter Legal Hold; sofortige Sperrung fuer alle normalen Nutzerpfade. '
|
||||
'Retention-Job darf dieses Medium nicht purgen. '
|
||||
'rights_status wird bei Aktivierung auf ''blocked'' gespiegelt.';
|
||||
|
||||
COMMENT ON COLUMN media_assets.legal_hold_reason_code IS
|
||||
'P-11: Kategorie des Legal Holds. Pflicht beim Setzen.';
|
||||
|
||||
COMMENT ON COLUMN media_assets.legal_hold_reason_note IS
|
||||
'P-11: Freitext-Begruendung fuer den Legal Hold.';
|
||||
|
||||
COMMENT ON COLUMN media_assets.legal_hold_set_by_profile_id IS
|
||||
'P-11: Profil das den Legal Hold gesetzt hat (Superadmin).';
|
||||
|
||||
COMMENT ON COLUMN media_assets.legal_hold_set_at IS
|
||||
'P-11: Zeitpunkt der Legal-Hold-Aktivierung.';
|
||||
|
||||
COMMENT ON COLUMN media_assets.legal_hold_released_by_profile_id IS
|
||||
'P-11: Profil das den Legal Hold aufgehoben hat.';
|
||||
|
||||
COMMENT ON COLUMN media_assets.legal_hold_released_at IS
|
||||
'P-11: Zeitpunkt der Legal-Hold-Freigabe.';
|
||||
|
||||
COMMENT ON COLUMN media_assets.legal_hold_release_note IS
|
||||
'P-11: Begruendung fuer die Aufhebung des Legal Holds.';
|
||||
|
||||
-- Index fuer Admin-Liste aktiver Legal Holds
|
||||
CREATE INDEX IF NOT EXISTS idx_media_assets_legal_hold_active
|
||||
ON media_assets (legal_hold_active)
|
||||
WHERE legal_hold_active = TRUE;
|
||||
|
||||
-- Neue event_types fuer media_asset_audit_log
|
||||
ALTER TABLE media_asset_audit_log
|
||||
DROP CONSTRAINT IF EXISTS media_asset_audit_log_event_type_check;
|
||||
|
||||
ALTER TABLE media_asset_audit_log
|
||||
ADD CONSTRAINT media_asset_audit_log_event_type_check
|
||||
CHECK (event_type IN (
|
||||
'visibility_change',
|
||||
'copyright_change',
|
||||
'metadata_change',
|
||||
'lifecycle_change',
|
||||
'legal_hold_set',
|
||||
'legal_hold_released'
|
||||
));
|
||||
73
backend/migrations/052_content_reports.sql
Normal file
73
backend/migrations/052_content_reports.sql
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
-- P-13: Content-Melde-Backend
|
||||
-- Meldungen rechtswidriger Inhalte (DSA-konformes Meldeverfahren, KRIT-03)
|
||||
--
|
||||
-- Architektur: Diese Tabelle traegt alle fachlichen Report-Daten.
|
||||
-- Die bestehende Admin-Inbox (InboxPage.jsx, GET /api/me/inbox/join-requests)
|
||||
-- wird um einen zweiten Abschnitt erweitert, der Content-Reports anzeigt.
|
||||
-- Keine separate Admin-Queue, keine generische inbox_items-Tabelle.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS content_reports (
|
||||
id SERIAL PRIMARY KEY,
|
||||
|
||||
-- Ziel der Meldung (erweiterbar auf weitere Typen)
|
||||
target_type VARCHAR(20) NOT NULL DEFAULT 'media_asset'
|
||||
CHECK (target_type IN ('media_asset', 'exercise')),
|
||||
target_id INTEGER NOT NULL,
|
||||
|
||||
-- Meldungsinhalt
|
||||
report_reason VARCHAR(50) NOT NULL
|
||||
CHECK (report_reason IN (
|
||||
'copyright',
|
||||
'image_rights',
|
||||
'privacy',
|
||||
'minors',
|
||||
'illegal_content',
|
||||
'youth_protection',
|
||||
'offensive_content',
|
||||
'other'
|
||||
)),
|
||||
report_description TEXT NOT NULL,
|
||||
|
||||
-- Meldende Person (Name + E-Mail Pflicht; Profil optional bei eingeloggten Nutzern)
|
||||
reporter_name VARCHAR(200) NOT NULL,
|
||||
reporter_email VARCHAR(200) NOT NULL,
|
||||
reporter_profile_id INTEGER REFERENCES profiles(id) ON DELETE SET NULL,
|
||||
|
||||
-- Gutglaubenserklärung (Pflicht)
|
||||
good_faith_confirmed BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
|
||||
-- Automatische Priorisierung (high fuer minors/youth_protection/illegal_content)
|
||||
priority VARCHAR(20) NOT NULL DEFAULT 'normal'
|
||||
CHECK (priority IN ('high', 'normal')),
|
||||
|
||||
-- Workflow-Status
|
||||
status VARCHAR(30) NOT NULL DEFAULT 'submitted'
|
||||
CHECK (status IN (
|
||||
'submitted',
|
||||
'under_review',
|
||||
'resolved_no_action',
|
||||
'resolved_legal_hold',
|
||||
'rejected_invalid'
|
||||
)),
|
||||
|
||||
-- Bearbeitung
|
||||
assigned_to_profile_id INTEGER REFERENCES profiles(id) ON DELETE SET NULL,
|
||||
reviewed_by_profile_id INTEGER REFERENCES profiles(id) ON DELETE SET NULL,
|
||||
reviewed_at TIMESTAMP,
|
||||
resolution_note TEXT,
|
||||
|
||||
-- Zeitstempel
|
||||
submitted_at TIMESTAMP DEFAULT NOW(),
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
updated_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Indices fuer Admin-Liste
|
||||
CREATE INDEX IF NOT EXISTS idx_content_reports_status_created
|
||||
ON content_reports (status, created_at DESC);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_content_reports_target
|
||||
ON content_reports (target_type, target_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_content_reports_priority
|
||||
ON content_reports (priority, status, created_at DESC);
|
||||
21
backend/migrations/053_content_report_audit_event.sql
Normal file
21
backend/migrations/053_content_report_audit_event.sql
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
-- Migration 053: Audit-Log event_type-Constraint korrigieren + 'content_report_filed' hinzufuegen
|
||||
--
|
||||
-- Hintergrund: Die Tabelle media_asset_audit_log wurde per CREATE TABLE IF NOT EXISTS angelegt.
|
||||
-- Da die Tabelle bereits existierte, wurde die CHECK-Constraint aus Migration 050 nie angewendet.
|
||||
-- In der DB existieren Zeilen mit legal_hold_set und legal_hold_released (aus P-11).
|
||||
-- Diese Migration setzt die Constraint erstmalig mit allen gueltigen Werten.
|
||||
|
||||
ALTER TABLE media_asset_audit_log
|
||||
DROP CONSTRAINT IF EXISTS media_asset_audit_log_event_type_check;
|
||||
|
||||
ALTER TABLE media_asset_audit_log
|
||||
ADD CONSTRAINT media_asset_audit_log_event_type_check
|
||||
CHECK (event_type IN (
|
||||
'visibility_change',
|
||||
'copyright_change',
|
||||
'metadata_change',
|
||||
'lifecycle_change',
|
||||
'legal_hold_set',
|
||||
'legal_hold_released',
|
||||
'content_report_filed'
|
||||
));
|
||||
60
backend/migrations/054_training_modules.sql
Normal file
60
backend/migrations/054_training_modules.sql
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
-- Migration 054: Trainingsmodule (Bibliothek / Planung) — Phase 1 MVP
|
||||
-- Fachgrundlage: functional/Shinkan Trainingsmodule Kombinationsuebungen Spezifikation V2.md
|
||||
|
||||
CREATE TABLE IF NOT EXISTS training_modules (
|
||||
id SERIAL PRIMARY KEY,
|
||||
club_id INT REFERENCES clubs(id) ON DELETE SET NULL,
|
||||
created_by INT REFERENCES profiles(id) ON DELETE SET NULL,
|
||||
title VARCHAR(200) NOT NULL,
|
||||
summary TEXT,
|
||||
goal TEXT,
|
||||
recommended_duration_min INT,
|
||||
target_group_notes TEXT,
|
||||
deployment_context_notes TEXT,
|
||||
primary_method_id INT REFERENCES training_methods(id) ON DELETE SET NULL,
|
||||
visibility VARCHAR(50) NOT NULL DEFAULT 'club'
|
||||
CHECK (visibility IN ('private', 'club', 'official')),
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
updated_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_training_modules_club ON training_modules(club_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_training_modules_creator ON training_modules(created_by);
|
||||
CREATE INDEX IF NOT EXISTS idx_training_modules_visibility ON training_modules(visibility);
|
||||
CREATE INDEX IF NOT EXISTS idx_training_modules_method ON training_modules(primary_method_id)
|
||||
WHERE primary_method_id IS NOT NULL;
|
||||
|
||||
DROP TRIGGER IF EXISTS training_modules_update ON training_modules;
|
||||
CREATE TRIGGER training_modules_update
|
||||
BEFORE UPDATE ON training_modules
|
||||
FOR EACH ROW EXECUTE FUNCTION update_timestamp();
|
||||
|
||||
CREATE TABLE IF NOT EXISTS training_module_items (
|
||||
id SERIAL PRIMARY KEY,
|
||||
module_id INT NOT NULL REFERENCES training_modules(id) ON DELETE CASCADE,
|
||||
order_index INT NOT NULL,
|
||||
item_type VARCHAR(20) NOT NULL CHECK (item_type IN ('exercise', 'note')),
|
||||
exercise_id INT REFERENCES exercises(id) ON DELETE SET NULL,
|
||||
exercise_variant_id INT REFERENCES exercise_variants(id) ON DELETE SET NULL,
|
||||
planned_duration_min INT,
|
||||
notes TEXT,
|
||||
note_body TEXT,
|
||||
UNIQUE (module_id, order_index),
|
||||
CHECK (
|
||||
(item_type = 'exercise' AND exercise_id IS NOT NULL AND note_body IS NULL)
|
||||
OR
|
||||
(item_type = 'note' AND exercise_id IS NULL)
|
||||
)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_training_module_items_module ON training_module_items(module_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_training_module_items_exercise ON training_module_items(exercise_id)
|
||||
WHERE exercise_id IS NOT NULL;
|
||||
|
||||
-- Herkunft bei Übernahme aus Modul-Bibliothek (Kopie, keine Live-Verknüpfung)
|
||||
ALTER TABLE training_unit_section_items
|
||||
ADD COLUMN IF NOT EXISTS source_training_module_id INT REFERENCES training_modules(id) ON DELETE SET NULL;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_training_unit_section_items_source_module
|
||||
ON training_unit_section_items(source_training_module_id)
|
||||
WHERE source_training_module_id IS NOT NULL;
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
-- Persönliche Planungs-UI-Präferenzen (JSONB, selbst vom Nutzer setzbar)
|
||||
ALTER TABLE profiles
|
||||
ADD COLUMN IF NOT EXISTS training_planning_prefs JSONB NOT NULL DEFAULT '{}'::jsonb;
|
||||
33
backend/migrations/056_combination_exercises.sql
Normal file
33
backend/migrations/056_combination_exercises.sql
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
-- Migration 056: Kombinationsübungen (Phase 2 MVP) — Slots + Pool-Kandidaten
|
||||
-- Fachgrundlage: functional/Shinkan Trainingsmodule Kombinationsuebungen Spezifikation V2.md §6
|
||||
|
||||
ALTER TABLE exercises
|
||||
ADD COLUMN IF NOT EXISTS exercise_kind VARCHAR(20) NOT NULL DEFAULT 'simple'
|
||||
CHECK (exercise_kind IN ('simple', 'combination')),
|
||||
ADD COLUMN IF NOT EXISTS method_archetype VARCHAR(80),
|
||||
ADD COLUMN IF NOT EXISTS method_profile JSONB NOT NULL DEFAULT '{}'::jsonb;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_exercises_exercise_kind ON exercises(exercise_kind);
|
||||
CREATE INDEX IF NOT EXISTS idx_exercises_method_archetype ON exercises(method_archetype)
|
||||
WHERE method_archetype IS NOT NULL;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS combination_exercise_slots (
|
||||
id SERIAL PRIMARY KEY,
|
||||
exercise_id INT NOT NULL REFERENCES exercises(id) ON DELETE CASCADE,
|
||||
slot_index INT NOT NULL,
|
||||
title VARCHAR(200),
|
||||
UNIQUE (exercise_id, slot_index)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_combination_exercise_slots_exercise ON combination_exercise_slots(exercise_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS combination_slot_candidates (
|
||||
id SERIAL PRIMARY KEY,
|
||||
slot_id INT NOT NULL REFERENCES combination_exercise_slots(id) ON DELETE CASCADE,
|
||||
candidate_exercise_id INT NOT NULL REFERENCES exercises(id) ON DELETE CASCADE,
|
||||
sort_order INT NOT NULL DEFAULT 0,
|
||||
UNIQUE (slot_id, candidate_exercise_id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_combination_slot_candidates_slot ON combination_slot_candidates(slot_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_combination_slot_candidates_exercise ON combination_slot_candidates(candidate_exercise_id);
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
-- 057: Terminspezifisches Ablaufprofil fuer Kombinationsuebungen in der Planung
|
||||
-- NULL = method_profile vom Katalog (exercises) verwenden; sonst dieser JSONB-Stand gilt fuer diese Platzierung.
|
||||
|
||||
ALTER TABLE training_unit_section_items
|
||||
ADD COLUMN IF NOT EXISTS planning_method_profile JSONB NULL;
|
||||
|
||||
COMMENT ON COLUMN training_unit_section_items.planning_method_profile IS
|
||||
'Snapshots des Ablaufprofils fuer diese Einheit/Zeile; NULL = exercises.method_profile.';
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
-- Unterstützung für GET /api/exercises: ORDER BY e.updated_at DESC
|
||||
-- und häufiger Pfad created_by_me (= e.created_by = Profil) mit derselben Sortierung.
|
||||
-- Hinweis: idx_exercises_created_at (014) betrifft created_at, nicht updated_at.
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_exercises_updated_at_desc ON exercises (updated_at DESC);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_exercises_created_by_updated_at_desc ON exercises (created_by, updated_at DESC);
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
-- GET /api/training-units: Liste nutzt immer tu.framework_slot_id IS NULL (keine Rahmen-Blueprints)
|
||||
-- und sortiert nach planned_date, planned_time_start (ASC/DESC mit NULLS LAST).
|
||||
-- Teilindex verkleinert die Menge und unterstützt die Sortierung.
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_training_units_scheduled_order
|
||||
ON training_units (planned_date DESC, planned_time_start DESC NULLS LAST)
|
||||
WHERE framework_slot_id IS NULL;
|
||||
33
backend/migrations/060_exercises_list_scale_indexes.sql
Normal file
33
backend/migrations/060_exercises_list_scale_indexes.sql
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
-- Migration 060: Übungslisten bei großem Bestand (Ziel: Tausende Übungen, viele Filterkombinationen).
|
||||
-- Ergänzt 058 (globale Sortierung / created_by): kleinere Partial-Indizes für häufige
|
||||
-- Sichtbarkeits-Pfade der Bibliothek sowie Junction-Indizes für die List-Subqueries
|
||||
-- (primary_focus_name / JSON-Aggregate mit is_primary).
|
||||
--
|
||||
-- Bereits vorhanden und sinnvoll: UNIQUE(exercise_id, …) auf den M:N-Tabellen für EXISTS-Joins;
|
||||
-- GIN auf exercises.search_vector (014); idx_exercises_exercise_kind (056).
|
||||
|
||||
-- Official: OR-Zweig der Bibliothek — kompakter als Full-Table-Scan bei BitmapOr mit anderen Partial-Indizes
|
||||
CREATE INDEX IF NOT EXISTS idx_exercises_list_official_updated
|
||||
ON exercises (updated_at DESC)
|
||||
WHERE visibility = 'official'
|
||||
AND COALESCE(status, '') <> 'archived';
|
||||
|
||||
-- Club: häufig club_id + Sortierung nach updated_at (Mandanten-Bibliothek)
|
||||
CREATE INDEX IF NOT EXISTS idx_exercises_list_club_updated
|
||||
ON exercises (club_id, updated_at DESC)
|
||||
WHERE visibility = 'club'
|
||||
AND club_id IS NOT NULL
|
||||
AND COALESCE(status, '') <> 'archived';
|
||||
|
||||
-- List-SELECT: Subqueries / json_agg sortieren zuerst nach is_primary (siehe exercises.py)
|
||||
CREATE INDEX IF NOT EXISTS idx_exercise_focus_areas_exercise_primary
|
||||
ON exercise_focus_areas (exercise_id, is_primary DESC NULLS LAST, focus_area_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_exercise_style_directions_exercise_primary
|
||||
ON exercise_style_directions (exercise_id, is_primary DESC NULLS LAST, style_direction_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_exercise_training_types_exercise_primary
|
||||
ON exercise_training_types (exercise_id, is_primary DESC NULLS LAST, training_type_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_exercise_target_groups_exercise_primary
|
||||
ON exercise_target_groups (exercise_id, is_primary DESC NULLS LAST, target_group_id);
|
||||
22
backend/migrations/061_training_units_keyset_indexes.sql
Normal file
22
backend/migrations/061_training_units_keyset_indexes.sql
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
-- GET /api/training-units: Keyset über (planned_date, planned_time_start NULLS LAST per Sort, id)
|
||||
-- Ersetzt den reinen Datum/Uhrzeit-Teilindex 059 durch zwei Richtungen mit Tie-Break id.
|
||||
|
||||
DROP INDEX IF EXISTS idx_training_units_scheduled_order;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_training_units_list_keyset_desc
|
||||
ON training_units (
|
||||
planned_date DESC,
|
||||
(planned_time_start IS NULL) ASC,
|
||||
planned_time_start DESC NULLS LAST,
|
||||
id DESC
|
||||
)
|
||||
WHERE framework_slot_id IS NULL;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_training_units_list_keyset_asc
|
||||
ON training_units (
|
||||
planned_date ASC,
|
||||
(planned_time_start IS NULL) ASC,
|
||||
planned_time_start ASC NULLS LAST,
|
||||
id ASC
|
||||
)
|
||||
WHERE framework_slot_id IS NULL;
|
||||
41
backend/migrations/062_exercise_skills_level_rank_index.sql
Normal file
41
backend/migrations/062_exercise_skills_level_rank_index.sql
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
-- list_exercises mit skill_min_level / skill_max_level: EXISTS auf exercise_skills mit numerischem Stufen-Rang.
|
||||
-- Ausdruck muss mit backend/routers/exercises.py _EXERCISE_SKILL_LEVEL_RANK_SQL (Alias „es“) übereinstimmen.
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_exercise_skills_exercise_level_rank
|
||||
ON exercise_skills (
|
||||
exercise_id,
|
||||
(CASE COALESCE(
|
||||
NULLIF(TRIM(LOWER(target_level::text)), ''),
|
||||
NULLIF(TRIM(LOWER(required_level::text)), '')
|
||||
)
|
||||
WHEN 'basis' THEN 1
|
||||
WHEN 'grundlagen' THEN 2
|
||||
WHEN 'aufbau' THEN 3
|
||||
WHEN 'fortgeschritten' THEN 4
|
||||
WHEN 'optimierung' THEN 5
|
||||
WHEN 'einsteiger' THEN 1
|
||||
WHEN 'experte' THEN 5
|
||||
WHEN '1' THEN 1
|
||||
WHEN '2' THEN 2
|
||||
WHEN '3' THEN 3
|
||||
WHEN '4' THEN 4
|
||||
WHEN '5' THEN 5
|
||||
ELSE NULL END)
|
||||
)
|
||||
WHERE (CASE COALESCE(
|
||||
NULLIF(TRIM(LOWER(target_level::text)), ''),
|
||||
NULLIF(TRIM(LOWER(required_level::text)), '')
|
||||
)
|
||||
WHEN 'basis' THEN 1
|
||||
WHEN 'grundlagen' THEN 2
|
||||
WHEN 'aufbau' THEN 3
|
||||
WHEN 'fortgeschritten' THEN 4
|
||||
WHEN 'optimierung' THEN 5
|
||||
WHEN 'einsteiger' THEN 1
|
||||
WHEN 'experte' THEN 5
|
||||
WHEN '1' THEN 1
|
||||
WHEN '2' THEN 2
|
||||
WHEN '3' THEN 3
|
||||
WHEN '4' THEN 4
|
||||
WHEN '5' THEN 5
|
||||
ELSE NULL END) IS NOT NULL;
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
-- Migration 063: Phasen und parallele Streams pro Trainingseinheit (Grundlage Breakout).
|
||||
-- Bestehende Sektionen werden einer Default-whole_group-Phase zugeordnet.
|
||||
-- UNIQUE (training_unit_id, order_index) auf Sektionen entfällt zugunsten
|
||||
-- eindeutiger order_index je Phase bzw. je parallel_stream.
|
||||
|
||||
-- ── Phasen ───────────────────────────────────────────────────────────────
|
||||
CREATE TABLE IF NOT EXISTS training_unit_phases (
|
||||
id SERIAL PRIMARY KEY,
|
||||
training_unit_id INT NOT NULL REFERENCES training_units(id) ON DELETE CASCADE,
|
||||
order_index INT NOT NULL,
|
||||
phase_kind VARCHAR(20) NOT NULL CHECK (phase_kind IN ('whole_group', 'parallel')),
|
||||
title VARCHAR(200),
|
||||
guidance_notes TEXT,
|
||||
UNIQUE (training_unit_id, order_index)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_training_unit_phases_unit ON training_unit_phases(training_unit_id);
|
||||
|
||||
-- ── Streams innerhalb einer Parallelphase ──────────────────────────────────
|
||||
CREATE TABLE IF NOT EXISTS training_unit_parallel_streams (
|
||||
id SERIAL PRIMARY KEY,
|
||||
phase_id INT NOT NULL REFERENCES training_unit_phases(id) ON DELETE CASCADE,
|
||||
order_index INT NOT NULL,
|
||||
title VARCHAR(200),
|
||||
notes TEXT,
|
||||
assigned_trainer_profile_ids JSONB,
|
||||
UNIQUE (phase_id, order_index)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_training_unit_parallel_streams_phase
|
||||
ON training_unit_parallel_streams(phase_id);
|
||||
|
||||
COMMENT ON COLUMN training_unit_parallel_streams.assigned_trainer_profile_ids IS
|
||||
'Optionale Co-Trainer-IDs (JSON-Array von Profil-IDs) für diese Teilstrecke; MVP+';
|
||||
|
||||
-- ── Sektionen: Zuordnung zu Phase (gemeinsam) oder Stream (parallel) ─────
|
||||
ALTER TABLE training_unit_sections
|
||||
ADD COLUMN IF NOT EXISTS phase_id INT REFERENCES training_unit_phases(id) ON DELETE CASCADE,
|
||||
ADD COLUMN IF NOT EXISTS parallel_stream_id INT REFERENCES training_unit_parallel_streams(id) ON DELETE CASCADE;
|
||||
|
||||
-- Backfill: je Einheit mit Sektionen eine whole_group-Phase, alle Sektionen dorthin
|
||||
INSERT INTO training_unit_phases (training_unit_id, order_index, phase_kind, title)
|
||||
SELECT tu.id, 0, 'whole_group', NULL
|
||||
FROM training_units tu
|
||||
WHERE EXISTS (SELECT 1 FROM training_unit_sections s WHERE s.training_unit_id = tu.id)
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM training_unit_phases p
|
||||
WHERE p.training_unit_id = tu.id AND p.order_index = 0 AND p.phase_kind = 'whole_group'
|
||||
);
|
||||
|
||||
UPDATE training_unit_sections tus
|
||||
SET phase_id = p.id
|
||||
FROM training_unit_phases p
|
||||
WHERE tus.phase_id IS NULL
|
||||
AND p.training_unit_id = tus.training_unit_id
|
||||
AND p.order_index = 0
|
||||
AND p.phase_kind = 'whole_group';
|
||||
|
||||
-- Alte globale Reihenfolge-Eindeutigkeit pro Einheit entfernen
|
||||
ALTER TABLE training_unit_sections
|
||||
DROP CONSTRAINT IF EXISTS training_unit_sections_training_unit_id_order_index_key;
|
||||
|
||||
-- Genau eine Zielspalte gesetzt: gemeinsame Phase ODER paralleler Stream
|
||||
ALTER TABLE training_unit_sections
|
||||
DROP CONSTRAINT IF EXISTS training_unit_sections_phase_or_stream_chk;
|
||||
|
||||
ALTER TABLE training_unit_sections
|
||||
ADD CONSTRAINT training_unit_sections_phase_or_stream_chk CHECK (
|
||||
(phase_id IS NOT NULL AND parallel_stream_id IS NULL)
|
||||
OR (phase_id IS NULL AND parallel_stream_id IS NOT NULL)
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uq_training_unit_sections_phase_order
|
||||
ON training_unit_sections (phase_id, order_index)
|
||||
WHERE phase_id IS NOT NULL;
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uq_training_unit_sections_stream_order
|
||||
ON training_unit_sections (parallel_stream_id, order_index)
|
||||
WHERE parallel_stream_id IS NOT NULL;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_training_unit_sections_phase
|
||||
ON training_unit_sections(phase_id) WHERE phase_id IS NOT NULL;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_training_unit_sections_parallel_stream
|
||||
ON training_unit_sections(parallel_stream_id) WHERE parallel_stream_id IS NOT NULL;
|
||||
8
backend/migrations/064_training_plan_template_phases.sql
Normal file
8
backend/migrations/064_training_plan_template_phases.sql
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
-- Vorlagen: Phasen/Parallel-Streams wie im Einheiten-Editor (planLoc-Abbild)
|
||||
ALTER TABLE training_plan_template_sections
|
||||
ADD COLUMN IF NOT EXISTS phase_kind VARCHAR(20) NOT NULL DEFAULT 'whole_group',
|
||||
ADD COLUMN IF NOT EXISTS phase_order_index INT NOT NULL DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS parallel_stream_order_index INT NULL;
|
||||
|
||||
COMMENT ON COLUMN training_plan_template_sections.parallel_stream_order_index IS
|
||||
'NULL = Ganzgruppen-Abschnitt; 0..n = Stream innerhalb paralleler Phase';
|
||||
11
backend/migrations/065_skills_wiki_karate_relevance.sql
Normal file
11
backend/migrations/065_skills_wiki_karate_relevance.sql
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
-- Migration 065: Wiki-spezifische Felder fuer Fähigkeiten (KarateRelevanz, RelevanzLevel)
|
||||
-- SMW karatetrainer.net; Import mappt in strukturierte Spalten statt nur Freitext in description
|
||||
|
||||
ALTER TABLE skills
|
||||
ADD COLUMN IF NOT EXISTS karate_relevance TEXT;
|
||||
|
||||
ALTER TABLE skills
|
||||
ADD COLUMN IF NOT EXISTS relevance_level SMALLINT CHECK (relevance_level IS NULL OR relevance_level BETWEEN 1 AND 3);
|
||||
|
||||
COMMENT ON COLUMN skills.karate_relevance IS 'Wiki Karate-Relevanz (Plaintext aus SMW Property KarateRelevanz)';
|
||||
COMMENT ON COLUMN skills.relevance_level IS 'Wiki-RelevanzLevel 1–3 (Semantic MediaWiki)';
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
-- Geplante Gesamt- und Abschnittsdauer; Rahmenprogramm: Fokus/Stil als M:N (wie Trainingsarten/Zielgruppen)
|
||||
|
||||
ALTER TABLE training_units
|
||||
ADD COLUMN IF NOT EXISTS planned_duration_min INT;
|
||||
|
||||
ALTER TABLE training_unit_sections
|
||||
ADD COLUMN IF NOT EXISTS planned_duration_min INT;
|
||||
|
||||
ALTER TABLE training_plan_template_sections
|
||||
ADD COLUMN IF NOT EXISTS planned_duration_min INT;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS training_framework_program_focus_areas (
|
||||
framework_program_id INT NOT NULL REFERENCES training_framework_programs(id) ON DELETE CASCADE,
|
||||
focus_area_id INT NOT NULL REFERENCES focus_areas(id) ON DELETE CASCADE,
|
||||
PRIMARY KEY (framework_program_id, focus_area_id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_tfpfa_focus ON training_framework_program_focus_areas(focus_area_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS training_framework_program_style_directions (
|
||||
framework_program_id INT NOT NULL REFERENCES training_framework_programs(id) ON DELETE CASCADE,
|
||||
style_direction_id INT NOT NULL REFERENCES style_directions(id) ON DELETE CASCADE,
|
||||
PRIMARY KEY (framework_program_id, style_direction_id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_tfpsd_style ON training_framework_program_style_directions(style_direction_id);
|
||||
|
||||
INSERT INTO training_framework_program_focus_areas (framework_program_id, focus_area_id)
|
||||
SELECT id, focus_area_id FROM training_framework_programs
|
||||
WHERE focus_area_id IS NOT NULL
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
INSERT INTO training_framework_program_style_directions (framework_program_id, style_direction_id)
|
||||
SELECT id, style_direction_id FROM training_framework_programs
|
||||
WHERE style_direction_id IS NOT NULL
|
||||
ON CONFLICT DO NOTHING;
|
||||
141
backend/migrations/067_ai_prompts_exercise_assistant.sql
Normal file
141
backend/migrations/067_ai_prompts_exercise_assistant.sql
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
-- Migration 067: Konfigurierbare KI-Prompts + Tracking-Feld fuer Uebungs-Zusammenfassung
|
||||
-- Datum: 2026-05-22
|
||||
-- Spec: technical/KI_FEATURES_SPEC.md, AI_PROMPT_SYSTEM_SPEC.md
|
||||
|
||||
-- ============================================================================
|
||||
-- AI PROMPTS
|
||||
-- ============================================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ai_prompts (
|
||||
id SERIAL PRIMARY KEY,
|
||||
slug VARCHAR(100) NOT NULL UNIQUE,
|
||||
display_name VARCHAR(200) NOT NULL,
|
||||
description TEXT,
|
||||
template TEXT NOT NULL,
|
||||
|
||||
category VARCHAR(50) DEFAULT 'exercise'
|
||||
CHECK (category IN ('exercise', 'training', 'matrix', 'import', 'admin')),
|
||||
|
||||
output_format VARCHAR(10) DEFAULT 'text'
|
||||
CHECK (output_format IN ('text', 'json')),
|
||||
|
||||
output_schema JSONB,
|
||||
is_system_default BOOLEAN DEFAULT false,
|
||||
default_template TEXT,
|
||||
|
||||
active BOOLEAN DEFAULT true,
|
||||
sort_order INT DEFAULT 0,
|
||||
|
||||
created_by INT REFERENCES profiles(id) ON DELETE SET NULL,
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
updated_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_ai_prompts_slug ON ai_prompts(slug);
|
||||
CREATE INDEX IF NOT EXISTS idx_ai_prompts_category ON ai_prompts(category);
|
||||
CREATE INDEX IF NOT EXISTS idx_ai_prompts_active ON ai_prompts(active, sort_order);
|
||||
|
||||
DROP TRIGGER IF EXISTS ai_prompts_update ON ai_prompts;
|
||||
CREATE TRIGGER ai_prompts_update
|
||||
BEFORE UPDATE ON ai_prompts
|
||||
FOR EACH ROW EXECUTE FUNCTION update_timestamp();
|
||||
|
||||
-- ============================================================================
|
||||
-- TRACKING SUMMARY (KI)
|
||||
-- ============================================================================
|
||||
|
||||
ALTER TABLE exercises ADD COLUMN IF NOT EXISTS summary_ai_generated BOOLEAN DEFAULT false;
|
||||
|
||||
COMMENT ON COLUMN exercises.summary_ai_generated IS 'TRUE wenn Kurzbeschreibung zuletzt von KI vorgeschlagen und uebernommen (UI setzt bei manueller Aenderung false)';
|
||||
|
||||
-- ============================================================================
|
||||
-- SEED PROMPTS (idempotent)
|
||||
-- ============================================================================
|
||||
|
||||
INSERT INTO ai_prompts (
|
||||
slug, display_name, description, template,
|
||||
category, output_format, is_system_default, default_template, active, sort_order
|
||||
)
|
||||
SELECT
|
||||
'pipeline',
|
||||
'Mehrstufige Gesamtanalyse',
|
||||
'Master-Schalter fuer die Pipeline-Anzeige.',
|
||||
'PIPELINE_MASTER',
|
||||
'admin',
|
||||
'text',
|
||||
false,
|
||||
'PIPELINE_MASTER',
|
||||
true,
|
||||
-10
|
||||
WHERE NOT EXISTS (SELECT 1 FROM ai_prompts WHERE slug = 'pipeline');
|
||||
|
||||
INSERT INTO ai_prompts (
|
||||
slug, display_name, description, template,
|
||||
category, output_format, is_system_default, default_template, active, sort_order
|
||||
)
|
||||
SELECT
|
||||
'exercise_summary',
|
||||
'Uebungs-Zusammenfassung',
|
||||
'Erzeugt eine kurze Kurzbeschreibung fuer Listen/Galerie.',
|
||||
$s$Du bist Assistent fuer Kampfsport-Trainer.
|
||||
Erstelle eine kurze Kurzbeschreibung fuer Listen und Trainingsplaene.
|
||||
|
||||
Anforderungen:
|
||||
- Hochstens etwa 200 Zeichen (bei Bedarf gekuerzt fuer Mobile)
|
||||
- Kern: Welche Trainingsqualitaeten? Wie fuehrt man die Uebung kurz aus?
|
||||
- Sachlich, auf Deutsch
|
||||
|
||||
Uebung: {{exercise_title}}
|
||||
Fokuskontext: {{exercise_focus_area}}
|
||||
Ziel (Fliesstext, kann HTML sein): {{exercise_goal}}
|
||||
Durchfuehrung (Fliesstext, kann HTML sein): {{exercise_execution}}
|
||||
|
||||
Antworte NUR mit der Kurzbeschreibung als einfachen Text (keine Markdown-Codeblocks, keine Anfuehrungszeichen um den ganzen Text).$s$,
|
||||
'exercise',
|
||||
'text',
|
||||
true,
|
||||
NULL,
|
||||
true,
|
||||
1
|
||||
WHERE NOT EXISTS (SELECT 1 FROM ai_prompts WHERE slug = 'exercise_summary');
|
||||
|
||||
INSERT INTO ai_prompts (
|
||||
slug, display_name, description, template,
|
||||
category, output_format, is_system_default, default_template, active, sort_order
|
||||
)
|
||||
SELECT
|
||||
'exercise_skill_suggestions',
|
||||
'Faehigkeiten-Empfehlungen',
|
||||
'Schlaegt passende Skills mit Stufen/Intensitaet vor (JSON-Ausgabe-Prompt).',
|
||||
$j$Du bist Assistent fuer Kampfsport-Trainer.
|
||||
Ordne diese Uebung dem globalen Skill-Katalog zu.
|
||||
|
||||
Daten zur Uebung:
|
||||
Titel: {{exercise_title}}
|
||||
Fokuskontext (optional): {{exercise_focus_area}}
|
||||
Ziel (gekuerzt_plain): {{exercise_goal}}
|
||||
Durchfuehrung (gekuerzt_plain): {{exercise_execution}}
|
||||
|
||||
Verfuegbare Faehigkeiten (Auswahl NUR ueber diese IDs — keine anderen IDs verwenden):
|
||||
{{skills_catalog}}
|
||||
|
||||
Waehle hoechstens 5 passende Skills. Für jede Faehigkeit:
|
||||
- skill_id: ganze Zahl aus der Liste
|
||||
- required_level: eines von basis, grundlagen, aufbau, fortgeschritten, optimierung
|
||||
- target_level: derselbe Wertvorrat
|
||||
- intensity: eines von niedrig, mittel, hoch
|
||||
- is_primary (optional): true fuer die Hauptfaehigkeit der Uebung, sondern false/weglassen
|
||||
|
||||
Antworte NUR mit einem JSON-Array ohne Erklaertext, keine Markdown-Fences.
|
||||
|
||||
Beispielformat:
|
||||
[{"skill_id": 1, "required_level": "grundlagen", "target_level": "aufbau", "intensity": "hoch", "is_primary": true}]
|
||||
|
||||
Wenn nichts gut passt, antworte mit [].$j$,
|
||||
'exercise',
|
||||
'json',
|
||||
true,
|
||||
NULL,
|
||||
true,
|
||||
2
|
||||
WHERE NOT EXISTS (SELECT 1 FROM ai_prompts WHERE slug = 'exercise_skill_suggestions');
|
||||
125
backend/migrations/068_ai_skill_retrieval_profiles.sql
Normal file
125
backend/migrations/068_ai_skill_retrieval_profiles.sql
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
-- Migration 068: KI Skill-Retrieval-Profile pro Fokusbereich (+ Standardprofil)
|
||||
-- Purpose: Gewichtungen/Quota fuer exercise_ai Skill-Katalog (OpenRouter Kontext)
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ai_skill_retrieval_profiles (
|
||||
id SERIAL PRIMARY KEY,
|
||||
focus_area_id INT REFERENCES focus_areas(id) ON DELETE CASCADE,
|
||||
is_default BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
name VARCHAR(200) NOT NULL,
|
||||
description TEXT,
|
||||
active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
config JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
updated_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS ux_ai_skill_retrieval_profile_focus_area
|
||||
ON ai_skill_retrieval_profiles (focus_area_id)
|
||||
WHERE focus_area_id IS NOT NULL AND active = TRUE;
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS ux_ai_skill_retrieval_profile_default_only
|
||||
ON ai_skill_retrieval_profiles (is_default)
|
||||
WHERE is_default IS TRUE AND active = TRUE;
|
||||
|
||||
COMMENT ON TABLE ai_skill_retrieval_profiles IS
|
||||
'Gewichte/Quota fuer Skill-Katalog in exercise_ai; optional gebunden an focus_areas, genau eine is_default=TRUE';
|
||||
|
||||
INSERT INTO ai_skill_retrieval_profiles (focus_area_id, is_default, name, description, active, config)
|
||||
VALUES (
|
||||
NULL,
|
||||
TRUE,
|
||||
'Standard',
|
||||
'Kein/Undefinierter Fokusbereich: neutrale Gewichte mit sanften Caps auf sehr breite Unterkategorien.',
|
||||
TRUE,
|
||||
'{
|
||||
"version": 1,
|
||||
"importance_multiplier": 1,
|
||||
"text_overlap_bonus": 2,
|
||||
"main_slug_weights": { "karate": 1, "allgemeine": 1 },
|
||||
"category_slug_weights": {},
|
||||
"category_max_share": {
|
||||
"kondition": 0.38,
|
||||
"koordination": 0.35
|
||||
},
|
||||
"main_min_share": {},
|
||||
"description_plain_max_len": 160,
|
||||
"karate_relevance_max_len": 72,
|
||||
"keyword_overrides": [
|
||||
{
|
||||
"keywords_any": ["rollenspiel", "szenario", "deesk", "diskussion"],
|
||||
"case_insensitive": true,
|
||||
"patch": {
|
||||
"category_slug_weights": {
|
||||
"psychische_faehigkeiten": 1.65,
|
||||
"soziale_faehigkeiten": 1.65,
|
||||
"kognition": 1.4
|
||||
},
|
||||
"category_max_share": {
|
||||
"kondition": 0.08,
|
||||
"koordination": 0.1
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"keywords_any": ["befreiung", "haltegriff", "greifer", "umklammer"],
|
||||
"case_insensitive": true,
|
||||
"patch": {
|
||||
"category_slug_weights": {
|
||||
"selbstverteidigung": 2.2,
|
||||
"koordination": 0.9
|
||||
},
|
||||
"main_slug_weights": { "karate": 1.35 }
|
||||
}
|
||||
}
|
||||
]
|
||||
}'::jsonb
|
||||
);
|
||||
|
||||
INSERT INTO ai_skill_retrieval_profiles (focus_area_id, is_default, name, description, active, config)
|
||||
SELECT
|
||||
fa.id,
|
||||
FALSE,
|
||||
'Gewaltschutz',
|
||||
'Kaum klassische Sportfaehigkeit; Gewicht auf Deeskalation, Kognition/Soziales; SV-Schwerpunkt per Keywords verstaerken.',
|
||||
TRUE,
|
||||
'{
|
||||
"version": 1,
|
||||
"importance_multiplier": 1,
|
||||
"text_overlap_bonus": 2.25,
|
||||
"main_slug_weights": { "karate": 1.08, "allgemeine": 1.06 },
|
||||
"category_slug_weights": {
|
||||
"kognition": 1.72,
|
||||
"psychische_faehigkeiten": 1.78,
|
||||
"soziale_faehigkeiten": 1.78,
|
||||
"selbstverteidigung": 1.82,
|
||||
"kondition": 0.32,
|
||||
"koordination": 0.4
|
||||
},
|
||||
"category_max_share": {
|
||||
"kondition": 0.12,
|
||||
"koordination": 0.16
|
||||
},
|
||||
"main_min_share": {},
|
||||
"description_plain_max_len": 150,
|
||||
"karate_relevance_max_len": 0,
|
||||
"keyword_overrides": [
|
||||
{
|
||||
"keywords_any": ["befreiung", "haltegriff", "greifer"],
|
||||
"case_insensitive": true,
|
||||
"patch": {
|
||||
"category_slug_weights": {
|
||||
"selbstverteidigung": 3.25,
|
||||
"koordination": 1.08
|
||||
},
|
||||
"main_slug_weights": { "karate": 1.5 }
|
||||
}
|
||||
}
|
||||
]
|
||||
}'::jsonb
|
||||
FROM focus_areas fa
|
||||
WHERE fa.name = 'Gewaltschutz'
|
||||
AND (fa.status IS NULL OR fa.status = 'active')
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM ai_skill_retrieval_profiles p
|
||||
WHERE p.focus_area_id = fa.id AND p.active = TRUE
|
||||
)
|
||||
LIMIT 1;
|
||||
10
backend/migrations/069_ai_prompts_default_template.sql
Normal file
10
backend/migrations/069_ai_prompts_default_template.sql
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
-- Migration 069: ai_prompts default_template fuer Ruecksetzen & Transparenz
|
||||
-- Setzt fuer bestehende System-Prompt-Zeilen default_template aus dem aktuellen template,
|
||||
-- sofern noch kein Referenzinhalt gespeichert war (Migration 067 hatte NULL fuer exercise_*).
|
||||
|
||||
UPDATE ai_prompts
|
||||
SET default_template = template,
|
||||
updated_at = NOW()
|
||||
WHERE default_template IS NULL
|
||||
AND template IS NOT NULL
|
||||
AND LENGTH(TRIM(template)) > 0;
|
||||
7
backend/migrations/070_ai_prompts_openrouter_model.sql
Normal file
7
backend/migrations/070_ai_prompts_openrouter_model.sql
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
-- Migration 070: optionales OpenRouter-Modell pro Prompt-Zeile
|
||||
-- Leer/NULL → Umgebungsvariable OPENROUTER_MODEL (wie bisher).
|
||||
|
||||
ALTER TABLE ai_prompts ADD COLUMN IF NOT EXISTS openrouter_model VARCHAR(200);
|
||||
|
||||
COMMENT ON COLUMN ai_prompts.openrouter_model IS
|
||||
'Optional: OpenRouter model id (z.B. anthropic/claude-3.5-haiku); NULL = OPENROUTER_MODEL aus Env';
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
-- Migration 071: KI-Prompt fuer Anleitungs-Ueberarbeitung (Ziel, Durchfuehrung, Vorbereitung, Trainer-Hinweise)
|
||||
-- JSON-Ausgabe; praezise HTML-Fragmente fuer RichTextEditor.
|
||||
|
||||
INSERT INTO ai_prompts (
|
||||
slug, display_name, description, template,
|
||||
category, output_format, output_schema, is_system_default, default_template, active, sort_order
|
||||
)
|
||||
SELECT
|
||||
'exercise_instruction_rewrite',
|
||||
'Anleitung ueberarbeiten',
|
||||
'Ueberarbeitet Ziel, Durchfuehrung, Vorbereitung und Trainer-Hinweise — praezise, strukturiert, ohne Aufblaehen.',
|
||||
$t$Du bist Assistent fuer Kampfsport-Trainer.
|
||||
Ueberarbeite die Anleitung dieser Uebung: verbessere Formulierung, ergaenze fehlende Kernpunkte, kuerze ueberfluessige Passagen.
|
||||
Wichtig: Texte sollen praezise und nachvollziehbar bleiben — keine Fuellsaetze, keine Wiederholungen, kein Marketing.
|
||||
|
||||
Stil:
|
||||
- Deutsch, sachlich, direkt an Trainer gerichtet (Durchfuehrung: Imperativ oder klare Schritte)
|
||||
- Ziel: 1–3 kurze Absaetze (Kern des Trainingsziels)
|
||||
- Durchfuehrung: klare Schritte (nummerierte Liste oder kurze Absaetze)
|
||||
- Vorbereitung/Aufbau: nur wenn noetig (Raum, Material, Aufbau) — sonst leerer String
|
||||
- Trainer-Hinweise: Sicherheit, typische Fehler, Coaching-Tipps — knapp, Stichpunkte oder kurze Absaetze
|
||||
|
||||
Format (HTML fuer Rich-Text-Editor):
|
||||
- Erlaubt: <p>, <ul>, <ol>, <li>, <strong>, <em>, <br>
|
||||
- Keine Ueberschriften (h1–h6), keine Tabellen, kein Markdown, keine Code-Fences
|
||||
- Medienverweise {{exerciseMedia:ID}} aus den Eingabetexten UNVERAENDERT an passender Stelle uebernehmen
|
||||
|
||||
Eingabe:
|
||||
Titel: {{exercise_title}}
|
||||
Fokuskontext: {{exercise_focus_area}}
|
||||
|
||||
Ziel (Plaintext, Ausgang): {{exercise_goal}}
|
||||
Durchfuehrung (Plaintext, Ausgang): {{exercise_execution}}
|
||||
Vorbereitung/Aufbau (Plaintext, Ausgang): {{exercise_preparation}}
|
||||
Trainer-Hinweise (Plaintext, Ausgang): {{exercise_trainer_notes}}
|
||||
|
||||
Antworte NUR mit einem JSON-Objekt (kein Text davor/danach):
|
||||
{
|
||||
"goal": "<p>…</p>",
|
||||
"execution": "<ol><li>…</li></ol>",
|
||||
"preparation": "<p>…</p> oder \"\"",
|
||||
"trainer_notes": "<ul><li>…</li></ul> oder \"\""
|
||||
}
|
||||
|
||||
Leere Felder als leerer String "" wenn nichts Sinnvolles ergibt.$t$,
|
||||
'exercise',
|
||||
'json',
|
||||
'{"type":"object","required":["goal","execution","preparation","trainer_notes"],"properties":{"goal":{"type":"string"},"execution":{"type":"string"},"preparation":{"type":"string"},"trainer_notes":{"type":"string"}}}'::jsonb,
|
||||
true,
|
||||
NULL,
|
||||
true,
|
||||
3
|
||||
WHERE NOT EXISTS (SELECT 1 FROM ai_prompts WHERE slug = 'exercise_instruction_rewrite');
|
||||
|
||||
-- Referenztext fuer Admin-Ruecksetzen (wie 069)
|
||||
UPDATE ai_prompts
|
||||
SET default_template = template
|
||||
WHERE slug = 'exercise_instruction_rewrite'
|
||||
AND (default_template IS NULL OR TRIM(default_template) = '');
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
-- Migration 072: KI-Prompt Planungs-Übungssuche — LLM-Rerank (Phase 2)
|
||||
-- Spec: .claude/docs/working/PLANNING_EXERCISE_SUGGEST_CONTEXT.md §14
|
||||
|
||||
INSERT INTO ai_prompts (
|
||||
slug, display_name, description, template,
|
||||
category, output_format, output_schema, is_system_default, default_template, active, sort_order
|
||||
)
|
||||
SELECT
|
||||
'planning_exercise_search_rank',
|
||||
'Planungs-Übungssuche Rerank',
|
||||
'Ordnet Kandidaten für die Trainingsplanung nach Intent und Kontext; nur IDs aus candidates_json.',
|
||||
$t$Du bist Assistent für Kampfsport-Trainer bei der Trainingsplanung.
|
||||
Ordne die vorgegebenen Übungs-Kandidaten nach Eignung für die aktuelle Planungssituation.
|
||||
|
||||
Regeln:
|
||||
- Verwende NUR exercise_id-Werte aus candidates_json (keine erfundenen IDs).
|
||||
- Berücksichtige search_query, intent, planning_context_json und target_profile_json.
|
||||
- Bewerte anhand von Titel, summary, goal und skills jedes Kandidaten.
|
||||
- Gib maximal {{result_limit}} IDs in sinnvoller Reihenfolge zurück (beste zuerst).
|
||||
- Kurze Begründung pro Top-Treffer auf Deutsch (1 Satz, sachlich).
|
||||
|
||||
Intent-Hinweise:
|
||||
- suggest_next / progression_next: logische Fortsetzung, Progression, passende Skills
|
||||
- deepen_exercise: Vertiefung zum Anker, ähnlicher Fokus
|
||||
- continue_plan_goal: schließt an bisherigen Plan und Skill-Lücken an
|
||||
- free_search: Freitext-Relevanz
|
||||
|
||||
Kontext:
|
||||
Intent: {{intent}}
|
||||
Suchanfrage: {{search_query}}
|
||||
Planung: {{planning_context_json}}
|
||||
Zielprofil: {{target_profile_json}}
|
||||
|
||||
Kandidaten (JSON):
|
||||
{{candidates_json}}
|
||||
|
||||
Antworte NUR mit JSON (kein Text davor/danach):
|
||||
{
|
||||
"ranked_ids": [123, 456],
|
||||
"reasons": { "123": "…", "456": "…" }
|
||||
}$t$,
|
||||
'training',
|
||||
'json',
|
||||
'{"type":"object","required":["ranked_ids"],"properties":{"ranked_ids":{"type":"array","items":{"type":"integer"}},"reasons":{"type":"object"}}}'::jsonb,
|
||||
true,
|
||||
NULL,
|
||||
true,
|
||||
10
|
||||
WHERE NOT EXISTS (SELECT 1 FROM ai_prompts WHERE slug = 'planning_exercise_search_rank');
|
||||
|
||||
UPDATE ai_prompts
|
||||
SET default_template = template
|
||||
WHERE slug = 'planning_exercise_search_rank'
|
||||
AND (default_template IS NULL OR TRIM(default_template) = '');
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
-- Migration 073: KI-Prompt Planungs-Übungssuche — Intent/Query-Overlay (P1)
|
||||
-- Spec: .claude/docs/working/PLANNING_EXERCISE_SUGGEST_CONTEXT.md §16
|
||||
|
||||
INSERT INTO ai_prompts (
|
||||
slug, display_name, description, template,
|
||||
category, output_format, output_schema, is_system_default, default_template, active, sort_order
|
||||
)
|
||||
SELECT
|
||||
'planning_exercise_search_intent',
|
||||
'Planungs-Übungssuche Intent',
|
||||
'Strukturiert Freitext-Anfrage in Intent, Szenario und Katalog-Hints für Erwartungsprofil-Overlay.',
|
||||
$t$Du bist Assistent für Kampfsport-Trainer in der Trainingsplanung.
|
||||
Analysiere die Suchanfrage im Kontext der Einheit und des bisherigen Plans.
|
||||
|
||||
Ziel: JSON für ein Erwartungsprofil-Overlay (Fähigkeiten, Fokus, Stil …) — NICHT Übungs-IDs erfinden.
|
||||
|
||||
Szenario-Klassen (scenario):
|
||||
- preset_next: nur „nächste Übung“ ohne Zusatz — selten bei Freitext
|
||||
- progression: Progressionsgraph / Pfad / Folgeübung im Graph
|
||||
- deepen: Vertiefung zur Anker-Übung
|
||||
- continue_plan: baut auf bisherigem Plan der Einheit auf
|
||||
- additive_constraint: Plan beibehalten UND zusätzliche Anforderung (z. B. „außerdem Schnellkraft“)
|
||||
- free_search: offene Stichwortsuche / neues Thema
|
||||
|
||||
Intent (intent): suggest_next | progression_next | deepen_exercise | continue_plan_goal | free_search
|
||||
|
||||
emphasis:
|
||||
- additive: Zusatz zur bestehenden Planung (Default bei „zusätzlich/auch/dazu“)
|
||||
- replace: Suchanfrage soll Schwerpunkt eher ersetzen
|
||||
- neutral: nur leichte Gewichtung
|
||||
|
||||
Nutze skill_hints/focus_hints etc. mit Namen aus den Katalog-JSONs (beste Übereinstimmung).
|
||||
Bei requires_partner: true/false/null wenn Partnerbezug erkennbar.
|
||||
|
||||
Eingabe:
|
||||
Suchanfrage: {{search_query}}
|
||||
Heuristik-Intent: {{heuristic_intent}}
|
||||
Szenario-Hinweis (Server): {{scenario_hint}}
|
||||
Planungskontext: {{planning_context_json}}
|
||||
Basis-Zielprofil (deterministisch): {{target_profile_json}}
|
||||
|
||||
Kataloge (Auszug — nur diese Namen/IDs verwenden):
|
||||
Skills: {{skills_catalog_json}}
|
||||
Fokus: {{focus_areas_catalog_json}}
|
||||
Trainingsstil: {{training_types_catalog_json}}
|
||||
Stilrichtung: {{style_directions_catalog_json}}
|
||||
Zielgruppe: {{target_groups_catalog_json}}
|
||||
|
||||
Antworte NUR mit JSON:
|
||||
{
|
||||
"intent": "continue_plan_goal",
|
||||
"scenario": "additive_constraint",
|
||||
"skill_hints": [{"name": "Schnellkraft", "weight": 1.0}],
|
||||
"focus_hints": [],
|
||||
"style_hints": [],
|
||||
"training_type_hints": [],
|
||||
"target_group_hints": [],
|
||||
"requires_partner": null,
|
||||
"emphasis": "additive",
|
||||
"rationale": "Kurz auf Deutsch, 1 Satz"
|
||||
}$t$,
|
||||
'training',
|
||||
'json',
|
||||
'{"type":"object","required":["intent","scenario"],"properties":{"intent":{"type":"string"},"scenario":{"type":"string"},"skill_hints":{"type":"array"},"emphasis":{"type":"string"},"rationale":{"type":"string"}}}'::jsonb,
|
||||
true,
|
||||
NULL,
|
||||
true,
|
||||
11
|
||||
WHERE NOT EXISTS (SELECT 1 FROM ai_prompts WHERE slug = 'planning_exercise_search_intent');
|
||||
|
||||
UPDATE ai_prompts
|
||||
SET default_template = template
|
||||
WHERE slug = 'planning_exercise_search_intent'
|
||||
AND (default_template IS NULL OR TRIM(default_template) = '');
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
-- Migration 074: KI-Prompt Planungs-Übungssuche — Erwartungsprofil aus Planungskontext (Preset)
|
||||
-- Spec: .claude/docs/working/PLANNING_EXERCISE_SUGGEST_CONTEXT.md §16
|
||||
|
||||
INSERT INTO ai_prompts (
|
||||
slug, display_name, description, template,
|
||||
category, output_format, output_schema, is_system_default, default_template, active, sort_order
|
||||
)
|
||||
SELECT
|
||||
'planning_exercise_expectation_profile',
|
||||
'Planungs-Übungssuche Erwartungsprofil',
|
||||
'Leitet aus Einheit, Abschnitt, Anker und bisherigem Plan ein Erwartungsprofil für die nächste Übung ab (ohne Freitext-Anfrage).',
|
||||
$t$Du bist Assistent für Kampfsport-Trainer in der Trainingsplanung.
|
||||
Der Trainer wählt „nächste Übung aus Kontext“ — es gibt KEINE zusätzliche Freitext-Suchanfrage.
|
||||
|
||||
Deine Aufgabe: Aus dem Planungskontext und dem deterministischen Basis-Zielprofil ein präzises Erwartungsprofil ableiten:
|
||||
- Was soll die nächste Übung fachlich leisten (Fortsetzen, Vertiefen, Lücke schließen, Abwechslung)?
|
||||
- Welche Fähigkeiten, Fokus-Bereiche, Trainingsstile passen dazu?
|
||||
- Berücksichtige: Rahmen/Einheit, Abschnittsziel (guidance_notes), letzte Übung im Abschnitt, Anker-Übung, Skill-Profile Einheit vs. Abschnitt, Skill-Lücken im Basisprofil.
|
||||
|
||||
Intent (intent): meist suggest_next oder continue_plan_goal; progression_next nur wenn Progressionsgraph/Anker klar nahelegt; deepen_exercise nur bei klarer Vertiefungslage.
|
||||
|
||||
continuation (optional, Kurzlabel):
|
||||
- build_on_section: nahtlos an Abschnitt/letzte Übung anknüpfen
|
||||
- close_skill_gap: fehlende Fähigkeiten aus Plan/Rahmen nachziehen
|
||||
- deepen_anchor: Anker-Übung vertiefen
|
||||
- variety: bewusst variieren nach bisherigem Block
|
||||
- balance_load: Belastung ausgleichen / Tempo wechseln
|
||||
|
||||
Nutze skill_hints/focus_hints etc. mit Namen aus den Katalog-JSONs (beste Übereinstimmung).
|
||||
emphasis: fast immer additive (baut auf Basisprofil auf), nur replace wenn Kontext eindeutig neuen Schwerpunkt verlangt.
|
||||
|
||||
Eingabe:
|
||||
Heuristik-Intent: {{heuristic_intent}}
|
||||
Planungskontext: {{planning_context_json}}
|
||||
Basis-Zielprofil (deterministisch): {{target_profile_json}}
|
||||
|
||||
Kataloge (Auszug — nur diese Namen/IDs verwenden):
|
||||
Skills: {{skills_catalog_json}}
|
||||
Fokus: {{focus_areas_catalog_json}}
|
||||
Trainingsstil: {{training_types_catalog_json}}
|
||||
Stilrichtung: {{style_directions_catalog_json}}
|
||||
Zielgruppe: {{target_groups_catalog_json}}
|
||||
|
||||
Antworte NUR mit JSON:
|
||||
{
|
||||
"intent": "suggest_next",
|
||||
"scenario": "preset_next",
|
||||
"continuation": "build_on_section",
|
||||
"skill_hints": [{"name": "Kime", "weight": 0.9}],
|
||||
"focus_hints": [],
|
||||
"style_hints": [],
|
||||
"training_type_hints": [],
|
||||
"target_group_hints": [],
|
||||
"requires_partner": null,
|
||||
"emphasis": "additive",
|
||||
"rationale": "Kurz auf Deutsch, 1–2 Sätze: warum diese nächste Übung sinnvoll ist"
|
||||
}$t$,
|
||||
'training',
|
||||
'json',
|
||||
'{"type":"object","required":["intent","scenario","rationale"],"properties":{"intent":{"type":"string"},"scenario":{"type":"string"},"continuation":{"type":"string"},"skill_hints":{"type":"array"},"emphasis":{"type":"string"},"rationale":{"type":"string"}}}'::jsonb,
|
||||
true,
|
||||
NULL,
|
||||
true,
|
||||
12
|
||||
WHERE NOT EXISTS (SELECT 1 FROM ai_prompts WHERE slug = 'planning_exercise_expectation_profile');
|
||||
|
||||
UPDATE ai_prompts
|
||||
SET default_template = template
|
||||
WHERE slug = 'planning_exercise_expectation_profile'
|
||||
AND (default_template IS NULL OR TRIM(default_template) = '');
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
-- Migration 075: Planungs-KI Phase E — Semantik-Enrichment + Pfad-QA Prompts
|
||||
|
||||
INSERT INTO ai_prompts (
|
||||
slug, display_name, description, template,
|
||||
category, output_format, output_schema, is_system_default, default_template, active, sort_order
|
||||
)
|
||||
SELECT
|
||||
'planning_exercise_query_semantics',
|
||||
'Planungs-Übungssuche Semantik',
|
||||
'Erweitert deterministisches Semantic Brief um must/exclude phrases und Entwicklungsbogen.',
|
||||
$t$Du bist Assistent für Kampfsport-Trainer bei der semantischen Analyse von Planungs-Anfragen.
|
||||
|
||||
Ziel: JSON für ein Semantic Brief — präzise Kernbegriffe, Ausschlüsse, Entwicklungsbogen.
|
||||
Nutze das bestehende Brief als Basis; ergänze/verfeinere, ersetze aber keine eindeutige Technik-Identität.
|
||||
|
||||
Anfrage: {{search_query}}
|
||||
Bestehendes Brief (deterministisch): {{semantic_brief_json}}
|
||||
|
||||
Regeln:
|
||||
- must_phrases: konkrete Technik-/Themen-Phrasen aus der Anfrage (z. B. "mae geri", nicht nur "geri")
|
||||
- exclude_phrases: konkurrierende Techniken/Themen, die NICHT gemeint sind
|
||||
- development_arc: geordnete Phasen aus: einstieg, grundlage, vertiefung, anwendung, perfektion
|
||||
- semantic_strength: 0.0–1.0 (höher bei spezifischer Technik/Thema)
|
||||
- primary_topic: Hauptthema in wenigen Worten
|
||||
- topic_type: technique | focus | method | skill | general
|
||||
|
||||
Antworte NUR mit JSON:
|
||||
{
|
||||
"primary_topic": "Mae Geri",
|
||||
"topic_type": "technique",
|
||||
"must_phrases": ["mae geri"],
|
||||
"exclude_phrases": ["mawashi geri", "sakuto geri"],
|
||||
"development_arc": ["einstieg", "grundlage", "vertiefung", "perfektion"],
|
||||
"semantic_strength": 0.9,
|
||||
"rationale": "Kurz auf Deutsch"
|
||||
}$t$,
|
||||
'training',
|
||||
'json',
|
||||
'{"type":"object","properties":{"must_phrases":{"type":"array"},"exclude_phrases":{"type":"array"},"development_arc":{"type":"array"},"semantic_strength":{"type":"number"}}}'::jsonb,
|
||||
true,
|
||||
NULL,
|
||||
true,
|
||||
12
|
||||
WHERE NOT EXISTS (SELECT 1 FROM ai_prompts WHERE slug = 'planning_exercise_query_semantics');
|
||||
|
||||
INSERT INTO ai_prompts (
|
||||
slug, display_name, description, template,
|
||||
category, output_format, output_schema, is_system_default, default_template, active, sort_order
|
||||
)
|
||||
SELECT
|
||||
'planning_exercise_path_qa',
|
||||
'Planungs-Pfad QA',
|
||||
'Semantische Qualitätsprüfung eines vorgeschlagenen Übungspfads inkl. Lücken und Brücken.',
|
||||
$t$Du bist Assistent für Kampfsport-Trainer und prüfst einen vorgeschlagenen Übungspfad.
|
||||
|
||||
Ziel-Anfrage: {{goal_query}}
|
||||
Semantic Brief: {{semantic_brief_json}}
|
||||
Schritte (JSON): {{steps_json}}
|
||||
Erkannte Lücken: {{gaps_json}}
|
||||
Eingefügte Brücken: {{bridge_inserts_json}}
|
||||
|
||||
Prüfe:
|
||||
1. Deckt der Pfad das Hauptthema der Anfrage ab (nicht nur Oberbegriffe)?
|
||||
2. Ist die Reihenfolge didaktisch sinnvoll (Einstieg → Vertiefung → Ziel)?
|
||||
3. Sind Sprünge zwischen benachbarten Schritten zu groß?
|
||||
4. Sind Brücken-Übungen sinnvoll oder überflüssig?
|
||||
5. Fehlen wichtige Zwischenschritte?
|
||||
|
||||
Antworte NUR mit JSON:
|
||||
{
|
||||
"overall_ok": true,
|
||||
"quality_score": 0.85,
|
||||
"topic_coverage": "Kurz: wie gut das Hauptthema abgedeckt ist",
|
||||
"issues": ["…"],
|
||||
"sequence_notes": ["…"],
|
||||
"recommendations": ["…"]
|
||||
}$t$,
|
||||
'training',
|
||||
'json',
|
||||
'{"type":"object","required":["overall_ok"],"properties":{"overall_ok":{"type":"boolean"},"quality_score":{"type":"number"},"issues":{"type":"array"},"sequence_notes":{"type":"array"},"recommendations":{"type":"array"}}}'::jsonb,
|
||||
true,
|
||||
NULL,
|
||||
true,
|
||||
13
|
||||
WHERE NOT EXISTS (SELECT 1 FROM ai_prompts WHERE slug = 'planning_exercise_path_qa');
|
||||
|
||||
UPDATE ai_prompts SET default_template = template
|
||||
WHERE slug IN ('planning_exercise_query_semantics', 'planning_exercise_path_qa')
|
||||
AND (default_template IS NULL OR TRIM(default_template) = '');
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
-- Migration 076: Planungs-Pfad-QA — Neuordnung + KI-Lückenfüller (Phase E2)
|
||||
|
||||
UPDATE ai_prompts
|
||||
SET template = $t$Du bist Assistent für Kampfsport-Trainer und prüfst einen vorgeschlagenen Übungspfad.
|
||||
|
||||
Ziel-Anfrage: {{goal_query}}
|
||||
Semantic Brief: {{semantic_brief_json}}
|
||||
Schritte (JSON): {{steps_json}}
|
||||
Erkannte Lücken: {{gaps_json}}
|
||||
Eingefügte Brücken: {{bridge_inserts_json}}
|
||||
|
||||
Prüfe:
|
||||
1. Deckt der Pfad das Hauptthema der Anfrage ab (nicht nur Oberbegriffe)?
|
||||
2. Ist die Reihenfolge didaktisch sinnvoll (Einstieg → Vertiefung → Ziel)?
|
||||
3. Sind Sprünge zwischen benachbarten Schritten zu groß?
|
||||
4. Sind Brücken-Übungen sinnvoll oder überflüssig?
|
||||
5. Fehlen wichtige Zwischenschritte?
|
||||
|
||||
Wenn die Reihenfolge verbessert werden sollte: ordered_step_indices = Permutation der aktuellen 0-basierten Schritt-Indizes (beste didaktische Reihenfolge).
|
||||
Nur Indizes aus dem steps_json verwenden — Länge muss exakt der Schrittzahl entsprechen.
|
||||
|
||||
Antworte NUR mit JSON:
|
||||
{
|
||||
"overall_ok": true,
|
||||
"quality_score": 0.85,
|
||||
"topic_coverage": "Kurz: wie gut das Hauptthema abgedeckt ist",
|
||||
"ordered_step_indices": [0, 1, 2, 3],
|
||||
"issues": ["…"],
|
||||
"sequence_notes": ["…"],
|
||||
"recommendations": ["…"]
|
||||
}$t$,
|
||||
default_template = $t$Du bist Assistent für Kampfsport-Trainer und prüfst einen vorgeschlagenen Übungspfad.
|
||||
|
||||
Ziel-Anfrage: {{goal_query}}
|
||||
Semantic Brief: {{semantic_brief_json}}
|
||||
Schritte (JSON): {{steps_json}}
|
||||
Erkannte Lücken: {{gaps_json}}
|
||||
Eingefügte Brücken: {{bridge_inserts_json}}
|
||||
|
||||
Prüfe:
|
||||
1. Deckt der Pfad das Hauptthema der Anfrage ab (nicht nur Oberbegriffe)?
|
||||
2. Ist die Reihenfolge didaktisch sinnvoll (Einstieg → Vertiefung → Ziel)?
|
||||
3. Sind Sprünge zwischen benachbarten Schritten zu groß?
|
||||
4. Sind Brücken-Übungen sinnvoll oder überflüssig?
|
||||
5. Fehlen wichtige Zwischenschritte?
|
||||
|
||||
Wenn die Reihenfolge verbessert werden sollte: ordered_step_indices = Permutation der aktuellen 0-basierten Schritt-Indizes (beste didaktische Reihenfolge).
|
||||
Nur Indizes aus dem steps_json verwenden — Länge muss exakt der Schrittzahl entsprechen.
|
||||
|
||||
Antworte NUR mit JSON:
|
||||
{
|
||||
"overall_ok": true,
|
||||
"quality_score": 0.85,
|
||||
"topic_coverage": "Kurz: wie gut das Hauptthema abgedeckt ist",
|
||||
"ordered_step_indices": [0, 1, 2, 3],
|
||||
"issues": ["…"],
|
||||
"sequence_notes": ["…"],
|
||||
"recommendations": ["…"]
|
||||
}$t$
|
||||
WHERE slug = 'planning_exercise_path_qa';
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
-- Migration 077: Planungs-Pfad-QA — strukturierte Neuanlage-Vorschläge (Phase E3)
|
||||
|
||||
UPDATE ai_prompts
|
||||
SET template = $t$Du bist Assistent für Kampfsport-Trainer und prüfst einen vorgeschlagenen Übungspfad.
|
||||
|
||||
Ziel-Anfrage: {{goal_query}}
|
||||
Semantic Brief: {{semantic_brief_json}}
|
||||
Schritte (JSON): {{steps_json}}
|
||||
Erkannte Lücken: {{gaps_json}}
|
||||
Eingefügte Brücken: {{bridge_inserts_json}}
|
||||
|
||||
Prüfe:
|
||||
1. Deckt der Pfad das Hauptthema der Anfrage ab (nicht nur Oberbegriffe)?
|
||||
2. Ist die Reihenfolge didaktisch sinnvoll (Einstieg → Vertiefung → Ziel)?
|
||||
3. Sind Sprünge zwischen benachbarten Schritten zu groß?
|
||||
4. Sind Brücken-Übungen sinnvoll oder überflüssig?
|
||||
5. Fehlen wichtige Zwischenschritte (Kraft, Geschwindigkeit, Anwendung, Perfektion)?
|
||||
6. Gibt es Schritte ohne Bezug zum Hauptthema (z. B. reine Kraftübungen bei einer Technik)?
|
||||
|
||||
Wenn die Reihenfolge verbessert werden sollte: ordered_step_indices = Permutation der aktuellen 0-basierten Schritt-Indizes (beste didaktische Reihenfolge).
|
||||
Nur Indizes aus dem steps_json verwenden — Länge muss exakt der Schrittzahl entsprechen.
|
||||
|
||||
Wenn wichtige Zwischenschritte fehlen oder Schritte themenfremd sind: suggested_new_exercises mit konkreten Übungs-Ideen (Titel + Kurzskizze), jeweils mit insert_after_step_index (0-basiert: nach welchem Schritt einfügen).
|
||||
|
||||
Antworte NUR mit JSON:
|
||||
{
|
||||
"overall_ok": true,
|
||||
"quality_score": 0.85,
|
||||
"topic_coverage": "Kurz: wie gut das Hauptthema abgedeckt ist",
|
||||
"ordered_step_indices": [0, 1, 2, 3],
|
||||
"issues": ["…"],
|
||||
"sequence_notes": ["…"],
|
||||
"recommendations": ["…"],
|
||||
"suggested_new_exercises": [
|
||||
{
|
||||
"title_hint": "Mae Geri Kraftentwicklung am Sandsack",
|
||||
"sketch": "Gezielte Kraft- und Schnelligkeitsentwicklung für Mae Geri …",
|
||||
"phase": "vertiefung",
|
||||
"insert_after_step_index": 2,
|
||||
"rationale": "Schließt Lücke zwischen Grundlagen und Gleichgewichtstritt"
|
||||
}
|
||||
]
|
||||
}$t$,
|
||||
default_template = $t$Du bist Assistent für Kampfsport-Trainer und prüfst einen vorgeschlagenen Übungspfad.
|
||||
|
||||
Ziel-Anfrage: {{goal_query}}
|
||||
Semantic Brief: {{semantic_brief_json}}
|
||||
Schritte (JSON): {{steps_json}}
|
||||
Erkannte Lücken: {{gaps_json}}
|
||||
Eingefügte Brücken: {{bridge_inserts_json}}
|
||||
|
||||
Prüfe:
|
||||
1. Deckt der Pfad das Hauptthema der Anfrage ab (nicht nur Oberbegriffe)?
|
||||
2. Ist die Reihenfolge didaktisch sinnvoll (Einstieg → Vertiefung → Ziel)?
|
||||
3. Sind Sprünge zwischen benachbarten Schritten zu groß?
|
||||
4. Sind Brücken-Übungen sinnvoll oder überflüssig?
|
||||
5. Fehlen wichtige Zwischenschritte (Kraft, Geschwindigkeit, Anwendung, Perfektion)?
|
||||
6. Gibt es Schritte ohne Bezug zum Hauptthema (z. B. reine Kraftübungen bei einer Technik)?
|
||||
|
||||
Wenn die Reihenfolge verbessert werden sollte: ordered_step_indices = Permutation der aktuellen 0-basierten Schritt-Indizes (beste didaktische Reihenfolge).
|
||||
Nur Indizes aus dem steps_json verwenden — Länge muss exakt der Schrittzahl entsprechen.
|
||||
|
||||
Wenn wichtige Zwischenschritte fehlen oder Schritte themenfremd sind: suggested_new_exercises mit konkreten Übungs-Ideen (Titel + Kurzskizze), jeweils mit insert_after_step_index (0-basiert: nach welchem Schritt einfügen).
|
||||
|
||||
Antworte NUR mit JSON:
|
||||
{
|
||||
"overall_ok": true,
|
||||
"quality_score": 0.85,
|
||||
"topic_coverage": "Kurz: wie gut das Hauptthema abgedeckt ist",
|
||||
"ordered_step_indices": [0, 1, 2, 3],
|
||||
"issues": ["…"],
|
||||
"sequence_notes": ["…"],
|
||||
"recommendations": ["…"],
|
||||
"suggested_new_exercises": [
|
||||
{
|
||||
"title_hint": "Mae Geri Kraftentwicklung am Sandsack",
|
||||
"sketch": "Gezielte Kraft- und Schnelligkeitsentwicklung für Mae Geri …",
|
||||
"phase": "vertiefung",
|
||||
"insert_after_step_index": 2,
|
||||
"rationale": "Schließt Lücke zwischen Grundlagen und Gleichgewichtstritt"
|
||||
}
|
||||
]
|
||||
}$t$,
|
||||
output_schema = '{"type":"object","required":["overall_ok"],"properties":{"overall_ok":{"type":"boolean"},"quality_score":{"type":"number"},"issues":{"type":"array"},"sequence_notes":{"type":"array"},"recommendations":{"type":"array"},"ordered_step_indices":{"type":"array"},"suggested_new_exercises":{"type":"array"}}}'::jsonb
|
||||
WHERE slug = 'planning_exercise_path_qa';
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
-- Migration 078: Planungs-KI Phase F — Progressions-Roadmap Prompts (Zielanalyse + Roadmap)
|
||||
|
||||
INSERT INTO ai_prompts (
|
||||
slug, display_name, description, template,
|
||||
category, output_format, output_schema, is_system_default, default_template, active, sort_order
|
||||
)
|
||||
SELECT
|
||||
'planning_progression_goal_analysis',
|
||||
'Progressions-Roadmap Zielanalyse',
|
||||
'Phase A: Ist-/Soll-Zustand und Erfolgskriterien für einen Progressionsgraphen (ohne Gruppenkontext).',
|
||||
$t$Du bist Assistent für Kampfsport-Trainer und analysierst eine Anfrage für einen Progressionsgraphen.
|
||||
|
||||
Anfrage: {{goal_query}}
|
||||
Semantic Brief: {{semantic_brief_json}}
|
||||
|
||||
Wichtig: Keine Gruppenanalyse — nur didaktischer Pfad für die Technik/das Thema.
|
||||
|
||||
Antworte NUR mit JSON:
|
||||
{
|
||||
"primary_topic": "Mae Geri",
|
||||
"start_assumption": "Welche Voraussetzungen werden für den Einstieg angenommen",
|
||||
"target_state": "Konkreter Zielzustand der Progression",
|
||||
"success_criteria": ["messbare Kriterien"],
|
||||
"constraints": { "partner_required": false }
|
||||
}$t$,
|
||||
'training',
|
||||
'json',
|
||||
'{"type":"object","properties":{"primary_topic":{"type":"string"},"target_state":{"type":"string"},"success_criteria":{"type":"array"}}}'::jsonb,
|
||||
true,
|
||||
NULL,
|
||||
true,
|
||||
14
|
||||
WHERE NOT EXISTS (SELECT 1 FROM ai_prompts WHERE slug = 'planning_progression_goal_analysis');
|
||||
|
||||
INSERT INTO ai_prompts (
|
||||
slug, display_name, description, template,
|
||||
category, output_format, output_schema, is_system_default, default_template, active, sort_order
|
||||
)
|
||||
SELECT
|
||||
'planning_progression_roadmap',
|
||||
'Progressions-Roadmap Major Steps',
|
||||
'Phase B: 8–12 micro_objectives, Konsolidierung auf N major_steps.',
|
||||
$t$Du bist Assistent für Kampfsport-Trainer und erstellst eine didaktische Roadmap für einen Progressionsgraphen.
|
||||
|
||||
Anfrage: {{goal_query}}
|
||||
Zielanalyse: {{goal_analysis_json}}
|
||||
Semantic Brief: {{semantic_brief_json}}
|
||||
Anzahl Major Steps (N): {{max_steps}}
|
||||
|
||||
Erzeuge zuerst 8–12 micro_objectives (phase, title, weight, depends_on), dann konsolidiere auf genau N major_steps.
|
||||
Phasen: einstieg, grundlage, vertiefung, anwendung, perfektion — in sinnvoller Reihenfolge (Grundlagen vor Perfektion).
|
||||
|
||||
Antworte NUR mit JSON:
|
||||
{
|
||||
"micro_objectives": [
|
||||
{ "id": "m1", "phase": "grundlage", "title": "…", "weight": 0.9, "depends_on": [] }
|
||||
],
|
||||
"major_steps": [
|
||||
{ "index": 0, "phase": "grundlage", "learning_goal": "…", "consolidates": ["m1","m2"], "rationale": "…" }
|
||||
],
|
||||
"consolidation_notes": ["…"]
|
||||
}$t$,
|
||||
'training',
|
||||
'json',
|
||||
'{"type":"object","properties":{"micro_objectives":{"type":"array"},"major_steps":{"type":"array"},"consolidation_notes":{"type":"array"}}}'::jsonb,
|
||||
true,
|
||||
NULL,
|
||||
true,
|
||||
15
|
||||
WHERE NOT EXISTS (SELECT 1 FROM ai_prompts WHERE slug = 'planning_progression_roadmap');
|
||||
|
||||
UPDATE ai_prompts SET default_template = template
|
||||
WHERE slug IN ('planning_progression_goal_analysis', 'planning_progression_roadmap')
|
||||
AND (default_template IS NULL OR TRIM(default_template) = '');
|
||||
286
backend/migrations/078_club_features_and_plans.sql
Normal file
286
backend/migrations/078_club_features_and_plans.sql
Normal file
|
|
@ -0,0 +1,286 @@
|
|||
-- Migration 078: Vereins-Feature-Registry (Mitai-v9c-Pattern) + club_plans/subscriptions
|
||||
-- Spez: .claude/docs/technical/CLUB_MEMBERSHIP_AND_FEATURES.v1.md (M1)
|
||||
-- Legacy 001 (SERIAL features, profile tier_limits) wird archiviert, nicht gelöscht.
|
||||
|
||||
-- ── 1. Legacy-Tabellen archivieren (nur alte Struktur) ─────────────────────
|
||||
DO $migration$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM information_schema.tables
|
||||
WHERE table_schema = 'public' AND table_name = 'features'
|
||||
) AND EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = 'public' AND table_name = 'features' AND column_name = 'name'
|
||||
) AND NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = 'public' AND table_name = 'features' AND column_name = 'limit_type'
|
||||
) THEN
|
||||
-- Nach abgebrochenem Erstversuch kann features_legacy_001 schon existieren
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM information_schema.tables
|
||||
WHERE table_schema = 'public' AND table_name = 'features_legacy_001'
|
||||
) THEN
|
||||
DROP TABLE features;
|
||||
ELSE
|
||||
ALTER TABLE features RENAME TO features_legacy_001;
|
||||
END IF;
|
||||
END IF;
|
||||
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM information_schema.tables
|
||||
WHERE table_schema = 'public' AND table_name = 'tier_limits'
|
||||
) AND EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = 'public' AND table_name = 'tier_limits' AND column_name = 'tier'
|
||||
) THEN
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM information_schema.tables
|
||||
WHERE table_schema = 'public' AND table_name = 'tier_limits_legacy_001'
|
||||
) THEN
|
||||
DROP TABLE tier_limits;
|
||||
ELSE
|
||||
ALTER TABLE tier_limits RENAME TO tier_limits_legacy_001;
|
||||
END IF;
|
||||
END IF;
|
||||
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM information_schema.tables
|
||||
WHERE table_schema = 'public' AND table_name = 'user_feature_usage'
|
||||
) AND EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = 'public' AND table_name = 'user_feature_usage' AND column_name = 'profile_id'
|
||||
) THEN
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM information_schema.tables
|
||||
WHERE table_schema = 'public' AND table_name = 'user_feature_usage_legacy_001'
|
||||
) THEN
|
||||
DROP TABLE user_feature_usage;
|
||||
ELSE
|
||||
ALTER TABLE user_feature_usage RENAME TO user_feature_usage_legacy_001;
|
||||
END IF;
|
||||
END IF;
|
||||
END
|
||||
$migration$;
|
||||
|
||||
-- ── 2. Feature-Registry (TEXT-PK, app=shinkan) ────────────────────────────
|
||||
CREATE TABLE IF NOT EXISTS features (
|
||||
id TEXT PRIMARY KEY,
|
||||
app TEXT NOT NULL DEFAULT 'shinkan',
|
||||
name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
category TEXT NOT NULL DEFAULT 'content',
|
||||
limit_type TEXT NOT NULL DEFAULT 'count'
|
||||
CHECK (limit_type IN ('count', 'boolean')),
|
||||
reset_period TEXT NOT NULL DEFAULT 'never'
|
||||
CHECK (reset_period IN ('never', 'daily', 'monthly')),
|
||||
default_limit INTEGER,
|
||||
enforcement_subject TEXT NOT NULL DEFAULT 'club'
|
||||
CHECK (enforcement_subject IN ('club', 'profile', 'portal')),
|
||||
active BOOLEAN NOT NULL DEFAULT true,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_features_app ON features(app) WHERE active = true;
|
||||
|
||||
-- ── 3. Vereins-Produkte ─────────────────────────────────────────────────────
|
||||
CREATE TABLE IF NOT EXISTS club_plans (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
price_monthly_cents INTEGER,
|
||||
price_yearly_cents INTEGER,
|
||||
stripe_price_id_monthly TEXT,
|
||||
stripe_price_id_yearly TEXT,
|
||||
active BOOLEAN NOT NULL DEFAULT true,
|
||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS club_plan_limits (
|
||||
id SERIAL PRIMARY KEY,
|
||||
plan_id TEXT NOT NULL REFERENCES club_plans(id) ON DELETE CASCADE,
|
||||
feature_id TEXT NOT NULL REFERENCES features(id) ON DELETE CASCADE,
|
||||
limit_value INTEGER,
|
||||
UNIQUE (plan_id, feature_id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_club_plan_limits_plan ON club_plan_limits(plan_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS club_subscriptions (
|
||||
id SERIAL PRIMARY KEY,
|
||||
club_id INT NOT NULL REFERENCES clubs(id) ON DELETE CASCADE,
|
||||
plan_id TEXT NOT NULL REFERENCES club_plans(id),
|
||||
status TEXT NOT NULL DEFAULT 'active'
|
||||
CHECK (status IN ('active', 'trial', 'past_due', 'cancelled')),
|
||||
started_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
ends_at TIMESTAMPTZ,
|
||||
trial_ends_at TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
UNIQUE (club_id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_club_subscriptions_plan ON club_subscriptions(plan_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS club_feature_overrides (
|
||||
id SERIAL PRIMARY KEY,
|
||||
club_id INT NOT NULL REFERENCES clubs(id) ON DELETE CASCADE,
|
||||
feature_id TEXT NOT NULL REFERENCES features(id) ON DELETE CASCADE,
|
||||
limit_value INTEGER NOT NULL,
|
||||
reason TEXT,
|
||||
set_by_profile_id INT REFERENCES profiles(id) ON DELETE SET NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
UNIQUE (club_id, feature_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS club_access_grants (
|
||||
id SERIAL PRIMARY KEY,
|
||||
club_id INT NOT NULL REFERENCES clubs(id) ON DELETE CASCADE,
|
||||
plan_id TEXT REFERENCES club_plans(id) ON DELETE SET NULL,
|
||||
feature_id TEXT REFERENCES features(id) ON DELETE SET NULL,
|
||||
grant_limit INTEGER,
|
||||
starts_at TIMESTAMPTZ NOT NULL,
|
||||
ends_at TIMESTAMPTZ NOT NULL,
|
||||
reason TEXT,
|
||||
created_by_profile_id INT REFERENCES profiles(id) ON DELETE SET NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_club_access_grants_club ON club_access_grants(club_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_club_access_grants_window ON club_access_grants(club_id, starts_at, ends_at);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS club_feature_usage (
|
||||
id SERIAL PRIMARY KEY,
|
||||
club_id INT NOT NULL REFERENCES clubs(id) ON DELETE CASCADE,
|
||||
feature_id TEXT NOT NULL REFERENCES features(id) ON DELETE CASCADE,
|
||||
usage_count INTEGER NOT NULL DEFAULT 0,
|
||||
reset_at TIMESTAMPTZ,
|
||||
last_used_at TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
UNIQUE (club_id, feature_id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_club_feature_usage_club ON club_feature_usage(club_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS club_feature_usage_events (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
club_id INT NOT NULL REFERENCES clubs(id) ON DELETE CASCADE,
|
||||
feature_id TEXT NOT NULL REFERENCES features(id) ON DELETE CASCADE,
|
||||
profile_id INT REFERENCES profiles(id) ON DELETE SET NULL,
|
||||
action TEXT NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_club_feature_usage_events_club
|
||||
ON club_feature_usage_events(club_id, created_at DESC);
|
||||
|
||||
-- ── 4. Seed: Features ─────────────────────────────────────────────────────
|
||||
INSERT INTO features (id, app, name, description, category, limit_type, reset_period, default_limit, enforcement_subject)
|
||||
VALUES
|
||||
('exercises', 'shinkan', 'Übungen', 'Anzahl Übungen im Verein (Bestand)', 'content', 'count', 'never', 100, 'club'),
|
||||
('exercise_media', 'shinkan', 'Medien-Uploads', 'Medien-Uploads pro Monat', 'content', 'count', 'monthly', 20, 'club'),
|
||||
('training_units', 'shinkan', 'Trainingseinheiten', 'Trainingseinheiten pro Monat', 'planning', 'count', 'monthly', 40, 'club'),
|
||||
('training_programs', 'shinkan', 'Trainingsprogramme', 'Module und Rahmenprogramme (Bestand)', 'planning', 'count', 'never', 5, 'club'),
|
||||
('training_groups', 'shinkan', 'Trainingsgruppen', 'Anzahl Trainingsgruppen', 'org', 'count', 'never', 10, 'club'),
|
||||
('active_members', 'shinkan', 'Aktive Mitglieder', 'Anzahl aktiver Vereinsmitglieder', 'org', 'count', 'never', 25, 'club'),
|
||||
('ai_calls', 'shinkan', 'KI-Aufrufe', 'KI-Aufrufe pro Monat (Suggest, Regenerate, Planung)', 'ai', 'count', 'monthly', 0, 'club'),
|
||||
('ai_pipeline', 'shinkan', 'KI-Pipeline', 'Erweiterte KI-Batch-Pipelines', 'ai', 'boolean', 'never', 0, 'club'),
|
||||
('wiki_import', 'shinkan', 'Wiki-Import', 'MediaWiki-Import (Plattform)', 'integration', 'boolean', 'never', 0, 'portal'),
|
||||
('data_export', 'shinkan', 'Daten-Export', 'Export-Funktionen', 'integration', 'boolean', 'never', 0, 'club')
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- ── 5. Seed: Pläne ──────────────────────────────────────────────────────────
|
||||
INSERT INTO club_plans (id, name, description, sort_order, active)
|
||||
VALUES
|
||||
('free', 'Free', 'Einstieg für Vereine', 0, true),
|
||||
('verein_starter', 'Verein Starter', 'Erweiterte Kontingente', 10, true),
|
||||
('verein_pro', 'Verein Pro', 'Hohe Limits und KI-Kontingent', 20, true),
|
||||
('pilot', 'Pilot', 'Pilotverein mit großzügigen Limits', 5, true)
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- Plan-Limits: free
|
||||
INSERT INTO club_plan_limits (plan_id, feature_id, limit_value)
|
||||
SELECT 'free', f.id,
|
||||
CASE f.id
|
||||
WHEN 'exercises' THEN 100
|
||||
WHEN 'exercise_media' THEN 20
|
||||
WHEN 'training_units' THEN 40
|
||||
WHEN 'training_programs' THEN 5
|
||||
WHEN 'training_groups' THEN 10
|
||||
WHEN 'active_members' THEN 25
|
||||
WHEN 'ai_calls' THEN 0
|
||||
WHEN 'ai_pipeline' THEN 0
|
||||
WHEN 'wiki_import' THEN 0
|
||||
WHEN 'data_export' THEN 0
|
||||
END
|
||||
FROM features f
|
||||
WHERE f.app = 'shinkan'
|
||||
ON CONFLICT (plan_id, feature_id) DO NOTHING;
|
||||
|
||||
-- Plan-Limits: verein_starter
|
||||
INSERT INTO club_plan_limits (plan_id, feature_id, limit_value)
|
||||
SELECT 'verein_starter', f.id,
|
||||
CASE f.id
|
||||
WHEN 'exercises' THEN 500
|
||||
WHEN 'exercise_media' THEN 80
|
||||
WHEN 'training_units' THEN 200
|
||||
WHEN 'training_programs' THEN 30
|
||||
WHEN 'training_groups' THEN 30
|
||||
WHEN 'active_members' THEN 80
|
||||
WHEN 'ai_calls' THEN 30
|
||||
WHEN 'ai_pipeline' THEN 0
|
||||
WHEN 'wiki_import' THEN 0
|
||||
WHEN 'data_export' THEN 1
|
||||
END
|
||||
FROM features f
|
||||
WHERE f.app = 'shinkan'
|
||||
ON CONFLICT (plan_id, feature_id) DO NOTHING;
|
||||
|
||||
-- Plan-Limits: verein_pro (NULL = unbegrenzt wo sinnvoll)
|
||||
INSERT INTO club_plan_limits (plan_id, feature_id, limit_value)
|
||||
SELECT 'verein_pro', f.id,
|
||||
CASE f.id
|
||||
WHEN 'exercises' THEN NULL
|
||||
WHEN 'exercise_media' THEN 300
|
||||
WHEN 'training_units' THEN NULL
|
||||
WHEN 'training_programs' THEN NULL
|
||||
WHEN 'training_groups' THEN NULL
|
||||
WHEN 'active_members' THEN NULL
|
||||
WHEN 'ai_calls' THEN 200
|
||||
WHEN 'ai_pipeline' THEN 1
|
||||
WHEN 'wiki_import' THEN 0
|
||||
WHEN 'data_export' THEN 1
|
||||
END
|
||||
FROM features f
|
||||
WHERE f.app = 'shinkan'
|
||||
ON CONFLICT (plan_id, feature_id) DO NOTHING;
|
||||
|
||||
-- Plan-Limits: pilot
|
||||
INSERT INTO club_plan_limits (plan_id, feature_id, limit_value)
|
||||
SELECT 'pilot', f.id,
|
||||
CASE f.id
|
||||
WHEN 'exercises' THEN NULL
|
||||
WHEN 'exercise_media' THEN NULL
|
||||
WHEN 'training_units' THEN NULL
|
||||
WHEN 'training_programs' THEN NULL
|
||||
WHEN 'training_groups' THEN NULL
|
||||
WHEN 'active_members' THEN NULL
|
||||
WHEN 'ai_calls' THEN 100
|
||||
WHEN 'ai_pipeline' THEN 1
|
||||
WHEN 'wiki_import' THEN 0
|
||||
WHEN 'data_export' THEN 1
|
||||
END
|
||||
FROM features f
|
||||
WHERE f.app = 'shinkan'
|
||||
ON CONFLICT (plan_id, feature_id) DO NOTHING;
|
||||
|
||||
-- ── 6. Backfill: bestehende Vereine → Plan free ───────────────────────────
|
||||
INSERT INTO club_subscriptions (club_id, plan_id, status)
|
||||
SELECT c.id, 'free', 'active'
|
||||
FROM clubs c
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM club_subscriptions cs WHERE cs.club_id = c.id
|
||||
);
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
-- Migration 079: Planungs-KI Phase F — Stufenspezifikation (Prompt in ai_prompts, nicht im Code)
|
||||
|
||||
INSERT INTO ai_prompts (
|
||||
slug, display_name, description, template,
|
||||
category, output_format, output_schema, is_system_default, default_template, active, sort_order
|
||||
)
|
||||
SELECT
|
||||
'planning_progression_stage_spec',
|
||||
'Progressions-Roadmap Stufenspezifikation',
|
||||
'Phase C: Belastungsprofil, Übungstyp und Erfolgskriterien je Major Step.',
|
||||
$t$Du bist Assistent für Kampfsport-Trainer und spezifizierst didaktische Stufen eines Progressionsgraphen.
|
||||
|
||||
Anfrage: {{goal_query}}
|
||||
Zielanalyse: {{goal_analysis_json}}
|
||||
Major Steps: {{major_steps_json}}
|
||||
|
||||
Für jeden Major Step: messbares Lernziel, load_profile (z. B. koordination, präzision, kraft), exercise_type (kihon_einzel, partner_drill, kombination, kraft_auxiliary), success_criteria, anti_patterns (z. B. reine Kraft ohne Technikbezug).
|
||||
|
||||
Antworte NUR mit JSON:
|
||||
{
|
||||
"stage_specs": [
|
||||
{
|
||||
"major_step_index": 0,
|
||||
"learning_goal": "…",
|
||||
"load_profile": ["koordination", "gleichgewicht"],
|
||||
"exercise_type": "kihon_einzel",
|
||||
"success_criteria": ["…"],
|
||||
"anti_patterns": ["…"]
|
||||
}
|
||||
]
|
||||
}$t$,
|
||||
'training',
|
||||
'json',
|
||||
'{"type":"object","properties":{"stage_specs":{"type":"array"}}}'::jsonb,
|
||||
true,
|
||||
NULL,
|
||||
true,
|
||||
16
|
||||
WHERE NOT EXISTS (SELECT 1 FROM ai_prompts WHERE slug = 'planning_progression_stage_spec');
|
||||
|
||||
UPDATE ai_prompts SET default_template = template
|
||||
WHERE slug = 'planning_progression_stage_spec'
|
||||
AND (default_template IS NULL OR TRIM(default_template) = '');
|
||||
225
backend/migrations/079_capabilities.sql
Normal file
225
backend/migrations/079_capabilities.sql
Normal file
|
|
@ -0,0 +1,225 @@
|
|||
-- Migration 079: Capability-Registry + Rollen-Grants (M3 / CAPABILITY_CATALOG.v1.md C1)
|
||||
-- Account-Gates und Enforcement in Python (account_lifecycle.py, capabilities.py).
|
||||
-- Voraussetzung: Migration 078 (features.id TEXT). Kein FK auf features — vermeidet
|
||||
-- Startup-Abbruch wenn 078 noch aussteht oder features-Schema driftet (001 vs v9c).
|
||||
|
||||
DO $migration$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = 'public' AND table_name = 'features' AND column_name = 'limit_type'
|
||||
) THEN
|
||||
RAISE EXCEPTION
|
||||
'Migration 079: features-Tabelle nicht v9c (limit_type fehlt). Zuerst 078_club_features_and_plans anwenden.';
|
||||
END IF;
|
||||
END
|
||||
$migration$;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS capabilities (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
domain TEXT NOT NULL,
|
||||
min_account_state TEXT NOT NULL DEFAULT 'active_member'
|
||||
CHECK (min_account_state IN (
|
||||
'unverified', 'verified_pending_club', 'active_member', 'platform_admin'
|
||||
)),
|
||||
linked_feature_id TEXT,
|
||||
active BOOLEAN NOT NULL DEFAULT true,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_capabilities_domain ON capabilities(domain) WHERE active = true;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS club_role_capability_grants (
|
||||
role_code TEXT NOT NULL,
|
||||
capability_id TEXT NOT NULL REFERENCES capabilities(id) ON DELETE CASCADE,
|
||||
PRIMARY KEY (role_code, capability_id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_club_role_cap_grants_cap ON club_role_capability_grants(capability_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS portal_role_capability_grants (
|
||||
portal_role TEXT NOT NULL,
|
||||
capability_id TEXT NOT NULL REFERENCES capabilities(id) ON DELETE CASCADE,
|
||||
PRIMARY KEY (portal_role, capability_id)
|
||||
);
|
||||
|
||||
-- ── Seed: Capabilities (v1 Katalog §5) ───────────────────────────────────────
|
||||
INSERT INTO capabilities (id, name, domain, min_account_state, linked_feature_id) VALUES
|
||||
('account.settings.read', 'Einstellungen lesen', 'account', 'unverified', NULL),
|
||||
('account.settings.update', 'Einstellungen ändern', 'account', 'unverified', NULL),
|
||||
('account.password.change', 'Passwort ändern', 'account', 'unverified', NULL),
|
||||
('account.resend_verification', 'Verifizierung erneut senden', 'account', 'unverified', NULL),
|
||||
('club.directory.read', 'Vereinsverzeichnis', 'club', 'verified_pending_club', NULL),
|
||||
('club.join_request.create', 'Vereinsbeitritt beantragen', 'club', 'verified_pending_club', NULL),
|
||||
('club.join_request.withdraw', 'Beitrittsantrag zurückziehen', 'club', 'verified_pending_club', NULL),
|
||||
('club.join_request.read_own', 'Eigene Beitrittsanträge', 'club', 'verified_pending_club', NULL),
|
||||
('org.club.read', 'Vereine lesen', 'org', 'active_member', NULL),
|
||||
('org.club.create', 'Verein anlegen', 'org', 'platform_admin', NULL),
|
||||
('org.club.update', 'Verein bearbeiten', 'org', 'active_member', NULL),
|
||||
('org.club.delete', 'Verein löschen', 'org', 'platform_admin', NULL),
|
||||
('org.structure.manage', 'Vereinsstruktur verwalten', 'org', 'active_member', 'training_groups'),
|
||||
('org.members.read', 'Mitgliederliste', 'org', 'active_member', NULL),
|
||||
('org.members.manage', 'Mitglieder verwalten', 'org', 'active_member', 'active_members'),
|
||||
('org.members.directory', 'Mitglieder-Verzeichnis', 'org', 'active_member', NULL),
|
||||
('org.join_request.review', 'Beitrittsanträge prüfen', 'org', 'active_member', NULL),
|
||||
('org.inbox.read', 'Posteingang', 'org', 'active_member', NULL),
|
||||
('exercises.read', 'Übungen lesen', 'exercises', 'active_member', NULL),
|
||||
('exercises.create', 'Übung anlegen', 'exercises', 'active_member', 'exercises'),
|
||||
('exercises.update', 'Übung bearbeiten', 'exercises', 'active_member', NULL),
|
||||
('exercises.delete', 'Übung löschen', 'exercises', 'active_member', NULL),
|
||||
('exercises.bulk_metadata', 'Übungen Stapel-Metadaten', 'exercises', 'active_member', NULL),
|
||||
('exercises.ai.suggest', 'KI-Vorschlag Übung', 'exercises', 'active_member', 'ai_calls'),
|
||||
('exercises.ai.regenerate', 'KI neu generieren', 'exercises', 'active_member', 'ai_calls'),
|
||||
('exercises.media.read', 'Übungsmedien lesen', 'exercises', 'active_member', NULL),
|
||||
('exercises.media.upload', 'Übungsmedien hochladen', 'exercises', 'active_member', 'exercise_media'),
|
||||
('exercises.variants.manage', 'Übungsvarianten', 'exercises', 'active_member', NULL),
|
||||
('media.library.read', 'Medienbibliothek lesen', 'media', 'active_member', NULL),
|
||||
('media.library.upload', 'Medienbibliothek Upload', 'media', 'active_member', 'exercise_media'),
|
||||
('media.library.update', 'Medienbibliothek bearbeiten', 'media', 'active_member', NULL),
|
||||
('media.library.lifecycle', 'Medien-Lifecycle', 'media', 'active_member', NULL),
|
||||
('media.rights.declare', 'Medienrechte erklären', 'media', 'active_member', NULL),
|
||||
('media.admin.rights_review', 'Medienrechte Review (Plattform)', 'media', 'platform_admin', NULL),
|
||||
('modules.read', 'Trainingsmodule lesen', 'modules', 'active_member', NULL),
|
||||
('modules.create', 'Trainingsmodul anlegen', 'modules', 'active_member', 'training_programs'),
|
||||
('modules.update', 'Trainingsmodul bearbeiten', 'modules', 'active_member', NULL),
|
||||
('modules.delete', 'Trainingsmodul löschen', 'modules', 'active_member', NULL),
|
||||
('framework.read', 'Rahmenprogramme lesen', 'framework', 'active_member', NULL),
|
||||
('framework.create', 'Rahmenprogramm anlegen', 'framework', 'active_member', 'training_programs'),
|
||||
('framework.update', 'Rahmenprogramm bearbeiten', 'framework', 'active_member', NULL),
|
||||
('framework.delete', 'Rahmenprogramm löschen', 'framework', 'active_member', NULL),
|
||||
('plan_templates.read', 'Planungsvorlagen lesen', 'planning', 'active_member', NULL),
|
||||
('plan_templates.manage', 'Planungsvorlagen verwalten', 'planning', 'active_member', NULL),
|
||||
('progression.read', 'Progressionspfade lesen', 'progression', 'active_member', NULL),
|
||||
('progression.manage', 'Progressionspfade verwalten', 'progression', 'active_member', NULL),
|
||||
('planning.calendar.read', 'Planungskalender lesen', 'planning', 'active_member', NULL),
|
||||
('planning.units.create', 'Trainingseinheit anlegen', 'planning', 'active_member', 'training_units'),
|
||||
('planning.units.update', 'Trainingseinheit bearbeiten', 'planning', 'active_member', NULL),
|
||||
('planning.units.delete', 'Trainingseinheit löschen', 'planning', 'active_member', NULL),
|
||||
('planning.units.run', 'Training durchführen', 'planning', 'active_member', NULL),
|
||||
('planning.coach.execute', 'Coach ausführen', 'planning', 'active_member', NULL),
|
||||
('planning.ai.suggest', 'Planungs-KI Suggest', 'planning', 'active_member', 'ai_calls'),
|
||||
('planning.ai.progression_path', 'Planungs-KI Progressionspfad', 'planning', 'active_member', 'ai_calls'),
|
||||
('skills.catalog.read', 'Fähigkeitenkatalog', 'skills', 'active_member', NULL),
|
||||
('skills.discovery.read', 'Fähigkeiten-Discovery', 'skills', 'active_member', NULL),
|
||||
('skill_profiles.read', 'Skill-Profile lesen', 'skills', 'active_member', NULL),
|
||||
('governance.content_report.create', 'Inhalt melden', 'governance', 'active_member', NULL),
|
||||
('governance.content_report.review', 'Meldungen prüfen', 'governance', 'active_member', NULL),
|
||||
('platform.admin.access', 'Plattform-Admin-Bereich', 'platform', 'platform_admin', NULL),
|
||||
('platform.users.manage', 'Nutzer verwalten', 'platform', 'platform_admin', NULL),
|
||||
('platform.catalogs.manage', 'Kataloge verwalten', 'platform', 'platform_admin', NULL),
|
||||
('platform.maturity_models.manage', 'Reifegradmodelle', 'platform', 'platform_admin', NULL),
|
||||
('platform.wiki_import.execute', 'Wiki-Import', 'platform', 'platform_admin', 'wiki_import'),
|
||||
('platform.ai_prompts.manage', 'KI-Prompts verwalten', 'platform', 'platform_admin', NULL),
|
||||
('platform.exercise_enrichment.execute', 'Übungs-Anreicherung KI', 'platform', 'platform_admin', 'ai_calls'),
|
||||
('platform.user_content.moderate', 'Nutzer-Inhalte moderieren', 'platform', 'platform_admin', NULL),
|
||||
('platform.legal_documents.manage', 'Rechtstexte verwalten', 'platform', 'platform_admin', NULL),
|
||||
('platform.media_storage.manage', 'Medienspeicher verwalten', 'platform', 'platform_admin', NULL),
|
||||
('platform.club_creation.approve', 'Vereinsgründung freigeben', 'platform', 'platform_admin', NULL)
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- ── Vereinsrollen-Grants (§6 — nur eingeschränkte Capabilities) ─────────────
|
||||
-- Konvention: keine Grant-Zeile = alle aktiven Mitglieder (min_account_state reicht).
|
||||
|
||||
INSERT INTO club_role_capability_grants (role_code, capability_id)
|
||||
SELECT r.role_code, c.id
|
||||
FROM (VALUES
|
||||
('club_admin', 'org.structure.manage'),
|
||||
('division_lead', 'org.structure.manage'),
|
||||
('club_admin', 'org.members.manage'),
|
||||
('club_admin', 'org.join_request.review'),
|
||||
('club_admin', 'org.inbox.read'),
|
||||
('club_admin', 'exercises.create'),
|
||||
('trainer', 'exercises.create'),
|
||||
('content_editor', 'exercises.create'),
|
||||
('division_lead', 'exercises.create'),
|
||||
('club_admin', 'exercises.update'),
|
||||
('trainer', 'exercises.update'),
|
||||
('content_editor', 'exercises.update'),
|
||||
('division_lead', 'exercises.update'),
|
||||
('club_admin', 'exercises.delete'),
|
||||
('club_admin', 'exercises.bulk_metadata'),
|
||||
('content_editor', 'exercises.bulk_metadata'),
|
||||
('club_admin', 'exercises.ai.suggest'),
|
||||
('trainer', 'exercises.ai.suggest'),
|
||||
('content_editor', 'exercises.ai.suggest'),
|
||||
('division_lead', 'exercises.ai.suggest'),
|
||||
('club_admin', 'exercises.ai.regenerate'),
|
||||
('trainer', 'exercises.ai.regenerate'),
|
||||
('content_editor', 'exercises.ai.regenerate'),
|
||||
('division_lead', 'exercises.ai.regenerate'),
|
||||
('club_admin', 'exercises.media.upload'),
|
||||
('trainer', 'exercises.media.upload'),
|
||||
('content_editor', 'exercises.media.upload'),
|
||||
('club_admin', 'exercises.variants.manage'),
|
||||
('trainer', 'exercises.variants.manage'),
|
||||
('content_editor', 'exercises.variants.manage'),
|
||||
('club_admin', 'media.library.upload'),
|
||||
('trainer', 'media.library.upload'),
|
||||
('content_editor', 'media.library.upload'),
|
||||
('club_admin', 'media.library.update'),
|
||||
('trainer', 'media.library.update'),
|
||||
('content_editor', 'media.library.update'),
|
||||
('club_admin', 'media.library.lifecycle'),
|
||||
('trainer', 'media.library.lifecycle'),
|
||||
('club_admin', 'media.rights.declare'),
|
||||
('trainer', 'media.rights.declare'),
|
||||
('club_admin', 'modules.create'),
|
||||
('trainer', 'modules.create'),
|
||||
('content_editor', 'modules.create'),
|
||||
('club_admin', 'modules.update'),
|
||||
('trainer', 'modules.update'),
|
||||
('content_editor', 'modules.update'),
|
||||
('club_admin', 'modules.delete'),
|
||||
('club_admin', 'framework.create'),
|
||||
('trainer', 'framework.create'),
|
||||
('club_admin', 'framework.update'),
|
||||
('trainer', 'framework.update'),
|
||||
('club_admin', 'framework.delete'),
|
||||
('club_admin', 'plan_templates.manage'),
|
||||
('trainer', 'plan_templates.manage'),
|
||||
('club_admin', 'progression.manage'),
|
||||
('trainer', 'progression.manage'),
|
||||
('content_editor', 'progression.manage'),
|
||||
('club_admin', 'planning.units.create'),
|
||||
('trainer', 'planning.units.create'),
|
||||
('division_lead', 'planning.units.create'),
|
||||
('club_admin', 'planning.units.update'),
|
||||
('trainer', 'planning.units.update'),
|
||||
('division_lead', 'planning.units.update'),
|
||||
('club_admin', 'planning.units.delete'),
|
||||
('trainer', 'planning.units.delete'),
|
||||
('club_admin', 'planning.units.run'),
|
||||
('trainer', 'planning.units.run'),
|
||||
('division_lead', 'planning.units.run'),
|
||||
('club_admin', 'planning.coach.execute'),
|
||||
('trainer', 'planning.coach.execute'),
|
||||
('club_admin', 'planning.ai.suggest'),
|
||||
('trainer', 'planning.ai.suggest'),
|
||||
('division_lead', 'planning.ai.suggest'),
|
||||
('club_admin', 'planning.ai.progression_path'),
|
||||
('trainer', 'planning.ai.progression_path'),
|
||||
('division_lead', 'planning.ai.progression_path'),
|
||||
('club_admin', 'skills.discovery.read'),
|
||||
('trainer', 'skills.discovery.read'),
|
||||
('content_editor', 'skills.discovery.read'),
|
||||
('club_admin', 'governance.content_report.review')
|
||||
) AS r(role_code, cap_id)
|
||||
JOIN capabilities c ON c.id = r.cap_id
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
-- org.club.update: club_admin (zusätzlich zu platform_admin via Bypass)
|
||||
INSERT INTO club_role_capability_grants (role_code, capability_id)
|
||||
VALUES ('club_admin', 'org.club.update')
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
-- ── Portal-Rollen ───────────────────────────────────────────────────────────
|
||||
INSERT INTO portal_role_capability_grants (portal_role, capability_id)
|
||||
SELECT 'admin', id FROM capabilities WHERE id = 'platform.admin.access'
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
INSERT INTO portal_role_capability_grants (portal_role, capability_id)
|
||||
SELECT 'superadmin', id FROM capabilities WHERE domain = 'platform'
|
||||
ON CONFLICT DO NOTHING;
|
||||
41
backend/migrations/080_club_creation_requests.sql
Normal file
41
backend/migrations/080_club_creation_requests.sql
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
-- Migration 080: Antrag auf Vereinsgründung (M7)
|
||||
-- Nutzer verified_pending_club stellt Antrag; Plattform-Admin legt Verein + Abo an.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS club_creation_requests (
|
||||
id SERIAL PRIMARY KEY,
|
||||
profile_id INT NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
|
||||
proposed_name VARCHAR(200) NOT NULL,
|
||||
proposed_abbreviation VARCHAR(50),
|
||||
proposed_description TEXT,
|
||||
message TEXT,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'pending'
|
||||
CHECK (status IN ('pending', 'approved', 'rejected', 'withdrawn')),
|
||||
decided_by_profile_id INT REFERENCES profiles(id) ON DELETE SET NULL,
|
||||
decided_at TIMESTAMP,
|
||||
created_club_id INT REFERENCES clubs(id) ON DELETE SET NULL,
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
updated_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uq_club_creation_requests_pending
|
||||
ON club_creation_requests (profile_id)
|
||||
WHERE status = 'pending';
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_club_creation_requests_status
|
||||
ON club_creation_requests (status, created_at);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_club_creation_requests_profile
|
||||
ON club_creation_requests (profile_id);
|
||||
|
||||
DROP TRIGGER IF EXISTS club_creation_requests_update ON club_creation_requests;
|
||||
CREATE TRIGGER club_creation_requests_update
|
||||
BEFORE UPDATE ON club_creation_requests
|
||||
FOR EACH ROW EXECUTE FUNCTION update_timestamp();
|
||||
|
||||
-- Capabilities (CAPABILITY_CATALOG.v1.md — club.creation_request.*)
|
||||
INSERT INTO capabilities (id, name, domain, min_account_state, linked_feature_id)
|
||||
VALUES
|
||||
('club.creation_request.create', 'Vereinsgründung beantragen', 'club', 'verified_pending_club', NULL),
|
||||
('club.creation_request.read_own', 'Eigene Gründungsanträge', 'club', 'verified_pending_club', NULL),
|
||||
('club.creation_request.withdraw', 'Gründungsantrag zurückziehen', 'club', 'verified_pending_club', NULL)
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user