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.
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 |
|
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 |
Key Link Verification
| 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.user → fetchMe → /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 assertionevents.test.ts— color field, multi-calendar aggregation,isSharedflag,ownerUserId, 400 on bad paramshydrateEvents.test.ts— all-day →PlainDate, timed →ZonedDateTime, shared →'shared', personal →String(ownerUserId)calendarConfig.test.ts—WEEK_START_DAY=0→firstDayOfWeek=7EventDetailPopover.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)