feat: consolidate datastore into sleep, nutrition, mila databases
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user