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

183 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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"
- "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)"
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 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"
</read_first>
<action>
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.
</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
- 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, 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>