Files
patrickandClaude Sonnet 5.5 347f55cf7c
test / pytest (3.11) (push) Canceled after 0s
test / pytest (3.13) (push) Canceled after 0s
feat: Phase 6c baseline, ack, host alarms, outage notice, send limit, privacy mode, UI link
Includes atomic migrations, per-rule reminder semantics, first-run helper script
and the note on the undecided UI chat idea.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-06 03:06:23 +01:00

80 lines
3.8 KiB
Python

from dataclasses import dataclass
from typing import Sequence
from ..db import Database
from ..models import STATE_OK, Key, StateRow
RECENT_CHANGES_DEFAULT = 5
# last_reminder_ts bleibt beim Update unangetastet, damit der Erinnerungs-Worker nicht überschrieben wird.
UPSERT_SQL = (
"INSERT INTO service_states(host, service, state, output, last_state_change, in_downtime, "
"flapping, reported_state, flap_reported, problem_since, reported_at, reported_since, "
"last_reminder_ts, ack_state, host_held) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) "
"ON CONFLICT(host, service) DO UPDATE SET state=excluded.state, output=excluded.output, "
"last_state_change=excluded.last_state_change, in_downtime=excluded.in_downtime, "
"flapping=excluded.flapping, reported_state=excluded.reported_state, "
"flap_reported=excluded.flap_reported, problem_since=excluded.problem_since, "
"reported_at=excluded.reported_at, reported_since=excluded.reported_since, "
"ack_state=excluded.ack_state, host_held=excluded.host_held"
)
LOG_SQL = "INSERT INTO state_log(host, service, state, ts) VALUES (?,?,?,?)"
@dataclass(frozen=True)
class StateChange:
ts: int
state: int
def _from_db(row) -> StateRow:
return StateRow(row["host"], row["service"], row["state"], row["output"], row["last_state_change"],
bool(row["in_downtime"]), bool(row["flapping"]), row["reported_state"],
bool(row["flap_reported"]), row["problem_since"], row["reported_at"],
row["reported_since"], row["last_reminder_ts"], row["ack_state"], bool(row["host_held"]))
class StateStore:
def __init__(self, db: Database):
self._db = db
def load(self) -> dict[Key, StateRow]:
rows = self._db.query("SELECT * FROM service_states")
return {(r["host"], r["service"]): _from_db(r) for r in rows}
def apply(self, previous: dict[Key, StateRow], rows: list[StateRow], now: int,
extra: Sequence[tuple[str, tuple]] = ()) -> None:
statements: list[tuple[str, tuple]] = []
new_keys = {row.key for row in rows}
for key, prev in previous.items():
if key in new_keys:
continue
statements.append(("DELETE FROM service_states WHERE host=? AND service=?", key))
if prev.state != STATE_OK:
statements.append((LOG_SQL, (*key, STATE_OK, now)))
for row in rows:
prev = previous.get(row.key)
if prev == row:
continue
if prev is None or prev.state != row.state:
statements.append((LOG_SQL, (row.host, row.service, row.state, row.last_state_change or now)))
statements.append((UPSERT_SQL, (row.host, row.service, row.state, row.output,
row.last_state_change, int(row.in_downtime), int(row.flapping),
row.reported_state, int(row.flap_reported), row.problem_since,
row.reported_at, row.reported_since, row.last_reminder_ts,
row.ack_state, int(row.host_held))))
statements.extend(extra)
if statements:
self._db.execute_many(statements)
def mark_reminded(self, host: str, service: str, ts: int) -> None:
self._db.execute("UPDATE service_states SET last_reminder_ts=? WHERE host=? AND service=?",
(ts, host, service))
def recent_changes(self, host: str, service: str, limit: int = RECENT_CHANGES_DEFAULT) -> list[StateChange]:
rows = self._db.query(
"SELECT ts, state FROM state_log WHERE host=? AND service=? ORDER BY ts DESC, id DESC LIMIT ?",
(host, service, limit),
)
return [StateChange(r["ts"], r["state"]) for r in rows]