74 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 - 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 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
- Health: nieuwe
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
- 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) - Done when: alle scenario's lopen door zonder onverwachte gedragingen
- Sectie "Realtime updates" in
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/redirectapp/login/page.tsx: knop "Inloggen via mobiel" naast bestaande wachtwoord-form (MD3-tokens uitdocs/design/styling.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
- Dependency:
-
ST-1008 Documentatie + acceptatietest
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-jardocs/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 />components/shared/notifications-bell.tsx— Bell-icon (Lucide) met badge in NavBar (links van avatar); MD3-tokens uitdocs/design/styling.mdcomponents/notifications/notifications-sheet.tsx— shadcn Sheet van rechts; lijst gegroepeerd per product; story-assignee krijgt visuele "wacht op jou"-emphasecomponents/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
docs/api/rest-contract.md: secties "SSE — Notifications" + "Cron — Expire questions" met curl-voorbeeldendocs/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