""" Layer 2b: Recovery/Erholung — Bundle für Verlauf unter Fitness (Issue 53). """ from __future__ import annotations from typing import Any, Dict, Optional from db import get_db, get_cursor from data_layer.recovery_chart_payloads import ( build_hrv_rhr_baseline_chart_payload, build_recovery_score_chart_payload, build_sleep_debt_chart_payload, build_sleep_duration_quality_chart_payload, build_vital_signs_matrix_chart_payload, ) from data_layer.recovery_interpretation import ( build_recovery_dashboard_kpi_tiles, build_recovery_progress_insights, ) from data_layer.recovery_metrics import ( calculate_hrv_vs_baseline_pct, calculate_recovery_score_v2, calculate_rhr_vs_baseline_pct, calculate_sleep_debt_hours, get_sleep_duration_data, ) def _has_recovery_sources(profile_id: str) -> bool: with get_db() as conn: cur = get_cursor(conn) cur.execute("SELECT 1 FROM sleep_log WHERE profile_id=%s LIMIT 1", (profile_id,)) if cur.fetchone(): return True cur.execute("SELECT 1 FROM vitals_baseline WHERE profile_id=%s LIMIT 1", (profile_id,)) return cur.fetchone() is not None def get_recovery_dashboard_viz_bundle(profile_id: str, days: int) -> Dict[str, Any]: """ Ein Request: KPIs, Insights, Charts R1–R5 (Chart.js-kompatibel). """ if not _has_recovery_sources(profile_id): return { "confidence": "insufficient", "has_recovery_data": False, "message": "Noch keine Schlaf- oder Vitaldaten", "kpi_tiles": [], "progress_insights": [], "charts": {}, "meta": {"layer_1": "recovery_metrics", "layer_2b": "recovery_viz"}, } all_history = days >= 9999 eff_days = 3650 if all_history else max(7, min(int(days), 3650)) chart_days = min(90, max(7, min(eff_days, 365))) # Vital-Matrix: längeres Fenster + Fallback im Builder, damit nicht nur „letzte 30 Tage“ vital_days = min(365, max(30, min(eff_days, 365))) recovery_score_val = calculate_recovery_score_v2(profile_id) sleep_debt = calculate_sleep_debt_hours(profile_id) dur = get_sleep_duration_data(profile_id, chart_days) avg_sleep = None if dur.get("confidence") != "insufficient": avg_sleep = float(dur.get("avg_duration_hours") or 0) or None hrv_dev = calculate_hrv_vs_baseline_pct(profile_id) rhr_dev = calculate_rhr_vs_baseline_pct(profile_id) kpi_tiles = build_recovery_dashboard_kpi_tiles( recovery_score_val, float(sleep_debt) if sleep_debt is not None else None, avg_sleep, float(hrv_dev) if hrv_dev is not None else None, float(rhr_dev) if rhr_dev is not None else None, ) insights = build_recovery_progress_insights( recovery_score_val, float(sleep_debt) if sleep_debt is not None else None, float(hrv_dev) if hrv_dev is not None else None, ) charts = { "recovery_score": build_recovery_score_chart_payload(profile_id, chart_days), "hrv_rhr": build_hrv_rhr_baseline_chart_payload(profile_id, chart_days), "sleep_duration_quality": build_sleep_duration_quality_chart_payload(profile_id, chart_days), "sleep_debt": build_sleep_debt_chart_payload(profile_id, chart_days), "vital_signs_matrix": build_vital_signs_matrix_chart_payload(profile_id, vital_days), } conf = "medium" if recovery_score_val is None and sleep_debt is None: conf = "low" return { "confidence": conf, "has_recovery_data": True, "days_requested": days, "effective_window_days": eff_days, "chart_days_used": chart_days, "vital_matrix_days_used": vital_days, "kpi_tiles": kpi_tiles, "progress_insights": insights, "charts": charts, "meta": { "layer_1": "recovery_metrics", "layer_2b": "recovery_viz", "issue": "53-layer-2b-recovery", }, }