docs: AI-optimized docs restructure (Phases 1–8) (#61)

* docs(dialog-pattern): add generic entity-dialog spec

Introduceert docs/patterns/dialog.md als bron-of-truth voor elke
create/edit/detail-dialog in Scrum4Me, ongeacht het achterliggende
dataobject. Bevat 14 secties: uitgangspunten, stack, component-
architectuur, layout, validatie, drielaagse demo-policy, submission,
dialog-gedrag, theming, footer, triggers/URL-state, per-entiteit
profile-template, out-of-scope, en een verificatie-checklist.

Registreert het patroon in CLAUDE.md "Implementatiepatronen"-tabel
zodat Claude (en mensen) de spec verplicht raadplegen voor elke
nieuwe dialog.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(dialog-pattern): convert task spec + add pbi/story entity-profiles

Reduceert docs/scrum4me-task-dialog.md van 507 naar ~140 regels: alle
gedeelde regels verhuisd naar docs/patterns/dialog.md, dit document
bevat nu alleen Task-specifieke velden, URL-pattern, status-veld,
server actions, triggers en bewuste out-of-scope-keuzes.

Voegt twee nieuwe entity-profielen toe voor bestaande dialogen:
- docs/scrum4me-pbi-dialog.md (PbiDialog: state-based, code+title-rij,
  PbiStatusSelect, geen delete in v1)
- docs/scrum4me-story-dialog.md (StoryDialog: state-based, header met
  status/priority badges, inline activity-log, demo-readonly-fallback,
  inline-delete-confirm i.p.v. AlertDialog)

Beide profielen documenteren expliciet de "Bekende gaps t.o.v.
generieke spec" zodat opvolgende PR's de afwijkingen kunnen
rechtzetten of bewust kunnen accorderen.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Added pdevelopment docs

* docs(plans): add docs-restructure plan for AI-optimized lookup

Audit of existing 39 doc files (~10.700 lines) and a phased restructure
proposal aimed at minimising the tokens an AI agent has to read to find
the right reference. Captures resolved decisions on language (English),
ADR template (Nygard default with MADR escape-hatch), index generator
(node script), and folder taxonomy. Proposal status — fase 1 to follow.

* docs(adr): add ADR scaffolding (templates, README, meta-ADR)

Set up docs/adr/ as the canonical home for architecture decisions:

- templates/nygard.md — default four-section format (Status, Context,
  Decision, Consequences) for one-way-door decisions.
- templates/madr.md — MADR v4 with YAML front-matter and explicit
  Considered Options for decisions where rejected alternatives matter.
- README.md — naming convention (NNNN-kebab-case), template-selection
  guidance (Nygard default; MADR for auth, queue mechanics, agent
  integration), status lifecycle, and ADR roster.
- 0000-record-architecture-decisions.md — meta-ADR establishing the
  practice itself, in Nygard format.

Backfilling existing implicit decisions (base-ui-over-radix, float
sort_order, demo-user three-layer policy, etc.) is fase 6 of the
docs-restructure plan.

* feat(docs): add docs index generator + initial INDEX.md

scripts/generate-docs-index.mjs walks docs/**/*.md, parses YAML
front-matter (or first H1 fallback) and a Nygard-style ## Status
section, then writes docs/INDEX.md with grouped tables for ADRs,
Specs, Plans (with archive subsection), Patterns, and Other.

Pure Node 20 (no external deps); idempotent — running it twice
produces byte-identical output. Excludes adr/templates/, the ADR
README, INDEX.md itself, and any *_*.md sidecar file.

Wire-up:
- package.json: docs:index → node scripts/generate-docs-index.mjs

Initial run indexed 35 docs across the existing structure; the
generated INDEX.md is committed so the table is reviewable in the
PR before hooking generation into a pre-commit step.

* chore: ignore Obsidian vault and personal sidecar files

Add .obsidian/ (Obsidian vault config) and _*.md (personal sidecar
notes) to .gitignore so the docs/ tree can serve as canonical source
of truth while still being usable as an Obsidian vault for personal
authoring. The docs index generator already excludes the same _*.md
pattern from INDEX.md.

* docs(plans): add PBI bulk-create spec for docs-restructure

Machine-parseable spec for an executor that calls the scrum4me MCP
(create_pbi → create_story → create_task) to seed the docs-restructure
work into the DB.

- Section 1 (Context) is the PBI description; serves as task-context
  via mcp__scrum4me__get_claude_context.
- Section 2 lists the 6 resolved decisions (English, MD3+styling
  merged, solo-paneel merged, .Plans archived, Nygard ADR default,
  node index script).
- Section 3 records what already shipped on this branch so the
  executor doesn't duplicate the ADR scaffolding or index generator.
- Section 4 carries the structured YAML graph: 1 PBI, 8 stories
  (one per phase), 39 tasks. product_id is REPLACE_ME — fill before
  running.
- YAML validated with PyYAML; field schema sanity-checked.

* docs(junk-cleanup): remove stub patterns/test.md

* docs(junk-cleanup): archive .Plans/ to docs/plans/archive/

* docs(front-matter): add YAML front-matter to docs/ root

* docs(front-matter): add YAML front-matter to patterns/

* docs(front-matter): add YAML front-matter to plans + agent files

* docs(index): regenerate INDEX.md after front-matter pass

* docs(naming): drop scrum4me- prefix from doc filenames

* docs(naming): lowercase API.md and MD3 filenames

* docs(naming): rename plan file to kebab-case ASCII

* docs(naming): rename middleware.md to proxy.md (next 16)

* docs(naming): polish CLAUDE.md doc-index after renames

* docs(taxonomy): scaffold topical folders under docs/

* docs(taxonomy): move spec files into docs/specs/

* docs(taxonomy): move design/api/qa/backlog/assets into folders

* docs(taxonomy): move agent-instruction-audit into decisions/

* docs(split): break architecture.md into 6 topical files

* docs(split): merge solo-paneel-spec into specs/functional.md

* docs(split): merge md3-color-scheme into design/styling

* docs(trim): extract branch/commit rules into runbook

* docs(trim): extract MCP integration into runbook

* docs(adr): add 0001-base-ui-over-radix

* docs(adr): add 0002-float-sort-order

* docs(adr): add 0003-one-branch-per-milestone

* docs(adr): add 0004-status-enum-mapping

* docs(adr): add 0005-iron-session-over-nextauth

* docs(adr): add 0006-demo-user-three-layer-policy

* docs(adr): add 0007-claude-question-channel-design

* docs(adr): add 0008-agent-instructions-in-claude-md + update README index

* docs(index): regenerate after ADR 0001-0008

* docs(glossary): add docs/glossary.md

* chore(docs): regenerate INDEX.md in pre-commit hook

* docs(readme): link INDEX + glossary + agent instructions

* feat(docs): add doc-link checker script

* chore(docs): wire docs:check-links and docs npm scripts

* ci(docs): block merge on broken doc links

* docs(links): fix broken cross-references after restructure

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Janpeter Visser 2026-05-03 03:21:59 +02:00 committed by GitHub
parent 289bcf9bf0
commit 7e45bbdbc0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
81 changed files with 12364 additions and 3154 deletions

View file

@ -71,7 +71,14 @@
"Bash(Sort-Object)", "Bash(Sort-Object)",
"PowerShell(Push-Location \"C:\\\\Users\\\\Madhu\\\\Projects\\\\scrum4me-mcp\"; npx tsc --noEmit; $result = $?; Pop-Location; Write-Output \"typecheck ok: $result\")", "PowerShell(Push-Location \"C:\\\\Users\\\\Madhu\\\\Projects\\\\scrum4me-mcp\"; npx tsc --noEmit; $result = $?; Pop-Location; Write-Output \"typecheck ok: $result\")",
"PowerShell(git *)", "PowerShell(git *)",
"mcp__scrum4me__verify_task_against_plan" "mcp__scrum4me__verify_task_against_plan",
"Bash(mkdir -p docs/plans/archive)",
"Bash(rmdir .Plans)",
"Bash(mv .Plans/2026-04-27-claude-md-workflow-update.md docs/plans/archive/)",
"Bash(mv .Plans/2026-04-27-insert-milestone-tool.md docs/plans/archive/)",
"Bash(mv .Plans/2026-04-27-m8-realtime-solo.md docs/plans/archive/)",
"Bash(xargs sed *)",
"Bash(python3 *)"
] ]
}, },
"enableAllProjectMcpServers": true, "enableAllProjectMcpServers": true,

View file

@ -39,6 +39,9 @@ jobs:
- name: Test - name: Test
run: npm test run: npm test
- name: Check doc links
run: npm run docs:check-links
- name: Build - name: Build
run: npm run build run: npm run build
env: env:

7
.gitignore vendored
View file

@ -52,7 +52,6 @@ next-env.d.ts
.claude/settings.local.json .claude/settings.local.json
# Local plan/scratch files (per-developer, not shared) # Local plan/scratch files (per-developer, not shared)
.Plans/
# Editor # Editor
.vscode/ .vscode/
@ -72,4 +71,8 @@ jp.sh
# Lokale scratch-bestanden # Lokale scratch-bestanden
Brainstro Brainstro
/graphify-out /graphify-out
# Personal Obsidian authoring layer (vault config + sidecar files prefixed `_`)
.obsidian/
_*.md

View file

@ -1 +1,6 @@
npx lint-staged npx lint-staged
if git diff --cached --name-only | grep -q '^docs/.*\.md$'; then
npm run docs:index
git add docs/INDEX.md
fi

View file

@ -1,38 +1,13 @@
<!-- BEGIN:nextjs-agent-rules --> ---
# This is NOT the Next.js you know title: "AGENTS.md — Scrum4Me agent rules"
status: active
audience: [ai-agent]
language: en
last_updated: 2026-05-03
---
This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` before writing any code. Heed deprecation notices. # Agent Instructions — Scrum4Me
<!-- END:nextjs-agent-rules -->
# Scrum4Me Codex Rules This file is a redirect stub. All agent instructions live in **[CLAUDE.md](./CLAUDE.md)**.
Read `CLAUDE.md` and the relevant files in `docs/` before changing behavior. The same product and security rules apply to Codex work. For Claude Code specifically, CLAUDE.md is loaded automatically. Start there.
## Access Control
- Product-scoped access is owner-or-member: use `productAccessFilter(userId)` from `lib/product-access.ts`.
- Use owner-only `user_id` checks only for actions that truly require ownership, such as product archiving and team management.
- Never trust client-provided IDs by themselves. For reorder, promotion, completion, or bulk updates, fetch the records with both `id in (...)` and the parent scope (`product_id`, `pbi_id`, `sprint_id`, or `story_id`) before writing.
- Reject duplicate IDs in ordered lists or decision payloads.
- Derive denormalized fields from database parents, for example `pbi.product_id`, not from form data or JSON bodies.
- Demo users and demo API tokens must receive 403 on write operations.
## Documentation Sync
When changing behavior, API responses, dependencies, environment variables, deployment behavior, or analytics, update the matching docs in the same change:
- `README.md` for setup, dependencies, deployment, and API overview.
- `docs/functional.md` for user-facing/API requirements.
- `docs/architecture.md` for stack, access model, data model, env vars, and deployment.
- `docs/patterns/` when a reusable implementation rule changes.
- `CLAUDE.md` and this file when an agent instruction would have prevented the issue.
## Verification
Before handing work back, run:
```bash
npm run lint
npm test
npm run build
```

396
CLAUDE.md
View file

@ -1,372 +1,114 @@
---
title: "CLAUDE.md — Scrum4Me"
status: active
audience: [ai-agent]
language: nl
last_updated: 2026-05-03
---
# CLAUDE.md — Scrum4Me # CLAUDE.md — Scrum4Me
Dit is het centrale instructiedocument voor Claude Code. Lees dit volledig voordat je iets bouwt. Desktop-first Scrum-app voor solo developers en kleine teams. Hiërarchie: product → PBI → story → taak. Zie [README.md](./README.md) voor setup.
--- ---
## Wat is Scrum4Me? ## Orientatie
Een desktop-first fullstack webapplicatie voor solo developers en kleine Scrum Teams die meerdere softwareprojecten parallel beheren. De app organiseert werk hiërarchisch (product → PBI → story → taak), biedt gesplitste planningsschermen met drag-and-drop, en integreert met Claude Code via een REST API. | Bestand | Waarvoor |
---
## Specificatiedocumenten
Lees het relevante document voordat je aan een feature begint. Nooit gokken over requirements.
| Document | Gebruik voor |
|---|---| |---|---|
| `docs/functional.md` | Acceptatiecriteria, randgevallen, user flows | | `docs/INDEX.md` | Gegenereerde index van alle docs — begin hier |
| `docs/architecture.md` | Stack, datamodel, Prisma schema, Zustand stores | | `docs/specs/functional.md` | Acceptatiecriteria, user flows |
| `docs/backlog.md` | Welke task bouwen, volgorde, "done when"-criteria | | `docs/architecture.md` | Breadcrumb → 6 topische arch-bestanden |
| `docs/personas.md` | Lars (primair), Dina, Remi — gebruik bij UI-beslissingen | | `docs/backlog/index.md` | Implementatievolgorde, "done when"-criteria |
| `docs/product-backlog.md` | Historische domein-backlog (referentie); seed wordt sinds ST-004 gegenereerd uit `backlog.md` via `prisma/seed-data/parse-backlog.ts` | | `docs/api/rest-contract.md` | REST API contract voor Claude Code |
| `docs/api.md` | REST-API contract voor Claude Code — endpoints, status-enums, foutcodes, voorbeeld-curls | | `docs/design/styling.md` | **Lees vóór elk component** — MD3-tokens, shadcn |
| `docs/styling.md` | **Lees dit voor elk component** — MD3-kleuren, shadcn patronen | | `docs/plans/<key>-*.md` | Implementatieplan per milestone |
| `docs/agent-instruction-audit.md` | Waarom de agent-instructies zijn aangescherpt; checklist voor toekomstige wijzigingen |
| `docs/plans/<milestone-key>-*.md` | Implementatieplan per milestone — Bestanden, Stappen, Aandachtspunten, Verificatie. Lees vóór je aan een ST begint. Milestone-key matcht backlog-header (`M9`, `M3.5`, `PBI-9`, …). |
| [`madhura68/scrum4me-mcp`](https://github.com/madhura68/scrum4me-mcp) | MCP-server repo: native tools voor Claude Code, schema-sync via git submodule |
--- ---
## Waar te beginnen ## Hoe werk vinden
Volg de backlog strikt op volgorde. Start bij **ST-001**. Sla geen milestone over. **Track A — MCP (aanbevolen):**
1. `mcp__scrum4me__get_claude_context` → pak de next story
``` 2. Voer taken uit in `sort_order`; update status per taak
M0 (ST-001008) → M1 (ST-101110) → M2 (ST-201210) 3. Lees het relevante patroon en styling vóór je begint
→ M3 (ST-301312) → M4 (ST-401410) → M5 (ST-501506)
→ M6 (ST-601612)
```
Werken aan een task kan via twee tracks. Track A heeft de voorkeur als je in Claude Code zit; Track B is voor Codex of omgevingen zonder MCP.
### Track A — via Claude Code MCP (aanbevolen)
1. Roep `mcp__scrum4me__implement_next_story` aan met `product_id` (gebruik `mcp__scrum4me__list_products` als je het id niet weet)
2. De prompt orkestreert: `get_claude_context``log_implementation` → per task `update_task_status(in_progress)` → bouw → `update_task_status(done)``log_test_result``log_commit`
3. Bouw de tasks in volgorde van `sort_order`; lees per task de relevante pattern-doc en styling
4. Verifieer: `npm run lint && npm test && npm run build` 4. Verifieer: `npm run lint && npm test && npm run build`
5. Commit per laag (zie Commit Strategy) 5. Commit per laag — zie [docs/runbooks/branch-and-commit.md](./docs/runbooks/branch-and-commit.md)
### Track B — manueel (Codex of zonder MCP) **Track B — manueel:**
1. Lees taak in `docs/backlog/index.md`
2. Zoek spec in `docs/specs/functional.md`
3. Lees patroon + styling → bouw → verifieer → vraag bevestiging → commit
1. Lees de task in `backlog.md` Volledige MCP-tool documentatie: [docs/runbooks/mcp-integration.md](./docs/runbooks/mcp-integration.md)
2. Zoek de bijbehorende feature-spec in `functional.md`
3. Lees het relevante patroon in `docs/patterns/` en styling in `docs/styling.md` als dat van toepassing is
4. Bouw — test — verifieer de "Done when"-criteria
5. Vraag of de code correct is
6. Commit (zie Commit Strategy hieronder)
7. Vraag of de volgende taak gedaan moet worden
--- ---
## Tech stack ## Hardstop regels
``` - **Styling:** nooit `bg-blue-500`; altijd MD3-tokens (`bg-primary`, `bg-status-done`, …)
Next.js 16 (App Router) + React 19 - **UI:** gebruik `@base-ui/react` met `render`-prop, niet Radix `asChild`
TypeScript strict - **Push:** nooit pushen zonder expliciete gebruikersbevestiging — zie [branch-and-commit.md](./docs/runbooks/branch-and-commit.md)
Tailwind CSS + shadcn/ui - **Demo:** drie lagen — proxy.ts + server action + UI disabled knop
MD3 kleurensysteem via app/styles/theme.css - **Enum:** DB UPPER_SNAKE ↔ API lowercase — uitsluitend via `lib/task-status.ts`
Zustand (client state) - **Foutcodes:** 400 = parse-fout, 422 = Zod-validatie, 403 = demo-token
dnd-kit (drag-and-drop) - **Server/client grens:** `*-server.ts` bevat DB/node-only; nooit importeren in client component
Prisma v7 + PostgreSQL (Neon) - **Deployment:** `npm run lint && npm test && npm run build` vóór elke PR
iron-session (auth cookies)
bcryptjs + Zod + Sonner
Sharp (avatarverwerking)
Vercel Analytics (@vercel/analytics/next)
```
> ⚠️ **Stylingregel:** Gebruik **nooit** `bg-blue-500` of willekeurige Tailwind-kleuren.
> Gebruik altijd semantische MD3-tokens: `bg-primary`, `bg-status-done`, `bg-priority-critical`.
> Zie `styling.md` voor alle patronen.
> ⚠️ **Next.js-versie:** Lees `node_modules/next/dist/docs/` bij twijfel — API's kunnen afwijken van trainingsdata.
--- ---
## UI Library Conventions ## Stack
- Dit project gebruikt **`@base-ui/react`**, *niet* Radix UI — ondanks dat shadcn-componenten visueel-identiek zijn | Laag | Technologie |
- Composition gebeurt via de **`render`-prop**, niet via Radix's `asChild`: |---|---|
- ✅ `<TooltipTrigger render={<button />}>...</TooltipTrigger>` | Framework | Next.js 16 (App Router) + React 19 |
- ❌ `<TooltipTrigger asChild><button>...</button></TooltipTrigger>` — geeft TS-errors | Taal | TypeScript strict |
- Vóór je een nieuwe shadcn-/UI-primitive gebruikt: grep eerst de codebase voor bestaand gebruik en volg dat patroon (`grep -rn "PrimitiveTrigger" components/`) | Styling | Tailwind CSS + shadcn/ui + MD3 via `app/styles/theme.css` |
- shadcn-componenten in `components/ui/` zijn dunne wrappers rond `@base-ui/react`-primitives; lees die voor de exacte prop-API | State | Zustand + dnd-kit |
| DB | Prisma v7 + PostgreSQL (Neon) |
| Auth | iron-session + bcryptjs |
| Utilities | Zod, Sonner, Sharp, Vercel Analytics |
--- ---
## Implementatiepatronen ## Patterns quickref
Lees het relevante patroon vóór je begint. Nooit uit het hoofd schrijven.
| Patroon | Bestand | | Patroon | Bestand |
|---|---| |---|---|
| iron-session (auth cookies) | `docs/patterns/iron-session.md` | | iron-session | `docs/patterns/iron-session.md` |
| Prisma Client singleton | `docs/patterns/prisma-client.md` | | Prisma singleton | `docs/patterns/prisma-client.md` |
| Server Action (met auth + Zod) | `docs/patterns/server-action.md` | | Server Action (auth + Zod) | `docs/patterns/server-action.md` |
| Route Handler (REST API) | `docs/patterns/route-handler.md` | | Route Handler (REST) | `docs/patterns/route-handler.md` |
| Zustand optimistische update + rollback | `docs/patterns/zustand-optimistic.md` | | Zustand optimistic update | `docs/patterns/zustand-optimistic.md` |
| Float sort_order drag-and-drop | `docs/patterns/sort-order.md` | | Float sort_order / drag-and-drop | `docs/patterns/sort-order.md` |
| Proxy middleware (route protection) | `docs/patterns/proxy.md` | | Proxy / route protection | `docs/patterns/proxy.md` |
| QR-pairing (unauth-SSE + pre-auth cookie) | `docs/patterns/qr-login.md` | | QR-pairing | `docs/patterns/qr-login.md` |
| Bidirectionele async-comms MCP-agent ↔ user | `docs/patterns/claude-question-channel.md` | | Claude ↔ user vraagkanaal | `docs/patterns/claude-question-channel.md` |
| **Entity Dialog (verplicht voor élke create/edit/detail-dialog)** | `docs/patterns/dialog.md` — bron-of-truth; per entiteit één profile-doc (bv. `docs/task-dialog.md`) | | Entity Dialog (verplicht) | `docs/patterns/dialog.md` |
| **Story met UI-component (verplicht 3-task-patroon: Helper / Component / Integration)** | `docs/patterns/story-with-ui-component.md` — elke story met een `*-component.tsx` vereist een afsluitende Integration-task die de component in `page.tsx` wirt |
| Status-enum mapping (DB ↔ API) | `lib/task-status.ts` |
| Client/server module-boundary | `*-server.ts` bevat DB-calls of node-only deps; `*.ts` is pure (client-safe). Nooit `import { ... } from '@/lib/foo-server'` in een client-component, anders krijg je `Module not found: 'dns'`/`'pg'`-style runtime fouten |
---
## Integration-task verificatie (smoke-test)
Voor stories met `*-component.tsx`: de Integration-task moet vóór
`update_job_status(done)` een smoke-test draaien op de daadwerkelijke
HTML-render:
```bash
# In de worktree — pas ROUTE en SECTIONS aan per story
ROUTE="/insights"
SECTIONS=("Sprint Health" "Plan-quality" "Agent throughput" "Velocity" "Backlog health")
npm run dev > /tmp/dev.log 2>&1 &
DEV_PID=$!
sleep 8 # wacht tot Next.js compiled
curl -s http://localhost:3000${ROUTE} > /tmp/page.html
SMOKE_FAIL=
for section in "${SECTIONS[@]}"; do
grep -q "$section" /tmp/page.html || { echo "MISSING: $section"; SMOKE_FAIL=1; }
done
kill $DEV_PID
[ -z "$SMOKE_FAIL" ] # exit-code 1 als iets miste
```
Als de smoke-test faalt: pas `page.tsx` aan zodat alle secties renderen, herhaal.
Markeer Integration-task DONE pas wanneer alle verwachte sections in de HTML zitten.
--- ---
## Env vars ## Env vars
```bash ```bash
DATABASE_URL="" # postgresql://... (verplicht) DATABASE_URL="" # postgresql://...
DIRECT_URL="" # postgresql://... — pooler-bypass voor LISTEN/NOTIFY (Neon/cloud) DIRECT_URL="" # pooler-bypass voor LISTEN/NOTIFY
SESSION_SECRET="" # min 32 chars; openssl rand -base64 32 SESSION_SECRET="" # min 32 chars
CRON_SECRET="" # M11 — Bearer-secret voor /api/cron/*; verplicht in productie, optioneel lokaal (genereer met openssl rand -base64 32) CRON_SECRET="" # Bearer-secret /api/cron/*
``` ```
Volledige Zod-schema in `lib/env.ts`. `.env.example` is de canonieke lijst voor nieuwe checkouts. Volledig schema: `lib/env.ts`. Canonieke lijst: `.env.example`.
--- ---
## Conventies
- **Branches:** `feat/ST-001-scaffolding`
- **Server Actions:** altijd in `actions/[domein].ts`, nooit inline in page.tsx
- **Validatie:** altijd Zod, nooit handmatige checks
- **Toegangsmodel:** product-scoped resources gebruiken `productAccessFilter(userId)` tenzij het expliciet een eigenaarsactie is
- **Bulk-ID's:** reorder- en beslissingsacties valideren dat alle meegegeven IDs binnen dezelfde parent-scope vallen voordat er geschreven wordt
- **Foreign keys:** denormalized keys zoals `story.product_id` worden afgeleid uit de database-parent (`pbi.product_id`), nooit uit client-input
- **Demo-check (drie lagen — ST-1110):** write-acties zijn drielaags afgedekt: (1) middleware-guard in `proxy.ts` blokkeert non-GET op `/api/*` voor demo; (2) elke Server Action / Route Handler controleert `session.isDemo` vóór schrijven; (3) write-knoppen in UI zijn `disabled` met `<DemoTooltip show={isDemo}>`. Zie `docs/architecture.md#demo-user-policy` en `docs/plans/ST-1110-demo-readonly.md`
- **Foutberichten:** Nederlands voor eindgebruikers — comments in code: Engels
- **Dependencies:** elke geïmporteerde runtime package staat direct in `dependencies`, niet alleen transitief in `package-lock.json`
- **Docs-sync:** elke gedrags-, dependency-, API- of deploymentwijziging werkt README, relevante docs en patterns bij in dezelfde change
- **Entity codes:** gebruik product/PBI/story-codes in commit-titles wanneer aanwezig (`feat(ST-356.2): ...`); branchnaam blijft `feat/ST-XXX-slug`
- **Status-enums op API:** lowercase (`todo|in_progress|review|done`, `open|in_sprint|done`); DB houdt UPPER_SNAKE; conversie uitsluitend via `lib/task-status.ts`-mappers — nooit ad-hoc `.toLowerCase()` elders
- **Foutcodes API:** `400` alleen voor malformed JSON-body (parse-fout via `request.json()`); `422` voor zod-validatie en well-formed-maar-niet-acceptabel; `403` voor demo-tokens. Documenteer per endpoint in `docs/api.md`
- **Tests volgen contract:** bij een API-contract-wijziging (status, foutcode, response-shape) MOET in dezelfde commit ook `__tests__/api/` mee — een test die rood gaat omdat de oude waarde wordt verwacht is een onvolledige wijziging, niet een "kapotte test"
- **Dev port:** `npm run dev` draait altijd op **3000**. Een `predev`-hook killt vooraf elk proces op 3000 (stale Next.js dev-server, vorige sessie) zodat sessies, cookies en MCP-config consistent op één poort werken. Wijk hier niet van af — geen `-p 3001` o.i.d. tenzij je expliciet twee dev-servers naast elkaar wil draaien
---
## Branch & PR Strategy (STRICT — kostenbeheersing)
> **Core rule: één branch per milestone, PR alleen na gebruikerstest**
Elke `git push` naar een feature-branch triggert een Vercel preview-deployment. Op het huidige Hobby-account zijn die schaars en kosten geld; we minimaliseren preview-builds tot er werkelijk iets te reviewen valt.
### Wel doen
- Eén branch voor de hele milestone — `feat/M{N}-{slug}` (bv. `feat/M10-qr-login`); voor losse stories zonder milestone blijft `feat/ST-XXX-{slug}` geldig
- Commits accumuleren lokaal volgens de Commit Strategy hieronder — één commit per stap, ST-code in de titel
- Pushen + PR openen **pas nadat de gebruiker de milestone handmatig heeft getest en goedgekeurd** — vraag expliciet om bevestiging vóór `git push`
- Tussentijdse "klaar voor jouw test"-momenten markeren met een lokale tag of een berichtje in chat, niet met een push
### Niet doen
- Pushen na elke story of commit
- Een PR per story openen tijdens de implementatie
- "Just-in-case" pushen om backup te hebben — gebruik `git stash`, een lokale tag, of meerdere lokale branches
- `--force-push` om eerdere preview-builds "weg te toveren" (kost dezelfde build opnieuw bij hercreatie)
- **Direct pushen naar `main`** — die branch heeft protection rules; gebruik altijd een PR
### Wanneer wel commit-zonder-vragen, wanneer niet
- **Tijdens een directed sprint-flow** (Track A: `mcp__scrum4me__implement_next_story` of een expliciete *"implementeer M{N}"*-opdracht): commit-per-laag conform de Commit Strategy hieronder is impliciet geautoriseerd — niet per commit vragen
- **Bij ad-hoc / out-of-band werk** (bug-fix tussendoor, refactor, kleine wijziging op verzoek): toon de diff + voorgestelde commit-message en wacht op `"commit it"` voordat je `git commit` draait
- **`git push` is altijd expliciet** — de scope van de policy gaat over preview-builds, dus push gebeurt alleen na gebruiker-test, ongeacht commit-context
### Uitzonderingen op de push-regel
- Een **planning-PR** zonder code-wijzigingen (alleen docs in `docs/plans/` of `docs/`) mag direct gepusht worden — die triggert geen functional regressie en is goedkoop te bouwen
- Een **bugfix-hotfix** op `main` met aantoonbare productie-impact mag direct gepusht worden (via een PR — zie boven)
### Wanneer aanpassen
Zodra het Vercel-account naar Pro (of andere omgeving zonder per-build-kosten) gaat: vervang deze regel door "branch + PR per story" zoals oorspronkelijk in dit document stond. Werk deze sectie bij én documenteer de wijziging in `docs/agent-instruction-audit.md`.
---
## Plan Mode
- Voor simpele, goed-afgebakende file-edits: **niet** in plan mode gaan — gewoon de wijziging maken
- Reserveer plan mode voor multi-step refactors, ambigue verzoeken, of milestone-planning waarbij design-keuzes vooraf bevestigd moeten worden
- Plannen die uit plan mode komen: opslaan als `docs/plans/M{N}-{slug}.md` (zie memory `feedback_plan_location`), niet als ephemeral systeem-bestand
---
## Commit Strategy (STRICT)
> **Core rule: één commit = één verantwoordelijkheid**
### Nooit doen
- Database + API + UI in één commit mengen
- Feature + documentatie combineren
- Grote "alles gewijzigd" commits
- Vage berichten zoals "update stuff"
### Verplichte structuur
Splits werk op in logische lagen:
1. Database / Prisma
2. API / server actions
3. UI / components
4. Config / infra
5. Documentatie
### Commit-formaat
```
feat(ST-XXX): korte beschrijving
fix(ST-XXX): korte beschrijving
chore(ST-XXX): korte beschrijving
docs(ST-XXX): korte beschrijving
```
### Voorbeeld (verplicht patroon)
In plaats van:
```bash
feat: add profile system
```
Splits altijd op in:
```bash
feat(ST-XXX): add user profile fields to Prisma schema
feat(ST-XXX): add avatar upload endpoint
feat(ST-XXX): add profile editor component
chore(ST-XXX): configure sharp for avatar processing
docs(ST-XXX): document profile feature
```
---
## Scrum-terminologie ## Scrum-terminologie
| Correct | Niet gebruiken | PBI (niet: Feature/Epic) · Story (niet: Ticket) · Sprint Goal (niet: Objective)
|---|---|
| Product Backlog Item (PBI) | Feature, Epic, Issue |
| Story | User Story, Ticket |
| Sprint Goal | Sprint Objective |
| Scrum Team | Team |
--- ---
## MCP-integratie ## Verificatie
Scrum4Me heeft een eigen MCP-server in repo [`madhura68/scrum4me-mcp`](https://github.com/madhura68/scrum4me-mcp) die de REST-API als native tools voor Claude Code aanbiedt. Schema's worden gedeeld via een git submodule (`vendor/scrum4me`), niet gedupliceerd. ```bash
npm run lint && npm test && npm run build
### Tools beschikbaar in Claude Code (18) ```
**Read / context:**
- `mcp__scrum4me__health` — service + DB ping
- `mcp__scrum4me__list_products` — producten waar de tokengebruiker toegang tot heeft
- `mcp__scrum4me__get_claude_context` — bundled product / actieve sprint / next story (met tasks) / open todos
**Authoring (PBI/Story/Task aanmaken):**
- `mcp__scrum4me__create_pbi``{ product_id, title, description?, priority, sort_order? }`; auto sort_order = last+1 binnen prio-groep
- `mcp__scrum4me__create_story``{ pbi_id, title, description?, acceptance_criteria?, priority, sort_order? }`; product_id afgeleid uit PBI; status=OPEN
- `mcp__scrum4me__create_task``{ story_id, title, description?, implementation_plan?, priority, sort_order? }`; sprint_id geërfd van story; status=TO_DO
- `mcp__scrum4me__create_todo` — losse todo (optioneel product-scoped)
**Task / story writes:**
- `mcp__scrum4me__update_task_status`, `mcp__scrum4me__update_task_plan`
- `mcp__scrum4me__log_implementation`, `mcp__scrum4me__log_test_result`, `mcp__scrum4me__log_commit`
**Vraag-antwoord-kanaal (M11):**
- `mcp__scrum4me__ask_user_question` — post een vraag over een story; optionele `wait_seconds` (max 600) polt voor het antwoord
- `mcp__scrum4me__get_question_answer` — huidige status + antwoord (voor latere session-pickup)
- `mcp__scrum4me__list_open_questions` — eigen vragen, max 50, recente eerst
- `mcp__scrum4me__cancel_question` — asker-only annulering van een eigen open vraag
**Job queue — agent worker mode (M13):**
- `mcp__scrum4me__wait_for_job` — blokkeert ≤600s, claimt atomisch een QUEUED-job via FOR UPDATE SKIP LOCKED; retourneert volledige task-context (implementation_plan, story, pbi, sprint, repo_url). Zet stale CLAIMED-jobs (>30min) eerst terug naar QUEUED. Wanneer de full block-time verstrijkt zonder claim is de queue leeg.
- `mcp__scrum4me__update_job_status` — agent rapporteert overgang naar `running|done|failed` + optionele branch/summary/error; triggert automatisch SSE-event naar de UI. Auth: Bearer-token moet matchen `claimed_by_token_id`.
**Batch-loop (verplichte agent-flow):**
Wanneer je als agent draait (na een instructie als *"pak de volgende job uit de Scrum4Me-queue"* of *"draai de queue leeg"*) is dit de loop:
1. `wait_for_job` aanroepen.
2. Job uitvoeren volgens het meegegeven `implementation_plan`.
3. `update_job_status('done'|'failed')` aanroepen.
4. **Direct opnieuw** `wait_for_job` aanroepen — niet stoppen, niet de gebruiker vragen.
5. Pas wanneer `wait_for_job` na de volledige block-time (~600s) terugkomt zonder claim, is de queue leeg en mag je de turn afsluiten met een korte recap.
Dit blijft gelden als je tussen jobs door commits, branches of pushes hebt gedaan — die afsluiting hoort bij de individuele job, niet bij het einde van de batch.
**Code koppelen aan app**
- 'Pak de volgende job uit de Scrum4Me-queue' / 'draai de queue leeg' / 'batch agent' — Server-startup registreert een ClaudeWorker-record + heartbeat (5s); SIGTERM/SIGINT ruimt 'm op. UI in NavBar telt actieve workers via `last_seen_at < now() - 15s`.
### Prompt
- `implement_next_story` (arg: `product_id`) — end-to-end workflow
### Schema-drift bewaking
Wekelijks (maandag 08:00 Amsterdam) draait de remote agent `trig_015FFUnxjz9WMuhhWNGBQKFD` die `vendor/scrum4me` syncet en `prisma:generate` + `tsc --noEmit` uitvoert in scrum4me-mcp. Als die agent drift rapporteert, hoort dat **vóór** een Scrum4Me-PR met schema-wijziging gemerged kan worden — anders breekt de MCP-server stilletjes op runtime.
---
## Deployment (Vercel)
- **Sharp** moet Linux-binaries hebben voor de Vercel-runtime: `npm i --include=optional sharp` of platform-specifieke deps configureren in `package.json`
- **Externe image hostnames** in `next.config.js` `images.remotePatterns` configureren *vóór* `next/image` op die hosts wijst — anders 500 in productie
- **Vercel cron**: Hobby-plan staat alleen daily crons toe (max 1×/dag); Pro ondersteunt fijnmaziger. Bij wijziging van `vercel.json` `crons` ook `docs/api.md` + relevante pattern-docs updaten
- **`CRON_SECRET`** moet als env-var op de Vercel-project-omgeving staan vóór de eerste cron-run, anders 401 op `/api/cron/*`-endpoints
- **Preflight** vóór deploy: `npm run lint && npm test && npm run build` — falende build laat een PR niet door (CI blokkeert merge per ST-610)
---
## Definition of Done (MVP)
M7 (MCP-server) is post-MVP en heeft eigen acceptatie in `docs/backlog.md`.
- [ ] Alle 62 tasks (ST-001 t/m ST-612) afgerond
- [ ] Volledige Lars-flow zonder fouten (ST-612)
- [ ] Alle gedocumenteerde API-endpoints werken via curl (zie `docs/api.md`)
- [ ] Demo-gebruiker heeft geen schrijfrechten
- [ ] App opzetbaar via README zonder extra hulp
- [ ] CI/CD actief — falende build blokkeert merge
- [ ] Beveiligingsreview API geslaagd (cross-user toegang onmogelijk)
- [ ] Documentatie is bijgewerkt voor gewijzigde API's, dependencies, deployment en agent-instructies

View file

@ -47,6 +47,12 @@ Scrum4Me biedt een lichtgewicht, web-based oplossing voor het beheren van sprint
- Vercel hosting - Vercel hosting
- GitHub Actions / CI-CD - GitHub Actions / CI-CD
## Documentation
- [docs/INDEX.md](docs/INDEX.md) — generated index of all docs (front-matter driven)
- [docs/glossary.md](docs/glossary.md) — domain terms (PBI, Story, MCP-job, etc.)
- [CLAUDE.md](CLAUDE.md) / [AGENTS.md](AGENTS.md) — agent instructions
## Architectuur (kort) ## Architectuur (kort)
- Frontend en backend via Next.js App Router - Frontend en backend via Next.js App Router
@ -122,7 +128,7 @@ npx prisma db push
npm run db:erd npm run db:erd
``` ```
Deze command voert lokaal `prisma generate` uit. Daardoor worden zowel de Prisma Client als `docs/erd.svg` opnieuw opgebouwd. Deze command voert lokaal `prisma generate` uit. Daardoor worden zowel de Prisma Client als `docs/assets/erd.svg` opnieuw opgebouwd.
In CI en deployment wordt bewust alleen de Prisma Client gegenereerd met `prisma generate --generator client`. Het ERD-diagram gebruikt Mermaid/Puppeteer en wordt daarom niet in GitHub Actions of Vercel gegenereerd. In CI en deployment wordt bewust alleen de Prisma Client gegenereerd met `prisma generate --generator client`. Het ERD-diagram gebruikt Mermaid/Puppeteer en wordt daarom niet in GitHub Actions of Vercel gegenereerd.
@ -155,11 +161,11 @@ Verwacht: alle 69 tests slagen, 0 failures.
bash scripts/test-api.sh bash scripts/test-api.sh
``` ```
De curl-tests dekken alle 7 API-endpoints: auth (401), demo-blokkering (403), inputvalidatie (400) en happy paths. Zie `docs/test-plan.md` voor het volledige testplan. De curl-tests dekken alle 7 API-endpoints: auth (401), demo-blokkering (403), inputvalidatie (400) en happy paths. Zie `docs/qa/api-test-plan.md` voor het volledige testplan.
## Database ## Database
![ERD](./docs/erd.svg) ![ERD](./docs/assets/erd.svg)
De databasevisualisatie wordt lokaal gegenereerd uit `prisma/schema.prisma` via `prisma-erd-generator`. De databasevisualisatie wordt lokaal gegenereerd uit `prisma/schema.prisma` via `prisma-erd-generator`.
@ -169,7 +175,7 @@ Handmatige generatie:
npm run db:erd npm run db:erd
``` ```
Tijdens lokale development draait `npm run dev` naast Next.js ook `npm run db:erd:watch`. Bij wijzigingen in `prisma/schema.prisma` wordt `docs/erd.svg` automatisch opnieuw gegenereerd. Tijdens lokale development draait `npm run dev` naast Next.js ook `npm run db:erd:watch`. Bij wijzigingen in `prisma/schema.prisma` wordt `docs/assets/erd.svg` automatisch opnieuw gegenereerd.
Gebruik `npx prisma db push` alleen om het schema naar de database te synchroniseren. Gebruik `npm run db:erd` om lokaal Prisma Client en de ERD te genereren. Gebruik in CI uitsluitend `npx prisma generate --generator client`. Gebruik `npx prisma db push` alleen om het schema naar de database te synchroniseren. Gebruik `npm run db:erd` om lokaal Prisma Client en de ERD te genereren. Gebruik in CI uitsluitend `npx prisma generate --generator client`.
@ -182,7 +188,7 @@ npm run dev # lokale development server
npm run lint # ESLint npm run lint # ESLint
npm test # Vitest test suite npm test # Vitest test suite
npm run build # productiebuild zoals Vercel die verwacht npm run build # productiebuild zoals Vercel die verwacht
npm run db:erd # Prisma Client + docs/erd.svg genereren npm run db:erd # Prisma Client + docs/assets/erd.svg genereren
``` ```
### Environment variables ### Environment variables
@ -279,7 +285,7 @@ De productieomgeving is gericht op Vercel + Neon.
### Documentatie ### Documentatie
- [Functionele specificatie](docs/functional.md) - [Functionele specificatie](docs/specs/functional.md)
- [Technische architectuur](docs/architecture.md) - [Technische architectuur](docs/architecture.md)
- [Backlog](docs/backlog.md) - [Backlog](docs/backlog/index.md)
- [Agent-instructie audit](docs/agent-instruction-audit.md) - [Agent-instructie audit](docs/decisions/agent-instructions-history.md)

107
docs/INDEX.md Normal file
View file

@ -0,0 +1,107 @@
<!-- Generated by scripts/generate-docs-index.mjs. Do not edit by hand. Run `npm run docs:index`. -->
# Documentation Index
Auto-generated on 2026-05-03 from front-matter and headings.
## Architecture Decision Records
| # | Title | Status |
|---|---|---|
| 0000 | [ADR-0000: Record architecture decisions](./adr/0000-record-architecture-decisions.md) | accepted |
| 0001 | [ADR-0001: Use @base-ui/react instead of Radix UI](./adr/0001-base-ui-over-radix.md) | accepted |
| 0002 | [ADR-0002: Use float sort_order for drag-and-drop ordering](./adr/0002-float-sort-order.md) | accepted |
| 0003 | [ADR-0003: One branch per milestone, push only after user test](./adr/0003-one-branch-per-milestone.md) | accepted |
| 0004 | [ADR-0004: DB enums UPPER_SNAKE, API enums lowercase, mapped exclusively via lib/task-status.ts](./adr/0004-status-enum-mapping.md) | accepted |
| 0005 | [ADR-0005: Use iron-session for authentication instead of NextAuth/Clerk/Supabase Auth](./adr/0005-iron-session-over-nextauth.md) | accepted |
| 0006 | [ADR-0006: Demo-user write protection enforced in three layers](./adr/0006-demo-user-three-layer-policy.md) | accepted |
| 0007 | [ADR-0007: Agent ↔ user question channel via persistent table + LISTEN/NOTIFY](./adr/0007-claude-question-channel-design.md) | accepted |
| 0008 | [ADR-0008: Agent instructions in CLAUDE.md + topical runbooks](./adr/0008-agent-instructions-in-claude-md.md) | accepted |
| 0009 | [ADR-0009: Three-phase agent pipeline for feature ideation → plan → implementation](./adr/0009-three-phase-feature-pipeline.md) | proposed |
## Specifications
| Title | Status | Updated |
|---|---|---|
| [PbiDialog Profiel](./specs/dialogs/pbi.md) | active | 2026-05-03 |
| [StoryDialog Profiel](./specs/dialogs/story.md) | active | 2026-05-03 |
| [TaskDialog Profiel](./specs/dialogs/task.md) | active | 2026-05-03 |
| [Scrum4Me — Functionele Specificatie](./specs/functional.md) | active | 2026-05-03 |
| [DevPlanner — User Personas](./specs/personas.md) | active | 2026-05-03 |
## Plans
| Title | Status | Updated |
|---|---|---|
| [Docs-restructuur — geoptimaliseerd voor AI-lookup](./plans/docs-restructure-ai-lookup.md) | proposal | 2026-05-02 |
| [PBI Bulk-Create Spec — Docs-Restructure for AI-Optimized Lookup](./plans/docs-restructure-pbi-spec.md) | — | — |
| [M10 — Password-loze inlog via QR-pairing](./plans/M10-qr-pairing-login.md) | active | 2026-05-03 |
| [M11 — Claude vraagt, gebruiker antwoordt](./plans/M11-claude-questions.md) | active | 2026-05-03 |
| [M12 — Drie-fase agent-pipeline voor feature-ideatie](./plans/M12-three-phase-feature-pipeline.md) | proposal | 2026-05-03 |
| [M9 — Actief Product Backlog](./plans/M9-active-product-backlog.md) | active | 2026-05-03 |
| [ST-1109 — PBI krijgt een status (Ready / Blocked / Done)](./plans/ST-1109-pbi-status.md) | active | 2026-05-03 |
| [ST-1110 — Demo gebruiker read-only](./plans/ST-1110-demo-readonly.md) | active | 2026-05-03 |
| [ST-1111 — Voer uit-knop met Claude Code job queue](./plans/ST-1111-claude-job-trigger.md) | active | 2026-05-03 |
| [ST-1114 — Copilot reviews op dashboard](./plans/ST-1114-copilot-reviews.md) | active | 2026-05-03 |
| [Tweede Claude Agent — Planning Agent](./plans/tweede-claude-agent-planning.md) | proposal | 2026-05-03 |
### Archive
| Title | Updated |
|---|---|
| [CLAUDE.md workflow-update na M7 + ST-509/511/512/513](./plans/archive/2026-04-27-claude-md-workflow-update.md) | 2026-05-03 |
| [Herbruikbaar scripts/insert-milestone.ts](./plans/archive/2026-04-27-insert-milestone-tool.md) | 2026-05-03 |
| [Realtime updates voor Solo Paneel (M8)](./plans/archive/2026-04-27-m8-realtime-solo.md) | 2026-05-03 |
## Patterns
| Title | Status | Updated |
|---|---|---|
| [Bidirectionele async-comms MCP-agent ↔ user](./patterns/claude-question-channel.md) | active | 2026-05-03 |
| [Entity Dialog](./patterns/dialog.md) | active | 2026-05-03 |
| [iron-session](./patterns/iron-session.md) | active | 2026-05-03 |
| [Prisma Client singleton](./patterns/prisma-client.md) | active | 2026-05-03 |
| [Proxy (route protection)](./patterns/proxy.md) | active | 2026-05-03 |
| [QR-pairing via unauth-SSE + pre-auth cookie](./patterns/qr-login.md) | active | 2026-05-03 |
| [Route Handler (REST API)](./patterns/route-handler.md) | active | 2026-05-03 |
| [Server Action](./patterns/server-action.md) | active | 2026-05-03 |
| [Float sort_order (drag-and-drop volgorde)](./patterns/sort-order.md) | active | 2026-05-03 |
| [Patroon: Story met UI-component](./patterns/story-with-ui-component.md) | — | — |
| [Zustand optimistische update + rollback](./patterns/zustand-optimistic.md) | active | 2026-05-03 |
## Other Docs
| Title | Path | Status | Updated |
|---|---|---|---|
| [Scrum4Me REST API](./api.md) | `api.md` | active | 2026-05-03 |
| [Scrum4Me REST API](./api/rest-contract.md) | `api/rest-contract.md` | active | 2026-05-03 |
| [route-handlers](./app/getting-started/route-handlers.md) | `app/getting-started/route-handlers.md` | — | — |
| [Scrum4Me — Technische Architectuur (breadcrumb)](./architecture.md) | `architecture.md` | active | 2026-05-03 |
| [Authentication, Sessions & Demo Policy](./architecture/auth-and-sessions.md) | `architecture/auth-and-sessions.md` | active | 2026-05-03 |
| [Claude ↔ User Question Channel](./architecture/claude-question-channel.md) | `architecture/claude-question-channel.md` | active | 2026-05-03 |
| [Data Model & Prisma Schema](./architecture/data-model.md) | `architecture/data-model.md` | active | 2026-05-03 |
| [Scrum4Me — Architecture Overview](./architecture/overview.md) | `architecture/overview.md` | active | 2026-05-03 |
| [Project Structure, Stores, Realtime & Job Queue](./architecture/project-structure.md) | `architecture/project-structure.md` | active | 2026-05-03 |
| [QR-pairing Login Flow](./architecture/qr-pairing.md) | `architecture/qr-pairing.md` | active | 2026-05-03 |
| [Scrum4Me — Implementatie Backlog](./backlog.md) | `backlog.md` | active | 2026-05-03 |
| [Scrum4Me — Implementatie Backlog](./backlog/index.md) | `backlog/index.md` | active | 2026-05-03 |
| [DevPlanner — Product Backlog](./backlog/product-historical.md) | `backlog/product-historical.md` | active | 2026-05-03 |
| [Agent Instruction Audit](./decisions/agent-instructions-history.md) | `decisions/agent-instructions-history.md` | active | 2026-05-03 |
| [Scrum4Me — Styling & Design System](./design/styling.md) | `design/styling.md` | active | 2026-05-03 |
| [Docker smoke test — task 1](./docker-smoke/2-mei-task-1.md) | `docker-smoke/2-mei-task-1.md` | — | — |
| [Docker smoke test — task 2](./docker-smoke/2-mei-task-2.md) | `docker-smoke/2-mei-task-2.md` | — | — |
| [Scrum4Me — Functionele Specificatie](./functional.md) | `functional.md` | active | 2026-05-03 |
| [Scrum4Me — Glossary](./glossary.md) | `glossary.md` | active | 2026-05-03 |
| [Scrum4Me — Styling & Design System](./md3-color-scheme.md) | `md3-color-scheme.md` | active | 2026-05-03 |
| [Obsidian as Personal Authoring Layer](./obsidian-authoring.md) | `obsidian-authoring.md` | active | 2026-05-02 |
| [PbiDialog Profiel](./pbi-dialog.md) | `pbi-dialog.md` | — | — |
| [DevPlanner — User Personas](./personas.md) | `personas.md` | active | 2026-05-03 |
| [DevPlanner — Product Backlog](./product-backlog.md) | `product-backlog.md` | active | 2026-05-03 |
| [Scrum4Me — API Test Plan](./qa/api-test-plan.md) | `qa/api-test-plan.md` | active | 2026-05-03 |
| [Realtime smoke-checklist — PBI / Story / Task](./realtime-smoke.md) | `realtime-smoke.md` | — | — |
| [Branch, PR & Commit Strategy](./runbooks/branch-and-commit.md) | `runbooks/branch-and-commit.md` | active | 2026-05-03 |
| [Vercel Deployment](./runbooks/deploy-vercel.md) | `runbooks/deploy-vercel.md` | active | 2026-05-03 |
| [MCP Integration — Scrum4Me Tools](./runbooks/mcp-integration.md) | `runbooks/mcp-integration.md` | active | 2026-05-03 |
| [StoryDialog Profiel](./story-dialog.md) | `story-dialog.md` | — | — |
| [TaskDialog Profiel](./task-dialog.md) | `task-dialog.md` | — | — |
| [Scrum4Me — API Test Plan](./test-plan.md) | `test-plan.md` | active | 2026-05-03 |

View file

@ -0,0 +1,66 @@
# ADR-0000: Record architecture decisions
## Status
accepted
## Context
Scrum4Me makes several non-obvious architectural choices that aren't visible
from the code alone — for example, why we use `@base-ui/react` rather than
Radix, why drag-and-drop ordering uses float `sort_order` instead of integer
positions, why authentication runs on iron-session rather than NextAuth,
and why the demo-user policy is enforced in three layers. These decisions
are scattered across `CLAUDE.md`, individual pattern docs, plan files, and
commit messages. New contributors and AI agents working on the codebase
have no fast path to the *why*, which leads to one of two failure modes:
they either re-litigate decisions that were already settled, or they make
changes that violate constraints they didn't know about.
We want a single, predictable place to record significant architectural
choices, with enough context that a reader six months from now can decide
whether the decision still holds.
## Decision
We adopt Architecture Decision Records (ADRs) as the canonical format for
documenting significant architectural choices in this codebase. ADRs live
in `docs/adr/`, are numbered sequentially with a four-digit prefix
(`0001-...md`, `0002-...md`, …), and follow one of two templates:
- **Nygard** ([`templates/nygard.md`](./templates/nygard.md)) — default,
for one-way-door decisions with a clear motivating context.
- **MADR v4** ([`templates/madr.md`](./templates/madr.md)) — for
decisions where weighing multiple alternatives is part of the value
the record provides (auth, queue mechanics, agent integration).
The full conventions — file naming, status lifecycle, template selection
guidance — are documented in [`README.md`](./README.md).
ADRs are immutable once accepted: course corrections create a new ADR
that supersedes the old one rather than editing the original.
## Consequences
### Positive
- Architectural choices have a single, predictable home that an AI agent
or new contributor can find with one `ls docs/adr/`.
- The "why" of each decision is captured at the moment it's made, when
the context is fresh, rather than reconstructed later from commits.
- Superseded decisions remain readable, so future contributors can see the
history of a choice without git archaeology.
- The format scales: writing an ADR is a 15-minute activity for the
default Nygard template, low enough overhead to be worth doing every
time.
### Negative
- Adds a small ritual to every significant architectural decision — easy
to skip when moving fast, leading to a stale or incomplete record if
not enforced through review.
- Backfilling existing decisions requires writing 58 retrospective ADRs
for choices that were never recorded (planned in fase 6 of
[`../plans/docs-restructure-ai-lookup.md`](../plans/docs-restructure-ai-lookup.md)).
- Two templates means a per-decision choice about which to use. Mitigated
by making Nygard the explicit default in `README.md`.

View file

@ -0,0 +1,34 @@
# ADR-0001: Use @base-ui/react instead of Radix UI
## Status
accepted
## Context
shadcn/ui ships visual components that are typically built on Radix UI primitives. When we bootstrapped Scrum4Me with shadcn, the component wrappers in `components/ui/` were adapted to use `@base-ui/react` instead of the Radix packages. `@base-ui/react` exposes the same accessibility primitives but uses a `render` prop for composition instead of Radix's `asChild` pattern. Attempting to use `asChild` in our TypeScript-strict setup produced type errors because the prop is not declared in `@base-ui/react`'s API surface.
## Decision
We use `@base-ui/react` exclusively. No Radix UI package (`@radix-ui/*`) is imported anywhere in the codebase. Composition always uses the `render` prop:
```tsx
// ✅ correct
<TooltipTrigger render={<button />}>…</TooltipTrigger>
// ❌ wrong — asChild does not exist on @base-ui/react primitives
<TooltipTrigger asChild><button></button></TooltipTrigger>
```
## Consequences
### Positive
- TypeScript stays clean; no `any` casts or `asChild` workarounds.
- `@base-ui/react` is actively maintained by the MUI team with React 19 support.
- Composition pattern is explicit and grep-friendly.
### Negative
- AI agents trained on Radix-based shadcn documentation will default to `asChild` — they must be reminded of the `render`-prop pattern (this ADR exists for that reason).
- shadcn CLI-generated components may need manual adjustment when installed.

View file

@ -0,0 +1,26 @@
# ADR-0002: Use float sort_order for drag-and-drop ordering
## Status
accepted
## Context
The planning screens (PBI list, Story list, Task list, Solo board) all support drag-and-drop reordering. With integer positions, inserting an item between positions 3 and 4 requires renumbering every subsequent row — an O(N) write for every drag operation. At the scale of a sprint board this is tolerable, but it causes unnecessary lock contention and makes optimistic UI rollback harder. See `docs/patterns/sort-order.md` for the full implementation pattern.
## Decision
Every ordered collection uses a `Float` column named `sort_order`. Inserting between two items sets `sort_order = (prev + next) / 2`. New items appended to the end get `last + 1.0`.
## Consequences
### Positive
- Reorder writes are O(1) — only the moved item's row is updated.
- Optimistic UI updates map directly to the same midpoint calculation.
- No lock contention on adjacent rows during concurrent drags.
### Negative
- Repeated insertions between the same two items cause float precision drift. Mitigation: a periodic compaction job normalizes `sort_order` values to integers × 1000 when the gap drops below `0.001`. This is a known trade-off; compaction has not been needed in practice yet.
- Queries that return items in `sort_order` order must always include `ORDER BY sort_order` — there is no implicit ordering.

View file

@ -0,0 +1,61 @@
---
status: accepted
date: 2026-05-03
decision-makers: [janpetervisser]
---
# ADR-0003: One branch per milestone, push only after user test
## Context and Problem Statement
Every `git push` to a feature branch triggers a Vercel preview deployment. On the Hobby plan, preview builds are limited and cost money. How should we structure branches and pushes to minimize preview-build spend while still supporting a fast AI-driven development loop?
## Decision Drivers
- Vercel Hobby plan: preview builds are finite and billed per deployment.
- Small team (primarily solo developer + AI agent): branch overhead should be minimal.
- AI-driven flow: the agent commits frequently in small logical layers; we don't want a push per commit.
- User acceptance is done interactively per milestone, not per story.
## Considered Options
- **Branch per story** — one branch per story, PR per story.
- **Branch per milestone** — one branch for all stories in a milestone, single PR after user test.
- **Trunk-based development** — commit directly to `main` with feature flags.
## Decision Outcome
Chosen option: **Branch per milestone**, because it is the only option that keeps preview-build count proportional to milestones (not stories), while still enabling isolated review via a single PR.
### Consequences
- Good, because preview deployments are rare — only one per milestone reaching review.
- Good, because PR history maps to milestones, not micro-stories.
- Bad, because branches live longer; merge conflicts are larger but less frequent.
- Bad, because a single failed story blocks the milestone PR.
### Confirmation
Before pushing, the developer/agent must confirm explicitly. `git push` is never automated. See `docs/runbooks/branch-and-commit.md`.
## Pros and Cons of the Options
### Branch per story
- Good, because small, focused PRs are easy to review.
- Bad, because each push triggers a preview build — N stories = N builds per milestone.
### Branch per milestone
- Good, because minimal preview builds.
- Good, because the PR represents a coherent feature set.
- Bad, because long-lived branches.
### Trunk-based development
- Good, because no branch management overhead.
- Bad, because requires feature flags to hide incomplete work — too much infrastructure for this scale.
## More Information
Revisit this decision if/when the Vercel account upgrades to Pro (unlimited preview builds). At that point, branch-per-story is the preferred default. Update `docs/runbooks/branch-and-commit.md` and this ADR when that happens.

View file

@ -0,0 +1,28 @@
# ADR-0004: DB enums UPPER_SNAKE, API enums lowercase, mapped exclusively via lib/task-status.ts
## Status
accepted
## Context
Prisma generates TypeScript types from PostgreSQL enum values verbatim. Our DB enums use `UPPER_SNAKE_CASE` (e.g. `TO_DO`, `IN_PROGRESS`, `DONE`) because that is the PostgreSQL convention and it keeps Prisma-generated code readable. However, REST API consumers — including Claude Code via MCP and frontend fetch calls — expect lowercase, underscore-separated values (`todo`, `in_progress`, `done`). Without a single conversion boundary, ad-hoc `.toLowerCase()` calls scattered across route handlers and actions introduce silent mapping bugs when enum values change.
## Decision
- The database retains `UPPER_SNAKE_CASE` enum values. Prisma schema is the source of truth.
- The REST API (route handlers) and MCP server always expose and accept **lowercase** enum strings.
- All conversion happens exclusively in `lib/task-status.ts` via named mapper functions. No `.toLowerCase()`, `.toUpperCase()`, or inline string mapping anywhere else.
## Consequences
### Positive
- A single file to audit when enum values change.
- TypeScript types catch missing branches in mapper exhaustive checks.
- API contract is stable and grep-friendly.
### Negative
- Every developer (and AI agent) must know to use the mappers rather than string coercion. Violations compile fine but break the API contract at runtime.
- Mitigated by an ESLint rule that flags direct `.toLowerCase()` on known enum types (pending implementation).

View file

@ -0,0 +1,71 @@
---
status: accepted
date: 2026-05-03
decision-makers: [janpetervisser]
---
# ADR-0005: Use iron-session for authentication instead of NextAuth/Clerk/Supabase Auth
## Context and Problem Statement
Scrum4Me requires username/password login without email verification, a synchronous demo-user check on every request, and full control over the session cookie shape (including an `isDemo` flag). Which authentication solution fits these constraints at minimal complexity?
## Decision Drivers
- No email required — username/password only.
- Demo-user policy (ADR-0006) requires a synchronous `isDemo` check in both middleware and server actions.
- No third-party redirect chain — auth must stay in-process.
- Solo-developer project: minimal external dependencies preferred.
## Considered Options
- **NextAuth / Auth.js v5**
- **Clerk**
- **Supabase Auth**
- **iron-session + bcryptjs**
## Decision Outcome
Chosen option: **iron-session + bcryptjs**, because it is the only option that gives us full control over cookie contents, has zero external redirect dependency, and lets us embed `isDemo` directly in the session payload.
### Consequences
- Good, because session structure is fully controlled — we add any field we need.
- Good, because no external service dependency for auth; works offline and in CI.
- Good, because synchronous cookie read in `proxy.ts` middleware is trivial.
- Bad, because we own the password hashing, session rotation, and CSRF protection.
- Bad, because no OAuth/social login without building it ourselves.
### Confirmation
`lib/session.ts` defines the session type. `docs/patterns/iron-session.md` documents the pattern. Any new field on the session object must be added to the type there.
## Pros and Cons of the Options
### NextAuth / Auth.js v5
- Good, because OAuth, email magic links, and credentials all in one library.
- Bad, because credentials provider is discouraged in v5; session shape is opaque.
- Bad, because adding `isDemo` to the JWT requires custom callbacks.
### Clerk
- Good, because fully managed, beautiful UI, no session code to maintain.
- Bad, because requires third-party redirect; adds external dependency.
- Bad, because demo-user policy would require custom session metadata sync.
### Supabase Auth
- Good, because integrates with Supabase storage (but we use Neon).
- Bad, because username/password without email is not the primary use case.
- Bad, because adds a second database dependency just for auth.
### iron-session + bcryptjs
- Good, because minimal, explicit, and TypeScript-native.
- Good, because session payload is a plain object we fully control.
- Neutral, because we write our own password logic (bcrypt makes it safe).
## More Information
See `docs/patterns/iron-session.md` for implementation details. Revisit if multi-tenant or SSO requirements emerge.

View file

@ -0,0 +1,30 @@
# ADR-0006: Demo-user write protection enforced in three layers
## Status
accepted
## Context
Scrum4Me has a demo account that allows prospective users to explore the app without signing up. The demo user must never be able to create, update, or delete any data. A single guard at one layer is insufficient: a bug or a missing check in any one layer would expose a write path. See `docs/architecture/auth-and-sessions.md` and `docs/plans/ST-1110-demo-readonly.md` for implementation details.
## Decision
Write protection for the demo user is enforced at **three independent layers**:
1. **Network — `proxy.ts`:** The Next.js proxy middleware rejects all non-GET requests from demo sessions before they reach any route handler or server action.
2. **Server — every Server Action and Route Handler:** Each write endpoint checks `session.isDemo` and returns `403` immediately if true.
3. **UI — disabled buttons + `<DemoTooltip>`:** Write controls (create, edit, delete, reorder) are rendered as `disabled` with a tooltip explaining the demo restriction. No write request is ever sent.
## Consequences
### Positive
- Defense-in-depth: any single layer can fail independently without exposing a write path.
- Clear user feedback at the UI layer without relying on error responses.
- Straightforward to audit: search for `isDemo` to find all enforcement points.
### Negative
- Three enforcement sites for every new write operation — easy to miss one when adding a new feature.
- Mitigation: the `DemoTooltip` pattern is documented in `docs/patterns/` and enforced in code review.

View file

@ -0,0 +1,64 @@
---
status: accepted
date: 2026-05-03
decision-makers: [janpetervisser]
---
# ADR-0007: Agent ↔ user question channel via persistent table + LISTEN/NOTIFY
## Context and Problem Statement
When Claude Code is executing a task and needs human input, it must be able to pause, post a question, and receive an answer — potentially across separate sessions. The app must notify an active user that a question is waiting. How should this async communication channel be designed?
## Decision Drivers
- Questions must survive agent session restarts (persistent, not in-memory).
- The app user needs a real-time notification without polling from the client.
- The infrastructure already includes PostgreSQL with LISTEN/NOTIFY (used for M8 realtime updates).
- Answers must be readable by the agent in a future session without the original connection.
## Considered Options
- **Synchronous polling only** — agent polls an endpoint every N seconds.
- **Push via SSE without persistence** — agent opens SSE connection, user pushes answer over it.
- **Persistent `claude_questions` table + PostgreSQL LISTEN/NOTIFY**
## Decision Outcome
Chosen option: **Persistent table + LISTEN/NOTIFY**, because it is the only option that survives session restarts on both ends and reuses existing infrastructure.
### Consequences
- Good, because questions survive agent and user session restarts.
- Good, because reuses the `scrum4me_changes` LISTEN/NOTIFY channel already in place.
- Good, because any product member with access can answer, not just the original session.
- Bad, because adds a `claude_questions` table and trigger to the schema.
- Bad, because LISTEN/NOTIFY requires a persistent DB connection (`DIRECT_URL` env var).
### Confirmation
`docs/patterns/claude-question-channel.md` documents the full implementation. MCP tools: `ask_user_question`, `get_question_answer`, `list_open_questions`, `cancel_question`.
## Pros and Cons of the Options
### Synchronous polling only
- Good, because simple — no extra infrastructure.
- Bad, because agent blocks a CPU slot while polling; question lost if agent restarts.
- Bad, because no real-time notification to the user.
### Push via SSE without persistence
- Good, because low latency.
- Bad, because agent-side SSE connection is fragile across restarts.
- Bad, because no persistence — if the user isn't connected at the moment the question is posted, it is lost.
### Persistent table + LISTEN/NOTIFY
- Good, because fully durable.
- Good, because real-time notification to the user reuses existing M8 infrastructure.
- Neutral, because requires `DIRECT_URL` for a persistent PostgreSQL connection (already required for M8).
## More Information
See `docs/plans/M11-claude-questions.md` and `docs/patterns/claude-question-channel.md`.

View file

@ -0,0 +1,41 @@
---
title: "ADR-0008: Agent instructions in CLAUDE.md + topical runbooks"
status: accepted
date: 2026-05-03
---
# ADR-0008: Agent instructions in CLAUDE.md + topical runbooks
## Status
Accepted
## Context
Claude Code reads `CLAUDE.md` at session start and injects it verbatim into the system prompt. As the project grew, the file expanded to ~350 lines covering stack, patterns, MCP tools, commit strategy, branch policy, Vercel deployment, and demo-user rules. At that length the token cost was significant and the file was hard to navigate — both for humans and for AI agents skimming for a specific rule.
A separate `docs/agent-instruction-audit.md` tracked *why* each rule existed, but that meta-history was buried and rarely consulted. The result: rules were added but never removed, and the file drifted toward being a reference dump rather than a decision-forcing instrument.
## Decision
Split `CLAUDE.md` into a short ≤150-line orientation hub that hard-links to topical runbooks:
- `docs/runbooks/branch-and-commit.md` — branch policy, plan mode, commit strategy
- `docs/runbooks/mcp-integration.md` — all 18 MCP tools + batch-loop invariants
- `docs/runbooks/deploy-vercel.md` — Sharp, cron, env-var, preflight rules
`CLAUDE.md` retains only the rules an agent **must** have in active context on every turn: the orientation table, the 8 hardstop rules, stack table, patterns quickref, and env vars. Everything else is a hyperlink.
`AGENTS.md` (Codex entry-point) becomes a 10-line redirect stub pointing at `CLAUDE.md`.
## Consequences
**Positive**
- Active-context token cost drops ~60 % — the hub is ~114 lines vs 350.
- Each runbook is standalone and audience-tagged (`audience: [ai-agent]` etc.), so a future agent can fetch exactly the doc it needs.
- The split gives a natural place to add rules without polluting the root file.
- `docs/decisions/agent-instructions-history.md` (renamed from `agent-instruction-audit.md`) documents the *why* behind each rule as persistent institutional memory.
**Negative**
- An agent that reads only `CLAUDE.md` will miss branch/MCP/deploy rules unless it follows the links. Mitigation: critical one-liners (e.g., "PR only after user test") are kept as hardstops in the hub.
- The split increases the number of files to maintain; runbooks can drift from `CLAUDE.md` if authors forget to update both. Mitigation: docs-sync convention in `CLAUDE.md` Conventions section.

101
docs/adr/README.md Normal file
View file

@ -0,0 +1,101 @@
---
title: Architecture Decision Records
status: active
audience: ai-agent, maintainer, contributor
language: en
last_updated: 2026-05-02
---
# Architecture Decision Records
This directory contains the Architecture Decision Records (ADRs) for Scrum4Me.
## What is an ADR
An ADR is a short document that captures a single significant architectural
decision: the context that forced the decision, the choice we made, and the
consequences of that choice. ADRs are immutable once accepted — if a later
decision changes course, we write a new ADR that supersedes the old one.
We use ADRs because the project mixes several non-obvious choices (Next.js 16
specifics, `@base-ui/react` over Radix, float `sort_order` for drag-and-drop,
iron-session over NextAuth, demo-user three-layer policy, MCP integration
patterns) and an AI agent reading the codebase six months from now needs to
find the *why* without spelunking through commit history.
## File naming
```
NNNN-kebab-case-title.md
```
- `NNNN` — four-digit zero-padded sequential number, starting at `0001`
(`0000` is reserved for the meta-ADR that introduces the practice).
- `kebab-case-title` — lowercase, hyphen-separated, short noun phrase
echoing the decision (`base-ui-over-radix`, not `decided-to-use-baseui`).
- Always `.md`.
## Choosing a template
Two templates live in [`templates/`](./templates/). Default to Nygard.
### Nygard (default — [`templates/nygard.md`](./templates/nygard.md))
Use Nygard for the common case: a decision that is essentially a one-way
door with a clear motivating context and one obvious choice. Four sections:
**Title, Status, Context, Decision, Consequences (Positive / Negative)**.
Aim: ≤60 lines. Reads in under a minute.
### MADR v4 (when alternatives matter — [`templates/madr.md`](./templates/madr.md))
Use MADR when the decision involves weighing multiple alternatives that a
future reader would otherwise re-litigate. Triggers:
- **Authentication / session strategy** (NextAuth vs iron-session vs Clerk).
- **Queue / messaging mechanics** (LISTEN/NOTIFY vs Redis vs SQS).
- **Agent integration patterns** (REST polling vs MCP vs SSE channel).
- **Schema or data-model choices with non-trivial migration cost.**
- Any decision where you want to record the *rejected* options so future
contributors don't propose them again.
MADR adds: YAML front-matter (status, date, decision-makers, consulted,
informed), explicit Decision Drivers, Considered Options, Pros and Cons of
each option, Confirmation, and More Information.
## Status lifecycle
```
proposed → accepted → (optionally) superseded by NNNN
↘ (optionally) deprecated
```
- **proposed** — drafted, awaiting decision-maker sign-off.
- **accepted** — current binding decision; the codebase reflects this.
- **superseded by ADR-NNNN** — replaced. Keep the file; never edit the
Decision section. Add a one-line "Superseded by …" note at the top of
the Status section and link to the new ADR.
- **deprecated** — still current but no longer recommended; usually a
precursor to a future supersession.
Once an ADR is accepted, it is immutable except for the Status field and
typo fixes. Course corrections always create a new ADR.
## Index of ADRs
| # | Title | Status | Template |
|---|---|---|---|
| [0000](./0000-record-architecture-decisions.md) | Record architecture decisions | accepted | Nygard |
| [0001](./0001-base-ui-over-radix.md) | @base-ui/react over Radix UI | accepted | Nygard |
| [0002](./0002-float-sort-order.md) | Float sort_order for drag-and-drop reorder | accepted | Nygard |
| [0003](./0003-one-branch-per-milestone.md) | One branch per milestone (Hobby plan) | accepted | MADR |
| [0004](./0004-status-enum-mapping.md) | Status enum mapping via lib/task-status.ts | accepted | Nygard |
| [0005](./0005-iron-session-over-nextauth.md) | iron-session over NextAuth/Clerk | accepted | MADR |
| [0006](./0006-demo-user-three-layer-policy.md) | Demo-user three-layer write guard | accepted | Nygard |
| [0007](./0007-claude-question-channel-design.md) | Agent ↔ user question channel via persistent table + LISTEN/NOTIFY | accepted | MADR |
| [0008](./0008-agent-instructions-in-claude-md.md) | Agent instructions in CLAUDE.md + topical runbooks | accepted | Nygard |
When new ADRs are added, the docs index generator (`npm run docs:index`)
will list them in [`../INDEX.md`](../INDEX.md). Update this table by hand
when you add or supersede an ADR — the script aggregates across the whole
docs tree, this README is the canonical ADR-only roster.

View file

@ -0,0 +1,78 @@
---
status: {{proposed | rejected | accepted | deprecated | superseded by ADR-NNNN}}
date: {{YYYY-MM-DD when the decision was last updated}}
decision-makers: {{list everyone who participated in the decision}}
consulted: {{list everyone whose opinions were sought (typically subject-matter experts), and with whom there was a two-way communication}}
informed: {{list everyone who is kept up-to-date on progress, and with whom there is one-way communication}}
---
# ADR-{{NNNN}}: {{short title, representative of solved problem and found solution}}
## Context and Problem Statement
{{Describe the context and problem statement, e.g., in free form using two
to three sentences or in the form of an illustrative story. You may want
to articulate the problem in form of a question and add links to
collaboration boards or issue management systems.}}
## Decision Drivers
- {{decision driver 1, e.g., a force, facing concern, …}}
- {{decision driver 2, e.g., a force, facing concern, …}}
## Considered Options
- {{title of option 1}}
- {{title of option 2}}
- {{title of option 3}}
## Decision Outcome
Chosen option: "{{title of option 1}}", because {{justification — e.g., only
option which meets a knock-out criterion / which resolves a force / …
turned out best (see "Pros and Cons of the Options" below)}}.
### Consequences
- Good, because {{positive consequence, e.g., improvement of one or more
desired qualities, …}}
- Bad, because {{negative consequence, e.g., compromising one or more
desired qualities, …}}
### Confirmation
{{Describe how the implementation of/compliance with the ADR can be
confirmed. E.g., a test, a peer review, a runtime check.}}
## Pros and Cons of the Options
### {{title of option 1}}
{{example | description | pointer to more information | …}}
- Good, because {{argument a}}
- Good, because {{argument b}}
- Neutral, because {{argument c}}
- Bad, because {{argument d}}
### {{title of option 2}}
{{example | description | pointer to more information | …}}
- Good, because {{argument a}}
- Bad, because {{argument b}}
### {{title of option 3}}
{{example | description | pointer to more information | …}}
- Good, because {{argument a}}
- Bad, because {{argument b}}
## More Information
{{You might want to provide additional evidence/confidence for the decision
outcome here and/or document the team agreement on the decision and/or
define when this decision the decision should be realized and if/when it
should be re-visited. Links to other decisions and resources might appear
here as well.}}

View file

@ -0,0 +1,31 @@
# ADR-{{NNNN}}: {{Short noun phrase describing the decision}}
## Status
{{proposed | accepted | superseded by ADR-NNNN | deprecated}}
## Context
{{What is the issue we're seeing that motivates this decision? Describe the
forces at play — technical, organizational, business — that make this choice
necessary now. State facts, not opinions. Keep it short: one or two
paragraphs is usually enough. If a reader needs background that lives
elsewhere, link to it instead of duplicating.}}
## Decision
{{The choice we've made, written in present tense as a declarative statement.
"We will use X." "We adopt Y." Avoid hedging language. One paragraph.}}
## Consequences
### Positive
- {{What becomes easier or possible because of this decision?}}
- {{What problem is no longer relevant?}}
### Negative
- {{What becomes harder, slower, or more expensive?}}
- {{What did we accept as a trade-off?}}
- {{What new risks does this introduce, and how do we mitigate them?}}

View file

@ -1,3 +1,11 @@
---
title: "Scrum4Me REST API"
status: active
audience: [ai-agent, contributor]
language: en
last_updated: 2026-05-03
---
# Scrum4Me REST API # Scrum4Me REST API
REST-API contract voor Claude Code en andere clients. REST-API contract voor Claude Code en andere clients.
@ -419,7 +427,7 @@ curl -i -X POST -b /tmp/jar -c /tmp/jar \
## Notifications — Vraag-antwoord-kanaal (M11) ## Notifications — Vraag-antwoord-kanaal (M11)
Endpoints voor de Claude vraag-antwoord-flow. De **MCP-tools** in de scrum4me-mcp-repo (`ask_user_question`, `get_question_answer`, `list_open_questions`, `cancel_question`) zijn de primaire schrijf-interface; de endpoints hieronder zijn voor de browser-UI en cron. Endpoints voor de Claude vraag-antwoord-flow. De **MCP-tools** in de mcp-repo (`ask_user_question`, `get_question_answer`, `list_open_questions`, `cancel_question`) zijn de primaire schrijf-interface; de endpoints hieronder zijn voor de browser-UI en cron.
### `GET /api/realtime/notifications` ### `GET /api/realtime/notifications`

528
docs/api/rest-contract.md Normal file
View file

@ -0,0 +1,528 @@
---
title: "Scrum4Me REST API"
status: active
audience: [ai-agent, contributor]
language: en
last_updated: 2026-05-03
---
# Scrum4Me REST API
REST-API contract voor Claude Code en andere clients.
## Authenticatie
Alle endpoints behalve `GET /api/health` vereisen een Bearer-token:
```
Authorization: Bearer <token>
```
Tokens beheer je via Instellingen → Tokens (`/settings/tokens`). Een token is gekoppeld aan één gebruiker; een demo-account-token kan lezen maar niet schrijven (`403`).
## Status-enums
De API gebruikt **lowercase** statussen. De database gebruikt UPPER_SNAKE; de vertaling gebeurt op de boundary.
| Entiteit | Waarden |
|---|---|
| Task status | `todo`, `in_progress`, `review`, `done` |
| Story status | `open`, `in_sprint`, `done` |
## Foutcodes
| Code | Betekenis |
|---|---|
| `200` | OK |
| `201` | Created |
| `400` | Malformed body (bv. ongeldige JSON) |
| `401` | Token ontbreekt of ongeldig |
| `403` | Token heeft geen toegang (demo-account, geen lid van product) |
| `404` | Resource niet gevonden |
| `422` | Validatiefout — body is wel-gevormd maar niet acceptabel |
| `500` | Onverwachte serverfout |
---
## Endpoints
### `GET /api/health`
Health-probe. Geen authenticatie vereist.
**Query params:** `?db=1` voegt een DB-ping toe.
**Response (200):**
```json
{ "status": "ok", "version": "0.3.x", "time": "2026-04-26T20:00:00Z" }
```
Met `?db=1`:
```json
{ "status": "ok", "version": "0.3.x", "time": "...", "database": "ok" }
```
`database` is `"ok"` of `"down"`. De endpoint zelf retourneert altijd `200`.
```bash
curl https://scrum4me.app/api/health?db=1
```
---
### `GET /api/products`
Lijst van actieve producten waar de tokengebruiker eigenaar of lid van is.
**Response (200):**
```json
[
{
"id": "cmofu...",
"code": "SCRUM4ME",
"name": "Scrum4Me",
"description": "...",
"repo_url": "https://github.com/...",
"definition_of_done": "..."
}
]
```
```bash
curl -H "Authorization: Bearer $TOKEN" https://scrum4me.app/api/products
```
---
### `GET /api/products/:id/claude-context`
Bundled context voor Claude Code: product, actieve sprint, volgende story (met tasks) en open todos van de tokengebruiker — in één call.
**Response (200):**
```json
{
"product": { "id", "code", "name", "description", "repo_url", "definition_of_done" },
"active_sprint": { "id": "...", "sprint_goal": "...", "status": "ACTIVE" } | null,
"next_story": {
"id", "code", "title", "description", "acceptance_criteria",
"priority", "status",
"tasks": [
{ "id", "code", "title", "description", "implementation_plan",
"priority", "sort_order", "status" }
]
} | null,
"open_todos": [
{ "id", "title", "description", "created_at" }
]
}
```
`open_todos` is gelimiteerd op 50 items, gesorteerd op `created_at` asc. Demo-tokens kunnen dit endpoint lezen.
```bash
curl -H "Authorization: Bearer $TOKEN" \
https://scrum4me.app/api/products/$PRODUCT_ID/claude-context
```
---
### `GET /api/products/:id/next-story`
Hoogst geprioriteerde open story in de actieve sprint.
**Response (200):**
```json
{
"id": "...",
"code": "ST-356",
"title": "Solo Kanban-bord met DnD en Zustand",
"description": "...",
"acceptance_criteria": "...",
"status": "in_sprint",
"tasks": [
{
"id": "...",
"code": "ST-356.1",
"title": "Store stores/solo-store.ts",
"description": "...",
"implementation_plan": null,
"priority": 2,
"sort_order": 1,
"status": "todo"
}
]
}
```
**Foutcodes:** `404` als geen actieve sprint of geen open stories.
---
### `GET /api/sprints/:id/tasks`
Lijst taken van de sprint, geordend op `(story.sort_order, task.priority, task.sort_order)`.
**Query params:** `?limit=N` (default 10, max 50)
**Response (200):**
```json
[
{
"id": "...",
"code": "ST-356.1",
"title": "...",
"description": "...",
"implementation_plan": null,
"story_id": "...",
"story_code": "ST-356",
"priority": 2,
"sort_order": 1,
"status": "todo"
}
]
```
---
### `PATCH /api/stories/:id/tasks/reorder`
Volgorde van taken binnen een story aanpassen.
**Body:**
```json
{ "task_ids": ["task-id-a", "task-id-b", "task-id-c"] }
```
Alle IDs moeten bij de story horen. **Foutcodes:** `422` bij Zod-fouten of als een task_id niet tot de story behoort.
---
### `PATCH /api/tasks/:id`
Status of implementation_plan bijwerken. Minstens één van beide is verplicht.
Toegestane status-waarden zijn `todo`, `in_progress` en `done`. `review`
wordt door deze endpoint geweigerd zolang de sprint-UI die state niet
rendert — gebruik de Kanban-board voor REVIEW-overgangen.
**Body:**
```json
{ "status": "in_progress", "implementation_plan": "..." }
```
**Response (200):**
```json
{
"id": "...",
"status": "in_progress",
"implementation_plan": "..."
}
```
**Foutcodes:** `422` bij ongeldige body of onbekende status. `403` bij demo-token.
---
### `POST /api/stories/:id/log`
Activiteit vastleggen op een story.
**Body — IMPLEMENTATION_PLAN:**
```json
{
"type": "IMPLEMENTATION_PLAN",
"content": "Plan: ...",
"metadata": { "branch": "feat/x" }
}
```
**Body — TEST_RESULT:**
```json
{
"type": "TEST_RESULT",
"content": "Alle tests groen",
"status": "PASSED",
"metadata": { "ci_run": "..." }
}
```
**Body — COMMIT:**
```json
{
"type": "COMMIT",
"content": "Werk afgerond",
"commit_hash": "abc123",
"commit_message": "feat(ST-XXX): ...",
"metadata": { "branch": "feat/x" }
}
```
`metadata` is optioneel, vrij JSON-object. **Response (201):**
```json
{ "id": "...", "created_at": "..." }
```
---
### `POST /api/todos`
Nieuwe todo voor de tokengebruiker.
**Body:**
```json
{
"title": "Een ding doen",
"description": "Optionele uitleg, max 2000 tekens",
"product_id": "cmof..."
}
```
**Response (201):**
```json
{ "id": "...", "title": "...", "description": "...", "created_at": "..." }
```
---
### `GET /api/realtime/solo?product_id=...`
Server-Sent Events stream voor het Solo Paneel. Wordt gebruikt door de browser-UI (`useSoloRealtime`); voor Claude Code zelden relevant, maar gedocumenteerd voor volledigheid.
**Auth:** iron-session cookie of Bearer-token. Demo-tokens mogen lezen.
**Query params:** `product_id` (verplicht).
**Response:** `text/event-stream`. Stream blijft open tot de client sluit of de server na 240s een hard-close doet (client herconnect dan transparant).
**Events:**
- `event: ready` — eenmalig direct na connect, met `{ product_id, sprint_id }` als payload.
- `event: error` — bij interne fouten (pg connect mislukt e.d.).
- `data: {...}` — task/story mutaties die binnen scope vallen (zie hieronder). Payload-shape:
```json
{
"op": "I" | "U" | "D",
"entity": "task" | "story",
"id": "cmof...",
"story_id": "cmof...",
"product_id": "cmof...",
"sprint_id": "cmog..." ,
"assignee_id": "cmof..." ,
"task_status": "TO_DO" | "IN_PROGRESS" | "REVIEW" | "DONE",
"task_title": "...",
"task_sort_order": 1,
"changed_fields": ["status", "updated_at"]
}
```
Niet alle velden zijn altijd aanwezig — `task_*` alleen voor `entity: "task"`, idem `story_*`. `task_status` gebruikt de **DB-enum** (UPPER_SNAKE), niet de lowercase API-vorm.
- `: heartbeat` — SSE-comment elke 25s, om proxies keep-alive te houden. Kan genegeerd worden.
**Server-side filter:**
- `product_id` matcht de query-param
- `sprint_id` matcht de actieve sprint van het product
- `assignee_id` is gelijk aan de ingelogde user (of `null` voor unassigned-story claims)
Niet-matchende events worden gedropt — clients ontvangen geen irrelevante data.
**Voorbeeld (browser):**
```js
const source = new EventSource('/api/realtime/solo?product_id=cmof...')
source.onmessage = (e) => console.log(JSON.parse(e.data))
```
---
## Auth — QR-pairing (M10)
Drie anonieme/cookie-geauthenticeerde endpoints voor de password-loze inlog
via QR-pairing. Worden door de browser gebruikt (niet door Claude Code) —
gedocumenteerd voor volledigheid en voor handmatige curl-tests.
**Cookie-mechaniek:** `pair/start` zet een korte `s4m_pair`-HttpOnly-cookie
(`Path=/api/auth/pair`, `Max-Age=300`, `SameSite=Lax`, `Secure` in productie).
`pair/stream` en `pair/claim` authenticeren tegen die cookie. Geheim materiaal
zit nooit in URL-paden of querystrings — `mobileSecret` reist alleen via QR-
fragment (`#s=…`) en POST-body, `desktopToken` alleen via cookie.
### `POST /api/auth/pair/start`
Anon. Maakt een nieuwe `LoginPairing` aan en zet de pre-auth cookie.
**Auth:** geen.
**Body:** geen.
**Rate-limit:** 10 per IP per minuut (zelfde patroon als `/login`).
**Response 200:**
```json
{
"pairingId": "cmoh...",
"mobileSecret": "<43-char base64url>",
"expiresAt": "2026-04-27T20:30:00.000Z",
"qrUrl": "https://.../m/pair#id=cmoh...&s=<mobileSecret>"
}
```
Plus `Set-Cookie: s4m_pair=<desktopToken>; HttpOnly; Path=/api/auth/pair; Max-Age=300; SameSite=Lax`.
**Foutcodes:** `429` bij rate-limit overschreden.
**Voorbeeld:**
```bash
curl -i -X POST -c /tmp/jar http://localhost:3000/api/auth/pair/start
```
---
### `GET /api/auth/pair/stream/:pairingId`
Server-Sent Events stream die de desktop opent direct na `pair/start` om op
de approve-bevestiging van de mobiel te wachten.
**Auth:** `s4m_pair`-cookie. Werkt vanuit `EventSource` met `withCredentials: true`.
**Path:** `pairingId` is niet vertrouwelijk; cookie is het bewijs.
**Stream-duur:** maximaal 240s (Vercel-buffer onder de 300s `maxDuration`); sluit
zodra status `consumed` of `cancelled` doorkomt.
**Events:**
- `event: state` — eenmalig direct na connect, met `{ pairing_id, status }` (status van pairing op moment van connecten — voorkomt race wanneer approve net vóór SSE-open landt).
- `data: {...}` — bij elke status-overgang. Payload:
```json
{ "op": "I" | "U", "pairing_id": "cmoh...", "status": "pending" | "approved" | "consumed" | "cancelled" }
```
- `: heartbeat` — SSE-comment elke 25s.
**Foutcodes:** `401` zonder/foute cookie, `404` als pairing onbekend, `410` als pairing verlopen.
**Voorbeeld:**
```bash
curl -N -i -b /tmp/jar http://localhost:3000/api/auth/pair/stream/<pairingId>
```
---
### `POST /api/auth/pair/claim`
Cookie-auth. Atomisch consume van een approved pairing → schrijft de echte
`session` cookie zodat de desktop is ingelogd.
**Auth:** `s4m_pair`-cookie.
**Body:** `{ "pairingId": "cmoh..." }`.
**Response 200:** `{ "ok": true }` plus
- `Set-Cookie: session=...; HttpOnly; SameSite=Lax` — paired-sessie met `paired: true` en `pairedExpiresAt = now + 8h` payload-velden.
- `Set-Cookie: s4m_pair=...; Max-Age=0` — pre-auth cookie wordt gewist.
**Foutcodes:**
- `400` bij ontbrekende of malformed body
- `401` zonder cookie of bij hash-mismatch (cookie matcht geen pairing)
- `410` als pairing al consumed/cancelled is (replay) of verlopen
**Voorbeeld:**
```bash
curl -i -X POST -b /tmp/jar -c /tmp/jar \
-H "Content-Type: application/json" \
-d '{"pairingId":"<pairingId>"}' \
http://localhost:3000/api/auth/pair/claim
```
---
## Notifications — Vraag-antwoord-kanaal (M11)
Endpoints voor de Claude vraag-antwoord-flow. De **MCP-tools** in de mcp-repo (`ask_user_question`, `get_question_answer`, `list_open_questions`, `cancel_question`) zijn de primaire schrijf-interface; de endpoints hieronder zijn voor de browser-UI en cron.
### `GET /api/realtime/notifications`
Server-Sent Events stream voor de notifications-bell in de NavBar. **User-scoped** — geen `product_id`-param; filtert server-side op alle producten waar de gebruiker eigenaar of teamlid is.
**Auth:** iron-session cookie. Demo-gebruikers mogen lezen.
**Response:** `text/event-stream`. Stream blijft open tot client sluit of server na 240s een hard-close doet (client herconnect).
**Events:**
- `event: state` — eenmalig direct na connect, met `{ questions: [...] }` als payload (zelfde shape als de live updates).
- `data: {...}` — bij elke status-overgang in `claude_questions`. Payload-shape:
```json
{
"op": "I" | "U",
"entity": "question",
"id": "cmoh...",
"product_id": "cmoh...",
"story_id": "cmoh...",
"task_id": "cmoh..." | null,
"assignee_id": "cmoh..." | null,
"status": "open" | "answered" | "cancelled" | "expired"
}
```
Het is een delta — voor de volledige vraag-tekst en options reconnect de client (initial-state-event levert ze opnieuw).
- `: heartbeat` — SSE-comment elke 25s.
**Server-side filter:**
- `payload.entity === 'question'` (`task` en `story` events horen op `/api/realtime/solo`)
- `payload.product_id` zit in de set producten met user-access (productAccessFilter)
**Voorbeeld:**
```js
const source = new EventSource('/api/realtime/notifications', { withCredentials: true })
```
---
## Cron — Expire questions
### `POST /api/cron/expire-questions`
Vercel cron handler die dagelijks draait. Markeert verlopen open vragen als `expired` en verlopen pending login_pairings als `cancelled`.
**Auth:** `Authorization: Bearer ${CRON_SECRET}` — header die Vercel automatisch injecteert wanneer de env-var op de project-omgeving staat. Zonder secret of bij mismatch: 401.
**Schedule:** `0 4 * * *` (dagelijks om 04:00 UTC; Vercel Hobby-plan staat alleen daily crons toe — Pro ondersteunt fijnmazigere schedules).
**Response 200:**
```json
{
"expired_questions": 0,
"expired_pairings": 0,
"ran_at": "2026-04-28T00:00:00.000Z"
}
```
**Voorbeeld (handmatige trigger):**
```bash
curl -X POST -H "Authorization: Bearer $CRON_SECRET" \
https://your-app.vercel.app/api/cron/expire-questions
```
---
## Cron — Cleanup agent artifacts
### `POST /api/cron/cleanup-agent-artifacts`
Vercel cron handler die dagelijks draait. Verwijdert `FAILED` en `CANCELLED` claude_jobs waarvan `finished_at` ouder is dan 7 dagen. Hard-delete — geen historische waarde; audit-trail zit in git-commits.
**Auth:** `Authorization: Bearer ${CRON_SECRET}` — zelfde mechanisme als `/api/cron/expire-questions`. Zonder secret of bij mismatch: 401.
**Schedule:** `0 3 * * *` (dagelijks om 03:00 UTC).
**Response 200:**
```json
{
"deleted": 3,
"ran_at": "2026-05-01T03:00:00.000Z"
}
```
**Voorbeeld (handmatige trigger):**
```bash
curl -X POST -H "Authorization: Bearer $CRON_SECRET" \
https://your-app.vercel.app/api/cron/cleanup-agent-artifacts
```
---
## Voorbeeldworkflow voor Claude Code
1. **Probe:** `GET /api/health?db=1` — bevestig dat de service en DB bereikbaar zijn.
2. **Context:** `GET /api/products/$ID/claude-context` — haal product, sprint, volgende story en todos op in één call.
3. **Plan vastleggen:** `POST /api/stories/$STORY_ID/log` met `type: IMPLEMENTATION_PLAN`.
4. **Per task:** `PATCH /api/tasks/$TASK_ID` met `status: "in_progress"`, daarna met `status: "done"` plus eventueel `implementation_plan`.
5. **Test:** `POST /api/stories/$STORY_ID/log` met `type: TEST_RESULT` en `status: PASSED|FAILED`.
6. **Commit:** `POST /api/stories/$STORY_ID/log` met `type: COMMIT`, `commit_hash`, `commit_message`, optioneel `metadata: { branch }`.

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,216 @@
---
title: "Authentication, Sessions & Demo Policy"
status: active
audience: [maintainer, contributor]
language: nl
last_updated: 2026-05-03
related: [qr-pairing.md](./qr-pairing.md)
---
## Authenticatieflow
```
Registratie:
POST /register → valideer username/wachtwoord → bcrypt hash → opslaan in DB
→ iron-session cookie zetten → redirect /dashboard
Inloggen:
POST /login → gebruiker ophalen op username → bcrypt vergelijken
→ bij match: iron-session cookie zetten → redirect /dashboard
→ bij mismatch: generieke foutmelding (geen onderscheid)
Sessie per request:
proxy.ts → sessiecookie-aanwezigheid controleren
→ beschermde routes: redirect /login als geen sessiecookie aanwezig is
→ app layout valideert de volledige sessie server-side
API-aanroepen (Claude Code):
Authorization: Bearer <token> header → SHA-256 hash → opzoeken in api_tokens
→ revoked_at null check → user_id ophalen → is_demo check voor schrijfrechten
Uitloggen:
Server Action → iron-session vernietigen → redirect /login
```
---
## Demo-user policy (ST-1110)
Demo-gebruikers (`is_demo = true` in de database, `isDemo: true` in de iron-session) hebben volledig read-only toegang. Bescherming is drielaags:
### Laag 1 — Middleware-guard (proxy.ts)
`proxy.ts` blokkeert alle non-GET requests op `/api/*` voor demo-gebruikers voordat de route handler draait (defense in depth). Implementatie gebruikt `unsealData` direct (geen `getIronSession`) omdat `request.cookies` in middleware `RequestCookies` is, niet de volledige `CookieStore`.
```ts
// Whitelist: paden die demo mag aanroepen ondanks non-GET
const DEMO_WRITE_ALLOWLIST = [
'/api/cron/', // machine-auth, irrelevant voor demo
]
// pair/start en pair/claim staan NIET in de allowlist — zie Laag 2
```
### Laag 2 — Per-route guards (Server Actions & Route Handlers)
Elke schrijfactie controleert `session.isDemo` vóór DB-toegang:
```ts
if (session.isDemo) return { error: 'Niet beschikbaar in demo-modus' }
```
**QR-pairing (M10):**
- `pair/start`: isDemo-check via `getIronSession(await cookies(), sessionOptions)` — blokkeert demo-desktops
- `pair/claim`: check `pairing.user?.is_demo` na DB-read — blokkeert demo-users die op mobiel hebben goedgekeurd
- `pair/approve` en `pair/cancel`: waren al geblokkeerd vóór ST-1110
**Realtime SSE en cron-routes:** niet relevant voor demo-bescherming (SSE is read-only, cron gebruikt Bearer-auth).
### Laag 3 — UI-laag (DemoTooltip)
Alle write-knoppen zijn `disabled` met een `DemoTooltip show={isDemo}` wrapper zodat demo-bezoekers de app-mogelijkheden kunnen zien. Consistente component: `components/shared/demo-tooltip.tsx`.
Patroon:
```tsx
<DemoTooltip show={isDemo}>
<Button disabled={isDemo} onClick={() => !isDemo && handleAction()}>
Actie
</Button>
</DemoTooltip>
```
**Let op:** drag-and-drop handles (`⠿`) blijven verborgen voor demo (`{!isDemo && <span {...listeners} />}`) — dragging is geen UI-showcase maar zou nep-optimistische updates triggeren.
---
## Claude job queue (M13 — ST-1111)
Developers kunnen vanuit de Task Detail Dialog een lokale Claude Code-sessie inschakelen. De job queue zorgt voor coördinatie en realtime-status.
### State machine
```
QUEUED → CLAIMED (snapshot capture) → RUNNING → DONE
→ FAILED
→ CANCELLED (door user)
CLAIMED → QUEUED (stale claim cleanup, >30min; snapshot gewist)
QUEUED → CLAIMED (re-claim na stale reset; snapshot refreshed)
```
**Snapshot-rationale:** bij atomic claim schrijft `wait_for_job` de dan-actuele `task.implementation_plan` naar `claude_jobs.plan_snapshot`. Dit veld blijft bevroren terwijl de job loopt — ook als een gebruiker `update_task_plan` aanroept. Zo kan een toekomstige verify-tool drift detecteren tussen de baseline (snapshot) en de actuele plan. Jobs zonder snapshot (NULL) zijn aangemaakt vóór deze feature en worden als "no baseline" gemarkeerd.
### ClaudeJob model
```
claude_jobs
id, user_id, product_id, task_id
status: ClaudeJobStatus (QUEUED|CLAIMED|RUNNING|DONE|FAILED|CANCELLED)
claimed_by_token_id (FK → api_tokens, nullable)
claimed_at, started_at, finished_at
plan_snapshot: String? — bevroren snapshot van task.implementation_plan bij claim
branch, pushed_at, summary, error
verify_result: VerifyResult? (ALIGNED|PARTIAL|EMPTY|DIVERGENT)
@@index([user_id, status])
@@index([task_id, status])
@@index([status, claimed_at]) — voor stale-claim cleanup
```
**VerifyResult enum** — vergelijking van de git-diff in de worktree versus `plan_snapshot`:
| Waarde | Betekenis |
|---|---|
| `ALIGNED` | Diff dekt het plan volledig — implementatie klopt met de intentie |
| `PARTIAL` | Diff dekt slechts een deel van het plan — waarschuwing, maar geen blocker |
| `EMPTY` | Geen codewijzigingen in de diff — blocker, tenzij de task `verify_only=true` heeft |
| `DIVERGENT` | Diff bevat significant meer dan het plan — review extra zorgvuldig |
**`verify_only` op Task** — wanneer `true` mag de agent de task als DONE markeren ook als de diff leeg is. Bedoeld voor taken die expliciet om verificatie (niet implementatie) vragen.
**`pushed_at`** — timestamp waarop de agent de feature-branch naar origin heeft gepusht. Aanwezig zodra de push slaagde; absent als er geen wijzigingen waren of de push mislukte.
### NOTIFY/LISTEN flow
```
UI klikt 'Voer uit'
→ enqueueClaudeJobAction() Server Action
→ prisma.claudeJob.create(QUEUED)
→ prisma.$executeRaw pg_notify('scrum4me_changes', {type:'claude_job_enqueued',...})
→ /api/realtime/solo SSE server-side filter: user_id + product_id
→ EventSource.onmessage browser: handleJobEvent()
→ useSoloStore.claudeJobsByTaskId map
→ SoloTaskCard pill + dialog-footer update
```
### Idempotency
`enqueueClaudeJobAction` weigert een tweede enqueue als er al een job bestaat met `status IN (QUEUED, CLAIMED, RUNNING)`. Teruggestuurde fout bevat het bestaande `jobId` zodat de UI ernaar kan linken.
### Auto-promote task-status op job-overgangen
Twee Postgres-triggers houden `task.status` in sync met `claude_job.status` zodat de Solo-kaart altijd in de juiste kolom staat:
- **`claude_job_claim_to_task`** (`prisma/migrations/20260501130000_promote_task_to_in_progress_on_claim`): bij INSERT met status `CLAIMED|RUNNING` of UPDATE OF status naar `CLAIMED|RUNNING`, promoot de bijbehorende task van `TO_DO` naar `IN_PROGRESS`. Forceert niet vanuit andere status — handmatige overrides (REVIEW, DONE) blijven staan.
- **`claude_job_status_to_task`** (`prisma/migrations/20260501110000_sync_task_status_from_claude_job`): bij DONE zet de task ook op `DONE`. Idempotent: skip wanneer task al DONE is.
De bestaande `notify_task_change`-trigger op `tasks` vuurt automatisch de pg_notify naar `/api/realtime/solo` zodat de UI direct synct — geen extra plumbing in de SSE-handler nodig.
### Hybride-ready
De huidige implementatie verwacht een lokale Claude Code-sessie die `wait_for_job` aanroept vanuit `madhura68/scrum4me-mcp`. Toekomstige uitbreiding naar Vercel Sandbox (serverless agent) vereist alleen een nieuw claim-endpoint — het datamodel en SSE-flow zijn ongewijzigd.
## Environment variables
| Variabele | Doel | Waar te vinden |
|---|---|---|
| `DATABASE_URL` | Prisma database-verbinding | Neon dashboard → Connection string (pooled) |
| `DIRECT_URL` | Directe verbinding voor migraties én voor de LISTEN/NOTIFY-verbinding van het Solo Paneel realtime-endpoint | Neon dashboard → Connection string (unpooled) |
| `SESSION_SECRET` | Versleutelingssleutel voor iron-session | Genereer met `openssl rand -base64 32` |
| `NODE_ENV` | Omgevingsmodus | Automatisch gezet door Vercel / Node |
`.env.example`:
```bash
# Database
DATABASE_URL="postgresql://user:password@host/dbname?sslmode=require"
DIRECT_URL="postgresql://user:password@host/dbname?sslmode=require"
# Sessie
SESSION_SECRET="vervang-dit-met-openssl-rand-base64-32-output"
# Optioneel
NODE_ENV="development"
```
---
## Deployment
**Hosting:** Vercel (Hobby — gratis voor v1)
**CI/CD:** GitHub Actions → lint + typecheck + `prisma validate` op elke PR; Vercel deploy automatisch bij merge naar `main`
**Database (cloud):** Neon — migraties via `prisma migrate deploy` in de Vercel build-stap
**Database (lokaal):** Neon (gratis tier) — `npx prisma db push` synchroniseert schema
**Prisma generatie:** CI/deployment gebruikt `prisma generate --generator client`; `npm run db:erd` is alleen lokaal en bouwt ook `docs/assets/erd.svg`
**Seeding:** `npx prisma db seed` laadt de testdata uit het Product Backlog document
### Deployment checklist (pre-launch)
- [ ] `DATABASE_URL` en `DIRECT_URL` gezet in Vercel dashboard (Neon connection strings)
- [ ] `SESSION_SECRET` gezet in Vercel dashboard (min. 32 tekens)
- [ ] `prisma migrate deploy` uitgevoerd op productiedatabase
- [ ] Demo-gebruiker aangemaakt via seed of handmatig
- [ ] API-token aangemaakt en getest met `curl`-aanroep naar `/api/products`
- [ ] Vercel Analytics actief in het Vercel dashboard na eerste productiebezoek
- [ ] Vercel preview-deployments getest op een PR
- [ ] `next build` lokaal geslaagd zonder TypeScript-fouten
---
## Kostenscattting
| Service | Plan | Maandelijkse kosten |
|---|---|---|
| Vercel | Hobby | Gratis |
| Neon | Free tier (0.5 GB, 190 compute-uren) | Gratis |
| GitHub | Free | Gratis |
| Domein | Eigen domein (optioneel) | ~€12/maand |
| **Totaal** | | **€02/maand** |
> Bij groei naar meerdere gebruikers (v2): Neon Launch plan (~$19/maand) en Vercel Pro (~$20/maand) zijn de eerste stappen omhoog.

View file

@ -0,0 +1,79 @@
---
title: "Claude ↔ User Question Channel"
status: active
audience: [maintainer, contributor]
language: nl
last_updated: 2026-05-03
related: [project-structure.md](./project-structure.md)
---
## Vraag-antwoord-kanaal Claude ↔ user (M11)
Persistent kanaal tussen Claude Code (via MCP) en de actieve Scrum4Me-gebruiker.
Wanneer Claude tijdens een implementatie vastloopt op een keuze, schrijft hij een
gestructureerde vraag naar `claude_questions`. Een Postgres-trigger emit op het
**bestaande** `scrum4me_changes`-kanaal (hergebruik uit M8) met `entity: 'question'`.
De Scrum4Me-app heeft een aparte user-scoped SSE-route die op dit kanaal abonneert,
filter't op product-toegang en de notifications-bell in de NavBar voedt. Iedere
gebruiker met product-membership kan antwoorden; story-assignee krijgt visuele
emphase. Claude leest het antwoord (sync via polling met `wait_seconds`, of in
een latere sessie via `get_question_answer`) en gaat door.
### Sequence
```mermaid
sequenceDiagram
participant C as Claude (MCP)
participant DB as Postgres
participant SC as scrum4me_changes channel
participant SSE as /api/realtime/notifications
participant U as Scrum4Me UI (browser)
C->>DB: INSERT claude_questions (status=open)
DB->>SC: pg_notify {entity:'question', op:'I', id, ...}
SC->>SSE: notification (filter: question + product-access)
SSE->>U: data event → Zustand store upsert → bell badge
Note over U: Gebruiker klikt bell → Sheet → Modal
U->>DB: answerQuestion(questionId, answer)<br/>Server Action: atomic updateMany WHERE status='open'
DB->>SC: pg_notify {entity:'question', op:'U', status:'answered'}
SC->>SSE: notification
SSE->>U: data event → store remove → bell badge -1
Note over C: Optioneel: ask_user_question(wait_seconds) polt elke 2s
C->>DB: SELECT status FROM claude_questions WHERE id=...
DB-->>C: status='answered', answer='...'
C->>C: gaat door met implementatie
```
### Threat-model
| Aanval | Mitigatie |
|---|---|
| **Race**: dubbele submit op zelfde vraag | Atomic `updateMany WHERE status='open'` — één caller ziet count=1, rest count=0 met disambiguatie via second findFirst |
| **Demo-account misbruik** | `requireWriteAccess` op MCP-write-tools (PERMISSION_DENIED), early-return op `session.isDemo` in answerQuestion Server Action, disabled submit + tooltip in AnswerModal |
| **Cross-product leak** | `productAccessFilter` op DB-query én SSE-server-side-filter (Set met user's accessible product-IDs) |
| **Cron-endpoint misbruik** | `Authorization: Bearer ${CRON_SECRET}` — Vercel injecteert automatisch; faalt 401 als secret niet gezet (geen open endpoint in dev) |
| **Onbeperkte vragen-groei** | `expires_at` 24 u + Vercel cron `0 4 * * *` (dagelijks; Hobby-plan-limiet) markeert `status='expired'` → uit notifications-bell |
| **Gevoelige info in logs** | Logging alleen `question_id`, nooit vraag- of antwoord-tekst |
### Waarom hergebruik scrum4me_changes-kanaal
In tegenstelling tot M10 (eigen `scrum4me_pairing`-kanaal) is M11 een uitbreiding van
de bestaande realtime-infra. Voordelen:
- Eén Postgres-NOTIFY-listener per route i.p.v. twee — minder DB-connecties
- Solo-realtime + notifications kunnen onafhankelijk evolueren via de `entity`-key
- Toekomstige entities (bijv. `entity: 'comment'`, `entity: 'mention'`) hoeven geen
nieuw kanaal — alleen een filter-aanpassing in de route die ze wil ontvangen
Risico: een nieuwe entity vergeten te filteren leidt tot lekkage. Mitigatie:
expliciet `if (payload.entity === 'X') return false` in elke SSE-route die
betrokken-features niet hoort te zien (zoals de solo-route die `entity:'question'`
weert).
Dit patroon (notification-channel via een bestaande pg_notify-stream) is
herbruikbaar — zie `docs/patterns/claude-question-channel.md`.
---

View file

@ -0,0 +1,460 @@
---
title: "Data Model & Prisma Schema"
status: active
audience: [maintainer, contributor]
language: nl
last_updated: 2026-05-03
related: [auth-and-sessions.md](./auth-and-sessions.md)
---
## Datamodel
### `users`
| Kolom | Type | Constraints | Noten |
|---|---|---|---|
| id | String (cuid) | PK | Gegenereerd door Prisma |
| username | String | unique, not null, min 3 | Inlognaam |
| password_hash | String | not null | bcrypt hash (cost factor 12) |
| is_demo | Boolean | default false | Demo-gebruiker heeft read-only rechten |
| bio | String | nullable, max 160 | Korte profielomschrijving |
| bio_detail | String | nullable, max 2000 | Uitgebreide profielbeschrijving |
| avatar_data | Bytes | nullable | Profielfoto als WebP bytea (max 700×700) |
| created_at | DateTime | default now() | |
| updated_at | DateTime | auto-update | Gebruikt als cache-buster voor avatar-URL |
**Indexes:** `username` (unique lookup bij inloggen)
---
### `user_roles`
| Kolom | Type | Constraints | Noten |
|---|---|---|---|
| id | String (cuid) | PK | |
| user_id | String | FK → users, not null | |
| role | Enum | PRODUCT_OWNER \| SCRUM_MASTER \| DEVELOPER | |
**Indexes:** `(user_id)` — meerdere rollen per gebruiker
**Constraint:** unique `(user_id, role)`
---
### `api_tokens`
| Kolom | Type | Constraints | Noten |
|---|---|---|---|
| id | String (cuid) | PK | |
| user_id | String | FK → users, not null | |
| token_hash | String | not null | SHA-256 hash van het token |
| label | String | nullable | Bijv. "Claude Code — laptop" |
| created_at | DateTime | default now() | |
| revoked_at | DateTime | nullable | Null = actief |
**Indexes:** `token_hash` (lookup bij elke API-aanroep — moet snel zijn)
---
### `products`
| Kolom | Type | Constraints | Noten |
|---|---|---|---|
| id | String (cuid) | PK | |
| user_id | String | FK → users, not null | |
| name | String | not null, max 200 | Uniek per gebruiker |
| description | String | nullable, max 1000 | |
| repo_url | String | nullable | Gevalideerde URL |
| definition_of_done | String | not null, max 500 | Vaste instelling per product |
| archived | Boolean | default false | |
| created_at | DateTime | default now() | |
| updated_at | DateTime | auto-update | |
**Indexes:** `(user_id, archived)` — standaard query filtert op actieve producten
**Constraint:** unique `(user_id, name)`
---
### `pbis`
| Kolom | Type | Constraints | Noten |
|---|---|---|---|
| id | String (cuid) | PK | |
| product_id | String | FK → products (cascade delete) | |
| code | String | nullable, max 30 | Auto-gegenereerd of handmatig |
| title | String | not null, max 200 | |
| description | String | nullable, max 2000 | |
| priority | Int | 14, not null | 1 = Kritiek, 4 = Laag |
| sort_order | Float | not null | Float voor volgorde tussen items zonder renummering |
| status | Enum | READY \| BLOCKED \| DONE, default READY | Auto-promotie naar DONE bij sprint-close (zie hieronder) |
| created_at | DateTime | default now() | |
| updated_at | DateTime | auto-update | |
**Indexes:** `(product_id, priority, sort_order)` — standaard query voor het gesplitste scherm; `(product_id, status)` — voor het statusfilter op de Product Backlog
**Cascade-regel (sprint-close):** wanneer een Sprint wordt afgerond via `completeSprintAction` en alle stories van een PBI eindigen op DONE (na toepassing van de afsluitbeslissingen), zet diezelfde transactie de PBI-status op DONE. Promotie alléén — een PBI op DONE wordt nooit automatisch teruggezet. Stories die niet in deze Sprint zaten worden meegerekend op hun huidige DB-status. Een PBI zonder stories blijft READY.
---
### `stories`
| Kolom | Type | Constraints | Noten |
|---|---|---|---|
| id | String (cuid) | PK | |
| pbi_id | String | FK → pbis (cascade delete) | |
| product_id | String | FK → products | Denormalisatie voor snellere queries |
| sprint_id | String | FK → sprints, nullable | Null = in Product Backlog |
| title | String | not null, max 200 | |
| description | String | nullable, max 2000 | |
| acceptance_criteria | String | nullable, max 2000 | |
| priority | Int | 14, not null | |
| sort_order | Float | not null | |
| status | Enum | OPEN \| IN_SPRINT \| DONE | |
| created_at | DateTime | default now() | |
| updated_at | DateTime | auto-update | |
**Indexes:** `(pbi_id, priority, sort_order)`, `(sprint_id, sort_order)`, `(product_id, status)`
**Auto-promotie/demotie via task-status:** zodra alle tasks van een story op `DONE` staan en de huidige story-status nog niet `DONE` is, promoot dezelfde transactie de story naar `DONE`. Wordt een task van een `DONE`-story uit `DONE` getrokken (heropening), dan demoot de story terug naar `IN_SPRINT` — niet naar `OPEN`, want `OPEN` betekent "terug in productbacklog" en is een sprint-management-actie. De logica zit in [lib/tasks-status-update.ts](../../lib/tasks-status-update.ts) en wordt aangeroepen door alle drie de task-status-write-paden (`updateTaskStatusAction`, `saveTask` edit-mode, REST `PATCH /api/tasks/[id]`).
---
### `story_logs`
| Kolom | Type | Constraints | Noten |
|---|---|---|---|
| id | String (cuid) | PK | |
| story_id | String | FK → stories (cascade delete) | |
| type | Enum | IMPLEMENTATION_PLAN \| TEST_RESULT \| COMMIT | |
| content | String | not null | Tekst van plan of testuitvoer |
| status | Enum | PASSED \| FAILED, nullable | Alleen bij type TEST_RESULT |
| commit_hash | String | nullable | Alleen bij type COMMIT |
| commit_message | String | nullable | Alleen bij type COMMIT |
| created_at | DateTime | default now() | |
**Indexes:** `(story_id, created_at)` — chronologische weergave in de UI
---
### `sprints`
| Kolom | Type | Constraints | Noten |
|---|---|---|---|
| id | String (cuid) | PK | |
| product_id | String | FK → products (cascade delete) | |
| sprint_goal | String | not null, max 500 | |
| status | Enum | ACTIVE \| COMPLETED | |
| created_at | DateTime | default now() | |
| completed_at | DateTime | nullable | |
**Indexes:** `(product_id, status)` — query voor actieve Sprint per product
**Constraint:** Max. 1 actieve Sprint per product (gehandhaafd in applicatielaag)
---
### `tasks`
| Kolom | Type | Constraints | Noten |
|---|---|---|---|
| id | String (cuid) | PK | |
| story_id | String | FK → stories (cascade delete) | |
| sprint_id | String | FK → sprints, nullable | Denormalisatie voor snellere queries |
| title | String | not null, max 200 | |
| description | String | nullable, max 1000 | |
| implementation_plan | String | nullable | Opgeslagen door Claude Code MCP via `PATCH /api/tasks/:id` |
| priority | Int | 14, not null | |
| sort_order | Float | not null | |
| status | Enum | TO_DO \| IN_PROGRESS \| REVIEW \| DONE | |
| created_at | DateTime | default now() | |
| updated_at | DateTime | auto-update | |
**Indexes:** `(story_id, priority, sort_order)`, `(sprint_id, status)`
---
### `todos`
| Kolom | Type | Constraints | Noten |
|---|---|---|---|
| id | String (cuid) | PK | |
| user_id | String | FK → users, not null | |
| product_id | String? | FK → products, nullable | Optioneel in UI; SetNull bij verwijderen product |
| title | String | not null | |
| done | Boolean | default false | |
| archived | Boolean | default false | |
| created_at | DateTime | default now() | |
| updated_at | DateTime | auto-update | |
**Indexes:** `(user_id, done, archived)` — standaard weergave filtert op actieve todo's; `(user_id, product_id)` — filteren per product
---
### `product_members`
| Kolom | Type | Constraints | Noten |
|---|---|---|---|
| id | String (cuid) | PK | |
| product_id | String | FK → products (cascade delete) | |
| user_id | String | FK → users (cascade delete) | |
| created_at | DateTime | default now() | |
**Indexes:** `(user_id)` — opzoeken van producten waarbij een gebruiker lid is
**Constraint:** unique `(product_id, user_id)` — één lidmaatschap per gebruiker per product
Koppelt Developer-gebruikers aan een product backlog. De eigenaar (`products.user_id`) heeft altijd volledige toegang; via `product_members` kunnen aanvullende Developers leesrechten en schrijfrechten op stories, taken en sprints van dat product krijgen. Rollen worden niet opgeslagen in deze tabel — dat doet `user_roles`. Een gebruiker kan alleen worden toegevoegd als hij/zij de rol `DEVELOPER` heeft.
---
## Toegangsmodel en schrijfbeveiliging
Producttoegang is centraal gedefinieerd als:
- eigenaar: `products.user_id === gebruiker.id`
- teamlid: `product_members` bevat `(product_id, user_id)`
Code gebruikt hiervoor `productAccessFilter(userId)` uit `lib/product-access.ts`. Route Handlers en Server Actions mogen geen eigenaar-only filter (`user_id`) gebruiken voor product-scoped resources tenzij het expliciet om eigenaarsbeheer gaat, zoals archiveren of teamleden beheren.
Schrijfoperaties volgen deze invarianten:
- Controleer eerst authenticatie en `session.isDemo`.
- Valideer input met Zod, maar behandel TypeScript types niet als runtime-beveiliging.
- Controleer de parent-resource met `productAccessFilter`.
- Vertrouw bulk-ID's nooit los: haal de records eerst op met `id in (...)` plus de parent-scope (`product_id`, `pbi_id`, `sprint_id` of `story_id`) en weiger de operatie als aantallen niet exact overeenkomen.
- Weiger dubbele IDs in reorder- en beslissingslijsten.
- Leid denormalized foreign keys af van de database-parent (`pbi.product_id`, `sprint.product_id`) en niet van form-data of JSON body.
- Delete of update alleen nadat de resource scoped is gevonden; gebruik scoped `deleteMany`/`updateMany` wanneer een unique `delete` anders onveilig zou zijn.
---
## Prisma Schema (excerpt)
```prisma
// prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
// Database wordt bepaald via prisma.config.ts — niet hier
enum Role {
PRODUCT_OWNER
SCRUM_MASTER
DEVELOPER
}
enum StoryStatus {
OPEN
IN_SPRINT
DONE
}
enum PbiStatus {
READY
BLOCKED
DONE
}
enum TaskStatus {
TO_DO
IN_PROGRESS
REVIEW
DONE
}
enum LogType {
IMPLEMENTATION_PLAN
TEST_RESULT
COMMIT
}
enum TestStatus {
PASSED
FAILED
}
enum SprintStatus {
ACTIVE
COMPLETED
}
model User {
id String @id @default(cuid())
username String @unique
password_hash String
is_demo Boolean @default(false)
bio String? @db.VarChar(160)
bio_detail String? @db.VarChar(2000)
avatar_data Bytes?
created_at DateTime @default(now())
updated_at DateTime @updatedAt
roles UserRole[]
api_tokens ApiToken[]
products Product[]
todos Todo[]
product_members ProductMember[]
}
model UserRole {
id String @id @default(cuid())
user User @relation(fields: [user_id], references: [id], onDelete: Cascade)
user_id String
role Role
@@unique([user_id, role])
}
model ApiToken {
id String @id @default(cuid())
user User @relation(fields: [user_id], references: [id], onDelete: Cascade)
user_id String
token_hash String @unique
label String?
created_at DateTime @default(now())
revoked_at DateTime?
@@index([token_hash])
}
model Product {
id String @id @default(cuid())
user User @relation(fields: [user_id], references: [id], onDelete: Cascade)
user_id String
name String
description String?
repo_url String?
definition_of_done String
archived Boolean @default(false)
created_at DateTime @default(now())
updated_at DateTime @updatedAt
pbis Pbi[]
sprints Sprint[]
stories Story[]
todos Todo[]
members ProductMember[]
@@unique([user_id, name])
@@index([user_id, archived])
}
model Pbi {
id String @id @default(cuid())
product Product @relation(fields: [product_id], references: [id], onDelete: Cascade)
product_id String
code String? @db.VarChar(30)
title String
description String?
priority Int
sort_order Float
status PbiStatus @default(READY)
created_at DateTime @default(now())
updated_at DateTime @updatedAt
stories Story[]
@@unique([product_id, code])
@@index([product_id, priority, sort_order])
@@index([product_id, status])
}
model Story {
id String @id @default(cuid())
pbi Pbi @relation(fields: [pbi_id], references: [id], onDelete: Cascade)
pbi_id String
product Product @relation(fields: [product_id], references: [id])
product_id String
sprint Sprint? @relation(fields: [sprint_id], references: [id])
sprint_id String?
title String
description String?
acceptance_criteria String?
priority Int
sort_order Float
status StoryStatus @default(OPEN)
created_at DateTime @default(now())
updated_at DateTime @updatedAt
logs StoryLog[]
tasks Task[]
@@index([pbi_id, priority, sort_order])
@@index([sprint_id, sort_order])
@@index([product_id, status])
}
model StoryLog {
id String @id @default(cuid())
story Story @relation(fields: [story_id], references: [id], onDelete: Cascade)
story_id String
type LogType
content String
status TestStatus?
commit_hash String?
commit_message String?
created_at DateTime @default(now())
@@index([story_id, created_at])
}
model Sprint {
id String @id @default(cuid())
product Product @relation(fields: [product_id], references: [id], onDelete: Cascade)
product_id String
sprint_goal String
status SprintStatus @default(ACTIVE)
created_at DateTime @default(now())
completed_at DateTime?
stories Story[]
tasks Task[]
@@index([product_id, status])
}
model Task {
id String @id @default(cuid())
story Story @relation(fields: [story_id], references: [id], onDelete: Cascade)
story_id String
sprint Sprint? @relation(fields: [sprint_id], references: [id])
sprint_id String?
title String
description String?
implementation_plan String?
priority Int
sort_order Float
status TaskStatus @default(TO_DO)
created_at DateTime @default(now())
updated_at DateTime @updatedAt
@@index([story_id, priority, sort_order])
@@index([sprint_id, status])
}
model Todo {
id String @id @default(cuid())
user User @relation(fields: [user_id], references: [id], onDelete: Cascade)
user_id String
product Product? @relation(fields: [product_id], references: [id], onDelete: SetNull)
product_id String?
title String
done Boolean @default(false)
archived Boolean @default(false)
created_at DateTime @default(now())
updated_at DateTime @updatedAt
@@index([user_id, done, archived])
@@index([user_id, product_id])
}
model ProductMember {
id String @id @default(cuid())
product Product @relation(fields: [product_id], references: [id], onDelete: Cascade)
product_id String
user User @relation(fields: [user_id], references: [id], onDelete: Cascade)
user_id String
created_at DateTime @default(now())
@@unique([product_id, user_id])
@@index([user_id])
@@map("product_members")
}
```
---

View file

@ -0,0 +1,59 @@
---
title: "Scrum4Me — Architecture Overview"
status: active
audience: [maintainer, contributor]
language: nl
last_updated: 2026-05-03
related: [data-model.md](./data-model.md), [project-structure.md](./project-structure.md)
---
**Versie:** 0.1 — april 2026
**Volgt op:** Functionele Specificatie v0.2
---
## Architectuursamenvatting
Scrum4Me is een desktop-first Next.js 16 webapplicatie die server-side wordt gerenderd en gedeployed op Vercel. De database is PostgreSQL via Neon, aangestuurd via Prisma v7. Authenticatie is custom username/password via iron-session — geen externe auth-provider, geen e-mail. De REST API voor Claude Code-integratie loopt via Next.js Route Handlers, beveiligd met API-tokens. Drag-and-drop in de planningsschermen wordt afgehandeld door dnd-kit. Vercel Analytics meet pageviews via de root layout; profielfoto's worden server-side verwerkt met Sharp.
---
## Stack
| Laag | Technologie | Rationale |
|---|---|---|
| Frontend framework | Next.js 16 (App Router) | Stabiel, wijdverbreid, naadloze Vercel-deployment; SSR vereist voor auth-cookie-management |
| UI runtime | React 19 | Standaard bij Next.js 16; brengt `useActionState`, `useFormStatus` en de React Compiler (experimenteel) mee — minder boilerplate bij Server Actions |
| Taal | TypeScript (strict) | Type-veiligheid is essentieel voor een solo developer zonder reviewlaag; vangt datamodel-mismatches vroeg |
| Client state | Zustand | Minimale boilerplate voor ephemere UI-staat (selectie, optimistische drag-and-drop volgorde); leeft naast Server Components zonder conflict |
| Styling | Tailwind CSS + shadcn/ui | Snelle iteratie; toegankelijke componentprimitieven; desktop-first layouts goed ondersteund |
| Database (cloud) | PostgreSQL via Neon | Serverless Postgres, gratis tier voldoende voor MVP; native PostgreSQL zonder vendor lock-in |
| ORM | Prisma v7 | Type-safe queries; PostgreSQL via adapter; migraties zijn deterministisch |
| Authenticatie | Custom — iron-session + bcrypt | Username/password zonder e-mail vereist geen externe auth-provider; iron-session beheert versleutelde cookies server-side |
| Drag-and-drop | dnd-kit | Actief onderhouden, React-native hooks, 60fps bij grote lijsten, ondersteuning voor meerdere containers |
| REST API | Next.js Route Handlers (`/app/api/`) | Naast Server Actions nodig voor Claude Code-integratie; Route Handlers zijn volledig HTTP-compatibel |
| Image processing | Sharp | Avataruploads worden gevalideerd, geschaald en als WebP opgeslagen in PostgreSQL |
| Analytics | Vercel Analytics (`@vercel/analytics/next`) | Pageviews zonder extra client-configuratie; component staat in `app/layout.tsx` |
| Hosting | Vercel | Zero-config Next.js deployment; preview-URLs per PR; gratis tier voldoende voor v1 |
| CI/CD | GitHub Actions | Lint + typecheck + build op elke PR; Vercel handelt de daadwerkelijke deploy af |
---
## Wat we NIET gebruiken (en waarom)
| Technologie | Afgewezen omdat |
|---|---|
| Supabase Auth | Username/password zonder e-mail past niet in Supabase Auth's flow; onnodige afhankelijkheid voor wat iron-session zelf afhandelt |
| NextAuth / Auth.js | Overkill voor username/password zonder providers; voegt complexiteit toe zonder voordeel bij deze auth-vereisten |
| Redux Toolkit | Te veel boilerplate (actions, reducers, slices, selectors, provider) voor deze schaal; Zustand doet hetzelfde met een kwart van de code |
| Jotai / Recoil | Atom-gebaseerd model is te granulaar voor de gecorreleerde state in de gesplitste schermen; Zustand stores zijn explicieter en beter uitbreidbaar |
| React Query / SWR | Server Components + Server Actions dekken de datalaag; client-side server-state caching introduceert een sync-probleem dat we bewust vermijden |
| Context API (React) | Veroorzaakt onnodige re-renders bij drag-and-drop updates; Zustand's selector-gebaseerde subscriptions zijn granulairder |
| WebSockets / real-time | Geen real-time vereisten in v1; polling of page-refresh volstaat |
| Redis | Geen caching- of queuerequirements op deze schaal |
| Docker (lokale dev) | Neon gratis tier volstaat voor lokale ontwikkeling; Docker voegt geen waarde toe |
| Supabase (als database) | Neon geeft directe PostgreSQL-toegang zonder Supabase-specifieke abstractielagen; past beter bij Prisma-first aanpak |
| tRPC | REST API is vereist voor Claude Code-integratie; tRPC werkt alleen vanuit TypeScript-clients |
---

View file

@ -0,0 +1,397 @@
---
title: "Project Structure, Stores, Realtime & Job Queue"
status: active
audience: [maintainer, contributor]
language: nl
last_updated: 2026-05-03
related: [data-model.md](./data-model.md)
---
## Projectstructuur
```
scrum4me/
├── app/
│ ├── (auth)/
│ │ ├── login/page.tsx
│ │ └── register/page.tsx
│ ├── (app)/ # Beschermde routes
│ │ ├── layout.tsx # Auth-check + navigatie
│ │ ├── dashboard/page.tsx # Productenlijst
│ │ ├── products/
│ │ │ ├── new/page.tsx
│ │ │ └── [id]/
│ │ │ ├── layout.tsx # Zet actief product in Zustand store
│ │ │ ├── page.tsx # Product Backlog (gesplitst scherm)
│ │ │ ├── solo/page.tsx # Solo board (Kanban per ingelogde gebruiker)
│ │ │ ├── sprint/
│ │ │ │ ├── page.tsx # Sprint Backlog (drie-paneel scherm)
│ │ │ │ └── planning/page.tsx # Redirect → /sprint
│ │ ├── todos/page.tsx
│ │ └── settings/
│ │ ├── page.tsx # Profiel, account, PB-overzicht, rollen, tokens
│ │ └── tokens/page.tsx
│ ├── api/ # REST API voor Claude Code
│ │ ├── products/
│ │ │ └── [id]/
│ │ │ └── next-story/route.ts
│ │ ├── profile/
│ │ │ └── avatar/route.ts # POST upload + GET serve profielfoto
│ │ ├── sprints/
│ │ │ └── [id]/
│ │ │ └── tasks/route.ts
│ │ ├── stories/
│ │ │ └── [id]/
│ │ │ ├── log/route.ts
│ │ │ └── tasks/reorder/route.ts
│ │ ├── tasks/
│ │ │ └── [id]/route.ts
│ │ └── todos/route.ts
├── components/
│ ├── ui/ # shadcn/ui primitieven
│ ├── split-pane/ # Gesplitst scherm component
│ ├── backlog/ # PBI- en story-componenten
│ ├── sprint/ # Sprint-componenten
│ ├── products/ # ProductForm, TeamManager, ArchiveProductButton
│ ├── settings/ # RoleManager, ProfileEditor, LeaveProductButton
│ └── dnd/ # dnd-kit wrappers
├── lib/
│ ├── prisma.ts # Prisma Client singleton
│ ├── session.ts # iron-session configuratie
│ ├── auth.ts # login/register/token helpers
│ ├── api-auth.ts # Bearer token middleware voor API
│ ├── product-access.ts # productAccessFilter helper (eigenaar of teamlid)
│ └── env.ts # Zod-gevalideerde env vars
├── stores/ # Zustand stores
│ ├── backlog-store.ts # PBI/story/task hydration + applyChange (SSE)
│ ├── planner-store.ts # Optimistische drag-and-drop volgorde
│ ├── selection-store.ts # Geselecteerd PBI / story (cascade-reset)
│ ├── sprint-store.ts # Sprint Backlog taakvolgordes
│ ├── solo-store.ts # Solo board optimistische taakstatus
│ └── product-store.ts # Actief product (naam + id) voor navbar
├── prisma/
│ ├── schema.prisma
│ ├── migrations/
│ └── seed.ts # Testdata uit Product Backlog document
├── proxy.ts # Next.js 16 proxy voor route protection
├── prisma.config.ts # Prisma v7 config (DATABASE_URL)
└── .env.example
```
---
## Sleutelarchitectuurbeslissingen
### Beslissing: iron-session in plaats van Auth.js / Supabase Auth
**Keuze:** iron-session voor versleutelde server-side sessiecookies
**Rationale:** Scrum4Me gebruikt username/wachtwoord zonder e-mail — een flow die Auth.js/NextAuth met Credentials Provider ondersteunt, maar met onnodige complexiteit (JWT-callbacks, adapter-configuratie). iron-session is minimaal: sla een gesigneerde, versleutelde cookie op met `{ userId, isDemo }` en klaar. Geen externe afhankelijkheid, geen database-adapter voor sessies.
**Trade-off:** Geen ingebouwde OAuth of magic links. Dat is bewust — v1 heeft die niet nodig.
### Beslissing: Route Handlers naast Server Actions
**Keuze:** Server Actions voor UI-mutaties; Route Handlers voor de Claude Code REST API
**Rationale:** Server Actions zijn ideaal voor form-submits en UI-interacties (CSRF-bescherming, progressive enhancement). Maar Claude Code heeft echte HTTP-endpoints nodig — Bearer token, JSON body, programmatisch aanroepbaar. Die twee aanpakken leven naast elkaar zonder conflict.
**Trade-off:** Duplicatie in validatie-logica. Opgelost door gedeelde service-functies in `lib/` die beide aanroepen.
### Beslissing: Float voor sort_order
**Keuze:** `Float` in plaats van `Int` voor volgorde van PBI's, stories en taken
**Rationale:** Bij drag-and-drop tussenvoeging kan de nieuwe positie worden berekend als het gemiddelde van de buurwaarden (bijv. `(1.0 + 2.0) / 2 = 1.5`). Hierdoor is nooit een herindexering van alle items nodig. Herindexering is alleen nodig als de float-precisie opraakt (in de praktijk na duizenden bewegingen).
**Trade-off:** Kleine kans op precisieverlies bij extreme fragmentatie. Opgelost door periodieke herindexering als de minimale afstand onder een drempelwaarde valt.
### Beslissing: Denormalisatie van `product_id` op `stories` en `sprint_id` op `tasks`
**Keuze:** `product_id` opslaan op zowel `pbis` als `stories`; `sprint_id` op zowel `stories` als `tasks`
**Rationale:** Veel queries in de gesplitste schermen filteren op product of Sprint zonder de volledige hiërarchie te doorlopen. Directe foreign keys voorkomen onnodige joins en N+1-risico's.
**Trade-off:** Redundante data vereist consistente updates. Gehandhaafd via Prisma-transacties in de service-laag.
### Beslissing: Zustand voor client-side state management
**Keuze:** Vijf Zustand-stores naast Server Components
**Rationale:** De gesplitste schermen met dnd-kit vereisen client-side staat die twee panelen tegelijk aanstuurt. `useState` per component leidt tot prop drilling; Context API veroorzaakt onnodige re-renders bij 60fps drag-events. Zustand's selector-gebaseerde subscriptions updaten alleen de componenten die de gewijzigde slice observeren. De gouden regel: Zustand beheert uitsluitend ephemere UI-staat — nooit server-data. Server-data blijft in Server Components en wordt opgehaald via Prisma.
**Trade-off:** Extra abstractielaag die geïnitialiseerd moet worden vanuit server-data. Opgelost via een patroon waarbij het Server Component de initiële ids doorgeeft aan een Client Component dat de store hydrateert.
---
## Zustand stores
### `usePlannerStore` — optimistische drag-and-drop volgorde
Beheert de lokale volgorde van PBI's, stories en taken tijdens en na drag-and-drop, voordat de server bevestigt. Houdt de UI vloeiend op 60fps ongeacht netwerklatency.
```ts
// stores/planner-store.ts
import { create } from 'zustand'
interface PlannerStore {
// Optimistische volgorde per container (id-arrays)
pbiOrder: Record<string, string[]> // productId → pbi-ids
storyOrder: Record<string, string[]> // pbiId → story-ids
taskOrder: Record<string, string[]> // storyId → taak-ids
// Initialiseren vanuit server-data (bij mount)
initPbis: (productId: string, ids: string[]) => void
initStories: (pbiId: string, ids: string[]) => void
initTasks: (storyId: string, ids: string[]) => void
// Optimistisch updaten (vóór server-bevestiging)
reorderPbis: (productId: string, newOrder: string[]) => void
reorderStories: (pbiId: string, newOrder: string[]) => void
reorderTasks: (storyId: string, newOrder: string[]) => void
// Terugdraaien bij server-fout
rollbackPbis: (productId: string, prevOrder: string[]) => void
rollbackStories: (pbiId: string, prevOrder: string[]) => void
rollbackTasks: (storyId: string, prevOrder: string[]) => void
}
```
**Gebruikspatroon:**
```ts
// 1. Server Component geeft ids door
// app/(app)/products/[id]/page.tsx
const pbis = await prisma.pbi.findMany({ where: { product_id: id }, orderBy: [...] })
return <BacklogPanel productId={id} initialPbiIds={pbis.map(p => p.id)} pbis={pbis} />
// 2. Client Component hydrateert store
// components/backlog/backlog-panel.tsx
'use client'
const { initPbis, reorderPbis, rollbackPbis } = usePlannerStore()
useEffect(() => { initPbis(productId, initialPbiIds) }, [])
// 3. dnd-kit onDragEnd → optimistisch updaten + Server Action
const prevOrder = usePlannerStore(s => s.pbiOrder[productId])
reorderPbis(productId, newOrder)
const result = await reorderPbisAction(productId, newOrder)
if (!result.success) rollbackPbis(productId, prevOrder)
```
---
### `useSelectionStore` — navigatieselectie
Beheert welk PBI of story geselecteerd is in het linkerpaneel, zodat beide panelen en de navigatiebar synchroon reageren zonder prop drilling.
```ts
// stores/selection-store.ts
interface SelectionStore {
selectedPbiId: string | null
selectedStoryId: string | null
selectPbi: (id: string | null) => void
selectStory: (id: string | null) => void
clearSelection: () => void
}
```
---
### `useSprintStore` — Sprint Backlog interacties
Beheert optimistische toevoegingen en verwijderingen van stories aan de Sprint Backlog tijdens drag-and-drop tussen de twee panelen.
```ts
// stores/sprint-store.ts
interface SprintStore {
// Stories per Sprint (optimistisch, op volgorde)
sprintStoryIds: Record<string, string[]> // sprintId → story-ids
initSprint: (sprintId: string, ids: string[]) => void
addStoryToSprint: (sprintId: string, storyId: string, atIndex: number) => void
removeStoryFromSprint: (sprintId: string, storyId: string) => void
reorderSprintStories: (sprintId: string, newOrder: string[]) => void
rollbackSprint: (sprintId: string, prevIds: string[]) => void
}
```
---
### `useSoloStore` — Solo board optimistische taakstatus
Beheert de taakstatus van de ingelogde gebruiker op het solo Kanban-board. Ondersteunt optimistische verplaatsingen tussen kolommen met rollback bij serverfout.
```ts
// stores/solo-store.ts
interface SoloStore {
tasks: Record<string, SoloTask>
initTasks: (tasks: SoloTask[]) => void
optimisticMove: (taskId: string, toStatus: TaskStatus) => TaskStatus | null
rollback: (taskId: string, prevStatus: TaskStatus) => void
updatePlan: (taskId: string, plan: string | null) => void
}
```
---
### `useProductStore` — Actief product voor navbar
Houdt het actief geselecteerde product (id + naam) bij zodat de navbar de productnaam kan tonen zonder prop drilling door de layout-hiërarchie.
```ts
// stores/product-store.ts
interface ProductStore {
currentProduct: { id: string; name: string } | null
setCurrentProduct: (id: string, name: string) => void
clearCurrentProduct: () => void
}
```
---
## Data flow architectuur
```
┌─────────────────────────────────────────┐
│ Server Component (page.tsx) │
│ Prisma query → initiële data + ids │
│ → props naar Client Component │
└──────────────────┬──────────────────────┘
│ initialIds, initialData
┌─────────────────────────────────────────┐
│ Client Component (panel.tsx) │
│ useEffect → store.init(ids) │
│ dnd-kit drag → store.reorder() │
│ → Server Action (async) │
│ → bij fout: store.rollback()│
└──────────────────┬──────────────────────┘
│ selecteert state via selector
┌─────────────────────────────────────────┐
│ Zustand Stores │
│ usePlannerStore useSelectionStore │
│ useSprintStore │
│ │
│ Alleen ephemere UI-staat │
│ Nooit server-data of business logic │
└─────────────────────────────────────────┘
```
**Keuze:** API-tokens opgeslagen als SHA-256 hashes in de `api_tokens` tabel
**Rationale:** Het token zelf wordt eenmalig getoond aan de gebruiker en nooit opgeslagen. De hash is voldoende voor lookup en verificatie. Redis of een aparte token-store zou overkill zijn voor v1-schaal.
**Trade-off:** Tokens kunnen niet worden verlengd of geroteerd zonder een nieuw token aan te maken.
---
## Realtime updates (M8)
Het Solo Paneel update live als andere gebruikers, scripts of admin-tools een task of story muteren. De pijplijn:
```
┌─────────────────────────┐
│ Mutatie (Prisma write) │ PATCH /api/tasks/:id
└────────────┬────────────┘ Server Action, MCP, etc.
┌─────────────────────────┐
│ Postgres row trigger │ AFTER INSERT/UPDATE/DELETE
│ scrum4me_notify_change()│ bouwt JSON payload
└────────────┬────────────┘
▼ pg_notify('scrum4me_changes', json)
┌─────────────────────────┐
│ /api/realtime/solo │ Node runtime, dedicated pg.Client
│ LISTEN scrum4me_changes │ filtert op product + sprint + assignee
└────────────┬────────────┘
▼ text/event-stream
┌─────────────────────────┐
│ EventSource (browser) │ beheerd door useSoloRealtime
│ → solo-store.handleEvent│ via flushSync + startViewTransition
└────────────┬────────────┘
┌─────────────────────────┐
│ SoloBoard re-render │ kanban-kaartje animeert naar
│ (View Transitions API) │ zijn nieuwe kolom
└─────────────────────────┘
```
**Keuze:** Postgres LISTEN/NOTIFY in plaats van polling, websockets of een externe broker (Pusher, Ably, Supabase Realtime).
**Rationale:** Eén Neon-database is al een verplichte dependency; LISTEN/NOTIFY voegt geen nieuwe infrastructuur toe. Polling zou voor één gebruiker prima werken maar schaalt slecht; een externe broker introduceert kosten, een tweede auth-laag, en synchronisatie-races tussen DB-writes en push-events.
**Trade-off:** Vereist een direct (unpooled) connection per open tab — Neon pooler ondersteunt LISTEN niet. Bij veel gelijktijdige gebruikers een keer her-evalueren.
### Mutaties die NOTIFY triggeren
De row trigger zit op `task` en `story`. Elke INSERT/UPDATE/DELETE op die tabellen — onafhankelijk van de bron (Prisma, MCP-server, raw SQL) — vuurt een NOTIFY met de geüpdate kolommen. Andere tabellen (Sprint, Product, etc.) doen dat niet; die hebben geen live-view in M8.
### Server-side filter
`/api/realtime/solo?product_id=...` filtert NOTIFY-payloads op:
- `product_id` matcht de query-param
- `sprint_id` matcht de actieve sprint van het product (resolve éénmaal per connect)
- `assignee_id` is gelijk aan de ingelogde `userId` (of `null` voor unassigned-story-claims)
Niet-matchende events worden server-side gedropt zodat de browser geen irrelevante data ontvangt en de solo-store geen onnodige diff-checks doet.
### Connection lifecycle
- **Open**: `EventSource('/api/realtime/solo?product_id=...')` zodra de gebruiker een actief product heeft. `SoloRealtimeBridge` mount in `(app)/layout` en krijgt het `productId` via prop, zodat de stream over de hele app open staat — niet alleen op `/solo`. Zo kunnen de Live-status-dot en worker-presence-indicator in de NavBar overal werken. Buiten `/solo` is de solo-store leeg en zijn binnenkomende task-events no-ops (`stores/solo-store.ts handleRealtimeEvent` skipt onbekende ids), dus de stream gedraagt zich automatisch als lichte presence-stream tot `SoloBoard` mount.
- **Reconnect**: exponential backoff bij `onerror` (1s → 30s, reset bij `ready` event).
- **Pause op tab-hidden**: `document.visibilityState === 'hidden'` sluit de stream actief. Bij `visible` wordt opnieuw verbonden. Dit voorkomt dat inactieve tabs DB-connecties open houden.
- **Hard close**: server sluit zelf na 240s (Vercel `maxDuration` is 300s); client herconnect transparant.
- **Heartbeat**: server stuurt elke 25s een `: heartbeat`-comment om proxies te keep-alive'n.
**Bekende beperking M8**: events die binnenkomen terwijl de tab `hidden` is, worden niet vervangen bij heropening. De gebruiker ziet de meest recente Postgres-state pas bij een page-refresh of een nieuwe mutatie. Voor v1 acceptabel; in M9+ overwegen we een replay-fetch op visibility-resume.
### Animatie
Voor `task UPDATE`-events wordt de store-update gewikkeld in `document.startViewTransition(() => flushSync(() => handleEvent(payload)))`. `flushSync` dwingt React om binnen de transition-callback synchroon te renderen, zodat View Transitions de oude en nieuwe DOM correct snapshot. Vereist `view-transition-name` op de task-cards (gezet op task-id). INSERT/DELETE-events animeren niet — die mutaties komen typisch met een page-load.
### Auth
Iron-session cookie of Bearer-token (demo). De auth-check loopt éénmalig bij de connect-request; tijdens de stream zelf is er geen herauth, dus een ingetrokken sessie blijft live tot de stream sluit (heartbeat-fail of hard-close). Voor M8 acceptabel — sessies expireren na 30 dagen.
---
## Realtime — Backlog SSE (ST-1115)
De Product Backlog-pagina (`/products/[id]`) update live als PBI's, stories of taken worden gemuteerd. De pijplijn is gelijk aan de Solo-SSE (M8), maar met een eenvoudiger server-side filter: alleen `product_id`-scope, geen sprint- of user-scope.
```
┌─────────────────────────┐
│ Mutatie (Prisma write) │ Server Action, MCP, etc.
└────────────┬────────────┘
┌─────────────────────────┐
│ Postgres row trigger │ AFTER INSERT/UPDATE/DELETE
│ scrum4me_notify_change()│ entity: 'pbi' | 'story' | 'task'
└────────────┬────────────┘
▼ pg_notify('scrum4me_changes', json)
┌─────────────────────────┐
│ /api/realtime/backlog │ Node runtime, dedicated pg.Client
│ LISTEN scrum4me_changes │ filtert op entity ∈ {pbi,story,task}
│ │ én product_id matcht query-param
└────────────┬────────────┘
▼ text/event-stream
┌─────────────────────────┐
│ EventSource (browser) │ beheerd door useBacklogRealtime
│ → backlog-store.apply │ via applyChange(entity, op, data)
│ Change(entity,op,data)│
└────────────┬────────────┘
┌─────────────────────────┐
│ PbiList / StoryPanel / │ re-render op basis van Zustand state
│ TaskPanel re-render │
└─────────────────────────┘
```
### Hydration en SSE-mount
De pagina is een Server Component die alle data parallel fetcht. Resultaten worden doorgegeven aan `BacklogHydrationWrapper` (client component), die:
1. `useBacklogStore.setInitialData(...)` aanroept op mount (eenmalig).
2. `useBacklogRealtime(productId)` mount — opent de SSE-stream.
Alle client-componenten (PbiList, StoryPanel, TaskPanel) lezen uitsluitend uit de Zustand store; ze accepteren geen data-props meer.
### backlog-store en applyChange
```ts
// stores/backlog-store.ts
applyChange(entity: 'pbi' | 'story' | 'task', op: 'I' | 'U' | 'D', data: Record<string, unknown>)
```
- **I (Insert):** voegt het nieuwe object toe aan de juiste sub-array
- **U (Update):** patcht de bestaande entry in-place via spread (`{ ...existing, ...data }`)
- **D (Delete):** filtert de entry weg op `id`; doorzoekt alle sub-arrays omdat de parent-ID afwezig kan zijn in het delete-payload
### Server-side filter (backlog)
`/api/realtime/backlog?product_id=...` filtert op:
- `entity ∈ {pbi, story, task}` — job/worker-events en questions worden genegeerd
- `product_id` matcht de query-param
Demo-gebruikers mogen lezen (geen 403). Overige lifecycle-kenmerken (heartbeat, hard-close, backoff, visibility-pause) zijn identiek aan de Solo SSE.
---

View file

@ -0,0 +1,88 @@
---
title: "QR-pairing Login Flow"
status: active
audience: [maintainer, contributor]
language: nl
last_updated: 2026-05-03
related: [auth-and-sessions.md](./auth-and-sessions.md)
---
## QR-pairing flow (M10)
Password-loze inlog op een (publieke) desktop. Mobiel — al ingelogd — bevestigt
door een QR te scannen die de desktop toont. Geen wachtwoord op het publieke
toetsenbord, geen credentials op de draad, demo-accounts geblokkeerd, paired-
sessie heeft eigen kortere TTL (8 u) + `paired`-vlag.
### Sequence
```mermaid
sequenceDiagram
participant D as Desktop (anon)
participant S as Server
participant M as Mobiel (ingelogd)
D->>S: POST /api/auth/pair/start
S->>S: maak LoginPairing { secret_hash, desktop_token_hash, status=pending, expires=+5min }
S-->>D: 200 { pairingId, mobileSecret, qrUrl }<br/>Set-Cookie: s4m_pair=desktopToken
D->>D: render QR met qrUrl (#id=…&s=mobileSecret)
D->>S: GET /api/auth/pair/stream/[pairingId]<br/>Cookie: s4m_pair
S->>S: LISTEN scrum4me_pairing
S-->>D: event: state { status: 'pending' }
Note over M: Gebruiker scant QR
M->>M: location.hash → mobileSecret
M->>S: getPairingForApproval(pairingId, mobileSecret)
S-->>M: { desktop_ua, desktop_ip, username }
M->>M: toont bevestigingskaart
Note over M: Tap "Bevestig"
M->>S: approvePairing(pairingId, mobileSecret)
S->>S: status pending→approved, expires +5min<br/>pg_notify scrum4me_pairing
S-->>D: data { status: 'approved' }
D->>S: POST /api/auth/pair/claim<br/>Cookie: s4m_pair, body: { pairingId }
S->>S: atomic UPDATE WHERE status=approved AND token-hash<br/>→ status=consumed
S->>S: getIronSession.save { userId, paired: true, pairedExpiresAt }
S-->>D: 200, Set-Cookie: session<br/>+ s4m_pair cleared
D->>D: redirect /dashboard
```
### Threat-model
| Aanval | Mitigatie |
|---|---|
| **Replay** van een geconsumeerde pairing | Atomic `updateMany WHERE status='approved'` — concurrent dubbele claim ziet count=0 → 410 |
| **Phishing-QR** ingesloten op een vreemde site | Mobiele bevestigingspagina toont desktop-UA + IP; gebruiker moet expliciet tappen; waarschuwing onder de kaart |
| **Demo-account misbruik** | `approvePairing` early-return op `session.isDemo` — pairing blijft `pending` |
| **Brute-force** van pairings | Rate-limit 10 starts per IP per minuut; `pairingId` is CUID (lange entropy) |
| **Secret-leak via DB-dump** | DB bevat alleen sha256-hashes; plaintext geheimen verlaten desktop alleen via QR-fragment + POST-body (mobile) of HttpOnly cookie (desktop) |
| **Long-lived sessie op publieke desktop** | Paired-sessie krijgt 8u TTL i.p.v. reguliere; `paired: true` markeert 'm voor toekomstige remote-revoke |
### TTL-rationale
- **Pending: 5 min.** Genoeg voor menselijke handeling (telefoon pakken, scannen, bevestigen) — kort genoeg dat een verloren QR een klein attack-window heeft.
- **Approved (na bump): nogmaals 5 min.** Klant claim moet binnen redelijke tijd plaatsvinden; voorkomt dat een approved-maar-onclaimed pairing eindeloos open blijft.
- **Paired-sessie: 8 uur.** Korter dan de reguliere wachtwoord-sessie omdat de use-case publieke apparaten zijn waar je niet wil dat de sessie 's nachts blijft hangen.
### Waarom geen secret in URL
Servers loggen URL-paden en querystrings standaard — `nginx`, Vercel access
logs, observability-stacks (Sentry, Datadog), reverse proxies, CDN's. Een
geheim in `?s=…` belandt onbedoeld in al die logs. Twee technieken voorkomen dit:
1. **URL-fragment voor `mobileSecret`.** Het deel achter de `#` wordt door
browsers nooit naar de server gestuurd in HTTP-requests. De mobiele Client
Component leest `window.location.hash` en POST't de waarde in een body —
ook niet in een URL.
2. **HttpOnly cookie voor `desktopToken`.** Cookie-headers worden meestal NIET
in toegangslogs gelogd (in tegenstelling tot URLs). De cookie is bovendien
`Path=/api/auth/pair`-scoped, dus verlaat die route nooit.
Twee gescheiden hashes (`secret_hash` voor mobiel-bewijs, `desktop_token_hash`
voor desktop-bewijs) zorgen dat een ene server-side compromis niet automatisch
de andere kant compromitteert.
Dit patroon is herbruikbaar — zie `docs/patterns/qr-login.md`.
---

View file

Before

Width:  |  Height:  |  Size: 521 KiB

After

Width:  |  Height:  |  Size: 521 KiB

Before After
Before After

View file

@ -1,3 +1,11 @@
---
title: "Scrum4Me — Implementatie Backlog"
status: active
audience: [maintainer, contributor]
language: nl
last_updated: 2026-05-03
---
# Scrum4Me — Implementatie Backlog # Scrum4Me — Implementatie Backlog
**Versie:** 0.1 — april 2026 **Versie:** 0.1 — april 2026
@ -23,7 +31,7 @@ De MVP is klaar wanneer Lars — de primaire persona — de volledige cyclus kan
| M4: Claude Code REST API | Alle endpoints, tokenbeheer | ST-401 ST-410 | | 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 | | 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 | | 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 | | M7: MCP-server voor Claude Code | Native MCP-laag bovenop Scrum4Me-DB (aparte repo `mcp`) | ST-701 ST-710 |
| M8: Realtime Solo Paneel | Live updates voor stories/tasks via SSE + Postgres LISTEN/NOTIFY | ST-801 ST-806 | | 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 | | 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 | | M10: Password-loze inlog via QR-pairing | Mobiel als bevestigingskanaal voor desktop-login zonder wachtwoord | ST-1001 ST-1008 |
@ -223,7 +231,7 @@ De MVP is klaar wanneer Lars — de primaire persona — de volledige cyclus kan
### M3.5: Solo Paneel & Story Assignment ### 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`. > **Doel:** een persoonlijk Kanban-bord per product dat de taken toont van stories die geclaimd zijn door de ingelogde developer. Story-level assignment volgt het Scrum self-organizing principe: developers claimen vrijwillig stories (pull, niet push). Volledige technische specificatie in `specs/functional.md#solo-panel`.
- [x] **ST-350** Story.assignee_id schema-migratie + auth-helpers - [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` - **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`
@ -409,8 +417,13 @@ De MVP is klaar wanneer Lars — de primaire persona — de volledige cyclus kan
- **`PATCH /api/tasks/:id`:** accepteert lowercase `status` via mapper; retourneert lowercase - **`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 - **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 - **Foutcodes:** Zod-validatie geeft `422` (was `400`); `400` blijft voor malformed body; `401`/`403`/`404`/`500` ongewijzigd
<<<<<<<< HEAD:docs/backlog/index.md
- **API-documentatie:** nieuwe `docs/api/rest-contract.md` met endpoints, request/response, foutcodes, status-enums en curl-voorbeelden; `CLAUDE.md` verwijst ernaar
- Done when: `curl /api/health` werkt zonder auth; `curl /api/products/:id/claude-context` retourneert bundled JSON; PATCH/PUT routes accepteren lowercase status en geven 422 bij ongeldige body; story-log POST bewaart `metadata`; `docs/api/rest-contract.md` is gepubliceerd
========
- **API-documentatie:** nieuwe `docs/api.md` met endpoints, request/response, foutcodes, status-enums en curl-voorbeelden; `CLAUDE.md` verwijst ernaar - **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 - Done when: `curl /api/health` werkt zonder auth; `curl /api/products/:id/claude-context` retourneert bundled JSON; PATCH/PUT routes accepteren lowercase status en geven 422 bij ongeldige body; story-log POST bewaart `metadata`; `docs/api.md` is gepubliceerd
>>>>>>>> origin/main:docs/backlog.md
- **`GET /api/products`:** voeg `code` toe (naast `id`, `name`, `repo_url`); optioneel `description` en `definition_of_done` - **`GET /api/products`:** 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/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}`) - **`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}`)
@ -475,7 +488,7 @@ De MVP is klaar wanneer Lars — de primaire persona — de volledige cyclus kan
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. 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 - [x] **ST-701** Repo-skeleton mcp
- npm init, tsconfig strict, .gitignore, MCP SDK 1.29, Prisma 7, zod, tsx; lege `src/index.ts` die op stdio start - 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 - Done when: `npx tsx src/index.ts` print `running on stdio` zonder crash; `tsc --noEmit` slaagt
@ -547,20 +560,24 @@ Filtering server-side: alleen events binnen de actieve sprint van een product wa
- Done when: twee tabs van Solo Paneel — mutatie in tab A komt binnen 12s in tab B zonder refresh - Done when: twee tabs van Solo Paneel — mutatie in tab A komt binnen 12s in tab B zonder refresh
- [x] **ST-806** Documentatie + acceptatietest - [x] **ST-806** Documentatie + acceptatietest
<<<<<<<< HEAD:docs/backlog/index.md
- Sectie "Realtime updates" in `docs/architecture.md` met diagram en filtering-regels; vermelding in `CLAUDE.md`; korte note over `/api/realtime/solo` in `docs/api/rest-contract.md`; handmatig E2E-scenario's gedraaid (zelfde gebruiker twee tabs, MCP-write, REST-write, story-claim, network-flap)
========
- Sectie "Realtime updates" in `docs/architecture.md` met diagram en filtering-regels; vermelding in `CLAUDE.md`; korte note over `/api/realtime/solo` in `docs/api.md`; handmatig E2E-scenario's gedraaid (zelfde gebruiker twee tabs, MCP-write, REST-write, story-claim, network-flap) - Sectie "Realtime updates" in `docs/architecture.md` met diagram en filtering-regels; vermelding in `CLAUDE.md`; korte note over `/api/realtime/solo` in `docs/api.md`; handmatig E2E-scenario's gedraaid (zelfde gebruiker twee tabs, MCP-write, REST-write, story-claim, network-flap)
>>>>>>>> origin/main:docs/backlog.md
- Done when: alle scenario's lopen door zonder onverwachte gedragingen - Done when: alle scenario's lopen door zonder onverwachte gedragingen
Volledig plan in `.Plans/2026-04-27-m8-realtime-solo.md` (lokaal, niet gecommit). Volledig plan in `.Plans/2026-04-27-m8-realtime-solo.md` (lokaal, niet gecommit).
### M9: Actief Product Backlog ### M9: Actief Product Backlog
**Implementatieplan:** [docs/plans/M9-active-product-backlog.md](plans/M9-active-product-backlog.md) **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. 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` - [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 - 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 - Done when: `npx prisma migrate dev` slaagt; `prisma studio` toont kolom; `npx prisma validate` zonder fouten; submodule `vendor/scrum4me` in mcp draait `prisma generate` + `tsc --noEmit` zonder fouten
- [x] **ST-902** Server Actions — actief product zetten en wissen - [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 - `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
@ -584,13 +601,13 @@ Eén "actief Product Backlog" per gebruiker — persistent in DB. De NavBar word
- [x] **ST-907** Documentatie en tests - [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 - 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 - Done when: `npm run lint && npx tsc --noEmit && npm test && npm run build` groen; spec-secties geschreven; `vendor/scrum4me`-submodule in mcp gesynced
--- ---
### M10: Password-loze inlog via QR-pairing ### M10: Password-loze inlog via QR-pairing
**Implementatieplan:** [docs/plans/M10-qr-pairing-login.md](plans/M10-qr-pairing-login.md) **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 12 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. 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 12 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.
@ -641,12 +658,20 @@ Volledige flow + threat-model: `docs/patterns/qr-login.md` (op te leveren in ST-
- [ ] **ST-1007** Desktop UI: QR-render + SSE-listener op `/login` - [ ] **ST-1007** Desktop UI: QR-render + SSE-listener op `/login`
- **Dependency:** `qrcode.react` (client SVG; mobileSecret blijft op desktop in JS-geheugen) - **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/qr-login-button.tsx`:** Client Component; klik → POST `pair/start` (`credentials: 'same-origin'` zodat `s4m_pair`-cookie wordt geaccepteerd) → render QR met `qrUrl` (fragment-URL) → open `EventSource('/api/auth/pair/stream/<pairingId>', { withCredentials: true })` → bij `approved` event POST `pair/claim` (cookie-only) → bij succes `router.push('/dashboard')`; aftellende timer (2 min); bij timeout "Vernieuwen"-knop; cleanup bij unmount/redirect
<<<<<<<< HEAD:docs/backlog/index.md
- **`app/login/page.tsx`:** knop "Inloggen via mobiel" naast bestaande wachtwoord-form (MD3-tokens uit `docs/design/styling.md`)
========
- **`app/login/page.tsx`:** knop "Inloggen via mobiel" naast bestaande wachtwoord-form (MD3-tokens uit `docs/styling.md`) - **`app/login/page.tsx`:** knop "Inloggen via mobiel" naast bestaande wachtwoord-form (MD3-tokens uit `docs/styling.md`)
>>>>>>>> origin/main:docs/backlog.md
- A11y: QR heeft alt-tekst met de URL voor screenreaders/copy-paste (de hash-suffix is onderdeel van die alt-tekst, niet van de page-URL die in browsergeschiedenis komt) - 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 - 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 - [ ] **ST-1008** Documentatie + acceptatietest
<<<<<<<< HEAD:docs/backlog/index.md
- **`docs/api/rest-contract.md`:** drie nieuwe endpoints (start/stream/claim) met request/response, cookie-mechaniek, foutcodes (400/401/403/404/410/422/429), curl-voorbeelden inclusief `--cookie-jar`
========
- **`docs/api.md`:** drie nieuwe endpoints (start/stream/claim) met request/response, cookie-mechaniek, foutcodes (400/401/403/404/410/422/429), curl-voorbeelden inclusief `--cookie-jar` - **`docs/api.md`:** drie nieuwe endpoints (start/stream/claim) met request/response, cookie-mechaniek, foutcodes (400/401/403/404/410/422/429), curl-voorbeelden inclusief `--cookie-jar`
>>>>>>>> origin/main:docs/backlog.md
- **`docs/architecture.md`:** sectie "QR-pairing flow" met sequence-diagram + threat-model; expliciete subsectie *"Waarom geen secret in URL"* — fragments worden niet naar server gestuurd; SSE/claim authenticeren via HttpOnly cookie zodat secret-materiaal niet in access logs / reverse-proxy logs / observability-tools / browsergeschiedenis kan belanden - **`docs/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 - **`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 - **`CLAUDE.md`:** verwijzing naar het nieuwe pattern-doc in de patterns-tabel
@ -657,7 +682,7 @@ Volledige flow + threat-model: `docs/patterns/qr-login.md` (op te leveren in ST-
### M11: Claude vraagt, gebruiker antwoordt ### M11: Claude vraagt, gebruiker antwoordt
**Implementatieplan:** [docs/plans/M11-claude-questions.md](plans/M11-claude-questions.md) **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. 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.
@ -669,7 +694,7 @@ Persistent vraag-antwoord-kanaal tussen Claude Code (via MCP) en de actieve Scru
- **Migratie:** `prisma migrate dev --name add_claude_questions` - **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 - 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) - [ ] **ST-1102** MCP-tools voor Claude (in mcp-repo)
- **`ask_user_question`** (write): input `{ story_id, question, options?, task_id?, wait_seconds? }`; insert pairing + optioneel pollen tot `wait_seconds` (max 600); demo-blok via `requireWriteAccess`; access-check via `userCanAccessProduct(story.product_id, ...)` - **`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 - **`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) - **`list_open_questions`** (read): lijst van eigen vragen (laatste 50, status open of answered)
@ -694,7 +719,11 @@ Persistent vraag-antwoord-kanaal tussen Claude Code (via MCP) en de actieve Scru
- **`stores/notifications-store.ts`** — Zustand store volgens `solo-store.ts`-patroon: `init`, `add`, `update`, `remove`, `optimisticAnswer`, `rollbackAnswer`; selectors `openCount`, `forYouCount` - **`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 - **`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/notifications/notifications-bridge.tsx`** — Server Component die initial-data fetcht en aan store geeft; mount in `app/(app)/layout.tsx` naast `<SoloRealtimeBridge />`
<<<<<<<< HEAD:docs/backlog/index.md
- **`components/shared/notifications-bell.tsx`** — Bell-icon (Lucide) met badge in NavBar (links van avatar); MD3-tokens uit `docs/design/styling.md`
========
- **`components/shared/notifications-bell.tsx`** — Bell-icon (Lucide) met badge in NavBar (links van avatar); MD3-tokens uit `docs/styling.md` - **`components/shared/notifications-bell.tsx`** — Bell-icon (Lucide) met badge in NavBar (links van avatar); MD3-tokens uit `docs/styling.md`
>>>>>>>> origin/main:docs/backlog.md
- **`components/notifications/notifications-sheet.tsx`** — shadcn Sheet van rechts; lijst gegroepeerd per product; story-assignee krijgt visuele *"wacht op jou"*-emphase - **`components/notifications/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 - **`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 - 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
@ -713,7 +742,11 @@ Persistent vraag-antwoord-kanaal tussen Claude Code (via MCP) en de actieve Scru
- Done when: handmatige `curl -X POST` met secret expireert oude rijen; Vercel-dashboard toont cron-config na deploy; onbevoegde call → 401 - 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 - [ ] **ST-1108** Documentatie + acceptatietest
<<<<<<<< HEAD:docs/backlog/index.md
- **`docs/api/rest-contract.md`:** secties "SSE — Notifications" + "Cron — Expire questions" met curl-voorbeelden
========
- **`docs/api.md`:** secties "SSE — Notifications" + "Cron — Expire questions" met curl-voorbeelden - **`docs/api.md`:** secties "SSE — Notifications" + "Cron — Expire questions" met curl-voorbeelden
>>>>>>>> origin/main:docs/backlog.md
- **`docs/architecture.md`:** sectie "Vraag-antwoord-kanaal Claude ↔ user" met Mermaid sequence-diagram + threat-model + "Waarom hergebruik scrum4me_changes-kanaal" - **`docs/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 - **`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 - **`CLAUDE.md`:** rij in Implementatiepatronen-tabel voor het nieuwe pattern

784
docs/backlog/index.md Normal file
View file

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

View file

@ -0,0 +1,462 @@
---
title: "DevPlanner — Product Backlog"
status: active
audience: [maintainer]
language: nl
last_updated: 2026-05-03
---
# DevPlanner — Product Backlog
**Versie:** 0.1 — april 2026
**Product:** DevPlanner
**Beschrijving:** Een lichtgewicht Scrum-gebaseerde projectplanner voor solo developers en kleine Scrum Teams die meerdere softwareprojecten parallel beheren. De app organiseert werk hiërarchisch (product → PBI → story → taak), biedt een visuele planningslaag en integreert met Claude Code voor geautomatiseerde implementatieflows.
**Git repo:** https://github.com/devplanner/devplanner
**Definition of Done:** Feature is geïmplementeerd, getest (unit + integratie), gedocumenteerd in code, en gedeployed naar de staging-omgeving zonder regressies.
---
## Prioriteiten
| Prioriteit | Betekenis |
|---|---|
| 1 — Kritiek | Blokkeert alle andere functionaliteit. Moet eerst. |
| 2 — Hoog | Core waarde van het product. MVP vereiste. |
| 3 — Middel | Verhoogt bruikbaarheid significant. v1 wenselijk. |
| 4 — Laag | Waardevol maar niet blokkerend. v2 kandidaat. |
---
## PBI-01 — Authenticatie & gebruikersbeheer
**Prioriteit:** 1 — Kritiek
**Omschrijving:** Het Scrum Team kan een account aanmaken en inloggen met gebruikersnaam en wachtwoord. Een demo-gebruiker heeft alleen leesrechten. Gebruikers kunnen één of meerdere Scrum-rollen aannemen.
### Stories
**S-01-01: Account aanmaken**
Als bezoeker wil ik een account aanmaken met gebruikersnaam en wachtwoord, zodat ik toegang krijg tot de app.
Acceptatiecriteria:
- Gebruikersnaam en wachtwoord zijn verplicht
- Gebruikersnaam is uniek; dubbele aanmelding geeft foutmelding
- Wachtwoord heeft minimaal 8 tekens
- Na aanmaken wordt de gebruiker direct ingelogd
- Geen e-mailverificatie vereist in v1
**S-01-02: Inloggen**
Als geregistreerde gebruiker wil ik inloggen met gebruikersnaam en wachtwoord, zodat ik mijn projecten kan beheren.
Acceptatiecriteria:
- Incorrecte combinatie geeft generieke foutmelding (geen onderscheid gebruikersnaam/wachtwoord)
- Na inloggen wordt de gebruiker doorgestuurd naar het dashboard
- Sessie blijft actief totdat de gebruiker uitlogt
**S-01-03: Uitloggen**
Als ingelogde gebruiker wil ik kunnen uitloggen, zodat mijn sessie veilig afgesloten wordt.
Acceptatiecriteria:
- Uitlogknop altijd zichtbaar in de navigatie
- Na uitloggen wordt de gebruiker naar de loginpagina gestuurd
- Sessiedata wordt gewist
**S-01-04: Demo-gebruiker (read-only)**
Als bezoeker wil ik kunnen inloggen als demo-gebruiker, zodat ik de app kan verkennen zonder een account aan te maken.
Acceptatiecriteria:
- Vaste inloggegevens voor de demo-gebruiker zijn beschikbaar op de loginpagina
- Demo-gebruiker ziet alle data maar kan niets aanmaken, aanpassen of verwijderen
- Alle actieknoppen (aanmaken, bewerken, verwijderen) zijn zichtbaar maar uitgeschakeld met tooltip "Niet beschikbaar in demo-modus"
- Demo-gebruiker kan niet van rol wisselen
**S-01-05: Roltoewijzing**
Als gebruiker wil ik één of meerdere Scrum-rollen kunnen aannemen (Product Owner, Scrum Master, Developer), zodat de app weet in welke context ik werk.
Acceptatiecriteria:
- Gebruiker kan bij registratie of in instellingen rollen selecteren
- Minimaal één rol is verplicht
- Alle drie de rollen tegelijk zijn toegestaan
- Rolkeuze is zichtbaar in de navigatie/profielbalk
- Rolkeuze heeft in v1 geen effect op zichtbare functionaliteit (voorbereiding op v2)
---
## PBI-02 — Productbeheer
**Prioriteit:** 1 — Kritiek
**Omschrijving:** Het Scrum Team kan producten aanmaken, bekijken, bewerken en archiveren. Een product heeft een naam, beschrijving en koppeling naar een git-repository.
### Stories
**S-02-01: Product aanmaken**
Als Product Owner wil ik een nieuw product aanmaken met naam, beschrijving en git-repo URL, zodat ik een werkruimte heb voor de Product Backlog.
Acceptatiecriteria:
- Naam is verplicht en uniek per gebruiker
- Beschrijving is optioneel (vrije tekst)
- Git-repo URL is optioneel maar wordt gevalideerd als geldige URL
- Product is direct zichtbaar in de productenlijst na aanmaken
**S-02-02: Product bewerken**
Als Product Owner wil ik de naam, beschrijving en git-repo URL van een product kunnen aanpassen, zodat de informatie actueel blijft.
Acceptatiecriteria:
- Alle velden zijn bewerkbaar
- Wijzigingen worden opgeslagen zonder de pagina te verlaten
- Lege naam geeft validatiefout
**S-02-03: Product archiveren**
Als Product Owner wil ik een product kunnen archiveren, zodat het niet meer in het overzicht verschijnt maar de data bewaard blijft.
Acceptatiecriteria:
- Gearchiveerde producten verschijnen niet in de standaardlijst
- Er is een optie om gearchiveerde producten te tonen
- Archiveren is omkeerbaar (product kan worden hersteld)
**S-02-04: Productenlijst bekijken**
Als gebruiker wil ik een overzicht zien van alle actieve producten, zodat ik snel naar het juiste product kan navigeren.
Acceptatiecriteria:
- Lijst toont naam, beschrijving (ingekort) en git-repo link
- Klikken op een product opent de Product Backlog van dat product
- Lege staat toont een prompt om een product aan te maken
---
## PBI-03 — Product Backlog
**Prioriteit:** 1 — Kritiek
**Omschrijving:** Het Scrum Team kan de Product Backlog beheren via een gesplitst scherm: links de PBI's, rechts de bijbehorende stories. Items kunnen aangemaakt, bewerkt, geprioriteerd en gerangschikt worden via drag-and-drop (dnd-kit).
### Stories
**S-03-01: PBI aanmaken**
Als Product Owner wil ik een PBI aanmaken in de Product Backlog, zodat ik nieuwe functionaliteit kan definiëren.
Acceptatiecriteria:
- PBI heeft een titel (verplicht) en omschrijving (optioneel)
- PBI krijgt een prioriteit (1 t/m 4)
- Nieuw PBI verschijnt onderaan de lijst voor de gekozen prioriteit
- Aanmaken via knop in de navigatiebar van het linkerpaneel
**S-03-02: PBI bewerken**
Als Product Owner wil ik de titel, omschrijving en prioriteit van een PBI kunnen aanpassen.
Acceptatiecriteria:
- Dubbelklikken of via contextmenu opent bewerkingsmodus
- Alle velden zijn inline bewerkbaar
- Prioriteitswijziging herplaatst het PBI visueel
**S-03-03: PBI verwijderen**
Als Product Owner wil ik een PBI kunnen verwijderen, zodat irrelevante items de backlog niet vervuilen.
Acceptatiecriteria:
- Verwijderen vereist bevestiging
- Verwijderen van een PBI verwijdert ook alle bijbehorende stories (cascade)
- Actie is niet ongedaan te maken; bevestigingsdialoog waarschuwt hiervoor
**S-03-04: PBI prioriteit instellen**
Als Product Owner wil ik per PBI een prioriteit kunnen instellen (1 t/m 4), zodat de volgorde van de backlog de businesswaarde weerspiegelt.
Acceptatiecriteria:
- Prioriteit is instelbaar via dropdown of inline label
- PBI's worden gegroepeerd per prioriteit in de lijst
- Visuele scheiding per prioriteitsgroep (kleurband of scheidingslijn)
**S-03-05: PBI volgorde aanpassen via drag-and-drop**
Als Product Owner wil ik de volgorde van PBI's binnen dezelfde prioriteit kunnen aanpassen via drag-and-drop, zodat ik fijnmazige prioritering kan doen.
Acceptatiecriteria:
- Drag-and-drop werkt vloeiend (60fps) via dnd-kit
- Volgorde wordt direct opgeslagen na loslaten
- Drag over prioriteitsgrens wisselt de prioriteit van het PBI
- Visuele placeholder toont de doelpositie tijdens het slepen
**S-03-06: PBI filteren**
Als gebruiker wil ik PBI's kunnen filteren op prioriteit of status, zodat ik me kan focussen op het relevante werk.
Acceptatiecriteria:
- Filteropties beschikbaar in de navigatiebar van het linkerpaneel
- Filter werkt realtime (geen herlaadactie)
- Actief filter is duidelijk zichtbaar; eenvoudig te wissen
**S-03-07: Gesplitst scherm Product Backlog**
Als gebruiker wil ik de Product Backlog bekijken als gesplitst scherm (PBI's links, stories rechts), zodat ik snel kan navigeren tussen PBI's en hun stories.
Acceptatiecriteria:
- Scherm is standaard 50/50 verdeeld
- De splitter is horizontaal versleepbaar
- Elk paneel heeft een eigen navigatiebar met acties
- Selecteren van een PBI links toont de bijbehorende stories rechts
- Geselecteerde PBI is visueel gemarkeerd
---
## PBI-04 — Story-beheer
**Prioriteit:** 1 — Kritiek
**Omschrijving:** Stories kunnen worden aangemaakt, bewerkt, geprioriteerd en gerangschikt binnen een PBI. Stories worden weergegeven als blokken van circa 10% schermbreedte, gerangschikt op prioriteit.
### Stories
**S-04-01: Story aanmaken**
Als Product Owner wil ik een story aanmaken binnen een PBI, zodat ik de functionaliteit kan uitwerken in uitvoerbare eenheden.
Acceptatiecriteria:
- Story heeft een titel (verplicht), omschrijving (optioneel) en prioriteit
- Aanmaken via navigatiebar van het rechterpaneel
- Nieuwe story verschijnt als blok rechts, in de juiste prioriteitsgroep
**S-04-02: Story weergave als blokken**
Als gebruiker wil ik stories zien als compacte blokken (~10% schermbreedte), zodat ik snel een overzicht heb van alle stories per PBI.
Acceptatiecriteria:
- Elk blok toont: storytitel, prioriteit, status
- Blokken zijn gerangschikt op prioriteit (hoog naar laag, links naar rechts)
- Elke nieuwe prioriteitsgroep heeft een visuele scheiding (kleurband of lijn)
- Blokken zijn klikbaar voor detail/bewerking
**S-04-03: Story prioriteit instellen**
Als Product Owner wil ik per story een prioriteit instellen, zodat de Developer weet wat als eerste opgepakt moet worden.
Acceptatiecriteria:
- Prioriteit instelbaar via het storyblok (dropdown of label)
- Prioriteitswijziging herplaatst het blok in de juiste groep
**S-04-04: Story volgorde aanpassen via drag-and-drop**
Als Product Owner wil ik de volgorde van stories binnen dezelfde prioriteit aanpassen via drag-and-drop, zodat ik de uitvoeringsvolgorde kan finetunen.
Acceptatiecriteria:
- Drag-and-drop werkt via dnd-kit tussen en binnen prioriteitsgroepen
- Volgorde wordt direct opgeslagen
- Slepen over een prioriteitsgrens wijzigt de prioriteit
**S-04-05: Story bewerken**
Als Product Owner wil ik de titel, omschrijving en prioriteit van een story kunnen aanpassen.
Acceptatiecriteria:
- Bewerkbaar via klikken op het storyblok
- Wijzigingen opgeslagen zonder paginaverversing
**S-04-06: Story verwijderen**
Als Product Owner wil ik een story kunnen verwijderen.
Acceptatiecriteria:
- Verwijderen vereist bevestiging
- Cascade verwijdering van gekoppelde taken
- Niet ongedaan te maken; waarschuwing in dialoog
---
## PBI-05 — Todo-lijst
**Prioriteit:** 2 — Hoog
**Omschrijving:** Gebruikers kunnen een snelle todo-lijst bijhouden voor ongeplande of kortstondige taken. Een todo-item kan worden gepromoveerd naar een PBI of story.
### Stories
**S-05-01: Todo-item aanmaken**
Als gebruiker wil ik snel een todo-item aanmaken zonder het aan een product te koppelen, zodat ik losse gedachten kan vastleggen zonder de planningsflow te onderbreken.
Acceptatiecriteria:
- Todo heeft alleen een titel (verplicht)
- Aanmaken via een snel-invoerveld (Enter om op te slaan)
- Todo's zijn zichtbaar in een aparte todo-sectie of zijpaneel
**S-05-02: Todo-item afvinken**
Als gebruiker wil ik een todo-item kunnen afvinken, zodat ik bij kan houden wat klaar is.
Acceptatiecriteria:
- Afgevinkte items zijn visueel doorgestreept
- Afgevinkte items blijven zichtbaar maar kunnen worden gearchiveerd
**S-05-03: Todo promoveren naar PBI**
Als Product Owner wil ik een todo-item promoveren naar een PBI in een bestaand product, zodat losse ideeën in de formele backlog terechtkomen.
Acceptatiecriteria:
- Promoten opent een dialoog om product en prioriteit te kiezen
- Het todo-item wordt omgezet naar een PBI en verdwijnt uit de todo-lijst
- De PBI-titel is gelijk aan de todo-titel (bewerkbaar in dialoog)
**S-05-04: Todo promoveren naar story**
Als Product Owner wil ik een todo-item promoveren naar een story binnen een bestaand PBI, zodat ik snel nieuwe stories kan toevoegen vanuit losse notities.
Acceptatiecriteria:
- Promoten opent een dialoog om product, PBI en prioriteit te kiezen
- Todo wordt omgezet naar een story en verdwijnt uit de todo-lijst
---
## PBI-06 — Sprint Backlog & Sprint Planning
**Prioriteit:** 2 — Hoog
**Omschrijving:** Het Scrum Team kan een Sprint aanmaken met een Sprint Goal, stories uit de Product Backlog naar de Sprint Backlog slepen, en de volgorde bepalen.
### Stories
**S-06-01: Sprint aanmaken**
Als Scrum Master wil ik een nieuwe Sprint aanmaken met een Sprint Goal, zodat het Scrum Team een duidelijk doel heeft voor de komende Sprint.
Acceptatiecriteria:
- Sprint heeft een Sprint Goal (verplicht, vrije tekst)
- Sprint is gekoppeld aan een product
- Er kan maar één actieve Sprint per product zijn
**S-06-02: Sprint Backlog scherm (gesplitst)**
Als gebruiker wil ik de Sprint Backlog kunnen beheren via een gesplitst scherm (Sprint Backlog links, stories per PBI rechts), zodat ik snel stories kan toevoegen aan de Sprint.
Acceptatiecriteria:
- Links: Sprint Backlog met geselecteerde stories in volgorde
- Rechts: stories uit de Product Backlog, gegroepeerd per PBI
- Splitter is horizontaal versleepbaar
- Elk paneel heeft eigen navigatiebar
**S-06-03: Story naar Sprint slepen**
Als Developer wil ik een story vanuit de Product Backlog naar de Sprint Backlog kunnen slepen, zodat ik bepaal wat we deze Sprint gaan oppakken.
Acceptatiecriteria:
- Drag-and-drop werkt via dnd-kit tussen rechterpaneel en linkerpaneel
- Story verschijnt in de Sprint Backlog op de gesleepte positie
- Story in de Product Backlog krijgt visuele markering "In Sprint"
- Een story kan maar aan één actieve Sprint gekoppeld zijn
**S-06-04: Volgorde stories in Sprint bepalen**
Als Developer wil ik de volgorde van stories in de Sprint Backlog kunnen aanpassen via drag-and-drop, zodat ik de uitvoeringsvolgorde bepaal.
Acceptatiecriteria:
- Drag-and-drop werkt binnen de Sprint Backlog
- Volgorde wordt direct opgeslagen
- Volgorde is onafhankelijk van de prioriteit in de Product Backlog
**S-06-05: Story uit Sprint verwijderen**
Als Developer wil ik een story uit de Sprint Backlog kunnen verwijderen (terugplaatsen in de Product Backlog), zodat we de Sprint scope kunnen aanpassen.
Acceptatiecriteria:
- Story verdwijnt uit de Sprint Backlog
- Story is weer beschikbaar in de Product Backlog
- Actie vereist geen bevestiging (is niet destructief)
---
## PBI-07 — Sprint Planning (taken per story)
**Prioriteit:** 2 — Hoog
**Omschrijving:** Tijdens Sprint Planning worden stories opgedeeld in taken. Hetzelfde gesplitste scherm wordt gebruikt: stories links, taken rechts. Taken kunnen geprioriteerd en gerangschikt worden.
### Stories
**S-07-01: Sprint Planning scherm**
Als Developer wil ik een Sprint Planning scherm zien met stories links en taken rechts, zodat ik per story taken kan aanmaken en rangschikken.
Acceptatiecriteria:
- Links: stories in de Sprint Backlog in volgorde
- Rechts: taken van de geselecteerde story
- Selecteren van een story links toont de bijbehorende taken rechts
- Gesplitst scherm is horizontaal versleepbaar
**S-07-02: Taak aanmaken**
Als Developer wil ik een taak aanmaken onder een story, zodat ik het uitvoerbare werk kan definiëren.
Acceptatiecriteria:
- Taak heeft een titel (verplicht), omschrijving (optioneel) en prioriteit
- Aanmaken via navigatiebar van het rechterpaneel
- Nieuwe taak verschijnt onderaan de takenlijst van de story
**S-07-03: Taak prioriteit instellen**
Als Developer wil ik per taak een prioriteit instellen, zodat de uitvoeringsvolgorde duidelijk is.
Acceptatiecriteria:
- Prioriteit instelbaar via taakregel (dropdown of label)
- Taken gegroepeerd en gerangschikt op prioriteit
**S-07-04: Taak volgorde aanpassen via drag-and-drop**
Als Developer wil ik de volgorde van taken binnen een story kunnen aanpassen via drag-and-drop, zodat de uitvoeringsvolgorde precies klopt.
Acceptatiecriteria:
- Drag-and-drop via dnd-kit binnen de takenlijst
- Volgorde direct opgeslagen na loslaten
**S-07-05: Taakstatus bijhouden**
Als Developer wil ik de status van een taak kunnen bijhouden (To Do, In Progress, Done), zodat de voortgang van de Sprint zichtbaar is.
Acceptatiecriteria:
- Status is instelbaar via de UI (dropdown of knoppen)
- Statuswijziging is direct zichtbaar in het Sprint Planning scherm
- Story toont een voortgangsindicator op basis van taakstatussen
---
## PBI-08 — Claude Code integratie
**Prioriteit:** 2 — Hoog
**Omschrijving:** Claude Code kan via een REST API (en later MCP) stories en taken ophalen, de volgorde beoordelen, een implementatieplan opstellen, tests uitvoeren en committen. Elk resultaat wordt vastgelegd in de story.
### Stories
**S-08-01: REST API — story ophalen**
Als Developer (via Claude Code) wil ik de hoogst geprioriteerde open story van een product kunnen ophalen via een API-endpoint, zodat Claude Code weet wat er gedaan moet worden.
Acceptatiecriteria:
- Endpoint: `GET /api/products/:id/next-story`
- Retourneert: story-id, titel, omschrijving, acceptatiecriteria, gekoppelde taken
- Authentiseerd via API-token
- Geeft 404 als er geen open stories zijn
**S-08-02: REST API — eerste 10 taken ophalen**
Als Developer (via Claude Code) wil ik de eerste 10 taken van de Sprint Backlog kunnen ophalen, zodat Claude Code de volgorde kan beoordelen en zo nodig aanpassen.
Acceptatiecriteria:
- Endpoint: `GET /api/sprints/:id/tasks?limit=10`
- Retourneert taken in huidige volgorde met id, titel, prioriteit, status
- Claude Code kan de volgorde aanpassen via een apart endpoint
**S-08-03: REST API — taakvolgorde aanpassen**
Als Developer (via Claude Code) wil ik de volgorde van taken kunnen aanpassen via de API, zodat Claude Code een optimale uitvoeringsvolgorde kan bepalen.
Acceptatiecriteria:
- Endpoint: `PATCH /api/stories/:id/tasks/reorder`
- Accepteert een geordende lijst van taak-id's
- Volgorde wordt direct weerspiegeld in de UI
**S-08-04: Implementatieplan vastleggen**
Als Developer (via Claude Code) wil ik een implementatieplan kunnen schrijven naar een story, zodat de ontwerpbeslissingen traceerbaar zijn.
Acceptatiecriteria:
- Endpoint: `POST /api/stories/:id/log`
- Veld: `type: "implementation_plan"`, `content: string`
- Log-entry verschijnt in de story-activiteitenlog in de UI
**S-08-05: Teststatus vastleggen**
Als Developer (via Claude Code) wil ik de uitkomst van testruns kunnen vastleggen in een story, zodat kwaliteitsbewijs per story bewaard blijft.
Acceptatiecriteria:
- Endpoint: `POST /api/stories/:id/log`
- Veld: `type: "test_result"`, `content: string`, `status: "passed" | "failed"`
- Teststatus zichtbaar in de story-activiteitenlog
**S-08-06: Commit-hash vastleggen**
Als Developer (via Claude Code) wil ik de commit-hash na een succesvolle commit kunnen vastleggen in een story, zodat code en planning direct gekoppeld zijn.
Acceptatiecriteria:
- Endpoint: `POST /api/stories/:id/log`
- Veld: `type: "commit"`, `hash: string`, `message: string`
- Commit-hash is klikbaar en linkt naar de git-repo (indien geconfigureerd)
**S-08-07: Story activiteitenlog in UI**
Als gebruiker wil ik per story een activiteitenlog zien met alle vastgelegde implementatieplannen, testresultaten en commits, zodat ik de volledige uitvoeringsgeschiedenis op één plek heb.
Acceptatiecriteria:
- Log toont alle entries in chronologische volgorde
- Elk type entry heeft een eigen visuele stijl (plan, test, commit)
- Log is zichtbaar in de story-detailweergave
- Log is read-only in de UI (schrijven gebeurt via API)
---
## PBI-09 — Infrastructuur & deployment
**Prioriteit:** 1 — Kritiek
**Omschrijving:** De app is deployable op Vercel + Neon (cloud) én volledig lokaal draaibaar zonder externe afhankelijkheden.
### Stories
**S-09-01: Cloud deployment (Vercel + Neon)**
Als Developer wil ik de app deployen op Vercel met een Neon PostgreSQL-database, zodat de app beschikbaar is via een URL.
Acceptatiecriteria:
- `next build` slaagt zonder fouten
- Database-migraties worden uitgevoerd via Prisma
- Environment variables zijn gedocumenteerd in `.env.example`
**S-09-02: Lokale modus**
Als Developer wil ik de app lokaal kunnen draaien met een Neon PostgreSQL-database, zodat de lokale setup overeenkomt met productie.
Acceptatiecriteria:
- `npm run dev` start de app lokaal zonder Vercel of Neon account
- Database wordt aangemaakt via `prisma db push`
- README bevat stap-voor-stap instructies voor lokale setup
**S-09-03: API-token authenticatie**
Als Developer wil ik een API-token kunnen genereren in de app, zodat Claude Code veilig kan communiceren met de REST API.
Acceptatiecriteria:
- Gebruiker kan een API-token aanmaken in de instellingenpagina
- Token wordt eenmalig getoond en daarna niet meer zichtbaar
- Token kan worden ingetrokken
- Alle API-endpoints vereisen een geldig token via `Authorization: Bearer`
---
## Backlog — v2 kandidaten (niet in v1)
| PBI | Omschrijving |
|---|---|
| Daily Scrum scherm | Voortgang per story bijhouden tijdens de Sprint |
| Sprint Review scherm | Demo en feedback vastleggen per story |
| Sprint Retrospective scherm | Reflectie vastleggen per Sprint |
| Meerdere gebruikers per Scrum Team | Uitgebreide auth met rol-gebaseerde permissies |
| Automatische statusupdate na commit | Story op Done zetten via API-aanroep |
| Velocity tracking | Statistieken over meerdere Sprints |
| Notificaties / reminders | Push of e-mailmeldingen |
| Timeline / kalenderweergave | Sprint-planning op kalender |
| Definition of Done per product configureerbaar | Nu vaste instelling; later flexibel |
| Integratie GitHub Issues / Linear | Import/export van PBI's en stories |
---
*Dit document dient als testdata voor de eerste implementatie van de datastructuur.*
*Versie 0.1 — te updaten na Sprint 1 Review.*

View file

@ -1,3 +1,11 @@
---
title: "Agent Instruction Audit"
status: active
audience: [maintainer]
language: en
last_updated: 2026-05-03
---
# Agent Instruction Audit # Agent Instruction Audit
Datum: 2026-04-25 Datum: 2026-04-25
@ -18,7 +26,7 @@ Dit document legt vast welke wijzigingen zijn gecontroleerd, welke documentatie
| Reorder-acties valideren alle IDs binnen de juiste parent-scope | `actions/stories.ts`, `actions/sprints.ts` | `docs/architecture.md`, `docs/patterns/server-action.md`, `docs/patterns/sort-order.md`, `CLAUDE.md`, `AGENTS.md` | | Reorder-acties valideren alle IDs binnen de juiste parent-scope | `actions/stories.ts`, `actions/sprints.ts` | `docs/architecture.md`, `docs/patterns/server-action.md`, `docs/patterns/sort-order.md`, `CLAUDE.md`, `AGENTS.md` |
| Sprint afronden accepteert alleen stories uit de actieve sprint | `actions/sprints.ts` | `docs/architecture.md`, `docs/patterns/server-action.md`, `AGENTS.md` | | Sprint afronden accepteert alleen stories uit de actieve sprint | `actions/sprints.ts` | `docs/architecture.md`, `docs/patterns/server-action.md`, `AGENTS.md` |
| Todo-promotie gebruikt scoped todo lookup en `pbi.product_id` als bron van waarheid | `actions/todos.ts` | `docs/architecture.md`, `docs/patterns/server-action.md`, `CLAUDE.md`, `AGENTS.md` | | Todo-promotie gebruikt scoped todo lookup en `pbi.product_id` als bron van waarheid | `actions/todos.ts` | `docs/architecture.md`, `docs/patterns/server-action.md`, `CLAUDE.md`, `AGENTS.md` |
| `GET /api/products` retourneert ook gedeelde producten via `product_members` | `app/api/products/route.ts` | `README.md`, `docs/functional.md`, `docs/backlog.md`, `docs/patterns/route-handler.md` | | `GET /api/products` retourneert ook gedeelde producten via `product_members` | `app/api/products/route.ts` | `README.md`, `docs/specs/functional.md`, `docs/backlog/index.md`, `docs/patterns/route-handler.md` |
| `sharp` is directe runtime dependency | `package.json`, `package-lock.json` | `README.md`, `docs/architecture.md`, `CLAUDE.md` | | `sharp` is directe runtime dependency | `package.json`, `package-lock.json` | `README.md`, `docs/architecture.md`, `CLAUDE.md` |
| Vercel Analytics is toegevoegd aan de root layout | `app/layout.tsx`, `package.json`, `package-lock.json` | `README.md`, `docs/architecture.md`, `CLAUDE.md` | | Vercel Analytics is toegevoegd aan de root layout | `app/layout.tsx`, `package.json`, `package-lock.json` | `README.md`, `docs/architecture.md`, `CLAUDE.md` |
| `.env.example` ontbrak ondanks verwijzingen in architectuurdocs | `.env.example` | `README.md`, `docs/architecture.md` | | `.env.example` ontbrak ondanks verwijzingen in architectuurdocs | `.env.example` | `README.md`, `docs/architecture.md` |
@ -44,9 +52,9 @@ Concrete regels:
Een codewijziging is niet klaar als de documentatie een oud contract beschrijft. Agents moeten bij elke wijziging nagaan of deze plekken geraakt worden: Een codewijziging is niet klaar als de documentatie een oud contract beschrijft. Agents moeten bij elke wijziging nagaan of deze plekken geraakt worden:
- `README.md`: setup, scripts, dependencies, deployment, API-overzicht. - `README.md`: setup, scripts, dependencies, deployment, API-overzicht.
- `docs/functional.md`: functionele eisen en API-contracten. - `docs/specs/functional.md`: functionele eisen en API-contracten.
- `docs/architecture.md`: stack, datamodel, securitymodel, env vars, deployment. - `docs/architecture.md`: stack, datamodel, securitymodel, env vars, deployment.
- `docs/backlog.md`: "done when"-criteria en scope van backlog-items. - `docs/backlog/index.md`: "done when"-criteria en scope van backlog-items.
- `docs/patterns/`: herbruikbare implementatiepatronen. - `docs/patterns/`: herbruikbare implementatiepatronen.
- `CLAUDE.md` en `AGENTS.md`: agent-regels die de fout in de toekomst voorkomen. - `CLAUDE.md` en `AGENTS.md`: agent-regels die de fout in de toekomst voorkomen.
@ -100,9 +108,9 @@ Sinds ronde 1 (2026-04-25) is er substantieel werk geland dat de agent-workflow
- **ST-509** Todo description (max 2000) - **ST-509** Todo description (max 2000)
- **ST-511** Entity codes voor Product/PBI/Story (auto-default + manual override + retry op race condition) - **ST-511** Entity codes voor Product/PBI/Story (auto-default + manual override + retry op race condition)
- **ST-512** REST API uitgebreid met `code`, `description`, `implementation_plan` in alle endpoints - **ST-512** REST API uitgebreid met `code`, `description`, `implementation_plan` in alle endpoints
- **ST-513** API hardening voor Claude Code: `GET /api/health`, `GET /api/products/:id/claude-context`, lowercase status-enums op de API-grens, `StoryLog.metadata` JSONB, validatie-fouten van `400``422`, nieuwe `docs/api.md` - **ST-513** API hardening voor Claude Code: `GET /api/health`, `GET /api/products/:id/claude-context`, lowercase status-enums op de API-grens, `StoryLog.metadata` JSONB, validatie-fouten van `400``422`, nieuwe `docs/api/rest-contract.md`
- **PR #2 Codex-review-saga** — 8 testbestanden faalden bij de contract-flip; tests werden niet meebijgewerkt. Twee P2-issues van Codex: malformed JSON moet `400` blijven (P2.1), en `status: review` werd geaccepteerd terwijl de sprint-UI er niet mee om kan gaan (P2.2) - **PR #2 Codex-review-saga** — 8 testbestanden faalden bij de contract-flip; tests werden niet meebijgewerkt. Twee P2-issues van Codex: malformed JSON moet `400` blijven (P2.1), en `status: review` werd geaccepteerd terwijl de sprint-UI er niet mee om kan gaan (P2.2)
- **M7: scrum4me-mcp** — aparte MCP-server repo (`madhura68/scrum4me-mcp`) met 9 tools en 1 prompt voor Claude Code, schema gedeeld via git submodule - **M7: mcp** — aparte MCP-server repo (`madhura68/scrum4me-mcp`) met 9 tools en 1 prompt voor Claude Code, schema gedeeld via git submodule
- **lib/code.ts vs lib/code-server.ts** — gesplitst om client-bundle vrij te houden van `pg` (gaf eerst `Module not found: 'dns'` build-error) - **lib/code.ts vs lib/code-server.ts** — gesplitst om client-bundle vrij te houden van `pg` (gaf eerst `Module not found: 'dns'` build-error)
- **Wekelijkse schema-drift cron** (`trig_015FFUnxjz9WMuhhWNGBQKFD`) — remote agent die ma 08:00 Amsterdam de MCP-submodule syncet en typecheckt - **Wekelijkse schema-drift cron** (`trig_015FFUnxjz9WMuhhWNGBQKFD`) — remote agent die ma 08:00 Amsterdam de MCP-submodule syncet en typecheckt
@ -110,13 +118,13 @@ Sinds ronde 1 (2026-04-25) is er substantieel werk geland dat de agent-workflow
| Wijziging | Code-locatie | Documentatie bijgewerkt | | Wijziging | Code-locatie | Documentatie bijgewerkt |
|---|---|---| |---|---|---|
| Lowercase status-enums op REST-grens, mappers naar DB-enum | `lib/task-status.ts`, `app/api/tasks/[id]/route.ts`, `app/api/sprints/[id]/tasks/route.ts`, `app/api/products/[id]/next-story/route.ts` | `docs/api.md`, `CLAUDE.md` | | Lowercase status-enums op REST-grens, mappers naar DB-enum | `lib/task-status.ts`, `app/api/tasks/[id]/route.ts`, `app/api/sprints/[id]/tasks/route.ts`, `app/api/products/[id]/next-story/route.ts` | `docs/api/rest-contract.md`, `CLAUDE.md` |
| 400 (malformed JSON) gescheiden van 422 (zod-validatie) | `app/api/tasks/[id]/route.ts`, `app/api/stories/[id]/tasks/reorder/route.ts`, `app/api/todos/route.ts`, `app/api/stories/[id]/log/route.ts` | `docs/api.md`, `CLAUDE.md` | | 400 (malformed JSON) gescheiden van 422 (zod-validatie) | `app/api/tasks/[id]/route.ts`, `app/api/stories/[id]/tasks/reorder/route.ts`, `app/api/todos/route.ts`, `app/api/stories/[id]/log/route.ts` | `docs/api/rest-contract.md`, `CLAUDE.md` |
| `status: review` geweigerd door PATCH `/api/tasks/:id` zolang sprint-UI geen REVIEW rendert | `app/api/tasks/[id]/route.ts` | `docs/api.md` | | `status: review` geweigerd door PATCH `/api/tasks/:id` zolang sprint-UI geen REVIEW rendert | `app/api/tasks/[id]/route.ts` | `docs/api/rest-contract.md` |
| Entity codes (Product/PBI/Story) met auto-default + retry-on-P2002 | `actions/products.ts`, `actions/pbis.ts`, `actions/stories.ts`, `lib/code.ts`, `lib/code-server.ts` | `docs/backlog.md` (ST-511) | | Entity codes (Product/PBI/Story) met auto-default + retry-on-P2002 | `actions/products.ts`, `actions/pbis.ts`, `actions/stories.ts`, `lib/code.ts`, `lib/code-server.ts` | `docs/backlog/index.md` (ST-511) |
| `StoryLog.metadata` JSONB | `prisma/schema.prisma`, `prisma/migrations/20260426214905_add_story_log_metadata/`, `app/api/stories/[id]/log/route.ts` | `docs/api.md` | | `StoryLog.metadata` JSONB | `prisma/schema.prisma`, `prisma/migrations/20260426214905_add_story_log_metadata/`, `app/api/stories/[id]/log/route.ts` | `docs/api/rest-contract.md` |
| Health- en bundled-context endpoints voor Claude Code | `app/api/health/route.ts`, `app/api/products/[id]/claude-context/route.ts` | `docs/api.md`, `CLAUDE.md` | | Health- en bundled-context endpoints voor Claude Code | `app/api/health/route.ts`, `app/api/products/[id]/claude-context/route.ts` | `docs/api/rest-contract.md`, `CLAUDE.md` |
| MCP-server gepubliceerd als aparte repo | `madhura68/scrum4me-mcp` (extern) | `CLAUDE.md` (sectie MCP-integratie), `docs/backlog.md` (M7) | | MCP-server gepubliceerd als aparte repo | `madhura68/scrum4me-mcp` (extern) | `CLAUDE.md` (sectie MCP-integratie), `docs/backlog/index.md` (M7) |
## Nieuwe preventieve regels ## Nieuwe preventieve regels
@ -140,7 +148,7 @@ API exposeert lowercase, DB houdt UPPER_SNAKE. Conversie uitsluitend via `lib/ta
- `400`: parse-fout op `request.json()` — wikkel altijd in `try/catch` - `400`: parse-fout op `request.json()` — wikkel altijd in `try/catch`
- `422`: zod-validatie of well-formed-maar-niet-acceptabel (bv. `task_id` van andere story) - `422`: zod-validatie of well-formed-maar-niet-acceptabel (bv. `task_id` van andere story)
- `403`: demo-token op write - `403`: demo-token op write
- Documenteer per endpoint in `docs/api.md` - Documenteer per endpoint in `docs/api/rest-contract.md`
### Client/server module-boundary ### Client/server module-boundary
@ -151,7 +159,7 @@ API exposeert lowercase, DB houdt UPPER_SNAKE. Conversie uitsluitend via `lib/ta
Naast bestaande lint/test/build-stappen voor MCP-relevante wijzigingen ook: Naast bestaande lint/test/build-stappen voor MCP-relevante wijzigingen ook:
```bash ```bash
# In scrum4me-mcp na een schema-wijziging in Scrum4Me: # In mcp na een schema-wijziging in Scrum4Me:
npm run sync-schema && npm run prisma:generate && npm run typecheck npm run sync-schema && npm run prisma:generate && npm run typecheck
``` ```

1601
docs/design/styling.md Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,3 +1,11 @@
---
title: "Scrum4Me — Functionele Specificatie"
status: active
audience: [maintainer, contributor]
language: nl
last_updated: 2026-05-03
---
# Scrum4Me — Functionele Specificatie # Scrum4Me — Functionele Specificatie
**Versie:** 0.2 — april 2026 **Versie:** 0.2 — april 2026
@ -468,7 +476,7 @@ Wanneer Claude Code tijdens het implementeren van een story een keuze niet uit d
**Data:** **Data:**
- Nieuw: `claude_questions` (id, story_id, task_id?, product_id, asked_by, question, options?, status, answer?, answered_by?, answered_at?, created_at, expires_at) - Nieuw: `claude_questions` (id, story_id, task_id?, product_id, asked_by, question, options?, status, answer?, answered_by?, answered_at?, created_at, expires_at)
- Postgres-trigger op `claude_questions` publiceert via `pg_notify('scrum4me_changes', ...)` - Postgres-trigger op `claude_questions` publiceert via `pg_notify('scrum4me_changes', ...)`
- Nieuwe MCP-tools in scrum4me-mcp: `ask_user_question`, `get_question_answer`, `list_open_questions`, `cancel_question` - Nieuwe MCP-tools in mcp: `ask_user_question`, `get_question_answer`, `list_open_questions`, `cancel_question`
--- ---
@ -648,3 +656,778 @@ Demo-gebruikers zien de knoppen maar krijgen een toast "Niet beschikbaar in demo
- **Product verlaten**: wanneer een lid het product verlaat, wordt hun `active_product_id` gecleard. - **Product verlaten**: wanneer een lid het product verlaat, wordt hun `active_product_id` gecleard.
- **Lid verwijderen**: wanneer een eigenaar een lid verwijdert, wordt dat lid's `active_product_id` gecleard. - **Lid verwijderen**: wanneer een eigenaar een lid verwijdert, wordt dat lid's `active_product_id` gecleard.
- **Stale referentie**: als bij een request `active_product_id` verwijst naar een gearchiveerd of onbereikbaar product (bijv. toegang ingetrokken in een andere sessie), cleared de layout de referentie server-side en redirect naar `/dashboard` met de toast "Je actieve product is niet meer beschikbaar". - **Stale referentie**: als bij een request `active_product_id` verwijst naar een gearchiveerd of onbereikbaar product (bijv. toegang ingetrokken in een andere sessie), cleared de layout de referentie server-side en redirect naar `/dashboard` met de toast "Je actieve product is niet meer beschikbaar".
---
## Solo Panel
> **Doel:** een persoonlijk Kanban-bord per product dat de taken toont van stories die geclaimd zijn door de ingelogde developer. Bord werkt met drie kolommen (TO_DO / IN_PROGRESS / DONE), drag-and-drop tussen kolommen, en koppelt aan een nieuw `Story.assignee_id` veld.
>
> **Scope v1:** geen REVIEW-status, geen multi-product aggregatie, geen taak-level overrides. Story-level assignment volstaat. Desktop-first conform ST-606 — onder 1024px tonen we dezelfde "te smal scherm"-melding als de rest van de app.
>
> **Versie:** v2 — verwerkt antwoorden uit `backlog.md` over sessie-flag, bestaande Server Actions en desktop-first scope.
---
## Wat veranderde t.o.v. v1
| Onderdeel | v1 aanname | v2 (op basis van backlog) |
|---|---|---|
| `isDemo` toegang | DB-lookup of session, ambivalent | **Komt uit `session.isDemo` (ST-006, ST-604)** — geen DB-call |
| Implementation_plan editen | Bestaande Server Action of API | **Nieuwe `updateTaskPlanAction`** (gericht, optimistisch-vriendelijk) |
| Mobiel | Optionele chunk 13 (tab-strip) | **Geen mobile UI**; volg ST-606 desktop-first patroon |
| Toast | Algemeen genoemd | **Sonner is geïnstalleerd (ST-603)** — gebruik consistent |
| Pending states | Niet uitgewerkt | **`useFormStatus` of `useTransition`** zoals ST-601 voorschrijft |
| Demo-tooltip tekst | "Read-only in demo-modus" | **"Niet beschikbaar in demo-modus"** zoals ST-604 |
| Sprint Board referentie | Generieke "sprint board" | **ST-313 drie-panelen Sprint Board** — assignee-UI komt in middenpaneel |
---
## 1. Datamodel — Prisma migratie
Eén veld erbij, één index erbij. Geen enum-wijzigingen.
```prisma
model Story {
// ... bestaande velden ongewijzigd ...
assignee_id String?
assignee User? @relation("StoryAssignee", fields: [assignee_id], references: [id], onDelete: SetNull)
@@index([sprint_id, assignee_id]) // hot path: solo-bord query
// bestaande indexen ongewijzigd
}
model User {
// ... bestaande velden ongewijzigd ...
assigned_stories Story[] @relation("StoryAssignee")
}
```
**Migratie:**
```bash
npx prisma migrate dev --name add_story_assignee
```
**onDelete-keuze:** `SetNull` zodat verwijderen van een user de stories behoudt (assignee valt terug naar "team"). Cascade zou stories vernietigen — niet wat we willen.
**Named relation `"StoryAssignee"`:** voorkomt botsing met andere mogelijke User↔Story relations in de toekomst.
---
## 2. Auth-helper (`lib/auth.ts` uitbreiding)
`isDemo` zit al in de sessiecookie sinds ST-006 — geen DB-lookup nodig.
```typescript
import { getIronSession } from 'iron-session'
import { cookies } from 'next/headers'
import { sessionOptions, type SessionData } from '@/lib/session'
import { prisma } from '@/lib/prisma'
export async function getSession() {
return getIronSession<SessionData>(await cookies(), sessionOptions)
}
export async function requireUser() {
const session = await getSession()
if (!session?.userId) throw new Error('Niet ingelogd')
return session
}
export async function requireWriter() {
const session = await requireUser()
if (session.isDemo) throw new Error('Niet beschikbaar in demo-modus')
return session
}
export async function requireProductAccess(productId: string) {
const session = await requireUser()
const product = await prisma.product.findFirst({
where: {
id: productId,
OR: [
{ user_id: session.userId }, // owner
{ members: { some: { user_id: session.userId } } }, // member
],
},
select: { id: true },
})
if (!product) throw new Error('Geen toegang tot dit product')
return session
}
export async function requireProductWriter(productId: string) {
const session = await requireProductAccess(productId)
if (session.isDemo) throw new Error('Niet beschikbaar in demo-modus')
return session
}
```
**Patroon-uitleg:**
- `requireUser` — ingelogd, anders fout
- `requireWriter` — ingelogd én niet-demo
- `requireProductAccess` — ingelogd én lid (read)
- `requireProductWriter` — ingelogd én lid én niet-demo (write)
**Afhankelijkheid:** controleer of bestaande `actions/*.ts` een eigen lokale `getSession` definiëren. Zo ja, optioneel migreren bij gelegenheid (geen blocker).
---
## 3. Server Actions
### 3a. Story-claim acties (`actions/stories.ts` uitbreiding)
```typescript
'use server'
import { z } from 'zod'
import { revalidatePath } from 'next/cache'
import { prisma } from '@/lib/prisma'
import { requireProductWriter } from '@/lib/auth'
// ---------------------------------------------------------------------------
const claimSchema = z.object({
storyId: z.string().cuid(),
productId: z.string().cuid(),
})
export async function claimStoryAction(input: z.infer<typeof claimSchema>) {
const { storyId, productId } = claimSchema.parse(input)
const session = await requireProductWriter(productId)
await prisma.story.update({
where: { id: storyId, product_id: productId }, // tenant-guard
data: { assignee_id: session.userId },
})
revalidatePath(`/products/${productId}/sprint`)
revalidatePath(`/products/${productId}/solo`)
}
// ---------------------------------------------------------------------------
export async function unclaimStoryAction(input: z.infer<typeof claimSchema>) {
const { storyId, productId } = claimSchema.parse(input)
await requireProductWriter(productId)
await prisma.story.update({
where: { id: storyId, product_id: productId },
data: { assignee_id: null },
})
revalidatePath(`/products/${productId}/sprint`)
revalidatePath(`/products/${productId}/solo`)
}
// ---------------------------------------------------------------------------
const reassignSchema = z.object({
storyId: z.string().cuid(),
productId: z.string().cuid(),
targetUserId: z.string().cuid(),
})
export async function reassignStoryAction(input: z.infer<typeof reassignSchema>) {
const { storyId, productId, targetUserId } = reassignSchema.parse(input)
await requireProductWriter(productId)
// Valideer dat target-user lid is van het product (anders cross-tenant assignment)
const isMember = await prisma.product.findFirst({
where: {
id: productId,
OR: [
{ user_id: targetUserId },
{ members: { some: { user_id: targetUserId } } },
],
},
select: { id: true },
})
if (!isMember) throw new Error('Doel-gebruiker is geen lid van dit product')
await prisma.story.update({
where: { id: storyId, product_id: productId },
data: { assignee_id: targetUserId },
})
revalidatePath(`/products/${productId}/sprint`)
revalidatePath(`/products/${productId}/solo`)
}
// ---------------------------------------------------------------------------
const bulkClaimSchema = z.object({ productId: z.string().cuid() })
export async function claimAllUnassignedInActiveSprintAction(
input: z.infer<typeof bulkClaimSchema>,
) {
const { productId } = bulkClaimSchema.parse(input)
const session = await requireProductWriter(productId)
const activeSprint = await prisma.sprint.findFirst({
where: { product_id: productId, status: 'ACTIVE' },
select: { id: true },
})
if (!activeSprint) throw new Error('Geen actieve sprint gevonden')
const result = await prisma.story.updateMany({
where: {
sprint_id: activeSprint.id,
product_id: productId,
assignee_id: null,
},
data: { assignee_id: session.userId },
})
revalidatePath(`/products/${productId}/sprint`)
revalidatePath(`/products/${productId}/solo`)
return { claimed: result.count }
}
```
### 3b. Implementation plan editen (`actions/tasks.ts` uitbreiding)
Bestaande `updateTaskStatus` (ST-310) en `updateTask` (ST-311) blijven ongewijzigd. We voegen één nieuwe gerichte action toe:
```typescript
'use server'
const planSchema = z.object({
taskId: z.string().cuid(),
productId: z.string().cuid(), // voor tenant-guard
implementationPlan: z.string().max(20000),
})
export async function updateTaskPlanAction(input: z.infer<typeof planSchema>) {
const { taskId, productId, implementationPlan } = planSchema.parse(input)
await requireProductWriter(productId)
// Tenant-guard via geneste relatie
await prisma.task.update({
where: {
id: taskId,
story: { product_id: productId }, // verifieer dat task bij product hoort
},
data: { implementation_plan: implementationPlan },
})
revalidatePath(`/products/${productId}/solo`)
revalidatePath(`/products/${productId}/sprint`)
}
```
**Waarom een aparte action:** korter, optimistisch-vriendelijk (kleine payload, lage latency), past bij save-on-blur in de detail-dialoog. De bestaande `updateTask` is voor volledige edits via een formulier.
**Toast/UX:** geen success-toast (te frequent bij save-on-blur). Wel error-toast bij fout. Indicator in dialoog (*"Bezig met opslaan…"* / *"Opgeslagen"*).
---
## 4. Routes en pagina's
```
app/
├── solo/
│ └── page.tsx # /solo → redirect of picker
└── products/
└── [id]/
├── sprint/page.tsx # bestaand (ST-313 drie-panelen) — krijgt UI-uitbreidingen
└── solo/
└── page.tsx # /products/[id]/solo → het bord
```
### 4a. `/solo` — Redirect-pagina
Server Component. Leest cookie `lastProductId`, valideert toegang, redirect.
```typescript
// app/solo/page.tsx
import { cookies } from 'next/headers'
import { redirect } from 'next/navigation'
import { prisma } from '@/lib/prisma'
import { requireUser } from '@/lib/auth'
import { ProductPicker } from '@/components/solo/product-picker'
export default async function SoloRedirectPage() {
const session = await requireUser()
const lastProductId = (await cookies()).get('lastProductId')?.value
if (lastProductId) {
const valid = await prisma.product.findFirst({
where: {
id: lastProductId,
archived: false,
OR: [
{ user_id: session.userId },
{ members: { some: { user_id: session.userId } } },
],
},
select: { id: true },
})
if (valid) redirect(`/products/${valid.id}/solo`)
}
// Geen valide cookie → toon picker
const products = await prisma.product.findMany({
where: {
archived: false,
OR: [
{ user_id: session.userId },
{ members: { some: { user_id: session.userId } } },
],
},
select: { id: true, name: true },
orderBy: { updated_at: 'desc' },
})
return <ProductPicker products={products} basePath="/solo" />
}
```
### 4b. `/products/[id]/solo` — Het Solo Bord
Server Component. Doet alle queries en geeft data door aan een client-side `<SoloBoard>`.
```typescript
// app/products/[id]/solo/page.tsx
import { notFound } from 'next/navigation'
import { prisma } from '@/lib/prisma'
import { requireUser } from '@/lib/auth'
import { setLastProductCookie } from '@/lib/cookies'
import { SoloBoard } from '@/components/solo/solo-board'
import { NoActiveSprint } from '@/components/solo/no-active-sprint'
export default async function SoloPage({
params,
}: {
params: Promise<{ id: string }>
}) {
const { id } = await params
const session = await requireUser()
await setLastProductCookie(id)
const product = await prisma.product.findFirst({
where: {
id,
OR: [
{ user_id: session.userId },
{ members: { some: { user_id: session.userId } } },
],
},
select: { id: true, name: true },
})
if (!product) notFound()
const activeSprint = await prisma.sprint.findFirst({
where: { product_id: id, status: 'ACTIVE' },
select: { id: true, sprint_goal: true },
})
if (!activeSprint) return <NoActiveSprint product={product} />
// Parallel: eigen taken + count ongeclaimde stories
const [tasks, unassignedStoryCount] = await Promise.all([
prisma.task.findMany({
where: {
sprint_id: activeSprint.id,
story: { assignee_id: session.userId },
},
select: {
id: true,
title: true,
priority: true,
sort_order: true,
status: true,
description: true,
implementation_plan: true,
story: { select: { id: true, title: true } },
},
orderBy: [{ priority: 'desc' }, { sort_order: 'asc' }],
}),
prisma.story.count({
where: { sprint_id: activeSprint.id, assignee_id: null },
}),
])
return (
<SoloBoard
product={product}
sprint={activeSprint}
tasks={tasks}
unassignedStoryCount={unassignedStoryCount}
isDemo={session.isDemo}
/>
)
}
```
**Performance:**
- Query gebruikt `[sprint_id, assignee_id]` index die we toevoegen → snelle filter
- `Promise.all` parallelliseert de twee onafhankelijke queries
- `select` projectie houdt payload klein
---
## 5. Cookie-helper (`lib/cookies.ts`)
```typescript
'use server'
import { cookies } from 'next/headers'
const ONE_MONTH = 60 * 60 * 24 * 30
export async function setLastProductCookie(productId: string) {
const store = await cookies()
store.set('lastProductId', productId, {
httpOnly: true,
sameSite: 'lax',
secure: process.env.NODE_ENV === 'production',
maxAge: ONE_MONTH,
path: '/',
})
}
```
---
## 6. Sprint Board (ST-313) uitbreidingen
In het middenpaneel (Sprint Backlog) van het drie-panelen Sprint Board komen de assignee-UI elementen.
### 6a. Story-kaart op het Sprint Backlog paneel
Nieuwe elementen op elke story-kaart:
```
┌──────────────────────────────────────────────────┐
│ ⚡ Story title [···] │ ← actie-menu rechts
│ Some PBI · 3 taken │
│ ───────────────────────────────────────────── │
│ [👤 jan.visser] of [— Niet geclaimd] │ ← assignee-chip
└──────────────────────────────────────────────────┘
```
**Assignee-chip:** klein component met `<UserAvatar size="xs">` + username, of een muted badge (`bg-muted text-muted-foreground`) als `assignee_id === null`.
**Actie-menu (shadcn `DropdownMenu`):**
- *Pak op*`claimStoryAction` — zichtbaar als ongeclaimd of niet-jij
- *Geef terug aan team*`unclaimStoryAction` — zichtbaar als geclaimd
- *Wijs toe aan ▶* (submenu met members) → `reassignStoryAction`
**Demo-modus:** hele dropdown disabled met tooltip *"Niet beschikbaar in demo-modus"* (consistent met ST-604).
### 6b. Bovenaan het Sprint Backlog paneel
```tsx
<div className="flex items-center justify-between">
<h2>Sprint Backlog</h2>
<Button
onClick={handleClaimAll}
disabled={unassignedCount === 0 || isDemo}
variant="outline"
>
Claim alle ongeclaimde stories ({unassignedCount})
</Button>
</div>
```
Na succes: Sonner-toast *"X stories geclaimd"* (gewone success-toast, niet drag-and-drop frequentie). Bij demo: knop disabled met tooltip *"Niet beschikbaar in demo-modus"*.
---
## 7. Solo Paneel componenten
```
components/solo/
├── solo-board.tsx # Client root, dnd context, layout
├── solo-column.tsx # Drop target per status
├── solo-task-card.tsx # Draggable kaart (bestaande task-card hergebruiken)
├── task-detail-dialog.tsx # Shadcn Dialog
├── unassigned-stories-sheet.tsx # Shadcn Sheet
├── no-active-sprint.tsx # Empty state
└── product-picker.tsx # Voor /solo zonder cookie
```
### 7a. `<SoloBoard>` — root component
```typescript
'use client'
interface Props {
product: { id: string; name: string }
sprint: { id: string; sprint_goal: string }
tasks: TaskWithStory[]
unassignedStoryCount: number
isDemo: boolean
}
export function SoloBoard({ product, sprint, tasks, unassignedStoryCount, isDemo }: Props) {
// Zustand store gehydrateerd met initiële taken
// DndContext (overslaan als isDemo) met sensor + collision detection
// Header: productnaam, sprint goal, knop "Toon openstaande stories (N)"
// Drie kolommen in een grid (md:grid-cols-3)
}
```
### 7b. Zustand store (`stores/solo-store.ts`)
Volgt het patroon van `usePlannerStore` (ST-201): `init*`, `optimistic*`, `rollback*`.
```typescript
import { create } from 'zustand'
import type { TaskStatus } from '@prisma/client'
interface SoloState {
tasks: TaskWithStory[]
initTasks: (tasks: TaskWithStory[]) => void
optimisticMove: (taskId: string, toStatus: TaskStatus) => TaskStatus | null // returns prev for rollback
rollback: (taskId: string, prevStatus: TaskStatus) => void
updatePlan: (taskId: string, plan: string) => void
}
export const useSoloStore = create<SoloState>((set, get) => ({
tasks: [],
initTasks: (tasks) => set({ tasks }),
optimisticMove: (taskId, toStatus) => {
const task = get().tasks.find(t => t.id === taskId)
if (!task) return null
const prev = task.status
set({ tasks: get().tasks.map(t => t.id === taskId ? { ...t, status: toStatus } : t) })
return prev
},
rollback: (taskId, prevStatus) => {
set({ tasks: get().tasks.map(t => t.id === taskId ? { ...t, status: prevStatus } : t) })
},
updatePlan: (taskId, plan) => {
set({ tasks: get().tasks.map(t => t.id === taskId ? { ...t, implementation_plan: plan } : t) })
},
}))
```
### 7c. Drag-and-drop (dnd-kit)
```typescript
function handleDragEnd(event: DragEndEvent) {
const { active, over } = event
if (!over) return
const taskId = String(active.id)
const toStatus = String(over.id) as TaskStatus // kolom-id = status enum-value
if (!['TO_DO', 'IN_PROGRESS', 'DONE'].includes(toStatus)) return
const prev = useSoloStore.getState().optimisticMove(taskId, toStatus)
if (prev === null || prev === toStatus) return
startTransition(async () => {
try {
await updateTaskStatusAction(taskId, toStatus)
} catch (err) {
useSoloStore.getState().rollback(taskId, prev)
toast.error('Status bijwerken mislukt — taak teruggeplaatst')
}
})
}
```
**Sensor-keuze:** `PointerSensor` met `activationConstraint: { distance: 5 }` om accidentele drags op klik te voorkomen. Klik = open dialog, drag = verplaats.
**Collision detection:** `closestCorners` voor kolom-niveau drops; geen sortering binnen kolom in v1.
**Toast-strategie:** consistent met ST-603 — geen success-toast bij drag (te frequent), wél error-toast bij rollback.
**Demo-user:** sla de hele DndContext over en wrap kaart-componenten zonder draggable. Klik werkt nog wel (lezen mag).
### 7d. `<SoloColumn>`
Status-token mapping (briefing):
| Status | Header background |
|---|---|
| `TO_DO` | `bg-status-todo/15 text-status-todo border-status-todo/30` |
| `IN_PROGRESS` | `bg-status-in-progress/15 text-status-in-progress border-status-in-progress/30` |
| `DONE` | `bg-status-done/15 text-status-done border-status-done/30` |
### 7e. `<SoloTaskCard>` — hergebruik bestaande task-card
Bestaande task-card uit het sprint board (ST-313 rechterpaneel) hergebruiken. Pas zo nodig aan:
- Linker-rand of dot met `bg-priority-{level}` voor prioriteit
- Taaktitel (`font-medium`, `truncate`)
- Story-titel (`text-sm text-muted-foreground`, `truncate`)
- Optionele `showProduct?: boolean` prop (off op product-specifieke pagina; reservering voor toekomstig multi-product bord)
Klik → opent `<TaskDetailDialog>` met deze taak.
### 7f. `<TaskDetailDialog>`
Shadcn `Dialog`. Inhoud:
- Header: taaktitel + statusbadge (gekleurd via MD3 tokens)
- Sectie *Beschrijving* (read-only `<p>` of formatted block — volg bestaand task-detailpatroon)
- Sectie *Implementatieplan*: `<Textarea>` met save-on-blur
- On blur: `updateTaskPlanAction({ taskId, productId, implementationPlan })`
- Indicator rechtsonder: *"Bezig met opslaan…"* tijdens transition, *"Opgeslagen"* daarna (vervaagt na 2s)
- Bij fout: error-toast + waarde rollback in store
- Footer: link *"Open in Sprint Board ↗"* naar `/products/[id]/sprint?storyId=...`
- Demo-modus: textarea heeft `readOnly` + tooltip *"Niet beschikbaar in demo-modus"*
```typescript
function handleBlur(plan: string) {
if (plan === task.implementation_plan) return // geen no-op call
startTransition(async () => {
try {
await updateTaskPlanAction({ taskId: task.id, productId, implementationPlan: plan })
useSoloStore.getState().updatePlan(task.id, plan)
} catch (err) {
toast.error('Opslaan mislukt')
}
})
}
```
Markdown-rendering voor implementatieplan kan in v2; voor v1 plain text in textarea — sneller te bouwen, past bij scope.
### 7g. `<UnassignedStoriesSheet>`
Shadcn `Sheet` (slide-out van rechts). Trigger: knop bovenaan het bord met badge `(N)`.
Inhoud:
- Lijst van ongeclaimde stories in actieve sprint, met titel + taakaantal
- Per item: knop *"Pak op"*`claimStoryAction` → revalidate → Sonner toast
- Sheet blijft open tot user 'm sluit (zodat meerdere achter elkaar claimen kan)
- Lege staat: *"Geen ongeclaimde stories. Lekker bezig!"*
`useFormStatus` op de claim-knoppen voor pending state (ST-601).
### 7h. `<NoActiveSprint>` — empty state
Geen ACTIVE sprint: nette empty-state met titel, korte uitleg en link naar productpagina om er een te starten (ST-302 stappen).
---
## 8. `<UserAvatar>` component (nieuw, herbruikbaar)
```
components/ui/user-avatar.tsx
```
```typescript
interface Props {
userId: string
username: string
size?: 'xs' | 'sm' | 'md' | 'lg'
className?: string
}
export function UserAvatar({ userId, username, size = 'md', className }: Props) {
const sizeClasses = {
xs: 'h-5 w-5 text-[10px]',
sm: 'h-6 w-6 text-xs',
md: 'h-8 w-8 text-sm',
lg: 'h-10 w-10 text-base',
}
const initials = username.slice(0, 2).toUpperCase()
return (
<Avatar className={cn(sizeClasses[size], className)}>
<AvatarImage
src={`/api/users/${userId}/avatar`}
alt={username}
/>
<AvatarFallback className="bg-primary-container text-primary">
{initials}
</AvatarFallback>
</Avatar>
)
}
```
Gebaseerd op shadcn `<Avatar>`. Fallback in MD3-token (`bg-primary-container`).
**Aandachtspunt:** als `/api/users/[id]/avatar` 404 returnt (user heeft geen avatar gezet), valt shadcn automatisch terug op `<AvatarFallback>` met initialen. Test dit gedrag — anders forceer je via `onError`.
**Hergebruik:** dit component is ook nuttig in toekomstige plekken (story-detail, instellingen, sprint board kaarten) — geen Solo-specifieke component.
---
## 9. Demo-modus
Eenvoudig nu we weten dat `isDemo` in de sessiecookie zit:
**Drie plekken waar `isDemo` ertoe doet:**
1. **Server Actions**`requireProductWriter` (en `requireWriter`) throwt early met *"Niet beschikbaar in demo-modus"*. Doe je niets, dan kan een demo-user via gespoofte requests toch wijzigen.
2. **UI-knoppen** — disabled + tooltip *"Niet beschikbaar in demo-modus"* (ST-604 conventie). Pass `isDemo` als prop door vanaf de Server Component.
3. **DndContext** — wrap kaarten zonder `useDraggable` als `isDemo`, of zet `disabled` op de hele context.
**Seed-vereiste:** in `prisma/seed.ts` (ST-004) zorgen dat de demo-user (`is_demo = true`) een product heeft met:
- Een ACTIVE sprint
- Stories met `assignee_id = demoUser.id` en bijbehorende taken in alle drie statussen (om bord werkend te tonen)
- Minstens 1 ongeclaimde story (om "Toon openstaande" te demonstreren — demo-user kan niet claimen, ziet wel hoe het werkt)
---
## 10. Navbar
```tsx
// components/navbar.tsx (uitbreiding)
<NavLink href="/solo" icon={<UserSquare className="h-4 w-4" />}>
Solo
</NavLink>
```
Plek: tussen "Producten" en "Todos" (of zoals layout het bepaalt). Altijd zichtbaar voor ingelogde users — geen product-context nodig, die kiest de redirect-handler zelf.
---
## 11. Werkvolgorde voor Claude Code (chunks)
Elke chunk komt overeen met een story uit M3.5 in de backlog en is **afzonderlijk reviewbaar en commitbaar**.
| # | Story | Inhoud | Verifiëer met |
|---|---|---|---|
| 1 | **ST-350** | Schema-migratie + auth-helpers | `prisma migrate dev` slaagt; helpers werken vanuit testbestand |
| 2 | **ST-351** | `<UserAvatar>` component | Visuele check op 4 sizes; fallback bij ontbrekende avatar |
| 3 | **ST-352** | Story-claim Server Actions (4 acties) | Aanroepen vanuit Sprint Board of test-route; demo-guard werkt |
| 4 | **ST-353** | Sprint Board: assignee-chip + dropdown | Klikken claimt; demo-user krijgt disabled tooltip |
| 5 | **ST-354** | Sprint Board: bulk-claim knop + count | Werkt in regular/demo (disabled) sessie + toast |
| 6 | **ST-355** | Solo route + queries + empty states + cookie | `/solo` redirect werkt; pagina toont juiste taken |
| 7 | **ST-356** | Solo Kanban + Zustand + DnD | Sleep tussen kolommen, status persisteert; netwerk-fail → rollback |
| 8 | **ST-357** | Task detail-dialoog + `updateTaskPlanAction` | Edit, blur, refresh: persisteert; demo: read-only |
| 9 | **ST-358** | Openstaande stories sheet | Sheet opent met N items; claimen werkt; lege staat correct |
| 10 | **ST-359** | Navbar-link "Solo" | Klik gaat naar `/solo` (en redirect verder) |
| 11 | **ST-360** | Demo-seed uitbreiden | Login als demo, Solo bord toont werkende staat |
**Bouwvolgorde-inzicht:** chunks 1-5 leveren al **op het Sprint Board** (ST-313) een werkend assignment-systeem. Daar is een natuurlijke release-grens. Chunks 6-9 vormen het Solo Paneel zelf. Chunks 10-11 zijn polish & demo.
---
## 12. Acceptatiecriteria (volledig v1)
**Functioneel:**
- [ ] Een user kan op het Sprint Board een story claimen, teruggeven, of aan een andere member toewijzen
- [ ] Een user kan met één klik alle ongeclaimde stories in de actieve sprint claimen
- [ ] `/solo` redirect naar laatst-bezochte product, met fallback naar product-picker
- [ ] Solo-bord toont alle taken van geclaimde stories in de actieve sprint, gegroepeerd in 3 kolommen
- [ ] Drag-and-drop tussen kolommen update status, met optimistische UI en rollback bij fout
- [ ] Klik op taakkaart opent dialoog met bewerkbaar implementatieplan (save-on-blur)
- [ ] Knop bovenaan toont openstaande stories en laat ze individueel claimen
- [ ] Navbar-link "Solo" altijd zichtbaar voor ingelogde users
**Niet-functioneel:**
- [ ] Demo-user kan lezen maar niets muteren — alle Server Actions throwen, alle knoppen disabled met tooltip *"Niet beschikbaar in demo-modus"*
- [ ] Membership-check werkt voor zowel owner (`Product.user_id`) als members (`ProductMember`)
- [ ] Reassignment kan alleen naar geldige product-members
- [ ] Foutberichten in het Nederlands voor eindgebruikers
- [ ] Stylingregels uit briefing (MD3-tokens) consistent toegepast
- [ ] Desktop-first; volgt ST-606 melding bij < 1024px
**Performance:**
- [ ] Solo-pagina laadt < 500ms voor sprint met 50 taken (lokaal)
- [ ] Optimistische update voelt direct (< 50ms)
---
## 13. Nog open / mogelijke v1.1
1. **Sortering binnen kolom** — drag binnen kolom is in v1 een no-op. Toekomstige uitbreiding via `solo_sort_order` veld of een aparte `UserTaskOrder`-tabel.
2. **Markdown-rendering implementatieplan** — v2; v1 is plain textarea.
3. **Multi-product Solo bord** — alle producten in één bord. Component is hier al op voorbereid via optionele `showProduct` prop op task card.
4. **REVIEW-status** — bewuste scope-uitstel; voegt later kolom + enum-migratie toe.
---
*Klaar om te valideren en aan Claude Code te geven.*

56
docs/glossary.md Normal file
View file

@ -0,0 +1,56 @@
---
title: "Scrum4Me — Glossary"
status: active
audience: [ai-agent, contributor]
language: en
last_updated: 2026-05-03
when_to_read: "When you encounter a domain term and need its canonical definition and the doc where it is specified."
---
# Scrum4Me — Glossary
Domain terms used across Scrum4Me docs, code, and MCP tooling.
## agent worker
A Claude Code session that has registered itself as a `ClaudeWorker` record and polls the job queue via `mcp__scrum4me__wait_for_job`. The NavBar counts active workers by `last_seen_at < now() - 15s`. See [MCP integration runbook](./runbooks/mcp-integration.md).
## claude-question
A pending question posted by an agent (via `mcp__scrum4me__ask_user_question`) to a human user, stored in the `claude_questions` table. The user answers in the UI; a PostgreSQL `LISTEN/NOTIFY` trigger pushes the answer back to the waiting agent. See [architecture: claude-question-channel](./architecture/claude-question-channel.md) and [ADR-0007](./adr/0007-claude-question-channel-design.md).
## demo-token
An API token whose owning user has `isDemo = true`. All write operations are blocked at three layers: the `proxy.ts` middleware, individual server actions, and disabled UI buttons. See [architecture: auth and sessions](./architecture/auth-and-sessions.md) and [ADR-0006](./adr/0006-demo-user-three-layer-policy.md).
## demo-user
A preconfigured read-only account used for public showcase. Shares product data with the main account but cannot create, update, or delete anything. See [architecture: auth and sessions](./architecture/auth-and-sessions.md).
## MCP-job
A `Task` record that has been queued for autonomous agent execution. An agent claims a job atomically via `mcp__scrum4me__wait_for_job` and reports completion via `mcp__scrum4me__update_job_status`. See [MCP integration runbook](./runbooks/mcp-integration.md).
## PBI (Product Backlog Item)
The second level of the work hierarchy: `Product → PBI → Story → Task`. A PBI groups related stories under a single theme or capability. Do not use "Epic", "Feature", or "Issue" as synonyms. See [backlog index](./backlog/index.md).
## Solo Panel
The single-user planning screen that shows all PBIs and stories for one product in a split-pane drag-and-drop layout. Contrast with the Sprint board (team-facing). See [functional spec — Solo Panel](./specs/functional.md).
## Sprint
A time-boxed iteration with a `sprint_goal`. Stories move from `OPEN` to `IN_SPRINT` when added to the active sprint. Only one sprint per product can be `ACTIVE` at a time. See [backlog index](./backlog/index.md).
## Story
The third level of the work hierarchy: `Product → PBI → Story → Task`. A Story has acceptance criteria and a status (`OPEN | IN_SPRINT | DONE`). See [functional spec](./specs/functional.md).
## Task
The leaf level of the work hierarchy: `Product → PBI → Story → Task`. A Task has an `implementation_plan`, a `status` (`TO_DO | IN_PROGRESS | REVIEW | DONE`), and an optional `sort_order`. API exposes status as lowercase (`todo | in_progress | review | done`). See [architecture: data model](./architecture/data-model.md) and [ADR-0004](./adr/0004-status-enum-mapping.md).
## Todo
A lightweight freeform note scoped to a product (or unscoped). Not part of the sprint hierarchy — used for quick capture. Created via `mcp__scrum4me__create_todo`. See [MCP integration runbook](./runbooks/mcp-integration.md).

View file

@ -1,27 +1,687 @@
# Material Design 3 Color Scheme Documentation ---
## Project Management Application for Scrum Teams title: "Scrum4Me — Styling & Design System"
status: active
audience: [ai-agent, contributor]
language: nl
last_updated: 2026-05-03
---
**Version:** 1.0 # Scrum4Me — Styling & Design System
**Date:** April 22, 2026
**Application:** Desktop-first fullstack web application for solo developers and small Scrum Teams **Versie:** 0.1 — april 2026
**Onderdeel van:** CLAUDE.md context-set
--- ---
## Table of Contents ## Overzicht
1. [Color Philosophy](#color-philosophy) Scrum4Me gebruikt **Material Design 3 (MD3)** als kleurfilosofie, geïmplementeerd via CSS custom properties in `theme.css` en direct bruikbaar als Tailwind utility classes. **shadcn/ui** levert alle UI-primitieven (Button, Dialog, Sheet, Badge, etc.) en is volledig compatibel met het MD3-kleurensysteem via de legacy-token-mapping.
2. [Complete Color Palette](#complete-color-palette)
3. [Surface Elevation System](#surface-elevation-system) Lees dit document voordat je een component schrijft. Gebruik **nooit** willekeurige Tailwind-kleuren zoals `bg-blue-500` of `bg-green-600` — gebruik altijd de semantische tokens uit dit systeem.
4. [Color Roles](#color-roles)
5. [Semantic States](#semantic-states)
6. [Project Management Colors](#project-management-colors)
7. [Usage Examples](#usage-examples)
8. [Implementation Guide](#implementation-guide)
9. [Accessibility](#accessibility)
10. [Best Practices](#best-practices)
--- ---
## Setup
### 1. theme.css plaatsen
Kopieer het meegeleverde `theme.css` bestand naar:
```
app/globals.css ← importeer theme.css hier, of plak de inhoud direct
```
Of als apart bestand:
```
styles/theme.css
```
Importeer in `app/globals.css`:
```css
@import './styles/theme.css';
```
### 2. shadcn/ui initialiseren
```bash
npx shadcn@latest init
```
Kies bij de setup:
- Style: **Default**
- Base color: **Slate** (wordt overschreven door theme.css)
- CSS variables: **Yes**
De `theme.css` overschrijft alle shadcn default-kleuren via CSS custom properties. Geen extra configuratie nodig.
### 3. Tailwind configuratie
`theme.css` registreert alle tokens via `@theme inline` — ze zijn direct beschikbaar als Tailwind utility classes:
```tsx
// Werkt direct:
className="bg-primary text-primary-foreground"
className="bg-surface-container-low"
className="bg-status-done"
className="bg-priority-critical"
```
### 4. Dark mode
Dark mode werkt via de `.dark` class op `<html>`:
```tsx
// components/theme-toggle.tsx
'use client'
import { useState, useEffect } from 'react'
export function ThemeToggle() {
const [isDark, setIsDark] = useState(false)
useEffect(() => {
const stored = localStorage.getItem('theme')
if (stored === 'dark') {
document.documentElement.classList.add('dark')
setIsDark(true)
}
}, [])
const toggle = () => {
document.documentElement.classList.toggle('dark')
const next = !isDark
setIsDark(next)
localStorage.setItem('theme', next ? 'dark' : 'light')
}
return (
<button onClick={toggle} className="text-muted-foreground hover:text-foreground">
{isDark ? '☀️' : '🌙'}
</button>
)
}
```
---
## Kleurfilosofie
Drie hoofdrollen, elk met een semantische betekenis voor een Scrum-planner:
| Rol | Kleur | Betekenis | Gebruik in Scrum4Me |
|---|---|---|---|
| **Primary** | Blauw `#0061a4` | Productiviteit, vertrouwen | Primaire knoppen, actieve navigatie, Sprint Goal |
| **Secondary** | Paars `#5b5e71` | Planning, organisatie | Secundaire acties, filters, toolbar-items |
| **Tertiary** | Teal `#006874` | Voortgang, data | Voortgangsindicatoren, story-tellers, metrics |
Diepte wordt gecreëerd via **tonal elevation** (lichtere/donkerdere oppervlakken), niet via schaduwen.
---
## Surface Elevation System
Gebruik deze hiërarchie consequent — nooit `shadow-lg` voor diepte:
```
HOOGSTE ELEVATIE (voorgrond)
surface-container-lowest → dialogs, modals, popovers
surface-container-low → kaarten, panelen
surface-container → standaard container
surface-container-high → geneste containers
surface-container-highest → achtergrondcontainers
LAAGSTE ELEVATIE (achtergrond)
background → app-achtergrond
```
### In Scrum4Me specifiek
| Element | Surface token |
|---|---|
| App achtergrond | `bg-background` |
| Navigatiebalk | `bg-surface-container-low` |
| Gesplitst scherm (elk paneel) | `bg-surface-container-low` |
| PBI-rij | `bg-surface-container` |
| Geselecteerde PBI-rij | `bg-primary-container` |
| Story-blok | `bg-surface-container-low border border-border` |
| Story-blok (geselecteerd) | `bg-primary-container border border-primary` |
| Taakregel | `bg-surface-container` |
| Dialogs / modals | `bg-surface-container-lowest` |
| Slide-over (story detail) | `bg-surface-container-lowest` |
| Todo-item | `bg-surface-container` |
| Navigatiebar per paneel | `bg-surface-container-highest` |
---
## Statuskleur mapping
### Story- en taakstatus
Gebruik **altijd** icoon + tekst naast kleur (toegankelijkheid):
```tsx
// Status badge component
const statusConfig = {
OPEN: {
label: 'Open',
className: 'bg-status-todo text-white',
},
IN_SPRINT: {
label: 'In Sprint',
className: 'bg-status-in-progress text-white',
},
DONE: {
label: 'Done',
className: 'bg-status-done text-white',
},
}
// Taakstatus
const taskStatusConfig = {
TO_DO: {
label: 'To Do',
className: 'bg-status-todo text-white',
},
IN_PROGRESS: {
label: 'In Progress',
className: 'bg-status-in-progress text-white',
},
DONE: {
label: 'Done',
className: 'bg-status-done text-white',
},
}
```
### Prioriteitskleur mapping
```tsx
const priorityConfig = {
1: {
label: 'Kritiek',
className: 'bg-priority-critical text-white',
borderClassName: 'border-l-4 border-priority-critical',
},
2: {
label: 'Hoog',
className: 'bg-priority-high text-white',
borderClassName: 'border-l-4 border-priority-high',
},
3: {
label: 'Middel',
className: 'bg-priority-medium text-white',
borderClassName: 'border-l-4 border-priority-medium',
},
4: {
label: 'Laag',
className: 'bg-priority-low text-white',
borderClassName: 'border-l-4 border-priority-low',
},
}
```
### Story-activiteitenlog
```tsx
const logTypeConfig = {
IMPLEMENTATION_PLAN: {
label: 'Implementatieplan',
className: 'bg-info-container text-info-container-foreground border-l-4 border-info',
},
TEST_RESULT: {
PASSED: {
label: 'Tests geslaagd',
className: 'bg-success-container text-success-container-foreground border-l-4 border-success',
},
FAILED: {
label: 'Tests mislukt',
className: 'bg-error-container text-error-container-foreground border-l-4 border-error',
},
},
COMMIT: {
label: 'Commit',
className: 'bg-secondary-container text-secondary-container-foreground border-l-4 border-secondary',
},
}
```
---
## shadcn/ui componenten — gebruik in Scrum4Me
Alle shadcn-componenten gebruiken automatisch het MD3-kleurensysteem. Hieronder de aanbevolen varianten per context.
### Button
```tsx
import { Button } from '@/components/ui/button'
// Primaire actie (Sprint starten, PBI aanmaken, Opslaan)
<Button>Sprint starten</Button>
// Secundaire actie (Annuleren, Filters, Exporteren)
<Button variant="secondary">Annuleren</Button>
// Destructieve actie (Verwijderen, Archiveren)
<Button variant="destructive">Verwijderen</Button>
// Ghost (icon-knoppen in navigatiebar)
<Button variant="ghost" size="icon">
<PlusIcon className="h-4 w-4" />
</Button>
// Outline (minder urgente acties)
<Button variant="outline">Details bekijken</Button>
```
### Badge (status en prioriteit)
```tsx
import { Badge } from '@/components/ui/badge'
// Gebruik custom className voor MD3-kleuren
// shadcn Badge variant="secondary" is ook bruikbaar voor neutrale badges
<Badge className="bg-status-done text-white">Done</Badge>
<Badge className="bg-priority-critical text-white">Kritiek</Badge>
<Badge className="bg-status-in-progress text-white">In Sprint</Badge>
// Neutrale info badge (bijv. "3 taken")
<Badge variant="secondary">3 taken</Badge>
```
**PBI-status (READY / BLOCKED / DONE):** hergebruikt bestaande tokens —
`status-todo` voor READY, `status-blocked` voor BLOCKED, `status-done` voor
DONE. Centraal gedefinieerd in `components/shared/pbi-status-select.tsx`
(`PBI_STATUS_LABELS`, `PBI_STATUS_COLORS`); importeer die in plaats van
kleuren ad-hoc te kopiëren.
### Dialog (bevestigingsdialogen)
```tsx
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from '@/components/ui/alert-dialog'
// Standaard bevestigingsdialoog voor verwijderacties
<AlertDialogContent className="bg-surface-container-lowest">
<AlertDialogHeader>
<AlertDialogTitle>PBI verwijderen?</AlertDialogTitle>
<AlertDialogDescription>
Dit verwijdert ook alle gekoppelde stories en taken. Deze actie is niet ongedaan te maken.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Annuleren</AlertDialogCancel>
<AlertDialogAction className="bg-destructive text-destructive-foreground">
Verwijderen
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
```
### Sheet (story detail slide-over)
```tsx
import { Sheet, SheetContent, SheetHeader, SheetTitle } from '@/components/ui/sheet'
<Sheet>
<SheetContent
side="right"
className="w-[480px] bg-surface-container-lowest border-l border-border"
>
<SheetHeader>
<SheetTitle className="text-foreground">{story.title}</SheetTitle>
</SheetHeader>
{/* story detail inhoud */}
</SheetContent>
</Sheet>
```
### Input en Textarea
```tsx
import { Input } from '@/components/ui/input'
import { Textarea } from '@/components/ui/textarea'
// shadcn Input gebruikt --input-background automatisch uit theme.css
<Input
placeholder="PBI titel"
className="bg-input-background border-border focus:ring-primary"
/>
<Textarea
placeholder="Omschrijving (optioneel)"
className="bg-input-background border-border focus:ring-primary resize-none"
/>
```
### Select (prioriteit dropdown)
```tsx
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
<Select>
<SelectTrigger className="bg-input-background border-border">
<SelectValue placeholder="Prioriteit" />
</SelectTrigger>
<SelectContent className="bg-surface-container-lowest border-border">
<SelectItem value="1">
<span className="flex items-center gap-2">
<span className="w-2 h-2 rounded-full bg-priority-critical" />
Kritiek
</span>
</SelectItem>
<SelectItem value="2">
<span className="flex items-center gap-2">
<span className="w-2 h-2 rounded-full bg-priority-high" />
Hoog
</span>
</SelectItem>
<SelectItem value="3">
<span className="flex items-center gap-2">
<span className="w-2 h-2 rounded-full bg-priority-medium" />
Middel
</span>
</SelectItem>
<SelectItem value="4">
<span className="flex items-center gap-2">
<span className="w-2 h-2 rounded-full bg-priority-low" />
Laag
</span>
</SelectItem>
</SelectContent>
</Select>
```
### Skeleton (loading states)
```tsx
import { Skeleton } from '@/components/ui/skeleton'
// PBI lijst skeleton
function PbiListSkeleton() {
return (
<div className="space-y-2 p-4">
{Array.from({ length: 5 }).map((_, i) => (
<Skeleton key={i} className="h-12 w-full bg-surface-container-high" />
))}
</div>
)
}
// Story blokken skeleton
function StoryGridSkeleton() {
return (
<div className="flex flex-wrap gap-3 p-4">
{Array.from({ length: 6 }).map((_, i) => (
<Skeleton key={i} className="h-24 w-[10%] min-w-[100px] bg-surface-container-high" />
))}
</div>
)
}
```
### Tooltip (demo-gebruiker write-protection)
```tsx
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
// Gebruik voor alle uitgeschakelde knoppen bij demo-gebruiker
function DemoProtectedButton({ children, isDemo, onClick, ...props }) {
if (!isDemo) {
return <Button onClick={onClick} {...props}>{children}</Button>
}
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<span>
<Button disabled {...props}>{children}</Button>
</span>
</TooltipTrigger>
<TooltipContent className="bg-surface-container-lowest border-border">
<p className="text-sm text-muted-foreground">Niet beschikbaar in demo-modus</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)
}
```
---
## Scrum4Me component patronen
### PBI-rij
```tsx
// Geselecteerd PBI heeft primary-container achtergrond
<div
className={cn(
"flex items-center gap-3 px-4 py-3 rounded-lg cursor-pointer transition-colors border-l-4",
priorityConfig[pbi.priority].borderClassName,
isSelected
? "bg-primary-container text-primary-container-foreground"
: "bg-surface-container hover:bg-surface-container-high"
)}
onClick={() => setSelectedPbi(pbi.id)}
>
<span className="flex-1 text-sm font-medium truncate">{pbi.title}</span>
<Badge className={priorityConfig[pbi.priority].className}>
{priorityConfig[pbi.priority].label}
</Badge>
</div>
```
### Story-blok
```tsx
// ~10% schermbreedte, compacte weergave
<div
className={cn(
"relative flex flex-col gap-1 p-3 rounded-lg border cursor-pointer",
"min-w-[100px] w-[10%] h-24 text-xs",
"transition-colors hover:border-primary",
"bg-surface-container-low border-border"
)}
>
<span className="font-medium leading-tight line-clamp-2">{story.title}</span>
<div className="mt-auto flex items-center justify-between">
<span className={cn("px-1.5 py-0.5 rounded text-[10px] font-medium", statusConfig[story.status].className)}>
{statusConfig[story.status].label}
</span>
<span className={cn("w-2 h-2 rounded-full", `bg-priority-${priorityLabel}`)}>
</span>
</div>
</div>
```
### Prioriteitsgroep scheidingslijn
```tsx
// Visuele scheiding per prioriteitsgroep in PBI-lijst en story-grid
<div className="mt-4 mb-2">
<div className="flex items-center gap-2">
<span className={cn(
"text-xs font-semibold uppercase tracking-wider",
priority === 1 && "text-priority-critical",
priority === 2 && "text-priority-high",
priority === 3 && "text-priority-medium",
priority === 4 && "text-priority-low",
)}>
{priorityConfig[priority].label}
</span>
<div className={cn(
"flex-1 h-px",
priority === 1 && "bg-priority-critical/30",
priority === 2 && "bg-priority-high/30",
priority === 3 && "bg-priority-medium/30",
priority === 4 && "bg-priority-low/30",
)} />
<span className="text-xs text-muted-foreground">{count}</span>
</div>
</div>
```
### Voortgangsindicator (story → taken)
```tsx
// Gebruikt tertiary kleur voor voortgang
function StoryProgress({ done, total }: { done: number; total: number }) {
const pct = total === 0 ? 0 : Math.round((done / total) * 100)
return (
<div className="flex items-center gap-2 text-xs">
<div className="flex-1 h-1.5 bg-surface-container-highest rounded-full overflow-hidden">
<div
className="h-full bg-tertiary rounded-full transition-all"
style={{ width: `${pct}%` }}
/>
</div>
<span className="text-muted-foreground tabular-nums">
{done}/{total}
</span>
</div>
)
}
```
### Activiteitenlog entry
```tsx
function LogEntry({ entry }: { entry: StoryLog }) {
const config = entry.type === 'TEST_RESULT'
? logTypeConfig.TEST_RESULT[entry.status ?? 'PASSED']
: logTypeConfig[entry.type]
return (
<div className={cn("rounded-lg p-3 text-sm", config.className)}>
<div className="flex items-center justify-between mb-1">
<span className="font-medium text-xs uppercase tracking-wide">
{config.label}
</span>
<span className="text-xs opacity-70">
{formatDate(entry.created_at)}
</span>
</div>
<p className="text-sm leading-relaxed whitespace-pre-wrap">
{entry.content}
</p>
{entry.commit_hash && (
<a
href={`${repoUrl}/commit/${entry.commit_hash}`}
target="_blank"
rel="noopener noreferrer"
className="mt-1 inline-flex items-center gap-1 text-xs font-mono opacity-80 hover:opacity-100 underline"
>
{entry.commit_hash.slice(0, 7)} — {entry.commit_message}
</a>
)}
</div>
)
}
```
### Sprint Goal banner
```tsx
// Prominent bovenaan Sprint-schermen
<div className="bg-primary-container text-primary-container-foreground rounded-lg px-4 py-3 mb-4">
<span className="text-xs font-semibold uppercase tracking-wider opacity-70">
Sprint Goal
</span>
<p className="mt-0.5 font-medium">{sprint.sprint_goal}</p>
</div>
```
### Toast notificaties (Sonner)
```tsx
import { toast } from 'sonner'
// Success (aanmaken, opslaan)
toast.success('PBI aangemaakt')
toast.success('Story toegevoegd aan Sprint')
// Error (mislukte Server Action)
toast.error('Opslaan mislukt. Probeer opnieuw.')
// Info (neutrale melding)
toast.info('Sprint afgerond')
// Geen toast bij drag-and-drop (te frequent)
```
---
## Regels (nooit overtreden)
```
❌ bg-blue-500, bg-green-600, bg-red-400 → gebruik semantische tokens
❌ shadow-lg, shadow-md → gebruik surface elevation
❌ opacity-50 op een primary button → gebruik -container variant
❌ Kleur alleen voor status (geen tekst) → altijd tekst + kleur
❌ Hardcoded hex-waarden in className → altijd via CSS token
❌ bg-white of bg-black → bg-background of bg-foreground
✅ bg-primary text-primary-foreground
✅ bg-surface-container-low
✅ bg-status-done + tekst "Done"
✅ bg-error-container text-error-container-foreground
✅ border-l-4 border-priority-critical
```
---
## Bestandslocaties
```
styles/
theme.css ← bronbestand, niet aanpassen
app/
globals.css ← importeert theme.css
components/
ui/ ← shadcn/ui (auto-gegenereerd, niet aanpassen)
shared/
status-badge.tsx ← herbruikbare status badge
priority-badge.tsx ← herbruikbare prioriteit badge
demo-button.tsx ← Button met demo-protection tooltip
story-log.tsx ← activiteitenlog entries
story-progress.tsx ← voortgangsindicator
priority-group.tsx ← prioriteitsgroep scheidingslijn
```
---
## Toegankelijkheid
- Alle kleurcombinaties voldoen aan **WCAG AA** (contrast ratio ≥ 4.5:1 voor normale tekst)
- Gebruik **altijd** tekst + kleur voor statusindicatoren, nooit kleur alleen
- Alle interactieve elementen hebben een zichtbare `focus:ring-primary`
- Dark mode is volledig ondersteund via de `.dark` class
---
*Bijlage bij CLAUDE.md — lees beide voor je begint met bouwen.*
---
## MD3 Color Scheme
## Color Philosophy ## Color Philosophy
This color scheme follows Material Design 3 principles with three main color roles optimized for productivity software: This color scheme follows Material Design 3 principles with three main color roles optimized for productivity software:

202
docs/obsidian-authoring.md Normal file
View file

@ -0,0 +1,202 @@
---
title: Obsidian as Personal Authoring Layer
status: active
audience: maintainer, contributor
language: en
last_updated: 2026-05-02
related:
- docs/adr/README.md
- scripts/generate-docs-index.mjs
- .gitignore
---
# Obsidian as Personal Authoring Layer
Scrum4Me's documentation lives as plain Markdown under `docs/`. The canonical
source of truth is the committed `.md` file — rendered on GitHub, read by
agents (Claude Code, Codex) directly from the file system, and indexed by
`scripts/generate-docs-index.mjs`.
Note: the `mcp` server does **not** expose `docs/` content. Its
20 tools serve database state (products, sprints, stories, tasks, jobs,
questions) via Prisma. Documentation reaches the agent only through the
file-system tools, so the canonical Markdown is the one and only channel.
Obsidian is **not** a second source of truth. It is a personal authoring
layer that you can opt into without changing what the repo or the agent
sees. This document records the conventions that keep both views consistent.
## Why an authoring layer at all
A canonical `0003-job-claim-strategy.md` is short, declarative, and final.
The thinking that produced it — alternatives weighed, links followed, half-
ideas — is messy and personal. Forcing that into the committed file inflates
ADRs and dilutes the signal future readers (human or agent) need.
Obsidian gives you a place to keep the thinking next to the artefact without
polluting it: graph view, backlinks, properties, scratch notes. The
`.gitignore` rules below ensure none of that crosses the repo boundary.
## Vault setup
Open the repo root as the vault, not just `docs/`.
Reasons:
- ADRs and patterns frequently reference code (`prisma/schema.prisma`,
`lib/task-status.ts`). Markdown links from a `docs/`-only vault cannot
resolve those targets; from a root vault they can.
- The same vault then surfaces `README.md`, `CLAUDE.md`, and `AGENTS.md`
as nodes in the graph — useful when reasoning about agent instructions.
- Per-developer Obsidian config lives in `.obsidian/` at the vault root,
which is already gitignored.
### Two repos, two vaults
The MCP server lives in a separate repo (`mcp`) with its own
`CLAUDE.md`, tools, and tests. Open it as a **separate** Obsidian vault
when you work there. Each `.obsidian/` directory is per-folder and stays
gitignored on both sides; cross-repo notes belong in whichever vault you
opened first, not in some shared third location.
Schema-related ADRs are an exception: the MCP server consumes the
Scrum4Me Prisma schema via the `vendor/scrum4me/` git submodule, so the
*decision* about the schema belongs in this repo's `docs/adr/`. The
mirror in `mcp` only needs an `npm run sync-schema` after the
ADR lands here, not its own ADR.
## Link style
In Settings → Files & Links:
- **Use [[Wikilinks]]** → off
- **New link format** → Relative path to file
- **Default location for new notes** → "Same folder as current file"
Wikilinks would render as plain text on GitHub and are not parsed by the
docs index generator. Stick to standard Markdown links (`./adr/0001-foo.md`)
so the same file works in Obsidian, GitHub, and any agent reader.
Obsidian still resolves Markdown links for backlinks and graph view, so you
do not lose Obsidian's navigation features.
## Front-matter is Obsidian Properties
`scripts/generate-docs-index.mjs` reads a small set of YAML front-matter
keys: `title`, `status`, `date` (or `last_updated`). Obsidian renders the
exact same YAML block as its Properties panel.
Practical consequence: flipping a plan from `proposal` to `accepted` in
the Obsidian Properties UI and then running `npm run docs:index` is a
complete workflow — no separate metadata store, no plugin needed.
The repo's front-matter is intentionally flat (no nested objects, no lists
beyond simple arrays) to stay parseable by the dependency-free generator.
When adding fields, keep them flat.
## Templates
Two ADR templates ship in `docs/adr/templates/`:
- `nygard.md` — default, for one-way-door decisions
- `madr.md` — for decisions where rejected alternatives must be recorded
Both use `{{curly braces}}` placeholders so they read naturally in any
editor and so Codex and Claude Code can fill them mechanically.
Two ways to use them from Obsidian:
1. **Core Templates plugin** (recommended starting point). Settings →
Templates → Template folder location: `docs/adr/templates`. Then
`Cmd-P → Insert template → nygard` inserts the body verbatim. Fill
placeholders by hand.
2. **Templater** (community plugin). If filling the next ADR number and
today's date by hand becomes a bottleneck, write a Templater wrapper
in a vault-only `_templates/` folder that reads `nygard.md` and
substitutes `<% tp.date.now("YYYY-MM-DD") %>` plus a computed next
number. Keep this wrapper out of git — it is personal tooling, not
part of the canonical convention.
Do not modify the canonical templates to use Templater syntax. The
placeholders must remain agent-readable.
## Sidecar files for personal notes
`.gitignore` already excludes `_*.md` and the docs index generator
mirrors that exclusion (`/\/_[^/]+\.md$/`). Use this for any file that
should stay personal:
- `docs/adr/_draft-0003-job-claim-strategy.md` — alternatives, doubts,
links you considered before settling on the canonical ADR
- `docs/plans/_questions-for-jp.md` — open questions you want to think
about but not raise yet
- `_scratch.md` anywhere in the tree
Workflow for an ADR:
1. Brainstorm in `docs/adr/_draft-NNNN-…md` — free-form, link-heavy.
2. When the decision is clear, copy the relevant prose into the canonical
`docs/adr/NNNN-…md`, applying the Nygard or MADR template.
3. Run `npm run docs:index`.
4. Commit only the canonical ADR (the sidecar is invisible to git).
The sidecar can stay as a personal trail or be deleted — both are local-
only choices.
## Plugin recommendations
Useful and low-risk:
- **Outline** (core) — table of contents per file
- **Backlinks**, **Outgoing Links** (core) — see what links to and from
the current document
- **Linter** (community) — can enforce that ADRs have a `status` field
in front-matter
Use with care:
- **Dataview** — tempting to replace `INDEX.md` with a Dataview query.
Don't. The query renders blank on GitHub and is invisible to Claude
Code. Use Dataview only in a sidecar (`_dashboard.md`) for personal
views.
Avoid as canonical source:
- **Canvas**, **Excalidraw** — not diff-able, not agent-readable. Keep
diagrams as committed SVG (`docs/assets/erd.svg`) or as Mermaid blocks inside
Markdown.
## Index generator interaction
`generate-docs-index.mjs` is the system of record for `docs/INDEX.md`.
Two reliability concerns when authoring through Obsidian:
- **Forgetting to regenerate the index.** If you create or rename a doc
in Obsidian and commit without running the generator, INDEX.md goes
stale. Mitigations: a `pre-commit` hook that runs `npm run docs:index`
whenever staged changes touch `docs/**/*.md`, or a habit of running
`npm run docs:index` before every `git add docs/`.
- **Renaming and "update links" prompts.** Obsidian's "automatically
update internal links" applies to wikilinks. Since this vault uses
Markdown links, Obsidian will still try to update them — verify the
diff before committing, especially for cross-folder renames.
## What Obsidian must not do
- Introduce wikilinks into committed files
- Add Obsidian-only blocks (callouts, embeds with `![[...]]`) into
canonical docs
- Replace `INDEX.md` with a Dataview view in the committed tree
- Modify front-matter keys outside the documented set without also
updating `generate-docs-index.mjs`
If a workflow benefits the personal vault but breaks one of those rules,
keep it in a `_*.md` sidecar.
## Summary
Treat Obsidian like a notebook bound to the same desk as the repo:
useful for thinking, never confused with what's published. The
gitignore rules and the index generator already enforce most of this
mechanically — these conventions cover the parts that still depend on
the author's judgement.

View file

@ -1,3 +1,12 @@
---
title: "Bidirectionele async-comms MCP-agent ↔ user"
status: active
audience: [ai-agent, contributor]
language: nl
last_updated: 2026-05-03
when_to_read: "When implementing or extending the claude-question channel for agent-user async communication."
---
# Patroon: Bidirectionele async-comms tussen MCP-agent en interactieve user # Patroon: Bidirectionele async-comms tussen MCP-agent en interactieve user
Het M11 vraag-antwoord-kanaal is herbruikbaar voor elke feature waarbij een Het M11 vraag-antwoord-kanaal is herbruikbaar voor elke feature waarbij een
@ -113,7 +122,7 @@ Specifiek voor M11. Kopieer en pas aan:
+ `answer-modal.tsx`: Bell met badge, slide-over met item-list, Dialog + `answer-modal.tsx`: Bell met badge, slide-over met item-list, Dialog
met free-text/options-radio met free-text/options-radio
### MCP-tools (scrum4me-mcp) ### MCP-tools (mcp)
- `src/tools/ask-user-question.ts`: write-tool met optionele `wait_seconds`- - `src/tools/ask-user-question.ts`: write-tool met optionele `wait_seconds`-
polling (intern setInterval tot status verandert of timeout) polling (intern setInterval tot status verandert of timeout)
- `src/tools/get-question-answer.ts`: read-tool voor latere session-pickup - `src/tools/get-question-answer.ts`: read-tool voor latere session-pickup
@ -139,7 +148,7 @@ Specifiek voor M11. Kopieer en pas aan:
- Volledige flow + threat-model: `docs/architecture.md` § Vraag- - Volledige flow + threat-model: `docs/architecture.md` § Vraag-
antwoord-kanaal Claude ↔ user antwoord-kanaal Claude ↔ user
- Endpoint-contract: `docs/api.md` § Notifications + Cron - Endpoint-contract: `docs/api/rest-contract.md` § Notifications + Cron
- LISTEN/NOTIFY-pattern: `app/api/realtime/solo/route.ts` (M8 ST-802) — zelfde - LISTEN/NOTIFY-pattern: `app/api/realtime/solo/route.ts` (M8 ST-802) — zelfde
ReadableStream + heartbeat + hard-close + abort-cleanup ReadableStream + heartbeat + hard-close + abort-cleanup
- M10 vs M11 keuze tussen eigen/gedeeld kanaal: zie threat-model-tabel - M10 vs M11 keuze tussen eigen/gedeeld kanaal: zie threat-model-tabel

View file

@ -1,10 +1,19 @@
---
title: "Entity Dialog"
status: active
audience: [ai-agent, contributor]
language: nl
last_updated: 2026-05-03
when_to_read: "Before building any create/edit/detail dialog component."
---
# Pattern — Entity Dialog # Pattern — Entity Dialog
Deze pagina is **bindend** voor elke create/edit/detail-dialog in Scrum4Me, ongeacht het achterliggende dataobject (PBI, Story, Task, Todo, Sprint, Product, User, of toekomstige entiteiten). Een nieuwe dialog die hier niet aan voldoet, hoort niet gemerged te worden. Deze pagina is **bindend** voor elke create/edit/detail-dialog in Scrum4Me, ongeacht het achterliggende dataobject (PBI, Story, Task, Todo, Sprint, Product, User, of toekomstige entiteiten). Een nieuwe dialog die hier niet aan voldoet, hoort niet gemerged te worden.
> **Doel:** elke dialog voelt identiek aan voor de gebruiker, hergebruikt dezelfde primitives, en heeft de drielaagse demo-policy + auth-scoping standaard ingebakken. > **Doel:** elke dialog voelt identiek aan voor de gebruiker, hergebruikt dezelfde primitives, en heeft de drielaagse demo-policy + auth-scoping standaard ingebakken.
Voor entity-specifieke afwijkingen of velden: schrijf één begeleidende doc per entiteit (zie sectie [§ Per-entiteit profile](#per-entiteit-profile-verplicht)). Voorbeeld: `docs/task-dialog.md` is het Task-profiel. Voor entity-specifieke afwijkingen of velden: schrijf één begeleidende doc per entiteit (zie sectie [§ Per-entiteit profile](#per-entiteit-profile-verplicht)). Voorbeeld: `docs/specs/dialogs/task.md` is het Task-profiel.
--- ---
@ -19,7 +28,7 @@ Voor entity-specifieke afwijkingen of velden: schrijf één begeleidende doc per
| 1.5 | **Drielaagse demo-policy** (verplicht — zie § 6) op elke write-actie. | `CLAUDE.md` "Demo-check" + `docs/architecture.md#demo-user-policy` | | 1.5 | **Drielaagse demo-policy** (verplicht — zie § 6) op elke write-actie. | `CLAUDE.md` "Demo-check" + `docs/architecture.md#demo-user-policy` |
| 1.6 | Validatie via één gedeeld zod-schema (`lib/schemas/<entity>.ts`) — gebruikt door zowel form als server action. | `CLAUDE.md` "Validatie" | | 1.6 | Validatie via één gedeeld zod-schema (`lib/schemas/<entity>.ts`) — gebruikt door zowel form als server action. | `CLAUDE.md` "Validatie" |
| 1.7 | Foutcodes volgen de project-conventie (§ 5). | `CLAUDE.md` "Foutcodes API" | | 1.7 | Foutcodes volgen de project-conventie (§ 5). | `CLAUDE.md` "Foutcodes API" |
| 1.8 | Geen willekeurige Tailwind-kleuren (`bg-blue-500` etc.). Alleen MD3-tokens uit `app/styles/theme.css`. | `docs/styling.md` | | 1.8 | Geen willekeurige Tailwind-kleuren (`bg-blue-500` etc.). Alleen MD3-tokens uit `app/styles/theme.css`. | `docs/design/styling.md` |
--- ---
@ -380,8 +389,8 @@ Reviewer en bouwer lopen deze door vóór merge:
## 15 — Referenties ## 15 — Referenties
- `CLAUDE.md` — UI Library Conventions, Demo-check, Foutcodes API, Validatie - `CLAUDE.md` — UI Library Conventions, Demo-check, Foutcodes API, Validatie
- `docs/styling.md` — MD3-tokens, kleurklassen - `docs/design/styling.md` — MD3-tokens, kleurklassen
- `docs/architecture.md` — Demo user policy, scope-helpers - `docs/architecture.md` — Demo user policy, scope-helpers
- `docs/patterns/server-action.md` — Server Action template (auth + Zod) - `docs/patterns/server-action.md` — Server Action template (auth + Zod)
- `docs/patterns/zustand-optimistic.md` — voor lijst-views die de dialog aanroepen - `docs/patterns/zustand-optimistic.md` — voor lijst-views die de dialog aanroepen
- `docs/task-dialog.md` — voorbeeld-profile voor entiteit "Task" - `docs/specs/dialogs/task.md` — voorbeeld-profile voor entiteit "Task"

View file

@ -1,3 +1,12 @@
---
title: "iron-session"
status: active
audience: [ai-agent, contributor]
language: nl
last_updated: 2026-05-03
when_to_read: "When reading or writing session cookies or implementing auth checks."
---
# Patroon: iron-session # Patroon: iron-session
## lib/session.ts ## lib/session.ts

View file

@ -1,3 +1,12 @@
---
title: "Prisma Client singleton"
status: active
audience: [ai-agent, contributor]
language: nl
last_updated: 2026-05-03
when_to_read: "When importing or initialising the Prisma client in server code."
---
# Patroon: Prisma Client singleton # Patroon: Prisma Client singleton
## lib/prisma.ts ## lib/prisma.ts
@ -51,14 +60,14 @@ generator client {
generator erd { generator erd {
provider = "prisma-erd-generator" provider = "prisma-erd-generator"
output = "../docs/erd.svg" output = "../docs/assets/erd.svg"
} }
``` ```
`prisma generate` bouwt dus twee artifacts: `prisma generate` bouwt dus twee artifacts:
- Prisma Client in `node_modules/@prisma/client` - Prisma Client in `node_modules/@prisma/client`
- het ERD-diagram in `docs/erd.svg` - het ERD-diagram in `docs/assets/erd.svg`
Gebruik volledige `prisma generate` alleen lokaal. De ERD-generator gebruikt Mermaid/Puppeteer en mag niet in CI of Vercel draaien. Gebruik volledige `prisma generate` alleen lokaal. De ERD-generator gebruikt Mermaid/Puppeteer en mag niet in CI of Vercel draaien.
@ -69,7 +78,7 @@ Gebruik volledige `prisma generate` alleen lokaal. De ERD-generator gebruikt Mer
| `npx prisma db push` | Schema synchroniseren naar de database | | `npx prisma db push` | Schema synchroniseren naar de database |
| `npx prisma db seed` | Seeddata laden | | `npx prisma db seed` | Seeddata laden |
| `npx prisma generate --generator client` | Alleen Prisma Client genereren; gebruiken in CI/deployment | | `npx prisma generate --generator client` | Alleen Prisma Client genereren; gebruiken in CI/deployment |
| `npm run db:erd` | `prisma generate`: Prisma Client en `docs/erd.svg` genereren | | `npm run db:erd` | `prisma generate`: Prisma Client en `docs/assets/erd.svg` genereren |
| `npm run db:erd:watch` | `prisma/schema.prisma` watchen en ERD opnieuw genereren | | `npm run db:erd:watch` | `prisma/schema.prisma` watchen en ERD opnieuw genereren |
| `npm run dev` | Next.js dev server plus ERD watcher starten | | `npm run dev` | Next.js dev server plus ERD watcher starten |

View file

@ -1,3 +1,12 @@
---
title: "Proxy (route protection)"
status: active
audience: [ai-agent, contributor]
language: nl
last_updated: 2026-05-03
when_to_read: "When adding or modifying route-level access control in proxy.ts."
---
# Patroon: Proxy (route protection) # Patroon: Proxy (route protection)
In Next.js 16 hernoemd van `middleware.ts` naar `proxy.ts`, functienaam van `middleware` naar `proxy`. In Next.js 16 hernoemd van `middleware.ts` naar `proxy.ts`, functienaam van `middleware` naar `proxy`.

View file

@ -1,3 +1,12 @@
---
title: "QR-pairing via unauth-SSE + pre-auth cookie"
status: active
audience: [ai-agent, contributor]
language: nl
last_updated: 2026-05-03
when_to_read: "When working on QR-code login flow or unauth SSE endpoints."
---
# Patroon: QR-pairing via unauth-SSE + pre-auth cookie # Patroon: QR-pairing via unauth-SSE + pre-auth cookie
Het M10 QR-login-mechanisme is herbruikbaar voor elke feature die **realtime- Het M10 QR-login-mechanisme is herbruikbaar voor elke feature die **realtime-
@ -90,6 +99,6 @@ Drie tijden in escalerende volgorde, alle korter dan de reguliere sessie:
## Referenties ## Referenties
- Volledige flow + threat-model: `docs/architecture.md` § QR-pairing flow - Volledige flow + threat-model: `docs/architecture.md` § QR-pairing flow
- Endpoint-contract: `docs/api.md` § Auth — QR-pairing - Endpoint-contract: `docs/api/rest-contract.md` § Auth — QR-pairing
- LISTEN/NOTIFY-pattern: `app/api/realtime/solo/route.ts` (M8 ST-802) — zelfde - LISTEN/NOTIFY-pattern: `app/api/realtime/solo/route.ts` (M8 ST-802) — zelfde
ReadableStream + heartbeat + hard-close + abort-cleanup, alleen ander channel ReadableStream + heartbeat + hard-close + abort-cleanup, alleen ander channel

View file

@ -1,3 +1,12 @@
---
title: "Route Handler (REST API)"
status: active
audience: [ai-agent, contributor]
language: nl
last_updated: 2026-05-03
when_to_read: "When writing a new Next.js route handler (GET/POST/PATCH/DELETE)."
---
# Patroon: Route Handler (REST API) # Patroon: Route Handler (REST API)
Alle endpoints vereisen: `Authorization: Bearer <token>` Alle endpoints vereisen: `Authorization: Bearer <token>`

View file

@ -1,3 +1,12 @@
---
title: "Server Action"
status: active
audience: [ai-agent, contributor]
language: nl
last_updated: 2026-05-03
when_to_read: "When writing a new server action with auth and Zod validation."
---
# Patroon: Server Action # Patroon: Server Action
Altijd in `actions/[domein].ts`. Nooit inline in page.tsx. Altijd in `actions/[domein].ts`. Nooit inline in page.tsx.

View file

@ -1,3 +1,12 @@
---
title: "Float sort_order (drag-and-drop volgorde)"
status: active
audience: [ai-agent, contributor]
language: nl
last_updated: 2026-05-03
when_to_read: "When implementing drag-and-drop reordering or inserting between items."
---
# Patroon: Float sort_order (drag-and-drop volgorde) # Patroon: Float sort_order (drag-and-drop volgorde)
## Berekening bij tussenvoeging ## Berekening bij tussenvoeging

View file

@ -1 +0,0 @@
test

View file

@ -1,3 +1,12 @@
---
title: "Zustand optimistische update + rollback"
status: active
audience: [ai-agent, contributor]
language: nl
last_updated: 2026-05-03
when_to_read: "When adding client-side state mutations that need optimistic UI and rollback."
---
# Patroon: Zustand optimistische update + rollback # Patroon: Zustand optimistische update + rollback
Gebruik dit patroon bij elke dnd-kit `onDragEnd` handler. Gebruik dit patroon bij elke dnd-kit `onDragEnd` handler.

View file

@ -1,3 +1,11 @@
---
title: "DevPlanner — User Personas"
status: active
audience: [maintainer]
language: nl
last_updated: 2026-05-03
---
# DevPlanner — User Personas # DevPlanner — User Personas
**Versie:** 0.1 — april 2026 **Versie:** 0.1 — april 2026

View file

@ -1,3 +1,12 @@
---
title: "M10 — Password-loze inlog via QR-pairing"
status: active
audience: [maintainer, contributor]
language: nl
last_updated: 2026-05-03
applies_to: [M10]
---
# M10 — Password-loze inlog via QR-pairing # M10 — Password-loze inlog via QR-pairing
Inloggen op een (publieke) desktop zonder wachtwoord: desktop toont QR, telefoon (al-ingelogd) scant en bevestigt expliciet, desktop is binnen 12 s ingelogd. Bouwt voort op M8 LISTEN/NOTIFY-infra met eigen channel `scrum4me_pairing`. Inloggen op een (publieke) desktop zonder wachtwoord: desktop toont QR, telefoon (al-ingelogd) scant en bevestigt expliciet, desktop is binnen 12 s ingelogd. Bouwt voort op M8 LISTEN/NOTIFY-infra met eigen channel `scrum4me_pairing`.
@ -7,8 +16,8 @@ Inloggen op een (publieke) desktop zonder wachtwoord: desktop toont QR, telefoon
- `desktopToken` reist alleen via HttpOnly cookie `s4m_pair` met `Path=/api/auth/pair`, `Max-Age=120`, `SameSite=Lax` - `desktopToken` reist alleen via HttpOnly cookie `s4m_pair` met `Path=/api/auth/pair`, `Max-Age=120`, `SameSite=Lax`
- Twee gescheiden hashes in DB scheiden mobiel-bewijs (`secret_hash`) van desktop-bewijs (`desktop_token_hash`) - Twee gescheiden hashes in DB scheiden mobiel-bewijs (`secret_hash`) van desktop-bewijs (`desktop_token_hash`)
Backlog-entries: zie [backlog.md § M10](../backlog.md#m10-password-loze-inlog-via-qr-pairing). Backlog-entries: zie [backlog.md § M10](../backlog/index.md#m10-password-loze-inlog-via-qr-pairing).
Functional spec: zie [functional.md § F-01b](../functional.md#f-01b-inloggen-via-mobiel-qr-pairing). Functional spec: zie [functional.md § F-01b](../specs/functional.md#f-01b-inloggen-via-mobiel-qr-pairing).
**Implementatie-volgorde** (commit-strategy uit CLAUDE.md): **Implementatie-volgorde** (commit-strategy uit CLAUDE.md):
@ -28,7 +37,7 @@ ST-1006 staat bij de API-laag (niet bij UI) omdat het een Route Handler is; ST-1
**Bestanden** **Bestanden**
- `prisma/schema.prisma` — nieuw model `LoginPairing` + back-relation op `User` - `prisma/schema.prisma` — nieuw model `LoginPairing` + back-relation op `User`
- `prisma/migrations/<timestamp>_add_login_pairing/migration.sql` — model + trigger - `prisma/migrations/<timestamp>_add_login_pairing/migration.sql` — model + trigger
- `vendor/scrum4me`-submodule in repo `scrum4me-mcp` — schema-sync ná merge - `vendor/scrum4me`-submodule in repo `mcp` — schema-sync ná merge
**Stappen** **Stappen**
@ -84,7 +93,7 @@ ST-1006 staat bij de API-laag (niet bij UI) omdat het een Route Handler is; ST-1
- `desktop_ip` houdt op 45 tekens om IPv6 te accommoderen (`xxxx:xxxx:…:255.255.255.255`). - `desktop_ip` houdt op 45 tekens om IPv6 te accommoderen (`xxxx:xxxx:…:255.255.255.255`).
- Geen index op `user_id` nodig voor v1 — er is geen lookup-pad "geef alle pairings van user X" (komt pas bij remote-revoke in M+1). - Geen index op `user_id` nodig voor v1 — er is geen lookup-pad "geef alle pairings van user X" (komt pas bij remote-revoke in M+1).
- Trigger emit ook bij DELETE niet nodig — pairings worden niet gedelete'd, ze gaan naar `consumed`/`cancelled`. - Trigger emit ook bij DELETE niet nodig — pairings worden niet gedelete'd, ze gaan naar `consumed`/`cancelled`.
- `vendor/scrum4me`-submodule in scrum4me-mcp moet ná merge op `main` direct gesynced worden, anders breekt de wekelijkse drift-check (`trig_015FFUnxjz9WMuhhWNGBQKFD`). Dit was ook een aandachtspunt bij ST-901. - `vendor/scrum4me`-submodule in mcp moet ná merge op `main` direct gesynced worden, anders breekt de wekelijkse drift-check (`trig_015FFUnxjz9WMuhhWNGBQKFD`). Dit was ook een aandachtspunt bij ST-901.
**Verificatie** **Verificatie**
- `npx prisma migrate dev` slaagt - `npx prisma migrate dev` slaagt
@ -767,7 +776,7 @@ ST-1006 staat bij de API-laag (niet bij UI) omdat het een Route Handler is; ST-1
<QrLoginButton /> <QrLoginButton />
``` ```
MD3-tokens uit `docs/styling.md`; geen willekeurige Tailwind-kleuren. MD3-tokens uit `docs/design/styling.md`; geen willekeurige Tailwind-kleuren.
4. **A11y**: QR-component krijgt `aria-label="QR-code voor mobiel inloggen"` en de URL wordt visueel als kopieer-bare tekst onder de QR getoond zodat screenreaders en gebruikers met cameraproblemen de URL handmatig kunnen openen. 4. **A11y**: QR-component krijgt `aria-label="QR-code voor mobiel inloggen"` en de URL wordt visueel als kopieer-bare tekst onder de QR getoond zodat screenreaders en gebruikers met cameraproblemen de URL handmatig kunnen openen.
@ -788,7 +797,7 @@ ST-1006 staat bij de API-laag (niet bij UI) omdat het een Route Handler is; ST-1
## ST-1008 — Documentatie + acceptatietest ## ST-1008 — Documentatie + acceptatietest
**Bestanden** **Bestanden**
- `docs/api.md` — drie nieuwe endpoints - `docs/api/rest-contract.md` — drie nieuwe endpoints
- `docs/architecture.md` — sectie "QR-pairing flow" + threat-model - `docs/architecture.md` — sectie "QR-pairing flow" + threat-model
- `docs/patterns/qr-login.md` — nieuw pattern-doc - `docs/patterns/qr-login.md` — nieuw pattern-doc
- `CLAUDE.md` — verwijzing naar het pattern-doc in de patterns-tabel - `CLAUDE.md` — verwijzing naar het pattern-doc in de patterns-tabel
@ -796,7 +805,7 @@ ST-1006 staat bij de API-laag (niet bij UI) omdat het een Route Handler is; ST-1
**Stappen** **Stappen**
1. **`docs/api.md`** — drie endpoints documenteren met request/response, foutcodes (400/401/403/404/410/422/429), curl-voorbeelden inclusief `--cookie-jar`. Voeg een sectie *"Cookie-mechaniek"* toe die uitlegt dat `s4m_pair` een tijdelijke pre-auth cookie is, anders dan de iron-session cookie. 1. **`docs/api/rest-contract.md`** — drie endpoints documenteren met request/response, foutcodes (400/401/403/404/410/422/429), curl-voorbeelden inclusief `--cookie-jar`. Voeg een sectie *"Cookie-mechaniek"* toe die uitlegt dat `s4m_pair` een tijdelijke pre-auth cookie is, anders dan de iron-session cookie.
2. **`docs/architecture.md`** — sectie *"QR-pairing flow"* met: 2. **`docs/architecture.md`** — sectie *"QR-pairing flow"* met:
- Sequence-diagram (mermaid of ASCII analoog aan M8) - Sequence-diagram (mermaid of ASCII analoog aan M8)
@ -826,19 +835,19 @@ ST-1006 staat bij de API-laag (niet bij UI) omdat het een Route Handler is; ST-1
7. **Secret niet in access logs** — controleer Vercel runtime-logs (via `mcp__a1fa0fcf-…__get_runtime_logs`) en lokale dev-logs; zoek op de secret-string en op `s=`-substrings; verwacht: 0 hits 7. **Secret niet in access logs** — controleer Vercel runtime-logs (via `mcp__a1fa0fcf-…__get_runtime_logs`) en lokale dev-logs; zoek op de secret-string en op `s=`-substrings; verwacht: 0 hits
**Aandachtspunten** **Aandachtspunten**
- Zorg dat de runtime-logs MCP-controle in `docs/test-plan.md` belandt zodat hij bij elke release herhaalbaar is. - Zorg dat de runtime-logs MCP-controle in `docs/qa/api-test-plan.md` belandt zodat hij bij elke release herhaalbaar is.
- `docs/patterns/qr-login.md` mag refereren naar bestaande pattern-docs (iron-session, route-handler) zonder ze te dupliceren. - `docs/patterns/qr-login.md` mag refereren naar bestaande pattern-docs (iron-session, route-handler) zonder ze te dupliceren.
**Verificatie** **Verificatie**
- `npm run lint && npx tsc --noEmit && npm test && npm run build` groen - `npm run lint && npx tsc --noEmit && npm test && npm run build` groen
- Alle zeven scenario's handmatig groen, beschreven in een test-rapport-sectie - Alle zeven scenario's handmatig groen, beschreven in een test-rapport-sectie
- `vendor/scrum4me`-submodule in scrum4me-mcp gesynced ná schema-merge - `vendor/scrum4me`-submodule in mcp gesynced ná schema-merge
--- ---
## Branch- en commit-strategie ## Branch- en commit-strategie
Per [CLAUDE.md → Branch & PR Strategy](../../CLAUDE.md#branch--pr-strategy-strict--kostenbeheersing): **één branch voor de hele milestone**, PR pas na handmatige acceptatie door de gebruiker. Reden: elke push triggert een Vercel preview-build, en op het Hobby-account zijn die schaars. Per [Branch & PR Strategy](../runbooks/branch-and-commit.md): **één branch voor de hele milestone**, PR pas na handmatige acceptatie door de gebruiker. Reden: elke push triggert een Vercel preview-build, en op het Hobby-account zijn die schaars.
**Branch:** `feat/M10-qr-login` — afgesplitst van `main` na merge van de planning-PR (#11). Alle ST-1001..ST-1008-werk landt op deze branch. **Branch:** `feat/M10-qr-login` — afgesplitst van `main` na merge van de planning-PR (#11). Alle ST-1001..ST-1008-werk landt op deze branch.
@ -866,7 +875,7 @@ docs(ST-1008): add qr-login pattern doc
**Pre-merge gates** (uit CLAUDE.md DoD): **Pre-merge gates** (uit CLAUDE.md DoD):
- `npm run lint && npm test && npm run build` groen op CI - `npm run lint && npm test && npm run build` groen op CI
- Schema-wijziging in ST-1001 → wekelijkse drift-check `trig_015FFUnxjz9WMuhhWNGBQKFD` mag niet rood staan; `vendor/scrum4me`-submodule in scrum4me-mcp meebewegen na merge - Schema-wijziging in ST-1001 → wekelijkse drift-check `trig_015FFUnxjz9WMuhhWNGBQKFD` mag niet rood staan; `vendor/scrum4me`-submodule in mcp meebewegen na merge
**Wanneer dit aanpassen:** zodra het Vercel-account naar Pro gaat — zie CLAUDE.md. **Wanneer dit aanpassen:** zodra het Vercel-account naar Pro gaat — zie CLAUDE.md.

View file

@ -1,10 +1,19 @@
---
title: "M11 — Claude vraagt, gebruiker antwoordt"
status: active
audience: [maintainer, contributor]
language: nl
last_updated: 2026-05-03
applies_to: [M11]
---
# M11 — Claude vraagt, gebruiker antwoordt # M11 — Claude vraagt, gebruiker antwoordt
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; de app toont een notificatie-badge; iedereen met product-toegang kan antwoorden; Claude leest het antwoord (sync via polling of in latere sessie via `get_question_answer`) en gaat door. 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; de app toont een notificatie-badge; iedereen met product-toegang kan antwoorden; Claude leest het antwoord (sync via polling of in latere sessie via `get_question_answer`) en gaat door.
Eerste concrete uitwerking van strategische **richting B** (verdiepen van de unieke AI-driven dev-flow). Eerste concrete uitwerking van strategische **richting B** (verdiepen van de unieke AI-driven dev-flow).
Backlog-entries: zie [backlog.md § M11](../backlog.md#m11-claude-vraagt-gebruiker-antwoordt) (op te leveren in ST-1108). Backlog-entries: zie [backlog.md § M11](../backlog/index.md#m11-claude-vraagt-gebruiker-antwoordt) (op te leveren in ST-1108).
**Beveiligingsuitgangspunten:** **Beveiligingsuitgangspunten:**
- Atomic answer via `updateMany WHERE status='open'` — concurrent dubbele submit kan niet - Atomic answer via `updateMany WHERE status='open'` — concurrent dubbele submit kan niet
@ -25,7 +34,7 @@ Backlog-entries: zie [backlog.md § M11](../backlog.md#m11-claude-vraagt-gebruik
**Bestanden** **Bestanden**
- `prisma/schema.prisma` — model `ClaudeQuestion` + relations op `User`/`Story`/`Task`/`Product` - `prisma/schema.prisma` — model `ClaudeQuestion` + relations op `User`/`Story`/`Task`/`Product`
- `prisma/migrations/<ts>_add_claude_questions/migration.sql` — table-DDL + trigger - `prisma/migrations/<ts>_add_claude_questions/migration.sql` — table-DDL + trigger
- `vendor/scrum4me`-submodule in `scrum4me-mcp` — schema-sync ná merge - `vendor/scrum4me`-submodule in `mcp` — schema-sync ná merge
**Stappen** **Stappen**
@ -104,16 +113,16 @@ Backlog-entries: zie [backlog.md § M11](../backlog.md#m11-claude-vraagt-gebruik
--- ---
## ST-1102 — MCP-tools (in `scrum4me-mcp`-repo) ## ST-1102 — MCP-tools (in `mcp`-repo)
**Bestanden** **Bestanden**
- `scrum4me-mcp/src/tools/ask-user-question.ts` — nieuw - `mcp/src/tools/ask-user-question.ts` — nieuw
- `scrum4me-mcp/src/tools/get-question-answer.ts` — nieuw - `mcp/src/tools/get-question-answer.ts` — nieuw
- `scrum4me-mcp/src/tools/list-open-questions.ts` — nieuw - `mcp/src/tools/list-open-questions.ts` — nieuw
- `scrum4me-mcp/src/tools/cancel-question.ts` — nieuw - `mcp/src/tools/cancel-question.ts` — nieuw
- `scrum4me-mcp/src/index.ts` — register de vier tools - `mcp/src/index.ts` — register de vier tools
- `scrum4me-mcp/scripts/smoke-test.ts` — uitbreiden met question-roundtrip - `mcp/scripts/smoke-test.ts` — uitbreiden met question-roundtrip
- `scrum4me-mcp/README.md` — tool-tabel uitbreiden - `mcp/README.md` — tool-tabel uitbreiden
**Stappen** **Stappen**
@ -153,7 +162,7 @@ Backlog-entries: zie [backlog.md § M11](../backlog.md#m11-claude-vraagt-gebruik
- MCP Inspector toont 4 nieuwe tools (totaal 13) - MCP Inspector toont 4 nieuwe tools (totaal 13)
- Smoke-test groen: ask + answer roundtrip binnen 5s - Smoke-test groen: ask + answer roundtrip binnen 5s
- Demo-token op `ask_user_question` of `cancel_question` geeft `PERMISSION_DENIED` - Demo-token op `ask_user_question` of `cancel_question` geeft `PERMISSION_DENIED`
- `tsc --noEmit` clean op `scrum4me-mcp` - `tsc --noEmit` clean op `mcp`
--- ---
@ -279,7 +288,7 @@ Backlog-entries: zie [backlog.md § M11](../backlog.md#m11-claude-vraagt-gebruik
**Aandachtspunten** **Aandachtspunten**
- Bell-icon en avatar moeten visueel balanceren — hoogte/padding gelijktrekken - Bell-icon en avatar moeten visueel balanceren — hoogte/padding gelijktrekken
- MD3-tokens uit `docs/styling.md`: badge `bg-error text-error-foreground` voor critical-count, `bg-primary` voor neutraal. Geen willekeurige Tailwind-kleuren - MD3-tokens uit `docs/design/styling.md`: badge `bg-error text-error-foreground` voor critical-count, `bg-primary` voor neutraal. Geen willekeurige Tailwind-kleuren
- Optimistic-answer in store: voor het Server Action-resultaat zet item op pending; bij error rollback met sonner-error-toast - Optimistic-answer in store: voor het Server Action-resultaat zet item op pending; bij error rollback met sonner-error-toast
- Sheet-content blijft open zodat de user meerdere vragen achter elkaar kan beantwoorden (zelfde patroon als ST-358 openstaande-stories-sheet) - Sheet-content blijft open zodat de user meerdere vragen achter elkaar kan beantwoorden (zelfde patroon als ST-358 openstaande-stories-sheet)
- ARIA: bell-icon heeft `aria-label="Notificaties — N open vragen"`, badge `role="status"` - ARIA: bell-icon heeft `aria-label="Notificaties — N open vragen"`, badge `role="status"`
@ -369,16 +378,16 @@ Backlog-entries: zie [backlog.md § M11](../backlog.md#m11-claude-vraagt-gebruik
## ST-1108 — Documentatie + acceptatietest ## ST-1108 — Documentatie + acceptatietest
**Bestanden** **Bestanden**
- `docs/api.md` — secties "SSE — Notifications" + "Cron — Expire questions" - `docs/api/rest-contract.md` — secties "SSE — Notifications" + "Cron — Expire questions"
- `docs/architecture.md` — sectie "Vraag-antwoord-kanaal" met sequence-diagram - `docs/architecture.md` — sectie "Vraag-antwoord-kanaal" met sequence-diagram
- `docs/patterns/claude-question-channel.md` — herbruikbaar pattern-doc - `docs/patterns/claude-question-channel.md` — herbruikbaar pattern-doc
- `docs/backlog.md` — M11-tabel-rij + M11-sectie - `docs/backlog/index.md` — M11-tabel-rij + M11-sectie
- `prisma/seed-data/parse-backlog.ts``M11: 'ACTIVE'`, `M10: 'COMPLETED'`, `M3.5: 'COMPLETED'` - `prisma/seed-data/parse-backlog.ts``M11: 'ACTIVE'`, `M10: 'COMPLETED'`, `M3.5: 'COMPLETED'`
- `CLAUDE.md` — pattern-doc verwijzing in Implementatiepatronen-tabel - `CLAUDE.md` — pattern-doc verwijzing in Implementatiepatronen-tabel
**Stappen** **Stappen**
1. Backlog-tabel-rij + M11-sectie in `docs/backlog.md` (mirror M10-format met **Implementatieplan:** verwijzing naar dit doc) 1. Backlog-tabel-rij + M11-sectie in `docs/backlog/index.md` (mirror M10-format met **Implementatieplan:** verwijzing naar dit doc)
2. `docs/architecture.md` § "Vraag-antwoord-kanaal": 2. `docs/architecture.md` § "Vraag-antwoord-kanaal":
- Mermaid sequence-diagram: Claude → MCP → DB → trigger → SSE → user → Server Action → DB → trigger → polling-tool - Mermaid sequence-diagram: Claude → MCP → DB → trigger → SSE → user → Server Action → DB → trigger → polling-tool
@ -406,21 +415,21 @@ Backlog-entries: zie [backlog.md § M11](../backlog.md#m11-claude-vraagt-gebruik
- Backlog-parser-self-test: `npx tsx prisma/seed-data/parse-backlog.ts` toont M11 met `priority=4 sprint=ACTIVE` - Backlog-parser-self-test: `npx tsx prisma/seed-data/parse-backlog.ts` toont M11 met `priority=4 sprint=ACTIVE`
- 6/6 acceptatie-scenario's groen - 6/6 acceptatie-scenario's groen
- `npm run lint && npx tsc --noEmit && npm test && npm run build` clean - `npm run lint && npx tsc --noEmit && npm test && npm run build` clean
- `vendor/scrum4me`-submodule sync in scrum4me-mcp na merge - `vendor/scrum4me`-submodule sync in mcp na merge
--- ---
## Branch- en commit-strategie ## Branch- en commit-strategie
Per [CLAUDE.md → Branch & PR Strategy](../../CLAUDE.md#branch--pr-strategy-strict--kostenbeheersing): Per [Branch & PR Strategy](../runbooks/branch-and-commit.md):
- **Eén branch op Scrum4Me**: `feat/M11-claude-questions` afgesplitst van `main` ná M10-merge - **Eén branch op Scrum4Me**: `feat/M11-claude-questions` afgesplitst van `main` ná M10-merge
- **Aparte branch op scrum4me-mcp**: `feat/M11-question-tools` - **Aparte branch op mcp**: `feat/M11-question-tools`
- Commits chronologisch per stap met ST-code in titel: - Commits chronologisch per stap met ST-code in titel:
``` ```
chore(M11): swap demo-active sprint from M10 to M11 chore(M11): swap demo-active sprint from M10 to M11
feat(ST-1101): add ClaudeQuestion model + notify_question_change trigger feat(ST-1101): add ClaudeQuestion model + notify_question_change trigger
feat(ST-1102): add 4 MCP question tools (in scrum4me-mcp) feat(ST-1102): add 4 MCP question tools (in mcp)
feat(ST-1103): add answerQuestion server action feat(ST-1103): add answerQuestion server action
feat(ST-1104): add /api/realtime/notifications user-scoped SSE feat(ST-1104): add /api/realtime/notifications user-scoped SSE
feat(ST-1104): filter entity='question' from solo-realtime stream feat(ST-1104): filter entity='question' from solo-realtime stream

View file

@ -1,8 +1,17 @@
---
title: "M9 — Actief Product Backlog"
status: active
audience: [maintainer, contributor]
language: nl
last_updated: 2026-05-03
applies_to: [M9]
---
# M9 — Actief Product Backlog # M9 — Actief Product Backlog
Eén "actief Product Backlog" per gebruiker, persistent op `User.active_product_id`. NavBar wordt: Producten | Product Backlog | Sprint | Solo | Todo's. Zonder actief PB zijn Backlog/Sprint/Solo disabled. Sprint is alleen klikbaar als er een sprint met status `ACTIVE` bestaat. Vervangt de bestaande `last_product`-cookieflow. Eén "actief Product Backlog" per gebruiker, persistent op `User.active_product_id`. NavBar wordt: Producten | Product Backlog | Sprint | Solo | Todo's. Zonder actief PB zijn Backlog/Sprint/Solo disabled. Sprint is alleen klikbaar als er een sprint met status `ACTIVE` bestaat. Vervangt de bestaande `last_product`-cookieflow.
Backlog-entries: zie [backlog.md § M9](../backlog.md#m9-actief-product-backlog). Backlog-entries: zie [backlog.md § M9](../backlog/index.md#m9-actief-product-backlog).
--- ---
@ -20,7 +29,7 @@ Backlog-entries: zie [backlog.md § M9](../backlog.md#m9-actief-product-backlog)
3. `npx prisma migrate dev --name add_user_active_product_id`. 3. `npx prisma migrate dev --name add_user_active_product_id`.
**Aandachtspunten** **Aandachtspunten**
- `vendor/scrum4me`-submodule in repo `scrum4me-mcp` heeft hetzelfde schema. Na merge moet daar `prisma generate && tsc --noEmit` slagen, anders breekt de wekelijkse drift-check (`trig_015FFUnxjz9WMuhhWNGBQKFD`). - `vendor/scrum4me`-submodule in repo `mcp` heeft hetzelfde schema. Na merge moet daar `prisma generate && tsc --noEmit` slagen, anders breekt de wekelijkse drift-check (`trig_015FFUnxjz9WMuhhWNGBQKFD`).
- Geen seed-wijziging nodig — `null` is correcte initiële staat. - Geen seed-wijziging nodig — `null` is correcte initiële staat.
**Verificatie** **Verificatie**

View file

@ -1,3 +1,12 @@
---
title: "ST-1109 — PBI krijgt een status (Ready / Blocked / Done)"
status: active
audience: [maintainer, contributor]
language: nl
last_updated: 2026-05-03
applies_to: [ST-1109]
---
# Plan — ST-1109 · PBI krijgt een status (Ready / Blocked / Done) # Plan — ST-1109 · PBI krijgt een status (Ready / Blocked / Done)
> Spiegel van het goedgekeurde plan dat tijdens de sessie is opgesteld in > Spiegel van het goedgekeurde plan dat tijdens de sessie is opgesteld in

View file

@ -1,3 +1,12 @@
---
title: "ST-1110 — Demo gebruiker read-only"
status: active
audience: [maintainer, contributor]
language: nl
last_updated: 2026-05-03
applies_to: [ST-1110]
---
# Plan: ST-1110 — Demo gebruiker read-only # Plan: ST-1110 — Demo gebruiker read-only
## Context ## Context

View file

@ -1,3 +1,12 @@
---
title: "ST-1111 — Voer uit-knop met Claude Code job queue"
status: active
audience: [maintainer, contributor]
language: nl
last_updated: 2026-05-03
applies_to: [ST-1111]
---
# ST-1111 — 'Voer uit'-knop met Claude Code job queue # ST-1111 — 'Voer uit'-knop met Claude Code job queue
**Story:** Als developer wil ik op het solo-scherm per task een 'Voer uit'-knop, zodat ik mijn lokale Claude Code-sessie kan inschakelen om de taak uit te voeren. **Story:** Als developer wil ik op het solo-scherm per task een 'Voer uit'-knop, zodat ik mijn lokale Claude Code-sessie kan inschakelen om de taak uit te voeren.
@ -14,7 +23,7 @@
| ST-1111.2 API: ClaudeJob status mappers | `a1b1f69` | | ST-1111.2 API: ClaudeJob status mappers | `a1b1f69` |
| ST-1111.3 Server actions: enqueue + cancel | `9d9fb4b` | | ST-1111.3 Server actions: enqueue + cancel | `9d9fb4b` |
| ST-1111.4 SSE: ClaudeJob events op solo-stream + initial state | `ece0aa9` | | ST-1111.4 SSE: ClaudeJob events op solo-stream + initial state | `ece0aa9` |
| ST-1111.5 MCP-tools (scrum4me-mcp repo — aparte PR) | — | | ST-1111.5 MCP-tools (mcp repo — aparte PR) | — |
| ST-1111.6 UI: 'Voer uit' + cancel in TaskDetailDialog | `b9c65eb` | | ST-1111.6 UI: 'Voer uit' + cancel in TaskDetailDialog | `b9c65eb` |
| ST-1111.7 UI: status-pill op SoloTaskCard | `dace427` | | ST-1111.7 UI: status-pill op SoloTaskCard | `dace427` |
| ST-1111.8 Tests: mappers + actions | `2c2a246` | | ST-1111.8 Tests: mappers + actions | `2c2a246` |
@ -41,7 +50,7 @@ Omdat `claude_jobs` geen row-trigger heeft (zoals `tasks` en `stories`), stuurt
await prisma.$executeRaw`SELECT pg_notify('scrum4me_changes', ${JSON.stringify(payload)}::text)` await prisma.$executeRaw`SELECT pg_notify('scrum4me_changes', ${JSON.stringify(payload)}::text)`
``` ```
Voordeel: expliciete controle over het payload-shape (met `type` i.p.v. `entity`). Nadeel: MCP-tools in de `scrum4me-mcp`-repo moeten hun eigen NOTIFY-aanroep hebben bij `update_job_status`. Voordeel: expliciete controle over het payload-shape (met `type` i.p.v. `entity`). Nadeel: MCP-tools in de `mcp`-repo moeten hun eigen NOTIFY-aanroep hebben bij `update_job_status`.
### SSE-routing ### SSE-routing

View file

@ -1,3 +1,12 @@
---
title: "ST-1114 — Copilot reviews op dashboard"
status: active
audience: [maintainer, contributor]
language: nl
last_updated: 2026-05-03
applies_to: [ST-1114]
---
# Plan — ST-1114 · Copilot reviews op dashboard # Plan — ST-1114 · Copilot reviews op dashboard
## Context ## Context

View file

@ -0,0 +1,150 @@
---
title: "CLAUDE.md workflow-update na M7 + ST-509/511/512/513"
status: done
audience: [maintainer, contributor]
language: nl
last_updated: 2026-05-03
applies_to: [M7, ST-509, ST-511, ST-512, ST-513]
---
# Plan: CLAUDE.md workflow-update na M7 + ST-509/511/512/513
## Aanleiding
`CLAUDE.md` is voor het laatst groot bijgewerkt op 2026-04-25 (`docs/decisions/agent-instructions-history.md`). Sindsdien is er substantieel werk geland dat de workflow raakt:
- **ST-511** entity codes (Product/PBI/Story) — branch- en commit-conventies hangen er nu aan vast
- **ST-513** API hardening — `400` (malformed JSON) vs `422` (zod-validatie), lowercase status-enums op API-grens, `StoryLog.metadata` JSONB
- **PR #2** review-saga (8 testbestanden faalden bij contract-flip) — duidelijk leerpunt: testpariteit hoort bij contract-wijziging
- **M7 MCP-server** — Claude Code praat nu native met Scrum4Me via `mcp__scrum4me__*` tools en de prompt `implement_next_story`. De huidige 7-stap "vraag-de-gebruiker"-loop in CLAUDE.md is daarmee gedateerd
- **lib/code-server.ts** vs **lib/code.ts** — split is nodig om client-bundle vrij te houden van `pg`. Als gotcha noemenswaard
- **Schema-drift cron** (`trig_015FFUnxjz9WMuhhWNGBQKFD`) — wekelijkse remote agent — agents moeten weten wat ze met zijn rapport doen
Doel: CLAUDE.md weerspiegelt de werkelijke 2026-04-27 workflow zonder dat het een changelog wordt.
## Scope — wat we wél en niet aanpassen
**Wel** (in `CLAUDE.md`):
1. Workflow-sectie — MCP-first met expliciete fallback
2. Conventies — uitbreiden met status-enums, foutcodes, test-pariteit, entity codes in commits
3. Implementatiepatronen — rij voor `lib/task-status.ts` en `lib/code-server.ts`-boundary toevoegen
4. Nieuwe sectie "MCP-integratie" — wat staat er, hoe te gebruiken, link naar mcp repo
5. Definition of Done — markeer expliciet als MVP-scope; M7 is post-MVP en heeft eigen acceptatie
**Niet**:
- Geen changelog of historiek in CLAUDE.md zelf — dat hoort in `docs/decisions/agent-instructions-history.md` (separate update)
- Geen volledige herschrijving — bestaande structuur blijft (Wat is Scrum4Me, Spec-tabel, Stack, Conventies, Commit Strategy, etc.)
- Geen wijziging in `AGENTS.md` (Codex) — die heeft geen MCP, mag los blijven
- Geen wijziging in functional-spec/architecture/styling docs — die zijn al actueel
## Concrete edits in `CLAUDE.md`
### 1. Sectie "Specificatiedocumenten" — uitbreiden
Voeg toe onder de bestaande tabel:
| Document | Gebruik voor |
|---|---|
| `https://github.com/madhura68/scrum4me-mcp` | MCP-server repo: tools, prompts, schema-sync workflow |
(`docs/api/rest-contract.md` staat er al — laten staan.)
### 2. Sectie "Waar te beginnen" — herschrijven
Vervang de 7-stap manual loop door een dual-track:
**Track A — via Claude Code MCP (aanbevolen)**:
```
1. Roep `mcp__scrum4me__implement_next_story` aan met product_id
(of `list_products` als je het id niet weet)
2. De prompt orkestreert: get_claude_context → log_implementation
→ per task in_progress/done → log_test_result → log_commit
3. Bouw de tasks in volgorde van `sort_order`
4. Test: `npm run lint && npm test && npm run build`
5. Commit per laag (zie Commit Strategy)
```
**Track B — manueel (Codex of zonder MCP)**:
- Lees task in `docs/backlog/index.md`
- Volg verder de bestaande 7-stappen-loop
### 3. Sectie "Implementatiepatronen" — uitbreiden
Twee rijen toevoegen aan de patronen-tabel:
| Patroon | Bestand |
|---|---|
| Status-enum mapping (DB ↔ API) | `lib/task-status.ts` |
| Client/server module-boundary | regel: `*-server.ts` bevat DB-calls, `*.ts` is pure helpers — nooit `import { ... } from 'lib/foo-server'` in een client component |
### 4. Sectie "Conventies" — vier regels toevoegen
Voeg toe aan de bestaande lijst:
- **Entity codes**: gebruik product/PBI/story-codes in commit-titles wanneer aanwezig (`feat(ST-356.2): ...`); branchnaam blijft `feat/ST-XXX-slug`
- **Status-enums op API**: lowercase (`todo|in_progress|review|done`, `open|in_sprint|done`); DB houdt UPPER_SNAKE; conversie via `lib/task-status.ts`-mappers — nooit ad-hoc lowercase elders
- **Foutcodes API**: `400` alleen voor malformed JSON-body (parse-fout); `422` voor zod-validatie en well-formed-maar-niet-acceptabel; `403` voor demo-tokens. Documenteren in `docs/api/rest-contract.md`
- **Tests volgen contract**: bij API-contract-wijziging (status, foutcode, response-shape) MOET in dezelfde commit ook `__tests__/api/` bijgewerkt worden — een falende test op review betekent niet dat de tests "stuk zijn" maar dat de wijziging onvolledig is
### 5. Nieuwe sectie "MCP-integratie" — toevoegen vóór "Definition of Done"
Korte sectie (~15 regels):
```markdown
## MCP-integratie
Scrum4Me heeft een eigen MCP-server (repo: `madhura68/scrum4me-mcp`)
die deze API exposed als native tools voor Claude Code.
### Tools beschikbaar in Claude Code
- `mcp__scrum4me__health` — service + DB ping
- `mcp__scrum4me__list_products` — producten waar je toegang tot hebt
- `mcp__scrum4me__get_claude_context` — bundled product/sprint/story/todos
- `mcp__scrum4me__update_task_status`, `_update_task_plan`
- `mcp__scrum4me__log_implementation`, `_log_test_result`, `_log_commit`
- `mcp__scrum4me__create_todo`
### Prompt
- `implement_next_story` (arg: `product_id`) — end-to-end workflow
### Schema-drift bewaking
Wekelijks (maandag 08:00 Amsterdam) draait een remote agent
(`trig_015FFUnxjz9WMuhhWNGBQKFD`) die `vendor/scrum4me` syncet en
`prisma:generate + typecheck` uitvoert in mcp. Als die agent
een drift-rapport opent, hoort dat **vóór** een Scrum4Me-PR met
schema-wijziging gemerged kan worden — zodat de MCP-server niet
stilletjes breekt op runtime.
```
### 6. Sectie "Definition of Done" — kop verduidelijken
Wijzig `## Definition of Done``## Definition of Done (MVP)` en voeg eronder een korte zin toe: *"M7 (MCP-server) is post-MVP en heeft eigen acceptatie in `docs/backlog/index.md`."*
## Bijwerken van auditdoc
Voeg een sectie aan `docs/decisions/agent-instructions-history.md` toe (datum: 2026-04-27) met:
- Aanleiding: ST-509/511/512/513 + M7 + PR #2 review-saga
- Gecontroleerde wijzigingen: zelfde tabel-stijl als 2026-04-25
- Nieuwe regels: status-enums op API-grens, error-code split 400/422, test-pariteit bij contract-wijziging, client/server module-boundary
- Verwijzing naar mcp repo en schema-drift cron
## Volgorde van uitvoering
1. **Edits in `CLAUDE.md`** — alle 6 secties hierboven, in volgorde
2. **Edits in `docs/decisions/agent-instructions-history.md`** — nieuwe sectie 2026-04-27
3. **`npm run lint`** — sanity check
4. **Commit als één logische change**`docs(workflow): align CLAUDE.md with M7 and post-PR-#2 contract`
5. **PR openen** — review-bare scope, deploys triggeren maar zijn docs-only
## Wat het NIET oplost
- `AGENTS.md` (Codex) blijft achter; los aan te pakken indien gewenst
- Eventuele drift in `docs/specs/functional.md` rond status-enums — niet onderzocht; te volgen bij volgende audit
- Geen check of de losse pattern-files in `docs/patterns/` nog kloppen — ook volgende audit
## Geschatte size
- ~80 regels toegevoegd/gewijzigd in `CLAUDE.md`
- ~30 regels nieuw in `docs/decisions/agent-instructions-history.md`
- 1 commit, 1 PR

View file

@ -0,0 +1,111 @@
---
title: "Herbruikbaar scripts/insert-milestone.ts"
status: done
audience: [maintainer, contributor]
language: nl
last_updated: 2026-05-03
applies_to: []
---
# Plan: herbruikbaar `scripts/insert-milestone.ts`
## Doel
Eén commando dat een specifieke milestone (PBI + stories + tasks) uit de backlog leest en idempotent toevoegt aan de DB, zónder bestaande data te raken. Voor M8 nu, en voor M9..M∞ later.
## Bron-keuze: backlog ipv plan-bestand
Twee bronnen denkbaar:
- **`.Plans/<datum>-<slug>.md`** — freeform plan-tekst, niet gestructureerd, niet gecommit
- **`docs/backlog/index.md`** — al strict gestructureerd, gecommit, single source of truth voor alle bestaande seed-pipelines
Voorstel: het script leest de **backlog**. Workflow blijft natuurlijk:
1. Plan schrijven naar `.Plans/<naam>.md` (lokaal, draft)
2. Milestone-sectie + stories formaliseren in `docs/backlog/index.md` (PR)
3. Na merge: `npm run db:insert-milestone -- M8 [--product SCRUM4ME]`
Eén canonical bron, geen ambiguïteit, en de bestaande parser doet 90% van het werk al.
## Wijzigingen
### 1. `prisma/seed-data/parse-backlog.ts` — tolerant maken
Huidige parser kent alleen M0..M6 in `MILESTONE_PRIORITY/_GOAL/_SPRINT_STATUS` + asserts ≥8 milestones / ≥60 stories. M7 en M8 worden nu stilletjes overgeslagen.
Concrete edits:
- Voeg `M7` en `M8` toe aan de drie maps (M7: priority 4, sprint COMPLETED, goal "MCP-server voor Claude Code"; M8: priority 4, sprint COMPLETED, goal "Realtime updates voor Solo Paneel")
- Voor onbekende sleutels: fallback naar `priority: 4`, `sprint_status: 'COMPLETED'`, `goal: <header-title>`. Dat maakt M9..M∞ vanzelf bruikbaar zonder code-wijziging
- Verwijder de strikte filter `KNOWN_KEYS.includes(...)` of verleg naar een "alle-M[\d.]+ headers" check
- Voeg optionele `loadBacklog(repoRoot, { strict?: boolean })` toe. `strict: true` (default) behoudt de bestaande "≥8 milestones, ≥60 stories" asserts (zodat de seed niet stilletjes anders gedraagt). Insert-milestone roept met `strict: false`
### 2. `scripts/insert-milestone.ts` (nieuw, ~90 regels)
```
Usage: tsx scripts/insert-milestone.ts <milestone-key> [--product <code>] [--dry-run]
Default product code: SCRUM4ME
```
Logica:
1. Parse args; valideer dat milestone-key matcht `^M[\d.]+$`
2. `loadBacklog(repoRoot, { strict: false })`
3. Zoek milestone op `key`; faal helder met "milestone <key> not found in docs/backlog/index.md" als ie er niet in staat
4. Lookup product via `code` (default `SCRUM4ME`); faal als niet gevonden
5. Upsert PBI:
- `where: { product_id_code: { product_id, code: milestone.key } }`
- sort_order = `(max(sort_order) van bestaande PBIs in product) + 1` als nieuw, anders ongemoeid
6. Voor elke story:
- Upsert Story op `(product_id, code = story.ref)`
- status = `'DONE'` of `'OPEN'` zoals gemarkeerd in markdown
- sort_order, priority en pbi_id correct ingesteld
7. Voor elke task: bulk insert **alleen** als de story op dit moment 0 tasks heeft (idempotent — herhaling dupliceert niets)
8. Print samenvatting: `M8: PBI created, 6 stories upserted (1 created, 5 unchanged), 6 tasks created`
9. `--dry-run`: alle DB-calls overslaan, alleen wat het zou doen printen
Edge cases:
- Story-code conflict tussen producten: schema heeft `@@unique([product_id, code])` op Story dus dit is per-product safe
- Tasks zonder `code` veld in DB (klopt — code wordt afgeleid van story.code + index in get_claude_context)
- Demo-product: script accepteert `--product DEMO` o.i.d. — niet hardcoded SCRUM4ME
### 3. `package.json` script
```json
"db:insert-milestone": "tsx scripts/insert-milestone.ts"
```
### 4. Verificatie na implementatie
- Dry-run eerst: `npm run db:insert-milestone -- M8 --dry-run`
- Daarna echt: `npm run db:insert-milestone -- M8`
- In Prisma Studio of via SQL: zie M8 PBI, 6 stories, 6 tasks onder SCRUM4ME-product
- Tweede run: `npm run db:insert-milestone -- M8` → "0 created, 6 unchanged" — geen duplicaten
- Niet-bestaande key: `npm run db:insert-milestone -- M99` → "milestone M99 not found"
- Bestaande seed-flow blijft werken: `prisma db seed` met `strict: true` faalt nog steeds bij format-drift in de backlog
## Branch- en PR-strategie
`scripts/insert-milestone.ts` is orthogonaal aan ST-801. Twee keuzes:
- **A. Eigen mini-branch + PR**`tooling/insert-milestone-script`, ~95 regels code, makkelijk reviewbaar, gemerged voordat M8 verder gaat. Daarna gebruiken om M8 in DB te zetten en met de implementatie door.
- **B. Aan ST-801 plakken** — voegt scope toe aan een PR die al code ↔ infra-overschrijdend is (migratie + tools).
Voorgestelde keuze: **A**. De tool is breder bruikbaar dan M8 alleen.
## Volgorde
1. Switch naar `main` (ST-801 blijft op zijn eigen branch staan)
2. Branch `tooling/insert-milestone-script`
3. Edit `parse-backlog.ts` (M7/M8 maps + tolerant + strict-mode option)
4. Schrijf `scripts/insert-milestone.ts`
5. Voeg `db:insert-milestone` toe aan `package.json`
6. Lokaal testen met M8 (dry-run + echt + tweede run)
7. Commit, push, PR
8. Na merge: tool gebruiken om M8 in DB te krijgen, daarna ST-802 oppakken op feat/ST-801-branch
## Geschatte size
- ~10 regels parser-edit
- ~95 regels nieuw script
- ~1 regel package.json
- ~25 regels test/usage doc in script-comment
- 1 commit, 1 PR

View file

@ -0,0 +1,195 @@
---
title: "Realtime updates voor Solo Paneel (M8)"
status: done
audience: [maintainer, contributor]
language: nl
last_updated: 2026-05-03
applies_to: [M8]
---
# Plan: Realtime updates voor Solo Paneel (M8)
## Aanleiding
Wanneer Lars in zijn Solo Paneel werkt en parallel Claude Code (via MCP) of Codex aan dezelfde sprint sleutelt, ziet hij de gevolgen pas na een refresh. We willen DB-wijzigingen op `tasks`/`stories` van zijn actieve sprint live in beeld zien. Vraag van de gebruiker: "open een websocket".
## Transport-keuze — niet écht een WebSocket
Vercel-deploys ondersteunen geen stateful native WebSockets in serverless of Edge functions. Drie reële opties:
| Optie | Werkt op Vercel | Externe dienst | Latency | Complexiteit |
|---|---|---|---|---|
| **A. SSE + Postgres LISTEN/NOTIFY** | ✅ (Node runtime, streaming response) | nee | <100ms na DB-write | gemiddeld |
| B. SSE + polling 23s | ✅ | nee | 13s | laag |
| C. Pusher/Ably (echte WS) | ✅ | ja (gratis tier) | <50ms | laag, maar elke schrijver moet publishen |
**Voorgestelde keuze: A — SSE met Postgres LISTEN/NOTIFY.**
Reden:
- Eén bron van waarheid: de DB. Web-mutations, REST-API én MCP schrijven allemaal naar Postgres; een trigger NOTIFY't onafhankelijk van de schrijver. Geen coördinatie nodig met mcp.
- Geen externe dienst, geen extra dep, geen kosten erbij.
- Neon ondersteunt LISTEN/NOTIFY op directe verbindingen. `DIRECT_URL` is al geconfigureerd.
- Naar de client toe: éénrichtingsverkeer — server pusht events, client doet mutaties via bestaande Server Actions/REST. SSE volstaat dus; we hoeven geen full-duplex.
- Voor de gebruiker is het verschil onmerkbaar: realtime updates komen binnen, browsers ondersteunen `EventSource` native.
We kiezen B (polling) niet omdat het meer DB-load geeft en je Pusher-achtige latency niet haalt. We kiezen C niet vanwege coördinatieoverhead met de MCP-server (extra publish-step in mcp).
## Architectuur
```
┌─────────────────────────────────────────────────────────────────────────┐
│ Postgres (Neon) │
│ ┌────────────────────────┐ │
│ │ TRIGGER on tasks │──► pg_notify('scrum4me_solo', payload_json) │
│ │ TRIGGER on stories │ │
│ └────────────────────────┘ │
└──────────────┬──────────────────────────────────────────────────────────┘
│ LISTEN scrum4me_solo
┌─────────────────────────────────────────────────────────────────────────┐
│ Next.js Node.js runtime route: /api/realtime/solo │
│ - auth via iron-session cookie │
│ - opent dedicated pg client (DIRECT_URL), LISTEN scrum4me_solo │
│ - filtert events: alleen tasks/stories in actieve sprint van een │
│ product waar user lid/eigenaar is, EN (assignee_id == user OR │
│ onbeklemtoonde unassigned-story-list) │
│ - stuurt SSE: data: {type, entity, id, fields} \n\n │
│ - heartbeat \n\n elke 25s │
│ - sluit zelf na 4 min (Vercel maxDuration safety); client reconnect │
└──────────────┬──────────────────────────────────────────────────────────┘
│ EventSource('/api/realtime/solo?product_id=...')
┌─────────────────────────────────────────────────────────────────────────┐
│ Browser — Solo Paneel │
│ - useSoloRealtime(productId) hook │
│ - reconnect met exponential backoff (max 30s) │
│ - Page Visibility API: close on hidden, reopen on visible │
│ - dispatcht naar solo-store: applyTaskUpdate, applyTaskCreate, │
│ applyTaskDelete, applyStoryUpdate (assignee/title/status) │
│ - reconcile-policy: skip update als optimistic in-flight is voor die │
│ task; anders server wint │
└─────────────────────────────────────────────────────────────────────────┘
```
## Filtering — wie krijgt welke events?
De trigger NOTIFY't elke task/story-mutatie globaal. De SSE-handler is verantwoordelijk voor toegangs- en relevantie-filtering:
1. **Toegang**: alleen events waarvan de gerelateerde `story.product_id` in `productAccessFilter(userId)` zit.
2. **Sprint-scope**: alleen events binnen de actieve sprint van het product dat in de query-parameter zit.
3. **Persoonlijke relevantie**: tasks waar `story.assignee_id == userId` (jouw kolommen), plus stories met `assignee_id == null` (de "claim me" lijst).
Per event extra DB-roundtrip om dit te checken zou duur zijn. Twee oplossingen, bij voorkeur (b):
(a) Triggerpayload bevat `product_id`, `sprint_id`, `assignee_id` zodat de handler in-memory kan filteren — geen extra DB-call.
(b) Cache in handler: bij connect resolveert de handler `userId → activeSprintId, productId, assignedStoryIds`. Bij elke notify checkt het de payload tegen die set; bij story-create/assignee-change herwoordt het de set on demand.
Strategie: combineer (a) trigger zet `product_id` en `assignee_id` in de payload + (b) handler cacht `(activeSprintId, productId, accessibleProducts)` voor de connectie-duur.
## Concrete implementatie — stories
### ST-801 Postgres LISTEN/NOTIFY-infrastructuur
- Migration `prisma/migrations/<ts>_add_solo_realtime_triggers/migration.sql`:
- `CREATE OR REPLACE FUNCTION notify_solo_change() RETURNS TRIGGER ...` — bouwt JSON met `op` (`INSERT`/`UPDATE`/`DELETE`), `entity` (`task`/`story`), `id`, `product_id`, `sprint_id`, `assignee_id`, `fields` (alleen gewijzigde kolommen bij UPDATE)
- Triggers `AFTER INSERT OR UPDATE OR DELETE ON tasks`, idem op `stories`
- Pas `prisma migrate deploy` toe (idempotent, geen schema-wijziging dus geen TS-impact)
- Done when: `psql $DIRECT_URL -c "LISTEN scrum4me_solo;"` toont een payload bij UI-mutatie
### ST-802 SSE-route `/api/realtime/solo`
- Bestand `app/api/realtime/solo/route.ts`, `runtime: 'nodejs'`, `maxDuration: 300`
- Gebruikt `pg.Client` (niet de Prisma adapter — directe `LISTEN`-verbinding)
- Auth via iron-session, 401 zonder cookie
- Query-parameter `product_id`, 403 zonder access
- Resolveert active sprint id eenmalig; cachet die in connection-scope
- `ReadableStream` met heartbeat-interval 25s, hard close na 240s
- Filter per event op `product_id == requested && (assignee_id == userId || (entity == 'story' && assignee_id == null))`
- Logged via `console.error` bij pg-disconnect
- Done when: handmatig met `curl -N` op localhost krijg je events binnen 1s na een UI-mutatie
### ST-803 Client hook `useSoloRealtime(productId)`
- `lib/realtime/use-solo-realtime.ts` (client-only)
- Opent `EventSource('/api/realtime/solo?product_id=' + productId)`
- Reconnect: exponential backoff start 1s → 30s, reset op succesvolle connect
- Page Visibility: `document.visibilityState === 'hidden'` → close; bij visible → reopen
- Cleanup op unmount
- Dispatcht events naar solo-store via nieuwe acties (zie ST-804)
- Done when: tab wisselen sluit/opent connectie zichtbaar in DevTools Network
### ST-804 Solo-store realtime-acties
- Uitbreiden `stores/solo-store.ts`:
- `applyTaskUpdate(taskId, fields)` — merge in tasks-record; skip als `pendingOps[taskId]` set is
- `applyTaskCreate(task)` — alleen als de task in de eigen kolommen hoort (assignee_id == userId)
- `applyTaskDelete(taskId)`
- `applyStoryAssignment(storyId, assigneeId)` — re-fetch unassigned-list (kleine GET) of ontvang als deel van payload
- `markPending(taskId)`/`clearPending(taskId)` — optimistic-flow markeert mutaties die we zelf doen, zodat we de echo van onze eigen NOTIFY niet dubbel verwerken
- Done when: unit-test op solo-store met simulated events laat juiste state zien
### ST-805 Wire-up in SoloBoard
- `components/solo/solo-board.tsx`: roep `useSoloRealtime(productId)` aan na `useEffect`-init van tasks
- Klein "live" / "verbinden..." status-indicator (status uit hook): groene stip / pulserende grijze stip
- Toast bij langer dan 5s disconnected
- Done when: open Solo paneel in twee tabs, mutate task in tab A, zie status flippen in tab B binnen 12s zonder refresh
### ST-806 Documentatie + acceptatietest
- Update `docs/architecture.md`: nieuwe sectie "Realtime updates" met diagram en filtering-regels
- Update `CLAUDE.md`: vermelding dat Solo Paneel realtime is + dat MCP-writes vanzelf doorkomen
- Update `docs/api/rest-contract.md`: korte note over `/api/realtime/solo` (Bearer auth, SSE format)
- E2E-acceptatie: lijst van scenario's (zelfde gebruiker twee tabs, MCP-write, REST-write, story-claim, network-flap) handmatig getest
- Done when: scenario's lopen door zonder onverwachte gedragingen
## Backlog-edits
In `docs/backlog/index.md`:
1. **Milestone-overzicht** — rij toevoegen onder M7:
```
| M8: Realtime Solo Paneel | Live updates voor stories/tasks via SSE+LISTEN/NOTIFY | ST-801 ST-806 |
```
2. **Sectie M8** toevoegen na de M7-sectie, met de zes stories hierboven (ST-801..ST-806) inclusief "Done when"-criteria. Allemaal `[ ]` (nog niet gestart).
## Wijzigingen elders
- `.env.example` blijft ongewijzigd (DIRECT_URL stond er al)
- `docs/architecture.md` — sectie "Realtime updates" met diagram en regel "alle UPDATE-triggers zitten op tasks/stories; nieuwe entiteiten erbij vragen om uitbreiding van de trigger-functie"
- Geen wijziging in `lib/code.ts` of `lib/code-server.ts` — dit is server-only realtime
- Schema-drift agent in mcp pikt de migratie automatisch op (geen Prisma-modelwijziging maar wel een nieuwe migratie); typecheck blijft groen omdat we geen Prisma Client-wijziging hebben
## Risico's en mitigaties
| Risico | Mitigatie |
|---|---|
| Vercel sluit Node-route na maxDuration | Hard-close server-side bij 240s + automatische client-reconnect; gebruiker merkt dit niet |
| Echo van eigen optimistic mutation | `markPending`/`clearPending` in solo-store; skip als `pendingOps[taskId]` set is |
| Connection leaks (open `pg.Client`'s) | `req.signal.addEventListener('abort')` cleanup; bij Edge cold-start sluit Vercel zelf |
| Trigger overhead op writes | Triggers zijn lichtgewicht (één pg_notify call); meet bij rollout |
| Oude pg_notify payloads >8kb | Zorg dat we alleen primitives (id, status, sort_order, etc.) sturen — geen description/implementation_plan in de payload, daar is een refetch voor |
| Test-DB heeft geen triggers | Migratie automatisch toegepast in CI (Prisma migrate deploy); bestaande tests blijven groen |
| MCP-server schema-sync detecteert migratie als drift | False alarm — wekelijkse cron rapporteert "schema-prisma diff", maar typecheck blijft groen omdat het alleen migratie-SQL is. Beoordeel handmatig bij rapport |
## Wat dit NIET oplost
- Realtime in Sprint Backlog of Product Backlog — alleen Solo Paneel
- Conflict-merge bij gelijktijdige updates van twee gebruikers (last-write-wins blijft)
- Mobile pagina (out of scope desktop-first MVP)
- Audit-trail van wie wat wanneer veranderde (bestaat al via StoryLog)
## Volgorde van uitvoering
1. Branch `feat/m8-realtime-solo` van main
2. ST-801 (migratie + trigger) — commit, lokaal verifiëren met `psql LISTEN`
3. ST-802 (SSE-route) — commit, `curl -N` lokaal testen tegen lokale UI-mutatie
4. ST-803 (client hook) — commit
5. ST-804 (store-uitbreiding) — commit, met unit-test
6. ST-805 (wire-up + UI-indicator) — commit
7. ST-806 (docs + acceptatie) — commit
8. PR openen — Vercel preview-deploy laat realtime werken op preview-DB (mits trigger via `migrate deploy` mee)
9. Na review: merge
## Geschatte size
- ~6 stories, ~1218 commits
- 1 migratie, 1 nieuwe route, 1 nieuwe hook, kleine store-uitbreiding, UI-indicator
- ~400 regels code + ~80 regels docs
- 1 PR

View file

@ -0,0 +1,499 @@
---
title: Docs-restructuur — geoptimaliseerd voor AI-lookup
status: proposal
audience: maintainer, ai-agent
language: nl
last_updated: 2026-05-02
related:
- CLAUDE.md
- AGENTS.md
- README.md
- docs/decisions/agent-instructions-history.md
---
# Plan — Docs-restructuur voor AI-lookup
> Doel van dit plan: de huidige documentatie- en instructielaag van Scrum4Me omzetten naar een structuur die een AI-agent (Claude Code, Codex, een MCP-worker) in zo min mogelijk tokens en tool-calls het juiste document laat vinden, lezen en toepassen — zonder dat de mens-leesbaarheid eronder lijdt.
Dit is een **proposal**, niet een afgerond ontwerp. Lees het, markeer wat je niet wil, en ik werk het uit naar een uitvoerbaar migratieplan met file-per-file diff.
---
## 1. Waarom dit plan
Een AI-agent doet voor élke beslissing typisch dit:
1. Leest `CLAUDE.md` (of `AGENTS.md`) volledig in context.
2. Scant `docs/` met `ls`/`grep`/`glob` om relevante bestanden te kiezen.
3. Leest één of meerdere docs volledig — vaak meer dan nodig, omdat doc-grenzen vaag zijn.
4. Vindt cross-refs (`zie docs/X#Y`) en herhaalt stap 3.
Elke stap kost tokens en latency. Als de bestandsboom, naamgeving of inhoud onduidelijk is, leest de agent te veel of het verkeerde — en maakt vervolgens beslissingen op verouderde of irrelevante informatie.
**Concrete kosten in deze repo (gemeten 2026-05-02):**
| Plek | Bestanden | Regels totaal | Grootste bestand |
|---|---:|---:|---|
| Root (CLAUDE.md, README.md, AGENTS.md, Brainstorm.md) | 4 | 679 | CLAUDE.md (340) |
| `docs/` (root, exclusief subdirs) | 13 | 5.873 | architecture.md (1.247) |
| `docs/patterns/` | 11 | 1.013 | dialog.md (387) |
| `docs/plans/` | 8 | 2.121 | M10-qr-pairing-login.md (885) |
| `.Plans/` (parallelle plan-historie) | 3 | ~600 | — |
| **Totaal** | **39** | **~10.700** | — |
Bij elke turn die met `CLAUDE.md` start, wordt minimaal 340 regels orientation in de context geladen — vóór er één regel code is gelezen. De agent kan vervolgens uit ~9.000 regels documentatie het juiste fragment moeten kiezen op basis van bestandsnamen alleen, want er is geen front-matter en geen index.
---
## 2. Wat ik aantrof — review per laag
### 2.1 Root-niveau orientation
| Bestand | Wat het doet vandaag | Probleem |
|---|---|---|
| `CLAUDE.md` (340 r) | Doel, doc-index, twee start-tracks, tech stack, UI-conventies, patroon-index, env vars, conventies, branch/commit-strategie, scrum-terminologie, MCP, deployment, DoD | Té breed: oriëntatie + harde regels + referentie-tabellen + procedures. Alles wordt elke turn geladen. |
| `AGENTS.md` (38 r) | Codex-variant van CLAUDE.md | Duplicatie: 80% overlapt met CLAUDE.md (access-control, doc-sync, verificatie). Twee waarheden die uit elkaar kunnen lopen. |
| `README.md` (285 r) | Portfolio-pitch, stack, setup, routes, dev-flow | Mensgericht (recruiters, GitHub-bezoekers). Goed dat het bestaat — niet aanraken. |
| `Brainstorm.md` (16 r) | Stukjes Prisma-schema, JSON-snippet en HTML-DOM-dump zonder context | **Dood bestand**, weghalen of verplaatsen naar `docs/scratch/`. |
### 2.2 `docs/` root
| Bestand | Regels | Waar het thuishoort |
|---|---:|---|
| `architecture.md` | 1.247 | `docs/architecture/` — splitsen (zie §4) |
| `functional.md` | 650 | `docs/specs/functional.md` |
| `backlog.md` | 751 | `docs/backlog/index.md` |
| `product-backlog.md` | 454 | `docs/backlog/product-historical.md` (referentie, zie noot in CLAUDE.md) |
| `personas.md` | 138 | `docs/specs/personas.md` |
| `styling.md` | 670 | `docs/design/styling.md` |
| `md3-color-scheme.md` | 941 | `docs/design/styling.md` (overlapt deels met `styling.md` — kandidaat voor merge) |
| `test-plan.md` | 454 | `docs/qa/api-test-plan.md` |
| `pbi-dialog.md` | 120 | `docs/specs/dialogs/pbi.md` |
| `story-dialog.md` | 163 | `docs/specs/dialogs/story.md` |
| `task-dialog.md` | 127 | `docs/specs/dialogs/task.md` |
| `solo-paneel-spec.md` | 771 | `docs/specs/solo-panel.md` |
| `api.md` | 520 | `docs/api/rest-contract.md` |
| `decisions/agent-instructions-history.md` | 173 | `docs/decisions/agent-instructions.md` (ADR-stijl) |
| `erd.svg`, `icons.html` | — | `docs/assets/` |
**Patroon dat opvalt:** alles met prefix `` — dat prefix is overbodig, je staat al in `docs/` van de Scrum4Me-repo. Verwijderen scheelt visuele ruis bij `ls`.
**Inconsistente capitalization:** één bestand `md3-color-scheme.md` (snake + UpperCamel), de rest kebab. Eén bestand `api.md` (UPPER), de rest lowercase.
### 2.3 `docs/patterns/`
11 patronen, elk 25390 regels. Goed als concept, maar:
- `test.md` bevat letterlijk het woord "test" — junkfile, weghalen.
- Geen front-matter; agent moet titel + intro lezen om te weten of het patroon van toepassing is.
- Naamgeving inconsistent: `iron-session.md`, `qr-login.md`, `claude-question-channel.md` — sommige domeinspecifiek, andere generiek. Geen prefix die laat zien of het een **rule** (verplicht), **recipe** (voorbeeldcode) of **explainer** is.
### 2.4 `docs/plans/`
- 8 plans. Eén heeft een filename met spaties en em-dash: `tweede-claude-agent-planning.md` — breekt grep/glob/git-workflows op sommige shells, en is moeilijk te linken. Omnoemen naar `tweede-claude-agent-planning.md`.
- ST-1109-pbi-status.md verwijst naar `~/.claude/plans/welke-rioriteiten-heeft-een-mighty-shell.md` (ook nog typo "rioriteiten") — externe locatie die niet in de repo zit en die de agent niet kan lezen.
- `MEMORY.md` wordt op meerdere plaatsen genoemd maar bestaat niet in de repo.
### 2.5 `.Plans/` (root)
3 historische planfiles uit april 2026, parallel aan `docs/plans/`. Twee waarheden voor "waar staan plannen". Voorstel: archiveren naar `docs/plans/archive/` of weghalen.
### 2.6 Cross-referenties en dode links
- CLAUDE.md verwijst naar `docs/architecture.md#demo-user-policy` — die anchor bestaat (regel 1068 `## Demo-user policy (ST-1110)`), dus dit is OK; maar er bestaat geen lint die garandeert dat dit zo blijft als de header verandert.
- ST-1109-pbi-status.md verwijst naar `~/.claude/plans/welke-rioriteiten-heeft-een-mighty-shell.md` — externe locatie buiten de repo, plus typo "rioriteiten". Een agent kan die niet lezen.
- README.md verwijst niet naar CLAUDE.md/AGENTS.md (mensbezoekers vinden de agent-instructie laag niet).
- Geen enkel doc heeft een "Zie ook"-blok aan de onderkant. Cross-navigatie tussen patroon ↔ spec ↔ plan moet de agent zelf reconstrueren.
### 2.7 Wat er níet is (en zou moeten)
- **Geen index/manifest.** Een agent die `glob "docs/**/*.md"` doet, krijgt 30+ paden zonder context.
- **Geen front-matter.** Geen status (draft/active/deprecated), audience, last-updated, related.
- **Geen ADR-laag.** Beslissingen zoals "waarom geen Radix maar @base-ui/react", "waarom float sort_order", "waarom one-branch-per-milestone" zitten verstrooid in CLAUDE.md, README en losse plans. Een `docs/decisions/`-folder met ADR-format zou ze vindbaar maken.
- **Geen glossary.** Domeintermen (PBI, Story, Sprint, Solo, Todo, demo-token) zijn alleen impliciet gedefinieerd in de functional spec.
- **Geen "lookup-hints" in de doc-index.** CLAUDE.md zegt *waarvoor* je een doc gebruikt, niet *wanneer je het NIET hoeft te lezen*.
---
## 3. Doelen voor de nieuwe structuur
In volgorde van belangrijkheid:
1. **Eén goedkope orientation-laag.** Een agent moet ≤150 regels lezen om te weten waar hij verder moet kijken.
2. **Voorspelbare paden.** `docs/<topic>/<entity-or-feature>.md` zonder uitzonderingen.
3. **Machine-leesbare metadata.** YAML-front-matter op élk doc met minimaal `status`, `audience`, `last_updated`, `related`.
4. **Per-doc lookup-hint.** Eén zin "Lees dit als …" bovenaan; één zin "Niet hiervoor lezen: …" om verkeerd ophalen te voorkomen.
5. **Splitsing van regels (verplicht) en uitleg (referentie).** Regels in een korte rule-doc; voorbeeldcode en rationale in een aparte recipe/explainer.
6. **Eén bron-van-waarheid per onderwerp.** Geen Codex-vs-Claude-duplicatie; AGENTS.md wordt een 10-regelige verwijzing naar CLAUDE.md.
---
## 4. Voorgestelde doelstructuur
```
/ (repo-root)
├── README.md (mens, portfolio — ongewijzigd)
├── CLAUDE.md (agent-orientation, ≤150 regels — zie §5)
├── AGENTS.md (10 regels: "alles in CLAUDE.md geldt ook voor jou")
├── docs/
│ ├── INDEX.md (NIEUW — manifest met front-matter van alle docs)
│ ├── glossary.md (NIEUW — PBI, Story, Sprint, demo-token, …)
│ │
│ ├── architecture/
│ │ ├── overview.md (uit huidige architecture.md §1§3)
│ │ ├── data-model.md (uit §Datamodel + §Prisma Schema)
│ │ ├── auth-and-sessions.md (uit §Authenticatieflow)
│ │ ├── qr-pairing.md (uit §QR-pairing flow)
│ │ ├── claude-question-channel.md (uit §Vraag-antwoord-kanaal)
│ │ └── project-structure.md (uit §Projectstructuur)
│ │
│ ├── specs/
│ │ ├── functional.md (huidige functional.md)
│ │ ├── personas.md
│ │ ├── solo-panel.md
│ │ └── dialogs/
│ │ ├── pbi.md
│ │ ├── story.md
│ │ └── task.md
│ │
│ ├── design/
│ │ ├── styling.md (samengevoegd uit styling + MD3-color)
│ │ └── color-tokens.md (alleen het token-overzicht)
│ │
│ ├── api/
│ │ ├── rest-contract.md (huidige api.md)
│ │ └── error-codes.md (afgesplitst — vandaag verspreid)
│ │
│ ├── patterns/ (RULES — kort en bindend)
│ │ ├── 00-conventions.md (server-action, prisma-client, route-handler — kort)
│ │ ├── dialog.md
│ │ ├── sort-order.md
│ │ ├── zustand-optimistic.md
│ │ ├── iron-session.md
│ │ ├── proxy.md (was middleware.md — nieuwe naam)
│ │ ├── qr-pairing.md
│ │ └── claude-question-channel.md
│ │
│ ├── recipes/ (NIEUW — uitgewerkte voorbeeldcode bij rules)
│ │ └── … (één recipe per pattern dat code-snippets had)
│ │
│ ├── runbooks/ (NIEUW — operationele procedures)
│ │ ├── deploy-vercel.md (uit CLAUDE.md §Deployment)
│ │ ├── env-vars.md (uit CLAUDE.md §Env vars + .env.example)
│ │ └── local-dev.md (huidige README §setup, geëxtraheerd)
│ │
│ ├── decisions/ (NIEUW — ADR-stijl)
│ │ ├── 0001-base-ui-over-radix.md
│ │ ├── 0002-float-sort-order.md
│ │ ├── 0003-one-branch-per-milestone.md
│ │ ├── 0004-status-enum-mapping.md
│ │ └── 0005-agent-instructions.md (was decisions/agent-instructions-history.md)
│ │
│ ├── backlog/
│ │ ├── index.md (huidige backlog.md)
│ │ └── product-historical.md (huidige product-backlog.md)
│ │
│ ├── plans/
│ │ ├── M9-active-product-backlog.md
│ │ ├── M10-qr-pairing-login.md
│ │ ├── M11-claude-questions.md
│ │ ├── ST-1109-pbi-status.md
│ │ ├── ST-1110-demo-readonly.md
│ │ ├── ST-1111-claude-job-trigger.md
│ │ ├── ST-1114-copilot-reviews.md
│ │ ├── tweede-claude-agent-planning.md (rename — geen spaties/em-dash)
│ │ └── archive/ (uit `.Plans/` aan repo-root)
│ │ ├── 2026-04-27-claude-md-workflow-update.md
│ │ ├── 2026-04-27-insert-milestone-tool.md
│ │ └── 2026-04-27-m8-realtime-solo.md
│ │
│ ├── qa/
│ │ └── api-test-plan.md
│ │
│ └── assets/
│ ├── erd.svg
│ └── icons.html
└── .Plans/ (WEG — naar docs/plans/archive/)
└── Brainstorm.md (WEG — junk, of naar docs/scratch/)
└── docs/patterns/test.md (WEG — junk)
```
**Prefix `` overal weg.** Je staat in de Scrum4Me-repo.
**Alle bestandsnamen kebab-case, lowercase.** Geen `api.md`, geen `MD3_…`.
---
## 5. CLAUDE.md herontwerp
CLAUDE.md wordt strikt **router + harde regels** — geen referentie-tabellen, geen voorbeelden, geen rationale.
Voorgestelde nieuwe inhoud (max ~150 regels):
```markdown
# CLAUDE.md — Scrum4Me
## 1. Wat is Scrum4Me
(2 zinnen — link naar README voor de pitch)
## 2. Eerst lezen, altijd
- docs/INDEX.md — manifest van alle docs
- docs/glossary.md — bij twijfel over een term
## 3. Hoe je werk vindt
Twee tracks (A: MCP, B: manueel) — verkort tot 10 regels.
Detail: docs/runbooks/sprint-flow.md
## 4. Hardstop-regels (nooit overtreden)
- demo-user heeft geen schrijfrechten (3-laagsdekking)
- @base-ui/react, niet Radix
- nooit bg-blue-500, altijd MD3-tokens
- één commit = één verantwoordelijkheid
- één branch per milestone, push pas na user-approval
- denormalized FKs uit DB-parent, niet uit client-input
(elk punt → 1 regel + link naar pattern/decision)
## 6. Stack op één regel per laag
(geen versie-uitleg, link naar docs/architecture/overview.md)
## 7. Snelreferentie patronen
| Wanneer | Lees |
|---|---|
| Server Action schrijven | docs/patterns/server-action.md |
| Drag-and-drop reorder | docs/patterns/sort-order.md |
| …(max 10 rijen)…|
## 8. Verificatie vóór hand-off
`npm run lint && npm test && npm run build`
```
Alles wat nu in CLAUDE.md §Conventies, §Branch & PR Strategy, §Commit Strategy, §MCP-integratie, §Deployment staat → verhuist naar:
- `docs/runbooks/branch-and-commit.md` (regels + voorbeelden samen)
- `docs/runbooks/deploy-vercel.md`
- `docs/runbooks/mcp-integration.md`
- `docs/decisions/0003-one-branch-per-milestone.md` (waarom)
CLAUDE.md houdt in §4 alleen de éénregelige regel + link.
**Effect:** elke turn 150 r in plaats van 340 r aan orientation-context. De agent leest aanvullende docs alleen wanneer de huidige taak ze raakt.
---
## 6. Front-matter spec
Élk markdown-bestand in `docs/` (en `CLAUDE.md`, `AGENTS.md`) krijgt bovenaan:
```yaml
---
title: <korte titel>
status: draft | active | deprecated
audience: ai-agent | maintainer | contributor | external
language: nl | en
last_updated: 2026-05-02
applies_to: [feature-of-module-of-milestone-keys] # optioneel
related:
- docs/<andere>.md
- CLAUDE.md
when_to_read: <één zin>
do_not_read_for: <één zin voorkomt mis-fetch>
---
```
**Waarom dit voor AI-lookup helpt:**
- `status: deprecated` → agent slaat het over zonder te lezen.
- `applies_to: [M10, qr-login]` → grep op milestone-key → directe hit.
- `when_to_read` / `do_not_read_for` → agent kan beslissen op de eerste 20 regels of dit doc nuttig is, zonder de hele 800-regelige spec in te lezen.
- `related` → expliciete graaf in plaats van impliciete cross-refs.
`docs/INDEX.md` wordt automatisch gegenereerd uit deze front-matter (klein script in `scripts/build-docs-index.ts` — onderdeel van het migratieplan).
---
## 7. AGENTS.md herontwerp
```markdown
# AGENTS.md
This repo's source of truth for agent instructions is **CLAUDE.md**.
Codex, Cursor, Continue, and any other coding agent: read CLAUDE.md first.
The same product, security, and verification rules apply regardless of which agent runs.
Repo-specific addendum (only if your agent does NOT speak markdown well):
- The "This is NOT the Next.js you know" note also applies to you.
- Run `npm run lint && npm test && npm run build` before handing work back.
```
Geen duplicatie van access-control of doc-sync — die regels staan exclusief in CLAUDE.md / `docs/patterns/` / `docs/decisions/`.
---
## 8. Migratie in fases
Elke fase is een eigen branch + PR. Geen big-bang. Volgorde gekozen zodat agents tijdens de migratie nog steeds werken.
### Fase 1 — Junk weg, front-matter erbij (laag risico)
- `docs/patterns/test.md` weghalen.
- `Brainstorm.md` weghalen of `docs/scratch/brainstorm-2026-05.md`.
- `.Plans/``docs/plans/archive/`.
- Front-matter toevoegen aan élk bestaand bestand (zonder verplaatsen). Status default = `active`.
- `docs/INDEX.md` genereren via script.
**Voor commit:** alle bestaande paden werken nog. Geen risico voor lopende sessies of CI.
### Fase 2 — Naamgeving normaliseren
- `` prefix overal weg via `git mv` (1 commit per groep — backlog/specs/personas/styling/dialogs).
- `api.md``api/rest-contract.md`.
- `md3-color-scheme.md``design/md3-color-scheme.md`.
- `tweede-claude-agent-planning.md``plans/tweede-claude-agent-planning.md`.
- `middleware.md``proxy.md` (volgt Next.js 16 hernoeming).
- Per `git mv`: in dezelfde commit zoek-en-vervang alle interne links + CLAUDE.md doc-index.
### Fase 3 — Folder-taxonomie
- Maak `docs/architecture/`, `docs/specs/`, `docs/design/`, `docs/api/`, `docs/runbooks/`, `docs/decisions/`, `docs/backlog/`, `docs/qa/`, `docs/assets/`.
- Verplaats per groep met `git mv`. Eén commit per groep.
- Update CLAUDE.md doc-index per stap.
### Fase 4 — Splits monolithische docs
- `architecture.md` (1.247 r) opdelen in 6 docs onder `docs/architecture/`.
- Originele file wordt 20 regels: titel + "Dit document is opgesplitst — zie:" + lijst met nieuwe paden.
- Idem voor `solo-paneel-spec.md` als dat onderdelen heeft die naar specs én patterns kunnen.
### Fase 5 — CLAUDE.md verkorten + AGENTS.md verkorten
- Knip CLAUDE.md naar het skelet uit §5.
- Verplaats verwijderde secties naar `docs/runbooks/` en `docs/decisions/`.
- AGENTS.md vervangen door de versie uit §7.
### Fase 6 — ADR-backfill
- Schrijf ADR's voor de impliciete beslissingen (58 stuks):
1. base-ui-over-radix
2. float-sort-order
3. one-branch-per-milestone
4. status-enum-mapping (db UPPER ↔ api lower)
5. iron-session-over-nextauth
6. demo-user-policy (3-laags)
7. claude-question-channel-design
8. agent-instructions-policy (was audit)
### Fase 7 — Glossary + index-script
- `docs/glossary.md` schrijven (PBI, Story, Sprint, Solo, Todo, demo-token, MCP-job, …).
- `scripts/build-docs-index.ts` — genereert `docs/INDEX.md` uit alle front-matters.
- Husky pre-commit hook: index regenereren bij wijziging van front-matter.
### Fase 8 — Cross-link-check
- Klein script dat alle `docs/...md` links volgt en rapporteert dode links én anchor-misses.
- Toevoegen aan `npm run lint` of `npm test`.
---
## 9. Wat dit oplevert (meetbaar)
| Metric | Vandaag | Doel |
|---|---:|---:|
| Regels die elke agent-turn standaard in context komen (CLAUDE.md) | 340 | ≤150 |
| Doc-bestanden in `docs/` root | 13 | 2 (INDEX.md, glossary.md) |
| Doc-bestanden zonder front-matter | 36 | 0 |
| Junk-bestanden | 3 (test.md, Brainstorm.md, .Plans/) | 0 |
| Bestandsnamen met spaties of niet-ASCII | 1 | 0 |
| Filename-prefixen die geen informatie toevoegen (``) | 8 | 0 |
| Documenten >800 regels | 4 | 0 (na splitsing) |
| Dode interne links | onbekend | 0 (na lint) |
---
## 10. Wat dit níet oplevert (eerlijk)
- Codequaliteit verbetert niet automatisch.
- Patronen die nu fout zijn worden niet gefixt — alleen vindbaar gemaakt.
- ADR's invullen kost denkwerk dat ik niet uit jouw hoofd kan halen — fase 6 vereist jouw input.
- AI-agents die geen front-matter parseren (oudere modellen, sommige codex-flavors) profiteren minder. Voor de `docs/INDEX.md` is het wel platte tekst — die helpt iedereen.
---
## 11. Open beslissingen — status
| # | Vraag | Besluit (2026-05-02) |
|---|---|---|
| 1 | Taal van docs, front-matter, INDEX.md | **English** — alle nieuwe en herschreven docs in het Engels. Code comments blijven Engels (al zo). UI-strings blijven Nederlands. |
| 2 | MD3-color + styling samenvoegen | **Eén doc**`docs/design/styling.md`. |
| 3 | `solo-paneel-spec.md` | **Samenvoegen** — opgaan in `docs/specs/functional.md` (eigen sectie). |
| 4 | `.Plans/` archief | **Bewaren** — verplaatsen naar `docs/plans/archive/`. |
| 5 | ADR-template (Nygard vs MADR) | **In discussie** — referentielink gedeeld, ik heb die niet kunnen openen (claude.ai share staat niet op de fetch-allowlist). Default voor fase 6: Nygard, klein en passend bij solo + kleine repo. Vervangbaar zodra besluit valt. |
| 6 | Index-generator | **Node-script** in `scripts/build-docs-index.ts`. |
## 12. Implicaties van besluit 1 — taalwissel
De keuze "alle docs Engels" is groter dan hij lijkt. Drie scope-niveaus:
**Niveau A — going-forward only (kleinste scope):**
- Élk nieuw doc en élke nieuw aangemaakte front-matter in het Engels.
- Dit plan, INDEX.md, glossary.md, runbooks/, decisions/ — allemaal Engels vanaf creatie.
- Bestaande Nederlandse docs blijven staan tot ze om een andere reden geraakt worden.
- **Risico:** mengvormen in docs/ — een agent vindt ene helft Engels, andere helft Nederlands. Grep op een Engels keyword mist Nederlandse hits.
**Niveau B — opportunistic (middel):**
- Niveau A + élke doc die we aanraken voor de restructuur (renames, splitsingen, front-matter toevoegen) wordt meteen vertaald.
- Aan het eind van fase 5 zijn `architecture/`, `specs/`, `design/`, `api/`, `patterns/`, `runbooks/`, `decisions/` allemaal Engels.
- Backlog, plans en QA blijven Nederlands tenzij ze ge-edit worden.
**Niveau C — full sweep:**
- Élke `.md` in de repo vertalen, ongeacht of de restructuur hem aanraakt.
- Aanzienlijk werk: ~10.700 regels prose. Schatting: 1-2 dagen agent-tijd of een batch-translate-pass met review.
**Voorstel: niveau B.** Sluit aan bij de migratiefases zonder een aparte translation-sprint te hoeven plannen. Niveau C kan later als één losse PR.
## 13. ADR-template — voorlopige keuze
Tot besluit valt op vraag 5: ik gebruik **Nygard-Light** als template voor fase 6. Eén ADR is één bestand met:
```markdown
---
title: <decision title>
adr_number: 0001
status: proposed | accepted | superseded by 00xx
date: 2026-05-02
---
# 0001. <decision title>
## Context
Why this decision matters now. What forces are in play.
## Decision
The choice we make. One paragraph, declarative.
## Consequences
What becomes easier, what becomes harder, what we accept.
```
Compact, grep-vriendelijk, en agent-leesbaar binnen ~30 regels. Als MADR de uitkomst wordt, swappen we het template voor fase 6 — alle eerdere fases zijn template-onafhankelijk.
## 14. Volgende stap
Met deze besluiten kan ik fase 1 omzetten naar een concrete uitvoeringslijst:
- exacte `rm` / `git mv` / `mkdir` commando's
- de front-matter-template in het Engels
- één `npm run` of bash-snippet die de hele fase in één commit zet
- bijbehorende update-diff voor CLAUDE.md (alleen de doc-index-tabel)
Zeg het woord en ik produceer dat als `docs/plans/docs-restructure-phase-1.md`.
---
## Verificatie van dit plan
- [x] Bestandsnamen en regelaantallen gecheckt tegen `find docs -type f` + `wc -l` op disk (2026-05-02).
- [x] Cross-refs in CLAUDE.md gegrep't en op bestaan getoetst.
- [x] Geen voorgestelde nieuwe paden conflicteren met bestaande.
- [x] Open beslissingen (§11) afgehandeld door maintainer (2026-05-02) — vraag 5 voorlopig op default.
- [ ] ADR-template definitief vastgesteld (vraag 5).
- [ ] Migratie fase 1 omgezet naar uitvoerbaar PR-plan (`docs/plans/docs-restructure-phase-1.md`).

View file

@ -0,0 +1,783 @@
---
spec_version: 1
spec_type: pbi-bulk-create
generated_at: 2026-05-02
language: en
notes: |
Input for a bulk-create executor that calls
mcp__scrum4me__create_pbi
mcp__scrum4me__create_story
mcp__scrum4me__create_task
in that order. The executor reads the YAML graph at the bottom of this
file. The prose above the graph is identical to what the executor sends
as the PBI's `description` field, so any agent that later picks up a
task gets the full context via mcp__scrum4me__get_claude_context.
product_id is intentionally left as a placeholder (REPLACE_ME) — fill it
in before running the executor. priority uses 1 = critical … 5 = trivial.
sort_order is omitted everywhere; the MCP server auto-assigns last+1
within the priority group.
---
# PBI Bulk-Create Spec — Docs-Restructure for AI-Optimized Lookup
## 1. Context (this becomes the PBI description)
This PBI executes the docs-restructure plan
([`docs/plans/docs-restructure-ai-lookup.md`](./docs-restructure-ai-lookup.md))
over eight phases, mapped here as eight stories with three to eight tasks
each. The goal is to cut the documentation surface an AI agent has to read
to find the right reference, without breaking existing workflows.
### Why this PBI exists
Today an agent loads `CLAUDE.md` (340 lines) every turn before reading a
single line of code, then has to choose from 35 docs in `docs/` whose
purpose is only inferable from the filename — there is no front-matter,
no index, no machine-readable status. After this PBI, the agent reads
`CLAUDE.md` (≤150 lines) plus `docs/INDEX.md` and knows where to go.
### Going-forward defaults — every task must respect these
- **Language:** all new and touched docs in English. Code comments stay
English (already the case). UI strings stay Dutch.
- **Front-matter:** every `.md` under `docs/` (and `CLAUDE.md`,
`AGENTS.md`) carries YAML front-matter with at minimum
`title`, `status`, `audience`, `language`, `last_updated`. Add
`related`, `when_to_read`, `do_not_read_for` where useful.
- **Naming:** lowercase kebab-case. No `` prefix. No spaces in
filenames. No UPPER. Files matching `_*.md` are personal sidecar and
excluded from the index.
- **Commits:** one commit per logical layer. Use `docs(<story-slug>):`
as the prefix when no ST-code applies. Reference the matching story
slug from this spec (e.g. `docs(naming): drop prefix`).
- **Pushing:** never push without explicit user approval. Local commits
on the feature branch only. The branch convention is
`feat/docs-restructure-<phase-slug>` (one branch per story).
- **Cross-references:** when renaming or moving a file, update every
internal link to it in the same commit. After the link-check story
(Story 8) is done, run `npm run docs:check-links` before each commit.
### Sequencing
Stories run in numeric order. Story N+1 assumes the changes from Story N
are merged. If you pick up a task and earlier stories aren't done, stop
and surface it — don't try to leapfrog. The exception is Story 6
(ADR-backfill) and Story 7 (glossary): both are content-only and can run
in parallel with Stories 35 if you want.
### Out of scope
- Translating Dutch docs that this PBI doesn't otherwise touch — that's
a separate full-sweep PR.
- Code changes outside `docs/`, `scripts/`, `package.json`, `.gitignore`.
- Pushing branches or opening PRs against `main`.
- Restyling, content rewrites beyond what each task asks for.
### Where to look first
- This file (the PBI context block above).
- [`docs/plans/docs-restructure-ai-lookup.md`](./docs-restructure-ai-lookup.md)
— the full plan, especially §3 (Goals), §4 (Target structure), §6
(Front-matter spec), §8 (Phased migration).
- [`docs/adr/README.md`](../adr/README.md) — when writing an ADR in
Story 6.
### Definition of done for this PBI
- All 8 stories' tasks accepted.
- `docs/INDEX.md` regenerates without errors and lists every doc grouped
correctly.
- `npm run docs:check-links` passes (after Story 8).
- `CLAUDE.md` ≤ 150 lines.
- Zero docs without front-matter.
- Zero junk files (`docs/patterns/test.md`, `Brainstorm.md` at root,
`.Plans/` at root) remain.
---
## 2. Resolved decisions (quick reference)
| # | Question | Decision |
|---|---|---|
| 1 | Doc language | English for all new/touched docs |
| 2 | MD3-color + styling | Merge into one `docs/design/styling.md` |
| 3 | `solo-paneel-spec.md` | Merge into `docs/specs/functional.md` |
| 4 | `.Plans/` | Archive under `docs/plans/archive/` |
| 5 | ADR template | Nygard default; MADR for auth, queue, agent integration |
| 6 | Index generator | Node script (`scripts/generate-docs-index.mjs`, already exists) |
---
## 3. What's already done (do not re-do)
These commits on `feat/docs-adr-and-index` already shipped:
- `docs(plans):` the restructure plan itself.
- `docs(adr):` ADR scaffolding — templates, README, meta-ADR (0000).
- `feat(docs):` the docs index generator + initial `docs/INDEX.md`.
- `chore:` `.gitignore` for `.obsidian/` + `_*.md`.
So Story 6 only needs the **content** (the actual ADR-0001 … 0008
files); Story 7 only needs the **glossary + husky hook**, the script
already exists.
---
## 4. Structured graph (executor reads this)
```yaml
pbi:
product_id: REPLACE_ME
title: "Docs-restructure for AI-optimized lookup"
description: |
See section 1 (Context) above. Every task in this PBI must respect:
- English for new/touched docs (UI strings stay Dutch).
- YAML front-matter on every doc (title, status, audience, language,
last_updated; related/when_to_read/do_not_read_for where useful).
- kebab-case, lowercase, no prefix, no spaces.
- One commit per logical layer (`docs(<story-slug>):` prefix).
- No pushes without user approval.
- Update every internal link in the same commit as a rename.
Read docs/plans/docs-restructure-ai-lookup.md §3, §4, §6, §8 first.
priority: 2
stories:
- slug: junk-cleanup-and-front-matter
title: "Phase 1 — Junk cleanup + front-matter on every doc"
description: |
Lowest-risk first phase. Remove stub/junk files, archive parallel
plan folder, and add YAML front-matter to every existing .md
under docs/. No paths change yet — every existing link still
works after this story.
acceptance_criteria: |
- docs/patterns/test.md is deleted.
- .Plans/ is moved to docs/plans/archive/ and the .gitignore
entry for .Plans/ is removed.
- Brainstorm.md at repo root is deleted (or moved to
docs/scratch/) — it's already gitignored.
- Every existing .md under docs/ (and CLAUDE.md, AGENTS.md) has
YAML front-matter with at least title, status, audience,
language, last_updated.
- `npm run docs:index` reports 0 docs missing front-matter and
regenerates docs/INDEX.md cleanly.
priority: 2
tasks:
- title: "Remove docs/patterns/test.md"
description: "One-word stub file (`test`) committed by accident."
implementation_plan: |
git rm docs/patterns/test.md
grep -rn "patterns/test" docs/ CLAUDE.md AGENTS.md README.md
# expect no matches
commit: docs(junk-cleanup): remove stub patterns/test.md
priority: 3
- title: "Archive .Plans/ into docs/plans/archive/"
description: "Three historical plan files at repo root parallel to docs/plans/."
implementation_plan: |
mkdir -p docs/plans/archive
git mv .Plans/2026-04-27-claude-md-workflow-update.md docs/plans/archive/
git mv .Plans/2026-04-27-insert-milestone-tool.md docs/plans/archive/
git mv .Plans/2026-04-27-m8-realtime-solo.md docs/plans/archive/
rmdir .Plans
# remove the .Plans/ entry from .gitignore (line 55 today)
commit: docs(junk-cleanup): archive .Plans/ to docs/plans/archive/
priority: 3
- title: "Delete root Brainstorm.md"
description: "Already gitignored; contains stray Prisma + DOM dump with no context."
implementation_plan: |
git rm -f Brainstorm.md
# leave the .gitignore Brainstorm.md entry — harmless if file gone
commit: docs(junk-cleanup): remove root Brainstorm.md scratch file
priority: 4
- title: "Add front-matter to all docs/*.md (root)"
description: "13 files at docs/ root currently have no front-matter."
implementation_plan: |
For each file in docs/*.md (excluding INDEX.md):
prepend a YAML block:
---
title: <copy from H1>
status: active
audience: [maintainer, contributor]
language: nl # or en, match the actual file
last_updated: 2026-05-02
---
Verify: head -1 docs/*.md | grep -c '^==>' should equal
(count of files), and head -2 docs/*.md should show "---" on
line 2 of every file.
commit: docs(front-matter): add YAML front-matter to docs/ root
priority: 2
- title: "Add front-matter to docs/patterns/*.md"
description: "11 patterns get front-matter with audience=ai-agent."
implementation_plan: |
For each docs/patterns/*.md:
---
title: <H1 minus 'Patroon: ' prefix>
status: active
audience: [ai-agent, contributor]
language: nl
last_updated: 2026-05-02
when_to_read: <one sentence what triggers reading this>
---
commit: docs(front-matter): add YAML front-matter to patterns/
priority: 2
- title: "Add front-matter to docs/plans/*.md and CLAUDE.md/AGENTS.md"
description: "Plans, the orientation file, and AGENTS.md."
implementation_plan: |
Plans (active + archive): include `applies_to: [<milestone>]`
and `status: <proposal|active|done|deprecated>` based on git
history of the file.
CLAUDE.md and AGENTS.md: status: active, audience: ai-agent.
commit: docs(front-matter): add YAML front-matter to plans + agent files
priority: 2
- title: "Verify INDEX.md picks up new metadata"
description: "Sanity check: status and dates now visible in INDEX.md."
implementation_plan: |
npm run docs:index
grep -c "| — |" docs/INDEX.md
# expect significantly fewer than before (most rows now
# show real status/date)
commit: docs(index): regenerate INDEX.md after front-matter pass
priority: 3
- slug: naming-normalization
title: "Phase 2 — Normalize file naming"
description: |
Drop the `` prefix. Lowercase everything. Remove the
space + em-dash filename. Rename Next.js 16 middleware to proxy.
After this story all filenames are kebab-case and grep-friendly.
acceptance_criteria: |
- No file under docs/ starts with ``.
- No file under docs/ uses UPPER, snake_case, or contains spaces.
- docs/patterns/proxy.md is renamed to docs/patterns/proxy.md.
- Every internal link to renamed files is updated in the same
commit as the rename.
- `npm run docs:index` runs cleanly with no missing files.
priority: 2
tasks:
- title: "Rename docs/* (drop prefix)"
description: "8 spec/backlog/styling files at docs/ root."
implementation_plan: |
git mv docs/architecture.md docs/architecture.md
git mv docs/backlog/index.md docs/backlog/index.md
git mv docs/specs/functional.md docs/specs/functional.md
git mv docs/specs/dialogs/pbi.md docs/specs/dialogs/pbi.md
git mv docs/specs/personas.md docs/specs/personas.md
git mv docs/backlog/product-historical.md docs/backlog/product-historical.md
git mv docs/specs/dialogs/story.md docs/specs/dialogs/story.md
git mv docs/design/styling.md docs/design/styling.md
git mv docs/specs/dialogs/task.md docs/specs/dialogs/task.md
git mv docs/qa/api-test-plan.md docs/qa/api-test-plan.md
# update every internal link
grep -rln "" docs/ CLAUDE.md AGENTS.md README.md \
| xargs sed -i '' 's|||g'
npm run docs:index
commit: docs(naming): drop prefix from doc filenames
priority: 2
- title: "Lowercase api.md and MD3 file"
description: "Two files use non-kebab capitalization."
implementation_plan: |
git mv docs/api/rest-contract.md docs/api/rest-contract.md
git mv docs/design/styling.md docs/design/styling.md
grep -rln "API\.md\|MD3_Color_Scheme" docs/ CLAUDE.md AGENTS.md README.md \
| xargs sed -i '' 's|API\.md|api.md|g; s|md3-color-scheme|md3-color-scheme|g'
npm run docs:index
commit: docs(naming): lowercase api.md and MD3 filenames
priority: 2
- title: "Rename plan file with spaces + em-dash"
description: "`tweede-claude-agent-planning.md` → kebab-case."
implementation_plan: |
git mv "docs/plans/tweede-claude-agent-planning.md" \
docs/plans/tweede-claude-agent-planning.md
grep -rln "Tweede Claude Agent" docs/ CLAUDE.md AGENTS.md README.md \
| xargs sed -i '' 's|Tweede Claude Agent — Planning Agent\.md|tweede-claude-agent-planning.md|g'
npm run docs:index
commit: docs(naming): rename plan file to kebab-case ASCII
priority: 3
- title: "Rename middleware.md → proxy.md"
description: "Next.js 16 renamed middleware.ts to proxy.ts."
implementation_plan: |
git mv docs/patterns/proxy.md docs/patterns/proxy.md
grep -rln "patterns/proxy" docs/ CLAUDE.md AGENTS.md README.md \
| xargs sed -i '' 's|patterns/proxy|patterns/proxy|g'
npm run docs:index
commit: docs(naming): rename middleware.md to proxy.md (next 16)
priority: 3
- title: "Update CLAUDE.md doc-index table"
description: "After renames, CLAUDE.md §Specificatiedocumenten and §Implementatiepatronen tables need a sweep."
implementation_plan: |
Manually review CLAUDE.md lines 1332 (specs table) and
99118 (patterns table) — the sed sweeps already updated the
paths, but column wording may need a quick polish.
commit: docs(naming): polish CLAUDE.md doc-index after renames
priority: 3
- slug: folder-taxonomy
title: "Phase 3 — Move docs into topical folders"
description: |
Create architecture/, specs/, design/, api/, runbooks/,
decisions/, backlog/, qa/, assets/ folders. Move existing files
into them. Keep one git mv per group so commits stay readable.
acceptance_criteria: |
- docs/ root contains only INDEX.md and (later) glossary.md.
- All existing docs moved into the right folder per
docs/plans/docs-restructure-ai-lookup.md §4.
- Internal links updated in the same commit as each move.
- `npm run docs:index` shows docs grouped correctly.
priority: 2
tasks:
- title: "Create folder skeleton"
description: "Empty folders with .gitkeep so structure is visible."
implementation_plan: |
for d in architecture specs specs/dialogs design api runbooks decisions backlog qa assets; do
mkdir -p docs/$d
touch docs/$d/.gitkeep
done
commit: docs(taxonomy): scaffold topical folders under docs/
priority: 3
- title: "Move spec files into docs/specs/"
description: "functional, personas, dialogs/."
implementation_plan: |
git mv docs/specs/functional.md docs/specs/functional.md
git mv docs/specs/personas.md docs/specs/personas.md
git mv docs/specs/dialogs/pbi.md docs/specs/dialogs/pbi.md
git mv docs/specs/dialogs/story.md docs/specs/dialogs/story.md
git mv docs/specs/dialogs/task.md docs/specs/dialogs/task.md
grep -rln "docs/specs/personas\|docs/specs/functional\|docs/.*-dialog" \
docs/ CLAUDE.md AGENTS.md README.md \
| xargs sed -i '' \
-e 's|docs/specs/functional|docs/specs/functional|g' \
-e 's|docs/specs/personas|docs/specs/personas|g' \
-e 's|docs/specs/dialogs/pbi|docs/specs/dialogs/pbi|g' \
-e 's|docs/specs/dialogs/story|docs/specs/dialogs/story|g' \
-e 's|docs/specs/dialogs/task|docs/specs/dialogs/task|g'
commit: docs(taxonomy): move spec files into docs/specs/
priority: 2
- title: "Move design + api + qa + backlog + assets"
description: "Bulk move per topic."
implementation_plan: |
git mv docs/design/styling.md docs/design/styling.md
git mv docs/design/styling.md docs/design/styling.md
git mv docs/api/rest-contract.md docs/api/rest-contract.md
git mv docs/qa/api-test-plan.md docs/qa/api-test-plan.md
git mv docs/backlog/index.md docs/backlog/index.md
git mv docs/backlog/product-historical.md docs/backlog/product-historical.md
git mv docs/assets/erd.svg docs/assets/erd.svg
git mv docs/assets/icons.html docs/assets/icons.html
# update links — sed sweep
commit: docs(taxonomy): move design/api/qa/backlog/assets into folders
priority: 2
- title: "Move decisions/agent-instructions-history → decisions/ as ADR seed"
description: "Sets up Story 6 ADR-0008 (will replace this file with a real ADR)."
implementation_plan: |
git mv docs/decisions/agent-instructions-history.md docs/decisions/agent-instructions-history.md
grep -rln "decisions/agent-instructions-history" docs/ CLAUDE.md AGENTS.md README.md \
| xargs sed -i '' 's|decisions/agent-instructions-history|decisions/agent-instructions-history|g'
commit: docs(taxonomy): move decisions/agent-instructions-history into decisions/
priority: 3
- slug: split-monoliths
title: "Phase 4 — Split oversized docs"
description: |
Four docs exceed 800 lines and conflate multiple topics. Split
them so an agent can grep for the right section directly.
acceptance_criteria: |
- No doc in docs/ exceeds 800 lines.
- The original location of each split doc still exists as a
breadcrumb file (≤25 lines) listing the new paths so old links
fail gracefully.
- solo-panel content lives inside docs/specs/functional.md as
its own H2 section.
- md3-color-scheme.md content is merged into docs/design/styling.md.
priority: 2
tasks:
- title: "Split docs/architecture.md into 6 files under docs/architecture/"
description: "Current size: 1247 lines."
implementation_plan: |
Target split (per plan §4):
docs/architecture/overview.md (today §1§3)
docs/architecture/data-model.md (Datamodel + Prisma Schema)
docs/architecture/auth-and-sessions.md (Authenticatieflow)
docs/architecture/qr-pairing.md (QR-pairing flow)
docs/architecture/claude-question-channel.md (Q&A kanaal)
docs/architecture/project-structure.md (Projectstructuur)
Replace docs/specs/architecture.md (after Phase 3 it's at
docs/architecture.md still — adjust path) with a 20-line
breadcrumb pointing to the six new files.
Each new file gets fresh front-matter.
commit: docs(split): break architecture.md into 6 topical files
priority: 2
- title: "Merge solo-paneel-spec.md into specs/functional.md"
description: "Per resolved decision 3."
implementation_plan: |
Append solo-paneel-spec.md content as a new H2 section
"Solo Panel" inside docs/specs/functional.md.
git rm docs/specs/functional.md#solo-panel
grep -rln "solo-paneel-spec" docs/ CLAUDE.md AGENTS.md README.md \
| xargs sed -i '' 's|docs/solo-paneel-spec\.md|docs/specs/functional.md#solo-panel|g'
commit: docs(split): merge solo-paneel-spec into specs/functional.md
priority: 3
- title: "Merge md3-color-scheme.md into design/styling.md"
description: "Per resolved decision 2."
implementation_plan: |
Append md3-color-scheme.md content as H2 sections inside
docs/design/styling.md. Drop redundant intro paragraphs.
git rm docs/design/styling.md
grep -rln "md3-color-scheme" docs/ CLAUDE.md AGENTS.md README.md \
| xargs sed -i '' 's|docs/design/md3-color-scheme\.md|docs/design/styling.md|g'
commit: docs(split): merge md3-color-scheme into design/styling
priority: 3
- slug: trim-orientation
title: "Phase 5 — Trim CLAUDE.md and AGENTS.md"
description: |
Move conventions, branch/commit rules, MCP details, and deploy
notes out of CLAUDE.md into runbooks/. Replace AGENTS.md with a
10-line stub. Goal: CLAUDE.md ≤ 150 lines.
acceptance_criteria: |
- CLAUDE.md is at most 150 lines and contains only: scope,
orientation pointers, hardstop rules, stack one-liner, pattern
quickref, verification command. (See plan §5 for the
skeleton.)
- AGENTS.md is the 10-line stub from plan §7.
- Removed sections live as their own files under docs/runbooks/.
priority: 2
tasks:
- title: "Extract Branch & Commit strategy → docs/runbooks/branch-and-commit.md"
description: "CLAUDE.md §Branch & PR Strategy + §Commit Strategy + §Plan Mode."
implementation_plan: |
Move CLAUDE.md lines covering Branch & PR Strategy, Plan
Mode, and Commit Strategy verbatim into
docs/runbooks/branch-and-commit.md (with front-matter).
In CLAUDE.md, leave a one-line link.
commit: docs(trim): extract branch/commit rules into runbook
priority: 2
- title: "Extract MCP integration → docs/runbooks/mcp-integration.md"
description: "CLAUDE.md §MCP-integratie."
implementation_plan: |
Move §MCP-integratie verbatim. Replace in CLAUDE.md with a
link + the bare list of tool names (no schemas).
commit: docs(trim): extract MCP integration into runbook
priority: 2
- title: "Extract Deployment → docs/runbooks/deploy-vercel.md"
description: "CLAUDE.md §Deployment."
implementation_plan: |
Move §Deployment verbatim. Replace in CLAUDE.md with a link.
commit: docs(trim): extract Vercel deployment into runbook
priority: 2
- title: "Rewrite CLAUDE.md skeleton ≤ 150 lines"
description: "Per plan §5 layout."
implementation_plan: |
Following plan §5, rewrite CLAUDE.md as:
§1 What is Scrum4Me (2 sentences + README link)
§2 Read first (INDEX, glossary)
§3 How to find work (compact 2-track summary)
§4 Hardstop rules (one line each, link to detail)
§5 Stack one-liner per layer
§6 Pattern quickref table (≤10 rows)
§7 Verification command
Verify: wc -l CLAUDE.md → ≤ 150
commit: docs(trim): rewrite CLAUDE.md as ≤150-line skeleton
priority: 2
- title: "Replace AGENTS.md with stub"
description: "10-line redirect to CLAUDE.md per plan §7."
implementation_plan: |
Overwrite AGENTS.md with the stub from plan §7 (English).
Verify no other doc references AGENTS.md for content
(only as a name).
commit: docs(trim): replace AGENTS.md with redirect stub
priority: 3
- slug: adr-backfill
title: "Phase 6 — Backfill ADRs for implicit decisions"
description: |
Write the actual ADR-0001 through ADR-0008 files. Scaffolding
(templates, README, meta-ADR 0000) already exists from
feat/docs-adr-and-index. Use Nygard for one-way-door decisions,
MADR where rejected alternatives matter (auth, agent integration).
acceptance_criteria: |
- 8 new ADR files exist under docs/adr/ with correct numbering.
- Each is status `accepted` and dated.
- docs/adr/README.md table-of-contents lists all 8.
- `npm run docs:index` shows them in the ADRs section.
priority: 3
tasks:
- title: "ADR-0001: base-ui-over-radix (Nygard)"
description: "Why @base-ui/react instead of Radix."
implementation_plan: |
Copy docs/adr/templates/nygard.md → docs/adr/0001-base-ui-over-radix.md
Context: shadcn/ui visually identical to Radix-based components,
but @base-ui/react uses render-prop composition and TS-correct
prop API. Radix asChild caused TS errors in our setup.
Decision: We adopt @base-ui/react and never import from Radix.
Consequences: positive — TS-clean composition; negative — agent
must remember the render-prop pattern (not asChild).
Update docs/adr/README.md TOC.
commit: docs(adr): add 0001-base-ui-over-radix
priority: 3
- title: "ADR-0002: float-sort-order (Nygard)"
description: "Why float instead of integer for drag-and-drop ordering."
implementation_plan: |
See docs/patterns/sort-order.md for the existing pattern.
Context: drag-and-drop reordering with N items — integer
positions force a renumber on every reorder.
Decision: Use float sort_order; insert between two items as
the midpoint.
Consequences: positive — O(1) inserts; negative — float
precision drift requires periodic compaction (document
when).
commit: docs(adr): add 0002-float-sort-order
priority: 3
- title: "ADR-0003: one-branch-per-milestone (MADR)"
description: "Why milestones, not stories, get branches — and the cost driver."
implementation_plan: |
Use MADR (alternatives matter — Vercel cost driver explicit).
Considered options: branch-per-story, branch-per-milestone,
trunk-based.
Decision: branch-per-milestone, push only after user-test.
Decision drivers: Vercel Hobby preview-build cost; small team
size; AI-driven dev flow.
More information: revisit when Vercel Pro is on.
commit: docs(adr): add 0003-one-branch-per-milestone
priority: 3
- title: "ADR-0004: status-enum-mapping (Nygard)"
description: "Why DB UPPER_SNAKE and API lowercase, plus the lib/task-status.ts mapper."
implementation_plan: |
Context: Prisma uses UPPER_SNAKE for enums; OpenAPI/REST
clients expect lowercase.
Decision: DB stays UPPER; API exposes lower; conversion
exclusively via lib/task-status.ts mappers.
Consequences: negative — anyone tempted to .toLowerCase()
elsewhere breaks the contract; mitigated by lint rule.
commit: docs(adr): add 0004-status-enum-mapping
priority: 3
- title: "ADR-0005: iron-session-over-nextauth (MADR)"
description: "Why iron-session — alternatives weighed."
implementation_plan: |
Use MADR.
Considered options: NextAuth/Auth.js v5, Clerk, Supabase Auth,
iron-session.
Decision: iron-session.
Drivers: full control over cookie shape; demo-user policy
requires synchronous check; no third-party redirect chain.
commit: docs(adr): add 0005-iron-session-over-nextauth
priority: 3
- title: "ADR-0006: demo-user-three-layer-policy (Nygard)"
description: "Why the same check exists in proxy + actions + UI."
implementation_plan: |
Context: demo-user must never write. Defense-in-depth across
network, server, and UI layers.
Decision: enforce in proxy.ts (network), every Server
Action / Route Handler (server), and disabled buttons +
DemoTooltip (UI).
See: docs/architecture/auth-and-sessions.md (after Phase 4
split) and ST-1110 plan.
commit: docs(adr): add 0006-demo-user-three-layer-policy
priority: 3
- title: "ADR-0007: claude-question-channel-design (MADR)"
description: "How the agent ↔ user async channel works and why."
implementation_plan: |
Use MADR.
Considered options: synchronous polling only, push via SSE,
persistent claude_questions table + LISTEN/NOTIFY.
Decision: persistent table + LISTEN/NOTIFY trigger.
See: docs/patterns/claude-question-channel.md and M11 plan.
commit: docs(adr): add 0007-claude-question-channel-design
priority: 3
- title: "ADR-0008: agent-instructions-policy (Nygard, supersedes audit doc)"
description: "Replaces docs/decisions/agent-instructions-history.md as a real ADR."
implementation_plan: |
Migrate the conclusions of docs/decisions/agent-instructions-history.md
into an ADR.
After accepted: mark history doc as superseded with a
top-of-file note linking to ADR-0008.
commit: docs(adr): add 0008-agent-instructions-policy
priority: 3
- slug: glossary-and-husky
title: "Phase 7 — Glossary + pre-commit hook for INDEX.md"
description: |
Write a glossary and wire the index generator into husky so
INDEX.md never goes stale. The script itself already exists.
acceptance_criteria: |
- docs/glossary.md exists with entries for PBI, Story, Sprint,
Solo, Todo, demo-token, MCP-job, agent worker, claude-question.
- .husky/pre-commit runs `npm run docs:index` only when staged
changes touch docs/**/*.md, then `git add docs/INDEX.md`.
- README points to docs/INDEX.md and docs/glossary.md.
priority: 3
tasks:
- title: "Write docs/glossary.md"
description: "Single-page domain-term reference."
implementation_plan: |
Front-matter: status active, audience [ai-agent, contributor],
language en.
Terms (alphabetical):
demo-token, demo-user, MCP-job, PBI, Solo Panel, Sprint,
Story, Task, Todo, agent worker, claude-question.
Each term: 1-2 sentences + link to canonical doc.
commit: docs(glossary): add docs/glossary.md
priority: 3
- title: "Wire docs:index into husky pre-commit"
description: "Avoid stale INDEX.md."
implementation_plan: |
Edit .husky/pre-commit:
if git diff --cached --name-only | grep -q '^docs/.*\.md$'; then
npm run docs:index
git add docs/INDEX.md
fi
Verify with: stage a doc edit and run git commit --dry-run
commit: chore(docs): regenerate INDEX.md in pre-commit hook
priority: 3
- title: "Add INDEX + glossary pointer to README"
description: "Mensbezoekers vinden dan ook het orientation-laagje."
implementation_plan: |
In README.md add a short section under "Architectuur (kort)":
## Documentation
- docs/INDEX.md — generated index of all docs
- docs/glossary.md — domain terms
- CLAUDE.md / AGENTS.md — agent instructions
commit: docs(readme): link INDEX + glossary + agent instructions
priority: 4
- slug: link-check
title: "Phase 8 — Doc-link health check"
description: |
Add a small node script that walks docs/, finds every markdown
link, and verifies the path (and anchor) exists. Wire into
npm test or CI so a broken link blocks merge.
acceptance_criteria: |
- scripts/check-doc-links.mjs exists, pure Node, no deps.
- It validates relative .md links and #anchors.
- It prints a non-zero exit code on failures.
- `npm run docs:check-links` is wired in package.json.
- At least one CI step (vitest config or a separate npm script)
runs it.
- All current docs pass the check after the renames from
Stories 24.
priority: 3
tasks:
- title: "Write scripts/check-doc-links.mjs"
description: "Walk docs/, parse markdown links, validate."
implementation_plan: |
Pure Node 20.
For each .md file:
extract markdown links via regex /\[([^\]]+)\]\(([^)]+)\)/g
skip http(s):// and mailto: links
for each relative link:
resolve against the file's directory
check the path exists
if the link includes #anchor:
read the target file, check for a heading whose
GitHub-style slug matches the anchor
Print a table of failures; exit 1 if any.
commit: feat(docs): add doc-link checker script
priority: 3
- title: "Wire docs:check-links in package.json"
description: "Plus a `docs` super-script that runs index + check together."
implementation_plan: |
Add to package.json scripts:
"docs:check-links": "node scripts/check-doc-links.mjs",
"docs": "npm run docs:index && npm run docs:check-links"
commit: chore(docs): wire docs:check-links and docs npm scripts
priority: 4
- title: "Add docs check to CI"
description: "Block merge on broken links."
implementation_plan: |
Edit .github/workflows/<existing>.yml:
after the lint+test step, add:
- run: npm run docs:check-links
commit: ci(docs): block merge on broken doc links
priority: 3
- title: "Run + fix any reported broken links"
description: "Final sweep across the whole tree."
implementation_plan: |
npm run docs:check-links
For each reported broken link: fix in a single commit per
file. If the target was renamed in Stories 24 but a link
slipped through, that's a missed sed sweep — fix and update
this story's notes.
commit: docs(links): fix broken cross-references after restructure
priority: 3
```
---
## 5. Executor notes
The executor reads the YAML graph above and calls, in order:
1. `mcp__scrum4me__create_pbi` with the `pbi.title`, `pbi.description`,
`pbi.priority`, `pbi.product_id` (substituted from `REPLACE_ME`).
Capture the returned `pbi_id`.
2. For each story under `pbi.stories`:
`mcp__scrum4me__create_story` with `pbi_id`, `story.title`,
`story.description`, `story.acceptance_criteria`, `story.priority`.
Capture the returned `story_id`.
3. For each task under `story.tasks`:
`mcp__scrum4me__create_task` with `story_id`, `task.title`,
`task.description`, `task.implementation_plan`, `task.priority`.
`sort_order` is intentionally omitted everywhere — the MCP server
auto-assigns `last + 1` within the priority group, which gives a stable
order matching this file's reading order.
If any call fails, halt and surface the response — don't retry blindly,
because a partial create leaves the graph half-built.
After a successful run: store the returned PBI/story/task IDs somewhere
the executor can hand back, so a follow-up can verify everything
exists and the totals match (1 PBI, 8 stories, 39 tasks).
---
## 6. Verification
- [x] Spec written.
- [x] YAML graph parses cleanly with PyYAML; story/task counts verified
(1 PBI, 8 stories, 39 tasks; phase distribution 7/5/4/3/5/8/3/4).
- [ ] Executor runs end-to-end against the docker sandbox.
- [ ] DB shows 1 PBI titled "Docs-restructure for AI-optimized lookup".
- [ ] DB shows 8 stories under it.
- [ ] DB shows 39 tasks across the 8 stories.
- [ ] `mcp__scrum4me__get_claude_context` for the PBI returns the
context block (section 1) verbatim as the description.

View file

@ -1,3 +1,12 @@
---
title: "Tweede Claude Agent — Planning Agent"
status: proposal
audience: [maintainer, contributor]
language: nl
last_updated: 2026-05-03
applies_to: []
---
# Plan: Tweede Claude Agent — Planning Agent (PBI/Story → children) # Plan: Tweede Claude Agent — Planning Agent (PBI/Story → children)
> **Eerder goedgekeurd plan in deze file:** *Scrum4Me v1.0 Release* (mobile shell + sprint-snapshots + release-discipline). Beschikbaar in chat-history; te verhuizen naar `docs/plans/v1-release.md` op een later moment. Dit nieuwe plan vervangt de plan-file inhoudelijk niet — het v1.0-werk blijft van kracht parallel hieraan. > **Eerder goedgekeurd plan in deze file:** *Scrum4Me v1.0 Release* (mobile shell + sprint-snapshots + release-discipline). Beschikbaar in chat-history; te verhuizen naar `docs/plans/v1-release.md` op een later moment. Dit nieuwe plan vervangt de plan-file inhoudelijk niet — het v1.0-werk blijft van kracht parallel hieraan.
@ -150,7 +159,7 @@ Plaatsing:
**Live updates:** bestaande `useClaudeJobsStore` (Zustand, populated uit SSE) — alleen `kind` toevoegen aan filter-helpers. **Live updates:** bestaande `useClaudeJobsStore` (Zustand, populated uit SSE) — alleen `kind` toevoegen aan filter-helpers.
### Stap 6 — MCP-tools (`scrum4me-mcp` repo, aparte PR) ### Stap 6 — MCP-tools (`mcp` repo, aparte PR)
**Wijziging 1 — bestaande tool uitbreiden:** **Wijziging 1 — bestaande tool uitbreiden:**
@ -202,10 +211,10 @@ Korte prompt-flow:
1. `wait_for_job({ accept_kinds: ['PLANNING'], wait_seconds: 600 })` — claim 1. `wait_for_job({ accept_kinds: ['PLANNING'], wait_seconds: 600 })` — claim
2. Lees `planning_target` uit response (PBI of STORY) + `existing_*` 2. Lees `planning_target` uit response (PBI of STORY) + `existing_*`
3. **Lees lokale docs uit Scrum4Me-checkout:** 3. **Lees lokale docs uit Scrum4Me-checkout:**
- `docs/functional.md` (functioneel kader) - `docs/specs/functional.md` (functioneel kader)
- `docs/architecture.md` (technisch kader) - `docs/architecture.md` (technisch kader)
- `docs/patterns/*.md` (relevante patterns op basis van target-titel/-beschrijving) - `docs/patterns/*.md` (relevante patterns op basis van target-titel/-beschrijving)
- `docs/styling.md` als target UI-werk betreft - `docs/design/styling.md` als target UI-werk betreft
4. Bedenk children: 4. Bedenk children:
- Voor `STORY`-target: 3-7 taken met titel, korte beschrijving, `implementation_plan` (verwijst naar relevante patterns + bestanden), priority - Voor `STORY`-target: 3-7 taken met titel, korte beschrijving, `implementation_plan` (verwijst naar relevante patterns + bestanden), priority
- Voor `PBI`-target: 2-5 stories met titel, beschrijving in user-story-format, acceptance_criteria, priority - Voor `PBI`-target: 2-5 stories met titel, beschrijving in user-story-format, acceptance_criteria, priority
@ -227,7 +236,7 @@ Bij failure: `update_job_status({ status: 'FAILED', error })` + toast voor user.
| Status-pill rendering per status | `__tests__/components/shared/planning-job-pill.test.tsx` | | Status-pill rendering per status | `__tests__/components/shared/planning-job-pill.test.tsx` |
| Knop disabled-state in StoryDialog/PbiDialog bij actieve job | `__tests__/components/backlog/dialog-planning-button.test.tsx` | | Knop disabled-state in StoryDialog/PbiDialog bij actieve job | `__tests__/components/backlog/dialog-planning-button.test.tsx` |
MCP-tools testen in `scrum4me-mcp` repo (aparte PR). MCP-tools testen in `mcp` repo (aparte PR).
--- ---
@ -252,16 +261,16 @@ MCP-tools testen in `scrum4me-mcp` repo (aparte PR).
| `components/shared/planning-job-pill.tsx` | NEW | Generic pill-component | | `components/shared/planning-job-pill.tsx` | NEW | Generic pill-component |
| `docs/patterns/claude-agent-roles.md` | NEW | Pattern-doc: één table, kind-enum, accept_kinds-arg, lokale agent-prompts | | `docs/patterns/claude-agent-roles.md` | NEW | Pattern-doc: één table, kind-enum, accept_kinds-arg, lokale agent-prompts |
| `docs/architecture.md` | MODIFY | Sectie "Claude Agents" uitbreiden — twee rollen, schema, queue, prompts | | `docs/architecture.md` | MODIFY | Sectie "Claude Agents" uitbreiden — twee rollen, schema, queue, prompts |
| `docs/pbi-dialog.md` | MODIFY | Sectie "Speciale gedragingen → Planning-trigger" toevoegen | | `docs/specs/dialogs/pbi.md` | MODIFY | Sectie "Speciale gedragingen → Planning-trigger" toevoegen |
| `docs/story-dialog.md` | MODIFY | Idem | | `docs/specs/dialogs/story.md` | MODIFY | Idem |
| `docs/task-dialog.md` | MODIFY | Vermelden dat tasks ook door planning-agent kunnen ontstaan | | `docs/specs/dialogs/task.md` | MODIFY | Vermelden dat tasks ook door planning-agent kunnen ontstaan |
| `__tests__/actions/claude-jobs-planning.test.ts` | NEW | | | `__tests__/actions/claude-jobs-planning.test.ts` | NEW | |
| `__tests__/lib/claude-job-status.test.ts` | MODIFY | `kind`-mapping testen | | `__tests__/lib/claude-job-status.test.ts` | MODIFY | `kind`-mapping testen |
| `__tests__/api/realtime-solo-planning.test.ts` | NEW | | | `__tests__/api/realtime-solo-planning.test.ts` | NEW | |
| `__tests__/components/shared/planning-job-pill.test.tsx` | NEW | | | `__tests__/components/shared/planning-job-pill.test.tsx` | NEW | |
| `__tests__/components/backlog/dialog-planning-button.test.tsx` | NEW | | | `__tests__/components/backlog/dialog-planning-button.test.tsx` | NEW | |
### scrum4me-mcp repo (aparte PR, na Scrum4Me-merge) ### mcp repo (aparte PR, na Scrum4Me-merge)
| File | Action | | File | Action |
|---|---| |---|---|
@ -281,12 +290,12 @@ MCP-tools testen in `scrum4me-mcp` repo (aparte PR).
4. **Status-pill component + tests** (Stap 5a) — losstaande primitive 4. **Status-pill component + tests** (Stap 5a) — losstaande primitive
5. **Trigger-knoppen in StoryDialog + PbiDialog** (Stap 5b) — UI-trigger werkt, agent nog niet 5. **Trigger-knoppen in StoryDialog + PbiDialog** (Stap 5b) — UI-trigger werkt, agent nog niet
6. **Pause** — verifieer end-to-end met handmatig insert in `claude_jobs` (kind=PLANNING) of via mock-MCP-call 6. **Pause** — verifieer end-to-end met handmatig insert in `claude_jobs` (kind=PLANNING) of via mock-MCP-call
7. **MCP-PR in scrum4me-mcp repo** (Stap 6) — `wait_for_job` uitbreiden, types updaten 7. **MCP-PR in mcp repo** (Stap 6) — `wait_for_job` uitbreiden, types updaten
8. **Lokaal `/generate-plan`-command schrijven + testen** (Stap 7) — agent claimt, leest, schrijft 8. **Lokaal `/generate-plan`-command schrijven + testen** (Stap 7) — agent claimt, leest, schrijft
9. **End-to-end test** (Stap 8) — story → klik knop → agent rendert taken → SSE → live in TaskPanel 9. **End-to-end test** (Stap 8) — story → klik knop → agent rendert taken → SSE → live in TaskPanel
10. **Docs-PR** — pattern-doc `claude-agent-roles.md`, architecture-update, dialog-profielen aanvullen 10. **Docs-PR** — pattern-doc `claude-agent-roles.md`, architecture-update, dialog-profielen aanvullen
Branch-naming: `feat/M15-planning-agent` (Scrum4Me) + `feat/planning-agent` (scrum4me-mcp). Branch-naming: `feat/M15-planning-agent` (Scrum4Me) + `feat/planning-agent` (mcp).
Conform CLAUDE.md "branch-per-milestone": commits accumuleren lokaal, pushen pas na gebruikerstest. Conform CLAUDE.md "branch-per-milestone": commits accumuleren lokaal, pushen pas na gebruikerstest.

View file

@ -1,3 +1,11 @@
---
title: "DevPlanner — Product Backlog"
status: active
audience: [maintainer]
language: nl
last_updated: 2026-05-03
---
# DevPlanner — Product Backlog # DevPlanner — Product Backlog
**Versie:** 0.1 — april 2026 **Versie:** 0.1 — april 2026

462
docs/qa/api-test-plan.md Normal file
View file

@ -0,0 +1,462 @@
---
title: "Scrum4Me — API Test Plan"
status: active
audience: [maintainer, contributor]
language: nl
last_updated: 2026-05-03
---
# Scrum4Me — API Test Plan
**Versie:** 1.0
**Datum:** 25 april 2026
**Auteur:** Jan Peter Visser
**Status:** Draft
---
## 1. Introduction
This document describes the test plan and test planning for the Scrum4Me API endpoints — the integration surface that Claude Code and external agents use to interact with the application. It covers strategy, scope, test cases, tooling, exit criteria, and a phased execution schedule.
The API consists of 7 endpoints that form the Definition of Done checkpoint: all 7 must pass curl-level verification before the project ships.
---
## 2. Objectives
| # | Objective |
|---|---|
| O-1 | Verify that all 7 API endpoints return correct responses for valid input |
| O-2 | Verify that unauthenticated requests are rejected with 401 |
| O-3 | Verify that demo users cannot perform write operations (403) |
| O-4 | Verify that cross-user access is impossible at every endpoint |
| O-5 | Verify that all Zod validation schemas reject malformed input with 400 |
| O-6 | Verify edge cases: resource-not-found (404), empty result sets, boundary values |
| O-7 | Produce executable curl scripts that satisfy the DoD requirement |
---
## 3. Scope
### 3.1 In scope
| Endpoint | Method | Auth type | Write | Demo check |
|---|---|---|---|---|
| `/api/products` | GET | Bearer token | No | No |
| `/api/products/:id/next-story` | GET | Bearer token | No | No |
| `/api/sprints/:id/tasks` | GET | Bearer token | No | No |
| `/api/stories/:id/tasks/reorder` | PATCH | Bearer token | Yes | Yes |
| `/api/stories/:id/log` | POST | Bearer token | Yes | Yes |
| `/api/tasks/:id` | PATCH | Bearer token | Yes | Yes |
| `/api/todos` | POST | Bearer token | Yes | Yes |
### 3.2 Out of scope
- `/api/profile/avatar` (GET/POST) — session-cookie auth, separate concern
- Server Actions (`actions/*.ts`) — UI-layer, covered by separate acceptance testing
- Frontend components and pages
- Database migrations and schema changes
- Performance and load testing
---
## 4. Test Strategy
### 4.1 Layers
The strategy uses two complementary test layers. Together they satisfy both the automated regression requirement and the DoD curl requirement.
```
┌─────────────────────────────────────────────────────────────────────┐
│ Layer 1 — Vitest Unit Tests (automated, mocked) │
│ Fast, deterministic, no external DB required │
│ Covers: auth, demo block, cross-user isolation, input validation │
├─────────────────────────────────────────────────────────────────────┤
│ Layer 2 — Curl Scripts (manual, real DB) │
│ Executable against localhost:3000 with seeded test data │
│ Covers: happy paths, response shape, DoD compliance │
└─────────────────────────────────────────────────────────────────────┘
```
No Playwright, no Jest — Vitest is already configured and the existing `__tests__/api/security.test.ts` establishes the mock pattern to follow.
### 4.2 Vitest approach
All unit tests mock two dependencies only:
```typescript
vi.mock('@/lib/prisma', () => ({
prisma: { /* model methods as vi.fn() */ }
}))
vi.mock('@/lib/api-auth', () => ({
authenticateApiRequest: vi.fn()
}))
```
This approach:
- Tests route handler logic in isolation
- Keeps tests fast (no network/DB)
- Follows the pattern already validated in `security.test.ts`
- Does not fall into the mock/prod divergence trap because the mocked boundary (`authenticateApiRequest` + `prisma`) is stable and narrow
### 4.3 Curl approach
A single shell script `scripts/test-api.sh` with:
- A `TOKEN` variable set at the top (obtained from a seeded user via the UI)
- One function per endpoint, each printing pass/fail based on HTTP status
- Run order that follows the Lars flow (read → write → verify)
### 4.4 Test data
The existing seed (`prisma/seed.ts`) creates:
- `lars` — full-permission user, used as the primary test actor
- `demo` — read-only user, used for demo-block tests
A second regular user (`tester`) must be created manually (or added to the seed) to test cross-user isolation scenarios.
---
## 5. Test Cases
The following tables list every test case by endpoint. Each case has an ID, description, input, expected HTTP status, and which layer covers it.
### TC format
| Field | Meaning |
|---|---|
| ID | Unique identifier, e.g. `TC-P-01` (P = products) |
| Layer | V = Vitest, C = Curl |
| Input | What is sent |
| Expected | HTTP status + key response fields |
---
### 5.1 GET /api/products
| ID | Layer | Scenario | Input | Expected |
|---|---|---|---|---|
| TC-P-01 | V | No token | No Authorization header | 401 |
| TC-P-02 | V | Invalid token | `Bearer invalid123` | 401 |
| TC-P-03 | V | Revoked token | Valid hash but `revoked_at` set | 401 |
| TC-P-04 | V | Valid token, owns 2 products | Valid token, 2 products in DB | 200, array of 2 |
| TC-P-05 | V | Valid token, is team member | Valid token, member of product owned by other user | 200, includes that product |
| TC-P-06 | V | Valid token, no products | Valid token, no products in DB | 200, empty array |
| TC-P-07 | V | Archived products excluded | 1 active + 1 archived product | 200, array of 1 |
| TC-P-08 | V | Cross-user: other user's products not returned | Token for user A, products owned by user B | 200, empty array |
| TC-P-09 | C | Happy path (lars) | Lars' token | 200, ≥1 product |
---
### 5.2 GET /api/products/:id/next-story
| ID | Layer | Scenario | Input | Expected |
|---|---|---|---|---|
| TC-NS-01 | V | No token | No Authorization header | 401 |
| TC-NS-02 | V | Invalid token | `Bearer invalid` | 401 |
| TC-NS-03 | V | Product not found / not accessible | Valid token, unknown product id | 404 |
| TC-NS-04 | V | No active sprint | Valid token, product with no ACTIVE sprint | 404 |
| TC-NS-05 | V | Active sprint, no IN_SPRINT stories | Valid token, sprint exists but 0 stories | 404 |
| TC-NS-06 | V | Returns highest-priority story | Valid token, 3 IN_SPRINT stories with tasks | 200, story with tasks array |
| TC-NS-07 | V | Cross-user: other user's product | Token for user A, product owned by user B | 404 |
| TC-NS-08 | C | Happy path (lars, active sprint) | Lars' token + DevPlanner product id | 200, story object |
---
### 5.3 GET /api/sprints/:id/tasks?limit=10
| ID | Layer | Scenario | Input | Expected |
|---|---|---|---|---|
| TC-ST-01 | V | No token | No Authorization header | 401 |
| TC-ST-02 | V | Invalid token | `Bearer invalid` | 401 |
| TC-ST-03 | V | Sprint not found / not accessible | Valid token, unknown sprint id | 404 |
| TC-ST-04 | V | Cross-user: other user's sprint | Token for user A, sprint in user B's product | 404 |
| TC-ST-05 | V | Default limit applied | No `?limit` param | 200, ≤10 tasks |
| TC-ST-06 | V | Custom limit respected | `?limit=3`, sprint has 5 tasks | 200, exactly 3 tasks |
| TC-ST-07 | V | Limit boundary: limit=1 | Sprint has multiple tasks | 200, exactly 1 task |
| TC-ST-08 | V | Sprint with 0 tasks | Valid sprint, no tasks | 200, empty array |
| TC-ST-09 | C | Happy path (lars) | Lars' token + active sprint id + `?limit=10` | 200, tasks array |
---
### 5.4 PATCH /api/stories/:id/tasks/reorder
| ID | Layer | Scenario | Input | Expected |
|---|---|---|---|---|
| TC-RO-01 | V | No token | No Authorization header | 401 |
| TC-RO-02 | V | Invalid token | `Bearer invalid` | 401 |
| TC-RO-03 | V | Demo user | Valid token, `isDemo: true` | 403 |
| TC-RO-04 | V | Story not found / not accessible | Valid token, unknown story id | 404 |
| TC-RO-05 | V | Cross-user: other user's story | Token for user A, story in user B's product | 404 |
| TC-RO-06 | V | Empty task_ids array | `{ "task_ids": [] }` | 400 |
| TC-RO-07 | V | task_ids not array | `{ "task_ids": "abc" }` | 400 |
| TC-RO-08 | V | task_ids contains IDs from different story | Valid token, mixed-story task IDs | 400 |
| TC-RO-09 | V | Happy path | Valid token + valid task_ids in new order | 200 |
| TC-RO-10 | C | Happy path (lars) | Lars' token + story id + ordered task ids | 200 |
---
### 5.5 POST /api/stories/:id/log
| ID | Layer | Scenario | Input | Expected |
|---|---|---|---|---|
| TC-L-01 | V | No token | No Authorization header | 401 |
| TC-L-02 | V | Invalid token | `Bearer invalid` | 401 |
| TC-L-03 | V | Demo user | Valid token, `isDemo: true` | 403 |
| TC-L-04 | V | Story not found / not accessible | Valid token, unknown story id | 404 |
| TC-L-05 | V | Cross-user: other user's story | Token for user A, story in user B's product | 404 |
| TC-L-06 | V | Missing `type` field | `{ "content": "..." }` | 400 |
| TC-L-07 | V | Unknown `type` value | `{ "type": "UNKNOWN", "content": "..." }` | 400 |
| TC-L-08 | V | IMPLEMENTATION_PLAN — missing content | `{ "type": "IMPLEMENTATION_PLAN" }` | 400 |
| TC-L-09 | V | IMPLEMENTATION_PLAN — happy path | `{ "type": "IMPLEMENTATION_PLAN", "content": "Approach: ..." }` | 201 |
| TC-L-10 | V | TEST_RESULT — missing status | `{ "type": "TEST_RESULT", "content": "..." }` | 400 |
| TC-L-11 | V | TEST_RESULT — invalid status | `{ "type": "TEST_RESULT", "content": "...", "status": "UNKNOWN" }` | 400 |
| TC-L-12 | V | TEST_RESULT — happy path PASSED | `{ "type": "TEST_RESULT", "content": "...", "status": "PASSED" }` | 201 |
| TC-L-13 | V | TEST_RESULT — happy path FAILED | `{ "type": "TEST_RESULT", "content": "...", "status": "FAILED" }` | 201 |
| TC-L-14 | V | COMMIT — missing commit_hash | `{ "type": "COMMIT", "content": "...", "commit_message": "..." }` | 400 |
| TC-L-15 | V | COMMIT — missing commit_message | `{ "type": "COMMIT", "content": "...", "commit_hash": "abc1234" }` | 400 |
| TC-L-16 | V | COMMIT — happy path | `{ "type": "COMMIT", "content": "...", "commit_hash": "abc1234", "commit_message": "feat: ..." }` | 201 |
| TC-L-17 | C | IMPLEMENTATION_PLAN (lars) | Lars' token + story id + IMPLEMENTATION_PLAN body | 201 |
| TC-L-18 | C | TEST_RESULT PASSED (lars) | Lars' token + story id + TEST_RESULT body | 201 |
| TC-L-19 | C | COMMIT (lars) | Lars' token + story id + COMMIT body | 201 |
---
### 5.6 PATCH /api/tasks/:id
| ID | Layer | Scenario | Input | Expected |
|---|---|---|---|---|
| TC-T-01 | V | No token | No Authorization header | 401 |
| TC-T-02 | V | Invalid token | `Bearer invalid` | 401 |
| TC-T-03 | V | Demo user | Valid token, `isDemo: true` | 403 |
| TC-T-04 | V | Task not found | Valid token, unknown task id | 404 |
| TC-T-05 | V | Cross-user: task in other user's product | Token for user A, task in user B's product | 404 |
| TC-T-06 | V | Invalid status value | `{ "status": "UNKNOWN" }` | 400 |
| TC-T-07 | V | Empty body (no recognized fields) | `{}` | 400 |
| TC-T-08 | V | Update status only | `{ "status": "IN_PROGRESS" }` | 200 |
| TC-T-09 | V | Update implementation_plan only | `{ "implementation_plan": "Step 1: ..." }` | 200 |
| TC-T-10 | V | Update both fields | `{ "status": "DONE", "implementation_plan": "..." }` | 200 |
| TC-T-11 | V | Team member can update task | Token for team member (not owner), valid task | 200 |
| TC-T-12 | C | Update status to IN_PROGRESS (lars) | Lars' token + task id + `{ "status": "IN_PROGRESS" }` | 200 |
| TC-T-13 | C | Update status to DONE (lars) | Lars' token + task id + `{ "status": "DONE" }` | 200 |
---
### 5.7 POST /api/todos
| ID | Layer | Scenario | Input | Expected |
|---|---|---|---|---|
| TC-TD-01 | V | No token | No Authorization header | 401 |
| TC-TD-02 | V | Invalid token | `Bearer invalid` | 401 |
| TC-TD-03 | V | Demo user | Valid token, `isDemo: true` | 403 |
| TC-TD-04 | V | Missing title | `{ "product_id": "..." }` | 400 |
| TC-TD-05 | V | Empty title | `{ "title": "" }` | 400 |
| TC-TD-06 | V | Without product_id (global todo) | `{ "title": "My todo" }` | 201 |
| TC-TD-07 | V | With valid product_id | `{ "title": "My todo", "product_id": "..." }` | 201 |
| TC-TD-08 | V | With product_id not accessible to user | `{ "title": "...", "product_id": "<other user's product>" }` | 403 or 404 |
| TC-TD-09 | C | Happy path without product (lars) | Lars' token + `{ "title": "Test todo" }` | 201 |
| TC-TD-10 | C | Happy path with product (lars) | Lars' token + `{ "title": "...", "product_id": "..." }` | 201 |
---
## 6. Test Files
| File | Endpoints covered | Layer |
|---|---|---|
| `__tests__/api/security.test.ts` | products (GET), tasks/:id (PATCH) — already exists, extend | V |
| `__tests__/api/products.test.ts` | GET /api/products — happy paths + edge cases | V |
| `__tests__/api/next-story.test.ts` | GET /api/products/:id/next-story | V |
| `__tests__/api/sprint-tasks.test.ts` | GET /api/sprints/:id/tasks | V |
| `__tests__/api/story-log.test.ts` | POST /api/stories/:id/log | V |
| `__tests__/api/reorder.test.ts` | PATCH /api/stories/:id/tasks/reorder | V |
| `__tests__/api/tasks.test.ts` | PATCH /api/tasks/:id | V |
| `__tests__/api/todos.test.ts` | POST /api/todos | V |
| `scripts/test-api.sh` | All 7 endpoints | C |
---
## 7. Exit Criteria
The test phase is complete when all of the following are met:
| Criterion | Target |
|---|---|
| All Vitest tests pass | `npm test` exits 0 |
| No test is skipped or pending | 0 skipped |
| All curl scripts return expected HTTP codes | 100% pass rate on `scripts/test-api.sh` |
| Demo user blocked on all 4 write endpoints | Verified via TC-RO-03, TC-L-03, TC-T-03, TC-TD-03 |
| Cross-user access impossible | Verified via TC-P-08, TC-NS-07, TC-ST-04, TC-RO-05, TC-L-05, TC-T-05, TC-TD-08 |
| Security review passed | No cross-user data leak found in any endpoint |
---
## 8. Risks & Mitigations
| Risk | Likelihood | Impact | Mitigation |
|---|---|---|---|
| Mock divergence: mocked Prisma behavior differs from real DB | Medium | High | Keep mocked boundary narrow (only `prisma.*` calls, not business logic); validate happy paths with curl against real DB |
| Seed data insufficient for cross-user tests | Medium | Medium | Add a second test user `tester` to `prisma/seed.ts` |
| API token not available during curl tests | Low | High | Document token creation step clearly in `test-api.sh` header |
| Zod schema changes break test expectations | Low | Low | Vitest tests will catch this immediately on next `npm test` run |
| Reorder scope validation not fully covered | Medium | High | TC-RO-08 explicitly tests mixed-story IDs; add to security review checklist |
---
## 9. Dependencies
- `prisma/seed.ts` must create (or document creation of) a second user for cross-user tests
- `scripts/` directory must exist before running the curl script
- A valid API token must be obtained from a seeded `lars` user session before running curl tests
- `npm test` must be runnable without a live database (all Vitest tests mock Prisma)
---
---
# Test Planning
## Overview
| Phase | What | Duration | Start | End |
|---|---|---|---|---|
| P0 | Setup & shared infrastructure | 0.5 day | 2026-04-28 | 2026-04-28 |
| P1 | Security & auth layer (Vitest) | 1 day | 2026-04-29 | 2026-04-29 |
| P2 | Per-endpoint unit tests (Vitest) | 2 days | 2026-04-30 | 2026-05-01 |
| P3 | Curl scripts | 0.5 day | 2026-05-02 | 2026-05-02 |
| P4 | Review, edge cases, DoD verification | 0.5 day | 2026-05-05 | 2026-05-05 |
Total: **4.5 days**
---
## Phase 0 — Setup & Infrastructure (2026-04-28)
**Goal:** Everything required to write and run tests is in place.
### Tasks
| ID | Task | Deliverable |
|---|---|---|
| P0-1 | Add `tester` user to `prisma/seed.ts` with no shared products | Updated seed file |
| P0-2 | Create `scripts/` directory with `test-api.sh` skeleton (TOKEN var, helper functions, empty cases) | `scripts/test-api.sh` |
| P0-3 | Verify `npm test` runs cleanly on `security.test.ts` | Green CI |
| P0-4 | Create `__tests__/api/` test file skeletons (empty describe blocks) for each new file | 7 new `.test.ts` files |
**Done when:** `npm test` passes, all skeleton files exist, seed creates 3 users (demo, lars, tester).
---
## Phase 1 — Security & Auth Layer (2026-04-29)
**Goal:** All auth (401), demo-block (403), and cross-user isolation test cases pass for all endpoints.
### Tasks
| ID | Task | Test cases | File |
|---|---|---|---|
| P1-1 | Extend `security.test.ts` — add missing endpoints to auth/demo/cross-user coverage | TC-NS-0103,07 / TC-ST-0104 / TC-RO-0105 / TC-L-0105 / TC-TD-0103,08 | `security.test.ts` |
| P1-2 | Verify all 401 cases return `{ error: 'Unauthorized' }` | TC-P-0103 etc. | `security.test.ts` |
| P1-3 | Verify all 403 demo cases return `{ error: 'Niet beschikbaar in demo-modus' }` | TC-RO-03, TC-L-03, TC-T-03, TC-TD-03 | `security.test.ts` |
| P1-4 | Verify cross-user returns empty array or 404 (not 403) for read endpoints | TC-P-08, TC-NS-07, TC-ST-04 | `security.test.ts` |
**Done when:** All P1 test cases green, 0 skipped. Security test file covers all 7 endpoints.
---
## Phase 2 — Per-Endpoint Unit Tests (2026-04-30 2026-05-01)
**Goal:** Happy paths, input validation, and edge cases covered per endpoint.
### Day 1 (2026-04-30) — Read endpoints
| ID | Task | Test cases | File |
|---|---|---|---|
| P2-1 | `products.test.ts` — happy paths, empty result, archived filter | TC-P-0407,09 | `products.test.ts` |
| P2-2 | `next-story.test.ts` — happy path, no sprint, no stories | TC-NS-0406,08 | `next-story.test.ts` |
| P2-3 | `sprint-tasks.test.ts` — happy path, limit param, empty sprint | TC-ST-0509 | `sprint-tasks.test.ts` |
### Day 2 (2026-05-01) — Write endpoints
| ID | Task | Test cases | File |
|---|---|---|---|
| P2-4 | `story-log.test.ts` — all 3 log types, each field validation | TC-L-0616 | `story-log.test.ts` |
| P2-5 | `reorder.test.ts` — happy path, empty array, mixed-story IDs | TC-RO-0609 | `reorder.test.ts` |
| P2-6 | `tasks.test.ts` — status update, plan update, both, invalid status, team member access | TC-T-0611 | `tasks.test.ts` |
| P2-7 | `todos.test.ts` — with/without product_id, empty title | TC-TD-0407 | `todos.test.ts` |
**Done when:** All Vitest files pass, `npm test` exits 0 with ≥60 test cases across all files.
---
## Phase 3 — Curl Scripts (2026-05-02)
**Goal:** `scripts/test-api.sh` covers all 7 endpoints and all curl test cases pass against localhost.
### Tasks
| ID | Task | Test cases | |
|---|---|---|---|
| P3-1 | Implement curl script for GET endpoints (products, next-story, sprint-tasks) | TC-P-09, TC-NS-08, TC-ST-09 | |
| P3-2 | Implement curl script for write endpoints (reorder, log ×3, tasks ×2, todos ×2) | TC-RO-10, TC-L-1719, TC-T-1213, TC-TD-0910 | |
| P3-3 | Add negative cases to curl script: no token (401), demo token (403) | TC-P-01, TC-TD-03 | |
| P3-4 | Run full script against seeded local DB, fix any failures | All C cases | |
| P3-5 | Document token acquisition steps in `scripts/README.md` or script header | | |
**Done when:** `bash scripts/test-api.sh` prints all PASS, no failures.
---
## Phase 4 — Review & DoD Verification (2026-05-05)
**Goal:** Test suite is complete, DoD exit criteria are all met.
### Tasks
| ID | Task |
|---|---|
| P4-1 | Run full `npm test` — verify 0 failures, 0 skipped |
| P4-2 | Run `scripts/test-api.sh` against staging/Neon DB — verify all PASS |
| P4-3 | Walk through security review checklist (cross-user access per endpoint) |
| P4-4 | Verify demo user is blocked on all 4 write endpoints via curl |
| P4-5 | Update `__tests__/lars-flow-checklist.md` to reference the new curl script |
| P4-6 | Add test instructions to README (`npm test`, `bash scripts/test-api.sh`) |
| P4-7 | Commit test files per commit strategy (separate commits per layer) |
**Done when:** All exit criteria from section 7 are met. Test plan status → `Approved`.
---
## Commit Plan
Following the strict commit strategy, test work is committed in these layers:
```
chore(tests): add tester user to prisma seed
test(security): extend security.test.ts to cover all 7 endpoints
test(products): add unit tests for GET /api/products
test(next-story): add unit tests for GET /api/products/:id/next-story
test(sprint-tasks): add unit tests for GET /api/sprints/:id/tasks
test(story-log): add unit tests for POST /api/stories/:id/log
test(reorder): add unit tests for PATCH /api/stories/:id/tasks/reorder
test(tasks): add unit tests for PATCH /api/tasks/:id
test(todos): add unit tests for POST /api/todos
chore(scripts): add test-api.sh curl test script
docs(tests): update README with test instructions
```
---
## Summary Timeline
```
Week of 2026-04-28
Mon 28 Apr │ P0 — Setup & infrastructure
Tue 29 Apr │ P1 — Security & auth layer (Vitest)
Wed 30 Apr │ P2 Day 1 — Read endpoint unit tests
Thu 01 May │ P2 Day 2 — Write endpoint unit tests
Fri 02 May │ P3 — Curl scripts
Week of 2026-05-05
Mon 05 May │ P4 — Review, DoD verification, commit
```

View file

@ -0,0 +1,104 @@
---
title: "Branch, PR & Commit Strategy"
status: active
audience: [ai-agent, contributor]
language: nl
last_updated: 2026-05-03
when_to_read: "Before creating a branch, commit, or PR."
---
# Branch, PR & Commit Strategy
## Branch & PR Strategy (STRICT — kostenbeheersing)
> **Core rule: één branch per milestone, PR alleen na gebruikerstest**
Elke `git push` naar een feature-branch triggert een Vercel preview-deployment. Op het huidige Hobby-account zijn die schaars en kosten geld; we minimaliseren preview-builds tot er werkelijk iets te reviewen valt.
### Wel doen
- Eén branch voor de hele milestone — `feat/M{N}-{slug}` (bv. `feat/M10-qr-login`); voor losse stories zonder milestone blijft `feat/ST-XXX-{slug}` geldig
- Commits accumuleren lokaal volgens de Commit Strategy hieronder — één commit per stap, ST-code in de titel
- Pushen + PR openen **pas nadat de gebruiker de milestone handmatig heeft getest en goedgekeurd** — vraag expliciet om bevestiging vóór `git push`
- Tussentijdse "klaar voor jouw test"-momenten markeren met een lokale tag of een berichtje in chat, niet met een push
### Niet doen
- Pushen na elke story of commit
- Een PR per story openen tijdens de implementatie
- "Just-in-case" pushen om backup te hebben — gebruik `git stash`, een lokale tag, of meerdere lokale branches
- `--force-push` om eerdere preview-builds "weg te toveren" (kost dezelfde build opnieuw bij hercreatie)
- **Direct pushen naar `main`** — die branch heeft protection rules; gebruik altijd een PR
### Wanneer wel commit-zonder-vragen, wanneer niet
- **Tijdens een directed sprint-flow** (Track A: `mcp__scrum4me__implement_next_story` of een expliciete *"implementeer M{N}"*-opdracht): commit-per-laag conform de Commit Strategy hieronder is impliciet geautoriseerd — niet per commit vragen
- **Bij ad-hoc / out-of-band werk** (bug-fix tussendoor, refactor, kleine wijziging op verzoek): toon de diff + voorgestelde commit-message en wacht op `"commit it"` voordat je `git commit` draait
- **`git push` is altijd expliciet** — de scope van de policy gaat over preview-builds, dus push gebeurt alleen na gebruiker-test, ongeacht commit-context
### Uitzonderingen op de push-regel
- Een **planning-PR** zonder code-wijzigingen (alleen docs in `docs/plans/` of `docs/`) mag direct gepusht worden — die triggert geen functional regressie en is goedkoop te bouwen
- Een **bugfix-hotfix** op `main` met aantoonbare productie-impact mag direct gepusht worden (via een PR — zie boven)
### Wanneer aanpassen
Zodra het Vercel-account naar Pro (of andere omgeving zonder per-build-kosten) gaat: vervang deze regel door "branch + PR per story" zoals oorspronkelijk in dit document stond. Werk deze sectie bij én documenteer de wijziging in `docs/decisions/agent-instructions-history.md`.
---
## Plan Mode
- Voor simpele, goed-afgebakende file-edits: **niet** in plan mode gaan — gewoon de wijziging maken
- Reserveer plan mode voor multi-step refactors, ambigue verzoeken, of milestone-planning waarbij design-keuzes vooraf bevestigd moeten worden
- Plannen die uit plan mode komen: opslaan als `docs/plans/M{N}-{slug}.md` (zie memory `feedback_plan_location`), niet als ephemeral systeem-bestand
---
## Commit Strategy (STRICT)
> **Core rule: één commit = één verantwoordelijkheid**
### Nooit doen
- Database + API + UI in één commit mengen
- Feature + documentatie combineren
- Grote "alles gewijzigd" commits
- Vage berichten zoals "update stuff"
### Verplichte structuur
Splits werk op in logische lagen:
1. Database / Prisma
2. API / server actions
3. UI / components
4. Config / infra
5. Documentatie
### Commit-formaat
```
feat(ST-XXX): korte beschrijving
fix(ST-XXX): korte beschrijving
chore(ST-XXX): korte beschrijving
docs(ST-XXX): korte beschrijving
```
### Voorbeeld (verplicht patroon)
In plaats van:
```bash
feat: add profile system
```
Splits altijd op in:
```bash
feat(ST-XXX): add user profile fields to Prisma schema
feat(ST-XXX): add avatar upload endpoint
feat(ST-XXX): add profile editor component
chore(ST-XXX): configure sharp for avatar processing
docs(ST-XXX): document profile feature
```

View file

@ -0,0 +1,16 @@
---
title: "Vercel Deployment"
status: active
audience: [ai-agent, contributor]
language: nl
last_updated: 2026-05-03
when_to_read: "Before deploying to Vercel or modifying cron/image/env config."
---
# Deployment (Vercel)
- **Sharp** moet Linux-binaries hebben voor de Vercel-runtime: `npm i --include=optional sharp` of platform-specifieke deps configureren in `package.json`
- **Externe image hostnames** in `next.config.js` `images.remotePatterns` configureren *vóór* `next/image` op die hosts wijst — anders 500 in productie
- **Vercel cron**: Hobby-plan staat alleen daily crons toe (max 1×/dag); Pro ondersteunt fijnmaziger. Bij wijziging van `vercel.json` `crons` ook `docs/api/rest-contract.md` + relevante pattern-docs updaten
- **`CRON_SECRET`** moet als env-var op de Vercel-project-omgeving staan vóór de eerste cron-run, anders 401 op `/api/cron/*`-endpoints
- **Preflight** vóór deploy: `npm run lint && npm test && npm run build` — falende build laat een PR niet door (CI blokkeert merge per ST-610)

View file

@ -0,0 +1,62 @@
---
title: "MCP Integration — Scrum4Me Tools"
status: active
audience: [ai-agent]
language: nl
last_updated: 2026-05-03
when_to_read: "When using MCP tools to interact with the Scrum4Me backlog."
---
# MCP-integratie
Scrum4Me heeft een eigen MCP-server in repo [`madhura68/scrum4me-mcp`](https://github.com/madhura68/scrum4me-mcp) die de REST-API als native tools voor Claude Code aanbiedt. Schema's worden gedeeld via een git submodule (`vendor/scrum4me`), niet gedupliceerd.
## Tools beschikbaar in Claude Code (18)
**Read / context:**
- `mcp__scrum4me__health` — service + DB ping
- `mcp__scrum4me__list_products` — producten waar de tokengebruiker toegang tot heeft
- `mcp__scrum4me__get_claude_context` — bundled product / actieve sprint / next story (met tasks) / open todos
**Authoring (PBI/Story/Task aanmaken):**
- `mcp__scrum4me__create_pbi``{ product_id, title, description?, priority, sort_order? }`; auto sort_order = last+1 binnen prio-groep
- `mcp__scrum4me__create_story``{ pbi_id, title, description?, acceptance_criteria?, priority, sort_order? }`; product_id afgeleid uit PBI; status=OPEN
- `mcp__scrum4me__create_task``{ story_id, title, description?, implementation_plan?, priority, sort_order? }`; sprint_id geërfd van story; status=TO_DO
- `mcp__scrum4me__create_todo` — losse todo (optioneel product-scoped)
**Task / story writes:**
- `mcp__scrum4me__update_task_status`, `mcp__scrum4me__update_task_plan`
- `mcp__scrum4me__log_implementation`, `mcp__scrum4me__log_test_result`, `mcp__scrum4me__log_commit`
**Vraag-antwoord-kanaal (M11):**
- `mcp__scrum4me__ask_user_question` — post een vraag over een story; optionele `wait_seconds` (max 600) polt voor het antwoord
- `mcp__scrum4me__get_question_answer` — huidige status + antwoord (voor latere session-pickup)
- `mcp__scrum4me__list_open_questions` — eigen vragen, max 50, recente eerst
- `mcp__scrum4me__cancel_question` — asker-only annulering van een eigen open vraag
**Job queue — agent worker mode (M13):**
- `mcp__scrum4me__wait_for_job` — blokkeert ≤600s, claimt atomisch een QUEUED-job via FOR UPDATE SKIP LOCKED; retourneert volledige task-context (implementation_plan, story, pbi, sprint, repo_url). Zet stale CLAIMED-jobs (>30min) eerst terug naar QUEUED. Wanneer de full block-time verstrijkt zonder claim is de queue leeg.
- `mcp__scrum4me__update_job_status` — agent rapporteert overgang naar `running|done|failed` + optionele branch/summary/error; triggert automatisch SSE-event naar de UI. Auth: Bearer-token moet matchen `claimed_by_token_id`.
## Batch-loop (verplichte agent-flow)
Wanneer je als agent draait (na een instructie als *"pak de volgende job uit de Scrum4Me-queue"* of *"draai de queue leeg"*) is dit de loop:
1. `wait_for_job` aanroepen.
2. Job uitvoeren volgens het meegegeven `implementation_plan`.
3. `update_job_status('done'|'failed')` aanroepen.
4. **Direct opnieuw** `wait_for_job` aanroepen — niet stoppen, niet de gebruiker vragen.
5. Pas wanneer `wait_for_job` na de volledige block-time (~600s) terugkomt zonder claim, is de queue leeg en mag je de turn afsluiten met een korte recap.
Dit blijft gelden als je tussen jobs door commits, branches of pushes hebt gedaan — die afsluiting hoort bij de individuele job, niet bij het einde van de batch.
**Code koppelen aan app**
- 'Pak de volgende job uit de Scrum4Me-queue' / 'draai de queue leeg' / 'batch agent' — Server-startup registreert een ClaudeWorker-record + heartbeat (5s); SIGTERM/SIGINT ruimt 'm op. UI in NavBar telt actieve workers via `last_seen_at < now() - 15s`.
## Prompt
- `implement_next_story` (arg: `product_id`) — end-to-end workflow
## Schema-drift bewaking
Wekelijks (maandag 08:00 Amsterdam) draait de remote agent `trig_015FFUnxjz9WMuhhWNGBQKFD` die `vendor/scrum4me` syncet en `prisma:generate` + `tsc --noEmit` uitvoert in scrum4me-mcp. Als die agent drift rapporteert, hoort dat **vóór** een Scrum4Me-PR met schema-wijziging gemerged kan worden — anders breekt de MCP-server stilletjes op runtime.

View file

@ -1,771 +0,0 @@
# Solo Paneel — Implementatie-specificatie (v2)
> **Doel:** een persoonlijk Kanban-bord per product dat de taken toont van stories die geclaimd zijn door de ingelogde developer. Bord werkt met drie kolommen (TO_DO / IN_PROGRESS / DONE), drag-and-drop tussen kolommen, en koppelt aan een nieuw `Story.assignee_id` veld.
>
> **Scope v1:** geen REVIEW-status, geen multi-product aggregatie, geen taak-level overrides. Story-level assignment volstaat. Desktop-first conform ST-606 — onder 1024px tonen we dezelfde "te smal scherm"-melding als de rest van de app.
>
> **Versie:** v2 — verwerkt antwoorden uit `backlog.md` over sessie-flag, bestaande Server Actions en desktop-first scope.
---
## Wat veranderde t.o.v. v1
| Onderdeel | v1 aanname | v2 (op basis van backlog) |
|---|---|---|
| `isDemo` toegang | DB-lookup of session, ambivalent | **Komt uit `session.isDemo` (ST-006, ST-604)** — geen DB-call |
| Implementation_plan editen | Bestaande Server Action of API | **Nieuwe `updateTaskPlanAction`** (gericht, optimistisch-vriendelijk) |
| Mobiel | Optionele chunk 13 (tab-strip) | **Geen mobile UI**; volg ST-606 desktop-first patroon |
| Toast | Algemeen genoemd | **Sonner is geïnstalleerd (ST-603)** — gebruik consistent |
| Pending states | Niet uitgewerkt | **`useFormStatus` of `useTransition`** zoals ST-601 voorschrijft |
| Demo-tooltip tekst | "Read-only in demo-modus" | **"Niet beschikbaar in demo-modus"** zoals ST-604 |
| Sprint Board referentie | Generieke "sprint board" | **ST-313 drie-panelen Sprint Board** — assignee-UI komt in middenpaneel |
---
## 1. Datamodel — Prisma migratie
Eén veld erbij, één index erbij. Geen enum-wijzigingen.
```prisma
model Story {
// ... bestaande velden ongewijzigd ...
assignee_id String?
assignee User? @relation("StoryAssignee", fields: [assignee_id], references: [id], onDelete: SetNull)
@@index([sprint_id, assignee_id]) // hot path: solo-bord query
// bestaande indexen ongewijzigd
}
model User {
// ... bestaande velden ongewijzigd ...
assigned_stories Story[] @relation("StoryAssignee")
}
```
**Migratie:**
```bash
npx prisma migrate dev --name add_story_assignee
```
**onDelete-keuze:** `SetNull` zodat verwijderen van een user de stories behoudt (assignee valt terug naar "team"). Cascade zou stories vernietigen — niet wat we willen.
**Named relation `"StoryAssignee"`:** voorkomt botsing met andere mogelijke User↔Story relations in de toekomst.
---
## 2. Auth-helper (`lib/auth.ts` uitbreiding)
`isDemo` zit al in de sessiecookie sinds ST-006 — geen DB-lookup nodig.
```typescript
import { getIronSession } from 'iron-session'
import { cookies } from 'next/headers'
import { sessionOptions, type SessionData } from '@/lib/session'
import { prisma } from '@/lib/prisma'
export async function getSession() {
return getIronSession<SessionData>(await cookies(), sessionOptions)
}
export async function requireUser() {
const session = await getSession()
if (!session?.userId) throw new Error('Niet ingelogd')
return session
}
export async function requireWriter() {
const session = await requireUser()
if (session.isDemo) throw new Error('Niet beschikbaar in demo-modus')
return session
}
export async function requireProductAccess(productId: string) {
const session = await requireUser()
const product = await prisma.product.findFirst({
where: {
id: productId,
OR: [
{ user_id: session.userId }, // owner
{ members: { some: { user_id: session.userId } } }, // member
],
},
select: { id: true },
})
if (!product) throw new Error('Geen toegang tot dit product')
return session
}
export async function requireProductWriter(productId: string) {
const session = await requireProductAccess(productId)
if (session.isDemo) throw new Error('Niet beschikbaar in demo-modus')
return session
}
```
**Patroon-uitleg:**
- `requireUser` — ingelogd, anders fout
- `requireWriter` — ingelogd én niet-demo
- `requireProductAccess` — ingelogd én lid (read)
- `requireProductWriter` — ingelogd én lid én niet-demo (write)
**Afhankelijkheid:** controleer of bestaande `actions/*.ts` een eigen lokale `getSession` definiëren. Zo ja, optioneel migreren bij gelegenheid (geen blocker).
---
## 3. Server Actions
### 3a. Story-claim acties (`actions/stories.ts` uitbreiding)
```typescript
'use server'
import { z } from 'zod'
import { revalidatePath } from 'next/cache'
import { prisma } from '@/lib/prisma'
import { requireProductWriter } from '@/lib/auth'
// ---------------------------------------------------------------------------
const claimSchema = z.object({
storyId: z.string().cuid(),
productId: z.string().cuid(),
})
export async function claimStoryAction(input: z.infer<typeof claimSchema>) {
const { storyId, productId } = claimSchema.parse(input)
const session = await requireProductWriter(productId)
await prisma.story.update({
where: { id: storyId, product_id: productId }, // tenant-guard
data: { assignee_id: session.userId },
})
revalidatePath(`/products/${productId}/sprint`)
revalidatePath(`/products/${productId}/solo`)
}
// ---------------------------------------------------------------------------
export async function unclaimStoryAction(input: z.infer<typeof claimSchema>) {
const { storyId, productId } = claimSchema.parse(input)
await requireProductWriter(productId)
await prisma.story.update({
where: { id: storyId, product_id: productId },
data: { assignee_id: null },
})
revalidatePath(`/products/${productId}/sprint`)
revalidatePath(`/products/${productId}/solo`)
}
// ---------------------------------------------------------------------------
const reassignSchema = z.object({
storyId: z.string().cuid(),
productId: z.string().cuid(),
targetUserId: z.string().cuid(),
})
export async function reassignStoryAction(input: z.infer<typeof reassignSchema>) {
const { storyId, productId, targetUserId } = reassignSchema.parse(input)
await requireProductWriter(productId)
// Valideer dat target-user lid is van het product (anders cross-tenant assignment)
const isMember = await prisma.product.findFirst({
where: {
id: productId,
OR: [
{ user_id: targetUserId },
{ members: { some: { user_id: targetUserId } } },
],
},
select: { id: true },
})
if (!isMember) throw new Error('Doel-gebruiker is geen lid van dit product')
await prisma.story.update({
where: { id: storyId, product_id: productId },
data: { assignee_id: targetUserId },
})
revalidatePath(`/products/${productId}/sprint`)
revalidatePath(`/products/${productId}/solo`)
}
// ---------------------------------------------------------------------------
const bulkClaimSchema = z.object({ productId: z.string().cuid() })
export async function claimAllUnassignedInActiveSprintAction(
input: z.infer<typeof bulkClaimSchema>,
) {
const { productId } = bulkClaimSchema.parse(input)
const session = await requireProductWriter(productId)
const activeSprint = await prisma.sprint.findFirst({
where: { product_id: productId, status: 'ACTIVE' },
select: { id: true },
})
if (!activeSprint) throw new Error('Geen actieve sprint gevonden')
const result = await prisma.story.updateMany({
where: {
sprint_id: activeSprint.id,
product_id: productId,
assignee_id: null,
},
data: { assignee_id: session.userId },
})
revalidatePath(`/products/${productId}/sprint`)
revalidatePath(`/products/${productId}/solo`)
return { claimed: result.count }
}
```
### 3b. Implementation plan editen (`actions/tasks.ts` uitbreiding)
Bestaande `updateTaskStatus` (ST-310) en `updateTask` (ST-311) blijven ongewijzigd. We voegen één nieuwe gerichte action toe:
```typescript
'use server'
const planSchema = z.object({
taskId: z.string().cuid(),
productId: z.string().cuid(), // voor tenant-guard
implementationPlan: z.string().max(20000),
})
export async function updateTaskPlanAction(input: z.infer<typeof planSchema>) {
const { taskId, productId, implementationPlan } = planSchema.parse(input)
await requireProductWriter(productId)
// Tenant-guard via geneste relatie
await prisma.task.update({
where: {
id: taskId,
story: { product_id: productId }, // verifieer dat task bij product hoort
},
data: { implementation_plan: implementationPlan },
})
revalidatePath(`/products/${productId}/solo`)
revalidatePath(`/products/${productId}/sprint`)
}
```
**Waarom een aparte action:** korter, optimistisch-vriendelijk (kleine payload, lage latency), past bij save-on-blur in de detail-dialoog. De bestaande `updateTask` is voor volledige edits via een formulier.
**Toast/UX:** geen success-toast (te frequent bij save-on-blur). Wel error-toast bij fout. Indicator in dialoog (*"Bezig met opslaan…"* / *"Opgeslagen"*).
---
## 4. Routes en pagina's
```
app/
├── solo/
│ └── page.tsx # /solo → redirect of picker
└── products/
└── [id]/
├── sprint/page.tsx # bestaand (ST-313 drie-panelen) — krijgt UI-uitbreidingen
└── solo/
└── page.tsx # /products/[id]/solo → het bord
```
### 4a. `/solo` — Redirect-pagina
Server Component. Leest cookie `lastProductId`, valideert toegang, redirect.
```typescript
// app/solo/page.tsx
import { cookies } from 'next/headers'
import { redirect } from 'next/navigation'
import { prisma } from '@/lib/prisma'
import { requireUser } from '@/lib/auth'
import { ProductPicker } from '@/components/solo/product-picker'
export default async function SoloRedirectPage() {
const session = await requireUser()
const lastProductId = (await cookies()).get('lastProductId')?.value
if (lastProductId) {
const valid = await prisma.product.findFirst({
where: {
id: lastProductId,
archived: false,
OR: [
{ user_id: session.userId },
{ members: { some: { user_id: session.userId } } },
],
},
select: { id: true },
})
if (valid) redirect(`/products/${valid.id}/solo`)
}
// Geen valide cookie → toon picker
const products = await prisma.product.findMany({
where: {
archived: false,
OR: [
{ user_id: session.userId },
{ members: { some: { user_id: session.userId } } },
],
},
select: { id: true, name: true },
orderBy: { updated_at: 'desc' },
})
return <ProductPicker products={products} basePath="/solo" />
}
```
### 4b. `/products/[id]/solo` — Het Solo Bord
Server Component. Doet alle queries en geeft data door aan een client-side `<SoloBoard>`.
```typescript
// app/products/[id]/solo/page.tsx
import { notFound } from 'next/navigation'
import { prisma } from '@/lib/prisma'
import { requireUser } from '@/lib/auth'
import { setLastProductCookie } from '@/lib/cookies'
import { SoloBoard } from '@/components/solo/solo-board'
import { NoActiveSprint } from '@/components/solo/no-active-sprint'
export default async function SoloPage({
params,
}: {
params: Promise<{ id: string }>
}) {
const { id } = await params
const session = await requireUser()
await setLastProductCookie(id)
const product = await prisma.product.findFirst({
where: {
id,
OR: [
{ user_id: session.userId },
{ members: { some: { user_id: session.userId } } },
],
},
select: { id: true, name: true },
})
if (!product) notFound()
const activeSprint = await prisma.sprint.findFirst({
where: { product_id: id, status: 'ACTIVE' },
select: { id: true, sprint_goal: true },
})
if (!activeSprint) return <NoActiveSprint product={product} />
// Parallel: eigen taken + count ongeclaimde stories
const [tasks, unassignedStoryCount] = await Promise.all([
prisma.task.findMany({
where: {
sprint_id: activeSprint.id,
story: { assignee_id: session.userId },
},
select: {
id: true,
title: true,
priority: true,
sort_order: true,
status: true,
description: true,
implementation_plan: true,
story: { select: { id: true, title: true } },
},
orderBy: [{ priority: 'desc' }, { sort_order: 'asc' }],
}),
prisma.story.count({
where: { sprint_id: activeSprint.id, assignee_id: null },
}),
])
return (
<SoloBoard
product={product}
sprint={activeSprint}
tasks={tasks}
unassignedStoryCount={unassignedStoryCount}
isDemo={session.isDemo}
/>
)
}
```
**Performance:**
- Query gebruikt `[sprint_id, assignee_id]` index die we toevoegen → snelle filter
- `Promise.all` parallelliseert de twee onafhankelijke queries
- `select` projectie houdt payload klein
---
## 5. Cookie-helper (`lib/cookies.ts`)
```typescript
'use server'
import { cookies } from 'next/headers'
const ONE_MONTH = 60 * 60 * 24 * 30
export async function setLastProductCookie(productId: string) {
const store = await cookies()
store.set('lastProductId', productId, {
httpOnly: true,
sameSite: 'lax',
secure: process.env.NODE_ENV === 'production',
maxAge: ONE_MONTH,
path: '/',
})
}
```
---
## 6. Sprint Board (ST-313) uitbreidingen
In het middenpaneel (Sprint Backlog) van het drie-panelen Sprint Board komen de assignee-UI elementen.
### 6a. Story-kaart op het Sprint Backlog paneel
Nieuwe elementen op elke story-kaart:
```
┌──────────────────────────────────────────────────┐
│ ⚡ Story title [···] │ ← actie-menu rechts
│ Some PBI · 3 taken │
│ ───────────────────────────────────────────── │
│ [👤 jan.visser] of [— Niet geclaimd] │ ← assignee-chip
└──────────────────────────────────────────────────┘
```
**Assignee-chip:** klein component met `<UserAvatar size="xs">` + username, of een muted badge (`bg-muted text-muted-foreground`) als `assignee_id === null`.
**Actie-menu (shadcn `DropdownMenu`):**
- *Pak op*`claimStoryAction` — zichtbaar als ongeclaimd of niet-jij
- *Geef terug aan team*`unclaimStoryAction` — zichtbaar als geclaimd
- *Wijs toe aan ▶* (submenu met members) → `reassignStoryAction`
**Demo-modus:** hele dropdown disabled met tooltip *"Niet beschikbaar in demo-modus"* (consistent met ST-604).
### 6b. Bovenaan het Sprint Backlog paneel
```tsx
<div className="flex items-center justify-between">
<h2>Sprint Backlog</h2>
<Button
onClick={handleClaimAll}
disabled={unassignedCount === 0 || isDemo}
variant="outline"
>
Claim alle ongeclaimde stories ({unassignedCount})
</Button>
</div>
```
Na succes: Sonner-toast *"X stories geclaimd"* (gewone success-toast, niet drag-and-drop frequentie). Bij demo: knop disabled met tooltip *"Niet beschikbaar in demo-modus"*.
---
## 7. Solo Paneel componenten
```
components/solo/
├── solo-board.tsx # Client root, dnd context, layout
├── solo-column.tsx # Drop target per status
├── solo-task-card.tsx # Draggable kaart (bestaande task-card hergebruiken)
├── task-detail-dialog.tsx # Shadcn Dialog
├── unassigned-stories-sheet.tsx # Shadcn Sheet
├── no-active-sprint.tsx # Empty state
└── product-picker.tsx # Voor /solo zonder cookie
```
### 7a. `<SoloBoard>` — root component
```typescript
'use client'
interface Props {
product: { id: string; name: string }
sprint: { id: string; sprint_goal: string }
tasks: TaskWithStory[]
unassignedStoryCount: number
isDemo: boolean
}
export function SoloBoard({ product, sprint, tasks, unassignedStoryCount, isDemo }: Props) {
// Zustand store gehydrateerd met initiële taken
// DndContext (overslaan als isDemo) met sensor + collision detection
// Header: productnaam, sprint goal, knop "Toon openstaande stories (N)"
// Drie kolommen in een grid (md:grid-cols-3)
}
```
### 7b. Zustand store (`stores/solo-store.ts`)
Volgt het patroon van `usePlannerStore` (ST-201): `init*`, `optimistic*`, `rollback*`.
```typescript
import { create } from 'zustand'
import type { TaskStatus } from '@prisma/client'
interface SoloState {
tasks: TaskWithStory[]
initTasks: (tasks: TaskWithStory[]) => void
optimisticMove: (taskId: string, toStatus: TaskStatus) => TaskStatus | null // returns prev for rollback
rollback: (taskId: string, prevStatus: TaskStatus) => void
updatePlan: (taskId: string, plan: string) => void
}
export const useSoloStore = create<SoloState>((set, get) => ({
tasks: [],
initTasks: (tasks) => set({ tasks }),
optimisticMove: (taskId, toStatus) => {
const task = get().tasks.find(t => t.id === taskId)
if (!task) return null
const prev = task.status
set({ tasks: get().tasks.map(t => t.id === taskId ? { ...t, status: toStatus } : t) })
return prev
},
rollback: (taskId, prevStatus) => {
set({ tasks: get().tasks.map(t => t.id === taskId ? { ...t, status: prevStatus } : t) })
},
updatePlan: (taskId, plan) => {
set({ tasks: get().tasks.map(t => t.id === taskId ? { ...t, implementation_plan: plan } : t) })
},
}))
```
### 7c. Drag-and-drop (dnd-kit)
```typescript
function handleDragEnd(event: DragEndEvent) {
const { active, over } = event
if (!over) return
const taskId = String(active.id)
const toStatus = String(over.id) as TaskStatus // kolom-id = status enum-value
if (!['TO_DO', 'IN_PROGRESS', 'DONE'].includes(toStatus)) return
const prev = useSoloStore.getState().optimisticMove(taskId, toStatus)
if (prev === null || prev === toStatus) return
startTransition(async () => {
try {
await updateTaskStatusAction(taskId, toStatus)
} catch (err) {
useSoloStore.getState().rollback(taskId, prev)
toast.error('Status bijwerken mislukt — taak teruggeplaatst')
}
})
}
```
**Sensor-keuze:** `PointerSensor` met `activationConstraint: { distance: 5 }` om accidentele drags op klik te voorkomen. Klik = open dialog, drag = verplaats.
**Collision detection:** `closestCorners` voor kolom-niveau drops; geen sortering binnen kolom in v1.
**Toast-strategie:** consistent met ST-603 — geen success-toast bij drag (te frequent), wél error-toast bij rollback.
**Demo-user:** sla de hele DndContext over en wrap kaart-componenten zonder draggable. Klik werkt nog wel (lezen mag).
### 7d. `<SoloColumn>`
Status-token mapping (briefing):
| Status | Header background |
|---|---|
| `TO_DO` | `bg-status-todo/15 text-status-todo border-status-todo/30` |
| `IN_PROGRESS` | `bg-status-in-progress/15 text-status-in-progress border-status-in-progress/30` |
| `DONE` | `bg-status-done/15 text-status-done border-status-done/30` |
### 7e. `<SoloTaskCard>` — hergebruik bestaande task-card
Bestaande task-card uit het sprint board (ST-313 rechterpaneel) hergebruiken. Pas zo nodig aan:
- Linker-rand of dot met `bg-priority-{level}` voor prioriteit
- Taaktitel (`font-medium`, `truncate`)
- Story-titel (`text-sm text-muted-foreground`, `truncate`)
- Optionele `showProduct?: boolean` prop (off op product-specifieke pagina; reservering voor toekomstig multi-product bord)
Klik → opent `<TaskDetailDialog>` met deze taak.
### 7f. `<TaskDetailDialog>`
Shadcn `Dialog`. Inhoud:
- Header: taaktitel + statusbadge (gekleurd via MD3 tokens)
- Sectie *Beschrijving* (read-only `<p>` of formatted block — volg bestaand task-detailpatroon)
- Sectie *Implementatieplan*: `<Textarea>` met save-on-blur
- On blur: `updateTaskPlanAction({ taskId, productId, implementationPlan })`
- Indicator rechtsonder: *"Bezig met opslaan…"* tijdens transition, *"Opgeslagen"* daarna (vervaagt na 2s)
- Bij fout: error-toast + waarde rollback in store
- Footer: link *"Open in Sprint Board ↗"* naar `/products/[id]/sprint?storyId=...`
- Demo-modus: textarea heeft `readOnly` + tooltip *"Niet beschikbaar in demo-modus"*
```typescript
function handleBlur(plan: string) {
if (plan === task.implementation_plan) return // geen no-op call
startTransition(async () => {
try {
await updateTaskPlanAction({ taskId: task.id, productId, implementationPlan: plan })
useSoloStore.getState().updatePlan(task.id, plan)
} catch (err) {
toast.error('Opslaan mislukt')
}
})
}
```
Markdown-rendering voor implementatieplan kan in v2; voor v1 plain text in textarea — sneller te bouwen, past bij scope.
### 7g. `<UnassignedStoriesSheet>`
Shadcn `Sheet` (slide-out van rechts). Trigger: knop bovenaan het bord met badge `(N)`.
Inhoud:
- Lijst van ongeclaimde stories in actieve sprint, met titel + taakaantal
- Per item: knop *"Pak op"*`claimStoryAction` → revalidate → Sonner toast
- Sheet blijft open tot user 'm sluit (zodat meerdere achter elkaar claimen kan)
- Lege staat: *"Geen ongeclaimde stories. Lekker bezig!"*
`useFormStatus` op de claim-knoppen voor pending state (ST-601).
### 7h. `<NoActiveSprint>` — empty state
Geen ACTIVE sprint: nette empty-state met titel, korte uitleg en link naar productpagina om er een te starten (ST-302 stappen).
---
## 8. `<UserAvatar>` component (nieuw, herbruikbaar)
```
components/ui/user-avatar.tsx
```
```typescript
interface Props {
userId: string
username: string
size?: 'xs' | 'sm' | 'md' | 'lg'
className?: string
}
export function UserAvatar({ userId, username, size = 'md', className }: Props) {
const sizeClasses = {
xs: 'h-5 w-5 text-[10px]',
sm: 'h-6 w-6 text-xs',
md: 'h-8 w-8 text-sm',
lg: 'h-10 w-10 text-base',
}
const initials = username.slice(0, 2).toUpperCase()
return (
<Avatar className={cn(sizeClasses[size], className)}>
<AvatarImage
src={`/api/users/${userId}/avatar`}
alt={username}
/>
<AvatarFallback className="bg-primary-container text-primary">
{initials}
</AvatarFallback>
</Avatar>
)
}
```
Gebaseerd op shadcn `<Avatar>`. Fallback in MD3-token (`bg-primary-container`).
**Aandachtspunt:** als `/api/users/[id]/avatar` 404 returnt (user heeft geen avatar gezet), valt shadcn automatisch terug op `<AvatarFallback>` met initialen. Test dit gedrag — anders forceer je via `onError`.
**Hergebruik:** dit component is ook nuttig in toekomstige plekken (story-detail, instellingen, sprint board kaarten) — geen Solo-specifieke component.
---
## 9. Demo-modus
Eenvoudig nu we weten dat `isDemo` in de sessiecookie zit:
**Drie plekken waar `isDemo` ertoe doet:**
1. **Server Actions**`requireProductWriter` (en `requireWriter`) throwt early met *"Niet beschikbaar in demo-modus"*. Doe je niets, dan kan een demo-user via gespoofte requests toch wijzigen.
2. **UI-knoppen** — disabled + tooltip *"Niet beschikbaar in demo-modus"* (ST-604 conventie). Pass `isDemo` als prop door vanaf de Server Component.
3. **DndContext** — wrap kaarten zonder `useDraggable` als `isDemo`, of zet `disabled` op de hele context.
**Seed-vereiste:** in `prisma/seed.ts` (ST-004) zorgen dat de demo-user (`is_demo = true`) een product heeft met:
- Een ACTIVE sprint
- Stories met `assignee_id = demoUser.id` en bijbehorende taken in alle drie statussen (om bord werkend te tonen)
- Minstens 1 ongeclaimde story (om "Toon openstaande" te demonstreren — demo-user kan niet claimen, ziet wel hoe het werkt)
---
## 10. Navbar
```tsx
// components/navbar.tsx (uitbreiding)
<NavLink href="/solo" icon={<UserSquare className="h-4 w-4" />}>
Solo
</NavLink>
```
Plek: tussen "Producten" en "Todos" (of zoals layout het bepaalt). Altijd zichtbaar voor ingelogde users — geen product-context nodig, die kiest de redirect-handler zelf.
---
## 11. Werkvolgorde voor Claude Code (chunks)
Elke chunk komt overeen met een story uit M3.5 in de backlog en is **afzonderlijk reviewbaar en commitbaar**.
| # | Story | Inhoud | Verifiëer met |
|---|---|---|---|
| 1 | **ST-350** | Schema-migratie + auth-helpers | `prisma migrate dev` slaagt; helpers werken vanuit testbestand |
| 2 | **ST-351** | `<UserAvatar>` component | Visuele check op 4 sizes; fallback bij ontbrekende avatar |
| 3 | **ST-352** | Story-claim Server Actions (4 acties) | Aanroepen vanuit Sprint Board of test-route; demo-guard werkt |
| 4 | **ST-353** | Sprint Board: assignee-chip + dropdown | Klikken claimt; demo-user krijgt disabled tooltip |
| 5 | **ST-354** | Sprint Board: bulk-claim knop + count | Werkt in regular/demo (disabled) sessie + toast |
| 6 | **ST-355** | Solo route + queries + empty states + cookie | `/solo` redirect werkt; pagina toont juiste taken |
| 7 | **ST-356** | Solo Kanban + Zustand + DnD | Sleep tussen kolommen, status persisteert; netwerk-fail → rollback |
| 8 | **ST-357** | Task detail-dialoog + `updateTaskPlanAction` | Edit, blur, refresh: persisteert; demo: read-only |
| 9 | **ST-358** | Openstaande stories sheet | Sheet opent met N items; claimen werkt; lege staat correct |
| 10 | **ST-359** | Navbar-link "Solo" | Klik gaat naar `/solo` (en redirect verder) |
| 11 | **ST-360** | Demo-seed uitbreiden | Login als demo, Solo bord toont werkende staat |
**Bouwvolgorde-inzicht:** chunks 1-5 leveren al **op het Sprint Board** (ST-313) een werkend assignment-systeem. Daar is een natuurlijke release-grens. Chunks 6-9 vormen het Solo Paneel zelf. Chunks 10-11 zijn polish & demo.
---
## 12. Acceptatiecriteria (volledig v1)
**Functioneel:**
- [ ] Een user kan op het Sprint Board een story claimen, teruggeven, of aan een andere member toewijzen
- [ ] Een user kan met één klik alle ongeclaimde stories in de actieve sprint claimen
- [ ] `/solo` redirect naar laatst-bezochte product, met fallback naar product-picker
- [ ] Solo-bord toont alle taken van geclaimde stories in de actieve sprint, gegroepeerd in 3 kolommen
- [ ] Drag-and-drop tussen kolommen update status, met optimistische UI en rollback bij fout
- [ ] Klik op taakkaart opent dialoog met bewerkbaar implementatieplan (save-on-blur)
- [ ] Knop bovenaan toont openstaande stories en laat ze individueel claimen
- [ ] Navbar-link "Solo" altijd zichtbaar voor ingelogde users
**Niet-functioneel:**
- [ ] Demo-user kan lezen maar niets muteren — alle Server Actions throwen, alle knoppen disabled met tooltip *"Niet beschikbaar in demo-modus"*
- [ ] Membership-check werkt voor zowel owner (`Product.user_id`) als members (`ProductMember`)
- [ ] Reassignment kan alleen naar geldige product-members
- [ ] Foutberichten in het Nederlands voor eindgebruikers
- [ ] Stylingregels uit briefing (MD3-tokens) consistent toegepast
- [ ] Desktop-first; volgt ST-606 melding bij < 1024px
**Performance:**
- [ ] Solo-pagina laadt < 500ms voor sprint met 50 taken (lokaal)
- [ ] Optimistische update voelt direct (< 50ms)
---
## 13. Nog open / mogelijke v1.1
1. **Sortering binnen kolom** — drag binnen kolom is in v1 een no-op. Toekomstige uitbreiding via `solo_sort_order` veld of een aparte `UserTaskOrder`-tabel.
2. **Markdown-rendering implementatieplan** — v2; v1 is plain textarea.
3. **Multi-product Solo bord** — alle producten in één bord. Component is hier al op voorbereid via optionele `showProduct` prop op task card.
4. **REVIEW-status** — bewuste scope-uitstel; voegt later kolom + enum-migratie toe.
---
*Klaar om te valideren en aan Claude Code te geven.*

128
docs/specs/dialogs/pbi.md Normal file
View file

@ -0,0 +1,128 @@
---
title: "PbiDialog Profiel"
status: active
audience: [ai-agent, contributor]
language: nl
last_updated: 2026-05-03
---
# PbiDialog Profiel
> Volgt **`docs/patterns/dialog.md`** (de generieke spec voor élke entity-dialog in Scrum4Me).
> Dit document beschrijft alleen de PBI-specifieke afwijkingen en keuzes — alle gedeelde regels (layout, motion, demo-policy, foutcodes, validatie, theming, dialog-gedrag) staan in de generieke spec en worden hier niet herhaald.
> **Belangrijk:** als een regel in dit profiel botst met de generieke spec, wint de generieke spec. Documenteer hier de afwijking + reden, of pas de generieke spec aan.
---
## Velden
| Veld | Type | Mode | Validatie |
|---|---|---|---|
| `code` | `string \| null` | beide | optional, max 30 chars, mono-font, placeholder `auto` op create (server kent dan zelf een code toe) |
| `title` | `string` (required) | beide | trim, 1-200 chars |
| `priority` | `int` (1-4, P1 = hoogste) | beide | int 1-4, default 2 (kan via `defaultPriority`-prop bij create) |
| `status` | `PbiStatusApi` enum | beide | enum, default `'ready'` |
| `description` | `string \| null` | beide | optional, max 2000 chars, plain textarea (geen markdown rendering binnen de dialog) |
`PbiStatusApi` enum (lowercase, mapped via `lib/task-status.ts`): zie `<PbiStatusSelect>` voor de waarden.
### Veld-specifiek gedrag
- **Code + Titel** in één rij (`grid-cols-[6rem_1fr]`)
- **Prioriteit + Status** in één rij (`grid-cols-2`)
- **Prioriteit** via `<PrioritySelect>` (gedeelde primitive, géén segmented buttons in deze dialog)
- **Status** via `<PbiStatusSelect>` (PBI-specifieke wrapper rond gedeelde select)
- **Description** is `<Textarea rows={3} resize-none>` — géén auto-grow, géén markdown-hint, géén char-counter (afwijking van generieke spec; rationale: PBI-descriptions zijn doorgaans kort en richtinggevend)
---
## URL- of state-pattern
- **Gekozen:** state-based (`state: PbiDialogState | null` prop, gerendeerd binnen `PbiList`)
- **Reden:** PBI-dialog leeft altijd binnen `PbiList` op de product-backlog-pagina; deep-linking is niet vereist en zou een tweede edit-flow toevoegen.
- **State-shape:**
```ts
type PbiDialogState =
| { mode: 'create'; productId: string; defaultPriority?: number }
| { mode: 'edit'; pbi: PbiDialogPbi; productId: string }
```
- **Sluiten:** `onClose()` callback uit de parent — `setState(null)` in `PbiList`.
---
## Status-veld
- **Default bij create:** `'ready'` (PBI-default state)
- **Geen verberging in create-mode** — anders dan TaskDialog wordt status hier wél getoond bij create, omdat een PBI zonder expliciete status onhandig is voor backlog-grooming
---
## Server actions
| Actie | Locatie | Form-binding | Revalidatie |
|---|---|---|---|
| `createPbiAction` | `actions/pbis.ts` | via `useActionState` + `<form action>` (FormData) | server-side `revalidatePath` op product-backlog |
| `updatePbiAction` | `actions/pbis.ts` | idem | idem |
| ~~`deletePbiAction`~~ | **(ontbreekt)** | n.v.t. | n.v.t. |
Beide acties moeten de drielaagse demo-policy volgen (zie § Bekende gaps).
---
## Speciale gedragingen
### Form-state via `useActionState`
PbiDialog gebruikt het `useActionState` + `useFormStatus`-patroon (Server Actions / native React), niet `react-hook-form`. Dit is een toegestaan alternatief volgens de generieke spec § 2. Field-errors worden gemapt via een lokale `fieldError(field)`-helper die `result.error` als `Record<string, string[]>` interpreteert wanneer 'm geen string is.
### `key`-prop op `<form>`
Het `<form>`-element heeft `key={isEdit ? pbi!.id : 'create'}` — dit reset native form-state (defaultValues) wanneer de dialog tussen create en edit wisselt of wanneer een ander record bewerkt wordt.
### Hidden inputs voor server-binding
`priority` en `status` worden via `<input type="hidden">` doorgegeven aan de Server Action (de UI-controls zijn JS-state, niet directe form-fields).
---
## Triggers
- **Create-trigger:** `+ PBI`-knop in `PanelNavBar` van `PbiList``setPbiDialogState({ mode: 'create', ... })`
- **Edit-trigger:** edit-icoon op een PBI-rij in `PbiList``setPbiDialogState({ mode: 'edit', pbi, ... })`
---
## Bekende gaps t.o.v. generieke spec
> Deze items wijken af van `docs/patterns/dialog.md` en horen in een vervolg-PR rechtgezet (niet onderdeel van de huidige docs-introductie).
- ❌ **Geen `<DemoTooltip>`** rond submit-knop — laag 3 van de drielaagse demo-policy ontbreekt voor PBI-create/update. Dat betekent dat een demo-user de knop kan klikken; de server action blokkeert nog steeds (laag 2), maar de UX is suboptimaal.
- ❌ **Geen delete-knop / `deletePbiAction`** — alleen create + update. Of dat bewust is (PBI's worden nooit verwijderd, alleen status veranderd) of een gat, moet expliciet worden besloten en in dit profiel vastgelegd.
- ❌ **Geen dirty-close-guard** — Esc / backdrop / Cancel sluiten direct, ook met onopgeslagen wijzigingen. Generieke spec § 8.1 vereist een AlertDialog bij `isDirty`.
- ❌ **Geen Cmd/Ctrl+Enter shortcut** — alleen klik op submit-knop.
- ❌ **Geen char-counter / markdown-hint** op description — bewust weggelaten omdat PBI-descriptions kort zijn, maar verdient expliciete bevestiging.
- ⚠️ **Layout wijkt af** van de generieke responsive-tabel: `sm:max-w-md` i.p.v. de `max-w-[50vw]` / `90vw` / full-screen-progressie uit § 4.
---
## Bewust NIET in v1
Specifiek voor PbiDialog (boven op de algemene out-of-scope-lijst in `docs/patterns/dialog.md` § 13):
- ❌ Inline aanmaken van child-stories binnen de PBI-dialog (gebeurt via StoryDialog vanuit `StoryPanel`)
- ❌ Bulk-status-update over meerdere PBI's
- ❌ PBI-templates / kopiëren
---
## Referenties
- `components/backlog/pbi-dialog.tsx` — implementatie
- `actions/pbis.ts` — server actions
- `components/shared/priority-select.tsx` — gedeelde priority-control
- `components/shared/pbi-status-select.tsx` — PBI-status-select
- `lib/task-status.ts``PbiStatusApi`-mapper
- `docs/patterns/dialog.md` — generieke spec (bron-of-truth)
- `docs/architecture.md` — datamodel `Pbi`
- `docs/design/styling.md` — MD3-tokens, status- en priority-kleuren

171
docs/specs/dialogs/story.md Normal file
View file

@ -0,0 +1,171 @@
---
title: "StoryDialog Profiel"
status: active
audience: [ai-agent, contributor]
language: nl
last_updated: 2026-05-03
---
# StoryDialog Profiel
> Volgt **`docs/patterns/dialog.md`** (de generieke spec voor élke entity-dialog in Scrum4Me).
> Dit document beschrijft alleen de Story-specifieke afwijkingen en keuzes — alle gedeelde regels (layout, motion, demo-policy, foutcodes, validatie, theming, dialog-gedrag) staan in de generieke spec en worden hier niet herhaald.
> **Belangrijk:** als een regel in dit profiel botst met de generieke spec, wint de generieke spec. Documenteer hier de afwijking + reden, of pas de generieke spec aan.
---
## Velden
| Veld | Type | Mode | Validatie |
|---|---|---|---|
| `code` | `string \| null` | beide | optional, max 30 chars, mono-font, placeholder `auto` op create |
| `title` | `string` (required) | beide | trim, 1-200 chars |
| `priority` | `int` (1-4, P1 = hoogste) | beide | int 1-4, default 2 (overschrijfbaar via `defaultPriority`-prop bij create) |
| `description` | `string \| null` | beide | optional, plain textarea, placeholder `Als… wil ik… zodat…` (user-story-template) |
| `acceptance_criteria` | `string \| null` | beide | optional, plain textarea, placeholder `- Gegeven… Als… Dan…` (Gherkin-template) |
| `status` | `StoryStatus` enum | alleen edit | read-only badge in header, niet bewerkbaar in deze dialog |
`StoryStatus` enum: `OPEN | IN_SPRINT | DONE` (uppercase in DB).
### Veld-specifiek gedrag
- **Code + Titel** in één rij (`grid-cols-[6rem_1fr]`)
- **Prioriteit** via `<PrioritySelect>` (gedeelde primitive)
- **Description** als `<Textarea rows={3} resize-none>` — géén auto-grow, géén markdown-hint binnen de dialog (afwijking van generieke spec; rationale: stories zijn meestal één zin)
- **Acceptatiecriteria** idem — géén auto-grow, géén char-counter
- **Status** wordt **niet bewerkt** vanuit deze dialog. Status verandert via lijst-acties (sleep naar sprint = IN_SPRINT, taak-completion = DONE). Read-only badge in dialog-header.
---
## URL- of state-pattern
- **Gekozen:** state-based (`state: StoryDialogState | null` prop, gerendeerd binnen `StoryPanel`)
- **Reden:** StoryDialog leeft binnen `StoryPanel` met live-store-data (geselecteerde PBI bepaalt zichtbare stories); deep-linking zou parallelle data-fetch-paden vereisen.
- **State-shape:**
```ts
type StoryDialogState =
| { mode: 'create'; pbiId: string; productId: string; defaultPriority?: number }
| { mode: 'edit'; story: Story; productId: string }
```
- **Sluiten:** `onClose()` callback uit de parent — `setState(null)` in `StoryPanel`.
---
## Status-veld
- **Niet bewerkbaar in deze dialog** — alleen weergegeven als badge in de header (edit-mode)
- **Default bij create:** `OPEN` (server-default, niet expliciet gezet vanuit form)
- Status-overgangen lopen via:
- `OPEN → IN_SPRINT` — drag-and-drop naar een sprint of `sprint-id` zetten via story-actions
- `IN_SPRINT → DONE` — alle taken op `DONE` zetten triggert auto-promotion (zie story-status-logic in `actions/stories.ts`)
---
## Server actions
| Actie | Locatie | Form-binding | Revalidatie |
|---|---|---|---|
| `createStoryAction` | `actions/stories.ts` | via `useActionState` + `<form action>` (FormData) | server-side `revalidatePath` op product-backlog |
| `updateStoryAction` | `actions/stories.ts` | idem | idem |
| `deleteStoryAction` | `actions/stories.ts` | aangeroepen vanuit `useTransition` (geen form) | server-side `revalidatePath` |
| `getStoryLogsAction` | `actions/stories.ts` | aangeroepen on-mount in edit-mode | n.v.t. (read-only) |
Alle write-acties zijn drielaags afgedekt (proxy-guard + server-action-check + DemoTooltip op submit-knop).
---
## Speciale gedragingen
### Header-presentatie (afwijking van generieke spec)
In edit-mode toont de dialog-header **drie elementen** boven op de standaard titel:
1. Story-titel als dialog-title (groot)
2. Story-code als monospace-badge rechtsboven (klein)
3. Twee badges direct onder de titel: priority-badge (kleur via `PRIORITY_COLORS`) en status-badge (kleur via `STATUS_COLORS`)
Generieke spec gaat uit van een sobere header met alleen `headline-small` titel + optioneel een `created_at`-meta-string. StoryDialog wijkt hier bewust van af omdat status + priority belangrijke context zijn voor de gebruiker bij het openen van een story (vaak wisselt iemand vlot tussen meerdere stories).
### Demo-modus = read-only weergave
Wanneer `isDemo === true` én `isEdit === true`, wordt het form **vervangen** door een read-only weergave:
- `description` via gedeelde `<Markdown>`-wrapper (`react-markdown` + `remark-gfm`)
- `acceptance_criteria` als plain whitespace-pre-line tekst
In create-mode is er voor demo-users niets te tonen — de dialog wordt alsnog geopend maar de submit-knop is `disabled` met `<DemoTooltip>`.
> Dit "read-only-fallback"-patroon is uniek voor StoryDialog tot nu toe. Het zou geadopteerd kunnen worden door andere edit-dialogs zodra demo-flow-vereisten dat rechtvaardigen.
### Activity-log (StoryLog) inline
In edit-mode wordt onder het form een `<StoryLog>`-paneel getoond met de chronologische logs van deze story (commit-hashes, status-transitions, etc.). Logs worden lazy-fetched via `getStoryLogsAction(story.id)` zodra de dialog opent.
Dit is een **read-only side-panel** en valt binnen de uitzondering die de generieke spec § 13 maakt voor `<StoryLog>`-style activity-rendering.
### Delete-flow (afwijking van generieke spec)
Generieke spec § 10.4 vereist een **`AlertDialog`** voor delete-confirmatie. StoryDialog gebruikt in plaats daarvan een **inline-confirm** in dezelfde footer-rij:
```
[ Weet je het zeker? Taken worden ook verwijderd. [Verwijderen] [Annuleren] ]
```
Een `AlertDialog` zou een tweede modale laag toevoegen die in deze context onhandig voelt (de dialog zelf is al een interruptive overlay). De inline-confirm is een **bewuste afwijking** van de generieke spec.
### Form-state via `useActionState`
Net als PbiDialog gebruikt StoryDialog `useActionState` + `useFormStatus`, niet `react-hook-form`. Dit is een toegestaan alternatief volgens de generieke spec § 2.
### `key`-prop op `<form>`
Het `<form>` heeft `key={isEdit ? story!.id : 'create'}` — reset native form-state bij record-wissel of mode-switch.
---
## Triggers
- **Create-trigger:** `+ Story`-knop in `PanelNavBar` van `StoryPanel``setStoryDialogState({ mode: 'create', pbiId, productId, defaultPriority: 2 })`
- **Edit-trigger:** edit-icoon op een story-card in `StoryPanel``setStoryDialogState({ mode: 'edit', story, productId })`
- **Empty-state-trigger:** `Maak je eerste story aan`-knop in `EmptyPanel` (zelfde state als create-trigger)
---
## Bekende gaps t.o.v. generieke spec
> Deze items wijken af van `docs/patterns/dialog.md` en horen in een vervolg-PR rechtgezet (niet onderdeel van de huidige docs-introductie).
- ❌ **Geen dirty-close-guard** — Esc / backdrop / Cancel sluiten direct, ook met onopgeslagen wijzigingen. Generieke spec § 8.1 vereist een AlertDialog bij `isDirty`.
- ❌ **Geen Cmd/Ctrl+Enter shortcut** — alleen klik op submit-knop.
- ❌ **Geen char-counter / markdown-hint** op description / acceptance_criteria — bewust weggelaten, maar verdient expliciete bevestiging als design-keuze.
- ⚠️ **Inline-delete-confirm** in plaats van AlertDialog (zie § Speciale gedragingen). Bewuste afwijking; de generieke spec mag deze variant expliciet toestaan, of dit profile moet als precedent gelden voor toekomstige dialogen.
- ⚠️ **Header-layout** met meerdere badges wijkt af van de sobere header in § 4. Bewuste afwijking — context-zwaar bij story-wisselen.
- ⚠️ **Layout wijkt af** van de generieke responsive-tabel: `sm:max-w-lg` met eigen `max-h-[90vh]` + `flex flex-col` i.p.v. de exacte `max-w-[50vw]` / `90vw` / full-screen-progressie uit § 4.
---
## Bewust NIET in v1
Specifiek voor StoryDialog (boven op de algemene out-of-scope-lijst in `docs/patterns/dialog.md` § 13):
- ❌ Status bewerken vanuit de dialog (gebeurt via lijst-acties / drag-and-drop / auto-promotion)
- ❌ Inline aanmaken van child-tasks (gebeurt via TaskDialog vanuit `TaskPanel`)
- ❌ Bulk-edit over meerdere stories
- ❌ Story-templates
- ❌ Linking aan externe issues (GitHub / Linear) — staat op v1.1+ roadmap
---
## Referenties
- `components/backlog/story-dialog.tsx` — implementatie
- `actions/stories.ts` — server actions (incl. `getStoryLogsAction`)
- `components/shared/priority-select.tsx` — gedeelde priority-control
- `components/shared/story-log.tsx` — activity-log paneel
- `components/shared/demo-tooltip.tsx` — demo-policy laag 3
- `components/markdown.tsx` — gedeelde markdown-wrapper
- `lib/task-status.ts` — status-enum-mapper
- `docs/patterns/dialog.md` — generieke spec (bron-of-truth)
- `docs/architecture.md` — datamodel `Story`
- `docs/design/styling.md` — MD3-tokens, status- en priority-kleuren

135
docs/specs/dialogs/task.md Normal file
View file

@ -0,0 +1,135 @@
---
title: "TaskDialog Profiel"
status: active
audience: [ai-agent, contributor]
language: nl
last_updated: 2026-05-03
---
# TaskDialog Profiel
> Volgt **`docs/patterns/dialog.md`** (de generieke spec voor élke entity-dialog in Scrum4Me).
> Dit document beschrijft alleen de Task-specifieke afwijkingen en keuzes — alle gedeelde regels (layout, motion, demo-policy, foutcodes, validatie, theming, dialog-gedrag) staan in de generieke spec en worden hier niet herhaald.
> **Belangrijk:** als een regel in dit profiel botst met de generieke spec, wint de generieke spec. Documenteer hier de afwijking + reden, of pas de generieke spec aan.
---
## Velden
| Veld | Type | Mode | Validatie |
|---|---|---|---|
| `title` | `string` (required) | beide | trim, 1-120 chars |
| `description` | `string \| null` | beide | optional, max 2.000 chars, markdown |
| `implementation_plan` | `string \| null` | beide | optional, max 10.000 chars, markdown |
| `priority` | `int` (1-4, P1 = hoogste) | beide | int 1-4, default 3 |
| `status` | `TaskStatus` enum | alleen edit (default `TO_DO` op create, niet getoond) | enum |
| `created_at` | `Date` | alleen edit | read-only metadata in header |
`TaskStatus` enum: `TO_DO | IN_PROGRESS | REVIEW | DONE`.
### Veld-specifiek gedrag
- **Auto-grow textareas** (`description`, `implementation_plan`) via `react-textarea-autosize`. Max 6 regels (description) / 12 regels (implementation_plan), daarna `overflow-y-auto`.
- **Karakter-counter** vanaf 75% van de limiet, klein, rechtsonder, `text-muted-foreground`. Bv. `1547 / 2000`.
- **Markdown-hint** onder elk textarea: `Markdown ondersteund (lijstjes, **vet**, \`code\`)`.
- **Priority** als segmented buttons via `<PrioritySelect>` / `<PrioritySegmented>`. Default P3 (Medium).
- **Status** met gekleurde dot:
- `TO_DO` — grijs
- `IN_PROGRESS``status-in-progress` (blauw)
- `REVIEW` — paars
- `DONE``status-done` (groen)
- **`created_at` als header-metadata** in edit-mode, naast de titel: `Aangemaakt: 23 apr 2026`. Klein, `muted-foreground`, géén form-veld.
---
## URL- of state-pattern
- **Gekozen:** URL-based (`searchParams`)
- **Reden:** TaskDialog wordt geopend vanuit twee context-pagina's (sprint-detail en product-backlog) en moet deep-linkable zijn voor share/refresh-scenario's. Suspense + skeleton voor edit-mode loading is gewenst.
- **Routes:**
```
/sprint/<sprintId>?newTask=1 → create
/sprint/<sprintId>?editTask=<taskId> → edit
/products/<productId>/backlog?newTask=1 → create
/products/<productId>/backlog?editTask=<taskId> → edit
```
- **Sluiten:** `router.push(<base-route>)` zonder query-params.
- **Server-side fetch in edit-mode:** server component fetcht de taak vóór render mét `productAccessFilter(userId)`. Bestaat de taak niet of valt 'm buiten scope → toast + redirect naar de context-route.
- Optioneel: `nuqs` als de query-state-handling te omslachtig wordt — pas introduceren als losse refactor-task, niet inline.
---
## Status-veld
Verberg `status` in **create-mode** (default = `TO_DO` is genoeg). Toon alleen in edit-mode als `<Select>` met gekleurde dot per optie.
---
## Server actions
| Actie | Locatie | Context-arg | Revalidatie |
|---|---|---|---|
| `saveTask` | `app/actions/tasks.ts` | `{ sprintId?: string; productId?: string }` | `revalidatePath('/sprint/<sprintId>')` óf `revalidatePath('/products/<productId>/backlog')` afhankelijk van context |
| `deleteTask` | `app/actions/tasks.ts` | idem | idem |
Beide acties volgen de drielaagse demo-policy + auth-scoping uit `docs/patterns/dialog.md` § 67.
---
## Speciale gedragingen
### Triggers (bestaande UI vervangen)
Deze TaskDialog is de **enige** create/edit-flow voor taken in beide contexten (sprint én backlog). Bestaande inline-edit-paden in `components/sprint/task-list.tsx` en het backlog-equivalent worden vervangen, niet ernaast geplaatst.
- **Create-trigger:** filled button `+ Nieuwe taak` in tasklist-header → zet `?newTask=1` op huidige route
- **Edit-trigger:** klik op de hele rij in de tasklist (geen apart edit-icoon) → zet `?editTask=<id>` op huidige route
- **Loading edit-mode:** Suspense met minimale skeleton (3 grijze balken), `200ms`-delay zodat snelle fetches geen flicker tonen
### Markdown-rendering elders
`description` en `implementation_plan` worden buiten de dialog (taakdetail, hover-card) gerenderd via de gedeelde `<Markdown>`-wrapper (`react-markdown` + `remark-gfm`). Niet in de dialog zelf.
---
## Implementatie-volgorde (suggestie)
Hergebruik dit als checklist bij het bouwen of refactoren van TaskDialog:
1. Dependencies in `package.json` (zie `docs/patterns/dialog.md` § 2)
2. zod-schema in `lib/schemas/task.ts` — gedeeld door form en action
3. `productAccessFilter`-helper checken in `lib/auth/`
4. `saveTask` / `deleteTask` in `app/actions/tasks.ts` met auth-scoping + demo-check (laag 2)
5. `proxy.ts`-guard voor demo-write-routes (laag 1) — alleen als nog niet aanwezig
6. Eventueel ontbrekende MD3-tokens in `app/styles/theme.css` aanvullen
7. `<DemoTooltip>` rond submit/delete-knoppen (laag 3)
8. TaskDialog — create-mode eerst (minder edge cases)
9. Edit-mode toevoegen (status, delete, `created_at`-metadata)
10. URL-state via native `searchParams` op beide context-pagina's
11. Bestaande task-row trigger refactoren (klikbaar maken naar dialog)
12. Suspense + skeleton voor edit-mode + scope-check op fetch
13. Dirty-close-guard
14. Keyboard shortcuts (Cmd+Enter)
---
## Bewust NIET in v1
Specifiek voor TaskDialog (boven op de algemene out-of-scope-lijst in `docs/patterns/dialog.md` § 13):
- ❌ Sub-tasks / parent-child relaties tussen taken
- ❌ Tags / labels / categorieën op taken
- ❌ Due dates / reminders per taak
- ❌ Time tracking (uren-registratie) — wel relevant voor inspannings-monitor, eigen feature
- ❌ Sharing / collaboration per taak
- ❌ Templates voor terugkerende taken
---
## Referenties
- `docs/patterns/dialog.md` — generieke spec (bron-of-truth voor alles wat hier niet beschreven is)
- `docs/architecture.md` — datamodel `Task`
- `docs/design/styling.md` — MD3-tokens, status- en priority-kleuren
- `lib/task-status.ts` — enum-mapper DB ↔ API

1433
docs/specs/functional.md Normal file

File diff suppressed because it is too large Load diff

146
docs/specs/personas.md Normal file
View file

@ -0,0 +1,146 @@
---
title: "DevPlanner — User Personas"
status: active
audience: [maintainer]
language: nl
last_updated: 2026-05-03
---
# DevPlanner — User Personas
**Versie:** 0.1 — april 2026
**Volgt op:** Concept & Feature Brainstorm v0.3
---
## Persona-overzicht
| Naam | Leeftijd | Situatie | Primaire behoefte |
|---|---|---|---|
| Lars | 34 | Solo developer, 4 eigen SaaS-projecten naast zijn dagtaak | Overzicht houden zonder planningssysteem te worden |
| Dina | 28 | Freelance developer, werkt voor 3 klanten tegelijk | Klantprojecten gescheiden houden en voortgang aantonen |
| Remi | 41 | Lead van een klein team (3 personen), geen dedicated PM | Scrum licht toepassen zonder Jira-overhead |
---
## Lars
**Leeftijd:** 34 — **Situatie:** Solo developer met 4 actieve side projects naast een fulltime baan
### Achtergrond
Lars werkt overdag als backend developer bij een middelgroot bedrijf en bouwt 's avonds en in het weekend aan zijn eigen projecten: een SaaS-tool voor factuurverwerking, een open-source CLI-utility, een persoonlijk dashboard en een experimenteel AI-project. Hij werkt alleen, heeft geen teamleden en geen deadlines die van buitenaf worden opgelegd. Alles loopt via zijn hoofd of verspreide markdown-bestanden in de repositories zelf.
### Een typische slechte avond
Hij opent zijn laptop na het werk en weet niet meer waar hij gebleven was. In welk project zat hij? Wat stond er open? Hij herinnert zich dat er vorige week een bug was gerapporteerd in de factuur-tool, maar hij weet niet meer of hij die heeft gefixt of alleen heeft opgeschreven. Hij besteedt twintig minuten aan het doorzoeken van commit-logs en README's voordat hij kan beginnen. Als Claude Code dan eindelijk een taak oppakt, is de context al kwijt.
### Hoe hij het nu oplost
Elke repository heeft een `TODO.md` die hij bijhoudt zolang een project actief is. Als hij een week niet heeft gekeken, is het bestand achterhaald. Prioritering is impliciet — hij werkt aan wat op dat moment het interessantst voelt, niet aan wat het belangrijkst is. Jira heeft hij één keer geprobeerd voor zijn side projects: duurde twee uur om in te richten en stopte na drie weken.
### Doelen met deze app
- Elke avond binnen één minuut weten welke taak het meest urgent is per project
- Claude Code laten oppakken wat open staat zonder zelf de context te hoeven herstellen
- Achteraf kunnen zien wat er gedaan is en hoe (implementatieplan, commit, testresultaat)
- Op klantbezoek of bij familie zijn side projects kunnen demonstreren op een geleende laptop, zonder dat hij zijn wachtwoord op een vreemd toetsenbord hoeft te typen — door zijn telefoon (waar hij al ingelogd is) een QR-code op het scherm te laten scannen
### Frustraties om te vermijden
- Verplichte velden en formulieren die langer duren dan de taak zelf
- Systemen die vragen om updates die hij toch niet bijhoudt (daily standup-achtige inputs)
- Alles wat aanvoelt als "project management voor grote teams" — hij is de enige gebruiker
### Relatie met technologie
Power user. Leeft in de terminal, gebruikt Claude Code dagelijks, bouwt zijn eigen tooling als iets hem niet bevalt. Wil een app die hij ook via een API kan aansturen.
---
## Dina
**Leeftijd:** 28 — **Situatie:** Freelance full-stack developer, werkt parallel voor drie klanten
### Achtergrond
Dina werkt als zelfstandige en heeft op dit moment drie lopende klantprojecten: een e-commerceplatform in onderhoudsfase, een nieuw admin-dashboard voor een logistiek bedrijf en een kleine MVP voor een startup. Elk project heeft zijn eigen ritme, zijn eigen repo en zijn eigen verwachtingen. Ze werkt alleen maar heeft per klant een contactpersoon die wil weten wat er gedaan is.
### Een typische slechte dag
Ze heeft drie context-switches voor de lunch. Het logistiek-dashboard heeft een bugmelding binnengekomen, de startup wil een update, en de e-commerce klant heeft een vraag over iets wat twee Sprints geleden geleverd is. Ze heeft geen centraal overzicht — ze zoekt in Slack-threads, e-mails en commit-logs om te reconstrueren wat er wanneer gedaan is. Aan het eind van de dag heeft ze productief werk gedaan maar kan ze niet meer zeggen wat precies.
### Hoe ze het nu oplost
Elk klantproject heeft een Notion-pagina met een ruwe takenlijst. Notion is flexible maar heeft geen structuur die Scrum-begrippen begrijpt. Ze heeft geprobeerd Trello te gebruiken maar het mist de hiërarchie die ze nodig heeft (project → feature → taak). Ze voelt dat ze iets robuusters nodig heeft nu het derde project erbij is gekomen.
### Doelen met deze app
- Per klant een gestructureerde Product Backlog bijhouden zonder overlap
- Snel kunnen antwoorden op "wat heb je de afgelopen week gedaan?" met concrete verwijzingen naar commits en stories
- Claude Code gebruiken voor routinetaken zodat zij zich kan richten op complexere beslissingen
### Frustraties om te vermijden
- Systemen waarbij klantdata vermengd raakt (ze wil strikte projectscheiding)
- Dashboards die statistieken tonen die ze niet nodig heeft (velocity, burndown)
- Alles wat ze verplicht dagelijks bij te houden — ze heeft soms een dag vrij of ziek
### Relatie met technologie
Comfortabel maar niet fanatiek. Gebruikt VS Code, GitHub en een handvol SaaS-tools. Geen terminal-purist zoals Lars — ze gebruikt liever een goede UI dan een CLI als beide beschikbaar zijn.
---
## Remi
**Leeftijd:** 41 — **Situatie:** Lead developer van een team van drie, zonder dedicated projectmanager
### Achtergrond
Remi werkt bij een klein softwarebedrijf met in totaal acht mensen. Zijn team bestaat uit hemzelf en twee juniordevelopers. Ze bouwen interne tools voor twee afdelingen en onderhouden drie legacy-systemen. Er is geen Scrum Master, geen Product Owner en geen projectmanager — Remi doet dat er allemaal bij. Hij heeft Scrum gelezen en wil het toepassen, maar Jira is te zwaar en te duur voor hun schaal. Ze werken nu met een gedeeld Excel-bestand dat niemand consequent bijhoudt.
### Een typische slechte week
Ze beginnen maandagochtend zonder duidelijk Sprint-doel. Iedereen werkt aan wat binnenkomt. Woensdag vraagt zijn manager naar de status van het rapportage-dashboard. Remi weet dat er aan gewerkt is maar niet hoe ver het is. Hij moet twee junioren ondervragen en drie feature-branches bekijken om een antwoord te kunnen geven. Vrijdagmiddag hebben ze een "retrospective" die eigenlijk een statusmeeting is.
### Hoe hij het nu oplost
Excel voor taakregistratie, Teams-kanalen per project voor communicatie, GitHub Issues voor bugs. Drie systemen die niet met elkaar praten. Hij heeft Linear geprobeerd maar de leercurve was te groot voor de junioren. Hij zoekt iets dat hij in een middag kan inrichten en dat zijn teamleden zonder training kunnen gebruiken.
### Doelen met deze app
- Elke Sprint starten met een duidelijk Sprint Goal dat iedereen ziet
- Wekelijks in één scherm kunnen zien wat open staat, wat in progress is en wat klaar is
- In de toekomst rollen kunnen toewijzen aan teamleden zodat de Product Owner (hijzelf) en de Developers (de junioren) gescheiden rechten hebben
### Frustraties om te vermijden
- Alles wat de junioren extra werk geeft zonder directe waarde voor hen
- Rapportages en grafieken die hij niet nodig heeft
- Systemen waarbij hij afhankelijk is van een externe SaaS die duur wordt bij groei
### Relatie met technologie
Ervaren developer, maar kiest bewust voor eenvoud. Wil een tool die hij zelf kan hosten als dat goedkoper uitpakt. Waardeert open source maar heeft geen tijd om een tool te onderhouden die hij zelf gebouwd heeft.
---
## Persona-spanningen
| Spanning | Lars wil | Dina wil | Remi wil | Oplossing |
|---|---|---|---|---|
| API vs. UI | Alles via API en Claude Code | Goede UI, API is bonus | UI eerst, team moet het kunnen gebruiken | UI is primair; API is volwaardige burgerklasse, niet bijzaak |
| Eén vs. meerdere gebruikers | Altijd solo | Solo maar met klantcontext | Team van 3 met rolscheiding | v1 is solo; rolbeheer is v1-fundament voor v2-teamuitbreiding |
| Scrum-striktheid | Licht, pragmatisch | Structuur helpt maar geen dogma | Wil Scrum leren toepassen | Scrum-terminologie consistent; events zijn optioneel, niet verplicht |
| Zichtbaarheid voortgang | Eigen overzicht, geen rapportages | Klantverantwoording via commits | Teamstatus in één scherm | Activiteitenlog per story volstaat voor alle drie; aparte rapportagelaag is v2 |
---
## Primaire persona
**Lars** is de primaire designtarget voor v1.
Rationale: Lars vertegenwoordigt de meest veeleisende gebruiker in termen van API-integratie en Claude Code-koppeling — de kern-differentiator van DevPlanner. Als de app goed werkt voor Lars (solo, API-gedreven, meerdere projecten, minimale overhead), werkt het ook voor Dina (zelfde gebruik, lichtere techvoorkeur). Remi's behoeften — teamgebruik en rolscheiding — zijn bewust naar v2 verschoven; het fundament (rolmodel in de datastructuur) wordt in v1 al gelegd.
Een feature die Lars zou doen stoppen — verplichte velden, trage UI, geen API — wordt niet gebouwd, ook niet als Remi er baat bij zou hebben.

View file

@ -1,670 +0,0 @@
# Scrum4Me — Styling & Design System
**Versie:** 0.1 — april 2026
**Onderdeel van:** CLAUDE.md context-set
---
## Overzicht
Scrum4Me gebruikt **Material Design 3 (MD3)** als kleurfilosofie, geïmplementeerd via CSS custom properties in `theme.css` en direct bruikbaar als Tailwind utility classes. **shadcn/ui** levert alle UI-primitieven (Button, Dialog, Sheet, Badge, etc.) en is volledig compatibel met het MD3-kleurensysteem via de legacy-token-mapping.
Lees dit document voordat je een component schrijft. Gebruik **nooit** willekeurige Tailwind-kleuren zoals `bg-blue-500` of `bg-green-600` — gebruik altijd de semantische tokens uit dit systeem.
---
## Setup
### 1. theme.css plaatsen
Kopieer het meegeleverde `theme.css` bestand naar:
```
app/globals.css ← importeer theme.css hier, of plak de inhoud direct
```
Of als apart bestand:
```
styles/theme.css
```
Importeer in `app/globals.css`:
```css
@import './styles/theme.css';
```
### 2. shadcn/ui initialiseren
```bash
npx shadcn@latest init
```
Kies bij de setup:
- Style: **Default**
- Base color: **Slate** (wordt overschreven door theme.css)
- CSS variables: **Yes**
De `theme.css` overschrijft alle shadcn default-kleuren via CSS custom properties. Geen extra configuratie nodig.
### 3. Tailwind configuratie
`theme.css` registreert alle tokens via `@theme inline` — ze zijn direct beschikbaar als Tailwind utility classes:
```tsx
// Werkt direct:
className="bg-primary text-primary-foreground"
className="bg-surface-container-low"
className="bg-status-done"
className="bg-priority-critical"
```
### 4. Dark mode
Dark mode werkt via de `.dark` class op `<html>`:
```tsx
// components/theme-toggle.tsx
'use client'
import { useState, useEffect } from 'react'
export function ThemeToggle() {
const [isDark, setIsDark] = useState(false)
useEffect(() => {
const stored = localStorage.getItem('theme')
if (stored === 'dark') {
document.documentElement.classList.add('dark')
setIsDark(true)
}
}, [])
const toggle = () => {
document.documentElement.classList.toggle('dark')
const next = !isDark
setIsDark(next)
localStorage.setItem('theme', next ? 'dark' : 'light')
}
return (
<button onClick={toggle} className="text-muted-foreground hover:text-foreground">
{isDark ? '☀️' : '🌙'}
</button>
)
}
```
---
## Kleurfilosofie
Drie hoofdrollen, elk met een semantische betekenis voor een Scrum-planner:
| Rol | Kleur | Betekenis | Gebruik in Scrum4Me |
|---|---|---|---|
| **Primary** | Blauw `#0061a4` | Productiviteit, vertrouwen | Primaire knoppen, actieve navigatie, Sprint Goal |
| **Secondary** | Paars `#5b5e71` | Planning, organisatie | Secundaire acties, filters, toolbar-items |
| **Tertiary** | Teal `#006874` | Voortgang, data | Voortgangsindicatoren, story-tellers, metrics |
Diepte wordt gecreëerd via **tonal elevation** (lichtere/donkerdere oppervlakken), niet via schaduwen.
---
## Surface Elevation System
Gebruik deze hiërarchie consequent — nooit `shadow-lg` voor diepte:
```
HOOGSTE ELEVATIE (voorgrond)
surface-container-lowest → dialogs, modals, popovers
surface-container-low → kaarten, panelen
surface-container → standaard container
surface-container-high → geneste containers
surface-container-highest → achtergrondcontainers
LAAGSTE ELEVATIE (achtergrond)
background → app-achtergrond
```
### In Scrum4Me specifiek
| Element | Surface token |
|---|---|
| App achtergrond | `bg-background` |
| Navigatiebalk | `bg-surface-container-low` |
| Gesplitst scherm (elk paneel) | `bg-surface-container-low` |
| PBI-rij | `bg-surface-container` |
| Geselecteerde PBI-rij | `bg-primary-container` |
| Story-blok | `bg-surface-container-low border border-border` |
| Story-blok (geselecteerd) | `bg-primary-container border border-primary` |
| Taakregel | `bg-surface-container` |
| Dialogs / modals | `bg-surface-container-lowest` |
| Slide-over (story detail) | `bg-surface-container-lowest` |
| Todo-item | `bg-surface-container` |
| Navigatiebar per paneel | `bg-surface-container-highest` |
---
## Statuskleur mapping
### Story- en taakstatus
Gebruik **altijd** icoon + tekst naast kleur (toegankelijkheid):
```tsx
// Status badge component
const statusConfig = {
OPEN: {
label: 'Open',
className: 'bg-status-todo text-white',
},
IN_SPRINT: {
label: 'In Sprint',
className: 'bg-status-in-progress text-white',
},
DONE: {
label: 'Done',
className: 'bg-status-done text-white',
},
}
// Taakstatus
const taskStatusConfig = {
TO_DO: {
label: 'To Do',
className: 'bg-status-todo text-white',
},
IN_PROGRESS: {
label: 'In Progress',
className: 'bg-status-in-progress text-white',
},
DONE: {
label: 'Done',
className: 'bg-status-done text-white',
},
}
```
### Prioriteitskleur mapping
```tsx
const priorityConfig = {
1: {
label: 'Kritiek',
className: 'bg-priority-critical text-white',
borderClassName: 'border-l-4 border-priority-critical',
},
2: {
label: 'Hoog',
className: 'bg-priority-high text-white',
borderClassName: 'border-l-4 border-priority-high',
},
3: {
label: 'Middel',
className: 'bg-priority-medium text-white',
borderClassName: 'border-l-4 border-priority-medium',
},
4: {
label: 'Laag',
className: 'bg-priority-low text-white',
borderClassName: 'border-l-4 border-priority-low',
},
}
```
### Story-activiteitenlog
```tsx
const logTypeConfig = {
IMPLEMENTATION_PLAN: {
label: 'Implementatieplan',
className: 'bg-info-container text-info-container-foreground border-l-4 border-info',
},
TEST_RESULT: {
PASSED: {
label: 'Tests geslaagd',
className: 'bg-success-container text-success-container-foreground border-l-4 border-success',
},
FAILED: {
label: 'Tests mislukt',
className: 'bg-error-container text-error-container-foreground border-l-4 border-error',
},
},
COMMIT: {
label: 'Commit',
className: 'bg-secondary-container text-secondary-container-foreground border-l-4 border-secondary',
},
}
```
---
## shadcn/ui componenten — gebruik in Scrum4Me
Alle shadcn-componenten gebruiken automatisch het MD3-kleurensysteem. Hieronder de aanbevolen varianten per context.
### Button
```tsx
import { Button } from '@/components/ui/button'
// Primaire actie (Sprint starten, PBI aanmaken, Opslaan)
<Button>Sprint starten</Button>
// Secundaire actie (Annuleren, Filters, Exporteren)
<Button variant="secondary">Annuleren</Button>
// Destructieve actie (Verwijderen, Archiveren)
<Button variant="destructive">Verwijderen</Button>
// Ghost (icon-knoppen in navigatiebar)
<Button variant="ghost" size="icon">
<PlusIcon className="h-4 w-4" />
</Button>
// Outline (minder urgente acties)
<Button variant="outline">Details bekijken</Button>
```
### Badge (status en prioriteit)
```tsx
import { Badge } from '@/components/ui/badge'
// Gebruik custom className voor MD3-kleuren
// shadcn Badge variant="secondary" is ook bruikbaar voor neutrale badges
<Badge className="bg-status-done text-white">Done</Badge>
<Badge className="bg-priority-critical text-white">Kritiek</Badge>
<Badge className="bg-status-in-progress text-white">In Sprint</Badge>
// Neutrale info badge (bijv. "3 taken")
<Badge variant="secondary">3 taken</Badge>
```
**PBI-status (READY / BLOCKED / DONE):** hergebruikt bestaande tokens —
`status-todo` voor READY, `status-blocked` voor BLOCKED, `status-done` voor
DONE. Centraal gedefinieerd in `components/shared/pbi-status-select.tsx`
(`PBI_STATUS_LABELS`, `PBI_STATUS_COLORS`); importeer die in plaats van
kleuren ad-hoc te kopiëren.
### Dialog (bevestigingsdialogen)
```tsx
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from '@/components/ui/alert-dialog'
// Standaard bevestigingsdialoog voor verwijderacties
<AlertDialogContent className="bg-surface-container-lowest">
<AlertDialogHeader>
<AlertDialogTitle>PBI verwijderen?</AlertDialogTitle>
<AlertDialogDescription>
Dit verwijdert ook alle gekoppelde stories en taken. Deze actie is niet ongedaan te maken.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Annuleren</AlertDialogCancel>
<AlertDialogAction className="bg-destructive text-destructive-foreground">
Verwijderen
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
```
### Sheet (story detail slide-over)
```tsx
import { Sheet, SheetContent, SheetHeader, SheetTitle } from '@/components/ui/sheet'
<Sheet>
<SheetContent
side="right"
className="w-[480px] bg-surface-container-lowest border-l border-border"
>
<SheetHeader>
<SheetTitle className="text-foreground">{story.title}</SheetTitle>
</SheetHeader>
{/* story detail inhoud */}
</SheetContent>
</Sheet>
```
### Input en Textarea
```tsx
import { Input } from '@/components/ui/input'
import { Textarea } from '@/components/ui/textarea'
// shadcn Input gebruikt --input-background automatisch uit theme.css
<Input
placeholder="PBI titel"
className="bg-input-background border-border focus:ring-primary"
/>
<Textarea
placeholder="Omschrijving (optioneel)"
className="bg-input-background border-border focus:ring-primary resize-none"
/>
```
### Select (prioriteit dropdown)
```tsx
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
<Select>
<SelectTrigger className="bg-input-background border-border">
<SelectValue placeholder="Prioriteit" />
</SelectTrigger>
<SelectContent className="bg-surface-container-lowest border-border">
<SelectItem value="1">
<span className="flex items-center gap-2">
<span className="w-2 h-2 rounded-full bg-priority-critical" />
Kritiek
</span>
</SelectItem>
<SelectItem value="2">
<span className="flex items-center gap-2">
<span className="w-2 h-2 rounded-full bg-priority-high" />
Hoog
</span>
</SelectItem>
<SelectItem value="3">
<span className="flex items-center gap-2">
<span className="w-2 h-2 rounded-full bg-priority-medium" />
Middel
</span>
</SelectItem>
<SelectItem value="4">
<span className="flex items-center gap-2">
<span className="w-2 h-2 rounded-full bg-priority-low" />
Laag
</span>
</SelectItem>
</SelectContent>
</Select>
```
### Skeleton (loading states)
```tsx
import { Skeleton } from '@/components/ui/skeleton'
// PBI lijst skeleton
function PbiListSkeleton() {
return (
<div className="space-y-2 p-4">
{Array.from({ length: 5 }).map((_, i) => (
<Skeleton key={i} className="h-12 w-full bg-surface-container-high" />
))}
</div>
)
}
// Story blokken skeleton
function StoryGridSkeleton() {
return (
<div className="flex flex-wrap gap-3 p-4">
{Array.from({ length: 6 }).map((_, i) => (
<Skeleton key={i} className="h-24 w-[10%] min-w-[100px] bg-surface-container-high" />
))}
</div>
)
}
```
### Tooltip (demo-gebruiker write-protection)
```tsx
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
// Gebruik voor alle uitgeschakelde knoppen bij demo-gebruiker
function DemoProtectedButton({ children, isDemo, onClick, ...props }) {
if (!isDemo) {
return <Button onClick={onClick} {...props}>{children}</Button>
}
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<span>
<Button disabled {...props}>{children}</Button>
</span>
</TooltipTrigger>
<TooltipContent className="bg-surface-container-lowest border-border">
<p className="text-sm text-muted-foreground">Niet beschikbaar in demo-modus</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)
}
```
---
## Scrum4Me component patronen
### PBI-rij
```tsx
// Geselecteerd PBI heeft primary-container achtergrond
<div
className={cn(
"flex items-center gap-3 px-4 py-3 rounded-lg cursor-pointer transition-colors border-l-4",
priorityConfig[pbi.priority].borderClassName,
isSelected
? "bg-primary-container text-primary-container-foreground"
: "bg-surface-container hover:bg-surface-container-high"
)}
onClick={() => setSelectedPbi(pbi.id)}
>
<span className="flex-1 text-sm font-medium truncate">{pbi.title}</span>
<Badge className={priorityConfig[pbi.priority].className}>
{priorityConfig[pbi.priority].label}
</Badge>
</div>
```
### Story-blok
```tsx
// ~10% schermbreedte, compacte weergave
<div
className={cn(
"relative flex flex-col gap-1 p-3 rounded-lg border cursor-pointer",
"min-w-[100px] w-[10%] h-24 text-xs",
"transition-colors hover:border-primary",
"bg-surface-container-low border-border"
)}
>
<span className="font-medium leading-tight line-clamp-2">{story.title}</span>
<div className="mt-auto flex items-center justify-between">
<span className={cn("px-1.5 py-0.5 rounded text-[10px] font-medium", statusConfig[story.status].className)}>
{statusConfig[story.status].label}
</span>
<span className={cn("w-2 h-2 rounded-full", `bg-priority-${priorityLabel}`)}>
</span>
</div>
</div>
```
### Prioriteitsgroep scheidingslijn
```tsx
// Visuele scheiding per prioriteitsgroep in PBI-lijst en story-grid
<div className="mt-4 mb-2">
<div className="flex items-center gap-2">
<span className={cn(
"text-xs font-semibold uppercase tracking-wider",
priority === 1 && "text-priority-critical",
priority === 2 && "text-priority-high",
priority === 3 && "text-priority-medium",
priority === 4 && "text-priority-low",
)}>
{priorityConfig[priority].label}
</span>
<div className={cn(
"flex-1 h-px",
priority === 1 && "bg-priority-critical/30",
priority === 2 && "bg-priority-high/30",
priority === 3 && "bg-priority-medium/30",
priority === 4 && "bg-priority-low/30",
)} />
<span className="text-xs text-muted-foreground">{count}</span>
</div>
</div>
```
### Voortgangsindicator (story → taken)
```tsx
// Gebruikt tertiary kleur voor voortgang
function StoryProgress({ done, total }: { done: number; total: number }) {
const pct = total === 0 ? 0 : Math.round((done / total) * 100)
return (
<div className="flex items-center gap-2 text-xs">
<div className="flex-1 h-1.5 bg-surface-container-highest rounded-full overflow-hidden">
<div
className="h-full bg-tertiary rounded-full transition-all"
style={{ width: `${pct}%` }}
/>
</div>
<span className="text-muted-foreground tabular-nums">
{done}/{total}
</span>
</div>
)
}
```
### Activiteitenlog entry
```tsx
function LogEntry({ entry }: { entry: StoryLog }) {
const config = entry.type === 'TEST_RESULT'
? logTypeConfig.TEST_RESULT[entry.status ?? 'PASSED']
: logTypeConfig[entry.type]
return (
<div className={cn("rounded-lg p-3 text-sm", config.className)}>
<div className="flex items-center justify-between mb-1">
<span className="font-medium text-xs uppercase tracking-wide">
{config.label}
</span>
<span className="text-xs opacity-70">
{formatDate(entry.created_at)}
</span>
</div>
<p className="text-sm leading-relaxed whitespace-pre-wrap">
{entry.content}
</p>
{entry.commit_hash && (
<a
href={`${repoUrl}/commit/${entry.commit_hash}`}
target="_blank"
rel="noopener noreferrer"
className="mt-1 inline-flex items-center gap-1 text-xs font-mono opacity-80 hover:opacity-100 underline"
>
{entry.commit_hash.slice(0, 7)} — {entry.commit_message}
</a>
)}
</div>
)
}
```
### Sprint Goal banner
```tsx
// Prominent bovenaan Sprint-schermen
<div className="bg-primary-container text-primary-container-foreground rounded-lg px-4 py-3 mb-4">
<span className="text-xs font-semibold uppercase tracking-wider opacity-70">
Sprint Goal
</span>
<p className="mt-0.5 font-medium">{sprint.sprint_goal}</p>
</div>
```
### Toast notificaties (Sonner)
```tsx
import { toast } from 'sonner'
// Success (aanmaken, opslaan)
toast.success('PBI aangemaakt')
toast.success('Story toegevoegd aan Sprint')
// Error (mislukte Server Action)
toast.error('Opslaan mislukt. Probeer opnieuw.')
// Info (neutrale melding)
toast.info('Sprint afgerond')
// Geen toast bij drag-and-drop (te frequent)
```
---
## Regels (nooit overtreden)
```
❌ bg-blue-500, bg-green-600, bg-red-400 → gebruik semantische tokens
❌ shadow-lg, shadow-md → gebruik surface elevation
❌ opacity-50 op een primary button → gebruik -container variant
❌ Kleur alleen voor status (geen tekst) → altijd tekst + kleur
❌ Hardcoded hex-waarden in className → altijd via CSS token
❌ bg-white of bg-black → bg-background of bg-foreground
✅ bg-primary text-primary-foreground
✅ bg-surface-container-low
✅ bg-status-done + tekst "Done"
✅ bg-error-container text-error-container-foreground
✅ border-l-4 border-priority-critical
```
---
## Bestandslocaties
```
styles/
theme.css ← bronbestand, niet aanpassen
app/
globals.css ← importeert theme.css
components/
ui/ ← shadcn/ui (auto-gegenereerd, niet aanpassen)
shared/
status-badge.tsx ← herbruikbare status badge
priority-badge.tsx ← herbruikbare prioriteit badge
demo-button.tsx ← Button met demo-protection tooltip
story-log.tsx ← activiteitenlog entries
story-progress.tsx ← voortgangsindicator
priority-group.tsx ← prioriteitsgroep scheidingslijn
```
---
## Toegankelijkheid
- Alle kleurcombinaties voldoen aan **WCAG AA** (contrast ratio ≥ 4.5:1 voor normale tekst)
- Gebruik **altijd** tekst + kleur voor statusindicatoren, nooit kleur alleen
- Alle interactieve elementen hebben een zichtbare `focus:ring-primary`
- Dark mode is volledig ondersteund via de `.dark` class
---
*Bijlage bij CLAUDE.md — lees beide voor je begint met bouwen.*

View file

@ -1,3 +1,11 @@
---
title: "Scrum4Me — API Test Plan"
status: active
audience: [maintainer, contributor]
language: nl
last_updated: 2026-05-03
---
# Scrum4Me — API Test Plan # Scrum4Me — API Test Plan
**Versie:** 1.0 **Versie:** 1.0

View file

@ -15,7 +15,10 @@
"db:erd": "prisma generate", "db:erd": "prisma generate",
"db:erd:watch": "chokidar \"prisma/schema.prisma\" -c \"npm run db:erd\"", "db:erd:watch": "chokidar \"prisma/schema.prisma\" -c \"npm run db:erd\"",
"db:insert-milestone": "tsx scripts/insert-milestone.ts", "db:insert-milestone": "tsx scripts/insert-milestone.ts",
"seed": "prisma db seed" "seed": "prisma db seed",
"docs:index": "node scripts/generate-docs-index.mjs",
"docs:check-links": "node scripts/check-doc-links.mjs",
"docs": "npm run docs:index && npm run docs:check-links"
}, },
"dependencies": { "dependencies": {
"@base-ui/react": "^1.4.1", "@base-ui/react": "^1.4.1",

115
scripts/check-doc-links.mjs Normal file
View file

@ -0,0 +1,115 @@
#!/usr/bin/env node
/**
* Doc-link checker: walks docs/ (and README.md, CLAUDE.md, AGENTS.md),
* extracts relative markdown links, and verifies that every target file
* (and optional #anchor) actually exists.
*
* Exits 0 if all links are valid, 1 if any are broken.
*/
import { readFileSync, existsSync, readdirSync, statSync } from 'fs';
import { resolve, dirname, extname } from 'path';
import { fileURLToPath } from 'url';
const __dirname = dirname(fileURLToPath(import.meta.url));
const ROOT = resolve(__dirname, '..');
// Collect all .md files under a directory recursively
function collectMd(dir) {
const results = [];
for (const entry of readdirSync(dir)) {
const full = resolve(dir, entry);
const stat = statSync(full);
if (stat.isDirectory()) {
results.push(...collectMd(full));
} else if (extname(entry) === '.md') {
results.push(full);
}
}
return results;
}
// Convert a heading text to a GitHub-style anchor slug
function toSlug(text) {
return text
.toLowerCase()
.replace(/[^\w\s-]/g, '')
.trim()
.replace(/\s+/g, '-');
}
// Extract all heading slugs from a markdown file
function headingSlugs(filePath) {
const content = readFileSync(filePath, 'utf8');
const slugs = new Set();
for (const line of content.split('\n')) {
const m = line.match(/^#{1,6}\s+(.+)/);
if (m) slugs.add(toSlug(m[1]));
}
return slugs;
}
const LINK_RE = /\[(?:[^\]]*)\]\(([^)]+)\)/g;
function checkFile(filePath) {
const content = readFileSync(filePath, 'utf8');
const failures = [];
let m;
while ((m = LINK_RE.exec(content)) !== null) {
const raw = m[1];
// Skip external links and anchors-only
if (/^https?:\/\//.test(raw) || /^mailto:/.test(raw) || raw.startsWith('#')) continue;
const [pathPart, anchor] = raw.split('#');
const target = resolve(dirname(filePath), pathPart);
if (!existsSync(target)) {
failures.push({ file: filePath, link: raw, reason: 'file not found' });
continue;
}
if (anchor) {
const slugs = headingSlugs(target);
if (!slugs.has(anchor)) {
failures.push({ file: filePath, link: raw, reason: `anchor #${anchor} not found` });
}
}
}
return failures;
}
const roots = [
resolve(ROOT, 'docs'),
resolve(ROOT, 'README.md'),
resolve(ROOT, 'CLAUDE.md'),
resolve(ROOT, 'AGENTS.md'),
];
const files = [];
for (const r of roots) {
if (!existsSync(r)) continue;
const stat = statSync(r);
if (stat.isDirectory()) {
files.push(...collectMd(r));
} else {
files.push(r);
}
}
const allFailures = [];
for (const f of files) {
allFailures.push(...checkFile(f));
}
if (allFailures.length === 0) {
console.log(`✓ All doc links valid (${files.length} files checked)`);
process.exit(0);
} else {
console.error(`\n✗ Broken doc links (${allFailures.length}):\n`);
for (const { file, link, reason } of allFailures) {
const rel = file.replace(ROOT + '/', '');
console.error(` ${rel}\n${link} (${reason})`);
}
console.error('');
process.exit(1);
}

View file

@ -0,0 +1,277 @@
#!/usr/bin/env node
// Generate docs/INDEX.md from the front-matter and headings of every
// .md file under docs/. Pure Node 20 — no external dependencies.
//
// Usage: `npm run docs:index` (or `node scripts/generate-docs-index.mjs`).
//
// Idempotent: rewriting INDEX.md from the same inputs produces identical
// output (apart from the generation date in the header), so the script
// is safe to run repeatedly and in pre-commit hooks.
import { readdir, readFile, writeFile } from 'node:fs/promises';
import { join, relative, basename, sep } from 'node:path';
import { fileURLToPath } from 'node:url';
const SCRIPT_DIR = fileURLToPath(new URL('.', import.meta.url));
const REPO_ROOT = join(SCRIPT_DIR, '..');
const DOCS_DIR = join(REPO_ROOT, 'docs');
const INDEX_PATH = join(DOCS_DIR, 'INDEX.md');
// Paths (relative to repo root, forward-slashed) that the index should
// skip entirely. Templates and archived plans aren't useful in the live
// roster; sidecar files prefixed with `_` are personal Obsidian scratch.
const EXCLUDE_PATTERNS = [
/^docs\/adr\/templates\//,
/^docs\/adr\/README\.md$/,
/\/_[^/]+\.md$/,
/^docs\/INDEX\.md$/,
];
async function walk(dir) {
const entries = await readdir(dir, { withFileTypes: true });
const files = [];
for (const e of entries) {
const full = join(dir, e.name);
if (e.isDirectory()) {
files.push(...(await walk(full)));
} else if (e.isFile() && e.name.endsWith('.md')) {
files.push(full);
}
}
return files;
}
// Minimal YAML front-matter parser. Front-matter in this repo is restricted
// to flat `key: value` pairs, so a hand-rolled parser is enough — and
// keeps the script dependency-free.
function parseFrontMatter(content) {
if (!content.startsWith('---\n')) return { data: {}, body: content };
const end = content.indexOf('\n---\n', 4);
if (end === -1) return { data: {}, body: content };
const block = content.slice(4, end);
const data = {};
for (const raw of block.split('\n')) {
const line = raw.trim();
if (!line || line.startsWith('#')) continue;
const m = line.match(/^([A-Za-z][\w-]*)\s*:\s*(.*?)\s*$/);
if (!m) continue;
let val = m[2];
if (
(val.startsWith('"') && val.endsWith('"')) ||
(val.startsWith("'") && val.endsWith("'"))
) {
val = val.slice(1, -1);
}
data[m[1]] = val;
}
return { data, body: content.slice(end + 5) };
}
function extractFirstH1(text) {
const m = text.match(/^#\s+(.+?)\s*$/m);
return m ? m[1] : null;
}
// For Nygard-style ADRs the status lives under a `## Status` heading
// instead of YAML front-matter. Pull the first non-empty line after the
// heading so the index can still show it.
function extractStatusSection(text) {
const m = text.match(/^##\s+Status\s*\n+([^\n#].*?)(?:\n|$)/m);
return m ? m[1].trim() : null;
}
function isExcluded(relPath) {
return EXCLUDE_PATTERNS.some((rx) => rx.test(relPath));
}
// Map a path under docs/ to one of the four named sections, or "Other".
// Folder-based first; root-level docs fall back to a name-prefix rule
// so legacy `scrum4me-*.md` files still surface under Specs until the
// docs-restructure migrates them into `docs/specs/`.
function categorize(relPath) {
const parts = relPath.split('/');
if (parts[0] !== 'docs') return 'Other';
if (parts.length === 2) {
return /^scrum4me-/.test(parts[1]) ? 'Specs' : 'Other';
}
const sub = parts[1];
if (sub === 'adr') return 'ADRs';
if (sub === 'specs') return 'Specs';
if (sub === 'plans') return 'Plans';
if (sub === 'patterns') return 'Patterns';
return 'Other';
}
function adrNumber(filename) {
const m = filename.match(/^(\d{4})-/);
return m ? parseInt(m[1], 10) : null;
}
function escapePipe(s) {
return String(s).replace(/\|/g, '\\|');
}
async function main() {
const files = await walk(DOCS_DIR);
const docs = [];
for (const full of files) {
const rel = relative(REPO_ROOT, full).split(sep).join('/');
if (isExcluded(rel)) continue;
const content = await readFile(full, 'utf8');
const { data, body } = parseFrontMatter(content);
const title =
data.title || extractFirstH1(body) || basename(full, '.md');
const status = data.status || extractStatusSection(body) || '';
const date = data.date || data.last_updated || '';
const linkPath = './' + rel.replace(/^docs\//, '');
const category = categorize(rel);
docs.push({
rel,
title,
status,
date,
linkPath,
category,
basename: basename(full),
});
}
const groups = { ADRs: [], Specs: [], Plans: [], Patterns: [], Other: [] };
for (const d of docs) {
if (groups[d.category]) groups[d.category].push(d);
}
groups.ADRs.sort((a, b) => {
const na = adrNumber(a.basename) ?? 9999;
const nb = adrNumber(b.basename) ?? 9999;
if (na !== nb) return na - nb;
return a.basename.localeCompare(b.basename);
});
for (const k of ['Specs', 'Plans', 'Patterns', 'Other']) {
groups[k].sort((a, b) => a.rel.localeCompare(b.rel));
}
const lines = [];
lines.push(
'<!-- Generated by scripts/generate-docs-index.mjs. Do not edit by hand. Run `npm run docs:index`. -->'
);
lines.push('');
lines.push('# Documentation Index');
lines.push('');
lines.push(
`Auto-generated on ${new Date().toISOString().slice(0, 10)} from front-matter and headings.`
);
lines.push('');
// --- ADRs ---
lines.push('## Architecture Decision Records');
lines.push('');
if (groups.ADRs.length === 0) {
lines.push('_No ADRs yet._');
lines.push('');
} else {
lines.push('| # | Title | Status |');
lines.push('|---|---|---|');
for (const d of groups.ADRs) {
const n = adrNumber(d.basename);
const num = n !== null ? String(n).padStart(4, '0') : '—';
lines.push(
`| ${num} | [${escapePipe(d.title)}](${d.linkPath}) | ${escapePipe(d.status || '—')} |`
);
}
lines.push('');
}
// --- Specs ---
lines.push('## Specifications');
lines.push('');
if (groups.Specs.length === 0) {
lines.push('_No specs yet._');
lines.push('');
} else {
lines.push('| Title | Status | Updated |');
lines.push('|---|---|---|');
for (const d of groups.Specs) {
lines.push(
`| [${escapePipe(d.title)}](${d.linkPath}) | ${escapePipe(d.status || '—')} | ${escapePipe(d.date || '—')} |`
);
}
lines.push('');
}
// --- Plans (with archive subsection) ---
lines.push('## Plans');
lines.push('');
const plansActive = groups.Plans.filter((d) => !d.rel.includes('/archive/'));
const plansArchive = groups.Plans.filter((d) => d.rel.includes('/archive/'));
if (plansActive.length === 0) {
lines.push('_No active plans._');
lines.push('');
} else {
lines.push('| Title | Status | Updated |');
lines.push('|---|---|---|');
for (const d of plansActive) {
lines.push(
`| [${escapePipe(d.title)}](${d.linkPath}) | ${escapePipe(d.status || '—')} | ${escapePipe(d.date || '—')} |`
);
}
lines.push('');
}
if (plansArchive.length > 0) {
lines.push('### Archive');
lines.push('');
lines.push('| Title | Updated |');
lines.push('|---|---|');
for (const d of plansArchive) {
lines.push(
`| [${escapePipe(d.title)}](${d.linkPath}) | ${escapePipe(d.date || '—')} |`
);
}
lines.push('');
}
// --- Patterns ---
lines.push('## Patterns');
lines.push('');
if (groups.Patterns.length === 0) {
lines.push('_No patterns yet._');
lines.push('');
} else {
lines.push('| Title | Status | Updated |');
lines.push('|---|---|---|');
for (const d of groups.Patterns) {
lines.push(
`| [${escapePipe(d.title)}](${d.linkPath}) | ${escapePipe(d.status || '—')} | ${escapePipe(d.date || '—')} |`
);
}
lines.push('');
}
// --- Other (catches design/, api/, runbooks/, etc. until they get
// dedicated sections after the docs-restructure) ---
if (groups.Other.length > 0) {
lines.push('## Other Docs');
lines.push('');
lines.push('| Title | Path | Status | Updated |');
lines.push('|---|---|---|---|');
for (const d of groups.Other) {
lines.push(
`| [${escapePipe(d.title)}](${d.linkPath}) | \`${d.rel.replace(/^docs\//, '')}\` | ${escapePipe(d.status || '—')} | ${escapePipe(d.date || '—')} |`
);
}
lines.push('');
}
const out = lines.join('\n');
await writeFile(INDEX_PATH, out, 'utf8');
console.log(`Wrote ${relative(REPO_ROOT, INDEX_PATH)} (${docs.length} docs indexed)`);
}
main().catch((err) => {
console.error(err);
process.exit(1);
});