From cb5150c2ae4413e448220c9629d14c8291fadf19 Mon Sep 17 00:00:00 2001 From: Madhura68 Date: Thu, 14 May 2026 22:14:54 +0200 Subject: [PATCH] =?UTF-8?q?docs(T-1014):=20PB-workflow=20doc=20=E2=80=94?= =?UTF-8?q?=20skelet=20+=20as-is=20architectuur-lagen=20en=20stores?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- docs/INDEX.md | 1 + docs/architecture/product-backlog-workflow.md | 94 +++++++++++++++++++ 2 files changed, 95 insertions(+) create mode 100644 docs/architecture/product-backlog-workflow.md diff --git a/docs/INDEX.md b/docs/INDEX.md index 1c5a0a7..258f807 100644 --- a/docs/INDEX.md +++ b/docs/INDEX.md @@ -92,6 +92,7 @@ Auto-generated on 2026-05-14 from front-matter and headings. | [Claude ↔ User Question Channel](./architecture/claude-question-channel.md) | `architecture/claude-question-channel.md` | active | 2026-05-03 | | [Data Model & Prisma Schema](./architecture/data-model.md) | `architecture/data-model.md` | active | 2026-05-08 | | [Scrum4Me — Architecture Overview](./architecture/overview.md) | `architecture/overview.md` | active | 2026-05-08 | +| [Product Backlog page — workflow & states](./architecture/product-backlog-workflow.md) | `architecture/product-backlog-workflow.md` | active | 2026-05-14 | | [Project Structure, Stores, Realtime & Job Queue](./architecture/project-structure.md) | `architecture/project-structure.md` | active | 2026-05-08 | | [QR-pairing Login Flow](./architecture/qr-pairing.md) | `architecture/qr-pairing.md` | active | 2026-05-03 | | [Sprint execution modes — PER_TASK vs SPRINT_BATCH](./architecture/sprint-execution-modes.md) | `architecture/sprint-execution-modes.md` | active | 2026-05-07 | diff --git a/docs/architecture/product-backlog-workflow.md b/docs/architecture/product-backlog-workflow.md new file mode 100644 index 0000000..70a194e --- /dev/null +++ b/docs/architecture/product-backlog-workflow.md @@ -0,0 +1,94 @@ +--- +title: "Product Backlog page — workflow & states" +status: active +audience: [maintainer, contributor] +language: nl +last_updated: 2026-05-14 +related: [project-structure.md](./project-structure.md), [sprint-execution-modes.md](./sprint-execution-modes.md) +--- + +# 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](../specs/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](./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](./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_change` → `notify_task_change()` | `20260426230316_add_solo_realtime_triggers` | +| `stories` | `stories_notify_change` → `notify_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](../patterns/realtime-notify-payload.md). + +### 2. SSE-route + +[app/api/realtime/backlog/route.ts](../../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](../../app/api/realtime/sprint/route.ts). + +### 3. Zustand-store + +De client-hook `useBacklogRealtime` ([lib/realtime/use-backlog-realtime.ts](../../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](../../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](../patterns/workspace-store.md). Dit doc bouwt op die opdeling voort; het herstructureert die niet. + +### `product-workspace` — [stores/product-workspace/store.ts](../../stores/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 (`toggleStorySprintMembership` → `pending`), met `applyMembershipCommitResult` als gericht patch-pad ná de server-action-commit. + +### `sprint-workspace` — [stores/sprint-workspace/store.ts](../../stores/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-settings` — [stores/user-settings/store.ts](../../stores/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](../patterns/workspace-store.md) — hier niet gedupliceerd.