Files
FabledSteward/steward/templates/settings/system.html
T
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

130 lines
6.2 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{# settings/system.html — Steward's own health (#5467): a verdict, then each of
its process metrics now and over the last day, from the self-monitor tick.
Each is a metric an alert rule can watch under "Steward itself". #}
{% extends "base.html" %}
{% from "_macros.html" import crumbs %}
{% from "settings/_tabs.html" import settings_tabs %}
{% block title %}Settings — System — Steward{% endblock %}
{% block section_tabs %}{{ settings_tabs("system") }}{% endblock %}
{% block breadcrumb %}{{ crumbs([("Settings", "/settings/"), ("System", "")]) }}{% endblock %}
{% block content %}
<p class="page-verdict">
{%- if problems %}{% set said = problems|join("; ") %}<span class="tone-warn">{{ said[:1]|upper }}{{ said[1:] }}.</span>
{%- elif not seen %}<span class="tone-dim">No readings yet: the first arrive within a minute of Steward starting.</span>
{%- else %}<span class="tone-ok">Steward is running normally.</span>{% endif -%}
</p>
<h2 class="section-title" style="margin:1.5rem 0 1rem;">Steward itself</h2>
<div class="card-flush">
<table class="table">
<thead><tr><th>Measure</th><th>Now</th><th>Last 24 h</th></tr></thead>
<tbody>
{% for r in rows %}
<tr>
<td class="cell-name"><span title="{{ metric_label(r.metric) }}">{{ metric_label(r.metric) }}</span>
<div class="cell-sub" title="{{ metric_about(r.metric) }}">{{ metric_about(r.metric) }}</div></td>
<td class="nowrap">{{ metric_value(r.metric, r.now) }}
{%- if r.at %}<div class="cell-sub">{{ r.at|ago }}</div>{% endif %}</td>
<td class="nowrap">
{%- if r.low is not none %}{{ metric_value(r.metric, r.low) }} – {{ metric_value(r.metric, r.high) }}
<span class="metric-preview-spark" aria-hidden="true">{{ r.spark|safe }}</span>
{%- else %}<span class="tone-dim">—</span>{% endif -%}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<p class="field-hint" style="margin-top:0.75rem;">
Recorded each minute (the cleanup each hour). Any of these can be watched by an
<a href="/alerts/rules/new">alert rule</a>: pick the source "Steward itself".</p>
{# Outgoing email (#5509): every email waits in the outbox until the mail
server takes it. What hasn't gone yet, failures first, then the longest
retrying; sent mail is only counted. #}
<h2 class="section-title" style="margin:2rem 0 0.5rem;">Outgoing email</h2>
<p style="margin:0 0 1rem;">
{%- if not email.rows %}Nothing waiting to send. {% endif -%}
{{ "{:,}".format(email.sent) }} sent in the last 7 days.</p>
{% if email.rows %}
<div class="card-flush">
<table class="table">
<thead><tr><th>To</th><th>What</th><th>State</th><th>Queued</th></tr></thead>
<tbody>
{% for r in email.rows %}
<tr>
<td class="cell-name"><span title="{{ r.recipient }}">{{ r.recipient }}</span></td>
<td class="nowrap">{{ kind_labels.get(r.kind, r.kind) }}</td>
<td>
{%- if r.state == "failed" %}<span class="badge badge-red">failed</span>
{%- elif r.state == "pending" and r.attempts %}<span class="badge badge-yellow">retrying</span>
<span class="tone-dim">attempt {{ r.attempts }}{% if r.next_attempt_at %}, next {{ r.next_attempt_at|when }}{% endif %}</span>
{%- elif r.state == "held" %}<span class="badge badge-yellow">gathering</span>
<span class="tone-dim">sends {{ r.send_after|when }}</span>
{%- else %}<span class="badge badge-yellow">sending</span>{% endif %}
{%- if r.last_error %}<div class="cell-note" style="max-width:32rem;overflow-wrap:anywhere;">{{ r.last_error }}</div>{% endif -%}
</td>
<td class="nowrap">{{ r.created_at|ago }}</td>
</tr>
{% endfor %}
{% if email.more %}
<tr><td colspan="4" class="tone-dim">+{{ email.more }} more not yet sent</td></tr>
{% endif %}
</tbody>
</table>
</div>
{% endif %}
<p class="field-hint" style="margin-top:0.75rem;">
An email the server refuses is failed at once; one it can't take yet is tried
again for up to an hour. The mail server is set under
<a href="/settings/notifications/">Notifications</a>.</p>
{# Storage (#5469): Postgres's own figures for each table, then what the
hourly cleanup last removed. Read from the catalog, never by counting. #}
<h2 class="section-title" style="margin:2rem 0 0.5rem;">Storage</h2>
<p class="field-hint" style="margin:0 0 1rem;">The database takes {{ storage.total|bytes }} on disk.</p>
<div class="card-flush">
<table class="table">
<thead><tr><th>Table</th><th class="num">On disk</th><th class="num">Rows (estimate)</th></tr></thead>
<tbody>
{% for t in storage.tables %}
<tr>
<td class="cell-name"><span title="{{ t.name }}">{{ t.name }}</span></td>
<td class="num nowrap">{{ t.bytes|bytes }}</td>
<td class="num nowrap">{% if t.rows is not none %}{{ "{:,}".format(t.rows) }}{% else %}—{% endif %}</td>
</tr>
{% endfor %}
{% if storage.rest.count %}
<tr><td class="tone-dim">+{{ storage.rest.count }} smaller tables</td>
<td class="num nowrap tone-dim">{{ storage.rest.bytes|bytes }}</td><td></td></tr>
{% endif %}
</tbody>
</table>
</div>
<h2 class="section-title" style="margin:2rem 0 0.5rem;">Last cleanup</h2>
{% if not cleanup %}
<p class="tone-dim">None has finished since this version started: the cleanup runs each hour.</p>
{% else %}
<p style="margin:0 0 1rem;">Ran {{ cleanup.at|ago }} and took {{ cleanup.secs|duration }}.
{%- if not cleanup.removed %} Nothing was past its window ({{ cleanup.passes }} passes).{% endif %}</p>
{% if cleanup.removed %}
<div class="card-flush">
<table class="table">
<thead><tr><th>What went</th><th class="num">Rows</th></tr></thead>
<tbody>
{% for what, n in cleanup.removed %}
<tr><td class="cell-name"><span title="{{ what }}">{{ what }}</span></td>
<td class="num nowrap">{{ "{:,}".format(n) }}</td></tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
{% endif %}
<p class="field-hint" style="margin-top:0.75rem;">
How long each kind of data is kept is set under
<a href="/settings/general/">General</a> (data and audit log) and
<a href="/settings/thresholds/">Thresholds &amp; retention</a> (metrics, Docker and logs).</p>
{% endblock %}