feat(datastore): add diaper and feeding tracking, automated update_website posthook, and morning_briefing
This commit is contained in:
@@ -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 {}
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user