# Improve the first connection page Status: approved, not yet implemented. ## Problem `docs/IDEAS.md` backlog item: "improve first connection page: in CreateProfile.tsx, in addition to the display name, we should directly ask garmin login/password and manage MFA from there, so the synchronization experience in profile page will be easier later on". Today, `CreateProfile.tsx` only asks for a display name (`POST /api/setup`) and drops the user straight into the main app -- Garmin credentials are filled in later, whenever the user happens to visit the Profile page. This means a brand-new account can sit around fully "set up" but never actually synced, with no prompt nudging the user to connect Garmin. ## Design ### Scope decision: mandatory, persisted gate Connecting Garmin becomes a **hard requirement** to enter the app, enforced on every login, not just the first one -- confirmed explicitly, including the corollary that this can't be a purely in-session check: account provisioning (`POST /api/setup`) must happen before Garmin login can even be attempted (the per-user Garmin client is keyed by an already-existing account), so a user who abandons the flow mid-connect already has a `users`/`profile` row. Enforcing the gate on *every subsequent login* (not just immediately after signup) requires a persisted "has this account ever successfully connected" flag -- the in-memory `Server.userAuthStatus` map resets on every backend restart and can't be trusted for this. Also confirmed: both `CreateProfile` and the new gate screen get a "Log out" link (neither has one today), since a mandatory gate a user can get genuinely stuck on (no Garmin credentials handy right now) needs an escape hatch back to the login screen. ### Data model `schema.sql`, `profile` table -- one new nullable column: ```sql -- Set once, the first time this user successfully authenticates with -- Garmin (handleAuthLogin/handleAuthMFA). Null means never connected -- -- the login gate uses this (not the in-memory auth status, which -- resets on restart) to decide whether a returning user must -- reconnect before entering the app. garmin_connected_at TEXT, ``` `store.Profile` gets a matching `GarminConnectedAt *string` field (same nullable-TEXT-pointer pattern as `sync_state.EarliestSyncedDate`). New store method (`internal/store/users.go` or a new small file, e.g. `profile.go` wherever `UpdateProfile`/`GetProfile` already live): ```go // MarkGarminConnected records the first time userID successfully // authenticates with Garmin. A no-op if already set, so it always reflects // the first connection, not the most recent one. func (db *DB) MarkGarminConnected(ctx context.Context, userID int64) error ``` Implemented as `UPDATE profile SET garmin_connected_at = datetime('now') WHERE user_id = ? AND garmin_connected_at IS NULL`. After the schema edit, regenerate `docs/DATABASE.md` via `go run ./cmd/dumpschema` (per project convention). ### Backend API - **`handleGarminAuthLogin`/`handleGarminAuthMFA`** (`internal/api/auth.go`): right after the existing `s.recordAuthResult(userID, res)` call, if `res.Status == garmin.AuthSuccess`, call `s.DB.MarkGarminConnected(r.Context(), userID)`. A failure here is logged and does *not* fail the HTTP response -- the Garmin authentication itself already succeeded; failing to persist the flag is a non-fatal side effect (the next successful login attempt would just set it then). - **`sessionMeResponse`** (`internal/api/session.go`): add `GarminConnected bool \`json:"garmin_connected"\``. - **`handleSessionMe`**: when the session already has a profile, fetch it (`s.DB.GetProfile`) and set `resp.GarminConnected = profile.GarminConnectedAt != nil`. - No other endpoint changes. The new frontend screen calls the exact same `PUT /api/profile`, `POST /api/auth/login`, `POST /api/auth/mfa` the existing Profile page already uses. ### Frontend flow - **`types/api.ts`**: `SessionInfo` gains `garmin_connected: boolean`. - **`LoginGate.tsx`**: becomes a three-way fork instead of two: ``` !authenticated -> login screen authenticated && !has_profile -> CreateProfile authenticated && has_profile && !garmin_connected -> ConnectGarmin authenticated && has_profile && garmin_connected -> App ``` `CreateProfile.onCreated` and the new `ConnectGarmin.onConnected` both just flip the relevant boolean in the local `session` state object (same pattern `has_profile` already uses). - **`CreateProfile.tsx`**: behavior unchanged (display name -> `POST /api/setup`); it's simply no longer the last screen. Gains a "Log out" link (`