shinkan-jinkendo/.claude/docs/technical/EXERCISES_API_SPEC.md
Lars f5895b6637
Some checks failed
Deploy Development / deploy (push) Successful in 35s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 6s
Test Suite / playwright-tests (push) Failing after 41s
chore: update documentation and enhance exercise progression graph details
- Updated CLAUDE.md to reflect the addition of exercise_progression_graphs in the backend routers.
- Revised PROJECT_STATUS.md to document the current project status and recent milestones, including the implementation of the exercise progression graph feature.
- Incremented versioning in DOMAIN_MODEL.md and DATABASE_SCHEMA.md to align with the latest migration updates.
- Enhanced technical specifications in TRAINING_FRAMEWORK_SPEC.md to clarify the implementation details of the exercise progression graph and its integration with the training framework.
2026-05-05 08:30:48 +02:00

953 lines
22 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Exercises API Specification
**Version:** 1.4
**Datum:** 2026-04-30
**Status:** Teilweise implementiert (Liste mit Filtern + Varianten + Medienlimits + Progressionsgraphen siehe Code)
**Autor:** Claude Code
**Änderungen v1.4:** Endpoints **`/exercise-progression-graphs`** inkl. Kanten, **`POST …/edges/sequence`**, **`POST …/edges/delete-batch`** — Detailtabellen siehe **`TRAINING_FRAMEWORK_SPEC.md`** §3.3
**Änderungen v1.3:** `GET /exercises` erweiterte Query-Parameter (`include_variants`, Multi-Filter, `ai_search`-Platzhalter); Dokumentation angepasst
**Ä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: <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) |
| **Progressionsgraphen** (Übung→Übung) |
| GET | `/exercise-progression-graphs` | Liste Graphen |
| GET | `/exercise-progression-graphs/{id}` | Detail; Query `include_edges` |
| POST | `/exercise-progression-graphs` | Graph anlegen |
| PUT | `/exercise-progression-graphs/{id}` | Metadaten |
| DELETE | `/exercise-progression-graphs/{id}` | Graph + Kanten |
| GET | `/exercise-progression-graphs/{id}/edges` | Kantenliste |
| POST | `/exercise-progression-graphs/{id}/edges` | Einzelkante |
| POST | `/exercise-progression-graphs/{id}/edges/sequence` | Reihe (`steps`) in einer Transaktion |
| PUT | `/exercise-progression-graphs/{id}/edges/{edge_id}` | z.B. Notiz |
| DELETE | `/exercise-progression-graphs/{id}/edges/{edge_id}` | Kante löschen |
| POST | `/exercise-progression-graphs/{id}/edges/delete-batch` | `{ edge_ids }` |
Vollständige Pfadtabelle, Auth und Feldgrenzen: **`TRAINING_FRAMEWORK_SPEC.md`** §3.
---
## Exercises
### `GET /exercises`
**Query Parameters (Auswahl):**
| Parameter | Beschreibung |
|-----------|----------------|
| `focus_area_ids[]`, `focus_area` | Fokusbereiche (ODER über Liste oder Legacy Einzel-ID) |
| `visibility_any[]`, `visibility` | Sichtbarkeit(en) |
| `status_any[]`, `status` | Status |
| `skill_ids[]`, `skill_id` | Fähigkeit(en) |
| `skill_min_level`, `skill_max_level` | Stufe 15 auf Übung↔Skill-Zuordnung |
| `style_direction_ids[]`, `style_direction_id` | Stilrichtung(en) |
| `training_type_ids[]`, `training_type_id` | Trainingsstil(e) |
| `target_group_ids[]`, `target_group_id` | Zielgruppe(n) |
| `search`, `ai_search` | Volltext (aktuell gleiche Logik; `ai_search` Platzhalter für spätere KI-Suche) |
| `include_variants` | `true`: jedes Listenelement enthält optional kompaktes **`variants`** für UI/Planung |
| `limit` | Default 50, max 100 |
| `offset` | Default 0 |
**Response:** `200 OK`
Lightweight-Liste; bei `include_variants=true` zusätzlich z.B.:
```json
{
"id": 1,
"title": "Maai - Distanzübung",
"variants": [
{ "id": 10, "variant_name": "Basis", "sequence_order": 1 }
]
}
```
**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