* 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>
4 KiB
| title | status | audience | language | last_updated | when_to_read | ||
|---|---|---|---|---|---|---|---|
| Demo client-state (UI-prefs zonder DB) | active |
|
nl | 2026-05-12 | Bij elk nieuw UI-element dat de demo-gebruiker zou willen kunnen wijzigen — filter, sortering, panel-state, geselecteerde scope (product/sprint), enz. |
Patroon: Demo client-state
De demo-gebruiker (session.isDemo === true) deelt één DB-rij met alle andere
demo-bezoekers. DB-writes voor demo zouden cross-bezoeker-pollution geven, dus
de three-layer policy uit ADR-0006
blokkeert ze. PBI-80 introduceert één uitzondering: client-side UI-state mag
gewijzigd worden, in-memory en zonder server-call.
Wanneer toepassen
| Soort wijziging | Voor demo? | Hoe |
|---|---|---|
| Filter / sortering / collapse / split-pane / selectie | Ja | useUserSettingsStore.setPref([...], value) — store regelt de demo-fork al |
| Wisselen van actief product of sprint | Ja | router.push('/products/...') zonder server-action |
| PBI/story/taak/sprint create/update/delete/reorder | Nee | Server-action met 403-guard blijft hard verplicht |
| Account, rollen, pairing, web-push | Nee | Idem |
Hoe isDemo lezen (client component)
import { useUserSettingsStore } from '@/stores/user-settings/store'
const isDemo = useUserSettingsStore(s => s.context.isDemo)
UserSettingsBridge hydrateert deze waarde in app/(app)/layout.tsx,
dus elke client child ziet meteen de juiste vlag.
Voorbeeld 1 — UI pref (filters, sort, layout)
Geen extra werk. De store-actie regelt de demo-fork zelf:
// Werkt voor alle gebruikers, demo + niet-demo
useUserSettingsStore.getState().setPref(
['views', 'pbiList', 'filterStatus'],
'OPEN',
)
Voor demo doet setPref een lokale Zustand-merge zonder server-call;
voor niet-demo gaat het via updateUserSettingsAction (DB + SSE).
Voorbeeld 2 — Scope-wissel (product/sprint)
Fork in de UI-handler — server-action blijft achter de fork onveranderd:
function handleSwitchProduct(productId: string) {
if (productId === activeId) return
if (isDemo) {
router.push(`/products/${productId}`)
return
}
startTransition(async () => {
const result = await setActiveProductAction(productId)
// ... bestaande not-demo flow
})
}
Voor pagina's waarvan de scope al in de URL zit (zoals /products/[id]/sprint/[sprintId])
is router.push met de gewenste path voldoende — server resolveert de
juiste data uit de URL-params.
Visuele consistentie na URL-only switch
Server-rendered layouts blijven voor demo de seed-default lezen
(user.active_product_id, user.settings.layout.activeSprints[...]). Als de
UI een "actief X"-label toont dat van de server-prop komt, leid het voor demo
af uit pathname:
const urlProductId = pathname.match(/^\/products\/([^/]+)/)?.[1] ?? null
const displayActive =
isDemo && urlProductId
? products.find(p => p.id === urlProductId) ?? activeProduct
: activeProduct
Gebruik displayActive in de render in plaats van de prop.
Verboden voor demo
- Server-action aanroepen zonder fork — 403 + onnodige toast.
- Wegschrijven naar cookies of localStorage — pollutie tussen bezoekers.
setActiveSprintInSettings/ vergelijkbare DB-helpers rechtstreeks aanroepen.- Web-push subscription registreren — schrijft naar gedeelde
PushSubscription-tabel.
Defense in depth
Server-actions (actions/active-product.ts, actions/active-sprint.ts,
actions/user-settings.ts) behouden hun if (session.isDemo) return 403-guard.
Als toekomstige UI-code per ongeluk de fork mist, faalt de call hard met 403 en
zien we het via toast/logs.
Zie ook
- ADR-0006 — three-layer beschermingen + de PBI-80-uitzondering.
- docs/patterns/proxy.md — proxy-laag die
/api/*-writes voor demo afvangt. - stores/user-settings/store.ts — bron
van waarheid voor
isDemo+setPrefmet demo-fork.