All checks were successful
Deploy Development / deploy (push) Successful in 35s
Test Suite / pytest-backend (push) Successful in 25s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 6s
Test Suite / playwright-tests (push) Successful in 23s
Test Suite / pytest-backend (pull_request) Successful in 23s
Test Suite / lint-backend (pull_request) Successful in 0s
Test Suite / build-frontend (pull_request) Successful in 14s
Test Suite / playwright-tests (pull_request) Successful in 22s
- Incremented application version to 0.8.64 and updated changelog with new features. - Implemented inline media support in Rich Text Editor, allowing for drag-and-drop functionality and auto-scrolling. - Enhanced media handling with a modal picker for media insertion, size selection, and improved user experience. - Updated documentation to reflect changes in media handling and inline media specifications. - Adjusted various API specifications to support new inline media features.
957 lines
23 KiB
Markdown
957 lines
23 KiB
Markdown
# Exercises API Specification
|
||
|
||
**Version:** 1.5
|
||
**Datum:** 2026-05-08
|
||
**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.5:** Medien-/Inline-Workflow aktualisiert (Modal-Picker, Drag&Drop UX im Frontend), Klarstellung zu `context` (legacy/optional), Hinweise zu Platzhaltern in Rich-Text-Feldern.
|
||
**Ä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 1–5 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)
|
||
**Inline-Hinweis:** Rich-Text-Felder (`summary`, `goal`, `execution`, `preparation`, `trainer_notes` sowie `execution_changes` bei Varianten) unterstützen Platzhalter `{{exerciseMedia:id}}`, die serverseitig auf kanonisches Span-Markup normalisiert werden. Beim erstmaligen Anlegen ohne vorhandene `exercise_media` wird dies mit 400 abgelehnt.
|
||
|
||
**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, legacy UI-Feld) - `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` bleibt backendseitig kompatibel, wird in aktuellen Bearbeitungsflüssen jedoch nicht mehr aktiv zur UI-Zuordnung verwendet (Inline-Platzhalter priorisiert).
|
||
|
||
**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`
|
||
- Inline-Rich-Text: `{{exerciseMedia:id}}` bzw. `data-shinkan-exercise-media="<id>"`; optional `data-shinkan-exercise-media-size="small|medium|full"` für Layout.
|
||
|
||
### 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.5
|
||
**Letzte Änderung:** 2026-05-08
|
||
**Status:** Living Spec
|