feat: add AI evaluation placeholders for v9d Phase 2 modules
All checks were successful
Deploy Development / deploy (push) Successful in 44s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 14s

**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 <noreply@anthropic.com>
This commit is contained in:
Lars 2026-03-23 15:30:17 +01:00
parent bf87e03100
commit 37fd28ec5a
2 changed files with 88 additions and 2 deletions

View File

@ -77,13 +77,23 @@ def _get_profile_data(pid: str):
nutrition = [r2d(r) for r in cur.fetchall()] 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,)) 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()] 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 { return {
"profile": prof, "profile": prof,
"weight": weight, "weight": weight,
"circumference": circ, "circumference": circ,
"caliper": caliper, "caliper": caliper,
"nutrition": nutrition, "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'] caliper = data['caliper']
nutrition = data['nutrition'] nutrition = data['nutrition']
activity = data['activity'] activity = data['activity']
sleep = data.get('sleep', [])
rest_days = data.get('rest_days', [])
vitals = data.get('vitals', [])
vars = { vars = {
"name": prof.get('name', 'Nutzer'), "name": prof.get('name', 'Nutzer'),
@ -192,6 +205,75 @@ def _prepare_template_vars(data: dict) -> dict:
vars['activity_detail'] = "keine Daten" vars['activity_detail'] = "keine Daten"
vars['activity_kcal_summary'] = "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 return vars

View File

@ -57,7 +57,11 @@ function PromptEditor({ prompt, onSave, onCancel }) {
'{{weight_trend}}','{{weight_aktuell}}','{{kf_aktuell}}','{{caliper_summary}}', '{{weight_trend}}','{{weight_aktuell}}','{{kf_aktuell}}','{{caliper_summary}}',
'{{circ_summary}}','{{nutrition_summary}}','{{nutrition_detail}}', '{{circ_summary}}','{{nutrition_summary}}','{{nutrition_detail}}',
'{{protein_ziel_low}}','{{protein_ziel_high}}','{{activity_summary}}', '{{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 ( return (
<div className="card section-gap"> <div className="card section-gap">