docs(cleanup): archief verouderde plannen, backlog en root-duplicaten (#191)

* docs(cleanup): archief verouderde plannen, backlog en root-duplicaten

- 6 plans naar docs/old/plans/ (PBI-11/75/78, user-settings-store, Local github setup, lees-de-readme — laatste was verkeerde repo)
- docs/backlog/ naar docs/old/backlog/ (pre-MCP statische registry; live werk loopt via Scrum4Me-MCP)
- 6 root-level duplicaten naar docs/old/ (functional, {pbi,story,task}-dialog, product-backlog, backlog)
- 2 landing plans (niet uitgevoerd) krijgen archived: true frontmatter — blijven op plek maar uit INDEX
- scripts/generate-docs-index.mjs: skip docs/old/** + skip archived: true
- CLAUDE.md: rijen docs/backlog/, docs/plans/<key>-*.md, docs/manual/ weg; Track B-sectie verwijderd
- README.md / CHANGELOG.md / docs/plans/v1-readiness.md: link-fixes naar nieuwe locaties

Verify groen (lint + typecheck + 718 tests). docs/INDEX.md geregenereerd.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(cleanup): registreer handmatige verplaatsingen en fix referenties

- 4 plans verplaatst naar docs/old/plans/ (M10-qr-pairing-login, auto-pr-deploy-sync, docs-restructure-ai-lookup, v1-readiness)
- 3 archive-plans verplaatst naar docs/old/plans/ (archive-map nu leeg)
- ST-1114-copilot-reviews + 3 research-docs naar nieuwe docs/Ideas/ map
- Duplicaat docs/old/2026-04-27-m8-realtime-solo.md verwijderd (origineel zit in docs/old/plans/)
- Link-fixes naar nieuwe locaties:
  - CHANGELOG.md → docs/old/plans/v1-readiness.md
  - docs/runbooks/deploy-control.md → docs/old/plans/auto-pr-deploy-sync.md (2x)
  - docs/runbooks/worker-idempotency.md → docs/old/plans/auto-pr-deploy-sync.md
  - docs/plans/docs-restructure-pbi-spec.md → docs/old/plans/docs-restructure-ai-lookup.md (4x text + 2x href)
- docs/INDEX.md geregenereerd (96 docs, was 100)

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:
Janpeter Visser 2026-05-11 19:46:00 +02:00 committed by GitHub
parent d587be2fb3
commit b39c3ec2e1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
36 changed files with 1068 additions and 49 deletions

784
docs/old/backlog.md Normal file
View file

@ -0,0 +1,784 @@
---
title: "Scrum4Me — Implementatie Backlog"
status: active
audience: [maintainer, contributor]
language: nl
last_updated: 2026-05-03
---
# Scrum4Me — Implementatie Backlog
**Versie:** 0.1 — april 2026
**Volgt op:** Functionele Specificatie v0.2, Architectuur v0.1
---
## MVP-definitie
De MVP is klaar wanneer Lars — de primaire persona — de volledige cyclus kan doorlopen: een product aanmaken, een Product Backlog opbouwen met PBI's en stories, een Sprint plannen, taken aanmaken, en Claude Code de volgende story laten ophalen, implementeren en vastleggen — allemaal zonder hulp of handleiding. De app draait stabiel op Vercel en is volledig lokaal opzetbaar via één README.
---
## Milestone-overzicht
| Milestone | Doel | Tasks |
|---|---|---|
| M0: Foundation | Project, database, auth, navigatieshell | ST-001 ST-008 |
| M1: Producten & Product Backlog | Producten, PBI's, gesplitst scherm | ST-101 ST-110 |
| M2: Stories & Drag-and-drop | Stories als blokken, dnd-kit, Zustand | ST-201 ST-210 |
| M3: Sprint Backlog & Sprint Planning | Sprint aanmaken, stories slepen, taken | ST-301 ST-313 |
| M3.5: Solo Paneel & Story Assignment | Story-claim, persoonlijk Kanban-bord per product | ST-350 ST-360 |
| M4: Claude Code REST API | Alle endpoints, tokenbeheer | ST-401 ST-410 |
| M5: Todo-lijst | Todo CRUD, promotie naar PBI/story; Data Table + detail-kaart | ST-501 ST-506, ST-509 ST-510 |
| M6: Polish & Launch-ready | Foutafhandeling, toegankelijkheid, CI/CD, beveiliging | ST-601 ST-612 |
| M7: MCP-server voor Claude Code | Native MCP-laag bovenop Scrum4Me-DB (aparte repo `mcp`) | ST-701 ST-710 |
| M8: Realtime Solo Paneel | Live updates voor stories/tasks via SSE + Postgres LISTEN/NOTIFY | ST-801 ST-806 |
| M9: Actief Product Backlog | Persistente actieve PB-keuze, gesplitste navigatie, disabled-states | ST-901 ST-907 |
| M10: Password-loze inlog via QR-pairing | Mobiel als bevestigingskanaal voor desktop-login zonder wachtwoord | ST-1001 ST-1008 |
| M11: Claude vraagt, gebruiker antwoordt | Persistent vraag-antwoord-kanaal tussen Claude (MCP) en de actieve gebruiker | ST-1101 ST-1108 |
---
## Backlog
### M0: Foundation
- [x] **ST-001** Project scaffolding
- `create-next-app` met TypeScript strict, Tailwind CSS, App Router; installeer shadcn/ui, Zustand, dnd-kit, iron-session, bcrypt, Zod; configureer path aliases (`@/`)
- Done when: `npm run dev` start zonder fouten; `npm run lint` geeft geen errors; shadcn `Button` rendert op een testpagina
- [x] **ST-002** Prisma v7 setup + `prisma.config.ts`
- Installeer Prisma v7 + `@prisma/adapter-pg`; schrijf `prisma.config.ts` met `DATABASE_URL` via Zod-gevalideerde env; schrijf `lib/prisma.ts` singleton
- Done when: `npx prisma db push` slaagt; Prisma Client importeerbaar in een testbestand zonder fouten
- [x] **ST-003** Database schema migratie (volledige initiële migratie)
- Schrijf het volledige `schema.prisma` op basis van het architectuurdocument: `User`, `UserRole`, `ApiToken`, `Product`, `Pbi`, `Story`, `StoryLog`, `Sprint`, `Task`, `Todo`; alle enums, indexes, cascade deletes
- Done when: `npx prisma migrate dev --name init` slaagt; alle tabellen zichtbaar in DB-client; `npx prisma validate` geeft geen fouten
- [x] **ST-004** Seed met testdata
- Schrijf `prisma/seed.ts` op basis van het Product Backlog document (devplanner-product-backlog.md); seed één gebruiker, één product (Scrum4Me zelf), alle PBI's en stories als testdata; voeg demo-gebruiker toe
- Done when: `npx prisma db seed` slaagt; DB bevat alle PBI's en stories uit het backlog-document; demo-gebruiker aanwezig
- [x] **ST-005** Environment variabelen + `lib/env.ts`
- Schrijf Zod-schema voor alle env vars (`DATABASE_URL`, `DIRECT_URL`, `SESSION_SECRET`, `NODE_ENV`); exporteer gevalideerd `env` object; schrijf `.env.example` met instructies
- Done when: app gooit een begrijpelijke fout bij ontbrekende env var; `.env.example` volledig gedocumenteerd
- [x] **ST-006** Authenticatie — registratie en inloggen
- Schrijf `lib/auth.ts` (registreer met bcrypt hash, verifieer bij inloggen); schrijf `lib/session.ts` (iron-session config); implementeer `/register` en `/login` pagina's met Server Actions; sla `{ userId, isDemo }` op in sessiecookie
- Done when: registreren → ingelogde sessie → redirect `/dashboard`; inloggen met verkeerde credentials geeft generieke foutmelding; sessie blijft actief na paginaverversing
- [x] **ST-007** Route-beveiliging via `proxy.ts`
- Schrijf `proxy.ts` die sessiecookie-aanwezigheid controleert; redirect naar `/login` bij alle `/dashboard`, `/products/*`, `/todos`, `/settings/*` routes zonder sessiecookie; authenticated users worden van `/login` en `/register` doorgestuurd naar `/dashboard`; volledige sessievalidatie gebeurt server-side in de app layout
- Done when: directe navigatie naar `/dashboard` zonder sessie redirect naar `/login`; ingelogde gebruiker op `/login` redirect naar `/dashboard`
- [x] **ST-008** Navigatieshell + dashboard-layout
- Schrijf `app/(app)/layout.tsx` met navigatiebalk (logo, productenlink, todolink, instellingen, uitlogknop); implementeer uitlog Server Action; implementeer `/dashboard` als lege productenlijstpagina met "Maak je eerste product aan" lege staat; zet demo-badge zichtbaar als `isDemo === true`
- Done when: volledige auth-flow (register → login → dashboard → logout → login) werkt end-to-end; demo-gebruiker ziet badge in navigatie
---
### M1: Producten & Product Backlog
- [x] **ST-101** Product aanmaken
- `/products/new` pagina met formulier (naam, beschrijving, repo URL, definition of done); `createProduct` Server Action met Zod-validatie; uniekheidscontrole op naam per gebruiker; redirect naar `/products/[id]` na aanmaken
- Done when: product aangemaakt en zichtbaar op dashboard; dubbele naam geeft inline validatiefout; lege naam blokkeert submit
- [x] **ST-102** Productenlijst op dashboard
- Haal actieve producten op via Prisma Server Component; toon naam, beschrijving (ingekort 80 tekens), repo-link; lege staat met CTA; klikken opent Product Backlog
- Done when: twee producten zichtbaar na aanmaken; gearchiveerd product niet zichtbaar in standaardlijst
- [x] **ST-103** Product bewerken en archiveren
- Bewerkformulier (naam, beschrijving, repo URL, DoD) via Server Action; archiveerknop met bevestigingsdialoog; hersteloptie voor gearchiveerde producten; "toon gearchiveerd"-filter op dashboard
- Done when: naam bijwerken persisteert; archiveren verbergt product; herstel maakt het weer zichtbaar
- [x] **ST-104** Gesplitst scherm layout component (`SplitPane`)
- Bouw herbruikbaar `<SplitPane>` Client Component met versleepbare horizontale splitter; sla splitter-positie op in `localStorage` per sleutel; standaard 40/60 verhouding; minimale panelbreedte 200px; responsive fallback naar tabs op < 1024px
- Done when: splitter versleepbaar en positie behouden na paginaverversing; tabs getoond op smal scherm
- [x] **ST-105** Navigatiebar-component per paneel
- Bouw herbruikbaar `<PanelNavBar>` component met slots voor knoppen (aanmaken, filter, verwijderen); consistent design voor linker- en rechterpaneel
- Done when: navigatiebar herbruikt in minimaal twee gesplitste schermen zonder duplicatie
- [x] **ST-106** PBI aanmaken en weergeven
- Linkerpaneel van `/products/[id]`: haal PBI's op gegroepeerd op prioriteit en sort_order; "PBI aanmaken" knop opent inline formulier (titel, prioriteit); `createPbi` Server Action; nieuw PBI verschijnt onderaan de juiste prioriteitsgroep
- Done when: PBI aangemaakt en zichtbaar in juiste prioriteitsgroep; lege staat toont prompt
- [x] **ST-107** PBI prioriteitsgroepen met visuele scheiding
- Render PBI's gegroepeerd per prioriteit (14) met gelabelde scheidingslijn per groep (bijv. "Kritiek", "Hoog"); lege groepen zijn niet zichtbaar; prioriteitsbadge per PBI
- Done when: vier prioriteitsgroepen correct gerenderd met labels; PBI met prioriteit 1 staat boven prioriteit 4
- [x] **ST-108** PBI bewerken en verwijderen
- Inline bewerkingsmodus via dubbelklik of contextmenu (titel, omschrijving, prioriteit); `updatePbi` Server Action; verwijderen met bevestigingsdialoog inclusief waarschuwing cascade; `deletePbi` Server Action
- Done when: titelbewering opgeslagen zonder paginaverversing; verwijderen cascade-verwijdert stories (verifieerbaar in DB)
- [x] **ST-109** PBI selecteren → stories laden
- Klikken op PBI in linkerpaneel toont bijbehorende stories rechts via `useSelectionStore`; geselecteerd PBI visueel gemarkeerd; lege staat rechts als geen stories
- Done when: klikken op PBI A toont stories van A rechts; klikken op PBI B schakelt direct over
- [x] **ST-110** PBI filter
- Filterknop in linkerpaneel navigatiebar; dropdown voor prioriteit (14, alle); filter werkt realtime op gerenderde lijst; actief filter zichtbaar als badge; wissen via ×-knop
- Done when: filter op prioriteit 1 verbergt alle andere PBI's; wissen herstelt volledige lijst
---
### M2: Stories & Drag-and-drop
- [x] **ST-201** `usePlannerStore` Zustand-store
- Schrijf `stores/planner-store.ts` met `pbiOrder`, `storyOrder`, `taskOrder`; `init*`, `reorder*`, `rollback*` actions; TypeScript strict types
- Done when: store importeerbaar in een Client Component; `initPbis` vult order; `reorderPbis` muteert order; `rollbackPbis` herstelt vorige staat
- [x] **ST-202** `useSelectionStore` Zustand-store
- Schrijf `stores/selection-store.ts` met `selectedPbiId`, `selectedStoryId`, setters en `clearSelection`
- Done when: selectie in linkerpaneel via store zichtbaar in rechterpaneel zonder prop drilling
- [x] **ST-203** dnd-kit setup + PBI drag-and-drop
- Installeer dnd-kit; wrap linkerpaneel in `DndContext` + `SortableContext`; implementeer `useSortable` per PBI-rij; `onDragEnd`: bereken nieuwe `sort_order` via float-gemiddelde; optimistisch updaten via `usePlannerStore`; `reorderPbisAction` Server Action; rollback bij fout
- Done when: PBI versleepbaar binnen prioriteitsgroep; volgorde opgeslagen na loslaten; UI rollback bij gesimuleerde server-fout
- [x] **ST-204** PBI drag-and-drop over prioriteitsgrens
- Uitbreiding ST-203: slepen over een prioriteitsgrens wijzigt `priority` van het PBI; `sort_order` wordt onderaan de doelgroep geplaatst; `updatePbiPriority` Server Action
- Done when: PBI naar prioriteit 2 slepen vanuit prioriteit 3 wijzigt zowel prioriteit als volgorde
- [x] **ST-205** Story aanmaken en weergeven als blokken
- Rechterpaneel van Product Backlog: haal stories op voor geselecteerd PBI; render als blokken (~10% schermbreedte, horizontaal); elk blok toont titel (ingekort), prioriteitsbadge, statusbadge; "Story aanmaken" knop; `createStory` Server Action
- Done when: drie stories zichtbaar als blokken; nieuw blok verschijnt in juiste prioriteitsgroep
- [x] **ST-206** Story prioriteitsgroepen met visuele scheiding
- Groepeer story-blokken per prioriteit; gekleurde band of scheidingslijn per groep; blokken horizontaal gerangschikt per rij; nieuwe rij bij overloop
- Done when: stories van vier prioriteiten correct gescheiden weergegeven
- [x] **ST-207** Story drag-and-drop (horizontaal, binnen en tussen groepen)
- dnd-kit horizontale `SortableContext` per prioriteitsgroep; `onDragEnd`: herrangschikking via float-gemiddelde in `storyOrder`; slepen naar andere groep wijzigt prioriteit; optimistisch via `usePlannerStore`; `reorderStoriesAction` Server Action; rollback bij fout
- Done when: story versleepbaar binnen groep en naar andere groep; volgorde en prioriteit persistent na loslaten
- [x] **ST-208** Story detail-modal / slide-over
- Klikken op storyblok opent slide-over of modal met titel, omschrijving, acceptatiecriteria, statusbadge, activiteitenlog (leeg bij nieuwe story); bewerkformulier voor titel/omschrijving/acceptatiecriteria; `updateStory` Server Action
- Done when: klikken op blok opent detail; bewerken persisteert; sluiten keert terug naar backlog
- [x] **ST-209** Story verwijderen
- Verwijderknop in story-detail of contextmenu; bevestigingsdialoog met waarschuwing cascade (taken); `deleteStory` Server Action; blok verdwijnt optimistisch uit het rechterpaneel
- Done when: story verwijderd incl. cascade-taken (verifieerbaar in DB); blok direct verdwenen uit UI
- [x] **ST-210** Story filter in rechterpaneel
- Filterknop in rechterpaneel navigatiebar; filter op status (OPEN, IN_SPRINT, DONE) en prioriteit; realtime; actief filter als badge; wissbaar
- Done when: filter op OPEN verbergt IN_SPRINT stories
---
### M3: Sprint Backlog & Sprint Planning
- [x] **ST-301** `useSprintStore` Zustand-store
- Schrijf `stores/sprint-store.ts`; `initSprint`, `addStoryToSprint`, `removeStoryFromSprint`, `reorderSprintStories`, `rollbackSprint`
- Done when: store beheert sprint-story-volgorde onafhankelijk van planner-store
- [x] **ST-302** Sprint aanmaken
- "Sprint starten" knop op productpagina (zichtbaar als geen actieve Sprint); modal met Sprint Goal invoerveld; `createSprint` Server Action; max. 1 actieve Sprint per product afgedwongen in service-laag
- Done when: Sprint aangemaakt met Goal; tweede sprint aanmaken terwijl eerste actief is geeft foutmelding
- [x] **ST-303** Sprint Backlog scherm — layout
- `/products/[id]/sprint` pagina; `SplitPane` met Sprint Backlog links (stories in Sprint op volgorde) en rechts de Product Backlog stories gegroepeerd per PBI (inklapbaar); Sprint Goal zichtbaar bovenaan; lege staat links met instructie
- Done when: pagina rendert correct; Sprint Goal zichtbaar; beide panelen tonen juiste data
- [x] **ST-304** Story vanuit Product Backlog naar Sprint slepen
- dnd-kit drag vanuit rechterpaneel naar linkerpaneel; `onDragEnd`: `addStoryToSprint` in store; story krijgt badge "In Sprint" in Product Backlog; `addStoryToSprintAction` Server Action (zet `sprint_id` + status `IN_SPRINT`); rollback bij fout
- Done when: story gesleept naar Sprint verschijnt links en toont "In Sprint" badge rechts; persistent na herlaad
- [x] **ST-305** Sprint Backlog story volgorde aanpassen
- dnd-kit verticale `SortableContext` in linkerpaneel; herrangschikking via float-gemiddelde in `useSprintStore`; `reorderSprintStoriesAction` Server Action
- Done when: volgorde in Sprint Backlog persistent na loslaten en na paginaverversing
- [x] **ST-306** Story uit Sprint verwijderen
- Verwijderknop per story in Sprint Backlog; `removeStoryFromSprintAction` Server Action (wist `sprint_id`, zet status terug op `OPEN`); story verdwijnt links en badge verdwijnt rechts
- Done when: verwijderen persistent; story beschikbaar in Product Backlog rechterpaneel
- [x] **ST-307** Sprint Planning scherm — layout
- `/products/[id]/sprint/planning` pagina; `SplitPane` met Sprint Backlog stories links (op volgorde) en taken van geselecteerde story rechts; Sprint Goal zichtbaar; lege staat rechts als geen story geselecteerd
- Done when: pagina rendert; story selecteren links toont taken rechts
- [x] **ST-308** Taak aanmaken
- "Taak aanmaken" knop in rechterpaneel navigatiebar; inline formulier (titel, omschrijving, prioriteit); `createTask` Server Action; voortgangsindicator per story (bijv. "0/0 Done")
- Done when: taak aangemaakt en zichtbaar in takenlijst; voortgangsindicator toont "0/1 Done"
- [x] **ST-309** Taak drag-and-drop (verticaal)
- dnd-kit verticale `SortableContext` in rechterpaneel; herrangschikking via float-gemiddelde in `usePlannerStore.taskOrder`; `reorderTasksAction` Server Action
- Done when: taken versleepbaar; volgorde persistent na loslaten
- [x] **ST-310** Taakstatus bijhouden
- Status-toggle per taak (TO_DO → IN_PROGRESS → DONE) via klikbare badge of dropdown; `updateTaskStatus` Server Action; voortgangsindicator op story updatet optimistisch
- Done when: taak op DONE zetten verhoogt teller in voortgangsindicator; persistent na herlaad
- [x] **ST-311** Taak bewerken en verwijderen
- Inline bewerken van titel, omschrijving en prioriteit; `updateTask` Server Action; verwijderen met bevestiging; `deleteTask` Server Action
- Done when: titelwijziging persisteert; verwijderde taak verdwijnt uit lijst
- [x] **ST-312** Sprint afronden
- "Sprint afronden" knop op Sprint-pagina; dialoog toont per story de status en vraagt: "Markeer als Done of terug naar Backlog?"; `completeSprint` Server Action zet Sprint op COMPLETED, verwerkt keuzes per story
- Done when: Sprint afgerond; stories correct verplaatst naar DONE of OPEN; nieuwe Sprint aanmaakbaar
- [x] **ST-313** Sprint Board — drie-panelen layout (vervangt ST-303 + ST-307)
- **Doel:** `/products/[id]/sprint` wordt één scherm met drie panelen van links naar rechts: Product Backlog · Sprint Backlog · Taken. De losse `/sprint/planning` route wordt verwijderd (redirect → `/sprint`).
- **Panelen:**
- *Links — Product Backlog:* PBIs met stories gegroepeerd en inklapbaar; stories die al in sprint zijn grijs/disabled; klikken of slepen voegt story toe aan Sprint Backlog (midden)
- *Midden — Sprint Backlog:* stories in sprint op volgorde; klikken selecteert story → taken laden rechts; versleepbaar om te sorteren; trash-knop verwijdert uit sprint
- *Rechts — Taken:* `TaskList` voor de geselecteerde story; lege staat "Selecteer een story" als niets geselecteerd; "+ Taak" knop zoals huidig
- **Layout:** `TriplePane` component — drie verticale panelen met twee versleepbare scheidingslijnen; opslaan in `localStorage` per product (key: `sprint-triple-${productId}`)
- **DnD:** één `DndContext` omhult alle drie panelen; drag van links naar midden werkt via `DragOverlay`; reorder binnen midden via `SortableContext`; taken-reorder in eigen geneste `DndContext`
- **State:** `SprintBoardClient` beheert sprint stories, product backlog data, `selectedStoryId`, en taken per story (vanuit server props); `useSelectionStore.selectedStoryId` voor story-selectie
- **Navigatie:** "Sprint Planning →" link onderaan Sprint Backlog pagina verwijderd; `SprintHeader` blijft bovenaan met "Sprint afronden"
- **Route cleanup:** `/sprint/planning/page.tsx` vervangt door redirect naar `/products/[id]/sprint`; `PlanningLeft`, `PlanningRightClient` components verwijderen
- Done when: één `/sprint` pagina toont alle drie panelen; story slepen van links naar midden werkt; story selecteren toont taken rechts; taak aanmaken en sorteren werkt; pagina hervat na herlaad met juiste data; `/sprint/planning` redirect werkt
---
### M3.5: Solo Paneel & Story Assignment
> **Doel:** een persoonlijk Kanban-bord per product dat de taken toont van stories die geclaimd zijn door de ingelogde developer. Story-level assignment volgt het Scrum self-organizing principe: developers claimen vrijwillig stories (pull, niet push). Volledige technische specificatie in `specs/functional.md#solo-panel`.
- [x] **ST-350** Story.assignee_id schema-migratie + auth-helpers
- **Schema:** voeg `assignee_id String?` + `assignee User? @relation("StoryAssignee", fields: [assignee_id], references: [id], onDelete: SetNull)` toe aan `Story`; voeg `assigned_stories Story[] @relation("StoryAssignee")` toe aan `User`; voeg index `@@index([sprint_id, assignee_id])` toe; migratie via `prisma migrate dev --name add_story_assignee`
- **Auth-helpers:** schrijf `lib/auth.ts` met `getSession`, `requireUser`, `requireWriter`, `requireProductAccess`, `requireProductWriter` — laatste twee doen membership-check via owner (`Product.user_id`) OF lid (`ProductMember`); demo-check op basis van `session.isDemo` (uit ST-006); throwt *"Niet beschikbaar in demo-modus"* bij demo-write-poging
- Done when: migratie slaagt; `requireProductWriter` blokkeert demo-user; `requireProductAccess` accepteert zowel owner als member
- [x] **ST-351** `<UserAvatar>` herbruikbare component
- Wrapper rond shadcn `Avatar`; props: `userId`, `username`, `size` ('xs' | 'sm' | 'md' | 'lg'), `className`; `<AvatarImage src="/api/users/{userId}/avatar">` met fallback naar initialen (eerste 2 tekens username) op `bg-primary-container`; vier groottes via Tailwind classes
- Done when: avatar rendert in 4 sizes; bij ontbrekende avatar-data (404) fallback naar initialen zichtbaar; component bruikbaar in story-kaart, sprint board, instellingen
- [x] **ST-352** Story-claim Server Actions
- Vier acties in `actions/stories.ts`: `claimStoryAction` (zet `assignee_id = currentUserId`), `unclaimStoryAction` (null), `reassignStoryAction` (valideert dat target user lid van product is), `claimAllUnassignedInActiveSprintAction` (bulk via `updateMany` voor ongeclaimde stories in actieve sprint); allemaal Zod-gevalideerd, achter `requireProductWriter`, met `revalidatePath` voor `/sprint` én `/solo`; tenant-guard via `where: { id, product_id }`
- Done when: alle vier acties testbaar via testbestand; demo-user krijgt foutmelding; reassignment naar niet-lid faalt met foutmelding; bulk claimt alleen ongeclaimde
- [x] **ST-353** Sprint Board: assignee-chip + dropdown menu op story-kaart
- Op story-kaart in middenpaneel van ST-313 Sprint Board: assignee-chip onderaan met `<UserAvatar size="xs">` + username (of muted "Niet geclaimd" badge als `assignee_id === null`); shadcn `DropdownMenu` (3-dots rechtsboven) met items "Pak op" / "Geef terug aan team" / "Wijs toe aan ▶" (submenu met members); items conditioneel zichtbaar op basis van huidige assignee; demo-modus: dropdown disabled met tooltip "Niet beschikbaar in demo-modus"
- Done when: chip toont juiste state; dropdown roept juiste acties aan; revalidatie ververst kaart; toast "Story geclaimd" / "Toegewezen aan X" bij succes; demo-user ziet disabled-tooltip
- [x] **ST-354** Sprint Board: bulk-claim knop "Claim alle ongeclaimde"
- Knop bovenaan Sprint Backlog paneel met telling: "Claim alle ongeclaimde stories (N)"; disabled als N=0 of `isDemo`; klik roept `claimAllUnassignedInActiveSprintAction` aan; Sonner success-toast "{count} stories geclaimd"; pending state via `useTransition`
- Done when: telling correct; claimen werkt; knop disabled bij 0 ongeclaimd of demo; toast verschijnt na succes
- [x] **ST-355** Solo route — `/solo` redirect + `/products/[id]/solo` pagina + cookie
- **Cookie-helper:** schrijf `lib/cookies.ts` met `setLastProductCookie(productId)` (HTTP-only, sameSite lax, 30 dagen)
- **`/solo` page.tsx:** Server Component; leest cookie `lastProductId`; valideert toegang en redirect naar `/products/[id]/solo`, of toont `<ProductPicker>` als geen cookie of cookie ongeldig
- **`/products/[id]/solo` page.tsx:** Server Component; haalt active sprint op (404 → empty state `<NoActiveSprint>`); haalt taken op via `Task.findMany` met `where: { sprint_id, story: { assignee_id: session.userId } }` + count ongeclaimde stories parallel; geeft data door aan `<SoloBoard>`; zet `lastProductId` cookie bij elk bezoek
- **Empty state:** `<NoActiveSprint>` met titel, uitleg, link naar productpagina
- **`<ProductPicker>`:** lijst van toegankelijke producten, klikken redirect naar `/products/[id]/solo`
- Done when: `/solo` zonder cookie toont picker; met geldige cookie redirect; pagina toont juiste taken; geen actieve sprint toont empty state; cookie persisteert tussen sessies
- [x] **ST-356** Solo Kanban-bord met DnD en Zustand
- **Store `stores/solo-store.ts`:** `tasks`, `initTasks`, `optimisticMove(taskId, toStatus)` (returnt vorige status), `rollback(taskId, prevStatus)`, `updatePlan(taskId, plan)`; volgt patroon van `usePlannerStore` (ST-201)
- **`<SoloBoard>` Client Component:** root met `DndContext` (overslaan als `isDemo`), `PointerSensor` met `activationConstraint: { distance: 5 }`, `closestCorners` collision detection; header met productnaam, sprint goal, knop "Toon openstaande stories (N)"; grid met drie kolommen
- **`<SoloColumn>`:** drop target per status (`TO_DO` / `IN_PROGRESS` / `DONE`); header met statuskleur via MD3 tokens (`bg-status-todo/15` etc.); count en lege staat
- **`<SoloTaskCard>`:** hergebruik bestaande task-card (ST-310); draggable; toont prioriteit-indicator, taaktitel, story-titel; klik opent detail-dialoog (ST-357); demo: niet draggable
- **`onDragEnd` flow:** optimistische update via `optimisticMove`, dan `updateTaskStatusAction` aanroepen, op error rollback + Sonner error-toast "Status bijwerken mislukt — taak teruggeplaatst"; geen success-toast (te frequent)
- Done when: kaart sleepbaar tussen kolommen; status persisteert; gesimuleerde server-fout rollbackt UI; demo-user kan niet slepen
- [x] **ST-357** Task detail-dialoog + `updateTaskPlanAction`
- **`updateTaskPlanAction`** in `actions/tasks.ts`: Zod-schema `{ taskId, productId, implementationPlan }`; `requireProductWriter`; tenant-guard via `where: { id: taskId, story: { product_id: productId } }`; `revalidatePath`
- **`<TaskDetailDialog>`** shadcn `Dialog`: header met taaktitel + statusbadge (MD3 tokens); sectie *Beschrijving* (read-only, volg bestaand patroon); sectie *Implementatieplan* met `<Textarea>` save-on-blur; on-blur roept `updateTaskPlanAction`, indicator rechtsonder ("Bezig met opslaan…" → "Opgeslagen", vervaagt na 2s); error-toast bij fout; footer-link "Open in Sprint Board ↗"; demo-modus: textarea `readOnly` met tooltip
- Done when: edit + blur + refresh persisteert; gesimuleerde server-fout toont error-toast; demo-user kan dialoog openen maar niet bewerken
- [x] **ST-358** Openstaande stories sheet
- Knop "Toon openstaande stories (N)" bovenaan Solo bord opent shadcn `<Sheet>` (slide-out van rechts); inhoud: lijst van ongeclaimde stories in actieve sprint met titel, taakaantal, "Pak op"-knop per item; klik roept `claimStoryAction`, sheet blijft open (zodat meerdere achter elkaar claimen kan); Sonner success-toast per claim; lege staat "Geen ongeclaimde stories. Lekker bezig!"; pending state via `useFormStatus`; demo: knoppen disabled met tooltip
- Done when: sheet opent met N items; claimen verwijdert item uit lijst en verlaagt teller; lege staat correct; demo-user ziet sheet maar kan niet claimen
- [x] **ST-359** Navbar-link "Solo"
- Voeg `<NavLink href="/solo" icon={<UserSquare />}>Solo</NavLink>` toe aan navigatieshell (ST-008); altijd zichtbaar voor ingelogde users (geen product-context); plek tussen "Producten" en "Todos"
- Done when: link altijd zichtbaar in nav; klik gaat naar `/solo` en redirect verder
- [x] **ST-360** Demo-seed uitbreiden met geclaimde stories
- Update `prisma/seed.ts` (ST-004): demo-user (`is_demo = true`) heeft minimaal één product met ACTIVE sprint; minimaal 3 stories met `assignee_id = demoUser.id` (variërend over taakstatussen TO_DO, IN_PROGRESS, DONE); minimaal 1 ongeclaimde story (om "Toon openstaande" te demonstreren — demo-user kan niet claimen, ziet wel hoe het werkt)
- Done when: login als demo → Solo bord toont werkend Kanban met taken in alle drie kolommen; "Toon openstaande" sheet toont ten minste 1 story (claim-knoppen disabled)
---
### M4: Claude Code REST API
- [x] **ST-401** API-token infrastructuur
- Schrijf `lib/api-auth.ts`: parseer `Authorization: Bearer` header; bereken SHA-256 hash; zoek op in `api_tokens`; controleer `revoked_at`; retourneer `userId` of 401; retourneer 403 als `is_demo`
- Done when: geldige token geeft userId terug; ongeldige token geeft 401; ingetrokken token geeft 401; demo-token op schrijf-endpoint geeft 403
- [x] **ST-402** API-tokenbeheer UI
- `/settings/tokens` pagina; token aanmaken (label optioneel); token eenmalig getoond in kopieerbaar veld na aanmaken; tokenoverzicht (label, datum, actief/ingetrokken); intrekken via Server Action; max. 10 actieve tokens
- Done when: token aangemaakt en waarde zichtbaar; na sluiten dialoog niet meer te zien; intrekken maakt token onbruikbaar (getest via curl)
- [x] **ST-403** `GET /api/products` — productenlijst
- Route Handler; authenticatie via `api-auth.ts`; retourneer actieve producten `[{ id, name, repo_url }]` als JSON voor producten waar de tokengebruiker eigenaar of teamlid is
- Done when: `curl -H "Authorization: Bearer <token>" /api/products` retourneert correct JSON inclusief gedeelde product backlogs; 401 zonder token
- [x] **ST-404** `GET /api/products/:id/next-story` — volgende story ophalen
- Route Handler; haal hoogst geprioriteerde OPEN story op van actieve Sprint van het product (priority ASC, sort_order ASC); retourneer `{ id, title, description, acceptance_criteria, tasks[] }`; 404 als geen open stories
- Done when: endpoint retourneert eerste story van Sprint; 404 als Sprint leeg; 404 als geen actieve Sprint
- [x] **ST-405** `GET /api/sprints/:id/tasks` — taken ophalen
- Route Handler met `?limit=N` query param (default 10, max 50); retourneer taken van actieve Sprint op `(story.sort_order, task.priority, task.sort_order)` volgorde; retourneer `{ id, title, story_id, priority, sort_order, status }`
- Done when: endpoint retourneert max N taken in juiste volgorde; `?limit=5` retourneert max 5
- [x] **ST-406** `PATCH /api/stories/:id/tasks/reorder` — taakvolgorde aanpassen
- Route Handler; body: `{ task_ids: string[] }`; valideer alle IDs behoren tot de story; update `sort_order` via float-verdeling; retourneer `{ success: true }`
- Done when: volgorde in DB veranderd na PATCH; gewijzigde volgorde zichtbaar in Sprint Planning UI na herlaad; ongeldige task_id geeft 400
- [x] **ST-407** `POST /api/stories/:id/log` — activiteit vastleggen
- Route Handler; body: `{ type, content, status?, commit_hash?, commit_message? }`; Zod-validatie per type; schrijf naar `story_logs`; retourneer `{ id, created_at }`
- Done when: drie typen werken (IMPLEMENTATION_PLAN, TEST_RESULT, COMMIT); log-entry zichtbaar in story-detail UI na aanmaken via API; ontbrekend verplicht veld geeft 400
- [x] **ST-408** `PATCH /api/tasks/:id` — taakstatus en implementatieplan bijwerken
- Route Handler; body: `{ status?: "TO_DO" | "IN_PROGRESS" | "DONE", implementation_plan?: string }`; minimaal één veld verplicht; valideer dat taak aan requester's product behoort; retourneer `{ id, status, implementation_plan }`
- Done when: status update via API zichtbaar in Sprint Planning UI; implementation_plan opgeslagen en opvraagbaar; lege body geeft 400; andere gebruikers taak geeft 403
- [x] **ST-409** `POST /api/todos` — todo aanmaken
- Route Handler; body: `{ title: string, product_id: string }`; valideer dat product bij de geverifieerde gebruiker hoort; schrijf naar `todos`; retourneer `{ id, title, created_at }`
- Done when: todo aangemaakt via API met product_id verschijnt in todo-lijst UI gekoppeld aan het juiste product; lege titel of ontbrekend product_id geeft 400; onbekend product geeft 404
- [x] **ST-410** Story-activiteitenlog UI
- Activiteitenlog sectie in story-detail slide-over; haal `story_logs` op via Server Component; render chronologisch; visuele stijl per type (IMPLEMENTATION_PLAN = blauw, TEST_RESULT passed = groen, failed = rood, COMMIT = paars); commit-hash klikbaar als `repo_url` ingesteld; lege staat
- Done when: drie log-entries (plan, test, commit) correct gestyled; commit-hash link opent in nieuw tabblad
---
### M5: Todo-lijst
> **Herontwerp (april 2026):** ST-501505 beschreven de oorspronkelijke QuickInput-aanpak. Die is geïmplementeerd maar vervangen door een Data Table + detail-kaart ontwerp (ST-509510). ST-501505 zijn als referentie bewaard; de functionele eisen zijn ongewijzigd.
- [x] **ST-501** Todo-lijst pagina *(vervangen door ST-509)*
- `/todos` pagina; haal actieve (niet-gearchiveerde) todos op inclusief productnaam; snel-invoerveld bovenaan met product-dropdown (verplicht) en titel (Enter om op te slaan); `createTodo` Server Action; lege staat met instructie; productnaam-badge per todo-item
- Done when: todo aanmaken via Enter persisteert en verschijnt in lijst met productnaam; aanmaken zonder product geblokkeerd; lege staat zichtbaar bij geen todos
- [x] **ST-502** Todo afvinken *(vervangen door ST-509)*
- Checkbox per todo; `toggleTodo` Server Action; afgevinkte todos visueel doorgestreept; afgevinkte todos blijven zichtbaar onderaan de lijst
- Done when: afvinken persistent na herlaad; visuele doorstreping correct
- [x] **ST-503** Afgevinkte todos archiveren *(vervangen door ST-510)*
- "Archiveer afgeronde items" knop; `archiveCompletedTodos` Server Action; gearchiveerde todos verdwijnen uit standaardweergave
- Done when: archiveren verbergt alle afgevinkte todos; telling correct
- [x] **ST-504** Todo promoveren naar PBI *(vervangen door ST-510)*
- "Promoveer naar PBI" contextmenu of knop per todo; dialoog: product dropdown (actieve producten), prioriteit dropdown; titel vooringevuld (bewerkbaar); bevestigingswaarschuwing; `promoteTodeToPbi` Server Action (maak PBI aan, verwijder todo)
- Done when: gepromoveerde todo verdwijnt; PBI zichtbaar in juist product met juiste prioriteit
- [x] **ST-505** Todo promoveren naar story *(vervangen door ST-510)*
- "Promoveer naar story" knop per todo; dialoog: product dropdown → PBI dropdown (gefilterd op product), prioriteit; titel vooringevuld; `promoteTodoToStory` Server Action (maak story aan, verwijder todo)
- Done when: gepromoveerde todo verdwijnt; story zichtbaar in juist PBI met juiste prioriteit
- [x] **ST-509** Todo Data Table
- Installeer `@tanstack/react-table`; voeg shadcn `data-table`-patroon toe
- **Kolommen:**
- Selectie-checkbox (kolom 1): multi-select voor bulk-archivering; header-checkbox selecteert/deselecteert alle zichtbare rijen
- Titel (kolom 2): max 2 regels, `line-clamp-2 truncate`; afgevinkte todos doorgestreept; klik op rij (buiten checkbox) opent detail-kaart
- Productnaam-badge (kolom 3)
- Aanmaakdatum (kolom 4)
- **Toolbar boven de tabel:**
- Product-filter dropdown (Alles / Geen product / per product)
- "+" knop: opent lege detail-kaart voor nieuw aanmaken (erft geselecteerd filter-product)
- "Archiveer geselecteerde (N)" knop: actief zodra ≥ 1 checkbox aangevinkt; roept `archiveSelectedTodosAction` aan met de geselecteerde IDs; resettet selectie na afloop
- **Paginering:** max 10 rijen per pagina; vorige/volgende knoppen; paginatelling ("110 van 23")
- **Lege staat:** "Geen todo's voor deze selectie." bij lege filter; "Nog geen todo's. Gebruik + om er een aan te maken." bij volledig lege lijst
- **`archiveSelectedTodosAction`** toevoegen aan `actions/todos.ts`: valideert dat alle meegegeven IDs bij de ingelogde gebruiker horen vóór schrijven; `archiveMany` via `updateMany`
- Done when: tabel toont alle actieve todos; paginering werkt; product-filter werkt; selectie-checkbox selecteert meerdere rijen; bulk-archiveren verwijdert geselecteerde rijen uit de weergave
- [x] **ST-510** Todo detail-kaart
- Kaart onder de tabel; altijd zichtbaar (leeg als geen todo geselecteerd of aangemaakt wordt)
- **Aanmaken:** "+" in toolbar zet kaart in aanmaak-modus; velden: product-dropdown (erft filter-product, of "Geen product" bij "Alles"), titel; opslaan via `createTodoAction`; na opslaan kaart leegmaken en tabel ververst
- **Bewerken:** klik op tabelrij (buiten checkbox) laadt todo in kaart; velden: product-dropdown, titel, done-toggle; opslaan via nieuwe `updateTodoAction` (title + product_id + done); annuleren deselecteert rij en leegt kaart
- **Promoveren:** knoppen "→ PBI" en "→ Story" in de kaart; openen de bestaande `PromotePbiDialog` en `PromoteStoryDialog`; alleen zichtbaar bij een bestaande geselecteerde todo
- **Demo-modus:** kaart-invoervelden uitgeschakeld; knoppen verborgen of disabled
- **`updateTodoAction`** toevoegen aan `actions/todos.ts`: valideert eigenaarschap; past `title`, `product_id` en/of `done` aan; `revalidatePath('/todos')`
- Done when: aanmaken via kaart persisteert; bewerken van titel, product en done-status werkt; promote vanuit kaart opent juist dialoog en verwijdert todo na bevestiging; kaart leeg bij geen selectie; demo-gebruiker ziet uitgeschakelde kaart
- [x] **ST-506** Rolbeheer in instellingen
- `/settings` pagina met roltoewijzing (checkbox per rol: Product Owner, Scrum Master, Developer); minimaal één rol verplicht; `updateRoles` Server Action; geselecteerde rollen zichtbaar in profielbalk
- Done when: rollen bijwerken persisterend; profielbalk toont gekozen rollen; uitvinken van alle rollen geeft validatiefout
- [x] **ST-507** Gebruikersprofiel (buiten originele backlog toegevoegd)
- Profielfoto-upload (JPEG/PNG/WebP, max 12 MB), server-side resizing naar max 700×700 WebP met Sharp, opgeslagen als bytea in Neon; bio (max 160) en bio_detail (max 2000) als aparte velden; `POST /api/profile/avatar` + `GET /api/profile/avatar` + `updateProfileAction`
- Done when: foto geüpload en zichtbaar in instellingen; bio opgeslagen; ongeldige bestanden geweigerd vóór verwerking
- [x] **ST-508** Product Backlog-overzicht in instellingen (buiten originele backlog toegevoegd)
- Gecombineerde lijst op `/settings` van eigen producten (badge "Eigenaar") en team-lidmaatschappen (badge "Developer" + eigenaarsnaam); klikbaar naar product; "Verlaten"-knop met bevestiging voor lidmaatschappen; lege staat met CTA
- Done when: eigenaar-producten en team-producten zichtbaar in één lijst; verlaten werkt en verwijdert rij
- [x] **ST-511** Entity codes voor Product, PBI en Story (buiten originele backlog toegevoegd)
- **Schema:** `code String? @db.VarChar(30)` op `Product`, `Pbi` en `Story`; unique per parent (`user_id` voor Product, `product_id` voor Pbi/Story); `Task` heeft geen DB-veld — code wordt afgeleid als `${story.code}.${index_in_story}`
- **Auto-generatie:** `lib/code-server.ts` met `generateNextStoryCode` (`ST-001`, `ST-002`, … 3-cijferig per product) en `generateNextPbiCode` (`PBI-1`, `PBI-2`, … per product); `createWithCodeRetry`-helper vangt P2002 op het code-veld op en probeert max 3× opnieuw zodat gelijktijdige inserts niet crashen
- **Validatie:** Zod max 30 tekens, regex `^[A-Za-z0-9._-]+$`; handmatige override mag elk format dat aan de basis-regex voldoet (geen format-enforcement op `ST-NNN`)
- **Forms:** code-input op Product/Pbi/Story dialogen; auto-default zichtbaar als placeholder `auto`; field-level error-rendering onder code-input voor zowel create- als edit-mode (uniciteits-conflict, ongeldig format)
- **Display:** `CodeBadge` (`components/shared/code-badge.tsx`) consistent op dashboard product-list, PBI-list, story-blocks (Product Backlog), sprint board (alle drie panelen incl. PBI-headers), solo-bord task-cards, task-detail-dialoog, sprint-afronden-dialoog en de story-dialoog title; task-card toont derived `${story.code}.${index}`-badge rechtsboven uitgelijnd
- **Seed:** parser strip `ST-XXX:`-prefix uit titles, vult `code` apart; product `Scrum4Me` krijgt `code: 'SCRUM4ME'`, milestones krijgen `M0`/`M3.5`/etc., stories krijgen `ST-001…ST-612`
- Done when: auto-toegekende codes per product oplopend en uniek; race-conflict wordt opgevangen door retry-helper i.p.v. te crashen; handmatige duplicate code toont inline error onder de input in zowel create- als edit-mode; codes zichtbaar als badge in alle lijsten/cards/dialogen; seed verdeelt codes correct (8 PBI's met `M*`, 84 stories met `ST-NNN`)
- [x] **ST-512** REST API uitbreidingen voor codes, todo-description en task implementation_plan (buiten originele backlog toegevoegd)
- **`GET /api/products`:** voeg `code` toe (naast `id`, `name`, `repo_url`); optioneel `description` en `definition_of_done`
- **`GET /api/products/:id/next-story`:** voeg `code` toe op story; voeg per task `code` (derived `${story.code}.${index_in_story}`) en `implementation_plan` toe
- **`GET /api/sprints/:id/tasks`:** voeg `description`, `implementation_plan` en `story_code` toe per task; voeg een derived `code`-veld per task toe (`${story.code}.${index_in_story}`)
- **`POST /api/todos`:** accepteer optionele `description` (max 2000 tekens); valideer en sla op; retourneer `description` in response
- Done when: alle vier endpoints retourneren / accepteren de nieuwe velden zoals beschreven; curl-test toont `code` op products, story en tasks; todo aanmaken via API met `description` slaat op
- [x] **ST-513** REST API hardening voor Claude Code (buiten originele backlog toegevoegd)
- **Health:** nieuwe `GET /api/health` zonder auth; retourneert `{ status, version, time }`; optioneel `?db=1` voor DB-ping (`{ database: 'ok' | 'down' }`)
- **Claude-context:** nieuwe `GET /api/products/:id/claude-context` (auth) die in één call `product`, `active_sprint`, `next_story` (met tasks), en `open_todos` van de gebruiker terugbrengt — voorkomt round-trips
- **Status-case op API-boundary:** nieuwe `lib/task-status.ts` mapper; API exposeert lowercase (`todo`/`in_progress`/`review`/`done` voor tasks; `open`/`in_sprint`/`done` voor stories); DB blijft UPPER_SNAKE; UI ongewijzigd
- **`PATCH /api/tasks/:id`:** accepteert lowercase `status` via mapper; retourneert lowercase
- **Story-log metadata:** nieuwe optionele `metadata Json?` kolom op `StoryLog`; `POST /api/stories/:id/log` accepteert per type een optioneel `metadata`-veld (bv. `{ branch: 'feat/x' }`); bestaande velden ongewijzigd → backwards-compatible
- **Foutcodes:** Zod-validatie geeft `422` (was `400`); `400` blijft voor malformed body; `401`/`403`/`404`/`500` ongewijzigd
<<<<<<<< HEAD:docs/backlog/index.md
- **API-documentatie:** nieuwe `docs/api/rest-contract.md` met endpoints, request/response, foutcodes, status-enums en curl-voorbeelden; `CLAUDE.md` verwijst ernaar
- Done when: `curl /api/health` werkt zonder auth; `curl /api/products/:id/claude-context` retourneert bundled JSON; PATCH/PUT routes accepteren lowercase status en geven 422 bij ongeldige body; story-log POST bewaart `metadata`; `docs/api/rest-contract.md` is gepubliceerd
========
- **API-documentatie:** nieuwe `docs/api.md` met endpoints, request/response, foutcodes, status-enums en curl-voorbeelden; `CLAUDE.md` verwijst ernaar
- Done when: `curl /api/health` werkt zonder auth; `curl /api/products/:id/claude-context` retourneert bundled JSON; PATCH/PUT routes accepteren lowercase status en geven 422 bij ongeldige body; story-log POST bewaart `metadata`; `docs/api.md` is gepubliceerd
>>>>>>>> origin/main:docs/backlog.md
- **`GET /api/products`:** voeg `code` toe (naast `id`, `name`, `repo_url`); optioneel `description` en `definition_of_done`
- **`GET /api/products/:id/next-story`:** voeg `code` toe op story; voeg per task `code` (derived `${story.code}.${index_in_story}`) en `implementation_plan` toe
- **`GET /api/sprints/:id/tasks`:** voeg `description`, `implementation_plan` en `story_code` toe per task; voeg een derived `code`-veld per task toe (`${story.code}.${index_in_story}`)
- **`POST /api/todos`:** accepteer optionele `description` (max 2000 tekens); valideer en sla op; retourneer `description` in response
- **Backwards-compat:** alle wijzigingen zijn additief — bestaande clients negeren onbekende keys; nieuwe input-velden zijn optioneel
- Done when: alle vier endpoints retourneren / accepteren de nieuwe velden zoals beschreven; curl-test toont `code` op products, story en tasks; todo aanmaken via API met `description` slaat op
---
### M6: Polish & Launch-ready
- [x] **ST-601** Loading states en skeletons
- `loading.tsx` voor alle zware routes (`/products/[id]`, `/sprint`, `/sprint/planning`); skeletoncomponenten voor PBI-lijst, story-blokken en takenlijst; pending states op alle form-submit-knoppen via `useFormStatus`
- Done when: navigeren naar een trage route toont skeleton; submit-knoppen disablen tijdens Server Action
- [x] **ST-602** Error boundaries
- `error.tsx` voor alle beschermde routes; toon gebruiksvriendelijke foutmelding met "Probeer opnieuw" knop; log fout naar console (Sentry in M6)
- Done when: gesimuleerde Server Action-fout toont error boundary zonder witte pagina
- [x] **ST-603** Toast-notificaties (Sonner)
- Installeer Sonner; success-toast na aanmaken/bewerken/verwijderen van producten, PBI's, stories, taken, todos; error-toast bij mislukte Server Actions; toast niet bij drag-and-drop (te frequent)
- Done when: aanmaken van PBI toont success-toast; gesimuleerde netwerk-fout toont error-toast
- [x] **ST-604** Demo-gebruiker write-protection in UI
- Alle aanmaak-, bewerk- en verwijderknoppen disabled + tooltip "Niet beschikbaar in demo-modus" voor demo-sessies; gebaseerd op `isDemo` in sessie
- Done when: demo-gebruiker ziet alle knoppen maar kan niets wijzigen; tooltip zichtbaar bij hover
- [x] **ST-605** Keyboard-navigatie
- Tab-volgorde logisch in alle formulieren; Enter submits formulieren; Escape sluit modals/slide-overs; dnd-kit keyboard-drag (Space om te pakken, pijltjestoetsen, Space om te laten vallen)
- Done when: volledige PBI aanmaken-flow keyboard-only uitvoerbaar; dnd-kit drag via keyboard werkt
- [x] **ST-606** Desktop-first UI-review
- Test alle flows op 1280px, 1440px en 1920px breedte; fix overflow, uitlijning en proportie-issues; controleer minimum schermbreedte 1024px (toon melding bij smaller)
- Done when: alle M0M5 flows correct op drie schermbreedtes; melding bij < 1024px
- [x] **ST-607** Toegankelijkheid (WCAG AA)
- Kleurcontrast-check op alle tekst en badges; aria-labels op icon-only knoppen; focus-ring zichtbaar op alle interactieve elementen; `role` en `aria-selected` op geselecteerde PBI in linkerpaneel
- Done when: geen WCAG AA contrastfouten op primaire flows; alle knoppen hebben toegankelijke labels
- [x] **ST-608** Ratelimiting op auth-endpoints
- Max. 10 inlogpogingen per IP per minuut; max. 5 registraties per IP per uur; implementeer via in-memory counter (v1) of Vercel Edge middleware
- Done when: 11 snelle inlogpogingen leiden tot 429-respons met duidelijke melding
- [x] **ST-609** Beveiligingsreview API-endpoints
- Controleer alle Route Handlers: elke schrijfoperatie valideert dat de resource binnen de toegankelijke product-scope valt; cross-scope reads zijn onmogelijk tenzij de gebruiker via `product_members` gekoppeld is; voeg integratietests toe die cross-user toegang testen
- Done when: poging om een niet-gedeeld product van een andere gebruiker op te halen via API geeft 403 of 404; gedeelde producten zijn wel zichtbaar; getest met twee test-gebruikers
- [x] **ST-610** CI/CD via GitHub Actions
- Workflow: `lint` (ESLint), `typecheck` (tsc --noEmit), `prisma validate`, `build` (next build) op elke PR en push naar main; Vercel auto-deploy op main
- Done when: een TypeScript-fout in een PR blokkeert merge; succesvolle merge triggert Vercel-deploy
- [x] **ST-611** README en lokale setup-documentatie
- Schrijf `README.md` met: wat is Scrum4Me, quickstart lokaal (clone → env → prisma push → seed → dev), cloud deployment (Vercel + Neon stappenplan), API-documentatie (alle 7 endpoints met voorbeelden), Claude Code-integratie uitleg, Vercel Analytics status en directe dependencies zoals Sharp
- De in-app landingspagina (`/`) bevat al een gebruikershandleiding, Scrum-samenvatting en API-overzicht — de README richt zich op lokale setup en deployment
- Done when: iemand zonder context de app lokaal kan draaien op basis van alleen de README en `.env.example`
- [x] **ST-612** End-to-end acceptatietest
- Voer handmatig de volledige Lars-flow uit: product aanmaken → PBI's en stories aanmaken → Sprint starten → stories slepen → taken aanmaken → API-token aanmaken → curl `next-story` → curl `log` (plan, test, commit) → activiteitenlog controleren in UI
- Done when: volledige flow werkt zonder fouten of onverwacht gedrag; alle API-responses correct JSON
### M7: MCP-server voor Claude Code
Aparte repo: [`madhura68/scrum4me-mcp`](https://github.com/madhura68/scrum4me-mcp). Native Prisma-toegang (geen REST-tussenlaag), stdio-transport, Scrum4Me-schema gevendord als git submodule. Tokens hergebruikt uit `api_tokens`. v1 is alleen dev-flow tools — geen PBI/sprint-creatie of profielbeheer.
- [x] **ST-701** Repo-skeleton mcp
- npm init, tsconfig strict, .gitignore, MCP SDK 1.29, Prisma 7, zod, tsx; lege `src/index.ts` die op stdio start
- Done when: `npx tsx src/index.ts` print `running on stdio` zonder crash; `tsc --noEmit` slaagt
- [x] **ST-702** Schema-sync via git submodule
- Submodule `vendor/scrum4me`, `scripts/sync-schema.sh` kopieert `schema.prisma` en strip de `generator erd`-block, `npm run prisma:generate` als postinstall
- Done when: `npm run sync-schema && npm run prisma:generate` werkt op een verse clone
- [x] **ST-703** Auth en Prisma-singleton
- `src/auth.ts` SHA-256 hash van `SCRUM4ME_TOKEN` → lookup in `api_tokens`, cached `{ userId, isDemo }`; `requireWriteAccess()` throwt `PermissionDeniedError` voor demo
- `src/prisma.ts` lazy proxy zodat bootstrap niet crasht zonder `DATABASE_URL`
- Done when: ongeldig token geeft `SCRUM4ME_TOKEN is invalid or revoked`; demo-tokens blokkeren writes
- [x] **ST-704** Status-mappers + error-helpers
- `src/status.ts` zelfde mappers als REST `lib/task-status.ts`
- `src/errors.ts` `formatZodError`, `toolError`, `toolJson`, `withToolErrors` wrapper
- Done when: zod-fouten en `PermissionDenied` worden als gestructureerde MCP-errors teruggegeven
- [x] **ST-705** Read-tools — `health`, `list_products`, `get_claude_context`
- `health` doet `SELECT 1`; `list_products` met product-access filter; `get_claude_context` bundelt product + active sprint + next story (met tasks) + 50 open todos
- Done when: smoke-test tegen live DB groen voor alle drie
- [x] **ST-706** Write-tools tasks — `update_task_status`, `update_task_plan`
- Status-input lowercase (`todo|in_progress|review|done`), conversie via mapper; access-check via story → product → membership/owner
- Done when: niet-eigenaar krijgt 'not accessible'; demo geeft `PERMISSION_DENIED`
- [x] **ST-707** Log-tools — `log_implementation`, `log_test_result`, `log_commit`
- Append `StoryLog` met juiste `type`; optioneel `metadata` JSONB
- Done when: drie logs verschijnen in story-activiteit met `type`/`status`/`commit_hash`/`metadata` zoals meegegeven
- [x] **ST-708** `create_todo`-tool
- Optionele `description` (max 2000) en `product_id` (gevalideerd via access-check)
- Done when: nieuwe todo verschijnt in `/todos` voor de tokengebruiker
- [x] **ST-709** Prompt `implement_next_story`
- Workflow: `get_claude_context` → plan → log_implementation → per task `in_progress`/`done` → tests → `log_test_result``log_commit`
- Done when: prompt zichtbaar in MCP-clients met argument `product_id`
- [x] **ST-710** README + Claude Code-config + smoke-test
- README beschrijft setup, tools-tabel, schema-sync, `~/.claude/mcp_servers.json` snippet, risico's
- `scripts/smoke-test.ts` valideert read-tools tegen live DB
- Done when: smoke-test groen; MCP Inspector toont 9 tools + 1 prompt
### M8: Realtime Solo Paneel
Live updates voor stories en tasks in het Solo Paneel zonder pagina-refresh. Wanneer Claude Code (via MCP), Codex (via REST) of een andere browser-tab een task/story muteert, ziet de gebruiker het binnen 12 seconden in zijn kanban-bord.
Transport: Server-Sent Events (Vercel ondersteunt geen stateful WebSockets). Bron: Postgres `LISTEN/NOTIFY` via row-level triggers op `tasks` en `stories`. Eén-richting (server → client) — mutaties blijven via Server Actions/REST/MCP.
Filtering server-side: alleen events binnen de actieve sprint van een product waar de gebruiker eigenaar of lid van is, plus `assignee_id == userId` (eigen kolommen) of `assignee_id IS NULL` (claim-lijst).
- [x] **ST-801** Postgres LISTEN/NOTIFY-infrastructuur
- Migratie met `notify_solo_change()`-functie + `AFTER INSERT/UPDATE/DELETE`-triggers op `tasks` en `stories`; payload bevat `op`, `entity`, `id`, `product_id`, `sprint_id`, `assignee_id`, `fields` (gewijzigde kolommen)
- Done when: `psql $DIRECT_URL -c "LISTEN scrum4me_solo;"` toont een payload bij een UI-mutatie
- [x] **ST-802** SSE-route `/api/realtime/solo`
- `app/api/realtime/solo/route.ts`, `runtime: 'nodejs'`, `maxDuration: 300`; auth via iron-session, query-param `product_id`, opent `pg.Client` op `DIRECT_URL` met `LISTEN`; heartbeat 25s; hard close 240s; in-handler filtering op product/sprint/assignee
- Done when: `curl -N` op localhost levert binnen 1s een event op na een task-mutatie via UI
- [x] **ST-803** Client hook `useSoloRealtime(productId)`
- `lib/realtime/use-solo-realtime.ts`; opent `EventSource`, exponential backoff reconnect (1s → 30s); Page Visibility API voor pauseren/hervatten; cleanup op unmount
- Done when: tab wisselen sluit/opent connectie zichtbaar in DevTools Network
- [x] **ST-804** Solo-store realtime-acties
- `applyTaskUpdate`, `applyTaskCreate`, `applyTaskDelete`, `applyStoryAssignment`, `markPending`/`clearPending` om eigen optimistic-echo te onderdrukken
- Done when: unit-test op solo-store met gesimuleerde events laat juiste eindstate zien
- [x] **ST-805** Wire-up in SoloBoard + UI-indicator
- `components/solo/solo-board.tsx` roept de hook aan; klein "live"/"verbinden..."-statusindicator; toast bij langer dan 5s disconnected
- Done when: twee tabs van Solo Paneel — mutatie in tab A komt binnen 12s in tab B zonder refresh
- [x] **ST-806** Documentatie + acceptatietest
<<<<<<<< HEAD:docs/backlog/index.md
- Sectie "Realtime updates" in `docs/architecture.md` met diagram en filtering-regels; vermelding in `CLAUDE.md`; korte note over `/api/realtime/solo` in `docs/api/rest-contract.md`; handmatig E2E-scenario's gedraaid (zelfde gebruiker twee tabs, MCP-write, REST-write, story-claim, network-flap)
========
- Sectie "Realtime updates" in `docs/architecture.md` met diagram en filtering-regels; vermelding in `CLAUDE.md`; korte note over `/api/realtime/solo` in `docs/api.md`; handmatig E2E-scenario's gedraaid (zelfde gebruiker twee tabs, MCP-write, REST-write, story-claim, network-flap)
>>>>>>>> origin/main:docs/backlog.md
- Done when: alle scenario's lopen door zonder onverwachte gedragingen
Volledig plan in `.Plans/2026-04-27-m8-realtime-solo.md` (lokaal, niet gecommit).
### M9: Actief Product Backlog
**Implementatieplan:** [docs/plans/M9-active-product-backlog.md](./plans/M9-active-product-backlog.md)
Eén "actief Product Backlog" per gebruiker — persistent in DB. De NavBar wordt gesplitst in **Producten** (lijst) en **Product Backlog** (PB-view van actief PB), met **Sprint** en **Solo** als aparte tabs die op het actieve PB werken. Geen actief PB → die drie tabs zijn disabled. Vervangt de bestaande `last_product`-cookieflow.
- [x] **ST-901** Database — `user.active_product_id`
- Voeg `active_product_id String? @db.Uuid` toe aan `User` met FK naar `Product.id` en `onDelete: SetNull`; migratie `add_user_active_product_id`; index op `active_product_id` voor join-performance
- Done when: `npx prisma migrate dev` slaagt; `prisma studio` toont kolom; `npx prisma validate` zonder fouten; submodule `vendor/scrum4me` in mcp draait `prisma generate` + `tsc --noEmit` zonder fouten
- [x] **ST-902** Server Actions — actief product zetten en wissen
- `actions/active-product.ts` met `setActiveProduct(productId)` en `clearActiveProduct()`; Zod + auth + `productAccessFilter`; demo-gebruikers mogen wisselen (sessie-effect alleen, geen DB-write); `archiveProduct` en `leaveProduct` zetten `active_product_id` op `null` als het hetzelfde product betreft
- Done when: setActive met onbekend/onbereikbaar product → 422; archiveren van actief product clearet de keuze; demo-flow geeft toast "Niet beschikbaar in demo-modus"
- [x] **ST-903** App-layout laadt actief product + redirects
- `app/(app)/layout.tsx` haalt `activeProduct` (id, name, archived) op naast user; geef door aan `NavBar`; `app/(app)/solo/page.tsx` gebruikt `user.active_product_id` i.p.v. `getLastProductCookie`; helper `lib/cookies.ts:getLastProductCookie` markeren deprecated of verwijderen plus call-sites opruimen
- Done when: ingelogd zonder actief PB toont NavBar zonder geactiveerde tabs; met actief PB redirect `/solo``/products/[active]/solo` zonder cookie te raadplegen
- [x] **ST-904** NavBar — splits + disabled-states + switcher
- Tabs worden: **Producten** (`/dashboard`) | **Product Backlog** (`/products/[active]`) | **Sprint** (`/products/[active]/sprint`) | **Solo** (`/products/[active]/solo`) | **Todo's** (`/todos`); zonder actief PB zijn de middelste drie disabled-spans (zelfde stijl als huidige Sprint-disabled); productnaam in midden wordt een dropdown-trigger (shadcn `DropdownMenu`) met je producten + "Producten beheren →"; Sprint krijgt `aria-disabled` + tooltip "Geen actieve sprint" als er geen sprint met status `ACTIVE` is
- Done when: handmatige test: zonder PB drie tabs grijs; activeer PB → tabs klikbaar; dropdown wisselt PB en redirect naar Product Backlog; Sprint-tab disabled tot sprint gestart
- [x] **ST-905** Producten-scherm — Activeer-knop per rij
- `components/dashboard/product-list.tsx`: per rij "Activeer"-knop (verborgen voor reeds actief PB); actieve rij krijgt badge "Actief" (MD3-token `bg-primary-container`); klik op Activeer → `setActiveProduct` + `router.push('/products/[id]')`; ook in `/products/[id]` header een Activeer-knop als dat product nog niet actief is
- Done when: activeer in dashboard markeert juiste rij + landt op Product Backlog; demo-gebruiker krijgt toast en geen DB-mutatie
- [x] **ST-906** Edge cases — toegangsverlies en archivering
- Wanneer een PB wordt gearchiveerd, ge-leaved, of een productmember wordt verwijderd: `active_product_id` automatisch `null` voor betroffen users (server actions van `archiveProduct`, `leaveProduct`, `removeMember`); guard in `app/(app)/layout.tsx`: als `active_product_id` is gezet maar product is archived/onbereikbaar, server-side clear + redirect naar `/dashboard` met toast "Je actieve product is niet meer beschikbaar"
- Done when: scenario test — eigenaar archiveert → membership-gebruikers landen op dashboard met toast en active is gecleared
- [x] **ST-907** Documentatie en tests
- Functional spec: nieuw hoofdstuk "Actief Product Backlog" (concept, menugedrag, edge cases); README: navigatie-screenshot bijwerken; `docs/patterns/` indien nieuwe patroon (n.v.t. tenzij dropdown-switcher een herbruikbaar component wordt); jest-tests in `__tests__/actions/active-product.test.ts` voor setActive (toegang, demo, archived); Playwright/manueel scenario: log in → activeer PB → wissel via dropdown → archiveer → verifieer auto-clear
- Done when: `npm run lint && npx tsc --noEmit && npm test && npm run build` groen; spec-secties geschreven; `vendor/scrum4me`-submodule in mcp gesynced
---
### M10: Password-loze inlog via QR-pairing
**Implementatieplan:** [docs/plans/M10-qr-pairing-login.md](./plans/M10-qr-pairing-login.md)
Inloggen op een (publieke) desktop zonder wachtwoord: de desktop toont een QR-code, de gebruiker scant met een telefoon waar hij al ingelogd is, bevestigt expliciet, en de desktop is binnen 12 seconden ingelogd. Bouwt voort op de Postgres LISTEN/NOTIFY-infra van M8 (eigen kanaal `scrum4me_pairing`). Geen wachtwoord ingetypt op het publieke apparaat, geen credentials op de draad, demo-accounts geblokkeerd, paired-sessie heeft eigen kortere TTL (8 u) + `paired`-vlag voor toekomstige remote-revoke.
**Beveiligingsuitgangspunt:** `mobileSecret` reist alleen via QR-fragment (`#s=…`) → `location.hash` op de mobiel → POST-body. Desktop-SSE en claim authenticeren via een **HttpOnly pre-auth cookie** (`s4m_pair`, `Path=/api/auth/pair`, `Max-Age=120`, `SameSite=Lax`). Twee gescheiden hashes in DB (`secret_hash` voor mobiel-bewijs, `desktop_token_hash` voor desktop-bewijs) zodat geheim materiaal niet in URL-paden, querystrings, access logs, reverse-proxy logs, observability of browsergeschiedenis kan belanden.
Volledige flow + threat-model: `docs/patterns/qr-login.md` (op te leveren in ST-1008).
- [ ] **ST-1001** LoginPairing schema + Postgres-trigger
- **Schema:** `LoginPairing { id, secret_hash, desktop_token_hash, status, user_id?, desktop_ua?, desktop_ip?, created_at, expires_at, approved_at?, consumed_at? }`; back-relation `User.login_pairings`; `@@index([expires_at])`, `@@index([status, expires_at])`; `status` als string (`pending|approved|consumed|cancelled`); twee hash-kolommen scheiden mobiel-bewijs van desktop-bewijs
- **Trigger:** `notify_pairing_change()` + `AFTER INSERT/UPDATE` op `login_pairings`; `pg_notify('scrum4me_pairing', payload)` met `{ pairing_id, status, op }`; analoog aan `notify_solo_change` uit ST-801
- **Migratie:** `prisma migrate dev --name add_login_pairing`
- Done when: migratie slaagt; `psql $DIRECT_URL -c "LISTEN scrum4me_pairing;"` levert payload bij INSERT op `login_pairings`; beide hash-kolommen zijn `NOT NULL`
- [ ] **ST-1002** Pairing-helpers + sessie-uitbreiding + pre-auth-cookie
- **`lib/auth/pairing.ts`:** `generateMobileSecret()` en `generateDesktopToken()` (beide 32 bytes → base64url, los gegenereerd zodat ze elkaar niet onthullen), `hashToken(t)` (sha256-hex), `verifyToken(t, hash)` (timing-safe compare)
- **`lib/auth/pair-cookie.ts`:** `setPairCookie(response, desktopToken)` (`HttpOnly`, `Secure` in prod, `SameSite=Lax`, `Path=/api/auth/pair`, `Max-Age=120`); `readPairCookie(request)` returnt `desktopToken | null`; `clearPairCookie(response)` op claim/cancel
- **`SessionData` in `lib/session.ts`:** voeg optionele `paired?: boolean` en `pairedExpiresAt?: number` toe
- **`app/(app)/layout.tsx`:** extra guard — als `session.paired && session.pairedExpiresAt < Date.now()``session.destroy()` + `redirect('/login')`
- Done when: helpers hebben unit-tests; paired-sessie verloopt zichtbaar na vervaltijd; cookie wordt nooit door client-JS gelezen (HttpOnly-test)
- [ ] **ST-1003** `POST /api/auth/pair/start` — pairing aanmaken (anon)
- Route Handler zonder auth; leest UA + best-effort IP (`x-forwarded-for`); genereert los `mobileSecret` + `desktopToken`; insert `LoginPairing` met beide hashes, `status='pending'`, `expires_at = now() + 2 min`
- **Response body:** `{ pairingId, mobileSecret, expiresAt, qrUrl }``qrUrl = ${origin}/m/pair#id=…&s=…` (fragment, geen querystring)
- **Response header:** `Set-Cookie: s4m_pair=<desktopToken>; HttpOnly; Secure; SameSite=Lax; Path=/api/auth/pair; Max-Age=120`
- **Rate-limit:** patroon ST-608 (max 10 starts per IP per minuut)
- Done when: curl POST levert pairingId+mobileSecret in body en `s4m_pair`-cookie in header; 11e call binnen 60s geeft 429; rij in `login_pairings` zonder plaintext secret of desktop-token
- [ ] **ST-1004** SSE-route `/api/auth/pair/stream/[pairingId]` (cookie-auth)
- `runtime: 'nodejs'`, `maxDuration: 300`; pairingId in pad (niet sensitief), auth via `s4m_pair`-cookie: sha256(cookie) matcht `desktop_token_hash` van pairing met `pairingId` en `expires_at > now()`; anders 401
- **Geen query-parameters met geheim materiaal.** Browser stuurt cookie automatisch mee.
- Hergebruik LISTEN/NOTIFY-pattern uit `app/api/realtime/solo/route.ts` op kanaal `scrum4me_pairing`; filter notifications op `pairing_id`
- Auto-close bij status `consumed`/`cancelled` of na 240 s; heartbeat 25 s
- Done when: SSE-verbinding zonder `s4m_pair`-cookie geeft 401; met geldige cookie levert event binnen 1s na approve; stream sluit na consume; pairingId in URL is OK (niet vertrouwelijk)
- [ ] **ST-1005** Server actions + mobiele bevestigingspagina
- **`actions/pairing.ts`:** `getPairingForApproval(pairingId, mobileSecret)`, `approvePairing(pairingId, mobileSecret)` (demo-blokkade, hash-vergelijk tegen `secret_hash`, status pending→approved, bumpt `expires_at` +5 min, zet `user_id` + `approved_at`), `cancelPairing(pairingId, mobileSecret)`
- **`app/(app)/m/pair/page.tsx`:** Server Component achter de bestaande `(app)/layout.tsx` auth-guard; **leest géén query-params** — alleen statische uitleg + een client-island
- **`app/(app)/m/pair/pair-confirmation.tsx`:** Client Component die bij mount `window.location.hash` parseert (`#id=…&s=…`), via Server Action `getPairingForApproval` de UA/IP/username ophaalt, dan toont *"Inloggen op {ua} ({ip}) als {jouw-username}?"* met Bevestig/Annuleer-knoppen die `approvePairing`/`cancelPairing` aanroepen; succes-state *"Klaar — je kunt deze tab sluiten"*. Wist `location.hash` na approve zodat back/forward de secret niet onthult
- Demo-modus: approve geeft Nederlandse foutmelding (consistent ST-604)
- Done when: ingelogde mobiel ziet bevestigingspagina met UA + IP; secret komt nooit in een GET-URL voor; tap "Bevestig" zet status approved; demo-user ziet foutmelding en pairing blijft `pending`
- [ ] **ST-1006** `POST /api/auth/pair/claim` — desktop-cookie zetten (cookie-auth)
- Auth via `s4m_pair`-cookie (geen body-secret nodig); atomic update: `UPDATE login_pairings SET status='consumed', consumed_at=now() WHERE id=$1 AND status='approved' AND desktop_token_hash=$2 AND expires_at > now() RETURNING user_id`
- Bij rij geretourneerd: `getIronSession``session.userId = user.id; session.isDemo = user.is_demo; session.paired = true; session.pairedExpiresAt = Date.now() + 8h`; clear `s4m_pair`-cookie; anders 410 (al consumed) / 404 / 401
- Logging alleen `pairingId`, nooit cookie-waarde of mobileSecret
- Done when: claim met geldige cookie schrijft iron-session cookie en retourneert 200; tweede claim 410; ontbrekende/foute cookie 401; `s4m_pair` is na succes geclear'd
- [ ] **ST-1007** Desktop UI: QR-render + SSE-listener op `/login`
- **Dependency:** `qrcode.react` (client SVG; mobileSecret blijft op desktop in JS-geheugen)
- **`app/login/qr-login-button.tsx`:** Client Component; klik → POST `pair/start` (`credentials: 'same-origin'` zodat `s4m_pair`-cookie wordt geaccepteerd) → render QR met `qrUrl` (fragment-URL) → open `EventSource('/api/auth/pair/stream/<pairingId>', { withCredentials: true })` → bij `approved` event POST `pair/claim` (cookie-only) → bij succes `router.push('/dashboard')`; aftellende timer (2 min); bij timeout "Vernieuwen"-knop; cleanup bij unmount/redirect
<<<<<<<< HEAD:docs/backlog/index.md
- **`app/login/page.tsx`:** knop "Inloggen via mobiel" naast bestaande wachtwoord-form (MD3-tokens uit `docs/design/styling.md`)
========
- **`app/login/page.tsx`:** knop "Inloggen via mobiel" naast bestaande wachtwoord-form (MD3-tokens uit `docs/styling.md`)
>>>>>>>> origin/main:docs/backlog.md
- A11y: QR heeft alt-tekst met de URL voor screenreaders/copy-paste (de hash-suffix is onderdeel van die alt-tekst, niet van de page-URL die in browsergeschiedenis komt)
- Done when: end-to-end happy path werkt op localhost (twee browsers): A toont QR → B scant + bevestigt → A redirect naar `/dashboard` met `session.paired === true`; QR vernieuwt na expiry; geen secret zichtbaar in DevTools Network-tab onder URL-kolommen
- [ ] **ST-1008** Documentatie + acceptatietest
<<<<<<<< HEAD:docs/backlog/index.md
- **`docs/api/rest-contract.md`:** drie nieuwe endpoints (start/stream/claim) met request/response, cookie-mechaniek, foutcodes (400/401/403/404/410/422/429), curl-voorbeelden inclusief `--cookie-jar`
========
- **`docs/api.md`:** drie nieuwe endpoints (start/stream/claim) met request/response, cookie-mechaniek, foutcodes (400/401/403/404/410/422/429), curl-voorbeelden inclusief `--cookie-jar`
>>>>>>>> origin/main:docs/backlog.md
- **`docs/architecture.md`:** sectie "QR-pairing flow" met sequence-diagram + threat-model; expliciete subsectie *"Waarom geen secret in URL"* — fragments worden niet naar server gestuurd; SSE/claim authenticeren via HttpOnly cookie zodat secret-materiaal niet in access logs / reverse-proxy logs / observability-tools / browsergeschiedenis kan belanden
- **`docs/patterns/qr-login.md`:** nieuw pattern-doc voor toekomstige features die hetzelfde unauth-SSE-via-pre-auth-cookie-patroon willen hergebruiken
- **`CLAUDE.md`:** verwijzing naar het nieuwe pattern-doc in de patterns-tabel
- **Acceptatietest:** zeven scenario's handmatig: happy path, demo-block, replay, expiry tijdens pending, expiry tussen approve+claim, ontbrekende cookie op SSE/claim, secret niet aanwezig in `nginx`/Vercel access logs (controle via runtime-logs MCP-tool)
- Done when: docs gepubliceerd; alle zeven scenario's groen
---
### M11: Claude vraagt, gebruiker antwoordt
**Implementatieplan:** [docs/plans/M11-claude-questions.md](./plans/M11-claude-questions.md)
Persistent vraag-antwoord-kanaal tussen Claude Code (via MCP) en de actieve Scrum4Me-gebruiker. Claude schrijft een vraag naar `claude_questions` als hij vastloopt op een keuze; een Postgres-trigger emit op het bestaande `scrum4me_changes`-kanaal (uitgebreid met `entity: 'question'`); de Scrum4Me-app toont een notificatie-badge in de NavBar; iedereen met product-toegang kan antwoorden; Claude leest het antwoord (sync via polling met `wait_seconds`, of in een latere sessie via `get_question_answer`) en gaat door. Eerste concrete uitwerking van de AI-driven dev-flow-richting.
**Beveiligingsuitgangspunt:** atomic answer via `updateMany WHERE status='open'` voorkomt double-submit; demo-blok op zowel MCP-write-tools als Server Action; access-check via `productAccessFilter` in DB-query én SSE-filter; cron-endpoint voor expire-cleanup beveiligd met `Authorization: Bearer ${CRON_SECRET}`-header; logging alleen `question_id` (vraag/antwoord-tekst kan gevoelig materiaal bevatten).
- [ ] **ST-1101** `ClaudeQuestion` schema + Postgres-trigger
- **Schema:** `ClaudeQuestion { id, story_id, task_id?, product_id, asked_by, question, options?: Json, status, answer?, answered_by?, answered_at?, created_at, expires_at }`; relations op `User` (`asked_questions`, `answered_questions`), `Story`, `Task`, `Product`; indexes `(story_id, status)`, `(product_id, status)`, `(status, expires_at)`; `product_id` gedenormaliseerd voor SSE-filter
- **Trigger:** `notify_question_change()` `AFTER INSERT/UPDATE`; emit op `scrum4me_changes`-kanaal met payload `{ op, entity: 'question', id, product_id, story_id, task_id, assignee_id, status }`
- **Migratie:** `prisma migrate dev --name add_claude_questions`
- Done when: migratie slaagt; `psql LISTEN scrum4me_changes` toont nieuwe `entity: 'question'`-payload bij INSERT; bestaande solo-realtime-flow ongewijzigd; submodule sync na merge
- [ ] **ST-1102** MCP-tools voor Claude (in mcp-repo)
- **`ask_user_question`** (write): input `{ story_id, question, options?, task_id?, wait_seconds? }`; insert pairing + optioneel pollen tot `wait_seconds` (max 600); demo-blok via `requireWriteAccess`; access-check via `userCanAccessProduct(story.product_id, ...)`
- **`get_question_answer`** (read): haalt status + antwoord op een specifieke vraag op
- **`list_open_questions`** (read): lijst van eigen vragen (laatste 50, status open of answered)
- **`cancel_question`** (write): asker mag eigen vraag annuleren; status pending→cancelled
- Smoke-test in `scripts/smoke-test.ts`: `ask_user_question` met `wait_seconds=5` + parallel answer roundtrip
- Done when: MCP Inspector toont 4 nieuwe tools; smoke-test groen; demo-token op write-tools krijgt PERMISSION_DENIED; `tsc --noEmit` clean
- [ ] **ST-1103** Server Action `answerQuestion`
- `actions/questions.ts`: `answerQuestion(questionId, answer)` met getSession + Zod + demo-blok + `requireProductWriter` via `question.product_id`; atomic `updateMany WHERE status='open' AND expires_at>now`; `revalidatePath('/', 'layout')` voor badge-refresh
- Bij `count === 0`: disambigueer (al-answered/expired/access-fail) met begrijpelijke foutmelding
- Tests: 6 cases (happy, demo-block, geen access, race, expired, lege answer)
- Done when: `npm test` 6/6; handmatig: open vraag → antwoord → badge-count daalt met 1; demo-toast bij submit
- [ ] **ST-1104** User-scoped SSE-route `/api/realtime/notifications`
- Route Handler `runtime: 'nodejs'`, `maxDuration: 300`; auth via iron-session; **user-scoped** (geen product_id-param); filter `payload.entity === 'question'` én `payload.product_id` in user's accessible-product-ids
- Initial-state-event direct na connect (na LISTEN actief, conform M10 ST-1004 race-fix): summary-array van openstaande vragen voor deze user
- Update solo-route in `app/api/realtime/solo/route.ts`: in `shouldEmit` `if (payload.entity === 'question') return false` toevoegen — anders krijgen solo-clients ongewenst question-events
- Tests: 401 zonder cookie, filter op product-access, geen `entity:'question'`-events op solo-route
- Done when: `curl -N` levert events binnen 1s na INSERT; cross-product-test (user-A ziet user-B's vragen niet)
- [ ] **ST-1105** Notifications-UI (Bell + Sheet + Answer-modal + Zustand-store)
- **`stores/notifications-store.ts`** — Zustand store volgens `solo-store.ts`-patroon: `init`, `add`, `update`, `remove`, `optimisticAnswer`, `rollbackAnswer`; selectors `openCount`, `forYouCount`
- **`lib/realtime/use-notifications-realtime.ts`** — analoog aan `useSoloRealtime`; EventSource op `/api/realtime/notifications` met reconnect-backoff
- **`components/notifications/notifications-bridge.tsx`** — Server Component die initial-data fetcht en aan store geeft; mount in `app/(app)/layout.tsx` naast `<SoloRealtimeBridge />`
<<<<<<<< HEAD:docs/backlog/index.md
- **`components/shared/notifications-bell.tsx`** — Bell-icon (Lucide) met badge in NavBar (links van avatar); MD3-tokens uit `docs/design/styling.md`
========
- **`components/shared/notifications-bell.tsx`** — Bell-icon (Lucide) met badge in NavBar (links van avatar); MD3-tokens uit `docs/styling.md`
>>>>>>>> origin/main:docs/backlog.md
- **`components/notifications/notifications-sheet.tsx`** — shadcn Sheet van rechts; lijst gegroepeerd per product; story-assignee krijgt visuele *"wacht op jou"*-emphase
- **`components/notifications/answer-modal.tsx`** — shadcn Dialog; story-context-link, vraag-tekst, RadioGroup (als options) of Textarea (free-text), submit via `useTransition` + Server Action; demo-blok met tooltip
- Done when: bell + badge zichtbaar; klik opent Sheet met items; submit verwijdert item optimistisch; tweede tab van zelfde user ziet nieuwe vraag binnen 1-2s; demo-modus rendert maar Verstuur disabled
- [ ] **ST-1106** Demo-policy + access-tests
- Demo: Sheet rendert + Modal opent + Verstuur disabled met tooltip
- Access-isolation: cross-product test in `__tests__/api/notifications-stream.test.ts` (al gedeeltelijk in ST-1104)
- Story-assignee-emphase: visueel-only, toegang blijft product-membership-breed
- Done when: 4 access-tests groen; handmatige cross-product-verificatie
- [ ] **ST-1107** Vercel cron `expire-questions`
- **`app/api/cron/expire-questions/route.ts`** — POST handler beveiligd via `Authorization: Bearer ${CRON_SECRET}`; `updateMany WHERE status='open' AND expires_at<now → status='expired'`
- **`vercel.json`** — `crons` entry: `{ path: '/api/cron/expire-questions', schedule: '0 4 * * *' }` (dagelijks; Vercel Hobby-plan staat alleen daily crons toe)
- **`lib/env.ts`** + `.env.example``CRON_SECRET` via Zod
- Optioneel: ook M10's `login_pairings`-cleanup in dezelfde route opnemen
- Done when: handmatige `curl -X POST` met secret expireert oude rijen; Vercel-dashboard toont cron-config na deploy; onbevoegde call → 401
- [ ] **ST-1108** Documentatie + acceptatietest
<<<<<<<< HEAD:docs/backlog/index.md
- **`docs/api/rest-contract.md`:** secties "SSE — Notifications" + "Cron — Expire questions" met curl-voorbeelden
========
- **`docs/api.md`:** secties "SSE — Notifications" + "Cron — Expire questions" met curl-voorbeelden
>>>>>>>> origin/main:docs/backlog.md
- **`docs/architecture.md`:** sectie "Vraag-antwoord-kanaal Claude ↔ user" met Mermaid sequence-diagram + threat-model + "Waarom hergebruik scrum4me_changes-kanaal"
- **`docs/patterns/claude-question-channel.md`:** nieuw herbruikbaar pattern-doc voor toekomstige bidirectionele async-communicatie tussen MCP-agents en interactieve users
- **`CLAUDE.md`:** rij in Implementatiepatronen-tabel voor het nieuwe pattern
- **Acceptatietest** zes scenario's: sync happy (wait_seconds), async happy (geen wait), demo-block, access-isolation, expiry via cron, race op double-submit
- Done when: docs gepubliceerd; alle zes scenario's groen; backlog-parser-self-test toont M11 met ACTIVE-status
---
## v2 Backlog (na MVP)
- [ ] Uitnodigingsflow voor teams — e-mailuitnodiging of link-gebaseerd; nu kunnen alleen admins met toegang tot het systeem Developers toevoegen via gebruikersnaam
- [ ] Daily Scrum scherm — voortgang per story bijhouden tijdens Sprint
- [ ] Sprint Review scherm — demo en feedback vastleggen per story
- [ ] Sprint Retrospective scherm — reflectie per Sprint
- [ ] Automatische story-statusupdate na commit via API
- [ ] Velocity tracking — statistieken over meerdere Sprints
- [ ] Definition of Done per product configureerbaar (nu vaste instelling)
- [ ] Notificaties / reminders
- [ ] Timeline / kalenderweergave per Sprint
- [ ] Integratie GitHub Issues / Linear
- [ ] Mobiele app — uitsluitend taken afvinken
- [ ] Export van Product Backlog en Sprint als markdown of CSV
---
## Definition of MVP Done
- [x] Alle M0M6 tasks afgerond
- [x] Volledige Lars-flow succesvol doorlopen (ST-612)
- [x] Alle 7 API-endpoints getest via curl (ST-403 t/m ST-409)
- [x] Demo-gebruiker kan inloggen en heeft geen schrijfrechten (ST-604)
- [x] App lokaal opzetbaar via README zonder extra hulp (ST-611)
- [x] CI/CD actief — falende build blokkeert merge (ST-610)
- [x] Beveiligingsreview API geslaagd (ST-609)
- [x] Geen bekende blocker-bugs

View file

@ -0,0 +1 @@
# placeholder — remove when first file is added

828
docs/old/backlog/index.md Normal file
View file

@ -0,0 +1,828 @@
---
title: "Scrum4Me — Implementatie Backlog"
status: active
audience: [maintainer, contributor]
language: nl
last_updated: 2026-05-03
---
# Scrum4Me — Implementatie Backlog
**Versie:** 0.1 — april 2026
**Volgt op:** Functionele Specificatie v0.2, Architectuur v0.1
---
## MVP-definitie
De MVP is klaar wanneer Lars — de primaire persona — de volledige cyclus kan doorlopen: een product aanmaken, een Product Backlog opbouwen met PBI's en stories, een Sprint plannen, taken aanmaken, en Claude Code de volgende story laten ophalen, implementeren en vastleggen — allemaal zonder hulp of handleiding. De app draait stabiel op Vercel en is volledig lokaal opzetbaar via één README.
---
## Milestone-overzicht
| Milestone | Doel | Tasks |
|---|---|---|
| M0: Foundation | Project, database, auth, navigatieshell | ST-001 ST-008 |
| M1: Producten & Product Backlog | Producten, PBI's, gesplitst scherm | ST-101 ST-110 |
| M2: Stories & Drag-and-drop | Stories als blokken, dnd-kit, Zustand | ST-201 ST-210 |
| M3: Sprint Backlog & Sprint Planning | Sprint aanmaken, stories slepen, taken | ST-301 ST-313 |
| M3.5: Solo Paneel & Story Assignment | Story-claim, persoonlijk Kanban-bord per product | ST-350 ST-360 |
| M4: Claude Code REST API | Alle endpoints, tokenbeheer | ST-401 ST-410 |
| M5: Todo-lijst | Todo CRUD, promotie naar PBI/story; Data Table + detail-kaart | ST-501 ST-506, ST-509 ST-510 |
| M6: Polish & Launch-ready | Foutafhandeling, toegankelijkheid, CI/CD, beveiliging | ST-601 ST-612 |
| M7: MCP-server voor Claude Code | Native MCP-laag bovenop Scrum4Me-DB (aparte repo `mcp`) | ST-701 ST-710 |
| M8: Realtime Solo Paneel | Live updates voor stories/tasks via SSE + Postgres LISTEN/NOTIFY | ST-801 ST-806 |
| M9: Actief Product Backlog | Persistente actieve PB-keuze, gesplitste navigatie, disabled-states | ST-901 ST-907 |
| M10: Password-loze inlog via QR-pairing | Mobiel als bevestigingskanaal voor desktop-login zonder wachtwoord | ST-1001 ST-1008 |
| M11: Claude vraagt, gebruiker antwoordt | Persistent vraag-antwoord-kanaal tussen Claude (MCP) en de actieve gebruiker | ST-1101 ST-1108 |
| M12: Ideeën & Grill/Plan jobs | Idee-entity tussen Todo en PBI; interactief grillen + deterministisch materialiseren | ST-1192 ST-1201 |
---
## Backlog
### M0: Foundation
- [x] **ST-001** Project scaffolding
- `create-next-app` met TypeScript strict, Tailwind CSS, App Router; installeer shadcn/ui, Zustand, dnd-kit, iron-session, bcrypt, Zod; configureer path aliases (`@/`)
- Done when: `npm run dev` start zonder fouten; `npm run lint` geeft geen errors; shadcn `Button` rendert op een testpagina
- [x] **ST-002** Prisma v7 setup + `prisma.config.ts`
- Installeer Prisma v7 + `@prisma/adapter-pg`; schrijf `prisma.config.ts` met `DATABASE_URL` via Zod-gevalideerde env; schrijf `lib/prisma.ts` singleton
- Done when: `npx prisma db push` slaagt; Prisma Client importeerbaar in een testbestand zonder fouten
- [x] **ST-003** Database schema migratie (volledige initiële migratie)
- Schrijf het volledige `schema.prisma` op basis van het architectuurdocument: `User`, `UserRole`, `ApiToken`, `Product`, `Pbi`, `Story`, `StoryLog`, `Sprint`, `Task`, `Todo`; alle enums, indexes, cascade deletes
- Done when: `npx prisma migrate dev --name init` slaagt; alle tabellen zichtbaar in DB-client; `npx prisma validate` geeft geen fouten
- [x] **ST-004** Seed met testdata
- Schrijf `prisma/seed.ts` op basis van het Product Backlog document (devplanner-product-backlog.md); seed één gebruiker, één product (Scrum4Me zelf), alle PBI's en stories als testdata; voeg demo-gebruiker toe
- Done when: `npx prisma db seed` slaagt; DB bevat alle PBI's en stories uit het backlog-document; demo-gebruiker aanwezig
- [x] **ST-005** Environment variabelen + `lib/env.ts`
- Schrijf Zod-schema voor alle env vars (`DATABASE_URL`, `DIRECT_URL`, `SESSION_SECRET`, `NODE_ENV`); exporteer gevalideerd `env` object; schrijf `.env.example` met instructies
- Done when: app gooit een begrijpelijke fout bij ontbrekende env var; `.env.example` volledig gedocumenteerd
- [x] **ST-006** Authenticatie — registratie en inloggen
- Schrijf `lib/auth.ts` (registreer met bcrypt hash, verifieer bij inloggen); schrijf `lib/session.ts` (iron-session config); implementeer `/register` en `/login` pagina's met Server Actions; sla `{ userId, isDemo }` op in sessiecookie
- Done when: registreren → ingelogde sessie → redirect `/dashboard`; inloggen met verkeerde credentials geeft generieke foutmelding; sessie blijft actief na paginaverversing
- [x] **ST-007** Route-beveiliging via `proxy.ts`
- Schrijf `proxy.ts` die sessiecookie-aanwezigheid controleert; redirect naar `/login` bij alle `/dashboard`, `/products/*`, `/todos`, `/settings/*` routes zonder sessiecookie; authenticated users worden van `/login` en `/register` doorgestuurd naar `/dashboard`; volledige sessievalidatie gebeurt server-side in de app layout
- Done when: directe navigatie naar `/dashboard` zonder sessie redirect naar `/login`; ingelogde gebruiker op `/login` redirect naar `/dashboard`
- [x] **ST-008** Navigatieshell + dashboard-layout
- Schrijf `app/(app)/layout.tsx` met navigatiebalk (logo, productenlink, todolink, instellingen, uitlogknop); implementeer uitlog Server Action; implementeer `/dashboard` als lege productenlijstpagina met "Maak je eerste product aan" lege staat; zet demo-badge zichtbaar als `isDemo === true`
- Done when: volledige auth-flow (register → login → dashboard → logout → login) werkt end-to-end; demo-gebruiker ziet badge in navigatie
---
### M1: Producten & Product Backlog
- [x] **ST-101** Product aanmaken
- `/products/new` pagina met formulier (naam, beschrijving, repo URL, definition of done); `createProduct` Server Action met Zod-validatie; uniekheidscontrole op naam per gebruiker; redirect naar `/products/[id]` na aanmaken
- Done when: product aangemaakt en zichtbaar op dashboard; dubbele naam geeft inline validatiefout; lege naam blokkeert submit
- [x] **ST-102** Productenlijst op dashboard
- Haal actieve producten op via Prisma Server Component; toon naam, beschrijving (ingekort 80 tekens), repo-link; lege staat met CTA; klikken opent Product Backlog
- Done when: twee producten zichtbaar na aanmaken; gearchiveerd product niet zichtbaar in standaardlijst
- [x] **ST-103** Product bewerken en archiveren
- Bewerkformulier (naam, beschrijving, repo URL, DoD) via Server Action; archiveerknop met bevestigingsdialoog; hersteloptie voor gearchiveerde producten; "toon gearchiveerd"-filter op dashboard
- Done when: naam bijwerken persisteert; archiveren verbergt product; herstel maakt het weer zichtbaar
- [x] **ST-104** Gesplitst scherm layout component (`SplitPane`)
- Bouw herbruikbaar `<SplitPane>` Client Component met versleepbare horizontale splitter; sla splitter-positie op in `localStorage` per sleutel; standaard 40/60 verhouding; minimale panelbreedte 200px; responsive fallback naar tabs op < 1024px
- Done when: splitter versleepbaar en positie behouden na paginaverversing; tabs getoond op smal scherm
- [x] **ST-105** Navigatiebar-component per paneel
- Bouw herbruikbaar `<PanelNavBar>` component met slots voor knoppen (aanmaken, filter, verwijderen); consistent design voor linker- en rechterpaneel
- Done when: navigatiebar herbruikt in minimaal twee gesplitste schermen zonder duplicatie
- [x] **ST-106** PBI aanmaken en weergeven
- Linkerpaneel van `/products/[id]`: haal PBI's op gegroepeerd op prioriteit en sort_order; "PBI aanmaken" knop opent inline formulier (titel, prioriteit); `createPbi` Server Action; nieuw PBI verschijnt onderaan de juiste prioriteitsgroep
- Done when: PBI aangemaakt en zichtbaar in juiste prioriteitsgroep; lege staat toont prompt
- [x] **ST-107** PBI prioriteitsgroepen met visuele scheiding
- Render PBI's gegroepeerd per prioriteit (14) met gelabelde scheidingslijn per groep (bijv. "Kritiek", "Hoog"); lege groepen zijn niet zichtbaar; prioriteitsbadge per PBI
- Done when: vier prioriteitsgroepen correct gerenderd met labels; PBI met prioriteit 1 staat boven prioriteit 4
- [x] **ST-108** PBI bewerken en verwijderen
- Inline bewerkingsmodus via dubbelklik of contextmenu (titel, omschrijving, prioriteit); `updatePbi` Server Action; verwijderen met bevestigingsdialoog inclusief waarschuwing cascade; `deletePbi` Server Action
- Done when: titelbewering opgeslagen zonder paginaverversing; verwijderen cascade-verwijdert stories (verifieerbaar in DB)
- [x] **ST-109** PBI selecteren → stories laden
- Klikken op PBI in linkerpaneel toont bijbehorende stories rechts via `useSelectionStore`; geselecteerd PBI visueel gemarkeerd; lege staat rechts als geen stories
- Done when: klikken op PBI A toont stories van A rechts; klikken op PBI B schakelt direct over
- [x] **ST-110** PBI filter
- Filterknop in linkerpaneel navigatiebar; dropdown voor prioriteit (14, alle); filter werkt realtime op gerenderde lijst; actief filter zichtbaar als badge; wissen via ×-knop
- Done when: filter op prioriteit 1 verbergt alle andere PBI's; wissen herstelt volledige lijst
---
### M2: Stories & Drag-and-drop
- [x] **ST-201** `usePlannerStore` Zustand-store
- Schrijf `stores/planner-store.ts` met `pbiOrder`, `storyOrder`, `taskOrder`; `init*`, `reorder*`, `rollback*` actions; TypeScript strict types
- Done when: store importeerbaar in een Client Component; `initPbis` vult order; `reorderPbis` muteert order; `rollbackPbis` herstelt vorige staat
- [x] **ST-202** `useSelectionStore` Zustand-store
- Schrijf `stores/selection-store.ts` met `selectedPbiId`, `selectedStoryId`, setters en `clearSelection`
- Done when: selectie in linkerpaneel via store zichtbaar in rechterpaneel zonder prop drilling
- [x] **ST-203** dnd-kit setup + PBI drag-and-drop
- Installeer dnd-kit; wrap linkerpaneel in `DndContext` + `SortableContext`; implementeer `useSortable` per PBI-rij; `onDragEnd`: bereken nieuwe `sort_order` via float-gemiddelde; optimistisch updaten via `usePlannerStore`; `reorderPbisAction` Server Action; rollback bij fout
- Done when: PBI versleepbaar binnen prioriteitsgroep; volgorde opgeslagen na loslaten; UI rollback bij gesimuleerde server-fout
- [x] **ST-204** PBI drag-and-drop over prioriteitsgrens
- Uitbreiding ST-203: slepen over een prioriteitsgrens wijzigt `priority` van het PBI; `sort_order` wordt onderaan de doelgroep geplaatst; `updatePbiPriority` Server Action
- Done when: PBI naar prioriteit 2 slepen vanuit prioriteit 3 wijzigt zowel prioriteit als volgorde
- [x] **ST-205** Story aanmaken en weergeven als blokken
- Rechterpaneel van Product Backlog: haal stories op voor geselecteerd PBI; render als blokken (~10% schermbreedte, horizontaal); elk blok toont titel (ingekort), prioriteitsbadge, statusbadge; "Story aanmaken" knop; `createStory` Server Action
- Done when: drie stories zichtbaar als blokken; nieuw blok verschijnt in juiste prioriteitsgroep
- [x] **ST-206** Story prioriteitsgroepen met visuele scheiding
- Groepeer story-blokken per prioriteit; gekleurde band of scheidingslijn per groep; blokken horizontaal gerangschikt per rij; nieuwe rij bij overloop
- Done when: stories van vier prioriteiten correct gescheiden weergegeven
- [x] **ST-207** Story drag-and-drop (horizontaal, binnen en tussen groepen)
- dnd-kit horizontale `SortableContext` per prioriteitsgroep; `onDragEnd`: herrangschikking via float-gemiddelde in `storyOrder`; slepen naar andere groep wijzigt prioriteit; optimistisch via `usePlannerStore`; `reorderStoriesAction` Server Action; rollback bij fout
- Done when: story versleepbaar binnen groep en naar andere groep; volgorde en prioriteit persistent na loslaten
- [x] **ST-208** Story detail-modal / slide-over
- Klikken op storyblok opent slide-over of modal met titel, omschrijving, acceptatiecriteria, statusbadge, activiteitenlog (leeg bij nieuwe story); bewerkformulier voor titel/omschrijving/acceptatiecriteria; `updateStory` Server Action
- Done when: klikken op blok opent detail; bewerken persisteert; sluiten keert terug naar backlog
- [x] **ST-209** Story verwijderen
- Verwijderknop in story-detail of contextmenu; bevestigingsdialoog met waarschuwing cascade (taken); `deleteStory` Server Action; blok verdwijnt optimistisch uit het rechterpaneel
- Done when: story verwijderd incl. cascade-taken (verifieerbaar in DB); blok direct verdwenen uit UI
- [x] **ST-210** Story filter in rechterpaneel
- Filterknop in rechterpaneel navigatiebar; filter op status (OPEN, IN_SPRINT, DONE) en prioriteit; realtime; actief filter als badge; wissbaar
- Done when: filter op OPEN verbergt IN_SPRINT stories
---
### M3: Sprint Backlog & Sprint Planning
- [x] **ST-301** `useSprintStore` Zustand-store
- Schrijf `stores/sprint-store.ts`; `initSprint`, `addStoryToSprint`, `removeStoryFromSprint`, `reorderSprintStories`, `rollbackSprint`
- Done when: store beheert sprint-story-volgorde onafhankelijk van planner-store
- [x] **ST-302** Sprint aanmaken
- "Sprint starten" knop op productpagina (zichtbaar als geen actieve Sprint); modal met Sprint Goal invoerveld; `createSprint` Server Action; max. 1 actieve Sprint per product afgedwongen in service-laag
- Done when: Sprint aangemaakt met Goal; tweede sprint aanmaken terwijl eerste actief is geeft foutmelding
- [x] **ST-303** Sprint Backlog scherm — layout
- `/products/[id]/sprint` pagina; `SplitPane` met Sprint Backlog links (stories in Sprint op volgorde) en rechts de Product Backlog stories gegroepeerd per PBI (inklapbaar); Sprint Goal zichtbaar bovenaan; lege staat links met instructie
- Done when: pagina rendert correct; Sprint Goal zichtbaar; beide panelen tonen juiste data
- [x] **ST-304** Story vanuit Product Backlog naar Sprint slepen
- dnd-kit drag vanuit rechterpaneel naar linkerpaneel; `onDragEnd`: `addStoryToSprint` in store; story krijgt badge "In Sprint" in Product Backlog; `addStoryToSprintAction` Server Action (zet `sprint_id` + status `IN_SPRINT`); rollback bij fout
- Done when: story gesleept naar Sprint verschijnt links en toont "In Sprint" badge rechts; persistent na herlaad
- [x] **ST-305** Sprint Backlog story volgorde aanpassen
- dnd-kit verticale `SortableContext` in linkerpaneel; herrangschikking via float-gemiddelde in `useSprintStore`; `reorderSprintStoriesAction` Server Action
- Done when: volgorde in Sprint Backlog persistent na loslaten en na paginaverversing
- [x] **ST-306** Story uit Sprint verwijderen
- Verwijderknop per story in Sprint Backlog; `removeStoryFromSprintAction` Server Action (wist `sprint_id`, zet status terug op `OPEN`); story verdwijnt links en badge verdwijnt rechts
- Done when: verwijderen persistent; story beschikbaar in Product Backlog rechterpaneel
- [x] **ST-307** Sprint Planning scherm — layout
- `/products/[id]/sprint/planning` pagina; `SplitPane` met Sprint Backlog stories links (op volgorde) en taken van geselecteerde story rechts; Sprint Goal zichtbaar; lege staat rechts als geen story geselecteerd
- Done when: pagina rendert; story selecteren links toont taken rechts
- [x] **ST-308** Taak aanmaken
- "Taak aanmaken" knop in rechterpaneel navigatiebar; inline formulier (titel, omschrijving, prioriteit); `createTask` Server Action; voortgangsindicator per story (bijv. "0/0 Done")
- Done when: taak aangemaakt en zichtbaar in takenlijst; voortgangsindicator toont "0/1 Done"
- [x] **ST-309** Taak drag-and-drop (verticaal)
- dnd-kit verticale `SortableContext` in rechterpaneel; herrangschikking via float-gemiddelde in `usePlannerStore.taskOrder`; `reorderTasksAction` Server Action
- Done when: taken versleepbaar; volgorde persistent na loslaten
- [x] **ST-310** Taakstatus bijhouden
- Status-toggle per taak (TO_DO → IN_PROGRESS → DONE) via klikbare badge of dropdown; `updateTaskStatus` Server Action; voortgangsindicator op story updatet optimistisch
- Done when: taak op DONE zetten verhoogt teller in voortgangsindicator; persistent na herlaad
- [x] **ST-311** Taak bewerken en verwijderen
- Inline bewerken van titel, omschrijving en prioriteit; `updateTask` Server Action; verwijderen met bevestiging; `deleteTask` Server Action
- Done when: titelwijziging persisteert; verwijderde taak verdwijnt uit lijst
- [x] **ST-312** Sprint afronden
- "Sprint afronden" knop op Sprint-pagina; dialoog toont per story de status en vraagt: "Markeer als Done of terug naar Backlog?"; `completeSprint` Server Action zet Sprint op COMPLETED, verwerkt keuzes per story
- Done when: Sprint afgerond; stories correct verplaatst naar DONE of OPEN; nieuwe Sprint aanmaakbaar
- [x] **ST-313** Sprint Board — drie-panelen layout (vervangt ST-303 + ST-307)
- **Doel:** `/products/[id]/sprint` wordt één scherm met drie panelen van links naar rechts: Product Backlog · Sprint Backlog · Taken. De losse `/sprint/planning` route wordt verwijderd (redirect → `/sprint`).
- **Panelen:**
- *Links — Product Backlog:* PBIs met stories gegroepeerd en inklapbaar; stories die al in sprint zijn grijs/disabled; klikken of slepen voegt story toe aan Sprint Backlog (midden)
- *Midden — Sprint Backlog:* stories in sprint op volgorde; klikken selecteert story → taken laden rechts; versleepbaar om te sorteren; trash-knop verwijdert uit sprint
- *Rechts — Taken:* `TaskList` voor de geselecteerde story; lege staat "Selecteer een story" als niets geselecteerd; "+ Taak" knop zoals huidig
- **Layout:** `TriplePane` component — drie verticale panelen met twee versleepbare scheidingslijnen; opslaan in `localStorage` per product (key: `sprint-triple-${productId}`)
- **DnD:** één `DndContext` omhult alle drie panelen; drag van links naar midden werkt via `DragOverlay`; reorder binnen midden via `SortableContext`; taken-reorder in eigen geneste `DndContext`
- **State:** `SprintBoardClient` beheert sprint stories, product backlog data, `selectedStoryId`, en taken per story (vanuit server props); `useSelectionStore.selectedStoryId` voor story-selectie
- **Navigatie:** "Sprint Planning →" link onderaan Sprint Backlog pagina verwijderd; `SprintHeader` blijft bovenaan met "Sprint afronden"
- **Route cleanup:** `/sprint/planning/page.tsx` vervangt door redirect naar `/products/[id]/sprint`; `PlanningLeft`, `PlanningRightClient` components verwijderen
- Done when: één `/sprint` pagina toont alle drie panelen; story slepen van links naar midden werkt; story selecteren toont taken rechts; taak aanmaken en sorteren werkt; pagina hervat na herlaad met juiste data; `/sprint/planning` redirect werkt
---
### M3.5: Solo Paneel & Story Assignment
> **Doel:** een persoonlijk Kanban-bord per product dat de taken toont van stories die geclaimd zijn door de ingelogde developer. Story-level assignment volgt het Scrum self-organizing principe: developers claimen vrijwillig stories (pull, niet push). Volledige technische specificatie in `specs/functional.md#solo-panel`.
- [x] **ST-350** Story.assignee_id schema-migratie + auth-helpers
- **Schema:** voeg `assignee_id String?` + `assignee User? @relation("StoryAssignee", fields: [assignee_id], references: [id], onDelete: SetNull)` toe aan `Story`; voeg `assigned_stories Story[] @relation("StoryAssignee")` toe aan `User`; voeg index `@@index([sprint_id, assignee_id])` toe; migratie via `prisma migrate dev --name add_story_assignee`
- **Auth-helpers:** schrijf `lib/auth.ts` met `getSession`, `requireUser`, `requireWriter`, `requireProductAccess`, `requireProductWriter` — laatste twee doen membership-check via owner (`Product.user_id`) OF lid (`ProductMember`); demo-check op basis van `session.isDemo` (uit ST-006); throwt *"Niet beschikbaar in demo-modus"* bij demo-write-poging
- Done when: migratie slaagt; `requireProductWriter` blokkeert demo-user; `requireProductAccess` accepteert zowel owner als member
- [x] **ST-351** `<UserAvatar>` herbruikbare component
- Wrapper rond shadcn `Avatar`; props: `userId`, `username`, `size` ('xs' | 'sm' | 'md' | 'lg'), `className`; `<AvatarImage src="/api/users/{userId}/avatar">` met fallback naar initialen (eerste 2 tekens username) op `bg-primary-container`; vier groottes via Tailwind classes
- Done when: avatar rendert in 4 sizes; bij ontbrekende avatar-data (404) fallback naar initialen zichtbaar; component bruikbaar in story-kaart, sprint board, instellingen
- [x] **ST-352** Story-claim Server Actions
- Vier acties in `actions/stories.ts`: `claimStoryAction` (zet `assignee_id = currentUserId`), `unclaimStoryAction` (null), `reassignStoryAction` (valideert dat target user lid van product is), `claimAllUnassignedInActiveSprintAction` (bulk via `updateMany` voor ongeclaimde stories in actieve sprint); allemaal Zod-gevalideerd, achter `requireProductWriter`, met `revalidatePath` voor `/sprint` én `/solo`; tenant-guard via `where: { id, product_id }`
- Done when: alle vier acties testbaar via testbestand; demo-user krijgt foutmelding; reassignment naar niet-lid faalt met foutmelding; bulk claimt alleen ongeclaimde
- [x] **ST-353** Sprint Board: assignee-chip + dropdown menu op story-kaart
- Op story-kaart in middenpaneel van ST-313 Sprint Board: assignee-chip onderaan met `<UserAvatar size="xs">` + username (of muted "Niet geclaimd" badge als `assignee_id === null`); shadcn `DropdownMenu` (3-dots rechtsboven) met items "Pak op" / "Geef terug aan team" / "Wijs toe aan ▶" (submenu met members); items conditioneel zichtbaar op basis van huidige assignee; demo-modus: dropdown disabled met tooltip "Niet beschikbaar in demo-modus"
- Done when: chip toont juiste state; dropdown roept juiste acties aan; revalidatie ververst kaart; toast "Story geclaimd" / "Toegewezen aan X" bij succes; demo-user ziet disabled-tooltip
- [x] **ST-354** Sprint Board: bulk-claim knop "Claim alle ongeclaimde"
- Knop bovenaan Sprint Backlog paneel met telling: "Claim alle ongeclaimde stories (N)"; disabled als N=0 of `isDemo`; klik roept `claimAllUnassignedInActiveSprintAction` aan; Sonner success-toast "{count} stories geclaimd"; pending state via `useTransition`
- Done when: telling correct; claimen werkt; knop disabled bij 0 ongeclaimd of demo; toast verschijnt na succes
- [x] **ST-355** Solo route — `/solo` redirect + `/products/[id]/solo` pagina + cookie
- **Cookie-helper:** schrijf `lib/cookies.ts` met `setLastProductCookie(productId)` (HTTP-only, sameSite lax, 30 dagen)
- **`/solo` page.tsx:** Server Component; leest cookie `lastProductId`; valideert toegang en redirect naar `/products/[id]/solo`, of toont `<ProductPicker>` als geen cookie of cookie ongeldig
- **`/products/[id]/solo` page.tsx:** Server Component; haalt active sprint op (404 → empty state `<NoActiveSprint>`); haalt taken op via `Task.findMany` met `where: { sprint_id, story: { assignee_id: session.userId } }` + count ongeclaimde stories parallel; geeft data door aan `<SoloBoard>`; zet `lastProductId` cookie bij elk bezoek
- **Empty state:** `<NoActiveSprint>` met titel, uitleg, link naar productpagina
- **`<ProductPicker>`:** lijst van toegankelijke producten, klikken redirect naar `/products/[id]/solo`
- Done when: `/solo` zonder cookie toont picker; met geldige cookie redirect; pagina toont juiste taken; geen actieve sprint toont empty state; cookie persisteert tussen sessies
- [x] **ST-356** Solo Kanban-bord met DnD en Zustand
- **Store `stores/solo-store.ts`:** `tasks`, `initTasks`, `optimisticMove(taskId, toStatus)` (returnt vorige status), `rollback(taskId, prevStatus)`, `updatePlan(taskId, plan)`; volgt patroon van `usePlannerStore` (ST-201)
- **`<SoloBoard>` Client Component:** root met `DndContext` (overslaan als `isDemo`), `PointerSensor` met `activationConstraint: { distance: 5 }`, `closestCorners` collision detection; header met productnaam, sprint goal, knop "Toon openstaande stories (N)"; grid met drie kolommen
- **`<SoloColumn>`:** drop target per status (`TO_DO` / `IN_PROGRESS` / `DONE`); header met statuskleur via MD3 tokens (`bg-status-todo/15` etc.); count en lege staat
- **`<SoloTaskCard>`:** hergebruik bestaande task-card (ST-310); draggable; toont prioriteit-indicator, taaktitel, story-titel; klik opent detail-dialoog (ST-357); demo: niet draggable
- **`onDragEnd` flow:** optimistische update via `optimisticMove`, dan `updateTaskStatusAction` aanroepen, op error rollback + Sonner error-toast "Status bijwerken mislukt — taak teruggeplaatst"; geen success-toast (te frequent)
- Done when: kaart sleepbaar tussen kolommen; status persisteert; gesimuleerde server-fout rollbackt UI; demo-user kan niet slepen
- [x] **ST-357** Task detail-dialoog + `updateTaskPlanAction`
- **`updateTaskPlanAction`** in `actions/tasks.ts`: Zod-schema `{ taskId, productId, implementationPlan }`; `requireProductWriter`; tenant-guard via `where: { id: taskId, story: { product_id: productId } }`; `revalidatePath`
- **`<TaskDetailDialog>`** shadcn `Dialog`: header met taaktitel + statusbadge (MD3 tokens); sectie *Beschrijving* (read-only, volg bestaand patroon); sectie *Implementatieplan* met `<Textarea>` save-on-blur; on-blur roept `updateTaskPlanAction`, indicator rechtsonder ("Bezig met opslaan…" → "Opgeslagen", vervaagt na 2s); error-toast bij fout; footer-link "Open in Sprint Board ↗"; demo-modus: textarea `readOnly` met tooltip
- Done when: edit + blur + refresh persisteert; gesimuleerde server-fout toont error-toast; demo-user kan dialoog openen maar niet bewerken
- [x] **ST-358** Openstaande stories sheet
- Knop "Toon openstaande stories (N)" bovenaan Solo bord opent shadcn `<Sheet>` (slide-out van rechts); inhoud: lijst van ongeclaimde stories in actieve sprint met titel, taakaantal, "Pak op"-knop per item; klik roept `claimStoryAction`, sheet blijft open (zodat meerdere achter elkaar claimen kan); Sonner success-toast per claim; lege staat "Geen ongeclaimde stories. Lekker bezig!"; pending state via `useFormStatus`; demo: knoppen disabled met tooltip
- Done when: sheet opent met N items; claimen verwijdert item uit lijst en verlaagt teller; lege staat correct; demo-user ziet sheet maar kan niet claimen
- [x] **ST-359** Navbar-link "Solo"
- Voeg `<NavLink href="/solo" icon={<UserSquare />}>Solo</NavLink>` toe aan navigatieshell (ST-008); altijd zichtbaar voor ingelogde users (geen product-context); plek tussen "Producten" en "Todos"
- Done when: link altijd zichtbaar in nav; klik gaat naar `/solo` en redirect verder
- [x] **ST-360** Demo-seed uitbreiden met geclaimde stories
- Update `prisma/seed.ts` (ST-004): demo-user (`is_demo = true`) heeft minimaal één product met ACTIVE sprint; minimaal 3 stories met `assignee_id = demoUser.id` (variërend over taakstatussen TO_DO, IN_PROGRESS, DONE); minimaal 1 ongeclaimde story (om "Toon openstaande" te demonstreren — demo-user kan niet claimen, ziet wel hoe het werkt)
- Done when: login als demo → Solo bord toont werkend Kanban met taken in alle drie kolommen; "Toon openstaande" sheet toont ten minste 1 story (claim-knoppen disabled)
---
### M4: Claude Code REST API
- [x] **ST-401** API-token infrastructuur
- Schrijf `lib/api-auth.ts`: parseer `Authorization: Bearer` header; bereken SHA-256 hash; zoek op in `api_tokens`; controleer `revoked_at`; retourneer `userId` of 401; retourneer 403 als `is_demo`
- Done when: geldige token geeft userId terug; ongeldige token geeft 401; ingetrokken token geeft 401; demo-token op schrijf-endpoint geeft 403
- [x] **ST-402** API-tokenbeheer UI
- `/settings/tokens` pagina; token aanmaken (label optioneel); token eenmalig getoond in kopieerbaar veld na aanmaken; tokenoverzicht (label, datum, actief/ingetrokken); intrekken via Server Action; max. 10 actieve tokens
- Done when: token aangemaakt en waarde zichtbaar; na sluiten dialoog niet meer te zien; intrekken maakt token onbruikbaar (getest via curl)
- [x] **ST-403** `GET /api/products` — productenlijst
- Route Handler; authenticatie via `api-auth.ts`; retourneer actieve producten `[{ id, name, repo_url }]` als JSON voor producten waar de tokengebruiker eigenaar of teamlid is
- Done when: `curl -H "Authorization: Bearer <token>" /api/products` retourneert correct JSON inclusief gedeelde product backlogs; 401 zonder token
- [x] **ST-404** `GET /api/products/:id/next-story` — volgende story ophalen
- Route Handler; haal hoogst geprioriteerde OPEN story op van actieve Sprint van het product (priority ASC, sort_order ASC); retourneer `{ id, title, description, acceptance_criteria, tasks[] }`; 404 als geen open stories
- Done when: endpoint retourneert eerste story van Sprint; 404 als Sprint leeg; 404 als geen actieve Sprint
- [x] **ST-405** `GET /api/sprints/:id/tasks` — taken ophalen
- Route Handler met `?limit=N` query param (default 10, max 50); retourneer taken van actieve Sprint op `(story.sort_order, task.priority, task.sort_order)` volgorde; retourneer `{ id, title, story_id, priority, sort_order, status }`
- Done when: endpoint retourneert max N taken in juiste volgorde; `?limit=5` retourneert max 5
- [x] **ST-406** `PATCH /api/stories/:id/tasks/reorder` — taakvolgorde aanpassen
- Route Handler; body: `{ task_ids: string[] }`; valideer alle IDs behoren tot de story; update `sort_order` via float-verdeling; retourneer `{ success: true }`
- Done when: volgorde in DB veranderd na PATCH; gewijzigde volgorde zichtbaar in Sprint Planning UI na herlaad; ongeldige task_id geeft 400
- [x] **ST-407** `POST /api/stories/:id/log` — activiteit vastleggen
- Route Handler; body: `{ type, content, status?, commit_hash?, commit_message? }`; Zod-validatie per type; schrijf naar `story_logs`; retourneer `{ id, created_at }`
- Done when: drie typen werken (IMPLEMENTATION_PLAN, TEST_RESULT, COMMIT); log-entry zichtbaar in story-detail UI na aanmaken via API; ontbrekend verplicht veld geeft 400
- [x] **ST-408** `PATCH /api/tasks/:id` — taakstatus en implementatieplan bijwerken
- Route Handler; body: `{ status?: "TO_DO" | "IN_PROGRESS" | "DONE", implementation_plan?: string }`; minimaal één veld verplicht; valideer dat taak aan requester's product behoort; retourneer `{ id, status, implementation_plan }`
- Done when: status update via API zichtbaar in Sprint Planning UI; implementation_plan opgeslagen en opvraagbaar; lege body geeft 400; andere gebruikers taak geeft 403
- [x] **ST-409** `POST /api/todos` — todo aanmaken
- Route Handler; body: `{ title: string, product_id: string }`; valideer dat product bij de geverifieerde gebruiker hoort; schrijf naar `todos`; retourneer `{ id, title, created_at }`
- Done when: todo aangemaakt via API met product_id verschijnt in todo-lijst UI gekoppeld aan het juiste product; lege titel of ontbrekend product_id geeft 400; onbekend product geeft 404
- [x] **ST-410** Story-activiteitenlog UI
- Activiteitenlog sectie in story-detail slide-over; haal `story_logs` op via Server Component; render chronologisch; visuele stijl per type (IMPLEMENTATION_PLAN = blauw, TEST_RESULT passed = groen, failed = rood, COMMIT = paars); commit-hash klikbaar als `repo_url` ingesteld; lege staat
- Done when: drie log-entries (plan, test, commit) correct gestyled; commit-hash link opent in nieuw tabblad
---
### M5: Todo-lijst
> **Herontwerp (april 2026):** ST-501505 beschreven de oorspronkelijke QuickInput-aanpak. Die is geïmplementeerd maar vervangen door een Data Table + detail-kaart ontwerp (ST-509510). ST-501505 zijn als referentie bewaard; de functionele eisen zijn ongewijzigd.
- [x] **ST-501** Todo-lijst pagina *(vervangen door ST-509)*
- `/todos` pagina; haal actieve (niet-gearchiveerde) todos op inclusief productnaam; snel-invoerveld bovenaan met product-dropdown (verplicht) en titel (Enter om op te slaan); `createTodo` Server Action; lege staat met instructie; productnaam-badge per todo-item
- Done when: todo aanmaken via Enter persisteert en verschijnt in lijst met productnaam; aanmaken zonder product geblokkeerd; lege staat zichtbaar bij geen todos
- [x] **ST-502** Todo afvinken *(vervangen door ST-509)*
- Checkbox per todo; `toggleTodo` Server Action; afgevinkte todos visueel doorgestreept; afgevinkte todos blijven zichtbaar onderaan de lijst
- Done when: afvinken persistent na herlaad; visuele doorstreping correct
- [x] **ST-503** Afgevinkte todos archiveren *(vervangen door ST-510)*
- "Archiveer afgeronde items" knop; `archiveCompletedTodos` Server Action; gearchiveerde todos verdwijnen uit standaardweergave
- Done when: archiveren verbergt alle afgevinkte todos; telling correct
- [x] **ST-504** Todo promoveren naar PBI *(vervangen door ST-510)*
- "Promoveer naar PBI" contextmenu of knop per todo; dialoog: product dropdown (actieve producten), prioriteit dropdown; titel vooringevuld (bewerkbaar); bevestigingswaarschuwing; `promoteTodeToPbi` Server Action (maak PBI aan, verwijder todo)
- Done when: gepromoveerde todo verdwijnt; PBI zichtbaar in juist product met juiste prioriteit
- [x] **ST-505** Todo promoveren naar story *(vervangen door ST-510)*
- "Promoveer naar story" knop per todo; dialoog: product dropdown → PBI dropdown (gefilterd op product), prioriteit; titel vooringevuld; `promoteTodoToStory` Server Action (maak story aan, verwijder todo)
- Done when: gepromoveerde todo verdwijnt; story zichtbaar in juist PBI met juiste prioriteit
- [x] **ST-509** Todo Data Table
- Installeer `@tanstack/react-table`; voeg shadcn `data-table`-patroon toe
- **Kolommen:**
- Selectie-checkbox (kolom 1): multi-select voor bulk-archivering; header-checkbox selecteert/deselecteert alle zichtbare rijen
- Titel (kolom 2): max 2 regels, `line-clamp-2 truncate`; afgevinkte todos doorgestreept; klik op rij (buiten checkbox) opent detail-kaart
- Productnaam-badge (kolom 3)
- Aanmaakdatum (kolom 4)
- **Toolbar boven de tabel:**
- Product-filter dropdown (Alles / Geen product / per product)
- "+" knop: opent lege detail-kaart voor nieuw aanmaken (erft geselecteerd filter-product)
- "Archiveer geselecteerde (N)" knop: actief zodra ≥ 1 checkbox aangevinkt; roept `archiveSelectedTodosAction` aan met de geselecteerde IDs; resettet selectie na afloop
- **Paginering:** max 10 rijen per pagina; vorige/volgende knoppen; paginatelling ("110 van 23")
- **Lege staat:** "Geen todo's voor deze selectie." bij lege filter; "Nog geen todo's. Gebruik + om er een aan te maken." bij volledig lege lijst
- **`archiveSelectedTodosAction`** toevoegen aan `actions/todos.ts`: valideert dat alle meegegeven IDs bij de ingelogde gebruiker horen vóór schrijven; `archiveMany` via `updateMany`
- Done when: tabel toont alle actieve todos; paginering werkt; product-filter werkt; selectie-checkbox selecteert meerdere rijen; bulk-archiveren verwijdert geselecteerde rijen uit de weergave
- [x] **ST-510** Todo detail-kaart
- Kaart onder de tabel; altijd zichtbaar (leeg als geen todo geselecteerd of aangemaakt wordt)
- **Aanmaken:** "+" in toolbar zet kaart in aanmaak-modus; velden: product-dropdown (erft filter-product, of "Geen product" bij "Alles"), titel; opslaan via `createTodoAction`; na opslaan kaart leegmaken en tabel ververst
- **Bewerken:** klik op tabelrij (buiten checkbox) laadt todo in kaart; velden: product-dropdown, titel, done-toggle; opslaan via nieuwe `updateTodoAction` (title + product_id + done); annuleren deselecteert rij en leegt kaart
- **Promoveren:** knoppen "→ PBI" en "→ Story" in de kaart; openen de bestaande `PromotePbiDialog` en `PromoteStoryDialog`; alleen zichtbaar bij een bestaande geselecteerde todo
- **Demo-modus:** kaart-invoervelden uitgeschakeld; knoppen verborgen of disabled
- **`updateTodoAction`** toevoegen aan `actions/todos.ts`: valideert eigenaarschap; past `title`, `product_id` en/of `done` aan; `revalidatePath('/todos')`
- Done when: aanmaken via kaart persisteert; bewerken van titel, product en done-status werkt; promote vanuit kaart opent juist dialoog en verwijdert todo na bevestiging; kaart leeg bij geen selectie; demo-gebruiker ziet uitgeschakelde kaart
- [x] **ST-506** Rolbeheer in instellingen
- `/settings` pagina met roltoewijzing (checkbox per rol: Product Owner, Scrum Master, Developer); minimaal één rol verplicht; `updateRoles` Server Action; geselecteerde rollen zichtbaar in profielbalk
- Done when: rollen bijwerken persisterend; profielbalk toont gekozen rollen; uitvinken van alle rollen geeft validatiefout
- [x] **ST-507** Gebruikersprofiel (buiten originele backlog toegevoegd)
- Profielfoto-upload (JPEG/PNG/WebP, max 12 MB), server-side resizing naar max 700×700 WebP met Sharp, opgeslagen als bytea in Neon; bio (max 160) en bio_detail (max 2000) als aparte velden; `POST /api/profile/avatar` + `GET /api/profile/avatar` + `updateProfileAction`
- Done when: foto geüpload en zichtbaar in instellingen; bio opgeslagen; ongeldige bestanden geweigerd vóór verwerking
- [x] **ST-508** Product Backlog-overzicht in instellingen (buiten originele backlog toegevoegd)
- Gecombineerde lijst op `/settings` van eigen producten (badge "Eigenaar") en team-lidmaatschappen (badge "Developer" + eigenaarsnaam); klikbaar naar product; "Verlaten"-knop met bevestiging voor lidmaatschappen; lege staat met CTA
- Done when: eigenaar-producten en team-producten zichtbaar in één lijst; verlaten werkt en verwijdert rij
- [x] **ST-511** Entity codes voor Product, PBI en Story (buiten originele backlog toegevoegd)
- **Schema:** `code String? @db.VarChar(30)` op `Product`, `Pbi` en `Story`; unique per parent (`user_id` voor Product, `product_id` voor Pbi/Story); `Task` heeft geen DB-veld — code wordt afgeleid als `${story.code}.${index_in_story}`
- **Auto-generatie:** `lib/code-server.ts` met `generateNextStoryCode` (`ST-001`, `ST-002`, … 3-cijferig per product) en `generateNextPbiCode` (`PBI-1`, `PBI-2`, … per product); `createWithCodeRetry`-helper vangt P2002 op het code-veld op en probeert max 3× opnieuw zodat gelijktijdige inserts niet crashen
- **Validatie:** Zod max 30 tekens, regex `^[A-Za-z0-9._-]+$`; handmatige override mag elk format dat aan de basis-regex voldoet (geen format-enforcement op `ST-NNN`)
- **Forms:** code-input op Product/Pbi/Story dialogen; auto-default zichtbaar als placeholder `auto`; field-level error-rendering onder code-input voor zowel create- als edit-mode (uniciteits-conflict, ongeldig format)
- **Display:** `CodeBadge` (`components/shared/code-badge.tsx`) consistent op dashboard product-list, PBI-list, story-blocks (Product Backlog), sprint board (alle drie panelen incl. PBI-headers), solo-bord task-cards, task-detail-dialoog, sprint-afronden-dialoog en de story-dialoog title; task-card toont derived `${story.code}.${index}`-badge rechtsboven uitgelijnd
- **Seed:** parser strip `ST-XXX:`-prefix uit titles, vult `code` apart; product `Scrum4Me` krijgt `code: 'SCRUM4ME'`, milestones krijgen `M0`/`M3.5`/etc., stories krijgen `ST-001…ST-612`
- Done when: auto-toegekende codes per product oplopend en uniek; race-conflict wordt opgevangen door retry-helper i.p.v. te crashen; handmatige duplicate code toont inline error onder de input in zowel create- als edit-mode; codes zichtbaar als badge in alle lijsten/cards/dialogen; seed verdeelt codes correct (8 PBI's met `M*`, 84 stories met `ST-NNN`)
- [x] **ST-512** REST API uitbreidingen voor codes, todo-description en task implementation_plan (buiten originele backlog toegevoegd)
- **`GET /api/products`:** voeg `code` toe (naast `id`, `name`, `repo_url`); optioneel `description` en `definition_of_done`
- **`GET /api/products/:id/next-story`:** voeg `code` toe op story; voeg per task `code` (derived `${story.code}.${index_in_story}`) en `implementation_plan` toe
- **`GET /api/sprints/:id/tasks`:** voeg `description`, `implementation_plan` en `story_code` toe per task; voeg een derived `code`-veld per task toe (`${story.code}.${index_in_story}`)
- **`POST /api/todos`:** accepteer optionele `description` (max 2000 tekens); valideer en sla op; retourneer `description` in response
- Done when: alle vier endpoints retourneren / accepteren de nieuwe velden zoals beschreven; curl-test toont `code` op products, story en tasks; todo aanmaken via API met `description` slaat op
- [x] **ST-513** REST API hardening voor Claude Code (buiten originele backlog toegevoegd)
- **Health:** nieuwe `GET /api/health` zonder auth; retourneert `{ status, version, time }`; optioneel `?db=1` voor DB-ping (`{ database: 'ok' | 'down' }`)
- **Claude-context:** nieuwe `GET /api/products/:id/claude-context` (auth) die in één call `product`, `active_sprint`, `next_story` (met tasks), en `open_todos` van de gebruiker terugbrengt — voorkomt round-trips
- **Status-case op API-boundary:** nieuwe `lib/task-status.ts` mapper; API exposeert lowercase (`todo`/`in_progress`/`review`/`done` voor tasks; `open`/`in_sprint`/`done` voor stories); DB blijft UPPER_SNAKE; UI ongewijzigd
- **`PATCH /api/tasks/:id`:** accepteert lowercase `status` via mapper; retourneert lowercase
- **Story-log metadata:** nieuwe optionele `metadata Json?` kolom op `StoryLog`; `POST /api/stories/:id/log` accepteert per type een optioneel `metadata`-veld (bv. `{ branch: 'feat/x' }`); bestaande velden ongewijzigd → backwards-compatible
- **Foutcodes:** Zod-validatie geeft `422` (was `400`); `400` blijft voor malformed body; `401`/`403`/`404`/`500` ongewijzigd
<<<<<<<< HEAD:docs/backlog/index.md
- **API-documentatie:** nieuwe `docs/api/rest-contract.md` met endpoints, request/response, foutcodes, status-enums en curl-voorbeelden; `CLAUDE.md` verwijst ernaar
- Done when: `curl /api/health` werkt zonder auth; `curl /api/products/:id/claude-context` retourneert bundled JSON; PATCH/PUT routes accepteren lowercase status en geven 422 bij ongeldige body; story-log POST bewaart `metadata`; `docs/api/rest-contract.md` is gepubliceerd
========
- **API-documentatie:** nieuwe `docs/api.md` met endpoints, request/response, foutcodes, status-enums en curl-voorbeelden; `CLAUDE.md` verwijst ernaar
- Done when: `curl /api/health` werkt zonder auth; `curl /api/products/:id/claude-context` retourneert bundled JSON; PATCH/PUT routes accepteren lowercase status en geven 422 bij ongeldige body; story-log POST bewaart `metadata`; `docs/api.md` is gepubliceerd
>>>>>>>> origin/main:docs/backlog.md
- **`GET /api/products`:** voeg `code` toe (naast `id`, `name`, `repo_url`); optioneel `description` en `definition_of_done`
- **`GET /api/products/:id/next-story`:** voeg `code` toe op story; voeg per task `code` (derived `${story.code}.${index_in_story}`) en `implementation_plan` toe
- **`GET /api/sprints/:id/tasks`:** voeg `description`, `implementation_plan` en `story_code` toe per task; voeg een derived `code`-veld per task toe (`${story.code}.${index_in_story}`)
- **`POST /api/todos`:** accepteer optionele `description` (max 2000 tekens); valideer en sla op; retourneer `description` in response
- **Backwards-compat:** alle wijzigingen zijn additief — bestaande clients negeren onbekende keys; nieuwe input-velden zijn optioneel
- Done when: alle vier endpoints retourneren / accepteren de nieuwe velden zoals beschreven; curl-test toont `code` op products, story en tasks; todo aanmaken via API met `description` slaat op
---
### M6: Polish & Launch-ready
- [x] **ST-601** Loading states en skeletons
- `loading.tsx` voor alle zware routes (`/products/[id]`, `/sprint`, `/sprint/planning`); skeletoncomponenten voor PBI-lijst, story-blokken en takenlijst; pending states op alle form-submit-knoppen via `useFormStatus`
- Done when: navigeren naar een trage route toont skeleton; submit-knoppen disablen tijdens Server Action
- [x] **ST-602** Error boundaries
- `error.tsx` voor alle beschermde routes; toon gebruiksvriendelijke foutmelding met "Probeer opnieuw" knop; log fout naar console (Sentry in M6)
- Done when: gesimuleerde Server Action-fout toont error boundary zonder witte pagina
- [x] **ST-603** Toast-notificaties (Sonner)
- Installeer Sonner; success-toast na aanmaken/bewerken/verwijderen van producten, PBI's, stories, taken, todos; error-toast bij mislukte Server Actions; toast niet bij drag-and-drop (te frequent)
- Done when: aanmaken van PBI toont success-toast; gesimuleerde netwerk-fout toont error-toast
- [x] **ST-604** Demo-gebruiker write-protection in UI
- Alle aanmaak-, bewerk- en verwijderknoppen disabled + tooltip "Niet beschikbaar in demo-modus" voor demo-sessies; gebaseerd op `isDemo` in sessie
- Done when: demo-gebruiker ziet alle knoppen maar kan niets wijzigen; tooltip zichtbaar bij hover
- [x] **ST-605** Keyboard-navigatie
- Tab-volgorde logisch in alle formulieren; Enter submits formulieren; Escape sluit modals/slide-overs; dnd-kit keyboard-drag (Space om te pakken, pijltjestoetsen, Space om te laten vallen)
- Done when: volledige PBI aanmaken-flow keyboard-only uitvoerbaar; dnd-kit drag via keyboard werkt
- [x] **ST-606** Desktop-first UI-review
- Test alle flows op 1280px, 1440px en 1920px breedte; fix overflow, uitlijning en proportie-issues; controleer minimum schermbreedte 1024px (toon melding bij smaller)
- Done when: alle M0M5 flows correct op drie schermbreedtes; melding bij < 1024px
- [x] **ST-607** Toegankelijkheid (WCAG AA)
- Kleurcontrast-check op alle tekst en badges; aria-labels op icon-only knoppen; focus-ring zichtbaar op alle interactieve elementen; `role` en `aria-selected` op geselecteerde PBI in linkerpaneel
- Done when: geen WCAG AA contrastfouten op primaire flows; alle knoppen hebben toegankelijke labels
- [x] **ST-608** Ratelimiting op auth-endpoints
- Max. 10 inlogpogingen per IP per minuut; max. 5 registraties per IP per uur; implementeer via in-memory counter (v1) of Vercel Edge middleware
- Done when: 11 snelle inlogpogingen leiden tot 429-respons met duidelijke melding
- [x] **ST-609** Beveiligingsreview API-endpoints
- Controleer alle Route Handlers: elke schrijfoperatie valideert dat de resource binnen de toegankelijke product-scope valt; cross-scope reads zijn onmogelijk tenzij de gebruiker via `product_members` gekoppeld is; voeg integratietests toe die cross-user toegang testen
- Done when: poging om een niet-gedeeld product van een andere gebruiker op te halen via API geeft 403 of 404; gedeelde producten zijn wel zichtbaar; getest met twee test-gebruikers
- [x] **ST-610** CI/CD via GitHub Actions
- Workflow: `lint` (ESLint), `typecheck` (tsc --noEmit), `prisma validate`, `build` (next build) op elke PR en push naar main; Vercel auto-deploy op main
- Done when: een TypeScript-fout in een PR blokkeert merge; succesvolle merge triggert Vercel-deploy
- [x] **ST-611** README en lokale setup-documentatie
- Schrijf `README.md` met: wat is Scrum4Me, quickstart lokaal (clone → env → prisma push → seed → dev), cloud deployment (Vercel + Neon stappenplan), API-documentatie (alle 7 endpoints met voorbeelden), Claude Code-integratie uitleg, Vercel Analytics status en directe dependencies zoals Sharp
- De in-app landingspagina (`/`) bevat al een gebruikershandleiding, Scrum-samenvatting en API-overzicht — de README richt zich op lokale setup en deployment
- Done when: iemand zonder context de app lokaal kan draaien op basis van alleen de README en `.env.example`
- [x] **ST-612** End-to-end acceptatietest
- Voer handmatig de volledige Lars-flow uit: product aanmaken → PBI's en stories aanmaken → Sprint starten → stories slepen → taken aanmaken → API-token aanmaken → curl `next-story` → curl `log` (plan, test, commit) → activiteitenlog controleren in UI
- Done when: volledige flow werkt zonder fouten of onverwacht gedrag; alle API-responses correct JSON
### M7: MCP-server voor Claude Code
Aparte repo: [`madhura68/scrum4me-mcp`](https://github.com/madhura68/scrum4me-mcp). Native Prisma-toegang (geen REST-tussenlaag), stdio-transport, Scrum4Me-schema gevendord als git submodule. Tokens hergebruikt uit `api_tokens`. v1 is alleen dev-flow tools — geen PBI/sprint-creatie of profielbeheer.
- [x] **ST-701** Repo-skeleton mcp
- npm init, tsconfig strict, .gitignore, MCP SDK 1.29, Prisma 7, zod, tsx; lege `src/index.ts` die op stdio start
- Done when: `npx tsx src/index.ts` print `running on stdio` zonder crash; `tsc --noEmit` slaagt
- [x] **ST-702** Schema-sync via git submodule
- Submodule `vendor/scrum4me`, `scripts/sync-schema.sh` kopieert `schema.prisma` en strip de `generator erd`-block, `npm run prisma:generate` als postinstall
- Done when: `npm run sync-schema && npm run prisma:generate` werkt op een verse clone
- [x] **ST-703** Auth en Prisma-singleton
- `src/auth.ts` SHA-256 hash van `SCRUM4ME_TOKEN` → lookup in `api_tokens`, cached `{ userId, isDemo }`; `requireWriteAccess()` throwt `PermissionDeniedError` voor demo
- `src/prisma.ts` lazy proxy zodat bootstrap niet crasht zonder `DATABASE_URL`
- Done when: ongeldig token geeft `SCRUM4ME_TOKEN is invalid or revoked`; demo-tokens blokkeren writes
- [x] **ST-704** Status-mappers + error-helpers
- `src/status.ts` zelfde mappers als REST `lib/task-status.ts`
- `src/errors.ts` `formatZodError`, `toolError`, `toolJson`, `withToolErrors` wrapper
- Done when: zod-fouten en `PermissionDenied` worden als gestructureerde MCP-errors teruggegeven
- [x] **ST-705** Read-tools — `health`, `list_products`, `get_claude_context`
- `health` doet `SELECT 1`; `list_products` met product-access filter; `get_claude_context` bundelt product + active sprint + next story (met tasks) + 50 open todos
- Done when: smoke-test tegen live DB groen voor alle drie
- [x] **ST-706** Write-tools tasks — `update_task_status`, `update_task_plan`
- Status-input lowercase (`todo|in_progress|review|done`), conversie via mapper; access-check via story → product → membership/owner
- Done when: niet-eigenaar krijgt 'not accessible'; demo geeft `PERMISSION_DENIED`
- [x] **ST-707** Log-tools — `log_implementation`, `log_test_result`, `log_commit`
- Append `StoryLog` met juiste `type`; optioneel `metadata` JSONB
- Done when: drie logs verschijnen in story-activiteit met `type`/`status`/`commit_hash`/`metadata` zoals meegegeven
- [x] **ST-708** `create_todo`-tool
- Optionele `description` (max 2000) en `product_id` (gevalideerd via access-check)
- Done when: nieuwe todo verschijnt in `/todos` voor de tokengebruiker
- [x] **ST-709** Prompt `implement_next_story`
- Workflow: `get_claude_context` → plan → log_implementation → per task `in_progress`/`done` → tests → `log_test_result``log_commit`
- Done when: prompt zichtbaar in MCP-clients met argument `product_id`
- [x] **ST-710** README + Claude Code-config + smoke-test
- README beschrijft setup, tools-tabel, schema-sync, `~/.claude/mcp_servers.json` snippet, risico's
- `scripts/smoke-test.ts` valideert read-tools tegen live DB
- Done when: smoke-test groen; MCP Inspector toont 9 tools + 1 prompt
### M8: Realtime Solo Paneel
Live updates voor stories en tasks in het Solo Paneel zonder pagina-refresh. Wanneer Claude Code (via MCP), Codex (via REST) of een andere browser-tab een task/story muteert, ziet de gebruiker het binnen 12 seconden in zijn kanban-bord.
Transport: Server-Sent Events (Vercel ondersteunt geen stateful WebSockets). Bron: Postgres `LISTEN/NOTIFY` via row-level triggers op `tasks` en `stories`. Eén-richting (server → client) — mutaties blijven via Server Actions/REST/MCP.
Filtering server-side: alleen events binnen de actieve sprint van een product waar de gebruiker eigenaar of lid van is, plus `assignee_id == userId` (eigen kolommen) of `assignee_id IS NULL` (claim-lijst).
- [x] **ST-801** Postgres LISTEN/NOTIFY-infrastructuur
- Migratie met `notify_solo_change()`-functie + `AFTER INSERT/UPDATE/DELETE`-triggers op `tasks` en `stories`; payload bevat `op`, `entity`, `id`, `product_id`, `sprint_id`, `assignee_id`, `fields` (gewijzigde kolommen)
- Done when: `psql $DIRECT_URL -c "LISTEN scrum4me_solo;"` toont een payload bij een UI-mutatie
- [x] **ST-802** SSE-route `/api/realtime/solo`
- `app/api/realtime/solo/route.ts`, `runtime: 'nodejs'`, `maxDuration: 300`; auth via iron-session, query-param `product_id`, opent `pg.Client` op `DIRECT_URL` met `LISTEN`; heartbeat 25s; hard close 240s; in-handler filtering op product/sprint/assignee
- Done when: `curl -N` op localhost levert binnen 1s een event op na een task-mutatie via UI
- [x] **ST-803** Client hook `useSoloRealtime(productId)`
- `lib/realtime/use-solo-realtime.ts`; opent `EventSource`, exponential backoff reconnect (1s → 30s); Page Visibility API voor pauseren/hervatten; cleanup op unmount
- Done when: tab wisselen sluit/opent connectie zichtbaar in DevTools Network
- [x] **ST-804** Solo-store realtime-acties
- `applyTaskUpdate`, `applyTaskCreate`, `applyTaskDelete`, `applyStoryAssignment`, `markPending`/`clearPending` om eigen optimistic-echo te onderdrukken
- Done when: unit-test op solo-store met gesimuleerde events laat juiste eindstate zien
- [x] **ST-805** Wire-up in SoloBoard + UI-indicator
- `components/solo/solo-board.tsx` roept de hook aan; klein "live"/"verbinden..."-statusindicator; toast bij langer dan 5s disconnected
- Done when: twee tabs van Solo Paneel — mutatie in tab A komt binnen 12s in tab B zonder refresh
- [x] **ST-806** Documentatie + acceptatietest
<<<<<<<< HEAD:docs/backlog/index.md
- Sectie "Realtime updates" in `docs/architecture.md` met diagram en filtering-regels; vermelding in `CLAUDE.md`; korte note over `/api/realtime/solo` in `docs/api/rest-contract.md`; handmatig E2E-scenario's gedraaid (zelfde gebruiker twee tabs, MCP-write, REST-write, story-claim, network-flap)
========
- Sectie "Realtime updates" in `docs/architecture.md` met diagram en filtering-regels; vermelding in `CLAUDE.md`; korte note over `/api/realtime/solo` in `docs/api.md`; handmatig E2E-scenario's gedraaid (zelfde gebruiker twee tabs, MCP-write, REST-write, story-claim, network-flap)
>>>>>>>> origin/main:docs/backlog.md
- Done when: alle scenario's lopen door zonder onverwachte gedragingen
Volledig plan in `.Plans/2026-04-27-m8-realtime-solo.md` (lokaal, niet gecommit).
### M9: Actief Product Backlog
**Implementatieplan:** [docs/plans/M9-active-product-backlog.md](../plans/M9-active-product-backlog.md)
Eén "actief Product Backlog" per gebruiker — persistent in DB. De NavBar wordt gesplitst in **Producten** (lijst) en **Product Backlog** (PB-view van actief PB), met **Sprint** en **Solo** als aparte tabs die op het actieve PB werken. Geen actief PB → die drie tabs zijn disabled. Vervangt de bestaande `last_product`-cookieflow.
- [x] **ST-901** Database — `user.active_product_id`
- Voeg `active_product_id String? @db.Uuid` toe aan `User` met FK naar `Product.id` en `onDelete: SetNull`; migratie `add_user_active_product_id`; index op `active_product_id` voor join-performance
- Done when: `npx prisma migrate dev` slaagt; `prisma studio` toont kolom; `npx prisma validate` zonder fouten; submodule `vendor/scrum4me` in mcp draait `prisma generate` + `tsc --noEmit` zonder fouten
- [x] **ST-902** Server Actions — actief product zetten en wissen
- `actions/active-product.ts` met `setActiveProduct(productId)` en `clearActiveProduct()`; Zod + auth + `productAccessFilter`; demo-gebruikers mogen wisselen (sessie-effect alleen, geen DB-write); `archiveProduct` en `leaveProduct` zetten `active_product_id` op `null` als het hetzelfde product betreft
- Done when: setActive met onbekend/onbereikbaar product → 422; archiveren van actief product clearet de keuze; demo-flow geeft toast "Niet beschikbaar in demo-modus"
- [x] **ST-903** App-layout laadt actief product + redirects
- `app/(app)/layout.tsx` haalt `activeProduct` (id, name, archived) op naast user; geef door aan `NavBar`; `app/(app)/solo/page.tsx` gebruikt `user.active_product_id` i.p.v. `getLastProductCookie`; helper `lib/cookies.ts:getLastProductCookie` markeren deprecated of verwijderen plus call-sites opruimen
- Done when: ingelogd zonder actief PB toont NavBar zonder geactiveerde tabs; met actief PB redirect `/solo``/products/[active]/solo` zonder cookie te raadplegen
- [x] **ST-904** NavBar — splits + disabled-states + switcher
- Tabs worden: **Producten** (`/dashboard`) | **Product Backlog** (`/products/[active]`) | **Sprint** (`/products/[active]/sprint`) | **Solo** (`/products/[active]/solo`) | **Todo's** (`/todos`); zonder actief PB zijn de middelste drie disabled-spans (zelfde stijl als huidige Sprint-disabled); productnaam in midden wordt een dropdown-trigger (shadcn `DropdownMenu`) met je producten + "Producten beheren →"; Sprint krijgt `aria-disabled` + tooltip "Geen actieve sprint" als er geen sprint met status `ACTIVE` is
- Done when: handmatige test: zonder PB drie tabs grijs; activeer PB → tabs klikbaar; dropdown wisselt PB en redirect naar Product Backlog; Sprint-tab disabled tot sprint gestart
- [x] **ST-905** Producten-scherm — Activeer-knop per rij
- `components/dashboard/product-list.tsx`: per rij "Activeer"-knop (verborgen voor reeds actief PB); actieve rij krijgt badge "Actief" (MD3-token `bg-primary-container`); klik op Activeer → `setActiveProduct` + `router.push('/products/[id]')`; ook in `/products/[id]` header een Activeer-knop als dat product nog niet actief is
- Done when: activeer in dashboard markeert juiste rij + landt op Product Backlog; demo-gebruiker krijgt toast en geen DB-mutatie
- [x] **ST-906** Edge cases — toegangsverlies en archivering
- Wanneer een PB wordt gearchiveerd, ge-leaved, of een productmember wordt verwijderd: `active_product_id` automatisch `null` voor betroffen users (server actions van `archiveProduct`, `leaveProduct`, `removeMember`); guard in `app/(app)/layout.tsx`: als `active_product_id` is gezet maar product is archived/onbereikbaar, server-side clear + redirect naar `/dashboard` met toast "Je actieve product is niet meer beschikbaar"
- Done when: scenario test — eigenaar archiveert → membership-gebruikers landen op dashboard met toast en active is gecleared
- [x] **ST-907** Documentatie en tests
- Functional spec: nieuw hoofdstuk "Actief Product Backlog" (concept, menugedrag, edge cases); README: navigatie-screenshot bijwerken; `docs/patterns/` indien nieuwe patroon (n.v.t. tenzij dropdown-switcher een herbruikbaar component wordt); jest-tests in `__tests__/actions/active-product.test.ts` voor setActive (toegang, demo, archived); Playwright/manueel scenario: log in → activeer PB → wissel via dropdown → archiveer → verifieer auto-clear
- Done when: `npm run lint && npx tsc --noEmit && npm test && npm run build` groen; spec-secties geschreven; `vendor/scrum4me`-submodule in mcp gesynced
---
### M10: Password-loze inlog via QR-pairing
**Implementatieplan:** [docs/plans/M10-qr-pairing-login.md](../plans/M10-qr-pairing-login.md)
Inloggen op een (publieke) desktop zonder wachtwoord: de desktop toont een QR-code, de gebruiker scant met een telefoon waar hij al ingelogd is, bevestigt expliciet, en de desktop is binnen 12 seconden ingelogd. Bouwt voort op de Postgres LISTEN/NOTIFY-infra van M8 (eigen kanaal `scrum4me_pairing`). Geen wachtwoord ingetypt op het publieke apparaat, geen credentials op de draad, demo-accounts geblokkeerd, paired-sessie heeft eigen kortere TTL (8 u) + `paired`-vlag voor toekomstige remote-revoke.
**Beveiligingsuitgangspunt:** `mobileSecret` reist alleen via QR-fragment (`#s=…`) → `location.hash` op de mobiel → POST-body. Desktop-SSE en claim authenticeren via een **HttpOnly pre-auth cookie** (`s4m_pair`, `Path=/api/auth/pair`, `Max-Age=120`, `SameSite=Lax`). Twee gescheiden hashes in DB (`secret_hash` voor mobiel-bewijs, `desktop_token_hash` voor desktop-bewijs) zodat geheim materiaal niet in URL-paden, querystrings, access logs, reverse-proxy logs, observability of browsergeschiedenis kan belanden.
Volledige flow + threat-model: `docs/patterns/qr-login.md` (op te leveren in ST-1008).
- [ ] **ST-1001** LoginPairing schema + Postgres-trigger
- **Schema:** `LoginPairing { id, secret_hash, desktop_token_hash, status, user_id?, desktop_ua?, desktop_ip?, created_at, expires_at, approved_at?, consumed_at? }`; back-relation `User.login_pairings`; `@@index([expires_at])`, `@@index([status, expires_at])`; `status` als string (`pending|approved|consumed|cancelled`); twee hash-kolommen scheiden mobiel-bewijs van desktop-bewijs
- **Trigger:** `notify_pairing_change()` + `AFTER INSERT/UPDATE` op `login_pairings`; `pg_notify('scrum4me_pairing', payload)` met `{ pairing_id, status, op }`; analoog aan `notify_solo_change` uit ST-801
- **Migratie:** `prisma migrate dev --name add_login_pairing`
- Done when: migratie slaagt; `psql $DIRECT_URL -c "LISTEN scrum4me_pairing;"` levert payload bij INSERT op `login_pairings`; beide hash-kolommen zijn `NOT NULL`
- [ ] **ST-1002** Pairing-helpers + sessie-uitbreiding + pre-auth-cookie
- **`lib/auth/pairing.ts`:** `generateMobileSecret()` en `generateDesktopToken()` (beide 32 bytes → base64url, los gegenereerd zodat ze elkaar niet onthullen), `hashToken(t)` (sha256-hex), `verifyToken(t, hash)` (timing-safe compare)
- **`lib/auth/pair-cookie.ts`:** `setPairCookie(response, desktopToken)` (`HttpOnly`, `Secure` in prod, `SameSite=Lax`, `Path=/api/auth/pair`, `Max-Age=120`); `readPairCookie(request)` returnt `desktopToken | null`; `clearPairCookie(response)` op claim/cancel
- **`SessionData` in `lib/session.ts`:** voeg optionele `paired?: boolean` en `pairedExpiresAt?: number` toe
- **`app/(app)/layout.tsx`:** extra guard — als `session.paired && session.pairedExpiresAt < Date.now()``session.destroy()` + `redirect('/login')`
- Done when: helpers hebben unit-tests; paired-sessie verloopt zichtbaar na vervaltijd; cookie wordt nooit door client-JS gelezen (HttpOnly-test)
- [ ] **ST-1003** `POST /api/auth/pair/start` — pairing aanmaken (anon)
- Route Handler zonder auth; leest UA + best-effort IP (`x-forwarded-for`); genereert los `mobileSecret` + `desktopToken`; insert `LoginPairing` met beide hashes, `status='pending'`, `expires_at = now() + 2 min`
- **Response body:** `{ pairingId, mobileSecret, expiresAt, qrUrl }``qrUrl = ${origin}/m/pair#id=…&s=…` (fragment, geen querystring)
- **Response header:** `Set-Cookie: s4m_pair=<desktopToken>; HttpOnly; Secure; SameSite=Lax; Path=/api/auth/pair; Max-Age=120`
- **Rate-limit:** patroon ST-608 (max 10 starts per IP per minuut)
- Done when: curl POST levert pairingId+mobileSecret in body en `s4m_pair`-cookie in header; 11e call binnen 60s geeft 429; rij in `login_pairings` zonder plaintext secret of desktop-token
- [ ] **ST-1004** SSE-route `/api/auth/pair/stream/[pairingId]` (cookie-auth)
- `runtime: 'nodejs'`, `maxDuration: 300`; pairingId in pad (niet sensitief), auth via `s4m_pair`-cookie: sha256(cookie) matcht `desktop_token_hash` van pairing met `pairingId` en `expires_at > now()`; anders 401
- **Geen query-parameters met geheim materiaal.** Browser stuurt cookie automatisch mee.
- Hergebruik LISTEN/NOTIFY-pattern uit `app/api/realtime/solo/route.ts` op kanaal `scrum4me_pairing`; filter notifications op `pairing_id`
- Auto-close bij status `consumed`/`cancelled` of na 240 s; heartbeat 25 s
- Done when: SSE-verbinding zonder `s4m_pair`-cookie geeft 401; met geldige cookie levert event binnen 1s na approve; stream sluit na consume; pairingId in URL is OK (niet vertrouwelijk)
- [ ] **ST-1005** Server actions + mobiele bevestigingspagina
- **`actions/pairing.ts`:** `getPairingForApproval(pairingId, mobileSecret)`, `approvePairing(pairingId, mobileSecret)` (demo-blokkade, hash-vergelijk tegen `secret_hash`, status pending→approved, bumpt `expires_at` +5 min, zet `user_id` + `approved_at`), `cancelPairing(pairingId, mobileSecret)`
- **`app/(app)/m/pair/page.tsx`:** Server Component achter de bestaande `(app)/layout.tsx` auth-guard; **leest géén query-params** — alleen statische uitleg + een client-island
- **`app/(app)/m/pair/pair-confirmation.tsx`:** Client Component die bij mount `window.location.hash` parseert (`#id=…&s=…`), via Server Action `getPairingForApproval` de UA/IP/username ophaalt, dan toont *"Inloggen op {ua} ({ip}) als {jouw-username}?"* met Bevestig/Annuleer-knoppen die `approvePairing`/`cancelPairing` aanroepen; succes-state *"Klaar — je kunt deze tab sluiten"*. Wist `location.hash` na approve zodat back/forward de secret niet onthult
- Demo-modus: approve geeft Nederlandse foutmelding (consistent ST-604)
- Done when: ingelogde mobiel ziet bevestigingspagina met UA + IP; secret komt nooit in een GET-URL voor; tap "Bevestig" zet status approved; demo-user ziet foutmelding en pairing blijft `pending`
- [ ] **ST-1006** `POST /api/auth/pair/claim` — desktop-cookie zetten (cookie-auth)
- Auth via `s4m_pair`-cookie (geen body-secret nodig); atomic update: `UPDATE login_pairings SET status='consumed', consumed_at=now() WHERE id=$1 AND status='approved' AND desktop_token_hash=$2 AND expires_at > now() RETURNING user_id`
- Bij rij geretourneerd: `getIronSession``session.userId = user.id; session.isDemo = user.is_demo; session.paired = true; session.pairedExpiresAt = Date.now() + 8h`; clear `s4m_pair`-cookie; anders 410 (al consumed) / 404 / 401
- Logging alleen `pairingId`, nooit cookie-waarde of mobileSecret
- Done when: claim met geldige cookie schrijft iron-session cookie en retourneert 200; tweede claim 410; ontbrekende/foute cookie 401; `s4m_pair` is na succes geclear'd
- [ ] **ST-1007** Desktop UI: QR-render + SSE-listener op `/login`
- **Dependency:** `qrcode.react` (client SVG; mobileSecret blijft op desktop in JS-geheugen)
- **`app/login/qr-login-button.tsx`:** Client Component; klik → POST `pair/start` (`credentials: 'same-origin'` zodat `s4m_pair`-cookie wordt geaccepteerd) → render QR met `qrUrl` (fragment-URL) → open `EventSource('/api/auth/pair/stream/<pairingId>', { withCredentials: true })` → bij `approved` event POST `pair/claim` (cookie-only) → bij succes `router.push('/dashboard')`; aftellende timer (2 min); bij timeout "Vernieuwen"-knop; cleanup bij unmount/redirect
<<<<<<<< HEAD:docs/backlog/index.md
- **`app/login/page.tsx`:** knop "Inloggen via mobiel" naast bestaande wachtwoord-form (MD3-tokens uit `docs/design/styling.md`)
========
- **`app/login/page.tsx`:** knop "Inloggen via mobiel" naast bestaande wachtwoord-form (MD3-tokens uit `docs/styling.md`)
>>>>>>>> origin/main:docs/backlog.md
- A11y: QR heeft alt-tekst met de URL voor screenreaders/copy-paste (de hash-suffix is onderdeel van die alt-tekst, niet van de page-URL die in browsergeschiedenis komt)
- Done when: end-to-end happy path werkt op localhost (twee browsers): A toont QR → B scant + bevestigt → A redirect naar `/dashboard` met `session.paired === true`; QR vernieuwt na expiry; geen secret zichtbaar in DevTools Network-tab onder URL-kolommen
- [ ] **ST-1008** Documentatie + acceptatietest
<<<<<<<< HEAD:docs/backlog/index.md
- **`docs/api/rest-contract.md`:** drie nieuwe endpoints (start/stream/claim) met request/response, cookie-mechaniek, foutcodes (400/401/403/404/410/422/429), curl-voorbeelden inclusief `--cookie-jar`
========
- **`docs/api.md`:** drie nieuwe endpoints (start/stream/claim) met request/response, cookie-mechaniek, foutcodes (400/401/403/404/410/422/429), curl-voorbeelden inclusief `--cookie-jar`
>>>>>>>> origin/main:docs/backlog.md
- **`docs/architecture.md`:** sectie "QR-pairing flow" met sequence-diagram + threat-model; expliciete subsectie *"Waarom geen secret in URL"* — fragments worden niet naar server gestuurd; SSE/claim authenticeren via HttpOnly cookie zodat secret-materiaal niet in access logs / reverse-proxy logs / observability-tools / browsergeschiedenis kan belanden
- **`docs/patterns/qr-login.md`:** nieuw pattern-doc voor toekomstige features die hetzelfde unauth-SSE-via-pre-auth-cookie-patroon willen hergebruiken
- **`CLAUDE.md`:** verwijzing naar het nieuwe pattern-doc in de patterns-tabel
- **Acceptatietest:** zeven scenario's handmatig: happy path, demo-block, replay, expiry tijdens pending, expiry tussen approve+claim, ontbrekende cookie op SSE/claim, secret niet aanwezig in `nginx`/Vercel access logs (controle via runtime-logs MCP-tool)
- Done when: docs gepubliceerd; alle zeven scenario's groen
---
### M11: Claude vraagt, gebruiker antwoordt
**Implementatieplan:** [docs/plans/M11-claude-questions.md](../plans/M11-claude-questions.md)
Persistent vraag-antwoord-kanaal tussen Claude Code (via MCP) en de actieve Scrum4Me-gebruiker. Claude schrijft een vraag naar `claude_questions` als hij vastloopt op een keuze; een Postgres-trigger emit op het bestaande `scrum4me_changes`-kanaal (uitgebreid met `entity: 'question'`); de Scrum4Me-app toont een notificatie-badge in de NavBar; iedereen met product-toegang kan antwoorden; Claude leest het antwoord (sync via polling met `wait_seconds`, of in een latere sessie via `get_question_answer`) en gaat door. Eerste concrete uitwerking van de AI-driven dev-flow-richting.
**Beveiligingsuitgangspunt:** atomic answer via `updateMany WHERE status='open'` voorkomt double-submit; demo-blok op zowel MCP-write-tools als Server Action; access-check via `productAccessFilter` in DB-query én SSE-filter; cron-endpoint voor expire-cleanup beveiligd met `Authorization: Bearer ${CRON_SECRET}`-header; logging alleen `question_id` (vraag/antwoord-tekst kan gevoelig materiaal bevatten).
- [ ] **ST-1101** `ClaudeQuestion` schema + Postgres-trigger
- **Schema:** `ClaudeQuestion { id, story_id, task_id?, product_id, asked_by, question, options?: Json, status, answer?, answered_by?, answered_at?, created_at, expires_at }`; relations op `User` (`asked_questions`, `answered_questions`), `Story`, `Task`, `Product`; indexes `(story_id, status)`, `(product_id, status)`, `(status, expires_at)`; `product_id` gedenormaliseerd voor SSE-filter
- **Trigger:** `notify_question_change()` `AFTER INSERT/UPDATE`; emit op `scrum4me_changes`-kanaal met payload `{ op, entity: 'question', id, product_id, story_id, task_id, assignee_id, status }`
- **Migratie:** `prisma migrate dev --name add_claude_questions`
- Done when: migratie slaagt; `psql LISTEN scrum4me_changes` toont nieuwe `entity: 'question'`-payload bij INSERT; bestaande solo-realtime-flow ongewijzigd; submodule sync na merge
- [ ] **ST-1102** MCP-tools voor Claude (in mcp-repo)
- **`ask_user_question`** (write): input `{ story_id, question, options?, task_id?, wait_seconds? }`; insert pairing + optioneel pollen tot `wait_seconds` (max 600); demo-blok via `requireWriteAccess`; access-check via `userCanAccessProduct(story.product_id, ...)`
- **`get_question_answer`** (read): haalt status + antwoord op een specifieke vraag op
- **`list_open_questions`** (read): lijst van eigen vragen (laatste 50, status open of answered)
- **`cancel_question`** (write): asker mag eigen vraag annuleren; status pending→cancelled
- Smoke-test in `scripts/smoke-test.ts`: `ask_user_question` met `wait_seconds=5` + parallel answer roundtrip
- Done when: MCP Inspector toont 4 nieuwe tools; smoke-test groen; demo-token op write-tools krijgt PERMISSION_DENIED; `tsc --noEmit` clean
- [ ] **ST-1103** Server Action `answerQuestion`
- `actions/questions.ts`: `answerQuestion(questionId, answer)` met getSession + Zod + demo-blok + `requireProductWriter` via `question.product_id`; atomic `updateMany WHERE status='open' AND expires_at>now`; `revalidatePath('/', 'layout')` voor badge-refresh
- Bij `count === 0`: disambigueer (al-answered/expired/access-fail) met begrijpelijke foutmelding
- Tests: 6 cases (happy, demo-block, geen access, race, expired, lege answer)
- Done when: `npm test` 6/6; handmatig: open vraag → antwoord → badge-count daalt met 1; demo-toast bij submit
- [ ] **ST-1104** User-scoped SSE-route `/api/realtime/notifications`
- Route Handler `runtime: 'nodejs'`, `maxDuration: 300`; auth via iron-session; **user-scoped** (geen product_id-param); filter `payload.entity === 'question'` én `payload.product_id` in user's accessible-product-ids
- Initial-state-event direct na connect (na LISTEN actief, conform M10 ST-1004 race-fix): summary-array van openstaande vragen voor deze user
- Update solo-route in `app/api/realtime/solo/route.ts`: in `shouldEmit` `if (payload.entity === 'question') return false` toevoegen — anders krijgen solo-clients ongewenst question-events
- Tests: 401 zonder cookie, filter op product-access, geen `entity:'question'`-events op solo-route
- Done when: `curl -N` levert events binnen 1s na INSERT; cross-product-test (user-A ziet user-B's vragen niet)
- [ ] **ST-1105** Notifications-UI (Bell + Sheet + Answer-modal + Zustand-store)
- **`stores/notifications-store.ts`** — Zustand store volgens `solo-store.ts`-patroon: `init`, `add`, `update`, `remove`, `optimisticAnswer`, `rollbackAnswer`; selectors `openCount`, `forYouCount`
- **`lib/realtime/use-notifications-realtime.ts`** — analoog aan `useSoloRealtime`; EventSource op `/api/realtime/notifications` met reconnect-backoff
- **`components/notifications/notifications-bridge.tsx`** — Server Component die initial-data fetcht en aan store geeft; mount in `app/(app)/layout.tsx` naast `<SoloRealtimeBridge />`
<<<<<<<< HEAD:docs/backlog/index.md
- **`components/shared/notifications-bell.tsx`** — Bell-icon (Lucide) met badge in NavBar (links van avatar); MD3-tokens uit `docs/design/styling.md`
========
- **`components/shared/notifications-bell.tsx`** — Bell-icon (Lucide) met badge in NavBar (links van avatar); MD3-tokens uit `docs/styling.md`
>>>>>>>> origin/main:docs/backlog.md
- **`components/notifications/notifications-sheet.tsx`** — shadcn Sheet van rechts; lijst gegroepeerd per product; story-assignee krijgt visuele *"wacht op jou"*-emphase
- **`components/notifications/answer-modal.tsx`** — shadcn Dialog; story-context-link, vraag-tekst, RadioGroup (als options) of Textarea (free-text), submit via `useTransition` + Server Action; demo-blok met tooltip
- Done when: bell + badge zichtbaar; klik opent Sheet met items; submit verwijdert item optimistisch; tweede tab van zelfde user ziet nieuwe vraag binnen 1-2s; demo-modus rendert maar Verstuur disabled
- [ ] **ST-1106** Demo-policy + access-tests
- Demo: Sheet rendert + Modal opent + Verstuur disabled met tooltip
- Access-isolation: cross-product test in `__tests__/api/notifications-stream.test.ts` (al gedeeltelijk in ST-1104)
- Story-assignee-emphase: visueel-only, toegang blijft product-membership-breed
- Done when: 4 access-tests groen; handmatige cross-product-verificatie
- [ ] **ST-1107** Vercel cron `expire-questions`
- **`app/api/cron/expire-questions/route.ts`** — POST handler beveiligd via `Authorization: Bearer ${CRON_SECRET}`; `updateMany WHERE status='open' AND expires_at<now → status='expired'`
- **`vercel.json`** — `crons` entry: `{ path: '/api/cron/expire-questions', schedule: '0 4 * * *' }` (dagelijks; Vercel Hobby-plan staat alleen daily crons toe)
- **`lib/env.ts`** + `.env.example``CRON_SECRET` via Zod
- Optioneel: ook M10's `login_pairings`-cleanup in dezelfde route opnemen
- Done when: handmatige `curl -X POST` met secret expireert oude rijen; Vercel-dashboard toont cron-config na deploy; onbevoegde call → 401
- [ ] **ST-1108** Documentatie + acceptatietest
<<<<<<<< HEAD:docs/backlog/index.md
- **`docs/api/rest-contract.md`:** secties "SSE — Notifications" + "Cron — Expire questions" met curl-voorbeelden
========
- **`docs/api.md`:** secties "SSE — Notifications" + "Cron — Expire questions" met curl-voorbeelden
>>>>>>>> origin/main:docs/backlog.md
- **`docs/architecture.md`:** sectie "Vraag-antwoord-kanaal Claude ↔ user" met Mermaid sequence-diagram + threat-model + "Waarom hergebruik scrum4me_changes-kanaal"
- **`docs/patterns/claude-question-channel.md`:** nieuw herbruikbaar pattern-doc voor toekomstige bidirectionele async-communicatie tussen MCP-agents en interactieve users
- **`CLAUDE.md`:** rij in Implementatiepatronen-tabel voor het nieuwe pattern
- **Acceptatietest** zes scenario's: sync happy (wait_seconds), async happy (geen wait), demo-block, access-isolation, expiry via cron, race op double-submit
- Done when: docs gepubliceerd; alle zes scenario's groen; backlog-parser-self-test toont M11 met ACTIVE-status
---
### M12: Ideeën & Grill/Plan jobs
**Implementatieplan:** [docs/plans/M12-ideas.md](../plans/M12-ideas.md)
**Dialog-profiel:** [docs/specs/dialogs/idea.md](../specs/dialogs/idea.md)
Idee is een nieuw concept tussen Todo en PBI. Strikt user_id-only (privé), met
twee Claude-jobs: **Grill Me** (interactief vragen-stellen via MCP) en **Make
Plan** (single-pass yaml-frontmatter genereren). De **Materialiseer**-knop
parseert het plan deterministisch en creëert PBI + stories + taken.
- [x] **ST-1192** — DB-schema & migratie voor Idea (T-491, T-492, T-489)
- Idea-model + IdeaLog-model + 3 enums; ClaudeJob.task_id nullable + idea_id +
kind; ClaudeQuestion.story_id nullable + idea_id; check-constraints +
pg_notify-trigger update
- [x] **ST-1193** — Lib + schemas + embedded prompts (T-493, T-494, T-495)
- zod-schemas, status-mapper + transition-guard, atomic code-generator,
yaml-frontmatter parser, embedded grill+make-plan prompts
- [x] **ST-1194** — Server actions + Todo→Idea promotie (T-496..T-499)
- CRUD, md-edit, job-triggers, materialize, relink, promoteTodoToIdeaAction
- [x] **ST-1195** — REST API + proxy demo-laag (T-500, T-501)
- /api/ideas + /api/ideas/[id]; demo-403 via proxy.ts catch-all
- [x] **ST-1196** — Realtime SSE + idea-store (T-502, T-503)
- SSE-routing voor idea-events; Zustand idea-store; extension van bestaande
notifications-realtime hook
- [ ] **ST-1197** — MCP-server tools (extern: madhura68/scrum4me-mcp)
- get_idea_context, update_idea_grill_md, update_idea_plan_md, log_idea_decision;
uitbreiding ask_user_question/wait_for_job/update_job_status; Docker rebuild
- [x] **ST-1198** — UI lijstpagina + row-actions (T-507, T-508, T-509)
- /ideas pagina, IdeaList tabel met filters, IdeaRowActions met
disabled-rules per status, idea-status-badge helper
- [x] **ST-1199** — UI detail + dialog + tabs (T-510..T-513)
- /ideas/[id] met 4 tabs (Idee/Grill/Plan/Timeline); md-editor met
yaml-validate; timeline met UNION view; pbi-link-card; dialog-profiel doc
- [x] **ST-1200** — Promote-from-Todo + sidebar (T-514, T-515)
- "→ Idee" knop in TodoCard, PromoteIdeaDialog, "Ideeën" nav-entry
- [ ] **ST-1201** — End-to-end smoke + docs-update (T-516, T-517)
- Volledige flow doorlopen volgens M12-ideas.md verificatie-script;
docs/runbooks/mcp-integration.md uitbreiden voor IDEA_*-job-kinds
- Done when: docs/INDEX.md opnieuw gegenereerd, alle stories ✓, MCP-server
PR met passende versie-bump gedeployed
---
## v2 Backlog (na MVP)
- [ ] Uitnodigingsflow voor teams — e-mailuitnodiging of link-gebaseerd; nu kunnen alleen admins met toegang tot het systeem Developers toevoegen via gebruikersnaam
- [ ] Daily Scrum scherm — voortgang per story bijhouden tijdens Sprint
- [ ] Sprint Review scherm — demo en feedback vastleggen per story
- [ ] Sprint Retrospective scherm — reflectie per Sprint
- [ ] Automatische story-statusupdate na commit via API
- [ ] Velocity tracking — statistieken over meerdere Sprints
- [ ] Definition of Done per product configureerbaar (nu vaste instelling)
- [ ] Notificaties / reminders
- [ ] Timeline / kalenderweergave per Sprint
- [ ] Integratie GitHub Issues / Linear
- [ ] Mobiele app — uitsluitend taken afvinken
- [ ] Export van Product Backlog en Sprint als markdown of CSV
---
## Definition of MVP Done
- [x] Alle M0M6 tasks afgerond
- [x] Volledige Lars-flow succesvol doorlopen (ST-612)
- [x] Alle 7 API-endpoints getest via curl (ST-403 t/m ST-409)
- [x] Demo-gebruiker kan inloggen en heeft geen schrijfrechten (ST-604)
- [x] App lokaal opzetbaar via README zonder extra hulp (ST-611)
- [x] CI/CD actief — falende build blokkeert merge (ST-610)
- [x] Beveiligingsreview API geslaagd (ST-609)
- [x] Geen bekende blocker-bugs

View file

@ -0,0 +1,462 @@
---
title: "DevPlanner — Product Backlog"
status: active
audience: [maintainer]
language: nl
last_updated: 2026-05-03
---
# DevPlanner — Product Backlog
**Versie:** 0.1 — april 2026
**Product:** DevPlanner
**Beschrijving:** Een lichtgewicht Scrum-gebaseerde projectplanner voor solo developers en kleine Scrum Teams die meerdere softwareprojecten parallel beheren. De app organiseert werk hiërarchisch (product → PBI → story → taak), biedt een visuele planningslaag en integreert met Claude Code voor geautomatiseerde implementatieflows.
**Git repo:** https://github.com/devplanner/devplanner
**Definition of Done:** Feature is geïmplementeerd, getest (unit + integratie), gedocumenteerd in code, en gedeployed naar de staging-omgeving zonder regressies.
---
## Prioriteiten
| Prioriteit | Betekenis |
|---|---|
| 1 — Kritiek | Blokkeert alle andere functionaliteit. Moet eerst. |
| 2 — Hoog | Core waarde van het product. MVP vereiste. |
| 3 — Middel | Verhoogt bruikbaarheid significant. v1 wenselijk. |
| 4 — Laag | Waardevol maar niet blokkerend. v2 kandidaat. |
---
## PBI-01 — Authenticatie & gebruikersbeheer
**Prioriteit:** 1 — Kritiek
**Omschrijving:** Het Scrum Team kan een account aanmaken en inloggen met gebruikersnaam en wachtwoord. Een demo-gebruiker heeft alleen leesrechten. Gebruikers kunnen één of meerdere Scrum-rollen aannemen.
### Stories
**S-01-01: Account aanmaken**
Als bezoeker wil ik een account aanmaken met gebruikersnaam en wachtwoord, zodat ik toegang krijg tot de app.
Acceptatiecriteria:
- Gebruikersnaam en wachtwoord zijn verplicht
- Gebruikersnaam is uniek; dubbele aanmelding geeft foutmelding
- Wachtwoord heeft minimaal 8 tekens
- Na aanmaken wordt de gebruiker direct ingelogd
- Geen e-mailverificatie vereist in v1
**S-01-02: Inloggen**
Als geregistreerde gebruiker wil ik inloggen met gebruikersnaam en wachtwoord, zodat ik mijn projecten kan beheren.
Acceptatiecriteria:
- Incorrecte combinatie geeft generieke foutmelding (geen onderscheid gebruikersnaam/wachtwoord)
- Na inloggen wordt de gebruiker doorgestuurd naar het dashboard
- Sessie blijft actief totdat de gebruiker uitlogt
**S-01-03: Uitloggen**
Als ingelogde gebruiker wil ik kunnen uitloggen, zodat mijn sessie veilig afgesloten wordt.
Acceptatiecriteria:
- Uitlogknop altijd zichtbaar in de navigatie
- Na uitloggen wordt de gebruiker naar de loginpagina gestuurd
- Sessiedata wordt gewist
**S-01-04: Demo-gebruiker (read-only)**
Als bezoeker wil ik kunnen inloggen als demo-gebruiker, zodat ik de app kan verkennen zonder een account aan te maken.
Acceptatiecriteria:
- Vaste inloggegevens voor de demo-gebruiker zijn beschikbaar op de loginpagina
- Demo-gebruiker ziet alle data maar kan niets aanmaken, aanpassen of verwijderen
- Alle actieknoppen (aanmaken, bewerken, verwijderen) zijn zichtbaar maar uitgeschakeld met tooltip "Niet beschikbaar in demo-modus"
- Demo-gebruiker kan niet van rol wisselen
**S-01-05: Roltoewijzing**
Als gebruiker wil ik één of meerdere Scrum-rollen kunnen aannemen (Product Owner, Scrum Master, Developer), zodat de app weet in welke context ik werk.
Acceptatiecriteria:
- Gebruiker kan bij registratie of in instellingen rollen selecteren
- Minimaal één rol is verplicht
- Alle drie de rollen tegelijk zijn toegestaan
- Rolkeuze is zichtbaar in de navigatie/profielbalk
- Rolkeuze heeft in v1 geen effect op zichtbare functionaliteit (voorbereiding op v2)
---
## PBI-02 — Productbeheer
**Prioriteit:** 1 — Kritiek
**Omschrijving:** Het Scrum Team kan producten aanmaken, bekijken, bewerken en archiveren. Een product heeft een naam, beschrijving en koppeling naar een git-repository.
### Stories
**S-02-01: Product aanmaken**
Als Product Owner wil ik een nieuw product aanmaken met naam, beschrijving en git-repo URL, zodat ik een werkruimte heb voor de Product Backlog.
Acceptatiecriteria:
- Naam is verplicht en uniek per gebruiker
- Beschrijving is optioneel (vrije tekst)
- Git-repo URL is optioneel maar wordt gevalideerd als geldige URL
- Product is direct zichtbaar in de productenlijst na aanmaken
**S-02-02: Product bewerken**
Als Product Owner wil ik de naam, beschrijving en git-repo URL van een product kunnen aanpassen, zodat de informatie actueel blijft.
Acceptatiecriteria:
- Alle velden zijn bewerkbaar
- Wijzigingen worden opgeslagen zonder de pagina te verlaten
- Lege naam geeft validatiefout
**S-02-03: Product archiveren**
Als Product Owner wil ik een product kunnen archiveren, zodat het niet meer in het overzicht verschijnt maar de data bewaard blijft.
Acceptatiecriteria:
- Gearchiveerde producten verschijnen niet in de standaardlijst
- Er is een optie om gearchiveerde producten te tonen
- Archiveren is omkeerbaar (product kan worden hersteld)
**S-02-04: Productenlijst bekijken**
Als gebruiker wil ik een overzicht zien van alle actieve producten, zodat ik snel naar het juiste product kan navigeren.
Acceptatiecriteria:
- Lijst toont naam, beschrijving (ingekort) en git-repo link
- Klikken op een product opent de Product Backlog van dat product
- Lege staat toont een prompt om een product aan te maken
---
## PBI-03 — Product Backlog
**Prioriteit:** 1 — Kritiek
**Omschrijving:** Het Scrum Team kan de Product Backlog beheren via een gesplitst scherm: links de PBI's, rechts de bijbehorende stories. Items kunnen aangemaakt, bewerkt, geprioriteerd en gerangschikt worden via drag-and-drop (dnd-kit).
### Stories
**S-03-01: PBI aanmaken**
Als Product Owner wil ik een PBI aanmaken in de Product Backlog, zodat ik nieuwe functionaliteit kan definiëren.
Acceptatiecriteria:
- PBI heeft een titel (verplicht) en omschrijving (optioneel)
- PBI krijgt een prioriteit (1 t/m 4)
- Nieuw PBI verschijnt onderaan de lijst voor de gekozen prioriteit
- Aanmaken via knop in de navigatiebar van het linkerpaneel
**S-03-02: PBI bewerken**
Als Product Owner wil ik de titel, omschrijving en prioriteit van een PBI kunnen aanpassen.
Acceptatiecriteria:
- Dubbelklikken of via contextmenu opent bewerkingsmodus
- Alle velden zijn inline bewerkbaar
- Prioriteitswijziging herplaatst het PBI visueel
**S-03-03: PBI verwijderen**
Als Product Owner wil ik een PBI kunnen verwijderen, zodat irrelevante items de backlog niet vervuilen.
Acceptatiecriteria:
- Verwijderen vereist bevestiging
- Verwijderen van een PBI verwijdert ook alle bijbehorende stories (cascade)
- Actie is niet ongedaan te maken; bevestigingsdialoog waarschuwt hiervoor
**S-03-04: PBI prioriteit instellen**
Als Product Owner wil ik per PBI een prioriteit kunnen instellen (1 t/m 4), zodat de volgorde van de backlog de businesswaarde weerspiegelt.
Acceptatiecriteria:
- Prioriteit is instelbaar via dropdown of inline label
- PBI's worden gegroepeerd per prioriteit in de lijst
- Visuele scheiding per prioriteitsgroep (kleurband of scheidingslijn)
**S-03-05: PBI volgorde aanpassen via drag-and-drop**
Als Product Owner wil ik de volgorde van PBI's binnen dezelfde prioriteit kunnen aanpassen via drag-and-drop, zodat ik fijnmazige prioritering kan doen.
Acceptatiecriteria:
- Drag-and-drop werkt vloeiend (60fps) via dnd-kit
- Volgorde wordt direct opgeslagen na loslaten
- Drag over prioriteitsgrens wisselt de prioriteit van het PBI
- Visuele placeholder toont de doelpositie tijdens het slepen
**S-03-06: PBI filteren**
Als gebruiker wil ik PBI's kunnen filteren op prioriteit of status, zodat ik me kan focussen op het relevante werk.
Acceptatiecriteria:
- Filteropties beschikbaar in de navigatiebar van het linkerpaneel
- Filter werkt realtime (geen herlaadactie)
- Actief filter is duidelijk zichtbaar; eenvoudig te wissen
**S-03-07: Gesplitst scherm Product Backlog**
Als gebruiker wil ik de Product Backlog bekijken als gesplitst scherm (PBI's links, stories rechts), zodat ik snel kan navigeren tussen PBI's en hun stories.
Acceptatiecriteria:
- Scherm is standaard 50/50 verdeeld
- De splitter is horizontaal versleepbaar
- Elk paneel heeft een eigen navigatiebar met acties
- Selecteren van een PBI links toont de bijbehorende stories rechts
- Geselecteerde PBI is visueel gemarkeerd
---
## PBI-04 — Story-beheer
**Prioriteit:** 1 — Kritiek
**Omschrijving:** Stories kunnen worden aangemaakt, bewerkt, geprioriteerd en gerangschikt binnen een PBI. Stories worden weergegeven als blokken van circa 10% schermbreedte, gerangschikt op prioriteit.
### Stories
**S-04-01: Story aanmaken**
Als Product Owner wil ik een story aanmaken binnen een PBI, zodat ik de functionaliteit kan uitwerken in uitvoerbare eenheden.
Acceptatiecriteria:
- Story heeft een titel (verplicht), omschrijving (optioneel) en prioriteit
- Aanmaken via navigatiebar van het rechterpaneel
- Nieuwe story verschijnt als blok rechts, in de juiste prioriteitsgroep
**S-04-02: Story weergave als blokken**
Als gebruiker wil ik stories zien als compacte blokken (~10% schermbreedte), zodat ik snel een overzicht heb van alle stories per PBI.
Acceptatiecriteria:
- Elk blok toont: storytitel, prioriteit, status
- Blokken zijn gerangschikt op prioriteit (hoog naar laag, links naar rechts)
- Elke nieuwe prioriteitsgroep heeft een visuele scheiding (kleurband of lijn)
- Blokken zijn klikbaar voor detail/bewerking
**S-04-03: Story prioriteit instellen**
Als Product Owner wil ik per story een prioriteit instellen, zodat de Developer weet wat als eerste opgepakt moet worden.
Acceptatiecriteria:
- Prioriteit instelbaar via het storyblok (dropdown of label)
- Prioriteitswijziging herplaatst het blok in de juiste groep
**S-04-04: Story volgorde aanpassen via drag-and-drop**
Als Product Owner wil ik de volgorde van stories binnen dezelfde prioriteit aanpassen via drag-and-drop, zodat ik de uitvoeringsvolgorde kan finetunen.
Acceptatiecriteria:
- Drag-and-drop werkt via dnd-kit tussen en binnen prioriteitsgroepen
- Volgorde wordt direct opgeslagen
- Slepen over een prioriteitsgrens wijzigt de prioriteit
**S-04-05: Story bewerken**
Als Product Owner wil ik de titel, omschrijving en prioriteit van een story kunnen aanpassen.
Acceptatiecriteria:
- Bewerkbaar via klikken op het storyblok
- Wijzigingen opgeslagen zonder paginaverversing
**S-04-06: Story verwijderen**
Als Product Owner wil ik een story kunnen verwijderen.
Acceptatiecriteria:
- Verwijderen vereist bevestiging
- Cascade verwijdering van gekoppelde taken
- Niet ongedaan te maken; waarschuwing in dialoog
---
## PBI-05 — Todo-lijst
**Prioriteit:** 2 — Hoog
**Omschrijving:** Gebruikers kunnen een snelle todo-lijst bijhouden voor ongeplande of kortstondige taken. Een todo-item kan worden gepromoveerd naar een PBI of story.
### Stories
**S-05-01: Todo-item aanmaken**
Als gebruiker wil ik snel een todo-item aanmaken zonder het aan een product te koppelen, zodat ik losse gedachten kan vastleggen zonder de planningsflow te onderbreken.
Acceptatiecriteria:
- Todo heeft alleen een titel (verplicht)
- Aanmaken via een snel-invoerveld (Enter om op te slaan)
- Todo's zijn zichtbaar in een aparte todo-sectie of zijpaneel
**S-05-02: Todo-item afvinken**
Als gebruiker wil ik een todo-item kunnen afvinken, zodat ik bij kan houden wat klaar is.
Acceptatiecriteria:
- Afgevinkte items zijn visueel doorgestreept
- Afgevinkte items blijven zichtbaar maar kunnen worden gearchiveerd
**S-05-03: Todo promoveren naar PBI**
Als Product Owner wil ik een todo-item promoveren naar een PBI in een bestaand product, zodat losse ideeën in de formele backlog terechtkomen.
Acceptatiecriteria:
- Promoten opent een dialoog om product en prioriteit te kiezen
- Het todo-item wordt omgezet naar een PBI en verdwijnt uit de todo-lijst
- De PBI-titel is gelijk aan de todo-titel (bewerkbaar in dialoog)
**S-05-04: Todo promoveren naar story**
Als Product Owner wil ik een todo-item promoveren naar een story binnen een bestaand PBI, zodat ik snel nieuwe stories kan toevoegen vanuit losse notities.
Acceptatiecriteria:
- Promoten opent een dialoog om product, PBI en prioriteit te kiezen
- Todo wordt omgezet naar een story en verdwijnt uit de todo-lijst
---
## PBI-06 — Sprint Backlog & Sprint Planning
**Prioriteit:** 2 — Hoog
**Omschrijving:** Het Scrum Team kan een Sprint aanmaken met een Sprint Goal, stories uit de Product Backlog naar de Sprint Backlog slepen, en de volgorde bepalen.
### Stories
**S-06-01: Sprint aanmaken**
Als Scrum Master wil ik een nieuwe Sprint aanmaken met een Sprint Goal, zodat het Scrum Team een duidelijk doel heeft voor de komende Sprint.
Acceptatiecriteria:
- Sprint heeft een Sprint Goal (verplicht, vrije tekst)
- Sprint is gekoppeld aan een product
- Er kan maar één actieve Sprint per product zijn
**S-06-02: Sprint Backlog scherm (gesplitst)**
Als gebruiker wil ik de Sprint Backlog kunnen beheren via een gesplitst scherm (Sprint Backlog links, stories per PBI rechts), zodat ik snel stories kan toevoegen aan de Sprint.
Acceptatiecriteria:
- Links: Sprint Backlog met geselecteerde stories in volgorde
- Rechts: stories uit de Product Backlog, gegroepeerd per PBI
- Splitter is horizontaal versleepbaar
- Elk paneel heeft eigen navigatiebar
**S-06-03: Story naar Sprint slepen**
Als Developer wil ik een story vanuit de Product Backlog naar de Sprint Backlog kunnen slepen, zodat ik bepaal wat we deze Sprint gaan oppakken.
Acceptatiecriteria:
- Drag-and-drop werkt via dnd-kit tussen rechterpaneel en linkerpaneel
- Story verschijnt in de Sprint Backlog op de gesleepte positie
- Story in de Product Backlog krijgt visuele markering "In Sprint"
- Een story kan maar aan één actieve Sprint gekoppeld zijn
**S-06-04: Volgorde stories in Sprint bepalen**
Als Developer wil ik de volgorde van stories in de Sprint Backlog kunnen aanpassen via drag-and-drop, zodat ik de uitvoeringsvolgorde bepaal.
Acceptatiecriteria:
- Drag-and-drop werkt binnen de Sprint Backlog
- Volgorde wordt direct opgeslagen
- Volgorde is onafhankelijk van de prioriteit in de Product Backlog
**S-06-05: Story uit Sprint verwijderen**
Als Developer wil ik een story uit de Sprint Backlog kunnen verwijderen (terugplaatsen in de Product Backlog), zodat we de Sprint scope kunnen aanpassen.
Acceptatiecriteria:
- Story verdwijnt uit de Sprint Backlog
- Story is weer beschikbaar in de Product Backlog
- Actie vereist geen bevestiging (is niet destructief)
---
## PBI-07 — Sprint Planning (taken per story)
**Prioriteit:** 2 — Hoog
**Omschrijving:** Tijdens Sprint Planning worden stories opgedeeld in taken. Hetzelfde gesplitste scherm wordt gebruikt: stories links, taken rechts. Taken kunnen geprioriteerd en gerangschikt worden.
### Stories
**S-07-01: Sprint Planning scherm**
Als Developer wil ik een Sprint Planning scherm zien met stories links en taken rechts, zodat ik per story taken kan aanmaken en rangschikken.
Acceptatiecriteria:
- Links: stories in de Sprint Backlog in volgorde
- Rechts: taken van de geselecteerde story
- Selecteren van een story links toont de bijbehorende taken rechts
- Gesplitst scherm is horizontaal versleepbaar
**S-07-02: Taak aanmaken**
Als Developer wil ik een taak aanmaken onder een story, zodat ik het uitvoerbare werk kan definiëren.
Acceptatiecriteria:
- Taak heeft een titel (verplicht), omschrijving (optioneel) en prioriteit
- Aanmaken via navigatiebar van het rechterpaneel
- Nieuwe taak verschijnt onderaan de takenlijst van de story
**S-07-03: Taak prioriteit instellen**
Als Developer wil ik per taak een prioriteit instellen, zodat de uitvoeringsvolgorde duidelijk is.
Acceptatiecriteria:
- Prioriteit instelbaar via taakregel (dropdown of label)
- Taken gegroepeerd en gerangschikt op prioriteit
**S-07-04: Taak volgorde aanpassen via drag-and-drop**
Als Developer wil ik de volgorde van taken binnen een story kunnen aanpassen via drag-and-drop, zodat de uitvoeringsvolgorde precies klopt.
Acceptatiecriteria:
- Drag-and-drop via dnd-kit binnen de takenlijst
- Volgorde direct opgeslagen na loslaten
**S-07-05: Taakstatus bijhouden**
Als Developer wil ik de status van een taak kunnen bijhouden (To Do, In Progress, Done), zodat de voortgang van de Sprint zichtbaar is.
Acceptatiecriteria:
- Status is instelbaar via de UI (dropdown of knoppen)
- Statuswijziging is direct zichtbaar in het Sprint Planning scherm
- Story toont een voortgangsindicator op basis van taakstatussen
---
## PBI-08 — Claude Code integratie
**Prioriteit:** 2 — Hoog
**Omschrijving:** Claude Code kan via een REST API (en later MCP) stories en taken ophalen, de volgorde beoordelen, een implementatieplan opstellen, tests uitvoeren en committen. Elk resultaat wordt vastgelegd in de story.
### Stories
**S-08-01: REST API — story ophalen**
Als Developer (via Claude Code) wil ik de hoogst geprioriteerde open story van een product kunnen ophalen via een API-endpoint, zodat Claude Code weet wat er gedaan moet worden.
Acceptatiecriteria:
- Endpoint: `GET /api/products/:id/next-story`
- Retourneert: story-id, titel, omschrijving, acceptatiecriteria, gekoppelde taken
- Authentiseerd via API-token
- Geeft 404 als er geen open stories zijn
**S-08-02: REST API — eerste 10 taken ophalen**
Als Developer (via Claude Code) wil ik de eerste 10 taken van de Sprint Backlog kunnen ophalen, zodat Claude Code de volgorde kan beoordelen en zo nodig aanpassen.
Acceptatiecriteria:
- Endpoint: `GET /api/sprints/:id/tasks?limit=10`
- Retourneert taken in huidige volgorde met id, titel, prioriteit, status
- Claude Code kan de volgorde aanpassen via een apart endpoint
**S-08-03: REST API — taakvolgorde aanpassen**
Als Developer (via Claude Code) wil ik de volgorde van taken kunnen aanpassen via de API, zodat Claude Code een optimale uitvoeringsvolgorde kan bepalen.
Acceptatiecriteria:
- Endpoint: `PATCH /api/stories/:id/tasks/reorder`
- Accepteert een geordende lijst van taak-id's
- Volgorde wordt direct weerspiegeld in de UI
**S-08-04: Implementatieplan vastleggen**
Als Developer (via Claude Code) wil ik een implementatieplan kunnen schrijven naar een story, zodat de ontwerpbeslissingen traceerbaar zijn.
Acceptatiecriteria:
- Endpoint: `POST /api/stories/:id/log`
- Veld: `type: "implementation_plan"`, `content: string`
- Log-entry verschijnt in de story-activiteitenlog in de UI
**S-08-05: Teststatus vastleggen**
Als Developer (via Claude Code) wil ik de uitkomst van testruns kunnen vastleggen in een story, zodat kwaliteitsbewijs per story bewaard blijft.
Acceptatiecriteria:
- Endpoint: `POST /api/stories/:id/log`
- Veld: `type: "test_result"`, `content: string`, `status: "passed" | "failed"`
- Teststatus zichtbaar in de story-activiteitenlog
**S-08-06: Commit-hash vastleggen**
Als Developer (via Claude Code) wil ik de commit-hash na een succesvolle commit kunnen vastleggen in een story, zodat code en planning direct gekoppeld zijn.
Acceptatiecriteria:
- Endpoint: `POST /api/stories/:id/log`
- Veld: `type: "commit"`, `hash: string`, `message: string`
- Commit-hash is klikbaar en linkt naar de git-repo (indien geconfigureerd)
**S-08-07: Story activiteitenlog in UI**
Als gebruiker wil ik per story een activiteitenlog zien met alle vastgelegde implementatieplannen, testresultaten en commits, zodat ik de volledige uitvoeringsgeschiedenis op één plek heb.
Acceptatiecriteria:
- Log toont alle entries in chronologische volgorde
- Elk type entry heeft een eigen visuele stijl (plan, test, commit)
- Log is zichtbaar in de story-detailweergave
- Log is read-only in de UI (schrijven gebeurt via API)
---
## PBI-09 — Infrastructuur & deployment
**Prioriteit:** 1 — Kritiek
**Omschrijving:** De app is deployable op Vercel + Neon (cloud) én volledig lokaal draaibaar zonder externe afhankelijkheden.
### Stories
**S-09-01: Cloud deployment (Vercel + Neon)**
Als Developer wil ik de app deployen op Vercel met een Neon PostgreSQL-database, zodat de app beschikbaar is via een URL.
Acceptatiecriteria:
- `next build` slaagt zonder fouten
- Database-migraties worden uitgevoerd via Prisma
- Environment variables zijn gedocumenteerd in `.env.example`
**S-09-02: Lokale modus**
Als Developer wil ik de app lokaal kunnen draaien met een Neon PostgreSQL-database, zodat de lokale setup overeenkomt met productie.
Acceptatiecriteria:
- `npm run dev` start de app lokaal zonder Vercel of Neon account
- Database wordt aangemaakt via `prisma db push`
- README bevat stap-voor-stap instructies voor lokale setup
**S-09-03: API-token authenticatie**
Als Developer wil ik een API-token kunnen genereren in de app, zodat Claude Code veilig kan communiceren met de REST API.
Acceptatiecriteria:
- Gebruiker kan een API-token aanmaken in de instellingenpagina
- Token wordt eenmalig getoond en daarna niet meer zichtbaar
- Token kan worden ingetrokken
- Alle API-endpoints vereisen een geldig token via `Authorization: Bearer`
---
## Backlog — v2 kandidaten (niet in v1)
| PBI | Omschrijving |
|---|---|
| Daily Scrum scherm | Voortgang per story bijhouden tijdens de Sprint |
| Sprint Review scherm | Demo en feedback vastleggen per story |
| Sprint Retrospective scherm | Reflectie vastleggen per Sprint |
| Meerdere gebruikers per Scrum Team | Uitgebreide auth met rol-gebaseerde permissies |
| Automatische statusupdate na commit | Story op Done zetten via API-aanroep |
| Velocity tracking | Statistieken over meerdere Sprints |
| Notificaties / reminders | Push of e-mailmeldingen |
| Timeline / kalenderweergave | Sprint-planning op kalender |
| Definition of Done per product configureerbaar | Nu vaste instelling; later flexibel |
| Integratie GitHub Issues / Linear | Import/export van PBI's en stories |
---
*Dit document dient als testdata voor de eerste implementatie van de datastructuur.*
*Versie 0.1 — te updaten na Sprint 1 Review.*

1433
docs/old/functional.md Normal file

File diff suppressed because it is too large Load diff

128
docs/old/pbi-dialog.md Normal file
View file

@ -0,0 +1,128 @@
---
title: "PbiDialog Profiel"
status: active
audience: [maintainer, contributor]
language: nl
last_updated: 2026-05-03
---
# PbiDialog Profiel
> Volgt **`docs/patterns/dialog.md`** (de generieke spec voor élke entity-dialog in Scrum4Me).
> Dit document beschrijft alleen de PBI-specifieke afwijkingen en keuzes — alle gedeelde regels (layout, motion, demo-policy, foutcodes, validatie, theming, dialog-gedrag) staan in de generieke spec en worden hier niet herhaald.
> **Belangrijk:** als een regel in dit profiel botst met de generieke spec, wint de generieke spec. Documenteer hier de afwijking + reden, of pas de generieke spec aan.
---
## Velden
| Veld | Type | Mode | Validatie |
|---|---|---|---|
| `code` | `string \| null` | beide | optional, max 30 chars, mono-font, placeholder `auto` op create (server kent dan zelf een code toe) |
| `title` | `string` (required) | beide | trim, 1-200 chars |
| `priority` | `int` (1-4, P1 = hoogste) | beide | int 1-4, default 2 (kan via `defaultPriority`-prop bij create) |
| `status` | `PbiStatusApi` enum | beide | enum, default `'ready'` |
| `description` | `string \| null` | beide | optional, max 2000 chars, plain textarea (geen markdown rendering binnen de dialog) |
`PbiStatusApi` enum (lowercase, mapped via `lib/task-status.ts`): zie `<PbiStatusSelect>` voor de waarden.
### Veld-specifiek gedrag
- **Code + Titel** in één rij (`grid-cols-[6rem_1fr]`)
- **Prioriteit + Status** in één rij (`grid-cols-2`)
- **Prioriteit** via `<PrioritySelect>` (gedeelde primitive, géén segmented buttons in deze dialog)
- **Status** via `<PbiStatusSelect>` (PBI-specifieke wrapper rond gedeelde select)
- **Description** is `<Textarea rows={3} resize-none>` — géén auto-grow, géén markdown-hint, géén char-counter (afwijking van generieke spec; rationale: PBI-descriptions zijn doorgaans kort en richtinggevend)
---
## URL- of state-pattern
- **Gekozen:** state-based (`state: PbiDialogState | null` prop, gerendeerd binnen `PbiList`)
- **Reden:** PBI-dialog leeft altijd binnen `PbiList` op de product-backlog-pagina; deep-linking is niet vereist en zou een tweede edit-flow toevoegen.
- **State-shape:**
```ts
type PbiDialogState =
| { mode: 'create'; productId: string; defaultPriority?: number }
| { mode: 'edit'; pbi: PbiDialogPbi; productId: string }
```
- **Sluiten:** `onClose()` callback uit de parent — `setState(null)` in `PbiList`.
---
## Status-veld
- **Default bij create:** `'ready'` (PBI-default state)
- **Geen verberging in create-mode** — anders dan TaskDialog wordt status hier wél getoond bij create, omdat een PBI zonder expliciete status onhandig is voor backlog-grooming
---
## Server actions
| Actie | Locatie | Form-binding | Revalidatie |
|---|---|---|---|
| `createPbiAction` | `actions/pbis.ts` | via `useActionState` + `<form action>` (FormData) | server-side `revalidatePath` op product-backlog |
| `updatePbiAction` | `actions/pbis.ts` | idem | idem |
| ~~`deletePbiAction`~~ | **(ontbreekt)** | n.v.t. | n.v.t. |
Beide acties moeten de drielaagse demo-policy volgen (zie § Bekende gaps).
---
## Speciale gedragingen
### Form-state via `useActionState`
PbiDialog gebruikt het `useActionState` + `useFormStatus`-patroon (Server Actions / native React), niet `react-hook-form`. Dit is een toegestaan alternatief volgens de generieke spec § 2. Field-errors worden gemapt via een lokale `fieldError(field)`-helper die `result.error` als `Record<string, string[]>` interpreteert wanneer 'm geen string is.
### `key`-prop op `<form>`
Het `<form>`-element heeft `key={isEdit ? pbi!.id : 'create'}` — dit reset native form-state (defaultValues) wanneer de dialog tussen create en edit wisselt of wanneer een ander record bewerkt wordt.
### Hidden inputs voor server-binding
`priority` en `status` worden via `<input type="hidden">` doorgegeven aan de Server Action (de UI-controls zijn JS-state, niet directe form-fields).
---
## Triggers
- **Create-trigger:** `+ PBI`-knop in `PanelNavBar` van `PbiList``setPbiDialogState({ mode: 'create', ... })`
- **Edit-trigger:** edit-icoon op een PBI-rij in `PbiList``setPbiDialogState({ mode: 'edit', pbi, ... })`
---
## Bekende gaps t.o.v. generieke spec
> Deze items wijken af van `docs/patterns/dialog.md` en horen in een vervolg-PR rechtgezet (niet onderdeel van de huidige docs-introductie).
- ❌ **Geen `<DemoTooltip>`** rond submit-knop — laag 3 van de drielaagse demo-policy ontbreekt voor PBI-create/update. Dat betekent dat een demo-user de knop kan klikken; de server action blokkeert nog steeds (laag 2), maar de UX is suboptimaal.
- ❌ **Geen delete-knop / `deletePbiAction`** — alleen create + update. Of dat bewust is (PBI's worden nooit verwijderd, alleen status veranderd) of een gat, moet expliciet worden besloten en in dit profiel vastgelegd.
- ❌ **Geen dirty-close-guard** — Esc / backdrop / Cancel sluiten direct, ook met onopgeslagen wijzigingen. Generieke spec § 8.1 vereist een AlertDialog bij `isDirty`.
- ❌ **Geen Cmd/Ctrl+Enter shortcut** — alleen klik op submit-knop.
- ❌ **Geen char-counter / markdown-hint** op description — bewust weggelaten omdat PBI-descriptions kort zijn, maar verdient expliciete bevestiging.
- ⚠️ **Layout wijkt af** van de generieke responsive-tabel: `sm:max-w-md` i.p.v. de `max-w-[50vw]` / `90vw` / full-screen-progressie uit § 4.
---
## Bewust NIET in v1
Specifiek voor PbiDialog (boven op de algemene out-of-scope-lijst in `docs/patterns/dialog.md` § 13):
- ❌ Inline aanmaken van child-stories binnen de PBI-dialog (gebeurt via StoryDialog vanuit `StoryPanel`)
- ❌ Bulk-status-update over meerdere PBI's
- ❌ PBI-templates / kopiëren
---
## Referenties
- `components/backlog/pbi-dialog.tsx` — implementatie
- `actions/pbis.ts` — server actions
- `components/shared/priority-select.tsx` — gedeelde priority-control
- `components/shared/pbi-status-select.tsx` — PBI-status-select
- `lib/task-status.ts``PbiStatusApi`-mapper
- `docs/patterns/dialog.md` — generieke spec (bron-of-truth)
- `docs/architecture.md` — datamodel `Pbi`
- `docs/styling.md` — MD3-tokens, status- en priority-kleuren

View file

@ -0,0 +1,150 @@
---
title: "CLAUDE.md workflow-update na M7 + ST-509/511/512/513"
status: done
audience: [maintainer, contributor]
language: nl
last_updated: 2026-05-03
applies_to: [M7, ST-509, ST-511, ST-512, ST-513]
---
# Plan: CLAUDE.md workflow-update na M7 + ST-509/511/512/513
## Aanleiding
`CLAUDE.md` is voor het laatst groot bijgewerkt op 2026-04-25 (`docs/decisions/agent-instructions-history.md`). Sindsdien is er substantieel werk geland dat de workflow raakt:
- **ST-511** entity codes (Product/PBI/Story) — branch- en commit-conventies hangen er nu aan vast
- **ST-513** API hardening — `400` (malformed JSON) vs `422` (zod-validatie), lowercase status-enums op API-grens, `StoryLog.metadata` JSONB
- **PR #2** review-saga (8 testbestanden faalden bij contract-flip) — duidelijk leerpunt: testpariteit hoort bij contract-wijziging
- **M7 MCP-server** — Claude Code praat nu native met Scrum4Me via `mcp__scrum4me__*` tools en de prompt `implement_next_story`. De huidige 7-stap "vraag-de-gebruiker"-loop in CLAUDE.md is daarmee gedateerd
- **lib/code-server.ts** vs **lib/code.ts** — split is nodig om client-bundle vrij te houden van `pg`. Als gotcha noemenswaard
- **Schema-drift cron** (`trig_015FFUnxjz9WMuhhWNGBQKFD`) — wekelijkse remote agent — agents moeten weten wat ze met zijn rapport doen
Doel: CLAUDE.md weerspiegelt de werkelijke 2026-04-27 workflow zonder dat het een changelog wordt.
## Scope — wat we wél en niet aanpassen
**Wel** (in `CLAUDE.md`):
1. Workflow-sectie — MCP-first met expliciete fallback
2. Conventies — uitbreiden met status-enums, foutcodes, test-pariteit, entity codes in commits
3. Implementatiepatronen — rij voor `lib/task-status.ts` en `lib/code-server.ts`-boundary toevoegen
4. Nieuwe sectie "MCP-integratie" — wat staat er, hoe te gebruiken, link naar mcp repo
5. Definition of Done — markeer expliciet als MVP-scope; M7 is post-MVP en heeft eigen acceptatie
**Niet**:
- Geen changelog of historiek in CLAUDE.md zelf — dat hoort in `docs/decisions/agent-instructions-history.md` (separate update)
- Geen volledige herschrijving — bestaande structuur blijft (Wat is Scrum4Me, Spec-tabel, Stack, Conventies, Commit Strategy, etc.)
- Geen wijziging in `AGENTS.md` (Codex) — die heeft geen MCP, mag los blijven
- Geen wijziging in functional-spec/architecture/styling docs — die zijn al actueel
## Concrete edits in `CLAUDE.md`
### 1. Sectie "Specificatiedocumenten" — uitbreiden
Voeg toe onder de bestaande tabel:
| Document | Gebruik voor |
|---|---|
| `https://github.com/madhura68/scrum4me-mcp` | MCP-server repo: tools, prompts, schema-sync workflow |
(`docs/api/rest-contract.md` staat er al — laten staan.)
### 2. Sectie "Waar te beginnen" — herschrijven
Vervang de 7-stap manual loop door een dual-track:
**Track A — via Claude Code MCP (aanbevolen)**:
```
1. Roep `mcp__scrum4me__implement_next_story` aan met product_id
(of `list_products` als je het id niet weet)
2. De prompt orkestreert: get_claude_context → log_implementation
→ per task in_progress/done → log_test_result → log_commit
3. Bouw de tasks in volgorde van `sort_order`
4. Test: `npm run lint && npm test && npm run build`
5. Commit per laag (zie Commit Strategy)
```
**Track B — manueel (Codex of zonder MCP)**:
- Lees task in `docs/backlog/index.md`
- Volg verder de bestaande 7-stappen-loop
### 3. Sectie "Implementatiepatronen" — uitbreiden
Twee rijen toevoegen aan de patronen-tabel:
| Patroon | Bestand |
|---|---|
| Status-enum mapping (DB ↔ API) | `lib/task-status.ts` |
| Client/server module-boundary | regel: `*-server.ts` bevat DB-calls, `*.ts` is pure helpers — nooit `import { ... } from 'lib/foo-server'` in een client component |
### 4. Sectie "Conventies" — vier regels toevoegen
Voeg toe aan de bestaande lijst:
- **Entity codes**: gebruik product/PBI/story-codes in commit-titles wanneer aanwezig (`feat(ST-356.2): ...`); branchnaam blijft `feat/ST-XXX-slug`
- **Status-enums op API**: lowercase (`todo|in_progress|review|done`, `open|in_sprint|done`); DB houdt UPPER_SNAKE; conversie via `lib/task-status.ts`-mappers — nooit ad-hoc lowercase elders
- **Foutcodes API**: `400` alleen voor malformed JSON-body (parse-fout); `422` voor zod-validatie en well-formed-maar-niet-acceptabel; `403` voor demo-tokens. Documenteren in `docs/api/rest-contract.md`
- **Tests volgen contract**: bij API-contract-wijziging (status, foutcode, response-shape) MOET in dezelfde commit ook `__tests__/api/` bijgewerkt worden — een falende test op review betekent niet dat de tests "stuk zijn" maar dat de wijziging onvolledig is
### 5. Nieuwe sectie "MCP-integratie" — toevoegen vóór "Definition of Done"
Korte sectie (~15 regels):
```markdown
## MCP-integratie
Scrum4Me heeft een eigen MCP-server (repo: `madhura68/scrum4me-mcp`)
die deze API exposed als native tools voor Claude Code.
### Tools beschikbaar in Claude Code
- `mcp__scrum4me__health` — service + DB ping
- `mcp__scrum4me__list_products` — producten waar je toegang tot hebt
- `mcp__scrum4me__get_claude_context` — bundled product/sprint/story/todos
- `mcp__scrum4me__update_task_status`, `_update_task_plan`
- `mcp__scrum4me__log_implementation`, `_log_test_result`, `_log_commit`
- `mcp__scrum4me__create_todo`
### Prompt
- `implement_next_story` (arg: `product_id`) — end-to-end workflow
### Schema-drift bewaking
Wekelijks (maandag 08:00 Amsterdam) draait een remote agent
(`trig_015FFUnxjz9WMuhhWNGBQKFD`) die `vendor/scrum4me` syncet en
`prisma:generate + typecheck` uitvoert in mcp. Als die agent
een drift-rapport opent, hoort dat **vóór** een Scrum4Me-PR met
schema-wijziging gemerged kan worden — zodat de MCP-server niet
stilletjes breekt op runtime.
```
### 6. Sectie "Definition of Done" — kop verduidelijken
Wijzig `## Definition of Done``## Definition of Done (MVP)` en voeg eronder een korte zin toe: *"M7 (MCP-server) is post-MVP en heeft eigen acceptatie in `docs/backlog/index.md`."*
## Bijwerken van auditdoc
Voeg een sectie aan `docs/decisions/agent-instructions-history.md` toe (datum: 2026-04-27) met:
- Aanleiding: ST-509/511/512/513 + M7 + PR #2 review-saga
- Gecontroleerde wijzigingen: zelfde tabel-stijl als 2026-04-25
- Nieuwe regels: status-enums op API-grens, error-code split 400/422, test-pariteit bij contract-wijziging, client/server module-boundary
- Verwijzing naar mcp repo en schema-drift cron
## Volgorde van uitvoering
1. **Edits in `CLAUDE.md`** — alle 6 secties hierboven, in volgorde
2. **Edits in `docs/decisions/agent-instructions-history.md`** — nieuwe sectie 2026-04-27
3. **`npm run lint`** — sanity check
4. **Commit als één logische change**`docs(workflow): align CLAUDE.md with M7 and post-PR-#2 contract`
5. **PR openen** — review-bare scope, deploys triggeren maar zijn docs-only
## Wat het NIET oplost
- `AGENTS.md` (Codex) blijft achter; los aan te pakken indien gewenst
- Eventuele drift in `docs/specs/functional.md` rond status-enums — niet onderzocht; te volgen bij volgende audit
- Geen check of de losse pattern-files in `docs/patterns/` nog kloppen — ook volgende audit
## Geschatte size
- ~80 regels toegevoegd/gewijzigd in `CLAUDE.md`
- ~30 regels nieuw in `docs/decisions/agent-instructions-history.md`
- 1 commit, 1 PR

View file

@ -0,0 +1,111 @@
---
title: "Herbruikbaar scripts/insert-milestone.ts"
status: done
audience: [maintainer, contributor]
language: nl
last_updated: 2026-05-03
applies_to: []
---
# Plan: herbruikbaar `scripts/insert-milestone.ts`
## Doel
Eén commando dat een specifieke milestone (PBI + stories + tasks) uit de backlog leest en idempotent toevoegt aan de DB, zónder bestaande data te raken. Voor M8 nu, en voor M9..M∞ later.
## Bron-keuze: backlog ipv plan-bestand
Twee bronnen denkbaar:
- **`.Plans/<datum>-<slug>.md`** — freeform plan-tekst, niet gestructureerd, niet gecommit
- **`docs/backlog/index.md`** — al strict gestructureerd, gecommit, single source of truth voor alle bestaande seed-pipelines
Voorstel: het script leest de **backlog**. Workflow blijft natuurlijk:
1. Plan schrijven naar `.Plans/<naam>.md` (lokaal, draft)
2. Milestone-sectie + stories formaliseren in `docs/backlog/index.md` (PR)
3. Na merge: `npm run db:insert-milestone -- M8 [--product SCRUM4ME]`
Eén canonical bron, geen ambiguïteit, en de bestaande parser doet 90% van het werk al.
## Wijzigingen
### 1. `prisma/seed-data/parse-backlog.ts` — tolerant maken
Huidige parser kent alleen M0..M6 in `MILESTONE_PRIORITY/_GOAL/_SPRINT_STATUS` + asserts ≥8 milestones / ≥60 stories. M7 en M8 worden nu stilletjes overgeslagen.
Concrete edits:
- Voeg `M7` en `M8` toe aan de drie maps (M7: priority 4, sprint COMPLETED, goal "MCP-server voor Claude Code"; M8: priority 4, sprint COMPLETED, goal "Realtime updates voor Solo Paneel")
- Voor onbekende sleutels: fallback naar `priority: 4`, `sprint_status: 'COMPLETED'`, `goal: <header-title>`. Dat maakt M9..M∞ vanzelf bruikbaar zonder code-wijziging
- Verwijder de strikte filter `KNOWN_KEYS.includes(...)` of verleg naar een "alle-M[\d.]+ headers" check
- Voeg optionele `loadBacklog(repoRoot, { strict?: boolean })` toe. `strict: true` (default) behoudt de bestaande "≥8 milestones, ≥60 stories" asserts (zodat de seed niet stilletjes anders gedraagt). Insert-milestone roept met `strict: false`
### 2. `scripts/insert-milestone.ts` (nieuw, ~90 regels)
```
Usage: tsx scripts/insert-milestone.ts <milestone-key> [--product <code>] [--dry-run]
Default product code: SCRUM4ME
```
Logica:
1. Parse args; valideer dat milestone-key matcht `^M[\d.]+$`
2. `loadBacklog(repoRoot, { strict: false })`
3. Zoek milestone op `key`; faal helder met "milestone <key> not found in docs/backlog/index.md" als ie er niet in staat
4. Lookup product via `code` (default `SCRUM4ME`); faal als niet gevonden
5. Upsert PBI:
- `where: { product_id_code: { product_id, code: milestone.key } }`
- sort_order = `(max(sort_order) van bestaande PBIs in product) + 1` als nieuw, anders ongemoeid
6. Voor elke story:
- Upsert Story op `(product_id, code = story.ref)`
- status = `'DONE'` of `'OPEN'` zoals gemarkeerd in markdown
- sort_order, priority en pbi_id correct ingesteld
7. Voor elke task: bulk insert **alleen** als de story op dit moment 0 tasks heeft (idempotent — herhaling dupliceert niets)
8. Print samenvatting: `M8: PBI created, 6 stories upserted (1 created, 5 unchanged), 6 tasks created`
9. `--dry-run`: alle DB-calls overslaan, alleen wat het zou doen printen
Edge cases:
- Story-code conflict tussen producten: schema heeft `@@unique([product_id, code])` op Story dus dit is per-product safe
- Tasks zonder `code` veld in DB (klopt — code wordt afgeleid van story.code + index in get_claude_context)
- Demo-product: script accepteert `--product DEMO` o.i.d. — niet hardcoded SCRUM4ME
### 3. `package.json` script
```json
"db:insert-milestone": "tsx scripts/insert-milestone.ts"
```
### 4. Verificatie na implementatie
- Dry-run eerst: `npm run db:insert-milestone -- M8 --dry-run`
- Daarna echt: `npm run db:insert-milestone -- M8`
- In Prisma Studio of via SQL: zie M8 PBI, 6 stories, 6 tasks onder SCRUM4ME-product
- Tweede run: `npm run db:insert-milestone -- M8` → "0 created, 6 unchanged" — geen duplicaten
- Niet-bestaande key: `npm run db:insert-milestone -- M99` → "milestone M99 not found"
- Bestaande seed-flow blijft werken: `prisma db seed` met `strict: true` faalt nog steeds bij format-drift in de backlog
## Branch- en PR-strategie
`scripts/insert-milestone.ts` is orthogonaal aan ST-801. Twee keuzes:
- **A. Eigen mini-branch + PR**`tooling/insert-milestone-script`, ~95 regels code, makkelijk reviewbaar, gemerged voordat M8 verder gaat. Daarna gebruiken om M8 in DB te zetten en met de implementatie door.
- **B. Aan ST-801 plakken** — voegt scope toe aan een PR die al code ↔ infra-overschrijdend is (migratie + tools).
Voorgestelde keuze: **A**. De tool is breder bruikbaar dan M8 alleen.
## Volgorde
1. Switch naar `main` (ST-801 blijft op zijn eigen branch staan)
2. Branch `tooling/insert-milestone-script`
3. Edit `parse-backlog.ts` (M7/M8 maps + tolerant + strict-mode option)
4. Schrijf `scripts/insert-milestone.ts`
5. Voeg `db:insert-milestone` toe aan `package.json`
6. Lokaal testen met M8 (dry-run + echt + tweede run)
7. Commit, push, PR
8. Na merge: tool gebruiken om M8 in DB te krijgen, daarna ST-802 oppakken op feat/ST-801-branch
## Geschatte size
- ~10 regels parser-edit
- ~95 regels nieuw script
- ~1 regel package.json
- ~25 regels test/usage doc in script-comment
- 1 commit, 1 PR

View file

@ -0,0 +1,195 @@
---
title: "Realtime updates voor Solo Paneel (M8)"
status: done
audience: [maintainer, contributor]
language: nl
last_updated: 2026-05-03
applies_to: [M8]
---
# Plan: Realtime updates voor Solo Paneel (M8)
## Aanleiding
Wanneer Lars in zijn Solo Paneel werkt en parallel Claude Code (via MCP) of Codex aan dezelfde sprint sleutelt, ziet hij de gevolgen pas na een refresh. We willen DB-wijzigingen op `tasks`/`stories` van zijn actieve sprint live in beeld zien. Vraag van de gebruiker: "open een websocket".
## Transport-keuze — niet écht een WebSocket
Vercel-deploys ondersteunen geen stateful native WebSockets in serverless of Edge functions. Drie reële opties:
| Optie | Werkt op Vercel | Externe dienst | Latency | Complexiteit |
|---|---|---|---|---|
| **A. SSE + Postgres LISTEN/NOTIFY** | ✅ (Node runtime, streaming response) | nee | <100ms na DB-write | gemiddeld |
| B. SSE + polling 23s | ✅ | nee | 13s | laag |
| C. Pusher/Ably (echte WS) | ✅ | ja (gratis tier) | <50ms | laag, maar elke schrijver moet publishen |
**Voorgestelde keuze: A — SSE met Postgres LISTEN/NOTIFY.**
Reden:
- Eén bron van waarheid: de DB. Web-mutations, REST-API én MCP schrijven allemaal naar Postgres; een trigger NOTIFY't onafhankelijk van de schrijver. Geen coördinatie nodig met mcp.
- Geen externe dienst, geen extra dep, geen kosten erbij.
- Neon ondersteunt LISTEN/NOTIFY op directe verbindingen. `DIRECT_URL` is al geconfigureerd.
- Naar de client toe: éénrichtingsverkeer — server pusht events, client doet mutaties via bestaande Server Actions/REST. SSE volstaat dus; we hoeven geen full-duplex.
- Voor de gebruiker is het verschil onmerkbaar: realtime updates komen binnen, browsers ondersteunen `EventSource` native.
We kiezen B (polling) niet omdat het meer DB-load geeft en je Pusher-achtige latency niet haalt. We kiezen C niet vanwege coördinatieoverhead met de MCP-server (extra publish-step in mcp).
## Architectuur
```
┌─────────────────────────────────────────────────────────────────────────┐
│ Postgres (Neon) │
│ ┌────────────────────────┐ │
│ │ TRIGGER on tasks │──► pg_notify('scrum4me_solo', payload_json) │
│ │ TRIGGER on stories │ │
│ └────────────────────────┘ │
└──────────────┬──────────────────────────────────────────────────────────┘
│ LISTEN scrum4me_solo
┌─────────────────────────────────────────────────────────────────────────┐
│ Next.js Node.js runtime route: /api/realtime/solo │
│ - auth via iron-session cookie │
│ - opent dedicated pg client (DIRECT_URL), LISTEN scrum4me_solo │
│ - filtert events: alleen tasks/stories in actieve sprint van een │
│ product waar user lid/eigenaar is, EN (assignee_id == user OR │
│ onbeklemtoonde unassigned-story-list) │
│ - stuurt SSE: data: {type, entity, id, fields} \n\n │
│ - heartbeat \n\n elke 25s │
│ - sluit zelf na 4 min (Vercel maxDuration safety); client reconnect │
└──────────────┬──────────────────────────────────────────────────────────┘
│ EventSource('/api/realtime/solo?product_id=...')
┌─────────────────────────────────────────────────────────────────────────┐
│ Browser — Solo Paneel │
│ - useSoloRealtime(productId) hook │
│ - reconnect met exponential backoff (max 30s) │
│ - Page Visibility API: close on hidden, reopen on visible │
│ - dispatcht naar solo-store: applyTaskUpdate, applyTaskCreate, │
│ applyTaskDelete, applyStoryUpdate (assignee/title/status) │
│ - reconcile-policy: skip update als optimistic in-flight is voor die │
│ task; anders server wint │
└─────────────────────────────────────────────────────────────────────────┘
```
## Filtering — wie krijgt welke events?
De trigger NOTIFY't elke task/story-mutatie globaal. De SSE-handler is verantwoordelijk voor toegangs- en relevantie-filtering:
1. **Toegang**: alleen events waarvan de gerelateerde `story.product_id` in `productAccessFilter(userId)` zit.
2. **Sprint-scope**: alleen events binnen de actieve sprint van het product dat in de query-parameter zit.
3. **Persoonlijke relevantie**: tasks waar `story.assignee_id == userId` (jouw kolommen), plus stories met `assignee_id == null` (de "claim me" lijst).
Per event extra DB-roundtrip om dit te checken zou duur zijn. Twee oplossingen, bij voorkeur (b):
(a) Triggerpayload bevat `product_id`, `sprint_id`, `assignee_id` zodat de handler in-memory kan filteren — geen extra DB-call.
(b) Cache in handler: bij connect resolveert de handler `userId → activeSprintId, productId, assignedStoryIds`. Bij elke notify checkt het de payload tegen die set; bij story-create/assignee-change herwoordt het de set on demand.
Strategie: combineer (a) trigger zet `product_id` en `assignee_id` in de payload + (b) handler cacht `(activeSprintId, productId, accessibleProducts)` voor de connectie-duur.
## Concrete implementatie — stories
### ST-801 Postgres LISTEN/NOTIFY-infrastructuur
- Migration `prisma/migrations/<ts>_add_solo_realtime_triggers/migration.sql`:
- `CREATE OR REPLACE FUNCTION notify_solo_change() RETURNS TRIGGER ...` — bouwt JSON met `op` (`INSERT`/`UPDATE`/`DELETE`), `entity` (`task`/`story`), `id`, `product_id`, `sprint_id`, `assignee_id`, `fields` (alleen gewijzigde kolommen bij UPDATE)
- Triggers `AFTER INSERT OR UPDATE OR DELETE ON tasks`, idem op `stories`
- Pas `prisma migrate deploy` toe (idempotent, geen schema-wijziging dus geen TS-impact)
- Done when: `psql $DIRECT_URL -c "LISTEN scrum4me_solo;"` toont een payload bij UI-mutatie
### ST-802 SSE-route `/api/realtime/solo`
- Bestand `app/api/realtime/solo/route.ts`, `runtime: 'nodejs'`, `maxDuration: 300`
- Gebruikt `pg.Client` (niet de Prisma adapter — directe `LISTEN`-verbinding)
- Auth via iron-session, 401 zonder cookie
- Query-parameter `product_id`, 403 zonder access
- Resolveert active sprint id eenmalig; cachet die in connection-scope
- `ReadableStream` met heartbeat-interval 25s, hard close na 240s
- Filter per event op `product_id == requested && (assignee_id == userId || (entity == 'story' && assignee_id == null))`
- Logged via `console.error` bij pg-disconnect
- Done when: handmatig met `curl -N` op localhost krijg je events binnen 1s na een UI-mutatie
### ST-803 Client hook `useSoloRealtime(productId)`
- `lib/realtime/use-solo-realtime.ts` (client-only)
- Opent `EventSource('/api/realtime/solo?product_id=' + productId)`
- Reconnect: exponential backoff start 1s → 30s, reset op succesvolle connect
- Page Visibility: `document.visibilityState === 'hidden'` → close; bij visible → reopen
- Cleanup op unmount
- Dispatcht events naar solo-store via nieuwe acties (zie ST-804)
- Done when: tab wisselen sluit/opent connectie zichtbaar in DevTools Network
### ST-804 Solo-store realtime-acties
- Uitbreiden `stores/solo-store.ts`:
- `applyTaskUpdate(taskId, fields)` — merge in tasks-record; skip als `pendingOps[taskId]` set is
- `applyTaskCreate(task)` — alleen als de task in de eigen kolommen hoort (assignee_id == userId)
- `applyTaskDelete(taskId)`
- `applyStoryAssignment(storyId, assigneeId)` — re-fetch unassigned-list (kleine GET) of ontvang als deel van payload
- `markPending(taskId)`/`clearPending(taskId)` — optimistic-flow markeert mutaties die we zelf doen, zodat we de echo van onze eigen NOTIFY niet dubbel verwerken
- Done when: unit-test op solo-store met simulated events laat juiste state zien
### ST-805 Wire-up in SoloBoard
- `components/solo/solo-board.tsx`: roep `useSoloRealtime(productId)` aan na `useEffect`-init van tasks
- Klein "live" / "verbinden..." status-indicator (status uit hook): groene stip / pulserende grijze stip
- Toast bij langer dan 5s disconnected
- Done when: open Solo paneel in twee tabs, mutate task in tab A, zie status flippen in tab B binnen 12s zonder refresh
### ST-806 Documentatie + acceptatietest
- Update `docs/architecture.md`: nieuwe sectie "Realtime updates" met diagram en filtering-regels
- Update `CLAUDE.md`: vermelding dat Solo Paneel realtime is + dat MCP-writes vanzelf doorkomen
- Update `docs/api/rest-contract.md`: korte note over `/api/realtime/solo` (Bearer auth, SSE format)
- E2E-acceptatie: lijst van scenario's (zelfde gebruiker twee tabs, MCP-write, REST-write, story-claim, network-flap) handmatig getest
- Done when: scenario's lopen door zonder onverwachte gedragingen
## Backlog-edits
In `docs/backlog/index.md`:
1. **Milestone-overzicht** — rij toevoegen onder M7:
```
| M8: Realtime Solo Paneel | Live updates voor stories/tasks via SSE+LISTEN/NOTIFY | ST-801 ST-806 |
```
2. **Sectie M8** toevoegen na de M7-sectie, met de zes stories hierboven (ST-801..ST-806) inclusief "Done when"-criteria. Allemaal `[ ]` (nog niet gestart).
## Wijzigingen elders
- `.env.example` blijft ongewijzigd (DIRECT_URL stond er al)
- `docs/architecture.md` — sectie "Realtime updates" met diagram en regel "alle UPDATE-triggers zitten op tasks/stories; nieuwe entiteiten erbij vragen om uitbreiding van de trigger-functie"
- Geen wijziging in `lib/code.ts` of `lib/code-server.ts` — dit is server-only realtime
- Schema-drift agent in mcp pikt de migratie automatisch op (geen Prisma-modelwijziging maar wel een nieuwe migratie); typecheck blijft groen omdat we geen Prisma Client-wijziging hebben
## Risico's en mitigaties
| Risico | Mitigatie |
|---|---|
| Vercel sluit Node-route na maxDuration | Hard-close server-side bij 240s + automatische client-reconnect; gebruiker merkt dit niet |
| Echo van eigen optimistic mutation | `markPending`/`clearPending` in solo-store; skip als `pendingOps[taskId]` set is |
| Connection leaks (open `pg.Client`'s) | `req.signal.addEventListener('abort')` cleanup; bij Edge cold-start sluit Vercel zelf |
| Trigger overhead op writes | Triggers zijn lichtgewicht (één pg_notify call); meet bij rollout |
| Oude pg_notify payloads >8kb | Zorg dat we alleen primitives (id, status, sort_order, etc.) sturen — geen description/implementation_plan in de payload, daar is een refetch voor |
| Test-DB heeft geen triggers | Migratie automatisch toegepast in CI (Prisma migrate deploy); bestaande tests blijven groen |
| MCP-server schema-sync detecteert migratie als drift | False alarm — wekelijkse cron rapporteert "schema-prisma diff", maar typecheck blijft groen omdat het alleen migratie-SQL is. Beoordeel handmatig bij rapport |
## Wat dit NIET oplost
- Realtime in Sprint Backlog of Product Backlog — alleen Solo Paneel
- Conflict-merge bij gelijktijdige updates van twee gebruikers (last-write-wins blijft)
- Mobile pagina (out of scope desktop-first MVP)
- Audit-trail van wie wat wanneer veranderde (bestaat al via StoryLog)
## Volgorde van uitvoering
1. Branch `feat/m8-realtime-solo` van main
2. ST-801 (migratie + trigger) — commit, lokaal verifiëren met `psql LISTEN`
3. ST-802 (SSE-route) — commit, `curl -N` lokaal testen tegen lokale UI-mutatie
4. ST-803 (client hook) — commit
5. ST-804 (store-uitbreiding) — commit, met unit-test
6. ST-805 (wire-up + UI-indicator) — commit
7. ST-806 (docs + acceptatie) — commit
8. PR openen — Vercel preview-deploy laat realtime werken op preview-DB (mits trigger via `migrate deploy` mee)
9. Na review: merge
## Geschatte size
- ~6 stories, ~1218 commits
- 1 migratie, 1 nieuwe route, 1 nieuwe hook, kleine store-uitbreiding, UI-indicator
- ~400 regels code + ~80 regels docs
- 1 PR

View file

@ -0,0 +1,371 @@
# Advies — Zelf een Git-platform hosten naast of in plaats van GitHub
## Situatie
Je wilt onderzoeken of je lokaal of op een eigen server een Git repository-platform kunt draaien vergelijkbaar met GitHub.
In jouw situatie spelen mee:
- Next.js/Vercel apps
- AI-workers / automation
- batch processing
- deploy pipelines
- private code
- mogelijk draaien op NAS of VPS
- integratie met Claude Code / Codex / agents
Het antwoord is: ja, dit kan uitstekend.
---
# Architectuur-opties
## Optie 1 — Alleen een centrale Git remote
De lichtste oplossing.
Je draait alleen een zogenaamde "bare repo" op een Linux server.
### Voordelen
- extreem simpel
- weinig resources
- volledige controle
- SSH push/pull
### Nadelen
- geen webinterface
- geen PRs
- geen issues
- geen gebruikersbeheer
- geen CI/CD UI
### Setup
Server:
```bash
mkdir -p /srv/git/myapp.git
cd /srv/git/myapp.git
git init --bare
```
Client:
```bash
git remote add origin ssh://user@server:/srv/git/myapp.git
git push -u origin main
```
---
# Optie 2 — Self-hosted GitHub alternatief
Dit is meestal de beste keuze.
Software opties:
| Software | Omschrijving |
|---|---|
| Gitea | Lichtgewicht GitHub alternatief |
| Forgejo | Community fork van Gitea |
| GitLab | Zeer compleet maar zwaar |
| OneDev | Moderne alles-in-één oplossing |
---
# Aanbevolen keuze: Gitea
## Waarom
Voor jouw situatie is Gitea waarschijnlijk de beste balans tussen:
- eenvoud
- performance
- features
- beheerlast
Je krijgt:
- Git hosting
- web UI
- pull requests
- issues
- SSH support
- webhooks
- CI integratie
- Docker support
- private repos
- multi-user support
---
# Aanbevolen architectuur voor jouw setup
## Huidige richting
```text
MacBook
GitHub
Vercel deploy
```
## Uitgebreide AI workflow
```text
MacBook
Gitea / GitHub
↓ webhook
AI Worker Server
Repo clone
Code generatie
Commit + push
PR creation
Merge
Vercel deploy
```
---
# Beste strategie voor jouw situatie
## Advies: hybride model
Gebruik:
| Component | Platform |
|---|---|
| publieke repos | GitHub |
| deploys | Vercel |
| AI worker orchestration | eigen server |
| interne experimenten | Gitea |
| automation | self-hosted |
Waarom:
- GitHub ecosystem blijft beschikbaar
- recruiters herkennen GitHub
- Copilot integratie blijft optimaal
- minder beheer
- sneller stabiel
---
# Wanneer volledig self-hosted interessant wordt
Volledig self-hosted wordt interessant als:
- privacy belangrijk is
- AI agents autonoom moeten kunnen werken
- je volledige controle wilt
- je GitHub limieten wilt vermijden
- je meerdere workers wilt draaien
Dan bouw je:
```text
Gitea
+ Postgres
+ Docker Registry
+ CI Runners
+ Reverse Proxy
+ Backups
+ Monitoring
```
---
# Aanbevolen infrastructuur
## Lichtgewicht setup
### Hardware
- Synology NAS of mini-PC
- 816 GB RAM
- SSD opslag
### Software stack
| Component | Advies |
|---|---|
| OS | Ubuntu Server |
| Containers | Docker Compose |
| Git platform | Gitea |
| Reverse proxy | Traefik |
| Database | Postgres |
| SSL | Let's Encrypt |
| Deploys | Vercel |
---
# Docker Compose voorbeeld
```yaml
services:
gitea:
image: gitea/gitea:latest
container_name: gitea
ports:
- "3000:3000"
- "222:22"
volumes:
- ./gitea:/data
restart: always
```
Starten:
```bash
docker compose up -d
```
Daarna bereikbaar via:
```text
http://server-ip:3000
```
---
# Belangrijke aandachtspunten
## Backups
Bij self-hosting moet je zelf regelen:
- database backups
- repo backups
- disaster recovery
---
## Security
Je bent zelf verantwoordelijk voor:
- updates
- SSH security
- firewall
- SSL certificaten
- gebruikersbeheer
---
## CI/CD
GitHub Actions vervang je mogelijk door:
- Gitea Actions
- Drone CI
- Woodpecker CI
- self-hosted runners
---
# Integratie met jouw AI-worker ideeën
Dit sluit zeer goed aan op jouw eerdere ideeën:
- Neon database events
- worker servers
- auto-generated PRs
- selective deploys
- batch execution
Je kunt bijvoorbeeld:
1. story wordt aangemaakt
2. worker krijgt event via SSE/webhook
3. repo wordt gecloned
4. AI implementeert wijziging
5. commit + push
6. PR automatisch aangemaakt
7. review pipeline start
8. merge → deploy
Dit wordt veel eenvoudiger wanneer je volledige controle hebt over de Git infrastructuur.
---
# Concrete roadmap
## Fase 1 — huidige setup stabiliseren
Hou:
- GitHub
- Vercel
- Neon
Voeg toe:
- AI worker server
- webhooks
- automation pipeline
---
## Fase 2 — interne Git infrastructuur
Installeer:
- Gitea
- Docker
- Postgres
Gebruik dit voor:
- experimenten
- AI-generated branches
- interne repos
- automation testing
---
## Fase 3 — geavanceerde automation
Later toevoegen:
- self-hosted runners
- preview environments
- deploy approvals
- selective deployments
- agent orchestration
---
# Eindadvies
Voor jouw situatie:
## Niet meteen GitHub vervangen
Dat levert nu vooral extra beheerlast op.
## Wel nu al beginnen met:
- eigen AI worker server
- webhook automation
- lokale Git orchestration
- Gitea testomgeving
Dat sluit perfect aan op:
- Scrum4Me
- AI-assisted development
- batch story execution
- autonome pipelines

View file

@ -0,0 +1,894 @@
---
title: "M10 — Password-loze inlog via QR-pairing"
status: active
audience: [maintainer, contributor]
language: nl
last_updated: 2026-05-03
applies_to: [M10]
---
# M10 — Password-loze inlog via QR-pairing
Inloggen op een (publieke) desktop zonder wachtwoord: desktop toont QR, telefoon (al-ingelogd) scant en bevestigt expliciet, desktop is binnen 12 s ingelogd. Bouwt voort op M8 LISTEN/NOTIFY-infra met eigen channel `scrum4me_pairing`.
**Beveiligingsuitgangspunt:** geheim materiaal nooit in URL-paden, querystrings, access logs of browsergeschiedenis.
- `mobileSecret` reist alleen via QR-fragment (`#s=…`) → mobile `location.hash` → POST-body
- `desktopToken` reist alleen via HttpOnly cookie `s4m_pair` met `Path=/api/auth/pair`, `Max-Age=120`, `SameSite=Lax`
- Twee gescheiden hashes in DB scheiden mobiel-bewijs (`secret_hash`) van desktop-bewijs (`desktop_token_hash`)
Backlog-entries: zie [backlog.md § M10](../backlog/index.md#m10-password-loze-inlog-via-qr-pairing).
Functional spec: zie [functional.md § F-01b](../specs/functional.md#f-01b-inloggen-via-mobiel-qr-pairing).
**Implementatie-volgorde** (commit-strategy uit CLAUDE.md):
1. **DB-laag** — ST-1001 (schema + trigger)
2. **Auth helpers + sessie-uitbreiding** — ST-1002
3. **API-laag** — ST-1003 (start), ST-1004 (SSE), ST-1006 (claim)
4. **Server actions + mobile UI** — ST-1005
5. **Desktop UI** — ST-1007
6. **Documentatie + acceptatietest** — ST-1008
ST-1006 staat bij de API-laag (niet bij UI) omdat het een Route Handler is; ST-1005 levert tegelijk de server actions en de mobiele bevestigingspagina omdat die strak gekoppeld zijn.
---
## ST-1001 — LoginPairing schema + Postgres-trigger
**Bestanden**
- `prisma/schema.prisma` — nieuw model `LoginPairing` + back-relation op `User`
- `prisma/migrations/<timestamp>_add_login_pairing/migration.sql` — model + trigger
- `vendor/scrum4me`-submodule in repo `mcp` — schema-sync ná merge
**Stappen**
1. **Schema-uitbreiding**:
```prisma
model LoginPairing {
id String @id @default(cuid())
secret_hash String // sha256 hex van mobileSecret
desktop_token_hash String // sha256 hex van desktopToken (HttpOnly cookie)
status String // 'pending' | 'approved' | 'consumed' | 'cancelled'
user_id String?
user User? @relation(fields: [user_id], references: [id], onDelete: SetNull)
desktop_ua String? @db.VarChar(255)
desktop_ip String? @db.VarChar(45) // IPv6 max
created_at DateTime @default(now())
expires_at DateTime
approved_at DateTime?
consumed_at DateTime?
@@index([expires_at])
@@index([status, expires_at])
@@map("login_pairings")
}
```
Op `User`: `login_pairings LoginPairing[]` toevoegen.
2. **Migratie-SQL** voegt naast de tabel ook trigger toe (mirror van `notify_solo_change` in `prisma/migrations/20260426230316_add_solo_realtime_triggers/migration.sql`):
```sql
CREATE OR REPLACE FUNCTION notify_pairing_change() RETURNS trigger AS $$
DECLARE payload jsonb;
BEGIN
payload := jsonb_build_object(
'op', CASE TG_OP WHEN 'INSERT' THEN 'I' WHEN 'UPDATE' THEN 'U' ELSE 'D' END,
'pairing_id', COALESCE(NEW.id, OLD.id),
'status', COALESCE(NEW.status, OLD.status)
);
PERFORM pg_notify('scrum4me_pairing', payload::text);
RETURN COALESCE(NEW, OLD);
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER login_pairings_notify
AFTER INSERT OR UPDATE ON login_pairings
FOR EACH ROW EXECUTE FUNCTION notify_pairing_change();
```
3. `npx prisma migrate dev --name add_login_pairing`.
**Aandachtspunten**
- `desktop_ip` houdt op 45 tekens om IPv6 te accommoderen (`xxxx:xxxx:…:255.255.255.255`).
- Geen index op `user_id` nodig voor v1 — er is geen lookup-pad "geef alle pairings van user X" (komt pas bij remote-revoke in M+1).
- Trigger emit ook bij DELETE niet nodig — pairings worden niet gedelete'd, ze gaan naar `consumed`/`cancelled`.
- `vendor/scrum4me`-submodule in mcp moet ná merge op `main` direct gesynced worden, anders breekt de wekelijkse drift-check (`trig_015FFUnxjz9WMuhhWNGBQKFD`). Dit was ook een aandachtspunt bij ST-901.
**Verificatie**
- `npx prisma migrate dev` slaagt
- `npx prisma validate` zonder fouten
- `psql $DIRECT_URL -c "LISTEN scrum4me_pairing;"` toont payload bij `INSERT INTO login_pairings(...) VALUES(...)`
- `prisma studio` toont tabel met beide hash-kolommen `NOT NULL`
---
## ST-1002 — Pairing-helpers + sessie-uitbreiding + pre-auth-cookie
**Bestanden**
- `lib/auth/pairing.ts` — nieuw, secret/token-generatie en hash-helpers
- `lib/auth/pair-cookie.ts` — nieuw, set/read/clear van `s4m_pair`-cookie
- `lib/session.ts``SessionData` uitbreiden met `paired` en `pairedExpiresAt`
- `app/(app)/layout.tsx` — extra guard op vervallen paired-sessie
- `__tests__/lib/auth/pairing.test.ts` — nieuw
**Stappen**
1. **`lib/auth/pairing.ts`**:
```ts
import { createHash, randomBytes, timingSafeEqual } from 'crypto'
export function generateMobileSecret(): string {
return randomBytes(32).toString('base64url')
}
export function generateDesktopToken(): string {
return randomBytes(32).toString('base64url')
}
export function hashToken(token: string): string {
return createHash('sha256').update(token).digest('hex')
}
export function verifyToken(token: string, hash: string): boolean {
const a = Buffer.from(hashToken(token), 'hex')
const b = Buffer.from(hash, 'hex')
if (a.length !== b.length) return false
return timingSafeEqual(a, b)
}
```
Twee aparte generators (niet één functie met arg) voorkomen dat dezelfde geheim per ongeluk twee keer wordt gebruikt.
2. **`lib/auth/pair-cookie.ts`**:
```ts
import { cookies } from 'next/headers'
const COOKIE_NAME = 's4m_pair'
const MAX_AGE = 120 // 2 min, gelijk aan pending-TTL van pairing
export async function setPairCookie(desktopToken: string) {
const jar = await cookies()
jar.set(COOKIE_NAME, desktopToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
path: '/api/auth/pair',
maxAge: MAX_AGE,
})
}
export async function readPairCookie(): Promise<string | null> {
const jar = await cookies()
return jar.get(COOKIE_NAME)?.value ?? null
}
export async function clearPairCookie() {
const jar = await cookies()
jar.delete({ name: COOKIE_NAME, path: '/api/auth/pair' })
}
```
`Path=/api/auth/pair` zorgt dat de cookie alleen naar pair-endpoints wordt gestuurd — niet naar elke route.
3. **`lib/session.ts`** — `SessionData` interface:
```ts
export interface SessionData {
userId: string
isDemo: boolean
paired?: boolean // true als sessie is aangemaakt via QR-pairing
pairedExpiresAt?: number // unix ms
}
```
Bestaande sessies blijven werken — beide velden zijn optioneel.
4. **`app/(app)/layout.tsx`** — guard ná de bestaande `if (!session.userId) redirect('/login')`:
```ts
if (session.paired && session.pairedExpiresAt && session.pairedExpiresAt < Date.now()) {
session.destroy()
redirect('/login?notice=paired-expired')
}
```
Hergebruikt het bestaande `notice`-querystring-patroon van M9's `<NoticeToast />` voor de melding "Je sessie is verlopen, log opnieuw in".
5. **Tests**`__tests__/lib/auth/pairing.test.ts`:
- `generateMobileSecret()` produceert 43-karakter base64url (32 bytes)
- `hashToken` is deterministisch
- `verifyToken` is true voor geldig paar, false voor ongeldig
- Twee verschillende `generateMobileSecret()`-calls geven verschillende waardes
- Cookie helpers: HttpOnly bit gezet (via Next.js cookie-store mock)
**Aandachtspunten**
- Geen middleware nodig voor de paired-expiry-check; layout-guard is voldoende. Middleware komt pas in beeld als `proxy.ts` herzien wordt (uit scope hier).
- `cookies().delete({ name, path })` moet **dezelfde path** specificeren als bij set, anders blijft de cookie staan.
- `crypto.randomBytes` is sync en blocking — voor 32 bytes ruim < 1ms; geen async-variant nodig.
**Verificatie**
- `npm run lint && npx tsc --noEmit && npm test && npm run build` groen
- Handmatig: in DevTools Application-tab is de cookie zichtbaar als HttpOnly + Path scoped
- `document.cookie` op de pagina laat de cookie *niet* zien
---
## ST-1003 — `POST /api/auth/pair/start` (anon, sets pre-auth cookie)
**Bestanden**
- `app/api/auth/pair/start/route.ts` — nieuw
- `lib/rate-limit.ts` — checken of bestaand (uit ST-608); anders helper toevoegen
- `__tests__/api/pair-start.test.ts` — nieuw
**Stappen**
1. Route Handler (vrij van `authenticateApiRequest` — dit is anon):
```ts
import { NextRequest } from 'next/server'
import { prisma } from '@/lib/prisma'
import {
generateMobileSecret, generateDesktopToken, hashToken,
} from '@/lib/auth/pairing'
import { setPairCookie } from '@/lib/auth/pair-cookie'
import { rateLimit } from '@/lib/rate-limit'
export const runtime = 'nodejs'
const PENDING_TTL_MS = 2 * 60 * 1000
export async function POST(request: NextRequest) {
const ip = request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() ?? null
const ua = request.headers.get('user-agent')?.slice(0, 255) ?? null
// Rate-limit per IP — 10/min (zelfde patroon als ST-608)
const limited = await rateLimit(`pair-start:${ip ?? 'anon'}`, 10, 60_000)
if (limited) {
return Response.json({ error: 'Te veel verzoeken' }, { status: 429 })
}
const mobileSecret = generateMobileSecret()
const desktopToken = generateDesktopToken()
const pairing = await prisma.loginPairing.create({
data: {
secret_hash: hashToken(mobileSecret),
desktop_token_hash: hashToken(desktopToken),
status: 'pending',
desktop_ua: ua,
desktop_ip: ip,
expires_at: new Date(Date.now() + PENDING_TTL_MS),
},
select: { id: true, expires_at: true },
})
await setPairCookie(desktopToken)
const origin = request.nextUrl.origin
const qrUrl = `${origin}/m/pair#id=${pairing.id}&s=${mobileSecret}`
return Response.json({
pairingId: pairing.id,
mobileSecret,
expiresAt: pairing.expires_at.toISOString(),
qrUrl,
})
}
```
2. **Rate-limit helper** — als `lib/rate-limit.ts` nog niet bestaat:
- In-memory Map keyed op `key`, met sliding-window van timestamps; thread-safe genoeg voor v1 single-instance Vercel Functions.
- Wordt ook door `actions/auth.ts` (login) gebruikt; verifieer met grep of die al bestaat — zo ja, hergebruik exact.
3. **Tests**`__tests__/api/pair-start.test.ts`:
- 200 response bevat `pairingId`, `mobileSecret`, `qrUrl` met fragment-syntax (`#id=…&s=…`)
- `Set-Cookie`-header bevat `s4m_pair=...; HttpOnly; SameSite=Lax; Path=/api/auth/pair; Max-Age=120`
- Database-rij heeft `secret_hash`, `desktop_token_hash`, geen plaintext
- 11e call binnen 60s → 429
- `desktop_ua` en `desktop_ip` worden opgeslagen bij aanwezigheid, anders `null`
**Aandachtspunten**
- `mobileSecret` mag in de JSON-response — die is HTTPS-encrypted en wordt niet door browsers naar logs geschreven. Kritisch is dat het niet in **request**-URLs of **server**-toegangslogs belandt.
- `qrUrl` gebruikt `#` (fragment), niet `?`. Browsers strippen het fragment voordat ze de mobile pair-page ophalen — maar dat is uitvoerig getest in ST-1005 server-side: de page leest de URL niet, alleen de client component leest `window.location.hash`.
- Geen idempotency-key nodig: een tweede start vanuit dezelfde tab maakt simpelweg een nieuwe pairing en cookie aan; oude cookie wordt overschreven.
- Vercel-edge-of-fluid: `runtime: 'nodejs'` expliciet (niet 'edge') want we gebruiken `crypto.randomBytes`.
**Verificatie**
- `curl -i -X POST http://localhost:3000/api/auth/pair/start --cookie-jar /tmp/jar` retourneert JSON + `Set-Cookie`
- Body bevat `qrUrl` met `#id=…&s=…`
- Rij in `login_pairings` heeft beide hashes ingevuld
- Tests groen
---
## ST-1004 — SSE-route `/api/auth/pair/stream/[pairingId]` (cookie-auth)
**Bestanden**
- `app/api/auth/pair/stream/[pairingId]/route.ts` — nieuw
- `__tests__/api/pair-stream.test.ts` — nieuw (auth-test, geen full SSE-test)
**Stappen**
1. **Routebestand** — exacte structuur uit `app/api/realtime/solo/route.ts` (incl. heartbeat, hard-close, abort-handler), maar:
- Geen iron-session check; auth via `s4m_pair`-cookie:
```ts
const desktopToken = await readPairCookie()
if (!desktopToken) return Response.json({ error: 'Geen pairing-cookie' }, { status: 401 })
const pairing = await prisma.loginPairing.findUnique({
where: { id: pairingId },
select: { desktop_token_hash: true, status: true, expires_at: true },
})
if (!pairing) return Response.json({ error: 'Pairing niet gevonden' }, { status: 404 })
if (pairing.expires_at < new Date()) return Response.json({ error: 'Pairing verlopen' }, { status: 410 })
if (!verifyToken(desktopToken, pairing.desktop_token_hash)) {
return Response.json({ error: 'Ongeldige cookie' }, { status: 401 })
}
```
- Channel = `'scrum4me_pairing'`
- Filter `shouldEmit`: `payload.pairing_id === pairingId`
- Auto-close ook bij payload `status``{'consumed', 'cancelled'}` — niet alleen na 240 s
- Geen sprint-resolve, geen userId-filter — eenvoudiger dan solo-route
2. **Initial-state-event** vlak na verbinden: query de pairing-status één keer uit DB en stuur als `event: state\ndata: {"status":"pending"}` zodat de desktop niet hoeft te wachten op het eerste pg_notify (handig als pairing al `approved` is voordat de SSE opent — race-conditie).
3. **Tests**:
- GET zonder cookie → 401
- GET met cookie maar onbekende `pairingId` → 404
- GET met verlopen pairing → 410
- GET met cookie die hasht naar een andere `desktop_token_hash` → 401
Geen full-stream-test — vereist een Postgres-event-mock die het niet waard is voor v1. Manuele test dekt dit (zie verificatie).
**Aandachtspunten**
- `pairingId` in het URL-pad is OK — niet vertrouwelijk. De cookie is het bewijs.
- `EventSource` op de client kan geen custom headers; cookie is daarom de enige praktische auth-methode. `withCredentials: true` op de client is verplicht.
- Bij browser-tab-sluiten wordt `request.signal.abort` getriggered → cleanup zoals in solo-route.
- Vermijd Prisma in de notification-handler; gebruik alleen `pg.Client` zoals solo-route. Status-check hierboven is de enige Prisma-call (vóór de stream start).
**Verificatie**
- `curl -N --cookie /tmp/jar http://localhost:3000/api/auth/pair/stream/<id>` blijft open en print `: heartbeat` elke 25 s
- Andere terminal: `psql $DIRECT_URL -c "UPDATE login_pairings SET status='approved' WHERE id='<id>'"` → curl-uitvoer toont event binnen 1 s
- Manuele 401-test: `curl -N` zonder cookie → JSON 401
---
## ST-1005 — Server actions + mobiele bevestigingspagina
**Bestanden**
- `actions/pairing.ts` — nieuw, drie Server Actions
- `app/(app)/m/pair/page.tsx` — nieuw, Server Component
- `app/(app)/m/pair/pair-confirmation.tsx` — nieuw, Client Component
- `__tests__/actions/pairing.test.ts` — nieuw
**Stappen**
1. **`actions/pairing.ts`** — volgt `docs/patterns/server-action.md`:
```ts
'use server'
import { revalidatePath } from 'next/cache'
import { z } from 'zod'
import { prisma } from '@/lib/prisma'
import { getSession } from '@/lib/auth'
import { verifyToken } from '@/lib/auth/pairing'
const inputSchema = z.object({
pairingId: z.string().cuid(),
mobileSecret: z.string().min(40), // base64url van 32 bytes ≈ 43 chars
})
export async function getPairingForApproval(pairingId: string, mobileSecret: string) {
const session = await getSession()
if (!session.userId) return { ok: false, error: 'Niet ingelogd' } as const
const parsed = inputSchema.safeParse({ pairingId, mobileSecret })
if (!parsed.success) return { ok: false, error: 'Ongeldige invoer' } as const
const pairing = await prisma.loginPairing.findUnique({
where: { id: pairingId },
select: {
status: true, expires_at: true, secret_hash: true,
desktop_ua: true, desktop_ip: true,
},
})
if (!pairing) return { ok: false, error: 'Pairing niet gevonden' } as const
if (pairing.expires_at < new Date()) return { ok: false, error: 'Pairing verlopen' } as const
if (pairing.status !== 'pending') return { ok: false, error: 'Pairing al afgehandeld' } as const
if (!verifyToken(mobileSecret, pairing.secret_hash)) {
return { ok: false, error: 'Ongeldig pairing-geheim' } as const
}
const me = await prisma.user.findUnique({
where: { id: session.userId },
select: { username: true },
})
return {
ok: true,
desktop_ua: pairing.desktop_ua,
desktop_ip: pairing.desktop_ip,
username: me?.username ?? '',
} as const
}
export async function approvePairing(pairingId: string, mobileSecret: string) {
const session = await getSession()
if (!session.userId) return { ok: false, error: 'Niet ingelogd' } as const
if (session.isDemo) return { ok: false, error: 'Niet beschikbaar in demo-modus' } as const
const parsed = inputSchema.safeParse({ pairingId, mobileSecret })
if (!parsed.success) return { ok: false, error: 'Ongeldige invoer' } as const
const pairing = await prisma.loginPairing.findUnique({
where: { id: pairingId },
select: { status: true, expires_at: true, secret_hash: true },
})
if (!pairing) return { ok: false, error: 'Pairing niet gevonden' } as const
if (pairing.expires_at < new Date()) return { ok: false, error: 'Pairing verlopen' } as const
if (pairing.status !== 'pending') return { ok: false, error: 'Pairing al afgehandeld' } as const
if (!verifyToken(mobileSecret, pairing.secret_hash)) {
return { ok: false, error: 'Ongeldig pairing-geheim' } as const
}
const APPROVED_TTL_MS = 5 * 60 * 1000
await prisma.loginPairing.update({
where: { id: pairingId },
data: {
status: 'approved',
user_id: session.userId,
approved_at: new Date(),
expires_at: new Date(Date.now() + APPROVED_TTL_MS),
},
})
// Trigger emit pg_notify automatisch — geen revalidatePath nodig
return { ok: true } as const
}
export async function cancelPairing(pairingId: string, mobileSecret: string) {
// Vergelijkbaar met approvePairing maar status='cancelled', geen user_id; demo mag annuleren.
}
```
2. **`app/(app)/m/pair/page.tsx`** — Server Component, achter bestaande `(app)/layout.tsx` auth-guard:
```tsx
import { PairConfirmation } from './pair-confirmation'
export default function PairPage() {
return (
<main className="container mx-auto max-w-md py-12">
<h1 className="text-h2">Inloggen op desktop</h1>
<p className="text-muted-foreground mt-2">
Bevestig hieronder dat je wilt inloggen op het apparaat dat de QR-code toont.
</p>
<PairConfirmation />
</main>
)
}
```
Geen searchParams! De page leest de URL überhaupt niet — alleen het client-island doet dat client-side via `window.location.hash`.
3. **`app/(app)/m/pair/pair-confirmation.tsx`** — Client Component:
```tsx
'use client'
import { useEffect, useState, useTransition } from 'react'
import { Button } from '@/components/ui/button'
import { toast } from 'sonner'
import { getPairingForApproval, approvePairing, cancelPairing } from '@/actions/pairing'
type State =
| { kind: 'loading' }
| { kind: 'invalid'; error: string }
| { kind: 'ready'; pairingId: string; secret: string; ua: string | null; ip: string | null; username: string }
| { kind: 'success' }
| { kind: 'error'; error: string }
function parseHash(): { id: string; s: string } | null {
if (typeof window === 'undefined') return null
const hash = window.location.hash.replace(/^#/, '')
const params = new URLSearchParams(hash)
const id = params.get('id'); const s = params.get('s')
return id && s ? { id, s } : null
}
export function PairConfirmation() {
const [state, setState] = useState<State>({ kind: 'loading' })
const [pending, startTransition] = useTransition()
useEffect(() => {
const parsed = parseHash()
if (!parsed) {
setState({ kind: 'invalid', error: 'Ongeldige pairing-link' })
return
}
getPairingForApproval(parsed.id, parsed.s).then((res) => {
if (!res.ok) setState({ kind: 'invalid', error: res.error })
else setState({
kind: 'ready',
pairingId: parsed.id, secret: parsed.s,
ua: res.desktop_ua, ip: res.desktop_ip, username: res.username,
})
})
}, [])
function onApprove() {
if (state.kind !== 'ready') return
startTransition(async () => {
const res = await approvePairing(state.pairingId, state.secret)
if (!res.ok) { toast.error(res.error); return }
// Wist secret uit URL zodat back/forward 'm niet onthult
if (typeof window !== 'undefined') {
window.history.replaceState(null, '', window.location.pathname)
}
setState({ kind: 'success' })
})
}
function onCancel() {
if (state.kind !== 'ready') return
startTransition(async () => {
await cancelPairing(state.pairingId, state.secret)
setState({ kind: 'invalid', error: 'Pairing geannuleerd' })
})
}
// Render-logica per state — kort:
// loading → spinner
// invalid → foutmelding + link "Terug naar dashboard"
// ready → kaart met UA/IP/username + Bevestig/Annuleer
// success → "Klaar — je kunt deze tab sluiten"
// … (volledige JSX in implementation)
}
```
4. **Tests**`__tests__/actions/pairing.test.ts`:
- `getPairingForApproval` met `pending`-pairing → `ok: true` + ua/ip/username
- `getPairingForApproval` met al-`approved``ok: false`
- `getPairingForApproval` met verlopen → `ok: false`
- `getPairingForApproval` met verkeerd secret → `ok: false`
- `approvePairing` als demo-user → `ok: false` + DB onveranderd
- `approvePairing` happy path → status `approved`, `user_id` gezet, `expires_at` bumped
- `cancelPairing` happy path → status `cancelled`
**Aandachtspunten**
- `getPairingForApproval` mág door demo-users worden aangeroepen (om iets te zien); alleen `approvePairing` blokkeert demo's.
- `pair-confirmation.tsx` gebruikt **geen** `useSearchParams` — die kan in Next.js 16 hash niet lezen, en we willen het ook niet via routing zien.
- Na approve `window.history.replaceState` zonder de `#hash` zorgt dat browser-back de secret niet opnieuw onthult.
- `revalidatePath` in `approvePairing` is niet nodig — de desktop hoort het via SSE, en de mobiele page heeft geen server-state om te ververversen.
**Verificatie**
- `npm run lint && npx tsc --noEmit && npm test && npm run build` groen
- Handmatig: log in op telefoon-emulator → bezoek `/m/pair#id=…&s=…` → kaart toont UA/IP → Bevestig → succes-state, URL is `/m/pair` zonder hash
---
## ST-1006 — `POST /api/auth/pair/claim` (cookie-auth, schrijft iron-session)
**Bestanden**
- `app/api/auth/pair/claim/route.ts` — nieuw
- `__tests__/api/pair-claim.test.ts` — nieuw
**Stappen**
1. Route Handler:
```ts
import { NextRequest } from 'next/server'
import { getIronSession } from 'iron-session'
import { cookies } from 'next/headers'
import { prisma } from '@/lib/prisma'
import { SessionData, sessionOptions } from '@/lib/session'
import { hashToken } from '@/lib/auth/pairing'
import { readPairCookie, clearPairCookie } from '@/lib/auth/pair-cookie'
export const runtime = 'nodejs'
const PAIRED_TTL_MS = 8 * 60 * 60 * 1000 // 8 uur
export async function POST(request: NextRequest) {
const desktopToken = await readPairCookie()
if (!desktopToken) return Response.json({ error: 'Geen pairing-cookie' }, { status: 401 })
const body = await request.json().catch(() => null) as { pairingId?: string } | null
const pairingId = body?.pairingId
if (!pairingId) return Response.json({ error: 'pairingId vereist' }, { status: 400 })
const desktopTokenHash = hashToken(desktopToken)
// Atomic: WHERE status='approved' AND token-hash + expiry → consumed, RETURNING user_id
const updated = await prisma.loginPairing.updateMany({
where: {
id: pairingId,
status: 'approved',
desktop_token_hash: desktopTokenHash,
expires_at: { gt: new Date() },
},
data: { status: 'consumed', consumed_at: new Date() },
})
if (updated.count !== 1) {
// Was het wél een geldige cookie maar al consumed? → 410. Anders → 401.
const exists = await prisma.loginPairing.findFirst({
where: { id: pairingId, desktop_token_hash: desktopTokenHash },
select: { status: true, expires_at: true },
})
await clearPairCookie()
if (!exists) return Response.json({ error: 'Ongeldig' }, { status: 401 })
if (exists.status === 'consumed') return Response.json({ error: 'Al gebruikt' }, { status: 410 })
return Response.json({ error: 'Niet beschikbaar' }, { status: 410 })
}
const pairing = await prisma.loginPairing.findUnique({
where: { id: pairingId },
select: { user_id: true, user: { select: { is_demo: true } } },
})
if (!pairing?.user_id) {
await clearPairCookie()
return Response.json({ error: 'Pairing zonder user' }, { status: 500 })
}
const session = await getIronSession<SessionData>(await cookies(), sessionOptions)
session.userId = pairing.user_id
session.isDemo = pairing.user?.is_demo ?? false
session.paired = true
session.pairedExpiresAt = Date.now() + PAIRED_TTL_MS
await session.save()
await clearPairCookie()
return Response.json({ ok: true })
}
```
2. **Tests**:
- 200 + iron-session cookie + clear `s4m_pair` na succes
- 410 op tweede claim met dezelfde cookie
- 401 zonder cookie
- 401 met cookie die hasht naar andere pairing
- paired-sessie bevat `paired: true` en `pairedExpiresAt` rond `now + 8h`
**Aandachtspunten**
- `updateMany` gebruiken (niet `update`) want we hebben een composite WHERE met `status` + `desktop_token_hash` + `expires_at`; `update` kan alleen op unique keys.
- Het WHERE-criterium garandeert atomiciteit: PostgreSQL UPDATE met meerdere predicates is row-level locked; concurrent dubbele claim resulteert in `count = 1` voor één caller en `count = 0` voor de ander.
- `clearPairCookie` ook bij faalpaden, anders blijft 'm na expiry hangen (cosmetisch — `Max-Age=120` regelt het ook).
- De `session.isDemo` check overneemt: als de approver een demo-user is — wat ST-1005 al blokkeert — komen we hier niet eens, maar `is_demo` doorzetten is een extra vangnet.
**Verificatie**
- Handmatig: na approve in mobiele tab, POST naar `/api/auth/pair/claim` met de cookie van start → 200 + `Set-Cookie: session=...`
- `curl -X POST` zonder cookie → 401
- Tweede claim → 410
---
## ST-1007 — Desktop UI: QR-render + SSE-listener op `/login`
**Bestanden**
- `app/login/page.tsx` — bestaand, knop "Inloggen via mobiel" toevoegen
- `app/login/qr-login-button.tsx` — nieuw, Client Component
- `package.json``qrcode.react` toevoegen
- `__tests__/components/qr-login-button.test.tsx` — minimale render-test
**Stappen**
1. **Dependency**: `npm install qrcode.react` — direct in `dependencies` per CLAUDE.md-conventie.
2. **`qr-login-button.tsx`**:
```tsx
'use client'
import { useEffect, useRef, useState } from 'react'
import { useRouter } from 'next/navigation'
import { QRCodeSVG } from 'qrcode.react'
import { Button } from '@/components/ui/button'
import { toast } from 'sonner'
type Phase =
| { kind: 'idle' }
| { kind: 'starting' }
| { kind: 'showing'; pairingId: string; qrUrl: string; expiresAt: number }
| { kind: 'expired'; pairingId: string }
| { kind: 'claiming' }
export function QrLoginButton() {
const router = useRouter()
const [phase, setPhase] = useState<Phase>({ kind: 'idle' })
const sseRef = useRef<EventSource | null>(null)
async function start() {
setPhase({ kind: 'starting' })
const res = await fetch('/api/auth/pair/start', {
method: 'POST', credentials: 'same-origin',
})
if (!res.ok) { toast.error('Kon QR-code niet aanmaken'); setPhase({ kind: 'idle' }); return }
const data = await res.json() as { pairingId: string; qrUrl: string; expiresAt: string }
setPhase({
kind: 'showing',
pairingId: data.pairingId,
qrUrl: data.qrUrl,
expiresAt: new Date(data.expiresAt).getTime(),
})
}
// SSE-koppeling
useEffect(() => {
if (phase.kind !== 'showing') return
const es = new EventSource(`/api/auth/pair/stream/${phase.pairingId}`, {
withCredentials: true,
})
sseRef.current = es
es.addEventListener('message', async (ev) => {
const data = JSON.parse(ev.data) as { status?: string }
if (data.status === 'approved') {
es.close()
setPhase({ kind: 'claiming' })
const res = await fetch('/api/auth/pair/claim', {
method: 'POST',
credentials: 'same-origin',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ pairingId: phase.pairingId }),
})
if (!res.ok) {
toast.error('Inloggen mislukt')
setPhase({ kind: 'idle' }); return
}
router.push('/dashboard')
}
})
es.addEventListener('error', () => { /* silent — laat reconnecten */ })
return () => { es.close() }
}, [phase, router])
// Aftellende timer + auto-expire
useEffect(() => {
if (phase.kind !== 'showing') return
const t = setInterval(() => {
if (Date.now() > phase.expiresAt) {
sseRef.current?.close()
setPhase({ kind: 'expired', pairingId: phase.pairingId })
clearInterval(t)
}
}, 1000)
return () => clearInterval(t)
}, [phase])
// Render: knop / QR + countdown / "Vernieuwen" — JSX hier weggelaten voor brevity
}
```
3. **`app/login/page.tsx`** — knop toevoegen onder of naast het wachtwoord-formulier:
```tsx
<div className="my-4 flex items-center gap-2">
<div className="border-border h-px flex-1 border-t" />
<span className="text-muted-foreground text-sm">of</span>
<div className="border-border h-px flex-1 border-t" />
</div>
<QrLoginButton />
```
MD3-tokens uit `docs/design/styling.md`; geen willekeurige Tailwind-kleuren.
4. **A11y**: QR-component krijgt `aria-label="QR-code voor mobiel inloggen"` en de URL wordt visueel als kopieer-bare tekst onder de QR getoond zodat screenreaders en gebruikers met cameraproblemen de URL handmatig kunnen openen.
**Aandachtspunten**
- `EventSource({ withCredentials: true })` is verplicht zodat de browser de `s4m_pair`-cookie meestuurt; standaard verstuurt EventSource geen credentials.
- Cleanup-volgorde: `es.close()` eerst, dán fetch claim. Anders kan een tweede `approved`-event tussen close-en-claim binnenkomen en een dubbele claim triggeren (server vangt het op met 410, maar netter is het netjes te sluiten).
- `qrcode.react` exporteert zowel `QRCodeSVG` als `QRCodeCanvas`. Kies SVG: schaalbaarder voor printen/screenshots en kleinere bundle.
- Geen `next/image` — QR is dynamisch en client-side.
**Verificatie**
- `npm run lint && npx tsc --noEmit && npm test && npm run build` groen
- Handmatige twee-browser-test: A toont QR → B (ingelogd op mobile-emulator) opent QR-URL en bevestigt → A redirect naar `/dashboard` met `session.paired === true`
- DevTools Network-tab: geen URL bevat `s=` (alleen `Set-Cookie`/`Cookie` headers)
- Speel met een verlopen QR: na 2 min toont knop "Vernieuwen", `Vernieuwen` start nieuwe pairing
---
## ST-1008 — Documentatie + acceptatietest
**Bestanden**
- `docs/api/rest-contract.md` — drie nieuwe endpoints
- `docs/architecture.md` — sectie "QR-pairing flow" + threat-model
- `docs/patterns/qr-login.md` — nieuw pattern-doc
- `CLAUDE.md` — verwijzing naar het pattern-doc in de patterns-tabel
- `__tests__/integration/qr-pairing-e2e.test.ts` — optioneel, alleen als de test-infra het toelaat
**Stappen**
1. **`docs/api/rest-contract.md`** — drie endpoints documenteren met request/response, foutcodes (400/401/403/404/410/422/429), curl-voorbeelden inclusief `--cookie-jar`. Voeg een sectie *"Cookie-mechaniek"* toe die uitlegt dat `s4m_pair` een tijdelijke pre-auth cookie is, anders dan de iron-session cookie.
2. **`docs/architecture.md`** — sectie *"QR-pairing flow"* met:
- Sequence-diagram (mermaid of ASCII analoog aan M8)
- Threat-model:
- **Replay**: atomic `updateMany` met `status='approved'` voorkomt dubbele claim
- **Phishing-QR**: mobiele bevestigingspagina toont UA + IP zodat gebruiker een vreemd apparaat herkent; expliciete tap vereist
- **Demo-block**: `approvePairing` early return op `session.isDemo`
- **Rate-limit**: 10 starts per IP per minuut
- **Secret-hashing**: alleen sha256-hashes in DB; secrets verlaten desktop alleen via QR-fragment + POST-body
- **TTL-rationale**: 2 min pending vs. 5 min approved vs. 8 u paired-sessie — verschillen verklaren
- **Subsectie "Waarom geen secret in URL"**: fragment-eigenschap (browsers sturen `#…` niet naar server); HttpOnly cookie voor desktop-bewijs; geen secret in access logs / reverse-proxy logs / observability / browsergeschiedenis
3. **`docs/patterns/qr-login.md`** — herbruikbaar patroon: "unauth-SSE-via-pre-auth-cookie". Toekomstige features die een pre-auth flow met realtime-updates willen kunnen dit kopiëren (bv. "ontvang webhook live", "long-running export"). Inclusief:
- Wanneer dit patroon te gebruiken (er moet realtime-feedback zijn vóór de gebruiker is geauthenticeerd)
- Verwijzingen naar `lib/auth/pair-cookie.ts` als sjabloon
- Risico's en mitigaties
4. **`CLAUDE.md`** — in de *Implementatiepatronen*-tabel een rij `| QR-pairing (unauth-SSE + pre-auth cookie) | docs/patterns/qr-login.md |`.
5. **Acceptatie-scenario's** (handmatig, eventueel automatiseerbaar in v2):
1. Happy path — twee browsers, end-to-end binnen 2 minuten ingelogd
2. Demo-block — mobiel ingelogd als demo-user, scant QR → kan niet bevestigen
3. Replay — claim de pairing twee keer → tweede call 410
4. Expiry tijdens pending — wacht 3 min na start, scan dan → mobiel toont "Pairing verlopen"
5. Expiry tussen approve en claim — approve, wacht 6 min, claim → 410
6. Ontbrekende cookie op SSE/claim — verwijder `s4m_pair` in DevTools, herhaal → 401
7. **Secret niet in access logs** — controleer Vercel runtime-logs (via `mcp__a1fa0fcf-…__get_runtime_logs`) en lokale dev-logs; zoek op de secret-string en op `s=`-substrings; verwacht: 0 hits
**Aandachtspunten**
- Zorg dat de runtime-logs MCP-controle in `docs/qa/api-test-plan.md` belandt zodat hij bij elke release herhaalbaar is.
- `docs/patterns/qr-login.md` mag refereren naar bestaande pattern-docs (iron-session, route-handler) zonder ze te dupliceren.
**Verificatie**
- `npm run lint && npx tsc --noEmit && npm test && npm run build` groen
- Alle zeven scenario's handmatig groen, beschreven in een test-rapport-sectie
- `vendor/scrum4me`-submodule in mcp gesynced ná schema-merge
---
## Branch- en commit-strategie
Per [Branch & PR Strategy](../runbooks/branch-and-commit.md): **één branch voor de hele milestone**, PR pas na handmatige acceptatie door de gebruiker. Reden: elke push triggert een Vercel preview-build, en op het Hobby-account zijn die schaars.
**Branch:** `feat/M10-qr-login` — afgesplitst van `main` na merge van de planning-PR (#11). Alle ST-1001..ST-1008-werk landt op deze branch.
**Commits** in chronologische volgorde, één per stap, ST-code in de titel. Voorbeeld-progressie:
```
feat(ST-1001): add LoginPairing model
feat(ST-1001): add pg_notify trigger on scrum4me_pairing channel
feat(ST-1002): add pairing helpers and pre-auth cookie
feat(ST-1002): extend SessionData with paired flag
feat(ST-1002): guard expired paired sessions in app layout
feat(ST-1003): add /api/auth/pair/start with rate-limit and pre-auth cookie
feat(ST-1004): add SSE /api/auth/pair/stream with cookie auth
feat(ST-1005): add pairing server actions
feat(ST-1005): add mobile pair confirmation page with hash-fragment client island
feat(ST-1006): add /api/auth/pair/claim with atomic consume
chore(ST-1007): add qrcode.react dependency
feat(ST-1007): add QR login button on /login with SSE listener
docs(ST-1008): document QR-pairing endpoints in api.md
docs(ST-1008): add QR-pairing flow and threat-model to architecture
docs(ST-1008): add qr-login pattern doc
```
**Push + PR**: pas nadat ST-1008-acceptatie-scenario 1 (happy path, end-to-end op localhost) handmatig groen is bevonden door de gebruiker. Tussentijdse "klaar voor jouw test"-momenten markeren we lokaal — niet met een push.
**Pre-merge gates** (uit CLAUDE.md DoD):
- `npm run lint && npm test && npm run build` groen op CI
- Schema-wijziging in ST-1001 → wekelijkse drift-check `trig_015FFUnxjz9WMuhhWNGBQKFD` mag niet rood staan; `vendor/scrum4me`-submodule in mcp meebewegen na merge
**Wanneer dit aanpassen:** zodra het Vercel-account naar Pro gaat — zie CLAUDE.md.
---
## Reseed-stap (eenmalig vóór ST-1001-implementatie)
De backlog-markdown bevat ST-1001..1008, maar de live database heeft die rijen nog niet. Voordat `mcp__scrum4me__get_claude_context` ze als next-story kan teruggeven:
```
npx prisma db seed
```
Verifieer dat M10 en zijn 8 stories als `status=OPEN` in de DB staan. Daarna geeft `mcp__scrum4me__implement_next_story product_id=cmohfotr70000jwrt0hw4q020` automatisch ST-1001 als startpunt.
> **Let op:** seed kan bestaande dev-data overschrijven. Doe dit op een dev-DB, niet productie. Voor productie volstaat het om de stories handmatig of via een eenmalige migratie-script in te voegen — buiten scope hier.

View file

@ -0,0 +1,198 @@
# PBI-11 — Mobile-shell met landscape-lock (settings + backlog + solo)
> **Status:** READY · priority 3 · sort_order 8
> **Stories:** ST-1133 (TaskDialog full-screen) · ST-1134 (foundation) · ST-1135 (UA-redirect) · ST-1136 (settings) · ST-1137 (backlog) · ST-1138 (solo) · ST-1139 (docs + E2E)
## Doel
Scrum4Me bruikbaar maken op een mobiele telefoon, beperkt tot drie schermen — Settings (account + product-selector + QR-pairing-instructie + logout), Product Backlog (PBI/Story/Task aanmaken), Solo Paneel (voortgang vastleggen). Landscape-orientatie afgedwongen via PWA-manifest + CSS-overlay. App-naam en -icoon onderdrukken op `/m/*`. Desktop-app blijft ongewijzigd.
## Drie architectuur-beslissingen
### Beslissing A — gedeelde dialog-classes (raakt ST-1133 + ST-1138)
Alle entity-dialogen (PbiDialog, StoryDialog, TaskDialog, TaskDetailDialog) delen dezelfde class-string in [components/shared/entity-dialog-layout.ts](../../components/shared/entity-dialog-layout.ts):
```ts
export const entityDialogContentClasses = cn(
'flex flex-col p-0 gap-0',
'max-h-[90vh] w-full max-w-[calc(100%-2rem)]',
'sm:max-w-[90vw] sm:max-h-[85vh]',
'lg:max-w-[50vw] lg:min-w-[480px]',
)
```
→ Mobile-fullscreen wordt via één edit op deze constant geregeld:
```ts
'max-sm:w-screen max-sm:h-screen max-sm:max-w-none max-sm:rounded-none'
```
**Gevolg voor stories:**
- ST-1133 T-317 muteert `entity-dialog-layout.ts`, niet `task-dialog.tsx` rechtstreeks
- ST-1138 T-332 vervalt als file-edit — wordt verify-only (controleer dat TaskDetailDialog mee-erft)
- PBI/Story-dialogen krijgen mobile-fullscreen "voor niets" (handig voor ST-1137)
### Beslissing B — eigen route group `app/(mobile)/`
Parent layout `app/(app)/layout.tsx` rendert NavBar, MinWidthBanner, StatusBar, SoloRealtimeBridge, NotificationsBridge. Een nested layout in `(app)/m/` kan deze parent-output **niet** verwijderen (Next.js layouts erven naar binnen, niet vervangen).
**Keuze:** verplaats `/m/*` naar een eigen route group `app/(mobile)/m/{settings,pair,products}/...` met eigen `app/(mobile)/layout.tsx`.
**Auth-guard duplicatie voorkomen** door `getSession()`-check te extraheren naar `lib/auth-guard.ts`:
```ts
// lib/auth-guard.ts
export async function requireSession() {
const session = await getSession()
if (!session.userId) redirect('/login')
return session
}
```
Beide layouts (`(app)/layout.tsx` en `(mobile)/layout.tsx`) roepen deze helper aan. Bestaande `/m/pair/page.tsx` (M10 QR-pairing) verhuist mee naar `app/(mobile)/m/pair/page.tsx` — geen URL-wijziging, alleen filesystem-move.
**Gevolg voor stories:**
- ST-1134 T-321 schrijft `app/(mobile)/layout.tsx`, niet `app/(app)/m/layout.tsx`
- ST-1136 page wordt `app/(mobile)/m/settings/page.tsx`
- ST-1137 page wordt `app/(mobile)/m/products/[id]/page.tsx`
- ST-1138 page wordt `app/(mobile)/m/products/[id]/solo/page.tsx`
- M10's `/m/pair` verhuist naar `app/(mobile)/m/pair/` — URL ongewijzigd, geen redirect-migratie nodig
### Beslissing C — gescheiden SplitPane cookie-key
ST-1137 hergebruikt `BacklogSplitPane` (drie panelen). Op mobile rendert die in tab-mode (auto-switch + back-button uit ST-1116). De SplitPane bewaart split-percentages in een cookie.
**Keuze:** gescheiden cookie-key voor mobile — `split-pane:backlog-3-mobile:<id>` — zodat mobile-gebruikers (die in tab-mode geen split-percentages bewerken maar wel terug kunnen schakelen) de desktop-split niet beïnvloeden.
**Gevolg voor stories:**
- ST-1137 T-328 geeft expliciete `cookieKey`-prop aan `BacklogSplitPane` op de mobile-route
## Hergebruik (al aanwezig)
| Wat | Bron |
|---|---|
| Mobile tab-mode in `SplitPane` (incl. `tabLabels`, `mobileBreakpoint`, `activeTab`) | ST-1116 — [components/split-pane/split-pane.tsx](../../components/split-pane/split-pane.tsx) |
| Click-cascade auto-switch in `BacklogSplitPane` | ST-1116 commit `3e86a8d` |
| QR-pairing route `/m/pair` | M10 commit `625221f` |
| `/m/pair` confirmation page | bestaand |
| Functional-spec mobile-tabs sectie | `docs/specs/functional.md:234-235` |
## Stories
### ST-1133 — TaskDialog full-screen op mobile (verifieer en fix)
**Doel:** entity-dialogen renderen 100vw × 100vh op viewport `<640px`.
**Acceptance:**
- `entityDialogContentClasses` in `components/shared/entity-dialog-layout.ts` bevat `max-sm:w-screen max-sm:h-screen max-sm:max-w-none max-sm:rounded-none`
- Sticky header en footer blijven bereikbaar; body scrollt
- Werkt voor TaskDialog, TaskDetailDialog, PbiDialog, StoryDialog (alle gebruiken de constant)
- Tests dekken mobile-render via `window.innerWidth`-mock voor minstens TaskDialog en TaskDetailDialog
- Geen regressie op desktop (`sm:max-w-[90vw]` blijft op `>=640px`)
**Tasks:**
- T-316 inventariseer huidige render
- T-317 fix de gedeelde constant
- T-318 tests
### ST-1134 — Mobile shell foundation (route group + landscape-guard + tab-bar + manifest)
**Doel:** route group `(mobile)`, landscape-overlay, bottom tab-bar, PWA-manifest.
**Acceptance:**
- `app/(mobile)/layout.tsx` rendert zonder NavBar / AppIcon / MinWidthBanner / StatusBar
- Auth-guard via gedeelde `lib/auth-guard.ts` helper; `(app)/layout.tsx` gebruikt dezelfde helper
- `<LandscapeGuard>` toont rotate-overlay in portrait (window.matchMedia)
- `<MobileTabBar>` bottom-fixed met 3 lucide-iconen (ListTree, Activity, Settings); tap-targets ≥44×44 px
- `public/manifest.json` bevat `"orientation": "landscape"`
- M10 `/m/pair` verhuist filesystem-only naar `app/(mobile)/m/pair/` — URL onveranderd
- Tests: LandscapeGuard render-states, TabBar route-active, auth-guard helper
**Tasks:**
- T-319 LandscapeGuard
- T-320 MobileTabBar
- T-321 `(mobile)/layout.tsx` + manifest + auth-guard extractie + filesystem-move van `/m/pair`
### ST-1135 — Mobile UA-redirect bij login
**Acceptance:**
- `lib/user-agent.ts` exporteert `isPhoneUA(ua: string | null): boolean` op basis van `Mobi`-substring
- `actions/auth.ts` `loginAction` redirect bij phone-UA naar `/m/products/[active]/solo`; zonder actief product naar `/m/settings`
- Tablet-UA en desktop-UA blijven op `/dashboard`
- Demo-user volgt zelfde routing
- Tests dekken alle paden (phone met/zonder product, tablet, desktop, null UA, demo)
**Tasks:** T-322 helper · T-323 loginAction integratie · T-324 tests
### ST-1136 — Mobile Settings-pagina
**Acceptance:**
- `app/(mobile)/m/settings/page.tsx`
- Toont username, isDemo-badge, actief-product-naam
- Product-selector — klik → `setActiveProductAction` + redirect `/m/products/[id]/solo`
- QR-pairing-instructie — link "Open scrum4me.app/login op je desktop om in te loggen via QR"
- Logout-knop met AlertDialog "Uitloggen?" → `logoutAction`
- Geen avatar-upload, geen bio-edit
- Tests render-states + logout-flow
**Tasks:** T-325 layout · T-326 logout-flow · T-327 tests
### ST-1137 — Mobile Product Backlog-pagina
**Acceptance:**
- `app/(mobile)/m/products/[id]/page.tsx` hergebruikt PbiList/StoryPanel/TaskPanel + backlog-store
- `BacklogSplitPane` rendert in tab-mode op `<1024px`; auto-switch op selectie blijft werken
- TaskDialog-searchParams wiring (`?newTask=`, `?editTask=`, `?storyId=`) werkt
- Cookie-key gescheiden: `split-pane:backlog-3-mobile:<id>`
- + knoppen voor PBI/Story/Task werken; demo blijft read-only
- Tests: page-rendering met initial state, tab-mode, click-cascade-flow
**Tasks:** T-328 page wrapper + cookie-key · T-329 TaskDialog wiring · T-330 tests
### ST-1138 — Mobile Solo Paneel
**Acceptance:**
- `app/(mobile)/m/products/[id]/solo/page.tsx` hergebruikt SoloBoard
- 3 kanban-kolommen blijven; horizontal scroll
- TaskDetailDialog rendert 100vw × 100vh op `<640px`**gedekt door beslissing A** (entityDialogContentClasses)
- "Voer uit"-knop bereikbaar
- SSE-stream blijft werken
- Tests: solo-page rendert, TaskDetailDialog erft mobile-classes (zonder eigen file-edit)
**Tasks:**
- T-331 page wrapper
- T-332 verify-only (geen file-edit; controleer dat shared constant uit ST-1133 doorwerkt)
- T-333 tests
### ST-1139 — Docs sync + end-to-end verificatie
**Acceptance:**
- `docs/specs/functional.md` heeft "Mobile shell"-sectie; desktop-first-clausule herzien
- `docs/architecture.md` beschrijft route group `(mobile)`, manifest landscape, UA-redirect, gedeelde auth-guard
- `npm run lint && npm test && npm run build` slagen
- E2E checklist (11 punten — zie hieronder)
- Bekende limiet: iOS Safari PWA-orientation-lock werkt niet 100% — CSS-overlay als fallback
**Tasks:** T-334 functional-spec · T-335 architecture-doc · T-336 E2E-verificatie
## Verificatie (E2E checklist uit T-336)
1. `npm run lint && npm test && npm run build` slagen
2. DevTools mobile-emulatie iPhone 12 landscape: `/m/products/[id]` rendert tab-mode, geen NavBar, tab-bar onderaan
3. Portrait → rotate-overlay zichtbaar; landscape → overlay verdwijnt
4. Tab-bar 3 iconen werken (Backlog/Solo/Settings)
5. Login phone-UA → redirect `/m/products/[id]/solo`; desktop-UA → `/dashboard`
6. Backlog-flow: + PBI, + Story, + Task in TaskDialog
7. Solo-flow: tap task → TaskDetailDialog full-screen, "Voer uit"-knop bereikbaar
8. TaskDialog full-screen op `<640px` (via shared constant)
9. PWA-installatie test op echte mobile (Android of iOS)
10. `/m/pair` QR-flow intact na route-group-verhuizing
11. Demo op mobile read-only; logout via `/m/settings` werkt; geen Scrum4Me-tekst of AppIcon op `/m/*`
## Out of scope
- Tablets (geen Mobi-UA) blijven desktop-flow gebruiken
- iOS PWA full-orientation-lock (CSS-overlay is fallback)
- Avatar/bio editor op mobile-settings
- 1-koloms-kanban (3-koloms blijft, swipe horizontaal)

View file

@ -0,0 +1,128 @@
# PBI-75 — Sprint task-edit client-side via workspace-store
## Context
In het Sprint-scherm (`/products/<id>/sprint/<sprintId>`) duurt het bewerken van een taak onevenredig lang. Klik op een taakregel of het potlood-icoon roept `router.push(?editTask=<id>)` aan vanuit [`components/sprint/task-list.tsx:226`](../../components/sprint/task-list.tsx). Dat triggert:
- **Volledige server re-render** van [`app/(app)/products/[id]/sprint/[sprintId]/page.tsx`](../../app/(app)/products/[id]/sprint/[sprintId]/page.tsx) met zware queries (sprint, alle sprintStories+tasks, productMembers, alle PBIs+stories voor het backlog-paneel)
- **Tweede Prisma-query** in [`EditTaskLoader`](../../app/_components/tasks/edit-task-loader.tsx) voor task-detail (incl. `implementation_plan`)
- **Na save**: `router.push(closePath)` + `revalidatePath` → opnieuw alle queries
De [`sprint-workspace-store`](../../stores/sprint-workspace/store.ts) (sinds PBI-74 Story 9) bevat al alles voor een client-side edit-flow:
- [`setActiveTask(taskId)`](../../stores/sprint-workspace/store.ts) (regel 337) — zet `context.activeTaskId` + roept `ensureTaskLoaded()` aan
- [`ensureTaskLoaded(taskId)`](../../stores/sprint-workspace/store.ts) (regel 414) — `GET /api/tasks/{id}` → upsert met `_detail: true`
- [`selectActiveTask`](../../stores/sprint-workspace/selectors.ts) (regel 91) — bestaat, nog geen consumer
- [`applyTaskEvent`](../../stores/sprint-workspace/store.ts) (regel 748) — SSE-events propageren idempotent na server-save
- Optimistic-mutation primitives (`applyOptimisticMutation` / `settleMutation` / `rollbackMutation`)
Het patroon "URL-param → store" bestaat al voor product-workspace in [`components/backlog/url-task-sync.tsx`](../../components/backlog/url-task-sync.tsx) — we volgen dat als precedent.
**Doel**: klik op een taak opent de edit-dialoog client-side via store-state binnen ~100ms. Geen URL-navigatie, geen server re-render, alleen `GET /api/tasks/{id}` voor het detail.
## Aanpak
**Architectuur**: store-mounted dialog + URL-sync component voor deeplinks.
1. **Klik-flow**: `TaskList.openEditDialog` roept `setActiveTask(taskId)` aan op de store. Geen `router.push`.
2. **Render-flow**: nieuwe client-component `SprintTaskDialogMount` zit binnen `SprintHydrationWrapper`, subscribet `selectActiveTask`, en rendert `<TaskDialog>` zodra de active task `_detail === true` is.
3. **Save-flow**: `TaskDialog` krijgt optionele `onClose`/`onSaved` callbacks (backwards compatible met bestaande `closePath`). Mount geeft `onClose = () => setActiveTask(null)`. Server action `saveTask` blijft `revalidatePath` doen voor server-cache; SSE-event update store via `applyTaskEvent`.
4. **Deeplink-flow**: nieuwe `SprintUrlTaskSync` leest `?editTask=<id>` en roept `setActiveTask(id)` aan (analoog aan `url-task-sync.tsx`).
## Bestanden + wijzigingen
### Nieuw — `components/sprint/sprint-task-dialog-mount.tsx`
Client component. Subscribet `selectActiveTask` (single-value, geen `useShallow`). Wanneer task aanwezig is en `isDetail(task)` true, mappt naar `TaskDialogTask`-shape:
- `status`: via `taskStatusFromApi` uit [`lib/task-status.ts`](../../lib/task-status.ts) (lowercase API → Prisma UPPER_SNAKE)
- `implementation_plan: task.implementation_plan ?? null`
- `created_at: new Date(task.created_at)`
Rendert `<TaskDialog task={mapped} productId={productId} onClose={() => setActiveTask(null)} isDemo={isDemo} />`. Geen render tussen `setActiveTask` en `_detail: true` (detail-fetch <100ms).
### Nieuw — `components/sprint/sprint-url-task-sync.tsx`
Kopie van [`components/backlog/url-task-sync.tsx`](../../components/backlog/url-task-sync.tsx) maar tegen `useSprintWorkspaceStore` en `writeTaskHint` uit [`stores/sprint-workspace/restore`](../../stores/sprint-workspace/restore.ts).
### Wijziging — `components/sprint/task-list.tsx` (regels 225-227)
Vervang:
```ts
function openEditDialog(taskId: string) {
router.push(`${pathname}?editTask=${taskId}`)
}
```
door:
```ts
function openEditDialog(taskId: string) {
useSprintWorkspaceStore.getState().setActiveTask(taskId)
}
```
`openCreateDialog` (regel 222) blijft URL-gebaseerd — out-of-scope.
### Wijziging — `app/(app)/products/[id]/sprint/[sprintId]/page.tsx`
- Verwijder `editTask` uit searchParams-destructuring (regel 36)
- Verwijder `editTask &&`-block met `<Suspense><EditTaskLoader>` (regels 250-260)
- Verwijder ongebruikte imports (`EditTaskLoader`, `TaskDialogSkeleton`, evt. `Suspense`)
- Mount binnen `SprintHydrationWrapper`:
```tsx
<SprintHydrationWrapper ...>
<SprintBoardClient ... />
<SprintTaskDialogMount productId={id} isDemo={isDemo} />
<SprintUrlTaskSync />
</SprintHydrationWrapper>
```
- `newTask`-block (regels 241-248) blijft ongemoeid — out-of-scope.
### Wijziging — `app/_components/tasks/task-dialog.tsx`
Maak `closePath` optioneel + voeg `onClose`/`onSaved` toe (backwards compatible):
```ts
interface TaskDialogProps {
task?: TaskDialogTask
storyId?: string
productId: string
closePath?: string
onClose?: () => void
onSaved?: (taskId: string) => void
isDemo?: boolean
}
```
Refactor de drie `router.push(closePath)`-calls (regels 104, 120, 155) naar één helper:
```ts
function close() {
if (onClose) { onClose(); return }
if (closePath) router.push(closePath)
}
```
Bestaande callers (`EditTaskLoader`, mobile, product-page, sprint `newTask`-block) blijven werken via `closePath`. Nieuwe `SprintTaskDialogMount` gebruikt `onClose`.
### Geen wijziging
- `stores/sprint-workspace/selectors.ts``selectActiveTask` bestaat al
- `app/_components/tasks/edit-task-loader.tsx` — nog gebruikt door product-page en mobile
## Edge cases
- **Status-enum mapping**: store API-lowercase → Prisma UPPER_SNAKE via `taskStatusFromApi`, fallback `'TO_DO'`
- **`_detail: true` race**: mount rendert pas wanneer `isDetail(task)` true is — geen flash met undefined velden
- **Demo-mode**: prop blijft via server doorlopen, dialog respecteert al `isDemo`
- **Dirty-close-guard**: ingebouwd in dialog (regels 107, 172) — werkt via `onClose`
- **SSE na save**: `applyTaskEvent` updatet store automatisch
- **Deeplink + task niet bestaat**: `GET /api/tasks/{id}` 404 → store doet niets, dialog opent niet (huidige `redirect()` verdwijnt — acceptabel)
## Verificatie
1. **Browser** (`npm run dev`): klik op task in takenlijst → dialog opent <100ms, geen URL-verandering, alleen `GET /api/tasks/<id>` in Network
2. **Save**: wijzig titel → Opslaan → dialog sluit → store toont nieuwe titel via SSE
3. **Deeplink**: `?editTask=<id>` → dialog opent via `SprintUrlTaskSync`
4. **Bestaande flows ongebroken**: product-page edit, mobile edit, sprint `?newTask=1`
5. **`npm run verify && npm run build`**
6. **Vitest**: `__tests__/components/sprint/sprint-task-dialog-mount.test.tsx` — hydreer store, mock fetch, `setActiveTask(id)`, assert UPPER_SNAKE status + `onClose` clear
## Risico's
- Andere mounts (mobile, product-backlog, sprint `newTask`) blijven URL-gebaseerd — `closePath?` optional houdt ze werkend
- Geen `redirect()` bij not-found-deeplink (klein UX-verschil)
- SSE-latency 100-500ms na save — eventueel later mitigeren via `applyOptimisticMutation` in `onSaved`-callback
## Out-of-scope (follow-up PBIs)
- `?newTask=1`-flow naar store
- Mobile + product-backlog mounts
- `EditTaskLoader` verwijderen wanneer alle callers over zijn

View file

@ -0,0 +1,186 @@
# PBI-78 — Cost-analyse widget op Insights-pagina
## Context
De insights-pagina heeft al een `TokenUsageCard` die KPI's + per-job tabel toont, maar **alleen voor de actieve sprint van een gefilterd product**. Daardoor mis je het globale plaatje: hoeveel geef je deze maand uit, welk model is de grootste kostenpost, hoe goed werkt prompt-caching, en welke job-kinds (IDEA_MAKE_PLAN met Opus vs TASK_IMPLEMENTATION met Sonnet) trekken het budget.
We voegen een nieuwe sectie **"Cost analyse"** toe (tussen Sprint Health en Plan-quality). Eén shared periode-selector (7d/30d/90d/MTD) stuurt vier visualisaties aan op basis van best practices uit de Anthropic Console + LLM-observability tools (Datadog, Portkey):
1. Trend-chart over tijd
2. Breakdown per model
3. Breakdown per job-kind
4. Cache efficiency
De bestaande `TokenUsageCard` blijft staan als sprint-detail (top duurste jobs voor de actieve sprint).
## Bestaande infrastructuur (hergebruik)
**Reeds aanwezig in DB:**
- [prisma/schema.prisma](../../prisma/schema.prisma) — `ClaudeJob` heeft `input_tokens`, `output_tokens`, `cache_read_tokens`, `cache_write_tokens`, `actual_thinking_tokens`, `model_id`, `kind`, `finished_at`
- `ModelPrice` tabel met prijzen per 1M tokens (input/output/cache_read/cache_write)
- Prijzen worden gesynced via [scripts/sync-model-prices.ts](../../scripts/sync-model-prices.ts)
**Hergebruikbare patronen:**
- KPI-strip stijl: zie [app/(app)/insights/components/token-usage.tsx](../../app/(app)/insights/components/token-usage.tsx) (regels 43-64)
- URL-param-gestuurde filter met `useTransition` + `router.replace`: zie [app/(app)/insights/components/agent-throughput.tsx](../../app/(app)/insights/components/agent-throughput.tsx) (regels 38-62)
- Recharts BarChart pattern: zie [app/(app)/insights/components/agent-throughput.tsx](../../app/(app)/insights/components/agent-throughput.tsx) (regels 110-130)
- Cost-formule (zelfde overal): zie [lib/insights/token-stats.ts](../../lib/insights/token-stats.ts) (regels 73-79) — input + output + cache_read + cache_write + thinking, allemaal `× price_per_1m / 1_000_000`
- Server component → parallel data-fetch via `Promise.all`: zie [app/(app)/insights/page.tsx](../../app/(app)/insights/page.tsx) (regels 46-80)
## Te bouwen
### Taak 1 — Data-laag — `lib/insights/cost-analysis.ts` (nieuw)
Eén bestand met vijf functies, allemaal `(userId, period)` als parameters. Period wordt naar `WHERE cj.finished_at >= NOW() - INTERVAL '<n> days'` vertaald (MTD = `>= date_trunc('month', NOW())`).
```ts
export type Period = '7d' | '30d' | '90d' | 'mtd'
export interface CostKpi {
totalCostUsd: number
totalTokens: number
jobCount: number
avgPerDayUsd: number
cacheSavingsUsd: number // (input_price - cache_read_price) × cache_read_tokens
topModelId: string | null
topModelCostUsd: number
}
export interface CostByDayRow { day: string; costUsd: number }
export interface CostByModelRow { modelId: string; costUsd: number; jobCount: number }
export interface CostByKindRow { kind: string; costUsd: number; jobCount: number }
export interface CacheEfficiency {
cacheReadTokens: number
uncachedInputTokens: number
cacheHitRatio: number // cache_read / (cache_read + input)
savingsUsd: number
spentOnCacheWriteUsd: number
}
export async function getCostKpi(userId: string, period: Period): Promise<CostKpi>
export async function getCostByDay(userId: string, period: Period): Promise<CostByDayRow[]>
export async function getCostByModel(userId: string, period: Period): Promise<CostByModelRow[]>
export async function getCostByKind(userId: string, period: Period): Promise<CostByKindRow[]>
export async function getCacheEfficiency(userId: string, period: Period): Promise<CacheEfficiency>
```
**Belangrijke details:**
- Alle queries: `WHERE cj.user_id = ${userId} AND cj.status = 'DONE' AND cj.finished_at >= <periodStart>`
- Geen `productAccessFilter` nodig — `cj.user_id = ${userId}` filtert al op de eigenaar
- `getCostByDay` vult ontbrekende dagen op met `0` (anders breekt de chart-x-as) — vul aan client- of server-side, kies één
- Periode → days mapping inline: `7d`→7, `30d`→30, `90d`→90, `mtd`→huidige dag-van-maand
- Cache savings: `cache_read_tokens × (input_price - cache_read_price) / 1_000_000` — "wat je betaald zou hebben zonder cache, minus wat je betaalde mét cache"
### Taak 2 — UI — `app/(app)/insights/components/cost-analysis.tsx` (nieuw)
Eén client-component die de hele sectie rendert. Structuur:
```
[Period selector rechtsboven]
[KPI strip: Totaal | Cache savings | Avg/dag | Top model ($X op claude-opus-4-7)]
[grid grid-cols-1 md:grid-cols-2 gap-4]
[Daily cost line/bar chart] [Model breakdown - horizontal bar of donut]
[Job-kind breakdown - bar] [Cache efficiency - donut + label "X% hit, $Y bespaard"]
```
**Period selector:** kopieer pattern uit [agent-throughput.tsx](../../app/(app)/insights/components/agent-throughput.tsx) (regels 50-61) — `useTransition` + `router.replace` met `?period=` in URL. Default tonen als "30d".
**Charts:** Recharts (al gebruikt in `BurndownChart`, `AgentThroughputCard`, `VelocityChart`):
- Daily: `<BarChart>` met één bar (cost in USD), x-as = dag (`MM-DD`), tooltip toont `$X.XXXX`
- Model: `<BarChart layout="vertical">` met model_id labels — beperkt tot top 5
- Kind: `<BarChart layout="vertical">` met kind labels — beperkt tot top 5
- Cache: `<PieChart>` met twee segmenten (cached / uncached input) + tekst "X% cache hit · $Y bespaard"
**Empty state:** als `kpi.jobCount === 0`: render één regel "Geen jobs in deze periode."
### Taak 3 — Integratie — `app/(app)/insights/page.tsx` (edit)
Wijzigingen:
```diff
interface InsightsPageProps {
- searchParams: Promise<{ product?: string }>
+ searchParams: Promise<{ product?: string; period?: string }>
}
```
```diff
- const { product: filterProductId } = await searchParams
+ const { product: filterProductId, period: rawPeriod } = await searchParams
+ const period = (['7d','30d','90d','mtd'].includes(rawPeriod ?? '') ? rawPeriod : '30d') as Period
```
In de `Promise.all`, voeg toe:
```ts
getCostKpi(userId, period),
getCostByDay(userId, period),
getCostByModel(userId, period),
getCostByKind(userId, period),
getCacheEfficiency(userId, period),
```
Nieuwe sectie tussen Sprint Health en Plan-quality:
```tsx
<section className="space-y-3">
<h2 className="text-lg font-medium text-foreground">Cost analyse</h2>
<CostAnalysisCard
period={period}
kpi={costKpi}
byDay={costByDay}
byModel={costByModel}
byKind={costByKind}
cache={cacheEff}
/>
</section>
```
De bestaande "Token gebruik" sectie blijft staan (sprint-detail tabel).
## Bestanden
**Nieuw:**
- `lib/insights/cost-analysis.ts` — 5 query-functies + types
- `app/(app)/insights/components/cost-analysis.tsx` — client-component met period-selector + 4 charts
**Edit:**
- `app/(app)/insights/page.tsx` — period uit searchParams, parallel-fetch, nieuwe sectie
**Geen wijzigingen aan:**
- Prisma schema (alle data is er al)
- MCP server (token-data wordt al weggeschreven via `update_job_status`)
- `TokenUsageCard` (blijft als sprint-detail tabel)
## Verificatie
```bash
npm run verify && npm run build
```
**Handmatig:**
1. Open `/insights` zonder query — period default `30d`, sectie toont KPI + 4 charts
2. Wissel period via selector → URL updatet `?period=7d`, charts laden nieuwe data via `router.replace`
3. Check empty state: kies periode zonder jobs → "Geen jobs in deze periode."
4. Sanity-check KPI's tegen ruwe DB-query:
```sql
SELECT SUM(input_tokens * mp.input_price_per_1m / 1e6
+ output_tokens * mp.output_price_per_1m / 1e6
+ cache_read_tokens * mp.cache_read_price_per_1m / 1e6
+ cache_write_tokens * mp.cache_write_price_per_1m / 1e6
+ COALESCE(actual_thinking_tokens, 0) * mp.input_price_per_1m / 1e6)
FROM claude_jobs cj
LEFT JOIN model_prices mp ON mp.model_id = cj.model_id
WHERE cj.user_id = '<id>' AND cj.status = 'DONE'
AND cj.finished_at >= NOW() - INTERVAL '30 days';
```
5. Cache savings sanity: `cacheSavingsUsd ≈ cache_read_tokens × 0.9 × input_price / 1M`
(cache_read prijs = 0.1× input prijs, dus savings is 90%)

View file

@ -0,0 +1,486 @@
# Plan — Auto-PR + selectieve deploy-controle + sync-zicht (end-to-end batch flow)
> Bij merge: dit plan verplaatsen naar `docs/plans/auto-pr-deploy-sync.md`
> conform feedback-memory (plans in `docs/plans/`).
## Context
Drie samenhangende problemen rond de "idee → uitvoeren"-keten:
1. **Worker stopt bij `commit`.** De Scrum4Me NAS-worker werkt lokaal:
commits blijven op de machine staan totdat de gebruiker zelf pusht en
een PR aanmaakt. Voor batch-uitvoer van story-jobs is dit een harde
menselijke gate.
2. **Deploy is alles-of-niets.** `.github/workflows/ci.yml` deployt nu
**elke** push naar `main` automatisch naar productie en **elke** PR
naar preview. `vercel.json` heeft geen `git.deploymentEnabled: false`,
dus Vercel's eigen Git-integratie deployt waarschijnlijk parallel mee
→ dubbele deploys en geen selectieve controle.
3. **Geen zicht op voortgang per Idea/PBI.** Concreet getest geval:
PBI-33 wordt nu de eerste sprint-batch — er is **geen git-voetafdruk**
(geen branch/commit/PR met "PBI-33"), **geen activiteitenlog-entry**,
en geen UI-pagina die per Story toont of er een ClaudeJob loopt, een
commit gepusht is, of een PR open/merged is. De data zit in
`Story.status`, `ClaudeJob.pushed_at/branch/pr_url`,
`Pbi.pr_url/pr_merged_at` — er is alleen geen view die het joint.
Doel: de complete keten **plan → job → commit → push → PR → auto-merge →
deploy** in één coherent ontwerp leggen, met (a) selectieve
deploy-controle als veiligheidsklep en (b) een sync-tab die per Idea
laat zien wat er werkelijk in git/PR-land gebeurd is.
## Vastgelegde keuzes
### Deploy-controle
1. **Mechanisme**: PR-labels (B) + path-filter (C) gecombineerd.
2. **Eigenaar**: GitHub Actions-workflow (A). Vercel Git-integratie uit.
3. **Defaults**: PR → preview, push naar `main` → productie.
4. **Override-richtingen**:
- `skip-deploy` label: voorkomt preview-deploy op een PR.
- `force-deploy` label: forceert deploy ook als path-filter doc-only
zegt.
### Auto-PR (uit IDEA-007-grill)
5. **Triggers in worker**: na elke succesvolle `update_job_status('done')`
pusht de worker; na laatste story van een PBI maakt de worker een PR
aan en activeert auto-merge (SQUASH).
6. **Auth**: `GITHUB_TOKEN` als omgevingsvariabele op de worker; geen UI
of GitHub App in v1.
7. **Foutafhandeling**: push/PR-aanmaak-fail → `update_job_status('failed',
error: …)`; geen force-push, geen automatische retry.
### Interactie tussen beide
8. **Worker-PRs gebruiken hetzelfde labelsysteem als alle andere PRs.**
Default = preview deploy, auto-merge wacht op CI groen, na merge
prod-deploy (mits path-filter zegt "code"). De worker zet **geen**
labels automatisch — als je batch-output zonder preview wilt mergen
moet je `skip-deploy` zelf toevoegen, of preview later uitzetten via
een product-instelling (out-of-scope v1).
9. **Implementatievolgorde**: eerst deploy-controle (infra,
onafhankelijk), daarna auto-PR (afhankelijk van stabiele deploy-flow).
## Architectuur in één plaat
```
auto-merge wacht op
[story-job DONE] ─push branch─┐ deploy-preview groen
▼ │
[laatste story?]──ja──[PR + auto-merge]──CI──┴──merge naar main
[job: ci] altijd
[paths-filter]
├ PR → deploy-preview
│ if code && !skip-deploy
│ || force-deploy
└ push → deploy-production
if code
```
---
## Deel A — Deploy-controle
### A.1 `vercel.json` — Vercel Git-deploy uitzetten
```json
{
"$schema": "https://openapi.vercel.sh/vercel.json",
"git": { "deploymentEnabled": false },
"crons": [
{ "path": "/api/cron/expire-questions", "schedule": "0 4 * * *" },
{ "path": "/api/cron/cleanup-agent-artifacts", "schedule": "0 3 * * *" }
]
}
```
Effect: Vercel deployt niet meer automatisch op git-events. Alleen
`vercel deploy` vanuit de workflow (met `VERCEL_TOKEN`) maakt nog
deployments.
### A.2 `.github/workflows/ci.yml` — path-filter + label-checks
Triggers uitbreiden met `workflow_dispatch`:
```yaml
on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:
inputs:
target:
type: choice
description: Deploy target
options: [preview, production]
default: preview
```
Nieuwe job vóór de deploy-jobs:
```yaml
changes:
name: Detect deploy-relevant changes
runs-on: ubuntu-latest
needs: ci
outputs:
code: ${{ steps.filter.outputs.code }}
steps:
- uses: actions/checkout@v5
- uses: dorny/paths-filter@v3
id: filter
with:
filters: |
code:
- 'app/**'
- 'components/**'
- 'lib/**'
- 'actions/**'
- 'stores/**'
- 'prisma/**'
- 'public/**'
- 'package.json'
- 'package-lock.json'
- 'next.config.ts'
- 'tsconfig.json'
- 'vercel.json'
- 'proxy.ts'
- 'middleware.ts'
- '.github/workflows/**'
```
`deploy-preview` if-conditie aanpassen:
```yaml
deploy-preview:
needs: [ci, changes]
if: |
github.event_name == 'pull_request' && (
(needs.changes.outputs.code == 'true'
&& !contains(github.event.pull_request.labels.*.name, 'skip-deploy'))
|| contains(github.event.pull_request.labels.*.name, 'force-deploy')
)
```
`deploy-production` if-conditie aanpassen:
```yaml
deploy-production:
needs: [ci, changes]
if: |
github.ref == 'refs/heads/main'
&& github.event_name == 'push'
&& needs.changes.outputs.code == 'true'
```
Nieuwe `deploy-manual` job voor `workflow_dispatch` met `inputs.target`
`vercel deploy` of `vercel deploy --prod`.
### A.3 GitHub-labels aanmaken
```bash
gh label create skip-deploy --color BFBFBF --description "Preview-deploy overslaan"
gh label create force-deploy --color 0E8A16 --description "Forceer deploy ondanks path-filter"
```
### A.4 Documentatie
`docs/runbooks/deploy-control.md` — triggers, labels, path-filter,
voorbeelden. `CLAUDE.md` § Deployment-regel verwijst naar runbook.
---
## Deel B — Auto-PR (worker → GitHub)
### B.1 Acceptatiecriteria (uit IDEA-007)
- **AC 1 — Push per story**: Na succesvolle `update_job_status('done')`
pusht de worker via HTTPS (`https://$GITHUB_TOKEN@github.com/…`) naar
origin. Push-timestamp via nieuwe MCP-call in `ClaudeJob.pushed_at`.
- **AC 2 — Detectie laatste story**: Nieuwe MCP-call `check_pbi_complete`
retourneert `{ complete: boolean, pbi_id }`.
- **AC 3 — PR aanmaken**: Op `complete: true` POST naar
`/repos/{owner}/{repo}/pulls`; titel/body uit PBI-naam + voltooide
stories; PR-URL via `set_pbi_pr`.
- **AC 4 — Auto-merge activeren**: Direct na PR-aanmaak GraphQL
`enablePullRequestAutoMerge` (SQUASH).
- **AC 5 — Foutafhandeling**: push/PR-fail →
`update_job_status('failed', error)`; PR-URL blijft bewaard voor
handmatige inspectie.
### B.2 Server-side wijzigingen (Scrum4Me-repo)
Velden bestaan al in schema:
- `Product.auto_pr Boolean @default(false)` (regel 176)
- `Pbi.pr_url String?` + `Pbi.pr_merged_at DateTime?` (regel 207208)
- `ClaudeJob.pushed_at DateTime?` + `ClaudeJob.pr_url String?` +
`ClaudeJob.branch String?` (regel 335, 338, 339)
Geen migratie nodig.
Server actions / REST: bestaande `set_pbi_pr` en `mark_pbi_pr_merged`
MCP-tools blijven. Nieuwe action:
- `actions/jobs.ts``recordJobPushedAtAction(jobId)` voor
`pushed_at`-write (als die nog niet via MCP gaat).
### B.3 MCP-laag (`scrum4me-mcp`-repo)
Nieuwe tool:
- `check_pbi_complete(pbi_id) → { complete: boolean, pbi_id }`. Leest
alle ClaudeJobs gelinkt aan PBI; aggregeert status. `complete = true`
als **alle** story-jobs status DONE hebben.
Uitbreiding bestaande tool:
- `update_job_status`: bij `status: 'done'` ook `pushed_at` accepteren
(worker geeft timestamp door).
- `set_pbi_pr`: ongewijzigd, bestaat al.
Schema-drift watchdog (`docs/runbooks/mcp-integration.md`) moet groen
voor merge.
### B.4 Worker-laag (lokaal Claude-CLI worker)
Nieuwe stappen na elke story:
```
1. update_job_status('done', pushed_at: null) ← bestaand
2. git push https://$GITHUB_TOKEN@github.com/$OWNER/$REPO.git $BRANCH
3. record_pushed_at(job_id, now) ← nieuwe MCP-call
4. { complete } = check_pbi_complete(pbi_id)
5. if complete:
prNumber = POST /repos/.../pulls
set_pbi_pr(pbi_id, pr_url)
enablePullRequestAutoMerge(prNumber, MERGE_METHOD: SQUASH)
6. on any HTTP/git failure → update_job_status('failed', error)
```
GITHUB_TOKEN-scope: `repo` voor private, `public_repo` voor public.
Documenteer in worker-readme.
### B.5 Repo-instellingen (handmatig, one-time)
- GitHub repo Settings → General → "Allow auto-merge" → **aanvinken**.
- Branch protection op `main`: required CI checks = `ci`,
`deploy-preview` is **niet** required (kan skipped zijn door label).
---
## Deel C — Interactie & demo-policy
### C.1 Interactie deploy-controle ↔ auto-PR
| Scenario | Preview-deploy | Prod-deploy bij merge |
|--------------------------------------------------|----------------|------------------------|
| Worker maakt PR met code-changes (default) | ✅ runt | ✅ runt |
| Worker maakt PR met `skip-deploy` (manueel toegevoegd) | ❌ skipped | ✅ runt |
| Worker maakt PR met enkel docs-changes (path-filter) | ❌ skipped | ❌ skipped |
| User voegt `force-deploy` toe aan doc-only PR | ✅ runt | ✅ runt (path-filter) of ❌ (doc-only push) |
Auto-merge wacht op required CI checks. `deploy-preview` mag skipped
zijn — branch protection markeert hem niet als required.
### C.2 Demo-policy
Auto-PR-flow draait op de worker, niet vanuit de webapp. Geen
demo-sessie kan deze code triggeren — geen extra proxy.ts of
`session.isDemo`-guards nodig. Wel: `check_pbi_complete` MCP-call moet
`requireWriteAccess` doen (consistent met andere write-MCP-tools), zodat
demo-tokens hem niet kunnen aanroepen.
---
---
## Deel D — Sync-tab op Idea-detail (zicht op voortgang)
### D.1 Wat bestaat al
- `model StoryLog` (`prisma/schema.prisma:251`) met types
`IMPLEMENTATION_PLAN | TEST_RESULT | COMMIT`, plus `commit_hash`,
`commit_message`, `metadata`. **Dit is de activiteitenlog.**
- MCP-tools `log_implementation`, `log_commit`, `log_test_result`
schrijven naar deze tabel.
- UI-component `components/shared/story-log.tsx` rendert
`StoryLogEntry[]` met type-styling.
- `Story.status`, `ClaudeJob.pushed_at/branch/pr_url`,
`Pbi.pr_url/pr_merged_at` zijn al gevuld door bestaande flows.
Geen nieuwe tabellen, geen migraties.
### D.2 Nieuwe tab op `/ideas/[id]`
Voeg vijfde tab **Sync** toe (naast Idee · Grill · Plan · Timeline) op
Idea-detail-page. Alleen zichtbaar als `Idea.status === 'PLANNED'` en
`pbi_id` gevuld.
Layout per tab-content:
- Header: PBI-link + `pr_url` + `pr_merged_at` als badge.
- Per Story (volgorde uit PBI): collapsible card met:
- **Story-header**: code · titel · status-badge.
- **Job-rij**: voor elke `ClaudeJob` (kind=TASK_IMPLEMENTATION) gelinkt
aan een Task van deze Story → status, `branch`, `pushed_at`,
`pr_url`. Toont "geen job" als nog niets gequeued.
- **Activity-log**: `<StoryLog logs={logs} repoUrl={product.repo_url} />`
— bestaande component, ongewijzigd.
### D.3 Server-laag
Nieuwe loader in `app/(app)/ideas/[id]/page.tsx` (of nieuw
`sync-tab-server.ts`):
```ts
async function loadIdeaSyncData(ideaId: string, userId: string) {
// Auth-scope: idea.user_id === userId (M12-keuze 2)
return prisma.idea.findFirst({
where: { id: ideaId, user_id: userId },
include: {
pbi: {
include: {
stories: {
orderBy: { sort_order: 'asc' },
include: {
tasks: { include: { claude_jobs: true } },
logs: { orderBy: { created_at: 'desc' } },
},
},
},
},
},
})
}
```
Server-only. Nooit importeren in client component (zie hardstop
`*-server.ts` regel).
### D.4 Realtime refresh
Sync-tab abonneert op bestaande SSE-streams:
- `app/api/realtime/solo/route.ts``JobPayload` voor job-status-updates
(al uitgebreid met `kind` en `idea_id` per Deel B).
- `app/api/realtime/notifications/route.ts` — voor StoryLog-inserts; als
story_logs nog geen pg_notify-trigger heeft, voeg er een toe (nieuwe
migratie, payload `{op: 'INSERT', entity: 'story_log', id, story_id}`).
Op event → `router.refresh()` of `revalidate` van Sync-tab data.
### D.5 PBI-33 als live testgeval
PBI-33 is **nu** in TODO + gequeued als ClaudeJobs (gebruiker bevestigt:
"taken op TODO gezet en claude-job aangemaakt"). Verwacht gedrag zodra
deze sprint live is:
| Moment | Sync-tab toont |
|----------------------------|-----------------------------------------------|
| Job QUEUED | "Wachtend op worker" |
| Job RUNNING | Status RUNNING + log-entry IMPLEMENTATION_PLAN|
| Worker commit | log-entry COMMIT (hash + message) |
| Worker test | log-entry TEST_RESULT (status) |
| Worker push (Deel B AC 1) | `branch` + `pushed_at` zichtbaar |
| Laatste story → PR | PBI.`pr_url` zichtbaar |
| Auto-merge | PBI.`pr_merged_at` zichtbaar |
Als één van deze niet verschijnt: bug in MCP-tool of worker (niet in
sync-tab zelf).
---
## Bestanden
| Wijziging | Pad |
|-------------------|--------------------------------------------------|
| Edit | `vercel.json` |
| Edit | `.github/workflows/ci.yml` |
| Nieuw | `docs/runbooks/deploy-control.md` |
| Edit | `CLAUDE.md` (verwijzing toevoegen) |
| Nieuw (mcp-repo) | `src/tools/check-pbi-complete.ts` |
| Edit (mcp-repo) | `src/tools/update-job-status.ts` (pushed_at) |
| Edit | `actions/jobs.ts` (optioneel: record-pushed-at) |
| Edit | Worker-script (post-story-hook + PR-aanmaak) |
| Doc | `docs/runbooks/auto-pr-flow.md` (worker-flow) |
| Nieuw | `app/(app)/ideas/[id]/sync-tab-server.ts` |
| Nieuw | `components/ideas/idea-sync-tab.tsx` |
| Edit | `app/(app)/ideas/[id]/page.tsx` (5e tab toevoegen) |
| Migratie | `prisma/migrations/<ts>_story_logs_notify/migration.sql` (pg_notify-trigger op story_logs) |
| Edit | `app/api/realtime/notifications/route.ts` (story_log-payload doorlaten) |
| GitHub (extern) | Labels `skip-deploy`, `force-deploy` aanmaken |
| GitHub (extern) | Repo Settings → "Allow auto-merge" aan |
| Vercel-dashboard | `git.deploymentEnabled: false` actief verifiëren |
## Implementatievolgorde
1. **Deel A — Deploy-controle**
1. `vercel.json` aanpassen
2. `ci.yml` uitbreiden (path-filter, labels, dispatch)
3. Labels op GitHub aanmaken
4. Runbook + CLAUDE.md-verwijzing
5. Test-PR voor elk scenario (zie Verificatie)
2. **Deel D — Sync-tab** (kan parallel met B; alleen DB-reads + UI)
1. `loadIdeaSyncData` server-loader
2. `idea-sync-tab.tsx` component met `<StoryLog>`-hergebruik
3. 5e tab in `app/(app)/ideas/[id]/page.tsx`
4. pg_notify-trigger op `story_logs` + SSE-route uitbreiden
5. **Live test op PBI-33** (sprint loopt al — check of activity
verschijnt zodra worker logs schrijft)
3. **Deel B — Auto-PR**
1. MCP `check_pbi_complete` + `update_job_status(pushed_at)` PR
(parallel-repo, schema-drift-watchdog groen)
2. Worker-hook: push na done, PR + auto-merge bij complete
3. Repo-instelling "Allow auto-merge" aan
4. End-to-end smoke met één test-PBI
## Verificatie
Lokaal:
```bash
npm run lint && npm test && npm run build
```
Workflow-syntax:
```bash
gh workflow view ci.yml
```
End-to-end deploy-controle:
1. **Doc-only PR**`deploy-preview` skipped.
2. **Doc-only PR + `force-deploy`**`deploy-preview` runt.
3. **Code-PR + `skip-deploy`**`deploy-preview` skipped.
4. **Code-PR zonder labels**`deploy-preview` runt.
5. **Push naar `main` met code-change**`deploy-production` runt.
6. **Push naar `main` doc-only**`deploy-production` skipped.
7. **`workflow_dispatch` target=production** → manuele prod.
8. **Vercel dashboard** → geen auto-deploy bij geforceerde test-push.
End-to-end auto-PR:
9. Maak een test-PBI met 1 story + 1 task.
10. Worker draait → na `done`: `pushed_at` gevuld, branch op origin
zichtbaar.
11. `check_pbi_complete``complete: true`.
12. PR verschijnt op GitHub met titel = PBI-naam, body = story-list.
13. Auto-merge actief; CI groen → squash-merge.
14. `mark_pbi_pr_merged` getriggerd door `pull_request: closed`-webhook
(al bestaand) → `Pbi.pr_merged_at` gevuld.
15. Push-event op `main``deploy-production` runt (path-filter ja).
16. **Failure-test**: revoke GITHUB_TOKEN tijdelijk → push faalt →
`update_job_status('failed')` met error; geen PR aangemaakt.
## Out-of-scope (v1)
- UI-toggle voor `auto_pr` per product (veld bestaat, geen UI-wiring).
- GitHub App-installatie (per-repo tokens, scopes-finetuning).
- Multi-repo PBI's (huidig ontwerp: één `repo_url` per PBI).
- Force-push / non-fast-forward retry-flow.
- Notificaties (Slack, e-mail) bij merge of CI-failure.
- Rollback-flow bij gemergende regressie.
- Migratie naar `vercel.ts` (knowledge-update beveelt het aan; later).
- Auto-skip preview-deploy specifiek voor worker-PRs op basis van
product-instelling.

View file

@ -0,0 +1,499 @@
---
title: Docs-restructuur — geoptimaliseerd voor AI-lookup
status: proposal
audience: maintainer, ai-agent
language: nl
last_updated: 2026-05-02
related:
- CLAUDE.md
- AGENTS.md
- README.md
- docs/decisions/agent-instructions-history.md
---
# Plan — Docs-restructuur voor AI-lookup
> Doel van dit plan: de huidige documentatie- en instructielaag van Scrum4Me omzetten naar een structuur die een AI-agent (Claude Code, Codex, een MCP-worker) in zo min mogelijk tokens en tool-calls het juiste document laat vinden, lezen en toepassen — zonder dat de mens-leesbaarheid eronder lijdt.
Dit is een **proposal**, niet een afgerond ontwerp. Lees het, markeer wat je niet wil, en ik werk het uit naar een uitvoerbaar migratieplan met file-per-file diff.
---
## 1. Waarom dit plan
Een AI-agent doet voor élke beslissing typisch dit:
1. Leest `CLAUDE.md` (of `AGENTS.md`) volledig in context.
2. Scant `docs/` met `ls`/`grep`/`glob` om relevante bestanden te kiezen.
3. Leest één of meerdere docs volledig — vaak meer dan nodig, omdat doc-grenzen vaag zijn.
4. Vindt cross-refs (`zie docs/X#Y`) en herhaalt stap 3.
Elke stap kost tokens en latency. Als de bestandsboom, naamgeving of inhoud onduidelijk is, leest de agent te veel of het verkeerde — en maakt vervolgens beslissingen op verouderde of irrelevante informatie.
**Concrete kosten in deze repo (gemeten 2026-05-02):**
| Plek | Bestanden | Regels totaal | Grootste bestand |
|---|---:|---:|---|
| Root (CLAUDE.md, README.md, AGENTS.md, Brainstorm.md) | 4 | 679 | CLAUDE.md (340) |
| `docs/` (root, exclusief subdirs) | 13 | 5.873 | architecture.md (1.247) |
| `docs/patterns/` | 11 | 1.013 | dialog.md (387) |
| `docs/plans/` | 8 | 2.121 | M10-qr-pairing-login.md (885) |
| `.Plans/` (parallelle plan-historie) | 3 | ~600 | — |
| **Totaal** | **39** | **~10.700** | — |
Bij elke turn die met `CLAUDE.md` start, wordt minimaal 340 regels orientation in de context geladen — vóór er één regel code is gelezen. De agent kan vervolgens uit ~9.000 regels documentatie het juiste fragment moeten kiezen op basis van bestandsnamen alleen, want er is geen front-matter en geen index.
---
## 2. Wat ik aantrof — review per laag
### 2.1 Root-niveau orientation
| Bestand | Wat het doet vandaag | Probleem |
|---|---|---|
| `CLAUDE.md` (340 r) | Doel, doc-index, twee start-tracks, tech stack, UI-conventies, patroon-index, env vars, conventies, branch/commit-strategie, scrum-terminologie, MCP, deployment, DoD | Té breed: oriëntatie + harde regels + referentie-tabellen + procedures. Alles wordt elke turn geladen. |
| `AGENTS.md` (38 r) | Codex-variant van CLAUDE.md | Duplicatie: 80% overlapt met CLAUDE.md (access-control, doc-sync, verificatie). Twee waarheden die uit elkaar kunnen lopen. |
| `README.md` (285 r) | Portfolio-pitch, stack, setup, routes, dev-flow | Mensgericht (recruiters, GitHub-bezoekers). Goed dat het bestaat — niet aanraken. |
| `Brainstorm.md` (16 r) | Stukjes Prisma-schema, JSON-snippet en HTML-DOM-dump zonder context | **Dood bestand**, weghalen of verplaatsen naar `docs/scratch/`. |
### 2.2 `docs/` root
| Bestand | Regels | Waar het thuishoort |
|---|---:|---|
| `architecture.md` | 1.247 | `docs/architecture/` — splitsen (zie §4) |
| `functional.md` | 650 | `docs/specs/functional.md` |
| `backlog.md` | 751 | `docs/backlog/index.md` |
| `product-backlog.md` | 454 | `docs/backlog/product-historical.md` (referentie, zie noot in CLAUDE.md) |
| `personas.md` | 138 | `docs/specs/personas.md` |
| `styling.md` | 670 | `docs/design/styling.md` |
| `md3-color-scheme.md` | 941 | `docs/design/styling.md` (overlapt deels met `styling.md` — kandidaat voor merge) |
| `test-plan.md` | 454 | `docs/qa/api-test-plan.md` |
| `pbi-dialog.md` | 120 | `docs/specs/dialogs/pbi.md` |
| `story-dialog.md` | 163 | `docs/specs/dialogs/story.md` |
| `task-dialog.md` | 127 | `docs/specs/dialogs/task.md` |
| `solo-paneel-spec.md` | 771 | `docs/specs/solo-panel.md` |
| `api.md` | 520 | `docs/api/rest-contract.md` |
| `decisions/agent-instructions-history.md` | 173 | `docs/decisions/agent-instructions.md` (ADR-stijl) |
| `erd.svg`, `icons.html` | — | `docs/assets/` |
**Patroon dat opvalt:** alles met prefix `` — dat prefix is overbodig, je staat al in `docs/` van de Scrum4Me-repo. Verwijderen scheelt visuele ruis bij `ls`.
**Inconsistente capitalization:** één bestand `md3-color-scheme.md` (snake + UpperCamel), de rest kebab. Eén bestand `api.md` (UPPER), de rest lowercase.
### 2.3 `docs/patterns/`
11 patronen, elk 25390 regels. Goed als concept, maar:
- `test.md` bevat letterlijk het woord "test" — junkfile, weghalen.
- Geen front-matter; agent moet titel + intro lezen om te weten of het patroon van toepassing is.
- Naamgeving inconsistent: `iron-session.md`, `qr-login.md`, `claude-question-channel.md` — sommige domeinspecifiek, andere generiek. Geen prefix die laat zien of het een **rule** (verplicht), **recipe** (voorbeeldcode) of **explainer** is.
### 2.4 `docs/plans/`
- 8 plans. Eén heeft een filename met spaties en em-dash: `tweede-claude-agent-planning.md` — breekt grep/glob/git-workflows op sommige shells, en is moeilijk te linken. Omnoemen naar `tweede-claude-agent-planning.md`.
- ST-1109-pbi-status.md verwijst naar `~/.claude/plans/welke-rioriteiten-heeft-een-mighty-shell.md` (ook nog typo "rioriteiten") — externe locatie die niet in de repo zit en die de agent niet kan lezen.
- `MEMORY.md` wordt op meerdere plaatsen genoemd maar bestaat niet in de repo.
### 2.5 `.Plans/` (root)
3 historische planfiles uit april 2026, parallel aan `docs/plans/`. Twee waarheden voor "waar staan plannen". Voorstel: archiveren naar `docs/plans/archive/` of weghalen.
### 2.6 Cross-referenties en dode links
- CLAUDE.md verwijst naar `docs/architecture.md#demo-user-policy` — die anchor bestaat (regel 1068 `## Demo-user policy (ST-1110)`), dus dit is OK; maar er bestaat geen lint die garandeert dat dit zo blijft als de header verandert.
- ST-1109-pbi-status.md verwijst naar `~/.claude/plans/welke-rioriteiten-heeft-een-mighty-shell.md` — externe locatie buiten de repo, plus typo "rioriteiten". Een agent kan die niet lezen.
- README.md verwijst niet naar CLAUDE.md/AGENTS.md (mensbezoekers vinden de agent-instructie laag niet).
- Geen enkel doc heeft een "Zie ook"-blok aan de onderkant. Cross-navigatie tussen patroon ↔ spec ↔ plan moet de agent zelf reconstrueren.
### 2.7 Wat er níet is (en zou moeten)
- **Geen index/manifest.** Een agent die `glob "docs/**/*.md"` doet, krijgt 30+ paden zonder context.
- **Geen front-matter.** Geen status (draft/active/deprecated), audience, last-updated, related.
- **Geen ADR-laag.** Beslissingen zoals "waarom geen Radix maar @base-ui/react", "waarom float sort_order", "waarom one-branch-per-milestone" zitten verstrooid in CLAUDE.md, README en losse plans. Een `docs/decisions/`-folder met ADR-format zou ze vindbaar maken.
- **Geen glossary.** Domeintermen (PBI, Story, Sprint, Solo, Todo, demo-token) zijn alleen impliciet gedefinieerd in de functional spec.
- **Geen "lookup-hints" in de doc-index.** CLAUDE.md zegt *waarvoor* je een doc gebruikt, niet *wanneer je het NIET hoeft te lezen*.
---
## 3. Doelen voor de nieuwe structuur
In volgorde van belangrijkheid:
1. **Eén goedkope orientation-laag.** Een agent moet ≤150 regels lezen om te weten waar hij verder moet kijken.
2. **Voorspelbare paden.** `docs/<topic>/<entity-or-feature>.md` zonder uitzonderingen.
3. **Machine-leesbare metadata.** YAML-front-matter op élk doc met minimaal `status`, `audience`, `last_updated`, `related`.
4. **Per-doc lookup-hint.** Eén zin "Lees dit als …" bovenaan; één zin "Niet hiervoor lezen: …" om verkeerd ophalen te voorkomen.
5. **Splitsing van regels (verplicht) en uitleg (referentie).** Regels in een korte rule-doc; voorbeeldcode en rationale in een aparte recipe/explainer.
6. **Eén bron-van-waarheid per onderwerp.** Geen Codex-vs-Claude-duplicatie; AGENTS.md wordt een 10-regelige verwijzing naar CLAUDE.md.
---
## 4. Voorgestelde doelstructuur
```
/ (repo-root)
├── README.md (mens, portfolio — ongewijzigd)
├── CLAUDE.md (agent-orientation, ≤150 regels — zie §5)
├── AGENTS.md (10 regels: "alles in CLAUDE.md geldt ook voor jou")
├── docs/
│ ├── INDEX.md (NIEUW — manifest met front-matter van alle docs)
│ ├── glossary.md (NIEUW — PBI, Story, Sprint, demo-token, …)
│ │
│ ├── architecture/
│ │ ├── overview.md (uit huidige architecture.md §1§3)
│ │ ├── data-model.md (uit §Datamodel + §Prisma Schema)
│ │ ├── auth-and-sessions.md (uit §Authenticatieflow)
│ │ ├── qr-pairing.md (uit §QR-pairing flow)
│ │ ├── claude-question-channel.md (uit §Vraag-antwoord-kanaal)
│ │ └── project-structure.md (uit §Projectstructuur)
│ │
│ ├── specs/
│ │ ├── functional.md (huidige functional.md)
│ │ ├── personas.md
│ │ ├── solo-panel.md
│ │ └── dialogs/
│ │ ├── pbi.md
│ │ ├── story.md
│ │ └── task.md
│ │
│ ├── design/
│ │ ├── styling.md (samengevoegd uit styling + MD3-color)
│ │ └── color-tokens.md (alleen het token-overzicht)
│ │
│ ├── api/
│ │ ├── rest-contract.md (huidige api.md)
│ │ └── error-codes.md (afgesplitst — vandaag verspreid)
│ │
│ ├── patterns/ (RULES — kort en bindend)
│ │ ├── 00-conventions.md (server-action, prisma-client, route-handler — kort)
│ │ ├── dialog.md
│ │ ├── sort-order.md
│ │ ├── zustand-optimistic.md
│ │ ├── iron-session.md
│ │ ├── proxy.md (was middleware.md — nieuwe naam)
│ │ ├── qr-pairing.md
│ │ └── claude-question-channel.md
│ │
│ ├── recipes/ (NIEUW — uitgewerkte voorbeeldcode bij rules)
│ │ └── … (één recipe per pattern dat code-snippets had)
│ │
│ ├── runbooks/ (NIEUW — operationele procedures)
│ │ ├── deploy-vercel.md (uit CLAUDE.md §Deployment)
│ │ ├── env-vars.md (uit CLAUDE.md §Env vars + .env.example)
│ │ └── local-dev.md (huidige README §setup, geëxtraheerd)
│ │
│ ├── decisions/ (NIEUW — ADR-stijl)
│ │ ├── 0001-base-ui-over-radix.md
│ │ ├── 0002-float-sort-order.md
│ │ ├── 0003-one-branch-per-milestone.md
│ │ ├── 0004-status-enum-mapping.md
│ │ └── 0005-agent-instructions.md (was decisions/agent-instructions-history.md)
│ │
│ ├── backlog/
│ │ ├── index.md (huidige backlog.md)
│ │ └── product-historical.md (huidige product-backlog.md)
│ │
│ ├── plans/
│ │ ├── M9-active-product-backlog.md
│ │ ├── M10-qr-pairing-login.md
│ │ ├── M11-claude-questions.md
│ │ ├── ST-1109-pbi-status.md
│ │ ├── ST-1110-demo-readonly.md
│ │ ├── ST-1111-claude-job-trigger.md
│ │ ├── ST-1114-copilot-reviews.md
│ │ ├── tweede-claude-agent-planning.md (rename — geen spaties/em-dash)
│ │ └── archive/ (uit `.Plans/` aan repo-root)
│ │ ├── 2026-04-27-claude-md-workflow-update.md
│ │ ├── 2026-04-27-insert-milestone-tool.md
│ │ └── 2026-04-27-m8-realtime-solo.md
│ │
│ ├── qa/
│ │ └── api-test-plan.md
│ │
│ └── assets/
│ ├── erd.svg
│ └── icons.html
└── .Plans/ (WEG — naar docs/plans/archive/)
└── Brainstorm.md (WEG — junk, of naar docs/scratch/)
└── docs/patterns/test.md (WEG — junk)
```
**Prefix `` overal weg.** Je staat in de Scrum4Me-repo.
**Alle bestandsnamen kebab-case, lowercase.** Geen `api.md`, geen `MD3_…`.
---
## 5. CLAUDE.md herontwerp
CLAUDE.md wordt strikt **router + harde regels** — geen referentie-tabellen, geen voorbeelden, geen rationale.
Voorgestelde nieuwe inhoud (max ~150 regels):
```markdown
# CLAUDE.md — Scrum4Me
## 1. Wat is Scrum4Me
(2 zinnen — link naar README voor de pitch)
## 2. Eerst lezen, altijd
- docs/INDEX.md — manifest van alle docs
- docs/glossary.md — bij twijfel over een term
## 3. Hoe je werk vindt
Twee tracks (A: MCP, B: manueel) — verkort tot 10 regels.
Detail: docs/runbooks/sprint-flow.md
## 4. Hardstop-regels (nooit overtreden)
- demo-user heeft geen schrijfrechten (3-laagsdekking)
- @base-ui/react, niet Radix
- nooit bg-blue-500, altijd MD3-tokens
- één commit = één verantwoordelijkheid
- één branch per milestone, push pas na user-approval
- denormalized FKs uit DB-parent, niet uit client-input
(elk punt → 1 regel + link naar pattern/decision)
## 6. Stack op één regel per laag
(geen versie-uitleg, link naar docs/architecture/overview.md)
## 7. Snelreferentie patronen
| Wanneer | Lees |
|---|---|
| Server Action schrijven | docs/patterns/server-action.md |
| Drag-and-drop reorder | docs/patterns/sort-order.md |
| …(max 10 rijen)…|
## 8. Verificatie vóór hand-off
`npm run lint && npm test && npm run build`
```
Alles wat nu in CLAUDE.md §Conventies, §Branch & PR Strategy, §Commit Strategy, §MCP-integratie, §Deployment staat → verhuist naar:
- `docs/runbooks/branch-and-commit.md` (regels + voorbeelden samen)
- `docs/runbooks/deploy-vercel.md`
- `docs/runbooks/mcp-integration.md`
- `docs/decisions/0003-one-branch-per-milestone.md` (waarom)
CLAUDE.md houdt in §4 alleen de éénregelige regel + link.
**Effect:** elke turn 150 r in plaats van 340 r aan orientation-context. De agent leest aanvullende docs alleen wanneer de huidige taak ze raakt.
---
## 6. Front-matter spec
Élk markdown-bestand in `docs/` (en `CLAUDE.md`, `AGENTS.md`) krijgt bovenaan:
```yaml
---
title: <korte titel>
status: draft | active | deprecated
audience: ai-agent | maintainer | contributor | external
language: nl | en
last_updated: 2026-05-02
applies_to: [feature-of-module-of-milestone-keys] # optioneel
related:
- docs/<andere>.md
- CLAUDE.md
when_to_read: <één zin>
do_not_read_for: <één zin voorkomt mis-fetch>
---
```
**Waarom dit voor AI-lookup helpt:**
- `status: deprecated` → agent slaat het over zonder te lezen.
- `applies_to: [M10, qr-login]` → grep op milestone-key → directe hit.
- `when_to_read` / `do_not_read_for` → agent kan beslissen op de eerste 20 regels of dit doc nuttig is, zonder de hele 800-regelige spec in te lezen.
- `related` → expliciete graaf in plaats van impliciete cross-refs.
`docs/INDEX.md` wordt automatisch gegenereerd uit deze front-matter (klein script in `scripts/build-docs-index.ts` — onderdeel van het migratieplan).
---
## 7. AGENTS.md herontwerp
```markdown
# AGENTS.md
This repo's source of truth for agent instructions is **CLAUDE.md**.
Codex, Cursor, Continue, and any other coding agent: read CLAUDE.md first.
The same product, security, and verification rules apply regardless of which agent runs.
Repo-specific addendum (only if your agent does NOT speak markdown well):
- The "This is NOT the Next.js you know" note also applies to you.
- Run `npm run lint && npm test && npm run build` before handing work back.
```
Geen duplicatie van access-control of doc-sync — die regels staan exclusief in CLAUDE.md / `docs/patterns/` / `docs/decisions/`.
---
## 8. Migratie in fases
Elke fase is een eigen branch + PR. Geen big-bang. Volgorde gekozen zodat agents tijdens de migratie nog steeds werken.
### Fase 1 — Junk weg, front-matter erbij (laag risico)
- `docs/patterns/test.md` weghalen.
- `Brainstorm.md` weghalen of `docs/scratch/brainstorm-2026-05.md`.
- `.Plans/``docs/plans/archive/`.
- Front-matter toevoegen aan élk bestaand bestand (zonder verplaatsen). Status default = `active`.
- `docs/INDEX.md` genereren via script.
**Voor commit:** alle bestaande paden werken nog. Geen risico voor lopende sessies of CI.
### Fase 2 — Naamgeving normaliseren
- `` prefix overal weg via `git mv` (1 commit per groep — backlog/specs/personas/styling/dialogs).
- `api.md``api/rest-contract.md`.
- `md3-color-scheme.md``design/md3-color-scheme.md`.
- `tweede-claude-agent-planning.md``plans/tweede-claude-agent-planning.md`.
- `middleware.md``proxy.md` (volgt Next.js 16 hernoeming).
- Per `git mv`: in dezelfde commit zoek-en-vervang alle interne links + CLAUDE.md doc-index.
### Fase 3 — Folder-taxonomie
- Maak `docs/architecture/`, `docs/specs/`, `docs/design/`, `docs/api/`, `docs/runbooks/`, `docs/decisions/`, `docs/backlog/`, `docs/qa/`, `docs/assets/`.
- Verplaats per groep met `git mv`. Eén commit per groep.
- Update CLAUDE.md doc-index per stap.
### Fase 4 — Splits monolithische docs
- `architecture.md` (1.247 r) opdelen in 6 docs onder `docs/architecture/`.
- Originele file wordt 20 regels: titel + "Dit document is opgesplitst — zie:" + lijst met nieuwe paden.
- Idem voor `solo-paneel-spec.md` als dat onderdelen heeft die naar specs én patterns kunnen.
### Fase 5 — CLAUDE.md verkorten + AGENTS.md verkorten
- Knip CLAUDE.md naar het skelet uit §5.
- Verplaats verwijderde secties naar `docs/runbooks/` en `docs/decisions/`.
- AGENTS.md vervangen door de versie uit §7.
### Fase 6 — ADR-backfill
- Schrijf ADR's voor de impliciete beslissingen (58 stuks):
1. base-ui-over-radix
2. float-sort-order
3. one-branch-per-milestone
4. status-enum-mapping (db UPPER ↔ api lower)
5. iron-session-over-nextauth
6. demo-user-policy (3-laags)
7. claude-question-channel-design
8. agent-instructions-policy (was audit)
### Fase 7 — Glossary + index-script
- `docs/glossary.md` schrijven (PBI, Story, Sprint, Solo, Todo, demo-token, MCP-job, …).
- `scripts/build-docs-index.ts` — genereert `docs/INDEX.md` uit alle front-matters.
- Husky pre-commit hook: index regenereren bij wijziging van front-matter.
### Fase 8 — Cross-link-check
- Klein script dat alle `docs/...md` links volgt en rapporteert dode links én anchor-misses.
- Toevoegen aan `npm run lint` of `npm test`.
---
## 9. Wat dit oplevert (meetbaar)
| Metric | Vandaag | Doel |
|---|---:|---:|
| Regels die elke agent-turn standaard in context komen (CLAUDE.md) | 340 | ≤150 |
| Doc-bestanden in `docs/` root | 13 | 2 (INDEX.md, glossary.md) |
| Doc-bestanden zonder front-matter | 36 | 0 |
| Junk-bestanden | 3 (test.md, Brainstorm.md, .Plans/) | 0 |
| Bestandsnamen met spaties of niet-ASCII | 1 | 0 |
| Filename-prefixen die geen informatie toevoegen (``) | 8 | 0 |
| Documenten >800 regels | 4 | 0 (na splitsing) |
| Dode interne links | onbekend | 0 (na lint) |
---
## 10. Wat dit níet oplevert (eerlijk)
- Codequaliteit verbetert niet automatisch.
- Patronen die nu fout zijn worden niet gefixt — alleen vindbaar gemaakt.
- ADR's invullen kost denkwerk dat ik niet uit jouw hoofd kan halen — fase 6 vereist jouw input.
- AI-agents die geen front-matter parseren (oudere modellen, sommige codex-flavors) profiteren minder. Voor de `docs/INDEX.md` is het wel platte tekst — die helpt iedereen.
---
## 11. Open beslissingen — status
| # | Vraag | Besluit (2026-05-02) |
|---|---|---|
| 1 | Taal van docs, front-matter, INDEX.md | **English** — alle nieuwe en herschreven docs in het Engels. Code comments blijven Engels (al zo). UI-strings blijven Nederlands. |
| 2 | MD3-color + styling samenvoegen | **Eén doc**`docs/design/styling.md`. |
| 3 | `solo-paneel-spec.md` | **Samenvoegen** — opgaan in `docs/specs/functional.md` (eigen sectie). |
| 4 | `.Plans/` archief | **Bewaren** — verplaatsen naar `docs/plans/archive/`. |
| 5 | ADR-template (Nygard vs MADR) | **In discussie** — referentielink gedeeld, ik heb die niet kunnen openen (claude.ai share staat niet op de fetch-allowlist). Default voor fase 6: Nygard, klein en passend bij solo + kleine repo. Vervangbaar zodra besluit valt. |
| 6 | Index-generator | **Node-script** in `scripts/build-docs-index.ts`. |
## 12. Implicaties van besluit 1 — taalwissel
De keuze "alle docs Engels" is groter dan hij lijkt. Drie scope-niveaus:
**Niveau A — going-forward only (kleinste scope):**
- Élk nieuw doc en élke nieuw aangemaakte front-matter in het Engels.
- Dit plan, INDEX.md, glossary.md, runbooks/, decisions/ — allemaal Engels vanaf creatie.
- Bestaande Nederlandse docs blijven staan tot ze om een andere reden geraakt worden.
- **Risico:** mengvormen in docs/ — een agent vindt ene helft Engels, andere helft Nederlands. Grep op een Engels keyword mist Nederlandse hits.
**Niveau B — opportunistic (middel):**
- Niveau A + élke doc die we aanraken voor de restructuur (renames, splitsingen, front-matter toevoegen) wordt meteen vertaald.
- Aan het eind van fase 5 zijn `architecture/`, `specs/`, `design/`, `api/`, `patterns/`, `runbooks/`, `decisions/` allemaal Engels.
- Backlog, plans en QA blijven Nederlands tenzij ze ge-edit worden.
**Niveau C — full sweep:**
- Élke `.md` in de repo vertalen, ongeacht of de restructuur hem aanraakt.
- Aanzienlijk werk: ~10.700 regels prose. Schatting: 1-2 dagen agent-tijd of een batch-translate-pass met review.
**Voorstel: niveau B.** Sluit aan bij de migratiefases zonder een aparte translation-sprint te hoeven plannen. Niveau C kan later als één losse PR.
## 13. ADR-template — voorlopige keuze
Tot besluit valt op vraag 5: ik gebruik **Nygard-Light** als template voor fase 6. Eén ADR is één bestand met:
```markdown
---
title: <decision title>
adr_number: 0001
status: proposed | accepted | superseded by 00xx
date: 2026-05-02
---
# 0001. <decision title>
## Context
Why this decision matters now. What forces are in play.
## Decision
The choice we make. One paragraph, declarative.
## Consequences
What becomes easier, what becomes harder, what we accept.
```
Compact, grep-vriendelijk, en agent-leesbaar binnen ~30 regels. Als MADR de uitkomst wordt, swappen we het template voor fase 6 — alle eerdere fases zijn template-onafhankelijk.
## 14. Volgende stap
Met deze besluiten kan ik fase 1 omzetten naar een concrete uitvoeringslijst:
- exacte `rm` / `git mv` / `mkdir` commando's
- de front-matter-template in het Engels
- één `npm run` of bash-snippet die de hele fase in één commit zet
- bijbehorende update-diff voor CLAUDE.md (alleen de doc-index-tabel)
Zeg het woord en ik produceer dat als `docs/plans/docs-restructure-phase-1.md`.
---
## Verificatie van dit plan
- [x] Bestandsnamen en regelaantallen gecheckt tegen `find docs -type f` + `wc -l` op disk (2026-05-02).
- [x] Cross-refs in CLAUDE.md gegrep't en op bestaan getoetst.
- [x] Geen voorgestelde nieuwe paden conflicteren met bestaande.
- [x] Open beslissingen (§11) afgehandeld door maintainer (2026-05-02) — vraag 5 voorlopig op default.
- [ ] ADR-template definitief vastgesteld (vraag 5).
- [ ] Migratie fase 1 omgezet naar uitvoerbaar PR-plan (`docs/plans/docs-restructure-phase-1.md`).

View file

@ -0,0 +1,205 @@
# Scrum4Me-Research — Zustand rearchitecture (reset + execute)
> **Scope:** dit plan is geschreven voor de research-repo
> [`madhura68/Scrum4Me-Research`](https://github.com/madhura68/Scrum4Me-Research),
> niet voor dit hoofdproject. Bestandsverwijzingen die naar
> `stores/data-store.ts`, `hooks/use-event-stream.ts`,
> `components/*-select.tsx` etc. wijzen, bestaan in de research-repo —
> niet hier. Ze staan in `code`-tags zodat de doc-link-checker ze niet
> probeert te resolven.
## Context
Het bestaande [zustand-store-rearchitecture.md](./zustand-store-rearchitecture.md) beschrijft een doel-architectuur (`product-workspace-store` met genormaliseerde entities, race-safe loaders, resync-laag, optimistic mutations). De research-repo is dé plek om dat eerst te testen voordat het in `Scrum4Me/` belandt.
Probleem nu: de research-repo wijkt af van het hoofdproject. Mijn custom `data-store.ts` lijkt qua vorm op de doel-architectuur, maar springt over de baseline heen. We willen aantonen dat de migratie *vanaf* de huidige Scrum4Me-patronen werkt, niet vanaf een verzonnen tussenvorm.
Dus: eerst de research-repo terugbrengen naar dezelfde stores/hooks/routes als Scrum4Me nu heeft, dan de rearchitecture daarop uitvoeren.
## Bron-documenten
- **Doel-architectuur**: [zustand-store-rearchitecture.md](./zustand-store-rearchitecture.md) (in research-repo). Dit plan voert dat document uit; herhaalt het niet.
- **Conventies**: [CLAUDE.md](../../CLAUDE.md) hoofdproject. Taal NL, MD3 tokens, `@base-ui/react` render-prop, `*-server.ts`, enum UPPER_SNAKE↔lowercase via `lib/task-status.ts`.
## Drie-faseplan
### Fase A — Reset naar Scrum4Me-patronen
Doel: onze research-pagina werkt op exact dezelfde store/hook/route-vorm als het hoofdproject, met identiek gedrag.
**Verwijderen** (research-repo):
- `stores/data-store.ts` (research-repo) — mijn megastore
- `hooks/use-event-stream.ts` (research-repo) — vervangen door `use-backlog-realtime.ts`
- `hooks/use-browser-presence.ts` (research-repo) — niet in main, drop voor reset
- `app/api/realtime/events/route.ts` (research-repo) — vervangen door `app/api/realtime/backlog/route.ts`
- Mijn custom `loadX/resyncAll`-paden in selectie-componenten
**Kopiëren uit `/Users/janpetervisser/Development/Scrum4Me/`** (1-op-1 of stripped van auth):
| Bron | Doel |
|---|---|
| `stores/backlog-store.ts` | `stores/backlog-store.ts` (`pbis`, `storiesByPbi`, `tasksByStory`; `setInitialData`, `applyChange`) |
| `stores/planner-store.ts` | `stores/planner-store.ts` (DnD-order; voor research nog niet gebruikt maar we zetten 'm klaar) |
| `stores/selection-store.ts` | overschrijf bestaand (state: `selectedPbiId`, `selectedStoryId`, geen taskId/productId in main; add `selectedTaskId` + `productId` als research-uitbreiding) |
| `stores/product-store.ts` | `stores/product-store.ts` (`currentProduct`) |
| `stores/products-store.ts` | `stores/products-store.ts` (lijst, voor pulldown) |
| `lib/realtime/use-backlog-realtime.ts` | `lib/realtime/use-backlog-realtime.ts` (SSE-client → `applyChange` op backlog-store) |
| `lib/task-status.ts` | `lib/task-status.ts` (enum-converters) |
| `app/api/realtime/backlog/route.ts` | `app/api/realtime/backlog/route.ts` (SSE+LISTEN, **research-only: strip auth/session/getAccessibleProduct** — vraagt enkel `product_id` querystring) |
> ⚠️ **Auth-strip is research-only.** Het hoofdproject MOET sessie + `getAccessibleProduct()`-check op SSE en read-routes behouden. Bij backport vanaf de research-repo nooit de geknipte route 1-op-1 overnemen. Dit geldt voor zowel `app/api/realtime/backlog/route.ts` als alle read-routes onder `app/api/products/...`, `/pbis/...`, `/stories/...`, `/tasks/...`.
**API-routes (read)** — bestaande paden behouden, alleen `force-dynamic` blijft:
- `GET /api/products` (list voor pulldown)
- `GET /api/products/[id]/pbis` (open: READY+BLOCKED)
- `GET /api/pbis/[id]/stories`
- `GET /api/stories/[id]/tasks`
- `GET /api/tasks/[id]`
**Componenten herschrijven**:
- `components/product-select.tsx` (research-repo) → leest `useProductsStore`, schrijft naar `useProductStore.setCurrentProduct`
- `components/pbi-select.tsx` (research-repo) → leest `useBacklogStore` (filter op currentProduct), `useSelectionStore.selectPbi`. Triggert fetch op product-mount via een `useBacklogLoader`-helper die initial data binnenhaalt.
- `components/story-select.tsx` (research-repo) → idem voor stories
- `components/tasks-table.tsx` (research-repo) → leest `tasksByStory[selectedStoryId]`. **Max 10 rijen, scrollbaar** (al ingebouwd, behouden)
- `components/task-detail-card.tsx` (research-repo) → fetcht task detail apart (geen full-fat backlog veld; matcht main's `tasks/[id]` route)
- `components/event-stream-panel.tsx` (research-repo) → blijft bestaan voor research-doel (event-tap), maar luistert nu mee op dezelfde EventSource via `use-backlog-realtime` (of een tweede readonly listener); selecteerbare events met JSON-detail rechts blijven. Twee checkboxes (Postgres / Browser). Truncate met ellipsis in de lijst.
**Werkwijzen (verifiëren tijdens reset)**:
- Comments en UI-tekst NL
- Geen `bg-blue-500` etc; enkel MD3 tokens (`bg-primary`, `bg-card`, `bg-status-done`, ...)
- shadcn-componenten al `base-nova` style
- Server-only files krijgen `*-server.ts` suffix waar van toepassing (in deze fase niet nodig — alle DB-toegang loopt via `lib/prisma.ts` in route handlers)
- TaskStatus-mapping via `lib/task-status.ts` als de UI lowercase wil
**Acceptatie Fase A**:
- `npx tsc --noEmit` schoon
- Pagina rendert, cascading werkt, tabel toont taken, detail-card vult, events stromen door (preview-verificatie)
- Stores matchen het hoofdproject qua vorm (vergelijking via `diff` uitvoerbaar voor backlog-store etc.)
### Fase B — Rearchitecture uitvoeren
Volgt de 15 stappen uit [zustand-store-rearchitecture.md](./zustand-store-rearchitecture.md) §Implementatiepad. Concreet voor de research-repo:
1. **Map** `stores/product-workspace/` aanmaken (factory + provider + selectors).
2. **`activeProduct`** wordt nu nog gespiegeld vanuit `useProductStore`; voor de research-pagina geen layout/server-side bepaling — we lezen het uit de pulldown-state.
3. **Selection migreren**`selection-store``context.{activePbiId, activeStoryId, activeTaskId}` + `productId`. Setters cascaden de reset naar children (zoals doc beschrijft).
4. **Backlog naar entities + relations**`pbisById`, `storiesById`, `tasksById`, `pbiIds`, `storyIdsByPbi`, `taskIdsByStory`. Selectors:
- `selectVisiblePbis(productId)`
- `selectStoriesForActivePbi(state)`
- `selectTasksForActiveStory(state)`
- `selectActivePbi/Story/Task(state)`
5. **Planner-state** in dezelfde workspace-store landen (`relations` slice); voor research: niet actief gebruikt, wel structureel meekoppen.
6. **Race-safe loaders**`ensureProductLoaded`, `ensurePbiLoaded`, `ensureStoryLoaded`, `ensureTaskLoaded` met `requestId`-guard. Implementatie:
```ts
setActivePbi(pbiId) {
const requestId = crypto.randomUUID()
set({ context: { ..., activePbiId: pbiId, ... }, loading: { ..., activeRequestId: requestId } })
void get().ensurePbiLoaded(pbiId, requestId)
}
// in ensure: if (get().loading.activeRequestId !== requestId) return
```
7. **localStorage = restore hints**`lastActivePbiIdByProduct`, `lastActiveStoryIdByProduct`, `lastActiveTaskIdByProduct`. Niet de waarheid, alleen hint die getoetst wordt aan toegankelijkheid.
8. **`use-backlog-realtime` dispatcht naar `applyRealtimeEvent`** — store interpreteert pbi/story/task I|U|D events, doet upsert + parent-id move + sort.
9. **Hidden tab beleid**`EventSource` openhouden bij `hidden`. Op `visible``resyncActiveScopes('visible')`.
10. **Reconnect resync** — bij `ready` na disconnect of na exponential backoff: `resyncActiveScopes('reconnect')`.
11. **Unknown-event fallback** — onbekend event met `payload.product_id === activeProductId``resyncActiveScopes('unknown-event')`. Dit is wat het "veel events maar geen update" issue oplost.
12. **`force-dynamic` + `cache: 'no-store'`** — al gedaan in mijn fixes; behouden bij reset en versterken.
13. **Componenten naar selectors** — backlog-componenten lezen via `selectStoriesForActivePbi` etc., niet via raw store-velden.
14. **Tests** (Vitest, conform main):
- hydrate snapshot
- active selectie cascade
- race-safe ensure (laat trage promise van oude selectie geen nieuwe data overschrijven)
- SSE I|U|D voor pbi/story/task
- parent-move (story verandert van pbi)
- hidden→visible resync
- reconnect resync
- unknown-event resync
- delete-cleanup van actieve selectie
- localStorage restore-hint validatie tegen toegankelijkheid
15. **Sprint-workspace** — buiten scope; flag voor latere herhaling.
**Optimistic mutations** (§Optimistic in doc): voor research geen DnD, dus alleen het patroon dropunten en niet bouwen. Wel: `applyOptimisticMutation`-action wel klaarzetten in de store-API zodat het patroon zichtbaar is.
### Fase C — Werkwijzen verweven en doortrekken
**Tijdens Fase A én B respecteren**:
1. **Plan mode workflow** — eerst Plan, ExitPlanMode, dan code. Bij grote wendingen opnieuw plannen.
2. **TodoWrite** voor multi-step werk; markeer immediate completion.
3. **Verify via preview** voor elke observable verandering (de hook reminder doet dit al).
4. **`tsc --noEmit`** voor afronden van een stap.
5. **Comments/Dutch** consistent. WHY-comments over de invariant; geen WHAT-comments.
6. **MD3 tokens** alleen.
7. **Geen secrets in chat**`.env.local` blijft lokaal.
8. **Niet schrijven naar shared DB** zonder expliciete user-toestemming (geen `pg_notify` op shared channel).
9. **Source of truth = DB**. Zustand is projectie. localStorage = hint.
10. **Vóór elke fase**: kort statusrapport in de chat met wat er aankomt en waarom.
**Doortrekken naar hoofdproject** (out-of-scope deze run, maar geflagd):
- Na bewezen werking in research-repo: backport `product-workspace-store` + selectors + realtime-apply + resync-laag naar `Scrum4Me/stores/product-workspace/`.
- **Niet backporten**: de auth-stripped routes uit research. Main behoudt iron-session, `getAccessibleProduct()`, en alle product-access/sprint/personal filters in z'n SSE- en read-routes.
- **Wel backporten**: store-shape, selectors, race-safe `ensure*Loaded`, hidden-tab beleid, `resyncActiveScopes`, unknown-event fallback, restore-hint patroon, `force-dynamic` + `cache: 'no-store'`.
- Migratie main-project zal langer duren (DnD, sprint, jobs, tests). Apart plan.
## Bestandsmutaties (overzicht)
### Verwijderen na Fase A
- `stores/data-store.ts` (research-repo)
- `hooks/use-event-stream.ts` (research-repo)
- `hooks/use-browser-presence.ts` (research-repo) — komt deels terug in Fase B als helper voor visibility/online resync trigger
- `app/api/realtime/events/route.ts` (research-repo)
### Toevoegen Fase A (uit Scrum4Me)
- `stores/backlog-store.ts`
- `stores/planner-store.ts`
- `stores/selection-store.ts` (overschrijf)
- `stores/product-store.ts`
- `stores/products-store.ts`
- `lib/realtime/use-backlog-realtime.ts`
- `lib/task-status.ts`
- `app/api/realtime/backlog/route.ts` (zonder auth)
### Toevoegen Fase B (nieuw, conform doc)
- `stores/product-workspace/store.ts` (zustand factory)
- `stores/product-workspace/selectors.ts`
- `stores/product-workspace/types.ts`
- `stores/product-workspace/restore.ts` (localStorage hints)
- `stores/product-workspace/realtime-apply.ts` (SSE event → store)
- `stores/product-workspace/resync.ts` (`resyncActiveScopes`, `resyncLoadedScopes`)
- `tests/product-workspace/*.test.ts` (Vitest, install vitest als devDep)
### Te aanpassen in Fase B
- Alle `components/*.tsx` (nu shadcn select/table/card panels) → consumeren via selectors uit workspace-store
- `lib/realtime/use-backlog-realtime.ts` → dispatcht `applyRealtimeEvent` naar workspace-store i.p.v. `applyChange` naar backlog-store
- `event-stream-panel.tsx` → blijft bestaan (research-tap), maar leest events ook uit workspace-store of via een dunne `event-log-store` ernaast (in bounded-context-stijl: aparte log-store voor pure observatie hoort er niet thuis in de workspace-store)
## Verificatie
### Na Fase A (baseline)
1. `npm run dev` op port 3001
2. Pagina laadt, cascading werkt: product → PBI → story → tasks
3. Detail-card vult bij klik op task
4. Event-paneel toont realtime events (truncate + JSON-detail)
5. `npx tsc --noEmit` schoon
6. Vergelijk: `diff Scrum4Me/stores/backlog-store.ts Scrum4Me-Research/stores/backlog-store.ts` → identiek (modulo lokale interface-uitbreidingen waar gedocumenteerd)
### Na Fase B (target)
Alle acceptatiecriteria uit [zustand-store-rearchitecture.md §Acceptatiecriteria](./zustand-store-rearchitecture.md):
- Eén waarheid per entity in de store ✓
- Selectors als enige UI-leesweg ✓
- SSE patcht zonder full-page refresh ✓
- Hidden→visible herstelt missers binnen één resync-cyclus ✓
- Reconnect resync werkt zonder NOTIFY-replay ✓
- Directe task-edits zonder `entity:'task'` NOTIFY worden via unknown-event fallback zichtbaar ✓
- LocalStorage = hint, geen forced state ✓
- `force-dynamic` + `cache: 'no-store'` overal ✓
### Manuele preview-verificatie (na elke fase)
- TODO via TodoWrite tijdens uitvoer; preview-screenshot na grote stappen
- Tab-switch test: open page, switch tab, doe een wijziging via een ander mechanisme (psql na user-akkoord, of UI in main-project), keer terug → verwacht: zonder warnings + data gerefresht
## Open vragen / risico's
1. **Reset-import** uit hoofdproject: voor de research-repo strippen we auth/session-deps uit de gekopieerde routes (research-repo heeft geen auth-laag, draait lokaal). Belangrijk: **dit is een research-repo-keuze; main behoudt de volledige auth-filters**. Zie de waarschuwing onder "API-routes (read)" hierboven.
2. **`use-backlog-realtime` heeft mogelijk auth-headers/session-checks**: bevestigen tijdens copy. Indien zo: research-versie gebruikt geen auth, route is publiek-bereikbaar binnen lokale dev. Geldt alleen lokaal — geen wijziging aan main.
3. **Tests-deps** (vitest, @testing-library/react) toevoegen tijdens Fase B. Of pas in Fase B step 14 vanwege scope.
4. **Event-paneel toekomst**: blijft het in research-repo of stoten we het af zodra de workspace-store af is? Voorstel: behouden als observatie-tool, maar er aparte `event-log-store` (kleine UI store) voor maken zodat het niet meelift in de workspace-store.
5. **README.md** update na Fase B (optioneel) — kort beschrijven dat dit nu het canonical migratie-pad demonstreert.

View file

@ -0,0 +1,212 @@
---
title: "User-settings store (DB-backed user prefs)"
status: draft
audience: [contributor, ai-agent]
language: nl
last_updated: 2026-05-10
---
# User-settings store (DB-backed user prefs)
> **Locatie na approval:** verhuis dit bestand naar `docs/plans/user-settings-store.md` in de repo.
> Trigger voor dit plan: zichtbare hydratie-flits op het sprint-scherm in v1.3.3 ([PR #184](https://github.com/madhura68/Scrum4Me/pull/184)).
> De fix daar (useEffect-hydratie + `prefsLoaded`-gate) is een tijdelijke patch; deze migratie elimineert de flits volledig.
## Context
Filter- en view-prefs zitten nu verspreid over `localStorage` (en deels cookies).
Bij SSR weet de server niets van `localStorage`, dus bij users met saved-state ≠
default ontstaat één render-flits direct na hydratie. Daarnaast werken die prefs
alleen per browser — geen cross-device, en cross-tab-sync vereist `storage`-events.
Doel: **één `User.settings` JSON-veld** als single source of truth, met:
- Server-component leest het veld bij elke page-render → SSR-correct, geen flits
- Zustand-store met optimistic updates patroon (zoals `product-workspace-store`)
- Cross-tab sync via bestaande `LISTEN/NOTIFY` + SSE-bridge
- Cross-device persistence (login op andere browser/laptop ziet zelfde prefs)
---
## Scope (gefaseerd)
### Fase 0 — Infrastructuur
Aparte PR. Geen UI-wijziging; legt het fundament. Resultaat is een werkende store
zonder migraties; bestaande localStorage-flow blijft intact tot Fase 1.
| # | Bestand | Wat |
|---|---|---|
| 0.1 | `prisma/schema.prisma` | `settings Json @default("{}")` op `User` model + migration |
| 0.2 | `lib/user-settings.ts` | Zod-schema + types + `mergeSettings(prev, patch)` deep-merge helper + defaults |
| 0.3 | `actions/user-settings.ts` | `updateUserSettingsAction(patch: Partial<UserSettings>)` — auth-guard, Zod-validate, deep-merge in DB transactie, `NOTIFY scrum4me_changes 'user_settings:${userId}'` |
| 0.4 | `stores/user-settings/store.ts` | Zustand met `entities.settings: UserSettings`, `hydrate(initial)`, generieke `setPref(path, value)` met optimistic + rollback. Zelfde mutation-flow als `product-workspace-store` |
| 0.5 | `app/api/realtime/user-settings/route.ts` | SSE-route per user, `LISTEN user_settings:${userId}`, push patches |
| 0.6 | `components/shared/user-settings-bridge.tsx` | Server reads `prisma.user.findUnique({select:{settings:true}})`, geeft door als prop, client mount roept `store.hydrate()` aan + opent SSE |
| 0.7 | Mount in `app/(app)/layout.tsx` | Bridge bovenin de app-layout zodat de store altijd beschikbaar is voor alle authenticated pagina's |
| 0.8 | Tests | `__tests__/lib/user-settings.test.ts` (merge-logic), `__tests__/actions/user-settings.test.ts` (auth + validation), `__tests__/stores/user-settings.test.ts` (optimistic flow) |
**Demo/anon-fallback:** `useUserSettingsStore` detecteert `session.isDemo` of geen `userId`
en valt terug op in-memory state (geen server-write). Bridge wordt voor demo niet
gemount — defaults blijven actief, geen persistence-verwachting.
### Fase 1 — Migreer huidige flits-bronnen
| Component | localStorage-keys | → `settings`-pad |
|---|---|---|
| `components/sprint/sprint-backlog.tsx` | `scrum4me:sprint_pb_*` (6) | `views.sprintBacklog.{filterPriority,filterStatus,sort,sortDir,collapsedPbis,filterPopoverOpen}` |
| `components/backlog/pbi-list.tsx` | `scrum4me:pbi_*` (4) | `views.pbiList.{sort,filterPriority,filterStatus,sortDir}` |
| `components/backlog/story-panel.tsx` | `scrum4me:story_sort` (1) | `views.storyPanel.sort` |
| `components/jobs/jobs-column.tsx` | `${prefix}_filter_kind`, `${prefix}_filter_status` (2 dyn.) | `views.jobsColumns[prefix].{kinds,statuses}` |
| `stores/debug-store.ts` (via `status-bar-debug-toggle`) | `scrum4me:debug-mode` (1) | `devTools.debugMode` |
Per component:
- Verwijder `useState` + `useEffect`-hydratie + `useEffect`-write
- Vervang door `useUserSettingsStore(s => s.entities.settings.views.sprintBacklog?.filterStatus ?? 'OPEN')`
- Setter wordt `useUserSettingsStore.getState().setPref(['views','sprintBacklog','filterStatus'], value)`
- `prefsLoaded`-state en helpers (`readLocalStoragePref`) verdwijnen
- `lib/use-local-storage-pref.ts` wordt verwijderd (niet meer in gebruik)
**Migratie-pad voor bestaande users:** bij eerste mount, voor de eerste `setPref`-call,
leest een one-shot `useEffect` de oude localStorage-keys en pusht ze als één bulk-patch
naar de server. Daarna `localStorage.removeItem(...)` om geen verwarring te wekken.
Idempotent: als `settings.views.sprintBacklog.filterStatus` al gezet is, sla over.
### Fase 2 — Cookie-consolidatie (optioneel, later PR)
| Bron | Huidig | → `settings`-pad |
|---|---|---|
| `components/shared/split-pane.tsx` | `document.cookie` (`sp:` prefix) | `layout.splitPanePositions[cookieKey]` |
| `lib/active-sprint.ts` + `actions/active-sprint.ts` | server-side cookie per product | `layout.activeSprints[productId]` |
Server-component-lezers veranderen — apart traject met meer regression-risico.
Niet onderdeel van de eerste user-settings-PR.
### Fase 3 — Skip / al persistent
- `idea-md-editor.tsx` drafts — werk-in-progress, geen pref
- `iron-session` cookies — auth, andere zorg
- `User.active_product_id` — al in DB (kolom op model)
- Modal/popover open-state behalve `filterPopoverOpen` — ephemeral
---
## JSON-shape (Fase 1)
```ts
// lib/user-settings.ts
import { z } from 'zod'
export const UserSettingsSchema = z.object({
views: z.object({
sprintBacklog: z.object({
filterPriority: z.union([z.number().int().min(1).max(4), z.literal('all')]).optional(),
filterStatus: z.enum(['OPEN', 'IN_SPRINT', 'DONE', 'all']).optional(),
sort: z.enum(['priority', 'status', 'code']).optional(),
sortDir: z.enum(['asc', 'desc']).optional(),
collapsedPbis: z.array(z.string()).optional(),
filterPopoverOpen: z.boolean().optional(),
}).optional(),
pbiList: z.object({
sort: z.enum(['priority', 'code', 'date']).optional(),
filterPriority: z.union([z.number().int().min(1).max(4), z.literal('all')]).optional(),
filterStatus: z.enum(['ready', 'blocked', 'done', 'all']).optional(),
sortDir: z.enum(['asc', 'desc']).optional(),
}).optional(),
storyPanel: z.object({
sort: z.enum(['priority', 'code', 'date']).optional(),
}).optional(),
jobsColumns: z.record(z.string(), z.object({
kinds: z.array(z.string()),
statuses: z.array(z.string()),
})).optional(),
}).optional(),
devTools: z.object({
debugMode: z.boolean().optional(),
}).optional(),
}).strict()
export type UserSettings = z.infer<typeof UserSettingsSchema>
```
Defaults zijn impliciet (alle keys optioneel). Selectors in de store geven
fallback-waardes terug zodat consumers niet `?? 'OPEN'` hoeven te schrijven —
maar het mag, geen big deal.
---
## Realtime-notificatie
Bestaand kanaal `scrum4me_changes` blijft. Payload-conventie:
```json
{ "kind": "user_settings", "userId": "...", "patch": { "views": { ... } } }
```
`/api/realtime/user-settings/route.ts` filtert payloads op `userId === session.userId`.
Andere tabs van zelfde user krijgen patches binnen, store roept `applyServerPatch(patch)`
aan zonder optimistic flow.
---
## Verificatie (per fase)
### Fase 0
- [ ] `npm run verify && npm run build` groen
- [ ] Migration draait op fresh + bestaande DB zonder data-verlies
- [ ] `updateUserSettingsAction` weigert auth-loze calls (test)
- [ ] Zod-validatie geeft 422 bij invalid patch (test)
- [ ] Optimistic update + rollback gedraagt zich zoals `product-workspace-store` (test)
- [ ] SSE-route levert patches alleen aan zelfde user (manueel: open twee tabs als A, schrijf, zie update; tab van user B blijft stil)
### Fase 1
- [ ] Geen `localStorage.getItem` of `localStorage.setItem` meer in de gemigreerde componenten
- [ ] Sprint screen: refresh → filter direct correct, geen flits, geen hydration error in console
- [ ] Product backlog screen: idem
- [ ] Jobs page: idem (per kolom-instance)
- [ ] Two-tab test: filter wijzigen in tab A → tab B updatet binnen ~100ms
- [ ] Demo-user: filter wijzigen werkt binnen sessie, niet gepersisteerd na refresh (verwacht)
- [ ] One-shot localStorage-migratie: bestaande user met oude keys ziet bij eerste login zijn waardes terug; na refresh zijn de localStorage-keys leeg
### Fase 2
- [ ] Split-pane positie persistent en SSR-correct
- [ ] Active-sprint per product werkt zonder cookie
---
## Schatting
| Fase | Tijd |
|---|---|
| 0 — Infra | ~3 uur |
| 1 — Migratie | ~2 uur |
| 2 — Cookies | ~2 uur (apart) |
| Totaal Fase 0 + 1 | **~5 uur**, 1 PR (of 2 als we 0 en 1 splitsen) |
Aanbevolen: **Fase 0 + 1 in één PR** als de infra klein blijft, anders splitsen
per fase. Fase 2 is altijd een aparte PR.
---
## Open vragen
1. **Cross-device merge-conflict.** Twee tabs van zelfde user op verschillende
devices wijzigen tegelijk. Server-side: `last-write-wins` of `JSON_PATCH`-merge?
Voorstel: deep-merge per top-level path, dus `views.sprintBacklog.filterStatus`
en `views.pbiList.sort` botsen niet — laatste schrijver per veld wint.
2. **Storage-grens.** PostgreSQL JSON kolom kan ~1GB; we zitten op <5KB per user.
Geen concern.
3. **Schema-versionering.** Als we het JSON-schema later wijzigen: voorzichtig
migreren via Zod `.catch()` voor onbekende keys. Voor v1: start klein.
4. **One-shot localStorage-migratie weglaten?** Voor solo-dev-tool kan het
acceptabel zijn dat users hun saved filters verliezen bij de migratie. Scheelt
~30 minuten implementatie + tests.
---
## Eerste stappen na approval
1. Verhuis dit plan naar `docs/plans/user-settings-store.md` in een nieuwe branch (bv. `feat/user-settings-store`)
2. Maak via Scrum4Me-MCP een PBI met story + taken voor Fase 0 (volgens CLAUDE.md werkflow)
3. Start met taken in `sort_order`; commit per laag
4. Fase 1 als opvolg-PBI (of in dezelfde sprint, los gelabeld)

View file

@ -0,0 +1,148 @@
---
title: "Scrum4Me — v1.0 readiness"
status: active
audience: [maintainer, contributor]
language: nl
last_updated: 2026-05-04
---
# Scrum4Me — v1.0 readiness
**Versie:** v0.9.0 (zojuist gepusht naar productie via Vercel)
**Doel:** v1.0.0 als eerste stabiele release. Living document — bijwerken na elke sprint of merge naar `main`.
---
## Summary
De kernfunctionaliteit (auth, producten, PBI/story/task-hiërarchie, sprints, solo-paneel, REST-API, MCP-integratie, QR-pairing, mobile-shell) is **af en in productie**. Tests, lint, build en doc-link-checker zijn allemaal groen. Wat ontbreekt voor v1 is geen feature-werk maar **launch-discipline**: een paar UI-gaten dichten, ops-instrumentatie (error monitoring, rate-limiting beredeneren), accessibility-audit, en de stale backlog-index opschonen. Alle "Expliciet buiten scope voor v1"-items uit de functional spec ([docs/specs/functional.md:20](../specs/functional.md)) blijven bewust uit scope.
---
## What's already done
- **#3 Rate-limiting op alle mutation-endpoints** — `enforceUserRateLimit(scope, userId)` helper in `lib/rate-limit.ts` met 11 nieuwe scopes; toegepast op create-actions (PBI/Story/Task/Todo/Sprint/Product/Token), enqueueClaudeJob(s), answerQuestion, en API-routes (story log POST, avatar upload). Limits zijn ruim genoeg voor normaal gebruik, eng genoeg om abuse-loops te stoppen
- **#2 Sentry error-monitoring** — `@sentry/nextjs` geconfigureerd via PR [#85](https://github.com/madhura68/Scrum4Me/pull/85); SDK is no-op zonder DSN, activatie via Vercel env-vars
- **#1 Edit-icoon op Product** (todo `cmoq3ox51`) — pencil-icoon op dashboard-card via PR [#83](https://github.com/madhura68/Scrum4Me/pull/83); product-detail-header behoudt tekst
- v0.9.0 ([release](https://github.com/madhura68/Scrum4Me/releases/tag/v0.9.0)): mobile-shell met landscape-lock (PBI-11, 7 stories, 21 tasks)
- v0.4.0 t/m v0.8.x: ondermeer sprint-screen filter-popover + edit-iconen, PBI/story/task edit-icons, code-velden verplicht, demo read-only, M11 Claude-vragen-kanaal, M10 QR-pairing
- CI op `main` en PR's: lint + typecheck + prisma validate + test + build via [`.github/workflows/ci.yml`](../../.github/workflows/ci.yml)
- 432 unit/integration tests · 60 test-files · doc-link-checker 86/86 valid
- Drie architectuur-beslissingen voor mobile geformaliseerd in [docs/architecture/project-structure.md](../architecture/project-structure.md)
---
## Now
Korte lijst (3-5 items) die je vóór de v1.0-tag wil afronden. Deze blokkeren een betekenisvolle launch.
### 1. ~~Edit-icoon op Product~~ ✅ klaar in PR [#83](https://github.com/madhura68/Scrum4Me/pull/83)
Verschoven naar *What's already done*. Pencil-icoon op dashboard-card; product-detail page-header behoudt tekst (matched naast andere text-acties).
### 2. Error monitoring (Sentry of vergelijkbaar)
CI vangt build-fouten af, maar er is geen runtime-monitoring. Voor een echte v1 wil je productie-fouten zien voordat een gebruiker het meldt. Vercel heeft native Sentry-integratie (Marketplace → Sentry).
Concreet:
- `npm i @sentry/nextjs`
- `npx @sentry/wizard@latest -i nextjs`
- DSN als env-var via Vercel project settings (development + production environments)
- Sample-rate conservatief (10% performance, 100% errors) — Hobby-plan-vriendelijk
- Bevestig dat Postgres-LISTEN/NOTIFY-fouten in worker-routes (`/api/realtime/*`) gevangen worden
### 3. ~~Rate-limiting op alle mutation-endpoints~~ ✅ klaar
Verschoven naar *What's already done*. Helper `enforceUserRateLimit(scope, userId)` in `lib/rate-limit.ts` toegepast op alle high-value create-paths.
### 4. Accessibility audit op happy-path
`@base-ui/react` levert WAI-ARIA defaults; we gebruiken semantische HTML; maar er is geen audit-bewijs.
Concreet:
- DevTools Lighthouse a11y-pass op `/login`, `/dashboard`, `/products/[id]`, `/products/[id]/sprint`, `/products/[id]/solo`, `/m/products/[id]`, `/m/products/[id]/solo`
- Score-doel ≥95 per pagina
- Fix wat onder de 95 valt — meestal contrast of missende labels
- Documenteer score in [docs/specs/functional.md § Niet-functionele vereisten](../specs/functional.md)
---
## Next
Belangrijk maar niet-blokkerend voor v1.
### Backlog-index sync
[docs/old/backlog/index.md](../old/backlog/index.md) toont M10 (ST-1001 t/m 1008) en M11 (ST-1101 t/m 1108) als unchecked, terwijl ze allemaal gemerged zijn. Loop één keer door en zet `[x]`. Is een 5-min-job die de doc weer betrouwbaar maakt voor wie 'm leest.
### Solo observaties (todo `cmohuu5h8`)
"Filters en sortering. blokjes kleiner maken 2 op een rij" — UX-polish op het Solo-paneel. Niet trivial: vereist een filter-popover-pattern (we hebben er net een uitgerold op het sprint-screen — herbruik kan).
### Algemene observaties (todo `cmohthfyw`)
"Dunne border om tekstvlakken (onzichtbaar als niet actief), default active PB kiezen, hover-card voor detail-info, landingpage AI-assisted/AI-driven framing." — verzameling kleinere UI-aanscherpingen, ieder eigen scope.
### ToDo prioriteit + AI-suggesties (todos `cmohtgdwf`, `cmohswbb9`)
Twee verwante todo's over de todo-feature uitbreiden. Past bij de strategische richting "AI-driven dev-flow" maar geen v1-blokker.
---
## Before launch
Must-do voor publieke aankondiging, maar mag pas vlak vóór v1.0-tag.
- [ ] **Smoke-test productie** — checklist klaar in [docs/runbooks/v1-smoke-test.md](../runbooks/v1-smoke-test.md), 11 secties, ~15 min
- [ ] **PWA-installatie test** op echt mobiel (Android + iOS) — bevestig manifest landscape, controleer iOS-fallback via CSS-overlay
- [x] **Demo-policy regression-pass** — code-side gefixt: 3 gaps gedicht (toggleTodo, archiveCompletedTodos, leaveProduct). Drielaags-block geverifieerd voor alle mutation-actions
- [x] **Privacy review** — Sentry sendDefaultPii=false; geen PII in logs; 4 debug-routes nu NODE_ENV-guarded (404 in productie)
- [x] **README + Quick start verifiëren** — test-count 69 → 445 gecorrigeerd, env-vars-tabel uitgebreid (CRON_SECRET, Sentry vars), CHANGELOG-link toegevoegd
- [x] **CHANGELOG.md** aangemaakt (Keep a Changelog formaat met [Unreleased] + [0.9.0])
- [ ] **Bump naar v1.0.0** + GitHub release met release-notes
---
## Later
Bewust uit scope voor v1 (uit functional spec § Expliciet buiten scope) — of grotere domein-uitbreidingen die hun eigen PBI verdienen.
- **Daily Scrum / Sprint Review / Retrospective**-schermen — v2
- **E-mail-uitnodigingsflow voor teams** — nu enkel via username
- **Notificaties + reminders** — out of scope
- **Native mobile app** — web-first; mobile-shell is genoeg
- **Tijdregistratie / burndown-charts** — buiten positionering
- **WIA AI agent** (todo `cmog2gzjb`) — eigen project-domein
- **Claude-code-integratie via tabel-trigger** (todo `cmohn3728`) — past bij M12-richting maar geen v1
- **Inspaningsmonitor-import** (todo `cmohul0ri`) — separate product
- **GitHub Issues / Linear / Jira-integratie** — v2
---
## Priority order (quick reference)
```
Now: ~~1. Edit-icoon op Product~~
~~2. Sentry/error-monitoring~~
~~3. Rate-limiting op mutation-endpoints~~
4. Accessibility-audit (Lighthouse a11y ≥95)
Next: 5. Backlog-index.md sync
6. Solo observaties (filters/sortering)
7. Algemene UI-observaties
8. Todo prioriteit + AI-suggesties
Before launch: 9. Smoke-test productie (desktop + mobile)
10. PWA-installatie test op echte mobiel
11. Demo-policy regression-pass
12. Privacy/PII review
13. README quick-start verificatie
14. CHANGELOG.md
15. Bump → v1.0.0 + release
Later: (zie sectie hierboven — v2-domein of buiten scope)
```
---
*Updated: 2026-05-04 (na v0.9.0 release). Refresh dit document na elke sprint of major merge.*

462
docs/old/product-backlog.md Normal file
View file

@ -0,0 +1,462 @@
---
title: "DevPlanner — Product Backlog"
status: active
audience: [maintainer]
language: nl
last_updated: 2026-05-03
---
# DevPlanner — Product Backlog
**Versie:** 0.1 — april 2026
**Product:** DevPlanner
**Beschrijving:** Een lichtgewicht Scrum-gebaseerde projectplanner voor solo developers en kleine Scrum Teams die meerdere softwareprojecten parallel beheren. De app organiseert werk hiërarchisch (product → PBI → story → taak), biedt een visuele planningslaag en integreert met Claude Code voor geautomatiseerde implementatieflows.
**Git repo:** https://github.com/devplanner/devplanner
**Definition of Done:** Feature is geïmplementeerd, getest (unit + integratie), gedocumenteerd in code, en gedeployed naar de staging-omgeving zonder regressies.
---
## Prioriteiten
| Prioriteit | Betekenis |
|---|---|
| 1 — Kritiek | Blokkeert alle andere functionaliteit. Moet eerst. |
| 2 — Hoog | Core waarde van het product. MVP vereiste. |
| 3 — Middel | Verhoogt bruikbaarheid significant. v1 wenselijk. |
| 4 — Laag | Waardevol maar niet blokkerend. v2 kandidaat. |
---
## PBI-01 — Authenticatie & gebruikersbeheer
**Prioriteit:** 1 — Kritiek
**Omschrijving:** Het Scrum Team kan een account aanmaken en inloggen met gebruikersnaam en wachtwoord. Een demo-gebruiker heeft alleen leesrechten. Gebruikers kunnen één of meerdere Scrum-rollen aannemen.
### Stories
**S-01-01: Account aanmaken**
Als bezoeker wil ik een account aanmaken met gebruikersnaam en wachtwoord, zodat ik toegang krijg tot de app.
Acceptatiecriteria:
- Gebruikersnaam en wachtwoord zijn verplicht
- Gebruikersnaam is uniek; dubbele aanmelding geeft foutmelding
- Wachtwoord heeft minimaal 8 tekens
- Na aanmaken wordt de gebruiker direct ingelogd
- Geen e-mailverificatie vereist in v1
**S-01-02: Inloggen**
Als geregistreerde gebruiker wil ik inloggen met gebruikersnaam en wachtwoord, zodat ik mijn projecten kan beheren.
Acceptatiecriteria:
- Incorrecte combinatie geeft generieke foutmelding (geen onderscheid gebruikersnaam/wachtwoord)
- Na inloggen wordt de gebruiker doorgestuurd naar het dashboard
- Sessie blijft actief totdat de gebruiker uitlogt
**S-01-03: Uitloggen**
Als ingelogde gebruiker wil ik kunnen uitloggen, zodat mijn sessie veilig afgesloten wordt.
Acceptatiecriteria:
- Uitlogknop altijd zichtbaar in de navigatie
- Na uitloggen wordt de gebruiker naar de loginpagina gestuurd
- Sessiedata wordt gewist
**S-01-04: Demo-gebruiker (read-only)**
Als bezoeker wil ik kunnen inloggen als demo-gebruiker, zodat ik de app kan verkennen zonder een account aan te maken.
Acceptatiecriteria:
- Vaste inloggegevens voor de demo-gebruiker zijn beschikbaar op de loginpagina
- Demo-gebruiker ziet alle data maar kan niets aanmaken, aanpassen of verwijderen
- Alle actieknoppen (aanmaken, bewerken, verwijderen) zijn zichtbaar maar uitgeschakeld met tooltip "Niet beschikbaar in demo-modus"
- Demo-gebruiker kan niet van rol wisselen
**S-01-05: Roltoewijzing**
Als gebruiker wil ik één of meerdere Scrum-rollen kunnen aannemen (Product Owner, Scrum Master, Developer), zodat de app weet in welke context ik werk.
Acceptatiecriteria:
- Gebruiker kan bij registratie of in instellingen rollen selecteren
- Minimaal één rol is verplicht
- Alle drie de rollen tegelijk zijn toegestaan
- Rolkeuze is zichtbaar in de navigatie/profielbalk
- Rolkeuze heeft in v1 geen effect op zichtbare functionaliteit (voorbereiding op v2)
---
## PBI-02 — Productbeheer
**Prioriteit:** 1 — Kritiek
**Omschrijving:** Het Scrum Team kan producten aanmaken, bekijken, bewerken en archiveren. Een product heeft een naam, beschrijving en koppeling naar een git-repository.
### Stories
**S-02-01: Product aanmaken**
Als Product Owner wil ik een nieuw product aanmaken met naam, beschrijving en git-repo URL, zodat ik een werkruimte heb voor de Product Backlog.
Acceptatiecriteria:
- Naam is verplicht en uniek per gebruiker
- Beschrijving is optioneel (vrije tekst)
- Git-repo URL is optioneel maar wordt gevalideerd als geldige URL
- Product is direct zichtbaar in de productenlijst na aanmaken
**S-02-02: Product bewerken**
Als Product Owner wil ik de naam, beschrijving en git-repo URL van een product kunnen aanpassen, zodat de informatie actueel blijft.
Acceptatiecriteria:
- Alle velden zijn bewerkbaar
- Wijzigingen worden opgeslagen zonder de pagina te verlaten
- Lege naam geeft validatiefout
**S-02-03: Product archiveren**
Als Product Owner wil ik een product kunnen archiveren, zodat het niet meer in het overzicht verschijnt maar de data bewaard blijft.
Acceptatiecriteria:
- Gearchiveerde producten verschijnen niet in de standaardlijst
- Er is een optie om gearchiveerde producten te tonen
- Archiveren is omkeerbaar (product kan worden hersteld)
**S-02-04: Productenlijst bekijken**
Als gebruiker wil ik een overzicht zien van alle actieve producten, zodat ik snel naar het juiste product kan navigeren.
Acceptatiecriteria:
- Lijst toont naam, beschrijving (ingekort) en git-repo link
- Klikken op een product opent de Product Backlog van dat product
- Lege staat toont een prompt om een product aan te maken
---
## PBI-03 — Product Backlog
**Prioriteit:** 1 — Kritiek
**Omschrijving:** Het Scrum Team kan de Product Backlog beheren via een gesplitst scherm: links de PBI's, rechts de bijbehorende stories. Items kunnen aangemaakt, bewerkt, geprioriteerd en gerangschikt worden via drag-and-drop (dnd-kit).
### Stories
**S-03-01: PBI aanmaken**
Als Product Owner wil ik een PBI aanmaken in de Product Backlog, zodat ik nieuwe functionaliteit kan definiëren.
Acceptatiecriteria:
- PBI heeft een titel (verplicht) en omschrijving (optioneel)
- PBI krijgt een prioriteit (1 t/m 4)
- Nieuw PBI verschijnt onderaan de lijst voor de gekozen prioriteit
- Aanmaken via knop in de navigatiebar van het linkerpaneel
**S-03-02: PBI bewerken**
Als Product Owner wil ik de titel, omschrijving en prioriteit van een PBI kunnen aanpassen.
Acceptatiecriteria:
- Dubbelklikken of via contextmenu opent bewerkingsmodus
- Alle velden zijn inline bewerkbaar
- Prioriteitswijziging herplaatst het PBI visueel
**S-03-03: PBI verwijderen**
Als Product Owner wil ik een PBI kunnen verwijderen, zodat irrelevante items de backlog niet vervuilen.
Acceptatiecriteria:
- Verwijderen vereist bevestiging
- Verwijderen van een PBI verwijdert ook alle bijbehorende stories (cascade)
- Actie is niet ongedaan te maken; bevestigingsdialoog waarschuwt hiervoor
**S-03-04: PBI prioriteit instellen**
Als Product Owner wil ik per PBI een prioriteit kunnen instellen (1 t/m 4), zodat de volgorde van de backlog de businesswaarde weerspiegelt.
Acceptatiecriteria:
- Prioriteit is instelbaar via dropdown of inline label
- PBI's worden gegroepeerd per prioriteit in de lijst
- Visuele scheiding per prioriteitsgroep (kleurband of scheidingslijn)
**S-03-05: PBI volgorde aanpassen via drag-and-drop**
Als Product Owner wil ik de volgorde van PBI's binnen dezelfde prioriteit kunnen aanpassen via drag-and-drop, zodat ik fijnmazige prioritering kan doen.
Acceptatiecriteria:
- Drag-and-drop werkt vloeiend (60fps) via dnd-kit
- Volgorde wordt direct opgeslagen na loslaten
- Drag over prioriteitsgrens wisselt de prioriteit van het PBI
- Visuele placeholder toont de doelpositie tijdens het slepen
**S-03-06: PBI filteren**
Als gebruiker wil ik PBI's kunnen filteren op prioriteit of status, zodat ik me kan focussen op het relevante werk.
Acceptatiecriteria:
- Filteropties beschikbaar in de navigatiebar van het linkerpaneel
- Filter werkt realtime (geen herlaadactie)
- Actief filter is duidelijk zichtbaar; eenvoudig te wissen
**S-03-07: Gesplitst scherm Product Backlog**
Als gebruiker wil ik de Product Backlog bekijken als gesplitst scherm (PBI's links, stories rechts), zodat ik snel kan navigeren tussen PBI's en hun stories.
Acceptatiecriteria:
- Scherm is standaard 50/50 verdeeld
- De splitter is horizontaal versleepbaar
- Elk paneel heeft een eigen navigatiebar met acties
- Selecteren van een PBI links toont de bijbehorende stories rechts
- Geselecteerde PBI is visueel gemarkeerd
---
## PBI-04 — Story-beheer
**Prioriteit:** 1 — Kritiek
**Omschrijving:** Stories kunnen worden aangemaakt, bewerkt, geprioriteerd en gerangschikt binnen een PBI. Stories worden weergegeven als blokken van circa 10% schermbreedte, gerangschikt op prioriteit.
### Stories
**S-04-01: Story aanmaken**
Als Product Owner wil ik een story aanmaken binnen een PBI, zodat ik de functionaliteit kan uitwerken in uitvoerbare eenheden.
Acceptatiecriteria:
- Story heeft een titel (verplicht), omschrijving (optioneel) en prioriteit
- Aanmaken via navigatiebar van het rechterpaneel
- Nieuwe story verschijnt als blok rechts, in de juiste prioriteitsgroep
**S-04-02: Story weergave als blokken**
Als gebruiker wil ik stories zien als compacte blokken (~10% schermbreedte), zodat ik snel een overzicht heb van alle stories per PBI.
Acceptatiecriteria:
- Elk blok toont: storytitel, prioriteit, status
- Blokken zijn gerangschikt op prioriteit (hoog naar laag, links naar rechts)
- Elke nieuwe prioriteitsgroep heeft een visuele scheiding (kleurband of lijn)
- Blokken zijn klikbaar voor detail/bewerking
**S-04-03: Story prioriteit instellen**
Als Product Owner wil ik per story een prioriteit instellen, zodat de Developer weet wat als eerste opgepakt moet worden.
Acceptatiecriteria:
- Prioriteit instelbaar via het storyblok (dropdown of label)
- Prioriteitswijziging herplaatst het blok in de juiste groep
**S-04-04: Story volgorde aanpassen via drag-and-drop**
Als Product Owner wil ik de volgorde van stories binnen dezelfde prioriteit aanpassen via drag-and-drop, zodat ik de uitvoeringsvolgorde kan finetunen.
Acceptatiecriteria:
- Drag-and-drop werkt via dnd-kit tussen en binnen prioriteitsgroepen
- Volgorde wordt direct opgeslagen
- Slepen over een prioriteitsgrens wijzigt de prioriteit
**S-04-05: Story bewerken**
Als Product Owner wil ik de titel, omschrijving en prioriteit van een story kunnen aanpassen.
Acceptatiecriteria:
- Bewerkbaar via klikken op het storyblok
- Wijzigingen opgeslagen zonder paginaverversing
**S-04-06: Story verwijderen**
Als Product Owner wil ik een story kunnen verwijderen.
Acceptatiecriteria:
- Verwijderen vereist bevestiging
- Cascade verwijdering van gekoppelde taken
- Niet ongedaan te maken; waarschuwing in dialoog
---
## PBI-05 — Todo-lijst
**Prioriteit:** 2 — Hoog
**Omschrijving:** Gebruikers kunnen een snelle todo-lijst bijhouden voor ongeplande of kortstondige taken. Een todo-item kan worden gepromoveerd naar een PBI of story.
### Stories
**S-05-01: Todo-item aanmaken**
Als gebruiker wil ik snel een todo-item aanmaken zonder het aan een product te koppelen, zodat ik losse gedachten kan vastleggen zonder de planningsflow te onderbreken.
Acceptatiecriteria:
- Todo heeft alleen een titel (verplicht)
- Aanmaken via een snel-invoerveld (Enter om op te slaan)
- Todo's zijn zichtbaar in een aparte todo-sectie of zijpaneel
**S-05-02: Todo-item afvinken**
Als gebruiker wil ik een todo-item kunnen afvinken, zodat ik bij kan houden wat klaar is.
Acceptatiecriteria:
- Afgevinkte items zijn visueel doorgestreept
- Afgevinkte items blijven zichtbaar maar kunnen worden gearchiveerd
**S-05-03: Todo promoveren naar PBI**
Als Product Owner wil ik een todo-item promoveren naar een PBI in een bestaand product, zodat losse ideeën in de formele backlog terechtkomen.
Acceptatiecriteria:
- Promoten opent een dialoog om product en prioriteit te kiezen
- Het todo-item wordt omgezet naar een PBI en verdwijnt uit de todo-lijst
- De PBI-titel is gelijk aan de todo-titel (bewerkbaar in dialoog)
**S-05-04: Todo promoveren naar story**
Als Product Owner wil ik een todo-item promoveren naar een story binnen een bestaand PBI, zodat ik snel nieuwe stories kan toevoegen vanuit losse notities.
Acceptatiecriteria:
- Promoten opent een dialoog om product, PBI en prioriteit te kiezen
- Todo wordt omgezet naar een story en verdwijnt uit de todo-lijst
---
## PBI-06 — Sprint Backlog & Sprint Planning
**Prioriteit:** 2 — Hoog
**Omschrijving:** Het Scrum Team kan een Sprint aanmaken met een Sprint Goal, stories uit de Product Backlog naar de Sprint Backlog slepen, en de volgorde bepalen.
### Stories
**S-06-01: Sprint aanmaken**
Als Scrum Master wil ik een nieuwe Sprint aanmaken met een Sprint Goal, zodat het Scrum Team een duidelijk doel heeft voor de komende Sprint.
Acceptatiecriteria:
- Sprint heeft een Sprint Goal (verplicht, vrije tekst)
- Sprint is gekoppeld aan een product
- Er kan maar één actieve Sprint per product zijn
**S-06-02: Sprint Backlog scherm (gesplitst)**
Als gebruiker wil ik de Sprint Backlog kunnen beheren via een gesplitst scherm (Sprint Backlog links, stories per PBI rechts), zodat ik snel stories kan toevoegen aan de Sprint.
Acceptatiecriteria:
- Links: Sprint Backlog met geselecteerde stories in volgorde
- Rechts: stories uit de Product Backlog, gegroepeerd per PBI
- Splitter is horizontaal versleepbaar
- Elk paneel heeft eigen navigatiebar
**S-06-03: Story naar Sprint slepen**
Als Developer wil ik een story vanuit de Product Backlog naar de Sprint Backlog kunnen slepen, zodat ik bepaal wat we deze Sprint gaan oppakken.
Acceptatiecriteria:
- Drag-and-drop werkt via dnd-kit tussen rechterpaneel en linkerpaneel
- Story verschijnt in de Sprint Backlog op de gesleepte positie
- Story in de Product Backlog krijgt visuele markering "In Sprint"
- Een story kan maar aan één actieve Sprint gekoppeld zijn
**S-06-04: Volgorde stories in Sprint bepalen**
Als Developer wil ik de volgorde van stories in de Sprint Backlog kunnen aanpassen via drag-and-drop, zodat ik de uitvoeringsvolgorde bepaal.
Acceptatiecriteria:
- Drag-and-drop werkt binnen de Sprint Backlog
- Volgorde wordt direct opgeslagen
- Volgorde is onafhankelijk van de prioriteit in de Product Backlog
**S-06-05: Story uit Sprint verwijderen**
Als Developer wil ik een story uit de Sprint Backlog kunnen verwijderen (terugplaatsen in de Product Backlog), zodat we de Sprint scope kunnen aanpassen.
Acceptatiecriteria:
- Story verdwijnt uit de Sprint Backlog
- Story is weer beschikbaar in de Product Backlog
- Actie vereist geen bevestiging (is niet destructief)
---
## PBI-07 — Sprint Planning (taken per story)
**Prioriteit:** 2 — Hoog
**Omschrijving:** Tijdens Sprint Planning worden stories opgedeeld in taken. Hetzelfde gesplitste scherm wordt gebruikt: stories links, taken rechts. Taken kunnen geprioriteerd en gerangschikt worden.
### Stories
**S-07-01: Sprint Planning scherm**
Als Developer wil ik een Sprint Planning scherm zien met stories links en taken rechts, zodat ik per story taken kan aanmaken en rangschikken.
Acceptatiecriteria:
- Links: stories in de Sprint Backlog in volgorde
- Rechts: taken van de geselecteerde story
- Selecteren van een story links toont de bijbehorende taken rechts
- Gesplitst scherm is horizontaal versleepbaar
**S-07-02: Taak aanmaken**
Als Developer wil ik een taak aanmaken onder een story, zodat ik het uitvoerbare werk kan definiëren.
Acceptatiecriteria:
- Taak heeft een titel (verplicht), omschrijving (optioneel) en prioriteit
- Aanmaken via navigatiebar van het rechterpaneel
- Nieuwe taak verschijnt onderaan de takenlijst van de story
**S-07-03: Taak prioriteit instellen**
Als Developer wil ik per taak een prioriteit instellen, zodat de uitvoeringsvolgorde duidelijk is.
Acceptatiecriteria:
- Prioriteit instelbaar via taakregel (dropdown of label)
- Taken gegroepeerd en gerangschikt op prioriteit
**S-07-04: Taak volgorde aanpassen via drag-and-drop**
Als Developer wil ik de volgorde van taken binnen een story kunnen aanpassen via drag-and-drop, zodat de uitvoeringsvolgorde precies klopt.
Acceptatiecriteria:
- Drag-and-drop via dnd-kit binnen de takenlijst
- Volgorde direct opgeslagen na loslaten
**S-07-05: Taakstatus bijhouden**
Als Developer wil ik de status van een taak kunnen bijhouden (To Do, In Progress, Done), zodat de voortgang van de Sprint zichtbaar is.
Acceptatiecriteria:
- Status is instelbaar via de UI (dropdown of knoppen)
- Statuswijziging is direct zichtbaar in het Sprint Planning scherm
- Story toont een voortgangsindicator op basis van taakstatussen
---
## PBI-08 — Claude Code integratie
**Prioriteit:** 2 — Hoog
**Omschrijving:** Claude Code kan via een REST API (en later MCP) stories en taken ophalen, de volgorde beoordelen, een implementatieplan opstellen, tests uitvoeren en committen. Elk resultaat wordt vastgelegd in de story.
### Stories
**S-08-01: REST API — story ophalen**
Als Developer (via Claude Code) wil ik de hoogst geprioriteerde open story van een product kunnen ophalen via een API-endpoint, zodat Claude Code weet wat er gedaan moet worden.
Acceptatiecriteria:
- Endpoint: `GET /api/products/:id/next-story`
- Retourneert: story-id, titel, omschrijving, acceptatiecriteria, gekoppelde taken
- Authentiseerd via API-token
- Geeft 404 als er geen open stories zijn
**S-08-02: REST API — eerste 10 taken ophalen**
Als Developer (via Claude Code) wil ik de eerste 10 taken van de Sprint Backlog kunnen ophalen, zodat Claude Code de volgorde kan beoordelen en zo nodig aanpassen.
Acceptatiecriteria:
- Endpoint: `GET /api/sprints/:id/tasks?limit=10`
- Retourneert taken in huidige volgorde met id, titel, prioriteit, status
- Claude Code kan de volgorde aanpassen via een apart endpoint
**S-08-03: REST API — taakvolgorde aanpassen**
Als Developer (via Claude Code) wil ik de volgorde van taken kunnen aanpassen via de API, zodat Claude Code een optimale uitvoeringsvolgorde kan bepalen.
Acceptatiecriteria:
- Endpoint: `PATCH /api/stories/:id/tasks/reorder`
- Accepteert een geordende lijst van taak-id's
- Volgorde wordt direct weerspiegeld in de UI
**S-08-04: Implementatieplan vastleggen**
Als Developer (via Claude Code) wil ik een implementatieplan kunnen schrijven naar een story, zodat de ontwerpbeslissingen traceerbaar zijn.
Acceptatiecriteria:
- Endpoint: `POST /api/stories/:id/log`
- Veld: `type: "implementation_plan"`, `content: string`
- Log-entry verschijnt in de story-activiteitenlog in de UI
**S-08-05: Teststatus vastleggen**
Als Developer (via Claude Code) wil ik de uitkomst van testruns kunnen vastleggen in een story, zodat kwaliteitsbewijs per story bewaard blijft.
Acceptatiecriteria:
- Endpoint: `POST /api/stories/:id/log`
- Veld: `type: "test_result"`, `content: string`, `status: "passed" | "failed"`
- Teststatus zichtbaar in de story-activiteitenlog
**S-08-06: Commit-hash vastleggen**
Als Developer (via Claude Code) wil ik de commit-hash na een succesvolle commit kunnen vastleggen in een story, zodat code en planning direct gekoppeld zijn.
Acceptatiecriteria:
- Endpoint: `POST /api/stories/:id/log`
- Veld: `type: "commit"`, `hash: string`, `message: string`
- Commit-hash is klikbaar en linkt naar de git-repo (indien geconfigureerd)
**S-08-07: Story activiteitenlog in UI**
Als gebruiker wil ik per story een activiteitenlog zien met alle vastgelegde implementatieplannen, testresultaten en commits, zodat ik de volledige uitvoeringsgeschiedenis op één plek heb.
Acceptatiecriteria:
- Log toont alle entries in chronologische volgorde
- Elk type entry heeft een eigen visuele stijl (plan, test, commit)
- Log is zichtbaar in de story-detailweergave
- Log is read-only in de UI (schrijven gebeurt via API)
---
## PBI-09 — Infrastructuur & deployment
**Prioriteit:** 1 — Kritiek
**Omschrijving:** De app is deployable op Vercel + Neon (cloud) én volledig lokaal draaibaar zonder externe afhankelijkheden.
### Stories
**S-09-01: Cloud deployment (Vercel + Neon)**
Als Developer wil ik de app deployen op Vercel met een Neon PostgreSQL-database, zodat de app beschikbaar is via een URL.
Acceptatiecriteria:
- `next build` slaagt zonder fouten
- Database-migraties worden uitgevoerd via Prisma
- Environment variables zijn gedocumenteerd in `.env.example`
**S-09-02: Lokale modus**
Als Developer wil ik de app lokaal kunnen draaien met een Neon PostgreSQL-database, zodat de lokale setup overeenkomt met productie.
Acceptatiecriteria:
- `npm run dev` start de app lokaal zonder Vercel of Neon account
- Database wordt aangemaakt via `prisma db push`
- README bevat stap-voor-stap instructies voor lokale setup
**S-09-03: API-token authenticatie**
Als Developer wil ik een API-token kunnen genereren in de app, zodat Claude Code veilig kan communiceren met de REST API.
Acceptatiecriteria:
- Gebruiker kan een API-token aanmaken in de instellingenpagina
- Token wordt eenmalig getoond en daarna niet meer zichtbaar
- Token kan worden ingetrokken
- Alle API-endpoints vereisen een geldig token via `Authorization: Bearer`
---
## Backlog — v2 kandidaten (niet in v1)
| PBI | Omschrijving |
|---|---|
| Daily Scrum scherm | Voortgang per story bijhouden tijdens de Sprint |
| Sprint Review scherm | Demo en feedback vastleggen per story |
| Sprint Retrospective scherm | Reflectie vastleggen per Sprint |
| Meerdere gebruikers per Scrum Team | Uitgebreide auth met rol-gebaseerde permissies |
| Automatische statusupdate na commit | Story op Done zetten via API-aanroep |
| Velocity tracking | Statistieken over meerdere Sprints |
| Notificaties / reminders | Push of e-mailmeldingen |
| Timeline / kalenderweergave | Sprint-planning op kalender |
| Definition of Done per product configureerbaar | Nu vaste instelling; later flexibel |
| Integratie GitHub Issues / Linear | Import/export van PBI's en stories |
---
*Dit document dient als testdata voor de eerste implementatie van de datastructuur.*
*Versie 0.1 — te updaten na Sprint 1 Review.*

171
docs/old/story-dialog.md Normal file
View file

@ -0,0 +1,171 @@
---
title: "StoryDialog Profiel"
status: active
audience: [maintainer, contributor]
language: nl
last_updated: 2026-05-03
---
# StoryDialog Profiel
> Volgt **`docs/patterns/dialog.md`** (de generieke spec voor élke entity-dialog in Scrum4Me).
> Dit document beschrijft alleen de Story-specifieke afwijkingen en keuzes — alle gedeelde regels (layout, motion, demo-policy, foutcodes, validatie, theming, dialog-gedrag) staan in de generieke spec en worden hier niet herhaald.
> **Belangrijk:** als een regel in dit profiel botst met de generieke spec, wint de generieke spec. Documenteer hier de afwijking + reden, of pas de generieke spec aan.
---
## Velden
| Veld | Type | Mode | Validatie |
|---|---|---|---|
| `code` | `string \| null` | beide | optional, max 30 chars, mono-font, placeholder `auto` op create |
| `title` | `string` (required) | beide | trim, 1-200 chars |
| `priority` | `int` (1-4, P1 = hoogste) | beide | int 1-4, default 2 (overschrijfbaar via `defaultPriority`-prop bij create) |
| `description` | `string \| null` | beide | optional, plain textarea, placeholder `Als… wil ik… zodat…` (user-story-template) |
| `acceptance_criteria` | `string \| null` | beide | optional, plain textarea, placeholder `- Gegeven… Als… Dan…` (Gherkin-template) |
| `status` | `StoryStatus` enum | alleen edit | read-only badge in header, niet bewerkbaar in deze dialog |
`StoryStatus` enum: `OPEN | IN_SPRINT | DONE` (uppercase in DB).
### Veld-specifiek gedrag
- **Code + Titel** in één rij (`grid-cols-[6rem_1fr]`)
- **Prioriteit** via `<PrioritySelect>` (gedeelde primitive)
- **Description** als `<Textarea rows={3} resize-none>` — géén auto-grow, géén markdown-hint binnen de dialog (afwijking van generieke spec; rationale: stories zijn meestal één zin)
- **Acceptatiecriteria** idem — géén auto-grow, géén char-counter
- **Status** wordt **niet bewerkt** vanuit deze dialog. Status verandert via lijst-acties (sleep naar sprint = IN_SPRINT, taak-completion = DONE). Read-only badge in dialog-header.
---
## URL- of state-pattern
- **Gekozen:** state-based (`state: StoryDialogState | null` prop, gerendeerd binnen `StoryPanel`)
- **Reden:** StoryDialog leeft binnen `StoryPanel` met live-store-data (geselecteerde PBI bepaalt zichtbare stories); deep-linking zou parallelle data-fetch-paden vereisen.
- **State-shape:**
```ts
type StoryDialogState =
| { mode: 'create'; pbiId: string; productId: string; defaultPriority?: number }
| { mode: 'edit'; story: Story; productId: string }
```
- **Sluiten:** `onClose()` callback uit de parent — `setState(null)` in `StoryPanel`.
---
## Status-veld
- **Niet bewerkbaar in deze dialog** — alleen weergegeven als badge in de header (edit-mode)
- **Default bij create:** `OPEN` (server-default, niet expliciet gezet vanuit form)
- Status-overgangen lopen via:
- `OPEN → IN_SPRINT` — drag-and-drop naar een sprint of `sprint-id` zetten via story-actions
- `IN_SPRINT → DONE` — alle taken op `DONE` zetten triggert auto-promotion (zie story-status-logic in `actions/stories.ts`)
---
## Server actions
| Actie | Locatie | Form-binding | Revalidatie |
|---|---|---|---|
| `createStoryAction` | `actions/stories.ts` | via `useActionState` + `<form action>` (FormData) | server-side `revalidatePath` op product-backlog |
| `updateStoryAction` | `actions/stories.ts` | idem | idem |
| `deleteStoryAction` | `actions/stories.ts` | aangeroepen vanuit `useTransition` (geen form) | server-side `revalidatePath` |
| `getStoryLogsAction` | `actions/stories.ts` | aangeroepen on-mount in edit-mode | n.v.t. (read-only) |
Alle write-acties zijn drielaags afgedekt (proxy-guard + server-action-check + DemoTooltip op submit-knop).
---
## Speciale gedragingen
### Header-presentatie (afwijking van generieke spec)
In edit-mode toont de dialog-header **drie elementen** boven op de standaard titel:
1. Story-titel als dialog-title (groot)
2. Story-code als monospace-badge rechtsboven (klein)
3. Twee badges direct onder de titel: priority-badge (kleur via `PRIORITY_COLORS`) en status-badge (kleur via `STATUS_COLORS`)
Generieke spec gaat uit van een sobere header met alleen `headline-small` titel + optioneel een `created_at`-meta-string. StoryDialog wijkt hier bewust van af omdat status + priority belangrijke context zijn voor de gebruiker bij het openen van een story (vaak wisselt iemand vlot tussen meerdere stories).
### Demo-modus = read-only weergave
Wanneer `isDemo === true` én `isEdit === true`, wordt het form **vervangen** door een read-only weergave:
- `description` via gedeelde `<Markdown>`-wrapper (`react-markdown` + `remark-gfm`)
- `acceptance_criteria` als plain whitespace-pre-line tekst
In create-mode is er voor demo-users niets te tonen — de dialog wordt alsnog geopend maar de submit-knop is `disabled` met `<DemoTooltip>`.
> Dit "read-only-fallback"-patroon is uniek voor StoryDialog tot nu toe. Het zou geadopteerd kunnen worden door andere edit-dialogs zodra demo-flow-vereisten dat rechtvaardigen.
### Activity-log (StoryLog) inline
In edit-mode wordt onder het form een `<StoryLog>`-paneel getoond met de chronologische logs van deze story (commit-hashes, status-transitions, etc.). Logs worden lazy-fetched via `getStoryLogsAction(story.id)` zodra de dialog opent.
Dit is een **read-only side-panel** en valt binnen de uitzondering die de generieke spec § 13 maakt voor `<StoryLog>`-style activity-rendering.
### Delete-flow (afwijking van generieke spec)
Generieke spec § 10.4 vereist een **`AlertDialog`** voor delete-confirmatie. StoryDialog gebruikt in plaats daarvan een **inline-confirm** in dezelfde footer-rij:
```
[ Weet je het zeker? Taken worden ook verwijderd. [Verwijderen] [Annuleren] ]
```
Een `AlertDialog` zou een tweede modale laag toevoegen die in deze context onhandig voelt (de dialog zelf is al een interruptive overlay). De inline-confirm is een **bewuste afwijking** van de generieke spec.
### Form-state via `useActionState`
Net als PbiDialog gebruikt StoryDialog `useActionState` + `useFormStatus`, niet `react-hook-form`. Dit is een toegestaan alternatief volgens de generieke spec § 2.
### `key`-prop op `<form>`
Het `<form>` heeft `key={isEdit ? story!.id : 'create'}` — reset native form-state bij record-wissel of mode-switch.
---
## Triggers
- **Create-trigger:** `+ Story`-knop in `PanelNavBar` van `StoryPanel``setStoryDialogState({ mode: 'create', pbiId, productId, defaultPriority: 2 })`
- **Edit-trigger:** edit-icoon op een story-card in `StoryPanel``setStoryDialogState({ mode: 'edit', story, productId })`
- **Empty-state-trigger:** `Maak je eerste story aan`-knop in `EmptyPanel` (zelfde state als create-trigger)
---
## Bekende gaps t.o.v. generieke spec
> Deze items wijken af van `docs/patterns/dialog.md` en horen in een vervolg-PR rechtgezet (niet onderdeel van de huidige docs-introductie).
- ❌ **Geen dirty-close-guard** — Esc / backdrop / Cancel sluiten direct, ook met onopgeslagen wijzigingen. Generieke spec § 8.1 vereist een AlertDialog bij `isDirty`.
- ❌ **Geen Cmd/Ctrl+Enter shortcut** — alleen klik op submit-knop.
- ❌ **Geen char-counter / markdown-hint** op description / acceptance_criteria — bewust weggelaten, maar verdient expliciete bevestiging als design-keuze.
- ⚠️ **Inline-delete-confirm** in plaats van AlertDialog (zie § Speciale gedragingen). Bewuste afwijking; de generieke spec mag deze variant expliciet toestaan, of dit profile moet als precedent gelden voor toekomstige dialogen.
- ⚠️ **Header-layout** met meerdere badges wijkt af van de sobere header in § 4. Bewuste afwijking — context-zwaar bij story-wisselen.
- ⚠️ **Layout wijkt af** van de generieke responsive-tabel: `sm:max-w-lg` met eigen `max-h-[90vh]` + `flex flex-col` i.p.v. de exacte `max-w-[50vw]` / `90vw` / full-screen-progressie uit § 4.
---
## Bewust NIET in v1
Specifiek voor StoryDialog (boven op de algemene out-of-scope-lijst in `docs/patterns/dialog.md` § 13):
- ❌ Status bewerken vanuit de dialog (gebeurt via lijst-acties / drag-and-drop / auto-promotion)
- ❌ Inline aanmaken van child-tasks (gebeurt via TaskDialog vanuit `TaskPanel`)
- ❌ Bulk-edit over meerdere stories
- ❌ Story-templates
- ❌ Linking aan externe issues (GitHub / Linear) — staat op v1.1+ roadmap
---
## Referenties
- `components/backlog/story-dialog.tsx` — implementatie
- `actions/stories.ts` — server actions (incl. `getStoryLogsAction`)
- `components/shared/priority-select.tsx` — gedeelde priority-control
- `components/shared/story-log.tsx` — activity-log paneel
- `components/shared/demo-tooltip.tsx` — demo-policy laag 3
- `components/markdown.tsx` — gedeelde markdown-wrapper
- `lib/task-status.ts` — status-enum-mapper
- `docs/patterns/dialog.md` — generieke spec (bron-of-truth)
- `docs/architecture.md` — datamodel `Story`
- `docs/styling.md` — MD3-tokens, status- en priority-kleuren

135
docs/old/task-dialog.md Normal file
View file

@ -0,0 +1,135 @@
---
title: "TaskDialog Profiel"
status: active
audience: [maintainer, contributor]
language: nl
last_updated: 2026-05-03
---
# TaskDialog Profiel
> Volgt **`docs/patterns/dialog.md`** (de generieke spec voor élke entity-dialog in Scrum4Me).
> Dit document beschrijft alleen de Task-specifieke afwijkingen en keuzes — alle gedeelde regels (layout, motion, demo-policy, foutcodes, validatie, theming, dialog-gedrag) staan in de generieke spec en worden hier niet herhaald.
> **Belangrijk:** als een regel in dit profiel botst met de generieke spec, wint de generieke spec. Documenteer hier de afwijking + reden, of pas de generieke spec aan.
---
## Velden
| Veld | Type | Mode | Validatie |
|---|---|---|---|
| `title` | `string` (required) | beide | trim, 1-120 chars |
| `description` | `string \| null` | beide | optional, max 2.000 chars, markdown |
| `implementation_plan` | `string \| null` | beide | optional, max 10.000 chars, markdown |
| `priority` | `int` (1-4, P1 = hoogste) | beide | int 1-4, default 3 |
| `status` | `TaskStatus` enum | alleen edit (default `TO_DO` op create, niet getoond) | enum |
| `created_at` | `Date` | alleen edit | read-only metadata in header |
`TaskStatus` enum: `TO_DO | IN_PROGRESS | REVIEW | DONE`.
### Veld-specifiek gedrag
- **Auto-grow textareas** (`description`, `implementation_plan`) via `react-textarea-autosize`. Max 6 regels (description) / 12 regels (implementation_plan), daarna `overflow-y-auto`.
- **Karakter-counter** vanaf 75% van de limiet, klein, rechtsonder, `text-muted-foreground`. Bv. `1547 / 2000`.
- **Markdown-hint** onder elk textarea: `Markdown ondersteund (lijstjes, **vet**, \`code\`)`.
- **Priority** als segmented buttons via `<PrioritySelect>` / `<PrioritySegmented>`. Default P3 (Medium).
- **Status** met gekleurde dot:
- `TO_DO` — grijs
- `IN_PROGRESS``status-in-progress` (blauw)
- `REVIEW` — paars
- `DONE``status-done` (groen)
- **`created_at` als header-metadata** in edit-mode, naast de titel: `Aangemaakt: 23 apr 2026`. Klein, `muted-foreground`, géén form-veld.
---
## URL- of state-pattern
- **Gekozen:** URL-based (`searchParams`)
- **Reden:** TaskDialog wordt geopend vanuit twee context-pagina's (sprint-detail en product-backlog) en moet deep-linkable zijn voor share/refresh-scenario's. Suspense + skeleton voor edit-mode loading is gewenst.
- **Routes:**
```
/sprint/<sprintId>?newTask=1 → create
/sprint/<sprintId>?editTask=<taskId> → edit
/products/<productId>/backlog?newTask=1 → create
/products/<productId>/backlog?editTask=<taskId> → edit
```
- **Sluiten:** `router.push(<base-route>)` zonder query-params.
- **Server-side fetch in edit-mode:** server component fetcht de taak vóór render mét `productAccessFilter(userId)`. Bestaat de taak niet of valt 'm buiten scope → toast + redirect naar de context-route.
- Optioneel: `nuqs` als de query-state-handling te omslachtig wordt — pas introduceren als losse refactor-task, niet inline.
---
## Status-veld
Verberg `status` in **create-mode** (default = `TO_DO` is genoeg). Toon alleen in edit-mode als `<Select>` met gekleurde dot per optie.
---
## Server actions
| Actie | Locatie | Context-arg | Revalidatie |
|---|---|---|---|
| `saveTask` | `app/actions/tasks.ts` | `{ sprintId?: string; productId?: string }` | `revalidatePath('/sprint/<sprintId>')` óf `revalidatePath('/products/<productId>/backlog')` afhankelijk van context |
| `deleteTask` | `app/actions/tasks.ts` | idem | idem |
Beide acties volgen de drielaagse demo-policy + auth-scoping uit `docs/patterns/dialog.md` § 67.
---
## Speciale gedragingen
### Triggers (bestaande UI vervangen)
Deze TaskDialog is de **enige** create/edit-flow voor taken in beide contexten (sprint én backlog). Bestaande inline-edit-paden in `components/sprint/task-list.tsx` en het backlog-equivalent worden vervangen, niet ernaast geplaatst.
- **Create-trigger:** filled button `+ Nieuwe taak` in tasklist-header → zet `?newTask=1` op huidige route
- **Edit-trigger:** klik op de hele rij in de tasklist (geen apart edit-icoon) → zet `?editTask=<id>` op huidige route
- **Loading edit-mode:** Suspense met minimale skeleton (3 grijze balken), `200ms`-delay zodat snelle fetches geen flicker tonen
### Markdown-rendering elders
`description` en `implementation_plan` worden buiten de dialog (taakdetail, hover-card) gerenderd via de gedeelde `<Markdown>`-wrapper (`react-markdown` + `remark-gfm`). Niet in de dialog zelf.
---
## Implementatie-volgorde (suggestie)
Hergebruik dit als checklist bij het bouwen of refactoren van TaskDialog:
1. Dependencies in `package.json` (zie `docs/patterns/dialog.md` § 2)
2. zod-schema in `lib/schemas/task.ts` — gedeeld door form en action
3. `productAccessFilter`-helper checken in `lib/auth/`
4. `saveTask` / `deleteTask` in `app/actions/tasks.ts` met auth-scoping + demo-check (laag 2)
5. `proxy.ts`-guard voor demo-write-routes (laag 1) — alleen als nog niet aanwezig
6. Eventueel ontbrekende MD3-tokens in `app/styles/theme.css` aanvullen
7. `<DemoTooltip>` rond submit/delete-knoppen (laag 3)
8. TaskDialog — create-mode eerst (minder edge cases)
9. Edit-mode toevoegen (status, delete, `created_at`-metadata)
10. URL-state via native `searchParams` op beide context-pagina's
11. Bestaande task-row trigger refactoren (klikbaar maken naar dialog)
12. Suspense + skeleton voor edit-mode + scope-check op fetch
13. Dirty-close-guard
14. Keyboard shortcuts (Cmd+Enter)
---
## Bewust NIET in v1
Specifiek voor TaskDialog (boven op de algemene out-of-scope-lijst in `docs/patterns/dialog.md` § 13):
- ❌ Sub-tasks / parent-child relaties tussen taken
- ❌ Tags / labels / categorieën op taken
- ❌ Due dates / reminders per taak
- ❌ Time tracking (uren-registratie) — wel relevant voor inspannings-monitor, eigen feature
- ❌ Sharing / collaboration per taak
- ❌ Templates voor terugkerende taken
---
## Referenties
- `docs/patterns/dialog.md` — generieke spec (bron-of-truth voor alles wat hier niet beschreven is)
- `docs/architecture.md` — datamodel `Task`
- `docs/styling.md` — MD3-tokens, status- en priority-kleuren
- `lib/task-status.ts` — enum-mapper DB ↔ API