Skip to content
rosselmPublic

About

Live metrics dashboard for a Raspberry Pi — Go backend, no dependencies, frontend embedded in the binary

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

pidash

ci

A live metrics dashboard for this Raspberry Pi. Go backend, no dependencies, frontend embedded in the binary.

https://<node>.<tailnet>.ts.net:8443/     # via `tailscale serve`, real cert
http://127.0.0.1:8090/                    # what pidash itself listens on

install.sh prints whichever applies when it finishes. The service binds loopback by default because the API is unauthenticated — see HTTPS.

Download

Grab a prebuilt binary from Releases — no Go toolchain needed, and no runtime dependencies, since the frontend is embedded in the binary:

curl -LO https://github.com/rosselm/pidash/releases/latest/download/pidash-linux-arm64
chmod +x pidash-linux-arm64
./pidash-linux-arm64 -addr 127.0.0.1:8090

(-addr :8090 listens on the LAN instead, in the clear — see Security before doing that.)

pidash-linux-arm64 covers a Pi 3/4/5 on 64-bit Raspberry Pi OS; armv7 and armv6 builds cover 32-bit installs and the Zero. To run it as a service instead, clone and use install.sh — see Install.

Why it stands apart from your telemetry pipeline

If a Pi ships its metrics to a collector elsewhere, the dashboard goes dark at precisely the moment you want it: a network partition, a full disk, a wedged agent. pidash reads /proc, /sys and the VideoCore firmware directly and serves its own UI from the same binary, so it keeps working when the telemetry path does not. It shares no code, no configuration and no port with whatever agent is already running.

What it shows

Panel Source
Temp / CPU / memory gauges /sys/class/thermal, /proc/stat, /proc/meminfo
Processor history, per-core bars /proc/stat deltas, /sys/.../cpufreq
Thermal & throttle flags vcgencmd get_throttled / measure_volts core
Memory breakdown, load /proc/meminfo, /proc/loadavg
Storage /proc/mounts + statfs(2)
Network /proc/net/dev deltas, /sys/class/net/*/operstate
Containers Docker Engine API over /var/run/docker.sock
Services systemctl show
Top processes /proc/<pid>/{stat,statm,cmdline,status}
Journal drawer (J) one shared journalctl -f -o json, whole journal by default

Throttle flags are reported in both tenses: the low bits of the firmware word are "right now", the same bits 16 places up are "since boot". This board currently reads 0x50000 — under-voltage and throttling have occurred, nothing active. That is a power-supply signal worth keeping visible, which is why a past-only flag still lights the panel amber.

The journal drawer

The journal is not a card. It lives in a drawer fixed to the bottom of the viewport, because a log panel at the end of a long grid is only reachable by scrolling past everything else — and the moment you actually want logs is the moment you are staring at a spike somewhere further up the page.

  • J (or backtick) toggles it, Esc closes it.
  • The whole journal is tailed by default. The unit dropdown is built from what actually arrives, so it lists the units that are really logging rather than a list configured up front. -log-units narrows it at the source.
  • Drag its top edge to resize; open/closed and height are remembered.
  • It follows the newest line by default and re-arms as soon as you scroll back down. ↓ latest appears while you are scrolled away.
  • While the drawer is shut, the journal button in the top bar counts anything at warning priority or worse, so a failure still announces itself.

500 lines stay searchable in the buffer; 300 are rendered. New lines are appended rather than re-rendering the list, so the arrival animation fires only on genuinely new rows.

Rearranging the dashboard

Every card is movable and resizable, and the arrangement is remembered per browser:

  • Drag a card by its header to move it. A dashed placeholder shows where it will land.
  • Drag the grip in the bottom-right corner to resize: horizontally it snaps to the 12-column grid (3–12 columns), vertically it sets a pixel height. A readout shows span/12 · height while you drag.
  • reset layout in the top bar restores the defaults. It only appears once you have actually changed something.

Layout lives in localStorage under pidash.layout.v1 — order, spans and heights, nothing else. It is per-browser, not shared between devices, and the server never sees it. Charts repaint through a ResizeObserver, so a resized card redraws its chart at the new size rather than stretching a stale bitmap.

Below 1280px the grid reflows on its own and saved spans give way to the responsive rules; the three gauges stay on one row as far down as they fit.

Install

./install.sh          # build, install to /usr/local/bin, enable the unit
systemctl status pidash
journalctl -u pidash -f

Re-run install.sh after any change; it rebuilds and restarts in place.

The service runs as a login user rather than a dedicated service account, because it needs three group memberships: docker (the socket), video (/dev/vchiq, for vcgencmd) and adm (the journal). install.sh sets User= to whoever runs it and warns about any of the three that are missing. The unit is otherwise read-only — ProtectSystem=strict, ProtectHome=read-only, NoNewPrivileges.

Two hardening settings are deliberately not tightened further:

  • PrivateDevices stays off. Turning it on hides /dev/vchiq, which silently removes SoC temperature, core voltage and the throttle flags.
  • RestrictAddressFamilies includes AF_NETLINK. Go enumerates interface addresses over netlink; without it every interface renders with a blank IP.

Local configuration

Site-specific settings belong in a systemd drop-in rather than in the unit that ships here, so install.sh never overwrites them and they never end up in version control:

# /etc/systemd/system/pidash.service.d/local.conf
[Service]
ExecStart=
ExecStart=/usr/local/bin/pidash -addr 127.0.0.1:8090 -units ssh,docker,my-agent
sudo systemctl daemon-reload && sudo systemctl restart pidash

Flags

-addr           :8090                    listen address
-interval       1s                       sampling interval
-proc-interval  3s                       how often to walk /proc for the process table
-top            8                        processes in the top table
-expose-cmdline false                    publish full process command lines
-units          ssh,docker               systemd units for the Services panel
-log-units      (empty)                  units to tail; empty = the whole journal
-docker-sock    /var/run/docker.sock     empty to disable the panel
-version        print version and exit

-proc-interval is separate from -interval because walking every /proc/<pid>/stat is by far the most expensive thing done per tick, and a top-N table does not need to move as fast as a gauge. Measured on an idle Pi 4 with ~190 processes, over two 60-second windows:

-proc-interval CPU used in 60s share of one core
1s 1.24 s 2.07%
3s (default) 0.89 s 1.48%

A monitoring tool that perturbs what it measures is a problem, so this is worth re-checking after a change: systemctl show pidash -p CPUUsageNSec twice, a minute apart, is enough.

API

Three endpoints, all useful from the shell:

curl -s localhost:8090/api/snapshot | jq .     # one pretty-printed sample
curl -sN localhost:8090/api/stream             # SSE, one snapshot per interval
curl -sN localhost:8090/api/logs               # SSE, journal lines
curl -s localhost:8090/healthz

/api/stream and /api/logs are Server-Sent Events. One sampler goroutine feeds every connected browser: opening ten tabs does not multiply the /proc traffic, and all tabs agree on the same CPU percentages.

Security

There is no authentication. Anyone who can reach the port can read every metric. That is a deliberate fit for a trusted home LAN, but it has one consequence worth stating plainly:

/proc/<pid>/cmdline routinely contains credentials — --token=, --password=, --api-key= — passed as flags. On an ordinary Pi a handful of the running processes will match that pattern. So only argv[0] is published: the executable path, never the arguments. -expose-cmdline opts back in to full command lines and logs a warning at startup.

The shipped unit therefore binds loopback only — see HTTPS for putting a certificate in front of it, or for the SSH-tunnel alternative. Pass -addr :8090 to listen on the LAN in the clear, which is worth doing only on a network you fully trust.

The service itself needs no privileges beyond the three groups in the unit, and never writes anything: ProtectSystem=strict, ProtectHome=read-only, NoNewPrivileges.

HTTPS

pidash speaks plain HTTP and has no certificate handling of its own — that is deliberate. Put a TLS terminator in front of it and bind pidash to loopback, so the unencrypted port is not spoken on the wire at all. That is what the shipped unit does:

ExecStart=/usr/local/bin/pidash -addr 127.0.0.1:8090

With Tailscale (no certificate management, no exposure)

tailscale serve obtains and renews a real Let's Encrypt certificate for the node's MagicDNS name and proxies to pidash over loopback. Nothing is published to the internet, and nothing needs renewing by hand:

sudo tailscale up
sudo tailscale serve --bg --https=8443 http://127.0.0.1:8090
tailscale serve status

Requires MagicDNS and HTTPS Certificates enabled for the tailnet, under DNS settings. The result is a genuine padlock at https://<node>.<tailnet>.ts.net:8443/, reachable from any device signed into the same tailnet and from nowhere else — the name is NXDOMAIN on public resolvers.

The trade-off is that every device that wants in has to join the tailnet. For a name that resolves on a plain LAN with no client software, register a domain, point an A record at the Pi's private address (private IPs are legal in public DNS) and issue a certificate by DNS-01 challenge, which needs no inbound ports either.

Why 8443 rather than 443

If k3s is running on the same host, its servicelb installs an iptables rule with no destination-address match:

-A CNI-DN-... -p tcp -m tcp --dport 443 -j DNAT --to-destination 10.42.0.7:443

Every packet to port 443 on any local address is redirected to Traefik, including the Tailscale address — so tailscaled binds 100.x.y.z:443 successfully and still never receives a connection. The symptom is a CN = TRAEFIK DEFAULT CERT certificate where a Let's Encrypt one was expected. Serving on 8443 sidesteps it without touching the cluster.

Reaching it without Tailscale

An SSH tunnel needs nothing installed on the Pi:

ssh -N -L 8090:127.0.0.1:8090 pi@<host>   # then open http://localhost:8090/

Diagnosing under-voltage

The firmware's sticky "since boot" throttle bits tell you under-voltage happened, but not when, for how long, or under what conditions — and they cannot be cleared without a reboot. tools/psu-test.sh pins every core while sampling the live bits each second, so a marginal supply becomes a measurement:

./tools/psu-test.sh            # 20s idle, 90s loaded, 20s idle
LOAD=180 ./tools/psu-test.sh   # longer soak
  baseline  ....
  load      .!!!!!!.!!!!!!
  cooldown  !!!!

  under-voltage : 16 of 22 seconds (73%)
  longest spell : 10 seconds
  while loaded  : 12 of 14 (86%)
  while idle    : 4 of 8 (50%)
  temperature   : 42.3 -> 48.2 C

  VERDICT: under-voltage even at idle — suspect the supply or its cable.

Run it once as a baseline, then again after changing exactly one thing — a different wall socket, no extension lead, another supply — and compare. It exits non-zero when under-voltage is seen, so it also works in a cron check. Temperature is reported alongside so a hot board is not mistaken for a weak supply; thermal throttling starts far above anything a supply problem produces.

Needs nothing installed: vcgencmd, awk and a shell.

Tests

go test ./...          # parsers, decoders, and the sampler's ranking
go test -race ./...    # note: does not run on a Pi, see below

The suite covers the pure functions — /proc/stat, /proc/meminfo, /proc/net/dev and /proc/<pid>/stat parsing, the throttle bit-word, journald's two MESSAGE encodings, Docker's CPU-percent and page-cache arithmetic, systemctl show records, and process ranking — at 90–100% each. Two of them are regression tests for bugs that actually shipped: filesystems deduplicating by device, and argv never being published by default.

-race aborts on a Raspberry Pi with ThreadSanitizer: unsupported VMA range (the Pi kernel uses a 39-bit address space, TSan wants 48). CI runs it on amd64.

Known limitation: cgroup memory accounting is off by default

/sys/fs/cgroup/cgroup.controllers lists cpuset cpu io pids — no memory. Raspberry Pi OS ships that way. The consequence is that Docker's stats endpoint returns an empty memory_stats and systemd reports MemoryCurrent=[not set], so neither container nor service memory is directly readable.

pidash works around it by summing the RSS of every process in the cgroup (cgroup.procs is present regardless of controller). That over-counts shared pages slightly, but it is a real number rather than a dash.

To get exact accounting instead, append to /boot/firmware/cmdline.txt (one line, space-separated) and reboot:

cgroup_memory=1 cgroup_enable=memory

That costs a small amount of kernel memory overhead and requires a reboot, so it is left as a deliberate choice rather than done for you.

Checking the UI

pidash is a visual program, and for several commits it was verified only by reading its own source — which is how a table whose numeric columns collided, and a Storage panel listing the root filesystem three times, both shipped. tools/uicheck.py drives the running dashboard in headless Chromium and fails on the things source review cannot see:

sudo apt install chromium
python3 -m venv .venv && .venv/bin/pip install playwright
.venv/bin/python tools/uicheck.py --url http://localhost:8090 --out /tmp/shots

It asserts no console errors, no horizontal overflow at 1680/1280/980/820/420px, no card clipping its own content, no table cell wrapping, that the drawer opens on J, and that the per-core bars, filesystems and four throttle flags all render. It also writes screenshots of the dashboard and the open drawer.

Playwright ships no arm64 browser build, so it drives the system Chromium and downloads nothing.

Layout

main.go       flags, HTTP routes, SSE plumbing
sampler.go    the single sampling loop; fans one snapshot out to all viewers
metrics.go    /proc, /sys and vcgencmd readers
docker.go     Engine API over the unix socket
units.go      systemd unit state
logs.go       shared journal tail with a replay backlog
web/          index.html, style.css, app.js — embedded via go:embed

Standard library only. The frontend has no build step and no external assets: no CDN, no web fonts, so it loads fine on a LAN with no internet route.

Licence

MIT — see LICENSE.

About

Live metrics dashboard for a Raspberry Pi — Go backend, no dependencies, frontend embedded in the binary

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages