shinkan-jinkendo/.claude/docs/technical/EXERCISES_ARCHITECTURE.md
Lars e4451e1362
Some checks failed
Test Suite / playwright-tests (push) Waiting to run
Deploy Development / deploy (push) Successful in 43s
Test Suite / pytest-backend (push) Failing after 1s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Has been cancelled
Enhance Exercise Management and AI Integration
- Updated the exercise form to include a tabbed navigation structure, improving user experience with sections for Stammdaten, Anleitung, Einordnung, Varianten, and Medien & Mehr.
- Introduced the concept of **Freigabelevel** (visibility level) in the UI, replacing previous terminology for clarity and consistency across components.
- Implemented new AI endpoints for exercise suggestions and regeneration, allowing for dynamic content generation without direct database writes.
- Removed the legacy `is_primary` flag from exercise skills in the UI, ensuring that intensity levels (`niedrig`, `mittel`, `hoch`) are the primary focus for skill management.
- Enhanced the variant management process with improved saving mechanisms and UI updates to reflect changes more intuitively.
2026-05-22 07:52:31 +02:00

645 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Exercise System Architecture
**Version:** 1.1
**Datum:** 2026-04-30
**Status:** DRAFT - Awaiting Review
**Autor:** Claude Code
**Änderungen v1.1:** Progressionsgraph **zwischen** Übungen (Migration 032034); 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