feat(dashboard): an Alerts widget beside the Overview; the Overview's overflow links to Alerts and Monitors (#5510)
CI / lint (push) Successful in 3s
CI / integration (push) Successful in 1m16s
CI / unit (push) Failing after 1m28s
CI / publish (push) Skipped

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:
2026-10-09 16:27:17 -04:00
co-authored by Claude Opus 5.5
parent defad2e98b
commit 57e1960dc1
10 changed files with 279 additions and 13 deletions
+27 -4
View File
@@ -28,8 +28,11 @@ def test_retired_widgets_point_at_the_one_that_replaced_them():
def test_the_recommended_layout_fills_rows_without_overlap():
items = recommended_layout(ALL_ON)
assert [i["key"] for i in items] == [key for key, *_ in RECOMMENDED]
assert items[0] == {"key": "status_overview", "x": 0, "y": 0, "w": 12, "h": 3, "config": {}}
assert (items[1]["x"], items[1]["y"], items[2]["x"], items[2]["y"]) == (0, 3, 6, 3)
# The Overview and the Alerts side by side first (#5510), then the fleet.
assert items[0] == {"key": "status_overview", "x": 0, "y": 0, "w": 6, "h": 4, "config": {}}
assert items[1] == {"key": "alerts", "x": 6, "y": 0, "w": 6, "h": 4, "config": {}}
assert [(i["key"], i["x"], i["y"]) for i in items[2:4]] == [
("hosts_overview", 0, 4), ("monitors", 6, 4)]
for i, a in enumerate(items):
assert a["x"] + a["w"] <= 12
assert not any(_overlap(a, b) for b in items[i + 1:]), a
@@ -37,8 +40,8 @@ def test_the_recommended_layout_fills_rows_without_overlap():
def test_the_recommended_layout_skips_what_is_not_enabled():
items = recommended_layout({})
assert [i["key"] for i in items] == ["status_overview", "hosts_overview", "monitors"]
assert [(i["x"], i["y"]) for i in items] == [(0, 0), (0, 3), (6, 3)]
assert [i["key"] for i in items] == ["status_overview", "alerts", "hosts_overview", "monitors"]
assert [(i["x"], i["y"]) for i in items] == [(0, 0), (6, 0), (0, 4), (6, 4)]
def test_overview_alerts_put_the_breaching_first():
@@ -77,6 +80,26 @@ async def test_the_overview_labels_its_counts_and_lists_what_is_wrong(app):
assert not hex_colours(page), hex_colours(page)
async def test_the_overviews_overflow_links_to_alerts_and_to_monitors(app):
"""#5510: what doesn't fit goes to its own list; /status/ is gone."""
now = datetime.now(timezone.utc)
alerts = [{"rule": SimpleNamespace(id=f"r{i}", name=f"rule {i}"), "label": "firing", "since": now}
for i in range(7)]
entries = [StatusEntry(kind="tcp", key=str(i), name=f"m{i}", status="down", detail_url=f"/monitors/{i}")
for i in range(10)]
async with app.test_request_context("/status/widget"):
session["user_id"] = "u1"
page = await render_template("status/widget.html", entries=entries, up=0, down=10, pending=0,
total_hosts=1, alerts=alerts, widget_id="1")
assert '<a href="/alerts/">+1 more alerts</a> · <a href="/monitors/">+2 more monitors</a>' in page
assert "/status/" not in page
async with app.test_request_context("/status/widget"):
page = await render_template("status/widget.html", entries=entries[:3], up=0, down=3, pending=0,
total_hosts=1, alerts=alerts, widget_id="1")
assert "+1 more alerts" in page and "more monitors" not in page
assert 'href="/alerts/"' not in page # the share view has no links
async def test_a_quiet_overview_says_so(app):
async with app.test_request_context("/status/widget"):
page = await render_template("status/widget.html", entries=[], up=0, down=0, pending=0,