---
phase: 02-calendar-display
plan: 04
type: execute
wave: 3
depends_on: ['02-02', '02-03']
files_modified:
  - apps/pwa/src/components/CalendarShell.tsx
  - apps/pwa/src/App.tsx
  - apps/pwa/src/components/CalendarShell.test.tsx
autonomous: true
requirements: [CAL-02, CAL-03, CAL-07]
user_setup: []

must_haves:
  truths:
    - '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 (D-06)"
    - 'The user can switch between day, week, month, and agenda views and events render in each (D-04)'
    - '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)'
  artifacts:
    - path: 'apps/pwa/src/components/CalendarShell.tsx'
      provides: 'Schedule-X calendar wired to TanStack Query + hydrateEvents + Zustand range'
      min_lines: 60
    - path: 'apps/pwa/src/App.tsx'
      provides: 'renders CalendarShell as the app root (replaces EventProof landing)'
      contains: 'CalendarShell'
  key_links:
    - from: 'apps/pwa/src/components/CalendarShell.tsx'
      to: '/api/events'
      via: "useQuery(['events',start,end]) → fetchEvents"
      pattern: 'fetchEvents'
    - from: 'apps/pwa/src/components/CalendarShell.tsx'
      to: 'Schedule-X eventsService'
      via: 'eventsService.set(hydrateEvents(data.occurrences))'
      pattern: 'hydrateEvents'
    - from: 'apps/pwa/src/components/CalendarShell.tsx'
      to: 'apps/pwa/src/store/calendarStore.ts'
      via: 'calendarRange drives query key; onRangeUpdate updates it'
      pattern: 'useCalendarStore'
---

<objective>
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.
</objective>

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

<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
</context>

<tasks>

<task type="auto">
  <name>Task 1: CalendarShell — Schedule-X mounted + wired to TanStack Query + hydrate + Zustand range</name>
  <files>apps/pwa/src/components/CalendarShell.tsx, apps/pwa/src/App.tsx</files>
  <read_first>
    - apps/pwa/src/App.tsx (current root: MemberBadge + EventProof + meQuery useQuery pattern lines 58–92; 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 + its calendarId routing: 'shared' | String(ownerUserId) — Plan 03)
    - apps/pwa/src/lib/calendarConfig.ts (buildCalendarConfig keyed by String(userId) + 'shared', 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"
  </read_first>
  <action>
    Create `apps/pwa/src/components/CalendarShell.tsx`. Read the current user via `useQuery(['me'], fetchMe)` to source member colors, then build the Schedule-X calendars config with `buildCalendarConfig(members)` where members come from /api/me (current user). CRITICAL — the calendars config keys are `String(userId)` for each member plus the reserved `'shared'` entry; these MUST match the `calendarId` that hydrateEvents stamps on each event, which is `occ.isShared ? 'shared' : String(occ.ownerUserId)` (Plan 03 routing fix). Do NOT key the config by the DB calendar-row id (`occ.calendarId`) — events from a member who owns multiple calendars would then render with no color.

    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.

  </action>
  <verify>
    <automated>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</automated>
    <automated>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</automated>
    <automated>cd apps/pwa && pnpm exec tsc --noEmit</automated>
  </verify>
  <acceptance_criteria>
    - 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
    - The calendars config is keyed by String(userId) + 'shared' (matching hydrateEvents' calendarId routing), NOT by the DB calendar-row id
    - 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
  </acceptance_criteria>
  <done>Schedule-X renders real windowed Fastmail occurrences (color-coded via userId/shared-keyed config, all four views switchable) as the app root; PWA typechecks.</done>
</task>

<task type="auto" tdd="true">
  <name>Task 2: CalendarShell render smoke test (CAL-03 — four views, real-data render path)</name>
  <files>apps/pwa/src/components/CalendarShell.test.tsx</files>
  <read_first>
    - 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)
  </read_first>
  <behavior>
    - 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)
  </behavior>
  <action>
    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: [<one timed>, <one all-day>] }`. 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.
  </action>
  <verify>
    <automated>cd apps/pwa && pnpm test -- src/components/CalendarShell.test.tsx</automated>
  </verify>
  <acceptance_criteria>
    - 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)
  </acceptance_criteria>
  <done>CAL-03 render smoke test green; Schedule-X integration and Temporal hydration path validated under test.</done>
</task>

</tasks>

<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>

<verification>
- `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
</verification>

<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>

<output>
Create `.planning/phases/02-calendar-display/02-04-SUMMARY.md` when done
</output>
