feat(alerts): alert history page — transitions, deliveries, mutes and actions (#5066)
CI / lint (push) Successful in 3s
CI / integration (push) Successful in 50s
CI / unit (push) Successful in 1m7s
CI / publish (push) Successful in 1m4s

#5066 "Alert history page". /alerts/history lists every alert transition
in a range (1h–30d, the shared range picker), newest first, all rules or
one (?rule=). Each row: from → to, the value against the rule's condition
("no data" for a silent one), email and webhook sent or failed with the
error, "muted · <window>" (and "notified when the window ended") taken out
of the event note, other notes, and a link to the Ansible run an action
started. Range and rule changes swap only the rows (HTMX). Viewer role.

The Alerts page links each rule's history and the whole history. The query
moves to alert_svc.history, which MCP get_alert_history now uses too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-06 14:06:04 -04:00
co-authored by Claude Opus 5.5
parent c9f5c049db
commit dc22a35e9d
8 changed files with 302 additions and 12 deletions
+3 -11
View File
@@ -7,7 +7,7 @@ from sqlalchemy import select
from steward.alerts import service as alert_svc
from steward.mcp._context import current_caller
from steward.models.alerts import AlertEvent, AlertOperator, AlertRule, AlertState, MaintenanceWindow
from steward.models.alerts import AlertOperator, AlertRule, AlertState, MaintenanceWindow
from steward.models.users import User, UserRole
from ._audit import audit_mcp
@@ -95,16 +95,8 @@ async def get_alert_history(rule: str = "", range: str = "7d", limit: int = 50)
limit = clamp_limit(limit, 50, 500)
async with db_session() as db:
rules = await alert_svc.list_rules(db)
stmt = (select(AlertEvent, AlertRule)
.join(AlertRule, AlertEvent.rule_id == AlertRule.id)
.where(AlertEvent.transitioned_at >= since)
.order_by(AlertEvent.transitioned_at.desc())
.limit(limit + 1))
chosen = None
if rule:
chosen = pick(rule, rules, kind="alert rule", list_tool="list_alert_rules")
stmt = stmt.where(AlertEvent.rule_id == chosen.id)
rows = (await db.execute(stmt)).all()
chosen = pick(rule, rules, kind="alert rule", list_tool="list_alert_rules") if rule else None
rows = await alert_svc.history(db, since, chosen.id if chosen else None, limit)
events = [
{
"rule_id": r.id,