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