feat(PBI-80): demo-user mag eigen UI-voorkeuren wijzigen (#194)
* 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>
This commit is contained in:
parent
2bef1a4c20
commit
2b4b5bf719
9 changed files with 667 additions and 8 deletions
129
docs/patterns/demo-client-state.md
Normal file
129
docs/patterns/demo-client-state.md
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
---
|
||||
title: "Demo client-state (UI-prefs zonder DB)"
|
||||
status: active
|
||||
audience: [ai-agent, contributor]
|
||||
language: nl
|
||||
last_updated: 2026-05-12
|
||||
when_to_read: "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](../adr/0006-demo-user-three-layer-policy.md)
|
||||
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)
|
||||
|
||||
```tsx
|
||||
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:
|
||||
|
||||
```tsx
|
||||
// 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:
|
||||
|
||||
```tsx
|
||||
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`:
|
||||
|
||||
```tsx
|
||||
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](../adr/0006-demo-user-three-layer-policy.md) — three-layer
|
||||
beschermingen + de PBI-80-uitzondering.
|
||||
- [docs/patterns/proxy.md](./proxy.md) — proxy-laag die `/api/*`-writes voor
|
||||
demo afvangt.
|
||||
- [stores/user-settings/store.ts](../../stores/user-settings/store.ts) — bron
|
||||
van waarheid voor `isDemo` + `setPref` met demo-fork.
|
||||
Loading…
Add table
Add a link
Reference in a new issue