--- 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.