# Improve First Connection Page Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Make connecting a Garmin account a mandatory, persisted gate during onboarding (and on every subsequent login until it succeeds), instead of an optional step left to the Profile page.
**Architecture:** Add a nullable `garmin_connected_at` timestamp to `profile`, set once on the first successful Garmin authentication (`handleGarminAuthLogin`/`handleGarminAuthMFA`). Expose it via `GET /api/session/me`. `LoginGate.tsx` becomes a three-way fork (`!has_profile` → `CreateProfile`, `has_profile && !garmin_connected` → new `ConnectGarmin`, else → `App`), so a user who abandons the connect step re-enters at `ConnectGarmin` on their next login rather than skipping straight into the app.
**Tech Stack:** Go (`database/sql` via `modernc.org/sqlite`, chi router), React + TypeScript (Vite), no frontend test framework.
## Global Constraints
-`internal/store/schema.sql` is edited directly, never migrated (pre-production app, see `CLAUDE.md`).
- Every store/API method touching per-user data takes an explicit `userID` and filters by it.
- Cross-user isolation is tested adversarially (two real users, real IDs), not just checked for non-collision.
- After the `schema.sql` change, regenerate `docs/DATABASE.md` via `go run ./cmd/dumpschema` (from `backend/`).
-`gofmt -l .` must report nothing; `go vet ./...` and `go build ./...` must pass.
- No frontend test suite exists — frontend changes are verified via `npm run build`, `npm run lint`, and a manual browser check.
- This local dev DB predates schema changes made this session (`ON DELETE CASCADE`) but was already fixed up in place. A brand-new nullable column (`garmin_connected_at TEXT`) needs no such fix-up: SQLite's idempotent-skip `applySchema` never re-applies `schema.sql` to an existing DB file, but adding a nullable column to `profile`'s Go-side handling doesn't require the on-disk table to already have it selected correctly... **actually it does** — `GetProfile`'s `SELECT` will fail against the live dev DB once it lists `garmin_connected_at` in `profileColumns`, since that column doesn't exist there yet. Task 1 includes a real `ALTER TABLE` against the live dev DB (safe, additive, no rebuild needed this time) to keep manual testing working.
---
### Task 1: Persist `garmin_connected_at` in the store layer
**Files:**
- Modify: `backend/internal/store/schema.sql` (add column to the `profile` table)
_, err := db.ExecContext(ctx, `UPDATE profile SET garmin_connected_at = datetime('now') WHERE user_id = ? AND garmin_connected_at IS NULL`, userID)
if err != nil {
return fmt.Errorf("mark garmin connected for user %d: %w", userID, err)
}
return nil
}
```
- [ ]**Step 6: Run tests to verify they pass**
Run: `cd backend && go test ./internal/store/... -run 'TestMarkGarminConnected|TestIsolation_MarkGarminConnected' -v`
Expected: PASS.
- [ ]**Step 7: Run the full store test suite**
Run: `cd backend && go test ./internal/store/...`
Expected: PASS.
- [ ]**Step 8: Regenerate `docs/DATABASE.md`**
Run: `cd backend && go run ./cmd/dumpschema`
Expected: `docs/DATABASE.md` shows the new `garmin_connected_at` column in `profile`.
- [ ]**Step 9: Additively update the live local dev DB**
The real `backend/geniusrun.db` predates this column (schema.sql changes never retrofit an existing DB file — see `applySchema`'s idempotent skip in `db.go`). Unlike the earlier `ON DELETE CASCADE` fix, this is purely additive (a new nullable column), so a plain `ALTER TABLE` is sufficient — no rebuild-and-copy dance needed:
t.Fatal("userA's successful Garmin auth leaked into userB's garmin_connected flag")
}
}
```
- [ ]**Step 2: Run tests to verify they fail**
Run: `cd backend && go test ./internal/api/... -run 'TestSessionMe_ReportsGarminConnected|TestSessionMe_GarminConnectedStaysFalse|TestIsolation_GarminConnected' -v`
Expected: FAIL — `sessionMeResponse` has no field `GarminConnected`.
Run: `cd backend && go test ./internal/api/... -run 'TestSessionMe_ReportsGarminConnected|TestSessionMe_GarminConnectedStaysFalse|TestIsolation_GarminConnected' -v`
Expected: PASS.
- [ ]**Step 6: Run the full backend test suite**
Run: `cd backend && go build ./... && go vet ./... && gofmt -l . && go test ./...`
Also update the doc comment above the `LoginGate` function (currently describes only a two-way fork) -- change:
```tsx
// Wraps App: on mount, asks the backend whether this browser already has a
// valid session (GET /api/session/me). geniusrun has no anonymous view, so
// this is the first fork -- "show the login screen" vs "show the app." A
// second fork, once authenticated, is whether the session's account has a
// provisioned profile yet (session.has_profile) -- a brand-new OIDC login
// sees CreateProfile instead of App until it submits one.
```
to:
```tsx
// Wraps App: on mount, asks the backend whether this browser already has a
// valid session (GET /api/session/me). geniusrun has no anonymous view, so
// this is the first fork -- "show the login screen" vs "show the app." A
// second fork, once authenticated, is whether the session's account has a
// provisioned profile yet (session.has_profile) -- a brand-new OIDC login
// sees CreateProfile instead of App until it submits one. A third fork,
// once provisioned, is whether Garmin has ever been successfully connected
// (session.garmin_connected) -- mandatory, and re-checked on every login,
// not just immediately after signup.
```
- [ ]**Step 6: Build and lint**
Run: `cd frontend && npm run build && npm run lint`
Expected: build succeeds; lint reports no new warnings beyond the 3 pre-existing `PaceField.tsx` ones.
- [ ]**Step 7: Manual browser verification**
With the backend (`cd backend && ./start.sh`) and frontend (`cd frontend && ./start.sh`) running:
1. Delete your profile (or use a fresh OIDC account) so you land on `CreateProfile`.
2. Submit a display name — confirm you're immediately routed to `ConnectGarmin`, not the main app.
3. Try an obviously wrong Garmin password — confirm the error shows and the form stays editable.
4. Enter correct credentials (resolve MFA if prompted) — confirm "Connected to Garmin." appears with a "Continue to geniusrun" button, and clicking it lands in the main app.
5. Log out, log back in with the same account — confirm you land straight in the app (not back through `ConnectGarmin`), proving `garmin_connected_at` persisted.
6. On the `ConnectGarmin` screen specifically, confirm the "Log out" link works.
Run: `cd frontend && npm run build && npm run lint`
Expected: both succeed, no new lint warnings.
- [ ]**Step 3: Confirm `docs/DATABASE.md` is current**
Run: `cd backend && go run ./cmd/dumpschema && git status --short docs/DATABASE.md`
Expected: no output from `git status` (already committed in Task 1, unchanged since).
- [ ]**Step 4: Update `docs/IDEAS.md`**
Remove the now-implemented line from the Backlog section:
```
- 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
```
- [ ]**Step 5: Commit**
```bash
git add docs/IDEAS.md
git commit -m "docs: remove improve-first-connection-page from IDEAS backlog (implemented)"