# Exercise System Architecture **Version:** 1.1 **Datum:** 2026-04-30 **Status:** DRAFT - Awaiting Review **Autor:** Claude Code **Änderungen v1.1:** Progressionsgraph **zwischen** Übungen (Migration 032–034); Verweis `TRAINING_FRAMEWORK_SPEC.md` --- ## 1. Datenmodell-Entscheidungen ### 1.1 Übung vs. Variante vs. Block vs. Serie **Definitionen:** - **Übung (Exercise):** Atomare Trainingseinheit mit Ziel, Durchführung, Vorbereitung - Tabelle: `exercises` - Beispiel: "Maai - Distanzübung" - **Variante (Variant):** Angepasste Ausführung EINER Übung - Tabelle: `exercise_variants` - Beispiel: "Maai - Ohne Partner", "Maai - Mit Pratzen" - Beziehung: 1:N (eine Übung hat 0-n Varianten) - **Serie (Series):** Progression durch Varianten derselben Übung - **KEINE eigene Tabelle** - Progression via Metadata in `exercise_variants` - Felder: `progression_level`, `sequence_order`, `prerequisite_variant_id` - Beispiel: Liegestütz → Level 1 (Knie), Level 2 (Normal), Level 3 (Erhöht), Level 4 (Einarmig) - **Block (Block):** Sammlung UNTERSCHIEDLICHER Übungen als zusammenhängender Trainingsabschnitt - Tabelle: `exercise_blocks` + `exercise_block_items` - Beispiel: "Zirkel Oberkörper" → Liegestütz + Klimmzüge + Dips + Planks - Beziehung: M:N (ein Block enthält viele Übungen, eine Übung kann in vielen Blöcken sein) - **Template-Block:** Block mit Platzhaltern für spätere Befüllung - Flag: `is_template = true` in `exercise_blocks` - Beispiel: "Zirkeltraining (6 Stationen)" → Station 1: [Platzhalter: Push-Übung], Station 2: [Platzhalter: Pull-Übung], etc. **Architektur-Diagramm:** ``` Exercise (1) ──────┬──── (N) Variants │ ├── progression_level │ ├── sequence_order │ └── prerequisite_variant_id │ └──── (M:N) Skills └──── (M:N) Focus Areas └──── (M:N) Training Styles └──── (M:N) Target Groups └──── (1:N) Media Exercise Block ──── (N) Block Items ──── (1) Exercise └── (1) Variant (optional) └── is_placeholder (for templates) ``` ### 1.1b Progressionsgraph zwischen Übungen (nicht „Serie“) **Abgrenzung:** Separates Konzept von der **Varianten-Serie** (§1.1): hier geht es um **gerichtete Kanten zwischen verschiedenen Übungen** (optional mit Varianten als Knoten-Endpunkten), gruppiert in Bibliotheks-Containern (`exercise_progression_graphs`). Schema, REST, Produktgrenzen und Backlog (parallele Alternativ-Pakete): **`TRAINING_FRAMEWORK_SPEC.md`** §3–§4. --- ### 1.2 Medien-Strategie **Zwei Medien-Typen:** 1. **Lokale Medien** (eigene Uploads) - Storage: `/app/media/exercises/{uuid}_{filename}` - Unterstützte Formate: - Bilder: `image/jpeg`, `image/png`, `image/gif` - Videos: `video/mp4` - Dokumente: `application/pdf` (für Skizzen) - Max Size: **50MB pro Datei** - Max Count: **10 Dateien pro Übung** 2. **Embeds** (externe Plattformen) - Unterstützte Plattformen: - YouTube: `youtube.com/watch?v=...`, `youtu.be/...` - Instagram: `instagram.com/p/...`, `instagram.com/reel/...` - Vimeo: `vimeo.com/...` - Speicherung: Nur URL + Platform-Identifier - Rendering: iframe-Embed im Frontend **Medien-Felder:** - `file_path` (lokale Medien) **XOR** `embed_url` (externe Medien) - `mime_type` (nur bei lokalen Medien) - `embed_platform` (nur bei Embeds: 'youtube', 'instagram', 'vimeo') - `sort_order` (für Reorder via Drag & Drop) - `is_primary` (Hauptmedium, z.B. Vorschau-Bild) - `context` (Kontext: 'ablauf', 'detail', 'trainer_hint') **Storage-Management:** - Uploads via FastAPI `UploadFile` - Speicherort: Docker Volume `/app/media/exercises/` - Cleanup: Bei Exercise-Deletion automatisch via CASCADE + Cleanup-Job --- ### 1.3 M:N Beziehungen (Primary/Secondary Pattern) **Regel:** Katalog-Zuordnungen (Fokus, Stil, Zielgruppe, …) nutzen M:N mit optionalem `is_primary`-Flag. **Betroffene Relationen (mit `is_primary`):** - `exercise_focus_areas` (Übung ↔ Fokusbereiche) - `exercise_styles` / `exercise_style_directions` (Übung ↔ Stilrichtungen) - `exercise_training_types` (Übung ↔ Trainingsstile) - `exercise_target_groups` (Übung ↔ Zielgruppen) **Ausnahme — `exercise_skills`:** Kein Primär-Flag in UI/API mehr; stattdessen **`intensity`** (`niedrig` \| `mittel` \| `hoch`, Default `mittel`). Spalte `is_primary` bleibt Legacy (Backend speichert immer `false`). **Primary/Secondary Semantik (Katalog-Dimensionen):** - **Primary:** Hauptzuordnung, entscheidend für Filter/Suche - **Secondary:** Nebenzuordnung, zusätzlicher Kontext - **Regel:** Genau EINE Primary-Zuordnung pro Dimension (wo UI das noch anbietet) - **UI:** Primary wird visuell hervorgehoben (z. B. fett, farbig) — Fähigkeiten: Intensitäts-Segmente statt Primary **Legacy-Felder (DEPRECATED):** - `exercises.focus_area` → Ignorieren, nutze `exercise_focus_areas` - `exercises.training_style_id` → Ignorieren, nutze `exercise_styles` - `exercises.training_character_id` → Ignorieren, nutze `exercise_training_characters` - **NICHT löschen** (Rückwärtskompatibilität), aber **nicht verwenden** --- ### 1.4 Sichtbarkeit & Freigabe **3 Sichtbarkeits-Ebenen:** - `private` - Nur Ersteller sieht Übung - `club` - Alle Vereinsmitglieder sehen Übung - `official` - Alle Nutzer sehen Übung (globale Standards) **4 Status-Stufen:** - `draft` - Entwurf, in Bearbeitung - `in_review` - Zur Prüfung eingereicht - `approved` - Freigegeben, produktiv nutzbar - `archived` - Archiviert, nicht mehr aktiv **Berechtigungsmatrix:** | Aktion | Private | Club | Official | |--------|---------|------|----------| | **Sehen** | Nur Owner | Alle Club-Member | Alle Nutzer | | **Bearbeiten** | Nur Owner | Nur Owner | Nur Superadmin | | **Löschen** | Nur Owner | Nur Owner + Club-Admin | Nur Superadmin | | **Freigeben** | Owner → Club | Club-Admin → Official | Superadmin | **Freigabe-Workflow (vereinfacht in Phase 1):** 1. Trainer erstellt Übung (`private`, `draft`) 2. Trainer setzt Status auf `in_review` 3. Club-Admin prüft, setzt `approved` + `visibility = 'club'` 4. Superadmin kann auf `visibility = 'official'` setzen **Phase 2+:** Explizite Freigabe-Requests (`content_change_requests` Tabelle) --- ## 2. API-Konventionen ### 2.1 Naming **URL-Struktur:** - **Collections:** Plural (`/api/exercises`) - **Items:** Singular + ID (`/api/exercises/{id}`) - **Sub-Resources:** Nested (`/api/exercises/{id}/variants`, `/api/exercises/{id}/media`) - **Actions:** HTTP-Verben (POST, PUT, DELETE, GET) **HTTP-Methoden:** - `GET` - Read (Liste oder Detail) - `POST` - Create (neue Ressource) - `PUT` - Update (bestehende Ressource) - `DELETE` - Delete **Query-Parameter:** - Snake-case: `focus_area`, `skill_id`, `search` - Optional Filter: `?focus_area=karate&status=approved` - Pagination: `?limit=50&offset=0` --- ### 2.2 Response-Format **Erfolgreiche Responses:** ```json // Liste (Array) [ {"id": 1, "title": "...", ...}, {"id": 2, "title": "...", ...} ] // Detail (Object mit Enrichment) { "id": 1, "title": "...", "skills": [...], // enriched M:N "variants": [...], // enriched 1:N "media": [...], // enriched 1:N "focus_areas": [...], // enriched M:N "training_styles": [...], // enriched M:N "target_groups": [...], // enriched M:N "age_groups_catalog": ["Kinder", "Teenager"] } // Create/Update (Created Object) { "id": 42, ... // full object wie GET Detail } // Delete {"ok": true} ``` **Fehler-Responses:** ```json { "detail": "Human-readable error message" } ``` --- ### 2.3 Error-Handling **HTTP-Status-Codes:** - `200 OK` - Erfolgreiche GET/PUT/DELETE - `201 Created` - Erfolgreiche POST - `400 Bad Request` - Validierung fehlgeschlagen, fehlende Pflichtfelder - `401 Unauthorized` - Kein oder ungültiges Auth-Token - `403 Forbidden` - Keine Berechtigung (Sichtbarkeit/Ownership) - `404 Not Found` - Ressource nicht gefunden - `409 Conflict` - Duplikat, Constraint-Verletzung - `413 Payload Too Large` - Datei zu groß - `500 Internal Server Error` - Server-Fehler **Error-Detail-Format:** ```json { "detail": "Titel, Ziel und Durchführung sind Pflichtfelder" } ``` **Validation Errors (detailliert):** ```json { "detail": "Validation failed", "errors": [ {"field": "title", "message": "Titel muss mindestens 3 Zeichen lang sein"}, {"field": "goal", "message": "Ziel ist ein Pflichtfeld"} ] } ``` --- ## 3. Frontend-Architektur ### 3.1 Route-Struktur **Übungen-Routes:** - `/exercises` → Grid/List mit Filtern (ExercisesPage) - `/exercises/:id` → Detail-Ansicht (ExerciseDetailPage) - `/exercises/:id/edit` → Edit-Formular (ExerciseEditPage - eigene Route, kein Modal) - `/exercises/new` → Create-Formular (ExerciseCreatePage - eigene Route, kein Modal) **Rationale für eigene Routes statt Modals:** - Bessere Deep-Linking (shareable URLs) - Browser-Navigation (Back-Button) - State-Management einfacher - Mobile-freundlicher --- ### 3.2 Komponenten-Hierarchie ``` pages/ ExercisesPage.jsx (Grid + Filters) ExerciseDetailPage.jsx (Formatted View) ExerciseEditPage.jsx (Edit Form) ExerciseCreatePage.jsx (Create Form) components/ exercise/ ExerciseCard.jsx (Grid-Item) ExerciseQuickInfo.jsx (Summary-Box) ExerciseSection.jsx (Collapsible Section) VariantsList.jsx (Varianten-Akkordeon) VariantCard.jsx (Einzelne Variante) MediaGallery.jsx (Medien-Display) MediaUploader.jsx (Upload/Embed-UI) SkillsList.jsx (Fähigkeiten-Chips) CatalogAssignments.jsx (Zuordnungen-Display) forms/ ExerciseForm.jsx (Haupt-Formular, wiederverwendbar) VariantForm.jsx (Varianten-Formular) MediaUploadForm.jsx (Upload/Embed-Tabs) MultiSelect.jsx (M:N Auswahl mit Primary-Toggle) SkillsSelector.jsx (Skills mit Intensity/Level) common/ Chip.jsx (Badge/Tag) Badge.jsx (Status/Visibility) Accordion.jsx (Collapsible Section) Modal.jsx (Generisches Modal) DragDropList.jsx (Reorder-Support) ``` --- ### 3.3 State-Management **Strategie: Minimal, lokal, server-zentriert** - **Local State:** `useState` für UI (filter open/close, modal visible) - **Server State:** `useEffect` + API-Calls (keine globale Cache) - **Kein Redux/Zustand:** Context nur für Auth + Profile - **Optimistic Updates:** Nur bei unkritischen Actions (reorder) **Beispiel (ExercisesPage):** ```jsx const [exercises, setExercises] = useState([]) const [filters, setFilters] = useState({focus_area: '', status: ''}) const [loading, setLoading] = useState(true) useEffect(() => { loadExercises(filters) }, [filters]) const loadExercises = async (filters) => { setLoading(true) try { const data = await api.listExercises(filters) setExercises(data) } catch (err) { alert(err.message) } finally { setLoading(false) } } ``` --- ## 4. Datenbank-Konventionen ### 4.1 Naming - **Tabellen:** Plural, snake_case (`exercises`, `exercise_variants`) - **Spalten:** snake_case (`created_at`, `is_primary`) - **IDs:** `{table}_id` (`exercise_id`, `skill_id`) - **Flags:** `is_*`, `has_*` (`is_primary`, `is_template`, `has_media`) - **Timestamps:** `created_at`, `updated_at` --- ### 4.2 Standard-Spalten (IMMER) **Pflicht für alle Tabellen:** ```sql id SERIAL PRIMARY KEY created_at TIMESTAMP DEFAULT NOW() updated_at TIMESTAMP DEFAULT NOW() -- nur bei mutablen Tabellen ``` **Mutable vs. Immutable:** - **Mutable:** `exercises`, `exercise_variants`, `exercise_blocks` → haben `updated_at` - **Immutable:** `exercise_skills`, `exercise_focus_areas` → kein `updated_at` --- ### 4.3 Foreign Keys **Regeln:** - **Immer mit ON DELETE:** `CASCADE` oder `RESTRICT` (nie ohne!) - **Immer mit Index:** Für alle FK-Spalten - **Naming:** `fk_{from_table}_{to_table}` (optional, für Debugging) **Beispiele:** ```sql -- CASCADE: Löschen der Parent-Ressource löscht auch Children exercise_id INT REFERENCES exercises(id) ON DELETE CASCADE -- RESTRICT: Löschen der Parent-Ressource verhindert wenn Children existieren skill_id INT REFERENCES skills(id) ON DELETE RESTRICT -- SET NULL: Löschen setzt FK auf NULL (für optionale Beziehungen) prerequisite_variant_id INT REFERENCES exercise_variants(id) ON DELETE SET NULL ``` --- ### 4.4 JSONB vs. Tabellen **JSONB verwenden wenn:** - Daten unstrukturiert - Selten gefiltert/gesucht - Flexible Schema-Änderungen nötig **Beispiele:** - `equipment` JSONB - Liste von Geräten (["Matten", "Pratzen"]) - `secondary_method_ids` JSONB - Liste von IDs ([2, 5, 7]) - `placeholder_criteria` JSONB - Filter-Kriterien für Platzhalter **Tabelle verwenden wenn:** - Daten strukturiert - Oft gefiltert/gesucht - Referentielle Integrität nötig (Foreign Keys) **Beispiele:** - `exercise_skills` - M:N Beziehung, oft gefiltert - `exercise_variants` - Strukturierte Daten, oft einzeln abgerufen - `exercise_media` - Referentielle Integrität (ON DELETE CASCADE) --- ## 5. Migrations-Strategie ### 5.1 Naming **Format:** `{NNN}_{descriptive_name}.sql` - NNN = laufende Nummer (3-stellig mit führenden Nullen) - Beschreibend, nicht kryptisch - Beispiele: - `014_variants_progression.sql` - `015_media_upload.sql` - `016_exercise_blocks.sql` --- ### 5.2 Idempotenz **Regel:** Migrationen müssen mehrfach ausführbar sein ohne Fehler. **Immer verwenden:** ```sql -- Tabellen CREATE TABLE IF NOT EXISTS ... -- Spalten DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='...' AND column_name='...') THEN ALTER TABLE ... ADD COLUMN ...; END IF; END $$; -- Indizes CREATE INDEX IF NOT EXISTS ... -- Constraints ALTER TABLE ... ADD CONSTRAINT ... IF NOT EXISTS ... ``` **Niemals:** ```sql DROP TABLE ... -- ohne IF EXISTS ALTER TABLE ... DROP COLUMN ... -- ohne Check ``` --- ### 5.3 Data Migration **Reihenfolge bei Schema-Änderungen:** 1. **Backup:** Alte Daten sichern (z.B. `_backup` Suffix) 2. **Schema-Änderung:** Neue Spalten/Tabellen hinzufügen 3. **Data-Migration:** Alte Daten in neues Schema überführen 4. **Validation:** Daten-Integrität prüfen 5. **Cleanup:** Alte Spalten/Tabellen als deprecated markieren (nicht sofort löschen!) **Beispiel (Migration 008):** ```sql -- 1. Backup (implizit via alte Spalten behalten) -- 2. Schema CREATE TABLE exercise_focus_areas ... -- 3. Data Migration INSERT INTO exercise_focus_areas (exercise_id, focus_area_id, is_primary) SELECT id, focus_area_id, true FROM exercises WHERE focus_area_id IS NOT NULL ON CONFLICT DO NOTHING; -- 4. Validation (manuell via Query) -- 5. Cleanup (später, wenn sicher) -- ALTER TABLE exercises DROP COLUMN focus_area_id; -- NICHT JETZT! ``` --- ### 5.4 Rollback-Plan **Jede Migration braucht:** - Dokumentierter Rollback-Plan (im Kommentar) - Test auf Dev-System - Validierung nach Ausführung **Beispiel:** ```sql -- Migration 014: Variants Progression -- Rollback: DROP COLUMNS progression_level, sequence_order, prerequisite_variant_id -- Risk: LOW (nur neue Spalten, kein Data Loss) -- Validation: SELECT COUNT(*) FROM exercise_variants WHERE progression_level IS NOT NULL; ALTER TABLE exercise_variants ADD COLUMN IF NOT EXISTS progression_level INT; ``` --- ## 6. Testing-Strategie (Phase 2) ### 6.1 Backend **Unit-Tests:** - Resolver-Funktionen (z.B. `get_exercise()`) - Validation-Logik (z.B. `validate_exercise()`) - Helper-Funktionen (z.B. `parse_embed_url()`) **Integration-Tests:** - API-Endpoints (Request → Response) - DB-Transaktionen (Create → Read → Update → Delete) - Permissions (Auth + Visibility) **Migration-Tests:** - Up/Down-Migrationen - Data-Integrity nach Migration --- ### 6.2 Frontend **Component-Tests (Jest + React Testing Library):** - ExerciseCard rendert korrekt - ExerciseForm validiert Inputs - MultiSelect verhält sich korrekt **E2E-Tests (Playwright):** - User kann Übung erstellen - User kann Übung bearbeiten - User kann Varianten hinzufügen - User kann Medien hochladen **Visual Regression (optional):** - Chromatic / Percy - Screenshots vergleichen --- ## 7. Dokumentations-Konventionen ### 7.1 Code-Kommentare **Python Docstrings:** ```python def get_exercise(exercise_id: int, session: dict) -> dict: """ Get exercise by ID with all enriched data. Args: exercise_id: Exercise ID session: Auth session from require_auth() Returns: dict: Exercise object with skills, variants, media, catalog assignments Raises: HTTPException: 404 if not found, 403 if forbidden """ ``` **JSDoc (für komplexe Komponenten):** ```jsx /** * ExerciseCard - Grid item für Übungsdarstellung * * @param {Object} exercise - Exercise object from API * @param {Function} onClick - Handler für Detail-Klick * @param {Function} onEdit - Handler für Edit-Klick * @param {Function} onDelete - Handler für Delete-Klick * @param {boolean} compact - Kompakte Darstellung (optional) */ export function ExerciseCard({ exercise, onClick, onEdit, onDelete, compact = false }) { // ... } ``` **Inline-Kommentare:** - Nur für nicht-offensichtliche Logik - Warum, nicht Was - Beispiel: `// Prerequisite muss zur gleichen Übung gehören` --- ### 7.2 Markdown-Docs **Struktur:** - `functional/` - Fachliche Specs (WAS soll gebaut werden) - `technical/` - Technische Specs (WIE wird es gebaut) - `working/` - Temporäre Analysen, Notizen (kann veraltet sein) - `audit/` - Reviews, Matrizen, Checklisten **Update-Regel:** - Nach jedem Feature: Doku aktualisieren - Version im Header erhöhen - Änderungslog im Dokument --- ## 8. Entscheidungs-Log | Datum | Entscheidung | Rationale | Impact | |-------|--------------|-----------|--------| | 2026-04-24 | M:N statt 1:1 für Katalog-Zuordnungen | Flexibilität, keine Duplikate, bessere Filter | Alle Übungen müssen migriert werden (Migration 008) | | 2026-04-24 | Varianten für Progression statt eigene Serie-Tabelle | Einfacher, weniger Joins, direkte Beziehung | Variants-Tabelle erweitern (Migration 014) | | 2026-04-24 | Lokale Medien + Embeds (nicht Cloud) | Kostenkontrolle, Datenschutz, keine Vendor-Lock-in | Storage-Management nötig, Backup-Strategie | | 2026-04-24 | Eigene Routes statt Modals für Edit/Create | Deep-Linking, Browser-Navigation, Mobile-UX | Mehr Routes, aber bessere UX | | 2026-04-24 | MediaWiki API statt Export | Live-Sync möglich, aktuellere Daten | API-Client implementieren, Semantic MediaWiki verstehen | | 2026-04-24 | JSONB für Equipment, nicht eigene Tabelle | Flexibel, selten gefiltert, keine Overhead | Keine Referentielle Integrität, Full-Text-Suche schwieriger | --- ## 9. Offene Punkte / TODOs **Für Review:** - [ ] Sichtbarkeits-Matrix OK? (private/club/official) - [ ] Freigabe-Workflow ausreichend einfach? - [ ] Medien-Limits OK? (50MB, 10 Files) - [ ] JSONB vs. Tabellen für Equipment richtig entschieden? **Für Phase 2:** - [ ] Explizite Freigabe-Requests (content_change_requests) - [ ] Saved Searches - [ ] Advanced Filters (Kombination mehrerer Kriterien) - [ ] Batch-Operations (Multi-Select + Delete/Archive) --- **Version:** 1.0 **Letzte Änderung:** 2026-04-24 **Status:** DRAFT - Awaiting Review