Load/render workspace alignment (#182)
* docs: plan load render workspace alignment * fix: normalize workspace status hydration * fix: avoid duplicate backlog hydration load * refactor: use sprint store active story * refactor: migrate solo to workspace store * chore: stabilize verification ignores
This commit is contained in:
parent
98ee05d458
commit
3b5cee823c
28 changed files with 1845 additions and 577 deletions
|
|
@ -46,6 +46,7 @@ Auto-generated on 2026-05-10 from front-matter and headings.
|
|||
| [Landing v2 — lokaal & veilig + architectuurdiagram](./plans/landing-local-first.md) | active | 2026-05-03 |
|
||||
| [Landing v3 — van idee tot pull request](./plans/landing-v3-idea-flow.md) | active | 2026-05-04 |
|
||||
| [Scrum4Me-Research — Zustand rearchitecture (reset + execute)](./plans/lees-de-readme-md-validated-book.md) | — | — |
|
||||
| [Verbeterplan load/render Product Backlog, Sprint en Solo](./plans/load-render-improvement-plan-2026-05-10.md) | draft | 2026-05-10 |
|
||||
| [Advies — Zelf een Git-platform hosten naast of in plaats van GitHub](./plans/Local github setup.md) | — | — |
|
||||
| [M10 — Password-loze inlog via QR-pairing](./plans/M10-qr-pairing-login.md) | active | 2026-05-03 |
|
||||
| [M11 — Claude vraagt, gebruiker antwoordt](./plans/M11-claude-questions.md) | active | 2026-05-03 |
|
||||
|
|
@ -132,6 +133,7 @@ Auto-generated on 2026-05-10 from front-matter and headings.
|
|||
| [Realtime smoke-checklist — PBI / Story / Task](./realtime-smoke.md) | `realtime-smoke.md` | active | 2026-05-03 |
|
||||
| [Caveman plan — Beelink naar Ubuntu Scrum4Me server](./recommendations/beelink-ubuntu-scrum4me-server-caveman-plan.md) | `recommendations/beelink-ubuntu-scrum4me-server-caveman-plan.md` | draft | 2026-05-09 |
|
||||
| [Aanbeveling — Claude VM jobflow en gitstrategie](./recommendations/claude-vm-job-flow-git-strategy.md) | `recommendations/claude-vm-job-flow-git-strategy.md` | draft | 2026-05-09 |
|
||||
| [Load/render implementatie review](./recommendations/load-render-implementation-review-2026-05-10.md) | `recommendations/load-render-implementation-review-2026-05-10.md` | review | 2026-05-10 |
|
||||
| [Agent-flow: open issues & decision log](./runbooks/agent-flow-pitfalls.md) | `runbooks/agent-flow-pitfalls.md` | active | 2026-05-03 |
|
||||
| [Auto-PR flow: van story-DONE naar gemergde PR](./runbooks/auto-pr-flow.md) | `runbooks/auto-pr-flow.md` | active | 2026-05-06 |
|
||||
| [Branch, PR & Commit Strategy](./runbooks/branch-and-commit.md) | `runbooks/branch-and-commit.md` | active | 2026-05-03 |
|
||||
|
|
|
|||
201
docs/plans/load-render-improvement-plan-2026-05-10.md
Normal file
201
docs/plans/load-render-improvement-plan-2026-05-10.md
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
---
|
||||
title: "Verbeterplan load/render Product Backlog, Sprint en Solo"
|
||||
date: 2026-05-10
|
||||
status: draft
|
||||
scope: ["Product Backlog", "Sprint", "Solo", "workspace stores", "realtime resync"]
|
||||
source_review: "../recommendations/load-render-implementation-review-2026-05-10.md"
|
||||
chosen_solo_option: "5B - Solo workspace-store migratie"
|
||||
---
|
||||
|
||||
# Verbeterplan load/render Product Backlog, Sprint en Solo
|
||||
|
||||
## Doel
|
||||
|
||||
Maak de load/render-flow van Product Backlog, Sprint en Solo voorspelbaar, gelijkvormig en goedkoper:
|
||||
|
||||
- geen dubbele initial loads;
|
||||
- een expliciet status-contract tussen server, API, stores en UI;
|
||||
- consistente hydration/resync na reconnect, tab-visible en refresh;
|
||||
- minder stale render state in Solo;
|
||||
- duidelijke store-eigenaarschap per scherm.
|
||||
|
||||
## Uitgangspunten
|
||||
|
||||
- De route/API-boundary mag lowercase API-statussen blijven gebruiken.
|
||||
- De interne Product/Sprint workspace UI verwacht nu story/task statussen als DB `UPPER_SNAKE`.
|
||||
- Product Backlog en Sprint zijn de referentie voor het gewenste patroon: server snapshot, client hydration wrapper, workspace-store, SSE, directe store-resync.
|
||||
- Solo hoeft niet in dezelfde grote store te worden gemigreerd in de eerste stap, maar zijn refresh-hydration moet wel correct worden.
|
||||
|
||||
## Fase 1 - Status-contract vastleggen en afdwingen
|
||||
|
||||
### Stap 1.1 - Leg het interne contract vast
|
||||
|
||||
Besluit en documenteer:
|
||||
|
||||
- PBI-status in Product Backlog blijft API-lowercase zolang `PBI_STATUS_LABELS` en `PBI_STATUS_COLORS` daarop gebouwd zijn.
|
||||
- Story-status en task-status in Product/Sprint workspace-stores zijn intern `UPPER_SNAKE`.
|
||||
- API routes blijven lowercase teruggeven aan externe/REST clients.
|
||||
|
||||
### Stap 1.2 - Voeg adapters toe aan de workspace-store boundary
|
||||
|
||||
Maak kleine adapterfuncties voor API-responses voordat data in stores wordt gehydrateerd:
|
||||
|
||||
- Product workspace:
|
||||
- full backlog snapshot;
|
||||
- PBI stories;
|
||||
- story tasks;
|
||||
- task detail.
|
||||
- Sprint workspace:
|
||||
- sprint workspace snapshot;
|
||||
- story tasks;
|
||||
- task detail.
|
||||
|
||||
Gebruik bestaande mappers uit `lib/task-status.ts`, bijvoorbeeld `storyStatusFromApi` en `taskStatusFromApi`.
|
||||
|
||||
### Stap 1.3 - Voeg regressietests toe
|
||||
|
||||
Test minimaal:
|
||||
|
||||
- API lowercase `todo` wordt in task UI-store `TO_DO`;
|
||||
- API lowercase `in_sprint` wordt in story UI-store `IN_SPRINT`;
|
||||
- bestaande PBI lowercase status blijft lowercase;
|
||||
- Sprint `STATUS_CYCLE` krijgt nooit lowercase input vanuit de store.
|
||||
|
||||
## Fase 2 - Dubbele Product Backlog load verwijderen
|
||||
|
||||
### Stap 2.1 - Maak hydration eigenaar van de initial backlog snapshot
|
||||
|
||||
Pas Product Backlog aan naar hetzelfde eigenaarschap als Sprint:
|
||||
|
||||
- `BacklogHydrationWrapper` hydrateert snapshot;
|
||||
- wrapper zet ook `context.activeProduct`;
|
||||
- wrapper markeert `loadedProductId`;
|
||||
- `SetCurrentProduct` start op routes met eigen hydration geen full `ensureProductLoaded`.
|
||||
|
||||
### Stap 2.2 - Guard `setActiveProduct`
|
||||
|
||||
Voeg een guard toe zodat `setActiveProduct(product)` geen `ensureProductLoaded` start als:
|
||||
|
||||
- hetzelfde product al actief is;
|
||||
- `loading.loadedProductId === product.id`;
|
||||
- er al een volledige snapshot gehydrateerd is.
|
||||
|
||||
### Stap 2.3 - Meet en verifieer
|
||||
|
||||
Controleer in devtools/server logs:
|
||||
|
||||
- openen van Product Backlog doet geen extra `/api/products/:id/backlog` na de server-render;
|
||||
- navigeren tussen product routes laadt nog steeds correct;
|
||||
- restore hints voor laatste PBI/story/task blijven werken.
|
||||
|
||||
## Fase 3 - Sprint selectie gelijkvormig maken
|
||||
|
||||
### Stap 3.1 - Verplaats geselecteerde story naar de sprint workspace-store
|
||||
|
||||
Vervang lokale `selectedStoryId` in `SprintBoardClient` door:
|
||||
|
||||
- `useSprintWorkspaceStore((s) => s.context.activeStoryId)`;
|
||||
- `useSprintWorkspaceStore.getState().setActiveStory(storyId)`;
|
||||
- reset via `setActiveStory(null)` bij verwijderen uit sprint.
|
||||
|
||||
### Stap 3.2 - Laat `TaskList` active-context gebruiken
|
||||
|
||||
Maak `TaskList` gelijkvormig met Product Backlog:
|
||||
|
||||
- lees taken via `selectTasksForActiveStory`;
|
||||
- behoud `storyId` alleen als fallback of verwijder de prop;
|
||||
- zorg dat `resyncActiveScopes` nu de actieve story/task werkelijk kan meenemen.
|
||||
|
||||
### Stap 3.3 - Restore-hints testen
|
||||
|
||||
Verifieer:
|
||||
|
||||
- story-selectie blijft behouden na refresh/reconnect;
|
||||
- task-paneel toont dezelfde story na tab-visible resync;
|
||||
- verwijderen van de actieve story reset taakpaneel netjes.
|
||||
|
||||
## Fase 4 - Solo refresh-hydration correct maken
|
||||
|
||||
### Stap 4.1 - Vervang task-id-only dependency
|
||||
|
||||
Vervang `taskKey = initialTasks.map(t => t.id).join(',')` door een render-relevante fingerprint, bijvoorbeeld:
|
||||
|
||||
- `id`;
|
||||
- `status`;
|
||||
- `sort_order`;
|
||||
- `title`;
|
||||
- `implementation_plan`;
|
||||
- `story_id`;
|
||||
- `story_title`;
|
||||
- `story_code`;
|
||||
- `task_code`;
|
||||
- relevante verify/queue velden.
|
||||
|
||||
Of hydrateer op iedere nieuwe `initialTasks` prop als performance acceptabel is.
|
||||
|
||||
### Stap 4.2 - Sync unassigned stories uit props
|
||||
|
||||
Voeg een effect toe die `unassignedStories` bijwerkt wanneer `initialUnassigned` inhoudelijk wijzigt.
|
||||
|
||||
### Stap 4.3 - Sorteer solo kolommen expliciet
|
||||
|
||||
Render `columnTasks` gesorteerd op `sort_order` en daarna stabiel op code/titel/id. Vertrouw niet op object insertion order.
|
||||
|
||||
### Stap 4.4 - Test gemiste event scenario's
|
||||
|
||||
Test:
|
||||
|
||||
- tab hidden, task status wijzigt extern, tab visible: kaart staat in juiste kolom;
|
||||
- reconnect met dezelfde task ids maar gewijzigde titel/status: UI update;
|
||||
- nieuwe unassigned story verschijnt na refresh;
|
||||
- gewijzigde `sort_order` past de render-volgorde aan.
|
||||
|
||||
## Fase 5 - Solo naar een gelijkvormig workspace-store patroon
|
||||
|
||||
Gekozen route: **Optie B**. Solo wordt naar een workspace-store patroon gemigreerd dat aansluit op Product Backlog en Sprint.
|
||||
|
||||
### Optie B - Grote stap
|
||||
|
||||
Migreer Solo naar een workspace-store patroon vergelijkbaar met Product/Sprint:
|
||||
|
||||
- normalized entities;
|
||||
- active sprint/product context;
|
||||
- loaded scopes;
|
||||
- resync methods;
|
||||
- realtime event adapters.
|
||||
|
||||
Concrete taken:
|
||||
|
||||
- Introduceer `stores/solo-workspace/{types,selectors,store}.ts`.
|
||||
- Introduceer een `SoloHydrationWrapper` die server snapshot en actieve context hydrateert.
|
||||
- Laat `SoloBoard` renderen vanuit selectors in de solo workspace-store.
|
||||
- Verplaats realtime event handling en job/worker status naar de solo workspace-store.
|
||||
- Vervang `router.refresh()` als primaire resync door `resyncActiveScopes`.
|
||||
- Houd route refresh alleen over als expliciete fallback voor onbekende events of navigatiecases.
|
||||
|
||||
## Fase 6 - Observability en performance check
|
||||
|
||||
Voeg tijdelijk of permanent meetpunten toe:
|
||||
|
||||
- log of dev-only counter voor hydration calls per scherm;
|
||||
- log of dev-only counter voor API `ensure*Loaded` calls;
|
||||
- React Profiler rond Product Backlog/Sprint/Solo pane containers;
|
||||
- netwerkcheck op dubbele fetches.
|
||||
|
||||
Acceptatiecriteria:
|
||||
|
||||
- Product Backlog doet bij eerste openen maximaal een server snapshot plus SSE connect, geen extra full-backlog client fetch.
|
||||
- Product en Sprint stores bevatten geen lowercase story/task statussen.
|
||||
- Solo refresh verwerkt bestaande tasks met gewijzigde velden.
|
||||
- Product Backlog, Sprint en Solo hebben per scherm precies een duidelijke eigenaar voor initial hydration.
|
||||
|
||||
## Voorgestelde implementatievolgorde
|
||||
|
||||
1. Status adapters en tests toevoegen.
|
||||
2. Product Backlog dubbele load verwijderen.
|
||||
3. Sprint active story selectie naar store verplaatsen.
|
||||
4. Solo workspace-store introduceren en hydrateren.
|
||||
5. Solo realtime/resync naar workspace-store verplaatsen.
|
||||
6. Performance/netwerk verifiëren.
|
||||
|
||||
Deze volgorde beperkt risico: eerst het data-contract, daarna de extra load, daarna gelijkvormigheid en Solo-resync.
|
||||
|
|
@ -0,0 +1,112 @@
|
|||
---
|
||||
title: "Load/render implementatie review"
|
||||
date: 2026-05-10
|
||||
status: review
|
||||
scope: ["Product Backlog", "Sprint", "Solo"]
|
||||
---
|
||||
|
||||
# Load/render implementatie review
|
||||
|
||||
## Samenvatting
|
||||
|
||||
De drie schermen zijn niet gelijkvormig opgebouwd.
|
||||
|
||||
- Product Backlog en Sprint gebruiken allebei een server-fetched snapshot, een hydration wrapper, een genormaliseerde workspace-store, SSE en directe scope-resync.
|
||||
- Solo gebruikt server props, een eigen `useSoloStore`, een globale SSE-bridge en `router.refresh()` als resync-mechanisme.
|
||||
- Product Backlog wijkt af doordat het naast server hydration ook nog via de product layout een client-side full backlog fetch start. Dat kan de lange rendering verklaren.
|
||||
- Product Backlog en Sprint hebben daarnaast een status-contract mismatch: server pages hydrateren story/task statussen als DB `UPPER_SNAKE`, maar API-resync routes geven lowercase API-statussen terug terwijl de UI maps uppercase verwachten.
|
||||
|
||||
## Bevindingen
|
||||
|
||||
### P1 - Statussen wisselen tussen uppercase en lowercase na client load/resync
|
||||
|
||||
`lib/task-status.ts` zegt expliciet dat de DB `UPPER_SNAKE` houdt en de API lowercase exposeert (`lib/task-status.ts:1-2`). De API mapt bijvoorbeeld `TO_DO -> todo` en `OPEN -> open` (`lib/task-status.ts:12-35`).
|
||||
|
||||
De server-render paden hydrateren story/task statussen echter direct uit Prisma:
|
||||
|
||||
- Product Backlog stories/tasks blijven uppercase in `app/(app)/products/[id]/page.tsx:86-98`.
|
||||
- Sprint stories/tasks blijven uppercase in `app/(app)/products/[id]/sprint/[sprintId]/page.tsx:94-125`.
|
||||
|
||||
De client load/resync paden mappen dezelfde data naar lowercase:
|
||||
|
||||
- Product Backlog full snapshot: `app/api/products/[id]/backlog/route.ts:80-99`.
|
||||
- PBI stories: `app/api/pbis/[id]/stories/route.ts:49-50`.
|
||||
- Story tasks: `app/api/stories/[id]/tasks/route.ts:46-48`.
|
||||
- Sprint workspace snapshot: `app/api/sprints/[id]/workspace/route.ts:71-108`.
|
||||
|
||||
De UI verwacht voor stories/tasks juist uppercase:
|
||||
|
||||
- Backlog stories: `components/backlog/story-panel.tsx:41-50`.
|
||||
- Backlog tasks: `components/backlog/task-panel.tsx:42-53`.
|
||||
- Sprint stories: `components/sprint/sprint-backlog.tsx:33-38`.
|
||||
- Sprint tasks: `components/sprint/task-list.tsx:33-54`.
|
||||
|
||||
Impact: na een client fetch of resync kunnen labels, kleuren, filters en status-cycles anders of leeg renderen. In Sprint is dit extra riskant omdat `STATUS_CYCLE[task.status]` bij lowercase statussen terugvalt naar `TO_DO`.
|
||||
|
||||
Aanpak: kies een intern store-contract. Het meest consistent met de bestaande UI is: DB-uppercase in de workspace-stores houden, en API lowercase alleen aan de route/API-boundary gebruiken. Converteer API-responses dus terug naar DB-statussen voordat ze in Product/Sprint workspace stores landen, of pas alle UI maps en acties consequent aan op API-statussen.
|
||||
|
||||
### P1 - Product Backlog doet een dubbele full backlog load
|
||||
|
||||
Product Backlog haalt op de server al alle PBI's, stories en tasks op (`app/(app)/products/[id]/page.tsx:47-84`) en hydrateert die in de client via `BacklogHydrationWrapper` (`components/backlog/backlog-hydration-wrapper.tsx:60-67`).
|
||||
|
||||
Tegelijkertijd mount de product layout altijd `SetCurrentProduct` (`app/(app)/products/[id]/layout.tsx:19-22`). Die roept `setActiveProduct` aan (`components/shared/set-current-product.tsx:10-14`). `setActiveProduct` start altijd `ensureProductLoaded`, en die fetcht opnieuw de volledige backlog via `/api/products/:id/backlog` (`stores/product-workspace/store.ts:217-257`, `stores/product-workspace/store.ts:329-345`).
|
||||
|
||||
Impact: op Product Backlog komt na de server render nog een client full-backlog API-call en store hydration. Dat veroorzaakt extra werk, extra renders, en door de status mismatch hierboven kan de tweede load de net gehydrateerde uppercase data overschrijven met lowercase data.
|
||||
|
||||
Aanpak: maak server hydration en client ensure geen dubbele eigenaren van dezelfde initial load. Bijvoorbeeld:
|
||||
|
||||
- `SetCurrentProduct` alleen context laten zetten zonder `ensureProductLoaded` wanneer de route zelf een snapshot hydrateert.
|
||||
- Of `BacklogHydrationWrapper` ook `activeProduct` zetten en `loadedProductId` markeren, waarna `setActiveProduct`/`ensureProductLoaded` guarded wordt.
|
||||
- Of Product Backlog hetzelfde patroon geven als Sprint: wrapper hydrateert snapshot en zet de actieve context direct.
|
||||
|
||||
### P1 - Solo resync werkt niet voor bestaande taken met dezelfde ids
|
||||
|
||||
`useSoloRealtime` gebruikt `router.refresh()` om gemiste events na reconnect/visible/online op te halen (`lib/realtime/use-solo-realtime.ts:96-104`, `lib/realtime/use-solo-realtime.ts:190-205`). De comment zegt dat server props opnieuw binnenkomen en `initTasks` de store reset.
|
||||
|
||||
Maar `SoloBoard` roept `initTasks(initialTasks)` alleen opnieuw aan als de lijst task-ids verandert:
|
||||
|
||||
- `const taskKey = initialTasks.map(t => t.id).join(',')` (`components/solo/solo-board.tsx:79`)
|
||||
- effect dependency is alleen `[taskKey]` (`components/solo/solo-board.tsx:80-83`)
|
||||
- `initTasks` vervangt de store (`stores/solo-store.ts:105-106`)
|
||||
|
||||
Impact: als een gemist event alleen status, titel, sort_order, plan of andere velden wijzigt, en de task-id set gelijk blijft, dan doet de refresh niets in de solo-store. Het scherm blijft stale ondanks de resync.
|
||||
|
||||
Aanpak: gebruik een volledige fingerprint van de render-relevante velden, of hydrateer de store op iedere nieuwe `initialTasks` prop. Als renderperformance een zorg is, maak de fingerprint expliciet met `id`, `status`, `sort_order`, `title`, story metadata en planvelden.
|
||||
|
||||
### P2 - Solo sync't openstaande stories niet na refresh
|
||||
|
||||
`SoloBoard` initialiseert `unassignedStories` eenmalig uit props (`components/solo/solo-board.tsx:66`). De knop en sheet renderen daarna vanuit lokale state (`components/solo/solo-board.tsx:220-225`, `components/solo/solo-board.tsx:278-284`).
|
||||
|
||||
Impact: als `router.refresh()` nieuwe unassigned stories ophaalt, wordt de lokale state niet bijgewerkt. Het aantal en de sheet kunnen stale blijven.
|
||||
|
||||
Aanpak: sync `initialUnassigned` via een effect/fingerprint, of maak unassigned stories onderdeel van dezelfde hydrateerbare solo-store.
|
||||
|
||||
### P2 - Sprint gebruikt niet hetzelfde active-context patroon als Product Backlog
|
||||
|
||||
Product Backlog selecteert PBI/story/task via de workspace-store context. `TaskPanel` leest bijvoorbeeld `context.activeStoryId` en `selectTasksForActiveStory` (`components/backlog/task-panel.tsx:108-115`).
|
||||
|
||||
Sprint hydrateert wel een sprint workspace-store, maar de geselecteerde story staat lokaal in `SprintBoardClient`:
|
||||
|
||||
- `useState<string | null>(null)` voor `selectedStoryId` (`components/sprint/sprint-board-client.tsx:66`)
|
||||
- selectie wordt als prop doorgegeven (`components/sprint/sprint-board-client.tsx:238-257`)
|
||||
- `TaskList` leest tasks via `selectTasksForStory(s, storyId)`, niet via de actieve store-context (`components/sprint/task-list.tsx:161-164`)
|
||||
|
||||
De sprint-store heeft wel `setActiveStory`, `selectTasksForActiveStory` en resync van `activeStoryId`, maar het scherm gebruikt dat pad niet (`stores/sprint-workspace/store.ts:305-327`, `stores/sprint-workspace/store.ts:458-466`).
|
||||
|
||||
Impact: Sprint werkt deels, maar is niet gelijkvormig met Product Backlog. De restore-hints en active-scope resync voor story/task zijn in dit scherm praktisch omzeild.
|
||||
|
||||
Aanpak: zet story-selectie in de sprint workspace-store en laat `TaskList` dezelfde active-context selector gebruiken als Product Backlog, of verwijder de ongebruikte active story/task mechanismen uit de sprint-store.
|
||||
|
||||
## Vergelijking per scherm
|
||||
|
||||
| Scherm | Initial load | Client hydration | Realtime/resync | Selectie/render patroon |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| Product Backlog | Server haalt full backlog op | `BacklogHydrationWrapper` hydrateert product workspace-store | SSE + `resyncActiveScopes` | Store-context voor actieve PBI/story/task |
|
||||
| Sprint | Server haalt sprint snapshot op | `SprintHydrationWrapper` hydrateert sprint workspace-store en zet context | SSE + `resyncActiveScopes` | Sprint/story lijst uit store, maar story selectie lokaal |
|
||||
| Solo | Server haalt solo props op | `SoloBoard` init `useSoloStore` via effect op task-ids | Globale SSE + `router.refresh()` | Eigen store voor tasks, lokale state voor unassigned stories |
|
||||
|
||||
## Conclusie
|
||||
|
||||
De lange rendering is waarschijnlijk niet door `debug_id` op zichzelf veroorzaakt. De meest concrete render/load oorzaak zit in Product Backlog: server snapshot plus een tweede client-side full backlog load via `SetCurrentProduct`. Daarnaast zorgt de status-contract mismatch ervoor dat die tweede load en latere resyncs een andere datastructuur in dezelfde UI stoppen.
|
||||
|
||||
De schermen zijn functioneel verwant, maar niet gelijkvormig geimplementeerd. Product Backlog en Sprint moeten eerst hetzelfde status- en hydration-contract krijgen. Daarna kan Solo naar hetzelfde patroon groeien, of minimaal zijn `router.refresh()`-hydratie correct laten doorwerken op bestaande tasks en unassigned stories.
|
||||
Loading…
Add table
Add a link
Reference in a new issue