* 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
8.7 KiB
| title | date | status | scope | |||
|---|---|---|---|---|---|---|
| Load/render implementatie review | 2026-05-10 | review |
|
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 enrouter.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:
SetCurrentProductalleen context laten zetten zonderensureProductLoadedwanneer de route zelf een snapshot hydrateert.- Of
BacklogHydrationWrapperookactiveProductzetten enloadedProductIdmarkeren, waarnasetActiveProduct/ensureProductLoadedguarded 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) initTasksvervangt 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)voorselectedStoryId(components/sprint/sprint-board-client.tsx:66)- selectie wordt als prop doorgegeven (
components/sprint/sprint-board-client.tsx:238-257) TaskListleest tasks viaselectTasksForStory(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.