Enhance Progression Graph Management with F15 Features and Evaluation Improvements
All checks were successful
Deploy Development / deploy (push) Successful in 41s
Test Suite / pytest-backend (push) Successful in 43s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 34s
Test Suite / playwright-tests (push) Successful in 1m21s

- Updated `PROJECT_STATUS.md` to reflect the implementation of F15 features, including the unified slot review and handling of `findings_stale`.
- Enhanced `PROGRESSION_GRAPH_SLOT_EDITOR_SPEC.md` with detailed descriptions of new functionalities related to the match dialog and path quality assessments.
- Introduced new functions in `exercise_progression_graphs.py` to validate exercise visibility against progression graph settings, ensuring proper governance.
- Improved frontend components to support new governance parameters (visibility and club_id) in exercise creation workflows.
- Updated documentation in `HANDOVER.md` and `PLANNING_KI_ROADMAP.md` to outline the latest developments and validation results for the F15 features.
- Enhanced utility functions for exercise creation to incorporate governance settings, improving the overall user experience in the path builder and editor.
This commit is contained in:
Lars 2026-06-14 06:44:12 +02:00
parent b629f192ac
commit 4b9374765b
11 changed files with 291 additions and 50 deletions

View File

@ -15,7 +15,7 @@
**Plattform-Rechtstexte (P-01, 0.8.950.8.96):** Admin-Editor mit **Abschnitts- und Vollvorschau** (Markdown); fortlaufende Abschnittsnummerierung in der Anzeige/PDF (Darstellung, nicht DB-persistent). **Plattform-Rechtstexte (P-01, 0.8.950.8.96):** Admin-Editor mit **Abschnitts- und Vollvorschau** (Markdown); fortlaufende Abschnittsnummerierung in der Anzeige/PDF (Darstellung, nicht DB-persistent).
**Parallel weiter relevant:** **Trainingsplan Phasen & Streams** (Migration **063**, Coach + Planung **0.8.1370.8.140**; Handover **`docs/HANDOVER.md`** §3); **Trainingsrahmenprogramm** (036037), **Progressionsgraph** (032034) — siehe **`TRAINING_FRAMEWORK_SPEC.md`**. **Planungs-KI Progressionsgraph** (Roadmap-first, Auto-Optimierung, Katalog-Kontext **0.8.233**): Ist-Doku **`docs/architecture/PLANNING_PROGRESSION_GRAPH_KI.md`**, Handover **`docs/HANDOVER.md`** §2.8. **Parallel weiter relevant:** **Trainingsplan Phasen & Streams** (Migration **063**, Coach + Planung **0.8.1370.8.140**; Handover **`docs/HANDOVER.md`** §3); **Trainingsrahmenprogramm** (036037), **Progressionsgraph** (032034) — siehe **`TRAINING_FRAMEWORK_SPEC.md`**. **Planungs-KI Progressionsgraph** (Roadmap-first, Auto-Optimierung, Katalog-Kontext **0.8.233**, **F15** Match-Dialog + getrennte Pfad-QS lokal): Ist-Doku **`docs/architecture/PLANNING_PROGRESSION_GRAPH_KI.md`**, Handover **`docs/HANDOVER.md`** §2.8.
**Referenz:** [`library/FEATURES_DELIVERED_2026-Q2.md`](library/FEATURES_DELIVERED_2026-Q2.md) Abschnitt 12 · Medien-Norm: [`technical/MEDIA_ASSETS_AND_ARCHIVE_SPEC.md`](technical/MEDIA_ASSETS_AND_ARCHIVE_SPEC.md) (inkl. **Abschnitt 11 Inline-Medien**, umgesetzt) · **Fachlicher Nutzerüberblick:** [`../../docs/FACHLICHE_NUTZERFUNKTIONEN.md`](../../docs/FACHLICHE_NUTZERFUNKTIONEN.md) **Referenz:** [`library/FEATURES_DELIVERED_2026-Q2.md`](library/FEATURES_DELIVERED_2026-Q2.md) Abschnitt 12 · Medien-Norm: [`technical/MEDIA_ASSETS_AND_ARCHIVE_SPEC.md`](technical/MEDIA_ASSETS_AND_ARCHIVE_SPEC.md) (inkl. **Abschnitt 11 Inline-Medien**, umgesetzt) · **Fachlicher Nutzerüberblick:** [`../../docs/FACHLICHE_NUTZERFUNKTIONEN.md`](../../docs/FACHLICHE_NUTZERFUNKTIONEN.md)

View File

@ -1,6 +1,6 @@
# Progressionsgraph — Slot-Editor (Phase B) # Progressionsgraph — Slot-Editor (Phase B + F15)
**Stand:** 2026-06-10 · **Status:** In Umsetzung **Stand:** 2026-05-22 · **Status:** Umgesetzt (F14 + F15 lokal nach 0.8.233)
## Ziel ## Ziel
@ -35,35 +35,52 @@ Leere Slots in der Roadmap sind erlaubt; Kanten nur zwischen aufeinanderfolgende
slots: Slot[], // index = major_step_index slots: Slot[], // index = major_step_index
pathSkillExpectations?, pathSkillExpectations?,
lastFindings?, // path_qa-Snapshot lastFindings?, // path_qa-Snapshot
findingsStale?: boolean, // Bewertung veraltet (↔ Artefakt findings_stale)
dirty: boolean, dirty: boolean,
} }
``` ```
**Hydration:** `planning_roadmap` + Kanten → Slots; `slot_contents[]` für Entwürfe; Primärkette aus `next_exercise`. **Hydration:** `planning_roadmap` + Kanten → Slots; `slot_contents[]` für Entwürfe; Primärkette aus `next_exercise`.
**Speichern:** Batch-Delete bestehender Pfad-/Schwester-Kanten → `edges/sequence` (Primärkette) → einzelne `sibling`-Kanten → `PUT`/`sequence` mit Artefakt inkl. `slot_contents`, optional `last_findings`. **Speichern:** Batch-Delete bestehender Pfad-/Schwester-Kanten → `edges/sequence` (Primärkette) → einzelne `sibling`-Kanten → `PUT`/`sequence` mit Artefakt inkl. `slot_contents`, `last_findings`, **`findings_stale`**.
## Findings-Panel ## Findings-Panel
Nutzt `path_qa` (`overall_ok`, `quality_score`, `issues`, `recommendations`, `gap_fill_offers`, …). Nutzt `path_qa`:
| Feld | Bedeutung |
|------|-----------|
| `quality_score` | Gesamt = **min(`roadmap_qa`, `assignment_qa`)** |
| `roadmap_qa` | Stufen/Roadmap (LLM `topic_coverage`, …) |
| `assignment_qa` | Slot-Befüllung (`empty_slot_count`, …) |
| `overall_ok`, `issues`, `recommendations`, `gap_fill_offers`, … | wie bisher |
**API:** `POST /api/planning/progression-path-suggest` mit `evaluate_only: true` und `evaluate_steps[]` — QA ohne Re-Match. **API:** `POST /api/planning/progression-path-suggest` mit `evaluate_only: true` und `evaluate_steps[]` — QA ohne Re-Match.
Persistenz: `planning_roadmap.last_findings`. **Bewertung veraltet:** Jede Graph-Änderung setzt `findingsStale: true` → Banner im Panel. Nach „Graph bewerten“ → `false`. Persistenz: `planning_roadmap.findings_stale`.
## Match-Flow („Übungen matchen“)
1. **Schritt 1:** `evaluate_only` + volle Pfad-QS (wie „Graph bewerten“)
2. **Schritt 2:** `unified_slot_review: true`**`ProgressionOptimizeCompareModal`**
3. Pro Slot: aktuell vs. beste Bibliothek vs. optional KI-Vorschlag
4. **Vorauswahl:** Bibliothek nur wenn Stufen-Fit ≥ 50 % und besser als Baseline; sonst KI (bei leerem/schwachem Slot)
5. **Übernahme:** nur gewählte Slots speichern — **keine** automatische Nach-Bewertung
## Artefakt-Erweiterung (`GraphPlanningRoadmapArtifact`) ## Artefakt-Erweiterung (`GraphPlanningRoadmapArtifact`)
Zusätzlich optional: Optional:
- `slot_contents[]``{ major_step_index, primary, siblings[] }` - `slot_contents[]``{ major_step_index, primary, siblings[] }`
- `last_findings` — letzter `path_qa`-Snapshot - `last_findings` — letzter `path_qa`-Snapshot
- **`findings_stale`** — bool, Bewertung bezieht sich nicht mehr auf aktuellen Graph-Stand
## UI (konsolidiert) ## UI (konsolidiert)
- **Eine Oberfläche:** `ExerciseProgressionGraphPanel` embeddet `ProgressionGraphEditor` (Slots + Findings) - **Eine Oberfläche:** `ExerciseProgressionGraphPanel` embeddet `ProgressionGraphEditor` (Slots + Findings)
- Kein separater Slot-Editor, kein 4-Schritt-KI-Wizard, kein `ProgressionChainEditor` im Panel - Kein separater Slot-Editor, kein 4-Schritt-KI-Wizard, kein `ProgressionChainEditor` im Panel
- Route `/progression-graphs/:id` → Redirect nach `/exercises` (Deep-Link wählt Graph) - Route `/progression-graphs/:id` → Redirect nach `/exercises` (Deep-Link wählt Graph)
- **Phase C:** Übersicht mit Kacheln (Name, Start, Ziel) - **Slot-Keys:** stabil `slot-{index}` (nicht Lernziel-Text) — sonst Fokusverlust beim Tippen
## Ersetzt (Legacy, nicht mehr im Panel) ## Ersetzt (Legacy, nicht mehr im Panel)
@ -71,11 +88,14 @@ Zusätzlich optional:
## Implementierungsreihenfolge ## Implementierungsreihenfolge
| ID | Inhalt | | ID | Inhalt | Status |
|----|--------| |----|--------|--------|
| B.0 | Draft + Laden/Speichern Slots ↔ Kanten | | B.0 | Draft + Laden/Speichern Slots ↔ Kanten | ✅ |
| B.1 | Slot-Karten, Bibliothek + Entwurf | | B.1 | Slot-Karten, Bibliothek + Entwurf | ✅ |
| B.2 | Findings-Panel + `evaluate_only` | | B.2 | Findings-Panel + `evaluate_only` | ✅ |
| B.3 | Entwürfe im Artefakt + „Übung anlegen“ | | B.3 | Entwürfe im Artefakt + „Übung anlegen“ | ✅ |
| B.4 | Route + Panel vereinfachen | | B.4 | Route + Panel vereinfachen | ✅ |
| B.5 | `last_findings` + Phase-C-Vorbereitung | | B.5 | `last_findings` + Phase-C-Vorbereitung | ✅ |
| F15 | Unified Slot-Review, getrennte QS, `findings_stale` | ✅ |
**Ist-Doku:** `docs/architecture/PLANNING_PROGRESSION_GRAPH_KI.md` §8.1 · `docs/HANDOVER.md` §2.8 F15

View File

@ -4,7 +4,7 @@ Optional Übungsvarianten als Knoten-Endpunkte; Sequenz-Bulk-Anlage.
AuthZ analog training_plan_templates: Bearbeiten/Löschen wie Übungen; Governance-Übergänge zentral. AuthZ analog training_plan_templates: Bearbeiten/Löschen wie Übungen; Governance-Übergänge zentral.
""" """
import json import json
from typing import Any, Dict, List, Optional from typing import Any, Dict, List, Mapping, Optional
from fastapi import APIRouter, Depends, HTTPException, Query from fastapi import APIRouter, Depends, HTTPException, Query
from pydantic import BaseModel, Field, model_validator from pydantic import BaseModel, Field, model_validator
@ -19,6 +19,7 @@ from club_tenancy import (
assert_library_content_editable, assert_library_content_editable,
assert_library_content_governance_transition, assert_library_content_governance_transition,
assert_valid_governance_visibility, assert_valid_governance_visibility,
is_platform_admin,
library_content_visible_to_profile, library_content_visible_to_profile,
) )
@ -176,6 +177,87 @@ def _assert_variant_for_exercise(cur, exercise_id: int, variant_id: Optional[int
raise HTTPException(status_code=400, detail="Variante gehört nicht zur gewählten Übung") raise HTTPException(status_code=400, detail="Variante gehört nicht zur gewählten Übung")
def _exercise_allowed_in_progression_graph(
exercise_row: Mapping[str, Any],
*,
graph_visibility: str,
graph_club_id: Optional[int],
profile_id: int,
role: str,
) -> bool:
"""Prüft, ob eine Übung zur Sichtbarkeit des Progressionsgraphen passt."""
ex_vis = (exercise_row.get("visibility") or "private").strip().lower()
gvis = (graph_visibility or "private").strip().lower()
if gvis == "private":
if ex_vis == "official":
return True
if ex_vis == "club":
return True
if ex_vis == "private":
if is_platform_admin(role):
return True
try:
return int(exercise_row.get("created_by") or 0) == int(profile_id)
except (TypeError, ValueError):
return False
return False
if gvis == "club":
if ex_vis == "official":
return True
if ex_vis != "club":
return False
ex_club = exercise_row.get("club_id")
if ex_club is None:
return False
if graph_club_id is None:
return True
return int(ex_club) == int(graph_club_id)
return ex_vis == "official"
def _assert_exercises_allowed_in_graph(
cur,
graph_id: int,
profile_id: int,
role: str,
*exercise_ids: int,
) -> None:
"""400 wenn eine Übung nicht zur Graph-Sichtbarkeit passt."""
row = _graph_row(cur, graph_id)
gvis = (row.get("visibility") or "private").strip().lower()
gclub_raw = row.get("club_id")
gclub = int(gclub_raw) if gclub_raw is not None else None
unique = list(dict.fromkeys(exercise_ids))
if not unique:
return
ph = ",".join(["%s"] * len(unique))
cur.execute(
f"SELECT id, title, visibility, club_id, created_by FROM exercises WHERE id IN ({ph})",
tuple(unique),
)
by_id = {int(r2d(r)["id"]): r2d(r) for r in cur.fetchall()}
for eid in unique:
ex = by_id.get(int(eid))
if not ex:
continue
if _exercise_allowed_in_progression_graph(
ex,
graph_visibility=gvis,
graph_club_id=gclub,
profile_id=profile_id,
role=role,
):
continue
title = (ex.get("title") or "").strip() or f"#{eid}"
raise HTTPException(
status_code=400,
detail=(
f"Übung „{title}“ (Sichtbarkeit: {ex.get('visibility') or 'private'}) "
f"passt nicht zum Progressionsgraphen ({gvis})."
),
)
def _insert_edge_row( def _insert_edge_row(
cur, cur,
graph_id: int, graph_id: int,
@ -359,8 +441,10 @@ def list_visibility_promotion_candidates(
if not library_content_visible_to_profile( if not library_content_visible_to_profile(
cur, cur,
profile_id, profile_id,
(exd.get("visibility") or "private").strip().lower(),
exd.get("club_id"),
exd.get("created_by"),
role, role,
exd,
): ):
continue continue
exercises.append( exercises.append(
@ -565,6 +649,9 @@ def create_progression_edge(
cur = get_cursor(conn) cur = get_cursor(conn)
_require_graph_write(cur, graph_id, profile_id, role) _require_graph_write(cur, graph_id, profile_id, role)
_assert_exercises_exist(cur, body.from_exercise_id, body.to_exercise_id) _assert_exercises_exist(cur, body.from_exercise_id, body.to_exercise_id)
_assert_exercises_allowed_in_graph(
cur, graph_id, profile_id, role, body.from_exercise_id, body.to_exercise_id
)
fv = body.from_exercise_variant_id fv = body.from_exercise_variant_id
tv = body.to_exercise_variant_id tv = body.to_exercise_variant_id
_assert_variant_for_exercise(cur, body.from_exercise_id, fv) _assert_variant_for_exercise(cur, body.from_exercise_id, fv)
@ -613,6 +700,7 @@ def create_progression_sequence(
ex_ids = [s.exercise_id for s in steps] ex_ids = [s.exercise_id for s in steps]
_assert_exercises_exist(cur, *ex_ids) _assert_exercises_exist(cur, *ex_ids)
_assert_exercises_allowed_in_graph(cur, graph_id, profile_id, role, *ex_ids)
try: try:
for i in range(n_seg): for i in range(n_seg):

View File

@ -0,0 +1,59 @@
"""Sichtbarkeit: Progressionsgraph ↔ Übungen (Promotion, Kanten, Match)."""
from routers.exercise_progression_graphs import _exercise_allowed_in_progression_graph
def test_club_graph_rejects_private_exercise():
assert not _exercise_allowed_in_progression_graph(
{"visibility": "private", "club_id": None, "created_by": 1},
graph_visibility="club",
graph_club_id=5,
profile_id=1,
role="trainer",
)
def test_club_graph_accepts_matching_club_exercise():
assert _exercise_allowed_in_progression_graph(
{"visibility": "club", "club_id": 5, "created_by": 2},
graph_visibility="club",
graph_club_id=5,
profile_id=1,
role="trainer",
)
def test_club_graph_accepts_official_exercise():
assert _exercise_allowed_in_progression_graph(
{"visibility": "official", "club_id": None, "created_by": 99},
graph_visibility="club",
graph_club_id=5,
profile_id=1,
role="trainer",
)
def test_private_graph_accepts_own_private_exercise():
assert _exercise_allowed_in_progression_graph(
{"visibility": "private", "club_id": None, "created_by": 7},
graph_visibility="private",
graph_club_id=None,
profile_id=7,
role="trainer",
)
def test_official_graph_requires_official_exercise():
assert not _exercise_allowed_in_progression_graph(
{"visibility": "club", "club_id": 5, "created_by": 2},
graph_visibility="official",
graph_club_id=None,
profile_id=1,
role="trainer",
)
assert _exercise_allowed_in_progression_graph(
{"visibility": "official", "club_id": None, "created_by": 2},
graph_visibility="official",
graph_club_id=None,
profile_id=1,
role="trainer",
)

View File

@ -1,7 +1,7 @@
# Shinkan Jinkendo Entwicklungsstand & Handover # Shinkan Jinkendo Entwicklungsstand & Handover
**Stand:** 2026-05-22 **Stand:** 2026-05-22 (F15 Graph-Match & getrennte Pfad-QS, lokal nach **0.8.233**)
**App-Version / DB-Schema:** App **`0.8.233`** (Planungs-KI F11F14, Katalog-Kontext); DB siehe **`backend/version.py`** (`DB_SCHEMA_VERSION`, Migration **088**). **App-Version / DB-Schema:** App **`0.8.233`** (Planungs-KI F11F14, Katalog-Kontext); **F15** siehe §2.8 — DB unverändert (`DB_SCHEMA_VERSION`, Migration **088**).
Diese Datei ist die **Einstiegs-Doku für neue Chat-Sessions**: Anforderungen im Detail stehen in `.claude/docs/` (siehe unten); hier der **implementierte Stand**, **Medien-Meilenstein** und **sinnvolle nächste Schritte**. Diese Datei ist die **Einstiegs-Doku für neue Chat-Sessions**: Anforderungen im Detail stehen in `.claude/docs/` (siehe unten); hier der **implementierte Stand**, **Medien-Meilenstein** und **sinnvolle nächste Schritte**.
@ -114,11 +114,25 @@ Das Schema ist gegenüber dem Code zurück: Migration **`022_skills_schema_compl
| **F12** | Post-Match-Gate, LLM-QA nach Rematch, Gap-Timing, `roadmap_unfilled`-Sync | ✅ **0.8.2310.8.232** | | **F12** | Post-Match-Gate, LLM-QA nach Rematch, Gap-Timing, `roadmap_unfilled`-Sync | ✅ **0.8.2310.8.232** |
| **F13** | **`planning_catalog_context`** (Fokus/Stil/TT/ZG) im Match + Graph-Artefakt | ✅ **0.8.233** | | **F13** | **`planning_catalog_context`** (Fokus/Stil/TT/ZG) im Match + Graph-Artefakt | ✅ **0.8.233** |
| **F14** | **`ProgressionGraphEditor`** — Slot-UI + Planungskontext-Dropdowns | ✅ **0.8.233** | | **F14** | **`ProgressionGraphEditor`** — Slot-UI + Planungskontext-Dropdowns | ✅ **0.8.233** |
| **F15** | Unified Slot-Review (Match-Dialog), getrennte Pfad-QS, `findings_stale` | ✅ lokal (nach 0.8.233) |
| **H1** | Katalog-Prompt-Snippets (modulare LLM-Anweisungen) | 🔲 Spec **`docs/architecture/PLANNING_CATALOG_PROMPT_SNIPPETS.md`** | | **H1** | Katalog-Prompt-Snippets (modulare LLM-Anweisungen) | 🔲 Spec **`docs/architecture/PLANNING_CATALOG_PROMPT_SNIPPETS.md`** |
**Architektur (verbindlich):** Drei Schichten — (1) **Katalog-Dimensionen** (DB, jetzt im Match verdrahtet; **H1:** zusätzlich Prompt-Snippets), (2) **Technik-Disambiguierung** (Code, nur bei `topic_type=technique`), (3) **Didaktik** (Roadmap + LLM-QS, nicht im Vokabular). Progressionsgraph = **Roadmap-first**, **keine Gruppenanalyse**. Bestehender Graph = **leichter Nachfolger-Bias** ab Schritt 2. Trainingsplanung = **eigene Pipeline** (Phase G) — Wiederverwendung der Bausteine, siehe Ist-Doku §16. **Architektur (verbindlich):** Drei Schichten — (1) **Katalog-Dimensionen** (DB, jetzt im Match verdrahtet; **H1:** zusätzlich Prompt-Snippets), (2) **Technik-Disambiguierung** (Code, nur bei `topic_type=technique`), (3) **Didaktik** (Roadmap + LLM-QS, nicht im Vokabular). Progressionsgraph = **Roadmap-first**, **keine Gruppenanalyse**. Bestehender Graph = **leichter Nachfolger-Bias** ab Schritt 2. Trainingsplanung = **eigene Pipeline** (Phase G) — Wiederverwendung der Bausteine, siehe Ist-Doku §16.
**Validierung (Mae Geri, Härtetest):** Pfad-QS vor Optimierung ~65 % → nach Trainer-Roadmap + KI-Gap-Fill **~88 % OK**. Workbench ist **universell** gedacht; Mae Geri war Referenzfall, kein Sonder-Patch. **Validierung (Mae Geri, Härtetest):** Roadmap-QS nach Trainer-Roadmap oft **~8588 %** — gilt **Stufenlogik**, nicht leere Slots. **Gesamt-Pfad-QS** = Minimum aus **`roadmap_qa`** + **`assignment_qa`** (leere Slots → Besetzung ~815 %). Workbench universell; Mae Geri Referenzfall.
#### F15 — Match-Dialog, Bewertung, Pfad-QS (Stand 2026-05-22)
| Thema | Ist |
|--------|-----|
| **„Übungen matchen“** | Schritt 1: `evaluate_only` (wie „Graph bewerten“) · Schritt 2: `unified_slot_review: true` → Dialog **pro Slot** (Bewertung, Bibliotheks-Alternative, optional KI) |
| **Vorauswahl Dialog** | Bibliothek nur bei Stufen-Fit **≥ 50 %** und besser als aktuell; bei leerem Slot + schwacher Bibliothek → **KI-Vorschlag** vorausgewählt |
| **Übernahme** | Nur gewählte Slots speichern — **keine** automatische teure Nach-Bewertung |
| **Bewertung veraltet** | Nach Graph-Änderungen Hinweis im Findings-Panel; persistiert als **`findings_stale`** im `planning_roadmap`-Artefakt (mit Speichern) |
| **Getrennte QS** | `path_qa.roadmap_qa` (Stufen/Roadmap/LLM) + `path_qa.assignment_qa` (Slot-Befüllung); **`quality_score`** = Minimum beider |
| **UX-Fix** | Slot-Karten: stabiler React-Key (`slot-{index}`) — Lernziel editierbar ohne Fokusverlust |
**Code:** `ProgressionOptimizeCompareModal.jsx`, `planning_exercise_path_builder.py` (`_build_unified_slot_review_entry`, `_slot_auto_select_*`), `planning_exercise_path_qa.py` (`build_*_qa_snapshot`), `progression_graph_planning_artifact.py` (`findings_stale`), `progressionGraphDraft.js`
**Backend-Kern:** `planning_progression_roadmap.py`, `planning_exercise_path_builder.py`, `planning_catalog_context.py`, `planning_path_rematch.py`, `planning_path_refine_stage.py`, `planning_path_qa_pipeline.py`, `planning_skill_expectations.py`, `planning_exercise_form_context.py`, `planning_exercise_path_ai_fill.py`, `progression_graph_planning_artifact.py` **Backend-Kern:** `planning_progression_roadmap.py`, `planning_exercise_path_builder.py`, `planning_catalog_context.py`, `planning_path_rematch.py`, `planning_path_refine_stage.py`, `planning_path_qa_pipeline.py`, `planning_skill_expectations.py`, `planning_exercise_form_context.py`, `planning_exercise_path_ai_fill.py`, `progression_graph_planning_artifact.py`
@ -129,12 +143,12 @@ Das Schema ist gegenüber dem Code zurück: Migration **`022_skills_schema_compl
**Offen (priorisiert):** **Offen (priorisiert):**
1. Dev-Regression: Gewaltschutz / Breitensport / Kinder (nicht nur Mae Geri) 1. Dev-Regression: Gewaltschutz / Breitensport / Kinder (nicht nur Mae Geri)
2. **PathBuilder-Parität** — gleiche Katalog-Dropdowns wie GraphEditor 2. **PathBuilder-Parität** — gleiche Katalog-Dropdowns wie GraphEditor
3. QS-UI — positive LLM-Hinweise als Highlights 3. UI-Wizard (4 Schritte: Ziel → Roadmap → Match → Lücken)
4. UI-Wizard (4 Schritte: Ziel → Roadmap → Match → Lücken) 4. Graph-Erweiterungsmodus (Start ab Knoten)
5. Graph-Erweiterungsmodus (Start ab Knoten) 5. Phase D — Auto KI-Gap-Fill bei persistent leeren Slots
6. Phase D — Auto KI-Gap-Fill bei persistent leeren Slots 6. **Trainingsplanung Phase G** — Gruppenkontext-Pack, Scopes `training_section` / `framework_slot` (Ist-Doku §16)
7. **Trainingsplanung Phase G** — Gruppenkontext-Pack, Scopes `training_section` / `framework_slot` (Ist-Doku §16) 7. Technik-Katalog konfigurierbar (Backlog)
8. Technik-Katalog konfigurierbar (Backlog) 8. **H1** — Katalog-Prompt-Snippets (modulare LLM-Anweisungen)
#### Übungs-KI Formular / Schnellanlage (Stand **0.8.171**) #### Übungs-KI Formular / Schnellanlage (Stand **0.8.171**)
@ -271,8 +285,7 @@ Das Schema ist gegenüber dem Code zurück: Migration **`022_skills_schema_compl
1. **H1 Katalog-Prompt-Snippets:** modulare LLM-Anweisungen — **`docs/architecture/PLANNING_CATALOG_PROMPT_SNIPPETS.md`** (Priorität: Primärfokus → Trainingsstil → Zielgruppe → Stilrichtung). 1. **H1 Katalog-Prompt-Snippets:** modulare LLM-Anweisungen — **`docs/architecture/PLANNING_CATALOG_PROMPT_SNIPPETS.md`** (Priorität: Primärfokus → Trainingsstil → Zielgruppe → Stilrichtung).
2. **Dev-Regression:** Katalog-Match für Gewaltschutz, Breitensport, Kinder — nicht nur Mae-Geri-Härtetest. 2. **Dev-Regression:** Katalog-Match für Gewaltschutz, Breitensport, Kinder — nicht nur Mae-Geri-Härtetest.
2. **PathBuilder-Parität:** `planning_catalog_context`-Dropdowns auch in `ExerciseProgressionPathBuilder`. 3. **PathBuilder-Parität:** `planning_catalog_context`-Dropdowns auch in `ExerciseProgressionPathBuilder`.
3. **QS-UI:** positive LLM-Empfehlungen als Highlights statt nur Optimierungspotenziale.
4. **UI-Wizard:** 4 Schritte (Ziel & Katalog → Roadmap → Match → Lücken); Backend-Pipeline unverändert. 4. **UI-Wizard:** 4 Schritte (Ziel & Katalog → Roadmap → Match → Lücken); Backend-Pipeline unverändert.
5. **Phase D:** automatisches KI-Gap-Fill bei persistent `roadmap_unfilled`. 5. **Phase D:** automatisches KI-Gap-Fill bei persistent `roadmap_unfilled`.
6. **Trainingsplanung G0G4:** Katalog in Einheits-Editor, Scopes `training_section`/`framework_slot`, Abschnitts-QS, Gruppenkontext-Pack — Details **`PLANNING_PROGRESSION_GRAPH_KI.md`** §16, **`PLANNING_KI_ROADMAP.md`**. 6. **Trainingsplanung G0G4:** Katalog in Einheits-Editor, Scopes `training_section`/`framework_slot`, Abschnitts-QS, Gruppenkontext-Pack — Details **`PLANNING_PROGRESSION_GRAPH_KI.md`** §16, **`PLANNING_KI_ROADMAP.md`**.

View File

@ -89,14 +89,24 @@ Details und Module: **`PLANNING_PROGRESSION_GRAPH_KI.md`**.
- [x] Vier Planungskontext-Dropdowns im Editor - [x] Vier Planungskontext-Dropdowns im Editor
- [x] `progressionGraphDraft.js` — Artefakt + API-Payload - [x] `progressionGraphDraft.js` — Artefakt + API-Payload
### F15 — Match-Dialog & getrennte Pfad-QS (2026-05-22, lokal)
- [x] **`unified_slot_review`** — Dialog pro Slot (Bibliothek + KI, Stufen-Fit-Vergleich)
- [x] Vorauswahl: Bibliothek nur bei Stufen-Fit ≥ 50 %; sonst KI bei leerem/schwachem Slot
- [x] Übernahme ohne teure Auto-Nach-Bewertung; manuell „Graph bewerten“
- [x] **`path_qa.roadmap_qa`** + **`path_qa.assignment_qa`**; Gesamt = Minimum
- [x] **`findings_stale`** im Graph-Artefakt — Hinweis „Bewertung veraltet“ (persistiert)
- [x] Slot-Key-Fix — Lernziel editierbar ohne Fokusverlust
### Validierung (Referenz Mae Geri, 2026-05) ### Validierung (Referenz Mae Geri, 2026-05)
| Phase | Pfad-QS | Ergebnis | | Phase | Roadmap-QS | Besetzung | Gesamt | Ergebnis |
|-------|---------|----------| |-------|------------|-----------|--------|----------|
| Vor Roadmap/KI | ~65 % | Lücken, falsche Reihenfolge, Off-Topic | | Vor Roadmap/KI | — | — | ~65 % | Lücken, Off-Topic |
| Nach Trainer-Roadmap + KI-Gap-Fill | **~88 % OK** | Vollständige Abdeckung; positive LLM-Hinweise | | Roadmap ok, Slots leer | ~88 % | ~815 % | **~815 %** | Besetzung fehlt |
| Nach Match + Fill | ~88 % | hoch | **~85 %+** | Vollständige Abdeckung |
**Fazit:** Workbench + Katalog + Roadmap sind universell; Technik-Hardcoding allein reicht für Didaktik nicht. **Fazit:** Roadmap-QS und Besetzungs-QS getrennt betrachten; Workbench + Katalog + Roadmap universell.
--- ---

View File

@ -157,6 +157,10 @@ flowchart TB
| `auto_refine_stage_spec` | bool | Stufen-Spec bei `stage_mismatch` schärfen (Default **true**) | | `auto_refine_stage_spec` | bool | Stufen-Spec bei `stage_mismatch` schärfen (Default **true**) |
| `max_rematch_rounds` | int | Rematch-Runden 04 (Default **3**) | | `max_rematch_rounds` | int | Rematch-Runden 04 (Default **3**) |
| `include_path_qa`, `include_llm_path_qa`, `include_ai_gap_fill` | bool | QS, LLM-Ganzpfad, Lücken-Angebote | | `include_path_qa`, `include_llm_path_qa`, `include_ai_gap_fill` | bool | QS, LLM-Ganzpfad, Lücken-Angebote |
| `evaluate_only` | bool | Nur QS auf `evaluate_steps[]` — kein Match |
| `unified_slot_review` | bool | Pro-Slot-Review (Bibliothek + optional KI) für Match-Dialog; erfordert `baseline_evaluate_steps` + Roadmap |
| `baseline_evaluate_steps` | array? | Slot-Stand für Schritt 1 / Review-Baseline |
| `baseline_path_qa_snapshot` | object? | `path_qa` aus evaluate_only (Schritt 1 des Match-Flows) |
### 4.2 Wichtige Response-Felder ### 4.2 Wichtige Response-Felder
@ -166,7 +170,9 @@ flowchart TB
| `steps[]` | Gematchte Übungen; pro Schritt u. a. `roadmap_*`, `skill_expectations` | | `steps[]` | Gematchte Übungen; pro Schritt u. a. `roadmap_*`, `skill_expectations` |
| `path_skill_expectations` | Pfadweite Skill-Erwartungen | | `path_skill_expectations` | Pfadweite Skill-Erwartungen |
| `gap_fill_offers[]` | Lücken mit `context_preview`, `goal_for_ai` | | `gap_fill_offers[]` | Lücken mit `context_preview`, `goal_for_ai` |
| `path_qa` | QS inkl. `qa_tiers`, `optimization_hints`, `rematch_log`, `refine_log` | | `path_qa` | QS inkl. `qa_tiers`, `optimization_hints`, `rematch_log`, `refine_log`; **F15:** auch `roadmap_qa`, `assignment_qa` (siehe §8.1) |
| `slot_reviews[]` | Bei `unified_slot_review`: je Slot `library_alternative`, `ai_alternative`, `auto_select`-Flags |
| `findings_stale` | Im Graph-Artefakt (nicht API-Response): Bewertung veraltet seit letztem „Graph bewerten“ |
| `target_profile_summary` | Erwartungsprofil inkl. Katalog-Dimensionen (nach Match) | | `target_profile_summary` | Erwartungsprofil inkl. Katalog-Dimensionen (nach Match) |
| `match_summary` | `library_matches`, `gap_fill_offer_count`, `roadmap_unfilled_count` | | `match_summary` | `library_matches`, `gap_fill_offer_count`, `roadmap_unfilled_count` |
@ -221,12 +227,13 @@ Tests: `test_planning_roadmap_stage_match.py`, `test_planning_path_rematch.py`,
### Referenz-Validierung (Mae Geri, 2026-05) ### Referenz-Validierung (Mae Geri, 2026-05)
| Phase | Pfad-QS | Ergebnis | | Phase | Roadmap-QS | Besetzung | Gesamt (min) | Ergebnis |
|-------|---------|----------| |-------|------------|-----------|--------------|----------|
| Vor Roadmap/KI-Anpassung | ~65 % | Strukturelle Lücken (Grundlagen, Reihenfolge, Zielgenauigkeit) | | Vor Roadmap/KI-Anpassung | — | — | ~65 % | Strukturelle Lücken |
| Nach Trainer-Roadmap + KI-Angebote in leeren Slots | **~88 % OK** | Vollständige Curriculum-Abdeckung; positive LLM-Empfehlungen | | Nach Trainer-Roadmap, **Slots leer** | ~8588 % | ~815 % | **~815 %** | Roadmap ok, Besetzung fehlt |
| Nach Match + befüllte Slots | ~8588 % | hoch | **~85 %+** | Vollständige Curriculum-Abdeckung |
**Lesson:** Workbench + Katalog-Kontext + Roadmap sind der Hebel; Technik-Hardcoding allein reicht nicht für Didaktik. **Lesson:** **`roadmap_qa`** und **`assignment_qa`** getrennt interpretieren; Gesamt-QS allein bei leerer Roadmap irreführend (historisch nur LLM-Roadmap-Score).
--- ---
@ -330,6 +337,21 @@ API: `path_qa.qa_tiers`, `path_qa.optimization_hints` — **kein** anfrage-spezi
Phase G: gleiche Tiers für Abschnitts-QS nach Einheits-Match. Phase G: gleiche Tiers für Abschnitts-QS nach Einheits-Match.
### 8.1 Getrennte Pfad-QS — Roadmap vs. Übungsbesetzung (F15)
`build_path_qa_summary()` in `planning_exercise_path_qa.py` liefert drei Ebenen:
| Feld | Inhalt | Score-Logik |
|------|--------|-------------|
| **`roadmap_qa`** | Stufenlogik, LLM `topic_coverage`, Roadmap-Hinweise | LLM-`quality_score` oder heuristisch (Lücken, Hints) |
| **`assignment_qa`** | Leere Slots, Off-Topic auf belegten Slots, Fill-Statistik | Stark abwertend bei leeren Slots (~815 % bei 100 % leer) |
| **`quality_score`** (gesamt) | Anzeige „Pfad-QS gesamt“ | **`min(roadmap_qa, assignment_qa)`** |
| **`overall_ok`** | Gesamt-OK | Beide Dimensionen müssen OK sein |
UI: **`ProgressionFindingsPanel`** — zwei Unterblöcke; Match-Dialog zeigt Roadmap- vs. Besetzungs-Prozent. Nach Graph-Änderung: **`findings_stale: true`** im Artefakt → Hinweis „Bewertung veraltet“ (bis erneut „Graph bewerten“ + Speichern).
Tests: `test_planning_path_qa_split.py`, `test_planning_deterministic_quality_score.py`
## 9. Fähigkeiten-Scoring-Anbindung ## 9. Fähigkeiten-Scoring-Anbindung
Modul: `planning_skill_expectations.py` Modul: `planning_skill_expectations.py`
@ -379,6 +401,7 @@ Kontext-Helfer: `frontend/src/utils/planningContextForExerciseAi.js`
| **F12** | Post-Match-Gate, LLM-QA nach Rematch, Gap-Timing, `roadmap_unfilled`-Sync | ✅ | 0.8.2310.8.232 | | **F12** | Post-Match-Gate, LLM-QA nach Rematch, Gap-Timing, `roadmap_unfilled`-Sync | ✅ | 0.8.2310.8.232 |
| **F13** | **Katalog-Kontext** (`planning_catalog_context`) im Match + Graph-Artefakt | ✅ | **0.8.233** | | **F13** | **Katalog-Kontext** (`planning_catalog_context`) im Match + Graph-Artefakt | ✅ | **0.8.233** |
| **F14** | `ProgressionGraphEditor` Slot-UI + Planungskontext-Dropdowns | ✅ | 0.8.233 | | **F14** | `ProgressionGraphEditor` Slot-UI + Planungskontext-Dropdowns | ✅ | 0.8.233 |
| **F15** | Unified Slot-Review, getrennte Pfad-QS, `findings_stale`, Match-Vorauswahl | ✅ | lokal (2026-05-22) |
| **H1** | **Katalog-Prompt-Snippets** — modulare LLM-Anweisungen pro Dimension | 🔲 | Spec **`PLANNING_CATALOG_PROMPT_SNIPPETS.md`** | | **H1** | **Katalog-Prompt-Snippets** — modulare LLM-Anweisungen pro Dimension | 🔲 | Spec **`PLANNING_CATALOG_PROMPT_SNIPPETS.md`** |
| **G** | Trainingsplanung: eigene Pipeline + Wiederverwendung Bausteine (§16) | 🔲 | — | | **G** | Trainingsplanung: eigene Pipeline + Wiederverwendung Bausteine (§16) | 🔲 | — |
| **UX** | Wizard/Stepper; PathBuilder-Parität Katalog | 🔲 | — | | **UX** | Wizard/Stepper; PathBuilder-Parität Katalog | 🔲 | — |
@ -391,8 +414,7 @@ Kontext-Helfer: `frontend/src/utils/planningContextForExerciseAi.js`
1. **H1 Katalog-Prompt-Snippets** — modulare LLM-Anweisungen (Priorität: Primärfokus → Trainingsstil → Zielgruppe → Stilrichtung) — **`PLANNING_CATALOG_PROMPT_SNIPPETS.md`** 1. **H1 Katalog-Prompt-Snippets** — modulare LLM-Anweisungen (Priorität: Primärfokus → Trainingsstil → Zielgruppe → Stilrichtung) — **`PLANNING_CATALOG_PROMPT_SNIPPETS.md`**
2. **Dev-Regression:** Gewaltschutz + Breitensport + Kinder (ohne Mae Geri) — Katalog-Match verifizieren 2. **Dev-Regression:** Gewaltschutz + Breitensport + Kinder (ohne Mae Geri) — Katalog-Match verifizieren
2. **PathBuilder-Parität** — gleiche `planning_catalog_context`-Dropdowns in `ExerciseProgressionPathBuilder` 3. **PathBuilder-Parität** — gleiche `planning_catalog_context`-Dropdowns in `ExerciseProgressionPathBuilder`
3. **QS-UI** — positive LLM-Hinweise als „Highlights“, nicht als „Optimierungspotenziale“
4. **UI-Wizard** — 4 Schritte (Ziel → Roadmap → Match → Lücken); Backend unverändert 4. **UI-Wizard** — 4 Schritte (Ziel → Roadmap → Match → Lücken); Backend unverändert
5. **Graph-Erweiterungsmodus** — Start ab gewähltem Knoten / letzter Sequenz 5. **Graph-Erweiterungsmodus** — Start ab gewähltem Knoten / letzter Sequenz
6. **Phase D** — automatisches KI-Gap-Fill bei persistent leeren Slots 6. **Phase D** — automatisches KI-Gap-Fill bei persistent leeren Slots

View File

@ -285,10 +285,12 @@ function ExerciseProgressionGraphPanel(
} }
} }
const promoteClubId = nextVis === 'club' ? resolvePromoteClubId() : null
await api.updateExerciseProgressionGraph(selectedGraphId, { await api.updateExerciseProgressionGraph(selectedGraphId, {
name, name,
description: metaDescription.trim() || null, description: metaDescription.trim() || null,
visibility: metaVisibility, visibility: metaVisibility,
...(promoteClubId != null ? { club_id: promoteClubId } : {}),
}) })
await refreshGraphs() await refreshGraphs()
alert('Graph-Metadaten gespeichert.') alert('Graph-Metadaten gespeichert.')

View File

@ -589,6 +589,7 @@ export default function ExerciseProgressionPathBuilder({
const [gapPrepFocusAreaId, setGapPrepFocusAreaId] = useState('') const [gapPrepFocusAreaId, setGapPrepFocusAreaId] = useState('')
const [gapPrepError, setGapPrepError] = useState('') const [gapPrepError, setGapPrepError] = useState('')
const [loadedPlanningHint, setLoadedPlanningHint] = useState(false) const [loadedPlanningHint, setLoadedPlanningHint] = useState(false)
const [graphGovernance, setGraphGovernance] = useState({ visibility: 'private', clubId: null })
const [wizardStep, setWizardStep] = useState(1) const [wizardStep, setWizardStep] = useState(1)
const [pathInsertNotice, setPathInsertNotice] = useState('') const [pathInsertNotice, setPathInsertNotice] = useState('')
@ -670,6 +671,10 @@ export default function ExerciseProgressionPathBuilder({
.getExerciseProgressionGraph(Number(graphId)) .getExerciseProgressionGraph(Number(graphId))
.then((g) => { .then((g) => {
if (cancelled) return if (cancelled) return
setGraphGovernance({
visibility: g?.visibility || 'private',
clubId: g?.club_id ?? null,
})
const art = g?.planning_roadmap const art = g?.planning_roadmap
if (!art) return if (!art) return
if (art.goal_query) setGoalQuery(String(art.goal_query)) if (art.goal_query) setGoalQuery(String(art.goal_query))
@ -1056,7 +1061,7 @@ export default function ExerciseProgressionPathBuilder({
setQuickSaving(true) setQuickSaving(true)
setQuickAiError('') setQuickAiError('')
try { try {
const payload = buildQuickCreateExercisePayloadFromDraft(quickCreateDraft) const payload = buildQuickCreateExercisePayloadFromDraft(quickCreateDraft, graphGovernance)
const created = await api.createExercise(payload) const created = await api.createExercise(payload)
if (!created?.id) throw new Error('Anlegen fehlgeschlagen') if (!created?.id) throw new Error('Anlegen fehlgeschlagen')
insertExerciseFromOffer(created, activeOffer) insertExerciseFromOffer(created, activeOffer)

View File

@ -880,7 +880,10 @@ export default function ProgressionGraphEditor({ graphId, embedded = false, onSa
setSlotQuickSaving(true) setSlotQuickSaving(true)
setSlotQuickError('') setSlotQuickError('')
try { try {
const payload = buildQuickCreateExercisePayloadFromDraft(slotQuickCreateDraft) const payload = buildQuickCreateExercisePayloadFromDraft(slotQuickCreateDraft, {
visibility: graphMeta?.visibility || 'private',
clubId: graphMeta?.club_id ?? null,
})
const created = await api.createExercise(payload) const created = await api.createExercise(payload)
if (!created?.id) throw new Error('Anlegen fehlgeschlagen') if (!created?.id) throw new Error('Anlegen fehlgeschlagen')
setDraft((prev) => ({ setDraft((prev) => ({

View File

@ -208,11 +208,27 @@ export function aiPreviewToQuickCreateDraft(preview, { title, focusAreaId, sketc
} }
} }
/**
* Sichtbarkeit/club_id für Schnellanlage (z. B. aus Progressionsgraph).
* @param {{ visibility?: string, clubId?: number|null }} [governance]
*/
function resolveQuickCreateGovernance(governance) {
const rawVis = (governance?.visibility || 'private').trim().toLowerCase()
const vis = rawVis === 'club' || rawVis === 'official' ? rawVis : 'private'
let clubId = null
if (vis === 'club' && governance?.clubId != null && governance.clubId !== '') {
const n = Number(governance.clubId)
if (Number.isFinite(n) && n > 0) clubId = n
}
return { visibility: vis, club_id: clubId }
}
/** /**
* createExercise-Payload aus bearbeitetem Entwurf. * createExercise-Payload aus bearbeitetem Entwurf.
* @param {{ visibility?: string, clubId?: number|null }} [governance]
* @throws {Error} * @throws {Error}
*/ */
export function buildQuickCreateExercisePayloadFromDraft(draft) { export function buildQuickCreateExercisePayloadFromDraft(draft, governance) {
const title = (draft?.title || '').trim() const title = (draft?.title || '').trim()
if (title.length < 3) { if (title.length < 3) {
throw new Error('Titel: mindestens 3 Zeichen.') throw new Error('Titel: mindestens 3 Zeichen.')
@ -239,6 +255,7 @@ export function buildQuickCreateExercisePayloadFromDraft(draft) {
if (summary && !stripHtmlToText(summary).trim()) summary = null if (summary && !stripHtmlToText(summary).trim()) summary = null
const skills = (draft?.skillChoices || []).filter((c) => c.include).map((c) => c.after) const skills = (draft?.skillChoices || []).filter((c) => c.include).map((c) => c.after)
const { visibility, club_id: clubId } = resolveQuickCreateGovernance(governance)
return { return {
title, title,
@ -247,7 +264,7 @@ export function buildQuickCreateExercisePayloadFromDraft(draft) {
execution, execution,
preparation: prep, preparation: prep,
trainer_notes: trainerNotes, trainer_notes: trainerNotes,
visibility: 'private', visibility,
status: 'draft', status: 'draft',
equipment: [], equipment: [],
focus_areas_multi: [{ focus_area_id: fid, is_primary: true }], focus_areas_multi: [{ focus_area_id: fid, is_primary: true }],
@ -256,15 +273,16 @@ export function buildQuickCreateExercisePayloadFromDraft(draft) {
target_groups_multi: [], target_groups_multi: [],
age_groups: [], age_groups: [],
skills, skills,
club_id: null, club_id: clubId,
} }
} }
/** /**
* createExercise-Payload aus bestätigter Vorschau (Checkbox-Modus). * createExercise-Payload aus bestätigter Vorschau (Checkbox-Modus).
* @param {{ visibility?: string, clubId?: number|null }} [governance]
* @throws {Error} * @throws {Error}
*/ */
export function buildQuickCreateExercisePayloadFromPreview(preview, { title, focusAreaId, sketchPlain }) { export function buildQuickCreateExercisePayloadFromPreview(preview, { title, focusAreaId, sketchPlain, ...governance } = {}) {
const sketchHtml = aiPlainTextToMinimalHtml(sketchPlain) const sketchHtml = aiPlainTextToMinimalHtml(sketchPlain)
const fieldMap = {} const fieldMap = {}
for (const c of preview?.instructionChoices || []) { for (const c of preview?.instructionChoices || []) {
@ -288,6 +306,7 @@ export function buildQuickCreateExercisePayloadFromPreview(preview, { title, foc
} }
const skills = (preview?.skillChoices || []).filter((c) => c.include).map((c) => c.after) const skills = (preview?.skillChoices || []).filter((c) => c.include).map((c) => c.after)
const { visibility, club_id: clubId } = resolveQuickCreateGovernance(governance)
const fid = Number(focusAreaId) const fid = Number(focusAreaId)
if (!Number.isFinite(fid) || fid < 1) { if (!Number.isFinite(fid) || fid < 1) {
@ -301,7 +320,7 @@ export function buildQuickCreateExercisePayloadFromPreview(preview, { title, foc
execution, execution,
preparation: prep, preparation: prep,
trainer_notes: trainerNotes, trainer_notes: trainerNotes,
visibility: 'private', visibility,
status: 'draft', status: 'draft',
equipment: [], equipment: [],
focus_areas_multi: [{ focus_area_id: fid, is_primary: true }], focus_areas_multi: [{ focus_area_id: fid, is_primary: true }],
@ -310,7 +329,7 @@ export function buildQuickCreateExercisePayloadFromPreview(preview, { title, foc
target_groups_multi: [], target_groups_multi: [],
age_groups: [], age_groups: [],
skills, skills,
club_id: null, club_id: clubId,
} }
} }