* feat(PBI-79/ST-1333): active-sprint null-contract + clearActiveSprintAction
- lib/user-settings.ts: activeSprints values nullable in Zod-schema.
Key-aanwezigheid heeft nu betekenis (key+null = bewust geen sprint;
key ontbreekt = fallback-cascade).
- lib/active-sprint.ts: nieuwe readStoredActiveSprintState helper +
resolveActiveSprint respecteert expliciet 'cleared' state zonder fallback.
clearActiveSprintInSettings schrijft null i.p.v. de key te verwijderen.
- actions/active-sprint.ts: nieuwe clearActiveSprintAction met auth +
membership-check.
- components/shared/sprint-switcher.tsx: '— Geen actieve sprint —'-optie
in dropdown, disabled wanneer er geen actieve sprint is.
- Tests: nieuwe active-sprint.test.ts (resolver-paden + clear),
active-sprint-action.test.ts (action-laag), uitbreiding user-settings.test.ts.
Plan: docs/plans/PBI-79-backlog-sprint-workflow.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(PBI-79/ST-1334): user-settings pendingSprintDraft-slot
- lib/user-settings.ts: nieuw workflow.pendingSprintDraft veld met
compacte intent-shape (pbiIntent + per-PBI storyOverrides).
- actions/sprint-draft.ts: setPendingSprintDraftAction +
clearPendingSprintDraftAction met product-membership-check + Zod-validatie.
- stores/user-settings/store.ts: setPendingSprintDraft / clearPendingSprintDraft
optimistic acties + fine-grained mutators upsertPbiIntent / upsertStoryOverride.
Sprint-draft actions worden dynamisch geïmporteerd zodat jsdom-tests
zonder DATABASE_URL niet falen.
- Tests: nieuwe sprint-draft.test.ts (action-laag), uitbreiding
user-settings store-tests (5 nieuwe cases) en schema-tests (4 cases).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(PBI-79/ST-1343): sprint-conflicts helper-library
- lib/sprint-conflicts.ts: drie pure/server-side helpers voor eligibility
+ cross-sprint detectie.
- isEligibleForSprint(story): sprint_id IS NULL en status != DONE
- partitionByEligibility(prisma, storyIds, excludeSprintId): split in
eligible / notEligible / crossSprint met reden per story
- getBlockingSprintMap(prisma, productId, storyIds, excludeSprintId):
map storyId → { sprintId, sprintName } voor stories in andere OPEN sprint
- Tests: __tests__/lib/sprint-conflicts.test.ts (16 cases) — alle eligibility
paden + cross-sprint scoping + CLOSED-sprint filtering.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(PBI-79/ST-1335): sprint-membership-summary + cross-sprint-blocks endpoints
Twee nieuwe GET-route handlers, beide verplicht gescoped op pbiIds (geen
product-brede aanroepen).
- app/api/products/[id]/sprint-membership-summary/route.ts
Response: { [pbiId]: { total, inSprint } } via twee prisma.groupBy calls
(totaal + binnen actieve sprint). Voor state-B tri-state.
- app/api/products/[id]/cross-sprint-blocks/route.ts
Response: { [storyId]: { sprintId, sprintName } } voor stories in andere
OPEN sprints. UX-hint voor disabled-vinkjes; commit-acties blijven
autoritatief.
Tests: 13 cases dekken happy path, 400 zonder pbiIds, 400 zonder sprintId,
404 zonder product-access, auth-fail, en NOT-clause voor excludeSprintId.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(PBI-79/ST-1336): product-workspace sprint-membership slice + selectors
Datalaag voor de vinkje-UI van state A′ en state B.
types.ts:
- PbiSummaryEntry, CrossSprintBlock, SprintMembershipSlice toegevoegd.
store.ts:
- Nieuwe slice `sprintMembership` met pbiSummary, crossSprintBlocks,
pending: { adds[], removes[] }, loadedSummaryForSprintId.
- Acties: setPbiSummary, setCrossSprintBlocks, toggleStorySprintMembership
(cancel-out logic), resetSprintMembershipPending, fetchSprintMembershipSummary,
fetchCrossSprintBlocks.
- hydrateSnapshot reset óók de membership-slice.
selectors.ts:
- selectPbiTriState (aggregate-only zolang stories niet geladen; rekent
pending mee bij loaded PBI's).
- selectStoryEffectiveInSprint (DB ⊕ pending).
- selectStoryIsBlocked (cross-sprint hint).
- selectIsDirty, selectPendingCount.
Tests: 25 cases in nieuwe sprint-membership.test.ts dekken alle selector-
paden, toggle-cancel-out, fetch-helpers, en pbiId-scoping.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(PBI-79/ST-1337): state A′ UI — metadata dialog + sticky banner + PbiList ombouw
UI-laag voor de sprint-definitie-flow (state A′).
Nieuw:
- NewSprintMetadataDialog (stap 1): sprint_goal + optionele dates;
'Verder' schrijft via useUserSettingsStore.setPendingSprintDraft.
- SprintDefinitionBanner (sticky): toont doel + X PBI's / Y stories teller;
'Annuleren' → AlertDialog confirm → clearPendingSprintDraft;
'Sprint aanmaken' nog niet aangesloten (wacht op ST-1339).
- NewSprintTrigger: button in page header die de metadata-dialog opent;
verbergt zichzelf zolang er al een draft loopt.
- SprintDraftBanner: client-wrapper, rendert banner alleen als draft bestaat.
Wijzigingen:
- lib/user-settings.ts: pendingSprintDraft startAt/endAt → z.string().date().
- PbiList: oude selectionMode + selectedIds + NewSprintDialog vervangen door
hasDraft-afgeleide A′-mode met tri-state vinkjes; togglen muteert
upsertPbiIntent('all'|'none') en wist storyOverrides per PBI.
- StoryPanel: in A′-mode toont elke story een cherrypick-checkbox die
upsertStoryOverride('add'/'remove'/'clear') aanroept; cross-sprint-blocked
stories krijgen disabled-icoon met sprint-naam tooltip.
- app/(app)/products/[id]/page.tsx: StartSprintButton vervangen door
NewSprintTrigger; SprintDraftBanner gepositioneerd boven split-pane.
Tests: bestaande tests blijven groen (806 cases) — UI-specifieke component
tests volgen later. ST-1339 sluit createSprintWithSelectionAction aan.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(PBI-79/ST-1339): createSprintWithSelectionAction + banner wire-up
actions/sprints.ts:
- Nieuwe createSprintWithSelectionAction(productId, metadata, pbiIntent,
storyOverrides).
- Server-side intent-resolve:
1. Voor elke PBI met intent='all': fetch child-story-IDs minus
storyOverrides[pbi].remove.
2. Plus storyOverrides[*].add (cross-PBI cherrypick toegestaan).
- Eligibility-filter via partitionByEligibility (sprint_id IS NULL + status
!= DONE; stories in andere OPEN sprint → conflicts.crossSprint).
- Transactie wrapt sprint.create + story.updateMany (status='IN_SPRINT') +
task.updateMany (sprint_id cascade) — alles atomair.
- setActiveSprintInSettings na success.
- Return: { success, sprintId, affectedStoryIds, affectedPbiIds,
affectedTaskIds, conflicts: { notEligible, crossSprint } } of error.
components/backlog/sprint-definition-banner.tsx:
- 'Sprint aanmaken'-knop sluit aan op createSprintWithSelectionAction;
toast bij conflicts, success-toast anders, router.refresh() voor SSR
cycle. Pending draft wordt door de action zelf nog niet expliciet gewist
— dat gebeurt via revalidatePath en kan in ST-1340 finetuned worden.
Tests: __tests__/actions/create-sprint-with-selection.test.ts (6 cases)
dekken intent-resolve, override-respect, cross-sprint conflict, transactie-
binding van story.status + task.sprint_id, return-shape, en error-pad.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(PBI-79/ST-1340): commitSprintMembershipAction + gerichte client-store patches
actions/sprints.ts:
- Nieuwe commitSprintMembershipAction(activeSprintId, adds[], removes[]).
- Eligibility-filter voor adds via partitionByEligibility (sprint_id IS NULL
en niet DONE; cross-sprint conflicts → notEligible).
- Race-safety voor removes: alleen stories met huidige sprint_id ==
activeSprintId; rest → conflicts.alreadyRemoved.
- Transactie wrapt twee updateMany-paren (story status mee, task.sprint_id
cascade). Update-paren overgeslagen wanneer leeg.
- Return: { success, affectedStoryIds, affectedPbiIds, affectedTaskIds,
conflicts: { notEligible, alreadyRemoved } }.
stores/product-workspace/store.ts:
- applyMembershipCommitResult({ activeSprintId, addedStoryIds,
removedStoryIds }) patcht entities.storiesById met juiste sprint_id +
status; ledigt sprintMembership.pending. Geen task-veld omdat
BacklogTask geen sprint_id-kolom heeft in de store.
Tests: __tests__/actions/commit-sprint-membership.test.ts (8 cases) — happy
path, DONE-conflict, cross-sprint, race-safety voor removes, transactie-
inhoud (status='IN_SPRINT'/'OPEN'), task-cascade, return-shape, auth-fail.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(PBI-79/ST-1338): state B vinkjes-UI + 'Sprint opslaan'-knop met teller
State B (actieve sprint geselecteerd, geen draft) hangt nu aan dezelfde
vinkje-UI als state A′, maar muteert de transient pending-buffer in plaats
van de draft.
- PbiList: nieuwe prop activeSprintId. selectionMode = hasDraft ||
stateBMode. togglePbiInDraft routeert naar upsertPbiIntent (A′) of bulk-
toggleStorySprintMembership over eligible child-stories (B, skip blocked).
- StoryPanel: idem prop activeSprintId. StoryBlockWithCherrypick muteert
draft via upsertStoryOverride in A′ of pending buffer via
toggleStorySprintMembership in B (cross-sprint blocked = disabled).
- SaveSprintButton (nieuw): client component in page header, alleen
zichtbaar als er een actieve sprint is. Disabled bij clean buffer,
enabled met teller bij dirty. Klikken calls commitSprintMembershipAction
→ applyMembershipCommitResult gericht in store + toast bij conflicts.
- page.tsx: activeSprintItem.id wordt doorgegeven aan PbiList, StoryPanel
en SaveSprintButton.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(PBI-79/ST-1341+ST-1342): SprintEditDialog metadata-edit + multi-OPEN sprints
ST-1341 (T-946):
- actions/sprints.ts: nieuwe updateSprintAction(sprintId, fields) — JSON
input, accepteert optionele goal/startAt/endAt; auth + product-access
check, prisma.sprint.update, revalidatePath. Type-safe return.
- components/backlog/sprint-edit-dialog.tsx: Entity-Dialog-pattern voor
metadata-edit van een sprint. Velden: sprint_goal, start_date, end_date.
Link 'Sprint afronden… →' naar bestaande /products/[id]/sprint/[sprintId]
zodat de completion-flow (per-story DONE/OPEN beslissing + PBI-promotie)
niet wordt geduplicereerd. useDirtyCloseGuard.
ST-1342 (T-947):
- actions/sprints.ts: OPEN-uniqueness check in createSprintAction
verwijderd. Een product mag nu meerdere OPEN sprints tegelijk hebben;
cross-sprint-conflicts per story worden afgevangen door
partitionByEligibility in de membership-commit-flow.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(PBI-79/ST-1344): updateSprintAction regression coverage
Audits van de geplande non-regressie-tests laten zien dat alle invarianten
uit het ST-1344 plan reeds gedekt zijn door eerder toegevoegde tests:
- clearActiveSprintAction null-not-delete → __tests__/lib/active-sprint.test.ts
+ __tests__/actions/active-sprint-action.test.ts
- Endpoints rejecten zonder pbiIds (400) → __tests__/api/sprint-membership-summary.test.ts
+ __tests__/api/cross-sprint-blocks.test.ts
- Status-mutaties story.status=IN_SPRINT/OPEN met task.sprint_id cascade
in dezelfde transactie → __tests__/actions/create-sprint-with-selection.test.ts
+ __tests__/actions/commit-sprint-membership.test.ts
- Cross-sprint conflicts + DONE-eligibility → __tests__/lib/sprint-conflicts.test.ts
Nieuw: __tests__/actions/update-sprint.test.ts (6 cases) dekt
updateSprintAction die nog geen tests had — goal alleen, dates alleen,
null-clear, 403 zonder access, lege goal weigering, leeg fields-object
weigering.
Handmatige E2E checklist (T-949) blijft staan voor menselijke browser-
validatie tijdens PR-review.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(PBI-79): PBI-rij selecteert weer in A′/B-modus; vinkje is aparte trigger
Voor PBI-79 maakte het hele PBI-kaartje in selectionMode (state A′ én B)
de toggle. Daardoor:
- klik op rij = bulk-toggle stories (teller liep op);
- geen setActivePbi, dus StoryPanel kreeg geen content.
Fix: in selectionMode wordt onClick = onSelect (PBI activeren → stories
laden) en de tri-state-iconen verhuizen naar een eigen <button> in de
actions-slot met stopPropagation. Toggle gedrag (bulk add/remove in B,
upsertPbiIntent in A′) blijft ongewijzigd via die knop.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(PBI-79): cascade-restore alleen als hint-story bij nieuwe PBI hoort
Bug: setActivePbi reset activeStoryId/activeTaskId, maar het cascade-
restore-pad zette daarna een hint-story actief zonder te valideren of die
story bij de nieuw-geselecteerde PBI hoort. Bij PBI-switch bleef daardoor
de task-kolom de taken van de vorige story tonen.
Fix: alleen setActiveStory(hint) als entities.storiesById[hint].pbi_id ===
pbiId. Bij mismatch blijft activeStoryId null en is de task-kolom leeg
totdat de gebruiker een story uit de nieuwe PBI kiest.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(PBI-79): sprint-switch auto-select PBI/story + user-settings persist
Bij sprint-switch wordt de sprint-content server-side opgevraagd. Wanneer
de sprint precies één PBI (en die PBI exact één story binnen de sprint)
heeft, worden PBI en story automatisch geselecteerd. Alle drie keuzes
(sprint, pbi, story) worden atomair in user-settings opgeslagen zodat ze
cross-device blijven hangen.
- lib/user-settings.ts: layout krijgt nullable activePbis +
activeStories per product.
- lib/active-sprint.ts: setActiveSelectionInSettings schrijft de drie
keys atomair + notify pg_notify.
- actions/active-sprint.ts: switchActiveSprintAction(productId, sprintId)
doet de server-side auto-select-resolutie (single PBI → single story)
en returnt { sprintId, pbiId, storyId }.
- components/shared/sprint-switcher.tsx: handleSwitchSprint roept de
nieuwe action aan en synchroniseert de workspace-store gelijk zodat
de UI geen flash krijgt voor de SSR-refresh.
- components/backlog/active-selection-hydrator.tsx (nieuw): client-side
effect dat user-settings.activePbis/activeStories naar workspace-store
spiegelt; wint van de localStorage hint-restore.
- app/(app)/products/[id]/page.tsx: ActiveSelectionHydrator gemount
binnen BacklogHydrationWrapper.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(PBI-79): plan-update met implementatie-stand + scope-aanpassing
Documenteert wat er sinds de eerste implementatie-pass is gebeurd:
- Tabel van 14 commits met hun rol.
- Twee bugs die tijdens testen boven kwamen (PBI-rij-klik, cascade-restore).
- Nieuwe feature sprint-switch auto-select (server resolveert single-PBI/
single-story; user-settings persist).
En kondigt scope-aanpassing aan voor de volgende implementatie-ronde:
- pendingSprintDraft wordt session-only (geen server-persist meer).
- useDirtyCloseGuard wist draft op leave-with-confirm.
- Sprint-switcher krijgt concept-entry zolang er een draft loopt.
De rest van het plan beneden blijft van kracht behalve waar deze sectie
het overruled.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(PBI-79): pendingSprintDraft session-only + concept-entry + leave-guard
Scope-aanpassing uit plan-revisie: drafts persisten niet meer server-side.
Wijzigingen:
- stores/user-settings/store.ts:
- hydrate() strip nu workflow.pendingSprintDraft uit serverstate
(legacy DB-entries blijven harmless aanwezig maar worden niet
gehydreerd → effectief unreachable voor de UI).
- setPendingSprintDraft / clearPendingSprintDraft worden lokale-only;
geen import van sprint-draft-actions, geen server-roundtrip.
- upsertPbiIntent / upsertStoryOverride blijven via setPendingSprintDraft
routeren → ook session-only.
- components/shared/sprint-switcher.tsx: leest draft-goal uit user-settings
store en toont '⚙ Concept — [goal]' als niet-selecteerbare entry
bovenaan de dropdown zolang er een draft loopt.
- components/backlog/sprint-draft-leave-guard.tsx (nieuw): registreert
een beforeunload-listener zolang er een draft is. Browser-refresh,
tab-close en back-navigatie tonen daarmee de standaard confirm. In-app
route-changes blijven via de banner-Annuleren-knop lopen.
- app/(app)/products/[id]/page.tsx: SprintDraftLeaveGuard gemount naast
de banner.
- Tests: user-settings store-tests aangepast (geen server-call assert
meer, hydrate strip-assert toegevoegd; upsert-tests seed nu via
setPendingSprintDraft i.p.v. legacy hydrate).
setPendingSprintDraftAction + clearPendingSprintDraftAction blijven bestaan
voor eventuele toekomstige opruim-flows, maar worden niet meer aangeroepen
vanuit de UI.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(PBI-79): mark scope-aanpassing afgerond + localStorage overzicht
- Drie open punten uit plan-revisie afgevinkt (commit 2a4ee6a).
- Sectie 'Bewust niet geïmplementeerd': server-persist van manuele
PBI/story-klikken — op vraag van user nu out-of-scope voor deze PR.
- Tabel localStorage-gebruik in de codebase voor toekomstige referentie.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 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
* feat(PBI-74): sprint-workspace-store skelet (Story 9 / T-879)
- stores/sprint-workspace/{types,store,selectors,restore}.ts conform
product-workspace blueprint
- ContextSlice: activeProduct, activeSprintId, activeStoryId, activeTaskId
- EntitiesSlice: sprintsById, storiesById, tasksById
- RelationsSlice: sprintIdsByProduct, storyIdsBySprint, taskIdsByStory
- LoadingSlice met activeRequestId voor race-safe ensure*Loaded
- SyncSlice: realtimeStatus, lastResyncAt, resyncReason
- Realtime applyRealtimeEvent voor sprint/story/task entities + unknown-event
fallback, parent-move handling, child-cleanup bij D op sprint/story
- Optimistic mutations: sprint-story-order, sprint-task-order, entity-patch
- LocalStorage hints (storage key sprint-workspace-hints) per product/sprint
- 45 unit-tests groen — verplicht 13 cases uit workspace-store.md §Tests
* feat(PBI-74): sprint hydratie + realtime SSE (Story 9 / T-880)
- app/api/realtime/sprint/route.ts: SSE-stream LISTEN/NOTIFY op
scrum4me_changes, filter entity ∈ {sprint, story, task} per product_id;
ready-event, heartbeat 25s, hard-close 240s
- lib/realtime/use-sprint-realtime.ts: client-hook met backoff-reconnect;
ready-cycle telt; geen close op hidden; setRealtimeStatus
- lib/realtime/use-sprint-workspace-resync.ts: visibility + online triggers
resyncActiveScopes('visible' | 'reconnect')
- components/sprint/sprint-hydration-wrapper.tsx: hydrateSnapshot via
useEffect met fingerprint-check; mount realtime + resync
- app/(app)/products/[id]/sprint/[sprintId]/page.tsx: wrap SprintBoardClient
in SprintHydrationWrapper; bouw SprintWorkspaceTask-shape voor
tasksByStoryWorkspace en SprintHydrationData voor de wrapper
Schaduw-fase: useSprintStore blijft parallel werken in board components
totdat T-881 die migreert en T-883 de oude store opruimt.
* feat(PBI-74): migreer sprint-board componenten naar workspace-store (Story 9 / T-881)
- TaskList: leest tasks via selectTasksForStory met useShallow; DnD via
applyOptimisticMutation('sprint-task-order') + settle/rollback
- SprintBacklogLeft: leest stories via selectStoriesForActiveSprint met
useShallow; props 'stories' verwijderd
- SprintBoardClient: leest sprintStories uit selector i.p.v. lokale state;
add/remove via direct setState met manuele snapshot-rollback;
reorder via applyOptimisticMutation('sprint-story-order'); assignee-
change via store entity-mutation; tasksByStory en sprintStoryIdList
props weg
- app/(app)/.../sprint/[sprintId]/page.tsx: bouwt SprintHydrationData voor
wrapper; geeft alleen non-store props door aan SprintBoardClient
useSprintStore wordt nergens meer geïmporteerd — alleen comment-referentie
in SprintHydrationWrapper. Cleanup van het bestand zelf in T-883.
Verify groen (671 tests, typecheck, lint clean).
* feat(PBI-74): read-routes voor sprint-workspace + cache-headers (Story 9 / T-882)
- GET /api/products/[id]/sprints — lijst sprints per product
(ensureProductSprintsLoaded). force-dynamic, productAccessFilter,
start_date/end_date naar ISO-date string.
- GET /api/sprints/[id]/workspace — sprint snapshot met sprint-meta,
stories (incl. taskCount/doneCount/assignee), tasks gegroepeerd per
story (ensureSprintLoaded). force-dynamic, productAccessFilter via
product, status-vertaling via taskStatusToApi/storyStatusToApi.
Race-safe loaders (activeRequestId-guard), restore-flow (cascade-restore
via writeProductHint/writeSprintHint/writeStoryHint/writeTaskHint),
resync-laag (useSprintWorkspaceResync visibility + online), unknown-event
filter (isUnknownEntityEvent → resyncActiveScopes('unknown-event')) zijn
allemaal in T-879/T-880 al ingebouwd; T-882 sluit het loop met de
ontbrekende API-endpoints + cache-headers (cache: 'no-store' op fetches,
force-dynamic op routes).
* feat(PBI-74): cleanup oude sprint-store (Story 9 / T-883)
- rm stores/sprint-store.ts — alle componenten lezen nu via
useSprintWorkspaceStore (T-881 voltooide imports-migratie)
- update SprintHydrationWrapper-comment: schaduw-fase referenties
verwijderd
Verify: 671 tests groen, typecheck clean, build groen.
Grep useSprintStore = 0.
* docs(PBI-74): update Story 9 status in implementatieplan (T-884)
- Frontmatter: ready-to-execute → in-progress; revision 1 → 2;
last_updated 2026-05-09 → 2026-05-10
- Stories-tabel: kolom Status toegevoegd (Stories 1-8 DONE via PR #180,
Story 9 met T-884 op review)
- §Story 9: per-taak status + acceptatie-checklist voor T-884 manuele
staging-checks
- Aanbeveling-blokje: noteert dat Story 9 vroeger gestart is dan het
ontwerpdoc adviseerde
* feat(PBI-74): product-workspace store skelet + test-infra (Story 1)
Skelet voor de nieuwe `product-workspace-store` die op termijn de gefragmenteerde
`backlog-store`/`planner-store`/`selection-store`/`product-store` vervangt. Deze
PR levert alleen het skelet + tests; UI-consumers worden in latere stories
omgezet.
- vitest naar jsdom + tests/setup.ts (MemoryStorage, default fetch-stub) — G6/G8
- stores/product-workspace/{types,store,selectors,restore}.ts — immer-middleware,
alle slices en acties (hydrate, setActive*, ensure*Loaded met activeRequestId-
guard, applyRealtimeEvent, resyncActiveScopes/loadedScopes, optimistic
mutations). Restore-wiring in setters volgt in Story 4 (T-857/T-858).
- selectors gebruiken module-level EMPTY refs (G1) en documenteren useShallow-
vereiste (G2)
- 34 nieuwe unit-tests dekken §Testing setup-checklist uit het ontwerp:
hydrateSnapshot, selection-cascade, applyRealtimeEvent (I/U/D + parent-move +
ander-product + unknown-entity → resync), delete-cleanup, race-safe loaders,
ensureTaskLoaded _detail-flag, resyncActiveScopes ensure-keten, restore-hints
read/write/clear, optimistic mutation rollback/settle/SSE-echo idempotent
- docs/api/rest-contract.md: audit-sectie met de vier ontbrekende
ensure*Loaded-endpoints (worden toegevoegd in Story 7 / T-870)
Refs: PBI-74, ST-1318, T-837..T-843
Bron-ontwerp: docs/plans/zustand-store-rearchitecture.md
Implementatieplan: docs/plans/zustand-workspace-store-implementation.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(PBI-74): dual-dispatch hydratie + realtime naar workspace-store (Story 2)
Story 2 — schaduw-fase: BacklogHydrationWrapper en useBacklogRealtime voeden
nu ook de nieuwe product-workspace-store, terwijl de oude useBacklogStore /
useProductStore leidend blijft voor componenten. Story 3 verschuift consumers
één voor één; Story 8 ruimt de oude stores op.
- T-844: BacklogHydrationWrapper roept naast useBacklogStore.setInitialData
ook useProductWorkspaceStore.hydrateSnapshot aan. Productname-prop optioneel
toegevoegd voor activeProduct-context.
- T-845: useBacklogRealtime onmessage dispatcht events naar zowel oude store
(applyChange) als nieuwe store (applyRealtimeEvent). Geen wijziging aan
reconnect/visibility — Story 5.
- T-846: dev-only logWorkspaceFingerprint helper vergelijkt counts tussen
oude en nieuwe store na hydrate en na elk realtime-event. console.warn bij
mismatch; opt-in debug log via NEXT_PUBLIC_DEBUG_WORKSPACE_FINGERPRINT=1.
Bestand TODO-marked voor verwijdering in Story 8 (T-878).
- T-847: SetCurrentProduct schrijft naast oude useProductStore ook
useProductWorkspaceStore.setActiveProduct({id, name}); cleanup cleart beide.
setActiveProduct triggert ensureProductLoaded — fetch-stub tot Story 7
(T-870) de LIST-endpoints toevoegt.
Verify: lint+typecheck clean, 636/636 tests groen (geen UI-regressie omdat
oude store leidend blijft).
Refs: PBI-74, ST-1319, T-844..T-847
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(PBI-74): migreer backlog-componenten naar workspace-store (Story 3)
Story 3 verplaatst alle UI-consumers van de oude vier stores
(useBacklogStore/usePlannerStore/useSelectionStore/useProductStore) naar de
nieuwe product-workspace-store. De oude stores blijven nog bestaan voor
hydration-wrapper en realtime-hook (dual-dispatch); Story 8 ruimt ze op.
- T-848 backlog-split-pane.tsx: leest activePbiId/activeStoryId uit
context-slice (primitives, geen useShallow nodig).
- T-849 pbi-list.tsx: selectVisiblePbis(useShallow); DnD via
applyOptimisticMutation('pbi-order' + optionele 'entity-patch' bij
cross-priority drag), met settle/rollback per server-result.
- T-850 story-panel.tsx: selectStoriesForActivePbi(useShallow); DnD via
applyOptimisticMutation('story-order' + entity-patch bij priority change).
- T-851 task-panel.tsx: selectTasksForActiveStory(useShallow); DnD via
applyOptimisticMutation('task-order'); detail-view (ensureTaskLoaded +
isDetail) zit in de task-dialog (apart component, niet in deze lijst).
- T-852 start-sprint-button.tsx: selectActivePbi + selectStoriesForActivePbi
voor free-story count.
- T-853 set-current-product.tsx: alleen workspace-store.setActiveProduct
(oude useProductStore-import verwijderd).
- T-854 G1/G2-audit: alle nieuwe selectors gebruiken module-level EMPTY
refs (G1) en useShallow voor lijsten (G2). Geen 'Maximum update depth'-
warnings tijdens npm test.
- T-855 tests bijgewerkt: backlog-split-pane.test, task-panel.test,
integration.test gebruiken nu setState op workspace-store (helpers
resetWorkspace/setActiveStoryAndTasks/selectPbi/selectStory).
Verify: lint+typecheck clean, 636/636 tests groen. UI-consumers van
oude stores zijn nu nul (uitgezonderd dual-dispatch in hydration-wrapper en
realtime-hook + dev-fingerprint-helper, die in Story 8/T-873/T-878 verdwijnen).
Refs: PBI-74, ST-1320, T-848..T-855
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(PBI-74): race-safe loaders + restore-hints + URL-prioriteit (Story 4)
- T-856: activeRequestId-guard zat al in store.ts uit Story 1; bevestigd door
de race-safety test (in-flight ensurePbiLoaded mag niet overschrijven).
- T-857: restore-hint flow toegevoegd in setActiveProduct/setActivePbi/
setActiveStory. Async chain: await ensureXxxLoaded → guard check →
readHints → valideer hint via entities.byId → setActiveYyy(hint).
Geen setTimeout-trick — chain is alleen await-based.
- T-858: writeProductHint/writePbiHint/writeStoryHint/writeTaskHint
aangeroepen direct na set(...) zodat de hint-persistentie altijd
consistent is met de in-store selectie.
- T-859: nieuwe components/backlog/url-task-sync.tsx — leest
?editTask=<id> uit useSearchParams, schrijft de hint en roept
setActiveTask aan zodat de URL wint boven een eerder gepersisteerde
task-hint. Gemount in beide product-pages (desktop + mobile) binnen
BacklogHydrationWrapper.
- T-860: 6 nieuwe vitest-cases — 4 voor hint-persist per setter, 2 voor de
restore-flow chain (hint die niet in entities zit wordt genegeerd; hint
die wel in entities zit wordt toegepast). Bestaande race-safety test
blijft groen.
Verify: lint+typecheck clean, 642/642 tests groen.
Refs: PBI-74, ST-1321, T-856..T-860
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(PBI-74): hidden-tab + reconnect resync (Story 5)
Per ontwerp samen in één commit zodat geen vangnet wegvalt zonder vervanging.
- T-861: useBacklogRealtime sluit niet meer op visibilitychange hidden;
EventSource blijft open zolang browser/netwerk dit toelaten. Reconnect bij
netwerkfout blijft via backoff. visibilitychange fungeert nog wel als
re-connect-trigger als de stream tussentijds is gesloten (b.v. 240s
hard-close server-side).
- T-862: 'ready'-event-handler telt connect-cycles. De eerste 'ready' is de
initial connect (geen resync). Bij latere 'ready' (post-reconnect) wordt
resyncActiveScopes('reconnect') aangeroepen om gemiste events op te halen.
- T-863: nieuwe lib/realtime/use-workspace-resync.ts — luistert op
document.visibilitychange (hidden→visible) en window.online; dispatcht
resyncActiveScopes('visible') resp. 'reconnect'. Mounted in
BacklogHydrationWrapper na useBacklogRealtime.
- T-864: 4 nieuwe vitest-cases voor useWorkspaceResync (jsdom): visible→
visible event, online event, hidden negeren, cleanup-bij-unmount.
Daarnaast lint-cleanup: ongebruikte 'order'-variabelen in pbi-list en
story-panel weggehaald.
Verify: lint+typecheck clean, 646/646 tests groen.
Refs: PBI-74, ST-1322, T-861..T-864
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(PBI-74): unknown-event fallback tests (Story 6)
T-865 (isUnknownEntityEvent filter) en T-866 (resync-trigger in
applyRealtimeEvent) zijn al in Story 1 geïmplementeerd in store.ts;
deze story breidt de test-coverage uit met expliciete negatieve cases
voor het type-veld noise pattern.
T-867 — 5 nieuwe vitest-cases:
- unknown entity met ANDER product_id → geen resync
- claude_job_status (type) → geen resync
- worker_heartbeat (type) → geen resync
- claude_job_enqueued (type) → geen resync
- payload zonder entity en zonder type → genegeerd
- question-entity (entity-veld, geen type, niet pbi/story/task) → resync trigger
Verify: lint+typecheck clean, 651/651 tests groen.
Refs: PBI-74, ST-1323, T-865..T-867
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(PBI-74): cache-headers + LIST endpoints (Story 7)
- T-868: cache: 'no-store' was al ingebouwd in fetchJson helper (Story 1).
Bevestigd door bestaande ensureProductLoaded test die de fetch-init
controleert.
- T-869: force-dynamic toegevoegd op alle vier nieuwe LIST-endpoints.
- T-870: vier nieuwe routes voor ensure*Loaded:
- GET /api/products/:id/backlog → ProductBacklogSnapshot
- GET /api/pbis/:id/stories → BacklogStory[]
- GET /api/stories/:id/tasks → BacklogTask[]
- GET /api/tasks/:id (nieuwe handler naast bestaande PATCH) → TaskDetail
met _detail: true marker
Auth via authenticateApiRequest (Bearer of iron-session); access-control
via productAccessFilter (gebruiker is owner of member van het product).
Statussen worden via taskStatusToApi/storyStatusToApi/pbiStatusToApi
vertaald naar lowercase API-vorm.
- T-871: SSE-route /api/realtime/backlog stuurt al ready-event direct na
LISTEN (regel 106) — geen wijziging nodig.
Verify: lint+typecheck clean, 651/651 tests groen.
Refs: PBI-74, ST-1324, T-868..T-871
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(PBI-74): oude stores opruimen (Story 8)
Workspace-store is nu de enige bron voor product-backlog client-state. De
vier voorgangers en de dual-dispatch-infrastructuur zijn verwijderd.
- T-872: grep over codebase op useBacklogStore/usePlannerStore/
useSelectionStore/useProductStore is leeg.
- T-873..T-876: stores/{backlog,planner,selection,product}-store.ts deleted.
- T-877: __tests__/realtime/payload-contract.test.ts en
__tests__/api/backlog-realtime.test.ts deleted — pbi/story/task I|U|D
payload-handling wordt al gedekt door
__tests__/stores/product-workspace/store.test.ts (incl. parent-move,
idempotent inserts, delete-cleanup).
- T-878: lib/realtime/dev-workspace-fingerprint.ts deleted, dual-dispatch
uit BacklogHydrationWrapper en lib/realtime/use-backlog-realtime.ts
weggehaald. stores/products-store.ts (lijst van producten ≠ active
product) blijft ongewijzigd.
Bijwerkingen:
- BacklogPbi en BacklogStory types in components/backlog/story-panel.tsx en
components/sprint/sprint-backlog.tsx krijgen sort_order zodat ze met de
workspace-types overeenkomen.
- Server-pages /products/[id]/page.tsx (desktop+mobile) en
/products/[id]/sprint/[sprintId]/page.tsx selecteren sort_order op story
en mappen het door in de hydration-payload.
Verify: lint+typecheck clean, 626/626 tests groen (verlies van 25 redundante
oude-store tests; workspace-store tests dekken hetzelfde gedrag).
Refs: PBI-74, ST-1325, T-872..T-878
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(PBI-74): richtlijn workspace-store + realtime patroon
Documenteert het patroon dat in Stories 1-8 is opgeleverd, zodat een
volgende workspace-store (sprint, of een nieuwe bounded context) hetzelfde
recept volgt.
- docs/patterns/workspace-store.md (nieuw): wanneer een workspace-store, de
vijf state-slices, selectors-regels (G1/G2), race-safe ensure*Loaded met
activeRequestId-guard (G4), SSE-hook + applyRealtimeEvent met
unknown-event filter, hidden-tab + reconnect resync via
useWorkspaceResync, restore-hint flow met await-chain en URL-prioriteit,
optimistic mutations (applyOptimisticMutation/rollback/settle), API
endpoint-vereisten (force-dynamic, cache: no-store), test-setup met
MemoryStorage + originalActions snapshot + mockImplementation, gotchas
G1-G8 als comment-template, en het 8-staps migratiepad.
- docs/patterns/zustand-optimistic.md: bijgewerkt voor de nieuwe
workspace-store API; verwijst voor het bredere patroon naar
workspace-store.md. Voorbeelden voor pbi-order + entity-patch.
- CLAUDE.md: patterns quickref aangevuld met workspace-store-rij.
Verify: typecheck clean.
Refs: PBI-74
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(PBI-74): solo + notifications hooks volgen ook hidden-tab/resync patroon
Het uitgangspunt van PBI-74 (robuust tegen gemiste SSE-events, hidden tabs
en onbekende notify-vormen) gold universeel — niet alleen voor
product-workspace. use-solo-realtime en use-notifications-realtime hadden
nog dezelfde bug die use-backlog-realtime in Story 5 al opgelost kreeg:
sluit stream op hidden, geen resync.
Reproductie (zoals gemeld): solo-screen open in tab A, product-backlog
open in tab B; bewerk task-title in tab B → tab A's solo-SSE was gesloten
(hidden) en kreeg het NOTIFY-event nooit. Tab terug naar solo →
EventSource reconnect maar geen resync → oude title persisteert. Postgres
NOTIFY heeft geen replay, dus zonder resync zijn die events permanent
verloren.
Fix in beide hooks (zelfde patroon als Story 5 voor backlog):
- Stream blijft open op visibilitychange hidden — geen close() meer.
- Bij hidden→visible én bij window 'online': router.refresh() zodat de
server-component opnieuw fetcht en de initial-state-prop ververst (wat
voor solo de tasks-record reset via initTasks; voor notifications de
questions-bel-state).
- Bij latere 'ready'-events na reconnect (use-solo-realtime): zelfde
router.refresh() trigger zodat we niet vertrouwen op alleen het
visibility-pad.
Verify: lint + typecheck clean, 626/626 tests groen.
Refs: PBI-74
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs: fix broken links in research-repo plan
docs/plans/lees-de-readme-md-validated-book.md beschrijft een research-
repo migratiepad. De links waren geschreven vanuit het research-repo-
perspectief (paden als stores/data-store.ts, ../Scrum4Me/CLAUDE.md,
docs/plans/zustand-store-rearchitecture.md zonder relative-prefix), wat
de doc-link-checker hier laat falen.
- Header-note toegevoegd dat het document voor de research-repo is.
- Interne refs (zustand-store-rearchitecture.md, CLAUDE.md) → relatieve
paden die in deze repo wél resolven (./zustand-..., ../../CLAUDE.md).
- Research-repo-only refs (stores/data-store.ts,
hooks/use-event-stream.ts, components/*-select.tsx, etc.) → inline
code-tags met "(research-repo)" suffix; de link-checker slaat ze over
en de leesbaarheid blijft.
Verify: npm run docs:check-links → ✓ All doc links valid (118 files).
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(cleanup): verwijder Todo's navlink en todo-referenties uit marketing page [cmotto5ia000nx3178lq6xk8d]
- nav-bar.tsx: Todo's navLink verwijderd; Ideas-link blijft staan
- app/page.tsx: /todos quick-access link, feature-entry en /api/todos API-doc verwijderd
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(cleanup): verwijder app/(app)/todos/ en components/todos/ [cmottjvzo000cx3172472cu4g]
* test(cleanup): verwijder POST /api/todos import en describe-block uit security.test.ts [cmotto5jn000px317kjqlba89]
- Import 'POST as postTodo' uit verwijderde todos-route verwijderd
- describe('POST /api/todos') sectie (3 tests) verwijderd
- 73 testfiles / 561 tests groen
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test(cleanup): verwijder __tests__/api/todos.test.ts en __tests__/actions/todos-promote-idea.test.ts [cmottjw1u000fx317igq27mh5]
* feat(cleanup): verwijder actions/todos.ts en app/api/todos/route.ts; verplaats updateRolesAction naar actions/settings.ts [cmottjvy9000ax3173sgfjcqs]
* feat(db): migratie todos→ideas, counters bijwerken, todos dropt [cmotto5fh000jx317r7c5srvb]
Nieuwe Prisma-migratie die in één transactie actieve todos omzet naar
DRAFT-ideas met unieke IDEA-NNN codes, idea_code_counter per user
bijwerkt, en de todos-tabel dropt.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(schema): verwijder Todo model en relaties uit prisma/schema.prisma [cmottjvwu0008x317fwwodg3i]
* feat(cleanup): vervang open_todos door open_ideas in /api/products/:id/claude-context
Laatste prisma.todo-referentie verwijderd. Endpoint geeft nu open_ideas terug
(ideeën van de gebruiker voor dit product die niet gearchiveerd zijn en nog
niet status PLANNED hebben). Docs bijgewerkt in docs/api.md en
docs/api/rest-contract.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* docs(ST-511): add backlog entry for entity codes feature
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(ST-511): add createWithCodeRetry helper to handle P2002 race on auto codes
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(ST-511): retry on auto-code unique conflict in story and pbi create
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(ST-511): surface field errors for code and title in PBI dialog
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(ST-511): read create-state errors in Story dialog fieldError
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(ST-512): add backlog entry for REST API code/description/implementation_plan extensions; mark ST-511 done
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(ST-512): extend REST API with code, description and implementation_plan
- GET /api/products returns code, description and definition_of_done
- GET /api/products/:id/next-story returns story.code and per-task code + implementation_plan
- GET /api/sprints/:id/tasks returns description, implementation_plan, story_code and derived per-task code
- POST /api/todos accepts and returns optional description (max 2000)
All changes are additive — existing clients ignore unknown keys.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(ST-512): mark ST-512 as done
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(ST-513): add backlog entry for API hardening for Claude Code
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(ST-513): add task and story status mappers for API boundary
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(ST-513): expose lowercase status on API and accept lowercase in PATCH /api/tasks
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(ST-513): add metadata JSONB column to StoryLog
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(ST-513): accept optional metadata in story log and switch validation errors to 422
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(ST-513): add GET /api/health endpoint with optional db ping
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(ST-513): add GET /api/products/:id/claude-context bundled endpoint
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(ST-513): add docs/API.md and link from CLAUDE.md specs table
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(ST-513): mark ST-513 as done
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(ST-513): split 400 (malformed JSON) from 422 (validation), reject 'review'
Codex review on PR #2:
- P2.1: routes treated JSON parse failures as 422 instead of 400, breaking
the contract in docs/API.md. Replace `request.json().catch(() => null)`
with try/catch in 4 routes (tasks, reorder, todos, story-log) so a
malformed body returns 400 and only well-formed-but-invalid bodies
return 422.
- P2.2: PATCH /api/tasks/:id accepted `status: "review"`, but the sprint
task list UI does not render REVIEW (no label/color, the cycle helper
falls back to TO_DO). Reject `review` at the API until the sprint UI
is extended; document the subset in docs/API.md.
Tests in __tests__/api updated for the new contract (29 assertions:
zod-failures now expect 422, status payloads use lowercase API values,
sprint-tasks mocks include the new story relation).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Add ProductMember model (many-to-many User ↔ Product)
- Add productAccessFilter helper (owner OR member OR clause)
- Replace all ownership checks across actions and API routes
- Add addProductMemberAction / removeProductMemberAction / leaveProductAction
- Add TeamManager component in product settings (owner adds/removes Developers)
- Add LeaveProductButton in user settings (member leaves a product team)
- Regenerate Prisma Client after schema migration
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- api-auth.ts was al aanwezig; demo-check toegevoegd per endpoint (ST-401)
- Token aanmaken (SHA-256 hash, eenmalig tonen), intrekken, max 10 (ST-402)
- GET /api/products actieve productenlijst (ST-403)
- GET /api/products/:id/next-story hoogst geprioriteerde open story (ST-404)
- GET /api/sprints/:id/tasks met limit parameter (ST-405)
- PATCH /api/stories/:id/tasks/reorder met ID-validatie (ST-406)
- POST /api/stories/:id/log met discriminatedUnion per type (ST-407)
- PATCH /api/tasks/:id status bijwerken met cross-user bescherming (ST-408)
- POST /api/todos via API aanmaken (ST-409)
- StoryLog component met kleurcodering per type in story slide-over (ST-410)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>