Published June 15, 2026
SENTINEL: a status page that ships zero JavaScript
SENTINEL is my homelab status page, and it ships zero JavaScript. Health is collected out of band, baked into static HTML every minute, and served from K3s. A status page should be the one page that still loads when everything else is broken.
Most status pages are single-page apps that boot a JavaScript bundle and then poll an API from your browser to discover what is up. That has always struck me as backwards. The status page is the page you load specifically when things are broken, so it should depend on as little as possible. SENTINEL, the status page for my homelab, takes the opposite position: it ships zero JavaScript. The browser receives finished HTML, baked a minute ago, and nothing else. It also happens to look like an amber CRT, because if I am going to stare at it during an outage it may as well be pleasant.
A status page is a photograph, not a video
The trick is to decouple collection from display. The browser should not be the thing that discovers state, because the moment it is, your status page depends on a working API, on CORS behaving, on a JavaScript runtime, and on the user's network all cooperating at exactly the time something is already wrong. So instead a collector runs on a schedule, probes each service, and writes the results to a small JSON file, and a template step bakes that JSON into index.html. The page the browser receives is a photograph of the system as of the last run, not a live feed it has to assemble. The cost is staleness, bounded by how often the collector runs, and the page prints its own age so that staleness is never hidden.
Refreshing without JavaScript
Keeping the photograph current needs exactly one line, and it is the oldest trick on the web:
<meta http-equiv="refresh" content="60">
The browser re-fetches the page every sixty seconds and gets whatever the collector last baked. No polling code, no websocket, no bundle to download before the page can tell you anything. For this one job it is precisely the right tool, and it has the pleasant property of working in a text browser on a phone with one bar of signal.
Phosphor amber in pure CSS
The CRT look is all CSS, with no images and no canvas. The glow is a stack of text-shadows in the amber. The scanlines are a repeating-linear-gradient laid over everything at low opacity. A status is a colored span and an uptime bar is a background gradient. Because there is no JavaScript, the entire page is a few kilobytes of HTML and one stylesheet.
:root { --amber:#f0a93a; }
.glow {
color: var(--amber);
text-shadow: 0 0 2px var(--amber), 0 0 8px rgba(240,169,58,.45);
}
/* scanlines, drawn over the whole page */
body::after {
content: ""; position: fixed; inset: 0; pointer-events: none;
background: repeating-linear-gradient(
0deg, rgba(0,0,0,.16) 0 1px, transparent 1px 3px);
}
Where it lives, and why it fails gracefully
There are two pieces on the cluster. A CronJob runs the probe-and-template step every minute and writes index.html to a small persistent volume. An nginx Deployment mounts that same volume read-only and serves it through an Ingress. That split is the entire point. An nginx pod serving a static file is about as close to unkillable as a web service gets, and it has no dependency on the collector being alive. If the collector dies, the page does not go down, it goes stale, and the timestamp on the page makes that obvious at a glance. The failure mode is a slightly old photograph, not a blank screen or a spinner that never resolves.
apiVersion: batch/v1
kind: CronJob
metadata: { name: sentinel-collect }
spec:
schedule: "* * * * *" # every minute
jobTemplate:
spec:
template:
spec:
restartPolicy: OnFailure
containers:
- name: collect
image: registry.kerboul.me/sentinel:latest
volumeMounts:
- { name: site, mountPath: /out } # writes index.html here
volumes:
- name: site
persistentVolumeClaim: { claimName: sentinel-site }
# nginx mounts the same PVC read-only and serves /out. It never calls the collector.
The next iteration writes a small history file so the uptime bars cover the last ninety days rather than just now, still baked at build time, still zero JavaScript on the client. If you have built a status page that survives its own backend going down, I would like to see where you drew the line.