Scrum4Me/docs/plans/PBI-80-demo-prefs.md
Janpeter Visser 2b4b5bf719
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>
2026-05-12 20:03:40 +02:00

9 KiB

PBI-80 — Demo-gebruiker mag eigen UI-voorkeuren wijzigen

Stories: ST-1345, ST-1346, ST-1347, ST-1348 Branch: feat/demo-prefs Aangemaakt: 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.


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 uit pathname, 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 dat user.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):

  1. Reset: psql $DATABASE_URL -c "UPDATE \"User\" SET settings='{}'::jsonb, active_product_id=NULL WHERE username='demo';"
  2. Login als demo/demo1234.
  3. /dashboard → producten zichtbaar → klik ander product → URL klopt → label klopt → géén toast.
  4. Backlog → status/priority filter wijzigen → werkt direct → géén POST in Network-tab.
  5. Sort op kolom → werkt direct.
  6. Sprint-switcher → andere sprint → URL klopt → board laadt → géén toast.
  7. Split-pane verslepen → blijft binnen sessie.
  8. Hard refresh → defaults terug (verwacht in-memory).
  9. Tweede tab → eigen state, geen kruisbestuiving.

Defense in depth:

  1. DevTools console: await fetch('/api/products', {method:'POST',body:'{}'}) → 403.
  2. grep -rn "session.isDemo" actions/ → alle write-actions houden hun guard.

DB-no-pollution:

  1. SELECT settings, active_product_id FROM "User" WHERE username='demo';{} en NULL.

Tests:

  1. 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).