From 63497765b0b25116f960cbbda07b53a0433217b0 Mon Sep 17 00:00:00 2001 From: Moritz Graf Date: Sun, 11 Oct 2026 22:14:17 +0200 Subject: [PATCH] fix(datastore): include morning-ending night sleep in dashboard, pass data-dir, and auto git-sync on writes --- skills/datastore/scripts/datastore.py | 22 ++++++++++++++++++++-- skills/datastore/scripts/update_website.py | 22 +++++++++++++++++----- 2 files changed, 37 insertions(+), 7 deletions(-) diff --git a/skills/datastore/scripts/datastore.py b/skills/datastore/scripts/datastore.py index 025ea4d..9eef8e5 100755 --- a/skills/datastore/scripts/datastore.py +++ b/skills/datastore/scripts/datastore.py @@ -295,9 +295,9 @@ class DatastoreEngine: 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)] + cmd = [str(uv_bin), "run", str(update_script), "--data-dir", str(self.data_dir)] else: - cmd = [sys.executable, str(update_script)] + cmd = [sys.executable, str(update_script), "--data-dir", str(self.data_dir)] subprocess.Popen( cmd, stdout=subprocess.DEVNULL, @@ -306,6 +306,19 @@ class DatastoreEngine: except Exception as e: logger.warning(f"Could not trigger {script_name}: {e}") + def _git_sync(self, commit_msg: str): + """Safely commits and pushes data changes if data_dir is inside a git repository.""" + try: + repo_dir = self.data_dir.parent if (self.data_dir.parent / ".git").is_dir() else self.data_dir + if not (repo_dir / ".git").is_dir(): + return + subprocess.run(["git", "-C", str(repo_dir), "add", str(self.data_dir)], check=False, capture_output=True) + res = subprocess.run(["git", "-C", str(repo_dir), "commit", "-m", f"{commit_msg} [skip ci]"], check=False, capture_output=True) + if res.returncode == 0: + subprocess.run(["git", "-C", str(repo_dir), "push", "origin", "main"], check=False, capture_output=True, timeout=10) + except Exception as e: + logger.debug(f"Git auto-sync skipped/failed: {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: @@ -607,6 +620,7 @@ class DatastoreEngine: feedback["day_total_formatted"] = open_warning self._trigger_website_update(subject) + self._git_sync(f"chore(data): log {kind} for {subject}") return { "status": "success", @@ -689,6 +703,7 @@ class DatastoreEngine: duration_minutes = int((end_dt.astimezone(BERLIN_TZ) - start_dt.astimezone(BERLIN_TZ)).total_seconds() / 60) self._trigger_website_update(subject) + self._git_sync(f"chore(data): stop {kind} for {subject}") return { "status": "success", @@ -718,6 +733,7 @@ class DatastoreEngine: self._update_jsonl_record(domain, event_id, {"deleted_at": now_iso}) self._trigger_website_update(subject) + self._git_sync(f"chore(data): delete event {event_id} ({kind}) for {subject}") return {"status": "success", "deleted_event_id": event_id} @@ -779,6 +795,8 @@ class DatastoreEngine: values, ) + self._git_sync(f"chore(data): update entity {collection}/{entity_id}") + return {"status": "success", "collection": collection, "id": entity_id} # -------------------------------------------------------------------------- diff --git a/skills/datastore/scripts/update_website.py b/skills/datastore/scripts/update_website.py index a4dd062..11a7948 100644 --- a/skills/datastore/scripts/update_website.py +++ b/skills/datastore/scripts/update_website.py @@ -151,7 +151,10 @@ def generate_dashboard_html(engine: DatastoreEngine) -> str: CASE WHEN is_open THEN NULL ELSE strftime(local_end, '%H:%M') END AS end_fmt, sleep_type, location, note, duration_minutes, is_open FROM v_sleep - WHERE subject = 'mila' AND log_date = '{today_str}' + WHERE subject = 'mila' AND ( + log_date = '{today_str}' + OR (sleep_type = 'night' AND CAST(local_end AS DATE) = '{today_str}') + ) ORDER BY start ASC """ ).fetchall() @@ -173,12 +176,21 @@ def generate_dashboard_html(engine: DatastoreEngine) -> str: """ ).fetchone() - today_tot_hours = f"{today_agg[0]:.2f} h" if today_agg and today_agg[0] else "0 h" - today_night_min = today_agg[2] if today_agg else 0 + # Prefer the completed night sleep that ended this morning/today for the top card + morning_night = next((s for s in today_nights if not s[7] and s[6] is not None), None) + if morning_night and morning_night[6]: + today_night_min = morning_night[6] + else: + today_night_min = today_agg[2] if today_agg else 0 + + today_nap_count = today_agg[1] if today_agg else len(today_naps) + today_nap_min = sum(n[6] or 0 for n in today_naps) + + today_tot_min = today_night_min + today_nap_min + today_tot_hours = f"{today_tot_min / 60.0:.2f} h" if today_tot_min > 0 else "0 h" + today_night_h, today_night_m = divmod(today_night_min, 60) today_night_dur_str = f"{today_night_h} h {today_night_m:02d} min" if today_night_h > 0 else f"{today_night_m} min" - today_nap_count = today_agg[1] if today_agg else 0 - today_nap_min = today_agg[3] if today_agg else 0 # 3. Today's Feedings (conditional) today_feedings = con.execute(