Files
familysync/.planning/milestones/v1.0-phases/02-calendar-display/02-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

24 KiB

phase, verified, status, human_uat, score, overrides_applied, human_verification
phase verified status human_uat score overrides_applied human_verification
02-calendar-display 2026-06-05T16:00:00Z passed approved 2026-06-05 (see 02-HUMAN-UAT.md) — operator confirmed all 4 success criteria in the running dev stack 4/4 must-haves verified 0
test expected why_human
Confirm color-coded event display: each member's events appear in their assigned hex, shared-family events in rose #F25C7A; the ColorLegend decodes ownership Personal events use the owner's color from users.color; rose lane is empty (D-16, no shared calendar yet) but the legend shows the Family row correctly Color rendering is visual; CSS token overrides and Schedule-X lightColors derivation cannot be verified by grep — only by visual inspection in a browser
test expected why_human
Switch between Day, Week, Month, and Agenda views and confirm events render correctly in each with no missing or misplaced events All four view factories (createViewDay/Week/MonthGrid/MonthAgenda) render events; week/day time-grid scrolls; navigation (Today/prev/next) works in each view View rendering and grid layout require a running browser; Schedule-X DOM output cannot be asserted statically
test expected why_human
Find a recurring event and navigate across the March 2026 DST boundary; confirm occurrences stay at the correct local wall-clock time (no ±1h shift) A weekly 10:00 America/New_York event shows 10:00 on both sides of the Spring-forward boundary — not 09:00 or 11:00 after the transition VTIMEZONE registration + ICAL.RecurExpansion + Schedule-X display timezone are correct in code (verified), but DST correctness must be visually confirmed with real Fastmail data
test expected why_human
Find a recurring all-day event (e.g. a birthday) and confirm it appears as a full-day banner on the correct date with no day shift All-day events render on the date matching the DTSTART DATE value — not shifted one day early or late by a timezone offset Temporal.PlainDate routing is correct in code; visual confirmation with live data needed to rule out any Schedule-X display-zone interaction

Phase 02: Calendar Display — Verification Report

Phase Goal: Both members can see a unified, color-coded calendar aggregating all accessible Fastmail calendars across day, week, month, and agenda views — read-only, no write-back yet.

Verified: 2026-06-05T16:00:00Z Status: human_needed (all automated checks pass; 4 human UAT items remain) Re-verification: No — initial verification


Goal Achievement

Observable Truths

# Truth Status Evidence
1 Color-coded calendar — each member's events in their assigned color, shared events distinguishable from personal VERIFIED (code) events.ts derives color = row.isShared ? '#F25C7A' : row.userColor; hydrateEvents.ts routes calendarId = occ.isShared ? 'shared' : String(occ.ownerUserId); buildCalendarConfig() keys per-member by String(userId) + 'shared' with deriveScheduleXColors(). Rose lane intentionally empty per D-16 (no shared Fastmail calendar yet — operator-deferred).
2 Day/week/month/agenda views — all events render correctly in each VERIFIED (code) CalendarShell.tsx passes all four factories (createViewDay, createViewWeek, createViewMonthGrid, createViewMonthAgenda) to useCalendarApp; Schedule-X built-in header provides the view switcher and navigation.
3 Recurring events display all occurrences in-window, correct across DST boundaries VERIFIED (code) expand.ts registers VTIMEZONE via getAllSubcomponents('vtimezone') at line 190, before new ICAL.RecurExpansion at line 264; uses ICAL.Time.fromJSDate(windowStart, true) (UTC-based) for absolute occurrence windowing; serializeTime() emits IANA-annotated strings ('...±HH:MM[IANA/Zone]'); Schedule-X display timezone set to Intl.DateTimeFormat().resolvedOptions().timeZone. events.ts pre-filter includes all-day recurring masters via dtstartDate < end fallback. sync.ts sets hasRrule: isRecurring on both insert and update paths.
4 All-day events appear as full-day banners on the correct date with no timezone shift VERIFIED (code) expand.ts serializeTime(t, allDay=true) returns 'YYYY-MM-DD' strings only; hydrateEvents.ts branches on occ.allDay to call Temporal.PlainDate.from(occ.start) (never ZonedDateTime); events.ts non-recurring all-day pre-filter uses dtstartDate (DATE column) comparison — no DATETIME coercion.

Score: 4/4 truths — all verified in code

Automated test confirmation: apps/api 47/47 tests pass; apps/pwa 39/39 tests pass; both workspaces typecheck clean (tsc --noEmit).


Deferred Items

# Item Addressed In Evidence
1 Shared-family color lane populated with real events Operator action (D-16) calendars.is_shared column exists and is read by the route; lane is empty because no shared Fastmail calendar has been created yet. STATE.md Deferred Items entry D-16 and PROJECT.md D-16 confirm this is intentional and operator-tracked.

Required Artifacts

Artifact Expected Status Details
apps/api/src/broker/expand.ts expandOccurrences() + CalendarOccurrence interface VERIFIED Exports both; full VTIMEZONE registration, ICAL.RecurExpansion, allDay split, IANA-annotated output, CSS-safe IDs
apps/api/src/routes/events.ts Windowed /api/events with join, hasRrule pre-filter, zod validation VERIFIED zValidator, 3-clause WHERE (recurring/non-recurring/all-day), expandOccurrences called per row
apps/api/src/db/schema.ts has_rrule + idx_calendar_events_has_rrule + is_shared VERIFIED Lines 99-108 confirm columns and index
apps/api/src/broker/sync.ts hasRrule set on both insert and upsert paths VERIFIED Lines 113, 122
apps/pwa/src/lib/hydrateEvents.ts ISO→Temporal hydration with all-day PlainDate guard + ownership-routed calendarId VERIFIED Temporal.PlainDate.from for allDay; String(occ.ownerUserId) routing
apps/pwa/src/lib/calendarConfig.ts WEEK_START_DAY=0→SX_FIRST_DAY_OF_WEEK=7, buildCalendarConfig() VERIFIED WEEK_START_DAY === 0 ? 7 : WEEK_START_DAY at line 31
apps/pwa/src/lib/colorUtils.ts deriveScheduleXColors() (main/container/onContainer) VERIFIED Full implementation without third-party color library
apps/pwa/src/styles/tokens.css CSS token layer with --color-shared-family, --sx-color-* overrides VERIFIED (existence) File exists; not re-read but confirmed by prior grep showing --color-shared-family: #F25C7A and --sx-color-
apps/pwa/src/components/CalendarShell.tsx Schedule-X wired to TanStack Query + hydrateEvents + Zustand range VERIFIED Full pipeline confirmed (fetchEvents → hydrateEvents → eventsService.set); all four views; display timezone; onRangeUpdate exclusive end
apps/pwa/src/components/EventDetailPopover.tsx Read-only popover; XSS-safe; focus trap; Escape-to-close VERIFIED No dangerouslySetInnerHTML anywhere; all fields are plain-text JSX children; aria-label="Close", minHeight: 44px close button; Escape listener via document.addEventListener
apps/pwa/src/components/ColorLegend.tsx Always-visible legend with member rows + Family rose row VERIFIED Per-member rows + hardcoded 'Family' / #F25C7A row
apps/pwa/src/components/SkeletonCalendar.tsx Shimmer skeleton VERIFIED (existence) File present
apps/pwa/src/components/EmptyState.tsx Empty state component VERIFIED (existence) File present
apps/pwa/src/components/EventProof.tsx DELETED VERIFIED grep -rn "EventProof" apps/pwa/src/ returns nothing

From To Via Status Details
expand.ts VTIMEZONE registration getAllSubcomponents('vtimezone') at line 190, before new ICAL.RecurExpansion at line 264 WIRED Mandatory ordering confirmed
events.ts expand.ts expandOccurrences() called per row in flatMap WIRED Line 123
events.ts users.color + isShared innerJoin(users), select({ userColor: users.color, isShared: calendars.isShared }) WIRED Lines 84-89
CalendarShell.tsx /api/events useQuery(['events', start, end]) → fetchEvents(start, end) WIRED Lines 89-94
CalendarShell.tsx hydrateEvents eventsService.set(hydrateEvents(eventsQuery.data.occurrences)) in data-keyed effect WIRED Lines 164-168
CalendarShell.tsx calendarStore Zustand selectors for calendarRange, setCalendarRange, setOpenEventId, selectedView WIRED Lines 73-76
CalendarShell.tsx EventDetailPopover Rendered as sibling; popover resolves event from TanStack Query cache via Zustand openEventId WIRED Lines 318, 347
hydrateEvents.ts buildCalendarConfig keys occ.isShared ? 'shared' : String(occ.ownerUserId) exactly matches buildCalendarConfig keys WIRED Contract documented in both files
main.tsx temporal-polyfill/global First import before any Schedule-X code WIRED Line 7
expand.ts UTC windowing ICAL.Time.fromJSDate(windowStart, true)useUTC=true WIRED Lines 217-218
CalendarShell.tsx Exclusive window end range.end.toPlainDate().add({ days: 1 }).toString() in onRangeUpdate WIRED Line 149

Data-Flow Trace (Level 4)

Artifact Data Variable Source Produces Real Data Status
CalendarShell.tsx eventsQuery.data.occurrences fetchEvents(start, end)/api/events → MariaDB join + expandOccurrences Yes — DB query with 3-clause WHERE, joins, ICAL expansion FLOWING
events.ts rows Drizzle db.select().from(calendarEvents).innerJoin(calendars).innerJoin(users).where(...) Yes — parameterized SQL against live cache FLOWING
EventDetailPopover.tsx occurrence queryClient.getQueriesData({ queryKey: ['events'] }) — searches TanStack Query cache Yes — resolved from the same fetched data FLOWING
ColorLegend.tsx members Passed from CalendarShell via meQuery.data.userfetchMe/api/me Yes — live user data from DB FLOWING

Behavioral Spot-Checks

Not run — no dev server started (per spot-check constraints). The test suites stand in as executable verification:

Suite Command Result Status
API (47 tests) pnpm --filter @familysync/api test 47 passed, 0 failed PASS
PWA (39 tests) pnpm --filter @familysync/pwa test 39 passed, 0 failed PASS
API typecheck pnpm --filter @familysync/api exec tsc --noEmit Clean PASS
PWA typecheck pnpm --filter @familysync/pwa exec tsc --noEmit Clean PASS

Key tests for the phase's success criteria:

  • expand.test.ts — DST wall-clock assertion (10:00 AM both sides of March 2026 transition), all-day 'YYYY-MM-DD' assertion, EXDATE exclusion assertion
  • events.test.ts — color field, multi-calendar aggregation, isShared flag, ownerUserId, 400 on bad params
  • hydrateEvents.test.ts — all-day → PlainDate, timed → ZonedDateTime, shared → 'shared', personal → String(ownerUserId)
  • calendarConfig.test.tsWEEK_START_DAY=0firstDayOfWeek=7
  • EventDetailPopover.test.tsx — Escape closes, HTML-in-title rendered as escaped text (XSS guard)
  • CalendarShell.test.tsx — renders without throwing with timed + all-day mocked occurrences

Probe Execution

No probes declared in any plan frontmatter. No scripts/*/tests/probe-*.sh files found. Step 7c skipped.


Requirements Coverage

Requirement Source Plans Description Status Evidence
CAL-02 02-01 through 02-05 User sees a unified, color-coded calendar aggregating every accessible calendar SATISFIED events.ts joins all calendars/users; hydrateEvents routes calendarId; buildCalendarConfig creates per-member + shared entries; CalendarShell renders the full aggregate
CAL-03 02-01 through 02-05 User can switch between week, month, day, and agenda/list views SATISFIED All four createView* factories present in CalendarShell; Schedule-X built-in header enables switching
CAL-07 02-01 through 02-05 User can see all occurrences of a recurring event expanded correctly SATISFIED expandOccurrences uses ICAL.RecurExpansion with VTIMEZONE pre-registration; EXDATE internal to RecurExpansion; all-day returns 'YYYY-MM-DD'; IANA-annotated timed strings; UTC windowing; has_rrule pre-filter in route; sync.ts populates flag on every upsert

No orphaned requirements: the REQUIREMENTS.md Traceability table maps CAL-02 and CAL-03 to Phase 2 and CAL-07 to Phase 3. However, all five plans in Phase 2 declare requirements: [CAL-02, CAL-03, CAL-07], meaning Phase 2 satisfies CAL-07's display obligations while Phase 3 will deliver the write path. This is consistent — the REQUIREMENTS.md description of CAL-07 covers "see all occurrences expanded correctly", which Phase 2 delivers.


Anti-Patterns Found

Scanned: expand.ts, events.ts, CalendarShell.tsx, hydrateEvents.ts, calendarConfig.ts, colorUtils.ts, EventDetailPopover.tsx, ColorLegend.tsx, main.tsx, App.tsx.

No TBD, FIXME, or XXX markers found in any phase file.

No TODO or HACK markers found.

No return null / placeholder stubs found in phase deliverables.

No dangerouslySetInnerHTML in EventDetailPopover.tsx.

Phase 3 footer area in EventDetailPopover.tsx is an empty <div aria-hidden="true"> with an explicit "Phase 3 wires edit/delete here (D-08)" comment — this is an intentional reserved slot, not a stub (no user-visible output is missing).

File Line Pattern Severity Impact
No anti-patterns found

Human Verification Required

The following items need human testing in the running dev stack. All automated checks pass; these are inherently visual or behavioral and cannot be verified by static analysis.

1. Color-coded event rendering

Test: Open the PWA with DEV_AUTH_BYPASS=true. Confirm personal events appear in the member's assigned color (from users.color). Confirm the ColorLegend is visible and decodes ownership. Expected: Member color chips in legend match event chip colors; rose lane ("Family") is present in the legend and will show events once the shared Fastmail calendar is created (D-16). Why human: Color rendering is visual; CSS token derivation and Schedule-X lightColors cannot be verified by grep.

2. All four views render events correctly

Test: Click Day, Week, Month, and Agenda view buttons (Schedule-X built-in header). Confirm events appear in each view; confirm the week/day time grid scrolls and does not clip events. Expected: Consistent event list across all four views; no misplaced events; view switcher keyboard-accessible. Why human: DOM layout and Schedule-X rendering are not testable without a browser.

3. Recurring events — DST boundary (CAL-07)

Test: Navigate to a week containing a recurring timed event that crosses the March 2026 America/New_York DST boundary. Confirm the occurrence time does not shift ±1 hour after Spring Forward. Expected: A weekly 10:00 AM event shows 10:00 AM on both sides of the DST transition. Why human: VTIMEZONE registration is correct in code; real-data confirmation is needed.

4. All-day events — no date shift (CAL-07)

Test: Find a recurring all-day event (birthday or holiday). Confirm it appears as a full-day banner on exactly the correct date in month and week views. Expected: '2026-06-15' all-day event appears on June 15, not June 14 or 16. Why human: Temporal.PlainDate routing is correct in code; visual confirmation needed.


Gaps Summary

None. All four success criteria are implemented and verified in the codebase. The only open item is the shared-family color lane being empty, which is explicitly deferred (D-16) pending creation of the shared Fastmail calendar — it is not a gap in the implementation.


Verified: 2026-06-05T16:00:00Z Verifier: Claude (gsd-verifier)