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
229
docs/plans/PBI-80-demo-prefs.md
Normal file
229
docs/plans/PBI-80-demo-prefs.md
Normal file
|
|
@ -0,0 +1,229 @@
|
|||
# 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.**
|
||||
- [stores/user-settings/store.ts:80](../../stores/user-settings/store.ts) — `setPref`
|
||||
heeft al een demo-fork (lokale merge zonder server-call).
|
||||
- [components/shared/user-settings-bridge.tsx:34](../../components/shared/user-settings-bridge.tsx)
|
||||
— 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 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](../../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](../../components/shared/nav-bar.tsx) | Fork `handleSwitchProduct` voor demo |
|
||||
| T-953 | [components/shared/nav-bar.tsx](../../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](../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](../../components/shared/sprint-switcher.tsx):
|
||||
|
||||
```tsx
|
||||
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](../../components/shared/nav-bar.tsx):
|
||||
|
||||
```tsx
|
||||
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:
|
||||
- [actions/active-product.ts:20](../../actions/active-product.ts)
|
||||
- [actions/active-sprint.ts:24](../../actions/active-sprint.ts)
|
||||
- [actions/user-settings.ts:28](../../actions/user-settings.ts)
|
||||
|
||||
---
|
||||
|
||||
## Verificatie (end-to-end checklist)
|
||||
|
||||
```bash
|
||||
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:**
|
||||
|
||||
10. DevTools console: `await fetch('/api/products', {method:'POST',body:'{}'})` → 403.
|
||||
11. `grep -rn "session.isDemo" actions/` → alle write-actions houden hun guard.
|
||||
|
||||
**DB-no-pollution:**
|
||||
|
||||
12. `SELECT settings, active_product_id FROM "User" WHERE username='demo';` → `{}` en NULL.
|
||||
|
||||
**Tests:**
|
||||
|
||||
13. `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](../../lib/active-sprint.ts) 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). |
|
||||
Loading…
Add table
Add a link
Reference in a new issue