Files
FabledSteward/steward/templates/monitors/detail.html
T
bvandeusenandClaude Opus 5.5 e595bd922a
CI / lint (push) Successful in 3s
CI / unit (push) Failing after 1m23s
CI / integration (push) Successful in 1m12s
CI / publish (push) Skipped
feat(monitors): problems first, a page per monitor, adding on its own page (#5121)
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>
2026-10-06 17:54:30 -04:00

84 lines
4.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.
{% extends "base.html" %}
{% from "_macros.html" import crumbs, page_header, stat_tile, status_badge, icon_button, empty_state %}
{# One monitor: its state and since when, latency and uptime against Settings →
Thresholds, its last checks, and the failures behind them. #}
{% block title %}{{ monitor.name }} — Monitors — Steward{% endblock %}
{% block breadcrumb %}{{ crumbs([("Monitors", "/monitors/"), (monitor.name, "")]) }}{% endblock %}
{% block content %}
{% set can_edit = session.user_role in ['operator', 'admin'] %}
{% call page_header(monitor.name, (monitor.type|monitor_type) ~ " · " ~ monitor.target) %}
{% if can_edit %}
{{ icon_button("✎", "Edit " ~ monitor.name, href="/monitors/" ~ monitor.id ~ "/edit") }}
<form method="post" action="/monitors/{{ monitor.id }}/toggle">{{ csrf_field() }}
{{ icon_button("⏸" if monitor.enabled else "▶", ("Pause " if monitor.enabled else "Resume ") ~ monitor.name, type="submit") }}
</form>
<form method="post" action="/monitors/{{ monitor.id }}/delete">{{ csrf_field() }}
{{ icon_button("×", "Delete " ~ monitor.name, type="submit", cls="btn-danger", confirm="Delete monitor " ~ monitor.name ~ "? Its check history goes with it.") }}
</form>
{% endif %}
{% endcall %}
<div class="stat-strip">
{% set since_hint = ((("for at least " if since_at_least else "since ") ~ since|ago) if since and state in ("up", "down", "degraded") else none) %}
{{ stat_tile("State", status_info(state)[1], state=state, hint=since_hint) }}
{% if latest and latest.response_ms is not none %}
{{ stat_tile("Latency", "%.0f ms"|format(latest.response_ms), state=status_class(latest.response_ms, "latency")) }}
{% endif %}
{% for window in ("24h", "7d", "30d") %}
{% set pct = uptime.get(window) %}
{{ stat_tile("Uptime " ~ window, "%.2f%%"|format(pct) if pct is not none else "—", state=status_class(pct, "uptime")) }}
{% endfor %}
{% if tls_days is not none %}
{{ stat_tile("TLS certificate", "expired" if tls_days < 0 else (tls_days|round|int) ~ " days", state="crit" if tls_days < 0 else status_class(tls_days, "tls_days")) }}
{% endif %}
</div>
<section class="card" aria-labelledby="m-checks">
<h2 id="m-checks" class="section-title">Last checks</h2>
{% if heartbeat %}
<div class="ping-pills" style="margin:0.5rem 0;">
{% for _ in range([30 - heartbeat|length, 0]|max) %}<span class="pill pill-empty" title="No data"></span>{% endfor %}
{% for h in heartbeat %}<span class="pill pill-{{ h.state }}" title="{{ h.title }}"></span>{% endfor %}
</div>
<p class="field-hint">Newest on the right.{% if latest %} Last checked {{ latest.checked_at|ago }}.{% endif %}</p>
{% else %}
{{ empty_state("Not checked yet. The first check runs on the next scheduler tick." if monitor.enabled else "Paused, and never checked.") }}
{% endif %}
</section>
<section class="card" aria-labelledby="m-failures">
<h2 id="m-failures" class="section-title">Recent failures</h2>
{% if failures %}
<table class="table">
<thead><tr><th>When</th><th>Error</th>{% if monitor.type == "http" %}<th>Status</th>{% endif %}</tr></thead>
<tbody>
{% for f in failures %}
<tr>
<td style="white-space:nowrap;">{{ f.checked_at|ago }}</td>
<td style="overflow-wrap:anywhere;">{{ f.error_msg or "Down" }}</td>
{% if monitor.type == "http" %}<td>{{ f.status_code or "—" }}</td>{% endif %}
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
{{ empty_state("No failed checks on record.") }}
{% endif %}
</section>
<section class="card" aria-labelledby="m-config">
<h2 id="m-config" class="section-title">Settings</h2>
<dl class="kv">
<dt>Checked every</dt>
<dd>{{ monitor.check_interval_seconds|duration if monitor.check_interval_seconds else "the global interval" }}</dd>
{% if host %}<dt>Host</dt><dd><a href="/hosts/{{ host.id }}">{{ host.name }}</a></dd>{% endif %}
{% for key, value in monitor.config.items() %}
{% if value not in (none, "", {}) %}
<dt>{{ key|replace("_", " ")|capitalize }}</dt>
<dd style="overflow-wrap:anywhere;">{{ value|join(", ") if value is mapping else ("yes" if value is sameas true else ("no" if value is sameas false else value)) }}</dd>
{% endif %}
{% endfor %}
</dl>
</section>
{% endblock %}