docs: sync data-model, glossary en specs met huidig schema (#164)

Brengt de docs gelijk met de werkelijkheid na PBI-46/47/50/58/59/61/63
en M12. Belangrijkste fixes:

- data-model.md herschreven naar prisma/schema.prisma: nieuwe entiteiten
  (Idea, IdeaLog, IdeaProduct, UserQuestion, ClaudeQuestion, ClaudeJob,
  SprintRun, SprintTaskExecution, ClaudeWorker, LoginPairing,
  PushSubscription, ModelPrice, ProductMember), nieuwe enums
  (FAILED/EXCLUDED, OPEN/CLOSED/ARCHIVED, ADMIN, etc.) en codes
  (PBI/ST/T/SP-N) toegevoegd; verwijderde todos-tabel verwijderd.
- glossary.md: Sprint zonder "max 1 actief" (PBI-63), Story/Task incl.
  FAILED/EXCLUDED, Todo verwijderd, Idea/SprintRun/ClaudeJob/
  verify_result toegevoegd.
- project-structure.md: app/(app)/todos vervangen door
  ideas/insights/jobs/manual/admin/solo; api-tree volledig.
- overview.md: "geen realtime in v1" en Docker-rationale herschreven —
  Postgres LISTEN/NOTIFY + SSE, claude_jobs als queue, opt-in
  Docker-deploy-flow.
- functional.md: F-08 Todo-lijst -> Ideeen-laag, F-09 multi-sprint,
  F-10 Task-status incl. FAILED/EXCLUDED, F-11 endpoint-lijst,
  navigatiestructuur, datamodel-schets en Flow 3 bijgewerkt.
- README.md API-tabel: /api/todos weg, ideas/jobs/users/profile/health
  toegevoegd, kort over realtime/auth-pair/internal/cron.
- patterns + mcp-integration runbook: Todo-/ACTIVE-references vervangen
  door Idea/OPEN; create_todo MCP-tool note over verwijdering.

Linkcheck groen (105 files), INDEX hergegenereerd (98 docs).

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Janpeter Visser 2026-05-08 08:16:44 +02:00 committed by GitHub
parent 3842c05ae9
commit f7464db837
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 544 additions and 424 deletions

View file

@ -2,7 +2,7 @@
# Documentation Index
Auto-generated on 2026-05-07 from front-matter and headings.
Auto-generated on 2026-05-08 from front-matter and headings.
## Architecture Decision Records
@ -32,7 +32,7 @@ Auto-generated on 2026-05-07 from front-matter and headings.
| [StoryDialog Profiel](./specs/dialogs/story.md) | active | 2026-05-04 |
| [TaskDetailDialog Profiel](./specs/dialogs/task-detail.md) | active | 2026-05-04 |
| [TaskDialog Profiel](./specs/dialogs/task.md) | active | 2026-05-03 |
| [Scrum4Me — Functionele Specificatie](./specs/functional.md) | active | 2026-05-04 |
| [Scrum4Me — Functionele Specificatie](./specs/functional.md) | active | 2026-05-08 |
| [DevPlanner — User Personas](./specs/personas.md) | active | 2026-05-03 |
## Plans
@ -70,14 +70,14 @@ Auto-generated on 2026-05-07 from front-matter and headings.
| 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 |
| [Entity Dialog](./patterns/dialog.md) | active | 2026-05-08 |
| [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 |
| [Proxy (route protection)](./patterns/proxy.md) | active | 2026-05-08 |
| [QR-pairing via unauth-SSE + pre-auth cookie](./patterns/qr-login.md) | active | 2026-05-03 |
| [Realtime NOTIFY payload — veldnaam-contract](./patterns/realtime-notify-payload.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 |
| [Route Handler (REST API)](./patterns/route-handler.md) | active | 2026-05-08 |
| [Server Action](./patterns/server-action.md) | active | 2026-05-08 |
| [Float sort_order (drag-and-drop volgorde)](./patterns/sort-order.md) | active | 2026-05-03 |
| [Story met UI-component](./patterns/story-with-ui-component.md) | active | 2026-05-03 |
| [Web Push](./patterns/web-push.md) | active | 2026-05-07 |
@ -92,9 +92,9 @@ Auto-generated on 2026-05-07 from front-matter and headings.
| [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 |
| [Data Model & Prisma Schema](./architecture/data-model.md) | `architecture/data-model.md` | active | 2026-05-08 |
| [Scrum4Me — Architecture Overview](./architecture/overview.md) | `architecture/overview.md` | active | 2026-05-08 |
| [Project Structure, Stores, Realtime & Job Queue](./architecture/project-structure.md) | `architecture/project-structure.md` | active | 2026-05-08 |
| [QR-pairing Login Flow](./architecture/qr-pairing.md) | `architecture/qr-pairing.md` | active | 2026-05-03 |
| [Sprint execution modes — PER_TASK vs SPRINT_BATCH](./architecture/sprint-execution-modes.md) | `architecture/sprint-execution-modes.md` | active | 2026-05-07 |
| [Scrum4Me — Implementatie Backlog](./backlog.md) | `backlog.md` | active | 2026-05-03 |
@ -105,7 +105,7 @@ Auto-generated on 2026-05-07 from front-matter and headings.
| [Docker smoke test — task 1](./docker-smoke/2-mei-task-1.md) | `docker-smoke/2-mei-task-1.md` | done | 2026-05-03 |
| [Docker smoke test — task 2](./docker-smoke/2-mei-task-2.md) | `docker-smoke/2-mei-task-2.md` | done | 2026-05-03 |
| [Scrum4Me — Functionele Specificatie](./functional.md) | `functional.md` | active | 2026-05-03 |
| [Scrum4Me — Glossary](./glossary.md) | `glossary.md` | active | 2026-05-03 |
| [Scrum4Me — Glossary](./glossary.md) | `glossary.md` | active | 2026-05-08 |
| [Overview](./manual/01-overview.md) | `manual/01-overview.md` | active | 2026-05-07 |
| [Statuses & Transitions](./manual/02-statuses-and-transitions.md) | `manual/02-statuses-and-transitions.md` | active | 2026-05-07 |
| [Git Workflow](./manual/03-git-workflow.md) | `manual/03-git-workflow.md` | active | 2026-05-07 |
@ -125,7 +125,7 @@ Auto-generated on 2026-05-07 from front-matter and headings.
| [Branch, PR & Commit Strategy](./runbooks/branch-and-commit.md) | `runbooks/branch-and-commit.md` | active | 2026-05-03 |
| [Deploy-controle: triggers, labels, path-filter](./runbooks/deploy-control.md) | `runbooks/deploy-control.md` | active | 2026-05-07 |
| [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 |
| [MCP Integration — Scrum4Me Tools](./runbooks/mcp-integration.md) | `runbooks/mcp-integration.md` | active | 2026-05-08 |
| [v1.0 Smoke Test Checklist](./runbooks/v1-smoke-test.md) | `runbooks/v1-smoke-test.md` | active | 2026-05-04 |
| [Worker idempotency & job-status protocol](./runbooks/worker-idempotency.md) | `runbooks/worker-idempotency.md` | active | 2026-05-05 |
| [StoryDialog Profiel](./story-dialog.md) | `story-dialog.md` | active | 2026-05-03 |

View file

@ -3,27 +3,34 @@ title: "Data Model & Prisma Schema"
status: active
audience: [maintainer, contributor]
language: nl
last_updated: 2026-05-03
last_updated: 2026-05-08
related: [auth-and-sessions.md](./auth-and-sessions.md)
---
## Datamodel
> Bron van waarheid is [`prisma/schema.prisma`](../../prisma/schema.prisma); dit document samenvat de tabellen en sleutelinvarianten. Bij twijfel wint het schema.
### `users`
| Kolom | Type | Constraints | Noten |
|---|---|---|---|
| id | String (cuid) | PK | Gegenereerd door Prisma |
| id | String (cuid) | PK | |
| username | String | unique, not null, min 3 | Inlognaam |
| email | String? | unique | Optioneel; gebruikt voor wachtwoord-reset-flows |
| 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) |
| bio | String? | max 160 | Korte profielomschrijving |
| bio_detail | String? | max 2000 | Uitgebreide profielbeschrijving |
| must_reset_password | Boolean | default false | Forceert wachtwoord-reset bij volgende login |
| avatar_data | Bytes? | | Profielfoto als WebP bytea (max 700×700) |
| active_product_id | String? | FK → products (SetNull) | Persistente actieve PB-keuze (M9) |
| idea_code_counter | Int | default 0 | Sequentiële teller voor user-scoped idea-codes |
| min_quota_pct | Int | default 20 | Worker stand-by-drempel (M13 quota-check) |
| created_at | DateTime | default now() | |
| updated_at | DateTime | auto-update | Gebruikt als cache-buster voor avatar-URL |
| updated_at | DateTime | auto-update | Cache-buster voor avatar-URL |
**Indexes:** `username` (unique lookup bij inloggen)
**Indexes:** `username` (unique), `email` (unique), `active_product_id`
---
@ -32,10 +39,9 @@ related: [auth-and-sessions.md](./auth-and-sessions.md)
| Kolom | Type | Constraints | Noten |
|---|---|---|---|
| id | String (cuid) | PK | |
| user_id | String | FK → users, not null | |
| role | Enum | PRODUCT_OWNER \| SCRUM_MASTER \| DEVELOPER | |
| user_id | String | FK → users (Cascade) | |
| role | Enum | `PRODUCT_OWNER \| SCRUM_MASTER \| DEVELOPER \| ADMIN` | |
**Indexes:** `(user_id)` — meerdere rollen per gebruiker
**Constraint:** unique `(user_id, role)`
---
@ -45,13 +51,13 @@ related: [auth-and-sessions.md](./auth-and-sessions.md)
| 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" |
| user_id | String | FK → users (Cascade) | |
| token_hash | String | unique, not null | SHA-256 hash van het token |
| label | String? | | Bijv. "Claude Code — laptop" |
| created_at | DateTime | default now() | |
| revoked_at | DateTime | nullable | Null = actief |
| revoked_at | DateTime? | | Null = actief |
**Indexes:** `token_hash` (lookup bij elke API-aanroep — moet snel zijn)
**Indexes:** `token_hash` (unique). Eén token kan max. één `claude_workers`-record hebben.
---
@ -60,17 +66,20 @@ related: [auth-and-sessions.md](./auth-and-sessions.md)
| 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 |
| user_id | String | FK → users (Cascade) | Eigenaar |
| name | String | not null | Uniek per gebruiker |
| code | String? | max 30 | Optionele afkorting; uniek per gebruiker als gezet |
| description | String? | | |
| repo_url | String? | | Gevalideerde URL |
| definition_of_done | String | not null | Vaste instelling per product |
| auto_pr | Boolean | default false | Automatische PR creëren na sprint-completion |
| pr_strategy | Enum | `SPRINT \| STORY \| SPRINT_BATCH`, default `SPRINT` | Granulariteit voor auto-PR |
| 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)`
**Constraints:** unique `(user_id, name)`, unique `(user_id, code)`
---
@ -79,19 +88,22 @@ related: [auth-and-sessions.md](./auth-and-sessions.md)
| 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 |
| product_id | String | FK → products (Cascade) | |
| code | String | max 30, not null | Verplicht; auto-gegenereerd of handmatig |
| title | String | not null | |
| description | String? | | |
| priority | Int | 14 | 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) |
| status | Enum | `READY \| BLOCKED \| FAILED \| DONE`, default `READY` | Auto-promotie naar DONE bij sprint-close |
| pr_url | String? | | URL van de PR die deze PBI dekt (PBI-strategie) |
| pr_merged_at | DateTime? | | Gezet wanneer de PR daadwerkelijk gemerged is |
| 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
**Indexes:** `(product_id, priority, sort_order)`, `(product_id, status)`
**Constraint:** unique `(product_id, code)`
**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.
**Cascade-regel (sprint-close):** wanneer een Sprint wordt afgerond via `completeSprintAction` en alle stories van een PBI eindigen op DONE, zet diezelfde transactie de PBI-status op DONE. Promotie alléén — een DONE-PBI wordt nooit automatisch teruggezet. Stories die niet in deze Sprint zaten worden meegerekend op hun huidige DB-status. Een PBI zonder stories blijft READY.
---
@ -100,21 +112,24 @@ related: [auth-and-sessions.md](./auth-and-sessions.md)
| Kolom | Type | Constraints | Noten |
|---|---|---|---|
| id | String (cuid) | PK | |
| pbi_id | String | FK → pbis (cascade delete) | |
| pbi_id | String | FK → pbis (Cascade) | |
| 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 | |
| sprint_id | String? | FK → sprints | Null = in Product Backlog |
| assignee_id | String? | FK → users (SetNull) | Story-claim op het Solo-bord |
| code | String | max 30, not null | Auto-gegenereerd of handmatig |
| title | String | not null | |
| description | String? | | |
| acceptance_criteria | String? | | |
| priority | Int | 14 | |
| sort_order | Float | not null | |
| status | Enum | OPEN \| IN_SPRINT \| DONE | |
| status | Enum | `OPEN \| IN_SPRINT \| DONE \| FAILED`, default `OPEN` | |
| created_at | DateTime | default now() | |
| updated_at | DateTime | auto-update | |
**Indexes:** `(pbi_id, priority, sort_order)`, `(sprint_id, sort_order)`, `(product_id, status)`
**Indexes:** `(pbi_id, priority, sort_order)`, `(sprint_id, sort_order)`, `(product_id, status)`, `(sprint_id, assignee_id)`
**Constraint:** unique `(product_id, code)`
**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]`).
**Auto-promotie/demotie via task-status:** zodra alle tasks van een story op `DONE` staan en de story-status nog niet `DONE` is, promoot dezelfde transactie de story naar `DONE`. Wordt een task van een `DONE`-story heropend, dan demoot de story terug naar `IN_SPRINT` — niet naar `OPEN` (dat zou "terug in productbacklog" betekenen, een sprint-management-actie). 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]`).
---
@ -123,15 +138,16 @@ related: [auth-and-sessions.md](./auth-and-sessions.md)
| 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 |
| story_id | String | FK → stories (Cascade) | |
| type | Enum | `IMPLEMENTATION_PLAN \| TEST_RESULT \| COMMIT` | |
| content | String | not null | |
| status | Enum | `PASSED \| FAILED`? | Alleen bij type `TEST_RESULT` |
| commit_hash | String? | | Alleen bij type `COMMIT` |
| commit_message | String? | | Alleen bij type `COMMIT` |
| metadata | Json? | | Vrije bag voor bron-info (bv. agent, model_id) |
| created_at | DateTime | default now() | |
**Indexes:** `(story_id, created_at)` — chronologische weergave in de UI
**Indexes:** `(story_id, created_at)`
---
@ -140,14 +156,43 @@ related: [auth-and-sessions.md](./auth-and-sessions.md)
| 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 | |
| product_id | String | FK → products (Cascade) | |
| code | String | max 30, not null | Auto-gegenereerd `SP-N` per product (PBI-59) |
| sprint_goal | String | not null | |
| status | Enum | `OPEN \| CLOSED \| ARCHIVED \| FAILED`, default `OPEN` | |
| start_date | Date? | | Optionele planningsmetadata |
| end_date | Date? | | Optionele planningsmetadata |
| created_at | DateTime | default now() | |
| completed_at | DateTime | nullable | |
| completed_at | DateTime? | | Wordt gezet bij overgang naar CLOSED |
**Indexes:** `(product_id, status)` — query voor actieve Sprint per product
**Constraint:** Max. 1 actieve Sprint per product (gehandhaafd in applicatielaag)
**Indexes:** `(product_id, status)`
**Constraint:** unique `(product_id, code)`
**Eén product, meerdere sprints (PBI-63):** een product kan tegelijk meer dan één sprint hebben. `OPEN` is geen exclusieve status; de sprint-switcher in de product-header laat de gebruiker tussen sprints kiezen. Stories in `IN_SPRINT` linken via `sprint_id` naar één specifieke sprint.
---
### `sprint_runs`
Eén `sprint_runs`-record per uitvoering van de SPRINT_IMPLEMENTATION-flow (PBI-46/47/50). Houdt status, branch en chained retries bij wanneer een run is gepauzeerd of mislukt.
| Kolom | Type | Constraints | Noten |
|---|---|---|---|
| id | String (cuid) | PK | |
| sprint_id | String | FK → sprints (Cascade) | |
| started_by_id | String | FK → users | Wie de run startte |
| status | Enum | `QUEUED \| RUNNING \| PAUSED \| DONE \| FAILED \| CANCELLED` | |
| pr_strategy | Enum | `SPRINT \| STORY \| SPRINT_BATCH` | Snapshot van de strategie bij start |
| branch | String? | | Werkbranch voor de run |
| pr_url | String? | | |
| started_at / finished_at | DateTime? | | |
| failure_reason | String? | | Vrij tekstveld bij FAILED/CANCELLED |
| failed_task_id | String? | FK → tasks (SetNull) | Eerste task die de run brak (cascade-FAIL) |
| pause_context | Json? | | Gevalideerd door Zod (`lib/sprint-run/pause-context.ts`) |
| previous_run_id | String? | unique, FK → sprint_runs (SetNull) | Chain naar een eerdere run |
| created_at / updated_at | DateTime | | |
**Indexes:** `(sprint_id, status)`, `(started_by_id, status)`
---
@ -156,51 +201,269 @@ related: [auth-and-sessions.md](./auth-and-sessions.md)
| 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 | |
| story_id | String | FK → stories (Cascade) | |
| product_id | String | FK → products (Cascade) | Denormalisatie voor product-scoped queries |
| sprint_id | String? | FK → sprints | Denormalisatie; geërfd van story bij sprint-toevoeging |
| code | String | max 30, not null | Auto-gegenereerd `T-N` per product |
| title | String | not null | |
| description | String? | | |
| implementation_plan | String? | | Opgeslagen via Server Action of `PATCH /api/tasks/:id` |
| priority | Int | 14 | |
| sort_order | Float | not null | |
| status | Enum | TO_DO \| IN_PROGRESS \| REVIEW \| DONE | |
| status | Enum | `TO_DO \| IN_PROGRESS \| REVIEW \| DONE \| FAILED \| EXCLUDED`, default `TO_DO` | `EXCLUDED` slaat verify-skip op tijdens een sprint-run |
| verify_only | Boolean | default false | Run-mode: alleen verifiëren, niet implementeren |
| verify_required | Enum | `ALIGNED \| ALIGNED_OR_PARTIAL \| ANY`, default `ALIGNED_OR_PARTIAL` | Drempel waarop een job's verify_result als acceptabel telt |
| repo_url | String? | | Optionele override van `product.repo_url` voor tasks die in een andere repo wonen |
| created_at | DateTime | default now() | |
| updated_at | DateTime | auto-update | |
**Indexes:** `(story_id, priority, sort_order)`, `(sprint_id, status)`
**Indexes:** `(story_id, priority, sort_order)`, `(sprint_id, status)`, `(product_id)`
**Constraint:** unique `(product_id, code)``code` blijft stabiel bij re-parenting (Jira-stijl)
---
### `todos`
### `claude_jobs`
Job-queue waarop `wait_for_job` (MCP) atomisch claimt via `FOR UPDATE SKIP LOCKED`. Eén rij per task-implementatie, idea-grill, idea-make-plan, plan-chat of sprint-run.
| 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 | |
| user_id | String | FK → users (Cascade) | |
| product_id | String | FK → products (Cascade) | |
| task_id | String? | FK → tasks (Cascade) | Bij `TASK_IMPLEMENTATION` of `SPRINT_IMPLEMENTATION` |
| idea_id | String? | FK → ideas (Cascade) | Bij `IDEA_GRILL` / `IDEA_MAKE_PLAN` |
| sprint_run_id | String? | FK → sprint_runs (SetNull) | Koppel naar de bovenliggende run |
| kind | Enum | `TASK_IMPLEMENTATION \| IDEA_GRILL \| IDEA_MAKE_PLAN \| PLAN_CHAT \| SPRINT_IMPLEMENTATION` | |
| status | Enum | `QUEUED \| CLAIMED \| RUNNING \| DONE \| FAILED \| CANCELLED \| SKIPPED` | |
| claimed_by_token_id | String? | FK → api_tokens (SetNull) | Auth-koppel voor `update_job_status` |
| claimed_at / started_at / finished_at / pushed_at | DateTime? | | Lifecycle-stempels |
| verify_result | Enum? | `ALIGNED \| PARTIAL \| EMPTY \| DIVERGENT` | Alleen voor task-/sprint-jobs |
| model_id | String? | | Anthropic model dat de agent rapporteerde |
| input_tokens / output_tokens / cache_read_tokens / cache_write_tokens | Int? | | Token-usage voor billing-overzicht |
| plan_snapshot | String? | | Bevroren plan op claim-moment |
| base_sha / head_sha / branch / pr_url | String? | | Git-context |
| summary | String? | | Vrije agent-samenvatting bij DONE |
| error | String? | | Reden bij FAILED |
| retry_count | Int | default 0 | |
| lease_until | DateTime? | | Stale-CLAIMED → terug naar QUEUED na 30 min |
| created_at / updated_at | DateTime | | |
**Indexes:** `(user_id, done, archived)` — standaard weergave filtert op actieve todo's; `(user_id, product_id)` — filteren per product
**Indexes:** `(user_id, status)`, `(task_id, status)`, `(idea_id, status)`, `(sprint_run_id, status)`, `(status, claimed_at)`, `(status, finished_at)`, `(status, lease_until)`
---
### `sprint_task_executions`
Bevroren scope-snapshot per `SPRINT_IMPLEMENTATION`-claim (PBI-50). Bij claim wordt voor elke `TO_DO`-task in scope één `PENDING`-record gemaakt met `implementation_plan` + `verify_required` gesnapshot. Worker en gate werken uitsluitend op deze rows; latere wijzigingen aan Task hebben geen invloed op de lopende batch.
| Kolom | Type | Constraints | Noten |
|---|---|---|---|
| id | String (cuid) | PK | |
| sprint_job_id | String | FK → claude_jobs (Cascade) | De parent SPRINT_IMPLEMENTATION-job |
| task_id | String | FK → tasks (Cascade) | |
| order | Int | not null | Volgorde binnen de batch |
| plan_snapshot | String (Text) | not null | Het bevroren implementation_plan |
| verify_required_snapshot | Enum | `ALIGNED \| ALIGNED_OR_PARTIAL \| ANY` | |
| verify_only_snapshot | Boolean | default false | |
| base_sha / head_sha | String? | | |
| status | Enum | `PENDING \| RUNNING \| DONE \| FAILED \| SKIPPED` | |
| verify_result | Enum? | `ALIGNED \| PARTIAL \| EMPTY \| DIVERGENT` | |
| verify_summary / skip_reason | String? (Text) | | |
| started_at / finished_at | DateTime? | | |
| created_at / updated_at | DateTime | | |
**Constraint:** unique `(sprint_job_id, task_id)`
**Indexes:** `(sprint_job_id, order)`
---
### `model_prices`
Prijslookup voor Anthropic-modellen, gebruikt door de jobs-pagina om kosten te berekenen.
| Kolom | Type | Constraints | Noten |
|---|---|---|---|
| id | String (cuid) | PK | |
| model_id | String | unique | Anthropic model-id (bv. `claude-opus-4-7`) |
| input_price_per_1m | Decimal(12,6) | | USD per 1M tokens (default) |
| output_price_per_1m | Decimal(12,6) | | |
| cache_read_price_per_1m | Decimal(12,6) | | |
| cache_write_price_per_1m | Decimal(12,6) | | |
| currency | String | default `USD` | |
| created_at / updated_at | DateTime | | |
---
### `claude_workers`
Live-presence-record per actieve agent worker. Ingevoegd bij MCP-startup, geüpdatet via `worker_heartbeat` (5s), opgeruimd bij SIGTERM. NavBar telt actieve workers op `last_seen_at < now() - 15s`.
| Kolom | Type | Constraints | Noten |
|---|---|---|---|
| id | String (cuid) | PK | |
| user_id | String | FK → users (Cascade) | |
| token_id | String | unique, FK → api_tokens (Cascade) | Eén worker per token |
| product_id | String? | | Optioneel; gerapporteerd door de agent |
| started_at / last_seen_at | DateTime | default now() | |
| last_quota_pct | Int? | | M13 pre-flight quota-check |
| last_quota_check_at | DateTime? | | |
**Indexes:** `(user_id, last_seen_at)`
---
### `product_members`
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 hier opgeslagen — dat doet `user_roles`. Een gebruiker kan alleen worden toegevoegd als hij/zij de rol `DEVELOPER` heeft.
| Kolom | Type | Constraints | Noten |
|---|---|---|---|
| id | String (cuid) | PK | |
| product_id | String | FK → products (cascade delete) | |
| user_id | String | FK → users (cascade delete) | |
| product_id | String | FK → products (Cascade) | |
| user_id | String | FK → users (Cascade) | |
| 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
**Constraint:** unique `(product_id, user_id)`
**Indexes:** `(user_id)`
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.
---
### `ideas`
Idea-entity (M12) tussen losse notitie en PBI. Een idea wordt eerst _gegrilled_ (interactieve Q&A → `grill_md`), daarna gemateriaaliseerd tot een plan (`plan_md`) dat deterministisch geparseerd wordt naar PBI + stories + tasks. Vervangt de oude `todos`-tabel volledig (atomische migratie ST-1239 — todos zijn gedropt).
| Kolom | Type | Constraints | Noten |
|---|---|---|---|
| id | String (cuid) | PK | |
| user_id | String | FK → users (Cascade) | |
| product_id | String? | FK → products (SetNull) | Primaire scope; null = unscoped capture |
| code | String | max 30, not null | Sequentieel per gebruiker via `idea_code_counter` |
| title | String | not null | |
| description | String? | max 4000 | Initiële tekst |
| grill_md | String? (Text) | | Output van IDEA_GRILL |
| plan_md | String? (Text) | | Output van IDEA_MAKE_PLAN |
| pbi_id | String? | unique, FK → pbis (SetNull) | Wordt gevuld na materialisatie |
| status | Enum | `DRAFT \| GRILLING \| GRILL_FAILED \| GRILLED \| PLANNING \| PLAN_FAILED \| PLAN_READY \| PLANNED`, default `DRAFT` | |
| archived | Boolean | default false | |
| created_at / updated_at | DateTime | | |
**Constraint:** unique `(user_id, code)`
**Indexes:** `(user_id, archived, status)`, `(user_id, product_id)`
---
### `idea_products`
Optionele secundaire producten waar een idea ook impact heeft.
| Kolom | Type | Constraints | Noten |
|---|---|---|---|
| id | String (cuid) | PK | |
| idea_id | String | FK → ideas (Cascade) | |
| product_id | String | FK → products (Cascade) | |
| created_at | DateTime | default now() | |
**Constraint:** unique `(idea_id, product_id)`
**Indexes:** `(product_id)`
---
### `idea_logs`
Activiteitenlog per idea — soortgelijke rol als `story_logs` voor stories.
| Kolom | Type | Constraints | Noten |
|---|---|---|---|
| id | String (cuid) | PK | |
| idea_id | String | FK → ideas (Cascade) | |
| type | Enum | `DECISION \| NOTE \| GRILL_RESULT \| PLAN_RESULT \| STATUS_CHANGE \| JOB_EVENT` | |
| content | String (Text) | not null | |
| metadata | Json? | | |
| created_at | DateTime | default now() | |
**Indexes:** `(idea_id, created_at)`
---
### `user_questions`
Vrije vragen die een gebruiker aan zichzelf stelt op een idea (M12). Wordt door de Idea-detail-UI ingelezen om te helpen bij het grillen.
| Kolom | Type | Constraints | Noten |
|---|---|---|---|
| id | String (cuid) | PK | |
| idea_id | String | FK → ideas (Cascade) | |
| user_id | String | not null | Eigenaar |
| question | String (Text) | not null | |
| answer | String? (Text) | | |
| status | Enum (lowercase) | `pending \| answered`, default `pending` | |
| created_at / updated_at | DateTime | | |
**Indexes:** `(idea_id, status)`, `(user_id)`
---
### `claude_questions`
Persistent vraag-antwoord-kanaal van een agent (via `mcp__scrum4me__ask_user_question`) richting de actieve gebruiker (M11). LISTEN/NOTIFY pusht het antwoord terug naar de wachtende agent. Zie [architecture/claude-question-channel.md](./claude-question-channel.md).
| Kolom | Type | Constraints | Noten |
|---|---|---|---|
| id | String (cuid) | PK | |
| story_id | String? | FK → stories (Cascade) | Eén van story/task/idea is verplicht |
| task_id | String? | FK → tasks (SetNull) | |
| idea_id | String? | FK → ideas (Cascade) | |
| product_id | String | FK → products (Cascade) | Gedenormaliseerd voor het SSE-filter |
| asked_by | String | FK → users | Token-houder = Claude |
| question | String (Text) | not null | |
| options | Json? | | `string[]` voor multi-choice; null voor free-text |
| status | String | not null | `open \| answered \| cancelled \| expired` |
| answer | String? (Text) | | |
| answered_by | String? | FK → users | |
| answered_at | DateTime? | | |
| created_at | DateTime | default now() | |
| expires_at | DateTime | not null | Default `now() + 24h`, ingesteld door MCP-tool |
**Indexes:** `(story_id, status)`, `(idea_id, status)`, `(product_id, status)`, `(status, expires_at)`
---
### `login_pairings`
QR-pairing-flow (M10). Desktop start een pairing, telefoon scant en bevestigt; daarna kan de desktop ruilen voor een sessie.
| Kolom | Type | Constraints | Noten |
|---|---|---|---|
| id | String (cuid) | PK | |
| secret_hash | String | not null | Hash van het pairing-secret |
| desktop_token_hash | String | not null | Hash van het pre-auth desktop-token |
| status | String | not null | `pending \| approved \| consumed \| expired` |
| user_id | String? | FK → users (SetNull) | Gezet bij approval |
| desktop_ua | String? | max 255 | UA-string van de desktop-aanvraag |
| desktop_ip | String? | max 45 | IP voor audit |
| created_at | DateTime | default now() | |
| expires_at | DateTime | not null | TTL voor afhandeling |
| approved_at / consumed_at | DateTime? | | |
**Indexes:** `(expires_at)`, `(status, expires_at)`
---
### `push_subscriptions`
Web Push subscriptions per gebruiker, voor notificaties.
| Kolom | Type | Constraints | Noten |
|---|---|---|---|
| id | String (cuid) | PK | |
| user_id | String | FK → users (Cascade) | |
| endpoint | String | unique, not null | Push-service endpoint |
| p256dh / auth | String | not null | VAPID keys |
| user_agent | String? | | UA bij subscribe |
| created_at / last_used_at | DateTime | default now() | |
**Indexes:** `(user_id)`
---
@ -211,7 +474,7 @@ 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.
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 (archiveren, teamleden beheren).
Schrijfoperaties volgen deze invarianten:
@ -225,236 +488,32 @@ Schrijfoperaties volgen deze invarianten:
---
## Prisma Schema (excerpt)
## Enums (overzicht)
```prisma
// prisma/schema.prisma
| Enum | Waarden |
|---|---|
| `Role` | `PRODUCT_OWNER`, `SCRUM_MASTER`, `DEVELOPER`, `ADMIN` |
| `PbiStatus` | `READY`, `BLOCKED`, `FAILED`, `DONE` |
| `StoryStatus` | `OPEN`, `IN_SPRINT`, `DONE`, `FAILED` |
| `TaskStatus` | `TO_DO`, `IN_PROGRESS`, `REVIEW`, `DONE`, `FAILED`, `EXCLUDED` |
| `SprintStatus` | `OPEN`, `CLOSED`, `ARCHIVED`, `FAILED` |
| `SprintRunStatus` | `QUEUED`, `RUNNING`, `PAUSED`, `DONE`, `FAILED`, `CANCELLED` |
| `PrStrategy` | `SPRINT`, `STORY`, `SPRINT_BATCH` |
| `LogType` | `IMPLEMENTATION_PLAN`, `TEST_RESULT`, `COMMIT` |
| `TestStatus` | `PASSED`, `FAILED` |
| `ClaudeJobStatus` | `QUEUED`, `CLAIMED`, `RUNNING`, `DONE`, `FAILED`, `CANCELLED`, `SKIPPED` |
| `ClaudeJobKind` | `TASK_IMPLEMENTATION`, `IDEA_GRILL`, `IDEA_MAKE_PLAN`, `PLAN_CHAT`, `SPRINT_IMPLEMENTATION` |
| `VerifyResult` | `ALIGNED`, `PARTIAL`, `EMPTY`, `DIVERGENT` |
| `VerifyRequired` | `ALIGNED`, `ALIGNED_OR_PARTIAL`, `ANY` |
| `SprintTaskExecutionStatus` | `PENDING`, `RUNNING`, `DONE`, `FAILED`, `SKIPPED` |
| `IdeaStatus` | `DRAFT`, `GRILLING`, `GRILL_FAILED`, `GRILLED`, `PLANNING`, `PLAN_FAILED`, `PLAN_READY`, `PLANNED` |
| `IdeaLogType` | `DECISION`, `NOTE`, `GRILL_RESULT`, `PLAN_RESULT`, `STATUS_CHANGE`, `JOB_EVENT` |
| `UserQuestionStatus` | `pending`, `answered` (lowercase, niet UPPER_SNAKE) |
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")
}
```
> API-grens: `TaskStatus` en `StoryStatus` worden tussen DB (UPPER_SNAKE) en API (lowercase) vertaald via `lib/task-status.ts` (zie [ADR-0004](../adr/0004-status-enum-mapping.md)).
---
## Prisma Schema
De volledige, levende definitie staat in [`prisma/schema.prisma`](../../prisma/schema.prisma). Genereer de ERD lokaal met `npm run db:erd` (zie [README — Database](../../README.md#database)). Het ERD-diagram zelf staat in [docs/assets/erd.svg](../assets/erd.svg).

View file

@ -3,18 +3,18 @@ title: "Scrum4Me — Architecture Overview"
status: active
audience: [maintainer, contributor]
language: nl
last_updated: 2026-05-03
last_updated: 2026-05-08
related: [data-model.md](./data-model.md), [project-structure.md](./project-structure.md)
---
**Versie:** 0.1 — april 2026
**Versie:** 0.2 — mei 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.
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 door Prisma v7. Authenticatie is custom username/password via iron-session — geen externe auth-provider, geen verplichte 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. Realtime-updates voor Solo-bord en Backlog gaan via Postgres `LISTEN/NOTIFY` + Server-Sent Events; geen externe broker. Een eigen MCP-server (`scrum4me-mcp`) biedt agents een job-queue (`claude_jobs`) bovenop dezelfde database. Vercel Analytics meet pageviews via de root layout; profielfoto's worden server-side verwerkt met Sharp.
---
@ -49,9 +49,9 @@ Scrum4Me is een desktop-first Next.js 16 webapplicatie die server-side wordt ger
| 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 |
| WebSockets / externe realtime-broker (Pusher, Ably, Supabase Realtime) | Postgres `LISTEN/NOTIFY` + SSE dekt de realtime-behoefte zonder een tweede auth-laag of extra infrastructuur — zie `architecture/project-structure.md` |
| Redis | Geen caching- of queuerequirements; de `claude_jobs`-tabel met `FOR UPDATE SKIP LOCKED` doet het werk dat een queue-broker anders zou doen |
| Docker voor lokale dev | Voor lokale ontwikkeling volstaat Neon. Er is wél een opt-in Docker-deploy-flow (`scrum4me-docker`, native arm64 op Mac, NAS-flow opt-in) — die is voor deploy, niet voor dev |
| 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

@ -3,7 +3,7 @@ title: "Project Structure, Stores, Realtime & Job Queue"
status: active
audience: [maintainer, contributor]
language: nl
last_updated: 2026-05-03
last_updated: 2026-05-08
related: [data-model.md](./data-model.md)
---
@ -23,11 +23,20 @@ scrum4me/
│ │ │ └── [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
│ │ │ └── sprint/
│ │ │ ├── page.tsx # Sprint Backlog (drie-paneel scherm)
│ │ │ └── planning/page.tsx # Redirect → /sprint
│ │ ├── solo/page.tsx # Solo board (top-level; Kanban per ingelogde gebruiker)
│ │ ├── ideas/ # Idea-laag (M12) — vervangt vroegere /todos
│ │ │ ├── page.tsx
│ │ │ └── [id]/page.tsx
│ │ ├── jobs/page.tsx # Job-queue inzicht (PBI-59)
│ │ ├── insights/page.tsx # Tokenkosten + run-statistieken
│ │ ├── manual/ # In-app developer manual (PBI-58)
│ │ │ ├── layout.tsx
│ │ │ ├── _components/
│ │ │ └── [[...slug]]/page.tsx # Catch-all route voor alle manual-secties
│ │ ├── admin/ # Admin-only schermen
│ │ └── settings/
│ │ ├── page.tsx # Profiel, account, PB-overzicht, rollen, tokens
│ │ └── tokens/page.tsx
@ -41,22 +50,21 @@ scrum4me/
│ │ └── products/[id]/
│ │ ├── page.tsx # Mobile Product Backlog (tab-mode op <1024px)
│ │ └── solo/page.tsx # Mobile Solo (3-koloms-kanban)
│ ├── 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
│ ├── api/ # REST API voor Claude Code en interne SSE
│ │ ├── auth/pair/ # QR-pairing endpoints (start/claim/stream)
│ │ ├── cron/ # cleanup-agent-artifacts, expire-questions
│ │ ├── debug/ # emit-test-notify, realtime-stream (dev-only)
│ │ ├── health/route.ts # Liveness + ?db=1 ping
│ │ ├── ideas/ # Idea CRUD + grill/plan trigger
│ │ ├── internal/push/ # Web-push send + test-send (INTERNAL_PUSH_SECRET)
│ │ ├── jobs/[id]/sub-tasks/ # SprintTaskExecution-rows uitlezen
│ │ ├── products/[id]/ # next-story, claude-context
│ │ ├── profile/avatar/route.ts # POST upload + GET serve profielfoto
│ │ ├── realtime/ # SSE-streams: backlog, jobs, notifications, solo
│ │ ├── sprints/[id]/tasks/ # GET sprint-taken
│ │ ├── stories/[id]/ # log + tasks/reorder
│ │ ├── tasks/[id]/route.ts # PATCH status + implementation_plan
│ │ └── users/[id]/avatar/ # GET avatar van een specifieke user
├── components/
│ ├── ui/ # shadcn/ui primitieven
│ ├── split-pane/ # Gesplitst scherm component
@ -65,7 +73,9 @@ scrum4me/
│ ├── products/ # ProductForm, TeamManager, ArchiveProductButton
│ ├── settings/ # RoleManager, ProfileEditor, LeaveProductButton
│ ├── mobile/ # LandscapeGuard, MobileTabBar, LogoutButton
│ └── dnd/ # dnd-kit wrappers
│ ├── ideas/ # Idea-detail, grill-chat, plan-preview
│ ├── jobs/ # Job-card, filter-popover, view-switch
│ └── dnd/ # dnd-kit wrappers
├── lib/
│ ├── prisma.ts # Prisma Client singleton
│ ├── session.ts # iron-session configuratie

View file

@ -3,7 +3,7 @@ title: "Scrum4Me — Glossary"
status: active
audience: [ai-agent, contributor]
language: en
last_updated: 2026-05-03
last_updated: 2026-05-08
when_to_read: "When you encounter a domain term and need its canonical definition and the doc where it is specified."
---
@ -15,6 +15,10 @@ Domain terms used across Scrum4Me docs, code, and MCP tooling.
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).
## ClaudeJob
A row in the `claude_jobs` table — the queue from which agents claim work atomically (`FOR UPDATE SKIP LOCKED`). Distinguished by `kind` (`TASK_IMPLEMENTATION`, `IDEA_GRILL`, `IDEA_MAKE_PLAN`, `PLAN_CHAT`, `SPRINT_IMPLEMENTATION`) and tracked through `status` (`QUEUED → CLAIMED → RUNNING → DONE/FAILED/CANCELLED/SKIPPED`). See [data-model](./architecture/data-model.md) and [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).
@ -27,13 +31,17 @@ An API token whose owning user has `isDemo = true`. All write operations are blo
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).
## Idea
A user-scoped idea captured before it becomes a PBI. An idea is first _grilled_ via an interactive Q&A loop (`IDEA_GRILL` job → `grill_md`), then materialized through a deterministic plan (`IDEA_MAKE_PLAN` job → `plan_md`) into a PBI with stories and tasks. Replaced the legacy `todos` table in M12. Status enum: `DRAFT | GRILLING | GRILL_FAILED | GRILLED | PLANNING | PLAN_FAILED | PLAN_READY | PLANNED`. See [data-model](./architecture/data-model.md) and the [M12 plan](./plans/M12-ideas.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).
Synonym for **ClaudeJob** — used in agent-facing docs because Claude Code consumes the queue through MCP tools. 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).
The second level of the work hierarchy: `Product → PBI → Story → Task`. A PBI groups related stories under a single theme or capability. Status enum: `READY | BLOCKED | FAILED | DONE`. Has a stable `code` (`PBI-N`) per product. Do not use "Epic", "Feature", or "Issue" as synonyms. See [backlog index](./backlog/index.md).
## Solo Panel
@ -41,16 +49,20 @@ The single-user planning screen that shows all PBIs and stories for one product
## 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).
A time-boxed iteration with a `sprint_goal` and a stable `code` (`SP-N`) per product. A product can hold multiple sprints simultaneously (PBI-63) — `OPEN` is not exclusive; the sprint-switcher in the product header lets the user pick which sprint to plan against. Status enum: `OPEN | CLOSED | ARCHIVED | FAILED`. Stories enter `IN_SPRINT` when added to a sprint via `sprint_id`. See [data-model](./architecture/data-model.md).
## SprintRun
A single execution of the SPRINT_IMPLEMENTATION flow against one Sprint. Tracked in `sprint_runs` with status `QUEUED | RUNNING | PAUSED | DONE | FAILED | CANCELLED`, optional `pause_context` for resume, and a chain via `previous_run_id` for retries. The frozen scope-snapshot per run lives in `sprint_task_executions`. See [sprint execution modes](./architecture/sprint-execution-modes.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).
The third level of the work hierarchy: `Product → PBI → Story → Task`. A Story has acceptance criteria, an optional `assignee_id` (Solo-bord claim), a stable `code` (`ST-N`) per product, and a status (`OPEN | IN_SPRINT | DONE | FAILED`). 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).
The leaf level of the work hierarchy: `Product → PBI → Story → Task`. A Task has an `implementation_plan`, a stable `code` (`T-N`) per product, a `status` (`TO_DO | IN_PROGRESS | REVIEW | DONE | FAILED | EXCLUDED`), `verify_only` and `verify_required` flags, and an optional `repo_url` override. API exposes status as lowercase (`todo | in_progress | review | done | failed | excluded`). See [data-model](./architecture/data-model.md) and [ADR-0004](./adr/0004-status-enum-mapping.md).
## Todo
## verify_result
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).
The agent's outcome of a verification pass (`ALIGNED | PARTIAL | EMPTY | DIVERGENT`). Combined with the task's `verify_required` threshold (`ALIGNED | ALIGNED_OR_PARTIAL | ANY`) it determines whether a job's claim of "done" is accepted by the gate. See [agent-flow-pitfalls runbook](./runbooks/agent-flow-pitfalls.md).

View file

@ -3,13 +3,13 @@ title: "Entity Dialog"
status: active
audience: [ai-agent, contributor]
language: nl
last_updated: 2026-05-03
last_updated: 2026-05-08
when_to_read: "Before building any create/edit/detail dialog component."
---
# 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, Idea, 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.

View file

@ -3,7 +3,7 @@ title: "Proxy (route protection)"
status: active
audience: [ai-agent, contributor]
language: nl
last_updated: 2026-05-03
last_updated: 2026-05-08
when_to_read: "When adding or modifying route-level access control in proxy.ts."
---
@ -17,7 +17,16 @@ import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
import { sessionOptions } from '@/lib/session'
const protectedRoutes = ['/dashboard', '/products', '/todos', '/settings']
const protectedRoutes = [
'/dashboard',
'/products',
'/ideas',
'/solo',
'/jobs',
'/insights',
'/manual',
'/settings',
]
const authRoutes = ['/login', '/register']
export function proxy(request: NextRequest) {

View file

@ -3,7 +3,7 @@ title: "Route Handler (REST API)"
status: active
audience: [ai-agent, contributor]
language: nl
last_updated: 2026-05-03
last_updated: 2026-05-08
when_to_read: "When writing a new Next.js route handler (GET/POST/PATCH/DELETE)."
---
@ -59,7 +59,7 @@ export async function GET(
const { id } = await params
const sprint = await prisma.sprint.findFirst({
where: { product_id: id, status: 'ACTIVE', product: productAccessFilter(auth.userId) },
where: { product_id: id, status: 'OPEN', product: productAccessFilter(auth.userId) },
})
if (!sprint) {
return Response.json({ error: 'Geen actieve Sprint gevonden' }, { status: 404 })
@ -91,13 +91,16 @@ export async function GET(
| Methode | Endpoint | Doel |
|---|---|---|
| GET | `/api/health` | Liveness; `?db=1` voor DB-ping (geen auth) |
| GET | `/api/products` | Actieve producten ophalen |
| GET | `/api/products/:id/next-story` | Hoogst geprioriteerde open story |
| GET | `/api/products/:id/claude-context` | Bundled MCP-context |
| GET | `/api/sprints/:id/tasks?limit=10` | Eerste N taken van de Sprint |
| PATCH | `/api/stories/:id/tasks/reorder` | Taakvolgorde aanpassen |
| POST | `/api/stories/:id/log` | Plan / testresultaat / commit vastleggen |
| PATCH | `/api/tasks/:id` | Taakstatus bijwerken |
| POST | `/api/todos` | Todo aanmaken |
| PATCH | `/api/tasks/:id` | Taakstatus / `implementation_plan` bijwerken |
| GET / POST | `/api/ideas`, `GET / PATCH /api/ideas/:id` | Idea CRUD (vervangt voormalig `/api/todos`) |
| GET | `/api/jobs/:id/sub-tasks` | `sprint_task_executions` van een SPRINT_IMPLEMENTATION-job |
## Security-invarianten

View file

@ -3,7 +3,7 @@ title: "Server Action"
status: active
audience: [ai-agent, contributor]
language: nl
last_updated: 2026-05-03
last_updated: 2026-05-08
when_to_read: "When writing a new server action with auth and Zod validation."
---
@ -66,7 +66,7 @@ export async function createPbi(formData: FormData) {
- Controleer auth en `session.isDemo` voordat er geschreven wordt.
- Gebruik `productAccessFilter(userId)` voor resources waar eigenaar en gekoppelde Developer beide toegang hebben.
- Gebruik eigenaar-only filters (`user_id: session.userId`) alleen voor eigenaarsacties zoals product archiveren, teamleden beheren of persoonlijke todos.
- Gebruik eigenaar-only filters (`user_id: session.userId`) alleen voor eigenaarsacties zoals product archiveren, teamleden beheren of persoonlijke ideas.
- Vertrouw nooit losse client-ID's. Als een action meerdere IDs ontvangt, haal ze eerst op met `id in (...)` plus de parent-scope en weiger de operatie als het aantal gevonden records niet exact gelijk is.
- Weiger dubbele IDs in reorder-lijsten of beslissingsobjecten.
- Leid denormalized foreign keys af uit de database-parent. Voorbeeld: gebruik `pbi.product_id` bij story creation, niet `formData.get('productId')`.

View file

@ -3,7 +3,7 @@ title: "MCP Integration — Scrum4Me Tools"
status: active
audience: [ai-agent]
language: nl
last_updated: 2026-05-03
last_updated: 2026-05-08
when_to_read: "When using MCP tools to interact with the Scrum4Me backlog."
---
@ -16,13 +16,14 @@ Scrum4Me heeft een eigen MCP-server in repo [`madhura68/scrum4me-mcp`](https://g
**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
- `mcp__scrum4me__get_claude_context` — bundled product / actieve sprint (`status='OPEN'`) / next story (met tasks) / open ideas
**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)
> Idea-aanmaak loopt niet via MCP maar via de UI of `POST /api/ideas`. De voormalige `create_todo`-tool is verwijderd; idea-mutaties gaan via de Idea-tools onder *Idea-laag (M12)* hieronder.
**Task / story writes:**
- `mcp__scrum4me__update_task_status`, `mcp__scrum4me__update_task_plan`

View file

@ -3,7 +3,7 @@ title: "Scrum4Me — Functionele Specificatie"
status: active
audience: [maintainer, contributor]
language: nl
last_updated: 2026-05-04
last_updated: 2026-05-08
---
# Scrum4Me — Functionele Specificatie
@ -322,31 +322,30 @@ Elke story heeft een activiteitenlog die alle door Claude Code vastgelegde stapp
---
### F-08: Todo-lijst
### F-08: Ideeën-laag (Idea capture, grill, plan)
**Prioriteit:** v1 — Hoog
**Prioriteit:** v1 — Hoog (M12 — vervangt Todo-lijst)
**Persona:** Lars (snelle vastlegging), Dina (losse klantnotities)
**Omschrijving:**
Een snelle todo-lijst voor taken die aan een specifiek product zijn gekoppeld. Todo-items kunnen worden afgevinkt en gepromoveerd naar een PBI of story in dat product. Zowel de UI als de REST API vereisen een `product_id` bij aanmaken — zodat Claude Code altijd werkt binnen de context van de actieve product backlog.
Een idea is een gestructureerde voorganger van een PBI. De gebruiker maakt een idea snel aan (titel + optioneel product). Een agent _grilt_ het idea via een interactieve Q&A-loop (`IDEA_GRILL`-job → `grill_md`); daarna materialiseert een tweede agent het idea in een deterministisch plan (`IDEA_MAKE_PLAN`-job → `plan_md`) dat parsed wordt naar PBI + stories + tasks. De `todos`-tabel uit eerdere versies is per migratie ST-1239 gedropt en volledig vervangen door `ideas`.
**Acceptatiecriteria:**
- [ ] Todo aanmaken via snel-invoerveld (Enter om op te slaan); product (dropdown) en titel verplicht
- [ ] Todo aanmaken ook mogelijk via REST API: `POST /api/todos` (body: `{ "title": string, "product_id": string }`) — zodat Claude Code bevindingen kan vastleggen binnen de actieve product backlog
- [ ] Todo-lijst is zichtbaar als apart scherm of persistent zijpaneel
- [ ] Todo afvinken markeert het als afgerond (visueel doorgestreept)
- [ ] Afgevinkte todo's blijven zichtbaar; kunnen worden gearchiveerd via "Archiveer afgeronde items"
- [ ] Todo promoveren naar PBI: dialoog pre-selecteert het gekoppelde product (bewerkbaar), vraagt prioriteit; todo verdwijnt na promotie
- [ ] Todo promoveren naar story: dialoog pre-selecteert het gekoppelde product (bewerkbaar), vraagt PBI en prioriteit; todo verdwijnt na promotie
- [ ] Titel van het todo-item is vooringevuld in de promotiedialoog (bewerkbaar)
- [ ] Promotie is niet ongedaan te maken; dialoog waarschuwt hiervoor
- [ ] Idea aanmaken via snel-invoerveld (Enter om op te slaan); titel verplicht, product optioneel
- [ ] Idea-lijst toont items per status (`DRAFT`, `GRILLING`, `GRILLED`, `PLAN_READY`, `PLANNED`, …)
- [ ] Per idea kan de gebruiker een grill-job starten — agent stelt vragen via het claude-question-kanaal
- [ ] Per idea kan de gebruiker (na succesvol grillen) een make-plan-job starten — agent produceert strict yaml-frontmatter; parse-fail = `PLAN_FAILED`
- [ ] `PLAN_READY` toont de gegenereerde PBI/story/task-structuur als preview; bevestigen materialiseert ze in de productbacklog en zet de idea op `PLANNED`
- [ ] Idea's zijn user-private (geen `productAccessFilter`); secundaire producten via `idea_products`
- [ ] Demo-gebruiker kan idea's lezen maar niet schrijven of grillen
**Randgevallen:**
- Geen producten aangemaakt → promotie-dialoog toont melding "Maak eerst een product aan"
- Promoveren naar story zonder PBI's in het product → dialoog toont melding "Maak eerst een PBI aan"
- Idea heeft geen primair product → make-plan vraagt eerst om producttoekenning voordat materialisatie kan
- Grill-job time-out / agent crash → status valt terug naar `GRILL_FAILED`; gebruiker kan opnieuw grillen
- Plan-output past niet in het strict yaml-format → `PLAN_FAILED` + `IdeaLog{JOB_EVENT}` met de parse-error
**Data:**
- Opgeslagen: `todos` (id, user_id, product_id, title, done, archived, created_at, updated_at)
- Opgeslagen: `ideas`, `idea_products`, `idea_logs`, `user_questions` — zie [data-model](../architecture/data-model.md) en het [M12 plan](../plans/M12-ideas.md). Het profiel voor de IdeaDialog staat in [docs/specs/dialogs/idea.md](./dialogs/idea.md).
---
@ -356,12 +355,13 @@ Een snelle todo-lijst voor taken die aan een specifiek product zijn gekoppeld. T
**Persona:** Lars, Dina, Remi
**Omschrijving:**
Het Scrum Team kan een Sprint aanmaken met een Sprint Goal. Per product kan er één actieve Sprint zijn. Stories worden via een gesplitst scherm vanuit de Product Backlog naar de Sprint Backlog gesleept.
Het Scrum Team kan meerdere Sprints per product aanmaken (PBI-63), elk met een eigen Sprint Goal en stabiele `code` (`SP-N`). Een sprint-switcher in de product-header schakelt tussen sprints. Stories worden via een gesplitst scherm vanuit de Product Backlog naar de Sprint Backlog gesleept.
**Acceptatiecriteria:**
- [ ] Sprint aanmaken vereist een Sprint Goal (verplicht, max. 500 tekens)
- [ ] Sprint is gekoppeld aan een product
- [ ] Er kan maar één actieve Sprint per product tegelijk zijn
- [ ] Sprint is gekoppeld aan een product en krijgt automatisch een `code` (`SP-1`, `SP-2`, …) sequentieel per product
- [ ] Een product mag tegelijk meerdere `OPEN`-sprints hebben; de sprint-switcher in de product-header bepaalt welke actief is in de UI
- [ ] Optionele `start_date` en `end_date` op een sprint (puur planningsmetadata)
- [ ] Sprint Backlog scherm is gesplitst: Sprint Backlog links, stories per PBI rechts
- [ ] Rechterpaneel toont alle PBI's inklapbaar, met hun stories eronder
- [ ] Stories die al in de Sprint zitten zijn visueel gemarkeerd en niet opnieuw sleepbaar
@ -369,14 +369,14 @@ Het Scrum Team kan een Sprint aanmaken met een Sprint Goal. Per product kan er
- [ ] Story in de Sprint Backlog is herrangschikbaar via drag-and-drop
- [ ] Story uit Sprint verwijderen via contextmenu of verwijderknop → story keert terug in Product Backlog
- [ ] Sprint Goal is bewerkbaar na aanmaken
- [ ] Sprint afronden zet alle stories op DONE of terug op OPEN (keuze per story in afronden-dialoog)
- [ ] Sprint afronden zet status op `CLOSED` en past stories aan volgens de afsluit-keuze (DONE of terug naar OPEN, per story in afronden-dialoog)
**Randgevallen:**
- Gebruiker probeert tweede Sprint aan te maken terwijl er al een actieve Sprint is → foutmelding met link naar actieve Sprint
- Story wordt uit Sprint verwijderd terwijl er taken aan hangen → taken blijven bestaan maar worden losgekoppeld van de Sprint
- Sprint wordt afgerond met openstaande stories → afsluit-dialoog dwingt een keuze per story; geen impliciete defaults
**Data:**
- Opgeslagen: `sprints` (id, product_id, sprint_goal, status (ACTIVE | COMPLETED), created_at, completed_at?)
- Opgeslagen: `sprints` (id, product_id, code, sprint_goal, status (`OPEN | CLOSED | ARCHIVED | FAILED`), start_date?, end_date?, created_at, completed_at?). Voor uitvoering door agents zie ook `sprint_runs` + `sprint_task_executions` in [data-model](../architecture/data-model.md).
---
@ -395,16 +395,16 @@ In het Sprint Planning scherm worden stories uit de Sprint Backlog opgedeeld in
- [ ] Omschrijving is optioneel (max. 1000 tekens)
- [ ] Prioriteit is verplicht (14)
- [ ] Taken zijn gerangschikt op prioriteit en volgorde; volgorde instelbaar via drag-and-drop (dnd-kit)
- [ ] Taakstatus is instelbaar via de UI: TO_DO | IN_PROGRESS | DONE
- [ ] Story toont een voortgangsindicator (bijv. "2/5 taken Done")
- [ ] Taakstatus is instelbaar via de UI: `TO_DO | IN_PROGRESS | REVIEW | DONE` (plus `FAILED` en `EXCLUDED` gezet door agent-flows; zie `lib/task-status.ts`)
- [ ] Story toont een voortgangsindicator (bijv. "2/5 taken Done"); auto-promotie van story naar DONE wanneer alle tasks DONE zijn
- [ ] Taak verwijderen vereist bevestiging
**Randgevallen:**
- Story heeft geen taken → lege staat rechts met prompt om eerste taak aan te maken
- Alle taken van een story zijn Done → story-voortgang toont 100% maar story-status wijzigt niet automatisch
- Alle taken van een story zijn Done → story promoot automatisch naar DONE in dezelfde transactie
**Data:**
- Opgeslagen: `tasks` (id, story_id, sprint_id, title, description, priority (14), sort_order, status (TO_DO | IN_PROGRESS | DONE), created_at, updated_at)
- Opgeslagen: `tasks` (id, story_id, product_id, sprint_id?, code, title, description?, implementation_plan?, priority (14), sort_order, status (`TO_DO | IN_PROGRESS | REVIEW | DONE | FAILED | EXCLUDED`), verify_only, verify_required, repo_url?, created_at, updated_at)
---
@ -419,13 +419,16 @@ Een REST API waarmee Claude Code stories en taken kan ophalen, de taakvolgorde k
**Acceptatiecriteria:**
**Endpoints:**
- [ ] `GET /api/health` — liveness, optioneel `?db=1` voor DB-ping (geen auth)
- [ ] `GET /api/products` — lijst van actieve producten waarvoor de tokengebruiker eigenaar of teamlid is
- [ ] `GET /api/products/:id/next-story` — hoogst geprioriteerde open story van de actieve Sprint
- [ ] `GET /api/products/:id/next-story` — hoogst geprioriteerde open story van de actieve sprint
- [ ] `GET /api/products/:id/claude-context` — bundled context (product / sprint / story / tasks) voor MCP
- [ ] `GET /api/sprints/:id/tasks?limit=10` — eerste N taken in huidige volgorde
- [ ] `PATCH /api/stories/:id/tasks/reorder` — accepteert geordende lijst van taak-id's
- [ ] `POST /api/stories/:id/log` — vastleggen van implementatieplan, testresultaat of commit
- [ ] `PATCH /api/tasks/:id` — status bijwerken (TO_DO → IN_PROGRESS → DONE) en/of `implementation_plan` opslaan
- [ ] `POST /api/todos` — todo aanmaken vanuit Claude Code (body: `{ "title": string, "product_id": string }`)
- [ ] `PATCH /api/tasks/:id` — status bijwerken (`todo → in_progress → review → done`) en/of `implementation_plan` opslaan
- [ ] `GET / POST /api/ideas` en `GET / PATCH /api/ideas/:id` — idea CRUD (vervangt voormalig `POST /api/todos`)
- [ ] `GET /api/jobs/:id/sub-tasks` — sprint-task-executions van een SPRINT_IMPLEMENTATION-job
**Authenticatie:**
- [ ] Alle endpoints vereisen `Authorization: Bearer <token>` header
@ -529,9 +532,14 @@ De app is deployable op Vercel + Neon PostgreSQL en lokaal draaibaar met een Neo
/dashboard (productenlijst)
/products/new (product aanmaken)
/products/:id (Product Backlog — gesplitst scherm)
/products/:id/sprint (Sprint Backlog — gesplitst scherm)
/products/:id/sprint (Sprint Backlog — gesplitst scherm; sprint-switcher in product-header)
/products/:id/sprint/planning (Sprint Planning — gesplitst scherm)
/todos (todo-lijst)
/solo (Solo board — Kanban per ingelogde gebruiker, top-level)
/ideas (Idea-laag, vervangt voormalige /todos)
/ideas/:id (Idea-detail met grill / make-plan)
/jobs (Job-queue inzicht)
/insights (Tokenkosten + run-statistieken)
/manual (In-app developer manual)
/settings (profiel, account, product backlogs, rollen, API-tokens)
/settings/tokens (API-tokenbeheer)
@ -599,19 +607,29 @@ Telefoon-gebruikers (UA met `Mobi`-substring) krijgen een minimale mobile-shell
## Datamodel (schets)
> Volledige tabeldefinities staan in [data-model](../architecture/data-model.md). Onderstaande tabel is een korte schets per entiteit.
| Entiteit | Sleutelvelden | Relaties / opmerkingen |
|---|---|---|
| `users` | id, username, password_hash, is_demo, bio?, bio_detail?, avatar_data?, created_at | Profielvelden optioneel; avatar opgeslagen als WebP bytea |
| `user_roles` | id, user_id, role (enum) | Meervoudige rollen per gebruiker |
| `api_tokens` | id, user_id, token_hash, label, revoked_at | Max. 10 actief per gebruiker |
| `products` | id, user_id, name, description, repo_url, definition_of_done, archived | Hoogste niveau in de hiërarchie |
| `pbis` | id, product_id, title, description, priority (14), sort_order | Geordend binnen prioriteitsgroep |
| `stories` | id, pbi_id, product_id, title, description, acceptance_criteria, priority, sort_order, status, sprint_id? | Status: OPEN / IN_SPRINT / DONE |
| `story_logs` | id, story_id, type, content, status?, commit_hash?, commit_message?, created_at | Aangemaakt via API; read-only in UI |
| `sprints` | id, product_id, sprint_goal, status (ACTIVE / COMPLETED), created_at, completed_at? | Max. 1 actieve Sprint per product |
| `tasks` | id, story_id, sprint_id, title, description, implementation_plan?, priority, sort_order, status | Status: TO_DO / IN_PROGRESS / DONE; implementation_plan door MCP |
| `todos` | id, user_id, product_id, title, done, archived, created_at | Gekoppeld aan product backlog; verplicht in UI en API |
| `product_members` | id, product_id, user_id, created_at | Many-to-many; alleen Developers; eigenaar via products.user_id |
| `users` | id, username, email?, password_hash, is_demo, must_reset_password, active_product_id?, idea_code_counter, min_quota_pct, bio?, bio_detail?, avatar_data?, created_at | Profielvelden optioneel; avatar als WebP bytea |
| `user_roles` | id, user_id, role (`PRODUCT_OWNER \| SCRUM_MASTER \| DEVELOPER \| ADMIN`) | Meervoudige rollen per gebruiker |
| `api_tokens` | id, user_id, token_hash, label, revoked_at | Max. 10 actief per gebruiker; gekoppeld aan max. 1 ClaudeWorker |
| `products` | id, user_id, name, code?, description, repo_url, definition_of_done, auto_pr, pr_strategy, archived | Hoogste niveau; eigenaar + members |
| `pbis` | id, product_id, code, title, description, priority (14), sort_order, status (`READY \| BLOCKED \| FAILED \| DONE`), pr_url?, pr_merged_at? | Geordend binnen prioriteitsgroep; auto-DONE bij sprint-close |
| `stories` | id, pbi_id, product_id, sprint_id?, assignee_id?, code, title, description, acceptance_criteria, priority, sort_order, status (`OPEN \| IN_SPRINT \| DONE \| FAILED`) | Auto-promotie als alle tasks DONE |
| `story_logs` | id, story_id, type (`IMPLEMENTATION_PLAN \| TEST_RESULT \| COMMIT`), content, status?, commit_hash?, commit_message?, metadata?, created_at | Aangemaakt via API; read-only in UI |
| `sprints` | id, product_id, code, sprint_goal, status (`OPEN \| CLOSED \| ARCHIVED \| FAILED`), start_date?, end_date?, created_at, completed_at? | Meerdere sprints per product (PBI-63) |
| `sprint_runs` | id, sprint_id, started_by_id, status (`QUEUED \| RUNNING \| PAUSED \| DONE \| FAILED \| CANCELLED`), pr_strategy, branch?, pr_url?, pause_context?, previous_run_id? | Eén run per uitvoering; chained retries |
| `tasks` | id, story_id, product_id, sprint_id?, code, title, description?, implementation_plan?, priority, sort_order, status (`TO_DO \| IN_PROGRESS \| REVIEW \| DONE \| FAILED \| EXCLUDED`), verify_only, verify_required, repo_url? | `code` blijft stabiel bij re-parenting |
| `claude_jobs` | id, user_id, product_id, task_id?, idea_id?, sprint_run_id?, kind, status, claimed_by_token_id?, model_id?, tokens, plan_snapshot?, base/head_sha?, branch?, pr_url?, summary?, error?, retry_count, lease_until? | Job-queue voor agents |
| `sprint_task_executions` | id, sprint_job_id, task_id, order, plan_snapshot, verify_required_snapshot, verify_only_snapshot, status (`PENDING \| RUNNING \| DONE \| FAILED \| SKIPPED`), verify_result?, verify_summary?, skip_reason? | Bevroren scope per SPRINT_IMPLEMENTATION-claim |
| `claude_workers` | id, user_id, token_id (unique), product_id?, started_at, last_seen_at, last_quota_pct?, last_quota_check_at? | Live-presence per actieve agent |
| `model_prices` | id, model_id (unique), input/output/cache_read/cache_write_price_per_1m, currency | Prijslookup voor jobs-pagina |
| `ideas` / `idea_products` / `idea_logs` / `user_questions` | zie data-model | Idea-laag (M12); vervangt voormalige `todos` |
| `claude_questions` | id, story_id?, task_id?, idea_id?, product_id, asked_by, question, options?, status, answer?, answered_by?, answered_at?, created_at, expires_at | Agent ↔ user vraag-kanaal (M11) |
| `login_pairings` | id, secret_hash, desktop_token_hash, status, user_id?, desktop_ua?, desktop_ip?, expires_at, approved_at?, consumed_at? | QR-pairing-flow (M10) |
| `push_subscriptions` | id, user_id, endpoint (unique), p256dh, auth, user_agent?, last_used_at | Web-push subscriptions |
| `product_members` | id, product_id, user_id, created_at | Many-to-many; alleen Developers; eigenaar via `products.user_id` |
---
@ -672,17 +690,18 @@ Telefoon-gebruikers (UA met `Mobi`-substring) krijgen een minimale mobile-shell
---
### Flow 3: Todo promoveren naar story
### Flow 3: Idea grillen en materialiseren
**Startpunt:** Todo-lijst
1. Lars heeft een todo: "Voeg rate limiting toe aan de API"
2. Hij klikt op "Promoveren → Story"
3. Dialoog opent: product (vooringevuld met laatste product), PBI (dropdown), prioriteit
4. Hij kiest product "Factuur-tool", PBI "Beveiliging", prioriteit 2
5. Bevestigen → todo verdwijnt, story is aangemaakt
6. Lars navigeert naar de Product Backlog → story staat in de juiste prioriteitsgroep
**Startpunt:** Idea-lijst (`/ideas`)
1. Lars maakt een idea aan: "Voeg rate limiting toe aan de API"
2. Hij koppelt het aan product "Factuur-tool" en start een grill-job
3. Een agent claimt de `IDEA_GRILL`-job en stelt vragen via het claude-question-kanaal — Lars antwoordt in de UI
4. Agent eindigt met `update_idea_grill_md`; status → `GRILLED`
5. Lars start een make-plan-job; agent produceert strict yaml-frontmatter
6. Status → `PLAN_READY`; Lars bekijkt de preview (PBI + stories + tasks)
7. Bevestigen materialiseert de structuur in de Product Backlog; idea-status → `PLANNED`
**Resultaat:** Losse gedachte is in drie stappen onderdeel van de formele Product Backlog.
**Resultaat:** Een ruwe gedachte wordt via één gestructureerde dialoog een complete PBI-tak met stories en tasks — zonder handmatig opnieuw te tikken.
---
@ -696,9 +715,9 @@ Een gebruiker kan één product als "actief" markeren. Dit actieve product wordt
- **Producten** — altijd bereikbaar, toont alle producten van de gebruiker
- **Product Backlog** — alleen klikbaar als er een actief product is
- **Sprint** — alleen klikbaar als er een actief product is én een actieve sprint bestaat; anders tooltip "Geen actieve sprint"
- **Sprint** — alleen klikbaar als er een actief product is én minimaal één sprint bestaat; sprint-switcher in de product-header bepaalt welke
- **Solo** — alleen klikbaar als er een actief product is
- **Todo's** — altijd bereikbaar
- **Ideeën** — altijd bereikbaar (vervangt voormalig "Todo's")
In het midden van de NavBar staat een dropdown met de naam van het actieve product. Via deze dropdown kan de gebruiker wisselen tussen producten of naar "Producten beheren" navigeren.
@ -921,7 +940,7 @@ export async function claimAllUnassignedInActiveSprintAction(
const session = await requireProductWriter(productId)
const activeSprint = await prisma.sprint.findFirst({
where: { product_id: productId, status: 'ACTIVE' },
where: { product_id: productId, status: 'OPEN' },
select: { id: true },
})
if (!activeSprint) throw new Error('Geen actieve sprint gevonden')
@ -1075,7 +1094,7 @@ export default async function SoloPage({
if (!product) notFound()
const activeSprint = await prisma.sprint.findFirst({
where: { product_id: id, status: 'ACTIVE' },
where: { product_id: id, status: 'OPEN' },
select: { id: true, sprint_goal: true },
})
if (!activeSprint) return <NoActiveSprint product={product} />
@ -1353,7 +1372,7 @@ Inhoud:
### 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).
Geen OPEN sprint: nette empty-state met titel, korte uitleg en link naar productpagina om er een te starten (ST-302 stappen).
---
@ -1413,7 +1432,7 @@ Eenvoudig nu we weten dat `isDemo` in de sessiecookie zit:
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
- Een OPEN 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)
@ -1428,7 +1447,7 @@ Eenvoudig nu we weten dat `isDemo` in de sessiecookie zit:
</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.
Plek: tussen "Producten" en "Ideeën" (of zoals layout het bepaalt). Altijd zichtbaar voor ingelogde users — geen product-context nodig, die kiest de redirect-handler zelf.
---