feat(alerts): a covering rule's hosts fold away behind its "N hosts" (#5483)
CI / lint (push) Successful in 2s
CI / integration (push) Successful in 1m7s
CI / unit (push) Successful in 1m26s
CI / publish (push) Successful in 1m9s

#5483 "Alerts list: a covering rule's hosts start collapsed behind a toggle;
no dashed rows", from the operator's review of the Alerts list. Each rule is
its own tbody; its hosts sit in another, hidden until "N hosts" is pressed.
A rule with a host that isn't quiet opens on its own and names the host where
"last fired" was, so a problem never hides behind the fold. Host rows lose the
dashed rules for a sunken band under the indent.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-09 10:40:17 -04:00
co-authored by Claude Opus 5.5
parent 731e7cd6e6
commit a642b4c37c
4 changed files with 55 additions and 11 deletions
+6 -1
View File
@@ -139,6 +139,7 @@ async def list_alerts():
# pressing of their states.
rule_state = {r.id: alert_svc.alert_label(states[r.id]) for r in rules if r.id in states}
children: dict[str, list] = {}
uneasy: dict[str, list[str]] = {}
for r in rules:
if r.parent_id:
children.setdefault(r.parent_id, []).append(r)
@@ -149,11 +150,15 @@ async def list_alerts():
rule_state[parent_id] = min(
(lb for lb in labels if lb and lb not in ("inactive", "resolved")),
key=lambda lb: alert_svc.ACTIVE_ORDER.get(lb, 9), default="inactive")
# The hosts that aren't quiet: named on the rule's row, and they keep
# its hosts open (#5483).
uneasy[parent_id] = [k.resource_name for k, lb in zip(kids, labels)
if k.enabled and lb and lb not in ("inactive", "resolved")]
fired[parent_id] = max((fired[k.id] for k in kids if k.id in fired), default=None)
rules = sorted((r for r in rules if not r.parent_id),
key=lambda r: (not r.enabled, r.name.lower()))
return await render_template(
"alerts/list.html", active=active, rules=rules, children=children,
"alerts/list.html", active=active, rules=rules, children=children, uneasy=uneasy,
rule_state=rule_state, last_fired={k: v for k, v in fired.items() if v},
notify=await _notify(), operators=_OPERATORS,
poll_interval=current_app.config.get("MONITORS_POLL_INTERVAL", 60))