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>
This commit is contained in:
@@ -46,7 +46,6 @@
|
||||
<div class="nav-group">
|
||||
<div class="nav-group-label">Overview</div>
|
||||
<a href="/" {% if _p == '/' %}class="active" aria-current="page"{% endif %}>Dashboard</a>
|
||||
<a href="/status" {% if _p.startswith('/status') %}class="active" aria-current="page"{% endif %}>Status</a>
|
||||
</div>
|
||||
<div class="nav-group">
|
||||
<div class="nav-group-label">Infrastructure</div>
|
||||
|
||||
@@ -3,14 +3,12 @@
|
||||
{% block title %}Monitors — Steward{% endblock %}
|
||||
{% block content %}
|
||||
{% call page_header("Monitors", "Checked every " ~ poll_interval ~ "s. Problems first.") %}
|
||||
{% include "_time_range.html" %}
|
||||
{% if session.user_role in ['operator', 'admin'] %}<a class="btn btn-sm" href="/monitors/new">Add monitor</a>{% endif %}
|
||||
{% endcall %}
|
||||
|
||||
<div id="monitor-rows"
|
||||
hx-get="/monitors/rows"
|
||||
hx-trigger="load, every {{ poll_interval }}s, rangeChange from:body"
|
||||
hx-include="#time-range"
|
||||
hx-trigger="load, every {{ poll_interval }}s"
|
||||
hx-swap="innerHTML">
|
||||
<div class="card"><p class="field-hint">Loading…</p></div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
{# HTMX fragment — the Monitors list: counts, then every monitor, problems first
|
||||
(service.problems_first). Colours come from Settings → Thresholds. #}
|
||||
(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") }}
|
||||
@@ -10,13 +13,21 @@
|
||||
</div>
|
||||
|
||||
{% if monitor_data %}
|
||||
<div class="card ping-card">
|
||||
{% 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" style="min-width:160px;max-width:260px;">
|
||||
<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>
|
||||
@@ -34,28 +45,27 @@
|
||||
{% for h in d.heartbeat %}<span class="pill pill-{{ h.state }}" title="{{ h.title }}"></span>{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="ping-cur">
|
||||
<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>
|
||||
|
||||
<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>
|
||||
{% 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 %}
|
||||
|
||||
{% 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 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>
|
||||
{% endif %}
|
||||
|
||||
{% if can_edit %}
|
||||
<div class="row-actions">
|
||||
<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") }}
|
||||
@@ -68,6 +78,7 @@
|
||||
</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>
|
||||
|
||||
@@ -61,8 +61,8 @@
|
||||
</div>
|
||||
<p style="color:var(--text-muted);font-size:0.82rem;margin:0 0 0.75rem;max-width:720px;">
|
||||
Built-in ways to monitor your hosts (agent metrics, SNMP, Docker). These are
|
||||
facets of a host — you'll see their data in the <a href="/hosts/">Hosts</a> and
|
||||
<a href="/status">Status</a> sections, not as separate areas. On by default.
|
||||
facets of a host — you'll see their data on each host in <a href="/hosts/">Hosts</a>,
|
||||
not as separate areas. On by default.
|
||||
</p>
|
||||
{% if capabilities %}
|
||||
<div style="display:grid;gap:0.6rem;max-width:720px;margin-bottom:1.5rem;">
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Status — Steward{% endblock %}
|
||||
{% block content %}
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:1.25rem;gap:1rem;flex-wrap:wrap;">
|
||||
<h1 class="page-title" style="margin-bottom:0;">Status</h1>
|
||||
<span style="font-size:0.78rem;color:var(--text-dim);">Live — refreshes every {{ poll_interval }}s</span>
|
||||
</div>
|
||||
|
||||
<div hx-get="/status/rows" hx-trigger="load, every {{ poll_interval }}s" hx-swap="innerHTML">
|
||||
<div class="empty">Loading status…</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -1,108 +0,0 @@
|
||||
{# 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 %}
|
||||
Reference in New Issue
Block a user