Files
familysync/.planning/milestones/v1.0-phases/06-ux-polish/06-VERIFICATION.md
T
Lucas Berger 982438dc10 style(13-03): apply Prettier formatting across repo
Mechanical reformat — no logic changes. 398 files changed, 19125
insertions(+), 16457 deletions(-). Prettier 3.8.4 with .prettierrc
(singleQuote:true, semi:true, tabWidth:2, trailingComma:all,
printWidth:100). Isolated per D-13-08 for reviewability.
2026-06-11 20:35:18 -04:00

32 KiB

phase, verified, status, score, overrides_applied, human_verification
phase verified status score overrides_applied human_verification
06-ux-polish 2026-06-10T20:37:54Z human_needed 12/12 0
test expected why_human
iOS/standalone cold-load and OIDC redirect (D-10/D-11) PWA installed to iOS Home Screen cold-loads to the AuthSplash 'Signing you in' splash; Authelia redirects correctly in standalone mode; session-expiry interstitial fires and navigates back to /api/login without a hang. iOS Safari standalone OIDC redirect behavior is explicitly excluded from playwright-cli scope (CLAUDE.md convention; cannot simulate Safari standalone mode in desktop Chromium). Per 06-VALIDATION.md Manual-Only table.
test expected why_human
PushPermissionPrompt spinner on iOS device (CP-04.3) The Loader2 spinner in PushPermissionPrompt rotates using the global @keyframes spin from tokens.css after the local redundant redefinition was removed. PushPermissionPrompt only renders inside an installed iOS/standalone PWA. Desktop Chromium never surfaces the component. The global keyframe resolves correctly per code inspection but a real device spot-check was not run (documented residual in 06-04-SUMMARY.md).

Phase 06: UX Polish Verification Report

Phase Goal: Smooth the rough edges surfaced during live use — clearer all-day events, saner event-form date/recurrence behavior, recurring-series editing, and auth-flow polish — so the app feels slick for the non-technical Apple member (hard UX constraint). Verified: 2026-06-10T20:37:54Z Status: human_needed Re-verification: No — initial verification


Goal Achievement

Observable Truths

# Truth Status Evidence
1 Moving an event's start moves its end preserving duration; end never strands behind start (D-03/D-04) VERIFIED computeNewTimedEnd + computeNewAllDayEnd exported from eventDateTime.ts; wired in EventForm.tsx start onChange handlers at lines 729-760; 6 unit tests green; EventForm tests D-04 timed + all-day pass
2 A recurring series can be bounded via "Ends: Never / On date / After N times" (D-06) VERIFIED assembleRruleString in outboxWorker.ts; recurrenceUntil/recurrenceCount Zod fields in both events.ts and outboxWorker.ts; "Ends" control in EventForm.tsx (state at lines 214-216, rendered at line 888+); 7 assembleRruleString tests green; playwright-cli verified
3 Editing a recurring occurrence prompts "Edit recurring series" before saving (D-08/D-09) VERIFIED SeriesEditPrompt.tsx created with role="dialog", aria-modal, focus trap, Escape=cancel, correct copy; EventForm.tsx gates Save on occurrence?.hasRrule === true at line 419; hasRrule populated in expand.ts + mirrored in client.ts; playwright-cli verified
4 All-day events visually distinct from timed events at a glance (999.6/D-12) VERIFIED .sx__date-grid .sx__date-grid-event and .sx__month-grid-day__events .sx__month-grid-event:not(:has(.sx__month-grid-event-time)) CSS rules in index.css (lines 125-140) with border-radius:4px, font-weight:600, border-inline-start:none; real Schedule-X v4.6.0 selectors (not the non-existent .sx__all-day-event) verified correct after follow-up fix 6dbb166; playwright-cli verified
5 All-day edit off-by-one stays fixed — re-editing does not grow event by a day (D-05) VERIFIED exclusiveEndToInclusiveDate pre-fill at EventForm reset line intact; D-05 round-trip test in EventForm.test.tsx passes; playwright-cli verified
6 Unauthenticated cold load shows only the neutral "Signing you in" splash — no calendar/skeleton/alert flash (D-10) VERIFIED CalendarShell.tsx returns <AuthSplash state="loading" /> on meQuery.isLoading before any calendar content (line 269); AuthSplash.tsx created with role="status", correct copy, full-screen centered layout; playwright-cli checkpoint PASS for desktop Chromium
7 A session that expires mid-use shows "Session expired" interstitial and cleanly redirects (D-11) VERIFIED SessionExpiredError class in client.ts; handleAuthResponse covers all 7 fetch wrappers; QueryCache/MutationCache onError in main.tsx (not defaultOptions.onError); sessionExpired flag in calendarStore.ts; CalendarShell renders <AuthSplash state="redirecting" /> on sessionExpired=true with 1.5s redirect; dead-end state reachable when guard exhausted (follow-up fix e392c69); playwright-cli checkpoint PASS for desktop Chromium
8 Sync indicators actually animate — SyncStateToast spinner spins and LiveSyncIndicator reconnecting dot pulses (D-13) VERIFIED @keyframes pulse added to tokens.css at line 149 (0%,100% opacity:1; 50% opacity:0.4); redundant local @keyframes spin block removed from PushPermissionPrompt.tsx (confirmed absent); playwright-cli checkpoint PASS — both animationName values non-'none' in desktop Chromium
9 Nav chrome persists on /lists — BottomTabBar does not overlap Settings on desktop (UAT fixes FIX-3/FIX-4) VERIFIED AppNav lifted to App.tsx as a persistent sibling of <Routes> (outside any Route, line 112); BottomTabBar returns null on desktop via isPhone() guard (line 56); AppNav persistence test and BottomTabBar hidden-on-desktop test both green
10 D-04 floor rule: end snaps to newStart+1h (timed) / same day (all-day) when old end was already behind start VERIFIED deltaMs = oldEndMs > oldStartMs ? oldEndMs - oldStartMs : 60*60*1000 in computeNewTimedEnd; Math.max(0, dateDiffDays(...)) in computeNewAllDayEnd; two floor-rule unit tests green
11 RRULE UNTIL value-type matches DTSTART — DATE form for all-day, DATETIME UTC for timed (D-06, RFC 5545) VERIFIED assembleRruleString: all-day emits UNTIL=YYYYMMDD, timed emits UNTIL=YYYYMMDDTHHMMSSZ (T235959Z); three vevent.test.ts serialization assertions + five assembleRruleString unit tests green
12 FREQ=DAILY regression locked (D-07) VERIFIED FREQ persistence (D-07 regression) test in outboxWorker.test.ts asserts daily-recurrence payload emits RRULE:FREQ=DAILY; green

Score: 12/12 truths verified


Required Artifacts

Artifact Expected Status Details
apps/pwa/src/lib/eventDateTime.ts computeNewTimedEnd + computeNewAllDayEnd exports with floor rules; no toISOString().slice VERIFIED Both functions exported at lines 114/142; WR-05 compliance confirmed — no toISOString().slice in helper code
apps/pwa/src/lib/eventDateTime.test.ts 6 new tests: 3 timed + 3 all-day end-tracking, RED→GREEN VERIFIED 6 tests present in two describe blocks; computeNewTimedEnd + computeNewAllDayEnd imported; all pass
apps/api/src/broker/outboxWorker.ts assembleRruleString exported; recurrenceUntil/recurrenceCount in outboxPayloadSchema VERIFIED assembleRruleString exported at line 114; both fields at lines 83-84
apps/api/src/routes/events.ts eventFieldsSchema accepts recurrenceUntil + recurrenceCount VERIFIED Both fields at lines 111-112
apps/api/tests/broker/vevent.test.ts UNTIL-DATE, UNTIL-DATETIME, COUNT serialization assertions VERIFIED 3 assertions match verified ical.js 2.2.1 output strings
apps/api/tests/broker/outboxWorker.test.ts assembleRruleString describe + FREQ persistence test VERIFIED Both describe blocks present; 7+1 tests pass
apps/api/src/broker/expand.ts hasRrule: boolean on CalendarOccurrence; populated from event.isRecurring() in both push sites VERIFIED Field at line 68; const isRecurring capture at line 224; both push sites at lines 261/308
apps/api/tests/broker/expand.test.ts hasRrule true/false assertions + bounded COUNT=3 invariant VERIFIED hasRrule describe with 2 tests + Bounded RRULE describe with 3 tests; all pass
apps/api/tests/fixtures/weekly-count3.ics Bounded fixture for COUNT=3 test VERIFIED File exists at apps/api/tests/fixtures/weekly-count3.ics
apps/pwa/src/styles/tokens.css @keyframes pulse added globally VERIFIED Present at line 149; exactly once
apps/pwa/src/components/PushPermissionPrompt.tsx Redundant @keyframes spin <style> block removed VERIFIED grep -q '@keyframes spin' returns nothing
apps/pwa/src/api/client.ts SessionExpiredError; handleAuthResponse; redirect:'manual' on all wrappers; hasRrule on CalendarOccurrence; recurrenceUntil/recurrenceCount on CreateEventPayload VERIFIED All present: class SessionExpiredError at line 33; handleAuthResponse at line 51; 7 handleAuthResponse call sites; hasRrule at line 131; recurrenceUntil at line 190
apps/pwa/src/api/client.test.ts SessionExpiredError detection tests (opaqueredirect + 401 per wrapper; 500 = generic Error) VERIFIED 40 tests pass; opaqueredirect/401/500 cases for fetchEvents, createEvent, updateEvent, deleteEvent, fetchMe
apps/pwa/src/components/AuthSplash.tsx Full-screen interstitial; loading/redirecting/dead-end states; role="status" VERIFIED File created; AuthSplashState type at line 27; role="status" at line 61; all three states handled
apps/pwa/src/components/CalendarShell.tsx meQuery.isLoading → AuthSplash loading; meQuery.isError → AuthSplash redirecting/dead-end; content only on isSuccess; sessionExpired interstitial wiring VERIFIED Lines 269-281 gate render; sessionExpired effect at lines 231-243; enabled: meQuery.isSuccess at line 120
apps/pwa/src/main.tsx QueryCache/MutationCache onError (NOT defaultOptions.onError) routing SessionExpiredError to setSessionExpired VERIFIED QueryCache at line 37; MutationCache at line 38; no defaultOptions.onError in file
apps/pwa/src/store/calendarStore.ts sessionExpired: boolean + setSessionExpired action VERIFIED sessionExpired: false default at line 159; setSessionExpired at line 187
apps/pwa/src/components/EventForm.tsx Start onChange handlers call computeNewTimedEnd/computeNewAllDayEnd; recurrenceBound state + "Ends" control; hasRrule gates SeriesEditPrompt; payload sends recurrenceUntil/recurrenceCount VERIFIED computeNewTimedEnd wired at lines 732/755; computeNewAllDayEnd at line 730; recurrenceBound state at line 214; "Ends" control at line 888; hasRrule gate at line 419; payload spread at lines 399-403
apps/pwa/src/components/SeriesEditPrompt.tsx Bottom-sheet/dialog; focus trap; Escape=cancel; exact UI-SPEC copy; accent-filled "Update series"; ghost "Cancel" VERIFIED File created; role="dialog", aria-modal at lines 135-136; "Edit recurring series" at line 154; "Update series" at line 217
apps/pwa/src/styles/index.css .sx__date-grid-event + .sx__month-grid-event:not(:has(.sx__month-grid-event-time)) all-day pill overrides (real v4.6.0 selectors) VERIFIED Both rules at lines 125/134 with border-radius:4px, font-weight:600, border-inline-start:none
apps/pwa/src/App.tsx AppNav as persistent sibling of <Routes> (FIX 3) VERIFIED <AppNav> rendered at line 112, outside <Routes> which starts at line 121
apps/pwa/src/components/BottomTabBar.tsx Returns null on desktop (FIX 4) VERIFIED if (!isPhone()) return null at line 56

From To Via Status Details
EventForm.tsx eventDateTime.ts start onChange → computeNewTimedEnd / computeNewAllDayEnd WIRED Both imports at lines 45-46; both calls in onChange handlers at lines 730, 732, 755
EventForm.tsx client.ts payload carries recurrenceUntil/recurrenceCount; occurrence.hasRrule gates prompt WIRED recurrenceUntil spread at line 399; recurrenceCount spread at line 402; hasRrule check at line 419
main.tsx calendarStore.ts QueryCache/MutationCache onErrorsetSessionExpired(true) on SessionExpiredError WIRED useCalendarStore.getState().setSessionExpired(true) at line 32; imperative store access confirmed
CalendarShell.tsx AuthSplash.tsx meQuery.isLoading/isError and sessionExpired flag render AuthSplash WIRED Imports at line 50; <AuthSplash state="loading" /> at line 270; <AuthSplash state="redirecting" /> at lines 281/291
outboxWorker.ts vevent.ts assembleRruleString result passed to buildVeventString WIRED assembleRruleString called at lines 360/371/451/462; result flows as rruleString into the dispatch path
events.ts outboxWorker.ts recurrenceUntil/recurrenceCount in enqueued payload WIRED Zod schema accepts fields in both eventFieldsSchema (events.ts:111-112) and outboxPayloadSchema (outboxWorker.ts:83-84)
expand.ts client.ts (mirror) CalendarOccurrence.hasRrule server source-of-truth mirrored WIRED hasRrule: boolean at expand.ts line 68 (authoritative); mirrored at client.ts line 131 with explicit comment

Data-Flow Trace (Level 4)

Artifact Data Variable Source Produces Real Data Status
EventForm.tsx computeNewTimedEnd result → endDate/endTime state eventDateTime.ts pure functions over form state (no network) Yes — deterministic math, no network fetch, no empty source FLOWING
EventForm.tsx recurrenceUntil/recurrenceCount → submit payload User input (controlled form state) Yes — user input flows directly to payload spread FLOWING
EventForm.tsx occurrence.hasRrule gate CalendarOccurrence from parent prop (occurrence fetched from API via fetchEvents) Yes — hasRrule populated server-side in expandOccurrences from event.isRecurring() FLOWING
CalendarShell.tsx meQuery.isLoading/isError TanStack Query ['me'] query → fetchMe()/api/me Yes — real API call with redirect:'manual'; auth gating is live FLOWING
CalendarShell.tsx sessionExpired Zustand store, set by QueryCache/MutationCache onError on real SessionExpiredError Yes — fires on real 401/opaqueredirect from any query/mutation FLOWING

Behavioral Spot-Checks

Behavior Command Result Status
computeNewTimedEnd + computeNewAllDayEnd exported node -e "import('./src/lib/eventDateTime.ts').then(m => console.log(typeof m.computeNewTimedEnd, typeof m.computeNewAllDayEnd))" function function PASS
D-04 end-tracking unit tests pass pnpm --filter @familysync/pwa test -- run lib/eventDateTime 191/191 pass PASS
SessionExpiredError detection tests pass npx vitest run src/api/client.test.ts (apps/pwa) 40/40 pass PASS
assembleRruleString + FREQ persistence tests pass npx vitest run tests/broker/outboxWorker.test.ts tests/broker/vevent.test.ts (apps/api) 39/39 pass PASS
hasRrule + bounded RRULE tests pass npx vitest run tests/broker/expand.test.ts (apps/api) 10/10 pass PASS
Full PWA test suite green pnpm --filter @familysync/pwa test -- run 191/191 pass (17 files) PASS
All phase-06 API broker + events tests green npx vitest run tests/broker/ tests/routes/events.test.ts (apps/api) 114/114 pass (9 files) PASS

Note: tests/routes/lists.test.ts and tests/routes/push.test.ts fail with ER_ACCESS_DENIED_ERROR (MariaDB not running with password in current dev environment). These are pre-existing integration-test DB-connectivity failures, not regressions introduced by phase 06. All broker tests that phase 06 modified or created are green.


Probe Execution

No probe scripts found (scripts/*/tests/probe-*.sh absent). Step 7c: SKIPPED.


Requirements Coverage

No v1 REQ-IDs were assigned to this phase (confirmed by phase description and plan frontmatter — requirements: [] in all plans). Phase is tracked against backlog items 999.2/3/6/7/8/9 and locked decisions D-01..D-13. All backlog items verified via truth/artifact checks above.


Anti-Patterns Found

File Line Pattern Severity Impact
SeriesEditPrompt.tsx 86 if (!open) return null Info Correct conditional render guard — component is fully substantive when open === true; not a stub

No TBD, FIXME, or XXX markers found in any phase-06-modified file. No unreferenced debt markers.


Human Verification Required

1. iOS/Standalone Cold-Load and OIDC Redirect (D-10/D-11)

Test: Install the PWA to iOS Home Screen. Cold-load with no session cookie. Confirm the first painted frame is the neutral "Signing you in" splash (not the calendar shell or "Sign-in required" alert), and that Authelia redirect completes correctly in standalone mode. Then simulate a session expiry to confirm the "Session expired / Signing you back in..." interstitial appears and redirects to /api/login without hanging.

Expected: Splash shown on cold-load; Authelia round-trip succeeds; mid-use 401 shows interstitial then redirects within ~2s; no redirect loop.

Why human: iOS Safari standalone OIDC redirect behavior cannot be driven by playwright-cli. This is the documented CLAUDE.md exception (standalone-mode OIDC redirect, window.location.href cross-origin fallback behavior). Recorded in 06-VALIDATION.md Manual-Only table. Desktop Chromium checkpoints already PASS (playwright-cli verified in 06-05).

2. PushPermissionPrompt Spinner on iOS Device (CP-04.3)

Test: On a real iOS device with the PWA installed as a standalone app, trigger the push permission prompt and confirm the Loader2 spinner rotates.

Expected: Spinner rotates using the global @keyframes spin from tokens.css (the redundant local redefinition was removed in commit 81f2678).

Why human: PushPermissionPrompt only surfaces inside an installed iOS/standalone PWA. No desktop Chromium path to the component. Code-confirmed: the inline animation: 'spin 1s linear infinite' is still present on the Loader2 element and resolves to the global keyframe. A real-device spot-check is required for confidence. Documented residual in 06-04-SUMMARY.md.


Notable Deviations from Plan (Not Gaps)

The following deviations were auto-fixed during execution and do not constitute gaps:

  1. Schedule-X CSS selector correction (06-06): Plan assumed .sx__all-day-event but Schedule-X v4.6.0 does not emit that class. Executor discovered and fixed in commits 6dbb166 + 5620261 using real v4.6.0 selectors. playwright-cli re-verified PASS.

  2. AuthSplash dead-end state + redirect guard persistence (06-05): Initial implementation had the dead-end state unreachable and the one-shot guard cleared prematurely. Found during playwright-cli checkpoint; fixed in commits 36ef7a0 + e392c69. Re-verified PASS.

  3. hasExplicitRecurrence precedence bug (06-02): recurrence:'none' with _preservedRrule present incorrectly fell through to emit an RRULE. Fixed in GREEN commit d2abb91. Regression test CR-01 confirms the fix.

  4. EventDetailPopover.test.tsx fixture update (06-06): Existing test fixtures omitted the new required hasRrule field. Mechanical fix in commit 69e5ae8.

  5. Dev-seed gap: The dev-bypass user (id 1) has no CalDAV credential/calendars (those belong to user 2), so live event-create-via-form could not be exercised end-to-end against Fastmail. Server logic and form UI verified via route-mocks and direct DB occurrence inserts. Not a code defect.


Gaps Summary

No gaps. All 12 truths are VERIFIED. The two human verification items are device-only constraints (iOS/standalone behavior) that were explicitly pre-classified as manual checkpoints in 06-VALIDATION.md before execution began.


Verified: 2026-06-10T20:37:54Z Verifier: Claude (gsd-verifier)