feat(dashboard): an Alerts widget beside the Overview; the Overview's overflow links to Alerts and Monitors (#5510)
The Alerts widget (/alerts/widget) is shaped like every widget: a verdict
("2 firing, 1 silent." / "Nothing firing: all 12 rules are quiet."), then
what is firing or silent with its reading now, what the rule fires on and
since when, then what someone acknowledged and who; a line when alert
email failed in the last 7 days or is retrying (outbox.alert_trouble),
linking admins to Settings -> System; the quiet rules counted, a covering
rule once. It sits beside the Overview in the recommended layout (6x4
each).
The Overview's "+N more" line pointed at the removed /status/; alerts now
link to /alerts/ and monitors to /monitors/.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+25
-1
@@ -35,7 +35,7 @@ from datetime import datetime, timedelta, timezone
|
||||
from email.message import EmailMessage
|
||||
from typing import TYPE_CHECKING, Callable
|
||||
|
||||
from sqlalchemy import insert, or_, select, text, update
|
||||
from sqlalchemy import and_, insert, or_, select, text, update
|
||||
|
||||
from steward.models.outbox import EmailOutbox, email_outbox_events
|
||||
|
||||
@@ -434,3 +434,27 @@ async def outgoing(db, now: datetime | None = None, limit: int = 20) -> dict:
|
||||
shown = sum(counts.get(s, 0) for s in open_states)
|
||||
return {"rows": rows, "more": max(shown - len(rows), 0), "sent": counts.get("sent", 0),
|
||||
"problems": problems}
|
||||
|
||||
|
||||
async def alert_trouble(db, now: datetime | None = None) -> dict:
|
||||
"""The dashboard Alerts widget's delivery line (#5510): alert emails of
|
||||
the last SHOWN_DAYS that failed, and those still retrying (tried, not yet
|
||||
accepted), and the line that says so ("" when neither).
|
||||
{"failed": n, "retrying": n, "said": str}."""
|
||||
from sqlalchemy import func
|
||||
now = now or datetime.now(timezone.utc)
|
||||
rows = dict((await db.execute(
|
||||
select(EmailOutbox.state, func.count())
|
||||
.where(EmailOutbox.kind == "alert", EmailOutbox.created_at >= now - timedelta(days=SHOWN_DAYS),
|
||||
or_(EmailOutbox.state == "failed",
|
||||
and_(EmailOutbox.state.in_(("pending", "sending")), EmailOutbox.attempts > 0)))
|
||||
.group_by(EmailOutbox.state))).all())
|
||||
failed = rows.get("failed", 0)
|
||||
retrying = rows.get("pending", 0) + rows.get("sending", 0)
|
||||
said = []
|
||||
if failed:
|
||||
said.append(f"{failed} alert email{'s' if failed != 1 else ''} failed in the last {SHOWN_DAYS} days")
|
||||
if retrying:
|
||||
said.append(f"{retrying} still retrying" if failed
|
||||
else f"{retrying} alert email{'s' if retrying != 1 else ''} still retrying")
|
||||
return {"failed": failed, "retrying": retrying, "said": "; ".join(said)}
|
||||
|
||||
+15
-3
@@ -70,6 +70,16 @@ WIDGET_REGISTRY: dict[str, dict] = {
|
||||
"poll": True,
|
||||
"params": [],
|
||||
},
|
||||
"alerts": {
|
||||
"key": "alerts",
|
||||
"label": "Alerts",
|
||||
"description": "What is firing or silent, its reading and since when, then what someone has acknowledged; says when alert email is failing",
|
||||
"hx_url": "/alerts/widget",
|
||||
"detail_url": "/alerts/",
|
||||
"plugin": None,
|
||||
"poll": True,
|
||||
"params": [],
|
||||
},
|
||||
"hosts_overview": {
|
||||
"key": "hosts_overview",
|
||||
"label": "Hosts — Overview",
|
||||
@@ -305,13 +315,15 @@ RETIRED = {
|
||||
"uptime_summary": "monitors", # Monitors shows 24h/7d/30d uptime per monitor (#5250)
|
||||
}
|
||||
|
||||
# The recommended dashboard (#5213): what needs attention first, then the
|
||||
# The recommended dashboard (#5213): what needs attention first (the
|
||||
# Overview, and beside it the Alerts, #5510), then the
|
||||
# fleet, then each integration. (key, width, height, config) on the 12-column
|
||||
# grid, one cell 70px tall; a widget whose plugin is off is skipped and the
|
||||
# rest close up. Seeded into the first dashboard and offered by
|
||||
# "Reset to the recommended layout" in edit mode.
|
||||
RECOMMENDED: list[tuple[str, int, int, dict]] = [
|
||||
("status_overview", 12, 3, {}),
|
||||
("status_overview", 6, 4, {}),
|
||||
("alerts", 6, 4, {}),
|
||||
("hosts_overview", 6, 7, {}),
|
||||
("monitors", 6, 7, {"show_down_only": "no", "limit": 10}),
|
||||
("docker_containers", 6, 6, {}),
|
||||
@@ -342,7 +354,7 @@ def recommended_layout(plugins_cfg: dict) -> list[dict]:
|
||||
# Group each widget for the picker (Core monitors / Monitoring capabilities /
|
||||
# Integrations), mirroring the Settings → Plugins taxonomy. Capability plugins
|
||||
# are host/monitoring facets; traefik/unifi are discrete vendor integrations.
|
||||
_CORE_WIDGETS = {"monitors", "status_overview", "hosts_overview"}
|
||||
_CORE_WIDGETS = {"monitors", "status_overview", "alerts", "hosts_overview"}
|
||||
_INTEGRATION_PLUGINS = {"traefik", "unifi"}
|
||||
GROUP_LABELS = {
|
||||
"core": "Core monitors",
|
||||
|
||||
Reference in New Issue
Block a user