shipped2026-07-20draft

Server dashboard

The only monitoring on the production box. Host metrics, 24 hours of history, HTTP and SMTP probes, Discord alerts. One dependency. Behind a login, so no link.

at a glance

what
A one‑page view of the production host and the services on it, with alerts
stack
Node, one dependency (pino). No framework, no bundler, no build step: the file in git is what runs
collects
CPU, memory, swap, disk, load, uptime, network, temperatures, top processes, from /proc and /sys
keeps
A rolling 24‑hour history, downsampled, persisted to a volume so restarts lose nothing
alerts
Threshold and service alerts pushed to Discord from the process itself
access
No auth of its own: Traefik puts TinyAuth + Pocket ID (passkeys) in front of every request

What it does

One page, open on my phone or a second monitor most of the day. It shows the host’s CPU, memory, swap, disk, load and network right now and over the last 24 hours, the temperature if the hardware exposes one, the top processes, and a row of coloured checks for the things that matter: both sites, both Coolify instances, the mail relay and the sign‑in edge. Each check is an HTTP or SMTP probe on a schedule with a rolling uptime figure.

When a threshold is crossed or a check fails, the process posts to Discord. That happens whether or not a browser is open, because the server refreshes on its own timers; a connected tab is a subscriber, not the thing driving collection. Updates reach the browser over a server‑sent event stream, and the polling endpoints stay as a working fallback for proxies that buffer streams. There is also a Prometheus‑format /metrics endpoint, kept as an escape hatch.

How it’s built

server.js is wiring: credential checks, the refresh timers, listen. The substance is a dozen small modules with one concern each: config, shared state, procfs collection, history, service checks, alerts, logging, Prometheus text, the SSE stream, and HTTP routing with static serving. That granularity is deliberate, so parallel changes land in different files and so it stays debuggable at two in the morning. There is no framework, no bundler and no build step: npm start is node server.js, and the deployed artefact is the file in git.

The front end is a hand‑written index.html, app.js and app.css under a content security policy strict enough that inline style attributes are dropped, which has caused exactly two bugs and is why state is driven by classes.

The three percentages, CPU, memory and disk, are load‑bearing: every threshold compares against them and every sample in the history was recorded under their current definitions. Changing a formula means the stored history lies.

Auth

The app has none of its own, on purpose. Access control is at the edge: Traefik applies a TinyAuth middleware that requires a passkey sign‑in through Pocket ID before a request reaches the process. That is one Traefik label on the compose file; if it ever stops applying, the dashboard serves unauthenticated and nothing complains, so the check is a curl -I that expects a 401. Two systems disagreeing about who is allowed in would be worse than either alone, so the basic‑auth and per‑IP lockout code it used to carry were removed once SSO was verified in front.

Deploy and operations

Read‑only container, all capabilities dropped, no new privileges, runs as node, base image pinned by digest. /data is the only writable path, for the history. Host metrics come from three read‑only bind mounts of /proc, /sys/class/hwmon and a single file used as an anchor for the root filesystem’s statfs.

The image is built and published to GHCR on push, gated on a smoke suite that parses every source, runs the unit tests, and hits every endpoint twice: once against node server.js, once against the built container with production hardening. Then a POST to Coolify’s deploy webhook. The word POST is there because it was GET for weeks, one of three independent faults that each masked the next. That story is under broke.

The structural limitation, stated plainly: it runs on the machine it monitors, so a dead host alerts nobody. Closing that needs something off‑box, and that is not this repo’s job.