- Updated the exercise form to include a tabbed navigation structure, improving user experience with sections for Stammdaten, Anleitung, Einordnung, Varianten, and Medien & Mehr. - Introduced the concept of **Freigabelevel** (visibility level) in the UI, replacing previous terminology for clarity and consistency across components. - Implemented new AI endpoints for exercise suggestions and regeneration, allowing for dynamic content generation without direct database writes. - Removed the legacy `is_primary` flag from exercise skills in the UI, ensuring that intensity levels (`niedrig`, `mittel`, `hoch`) are the primary focus for skill management. - Enhanced the variant management process with improved saving mechanisms and UI updates to reflect changes more intuitively.
25 KiB
Exercises API Specification
Version: 1.6
Datum: 2026-05-20
Status: Teilweise implementiert (Liste mit Filtern + Varianten + Medienlimits + Progressionsgraphen siehe Code)
Autor: Claude Code
Änderungen v1.6: Freigabelevel-UI-Hinweis; exercise_skills ohne is_primary in Requests (Legacy-Feld wird ignoriert/forciert false); Permissions-Bereich an Ist-Code angeglichen; Intensität kanonisch niedrig|mittel|hoch
Ä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.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 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.:
{
"id": 1,
"title": "Maai - Distanzübung",
"variants": [
{ "id": 10, "variant_name": "Basis", "sequence_order": 1 }
]
}
Errors:
401- Unauthorized403- Forbidden
GET /exercises/{id}
Path Parameters:
id(int, required)
Response: 200 OK
{
"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",
"intensity": "hoch",
"required_level": "grundlagen",
"target_level": "aufbau",
"ai_suggested": false,
"is_primary": 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- Unauthorized403- Forbidden (private + not owner)404- Not found
POST /exercises
Request Body:
{
"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,
"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- Unauthorized403- 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 Request401- Unauthorized403- Forbidden (not owner)404- Not found
DELETE /exercises/{id}
Response: 200 OK
{"ok": true}
Errors:
401- Unauthorized403- Forbidden (not owner or admin)404- Not found409- Conflict (used in training units)
Variants
POST /exercises/{id}/variants
Request Body:
{
"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
{
"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 Request401- Unauthorized403- Forbidden (not owner)404- Exercise not found409- 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
{"ok": true}
Errors:
409- Conflict (andere Varianten referenzieren diese als Prerequisite)
PUT /exercises/{id}/variants/reorder
Request Body:
{
"variant_ids": [3, 1, 2]
}
Response: 200 OK
{"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 fileembed_url(string, optional) - YouTube/Instagram/Vimeo URLmedia_type(enum, required) -image | video | document | sketchtitle(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
{
"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- Unauthorized403- Forbidden413- 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:
{
"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
{"ok": true}
Deletes: DB-Eintrag + Datei (wenn file_path gesetzt)
PUT /exercises/{id}/media/reorder
Request Body:
{
"media_ids": [2, 1, 3]
}
Response: 200 OK
{"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:
{
"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
{
"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",
"confidence": 0.92
},
{
"skill_id": 15,
"skill_name": "Reaktionsschnelligkeit",
"skill_category": "Athletik",
"required_level": "einsteiger",
"target_level": "grundlagen",
"intensity": "mittel",
"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:
{
"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
UI-Hinweis: Das Feld visibility heißt in der Oberfläche Freigabelevel (exerciseGovernanceLabels.js).
Lesen (GET /exercises, GET /exercises/{id})
visibility |
Wer darf lesen? |
|---|---|
official |
Plattform-weit |
private |
Ersteller (created_by); Plattform-Admin |
club |
Aktive Mitglieder des Objekt-club_id; Plattform-Admin ohne Mitgliedschaft (Audit-Zugang) |
Implementierung: library_content_visible_to_profile / exercise_visible_to_profile in club_tenancy.py.
Bearbeiten (PUT, Varianten-CRUD, Medien an Übung)
| Bedingung | Wer darf bearbeiten? |
|---|---|
| Ersteller | Immer (eigene Übung) |
| Plattform-Admin | Immer |
visibility=club |
Zusätzlich can_plan_in_club im Objekt-Verein: club_admin, trainer, content_editor, division_lead |
Implementierung: _assert_can_edit_exercise in exercises.py. Varianten haben kein eigenes Owner-Feld — gleiche Prüfung wie Eltern-Übung.
Löschen (DELETE /exercises/{id})
visibility |
Wer darf löschen? |
|---|---|
official |
Nur Plattform-Admin |
club |
Nur club_admin im Objekt-Verein |
private |
Ersteller; oder Vereins-Admin, der mit dem Ersteller einen gemeinsamen Verein teilt |
Implementierung: _assert_can_delete_exercise in exercises.py.
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 (veraltet — siehe Tabellen oben)
Die folgenden Kurzregeln sind durch die Ist-Implementierung ersetzt; nur zur historischen Einordnung:
Bearbeiten (PUT): Nur Ersteller oder Club-Admin→ siehe Bearbeiten-Tabelle (can_plan_in_club)Löschen (DELETE): Nur Ersteller oder Super-Admin→ siehe Löschen-Tabelle
403 Fehler-Beispiel:
{"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 Blocksvisibility(enum, optional) -private | club | officialclub_id(int, optional) - Filter nach Vereinsearch(string, optional) - Suche in namelimit(int, optional, default: 50)offset(int, optional, default: 0)
Response: 200 OK
[
{
"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
{
"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:
{
"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
{"ok": true}
Errors:
403- Forbidden404- Not found409- Conflict (Block in Trainingsplan verwendet)
POST /exercise-blocks/{id}/items
Request Body (konkretes Exercise):
{
"exercise_id": 5,
"variant_id": null,
"sequence_order": 3,
"notes": "Leicht anfangen"
}
Request Body (Platzhalter):
{
"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
{
"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 gefunden409- 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
{"ok": true}
PUT /exercise-blocks/{id}/items/reorder
Request Body:
{
"item_ids": [3, 1, 2, 4]
}
Response: 200 OK
{
"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 clubgoal: 10-5000 charsexecution: 10-10000 charsduration_min/max: 0-480 (8 hours)group_size_min/max: 1-100visibility: enum (private, club, official)status: enum (draft, in_review, approved, archived)
Variant
variant_name: 3-200 charsprogression_level: 1-10prerequisite_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>"; optionaldata-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; Default beim Speichernmittel)is_primary: Legacy — Spalte existiert in DB, wird bei POST/PUT nicht ausgewertet (immerfalsegespeichert); UI liefert/speichert kein Primär-Flag mehr; Scoring ignoriert das Feldtarget_levelsollte >=required_levelsein (Warnung, kein Fehler)
Exercise Block Item
sequence_order: muss unique pro Block seinexercise_id: muss existieren und zugänglich sein (visibility check)is_placeholder = true:exercise_idmuss NULL seinplaceholder_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:
{
"detail": "Human-readable error message"
}
Validation (detailliert):
{
"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/DELETE201 Created- Erfolgreiche POST400 Bad Request- Validierung fehlgeschlagen401 Unauthorized- Kein/ungültiges Token403 Forbidden- Keine Berechtigung404 Not Found- Ressource nicht gefunden409 Conflict- Duplikat, Constraint413 Payload Too Large- Datei zu groß500 Internal Server Error- Server-Fehler
Version: 1.5
Letzte Änderung: 2026-05-08
Status: Living Spec