feat(hosts): one health header, graded by one set of thresholds (#5122)
CI / lint (push) Successful in 2s
CI / integration (push) Successful in 47s
CI / unit (push) Failing after 1m14s
CI / publish (push) Skipped

The host page's vitals strip becomes its health header: Live or Stale,
then one sentence — "Memory 93% is past its critical threshold", "Every
reading is within its thresholds", or, for a stale host, "No report since
20m ago; the readings below are from then" — and last seen as |ago. CPU,
memory, root disk and load per core follow as stat tiles graded by
Settings → Thresholds, with their sparklines.

The full-metrics page drew its bars and gauges at a hard-coded 70/90
(temperatures 70/85) while every other page used the configured cutoffs
(80/90 by default), so a 75 % CPU was amber here and fine everywhere else.
bar() now takes the metric kind and colours its .meter-fill through
status_class; gauges, temperatures and per-core bars do the same.

The host's monitor rows move onto the Monitors list's shapes: a status dot,
the name linking to the monitor's page, Ping/TCP/DNS/HTTP, pill-up/
pill-down, latency and uptime through status_class, labelled icon buttons.
HEX_CEILING drops to 54.

New formatting.worst_reading(readings, thresholds) picks the reading
furthest past its cutoff, the first of equals winning.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-06 17:58:22 -04:00
co-authored by Claude Opus 5.5
parent 629c466fd9
commit 8c19f3c21f
8 changed files with 177 additions and 73 deletions
+18
View File
@@ -131,3 +131,21 @@ def required(value: Any, what: str) -> Any:
if value is None or not str(value).strip():
raise ValueError(f"{what} is required")
return value
_SEVERITY = {"crit": 2, "warn": 1}
def worst_reading(readings, thresholds: dict):
"""The reading furthest past its cutoff, as (label, value, grade), or None
when every reading is within its cutoffs or missing.
`readings` is (label, value, kind) triples, graded like status_class; the
first of equally bad readings wins, so list them in the order a reader
should hear about them."""
worst, rank = None, 0
for label, value, kind in readings:
grade = status_class(value, kind, thresholds)
if _SEVERITY.get(grade, 0) > rank:
worst, rank = (label, value, grade), _SEVERITY[grade]
return worst
+10
View File
@@ -268,6 +268,16 @@ a.stat-tile:hover { border-color: var(--border-mid); background: var(--bg-elevat
/* A label/value list: <dl class="kv"><dt>…</dt><dd>…</dd></dl>. */
.kv { display: grid; grid-template-columns: max-content 1fr; gap: 0.35rem 1.25rem; font-size: 0.88rem; }
.kv dt { color: var(--text-muted); }
/* The host page's health header (_host_vitals.html). */
.host-health-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.35rem 0.75rem; margin-bottom: 0.75rem; }
.host-health-summary { font-size: 0.92rem; }
/* A filled bar for a percentage; the fill takes its grade from status_class. */
.meter { height: 8px; background: var(--bg-elevated); border: 1px solid var(--border); border-radius: 4px; overflow: hidden; }
.meter-fill { height: 100%; }
.meter-ok { background: var(--green); }
.meter-warn { background: var(--yellow); }
.meter-crit { background: var(--red); }
.meter-unknown { background: var(--text-dim); }
.problem-detail { color: var(--text-muted); font-size: 0.82rem; }
/* Breadcrumbs */
+19 -24
View File
@@ -1,5 +1,5 @@
{% extends "base.html" %}
{% from "_macros.html" import crumbs %}
{% from "_macros.html" import crumbs, status_dot, status_badge, icon_button %}
{% from "monitors/_fields.html" import type_fields, toggle_script %}
{% block title %}{{ host.name }} — Hosts — Steward{% endblock %}
{% block breadcrumb %}{{ crumbs([("Hosts", "/hosts/"), (host.name, "")]) }}{% endblock %}
@@ -39,9 +39,8 @@
<h3 class="section-title" style="margin-bottom:0;">Monitors</h3>
{% if uptime %}
<span style="font-size:0.8rem;color:var(--text-muted);">
uptime 24h <strong>{{ ("%.1f%%"|format(uptime['24h'])) if uptime['24h'] is not none else '—' }}</strong>
· 7d <strong>{{ ("%.1f%%"|format(uptime['7d'])) if uptime['7d'] is not none else '—' }}</strong>
· 30d <strong>{{ ("%.1f%%"|format(uptime['30d'])) if uptime['30d'] is not none else '—' }}</strong>
uptime{% for w in ("24h", "7d", "30d") %}{{ " ·" if not loop.first }} {{ w }}
<strong class="tone-{{ status_class(uptime[w], 'uptime') }}">{{ ("%.1f%%"|format(uptime[w])) if uptime[w] is not none else '—' }}</strong>{% endfor %}
</span>
{% endif %}
</div>
@@ -50,34 +49,30 @@
{% for d in monitors %}
{% set m = d.monitor %}
{% set latest = d.latest %}
<div class="ping-row" {% if not m.enabled %}style="opacity:0.5;"{% endif %}>
<div class="ping-row{% if not m.enabled %} ping-row-paused{% endif %}">
<div class="ping-meta" style="min-width:150px;max-width:230px;">
<span class="badge" style="font-size:0.62rem;">{{ m.type | upper }}</span>
<span class="ping-name" title="{{ m.name }}">{{ m.name }}</span>
<span class="ping-addr" title="{{ m.target }}">{{ m.target }}</span>
<div style="display:flex;align-items:baseline;gap:0.4rem;">
{{ status_dot('paused' if not m.enabled else ('pending' if latest is none else ('up' if latest.is_up else 'down'))) }}
<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" title="{{ h.title }}"
style="background:{{ '#1a6632' if h.state == 'up' else '#6a1515' }};"></span>
{% endfor %}
{% 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 latest is none %}<span class="ping-cur-nd">—</span>
{% elif not latest.is_up %}<span class="ping-cur-down">DOWN</span>
{% if latest is none %}<span class="tone-dim">—</span>
{% elif not latest.is_up %}{{ status_badge('down') }}
{% elif latest.response_ms is not none %}
<span style="{{ threshold_style(latest.response_ms, 'latency') or 'color:var(--green);' }}">{{ "%.0f"|format(latest.response_ms) }} ms</span>
{% else %}<span class="ping-cur-good">UP</span>{% endif %}
<span class="tone-{{ status_class(latest.response_ms, 'latency') }}">{{ "%.0f"|format(latest.response_ms) }} ms</span>
{% else %}{{ status_badge('up') }}{% endif %}
</div>
{% if session.user_role in ['operator', 'admin'] %}
<div style="display:flex;gap:0.35rem;flex-shrink:0;">
<a href="/monitors/{{ m.id }}/edit" class="btn btn-sm btn-ghost" style="padding:0.2rem 0.5rem;font-size:0.72rem;">Edit</a>
<form method="post" action="/monitors/{{ m.id }}/delete" style="display:inline;"
data-confirm="Delete monitor {{ m.name }}?">{{ csrf_field() }}
<button type="submit" class="btn btn-sm" style="padding:0.2rem 0.5rem;font-size:0.72rem;color:var(--red);">✕</button>
<div class="row-actions">
{{ icon_button("✎", "Edit " ~ m.name, href="/monitors/" ~ m.id ~ "/edit") }}
<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 %}