- Implemented a new SQL migration for wiki import tracking tables. - Created an import router for handling MediaWiki imports of exercises, skills, and methods. - Developed a Semantic MediaWiki API client for direct API interactions. - Added a mapper to convert SMW properties to local database fields. - Introduced background tasks for asynchronous import processing. - Implemented logging and error handling for import operations. - Added endpoints for previewing imports, checking import status, and managing import references.
21 KiB
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 IDvisibility(enum, optional) -private | club | officialstatus(enum, optional) -draft | in_review | approved | archivedskill_id(int, optional) - Skill IDsearch(string, optional) - Volltext (title, summary, execution)limit(int, optional, default: 50, max: 100)offset(int, optional, default: 0)
Response: 200 OK
[
{
"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- 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",
"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- 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,
"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- Unauthorized403- Forbidden
PUT /exercises/{id}
Request Body: Same as POST (all fields optional except id)
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) -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": "detail"
}
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",
"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:
{
"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:
{"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
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_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.0
Letzte Änderung: 2026-04-24
Status: DRAFT - Awaiting Review