- Updated the `build_vital_signs_matrix_chart_payload` function to accept optional keys for omitting specific snapshot data, improving flexibility in data presentation. - Enhanced the `build_recovery_dashboard_kpi_tiles` function to conditionally merge heart and autonomic tiles based on new parameters, refining the dashboard's insights. - Integrated new analytics features in the `RecoveryDashboardOverview` component, including consolidated paragraphs for better narrative context and visual representation of trends. - Improved the handling of vital signs data in the frontend, ensuring clearer messaging and enhanced user experience when displaying vital metrics.
217 lines
7.3 KiB
Python
217 lines
7.3 KiB
Python
"""
|
||
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],
|
||
merge_heart_autonomic_tiles: bool = True,
|
||
) -> 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"],
|
||
}
|
||
)
|
||
|
||
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"
|
||
if hrv_vs_baseline_pct is not None
|
||
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"],
|
||
}
|
||
)
|
||
|
||
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"],
|
||
}
|
||
)
|
||
|
||
return tiles
|
||
|
||
|
||
def build_recovery_progress_insights(
|
||
recovery_score: Optional[int],
|
||
sleep_debt_hours: Optional[float],
|
||
hrv_vs_baseline_pct: Optional[float],
|
||
include_autonomic_hrv_narrative: bool = False,
|
||
) -> List[Dict[str, Any]]:
|
||
"""HRV-Basistext optional: steckt gebündelt im Vital-Verlauf (consolidated_paragraphs)."""
|
||
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.",
|
||
}
|
||
)
|
||
|
||
if include_autonomic_hrv_narrative and hrv_vs_baseline_pct is not None:
|
||
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
|