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
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:
parent
b629f192ac
commit
4b9374765b
|
|
@ -15,7 +15,7 @@
|
||||||
|
|
||||||
**Plattform-Rechtstexte (P-01, 0.8.95–0.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.95–0.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.137–0.8.140**; Handover **`docs/HANDOVER.md`** §3); **Trainingsrahmenprogramm** (036–037), **Progressionsgraph** (032–034) — 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.137–0.8.140**; Handover **`docs/HANDOVER.md`** §3); **Trainingsrahmenprogramm** (036–037), **Progressionsgraph** (032–034) — 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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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):
|
||||||
|
|
|
||||||
59
backend/tests/test_exercise_progression_graph_visibility.py
Normal file
59
backend/tests/test_exercise_progression_graph_visibility.py
Normal 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",
|
||||||
|
)
|
||||||
|
|
@ -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 F11–F14, Katalog-Kontext); DB siehe **`backend/version.py`** (`DB_SCHEMA_VERSION`, Migration **088**).
|
**App-Version / DB-Schema:** App **`0.8.233`** (Planungs-KI F11–F14, 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.231–0.8.232** |
|
| **F12** | Post-Match-Gate, LLM-QA nach Rematch, Gap-Timing, `roadmap_unfilled`-Sync | ✅ **0.8.231–0.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 **~85–88 %** — gilt **Stufenlogik**, nicht leere Slots. **Gesamt-Pfad-QS** = Minimum aus **`roadmap_qa`** + **`assignment_qa`** (leere Slots → Besetzung ~8–15 %). 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 G0–G4:** 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 G0–G4:** Katalog in Einheits-Editor, Scopes `training_section`/`framework_slot`, Abschnitts-QS, Gruppenkontext-Pack — Details **`PLANNING_PROGRESSION_GRAPH_KI.md`** §16, **`PLANNING_KI_ROADMAP.md`**.
|
||||||
|
|
|
||||||
|
|
@ -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 % | ~8–15 % | **~8–15 %** | 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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 0–4 (Default **3**) |
|
| `max_rematch_rounds` | int | Rematch-Runden 0–4 (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** | ~85–88 % | ~8–15 % | **~8–15 %** | Roadmap ok, Besetzung fehlt |
|
||||||
|
| Nach Match + befüllte Slots | ~85–88 % | 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 (~8–15 % 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.231–0.8.232 |
|
| **F12** | Post-Match-Gate, LLM-QA nach Rematch, Gap-Timing, `roadmap_unfilled`-Sync | ✅ | 0.8.231–0.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
|
||||||
|
|
|
||||||
|
|
@ -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.')
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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) => ({
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user