feat(alerts): history reads as episodes; tests follow the confirm dialog (#5316, #5314)
CI / lint (push) Successful in 3s
CI / integration (push) Failing after 54s
CI / unit (push) Successful in 1m34s
CI / publish (push) Skipped

History (#5316):
- Transitions fold into episodes (steward/alerts/episodes.py): each stretch
  out of quiet, the worst state it reached, how long it lasted, its worst
  reading against the rule ('3m 21s (fires above 3m)'), who was told across
  its changes, and its Ansible runs. The changes themselves fold under it.
- An episode cut by paging says so ('cleared later (above)', 'earlier').
- For one rule: a verdict ('Fired 4 times in the last 7d, 1h 2m in all') and
  no Rule column. Notified and Action show only when the range has any.

Tests:
- The confirm guard looks for a confirm( call, not the word in the comment
  that says it is gone (lesson #4365).
- The Hosts viewer test looks for the add-host dialog by id, since every page
  now carries the confirm dialog.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-07 23:02:01 -04:00
co-authored by Claude Opus 5.5
parent 0c66c1e7d3
commit 531a193bd1
11 changed files with 346 additions and 61 deletions
+15
View File
@@ -299,6 +299,21 @@ async def history(db, since: datetime, rule_id: str | None = None,
return [tuple(row) for row in (await db.execute(stmt)).all()]
async def history_columns(db, since: datetime, rule_id: str | None = None) -> tuple[bool, bool]:
"""(whether any transition in the range notified someone or was muted,
whether any started an Ansible run): the history page shows its Notified
and Action columns only then (#5316)."""
from steward.core.alerts import MUTED
base = select(AlertEvent.id).where(AlertEvent.transitioned_at >= since)
if rule_id:
base = base.where(AlertEvent.rule_id == rule_id)
notified = await db.scalar(select(base.where(or_(
AlertEvent.email_sent.is_not(None), AlertEvent.webhook_sent.is_not(None),
AlertEvent.note.like(f"%{MUTED}%"))).exists()))
acted = await db.scalar(select(base.where(AlertEvent.ansible_run_id.is_not(None)).exists()))
return bool(notified), bool(acted)
async def list_maintenance_windows(db) -> list[MaintenanceWindow]:
"""Every window, latest start first."""
return list((await db.execute(