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:
parent
6cd98129f2
commit
8877ea469d
22 changed files with 2474 additions and 305 deletions
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -194,27 +194,37 @@ Gebruikers kunnen producten aanmaken, bewerken en archiveren. Een product is het
|
|||
|
||||
---
|
||||
|
||||
### F-04: Product Backlog — gesplitst scherm
|
||||
### F-04: Product Backlog — 3-paneels gesplitst scherm
|
||||
|
||||
**Prioriteit:** v1 — Kritiek
|
||||
**Persona:** Lars, Dina, Remi
|
||||
|
||||
**Omschrijving:**
|
||||
De Product Backlog wordt weergegeven als een gesplitst scherm: links de PBI's gerangschikt op prioriteit en volgorde, rechts de stories van het geselecteerde PBI. De splitter is horizontaal versleepbaar. Elk paneel heeft een eigen navigatiebar met acties (aanmaken, filteren, verwijderen).
|
||||
De Product Backlog wordt weergegeven als een 3-paneels gesplitst scherm: PBI's (links) | Stories (midden) | Taken (rechts). De splitters zijn versleepbaar. Selectie cascadeert: klikken op een PBI toont de bijbehorende stories; klikken op een story toont de bijbehorende taken. Elk paneel heeft een eigen navigatiebar met acties.
|
||||
|
||||
**Acceptatiecriteria:**
|
||||
- [ ] Standaard splitverhouding is 40/60 (PBI's / stories)
|
||||
- [ ] Splitter is versleepbaar; positie wordt lokaal opgeslagen (localStorage)
|
||||
- [ ] Selecteren van een PBI links toont de bijbehorende stories rechts
|
||||
- [ ] Standaard splitverhouding is 20/45/35 (PBI's / Stories / Taken)
|
||||
- [ ] Splitters zijn versleepbaar; positie wordt opgeslagen in een cookie (`sp:backlog-{id}`)
|
||||
- [ ] Selecteren van een PBI toont de bijbehorende stories in het middenpaneel
|
||||
- [ ] Geselecteerd PBI is visueel gemarkeerd (achtergrondkleur of rand)
|
||||
- [ ] Linkerpaneel navigatiebar bevat: [+ PBI aanmaken], [filter], [verwijderen]
|
||||
- [ ] Rechterpaneel navigatiebar bevat: [+ Story aanmaken], [filter], [verwijderen]
|
||||
- [ ] Lege staat links: prompt om eerste PBI aan te maken
|
||||
- [ ] Lege staat rechts (geen PBI geselecteerd): instructie om een PBI te selecteren
|
||||
- [ ] Lege staat rechts (PBI geselecteerd, geen stories): prompt om eerste story aan te maken
|
||||
- [ ] Selecteren van een story toont de bijbehorende taken in het rechterpaneel
|
||||
- [ ] Geselecteerde story is visueel gemarkeerd
|
||||
- [ ] Cascade-reset: selecteren van een ander PBI wist de geselecteerde story en taken
|
||||
- [ ] PBI-paneel navigatiebar bevat: [+ PBI aanmaken]
|
||||
- [ ] Stories-paneel navigatiebar bevat: [+ Story aanmaken], [sorteer], [filter status]
|
||||
- [ ] Taken-paneel navigatiebar bevat: [+ Nieuwe taak]
|
||||
- [ ] Lege staat PBI-paneel: prompt om eerste PBI aan te maken
|
||||
- [ ] Lege staat Stories-paneel (geen PBI geselecteerd): instructie om een PBI te selecteren
|
||||
- [ ] Lege staat Stories-paneel (PBI geselecteerd, geen stories): prompt om eerste story aan te maken
|
||||
- [ ] Lege staat Taken-paneel (geen story geselecteerd): instructie om een story te selecteren
|
||||
- [ ] Lege staat Taken-paneel (story geselecteerd, geen taken): prompt om eerste taak aan te maken
|
||||
- [ ] Taak aanmaken opent TaskDialog via `?newTask=1&storyId={id}`
|
||||
- [ ] Taak bewerken opent TaskDialog via `?editTask={id}`
|
||||
|
||||
**Randgevallen:**
|
||||
- Scherm smaller dan 768px → gesplitst scherm schakelt over naar tabbladen (PBI's / Stories)
|
||||
- Scherm smaller dan 1024px → 3-paneels scherm schakelt over naar 3 tabbladen (PBI's | Stories | Taken)
|
||||
- Mobile tab-navigatie: klikken op PBI schakelt automatisch naar Stories-tab; klikken op story schakelt naar Taken-tab
|
||||
- Mobile ← terug-knop in tab-header op tabs 2 en 3 navigeert naar het vorige tabblad
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue