Files
bvandeusenandClaude Opus 5.5 defad2e98b
CI / lint (push) Successful in 2s
CI / integration (push) Successful in 1m10s
CI / unit (push) Successful in 1m25s
CI / publish (push) Successful in 1m1s
feat(email): delivery you can see: each alert's email in history, outgoing email on Settings -> System (#5509)
#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>
2026-10-09 16:12:40 -04:00

84 lines
4.9 KiB
HTML

{# alerts/_history_page.html — one page of alert episodes (#5316), then the
sentinel that loads the next page as the reader nears it (core.paging,
pref 172). An episode is one stretch out of quiet: what it reached, for how
long, its worst reading, who was told; its transitions fold under it. #}
{% from "_macros.html" import status_badge, scroll_more, scroll_end %}
{% set cols = 3 + (0 if chosen else 1) + (1 if show_notified else 0) + (1 if show_action else 0) %}
{% for ep in episodes %}
<tr>
<td class="nowrap">
{%- if ep.began_here %}{{ ep.start|ago }}{% else %}<span class="tone-dim" title="It began before this part of the list">earlier</span>{% endif -%}
</td>
{% if not chosen %}
<td class="cell-name">
<a href="/alerts/history?rule={{ ep.rule.id }}&range={{ current_range }}" title="{{ ep.rule.name }}">{{ ep.rule.name }}</a>
<div class="cell-sub">{{ source_label(ep.rule.source_module) }} · {{ ep.rule.resource_name }}</div>
</td>
{% endif %}
<td>
{% if not ep.began_here and ep.steps|length == 1 %}
{%- set s = ep.steps[0] %}{{ status_badge(s.from_state) }} <span class="tone-dim">→</span> {{ status_badge(s.to_state) }}
{% else %}
{{ status_badge(ep.worst) }}
{%- if ep.ongoing %} <span class="tone-crit">{% if ep.began_here %}for {{ ep.duration|duration }}, {% endif %}still going</span>
{%- elif ep.continues %} <span class="tone-muted">cleared later (above)</span>
{%- elif ep.began_here %} for {{ ep.duration|duration }}<span class="tone-dim"> · cleared {{ ep.end|ago }}</span>
{%- else %} <span class="tone-dim">cleared {{ ep.end|ago }}</span>{% endif %}
{% endif %}
{% for n in ep.notes %}<div class="cell-note">{{ n }}</div>{% endfor %}
{% if ep.steps|length > 1 %}
<details class="episode-steps">
<summary>{{ ep.steps|length }} changes of state</summary>
<ol>
{% for s in ep.steps %}
<li>{{ status_badge(s.from_state) }} <span class="tone-dim">→</span> {{ status_badge(s.to_state) }}
<span class="tone-muted">{{ s.value }}</span> <span class="tone-dim">· {{ s.at|ago }}</span></li>
{% endfor %}
</ol>
</details>
{% endif %}
</td>
<td class="nowrap">
{%- if ep.reading %}{{ ep.reading }} <span class="tone-dim">({{ ep.fires_when }})</span>
{%- else %}<span class="tone-dim">no reading</span> <span class="tone-dim">({{ ep.fires_when }})</span>{% endif -%}
</td>
{% if show_notified %}
<td>
{% if ep.muted_by %}
<span class="badge badge-yellow" title="A maintenance window muted this notification">muted · {{ ep.muted_by }}</span>
{% if ep.released %}<div class="cell-note">notified when the window ended</div>{% endif %}
{% endif %}
{# Each channel's outcome; an email through the outbox (#5509) also
says when it sends, or how its retries stand, or why it failed. #}
{% for d in ep.deliveries %}
<div>
{% if d.state == "sent" %}<span class="badge badge-green">{{ d.label }} sent</span>
{%- if d.at %} <span class="tone-dim">{{ d.at|when }}</span>{% endif %}
{% elif d.state == "held" %}<span class="badge badge-yellow">{{ d.label }} gathering</span>
<div class="cell-note">waiting for other alerts; sends {{ d.at|when }}</div>
{% elif d.state == "sending" %}<span class="badge badge-yellow">{{ d.label }} sending</span>
{% elif d.state == "retrying" %}<span class="badge badge-yellow">{{ d.label }} retrying</span>
<div class="cell-note" style="max-width:28rem;overflow-wrap:anywhere;">attempt {{ d.attempts }}
{%- if d.at %}, next {{ d.at|when }}{% endif %}{% if d.error %}: {{ d.error }}{% endif %}</div>
{% else %}<span class="badge badge-red">{{ d.label }} failed</span>
{% if d.error %}<div class="cell-note" style="max-width:28rem;overflow-wrap:anywhere;">{{ d.error }}</div>{% endif %}
{% endif %}
</div>
{% endfor %}
{% if not ep.muted_by and not ep.deliveries %}<span class="tone-dim" title="No notification was sent for this">—</span>{% endif %}
</td>
{% endif %}
{% if show_action %}
<td>
{% for run_id in ep.runs %}<div><a href="/ansible/runs/{{ run_id }}">Ansible run</a></div>
{% else %}<span class="tone-dim">—</span>{% endfor %}
</td>
{% endif %}
</tr>
{% endfor %}
{% if after %}
{{ scroll_more("/alerts/history?before=" ~ (after|urlencode) ~ "&range=" ~ (current_range|urlencode) ~ (("&rule=" ~ (chosen.id|urlencode)) if chosen else ""), cols=cols) }}
{% else %}
{{ scroll_end(cols=cols) }}
{% endif %}