A single-page dashboard for one person's infrastructure. Home Assistant lights and climate, VPN tunnels, Railway deployments, OpenRouter credit, Navidrome, Docker containers and host metrics — grouped into visual regions on one uniform grid, served over Tailscale.
Tiles with history draw a faded 24-hour sparkline behind their numbers, and the header dropdown hides whole categories. No client-side data fetching, no build step. One request fans out to every service concurrently, and the page reloads itself every 60 seconds.
python demo.py --render --open # see it immediately, with synthetic data
Windows users can double-click demo.bat instead — it creates its own
virtualenv on first run.
demo.py renders the real template against entirely invented data. No network
calls, no credentials, no /proc, nothing personal — it runs anywhere,
including Windows, and is the fastest way to see the layout and to record a
screencast.
demo.bat / python demo.py --render --open |
writes demo.html and opens it |
demo-serve.bat / python demo.py --serve --open |
serves on 127.0.0.1:9121 with the 60s auto-refresh live |
The demo shows eight categories and deliberately includes unhealthy states — a failed deployment, a degraded tunnel, a nearly-exhausted API key, a low battery — so the colour language is visible rather than uniformly green. It also exercises the sparklines, including the two-series Aranet tile (temperature with CO₂ overlaid) and the VPN latency charts.
python3 -m venv venv
./venv/bin/pip install -r requirements.txt
./venv/bin/python dashboard.py # http://127.0.0.1:9120The host fetchers read /proc, df, uptime and ps, so the live dashboard is
Linux-only. Bind it to your tailnet address and run it under systemd — see
Deploying as a service.
→ AGENTS.md is the setup guide: per-service playbooks with the exact credential, where it goes, how to verify it, and which tiles appear. It is written for an AI agent doing the wiring, which also makes it the most direct reference for doing it by hand.
Every integration is optional and independent. A service whose credential or path is missing contributes no tiles and its category disappears — so you can start with one and add the rest later.
| Service | Needs | Shows |
|---|---|---|
| Home Assistant | long-lived access token | lights with brightness, CO₂/climate, weather, batteries, entity counts, media players |
| Railway | account token | projects, services, latest deployment status |
| OpenRouter | provisioning key | credit balance and spend, per-key limits and usage |
| Navidrome | readable SQLite path | artist/album/song counts, last scan, container state |
| Docker + Caddy | docker group |
container inventory with status and ports |
| Hermes agent | HERMES_HOME |
memory counts, gateway health, MCP errors |
| Host metrics | nothing | memory, load, disk, uptime |
| VPN servers | not yet implemented — tile type ready, fetcher isn't (worked example) |
Secrets are read from ~/.hermes, outside the repo, so they cannot be swept up
by git add. See .env.example for the format.
The dashboard has no authentication and displays account balances and infrastructure state. Bind it to a Tailscale address, not a public interface. If it must be reachable off-tailnet, put a real authenticating proxy in front of it.
Every tile is a direct sibling of one uniform CSS grid. Categories are not
wrapper elements — they are SVG regions drawn over the cells, because wrapping
is what creates ragged empty space. A packer assigns CSS order so each
category occupies a contiguous, connected run of cells, widening a tile to
absorb any row remainder so there are no holes. Column-count changes animate
with FLIP.
templates/dashboard.html documents the reasoning inline, including the
off-by-one in the connectivity bound (n > cols, not n >= cols) that would
otherwise split a category into two islands. window.__regions().split must
always be empty.
Sparklines sit outside that flow deliberately: each chart is absolutely positioned behind its tile's text, so a charted tile is exactly as tall as an uncharted one and mixed rows have no dead space. Hiding a category repacks the grid the same way a resize does. Both are covered in AGENTS.md.
GPL-3.0. You may use, modify and redistribute this, including commercially, but distributed modifications must also be GPL-3.0 and ship their source. The copyright line in each source header reads "Nermal Dashboard contributors" — replace it with your own name or handle if you prefer.
dashboard.py fetchers, tile building, the one route
demo.py synthetic data; renders or serves the demo
templates/dashboard.html the whole frontend — styles, tile types, layout engine
static/backdrop.jpg faint background texture
nermal-dashboard.service systemd unit template
AGENTS.md service-by-service setup guide
demo.bat, demo-serve.bat Windows launchers (self-bootstrapping)
LICENSE GPL-3.0