feat(datastore): add diaper and feeding tracking, automated update_website posthook, and morning_briefing

This commit is contained in:
Moritz Graf
2026-10-11 12:16:24 +02:00
parent 80e38ab64a
commit 967909c3a7
4 changed files with 1074 additions and 3 deletions
+70 -3
View File
@@ -280,6 +280,27 @@ class DatastoreEngine:
except Exception as e:
logger.warning(f"Error loading garmin record from {file_path}: {e}")
def _trigger_website_update(self, subject: str):
"""Triggers update_website.py automatically for Mila changes."""
if subject != "mila":
return
try:
update_script = Path(__file__).resolve().parent / "update_website.py"
if update_script.exists():
import subprocess, shutil
uv_bin = shutil.which("uv") or "/home/node/.openclaw/bin/uv"
if Path(uv_bin).is_file():
cmd = [str(uv_bin), "run", str(update_script)]
else:
cmd = [sys.executable, str(update_script)]
subprocess.Popen(
cmd,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
except Exception as e:
logger.warning(f"Could not trigger update_website.py: {e}")
def _load_jsonl_file(self, file_path: Path, table_name: str):
"""Reads lines from JSONL and inserts into target table."""
if not file_path.is_file() or file_path.stat().st_size == 0:
@@ -292,6 +313,13 @@ class DatastoreEngine:
continue
try:
record = json.loads(line)
if table_name == "events":
if "created_at" not in record or not record.get("created_at"):
record["created_at"] = record.get("start") or datetime.datetime.now(datetime.timezone.utc).isoformat()
if "estimated" not in record:
record["estimated"] = False
if "deleted_at" not in record:
record["deleted_at"] = None
columns = list(record.keys())
placeholders = ", ".join(["?"] * len(columns))
col_names = ", ".join([f'"{c}"' for c in columns])
@@ -304,8 +332,7 @@ class DatastoreEngine:
values,
)
except Exception as e:
# Ignore comment lines or malformed lines with warning
pass
logger.warning(f"Error loading {table_name} record from {file_path}: {e}")
def _matches_payload(self, kind: str, p1: Dict[str, Any], p2: Dict[str, Any]) -> bool:
"""Determines if two event payloads represent the same intake/session."""
@@ -554,6 +581,8 @@ class DatastoreEngine:
elif "day_total_formatted" not in feedback:
feedback["day_total_formatted"] = open_warning
self._trigger_website_update(subject)
return {
"status": "success",
"event": record,
@@ -621,6 +650,8 @@ class DatastoreEngine:
start_dt = datetime.datetime.fromisoformat(start_str.replace("Z", "+00:00"))
duration_minutes = int((end_dt.astimezone(BERLIN_TZ) - start_dt.astimezone(BERLIN_TZ)).total_seconds() / 60)
self._trigger_website_update(subject)
return {
"status": "success",
"event_id": resolved_event_id,
@@ -637,16 +668,19 @@ class DatastoreEngine:
def delete_event(self, event_id: str) -> Dict[str, Any]:
"""Soft-deletes an event."""
now_iso = datetime.datetime.now(datetime.timezone.utc).isoformat()
row = self.con.execute("SELECT kind FROM events WHERE id = ?", [event_id]).fetchone()
row = self.con.execute("SELECT kind, subject FROM events WHERE id = ?", [event_id]).fetchone()
if not row:
raise ValueError(f"Event with id '{event_id}' not found.")
kind = row[0]
subject = row[1]
domain = kind.split(".")[0]
self.con.execute("UPDATE events SET deleted_at = ? WHERE id = ?", [now_iso, event_id])
self._update_jsonl_record(domain, event_id, {"deleted_at": now_iso})
self._trigger_website_update(subject)
return {"status": "success", "deleted_event_id": event_id}
# --------------------------------------------------------------------------
@@ -770,6 +804,16 @@ class DatastoreEngine:
if open_sleep:
res["open_sleep"] = open_sleep[0]
# 4. Diaper summary (if subject has diaper data)
dia = self.query(f"SELECT * FROM v_diaper_daily WHERE subject = '{subject}' AND log_date = '{target_date}'")
if dia:
res["diaper"] = dia[0]
# 5. Feeding summary (if subject has feeding data)
fee = self.query(f"SELECT * FROM v_feeding_daily WHERE subject = '{subject}' AND log_date = '{target_date}'")
if fee:
res["feeding"] = fee[0]
return res
def find_places_near(
@@ -829,6 +873,29 @@ class DatastoreEngine:
"total_hours": daily[0],
"nap_count": daily[1],
}
elif kind.startswith("diaper"):
daily = self.con.execute(
"SELECT total_changes, wet_count, dirty_count FROM v_diaper_daily WHERE subject = ? AND log_date = ?",
[subject, date_str],
).fetchone()
if daily:
return {
"day_total_formatted": f"Windeln ({date_str}): {daily[0]} gewechselt ({daily[1]} Pipi, {daily[2]} Stuhl)",
"total_changes": daily[0],
"wet_count": daily[1],
"dirty_count": daily[2],
}
elif kind.startswith("feeding"):
daily = self.con.execute(
"SELECT bottle_count, total_consumed_ml FROM v_feeding_daily WHERE subject = ? AND log_date = ?",
[subject, date_str],
).fetchone()
if daily:
return {
"day_total_formatted": f"Fläschchen ({date_str}): {daily[0]} Fläschchen ({int(daily[1])} ml getrunken)",
"bottle_count": daily[0],
"total_consumed_ml": daily[1],
}
return {}
# --------------------------------------------------------------------------