* docs(ST-1101..1108): add M11 — Claude question-channel milestone to backlog
Plant acht stories ST-1101..ST-1108 voor het persistente vraag-antwoord-kanaal
tussen Claude (MCP) en de actieve gebruiker. Eerste concrete uitwerking van
de AI-driven dev-flow-richting (strategisch besluit "B" uit overleg na M10).
Beveiligingsuitgangspunt: atomic answer via updateMany WHERE status='open',
demo-blok op write-tools, access-check via productAccessFilter in DB-query én
SSE-filter, cron-endpoint via Bearer-secret, geen vraag/antwoord-tekst in logs.
Hergebruikt bestaande scrum4me_changes-channel (uitgebreid met entity:'question')
en het LISTEN/NOTIFY+ReadableStream-pattern uit M8/M10. Nieuw: user-scoped SSE
op /api/realtime/notifications zodat de bell globaal werkt over producten heen.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(M11): swap demo-active sprint from M10 to M11
M10 is gemerged en afgesloten — M11 wordt de nieuwe demo-actieve milestone
zodat get_claude_context (via MCP) ST-1101 als next-story teruggeeft.
Drie maps in parse-backlog.ts uitgebreid: M11 priority=4, goal omschrijving,
sprint_status='ACTIVE'. M10 → COMPLETED.
Vereist npx prisma db seed na deze commit zodat de live DB de nieuwe
sprint-state weerspiegelt.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(ST-1108): add F-11b — Claude question-channel to functional spec
Voegt feature-omschrijving toe naast bestaande F-11 (Claude Code REST API).
Beschrijft het verloop (Claude → MCP-tool → DB → trigger → SSE → user → answer
→ trigger → Claude polls), acceptatiecriteria (8 items), randgevallen (offline-
Claude, assignee-change, expiry, abuse) en datamodel (claude_questions tabel).
Persona Lars als primair, Dina secundair voor klant-werk.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(M11): drop parser ACTIVE-flip; sprint goes via UI from now on
Bij M9/M10 hebben we de seed-flip (MILESTONE_SPRINT_STATUS pivot) gebruikt om
nieuwe stories als IN_SPRINT in een verse sprint te krijgen. Dat werkt maar
is fragiel:
- npm run seed wist user-data
- de "sprint" die de seed maakt is geen echte planning-actie
- bij multi-product scenario's breekt het model
Vanaf M11 gebruiken we de bestaande Sprint-creatie-UI van Scrum4Me. Stories
voor M11 worden via scripts/insert-milestone.ts (idempotent insert, geen
seed-reset) aan de DB toegevoegd; de gebruiker maakt zelf een Sprint aan in
/products/[scrum4me]/sprint en sleept ST-1101..1108 ernaartoe.
Parser-map M11 dus terug naar COMPLETED zodat een eventuele re-seed niet meer
een fake sprint aanmaakt voor M11-stories.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(ST-1101): add ClaudeQuestion model + notify_question_change trigger
Schema (prisma/schema.prisma):
- Nieuw model ClaudeQuestion: id (cuid), story_id (FK Cascade), task_id?
(FK SetNull), product_id (FK Cascade — gedenormaliseerd uit story.product_id
voor SSE-filter zonder join), asked_by (FK Restrict — Claude-token-houder),
question (Text), options (Json? — string[] voor multi-choice), status
('open'|'answered'|'cancelled'|'expired'), answer (Text?), answered_by
(FK SetNull), answered_at?, created_at, expires_at
- Indexes: (story_id, status), (product_id, status), (status, expires_at)
- Back-relations: User.asked_questions (ClaudeQuestionAsker),
User.answered_questions (ClaudeQuestionAnswerer), Story.claude_questions,
Task.claude_questions, Product.claude_questions
Migratie (20260427224849_add_claude_questions):
- Prisma-gegenereerde DDL voor claude_questions + indexes + 5 FK's
- Toegevoegde notify_question_change() functie + claude_questions_notify trigger
op AFTER INSERT/UPDATE
- Emit op BESTAANDE scrum4me_changes-channel met entity:'question' (i.t.t. M10
dat eigen scrum4me_pairing-channel kreeg) — solo-route in ST-1104 moet
entity='question' wegfilteren om regressie op solo-board te voorkomen
- Trigger leest story.assignee_id voor "wacht op jou"-emphase in payload
- DELETE niet ondersteund — questions gaan naar answered/cancelled/expired
Verification: Node pg-client roundtrip via DATABASE_URL toonde correcte payloads
bij INSERT (op=I, status=open) en UPDATE (op=U, status=answered) met alle FK-IDs
en assignee_id correct uit story-join.
Volgende stap M11: ST-1102 — vier MCP-tools in scrum4me-mcp-repo
(ask_user_question, get_question_answer, list_open_questions, cancel_question).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(ST-1103): add answerQuestion server action
actions/questions.ts:
- answerQuestion(questionId, answer) — auth + Zod + demo-blok + access-check
via productAccessFilter (anyone met product-membership mag antwoorden,
consistent met Scrum self-organizing — niet alleen story-assignee)
- Atomic prisma.claudeQuestion.updateMany WHERE id + status='open' +
expires_at>now → status='answered'; concurrent dubbele submit: één wint
(count=1), rest count=0 met disambiguatie via second findFirst
- revalidatePath('/', 'layout') refresh't NavBar bell-count voor SSR-paths;
realtime updates voor andere clients gaan via SSE in ST-1104/1105
- Begrijpelijke NL-foutmeldingen voor elk faalpad
Tests __tests__/actions/questions.test.ts (6 cases):
- happy: status update + revalidatePath called
- demo-block: error + geen DB-call + geen revalidate
- geen access: error + geen update
- al-answered: race-error 'is al answered'
- expired: race-error 'is verlopen'
- lege answer: Zod-validatie
Quality gates: lint 0 errors, tsc clean, vitest 145/145 (17 files).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(ST-1104): add user-scoped /api/realtime/notifications + filter solo-route
Twee delen:
1. Solo-route filter (1-regel-fix in app/api/realtime/solo/route.ts):
- NotifyPayload uitgebreid met entity:'question'
- shouldEmit returnt direct false bij entity='question'
Voorkomt dat solo-clients M11 question-events ontvangen (geen lekkage naar
het Solo-bord; geen onnodig netwerk-verkeer; loose coupling tussen features).
2. Nieuwe SSE-route app/api/realtime/notifications/route.ts:
- User-scoped (geen ?product_id=); query alle accessible product-IDs één keer
bij connect via productAccessFilter
- LISTEN scrum4me_changes; filter entity='question' && product_id ∈ accessible
- Initial-state-event NA LISTEN actief (race-fix conform M10 ST-1004):
query open vragen voor deze user's accessible products, stuur als event:state
met summary (id, story_code/title, assignee_id, question, options, expires_at)
- Hergebruikt het pg.Client + ReadableStream + heartbeat 25s + hard-close 240s +
abort-cleanup-pattern uit solo-route
Tests __tests__/api/notifications-stream.test.ts:
- 401 zonder iron-session cookie (en geen DB-call)
- Solo-route filter wordt visueel/E2E gedekt in ST-1108-acceptatie
Quality gates: lint 0 errors, tsc clean, vitest 146/146 (18 files).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(ST-1105): add NavBar bell + sheet + answer-modal + Zustand store + SSE hook
UI-volledig voor de Claude vraag-antwoord-flow (M11). Bel-icon links van avatar
in NavBar; klik opent slide-over rechts met openstaande vragen; klik op een vraag
opent een modal voor antwoord. Story-assignee = current user krijgt visuele
"voor jou"-emphase met primary-container accent en error-color badge-ring.
Bestanden:
- stores/notifications-store.ts — Zustand store met init/upsert/remove +
openCount/forYouCount selectors (vereenvoudigd vs solo-store: geen pendingOps,
geen optimistic-echo-onderdrukking)
- lib/realtime/use-notifications-realtime.ts — EventSource hook met state-
event en message-event handling, exponential-backoff reconnect, Page
Visibility pause-resume
- components/notifications/notifications-bridge.tsx — Server Component die
initial open-questions fetcht via productAccessFilter
- components/notifications/notifications-realtime-mount.tsx — tiny client
island dat de store hydrateert + de hook activeert
- components/notifications/notifications-sheet.tsx — shadcn Sheet met item-
lijst, "voor jou"-accent voor assignee-vragen, lege staat
- components/notifications/answer-modal.tsx — Dialog met options-radio of
free-text Textarea (max 4000), char-counter, demo-blok via Tooltip; bij
succes optimistisch remove + sheet blijft open zodat meerdere vragen
achter elkaar te beantwoorden zijn
- components/shared/notifications-bell.tsx — Bell-icon met badge (count >9 → "9+"),
ring-accent als forYouCount > 0, ARIA-label voor screenreaders
Wiring:
- components/shared/nav-bar.tsx — <NotificationsBell /> rechts naast <UserMenu>
- app/(app)/layout.tsx — <NotificationsBridge /> naast <SoloRealtimeBridge />,
user.id (server-side) als prop
base-ui-aanpassingen: SheetTrigger/TooltipTrigger gebruiken render-prop ipv
asChild (geen Radix).
Quality gates: lint 0 errors, tsc clean, vitest 146/146, npm run build groen.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(ST-1106): add cross-product access-isolation test for notifications SSE
Demo-policy + assignee-emphase zaten al in eerdere stories:
- answerQuestion demo-blok in actions/questions.test.ts (ST-1103)
- AnswerModal demo-tooltip in components/notifications/answer-modal.tsx (ST-1105)
- requireWriteAccess in MCP write-tools (ST-1102)
Deze story voegt expliciet een access-isolation-test toe op de notifications-
SSE-route: productAccessFilter wordt met de echte userId aangeroepen, en
prisma.product.findMany filter't op archived=false + user_id-scope. Dat
garandeert dat een gebruiker geen question-events ontvangt voor producten waar
hij geen membership op heeft.
Story-assignee-emphase blijft visueel-only (NotificationsBell ring-accent +
Sheet primary-container) — toegang werkt product-membership-breed zodat een
team-lid kan invallen als de assignee niet beschikbaar is.
Quality gates: lint 0 errors, tsc clean, vitest 147/147 (was 146).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(ST-1107): add Vercel cron expire-questions (+ M10 pairing cleanup)
POST /api/cron/expire-questions:
- Auth via Authorization: Bearer ${CRON_SECRET} (Vercel injecteert dit
automatisch wanneer de env-var op de project-omgeving staat); 401 als secret
niet matcht of niet is gezet (faal-veilig — geen open endpoint in dev)
- updateMany op claude_questions WHERE status='open' AND expires_at<now →
'expired'
- Bonus: zelfde route ruimt M10 login_pairings op (status='pending' AND
expires_at<now → 'cancelled'). Eén cron-job is goedkoper qua Vercel-budget
en houdt cleanup-strategie centraal — opvolg-actie uit M10 dat geparkeerd was.
Config:
- vercel.json: crons-entry { path: '/api/cron/expire-questions', schedule: '0 */6 * * *' } (4x/dag)
- lib/env.ts: CRON_SECRET als optional in Zod-schema
- .env.example: documentatie + openssl rand-tip
Tests __tests__/api/cron-expire-questions.test.ts (4 cases):
- 401 zonder Authorization-header
- 401 met verkeerde secret
- 401 als CRON_SECRET niet is gezet (faal-veilig)
- 200 met juiste secret: response { expired_questions, expired_pairings, ran_at }
+ beide updateMany WHERE/data correct
Quality gates: lint 0 errors, tsc clean, vitest 151/151.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(ST-1108): document M11 question-channel — API + architecture + pattern
docs/API.md — twee nieuwe secties:
- 'Notifications' met /api/realtime/notifications SSE-endpoint (event-shapes,
filter-rules, voorbeeld)
- 'Cron — Expire questions' met /api/cron/expire-questions (Bearer-auth,
schedule, response-shape, manual curl)
docs/scrum4me-architecture.md — nieuw hoofdstuk 'Vraag-antwoord-kanaal Claude
↔ user' tussen QR-pairing-flow en Projectstructuur:
- Mermaid sequence-diagram (Claude → DB → trigger → SSE → user → answer →
trigger → Claude polls)
- Threat-model-tabel (race, demo-misbruik, cross-product leak, cron-misbruik,
growth, log-leakage)
- Subsectie 'Waarom hergebruik scrum4me_changes-kanaal' met trade-off vs M10's
eigen-kanaal-aanpak
docs/patterns/claude-question-channel.md — herbruikbaar pattern 'Bidirectionele
async-comms tussen MCP-agent en interactieve user' met de vier eindpunten,
vier security-uitgangspunten, channel-strategie-tabel, TTL-richtlijn, en
sjabloon-bestanden per laag (DB / server / client / MCP-tools).
CLAUDE.md — extra rij in Implementatiepatronen-tabel die naar het nieuwe
pattern-doc verwijst.
Acceptatie 6 scenario's:
1. Sync happy path (MCP wait_seconds + UI submit) — handmatig getest tijdens
ST-1105 acceptance-loop met de q-test injection
2. Async happy path — gedekt door get_question_answer-tool in ST-1102 +
list_open_questions
3. Demo-block — actions/questions.test.ts (case 2: demo-user) + AnswerModal
tooltip (visueel)
4. Access-isolation — notifications-stream.test.ts (case 'access-isolation')
5. Expiry — cron-expire-questions.test.ts (case '200 met juiste secret')
6. Race — actions/questions.test.ts (case 'al-answered' via atomic updateMany)
Quality gates: lint 0 errors, tsc clean, vitest 151/151 (19 files), npm run
build groen.
M11 is hiermee feature-compleet. feat/M11-claude-questions heeft 12 commits
lokaal, klaar voor user-acceptatie en PR.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(ST-1107): cron schedule daily — Vercel Hobby allows only 1 run/day
Vercel deploy faalde met:
> Hobby accounts are limited to daily cron jobs.
> This cron expression (0 */6 * * *) would run more than once per day.
Schedule van 4×/dag (0 */6 * * *) naar 1×/dag (0 4 * * * — 04:00 UTC, rustig
tijdstip). Functioneel acceptabel: ClaudeQuestion TTL is 24u, dus daily
cleanup pakt alles dat in de afgelopen 24u verlopen is. Login-pairings TTL
is 2 min — die zijn al onbruikbaar zodra ze expiren, cron is alleen voor
status-housekeeping.
Schedule-referenties consistent bijgewerkt in docs (API.md, architecture,
backlog M11-sectie, plan-doc, pattern-doc) + comment in route.ts. Vermelding
overal dat dit een Hobby-plan-beperking is en Pro fijnmaziger ondersteunt.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
751 lines
74 KiB
Markdown
751 lines
74 KiB
Markdown
# 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 `scrum4me-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 `solo-paneel-spec.md`.
|
||
|
||
- [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
|
||
- **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
|
||
- **`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 scrum4me-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
|
||
- Sectie "Realtime updates" in `docs/scrum4me-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)
|
||
- 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 scrum4me-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 scrum4me-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
|
||
- **`app/login/page.tsx`:** knop "Inloggen via mobiel" naast bestaande wachtwoord-form (MD3-tokens uit `docs/scrum4me-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 `/dashboard` met `session.paired === true`; QR vernieuwt na expiry; geen secret zichtbaar in DevTools Network-tab onder URL-kolommen
|
||
|
||
- [ ] **ST-1008** Documentatie + acceptatietest
|
||
- **`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`
|
||
- **`docs/scrum4me-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 scrum4me-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 />`
|
||
- **`components/shared/notifications-bell.tsx`** — Bell-icon (Lucide) met badge in NavBar (links van avatar); MD3-tokens uit `docs/scrum4me-styling.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
|
||
- **`docs/API.md`:** secties "SSE — Notifications" + "Cron — Expire questions" met curl-voorbeelden
|
||
- **`docs/scrum4me-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
|