chore: mark completed tasks as done in backlog (ST-001–ST-510, ST-605, ST-608)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Janpeter Visser 2026-04-26 16:03:43 +02:00
parent 21befd0e5b
commit 3a7861df44

View file

@ -18,7 +18,8 @@ De MVP is klaar wanneer Lars — de primaire persona — de volledige cyclus kan
| 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-312 |
| 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 |
@ -29,35 +30,35 @@ De MVP is klaar wanneer Lars — de primaire persona — de volledige cyclus kan
### M0: Foundation
- [ ] **ST-001** Project scaffolding
- [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
- [ ] **ST-002** Prisma v7 setup + `prisma.config.ts`
- [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
- [ ] **ST-003** Database schema migratie (volledige initiële migratie)
- [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
- [ ] **ST-004** Seed met testdata
- [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
- [ ] **ST-005** Environment variabelen + `lib/env.ts`
- [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
- [ ] **ST-006** Authenticatie — registratie en inloggen
- [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
- [ ] **ST-007** Route-beveiliging via `proxy.ts`
- [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`
- [ ] **ST-008** Navigatieshell + dashboard-layout
- [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
@ -65,43 +66,43 @@ De MVP is klaar wanneer Lars — de primaire persona — de volledige cyclus kan
### M1: Producten & Product Backlog
- [ ] **ST-101** Product aanmaken
- [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
- [ ] **ST-102** Productenlijst op dashboard
- [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
- [ ] **ST-103** Product bewerken en archiveren
- [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
- [ ] **ST-104** Gesplitst scherm layout component (`SplitPane`)
- [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
- [ ] **ST-105** Navigatiebar-component per paneel
- [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
- [ ] **ST-106** PBI aanmaken en weergeven
- [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
- [ ] **ST-107** PBI prioriteitsgroepen met visuele scheiding
- [x] **ST-107** PBI prioriteitsgroepen met visuele scheiding
- Render PBI's gegroepeerd per prioriteit (14) met gelabelde scheidingslijn per groep (bijv. "Kritiek", "Hoog"); lege groepen zijn niet zichtbaar; prioriteitsbadge per PBI
- Done when: vier prioriteitsgroepen correct gerenderd met labels; PBI met prioriteit 1 staat boven prioriteit 4
- [ ] **ST-108** PBI bewerken en verwijderen
- [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)
- [ ] **ST-109** PBI selecteren → stories laden
- [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
- [ ] **ST-110** PBI filter
- [x] **ST-110** PBI filter
- Filterknop in linkerpaneel navigatiebar; dropdown voor prioriteit (14, alle); filter werkt realtime op gerenderde lijst; actief filter zichtbaar als badge; wissen via ×-knop
- Done when: filter op prioriteit 1 verbergt alle andere PBI's; wissen herstelt volledige lijst
@ -109,43 +110,43 @@ De MVP is klaar wanneer Lars — de primaire persona — de volledige cyclus kan
### M2: Stories & Drag-and-drop
- [ ] **ST-201** `usePlannerStore` Zustand-store
- [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
- [ ] **ST-202** `useSelectionStore` Zustand-store
- [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
- [ ] **ST-203** dnd-kit setup + PBI drag-and-drop
- [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
- [ ] **ST-204** PBI drag-and-drop over prioriteitsgrens
- [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
- [ ] **ST-205** Story aanmaken en weergeven als blokken
- [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
- [ ] **ST-206** Story prioriteitsgroepen met visuele scheiding
- [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
- [ ] **ST-207** Story drag-and-drop (horizontaal, binnen en tussen groepen)
- [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
- [ ] **ST-208** Story detail-modal / slide-over
- [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
- [ ] **ST-209** Story verwijderen
- [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
- [ ] **ST-210** Story filter in rechterpaneel
- [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
@ -153,55 +154,55 @@ De MVP is klaar wanneer Lars — de primaire persona — de volledige cyclus kan
### M3: Sprint Backlog & Sprint Planning
- [ ] **ST-301** `useSprintStore` Zustand-store
- [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
- [ ] **ST-302** Sprint aanmaken
- [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
- [ ] **ST-303** Sprint Backlog scherm — layout
- [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
- [ ] **ST-304** Story vanuit Product Backlog naar Sprint slepen
- [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
- [ ] **ST-305** Sprint Backlog story volgorde aanpassen
- [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
- [ ] **ST-306** Story uit Sprint verwijderen
- [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
- [ ] **ST-307** Sprint Planning scherm — layout
- [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
- [ ] **ST-308** Taak aanmaken
- [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"
- [ ] **ST-309** Taak drag-and-drop (verticaal)
- [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
- [ ] **ST-310** Taakstatus bijhouden
- [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
- [ ] **ST-311** Taak bewerken en verwijderen
- [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
- [ ] **ST-312** Sprint afronden
- [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
- [ ] **ST-313** Sprint Board — drie-panelen layout (vervangt ST-303 + ST-307)
- [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)
@ -216,45 +217,105 @@ De MVP is klaar wanneer Lars — de primaire persona — de volledige cyclus kan
---
### 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`.
- [ ] **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
- [ ] **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
- [ ] **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
- [ ] **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
- [ ] **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
- [ ] **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
- [ ] **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
- [ ] **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
- [ ] **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
- [ ] **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
- [ ] **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
- [ ] **ST-401** API-token infrastructuur
- [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
- [ ] **ST-402** API-tokenbeheer UI
- [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)
- [ ] **ST-403** `GET /api/products` — productenlijst
- [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
- [ ] **ST-404** `GET /api/products/:id/next-story` — volgende story ophalen
- [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
- [ ] **ST-405** `GET /api/sprints/:id/tasks` — taken ophalen
- [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
- [ ] **ST-406** `PATCH /api/stories/:id/tasks/reorder` — taakvolgorde aanpassen
- [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
- [ ] **ST-407** `POST /api/stories/:id/log` — activiteit vastleggen
- [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
- [ ] **ST-408** `PATCH /api/tasks/:id` — taakstatus en implementatieplan bijwerken
- [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
- [ ] **ST-409** `POST /api/todos` — todo aanmaken
- [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
- [ ] **ST-410** Story-activiteitenlog UI
- [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
@ -284,7 +345,7 @@ De MVP is klaar wanneer Lars — de primaire persona — de volledige cyclus kan
- "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
- [ ] **ST-509** Todo Data Table
- [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
@ -300,7 +361,7 @@ De MVP is klaar wanneer Lars — de primaire persona — de volledige cyclus kan
- **`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
- [ ] **ST-510** Todo detail-kaart
- [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
@ -341,7 +402,7 @@ De MVP is klaar wanneer Lars — de primaire persona — de volledige cyclus kan
- 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
- [ ] **ST-605** Keyboard-navigatie
- [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
@ -353,7 +414,7 @@ De MVP is klaar wanneer Lars — de primaire persona — de volledige cyclus kan
- 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
- [ ] **ST-608** Ratelimiting op auth-endpoints
- [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