---
phase: 03-event-write-back-pwa-install
plan: 06
type: execute
wave: 4
depends_on: ['03-05', '03-03']
files_modified:
  - 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
autonomous: true
requirements: [CAL-05, CAL-06]
user_setup: []

must_haves:
  truths:
    - '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)'
  artifacts:
    - path: 'apps/pwa/src/components/SyncStateToast.tsx'
      provides: 'polled sync-state feedback toast (D-05/D-09)'
      min_lines: 40
    - path: 'apps/pwa/src/components/DeleteConfirmationDialog.tsx'
      provides: 'two-tap destructive delete confirmation'
  key_links:
    - from: 'apps/pwa/src/components/SyncStateToast.tsx'
      to: '/api/events/sync-status'
      via: 'useQuery refetchInterval while pending'
      pattern: 'syncStatus|sync-status'
    - from: 'apps/pwa/src/components/EventDetailPopover.tsx'
      to: 'DeleteConfirmationDialog'
      via: 'Delete footer button opens deleteDialog'
      pattern: 'deleteDialogOpen'
---

<objective>
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.
</objective>

<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
</execution_context>

<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
</context>

<tasks>

<task type="auto" tdd="true">
  <name>Task 1: deleteEvent + fetchSyncStatus client calls; delete/sync Zustand keys</name>
  <files>apps/pwa/src/api/client.ts, apps/pwa/src/store/calendarStore.ts</files>
  <read_first>
    - 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)
  </read_first>
  <action>
    Add `deleteEvent(uid): Promise<void>` (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.
  </action>
  <verify>
    <automated>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</automated>
  </verify>
  <acceptance_criteria>
    - `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.
  </acceptance_criteria>
  <done>deleteEvent/fetchSyncStatus and delete/sync Zustand keys exist and type-check.</done>
</task>

<task type="auto" tdd="true">
  <name>Task 2: SyncStateToast with polled sync-status (D-05/D-06/D-08/D-09)</name>
  <files>apps/pwa/src/components/SyncStateToast.tsx, apps/pwa/src/components/CalendarShell.tsx</files>
  <read_first>
    - .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)
  </read_first>
  <behavior>
    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.
  </behavior>
  <action>
    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 `<SyncStateToast />` 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).
  </action>
  <verify>
    <automated>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</automated>
  </verify>
  <acceptance_criteria>
    - 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.
  </acceptance_criteria>
  <done>SyncStateToast polls sync-status, renders all contract states, invalidates events on done/conflict, mounted in the shell.</done>
</task>

<task type="auto" tdd="true">
  <name>Task 3: EventDetailPopover Edit/Delete footer + DeleteConfirmationDialog</name>
  <files>apps/pwa/src/components/EventDetailPopover.tsx, apps/pwa/src/components/DeleteConfirmationDialog.tsx, apps/pwa/src/components/CalendarShell.tsx</files>
  <read_first>
    - 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)
  </read_first>
  <behavior>
    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.
  </behavior>
  <action>
    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 `<DeleteConfirmationDialog />` in CalendarShell (rendered while deleteDialogOpen). All tokens/touch-targets/plain-text-children per the contract.
  </action>
  <verify>
    <automated>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</automated>
  </verify>
  <acceptance_criteria>
    - 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.
  </acceptance_criteria>
  <done>Edit/Delete reachable from the popover; two-tap delete confirmation fires DELETE and feeds the sync toast.</done>
</task>

</tasks>

<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>

<verification>
- `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).
</verification>

<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>

<output>
Create `.planning/phases/03-event-write-back-pwa-install/03-06-SUMMARY.md` when done.
</output>
