- 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.
124 lines
4.4 KiB
Python
124 lines
4.4 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,
|
||
omit_snapshot_keys={"resting_hr", "hrv"},
|
||
),
|
||
"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",
|
||
},
|
||
}
|