From 8ed105d46710e05bd6b383990a515d1cea394a4d Mon Sep 17 00:00:00 2001 From: Lucas Berger Date: Tue, 9 Jun 2026 13:04:48 -0400 Subject: [PATCH] docs(04-04): complete item-CRUD vertical slice plan --- .planning/ROADMAP.md | 4 +- .planning/STATE.md | 20 ++- .../04-04-SUMMARY.md | 164 ++++++++++++++++++ 3 files changed, 178 insertions(+), 10 deletions(-) create mode 100644 .planning/phases/04-shared-lists-live-sync/04-04-SUMMARY.md diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 28af17f..c91272b 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -156,7 +156,7 @@ Plans: **Wave 3** *(blocked on Wave 2)* -- [ ] 04-04-PLAN.md — Item CRUD + checked-sink slice: item endpoints + fractional rank + per-field LWW PATCH + ListDetail/ItemRow/AddItemInput + optimistic UI (LIST-02, D-05/D-07/D-08/D-09) +- [x] 04-04-PLAN.md — Item CRUD + checked-sink slice: item endpoints + fractional rank + per-field LWW PATCH + ListDetail/ItemRow/AddItemInput + optimistic UI (LIST-02, D-05/D-07/D-08/D-09) **Wave 4** *(blocked on Wave 3)* @@ -213,7 +213,7 @@ Note: Phase 4 depends only on Phase 1 and can begin as soon as Phase 1 is comple | 1. Foundation + Broker Spike | 4/4 | Complete | 2026-06-04 | | 2. Calendar Display | 5/5 | Complete | 2026-06-05 | | 3. Event Write-Back + PWA Install | 12/12 | Complete | 2026-06-07 | -| 4. Shared Lists + Live Sync | 3/6 | In Progress| | +| 4. Shared Lists + Live Sync | 4/6 | In Progress| | | 5. Web Push Notifications | 0/? | Not started | - | | 6. UX Polish | 0/? | Not started | - | diff --git a/.planning/STATE.md b/.planning/STATE.md index fd72e51..f2a9068 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -4,13 +4,13 @@ milestone: v1.0 milestone_name: milestone status: executing stopped_at: Completed 04-03-PLAN.md -last_updated: "2026-06-09T16:47:43.764Z" -last_activity: 2026-06-09 -- Phase 04 execution started +last_updated: "2026-06-09T17:04:33.907Z" +last_activity: 2026-06-09 progress: total_phases: 15 completed_phases: 3 total_plans: 27 - completed_plans: 24 + completed_plans: 25 percent: 20 --- @@ -26,11 +26,11 @@ See: .planning/PROJECT.md (updated 2026-06-07) ## Current Position Phase: 04 (shared-lists-live-sync) — EXECUTING -Plan: 3 of 6 +Plan: 4 of 6 Status: Ready to execute -Last activity: 2026-06-09 -- Phase 04 execution started +Last activity: 2026-06-09 -Progress: [█████░░░░░] 50% (3 of 6 phases) +Progress: [█████████░] 93% ## Performance Metrics @@ -61,6 +61,7 @@ Progress: [█████░░░░░] 50% (3 of 6 phases) | Phase 03 P03-05 | 6 | 3 tasks | 6 files | | Phase 04 P01 | 65 | 4 tasks | 17 files | | Phase 04 P03 | 12 | 2 tasks | 9 files | +| Phase 04 P04 | 11 | 2 tasks | 10 files | ## Accumulated Context @@ -90,6 +91,9 @@ Recent decisions affecting current work: - [Phase ?]: T-03-15 XSS: EventForm renders all values as plain-text JSX children; no dangerouslySetInnerHTML in code - [Phase ?]: Phase 4 Plan 1 - [Phase ?]: D-04 GET scoped: two-select + Set union (owner + list_shares); ListDeleteDialog props-driven to preserve calendarStore dialog; zValidator returns 400 not 422 per existing convention +- [Phase 04-04]: listItemsRouter separate from listsRouter, mounted at /api/list-items for PATCH/DELETE item routes per RESEARCH architecture diagram +- [Phase 04-04]: Uncheck rank recomputed to active-bottom (generateKeyBetween(lastActiveRank, null)) in same DB write (Open Question 2 resolved) +- [Phase 04-04]: Delete-wins no-rollback: deleteMutation has no onError handler; item removal from cache is final (D-09) ### Roadmap Evolution @@ -130,6 +134,6 @@ Recent decisions affecting current work: ## Session Continuity -Last session: 2026-06-09T16:47:43.755Z +Last session: 2026-06-09T17:04:33.874Z Stopped at: Completed 04-03-PLAN.md -Resume file: .planning/phases/04-shared-lists-live-sync/04-UI-SPEC.md +Resume file: None diff --git a/.planning/phases/04-shared-lists-live-sync/04-04-SUMMARY.md b/.planning/phases/04-shared-lists-live-sync/04-04-SUMMARY.md new file mode 100644 index 0000000..22c910d --- /dev/null +++ b/.planning/phases/04-shared-lists-live-sync/04-04-SUMMARY.md @@ -0,0 +1,164 @@ +--- +phase: 04-shared-lists-live-sync +plan: "04" +subsystem: api-routes, pwa-components +tags: [item-crud, fractional-rank, optimistic-ui, D-05, D-06, D-07, D-08, D-09, D-13, tdd, list-02] +dependency_graph: + requires: + - 04-01 (list_items schema, BrowserRouter, react-router) + - 04-02 (listAccess.ts, listEmitter.ts primitives) + - 04-03 (listsRouter + ListsIndex + ListCard — prerequisite list data layer) + provides: + - POST/GET /api/lists/:id/items with fractional rank (D-13) + - PATCH /api/list-items/:id per-field LWW (D-08, exactly-one-field zod refine) + - DELETE /api/list-items/:id delete-wins (D-09) + - rank.ts: rankForAppend + rankBetween (fractional-indexing wrappers) + - ListDetail with active/completed split (D-05), optimistic mutations (D-07/D-09) + - ItemRow with checkbox, plain-text text, GripVertical slot, swipe/hover delete + - AddItemInput sticky bottom input + - listsClient item functions: fetchListItems, addItem, patchListItem, deleteItem + affects: + - apps/api/src/routes/lists.ts (item routes added, listItemsRouter exported) + - apps/api/src/index.ts (listItemsRouter mounted at /api/list-items) + - apps/api/src/lib/rank.ts (new) + - apps/api/src/lib/rank.test.ts (new) + - apps/api/tests/routes/lists.test.ts (item route tests added) + - apps/pwa/src/api/listsClient.ts (item functions added) + - apps/pwa/src/routes/ListDetail.tsx (placeholder replaced with real implementation) + - apps/pwa/src/routes/ListDetail.test.tsx (todo stubs replaced with real tests) + - apps/pwa/src/components/ItemRow.tsx (new) + - apps/pwa/src/components/AddItemInput.tsx (new) +tech_stack: + added: + - fractional-indexing (already installed from Plan 04-01) + patterns: + - rankForAppend wraps generateKeyBetween(lastRank, null) + - patchItemSchema .partial().refine(exactly one field) for D-08/T-04-07 + - listItemsRouter separate from listsRouter, mounted at /api/list-items + - Optimistic mutations: onMutate/onError/onSettled against ['list', listId] + - Delete-wins: no onError rollback in deleteMutation (D-09) + - Uncheck recomputes rank to active-bottom in same DB write (Open Question 2) +key_files: + created: + - apps/api/src/lib/rank.ts + - apps/api/src/lib/rank.test.ts + - apps/pwa/src/components/ItemRow.tsx + - apps/pwa/src/components/AddItemInput.tsx + modified: + - apps/api/src/routes/lists.ts (item routes, listItemsRouter export) + - apps/api/src/index.ts (listItemsRouter mount) + - apps/api/tests/routes/lists.test.ts (25 new tests) + - apps/pwa/src/api/listsClient.ts (item functions + ListItemsResponse type) + - apps/pwa/src/routes/ListDetail.tsx (placeholder replaced) + - apps/pwa/src/routes/ListDetail.test.tsx (7 real tests) +decisions: + - "listItemsRouter exported separately from listsRouter; mounted at /api/list-items so PATCH/DELETE resolve at /api/list-items/:id per RESEARCH architecture diagram" + - "Uncheck rank: recompute to active-bottom (generateKeyBetween(lastActiveRank, null)) in same write per Open Question 2 from 04-RESEARCH.md" + - "Optimistic add uses negative id as temporary identifier (item.id < 0 → dim opacity 0.6)" + - "Delete-wins: no onError rollback in deleteMutation; onSettled invalidates to reconcile" + - "GripVertical drag handle present in ItemRow but non-functional (Plan 05 wires dnd-kit)" +metrics: + duration: "~11 minutes" + completed: "2026-06-09" + task_count: 2 + file_count: 10 +--- + +# Phase 4 Plan 4: Item CRUD + Checked-Sink Vertical Slice Summary + +**One-liner:** Item CRUD vertical slice (LIST-02) — POST/GET/PATCH/DELETE item endpoints with fractional rank (D-13), per-field LWW (D-08), delete-wins (D-09), and ListDetail active/completed split with optimistic mutations (D-05/D-07). + +## TDD Gate Compliance + +| Gate | Commit | Status | +|------|--------|--------| +| RED — 16 failing item route tests + rank unit tests | b1dc9b8 | PASS — 16 route tests failed (404), rank.test.ts failed (no impl) | +| GREEN — rank.ts + item routes + listItemsRouter | 5e31514 | PASS — all 48 tests pass | +| REFACTOR | (skipped) | Implementation was clean on first pass | + +## Tasks Completed + +| Task | Name | Commit | Files | +|------|------|--------|-------| +| RED | Failing tests for item routes + rank helpers | b1dc9b8 | tests/routes/lists.test.ts, src/lib/rank.test.ts | +| GREEN | rank.ts + item endpoints + listItemsRouter + index.ts mount | 5e31514 | rank.ts, lists.ts, index.ts | +| 2 | ListDetail + ItemRow + AddItemInput + listsClient item fns | 6da9c2a | 5 files (2 new, 3 modified) | + +## Deviations from Plan + +### Auto-fixed Issues + +**1. [Rule 3 - Routing] listItemsRouter exported separately from listsRouter** +- **Found during:** GREEN phase — PATCH/DELETE routes at `listsRouter.patch('/list-items/:itemId')` resolved to `/api/lists/list-items/:id` not `/api/list-items/:id` as the tests expected and RESEARCH.md architecture diagram specified. +- **Issue:** The plan's note "Mount accordingly so both resolve under /api" required a second router export. Routes for single-item mutations must be at `/api/list-items/:id`, not nested under `/api/lists`. +- **Fix:** Added `export const listItemsRouter = new Hono()` in lists.ts for PATCH/DELETE routes; mounted it at `/api/list-items` in index.ts alongside the existing `listsRouter` at `/api/lists`. The two routers share the same helper functions (resolveUserId, checkListAccess, rankForAppend). +- **Files modified:** `apps/api/src/routes/lists.ts`, `apps/api/src/index.ts` +- **Commit:** 5e31514 + +## Playwright Browser Check + +Ran against `http://localhost:5173/lists/284` (list id 284, Test Groceries) with API on port 3000 (DEV_AUTH_BYPASS=true): + +1. `/lists/284` renders empty state: "Nothing here yet" + "Add your first item below." — PASS +2. Click input, type "milk", click Add → item appears in "Active items" list with checkbox + GripVertical handle — PASS +3. Click checkbox "milk" → item moves to "Completed (1)" section (sinks per D-05) — PASS +4. Hover over completed item → "Delete milk" button appears → click → item vanishes instantly, returns to "Nothing here yet" (no confirmation per D-06) — PASS + +## Verification Results + +### API Tests +- `tests/routes/lists.test.ts + src/lib/rank.test.ts`: 48 passed (0 failed) +- rank.ts pure unit tests: 8 passed (rankForAppend/rankBetween ordering/stability) +- Item POST assigns rank "a0" for first item; subsequent items rank > prior — PASS +- Per-field PATCH zod refine (exactly one field) — two-field body → 400 — PASS +- Uncheck rank recompute to active-bottom in same write — PASS +- Access gating T-04-05: 403 for non-member on GET/POST/PATCH/DELETE — PASS +- Delete-wins D-09: PATCH after DELETE returns 404 (no resurrection) — PASS + +### PWA Tests +- `src/routes/ListDetail.test.tsx`: 7 passed (0 failed) +- Optimistic check/uncheck/add/delete mutations +- Rollback on error restores previous state +- D-05 active/completed split verified +- D-09 delete-wins no-rollback verified + +### TypeScript +- `pnpm --filter @familysync/api typecheck` — PASS +- `pnpm --filter @familysync/pwa exec tsc --noEmit` — PASS + +## Known Stubs + +| File | Stub | Reason | +|------|------|--------| +| `apps/pwa/src/routes/ListDetail.tsx` | List header shows "List" (not the list name) | fetchListItems returns items only; list name not in the items response. Plan 05/06 can enrich from the ['lists'] cache. Non-blocking — user can still use the list. | +| `apps/api/src/routes/lists.ts` | Plan 06 SSE seam comments (`publishListEvent` calls commented out) | Plan 06 adds fan-out once the SSE `/api/sse/lists` endpoint exists | +| `apps/pwa/src/components/ItemRow.tsx` | GripVertical handle present but non-functional | Plan 05 wires dnd-kit; handle slot is structural as specified | + +The "List" heading stub does not prevent the plan's goal (add, check, delete items). Items are functionally correct. The heading will be enriched in Plan 05/06. + +## Threat Surface Scan + +All threats from the plan's threat model are mitigated: + +| Threat ID | Status | Notes | +|-----------|--------|-------| +| T-04-05 (EoP — mutating items in inaccessible list) | Mitigated | checkListAccess() on every item handler; 403 tested for GET/POST/PATCH/DELETE | +| T-04-07 (Tampering — overposting on item PATCH) | Mitigated | patchItemSchema .partial().refine(exactly one field); 400 on two-field body tested | +| T-04-06 (Tampering — XSS via item text) | Mitigated | Item text rendered as plain-text JSX child in ItemRow; no dangerouslySetInnerHTML | +| T-04-09 (Tampering — resurrecting deleted item) | Mitigated | DELETE final; PATCH on deleted id → 404 (no upsert); delete-wins test asserts no resurrection | + +No new threat surface beyond the plan's trust boundaries. + +## Self-Check: PASSED + +- `apps/api/src/lib/rank.ts` — FOUND +- `apps/api/src/lib/rank.test.ts` — FOUND +- `apps/api/src/routes/lists.ts` (POST /:id/items route) — FOUND +- `apps/api/src/index.ts` (listItemsRouter mounted at /api/list-items) — FOUND +- `apps/pwa/src/components/ItemRow.tsx` — FOUND +- `apps/pwa/src/components/AddItemInput.tsx` — FOUND +- `apps/pwa/src/routes/ListDetail.tsx` (real implementation, not placeholder) — FOUND +- `apps/pwa/src/api/listsClient.ts` (fetchListItems, addItem exported) — FOUND +- Commit b1dc9b8 (RED) — FOUND +- Commit 5e31514 (GREEN) — FOUND +- Commit 6da9c2a (Task 2) — FOUND