""" 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, include_avg_sleep_kpi: 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"], } ) if include_avg_sleep_kpi: 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