From 6801c60604c313b0230b0e92ebb6efae4fdec651 Mon Sep 17 00:00:00 2001 From: Lars Date: Fri, 24 Apr 2026 14:41:52 +0200 Subject: [PATCH] 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. --- .claude/docs/REVIEW_PROMPT_EXERCISES_SPECS.md | 599 ++++++++++ .../docs/technical/AI_PROMPT_SYSTEM_SPEC.md | 602 ++++++++++ .claude/docs/technical/EXERCISES_API_SPEC.md | 934 +++++++++++++++ .../docs/technical/EXERCISES_ARCHITECTURE.md | 638 ++++++++++ .../technical/EXERCISES_DATABASE_FINAL.md | 910 +++++++++++++++ .../technical/EXERCISES_FRONTEND_ROUTING.md | 681 +++++++++++ .claude/docs/technical/KI_FEATURES_SPEC.md | 370 ++++++ .../docs/technical/MEDIAWIKI_IMPORT_SPEC.md | 425 +++++++ .claude/docs/technical/MEDIA_UPLOAD_SPEC.md | 778 +++++++++++++ .claude/docs/technical/SEARCH_FILTER_SPEC.md | 760 ++++++++++++ .claude/docs/technical/SKILLS_MATRIX_SPEC.md | 454 ++++++++ .claude/docs/technical/UI_COMPONENTS_SPEC.md | 1036 +++++++++++++++++ .env.example | 10 + backend/main.py | 3 +- .../migrations/018_wiki_import_tracking.sql | 40 + backend/routers/import_wiki.py | 637 ++++++++++ backend/smw_client.py | 215 ++++ backend/smw_mapper.py | 365 ++++++ backend/version.py | 20 +- 19 files changed, 9472 insertions(+), 5 deletions(-) create mode 100644 .claude/docs/REVIEW_PROMPT_EXERCISES_SPECS.md create mode 100644 .claude/docs/technical/AI_PROMPT_SYSTEM_SPEC.md create mode 100644 .claude/docs/technical/EXERCISES_API_SPEC.md create mode 100644 .claude/docs/technical/EXERCISES_ARCHITECTURE.md create mode 100644 .claude/docs/technical/EXERCISES_DATABASE_FINAL.md create mode 100644 .claude/docs/technical/EXERCISES_FRONTEND_ROUTING.md create mode 100644 .claude/docs/technical/KI_FEATURES_SPEC.md create mode 100644 .claude/docs/technical/MEDIAWIKI_IMPORT_SPEC.md create mode 100644 .claude/docs/technical/MEDIA_UPLOAD_SPEC.md create mode 100644 .claude/docs/technical/SEARCH_FILTER_SPEC.md create mode 100644 .claude/docs/technical/SKILLS_MATRIX_SPEC.md create mode 100644 .claude/docs/technical/UI_COMPONENTS_SPEC.md create mode 100644 backend/migrations/018_wiki_import_tracking.sql create mode 100644 backend/routers/import_wiki.py create mode 100644 backend/smw_client.py create mode 100644 backend/smw_mapper.py diff --git a/.claude/docs/REVIEW_PROMPT_EXERCISES_SPECS.md b/.claude/docs/REVIEW_PROMPT_EXERCISES_SPECS.md new file mode 100644 index 0000000..aac507c --- /dev/null +++ b/.claude/docs/REVIEW_PROMPT_EXERCISES_SPECS.md @@ -0,0 +1,599 @@ +# Review Prompt: Exercises System Specifications + +**Zweck:** Vollständiger Review der erstellten Spezifikationen gegen ursprüngliche Anforderungen und User-Ergänzungen + +**Reviewer:** Plan Agent (oder anderer spezialisierter Agent) + +**Status:** ✅ Review abgeschlossen + Fixes implementiert + +**Datum:** 2026-04-24 +**Review-Datum:** 2026-04-24 +**Ergebnis:** APPROVED WITH CHANGES – alle BLOCKER und MAJOR Issues behoben + +--- + +## 1. Kontext & Auftrag + +Du bist ein Plan Agent, der die erstellten Spezifikationen für das Exercise System in Shinkan Jinkendo reviewt. Deine Aufgabe ist es, die 7 Core-Specs gegen die ursprünglichen Anforderungen und User-Ergänzungen zu prüfen und sicherzustellen, dass **keine Anforderungen vergessen** wurden und **keine architektonischen Drifts** entstanden sind. + +--- + +## 2. Zu reviewende Spezifikationen + +### 2.1 Core Specs (Phase 0 Foundation) + +**Lokation:** `c:/Dev/shinkan-jinkendo/.claude/docs/technical/` + +1. **EXERCISES_ARCHITECTURE.md** (ca. 800 Zeilen) + - Architektur-Entscheidungen + - Exercise/Variant/Block/Series Definitionen + - Media-Strategie + - API-Konventionen + - DB-Konventionen + - Decision Log + +2. **UI_COMPONENTS_SPEC.md** (ca. 600 Zeilen) + - Base Components (Chip, MultiSelect, Accordion, DragDropList) + - Exercise Components (Card, QuickInfo, VariantCard, MediaGallery, MediaUploader) + - Layout Patterns + - Responsive Breakpoints + - Accessibility + +3. **EXERCISES_API_SPEC.md** (ca. 574 Zeilen) + - Alle Endpoints (Exercises, Variants, Media) + - Request/Response Beispiele + - Validierung Rules + - Error Format + - HTTP Status Codes + +4. **EXERCISES_FRONTEND_ROUTING.md** (ca. 400 Zeilen) + - Route-Struktur + - Navigation Patterns + - State Management (URL, Local, Context) + - Deep Linking + - Navigation Guards + - Performance (Lazy Loading, Prefetching) + +5. **MEDIA_UPLOAD_SPEC.md** (ca. 500 Zeilen) + - Hybrid Upload-Strategie (Lokal + Embeds) + - Upload-Flow (Client + Server) + - Embed-Parsing (YouTube, Vimeo, Instagram, TikTok) + - Media-Galerie + - Drag & Drop Reordering + - Security & Performance + +6. **SEARCH_FILTER_SPEC.md** (ca. 450 Zeilen) + - Multi-Layer Search (Keyword, Katalog, Meta) + - Volltext-Suche (tsvector) + - Frontend Filter-UI + - Erweiterte Filter (Multi-Select, Range, Equipment) + - Faceted Search + - Saved Searches + - Sortierung + +7. **EXERCISES_DATABASE_FINAL.md** (ca. 600 Zeilen) + - Migration 014 (Variant Progression + Search Vector) + - Migration 015 (Semantic Matching - Optional) + - Migration 016 (Saved Searches) + - Vollständige Tabellenstruktur + - Indizes + - Constraints & Business Rules + - Trigger-Funktionen + +--- + +## 3. Ursprüngliche Anforderungen + +### 3.1 User-Anforderungen (Initial Message) + +**Quelle:** Erste User-Message in Session + +**Kern-Anforderungen:** + +1. **Formatierte Ausgabe:** Übungen mit Embedded Videos, Zeichnungen, optimiert für verschiedene Use Cases +2. **Varianten:** Mehrere Varianten mit progressiven Schwierigkeitsstufen +3. **Exercise Series:** Progression durch Varianten derselben Übung +4. **Exercise Blocks:** Sammlungen verschiedener Übungen +5. **Multi-dimensionale Kategorisierung:** Stil, Fokus, Zielgruppe, Fähigkeiten-Matrix mit Levels +6. **Trainingsplan-Integration:** Muss mit Trainingsplan-System integrieren +7. **MediaWiki-Import:** Direct API Integration (NICHT export-basiert) +8. **Robustheit:** Kein ständiger Drift, skalierbar, umfassend + +**Spec-First Approach gewählt:** Alle Specs VOR Implementation + +### 3.2 User-Ergänzungen (Follow-up) + +**Quelle:** Zweite User-Message mit 5 Klarstellungen + +**Präzisierungen:** + +1. **Priorisierung OK:** Spec-First Ansatz bestätigt +2. **Web + Mobile Priorität (NICHT Print/PDF):** + - Kurze vs. detaillierte Beschreibungen für Trainingspläne +3. **Media-Strategie:** Lokaler Upload + YouTube/Instagram Embeds +4. **Exercise Series vs. Blocks Klarstellung:** + - **Series:** Progressive Varianten derselben Übung (via Variant-Metadata) + - **Blocks:** Gruppierung VERSCHIEDENER Übungen (neue Entity) + - **Template Blocks:** Blocks mit Platzhaltern (gefüllt beim Planen) +5. **MediaWiki Import:** Via Direct API (NICHT Export) + +### 3.3 Referenz-Dokumente + +**Quelle:** Zu lesende Dokumente vor Spec-Erstellung + +1. **shinkan_anforderungsdokument_entwurf.md** (1425 Zeilen) + - Fachliche Anforderungen + - Reifegradmodelle + - Übungen & Methoden + - Trainingsplanung + - MVP Scope: Trainer-fokussiert + +2. **DOMAIN_MODEL.md** (Version 0.3.4) + - Hierarchische Struktur: Fokusbereich → Stil → Zielgruppen (M:N) + - Primary/Secondary Pattern für M:N + - Migration 009 deployed (Zielgruppen M:N Refactoring) + +3. **DATABASE_SCHEMA.md** (Version 0.3.4) + - Existierende M:N Tabellen + - Migrations 001-013 deployed + +4. **backend/routers/exercises.py** (454 Zeilen) + - Existierendes CRUD + - Enrichment-Pattern für M:N Relations + - Filter-Support + +5. **backend/migrations/005_exercises.sql** + - Base exercises table + - exercise_skills, exercise_variants, exercise_media + +6. **backend/migrations/008_mn_exercise_relations.sql** + - M:N Migration (focus_areas, styles, target_groups, age_groups) + +7. **frontend/src/pages/ExercisesPage.jsx** (609 Zeilen) + - Existierende UI (Grid, Filters, Modal) + - Limitation: Nutzt M:N Backend-Daten NICHT + +--- + +## 4. Review-Kriterien + +### 4.1 Vollständigkeit (Completeness) + +**Prüfe gegen ursprüngliche Anforderungen:** + +- [ ] **Formatierte Ausgabe:** Sind Embedded Videos, Zeichnungen, verschiedene Use Cases abgedeckt? +- [ ] **Varianten mit Progression:** Sind progressive Schwierigkeitsstufen spezifiziert? +- [ ] **Exercise Series:** Ist Progression durch Varianten klar definiert (OHNE separate Entity)? +- [ ] **Exercise Blocks:** Ist Gruppierung verschiedener Übungen spezifiziert? +- [ ] **Template Blocks:** Sind Platzhalter-Blocks für Planung definiert? +- [ ] **Multi-dimensionale Kategorisierung:** Sind Stil, Fokus, Zielgruppe, Skills mit Levels abgedeckt? +- [ ] **Trainingsplan-Integration:** Ist Integration mit Training Plan System spezifiziert? +- [ ] **MediaWiki-Import:** Ist Direct API Integration (NICHT Export) spezifiziert? +- [ ] **Kurz vs. Detail:** Sind verschiedene Beschreibungslängen für Trainingspläne definiert? + +### 4.2 Konsistenz (Consistency) + +**Prüfe gegen Referenz-Dokumente:** + +- [ ] **DOMAIN_MODEL.md:** Folgen Specs der hierarchischen Struktur (Fokusbereich → Stil → Zielgruppen)? +- [ ] **M:N Pattern:** Wird `is_primary` Flag korrekt genutzt? +- [ ] **DATABASE_SCHEMA.md:** Bauen Migrationen 014-016 auf 001-013 auf? +- [ ] **Existierender Code:** Sind Specs kompatibel mit `backend/routers/exercises.py`? +- [ ] **Migration-Pattern:** Folgen neue Migrationen dem idempotenten DO $$ Pattern? + +### 4.3 Architektonische Korrektheit (No Drift) + +**Prüfe gegen User-Ergänzungen:** + +- [ ] **Series vs. Blocks:** Ist die Unterscheidung klar und korrekt umgesetzt? + - Series = Progression durch Varianten (Metadata in `exercise_variants`) + - Blocks = Neue Entity für Gruppierung verschiedener Übungen +- [ ] **Media-Strategie:** Hybrid (Lokal + Embeds) korrekt spezifiziert? +- [ ] **Keine Cloud-Only Annahme:** Lokaler Upload vorhanden? +- [ ] **Direct API statt Export:** MediaWiki-Import via API spezifiziert? + +### 4.4 Technische Machbarkeit (Feasibility) + +- [ ] **Performance:** Sind Performance-Targets realistisch (<50ms List, <150ms Search)? +- [ ] **Skalierbarkeit:** DB-Schema skaliert auf 5.000+ Übungen? +- [ ] **Security:** File-Upload-Validierung mit python-magic (nicht nur Extension)? +- [ ] **UX:** Mobile-Optimierung (Filter-Drawer, Sticky Search, Bottom-Nav) vorhanden? + +### 4.5 Gaps & Fehlende Features + +**Prüfe auf vergessene Features:** + +- [ ] **Trainingscharakter (M:N):** Ist `exercise_training_characters` spezifiziert? +- [ ] **Fähigkeiten-Levels:** Sind `required_level` und `target_level` in `exercise_skills` definiert? +- [ ] **Visibility-Workflow:** Ist `private → club → official` Status-Flow spezifiziert? +- [ ] **Permissions:** Sind Owner-Checks (nur Ersteller darf bearbeiten) definiert? +- [ ] **Pagination:** Ist Pagination in List-Endpoints spezifiziert? +- [ ] **Reordering:** Ist Drag & Drop Reordering für Variants + Media definiert? + +--- + +## 5. Review-Prozess + +### 5.1 Schritt-für-Schritt Anleitung + +**Schritt 1: Dokumente lesen** +1. Lies alle 7 Core-Specs vollständig durch +2. Mache dir Notizen zu Auffälligkeiten +3. Markiere unklare oder widersprüchliche Stellen + +**Schritt 2: Checkliste durchgehen** +1. Gehe durch Abschnitt 4.1 (Vollständigkeit) - jede Anforderung einzeln prüfen +2. Gehe durch Abschnitt 4.2 (Konsistenz) - gegen Referenz-Docs prüfen +3. Gehe durch Abschnitt 4.3 (Architektur) - gegen User-Ergänzungen prüfen +4. Gehe durch Abschnitt 4.4 (Machbarkeit) - technische Risiken bewerten +5. Gehe durch Abschnitt 4.5 (Gaps) - fehlende Features identifizieren + +**Schritt 3: Cross-Check zwischen Specs** +1. ARCHITECTURE.md ↔ API_SPEC.md: Sind Decisions in API umgesetzt? +2. API_SPEC.md ↔ DATABASE_FINAL.md: Matchen Endpoints zur DB-Struktur? +3. UI_COMPONENTS_SPEC.md ↔ FRONTEND_ROUTING.md: Passen Komponenten zu Routes? +4. MEDIA_UPLOAD_SPEC.md ↔ DATABASE_FINAL.md: Passt `exercise_media` Tabelle? +5. SEARCH_FILTER_SPEC.md ↔ DATABASE_FINAL.md: Ist `search_vector` definiert? + +**Schritt 4: Gap-Analyse** +1. Erstelle Liste aller **fehlenden** Anforderungen +2. Erstelle Liste aller **widersprüchlichen** Definitionen +3. Erstelle Liste aller **unklaren** Spezifikationen +4. Erstelle Liste aller **technischen Risiken** + +**Schritt 5: Review-Report erstellen** +1. Folge Template in Abschnitt 6 +2. Fülle alle Abschnitte vollständig aus +3. Sei konkret: Datei + Zeile + Problem + Lösungsvorschlag + +--- + +## 6. Review-Report Template + +**Nutze dieses Template für deinen Review-Report:** + +```markdown +# Exercise System Specs - Review Report + +**Reviewer:** [Dein Agent-Name] +**Review-Datum:** [Datum] +**Review-Dauer:** [ca. X Minuten] +**Gesamtbewertung:** [APPROVED / APPROVED WITH CHANGES / REJECTED] + +--- + +## 1. Executive Summary + +[2-3 Sätze: Sind die Specs bereit für Implementation? Größte Probleme?] + +--- + +## 2. Vollständigkeit (Completeness) + +### 2.1 Erfüllte Anforderungen ✅ + +- [x] Formatierte Ausgabe: Abgedeckt in MEDIA_UPLOAD_SPEC.md (Abschnitt 4) +- [x] Varianten mit Progression: Abgedeckt in EXERCISES_DATABASE_FINAL.md (Migration 014) +- [... weitere ...] + +### 2.2 Fehlende Anforderungen ❌ + +| Anforderung | Wo fehlt es? | Kritikalität | Lösungsvorschlag | +|-------------|--------------|--------------|------------------| +| Exercise Blocks Entity | EXERCISES_ARCHITECTURE.md erwähnt, aber nicht in DATABASE_FINAL.md | HIGH | Migration 017 für `exercise_blocks` Tabelle hinzufügen | +| Template Blocks Platzhalter | Nirgendwo spezifiziert | MEDIUM | Erweitere `exercise_blocks` um `is_template` + `placeholders` JSONB | +| ... | ... | ... | ... | + +--- + +## 3. Konsistenz (Consistency) + +### 3.1 Inkonsistenzen zwischen Specs + +| Problem | Betroffene Dateien | Impact | Fix | +|---------|-------------------|--------|-----| +| API_SPEC.md definiert `age_groups_catalog` als JSONB, aber DOMAIN_MODEL.md sagt M:N | API_SPEC.md Zeile 104, DOMAIN_MODEL.md Abschnitt 3 | MEDIUM | Entscheide: JSONB ODER M:N (empfohlen: M:N für Konsistenz) | +| ... | ... | ... | ... | + +### 3.2 Inkonsistenzen mit Referenz-Dokumenten + +| Problem | Spec-Datei | Referenz-Dokument | Fix | +|---------|-----------|-------------------|-----| +| DOMAIN_MODEL.md sagt Zielgruppen sind global (M:N), aber API_SPEC.md zeigt `target_group_id` als FK | API_SPEC.md Zeile 267 | DOMAIN_MODEL.md Abschnitt 3.3 | Korrigiere API_SPEC zu M:N Array | +| ... | ... | ... | ... | + +--- + +## 4. Architektur-Drift + +### 4.1 Abweichungen von User-Ergänzungen + +| User-Anforderung | Spec-Umsetzung | Konform? | Problem / Fix | +|-----------------|----------------|----------|---------------| +| "Series = Varianten-Progression OHNE separate Entity" | EXERCISES_ARCHITECTURE.md Abschnitt 2.2 definiert Series korrekt als Metadata | ✅ JA | - | +| "Blocks = neue Entity für verschiedene Übungen" | EXERCISES_ARCHITECTURE.md erwähnt, DATABASE_FINAL.md fehlt Migration | ❌ NEIN | Migration 017 fehlt | +| "Direct API statt Export" | Nirgendwo spezifiziert | ❌ NEIN | Neue Spec MEDIAWIKI_IMPORT_SPEC.md nötig | +| ... | ... | ... | ... | + +--- + +## 5. Technische Risiken + +| Risiko | Betroffene Spec | Wahrscheinlichkeit | Impact | Mitigation | +|--------|----------------|-------------------|--------|------------| +| tsvector auf großen Texten langsam | SEARCH_FILTER_SPEC.md | MEDIUM | HIGH | Teste Performance mit 10.000 Zeichen Execution-Text | +| 50MB File-Upload blockiert Server | MEDIA_UPLOAD_SPEC.md | HIGH | MEDIUM | Chunk-Upload oder separater Upload-Worker | +| pgvector Extension nicht verfügbar | DATABASE_FINAL.md Migration 015 | LOW | LOW | Migration 015 ist optional (Phase 2) | +| ... | ... | ... | ... | ... | + +--- + +## 6. Gap-Analyse + +### 6.1 Komplett fehlende Features + +1. **Exercise Blocks Implementation** + - Wo benötigt: DATABASE_FINAL.md, API_SPEC.md, UI_COMPONENTS_SPEC.md + - Kritikalität: HIGH (User-Anforderung #4) + - Aufwand: 1 Migration + 3 Endpoints + 2 Komponenten + +2. **Template Blocks mit Platzhaltern** + - Wo benötigt: DATABASE_FINAL.md, API_SPEC.md + - Kritikalität: MEDIUM (User-Anforderung #4 Teil 2) + - Aufwand: Erweitere Blocks-Tabelle + 1 Endpoint + +3. **MediaWiki Direct API Import** + - Wo benötigt: Neue Spec MEDIAWIKI_IMPORT_SPEC.md + - Kritikalität: HIGH (User-Anforderung #7) + - Aufwand: Neue Spec + Backend-Integration + +4. [... weitere ...] + +### 6.2 Unvollständig spezifizierte Features + +1. **Trainingsplan-Integration** + - Status: Erwähnt in ARCHITECTURE.md, aber keine konkreten Endpoints + - Fehlende Details: Wie werden Übungen in Pläne eingefügt? API-Struktur? + - Fix: Erweitere API_SPEC.md um `/training-plans/{id}/exercises` Endpoints + +2. [... weitere ...] + +--- + +## 7. Kritische Findings (Must-Fix vor Implementation) + +### 🔴 BLOCKER (Implementation NICHT starten ohne Fix) + +1. **Exercise Blocks fehlen komplett** + - Impact: Kern-Anforderung #4 nicht erfüllt + - Fix: Migration 017 + API-Endpoints + UI-Komponenten spezifizieren + +2. **MediaWiki-Import nicht spezifiziert** + - Impact: Kern-Anforderung #7 nicht erfüllt + - Fix: Neue Spec MEDIAWIKI_IMPORT_SPEC.md erstellen + +### 🟠 MAJOR (Fix vor Phase 1 Abschluss) + +1. **age_groups: JSONB vs. M:N Inkonsistenz** + - Impact: Daten-Modell unklar, spätere Migration schwierig + - Fix: Entscheide für M:N (Konsistenz) oder JSONB (Einfachheit) + +2. [... weitere ...] + +### 🟡 MINOR (Fix in Phase 2 akzeptabel) + +1. **Semantic Matching (Migration 015) optional** + - Impact: Nice-to-have Feature, kein MVP-Blocker + - Fix: Kein Fix nötig, als Phase 2 markieren + +2. [... weitere ...] + +--- + +## 8. Empfehlungen + +### 8.1 Vor Implementation starten + +1. **Fixe alle BLOCKER:** Exercise Blocks + MediaWiki-Import spezifizieren +2. **Kläre MAJOR Issues:** age_groups Inkonsistenz auflösen +3. **Ergänze Extended Specs:** VARIANTS_PROGRESSION_SPEC.md, EXERCISE_BLOCKS_SPEC.md, PERMISSIONS_VISIBILITY_SPEC.md +4. **User-Review:** Zeige Specs dem User, hole Feedback zu Blocks/Template-Blocks + +### 8.2 Implementation-Reihenfolge (falls APPROVED) + +**Phase 1A - Foundation:** +1. Migration 014 (Search Vector + Variant Progression) +2. Migration 016 (Saved Searches) +3. Backend: GET/POST/PUT/DELETE `/exercises` + `/exercises/{id}/variants` +4. Frontend: ExercisesListPage + ExerciseDetailPage (ohne Blocks) + +**Phase 1B - Media:** +1. Backend: `/exercises/{id}/media` Endpoints +2. Frontend: MediaUploader + MediaGallery Komponenten + +**Phase 1C - Search:** +1. Backend: Search + Filter in GET `/exercises` +2. Frontend: SearchFilterBar Komponente + +**Phase 2 - Blocks (nach BLOCKER-Fix):** +1. Migration 017 (Exercise Blocks) +2. Backend: `/exercise-blocks` Endpoints +3. Frontend: BlockEditor Komponente + +**Phase 3 - Import:** +1. MEDIAWIKI_IMPORT_SPEC.md erstellen +2. Backend: MediaWiki API Connector +3. Frontend: Import-UI + +--- + +## 9. Positive Findings ✅ + +[Was ist gut gelaufen? Was ist besonders gut spezifiziert?] + +1. **Sehr detaillierte API-Spec:** Alle Request/Response-Beispiele vorhanden +2. **Konsistentes M:N Pattern:** is_primary Flag durchgängig genutzt +3. **Performance-bewusst:** Indizes, Caching, Lazy Loading spezifiziert +4. **Accessibility:** ARIA, Keyboard-Nav, Skip-Links definiert +5. [... weitere ...] + +--- + +## 10. Nächste Schritte + +### Für Spec-Autor (Original Agent): +1. [ ] Fixe BLOCKER #1: Exercise Blocks spezifizieren +2. [ ] Fixe BLOCKER #2: MediaWiki-Import spezifizieren +3. [ ] Kläre MAJOR Issue: age_groups JSONB vs. M:N +4. [ ] Aktualisiere betroffene Specs (siehe Abschnitt 7) + +### Für User: +1. [ ] Lese Executive Summary + Kritische Findings +2. [ ] Entscheide über Blocks/Template-Blocks Umsetzung +3. [ ] Priorisiere MediaWiki-Import (jetzt oder später?) +4. [ ] Gib Feedback zu age_groups Entscheidung + +### Für Implementation: +- **WARTE** bis alle BLOCKER gefixt sind +- **DANACH:** Starte mit Phase 1A (Foundation) + +--- + +**Review abgeschlossen:** [Datum/Uhrzeit] +**Nächster Review:** [Nach BLOCKER-Fixes] +``` + +--- + +## 7. Wichtige Hinweise für Reviewer + +### 7.1 Sei konkret + +❌ **SCHLECHT:** "Die API-Spec ist inkonsistent" +✅ **GUT:** "API_SPEC.md Zeile 267 zeigt `target_group_id` als FK, aber DOMAIN_MODEL.md Abschnitt 3.3 definiert Zielgruppen als M:N" + +### 7.2 Priorisiere nach Impact + +- **BLOCKER:** Verhindert Implementation-Start +- **MAJOR:** Muss vor Phase 1 Abschluss gefixt werden +- **MINOR:** Kann in Phase 2 gefixt werden + +### 7.3 Schlage Lösungen vor + +Nicht nur Probleme nennen, sondern auch: +- Konkrete Fix-Vorschläge +- Alternative Ansätze +- Trade-offs aufzeigen + +### 7.4 Sei fair + +- Lobe gute Spezifikationen +- Erkenne schwierige Trade-offs an +- Berücksichtige MVP-Scope (nicht alles muss perfekt sein) + +--- + +## 8. Erfolgs-Kriterien für APPROVED + +Specs sind **APPROVED**, wenn: + +1. ✅ Alle User-Anforderungen (1-8) spezifiziert sind +2. ✅ Alle User-Ergänzungen (1-5) korrekt umgesetzt sind +3. ✅ Keine architektonischen Widersprüche existieren +4. ✅ Specs konsistent mit Referenz-Dokumenten sind +5. ✅ Keine BLOCKER-Issues offen sind +6. ✅ Technische Machbarkeit gegeben ist +7. ✅ Implementation-Reihenfolge klar definiert ist + +Specs sind **APPROVED WITH CHANGES**, wenn: +- 1-2 MAJOR Issues existieren (aber klar, wie zu fixen) +- Mehrere MINOR Issues existieren +- Implementation kann starten, während Fixes parallel gemacht werden + +Specs sind **REJECTED**, wenn: +- 3+ BLOCKER Issues existieren +- Fundamentale architektonische Probleme vorhanden +- Große Teile der Anforderungen fehlen + +--- + +## 9. Beispiel-Review-Snippet + +**So sollte ein gutes Review-Finding aussehen:** + +```markdown +### 🔴 BLOCKER #1: Exercise Blocks fehlen in DB-Schema + +**Problem:** +- EXERCISES_ARCHITECTURE.md Abschnitt 2.3 definiert Exercise Blocks als "Gruppierung verschiedener Übungen" +- EXERCISES_DATABASE_FINAL.md (Migration 014-016) enthält KEINE `exercise_blocks` Tabelle +- API_SPEC.md spezifiziert KEINE `/exercise-blocks` Endpoints + +**Impact:** +- User-Anforderung #4 ("Exercise Blocks: Sammlungen verschiedener Übungen") NICHT erfüllt +- Trainingsplan-Integration (Anforderung #6) ohne Blocks schwierig + +**Betroffene Dateien:** +- EXERCISES_DATABASE_FINAL.md (fehlt Migration 017) +- EXERCISES_API_SPEC.md (fehlt Abschnitt "Exercise Blocks") +- UI_COMPONENTS_SPEC.md (fehlt "BlockEditor" Komponente) + +**Lösungsvorschlag:** + +1. **Migration 017: Exercise Blocks** +```sql +CREATE TABLE exercise_blocks ( + id SERIAL PRIMARY KEY, + name VARCHAR(200) NOT NULL, + description TEXT, + is_template BOOLEAN DEFAULT FALSE, + placeholders JSONB, -- für Template-Blocks + created_by INT REFERENCES profiles(id), + created_at TIMESTAMP DEFAULT NOW() +); + +CREATE TABLE exercise_block_items ( + id SERIAL PRIMARY KEY, + block_id INT REFERENCES exercise_blocks(id) ON DELETE CASCADE, + exercise_id INT REFERENCES exercises(id) ON DELETE CASCADE, + sequence_order INT NOT NULL, + notes TEXT, + UNIQUE(block_id, sequence_order) +); +``` + +2. **API-Endpoints ergänzen:** +- `GET /exercise-blocks` - List blocks +- `POST /exercise-blocks` - Create block +- `PUT /exercise-blocks/{id}` - Update block +- `POST /exercise-blocks/{id}/items` - Add exercise to block +- `PUT /exercise-blocks/{id}/items/reorder` - Reorder exercises + +3. **UI-Komponente spezifizieren:** +- BlockEditor mit Drag & Drop für Exercise-Reihenfolge +- Template-Modus mit Platzhalter-UI + +**Aufwand:** ~4-6h (1 Migration + 5 Endpoints + 1 Komponente spezifizieren) + +**Kritikalität:** BLOCKER - Implementation kann ohne Blocks NICHT User-Anforderungen erfüllen +``` + +--- + +## 10. Review starten + +**Bereit? Dann:** + +1. Lies alle 7 Spec-Dokumente gründlich +2. Gehe die Checkliste durch (Abschnitt 4) +3. Erstelle deinen Review-Report (Template Abschnitt 6) +4. Sei konkret, fair und konstruktiv +5. Priorisiere nach Impact (BLOCKER > MAJOR > MINOR) + +**Viel Erfolg beim Review! 🚀** + +--- + +**Letzte Aktualisierung:** 2026-04-24 +**Version:** 1.0 +**Autor:** Claude Code (Spec-Ersteller) diff --git a/.claude/docs/technical/AI_PROMPT_SYSTEM_SPEC.md b/.claude/docs/technical/AI_PROMPT_SYSTEM_SPEC.md new file mode 100644 index 0000000..b0b53bf --- /dev/null +++ b/.claude/docs/technical/AI_PROMPT_SYSTEM_SPEC.md @@ -0,0 +1,602 @@ +# KI-Prompt-System – Universelle Admin-Konfiguration + +**Version:** 1.0 +**Datum:** 2026-04-24 +**Status:** DRAFT +**Autor:** Claude Code +**Vorbild:** Mitai Jinkendo Issue #53 + `backend/routers/prompts.py` + Placeholder-System + +--- + +## 1. Konzept + +### 1.1 Ziel + +Alle KI-Aufrufe in Shinkan sind durch **admin-konfigurierbare Prompt-Templates** +steuerbar. Kein KI-Aufruf ist fest im Code verdrahtet. + +**Admins können:** +- Prompt-Texte anpassen (ohne Code-Änderung) +- Neue Prompt-Typen hinzufügen +- Prompts aktivieren/deaktivieren +- Prompts testen (Preview mit aufgelösten Platzhaltern) +- Platzhalter-Katalog einsehen + +### 1.2 Anwendungsfälle in Shinkan + +| Prompt-Slug | Verwendung | +|-------------|-----------| +| `exercise_summary` | Generiert `exercises.summary` aus goal + execution | +| `exercise_skill_suggestions` | Empfiehlt Skills + Stufen für eine Übung | +| `exercise_category_suggestions` | Empfiehlt Fokusbereich, Stil, Zielgruppe | +| `model_skill_level_description` | Generiert Stufen-Beschreibung in der Fähigkeitsmatrix | +| `training_plan_notes` | Erzeugt Trainer-Notizen für Trainingseinheiten | +| `wiki_import_cleanup` | Bereinigt importierten Wikitext in lesbares Deutsch | + +### 1.3 Architektur (aus Mitai übernommen, vereinfacht) + +``` +Admin konfiguriert Prompt-Template in DB (ai_prompts) + │ + ▼ +KI-Aufruf: Backend lädt Template, löst {{Platzhalter}} auf + │ + ▼ +OpenRouter API → Antwort + │ + ▼ +Antwort wird dem Trainer als Vorschlag angeboten (nicht blind gespeichert) +``` + +--- + +## 2. Datenbank + +### 2.1 Migration (020_ai_prompts.sql) + +```sql +-- Migration 020: AI Prompt System (admin-konfigurierbar) +-- Basis: Mitai Jinkendo prompts system (vereinfacht für Shinkan MVP) +-- Autor: Claude Code + +DO $$ +BEGIN + +-- ============================================================================ +-- AI PROMPTS (Admin-verwaltete Prompt-Templates) +-- ============================================================================ + +CREATE TABLE IF NOT EXISTS ai_prompts ( + id SERIAL PRIMARY KEY, + slug VARCHAR(100) NOT NULL UNIQUE, -- z.B. 'exercise_summary' + display_name VARCHAR(200) NOT NULL, + description TEXT, + + -- Template mit {{Platzhalter}}-Syntax + template TEXT NOT NULL, + + -- Kategorie: welcher Bereich der App nutzt diesen Prompt? + category VARCHAR(50) DEFAULT 'exercise' + CHECK (category IN ('exercise', 'training', 'matrix', 'import', 'admin')), + + -- Output-Format: was gibt die KI zurück? + output_format VARCHAR(10) DEFAULT 'text' + CHECK (output_format IN ('text', 'json')), + + -- JSON Schema für Validierung des KI-Outputs (nur wenn output_format='json') + output_schema JSONB, + + -- System-Default: kann auf Original zurückgesetzt werden + is_system_default BOOLEAN DEFAULT false, + default_template TEXT, -- Backup des originalen Templates + + -- Admin-Controls + active BOOLEAN DEFAULT true, + sort_order INT DEFAULT 0, + + -- Meta + created_by INT REFERENCES profiles(id) ON DELETE SET NULL, + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP DEFAULT NOW() +); + +CREATE INDEX IF NOT EXISTS idx_ai_prompts_slug ON ai_prompts(slug); +CREATE INDEX IF NOT EXISTS idx_ai_prompts_category ON ai_prompts(category); +CREATE INDEX IF NOT EXISTS idx_ai_prompts_active ON ai_prompts(active, sort_order); + +-- ============================================================================ +-- TRIGGER +-- ============================================================================ + +DROP TRIGGER IF EXISTS ai_prompts_update ON ai_prompts; +CREATE TRIGGER ai_prompts_update +BEFORE UPDATE ON ai_prompts +FOR EACH ROW EXECUTE FUNCTION update_timestamp(); + +-- ============================================================================ +-- SYSTEM-DEFAULT PROMPTS +-- ============================================================================ + +INSERT INTO ai_prompts (slug, display_name, description, template, category, output_format, is_system_default, default_template, sort_order) VALUES + +-- 1. Exercise Summary +('exercise_summary', + 'Übungs-Zusammenfassung', + 'Generiert eine kurze Zusammenfassung (2-3 Sätze) einer Übung für Listen und Trainingspläne.', +$$Du bist Assistent für einen Kampfsport-Trainer. +Erstelle eine prägnante Zusammenfassung dieser Übung für die Anzeige in Listen und Trainingsplänen. +Die Zusammenfassung soll: +- 2-3 Sätze lang sein (maximal 200 Zeichen) +- Das Wesentliche: Was trainiert die Übung? Wie läuft sie ab? +- Sachlich und klar (keine Werbebotschaften) +- Auf Deutsch + +Übung: {{exercise_title}} +Fokusbereich: {{exercise_focus_area}} +Ziel: {{exercise_goal}} +Durchführung: {{exercise_execution}} + +Antworte NUR mit dem Zusammenfassungstext, ohne Anführungszeichen.$$, + 'exercise', 'text', true, +$$Du bist Assistent für einen Kampfsport-Trainer. +Erstelle eine prägnante Zusammenfassung dieser Übung für die Anzeige in Listen und Trainingsplänen. +Die Zusammenfassung soll: +- 2-3 Sätze lang sein (maximal 200 Zeichen) +- Das Wesentliche: Was trainiert die Übung? Wie läuft sie ab? +- Sachlich und klar (keine Werbebotschaften) +- Auf Deutsch + +Übung: {{exercise_title}} +Fokusbereich: {{exercise_focus_area}} +Ziel: {{exercise_goal}} +Durchführung: {{exercise_execution}} + +Antworte NUR mit dem Zusammenfassungstext, ohne Anführungszeichen.$$, + 1), + +-- 2. Skill Suggestions +('exercise_skill_suggestions', + 'Fähigkeiten-Empfehlungen', + 'Empfiehlt passende Fähigkeiten + Stufen aus dem Katalog für eine Übung.', +$$Du bist Assistent für einen Kampfsport-Trainer. +Analysiere diese Übung und empfehle passende Fähigkeiten aus dem Katalog. + +Übung: {{exercise_title}} +Fokusbereich: {{exercise_focus_area}} +Ziel: {{exercise_goal}} +Durchführung: {{exercise_execution}} + +Verfügbare Fähigkeiten: +{{skills_catalog}} + +Wähle maximal 5 passende Fähigkeiten. Für jede gib an: +- skill_id (aus der Liste) +- required_level: Voraussetzung (einsteiger|grundlagen|aufbau|fortgeschritten|experte) +- target_level: Ziel nach regelmäßigem Training (gleiche Werte) +- intensity: Trainingsintensität (niedrig|mittel|hoch) +- is_primary: true wenn Hauptfähigkeit + +Antworte NUR als JSON-Array: +[{"skill_id": 1, "required_level": "grundlagen", "target_level": "aufbau", "intensity": "hoch", "is_primary": true}] + +Wenn keine Fähigkeit passt, antworte mit [].$$, + 'exercise', 'json', true, NULL, 2), + +-- 3. Category Suggestions +('exercise_category_suggestions', + 'Kategorie-Empfehlungen', + 'Empfiehlt Fokusbereich, Stilrichtung und Zielgruppe für eine Übung.', +$$Du bist Assistent für einen Kampfsport-Trainer. +Ordne diese Übung in die Katalog-Struktur ein. + +Übung: {{exercise_title}} +Beschreibung: {{exercise_goal}} +Durchführung: {{exercise_execution}} + +Verfügbare Fokusbereiche: {{focus_areas_catalog}} +Verfügbare Stilrichtungen: {{style_directions_catalog}} +Verfügbare Zielgruppen: {{target_groups_catalog}} + +Wähle die passendsten Zuordnungen. Antworte NUR als JSON: +{ + "focus_areas": [{"id": 1, "is_primary": true}], + "style_directions": [{"id": 2, "is_primary": true}], + "target_groups": [{"id": 5, "is_primary": true}] +}$$, + 'exercise', 'json', true, NULL, 3), + +-- 4. Matrix Level Description +('model_skill_level_description', + 'Fähigkeitsmatrix-Stufenbeschreibung', + 'Generiert Beschreibungen für Stufen in der Fähigkeitsmatrix.', +$$Du bist Fachexperte für {{focus_area}} und erstellst Lernziel-Beschreibungen. + +Modell: {{model_name}} +Fokusbereich: {{focus_area}} +Zielgruppe: {{target_group}} +Fähigkeit: {{skill_name}} +Stufenanzahl: {{level_count}} +Stufen: {{level_names}} + +Beschreibe für JEDE Stufe konkret und beobachtbar, was ein Schüler auf dieser Stufe +der Fähigkeit "{{skill_name}}" können soll. + +Antworte NUR als JSON-Array ({{level_count}} Einträge): +[ + {"level_number": 1, "description": "...", "observable_criteria": "..."}, + {"level_number": 2, "description": "...", "observable_criteria": "..."} +]$$, + 'matrix', 'json', true, NULL, 4), + +-- 5. Wiki Import Cleanup +('wiki_import_cleanup', + 'Wiki-Text-Bereinigung', + 'Bereinigt importierten Wikitext in lesbares, strukturiertes Deutsch.', +$$Bereinige folgenden Text aus einem Trainings-Wiki für die Darstellung in einer modernen App. + +Original-Text: +{{wiki_raw_text}} + +Regeln: +- Entferne alle Wiki-Markup-Syntax ([[Links]], {{Templates}}, ==Überschriften==) +- Behalte den fachlichen Inhalt vollständig +- Schreibe in klarem, professionellem Deutsch +- Strukturiere mit Absätzen (nicht mit Wiki-Markup) +- Maximal 1000 Zeichen + +Antworte NUR mit dem bereinigten Text.$$, + 'import', 'text', true, NULL, 5) + +ON CONFLICT (slug) DO NOTHING; + +RAISE NOTICE 'Migration 020 completed successfully (AI Prompt System)'; + +END $$; +``` + +--- + +## 3. Platzhalter-Katalog + +### 3.1 Verfügbare Platzhalter + +Alle `{{Platzhalter}}` werden vom Backend-Service `prompt_resolver.py` aufgelöst. + +**Kontext: exercise** (für exercise_summary, skill_suggestions, category_suggestions) + +| Platzhalter | Beschreibung | Beispielwert | +|-------------|--------------|--------------| +| `{{exercise_title}}` | Titel der Übung | "Maai - Distanzübung" | +| `{{exercise_goal}}` | Ziel (erste 500 Zeichen) | "Distanzgefühl entwickeln..." | +| `{{exercise_execution}}` | Durchführung (erste 500 Zeichen) | "1. Partnerwahl..." | +| `{{exercise_preparation}}` | Vorbereitung | "Matten auslegen..." | +| `{{exercise_focus_area}}` | Primärer Fokusbereich | "Karate" | +| `{{exercise_duration}}` | Dauer | "15-20 min" | +| `{{exercise_group_size}}` | Gruppengröße | "8-12 Personen" | +| `{{skills_catalog}}` | Liste aller Skills: "ID Name (Kategorie)" | "- ID 1: Dachi Waza (Kihon)\n..." | +| `{{focus_areas_catalog}}` | Liste aller Fokusbereiche | "- ID 1: Karate\n..." | +| `{{style_directions_catalog}}` | Liste aller Stilrichtungen | "- ID 1: Shotokan\n..." | +| `{{target_groups_catalog}}` | Liste aller Zielgruppen | "- ID 1: Breitensportler\n..." | + +**Kontext: matrix** (für model_skill_level_description) + +| Platzhalter | Beschreibung | Beispielwert | +|-------------|--------------|--------------| +| `{{model_name}}` | Name des Reifegradmodells | "Karate Shotokan Breitensport" | +| `{{focus_area}}` | Fokusbereich des Modells | "Karate" | +| `{{style_direction}}` | Stilrichtung | "Shotokan" | +| `{{target_group}}` | Zielgruppe | "Breitensportler" | +| `{{skill_name}}` | Fähigkeitsname | "Distanzgefühl" | +| `{{skill_description}}` | Fähigkeitsbeschreibung | "Kontrolle der Kampfdistanz..." | +| `{{level_count}}` | Anzahl der Stufen | "5" | +| `{{level_names}}` | Stufen-Namen kommagetrennt | "Einsteiger, Grundlagen, Aufbau, Fortgeschritten, Experte" | + +**Kontext: import** + +| Platzhalter | Beschreibung | +|-------------|--------------| +| `{{wiki_raw_text}}` | Roher Wikitext aus MediaWiki | + +### 3.2 Platzhalter-Auflösung (Backend) + +```python +# backend/prompt_resolver.py + +class ExercisePromptContext: + """Kontext für exercise-bezogene Prompts.""" + def resolve(self, template: str, exercise_data: dict, db) -> str: + variables = { + "exercise_title": exercise_data.get("title", ""), + "exercise_goal": exercise_data.get("goal", "")[:500], + "exercise_execution": exercise_data.get("execution", "")[:500], + "exercise_preparation": exercise_data.get("preparation", ""), + "exercise_focus_area": self._get_primary_focus_area(exercise_data, db), + "exercise_duration": self._format_duration(exercise_data), + "exercise_group_size": self._format_group_size(exercise_data), + "skills_catalog": self._format_skills_catalog(db), + "focus_areas_catalog": self._format_catalog(db, "focus_areas"), + "style_directions_catalog":self._format_catalog(db, "style_directions"), + "target_groups_catalog": self._format_catalog(db, "target_groups"), + } + return self._replace_placeholders(template, variables) +``` + +### 3.3 Unbekannte Platzhalter + +Wenn ein Platzhalter nicht aufgelöst werden kann: +- `{{unknown_key}}` → bleibt als `[NICHT VERFÜGBAR]` im Template +- Warnung im API-Response +- Kein Abbruch des KI-Aufrufs + +--- + +## 4. API-Endpoints + +### 4.1 Übersicht + +| Method | Endpoint | Beschreibung | +|--------|----------|--------------| +| GET | `/admin/ai-prompts` | Liste aller Prompts (Admin) | +| GET | `/admin/ai-prompts/{id}` | Prompt-Detail | +| POST | `/admin/ai-prompts` | Neuen Prompt anlegen | +| PUT | `/admin/ai-prompts/{id}` | Prompt bearbeiten | +| DELETE | `/admin/ai-prompts/{id}` | Prompt löschen (nur custom, nicht system) | +| POST | `/admin/ai-prompts/{id}/reset` | Auf System-Default zurücksetzen | +| POST | `/admin/ai-prompts/{id}/preview` | Platzhalter auflösen ohne KI-Call | +| GET | `/admin/ai-prompts/placeholders` | Platzhalter-Katalog mit Beschreibungen | +| POST | `/admin/ai-prompts/test` | Prompt mit Beispiel-Daten testen (echter KI-Call) | + +### 4.2 `GET /admin/ai-prompts` + +**Response:** `200 OK` +```json +[ + { + "id": 1, + "slug": "exercise_summary", + "display_name": "Übungs-Zusammenfassung", + "description": "Generiert eine kurze Zusammenfassung...", + "category": "exercise", + "output_format": "text", + "active": true, + "is_system_default": true, + "is_modified": false, + "sort_order": 1 + } +] +``` + +`is_modified`: true wenn `template != default_template` + +--- + +### 4.3 `PUT /admin/ai-prompts/{id}` + +**Request Body:** +```json +{ + "template": "Du bist ein erfahrener Karate-Trainer...\n{{exercise_title}}...", + "active": true, + "display_name": "Übungs-Zusammenfassung (angepasst)" +} +``` + +**Response:** `200 OK` (Prompt-Objekt) + +**Constraints:** +- Nur `template`, `display_name`, `description`, `active`, `sort_order` änderbar +- `slug`, `output_format`, `category` nicht änderbar (würde Code brechen) +- System-Default-Backup bleibt immer erhalten + +--- + +### 4.4 `POST /admin/ai-prompts/{id}/preview` + +Löst Platzhalter auf und zeigt das finale Prompt – **OHNE KI-Call**. + +**Request Body:** +```json +{ + "context": "exercise", + "example_data": { + "exercise_id": 42 + } +} +``` + +**Response:** `200 OK` +```json +{ + "resolved_template": "Du bist Assistent für einen Kampfsport-Trainer.\nÜbung: Maai - Distanzübung\n...", + "placeholders_resolved": ["exercise_title", "exercise_goal"], + "placeholders_missing": [], + "estimated_tokens": 320 +} +``` + +--- + +### 4.5 `GET /admin/ai-prompts/placeholders` + +Liefert den vollständigen Platzhalter-Katalog. + +**Response:** `200 OK` +```json +{ + "categories": { + "exercise": [ + { + "key": "exercise_title", + "placeholder": "{{exercise_title}}", + "description": "Titel der Übung", + "example": "Maai - Distanzübung", + "required_context": "exercise_id oder title direkt" + } + ], + "matrix": [...], + "import": [...] + } +} +``` + +--- + +## 5. Verwendung in Backend-Code + +### 5.1 Standard-Pattern für KI-Aufrufe + +```python +# backend/services/ai_service.py + +async def run_ai_prompt( + slug: str, + context_data: dict, + db, + openrouter_key: str +) -> dict: + """ + Lädt Prompt aus DB, löst Platzhalter auf, ruft KI auf. + Wirft AiNotConfiguredError wenn key fehlt oder Prompt inaktiv. + """ + # 1. Prompt laden + prompt = db.fetchrow("SELECT * FROM ai_prompts WHERE slug=$1 AND active=true", slug) + if not prompt: + raise AiNotConfiguredError(f"Prompt '{slug}' nicht gefunden oder inaktiv") + + # 2. Platzhalter auflösen + resolved = resolve_placeholders( + template=prompt["template"], + context=context_data, + db=db + ) + + # 3. KI-Call + response = await call_openrouter( + prompt=resolved, + model=settings.openrouter_model, + api_key=openrouter_key + ) + + # 4. JSON validieren (wenn output_format='json') + if prompt["output_format"] == "json": + response = parse_and_validate_json(response, prompt["output_schema"]) + + return { + "output": response, + "prompt_slug": slug, + "ai_generated": True, + "model": settings.openrouter_model + } +``` + +### 5.2 Aufruf in exercises Router + +```python +# backend/routers/exercises.py + +@router.post("/exercises/ai/suggest") +async def suggest_for_exercise(data: ExerciseSuggestRequest, session=Depends(require_auth)): + result = {} + + # Summary + try: + summary = await ai_service.run_ai_prompt( + slug="exercise_summary", + context_data={"exercise": data.dict()}, + db=db, + openrouter_key=settings.openrouter_api_key + ) + result["summary"] = summary + except AiNotConfiguredError: + result["summary"] = None + + # Skills + try: + skills = await ai_service.run_ai_prompt( + slug="exercise_skill_suggestions", + context_data={"exercise": data.dict()}, + db=db, + openrouter_key=settings.openrouter_api_key + ) + result["skills"] = skills + except AiNotConfiguredError: + result["skills"] = None + + return result +``` + +--- + +## 6. Frontend: Admin-UI + +### 6.1 Route + +``` +/admin/ai-prompts → AdminAiPromptsPage (Liste) +/admin/ai-prompts/{id} → AdminAiPromptDetailPage (Edit) +``` + +### 6.2 Layout (Prompt-Editor) + +``` +┌───────────────────────────────────────────────┐ +│ ✨ KI-Prompts [+ Neu] │ +│ ───────────────────────────────────────────── │ +│ exercise_summary [Aktiv] [Bearbeiten] │ +│ exercise_skill_suggestions [Aktiv] [Bearbeiten]│ +│ model_skill_level_description [Aktiv] │ +└───────────────────────────────────────────────┘ + +── Detailansicht ────────────────────────────────── +┌───────────────────────────────────────────────┐ +│ Übungs-Zusammenfassung │ +│ slug: exercise_summary · Kategorie: exercise │ +│ ───────────────────────────────────────────── │ +│ Template: │ +│ ┌────────────────────────────────────────────┐ │ +│ │ Du bist Assistent für einen Kampfsport- │ │ +│ │ Trainer. ... │ │ +│ │ {{exercise_title}} │ │ ← Syntaxhighlight +│ │ {{exercise_goal}} │ │ +│ └────────────────────────────────────────────┘ │ +│ [Verfügbare Platzhalter ▼] [Vorschau] │ +│ ───────────────────────────────────────────── │ +│ [Mit Beispiel testen] [Original wiederherstellen]│ +│ [Speichern] │ +└───────────────────────────────────────────────┘ +``` + +**Syntax-Highlighting:** `{{placeholder}}` in Farbe hervorheben. +**Platzhalter-Panel:** Ausklappbare Liste aller verfügbaren Platzhalter (aus `/admin/ai-prompts/placeholders`). + +--- + +## 7. Zusammenspiel KI_FEATURES_SPEC ↔ AI_PROMPT_SYSTEM_SPEC + +Die `KI_FEATURES_SPEC.md` beschreibt die **User-Flows** (wann wird KI angeboten, +wie sieht die Bestätigung aus). + +Die `AI_PROMPT_SYSTEM_SPEC.md` (diese Datei) beschreibt die **technische Basis** +(DB-Schema, Platzhalter-System, Admin-UI). + +``` +KI_FEATURES_SPEC: POST /exercises/ai/suggest + │ + ▼ +AI_PROMPT_SYSTEM_SPEC: ai_service.run_ai_prompt("exercise_summary", ...) + │ + ▼ + DB: ai_prompts WHERE slug='exercise_summary' + │ + ▼ + Template + {{Platzhalter}} auflösen + │ + ▼ + OpenRouter API +``` + +--- + +**Version:** 1.0 +**Datum:** 2026-04-24 +**Status:** DRAFT diff --git a/.claude/docs/technical/EXERCISES_API_SPEC.md b/.claude/docs/technical/EXERCISES_API_SPEC.md new file mode 100644 index 0000000..c6a6f35 --- /dev/null +++ b/.claude/docs/technical/EXERCISES_API_SPEC.md @@ -0,0 +1,934 @@ +# Exercises API Specification + +**Version:** 1.2 +**Datum:** 2026-04-24 +**Status:** REVIEWED - Pending Implementation +**Autor:** Claude Code +**Änderungen v1.2:** KI-Assistenz Endpoints, Skill-Level-System (benannte Stufen), intensity als low/medium/high +**Änderungen v1.1:** Exercise Blocks Endpoints, Permissions dokumentiert, age_groups korrigiert + +--- + +## Base URL + +``` +Production: https://shinkan.jinkendo.de/api +Development: https://dev.shinkan.jinkendo.de/api +``` + +--- + +## Authentication + +**Header:** `X-Auth-Token: ` + +**Alle Endpoints** (außer `/auth/*`) erfordern Authentication. + +--- + +## Endpoints Overview + +| Method | Endpoint | Beschreibung | +|--------|----------|--------------| +| **Exercises** | +| GET | `/exercises` | List exercises mit Filtern | +| GET | `/exercises/{id}` | Exercise Detail mit Enrichment | +| POST | `/exercises` | Create exercise | +| PUT | `/exercises/{id}` | Update exercise | +| DELETE | `/exercises/{id}` | Delete exercise | +| **Variants** | +| POST | `/exercises/{id}/variants` | Create variant | +| PUT | `/exercises/{id}/variants/{variant_id}` | Update variant | +| DELETE | `/exercises/{id}/variants/{variant_id}` | Delete variant | +| PUT | `/exercises/{id}/variants/reorder` | Reorder variants (DnD) | +| **Media** | +| POST | `/exercises/{id}/media` | Upload/Embed media | +| PUT | `/exercises/{id}/media/{media_id}` | Update media metadata | +| DELETE | `/exercises/{id}/media/{media_id}` | Delete media | +| PUT | `/exercises/{id}/media/reorder` | Reorder media (DnD) | +| **KI-Assistenz** | +| POST | `/exercises/ai/suggest` | KI-Vorschläge (Summary + Skills) für neues Formular | +| POST | `/exercises/{id}/ai/regenerate` | KI-Vorschläge neu generieren für bestehende Übung | +| **Exercise Blocks** | +| GET | `/exercise-blocks` | List blocks (mit Filtern) | +| GET | `/exercise-blocks/{id}` | Block Detail mit Items | +| POST | `/exercise-blocks` | Create block | +| PUT | `/exercise-blocks/{id}` | Update block | +| DELETE | `/exercise-blocks/{id}` | Delete block | +| POST | `/exercise-blocks/{id}/items` | Add item to block | +| PUT | `/exercise-blocks/{id}/items/{item_id}` | Update item | +| DELETE | `/exercise-blocks/{id}/items/{item_id}` | Remove item | +| PUT | `/exercise-blocks/{id}/items/reorder` | Reorder items (DnD) | + +--- + +## Exercises + +### `GET /exercises` + +**Query Parameters:** +- `focus_area` (int, optional) - Focus Area ID +- `visibility` (enum, optional) - `private | club | official` +- `status` (enum, optional) - `draft | in_review | approved | archived` +- `skill_id` (int, optional) - Skill ID +- `search` (string, optional) - Volltext (title, summary, execution) +- `limit` (int, optional, default: 50, max: 100) +- `offset` (int, optional, default: 0) + +**Response:** `200 OK` +```json +[ + { + "id": 1, + "title": "Maai - Distanzübung", + "summary": "Distanzgefühl entwickeln...", + "focus_area": "karate", + "visibility": "club", + "status": "approved", + "created_by": 1, + "creator_name": "Lars", + "club_id": 1, + "club_name": "Dojo Berlin", + "created_at": "2026-04-20T10:00:00Z", + "updated_at": "2026-04-22T14:30:00Z" + } +] +``` + +**Errors:** +- `401` - Unauthorized +- `403` - Forbidden + +--- + +### `GET /exercises/{id}` + +**Path Parameters:** +- `id` (int, required) + +**Response:** `200 OK` +```json +{ + "id": 1, + "title": "Maai - Distanzübung", + "summary": "Kurzbeschreibung...", + "goal": "Distanzgefühl entwickeln durch Partnerübungen...", + "execution": "1. Partnerwahl\n2. Ausgangsstellung Zenkutsu Dachi...", + "preparation": "Matten auslegen, Pratzen bereitlegen", + "trainer_notes": "Auf korrekten Abstand achten!", + "equipment": ["Matten", "Pratzen"], + "duration_min": 15, + "duration_max": 20, + "group_size_min": 8, + "group_size_max": 12, + + "focus_areas": [ + { + "id": 1, + "focus_area_id": 1, + "name": "Karate", + "abbreviation": "KAR", + "color": "#E63946", + "icon": "🥋", + "is_primary": true + } + ], + "training_styles": [ + { + "id": 1, + "training_style_id": 2, + "name": "Shotokan", + "abbreviation": "SKA", + "is_primary": true + }, + { + "id": 2, + "training_style_id": 3, + "name": "Goju-Ryu", + "abbreviation": "GJR", + "is_primary": false + } + ], + "target_groups": [ + { + "id": 1, + "target_group_id": 5, + "name": "Breitensportler", + "description": "Karate für Freizeit und Fitness", + "is_primary": true + } + ], + "age_groups": ["Kinder", "Teenager"], + + "skills": [ + { + "id": 1, + "skill_id": 10, + "skill_name": "Distanzgefühl", + "skill_category": "Kumite", + "is_primary": true, + "intensity": "hoch", + "required_level": "grundlagen", + "target_level": "aufbau", + "ai_suggested": false + } + ], + + "variants": [ + { + "id": 1, + "variant_name": "Ohne Partner", + "description": "Solo-Variante mit Schattenboxen", + "execution_changes": "Stelle dir einen imaginären Partner vor...", + "duration_min": 10, + "duration_max": 15, + "equipment_changes": [], + "difficulty_adjustment": "easier", + "progression_level": 1, + "sequence_order": 1, + "prerequisite_variant_id": null + }, + { + "id": 2, + "variant_name": "Mit Pratzen", + "description": "Fortgeschrittene Variante mit Pratzen-Training", + "execution_changes": "Partner hält Pratzen, Ausführung wie Haupt...", + "duration_min": 15, + "duration_max": 25, + "equipment_changes": ["+ Pratzen"], + "difficulty_adjustment": "harder", + "progression_level": 3, + "sequence_order": 3, + "prerequisite_variant_id": 1 + } + ], + + "media": [ + { + "id": 1, + "media_type": "video", + "file_path": "/media/exercises/a1b2c3d4_demo.mp4", + "file_size": 5242880, + "mime_type": "video/mp4", + "original_filename": "demo.mp4", + "embed_url": null, + "embed_platform": null, + "title": "Durchführung Demo", + "description": "Zeigt korrekten Abstand", + "sort_order": 1, + "is_primary": true, + "context": "ablauf" + }, + { + "id": 2, + "media_type": "video", + "file_path": null, + "file_size": null, + "mime_type": null, + "original_filename": null, + "embed_url": "https://www.youtube.com/watch?v=abc123", + "embed_platform": "youtube", + "title": "Erweiterte Techniken", + "description": "YouTube-Tutorial von Sensei XY", + "sort_order": 2, + "is_primary": false, + "context": "detail" + } + ], + + "visibility": "club", + "status": "approved", + "created_by": 1, + "creator_name": "Lars", + "club_id": 1, + "club_name": "Dojo Berlin", + "import_source": null, + "import_id": null, + "created_at": "2026-04-20T10:00:00Z", + "updated_at": "2026-04-22T14:30:00Z" +} +``` + +**Errors:** +- `401` - Unauthorized +- `403` - Forbidden (private + not owner) +- `404` - Not found + +--- + +### `POST /exercises` + +**Request Body:** +```json +{ + "title": "Neue Übung", + "summary": "Kurzbeschreibung...", + "goal": "Ziel der Übung...", + "execution": "Durchführung Schritt für Schritt...", + "preparation": "Aufbau und Vorbereitung...", + "trainer_notes": "Hinweise für Trainer...", + "equipment": ["Matten", "Pratzen"], + "duration_min": 15, + "duration_max": 20, + "group_size_min": 8, + "group_size_max": 12, + + "focus_areas_multi": [ + {"focus_area_id": 1, "is_primary": true}, + {"focus_area_id": 2, "is_primary": false} + ], + "training_styles_multi": [ + {"training_style_id": 2, "is_primary": true} + ], + "target_groups_multi": [ + {"target_group_id": 5, "is_primary": true} + ], + "age_groups": ["Kinder", "Teenager"], + + "skills": [ + { + "skill_id": 10, + "is_primary": true, + "intensity": "hoch", + "required_level": "grundlagen", + "target_level": "aufbau" + } + ], + + "visibility": "private", + "status": "draft", + "club_id": 1 +} +``` + +**Required Fields:** +- `title` (3-300 chars) +- `goal` (10-5000 chars) +- `execution` (10-10000 chars) + +**Response:** `201 Created` (full exercise object wie GET) + +**Errors:** +- `400` - Bad Request (validation error) +- `401` - Unauthorized +- `403` - Forbidden + +--- + +### `PUT /exercises/{id}` + +**Request Body:** Same as POST (all fields optional except id) + +**Response:** `200 OK` (full exercise object) + +**Errors:** +- `400` - Bad Request +- `401` - Unauthorized +- `403` - Forbidden (not owner) +- `404` - Not found + +--- + +### `DELETE /exercises/{id}` + +**Response:** `200 OK` +```json +{"ok": true} +``` + +**Errors:** +- `401` - Unauthorized +- `403` - Forbidden (not owner or admin) +- `404` - Not found +- `409` - Conflict (used in training units) + +--- + +## Variants + +### `POST /exercises/{id}/variants` + +**Request Body:** +```json +{ + "variant_name": "Mit Pratzen", + "description": "Fortgeschrittene Variante...", + "execution_changes": "Statt freier Distanz mit Pratzen...", + "duration_min": 15, + "duration_max": 25, + "equipment_changes": ["+ Pratzen"], + "difficulty_adjustment": "harder", + "progression_level": 3, + "sequence_order": 3, + "prerequisite_variant_id": 1 +} +``` + +**Required Fields:** +- `variant_name` (3-200 chars) + +**Response:** `201 Created` +```json +{ + "id": 2, + "exercise_id": 1, + "variant_name": "Mit Pratzen", + "description": "...", + "progression_level": 3, + "sequence_order": 3, + "prerequisite_variant_id": 1, + "created_at": "2026-04-24T10:00:00Z" +} +``` + +**Errors:** +- `400` - Bad Request +- `401` - Unauthorized +- `403` - Forbidden (not owner) +- `404` - Exercise not found +- `409` - Conflict (prerequisite_variant_id nicht zur gleichen Übung) + +--- + +### `PUT /exercises/{id}/variants/{variant_id}` + +**Request Body:** Same as POST + +**Response:** `200 OK` (variant object) + +--- + +### `DELETE /exercises/{id}/variants/{variant_id}` + +**Response:** `200 OK` +```json +{"ok": true} +``` + +**Errors:** +- `409` - Conflict (andere Varianten referenzieren diese als Prerequisite) + +--- + +### `PUT /exercises/{id}/variants/reorder` + +**Request Body:** +```json +{ + "variant_ids": [3, 1, 2] +} +``` + +**Response:** `200 OK` +```json +{"ok": true, "reordered": 3} +``` + +**Errors:** +- `400` - variant_ids nicht vollständig oder falsche Exercise + +--- + +## Media + +### `POST /exercises/{id}/media` + +**Content-Type:** `multipart/form-data` + +**Form Fields:** +- `file` (File, optional) - Image/Video file +- `embed_url` (string, optional) - YouTube/Instagram/Vimeo URL +- `media_type` (enum, required) - `image | video | document | sketch` +- `title` (string, optional) +- `description` (string, optional) +- `context` (enum, optional) - `ablauf | detail | trainer_hint` + +**Entweder `file` ODER `embed_url` (nicht beides).** + +**Response:** `201 Created` +```json +{ + "id": 5, + "exercise_id": 1, + "media_type": "video", + "file_path": "/media/exercises/a1b2c3d4_demo.mp4", + "file_size": 5242880, + "mime_type": "video/mp4", + "original_filename": "demo.mp4", + "embed_url": null, + "embed_platform": null, + "title": "Demo", + "description": null, + "sort_order": 3, + "is_primary": false, + "context": "ablauf", + "created_at": "2026-04-24T10:00:00Z" +} +``` + +**Errors:** +- `400` - Bad Request (invalid file type, missing file/embed_url) +- `401` - Unauthorized +- `403` - Forbidden +- `413` - File too large (> 50MB) + +**Supported Formats:** +- **Images:** `image/jpeg`, `image/png`, `image/gif` +- **Videos:** `video/mp4` +- **Documents:** `application/pdf` +- **Embeds:** `youtube.com`, `youtu.be`, `instagram.com`, `vimeo.com` + +--- + +### `PUT /exercises/{id}/media/{media_id}` + +**Request Body:** +```json +{ + "title": "Neuer Titel", + "description": "Neue Beschreibung", + "is_primary": true, + "context": "detail" +} +``` + +**Response:** `200 OK` (media object) + +--- + +### `DELETE /exercises/{id}/media/{media_id}` + +**Response:** `200 OK` +```json +{"ok": true} +``` + +**Deletes:** DB-Eintrag + Datei (wenn `file_path` gesetzt) + +--- + +### `PUT /exercises/{id}/media/reorder` + +**Request Body:** +```json +{ + "media_ids": [2, 1, 3] +} +``` + +**Response:** `200 OK` +```json +{"ok": true, "reordered": 3} +``` + +--- + +## KI-Assistenz + +### `POST /exercises/ai/suggest` + +Generiert KI-Vorschläge für eine **noch nicht gespeicherte** Übung. +Wird beim Klick auf „✨ KI-Vorschlag" im Formular aufgerufen. + +**Request Body:** +```json +{ + "title": "Maai - Distanzübung", + "goal": "Distanzgefühl entwickeln...", + "execution": "1. Partnerwahl\n2. Ausgangsstellung..." +} +``` + +**Mindestanforderung:** `goal` oder `execution` muss vorhanden sein (min. 50 Zeichen) + +**Response:** `200 OK` +```json +{ + "summary": { + "text": "Partnerübung zur Entwicklung des Distanzgefühls. Trainiert räumliche Wahrnehmung und reaktives Verhalten.", + "ai_generated": true, + "model": "anthropic/claude-sonnet-4" + }, + "skills": [ + { + "skill_id": 10, + "skill_name": "Distanzgefühl", + "skill_category": "Kumite", + "required_level": "grundlagen", + "target_level": "aufbau", + "intensity": "hoch", + "is_primary": true, + "confidence": 0.92 + }, + { + "skill_id": 15, + "skill_name": "Reaktionsschnelligkeit", + "skill_category": "Athletik", + "required_level": "einsteiger", + "target_level": "grundlagen", + "intensity": "mittel", + "is_primary": false, + "confidence": 0.74 + } + ] +} +``` + +**Errors:** +- `400` - Zu wenig Text (< 50 Zeichen) +- `503` - KI nicht verfügbar (OPENROUTER_API_KEY nicht konfiguriert) + +--- + +### `POST /exercises/{id}/ai/regenerate` + +Generiert Vorschläge für eine **bestehende** Übung neu. + +**Request Body:** +```json +{ + "regenerate": ["summary", "skills"] +} +``` + +**Response:** `200 OK` (gleiche Struktur wie `/ai/suggest`) + +**Hinweis:** Gibt nur Vorschläge zurück – nichts wird automatisch gespeichert. +Trainer muss im Frontend aktiv übernehmen. + +--- + +## Permissions + +### Sichtbarkeits-Workflow + +| Von → Nach | Wer darf das? | +|-----------|---------------| +| `draft → in_review` | Ersteller, Club-Admin | +| `in_review → approved` | Club-Admin, Super-Admin | +| `approved → archived` | Club-Admin, Super-Admin | +| `* → draft` | Super-Admin | + +### Sichtbarkeit (`visibility`) + +| Änderung | Wer darf das? | +|----------|---------------| +| `private → club` | Ersteller, Club-Admin | +| `club → official` | Club-Admin, Super-Admin | +| `official → club` | Super-Admin | + +### Owner-Checks + +- **Bearbeiten** (PUT): Nur Ersteller oder Club-Admin +- **Löschen** (DELETE): Nur Ersteller oder Super-Admin +- **Lesen** (`private`): Nur Ersteller + +**403 Fehler-Beispiel:** +```json +{"detail": "Keine Berechtigung. Nur der Ersteller oder ein Admin kann diese Übung bearbeiten."} +``` + +--- + +## Exercise Blocks + +### `GET /exercise-blocks` + +**Query Parameters:** +- `is_template` (bool, optional) - nur Templates oder nur reguläre Blocks +- `visibility` (enum, optional) - `private | club | official` +- `club_id` (int, optional) - Filter nach Verein +- `search` (string, optional) - Suche in name +- `limit` (int, optional, default: 50) +- `offset` (int, optional, default: 0) + +**Response:** `200 OK` +```json +[ + { + "id": 1, + "name": "Aufwärmblock Kumite", + "description": "Standard-Aufwärmprogramm für Kumite", + "is_template": false, + "item_count": 4, + "club_id": 1, + "club_name": "Dojo Berlin", + "created_by": 1, + "creator_name": "Lars", + "visibility": "club", + "created_at": "2026-04-20T10:00:00Z" + } +] +``` + +--- + +### `GET /exercise-blocks/{id}` + +**Response:** `200 OK` +```json +{ + "id": 1, + "name": "Aufwärmblock Kumite", + "description": "Standard-Aufwärmprogramm für Kumite", + "goal": "Sportler auf Kumite-Training vorbereiten", + "is_template": false, + "club_id": 1, + "club_name": "Dojo Berlin", + "created_by": 1, + "creator_name": "Lars", + "visibility": "club", + "items": [ + { + "id": 1, + "sequence_order": 1, + "is_placeholder": false, + "exercise_id": 5, + "exercise_title": "Maai - Distanzübung", + "exercise_summary": "Distanzgefühl entwickeln...", + "variant_id": null, + "variant_name": null, + "notes": "Leicht anfangen, nur 5min", + "placeholder_criteria": null, + "placeholder_label": null + }, + { + "id": 2, + "sequence_order": 2, + "is_placeholder": true, + "exercise_id": null, + "exercise_title": null, + "variant_id": null, + "notes": "Hier eine Schlag-Übung einsetzen", + "placeholder_criteria": {"focus_area_id": 1, "max_duration": 10}, + "placeholder_label": "Schlag-Übung (max. 10 min)" + } + ], + "created_at": "2026-04-20T10:00:00Z", + "updated_at": "2026-04-22T14:30:00Z" +} +``` + +**Errors:** +- `403` - Forbidden (private Block, nicht Ersteller) +- `404` - Not found + +--- + +### `POST /exercise-blocks` + +**Request Body:** +```json +{ + "name": "Aufwärmblock Kumite", + "description": "Standard-Aufwärmprogramm", + "goal": "Sportler vorbereiten", + "is_template": false, + "visibility": "private", + "club_id": 1 +} +``` + +**Required Fields:** +- `name` (3-200 chars) + +**Response:** `201 Created` (full block object wie GET) + +--- + +### `PUT /exercise-blocks/{id}` + +**Request Body:** Same as POST (all fields optional) + +**Response:** `200 OK` (full block object) + +**Errors:** +- `403` - Forbidden (nicht Ersteller/Admin) +- `404` - Not found + +--- + +### `DELETE /exercise-blocks/{id}` + +**Response:** `200 OK` +```json +{"ok": true} +``` + +**Errors:** +- `403` - Forbidden +- `404` - Not found +- `409` - Conflict (Block in Trainingsplan verwendet) + +--- + +### `POST /exercise-blocks/{id}/items` + +**Request Body (konkretes Exercise):** +```json +{ + "exercise_id": 5, + "variant_id": null, + "sequence_order": 3, + "notes": "Leicht anfangen" +} +``` + +**Request Body (Platzhalter):** +```json +{ + "is_placeholder": true, + "placeholder_label": "Schlag-Übung (max. 10 min)", + "placeholder_criteria": { + "focus_area_id": 1, + "max_duration": 10 + }, + "sequence_order": 4, + "notes": "Variabel je nach Gruppe" +} +``` + +**Response:** `201 Created` +```json +{ + "id": 5, + "block_id": 1, + "sequence_order": 3, + "is_placeholder": false, + "exercise_id": 5, + "exercise_title": "Maai - Distanzübung", + "variant_id": null, + "notes": "Leicht anfangen", + "created_at": "2026-04-24T10:00:00Z" +} +``` + +**Errors:** +- `400` - Bad Request (weder exercise_id noch is_placeholder=true) +- `404` - Block oder Exercise nicht gefunden +- `409` - Conflict (sequence_order bereits vergeben) + +--- + +### `PUT /exercise-blocks/{id}/items/{item_id}` + +**Request Body:** Gleiche Felder wie POST, alle optional + +**Response:** `200 OK` (item object) + +--- + +### `DELETE /exercise-blocks/{id}/items/{item_id}` + +**Response:** `200 OK` +```json +{"ok": true} +``` + +--- + +### `PUT /exercise-blocks/{id}/items/reorder` + +**Request Body:** +```json +{ + "item_ids": [3, 1, 2, 4] +} +``` + +**Response:** `200 OK` +```json +{ + "ok": true, + "reordered": 4, + "items": [ + {"id": 3, "sequence_order": 1}, + {"id": 1, "sequence_order": 2}, + {"id": 2, "sequence_order": 3}, + {"id": 4, "sequence_order": 4} + ] +} +``` + +**Errors:** +- `400` - item_ids unvollständig oder gehören nicht zu diesem Block + +--- + +## Validation Rules + +### Exercise +- `title`: 3-300 chars, unique per club +- `goal`: 10-5000 chars +- `execution`: 10-10000 chars +- `duration_min/max`: 0-480 (8 hours) +- `group_size_min/max`: 1-100 +- `visibility`: enum (private, club, official) +- `status`: enum (draft, in_review, approved, archived) + +### Variant +- `variant_name`: 3-200 chars +- `progression_level`: 1-10 +- `prerequisite_variant_id`: must exist, must be same exercise + +### Media +- File size: max 50MB +- Mime types: `image/jpeg, image/png, image/gif, video/mp4, application/pdf` +- Embed platforms: `youtube, instagram, vimeo` + +### Exercise Block +- `name`: 3-200 chars + +### Exercise Skills +- `required_level`: enum – `einsteiger | grundlagen | aufbau | fortgeschritten | experte` (optional/nullable) +- `target_level`: enum – gleiche Werte (optional/nullable) +- `intensity`: enum – `niedrig | mittel | hoch` (optional/nullable) +- `target_level` sollte >= `required_level` sein (Warnung, kein Fehler) + +### Exercise Block Item +- `sequence_order`: muss unique pro Block sein +- `exercise_id`: muss existieren und zugänglich sein (visibility check) +- `is_placeholder = true`: `exercise_id` muss NULL sein +- `placeholder_criteria`: bekannte Keys nur (focus_area_id, training_style_id, target_group_id, skill_ids, max_duration, min_duration, difficulty, visibility) + +--- + +## Error Response Format + +**Standard:** +```json +{ + "detail": "Human-readable error message" +} +``` + +**Validation (detailliert):** +```json +{ + "detail": "Validation failed", + "errors": [ + {"field": "title", "message": "Titel muss mindestens 3 Zeichen lang sein"}, + {"field": "goal", "message": "Ziel ist ein Pflichtfeld"} + ] +} +``` + +--- + +## HTTP Status Codes + +- `200 OK` - Erfolgreiche GET/PUT/DELETE +- `201 Created` - Erfolgreiche POST +- `400 Bad Request` - Validierung fehlgeschlagen +- `401 Unauthorized` - Kein/ungültiges Token +- `403 Forbidden` - Keine Berechtigung +- `404 Not Found` - Ressource nicht gefunden +- `409 Conflict` - Duplikat, Constraint +- `413 Payload Too Large` - Datei zu groß +- `500 Internal Server Error` - Server-Fehler + +--- + +**Version:** 1.0 +**Letzte Änderung:** 2026-04-24 +**Status:** DRAFT - Awaiting Review diff --git a/.claude/docs/technical/EXERCISES_ARCHITECTURE.md b/.claude/docs/technical/EXERCISES_ARCHITECTURE.md new file mode 100644 index 0000000..42fecfa --- /dev/null +++ b/.claude/docs/technical/EXERCISES_ARCHITECTURE.md @@ -0,0 +1,638 @@ +# 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 diff --git a/.claude/docs/technical/EXERCISES_DATABASE_FINAL.md b/.claude/docs/technical/EXERCISES_DATABASE_FINAL.md new file mode 100644 index 0000000..5510c0b --- /dev/null +++ b/.claude/docs/technical/EXERCISES_DATABASE_FINAL.md @@ -0,0 +1,910 @@ +# Database Schema Final - Exercises System + +**Version:** 1.2 +**Datum:** 2026-04-24 +**Status:** REVIEWED - Pending Implementation +**Autor:** Claude Code +**Änderungen v1.2:** Skill-Level auf benannte Stufen (einsteiger–experte), intensity auf niedrig/mittel/hoch, ai_suggested Felder, summary_ai_generated +**Änderungen v1.1:** age_groups JSONB entfernt, Legacy-DROP ergänzt, Migration 017 (Exercise Blocks) + +--- + +## 1. Übersicht + +**Neue Migrationen:** +- **Migration 014:** Variant Progression System + Search Vector + Legacy-Cleanup +- **Migration 015:** Semantic Matching (OPTIONAL - Phase 2) +- **Migration 016:** Saved Searches +- **Migration 017:** Exercise Blocks + Template Blocks + +**Basis:** Migrationen 001-013 (bereits deployed) + +--- + +## 2. Migration 014: Variant Progression + Search + +### 2.1 Vollständige Migration + +```sql +-- Migration 014: Variant Progression System + Search Vector +-- Autor: Claude Code +-- Datum: 2026-04-24 + +DO $$ +BEGIN + +-- ============================================================================ +-- VARIANT PROGRESSION +-- ============================================================================ + +-- Erweitere exercise_variants Tabelle +ALTER TABLE exercise_variants +ADD COLUMN IF NOT EXISTS progression_level INT DEFAULT 1 CHECK (progression_level BETWEEN 1 AND 10), +ADD COLUMN IF NOT EXISTS sequence_order INT, +ADD COLUMN IF NOT EXISTS prerequisite_variant_id INT REFERENCES exercise_variants(id) ON DELETE SET NULL; + +-- Index für Prerequisites +CREATE INDEX IF NOT EXISTS idx_exercise_variants_prerequisite +ON exercise_variants(prerequisite_variant_id); + +-- ============================================================================ +-- SEARCH VECTOR (Volltext-Suche) +-- ============================================================================ + +-- Füge search_vector zu exercises hinzu +ALTER TABLE exercises +ADD COLUMN IF NOT EXISTS search_vector tsvector; + +-- Index für Volltext-Suche +CREATE INDEX IF NOT EXISTS idx_exercises_search +ON exercises USING gin(search_vector); + +-- Funktion für automatisches Update +CREATE OR REPLACE FUNCTION update_exercises_search_vector() +RETURNS trigger AS $func$ +BEGIN + NEW.search_vector := + setweight(to_tsvector('german', COALESCE(NEW.title, '')), 'A') || + setweight(to_tsvector('german', COALESCE(NEW.summary, '')), 'B') || + setweight(to_tsvector('german', COALESCE(NEW.execution, '')), 'C') || + setweight(to_tsvector('german', COALESCE(NEW.trainer_notes, '')), 'D'); + RETURN NEW; +END; +$func$ LANGUAGE plpgsql; + +-- Trigger +DROP TRIGGER IF EXISTS exercises_search_update ON exercises; +CREATE TRIGGER exercises_search_update +BEFORE INSERT OR UPDATE ON exercises +FOR EACH ROW EXECUTE FUNCTION update_exercises_search_vector(); + +-- Initiales Befüllen (für existierende Zeilen) +UPDATE exercises SET search_vector = ( + setweight(to_tsvector('german', COALESCE(title, '')), 'A') || + setweight(to_tsvector('german', COALESCE(summary, '')), 'B') || + setweight(to_tsvector('german', COALESCE(execution, '')), 'C') || + setweight(to_tsvector('german', COALESCE(trainer_notes, '')), 'D') +) WHERE search_vector IS NULL; + +-- ============================================================================ +-- LEGACY COLUMN CLEANUP +-- Deprecated Felder aus exercises (ersetzt durch M:N Tabellen in Migration 008+) +-- ============================================================================ + +-- age_groups JSONB → ersetzt durch exercise_age_groups M:N (seit Migration 008) +ALTER TABLE exercises DROP COLUMN IF EXISTS age_groups; + +-- focus_area VARCHAR → ersetzt durch exercise_focus_areas M:N (seit Migration 008) +ALTER TABLE exercises DROP COLUMN IF EXISTS focus_area; + +-- secondary_areas JSONB → ersetzt durch exercise_focus_areas M:N +ALTER TABLE exercises DROP COLUMN IF EXISTS secondary_areas; + +-- training_character VARCHAR → ersetzt durch exercise_training_characters M:N (seit Migration 012) +ALTER TABLE exercises DROP COLUMN IF EXISTS training_character; + +-- ============================================================================ +-- ADDITIONAL INDEXES (Performance) +-- ============================================================================ + +-- Häufige Filter +CREATE INDEX IF NOT EXISTS idx_exercises_visibility ON exercises(visibility); +CREATE INDEX IF NOT EXISTS idx_exercises_status ON exercises(status); +CREATE INDEX IF NOT EXISTS idx_exercises_created_at ON exercises(created_at DESC); + +-- M:N Relations (falls noch nicht vorhanden) +CREATE INDEX IF NOT EXISTS idx_exercise_focus_areas_focus +ON exercise_focus_areas(focus_area_id); + +CREATE INDEX IF NOT EXISTS idx_exercise_styles_style +ON exercise_training_styles(training_style_id); + +CREATE INDEX IF NOT EXISTS idx_exercise_target_groups_group +ON exercise_target_groups(target_group_id); + +CREATE INDEX IF NOT EXISTS idx_exercise_skills_skill +ON exercise_skills(skill_id); + +RAISE NOTICE 'Migration 014 completed successfully'; + +END $$; +``` + +--- + +## 3. Migration 015: Semantic Matching (Optional Phase 2) + +**Diese Migration ist optional für MVP Phase 1** + +```sql +-- Migration 015: Semantic Exercise Matching (OPTIONAL - Phase 2) +-- Nutzt pgvector extension für Similarity-Matching +-- Autor: Claude Code +-- Datum: 2026-04-24 + +DO $$ +BEGIN + +-- ============================================================================ +-- SEMANTIC EMBEDDINGS (Optional) +-- ============================================================================ + +-- Benötigt pgvector extension +CREATE EXTENSION IF NOT EXISTS vector; + +-- Erweitere exercises Tabelle +ALTER TABLE exercises +ADD COLUMN IF NOT EXISTS embedding vector(1536); -- OpenAI ada-002 embedding size + +-- Index für Ähnlichkeitssuche +CREATE INDEX IF NOT EXISTS idx_exercises_embedding +ON exercises USING ivfflat (embedding vector_cosine_ops) +WITH (lists = 100); + +-- Funktion für Similarity-Search +CREATE OR REPLACE FUNCTION find_similar_exercises( + query_embedding vector(1536), + limit_count INT DEFAULT 10 +) +RETURNS TABLE ( + exercise_id INT, + title VARCHAR, + similarity FLOAT +) AS $func$ +BEGIN + RETURN QUERY + SELECT + id, + title, + 1 - (embedding <=> query_embedding) AS similarity + FROM exercises + WHERE embedding IS NOT NULL + ORDER BY embedding <=> query_embedding + LIMIT limit_count; +END; +$func$ LANGUAGE plpgsql; + +RAISE NOTICE 'Migration 015 completed successfully (Semantic Matching - OPTIONAL)'; + +END $$; +``` + +**Hinweis:** Embeddings werden über separaten Background-Job befüllt, nicht beim INSERT. + +--- + +## 4. Migration 016: Saved Searches + +```sql +-- Migration 016: Saved Exercise Searches +-- Autor: Claude Code +-- Datum: 2026-04-24 + +DO $$ +BEGIN + +-- ============================================================================ +-- SAVED SEARCHES +-- ============================================================================ + +CREATE TABLE IF NOT EXISTS saved_exercise_searches ( + id SERIAL PRIMARY KEY, + profile_id INT NOT NULL REFERENCES profiles(id) ON DELETE CASCADE, + name VARCHAR(100) NOT NULL, + filters JSONB NOT NULL, + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP DEFAULT NOW() +); + +-- Index für User-Zugriff +CREATE INDEX IF NOT EXISTS idx_saved_searches_profile +ON saved_exercise_searches(profile_id); + +-- Trigger für updated_at +CREATE OR REPLACE FUNCTION update_saved_searches_timestamp() +RETURNS trigger AS $func$ +BEGIN + NEW.updated_at = NOW(); + RETURN NEW; +END; +$func$ LANGUAGE plpgsql; + +DROP TRIGGER IF EXISTS saved_searches_update ON saved_exercise_searches; +CREATE TRIGGER saved_searches_update +BEFORE UPDATE ON saved_exercise_searches +FOR EACH ROW EXECUTE FUNCTION update_saved_searches_timestamp(); + +RAISE NOTICE 'Migration 016 completed successfully'; + +END $$; +``` + +--- + +## 5. Migration 017: Exercise Blocks + Template Blocks + +```sql +-- Migration 017: Exercise Blocks + Template Blocks +-- Autor: Claude Code +-- Datum: 2026-04-24 +-- Zweck: Gruppierung verschiedener Übungen in Blöcken (User-Anforderung #4) +-- Series = Varianten-Progression (via exercise_variants, KEINE eigene Tabelle) +-- Blocks = Verschiedene Übungen in Reihenfolge (DIESE Migration) + +DO $$ +BEGIN + +-- ============================================================================ +-- EXERCISE BLOCKS +-- Eine Sammlung verschiedener Übungen in definierter Reihenfolge +-- ============================================================================ + +CREATE TABLE IF NOT EXISTS exercise_blocks ( + id SERIAL PRIMARY KEY, + name VARCHAR(200) NOT NULL, + description TEXT, + goal TEXT, + + -- Template-Modus: Block mit Platzhaltern für flexible Planung + is_template BOOLEAN DEFAULT false, + + -- Ownership & Sichtbarkeit + club_id INT REFERENCES clubs(id) ON DELETE SET NULL, + created_by INT REFERENCES profiles(id) ON DELETE SET NULL, + visibility VARCHAR(20) DEFAULT 'private' CHECK (visibility IN ('private', 'club', 'official')), + + -- Timestamps + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP DEFAULT NOW() +); + +-- ============================================================================ +-- EXERCISE BLOCK ITEMS +-- Einzelne Positionen innerhalb eines Blocks +-- ============================================================================ + +CREATE TABLE IF NOT EXISTS exercise_block_items ( + id SERIAL PRIMARY KEY, + block_id INT NOT NULL REFERENCES exercise_blocks(id) ON DELETE CASCADE, + + -- Konkrete Übung (NULL wenn is_placeholder = true) + exercise_id INT REFERENCES exercises(id) ON DELETE RESTRICT, + + -- Optionale Variante (kann NULL sein → Haupt-Übung wird genutzt) + variant_id INT REFERENCES exercise_variants(id) ON DELETE SET NULL, + + -- Reihenfolge innerhalb des Blocks + sequence_order INT NOT NULL, + + -- Template-Modus: Platzhalter statt konkreter Übung + is_placeholder BOOLEAN DEFAULT false, + + -- Kriterien für Platzhalter-Auflösung (nur relevant wenn is_placeholder = true) + -- Schema: {"focus_area_id": 1, "max_duration": 10, "skill_ids": [3, 7], "difficulty": "easier"} + -- Alle Felder optional, werden als AND-Filter bei der Übungssuche genutzt + placeholder_criteria JSONB, + + -- Platzhalter-Beschriftung (für UX im Template-Modus) + placeholder_label VARCHAR(100), -- z.B. "Aufwärmübung Schlag", "Hauptübung Kumite" + + -- Zusätzliche Notizen für diese Position + notes TEXT, + + -- Timestamps + created_at TIMESTAMP DEFAULT NOW(), + + -- Constraints + UNIQUE(block_id, sequence_order), + -- Entweder exercise_id ODER is_placeholder=true + CHECK ( + (is_placeholder = false AND exercise_id IS NOT NULL) OR + (is_placeholder = true AND exercise_id IS NULL) + ) +); + +-- ============================================================================ +-- INDEXES +-- ============================================================================ + +CREATE INDEX IF NOT EXISTS idx_exercise_blocks_club ON exercise_blocks(club_id); +CREATE INDEX IF NOT EXISTS idx_exercise_blocks_creator ON exercise_blocks(created_by); +CREATE INDEX IF NOT EXISTS idx_exercise_blocks_visibility ON exercise_blocks(visibility); +CREATE INDEX IF NOT EXISTS idx_exercise_blocks_template ON exercise_blocks(is_template) WHERE is_template = true; + +CREATE INDEX IF NOT EXISTS idx_exercise_block_items_block ON exercise_block_items(block_id); +CREATE INDEX IF NOT EXISTS idx_exercise_block_items_exercise ON exercise_block_items(exercise_id); +CREATE INDEX IF NOT EXISTS idx_exercise_block_items_placeholder ON exercise_block_items(is_placeholder) WHERE is_placeholder = true; + +-- ============================================================================ +-- UPDATED_AT TRIGGER +-- ============================================================================ + +DROP TRIGGER IF EXISTS exercise_blocks_update ON exercise_blocks; +CREATE TRIGGER exercise_blocks_update +BEFORE UPDATE ON exercise_blocks +FOR EACH ROW EXECUTE FUNCTION update_timestamp(); + +RAISE NOTICE 'Migration 017 completed successfully (Exercise Blocks)'; + +END $$; +``` + +### 5.0 Placeholder Criteria Schema + +Das `placeholder_criteria` JSONB-Feld in `exercise_block_items` erlaubt folgende Schlüssel (alle optional, werden als AND-Filter kombiniert): + +```json +{ + "focus_area_id": 1, // INT: Fokusbereich-ID + "training_style_id": 2, // INT: Stil-ID + "target_group_id": 5, // INT: Zielgruppen-ID + "skill_ids": [3, 7], // INT[]: Mindestens eine dieser Fähigkeiten + "max_duration": 15, // INT: Maximale Dauer in Minuten + "min_duration": 5, // INT: Minimale Dauer in Minuten + "difficulty": "easier", // "easier" | "same" | "harder" + "visibility": "club" // "private" | "club" | "official" +} +``` + +**Validierung:** Backend prüft, dass alle vorhandenen Schlüssel bekannte Felder sind und die Werte dem erwarteten Typ entsprechen. + +--- + +## 6. Vollständige Tabellenstruktur (Final State) + +### 5.1 exercises (Kern-Tabelle) + +```sql +CREATE TABLE exercises ( + id SERIAL PRIMARY KEY, + + -- Basis-Info + title VARCHAR(300) NOT NULL, + summary TEXT, + goal TEXT NOT NULL, + execution TEXT NOT NULL, + preparation TEXT, + trainer_notes TEXT, + + -- Dauer & Gruppengröße + duration_min INT, + duration_max INT, + group_size_min INT, + group_size_max INT, + + -- JSONB-Felder + equipment JSONB DEFAULT '[]'::jsonb, + -- HINWEIS: age_groups werden via exercise_age_groups M:N Tabelle verwaltet (nicht JSONB) + + -- Suche + search_vector tsvector, -- NEU in Migration 014 + + -- Semantic Matching (optional) + embedding vector(1536), -- NEU in Migration 015 (optional) + + -- Sichtbarkeit & Status + visibility VARCHAR(20) DEFAULT 'private' CHECK (visibility IN ('private', 'club', 'official')), + status VARCHAR(20) DEFAULT 'draft' CHECK (status IN ('draft', 'in_review', 'approved', 'archived')), + + -- Ownership + created_by INT REFERENCES profiles(id) ON DELETE SET NULL, + club_id INT REFERENCES clubs(id) ON DELETE SET NULL, + + -- Import-Tracking + import_source VARCHAR(50), -- 'mediawiki', 'csv', etc. + import_id VARCHAR(100), -- Original-ID aus Quellsystem + + -- Timestamps + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP DEFAULT NOW() +); +``` + +### 5.2 exercise_variants + +```sql +CREATE TABLE exercise_variants ( + id SERIAL PRIMARY KEY, + exercise_id INT NOT NULL REFERENCES exercises(id) ON DELETE CASCADE, + + -- Variant-Details + variant_name VARCHAR(200) NOT NULL, + description TEXT, + execution_changes TEXT, + + -- Dauer (Override) + duration_min INT, + duration_max INT, + + -- Equipment-Änderungen + equipment_changes JSONB DEFAULT '[]'::jsonb, + + -- Schwierigkeit + difficulty_adjustment VARCHAR(20) CHECK (difficulty_adjustment IN ('easier', 'same', 'harder')), + + -- Progression (NEU in Migration 014) + progression_level INT DEFAULT 1 CHECK (progression_level BETWEEN 1 AND 10), + sequence_order INT, + prerequisite_variant_id INT REFERENCES exercise_variants(id) ON DELETE SET NULL, + + -- Timestamps + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP DEFAULT NOW() +); +``` + +### 5.3 exercise_media + +```sql +CREATE TABLE exercise_media ( + id SERIAL PRIMARY KEY, + exercise_id INT NOT NULL REFERENCES exercises(id) ON DELETE CASCADE, + + -- Media-Type + media_type VARCHAR(20) CHECK (media_type IN ('image', 'video', 'document', 'sketch')), + + -- Lokale Datei (exklusiv mit embed_url) + file_path VARCHAR(500), + file_size INT, + mime_type VARCHAR(100), + original_filename VARCHAR(300), + + -- Embed (exklusiv mit file_path) + embed_url TEXT, + embed_platform VARCHAR(50), -- 'youtube', 'vimeo', 'instagram', 'tiktok' + + -- Metadata + title VARCHAR(200), + description TEXT, + sort_order INT DEFAULT 1, + is_primary BOOLEAN DEFAULT FALSE, + context VARCHAR(50) DEFAULT 'ablauf' CHECK (context IN ('ablauf', 'detail', 'trainer_hint')), + + -- Timestamps + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP DEFAULT NOW(), + + -- Constraint: Entweder file_path ODER embed_url + CHECK ( + (file_path IS NOT NULL AND embed_url IS NULL) OR + (file_path IS NULL AND embed_url IS NOT NULL) + ) +); +``` + +### 5.4 M:N Relation Tables + +**exercise_focus_areas:** +```sql +CREATE TABLE exercise_focus_areas ( + id SERIAL PRIMARY KEY, + exercise_id INT NOT NULL REFERENCES exercises(id) ON DELETE CASCADE, + focus_area_id INT NOT NULL REFERENCES focus_areas(id) ON DELETE RESTRICT, + is_primary BOOLEAN DEFAULT FALSE, + created_at TIMESTAMP DEFAULT NOW(), + UNIQUE(exercise_id, focus_area_id) +); +``` + +**exercise_training_styles:** +```sql +CREATE TABLE exercise_training_styles ( + id SERIAL PRIMARY KEY, + exercise_id INT NOT NULL REFERENCES exercises(id) ON DELETE CASCADE, + training_style_id INT NOT NULL REFERENCES training_styles(id) ON DELETE RESTRICT, + is_primary BOOLEAN DEFAULT FALSE, + created_at TIMESTAMP DEFAULT NOW(), + UNIQUE(exercise_id, training_style_id) +); +``` + +**exercise_target_groups:** +```sql +CREATE TABLE exercise_target_groups ( + id SERIAL PRIMARY KEY, + exercise_id INT NOT NULL REFERENCES exercises(id) ON DELETE CASCADE, + target_group_id INT NOT NULL REFERENCES target_groups(id) ON DELETE RESTRICT, + is_primary BOOLEAN DEFAULT FALSE, + created_at TIMESTAMP DEFAULT NOW(), + UNIQUE(exercise_id, target_group_id) +); +``` + +**exercise_age_groups:** +```sql +CREATE TABLE exercise_age_groups ( + id SERIAL PRIMARY KEY, + exercise_id INT NOT NULL REFERENCES exercises(id) ON DELETE CASCADE, + age_group_name VARCHAR(50) NOT NULL, + created_at TIMESTAMP DEFAULT NOW(), + UNIQUE(exercise_id, age_group_name) +); +``` + +**exercise_skills:** +```sql +CREATE TABLE exercise_skills ( + id SERIAL PRIMARY KEY, + exercise_id INT NOT NULL REFERENCES exercises(id) ON DELETE CASCADE, + skill_id INT NOT NULL REFERENCES skills(id) ON DELETE RESTRICT, + is_primary BOOLEAN DEFAULT FALSE, + + -- Kompetenzmodell: benannte Stufen (nicht numerisch 1-10) + -- NULL = nicht definiert / nicht relevant + required_level VARCHAR(20) CHECK (required_level IN ('einsteiger', 'grundlagen', 'aufbau', 'fortgeschritten', 'experte')), + target_level VARCHAR(20) CHECK (target_level IN ('einsteiger', 'grundlagen', 'aufbau', 'fortgeschritten', 'experte')), + + -- Trainingsintensität: Wie stark wird diese Fähigkeit in der Übung trainiert + intensity VARCHAR(10) CHECK (intensity IN ('niedrig', 'mittel', 'hoch')), + + -- KI-generierte Zuordnung (false = manuell bestätigt) + ai_suggested BOOLEAN DEFAULT false, + + created_at TIMESTAMP DEFAULT NOW(), + UNIQUE(exercise_id, skill_id) +); +``` + +**Skill-Level-Definitionen (Kompetenzmodell):** + +| Stufe | Wert | Beschreibung | Beispiel Distanzgefühl | +|-------|------|--------------|----------------------| +| 1 | `einsteiger` | Erste Berührung mit der Fähigkeit, kein Vorwissen nötig | Versteht das Konzept Distanz | +| 2 | `grundlagen` | Grundprinzipien bekannt, in einfachen Situationen anwendbar | Hält in ruhigen Übungen Distanz | +| 3 | `aufbau` | Sicher in Standard-Situationen, braucht noch Korrektur | Distanzkontrolle in Partnerübungen | +| 4 | `fortgeschritten` | Zuverlässig auch unter Druck, wenig Fehler | Stabile Distanz im Sparring | +| 5 | `experte` | Automatisiert, intuitiv, auch in komplexen Situationen | Feines Distanzgefühl im Wettkampf | + +**Für Migration 014 ergänzen:** +```sql +-- In Migration 014 nach den Variant-Columns ergänzen: +-- Skill-Level auf VARCHAR umstellen (bestehende INT-Werte migrieren) +ALTER TABLE exercise_skills + ALTER COLUMN required_level TYPE VARCHAR(20) USING + CASE required_level + WHEN 0 THEN NULL + WHEN 1 THEN 'einsteiger' + WHEN 2 THEN 'grundlagen' + WHEN 3 THEN 'aufbau' + WHEN 4 THEN 'fortgeschritten' + WHEN 5 THEN 'experte' + ELSE NULL + END, + ALTER COLUMN target_level TYPE VARCHAR(20) USING + CASE target_level + WHEN 0 THEN NULL + WHEN 1 THEN 'einsteiger' + WHEN 2 THEN 'grundlagen' + WHEN 3 THEN 'aufbau' + WHEN 4 THEN 'fortgeschritten' + WHEN 5 THEN 'experte' + ELSE NULL + END, + ALTER COLUMN intensity TYPE VARCHAR(10) USING + CASE + WHEN intensity <= 3 THEN 'niedrig' + WHEN intensity <= 7 THEN 'mittel' + ELSE 'hoch' + END; + +-- Neue Constraints hinzufügen +ALTER TABLE exercise_skills + ADD CONSTRAINT ck_required_level CHECK (required_level IN ('einsteiger', 'grundlagen', 'aufbau', 'fortgeschritten', 'experte')), + ADD CONSTRAINT ck_target_level CHECK (target_level IN ('einsteiger', 'grundlagen', 'aufbau', 'fortgeschritten', 'experte')), + ADD CONSTRAINT ck_intensity CHECK (intensity IN ('niedrig', 'mittel', 'hoch')); + +-- KI-Tracking Feld +ALTER TABLE exercises +ADD COLUMN IF NOT EXISTS summary_ai_generated BOOLEAN DEFAULT false; + +ALTER TABLE exercise_skills +ADD COLUMN IF NOT EXISTS ai_suggested BOOLEAN DEFAULT false; +``` + +**exercise_training_characters:** +```sql +CREATE TABLE exercise_training_characters ( + id SERIAL PRIMARY KEY, + exercise_id INT NOT NULL REFERENCES exercises(id) ON DELETE CASCADE, + training_character_id INT NOT NULL REFERENCES training_characters(id) ON DELETE RESTRICT, + is_primary BOOLEAN DEFAULT FALSE, + created_at TIMESTAMP DEFAULT NOW(), + UNIQUE(exercise_id, training_character_id) +); +``` + +### 6.5 Exercise Blocks (Migration 017) + +```sql +CREATE TABLE exercise_blocks ( + id SERIAL PRIMARY KEY, + name VARCHAR(200) NOT NULL, + description TEXT, + goal TEXT, + is_template BOOLEAN DEFAULT false, + club_id INT REFERENCES clubs(id) ON DELETE SET NULL, + created_by INT REFERENCES profiles(id) ON DELETE SET NULL, + visibility VARCHAR(20) DEFAULT 'private' CHECK (visibility IN ('private', 'club', 'official')), + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP DEFAULT NOW() +); + +CREATE TABLE exercise_block_items ( + id SERIAL PRIMARY KEY, + block_id INT NOT NULL REFERENCES exercise_blocks(id) ON DELETE CASCADE, + exercise_id INT REFERENCES exercises(id) ON DELETE RESTRICT, + variant_id INT REFERENCES exercise_variants(id) ON DELETE SET NULL, + sequence_order INT NOT NULL, + is_placeholder BOOLEAN DEFAULT false, + placeholder_criteria JSONB, + placeholder_label VARCHAR(100), + notes TEXT, + created_at TIMESTAMP DEFAULT NOW(), + UNIQUE(block_id, sequence_order), + CHECK ( + (is_placeholder = false AND exercise_id IS NOT NULL) OR + (is_placeholder = true AND exercise_id IS NULL) + ) +); +``` + +### 6.6 Saved Searches (Migration 016) + +```sql +CREATE TABLE saved_exercise_searches ( + id SERIAL PRIMARY KEY, + profile_id INT NOT NULL REFERENCES profiles(id) ON DELETE CASCADE, + name VARCHAR(100) NOT NULL, + filters JSONB NOT NULL, + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP DEFAULT NOW() +); +``` + +--- + +## 6. Indizes (Komplett-Übersicht) + +```sql +-- Volltext-Suche +CREATE INDEX idx_exercises_search ON exercises USING gin(search_vector); + +-- Semantic Matching (optional) +CREATE INDEX idx_exercises_embedding ON exercises USING ivfflat (embedding vector_cosine_ops) WITH (lists = 100); + +-- Häufige Filter +CREATE INDEX idx_exercises_visibility ON exercises(visibility); +CREATE INDEX idx_exercises_status ON exercises(status); +CREATE INDEX idx_exercises_created_at ON exercises(created_at DESC); +CREATE INDEX idx_exercises_club ON exercises(club_id); +CREATE INDEX idx_exercises_creator ON exercises(created_by); + +-- M:N Relations +CREATE INDEX idx_exercise_focus_areas_exercise ON exercise_focus_areas(exercise_id); +CREATE INDEX idx_exercise_focus_areas_focus ON exercise_focus_areas(focus_area_id); + +CREATE INDEX idx_exercise_styles_exercise ON exercise_training_styles(exercise_id); +CREATE INDEX idx_exercise_styles_style ON exercise_training_styles(training_style_id); + +CREATE INDEX idx_exercise_target_groups_exercise ON exercise_target_groups(exercise_id); +CREATE INDEX idx_exercise_target_groups_group ON exercise_target_groups(target_group_id); + +CREATE INDEX idx_exercise_skills_exercise ON exercise_skills(exercise_id); +CREATE INDEX idx_exercise_skills_skill ON exercise_skills(skill_id); + +CREATE INDEX idx_exercise_characters_exercise ON exercise_training_characters(exercise_id); +CREATE INDEX idx_exercise_characters_character ON exercise_training_characters(training_character_id); + +-- Variants +CREATE INDEX idx_exercise_variants_exercise ON exercise_variants(exercise_id); +CREATE INDEX idx_exercise_variants_prerequisite ON exercise_variants(prerequisite_variant_id); + +-- Media +CREATE INDEX idx_exercise_media_exercise ON exercise_media(exercise_id); +CREATE INDEX idx_exercise_media_primary ON exercise_media(is_primary) WHERE is_primary = true; +CREATE INDEX idx_exercise_media_context ON exercise_media(context); + +-- Saved Searches +CREATE INDEX idx_saved_searches_profile ON saved_exercise_searches(profile_id); + +-- Exercise Blocks +CREATE INDEX idx_exercise_blocks_club ON exercise_blocks(club_id); +CREATE INDEX idx_exercise_blocks_creator ON exercise_blocks(created_by); +CREATE INDEX idx_exercise_blocks_visibility ON exercise_blocks(visibility); +CREATE INDEX idx_exercise_blocks_template ON exercise_blocks(is_template) WHERE is_template = true; + +CREATE INDEX idx_exercise_block_items_block ON exercise_block_items(block_id); +CREATE INDEX idx_exercise_block_items_exercise ON exercise_block_items(exercise_id); +CREATE INDEX idx_exercise_block_items_placeholder ON exercise_block_items(is_placeholder) WHERE is_placeholder = true; +``` + +--- + +## 7. Constraints & Business Rules + +### 7.1 Data Integrity + +**Exercises:** +- `title` mindestens 3 Zeichen +- `goal` mindestens 10 Zeichen +- `execution` mindestens 10 Zeichen +- `duration_min <= duration_max` +- `group_size_min <= group_size_max` + +**Variants:** +- `variant_name` mindestens 3 Zeichen +- `prerequisite_variant_id` muss zum gleichen Exercise gehören + +**Media:** +- Max. 50 MB per file (enforced in Backend) +- Max. 10 media items per exercise (enforced in Backend) +- Entweder `file_path` ODER `embed_url` (enforced in CHECK) + +### 7.2 Cascading Delete + +**ON DELETE CASCADE:** +- exercise → variants +- exercise → media +- exercise → M:N relations (focus_areas, styles, etc.) +- profile → saved searches + +**ON DELETE RESTRICT:** +- focus_area → exercise_focus_areas (verhindert Löschen genutzter Katalog-Einträge) +- skill → exercise_skills +- target_group → exercise_target_groups + +**ON DELETE SET NULL:** +- profile (creator) → exercises +- club → exercises +- variant (prerequisite) → variants + +--- + +## 8. Trigger-Funktionen + +### 8.1 Search Vector Auto-Update + +```sql +CREATE OR REPLACE FUNCTION update_exercises_search_vector() +RETURNS trigger AS $$ +BEGIN + NEW.search_vector := + setweight(to_tsvector('german', COALESCE(NEW.title, '')), 'A') || + setweight(to_tsvector('german', COALESCE(NEW.summary, '')), 'B') || + setweight(to_tsvector('german', COALESCE(NEW.execution, '')), 'C') || + setweight(to_tsvector('german', COALESCE(NEW.trainer_notes, '')), 'D'); + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER exercises_search_update +BEFORE INSERT OR UPDATE ON exercises +FOR EACH ROW EXECUTE FUNCTION update_exercises_search_vector(); +``` + +### 8.2 Updated_At Timestamp + +```sql +CREATE OR REPLACE FUNCTION update_timestamp() +RETURNS trigger AS $$ +BEGIN + NEW.updated_at = NOW(); + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +-- Trigger für alle relevanten Tabellen +CREATE TRIGGER exercises_update_timestamp +BEFORE UPDATE ON exercises +FOR EACH ROW EXECUTE FUNCTION update_timestamp(); + +CREATE TRIGGER exercise_variants_update_timestamp +BEFORE UPDATE ON exercise_variants +FOR EACH ROW EXECUTE FUNCTION update_timestamp(); + +CREATE TRIGGER exercise_media_update_timestamp +BEFORE UPDATE ON exercise_media +FOR EACH ROW EXECUTE FUNCTION update_timestamp(); + +CREATE TRIGGER saved_searches_update_timestamp +BEFORE UPDATE ON saved_exercise_searches +FOR EACH ROW EXECUTE FUNCTION update_timestamp(); +``` + +--- + +## 9. Migrations-Tracking + +**Tabelle: schema_migrations** +```sql +CREATE TABLE IF NOT EXISTS schema_migrations ( + migration_id VARCHAR(50) PRIMARY KEY, + description TEXT, + applied_at TIMESTAMP DEFAULT NOW() +); +``` + +**Nach jeder Migration:** +```sql +INSERT INTO schema_migrations (migration_id, description) VALUES +('014', 'Variant Progression System + Search Vector + Legacy-Cleanup'), +('015', 'Semantic Exercise Matching (OPTIONAL)'), +('016', 'Saved Exercise Searches'), +('017', 'Exercise Blocks + Template Blocks'); +``` + +--- + +## 10. Datenbank-Größenabschätzung + +**Annahmen:** +- 5.000 Übungen +- Durchschnittlich 2 Varianten pro Übung +- Durchschnittlich 3 Media-Items pro Übung +- Durchschnittlich 5 M:N Zuordnungen pro Übung + +**Geschätzte Zeilen:** +``` +exercises: 5.000 +exercise_variants: 10.000 +exercise_media: 15.000 +exercise_focus_areas: 10.000 +exercise_training_styles: 10.000 +exercise_target_groups: 10.000 +exercise_skills: 25.000 +exercise_blocks: 1.000 +exercise_block_items: 5.000 +───────────────────────────────────── +TOTAL: 101.000 Zeilen +``` + +**Speicherbedarf (ohne Media-Files):** +- exercises Tabelle: ~50 MB (mit search_vector + embedding) +- Alle M:N Tabellen: ~10 MB +- Variants + Media: ~5 MB +- **Total DB:** ~65 MB + +**Media-Files (lokal gespeichert):** +- 15.000 Items × 5 MB Durchschnitt = ~75 GB + +--- + +## 11. Performance-Benchmarks + +**Target Query Performance:** +- List Exercises (ohne Filter): < 50ms +- List Exercises (mit 3 Filtern): < 100ms +- Volltext-Suche: < 150ms +- Exercise Detail (enriched): < 80ms +- Semantic Similarity: < 200ms (optional) + +**Optimierungen:** +- GIN-Index für tsvector → 10x schnellere Suche +- IVFFlat-Index für Embeddings → 50x schnellere Similarity-Search +- Partial Index für `is_primary` Media → schnellere Primary-Lookups + +--- + +**Version:** 1.0 +**Letzte Änderung:** 2026-04-24 +**Status:** DRAFT - Awaiting Review diff --git a/.claude/docs/technical/EXERCISES_FRONTEND_ROUTING.md b/.claude/docs/technical/EXERCISES_FRONTEND_ROUTING.md new file mode 100644 index 0000000..86b6efb --- /dev/null +++ b/.claude/docs/technical/EXERCISES_FRONTEND_ROUTING.md @@ -0,0 +1,681 @@ +# Frontend Routing & Navigation Specification + +**Version:** 1.0 +**Datum:** 2026-04-24 +**Status:** DRAFT - Awaiting Review +**Autor:** Claude Code + +--- + +## 1. Route-Struktur + +### 1.1 Route-Übersicht + +``` +/exercises → ExercisesListPage (Grid + Filter) +/exercises/new → ExerciseFormPage (Create) +/exercises/{id} → ExerciseDetailPage (Accordion-Layout) +/exercises/{id}/edit → ExerciseFormPage (Edit) +/exercises/{id}/variants/new → VariantFormPage (Create) +/exercises/{id}/variants/{vid}/edit → VariantFormPage (Edit) + +/exercise-blocks → ExerciseBlocksListPage (Meine Blocks) +/exercise-blocks/new → ExerciseBlockFormPage (Create) +/exercise-blocks/{id} → ExerciseBlockDetailPage (mit BlockEditor) +/exercise-blocks/{id}/edit → ExerciseBlockFormPage (Edit) + +/admin/exercises/catalog → AdminCatalogPage (Fokusbereiche, Stile, Zielgruppen, Altersgruppen) +/admin/exercises/skills → AdminSkillsPage (Fähigkeiten-Matrix) +/admin/exercises/methods → AdminMethodsPage (Methoden-Katalog) +``` + +**URL-Parameter-Konventionen:** +- IDs immer numerisch: `/exercises/42` (nicht `/exercises/uuid`) +- Query-Parameter für Filter: `/exercises?focus_area=1&visibility=club` +- Pagination: `/exercises?limit=50&offset=100` +- Sortierung: `/exercises?sort=created_at&order=desc` + +--- + +## 2. Navigation-Patterns + +### 2.1 Haupt-Navigation (Desktop + Tablet) + +**Primäre Nav (Top-Level):** +``` +┌─────────────────────────────────────────┐ +│ Shinkan │ Übungen │ Planung │ Admin │ +└─────────────────────────────────────────┘ +``` + +**Übungen-Submenu (Dropdown):** +``` +Übungen +├─ Alle Übungen → /exercises +├─ Neue Übung → /exercises/new +├─ Meine Blocks → /exercise-blocks +├─ Neuer Block → /exercise-blocks/new +└─ Katalog verwalten → /admin/exercises/catalog (nur Admin) +``` + +### 2.2 Mobile Navigation (Hamburger-Menu) + +``` +☰ Menu +├─ Übungen +│ ├─ Alle anzeigen +│ ├─ Neue Übung +│ └─ Filter & Suche +├─ Planung +└─ Admin + └─ Katalog +``` + +### 2.3 Breadcrumbs (Detail/Edit-Seiten) + +**Pattern:** +``` +Home → Übungen → [Übungsname] → Bearbeiten +Home → Übungen → [Übungsname] → Variante anlegen +``` + +**Implementation:** +- Breadcrumbs dynamisch aus Route-Parametern generiert +- Letztes Element (aktuelle Seite) nicht klickbar +- Max. 4 Ebenen, dann `...` für gekürzte Pfade + +--- + +## 3. State Management + +### 3.1 URL-State (Query-Parameter) + +**Filter-State in URL persistieren:** +```javascript +// ExercisesListPage.jsx +const [filters, setFilters] = useState({ + focus_area: parseInt(searchParams.get('focus_area')) || null, + visibility: searchParams.get('visibility') || null, + status: searchParams.get('status') || null, + search: searchParams.get('search') || '', + limit: parseInt(searchParams.get('limit')) || 50, + offset: parseInt(searchParams.get('offset')) || 0, +}) + +// Bei Filter-Änderung → URL aktualisieren +const updateFilters = (newFilters) => { + const params = new URLSearchParams() + Object.entries(newFilters).forEach(([key, val]) => { + if (val) params.set(key, val) + }) + navigate(`/exercises?${params.toString()}`) +} +``` + +**Vorteile:** +- Sharable URLs (Filter-Zustand übertragbar) +- Browser-Back funktioniert korrekt +- Keine verlorenen Filter beim Reload + +### 3.2 Local State (React useState) + +**Form-State (nicht in URL):** +```javascript +// ExerciseFormPage.jsx +const [formData, setFormData] = useState({ + title: '', + summary: '', + goal: '', + execution: '', + // ... weitere Felder +}) +``` + +**UI-State (nicht in URL):** +```javascript +// ExerciseDetailPage.jsx +const [expandedSections, setExpandedSections] = useState({ + basics: true, + execution: false, + variants: false, + media: false, +}) +``` + +### 3.3 Context (Global State) + +**Katalog-Daten (einmalig laden, global verfügbar):** +```javascript +// CatalogContext.jsx +const CatalogContext = createContext() + +export function CatalogProvider({ children }) { + const [focusAreas, setFocusAreas] = useState([]) + const [trainingStyles, setTrainingStyles] = useState([]) + const [targetGroups, setTargetGroups] = useState([]) + const [ageGroups, setAgeGroups] = useState([]) + const [skills, setSkills] = useState([]) + + useEffect(() => { + // Kataloge einmalig beim App-Start laden + Promise.all([ + api.getFocusAreas(), + api.getTrainingStyles(), + api.getTargetGroups(), + api.getAgeGroups(), + api.getSkills(), + ]).then(([fa, ts, tg, ag, sk]) => { + setFocusAreas(fa) + setTrainingStyles(ts) + setTargetGroups(tg) + setAgeGroups(ag) + setSkills(sk) + }) + }, []) + + return ( + + {children} + + ) +} + +export const useCatalog = () => useContext(CatalogContext) +``` + +**Usage in Komponenten:** +```javascript +// ExerciseFormPage.jsx +const { focusAreas, trainingStyles } = useCatalog() +``` + +--- + +## 4. Deep Linking + +### 4.1 Detail-Seite mit Accordion-State + +**Problem:** Link teilen → Empfänger soll direkt zur "Varianten"-Sektion springen + +**Lösung:** Hash-Fragment in URL: +``` +/exercises/42#variants +/exercises/42#media +``` + +**Implementation:** +```javascript +// ExerciseDetailPage.jsx +useEffect(() => { + const hash = window.location.hash.replace('#', '') + if (hash && expandedSections[hash] !== undefined) { + setExpandedSections(prev => ({ ...prev, [hash]: true })) + + // Scroll zur Sektion + setTimeout(() => { + document.getElementById(hash)?.scrollIntoView({ behavior: 'smooth' }) + }, 100) + } +}, []) +``` + +### 4.2 Externe Verknüpfung aus Trainingsplan + +**Szenario:** Trainingsplan-Editor soll Übung verlinken + +**URL-Format:** +``` +/exercises/42?from=training_plan&plan_id=7 +``` + +**Navigation-Logik:** +```javascript +// ExerciseDetailPage.jsx +const searchParams = useSearchParams() +const returnUrl = searchParams.get('from') === 'training_plan' + ? `/training-plans/${searchParams.get('plan_id')}` + : '/exercises' + +// "Zurück"-Button zeigt entsprechendes Ziel + +``` + +--- + +## 5. Navigation Guards + +### 5.1 Unsaved Changes Warning + +**Pattern:** +```javascript +// ExerciseFormPage.jsx +const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false) + +// Bei Form-Änderung +const handleChange = (field, value) => { + setFormData(prev => ({ ...prev, [field]: value })) + setHasUnsavedChanges(true) +} + +// Browser-Warning bei Reload/Close +useEffect(() => { + const handleBeforeUnload = (e) => { + if (hasUnsavedChanges) { + e.preventDefault() + e.returnValue = '' + } + } + window.addEventListener('beforeunload', handleBeforeUnload) + return () => window.removeEventListener('beforeunload', handleBeforeUnload) +}, [hasUnsavedChanges]) + +// React-Router Warning bei Navigation +const blocker = useBlocker( + ({ currentLocation, nextLocation }) => + hasUnsavedChanges && + currentLocation.pathname !== nextLocation.pathname +) + +useEffect(() => { + if (blocker.state === 'blocked') { + const confirmed = window.confirm( + 'Du hast ungespeicherte Änderungen. Seite wirklich verlassen?' + ) + if (confirmed) { + blocker.proceed() + } else { + blocker.reset() + } + } +}, [blocker]) +``` + +### 5.2 Permission-Check (Admin-Routes) + +**Pattern:** +```javascript +// ProtectedRoute.jsx +function ProtectedRoute({ children, requiredRole = 'admin' }) { + const { profile } = useAuth() + + if (!profile) { + return + } + + if (profile.role !== requiredRole) { + return + } + + return children +} + +// App.jsx + + + +} /> +``` + +--- + +## 6. Performance-Optimierung + +### 6.1 Lazy Loading von Routen + +**Pattern:** +```javascript +// App.jsx +const ExercisesListPage = lazy(() => import('./pages/ExercisesListPage')) +const ExerciseDetailPage = lazy(() => import('./pages/ExerciseDetailPage')) +const ExerciseFormPage = lazy(() => import('./pages/ExerciseFormPage')) + +function App() { + return ( + }> + + } /> + } /> + } /> + + + ) +} +``` + +**Vorteil:** Initial Bundle kleiner, Routen nur bei Bedarf nachgeladen + +### 6.2 Prefetching von Detail-Daten + +**Pattern:** +```javascript +// ExercisesListPage.jsx - Hover-Prefetch +const handleCardHover = (exerciseId) => { + // Prefetch Detail-Daten beim Hover + queryClient.prefetchQuery(['exercise', exerciseId], () => + api.getExercise(exerciseId) + ) +} + + handleCardHover(exercise.id)} + onClick={() => navigate(`/exercises/${exercise.id}`)} +/> +``` + +**Vorteil:** Detail-Seite lädt sofort, da Daten bereits im Cache + +--- + +## 7. Error-Handling + +### 7.1 404 - Exercise Not Found + +**Pattern:** +```javascript +// ExerciseDetailPage.jsx +const { data: exercise, error, isLoading } = useQuery( + ['exercise', exerciseId], + () => api.getExercise(exerciseId) +) + +if (error?.response?.status === 404) { + return ( + + ) +} +``` + +### 7.2 403 - No Permission + +**Pattern:** +```javascript +if (error?.response?.status === 403) { + return ( + + ) +} +``` + +### 7.3 Network Error + +**Pattern:** +```javascript +if (error?.message === 'Network Error') { + return ( + refetch()} + /> + ) +} +``` + +--- + +## 8. Mobile-Spezifische Navigation + +### 8.1 Bottom Navigation (Mobile) + +**Pattern:** +``` +┌─────────────────────────────────────┐ +│ │ +│ (Content Area) │ +│ │ +├─────────────────────────────────────┤ +│ 🏠 Home │ 📋 Übungen │ 📅 Plan │ +└─────────────────────────────────────┘ +``` + +**Implementation:** +```javascript +// MobileBottomNav.jsx + +``` + +**CSS:** +```css +.mobile-bottom-nav { + position: fixed; + bottom: 0; + left: 0; + right: 0; + height: 60px; + background: var(--surface); + border-top: 1px solid var(--border); + display: flex; + justify-content: space-around; + z-index: 100; +} + +@media (min-width: 768px) { + .mobile-bottom-nav { + display: none; /* Desktop nutzt Top-Nav */ + } +} +``` + +### 8.2 Swipe-Navigation (Detail-Seite) + +**Pattern:** Swipe left/right zwischen Übungen (gleicher Filter) + +**Implementation:** +```javascript +// ExerciseDetailPage.jsx +const { exercises } = useExercisesList() // aus Context/Query +const currentIndex = exercises.findIndex(e => e.id === parseInt(exerciseId)) + +const swipeHandlers = useSwipeable({ + onSwipedLeft: () => { + const nextExercise = exercises[currentIndex + 1] + if (nextExercise) navigate(`/exercises/${nextExercise.id}`) + }, + onSwipedRight: () => { + const prevExercise = exercises[currentIndex - 1] + if (prevExercise) navigate(`/exercises/${prevExercise.id}`) + }, + preventDefaultTouchmoveEvent: true, + trackMouse: false, +}) + +
+ {/* Exercise Detail Content */} +
+``` + +--- + +## 9. Accessibility + +### 9.1 Skip Links + +**Pattern:** +```javascript +// App.jsx + + Zum Hauptinhalt springen + + +
+ {/* Content */} +
+``` + +**CSS:** +```css +.skip-link { + position: absolute; + top: -40px; + left: 0; + background: var(--accent); + color: white; + padding: 8px; + z-index: 1000; +} + +.skip-link:focus { + top: 0; +} +``` + +### 9.2 Focus Management + +**Pattern:** Bei Navigation → Focus auf Hauptüberschrift setzen + +```javascript +// ExerciseDetailPage.jsx +const titleRef = useRef(null) + +useEffect(() => { + titleRef.current?.focus() +}, [exerciseId]) + +

+ {exercise.title} +

+``` + +### 9.3 ARIA Live-Region für Filter + +**Pattern:** +```javascript +// ExercisesListPage.jsx +
+ {exercises.length} Übungen gefunden +
+``` + +--- + +## 10. Testing-Strategie + +### 10.1 Route-Tests (Playwright) + +```javascript +// exercises.routing.spec.js +test('Navigation zu Detail-Seite funktioniert', async ({ page }) => { + await page.goto('/exercises') + + const firstCard = page.locator('.exercise-card').first() + await firstCard.click() + + await expect(page).toHaveURL(/\/exercises\/\d+/) + await expect(page.locator('h1')).toBeVisible() +}) + +test('Filter-State bleibt in URL erhalten', async ({ page }) => { + await page.goto('/exercises?focus_area=1') + + await page.reload() + + const url = new URL(page.url()) + expect(url.searchParams.get('focus_area')).toBe('1') +}) +``` + +### 10.2 Navigation-Guard-Tests + +```javascript +test('Unsaved Changes Warning', async ({ page }) => { + await page.goto('/exercises/new') + + await page.fill('[name="title"]', 'Test Übung') + + page.on('dialog', dialog => { + expect(dialog.message()).toContain('ungespeicherte') + dialog.accept() + }) + + await page.goto('/exercises') +}) +``` + +--- + +## 11. Route-Konfiguration (React Router) + +### 11.1 Vollständige Route-Definitionen + +```javascript +// App.jsx +import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom' + +function App() { + return ( + + + + {/* Exercises Routes */} + + } /> + } /> + } /> + } /> + } /> + } /> + + + {/* Exercise Blocks Routes */} + + } /> + } /> + } /> + } /> + + + {/* Admin Routes */} + + + + }> + } /> + } /> + } /> + + + {/* Redirects */} + } /> + } /> + + + + ) +} +``` + +--- + +**Version:** 1.1 +**Letzte Änderung:** 2026-04-24 +**Status:** REVIEWED - Pending Implementation +**Review-Änderungen:** Exercise Blocks Routes + Navigation hinzugefügt diff --git a/.claude/docs/technical/KI_FEATURES_SPEC.md b/.claude/docs/technical/KI_FEATURES_SPEC.md new file mode 100644 index 0000000..e3705c1 --- /dev/null +++ b/.claude/docs/technical/KI_FEATURES_SPEC.md @@ -0,0 +1,370 @@ +# KI-Funktionen Specification – Exercises + +**Version:** 1.1 +**Datum:** 2026-04-24 +**Status:** DRAFT +**Autor:** Claude Code +**Änderungen v1.1:** Prompts sind nicht hardcoded – sie werden aus der DB geladen (AI_PROMPT_SYSTEM_SPEC.md) +**Verwandte Specs:** AI_PROMPT_SYSTEM_SPEC.md (Prompt-DB + Platzhalter), SKILLS_MATRIX_SPEC.md (Fähigkeitsmatrix) + +--- + +## 1. Übersicht + +Zwei KI-gestützte Assistenzfunktionen beim Anlegen und Bearbeiten von Übungen: + +| Funktion | Ziel | +|---------|------| +| **KI-Zusammenfassung** | Generiert `summary` aus `goal` + `execution` (für Listen + Trainingspläne) | +| **KI-Skill-Empfehlung** | Schlägt passende Fähigkeiten + Stufen vor (reduziert manuelle Zuordungsarbeit) | + +**Abhängigkeit:** OpenRouter API (`OPENROUTER_API_KEY` in `.env`). +Ist der Key nicht gesetzt, sind alle KI-Funktionen deaktiviert – Formular funktioniert ohne KI normal. + +--- + +## 2. UX-Prinzip: Manual First, Auto als Fallback + +``` +Trainer gibt goal + execution ein + │ + ├─► [KI-Vorschlag]-Button sichtbar + │ │ + │ ▼ (Klick) + │ KI generiert sofort Vorschlag + │ → Trainer sieht Vorschlag inline + │ → Kann akzeptieren / bearbeiten / ablehnen + │ + └─► Kein Klick bis zum Speichern? + │ + ▼ (automatisch beim POST/PUT) + KI läuft im Hintergrund + → summary + skill_suggestions werden gesetzt + → Trainer kann danach noch bearbeiten +``` + +**Regel:** Manuell generierte Werte (wo Trainer aktiv bestätigt hat) werden +beim Auto-Fallback NICHT überschrieben. Auto-generierte Werte werden als +`ai_generated: true` markiert und können jederzeit neu generiert oder überschrieben werden. + +--- + +## 3. KI-Zusammenfassung (`summary`) + +### 3.1 Was wird generiert + +Aus `title` + `goal` + `execution` generiert die KI eine **kurze Zusammenfassung** +(2-4 Sätze, max. 200 Zeichen) für: +- Übungslisten (Karten-Ansicht) +- Trainingsplan-Darstellung (kompakte Anzeige) +- Export-Berichte + +### 3.2 Prompt-Logik (Backend) + +**Prompt ist NICHT hardcoded** – er wird aus der `ai_prompts` Tabelle geladen +(Slug: `exercise_summary`). Admins können das Template anpassen. + +```python +# Aufruf via zentralem AI-Service (siehe AI_PROMPT_SYSTEM_SPEC.md) +result = await ai_service.run_ai_prompt( + slug="exercise_summary", + context_data={"exercise": exercise_data}, + db=db, + openrouter_key=settings.openrouter_api_key +) +``` + +**Default-Template** (in Migration 020 als `default_template` gespeichert): +Siehe `AI_PROMPT_SYSTEM_SPEC.md` §2.1 – Prompt `exercise_summary`. + +### 3.3 DB-Feld + +Kein neues Feld nötig – `exercises.summary` nimmt den generierten Text auf. +Zusätzliches Tracking-Feld: `summary_ai_generated BOOLEAN DEFAULT false` + +--- + +## 4. KI-Skill-Empfehlung + +### 4.1 Was wird empfohlen + +Aus `title` + `goal` + `execution` + Katalog aller verfügbaren Skills empfiehlt die KI: + +- **Welche Skills** relevant sind (aus dem bestehenden Skill-Katalog) +- **Erforderliche Stufe** (`required_level`) – was der Trainierende mitbringen muss +- **Ziel-Stufe** (`target_level`) – was durch regelmäßige Ausführung erreicht wird +- **Intensität** (`intensity`) – wie stark diese Fähigkeit trainiert wird + +### 4.2 Prompt-Logik (Backend) + +**Prompt ist NICHT hardcoded** – Slug: `exercise_skill_suggestions`. +Der `{{skills_catalog}}`-Platzhalter wird vom Backend automatisch mit der aktuellen +Skill-Liste aus der DB aufgelöst. + +Admins können den Prompt anpassen (z.B. die Anweisung zur Auswahl verschärfen oder +fachliche Hinweise ergänzen). Siehe `AI_PROMPT_SYSTEM_SPEC.md`. + +### 4.3 Bestätigungsflow (UX) + +``` +KI gibt Vorschläge + │ + ▼ +┌─────────────────────────────────────────┐ +│ KI-Vorschläge (3 Fähigkeiten) │ +│ ─────────────────────────────────────── │ +│ ✓ Distanzgefühl │ +│ Voraussetzung: Grundlagen │ +│ Ziel: Aufbau · Intensität: Hoch │ +│ [✓ Übernehmen] [Stufe ändern ▼] [✕] │ +│ ─────────────────────────────────────── │ +│ ✓ Reaktionsschnelligkeit │ +│ Voraussetzung: Einsteiger │ +│ Ziel: Grundlagen · Intensität: Mittel │ +│ [✓ Übernehmen] [Stufe ändern ▼] [✕] │ +│ ─────────────────────────────────────── │ +│ ◦ Gleichgewicht (optional) │ +│ Voraussetzung: Grundlagen │ +│ Ziel: Grundlagen · Intensität: Niedrig│ +│ [✓ Übernehmen] [Stufe ändern ▼] [✕] │ +│ ─────────────────────────────────────── │ +│ [Alle übernehmen] [Verwerfen] │ +└─────────────────────────────────────────┘ +``` + +- Jeder Vorschlag kann **einzeln** übernommen, angepasst oder abgelehnt werden +- „Alle übernehmen" nimmt alle aktuell angezeigten Vorschläge +- Stufen können per Dropdown direkt angepasst werden bevor sie übernommen werden +- Abgelehnte Vorschläge werden nicht gespeichert + +--- + +## 5. API-Endpoints + +### 5.1 Endpoints Overview + +| Method | Endpoint | Beschreibung | +|--------|----------|--------------| +| POST | `/exercises/ai/suggest` | Vorschläge für neue Übung (vor dem Speichern) | +| POST | `/exercises/{id}/ai/regenerate` | Neu generieren für bestehende Übung | + +--- + +### 5.2 `POST /exercises/ai/suggest` + +Liefert KI-Vorschläge auf Basis von Eingabe-Text, **bevor** die Übung gespeichert wurde. +Wird beim Klick auf „KI-Vorschlag" im Formular aufgerufen. + +**Request Body:** +```json +{ + "title": "Maai - Distanzübung", + "goal": "Distanzgefühl entwickeln durch Partnerübungen...", + "execution": "1. Partnerwahl\n2. Ausgangsstellung..." +} +``` + +**Required Fields:** mindestens `goal` ODER `execution` (je länger, desto besser) + +**Response:** `200 OK` +```json +{ + "summary": { + "text": "Partnerübung zur Entwicklung des Distanzgefühls. Trainiert werden räumliche Wahrnehmung und reaktives Verhalten in der Kumite-Distanz.", + "ai_generated": true, + "model": "anthropic/claude-sonnet-4" + }, + "skills": [ + { + "skill_id": 10, + "skill_name": "Distanzgefühl", + "skill_category": "Kumite", + "required_level": "grundlagen", + "target_level": "aufbau", + "intensity": "hoch", + "is_primary": true, + "confidence": 0.92 + }, + { + "skill_id": 15, + "skill_name": "Reaktionsschnelligkeit", + "skill_category": "Athletik", + "required_level": "einsteiger", + "target_level": "grundlagen", + "intensity": "mittel", + "is_primary": false, + "confidence": 0.74 + } + ] +} +``` + +**Errors:** +- `400` - Zu wenig Text (goal und execution leer) +- `503` - KI nicht verfügbar (OPENROUTER_API_KEY fehlt) + +--- + +### 5.3 `POST /exercises/{id}/ai/regenerate` + +Generiert summary und/oder skill-Empfehlungen für eine bestehende Übung neu. +Verwendet die aktuellen gespeicherten Felder als Input. + +**Request Body:** +```json +{ + "regenerate": ["summary", "skills"] +} +``` + +**Werte:** `"summary"` | `"skills"` (oder beide) + +**Response:** `200 OK` (gleiche Struktur wie `/ai/suggest`) + +**Hinweis:** Regenerieren überschreibt NICHT Felder, die manuell gesetzt wurden +(`summary_ai_generated = false`). Es wird nur der Vorschlag zurückgegeben, +Trainer muss aktiv übernehmen. + +--- + +## 6. Datenbank + +### 6.1 Neues Feld: `summary_ai_generated` + +```sql +-- Als Teil von Migration 014 ergänzen: +ALTER TABLE exercises +ADD COLUMN IF NOT EXISTS summary_ai_generated BOOLEAN DEFAULT false; +``` + +**Semantik:** +- `false` (default) = Zusammenfassung manuell geschrieben oder noch leer +- `true` = Zuletzt von KI generiert (kann überschrieben werden) + +Beim manuellen Überschreiben durch den Trainer: `summary_ai_generated = false` setzen. + +### 6.2 Kein Feld für Skill-Empfehlungen + +Skill-Vorschläge der KI werden **nicht** persistent gespeichert – sie erscheinen +nur im Formular-UI und werden erst bei expliziter Bestätigung als reguläre +`exercise_skills`-Einträge gespeichert. + +--- + +## 7. Integration in Speicher-Flow + +### 7.1 Auto-Fallback beim Speichern (POST/PUT exercises) + +```python +# In backend/routers/exercises.py +async def create_exercise(data, session): + # 1. Übung normal speichern + exercise_id = db.insert_exercise(data) + + # 2. Auto-KI nur wenn: + # - OPENROUTER_API_KEY gesetzt + # - summary noch leer + # - goal oder execution vorhanden + if settings.openrouter_api_key and not data.summary and (data.goal or data.execution): + # Background Task (non-blocking) + background_tasks.add_task( + auto_generate_summary, + exercise_id=exercise_id, + title=data.title, + goal=data.goal, + execution=data.execution + ) + + return exercise_id +``` + +### 7.2 Skill-Auto-Suggestion + +Skill-Empfehlungen werden beim Auto-Fallback **NICHT** automatisch gespeichert +(wegen Bestätigungs-Pflicht). Sie werden nur beim manuellen Trigger angeboten. + +--- + +## 8. Frontend-Integration + +### 8.1 ExerciseFormPage – KI-Buttons + +``` +┌──────────────────────────────────────┐ +│ Ziel der Übung * │ +│ ┌────────────────────────────────┐ │ +│ │ Distanzgefühl entwickeln... │ │ +│ └────────────────────────────────┘ │ +│ │ +│ Durchführung * │ +│ ┌────────────────────────────────┐ │ +│ │ 1. Partnerwahl... │ │ +│ └────────────────────────────────┘ │ +│ ↓ │ +│ Zusammenfassung (für Listen) │ +│ ┌────────────────────────────────┐ │ +│ │ [Leer] │ │ +│ └────────────────────────────────┘ │ +│ [✨ KI-Vorschlag] ← Button │ +│ │ +│ ─────────────────────────────────── │ +│ Fähigkeiten │ +│ [Keine zugeordnet] │ +│ [+ Manuell hinzufügen] │ +│ [✨ KI-Empfehlungen] ← Button │ +└──────────────────────────────────────┘ +``` + +### 8.2 KI-Vorschlag für Summary (Inline) + +Beim Klick auf „✨ KI-Vorschlag": +``` +┌──────────────────────────────────────┐ +│ ✨ KI-Vorschlag │ +│ ─────────────────────────────────── │ +│ "Partnerübung zur Entwicklung des │ +│ Distanzgefühls. Trainiert räumliche │ +│ Wahrnehmung und reaktives Verhalten."│ +│ ─────────────────────────────────── │ +│ [✓ Übernehmen] [✕ Verwerfen] │ +└──────────────────────────────────────┘ +``` + +Nach „Übernehmen": Text landet im Summary-Feld, Button zeigt „↺ Neu generieren". + +### 8.3 Anzeige KI-generierter Inhalte + +Felder die KI-generiert sind, werden mit einem kleinen Badge markiert: +``` +Zusammenfassung ✨ KI-generiert [bearbeiten] +``` + +Nach manuellem Bearbeiten verschwindet der Badge. + +--- + +## 9. Konfiguration + +```env +OPENROUTER_API_KEY=sk-or-... +OPENROUTER_MODEL=anthropic/claude-sonnet-4 # Für Zusammenfassung +OPENROUTER_MODEL_FAST=anthropic/claude-haiku-4-5 # Für Skill-Empfehlungen (billiger) +``` + +--- + +## 10. Fehlerbehandlung + +| Szenario | Verhalten | +|----------|-----------| +| OPENROUTER_API_KEY nicht gesetzt | KI-Buttons nicht anzeigen, kein Auto-Fallback | +| API-Timeout (> 10s) | Fehlermeldung inline: "KI momentan nicht verfügbar. Bitte manuell ausfüllen." | +| API-Fehler (5xx) | Gleiche Meldung, kein Absturz | +| Unbekannte Skills in Empfehlung | Backend filtert Skills die nicht im Katalog sind heraus | +| Summary zu lang generiert | Backend kürzt auf 200 Zeichen | + +--- + +**Version:** 1.0 +**Datum:** 2026-04-24 +**Status:** DRAFT diff --git a/.claude/docs/technical/MEDIAWIKI_IMPORT_SPEC.md b/.claude/docs/technical/MEDIAWIKI_IMPORT_SPEC.md new file mode 100644 index 0000000..2afd7f2 --- /dev/null +++ b/.claude/docs/technical/MEDIAWIKI_IMPORT_SPEC.md @@ -0,0 +1,425 @@ +# MediaWiki Import Specification + +**Version:** 1.0 +**Datum:** 2026-04-24 +**Status:** DRAFT - Ready for Review +**Autor:** Claude Code + +--- + +## 1. Übersicht + +**Ziel:** Übungen aus dem Jinkendo MediaWiki-System direkt via API importieren +(NICHT über Export/XML-Dump – siehe Architektur-Entscheidung in EXERCISES_ARCHITECTURE.md §8). + +**Importrichtung:** Einseitig (MediaWiki → Shinkan). Kein bidirektionaler Sync. + +**Import-Reihenfolge:** Skills → Methods → Exercises (Abhängigkeiten zuerst) + +**Import-Tracking:** Jeder Import wird geloggt, Duplikate werden via `wiki_page_title` +erkannt und nicht doppelt angelegt. + +--- + +## 2. MediaWiki API-Zugriff + +### 2.1 API-Endpoint + +``` +GET https://karatetrainer.net/api.php +``` + +**Benötigte Parameter:** +- `action=query` – Seiteninhalte abfragen +- `action=parse` – Einzelseite parsen (mit Semantic MediaWiki Properties) +- `prop=text|properties|categories` – Relevante Felder +- `format=json` + +### 2.2 Authentifizierung + +Aktuell: Kein Auth (wenn Wiki öffentlich zugänglich). +Bei privatem Wiki: `lgtoken` via `/api.php?action=login`. + +### 2.3 Relevante API-Calls + +**Alle Übungen einer Kategorie auflisten:** +``` +GET /api.php?action=query&list=categorymembers&cmtitle=Kategorie:Übungen&cmlimit=500&format=json +``` + +**Einzelne Übung abrufen (mit SMW Properties):** +``` +GET /api.php?action=parse&page=Übungsname&prop=text|properties&format=json +``` + +**SMW-Properties einer Seite:** +``` +GET /api.php?action=browsebysubject&subject=Übungsname&format=json +``` + +--- + +## 3. Field-Mapping (MediaWiki → Exercises) + +**Echte Property-Namen von karatetrainer.net** (via `discover_properties()` ermittelt, 2026-04-24) + +### 3.1 Kern-Felder + +| Wiki-Property (SMW) | Exercises-Feld | Transformation | +|---------------------|---------------|----------------| +| `Übungsbezeichnung` | `title` | Bevorzugt über Seitentitel | +| `Summary` | `summary` | Direkt | +| `Ziel` | `goal` | Direkt | +| `Durchführung` | `execution` | Wikitext → Plaintext | +| `Hinweise` | `trainer_notes` | Wikitext → Plaintext | +| `Plandauer` | `duration_min` = `duration_max` | Zahl in Minuten z.B. `"10"` | +| `Gruppengröße` | `group_size_min` | Zahl z.B. `"2"` | +| `Hilfsmittel` | `equipment` (JSONB) | Array von Strings | + +### 3.2 Katalog-Mapping + +| Wiki-Property | Exercises-Tabelle | Transformation | +|---------------|------------------|----------------| +| `Übungstyp` | `exercise_focus_areas` | z.B. `"Karate"` → focus_area Name → ID | +| `Zielgruppe` | `exercise_target_groups` | Name → ID Lookup | +| `Altersgruppe` | `exercise_age_groups` | Name → ID Lookup | +| `Trainingsmethode` | exercise method ref | Wiki-Seitenname → Label (Unterstriche → Leerzeichen) | +| `PrimaryCapability` | `exercise_skills` (Name) | Skill-Name → ID Lookup | +| `CapabilityLevel` | `exercise_skills` (target_level) | Integer → Named Level (1=einsteiger…5=experte) | +| `Schlüsselworte` | — | Keywords (für zukünftige Tag-Funktion) | + +### 3.3 Skill-Level-Mapping (CapabilityLevel → Stufen) + +| Wiki-Wert | Shinkan-Stufe | +|-----------|---------------| +| 1 | einsteiger | +| 2 | grundlagen | +| 3 | aufbau | +| 4 | fortgeschritten | +| 5 | experte | + +### 3.4 Kategorien (karatetrainer.net) + +| Import-Typ | Wiki-Kategorie | +|------------|---------------| +| exercises | `Übungen` | +| skills | `Fähigkeitsbeschreibung` | +| methods | `Methodenbeschreibung` | + +**Unbekannte Katalog-Werte:** Warnmeldung im Import-Log, Übung wird ohne +diese Zuordnung importiert (kein Abbruch). + +### 3.3 Import-Tracking-Felder + +| Feld | Wert | +|------|------| +| `import_source` | `'mediawiki'` | +| `import_id` | Wiki-Seitenname (URL-kodiert) | +| `visibility` | `'private'` (Trainer prüft vor Veröffentlichung) | +| `status` | `'draft'` | + +### 3.4 Wikitext-Bereinigung + +Vor dem Speichern werden folgende Wikitext-Elemente in Plaintext umgewandelt: +- `[[Link|Text]]` → `Text` +- `[[Link]]` → `Link` +- `{{Template}}` → (entfernt) +- `'''Bold'''` → `Bold` +- `''Italic''` → `Italic` +- `==Überschrift==` → `\nÜberschrift\n` +- Wikitables → (entfernt, zu komplex für automatischen Import) + +--- + +## 4. Tracking-Tabellen + +```sql +-- Migration: wiki_import_tracking +-- (In eigene Migration auslagern: 018_wiki_import_tracking.sql) + +CREATE TABLE IF NOT EXISTS wiki_import_log ( + id SERIAL PRIMARY KEY, + import_type VARCHAR(50) NOT NULL, -- 'exercise', 'skill', 'method' + import_status VARCHAR(20) CHECK (import_status IN ('running', 'completed', 'failed')), + items_total INT DEFAULT 0, + items_imported INT DEFAULT 0, + items_skipped INT DEFAULT 0, + items_failed INT DEFAULT 0, + error_log JSONB DEFAULT '[]'::jsonb, -- Array von {item, error} Objekten + imported_by INT REFERENCES profiles(id), + started_at TIMESTAMP DEFAULT NOW(), + finished_at TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS wiki_import_references ( + id SERIAL PRIMARY KEY, + wiki_page_title VARCHAR(500) NOT NULL, + wiki_page_id INT, -- MediaWiki interne ID + content_type VARCHAR(50) NOT NULL, -- 'exercise', 'skill', 'method' + local_id INT NOT NULL, -- ID in der lokalen DB + last_imported TIMESTAMP DEFAULT NOW(), + created_at TIMESTAMP DEFAULT NOW(), + UNIQUE(wiki_page_title, content_type) +); + +CREATE INDEX IF NOT EXISTS idx_wiki_refs_title ON wiki_import_references(wiki_page_title); +CREATE INDEX IF NOT EXISTS idx_wiki_refs_type ON wiki_import_references(content_type); +``` + +--- + +## 5. API-Endpoints + +### 5.1 Endpoints Overview + +| Method | Endpoint | Beschreibung | +|--------|----------|--------------| +| GET | `/import/mediawiki/preview` | Vorschau: Was wird importiert? | +| POST | `/import/mediawiki/execute` | Import ausführen | +| GET | `/import/mediawiki/status/{log_id}` | Import-Status abrufen | +| GET | `/import/mediawiki/logs` | Alle Import-Logs | +| DELETE | `/import/mediawiki/references/{id}` | Referenz löschen (für Re-Import) | + +### 5.2 `GET /import/mediawiki/preview` + +**Query Parameters:** +- `category` (string, required) – Wiki-Kategorie z.B. `Übungen` +- `limit` (int, optional, default: 10) – Maximale Anzahl zum Voranschauen + +**Response:** `200 OK` +```json +{ + "category": "Übungen", + "total_found": 47, + "preview": [ + { + "wiki_page_title": "Maai - Distanzübung", + "wiki_page_id": 1234, + "already_imported": false, + "last_imported_at": null, + "mapped_fields": { + "title": "Maai - Distanzübung", + "summary": "Distanzgefühl entwickeln...", + "goal": "Ziel...", + "focus_area": "Karate", + "focus_area_found": true + }, + "warnings": [], + "errors": [] + }, + { + "wiki_page_title": "Kumite Grundtechniken", + "already_imported": true, + "last_imported_at": "2026-04-10T10:00:00Z", + "mapped_fields": {...}, + "warnings": ["Feld 'Zielgruppe' hat unbekannten Wert 'Fortgeschrittene' - wird ignoriert"], + "errors": [] + } + ] +} +``` + +--- + +### 5.3 `POST /import/mediawiki/execute` + +**Request Body:** +```json +{ + "category": "Übungen", + "reimport_existing": false, // true: Bereits importierte Übungen überschreiben + "dry_run": false, // true: Nur simulieren, nichts speichern + "limit": null // null: alle importieren +} +``` + +**Response:** `202 Accepted` (Import läuft asynchron) +```json +{ + "log_id": 5, + "status": "running", + "message": "Import gestartet. Verwende GET /import/mediawiki/status/5 für Status." +} +``` + +--- + +### 5.4 `GET /import/mediawiki/status/{log_id}` + +**Response:** `200 OK` +```json +{ + "id": 5, + "import_type": "exercise", + "import_status": "completed", + "items_total": 47, + "items_imported": 44, + "items_skipped": 2, + "items_failed": 1, + "error_log": [ + { + "item": "Unvollständige Übung", + "error": "Pflichtfeld 'Durchführung' fehlt in Wiki-Seite" + } + ], + "imported_by": 1, + "started_at": "2026-04-24T10:00:00Z", + "finished_at": "2026-04-24T10:02:30Z" +} +``` + +--- + +### 5.5 `GET /import/mediawiki/logs` + +**Response:** `200 OK` – Array von Import-Log Objekten (ohne error_log Details) + +--- + +## 6. Backend-Implementation + +### 6.1 Router-Datei + +`backend/routers/import_wiki.py` + +### 6.2 Import-Prozess (Pseudocode) + +```python +async def execute_wiki_import(category: str, reimport: bool, dry_run: bool): + # 1. Alle Seiten der Kategorie via MediaWiki API abrufen + pages = wiki_api.get_category_members(category) + + for page_title in pages: + # 2. Duplikat-Check + existing_ref = db.get_wiki_ref(page_title, 'exercise') + if existing_ref and not reimport: + log.skip(page_title, "bereits importiert") + continue + + # 3. Seite parsen + wiki_data = wiki_api.parse_page(page_title) + + # 4. Field-Mapping + exercise_data = map_wiki_to_exercise(wiki_data) + + # 5. Validierung + errors = validate_exercise(exercise_data) + if errors: + log.fail(page_title, errors) + continue + + # 6. Katalog-IDs auflösen + exercise_data = resolve_catalog_ids(exercise_data) + + if not dry_run: + # 7. Speichern + exercise_id = db.upsert_exercise(exercise_data) + + # 8. Import-Referenz aktualisieren + db.upsert_wiki_ref(page_title, 'exercise', exercise_id) + + log.success(page_title) +``` + +### 6.3 Wikitext Parser + +```python +import re + +def wikitext_to_plaintext(wikitext: str) -> str: + """Konvertiert Wikitext in lesbares Plaintext.""" + text = wikitext + + # Links: [[Link|Text]] → Text, [[Link]] → Link + text = re.sub(r'\[\[([^|]+)\|([^\]]+)\]\]', r'\2', text) + text = re.sub(r'\[\[([^\]]+)\]\]', r'\1', text) + + # Templates entfernen + text = re.sub(r'\{\{[^}]+\}\}', '', text) + + # Formatierung + text = re.sub(r"'''(.+?)'''", r'\1', text) + text = re.sub(r"''(.+?)''", r'\1', text) + + # Überschriften + text = re.sub(r'={2,6}\s*(.+?)\s*={2,6}', r'\n\1\n', text) + + # Mehrfache Leerzeilen normalisieren + text = re.sub(r'\n{3,}', '\n\n', text) + + return text.strip() +``` + +--- + +## 7. Frontend-Import-UI + +### 7.1 Route + +``` +/admin/import/mediawiki → AdminWikiImportPage +``` + +### 7.2 Layout + +``` +┌─────────────────────────────────────────┐ +│ MediaWiki Import │ +│ ─────────────────────────────────────── │ +│ Wiki-Kategorie: [Übungen ] │ +│ [+ Vorschau anzeigen] │ +│ ─────────────────────────────────────── │ +│ Vorschau (10 von 47): │ +│ │ +│ ✅ Maai - Distanzübung (neu) │ +│ ✅ Kizami-Zuki (neu) │ +│ ⚠️ Kumite XY (bereits importiert) │ +│ ❌ Defekte Seite (Pflichtfeld fehlt) │ +│ ... │ +│ ─────────────────────────────────────── │ +│ ☐ Bereits importierte überschreiben │ +│ ☐ Nur simulieren (dry run) │ +│ ─────────────────────────────────────── │ +│ [47 Übungen importieren] │ +│ ─────────────────────────────────────── │ +│ Letzter Import: 44/47 · 2026-04-10 │ +│ [Import-Logs anzeigen] │ +└─────────────────────────────────────────┘ +``` + +--- + +## 8. Fehlerbehandlung + +| Fehler | Verhalten | +|--------|-----------| +| Wiki-Seite nicht erreichbar | Import-Prozess abbricht, Log-Eintrag | +| Pflichtfeld (`goal`, `execution`) fehlt | Item wird übersprungen, Warnung im Log | +| Unbekannter Katalog-Wert (Fokusbereich etc.) | Item wird ohne diese Zuordnung importiert | +| Duplikat-Titel im selben Club | Item wird übersprungen (außer reimport=true) | +| Wiki-API Timeout | Retry 3x, dann fail mit Log-Eintrag | + +--- + +## 9. Sicherheit + +- Nur Super-Admin darf Import ausführen (`require_role('superadmin')`) +- MediaWiki-URL ist konfigurierbar via `.env` (kein Hardcoding) +- Rate-Limiting: max. 1 Import parallel, max. 10/Tag +- Importierte Übungen starten immer als `visibility='private'`, `status='draft'` + +--- + +## 10. Konfiguration (`.env`) + +``` +MEDIAWIKI_API_URL=https://karatetrainer.net/api.php +MEDIAWIKI_USER=import-bot # optional, für privates Wiki +MEDIAWIKI_PASSWORD=... # optional +``` + +--- + +**Version:** 1.0 +**Datum:** 2026-04-24 +**Status:** DRAFT - Ready for Review diff --git a/.claude/docs/technical/MEDIA_UPLOAD_SPEC.md b/.claude/docs/technical/MEDIA_UPLOAD_SPEC.md new file mode 100644 index 0000000..c2ba1dd --- /dev/null +++ b/.claude/docs/technical/MEDIA_UPLOAD_SPEC.md @@ -0,0 +1,778 @@ +# Media Upload & Embed Specification + +**Version:** 1.0 +**Datum:** 2026-04-24 +**Status:** DRAFT - Awaiting Review +**Autor:** Claude Code + +--- + +## 1. Upload-Strategie + +### 1.1 Hybrid-Ansatz + +**Zwei Medienquellen:** +1. **Lokale Uploads:** Datei-Upload auf Server (Images, Videos, PDFs, Sketches) +2. **Embeds:** Einbetten von YouTube, Instagram, Vimeo, TikTok + +**Vorteile:** +- Lokale Kontrolle über kritische Inhalte +- Keine Abhängigkeit von Drittanbietern für Kern-Content +- Externe Plattformen für ergänzende Videos + +**Limitierungen:** +- Max. 50 MB pro Datei +- Max. 10 Media-Items pro Übung (kombiniert Local + Embeds) +- Erlaubte Formate: JPEG, PNG, GIF, MP4, PDF + +--- + +## 2. Upload-Flow (Lokale Dateien) + +### 2.1 Frontend-Komponente: MediaUploader + +**Props:** +```typescript +interface MediaUploaderProps { + exerciseId: number + existingMedia: Media[] + onUploadComplete: (media: Media) => void + maxFiles?: number // default: 10 + maxSizeMB?: number // default: 50 +} +``` + +**UI-Struktur:** +```jsx + +
+ {/* Drag & Drop Area */} + + + {/* Preview Grid */} +
+ {uploadQueue.map(file => ( +
+ + + +
+ ))} +
+ + {/* Metadata Form (per File) */} +
+ +