183 lines
12 KiB
Markdown
183 lines
12 KiB
Markdown
---
|
||
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 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 — 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>
|