* feat(user-settings): voeg IdeasListPrefs schema toe met filterStatuses
Nieuw IdeasListPrefs-subschema met filterStatuses (array van IdeaStatusApi-waarden),
ingehangen als views.ideasList in ViewsPrefs. Testdekking voor geldig, ongeldig en
leeg filterStatuses.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor: extraheer MultiFilterPills naar backlog-filter-popover
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(ideas): voeg IdeasFilterPopover component toe
Nieuwe client-component met multi-select statusfilter popover voor het
Ideeënscherm; hergebruikt MultiFilterPills uit backlog-filter-popover.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(ideas): vervang inline statuschips door IdeasFilterPopover met user-settings persistentie
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test(ideas): voeg componenttests toe voor IdeasFilterPopover en persistentie
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(ideas): voeg activeProductId-prop toe aan IdeaList
IdeaListProps uitgebreid met activeProductId: string | null.
Create-form initialiseert en reset naar het actieve product na aanmaken.
Tests en page.tsx bijgewerkt (page.tsx krijgt echte waarde in volgende taak).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(ideas): resolve active_product_id en geef door aan IdeaList
Haalt active_product_id op via prisma.user.findUnique en resolveert
het tegen de al opgehaalde toegankelijke productenlijst (AC4). Geeft
het resultaat als prop door aan IdeaList in plaats van hardcoded null.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(ideas): stuur activeProductId mee bij snel idee aanmaken
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test(ideas): voeg component-tests toe voor activeProductId-voorvulling
AC1: "Nieuw idee"-select voorgevuld met activeProductId
AC2: "Nieuw idee"-select leeg bij activeProductId=null
AC3: "Snel idee" stuurt product_id=activeProductId mee bij createIdeaAction
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(notifications): toon textarea altijd in answer-modal naast opties
Vervang opties-XOR-textarea door twee onafhankelijke blokken: opties
alleen wanneer aanwezig, vrij tekstveld altijd zichtbaar. Bij opties
een visuele scheiding (border-t) en label 'Of typ een eigen antwoord'.
Verstuur-knop nu altijd in footer zichtbaar (was verborgen bij opties).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor(notifications): gebruik question.options?.length als conditie
Gebruik de kortere optional chaining variant consistent, conform plan.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test(notifications): voeg component-tests toe voor AnswerModal
Dekt: optieknoppen + textarea + Verstuur zichtbaar met opties,
submit via optieknop, submit via vrij tekstveld, disabled Verstuur
bij leeg veld, en demo-modus (textarea + Verstuur disabled).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* docs(notifications): werk answer-modal spec bij voor vrije tekstveld naast opties
Beschrijft dat textarea + Verstuur altijd zichtbaar zijn in multiple-choice
mode. Corrigeert de Cmd/Ctrl+Enter-bullet: shortcut werkt nu ook daar.
Bijgewerkt naar 2026-05-15.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(jobs): voeg lib/jobs-time-filter.ts toe met tijdvenster-predikaat
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(user-settings): voeg views.jobs.timeFilter toe aan UserSettingsSchema
Breidt ViewsPrefs uit met een jobs-object (JobsViewPrefs) dat timeFilter
accepteert met waarden '1h' | '24h' | 'all'. ViewsPrefs blijft .strict().
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test(jobs-time-filter): voeg unit-tests toe voor isWithinTimeWindow en UserSettings-schema
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(jobs-time-filter): voeg JobsTimeFilterControl component toe
Nieuw client-component dat views.jobs.timeFilter leest/schrijft
via useUserSettingsStore met pill-stijl (MD3-tokens).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(jobs): wire JobsTimeFilter in jobs page header
Plaatst het tijdfilter-component rechts van de Jobs-kop via justify-between op de header-div.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(jobs): pas tijdvenster-filter toe in JobsColumn
Lees views.jobs.timeFilter uit de store en filter jobs op createdAt via isWithinTimeWindow, als eerste check vóór de bestaande kind/status-filters.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* 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>
* feat(PBI-76): extend UserSettings schema with layout
Adds layout.splitPanePositions and layout.activeSprints. These will
hold values currently kept in client-side and server-side cookies
(Phase 2). Two new tests cover the shape.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(PBI-76): migrate SplitPane positions to user-settings store
Outside of a drag the store is the source of truth (cross-tab
updates flow in for free). During a drag we keep splits in local
state so mousemove does not round-trip through the store. On
mouseup we persist the final splits via setPref. Removes
document.cookie reads/writes — cookieKey is reused as the
store-key for backwards compat.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(PBI-76): resolveActiveSprint reads from User.settings
lib/active-sprint:
- New helpers: getActiveSprintIdFromSettings, setActiveSprintInSettings,
clearActiveSprintInSettings — all read/write user.settings.layout.activeSprints.
- resolveActiveSprint(productId, userId) — userId now required, falls back
to first OPEN, then most recent CLOSED sprint.
- Cookie helpers (getActiveSprintIdFromCookie/setActiveSprintCookie/
clearActiveSprintCookie) removed.
Callers updated to pass session.userId. The cookie-based fallback path
is gone — `actions/active-sprint.ts` and `actions/sprints.ts` will be
updated in the next commit (T-917).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(PBI-76): rewrite setActiveSprint callers to use settings
setActiveSprintAction, syncActiveSprintCookieAction, and the two
sprint-creation paths in actions/sprints.ts now write through
setActiveSprintInSettings (which also emits pg_notify for cross-tab
sync) instead of dropping a cookie. The action names keep the
'cookie' suffix in the user-visible API for now — clean rename can
come later.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(PBI-76): migration helper v2 — handle legacy cookies
Bumps marker version to 'v2'. buildMigrationPatch now also scans
document.cookie for `sp:*` (split-pane positions) and
`active_sprint_*` (active sprint per product) and lifts them into
layout.splitPanePositions / layout.activeSprints. clearLegacyStorage
replaces clearLegacyLocalStorage and clears both keys and cookies.
clearLegacyLocalStorage stays as a deprecated alias so the bridge
upgrade is a single rename.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(PBI-76): align tests with new SplitPane and active-sprint flow
- split-pane.test.tsx: seed positions via Zustand store instead of
document.cookie; mock @/actions/user-settings so the prisma client
is not transitively initialised in jsdom.
- backlog-split-pane.test.tsx: same action mock.
- sprint-dates.test.ts: add user.findUnique/update + $executeRaw
mocks because createSprintAction now writes to user-settings.
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(PBI-76): plan for user-settings DB-store
Persists view/filter prefs in User.settings (Json) instead of
localStorage. SSR-correct hydration, cross-tab sync via
LISTEN/NOTIFY + SSE, cross-device persistence.
Phased: 0=infra, 1=migrate flicker sources, 2=cookie consolidation.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(PBI-76): User.settings json column + migration
Adds JSONB column to users table for persistent user prefs.
Idempotent SQL — safe on databases where column already exists.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(PBI-76): user-settings types and merge helpers
Zod schema for User.settings shape (views/devTools), deep-merge
helper that replaces arrays and merges nested objects, and a
safe parser that returns defaults on invalid input.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(PBI-76): updateUserSettingsAction with notify
Validates patch via Zod, deep-merges with current settings in
a transaction, persists to DB, and emits pg_notify on
scrum4me_changes for cross-tab/cross-device sync. Demo accounts
get 403, unauthenticated 401, invalid input 422.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(PBI-76): user-settings zustand store with optimistic flow
Hydrate from prop (SSR-correct), setPref via path with optimistic
update + rollback on server error, applyServerPatch for SSE-driven
cross-tab updates. Demo accounts skip server-write entirely.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(PBI-76): SSE route for user-settings
User-scoped /api/realtime/user-settings stream that filters
scrum4me_changes notifications on kind=user_settings and matching
userId. Forwards the patch as a data: event so other tabs can
applyServerPatch without re-fetching settings.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(PBI-76): user-settings bridge mounted in app layout
Hydrates the zustand store with the user's persisted settings via
prop (SSR-correct, no flicker). Opens an EventSource to
/api/realtime/user-settings so changes from other tabs/devices
flow into the same store. Demo accounts skip the SSE subscription.
Layout now selects user.settings alongside the other user fields,
no extra DB roundtrip.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(PBI-76): user-settings lib/action/store coverage
22 vitest cases covering merge semantics (no mutation, array
replace, nested merge), Zod schema strictness, server action
auth/demo/validation paths, and the optimistic store flow
including rollback and demo-mode skip.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(PBI-76): sync package-lock to v1.3.3
Lockfile drifted after @prisma/client reinstall during the
schema regenerate. No dependency changes — just the version
field tracking package.json bumped in #184.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>