shinkan-jinkendo/.claude/docs/technical/EXERCISES_API_SPEC.md

935 lines
21 KiB
Markdown
Raw Normal View History

# 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: <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