thenug.net

live server stats on a static site

the stats page on this site shows live CPU, memory, GPU, and disk usage from my home server. here's how it works given that the rest of the site is fully static.

the problem

this site is built with Eleventy — a static site generator. everything is pre-rendered HTML at build time. there's no database, no server-side rendering, no runtime templating.

that's great for a blog. not so great for live data.

the solution

the site is served by Fastify, a Node.js web framework. Fastify handles static files just fine, but it can also run real API routes alongside them.

so the stats page is a static HTML file with a small <script> that fetches /api/stats — a route handled by Fastify at runtime, not at build time.

browser → GET /stats/        → Fastify → static HTML file
browser → GET /api/stats     → Fastify → fetches Glances → returns JSON

Glances

Glances is a system monitoring tool with a built-in REST API. it runs as a container on my GPU server and exposes metrics at :61208/api/4/.

the Fastify route fetches six endpoints in parallel and combines them:

const get = (url) => fetch(url).then(r => r.json()).catch(() => null);
const [cpu, mem, fs, gpu, uptime, load] = await Promise.all([
  get(`${GLANCES}/cpu`),
  get(`${GLANCES}/mem`),
  get(`${GLANCES}/fs`),
  get(`${GLANCES}/gpu`),
  get(`${GLANCES}/uptime`),
  get(`${GLANCES}/load`),
]);

the GPU server is on my LAN at 10.0.3.21 — the blog container on 10.0.3.11 can reach it directly. the browser can't, which is why the proxy route is needed.

the filesystem problem

Glances runs in a Docker container. by default it can only see Docker's own bind mounts — not the host's drives. to fix this, the host filesystem is mounted into the container:

environment:
  - HOST_FS=/host
volumes:
  - /:/host:ro

with HOST_FS=/host set, Glances reads disk stats from /host/mnt/... instead of /mnt/.... the stats page strips the prefix back off before displaying it.

one quirk: the mergerfs union mount (/mnt/media) isn't visible inside the container even with the host bind mount — FUSE filesystems don't cross that boundary. so the individual drives show up instead, which is actually more useful anyway.

the page

the stats page is a regular Eleventy page (stats.njk) using the same base layout as the rest of the site. it fetches /api/stats on load and every 30 seconds, then updates the DOM directly — no framework, just a few document.getElementById calls.

bars turn orange at 80% usage. GPU is hidden if unavailable.

the whole thing is about 80 lines of vanilla JS and stays consistent with the rest of the site's no-dependency philosophy.

eli5

the blog is a pre-printed book — all pages are made in advance, so nothing can be "live." but the stats page needs real-time numbers, which is a problem. the solution: the delivery person (Fastify) has a side window. when your browser asks for the stats page, it gets a normal pre-printed page, but that page immediately calls back through the side window and asks "hey, what are the current numbers?" Fastify runs to the server room, reads the thermometer (Glances), and passes back the answer. your browser updates the page with live numbers every 30 seconds, without the rest of the site needing to be anything other than a static book.


← back to blog