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.
- 69metrics stored per day
- 365days of history kept
- 0calls to Garmin when the page opens
- 1click copies a summary for a chat model
From Garmin to the page
- Sync
- The only part that calls Garmin.
- Read path
- Page loads and exports read SQLite only.
When Garmin is late or down
-
Catches late sleep and HRV
Garmin fills in sleep and HRV after the fact, so each hourly run re-reads the last 5 days. An empty field does not overwrite a stored one.
-
Loads through an outage
If a sync fails or Garmin's login expires, the page still shows the stored data, under a banner that says what failed.
-
Says which side is stale
The footer shows the last sync and the watch's last upload, so you can tell whether the sync or the watch stopped.
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
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