shinkan-jinkendo/.claude/docs/technical/EXERCISES_API_SPEC.md
Lars 7134fd1a25
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 5s
Test Suite / playwright-tests (push) Failing after 1m54s
feat: update version to 0.7.9 and enhance project documentation
- Incremented application version to 0.7.9 and updated database schema version to 20260427030.
- Revised project status documentation to reflect recent milestones and changes, including detailed logs of implemented features and next steps.
- Enhanced API specifications for exercises, including support for exercise variants and improved query parameters.
- Updated frontend routing to streamline exercise variant management within the ExerciseFormPage.
- Implemented role-based media upload limits and refined search/filter specifications for better user experience.
2026-04-28 16:18:25 +02:00

21 KiB
Raw Blame History

Exercises API Specification

Version: 1.3
Datum: 2026-04-27
Status: Teilweise implementiert (Liste mit Filtern + Varianten + Medienlimits siehe Code)
Autor: Claude Code
Ä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)

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

{
  "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

{
  "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:

{
  "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

{"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:

{
  "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 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

{"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 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

{
  "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:

{
  "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 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

[
  {
    "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 - Forbidden
  • 404 - Not found
  • 409 - 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 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

{"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 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:

{
  "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/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