LanceOS: Building a Personal Dashboard with FastAPI and Real Device Data
Most dashboards show metrics from servers you'll never touch. I wanted one that showed my data — my heart rate from this morning, whether my solar panels are generating right now, and whether I took my medication today. The catch: every data source fought back.
The Stack
LanceOS is a FastAPI application with Jinja2 templates and SQLite. Simple on purpose. I didn't need Postgres for personal health data. I needed something I could deploy in a single Docker container and forget about.
The architecture is straightforward:
- FastAPI serves the routes and handles API integrations
- SQLite stores health data and medication logs
- Jinja2 renders the dashboard HTML server-side
- Docker wraps it all for deployment on Unraid
Google Health API: The Pagination Monster
The Google Health API (which replaced Fitbit's API) is the primary source for heart rate, steps, and sleep data. The OAuth flow works fine, but the data endpoints have quirks.
Pagination is the big one. Health data comes in pages of varying sizes, and the nextPageToken field is sometimes present, sometimes not — with no documentation explaining when. I had to write a loop that handles both cases:
while next_page_token:
response = await client.get_data(page_token=next_page_token)
points.extend(response.get("point", []))
next_page_token = response.get("nextPageToken")
if not next_page_token:
break
This sounds trivial. It wasn't. The token expires mid-pagination if you're fetching large date ranges, so I had to add token refresh logic inside the loop. The API also rate-limits at 30 requests per minute, which I hit regularly during backfill operations.
EG4 Solar: The Inverter That Doesn't Want to Talk
My EG2 solar inverter has a local API, but it's... temperamental. The data format changes depending on whether the inverter is in grid-tied or battery mode. The power values are sometimes negative with no explanation. And the API endpoint doesn't use HTTPS — it's plain HTTP on the local network.
I wrote a polling service that fetches solar data every 60 seconds and stores it in SQLite. The key insight was normalizing the data: regardless of which mode the inverter reports, I calculate net power as solar_generation - home_consumption and store that.
Medication Logging via Discord
This is where it gets fun. Instead of building a medication UI, I log medications through Discord. I tell my AI agent "took metformin 500mg" and it writes to the LanceOS medications database via an API endpoint.
The bot parses natural language medication entries, extracts the drug name and dosage, and POSTs to the LanceOS API. It handles corrections ("actually that was 1000mg not 500mg") and can show a history of doses for any time period.
The Two-Week 500 Error
For two weeks, the LanceOS dashboard returned a 500 error on every page load. The logs showed a template rendering error, but the template file existed and looked correct.
The issue? The Docker container was mounting the data directory correctly, but the template directory wasn't included in the Docker image. The .dockerignore file was excluding the templates/ folder because it matched a glob pattern. One line fix in .dockerignore, rebuild the image, and everything worked.
This is the kind of bug that's obvious in retrospect but invisible while you're debugging. The error message said "template not found" but I kept looking at the file permissions instead of the Docker build context.
Deployment
LanceOS runs as a Docker container on my Unraid server, exposed on port 8500. The deployment pipeline is simple:
- Edit code in WSL
- Run
deploy.shwhich rsyncs to Unraid via SMB - SSH into Unraid and run
docker compose up -d --build
Total cost: $0/month on hardware I already own. Total uptime since migration: 99.8%.
Lessons
- Start with SQLite. You don't need Postgres for a personal dashboard. SQLite handles concurrent reads fine for single-user workloads.
- APIs will fight you. Every health/solar/IoT API has undocumented behavior. Write defensive code from the start.
- Check your Docker build context. If something works locally but fails in Docker, the first thing to check is what's actually in the image.
- Natural language interfaces beat forms. Logging meds via Discord is faster and more natural than any form I could build.
I build custom FastAPI dashboards with real device integrations. Health data, solar monitoring, service status — all in one place.
Work with me →