Files
FabledSteward/steward/templates/monitors/rows.html
T
bvandeusenandClaude Opus 5.5 434127b3e1
CI / lint (push) Successful in 4s
CI / integration (push) Successful in 1m10s
CI / unit (push) Successful in 1m36s
CI / publish (push) Successful in 1m28s
feat(monitors): Monitors absorbs the Status page; Status is removed (#5270)
Status repeated the Monitors page's counts, check strip and uptime. Monitors
now has one header row and every column Status had: the last 30 checks,
latest response with its trend, uptime over 24h / 7d / 30d (the range picker
goes) and TLS days on every row. /status and /status/rows and the nav link
are removed outright; the dashboard Overview (/status/widget) stays and its
Details link goes to Monitors.

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

86 lines
4.6 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), under one header: the last 30 checks, latest
response with its trend, uptime over 24h / 7d / 30d and TLS days. This is
the one home for monitor state; the Status page it replaced is gone (#5270).
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 %}
{% set can_edit = session.user_role in ['operator', 'admin'] %}
<div class="card ping-card">
<div class="ping-row mon-head" aria-hidden="true">
<span class="ping-meta mon-col-name">Monitor</span>
<span class="ping-pills">Last 30 checks</span>
<span class="mon-latest">Latest</span>
<span class="mon-up">24h</span><span class="mon-up">7d</span><span class="mon-up">30d</span>
<span class="mon-tls">TLS</span>
{% if can_edit %}<span class="mon-act"></span>{% endif %}
</div>
{% 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 mon-col-name">
<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="mon-latest">
{% if d.state in ('down', 'paused', 'pending') %}{{ status_badge(d.state) }}
{% elif latest.response_ms is not none %}
{% if d.spark %}<span class="mon-spark" title="Response time over the last 30 checks">{{ d.spark|safe }}</span>{% endif %}
<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>
{% for k in ("24h", "7d", "30d") %}{% set u = d.uptime.get(k) %}
<div class="mon-up">{% if u is none %}<span class="tone-unknown">—</span>{% else %}<span class="tone-{{ status_class(u, 'uptime') }}">{{ "%.2f"|format(u) }}%</span>{% endif %}</div>
{% endfor %}
<div class="mon-tls">
{% if d.tls_days is none %}<span class="tone-unknown">—</span>
{% elif d.tls_days < 0 %}<span class="tone-crit" title="Certificate expired">expired</span>
{% else %}<span class="tone-{{ status_class(d.tls_days, 'tls_days') }}" title="TLS certificate expires in {{ d.tls_days|round|int }} days">{{ d.tls_days|round|int }}d</span>{% endif %}
</div>
{% if can_edit %}
<div class="row-actions mon-act">
{{ 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>
<p class="field-hint" style="margin-top:0.6rem;">Last 30 checks: newest on the right; hover one for its result. Uptime is over the rolling window. TLS is days until the certificate expires (HTTPS monitors).</p>
{% 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 %}