# 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
