feat(email): delivery you can see: each alert's email in history, outgoing email on Settings -> System (#5509)
CI / lint (push) Successful in 2s
CI / integration (push) Successful in 1m10s
CI / unit (push) Successful in 1m25s
CI / publish (push) Successful in 1m1s

#5509 "Delivery you can see: each alert's email state in history, and
outgoing email on Settings -> System" (milestone 501).

- Alerts -> History: an alert's email reads from the outbox (#5506):
  "gathering" with when it sends, "sending", "retrying" with the attempt,
  next try and the server's last answer, "sent", or "failed" naming each
  reader it didn't reach and why (outbox.delivery / email_states). Events
  from before the outbox keep what they recorded. The Notified column
  shows once an alert has a queued email.
- Settings -> System gets "Outgoing email": every email of the last 7
  days not yet sent, failures first, then the longest retrying, with
  recipient, kind, state and last error, and a count of what was sent.
  Failures and anything still retrying after 5 minutes lead the page's
  verdict (outbox.outgoing).
- get_alert_history (MCP) adds `email`: the same delivery state.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-09 16:12:40 -04:00
co-authored by Claude Opus 5.5
parent 249d22e41f
commit defad2e98b
14 changed files with 375 additions and 26 deletions
+7 -3
View File
@@ -74,14 +74,18 @@ def test_an_episode_split_by_paging_says_so():
assert not newer[0]["began_here"] and newer[0]["end"] == NOW - timedelta(minutes=10)
def _d(label, state="sent", error=None):
return {"label": label, "state": state, "at": None, "attempts": None, "error": error}
def test_who_was_told_is_gathered_once_per_episode():
r = _rule()
eps = episodes(_newest_first(
_t(r, 30, "inactive", "firing", 200.0, deliveries=[("email", False, "timed out"), ("webhook", True, None)],
_t(r, 30, "inactive", "firing", 200.0, deliveries=[_d("email", "failed", "timed out"), _d("webhook")],
run="run-1"),
_t(r, 20, "firing", "resolved", 5.0, deliveries=[("webhook", True, None)], muted_by="patch")),
_t(r, 20, "firing", "resolved", 5.0, deliveries=[_d("webhook")], muted_by="patch")),
newest_page=True, now=NOW, since=SINCE)
assert eps[0]["deliveries"] == [("email", False, "timed out"), ("webhook", True, None)]
assert eps[0]["deliveries"] == [_d("email", "failed", "timed out"), _d("webhook")]
assert eps[0]["muted_by"] == "patch" and eps[0]["runs"] == ["run-1"]