Files
familysync/.planning/phases/12-initial-setup-wizard/12-07-SUMMARY.md
T

8.0 KiB

phase, plan, subsystem, tags, requires, provides, affects, tech-stack, key-files, key-decisions, patterns-established, requirements-completed, duration, completed
phase plan subsystem tags requires provides affects tech-stack key-files key-decisions patterns-established requirements-completed duration completed
12-initial-setup-wizard 07 ui
react
tanstack-query
react-router
setup-wizard
pwa
oidc
phase provides
12-initial-setup-wizard SetupPage wizard with alreadyLocked Surface 8; /setup route gate; first-login-claim in upsertUser; SetupBanner self-service onboarding (12-03/12-04)
/setup route reverse-gated on setupComplete — Surface 8 ('Setup already complete') after completion, never re-mounts the wizard
['me'] freshness fix (staleTime 0) so the post-wizard 'Set up your calendar' banner clears once the claimed credential is in effect
SetupBanner.test.tsx regression coverage for the success-only dismissal contract + stale-cache refetch
setup-wizard
onboarding
pwa-app-shell
added patterns
Reverse route-gate: conditional route element keyed on setupComplete/setupLoading mirroring the existing `*`-route gate
staleTime 0 on a boot-critical ['me'] query so authenticated-shell entry always reflects fresh server truth (post-claim)
created modified
apps/pwa/src/components/SetupBanner.test.tsx
apps/pwa/src/App.tsx
apps/pwa/src/App.test.tsx
apps/pwa/src/components/SetupBanner.tsx
D-12-07-GAP6-MECH: gap 6 root cause is mechanism (ii) — ['me'] client-cache staleness, NOT a backend linking gap. upsertUser's first-login claim preserves users.id (where eq(users.id, unclaimed.id)), so the wizard-stored CalDAV credential stays linked and the DB reports needsProviderSetup=false. Fix is client-only.
D-12-07-STALE0: ['me'] staleTime set to 0 in both App.tsx (boot) and SetupBanner.tsx so a pre-claim stale entry is refetched on shell entry; success-only dismissal contract preserved (no dismiss/X button added).
D-12-07-LOCKED-PROP: /setup reverse-gate uses SetupPage alreadyLocked prop (Surface 8) rather than Navigate, keeping the operator on /setup with a terminal surface per UAT expectation.
Reverse-gate a standalone route by swapping its element via the same loading/complete derivation used by the app-shell gate.
SETUP-01
SETUP-04
11min 2026-06-16

Phase 12 Plan 07: UAT Gap-Closure (gaps 5 & 6) Summary

The /setup wizard no longer re-mounts after completion (shows Surface 8 'Setup already complete'), and the '/calendar' setup banner no longer nags the operator after they finish the wizard — fixed by reverse-gating the route and making the ['me'] query fresh on shell entry.

Performance

  • Duration: ~11 min
  • Started: 2026-06-16T21:19Z
  • Completed: 2026-06-16T21:24Z
  • Tasks: 2
  • Files modified: 4 (3 modified, 1 created)

Accomplishments

  • Gap 5 closed: /setup is now reverse-gated on setupComplete. After completion, manually visiting /setup renders SetupPage alreadyLocked → Surface 8 "Setup already complete" (the backend already 423s setup mutations; this is the matching frontend gate). Loading state renders a no-flash placeholder; setupComplete===false still mounts the active wizard.
  • Gap 6 closed: the "Set up your calendar" banner no longer persists after the operator completes the wizard. Root cause confirmed as a ['me'] cache-staleness gap (mechanism ii), NOT a backend linking gap. Set ['me'] staleTime to 0 in both App.tsx (boot) and SetupBanner.tsx so a pre-claim stale entry is refetched on entry to the authenticated shell — needsProviderSetup then reflects the just-claimed credential and the banner hides.
  • Regression coverage added: new SetupBanner.test.tsx (3 tests) + 2 new App reverse-gate tests. Full PWA suite green at 258 tests; typecheck clean.

Gap 6 — Root Cause Investigation (Task 2 Step A)

The UAT root_cause was flagged PRELIMINARY. Reading apps/api/src/auth/user.ts upsertUser confirmed the first-login claim branch (lines ~118-130) updates the unclaimed row via where(eq(users.id, unclaimed.id)) — it preserves the same users.id. Because member_credentials is keyed on userId, the wizard-stored CalDAV credential stays linked to the claimed operator row, so the DB correctly returns needsProviderSetup=false after first OIDC login.

Conclusion: mechanism (i) (a backend claiming/linking gap) is NOT the cause — consistent with the 12-03 summary and threat-register disposition T-12-10 = accept. The cause is mechanism (ii): ['me'] had staleTime: 5 * 60 * 1000, so a cache entry populated before the claim (e.g. a pre-auth visit) served needsProviderSetup=true for up to 5 minutes after the operator authenticated post-wizard. No backend change was made; the fix is purely client-side cache freshness.

Task Commits

  1. Task 1: Gate the /setup route on setupComplete (gap 5)fdcb4dc (feat)
    • (also carried the App.tsx ['me'] staleTime → 0 edit, staged together; the SetupBanner-side change + its test landed in Task 2)
  2. Task 2: Diagnose + fix the persistent calendar banner (gap 6)2b3569f (fix)

Files Created/Modified

  • apps/pwa/src/App.tsx — reverse-gated /setup route element (loading placeholder / alreadyLocked Surface 8 / active wizard); boot ['me'] staleTime → 0 with mechanism note.
  • apps/pwa/src/components/SetupBanner.tsx['me'] staleTime 5min → 0 (gap-6 freshness); no dismiss button added; success-only dismissal contract restated in comments.
  • apps/pwa/src/App.test.tsx — SetupPage mock now respects alreadyLocked; 2 new reverse-gate tests (already-complete surface + active wizard on /setup).
  • apps/pwa/src/components/SetupBanner.test.tsx — NEW: banner absent when needsProviderSetup=false, present (no dismiss button) when true, and stale-cache refetch hides the banner on mount (staleTime 0).

Decisions Made

  • D-12-07-GAP6-MECH — gap 6 is mechanism (ii) ['me'] staleness, not a linking gap (evidence: id-preserving claim in upsertUser).
  • D-12-07-STALE0['me'] staleTime set to 0 in App.tsx + SetupBanner.tsx; success-only dismissal contract preserved.
  • D-12-07-LOCKED-PROP/setup reverse-gate uses the alreadyLocked prop (Surface 8), not Navigate, per the UAT expectation that manual /setup navigation shows the "already complete" surface.

Deviations from Plan

None — plan executed as written. The plan's expected gap-6 finding (mechanism ii) was confirmed by the Task 2 Step A investigation; the prescribed staleTime fix was applied. No architectural changes; no backend changes required.

Issues Encountered

  • Pre-existing PWA lint errors (out of scope). pnpm lint in apps/pwa reports 22 errors in src/api/setupClient.contract.test.ts (no-unsafe-*) and src/routes/SetupPage.test.tsx:152 (no-unused-vars). Neither file was touched by this plan; both were last modified in earlier Phase-12 commits. The four files this plan touched lint clean (exit 0). Logged to .planning/phases/12-initial-setup-wizard/deferred-items.md and left untouched per the executor SCOPE BOUNDARY rule. Recommend a follow-up lint-cleanup quick task.

Verification

  • apps/pwa full suite: 258 tests passed (22 files); App.test.tsx 8 passed; SetupBanner.test.tsx 3 passed.
  • pnpm typecheck (apps/pwa): clean.
  • grep -nE "alreadyLocked" apps/pwa/src/App.tsx/setup route gated on setupComplete (Surface 8).
  • grep for new dismiss/X button in SetupBanner.tsx → none added (only contract comments).
  • Touched-files lint: eslint over the 4 files → exit 0.

Known Stubs

None.

User Setup Required

None — no external service configuration required.

Next Phase Readiness

  • Both UAT major gaps (5 and 6) are closed in code with regression tests. Ready for re-UAT of the post-completion /setup surface and the post-wizard calendar banner.
  • Remaining UAT gaps (if any from 12-05/12-06) are tracked in their own gap-closure plans; this plan scoped only gaps 5 & 6.
  • Pre-existing PWA lint debt deferred (see deferred-items.md) — does not block this plan's UI behavior.

Phase: 12-initial-setup-wizard Completed: 2026-06-16