feat(datastore): support --since in stop command, trigger website update from render_views, update docs

This commit is contained in:
Moritz Graf
2026-10-11 15:07:35 +02:00
parent 967909c3a7
commit 3abfa18182
3 changed files with 55 additions and 5 deletions
+14 -2
View File
@@ -60,7 +60,9 @@ Closes the currently active open interval event (e.g. when Mila wakes up).
- **Parameters:** - **Parameters:**
- `kind` (string, required): `'sleep.session'`. - `kind` (string, required): `'sleep.session'`.
- `subject` (string, required): `'mila'`. - `subject` (string, required): `'mila'`.
- `end_time` (string, optional): ISO timestamp (defaults to current time). - `end_time` (string, optional): ISO timestamp (e.g. `'2026-10-11T11:40:00+02:00'`). Defaults to current time if omitted.
- `since` (string, optional): Relative wake time offset backwards from now (e.g. `'10m'`, `'15m'`).
- `event_id` (string, optional): Specific open event ID to close.
### 3. `datastore_get_summary` ### 3. `datastore_get_summary`
Retrieves daily nutrition, sleep, and active status totals computed by DuckDB. Retrieves daily nutrition, sleep, and active status totals computed by DuckDB.
@@ -113,10 +115,20 @@ uv run /home/node/mop-skills/mop-skills/skills/datastore/scripts/datastore.py ad
--since "15m" \ --since "15m" \
--payload '{"type": "nap", "location": "bett"}' --payload '{"type": "nap", "location": "bett"}'
# Stop Mila's nap (closes open interval) # Stop Mila's nap (closes open interval at current time)
uv run /home/node/mop-skills/mop-skills/skills/datastore/scripts/datastore.py stop sleep.session \ uv run /home/node/mop-skills/mop-skills/skills/datastore/scripts/datastore.py stop sleep.session \
--subject mila --subject mila
# Stop Mila's nap retroactively with specific wake time
uv run /home/node/mop-skills/mop-skills/skills/datastore/scripts/datastore.py stop sleep.session \
--subject mila \
--end "2026-10-11T11:40:00+02:00"
# Stop Mila's nap retroactively with relative wake time (e.g. woke up 15 min ago)
uv run /home/node/mop-skills/mop-skills/skills/datastore/scripts/datastore.py stop sleep.session \
--subject mila \
--since "15m"
# Query daily summary # Query daily summary
uv run /home/node/mop-skills/mop-skills/skills/datastore/scripts/datastore.py summary --subject moritz uv run /home/node/mop-skills/mop-skills/skills/datastore/scripts/datastore.py summary --subject moritz
+27 -3
View File
@@ -594,10 +594,14 @@ class DatastoreEngine:
kind: str, kind: str,
subject: str, subject: str,
end_time: Optional[str] = None, end_time: Optional[str] = None,
since: Optional[str] = None,
event_id: Optional[str] = None, event_id: Optional[str] = None,
actor: Optional[str] = None, actor: Optional[str] = None,
) -> Dict[str, Any]: ) -> Dict[str, Any]:
"""Finds the open event (end IS NULL) for kind and subject, and closes it.""" """Finds the open event (end IS NULL) for kind and subject, and closes it."""
if since and end_time:
raise ValueError("Cannot specify both 'since' and 'end_time'. Use 'since' for relative times or 'end_time' for absolute times.")
if event_id: if event_id:
row = self.con.execute( row = self.con.execute(
""" """
@@ -622,13 +626,22 @@ class DatastoreEngine:
resolved_event_id, start_str = row[0], str(row[1]) resolved_event_id, start_str = row[0], str(row[1])
now_dt = datetime.datetime.now(datetime.timezone.utc) now_dt = datetime.datetime.now(datetime.timezone.utc)
if end_time: if since:
delta = parse_duration(since)
end_dt = (now_dt - delta).astimezone(BERLIN_TZ)
elif end_time:
end_dt = datetime.datetime.fromisoformat(end_time) end_dt = datetime.datetime.fromisoformat(end_time)
if end_dt.tzinfo is None: if end_dt.tzinfo is None:
end_dt = end_dt.replace(tzinfo=BERLIN_TZ) end_dt = end_dt.replace(tzinfo=BERLIN_TZ)
else: else:
end_dt = now_dt.astimezone(BERLIN_TZ) end_dt = now_dt.astimezone(BERLIN_TZ)
start_dt = datetime.datetime.fromisoformat(start_str.replace("Z", "+00:00"))
if start_dt.tzinfo is None:
start_dt = start_dt.replace(tzinfo=BERLIN_TZ)
if end_dt < start_dt:
raise ValueError(f"End time {end_dt.isoformat()} cannot be earlier than start time {start_dt.isoformat()}")
end_iso = end_dt.isoformat() end_iso = end_dt.isoformat()
now_iso = now_dt.isoformat() now_iso = now_dt.isoformat()
@@ -985,6 +998,8 @@ def run_mcp_server():
"kind": {"type": "string", "description": "e.g. 'sleep.session'"}, "kind": {"type": "string", "description": "e.g. 'sleep.session'"},
"subject": {"type": "string", "description": "'mila', 'moritz', 'amelie'"}, "subject": {"type": "string", "description": "'mila', 'moritz', 'amelie'"},
"end_time": {"type": "string", "description": "ISO timestamp (optional, defaults to now)"}, "end_time": {"type": "string", "description": "ISO timestamp (optional, defaults to now)"},
"since": {"type": "string", "description": "Relative end offset backwards from now (e.g. '5m', '15m')"},
"event_id": {"type": "string", "description": "Specific open event ID to close (optional)"},
}, },
"required": ["kind", "subject"], "required": ["kind", "subject"],
}, },
@@ -1100,6 +1115,8 @@ def run_mcp_server():
kind=args["kind"], kind=args["kind"],
subject=args["subject"], subject=args["subject"],
end_time=args.get("end_time"), end_time=args.get("end_time"),
since=args.get("since"),
event_id=args.get("event_id"),
) )
elif name == "datastore_get_summary": elif name == "datastore_get_summary":
data = engine.get_summary(subject=args["subject"], date_str=args.get("date")) data = engine.get_summary(subject=args["subject"], date_str=args.get("date"))
@@ -1177,7 +1194,8 @@ def run_cli():
p_stop.add_argument("--kind", dest="kind_flag", help="e.g. sleep.session") p_stop.add_argument("--kind", dest="kind_flag", help="e.g. sleep.session")
p_stop.add_argument("--subject", required=True, help="mila, moritz") p_stop.add_argument("--subject", required=True, help="mila, moritz")
p_stop.add_argument("--id", dest="event_id", help="Specific event ID to close") p_stop.add_argument("--id", dest="event_id", help="Specific event ID to close")
p_stop.add_argument("--end", help="ISO end time (defaults to now)") p_stop.add_argument("--end", dest="end_time", help="ISO end time (defaults to now)")
p_stop.add_argument("--since", help="Relative end offset backwards from now (e.g. '5m', '15m')")
# Query # Query
p_query = subparsers.add_parser("query", help="Execute read-only SQL query") p_query = subparsers.add_parser("query", help="Execute read-only SQL query")
@@ -1226,7 +1244,13 @@ def run_cli():
kind = args.kind or args.kind_flag kind = args.kind or args.kind_flag
if not kind: if not kind:
parser.error("the 'stop' command requires 'kind' (positional or via --kind)") parser.error("the 'stop' command requires 'kind' (positional or via --kind)")
res = engine.stop_event(kind=kind, subject=args.subject, end_time=args.end, event_id=getattr(args, "event_id", None)) res = engine.stop_event(
kind=kind,
subject=args.subject,
end_time=getattr(args, "end_time", None),
since=getattr(args, "since", None),
event_id=getattr(args, "event_id", None),
)
print(json.dumps(res, indent=2, ensure_ascii=False)) print(json.dumps(res, indent=2, ensure_ascii=False))
elif args.command == "query": elif args.command == "query":
res = engine.query(args.sql) res = engine.query(args.sql)
+14
View File
@@ -579,6 +579,20 @@ def main():
if not args.no_html: if not args.no_html:
render_html_dashboards(engine, html_dir) render_html_dashboards(engine, html_dir)
# Automatically trigger update_website.py for live publishing to hosting
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.run(cmd, check=False)
except Exception as e:
print(f"Warning: could not trigger update_website.py from render_views: {e}")
if __name__ == "__main__": if __name__ == "__main__":
main() main()