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