Blog · May 2026 · 11 min read

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:

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:

  1. Edit code in WSL
  2. Run deploy.sh which rsyncs to Unraid via SMB
  3. 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

Need help with this?

I build custom FastAPI dashboards with real device integrations. Health data, solar monitoring, service status — all in one place.

Work with me →
← Back to blog