Scrum4Me/docs/architecture/product-backlog-workflow.md
Madhura68 cb5150c2ae docs(T-1014): PB-workflow doc — skelet + as-is architectuur-lagen en stores
Eerste laag van het Product Backlog page workflow-doc (PBI-88 / ST-1363):
frontmatter, Context & scope, de architectuur-lagen (PG-triggers -> SSE ->
Zustand -> React) en de drie voedende Zustand-stores.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 22:14:54 +02:00

7.6 KiB


Product Backlog page — workflow & states (PBI-88)

Eén autoritatieve beschrijving van het Product Backlog-scherm: de architectuur-lagen, de impliciete workflow-states, en — als doelbeeld — een expliciete state machine met gap-analyse.

Context & scope

De Product Backlog page (app/(app)/products/[id]/page.tsx) is het scherm waar een gebruiker de PBI's, stories en taken van één product beheert en — sinds PBI-79 — een nieuwe sprint samenstelt zónder het scherm te verlaten. De layout zelf (3-pane split: PBI's · Stories · Taken) staat in functional.md (F-04..F-06); dit doc beschrijft het gedrag eromheen.

Waarom dit doc bestaat: het scherm kent vandaag een handvol impliciete workflow-states die ad-hoc worden afgeleid uit losse SSR-flags en store-flags. Een samenhangende beschrijving ontbrak — kennis zat verspreid over functional.md (alleen layout), de patterns-docs en project-structure.md. Dit doc bundelt die kennis en zet er een doelbeeld naast.

Grens met de sprint-execution-page. Dit scherm gaat over backlog-beheer + sprint-samenstelling. Zodra een sprint draait, verschuift het werk naar de sprint-execution-flow — zie sprint-execution-modes.md. De CLOSED/ARCHIVED-levenscyclus van een sprint valt buiten dit scherm.

Architectuur-lagen (as-is)

Het scherm volgt het lagenmodel PostgreSQL-triggers → SSE → Zustand → React. Belangrijk: dit is geen toekomstplan — het draait al. De lagen, van onder naar boven:

1. PostgreSQL NOTIFY-triggers

Row-level AFTER INSERT/UPDATE/DELETE-triggers emitteren een JSON-payload op het hardcoded kanaal scrum4me_changes:

Tabel Trigger / functie Migratie
tasks tasks_notify_changenotify_task_change() 20260426230316_add_solo_realtime_triggers
stories stories_notify_changenotify_story_change() 20260426230316_add_solo_realtime_triggers
pbis NOTIFY-trigger op pbis 20260502190200_add_pbi_notify_trigger

De payload bevat minimaal { op: 'I'|'U'|'D', entity, id, product_id, … } en bij UPDATE een changed_fields-array. Latere migraties breiden de payload uit (20260427000216_extend_realtime_payload) en voegen story_logs-notificaties toe (20260506001700_story_logs_notify). Volledig payload-contract: realtime-notify-payload.md.

2. SSE-route

app/api/realtime/backlog/route.ts opent een pg.Client op DIRECT_URL (pooler-bypass), doet LISTEN scrum4me_changes en streamt via een ReadableStream:

  • Filter (shouldEmit): events met een type-veld (job/worker) worden genegeerd; alleen entity ∈ {pbi, story, task} met matchend product_id gaat door.
  • Heartbeat elke 25s (: heartbeat), hard-close na 240s (Next-maxDuration is 300s) — de client reconnect.
  • Auth via iron-session; demo-users mogen meelezen.

De sprint-board heeft een eigen route met dezelfde opzet: app/api/realtime/sprint/route.ts.

3. Zustand-store

De client-hook useBacklogRealtime (lib/realtime/use-backlog-realtime.ts), gemount in BacklogHydrationWrapper, opent een EventSource naar de SSE-route en:

  • dispatcht elk event naar useProductWorkspaceStore.applyRealtimeEvent();
  • beheert exponential backoff (1s → 30s) bij onerror, en reconnect alleen als de tab visible is;
  • triggert bij een latere ready (post-reconnect) resyncActiveScopes('reconnect'), zodat events die tijdens een disconnect gemist zijn alsnog binnenkomen.

applyRealtimeEvent (stores/product-workspace/store.ts) filtert op product_id, stuurt onbekende entities naar resyncActiveScopes('unknown-event') en dispatcht bekende events naar applyPbiEvent / applyStoryEvent / applyTaskEvent: idempotente upsert + sort, parent-move bij gewijzigd pbi_id/story_id, cleanup bij DELETE.

4. React

Componenten lezen via selectors uit de store en re-renderen op mutaties. De SSR-render (page.tsx) levert de initiële snapshot + sprint-switcher-data; daarna is de store leidend en haalt router.refresh() na een server-action de verse SSR-render op.

Kernconclusie

Het lagenmodel uit het architectuurvoorstel draait al: triggers, SSE, stores met applyRealtimeEvent, en backoff/reconnect/resync zijn aanwezig. SSE fungeert hier puur als sync-mechanisme, niet als UI-bestuurder — de stream zegt alleen "er is iets veranderd aan X", de store bepaalt de betekenis. Wat ontbreekt zit niet in deze lagen, maar in de state-modellering erbovenop; dat is het onderwerp van de volgende secties.

Stores (as-is)

Drie Zustand-stores voeden dit scherm. De opdeling volgt het bounded-context-patroon uit PBI-74 (één store per coherente workflow — niet per pagina, geen megastore), vastgelegd in workspace-store.md. Dit doc bouwt op die opdeling voort; het herstructureert die niet.

product-workspacestores/product-workspace/store.ts

De hoofdstore van dit scherm: PBI's, stories en taken van de backlog. Slices:

Slice Inhoud
context activeProduct, activePbiId, activeStoryId, activeTaskId — de cascade-selectie
entities pbisById, storiesById, tasksById — genormaliseerde entity-maps
relations pbiIds, storyIdsByPbi, taskIdsByStory — gesorteerde id-lijsten
loading loaded*Ids, activeRequestId — race-safe markers voor ensure*Loaded
sync realtimeStatus, lastEventAt, lastResyncAt, resyncReason — SSE-connectiestatus
pendingMutations rollback-snapshots voor optimistische DnD/patch-mutaties
sprintMembership pbiSummary, crossSprintBlocks, pending: { adds, removes }, loadedSummaryForSprintId — de sprint-samenstel-laag (PBI-79)

De sprintMembership-slice is uniek voor dit scherm: hij houdt bij welke stories de gebruiker in/uit de actieve sprint cherry-pickt (toggleStorySprintMembershippending), met applyMembershipCommitResult als gericht patch-pad ná de server-action-commit.

sprint-workspacestores/sprint-workspace/store.ts

Spiegelbeeld voor de sprint-board: stories/taken binnen één sprint. Zelfde slice-vorm, maar context heeft activeSprintId i.p.v. activePbiId, en relations is per-sprint georganiseerd (storyIdsBySprint). Op de Product Backlog page niet direct gebruikt, maar relevant voor de grens met de sprint-flow.

user-settingsstores/user-settings/store.ts

Gebruikersvoorkeuren + cross-tab sync. Voor dit scherm cruciaal: workflow.pendingSprintDraft[productId] — de concept-sprint die ontstaat bij "Nieuwe sprint". Sinds PBI-79 is die draft session-only: setPendingSprintDraft / clearPendingSprintDraft schrijven alleen lokaal (geen server-roundtrip), en hydrate() stript legacy DB-entries weg zodat de draft niet "spookt". upsertPbiIntent / upsertStoryOverride muteren de draft-selectie.

Slice-details van het workspace-patroon (ensure*Loaded, selectors, optimistic mutations, gotchas) staan in workspace-store.md — hier niet gedupliceerd.