Files
MileVault/CLAUDE.md
T
owain 4a4cbdcc92
Build and push images / validate (push) Successful in 2s
Build and push images / build-backend (push) Successful in 5s
Build and push images / build-worker (push) Successful in 5s
Build and push images / build-frontend (push) Successful in 8s
Fix pace sentinel, route map thumbnails, tiled segments, health/dashboard layout
- Pace: FIT 0xFFFF sentinel (65.535 m/s) was stored as avg_speed_ms on every
  activity and lap; add _sanitize_speed() to parser falling back to dist/dur,
  plus a startup SQL migration that fixed 120 activities and 688 laps in-place
- Records: remove swimming from Distance PRs; Route Records rows are clickable
  (navigate to activity), View button removed, small SVG route map per row;
  Segment Records uses same tiled route-card layout as Segments page
- Segments: replace route dropdown with responsive tile grid showing SVG map
  thumbnails; selecting a tile reveals the segment management panel below
- RouteMiniMap: new pure-SVG component (no Leaflet) for route thumbnails,
  decodes polyline and normalises coords into a fixed viewBox
- Health: rename "Avg Heart Rate (day)" → "Heart Rate"; weight chart now
  filters to non-null rows and enables connectNulls + dots for sparse data
- Dashboard: 4-col layout at lg breakpoint so Body Battery sits between weekly
  chart and Health Today; Body Battery card gains a 24-hr sparkline from the
  values[] already present in the health summary response

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 16:36:54 +01:00

5.9 KiB

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

What this project is

MileVault is a self-hosted fitness tracker. It ingests Garmin FIT files and Strava exports, stores activity and wellness data in TimescaleDB (PostgreSQL), and serves a React dashboard with maps, charts, personal records, and health trends.

Running locally

Everything runs in Docker Compose. There is no way to run individual services without Docker unless you wire up your own Postgres + Redis.

# First-time setup (generates .env with secrets, then starts containers):
./scripts/manage.sh setup

# Start/stop:
./scripts/manage.sh start
./scripts/manage.sh stop

# Follow logs (all services, or a specific one):
./scripts/manage.sh logs
./scripts/manage.sh logs backend

# Backup/restore the database:
./scripts/manage.sh backup
./scripts/manage.sh restore milevault_backup_20240101_120000.sql

The app is served on port 80 by nginx, which proxies /api/* to the backend (port 8000) and serves the React SPA for everything else.

Building and deploying

docker-compose.yml — build from source (dev/CI).
docker-compose.deploy.yml — pull pre-built images from the Gitea registry (production).

The Gitea Actions workflow (.gitea/workflows/build.yml) auto-builds and pushes images on push to main. Deployment machines only need docker-compose.deploy.yml and nginx.conf.

# Rebuild and restart from source:
docker compose build --no-cache
docker compose up -d

# Update a deployed instance:
docker compose -f docker-compose.deploy.yml pull
docker compose -f docker-compose.deploy.yml up -d

Architecture

Services

Service Purpose
db TimescaleDB (PostgreSQL 16) — activity_data_points is a hypertable
redis Celery broker + result backend
backend FastAPI (async) — uvicorn, single worker
worker Celery worker — synchronous SQLAlchemy (asyncio incompatible with prefork)
frontend React SPA built by Vite at container build time
nginx Reverse proxy, serves the SPA

Backend (backend/app/)

  • 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
  • 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
  • workers/tasks.py — Celery tasks: process_activity_file, parse_wellness_fit, detect_route, compute_personal_records, process_garmin_health_zip

Key design decisions

Async vs sync split: FastAPI uses async SQLAlchemy (asyncpg). Celery workers use sync SQLAlchemy (psycopg2) because Celery's prefork model doesn't survive asyncio engine forks. The DATABASE_URL uses postgresql+asyncpg://; the worker converts it to postgresql+psycopg2:// at runtime.

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.

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.

Frontend (frontend/src/)

  • 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
  • components/activity/ActivityMap (Leaflet), MetricTimeline (Recharts), HRZoneBar, LapTable

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.

Environment variables

Required in .env (or passed to Docker Compose):

Variable Purpose
DATABASE_URL Full async DB URL (postgresql+asyncpg://...)
SECRET_KEY JWT signing key — generate with openssl rand -hex 32
ADMIN_PASSWORD Seeds the admin user on first start
REDIS_URL Celery broker
BASE_URL Used for PocketID OAuth callback redirect URI
VITE_MAPBOX_TOKEN Optional — enables satellite tile layer
POCKETID_ISSUER / POCKETID_CLIENT_ID / POCKETID_CLIENT_SECRET Optional OIDC

Debugging and troubleshooting

  • The latest build will always be running in docker at ~/milevault_docker with the following container names: milevault_backend milevault_db milevault_frontend milevault_redis milevault_worker
  • When an issue is highlighted by the user, check the logs on these containers for the error, do not spin up new containers, rectify the issues in ~/milevault where the development is happening
  • Do NOT patch the running files under any circumstances, fix the development files.