Commit Graph

45 Commits

Author SHA1 Message Date
owain d57054509c Fix HealthPage crash: move intradayData query below selectedDay declaration
Build and push images / validate (push) Successful in 3s
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 10s
The useQuery for intradayData referenced selectedDay (a useMemo) before it
was declared in the function body, causing ReferenceError on every render
and breaking the health page entirely.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 10:51:14 +01:00
owain f927e32853 Fix missing avg_hr_day/weight data; add 24hr HR chart to daily snapshot
Build and push images / validate (push) Successful in 3s
Build and push images / build-backend (push) Successful in 6s
Build and push images / build-worker (push) Successful in 5s
Build and push images / build-frontend (push) Successful in 10s
Backend:
- main.py: add ADD COLUMN IF NOT EXISTS migrations for avg_hr_day, max_hr_day,
  and intraday_hr (JSONB) on health_metrics — these columns were in the model
  but missing from existing DB instances, silently dropping all avg/max HR data.
- models/user.py: add intraday_hr JSON column to HealthMetric.
- garmin_connect_sync.py: fetch body composition (weight, BMI, body fat, muscle
  mass) via get_body_composition() per day, with stats.bodyWeight as fallback.
  Fetch intraday heart rate via get_heart_rates() and store non-null
  [epoch_ms, bpm] pairs in intraday_hr.
- health.py: add GET /health-metrics/intraday?date=YYYY-MM-DD endpoint that
  returns the stored intraday_hr array for a specific day.

Frontend (HealthPage):
- Add IntradayHrChart component: AreaChart rendering the 24-hour HR trace
  with time-of-day x-axis.
- DailySnapshot: show 24-hour HR chart (when intraday data present) above
  the activity strip; add weight + body fat % to the Heart & HRV card;
  show max HR alongside avg HR.
- HealthPage: query /intraday for the selected day and pass data down.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 10:47:53 +01:00
owain a28ce0e009 Add sync progress bar; change auto-sync to every 30 minutes
Build and push images / validate (push) Successful in 2s
Build and push images / build-backend (push) Successful in 6s
Build and push images / build-worker (push) Successful in 5s
Build and push images / build-frontend (push) Successful in 16s
Backend:
- Change beat schedule from 3600s (hourly) to 1800s (30 minutes)
- Emit intermediate last_sync_status DB commits at each phase of
  sync_garmin_connect_user ("Connecting to Garmin...", "Syncing activities...",
  "Syncing wellness data...") so the frontend can reflect live progress.
  Snapshot config fields upfront to avoid reading expired ORM attrs after commits.

Frontend (ProfilePage):
- Replace blind 3-second timeout with 2s polling loop that reads the live
  last_sync_status from /garmin-sync/config after triggering a sync.
- Wait until an in-progress status is observed before declaring completion,
  avoiding a false-finish on the previous terminal status.
- Show an animated progress bar that advances through the sync phases with
  the current status text below it. Safety timeout stops polling after 10 min.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 10:36:15 +01:00
owain a9b3da858d Fix five code-review findings: token auth, sync rate-limiting, model drift, FK cascade
Build and push images / validate (push) Successful in 2s
Build and push images / build-backend (push) Successful in 6s
Build and push images / build-worker (push) Successful in 5s
Build and push images / build-frontend (push) Successful in 5s
- garmin_connect_sync: revert to garth.loads() for token auth — login(tokenstore=)
  dispatches on len>512, treating compact tokens as filesystem paths and forcing a
  full re-login on every sync. Explicitly set display_name from the embedded profile.
- garmin_connect_sync: restore incremental sync for both activities and wellness —
  always re-fetching the full lookback window was generating ~270 Garmin API calls
  per wellness sync run, risking rate-limits. Now uses since-1d when since is set.
  Add 0.25s per-day sleep in sync_wellness as an additional rate-limit guard.
- models/user.py: replace the dropped uq_pr_current UniqueConstraint in
  PersonalRecord.__table_args__ with the partial Index the DB actually has,
  so the model and live schema no longer permanently diverge.
- models/user.py: add ondelete="SET NULL" to Activity.named_route_id FK so the
  DB cascade handles unlinks if routes are deleted outside the API endpoint.
- main.py: add startup migration to re-add activities_named_route_id_fkey with
  ON DELETE SET NULL on existing deployments.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 10:30:20 +01:00
owain 211f77a574 Fix sync_lookback_days actually controlling the sync window
Build and push images / validate (push) Successful in 2s
Build and push images / build-backend (push) Successful in 48s
Build and push images / build-worker (push) Successful in 52s
Build and push images / build-frontend (push) Successful in 29s
Activities: lookback_days was ignored once last_sync_at was set (since
always took priority). Now lookback_days always sets the window; -1 is
all-time on first sync then incremental.

Wellness: lookback_days was never passed to sync_wellness at all —
hardcoded 90-day cap regardless of settings. Fixed by adding lookback_days
param and wiring it through from the Celery task.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 01:34:18 +01:00
owain 22b41109f5 Fix Garmin stats sync, add route merge/map/links, fix PR constraint
Build and push images / validate (push) Successful in 3s
Build and push images / build-backend (push) Successful in 7s
Build and push images / build-worker (push) Successful in 7s
Build and push images / build-frontend (push) Successful in 10s
Garmin health: fix display_name=None when using stored OAuth tokens.
authenticate_garmin() now calls login(tokenstore=...) instead of
garth.loads() directly, so display_name is populated and get_user_summary
works. Also add avg_hr_day / max_hr_day from stats response.

Routes: add merge endpoint (POST /{id}/merge/{source}), delete endpoint.
Routes page: polyline SVG mini-map on each route card, merge UI with
confirmation, activity rows are now Links to the activity detail page.

Personal records: replace all-columns unique constraint with a partial
index (unique on current records only) to stop UniqueViolation crashes
when parallel workers deactivate the same PR.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 01:25:01 +01:00
owain edeb3ccece Fix: commit missing init_db migration for sync_lookback_days column
Build and push images / validate (push) Successful in 2s
Build and push images / build-backend (push) Successful in 52s
Build and push images / build-worker (push) Successful in 6s
Build and push images / build-frontend (push) Successful in 5s
This ALTER TABLE was written locally but never staged, so the production
image ran without it and the column was never added to the DB.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 01:11:55 +01:00
owain 17ec83bfc2 Fix: add sync_lookback_days column to GarminConnectConfig model
Build and push images / validate (push) Successful in 3s
Build and push images / build-backend (push) Successful in 45s
Build and push images / build-worker (push) Successful in 45s
Build and push images / build-frontend (push) Successful in 22s
This was missing from the earlier commit — tasks.py referenced
cfg.sync_lookback_days but the column wasn't defined in the SQLAlchemy model,
causing AttributeError when connecting or syncing Garmin.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 01:06:38 +01:00
owain a3c039b3ea Fix Garmin Connect save — make password optional for settings updates
Build and push images / validate (push) Successful in 2s
Build and push images / build-backend (push) Successful in 7s
Build and push images / build-worker (push) Successful in 49s
Build and push images / build-frontend (push) Successful in 27s
password was required in GarminConfigIn, causing a 422 when users updated
toggles or sync_lookback_days without re-entering their credentials.

Now only re-authenticates when a new password is supplied; settings-only
updates (sync_enabled, sync_activities, sync_wellness, sync_lookback_days)
go through without touching the stored credentials.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 00:58:00 +01:00
owain f8c126fbda Add configurable sync_lookback_days for Garmin Connect
Build and push images / validate (push) Successful in 2s
Build and push images / build-backend (push) Successful in 7s
Build and push images / build-worker (push) Successful in 6s
Build and push images / build-frontend (push) Successful in 10s
Users can now set how many days back the first sync fetches. -1 syncs all
history back to 2010; any positive value sets a rolling window. Values
over 365 show a rate-limit warning in the UI. The default remains 30 days.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 00:40:55 +01:00
owain 335bd0a053 Fix Garmin Connect sync to import full history and prevent re-downloads
Build and push images / validate (push) Successful in 3s
Build and push images / build-backend (push) Successful in 54s
Build and push images / build-worker (push) Successful in 6s
Build and push images / build-frontend (push) Successful in 22s
Activity sync:
- First sync (no last_sync_at) now fetches from 2010-01-01 instead of -30 days,
  importing the full account history rather than only the last month
- Pre-download dedup: check existing activities by start_time before downloading;
  stamps garmin_activity_id on the match so subsequent syncs take the fast path
- process_activity_file stamps garmin_activity_id on duplicate detection for
  the same reason (covers activities imported via bulk export)
- 0.5 s sleep between downloads to avoid Garmin API rate limiting

Wellness sync:
- First sync now covers last 90 days instead of 7

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 00:33:49 +01:00
owain 7d6d34f61f Fix health trends range selector; add day navigation and chart click
Build and push images / validate (push) Successful in 2s
Build and push images / build-backend (push) Successful in 51s
Build and push images / build-worker (push) Successful in 49s
Build and push images / build-frontend (push) Successful in 23s
- Fix keepPreviousData v4→v5: import keepPreviousData from @tanstack/react-query
  and use as placeholderData so charts don't blank out when switching ranges
- Normalise all metric dates to YYYY-MM-DD in queryFn so XAxis values and
  ReferenceLine x values always match
- Add allDays query (last 365 days) for snapshot navigation, keyed under
  ['health-metrics', 'all'] so UploadPage invalidation covers it
- Arrow nav (← →) in snapshot header steps through available days
- Clicking any trend chart data point loads that day in the snapshot
- Blue dashed ReferenceLine marks the selected day in every chart

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 00:27:35 +01:00
owain 6d224d51c5 Add Garmin Connect auto-sync via python-garminconnect
Build and push images / validate (push) Successful in 2s
Build and push images / build-backend (push) Successful in 7s
Build and push images / build-worker (push) Successful in 6s
Build and push images / build-frontend (push) Successful in 8s
- GarminConnectConfig model stores encrypted credentials and OAuth token
- garmin_connect_sync service: token-based auth with password fallback,
  activity FIT download + queue, daily wellness from JSON API
- Celery beat schedule: sync_all_garmin_connect fires every hour
- New API router /api/garmin-sync: config CRUD, manual trigger
- Beat container added to docker-compose.yml and docker-compose.deploy.yml
- ProfilePage: Garmin Connect section with connect/update/disconnect and Sync now

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 00:08:12 +01:00
owain 0cdc653664 Add daily health snapshot to Health page
Build and push images / validate (push) Successful in 2s
Build and push images / build-backend (push) Successful in 49s
Build and push images / build-worker (push) Successful in 47s
Build and push images / build-frontend (push) Successful in 9s
Replaces the flat stat card grid with a rich daily view at the top: sleep card
with duration, stage bar and times; heart/HRV card; activity strip (steps with
progress bar, calories, stress, SpO2). Trend charts moved below under a Trends
heading with the range selector inline.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-06 23:46:08 +01:00
owain c3637fa3fa Fix wellness parser: field names, sleep epoch durations, HRV, sleep score
Build and push images / validate (push) Successful in 2s
Build and push images / build-backend (push) Successful in 6s
Build and push images / build-worker (push) Successful in 6s
Build and push images / build-frontend (push) Successful in 5s
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>
2026-06-06 23:33:50 +01:00
owain 95f704cb54 Fix upload auto-refresh, health data refresh, and HR zone recalculation
Build and push images / validate (push) Successful in 2s
Build and push images / build-backend (push) Successful in 6s
Build and push images / build-worker (push) Successful in 5s
Build and push images / build-frontend (push) Successful in 10s
- UploadPage now polls task status every 2s and invalidates activity,
  health-summary, and health-metrics queries on completion so new
  activities and health data appear without a hard refresh
- Garmin and Strava export endpoints now return a task_id for polling
- Updating max HR in Profile triggers a background Celery task to
  recalculate hr_zones for all existing activities; profile page shows
  a confirmation note when this is queued
- Add CLAUDE.md with repo architecture and dev commands

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-06 23:13:44 +01:00
owain b5fd17a597 Fix PocketID login redirect
Build and push images / validate (push) Successful in 3s
Build and push images / build-backend (push) Successful in 6s
Build and push images / build-worker (push) Successful in 5s
Build and push images / build-frontend (push) Successful in 11s
2026-06-06 19:56:18 +01:00
owain df6c993709 Fix PocketID OIDC endpoints for auth.jarrett.eu
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 5s
2026-06-06 19:51:08 +01:00
owain 34284f3d9d Fix PocketID callback URL to use full base URL
Build and push images / validate (push) Successful in 2s
Build and push images / build-backend (push) Successful in 6s
Build and push images / build-worker (push) Successful in 4s
Build and push images / build-frontend (push) Successful in 5s
2026-06-06 19:43:26 +01:00
owain 16cf4a9313 Fix wellness_parser - had fit_parser content instead of wellness parser
Build and push images / validate (push) Successful in 3s
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 4s
2026-06-06 19:38:53 +01:00
owain ed4ab0eff8 Fix FIT parser - handle raw timestamps and semicircle auto-detection
Build and push images / validate (push) Successful in 3s
Build and push images / build-backend (push) Successful in 6s
Build and push images / build-worker (push) Successful in 5s
Build and push images / build-frontend (push) Successful in 5s
2026-06-06 19:32:34 +01:00
owain 0fd3ff7414 Fix SDK field names - use camelCase throughout
Build and push images / validate (push) Successful in 3s
Build and push images / build-backend (push) Successful in 51s
Build and push images / build-worker (push) Successful in 6s
Build and push images / build-frontend (push) Successful in 5s
2026-06-06 19:27:35 +01:00
owain f609931ebc Remove fitparse entirely - use Garmin SDK only with messages dict approach
Build and push images / validate (push) Successful in 2s
Build and push images / build-backend (push) Successful in 31s
Build and push images / build-worker (push) Successful in 32s
Build and push images / build-frontend (push) Successful in 24s
2026-06-06 19:17:51 +01:00
owain e9811d8d83 Fix duplicate detection, add wellness suffixes, add reprocess endpoint
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 5s
2026-06-06 19:02:42 +01:00
owain 93b8f00f94 Fix map route not rendering; fix health date filter timezone mismatch
Build and push images / validate (push) Successful in 3s
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 9s
2026-06-06 18:52:29 +01:00
owain 24f8417982 Fix package.json, Dockerfile, add CI validation to prevent recurrence
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 21s
2026-06-06 18:27:29 +01:00
owain 070267eee5 Fix package.json trailing comma
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 22s
2026-06-06 18:20:40 +01:00
owain 4b93cbf5e0 Remove non-existent polyline-codec package
Build and push images / build-backend (push) Successful in 5s
Build and push images / build-worker (push) Successful in 6s
Build and push images / build-frontend (push) Failing after 5s
2026-06-06 18:19:35 +01:00
owain b0248fb581 Fix frontend Dockerfile - use npm install not npm ci
Build and push images / build-backend (push) Successful in 6s
Build and push images / build-worker (push) Successful in 5s
Build and push images / build-frontend (push) Failing after 15s
2026-06-06 18:17:38 +01:00
owain ec5a01d12a All tweaks added
Build and push images / build-backend (push) Successful in 33s
Build and push images / build-worker (push) Successful in 32s
Build and push images / build-frontend (push) Failing after 6s
2026-06-06 18:10:35 +01:00
owain 043b3b7269 Switch to official Garmin FIT Python SDK for both activity and wellness parsing
Build and push images / build-backend (push) Successful in 7s
Build and push images / build-worker (push) Successful in 5s
Build and push images / build-frontend (push) Successful in 6s
2026-06-06 16:09:57 +01:00
owain 38632cfe4f Use ON CONFLICT upsert for health metrics - fixes concurrent worker race condition
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 5s
2026-06-06 15:53:56 +01:00
owain 8104ca5ed0 Route wellness FIT files to health parser, parse HR/HRV/sleep/stress/SpO2
Build and push images / build-backend (push) Successful in 6s
Build and push images / build-worker (push) Successful in 6s
Build and push images / build-frontend (push) Successful in 5s
2026-06-06 15:50:25 +01:00
owain c4e5eb91ed Use sync SQLAlchemy in Celery worker - fixes asyncpg connection issues
Build and push images / build-backend (push) Successful in 1m52s
Build and push images / build-worker (push) Successful in 44s
Build and push images / build-frontend (push) Successful in 25s
2026-06-06 15:29:36 +01:00
owain 29c39c3bbb Make admin_password optional so worker can start without it
Build and push images / build-backend (push) Successful in 4s
Build and push images / build-worker (push) Successful in 4s
Build and push images / build-frontend (push) Successful in 5s
2026-06-06 15:09:27 +01:00
owain bfb3daba05 Fix worker race condition - single uvicorn worker + tolerate duplicates
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 5s
2026-06-06 15:09:22 +01:00
owain 5a57e84e80 Pin bcrypt to 4.0.1, add celery_app entry point
Build and push images / build-backend (push) Successful in 33s
Build and push images / build-worker (push) Successful in 32s
Build and push images / build-frontend (push) Successful in 5s
2026-06-06 15:05:16 +01:00
owain 264c27469b Fix DB init - composite PK for hypertable + separate transactions
Build and push images / build-backend (push) Successful in 6s
Build and push images / build-worker (push) Successful in 6s
Build and push images / build-frontend (push) Successful in 7s
2026-06-06 15:01:39 +01:00
owain ecc077f153 Fix package.json trailing comma
Build and push images / build-backend (push) Successful in 4s
Build and push images / build-worker (push) Successful in 4s
Build and push images / build-frontend (push) Successful in 21s
2026-06-06 14:48:50 +01:00
owain 9fd12676d1 Remove non-existent polyline-codec package
Build and push images / build-backend (push) Successful in 5s
Build and push images / build-worker (push) Successful in 4s
Build and push images / build-frontend (push) Failing after 5s
2026-06-06 14:45:26 +01:00
owain 8fdf3df013 Fix frontend build - use npm install instead of npm ci
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) Failing after 13s
2026-06-06 14:42:28 +01:00
owain e0b09d6d44 Hardcode registry URL to fix variable issue
Build and push images / build-backend (push) Successful in 52s
Build and push images / build-worker (push) Successful in 46s
Build and push images / build-frontend (push) Failing after 14s
2026-06-06 14:39:27 +01:00
owain 97e79fd020 Hardcode registry URL to fix variable issue 2026-06-06 14:39:01 +01:00
owain 5e2b220366 Rename fittracker to milevault throughout
Build and push images / build-backend (push) Failing after 2m5s
Build and push images / build-worker (push) Failing after 4s
Build and push images / build-frontend (push) Failing after 4s
2026-06-06 14:12:28 +01:00
owain 1a0d45dd67 Initial Commit 2026-06-06 13:23:33 +01:00