Foyn
Lab

Health dashboard

Health

One private page with a year of readings from a Garmin watch and scale, synced every hour, and a one-click summary to paste into a chat model.

From Garmin to the page

Garmin Connect where the watch and scale upload every 60 min Sync job re-reads the last 5 days, calls 0.6 s apart page loads stop here SQLite 69 metrics a day, 365 days kept read while the sync writes (WAL mode) Dashboard 9 cards, 5 trends export.md Markdown, 30 days 1 click You, in a browser A chat model
Sync
The only part that calls Garmin.
Read path
Page loads and exports read SQLite only.
Opening the page never calls Garmin. It shows what the last sync stored and when.

When Garmin is late or down

Next to Garmin Connect

  • Opening the page

    Garmin ConnectLoads from Garmin's servers

    HealthReads a local SQLite file

  • Personal ranges

    Garmin ConnectGarmin's ranges for HRV and training load

    Health10th, 50th and 90th percentiles for 20 metrics, beside the 7-day median

  • Into a chat model

    Garmin ConnectActivity files one at a time, or an account export by email

    HealthOne click copies a Markdown summary of the last 30 days

Both show the same Garmin readings. Health keeps a year of them in one file and writes them out as Markdown.

Under the hood

Stack
Python 3.13, FastAPI, APScheduler and SQLite in one container; the page is Astro
Garmin client
garminconnect, resumed from a saved token store, so no password at runtime
Sync
Every 60 min: 34 endpoint calls, 0.6 s apart, never retried
Tables
daily_metrics (69 metric columns), activities, device_state, sync_log
Export
GET /api/export.md?days=N, 30 days by default, up to the 365 kept
Access
Cloudflare Access at the edge; the app checks its JWT on every route but /healthz