- 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_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>
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>
- 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>