feat(email): alert and weekly-report emails read as designed messages (#5335)
#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:
@@ -240,6 +240,7 @@ async def save_notifications():
|
||||
await set_setting(db, "smtp.password", form.get("smtp.password"))
|
||||
await set_setting(db, "smtp.recipients", recipients)
|
||||
await set_setting(db, "smtp.from", form.get("smtp.from", "").strip())
|
||||
await set_setting(db, "smtp.tz", session.get("tz") or "")
|
||||
if "webhook.remove" in form:
|
||||
await set_setting(db, "webhook.url", "")
|
||||
elif form.get("webhook.url", "").strip():
|
||||
@@ -255,7 +256,7 @@ def _sample_alert() -> dict:
|
||||
has quotes in it, so a template that cannot take them shows it."""
|
||||
from datetime import datetime, timezone
|
||||
return {"rule_name": 'Test alert "from" Steward', "state": "TEST", "metric": "cpu_pct",
|
||||
"value": 97.5, "threshold": 90.0, "resource": "example-host",
|
||||
"value": 97.5, "operator": ">", "threshold": 90.0, "resource": "example-host",
|
||||
"source_module": "host_agent", "note": "sent from Settings → Notifications",
|
||||
"timestamp": datetime.now(timezone.utc).isoformat()}
|
||||
|
||||
@@ -296,7 +297,8 @@ async def test_webhook():
|
||||
async def test_email():
|
||||
"""Send a sample alert email using the SMTP settings in the form (unsaved);
|
||||
a blank password uses the saved one."""
|
||||
from steward.core.notifications import _send_email, email_body, smtp_mode
|
||||
from steward.core.mail import alert_email
|
||||
from steward.core.notifications import _send_email, smtp_mode
|
||||
form = await request.form
|
||||
async with current_app.db_sessionmaker() as db:
|
||||
saved = to_smtp_cfg(await get_all_settings(db))
|
||||
@@ -308,6 +310,7 @@ async def test_email():
|
||||
"tls": "smtp.tls" in form, "username": form.get("smtp.username", ""),
|
||||
"password": form.get("smtp.password") or saved["password"],
|
||||
"from": (form.get("smtp.from") or "").strip(),
|
||||
"tz": session.get("tz") or "", "base_url": saved["base_url"],
|
||||
"recipients": [r.strip() for r in form.get("smtp.recipients", "").split(",") if r.strip()]}
|
||||
if not cfg["host"] or not cfg["recipients"]:
|
||||
return await _notify_test_result("email", False, "Enter an SMTP host and at least one recipient.")
|
||||
@@ -316,7 +319,7 @@ async def test_email():
|
||||
how = {"ssl": "implicit TLS", "starttls": "STARTTLS", "plain": "no TLS"}[smtp_mode(port, cfg["tls"])]
|
||||
result = (f"Sent to {', '.join(cfg['recipients'])} via {cfg['host']}:{port} ({how})."
|
||||
if sent["sent"] else f"{cfg['host']}:{port} ({how}): {sent['error']}")
|
||||
return await _notify_test_result("email", sent["sent"], result, email_body(alert))
|
||||
return await _notify_test_result("email", sent["sent"], result, alert_email(cfg, alert)[1])
|
||||
|
||||
|
||||
# ── Ansible Sources ───────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user