Files
mop-skills/skills/datastore/scripts/render_views.py
T

585 lines
23 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
# /// script
# dependencies = [
# "duckdb>=1.1.0",
# "python-ulid>=3.0.0",
# "pytz>=2024.1",
# ]
# ///
"""
render_views.py — Generates clean Markdown view pages and HTML dashboards from DuckDB JSONL.
Uses DatastoreEngine for exact schema validation, typed loading, and Berlin timezone attribution.
"""
from __future__ import annotations
import argparse
import datetime
import os
from pathlib import Path
from zoneinfo import ZoneInfo
from datastore import DatastoreEngine
BERLIN_TZ = ZoneInfo("Europe/Berlin")
WEEKDAYS_DE = {
0: "Montag",
1: "Dienstag",
2: "Mittwoch",
3: "Donnerstag",
4: "Freitag",
5: "Samstag",
6: "Sonntag",
}
def find_default_dirs():
candidates = [
Path.cwd() / "knowledge-wiki",
Path("/Users/moritz/src/infrapuzzle/knowledge-wiki"),
Path("/home/node/.openclaw/wiki/main"),
]
for c in candidates:
if (c / "data").is_dir():
return c, c / "data"
return Path.cwd(), Path.cwd() / "data"
def render_nutrition_views(engine: DatastoreEngine, wiki_dir: Path):
con = engine.con
months = ["2026-09", "2026-10"]
for m in months:
out_file = wiki_dir / "entities" / "nutrition-log" / "moritz" / f"{m}.md"
out_file.parent.mkdir(parents=True, exist_ok=True)
month_name = "September" if m.endswith("09") else "Oktober"
# Query days in month descending
days = con.execute(
f"""
SELECT DISTINCT log_date
FROM v_nutrition
WHERE strftime(log_date, '%Y-%m') = '{m}'
ORDER BY log_date DESC
"""
).fetchall()
md = []
md.append("---")
md.append(f"id: nutrition-log-moritz-{m}")
md.append("pageType: source")
md.append(f'title: "Ernährungsprotokoll Moritz — {month_name} 2026"')
md.append(f"month: {m}")
md.append("tags:")
md.append(" - nutrition-log")
md.append(" - personal")
md.append("lifecycle:")
md.append(" state: active")
md.append("---")
md.append("")
md.append(f"# Ernährungsprotokoll Moritz — {month_name} 2026")
md.append("")
md.append("<!-- DO NOT EDIT: GENERATED BY DATASTORE -->")
md.append("<!-- Source of Truth: data/events/nutrition/{YYYY-MM}.jsonl -->")
md.append("")
for (d,) in days:
d_str = str(d)
d_date = datetime.date.fromisoformat(d_str)
weekday_str = WEEKDAYS_DE[d_date.weekday()]
# Fetch daily aggregate
daily_agg = con.execute(
f"""
SELECT total_protein_g, total_carbs_g, total_sugar_g, total_fat_g, total_fiber_g, total_salt_g, total_kcal
FROM v_nutrition_daily
WHERE log_date = '{d_str}' AND subject = 'moritz'
"""
).fetchone()
# Check if daily balance against Garmin is available
balance_row = con.execute(
f"""
SELECT intake_kcal, expenditure_kcal, active_kcal, rmr_kcal, steps, balance_kcal, balance_status
FROM v_daily_balance
WHERE log_date = '{d_str}' AND subject = 'moritz'
"""
).fetchone()
md.append(f"## {d_str} {weekday_str}")
md.append("")
md.append("| Zeit | Input (original) | Menge | Eiweiß | KH | Zucker | Fett | Ballaststoffe | Salz | kcal |")
md.append("|---|---|---|---|---|---|---|---|---|---|")
# Fetch rows for this day
rows = con.execute(
f"""
SELECT strftime(local_time, '%H:%M'), item, qty, unit, protein_g, carbs_g, sugar_g, fat_g, fiber_g, salt_g, kcal, estimated
FROM v_nutrition
WHERE log_date = '{d_str}' AND subject = 'moritz'
ORDER BY start ASC
"""
).fetchall()
for r in rows:
t_str = r[0]
item = r[1]
qty = f"{int(r[2]) if r[2].is_integer() else r[2]} {r[3]}" if r[2] is not None and r[3] else ""
est = "~" if r[11] else ""
p = f"{est}{r[4]:.1f} g"
c = f"{est}{r[5]:.1f} g"
s = f"{est}{r[6]:.1f} g"
f_val = f"{est}{r[7]:.1f} g"
fib = f"{est}{r[8]:.1f} g"
salt = f"{est}{r[9]:.2f} g"
kc = f"{est}{int(round(r[10]))}"
md.append(f"| {t_str} | {item} | {qty} | {p} | {c} | {s} | {f_val} | {fib} | {salt} | {kc} |")
if daily_agg:
tot_p = f"{daily_agg[0]:.1f} g"
tot_c = f"{daily_agg[1]:.1f} g"
tot_s = f"{daily_agg[2]:.1f} g"
tot_f = f"{daily_agg[3]:.1f} g"
tot_fib = f"{daily_agg[4]:.1f} g"
tot_salt = f"{daily_agg[5]:.2f} g"
tot_kc = f"**{int(round(daily_agg[6]))}**"
md.append(f"| **Tages-Summe** | | | **{tot_p}** | **{tot_c}** | **{tot_s}** | **{tot_f}** | **{tot_fib}** | **{tot_salt}** | {tot_kc} |")
md.append("")
# Render Garmin balance block if available
if balance_row and balance_row[1] is not None:
intake = int(round(balance_row[0]))
expend = int(round(balance_row[1]))
diff = int(round(balance_row[5]))
steps = balance_row[4] or 0
active = balance_row[2] or 0
sign = "+" if diff > 0 else ""
md.append(f"- **Vortags-Bilanz (Garmin)**: {intake} kcal Verzehr - {expend} kcal Verbrauch = **{sign}{diff} kcal {balance_row[6]}** ({steps} Schritte, {active} Aktiv-kcal)")
md.append("")
out_file.write_text("\n".join(md), encoding="utf-8")
print(f"Rendered {out_file}")
def render_sleep_view(engine: DatastoreEngine, wiki_dir: Path):
con = engine.con
out_file = wiki_dir / "entities" / "people" / "mila-schlafprotokoll.md"
out_file.parent.mkdir(parents=True, exist_ok=True)
days = con.execute(
"""
SELECT DISTINCT log_date
FROM v_sleep
WHERE subject = 'mila'
ORDER BY log_date DESC
"""
).fetchall()
md = []
md.append("---")
md.append("id: mila-schlafprotokoll")
md.append("pageType: log")
md.append('title: "Schlafprotokoll Mila"')
md.append("subject: mila")
md.append("tags:")
md.append(" - sleep")
md.append(" - baby")
md.append(" - mila")
md.append("lifecycle:")
md.append(" state: active")
md.append("---")
md.append("")
md.append("# Schlafprotokoll Mila")
md.append("")
md.append("<!-- DO NOT EDIT: GENERATED BY DATASTORE -->")
md.append("<!-- Source of Truth: data/events/sleep/{YYYY-MM}.jsonl -->")
md.append("")
current_month_label = ""
for (d,) in days:
d_str = str(d)
d_date = datetime.date.fromisoformat(d_str)
month_label = f"{'September' if d_date.month == 9 else 'Oktober'} {d_date.year}"
if month_label != current_month_label:
current_month_label = month_label
md.append(f"# {month_label}")
md.append("")
weekday_str = WEEKDAYS_DE[d_date.weekday()]
daily_agg = con.execute(
f"""
SELECT total_sleep_hours, nap_count, night_sleep_minutes, nap_sleep_minutes
FROM v_sleep_daily
WHERE log_date = '{d_str}' AND subject = 'mila'
"""
).fetchone()
day_sum_str = ""
if daily_agg:
tot_h = daily_agg[0]
naps = daily_agg[1]
day_sum_str = f" ({tot_h:.2f} h gesamt, {naps} Schläfchen)"
md.append(f"## {weekday_str}, {d_date.strftime('%d.%m.%Y')}{day_sum_str}")
sessions = con.execute(
f"""
SELECT strftime(local_start, '%H:%M'),
CASE WHEN is_open THEN NULL ELSE strftime(local_end, '%H:%M') END,
sleep_type, location, note, duration_minutes, is_open
FROM v_sleep
WHERE log_date = '{d_str}' AND subject = 'mila'
ORDER BY start ASC
"""
).fetchall()
for s in sessions:
s_time = s[0]
e_time = s[1]
s_type = s[2]
loc = s[3]
note = s[4] or ""
dur = s[5]
is_open = s[6]
dur_str = f"{dur // 60} h {dur % 60:02d} min" if dur >= 60 else f"{dur} min"
if is_open:
md.append(f"- {s_time} – | {note or 'Nacht'} (läuft)")
else:
label = note if note else ("Nacht" if s_type == "night" else "Schläfchen")
md.append(f"- {s_time} – {e_time} | {label} ({dur_str})")
md.append("")
# Preserve narrative analysis block
md.append("""---
# Monatsanalyse September 2026 (nachgetragen 03.10.)
*Bewertet aus Sicht einer Kinderarzt-Praxis mit Schlafspezialisierung – Hinweise, keine Diagnose.*
**Zahlen (19.–30.09., 11 protokollierte Tage):**
- Nacht: Ø ~11 h 18 min (10 h 10 – 11 h 55)
- Tag: Ø ~2 h 29 min in 4–5 Schläfchen
- 24-h-Summe: Ø ~13 h 47 min
**Einordnung:** Das liegt am oberen, aber völlig normalen Rand des altersüblichen Bereichs (Säugling ~6 Mon.: 12–16 h/24 h, vgl. [Wikipedia: Schlaf](https://de.wikipedia.org/wiki/Schlaf) und [Wikipedia: Säugling](https://de.wikipedia.org/wiki/S%C3%A4ugling)).
**Trends im Monat:**
1. **Konsolidierung:** Von 5 kurzen Mikro-Schläfchen (Sep Anfang) hin zu 3–4 Schläfchen, erste lange Blöcke (85–140 min) am 20./21./27.09. und 02.10.
2. **Spätere Bettzeit:** 18:34–19:55 (Mitte Sep) → 20:10–21:00 (Ende Sep/Okt) – stabile Morgenwache ~06:00–06:50 bleibt.
3. **Durchschlafen:** Durchgehend lange Nachtblöcke, Wachphasen nachts selten und kurz.
**Erwartbar ab ~7–8 Monaten:** Übergang zum 3→2-Naps-Schema, Morgenschläfchen fällt zuerst weg. Zirkadiane Reifung unterstützt das (vgl. [Wikipedia: Zirkadiane Rhythmik](https://de.wikipedia.org/wiki/Zirkadiane_Rhythmik)).
---
""")
out_file.write_text("\n".join(md), encoding="utf-8")
print(f"Rendered {out_file}")
def render_html_dashboards(engine: DatastoreEngine, html_dir: Path):
html_dir.mkdir(parents=True, exist_ok=True)
con = engine.con
# 1. Nutrition HTML Dashboard
nutrition_days = con.execute(
"""
SELECT
d.log_date,
d.total_kcal,
d.total_protein_g,
d.total_carbs_g,
d.total_fat_g,
d.item_count,
b.expenditure_kcal,
b.balance_kcal,
b.balance_status,
b.steps
FROM v_nutrition_daily d
LEFT JOIN v_daily_balance b ON d.log_date = b.log_date AND d.subject = b.subject
WHERE d.subject = 'moritz'
ORDER BY d.log_date DESC
LIMIT 30
"""
).fetchall()
latest_n = nutrition_days[0] if nutrition_days else None
latest_kcal = int(round(latest_n[1])) if latest_n else 0
latest_protein = f"{latest_n[2]:.1f}" if latest_n else "0"
latest_garmin = f"{int(round(latest_n[6]))} kcal" if latest_n and latest_n[6] is not None else "Ausstehend"
latest_balance = f"{int(round(latest_n[7]))} kcal ({latest_n[8]})" if latest_n and latest_n[7] is not None else "—"
# Compute 7-day average
avg_7 = con.execute(
"""
SELECT ROUND(AVG(total_kcal), 0), ROUND(AVG(total_protein_g), 1)
FROM (SELECT total_kcal, total_protein_g FROM v_nutrition_daily WHERE subject = 'moritz' ORDER BY log_date DESC LIMIT 7)
"""
).fetchone()
avg_7_kcal = int(avg_7[0]) if avg_7 and avg_7[0] is not None else 0
avg_7_prot = avg_7[1] if avg_7 and avg_7[1] is not None else 0
nutrition_rows_html = []
for r in nutrition_days:
d_str = str(r[0])
kc = int(round(r[1]))
prot = f"{r[2]:.1f}"
carbs = f"{r[3]:.1f}"
fat = f"{r[4]:.1f}"
exp = int(round(r[6])) if r[6] is not None else "—"
diff = int(round(r[7])) if r[7] is not None else "—"
diff_class = "text-emerald-400" if (r[7] is not None and r[7] < 0) else ("text-amber-400" if (r[7] is not None and r[7] > 0) else "text-slate-400")
steps = f"{r[9]:,}" if r[9] is not None else "—"
nutrition_rows_html.append(f"""
<tr class="border-b border-slate-700/50 hover:bg-slate-800/40 transition">
<td class="py-3 px-4 font-mono font-medium text-slate-200">{d_str}</td>
<td class="py-3 px-4 font-bold text-sky-400">{kc}</td>
<td class="py-3 px-4 font-semibold text-emerald-400">{prot} g</td>
<td class="py-3 px-4 text-slate-300">{carbs} g</td>
<td class="py-3 px-4 text-slate-300">{fat} g</td>
<td class="py-3 px-4 text-slate-400">{exp}</td>
<td class="py-3 px-4 font-bold {diff_class}">{diff}</td>
<td class="py-3 px-4 text-slate-400">{steps}</td>
</tr>
""")
nutrition_html = f"""<!DOCTYPE html>
<html lang="de" class="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Moritz — Ernährung & Energiebilanz</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="bg-slate-950 text-slate-100 min-h-screen p-4 sm:p-8 font-sans antialiased selection:bg-emerald-500 selection:text-black">
<div class="max-w-6xl mx-auto space-y-6">
<!-- Header -->
<header class="flex flex-col sm:flex-row sm:items-center justify-between pb-6 border-b border-slate-800 gap-4">
<div>
<h1 class="text-2xl sm:text-3xl font-bold tracking-tight bg-gradient-to-r from-sky-400 via-emerald-400 to-teal-300 bg-clip-text text-transparent">
Moritz — Ernährung & Energiebilanz
</h1>
<p class="text-sm text-slate-400 mt-1">Strikte DuckDB-Aggregatdaten · Single Source of Truth</p>
</div>
<div class="inline-flex items-center gap-2 px-3 py-1.5 rounded-full bg-slate-900 border border-slate-800 text-xs text-slate-400">
<span class="w-2 h-2 rounded-full bg-emerald-500 animate-pulse"></span>
<span>Letztes Update: {datetime.date.today().strftime('%d.%m.%Y')}</span>
</div>
</header>
<!-- Key Metrics Grid -->
<div class="grid grid-cols-2 lg:grid-cols-4 gap-4">
<div class="bg-slate-900/80 border border-slate-800 rounded-2xl p-5 shadow-lg">
<div class="text-xs font-semibold uppercase tracking-wider text-slate-400">Letzter Tag (Kcal)</div>
<div class="text-3xl font-extrabold text-sky-400 mt-2">{latest_kcal}</div>
<div class="text-xs text-slate-400 mt-1">7-Tage-Schnitt: {avg_7_kcal} kcal</div>
</div>
<div class="bg-slate-900/80 border border-slate-800 rounded-2xl p-5 shadow-lg">
<div class="text-xs font-semibold uppercase tracking-wider text-slate-400">Eiweiß Intake</div>
<div class="text-3xl font-extrabold text-emerald-400 mt-2">{latest_protein} g</div>
<div class="text-xs text-slate-400 mt-1">7-Tage-Schnitt: {avg_7_prot} g</div>
</div>
<div class="bg-slate-900/80 border border-slate-800 rounded-2xl p-5 shadow-lg">
<div class="text-xs font-semibold uppercase tracking-wider text-slate-400">Garmin Verbrauch</div>
<div class="text-3xl font-extrabold text-amber-400 mt-2">{latest_garmin}</div>
<div class="text-xs text-slate-400 mt-1">RMR + Aktivität</div>
</div>
<div class="bg-slate-900/80 border border-slate-800 rounded-2xl p-5 shadow-lg">
<div class="text-xs font-semibold uppercase tracking-wider text-slate-400">Tages-Bilanz</div>
<div class="text-3xl font-extrabold text-indigo-400 mt-2">{latest_balance}</div>
<div class="text-xs text-slate-400 mt-1">Netto-Differenz</div>
</div>
</div>
<!-- Data Table Card -->
<div class="bg-slate-900/80 border border-slate-800 rounded-2xl p-5 shadow-xl overflow-hidden">
<h2 class="text-lg font-semibold text-slate-200 mb-4">Verlauf (Letzte 30 Tage)</h2>
<div class="overflow-x-auto">
<table class="w-full text-left text-sm whitespace-nowrap">
<thead class="text-xs uppercase bg-slate-800/60 text-slate-400">
<tr>
<th class="py-3 px-4 rounded-l-lg">Datum</th>
<th class="py-3 px-4">Aufnahme</th>
<th class="py-3 px-4">Protein</th>
<th class="py-3 px-4">KH</th>
<th class="py-3 px-4">Fett</th>
<th class="py-3 px-4">Garmin</th>
<th class="py-3 px-4">Bilanz</th>
<th class="py-3 px-4 rounded-r-lg">Schritte</th>
</tr>
</thead>
<tbody>
{''.join(nutrition_rows_html)}
</tbody>
</table>
</div>
</div>
</div>
</body>
</html>
"""
out_nutrition_html = html_dir / "nutrition.html"
out_nutrition_html.write_text(nutrition_html, encoding="utf-8")
print(f"Rendered HTML Dashboard: {out_nutrition_html}")
# 2. Sleep HTML Dashboard
sleep_days = con.execute(
"""
SELECT
log_date,
total_sleep_hours,
nap_count,
night_sleep_minutes,
nap_sleep_minutes,
total_sessions
FROM v_sleep_daily
WHERE subject = 'mila'
ORDER BY log_date DESC
LIMIT 30
"""
).fetchall()
latest_s = sleep_days[0] if sleep_days else None
latest_tot_h = f"{latest_s[1]:.2f} h" if latest_s else "0 h"
latest_night_str = f"{latest_s[3] // 60} h {latest_s[3] % 60:02d} min" if latest_s else "—"
latest_nap_str = f"{latest_s[2]} Naps ({latest_s[4]} min)" if latest_s else "—"
# Compute 7-day average total sleep hours
avg_sleep = con.execute(
"""
SELECT ROUND(AVG(total_sleep_hours), 2)
FROM (SELECT total_sleep_hours FROM v_sleep_daily WHERE subject = 'mila' ORDER BY log_date DESC LIMIT 7)
"""
).fetchone()
avg_sleep_h = f"{avg_sleep[0]:.2f} h" if avg_sleep and avg_sleep[0] is not None else "—"
sleep_rows_html = []
for r in sleep_days:
d_str = str(r[0])
tot = f"{r[1]:.2f} h"
night_h = f"{r[3] // 60} h {r[3] % 60:02d} min"
nap_info = f"{r[2]} ({r[4]} min)"
sleep_rows_html.append(f"""
<tr class="border-b border-slate-700/50 hover:bg-slate-800/40 transition">
<td class="py-3 px-4 font-mono font-medium text-slate-200">{d_str}</td>
<td class="py-3 px-4 font-bold text-purple-400">{tot}</td>
<td class="py-3 px-4 text-sky-400 font-semibold">{night_h}</td>
<td class="py-3 px-4 text-emerald-400">{nap_info}</td>
</tr>
""")
sleep_html = f"""<!DOCTYPE html>
<html lang="de" class="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Mila — Schlafprotokoll</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="bg-slate-950 text-slate-100 min-h-screen p-4 sm:p-8 font-sans antialiased selection:bg-purple-500 selection:text-white">
<div class="max-w-5xl mx-auto space-y-6">
<!-- Header -->
<header class="flex flex-col sm:flex-row sm:items-center justify-between pb-6 border-b border-slate-800 gap-4">
<div>
<h1 class="text-2xl sm:text-3xl font-bold tracking-tight bg-gradient-to-r from-purple-400 via-pink-400 to-indigo-300 bg-clip-text text-transparent">
Mila — Schlafprotokoll
</h1>
<p class="text-sm text-slate-400 mt-1">Deterministische Schlafdauer & Wachfenster via DuckDB</p>
</div>
<div class="inline-flex items-center gap-2 px-3 py-1.5 rounded-full bg-slate-900 border border-slate-800 text-xs text-slate-400">
<span class="w-2 h-2 rounded-full bg-purple-500 animate-pulse"></span>
<span>Letztes Update: {datetime.date.today().strftime('%d.%m.%Y')}</span>
</div>
</header>
<!-- Key Metrics Grid -->
<div class="grid grid-cols-2 sm:grid-cols-4 gap-4">
<div class="bg-slate-900/80 border border-slate-800 rounded-2xl p-5 shadow-lg">
<div class="text-xs font-semibold uppercase tracking-wider text-slate-400">24h Schlaf</div>
<div class="text-3xl font-extrabold text-purple-400 mt-2">{latest_tot_h}</div>
<div class="text-xs text-slate-400 mt-1">Letzter Tag</div>
</div>
<div class="bg-slate-900/80 border border-slate-800 rounded-2xl p-5 shadow-lg">
<div class="text-xs font-semibold uppercase tracking-wider text-slate-400">Nachtschlaf</div>
<div class="text-3xl font-extrabold text-sky-400 mt-2">{latest_night_str}</div>
<div class="text-xs text-slate-400 mt-1">Hauptschlafphase</div>
</div>
<div class="bg-slate-900/80 border border-slate-800 rounded-2xl p-5 shadow-lg">
<div class="text-xs font-semibold uppercase tracking-wider text-slate-400">Tagesschläfchen</div>
<div class="text-2xl font-extrabold text-emerald-400 mt-2">{latest_nap_str}</div>
<div class="text-xs text-slate-400 mt-1">Tagsüber</div>
</div>
<div class="bg-slate-900/80 border border-slate-800 rounded-2xl p-5 shadow-lg">
<div class="text-xs font-semibold uppercase tracking-wider text-slate-400">7-Tage-Schnitt</div>
<div class="text-3xl font-extrabold text-pink-400 mt-2">{avg_sleep_h}</div>
<div class="text-xs text-slate-400 mt-1">Altersbereich: 12-16 h</div>
</div>
</div>
<!-- Data Table Card -->
<div class="bg-slate-900/80 border border-slate-800 rounded-2xl p-5 shadow-xl overflow-hidden">
<h2 class="text-lg font-semibold text-slate-200 mb-4">Schlafhistorie (Letzte 30 Tage)</h2>
<div class="overflow-x-auto">
<table class="w-full text-left text-sm whitespace-nowrap">
<thead class="text-xs uppercase bg-slate-800/60 text-slate-400">
<tr>
<th class="py-3 px-4 rounded-l-lg">Datum</th>
<th class="py-3 px-4">24h Gesamtschlaf</th>
<th class="py-3 px-4">Nachtschlaf</th>
<th class="py-3 px-4 rounded-r-lg">Tagesschläfchen (Dauer)</th>
</tr>
</thead>
<tbody>
{''.join(sleep_rows_html)}
</tbody>
</table>
</div>
</div>
</div>
</body>
</html>
"""
out_sleep_html = html_dir / "sleep.html"
out_sleep_html.write_text(sleep_html, encoding="utf-8")
print(f"Rendered HTML Dashboard: {out_sleep_html}")
def main():
parser = argparse.ArgumentParser(description="Render Markdown and HTML views from DuckDB Datastore")
parser.add_argument("--wiki-dir", help="Path to knowledge-wiki repository")
parser.add_argument("--data-dir", help="Path to knowledge-wiki/data directory")
parser.add_argument("--html-dir", help="Path to output HTML dashboards")
parser.add_argument("--no-md", action="store_true", help="Skip Markdown rendering")
parser.add_argument("--no-html", action="store_true", help="Skip HTML dashboard rendering")
args = parser.parse_args()
default_wiki, default_data = find_default_dirs()
wiki_dir = Path(args.wiki_dir) if args.wiki_dir else default_wiki
data_dir = Path(args.data_dir) if args.data_dir else default_data
html_dir = Path(args.html_dir) if args.html_dir else (wiki_dir / "public" / "family")
engine = DatastoreEngine(data_dir)
if not args.no_md:
render_nutrition_views(engine, wiki_dir)
render_sleep_view(engine, wiki_dir)
if not args.no_html:
render_html_dashboards(engine, html_dir)
if __name__ == "__main__":
main()