Files
checkbot/app/db.py
T
patrickandClaude Sonnet 5.5 9c4f7366eb
test / pytest (3.11) (push) Waiting to run
test / pytest (3.13) (push) Waiting to run
feat: Phase 6 prompt versioning with test area, retention
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-06 02:17:01 +01:00

201 lines
6.4 KiB
Python

import sqlite3
import threading
from pathlib import Path
MIGRATIONS = [
"""
CREATE TABLE settings (
key TEXT PRIMARY KEY,
value TEXT NOT NULL,
is_secret INTEGER NOT NULL DEFAULT 0,
updated_at INTEGER NOT NULL
);
CREATE TABLE audit (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ts INTEGER NOT NULL,
user TEXT NOT NULL,
action TEXT NOT NULL,
target TEXT NOT NULL,
old_value TEXT,
new_value TEXT
);
CREATE INDEX audit_ts ON audit(ts);
""",
"""
CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL UNIQUE COLLATE NOCASE,
password_hash TEXT NOT NULL,
role TEXT NOT NULL CHECK (role IN ('admin', 'viewer')),
totp_secret TEXT,
session_version INTEGER NOT NULL DEFAULT 0,
disabled INTEGER NOT NULL DEFAULT 0,
created_at INTEGER NOT NULL
);
""",
"""
CREATE TABLE service_states (
host TEXT NOT NULL,
service TEXT NOT NULL,
state INTEGER NOT NULL,
output TEXT NOT NULL DEFAULT '',
last_state_change INTEGER NOT NULL,
in_downtime INTEGER NOT NULL DEFAULT 0,
flapping INTEGER NOT NULL DEFAULT 0,
reported_state INTEGER NOT NULL DEFAULT 0,
flap_reported INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (host, service)
);
CREATE TABLE state_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
host TEXT NOT NULL,
service TEXT NOT NULL,
state INTEGER NOT NULL,
ts INTEGER NOT NULL
);
CREATE INDEX state_log_lookup ON state_log(host, service, ts);
CREATE TABLE alerts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ts INTEGER NOT NULL,
host TEXT NOT NULL,
service TEXT NOT NULL,
kind TEXT NOT NULL,
state INTEGER NOT NULL,
output TEXT NOT NULL DEFAULT '',
suppressed INTEGER NOT NULL DEFAULT 0,
suppress_reason TEXT,
ai_status TEXT NOT NULL DEFAULT 'skipped'
CHECK (ai_status IN ('pending', 'done', 'failed', 'skipped')),
ai_text TEXT,
message_group TEXT NOT NULL
);
CREATE INDEX alerts_ts ON alerts(ts);
CREATE INDEX alerts_group ON alerts(message_group);
CREATE TABLE deliveries (
id INTEGER PRIMARY KEY AUTOINCREMENT,
alert_id INTEGER NOT NULL REFERENCES alerts(id) ON DELETE CASCADE,
backend TEXT NOT NULL,
target TEXT NOT NULL,
message_id TEXT NOT NULL
);
CREATE INDEX deliveries_alert ON deliveries(alert_id);
""",
"""
CREATE TABLE routes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
position INTEGER NOT NULL,
enabled INTEGER NOT NULL DEFAULT 1,
match_host TEXT NOT NULL DEFAULT '',
match_service TEXT NOT NULL DEFAULT '',
min_state INTEGER NOT NULL DEFAULT 1 CHECK (min_state BETWEEN 0 AND 3),
kinds TEXT NOT NULL DEFAULT 'problem,recovery,flapping',
stop_on_match INTEGER NOT NULL DEFAULT 0,
targets TEXT NOT NULL DEFAULT '[]'
);
CREATE INDEX routes_position ON routes(position);
CREATE TABLE mutes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
host_glob TEXT NOT NULL,
service_glob TEXT NOT NULL,
expires_at INTEGER NOT NULL,
reason TEXT NOT NULL DEFAULT '',
created_by TEXT NOT NULL,
created_at INTEGER NOT NULL,
source TEXT NOT NULL CHECK (source IN ('ui', 'chat'))
);
CREATE INDEX mutes_expires ON mutes(expires_at);
""",
"""
CREATE TABLE agent_runs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ts INTEGER NOT NULL,
backend TEXT NOT NULL,
sender TEXT NOT NULL,
command TEXT NOT NULL,
question TEXT NOT NULL,
answer TEXT NOT NULL,
abort_reason TEXT,
steps TEXT NOT NULL DEFAULT '[]'
);
CREATE INDEX agent_runs_ts ON agent_runs(ts);
CREATE TABLE listener_state (
key TEXT PRIMARY KEY,
value TEXT NOT NULL
);
""",
"""
CREATE TABLE prompt_versions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
version INTEGER NOT NULL,
text TEXT NOT NULL,
note TEXT NOT NULL DEFAULT '',
created_by TEXT NOT NULL,
created_at INTEGER NOT NULL,
UNIQUE (name, version)
);
CREATE TABLE prompt_active (
name TEXT PRIMARY KEY,
version INTEGER NOT NULL
);
CREATE TABLE retention_state (
id INTEGER PRIMARY KEY CHECK (id = 1),
ts INTEGER NOT NULL,
origin TEXT NOT NULL,
counts TEXT NOT NULL
);
CREATE INDEX state_log_ts ON state_log(ts);
""",
]
class Database:
def __init__(self, path):
if str(path) != ":memory:":
Path(path).parent.mkdir(parents=True, exist_ok=True)
self._conn = sqlite3.connect(str(path), check_same_thread=False)
self._conn.row_factory = sqlite3.Row
self._conn.execute("PRAGMA journal_mode=WAL")
self._conn.execute("PRAGMA foreign_keys=ON")
self._lock = threading.Lock()
self._migrate()
def _migrate(self) -> None:
with self._lock:
version = self._conn.execute("PRAGMA user_version").fetchone()[0]
for index in range(version, len(MIGRATIONS)):
self._conn.executescript(MIGRATIONS[index])
self._conn.execute(f"PRAGMA user_version={index + 1}")
self._conn.commit()
def execute(self, sql: str, params: tuple = ()) -> int:
with self._lock:
cursor = self._conn.execute(sql, params)
self._conn.commit()
return cursor.rowcount
def insert(self, sql: str, params: tuple = ()) -> int:
with self._lock:
cursor = self._conn.execute(sql, params)
self._conn.commit()
return cursor.lastrowid
def execute_many(self, statements: list[tuple[str, tuple]]) -> None:
with self._lock:
try:
for sql, params in statements:
self._conn.execute(sql, params)
self._conn.commit()
except Exception:
self._conn.rollback()
raise
def query(self, sql: str, params: tuple = ()) -> list[sqlite3.Row]:
with self._lock:
return self._conn.execute(sql, params).fetchall()
def close(self) -> None:
with self._lock:
self._conn.close()