diff --git a/docs/INDEX.md b/docs/INDEX.md index 17e34ae..3848845 100644 --- a/docs/INDEX.md +++ b/docs/INDEX.md @@ -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 | diff --git a/docs/plans/load-render-improvement-plan-2026-05-10.md b/docs/plans/load-render-improvement-plan-2026-05-10.md new file mode 100644 index 0000000..a47c665 --- /dev/null +++ b/docs/plans/load-render-improvement-plan-2026-05-10.md @@ -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. diff --git a/docs/recommendations/load-render-implementation-review-2026-05-10.md b/docs/recommendations/load-render-implementation-review-2026-05-10.md new file mode 100644 index 0000000..9d32f14 --- /dev/null +++ b/docs/recommendations/load-render-implementation-review-2026-05-10.md @@ -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(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.