fix(datastore): support retroactive date logging and fix Garmin balance display logic
This commit is contained in:
@@ -474,6 +474,26 @@ class DatastoreEngine:
|
|||||||
start_dt = (now_dt - delta).astimezone(BERLIN_TZ)
|
start_dt = (now_dt - delta).astimezone(BERLIN_TZ)
|
||||||
start_iso = start_dt.isoformat()
|
start_iso = start_dt.isoformat()
|
||||||
elif not start:
|
elif not start:
|
||||||
|
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_dt = now_dt.astimezone(BERLIN_TZ)
|
||||||
start_iso = start_dt.isoformat()
|
start_iso = start_dt.isoformat()
|
||||||
else:
|
else:
|
||||||
@@ -1186,6 +1206,7 @@ def run_cli():
|
|||||||
p_add.add_argument("--actor", default="cli", help="Reporter identifier")
|
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("--payload", required=True, help="JSON payload string")
|
||||||
p_add.add_argument("--start", help="ISO start time")
|
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("--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("--since", help="Relative start offset backwards from now (e.g. '5m', '15m', '1h')")
|
||||||
p_add.add_argument("--meal", help="Meal grouping")
|
p_add.add_argument("--meal", help="Meal grouping")
|
||||||
@@ -1232,12 +1253,25 @@ def run_cli():
|
|||||||
if "." not in kind:
|
if "." not in kind:
|
||||||
parser.error(f"Invalid kind '{kind}'. Expected domain.event (e.g. 'nutrition.intake', 'sleep.session', 'feeding.bottle').")
|
parser.error(f"Invalid kind '{kind}'. Expected domain.event (e.g. 'nutrition.intake', 'sleep.session', 'feeding.bottle').")
|
||||||
payload = json.loads(args.payload)
|
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(
|
res = engine.add_event(
|
||||||
kind=kind,
|
kind=kind,
|
||||||
subject=args.subject,
|
subject=args.subject,
|
||||||
actor=args.actor,
|
actor=args.actor,
|
||||||
payload=payload,
|
payload=payload,
|
||||||
start=args.start,
|
start=start_val,
|
||||||
end=args.end,
|
end=args.end,
|
||||||
since=args.since,
|
since=args.since,
|
||||||
meal=args.meal,
|
meal=args.meal,
|
||||||
|
|||||||
@@ -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')}"
|
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')}"
|
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)
|
# 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
|
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
|
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
|
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
|
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")
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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 "Ausgeglichen"
|
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
|
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
|
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
|
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
|
# 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:
|
if val < 0:
|
||||||
return f"-{abs(val):,} kcal".replace(",", ".")
|
return f"-{abs(val):,} kcal".replace(",", ".")
|
||||||
elif val > 0:
|
elif val > 0:
|
||||||
@@ -326,7 +346,9 @@ def generate_dashboard_html(engine: DatastoreEngine) -> str:
|
|||||||
return "0 kcal"
|
return "0 kcal"
|
||||||
|
|
||||||
def balance_badge(val: int | None, status: str) -> str:
|
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 '<span class="px-2.5 py-0.5 rounded-full text-xs font-semibold bg-slate-100 text-slate-700">Garmin ausstehend</span>'
|
||||||
|
if val == 0:
|
||||||
return '<span class="px-2.5 py-0.5 rounded-full text-xs font-semibold bg-slate-100 text-slate-700">Ausgeglichen</span>'
|
return '<span class="px-2.5 py-0.5 rounded-full text-xs font-semibold bg-slate-100 text-slate-700">Ausgeglichen</span>'
|
||||||
if val < 0:
|
if val < 0:
|
||||||
return f'<span class="inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-xs font-semibold bg-emerald-100 text-emerald-800 border border-emerald-200"><i data-lucide="trending-down" class="w-3 h-3"></i> {abs(val):,} kcal Defizit</span>'.replace(",", ".")
|
return f'<span class="inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-xs font-semibold bg-emerald-100 text-emerald-800 border border-emerald-200"><i data-lucide="trending-down" class="w-3 h-3"></i> {abs(val):,} kcal Defizit</span>'.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')}"
|
manifest_data_uri = f"data:application/manifest+json;base64,{base64.b64encode(manifest_json.encode('utf-8')).decode('utf-8')}"
|
||||||
|
|
||||||
# Hero Balance Styling
|
# 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"
|
if bal_y is None or exp_y == 0:
|
||||||
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")
|
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"""<!DOCTYPE html>
|
html = f"""<!DOCTYPE html>
|
||||||
<html lang="de" class="h-full bg-slate-50">
|
<html lang="de" class="h-full bg-slate-50">
|
||||||
@@ -595,7 +631,7 @@ def generate_dashboard_html(engine: DatastoreEngine) -> str:
|
|||||||
<span class="text-slate-400 font-bold">−</span>
|
<span class="text-slate-400 font-bold">−</span>
|
||||||
<div class="flex items-center gap-1.5 text-indigo-700">
|
<div class="flex items-center gap-1.5 text-indigo-700">
|
||||||
<i data-lucide="zap" class="w-4 h-4 text-indigo-500"></i>
|
<i data-lucide="zap" class="w-4 h-4 text-indigo-500"></i>
|
||||||
<span class="font-bold">{exp_y:,}</span> <span class="text-slate-500 font-sans text-xs">Garmin</span>
|
<span class="font-bold">{f'{exp_y:,}' if exp_y else 'ausstehend'}</span> <span class="text-slate-500 font-sans text-xs">Garmin</span>
|
||||||
</div>
|
</div>
|
||||||
<span class="text-slate-400 font-bold">=</span>
|
<span class="text-slate-400 font-bold">=</span>
|
||||||
<div class="font-bold {hero_bal_class.split()[0]}">
|
<div class="font-bold {hero_bal_class.split()[0]}">
|
||||||
@@ -685,7 +721,7 @@ def generate_dashboard_html(engine: DatastoreEngine) -> str:
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex justify-between">
|
<div class="flex justify-between">
|
||||||
<span>Garmin:</span>
|
<span>Garmin:</span>
|
||||||
<span class="font-mono font-medium text-slate-700">{exp_y:,} kcal</span>
|
<span class="font-mono font-medium text-slate-700">{f'{exp_y:,} kcal' if exp_y else 'ausstehend'}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-between pt-1 border-t border-slate-100">
|
<div class="flex justify-between pt-1 border-t border-slate-100">
|
||||||
<span>Schritte:</span>
|
<span>Schritte:</span>
|
||||||
|
|||||||
Reference in New Issue
Block a user