diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 9eb3278..1660d28 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -107,7 +107,7 @@ Plans: **Wave 3** *(blocked on Wave 2)* - [x] 03-04-PLAN.md — TDD: outbox worker state machine (D-05/06/07/08 retry/backoff/dead/conflict, edit-as-move ordering) + index.ts wiring -- [ ] 03-05-PLAN.md — Frontend create/edit slice: write client calls + Zustand keys + EventForm (D-01/02/11) + New Event FAB +- [x] 03-05-PLAN.md — Frontend create/edit slice: write client calls + Zustand keys + EventForm (D-01/02/11) + New Event FAB - [x] 03-07-PLAN.md — PWA install: VitePWA manifest + auth-safe SW denylist + icons + InstallPrompt (iOS walkthrough + Android prompt) (PWA-01/02) **Wave 4** *(blocked on Wave 3)* @@ -161,7 +161,7 @@ Note: Phase 4 depends only on Phase 1 and can begin as soon as Phase 1 is comple |-------|----------------|--------|-----------| | 1. Foundation + Broker Spike | 4/4 | Complete | 2026-06-04 | | 2. Calendar Display | 5/5 | Complete | 2026-06-05 | -| 3. Event Write-Back + PWA Install | 5/8 | In Progress| | +| 3. Event Write-Back + PWA Install | 6/8 | In Progress| | | 4. Shared Lists + Live Sync | 0/? | Not started | - | | 5. Web Push Notifications | 0/? | Not started | - | @@ -171,7 +171,7 @@ Note: Phase 4 depends only on Phase 1 and can begin as soon as Phase 1 is comple **Goal:** [Captured for future planning] Abstract the calendar backend behind a provider interface so Fastmail/CalDAV is one implementation among potentially many. Shipping with a single provider is fine, but the broker, sync, and event-expansion layers should be structured so additional providers (e.g. other CalDAV hosts, Google Calendar, generic ICS feeds) can be added without rework. Captures the "provider" seam as an explicit architectural concern. **Requirements:** TBD -**Plans:** 5/8 plans executed +**Plans:** 6/8 plans executed Plans: diff --git a/.planning/STATE.md b/.planning/STATE.md index cb6c46a..afd38f8 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -3,14 +3,14 @@ gsd_state_version: 1.0 milestone: v1.0 milestone_name: milestone status: executing -stopped_at: Completed 03-04-PLAN.md -last_updated: "2026-06-05T22:22:47.716Z" +stopped_at: Completed 03-05-PLAN.md +last_updated: "2026-06-05T22:32:25.900Z" last_activity: 2026-06-05 progress: total_phases: 6 completed_phases: 2 total_plans: 17 - completed_plans: 14 + completed_plans: 15 percent: 33 --- @@ -26,7 +26,7 @@ See: .planning/PROJECT.md (updated 2026-06-03) ## Current Position Phase: 03 (event-write-back-pwa-install) — EXECUTING -Plan: 6 of 8 +Plan: 7 of 8 Status: Ready to execute Last activity: 2026-06-05 @@ -57,6 +57,7 @@ Progress: [███████░░░] 65% | Phase 03 P03-03 | 7 | 3 tasks | 2 files | | Phase 03 P03-07 | 5 | 2 tasks | 7 files | | Phase 03 P03-04 | 15 | 2 tasks | 3 files | +| Phase 03 P03-05 | 6 | 3 tasks | 6 files | ## Accumulated Context @@ -81,6 +82,9 @@ Recent decisions affecting current work: - [Phase ?]: D-03-07-layout: InstallPrompt desktop layout wraps CalendarContent in flex-column div so banner appears above calendar grid - [Phase ?]: D-03-04-hoisting: vi.hoisted() required for vi.mock() factory variables when test file has static import of module under test (TDZ issue) - [Phase ?]: D-03-04-where: Drizzle types disallow chained .where().where(); use and() in single .where() call +- [Phase ?]: D-01 calendar default: last-used URL from localStorage (eventForm.lastCalendarUrl), first writable calendar as fallback +- [Phase ?]: D-02 calendar picker: hidden when writableCalendars.length === 1, shown when >1 — authoritative from GET /api/events/writable-calendars +- [Phase ?]: T-03-15 XSS: EventForm renders all values as plain-text JSX children; no dangerouslySetInnerHTML in code ### Pending Todos @@ -109,6 +113,6 @@ Recent decisions affecting current work: ## Session Continuity -Last session: 2026-06-05T22:22:47.710Z -Stopped at: Completed 03-04-PLAN.md +Last session: 2026-06-05T22:32:25.896Z +Stopped at: Completed 03-05-PLAN.md Resume file: None diff --git a/.planning/phases/03-event-write-back-pwa-install/03-05-SUMMARY.md b/.planning/phases/03-event-write-back-pwa-install/03-05-SUMMARY.md new file mode 100644 index 0000000..e089026 --- /dev/null +++ b/.planning/phases/03-event-write-back-pwa-install/03-05-SUMMARY.md @@ -0,0 +1,130 @@ +--- +phase: "03" +plan: "05" +subsystem: pwa-event-write-ui +tags: [react, tanstack-query, zustand, eventform, caldav-write, pwa] +dependency_graph: + requires: ["03-03"] + provides: ["EventForm component", "createEvent/updateEvent/fetchWritableCalendars client calls", "eventFormOpen/eventFormMode/eventFormUid Zustand keys"] + affects: ["CalendarShell", "EventDetailPopover (future edit trigger)"] +tech_stack: + added: [] + patterns: ["TanStack Query useMutation + useQuery", "Zustand UI-only state", "bottom-sheet/dialog responsive overlay", "vi.hoisted() for mock TDZ", "D-01 last-used calendar localStorage", "D-02 conditional calendar picker", "D-11 whole-series recurrence presets", "T-03-15 plain-text JSX XSS guard"] +key_files: + created: + - apps/pwa/src/components/EventForm.tsx + - apps/pwa/src/api/client.test.ts + - apps/pwa/src/components/EventForm.test.tsx + modified: + - apps/pwa/src/api/client.ts + - apps/pwa/src/store/calendarStore.ts + - apps/pwa/src/components/CalendarShell.tsx +decisions: + - "D-01 calendar default: last-used URL from localStorage (eventForm.lastCalendarUrl), first writable calendar as fallback" + - "D-02 calendar picker: hidden when writableCalendars.length === 1, shown when >1 — driven by GET /api/events/writable-calendars" + - "T-03-15 XSS: all field values as plain-text JSX children in EventForm; no dangerouslySetInnerHTML in code" + - "vi.hoisted() required for mock factory variables to avoid TDZ (D-03-04-hoisting pattern)" + - "eventFormOpen selector added to CalendarShell per-field selector pattern (Bug B guard preserved)" +metrics: + duration_minutes: 6 + completed_date: "2026-06-05" + tasks_completed: 3 + files_created: 3 + files_modified: 3 +--- + +# Phase 03 Plan 05: Event Write UI (EventForm + Client Calls) Summary + +**One-liner:** EventForm modal with timed/all-day/recurring fields, conditional calendar picker (D-02), and typed write client (createEvent/updateEvent/fetchWritableCalendars) wired to the Plan 03 write API via TanStack Query mutations. + +## What Was Built + +### Task 1: Typed write client calls + Zustand form-state keys + +Extended `apps/pwa/src/api/client.ts` with: +- `CreateEventPayload` interface (title, allDay, start, end, recurrence, optional location/description/calendarUrl) +- `CreateEventResponse` interface ({ uid }) +- `WritableCalendar` interface ({ url, displayName, color, isShared }) — D-03 server-authoritative shape +- `RecurrencePreset` type ('none'|'daily'|'weekly'|'monthly'|'yearly') +- `createEvent(payload)` — POST /api/events/create, credentials:include, returns {uid} +- `updateEvent(uid, payload)` — PATCH /api/events/:uid/edit +- `fetchWritableCalendars()` — GET /api/events/writable-calendars, parses `{ calendars }` envelope, returns `WritableCalendar[]` + +Extended `apps/pwa/src/store/calendarStore.ts` with: +- `eventFormOpen: boolean` (default: false) +- `eventFormMode: 'create' | 'edit'` (default: 'create') +- `eventFormUid: string | null` (default: null) +- `setEventForm(open, mode?, uid?)` setter — no server data in Zustand + +### Task 2: EventForm modal + +New `apps/pwa/src/components/EventForm.tsx` (715 lines): +- Bottom sheet on phone (≤767px), centered 480px dialog on tablet/desktop — reuses EventDetailPopover pattern +- Fields per UI-SPEC §EventForm order: title, all-day toggle, start date/time, end date/time, calendar picker (conditional), recurrence, location, description +- All-day toggle (`role="switch"`, aria-checked): hides time inputs when on, restores 09:00/10:00 defaults when off +- Recurrence: `