Files
familysync/.planning/phases/03-event-write-back-pwa-install/03-06-PLAN.md
T

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
03-05
03-03
apps/pwa/src/api/client.ts
apps/pwa/src/store/calendarStore.ts
apps/pwa/src/components/EventDetailPopover.tsx
apps/pwa/src/components/DeleteConfirmationDialog.tsx
apps/pwa/src/components/SyncStateToast.tsx
apps/pwa/src/components/CalendarShell.tsx
true
CAL-05
CAL-06
truths artifacts key_links
The EventDetailPopover footer shows Edit and Delete actions (D-10)
Tapping Edit opens EventForm pre-populated; tapping Delete opens a two-tap confirmation dialog
Confirming delete calls DELETE /api/events/:uid and shows the sync toast
After any write the SyncStateToast polls /api/events/sync-status and shows Syncing/Saved/Didn't save; on done it invalidates the events query (D-06/D-09)
A 412 conflict shows the conflict copy and re-fetches the calendar (D-08)
path provides min_lines
apps/pwa/src/components/SyncStateToast.tsx polled sync-state feedback toast (D-05/D-09) 40
path provides
apps/pwa/src/components/DeleteConfirmationDialog.tsx two-tap destructive delete confirmation
from to via pattern
apps/pwa/src/components/SyncStateToast.tsx /api/events/sync-status useQuery refetchInterval while pending syncStatus|sync-status
from to via pattern
apps/pwa/src/components/EventDetailPopover.tsx DeleteConfirmationDialog Delete footer button opens deleteDialog deleteDialogOpen
Complete the edit/delete vertical slices and the write-feedback loop: wire the EventDetailPopover reserved footer to Edit/Delete actions (D-10), add the two-tap `DeleteConfirmationDialog`, and add the `SyncStateToast` that polls `/api/events/sync-status` (D-09) to surface Syncing → Saved / Didn't save, invalidating the events cache on confirm (D-06) and showing the conflict copy on 412 (D-08).

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>
Create `.planning/phases/03-event-write-back-pwa-install/03-06-SUMMARY.md` when done.