Mechanical reformat — no logic changes. 398 files changed, 19125 insertions(+), 16457 deletions(-). Prettier 3.8.4 with .prettierrc (singleQuote:true, semi:true, tabWidth:2, trailingComma:all, printWidth:100). Isolated per D-13-08 for reviewability.
13 KiB
13 KiB
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 | |||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 03-event-write-back-pwa-install | 06 | execute | 4 |
|
|
true |
|
|
Purpose: CAL-05 (edit) and CAL-06 (delete) become user-reachable, and every write (create from Plan 05 included) gets the non-blocking optimistic feedback the non-technical member depends on (D-05). No SSE — polling only (D-09).
Output: edit/delete footer, delete dialog, sync toast + polling, all per the UI Design Contract.
<execution_context> @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md </execution_context>
@.planning/phases/03-event-write-back-pwa-install/03-UI-SPEC.md @.planning/phases/03-event-write-back-pwa-install/03-PATTERNS.md @apps/pwa/src/components/EventDetailPopover.tsx @apps/pwa/src/api/client.ts @apps/pwa/src/store/calendarStore.ts Task 1: deleteEvent + fetchSyncStatus client calls; delete/sync Zustand keys apps/pwa/src/api/client.ts, apps/pwa/src/store/calendarStore.ts - apps/pwa/src/api/client.ts (existing + Plan 05 additions — fetch shape) - apps/pwa/src/store/calendarStore.ts (existing + Plan 05 form keys) - .planning/phases/03-event-write-back-pwa-install/03-RESEARCH.md (§Pattern 8 — sync-status response { uid, status, error? }) - .planning/phases/03-event-write-back-pwa-install/03-UI-SPEC.md (§State Management Contract — deleteDialogOpen/deleteDialogUid/lastSyncedUid; ['syncStatus', uid] key) Add `deleteEvent(uid): Promise` (DELETE `/api/events/${uid}`, credentials:'include', throw on !ok) and `fetchSyncStatus(uid): Promise<{ uid: string; status: 'pending'|'done'|'failed'|'dead'; error?: string }>` (GET `/api/events/sync-status?uid=`). Export the SyncStatus type. Extend the Zustand store with `deleteDialogOpen: boolean`, `deleteDialogUid: string|null`, `lastSyncedUid: string|null` plus setters `setDeleteDialog(open, uid?)` and `setLastSyncedUid(uid)`. Defaults closed/null. cd /home/luc/Projects/familysync && grep -q "fetchSyncStatus" apps/pwa/src/api/client.ts && grep -q "deleteDialogOpen" apps/pwa/src/store/calendarStore.ts && pnpm --filter @familysync/pwa exec tsc --noEmit && pnpm --filter @familysync/pwa test - `grep -Eq "deleteEvent|fetchSyncStatus" apps/pwa/src/api/client.ts`. - `grep -q "lastSyncedUid" apps/pwa/src/store/calendarStore.ts`. - PWA tsc --noEmit passes; existing tests green. deleteEvent/fetchSyncStatus and delete/sync Zustand keys exist and type-check. Task 2: SyncStateToast with polled sync-status (D-05/D-06/D-08/D-09) apps/pwa/src/components/SyncStateToast.tsx, apps/pwa/src/components/CalendarShell.tsx - .planning/phases/03-event-write-back-pwa-install/03-UI-SPEC.md (§SyncStateToast — states/icons/copy/colors/position/auto-dismiss; §Interaction Contract sync-state feedback; §Copywriting toast strings; role=status/alert) - .planning/phases/03-event-write-back-pwa-install/03-RESEARCH.md (§Pattern 8 / §Code Examples useSyncStatus — refetchInterval 3000 while pending) - apps/pwa/src/api/client.ts (fetchSyncStatus from Task 1) - apps/pwa/src/components/EventDetailPopover.tsx (token usage + lucide icon import pattern) Tests (SyncStateToast.test.tsx) with mocked fetchSyncStatus: - status 'pending' renders "Syncing…" + spinner, role="status". - status 'done' renders "Saved", auto-dismiss after 2s, and triggers queryClient.invalidateQueries(['events']). - status 'failed' (generic) renders "Didn't save. Try again." role="alert", persists with a dismiss button. - status 'failed' with a 412/conflict error renders the conflict copy and invalidates ['events']. - status 'dead' renders "Not saved. Check your connection.". - refetchInterval is active (3000) only while pending. Implement `SyncStateToast.tsx`: a `useQuery(['syncStatus', uid], fetchSyncStatus, { enabled: uid!==null, refetchInterval: d => d?.status==='pending' ? 3000 : false, staleTime:0 })` keyed on `lastSyncedUid` from Zustand. Render the toast per UI-SPEC states table (Loader2/Check/AlertCircle icons, exact copy, exact colors/tokens, bottom-of-screen position, auto-dismiss done after 2s, persistent failed/dead with an X dismiss that clears lastSyncedUid). On transition to 'done' OR a 412-conflict, call `queryClient.invalidateQueries({ queryKey: ['events'] })` (D-06/D-08). Use role="status" for pending/done and role="alert" for failed/dead. Mount `` in CalendarShell (always rendered; renders nothing when lastSyncedUid is null). Set `lastSyncedUid` from the EventForm create/edit mutations (Plan 05 stored the uid; wire it via setLastSyncedUid) and from the delete flow (Task 3). No SSE (D-09). cd /home/luc/Projects/familysync && pnpm --filter @familysync/pwa test -- SyncStateToast && grep -q "invalidateQueries" apps/pwa/src/components/SyncStateToast.tsx && pnpm --filter @familysync/pwa exec tsc --noEmit - SyncStateToast.test.tsx GREEN for all five states + conflict + refetchInterval-while-pending. - `grep -q "refetchInterval" apps/pwa/src/components/SyncStateToast.tsx`. - No SSE / EventSource reference in the toast: `grep -c "EventSource" apps/pwa/src/components/SyncStateToast.tsx` returns 0. SyncStateToast polls sync-status, renders all contract states, invalidates events on done/conflict, mounted in the shell. Task 3: EventDetailPopover Edit/Delete footer + DeleteConfirmationDialog apps/pwa/src/components/EventDetailPopover.tsx, apps/pwa/src/components/DeleteConfirmationDialog.tsx, apps/pwa/src/components/CalendarShell.tsx - apps/pwa/src/components/EventDetailPopover.tsx (lines ~380-388 reserved footer; button style ~235-251; Zustand+TanStack usage ~109-137) - .planning/phases/03-event-write-back-pwa-install/03-UI-SPEC.md (§EventDetailPopover extended footer; §DeleteConfirmationDialog layout/copy/colors; §Interaction Contract delete interaction 1-6; §Copywriting delete strings) - .planning/phases/03-event-write-back-pwa-install/03-PATTERNS.md (§EventDetailPopover.tsx — replace reserved footer, button style, design tokens, XSS guard) - apps/pwa/src/store/calendarStore.ts (setEventForm, setDeleteDialog, setLastSyncedUid) - apps/pwa/src/api/client.ts (deleteEvent) Tests: EventDetailPopover footer renders an "Edit" button (opens EventForm in edit mode with the event's uid, closes popover) and a "Delete" button (`--color-destructive`, opens DeleteConfirmationDialog). DeleteConfirmationDialog renders heading "Delete event?" + body, a Cancel that closes without deleting, and a "Delete" (red, 48px) that calls deleteEvent, sets lastSyncedUid, closes both surfaces. Escape closes the dialog without deleting; focus trapped. Replace the EventDetailPopover reserved footer (`aria-hidden` placeholder) with a flex space-between row: a left "Edit" ghost button (Edit2 icon, `--color-text-primary`, opens `setEventForm(true,'edit', occurrence.uid)` and closes the popover) and a right "Delete" ghost button (Trash2 icon, `--color-destructive`, calls `setDeleteDialog(true, occurrence.uid)`). Remove `aria-hidden`. Implement `DeleteConfirmationDialog.tsx` as a centered modal (max-width 320px, backdrop `--color-overlay`, focus trap, Escape-to-cancel) per UI-SPEC: heading "Delete event?", body "This will be removed from your Fastmail calendar.", Cancel (ghost) and Delete (filled `--color-destructive`, white label, Trash2, 48px). On Delete: call `deleteEvent(deleteDialogUid)` via a TanStack mutation, `setLastSyncedUid(uid)` so the toast tracks it, close the dialog and popover; on the calendar, optimistic removal is acceptable but server state wins on refetch (no silent loss). Mount `` in CalendarShell (rendered while deleteDialogOpen). All tokens/touch-targets/plain-text-children per the contract. cd /home/luc/Projects/familysync && pnpm --filter @familysync/pwa test && grep -q "deleteDialogOpen" apps/pwa/src/components/EventDetailPopover.tsx && grep -q "Delete event?" apps/pwa/src/components/DeleteConfirmationDialog.tsx && pnpm --filter @familysync/pwa exec tsc --noEmit - Footer Edit opens EventForm edit mode; Delete opens the confirmation dialog (tests GREEN). - DeleteConfirmationDialog requires explicit confirm; Cancel/Escape do not delete. - `grep -c 'aria-hidden="true"' apps/pwa/src/components/EventDetailPopover.tsx` does not count the old footer placeholder (it is replaced). - Full PWA suite green; tsc --noEmit passes. Edit/Delete reachable from the popover; two-tap delete confirmation fires DELETE and feeds the sync toast.<threat_model>
Trust Boundaries
| Boundary | Description |
|---|---|
| delete action → API | A destructive operation crosses to the write API |
STRIDE Threat Register
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|---|---|---|---|---|
| T-03-17 | Tampering | accidental/irreversible delete | mitigate | Mandatory two-tap DeleteConfirmationDialog; no inline single-tap delete; no "don't ask again" (UI-SPEC) |
| T-03-18 | Repudiation | silent data loss on failed delete sync | mitigate | failed/dead toast persists until dismissed; server-authoritative refetch restores the event; no silent loss (D-08) |
| T-03-19 | Information Disclosure | sync-status of another member surfaced in toast | mitigate | sync-status is member-scoped server-side (Plan 03 T-03-07); toast only queries the current member's uid |
</threat_model>
- `pnpm --filter @familysync/pwa test` green (SyncStateToast, DeleteConfirmationDialog, popover footer + existing). - `pnpm --filter @familysync/pwa exec tsc --noEmit` passes. - No SSE/EventSource in any Phase 3 sync-feedback component (D-09).<success_criteria>
- CAL-05 edit and CAL-06 delete are user-reachable from the popover.
- Every write surfaces non-blocking polled sync feedback; 412 conflict shows the warning + re-fetch (D-08); done invalidates events (D-06). </success_criteria>