- 6 plans naar docs/old/plans/ (PBI-11/75/78, user-settings-store, Local github setup, lees-de-readme — laatste was verkeerde repo)
- docs/backlog/ naar docs/old/backlog/ (pre-MCP statische registry; live werk loopt via Scrum4Me-MCP)
- 6 root-level duplicaten naar docs/old/ (functional, {pbi,story,task}-dialog, product-backlog, backlog)
- 2 landing plans (niet uitgevoerd) krijgen archived: true frontmatter — blijven op plek maar uit INDEX
- scripts/generate-docs-index.mjs: skip docs/old/** + skip archived: true
- CLAUDE.md: rijen docs/backlog/, docs/plans/<key>-*.md, docs/manual/ weg; Track B-sectie verwijderd
- README.md / CHANGELOG.md / docs/plans/v1-readiness.md: link-fixes naar nieuwe locaties
Verify groen (lint + typecheck + 718 tests). docs/INDEX.md geregenereerd.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
9.7 KiB
| title | status | audience | language | last_updated | ||
|---|---|---|---|---|---|---|
| User-settings store (DB-backed user prefs) | draft |
|
nl | 2026-05-10 |
User-settings store (DB-backed user prefs)
Locatie na approval: verhuis dit bestand naar
docs/plans/user-settings-store.mdin de repo. Trigger voor dit plan: zichtbare hydratie-flits op het sprint-scherm in v1.3.3 (PR #184). De fix daar (useEffect-hydratie +prefsLoaded-gate) is een tijdelijke patch; deze migratie elimineert de flits volledig.
Context
Filter- en view-prefs zitten nu verspreid over localStorage (en deels cookies).
Bij SSR weet de server niets van localStorage, dus bij users met saved-state ≠
default ontstaat één render-flits direct na hydratie. Daarnaast werken die prefs
alleen per browser — geen cross-device, en cross-tab-sync vereist storage-events.
Doel: één User.settings JSON-veld als single source of truth, met:
- Server-component leest het veld bij elke page-render → SSR-correct, geen flits
- Zustand-store met optimistic updates patroon (zoals
product-workspace-store) - Cross-tab sync via bestaande
LISTEN/NOTIFY+ SSE-bridge - Cross-device persistence (login op andere browser/laptop ziet zelfde prefs)
Scope (gefaseerd)
Fase 0 — Infrastructuur
Aparte PR. Geen UI-wijziging; legt het fundament. Resultaat is een werkende store zonder migraties; bestaande localStorage-flow blijft intact tot Fase 1.
| # | Bestand | Wat |
|---|---|---|
| 0.1 | prisma/schema.prisma |
settings Json @default("{}") op User model + migration |
| 0.2 | lib/user-settings.ts |
Zod-schema + types + mergeSettings(prev, patch) deep-merge helper + defaults |
| 0.3 | actions/user-settings.ts |
updateUserSettingsAction(patch: Partial<UserSettings>) — auth-guard, Zod-validate, deep-merge in DB transactie, NOTIFY scrum4me_changes 'user_settings:${userId}' |
| 0.4 | stores/user-settings/store.ts |
Zustand met entities.settings: UserSettings, hydrate(initial), generieke setPref(path, value) met optimistic + rollback. Zelfde mutation-flow als product-workspace-store |
| 0.5 | app/api/realtime/user-settings/route.ts |
SSE-route per user, LISTEN user_settings:${userId}, push patches |
| 0.6 | components/shared/user-settings-bridge.tsx |
Server reads prisma.user.findUnique({select:{settings:true}}), geeft door als prop, client mount roept store.hydrate() aan + opent SSE |
| 0.7 | Mount in app/(app)/layout.tsx |
Bridge bovenin de app-layout zodat de store altijd beschikbaar is voor alle authenticated pagina's |
| 0.8 | Tests | __tests__/lib/user-settings.test.ts (merge-logic), __tests__/actions/user-settings.test.ts (auth + validation), __tests__/stores/user-settings.test.ts (optimistic flow) |
Demo/anon-fallback: useUserSettingsStore detecteert session.isDemo of geen userId
en valt terug op in-memory state (geen server-write). Bridge wordt voor demo niet
gemount — defaults blijven actief, geen persistence-verwachting.
Fase 1 — Migreer huidige flits-bronnen
| Component | localStorage-keys | → settings-pad |
|---|---|---|
components/sprint/sprint-backlog.tsx |
scrum4me:sprint_pb_* (6) |
views.sprintBacklog.{filterPriority,filterStatus,sort,sortDir,collapsedPbis,filterPopoverOpen} |
components/backlog/pbi-list.tsx |
scrum4me:pbi_* (4) |
views.pbiList.{sort,filterPriority,filterStatus,sortDir} |
components/backlog/story-panel.tsx |
scrum4me:story_sort (1) |
views.storyPanel.sort |
components/jobs/jobs-column.tsx |
${prefix}_filter_kind, ${prefix}_filter_status (2 dyn.) |
views.jobsColumns[prefix].{kinds,statuses} |
stores/debug-store.ts (via status-bar-debug-toggle) |
scrum4me:debug-mode (1) |
devTools.debugMode |
Per component:
- Verwijder
useState+useEffect-hydratie +useEffect-write - Vervang door
useUserSettingsStore(s => s.entities.settings.views.sprintBacklog?.filterStatus ?? 'OPEN') - Setter wordt
useUserSettingsStore.getState().setPref(['views','sprintBacklog','filterStatus'], value) prefsLoaded-state en helpers (readLocalStoragePref) verdwijnenlib/use-local-storage-pref.tswordt verwijderd (niet meer in gebruik)
Migratie-pad voor bestaande users: bij eerste mount, voor de eerste setPref-call,
leest een one-shot useEffect de oude localStorage-keys en pusht ze als één bulk-patch
naar de server. Daarna localStorage.removeItem(...) om geen verwarring te wekken.
Idempotent: als settings.views.sprintBacklog.filterStatus al gezet is, sla over.
Fase 2 — Cookie-consolidatie (optioneel, later PR)
| Bron | Huidig | → settings-pad |
|---|---|---|
components/shared/split-pane.tsx |
document.cookie (sp: prefix) |
layout.splitPanePositions[cookieKey] |
lib/active-sprint.ts + actions/active-sprint.ts |
server-side cookie per product | layout.activeSprints[productId] |
Server-component-lezers veranderen — apart traject met meer regression-risico. Niet onderdeel van de eerste user-settings-PR.
Fase 3 — Skip / al persistent
idea-md-editor.tsxdrafts — werk-in-progress, geen prefiron-sessioncookies — auth, andere zorgUser.active_product_id— al in DB (kolom op model)- Modal/popover open-state behalve
filterPopoverOpen— ephemeral
JSON-shape (Fase 1)
// lib/user-settings.ts
import { z } from 'zod'
export const UserSettingsSchema = z.object({
views: z.object({
sprintBacklog: z.object({
filterPriority: z.union([z.number().int().min(1).max(4), z.literal('all')]).optional(),
filterStatus: z.enum(['OPEN', 'IN_SPRINT', 'DONE', 'all']).optional(),
sort: z.enum(['priority', 'status', 'code']).optional(),
sortDir: z.enum(['asc', 'desc']).optional(),
collapsedPbis: z.array(z.string()).optional(),
filterPopoverOpen: z.boolean().optional(),
}).optional(),
pbiList: z.object({
sort: z.enum(['priority', 'code', 'date']).optional(),
filterPriority: z.union([z.number().int().min(1).max(4), z.literal('all')]).optional(),
filterStatus: z.enum(['ready', 'blocked', 'done', 'all']).optional(),
sortDir: z.enum(['asc', 'desc']).optional(),
}).optional(),
storyPanel: z.object({
sort: z.enum(['priority', 'code', 'date']).optional(),
}).optional(),
jobsColumns: z.record(z.string(), z.object({
kinds: z.array(z.string()),
statuses: z.array(z.string()),
})).optional(),
}).optional(),
devTools: z.object({
debugMode: z.boolean().optional(),
}).optional(),
}).strict()
export type UserSettings = z.infer<typeof UserSettingsSchema>
Defaults zijn impliciet (alle keys optioneel). Selectors in de store geven
fallback-waardes terug zodat consumers niet ?? 'OPEN' hoeven te schrijven —
maar het mag, geen big deal.
Realtime-notificatie
Bestaand kanaal scrum4me_changes blijft. Payload-conventie:
{ "kind": "user_settings", "userId": "...", "patch": { "views": { ... } } }
/api/realtime/user-settings/route.ts filtert payloads op userId === session.userId.
Andere tabs van zelfde user krijgen patches binnen, store roept applyServerPatch(patch)
aan zonder optimistic flow.
Verificatie (per fase)
Fase 0
npm run verify && npm run buildgroen- Migration draait op fresh + bestaande DB zonder data-verlies
updateUserSettingsActionweigert auth-loze calls (test)- Zod-validatie geeft 422 bij invalid patch (test)
- Optimistic update + rollback gedraagt zich zoals
product-workspace-store(test) - SSE-route levert patches alleen aan zelfde user (manueel: open twee tabs als A, schrijf, zie update; tab van user B blijft stil)
Fase 1
- Geen
localStorage.getItemoflocalStorage.setItemmeer in de gemigreerde componenten - Sprint screen: refresh → filter direct correct, geen flits, geen hydration error in console
- Product backlog screen: idem
- Jobs page: idem (per kolom-instance)
- Two-tab test: filter wijzigen in tab A → tab B updatet binnen ~100ms
- Demo-user: filter wijzigen werkt binnen sessie, niet gepersisteerd na refresh (verwacht)
- One-shot localStorage-migratie: bestaande user met oude keys ziet bij eerste login zijn waardes terug; na refresh zijn de localStorage-keys leeg
Fase 2
- Split-pane positie persistent en SSR-correct
- Active-sprint per product werkt zonder cookie
Schatting
| Fase | Tijd |
|---|---|
| 0 — Infra | ~3 uur |
| 1 — Migratie | ~2 uur |
| 2 — Cookies | ~2 uur (apart) |
| Totaal Fase 0 + 1 | ~5 uur, 1 PR (of 2 als we 0 en 1 splitsen) |
Aanbevolen: Fase 0 + 1 in één PR als de infra klein blijft, anders splitsen per fase. Fase 2 is altijd een aparte PR.
Open vragen
- Cross-device merge-conflict. Twee tabs van zelfde user op verschillende
devices wijzigen tegelijk. Server-side:
last-write-winsofJSON_PATCH-merge? Voorstel: deep-merge per top-level path, dusviews.sprintBacklog.filterStatusenviews.pbiList.sortbotsen niet — laatste schrijver per veld wint. - Storage-grens. PostgreSQL JSON kolom kan ~1GB; we zitten op <5KB per user. Geen concern.
- Schema-versionering. Als we het JSON-schema later wijzigen: voorzichtig
migreren via Zod
.catch()voor onbekende keys. Voor v1: start klein. - One-shot localStorage-migratie weglaten? Voor solo-dev-tool kan het acceptabel zijn dat users hun saved filters verliezen bij de migratie. Scheelt ~30 minuten implementatie + tests.
Eerste stappen na approval
- Verhuis dit plan naar
docs/plans/user-settings-store.mdin een nieuwe branch (bv.feat/user-settings-store) - Maak via Scrum4Me-MCP een PBI met story + taken voor Fase 0 (volgens CLAUDE.md werkflow)
- Start met taken in
sort_order; commit per laag - Fase 1 als opvolg-PBI (of in dezelfde sprint, los gelabeld)