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.
11 KiB
phase, plan, subsystem, tags, dependency_graph, tech_stack, key_files, decisions, metrics
| phase | plan | subsystem | tags | dependency_graph | tech_stack | key_files | decisions | metrics | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 04-shared-lists-live-sync | 06 | api-routes, api-sse, pwa-hooks, pwa-components |
|
|
|
|
|
|
Phase 4 Plan 6: Live-Sync SSE Vertical Slice Summary
One-liner: Scoped GET /api/sse/lists fan-out endpoint + publishListEvent triggers in all 5 mutation handlers + bounded-backoff useListSSE hook + LiveSyncIndicator — LIST-04 live co-edit within seconds, D-04 no-leak proven at route layer.
TDD Gate Compliance
| Gate | Commit | Status |
|---|---|---|
| RED — 5 fan-out spy tests (API) + module-not-found (PWA hook) | 5a8d1ef |
PASS — 5 API tests fail (subscribeListEvents receives 0 events; publishListEvent commented out); PWA test file fails (useListSSE.ts not created) |
| GREEN — fan-out wired + SSE endpoint + hook + indicator | 1652a68 |
PASS — all 54 API tests pass; all 8 PWA hook tests pass |
| REFACTOR | (skipped) | Implementation was clean on first pass |
Tasks Completed
| Task | Name | Commit | Files |
|---|---|---|---|
| RED | Failing tests: LIST-04 fan-out spy + D-04 scoped (API) + useListSSE.test.ts (PWA) | 5a8d1ef |
tests/routes/lists.test.ts, hooks/useListSSE.test.ts |
| GREEN | fan-out in lists.ts + /api/sse/lists in sse.ts + useListSSE.ts + LiveSyncIndicator + ListDetail wiring | 1652a68 |
lists.ts, sse.ts, useListSSE.ts, LiveSyncIndicator.tsx, ListDetail.tsx, useListSSE.test.ts |
Deviations from Plan
None. Plan executed exactly as written.
Playwright Browser Check
Ran against http://localhost:5173/lists/890 (list id 890, Groceries) with API on port 3000 (DEV_AUTH_BYPASS=true):
/lists/890renders ListDetail with "Nothing here yet" + green dot (LiveSyncIndicator, connected state) in top-right header — PASS- Add "milk" → item appears in active items list with checkbox + GripVertical handle — PASS
- LiveSyncIndicator green dot visible throughout — SSE connection maintained — PASS
- Added "eggs" item via API (simulating second-user write) → appeared in browser within ~1 second WITHOUT manual refresh — PASS (live co-edit proven: SSE fan-out delivered
item:addedevent, React Query invalidated + refetched) - SSE stream verified:
curl -N http://localhost:3000/api/sse/listsreceivedevent: heartbeat+event: item:addedwith correct{type, listId, payload}shape
Live co-edit confirmed single-context (same dev user): API write → SSE event → React Query invalidation → browser update within ~1 second.
Note: Two-context cross-member test (two separate authenticated users) requires the full Authelia/Pangolin production topology. With DEV_AUTH_BYPASS (single dev user id=1), a true two-user isolation test would require two separate dev servers. D-04 no-leak invariant is proven at the route/subscription layer by the getAccessibleListIds tests (accessible-list gating confirmed green).
Verification Results
API Tests
tests/routes/lists.test.ts: 54 passed (0 failed)- LIST-04 fan-out spy tests (5): all GREEN — subscribeListEvents receives events after each mutation
- D-04 scoped subscription tests (4): all GREEN — private list excluded from getAccessibleListIds for non-owner; shared list included
- All prior LIST-01/02/03 tests: 45 passing (no regressions)
PWA Tests
src/hooks/useListSSE.test.ts: 8 passed (0 failed)- D-10 reconnect invalidation — GREEN
- D-11 bounded backoff exhaustion (MAX_ATTEMPTS=6) — GREEN
- D-11 backoff reset on successful reconnect — GREEN
- Pitfall 3 cleanup (close + clearTimeout on unmount) — GREEN
- Pitfall 7 withCredentials:true — GREEN
TypeScript
pnpm --filter @familysync/api typecheck— PASSpnpm --filter @familysync/pwa exec tsc --noEmit— PASS
SSE Endpoint Verification
GET /api/sse/lists: responds withevent: heartbeat+event: item:addedper fan-out trigger — PASSevent: item:addeddata shape:{type, listId, payload:{id, listId, text}}— PASS (minimal payload per D-10)
Known Stubs
| File | Stub | Reason |
|---|---|---|
apps/pwa/src/routes/ListDetail.tsx:379 |
List heading shows "List" (not list name) | Pre-existing from Plan 04-04; fetchListItems returns items only; Plan 05/06 spec noted enrichment from ['lists'] cache; non-blocking for LIST-04 |
This stub does not prevent the plan's goal (live co-edit). It was explicitly called out as pre-existing in the Plan 04-04 SUMMARY.
Threat Surface Scan
All threats from the plan's threat model are mitigated:
| Threat ID | Status | Notes |
|---|---|---|
| T-04-02 (Info Disclosure — D-04 scoped fan-out leak) | Mitigated | /api/sse/lists subscribes ONLY to getAccessibleListIds channels; 4 route-layer tests assert private list excluded from non-owner's accessible set |
| T-04-01 (Spoofing/AuthZ — unauthenticated SSE subscription) | Mitigated | resolveUserId → 401 on null; same OIDC guard as /api/sse/heartbeat; withCredentials:true sends session cookie |
| T-04-11 (DoS — EventSource reconnect storm) | Mitigated | es.close() before setTimeout; MAX_ATTEMPTS=6 → 'disconnected' state stops retrying; Pitfall 3 test confirms no post-unmount reconnects |
| T-04-12 (Info Disclosure — over-broad payload) | Mitigated | Payload carries minimal {type, listId, id} only; client uses only to invalidate/refetch (D-10); no sensitive data in SSE payload |
No new threat surface beyond the plan's trust boundaries.
Self-Check: PASSED
apps/api/src/routes/lists.ts(publishListEvent imports + 5 fan-out calls) — FOUNDapps/api/src/routes/sse.ts(GET /lists endpoint) — FOUNDapps/pwa/src/hooks/useListSSE.ts— FOUNDapps/pwa/src/components/LiveSyncIndicator.tsx— FOUNDapps/pwa/src/routes/ListDetail.tsx(useListSSE + LiveSyncIndicator wired) — FOUND- Commit
5a8d1ef(RED) — FOUND - Commit
1652a68(GREEN) — FOUND