shinkan-jinkendo/.claude/docs/technical/EXERCISES_ARCHITECTURE.md
Lars 6801c60604
Some checks failed
Deploy Development / deploy (push) Successful in 35s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 5s
Test Suite / playwright-tests (push) Failing after 1m55s
feat: Add MediaWiki import functionality with tracking and mapping
- Implemented a new SQL migration for wiki import tracking tables.
- Created an import router for handling MediaWiki imports of exercises, skills, and methods.
- Developed a Semantic MediaWiki API client for direct API interactions.
- Added a mapper to convert SMW properties to local database fields.
- Introduced background tasks for asynchronous import processing.
- Implemented logging and error handling for import operations.
- Added endpoints for previewing imports, checking import status, and managing import references.
2026-04-24 14:41:52 +02:00

639 lines
18 KiB
Markdown

# Exercise System Architecture
**Version:** 1.0
**Datum:** 2026-04-24
**Status:** DRAFT - Awaiting Review
**Autor:** Claude Code
---
## 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.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:** Alle Katalog-Zuordnungen nutzen M:N mit `is_primary` Flag.
**Betroffene Relationen:**
- `exercise_focus_areas` (Übung ↔ Fokusbereiche)
- `exercise_styles` (Übung ↔ Trainingsstile)
- `exercise_target_groups` (Übung ↔ Zielgruppen)
- `exercise_training_characters` (Übung ↔ Trainingscharaktere)
- `exercise_skills` (Übung ↔ Fähigkeiten)
**Primary/Secondary Semantik:**
- **Primary:** Hauptzuordnung, entscheidend für Filter/Suche
- **Secondary:** Nebenzuordnung, zusätzlicher Kontext
- **Regel:** Genau EINE Primary-Zuordnung pro Dimension
- **UI:** Primary wird visuell hervorgehoben (z.B. fett, farbig)
**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