186 lines
13 KiB
Markdown
186 lines
13 KiB
Markdown
---
|
|
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>
|