#5093 "Shared format filters and status_class driven by Settings → Thresholds". - steward/core/formatting.py: human_bytes (binary units), human_rate, human_duration (two largest units), ago (a <time> titled with the absolute UTC time) and status_class(value, kind, thresholds) -> ok/warn/crit/unknown, which raises on a kind the thresholds don't name. - app.py registers them as the bytes, rate, duration and ago filters and the status_class global; threshold_style is now graded by status_class (and caps its weight at 500). - Replaces the copies: docker's _human_bytes/_human_uptime, core alerts' _duration, the host-agent fmt_bps/fmt_bytes macros, the Traefik and UniFi byte-rate arithmetic, the swarm-failures ago macro and two uptime spans. - Tests: a table per helper, and every template compiled against the app's filters, so an unknown filter fails CI rather than a page. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
166 lines
10 KiB
HTML
166 lines
10 KiB
HTML
{# Current-state fragment for the full-metrics page — polled live via HTMX.
|
|
Each poll re-renders the latest snapshot the server has. #}
|
|
{% macro bar(pct) %}
|
|
{%- set p = pct if pct is not none else 0 -%}
|
|
<div style="height:8px;background:var(--bg-elevated);border-radius:4px;overflow:hidden;border:1px solid var(--border);">
|
|
<div style="height:100%;width:{{ p|round(1) }}%;background:{% if pct is none %}var(--text-dim){% elif pct < 70 %}var(--green){% elif pct < 90 %}var(--yellow){% else %}var(--red){% endif %};"></div>
|
|
</div>
|
|
{% endmacro %}
|
|
|
|
{# ── Identity / metadata ─────────────────────────────────────────────────── #}
|
|
<div class="card" style="display:flex;flex-wrap:wrap;gap:0.5rem 1.25rem;align-items:center;font-size:0.82rem;color:var(--text-muted);">
|
|
<span>{% if stale %}<span class="badge badge-red">stale</span>{% else %}<span class="badge badge-green">live</span>{% endif %}</span>
|
|
<span><span style="color:var(--text-dim);">Address</span> {{ host.address or "—" }}</span>
|
|
{% if reg %}
|
|
{% if reg.distro %}<span><span style="color:var(--text-dim);">OS</span> {{ reg.distro }}</span>{% endif %}
|
|
{% if reg.kernel %}<span><span style="color:var(--text-dim);">Kernel</span> {{ reg.kernel }}</span>{% endif %}
|
|
{% if reg.arch %}<span><span style="color:var(--text-dim);">Arch</span> {{ reg.arch }}</span>{% endif %}
|
|
{% if reg.agent_version %}<span><span style="color:var(--text-dim);">Agent</span> v{{ reg.agent_version }}</span>{% endif %}
|
|
{% set up = hostlvl.get('uptime_secs') %}
|
|
{% if up %}<span><span style="color:var(--text-dim);">Uptime</span> {{ up|duration }}</span>{% endif %}
|
|
<span><span style="color:var(--text-dim);">Last seen</span> {{ reg.last_seen_at.strftime("%Y-%m-%d %H:%M:%S") ~ " UTC" if reg.last_seen_at else "never" }}</span>
|
|
{% else %}
|
|
<span>No agent registration found for this host.</span>
|
|
{% endif %}
|
|
</div>
|
|
|
|
{% if not hostlvl %}
|
|
<div class="card empty">No metrics reported yet. Once the agent checks in, CPU, memory, network, disk I/O, temperatures, and pressure will appear here.</div>
|
|
{% else %}
|
|
|
|
{# ── Current headline gauges ─────────────────────────────────────────────── #}
|
|
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:0.75rem;margin-bottom:1rem;">
|
|
<div class="card" style="margin-bottom:0;">
|
|
<div class="section-title" style="margin-bottom:0.35rem;">CPU</div>
|
|
{% set c = hostlvl.get('cpu_pct') %}
|
|
<span class="stat-val" style="font-size:1.6rem;color:{% if c is none %}var(--text-dim){% elif c < 70 %}var(--green){% elif c < 90 %}var(--yellow){% else %}var(--red){% endif %};">{% if c is not none %}{{ "%.0f"|format(c) }}%{% else %}—{% endif %}</span>
|
|
<div style="margin-top:0.5rem;">{{ bar(c) }}</div>
|
|
</div>
|
|
<div class="card" style="margin-bottom:0;">
|
|
<div class="section-title" style="margin-bottom:0.35rem;">Memory</div>
|
|
{% set mp = hostlvl.get('mem_used_pct') %}
|
|
<span class="stat-val" style="font-size:1.6rem;color:{% if mp is none %}var(--text-dim){% elif mp < 70 %}var(--green){% elif mp < 90 %}var(--yellow){% else %}var(--red){% endif %};">{% if mp is not none %}{{ "%.0f"|format(mp) }}%{% else %}—{% endif %}</span>
|
|
<div style="margin-top:0.5rem;">{{ bar(mp) }}</div>
|
|
<div style="margin-top:0.45rem;font-size:0.72rem;color:var(--text-dim);">
|
|
avail {{ hostlvl.get('mem_available_bytes')|bytes }} · cache {{ hostlvl.get('mem_cached_bytes')|bytes }} · swap {{ hostlvl.get('swap_used_bytes')|bytes }}
|
|
</div>
|
|
</div>
|
|
<div class="card" style="margin-bottom:0;">
|
|
<div class="section-title" style="margin-bottom:0.35rem;">Load</div>
|
|
<span class="stat-val" style="font-size:1.6rem;">{% if hostlvl.get('load_1m') is not none %}{{ "%.2f"|format(hostlvl.get('load_1m')) }}{% else %}—{% endif %}</span>
|
|
<div style="margin-top:0.45rem;font-size:0.72rem;color:var(--text-dim);">
|
|
5m {% if hostlvl.get('load_5m') is not none %}{{ "%.2f"|format(hostlvl.get('load_5m')) }}{% else %}—{% endif %} ·
|
|
15m {% if hostlvl.get('load_15m') is not none %}{{ "%.2f"|format(hostlvl.get('load_15m')) }}{% else %}—{% endif %}
|
|
</div>
|
|
</div>
|
|
<div class="card" style="margin-bottom:0;">
|
|
<div class="section-title" style="margin-bottom:0.35rem;">Network</div>
|
|
<div style="font-size:0.95rem;font-variant-numeric:tabular-nums;">
|
|
<div><span style="color:var(--green);">↓</span> {{ hostlvl.get('net_rx_bps')|rate }}</div>
|
|
<div><span style="color:var(--red);">↑</span> {{ hostlvl.get('net_tx_bps')|rate }}</div>
|
|
</div>
|
|
</div>
|
|
<div class="card" style="margin-bottom:0;">
|
|
<div class="section-title" style="margin-bottom:0.35rem;">Disk I/O</div>
|
|
<div style="font-size:0.95rem;font-variant-numeric:tabular-nums;">
|
|
<div><span style="color:var(--text-dim);">rd</span> {{ hostlvl.get('disk_read_bps')|rate }}</div>
|
|
<div><span style="color:var(--text-dim);">wr</span> {{ hostlvl.get('disk_write_bps')|rate }}</div>
|
|
</div>
|
|
</div>
|
|
{% if hostlvl.get('temp_c_max') is not none %}
|
|
<div class="card" style="margin-bottom:0;">
|
|
<div class="section-title" style="margin-bottom:0.35rem;">Temp (max)</div>
|
|
{% set t = hostlvl.get('temp_c_max') %}
|
|
<span class="stat-val" style="font-size:1.6rem;color:{% if t < 70 %}var(--green){% elif t < 85 %}var(--yellow){% else %}var(--red){% endif %};">{{ "%.0f"|format(t) }}°C</span>
|
|
</div>
|
|
{% endif %}
|
|
{% if hostlvl.get('psi_mem_some_avg10') is not none %}
|
|
<div class="card" style="margin-bottom:0;">
|
|
<div class="section-title" style="margin-bottom:0.35rem;" title="Pressure stall — % of time stalled, 10s avg">Pressure (10s)</div>
|
|
<div style="font-size:0.78rem;font-variant-numeric:tabular-nums;color:var(--text-muted);">
|
|
<div>mem {{ "%.1f"|format(hostlvl.get('psi_mem_some_avg10')) }}%</div>
|
|
{% if hostlvl.get('psi_cpu_some_avg10') is not none %}<div>cpu {{ "%.1f"|format(hostlvl.get('psi_cpu_some_avg10')) }}%</div>{% endif %}
|
|
{% if hostlvl.get('psi_io_some_avg10') is not none %}<div>io {{ "%.1f"|format(hostlvl.get('psi_io_some_avg10')) }}%</div>{% endif %}
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
{# ── Per-core CPU ─────────────────────────────────────────────────────────── #}
|
|
{% if cores %}
|
|
<div class="card">
|
|
<div class="section-title" style="margin-bottom:0.6rem;">Per-core CPU</div>
|
|
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(120px,1fr));gap:0.5rem 0.9rem;">
|
|
{% for idx, pct in cores %}
|
|
<div>
|
|
<div style="display:flex;justify-content:space-between;font-size:0.72rem;color:var(--text-dim);margin-bottom:0.2rem;">
|
|
<span>core {{ idx }}</span><span>{% if pct is not none %}{{ "%.0f"|format(pct) }}%{% else %}—{% endif %}</span>
|
|
</div>
|
|
{{ bar(pct) }}
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{# ── Filesystems ──────────────────────────────────────────────────────────── #}
|
|
{% if mounts %}
|
|
<div class="card">
|
|
<div class="section-title" style="margin-bottom:0.6rem;">Filesystems</div>
|
|
{% for mount, m in mounts.items() %}
|
|
<div style="margin-bottom:0.6rem;">
|
|
<div style="display:flex;justify-content:space-between;font-size:0.8rem;margin-bottom:0.2rem;">
|
|
<span style="font-family:ui-monospace,monospace;">{{ mount }}</span>
|
|
<span style="color:var(--text-muted);">{{ m.get('disk_used_bytes')|bytes }} / {{ m.get('disk_total_bytes')|bytes }}{% if m.get('disk_used_pct') is not none %} · {{ "%.0f"|format(m.get('disk_used_pct')) }}%{% endif %}</span>
|
|
</div>
|
|
{{ bar(m.get('disk_used_pct')) }}
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
{% endif %}
|
|
|
|
{# ── Interfaces / disks (short panels, side by side at natural height) ─────── #}
|
|
{% if nets or disks_io %}
|
|
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:1rem;align-items:start;">
|
|
{% if nets %}
|
|
<div class="card" style="margin-bottom:0;">
|
|
<div class="section-title" style="margin-bottom:0.5rem;">Interfaces</div>
|
|
{% for iface, m in nets.items() %}
|
|
<div style="display:flex;justify-content:space-between;gap:0.75rem;font-size:0.8rem;padding:0.15rem 0;">
|
|
<span style="font-family:ui-monospace,monospace;">{{ iface }}</span>
|
|
<span style="color:var(--text-muted);font-variant-numeric:tabular-nums;white-space:nowrap;">↓ {{ m.get('net_rx_bps')|rate }} · ↑ {{ m.get('net_tx_bps')|rate }}</span>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
{% endif %}
|
|
{% if disks_io %}
|
|
<div class="card" style="margin-bottom:0;">
|
|
<div class="section-title" style="margin-bottom:0.5rem;">Disks</div>
|
|
{% for dev, m in disks_io.items() %}
|
|
<div style="display:flex;justify-content:space-between;gap:0.75rem;font-size:0.8rem;padding:0.15rem 0;">
|
|
<span style="font-family:ui-monospace,monospace;">{{ dev }}</span>
|
|
<span style="color:var(--text-muted);font-variant-numeric:tabular-nums;white-space:nowrap;">rd {{ m.get('disk_read_bps')|rate }} · wr {{ m.get('disk_write_bps')|rate }}</span>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
{% endif %}
|
|
|
|
{# ── Temperatures (full width; cores flow into a compact multi-column grid) ── #}
|
|
{% if temps %}
|
|
<div class="card" style="margin-top:1rem;margin-bottom:0;">
|
|
<div class="section-title" style="margin-bottom:0.5rem;">Temperatures</div>
|
|
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(130px,1fr));gap:0.1rem 1.25rem;">
|
|
{% for label, c in temps.items() %}
|
|
<div style="display:flex;justify-content:space-between;gap:0.5rem;font-size:0.8rem;padding:0.15rem 0;">
|
|
<span style="white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">{{ label }}</span>
|
|
<span style="color:{% if c is none %}var(--text-dim){% elif c < 70 %}var(--green){% elif c < 85 %}var(--yellow){% else %}var(--red){% endif %};font-variant-numeric:tabular-nums;">{% if c is not none %}{{ "%.0f"|format(c) }}°C{% else %}—{% endif %}</span>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% endif %}
|