- Removed unnecessary snapshot key omission in the `build_vital_signs_matrix_chart_payload` function for improved data clarity. - Introduced new components for better organization and presentation of vital signs insights, including `SectionHeading` and `VitalZoneHint`. - Enhanced axis tick formatting in the `RecoveryDashboardOverview` component for clearer data representation. - Updated narrative rendering logic to improve user experience and contextual understanding of vital metrics.
120 lines
4.3 KiB
Python
120 lines
4.3 KiB
Python
"""
|
||
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.vitals_fitness_insights import build_vitals_history_and_analytics
|
||
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,
|
||
)
|
||
|
||
hrv_f = float(hrv_dev) if hrv_dev is not None else None
|
||
rhr_f = float(rhr_dev) if rhr_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),
|
||
"vitals_history": build_vitals_history_and_analytics(
|
||
profile_id, vital_days, hrv_vs_baseline_pct=hrv_f, rhr_vs_baseline_pct=rhr_f
|
||
),
|
||
}
|
||
|
||
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",
|
||
},
|
||
}
|