feat: consolidate datastore into sleep, nutrition, mila databases

This commit is contained in:
Moritz Graf
2026-10-13 09:28:07 +02:00
parent fe4edcc217
commit 7b5d039323
5 changed files with 896 additions and 161 deletions
@@ -266,6 +266,105 @@ def generate_dashboard_html(engine: DatastoreEngine) -> str:
[yesterday_str]
).fetchall()
# Query v_sleep_daily for Moritz (Garmin Sleep & Recovery)
sleep_y_row = con.execute(
"""
SELECT
total_sleep_hours,
total_sleep_minutes,
sleep_score,
sleep_score_qualifier,
avg_overnight_hrv,
hrv_status,
body_battery_change,
resting_heart_rate,
deep_sleep_minutes,
light_sleep_minutes,
rem_sleep_minutes,
awake_sleep_minutes
FROM v_sleep_daily
WHERE subject = 'moritz' AND log_date = ?
""",
[yesterday_str]
).fetchone()
# Fallback to latest available sleep entry if yesterday has not synced yet
if not sleep_y_row or sleep_y_row[1] is None or sleep_y_row[1] == 0:
latest_sleep = con.execute(
"""
SELECT
total_sleep_hours,
total_sleep_minutes,
sleep_score,
sleep_score_qualifier,
avg_overnight_hrv,
hrv_status,
body_battery_change,
resting_heart_rate,
deep_sleep_minutes,
light_sleep_minutes,
rem_sleep_minutes,
awake_sleep_minutes,
log_date
FROM v_sleep_daily
WHERE subject = 'moritz' AND total_sleep_minutes > 0
ORDER BY log_date DESC
LIMIT 1
"""
).fetchone()
if latest_sleep:
sleep_y_row = latest_sleep[:12]
sleep_log_date_str = str(latest_sleep[12])
else:
sleep_log_date_str = yesterday_str
else:
sleep_log_date_str = yesterday_str
if sleep_y_row and sleep_y_row[1]:
sleep_dur_tot_m = int(sleep_y_row[1])
s_dh, s_dm = divmod(sleep_dur_tot_m, 60)
sleep_dur_str = f"{s_dh}h {s_dm:02d}m"
sleep_score = sleep_y_row[2]
sleep_qualifier = sleep_y_row[3] or ""
hrv_val = round(sleep_y_row[4]) if sleep_y_row[4] is not None else None
hrv_status = sleep_y_row[5] or ""
bb_change = sleep_y_row[6]
rhr_val = sleep_y_row[7] or rhr_y
deep_m = int(sleep_y_row[8] or 0)
light_m = int(sleep_y_row[9] or 0)
rem_m = int(sleep_y_row[10] or 0)
awake_m = int(sleep_y_row[11] or 0)
tot_stages = (deep_m + light_m + rem_m + awake_m) or 1
deep_pct = round((deep_m / tot_stages) * 100, 1)
light_pct = round((light_m / tot_stages) * 100, 1)
rem_pct = round((rem_m / tot_stages) * 100, 1)
awake_pct = round((awake_m / tot_stages) * 100, 1)
else:
sleep_dur_str = "—"
sleep_score = None
sleep_qualifier = ""
hrv_val = None
hrv_status = ""
bb_change = None
rhr_val = rhr_y
deep_m = 0
light_m = 0
rem_m = 0
awake_m = 0
deep_pct = 0
light_pct = 0
rem_pct = 0
awake_pct = 0
if sleep_log_date_str == yesterday_str:
sleep_date_label = yesterday_pretty
else:
try:
d_obj = datetime.date.fromisoformat(sleep_log_date_str)
sleep_date_label = f"{WEEKDAYS_LONG_DE[d_obj.weekday()]}, {d_obj.strftime('%d.%m.%Y')}"
except Exception:
sleep_date_label = sleep_log_date_str
# -------------------------------------------------------------------------
# 3. LETZTE 7 TAGE (Vollendete Tage bis gestern)
# -------------------------------------------------------------------------
@@ -717,6 +816,67 @@ def generate_dashboard_html(engine: DatastoreEngine) -> str:
</section>
<!-- SCHLAF & ERHOLUNG (GARMIN) TILE -->
<section class="bg-white rounded-3xl p-5 sm:p-6 border border-slate-200/90 shadow-sm hover:shadow-md hover:border-slate-300 transition cursor-pointer group" onclick="openModal('modal-yesterday-detail')">
<div class="flex items-center justify-between pb-3 border-b border-slate-100">
<div class="flex items-center gap-2">
<span class="w-2.5 h-2.5 rounded-full bg-indigo-500"></span>
<span class="text-xs font-bold uppercase tracking-wider text-slate-500">Schlaf & Erholung (Garmin)</span>
</div>
<span class="text-xs font-semibold text-slate-400 group-hover:text-indigo-600 flex items-center gap-1 transition">
{sleep_date_label} · Deep Dive <i data-lucide="chevron-right" class="w-3.5 h-3.5"></i>
</span>
</div>
<div class="grid grid-cols-2 sm:grid-cols-4 gap-3 pt-4">
<!-- Sleep Score -->
<div class="p-3 rounded-2xl bg-slate-50 border border-slate-100">
<span class="block text-[11px] text-slate-500 font-medium">Sleep Score</span>
<div class="flex items-baseline gap-1 mt-0.5">
<span class="text-2xl font-black font-mono text-indigo-700">{sleep_score if sleep_score is not None else '—'}</span>
<span class="text-xs font-semibold text-slate-500">{f'({sleep_qualifier})' if sleep_qualifier else ''}</span>
</div>
</div>
<!-- Duration -->
<div class="p-3 rounded-2xl bg-slate-50 border border-slate-100">
<span class="block text-[11px] text-slate-500 font-medium">Schlafdauer</span>
<div class="text-2xl font-black font-mono text-slate-800 mt-0.5">{sleep_dur_str}</div>
<div class="text-[10px] text-slate-400 mt-0.5">Tief {deep_m}m · Leicht {light_m}m · REM {rem_m}m</div>
</div>
<!-- Overnight HRV -->
<div class="p-3 rounded-2xl bg-slate-50 border border-slate-100">
<span class="block text-[11px] text-slate-500 font-medium">Overnight HRV</span>
<div class="flex items-baseline gap-1 mt-0.5">
<span class="text-2xl font-black font-mono text-emerald-700">{f'{hrv_val} ms' if hrv_val is not None else '—'}</span>
<span class="text-[10px] text-slate-400">{hrv_status}</span>
</div>
</div>
<!-- Body Battery & Resting HR -->
<div class="p-3 rounded-2xl bg-slate-50 border border-slate-100">
<span class="block text-[11px] text-slate-500 font-medium">Body Battery & Puls</span>
<div class="text-2xl font-black font-mono text-sky-700 mt-0.5">{f'+{bb_change}' if bb_change is not None else '—'}</div>
<div class="text-[10px] text-slate-400 mt-0.5">Ruhepuls: {rhr_val if rhr_val is not None else '—'} bpm</div>
</div>
</div>
<!-- Sleep Phase Breakdown Bar -->
<div class="mt-3 pt-3 border-t border-slate-100">
<div class="flex justify-between text-[11px] text-slate-500 mb-1">
<span>Phasen: Tief ({deep_m}m) · Leicht ({light_m}m) · REM ({rem_m}m) · Wach ({awake_m}m)</span>
<span class="font-semibold text-indigo-600">Details anzeigen →</span>
</div>
<div class="w-full bg-slate-100 rounded-full h-2.5 flex overflow-hidden">
<div class="bg-indigo-600 h-2.5" style="width: {deep_pct}%" title="Tiefschlaf"></div>
<div class="bg-sky-400 h-2.5" style="width: {light_pct}%" title="Leichtschlaf"></div>
<div class="bg-purple-400 h-2.5" style="width: {rem_pct}%" title="REM-Schlaf"></div>
<div class="bg-amber-300 h-2.5" style="width: {awake_pct}%" title="Wach"></div>
</div>
</div>
</section>
<!-- AGGREGATIONS-STRIP: Gestern, Letzte 7 Tage, Letzte 30 Tage -->
<section class="space-y-3">
<div class="flex items-center justify-between px-1">
@@ -992,6 +1152,50 @@ def generate_dashboard_html(engine: DatastoreEngine) -> str:
</div>
</div>
<!-- Sleep & Regeneration Deep Dive -->
<div class="p-4 rounded-2xl bg-indigo-50/50 border border-indigo-100 space-y-3">
<div class="flex items-center justify-between">
<div class="flex items-center gap-1.5 text-xs font-bold uppercase tracking-wider text-indigo-900">
<i data-lucide="moon" class="w-4 h-4 text-indigo-600"></i>
Schlaf & Regeneration (Garmin)
</div>
<span class="text-xs font-bold text-indigo-700 font-mono">Score: {sleep_score if sleep_score is not None else '—'} {f'({sleep_qualifier})' if sleep_qualifier else ''}</span>
</div>
<div class="grid grid-cols-2 sm:grid-cols-4 gap-2 text-center text-xs">
<div class="p-2.5 rounded-xl bg-white border border-indigo-100/80">
<span class="block text-[10px] text-slate-500">Dauer</span>
<strong class="text-sm font-mono text-slate-800">{sleep_dur_str}</strong>
</div>
<div class="p-2.5 rounded-xl bg-white border border-indigo-100/80">
<span class="block text-[10px] text-slate-500">Overnight HRV</span>
<strong class="text-sm font-mono text-emerald-700">{f'{hrv_val} ms' if hrv_val is not None else '—'}</strong>
<span class="block text-[9px] text-slate-400">{hrv_status}</span>
</div>
<div class="p-2.5 rounded-xl bg-white border border-indigo-100/80">
<span class="block text-[10px] text-slate-500">Body Battery</span>
<strong class="text-sm font-mono text-sky-700">{f'+{bb_change}' if bb_change is not None else '—'}</strong>
</div>
<div class="p-2.5 rounded-xl bg-white border border-indigo-100/80">
<span class="block text-[10px] text-slate-500">Ruhepuls</span>
<strong class="text-sm font-mono text-slate-800">{rhr_val if rhr_val is not None else '—'} bpm</strong>
</div>
</div>
<!-- Sleep Stages Breakdown -->
<div class="pt-1">
<div class="flex justify-between text-[11px] text-slate-600 mb-1">
<span>Tief: <strong>{deep_m}m</strong> · Leicht: <strong>{light_m}m</strong> · REM: <strong>{rem_m}m</strong> · Wach: <strong>{awake_m}m</strong></span>
</div>
<div class="w-full bg-slate-200/80 rounded-full h-2 flex overflow-hidden">
<div class="bg-indigo-600 h-2" style="width: {deep_pct}%" title="Tiefschlaf"></div>
<div class="bg-sky-400 h-2" style="width: {light_pct}%" title="Leichtschlaf"></div>
<div class="bg-purple-400 h-2" style="width: {rem_pct}%" title="REM-Schlaf"></div>
<div class="bg-amber-300 h-2" style="width: {awake_pct}%" title="Wach"></div>
</div>
</div>
</div>
<!-- Yesterday Meals List -->
<div>
<h4 class="text-xs font-bold uppercase text-slate-500 mb-2">Gestern gegessen</h4>