* docs(dialog-pattern): add generic entity-dialog spec Introduceert docs/patterns/dialog.md als bron-of-truth voor elke create/edit/detail-dialog in Scrum4Me, ongeacht het achterliggende dataobject. Bevat 14 secties: uitgangspunten, stack, component- architectuur, layout, validatie, drielaagse demo-policy, submission, dialog-gedrag, theming, footer, triggers/URL-state, per-entiteit profile-template, out-of-scope, en een verificatie-checklist. Registreert het patroon in CLAUDE.md "Implementatiepatronen"-tabel zodat Claude (en mensen) de spec verplicht raadplegen voor elke nieuwe dialog. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(dialog-pattern): convert task spec + add pbi/story entity-profiles Reduceert docs/scrum4me-task-dialog.md van 507 naar ~140 regels: alle gedeelde regels verhuisd naar docs/patterns/dialog.md, dit document bevat nu alleen Task-specifieke velden, URL-pattern, status-veld, server actions, triggers en bewuste out-of-scope-keuzes. Voegt twee nieuwe entity-profielen toe voor bestaande dialogen: - docs/scrum4me-pbi-dialog.md (PbiDialog: state-based, code+title-rij, PbiStatusSelect, geen delete in v1) - docs/scrum4me-story-dialog.md (StoryDialog: state-based, header met status/priority badges, inline activity-log, demo-readonly-fallback, inline-delete-confirm i.p.v. AlertDialog) Beide profielen documenteren expliciet de "Bekende gaps t.o.v. generieke spec" zodat opvolgende PR's de afwijkingen kunnen rechtzetten of bewust kunnen accorderen. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Added pdevelopment docs * docs(plans): add docs-restructure plan for AI-optimized lookup Audit of existing 39 doc files (~10.700 lines) and a phased restructure proposal aimed at minimising the tokens an AI agent has to read to find the right reference. Captures resolved decisions on language (English), ADR template (Nygard default with MADR escape-hatch), index generator (node script), and folder taxonomy. Proposal status — fase 1 to follow. * docs(adr): add ADR scaffolding (templates, README, meta-ADR) Set up docs/adr/ as the canonical home for architecture decisions: - templates/nygard.md — default four-section format (Status, Context, Decision, Consequences) for one-way-door decisions. - templates/madr.md — MADR v4 with YAML front-matter and explicit Considered Options for decisions where rejected alternatives matter. - README.md — naming convention (NNNN-kebab-case), template-selection guidance (Nygard default; MADR for auth, queue mechanics, agent integration), status lifecycle, and ADR roster. - 0000-record-architecture-decisions.md — meta-ADR establishing the practice itself, in Nygard format. Backfilling existing implicit decisions (base-ui-over-radix, float sort_order, demo-user three-layer policy, etc.) is fase 6 of the docs-restructure plan. * feat(docs): add docs index generator + initial INDEX.md scripts/generate-docs-index.mjs walks docs/**/*.md, parses YAML front-matter (or first H1 fallback) and a Nygard-style ## Status section, then writes docs/INDEX.md with grouped tables for ADRs, Specs, Plans (with archive subsection), Patterns, and Other. Pure Node 20 (no external deps); idempotent — running it twice produces byte-identical output. Excludes adr/templates/, the ADR README, INDEX.md itself, and any *_*.md sidecar file. Wire-up: - package.json: docs:index → node scripts/generate-docs-index.mjs Initial run indexed 35 docs across the existing structure; the generated INDEX.md is committed so the table is reviewable in the PR before hooking generation into a pre-commit step. * chore: ignore Obsidian vault and personal sidecar files Add .obsidian/ (Obsidian vault config) and _*.md (personal sidecar notes) to .gitignore so the docs/ tree can serve as canonical source of truth while still being usable as an Obsidian vault for personal authoring. The docs index generator already excludes the same _*.md pattern from INDEX.md. * docs(plans): add PBI bulk-create spec for docs-restructure Machine-parseable spec for an executor that calls the scrum4me MCP (create_pbi → create_story → create_task) to seed the docs-restructure work into the DB. - Section 1 (Context) is the PBI description; serves as task-context via mcp__scrum4me__get_claude_context. - Section 2 lists the 6 resolved decisions (English, MD3+styling merged, solo-paneel merged, .Plans archived, Nygard ADR default, node index script). - Section 3 records what already shipped on this branch so the executor doesn't duplicate the ADR scaffolding or index generator. - Section 4 carries the structured YAML graph: 1 PBI, 8 stories (one per phase), 39 tasks. product_id is REPLACE_ME — fill before running. - YAML validated with PyYAML; field schema sanity-checked. * docs(junk-cleanup): remove stub patterns/test.md * docs(junk-cleanup): archive .Plans/ to docs/plans/archive/ * docs(front-matter): add YAML front-matter to docs/ root * docs(front-matter): add YAML front-matter to patterns/ * docs(front-matter): add YAML front-matter to plans + agent files * docs(index): regenerate INDEX.md after front-matter pass * docs(naming): drop scrum4me- prefix from doc filenames * docs(naming): lowercase API.md and MD3 filenames * docs(naming): rename plan file to kebab-case ASCII * docs(naming): rename middleware.md to proxy.md (next 16) * docs(naming): polish CLAUDE.md doc-index after renames * docs(taxonomy): scaffold topical folders under docs/ * docs(taxonomy): move spec files into docs/specs/ * docs(taxonomy): move design/api/qa/backlog/assets into folders * docs(taxonomy): move agent-instruction-audit into decisions/ * docs(split): break architecture.md into 6 topical files * docs(split): merge solo-paneel-spec into specs/functional.md * docs(split): merge md3-color-scheme into design/styling * docs(trim): extract branch/commit rules into runbook * docs(trim): extract MCP integration into runbook * docs(adr): add 0001-base-ui-over-radix * docs(adr): add 0002-float-sort-order * docs(adr): add 0003-one-branch-per-milestone * docs(adr): add 0004-status-enum-mapping * docs(adr): add 0005-iron-session-over-nextauth * docs(adr): add 0006-demo-user-three-layer-policy * docs(adr): add 0007-claude-question-channel-design * docs(adr): add 0008-agent-instructions-in-claude-md + update README index * docs(index): regenerate after ADR 0001-0008 * docs(glossary): add docs/glossary.md * chore(docs): regenerate INDEX.md in pre-commit hook * docs(readme): link INDEX + glossary + agent instructions * feat(docs): add doc-link checker script * chore(docs): wire docs:check-links and docs npm scripts * ci(docs): block merge on broken doc links * docs(links): fix broken cross-references after restructure --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
76 KiB
| title | status | audience | language | last_updated | ||
|---|---|---|---|---|---|---|
| Scrum4Me — Implementatie Backlog | active |
|
nl | 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
-
ST-001 Project scaffolding
create-next-appmet TypeScript strict, Tailwind CSS, App Router; installeer shadcn/ui, Zustand, dnd-kit, iron-session, bcrypt, Zod; configureer path aliases (@/)- Done when:
npm run devstart zonder fouten;npm run lintgeeft geen errors; shadcnButtonrendert op een testpagina
-
ST-002 Prisma v7 setup +
prisma.config.ts- Installeer Prisma v7 +
@prisma/adapter-pg; schrijfprisma.config.tsmetDATABASE_URLvia Zod-gevalideerde env; schrijflib/prisma.tssingleton - Done when:
npx prisma db pushslaagt; Prisma Client importeerbaar in een testbestand zonder fouten
- Installeer Prisma v7 +
-
ST-003 Database schema migratie (volledige initiële migratie)
- Schrijf het volledige
schema.prismaop 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 initslaagt; alle tabellen zichtbaar in DB-client;npx prisma validategeeft geen fouten
- Schrijf het volledige
-
ST-004 Seed met testdata
- Schrijf
prisma/seed.tsop 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 seedslaagt; DB bevat alle PBI's en stories uit het backlog-document; demo-gebruiker aanwezig
- Schrijf
-
ST-005 Environment variabelen +
lib/env.ts- Schrijf Zod-schema voor alle env vars (
DATABASE_URL,DIRECT_URL,SESSION_SECRET,NODE_ENV); exporteer gevalideerdenvobject; schrijf.env.examplemet instructies - Done when: app gooit een begrijpelijke fout bij ontbrekende env var;
.env.examplevolledig gedocumenteerd
- Schrijf Zod-schema voor alle env vars (
-
ST-006 Authenticatie — registratie en inloggen
- Schrijf
lib/auth.ts(registreer met bcrypt hash, verifieer bij inloggen); schrijflib/session.ts(iron-session config); implementeer/registeren/loginpagina'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
- Schrijf
-
ST-007 Route-beveiliging via
proxy.ts- Schrijf
proxy.tsdie sessiecookie-aanwezigheid controleert; redirect naar/loginbij alle/dashboard,/products/*,/todos,/settings/*routes zonder sessiecookie; authenticated users worden van/loginen/registerdoorgestuurd naar/dashboard; volledige sessievalidatie gebeurt server-side in de app layout - Done when: directe navigatie naar
/dashboardzonder sessie redirect naar/login; ingelogde gebruiker op/loginredirect naar/dashboard
- Schrijf
-
ST-008 Navigatieshell + dashboard-layout
- Schrijf
app/(app)/layout.tsxmet navigatiebalk (logo, productenlink, todolink, instellingen, uitlogknop); implementeer uitlog Server Action; implementeer/dashboardals lege productenlijstpagina met "Maak je eerste product aan" lege staat; zet demo-badge zichtbaar alsisDemo === true - Done when: volledige auth-flow (register → login → dashboard → logout → login) werkt end-to-end; demo-gebruiker ziet badge in navigatie
- Schrijf
M1: Producten & Product Backlog
-
ST-101 Product aanmaken
/products/newpagina met formulier (naam, beschrijving, repo URL, definition of done);createProductServer 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
-
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
-
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
-
ST-104 Gesplitst scherm layout component (
SplitPane)- Bouw herbruikbaar
<SplitPane>Client Component met versleepbare horizontale splitter; sla splitter-positie op inlocalStorageper 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
- Bouw herbruikbaar
-
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
- Bouw herbruikbaar
-
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);createPbiServer Action; nieuw PBI verschijnt onderaan de juiste prioriteitsgroep - Done when: PBI aangemaakt en zichtbaar in juiste prioriteitsgroep; lege staat toont prompt
- Linkerpaneel van
-
ST-107 PBI prioriteitsgroepen met visuele scheiding
- Render PBI's gegroepeerd per prioriteit (1–4) 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
-
ST-108 PBI bewerken en verwijderen
- Inline bewerkingsmodus via dubbelklik of contextmenu (titel, omschrijving, prioriteit);
updatePbiServer Action; verwijderen met bevestigingsdialoog inclusief waarschuwing cascade;deletePbiServer Action - Done when: titelbewering opgeslagen zonder paginaverversing; verwijderen cascade-verwijdert stories (verifieerbaar in DB)
- Inline bewerkingsmodus via dubbelklik of contextmenu (titel, omschrijving, prioriteit);
-
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
- Klikken op PBI in linkerpaneel toont bijbehorende stories rechts via
-
ST-110 PBI filter
- Filterknop in linkerpaneel navigatiebar; dropdown voor prioriteit (1–4, 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
-
ST-201
usePlannerStoreZustand-store- Schrijf
stores/planner-store.tsmetpbiOrder,storyOrder,taskOrder;init*,reorder*,rollback*actions; TypeScript strict types - Done when: store importeerbaar in een Client Component;
initPbisvult order;reorderPbismuteert order;rollbackPbisherstelt vorige staat
- Schrijf
-
ST-202
useSelectionStoreZustand-store- Schrijf
stores/selection-store.tsmetselectedPbiId,selectedStoryId, setters enclearSelection - Done when: selectie in linkerpaneel via store zichtbaar in rechterpaneel zonder prop drilling
- Schrijf
-
ST-203 dnd-kit setup + PBI drag-and-drop
- Installeer dnd-kit; wrap linkerpaneel in
DndContext+SortableContext; implementeeruseSortableper PBI-rij;onDragEnd: bereken nieuwesort_ordervia float-gemiddelde; optimistisch updaten viausePlannerStore;reorderPbisActionServer Action; rollback bij fout - Done when: PBI versleepbaar binnen prioriteitsgroep; volgorde opgeslagen na loslaten; UI rollback bij gesimuleerde server-fout
- Installeer dnd-kit; wrap linkerpaneel in
-
ST-204 PBI drag-and-drop over prioriteitsgrens
- Uitbreiding ST-203: slepen over een prioriteitsgrens wijzigt
priorityvan het PBI;sort_orderwordt onderaan de doelgroep geplaatst;updatePbiPriorityServer Action - Done when: PBI naar prioriteit 2 slepen vanuit prioriteit 3 wijzigt zowel prioriteit als volgorde
- Uitbreiding ST-203: slepen over een prioriteitsgrens wijzigt
-
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;
createStoryServer Action - Done when: drie stories zichtbaar als blokken; nieuw blok verschijnt in juiste prioriteitsgroep
- 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;
-
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
-
ST-207 Story drag-and-drop (horizontaal, binnen en tussen groepen)
- dnd-kit horizontale
SortableContextper prioriteitsgroep;onDragEnd: herrangschikking via float-gemiddelde instoryOrder; slepen naar andere groep wijzigt prioriteit; optimistisch viausePlannerStore;reorderStoriesActionServer Action; rollback bij fout - Done when: story versleepbaar binnen groep en naar andere groep; volgorde en prioriteit persistent na loslaten
- dnd-kit horizontale
-
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;
updateStoryServer Action - Done when: klikken op blok opent detail; bewerken persisteert; sluiten keert terug naar backlog
- Klikken op storyblok opent slide-over of modal met titel, omschrijving, acceptatiecriteria, statusbadge, activiteitenlog (leeg bij nieuwe story); bewerkformulier voor titel/omschrijving/acceptatiecriteria;
-
ST-209 Story verwijderen
- Verwijderknop in story-detail of contextmenu; bevestigingsdialoog met waarschuwing cascade (taken);
deleteStoryServer Action; blok verdwijnt optimistisch uit het rechterpaneel - Done when: story verwijderd incl. cascade-taken (verifieerbaar in DB); blok direct verdwenen uit UI
- Verwijderknop in story-detail of contextmenu; bevestigingsdialoog met waarschuwing cascade (taken);
-
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
-
ST-301
useSprintStoreZustand-store- Schrijf
stores/sprint-store.ts;initSprint,addStoryToSprint,removeStoryFromSprint,reorderSprintStories,rollbackSprint - Done when: store beheert sprint-story-volgorde onafhankelijk van planner-store
- Schrijf
-
ST-302 Sprint aanmaken
- "Sprint starten" knop op productpagina (zichtbaar als geen actieve Sprint); modal met Sprint Goal invoerveld;
createSprintServer 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
- "Sprint starten" knop op productpagina (zichtbaar als geen actieve Sprint); modal met Sprint Goal invoerveld;
-
ST-303 Sprint Backlog scherm — layout
/products/[id]/sprintpagina;SplitPanemet 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
-
ST-304 Story vanuit Product Backlog naar Sprint slepen
- dnd-kit drag vanuit rechterpaneel naar linkerpaneel;
onDragEnd:addStoryToSprintin store; story krijgt badge "In Sprint" in Product Backlog;addStoryToSprintActionServer Action (zetsprint_id+ statusIN_SPRINT); rollback bij fout - Done when: story gesleept naar Sprint verschijnt links en toont "In Sprint" badge rechts; persistent na herlaad
- dnd-kit drag vanuit rechterpaneel naar linkerpaneel;
-
ST-305 Sprint Backlog story volgorde aanpassen
- dnd-kit verticale
SortableContextin linkerpaneel; herrangschikking via float-gemiddelde inuseSprintStore;reorderSprintStoriesActionServer Action - Done when: volgorde in Sprint Backlog persistent na loslaten en na paginaverversing
- dnd-kit verticale
-
ST-306 Story uit Sprint verwijderen
- Verwijderknop per story in Sprint Backlog;
removeStoryFromSprintActionServer Action (wistsprint_id, zet status terug opOPEN); story verdwijnt links en badge verdwijnt rechts - Done when: verwijderen persistent; story beschikbaar in Product Backlog rechterpaneel
- Verwijderknop per story in Sprint Backlog;
-
ST-307 Sprint Planning scherm — layout
/products/[id]/sprint/planningpagina;SplitPanemet 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
-
ST-308 Taak aanmaken
- "Taak aanmaken" knop in rechterpaneel navigatiebar; inline formulier (titel, omschrijving, prioriteit);
createTaskServer Action; voortgangsindicator per story (bijv. "0/0 Done") - Done when: taak aangemaakt en zichtbaar in takenlijst; voortgangsindicator toont "0/1 Done"
- "Taak aanmaken" knop in rechterpaneel navigatiebar; inline formulier (titel, omschrijving, prioriteit);
-
ST-309 Taak drag-and-drop (verticaal)
- dnd-kit verticale
SortableContextin rechterpaneel; herrangschikking via float-gemiddelde inusePlannerStore.taskOrder;reorderTasksActionServer Action - Done when: taken versleepbaar; volgorde persistent na loslaten
- dnd-kit verticale
-
ST-310 Taakstatus bijhouden
- Status-toggle per taak (TO_DO → IN_PROGRESS → DONE) via klikbare badge of dropdown;
updateTaskStatusServer Action; voortgangsindicator op story updatet optimistisch - Done when: taak op DONE zetten verhoogt teller in voortgangsindicator; persistent na herlaad
- Status-toggle per taak (TO_DO → IN_PROGRESS → DONE) via klikbare badge of dropdown;
-
ST-311 Taak bewerken en verwijderen
- Inline bewerken van titel, omschrijving en prioriteit;
updateTaskServer Action; verwijderen met bevestiging;deleteTaskServer Action - Done when: titelwijziging persisteert; verwijderde taak verdwijnt uit lijst
- Inline bewerken van titel, omschrijving en prioriteit;
-
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?";
completeSprintServer Action zet Sprint op COMPLETED, verwerkt keuzes per story - Done when: Sprint afgerond; stories correct verplaatst naar DONE of OPEN; nieuwe Sprint aanmaakbaar
- "Sprint afronden" knop op Sprint-pagina; dialoog toont per story de status en vraagt: "Markeer als Done of terug naar Backlog?";
-
ST-313 Sprint Board — drie-panelen layout (vervangt ST-303 + ST-307)
- Doel:
/products/[id]/sprintwordt één scherm met drie panelen van links naar rechts: Product Backlog · Sprint Backlog · Taken. De losse/sprint/planningroute 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:
TaskListvoor de geselecteerde story; lege staat "Selecteer een story" als niets geselecteerd; "+ Taak" knop zoals huidig
- Layout:
TriplePanecomponent — drie verticale panelen met twee versleepbare scheidingslijnen; opslaan inlocalStorageper product (key:sprint-triple-${productId}) - DnD: één
DndContextomhult alle drie panelen; drag van links naar midden werkt viaDragOverlay; reorder binnen midden viaSortableContext; taken-reorder in eigen genesteDndContext - State:
SprintBoardClientbeheert sprint stories, product backlog data,selectedStoryId, en taken per story (vanuit server props);useSelectionStore.selectedStoryIdvoor story-selectie - Navigatie: "Sprint Planning →" link onderaan Sprint Backlog pagina verwijderd;
SprintHeaderblijft bovenaan met "Sprint afronden" - Route cleanup:
/sprint/planning/page.tsxvervangt door redirect naar/products/[id]/sprint;PlanningLeft,PlanningRightClientcomponents verwijderen - Done when: één
/sprintpagina 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/planningredirect werkt
- Doel:
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.
-
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 aanStory; voegassigned_stories Story[] @relation("StoryAssignee")toe aanUser; voeg index@@index([sprint_id, assignee_id])toe; migratie viaprisma migrate dev --name add_story_assignee - Auth-helpers: schrijf
lib/auth.tsmetgetSession,requireUser,requireWriter,requireProductAccess,requireProductWriter— laatste twee doen membership-check via owner (Product.user_id) OF lid (ProductMember); demo-check op basis vansession.isDemo(uit ST-006); throwt "Niet beschikbaar in demo-modus" bij demo-write-poging - Done when: migratie slaagt;
requireProductWriterblokkeert demo-user;requireProductAccessaccepteert zowel owner als member
- Schema: voeg
-
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) opbg-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
- Wrapper rond shadcn
-
ST-352 Story-claim Server Actions
- Vier acties in
actions/stories.ts:claimStoryAction(zetassignee_id = currentUserId),unclaimStoryAction(null),reassignStoryAction(valideert dat target user lid van product is),claimAllUnassignedInActiveSprintAction(bulk viaupdateManyvoor ongeclaimde stories in actieve sprint); allemaal Zod-gevalideerd, achterrequireProductWriter, metrevalidatePathvoor/sprintén/solo; tenant-guard viawhere: { 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
- Vier acties in
-
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 alsassignee_id === null); shadcnDropdownMenu(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
- Op story-kaart in middenpaneel van ST-313 Sprint Board: assignee-chip onderaan met
-
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 roeptclaimAllUnassignedInActiveSprintActionaan; Sonner success-toast "{count} stories geclaimd"; pending state viauseTransition - Done when: telling correct; claimen werkt; knop disabled bij 0 ongeclaimd of demo; toast verschijnt na succes
- Knop bovenaan Sprint Backlog paneel met telling: "Claim alle ongeclaimde stories (N)"; disabled als N=0 of
-
ST-355 Solo route —
/soloredirect +/products/[id]/solopagina + cookie- Cookie-helper: schrijf
lib/cookies.tsmetsetLastProductCookie(productId)(HTTP-only, sameSite lax, 30 dagen) /solopage.tsx: Server Component; leest cookielastProductId; valideert toegang en redirect naar/products/[id]/solo, of toont<ProductPicker>als geen cookie of cookie ongeldig/products/[id]/solopage.tsx: Server Component; haalt active sprint op (404 → empty state<NoActiveSprint>); haalt taken op viaTask.findManymetwhere: { sprint_id, story: { assignee_id: session.userId } }+ count ongeclaimde stories parallel; geeft data door aan<SoloBoard>; zetlastProductIdcookie 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:
/solozonder cookie toont picker; met geldige cookie redirect; pagina toont juiste taken; geen actieve sprint toont empty state; cookie persisteert tussen sessies
- Cookie-helper: schrijf
-
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 vanusePlannerStore(ST-201) <SoloBoard>Client Component: root metDndContext(overslaan alsisDemo),PointerSensormetactivationConstraint: { distance: 5 },closestCornerscollision 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/15etc.); 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 draggableonDragEndflow: optimistische update viaoptimisticMove, danupdateTaskStatusActionaanroepen, 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
- Store
-
ST-357 Task detail-dialoog +
updateTaskPlanActionupdateTaskPlanActioninactions/tasks.ts: Zod-schema{ taskId, productId, implementationPlan };requireProductWriter; tenant-guard viawhere: { id: taskId, story: { product_id: productId } };revalidatePath<TaskDetailDialog>shadcnDialog: header met taaktitel + statusbadge (MD3 tokens); sectie Beschrijving (read-only, volg bestaand patroon); sectie Implementatieplan met<Textarea>save-on-blur; on-blur roeptupdateTaskPlanAction, indicator rechtsonder ("Bezig met opslaan…" → "Opgeslagen", vervaagt na 2s); error-toast bij fout; footer-link "Open in Sprint Board ↗"; demo-modus: textareareadOnlymet tooltip- Done when: edit + blur + refresh persisteert; gesimuleerde server-fout toont error-toast; demo-user kan dialoog openen maar niet bewerken
-
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 roeptclaimStoryAction, sheet blijft open (zodat meerdere achter elkaar claimen kan); Sonner success-toast per claim; lege staat "Geen ongeclaimde stories. Lekker bezig!"; pending state viauseFormStatus; 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
- Knop "Toon openstaande stories (N)" bovenaan Solo bord opent shadcn
-
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
/soloen redirect verder
- Voeg
-
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 metassignee_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)
- Update
M4: Claude Code REST API
-
ST-401 API-token infrastructuur
- Schrijf
lib/api-auth.ts: parseerAuthorization: Bearerheader; bereken SHA-256 hash; zoek op inapi_tokens; controleerrevoked_at; retourneeruserIdof 401; retourneer 403 alsis_demo - Done when: geldige token geeft userId terug; ongeldige token geeft 401; ingetrokken token geeft 401; demo-token op schrijf-endpoint geeft 403
- Schrijf
-
ST-402 API-tokenbeheer UI
/settings/tokenspagina; 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)
-
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/productsretourneert correct JSON inclusief gedeelde product backlogs; 401 zonder token
- Route Handler; authenticatie via
-
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
- Route Handler; haal hoogst geprioriteerde OPEN story op van actieve Sprint van het product (priority ASC, sort_order ASC); retourneer
-
ST-405
GET /api/sprints/:id/tasks— taken ophalen- Route Handler met
?limit=Nquery 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=5retourneert max 5
- Route Handler met
-
ST-406
PATCH /api/stories/:id/tasks/reorder— taakvolgorde aanpassen- Route Handler; body:
{ task_ids: string[] }; valideer alle IDs behoren tot de story; updatesort_ordervia 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
- Route Handler; body:
-
ST-407
POST /api/stories/:id/log— activiteit vastleggen- Route Handler; body:
{ type, content, status?, commit_hash?, commit_message? }; Zod-validatie per type; schrijf naarstory_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
- Route Handler; body:
-
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
- Route Handler; body:
-
ST-409
POST /api/todos— todo aanmaken- Route Handler; body:
{ title: string, product_id: string }; valideer dat product bij de geverifieerde gebruiker hoort; schrijf naartodos; 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
- Route Handler; body:
-
ST-410 Story-activiteitenlog UI
- Activiteitenlog sectie in story-detail slide-over; haal
story_logsop via Server Component; render chronologisch; visuele stijl per type (IMPLEMENTATION_PLAN = blauw, TEST_RESULT passed = groen, failed = rood, COMMIT = paars); commit-hash klikbaar alsrepo_urlingesteld; lege staat - Done when: drie log-entries (plan, test, commit) correct gestyled; commit-hash link opent in nieuw tabblad
- Activiteitenlog sectie in story-detail slide-over; haal
M5: Todo-lijst
Herontwerp (april 2026): ST-501–505 beschreven de oorspronkelijke QuickInput-aanpak. Die is geïmplementeerd maar vervangen door een Data Table + detail-kaart ontwerp (ST-509–510). ST-501–505 zijn als referentie bewaard; de functionele eisen zijn ongewijzigd.
-
ST-501 Todo-lijst pagina (vervangen door ST-509)
/todospagina; haal actieve (niet-gearchiveerde) todos op inclusief productnaam; snel-invoerveld bovenaan met product-dropdown (verplicht) en titel (Enter om op te slaan);createTodoServer 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
-
ST-502 Todo afvinken (vervangen door ST-509)
- Checkbox per todo;
toggleTodoServer Action; afgevinkte todos visueel doorgestreept; afgevinkte todos blijven zichtbaar onderaan de lijst - Done when: afvinken persistent na herlaad; visuele doorstreping correct
- Checkbox per todo;
-
ST-503 Afgevinkte todos archiveren (vervangen door ST-510)
- "Archiveer afgeronde items" knop;
archiveCompletedTodosServer Action; gearchiveerde todos verdwijnen uit standaardweergave - Done when: archiveren verbergt alle afgevinkte todos; telling correct
- "Archiveer afgeronde items" knop;
-
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;
promoteTodeToPbiServer Action (maak PBI aan, verwijder todo) - Done when: gepromoveerde todo verdwijnt; PBI zichtbaar in juist product met juiste prioriteit
- "Promoveer naar PBI" contextmenu of knop per todo; dialoog: product dropdown (actieve producten), prioriteit dropdown; titel vooringevuld (bewerkbaar); bevestigingswaarschuwing;
-
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;
promoteTodoToStoryServer Action (maak story aan, verwijder todo) - Done when: gepromoveerde todo verdwijnt; story zichtbaar in juist PBI met juiste prioriteit
- "Promoveer naar story" knop per todo; dialoog: product dropdown → PBI dropdown (gefilterd op product), prioriteit; titel vooringevuld;
-
ST-509 Todo Data Table
- Installeer
@tanstack/react-table; voeg shadcndata-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
archiveSelectedTodosActionaan met de geselecteerde IDs; resettet selectie na afloop
- Paginering: max 10 rijen per pagina; vorige/volgende knoppen; paginatelling ("1–10 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
archiveSelectedTodosActiontoevoegen aanactions/todos.ts: valideert dat alle meegegeven IDs bij de ingelogde gebruiker horen vóór schrijven;archiveManyviaupdateMany- 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
- Installeer
-
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
PromotePbiDialogenPromoteStoryDialog; alleen zichtbaar bij een bestaande geselecteerde todo - Demo-modus: kaart-invoervelden uitgeschakeld; knoppen verborgen of disabled
updateTodoActiontoevoegen aanactions/todos.ts: valideert eigenaarschap; pasttitle,product_iden/ofdoneaan;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
-
ST-506 Rolbeheer in instellingen
/settingspagina met roltoewijzing (checkbox per rol: Product Owner, Scrum Master, Developer); minimaal één rol verplicht;updateRolesServer Action; geselecteerde rollen zichtbaar in profielbalk- Done when: rollen bijwerken persisterend; profielbalk toont gekozen rollen; uitvinken van alle rollen geeft validatiefout
-
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
- 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;
-
ST-508 Product Backlog-overzicht in instellingen (buiten originele backlog toegevoegd)
- Gecombineerde lijst op
/settingsvan 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
- Gecombineerde lijst op
-
ST-511 Entity codes voor Product, PBI en Story (buiten originele backlog toegevoegd)
- Schema:
code String? @db.VarChar(30)opProduct,PbienStory; unique per parent (user_idvoor Product,product_idvoor Pbi/Story);Taskheeft geen DB-veld — code wordt afgeleid als${story.code}.${index_in_story} - Auto-generatie:
lib/code-server.tsmetgenerateNextStoryCode(ST-001,ST-002, … 3-cijferig per product) engenerateNextPbiCode(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 opST-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, vultcodeapart; productScrum4Mekrijgtcode: 'SCRUM4ME', milestones krijgenM0/M3.5/etc., stories krijgenST-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 metST-NNN)
- Schema:
-
ST-512 REST API uitbreidingen voor codes, todo-description en task implementation_plan (buiten originele backlog toegevoegd)
GET /api/products: voegcodetoe (naastid,name,repo_url); optioneeldescriptionendefinition_of_doneGET /api/products/:id/next-story: voegcodetoe op story; voeg per taskcode(derived${story.code}.${index_in_story}) enimplementation_plantoeGET /api/sprints/:id/tasks: voegdescription,implementation_planenstory_codetoe per task; voeg een derivedcode-veld per task toe (${story.code}.${index_in_story})POST /api/todos: accepteer optioneledescription(max 2000 tekens); valideer en sla op; retourneerdescriptionin response- Done when: alle vier endpoints retourneren / accepteren de nieuwe velden zoals beschreven; curl-test toont
codeop products, story en tasks; todo aanmaken via API metdescriptionslaat op
-
ST-513 REST API hardening voor Claude Code (buiten originele backlog toegevoegd)
- Health: nieuwe
GET /api/healthzonder auth; retourneert{ status, version, time }; optioneel?db=1voor DB-ping ({ database: 'ok' | 'down' }) - Claude-context: nieuwe
GET /api/products/:id/claude-context(auth) die in één callproduct,active_sprint,next_story(met tasks), enopen_todosvan de gebruiker terugbrengt — voorkomt round-trips - Status-case op API-boundary: nieuwe
lib/task-status.tsmapper; API exposeert lowercase (todo/in_progress/review/donevoor tasks;open/in_sprint/donevoor stories); DB blijft UPPER_SNAKE; UI ongewijzigd PATCH /api/tasks/:id: accepteert lowercasestatusvia mapper; retourneert lowercase- Story-log metadata: nieuwe optionele
metadata Json?kolom opStoryLog;POST /api/stories/:id/logaccepteert per type een optioneelmetadata-veld (bv.{ branch: 'feat/x' }); bestaande velden ongewijzigd → backwards-compatible - Foutcodes: Zod-validatie geeft
422(was400);400blijft voor malformed body;401/403/404/500ongewijzigd <<<<<<<< HEAD:docs/backlog/index.md - API-documentatie: nieuwe
docs/api/rest-contract.mdmet endpoints, request/response, foutcodes, status-enums en curl-voorbeelden;CLAUDE.mdverwijst ernaar - Done when:
curl /api/healthwerkt zonder auth;curl /api/products/:id/claude-contextretourneert bundled JSON; PATCH/PUT routes accepteren lowercase status en geven 422 bij ongeldige body; story-log POST bewaartmetadata;docs/api/rest-contract.mdis gepubliceerd ======== - API-documentatie: nieuwe
docs/api.mdmet endpoints, request/response, foutcodes, status-enums en curl-voorbeelden;CLAUDE.mdverwijst ernaar - Done when:
curl /api/healthwerkt zonder auth;curl /api/products/:id/claude-contextretourneert bundled JSON; PATCH/PUT routes accepteren lowercase status en geven 422 bij ongeldige body; story-log POST bewaartmetadata;docs/api.mdis gepubliceerd
- Health: nieuwe
origin/main:docs/backlog.md
GET /api/products: voegcodetoe (naastid,name,repo_url); optioneeldescriptionendefinition_of_doneGET /api/products/:id/next-story: voegcodetoe op story; voeg per taskcode(derived${story.code}.${index_in_story}) enimplementation_plantoeGET /api/sprints/:id/tasks: voegdescription,implementation_planenstory_codetoe per task; voeg een derivedcode-veld per task toe (${story.code}.${index_in_story})POST /api/todos: accepteer optioneledescription(max 2000 tekens); valideer en sla op; retourneerdescriptionin 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
codeop products, story en tasks; todo aanmaken via API metdescriptionslaat op
M6: Polish & Launch-ready
-
ST-601 Loading states en skeletons
loading.tsxvoor alle zware routes (/products/[id],/sprint,/sprint/planning); skeletoncomponenten voor PBI-lijst, story-blokken en takenlijst; pending states op alle form-submit-knoppen viauseFormStatus- Done when: navigeren naar een trage route toont skeleton; submit-knoppen disablen tijdens Server Action
-
ST-602 Error boundaries
error.tsxvoor 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
-
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
-
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
isDemoin sessie - Done when: demo-gebruiker ziet alle knoppen maar kan niets wijzigen; tooltip zichtbaar bij hover
- Alle aanmaak-, bewerk- en verwijderknoppen disabled + tooltip "Niet beschikbaar in demo-modus" voor demo-sessies; gebaseerd op
-
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
-
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 M0–M5 flows correct op drie schermbreedtes; melding bij < 1024px
-
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;
roleenaria-selectedop geselecteerde PBI in linkerpaneel - Done when: geen WCAG AA contrastfouten op primaire flows; alle knoppen hebben toegankelijke labels
- Kleurcontrast-check op alle tekst en badges; aria-labels op icon-only knoppen; focus-ring zichtbaar op alle interactieve elementen;
-
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
-
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_membersgekoppeld 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
- 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
-
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
- Workflow:
-
ST-611 README en lokale setup-documentatie
- Schrijf
README.mdmet: 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
- Schrijf
-
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→ curllog(plan, test, commit) → activiteitenlog controleren in UI - Done when: volledige flow werkt zonder fouten of onverwacht gedrag; alle API-responses correct JSON
- Voer handmatig de volledige Lars-flow uit: product aanmaken → PBI's en stories aanmaken → Sprint starten → stories slepen → taken aanmaken → API-token aanmaken → curl
M7: MCP-server voor Claude Code
Aparte repo: 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.
-
ST-701 Repo-skeleton mcp
- npm init, tsconfig strict, .gitignore, MCP SDK 1.29, Prisma 7, zod, tsx; lege
src/index.tsdie op stdio start - Done when:
npx tsx src/index.tsprintrunning on stdiozonder crash;tsc --noEmitslaagt
- npm init, tsconfig strict, .gitignore, MCP SDK 1.29, Prisma 7, zod, tsx; lege
-
ST-702 Schema-sync via git submodule
- Submodule
vendor/scrum4me,scripts/sync-schema.shkopieertschema.prismaen strip degenerator erd-block,npm run prisma:generateals postinstall - Done when:
npm run sync-schema && npm run prisma:generatewerkt op een verse clone
- Submodule
-
ST-703 Auth en Prisma-singleton
src/auth.tsSHA-256 hash vanSCRUM4ME_TOKEN→ lookup inapi_tokens, cached{ userId, isDemo };requireWriteAccess()throwtPermissionDeniedErrorvoor demosrc/prisma.tslazy proxy zodat bootstrap niet crasht zonderDATABASE_URL- Done when: ongeldig token geeft
SCRUM4ME_TOKEN is invalid or revoked; demo-tokens blokkeren writes
-
ST-704 Status-mappers + error-helpers
src/status.tszelfde mappers als RESTlib/task-status.tssrc/errors.tsformatZodError,toolError,toolJson,withToolErrorswrapper- Done when: zod-fouten en
PermissionDeniedworden als gestructureerde MCP-errors teruggegeven
-
ST-705 Read-tools —
health,list_products,get_claude_contexthealthdoetSELECT 1;list_productsmet product-access filter;get_claude_contextbundelt product + active sprint + next story (met tasks) + 50 open todos- Done when: smoke-test tegen live DB groen voor alle drie
-
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
- Status-input lowercase (
-
ST-707 Log-tools —
log_implementation,log_test_result,log_commit- Append
StoryLogmet juistetype; optioneelmetadataJSONB - Done when: drie logs verschijnen in story-activiteit met
type/status/commit_hash/metadatazoals meegegeven
- Append
-
ST-708
create_todo-tool- Optionele
description(max 2000) enproduct_id(gevalideerd via access-check) - Done when: nieuwe todo verschijnt in
/todosvoor de tokengebruiker
- Optionele
-
ST-709 Prompt
implement_next_story- Workflow:
get_claude_context→ plan → log_implementation → per taskin_progress/done→ tests →log_test_result→log_commit - Done when: prompt zichtbaar in MCP-clients met argument
product_id
- Workflow:
-
ST-710 README + Claude Code-config + smoke-test
- README beschrijft setup, tools-tabel, schema-sync,
~/.claude/mcp_servers.jsonsnippet, risico's scripts/smoke-test.tsvalideert read-tools tegen live DB- Done when: smoke-test groen; MCP Inspector toont 9 tools + 1 prompt
- README beschrijft setup, tools-tabel, schema-sync,
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 1–2 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).
-
ST-801 Postgres LISTEN/NOTIFY-infrastructuur
- Migratie met
notify_solo_change()-functie +AFTER INSERT/UPDATE/DELETE-triggers optasksenstories; payload bevatop,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
- Migratie met
-
ST-802 SSE-route
/api/realtime/soloapp/api/realtime/solo/route.ts,runtime: 'nodejs',maxDuration: 300; auth via iron-session, query-paramproduct_id, opentpg.ClientopDIRECT_URLmetLISTEN; heartbeat 25s; hard close 240s; in-handler filtering op product/sprint/assignee- Done when:
curl -Nop localhost levert binnen 1s een event op na een task-mutatie via UI
-
ST-803 Client hook
useSoloRealtime(productId)lib/realtime/use-solo-realtime.ts; opentEventSource, 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
-
ST-804 Solo-store realtime-acties
applyTaskUpdate,applyTaskCreate,applyTaskDelete,applyStoryAssignment,markPending/clearPendingom eigen optimistic-echo te onderdrukken- Done when: unit-test op solo-store met gesimuleerde events laat juiste eindstate zien
-
ST-805 Wire-up in SoloBoard + UI-indicator
components/solo/solo-board.tsxroept 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 1–2s in tab B zonder refresh
-
ST-806 Documentatie + acceptatietest <<<<<<<< HEAD:docs/backlog/index.md
- Sectie "Realtime updates" in
docs/architecture.mdmet diagram en filtering-regels; vermelding inCLAUDE.md; korte note over/api/realtime/soloindocs/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.mdmet diagram en filtering-regels; vermelding inCLAUDE.md; korte note over/api/realtime/soloindocs/api.md; handmatig E2E-scenario's gedraaid (zelfde gebruiker twee tabs, MCP-write, REST-write, story-claim, network-flap)
- Sectie "Realtime updates" in
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
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.
-
ST-901 Database —
user.active_product_id- Voeg
active_product_id String? @db.Uuidtoe aanUsermet FK naarProduct.idenonDelete: SetNull; migratieadd_user_active_product_id; index opactive_product_idvoor join-performance - Done when:
npx prisma migrate devslaagt;prisma studiotoont kolom;npx prisma validatezonder fouten; submodulevendor/scrum4mein mcp draaitprisma generate+tsc --noEmitzonder fouten
- Voeg
-
ST-902 Server Actions — actief product zetten en wissen
actions/active-product.tsmetsetActiveProduct(productId)enclearActiveProduct(); Zod + auth +productAccessFilter; demo-gebruikers mogen wisselen (sessie-effect alleen, geen DB-write);archiveProductenleaveProductzettenactive_product_idopnullals 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"
-
ST-903 App-layout laadt actief product + redirects
app/(app)/layout.tsxhaaltactiveProduct(id, name, archived) op naast user; geef door aanNavBar;app/(app)/solo/page.tsxgebruiktuser.active_product_idi.p.v.getLastProductCookie; helperlib/cookies.ts:getLastProductCookiemarkeren 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]/solozonder cookie te raadplegen
-
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 (shadcnDropdownMenu) met je producten + "Producten beheren →"; Sprint krijgtaria-disabled+ tooltip "Geen actieve sprint" als er geen sprint met statusACTIVEis - 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
- Tabs worden: Producten (
-
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-tokenbg-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
-
ST-906 Edge cases — toegangsverlies en archivering
- Wanneer een PB wordt gearchiveerd, ge-leaved, of een productmember wordt verwijderd:
active_product_idautomatischnullvoor betroffen users (server actions vanarchiveProduct,leaveProduct,removeMember); guard inapp/(app)/layout.tsx: alsactive_product_idis gezet maar product is archived/onbereikbaar, server-side clear + redirect naar/dashboardmet 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
- Wanneer een PB wordt gearchiveerd, ge-leaved, of een productmember wordt verwijderd:
-
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.tsvoor 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 buildgroen; spec-secties geschreven;vendor/scrum4me-submodule in mcp gesynced
- Functional spec: nieuw hoofdstuk "Actief Product Backlog" (concept, menugedrag, edge cases); README: navigatie-screenshot bijwerken;
M10: Password-loze inlog via QR-pairing
Implementatieplan: docs/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 1–2 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-relationUser.login_pairings;@@index([expires_at]),@@index([status, expires_at]);statusals string (pending|approved|consumed|cancelled); twee hash-kolommen scheiden mobiel-bewijs van desktop-bewijs - Trigger:
notify_pairing_change()+AFTER INSERT/UPDATEoplogin_pairings;pg_notify('scrum4me_pairing', payload)met{ pairing_id, status, op }; analoog aannotify_solo_changeuit 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 oplogin_pairings; beide hash-kolommen zijnNOT NULL
- Schema:
-
ST-1002 Pairing-helpers + sessie-uitbreiding + pre-auth-cookie
lib/auth/pairing.ts:generateMobileSecret()engenerateDesktopToken()(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,Securein prod,SameSite=Lax,Path=/api/auth/pair,Max-Age=120);readPairCookie(request)returntdesktopToken | null;clearPairCookie(response)op claim/cancelSessionDatainlib/session.ts: voeg optionelepaired?: booleanenpairedExpiresAt?: numbertoeapp/(app)/layout.tsx: extra guard — alssession.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 losmobileSecret+desktopToken; insertLoginPairingmet 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 inlogin_pairingszonder plaintext secret of desktop-token
- Route Handler zonder auth; leest UA + best-effort IP (
-
ST-1004 SSE-route
/api/auth/pair/stream/[pairingId](cookie-auth)runtime: 'nodejs',maxDuration: 300; pairingId in pad (niet sensitief), auth vias4m_pair-cookie: sha256(cookie) matchtdesktop_token_hashvan pairing metpairingIdenexpires_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.tsop kanaalscrum4me_pairing; filter notifications oppairing_id - Auto-close bij status
consumed/cancelledof 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 tegensecret_hash, status pending→approved, bumptexpires_at+5 min, zetuser_id+approved_at),cancelPairing(pairingId, mobileSecret)app/(app)/m/pair/page.tsx: Server Component achter de bestaande(app)/layout.tsxauth-guard; leest géén query-params — alleen statische uitleg + een client-islandapp/(app)/m/pair/pair-confirmation.tsx: Client Component die bij mountwindow.location.hashparseert (#id=…&s=…), via Server ActiongetPairingForApprovalde UA/IP/username ophaalt, dan toont "Inloggen op {ua} ({ip}) als {jouw-username}?" met Bevestig/Annuleer-knoppen dieapprovePairing/cancelPairingaanroepen; succes-state "Klaar — je kunt deze tab sluiten". Wistlocation.hashna 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; clears4m_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_pairis na succes geclear'd
- Auth via
-
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 → POSTpair/start(credentials: 'same-origin'zodats4m_pair-cookie wordt geaccepteerd) → render QR metqrUrl(fragment-URL) → openEventSource('/api/auth/pair/stream/<pairingId>', { withCredentials: true })→ bijapprovedevent POSTpair/claim(cookie-only) → bij succesrouter.push('/dashboard'); aftellende timer (2 min); bij timeout "Vernieuwen"-knop; cleanup bij unmount/redirect <<<<<<<< HEAD:docs/backlog/index.mdapp/login/page.tsx: knop "Inloggen via mobiel" naast bestaande wachtwoord-form (MD3-tokens uitdocs/design/styling.md) ========app/login/page.tsx: knop "Inloggen via mobiel" naast bestaande wachtwoord-form (MD3-tokens uitdocs/styling.md)
- Dependency:
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
/dashboardmetsession.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 belandendocs/patterns/qr-login.md: nieuw pattern-doc voor toekomstige features die hetzelfde unauth-SSE-via-pre-auth-cookie-patroon willen hergebruikenCLAUDE.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
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
ClaudeQuestionschema + 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 opUser(asked_questions,answered_questions),Story,Task,Product; indexes(story_id, status),(product_id, status),(status, expires_at);product_idgedenormaliseerd voor SSE-filter - Trigger:
notify_question_change()AFTER INSERT/UPDATE; emit opscrum4me_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_changestoont nieuweentity: 'question'-payload bij INSERT; bestaande solo-realtime-flow ongewijzigd; submodule sync na merge
- Schema:
-
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 totwait_seconds(max 600); demo-blok viarequireWriteAccess; access-check viauserCanAccessProduct(story.product_id, ...)get_question_answer(read): haalt status + antwoord op een specifieke vraag oplist_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_questionmetwait_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 --noEmitclean
-
ST-1103 Server Action
answerQuestionactions/questions.ts:answerQuestion(questionId, answer)met getSession + Zod + demo-blok +requireProductWriterviaquestion.product_id; atomicupdateMany 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 test6/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); filterpayload.entity === 'question'énpayload.product_idin 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: inshouldEmitif (payload.entity === 'question') return falsetoevoegen — 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 -Nlevert events binnen 1s na INSERT; cross-product-test (user-A ziet user-B's vragen niet)
- Route Handler
-
ST-1105 Notifications-UI (Bell + Sheet + Answer-modal + Zustand-store)
stores/notifications-store.ts— Zustand store volgenssolo-store.ts-patroon:init,add,update,remove,optimisticAnswer,rollbackAnswer; selectorsopenCount,forYouCountlib/realtime/use-notifications-realtime.ts— analoog aanuseSoloRealtime; EventSource op/api/realtime/notificationsmet reconnect-backoffcomponents/notifications/notifications-bridge.tsx— Server Component die initial-data fetcht en aan store geeft; mount inapp/(app)/layout.tsxnaast<SoloRealtimeBridge /><<<<<<<< HEAD:docs/backlog/index.mdcomponents/shared/notifications-bell.tsx— Bell-icon (Lucide) met badge in NavBar (links van avatar); MD3-tokens uitdocs/design/styling.md========components/shared/notifications-bell.tsx— Bell-icon (Lucide) met badge in NavBar (links van avatar); MD3-tokens uitdocs/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 viauseTransition+ 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-questionsapp/api/cron/expire-questions/route.ts— POST handler beveiligd viaAuthorization: Bearer ${CRON_SECRET};updateMany WHERE status='open' AND expires_at<now → status='expired'vercel.json—cronsentry:{ path: '/api/cron/expire-questions', schedule: '0 4 * * *' }(dagelijks; Vercel Hobby-plan staat alleen daily crons toe)lib/env.ts+.env.example—CRON_SECRETvia Zod- Optioneel: ook M10's
login_pairings-cleanup in dezelfde route opnemen - Done when: handmatige
curl -X POSTmet 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 usersCLAUDE.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
- Alle M0–M6 tasks afgerond
- Volledige Lars-flow succesvol doorlopen (ST-612)
- Alle 7 API-endpoints getest via curl (ST-403 t/m ST-409)
- Demo-gebruiker kan inloggen en heeft geen schrijfrechten (ST-604)
- App lokaal opzetbaar via README zonder extra hulp (ST-611)
- CI/CD actief — falende build blokkeert merge (ST-610)
- Beveiligingsreview API geslaagd (ST-609)
- Geen bekende blocker-bugs