113 lines
3.7 KiB
Python
113 lines
3.7 KiB
Python
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
|