feat(email): alert and weekly-report emails read as designed messages (#5335)
CI / lint (push) Successful in 4s
CI / integration (push) Successful in 1m22s
CI / unit (push) Successful in 1m49s
CI / publish (push) Successful in 1m13s

#5335 "Emails Steward sends read as designed messages: an HTML alert email
and weekly report, with a plain-text twin".

- core/mail.py renders each email as multipart/alternative from
  templates/email/: a table-and-inline-style HTML part and a plain-text
  twin carrying the same facts. Colours are the design system's light
  theme, resolved from theme.css; nothing is fetched when a mail opens.
- Alert email: state badge, a headline sentence ("Disk used on web01 is
  94%."), rule, resource, reading, the condition in words, and the time in
  the zone Notifications was saved in with UTC beside it; "View in
  Steward" when Steward's address is set. Subject "[Steward] Firing: <rule>
  — <resource> 94%". Alerts now carry their operator.
- Weekly report: monitors down, active alerts, the week's alert events,
  uptime and busiest Traefik routers as small tables with links. Sent from
  the configured From address; the "Generated … UTC" line and the stale
  repo footer are gone.
- condition_text() states a condition from a rule's parts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-08 14:08:05 -04:00
co-authored by Claude Opus 5.5
parent 24700ee608
commit ec55faf737
18 changed files with 539 additions and 113 deletions
+63
View File
@@ -0,0 +1,63 @@
{# The weekly report's HTML part: d = reports.build_report_data(), alerts = its
active alerts with tone and label, period, sent, base = Steward's address or "". #}
{% extends "_layout.html" %}
{% from "_parts.html" import badge, button, section, rows, row with context %}
{% block title %}Weekly report, {{ period }}{% endblock %}
{% block body %}
<tr><td style="padding:10px 24px 0 24px;font:600 22px/1.3 {{ c.font }};color:{{ c.text }};">Weekly report</td></tr>
<tr><td style="padding:2px 24px 0 24px;font:14px/1.5 {{ c.font }};color:{{ c.muted }};">{{ period }}</td></tr>
{% if d.hosts_down %}
<tr><td style="padding:16px 24px 0 24px;">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0"
style="border:1px solid {{ c.crit }};border-left:4px solid {{ c.crit }};border-radius:8px;">
<tr><td style="padding:10px 14px;font:14px/1.5 {{ c.font }};color:{{ c.text }};">
<strong style="color:{{ c.crit }};">{{ d.hosts_down|length }} {{ "monitor is" if d.hosts_down|length == 1 else "monitors are" }} down now:</strong>
{{ d.hosts_down|join(", ") }}
</td></tr></table>
</td></tr>
{% endif %}
{{ section("Active alerts", base ~ "/alerts/" if base else "") }}
{% if alerts %}
{% call rows() %}
{% for a in alerts %}
{{ row([badge(a.tone, a.state), a.name ~ " — " ~ a.resource, a.condition]) }}
{% endfor %}
{% endcall %}
{% else %}
<tr><td style="padding:0 24px;font:14px/1.5 {{ c.font }};color:{{ c.muted }};">None. Nothing is firing.</td></tr>
{% endif %}
<tr><td style="padding:8px 24px 0 24px;font:13px/1.5 {{ c.font }};color:{{ c.muted }};">
This week {{ d.alert_events.fired }} {{ "alert" if d.alert_events.fired == 1 else "alerts" }} fired and {{ d.alert_events.resolved }} resolved.
</td></tr>
{{ section("Uptime over 7 days", base ~ "/monitors/" if base else "") }}
{% if d.uptime_summary %}
{% call rows() %}
{% for m in d.uptime_summary %}
{{ row([m.name, "—" if m.pct_7d is none else "%.2f%%"|format(m.pct_7d)]) }}
{% endfor %}
{% endcall %}
{% else %}
<tr><td style="padding:0 24px;font:14px/1.5 {{ c.font }};color:{{ c.muted }};">No monitors are enabled.</td></tr>
{% endif %}
{% if d.top_routers %}
{{ section("Busiest Traefik routers, last 24 hours", base ~ "/plugins/traefik/" if base else "") }}
{% call rows() %}
{% for r in d.top_routers %}
{{ row([r.name, "%.2f req/s"|format(r.avg_rate)]) }}
{% endfor %}
{% endcall %}
{% endif %}
{% if base %}
<tr><td style="padding:24px 24px 24px 24px;">{{ button(base ~ "/", "Open Steward") }}</td></tr>
{% else %}
<tr><td style="padding:0 0 16px 0;"></td></tr>
{% endif %}
{% endblock %}
{% block footer %}
Sent {{ sent }}. The weekly report is turned on, and its day and hour set, in Settings › Reports.
{% endblock %}