From 88c42aa1e5f0faa4d8ed0d3291e25e399a3a889b Mon Sep 17 00:00:00 2001 From: Moritz Graf Date: Sun, 11 Oct 2026 21:59:44 +0200 Subject: [PATCH] fix(datastore): support retroactive date logging and fix Garmin balance display logic --- skills/datastore/scripts/datastore.py | 40 ++++++++++++- .../scripts/update_moritz_website.py | 56 +++++++++++++++---- 2 files changed, 83 insertions(+), 13 deletions(-) diff --git a/skills/datastore/scripts/datastore.py b/skills/datastore/scripts/datastore.py index 987c7d5..025ea4d 100755 --- a/skills/datastore/scripts/datastore.py +++ b/skills/datastore/scripts/datastore.py @@ -474,8 +474,28 @@ class DatastoreEngine: start_dt = (now_dt - delta).astimezone(BERLIN_TZ) start_iso = start_dt.isoformat() elif not start: - start_dt = now_dt.astimezone(BERLIN_TZ) - start_iso = start_dt.isoformat() + payload_date = payload.get("date") if isinstance(payload, dict) else None + payload_time = payload.get("time") if isinstance(payload, dict) else None + if payload_date: + if payload_time: + try: + start_dt = datetime.datetime.fromisoformat(f"{payload_date}T{payload_time}").replace(tzinfo=BERLIN_TZ) + except Exception: + start_dt = datetime.datetime.fromisoformat(f"{payload_date}T12:00:00").replace(tzinfo=BERLIN_TZ) + else: + item_str = str(payload.get("item", "")).lower() if isinstance(payload, dict) else "" + if meal == "abendessen" or "abend" in item_str or "bier" in item_str: + start_dt = datetime.datetime.fromisoformat(f"{payload_date}T20:00:00").replace(tzinfo=BERLIN_TZ) + elif meal == "mittagessen" or "mittag" in item_str: + start_dt = datetime.datetime.fromisoformat(f"{payload_date}T12:30:00").replace(tzinfo=BERLIN_TZ) + elif meal == "fruehstueck" or "morgen" in item_str: + start_dt = datetime.datetime.fromisoformat(f"{payload_date}T08:00:00").replace(tzinfo=BERLIN_TZ) + else: + start_dt = datetime.datetime.fromisoformat(f"{payload_date}T12:00:00").replace(tzinfo=BERLIN_TZ) + start_iso = start_dt.isoformat() + else: + start_dt = now_dt.astimezone(BERLIN_TZ) + start_iso = start_dt.isoformat() else: start_dt = datetime.datetime.fromisoformat(start) if start_dt.tzinfo is None: @@ -1186,6 +1206,7 @@ def run_cli(): p_add.add_argument("--actor", default="cli", help="Reporter identifier") p_add.add_argument("--payload", required=True, help="JSON payload string") p_add.add_argument("--start", help="ISO start time") + p_add.add_argument("--date", help="Target date YYYY-MM-DD for retroactive logging") p_add.add_argument("--end", help="ISO end time") p_add.add_argument("--since", help="Relative start offset backwards from now (e.g. '5m', '15m', '1h')") p_add.add_argument("--meal", help="Meal grouping") @@ -1232,12 +1253,25 @@ def run_cli(): if "." not in kind: parser.error(f"Invalid kind '{kind}'. Expected domain.event (e.g. 'nutrition.intake', 'sleep.session', 'feeding.bottle').") payload = json.loads(args.payload) + start_val = args.start + if getattr(args, "date", None) and not start_val: + meal_val = args.meal or "" + item_str = str(payload.get("item", "")).lower() if isinstance(payload, dict) else "" + if meal_val == "abendessen" or "abend" in item_str or "bier" in item_str: + start_val = f"{args.date}T20:00:00+02:00" + elif meal_val == "mittagessen" or "mittag" in item_str: + start_val = f"{args.date}T12:30:00+02:00" + elif meal_val == "fruehstueck" or "morgen" in item_str: + start_val = f"{args.date}T08:00:00+02:00" + else: + start_val = f"{args.date}T12:00:00+02:00" + res = engine.add_event( kind=kind, subject=args.subject, actor=args.actor, payload=payload, - start=args.start, + start=start_val, end=args.end, since=args.since, meal=args.meal, diff --git a/skills/datastore/scripts/update_moritz_website.py b/skills/datastore/scripts/update_moritz_website.py index 169fe3f..6aec716 100644 --- a/skills/datastore/scripts/update_moritz_website.py +++ b/skills/datastore/scripts/update_moritz_website.py @@ -106,6 +106,20 @@ def generate_dashboard_html(engine: DatastoreEngine) -> str: today_pretty = f"{WEEKDAYS_LONG_DE[today_date.weekday()]}, {today_date.strftime('%d.%m.%Y')}" yesterday_pretty = f"{WEEKDAYS_LONG_DE[yesterday_date.weekday()]}, {yesterday_date.strftime('%d.%m.%Y')}" + # Auto-sync Garmin if yesterday's summary is missing in DuckDB + try: + g_check = con.execute("SELECT total_expenditure_kcal FROM v_garmin_daily WHERE log_date = ?", [yesterday_str]).fetchone() + if not g_check or g_check[0] is None: + from garmin_sync import GarminSyncClient, GarminMirrorWriter, get_token_dir, sync_date_range + token_dir = get_token_dir() + if (token_dir / "garmin_tokens.json").is_file(): + g_client = GarminSyncClient(token_dir) + g_writer = GarminMirrorWriter(engine.data_dir) + sync_date_range(g_client, g_writer, [yesterday_str]) + engine._load_garmin_daily_file(engine.data_dir / "garmin" / "daily" / f"{yesterday_str[:7]}.jsonl") + except Exception: + pass + # ------------------------------------------------------------------------- # 1. HEUTE: Intake & Mahlzeiten (Moritz) # ------------------------------------------------------------------------- @@ -190,8 +204,12 @@ def generate_dashboard_html(engine: DatastoreEngine) -> str: act_y = round(g_row[1]) if g_row and g_row[1] is not None else 0 rmr_y = round(g_row[2]) if g_row and g_row[2] is not None else 0 steps_y = int(g_row[3]) if g_row and g_row[3] is not None else 0 - bal_y = (intake_y - exp_y) if exp_y > 0 else 0 - status_y = "Defizit" if bal_y < 0 else ("Überschuss" if bal_y > 0 else "Ausgeglichen") + if exp_y > 0: + bal_y = intake_y - exp_y + status_y = "Defizit" if bal_y < 0 else ("Überschuss" if bal_y > 0 else "Ausgeglichen") + else: + bal_y = None + status_y = "Garmin ausstehend" prot_y = round(n_row[1], 1) if n_row and n_row[1] is not None else 0.0 carbs_y = round(n_row[2], 1) if n_row and n_row[2] is not None else 0.0 fat_y = round(n_row[3], 1) if n_row and n_row[3] is not None else 0.0 @@ -202,8 +220,8 @@ def generate_dashboard_html(engine: DatastoreEngine) -> str: act_y = round(yesterday_balance[2]) if yesterday_balance[2] is not None else 0 rmr_y = round(yesterday_balance[3]) if yesterday_balance[3] is not None else 0 steps_y = int(yesterday_balance[4]) if yesterday_balance[4] is not None else 0 - bal_y = round(yesterday_balance[5]) if yesterday_balance[5] is not None else 0 - status_y = yesterday_balance[6] or "Ausgeglichen" + bal_y = round(yesterday_balance[5]) if yesterday_balance[5] is not None else (intake_y - exp_y if exp_y > 0 else None) + status_y = yesterday_balance[6] or ("Garmin ausstehend" if not exp_y else "Ausgeglichen") prot_y = round(yesterday_balance[7], 1) if yesterday_balance[7] is not None else 0.0 carbs_y = round(yesterday_balance[8], 1) if yesterday_balance[8] is not None else 0.0 fat_y = round(yesterday_balance[9], 1) if yesterday_balance[9] is not None else 0.0 @@ -318,7 +336,9 @@ def generate_dashboard_html(engine: DatastoreEngine) -> str: # ------------------------------------------------------------------------- # HTML Render Helpers # ------------------------------------------------------------------------- - def format_balance(val: int) -> str: + def format_balance(val: int | None) -> str: + if val is None: + return "ausstehend" if val < 0: return f"-{abs(val):,} kcal".replace(",", ".") elif val > 0: @@ -326,7 +346,9 @@ def generate_dashboard_html(engine: DatastoreEngine) -> str: return "0 kcal" def balance_badge(val: int | None, status: str) -> str: - if val is None or val == 0: + if val is None or status == "Garmin ausstehend": + return 'Garmin ausstehend' + if val == 0: return 'Ausgeglichen' if val < 0: return f' {abs(val):,} kcal Defizit'.replace(",", ".") @@ -493,8 +515,22 @@ def generate_dashboard_html(engine: DatastoreEngine) -> str: manifest_data_uri = f"data:application/manifest+json;base64,{base64.b64encode(manifest_json.encode('utf-8')).decode('utf-8')}" # Hero Balance Styling - hero_bal_class = "text-emerald-700 bg-emerald-50 border-emerald-200" if bal_y < 0 else "text-amber-700 bg-amber-50 border-amber-200" - hero_bal_sign = f"-{abs(bal_y):,} kcal Defizit" if bal_y < 0 else (f"+{bal_y:,} kcal Überschuss" if bal_y > 0 else "0 kcal Ausgeglichen") + if bal_y is None or exp_y == 0: + hero_bal_class = "text-slate-700 bg-slate-100 border-slate-200" + hero_bal_sign = f"{intake_y:,} kcal geloggt" + status_y = "Garmin ausstehend" + elif bal_y < 0: + hero_bal_class = "text-emerald-700 bg-emerald-50 border-emerald-200" + hero_bal_sign = f"-{abs(bal_y):,} kcal Defizit" + status_y = "Defizit" + elif bal_y > 0: + hero_bal_class = "text-amber-700 bg-amber-50 border-amber-200" + hero_bal_sign = f"+{bal_y:,} kcal Überschuss" + status_y = "Überschuss" + else: + hero_bal_class = "text-slate-700 bg-slate-50 border-slate-200" + hero_bal_sign = "0 kcal Ausgeglichen" + status_y = "Ausgeglichen" html = f""" @@ -595,7 +631,7 @@ def generate_dashboard_html(engine: DatastoreEngine) -> str: −
- {exp_y:,} Garmin + {f'{exp_y:,}' if exp_y else 'ausstehend'} Garmin
=
@@ -685,7 +721,7 @@ def generate_dashboard_html(engine: DatastoreEngine) -> str:
Garmin: - {exp_y:,} kcal + {f'{exp_y:,} kcal' if exp_y else 'ausstehend'}
Schritte: