Refresh CLAUDE.md: lockfile status, new Celery tasks, sleep GMT convention, newer hooks/utils
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -25,6 +25,9 @@ Everything runs in Docker Compose. There is no way to run individual services wi
|
|||||||
# Backup/restore the database:
|
# Backup/restore the database:
|
||||||
./scripts/manage.sh backup
|
./scripts/manage.sh backup
|
||||||
./scripts/manage.sh restore milevault_backup_20240101_120000.sql
|
./scripts/manage.sh restore milevault_backup_20240101_120000.sql
|
||||||
|
|
||||||
|
# Pull latest from git, rebuild, and restart:
|
||||||
|
./scripts/manage.sh update
|
||||||
```
|
```
|
||||||
|
|
||||||
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.
|
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.
|
||||||
@@ -55,7 +58,7 @@ The Gitea Actions workflow (`.gitea/workflows/build.yml`) auto-builds and pushes
|
|||||||
|
|
||||||
`./deploy.sh "<commit message>"` is the normal dev loop here: it commits everything, pushes to `main` (triggering the image build), and stops the running stack in `../milevault_docker`. After the build finishes, run `docker compose pull && docker compose up -d` there. This matches the repo rule: fix files in `~/milevault`, push to git — never patch the running containers in `~/milevault_docker`.
|
`./deploy.sh "<commit message>"` is the normal dev loop here: it commits everything, pushes to `main` (triggering the image build), and stops the running stack in `../milevault_docker`. After the build finishes, run `docker compose pull && docker compose up -d` there. This matches the repo rule: fix files in `~/milevault`, push to git — never patch the running containers in `~/milevault_docker`.
|
||||||
|
|
||||||
**CI validation**: The build workflow runs a `validate` job before building images. It will fail if `@polyline-codec` appears in `frontend/package.json` or if `npm ci` is used in `frontend/Dockerfile` (no lockfile exists — always use `npm install`). Fix these before pushing.
|
**CI validation**: The build workflow runs a `validate` job before building images. It will fail if `@polyline-codec` appears in `frontend/package.json` or if `npm ci` is used in `frontend/Dockerfile` — keep `npm install` there (a `package-lock.json` is now tracked, but the validate job still rejects `npm ci`). Fix these before pushing.
|
||||||
|
|
||||||
**`VITE_MAPBOX_TOKEN`** is baked empty by the CI build (`build-args: VITE_MAPBOX_TOKEN=`), so satellite tiles are disabled in all pre-built images. To enable them, rebuild locally with the token set in `.env`.
|
**`VITE_MAPBOX_TOKEN`** is baked empty by the CI build (`build-args: VITE_MAPBOX_TOKEN=`), so satellite tiles are disabled in all pre-built images. To enable them, rebuild locally with the token set in `.env`.
|
||||||
|
|
||||||
@@ -93,7 +96,7 @@ docker compose -f docker-compose.deploy.yml up -d
|
|||||||
- `services/wellness_parser.py` — parses Garmin wellness FIT files (metrics, sleep, HRV, SPO2, etc.)
|
- `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/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
|
- `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`, `match_segment`, `match_activity_segments`, `process_garmin_health_zip`, `sync_garmin_connect_user`, `sync_all_garmin_connect` (beat-scheduled), `recalculate_hr_zones_for_user`
|
- `workers/tasks.py` — Celery tasks: `process_activity_file`, `parse_wellness_fit`, `detect_route`, `compute_personal_records`, `match_segment`, `match_activity_segments`, `process_garmin_health_zip`, `sync_garmin_connect_user`, `sync_all_garmin_connect` (beat-scheduled), `recalculate_hr_zones_for_user`, `backfill_moving_time`, `backfill_indoor_distances`, `recompute_personal_records_all`
|
||||||
|
|
||||||
### Key design decisions
|
### Key design decisions
|
||||||
|
|
||||||
@@ -103,16 +106,20 @@ docker compose -f docker-compose.deploy.yml up -d
|
|||||||
|
|
||||||
**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.
|
**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.
|
||||||
|
|
||||||
|
**Sleep data**: All sleep timestamps (`sleep_start`/`sleep_end` and the `sleep_stages` JSON hypnogram on `HealthMetric`) are stored as GMT/UTC, never device-local time — a past bug stored local time and displayed +1h in BST. `sleep_stages` is `[[ts_ms, level], ...]` with levels 0=unmeasurable, 1=awake, 2=light, 3=deep, 4=REM.
|
||||||
|
|
||||||
**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.
|
**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/`)
|
### Frontend (`frontend/src/`)
|
||||||
|
|
||||||
- `App.jsx` — React Router v6, `RequireAuth` wrapper, all routes defined here
|
- `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
|
- `hooks/useAuth.js` — Zustand store for auth state, reads JWT from `localStorage`, handles PocketID token-in-URL flow
|
||||||
|
- `hooks/useSync.js` — Zustand store polling Garmin sync status; maps backend status strings to progress percentages
|
||||||
- `utils/api.js` — Axios instance with JWT interceptor and 401→redirect handler
|
- `utils/api.js` — Axios instance with JWT interceptor and 401→redirect handler
|
||||||
- TanStack Query (`@tanstack/react-query`) handles all server-state fetching and caching; Zustand is used only for auth state
|
- TanStack Query (`@tanstack/react-query`) handles all server-state fetching and caching; Zustand is used only for auth state
|
||||||
- `utils/format.js` — shared formatting helpers: `formatDuration`, `formatPace`, `formatDistance`, `formatCadence`, `hrZoneColor`, `sportIcon`, `sportColor`, etc.
|
- `utils/format.js` — shared formatting helpers: `formatDuration`, `formatPace`, `formatDistance`, `formatCadence`, `hrZoneColor`, `sportIcon`, `sportColor`, etc.
|
||||||
- `pages/` — one file per route: `Dashboard`, `Activities`, `ActivityDetail`, `Routes`, `Records`, `Health`, `Upload`, `Profile`, `Users`, `Login`
|
- `utils/track.js` — projects a lat/lng onto a GPS track (interpolated along-line snapping, used for map hover and segment selection); `utils/bodyBattery.js` — shared Body Battery colour/state helpers used by both the Health page and Dashboard mini chart
|
||||||
|
- `pages/` — one `*Page.jsx` file per route: `Dashboard` (drag-to-edit widget grid), `Activities`, `ActivityDetail`, `Routes`, `Records`, `Health`, `Upload`, `Profile`, `Users`, `Login`
|
||||||
- `components/activity/` — `ActivityMap` (Leaflet), `MetricTimeline` (Recharts), `HRZoneBar`, `LapTable`, `SegmentsPanel` (per-activity segment efforts), `RouteLeaderboard` (top-10 by pace for a named route)
|
- `components/activity/` — `ActivityMap` (Leaflet), `MetricTimeline` (Recharts), `HRZoneBar`, `LapTable`, `SegmentsPanel` (per-activity segment efforts), `RouteLeaderboard` (top-10 by pace for a named route)
|
||||||
- `components/ui/` — `Layout` (nav shell), `StatCard`, `RouteMiniMap` (small Leaflet map used in route/segment cards)
|
- `components/ui/` — `Layout` (nav shell), `StatCard`, `RouteMiniMap` (small Leaflet map used in route/segment cards)
|
||||||
|
|
||||||
@@ -147,6 +154,7 @@ Required in `.env` (or passed to Docker Compose):
|
|||||||
## Rules
|
## Rules
|
||||||
- The current build will always be running in docker at ~/milevault_docker with the following container names:
|
- The current build will always be running in docker at ~/milevault_docker with the following container names:
|
||||||
`milevault_backend`
|
`milevault_backend`
|
||||||
|
`milevault_beat`
|
||||||
`milevault_db`
|
`milevault_db`
|
||||||
`milevault_frontend`
|
`milevault_frontend`
|
||||||
`milevault_redis`
|
`milevault_redis`
|
||||||
|
|||||||
Reference in New Issue
Block a user