from __future__ import annotations import json import sqlite3 from collections.abc import Callable from datetime import UTC, datetime from pathlib import Path from typing import Any class ConsoleHistoryStore: def __init__( self, db_path: str | Path = "tasks/host_console_history.sqlite3", *, limit: int = 200, now: Callable[[], datetime] | None = None, ) -> None: self.db_path = Path(db_path) self.limit = limit self._now = now or (lambda: datetime.now(UTC)) self.db_path.parent.mkdir(parents=True, exist_ok=True) self._ensure_schema() def record_assignment( self, *, task_id: str, attempt: int, status: str, failure_reason: str | None, device_id: str, ) -> None: summary = f"{task_id} attempt {attempt} on {device_id}: {status}" if failure_reason: summary = f"{summary} ({failure_reason})" detail = { "task_id": task_id, "attempt": attempt, "status": status, "failure_reason": failure_reason, "device_id": device_id, } self._insert("assignment", summary, detail) def record_heartbeat(self, *, device_count: int) -> None: summary = f"heartbeat: {device_count} devices" detail = {"device_count": device_count} self._insert("heartbeat", summary, detail) def record_policy_sync(self, *, revision: int) -> None: self._insert( "host_policy", f"host policy synchronized: revision {revision}", {"revision": revision}, ) def list_recent(self, limit: int | None = None) -> list[dict[str, Any]]: effective_limit = limit if limit is not None else self.limit with self._connect() as connection: rows = connection.execute( "select kind, occurred_at, summary, detail_json" " from history_entries order by id desc limit ?", (effective_limit,), ).fetchall() return [ { "kind": row["kind"], "occurred_at": row["occurred_at"], "summary": row["summary"], "detail": json.loads(row["detail_json"]), } for row in rows ] def _insert(self, kind: str, summary: str, detail: dict[str, Any]) -> None: occurred_at = self._now().isoformat() with self._connect() as connection: connection.execute( """ insert into history_entries (kind, occurred_at, summary, detail_json) values (?, ?, ?, ?) """, (kind, occurred_at, summary, json.dumps(detail, ensure_ascii=False)), ) connection.execute( """ delete from history_entries where id not in ( select id from history_entries order by id desc limit ? ) """, (self.limit,), ) def _ensure_schema(self) -> None: with self._connect() as connection: connection.execute( """ create table if not exists history_entries ( id integer primary key autoincrement, kind text not null, occurred_at text not null, summary text not null, detail_json text not null ) """ ) def _connect(self) -> sqlite3.Connection: connection = sqlite3.connect(self.db_path) connection.row_factory = sqlite3.Row return connection