Files
familysync/.planning/phases/06-ux-polish/06-05-SUMMARY.md
T
Lucas Berger fa90b7cf86 docs(phase-06): finalize 06-04/05/06 summaries + tracking
- 06-04-SUMMARY: pulse keyframe checkpoint PASS (playwright-cli); CP-04.3 iOS device-only noted
- 06-05-SUMMARY: TDD RED/GREEN + dead-end guard follow-up fix; playwright-cli cold-load + 401 PASS
- 06-06-SUMMARY: end-tracking/recurrence-bound/series-prompt/all-day-pill; Schedule-X selector fix noted
- ROADMAP: mark 06-04/05/06 complete; phase 6 row updated to 6/6 Complete 2026-06-10
- STATE: phase 06 position/status updated; 3 new metric rows; 6 new decisions; phase-level UX fixes (AppNav/BottomTabBar) documented; residual device-only items added to Blockers
2026-06-10 16:30:01 -04:00

11 KiB

phase, plan, subsystem, tags, dependency_graph, tech_stack, key_files, decisions, metrics
phase plan subsystem tags dependency_graph tech_stack key_files decisions metrics
06-ux-polish 05 pwa/auth
tdd
auth
session-expiry
d-10
d-11
requires provides affects
SessionExpiredError (apps/pwa/src/api/client.ts)
handleAuthResponse (apps/pwa/src/api/client.ts)
AuthSplash component (apps/pwa/src/components/AuthSplash.tsx)
sessionExpired flag (apps/pwa/src/store/calendarStore.ts)
QueryCache/MutationCache onError wiring (apps/pwa/src/main.tsx)
recurrenceUntil/recurrenceCount on CreateEventPayload (client.ts)
hasRrule on CalendarOccurrence client mirror (client.ts)
Plan 06-06 (EventForm consumes recurrenceUntil/recurrenceCount payload fields and occurrence.hasRrule)
CalendarShell (auth-gated render replaces optimistic pre-auth paint)
added patterns
TDD RED→GREEN for typed session-expiry detection (client.ts)
TanStack Query v5 global error handler via QueryCache/MutationCache constructor (NOT defaultOptions.onError)
Auth-gated render gate (meQuery.isSuccess required before CalendarContent paints)
One-shot redirect guard re-arm via clearLoginRedirect() + maybeRedirectToLogin()
created modified
apps/pwa/src/components/AuthSplash.tsx
apps/pwa/src/api/client.ts
apps/pwa/src/api/client.test.ts
apps/pwa/src/components/CalendarShell.tsx
apps/pwa/src/main.tsx
apps/pwa/src/store/calendarStore.ts
D-10
unauthenticated cold load shows only the neutral AuthSplash ("Signing you in") — no calendar shell, skeleton, or pre-auth flash; CalendarContent renders only on meQuery.isSuccess
D-11
mid-use session expiry detected via SessionExpiredError (opaqueredirect || 401) across all fetch wrappers; global QueryCache/MutationCache onError sets sessionExpired flag → AuthSplash "Session expired / Signing you back in…" interstitial → redirect after 1.5s
TanStack v5
QueryCache({onError})/MutationCache({onError}) constructor pattern confirmed; defaultOptions.onError is removed in v5 and was NOT used
One-shot guard
clearLoginRedirect() re-arms the guard before maybeRedirectToLogin() in the session-expiry path (prevents redirect loop)
In-flight write replay deferred (D-11 nice-to-have, RESEARCH Open Question 3)
duration_minutes completed_date tasks_completed files_changed
35 2026-06-10 4 6

Phase 06 Plan 05: Auth-Flow Gating + Session-Expiry (D-10/D-11) Summary

One-liner: Typed SessionExpiredError centralized across all fetch wrappers (TDD), AuthSplash component gating the app render until meQuery.isSuccess, and a global QueryCache/MutationCache onError handler that surfaces a "Session expired" interstitial and redirects on any mid-use 401.

Tasks Completed

Task Name Commit Files
1 (RED) Failing SessionExpiredError detection tests e5072ff client.test.ts
1 (GREEN) Centralize session-expiry detection + extend client types d7d4023 client.ts, client.test.ts
2 AuthSplash component + gate CalendarShell render on auth state e7b34a5 AuthSplash.tsx, CalendarShell.tsx
3 Global session-expiry interstitial via QueryCache/MutationCache onError 139ef00 main.tsx, calendarStore.ts, CalendarShell.tsx
4 (checkpoint) playwright-cli — no pre-auth flash; clean session-expiry redirect (verification only)
Follow-up (RED) Failing dead-end AuthSplash test for exhausted redirect guard 36ef7a0 CalendarShell.test.tsx
Follow-up (fix) Make AuthSplash dead-end state reachable + persist redirect guard e392c69 CalendarShell.tsx

What Was Built

Task 1: SessionExpiredError + handleAuthResponse (TDD)

Added to apps/pwa/src/api/client.ts:

  • class SessionExpiredError extends Error with Object.setPrototypeOf(this, SessionExpiredError.prototype) for reliable instanceof checks across TypeScript compilation boundaries.
  • handleAuthResponse(res, label) helper: throws SessionExpiredError on res.type === 'opaqueredirect' || res.status === 401; throws a generic Error on other non-ok responses; passes through on ok.
  • redirect: 'manual' added to all six fetch wrappers (fetchEvents, createEvent, updateEvent, deleteEvent, fetchSyncStatus, fetchWritableCalendars) — matching the existing fetchMe pattern.
  • Client type additions (exclusive client.ts ownership): recurrenceUntil?: string and recurrenceCount?: number on CreateEventPayload (D-06 payload contract for Plan 06-06); hasRrule: boolean on the client-side CalendarOccurrence mirror matching expand.ts exactly (Pitfall 4 — atomic mirror).

TDD gate: RED commit (e5072ff) — tests failing with "SessionExpiredError is not a constructor". GREEN commit (d7d4023) — all client tests pass; 500 responses throw a generic Error, not SessionExpiredError.

Task 2: AuthSplash + Gated CalendarShell (D-10)

Created apps/pwa/src/components/AuthSplash.tsx:

  • Full-screen centered column (height: 100dvh, --color-surface background).
  • Loader2 spinner (24px, --color-member-0, global spin keyframe).
  • role="status" + aria-label="Signing you in".
  • state: 'loading' | 'redirecting' | 'dead-end' prop driving copy per UI-SPEC Surface 1:
    • loading: "Signing you in" / Loader2 spinner.
    • redirecting: "Taking you to the sign-in page…" / Loader2 spinner.
    • dead-end: "Sign-in required. Tap here to try again." — tap calls clearLoginRedirect() then maybeRedirectToLogin().
  • Logo lockup omitted gracefully (brand asset not present; no block on that).

CalendarShell.tsx updated:

  • meQuery.isLoading → early-return <AuthSplash state="loading" /> (no skeleton or calendar paints pre-auth).
  • meQuery.isError<AuthSplash state="redirecting" /> (replaces the old role="alert" "Sign-in required" block; existing maybeRedirectToLogin() effect still fires).
  • CalendarContent/SkeletonCalendar render only on meQuery.isSuccess.

Task 3: Global Session-Expiry Interstitial (D-11)

calendarStore.ts: added sessionExpired: boolean (default false) + setSessionExpired(v: boolean) action.

main.tsx: QueryClient constructed with:

queryCache: new QueryCache({ onError(error) { if (error instanceof SessionExpiredError) setSessionExpired(true) } }),
mutationCache: new MutationCache({ onError(error) { if (error instanceof SessionExpiredError) setSessionExpired(true) } }),

TanStack Query v5 API confirmed via Context7 (/tanstack/query): defaultOptions.onError was removed in v5; QueryCache/MutationCache constructor onError is the correct path and always fires.

CalendarShell.tsx: when sessionExpired is true, renders <AuthSplash state="redirecting" /> with the Surface-2 copy ("Session expired / Signing you back in…"); on mount schedules clearLoginRedirect() then maybeRedirectToLogin() after ~1.5s.

Checkpoint Verification (Task 4)

Verified via playwright-cli against desktop Chromium with DEV_AUTH_BYPASS=true:

  1. Cold load (D-10): First painted frame = neutral "Signing you in" splash (role=status). No calendar shell, SkeletonCalendar, or "Sign-in required" alert before the redirect toward /api/login. PASS.
  2. Mid-use 401 (D-11): Intercepted /api/events returning 401 → "Session expired / Signing you back in…" interstitial appeared → navigated to /api/login within ~2s. No hang, no generic error. PASS.

Follow-Up Fix After Checkpoint

The checkpoint surfaced two related issues:

  1. Dead-end state unreachable: CalendarShell never rendered AuthSplash state="dead-end" — the render logic fell through to an empty fragment once the redirect guard was exhausted.
  2. One-shot redirect guard persistence: The guard (familysync.loginRedirectAttempted) was cleared during the navigation to /api/login, so it was not available to the new page load; a fresh 401 immediately re-triggered the redirect loop.

Fix (commits 36ef7a0 RED, e392c69 fix):

  • CalendarShell now renders <AuthSplash state="dead-end" /> once the redirect guard is exhausted after the interstitial fires.
  • Guard persistence hardened: clearLoginRedirect() is called only at the point the user explicitly taps "Sign-in required. Tap here to try again." — not during the automatic redirect path.

Re-verified PASS via playwright-cli after fix.

Residual Device-Only Item

iOS-Safari standalone cold-load/redirect remains a human/device checkpoint per 06-VALIDATION.md Manual-Only table. The standalone-mode OIDC redirect (no window.location.href cross-origin fallback) is not drivable in desktop Chromium.

TDD Gate Compliance

  • RED commit (test(06-05): ...): e5072ff — tests failing with SessionExpiredError is not a constructor
  • GREEN commit (feat(06-05): ...): d7d4023 — all client tests pass
  • Follow-up RED: 36ef7a0 — failing dead-end guard test
  • Follow-up fix: e392c69 — guard and dead-end state corrected; full suite green
  • RED→GREEN order confirmed via git log

Deviations from Plan

Auto-fixed Issues

1. [Rule 1 - Bug] Dead-end AuthSplash state unreachable + redirect guard not persisting

  • Found during: Task 4 (playwright-cli checkpoint)
  • Issue: CalendarShell never rendered AuthSplash state="dead-end" (fall-through to empty fragment); the one-shot redirect guard was cleared during navigation, not on user tap, making the guard unavailable to the landing page on a fresh 401.
  • Fix: CalendarShell now renders the dead-end state once the redirect guard exhausts; guard is cleared only on explicit user tap in the dead-end handler.
  • Files modified: apps/pwa/src/components/CalendarShell.tsx
  • Commits: 36ef7a0 (RED), e392c69 (fix)

Threat Flags

None beyond the plan's STRIDE register. T-06-05-info (pre-auth render gate), T-06-05-redirect (one-shot guard prevents redirect loop), T-06-05-session (opaqueredirect || 401 detection only — no token read), T-06-05-inflight (in-flight write replay deferred, documented). No new surfaces introduced.

Self-Check: PASSED

  • apps/pwa/src/components/AuthSplash.tsx — FOUND (created)
  • apps/pwa/src/api/client.ts — FOUND (class SessionExpiredError, handleAuthResponse, hasRrule, recurrenceUntil)
  • apps/pwa/src/main.tsx — FOUND (MutationCache, QueryCache; no defaultOptions.onError)
  • apps/pwa/src/store/calendarStore.ts — FOUND (sessionExpired, setSessionExpired)
  • Commit e5072ff — FOUND (RED: test(06-05))
  • Commit d7d4023 — FOUND (GREEN: feat(06-05))
  • Commit e7b34a5 — FOUND (feat(06-05): gate app render)
  • Commit 139ef00 — FOUND (feat(06-05): global session-expiry interstitial)
  • Commit 36ef7a0 — FOUND (test(06-05): dead-end guard RED)
  • Commit e392c69 — FOUND (fix(06-05): dead-end state reachable)