from __future__ import annotations from datetime import datetime, timedelta from typing import Any from zoneinfo import ZoneInfo import aiosqlite from server.config_loader import MedsConfig, Slot, load_meds_config, slot_datetime, today_str from server.db import utc_now_iso async def get_log_for_day(db: aiosqlite.Connection, day: str) -> dict[str, dict[str, Any]]: cur = await db.execute( "SELECT slot_id, status, logged_at, source FROM intake_log WHERE day = ? ORDER BY logged_at DESC", (day,), ) rows = await cur.fetchall() await cur.close() result: dict[str, dict[str, Any]] = {} for slot_id, status, logged_at, source in rows: if slot_id not in result: result[slot_id] = {"status": status, "logged_at": logged_at, "source": source} return result async def get_snooze_for_day(db: aiosqlite.Connection, day: str) -> dict[str, str]: cur = await db.execute("SELECT slot_id, snooze_until FROM snooze WHERE day = ?", (day,)) rows = await cur.fetchall() await cur.close() return {slot_id: snooze_until for slot_id, snooze_until in rows} def compute_status( slot: Slot, now: datetime, tz: ZoneInfo, log_entry: dict[str, Any] | None, snooze_until: str | None, ) -> str: if log_entry: return log_entry["status"] slot_dt = slot_datetime(now, slot, tz) window_end = slot_dt + timedelta(minutes=slot.reminder_window_minutes) if snooze_until: try: snooze_dt = datetime.fromisoformat(snooze_until) if now < snooze_dt: return "snoozed" except ValueError: pass if now < slot_dt: return "upcoming" if now <= window_end: return "pending" return "overdue" async def build_today(db: aiosqlite.Connection, config: MedsConfig | None = None) -> dict[str, Any]: config = config or load_meds_config() tz = config.timezone now = datetime.now(tz) day = today_str(tz) logs = await get_log_for_day(db, day) snoozes = await get_snooze_for_day(db, day) slots_out: list[dict[str, Any]] = [] for slot in config.slots: log_entry = logs.get(slot.id) snooze_until = snoozes.get(slot.id) status = compute_status(slot, now, tz, log_entry, snooze_until) slot_dt = slot_datetime(now, slot, tz) window_end = slot_dt + timedelta(minutes=slot.reminder_window_minutes) slots_out.append( { "id": slot.id, "time": slot.time.strftime("%H:%M"), "label": slot.label, "meds": [{"name": m.name, "dose": m.dose} for m in slot.meds], "status": status, "logged_at": log_entry["logged_at"] if log_entry else None, "window_end": window_end.isoformat(), "snooze_until": snooze_until, } ) return {"day": day, "timezone": str(tz), "now": now.isoformat(), "slots": slots_out} async def mark_missed_for_overdue(db: aiosqlite.Connection, config: MedsConfig | None = None) -> list[str]: from server.db import new_id config = config or load_meds_config() tz = config.timezone now = datetime.now(tz) day = today_str(tz) logs = await get_log_for_day(db, day) marked: list[str] = [] for slot in config.slots: if slot.id in logs: continue status = compute_status(slot, now, tz, None, None) if status == "overdue": await db.execute( "INSERT INTO intake_log (id, slot_id, day, status, logged_at, source) VALUES (?, ?, ?, ?, ?, ?)", (new_id(), slot.id, day, "missed", utc_now_iso(), "system"), ) marked.append(slot.id) if marked: await db.commit() return marked