Files
FabledSteward/steward/templates/status/rows.html
T
bvandeusenandClaude Opus 5.5 3a772716ae
CI / lint (push) Successful in 2s
CI / integration (push) Successful in 51s
CI / unit (push) Failing after 1m14s
CI / publish (push) Skipped
feat(ui): audit, alert rules and the rule form say things in words (#5212)
#5212 "Plain labels: no raw ids, scopes, metric keys or unexplained jargon",
batch 1:

- Audit log: each action reads as a sentence ("paused monitor"), with the
  code as its tooltip. Detail shows flags as yes/no, lists joined, a run's
  scope by name, spans as spans, and no ids the entity column already gives.
  The filter is now a picker of areas.
- Alert rules, history and Status say the condition in words: "is down",
  "Fullest disk >= 85%". Readings use metric_value, and history times use
  |ago.
- Rule form: sources and metrics by name, the threshold's unit, "fires
  when it is down/up" for an up/down metric instead of 0.5, and a metric
  reference built from the catalog.
- Schedules and Maintenance: viewers no longer see Run now, Pause, Edit,
  Delete or New. Intervals and next runs read as "every 2h" and "in 3h";
  a scope reads "Every alert on Host agent".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 00:23:02 -04:00

109 lines
6.3 KiB
HTML

{# status/rows.html — what is wrong first (down monitors, active alerts), then
every monitor; refreshed on poll #}
{% from "_macros.html" import status_badge, status_dot, stat_tile, empty_state %}
{% macro uptime_cell(pct) -%}
{%- if pct is none -%}<span class="tone-unknown">—</span>
{%- else -%}<span class="tone-{{ status_class(pct, 'uptime') }}">{{ "%.2f"|format(pct) }}%</span>{%- endif -%}
{%- endmacro %}
{% macro since(e) -%}
{%- if e.since -%}{{ "for at least " if e.since_at_least else "since " }}{{ e.since|ago }}{%- endif -%}
{%- endmacro %}
{# ── What is wrong ───────────────────────────────────────────────────────── #}
{% if problems or alerts %}
<section class="card problem-card" aria-labelledby="status-problems">
<h2 id="status-problems" class="section-title" style="margin-bottom:0.6rem;">Needs attention</h2>
<ul class="problem-list">
{% for e in problems %}
<li>
{{ status_badge('down') }}
{% if e.detail_url %}<a href="{{ e.detail_url }}">{{ e.name }}</a>{% else %}<strong>{{ e.name }}</strong>{% endif %}
<span class="problem-detail">{{ e.kind|upper }} · {{ e.target }} · down {{ since(e) }}</span>
</li>
{% endfor %}
{% for state, rule in alerts %}
<li>
{{ status_badge('no_data' if state.no_data_since and state.state.value == 'inactive' else state.state) }}
<a href="/alerts/history?rule={{ rule.id }}">{{ rule.name }}</a>
<span class="problem-detail">{{ rule.resource_name }} {{ rule_condition(rule) }} · since {{ (state.no_data_since if state.state.value == 'inactive' else state.last_transition_at)|ago }}</span>
</li>
{% endfor %}
</ul>
</section>
{% endif %}
{# ── Counts ──────────────────────────────────────────────────────────────── #}
<div class="stat-strip">
{{ stat_tile("Down", down, state="down" if down else "inactive") }}
{{ stat_tile("Active alerts", alerts|length, state="firing" if alerts else "inactive", href="/alerts/") }}
{{ stat_tile("Pending", pending, state="pending" if pending else "inactive") }}
{{ stat_tile("Up", up, state="up") }}
</div>
{% if not entries %}
<div class="card">{{ empty_state("No monitors yet. Add a host (ping or DNS) or an HTTP monitor to fill the board.", "Add a monitor", "/monitors/new") }}</div>
{% else %}
<div class="card-flush">
{# header row #}
<div style="display:flex;align-items:center;gap:1rem;padding:0.55rem 1rem;border-bottom:1px solid var(--border-mid);font-size:0.72rem;color:var(--text-dim);text-transform:uppercase;letter-spacing:0.04em;">
<span style="width:11rem;flex-shrink:0;">Monitor</span>
<span style="flex:1;min-width:0;">Recent checks</span>
<span style="width:92px;text-align:right;flex-shrink:0;">Latest</span>
<span style="width:54px;text-align:center;flex-shrink:0;">24h</span>
<span style="width:54px;text-align:center;flex-shrink:0;">7d</span>
<span style="width:54px;text-align:center;flex-shrink:0;">30d</span>
<span style="width:64px;text-align:right;flex-shrink:0;">TLS</span>
</div>
{% for e in entries %}
<div style="display:flex;align-items:center;gap:1rem;padding:0.55rem 1rem;border-bottom:1px solid var(--border);">
{# name + kind #}
<div style="width:11rem;flex-shrink:0;min-width:0;">
<div style="display:flex;align-items:baseline;gap:0.4rem;">
{{ status_dot(e.status) }}
{% if e.detail_url %}<a href="{{ e.detail_url }}" style="font-size:0.85rem;font-weight:500;color:var(--text);overflow-wrap:anywhere;">{{ e.name }}</a>
{% else %}<span style="font-size:0.85rem;font-weight:500;overflow-wrap:anywhere;">{{ e.name }}</span>{% endif %}
</div>
<div style="color:var(--text-dim);font-size:0.7rem;overflow-wrap:anywhere;padding-left:0.85rem;">
<span style="text-transform:uppercase;letter-spacing:0.04em;">{{ e.kind }}</span> · {{ e.target }}
</div>
</div>
{# heartbeat bar #}
<div style="flex:1;min-width:0;display:flex;gap:2px;align-items:center;overflow:hidden;">
{% set pad = 30 - e.heartbeat|length %}
{% if pad > 0 %}{% for _ in range(pad) %}<span class="pill pill-empty" title="No data"></span>{% endfor %}{% endif %}
{% for hb in e.heartbeat %}
<span class="pill pill-{{ 'down' if hb.state == 'down' else 'up' }}" title="{{ hb.title }}"></span>
{% endfor %}
</div>
{# latest latency + sparkline #}
<div style="width:92px;text-align:right;flex-shrink:0;font-size:0.8rem;font-variant-numeric:tabular-nums;">
{% if e.status == 'down' %}{{ status_badge('down') }}
{% elif e.latency_ms is not none %}
{% if e.spark_svg %}<span style="opacity:0.8;">{{ e.spark_svg|safe }}</span><br>{% endif %}
<span style="color:var(--text-muted);">{{ "%.0f"|format(e.latency_ms) }} ms</span>
{% elif e.status == 'up' %}{{ status_badge('up') }}
{% else %}<span style="color:var(--text-dim);">—</span>{% endif %}
</div>
{# uptime windows #}
<span style="width:54px;text-align:center;flex-shrink:0;font-size:0.8rem;font-family:ui-monospace,monospace;">{{ uptime_cell(e.uptime.get('24h')) }}</span>
<span style="width:54px;text-align:center;flex-shrink:0;font-size:0.8rem;font-family:ui-monospace,monospace;">{{ uptime_cell(e.uptime.get('7d')) }}</span>
<span style="width:54px;text-align:center;flex-shrink:0;font-size:0.8rem;font-family:ui-monospace,monospace;">{{ uptime_cell(e.uptime.get('30d')) }}</span>
{# TLS expiry countdown #}
<span style="width:64px;text-align:right;flex-shrink:0;font-size:0.78rem;font-variant-numeric:tabular-nums;">
{% if e.tls_days is none %}<span class="tone-unknown">—</span>
{% elif e.tls_days < 0 %}<span class="tone-crit" title="Certificate expired">expired</span>
{% else %}<span class="tone-{{ status_class(e.tls_days, 'tls_days') }}" title="TLS certificate expires in {{ e.tls_days|round|int }} days">{{ e.tls_days|round|int }}d</span>{% endif %}
</span>
</div>
{% endfor %}
</div>
<p style="color:var(--text-dim);font-size:0.74rem;margin-top:0.75rem;">
Heartbeat shows the last 30 checks (newest on the right). Uptime % is over the rolling window. TLS countdown applies to HTTPS monitors.
</p>
{% endif %}