The Monitors list now opens with a count strip (down, up with how many degraded, pending, paused) and sorts down, degraded, pending, up, paused, by name within each. Degraded is up but worth a look: uptime at or below its warning floor, or the latest response past its critical cutoff, both from Settings → Thresholds; a slow-but-tolerable reply is coloured, not sorted. A monitor had no page of its own: every link went to the list. Now /monitors/<id> shows its state and since when, latency, uptime for 24h/7d/ 30d, TLS days, the last checks, recent failures with their errors, and its settings (HTTP header names, never their values). Status links to it, and adding or editing returns to it. Adding is its own page (/monitors/new, form.html shared with edit) behind an "Add monitor" button; a refused add re-renders the form with the error and what was typed instead of a bare 400 text. Row actions are labelled icon buttons, type names read Ping/TCP/DNS/HTTP (|monitor_type), and the heartbeat uses pill-up/pill-down, so HEX_CEILING drops to 56. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
70 lines
3.6 KiB
HTML
70 lines
3.6 KiB
HTML
{# 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>
|
||
</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 %}
|