feat: machine metrics (CPU, memory, network) on the status page

The status page answered "are the components running" but said nothing
about the machine underneath them, so a server slowed to a crawl by a
busy processor or one about to have Postfix OOM-killed looked entirely
healthy until the queue backed up.

internal/health/machine.go reads the kernel's counters in /proc: the
aggregate processor times and core count from /proc/stat, the load
average from /proc/loadavg, memory and swap from /proc/meminfo, and
per-interface byte counters from /proc/net/dev.

CPU busy time and network throughput are rates, so a MachineSampler holds
the previous reading and each call reports the difference — one shared
sampler on the Server, since a per-request one would never have anything
to subtract. A window longer than a minute only re-baselines: a page
opened after the panel sat idle would otherwise average that whole
stretch and present it as the current load.

Memory is derived from MemAvailable rather than MemFree, because Linux
spends every spare page on cache and MemFree would report a permanent
emergency. A fully busy processor (>=90%) warns and an exhausted machine
(>=97%) errors, both counting towards the page's headline verdict, since
either delays or kills the mail path. Throughput has no comparable
threshold — what counts as a lot depends on the link — so it is reported
and never graded. Loopback is excluded: that traffic is the container
talking to itself.

Like every other check here, an unreadable counter degrades to "unknown"
with an explanation instead of failing the page, so the panel still runs
outside Linux for development.

The usage bars are <meter> elements. The panel's CSP has no inline-style
exemption, so a bar's length has to travel on an attribute; the element
also grades its own colour from low/high/optimum, and the percentage is
printed beside it for anything that does not render meters.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-07 03:49:23 +03:00
parent 3b0219f0eb
commit 251344f880
10 changed files with 1021 additions and 18 deletions
+14
View File
@@ -235,6 +235,20 @@ details form { margin-top: 0.6rem; }
.st-warn { background: var(--st-warn-bg); color: var(--st-warn-fg); border-color: var(--st-warn-border); }
.st-error { background: var(--st-error-bg); color: var(--st-error-fg); border-color: var(--st-error-border); }
.st-unknown { background: var(--st-unknown-bg); color: var(--st-unknown-fg); border-color: var(--st-unknown-border); }
/* Usage bars on the status page's machine card. <meter> rather than a div sized
from the reading, because the CSP forbids inline styles (see the note at the
top of this file) and a bar's length has to travel on an attribute. The
element grades itself from low/high/optimum, so the colour matches the
badges' meaning without this file restating the thresholds — and a browser
that does not render meters falls back to the percentage beside it, which is
printed either way. */
meter { width: 5rem; height: 0.7rem; vertical-align: middle; margin-right: 0.4rem; }
/* The card's own two narrow columns. Cells may break mid word by default (see
the th, td rule above), which the detail column needs and these two must not
have: the resource names and the readings are short, and the long detail
beside them would otherwise win the width and leave "Memory" broken across
two lines. */
.metric { white-space: nowrap; }
.code-row { display: flex; align-items: flex-start; gap: 0.5rem; }
.code-row .code { flex: 1; min-width: 0; }
/* Compact outlined button: same affordance as the filled one but quiet enough