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
- 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.
645 lines
19 KiB
Markdown
645 lines
19 KiB
Markdown
# 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
|