A board's spare thermistor inputs report whatever an open input reads, and the value rule from #5759 only catches the impossible ones. Once an hour, sensors.refresh judges each sensor against the host's CPU (Package id N, Tctl/Tdie/TccdN, PECI Agent N) over a week of hourly averages. A sensor is ruled out if it falls as the CPU warms (r <= -0.5) or holds within 0.5 °C while the CPU moved 10 °C or more. A judgment needs at least 72 shared hours. The host temperature leaves judged sensors out at ingest. The host page lists them under "Not counted", with each reason given once. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
174 lines
10 KiB
HTML
174 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. #}
|
|
{% from "_macros.html" import status_badge %}
|
|
{# A percentage bar graded by Settings → Thresholds for `kind` (cpu, mem, disk). #}
|
|
{% macro bar(pct, kind) %}
|
|
<div class="meter"><div class="meter-fill meter-{{ status_class(pct, kind) }}" style="width:{{ (pct or 0)|round(1) }}%;"></div></div>
|
|
{% endmacro %}
|
|
{% macro gauge(value, kind, text) %}<span class="stat-val tone-{{ status_class(value, kind) }}" style="font-size:1.6rem;">{{ text }}</span>{% 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>{{ status_badge('stale' if stale else 'up', 'Stale' if stale else 'Live') }}</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|ago 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') %}
|
|
{{ gauge(c, 'cpu', ("%.0f%%"|format(c)) if c is not none else "—") }}
|
|
<div style="margin-top:0.5rem;">{{ bar(c, 'cpu') }}</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') %}
|
|
{{ gauge(mp, 'mem', ("%.0f%%"|format(mp)) if mp is not none else "—") }}
|
|
<div style="margin-top:0.5rem;">{{ bar(mp, 'mem') }}</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') %}
|
|
{{ gauge(t, 'temp', "%.0f°C"|format(t)) }}
|
|
</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, 'cpu') }}
|
|
</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'), 'disk') }}
|
|
</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 class="tone-{{ status_class(c, 'temp') }}" style="font-variant-numeric:tabular-nums;">{% if c is not none %}{{ "%.0f"|format(c) }}°C{% else %}—{% endif %}</span>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
{% if unwired_temps %}
|
|
{# Sensors that aren't thermometers: they run against the CPU or hold a
|
|
constant (#5783), or read a value no wired sensor gives (#5759). Left
|
|
out of the host's temperature. Each reason is said once, after its
|
|
sensors, hottest first within it. #}
|
|
<p class="field-hint" style="margin:0.5rem 0 0;">Not counted, likely nothing wired to them:
|
|
{% for why, group in unwired_temps.items()|groupby("1.1") %}{% for label, v in group %}{{ label }} {{ "%.0f"|format(v[0]) }}°C{{ ", " if not loop.last }}{% endfor %} ({{ why }}){{ "; " if not loop.last }}{% endfor %}.</p>
|
|
{% endif %}
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% endif %}
|