feat(M14): 3-pane backlog — generic SplitPane, BacklogStore, SSE realtime, card-grid TaskPanel (#22)

* feat(split-pane): refactor to generic n-pane SplitPane with cookie persistence

New API: panes[], defaultSplit[], cookieKey, tabLabels. Supports arbitrary
number of panes with n-1 draggable dividers and JSON cookie persistence.
Replaces TriplePane; mobile renders tabs.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(split-pane): migrate callers to new panes[] API

Backlog page and sprint board now use generic SplitPane.
TriplePane removed; sprint board uses 3-pane with defaultSplit=[28,35,37].

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(split-pane): add unit tests for 2/3-pane, cookie-restore, mobile tabs

Added jsdom + @testing-library/react devDeps for component testing.
7 cases: render, divider count, cookie restore, invalid cookie fallback,
mobile tab render/switch, and no-dividers-on-mobile.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(backlog): add BacklogStore Zustand store with applyChange reducer

State: pbis, storiesByPbi, tasksByStory. setInitialData for server
hydration; applyChange(entity, op, data) handles I/U/D for SSE events.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(backlog): server-fetch tasks + hydrate BacklogStore on page load

Page now fetches tasks parallel to stories and groups by story_id.
BacklogHydrationWrapper calls setInitialData on mount so the store
is ready for downstream SSE consumers.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(backlog): add EmptyPanel shared component, replace inline empty states

EmptyPanel takes title?, message, and optional action with DemoTooltip.
Replaces duplicate inline empty-state markup in pbi-list and story-panel.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(backlog): add TaskPanel with sortable rows and TaskDialog wiring

Reads selectedStoryId + tasksByStory from stores. DnD reorder via
reorderTasksAction. Row click → ?editTask, + button → ?newTask&storyId.
DemoTooltip on drag handles and + button.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(backlog): wire TaskPanel + TaskDialog into backlog page

3-pane SplitPane [20,45,35]. searchParams for newTask/editTask.
TaskDialog and EditTaskLoader render on ?newTask and ?editTask.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(backlog): add TaskPanel tests for render states and click handlers

7 cases: no-story empty, no-tasks empty+action, tasks render, + button
router.push, row click router.push, demo disabled button, demo disabled handles.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(backlog): migrate PbiList to store-driven via useBacklogStore

Removes pbis prop; reads from useBacklogStore(s => s.pbis) so SSE
updates reflect in real-time without prop drilling.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(backlog): migrate StoryPanel to store-driven + selectStory on click

Removes storiesByPbi prop; reads from useBacklogStore. Card click now
dispatches selectStory(id) + shows isSelected highlight. Edit moved to
inline pencil button. page.tsx drops pbis/storiesByPbi props.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(backlog): add 3-pane integration tests for click-cascade flow

Covers: empty states, PBI→stories, story→tasks, cascade-reset,
isSelected highlight. localStorage mocked for sort-mode persistence.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(ST-1115): SSE backlog realtime — endpoint, hook, hydration mount, tests

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(ST-1116): mobile auto-switch tabs + back button in BacklogSplitPane

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* docs(ST-1116): update functional-spec (3-pane backlog + mobile) and architecture (backlog SSE + backlog-store)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(ST-1117): TaskPanel card-grid — BacklogCard + rectSortingStrategy, tests updated

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(tests): correct PbiStatusApi type and remove duplicate mock keys

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Janpeter Visser 2026-04-30 18:16:07 +02:00 committed by GitHub
parent 6cd98129f2
commit 8877ea469d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 2474 additions and 305 deletions

View file

@ -733,8 +733,9 @@ scrum4me/
│ ├── product-access.ts # productAccessFilter helper (eigenaar of teamlid)
│ └── env.ts # Zod-gevalideerde env vars
├── stores/ # Zustand stores
│ ├── backlog-store.ts # PBI/story/task hydration + applyChange (SSE)
│ ├── planner-store.ts # Optimistische drag-and-drop volgorde
│ ├── selection-store.ts # Geselecteerd PBI / story
│ ├── selection-store.ts # Geselecteerd PBI / story (cascade-reset)
│ ├── sprint-store.ts # Sprint Backlog taakvolgordes
│ ├── solo-store.ts # Solo board optimistische taakstatus
│ └── product-store.ts # Actief product (naam + id) voor navbar
@ -1003,6 +1004,67 @@ Iron-session cookie of Bearer-token (demo). De auth-check loopt éénmalig bij d
---
## Realtime — Backlog SSE (ST-1115)
De Product Backlog-pagina (`/products/[id]`) update live als PBI's, stories of taken worden gemuteerd. De pijplijn is gelijk aan de Solo-SSE (M8), maar met een eenvoudiger server-side filter: alleen `product_id`-scope, geen sprint- of user-scope.
```
┌─────────────────────────┐
│ Mutatie (Prisma write) │ Server Action, MCP, etc.
└────────────┬────────────┘
┌─────────────────────────┐
│ Postgres row trigger │ AFTER INSERT/UPDATE/DELETE
│ scrum4me_notify_change()│ entity: 'pbi' | 'story' | 'task'
└────────────┬────────────┘
▼ pg_notify('scrum4me_changes', json)
┌─────────────────────────┐
│ /api/realtime/backlog │ Node runtime, dedicated pg.Client
│ LISTEN scrum4me_changes │ filtert op entity ∈ {pbi,story,task}
│ │ én product_id matcht query-param
└────────────┬────────────┘
▼ text/event-stream
┌─────────────────────────┐
│ EventSource (browser) │ beheerd door useBacklogRealtime
│ → backlog-store.apply │ via applyChange(entity, op, data)
│ Change(entity,op,data)│
└────────────┬────────────┘
┌─────────────────────────┐
│ PbiList / StoryPanel / │ re-render op basis van Zustand state
│ TaskPanel re-render │
└─────────────────────────┘
```
### Hydration en SSE-mount
De pagina is een Server Component die alle data parallel fetcht. Resultaten worden doorgegeven aan `BacklogHydrationWrapper` (client component), die:
1. `useBacklogStore.setInitialData(...)` aanroept op mount (eenmalig).
2. `useBacklogRealtime(productId)` mount — opent de SSE-stream.
Alle client-componenten (PbiList, StoryPanel, TaskPanel) lezen uitsluitend uit de Zustand store; ze accepteren geen data-props meer.
### backlog-store en applyChange
```ts
// stores/backlog-store.ts
applyChange(entity: 'pbi' | 'story' | 'task', op: 'I' | 'U' | 'D', data: Record<string, unknown>)
```
- **I (Insert):** voegt het nieuwe object toe aan de juiste sub-array
- **U (Update):** patcht de bestaande entry in-place via spread (`{ ...existing, ...data }`)
- **D (Delete):** filtert de entry weg op `id`; doorzoekt alle sub-arrays omdat de parent-ID afwezig kan zijn in het delete-payload
### Server-side filter (backlog)
`/api/realtime/backlog?product_id=...` filtert op:
- `entity ∈ {pbi, story, task}` — job/worker-events en questions worden genegeerd
- `product_id` matcht de query-param
Demo-gebruikers mogen lezen (geen 403). Overige lifecycle-kenmerken (heartbeat, hard-close, backoff, visibility-pause) zijn identiek aan de Solo SSE.
---
## Demo-user policy (ST-1110)
Demo-gebruikers (`is_demo = true` in de database, `isDemo: true` in de iron-session) hebben volledig read-only toegang. Bescherming is drielaags: