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.