Fixed Garmin sync progress bar granularity, timeout issue, and lookback days input, plus redesigned the sleep timeline with taller bars and yellow Awake colour.
Build and push images / validate (push) Successful in 3s
Build and push images / build-backend (push) Successful in 48s
Build and push images / build-worker (push) Successful in 44s
Build and push images / build-frontend (push) Successful in 28s

This commit is contained in:
2026-06-07 18:15:07 +01:00
parent bf1920eb9d
commit 492418586a
5 changed files with 120 additions and 59 deletions
+11 -4
View File
@@ -63,11 +63,12 @@ docker compose -f docker-compose.deploy.yml up -d
- `main.py` — FastAPI app, DB init on startup (creates tables, seeds admin user, creates TimescaleDB hypertable)
- `core/``config.py` (pydantic-settings from env), `database.py` (async engine for FastAPI + sync engine for Celery), `security.py` (JWT, bcrypt)
- `api/` — routers: `auth`, `activities`, `routes`, `health`, `records`, `upload`, `profile`
- `models/user.py` — all SQLAlchemy models: `User`, `Activity`, `ActivityDataPoint`, `ActivityLap`, `NamedRoute`, `RouteSegment`, `PersonalRecord`, `HealthMetric`, `WeightLog`
- `api/` — routers: `auth`, `activities`, `routes`, `health`, `records`, `upload`, `profile`, `garmin_sync`
- `models/user.py` — all SQLAlchemy models: `User`, `Activity`, `ActivityDataPoint`, `ActivityLap`, `NamedRoute`, `RouteSegment`, `PersonalRecord`, `HealthMetric`, `WeightLog`, `GarminConnectConfig`
- `services/fit_parser.py` — parses Garmin FIT and GPX files; handles raw FIT timestamps (FIT epoch offset 631065600s) and semicircle→degree conversion
- `services/wellness_parser.py` — parses Garmin wellness FIT files (metrics, sleep, HRV, SPO2, etc.)
- `services/route_matcher.py` — bounding-box pre-filter + DTW (Dynamic Time Warping) for GPS track similarity
- `services/garmin_connect_sync.py` — Garmin Connect API integration; `authenticate_garmin()` tries stored OAuth token first, falls back to email/password; Garmin credentials stored Fernet-encrypted using `SECRET_KEY` as the key
- `workers/tasks.py` — Celery tasks: `process_activity_file`, `parse_wellness_fit`, `detect_route`, `compute_personal_records`, `process_garmin_health_zip`
### Key design decisions
@@ -76,7 +77,7 @@ docker compose -f docker-compose.deploy.yml up -d
**File routing in Celery**: `process_activity_file` inspects the filename; files matching wellness suffixes (`_METRICS.fit`, `_WELLNESS.fit`, `_SLEEP.fit`, etc.) are routed to `parse_wellness_fit` instead.
**Schema management**: No Alembic migrations are used in production. `Base.metadata.create_all` runs at startup with retry logic to handle multi-worker races. Health metrics upserts use raw SQL `ON CONFLICT ... DO UPDATE SET ... COALESCE(EXCLUDED.x, existing.x)` to merge data from multiple file sources without overwriting.
**Schema management**: No Alembic migrations are used in production. `Base.metadata.create_all` runs at startup with retry logic to handle multi-worker races. Post-initial schema changes (new columns, constraint changes) are applied as `ALTER TABLE ... ADD COLUMN IF NOT EXISTS` / `DROP CONSTRAINT IF EXISTS` statements in `init_db()` in `main.py` — this is the only place schema migrations happen. Health metrics upserts use raw SQL `ON CONFLICT ... DO UPDATE SET ... COALESCE(EXCLUDED.x, existing.x)` to merge data from multiple file sources without overwriting.
**PocketID OIDC**: Optional passkey auth. Config is read from the admin user's DB record first, falling back to env vars. The OAuth callback redirects to `/?token=<jwt>` and `useAuth.js` extracts the token from the URL at module load time.
@@ -85,8 +86,10 @@ docker compose -f docker-compose.deploy.yml up -d
- `App.jsx` — React Router v6, `RequireAuth` wrapper, all routes defined here
- `hooks/useAuth.js` — Zustand store for auth state, reads JWT from `localStorage`, handles PocketID token-in-URL flow
- `utils/api.js` — Axios instance with JWT interceptor and 401→redirect handler
- `pages/` — one file per route
- `utils/format.js` — shared formatting helpers: `formatDuration`, `formatPace`, `formatDistance`, `formatCadence`, `hrZoneColor`, `sportIcon`, `sportColor`, etc.
- `pages/` — one file per route; includes `SegmentsPage` for route segment management
- `components/activity/``ActivityMap` (Leaflet), `MetricTimeline` (Recharts), `HRZoneBar`, `LapTable`
- `components/ui/RouteMiniMap` — small Leaflet map used in route/segment cards
The Vite dev server proxies `/api` to `http://backend:8000` (for use inside the Docker Compose network). The production build bakes `VITE_API_URL` at build time.
@@ -104,6 +107,10 @@ Required in `.env` (or passed to Docker Compose):
| `VITE_MAPBOX_TOKEN` | Optional — enables satellite tile layer |
| `POCKETID_ISSUER` / `POCKETID_CLIENT_ID` / `POCKETID_CLIENT_SECRET` | Optional OIDC |
## milevault_export/
`milevault_export/` is a sanitised snapshot of the project used for public distribution (stripped of dev-only configs). It mirrors the main project structure. When making changes that affect deployment files (`docker-compose.yml`, `nginx.conf`, `scripts/manage.sh`, `docker/init.sql`, etc.), keep this directory in sync manually.
## Rules
- The current build will always be running in docker at ~/milevault_docker with the following container names:
`milevault_backend`