Garmin Connect exports use UDSFile_*.json (not DailyMetrics) for daily
wellness summaries, and pack activity FIT files inside nested sub-zips
under DI-Connect-Uploaded-Files/ rather than at the top level.
- process_garmin_health_zip: match UDSFile_*.json instead of DailyMetrics,
handle list-of-records format, extract stress from allDayStress.aggregatorList,
convert floorsAscendedInMeters to floor count
- upload_garmin_export: recurse into nested .zip files to find and queue
individual activity FIT files
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Increased le=1000 → le=2000 to allow the 5Y trend (1826 days) and the
allDays snapshot navigation query (limit 2000) to succeed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add biological_sex field to User model, profile API, and ProfilePage toggle (male/female) — used to select the correct ACSM VO2 max threshold table
- Replace simple VO2 max number in daily snapshot with a colour-coded SVG radial gauge (Very Poor=red, Poor=orange, Fair=green, Good=blue, Excellent=purple) driven by sex- and age-appropriate thresholds
- Shrink Sleep widget to half-width, expand Heart & HRV to half-width; reorganise Heart & HRV internals into a 2×2 grid to reduce vertical height
- Add connectNulls + showDots to VO2 Max trend chart so sparse readings connect with a continuous line
- Add 3Y and 5Y range options to the Trends selector; increase allDays limit to 2000 for full 5yr snapshot navigation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Sleep: store per-epoch stage timestamps in new sleep_stages JSON column;
DailySnapshot now renders a proper 4-lane hypnogram (Awake/REM/Light/Deep)
instead of the old proportional flat bar
- Body battery: replace grey background bars + white line with per-minute bars
coloured by inferred type (sleep=indigo, rest=teal, active=orange, stable=grey)
derived from sleep window + battery direction; Y-axis fixed 0-100
- Routes: convert sidebar list to tile grid sorted by most completions; tiles
colour-bordered by sport type (blue=running, orange=cycling); completion count
shown on each tile; detail panel displays below the grid when a tile is clicked
- Segments on activity detail: add column headers (This run / Best / Δ) and
show signed time delta vs best, green when faster, red when slower
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- 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>
- Body battery: replace circular ring with compact full-height colored bar chart,
level as line overlay, legend shows only types present in data
- Dashboard: add mini body battery summary card above health today panel
- Profile: remove editable resting HR and manual weight log; show 7-day avg
resting HR and latest Garmin weight as read-only
- Backend: add GET /routes/{id}/segment-bests bulk endpoint (fetches all matched
activity data points in one query, computes best segment time per segment)
- Backend: add GET /records/routes for fastest activity per named route
- Routes page: add Segments panel to route detail (grouped as 1km splits vs
hills/turns, best times, delete, theoretical best)
- Activity detail page: show segment times computed client-side from data points,
🏆 badge if new best
- Records page: add Route Records and Segment Records tabs alongside Distance PRs
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Segments page: new /segments route with auto-generate (1km splits, turn
detection, hill detection), manual segment creation, per-segment performance
times across matched activities; fixed auth on existing segment endpoints
- YTD distance: new /activities/stats/ytd endpoint; Dashboard replaces
'Total distance' with 'Running this year' + 'Cycling this year'; Activities
page shows YTD stats row
- Weekly chart click: clicking a Dashboard bar navigates to Activities filtered
to that week; Activities reads from/to query params with dismissable chip
- Route matching: add ±2.5% distance gate + 3% relative DTW threshold
(was flat 80m); tighten candidate pre-filter from 80/120% to 95/105%
- Body battery layout: HR chart and body battery now side-by-side at same
height on large screens instead of stacked full-width
- Pace display fix: MetricTimeline clamps GPS speed outliers before computing
Y-axis domain; tick formatter guards against v<=0 or v>25 m/s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Parses Garmin Connect get_body_battery() per day, storing charged/drained/
start+end levels and the fine-grained [[ts_ms, level, type, stress]] values
array in a new body_battery JSONB column on health_metrics.
Frontend adds:
- BatteryRing SVG gauge (color-scaled 0–100)
- BodyBatteryChart: ComposedChart with type-colored bars (REST/ACTIVE/SLEEP/
STRESS) and battery level overlay line, matching Garmin's layout
- Body battery trend chart in the Trends section (end_level per day)
Also adds avg_hr_day and weight data which now correctly sync with the
intraday_hr JSON serialization fix from the previous commit.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
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>
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>
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>
- 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>
- 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>