Implement Phase 3 Enhancements for Skill Scoring and Profiles
Some checks failed
Deploy Development / deploy (push) Failing after 22s
Test Suite / pytest-backend (push) Successful in 35s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Failing after 3s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m14s

- Added capabilities for weighted skill profiles, allowing trainers to compare training modules, frameworks, and regression paths based on skill contributions.
- Updated the skill scoring specification to include peer context separation and list filtering, ensuring accurate comparisons among visible artifacts of the same type.
- Enhanced the API to support batch summaries for skill profiles and discovery suggestions, improving data retrieval efficiency.
- Refactored frontend components to display skill metrics, including scores and peer percentages, with improved filtering options for better user experience.
- Updated documentation to reflect the latest changes and enhancements in the skill scoring system.
This commit is contained in:
Lars 2026-05-21 12:35:45 +02:00
parent a7a428745f
commit 1d698e4b0a
17 changed files with 296 additions and 88 deletions

View File

@ -104,6 +104,7 @@ Die exakten Zahlen hängen von der Umgebung ab (siehe Admin/DB). Die Skills/Übu
**Skills-System:**
- [x] Hierarchisches Schema, Fokusbereich-Zuordnung, Exercise-Skill mit Levels
- [x] **Gewichtetes Fähigkeiten-Profil (Phase 3):** Module, Rahmenprogramme, Regressionspfade; Peer-Kontext getrennt; Listen-Filter + Discovery — **`technical/SKILL_SCORING_SPEC.md`**
**Admin-UI:**

View File

@ -407,10 +407,9 @@ skill_level_definitions (
- Reaktion (Koordination, target_level: 2, intensity: mittel)
**Attribute pro Fähigkeitsbezug:**
- is_primary (Haupt- oder Nebenfähigkeit)
- intensity (niedrig/mittel/hoch)
- required_level (Voraussetzung, 1-5)
- target_level (Ziel-Level, 1-5)
- `intensity` — Nutzeneinschätzung: **niedrig | mittel | hoch** (Standard **mittel**)
- `required_level` / `target_level` — Stufen-Spanne (kanonische Slugs basis … optimierung)
- `is_primary` — Legacy-Feld; **nicht mehr in der UI**, beim Speichern immer false; Scoring ignoriert es
**🆕 Fokusbereich-Filterung:**
- Bei Übungen mit Fokusbereich "Karate" sollten primär KARATE-Fähigkeiten zugeordnet werden
@ -474,6 +473,34 @@ skill_level_definitions (
**Konkretisierung (037/API):** `POST /api/training-units/from-framework-slot` legt eine geplante Einheit aus dem SlotBlueprint an; **`origin_framework_slot_id`** dient als Herkunftsreferenz (**Lineage light**; weiteres Feedback/LineageKonzept: Konzeptpapier Schritt **E**).
### Trainingsmodul (Bibliothek)
**Abgrenzung:** Wiederverwendbare **Übungsfolge** (`training_modules` + `training_module_items`) — kein Kalendertermin, kein Rahmen-Slot. Übernahme in geplante Einheiten über Planung (`apply-training-module`).
**Governance:** wie andere Bibliotheksartefakte (`visibility`, `club_id`, `library_content_visibility_sql`).
### Gewichtetes Fähigkeiten-Profil (Planungs-Bausteine, Phase 3)
**Zweck:** Aus den verknüpften Übungen eines Planungsartefakts wird ein **Fähigkeiten-Profil** berechnet (Trainingsgewicht je Fähigkeit). Trainer vergleichen Bausteine **innerhalb desselben Typs**, um z.B. das passendste Modul für eine Ziel-Fähigkeit zu finden.
**Artefakttypen (getrennte Peer-Kontexte):**
| Typ | Vergleich |
|-----|-----------|
| `training_module` | nur sichtbare **Module** |
| `framework_program` | nur sichtbare **Rahmenprogramme** |
| `progression_graph` | nur sichtbare **Regressionspfade** |
**Metriken (Nutzer):**
- **Score / Gewicht** — absolut (Dauer × Häufigkeit × Intensität × Stufen-Spanne)
- **Prozent** — Anteil am stärksten sichtbaren Peer **desselben Typs** für diese Fähigkeit (max. 100 %)
- **★** — stärkster Peer in diesem Kontext
**UI:** Profile in Editoren; KPI-Kacheln und Filter in Listen (`/planning/framework-programs`, `/planning/training-modules`); Discovery auf der Fähigkeiten-Seite.
**Technik:** `backend/skill_scoring.py`, `routers/skill_profiles.py` — Spec **`technical/SKILL_SCORING_SPEC.md`**.
### Parallele Trainingsstreams (Breakout)
**Fachlich:** Eine Kalender**Einheit** kann aus **Phasen** bestehen — z.B. gemeinsamer Block, dann **beliebig viele parallele** „Teilstrecken“ (**Streams**) mit je eigenem Miniplan (Abschnitte/Übungen), erneut gemeinsamer Block. Das ist **nicht** dasselbe wie ein **RahmenprogrammSlot** (SerienSession über Wochen): Slots strukturieren **mehrere Einheiten** in einem Programm; **Streams** strukturieren **gleichzeitige** Abläufe **innerhalb einer** Einheit.
@ -648,12 +675,13 @@ skill_level_definitions (
- [ ] Level-Definitionen aus Fähigkeitsmatrix extrahieren (optional)
- [ ] Skills-Beschreibungen aus Wiki importieren (Migration 024)
- [ ] Admin-UI für Fähigkeiten-Kategorien (CRUD)
- [ ] Skill-Filter in Übungssuche integrieren
- [x] Skill-Filter in Übungssuche (SkillTreeMultiSelect + Stufen)
- [x] Gewichtetes Fähigkeiten-Profil für Planungs-Bausteine (Module, Rahmen, Pfade) — siehe `technical/SKILL_SCORING_SPEC.md`
- [ ] Reifegradmodelle definieren (Kombination Fokusbereich + Stil + Zielgruppe)
- [ ] KI-Unterstützung für Trainingsplanung (basierend auf Fähigkeiten-Level)
---
**Letzte Aktualisierung:** 2026-04-27
**Letzte Aktualisierung:** 2026-05-20
**Verantwortlich:** Claude Code
**Review:** Pending

View File

@ -1,6 +1,6 @@
# Gelieferte Features & technische Basis (Q2 2026)
**Stand:** 2026-05-12
**Stand:** 2026-05-20
**Referenz:** `backend/version.py` — aktuelle **APP_VERSION** / **DB_SCHEMA_VERSION** (Stand Code u. a. **0.8.96**)
Dieses Dokument bündelt die in der Entwicklungsphase erreichten **lieferbaren** Funktionen und die zugehörigen **technischen Artefakte**. TrainingsrahmenBibliothek + SlotBlueprint: **`technical/TRAINING_FRAMEWORK_SPEC.md`** §2. **Progressionsgraph zwischen Übungen** (Zwischenstand, Grenzen): **§§34**. **Medien-Archiv & Bibliothek:** Abschnitt **12** unten + **`MEDIA_ASSETS_AND_ARCHIVE_SPEC.md`**. Detail-Spezifikationen bleiben in den verlinkten Pfaden unter `.claude/docs/technical/` und `.claude/docs/functional/`.
@ -169,11 +169,35 @@ Einzelnorm: **`technical/MEDIA_ASSETS_AND_ARCHIVE_SPEC.md`**. Kurzüberblick gel
---
## 15. Verweise
## 15. Gewichtetes Fähigkeiten-Scoring (Phase 3, Stand 2026-05-20)
Norm: **`technical/SKILL_SCORING_SPEC.md`**.
### 15.1 Backend
- **`skill_scoring.py`:** Gewichtung (Dauer × Vorkommen × Intensität × Stufen); `compute_planning_corpus_by_type()` mit getrennten Corpora; `universal_percent` capped auf 100 %
- **`routers/skill_profiles.py`:** Profile-GET pro Artefakt; `POST /api/skill-profiles/batch-summaries`; `GET /api/skill-discovery/suggestions`
- Sichtbarkeit: **`library_content_visibility_sql`** (Planungs-Bibliothek, nicht „nur Verein club“)
### 15.2 Frontend
- **Listen:** Rahmenprogramme + Trainingsmodule — Filter-Modal (wie Übungen), Chips, `SkillTreeMultiSelect` (Portal-Dropdown)
- **KPI:** `SkillProfileCompact` — Top je Unterkategorie, Score + Peer-%
- **Editoren + Modal:** `SkillProfilePanel`, `SkillProfileFullModal`
- **Discovery:** `SkillDiscoveryPanel` auf Fähigkeiten-Seite
### 15.3 Offen
- Corpus-Caching; pytest für Typ-Trennung; Filter-Persistenz; Skill-Filter Import-Dialog „Rahmen übernehmen“
---
## 16. Verweise
| Thema | Dokument |
|--------|----------|
| Rahmenprogramm / Progressionsgraph | `technical/TRAINING_FRAMEWORK_SPEC.md` |
| Fähigkeiten-Scoring Planung | `technical/SKILL_SCORING_SPEC.md` |
| API Übungen | `technical/EXERCISES_API_SPEC.md` |
| Domänenmodell | `functional/DOMAIN_MODEL.md` |
| Datenbank Überblick | `technical/DATABASE_SCHEMA.md` |

View File

@ -1,12 +1,33 @@
# Gewichtetes Fähigkeiten-Scoring (Phase 3)
**Stand:** 2026-05-20
**Status:** Variante A (regelbasiert) umgesetzt — **v1.2** (Kategorien-Gruppierung + universelle Skala)
**Modul:** `backend/skill_scoring.py`, Router `skill_profiles`
**Status:** Variante A (regelbasiert) umgesetzt — **v1.3** (Peer-Kontext getrennt + Listen-Filter)
**Modul:** `backend/skill_scoring.py`, Router `backend/routers/skill_profiles.py`
## Ziel
Trainer wählen **Schwerpunkt-Fähigkeiten** und erhalten Vorschläge für **Rahmenprogramme**, **Trainingsmodule** und **Regressionspfade** (Progressionsgraphen), deren Übungen diese Fähigkeiten stark abdecken.
Trainer wählen **Schwerpunkt-Fähigkeiten** und finden passende **Bausteine** für die Trainingsplanung:
- **Trainingsmodule** — wiederverwendbare Übungsfolgen
- **Rahmenprogramme** — Programme mit Zielen und Session-Slots
- **Regressionspfade** (Progressionsgraphen) — Übungsketten
Das Scoring beantwortet: *Wie stark trainiert dieser Baustein eine Fähigkeit?* und *Wie stark ist er im Vergleich zu anderen **sichtbaren** Bausteinen **desselben Typs**?*
## Fachliche Kernregel: Peer-Kontext (nicht vermischen)
| Planungs-Artefakt | Vergleichsgruppe (`universal_percent`) |
|-------------------|----------------------------------------|
| Trainingsmodul | nur andere **sichtbare Module** |
| Rahmenprogramm | nur andere **sichtbare Rahmenprogramme** |
| Regressionspfad | nur andere **sichtbare Pfade** |
**Nicht** verglichen werden:
- Module vs. Rahmenprogramme vs. Pfade (kein Mix)
- Artefakte anderer Vereine, auf die der Nutzer keinen Planungszugriff hat
**Sichtbarkeit:** `library_content_visibility_sql` — private, vereinsinterne und offizielle Inhalte gemäß Mandant/Rolle, analog zu anderen Bibliothekslisten.
## Datenquellen
@ -19,7 +40,7 @@ Trainer wählen **Schwerpunkt-Fähigkeiten** und erhalten Vorschläge für **Rah
Fähigkeiten je Übung: `exercise_skills``skills` (nur `status = active`).
## Gewichtungsformel (v1.1)
## Gewichtungsformel (v1.1 / v1.2)
Pro **Übungsvorkommen** (eine Zeile im Ablauf / Modul / Kanten-Endpunkt):
@ -44,8 +65,6 @@ Kanonische Slugs: basis … optimierung (15). Fehlen beide: Faktor 1,0.
- **Mittelpunkt** = durchschnittliche Stufe
- Faktor ≈ `(0,92 + 0,04 × Spanne) × (0,95 + 0,025 × Mittelpunkt)` → typisch 0,961,20
Beispiel: von Grundlagen bis Aufbau (Spanne 2) > nur Basis (Spanne 1).
### Bewusst nicht im Scoring
| Feld | Grund |
@ -53,44 +72,113 @@ Beispiel: von Grundlagen bis Aufbau (Spanne 2) > nur Basis (Spanne 1).
| `is_primary` | Perspektivabhängig; bleibt in Übungs-UI, fließt nicht ins Profil ein |
| `development_contribution` | Legacy-DB-Feld, in UI nicht gepflegt |
Aggregation:
## Aggregierte Metriken
- Summe pro `skill_id``weight` / `score` (Trainingsgewicht in gewichteten Minuten — **absolut, über Programme vergleichbar**)
- `artifact_share_percent` / `share_percent` = Anteil an `total_weight` **innerhalb dieses Artefakts** (summiert 100 % — nur noch sekundär)
- `by_main_category[]` → je Unterkategorie `top_skill` (stärkste Fähigkeit nach absolutem Gewicht)
- `universal_percent` = `weight / max_weight_in_corpus(skill_id) × 100` — Referenz aus **Vereins-Artefakten** (`visibility=club`, aktiver Verein): Rahmenprogramme, Module, Regressionspfade
- `club_best` / `club_best_by_skill`: stärkstes Vereins-Element je Fähigkeit (Titel, Typ, Gewicht)
- Listen: `POST /api/skill-profiles/batch-summaries` — ein Corpus-Durchlauf, kompakte Profile für viele IDs
| Feld | Bedeutung |
|------|-----------|
| `weight` / `score` | Absolutes **Trainingsgewicht** (gewichtete Minuten) — über alle Fähigkeiten eines Artefakts vergleichbar |
| `share_percent` | Anteil am `total_weight` **innerhalb dieses Artefakts** (summiert 100 %) — sekundär |
| `by_main_category[]` | Je Unterkategorie `top_skill` (stärkste Fähigkeit nach Gewicht) |
| `universal_percent` | Anteil am **Maximum derselben Fähigkeit im Peer-Kontext** (max. 100 %) |
| `is_club_best_for_skill` | Stärkster sichtbarer Peer für diese Fähigkeit (★ in UI) |
| `club_best` | Referenz-Peer (Titel, Typ, Gewicht) — **Legacy-Name**, fachlich Peer-Best |
Discovery-Sortierung und Vorschläge nutzen **`match_score`** (= Summe absoluter Gewichte der gewählten Fähigkeiten), nicht den Plan-internen Anteil.
### Berechnung `universal_percent`
```
effective_ref = max(max_weight_in_peer_corpus(skill_id), eigenes_gewicht)
universal_percent = min(100, weight / effective_ref × 100)
```
Corpus je Typ: `compute_planning_corpus_by_type()` scannt sichtbare Artefakte getrennt nach `framework_program`, `training_module`, `progression_graph`.
Discovery-Sortierung nutzt **`match_score`** (= Summe absoluter Gewichte der gewählten Fähigkeiten), nicht den Plan-internen Anteil. Discovery verwendet typ-getrennte Referenz (`fw_ref`, `mod_ref`, `graph_ref`).
## API
| Methode | Pfad | Beschreibung |
|---------|------|--------------|
| GET | `/api/training-framework-programs/{id}/skill-profile` | `overall` + `slots[]` mit je `profile` |
| GET | `/api/training-modules/{id}/skill-profile` | `overall` |
| GET | `/api/exercise-progression-graphs/{id}/skill-profile` | `overall` |
| GET | `/api/training-framework-programs/{id}/skill-profile` | `overall` + `slots[]` mit je `profile`; `reference_scale` (Peer-Kontext Rahmenprogramme) |
| GET | `/api/training-modules/{id}/skill-profile` | `overall`; `reference_scale` (Peer-Kontext Module) |
| GET | `/api/exercise-progression-graphs/{id}/skill-profile` | `overall`; `reference_scale` (Peer-Kontext Pfade) |
| POST | `/api/skill-profiles/batch-summaries` | Kompakte Profile für Listen; Body: `frameworkProgramIds`, `trainingModuleIds`, …; Response: `summaries`, `reference_scale_by_type`, `club_best_by_skill` |
| GET | `/api/skill-discovery/suggestions?skill_ids=1,2,3` | Ranking sichtbarer Artefakte; Query `types`, `limit` |
Zugriff: `get_tenant_context` + gleiche Sichtbarkeit wie Parent-Artefakt (`library_content_visibility_sql`).
Zugriff: `get_tenant_context` + `library_content_visibility_sql` wie Parent-Artefakt.
### `reference_scale` / `reference_scale_by_type`
```json
{
"scope": "planning_peer",
"artifact_type": "training_module",
"artifacts_scanned": 12,
"skills_in_corpus": 34,
"description": "Prozent = Anteil am stärksten sichtbaren Eintrag unter Trainingsmodulen …"
}
```
## UI
- **Rahmenprogramm bearbeiten:** Panel „Fähigkeiten-Schwerpunkte“, inkl. Aufklapp pro Session
- **Trainingsmodul bearbeiten:** Panel „Fähigkeiten im Modul“
- **Progressionsgraph:** Panel „Fähigkeiten entlang des Pfads“
- **Fähigkeiten-Seite → Planungs-Vorschläge:** Multi-Select + Bibliothekssuche
### Bearbeitung (Vollprofil)
Profil wird nach **Speichern** neu geladen (`skillProfileTick`).
| Ort | Panel | `artifactType` |
|-----|-------|----------------|
| Rahmenprogramm bearbeiten | Fähigkeiten-Schwerpunkte (+ Sessions) | `framework_program` |
| Trainingsmodul bearbeiten | Fähigkeiten im Modul | `training_module` |
| Progressionsgraph | Fähigkeiten entlang des Pfads | `progression_graph` |
Anzeige: Top je Kategorie (Editor) oder alle Fähigkeiten (Modal). Hinweise nennen Peer-Kontext explizit (z. B. „72 % Rahmenpr.“).
### Listen & Filter (UX wie Übungsliste)
| Liste | Filter |
|-------|--------|
| Rahmenprogramme (`/planning/framework-programs`) | Suche, Katalog (Fokus/Trainingsart/Zielgruppe), Session-Dauer, **Fähigkeiten** (`SkillTreeMultiSelect`), Mindest-% im Peer-Kontext, Sortierung nach Stärke |
| Trainingsmodule (`/planning/training-modules`) | Suche, **Fähigkeiten** (+ Min-%, Sortierung) |
- **Filter-Button** mit Badge, entfernbare **Chips**, Einstellungen im **Modal** (`PlanningArtifactFilterModal`)
- KPI-Kacheln: Top-Fähigkeit **je Unterkategorie** mit Score + Peer-%
- Vollprofil-Modal: `SkillProfileFullModal` mit `displayMode=full`
Profil wird nach Speichern neu geladen (`skillProfileTick` in Editoren).
### Discovery
**Fähigkeiten-Seite → Planungs-Vorschläge:** Multi-Select + API `/api/skill-discovery/suggestions` (optional Filter `types`).
## Frontend-Module (Auswahl)
| Pfad | Rolle |
|------|--------|
| `frontend/src/components/planning/PlanningArtifactFilterModal.jsx` | Filter-Modal |
| `frontend/src/components/planning/PlanningSkillFilterSection.jsx` | Fähigkeiten-Block im Modal |
| `frontend/src/utils/planningArtifactFilterChips.js` | Chip-Labels + Entfernen |
| `frontend/src/utils/frameworkProgramListHelpers.js` | Client-Filter Rahmenprogramme |
| `frontend/src/utils/trainingModuleListHelpers.js` | Client-Filter Module |
| `frontend/src/components/skills/SkillProfileCompact.jsx` | KPI-Kacheln in Listen |
| `frontend/src/components/SkillTreeMultiSelect.jsx` | Baumauswahl (Portal-Dropdown in Modals) |
## Grenzen / später
- Kein Cache in DB (`skill_profile_json`) — on-the-fly; bei Performance >50 Artefakte serverseitiger Index
- Kein DB-Cache (`skill_profile_json`) — on-the-fly; bei >50 Artefakten pro Typ serverseitiger Index/Caching
- Entwicklungsziele am Rahmenkopf bleiben Freitext (kein Scoring)
- KI-Zusammenfassung (Variante B Roadmap) nicht Teil von v1.0
- KI-Zusammenfassung (Variante B) nicht Teil von v1.0
- Trainings**einheiten** (Kalender) optional als nächste Erweiterung
- Filter-Persistenz („Als Standard speichern“) wie bei Übungen — noch nicht für Planungslisten
- Fähigkeiten-Filter im Dialog **Planung → Rahmen übernehmen** — Katalog ja, Skill-Filter optional nachziehen
- API-Feldnamen `club_*` / `skillMinClubPercent` — technische Altlast, semantisch Peer-Kontext
## Tests
- `backend/tests/test_skill_scoring.py` — Multiplikator, Aggregation, Match-Score
- `backend/tests/test_skill_scoring.py` — Multiplikator, Aggregation, Match-Score, Cap 100 %
- **Offen:** dedizierte Tests für `compute_planning_corpus_by_type` (Typ-Trennung)
## Verweise
| Dokument | Inhalt |
|----------|--------|
| `functional/DOMAIN_MODEL.md` | Domänenabschnitt Planungs-Fähigkeiten-Profil |
| `docs/FACHLICHE_NUTZERFUNKTIONEN.md` | Nutzerüberblick Listen/Filter |
| `docs/HANDOVER.md` | Handover-Abschnitt Phase 3 |
| `technical/ACCESS_LAYER_AND_GOVERNANCE_PLAN.md` | Sichtbarkeit / Mandant |

View File

@ -16,6 +16,7 @@
| `functional/DOMAIN_MODEL.md` | Fachliche Begriffe; Kurzverweis auf Progressionsgraph ergänzt. |
| `TRAINING_CURRICULUM_AND_GOVERNANCE_CONCEPT.md` | **Was** und **warum** (Bibliothek vs. Instanz, Governance, CURRTabelle). |
| `functional/PARALLEL_TRAINING_STREAMS_CONCEPT.md`, `technical/PARALLEL_TRAINING_STREAMS_SPEC.md` | **Parallele Streams / Breakout innerhalb einer Einheit** — orthogonale Domäne zu **RahmenSlots** (SerienSessions). |
| `technical/SKILL_SCORING_SPEC.md` | **Fähigkeiten-Profil** der RahmenSlots / Module / Pfade; Listen-Filter und PeerVergleich (nur gleicher Artefakttyp). |
**Konsequenz:** Diese Datei bleibt der **technische Arbeitspool** für Rahmenprogramm Stufe 12. Abschnitt **§4** beschreibt explizit den **aktuellen Produktfreigabe-Umfang** und **bekannte Lücken** (damit Trainingsplanung weiter gebaut werden kann ohne falscher Erwartung an „AlternativePakete“ in der UI).

View File

@ -2,7 +2,7 @@
**Bezug:** `functional/Shinkan Trainingsmodule Kombinationsuebungen Spezifikation V2.md` (Kopf „V3“, inkl. **§10.2.1**, **§10.4 Coaching-Stufen**, **Anhang A** Implementierungsabgleich — Drift-Schutz)
**Technische Entwurfsspezifikation:** `technical/TRAINING_MODULES_AND_COMBINATION_EXERCISES_SPEC.md`
**Stand dieses Dokuments:** 2026-05-12 (Abgleich mit Code App **0.8.110**, siehe `backend/version.py`)
**Stand dieses Dokuments:** 2026-05-20 (Abgleich mit Code, siehe `backend/version.py`)
## Ziele
@ -12,7 +12,7 @@ Umsetzung der MVP-Punkte aus der Fachspezifikation ohne die bestehende Planung z
| Phase | Inhalt | Status |
|-------|--------|--------|
| **1** | **Trainingsmodule (Bibliothek):** Tabellen `training_modules`, `training_module_items`; REST CRUD mit Governance wie andere Bibliotheken; Übernahme in eine bestehende Einheit per `POST /api/training-units/{id}/apply-training-module` (Anfügen ans Ende eines Abschnitts via `section_order_index`); optionale Lineage-Spalte `source_training_module_id` auf Planungsitems; UI: Liste/Editor unter `/planning/training-modules`, Link von der Planung, Modal „Modul übernehmen“ | **umgesetzt (MVP Schritt 1)** |
| **1** | **Trainingsmodule (Bibliothek):** Tabellen `training_modules`, `training_module_items`; REST CRUD mit Governance wie andere Bibliotheken; Übernahme in eine bestehende Einheit per `POST /api/training-units/{id}/apply-training-module` (Anfügen ans Ende eines Abschnitts via `section_order_index`); optionale Lineage-Spalte `source_training_module_id` auf Planungsitems; UI: Liste/Editor unter `/planning/training-modules`, Link von der Planung, Modal „Modul übernehmen“; **Ergänzung 2026-05-20:** Fähigkeiten-Profil + Listen-Filter (Peer-Vergleich nur unter Modulen) — `technical/SKILL_SCORING_SPEC.md` | **umgesetzt (MVP Schritt 1)** |
| **2** | Kombinationsübungen: `exercise_kind`/`combination_*`, Slots, Pools, `method_archetype`, `method_profile` (JSON) | **teilweise** — wie links; zusätzlich **057** `planning_method_profile`; Planungs-Merge Client (`effectiveComboMethodProfile`); Archetypen weiterhin **nur Code-Konstanten** (kein Admin) | **Offen:** Archetyp-Admin-UI; Profil↔Archetyp-Validierung Backend; „alle Slots vorbelegen“ / Presets (siehe Fachspez **§10.6**); Haupt-/Nebenmethoden an Kombi wo Spec es verlangt |
| **3** | Planungsblöcke: Gruppierung, Auflösen, „als Modul speichern“, erweiterter Übernahmemodus (Zwischenposition) | geplant |
| **4** | Coaching: Archetyp-Support | **teilweise:** **Stufe A** — Merge Katalog+Planung; `CombinationPlanBracket` in Peek/Run; globale Profilzahlen mit Labels (`describeGlobalComboProfile`); Stations-/Timing-Zusammenfassung inkl. Wdh.-Hinweise. **Stufe B/C****offen**10.6, Anhang A) |

View File

@ -12,6 +12,7 @@
> | Setup-Dokument | `.claude/docs/working/SHINKAN_PROJECT_SETUP.md` |
> | Anforderungen | `.claude/docs/functional/SHINKAN_REQUIREMENTS.md` |
> | Medien-Archiv, Lifecycle, Inline (Plan §11) | `.claude/docs/technical/MEDIA_ASSETS_AND_ARCHIVE_SPEC.md` |
> | Fähigkeiten-Scoring (Planungs-Bausteine) | `.claude/docs/technical/SKILL_SCORING_SPEC.md` |
> | Handover / nächste Session | **`docs/HANDOVER.md`** |
> | Fachlicher Nutzerüberblick (Design/Product) | **`docs/FACHLICHE_NUTZERFUNKTIONEN.md`** |
> | Architektur-Zielbild, Refaktor-Roadmap, verbindliche Shinkan-Regeln | **`docs/architecture/README.md`** |
@ -90,6 +91,7 @@ Kurz (Stand 2026-05-14): App- und DB-Version siehe **`backend/version.py`**; Ker
### Log (Auszug)
- 2026-05-20: **Fähigkeiten-Scoring Phase 3** — gewichtete Profile für Module/Rahmen/Pfade; Peer-Vergleich getrennt nach Artefakttyp; Listen-Filter + Discovery — siehe `SKILL_SCORING_SPEC.md`, `docs/HANDOVER.md` §2.6, `FEATURES_DELIVERED_2026-Q2.md` §15.
- 2026-05-07: **Medien** — zentrales Archiv (`media_assets`), Bibliothek-UI, Lifecycle/Papierkorb, `from-asset`, Speicherpfade `library/…`, Governance `official`/Copyright; **0.8.59** aktiver Verein UI/API-Sync — siehe `.claude/docs/library/FEATURES_DELIVERED_2026-Q2.md` §12, `docs/HANDOVER.md`.
- 2026-05-05: Rahmen nur Bibliothek (**036**), SlotAblauf = `training_units` + Sektionen (**037**), `POST /api/training-units/from-framework-slot`, keine `training_framework_slot_exercises` mehr — siehe `DATABASE_SCHEMA.md` / `FEATURES_DELIVERED_2026-Q2.md`.
- 2026-04-27: Übungsvarianten API/UI, Migration 030, Listen-UX-Suche, Admin-Upload-Limits — siehe `PROJECT_STATUS.md` und `docs/library/FEATURES_DELIVERED_2026-Q2.md`.

View File

@ -113,6 +113,25 @@ def normalize_exercise_skill_level(value) -> Optional[str]:
return s
return _LEGACY_SKILL_LEVEL_SLUG.get(s)
_ALLOWED_SKILL_INTENSITY = frozenset({"niedrig", "mittel", "hoch"})
def normalize_exercise_skill_intensity(value) -> str:
"""Kanonische Nutzeneinschätzung; leer/ungültig → mittel (kein leerer Wert)."""
if value is None:
return "mittel"
key = str(value).strip().lower()
if key in ("low",):
return "niedrig"
if key in ("medium",):
return "mittel"
if key in ("high",):
return "hoch"
if key in _ALLOWED_SKILL_INTENSITY:
return key
return "mittel"
MEDIA_ROOT = Path(os.getenv("MEDIA_ROOT", str(Path(__file__).resolve().parent.parent / "media")))
MAX_EXERCISE_MEDIA = 10
# Upload-Limits (Übungs-Medien): Trainer wie bisher kleiner; Admin/Superadmin höheres Limit für große Videos
@ -1102,13 +1121,15 @@ def enrich_exercise_detail(exercise_id: int, cur) -> dict:
FROM exercise_skills es
JOIN skills s ON es.skill_id = s.id
WHERE es.exercise_id = %s
ORDER BY es.is_primary DESC, s.name""",
ORDER BY s.name""",
(exercise_id,)
)
exercise["skills"] = [r2d(r) for r in cur.fetchall()]
for sk in exercise["skills"]:
sk["required_level"] = normalize_exercise_skill_level(sk.get("required_level"))
sk["target_level"] = normalize_exercise_skill_level(sk.get("target_level"))
sk["intensity"] = normalize_exercise_skill_intensity(sk.get("intensity"))
sk["is_primary"] = False
# Variants (1:N) - mit Progression (Reihenfolge: sequence_order, dann progression_level)
cur.execute(
@ -1223,8 +1244,8 @@ def assign_exercise_relations(
(
exercise_id,
skill["skill_id"],
skill.get("is_primary", False),
skill.get("intensity"),
False,
normalize_exercise_skill_intensity(skill.get("intensity")),
normalize_exercise_skill_level(skill.get("required_level")),
normalize_exercise_skill_level(skill.get("target_level")),
skill.get("ai_suggested", False),

View File

@ -18,6 +18,7 @@ from db import get_db, get_cursor, r2d
from auth import require_auth
from smw_client import SmwClient, SmwClientError
from smw_mapper import map_wiki_to_exercise, map_wiki_to_skill, map_wiki_to_method, build_skill_assignments
from routers.exercises import normalize_exercise_skill_intensity
logger = logging.getLogger(__name__)
@ -615,14 +616,16 @@ def _assign_exercise_skills(cur, conn, exercise_id: int, skill_assignments: list
VALUES (%s, %s, %s, %s, %s, %s)
ON CONFLICT (exercise_id, skill_id) DO UPDATE SET
target_level = EXCLUDED.target_level,
is_primary = EXCLUDED.is_primary""",
required_level = EXCLUDED.required_level,
intensity = EXCLUDED.intensity,
is_primary = false""",
(
exercise_id,
sid,
assignment.get("target_level"),
assignment.get("required_level"),
assignment.get("intensity"),
assignment.get("is_primary", False),
normalize_exercise_skill_intensity(assignment.get("intensity")),
False,
)
)
conn.commit()

View File

@ -377,8 +377,8 @@ def build_skill_assignments(mapped: dict) -> list[dict]:
"skill_name": skill_name,
"target_level": target_slug,
"required_level": None,
"intensity": None,
"is_primary": idx == 0,
"intensity": "mittel",
"is_primary": False,
})
return assignments

View File

@ -0,0 +1,20 @@
"""Normalisierung Übung ↔ Fähigkeit Intensität."""
from routers.exercises import normalize_exercise_skill_intensity
def test_normalize_exercise_skill_intensity_defaults_to_mittel():
assert normalize_exercise_skill_intensity(None) == "mittel"
assert normalize_exercise_skill_intensity("") == "mittel"
assert normalize_exercise_skill_intensity("") == "mittel"
def test_normalize_exercise_skill_intensity_canonical():
assert normalize_exercise_skill_intensity("niedrig") == "niedrig"
assert normalize_exercise_skill_intensity("mittel") == "mittel"
assert normalize_exercise_skill_intensity("hoch") == "hoch"
def test_normalize_exercise_skill_intensity_legacy_aliases():
assert normalize_exercise_skill_intensity("low") == "niedrig"
assert normalize_exercise_skill_intensity("medium") == "mittel"
assert normalize_exercise_skill_intensity("high") == "hoch"

View File

@ -66,6 +66,8 @@ Die sichtbaren Funktionen hängen von **Rolle** und **Kontext** ab (eingeloggter
- **Globaler Fähigkeitskatalog** mit hierarchischer Struktur (Kategorien, Stufen); Zuordnung zu Übungen.
- **Trainingsmethoden-Katalog** (bestehende Domäne).
- **Admin/Katalog-Pflege** für Fokusbereiche, Stile, Zielgruppen und Zusammenhänge (Plattform-Admin-Bereich).
- **Planungs-Vorschläge (Phase 3):** Auf der Fähigkeiten-Seite können Schwerpunkte gewählt werden; Shinkan schlägt passende **Rahmenprogramme**, **Trainingsmodule** und **Regressionspfade** vor (Sortierung nach Trainingsgewicht).
- **Fähigkeiten-Profile an Planungs-Bausteinen:** Listen zeigen pro Modul/Rahmenprogramm KPI-Kacheln (Top je Kategorie) mit **Score** und **Peer-Prozent** — Vergleich nur unter sichtbaren Bausteinen **desselben Typs** (Modul vs. Modul, nicht Modul vs. Plan). Filter analog zur Übungsliste (Modal, Chips, Baumauswahl).
### 4.3 Reifegradmodelle (Fähigkeitsmatrix)
@ -78,6 +80,8 @@ Die sichtbaren Funktionen hängen von **Rolle** und **Kontext** ab (eingeloggter
- **Phasen & parallele Streams (Breakout):** Eine Einheit kann aus abwechselnden **Ganzgruppenphasen** und **Parallelphasen** bestehen; in einer Parallelphase führen **mehrere Streams** (Teilstrecken) je eigene Abschnitte/Übungen. Planung über Breakout-UI; API liefert **`phases`** und flache **`sections`** (Migration **063**, siehe **`docs/HANDOVER.md`**). Technische Details: `.claude/docs/technical/PARALLEL_TRAINING_STREAMS_SPEC.md`.
- **Trainingsvorlagen / Mikrovorlagen** (wo eingerichtet): Struktur wiederverwenden (Phasen in Vorlagen: Ausbau siehe Handover „offen“).
- **Trainingsrahmenprogramm (Bibliothek):** übergeordnete Programme mit **Zielen** und **Slots**; Slot-Inhalt technisch als **Blueprint-Trainingsunit** abgebildet.
- **Trainingsmodule (Bibliothek):** wiederverwendbare Übungsfolgen; Übernahme in geplante Einheiten; **Fähigkeiten-Profil** und **Filter** in der Modul-Liste (Peer-Vergleich nur unter Modulen).
- **Rahmenprogramm-Liste:** Suche und Filter (Katalog, Dauer, Fähigkeiten) — Peer-Vergleich nur unter Rahmenprogrammen.
- **Materialisierung:** aus einem Rahmen-Slot kann eine **konkrete Kalender-Einheit** für eine Gruppe erzeugt werden (API vorhanden; UI-Anbindung kann erweitert werden).
- **Durchführung („Plan & Ablauf“):** Ablauf anhand Phasen/Streams darstellen und abarbeiten (inkl. Split-Logik in der Anzeige).
- **Coaching-Modus:** eigener Ablauf mit Schritt-für-Schritt-Timeline, Stream-Wahl pro Parallelphase, Hinweis **„Parallelphase · Abschluss“** (Gruppen zusammenführen) vor der nächsten Ganzgruppenphase oder vor dem nächsten Split; **Nachbereitung** mit Ist-Minuten und Speichern wie in der Planung (inkl. **`phases`**). Nach erfolgreichem Speichern Wechsel zur **Plan- und Ablaufsicht** derselben Einheit. Bei **Kombinationsübungen** zusätzlich **Stations-/Kandidaten-Schicht und Archetyp-Hinweise** (Fachspez **Anhang A**; Ausbauschritte B/C).
@ -129,6 +133,7 @@ Nicht als „broken“ gemeint, sondern als **typische nächste Ausbaustellen**
| Datum | Änderung |
|-------|----------|
| 2026-05-20 | Fähigkeiten-Scoring Phase 3: Peer-Kontext, Listen-Filter Module/Rahmen, Planungs-Vorschläge. |
| 2026-05-14 | Trainingsplanung: Phasen/parallele Streams, Coaching (Rejoin, Nachbereitung → Planansicht); Lücken §5 ergänzt. Verweis `HANDOVER.md`. |
| 2026-05-12 | Erstfassung für Übergabe an fachliches Design; Abgleich mit Code-Navigation, `version.py`, `HANDOVER.md`, `FEATURES_DELIVERED`, `DOMAIN_MODEL`. |
| 2026-05-12 | Kombinationsübungen + Coaching Stufe A; Verweise auf Fachspezifikation (`…Kombinationsuebungen…` V3 Anhang A) und `TRAINING_MODULES_IMPLEMENTATION_PLAN.md`. |

View File

@ -1,6 +1,6 @@
# Shinkan Jinkendo Entwicklungsstand & Handover
**Stand:** 2026-05-19
**Stand:** 2026-05-20
**App-Version / DB-Schema:** App **`0.8.149`** (Einheiten-Editor Vollseite), DB-Schema **`20260515063`** — maßgeblich **`backend/version.py`**: `APP_VERSION`, `DB_SCHEMA_VERSION`
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**.
@ -35,6 +35,7 @@ Das Schema ist gegenüber dem Code zurück: Migration **`022_skills_schema_compl
| **Umsetzungsplan** (Module/Kombination/Coach) | `.claude/docs/working/TRAINING_MODULES_IMPLEMENTATION_PLAN.md` |
| Überblick DB | `.claude/docs/technical/DATABASE_SCHEMA.md` |
| Domäne | `.claude/docs/functional/DOMAIN_MODEL.md` |
| **Gewichtetes Fähigkeiten-Scoring (Phase 3)** | `.claude/docs/technical/SKILL_SCORING_SPEC.md` |
| **Lieferliste inkl. Medien** | `.claude/docs/library/FEATURES_DELIVERED_2026-Q2.md` §12 |
| **Fachlicher Nutzerüberblick (Design/Product)** | **`docs/FACHLICHE_NUTZERFUNKTIONEN.md`** |
@ -69,6 +70,15 @@ Das Schema ist gegenüber dem Code zurück: Migration **`022_skills_schema_compl
- **`AdminMaturityModelsPage.jsx`**, **`MaturityModelBindingsAdmin.jsx`**, **`MaturityMatrixToolsAdmin.jsx`**; APIs in `api.js`.
### 2.6 Gewichtetes Fähigkeiten-Scoring (Phase 3, Stand 2026-05-20)
- **Spec:** `.claude/docs/technical/SKILL_SCORING_SPEC.md`
- **Backend:** `skill_scoring.py`, `routers/skill_profiles.py` — Profile on-the-fly aus Übungsvorkommen + `exercise_skills`; **Peer-Kontext getrennt** (`framework_program` | `training_module` | `progression_graph`) über `library_content_visibility_sql`
- **Metriken:** Trainingsgewicht (`weight`); **Peer-%** (`universal_percent`, max. 100 %) nur unter sichtbaren Bausteinen **desselben Typs**; ★ = stärkster Peer je Fähigkeit
- **API:** Skill-Profile pro Artefakt; `POST /api/skill-profiles/batch-summaries` für Listen; `GET /api/skill-discovery/suggestions`
- **Frontend:** KPI-Kacheln + Filter-Modal (UX wie Übungsliste) auf **`/planning/framework-programs`** und **`/planning/training-modules`**; Panels in Editoren; Discovery auf Fähigkeiten-Seite; `SkillTreeMultiSelect` mit Portal-Dropdown in Modals
- **Offen (Backlog):** Corpus-Caching bei großen Bibliotheken; Tests für Typ-Trennung; Filter-Persistenz; Skill-Filter im Dialog „Rahmen übernehmen“; API-Umbenennung `club_*` → Peer-Namen
---
## 3. Trainingsrahmenprogramm & PlanungsBlueprint (kurz)

View File

@ -4,6 +4,7 @@
import { stripHtmlToText } from '../utils/htmlUtils'
import { normalizeAdvanceMode, parseComboRepSeriesCountUi } from '../utils/combinationMethodProfileUi'
import { normalizeExerciseSkillIntensity } from '../constants/exerciseSkillIntensity'
import { request, API_URL, ACTIVE_CLUB_STORAGE_KEY } from './client.js'
/** Wie `mergeActiveClubHeader` in client.js — lokal, damit Raw-`fetch`-Pfade nicht von einem Namensimport abhängen. */
@ -89,8 +90,7 @@ export function buildExerciseApiPayload(formData, extras = {}) {
age_groups: [],
skills: (formData.skills || []).map((s) => ({
skill_id: s.skill_id,
is_primary: !!s.is_primary,
intensity: s.intensity || null,
intensity: normalizeExerciseSkillIntensity(s.intensity),
required_level: s.required_level || null,
target_level: s.target_level || null,
})),

View File

@ -39,14 +39,11 @@ import {
} from '../../utils/navReturnContext'
import { useBeforeUnloadWhen, useUnsavedChangesBlocker } from '../../hooks/useUnsavedChangesBlocker'
const INTENSITY_OPTIONS = [
{ value: '', label: '—' },
{ value: 'niedrig', label: 'niedrig' },
{ value: 'mittel', label: 'mittel' },
{ value: 'hoch', label: 'hoch' },
]
const VARIANT_DIFFICULTY = [
import {
EXERCISE_SKILL_INTENSITY_DEFAULT,
EXERCISE_SKILL_INTENSITY_OPTIONS,
normalizeExerciseSkillIntensity,
} from '../../constants/exerciseSkillIntensity' = [
{ value: '', label: '—' },
{ value: 'easier', label: 'Einfacher' },
{ value: 'same', label: 'Gleich' },
@ -391,8 +388,7 @@ function detailToForm(exercise) {
skills:
exercise.skills?.map((s) => ({
skill_id: s.skill_id,
is_primary: s.is_primary || false,
intensity: s.intensity || '',
intensity: normalizeExerciseSkillIntensity(s.intensity),
required_level: normalizeSkillLevelSlug(s.required_level),
target_level: normalizeSkillLevelSlug(s.target_level),
})) || [],
@ -820,8 +816,7 @@ function ExerciseFormPageRoot() {
...formData.skills,
{
skill_id: id,
is_primary: formData.skills.length === 0,
intensity: '',
intensity: EXERCISE_SKILL_INTENSITY_DEFAULT,
required_level: '',
target_level: '',
},
@ -829,10 +824,10 @@ function ExerciseFormPageRoot() {
setSkillPick('')
}
const setSkillPrimary = (idx) => {
const removeSkillRow = (idx) => {
updateFormField(
'skills',
formData.skills.map((s, i) => ({ ...s, is_primary: i === idx })),
formData.skills.filter((_, i) => i !== idx),
)
}
@ -843,12 +838,6 @@ function ExerciseFormPageRoot() {
)
}
const removeSkillRow = (idx) => {
const next = formData.skills.filter((_, i) => i !== idx)
if (next.length && !next.some((s) => s.is_primary)) next[0].is_primary = true
updateFormField('skills', next)
}
const performSaveAttempt = useCallback(
async ({ fromUnsavedDialog = false, closeAfter = false } = {}) => {
if (!formData.title || formData.title.trim().length < 3) {
@ -1880,26 +1869,20 @@ function ExerciseFormPageRoot() {
</span>
) : null}
</div>
<label style={{ fontSize: '12px', display: 'flex', alignItems: 'center', gap: '4px' }}>
<input
type="radio"
name="skill-primary"
checked={row.is_primary}
onChange={() => setSkillPrimary(idx)}
/>
primär
<label className="exercise-filter-skill-level-field">
<span className="exercise-filter-skill-level-caption">Intensität</span>
<select
className="form-input"
value={normalizeExerciseSkillIntensity(row.intensity)}
onChange={(e) => updateSkillField(idx, 'intensity', e.target.value)}
>
{EXERCISE_SKILL_INTENSITY_OPTIONS.map((o) => (
<option key={o.value} value={o.value}>
{o.label}
</option>
))}
</select>
</label>
<select
className="form-input"
value={row.intensity || ''}
onChange={(e) => updateSkillField(idx, 'intensity', e.target.value)}
>
{INTENSITY_OPTIONS.map((o) => (
<option key={o.value || 'i'} value={o.value}>
{o.label}
</option>
))}
</select>
<select
className="form-input"
value={row.required_level || ''}

View File

@ -0,0 +1,21 @@
/** Kanonische Intensität Übung ↔ Fähigkeit (Scoring + Formular). */
export const EXERCISE_SKILL_INTENSITY_DEFAULT = 'mittel'
export const EXERCISE_SKILL_INTENSITY_OPTIONS = [
{ value: 'niedrig', label: 'niedrig' },
{ value: 'mittel', label: 'mittel' },
{ value: 'hoch', label: 'hoch' },
]
export function normalizeExerciseSkillIntensity(value) {
const key = String(value ?? '').trim().toLowerCase()
if (key === 'niedrig' || key === 'low') return 'niedrig'
if (key === 'hoch' || key === 'high') return 'hoch'
return 'mittel'
}
export function formatExerciseSkillIntensityLabel(value) {
const n = normalizeExerciseSkillIntensity(value)
return EXERCISE_SKILL_INTENSITY_OPTIONS.find((o) => o.value === n)?.label || n
}

View File

@ -12,6 +12,7 @@ import ExerciseAttachmentMediaStrip from '../components/ExerciseAttachmentMediaS
import CombinationPlanBracket from '../components/CombinationPlanBracket'
import ExercisePeekModal from '../components/ExercisePeekModal'
import { formatSkillLevelSlug } from '../constants/skillLevels'
import { formatExerciseSkillIntensityLabel } from '../constants/exerciseSkillIntensity'
function TagRow({ exercise }) {
const tags = []
@ -244,9 +245,9 @@ function ExerciseDetailPage() {
const lvl =
rl || tl ? ` (${[rl, tl].filter(Boolean).join(' → ')})` : ''
return (
<span key={s.id} className={`exercise-tag${s.is_primary ? ' exercise-tag--accent' : ''}`}>
<span key={s.id} className="exercise-tag">
{s.skill_name}
{s.intensity ? ` · ${s.intensity}` : ''}
{` · ${formatExerciseSkillIntensityLabel(s.intensity)}`}
{lvl}
</span>
)