- 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.
18 KiB
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"
- Tabelle:
-
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)
- Tabelle:
-
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)
- KEINE eigene Tabelle - Progression via Metadata in
-
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)
- Tabelle:
-
Template-Block: Block mit Platzhaltern für spätere Befüllung
- Flag:
is_template = trueinexercise_blocks - Beispiel: "Zirkeltraining (6 Stationen)" → Station 1: [Platzhalter: Push-Übung], Station 2: [Platzhalter: Pull-Übung], etc.
- Flag:
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:
-
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)
- Bilder:
- Max Size: 50MB pro Datei
- Max Count: 10 Dateien pro Übung
- Storage:
-
Embeds (externe Plattformen)
- Unterstützte Plattformen:
- YouTube:
youtube.com/watch?v=...,youtu.be/... - Instagram:
instagram.com/p/...,instagram.com/reel/... - Vimeo:
vimeo.com/...
- YouTube:
- Speicherung: Nur URL + Platform-Identifier
- Rendering: iframe-Embed im Frontend
- Unterstützte Plattformen:
Medien-Felder:
file_path(lokale Medien) XORembed_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, nutzeexercise_focus_areasexercises.training_style_id→ Ignorieren, nutzeexercise_stylesexercises.training_character_id→ Ignorieren, nutzeexercise_training_characters- NICHT löschen (Rückwärtskompatibilität), aber nicht verwenden
1.4 Sichtbarkeit & Freigabe
3 Sichtbarkeits-Ebenen:
private- Nur Ersteller sieht Übungclub- Alle Vereinsmitglieder sehen Übungofficial- Alle Nutzer sehen Übung (globale Standards)
4 Status-Stufen:
draft- Entwurf, in Bearbeitungin_review- Zur Prüfung eingereichtapproved- Freigegeben, produktiv nutzbararchived- 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):
- Trainer erstellt Übung (
private,draft) - Trainer setzt Status auf
in_review - Club-Admin prüft, setzt
approved+visibility = 'club' - 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:
// 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:
{
"detail": "Human-readable error message"
}
2.3 Error-Handling
HTTP-Status-Codes:
200 OK- Erfolgreiche GET/PUT/DELETE201 Created- Erfolgreiche POST400 Bad Request- Validierung fehlgeschlagen, fehlende Pflichtfelder401 Unauthorized- Kein oder ungültiges Auth-Token403 Forbidden- Keine Berechtigung (Sichtbarkeit/Ownership)404 Not Found- Ressource nicht gefunden409 Conflict- Duplikat, Constraint-Verletzung413 Payload Too Large- Datei zu groß500 Internal Server Error- Server-Fehler
Error-Detail-Format:
{
"detail": "Titel, Ziel und Durchführung sind Pflichtfelder"
}
Validation Errors (detailliert):
{
"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:
useStatefü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):
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:
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→ habenupdated_at - Immutable:
exercise_skills,exercise_focus_areas→ keinupdated_at
4.3 Foreign Keys
Regeln:
- Immer mit ON DELETE:
CASCADEoderRESTRICT(nie ohne!) - Immer mit Index: Für alle FK-Spalten
- Naming:
fk_{from_table}_{to_table}(optional, für Debugging)
Beispiele:
-- 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:
equipmentJSONB - Liste von Geräten (["Matten", "Pratzen"])secondary_method_idsJSONB - Liste von IDs ([2, 5, 7])placeholder_criteriaJSONB - 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 gefiltertexercise_variants- Strukturierte Daten, oft einzeln abgerufenexercise_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.sql015_media_upload.sql016_exercise_blocks.sql
5.2 Idempotenz
Regel: Migrationen müssen mehrfach ausführbar sein ohne Fehler.
Immer verwenden:
-- 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:
DROP TABLE ... -- ohne IF EXISTS
ALTER TABLE ... DROP COLUMN ... -- ohne Check
5.3 Data Migration
Reihenfolge bei Schema-Änderungen:
- Backup: Alte Daten sichern (z.B.
_backupSuffix) - Schema-Änderung: Neue Spalten/Tabellen hinzufügen
- Data-Migration: Alte Daten in neues Schema überführen
- Validation: Daten-Integrität prüfen
- Cleanup: Alte Spalten/Tabellen als deprecated markieren (nicht sofort löschen!)
Beispiel (Migration 008):
-- 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:
-- 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:
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):
/**
* 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