feat(hosts): one health header, graded by one set of thresholds (#5122)
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:
@@ -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 %}
|
||||
|
||||
Reference in New Issue
Block a user