feat(ui): alerts, monitors, hosts, schedules and maintenance lists put problems first, with since-times (#5210)
#5210 "Problems first, with since-time, across every list" (milestone 477), first batch. - Alerts: the Active list reads firing, no data, acknowledged, pending, the longest-running first, and re-polls itself. An acknowledged alert says how long it has been breaching (from the alert's events; acknowledging moves last_transition_at) and, apart from that, who acknowledged it and when. Rules show what each is doing now; disabled rules sort last. The current value reads the latest-value table (#5208) instead of plugin_metrics. - Monitors: a down monitor says "Down 12m · connection refused" in the list. - Hosts: sorted down, agent gone quiet, pending, healthy. The agent column shows "—" for a missing reading (was 0%) and a Stale badge with its age. agent_metrics_by_host is gone; the list reads agent_overview_by_host. - Schedules: failing first (failed or interrupted run, or a firing that could not start one), then running, then the rest, paused last; status badge from the shared states. - Maintenance windows: active (ending soonest), then upcoming (starting soonest), with expired folded away; each says when it ends/starts/ended. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -152,6 +152,20 @@ def row_state(monitor: Monitor, latest, uptime_pct, thresholds: dict) -> str:
|
||||
return "up"
|
||||
|
||||
|
||||
def down_reason(latest) -> str | None:
|
||||
"""Why a failed check failed, in a few words: the probe's error, else the
|
||||
HTTP status, else a content mismatch; None when the result says nothing."""
|
||||
if latest is None:
|
||||
return None
|
||||
if latest.error_msg:
|
||||
return latest.error_msg
|
||||
if latest.status_code:
|
||||
return f"HTTP {latest.status_code}"
|
||||
if latest.content_matched is False:
|
||||
return "expected content not found"
|
||||
return None
|
||||
|
||||
|
||||
def problems_first(rows: list[dict]) -> list[dict]:
|
||||
"""Down, then degraded, then pending, up and paused; by name within each."""
|
||||
return sorted(rows, key=lambda d: (STATE_ORDER[d["state"]], d["monitor"].name.lower()))
|
||||
@@ -191,7 +205,13 @@ async def monitor_rows(db, range_key: str, thresholds: dict):
|
||||
"title": _heartbeat_title(m.type, r)} for r in rows],
|
||||
"uptime_pct": uptime_pct,
|
||||
"tls_days": tls_days,
|
||||
"down_since": None,
|
||||
"down_reason": down_reason(latest) if latest is not None and not latest.is_up else None,
|
||||
})
|
||||
# When each down monitor went down (a query per down monitor: few, and indexed).
|
||||
for d in data:
|
||||
if d["state"] == "down":
|
||||
d["down_since"] = await down_since(db, d["monitor"].id)
|
||||
counts = {k: sum(d["state"] == k for d in data) for k in STATE_ORDER}
|
||||
counts["up"] += counts["degraded"]
|
||||
return problems_first(data), counts, range_key
|
||||
|
||||
Reference in New Issue
Block a user