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>