feat: Enhance sleep module with CSV import functionality and date parsing improvements
This commit is contained in:
parent
24f60c0a6d
commit
00437a92ab
|
|
@ -9,7 +9,8 @@ Endpoints:
|
||||||
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File
|
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from typing import Literal
|
from typing import Literal
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta, date
|
||||||
|
from decimal import Decimal, InvalidOperation
|
||||||
import csv
|
import csv
|
||||||
import io
|
import io
|
||||||
import json
|
import json
|
||||||
|
|
@ -19,6 +20,174 @@ from db import get_db, get_cursor
|
||||||
|
|
||||||
router = APIRouter(prefix="/api/sleep", tags=["sleep"])
|
router = APIRouter(prefix="/api/sleep", tags=["sleep"])
|
||||||
|
|
||||||
|
|
||||||
|
def _strip_row_keys(row: dict) -> dict:
|
||||||
|
return {(k or "").strip(): (v.strip() if isinstance(v, str) else v) for k, v in row.items()}
|
||||||
|
|
||||||
|
|
||||||
|
def _safe_float(value) -> float | None:
|
||||||
|
if value is None or value == "":
|
||||||
|
return None
|
||||||
|
if isinstance(value, (int, float)):
|
||||||
|
return float(value)
|
||||||
|
if isinstance(value, Decimal):
|
||||||
|
return float(value)
|
||||||
|
try:
|
||||||
|
s = str(value).strip().replace(",", ".")
|
||||||
|
return float(s)
|
||||||
|
except (ValueError, InvalidOperation):
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_apple_sleep_datetime(value: str) -> datetime:
|
||||||
|
raw = (value or "").strip()
|
||||||
|
if not raw:
|
||||||
|
raise ValueError("empty datetime")
|
||||||
|
fmts = (
|
||||||
|
"%Y-%m-%d %H:%M:%S %z",
|
||||||
|
"%d.%m.%y %H:%M:%S",
|
||||||
|
"%d.%m.%Y %H:%M:%S",
|
||||||
|
"%Y-%m-%d %H:%M:%S",
|
||||||
|
)
|
||||||
|
last_err = None
|
||||||
|
for fmt in fmts:
|
||||||
|
try:
|
||||||
|
return datetime.strptime(raw, fmt)
|
||||||
|
except ValueError as e:
|
||||||
|
last_err = e
|
||||||
|
raise ValueError(f"Unbekanntes Datumsformat: {raw!r}") from last_err
|
||||||
|
|
||||||
|
|
||||||
|
def _hr_to_minutes(hours: float | None) -> int:
|
||||||
|
if hours is None:
|
||||||
|
return 0
|
||||||
|
return int(round(float(hours) * 60))
|
||||||
|
|
||||||
|
|
||||||
|
def _detect_apple_sleep_csv_format(fieldnames: list[str] | None) -> Literal["segments", "summary"]:
|
||||||
|
if not fieldnames:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail="CSV enthält keine Spaltenüberschriften.",
|
||||||
|
)
|
||||||
|
fn = {(f or "").strip() for f in fieldnames}
|
||||||
|
if {"Start", "End", "Duration (hr)", "Value"}.issubset(fn):
|
||||||
|
return "segments"
|
||||||
|
if "Start" in fn and "End" in fn and "Total Sleep (hr)" in fn:
|
||||||
|
return "summary"
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail=(
|
||||||
|
"Unbekanntes Apple-Health-Schlaf-CSV. "
|
||||||
|
"Erwartet wird entweder der Segment-Export (Spalten Start, End, Duration (hr), Value) "
|
||||||
|
"oder die Schlafanalyse mit Nacht-Zusammenfassung (u. a. Total Sleep (hr), Core/Light, Deep, REM)."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _build_nights_from_apple_summary(reader: csv.DictReader) -> dict[date, dict]:
|
||||||
|
nights_dict: dict[date, dict] = {}
|
||||||
|
for raw in reader:
|
||||||
|
row = _strip_row_keys(raw)
|
||||||
|
start_s = row.get("Start") or ""
|
||||||
|
end_s = row.get("End") or ""
|
||||||
|
if not start_s or not end_s:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
start_dt = _parse_apple_sleep_datetime(start_s)
|
||||||
|
end_dt = _parse_apple_sleep_datetime(end_s)
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
|
||||||
|
dt_key = (row.get("Date/Time") or row.get("Datum/Uhrzeit") or "").strip()
|
||||||
|
if dt_key:
|
||||||
|
try:
|
||||||
|
wake_d = datetime.strptime(dt_key[:10], "%Y-%m-%d").date()
|
||||||
|
except ValueError:
|
||||||
|
wake_d = end_dt.date()
|
||||||
|
else:
|
||||||
|
wake_d = end_dt.date()
|
||||||
|
|
||||||
|
core_hr = _safe_float(row.get("Core (hr)"))
|
||||||
|
if core_hr is None:
|
||||||
|
core_hr = _safe_float(row.get("Light (hr)")) or 0.0
|
||||||
|
deep_min = _hr_to_minutes(_safe_float(row.get("Deep (hr)")))
|
||||||
|
rem_min = _hr_to_minutes(_safe_float(row.get("REM (hr)")))
|
||||||
|
light_min = _hr_to_minutes(core_hr)
|
||||||
|
awake_min = _hr_to_minutes(_safe_float(row.get("Awake (hr)")))
|
||||||
|
nights_dict[wake_d] = {
|
||||||
|
"bedtime": start_dt,
|
||||||
|
"wake_time": end_dt,
|
||||||
|
"segments": [],
|
||||||
|
"deep_minutes": deep_min,
|
||||||
|
"rem_minutes": rem_min,
|
||||||
|
"light_minutes": light_min,
|
||||||
|
"awake_minutes": awake_min,
|
||||||
|
}
|
||||||
|
return nights_dict
|
||||||
|
|
||||||
|
|
||||||
|
def _build_nights_from_apple_segments(reader: csv.DictReader, phase_map: dict) -> dict[date, dict]:
|
||||||
|
segments = []
|
||||||
|
for raw in reader:
|
||||||
|
row = _strip_row_keys(raw)
|
||||||
|
phase_key = (row.get("Value") or "").strip()
|
||||||
|
phase_en = phase_map.get(phase_key)
|
||||||
|
if phase_en is None:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
start_dt = _parse_apple_sleep_datetime(row.get("Start") or "")
|
||||||
|
end_dt = _parse_apple_sleep_datetime(row.get("End") or "")
|
||||||
|
duration_hr = _safe_float(row.get("Duration (hr)"))
|
||||||
|
if duration_hr is None:
|
||||||
|
continue
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
continue
|
||||||
|
duration_min = int(duration_hr * 60)
|
||||||
|
segments.append({
|
||||||
|
"start": start_dt,
|
||||||
|
"end": end_dt,
|
||||||
|
"duration_min": duration_min,
|
||||||
|
"phase": phase_en,
|
||||||
|
})
|
||||||
|
|
||||||
|
segments.sort(key=lambda s: s["start"])
|
||||||
|
nights = []
|
||||||
|
current_night = None
|
||||||
|
|
||||||
|
for seg in segments:
|
||||||
|
if current_night is None or (seg["start"] - current_night["wake_time"]).total_seconds() > 7200:
|
||||||
|
current_night = {
|
||||||
|
"bedtime": seg["start"],
|
||||||
|
"wake_time": seg["end"],
|
||||||
|
"segments": [],
|
||||||
|
"deep_minutes": 0,
|
||||||
|
"rem_minutes": 0,
|
||||||
|
"light_minutes": 0,
|
||||||
|
"awake_minutes": 0,
|
||||||
|
}
|
||||||
|
nights.append(current_night)
|
||||||
|
|
||||||
|
current_night["segments"].append(seg)
|
||||||
|
current_night["wake_time"] = max(current_night["wake_time"], seg["end"])
|
||||||
|
current_night["bedtime"] = min(current_night["bedtime"], seg["start"])
|
||||||
|
|
||||||
|
if seg["phase"] == "deep":
|
||||||
|
current_night["deep_minutes"] += seg["duration_min"]
|
||||||
|
elif seg["phase"] == "rem":
|
||||||
|
current_night["rem_minutes"] += seg["duration_min"]
|
||||||
|
elif seg["phase"] == "light":
|
||||||
|
current_night["light_minutes"] += seg["duration_min"]
|
||||||
|
elif seg["phase"] == "awake":
|
||||||
|
current_night["awake_minutes"] += seg["duration_min"]
|
||||||
|
|
||||||
|
nights_dict: dict[date, dict] = {}
|
||||||
|
for night in nights:
|
||||||
|
wake_date = night["wake_time"].date()
|
||||||
|
nights_dict[wake_date] = night
|
||||||
|
return nights_dict
|
||||||
|
|
||||||
|
|
||||||
# ── Models ────────────────────────────────────────────────────────────────────
|
# ── Models ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
class SleepCreate(BaseModel):
|
class SleepCreate(BaseModel):
|
||||||
|
|
@ -460,96 +629,46 @@ async def import_apple_health_sleep(
|
||||||
"""
|
"""
|
||||||
Import sleep data from Apple Health CSV export.
|
Import sleep data from Apple Health CSV export.
|
||||||
|
|
||||||
Expected CSV format:
|
Supports:
|
||||||
Start,End,Duration (hr),Value,Source
|
- Segment-Export: Start, End, Duration (hr), Value (Kern/Tief/REM/Wach oder Core/Deep/…)
|
||||||
2026-03-14 22:44:23,2026-03-14 23:00:19,0.266,Kern,Apple Watch
|
- Schlafanalyse (Nacht-Zusammenfassung): Date/Time, Start, End, Total Sleep (hr),
|
||||||
|
Core/Light (hr), Deep (hr), REM (hr), Awake (hr), …
|
||||||
|
|
||||||
- Aggregates segments by night (wake date)
|
- Aggregates segments by night (wake date) where applicable
|
||||||
- Maps German phase names: Kern→light, REM→rem, Tief→deep, Wach→awake
|
- Stores Roh-Segmente in JSONB (bei Zusammenfassung oft leer)
|
||||||
- Stores raw segments in JSONB
|
- Überschreibt keine manuellen Einträge (source='manual')
|
||||||
- Does NOT overwrite manual entries (source='manual')
|
|
||||||
"""
|
"""
|
||||||
pid = session['profile_id']
|
pid = session['profile_id']
|
||||||
|
|
||||||
# Read CSV
|
|
||||||
content = await file.read()
|
content = await file.read()
|
||||||
csv_text = content.decode('utf-8-sig') # Handle BOM
|
csv_text = content.decode('utf-8-sig')
|
||||||
reader = csv.DictReader(io.StringIO(csv_text))
|
reader = csv.DictReader(io.StringIO(csv_text))
|
||||||
|
fmt = _detect_apple_sleep_csv_format(reader.fieldnames)
|
||||||
|
|
||||||
# Phase mapping (German → English)
|
|
||||||
phase_map = {
|
phase_map = {
|
||||||
'Kern': 'light',
|
"Kern": "light",
|
||||||
'REM': 'rem',
|
"Core": "light",
|
||||||
'Tief': 'deep',
|
"Light": "light",
|
||||||
'Wach': 'awake',
|
"REM": "rem",
|
||||||
'Schlafend': None # Ignore initial sleep entry
|
"Tief": "deep",
|
||||||
|
"Deep": "deep",
|
||||||
|
"Wach": "awake",
|
||||||
|
"Awake": "awake",
|
||||||
|
"Schlafend": None,
|
||||||
|
"Asleep": None,
|
||||||
|
"In Bed": None,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Parse segments
|
if fmt == "summary":
|
||||||
segments = []
|
nights_dict = _build_nights_from_apple_summary(reader)
|
||||||
for row in reader:
|
else:
|
||||||
phase_de = row['Value'].strip()
|
nights_dict = _build_nights_from_apple_segments(reader, phase_map)
|
||||||
phase_en = phase_map.get(phase_de)
|
|
||||||
|
|
||||||
if phase_en is None: # Skip "Schlafend"
|
if not nights_dict:
|
||||||
continue
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
start_dt = datetime.strptime(row['Start'], '%Y-%m-%d %H:%M:%S')
|
detail="Keine importierbaren Schlafzeilen gefunden (prüfe Start/Ende und Format).",
|
||||||
end_dt = datetime.strptime(row['End'], '%Y-%m-%d %H:%M:%S')
|
)
|
||||||
duration_hr = float(row['Duration (hr)'])
|
|
||||||
duration_min = int(duration_hr * 60)
|
|
||||||
|
|
||||||
segments.append({
|
|
||||||
'start': start_dt,
|
|
||||||
'end': end_dt,
|
|
||||||
'duration_min': duration_min,
|
|
||||||
'phase': phase_en
|
|
||||||
})
|
|
||||||
|
|
||||||
# Sort segments chronologically
|
|
||||||
segments.sort(key=lambda s: s['start'])
|
|
||||||
|
|
||||||
# Group segments into nights (gap-based)
|
|
||||||
# If gap between segments > 2 hours → new night
|
|
||||||
nights = []
|
|
||||||
current_night = None
|
|
||||||
|
|
||||||
for seg in segments:
|
|
||||||
# Start new night if:
|
|
||||||
# 1. First segment
|
|
||||||
# 2. Gap > 2 hours since last segment
|
|
||||||
if current_night is None or (seg['start'] - current_night['wake_time']).total_seconds() > 7200:
|
|
||||||
current_night = {
|
|
||||||
'bedtime': seg['start'],
|
|
||||||
'wake_time': seg['end'],
|
|
||||||
'segments': [],
|
|
||||||
'deep_minutes': 0,
|
|
||||||
'rem_minutes': 0,
|
|
||||||
'light_minutes': 0,
|
|
||||||
'awake_minutes': 0
|
|
||||||
}
|
|
||||||
nights.append(current_night)
|
|
||||||
|
|
||||||
# Add segment to current night
|
|
||||||
current_night['segments'].append(seg)
|
|
||||||
current_night['wake_time'] = max(current_night['wake_time'], seg['end'])
|
|
||||||
current_night['bedtime'] = min(current_night['bedtime'], seg['start'])
|
|
||||||
|
|
||||||
# Sum phases
|
|
||||||
if seg['phase'] == 'deep':
|
|
||||||
current_night['deep_minutes'] += seg['duration_min']
|
|
||||||
elif seg['phase'] == 'rem':
|
|
||||||
current_night['rem_minutes'] += seg['duration_min']
|
|
||||||
elif seg['phase'] == 'light':
|
|
||||||
current_night['light_minutes'] += seg['duration_min']
|
|
||||||
elif seg['phase'] == 'awake':
|
|
||||||
current_night['awake_minutes'] += seg['duration_min']
|
|
||||||
|
|
||||||
# Convert nights list to dict with wake_date as key
|
|
||||||
nights_dict = {}
|
|
||||||
for night in nights:
|
|
||||||
wake_date = night['wake_time'].date() # Date when you woke up
|
|
||||||
nights_dict[wake_date] = night
|
|
||||||
|
|
||||||
# Insert nights
|
# Insert nights
|
||||||
imported = 0
|
imported = 0
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,202 @@
|
||||||
|
# Umsetzungsplan: Sportspezifisch auswertbare Trainingsprofile
|
||||||
|
|
||||||
|
**Status:** Konzept für fachliche Abstimmung / Konzeptagent
|
||||||
|
**Datum:** 2026-04-05
|
||||||
|
**Bezug:** Fachliches Teilkonzept „Sportspezifisch auswertbare Trainingsprofile“ (User-Entwurf)
|
||||||
|
**Hinweis:** Keine Implementierung ohne Freigabe der unter [Offene Fragen](#offene-fragen-für-den-fachlichen-konzeptagenten) gekläerten Punkte.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Kurzfazit: Machbarkeit
|
||||||
|
|
||||||
|
**Fachlich und technisch grundsätzlich machbar** als gestaffelte Erweiterung auf dem bestehenden System:
|
||||||
|
|
||||||
|
- `training_types.profile` (JSONB)
|
||||||
|
- `TrainingProfileEvaluator` (`backend/profile_evaluator.py`)
|
||||||
|
- Persistenz: `activity_log.evaluation`, `quality_label`, `overall_score`
|
||||||
|
|
||||||
|
**Engpass:** Viele im Teilkonzept genannten Kriterien (z. B. Muskelgruppen, Nähe zum muskulären Versagen, detaillierte Zonenzeiten) **lassen sich aus der aktuellen `activity_log`-Datenbasis nicht zuverlässig ableiten**. Dafür braucht es zusätzliche Erfassung, Importfelder oder explizite „niedrige Evidenz / unbekannt“-Markierung in Metriken und KI-Kontext (Placeholder-Registry, Erklärbarkeit).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Ist-Analyse (Abhängigkeiten)
|
||||||
|
|
||||||
|
### 2.1 Daten & Konfiguration
|
||||||
|
|
||||||
|
| Baustein | Rolle |
|
||||||
|
|---------|--------|
|
||||||
|
| `training_types` | `category`, `subcategory`, `name_*`, optional `abilities` (JSONB), `profile` (JSONB) |
|
||||||
|
| `activity_type_mappings` | Import/manuelles Mapping `activity_type` → `training_type_id` (unverändert lassen) |
|
||||||
|
| `training_parameters` | Registry messbarer Parameter für Regeln (`source_field` → `activity_log`) |
|
||||||
|
| `activity_log` | u. a. `training_type_id`, `evaluation`, `quality_label`, `overall_score` |
|
||||||
|
|
||||||
|
### 2.2 Bewertung heute
|
||||||
|
|
||||||
|
- Der Evaluator läuft über mehrere **rule_sets** (Minimumanforderungen, Zonen, Effekte, Periodisierung, KPI, Safety).
|
||||||
|
- **Ein** zusammengefasster `overall_score` und **ein** `quality_label` (`excellent` | `good` | `acceptable` | `poor`).
|
||||||
|
- **Relevanz, intrinsische Qualität, Zielbezug, Platzierung** sind im Ergebnis **nicht formal getrennt**; Periodisierung/Performance sind teils **MVP/vereinfacht**.
|
||||||
|
|
||||||
|
### 2.3 Downstream (nicht brechen)
|
||||||
|
|
||||||
|
- **Issue #31 / Quality Filter:** `profiles.quality_filter_level`, `quality_filter.py` (SQL-Fragmente nach `quality_label`)
|
||||||
|
- **Frontend:** Badges in Aktivitäts-UI, History-Hinweise
|
||||||
|
- **Data Layer / Charts:** u. a. `calculate_quality_sessions_pct`, `activity_score`, Korrelationen
|
||||||
|
- **Placeholder Registry:** u. a. `quality_sessions_pct` (bekannte Semantik-/Label-Diskrepanz, siehe unten)
|
||||||
|
|
||||||
|
### 2.4 Bekannte technische Inkonsistenz (Bestand)
|
||||||
|
|
||||||
|
`calculate_quality_sessions_pct` filtert u. a. auf `quality_label IN (..., 'very_good', ...)`, der `TrainingProfileEvaluator` setzt **`very_good` nicht**. Das sollte **vor oder zusammen mit** größeren Profil-Erweiterungen bereinigt werden, damit neue Dimensionen nicht auf wackeliger Basis validiert werden.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Umsetzungsprinzipien (Kompatibilität)
|
||||||
|
|
||||||
|
1. **Kein Big-Bang:** Consumer erwarten weiterhin sinnvolle `quality_label` / `overall_score`, es sei denn, es gibt eine **versionierte Cutover-Strategie**.
|
||||||
|
2. **Additive JSON-Struktur** in `evaluation` (z. B. mehrdimensionale Teilergebnisse), ohne bestehende Schlüssel willkürlich zu entfernen.
|
||||||
|
3. **Legacy-Vertrag festlegen:** z. B. `overall_score` = **primär intrinsische** Qualität (oder explizit „Legacy-Komposit“ mit `schema_version`).
|
||||||
|
4. **`training_types` bleibt Single Source** für Typ + Profil; Mapping-Import-Logik nicht „mitziehen“, außer es ist fachlich nötig.
|
||||||
|
5. **Neue Coach-Metriken:** über Placeholder Registry, mit Evidence-Tags; keine doppelte Wahrheit neben der Registry.
|
||||||
|
6. **KI:** `ai_context` / `interpretation_boundary` im Profil ausbaubar machen (was erlaubt/verboten ist, bei Datenlücken).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Phasenplan
|
||||||
|
|
||||||
|
### Phase 0 – Begriffe, Verträge, Bestandsaufnahme
|
||||||
|
|
||||||
|
- Glossar: Relevanz, intrinsische Qualität, Zielbeitrag, Platzierung, Belastung, sportspezifische Qualität → **Mapping auf bestehende `rule_sets`**.
|
||||||
|
- Vollständige Liste: wer liest `quality_label` / `evaluation` / `overall_score` (APIs, SQL, Frontend).
|
||||||
|
- Align: `very_good` vs. Evaluator-Labels; optional: Rolle von `acceptable` in „Qualitätssessions“ vs. globalem Filter **fachlich** klären.
|
||||||
|
|
||||||
|
### Phase 1 – Metamodell im Profil-JSON
|
||||||
|
|
||||||
|
- Erweiterung `training_types.profile`: z. B. `sport_logic`, `dimensions_config`, `goal_linkage`, `load_character`, `recovery_character`, `interference`, `ai_interpretation`.
|
||||||
|
- Evaluator liefert **Teilscores + Begründungen** pro Dimension; **Legacy-Felder** ableitbar und dokumentiert.
|
||||||
|
|
||||||
|
### Phase 2 – Zielbezug
|
||||||
|
|
||||||
|
- Input: aktive Ziele, `goal_mode`, Focus-Gewichte (`user_focus_area_weights`).
|
||||||
|
- Ausgabe: eigene Dimension **`goal_fit`**, ohne intrinsische Bewertung zu überschreiben.
|
||||||
|
|
||||||
|
### Phase 3 – Wochenkontext / Platzierung / Interferenz
|
||||||
|
|
||||||
|
- Kontext in `load_evaluation_context` erweitern (Reihenfolge, Vortag, Ruhetage, optional Vitals/Schlaf).
|
||||||
|
- Regeln schrittweise; False-Positive-Risiko beachten.
|
||||||
|
|
||||||
|
### Phase 4 – Sportspezifische Templates
|
||||||
|
|
||||||
|
- Profilvorlagen pro `sport_logic` (analog `profile_templates.py`), unterschiedliche Parameter und Schwellen.
|
||||||
|
- `confidence` / `data_sufficiency` pro Dimension.
|
||||||
|
|
||||||
|
### Phase 5 – Coach-Metriken & Placeholder
|
||||||
|
|
||||||
|
- Neue Platzhalter nur bei klarer Semantik; ggf. `quality_sessions_pct` **umschreiben** oder durch neuen Key ersetzen + Deprecation-Plan.
|
||||||
|
|
||||||
|
### Phase 6 – Frontend / Admin
|
||||||
|
|
||||||
|
- Dimensionen sichtbar machen; Badges können vorerst Legacy-Score anzeigen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Risiken (Kurz)
|
||||||
|
|
||||||
|
| Risiko | Maßnahme |
|
||||||
|
|--------|----------|
|
||||||
|
| Bedeutung von `quality_label` wechselt still | `evaluation.schema_version`, dokumentierter Vertrag, ggf. separates `intrinsic_quality_label` nach Cutover |
|
||||||
|
| Charts/Scores springen | Shadow-Vergleich, gestuftes Rollout |
|
||||||
|
| KI übertreibt bei Datenlücken | `ai_interpretation` + Metadata-Evidence |
|
||||||
|
| Registry-Drift | Alle neuen Kennzahlen registrieren |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Offene Fragen für den fachlichen Konzeptagenten
|
||||||
|
|
||||||
|
Die folgenden Blöcke sind bewusst als Markdown-Codeblöcke formatiert, damit sie **unverändert** in Prompts oder Tickets übernommen werden können.
|
||||||
|
|
||||||
|
### Block A – Priorität & Konflikte zwischen Dimensionen
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
Wenn sich Dimensionen widersprechen (z. B. intrinsische Qualität „hoch“, Zielpassung „niedrig“, Platzierung „problematisch“):
|
||||||
|
|
||||||
|
1. Welche Dimension soll für die **primäre Nutzer-/Coach-Aussage** in der UI führend sein?
|
||||||
|
2. Gibt es eine **feste Prioritätsreihenfolge** (z. B. Sicherheit > Platzierung > Zielpassung > intrinsische Qualität) oder ist sie kontextabhängig?
|
||||||
|
3. Soll es eine **explizite „Gesamt-Coach-Empfehlung“** geben, die aus den Dimensionen kombiniert wird, oder nur parallele Teilurteile ohne Gesamtnote?
|
||||||
|
```
|
||||||
|
|
||||||
|
### Block B – Minimaldatensatz & Sportlogiken
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
Pro Trainingsfamilie / „sport_logic“ (Kraft, Cardio, Schnellkraft, Kampfsport, Mobility, Erholung aktiv, Geist & Meditation):
|
||||||
|
|
||||||
|
1. Welche **messbaren Felder** sind minimal nötig, damit eine sportspezifische Bewertung **fachlich vertretbar** ist (nicht nur „nice to have“)?
|
||||||
|
2. Welche Aussagen dürfen **ohne** diese Felder **nicht** getroffen werden (nur „unbekannt“ / Confidence niedrig)?
|
||||||
|
3. Sollen fehlende Daten die **intrinsische** Bewertung hart begrenzen (Cap), oder nur **Zusatzdimensionen** betreffen?
|
||||||
|
```
|
||||||
|
|
||||||
|
### Block C – Einzeltraining vs. Wochenkontext
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
Platzierung / Kontextqualität:
|
||||||
|
|
||||||
|
1. Darf schlechte Platzierung die **sichtbare Einheit-Qualität** numerisch senken, oder nur **Flags/Hinweise** erzeugen?
|
||||||
|
2. Welche zeitliche Auflösung ist Pflicht: nur **Datum** oder auch **start_time** / Reihenfolge am Tag?
|
||||||
|
3. Wie gehen wir mit **unvollständiger Historie** um (Importlücken, manuell gelöschte Einträge)?
|
||||||
|
```
|
||||||
|
|
||||||
|
### Block D – Ziele & Multi-Ziel
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
Zielbezug (goal_fit):
|
||||||
|
|
||||||
|
1. Bewertung strikt gegen **ein Primärziel**, oder gewichtet gegen **alle aktiven Ziele**?
|
||||||
|
2. Wie werden **Focus Areas** vs. **konkrete Goals** priorisiert, wenn sie sich widersprechen?
|
||||||
|
3. Soll „behindert das Ziel“ (Teilkonzept) ein **eigener Score** oder nur eine **kategorische** Aussage sein?
|
||||||
|
```
|
||||||
|
|
||||||
|
### Block E – KI & Haftung / Grenzen
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
KI-Interpretationsrahmen (pro Profil oder global):
|
||||||
|
|
||||||
|
1. Welche Formulierungen sind **verboten** (medizinische Diagnosen, absolute Leistungsversprechen, …)?
|
||||||
|
2. Muss jede KI-gestützte Aussage einen **Evidenz-Hinweis** tragen („basierend auf Dauer/HF“, „Muskelgruppen nicht erfasst“)?
|
||||||
|
3. Soll der Nutzer **opt-in** für interpretative Texte jenseits der Regeln haben?
|
||||||
|
```
|
||||||
|
|
||||||
|
### Block F – Legacy-Metriken & Filter
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
Bestehende Kennzahlen:
|
||||||
|
|
||||||
|
1. Soll `quality_label` / `overall_score` dauerhaft **nur intrinsisch** bedeuten, oder ein **Komposit** aus mehreren Dimensionen?
|
||||||
|
2. Wie sollen `quality_sessions_pct` und der globale **Quality Filter** künftig mit mehrdimensionalen Urteilen umgehen (nur intrinsisch filtern vs. Kombination)?
|
||||||
|
3. Ist `acceptable` fachlich eine „Qualitätssession“ oder nur „formal ok“?
|
||||||
|
```
|
||||||
|
|
||||||
|
### Block G – Abnahme / Akzeptanz
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
Abnahme:
|
||||||
|
|
||||||
|
1. Welche **messbaren** Abnahmekriterien gelten pro Phase (z. B. % konsistent bewerteter Sessions, keine Regression bei Filter-Charts)?
|
||||||
|
2. Wer **fachlich** sign-off: Rolle „Domain Owner“ vs. Entwicklung?
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Anhang: Referenz im Repository
|
||||||
|
|
||||||
|
| Thema | Pfad (orientierend) |
|
||||||
|
|-------|---------------------|
|
||||||
|
| Evaluator | `backend/profile_evaluator.py` |
|
||||||
|
| Regeln | `backend/rule_engine.py` |
|
||||||
|
| Auswertung + Kontext | `backend/evaluation_helper.py` |
|
||||||
|
| Profile JSON / Migration | `backend/migrations/014_training_profiles.sql`, Templates `backend/profile_templates.py` |
|
||||||
|
| Quality Filter | `backend/quality_filter.py` |
|
||||||
|
| Aktivitäts-API inkl. Eval | `backend/routers/activity.py` |
|
||||||
|
| Quality-Sessions-Metrik | `backend/data_layer/activity_metrics.py` (`calculate_quality_sessions_pct`) |
|
||||||
|
| Placeholder Registry | `backend/placeholder_registrations/activity_metrics.py` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Ende des Dokuments.*
|
||||||
Loading…
Reference in New Issue
Block a user