docs(04): create phase plan — 6 plans across 5 waves for shared lists + live sync
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
d89eb47483
commit
38fa6f448b
@@ -0,0 +1,158 @@
|
||||
---
|
||||
phase: 04-shared-lists-live-sync
|
||||
plan: 05
|
||||
type: execute
|
||||
wave: 4
|
||||
depends_on: ["04-04"]
|
||||
files_modified:
|
||||
- apps/pwa/src/routes/ListDetail.tsx
|
||||
- apps/pwa/src/components/ItemRow.tsx
|
||||
- apps/pwa/src/api/listsClient.ts
|
||||
- apps/api/src/lib/rank.test.ts
|
||||
- apps/api/src/routes/lists.test.ts
|
||||
autonomous: true
|
||||
requirements: [LIST-03]
|
||||
user_setup: []
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "A member can drag an active item to a new position and the order persists"
|
||||
- "A reorder writes only the moved item's rank (one-row write), not a renumber"
|
||||
- "Touch drag requires a deliberate long-press on the handle (no accidental drags while scrolling)"
|
||||
- "A reorder arriving from another member animates to the new position rather than hard-snapping (D-14)"
|
||||
artifacts:
|
||||
- path: "apps/pwa/src/components/ItemRow.tsx"
|
||||
provides: "dnd-kit sortable item with drag handle"
|
||||
contains: "useSortable"
|
||||
- path: "apps/pwa/src/routes/ListDetail.tsx"
|
||||
provides: "DndContext/SortableContext over active items with onDragEnd → rank PATCH"
|
||||
contains: "DndContext"
|
||||
key_links:
|
||||
- from: "apps/pwa/src/routes/ListDetail.tsx"
|
||||
to: "PATCH /api/list-items/:id { position }"
|
||||
via: "onDragEnd computes generateKeyBetween + optimistic patch"
|
||||
pattern: "generateKeyBetween|position"
|
||||
- from: "apps/pwa/src/components/ItemRow.tsx"
|
||||
to: "@dnd-kit/sortable"
|
||||
via: "useSortable handle listeners"
|
||||
pattern: "useSortable"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Deliver the drag-to-reorder vertical slice (LIST-03): a member can drag an active item to a new position using @dnd-kit, and the move persists as a single-row fractional-rank write (D-13). Touch drag requires a 200ms long-press on the handle (no accidental drags); concurrent reorders converge via last-write-wins (D-15); and a reorder that arrives from another member animates to its new position rather than hard-snapping (D-14).
|
||||
|
||||
MVP slice: after this plan a real user can reorder list items — the last interactive capability of the lists surface — building directly on the items rendered in Plan 04.
|
||||
|
||||
Purpose: Layer drag-and-drop and client-side fractional-rank computation onto the existing ItemRow/ListDetail, reusing the server-side per-field position PATCH already built in Plan 04.
|
||||
Output: dnd-kit DndContext/SortableContext in ListDetail; sortable ItemRow with handle-scoped listeners + sensors; client computes the new rank via generateKeyBetween and PATCHes position optimistically.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@$HOME/.claude/gsd-core/workflows/execute-plan.md
|
||||
@$HOME/.claude/gsd-core/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/phases/04-shared-lists-live-sync/04-CONTEXT.md
|
||||
@.planning/phases/04-shared-lists-live-sync/04-RESEARCH.md
|
||||
@.planning/phases/04-shared-lists-live-sync/04-PATTERNS.md
|
||||
@.planning/phases/04-shared-lists-live-sync/04-UI-SPEC.md
|
||||
@.planning/phases/04-shared-lists-live-sync/04-VALIDATION.md
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Sortable ItemRow + DndContext reorder with optimistic rank PATCH (LIST-03, D-13/D-14/D-15)</name>
|
||||
<files>apps/pwa/src/components/ItemRow.tsx, apps/pwa/src/routes/ListDetail.tsx, apps/pwa/src/api/listsClient.ts</files>
|
||||
<read_first>
|
||||
- apps/pwa/src/components/ItemRow.tsx (from Plan 04 — add useSortable; handle slot already present)
|
||||
- apps/pwa/src/routes/ListDetail.tsx (active-items rendering from Plan 04)
|
||||
- apps/pwa/src/api/listsClient.ts (patchListItem supports { position })
|
||||
- .planning/phases/04-shared-lists-live-sync/04-RESEARCH.md Finding 7 (dnd-kit + handle + sensors + rank-on-drop)
|
||||
- .planning/phases/04-shared-lists-live-sync/04-PATTERNS.md §"ItemRow.tsx"
|
||||
- .planning/phases/04-shared-lists-live-sync/04-UI-SPEC.md §"Drag-to-Reorder", §"Accessibility Baseline" (keyboard reorder)
|
||||
</read_first>
|
||||
<action>
|
||||
Make ItemRow sortable: use useSortable({ id: item.id }) from @dnd-kit/sortable; attach setNodeRef + style (CSS.Transform.toString(transform), transition fallback 'transform 150ms ease-out' for D-14 remote animation, opacity 0.8 + slight scale-down when isDragging). Attach drag listeners to the GripVertical handle button ONLY (not the whole row) so taps on checkbox/text/delete still work. Drag handle only on active items (completed items not reorderable per UI-SPEC).
|
||||
|
||||
In ListDetail, wrap the active-items list in DndContext (collisionDetection={closestCenter}) + SortableContext (items = active item ids, verticalListSortingStrategy). Configure sensors via useSensors: PointerSensor/MouseSensor immediate, TouchSensor with activationConstraint { delay: 200, tolerance: 5 } (no accidental drags), and KeyboardSensor for the accessibility keyboard-reorder fallback.
|
||||
|
||||
onDragEnd: ignore no-op (no over / same id). Compute the destination index after the move; derive prevRank/nextRank from the active list at the destination and compute newRank = generateKeyBetween(prevRank, nextRank) (fractional-indexing). Fire an optimistic reorder mutation: setQueryData(['list', listId]) to reflect the new order immediately (snap), then patchListItem(itemId, { position: newRank }); onError animate back / rollback to previous; onSettled invalidate. Only the moved item's rank is written (one-row PATCH — D-13). Concurrent same-item reorder converges by server LWW on updatedAt (D-15) — no drag-state broadcasting.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>pnpm --filter @familysync/pwa exec tsc --noEmit && grep -q "useSortable" apps/pwa/src/components/ItemRow.tsx && grep -q "DndContext" apps/pwa/src/routes/ListDetail.tsx && grep -q "generateKeyBetween" apps/pwa/src/routes/ListDetail.tsx</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- ItemRow uses useSortable with listeners on the handle only; completed items have no handle.
|
||||
- ListDetail wraps active items in DndContext/SortableContext with Pointer/Touch(delay 200)/Keyboard sensors.
|
||||
- onDragEnd computes newRank via generateKeyBetween and issues a single-item position PATCH optimistically with rollback.
|
||||
- PWA typecheck passes.
|
||||
- Browser check (`playwright-cli`): drag an item to a new position; the new order persists after a reload (rank written). Record in SUMMARY. (Touch long-press + keyboard reorder are dnd-kit built-ins; note manual/device coverage where playwright cannot simulate long-press reliably.)
|
||||
</acceptance_criteria>
|
||||
<done>User can drag-reorder active items; move persists as a one-row rank write; remote reorders animate.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Strengthen server-side reorder ordering tests (LIST-03, D-13)</name>
|
||||
<files>apps/api/src/lib/rank.test.ts, apps/api/src/routes/lists.test.ts</files>
|
||||
<read_first>
|
||||
- apps/api/src/lib/rank.test.ts (from Plan 04)
|
||||
- apps/api/src/routes/lists.test.ts (PATCH position coverage)
|
||||
- .planning/phases/04-shared-lists-live-sync/04-VALIDATION.md (LIST-03 row: "PATCH new rank produces correct fractional order")
|
||||
- .planning/phases/04-shared-lists-live-sync/04-RESEARCH.md §"Common Pitfalls" Pitfall 2 (precision)
|
||||
</read_first>
|
||||
<action>
|
||||
Add server-side tests proving reorder correctness: (a) repeated mid-point inserts via rankBetween produce strictly increasing distinct strings over many iterations (precision does not collapse — Pitfall 2); (b) PATCH /api/list-items/:id { position } updates only rank and a subsequent GET returns items in the new ASC order; (c) moving an item between two neighbors yields a rank strictly between theirs. These align the LIST-03 row in 04-VALIDATION.md to a green automated check. No production behavior change — Plan 04 already implements the PATCH position path.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>pnpm --filter @familysync/api exec vitest run src/lib/rank.test.ts src/routes/lists.test.ts</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- LIST-03 ordering test ("PATCH new rank produces correct fractional order") is present and green.
|
||||
- Mid-point-insert precision test passes for many iterations.
|
||||
</acceptance_criteria>
|
||||
<done>Server-side reorder ordering + rank precision are covered by green automated tests.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| browser → PATCH /api/list-items/:id { position } | client supplies the new rank string — untrusted |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|-------------|-----------------|
|
||||
| T-04-07 | Tampering | client sending position alongside other fields | mitigate | zod patchItemSchema refine (exactly one field) already enforces position-only PATCH (Plan 04); reasserted by tests |
|
||||
| T-04-05 | Elevation of Privilege | reordering items in an inaccessible list | mitigate | PATCH list-items access-gated (owner OR list_shares) from Plan 04 |
|
||||
| T-04-10 | Denial of Service | pathological "zipper" inserts growing rank strings | accept | VARCHAR(255) headroom; fractional-indexing degrades gracefully; rebalance available via generateNKeysBetween if ever needed (not in scope) |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
<automated>pnpm --filter @familysync/api exec vitest run src/lib/rank.test.ts src/routes/lists.test.ts && pnpm --filter @familysync/pwa exec tsc --noEmit</automated>
|
||||
- `playwright-cli`: drag-reorder persists across reload.
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- LIST-03 satisfied: drag-to-reorder works, persists as a single-row rank write.
|
||||
- Touch long-press + keyboard reorder available; remote reorders animate (D-14).
|
||||
- Reorder ordering + precision covered by automated tests.
|
||||
</success_criteria>
|
||||
|
||||
<artifacts_produced>
|
||||
**Symbols/files this plan creates (exclude from drift verification):**
|
||||
- ItemRow gains useSortable + handle-scoped drag listeners
|
||||
- ListDetail gains DndContext/SortableContext + useSensors + onDragEnd rank computation
|
||||
- Additional rank/order tests in rank.test.ts and lists.test.ts (no new production endpoints — reuses Plan 04 PATCH position)
|
||||
</artifacts_produced>
|
||||
|
||||
<output>
|
||||
Create `.planning/phases/04-shared-lists-live-sync/04-05-SUMMARY.md` when done.
|
||||
</output>
|
||||
Reference in New Issue
Block a user