Files
familysync/.planning/phases/02-calendar-display/02-04-PLAN.md
T

12 KiB
Raw Blame History

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, user_setup, must_haves
phase plan type wave depends_on files_modified autonomous requirements user_setup must_haves
02-calendar-display 04 execute 3
02-02
02-03
apps/pwa/src/components/CalendarShell.tsx
apps/pwa/src/App.tsx
apps/pwa/src/components/CalendarShell.test.tsx
true
CAL-02
CAL-03
CAL-07
truths artifacts key_links
Opening the app renders a Schedule-X calendar populated with REAL windowed Fastmail occurrences fetched from /api/events
Events render in their owner's member color; shared-family events render in the reserved rose color
The user can switch between day, week, month, and agenda views and events render in each
Recurring events show all in-window occurrences; all-day events appear as full-day banners on the correct date with no shift
The visible window drives the TanStack Query key; navigating to a new window refetches
Default view is agenda on phone and month on tablet/desktop (D-05)
path provides min_lines
apps/pwa/src/components/CalendarShell.tsx Schedule-X calendar wired to TanStack Query + hydrateEvents + Zustand range 60
path provides contains
apps/pwa/src/App.tsx renders CalendarShell as the app root (replaces EventProof landing) CalendarShell
from to via pattern
apps/pwa/src/components/CalendarShell.tsx /api/events useQuery(['events',start,end]) → fetchEvents fetchEvents
from to via pattern
apps/pwa/src/components/CalendarShell.tsx Schedule-X eventsService eventsService.set(hydrateEvents(data.occurrences)) hydrateEvents
from to via pattern
apps/pwa/src/components/CalendarShell.tsx apps/pwa/src/store/calendarStore.ts calendarRange drives query key; onRangeUpdate updates it useCalendarStore
Deliver the phase's first true end-to-end user-facing slice: mount Schedule-X in a CalendarShell, fetch the visible window from /api/events via TanStack Query, hydrate the occurrences to Temporal events, feed them to Schedule-X's events service, and render the unified color-coded calendar with all four views switchable. After this plan a household member can open the app and SEE their real Fastmail calendar — color-coded, recurring + all-day correct — across day/week/month/agenda.

Purpose: This is where CAL-02, CAL-03, and the CAL-07 display path become observable to the user. It consumes the Plan 02 endpoint and the Plan 03 token layer / config / hydration / store. Output: CalendarShell.tsx wired to the full data pipeline; App.tsx renders it; a render smoke test.

<execution_context> @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md </execution_context>

@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/02-calendar-display/02-UI-SPEC.md @.planning/phases/02-calendar-display/02-RESEARCH.md @.planning/phases/02-calendar-display/02-PATTERNS.md @.planning/phases/02-calendar-display/02-03-SUMMARY.md @.planning/phases/02-calendar-display/02-02-SUMMARY.md Task 1: CalendarShell — Schedule-X mounted + wired to TanStack Query + hydrate + Zustand range apps/pwa/src/components/CalendarShell.tsx, apps/pwa/src/App.tsx - apps/pwa/src/App.tsx (current root: MemberBadge + EventProof + meQuery useQuery pattern lines 5892; this becomes the CalendarShell host) - apps/pwa/src/api/client.ts (fetchEvents(start,end), fetchMe — from Plan 03) - apps/pwa/src/lib/hydrateEvents.ts (hydrateEvents signature — Plan 03) - apps/pwa/src/lib/calendarConfig.ts (buildCalendarConfig, SX_FIRST_DAY_OF_WEEK, view factories — Plan 03) - apps/pwa/src/store/calendarStore.ts (useCalendarStore: calendarRange, selectedView, setCalendarRange, setOpenEventId — Plan 03) - .planning/phases/02-calendar-display/02-RESEARCH.md §"Pattern 4: TanStack Query + onRangeUpdate Wiring" + §"Pitfall 4" + §"A4 note" (do not depend on onRangeUpdate for first fetch) - .planning/phases/02-calendar-display/02-UI-SPEC.md §"Component Inventory: CalendarShell" + §"View default logic (D-05)" + §"View Layout Specification" - .planning/phases/02-calendar-display/02-PATTERNS.md §"apps/pwa/src/components/CalendarShell.tsx" Create `apps/pwa/src/components/CalendarShell.tsx`. Read the current user via `useQuery(['me'], fetchMe)` to source member colors; build the Schedule-X calendars config with `buildCalendarConfig(members)` where members come from /api/me (current user) — for Phase 2 the per-calendar color comes from users.color via the API occurrences, so the calendars config keys must match the `calendarId` (String(userId) or 'shared') that hydrateEvents stamps on each event.
Create the events service and event-modal plugins ONCE via `useState(() => createEventsServicePlugin())[0]` / `useState(() => createEventModalPlugin())[0]` (stable across renders). Build the app with `useCalendarApp({ views: [createViewDay(), createViewWeek(), createViewMonthGrid(), createViewMonthAgenda()], defaultView: <month-agenda on phone, month-grid on tablet/desktop per D-05>, firstDayOfWeek: SX_FIRST_DAY_OF_WEEK, calendars, plugins: [eventsService, eventModal], onRangeUpdate(range){ setCalendarRange({start, end}) } })`.

Fetch events with `useQuery({ queryKey: ['events', calendarRange.start, calendarRange.end], queryFn: () => fetchEvents(calendarRange.start, calendarRange.end), retry: 2, staleTime: 5*60*1000 })`. The initial calendarRange comes from the Zustand default (current month ± 1 week) — do NOT rely on onRangeUpdate firing on mount (A4 / Open Q2). In a `useEffect` keyed on `eventsQuery.data`, call `eventsService.set(hydrateEvents(eventsQuery.data.occurrences))` (Pitfall 4: must hydrate to Temporal before set). Wire event click to `setOpenEventId` (popover is built in Plan 05; here just store the id — keep the customComponents.eventModal slot reserved for Plan 05).

Render `<ScheduleXCalendar calendarApp={calendar} />` filling the available space. Use token-based styling only (className/var(--token)) — no hard-coded hex/px (Phase 2 rule). The AppNav/ViewToolbar/ColorLegend/popover chrome is Plan 05; CalendarShell here may render a minimal toolbar placeholder or rely on Schedule-X's built-in controls so the four views are switchable and verifiable now.

Update `apps/pwa/src/App.tsx`: replace the EventProof landing content with `<CalendarShell />` as the app root. Migrate any remaining hard-coded hex/px in App.tsx to tokens (Phase 2 rule). Leave the meQuery sign-in-required error branch intact for unauthenticated state.
cd apps/pwa && grep -q "ScheduleXCalendar" src/components/CalendarShell.tsx && grep -q "hydrateEvents" src/components/CalendarShell.tsx && grep -q "queryKey: \['events'" src/components/CalendarShell.tsx && grep -q "CalendarShell" src/App.tsx && echo SHELL_WIRED cd apps/pwa && grep -q "createViewDay" src/components/CalendarShell.tsx && grep -q "createViewWeek" src/components/CalendarShell.tsx && grep -q "createViewMonthGrid" src/components/CalendarShell.tsx && grep -q "createViewMonthAgenda" src/components/CalendarShell.tsx && echo ALL_FOUR_VIEWS cd apps/pwa && pnpm exec tsc --noEmit - CalendarShell.tsx mounts ScheduleXCalendar with all four view factories (day/week/month-grid/month-agenda) - CalendarShell uses useQuery(['events', start, end]) → fetchEvents and calls eventsService.set(hydrateEvents(...)) in a data-keyed effect - defaultView resolves to month-agenda on phone and month-grid on tablet/desktop (D-05) - firstDayOfWeek passed as SX_FIRST_DAY_OF_WEEK (=7), not 0 - App.tsx renders CalendarShell as root; EventProof landing content removed from the render path - No hard-coded hex/px in CalendarShell.tsx (token vars only); tsc --noEmit clean Schedule-X renders real windowed Fastmail occurrences (color-coded, all four views switchable) as the app root; PWA typechecks. Task 2: CalendarShell render smoke test (CAL-03 — four views, real-data render path) apps/pwa/src/components/CalendarShell.test.tsx - apps/pwa/src/components/CalendarShell.tsx (Task 1 output — the component under test) - apps/pwa/vitest.config.ts (jsdom env — Plan 01) - apps/api → .planning/phases/02-calendar-display/02-RESEARCH.md §"Phase Requirements → Test Map" (CAL-03 smoke row) + §"Pitfall 6" (test the @schedule-x/react + @schedule-x/calendar integration in Wave 0) - .planning/phases/02-calendar-display/02-PATTERNS.md §"apps/pwa/vitest.config.ts" (jsdom test pattern) - CalendarShell renders without throwing when all four views are configured (CAL-03 smoke; validates @schedule-x/react@4.1.0 + @schedule-x/calendar@4.6.0 compatibility — Pitfall 6/A2) - Given a mocked fetchEvents returning one timed + one all-day occurrence, eventsService receives hydrated Temporal events (no ISO-string rejection — Pitfall 4) Create `apps/pwa/src/components/CalendarShell.test.tsx` using @testing-library/react under jsdom. Import `'temporal-polyfill/global'` at top. Mock `../api/client` so `fetchMe` returns a member and `fetchEvents` returns `{ occurrences: [, ] }`. Wrap render in a QueryClientProvider with retry:false. Assert the component renders without throwing (the CAL-03 smoke from the test map) and that the Schedule-X root mounts. If asserting on eventsService internals is impractical, assert that hydrateEvents is invoked with the mocked occurrences (spy) and that no error is thrown for the all-day PlainDate event — this guards Pitfall 4 (Temporal hydration) and Pitfall 6 (adapter/core version compatibility) per the Wave 0 mandate. cd apps/pwa && pnpm test -- src/components/CalendarShell.test.tsx - CalendarShell.test.tsx renders the component under jsdom without throwing with all four views configured - The test exercises both a timed and an all-day occurrence through the hydrate→eventsService path - Test passes, confirming @schedule-x/react@4.1.0 ↔ @schedule-x/calendar@4.6.0 compatibility (A2/Pitfall 6 resolved) CAL-03 render smoke test green; Schedule-X integration and Temporal hydration path validated under test.

<threat_model>

Trust Boundaries

Boundary Description
/api/events JSON → calendar render server occurrences rendered into the DOM via React

STRIDE Threat Register

Threat ID Category Component Disposition Mitigation Plan
T-02d-01 Tampering (XSS) event title/location/description in render mitigate React JSX default escaping; never dangerouslySetInnerHTML for event fields (carried into Plan 05 popover)
T-02d-02 Information disclosure events from another member's account accept API already enforces auth + per-credential scoping (Plan 02 / CAL-08-DECISION); client renders only what the authed endpoint returns
</threat_model>
- `pnpm --filter @familysync/pwa test` green (CalendarShell smoke) - `tsc --noEmit` clean in apps/pwa - Manual (dev-auth bypass): app shows real color-coded events; all four views switch and render

<success_criteria>

  • Real Fastmail occurrences render color-coded across day/week/month/agenda (CAL-02, CAL-03)
  • Recurring + all-day occurrences render correctly in-window (CAL-07 display)
  • Window navigation refetches via TanStack Query </success_criteria>

<artifacts_produced>

Artifacts this phase produces (Plan 04)

  • CalendarShell (React component) — apps/pwa/src/components/CalendarShell.tsx
  • App.tsx now renders CalendarShell as root (EventProof landing removed from render path)
  • CalendarShell.test.tsx (CAL-03 render smoke)
  • Schedule-X eventsService + eventModal plugin instances + useCalendarApp config in CalendarShell </artifacts_produced>
Create `.planning/phases/02-calendar-display/02-04-SUMMARY.md` when done