Host resource metrics
What it is
Settings → System shows five rings for the whole host — CPU, memory, disk, IO and network — plus uptime as
a plain number. Clicking a ring opens the stored history for that resource, in the same modal the Sandbox
Project history draws: one range, the metric toggles, the shared time-series chart. Both metrics routes are
administrator-only (notAdmin → 403); the /system panel they sit beside is readable by any signed-in
account.
Owning code and consumers
- Live readings:
src/hostMetrics/resources.ts. One fresh read per request — CPU fromos.cpus(), memory fromos.totalmem()/os.freemem(), disk fromhostDiskUsageinsrc/api/systemDiagnostics.ts, IO from/proc/diskstatscounting whole physical block devices only. A device is skipped when it has no directory of its own under/sys/block(a partition), when it listsslaves(a composite such asdm-*ormd-*), or when it is a loop device, because each of these repeats I/O another line already reports. Network comes from/proc/net/devwith loopback excluded. - Sampling:
src/hostMetrics/sampler.ts, scheduled once a minute fromsrc/daemon/maintenance.ts. The daemon callsrepositories.maintenance.maintain('hostMetrics', undefined); the operation takes nonowfrom the loop and reads the host itself, so the sample's ownsampledAtis the clock it appends and prunes with. Core assembly supplies the process-owned sampler and its independent reader baseline; maintenance passes the boundevents.hostResourcesrepository to append, roll up and prune through PostgreSQL. Sampling happens outside retried SQL transactions. The first sample is requested one minute after boot. No sample is taken at boot. A failed sample reaches the daemon and is logged withdaemon.host_metrics_failed; shutdown drains admitted samples before closing the pool. - Storage and reads:
packages/db/src/events/hostResources.tsover theresource_samples(minute) andresource_rollups(hourly) tables, renamed bypackages/db/migrations/0005_resource_history_subject.sql. - Routes:
src/api/routes/system.ts—GET /system/metrics(live) andGET /system/metrics/history(stored), bothnotAdmin→ 403. - Shared vocabulary:
packages/plugin-shared/resourceHistory.mjs, the same module the Sandbox Project history uses. Metric ids, units, windows, retention and the bucket shaper live there once, so a host and a Project curve cannot round or label the same measurement differently. - Browser:
web/modules/settings/HostMetrics.tsx(rings) andweb/components/ui/ResourceHistoryModal.tsx(history), composed from the sharedResourceMeters,Modal,DateRangeFilterandTimeSeriesChart. Labels, figures and the chart come fromweb/lib/resourceHistoryChart.js, the generated byte-identical copy ofelowen-plugin-ui-kit/resourceHistorythat inlines the shared vocabulary (scripts/generate-resource-history.mjs).
Subject-aware history
events.hostResources.append(subject, samples) and read(subject, selection, now) require an explicit
'host' or 'computer:<projectId>' subject. The primary keys are (subject, metric, bucket_at);
migration 0005 preserves every existing host value under host. prepareSample still projects the
seven canonical series. The host sampler and system route pass host; CLI data messages append the
computer's first reading of each minute. Duplicate arrivals, including reconnects, cannot increment rollups twice.
prune(now) shares one 1000-row budget across all subjects and both retention tiers.
Computer appends take a key-share lock on the live local Project before writing. Project removal holds
the deletion lock and deletes both subjects' tables for that computer in its own transaction, so a late
frame cannot recreate orphaned history. Host rows remain independent. The computer read route requires
canExecute membership, including for administrators; host history stays administrator-only.
Cadence, retention and resolution
The sampler writes one sample a minute (all seven wire series) and the corresponding hourly rollup. Detail
rows live 48 hours, hourly rollups 30 days; one response carries at most 720 buckets, and an automatic
resolution moves from 1 m to 5 m to 1 h as the range grows. Retention, windows, resolution and the cap all
come from RESOURCE_HISTORY in the shared module, so the numbers the daemon stores and the numbers the
browser asks for cannot drift apart. The live route reports the sampling cadence from there, and the maintenance loop schedules the sampler with it.
Deliberate differences from the Sandbox Project history
- The host is one scope: no project id, and no machine generation to fence, so a point's
generationis alwaysnull. A bucket nothing was measured in stays an explicitmissingpoint so the curve leaves a gap. - The host has no quotas, so IO and network are absolute rates with no percentage, while CPU, memory and
disk carry a share of the host's own ceiling — its core count, its total memory and the size of the
filesystem that holds the database. Disk is sampled every 60 s rather than the Project tier's 300 s
because
statfsis free; there is no filesystem walk behind it. - A source the host cannot read is reported as
unavailable, never as a zero and never as an exception into the maintenance loop. The host has nostoppedorsamplingstate. - The completed sampler reply also carries the numeric problem-report projection described in
Problem reporting boundary. Boot caches the reply for the loop watchdog;
warning emission reads only that cache.
HostResourceReadnarrows ready readings to required figures, while the public shared resource wire shape remains unchanged.
Delta baselines
The host and CLI share the physical-disk classifier, Linux parsers and delta-per-second helper in
packages/plugin-shared/computerMetrics.mjs. CPU, IO and network are rates, so every reader keeps its own previous counters. The sampler holds its own
baselines and must never share readSystemDiagnostics()'s single previous value: two readers of one
baseline would each consume the other's delta and both would report nonsense. The live card's reader is a
third, independent baseline, which is why the route serves a freshly measured snapshot instead of reading
the stored samples back.
Wire contract
The live snapshot is declared in src/shared/wireContract.ts (HostResourceSnapshot). The history
response is the shared ResourceHistoryResponse (its series of ResourceHistoryPoint), declared once in
packages/plugin-shared/resourceHistory.d.mts; the wire contract re-exports the response and the point for the web; the
window values the route accepts are the shared RESOURCE_WINDOW_IDS.
A malformed history query is rejected at the boundary (zod) and by the shared cross-field rules; the reply
is a 400 with a stable error code (invalid_history_query for a malformed query or an invalid metric
selection, invalid_history_range, unsupported_history_resolution, range_outside_retention). An absent
history store answers 503 history_unavailable.