The Garmin FIT SDK returns snake_case field names but the parser was
looking for camelCase. Sleep epoch durations were wrong (fixed 30s each
instead of computing from timestamp gaps). HRV is in message 370 not 275
(275 now carries sleep levels in modern firmware). Multiple fixes:
- msg 55: use 'steps', 'heart_rate', 'active_calories' (not numeric keys)
- msg 211: use 'resting_heart_rate' (not msg.get(0))
- msg 227: use 'stress_level_time'/'stress_level_value' for named fields
- msg 132: use snake_case 'stress_level_time'/'stress_level_value'
- msg 275: detect sleep_level field → handle as sleep epoch (modern),
fall back to HRV handling for older firmware
- msg 370: new handler for modern hrv_status_summary (last_night_average,
last_night_5_min_high, status)
- msg 346: new handler for sleep_assessment → overall_sleep_score
- msg 21: new handler for sleep session start/stop events to close the
last sleep epoch and record sleep_start/sleep_end timestamps
- Sleep duration: computed from epoch timestamp gaps instead of 30s/epoch
- Celery task SQL: add sleep_score, sleep_start, sleep_end to INSERT/UPDATE;
use GREATEST for total_calories so most-complete value wins across files
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
MileVault
Self-hosted fitness tracking — Garmin & Strava import, maps, health trends, personal records.
For users — deploy with two files
Once this repo is pushed to Gitea and the Actions workflow has run once, anyone on your network only needs two files to run MileVault. No source code, no cloning.
mkdir milevault && cd milevault
# Download the two deployment files
curl -O https://gitea.yourdomain.com/yourusername/milevault/raw/branch/main/docker-compose.deploy.yml
curl -O https://gitea.yourdomain.com/yourusername/milevault/raw/branch/main/nginx.conf
# Start (images pulled automatically from your Gitea registry)
docker compose -f docker-compose.deploy.yml up -d
Default login: admin / admin
Change ADMIN_PASSWORD in a .env file before exposing to a network (see Configuration below).
To update when a new version is pushed to Gitea:
docker compose -f docker-compose.deploy.yml pull
docker compose -f docker-compose.deploy.yml up -d
For developers — first-time Gitea setup
1. Enable the Gitea container registry
In your Gitea instance (app.ini or admin panel):
[packages]
ENABLED = true
Restart Gitea. The registry is then available at gitea.yourdomain.com.
2. Create a Gitea Actions runner
Gitea Actions needs a runner on your server:
# On the server that will build images
docker run -d \
--name gitea-runner \
--restart always \
-v /var/run/docker.sock:/var/run/docker.sock \
-v gitea-runner-data:/data \
-e GITEA_INSTANCE_URL=https://gitea.yourdomain.com \
-e GITEA_RUNNER_REGISTRATION_TOKEN=<token from Gitea → Settings → Runners> \
gitea/act_runner:latest
Get the registration token from: Gitea → Your repo → Settings → Actions → Runners → Create Runner
3. Create a package token
The workflow needs a token to push images to the registry:
- Gitea → Your profile → Settings → Applications → Generate Token
- Scopes: tick
write:package - Copy the token
Then in your repo: Settings → Secrets → Actions → Add Secret
- Name:
PACKAGE_TOKEN - Value: the token you just copied
4. Set the registry URL variable
In your repo: Settings → Variables → Actions → Add Variable
- Name:
GITEA_URL - Value:
gitea.yourdomain.com(nohttps://)
5. Push the repo
git remote add origin https://gitea.yourdomain.com/yourusername/milevault.git
git push -u origin main
The Actions workflow (.gitea/workflows/build.yml) triggers automatically, builds all three images, and pushes them to your Gitea registry. Check progress under Actions in the Gitea UI.
6. Update docker-compose.deploy.yml
Before the first deploy, replace the placeholder registry URLs in docker-compose.deploy.yml:
gitea.yourdomain.com/yourusername/ → your actual Gitea host and username
Configuration
Create a .env file next to docker-compose.deploy.yml to override any defaults:
# Admin login
ADMIN_USERNAME=admin
ADMIN_PASSWORD=a_strong_password_here
# Generate with: openssl rand -hex 32
SECRET_KEY=
# Ports
HTTP_PORT=80
# Optional: Mapbox token for satellite tiles
VITE_MAPBOX_TOKEN=
# Optional: PocketID passkey auth
POCKETID_ISSUER=
POCKETID_CLIENT_ID=
POCKETID_CLIENT_SECRET=
Docker Compose picks up .env automatically.
If your Gitea registry requires authentication to pull
If your Gitea instance is private, add a pull secret on the deploy machine:
docker login gitea.yourdomain.com
# enter your Gitea username and password (or a read:package token)
Docker stores the credentials in ~/.docker/config.json and uses them automatically on docker compose pull.
Repo structure
.gitea/workflows/build.yml ← Gitea Actions: builds & pushes images on push to main
docker-compose.yml ← dev/build compose (builds from source)
docker-compose.deploy.yml ← production compose (pulls pre-built images)
nginx.conf ← standalone nginx config for deploy compose
backend/ ← FastAPI + Celery worker
frontend/ ← React + Vite
nginx/nginx.conf ← nginx config for dev compose
docker/init.sql ← DB init (enables TimescaleDB extension)