diff --git a/.claude/docs/PROJECT_STATUS.md b/.claude/docs/PROJECT_STATUS.md
index bf53926..493ad03 100644
--- a/.claude/docs/PROJECT_STATUS.md
+++ b/.claude/docs/PROJECT_STATUS.md
@@ -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-12
+**Version (Code):** 0.8.96 (`backend/version.py`, APP_VERSION)
+**DB-Schema-Version:** `20260511053` (`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.
+
+**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`).
+
+**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).
**Parallel weiter relevant:** **Trainingsrahmenprogramm** (036–037), **Progressionsgraph** (032–034) — siehe **`TRAINING_FRAMEWORK_SPEC.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)
+**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-08, für **neue Session**):
+**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,7 +27,7 @@
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).
---
@@ -150,17 +154,18 @@ 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-12 | neu, Ist-Überblick |
+| Lieferliste Q2 2026 | `library/FEATURES_DELIVERED_2026-Q2.md` | 2026-05-12 | Verweis Version siehe `version.py` |
| 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-12 | Version 0.4.5, Verweis Nutzerüberblick |
| 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 |
+| Projektstatus | `PROJECT_STATUS.md` | 2026-05-12 | auf 0.8.96 + P-13/P-01 + Nutzerüberblick |
---
@@ -171,4 +176,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-12 (Version 0.8.96, Executive Summary P-13/P-01, `docs/FACHLICHE_NUTZERFUNKTIONEN.md`)
diff --git a/.claude/docs/functional/DOMAIN_MODEL.md b/.claude/docs/functional/DOMAIN_MODEL.md
index 0c19ec9..8de4ca0 100644
--- a/.claude/docs/functional/DOMAIN_MODEL.md
+++ b/.claude/docs/functional/DOMAIN_MODEL.md
@@ -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.5
+**Stand:** 2026-05-12 (Fachlicher Nutzerüberblick: `docs/FACHLICHE_NUTZERFUNKTIONEN.md`)
**Basis:** `shinkan_anforderungsdokument_entwurf.md` + Fähigkeitsmatrix
---
diff --git a/.claude/docs/functional/SHINKAN_REQUIREMENTS.md b/.claude/docs/functional/SHINKAN_REQUIREMENTS.md
index 01d05b0..84c44ba 100644
--- a/.claude/docs/functional/SHINKAN_REQUIREMENTS.md
+++ b/.claude/docs/functional/SHINKAN_REQUIREMENTS.md
@@ -4,11 +4,15 @@ 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 |
-**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.
diff --git a/.claude/docs/functional/Shinkan Trainingsmodule Kombinationsuebungen Spezifikation V2.md b/.claude/docs/functional/Shinkan Trainingsmodule Kombinationsuebungen Spezifikation V2.md
new file mode 100644
index 0000000..55d3643
--- /dev/null
+++ b/.claude/docs/functional/Shinkan Trainingsmodule Kombinationsuebungen Spezifikation V2.md
@@ -0,0 +1,814 @@
+# Trainingsmodule und Kombinationsübungen — fachliche Spezifikation V3
+
+**Status:** fachlicher Spezifikationsentwurf
+**Stand:** 2026-05-12 (Anhang A **grob** App **0.8.104**; Zeit‑Pfad **`COMBINATION_TIMING_PROFILE_PLAN.md`**) · **Coaching/Archetypen:** § 10.2.1, § 10.4–10.5, **§ 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.103):** Pro Übungszeile in einer Trainingseinheit kann optional ein **JSON-Snapshot** des Ablaufprofils gespeichert werden (`planning_method_profile` in der DB). **`null`** bedeutet: es wirkt das Ablaufprofil aus dem **Katalog** (`method_profile` der Übung). Ist ein Snapshot gesetzt, ersetzt er den Katalog **vollständig** für diese Platzierung (kein serverseitiges Zusammenführen). Bearbeitung in der Planungs-UI: aufklappbarer Block **„Ablaufprofil für diese Planung (Kombination)“** mit denselben geführten Feldern wie im Übungsformular.
+
+**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).
+
+---
+
+## 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.104**, grob)
+
+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` (+ Pilot **`slot_profiles_v1`** je Station in derselben JSON‑Struktur) | Persistenz; Übungsformular: **geführte globale Felder** + **pro Slot** vier Zeitreihen ohne Nutzer‑JSON‑Pflicht; Schnellwahl typische Arbeit/Pause‑Relationen (**Zirkel**, **Intervall**); Reihenfolge UX: Stationen vor Ablaufprofil | JSON‑„Experte“ weiter abschaltbar; Schema‑Pflichtfelder nach Archetyp; Konvergenz flache Schlüssel ↔ `timing_schema` (siehe Arbeitsplan) |
+| **Einplanbarkeit (normale Planung)** | Kombi in Sektionen; **Zeitprofil‑Overrides** nach § 8.3 / § 10.5.1 | Picker, `exercise_kind` in Form/PUT, keine Variante bei Kombi; **Override:** DB **`planning_method_profile`** je Sektions-Item (Migration **057**), Planungseditor: Details „Ablaufprofil für diese Planung“, **„Planung wie Katalog“** / **„Aus Katalog kopieren“** | Planungsblöcke als Produktkonzept · Phase 3; serverseitige Validierung Snapshot↔Archetyp optional |
+| **Zeitphasen (global / pro Slot)** | § 6.3 | Über `method_profile` / Planungs‑Snapshot (**gleiche JSON-Struktur** wie Katalogprofil): globale Schlüssel im Übungs- und Planungseditor; weiterhin **keine** eigenständigen slotgebundenen Zeitlisten im UI | `slot_timing[]` oder äquivalent definieren und editieren |
+| **Coaching Stufe A** | Slots + Kandidaten sichtbar, Archetyp‑Hinweis, Profil lesbar | `CombinationCoachSlots`: wirksames Profil = **Planungs‑Snapshot wenn gesetzt, sonst Katalog**; Anzeige **Key/Value** | Profilwerte **lesend** benutzerfreundlicher labeln (statt nur Schlüsselnamen) |
+| **Coaching Stufe B** | Zeitleiste archetypnah (z. B. Schritt pro Station) | **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** Coach‑Timer pro Planungsitem | Pro Archetyp UI‑State + Anbindung an `method_profile` |
+| **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 | **Nein** / nicht als eigener Modus | Optional: dieselbe `CombinationCoachSlots`‑Ansicht read‑only im Übungseditor |
+
+**Pflege:** Bei jeder relevanten Codeänderung diese Tabelle **in demselben PR / derselben Session** anpassen (kein stiller Drift).
+
diff --git a/.claude/docs/library/FEATURES_DELIVERED_2026-Q2.md b/.claude/docs/library/FEATURES_DELIVERED_2026-Q2.md
index 118bbff..e19ad1b 100644
--- a/.claude/docs/library/FEATURES_DELIVERED_2026-Q2.md
+++ b/.claude/docs/library/FEATURES_DELIVERED_2026-Q2.md
@@ -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-12
+**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/`.
@@ -170,4 +170,5 @@ Einzelnorm: **`technical/MEDIA_ASSETS_AND_ARCHIVE_SPEC.md`**. Kurzüberblick gel
| 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` |
+| Fachlicher Nutzerüberblick | `docs/FACHLICHE_NUTZERFUNKTIONEN.md` (Repo-Root) |
| Projektstatus-Kachel | `../PROJECT_STATUS.md` |
diff --git a/.claude/docs/technical/TRAINING_MODULES_AND_COMBINATION_EXERCISES_SPEC.md b/.claude/docs/technical/TRAINING_MODULES_AND_COMBINATION_EXERCISES_SPEC.md
new file mode 100644
index 0000000..5a69629
--- /dev/null
+++ b/.claude/docs/technical/TRAINING_MODULES_AND_COMBINATION_EXERCISES_SPEC.md
@@ -0,0 +1,203 @@
+# Trainingsmodule und Kombinationsübungen — Spezifikation (Entwurf)
+
+**Status:** Entwurf zur fachlichen und technischen Abstimmung · **Stand:** 2026-05-12
+**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 (Stand ~0.8.101, 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.
+- **Coaching:** Stufe **A** (informations-/strukturierte Slot- und Kandidatenansicht + Archetyp-Hilfstext) umgesetzt im Trainings-Coach (`ExerciseFullContent` / `CombinationCoachSlots`); Stufen **B/C** bewusst offen — siehe Fachspez § 10.4 und **Anhang A** dort.
+- **Umsetzungsplan:** `working/TRAINING_MODULES_IMPLEMENTATION_PLAN.md` (Phasen 2/4 mit „teilweise“).
+
+**Verwandte Dokumente:**
+
+| Dokument | Bezug |
+|----------|--------|
+| `TRAINING_FRAMEWORK_SPEC.md` | Rahmen-Bibliothek, Slot-Blueprint, Kopiersemantik (`from-framework-slot`) |
+| `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 |
diff --git a/.claude/docs/working/ACCESS_LAYER_ENDPOINT_AUDIT.md b/.claude/docs/working/ACCESS_LAYER_ENDPOINT_AUDIT.md
index b0a613c..984c0eb 100644
--- a/.claude/docs/working/ACCESS_LAYER_ENDPOINT_AUDIT.md
+++ b/.claude/docs/working/ACCESS_LAYER_ENDPOINT_AUDIT.md
@@ -15,6 +15,7 @@ Fortlaufend gemäß `ACCESS_LAYER_AND_GOVERNANCE_PLAN.md` Stufe A–C.
| exercises | übrige geschützte `/api/exercises*` | ja | `get_tenant_context` | ja | PUT Einzelübung: bei Sichtbarkeit `official` Medien-§4.2 (422: Lifecycle/Promotion/Copyright) |
| 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 |
+| 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 |
@@ -36,12 +37,13 @@ Fortlaufend gemäß `ACCESS_LAYER_AND_GOVERNANCE_PLAN.md` Stufe A–C.
**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-05-12 — Trainingsmodule (`/api/training-modules*`); Governance wie Planungsbibliothek.
---
### Changelog (Fortführung)
+- **2026-05-12:** `training_modules` Router 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`.
diff --git a/.claude/docs/working/COMBINATION_TIMING_PROFILE_PLAN.md b/.claude/docs/working/COMBINATION_TIMING_PROFILE_PLAN.md
new file mode 100644
index 0000000..6a31017
--- /dev/null
+++ b/.claude/docs/working/COMBINATION_TIMING_PROFILE_PLAN.md
@@ -0,0 +1,101 @@
+# 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 darstellen, mittelfristig Labels statt Schlüsseln.
+
+---
+
+## 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.
diff --git a/.claude/docs/working/TRAINING_MODULES_IMPLEMENTATION_PLAN.md b/.claude/docs/working/TRAINING_MODULES_IMPLEMENTATION_PLAN.md
new file mode 100644
index 0000000..72cc011
--- /dev/null
+++ b/.claude/docs/working/TRAINING_MODULES_IMPLEMENTATION_PLAN.md
@@ -0,0 +1,40 @@
+# 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-12 (Abgleich mit Code ~App **0.8.102**)
+
+## 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“ | **umgesetzt (MVP Schritt 1)** |
+| **2** | Kombinationsübungen: `exercise_kind`/`combination_*`, Slots, Pools, `method_archetype`, `method_profile` (JSON) | **teilweise** — Migration 056, CRUD/API, Picker/Liste; Übungsformular: geführtes **`method_profile` nach Archetyp** (`CombinationMethodProfileEditor`, `combinationMethodProfileUi.js`) plus Roh‑JSON; **Backend:** keine strenge Validierung Profil ↔ Archetyp | Haupt-/Nebenmethoden an Kombi wo Spec es verlangt; serverseitige Validierung für Profil‑Schlüssel optional |
+| **3** | Planungsblöcke: Gruppierung, Auflösen, „als Modul speichern“, erweiterter Übernahmemodus (Zwischenposition) | geplant |
+| **4** | Coaching: Archetyp-Support | **teilweise:** **Stufe A** nach Fachspez § 10.4 (Slotliste, Kandidaten aus Katalog geladen, Archetyp-Hilfstexte in `CombinationCoachSlots`/`combinationArchetypes.js`); **Stufe B/C** (Zeitleisten-Splitting, Stations-/Intervall-Timing) — **offen**, siehe Anhang A der Fachspez |
+| **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 | Slots + Kandidaten; Archetyp-Hilfstext; `method_profile` **lesend** unter der Kopf-Zeile (Key/Wert‑Liste wenn gepflegt); Feintuning Labels optional. |
+| **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 4b/4c. |
+
+## 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.
diff --git a/CLAUDE.md b/CLAUDE.md
index 6e926ba..f48905a 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -13,6 +13,7 @@
> | Anforderungen | `.claude/docs/functional/SHINKAN_REQUIREMENTS.md` |
> | Medien-Archiv, Lifecycle, Inline (Plan §11) | `.claude/docs/technical/MEDIA_ASSETS_AND_ARCHIVE_SPEC.md` |
> | Handover / nächste Session | **`docs/HANDOVER.md`** |
+> | Fachlicher Nutzerüberblick (Design/Product) | **`docs/FACHLICHE_NUTZERFUNKTIONEN.md`** |
## Projekt-Übersicht
@@ -83,7 +84,7 @@ 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-12): App **0.8.96**, DB‑Schema‑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 Sektionen, **Trainingsrahmen Bibliothek + Slot‑Blueprint** (036–037), Progressionsgraph, Reifegrad/Matrix‑Stack — Details `PROJECT_STATUS.md`, `docs/HANDOVER.md`, Nutzerüberblick **`docs/FACHLICHE_NUTZERFUNKTIONEN.md`**, `MEDIA_ASSETS_AND_ARCHIVE_SPEC.md` (Abschnitt 11 umgesetzt).
### Log (Auszug)
diff --git a/backend/main.py b/backend/main.py
index 10547ec..108cfe0 100644
--- a/backend/main.py
+++ b/backend/main.py
@@ -193,7 +193,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, legal_documents, content_reports
+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_modules, training_framework_programs, catalogs, maturity_models, matrix_stack_bundle, import_wiki, import_wiki_admin, legal_documents, content_reports
app.include_router(auth.router)
app.include_router(profiles.router)
@@ -209,6 +209,7 @@ 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(training_planning.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)
diff --git a/backend/migrations/054_training_modules.sql b/backend/migrations/054_training_modules.sql
new file mode 100644
index 0000000..dc9046c
--- /dev/null
+++ b/backend/migrations/054_training_modules.sql
@@ -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;
diff --git a/backend/migrations/055_profiles_training_planning_prefs.sql b/backend/migrations/055_profiles_training_planning_prefs.sql
new file mode 100644
index 0000000..b5db616
--- /dev/null
+++ b/backend/migrations/055_profiles_training_planning_prefs.sql
@@ -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;
diff --git a/backend/migrations/056_combination_exercises.sql b/backend/migrations/056_combination_exercises.sql
new file mode 100644
index 0000000..781ec1b
--- /dev/null
+++ b/backend/migrations/056_combination_exercises.sql
@@ -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);
diff --git a/backend/migrations/057_planning_method_profile_section_items.sql b/backend/migrations/057_planning_method_profile_section_items.sql
new file mode 100644
index 0000000..865df43
--- /dev/null
+++ b/backend/migrations/057_planning_method_profile_section_items.sql
@@ -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.';
diff --git a/backend/models.py b/backend/models.py
index ded7613..c2dc7b0 100644
--- a/backend/models.py
+++ b/backend/models.py
@@ -47,6 +47,10 @@ class ProfileUpdate(BaseModel):
default=None,
description="JSON: gespeicherte Standardfilter für die Übungsliste",
)
+ training_planning_prefs: Optional[Dict[str, Any]] = Field(
+ default=None,
+ description="JSON: UI-Optionen Trainingsplanung (z.B. Darstellung kopierter Module)",
+ )
class ProfileResponse(BaseModel):
id: int
diff --git a/backend/routers/exercises.py b/backend/routers/exercises.py
index 39e0075..1cdae79 100644
--- a/backend/routers/exercises.py
+++ b/backend/routers/exercises.py
@@ -10,12 +10,13 @@ import logging
import os
import re
from pathlib import Path
-from typing import Any, Dict, Iterator, List, Optional, Tuple
+from typing import Any, Dict, Iterator, List, Literal, Optional, Tuple
from urllib.parse import quote
from fastapi import APIRouter, HTTPException, Depends, Query, Request, UploadFile, File, Form
from fastapi.responses import FileResponse, Response, StreamingResponse
from pydantic import BaseModel, Field, model_validator
+from psycopg2.extras import Json
from db import get_db, get_cursor, r2d
from club_tenancy import (
@@ -198,6 +199,26 @@ def _upload_limit_bytes(tenant: TenantContext) -> int:
# Pydantic Models
# ============================================================================
+# Archetyp-IDs (Maschinenlesbare Ablaufmuster) — konsistent zur technischen Entwurfsspezifikation
+COMBINATION_ARCHETYPE_IDS = frozenset(
+ {
+ "circuit_rotate_time",
+ "circuit_all_parallel",
+ "sequence_linear",
+ "station_parcour",
+ "pair_superset",
+ "time_domain_interval",
+ "free_method_block",
+ }
+)
+
+
+class CombinationSlotIn(BaseModel):
+ slot_index: int = Field(ge=0, le=99)
+ title: Optional[str] = Field(None, max_length=200)
+ candidate_exercise_ids: list[int] = Field(default_factory=list)
+
+
class ExerciseCreate(BaseModel):
# Basis-Felder (goal/execution: DB-Constraint mind. eines; Wiki oft nur eines)
title: str = Field(..., min_length=3, max_length=300)
@@ -231,6 +252,12 @@ class ExerciseCreate(BaseModel):
status: str = "draft"
club_id: Optional[int] = None
+ # Kombinationsübung (Phase 2)
+ exercise_kind: Literal["simple", "combination"] = "simple"
+ method_archetype: Optional[str] = Field(None, max_length=80)
+ method_profile: Dict[str, Any] = Field(default_factory=dict)
+ combination_slots: list[CombinationSlotIn] = Field(default_factory=list)
+
@model_validator(mode="after")
def normalize_goal_execution(self):
g = (self.goal or "").strip() or None
@@ -270,6 +297,11 @@ class ExerciseUpdate(BaseModel):
# Vereins-Übung: fehlende Copyrights an Datei-Assets nach Prompt-Text setzen (PUT-Retry)
default_club_media_copyright: Optional[str] = Field(default=None, max_length=2000)
+ exercise_kind: Optional[Literal["simple", "combination"]] = None
+ method_archetype: Optional[str] = Field(None, max_length=80)
+ method_profile: Optional[Dict[str, Any]] = None
+ combination_slots: Optional[list[CombinationSlotIn]] = None
+
@model_validator(mode="after")
def normalize_goal_execution(self):
if self.goal is not None:
@@ -811,6 +843,174 @@ def _resolve_local_media_file(
return path_under_media_root(media_root, asset_storage_key)
return _abs_media_path(file_path_db or "", media_root) if file_path_db else None
+
+def _normalize_method_profile_store(raw: Any) -> Dict[str, Any]:
+ if raw is None:
+ return {}
+ if isinstance(raw, dict):
+ return raw
+ raise HTTPException(status_code=400, detail="method_profile muss ein JSON-Objekt sein")
+
+
+def _validate_archetype_for_kind(kind: str, archetype: Optional[str]) -> None:
+ if kind != "combination":
+ return
+ if archetype is None or not str(archetype).strip():
+ return
+ a = str(archetype).strip()
+ if a not in COMBINATION_ARCHETYPE_IDS:
+ raise HTTPException(
+ status_code=400,
+ detail=(
+ "Unbekannter method_archetype. Erlaubt: "
+ + ", ".join(sorted(COMBINATION_ARCHETYPE_IDS))
+ ),
+ )
+
+
+def _assert_candidate_exercises_for_combination(cur, tenant: TenantContext, ids: List[int]) -> None:
+ if not ids:
+ return
+ seen: set[int] = set()
+ for cid_raw in ids:
+ cid = int(cid_raw)
+ if cid in seen:
+ continue
+ seen.add(cid)
+ cur.execute(
+ """SELECT id, exercise_kind, visibility, club_id, created_by
+ FROM exercises WHERE id = %s""",
+ (cid,),
+ )
+ row = cur.fetchone()
+ if not row:
+ raise HTTPException(status_code=400, detail=f"Slot-Verweis: Übung #{cid} nicht gefunden")
+ rd = r2d(row)
+ k = str(rd.get("exercise_kind") or "simple").strip().lower()
+ if k != "simple":
+ raise HTTPException(
+ status_code=400,
+ detail=f"Slot-Verweis: Übung #{cid} ist eine Kombinationsübung — nur Einzelübungen erlaubt",
+ )
+ if not library_content_visible_to_profile(
+ cur,
+ tenant.profile_id,
+ rd.get("visibility"),
+ rd.get("club_id"),
+ rd.get("created_by"),
+ tenant.global_role,
+ ):
+ raise HTTPException(status_code=403, detail=f"Slot-Verweis: keine Leserechte für Übung #{cid}")
+
+
+def _validate_and_normalize_combination_slots_payload(
+ cur,
+ tenant: TenantContext,
+ slots: Optional[List[CombinationSlotIn]],
+) -> List[Tuple[int, Optional[str], List[int]]]:
+ if slots is None:
+ return []
+ normalized: Dict[int, Tuple[Optional[str], List[int]]] = {}
+ for s in sorted(slots, key=lambda x: x.slot_index):
+ cid_list_raw = list(s.candidate_exercise_ids or [])
+ cid_list: List[int] = []
+ for x in cid_list_raw:
+ cid_list.append(int(x))
+ title = ((s.title or "").strip()) or None
+ normalized[int(s.slot_index)] = (title, cid_list)
+ out: List[Tuple[int, Optional[str], List[int]]] = []
+ for idx in sorted(normalized.keys()):
+ title, cands = normalized[idx]
+ if not cands:
+ raise HTTPException(
+ status_code=400,
+ detail=f"Station (Index {idx}): mindestens eine Einzelübung (Pool) ist erforderlich",
+ )
+ out.append((idx, title, cands))
+ return out
+
+
+def replace_combination_slots(
+ cur,
+ tenant: TenantContext,
+ exercise_id: int,
+ slots_norm: List[Tuple[int, Optional[str], List[int]]],
+) -> None:
+ flat_ids = [cid for _, __, xs in slots_norm for cid in xs]
+ _assert_candidate_exercises_for_combination(cur, tenant, flat_ids)
+ cur.execute("DELETE FROM combination_exercise_slots WHERE exercise_id = %s", (exercise_id,))
+ for slot_index, title, cand_ids in slots_norm:
+ cur.execute(
+ """INSERT INTO combination_exercise_slots (exercise_id, slot_index, title)
+ VALUES (%s, %s, %s) RETURNING id""",
+ (exercise_id, slot_index, title),
+ )
+ row = cur.fetchone()
+ sid = row["id"] if isinstance(row, dict) else row[0]
+ for so, cid in enumerate(cand_ids):
+ cur.execute(
+ """INSERT INTO combination_slot_candidates (slot_id, candidate_exercise_id, sort_order)
+ VALUES (%s, %s, %s)""",
+ (sid, int(cid), int(so)),
+ )
+
+
+def wipe_combination_structure(cur, exercise_id: int) -> None:
+ cur.execute("DELETE FROM combination_exercise_slots WHERE exercise_id = %s", (exercise_id,))
+
+
+def assert_exercise_not_combination(cur, exercise_id: int) -> None:
+ cur.execute(
+ "SELECT COALESCE(exercise_kind, 'simple') AS exercise_kind FROM exercises WHERE id = %s",
+ (exercise_id,),
+ )
+ row = cur.fetchone()
+ if not row:
+ raise HTTPException(status_code=404, detail="Übung nicht gefunden")
+ if str(r2d(row).get("exercise_kind") or "simple").strip().lower() == "combination":
+ raise HTTPException(
+ status_code=400,
+ detail="Kombinationsübungen unterstützen keine Varianten.",
+ )
+
+
+def load_combination_slots_for_exercise(cur, exercise_id: int) -> List[dict]:
+ """Stationsliste einer Kombinationsübung (gleiches Format wie GET /api/exercises/:id)."""
+ cur.execute(
+ """SELECT id, slot_index, title FROM combination_exercise_slots
+ WHERE exercise_id = %s ORDER BY slot_index ASC, id ASC""",
+ (exercise_id,),
+ )
+ slot_rows = [r2d(r) for r in cur.fetchall()]
+ slots_out: List[dict] = []
+ for sr in slot_rows:
+ slot_pk = sr["id"]
+ cur.execute(
+ """SELECT candidate_exercise_id FROM combination_slot_candidates
+ WHERE slot_id = %s ORDER BY sort_order ASC, id ASC""",
+ (slot_pk,),
+ )
+ crows = cur.fetchall()
+ cids = [int(r2d(c)["candidate_exercise_id"]) for c in crows]
+ cand_meta: Dict[int, Optional[str]] = {}
+ if cids:
+ ph = ",".join(["%s"] * len(cids))
+ cur.execute(
+ f"SELECT id, title FROM exercises WHERE id IN ({ph})",
+ tuple(cids),
+ )
+ cand_meta = {int(r2d(x)["id"]): r2d(x).get("title") for x in cur.fetchall()}
+ slots_out.append(
+ {
+ "slot_index": sr["slot_index"],
+ "title": sr.get("title"),
+ "candidate_exercise_ids": cids,
+ "candidates": [{"exercise_id": cid, "title": cand_meta.get(cid)} for cid in cids],
+ }
+ )
+ return slots_out
+
+
def enrich_exercise_detail(exercise_id: int, cur) -> dict:
"""
Lädt alle M:N Relations für eine Übung und gibt ein vollständiges
@@ -933,6 +1133,14 @@ def enrich_exercise_detail(exercise_id: int, cur) -> dict:
)
exercise["media"] = [r2d(r) for r in cur.fetchall()]
+ mp_raw = exercise.get("method_profile")
+ exercise["method_profile"] = mp_raw if isinstance(mp_raw, dict) else {}
+ exercise["exercise_kind"] = str(exercise.get("exercise_kind") or "simple").strip().lower()
+
+ exercise["combination_slots"] = []
+ if exercise["exercise_kind"] == "combination":
+ exercise["combination_slots"] = load_combination_slots_for_exercise(cur, exercise_id)
+
return exercise
@@ -1528,6 +1736,10 @@ def list_exercises(
default=False,
description="Nur Übungen, die vom aktuellen Profil angelegt wurden (created_by = Profil)",
),
+ exercise_kind_any: list[str] = Query(
+ default=[],
+ description="ODER: mind. einer dieser Übungsarten — simple oder combination",
+ ),
tenant: TenantContext = Depends(get_tenant_context),
):
"""
@@ -1558,6 +1770,21 @@ def list_exercises(
where.append("e.created_by = %s")
params.append(profile_id)
+ ek_filtered: List[str] = []
+ if exercise_kind_any:
+ for raw in exercise_kind_any:
+ s = str(raw or "").strip().lower()
+ if not s:
+ continue
+ if s not in ("simple", "combination"):
+ raise HTTPException(status_code=400, detail="exercise_kind_any: nur simple oder combination")
+ if s not in ek_filtered:
+ ek_filtered.append(s)
+ if ek_filtered:
+ ph = ",".join(["%s"] * len(ek_filtered))
+ where.append(f"(LOWER(TRIM(COALESCE(e.exercise_kind::text,''))) IN ({ph}))")
+ params.extend(ek_filtered)
+
vis_list = _merge_str_any(visibility_any, visibility)
if vis_list:
ph = ",".join(["%s"] * len(vis_list))
@@ -1776,6 +2003,7 @@ def list_exercises(
# Query (primary_focus_name für Listen-Ansicht gemäß Spec-Beispiel „focus_area“-Label)
query = f"""
SELECT e.id, e.title, e.summary, e.visibility, e.status,
+ e.exercise_kind, e.method_archetype,
e.created_by, p.name as creator_name,
e.club_id, c.name as club_name,
e.created_at, e.updated_at,
@@ -1829,6 +2057,7 @@ def list_exercises(
out = []
for r in rows:
d = r2d(r)
+ d["exercise_kind"] = str(d.get("exercise_kind") or "simple").strip().lower()
pfn = d.get("primary_focus_name")
d["focus_area"] = pfn
d["focus_area_names"] = _coerce_json_str_list(d.get("focus_area_names"))
@@ -1913,16 +2142,39 @@ def create_exercise(
cur, profile_id, tenant.global_role, body.visibility, club_id
)
+ kind_clean = str(body.exercise_kind or "simple").strip().lower()
+ if kind_clean not in ("simple", "combination"):
+ raise HTTPException(status_code=400, detail="exercise_kind: simple oder combination")
+
+ prof_dict = _normalize_method_profile_store(body.method_profile)
+ arch_raw = body.method_archetype
+ arch_val = (arch_raw.strip() if isinstance(arch_raw, str) and arch_raw.strip() else None)
+ _validate_archetype_for_kind(kind_clean, arch_val)
+
+ slots_norm: List[Tuple[int, Optional[str], List[int]]] = []
+ if kind_clean == "combination":
+ slots_norm = _validate_and_normalize_combination_slots_payload(
+ cur, tenant, body.combination_slots or []
+ )
+ if not slots_norm:
+ raise HTTPException(
+ status_code=400,
+ detail="Kombinationsübung: mindestens eine Station mit Übungen nötig",
+ )
+
+ mp_json = Json(prof_dict if kind_clean == "combination" else {})
+ arch_db = arch_val if kind_clean == "combination" else None
+
# Equipment als JSONB
equipment_json = json.dumps(body.equipment) if body.equipment else None
- # INSERT
cur.execute(
"""INSERT INTO exercises
(title, summary, goal, execution, preparation, trainer_notes,
duration_min, duration_max, group_size_min, group_size_max,
- equipment, visibility, status, created_by, club_id)
- VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)
+ equipment, visibility, status, created_by, club_id,
+ exercise_kind, method_archetype, method_profile)
+ VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)
RETURNING id""",
(
body.title, body.summary, body.goal, body.execution,
@@ -1931,11 +2183,15 @@ def create_exercise(
body.group_size_min, body.group_size_max,
equipment_json,
body.visibility, body.status, profile_id, club_id,
- )
+ kind_clean, arch_db, mp_json,
+ ),
)
row = cur.fetchone()
exercise_id = row['id'] if isinstance(row, dict) else row[0]
+ if kind_clean == "combination":
+ replace_combination_slots(cur, tenant, exercise_id, slots_norm)
+
data = body.dict()
assign_exercise_relations(cur, conn, exercise_id, data, do_commit=False)
if (body.visibility or "").strip().lower() == "club":
@@ -1963,7 +2219,7 @@ def update_exercise(
cur = get_cursor(conn)
cur.execute(
- f"""SELECT created_by, visibility, club_id,
+ f"""SELECT created_by, visibility, club_id, exercise_kind, method_archetype, method_profile,
{", ".join(sorted(RICH_HTML_EXERCISE_FIELDS))}
FROM exercises WHERE id = %s""",
(exercise_id,),
@@ -1993,6 +2249,84 @@ def update_exercise(
default_official_copy = data.pop("default_official_media_copyright", None)
default_club_copy = data.pop("default_club_media_copyright", None)
+ combo_slots_provided = "combination_slots" in data
+ combo_slots_payload = data.pop("combination_slots", None)
+
+ ec_kind_was = str(rd_full.get("exercise_kind") or "simple").strip().lower()
+ ek_provided = "exercise_kind" in data
+ next_kind = ec_kind_was
+ if ek_provided:
+ next_kind = str(data.pop("exercise_kind") or "simple").strip().lower()
+
+ arche_provided = "method_archetype" in data
+ meth_prof_provided = "method_profile" in data
+
+ next_ma_db = rd_full.get("method_archetype")
+ if isinstance(next_ma_db, str):
+ next_ma_db = next_ma_db.strip() or None
+ else:
+ next_ma_db = None
+
+ mp_row = rd_full.get("method_profile")
+ next_mp_db = mp_row if isinstance(mp_row, dict) else {}
+
+ if ek_provided and next_kind not in ("simple", "combination"):
+ raise HTTPException(status_code=400, detail="exercise_kind: simple oder combination")
+
+ if arche_provided:
+ va = data.pop("method_archetype")
+ if va is None or (isinstance(va, str) and not va.strip()):
+ next_ma_db = None
+ elif isinstance(va, str):
+ next_ma_db = va.strip() or None
+ else:
+ next_ma_db = None
+
+ if meth_prof_provided:
+ next_mp_db = _normalize_method_profile_store(data.pop("method_profile"))
+
+ if next_kind == "simple":
+ next_ma_db = None
+ next_mp_db = {}
+
+ _validate_archetype_for_kind(next_kind, next_ma_db)
+
+ if ec_kind_was == "simple" and next_kind == "combination":
+ if not combo_slots_provided:
+ raise HTTPException(
+ status_code=400,
+ detail='Umschalten auf Kombinationsübung: Feld "combination_slots" ist erforderlich',
+ )
+
+ combo_slots_normalized: Optional[List[Tuple[int, Optional[str], List[int]]]] = None
+ if combo_slots_provided:
+ if next_kind != "combination":
+ raise HTTPException(
+ status_code=400,
+ detail="combination_slots nur bei exercise_kind=combination erlaubt",
+ )
+ slots_in_raw = combo_slots_payload if combo_slots_payload is not None else []
+ slots_in: List[CombinationSlotIn] = []
+ for s in slots_in_raw:
+ if isinstance(s, CombinationSlotIn):
+ slots_in.append(s)
+ elif isinstance(s, dict):
+ slots_in.append(CombinationSlotIn(**s))
+ else:
+ raise HTTPException(status_code=400, detail="Ungültige combination_slots Payload-Struktur")
+ combo_slots_normalized = _validate_and_normalize_combination_slots_payload(
+ cur, tenant, slots_in
+ )
+ if not combo_slots_normalized:
+ raise HTTPException(status_code=400, detail="Kombinationsübung: mindestens eine Station mit Übungen")
+
+ update_combo_cols = (
+ ek_provided
+ or arche_provided
+ or meth_prof_provided
+ or (next_kind == "simple" and ec_kind_was != "simple")
+ )
+
merged_rich = {fld: rich_row.get(fld) for fld in RICH_HTML_EXERCISE_FIELDS}
for fld in RICH_HTML_EXERCISE_FIELDS:
if fld not in data:
@@ -2064,11 +2398,27 @@ def update_exercise(
fields.append("equipment = %s")
params.append(json.dumps(data["equipment"]) if data["equipment"] else None)
+ if update_combo_cols:
+ if ek_provided:
+ fields.append("exercise_kind = %s")
+ params.append(next_kind)
+ fields.append("method_archetype = %s")
+ params.append(next_ma_db)
+ fields.append("method_profile = %s")
+ params.append(Json(next_mp_db))
+
if fields:
fields.append("updated_at = NOW()")
params.append(exercise_id)
query = f"UPDATE exercises SET {', '.join(fields)} WHERE id = %s"
cur.execute(query, params)
+ elif combo_slots_normalized is not None:
+ cur.execute("UPDATE exercises SET updated_at = NOW() WHERE id = %s", (exercise_id,))
+
+ if combo_slots_normalized is not None:
+ replace_combination_slots(cur, tenant, exercise_id, combo_slots_normalized)
+ elif ec_kind_was == "combination" and next_kind == "simple":
+ wipe_combination_structure(cur, exercise_id)
assign_exercise_relations(cur, conn, exercise_id, data, do_commit=False)
try:
@@ -2144,6 +2494,7 @@ def reorder_exercise_variants(
with get_db() as conn:
cur = get_cursor(conn)
_assert_can_edit_exercise(cur, exercise_id, tenant)
+ assert_exercise_not_combination(cur, exercise_id)
cur.execute(
"SELECT id FROM exercise_variants WHERE exercise_id = %s",
@@ -2178,6 +2529,7 @@ def create_exercise_variant(
with get_db() as conn:
cur = get_cursor(conn)
_assert_can_edit_exercise(cur, exercise_id, tenant)
+ assert_exercise_not_combination(cur, exercise_id)
_validate_variant_prerequisite(cur, exercise_id, body.prerequisite_variant_id)
eq_json = _variant_equipment_json(body.equipment_changes)
@@ -2242,6 +2594,7 @@ def update_exercise_variant(
with get_db() as conn:
cur = get_cursor(conn)
_assert_can_edit_exercise(cur, exercise_id, tenant)
+ assert_exercise_not_combination(cur, exercise_id)
old = _fetch_variant_row(cur, exercise_id, variant_id)
if "variant_name" in data and data["variant_name"] is not None:
@@ -2323,6 +2676,7 @@ def delete_exercise_variant(
with get_db() as conn:
cur = get_cursor(conn)
_assert_can_edit_exercise(cur, exercise_id, tenant)
+ assert_exercise_not_combination(cur, exercise_id)
_fetch_variant_row(cur, exercise_id, variant_id)
cur.execute(
diff --git a/backend/routers/profiles.py b/backend/routers/profiles.py
index 3785b84..c554b3e 100644
--- a/backend/routers/profiles.py
+++ b/backend/routers/profiles.py
@@ -384,6 +384,15 @@ def _run_profile_update(pid: str, p: ProfileUpdate, tenant: TenantContext) -> di
else:
raise HTTPException(400, "exercise_list_prefs muss ein JSON-Objekt sein")
+ if "training_planning_prefs" in patch:
+ tp = patch.pop("training_planning_prefs")
+ if tp is None:
+ data["training_planning_prefs"] = Json({})
+ elif isinstance(tp, dict):
+ data["training_planning_prefs"] = Json(tp)
+ else:
+ raise HTTPException(400, "training_planning_prefs muss ein JSON-Objekt sein")
+
nullable_keys = {"goal_weight", "goal_bf_pct", "dob"}
for k, v in patch.items():
if k == "email":
diff --git a/backend/routers/training_modules.py b/backend/routers/training_modules.py
new file mode 100644
index 0000000..1fb40b3
--- /dev/null
+++ b/backend/routers/training_modules.py
@@ -0,0 +1,381 @@
+"""
+Trainingsmodule — wiederverwendbare Planungsbausteine (Bibliothek).
+
+Governance wie Trainings‑Mikrovorlagen (`training_plan_templates`):
+Liste/Detail über `library_content_visibility_sql`; Schreiben: Ersteller oder Plattform‑Admin.
+
+Siehe `.claude/docs/working/TRAINING_MODULES_IMPLEMENTATION_PLAN.md`.
+"""
+from typing import Any, Dict, List, Optional, Tuple
+
+from fastapi import APIRouter, Depends, HTTPException
+
+from db import get_db, get_cursor, r2d
+from tenant_context import TenantContext, get_tenant_context, library_content_visibility_sql
+from club_tenancy import (
+ assert_valid_governance_visibility,
+ is_platform_admin,
+ library_content_visible_to_profile,
+)
+
+router = APIRouter(prefix="/api", tags=["training_modules"])
+
+
+def _has_planning_role(role: Optional[str]) -> bool:
+ return role in ("admin", "superadmin", "trainer", "user")
+
+
+def _fetch_training_module_row(cur, mid: int) -> Dict[str, Any]:
+ cur.execute("SELECT * FROM training_modules WHERE id = %s", (mid,))
+ r = cur.fetchone()
+ if not r:
+ raise HTTPException(status_code=404, detail="Trainingsmodul nicht gefunden")
+ return r2d(r)
+
+
+def _module_assert_readable(cur, row: Dict[str, Any], profile_id: int, role: Optional[str]) -> None:
+ if is_platform_admin(role):
+ return
+ if not library_content_visible_to_profile(
+ cur,
+ profile_id,
+ row.get("visibility") or "club",
+ row.get("club_id"),
+ row.get("created_by"),
+ role,
+ ):
+ raise HTTPException(status_code=403, detail="Keine Berechtigung für dieses Modul")
+
+
+def _module_assert_writable(cur, row: Dict[str, Any], profile_id: int, role: Optional[str]) -> None:
+ if is_platform_admin(role):
+ return
+ if row.get("created_by") != profile_id:
+ raise HTTPException(status_code=403, detail="Nur der Ersteller darf dieses Modul ändern")
+
+
+def _module_access(cur, mid: int, profile_id: int, role: str) -> Dict[str, Any]:
+ row = _fetch_training_module_row(cur, mid)
+ _module_assert_readable(cur, row, profile_id, role)
+ return row
+
+
+def _validate_variant_for_exercise(cur, exercise_id: Optional[int], variant_id: Optional[int]):
+ if not variant_id:
+ return
+ if not exercise_id:
+ raise HTTPException(
+ status_code=400, detail="exercise_variant_id nur zusammen mit exercise_id erlaubt"
+ )
+ cur.execute(
+ "SELECT 1 FROM exercise_variants WHERE id = %s AND exercise_id = %s",
+ (variant_id, exercise_id),
+ )
+ if not cur.fetchone():
+ raise HTTPException(status_code=400, detail="Variante passt nicht zur gewählten Übung")
+
+
+def _optional_positive_int(val, field_name: str) -> Optional[int]:
+ if val is None or val == "":
+ return None
+ try:
+ i = int(val)
+ except (TypeError, ValueError):
+ raise HTTPException(status_code=400, detail=f"Ungültige {field_name}")
+ if i < 1:
+ raise HTTPException(status_code=400, detail=f"Ungültige {field_name}")
+ return i
+
+
+def _replace_module_items(cur, module_id: int, items_in: Optional[List[Any]]) -> None:
+ cur.execute("DELETE FROM training_module_items WHERE module_id = %s", (module_id,))
+ items_in = items_in or []
+ for i, raw in enumerate(items_in):
+ itype = raw.get("item_type")
+ if not itype:
+ itype = "exercise" if raw.get("exercise_id") else "note"
+ order_ix = raw.get("order_index")
+ if order_ix is None:
+ order_ix = i
+ order_ix = int(order_ix)
+
+ if itype == "note":
+ body = raw.get("note_body")
+ if body is None:
+ body = ""
+ cur.execute(
+ """
+ INSERT INTO training_module_items (
+ module_id, order_index, item_type,
+ exercise_id, exercise_variant_id,
+ planned_duration_min, notes, note_body
+ ) VALUES (%s, %s, 'note',
+ NULL, NULL, NULL, NULL, %s)
+ """,
+ (module_id, order_ix, body),
+ )
+ continue
+
+ eid = raw.get("exercise_id")
+ if not eid:
+ continue
+ eid = int(eid)
+ vid = _optional_positive_int(raw.get("exercise_variant_id"), "exercise_variant_id")
+ _validate_variant_for_exercise(cur, eid, vid)
+ cur.execute(
+ """
+ INSERT INTO training_module_items (
+ module_id, order_index, item_type,
+ exercise_id, exercise_variant_id,
+ planned_duration_min, notes, note_body
+ ) VALUES (%s, %s, 'exercise',
+ %s, %s, %s, %s, NULL)
+ """,
+ (
+ module_id,
+ order_ix,
+ eid,
+ vid,
+ raw.get("planned_duration_min"),
+ raw.get("notes"),
+ ),
+ )
+
+
+def load_training_module_for_apply(
+ cur, module_id: int, profile_id: int, role: Optional[str]
+) -> Tuple[List[Dict[str, Any]], int]:
+ """
+ Liest Modul inkl. Items für Übernahme in eine Trainingseinheit.
+ Returns (items_ordered, module_id).
+ Raises HTTPException bei 403/404.
+ """
+ row = _fetch_training_module_row(cur, module_id)
+ _module_assert_readable(cur, row, profile_id, role or "")
+ cur.execute(
+ """
+ SELECT item_type, exercise_id, exercise_variant_id,
+ planned_duration_min, notes, note_body
+ FROM training_module_items
+ WHERE module_id = %s
+ ORDER BY order_index ASC
+ """,
+ (module_id,),
+ )
+ raw_items = [r2d(x) for x in cur.fetchall()]
+ items: List[Dict[str, Any]] = []
+ for r in raw_items:
+ items.append(dict(r))
+ return items, int(module_id)
+
+
+@router.get("/training-modules")
+def list_training_modules(tenant: TenantContext = Depends(get_tenant_context)):
+ profile_id = tenant.profile_id
+ role = tenant.global_role
+ with get_db() as conn:
+ cur = get_cursor(conn)
+ vis_clause, vis_params = library_content_visibility_sql(
+ alias="m",
+ profile_id=profile_id,
+ role=role,
+ effective_club_id=tenant.effective_club_id,
+ )
+ cur.execute(
+ f"""
+ SELECT m.*,
+ (SELECT COUNT(*) FROM training_module_items i WHERE i.module_id = m.id)
+ AS items_count
+ FROM training_modules m
+ WHERE ({vis_clause})
+ ORDER BY m.updated_at DESC NULLS LAST, m.title
+ """,
+ vis_params,
+ )
+ return [r2d(r) for r in cur.fetchall()]
+
+
+@router.get("/training-modules/{module_id}")
+def get_training_module(module_id: int, tenant: TenantContext = Depends(get_tenant_context)):
+ profile_id = tenant.profile_id
+ role = tenant.global_role
+ with get_db() as conn:
+ cur = get_cursor(conn)
+ row = _module_access(cur, module_id, profile_id, role)
+ cur.execute(
+ """
+ SELECT *
+ FROM training_module_items
+ WHERE module_id = %s
+ ORDER BY order_index ASC
+ """,
+ (module_id,),
+ )
+ row["items"] = [r2d(r) for r in cur.fetchall()]
+ return row
+
+
+@router.post("/training-modules")
+def create_training_module(data: dict, tenant: TenantContext = Depends(get_tenant_context)):
+ profile_id = tenant.profile_id
+ role = tenant.global_role
+ if not _has_planning_role(role):
+ raise HTTPException(status_code=403, detail="Nur Trainer dürfen Trainingsmodule anlegen")
+
+ title = (data.get("title") or "").strip()
+ if not title:
+ raise HTTPException(status_code=400, detail="title ist Pflicht")
+
+ vis_raw = data.get("visibility")
+ visibility = (vis_raw if isinstance(vis_raw, str) else "club").strip() or "club"
+ club_id = data.get("club_id")
+ if club_id in ("", []):
+ club_id = None
+ if visibility == "club" and club_id is None:
+ club_id = tenant.effective_club_id
+
+ primary_method_id = data.get("primary_method_id")
+ if primary_method_id in ("", []):
+ primary_method_id = None
+ if primary_method_id is not None:
+ primary_method_id = int(primary_method_id)
+
+ items_in = data.get("items") or []
+
+ with get_db() as conn:
+ cur = get_cursor(conn)
+ assert_valid_governance_visibility(cur, profile_id, role, visibility, club_id)
+ if primary_method_id is not None:
+ cur.execute("SELECT 1 FROM training_methods WHERE id = %s", (primary_method_id,))
+ if not cur.fetchone():
+ raise HTTPException(status_code=400, detail="Trainingsmethode nicht gefunden")
+
+ cur.execute(
+ """
+ INSERT INTO training_modules (
+ club_id, created_by, title, summary, goal,
+ recommended_duration_min, target_group_notes, deployment_context_notes,
+ primary_method_id, visibility
+ ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
+ RETURNING id
+ """,
+ (
+ club_id,
+ profile_id,
+ title,
+ (data.get("summary") or "").strip() or None,
+ data.get("goal"),
+ data.get("recommended_duration_min"),
+ data.get("target_group_notes"),
+ data.get("deployment_context_notes"),
+ primary_method_id,
+ visibility,
+ ),
+ )
+ mid = cur.fetchone()["id"]
+ _replace_module_items(cur, mid, items_in if isinstance(items_in, list) else [])
+ conn.commit()
+
+ return get_training_module(mid, tenant)
+
+
+@router.put("/training-modules/{module_id}")
+def update_training_module(
+ module_id: int, data: dict, tenant: TenantContext = Depends(get_tenant_context)
+):
+ profile_id = tenant.profile_id
+ role = tenant.global_role
+ with get_db() as conn:
+ cur = get_cursor(conn)
+ row_prev = _fetch_training_module_row(cur, module_id)
+ _module_assert_writable(cur, row_prev, profile_id, role)
+
+ merged_vis = row_prev.get("visibility") or "club"
+ merged_club = row_prev.get("club_id")
+
+ if "visibility" in data:
+ v_in = data.get("visibility")
+ if not isinstance(v_in, str) or v_in not in ("private", "club", "official"):
+ raise HTTPException(status_code=400, detail="visibility ungültig")
+ merged_vis = v_in
+
+ if "club_id" in data:
+ merged_club = data.get("club_id")
+ if merged_club in ("", []):
+ merged_club = None
+
+ if "visibility" in data or "club_id" in data:
+ assert_valid_governance_visibility(cur, profile_id, role, merged_vis, merged_club)
+
+ fields: List[str] = []
+ params: List[Any] = []
+
+ if "title" in data:
+ t = data.get("title")
+ t = t.strip() if isinstance(t, str) else ""
+ if not t:
+ raise HTTPException(status_code=400, detail="title ist Pflicht")
+ fields.append("title = %s")
+ params.append(t)
+
+ for col in ("summary", "goal", "target_group_notes", "deployment_context_notes"):
+ if col in data:
+ fields.append(f"{col} = %s")
+ v = data.get(col)
+ if col == "summary" and isinstance(v, str):
+ v = v.strip() or None
+ params.append(v)
+
+ if "recommended_duration_min" in data:
+ fields.append("recommended_duration_min = %s")
+ params.append(data.get("recommended_duration_min"))
+
+ if "primary_method_id" in data:
+ pm = data.get("primary_method_id")
+ if pm in ("", [], None):
+ fields.append("primary_method_id = %s")
+ params.append(None)
+ else:
+ pm = int(pm)
+ cur.execute("SELECT 1 FROM training_methods WHERE id = %s", (pm,))
+ if not cur.fetchone():
+ raise HTTPException(status_code=400, detail="Trainingsmethode nicht gefunden")
+ fields.append("primary_method_id = %s")
+ params.append(pm)
+
+ if "club_id" in data:
+ fields.append("club_id = %s")
+ params.append(merged_club)
+
+ if "visibility" in data:
+ fields.append("visibility = %s")
+ params.append(merged_vis)
+
+ if fields:
+ fields.append("updated_at = NOW()")
+ params.append(module_id)
+ cur.execute(
+ f"UPDATE training_modules SET {', '.join(fields)} WHERE id = %s",
+ tuple(params),
+ )
+
+ if "items" in data:
+ items_in = data["items"]
+ _replace_module_items(cur, module_id, items_in if isinstance(items_in, list) else [])
+
+ conn.commit()
+
+ return get_training_module(module_id, tenant)
+
+
+@router.delete("/training-modules/{module_id}")
+def delete_training_module(module_id: int, tenant: TenantContext = Depends(get_tenant_context)):
+ profile_id = tenant.profile_id
+ role = tenant.global_role
+ with get_db() as conn:
+ cur = get_cursor(conn)
+ row_del = _fetch_training_module_row(cur, module_id)
+ _module_assert_writable(cur, row_del, profile_id, role)
+ cur.execute("DELETE FROM training_modules WHERE id = %s", (module_id,))
+ conn.commit()
+ return {"ok": True}
diff --git a/backend/routers/training_planning.py b/backend/routers/training_planning.py
index 78bc771..49e492f 100644
--- a/backend/routers/training_planning.py
+++ b/backend/routers/training_planning.py
@@ -8,6 +8,7 @@ from datetime import date, timedelta
from typing import Any, Dict, List, Optional
from fastapi import APIRouter, Depends, HTTPException, Query
+from psycopg2.extras import Json as PsycopgJson
from db import get_db, get_cursor, r2d
from tenant_context import TenantContext, get_tenant_context, library_content_visibility_sql
@@ -17,6 +18,9 @@ from club_tenancy import (
is_platform_admin,
library_content_visible_to_profile,
)
+from routers.training_modules import load_training_module_for_apply
+
+from routers.exercises import load_combination_slots_for_exercise
router = APIRouter(prefix="/api", tags=["training_planning"])
@@ -39,12 +43,28 @@ def _optional_positive_int(val, field_name: str) -> Optional[int]:
def _validate_variant_for_exercise(cur, exercise_id: Optional[int], variant_id: Optional[int]):
+ if not exercise_id:
+ if variant_id:
+ raise HTTPException(
+ status_code=400, detail="exercise_variant_id nur zusammen mit exercise_id erlaubt"
+ )
+ return
+ cur.execute(
+ "SELECT COALESCE(exercise_kind, 'simple') AS exercise_kind FROM exercises WHERE id = %s",
+ (int(exercise_id),),
+ )
+ ek_row = cur.fetchone()
+ if not ek_row:
+ raise HTTPException(status_code=400, detail="Übung nicht gefunden")
+ if str(r2d(ek_row).get("exercise_kind") or "simple").strip().lower() == "combination":
+ if variant_id:
+ raise HTTPException(
+ status_code=400,
+ detail="Kombinationsübungen haben keine Varianten — bitte exercise_variant_id weglassen",
+ )
+ return
if not variant_id:
return
- if not exercise_id:
- raise HTTPException(
- status_code=400, detail="exercise_variant_id nur zusammen mit exercise_id erlaubt"
- )
cur.execute(
"SELECT 1 FROM exercise_variants WHERE id = %s AND exercise_id = %s",
(variant_id, exercise_id),
@@ -390,6 +410,18 @@ def _normalize_assistant_trainer_profile_ids(
)
return uniq
+def _normalize_planning_method_profile_payload(raw) -> Optional[Dict[str, Any]]:
+ """None = Katalog wirksam; Dict = Snapshot fuer diese Platzierung."""
+ if raw is None:
+ return None
+ if isinstance(raw, dict):
+ return dict(raw)
+ raise HTTPException(
+ status_code=400,
+ detail="planning_method_profile muss ein JSON-Objekt oder null sein",
+ )
+
+
# Nachverfolgbarkeit: Übernahmen aus Rahmenprogramm über origin_framework_slot_id
_ORIGIN_LINEAGE_JOIN = """
LEFT JOIN training_framework_slots origin_slot ON origin_slot.id = tu.origin_framework_slot_id
@@ -403,6 +435,19 @@ _ORIGIN_LINEAGE_FIELDS = """
"""
+def _optional_source_training_module_id_payload(raw_val) -> Optional[int]:
+ """Erlaubt None; sonst positives int (FK-Verletzung bei ungültigem Modul möglich)."""
+ if raw_val is None or raw_val == "":
+ return None
+ try:
+ i = int(raw_val)
+ except (TypeError, ValueError):
+ return None
+ if i < 1:
+ return None
+ return i
+
+
def _fetch_sections(cur, unit_id: int) -> List[Dict[str, Any]]:
cur.execute(
"""
@@ -420,7 +465,10 @@ def _fetch_sections(cur, unit_id: int) -> List[Dict[str, Any]]:
"""
SELECT tusi.*,
e.title AS exercise_title,
+ e.exercise_kind AS exercise_kind,
e.summary AS exercise_summary,
+ e.method_archetype AS catalog_method_archetype,
+ e.method_profile AS catalog_method_profile,
(
SELECT fa.name FROM exercise_focus_areas efa
JOIN focus_areas fa ON fa.id = efa.focus_area_id
@@ -428,16 +476,34 @@ def _fetch_sections(cur, unit_id: int) -> List[Dict[str, Any]]:
ORDER BY efa.is_primary DESC NULLS LAST, fa.name ASC
LIMIT 1
) AS exercise_focus_area,
- ev.variant_name AS exercise_variant_name
+ ev.variant_name AS exercise_variant_name,
+ tm.title AS source_module_title
FROM training_unit_section_items tusi
LEFT JOIN exercises e ON tusi.exercise_id = e.id
LEFT JOIN exercise_variants ev ON tusi.exercise_variant_id = ev.id
+ LEFT JOIN training_modules tm ON tm.id = tusi.source_training_module_id
WHERE tusi.section_id = %s
ORDER BY tusi.order_index
""",
(sec["id"],),
)
sec["items"] = [r2d(r) for r in cur.fetchall()]
+ for it in sec["items"]:
+ if it.get("item_type") != "exercise":
+ continue
+ cmp_raw = it.get("catalog_method_profile")
+ if not isinstance(cmp_raw, dict):
+ it["catalog_method_profile"] = {}
+ else:
+ it["catalog_method_profile"] = dict(cmp_raw)
+ ek = str(it.get("exercise_kind") or "simple").strip().lower()
+ if ek == "combination" and it.get("exercise_id"):
+ try:
+ it["combination_slots"] = load_combination_slots_for_exercise(cur, int(it["exercise_id"]))
+ except (TypeError, ValueError):
+ it["combination_slots"] = []
+ else:
+ it["combination_slots"] = []
secs.append(sec)
return secs
@@ -452,28 +518,33 @@ def _sections_clone_payload(cur, unit_id: int) -> List[Dict[str, Any]]:
itype = it.get("item_type") or ("exercise" if it.get("exercise_id") else "note")
oix = it.get("order_index")
if itype == "note":
- items_clean.append(
- {
- "item_type": "note",
- "order_index": oix,
- "note_body": it.get("note_body") or "",
- }
- )
+ note_item = {
+ "item_type": "note",
+ "order_index": oix,
+ "note_body": it.get("note_body") or "",
+ }
+ sm = _optional_source_training_module_id_payload(it.get("source_training_module_id"))
+ if sm is not None:
+ note_item["source_training_module_id"] = sm
+ items_clean.append(note_item)
continue
if itype != "exercise" or not it.get("exercise_id"):
continue
- items_clean.append(
- {
- "item_type": "exercise",
- "order_index": oix,
- "exercise_id": it["exercise_id"],
- "exercise_variant_id": it.get("exercise_variant_id"),
- "planned_duration_min": it.get("planned_duration_min"),
- "actual_duration_min": it.get("actual_duration_min"),
- "notes": it.get("notes"),
- "modifications": it.get("modifications"),
- }
- )
+ ex_item = {
+ "item_type": "exercise",
+ "order_index": oix,
+ "exercise_id": it["exercise_id"],
+ "exercise_variant_id": it.get("exercise_variant_id"),
+ "planned_duration_min": it.get("planned_duration_min"),
+ "actual_duration_min": it.get("actual_duration_min"),
+ "notes": it.get("notes"),
+ "modifications": it.get("modifications"),
+ "planning_method_profile": it.get("planning_method_profile"),
+ }
+ sm = _optional_source_training_module_id_payload(it.get("source_training_module_id"))
+ if sm is not None:
+ ex_item["source_training_module_id"] = sm
+ items_clean.append(ex_item)
out.append(
{
"title": sec.get("title"),
@@ -568,6 +639,94 @@ def _hydrate_training_unit_payload(cur, unit: Dict[str, Any]) -> Dict[str, Any]:
return unit
+def _resolve_training_unit_section_id(cur, unit_id: int, section_order_index: int) -> int:
+ cur.execute(
+ """
+ SELECT id FROM training_unit_sections
+ WHERE training_unit_id = %s AND order_index = %s
+ """,
+ (unit_id, section_order_index),
+ )
+ r = cur.fetchone()
+ if not r:
+ raise HTTPException(
+ status_code=400, detail="Abschnitt für diese Reihenfolge nicht gefunden"
+ )
+ return int(r["id"])
+
+
+def _append_copied_module_items_to_section(
+ cur,
+ section_id: int,
+ module_items: List[Dict[str, Any]],
+ source_training_module_id: int,
+) -> None:
+ """Hängt kopierte Modul‑Items ans Ende eines Abschnitts (section_order_index in API)."""
+ cur.execute(
+ """
+ SELECT COALESCE(MAX(order_index), -1) AS mo
+ FROM training_unit_section_items
+ WHERE section_id = %s
+ """,
+ (section_id,),
+ )
+ row = cur.fetchone()
+ start = int(row["mo"]) + 1 if row and row["mo"] is not None else 0
+
+ for i, mi in enumerate(module_items):
+ oi = start + i
+ itype = mi.get("item_type")
+ if itype == "note":
+ body = mi.get("note_body")
+ if body is None:
+ body = ""
+ cur.execute(
+ """
+ INSERT INTO training_unit_section_items (
+ section_id, order_index, item_type,
+ exercise_id, exercise_variant_id,
+ planned_duration_min, actual_duration_min,
+ notes, modifications, note_body, source_training_module_id
+ ) VALUES (%s, %s, 'note',
+ NULL, NULL, NULL, NULL, NULL, NULL, %s, %s)
+ """,
+ (section_id, oi, body, source_training_module_id),
+ )
+ continue
+
+ eid = mi.get("exercise_id")
+ if not eid:
+ continue
+ eid = int(eid)
+ vid = mi.get("exercise_variant_id")
+ if vid is not None:
+ vid = int(vid)
+ else:
+ vid = None
+ _validate_variant_for_exercise(cur, eid, vid)
+ cur.execute(
+ """
+ INSERT INTO training_unit_section_items (
+ section_id, order_index, item_type,
+ exercise_id, exercise_variant_id,
+ planned_duration_min, actual_duration_min,
+ notes, modifications, note_body,
+ source_training_module_id, planning_method_profile
+ ) VALUES (%s, %s, 'exercise',
+ %s, %s, %s, NULL, %s, NULL, NULL, %s, NULL)
+ """,
+ (
+ section_id,
+ oi,
+ eid,
+ vid,
+ mi.get("planned_duration_min"),
+ mi.get("notes"),
+ source_training_module_id,
+ ),
+ )
+
+
def _insert_section_items(cur, section_id: int, items_in: Optional[List[Any]], start_order: int = 0):
if items_in is None:
items_in = []
@@ -582,18 +741,19 @@ def _insert_section_items(cur, section_id: int, items_in: Optional[List[Any]], s
body = raw.get("note_body")
if body is None:
body = ""
+ src_mod = _optional_source_training_module_id_payload(raw.get("source_training_module_id"))
cur.execute(
"""
INSERT INTO training_unit_section_items (
section_id, order_index, item_type,
exercise_id, exercise_variant_id,
planned_duration_min, actual_duration_min,
- notes, modifications, note_body
+ notes, modifications, note_body, source_training_module_id
) VALUES (%s, %s, 'note',
- NULL, NULL, NULL, NULL, NULL, NULL, %s
+ NULL, NULL, NULL, NULL, NULL, NULL, %s, %s
)
""",
- (section_id, order_ix, body),
+ (section_id, order_ix, body, src_mod),
)
continue
@@ -603,16 +763,27 @@ def _insert_section_items(cur, section_id: int, items_in: Optional[List[Any]], s
eid = int(eid)
vid = _optional_positive_int(raw.get("exercise_variant_id"), "exercise_variant_id")
_validate_variant_for_exercise(cur, eid, vid)
+ cur.execute(
+ """SELECT COALESCE(exercise_kind, 'simple') AS k FROM exercises WHERE id = %s""",
+ (eid,),
+ )
+ er = cur.fetchone()
+ ek = str(er["k"] if er and er.get("k") is not None else "simple").strip().lower()
+ planning_mp = _normalize_planning_method_profile_payload(raw.get("planning_method_profile"))
+ if ek != "combination":
+ planning_mp = None
+ planning_sql_val = PsycopgJson(planning_mp) if planning_mp is not None else None
+ src_mod = _optional_source_training_module_id_payload(raw.get("source_training_module_id"))
cur.execute(
"""
INSERT INTO training_unit_section_items (
section_id, order_index, item_type,
exercise_id, exercise_variant_id,
planned_duration_min, actual_duration_min,
- notes, modifications, note_body
+ notes, modifications, note_body,
+ source_training_module_id, planning_method_profile
) VALUES (%s, %s, 'exercise',
- %s, %s, %s, %s, %s, %s, NULL
- )
+ %s, %s, %s, %s, %s, %s, NULL, %s, %s)
""",
(
section_id,
@@ -623,6 +794,8 @@ def _insert_section_items(cur, section_id: int, items_in: Optional[List[Any]], s
raw.get("actual_duration_min"),
raw.get("notes"),
raw.get("modifications"),
+ src_mod,
+ planning_sql_val,
),
)
@@ -1443,6 +1616,46 @@ def get_training_unit(unit_id: int, tenant: TenantContext = Depends(get_tenant_c
return unit
+@router.post("/training-units/{unit_id}/apply-training-module")
+def apply_training_module_to_training_unit(
+ unit_id: int, data: dict, tenant: TenantContext = Depends(get_tenant_context)
+):
+ """Kopiert die Positionen eines Trainingsmoduls ans Ende eines Abschnitts (lokal bearbeitbar)."""
+ profile_id = tenant.profile_id
+ role = tenant.global_role
+ if not _has_planning_role(role):
+ raise HTTPException(status_code=403, detail="Nur Trainer dürfen Module übernehmen")
+
+ module_id_raw = data.get("module_id")
+ if module_id_raw is None or module_id_raw == "":
+ raise HTTPException(status_code=400, detail="module_id ist Pflicht")
+ try:
+ module_id = int(module_id_raw)
+ except (TypeError, ValueError):
+ raise HTTPException(status_code=400, detail="module_id ungültig")
+
+ soy = data.get("section_order_index")
+ try:
+ section_order_index = int(soy)
+ except (TypeError, ValueError):
+ raise HTTPException(status_code=400, detail="section_order_index ist Pflicht (Ganzzahl)")
+ if section_order_index < 0:
+ raise HTTPException(status_code=400, detail="section_order_index ungültig")
+
+ with get_db() as conn:
+ cur = get_cursor(conn)
+ unit_row = _training_unit_guard_row(cur, unit_id)
+ _assert_training_unit_permission(cur, unit_row, profile_id, role)
+
+ section_id = _resolve_training_unit_section_id(cur, unit_id, section_order_index)
+ mod_items, src_mid = load_training_module_for_apply(cur, module_id, profile_id, role)
+ _append_copied_module_items_to_section(cur, section_id, mod_items, src_mid)
+ _promote_private_exercises_used_in_unit(cur, unit_id, profile_id, role)
+ conn.commit()
+
+ return get_training_unit(unit_id, tenant)
+
+
@router.post("/training-units")
def create_training_unit(data: dict, tenant: TenantContext = Depends(get_tenant_context)):
profile_id = tenant.profile_id
diff --git a/backend/version.py b/backend/version.py
index bee357d..b011ef4 100644
--- a/backend/version.py
+++ b/backend/version.py
@@ -1,13 +1,13 @@
# Shinkan Jinkendo Version Information
-APP_VERSION = "0.8.94"
-BUILD_DATE = "2026-05-11"
-DB_SCHEMA_VERSION = "20260511053"
+APP_VERSION = "0.8.110"
+BUILD_DATE = "2026-05-12"
+DB_SCHEMA_VERSION = "20260512057"
MODULE_VERSIONS = {
- "legal_documents": "1.2.0", # jsPDF-Download auf LegalPage (oeffentlich) + Admin; Abschnitts-Sortierung/-Einfuegen
+ "legal_documents": "1.4.0", # Admin: Live-Vorschau pro Abschnitt + modale Vollvorschau (Editor + Dokumentenliste)
"auth": "1.2.3", # P-05b: reset-password min_length=8 via Pydantic PasswordResetConfirm
- "profiles": "1.7.0", # exercise_list_prefs JSONB (Standard Übungsfilter); Patch via ProfileUpdate + Json()
+ "profiles": "1.8.0", # training_planning_prefs JSONB (Planungs-UI); Patch via ProfileUpdate + Json(), Migration 055
"tenant_context": "1.0.5", # Plattform-Admin: effective_club ohne Header aus Profil active_club_id wenn Verein existiert
"clubs": "0.4.1", # Alle geschützten Endpoints Depends(get_tenant_context); profile_id/role aus TenantContext
"club_memberships": "1.0.1", # Depends(get_tenant_context)
@@ -21,10 +21,11 @@ MODULE_VERSIONS = {
"groups": "0.1.0",
"skills": "0.1.0",
"methods": "0.1.0",
- "exercises": "2.23.0", # P-11: enrich_exercise_detail + download_file blocken Legal-Hold-Assets (451)
+ "exercises": "2.27.3", # load_combination_slots_for_exercise (gemeinsam mit GET Übung); Hydrate für Planung
"training_units": "0.2.0",
"training_programs": "0.1.0",
- "planning": "0.8.1", # Vorlagen Leseprüfung library_content_visible_to_profile
+ "planning": "0.9.3", # GET training-units/:id Sektions-Items: combination_slots + Kandidaten-Titel für Druck/Run
+ "training_modules": "1.0.0",
"import_wiki": "1.0.0",
"admin": "1.0.0",
"membership": "1.0.0",
@@ -34,6 +35,121 @@ MODULE_VERSIONS = {
}
CHANGELOG = [
+ {
+ "version": "0.8.110",
+ "date": "2026-05-12",
+ "changes": [
+ "GET /api/training-units/:id: Bei Kombinationsübungen werden `combination_slots` inkl. Kandidaten-Titel mitgeliefert (für Plan & Ablauf / Druck).",
+ "Hilfsfunktion `load_combination_slots_for_exercise` im exercises-Router; GET Übung nutzt dieselbe Ladelogik.",
+ ],
+ },
+ {
+ "version": "0.8.109",
+ "date": "2026-05-12",
+ "changes": [
+ "Kombination: „Serien“ standardmäßig 1 (Formular/API); Archetyp kann via `ARCHETYPE_DEFAULT_REP_SERIES_COUNT` andere Vorgaben setzen; Profil‑Editor zeigt Fallback.",
+ ],
+ },
+ {
+ "version": "0.8.108",
+ "date": "2026-05-12",
+ "changes": [
+ "Kombination rep/manual: Feld „Pause zw. Serien“ nur ab 2 Serien sichtbar und speicherbar; Hinweis unterscheidet Wechsel zur nächsten Station; API verwirft intra_rep_rest_sec bei nur einer Serie.",
+ ],
+ },
+ {
+ "version": "0.8.107",
+ "date": "2026-05-12",
+ "changes": [
+ "Kombination Wiederholungsziel: `rep_series_count` in `slot_profiles_v1` (mehrere Serien à Ziel‑Wdh.); Formular‑ und Profil‑Editor‑Felder; Pause als „zwischen Serien“ beschriftet; Coach‑Zusammenfassung angepasst.",
+ ],
+ },
+ {
+ "version": "0.8.106",
+ "date": "2026-05-12",
+ "changes": [
+ "Kombinationsübung: Stationssteuerung `advance_mode` in `slot_profiles_v1` (zeitlich / Ziel‑Wiederholungen / Coach ohne Arbeitsuhr); Übungsformular + Planungs‑Profil‑Editor; API‑Payload verwirft Arbeit‑Sekunden außer bei Zeitmodus; Coach zeigt verkürzte Planzeile je Station.",
+ ],
+ },
+ {
+ "version": "0.8.105",
+ "date": "2026-05-12",
+ "changes": [
+ "Übungsbearbeitung Kombi: Stationen mit Pool per Modal (nur Einzelübungen), Zeiten pro Station in derselben Karte, Drag&Drop + Pfeile statt Index; API schreibt slot_index aus Reihenfolge; Gesamtdurchläufe bei Zirkel/Sequenz/Parcours/Parallel klar beschriftet.",
+ ],
+ },
+ {
+ "version": "0.8.104",
+ "date": "2026-05-12",
+ "changes": [
+ "Kombinations‑Ablaufprofil UX: Stationszeilen (slot_profiles_v1); Schnellwahlen Arbeit↔Pause (Zirkel + Intervall); Planungs‑Override ohne JSON; Übungsformular: Reihenfolge Stationen dann Ablaufprofil.",
+ "Arbeitspapier: `.claude/docs/working/COMBINATION_TIMING_PROFILE_PLAN.md`.",
+ ],
+ },
+ {
+ "version": "0.8.103",
+ "date": "2026-05-12",
+ "changes": [
+ "Trainingsplanung: bei Kombinationszeilen optionales `planning_method_profile` (Migration 057); Planungs-Editor mit Ablaufprofil-Details, „wie Katalog“ / „aus Katalog kopieren“; Payload/Coach-PUT übernehmen Snapshot.",
+ ],
+ },
+ {
+ "version": "0.8.102",
+ "date": "2026-05-12",
+ "changes": [
+ "Kombinationsübung beim Anlegen/Bearbeiten: archetypbezogenes Ablaufprofil (geführt) + eingeklappt Roh-JSON (`CombinationMethodProfileEditor`); Schlüsselmanifest `combinationMethodProfileUi.js`. Coach: angelegtes method_profile unter Stationenliste lesbar.",
+ ],
+ },
+ {
+ "version": "0.8.101",
+ "date": "2026-05-12",
+ "changes": [
+ "Training-Coach bei Kombinationsübungen: Stationen/Kandidaten mit geladenem Katalog (Kurzbeschreibung, aufklappbar Ablauf/Trainerhinweise); Archetyp-spezifischer Coach-Hilfstext; Archetyp-Labels aus `combinationArchetypes.js`.",
+ ],
+ },
+ {
+ "version": "0.8.100",
+ "date": "2026-05-12",
+ "changes": [
+ "Planungs-API/UI: Kombinationsübungen in Trainingsseinheiten (exercise_kind in Sektions-Responses; PATCH verbietet exercise_variant_id für combination); ExercisePicker ohne simple-only Filter, Badge Kombination.",
+ ],
+ },
+ {
+ "version": "0.8.99",
+ "date": "2026-05-12",
+ "changes": [
+ "exercises Phase 2 (Kombinationsübungen): Migration 056 (`exercise_kind`, `method_archetype`, `method_profile`; Tabellen Slots/Kandidaten); CRUD über POST/PUT/GET Übung mit `combination_slots`; Liste-Filter `exercise_kind_any`; Varianten-Endpoints verbieten `exercise_kind=combination`.",
+ ],
+ },
+ {
+ "version": "0.8.98",
+ "date": "2026-05-12",
+ "changes": [
+ "profiles: `training_planning_prefs` JSONB (Migration 055), Patch via PUT Profil — z.B. Darstellung kopierter Trainingsmodule in der Planungs-UI (nutzerspezifisch).",
+ ],
+ },
+ {
+ "version": "0.8.97",
+ "date": "2026-05-12",
+ "changes": [
+ "Trainingsmodule (Phase 1): Bibliothek `training_modules` + `training_module_items` (Migration 054); REST `/api/training-modules`; Übernahme in Einheiten per `POST /api/training-units/{id}/apply-training-module`; Herkunft `source_training_module_id` auf kopierten Sektions-Items; UI unter /planning/training-modules und Übernahme-Dialog in der Trainingsplanung.",
+ "Umsetzungsplan: `.claude/docs/working/TRAINING_MODULES_IMPLEMENTATION_PLAN.md`.",
+ ],
+ },
+ {
+ "version": "0.8.96",
+ "date": "2026-05-12",
+ "changes": [
+ "P-01 Admin Rechtstexte: Live-Vorschau je Abschnitt (Markdown) neben dem Editor; modale „Vollständige Vorschau“ aus dem Formular; Augen-Symbol in der Dokumentenliste für die gerenderte Ansicht (API-Laden).",
+ ],
+ },
+ {
+ "version": "0.8.95",
+ "date": "2026-05-12",
+ "changes": [
+ "P-01 Rechtstexte: Abschnitte in der Ausgabe mit fortlaufender §1, §2, … (nur Darstellung/PDF, nicht in der DB); Fließtext mit Markdown (react-markdown) inkl. PDF-Rendering (fett/kursiv, Listen, Links, Codeblöcke).",
+ ],
+ },
{
"version": "0.8.94",
"date": "2026-05-11",
diff --git a/docs/FACHLICHE_NUTZERFUNKTIONEN.md b/docs/FACHLICHE_NUTZERFUNKTIONEN.md
new file mode 100644
index 0000000..d98dc6a
--- /dev/null
+++ b/docs/FACHLICHE_NUTZERFUNKTIONEN.md
@@ -0,0 +1,130 @@
+# Shinkan Jinkendo – Fachliche Nutzerfunktionen (Ist-Stand)
+
+**Zweck:** Überblick über die **wesentlichen, produktiv nutzbaren Funktionen** aus Nutzer- und Fachperspektive – zur Weitergabe an Design, Product Discovery oder externe Fachplanung.
+
+**Technischer Detailstand:** App-Version und Schema siehe `backend/version.py` (Stand Code: **0.8.101**, **DB_SCHEMA_VERSION** siehe dort).
+
+**Vertiefung:** Domänenmodell `.claude/docs/functional/DOMAIN_MODEL.md`, Lieferdetal `.claude/docs/library/FEATURES_DELIVERED_2026-Q2.md`, Projektstatus `.claude/docs/PROJECT_STATUS.md`, Entwickler-Handover `docs/HANDOVER.md`.
+
+---
+
+## 1. Produktauftrag und Zielgruppe
+
+**Shinkan Jinkendo** ist eine **Web-Applikation für Trainer, Vereinsadmins und Inhaltsverantwortliche** in der Kampfsport- und Trainingsplanung: zentrale Übungs- und Methodenverwaltung, strukturierte **Trainingsplanung für Gruppen**, wiederverwendbare **Rahmenprogramme**, sowie **Governance** von Inhalten (Sichtbarkeit, Vereinszuordnung, Plattform-Inhalte).
+
+**Explizit keine persönliche Sportler-App:** Es geht nicht um individuelles Leistungstracking von Endnutzern oder um ein Athleten-Tagebuch; der Fokus liegt auf **vereinlicher/trainersicher Organisation von Wissen und Ablaufplänen**.
+
+---
+
+## 2. Rollen (vereinfachte Nutzerbilder)
+
+Die sichtbaren Funktionen hängen von **Rolle** und **Kontext** ab (eingeloggter Nutzer, aktiver Verein, Plattform- vs. Vereins-Admin).
+
+| Rollenprofil (fachlich) | Typische Aufgaben in der App |
+|-------------------------|------------------------------|
+| **Trainer / Redakteur** | Übungen anlegen und pflegen, medienreich beschreiben, filtern/suchen; Trainingseinheiten für Gruppen planen; Rahmenprogramme nutzen oder mitgestalten (je nach Berechtigung); Medienbibliothek nutzen. |
+| **Vereinsadmin** | Vereinsdaten, Mitgliedschaften, ggf. vereinsgebundene Inhalte und Medien; kann je nach Implementierung **Inhaltsmeldungen zu Vereinsmedien** bearbeiten und **Legal Hold** für Vereinsmedien auslösen. |
+| **Plattform-Admin** | Globale Kataloge, Hierarchien, Importe, Nutzerverwaltung (soweit freigeschaltet); **Posteingang** inkl. organisationsbezogener Meldungen; Reifegradmodelle / Matrix-Stack. |
+| **Superadmin** | Stärkste technische Rolle: u. a. **offizielle Plattform-Inhalte** (`official`), tiefe Medien-Lifecycle-Operationen, ausgewählte Hochrisiko-Aktionen (z. B. bestimmte Legal-Hold-Fälle). |
+
+**Aktiver Verein:** Nutzer mit Vereinsbezug arbeiten oft im Kontext eines **gewählten aktiven Vereins** (Profil, API-Header); das beeinflusst Sichtbarkeit von Inhalten und Mandantenlogik.
+
+---
+
+## 3. Hauptnavigation (Nutzerpfade)
+
+Über die **Hauptnavigation** (mobil und Desktop) sind u. a. erreichbar:
+
+- **Übersicht** – Einstieg / Dashboard.
+- **Posteingang** – für berechtigte Nutzer: **Änderungs- und Organisationsanfragen** sowie **Inhaltsmeldungen** (Workflow, Status, Archiv).
+- **Übungen** – Katalogarbeit, Suche, Filter, Detail, Bearbeitung; **Progressionsgraphen** zwischen Übungen; **Fähigkeiten** (Skills) als verknüpfte Dimension.
+- **Planung** – Kalender-/Listenlogik für **Trainingseinheiten** (Sektionen, Übungen, optional **Übungsvarianten**); **Trainingsrahmen (Bibliothek)** mit Zielen und Slots; **Durchführungsansicht** und **Coaching-Modus** pro Einheit (je Route).
+- **Medien** – zentrale **Medienbibliothek** (Filter, Suche, Tags, Lifecycle, Copyright-Hinweise; rollenabhängige Bearbeitung).
+- **Vereine** – Organisation: Vereine, Struktur, Gruppen (soweit für den Nutzer freigeschaltet).
+- **Einstellungen** – Profil, Systeminfos, ggf. Rechtstexte; **Trainer-Kontexte** separat (Route `trainer-contexts`).
+- **Admin** (nur Admin-Rolle) – Plattformbereich: Nutzer, Hierarchie/Kataloge, Reifegradmodelle, MediaWiki-Import, **Rechtstexte/P-01** u. a.
+
+Öffentlich bzw. ohne volle App: **Impressum, Datenschutz, Nutzungsbedingungen, Medienrichtlinie**; Login/Registrierung/Verifizierung.
+
+---
+
+## 4. Funktionsblöcke im Detail (fachlich)
+
+### 4.1 Übungen (Kernobjekt)
+
+- **Anlegen, Bearbeiten, Archivieren/Löschen** je nach Rolle und Sichtbarkeit.
+- **Mehrdimensionale Einordnung:** Fokusbereiche, Stilrichtungen, Trainingsstile, Zielgruppen, **Fähigkeiten mit Stufen**; Suche und Filter über diese Dimensionen.
+- **Übungsvarianten:** mehrere Ausprägungen einer Übung (z. B. Aufbau, Schwierigkeit, Material), mit Reihenfolge und optionaler **Voraussetzungsvariante**.
+- **Progressionsgraph:** gerichtete Beziehungen **von Übung zu Übung** (und Variantenbezug), Pflege in der Übungswelt; unterstützt didaktische „weiter“-Ketten.
+- **Medien an der Übung:** Upload, Einbettung, Verknüpfung aus dem **Archiv**; Darstellung in Detail- und Bearbeitungsansicht.
+- **Rich-Text-Felder** (Ablauf, Ziele, Hinweise): **Inline-Verweise auf verknüpfte Medien** über eine einheitliche Platzhalter-/Renderlogik (konsistent mit Archiv-Governance).
+- **Exercise Blocks** („Bausteine“) und gespeicherte Suchpräferenzen, wo implementiert.
+- **Kombinationsübungen** („combination“, Migration 056): Sonderform im Übungskatalog mit **Stationen/Slots**, **Trainingsmethode-/Archetyp** (`method_archetype`), optionalem strukturierten **Ablaufprofil** (`method_profile`). In der Planung wie eine Übung ohne Variante einsetzbar; im **Coach** werden Stations-Kandidaten und Archetyp-Hilfstexte angezeigt (Ausbauschritte B/C nach Fachspez § 10.4 dokumentiert unter `.claude/docs/functional/… Kombinationsübungen Spezifikation V2.md` Anhang A).
+
+### 4.2 Fähigkeiten, Methoden, Kataloge
+
+- **Globaler Fähigkeitskatalog** mit hierarchischer Struktur (Kategorien, Stufen); Zuordnung zu Übungen.
+- **Trainingsmethoden-Katalog** (bestehende Domäne).
+- **Admin/Katalog-Pflege** für Fokusbereiche, Stile, Zielgruppen und Zusammenhänge (Plattform-Admin-Bereich).
+
+### 4.3 Reifegradmodelle (Fähigkeitsmatrix)
+
+- **Matrixbasierte Modelle** mit Stufen und Zelltexten; **kontextsensitive Auflösung** (Fokus, optional Stilrichtung, Trainingsart) über Bindings.
+- **Export/Import** einzelner Modelle und **Komplett-Stack** (Admin-Werkzeuge) für Übertrag zwischen Umgebungen oder Backup.
+
+### 4.4 Trainingsplanung
+
+- **Trainingseinheiten** als planbare Objekte mit **Sektionen** und **Einträgen** (Übungen, ggf. mit **Variante** und Metadaten wie Dauer).
+- **Trainingsvorlagen / Mikrovorlagen** (wo eingerichtet): Struktur wiederverwenden.
+- **Trainingsrahmenprogramm (Bibliothek):** übergeordnete Programme mit **Zielen** und **Slots**; Slot-Inhalt technisch als **Blueprint-Trainingsunit** abgebildet.
+- **Materialisierung:** aus einem Rahmen-Slot kann eine **konkrete Kalender-Einheit** für eine Gruppe erzeugt werden (API vorhanden; UI-Anbindung kann erweitert werden).
+- **Durchführung:** Ansicht zum Abarbeiten einer Einheit; **Coaching-Modus** als separater Erlebnispfad (generischer Zeit-Block pro Platzierung); bei **Kombinationsübungen** zusätzliche **Stations-/Kandidaten-Schicht und Archetyp-Hinweise** siehe Kombination-Fachspez **Anhang A** (implementierter Umfang vs. nächste Stufen).
+
+### 4.5 Medienbibliothek und Archiv
+
+- **Zentrale Medienverwaltung:** Suche, Filter (u. a. Lifecycle, Medientyp, Verein für Admins), Tags, Copyright-Felder.
+- **Lifecycle:** aktive Nutzung, Papierkorb-Stufen, Wiederherstellung; endgültiges Entfernen stark eingeschränkt (Superadmin-Kontext).
+- **Governance:** Sichtbarkeit (z. B. privat, vereinsbezogen, **official**); **official** ist fachlich „Plattform offiziell“ und an **Superadmin** gebunden.
+- **Rechtliche Sofortmaßnahme:** **Legal Hold** kann Medien vor automatisiertem Lifecycle schützen (Fälle aus Meldungen oder Admin-Prozessen).
+
+### 4.6 Organisation und Mitgliedschaft
+
+- **Vereine (Clubs)** mit Struktur (Sparten/Divisions, Trainingsgruppen) je nach Ausprägung.
+- **Beitritts- und Mitgliedschaftslogik** (Requests, Rollen) für mandantenfähige Zusammenarbeit.
+
+### 4.7 Governance von Übungsinhalten
+
+- **Änderungsanfragen** (Content Change Requests) für vorgeschlagene Änderungen an Inhalten – Einreichung und Bearbeitung über Posteingang/Admin-Prozesse (Detailtiefe siehe Fachdoku).
+- **Sichtbarkeits- und Statusmodelle** für Übungen (Entwurf, veröffentlicht, archiviert – konkrete Werte siehe Datenmodell).
+
+### 4.8 Inhaltsmeldungen (P-13, vertrauens- und compliance-orientiert)
+
+- **Melden** von problematischen Inhalten (auch aus Medien- und Übungskontexten; **official**-Medien teils ohne Login meldbar).
+- **Posteingang für Admins:** Eingang neuer Meldungen, **Statusworkflow** (z. B. eingereicht, in Prüfung, erledigt/abgelehnt), Notizen, **Wiedereröffnen**; getrennte Darstellung abgeschlossener Fälle (Archiv).
+- **Priorisierung** bei sensiblen Kategorien (Minderjährige, illegaler Inhalt, Jugendschutz – fachlich automatisch höher gewichtet).
+- Anbindung an **Legal Hold** und Audit-Spuren im Medien-Journal wo vorgesehen.
+
+### 4.9 Import und Plattform-Werkzeuge
+
+- **MediaWiki-basierter Import** von Übungsinhalten mit Tracking und Duplikat-Referenzen (Admin).
+- **Plattformnutzerverwaltung** und **Rechtstexte** mit editorseitiger und listenbasierter Vorschau (Markdown, strukturierte Ausgabe inkl. PDF-Darstellung – siehe technische Versionsnotizen).
+
+---
+
+## 5. Bekannte Lücken und Planungshinweise (kurz)
+
+Nicht als „broken“ gemeint, sondern als **typische nächste Ausbaustellen** für Product/Design:
+
+- Kalender-UX: **„Aus Rahmen übernehmen“** flächendeckend und ggf. bulkfähig anbinden.
+- **Policies** für geteilte Rahmen (Wer darf Bibliotheks-Rahmen sehen/kopieren?).
+- **Skill-Kategorie-Admin-UI**, **Dark Mode/Responsive/PWA-Ausbau**, **KI-Suche** über Volltext hinaus – je nach Backlog.
+- **Coach / Kombination:** nächste Stufen **Zeitleisten-Splitting** und **Archetyp-Timer** (Fachspez § 10.4 Stufe B/C; Umsetzungsplan Phase 4b–d); **geführtes Erfassen** von `method_profile` im Übungseditor.
+
+---
+
+## 6. Änderungshistorie dieser Zusammenfassung
+
+| Datum | Änderung |
+|-------|----------|
+| 2026-05-12 | Erstfassung für Übergabe an fachliches Design; Abgleich mit Code-Navigation, `version.py`, `HANDOVER.md`, `FEATURES_DELIVERED`, `DOMAIN_MODEL`. |
+| 2026-05-12 | Kombinationsübungen + Coaching Stufe A; Verweise auf Fachspezifikation (`…Kombinationsuebungen…` V3 Anhang A) und `TRAINING_MODULES_IMPLEMENTATION_PLAN.md`. |
diff --git a/docs/HANDOVER.md b/docs/HANDOVER.md
index 1acde31..b9c3e97 100644
--- a/docs/HANDOVER.md
+++ b/docs/HANDOVER.md
@@ -1,7 +1,7 @@
# Shinkan Jinkendo – Entwicklungsstand & Handover
-**Stand:** 2026-05-11
-**App-Version / DB-Schema:** App **0.8.94**, DB-Schema siehe `backend/version.py` (`DB_SCHEMA_VERSION`)
+**Stand:** 2026-05-12
+**App-Version / DB-Schema:** App **0.8.101**, DB-Schema siehe `backend/version.py` (`DB_SCHEMA_VERSION`)
Diese Datei ist die **Einstiegs-Doku für neue Chat-Sessions**: Anforderungen im Detail stehen in `.claude/docs/` (siehe unten); hier der **implementierte Stand**, **Medien-Meilenstein** und **sinnvolle nächste Schritte**.
@@ -29,9 +29,12 @@ Das Schema ist gegenüber dem Code zurück: Migration **`022_skills_schema_compl
| Zugriffsschicht, Mandant, Governance | `.claude/docs/technical/ACCESS_LAYER_AND_GOVERNANCE_PLAN.md` |
| Tenant-Endpoints (Audit) | `.claude/docs/working/ACCESS_LAYER_ENDPOINT_AUDIT.md` |
| Rahmenprogramm · Planung | `.claude/docs/technical/TRAINING_FRAMEWORK_SPEC.md` |
+| **Trainingsmodule & Kombinationsübungen (Fachspez, Drift-Schutz)** | `.claude/docs/functional/Shinkan Trainingsmodule Kombinationsuebungen Spezifikation V2.md` (§ 10.2.1 Archetyp-IDs, § 10.4 Coaching-Stufen, **Anhang A** Code-Abgleich) |
+| **Umsetzungsplan** (Module/Kombination/Coach) | `.claude/docs/working/TRAINING_MODULES_IMPLEMENTATION_PLAN.md` |
| Überblick DB | `.claude/docs/technical/DATABASE_SCHEMA.md` |
| Domäne | `.claude/docs/functional/DOMAIN_MODEL.md` |
| **Lieferliste inkl. Medien** | `.claude/docs/library/FEATURES_DELIVERED_2026-Q2.md` §12 |
+| **Fachlicher Nutzerüberblick (Design/Product)** | **`docs/FACHLICHE_NUTZERFUNKTIONEN.md`** |
---
@@ -71,6 +74,12 @@ Das Schema ist gegenüber dem Code zurück: Migration **`022_skills_schema_compl
- **036 / 037:** Bibliotheks-Rahmen, Slot-Inhalt als **`training_units`** mit **`framework_slot_id`**; **`POST /api/training-units/from-framework-slot`**.
- **Code:** `training_framework_programs.py`, `training_planning.py`; Frontend **`TrainingFrameworkProgramEditPage.jsx`**, **`createTrainingUnitFromFrameworkSlot`** in `api.js`.
+### Trainingsmodule, Kombinationsübungen und Coach (Stand ~0.8.101)
+
+- **Fachspez & Drift-Schutz:** `.claude/docs/functional/Shinkan Trainingsmodule Kombinationsuebungen Spezifikation V2.md` (**§ 10.2.1** IDs, **§ 10.4** Coaching-Stufen, **Anhang A** Abgleich).
+- **Umsetzungsplan:** `.claude/docs/working/TRAINING_MODULES_IMPLEMENTATION_PLAN.md` (Phase **2** / **4** teilweise; Pakete **4a–d**).
+- **Ist kurz:** Trainingsmodule-Bibliothek (Phase **1**) umgesetzt; Kombi-Katalog (**056**) + Einplanung + Coach **Stufe A** (`CombinationCoachSlots`, `combinationArchetypes.js`). Coach **Stufe B/C** und geführtes **`method_profile`** offen — siehe Fachspez Anhang A.
+
---
## 4. Stand: Medien-Management (Ist, 2026-05-07)
@@ -105,21 +114,15 @@ Das Schema ist gegenüber dem Code zurück: Migration **`022_skills_schema_compl
---
-## 5. Geplant: Inline-Medienverlinkung (nicht umgesetzt)
+## 5. Inline-Medien im Fließtext (Spec Abschnitt 11 — umgesetzt)
-**Ziel:** Mediendarstellung **innerhalb** von Fließtext-Feldern (Ablauf, Ziele, Trainerhinweise), konsistent mit derselben **`exercise_media`‑** bzw. Asset-Governance wie die Medienliste.
+**Ist:** Platzhalter-Syntax, zentraler Render-Pfad, Modal-Picker, Größenwahl, Drag-and-Drop in den Übungstextfeldern — siehe **`MEDIA_ASSETS_AND_ARCHIVE_SPEC.md`** (Abschnitt 11), **`FEATURES_DELIVERED_2026-Q2.md`** Abschnitt 12.3 und **`PROJECT_STATUS.md`**.
-**Norm:** **`MEDIA_ASSETS_AND_ARCHIVE_SPEC.md` §11** — u. a.:
-
-- Verweis auf **`exercise_media.id`** (oder kanonisch übersetzte Markup-Syntax), **keine** zweite Sichtbarkeitslogik.
-- **Ein** zentraler Render-/Sanitize-Pfad für Übungstexte; keine verstreuten „roh `dangerouslySetInnerHTML`“-Pfade.
-- XSS/CSP: nur Allowlist-HTML und kontrollierte Player-Komponenten.
-
-**Reihenfolge:** Archiv & aktuelle Governance gelten als Basis; Inline ist die **nächste** inhaltliche Ausbaustufe für Medien (siehe **`PROJECT_STATUS.md`** Nächste Schritte).
+**Weiteres:** UX-Politik, ggf. strategische Vereinheitlichung der Referenzmodellierung (reine Asset-Referenz vs. `exercise_media`) — siehe Nächste Schritte in **`PROJECT_STATUS.md`**.
---
-## 5b. P-13: Content-Meldeverfahren (vollständig implementiert, 2026-05-11)
+## 6. P-13: Content-Meldeverfahren (vollständig implementiert, 2026-05-11)
**DSA-konformes Meldeverfahren (KRIT-03) — App 0.8.87–0.8.94.**
@@ -148,22 +151,25 @@ Das Schema ist gegenüber dem Code zurück: Migration **`022_skills_schema_compl
---
-## 6. Nächste Session — sinnvolle Arbeitspakete
+## 7. Nächste Session — sinnvolle Arbeitspakete
1. **P-13 Frontend-Verifikation:** Melde-Flow in Medienbibliothek, Inbox-Workflow (Status, Archiv, Wiedereröffnen), Club-Admin-Ansicht manuell auf Dev-System durchspielen. E-Mail-Benachrichtigungen verifizieren (SMTP-Log).
-2. **Inline §11:** Syntax festlegen (`{{exerciseMedia:id}}` → kanonisches HTML), Server normalisieren bei Speichern, einen `renderExerciseRichText()`-Pfad im Frontend.
+2. **Inline (Spec Abschnitt 11):** Basis umgesetzt — verbleibend: gezielte UX-Politik; optional Server-Normalisierung/Absicherung prüfen, falls Produkt es verlangt.
3. **Tests:** pytest für `media_assets`-Router (Leserechte, Lifecycle, `from-asset`); ggf. Snapshot der Pfad-Umzug-Logik.
4. **Retention:** Job-Dokumentation + Betrieb (ENV, Intervall); Dry-Run beschreiben.
-5. **S3/Adapter:** Speicher-Abstraktion §7 — wenn Produkt es verlangt.
+5. **S3/Adapter:** Speicher-Abstraktion (Spec Abschnitt 7) — wenn Produkt es verlangt.
6. **Rahmen/UI:** Kalender „aus Rahmen übernehmen” weiter anbinden (parallel, unabhängig von Medien).
+7. **Fachlicher Nutzerüberblick:** bei größeren UX-Änderungen **`docs/FACHLICHE_NUTZERFUNKTIONEN.md`** mitpflegen.
+8. **Kombinations-Coach (Archetyp B/C):** Fachspez § 10.4; nach Implementierung **Anhang A** + `TRAINING_MODULES_IMPLEMENTATION_PLAN.md` aktualisieren (kein Doc-Drift).
---
-## 7. Technische Referenz (kurz)
+## 8. Technische Referenz (kurz)
| Bereich | Einstieg |
|---------|----------|
-| Backend API | `backend/main.py`; u. a. **`media_assets.py`**, **`exercises.py`**, **`profiles.py`**, **`training_framework_programs.py`**, `tenant_context.py` |
+| Backend API | `backend/main.py`; u. a. **`media_assets.py`**, **`exercises.py`** (`COMBINATION_ARCHETYPE_IDS`, `enrich_exercise_detail`), **`profiles.py`**, **`training_framework_programs.py`**, `tenant_context.py` |
+| Coach-Kombination (Frontend) | `TrainingCoachPage.jsx`, `ExerciseFullContent.jsx`, `CombinationCoachSlots.jsx`, `constants/combinationArchetypes.js` |
| Migrationen | `backend/migrations/` (040+ Mitgliedschaft/Governance; **045+** Medien-Stack) |
| Frontend API | `frontend/src/utils/api.js` |
| Aktiver Verein (UI) | `frontend/src/utils/activeClub.js`, `AuthContext.jsx` |
@@ -171,7 +177,7 @@ Das Schema ist gegenüber dem Code zurück: Migration **`022_skills_schema_compl
---
-## 8. Veraltete Hinweise
+## 9. Veraltete Hinweise
`.claude/docs/working/HANDOVER_NEXT_SESSION.md` verweist auf **dieses** Dokument (`docs/HANDOVER.md`) als aktuelle Basis.
diff --git a/frontend/package.json b/frontend/package.json
index eb10a77..9ad335a 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -10,9 +10,12 @@
"dependencies": {
"jspdf": "^4.2.1",
"lucide-react": "^0.344.0",
+ "marked": "^18.0.3",
"react": "^18.3.1",
"react-dom": "^18.3.1",
- "react-router-dom": "^6.22.0"
+ "react-markdown": "^10.1.0",
+ "react-router-dom": "^6.22.0",
+ "remark-breaks": "^4.0.0"
},
"devDependencies": {
"@vitejs/plugin-react": "^4.2.1",
diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx
index ad2cc96..e462127 100644
--- a/frontend/src/App.jsx
+++ b/frontend/src/App.jsx
@@ -26,6 +26,8 @@ import SkillsPage from './pages/SkillsPage'
import TrainingPlanningPage from './pages/TrainingPlanningPage'
import TrainingFrameworkProgramsListPage from './pages/TrainingFrameworkProgramsListPage'
import TrainingFrameworkProgramEditPage from './pages/TrainingFrameworkProgramEditPage'
+import TrainingModulesListPage from './pages/TrainingModulesListPage'
+import TrainingModuleEditPage from './pages/TrainingModuleEditPage'
import TrainingUnitRunPage from './pages/TrainingUnitRunPage'
import TrainingCoachPage from './pages/TrainingCoachPage'
import AdminCatalogsPage from './pages/AdminCatalogsPage'
@@ -199,6 +201,9 @@ function AppRoutes() {
{archDisplay}
+ {archetypeCoachHint(archeKey)}
+
+ Keine globalen Zahlenfelder gesetzt — Zeiten und Steuerung siehe je Station unter „Plan:“ (oder nur im Freitext der Kombination).
+ Keine Stationen hinterlegt.
+ Plan: {timingSummary}
+ Keine Übung zugeordnet.
+ Übung #{cid}: {err}
+ {ex.title}
+
+ Im Katalog öffnen
+
+ {ex.title}
+ Keine Kurzbeschreibung im Katalog.
+
+
+ Volle Übungsseite
+
+
+ {candTitleFallback || `Übung #${cid}`}
+
+ {compactPlanningView ? 'Stationen & Einzelübungen (Katalog)' : 'Kombination · Stationen & Einzelübungen'}
+
+ {archDisplay ? (
+
+ {globalComboRows.map((row) => (
+
+ )}
+
+ {slots.map((slot, si) => {
+ const candIdsRaw = slot.candidate_exercise_ids || []
+ const candIds = candIdsRaw
+ .map((id) => (typeof id === 'number' ? id : parseInt(String(id), 10)))
+ .filter((n) => Number.isFinite(n))
+
+ const slotTitle =
+ (slot.title && String(slot.title).trim()) ||
+ (candIds.length <= 1 && slot.candidates?.[0]?.title) ||
+ `Station ${si + 1}`
+
+ const ix = slot.slot_index != null ? Number(slot.slot_index) : si
+ const timingSummary = effectiveStationTimingSummary(archeKey, methodProfile || {}, slotTimingByIx.get(ix))
+
+ return (
+
+ )}
+
+ Ablauf (Detail)
+
+
+ Hinweise Trainer
+
+
{presetHint}
+ ) : null} + + {arch ? ( ++ + Coach & Planung:{' '} + {archeLabel && archeLabel !== arch ? `${archeLabel} · ` : ''} + + {archetypeCoachHint(arch)} +
+ ) : ( ++ Wähle einen Methoden‑Archetyp — besonders beim freien Methodenblock stehen alle + typischen Stations‑Zeiten zur Verfügung. Ohne Archetyp keine geführten Eingaben. +
+ )} + + {!parseState.ok ? ( +{parseState.error}
+ ) : null} + + {fields && fields.length > 0 ? ( ++ Dieser Archetyp ist für maximal flexible Stationsblöcke gedacht — die Zeit‑Eckdaten sind + unten je Station möglich. Freitexte der Kombination beschreiben alles Organisatorische, was nicht in + Sekunden gefasst wird. +
++ Steuerung: zeitlich (Arbeits‑Countdown), Zielzahl Wiederholungen oder Coach‑geführt ohne + Arbeitsuhr. Pausen/Wechsel bleiben unabhängig planbar. Felder können leer bleiben — z. B. nutzt der + Zirkel erst die globalen Arbeit‑Sekunden. +
++ Wechsel (s) zur nächsten Station. „Pause zw. Serien“ nur ab 2 + Serien. +
+ ) : null} ++ Für Migrationen und Sonderfälle. Geführte Felder setzen weiterhin gültige Standardschlüssel. +
+{coachHint}
: null} ++ Keine globalen Zahlenfelder gesetzt — Steuerung erfolgt nur je Station oder über den Freitext der Kombination. +
+ )} + +diff --git a/frontend/src/components/ExercisePeekModal.jsx b/frontend/src/components/ExercisePeekModal.jsx index 208cac4..e3efdb8 100644 --- a/frontend/src/components/ExercisePeekModal.jsx +++ b/frontend/src/components/ExercisePeekModal.jsx @@ -1,10 +1,12 @@ /** * Schnellansicht einer Übung aus dem Katalog (ohne die Planungsseite zu verlassen). */ -import React, { useEffect, useState } from 'react' +import React, { useEffect, useMemo, useState } from 'react' import { Link } from 'react-router-dom' import api from '../utils/api' import ExerciseRichTextBlock from './ExerciseRichTextBlock' +import CombinationPlanBracket from './CombinationPlanBracket' +import { effectiveComboMethodProfile } from '../utils/comboPlanningMethodProfile' function TagMini({ exercise }) { const parts = [] @@ -29,6 +31,8 @@ export default function ExercisePeekModal({ variantId, onClose, titleFallback, + /** Nur Planung: effektives method_profile aus Zeilen-Katalog + Planungs-Override */ + peekExtras, }) { const [loading, setLoading] = useState(false) const [err, setErr] = useState(null) @@ -39,6 +43,22 @@ export default function ExercisePeekModal({ ? exercise.variants.find((v) => String(v.id) === String(variantId)) || null : null + const isCombination = + exercise && + String(exercise.exercise_kind || 'simple').toLowerCase().trim() === 'combination' + + const comboMethodProfileEffective = useMemo(() => { + if (!exercise || !isCombination) return {} + const fromPeek = + peekExtras?.catalog_method_profile && + typeof peekExtras.catalog_method_profile === 'object' && + !Array.isArray(peekExtras.catalog_method_profile) && + Object.keys(peekExtras.catalog_method_profile).length > 0 + ? peekExtras.catalog_method_profile + : exercise.method_profile || {} + return effectiveComboMethodProfile(fromPeek, peekExtras?.planning_method_profile ?? null) + }, [exercise, isCombination, peekExtras]) + useEffect(() => { if (!open) { setExercise(null) @@ -69,6 +89,8 @@ export default function ExercisePeekModal({ if (!open) return null + const sheetWide = Boolean(isCombination && exercise && !loading) + return (
+ So erscheint der Text für Besucher nach Veröffentlichung (Markdown wird gerendert, §-Nummern wie online). +
+{metaLine}
+ )} +Noch keine Abschnitte.
+ )} +Ohne strukturierten Inhalt angezeigt.
+ ) : ( ++ … und noch {modOutline.exercises.length - MODULE_OUTLINE_PREVIEW_MAX}{' '} + {modOutline.exercises.length - MODULE_OUTLINE_PREVIEW_MAX === 1 ? 'Übung' : 'Übungen'} +
+ ) : null} + {modOutline.notes > 0 ? ( ++ sowie {modOutline.notes}{' '} + {modOutline.notes === 1 ? 'Zwischen-Hinweis' : 'Zwischen-Hinweise'} +
+ ) : null} +- {noteHasText ? notePv : '—'} -
+ {!isSepLine && planningCompactLegend && curMn ? ( ++ {noteHasText ? notePv : '—'} +
+ )}+ Über die +-Zeilen zwischen den Einträgen fügst du an der gewünschten Stelle Inhalte ein. Reihenfolge + weiter per Ziehen oder den Pfeiltasten ändern. +
+ ) : ( ++ Die neue Zeile erscheint genau hier; Reihenfolge kannst du wie gewohnt per Ziehen oder Pfeilen + ändern. +
++ Planung für diesen Termin · {compactComboPlanningCaption(comboPlanningModalItem)} +
++ Vorschau unten entspricht der effektiven Planung (Katalog oder Anpassung). Stationen und Einzelübungen + kommen aus dem Katalog; hier änderst du nur Zeiten, Runden und Steuerung für diese Einheit. +
+ {comboPlanningResolvedSlots.length > 0 ? ( ++ Stationen werden geladen … oder die Kombination hat im Katalog keine Stationsliste. +
+ )} ++ Wie kopiert aus der Modul-Bibliothek übernommenen Blöcken in einer Einheit dargestellt werden. +
+ +diff --git a/frontend/src/pages/AdminLegalDocumentsPage.jsx b/frontend/src/pages/AdminLegalDocumentsPage.jsx index cb770e4..85558f3 100644 --- a/frontend/src/pages/AdminLegalDocumentsPage.jsx +++ b/frontend/src/pages/AdminLegalDocumentsPage.jsx @@ -1,90 +1,11 @@ import { useState, useEffect, useCallback } from 'react' -import { jsPDF } from 'jspdf' -import { FileText, Plus, Edit2, Archive, CheckCircle, Clock, Copy, Download, ChevronUp, ChevronDown } from 'lucide-react' +import { FileText, Plus, Edit2, Archive, CheckCircle, Clock, Copy, Download, ChevronUp, ChevronDown, Eye } from 'lucide-react' import api from '../utils/api' +import { generateLegalPdf, legalSectionNumber } from '../utils/legalPdfExport' +import LegalDocumentBody from '../components/LegalDocumentBody' +import { LegalPreviewModal } from '../components/LegalDocumentPreview' -// ─── PDF generation ────────────────────────────────────────────────────────── - -function generateLegalPdf(doc) { - const pdf = new jsPDF({ format: 'a4', unit: 'mm' }) - const marginL = 22 - const marginR = 22 - const marginTop = 28 - const pageW = 210 - const contentW = pageW - marginL - marginR - const bottomLimit = 277 // A4 297mm - 20mm bottom margin - let y = marginTop - - const checkBreak = (need) => { - if (y + need > bottomLimit) { - pdf.addPage() - y = marginTop - } - } - - // Title - pdf.setFont('helvetica', 'bold') - pdf.setFontSize(20) - pdf.text(doc.title, marginL, y) - y += 10 - - // Meta line - const STATUS_DE = { published: 'Gültig seit', draft: 'Entwurf — Stand', archived: 'Archiviert — Stand' } - const dateStr = doc.published_at - ? new Date(doc.published_at).toLocaleDateString('de-DE') - : new Date(doc.updated_at || doc.created_at).toLocaleDateString('de-DE') - const metaLine = `Version ${doc.version} | ${STATUS_DE[doc.status] || doc.status} ${dateStr}` - - pdf.setFont('helvetica', 'normal') - pdf.setFontSize(10) - pdf.setTextColor(90, 90, 90) - pdf.text(metaLine, marginL, y) - y += 3 - pdf.setDrawColor(0, 0, 0) - pdf.setLineWidth(0.4) - pdf.line(marginL, y, pageW - marginR, y) - y += 8 - pdf.setTextColor(0, 0, 0) - - // Sections - for (const section of (doc.content_sections || [])) { - checkBreak(14) - pdf.setFont('helvetica', 'bold') - pdf.setFontSize(11) - pdf.text(section.heading || '', marginL, y) - y += 6 - - if (section.content) { - pdf.setFont('helvetica', 'normal') - pdf.setFontSize(10) - const lines = pdf.splitTextToSize(section.content, contentW) - for (const line of lines) { - checkBreak(5) - pdf.text(line, marginL, y) - y += 5 - } - } - y += 5 - } - - // Footer on every page - const total = pdf.getNumberOfPages() - for (let i = 1; i <= total; i++) { - pdf.setPage(i) - pdf.setFont('helvetica', 'normal') - pdf.setFontSize(8) - pdf.setTextColor(150, 150, 150) - const fy = 289 - pdf.text( - `Shinkan Jinkendo | Exportiert am ${new Date().toLocaleDateString('de-DE')}`, - marginL, fy - ) - pdf.text(`Seite ${i} von ${total}`, pageW - marginR, fy, { align: 'right' }) - pdf.setTextColor(0, 0, 0) - } - - pdf.save(`${doc.document_type}_v${doc.version}.pdf`) -} +const PDF_STATUS_META = { published: 'Gültig seit', draft: 'Entwurf — Stand', archived: 'Archiviert — Stand' } // ─── Sub-components ────────────────────────────────────────────────────────── @@ -190,13 +111,35 @@ function SectionEditor({ sections, onChange }) { placeholder="Abschnittsüberschrift" />
{meta.join(' · ')}
}
+ Archetyp: {String(exercise.method_archetype)}
+
+ Parcours / Bahnsystem: typischerweise starten alle an Station 1 und durchlaufen der + Reihe nach alle Punkte (Geschwindigkeit variabel). Die Stationsreihenfolge unten ist der Ablaufweg; + Zeitangaben pro Station und Gesamtdurchläufe im Ablaufprofil strukturieren das + spätere Coaching. +
+ ) : null} ++ Pro Station oft eine feste Übung; höchstens drei als kleiner Auswahl‑Pool. + Unter Steuerung wählen: zeitlich, nach Wiederholungszahl oder ohne Arbeitsuhr (Coach führt). +
+ {(formData.combination_slots || []).map((row, idx) => { + const candIds = Array.isArray(row.candidate_exercise_ids) ? row.candidate_exercise_ids : [] + const comboPoolFull = candIds.length >= MAX_COMBO_CANDIDATES_PER_STATION + const slotAdv = normalizeAdvanceMode(row.advance_mode) + const serieLabel = + slotAdv === 'timed' ? 'Serie' : slotAdv === 'rep' ? 'Wdh. / Serie' : 'Richtwert' + const seriePlaceholder = slotAdv === 'rep' ? '10' : slotAdv === 'manual' ? '–' : '1' + const showMultiSeries = slotAdv === 'rep' || slotAdv === 'manual' + const serienCountUi = parseComboRepSeriesCountUi(row.rep_series_count) + const showInterSeriesPause = showMultiSeries && serienCountUi >= 2 + const intraLabel = slotAdv === 'timed' ? 'Pause (s)' : 'Pause zw. Serien' + const lbl = + row.exercise_title_by_id && typeof row.exercise_title_by_id === 'object' + ? row.exercise_title_by_id + : {} + const isDropHere = comboDropTargetIx === idx + return ( ++ Mindestens eine Übung — mit „+ Übung“ wählen. +
+ ) : ( ++ {slotAdv === 'timed' + ? 'Arbeit (s): geplantes Ende nach Countdown möglich. Serie: Wiederholungen ohne Stationswechsel innerhalb einer Phase.' + : slotAdv === 'rep' + ? 'Ohne Pflicht-Arbeits-Timer: Ziel über Wiederholungen. Ab zwei Serien: Pause zwischen diesen Serien; sonst nur Wechsel zur nächsten Station.' + : 'Coach: keine feste Arbeitsuhr — Fortschritt später per Tipp. Ab 2 Serien: Pause zwischen Serien; sonst nur Wechsel zur nächsten Station zeitlich planen.'} +
++ Wechsel (s) = Pause bis zur nächsten Station. Feld „Pause zw. + Serien“ erscheint erst ab 2 Serien (sonst keine Pause zwischen zwei Blöcken nötig). +
+ ) : null} ++ + ← Zurück zur Modul‑Bibliothek + +
++ Reihenfolge der Positionen entspricht der späteren Übernahme in einen Abschnitt der Einheit (Kopie). +
+ + {error ?{error}
: null} + {loading ? ( +Laden …
+ ) : ( + + )} + ++ Wiederverwendbare Übungsfolgen für die{' '} + + Trainingsplanung + + . Übernahme in eine Einheit erfolgt dort als lokale Kopie (mit Herkunftsmarkierung). +
+{error}
+ ) : null} + {loading ? ( +Laden …
+ ) : rows.length === 0 ? ( +Noch keine Module angelegt.
++ {(r.summary || '').trim() || '—'}{' '} + + ({Number(r.items_count) || 0} Position{r.items_count === 1 ? '' : 'en'}) + +
++ Sichtbarkeit: {r.visibility || '—'} +
++
Mehrere Einheiten strukturieren auf einmal:{' '} Trainingsrahmenprogramme {' '} - (Ziele, Slots, Übungen als Vorlage). + (Ziele, Sessions, Vorlagen‑Ablauf). +
++ Wiederverwendbare Blöcke innerhalb einer Einheit:{' '} + + Trainingsmodule + {' '} + (übernahme als Kopie beim Bearbeiten einer Einheit).
+ Alle Positionen des gewählten Moduls werden als neue Zeilen eingefügt (Kopie, mit klarer + Herkunft im Ablauf). Die Einheit brauchst du dafür nicht vorher gespeichert zu haben — Speichern am Ende + wie gewohnt. Vollständige Textsuche oder Modulkategorien planen wir serverseitig für + eine spätere Iteration; vorerst steht hier eine{' '} + Schnellsuche über Titel und Freitext-Felder zur Verfügung. +
+ + {moduleApplyErr ? ( +{moduleApplyErr}
+ ) : null} + + {moduleApplyPlacementLocked ? ( + <> ++ Aktuelle Einfügeposition: Abschnitt {modulePlacementSummary.secTitle}{' '} + / {modulePlacementSummary.positionDescription} +
++ {!moduleApplyList.length ? 'Keine Module verfügbar oder keine Berechtigung.' : 'Kein Modul entspricht der Suche.'} +
+ ) : ( + moduleApplyFilteredList.map((m) => { + const title = ((m.title || '').trim() || `Modul #${m.id}`).trim() + const visLbl = trainingVisibilityShortDE(m.visibility) + const nPos = typeof m.items_count === 'number' ? m.items_count : '—' + const selected = String(m.id) === String(moduleApplyModuleId) + return ( + + ) + }) + )} ++ Übungen und Hinweise laden … +
+ ) : modulePickPreview.err ? ( ++ {modulePickPreview.err} +
+ ) : !modulePickPreview.exercises.length && !modulePickPreview.notes ? ( ++ Keine Übungspositionen in diesem Eintrag gefunden (prüfen, ob Übungen im Modul gültige IDs haben). +
+ ) : ( + <> ++ … und noch {modulePickPreview.exercises.length - 12} weitere Übungen in genau dieser Modulreihenfolge. +
+ ) : null} + {modulePickPreview.notes > 0 ? ( ++ zusätzlich {modulePickPreview.notes}{' '} + {modulePickPreview.notes === 1 ? 'Position mit Hinweis' : 'Positionen mit Hinweisen'}{' '} + (ohne Aufzählung) +
+ ) : null} + > + )} ++ Neue Module kannst du unter{' '} + + Trainingsmodule + {' '} + anlegen. +
+