entry-18
When the homelab outgrew its spreadsheet, I built a command center
Building a private FastAPI and React dashboard around the lab inventory: 23 seeded device records, 14 services, Prometheus telemetry, audited changes, and explicit topology relationships.
revisions rev. 1 first set 2026-10-23 last revised 2026-10-23 7 min read
The lab inventory was scattered across a spreadsheet, the router’s client list, the network controller, Pi-hole, Proxmox, and a docs repo. Grafana showed measurements, but answering which service belonged to which machine still meant opening several places.
The spreadsheet could hold an address and a note. It did not give me a useful view of duplicate addresses, the services on a host, or the difference between a planned change and an observed status transition. Those were the questions I wanted the dashboard to answer.
The app brings inventory, measurements, and change records into one private interface. As of 2026-10-05, the checked-in seed has 23 device records and 14 services. It includes the current gateway and switch, three Wi-Fi access points, three Proxmox hosts, the DNS pair and virtual IP, and retired hardware. That count describes inventory, not 23 running physical machines or a live discovery result.
The inventory model
Devices carry a name, hostname, IP, MAC, VLAN, role, OS, address assignment type, DNS record, location, criticality, and notes. Services identify their host device. VLANs carry the CIDR, gateway, DHCP range, and firewall notes.
Phases track project status, goals, validation evidence, and lessons. Docs is a categorized link library. The manual Change Log records the reason for a change, affected systems, impact, rollback plan, and result.
| Layer | Choice |
|---|---|
| Backend | FastAPI, Motor, PyJWT, bcrypt |
| Database | MongoDB 7 |
| Frontend | React 18, React Router, Tailwind, shadcn-style components, sonner |
| Authentication | JWT Bearer token with an httpOnly cookie fallback |
The interface is a dark NOC dashboard, with dense tables and monospace addresses and measurements. Navigation groups inventory, topology, monitoring, and management. A device has its own routed workspace for the overview, telemetry, network fields, hosted services, and changes.
It is intended for LAN or Tailscale access. Prometheus supplies local operational telemetry; this is separate from sending product analytics to a third party. Webhook notifications go to the destination I configure.
Two roles and an explicit auth tradeoff
The API supports admin and viewer roles. Inventory writes require an admin; viewers can look up the same records without changing them. The frontend uses the role to expose editing controls, and the API enforces the permission.
Requests send the JWT as a Bearer header from localStorage, with an httpOnly cookie fallback. This supports the deployment paths the app was built around, but localStorage makes the token accessible to page JavaScript. The security notes document that tradeoff. Private access does not remove the need to protect the frontend from script injection.
An axios interceptor routes 401 responses to login. An ErrorBoundary gives a failed component a visible error state instead of a blank page.
For a fresh local installation:
cp .env.example .env
# Set JWT_SECRET, ADMIN_PASSWORD, and VIEWER_PASSWORD before starting.
docker compose up -d --build
The startup seeder creates the two accounts from configuration. The example credentials are defaults to replace before using the app.
Keeping the seed honest
The seed contains 23 device records, one flat-LAN network record, 14 services, five phases, and five documentation links. Its device mix covers the current gateway and switch, three Wi-Fi access points, three Proxmox hosts, the DNS pair and virtual IP, backup and monitoring roles, and retired hardware. Retired physical DNS nodes remain visible with a retired status.
This is a versioned starting inventory. It is not an automatic discovery feed, and a seed revision can fall behind a hardware replacement. Addresses and roles still need reconciliation against operational evidence.
Startup upserts these collections by their natural keys, such as device name and VLAN ID. A repeated run updates matching rows in place rather than duplicating them. It also overwrites the seed-defined fields on those rows, so edits to a seeded address or role do not automatically survive a restart. User-created rows with different keys are outside those upserts; a separate cleanup removes explicitly named legacy seed entries.
That boundary matters if the app is going to hold trusted inventory. The seed is part of the configuration to review when an infrastructure change becomes permanent.
The Change Log starts empty. Earlier fabricated example incidents are explicitly removed by the seeder. A sample outage or sync failure would look like operational evidence once it appeared in the timeline, so it does not belong there.
Measurements alongside records
Active checks run in a background task attached to the FastAPI lifespan. They use ICMP with an HTTP fallback and record when a device was checked and last seen. Maintenance and retired devices are skipped. The default outage threshold is three consecutive failed checks.
Prometheus integration is already implemented. The backend makes read-only queries against the existing Prometheus server, with typed responses and a short cache. It does not add another scraper.
The dashboard and device workspaces show CPU, memory, root-filesystem usage, and other available node data. A dedicated Live Telemetry page lists scrape targets with filters and polling. Inventory fields still describe the intended configuration; a reachable device or a successful scrape only establishes the observation made by that check.
Audit events and alerts
I use the manual Change Log to explain each change. Automatic audit events add the actor, timestamp, resource, action, and before/after field values for recorded mutations. System-generated device status transitions use the same event store.
The API exposes those events for authenticated reading, with no edit or delete route. This gives the application an append-only audit view; it does not make the database tamper-proof. Password and other credential-named fields are filtered out of the recorded field diffs.
Webhook alert support is also implemented. High-criticality devices generate outage notifications after confirmed status transitions and recovery notifications after a confirmed outage. Delivery retries are bounded, and delivery results are recorded in the audit log.
The distinction between support and configuration is visible here:
without ALERT_WEBHOOK_URL, the app records a dry-run alert event and
sends no notification. Having the feature in the code does not establish
that an external notification route is configured or tested.
Topology with evidence attached
The topology graph uses explicit device relationships: physical links, VMs hosted on hypervisors, and HA peers. Each relationship carries an evidence note. It is not inferred from devices sharing a VLAN.
Admins can maintain the links, and authenticated viewers can inspect them. Filters and the selected-device inspector make the graph useful for tracing a dependency back to the inventory. A missing relationship remains missing until there is evidence to add it.
The checks around the app
Duplicate IP and MAC reports identify conflicting inventory entries. CSV exports for devices, VLANs, and services provide a way to inspect and retain the data outside the application.
The backend shares CRUD behavior across domain routers. Its tests use an in-process HTTP client and a mock MongoDB implementation, so API behavior can be checked without a live database. Ruff handles lint and formatting, mypy checks types, and frontend tests and the production build cover the browser code.
These checks establish application behavior. Live reachability, Prometheus access, webhook delivery, and inventory accuracy still need verification in the deployment environment.
Search and history in the local update
The local update adds a Ctrl/Cmd+K search palette across devices, services, and manual Change Log entries. Results are grouped by module and open the matching device or selected record. The authenticated search API matches literal text and returns up to twenty results per group, enough to start with a name, address, or remembered change.
A new History tab puts audit events beside the device they describe. The server filters by the device’s resource type and ID, and the view shows the latest two thousand matching events with actor, timestamp, action, and before/after values. System status transitions appear there alongside my recorded edits. The separate Changes tab keeps the manual change records available.
Both additions are implemented locally and await release and deployment. They use the existing record APIs and audit store.
Remaining roadmap
TOTP, OIDC sign-in, API rate limiting, and a backup control in Settings remain on the longer roadmap. Backup and restore currently use the documented command-line procedure.
Related posts: Moving my home network from Omada to UniFi, looking back.