Scrum4Me/docs/recommendations/load-render-implementation-review-2026-05-10.md
Janpeter Visser 3b5cee823c
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
2026-05-10 07:34:58 +02:00

8.7 KiB

title date status scope
Load/render implementatie review 2026-05-10 review
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.