feat(alerts): alerts that arrive together go out as one email (#5465)
CI / lint (push) Successful in 2s
CI / integration (push) Successful in 1m9s
CI / unit (push) Successful in 1m26s
CI / publish (push) Successful in 1m13s

#5465 "One email per burst of alerts". When a switch reboots, every host's
agent-offline rule fires at once and each account got one email per rule.
Alert email now waits 30 s from the first alert: each recipient then gets one
message listing every alert in the window, oldest first, in their own zone
("[Steward] 3 alerts: 2 firing, 1 resolved"). One alert alone keeps its
usual email. Webhooks still post per alert, at once. Every alert's event
records the email's outcome. A burst held when Steward stops is not sent;
the alerts stay on the record and on the Alerts page.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-09 08:29:52 -04:00
co-authored by Claude Opus 5.5
parent 48a0de5600
commit 997aa50939
6 changed files with 264 additions and 20 deletions
+31
View File
@@ -0,0 +1,31 @@
{# Several alerts that arrived together (core.alert_burst, #5465), as one
email's HTML part: alerts = [mail.alert_facts()], oldest first; said = how
many of each ("3 firing, 1 resolved"); link = the alerts page or "". #}
{% extends "_layout.html" %}
{% from "_parts.html" import badge, button with context %}
{% block title %}{{ alerts|length }} alerts: {{ said }}{% endblock %}
{% block body %}
<tr><td style="padding:14px 24px 4px 24px;font:600 20px/1.35 {{ c.font }};color:{{ c.text }};">{{ alerts|length }} alerts changed together: {{ said }}.</td></tr>
<tr><td style="padding:2px 24px 6px 24px;font:14px/1.5 {{ c.font }};color:{{ c.muted }};">Gathered into one email because they arrived within moments of each other.</td></tr>
<tr><td style="padding:6px 24px 4px 24px;">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="border-top:1px solid {{ c.border }};">
{% for a in alerts %}
<tr><td style="padding:10px 0;border-bottom:1px solid {{ c.border }};">
<div>{{ badge(a.tone, a.state) }}</div>
<div style="padding-top:6px;font:600 15px/1.4 {{ c.font }};color:{{ c.text }};">{{ a.headline }}</div>
<div style="padding-top:2px;font:13px/1.5 {{ c.font }};color:{{ c.muted }};">{{ a.rule }} · fires when {{ a.condition }} · {{ a.when }}</div>
{% if a.note %}<div style="padding-top:2px;font:13px/1.5 {{ c.font }};color:{{ c.muted }};">{{ a.note }}</div>{% endif %}
</td></tr>
{% endfor %}
</table>
</td></tr>
{% if link %}
<tr><td style="padding:16px 24px 24px 24px;">{{ button(link, "View in Steward") }}</td></tr>
{% else %}
<tr><td style="padding:0 0 12px 0;"></td></tr>
{% endif %}
{% endblock %}
{% block footer %}
Steward sends this to everyone on its email list when alert rules change state.
Who gets it is set in Settings › Notifications.
{% endblock %}
+18
View File
@@ -0,0 +1,18 @@
{# Several alerts that arrived together, plain text; the same facts as burst.html. #}
{{ alerts|length }} alerts changed together: {{ said }}.
{% for a in alerts %}
{{ a.state }}: {{ a.headline }}
{{ a.rule }} · fires when {{ a.condition }} · {{ a.when }}
{%- if a.note %}
{{ a.note }}
{%- endif %}
{% endfor %}
{% if link %}
View in Steward: {{ link }}
{% endif %}
--
Steward sends this to everyone on its email list when alert rules change
state. Who gets it is set in Settings › Notifications.