From 37fd28ec5a5c754f8fdd270378ccfbcfe0c835f3 Mon Sep 17 00:00:00 2001 From: Lars Date: Mon, 23 Mar 2026 15:30:17 +0100 Subject: [PATCH] feat: add AI evaluation placeholders for v9d Phase 2 modules **Backend (insights.py):** - Extended _get_profile_data() to fetch sleep, rest_days, vitals - Added template variables for Sleep Module: {{sleep_summary}}, {{sleep_detail}}, {{sleep_avg_duration}}, {{sleep_avg_quality}} - Added template variables for Rest Days: {{rest_days_summary}}, {{rest_days_count}}, {{rest_days_types}} - Added template variables for Vitals: {{vitals_summary}}, {{vitals_detail}}, {{vitals_avg_hr}}, {{vitals_avg_hrv}}, {{vitals_avg_bp}}, {{vitals_vo2_max}} **Frontend (Analysis.jsx):** - Added 12 new template variables to VARS list in PromptEditor - Enables AI prompt creation for Sleep, Rest Days, and Vitals analysis All modules now have AI evaluation support for future prompt creation. Co-Authored-By: Claude Opus 4.6 --- backend/routers/insights.py | 84 ++++++++++++++++++++++++++++++++- frontend/src/pages/Analysis.jsx | 6 ++- 2 files changed, 88 insertions(+), 2 deletions(-) diff --git a/backend/routers/insights.py b/backend/routers/insights.py index 12a342e..410ba92 100644 --- a/backend/routers/insights.py +++ b/backend/routers/insights.py @@ -77,13 +77,23 @@ def _get_profile_data(pid: str): nutrition = [r2d(r) for r in cur.fetchall()] cur.execute("SELECT * FROM activity_log WHERE profile_id=%s ORDER BY date DESC LIMIT 90", (pid,)) activity = [r2d(r) for r in cur.fetchall()] + # v9d Phase 2: Sleep, Rest Days, Vitals + cur.execute("SELECT * FROM sleep_log WHERE profile_id=%s ORDER BY date DESC LIMIT 30", (pid,)) + sleep = [r2d(r) for r in cur.fetchall()] + cur.execute("SELECT * FROM rest_days WHERE profile_id=%s ORDER BY date DESC LIMIT 30", (pid,)) + rest_days = [r2d(r) for r in cur.fetchall()] + cur.execute("SELECT * FROM vitals_log WHERE profile_id=%s ORDER BY date DESC LIMIT 30", (pid,)) + vitals = [r2d(r) for r in cur.fetchall()] return { "profile": prof, "weight": weight, "circumference": circ, "caliper": caliper, "nutrition": nutrition, - "activity": activity + "activity": activity, + "sleep": sleep, + "rest_days": rest_days, + "vitals": vitals } @@ -103,6 +113,9 @@ def _prepare_template_vars(data: dict) -> dict: caliper = data['caliper'] nutrition = data['nutrition'] activity = data['activity'] + sleep = data.get('sleep', []) + rest_days = data.get('rest_days', []) + vitals = data.get('vitals', []) vars = { "name": prof.get('name', 'Nutzer'), @@ -192,6 +205,75 @@ def _prepare_template_vars(data: dict) -> dict: vars['activity_detail'] = "keine Daten" vars['activity_kcal_summary'] = "keine Daten" + # Sleep summary (v9d Phase 2b) + if sleep: + n = len(sleep) + avg_duration = sum(float(s.get('duration_minutes',0) or 0) for s in sleep) / n + avg_quality = sum(int(s.get('quality',0) or 0) for s in sleep if s.get('quality')) / max(sum(1 for s in sleep if s.get('quality')), 1) + deep_data = [s for s in sleep if s.get('deep_minutes')] + avg_deep = sum(float(s.get('deep_minutes',0)) for s in deep_data) / len(deep_data) if deep_data else 0 + vars['sleep_summary'] = f"{n} Nächte, Ø {avg_duration/60:.1f}h Schlafdauer, Qualität {avg_quality:.1f}/5" + vars['sleep_detail'] = f"Ø {avg_duration:.0f}min gesamt, {avg_deep:.0f}min Tiefschlaf" + vars['sleep_avg_duration'] = round(avg_duration) + vars['sleep_avg_quality'] = round(avg_quality, 1) + vars['sleep_nights'] = n + else: + vars['sleep_summary'] = "keine Daten" + vars['sleep_detail'] = "keine Daten" + vars['sleep_avg_duration'] = 0 + vars['sleep_avg_quality'] = 0 + vars['sleep_nights'] = 0 + + # Rest Days summary (v9d Phase 2a) + if rest_days: + n = len(rest_days) + types = {} + for rd in rest_days: + rt = rd.get('rest_type', 'unknown') + types[rt] = types.get(rt, 0) + 1 + type_summary = ", ".join([f"{k}: {v}x" for k, v in types.items()]) + vars['rest_days_summary'] = f"{n} Ruhetage (letzte 30d): {type_summary}" + vars['rest_days_count'] = n + vars['rest_days_types'] = type_summary + else: + vars['rest_days_summary'] = "keine Daten" + vars['rest_days_count'] = 0 + vars['rest_days_types'] = "keine" + + # Vitals summary (v9d Phase 2d) + if vitals: + n = len(vitals) + hr_data = [v for v in vitals if v.get('resting_hr')] + hrv_data = [v for v in vitals if v.get('hrv')] + bp_data = [v for v in vitals if v.get('blood_pressure_systolic') and v.get('blood_pressure_diastolic')] + vo2_data = [v for v in vitals if v.get('vo2_max')] + + avg_hr = sum(int(v.get('resting_hr')) for v in hr_data) / len(hr_data) if hr_data else 0 + avg_hrv = sum(int(v.get('hrv')) for v in hrv_data) / len(hrv_data) if hrv_data else 0 + avg_bp_sys = sum(int(v.get('blood_pressure_systolic')) for v in bp_data) / len(bp_data) if bp_data else 0 + avg_bp_dia = sum(int(v.get('blood_pressure_diastolic')) for v in bp_data) / len(bp_data) if bp_data else 0 + latest_vo2 = float(vo2_data[0].get('vo2_max')) if vo2_data else 0 + + parts = [] + if avg_hr: parts.append(f"Ruhepuls Ø {avg_hr:.0f}bpm") + if avg_hrv: parts.append(f"HRV Ø {avg_hrv:.0f}ms") + if avg_bp_sys: parts.append(f"Blutdruck Ø {avg_bp_sys:.0f}/{avg_bp_dia:.0f}mmHg") + if latest_vo2: parts.append(f"VO2 Max {latest_vo2:.1f}") + + vars['vitals_summary'] = f"{n} Messungen: " + ", ".join(parts) if parts else "keine verwertbaren Daten" + vars['vitals_detail'] = vars['vitals_summary'] + vars['vitals_avg_hr'] = round(avg_hr) + vars['vitals_avg_hrv'] = round(avg_hrv) + vars['vitals_avg_bp'] = f"{round(avg_bp_sys)}/{round(avg_bp_dia)}" if avg_bp_sys else "k.A." + vars['vitals_vo2_max'] = round(latest_vo2, 1) if latest_vo2 else "k.A." + else: + vars['vitals_summary'] = "keine Daten" + vars['vitals_detail'] = "keine Daten" + vars['vitals_avg_hr'] = 0 + vars['vitals_avg_hrv'] = 0 + vars['vitals_avg_bp'] = "k.A." + vars['vitals_vo2_max'] = "k.A." + return vars diff --git a/frontend/src/pages/Analysis.jsx b/frontend/src/pages/Analysis.jsx index 5d50ad2..17234b0 100644 --- a/frontend/src/pages/Analysis.jsx +++ b/frontend/src/pages/Analysis.jsx @@ -57,7 +57,11 @@ function PromptEditor({ prompt, onSave, onCancel }) { '{{weight_trend}}','{{weight_aktuell}}','{{kf_aktuell}}','{{caliper_summary}}', '{{circ_summary}}','{{nutrition_summary}}','{{nutrition_detail}}', '{{protein_ziel_low}}','{{protein_ziel_high}}','{{activity_summary}}', - '{{activity_kcal_summary}}','{{activity_detail}}'] + '{{activity_kcal_summary}}','{{activity_detail}}', + '{{sleep_summary}}','{{sleep_detail}}','{{sleep_avg_duration}}','{{sleep_avg_quality}}', + '{{rest_days_summary}}','{{rest_days_count}}','{{rest_days_types}}', + '{{vitals_summary}}','{{vitals_detail}}','{{vitals_avg_hr}}','{{vitals_avg_hrv}}', + '{{vitals_avg_bp}}','{{vitals_vo2_max}}'] return (