Garmin Connect sync:
- Incremental syncs now re-fetch only a 1-day buffer (yesterday + today)
instead of the full lookback window every run. Full lookback applies on
the first sync only. Cuts steady-state API calls ~10x.
- Beat interval is now configurable via GARMIN_SYNC_INTERVAL_MINUTES and
surfaced to the UI; the sync toggle is relabelled to the real cadence.
Frontend:
- Collapsible sidebar; clearer logged-in user + role display.
- Unified Body Battery colouring between dashboard and health (shared util).
- Sleep score trend chart on health page.
- Segments + medals on the dashboard's most-recent activity.
- Segments tab on the Records page.
Repo hygiene: add .gitignore, untrack committed __pycache__/*.pyc.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
PocketID OIDC already auto-provisioned users keyed by pocketid_sub, and the
data layer was already fully user-scoped. This adds the missing pieces for
running real multi-user:
- auth.py callback: link by email to an existing un-linked account (so the
admin keeps their data when first signing in by passkey), collision-safe
username generation, and request the `groups` scope.
- Group gating: optional pocketid_allowed_group (admin-config or
POCKETID_ALLOWED_GROUP env); users lacking the group are rejected at the
callback and redirected to /login?auth_error=not_authorized.
- New admin users API (app/api/users.py): list users, promote/demote admin
(guards against demoting/locking out the last admin or yourself), and delete
a user with ordered bulk deletes of all their data + on-disk files.
- ProfilePage: allowed-group field; LoginPage: rejected-login message;
Layout: admin-only Users nav; new UsersPage.
Resync milevault_export to current source (it had drifted many features behind
— missing garmin_sync, npm-ci Dockerfile and @polyline-codec that broke its own
CI) and add POCKETID_ALLOWED_GROUP to .env.example.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>