2026-04-20 08:11:23 +02:00
|
|
|
|
"""
|
|
|
|
|
|
KPIs und Kurz-Aussagen für Recovery-Dashboard (Layer 2b).
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
|
|
from typing import Any, Dict, List, Optional
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _verdict(status: str) -> str:
|
|
|
|
|
|
if status == "good":
|
|
|
|
|
|
return "Gut"
|
|
|
|
|
|
if status == "warn":
|
|
|
|
|
|
return "Hinweis"
|
|
|
|
|
|
return "Achtung"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _recovery_score_status(score: Optional[int]) -> str:
|
|
|
|
|
|
if score is None:
|
|
|
|
|
|
return "warn"
|
|
|
|
|
|
if score >= 70:
|
|
|
|
|
|
return "good"
|
|
|
|
|
|
if score >= 45:
|
|
|
|
|
|
return "warn"
|
|
|
|
|
|
return "bad"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _debt_status(hours: Optional[float]) -> str:
|
|
|
|
|
|
if hours is None:
|
|
|
|
|
|
return "warn"
|
|
|
|
|
|
if hours <= 2:
|
|
|
|
|
|
return "good"
|
|
|
|
|
|
if hours <= 8:
|
|
|
|
|
|
return "warn"
|
|
|
|
|
|
return "bad"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def build_recovery_dashboard_kpi_tiles(
|
|
|
|
|
|
recovery_score: Optional[int],
|
|
|
|
|
|
sleep_debt_hours: Optional[float],
|
|
|
|
|
|
avg_sleep_hours: Optional[float],
|
|
|
|
|
|
hrv_vs_baseline_pct: Optional[float],
|
|
|
|
|
|
rhr_vs_baseline_pct: Optional[float],
|
2026-04-20 10:29:43 +02:00
|
|
|
|
merge_heart_autonomic_tiles: bool = True,
|
2026-04-20 08:11:23 +02:00
|
|
|
|
) -> List[Dict[str, Any]]:
|
|
|
|
|
|
tiles: List[Dict[str, Any]] = []
|
|
|
|
|
|
|
|
|
|
|
|
rs = _recovery_score_status(recovery_score)
|
|
|
|
|
|
tiles.append(
|
|
|
|
|
|
{
|
|
|
|
|
|
"key": "recovery_score",
|
|
|
|
|
|
"category": "Recovery-Score",
|
|
|
|
|
|
"icon": "💚",
|
|
|
|
|
|
"value": str(recovery_score) if recovery_score is not None else "—",
|
|
|
|
|
|
"sublabel": "Modell aus Schlaf + Vitaldaten",
|
|
|
|
|
|
"status": rs,
|
|
|
|
|
|
"verdict": _verdict(rs),
|
|
|
|
|
|
"hoverTop": "Gesamt-Recovery-Score (0–100)",
|
|
|
|
|
|
"hoverBody": "calculate_recovery_score_v2 — gleiche Quelle wie Platzhalter.",
|
|
|
|
|
|
"keys": ["recovery_score"],
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
ds = _debt_status(sleep_debt_hours)
|
|
|
|
|
|
tiles.append(
|
|
|
|
|
|
{
|
|
|
|
|
|
"key": "sleep_debt",
|
|
|
|
|
|
"category": "Schlafschuld",
|
|
|
|
|
|
"icon": "⏳",
|
|
|
|
|
|
"value": f"{sleep_debt_hours:.1f} h".replace(".", ",")
|
|
|
|
|
|
if sleep_debt_hours is not None
|
|
|
|
|
|
else "—",
|
|
|
|
|
|
"sublabel": "Kumuliert (Ziel 8 h/Nacht)",
|
|
|
|
|
|
"status": ds,
|
|
|
|
|
|
"verdict": _verdict(ds),
|
|
|
|
|
|
"hoverTop": "Geschätzte Schlafschuld",
|
|
|
|
|
|
"hoverBody": "calculate_sleep_debt_hours",
|
|
|
|
|
|
"keys": ["sleep_debt_hours"],
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
tiles.append(
|
|
|
|
|
|
{
|
|
|
|
|
|
"key": "avg_sleep",
|
|
|
|
|
|
"category": "Ø Schlafdauer",
|
|
|
|
|
|
"icon": "🌙",
|
|
|
|
|
|
"value": f"{avg_sleep_hours:.1f} h".replace(".", ",") if avg_sleep_hours is not None else "—",
|
|
|
|
|
|
"sublabel": "Im gewählten Fenster",
|
|
|
|
|
|
"status": "good" if avg_sleep_hours and avg_sleep_hours >= 7 else "warn",
|
|
|
|
|
|
"verdict": "Gut" if avg_sleep_hours and avg_sleep_hours >= 7 else "Hinweis",
|
|
|
|
|
|
"hoverTop": "Durchschnittliche Schlafdauer",
|
|
|
|
|
|
"hoverBody": "get_sleep_duration_data",
|
|
|
|
|
|
"keys": ["sleep_duration_avg"],
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-04-20 10:29:43 +02:00
|
|
|
|
if merge_heart_autonomic_tiles and (
|
|
|
|
|
|
hrv_vs_baseline_pct is not None or rhr_vs_baseline_pct is not None
|
|
|
|
|
|
):
|
|
|
|
|
|
h_s = (
|
|
|
|
|
|
"good"
|
|
|
|
|
|
if hrv_vs_baseline_pct is not None and hrv_vs_baseline_pct >= 0
|
|
|
|
|
|
else "warn"
|
2026-04-20 08:11:23 +02:00
|
|
|
|
if hrv_vs_baseline_pct is not None
|
2026-04-20 10:29:43 +02:00
|
|
|
|
else "warn"
|
|
|
|
|
|
)
|
|
|
|
|
|
parts: List[str] = []
|
|
|
|
|
|
if hrv_vs_baseline_pct is not None:
|
|
|
|
|
|
parts.append(f"HRV {hrv_vs_baseline_pct:+.1f} %".replace(".", ","))
|
|
|
|
|
|
if rhr_vs_baseline_pct is not None:
|
|
|
|
|
|
parts.append(f"RHR {rhr_vs_baseline_pct:+.1f} %".replace(".", ","))
|
|
|
|
|
|
tiles.append(
|
|
|
|
|
|
{
|
|
|
|
|
|
"key": "herz_autonom",
|
|
|
|
|
|
"category": "Herz & autonomes System",
|
|
|
|
|
|
"icon": "❤️🩹",
|
|
|
|
|
|
"value": " · ".join(parts) if parts else "—",
|
|
|
|
|
|
"sublabel": "HRV/Ruhepuls vs. Referenz (3-Tage-Mittel vs. ältere Basis)",
|
|
|
|
|
|
"status": h_s,
|
|
|
|
|
|
"verdict": _verdict(h_s),
|
|
|
|
|
|
"hoverTop": "HRV und Ruhepuls relativ zur persönlichen Basis",
|
|
|
|
|
|
"hoverBody": "calculate_hrv_vs_baseline_pct · calculate_rhr_vs_baseline_pct",
|
|
|
|
|
|
"keys": ["hrv_vs_baseline", "rhr_vs_baseline"],
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
else:
|
|
|
|
|
|
h_s = (
|
|
|
|
|
|
"good"
|
|
|
|
|
|
if hrv_vs_baseline_pct is not None and hrv_vs_baseline_pct >= 0
|
|
|
|
|
|
else "warn"
|
|
|
|
|
|
if hrv_vs_baseline_pct is not None
|
|
|
|
|
|
else "warn"
|
|
|
|
|
|
)
|
|
|
|
|
|
tiles.append(
|
|
|
|
|
|
{
|
|
|
|
|
|
"key": "hrv_baseline",
|
|
|
|
|
|
"category": "HRV vs. Basis",
|
|
|
|
|
|
"icon": "〰️",
|
|
|
|
|
|
"value": f"{hrv_vs_baseline_pct:+.1f} %".replace(".", ",")
|
|
|
|
|
|
if hrv_vs_baseline_pct is not None
|
|
|
|
|
|
else "—",
|
|
|
|
|
|
"sublabel": "Letzte 3 Tage vs. ältere Basis",
|
|
|
|
|
|
"status": h_s,
|
|
|
|
|
|
"verdict": _verdict(h_s),
|
|
|
|
|
|
"hoverTop": "Abweichung HRV vom Referenzmittel",
|
|
|
|
|
|
"hoverBody": "calculate_hrv_vs_baseline_pct",
|
|
|
|
|
|
"keys": ["hrv_vs_baseline"],
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
2026-04-20 08:11:23 +02:00
|
|
|
|
|
2026-04-20 10:29:43 +02:00
|
|
|
|
tiles.append(
|
|
|
|
|
|
{
|
|
|
|
|
|
"key": "rhr_baseline",
|
|
|
|
|
|
"category": "Ruhepuls vs. Basis",
|
|
|
|
|
|
"icon": "❤️",
|
|
|
|
|
|
"value": f"{rhr_vs_baseline_pct:+.1f} %".replace(".", ",")
|
|
|
|
|
|
if rhr_vs_baseline_pct is not None
|
|
|
|
|
|
else "—",
|
|
|
|
|
|
"sublabel": "Niedriger oft günstiger",
|
|
|
|
|
|
"status": "good",
|
|
|
|
|
|
"verdict": "Gut",
|
|
|
|
|
|
"hoverTop": "Abweichung Ruhepuls",
|
|
|
|
|
|
"hoverBody": "calculate_rhr_vs_baseline_pct",
|
|
|
|
|
|
"keys": ["rhr_vs_baseline"],
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
2026-04-20 08:11:23 +02:00
|
|
|
|
|
|
|
|
|
|
return tiles
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def build_recovery_progress_insights(
|
|
|
|
|
|
recovery_score: Optional[int],
|
|
|
|
|
|
sleep_debt_hours: Optional[float],
|
|
|
|
|
|
hrv_vs_baseline_pct: Optional[float],
|
2026-04-20 10:29:43 +02:00
|
|
|
|
include_autonomic_hrv_narrative: bool = False,
|
2026-04-20 08:11:23 +02:00
|
|
|
|
) -> List[Dict[str, Any]]:
|
2026-04-20 10:29:43 +02:00
|
|
|
|
"""HRV-Basistext optional: steckt gebündelt im Vital-Verlauf (consolidated_paragraphs)."""
|
2026-04-20 08:11:23 +02:00
|
|
|
|
out: List[Dict[str, Any]] = []
|
|
|
|
|
|
|
|
|
|
|
|
if recovery_score is not None:
|
|
|
|
|
|
tone = "good" if recovery_score >= 65 else "warn" if recovery_score >= 45 else "bad"
|
|
|
|
|
|
out.append(
|
|
|
|
|
|
{
|
|
|
|
|
|
"key": "ins_rec",
|
|
|
|
|
|
"tone": tone,
|
|
|
|
|
|
"title": "Gesamterholung",
|
|
|
|
|
|
"body": f"Der Recovery-Score liegt bei {recovery_score}/100. "
|
|
|
|
|
|
"Er kombiniert Schlaf- und Vital-Signale — ideal für die Einordnung von Trainingstagen.",
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if sleep_debt_hours is not None:
|
|
|
|
|
|
tone = "good" if sleep_debt_hours <= 3 else "warn" if sleep_debt_hours <= 10 else "bad"
|
|
|
|
|
|
out.append(
|
|
|
|
|
|
{
|
|
|
|
|
|
"key": "ins_debt",
|
|
|
|
|
|
"tone": tone,
|
|
|
|
|
|
"title": "Schlaf nachholen",
|
|
|
|
|
|
"body": f"Geschätzte Schlafschuld: {sleep_debt_hours:.1f} h. "
|
|
|
|
|
|
"Hohe Schulden erhöhen Verletzungs- und Ermüdungsrisiko — Priorität Schlafhygiene.",
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-04-20 10:29:43 +02:00
|
|
|
|
if include_autonomic_hrv_narrative and hrv_vs_baseline_pct is not None:
|
2026-04-20 08:11:23 +02:00
|
|
|
|
tone = "good" if hrv_vs_baseline_pct >= 0 else "warn"
|
|
|
|
|
|
out.append(
|
|
|
|
|
|
{
|
|
|
|
|
|
"key": "ins_hrv",
|
|
|
|
|
|
"tone": tone,
|
|
|
|
|
|
"title": "Autonomes System",
|
|
|
|
|
|
"body": f"HRV liegt {hrv_vs_baseline_pct:+.1f} % relativ zur Basis. "
|
|
|
|
|
|
"Positive Werte werden oft mit guter Regeneration assoziiert (individuell interpretieren).",
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
return out
|