Files
FabledSteward/steward/templates/monitors/rows.html
T
bvandeusenandClaude Opus 5.5 5a14e7c693
CI / lint (push) Successful in 3s
CI / integration (push) Successful in 1m8s
CI / unit (push) Successful in 1m25s
CI / publish (push) Successful in 1m17s
feat(ui): alerts, monitors, hosts, schedules and maintenance lists put problems first, with since-times (#5210)
#5210 "Problems first, with since-time, across every list" (milestone 477),
first batch.

- Alerts: the Active list reads firing, no data, acknowledged, pending, the
  longest-running first, and re-polls itself. An acknowledged alert says how
  long it has been breaching (from the alert's events; acknowledging moves
  last_transition_at) and, apart from that, who acknowledged it and when.
  Rules show what each is doing now; disabled rules sort last. The current
  value reads the latest-value table (#5208) instead of plugin_metrics.
- Monitors: a down monitor says "Down 12m · connection refused" in the list.
- Hosts: sorted down, agent gone quiet, pending, healthy. The agent column
  shows "—" for a missing reading (was 0%) and a Stale badge with its age.
  agent_metrics_by_host is gone; the list reads agent_overview_by_host.
- Schedules: failing first (failed or interrupted run, or a firing that could
  not start one), then running, then the rest, paused last; status badge
  from the shared states.
- Maintenance windows: active (ending soonest), then upcoming (starting
  soonest), with expired folded away; each says when it ends/starts/ended.

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

75 lines
3.9 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.
{# HTMX fragment — the Monitors list: counts, then every monitor, problems first
(service.problems_first). Colours come from Settings → Thresholds. #}
{% from "_macros.html" import stat_tile, status_badge, status_dot, icon_button, empty_state %}
<div class="stat-strip">
{{ stat_tile("Down", counts.down, state="down" if counts.down else "inactive") }}
{{ stat_tile("Up", counts.up, state="up" if counts.up else "inactive",
hint=(counts.degraded ~ " degraded") if counts.degraded else none) }}
{{ stat_tile("Pending", counts.pending, state="pending" if counts.pending else "inactive") }}
{{ stat_tile("Paused", counts.paused, state="paused") }}
</div>
{% if monitor_data %}
<div class="card ping-card">
{% set can_edit = session.user_role in ['operator', 'admin'] %}
{% for d in monitor_data %}
{% set m = d.monitor %}
{% set latest = d.latest %}
<div class="ping-row{% if d.state == 'paused' %} ping-row-paused{% endif %}">
<div class="ping-meta" style="min-width:160px;max-width:260px;">
<div style="display:flex;align-items:baseline;gap:0.4rem;">
{{ status_dot(d.state) }}
<a class="ping-name" href="/monitors/{{ m.id }}">{{ m.name }}</a>
</div>
<span class="ping-addr" title="{{ m.target }}">{{ m.type|monitor_type }} · {{ m.target }}</span>
{% if d.state == 'down' %}
<span class="tone-crit" style="font-size:0.75rem;overflow-wrap:anywhere;">
Down{% if d.down_since %} {{ d.down_since|ago|replace(' ago', '') }}{% endif %}{% if d.down_reason %} · {{ d.down_reason }}{% endif %}
</span>
{% endif %}
</div>
<div class="ping-pills">
{% for _ in range([30 - d.heartbeat|length, 0]|max) %}<span class="pill pill-empty" title="No data"></span>{% endfor %}
{% for h in d.heartbeat %}<span class="pill pill-{{ h.state }}" title="{{ h.title }}"></span>{% endfor %}
</div>
<div class="ping-cur">
{% if d.state in ('down', 'paused', 'pending') %}{{ status_badge(d.state) }}
{% elif latest.response_ms is not none %}
<span class="tone-{{ status_class(latest.response_ms, 'latency') }}">{{ "%.0f"|format(latest.response_ms) }} ms</span>
{% elif m.type == 'dns' and latest.resolved_ip %}<span class="tone-ok">{{ latest.resolved_ip }}</span>
{% else %}{{ status_badge('up') }}{% endif %}
</div>
<div style="min-width:4.5rem;text-align:right;font-size:0.78rem;font-variant-numeric:tabular-nums;">
{% if d.uptime_pct is not none %}<span class="tone-{{ status_class(d.uptime_pct, 'uptime') }}">{{ "%.1f"|format(d.uptime_pct) }}%</span>{% else %}<span class="tone-unknown">—</span>{% endif %}
<span class="tone-dim" style="font-size:0.7rem;margin-left:0.15rem;">{{ range_key }}</span>
</div>
{% if d.tls_days is not none %}
<div style="min-width:4rem;text-align:right;font-size:0.72rem;">
{% if d.tls_days < 0 %}<span class="tone-crit" title="Certificate expired">cert expired</span>
{% else %}<span class="tone-{{ status_class(d.tls_days, 'tls_days') }}" title="TLS certificate expires in {{ d.tls_days|round|int }} days">cert {{ d.tls_days|round|int }}d</span>{% endif %}
</div>
{% endif %}
{% if can_edit %}
<div class="row-actions">
{{ icon_button("✎", "Edit " ~ m.name, href="/monitors/" ~ m.id ~ "/edit") }}
<form method="post" action="/monitors/{{ m.id }}/toggle">{{ csrf_field() }}
{{ icon_button("⏸" if m.enabled else "▶", ("Pause " if m.enabled else "Resume ") ~ m.name, type="submit") }}
</form>
<form method="post" action="/monitors/{{ m.id }}/delete">{{ csrf_field() }}
{{ icon_button("×", "Delete " ~ m.name, type="submit", confirm="Delete monitor " ~ m.name ~ "?") }}
</form>
</div>
{% endif %}
</div>
{% endfor %}
</div>
{% else %}
<div class="card">{{ empty_state("No monitors yet. Add one, or attach checks to a host from its page.",
"Add monitor" if session.user_role in ['operator', 'admin'] else none, "/monitors/new") }}</div>
{% endif %}