OAuth callbacks were creating a new session record on every login from
the same device because user-agent/IP were hardcoded as empty strings,
producing a pile-up of 6+ identical 'Unknown browser' sessions.
- Add upsertUserSession(): looks up existing session by user_id +
device_fingerprint (SHA-256 of ua::ip, first 16 hex chars); reuses
and touches it (returning the same authSessionId) if found, creates
a new record otherwise
- Add device_fingerprint field to UserSession interface
- Fix OAuth callback to read real user-agent/IP from request headers
(they are available in RequestHandler via request.headers)
- Switch both OAuth and password login to upsertUserSession so the
returned authSessionId is used for the auth token
- Extend pruneStaleUserSessions to also cap sessions at 10 per user
- Keep createUserSession as a deprecated shim for gradual migration
- Fix language not persisting after refresh: save locale in user_settings,
set PARAGLIDE_LOCALE cookie from DB preference on server load
- Fix theme change not applying: context setter was mismatched (setTheme vs current)
- Add font family (system/serif/mono) and text size (sm/md/lg/xl) user settings
stored in DB and applied via CSS custom properties
- Add theme color dots and language picker to desktop header for quick access
- Footer locale switcher now saves preference to DB before switching
- Remove change password section (OAuth-only, no password login)
- Fix active sessions piling up: reuse existing session on re-login via OAuth
- Extend speed step cycle to include 2.5× and 3.0× (matching profile slider)
- Replace plain checkbox with modern toggle switch for auto-next setting
- Fix catalogue status labels (ongoing/completed) to use translation keys
- Add font family and text size translation keys to all 5 locale files
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>