PERSONAL PROJECT · 2026

Pi Dashboard

ROLE
Design, build, and operation
TIMELINE
1 week to build · ongoing since Jul 2026
TEAM
Solo · AI as a working partner

A local status page for my homelab. It watches a Raspberry Pi, an Unraid NAS, and a Jellyfin VM. It checks whether backups finished, reads drive health, and estimates what the GPUs cost me in electricity. It runs on my network and replaces the five SSH sessions I used to open.

01 · AT A GLANCE

7

SYSTEMD TIMERS

60s

REGENERATION INTERVAL

0

NON-STDLIB DEPENDENCIES

02 · THE DASHBOARD

The dashboard starts with what needs me.

Attention first, then updates

First I see whether every collector is current and whether anything needs attention. That green line matters because it tells me the rest of the page is safe to read.

Backups come next, with file and byte totals checked against the source. A job can exit cleanly and still copy nothing.

Drive health, in the language SMART uses

PASSED, temperature, reallocated and pending sectors, powered-on hours. No invented health score sitting on top of it.

If a value is unavailable, such as bad-sector counts on the NVMe drive, the dashboard shows a question mark. Showing zero would look like good news.

The GPUs, and what they cost

Load, VRAM, temperature, power draw and fan, per card, across both hosts. Underneath, the running energy record and an estimated cost shown as a range.

Idle draw is the number I actually wanted. Roughly 15 W per card, all day, is what a homelab costs when nobody is using it.

Deeper views behind the summary

The front page answers the four questions. The energy page, backup timeline, and full SMART history sit one click away.

Devices can be renamed to something human, because “an unrecognised MAC address on the network” is not a useful sentence.

The only write endpoint in the whole service is that rename.

03 · THE PROBLEM

It started with Pi-hole. Then the commands began piling up.

I bought the Pi to filter DNS and learn more about my network. Soon I was using it to check backups, storage, drive health, and which machine was running what. Every useful command became another thing I had to remember.

I kept thinking, I should put that on the dashboard.

The data was there. It was scattered across machines, command lines, and tools that never showed the whole picture.

I was building this for myself, which made it easy to keep adding things. The harder part was deciding what deserved the front page.

How can one page tell me what needs attention, what changed, and whether the data is still current?

What I needed from it

A quick check before work, after a backup, or whenever a fan spun up. No terminal archaeology. No made-up health score. Just enough context to decide whether I needed to act.

THE FOUR QUESTIONS

I could check every piece. I just could not check them together.

The answers existed, but each lived behind a different command on a different machine. I wanted one page to tell me: is anything broken, did the backups finish, are the drives healthy, and what are the GPUs doing when nobody is using them?

Where everything lived

Four machines, four different ways of asking

The Pi knew its own load and temperature. The NAS knew about the array and the GPU. The Jellyfin VM knew about transcoding. The backup drive knew whether last night ran. None of them knew about each other.

What that cost:

Remembering which command answered which question, on which host

No way to see whether a problem was one machine or all of them

Checking took long enough that I mostly did not bother until something broke

The four questions

Everything on the page had to earn its place against one of these

I wrote them down before building anything, and they became the filter. If a panel did not help answer one of the four, it did not go on the front page.

What I actually wanted to know:

Is anything broken right now?

Did the backups really finish?

Is the hard drive healthy?

What are the GPUs doing when nobody is using them?

04 · HOW IT WORKS

Seven small jobs collect the data. One page pulls it together.

Each collector runs on the schedule its data deserves: temperatures often, SMART data less often, tariffs monthly. They write to one local store, and a Python generator rebuilds the page every 60 seconds.

Cadence per collector

Drives do not change minute to minute. Containers do.

Seven systemd timers, each picked to match how fast the underlying thing moves. Running everything at the fastest interval would cost more and tell me nothing extra.

The schedule:

Dashboard generation every minute · Docker every 2 · alerts every 5 · network scan every 10

SMART hourly, because drive health does not change faster than that

Backup verification daily, checksum verification monthly

Live metrics read at generation time from /proc and /sys, Jellyfin’s HTTP API, and the GPUs over SSH

How it stores and serves

SQLite for history, JSON for right now, static HTML for the page

Two stores because they answer different questions. History has to survive restarts and be queryable over years; current state only has to be correct this minute and get handed between collectors.

The split:

SQLite holds energy intervals, SMART readings, tariff rates and collector health

Small JSON snapshots carry live state and collector handoff

What it admits it does not know

A stale reading that looks live is worse than no reading

Where that shows up:

An attention panel that leads with whether collectors are current, before any number

Collector health tracked in the database alongside the data it collects

I treated AI like an orchestra. I composed the system: what each collector should listen for, how the parts should work together, and what counted as trustworthy. AI helped me build, test, and debug faster. I made the calls and kept tuning the result against the real machines.

05 · WHAT ALMOST BROKE IT

The SSH connection was fine. My safer key returned one field fewer than the parser expected.

The GPU panel went blank, and I blamed SSH. The connection was healthy. The restricted key returned 11 CSV fields; the parser expected 12 and discarded the reading. I fixed the parser, kept the safer key, and added a regression test for that exact shape.

What I locked down

Where AI helped, and where it did not

I treated AI like an orchestra. I decided what each part needed to do and how the pieces should work together. It helped me draft small collectors, look for security problems, and think through failure cases. I still tested every result against the actual Pi and network and rewrote anything I could not explain.

Where the security work ended up:

Runs as an unprivileged pi-dashboard user under systemd hardening

Credentials moved out of the application folder, and root-only checks separated from the web service

GPU SSH keys restricted to a single command

Dashboard state kept on the Pi’s internal storage, never on the external NAS backup drive

06 · MORE OF THE DASHBOARD

The number tells me now. The shape tells me whether now is normal.

The trend strips stay wide and short so spikes are easy to spot. A temperature of 63°C means more when I can see whether it has been flat all day or climbed in the last few minutes.

CPU load, temperature, and network activity keep their own scales so one quiet metric cannot flatten the rest.

07 · MEASURING HONESTLY

Turning GPU draw into a dollar amount is where the estimate can get dishonest.

The Pi samples each GPU over SSH, turns those readings into watt-hours, then applies the local electricity tiers. I show a range because missing samples, changing tariffs, and board power all limit how exact the number can be.

How the measurement works

One-minute samples, accumulated into watt-hours

A restricted SSH key runs a single nvidia-smi command against each GPU. What comes back is power draw in watts at that instant. Sampled every minute and integrated over time, that becomes energy, and energy is the thing you can put a price on.

The chain:

One-minute samples written to SQLite as energy intervals

Intervals summed into watt-hours per day, then priced against the tariff

Why it is a range, not a number

A single figure would be more satisfying and less true

What the estimate includes and excludes:

Tier 1 at 7.065¢/kWh and Tier 2 at 11.142¢, both shown with 14.975% tax applied

The 46.154¢ daily system access charge is excluded, because GPU use does not cause it

It covers GPU board power only. It is not a whole-home meter and does not claim to be

Keeping the inputs honest

The tariff is data too, and data goes out of date

An estimate is only as current as its assumptions. The rate the dashboard prices against is checked against the official source monthly, and every change is recorded with both the date it took effect and the date it was detected. Those are not the same date.

How gaps are handled:

A missing reading renders as a gap, never as a zero

08 · LOOKING BACK

I would keep the honesty and fix the gaps.

The last 30-day window showed 73.2% coverage, and I still have not found why some samples disappear. The NVMe SMART parser also needs its own path, alerts need to push, and the Pi needs a watcher of its own.

I would keep the decision to show gaps instead of smoothing them over. The dashboard is useful because it admits what it knows, when it last checked, and where the estimate stops.

IF YOU GOT THIS FAR...

Thank you.