* feat(PBI-80): SprintSwitcher demo-fork (ST-1345) Demo-sessies navigeren bij sprint-wissel direct via router.push, zonder de geblokkeerde setActiveSprintAction aan te roepen. De server-action behoudt zijn 403-guard als defense in depth. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(PBI-80): NavBar demo-fork + URL-derived actief product (ST-1346) Demo: product-switch in de NavBar navigeert direct via router.push zonder setActiveProductAction. Voor de weergave (label + dropdown-highlight + nav-links) leiden we voor demo de actieve product af uit pathname, zodat de UI consistent is met de URL — de server-render houdt de seed-default prop maar die wordt voor demo overschreven. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(PBI-80): ADR-0006 addendum + demo-client-state patroon (ST-1347) ADR-0006 krijgt een "Updated 2026-05-12"-sectie die de PBI-80-uitzondering documenteert: client-side UI-prefs (filters, sort, layout, scope-keuze) zijn voor demo toegestaan via in-memory store, terwijl alle data-mutaties three-layer beschermd blijven. Patroon-doc beschrijft wanneer en hoe `isDemo` te gebruiken in nieuwe componenten. CLAUDE.md quickref + docs/INDEX.md ge-update. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
9 KiB
PBI-80 — Demo-gebruiker mag eigen UI-voorkeuren wijzigen
Stories: ST-1345, ST-1346, ST-1347, ST-1348 Branch:
feat/demo-prefsAangemaakt: 2026-05-11
Context
De demo-gebruiker (username='demo', één gedeelde DB-rij met is_demo=true) zit nu
vast op het seed-default product en de seed-default sprint. Elke poging om te wisselen
of een filter te wijzigen geeft een 403-toast ("Niet beschikbaar in demo-modus"), wat
een potentiële klant geen goed beeld geeft van wat de app kan: hij kan niet door
producten bladeren, geen alternatieve sprint openen, geen filter ervaren.
Beperking. Alle demo-bezoekers delen één DB-rij. Directe DB-persistentie van
demo-prefs zou cross-bezoeker-pollution geven (A's keuze zichtbaar voor B). Schrijven
naar user.settings voor de demo-rij is dus structureel onveilig.
Doel. Demo mag binnen één browsertab zijn UI-context vrij wijzigen (product, sprint, filters, layout). Geen DB-mutaties — alle wijzigingen sterven aan het einde van de tab/refresh. De huidige three-layer beschermingen voor data-mutaties blijven volledig intact.
Bestaande infra die we hergebruiken.
- stores/user-settings/store.ts:80 —
setPrefheeft al een demo-fork (lokale merge zonder server-call). - components/shared/user-settings-bridge.tsx:34 — skipt SSE en server-sync voor demo.
- Filters/sort/layout/selecties lopen volledig via
useUserSettingsStore— alleen verifiëren dat de UI niets extra's vraagt aan de server.
Beslissingen
| Onderwerp | Keuze | Implicatie |
|---|---|---|
| Persistentie | In-memory (Zustand) | Geen cookie, geen localStorage, geen DB. Refresh = reset. |
| Scope prefs | Filters/sort + layout (split-panes, collapsed PBIs, selecties) | Debug-mode en notificaties buiten scope. |
| Documentatie | ADR-0006 update + addendum | One-stop: lezer ziet uitzondering bij oorspronkelijke beslissing. |
| Server-actions | Behouden 403 voor demo | Defense in depth blijft intact. UI roept ze gewoon niet aan voor demo. |
Scope
Demo MAG (nieuw):
- Wisselen van actief product (URL-navigatie + NavBar reflectie)
- Wisselen van actieve sprint binnen een product (URL-navigatie)
- Filters wijzigen (status, priority) op backlog en sprint-board — werkt al
- Sortering wijzigen (kolom-headers) — werkt al
- Collapse/expand van PBIs, selectie van actieve PBI/story/taak — werkt al
- Split-pane breedte verslepen — werkt al
Demo MAG NIET (ongewijzigd):
- PBI/story/taak aanmaken, wijzigen, verwijderen, verplaatsen
- Sprints openen/sluiten, builden, archiveren
- Rollen toekennen of intrekken (
UserRole) - Accountgegevens wijzigen (username, password, email)
- QR-pairing, web-push abonnement, notificaties
- Debug-mode toggle
- Cron / webhook secrets
Architectuur
In-memory only. Client-side useUserSettingsStore is de enige bron van
demo-state. Server-actions blijven 403 retourneren — die blokkade is geen bug maar
een veiligheidsnet voor het geval client-code per ongeluk een server-call doet. De UI
moet voor demo de server-call dus gewoon overslaan.
Product-switch (geen DB-write).
- Vandaag: NavBar roept
setActiveProductAction(productId)→ 403 → toast. - Nieuw: voor demo doet NavBar alleen
router.push('/products/X'). Geen action. - Server-render van layouts blijft
user.active_product_id(de seed-default) lezen, maar de NavBar leidt z'n weergegeven actieve product voor demo af uitpathname, zodat label en highlight kloppen met waar je daadwerkelijk bent.
Sprint-switch (geen DB-write).
- Vandaag: SprintSwitcher roept
setActiveSprintAction→ 403 → toast. - Nieuw: voor demo doet de switcher alleen
router.push('/products/X/sprint/Y'). De sprint-pagina is[sprintId]-driven, dus de juiste sprint laadt zonder datuser.settings.layout.activeSprints[X]ge-update hoeft te worden.
Filters / layout / selecties. Ongewijzigd. Te verifiëren dat alle UI-componenten
setPref gebruiken (niet rechtstreeks een server-action of fetch).
Stories & taken
ST-1345 — SprintSwitcher demo-fork
| Taak | Bestand | Beschrijving |
|---|---|---|
| T-950 | components/shared/sprint-switcher.tsx | Lees isDemo uit store + fork handleSwitchSprint |
| T-951 | __tests__/components/shared/sprint-switcher.test.tsx |
Vitest: demo vs niet-demo gedrag |
ST-1346 — NavBar demo-fork + URL-derived display
| Taak | Bestand | Beschrijving |
|---|---|---|
| T-952 | components/shared/nav-bar.tsx | Fork handleSwitchProduct voor demo |
| T-953 | components/shared/nav-bar.tsx | URL-derived displayActive voor label + highlight |
| T-954 | __tests__/components/shared/nav-bar.test.tsx |
Vitest: handler-fork + URL-derived display |
ST-1347 — ADR-0006 update + patroon-doc
| Taak | Bestand | Beschrijving |
|---|---|---|
| T-955 | docs/adr/0006-demo-user-three-layer-policy.md | "Updated 2026-05-11"-sectie met uitzondering |
| T-956 | docs/patterns/demo-client-state.md (optioneel) |
Patroon-doc + CLAUDE.md quickref-rij |
ST-1348 — Verificatie
| Taak | Bestand | Beschrijving |
|---|---|---|
| T-957 | __tests__/** |
Bestaande tests bijwerken die 403-toast voor demo verwachten |
| T-958 | n.v.t. | Browser-flow + DB-no-pollution + defense-in-depth + build |
Concrete code-wijzigingen (samengevat)
1. SprintSwitcher fork
components/shared/sprint-switcher.tsx:54:
const isDemo = useUserSettingsStore(s => s.context.isDemo)
function handleSwitchSprint(sprintId: string) {
if (sprintId === activeSprint?.id) return
if (isDemo) {
router.push(`/products/${productId}/sprint/${sprintId}`)
return
}
startTransition(async () => {
const result = await setActiveSprintAction(productId, sprintId)
// ... bestaande logica
})
}
2. NavBar fork + URL-derived display
components/shared/nav-bar.tsx:48:
const pathname = usePathname()
const urlProductId = pathname.match(/^\/products\/([^/]+)/)?.[1] ?? null
const displayActive = isDemo && urlProductId
? (products.find(p => p.id === urlProductId) ?? activeProduct)
: activeProduct
function handleSwitchProduct(productId: string) {
if (productId === activeProduct?.id) return
if (isDemo) {
router.push(`/products/${productId}`)
return
}
startTransition(async () => {
const result = await setActiveProductAction(productId)
// ... bestaande logica
})
}
In de render: vervang gebruik van activeProduct door displayActive in label,
highlight-class en onClick-equality-check.
3. ADR-0006 addendum
Nieuwe sectie "Updated 2026-05-11 — Exception for client-side UI preferences" na "Consequences" — zie T-955 implementation_plan voor volledige tekst.
4. Server-actions — geen wijziging
Alle 403-guards blijven:
Verificatie (end-to-end checklist)
npm run verify && npm run build
Functioneel — handmatig in browser (zie T-958):
- Reset:
psql $DATABASE_URL -c "UPDATE \"User\" SET settings='{}'::jsonb, active_product_id=NULL WHERE username='demo';" - Login als
demo/demo1234. /dashboard→ producten zichtbaar → klik ander product → URL klopt → label klopt → géén toast.- Backlog → status/priority filter wijzigen → werkt direct → géén POST in Network-tab.
- Sort op kolom → werkt direct.
- Sprint-switcher → andere sprint → URL klopt → board laadt → géén toast.
- Split-pane verslepen → blijft binnen sessie.
- Hard refresh → defaults terug (verwacht in-memory).
- Tweede tab → eigen state, geen kruisbestuiving.
Defense in depth:
- DevTools console:
await fetch('/api/products', {method:'POST',body:'{}'})→ 403. grep -rn "session.isDemo" actions/→ alle write-actions houden hun guard.
DB-no-pollution:
SELECT settings, active_product_id FROM "User" WHERE username='demo';→{}en NULL.
Tests:
npm test→ alle tests slagen, inclusief nieuwe NavBar/SprintSwitcher tests.
Risico's & mitigaties
| Risico | Mitigatie |
|---|---|
| Toekomstige UI-code roept per ongeluk een write-action aan voor demo | Server-action 403 blijft + nieuwe demo-client-state.md patroon-doc + ADR-0006 update |
Server-side render van NavBar toont seed-default activeProduct na product-switch |
URL-derived displayActive (T-953) |
setActiveSprintInSettings() in lib/active-sprint.ts:51 heeft geen interne demo-check (huidige tech debt) |
Buiten scope: alle bekende callers checken al session.isDemo. Eventueel apart op te pakken. |
| Demo verliest filterkeuze bij refresh | Acceptabel volgens vragenronde (in-memory gekozen). |