docs(cleanup): registreer handmatige verplaatsingen en fix referenties
- 4 plans verplaatst naar docs/old/plans/ (M10-qr-pairing-login, auto-pr-deploy-sync, docs-restructure-ai-lookup, v1-readiness) - 3 archive-plans verplaatst naar docs/old/plans/ (archive-map nu leeg) - ST-1114-copilot-reviews + 3 research-docs naar nieuwe docs/Ideas/ map - Duplicaat docs/old/2026-04-27-m8-realtime-solo.md verwijderd (origineel zit in docs/old/plans/) - Link-fixes naar nieuwe locaties: - CHANGELOG.md → docs/old/plans/v1-readiness.md - docs/runbooks/deploy-control.md → docs/old/plans/auto-pr-deploy-sync.md (2x) - docs/runbooks/worker-idempotency.md → docs/old/plans/auto-pr-deploy-sync.md - docs/plans/docs-restructure-pbi-spec.md → docs/old/plans/docs-restructure-ai-lookup.md (4x text + 2x href) - docs/INDEX.md geregenereerd (96 docs, was 100) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
0a265b96eb
commit
92e7e0c2fa
16 changed files with 1058 additions and 21 deletions
150
docs/old/plans/2026-04-27-claude-md-workflow-update.md
Normal file
150
docs/old/plans/2026-04-27-claude-md-workflow-update.md
Normal 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
|
||||
111
docs/old/plans/2026-04-27-insert-milestone-tool.md
Normal file
111
docs/old/plans/2026-04-27-insert-milestone-tool.md
Normal 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
|
||||
195
docs/old/plans/2026-04-27-m8-realtime-solo.md
Normal file
195
docs/old/plans/2026-04-27-m8-realtime-solo.md
Normal 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 2–3s | ✅ | nee | 1–3s | 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 1–2s 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, ~12–18 commits
|
||||
- 1 migratie, 1 nieuwe route, 1 nieuwe hook, kleine store-uitbreiding, UI-indicator
|
||||
- ~400 regels code + ~80 regels docs
|
||||
- 1 PR
|
||||
894
docs/old/plans/M10-qr-pairing-login.md
Normal file
894
docs/old/plans/M10-qr-pairing-login.md
Normal file
|
|
@ -0,0 +1,894 @@
|
|||
---
|
||||
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
|
||||
|
||||
Inloggen op een (publieke) desktop zonder wachtwoord: desktop toont QR, telefoon (al-ingelogd) scant en bevestigt expliciet, desktop is binnen 1–2 s ingelogd. Bouwt voort op M8 LISTEN/NOTIFY-infra met eigen channel `scrum4me_pairing`.
|
||||
|
||||
**Beveiligingsuitgangspunt:** geheim materiaal nooit in URL-paden, querystrings, access logs of browsergeschiedenis.
|
||||
- `mobileSecret` reist alleen via QR-fragment (`#s=…`) → mobile `location.hash` → POST-body
|
||||
- `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`)
|
||||
|
||||
Backlog-entries: zie [backlog.md § M10](../backlog/index.md#m10-password-loze-inlog-via-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):
|
||||
|
||||
1. **DB-laag** — ST-1001 (schema + trigger)
|
||||
2. **Auth helpers + sessie-uitbreiding** — ST-1002
|
||||
3. **API-laag** — ST-1003 (start), ST-1004 (SSE), ST-1006 (claim)
|
||||
4. **Server actions + mobile UI** — ST-1005
|
||||
5. **Desktop UI** — ST-1007
|
||||
6. **Documentatie + acceptatietest** — ST-1008
|
||||
|
||||
ST-1006 staat bij de API-laag (niet bij UI) omdat het een Route Handler is; ST-1005 levert tegelijk de server actions en de mobiele bevestigingspagina omdat die strak gekoppeld zijn.
|
||||
|
||||
---
|
||||
|
||||
## ST-1001 — LoginPairing schema + Postgres-trigger
|
||||
|
||||
**Bestanden**
|
||||
- `prisma/schema.prisma` — nieuw model `LoginPairing` + back-relation op `User`
|
||||
- `prisma/migrations/<timestamp>_add_login_pairing/migration.sql` — model + trigger
|
||||
- `vendor/scrum4me`-submodule in repo `mcp` — schema-sync ná merge
|
||||
|
||||
**Stappen**
|
||||
|
||||
1. **Schema-uitbreiding**:
|
||||
|
||||
```prisma
|
||||
model LoginPairing {
|
||||
id String @id @default(cuid())
|
||||
secret_hash String // sha256 hex van mobileSecret
|
||||
desktop_token_hash String // sha256 hex van desktopToken (HttpOnly cookie)
|
||||
status String // 'pending' | 'approved' | 'consumed' | 'cancelled'
|
||||
user_id String?
|
||||
user User? @relation(fields: [user_id], references: [id], onDelete: SetNull)
|
||||
desktop_ua String? @db.VarChar(255)
|
||||
desktop_ip String? @db.VarChar(45) // IPv6 max
|
||||
created_at DateTime @default(now())
|
||||
expires_at DateTime
|
||||
approved_at DateTime?
|
||||
consumed_at DateTime?
|
||||
|
||||
@@index([expires_at])
|
||||
@@index([status, expires_at])
|
||||
@@map("login_pairings")
|
||||
}
|
||||
```
|
||||
|
||||
Op `User`: `login_pairings LoginPairing[]` toevoegen.
|
||||
|
||||
2. **Migratie-SQL** voegt naast de tabel ook trigger toe (mirror van `notify_solo_change` in `prisma/migrations/20260426230316_add_solo_realtime_triggers/migration.sql`):
|
||||
|
||||
```sql
|
||||
CREATE OR REPLACE FUNCTION notify_pairing_change() RETURNS trigger AS $$
|
||||
DECLARE payload jsonb;
|
||||
BEGIN
|
||||
payload := jsonb_build_object(
|
||||
'op', CASE TG_OP WHEN 'INSERT' THEN 'I' WHEN 'UPDATE' THEN 'U' ELSE 'D' END,
|
||||
'pairing_id', COALESCE(NEW.id, OLD.id),
|
||||
'status', COALESCE(NEW.status, OLD.status)
|
||||
);
|
||||
PERFORM pg_notify('scrum4me_pairing', payload::text);
|
||||
RETURN COALESCE(NEW, OLD);
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE TRIGGER login_pairings_notify
|
||||
AFTER INSERT OR UPDATE ON login_pairings
|
||||
FOR EACH ROW EXECUTE FUNCTION notify_pairing_change();
|
||||
```
|
||||
|
||||
3. `npx prisma migrate dev --name add_login_pairing`.
|
||||
|
||||
**Aandachtspunten**
|
||||
- `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).
|
||||
- Trigger emit ook bij DELETE niet nodig — pairings worden niet gedelete'd, ze gaan naar `consumed`/`cancelled`.
|
||||
- `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**
|
||||
- `npx prisma migrate dev` slaagt
|
||||
- `npx prisma validate` zonder fouten
|
||||
- `psql $DIRECT_URL -c "LISTEN scrum4me_pairing;"` toont payload bij `INSERT INTO login_pairings(...) VALUES(...)`
|
||||
- `prisma studio` toont tabel met beide hash-kolommen `NOT NULL`
|
||||
|
||||
---
|
||||
|
||||
## ST-1002 — Pairing-helpers + sessie-uitbreiding + pre-auth-cookie
|
||||
|
||||
**Bestanden**
|
||||
- `lib/auth/pairing.ts` — nieuw, secret/token-generatie en hash-helpers
|
||||
- `lib/auth/pair-cookie.ts` — nieuw, set/read/clear van `s4m_pair`-cookie
|
||||
- `lib/session.ts` — `SessionData` uitbreiden met `paired` en `pairedExpiresAt`
|
||||
- `app/(app)/layout.tsx` — extra guard op vervallen paired-sessie
|
||||
- `__tests__/lib/auth/pairing.test.ts` — nieuw
|
||||
|
||||
**Stappen**
|
||||
|
||||
1. **`lib/auth/pairing.ts`**:
|
||||
|
||||
```ts
|
||||
import { createHash, randomBytes, timingSafeEqual } from 'crypto'
|
||||
|
||||
export function generateMobileSecret(): string {
|
||||
return randomBytes(32).toString('base64url')
|
||||
}
|
||||
|
||||
export function generateDesktopToken(): string {
|
||||
return randomBytes(32).toString('base64url')
|
||||
}
|
||||
|
||||
export function hashToken(token: string): string {
|
||||
return createHash('sha256').update(token).digest('hex')
|
||||
}
|
||||
|
||||
export function verifyToken(token: string, hash: string): boolean {
|
||||
const a = Buffer.from(hashToken(token), 'hex')
|
||||
const b = Buffer.from(hash, 'hex')
|
||||
if (a.length !== b.length) return false
|
||||
return timingSafeEqual(a, b)
|
||||
}
|
||||
```
|
||||
|
||||
Twee aparte generators (niet één functie met arg) voorkomen dat dezelfde geheim per ongeluk twee keer wordt gebruikt.
|
||||
|
||||
2. **`lib/auth/pair-cookie.ts`**:
|
||||
|
||||
```ts
|
||||
import { cookies } from 'next/headers'
|
||||
|
||||
const COOKIE_NAME = 's4m_pair'
|
||||
const MAX_AGE = 120 // 2 min, gelijk aan pending-TTL van pairing
|
||||
|
||||
export async function setPairCookie(desktopToken: string) {
|
||||
const jar = await cookies()
|
||||
jar.set(COOKIE_NAME, desktopToken, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'lax',
|
||||
path: '/api/auth/pair',
|
||||
maxAge: MAX_AGE,
|
||||
})
|
||||
}
|
||||
|
||||
export async function readPairCookie(): Promise<string | null> {
|
||||
const jar = await cookies()
|
||||
return jar.get(COOKIE_NAME)?.value ?? null
|
||||
}
|
||||
|
||||
export async function clearPairCookie() {
|
||||
const jar = await cookies()
|
||||
jar.delete({ name: COOKIE_NAME, path: '/api/auth/pair' })
|
||||
}
|
||||
```
|
||||
|
||||
`Path=/api/auth/pair` zorgt dat de cookie alleen naar pair-endpoints wordt gestuurd — niet naar elke route.
|
||||
|
||||
3. **`lib/session.ts`** — `SessionData` interface:
|
||||
|
||||
```ts
|
||||
export interface SessionData {
|
||||
userId: string
|
||||
isDemo: boolean
|
||||
paired?: boolean // true als sessie is aangemaakt via QR-pairing
|
||||
pairedExpiresAt?: number // unix ms
|
||||
}
|
||||
```
|
||||
|
||||
Bestaande sessies blijven werken — beide velden zijn optioneel.
|
||||
|
||||
4. **`app/(app)/layout.tsx`** — guard ná de bestaande `if (!session.userId) redirect('/login')`:
|
||||
|
||||
```ts
|
||||
if (session.paired && session.pairedExpiresAt && session.pairedExpiresAt < Date.now()) {
|
||||
session.destroy()
|
||||
redirect('/login?notice=paired-expired')
|
||||
}
|
||||
```
|
||||
|
||||
Hergebruikt het bestaande `notice`-querystring-patroon van M9's `<NoticeToast />` voor de melding "Je sessie is verlopen, log opnieuw in".
|
||||
|
||||
5. **Tests** — `__tests__/lib/auth/pairing.test.ts`:
|
||||
- `generateMobileSecret()` produceert 43-karakter base64url (32 bytes)
|
||||
- `hashToken` is deterministisch
|
||||
- `verifyToken` is true voor geldig paar, false voor ongeldig
|
||||
- Twee verschillende `generateMobileSecret()`-calls geven verschillende waardes
|
||||
- Cookie helpers: HttpOnly bit gezet (via Next.js cookie-store mock)
|
||||
|
||||
**Aandachtspunten**
|
||||
- Geen middleware nodig voor de paired-expiry-check; layout-guard is voldoende. Middleware komt pas in beeld als `proxy.ts` herzien wordt (uit scope hier).
|
||||
- `cookies().delete({ name, path })` moet **dezelfde path** specificeren als bij set, anders blijft de cookie staan.
|
||||
- `crypto.randomBytes` is sync en blocking — voor 32 bytes ruim < 1ms; geen async-variant nodig.
|
||||
|
||||
**Verificatie**
|
||||
- `npm run lint && npx tsc --noEmit && npm test && npm run build` groen
|
||||
- Handmatig: in DevTools Application-tab is de cookie zichtbaar als HttpOnly + Path scoped
|
||||
- `document.cookie` op de pagina laat de cookie *niet* zien
|
||||
|
||||
---
|
||||
|
||||
## ST-1003 — `POST /api/auth/pair/start` (anon, sets pre-auth cookie)
|
||||
|
||||
**Bestanden**
|
||||
- `app/api/auth/pair/start/route.ts` — nieuw
|
||||
- `lib/rate-limit.ts` — checken of bestaand (uit ST-608); anders helper toevoegen
|
||||
- `__tests__/api/pair-start.test.ts` — nieuw
|
||||
|
||||
**Stappen**
|
||||
|
||||
1. Route Handler (vrij van `authenticateApiRequest` — dit is anon):
|
||||
|
||||
```ts
|
||||
import { NextRequest } from 'next/server'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
import {
|
||||
generateMobileSecret, generateDesktopToken, hashToken,
|
||||
} from '@/lib/auth/pairing'
|
||||
import { setPairCookie } from '@/lib/auth/pair-cookie'
|
||||
import { rateLimit } from '@/lib/rate-limit'
|
||||
|
||||
export const runtime = 'nodejs'
|
||||
|
||||
const PENDING_TTL_MS = 2 * 60 * 1000
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const ip = request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() ?? null
|
||||
const ua = request.headers.get('user-agent')?.slice(0, 255) ?? null
|
||||
|
||||
// Rate-limit per IP — 10/min (zelfde patroon als ST-608)
|
||||
const limited = await rateLimit(`pair-start:${ip ?? 'anon'}`, 10, 60_000)
|
||||
if (limited) {
|
||||
return Response.json({ error: 'Te veel verzoeken' }, { status: 429 })
|
||||
}
|
||||
|
||||
const mobileSecret = generateMobileSecret()
|
||||
const desktopToken = generateDesktopToken()
|
||||
|
||||
const pairing = await prisma.loginPairing.create({
|
||||
data: {
|
||||
secret_hash: hashToken(mobileSecret),
|
||||
desktop_token_hash: hashToken(desktopToken),
|
||||
status: 'pending',
|
||||
desktop_ua: ua,
|
||||
desktop_ip: ip,
|
||||
expires_at: new Date(Date.now() + PENDING_TTL_MS),
|
||||
},
|
||||
select: { id: true, expires_at: true },
|
||||
})
|
||||
|
||||
await setPairCookie(desktopToken)
|
||||
|
||||
const origin = request.nextUrl.origin
|
||||
const qrUrl = `${origin}/m/pair#id=${pairing.id}&s=${mobileSecret}`
|
||||
|
||||
return Response.json({
|
||||
pairingId: pairing.id,
|
||||
mobileSecret,
|
||||
expiresAt: pairing.expires_at.toISOString(),
|
||||
qrUrl,
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
2. **Rate-limit helper** — als `lib/rate-limit.ts` nog niet bestaat:
|
||||
- In-memory Map keyed op `key`, met sliding-window van timestamps; thread-safe genoeg voor v1 single-instance Vercel Functions.
|
||||
- Wordt ook door `actions/auth.ts` (login) gebruikt; verifieer met grep of die al bestaat — zo ja, hergebruik exact.
|
||||
|
||||
3. **Tests** — `__tests__/api/pair-start.test.ts`:
|
||||
- 200 response bevat `pairingId`, `mobileSecret`, `qrUrl` met fragment-syntax (`#id=…&s=…`)
|
||||
- `Set-Cookie`-header bevat `s4m_pair=...; HttpOnly; SameSite=Lax; Path=/api/auth/pair; Max-Age=120`
|
||||
- Database-rij heeft `secret_hash`, `desktop_token_hash`, geen plaintext
|
||||
- 11e call binnen 60s → 429
|
||||
- `desktop_ua` en `desktop_ip` worden opgeslagen bij aanwezigheid, anders `null`
|
||||
|
||||
**Aandachtspunten**
|
||||
- `mobileSecret` mag in de JSON-response — die is HTTPS-encrypted en wordt niet door browsers naar logs geschreven. Kritisch is dat het niet in **request**-URLs of **server**-toegangslogs belandt.
|
||||
- `qrUrl` gebruikt `#` (fragment), niet `?`. Browsers strippen het fragment voordat ze de mobile pair-page ophalen — maar dat is uitvoerig getest in ST-1005 server-side: de page leest de URL niet, alleen de client component leest `window.location.hash`.
|
||||
- Geen idempotency-key nodig: een tweede start vanuit dezelfde tab maakt simpelweg een nieuwe pairing en cookie aan; oude cookie wordt overschreven.
|
||||
- Vercel-edge-of-fluid: `runtime: 'nodejs'` expliciet (niet 'edge') want we gebruiken `crypto.randomBytes`.
|
||||
|
||||
**Verificatie**
|
||||
- `curl -i -X POST http://localhost:3000/api/auth/pair/start --cookie-jar /tmp/jar` retourneert JSON + `Set-Cookie`
|
||||
- Body bevat `qrUrl` met `#id=…&s=…`
|
||||
- Rij in `login_pairings` heeft beide hashes ingevuld
|
||||
- Tests groen
|
||||
|
||||
---
|
||||
|
||||
## ST-1004 — SSE-route `/api/auth/pair/stream/[pairingId]` (cookie-auth)
|
||||
|
||||
**Bestanden**
|
||||
- `app/api/auth/pair/stream/[pairingId]/route.ts` — nieuw
|
||||
- `__tests__/api/pair-stream.test.ts` — nieuw (auth-test, geen full SSE-test)
|
||||
|
||||
**Stappen**
|
||||
|
||||
1. **Routebestand** — exacte structuur uit `app/api/realtime/solo/route.ts` (incl. heartbeat, hard-close, abort-handler), maar:
|
||||
- Geen iron-session check; auth via `s4m_pair`-cookie:
|
||||
|
||||
```ts
|
||||
const desktopToken = await readPairCookie()
|
||||
if (!desktopToken) return Response.json({ error: 'Geen pairing-cookie' }, { status: 401 })
|
||||
|
||||
const pairing = await prisma.loginPairing.findUnique({
|
||||
where: { id: pairingId },
|
||||
select: { desktop_token_hash: true, status: true, expires_at: true },
|
||||
})
|
||||
if (!pairing) return Response.json({ error: 'Pairing niet gevonden' }, { status: 404 })
|
||||
if (pairing.expires_at < new Date()) return Response.json({ error: 'Pairing verlopen' }, { status: 410 })
|
||||
if (!verifyToken(desktopToken, pairing.desktop_token_hash)) {
|
||||
return Response.json({ error: 'Ongeldige cookie' }, { status: 401 })
|
||||
}
|
||||
```
|
||||
|
||||
- Channel = `'scrum4me_pairing'`
|
||||
- Filter `shouldEmit`: `payload.pairing_id === pairingId`
|
||||
- Auto-close ook bij payload `status` ∈ `{'consumed', 'cancelled'}` — niet alleen na 240 s
|
||||
- Geen sprint-resolve, geen userId-filter — eenvoudiger dan solo-route
|
||||
|
||||
2. **Initial-state-event** vlak na verbinden: query de pairing-status één keer uit DB en stuur als `event: state\ndata: {"status":"pending"}` zodat de desktop niet hoeft te wachten op het eerste pg_notify (handig als pairing al `approved` is voordat de SSE opent — race-conditie).
|
||||
|
||||
3. **Tests**:
|
||||
- GET zonder cookie → 401
|
||||
- GET met cookie maar onbekende `pairingId` → 404
|
||||
- GET met verlopen pairing → 410
|
||||
- GET met cookie die hasht naar een andere `desktop_token_hash` → 401
|
||||
|
||||
Geen full-stream-test — vereist een Postgres-event-mock die het niet waard is voor v1. Manuele test dekt dit (zie verificatie).
|
||||
|
||||
**Aandachtspunten**
|
||||
- `pairingId` in het URL-pad is OK — niet vertrouwelijk. De cookie is het bewijs.
|
||||
- `EventSource` op de client kan geen custom headers; cookie is daarom de enige praktische auth-methode. `withCredentials: true` op de client is verplicht.
|
||||
- Bij browser-tab-sluiten wordt `request.signal.abort` getriggered → cleanup zoals in solo-route.
|
||||
- Vermijd Prisma in de notification-handler; gebruik alleen `pg.Client` zoals solo-route. Status-check hierboven is de enige Prisma-call (vóór de stream start).
|
||||
|
||||
**Verificatie**
|
||||
- `curl -N --cookie /tmp/jar http://localhost:3000/api/auth/pair/stream/<id>` blijft open en print `: heartbeat` elke 25 s
|
||||
- Andere terminal: `psql $DIRECT_URL -c "UPDATE login_pairings SET status='approved' WHERE id='<id>'"` → curl-uitvoer toont event binnen 1 s
|
||||
- Manuele 401-test: `curl -N` zonder cookie → JSON 401
|
||||
|
||||
---
|
||||
|
||||
## ST-1005 — Server actions + mobiele bevestigingspagina
|
||||
|
||||
**Bestanden**
|
||||
- `actions/pairing.ts` — nieuw, drie Server Actions
|
||||
- `app/(app)/m/pair/page.tsx` — nieuw, Server Component
|
||||
- `app/(app)/m/pair/pair-confirmation.tsx` — nieuw, Client Component
|
||||
- `__tests__/actions/pairing.test.ts` — nieuw
|
||||
|
||||
**Stappen**
|
||||
|
||||
1. **`actions/pairing.ts`** — volgt `docs/patterns/server-action.md`:
|
||||
|
||||
```ts
|
||||
'use server'
|
||||
|
||||
import { revalidatePath } from 'next/cache'
|
||||
import { z } from 'zod'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { verifyToken } from '@/lib/auth/pairing'
|
||||
|
||||
const inputSchema = z.object({
|
||||
pairingId: z.string().cuid(),
|
||||
mobileSecret: z.string().min(40), // base64url van 32 bytes ≈ 43 chars
|
||||
})
|
||||
|
||||
export async function getPairingForApproval(pairingId: string, mobileSecret: string) {
|
||||
const session = await getSession()
|
||||
if (!session.userId) return { ok: false, error: 'Niet ingelogd' } as const
|
||||
|
||||
const parsed = inputSchema.safeParse({ pairingId, mobileSecret })
|
||||
if (!parsed.success) return { ok: false, error: 'Ongeldige invoer' } as const
|
||||
|
||||
const pairing = await prisma.loginPairing.findUnique({
|
||||
where: { id: pairingId },
|
||||
select: {
|
||||
status: true, expires_at: true, secret_hash: true,
|
||||
desktop_ua: true, desktop_ip: true,
|
||||
},
|
||||
})
|
||||
if (!pairing) return { ok: false, error: 'Pairing niet gevonden' } as const
|
||||
if (pairing.expires_at < new Date()) return { ok: false, error: 'Pairing verlopen' } as const
|
||||
if (pairing.status !== 'pending') return { ok: false, error: 'Pairing al afgehandeld' } as const
|
||||
if (!verifyToken(mobileSecret, pairing.secret_hash)) {
|
||||
return { ok: false, error: 'Ongeldig pairing-geheim' } as const
|
||||
}
|
||||
|
||||
const me = await prisma.user.findUnique({
|
||||
where: { id: session.userId },
|
||||
select: { username: true },
|
||||
})
|
||||
return {
|
||||
ok: true,
|
||||
desktop_ua: pairing.desktop_ua,
|
||||
desktop_ip: pairing.desktop_ip,
|
||||
username: me?.username ?? '',
|
||||
} as const
|
||||
}
|
||||
|
||||
export async function approvePairing(pairingId: string, mobileSecret: string) {
|
||||
const session = await getSession()
|
||||
if (!session.userId) return { ok: false, error: 'Niet ingelogd' } as const
|
||||
if (session.isDemo) return { ok: false, error: 'Niet beschikbaar in demo-modus' } as const
|
||||
|
||||
const parsed = inputSchema.safeParse({ pairingId, mobileSecret })
|
||||
if (!parsed.success) return { ok: false, error: 'Ongeldige invoer' } as const
|
||||
|
||||
const pairing = await prisma.loginPairing.findUnique({
|
||||
where: { id: pairingId },
|
||||
select: { status: true, expires_at: true, secret_hash: true },
|
||||
})
|
||||
if (!pairing) return { ok: false, error: 'Pairing niet gevonden' } as const
|
||||
if (pairing.expires_at < new Date()) return { ok: false, error: 'Pairing verlopen' } as const
|
||||
if (pairing.status !== 'pending') return { ok: false, error: 'Pairing al afgehandeld' } as const
|
||||
if (!verifyToken(mobileSecret, pairing.secret_hash)) {
|
||||
return { ok: false, error: 'Ongeldig pairing-geheim' } as const
|
||||
}
|
||||
|
||||
const APPROVED_TTL_MS = 5 * 60 * 1000
|
||||
await prisma.loginPairing.update({
|
||||
where: { id: pairingId },
|
||||
data: {
|
||||
status: 'approved',
|
||||
user_id: session.userId,
|
||||
approved_at: new Date(),
|
||||
expires_at: new Date(Date.now() + APPROVED_TTL_MS),
|
||||
},
|
||||
})
|
||||
// Trigger emit pg_notify automatisch — geen revalidatePath nodig
|
||||
return { ok: true } as const
|
||||
}
|
||||
|
||||
export async function cancelPairing(pairingId: string, mobileSecret: string) {
|
||||
// Vergelijkbaar met approvePairing maar status='cancelled', geen user_id; demo mag annuleren.
|
||||
}
|
||||
```
|
||||
|
||||
2. **`app/(app)/m/pair/page.tsx`** — Server Component, achter bestaande `(app)/layout.tsx` auth-guard:
|
||||
|
||||
```tsx
|
||||
import { PairConfirmation } from './pair-confirmation'
|
||||
|
||||
export default function PairPage() {
|
||||
return (
|
||||
<main className="container mx-auto max-w-md py-12">
|
||||
<h1 className="text-h2">Inloggen op desktop</h1>
|
||||
<p className="text-muted-foreground mt-2">
|
||||
Bevestig hieronder dat je wilt inloggen op het apparaat dat de QR-code toont.
|
||||
</p>
|
||||
<PairConfirmation />
|
||||
</main>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
Geen searchParams! De page leest de URL überhaupt niet — alleen het client-island doet dat client-side via `window.location.hash`.
|
||||
|
||||
3. **`app/(app)/m/pair/pair-confirmation.tsx`** — Client Component:
|
||||
|
||||
```tsx
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState, useTransition } from 'react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { toast } from 'sonner'
|
||||
import { getPairingForApproval, approvePairing, cancelPairing } from '@/actions/pairing'
|
||||
|
||||
type State =
|
||||
| { kind: 'loading' }
|
||||
| { kind: 'invalid'; error: string }
|
||||
| { kind: 'ready'; pairingId: string; secret: string; ua: string | null; ip: string | null; username: string }
|
||||
| { kind: 'success' }
|
||||
| { kind: 'error'; error: string }
|
||||
|
||||
function parseHash(): { id: string; s: string } | null {
|
||||
if (typeof window === 'undefined') return null
|
||||
const hash = window.location.hash.replace(/^#/, '')
|
||||
const params = new URLSearchParams(hash)
|
||||
const id = params.get('id'); const s = params.get('s')
|
||||
return id && s ? { id, s } : null
|
||||
}
|
||||
|
||||
export function PairConfirmation() {
|
||||
const [state, setState] = useState<State>({ kind: 'loading' })
|
||||
const [pending, startTransition] = useTransition()
|
||||
|
||||
useEffect(() => {
|
||||
const parsed = parseHash()
|
||||
if (!parsed) {
|
||||
setState({ kind: 'invalid', error: 'Ongeldige pairing-link' })
|
||||
return
|
||||
}
|
||||
getPairingForApproval(parsed.id, parsed.s).then((res) => {
|
||||
if (!res.ok) setState({ kind: 'invalid', error: res.error })
|
||||
else setState({
|
||||
kind: 'ready',
|
||||
pairingId: parsed.id, secret: parsed.s,
|
||||
ua: res.desktop_ua, ip: res.desktop_ip, username: res.username,
|
||||
})
|
||||
})
|
||||
}, [])
|
||||
|
||||
function onApprove() {
|
||||
if (state.kind !== 'ready') return
|
||||
startTransition(async () => {
|
||||
const res = await approvePairing(state.pairingId, state.secret)
|
||||
if (!res.ok) { toast.error(res.error); return }
|
||||
// Wist secret uit URL zodat back/forward 'm niet onthult
|
||||
if (typeof window !== 'undefined') {
|
||||
window.history.replaceState(null, '', window.location.pathname)
|
||||
}
|
||||
setState({ kind: 'success' })
|
||||
})
|
||||
}
|
||||
|
||||
function onCancel() {
|
||||
if (state.kind !== 'ready') return
|
||||
startTransition(async () => {
|
||||
await cancelPairing(state.pairingId, state.secret)
|
||||
setState({ kind: 'invalid', error: 'Pairing geannuleerd' })
|
||||
})
|
||||
}
|
||||
|
||||
// Render-logica per state — kort:
|
||||
// loading → spinner
|
||||
// invalid → foutmelding + link "Terug naar dashboard"
|
||||
// ready → kaart met UA/IP/username + Bevestig/Annuleer
|
||||
// success → "Klaar — je kunt deze tab sluiten"
|
||||
// … (volledige JSX in implementation)
|
||||
}
|
||||
```
|
||||
|
||||
4. **Tests** — `__tests__/actions/pairing.test.ts`:
|
||||
- `getPairingForApproval` met `pending`-pairing → `ok: true` + ua/ip/username
|
||||
- `getPairingForApproval` met al-`approved` → `ok: false`
|
||||
- `getPairingForApproval` met verlopen → `ok: false`
|
||||
- `getPairingForApproval` met verkeerd secret → `ok: false`
|
||||
- `approvePairing` als demo-user → `ok: false` + DB onveranderd
|
||||
- `approvePairing` happy path → status `approved`, `user_id` gezet, `expires_at` bumped
|
||||
- `cancelPairing` happy path → status `cancelled`
|
||||
|
||||
**Aandachtspunten**
|
||||
- `getPairingForApproval` mág door demo-users worden aangeroepen (om iets te zien); alleen `approvePairing` blokkeert demo's.
|
||||
- `pair-confirmation.tsx` gebruikt **geen** `useSearchParams` — die kan in Next.js 16 hash niet lezen, en we willen het ook niet via routing zien.
|
||||
- Na approve `window.history.replaceState` zonder de `#hash` zorgt dat browser-back de secret niet opnieuw onthult.
|
||||
- `revalidatePath` in `approvePairing` is niet nodig — de desktop hoort het via SSE, en de mobiele page heeft geen server-state om te ververversen.
|
||||
|
||||
**Verificatie**
|
||||
- `npm run lint && npx tsc --noEmit && npm test && npm run build` groen
|
||||
- Handmatig: log in op telefoon-emulator → bezoek `/m/pair#id=…&s=…` → kaart toont UA/IP → Bevestig → succes-state, URL is `/m/pair` zonder hash
|
||||
|
||||
---
|
||||
|
||||
## ST-1006 — `POST /api/auth/pair/claim` (cookie-auth, schrijft iron-session)
|
||||
|
||||
**Bestanden**
|
||||
- `app/api/auth/pair/claim/route.ts` — nieuw
|
||||
- `__tests__/api/pair-claim.test.ts` — nieuw
|
||||
|
||||
**Stappen**
|
||||
|
||||
1. Route Handler:
|
||||
|
||||
```ts
|
||||
import { NextRequest } from 'next/server'
|
||||
import { getIronSession } from 'iron-session'
|
||||
import { cookies } from 'next/headers'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
import { SessionData, sessionOptions } from '@/lib/session'
|
||||
import { hashToken } from '@/lib/auth/pairing'
|
||||
import { readPairCookie, clearPairCookie } from '@/lib/auth/pair-cookie'
|
||||
|
||||
export const runtime = 'nodejs'
|
||||
|
||||
const PAIRED_TTL_MS = 8 * 60 * 60 * 1000 // 8 uur
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const desktopToken = await readPairCookie()
|
||||
if (!desktopToken) return Response.json({ error: 'Geen pairing-cookie' }, { status: 401 })
|
||||
|
||||
const body = await request.json().catch(() => null) as { pairingId?: string } | null
|
||||
const pairingId = body?.pairingId
|
||||
if (!pairingId) return Response.json({ error: 'pairingId vereist' }, { status: 400 })
|
||||
|
||||
const desktopTokenHash = hashToken(desktopToken)
|
||||
|
||||
// Atomic: WHERE status='approved' AND token-hash + expiry → consumed, RETURNING user_id
|
||||
const updated = await prisma.loginPairing.updateMany({
|
||||
where: {
|
||||
id: pairingId,
|
||||
status: 'approved',
|
||||
desktop_token_hash: desktopTokenHash,
|
||||
expires_at: { gt: new Date() },
|
||||
},
|
||||
data: { status: 'consumed', consumed_at: new Date() },
|
||||
})
|
||||
if (updated.count !== 1) {
|
||||
// Was het wél een geldige cookie maar al consumed? → 410. Anders → 401.
|
||||
const exists = await prisma.loginPairing.findFirst({
|
||||
where: { id: pairingId, desktop_token_hash: desktopTokenHash },
|
||||
select: { status: true, expires_at: true },
|
||||
})
|
||||
await clearPairCookie()
|
||||
if (!exists) return Response.json({ error: 'Ongeldig' }, { status: 401 })
|
||||
if (exists.status === 'consumed') return Response.json({ error: 'Al gebruikt' }, { status: 410 })
|
||||
return Response.json({ error: 'Niet beschikbaar' }, { status: 410 })
|
||||
}
|
||||
|
||||
const pairing = await prisma.loginPairing.findUnique({
|
||||
where: { id: pairingId },
|
||||
select: { user_id: true, user: { select: { is_demo: true } } },
|
||||
})
|
||||
if (!pairing?.user_id) {
|
||||
await clearPairCookie()
|
||||
return Response.json({ error: 'Pairing zonder user' }, { status: 500 })
|
||||
}
|
||||
|
||||
const session = await getIronSession<SessionData>(await cookies(), sessionOptions)
|
||||
session.userId = pairing.user_id
|
||||
session.isDemo = pairing.user?.is_demo ?? false
|
||||
session.paired = true
|
||||
session.pairedExpiresAt = Date.now() + PAIRED_TTL_MS
|
||||
await session.save()
|
||||
|
||||
await clearPairCookie()
|
||||
return Response.json({ ok: true })
|
||||
}
|
||||
```
|
||||
|
||||
2. **Tests**:
|
||||
- 200 + iron-session cookie + clear `s4m_pair` na succes
|
||||
- 410 op tweede claim met dezelfde cookie
|
||||
- 401 zonder cookie
|
||||
- 401 met cookie die hasht naar andere pairing
|
||||
- paired-sessie bevat `paired: true` en `pairedExpiresAt` rond `now + 8h`
|
||||
|
||||
**Aandachtspunten**
|
||||
- `updateMany` gebruiken (niet `update`) want we hebben een composite WHERE met `status` + `desktop_token_hash` + `expires_at`; `update` kan alleen op unique keys.
|
||||
- Het WHERE-criterium garandeert atomiciteit: PostgreSQL UPDATE met meerdere predicates is row-level locked; concurrent dubbele claim resulteert in `count = 1` voor één caller en `count = 0` voor de ander.
|
||||
- `clearPairCookie` ook bij faalpaden, anders blijft 'm na expiry hangen (cosmetisch — `Max-Age=120` regelt het ook).
|
||||
- De `session.isDemo` check overneemt: als de approver een demo-user is — wat ST-1005 al blokkeert — komen we hier niet eens, maar `is_demo` doorzetten is een extra vangnet.
|
||||
|
||||
**Verificatie**
|
||||
- Handmatig: na approve in mobiele tab, POST naar `/api/auth/pair/claim` met de cookie van start → 200 + `Set-Cookie: session=...`
|
||||
- `curl -X POST` zonder cookie → 401
|
||||
- Tweede claim → 410
|
||||
|
||||
---
|
||||
|
||||
## ST-1007 — Desktop UI: QR-render + SSE-listener op `/login`
|
||||
|
||||
**Bestanden**
|
||||
- `app/login/page.tsx` — bestaand, knop "Inloggen via mobiel" toevoegen
|
||||
- `app/login/qr-login-button.tsx` — nieuw, Client Component
|
||||
- `package.json` — `qrcode.react` toevoegen
|
||||
- `__tests__/components/qr-login-button.test.tsx` — minimale render-test
|
||||
|
||||
**Stappen**
|
||||
|
||||
1. **Dependency**: `npm install qrcode.react` — direct in `dependencies` per CLAUDE.md-conventie.
|
||||
|
||||
2. **`qr-login-button.tsx`**:
|
||||
|
||||
```tsx
|
||||
'use client'
|
||||
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { QRCodeSVG } from 'qrcode.react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
type Phase =
|
||||
| { kind: 'idle' }
|
||||
| { kind: 'starting' }
|
||||
| { kind: 'showing'; pairingId: string; qrUrl: string; expiresAt: number }
|
||||
| { kind: 'expired'; pairingId: string }
|
||||
| { kind: 'claiming' }
|
||||
|
||||
export function QrLoginButton() {
|
||||
const router = useRouter()
|
||||
const [phase, setPhase] = useState<Phase>({ kind: 'idle' })
|
||||
const sseRef = useRef<EventSource | null>(null)
|
||||
|
||||
async function start() {
|
||||
setPhase({ kind: 'starting' })
|
||||
const res = await fetch('/api/auth/pair/start', {
|
||||
method: 'POST', credentials: 'same-origin',
|
||||
})
|
||||
if (!res.ok) { toast.error('Kon QR-code niet aanmaken'); setPhase({ kind: 'idle' }); return }
|
||||
const data = await res.json() as { pairingId: string; qrUrl: string; expiresAt: string }
|
||||
setPhase({
|
||||
kind: 'showing',
|
||||
pairingId: data.pairingId,
|
||||
qrUrl: data.qrUrl,
|
||||
expiresAt: new Date(data.expiresAt).getTime(),
|
||||
})
|
||||
}
|
||||
|
||||
// SSE-koppeling
|
||||
useEffect(() => {
|
||||
if (phase.kind !== 'showing') return
|
||||
const es = new EventSource(`/api/auth/pair/stream/${phase.pairingId}`, {
|
||||
withCredentials: true,
|
||||
})
|
||||
sseRef.current = es
|
||||
|
||||
es.addEventListener('message', async (ev) => {
|
||||
const data = JSON.parse(ev.data) as { status?: string }
|
||||
if (data.status === 'approved') {
|
||||
es.close()
|
||||
setPhase({ kind: 'claiming' })
|
||||
const res = await fetch('/api/auth/pair/claim', {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ pairingId: phase.pairingId }),
|
||||
})
|
||||
if (!res.ok) {
|
||||
toast.error('Inloggen mislukt')
|
||||
setPhase({ kind: 'idle' }); return
|
||||
}
|
||||
router.push('/dashboard')
|
||||
}
|
||||
})
|
||||
es.addEventListener('error', () => { /* silent — laat reconnecten */ })
|
||||
|
||||
return () => { es.close() }
|
||||
}, [phase, router])
|
||||
|
||||
// Aftellende timer + auto-expire
|
||||
useEffect(() => {
|
||||
if (phase.kind !== 'showing') return
|
||||
const t = setInterval(() => {
|
||||
if (Date.now() > phase.expiresAt) {
|
||||
sseRef.current?.close()
|
||||
setPhase({ kind: 'expired', pairingId: phase.pairingId })
|
||||
clearInterval(t)
|
||||
}
|
||||
}, 1000)
|
||||
return () => clearInterval(t)
|
||||
}, [phase])
|
||||
|
||||
// Render: knop / QR + countdown / "Vernieuwen" — JSX hier weggelaten voor brevity
|
||||
}
|
||||
```
|
||||
|
||||
3. **`app/login/page.tsx`** — knop toevoegen onder of naast het wachtwoord-formulier:
|
||||
|
||||
```tsx
|
||||
<div className="my-4 flex items-center gap-2">
|
||||
<div className="border-border h-px flex-1 border-t" />
|
||||
<span className="text-muted-foreground text-sm">of</span>
|
||||
<div className="border-border h-px flex-1 border-t" />
|
||||
</div>
|
||||
<QrLoginButton />
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
**Aandachtspunten**
|
||||
- `EventSource({ withCredentials: true })` is verplicht zodat de browser de `s4m_pair`-cookie meestuurt; standaard verstuurt EventSource geen credentials.
|
||||
- Cleanup-volgorde: `es.close()` eerst, dán fetch claim. Anders kan een tweede `approved`-event tussen close-en-claim binnenkomen en een dubbele claim triggeren (server vangt het op met 410, maar netter is het netjes te sluiten).
|
||||
- `qrcode.react` exporteert zowel `QRCodeSVG` als `QRCodeCanvas`. Kies SVG: schaalbaarder voor printen/screenshots en kleinere bundle.
|
||||
- Geen `next/image` — QR is dynamisch en client-side.
|
||||
|
||||
**Verificatie**
|
||||
- `npm run lint && npx tsc --noEmit && npm test && npm run build` groen
|
||||
- Handmatige twee-browser-test: A toont QR → B (ingelogd op mobile-emulator) opent QR-URL en bevestigt → A redirect naar `/dashboard` met `session.paired === true`
|
||||
- DevTools Network-tab: geen URL bevat `s=` (alleen `Set-Cookie`/`Cookie` headers)
|
||||
- Speel met een verlopen QR: na 2 min toont knop "Vernieuwen", `Vernieuwen` start nieuwe pairing
|
||||
|
||||
---
|
||||
|
||||
## ST-1008 — Documentatie + acceptatietest
|
||||
|
||||
**Bestanden**
|
||||
- `docs/api/rest-contract.md` — drie nieuwe endpoints
|
||||
- `docs/architecture.md` — sectie "QR-pairing flow" + threat-model
|
||||
- `docs/patterns/qr-login.md` — nieuw pattern-doc
|
||||
- `CLAUDE.md` — verwijzing naar het pattern-doc in de patterns-tabel
|
||||
- `__tests__/integration/qr-pairing-e2e.test.ts` — optioneel, alleen als de test-infra het toelaat
|
||||
|
||||
**Stappen**
|
||||
|
||||
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:
|
||||
- Sequence-diagram (mermaid of ASCII analoog aan M8)
|
||||
- Threat-model:
|
||||
- **Replay**: atomic `updateMany` met `status='approved'` voorkomt dubbele claim
|
||||
- **Phishing-QR**: mobiele bevestigingspagina toont UA + IP zodat gebruiker een vreemd apparaat herkent; expliciete tap vereist
|
||||
- **Demo-block**: `approvePairing` early return op `session.isDemo`
|
||||
- **Rate-limit**: 10 starts per IP per minuut
|
||||
- **Secret-hashing**: alleen sha256-hashes in DB; secrets verlaten desktop alleen via QR-fragment + POST-body
|
||||
- **TTL-rationale**: 2 min pending vs. 5 min approved vs. 8 u paired-sessie — verschillen verklaren
|
||||
- **Subsectie "Waarom geen secret in URL"**: fragment-eigenschap (browsers sturen `#…` niet naar server); HttpOnly cookie voor desktop-bewijs; geen secret in access logs / reverse-proxy logs / observability / browsergeschiedenis
|
||||
|
||||
3. **`docs/patterns/qr-login.md`** — herbruikbaar patroon: "unauth-SSE-via-pre-auth-cookie". Toekomstige features die een pre-auth flow met realtime-updates willen kunnen dit kopiëren (bv. "ontvang webhook live", "long-running export"). Inclusief:
|
||||
- Wanneer dit patroon te gebruiken (er moet realtime-feedback zijn vóór de gebruiker is geauthenticeerd)
|
||||
- Verwijzingen naar `lib/auth/pair-cookie.ts` als sjabloon
|
||||
- Risico's en mitigaties
|
||||
|
||||
4. **`CLAUDE.md`** — in de *Implementatiepatronen*-tabel een rij `| QR-pairing (unauth-SSE + pre-auth cookie) | docs/patterns/qr-login.md |`.
|
||||
|
||||
5. **Acceptatie-scenario's** (handmatig, eventueel automatiseerbaar in v2):
|
||||
1. Happy path — twee browsers, end-to-end binnen 2 minuten ingelogd
|
||||
2. Demo-block — mobiel ingelogd als demo-user, scant QR → kan niet bevestigen
|
||||
3. Replay — claim de pairing twee keer → tweede call 410
|
||||
4. Expiry tijdens pending — wacht 3 min na start, scan dan → mobiel toont "Pairing verlopen"
|
||||
5. Expiry tussen approve en claim — approve, wacht 6 min, claim → 410
|
||||
6. Ontbrekende cookie op SSE/claim — verwijder `s4m_pair` in DevTools, herhaal → 401
|
||||
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**
|
||||
- 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.
|
||||
|
||||
**Verificatie**
|
||||
- `npm run lint && npx tsc --noEmit && npm test && npm run build` groen
|
||||
- Alle zeven scenario's handmatig groen, beschreven in een test-rapport-sectie
|
||||
- `vendor/scrum4me`-submodule in mcp gesynced ná schema-merge
|
||||
|
||||
---
|
||||
|
||||
## Branch- en commit-strategie
|
||||
|
||||
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.
|
||||
|
||||
**Commits** in chronologische volgorde, één per stap, ST-code in de titel. Voorbeeld-progressie:
|
||||
|
||||
```
|
||||
feat(ST-1001): add LoginPairing model
|
||||
feat(ST-1001): add pg_notify trigger on scrum4me_pairing channel
|
||||
feat(ST-1002): add pairing helpers and pre-auth cookie
|
||||
feat(ST-1002): extend SessionData with paired flag
|
||||
feat(ST-1002): guard expired paired sessions in app layout
|
||||
feat(ST-1003): add /api/auth/pair/start with rate-limit and pre-auth cookie
|
||||
feat(ST-1004): add SSE /api/auth/pair/stream with cookie auth
|
||||
feat(ST-1005): add pairing server actions
|
||||
feat(ST-1005): add mobile pair confirmation page with hash-fragment client island
|
||||
feat(ST-1006): add /api/auth/pair/claim with atomic consume
|
||||
chore(ST-1007): add qrcode.react dependency
|
||||
feat(ST-1007): add QR login button on /login with SSE listener
|
||||
docs(ST-1008): document QR-pairing endpoints in api.md
|
||||
docs(ST-1008): add QR-pairing flow and threat-model to architecture
|
||||
docs(ST-1008): add qr-login pattern doc
|
||||
```
|
||||
|
||||
**Push + PR**: pas nadat ST-1008-acceptatie-scenario 1 (happy path, end-to-end op localhost) handmatig groen is bevonden door de gebruiker. Tussentijdse "klaar voor jouw test"-momenten markeren we lokaal — niet met een push.
|
||||
|
||||
**Pre-merge gates** (uit CLAUDE.md DoD):
|
||||
- `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 mcp meebewegen na merge
|
||||
|
||||
**Wanneer dit aanpassen:** zodra het Vercel-account naar Pro gaat — zie CLAUDE.md.
|
||||
|
||||
---
|
||||
|
||||
## Reseed-stap (eenmalig vóór ST-1001-implementatie)
|
||||
|
||||
De backlog-markdown bevat ST-1001..1008, maar de live database heeft die rijen nog niet. Voordat `mcp__scrum4me__get_claude_context` ze als next-story kan teruggeven:
|
||||
|
||||
```
|
||||
npx prisma db seed
|
||||
```
|
||||
|
||||
Verifieer dat M10 en zijn 8 stories als `status=OPEN` in de DB staan. Daarna geeft `mcp__scrum4me__implement_next_story product_id=cmohfotr70000jwrt0hw4q020` automatisch ST-1001 als startpunt.
|
||||
|
||||
> **Let op:** seed kan bestaande dev-data overschrijven. Doe dit op een dev-DB, niet productie. Voor productie volstaat het om de stories handmatig of via een eenmalige migratie-script in te voegen — buiten scope hier.
|
||||
486
docs/old/plans/auto-pr-deploy-sync.md
Normal file
486
docs/old/plans/auto-pr-deploy-sync.md
Normal file
|
|
@ -0,0 +1,486 @@
|
|||
# Plan — Auto-PR + selectieve deploy-controle + sync-zicht (end-to-end batch flow)
|
||||
|
||||
> Bij merge: dit plan verplaatsen naar `docs/plans/auto-pr-deploy-sync.md`
|
||||
> conform feedback-memory (plans in `docs/plans/`).
|
||||
|
||||
## Context
|
||||
|
||||
Drie samenhangende problemen rond de "idee → uitvoeren"-keten:
|
||||
|
||||
1. **Worker stopt bij `commit`.** De Scrum4Me NAS-worker werkt lokaal:
|
||||
commits blijven op de machine staan totdat de gebruiker zelf pusht en
|
||||
een PR aanmaakt. Voor batch-uitvoer van story-jobs is dit een harde
|
||||
menselijke gate.
|
||||
2. **Deploy is alles-of-niets.** `.github/workflows/ci.yml` deployt nu
|
||||
**elke** push naar `main` automatisch naar productie en **elke** PR
|
||||
naar preview. `vercel.json` heeft geen `git.deploymentEnabled: false`,
|
||||
dus Vercel's eigen Git-integratie deployt waarschijnlijk parallel mee
|
||||
→ dubbele deploys en geen selectieve controle.
|
||||
3. **Geen zicht op voortgang per Idea/PBI.** Concreet getest geval:
|
||||
PBI-33 wordt nu de eerste sprint-batch — er is **geen git-voetafdruk**
|
||||
(geen branch/commit/PR met "PBI-33"), **geen activiteitenlog-entry**,
|
||||
en geen UI-pagina die per Story toont of er een ClaudeJob loopt, een
|
||||
commit gepusht is, of een PR open/merged is. De data zit in
|
||||
`Story.status`, `ClaudeJob.pushed_at/branch/pr_url`,
|
||||
`Pbi.pr_url/pr_merged_at` — er is alleen geen view die het joint.
|
||||
|
||||
Doel: de complete keten **plan → job → commit → push → PR → auto-merge →
|
||||
deploy** in één coherent ontwerp leggen, met (a) selectieve
|
||||
deploy-controle als veiligheidsklep en (b) een sync-tab die per Idea
|
||||
laat zien wat er werkelijk in git/PR-land gebeurd is.
|
||||
|
||||
## Vastgelegde keuzes
|
||||
|
||||
### Deploy-controle
|
||||
1. **Mechanisme**: PR-labels (B) + path-filter (C) gecombineerd.
|
||||
2. **Eigenaar**: GitHub Actions-workflow (A). Vercel Git-integratie uit.
|
||||
3. **Defaults**: PR → preview, push naar `main` → productie.
|
||||
4. **Override-richtingen**:
|
||||
- `skip-deploy` label: voorkomt preview-deploy op een PR.
|
||||
- `force-deploy` label: forceert deploy ook als path-filter doc-only
|
||||
zegt.
|
||||
|
||||
### Auto-PR (uit IDEA-007-grill)
|
||||
5. **Triggers in worker**: na elke succesvolle `update_job_status('done')`
|
||||
pusht de worker; na laatste story van een PBI maakt de worker een PR
|
||||
aan en activeert auto-merge (SQUASH).
|
||||
6. **Auth**: `GITHUB_TOKEN` als omgevingsvariabele op de worker; geen UI
|
||||
of GitHub App in v1.
|
||||
7. **Foutafhandeling**: push/PR-aanmaak-fail → `update_job_status('failed',
|
||||
error: …)`; geen force-push, geen automatische retry.
|
||||
|
||||
### Interactie tussen beide
|
||||
8. **Worker-PRs gebruiken hetzelfde labelsysteem als alle andere PRs.**
|
||||
Default = preview deploy, auto-merge wacht op CI groen, na merge
|
||||
prod-deploy (mits path-filter zegt "code"). De worker zet **geen**
|
||||
labels automatisch — als je batch-output zonder preview wilt mergen
|
||||
moet je `skip-deploy` zelf toevoegen, of preview later uitzetten via
|
||||
een product-instelling (out-of-scope v1).
|
||||
9. **Implementatievolgorde**: eerst deploy-controle (infra,
|
||||
onafhankelijk), daarna auto-PR (afhankelijk van stabiele deploy-flow).
|
||||
|
||||
## Architectuur in één plaat
|
||||
|
||||
```
|
||||
auto-merge wacht op
|
||||
[story-job DONE] ─push branch─┐ deploy-preview groen
|
||||
▼ │
|
||||
[laatste story?]──ja──[PR + auto-merge]──CI──┴──merge naar main
|
||||
│
|
||||
[job: ci] altijd
|
||||
│
|
||||
[paths-filter]
|
||||
│
|
||||
├ PR → deploy-preview
|
||||
│ if code && !skip-deploy
|
||||
│ || force-deploy
|
||||
│
|
||||
└ push → deploy-production
|
||||
if code
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Deel A — Deploy-controle
|
||||
|
||||
### A.1 `vercel.json` — Vercel Git-deploy uitzetten
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "https://openapi.vercel.sh/vercel.json",
|
||||
"git": { "deploymentEnabled": false },
|
||||
"crons": [
|
||||
{ "path": "/api/cron/expire-questions", "schedule": "0 4 * * *" },
|
||||
{ "path": "/api/cron/cleanup-agent-artifacts", "schedule": "0 3 * * *" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Effect: Vercel deployt niet meer automatisch op git-events. Alleen
|
||||
`vercel deploy` vanuit de workflow (met `VERCEL_TOKEN`) maakt nog
|
||||
deployments.
|
||||
|
||||
### A.2 `.github/workflows/ci.yml` — path-filter + label-checks
|
||||
|
||||
Triggers uitbreiden met `workflow_dispatch`:
|
||||
|
||||
```yaml
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
target:
|
||||
type: choice
|
||||
description: Deploy target
|
||||
options: [preview, production]
|
||||
default: preview
|
||||
```
|
||||
|
||||
Nieuwe job vóór de deploy-jobs:
|
||||
|
||||
```yaml
|
||||
changes:
|
||||
name: Detect deploy-relevant changes
|
||||
runs-on: ubuntu-latest
|
||||
needs: ci
|
||||
outputs:
|
||||
code: ${{ steps.filter.outputs.code }}
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: dorny/paths-filter@v3
|
||||
id: filter
|
||||
with:
|
||||
filters: |
|
||||
code:
|
||||
- 'app/**'
|
||||
- 'components/**'
|
||||
- 'lib/**'
|
||||
- 'actions/**'
|
||||
- 'stores/**'
|
||||
- 'prisma/**'
|
||||
- 'public/**'
|
||||
- 'package.json'
|
||||
- 'package-lock.json'
|
||||
- 'next.config.ts'
|
||||
- 'tsconfig.json'
|
||||
- 'vercel.json'
|
||||
- 'proxy.ts'
|
||||
- 'middleware.ts'
|
||||
- '.github/workflows/**'
|
||||
```
|
||||
|
||||
`deploy-preview` if-conditie aanpassen:
|
||||
|
||||
```yaml
|
||||
deploy-preview:
|
||||
needs: [ci, changes]
|
||||
if: |
|
||||
github.event_name == 'pull_request' && (
|
||||
(needs.changes.outputs.code == 'true'
|
||||
&& !contains(github.event.pull_request.labels.*.name, 'skip-deploy'))
|
||||
|| contains(github.event.pull_request.labels.*.name, 'force-deploy')
|
||||
)
|
||||
```
|
||||
|
||||
`deploy-production` if-conditie aanpassen:
|
||||
|
||||
```yaml
|
||||
deploy-production:
|
||||
needs: [ci, changes]
|
||||
if: |
|
||||
github.ref == 'refs/heads/main'
|
||||
&& github.event_name == 'push'
|
||||
&& needs.changes.outputs.code == 'true'
|
||||
```
|
||||
|
||||
Nieuwe `deploy-manual` job voor `workflow_dispatch` met `inputs.target`
|
||||
→ `vercel deploy` of `vercel deploy --prod`.
|
||||
|
||||
### A.3 GitHub-labels aanmaken
|
||||
|
||||
```bash
|
||||
gh label create skip-deploy --color BFBFBF --description "Preview-deploy overslaan"
|
||||
gh label create force-deploy --color 0E8A16 --description "Forceer deploy ondanks path-filter"
|
||||
```
|
||||
|
||||
### A.4 Documentatie
|
||||
|
||||
`docs/runbooks/deploy-control.md` — triggers, labels, path-filter,
|
||||
voorbeelden. `CLAUDE.md` § Deployment-regel verwijst naar runbook.
|
||||
|
||||
---
|
||||
|
||||
## Deel B — Auto-PR (worker → GitHub)
|
||||
|
||||
### B.1 Acceptatiecriteria (uit IDEA-007)
|
||||
|
||||
- **AC 1 — Push per story**: Na succesvolle `update_job_status('done')`
|
||||
pusht de worker via HTTPS (`https://$GITHUB_TOKEN@github.com/…`) naar
|
||||
origin. Push-timestamp via nieuwe MCP-call in `ClaudeJob.pushed_at`.
|
||||
- **AC 2 — Detectie laatste story**: Nieuwe MCP-call `check_pbi_complete`
|
||||
retourneert `{ complete: boolean, pbi_id }`.
|
||||
- **AC 3 — PR aanmaken**: Op `complete: true` POST naar
|
||||
`/repos/{owner}/{repo}/pulls`; titel/body uit PBI-naam + voltooide
|
||||
stories; PR-URL via `set_pbi_pr`.
|
||||
- **AC 4 — Auto-merge activeren**: Direct na PR-aanmaak GraphQL
|
||||
`enablePullRequestAutoMerge` (SQUASH).
|
||||
- **AC 5 — Foutafhandeling**: push/PR-fail →
|
||||
`update_job_status('failed', error)`; PR-URL blijft bewaard voor
|
||||
handmatige inspectie.
|
||||
|
||||
### B.2 Server-side wijzigingen (Scrum4Me-repo)
|
||||
|
||||
Velden bestaan al in schema:
|
||||
- `Product.auto_pr Boolean @default(false)` (regel 176)
|
||||
- `Pbi.pr_url String?` + `Pbi.pr_merged_at DateTime?` (regel 207–208)
|
||||
- `ClaudeJob.pushed_at DateTime?` + `ClaudeJob.pr_url String?` +
|
||||
`ClaudeJob.branch String?` (regel 335, 338, 339)
|
||||
|
||||
Geen migratie nodig.
|
||||
|
||||
Server actions / REST: bestaande `set_pbi_pr` en `mark_pbi_pr_merged`
|
||||
MCP-tools blijven. Nieuwe action:
|
||||
- `actions/jobs.ts` → `recordJobPushedAtAction(jobId)` voor
|
||||
`pushed_at`-write (als die nog niet via MCP gaat).
|
||||
|
||||
### B.3 MCP-laag (`scrum4me-mcp`-repo)
|
||||
|
||||
Nieuwe tool:
|
||||
- `check_pbi_complete(pbi_id) → { complete: boolean, pbi_id }`. Leest
|
||||
alle ClaudeJobs gelinkt aan PBI; aggregeert status. `complete = true`
|
||||
als **alle** story-jobs status DONE hebben.
|
||||
|
||||
Uitbreiding bestaande tool:
|
||||
- `update_job_status`: bij `status: 'done'` ook `pushed_at` accepteren
|
||||
(worker geeft timestamp door).
|
||||
- `set_pbi_pr`: ongewijzigd, bestaat al.
|
||||
|
||||
Schema-drift watchdog (`docs/runbooks/mcp-integration.md`) moet groen
|
||||
voor merge.
|
||||
|
||||
### B.4 Worker-laag (lokaal Claude-CLI worker)
|
||||
|
||||
Nieuwe stappen na elke story:
|
||||
|
||||
```
|
||||
1. update_job_status('done', pushed_at: null) ← bestaand
|
||||
2. git push https://$GITHUB_TOKEN@github.com/$OWNER/$REPO.git $BRANCH
|
||||
3. record_pushed_at(job_id, now) ← nieuwe MCP-call
|
||||
4. { complete } = check_pbi_complete(pbi_id)
|
||||
5. if complete:
|
||||
prNumber = POST /repos/.../pulls
|
||||
set_pbi_pr(pbi_id, pr_url)
|
||||
enablePullRequestAutoMerge(prNumber, MERGE_METHOD: SQUASH)
|
||||
6. on any HTTP/git failure → update_job_status('failed', error)
|
||||
```
|
||||
|
||||
GITHUB_TOKEN-scope: `repo` voor private, `public_repo` voor public.
|
||||
Documenteer in worker-readme.
|
||||
|
||||
### B.5 Repo-instellingen (handmatig, one-time)
|
||||
|
||||
- GitHub repo Settings → General → "Allow auto-merge" → **aanvinken**.
|
||||
- Branch protection op `main`: required CI checks = `ci`,
|
||||
`deploy-preview` is **niet** required (kan skipped zijn door label).
|
||||
|
||||
---
|
||||
|
||||
## Deel C — Interactie & demo-policy
|
||||
|
||||
### C.1 Interactie deploy-controle ↔ auto-PR
|
||||
|
||||
| Scenario | Preview-deploy | Prod-deploy bij merge |
|
||||
|--------------------------------------------------|----------------|------------------------|
|
||||
| Worker maakt PR met code-changes (default) | ✅ runt | ✅ runt |
|
||||
| Worker maakt PR met `skip-deploy` (manueel toegevoegd) | ❌ skipped | ✅ runt |
|
||||
| Worker maakt PR met enkel docs-changes (path-filter) | ❌ skipped | ❌ skipped |
|
||||
| User voegt `force-deploy` toe aan doc-only PR | ✅ runt | ✅ runt (path-filter) of ❌ (doc-only push) |
|
||||
|
||||
Auto-merge wacht op required CI checks. `deploy-preview` mag skipped
|
||||
zijn — branch protection markeert hem niet als required.
|
||||
|
||||
### C.2 Demo-policy
|
||||
|
||||
Auto-PR-flow draait op de worker, niet vanuit de webapp. Geen
|
||||
demo-sessie kan deze code triggeren — geen extra proxy.ts of
|
||||
`session.isDemo`-guards nodig. Wel: `check_pbi_complete` MCP-call moet
|
||||
`requireWriteAccess` doen (consistent met andere write-MCP-tools), zodat
|
||||
demo-tokens hem niet kunnen aanroepen.
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
## Deel D — Sync-tab op Idea-detail (zicht op voortgang)
|
||||
|
||||
### D.1 Wat bestaat al
|
||||
|
||||
- `model StoryLog` (`prisma/schema.prisma:251`) met types
|
||||
`IMPLEMENTATION_PLAN | TEST_RESULT | COMMIT`, plus `commit_hash`,
|
||||
`commit_message`, `metadata`. **Dit is de activiteitenlog.**
|
||||
- MCP-tools `log_implementation`, `log_commit`, `log_test_result`
|
||||
schrijven naar deze tabel.
|
||||
- UI-component `components/shared/story-log.tsx` rendert
|
||||
`StoryLogEntry[]` met type-styling.
|
||||
- `Story.status`, `ClaudeJob.pushed_at/branch/pr_url`,
|
||||
`Pbi.pr_url/pr_merged_at` zijn al gevuld door bestaande flows.
|
||||
|
||||
Geen nieuwe tabellen, geen migraties.
|
||||
|
||||
### D.2 Nieuwe tab op `/ideas/[id]`
|
||||
|
||||
Voeg vijfde tab **Sync** toe (naast Idee · Grill · Plan · Timeline) op
|
||||
Idea-detail-page. Alleen zichtbaar als `Idea.status === 'PLANNED'` en
|
||||
`pbi_id` gevuld.
|
||||
|
||||
Layout per tab-content:
|
||||
- Header: PBI-link + `pr_url` + `pr_merged_at` als badge.
|
||||
- Per Story (volgorde uit PBI): collapsible card met:
|
||||
- **Story-header**: code · titel · status-badge.
|
||||
- **Job-rij**: voor elke `ClaudeJob` (kind=TASK_IMPLEMENTATION) gelinkt
|
||||
aan een Task van deze Story → status, `branch`, `pushed_at`,
|
||||
`pr_url`. Toont "geen job" als nog niets gequeued.
|
||||
- **Activity-log**: `<StoryLog logs={logs} repoUrl={product.repo_url} />`
|
||||
— bestaande component, ongewijzigd.
|
||||
|
||||
### D.3 Server-laag
|
||||
|
||||
Nieuwe loader in `app/(app)/ideas/[id]/page.tsx` (of nieuw
|
||||
`sync-tab-server.ts`):
|
||||
|
||||
```ts
|
||||
async function loadIdeaSyncData(ideaId: string, userId: string) {
|
||||
// Auth-scope: idea.user_id === userId (M12-keuze 2)
|
||||
return prisma.idea.findFirst({
|
||||
where: { id: ideaId, user_id: userId },
|
||||
include: {
|
||||
pbi: {
|
||||
include: {
|
||||
stories: {
|
||||
orderBy: { sort_order: 'asc' },
|
||||
include: {
|
||||
tasks: { include: { claude_jobs: true } },
|
||||
logs: { orderBy: { created_at: 'desc' } },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
Server-only. Nooit importeren in client component (zie hardstop
|
||||
`*-server.ts` regel).
|
||||
|
||||
### D.4 Realtime refresh
|
||||
|
||||
Sync-tab abonneert op bestaande SSE-streams:
|
||||
- `app/api/realtime/solo/route.ts` — `JobPayload` voor job-status-updates
|
||||
(al uitgebreid met `kind` en `idea_id` per Deel B).
|
||||
- `app/api/realtime/notifications/route.ts` — voor StoryLog-inserts; als
|
||||
story_logs nog geen pg_notify-trigger heeft, voeg er een toe (nieuwe
|
||||
migratie, payload `{op: 'INSERT', entity: 'story_log', id, story_id}`).
|
||||
|
||||
Op event → `router.refresh()` of `revalidate` van Sync-tab data.
|
||||
|
||||
### D.5 PBI-33 als live testgeval
|
||||
|
||||
PBI-33 is **nu** in TODO + gequeued als ClaudeJobs (gebruiker bevestigt:
|
||||
"taken op TODO gezet en claude-job aangemaakt"). Verwacht gedrag zodra
|
||||
deze sprint live is:
|
||||
|
||||
| Moment | Sync-tab toont |
|
||||
|----------------------------|-----------------------------------------------|
|
||||
| Job QUEUED | "Wachtend op worker" |
|
||||
| Job RUNNING | Status RUNNING + log-entry IMPLEMENTATION_PLAN|
|
||||
| Worker commit | log-entry COMMIT (hash + message) |
|
||||
| Worker test | log-entry TEST_RESULT (status) |
|
||||
| Worker push (Deel B AC 1) | `branch` + `pushed_at` zichtbaar |
|
||||
| Laatste story → PR | PBI.`pr_url` zichtbaar |
|
||||
| Auto-merge | PBI.`pr_merged_at` zichtbaar |
|
||||
|
||||
Als één van deze niet verschijnt: bug in MCP-tool of worker (niet in
|
||||
sync-tab zelf).
|
||||
|
||||
---
|
||||
|
||||
## Bestanden
|
||||
|
||||
| Wijziging | Pad |
|
||||
|-------------------|--------------------------------------------------|
|
||||
| Edit | `vercel.json` |
|
||||
| Edit | `.github/workflows/ci.yml` |
|
||||
| Nieuw | `docs/runbooks/deploy-control.md` |
|
||||
| Edit | `CLAUDE.md` (verwijzing toevoegen) |
|
||||
| Nieuw (mcp-repo) | `src/tools/check-pbi-complete.ts` |
|
||||
| Edit (mcp-repo) | `src/tools/update-job-status.ts` (pushed_at) |
|
||||
| Edit | `actions/jobs.ts` (optioneel: record-pushed-at) |
|
||||
| Edit | Worker-script (post-story-hook + PR-aanmaak) |
|
||||
| Doc | `docs/runbooks/auto-pr-flow.md` (worker-flow) |
|
||||
| Nieuw | `app/(app)/ideas/[id]/sync-tab-server.ts` |
|
||||
| Nieuw | `components/ideas/idea-sync-tab.tsx` |
|
||||
| Edit | `app/(app)/ideas/[id]/page.tsx` (5e tab toevoegen) |
|
||||
| Migratie | `prisma/migrations/<ts>_story_logs_notify/migration.sql` (pg_notify-trigger op story_logs) |
|
||||
| Edit | `app/api/realtime/notifications/route.ts` (story_log-payload doorlaten) |
|
||||
| GitHub (extern) | Labels `skip-deploy`, `force-deploy` aanmaken |
|
||||
| GitHub (extern) | Repo Settings → "Allow auto-merge" aan |
|
||||
| Vercel-dashboard | `git.deploymentEnabled: false` actief verifiëren |
|
||||
|
||||
## Implementatievolgorde
|
||||
|
||||
1. **Deel A — Deploy-controle**
|
||||
1. `vercel.json` aanpassen
|
||||
2. `ci.yml` uitbreiden (path-filter, labels, dispatch)
|
||||
3. Labels op GitHub aanmaken
|
||||
4. Runbook + CLAUDE.md-verwijzing
|
||||
5. Test-PR voor elk scenario (zie Verificatie)
|
||||
|
||||
2. **Deel D — Sync-tab** (kan parallel met B; alleen DB-reads + UI)
|
||||
1. `loadIdeaSyncData` server-loader
|
||||
2. `idea-sync-tab.tsx` component met `<StoryLog>`-hergebruik
|
||||
3. 5e tab in `app/(app)/ideas/[id]/page.tsx`
|
||||
4. pg_notify-trigger op `story_logs` + SSE-route uitbreiden
|
||||
5. **Live test op PBI-33** (sprint loopt al — check of activity
|
||||
verschijnt zodra worker logs schrijft)
|
||||
|
||||
3. **Deel B — Auto-PR**
|
||||
1. MCP `check_pbi_complete` + `update_job_status(pushed_at)` PR
|
||||
(parallel-repo, schema-drift-watchdog groen)
|
||||
2. Worker-hook: push na done, PR + auto-merge bij complete
|
||||
3. Repo-instelling "Allow auto-merge" aan
|
||||
4. End-to-end smoke met één test-PBI
|
||||
|
||||
## Verificatie
|
||||
|
||||
Lokaal:
|
||||
|
||||
```bash
|
||||
npm run lint && npm test && npm run build
|
||||
```
|
||||
|
||||
Workflow-syntax:
|
||||
|
||||
```bash
|
||||
gh workflow view ci.yml
|
||||
```
|
||||
|
||||
End-to-end deploy-controle:
|
||||
|
||||
1. **Doc-only PR** → `deploy-preview` skipped.
|
||||
2. **Doc-only PR + `force-deploy`** → `deploy-preview` runt.
|
||||
3. **Code-PR + `skip-deploy`** → `deploy-preview` skipped.
|
||||
4. **Code-PR zonder labels** → `deploy-preview` runt.
|
||||
5. **Push naar `main` met code-change** → `deploy-production` runt.
|
||||
6. **Push naar `main` doc-only** → `deploy-production` skipped.
|
||||
7. **`workflow_dispatch` target=production** → manuele prod.
|
||||
8. **Vercel dashboard** → geen auto-deploy bij geforceerde test-push.
|
||||
|
||||
End-to-end auto-PR:
|
||||
|
||||
9. Maak een test-PBI met 1 story + 1 task.
|
||||
10. Worker draait → na `done`: `pushed_at` gevuld, branch op origin
|
||||
zichtbaar.
|
||||
11. `check_pbi_complete` → `complete: true`.
|
||||
12. PR verschijnt op GitHub met titel = PBI-naam, body = story-list.
|
||||
13. Auto-merge actief; CI groen → squash-merge.
|
||||
14. `mark_pbi_pr_merged` getriggerd door `pull_request: closed`-webhook
|
||||
(al bestaand) → `Pbi.pr_merged_at` gevuld.
|
||||
15. Push-event op `main` → `deploy-production` runt (path-filter ja).
|
||||
16. **Failure-test**: revoke GITHUB_TOKEN tijdelijk → push faalt →
|
||||
`update_job_status('failed')` met error; geen PR aangemaakt.
|
||||
|
||||
## Out-of-scope (v1)
|
||||
|
||||
- UI-toggle voor `auto_pr` per product (veld bestaat, geen UI-wiring).
|
||||
- GitHub App-installatie (per-repo tokens, scopes-finetuning).
|
||||
- Multi-repo PBI's (huidig ontwerp: één `repo_url` per PBI).
|
||||
- Force-push / non-fast-forward retry-flow.
|
||||
- Notificaties (Slack, e-mail) bij merge of CI-failure.
|
||||
- Rollback-flow bij gemergende regressie.
|
||||
- Migratie naar `vercel.ts` (knowledge-update beveelt het aan; later).
|
||||
- Auto-skip preview-deploy specifiek voor worker-PRs op basis van
|
||||
product-instelling.
|
||||
499
docs/old/plans/docs-restructure-ai-lookup.md
Normal file
499
docs/old/plans/docs-restructure-ai-lookup.md
Normal 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 25–390 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 (5–8 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`).
|
||||
148
docs/old/plans/v1-readiness.md
Normal file
148
docs/old/plans/v1-readiness.md
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
---
|
||||
title: "Scrum4Me — v1.0 readiness"
|
||||
status: active
|
||||
audience: [maintainer, contributor]
|
||||
language: nl
|
||||
last_updated: 2026-05-04
|
||||
---
|
||||
|
||||
# Scrum4Me — v1.0 readiness
|
||||
|
||||
**Versie:** v0.9.0 (zojuist gepusht naar productie via Vercel)
|
||||
**Doel:** v1.0.0 als eerste stabiele release. Living document — bijwerken na elke sprint of merge naar `main`.
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
De kernfunctionaliteit (auth, producten, PBI/story/task-hiërarchie, sprints, solo-paneel, REST-API, MCP-integratie, QR-pairing, mobile-shell) is **af en in productie**. Tests, lint, build en doc-link-checker zijn allemaal groen. Wat ontbreekt voor v1 is geen feature-werk maar **launch-discipline**: een paar UI-gaten dichten, ops-instrumentatie (error monitoring, rate-limiting beredeneren), accessibility-audit, en de stale backlog-index opschonen. Alle "Expliciet buiten scope voor v1"-items uit de functional spec ([docs/specs/functional.md:20](../specs/functional.md)) blijven bewust uit scope.
|
||||
|
||||
---
|
||||
|
||||
## What's already done
|
||||
|
||||
- **#3 Rate-limiting op alle mutation-endpoints** — `enforceUserRateLimit(scope, userId)` helper in `lib/rate-limit.ts` met 11 nieuwe scopes; toegepast op create-actions (PBI/Story/Task/Todo/Sprint/Product/Token), enqueueClaudeJob(s), answerQuestion, en API-routes (story log POST, avatar upload). Limits zijn ruim genoeg voor normaal gebruik, eng genoeg om abuse-loops te stoppen
|
||||
- **#2 Sentry error-monitoring** — `@sentry/nextjs` geconfigureerd via PR [#85](https://github.com/madhura68/Scrum4Me/pull/85); SDK is no-op zonder DSN, activatie via Vercel env-vars
|
||||
- **#1 Edit-icoon op Product** (todo `cmoq3ox51`) — pencil-icoon op dashboard-card via PR [#83](https://github.com/madhura68/Scrum4Me/pull/83); product-detail-header behoudt tekst
|
||||
- v0.9.0 ([release](https://github.com/madhura68/Scrum4Me/releases/tag/v0.9.0)): mobile-shell met landscape-lock (PBI-11, 7 stories, 21 tasks)
|
||||
- v0.4.0 t/m v0.8.x: ondermeer sprint-screen filter-popover + edit-iconen, PBI/story/task edit-icons, code-velden verplicht, demo read-only, M11 Claude-vragen-kanaal, M10 QR-pairing
|
||||
- CI op `main` en PR's: lint + typecheck + prisma validate + test + build via [`.github/workflows/ci.yml`](../../.github/workflows/ci.yml)
|
||||
- 432 unit/integration tests · 60 test-files · doc-link-checker 86/86 valid
|
||||
- Drie architectuur-beslissingen voor mobile geformaliseerd in [docs/architecture/project-structure.md](../architecture/project-structure.md)
|
||||
|
||||
---
|
||||
|
||||
## Now
|
||||
|
||||
Korte lijst (3-5 items) die je vóór de v1.0-tag wil afronden. Deze blokkeren een betekenisvolle launch.
|
||||
|
||||
### 1. ~~Edit-icoon op Product~~ ✅ klaar in PR [#83](https://github.com/madhura68/Scrum4Me/pull/83)
|
||||
|
||||
Verschoven naar *What's already done*. Pencil-icoon op dashboard-card; product-detail page-header behoudt tekst (matched naast andere text-acties).
|
||||
|
||||
### 2. Error monitoring (Sentry of vergelijkbaar)
|
||||
|
||||
CI vangt build-fouten af, maar er is geen runtime-monitoring. Voor een echte v1 wil je productie-fouten zien voordat een gebruiker het meldt. Vercel heeft native Sentry-integratie (Marketplace → Sentry).
|
||||
|
||||
Concreet:
|
||||
- `npm i @sentry/nextjs`
|
||||
- `npx @sentry/wizard@latest -i nextjs`
|
||||
- DSN als env-var via Vercel project settings (development + production environments)
|
||||
- Sample-rate conservatief (10% performance, 100% errors) — Hobby-plan-vriendelijk
|
||||
- Bevestig dat Postgres-LISTEN/NOTIFY-fouten in worker-routes (`/api/realtime/*`) gevangen worden
|
||||
|
||||
### 3. ~~Rate-limiting op alle mutation-endpoints~~ ✅ klaar
|
||||
|
||||
Verschoven naar *What's already done*. Helper `enforceUserRateLimit(scope, userId)` in `lib/rate-limit.ts` toegepast op alle high-value create-paths.
|
||||
|
||||
### 4. Accessibility audit op happy-path
|
||||
|
||||
`@base-ui/react` levert WAI-ARIA defaults; we gebruiken semantische HTML; maar er is geen audit-bewijs.
|
||||
|
||||
Concreet:
|
||||
- DevTools Lighthouse a11y-pass op `/login`, `/dashboard`, `/products/[id]`, `/products/[id]/sprint`, `/products/[id]/solo`, `/m/products/[id]`, `/m/products/[id]/solo`
|
||||
- Score-doel ≥95 per pagina
|
||||
- Fix wat onder de 95 valt — meestal contrast of missende labels
|
||||
- Documenteer score in [docs/specs/functional.md § Niet-functionele vereisten](../specs/functional.md)
|
||||
|
||||
---
|
||||
|
||||
## Next
|
||||
|
||||
Belangrijk maar niet-blokkerend voor v1.
|
||||
|
||||
### Backlog-index sync
|
||||
|
||||
[docs/old/backlog/index.md](../old/backlog/index.md) toont M10 (ST-1001 t/m 1008) en M11 (ST-1101 t/m 1108) als unchecked, terwijl ze allemaal gemerged zijn. Loop één keer door en zet `[x]`. Is een 5-min-job die de doc weer betrouwbaar maakt voor wie 'm leest.
|
||||
|
||||
### Solo observaties (todo `cmohuu5h8`)
|
||||
|
||||
"Filters en sortering. blokjes kleiner maken 2 op een rij" — UX-polish op het Solo-paneel. Niet trivial: vereist een filter-popover-pattern (we hebben er net een uitgerold op het sprint-screen — herbruik kan).
|
||||
|
||||
### Algemene observaties (todo `cmohthfyw`)
|
||||
|
||||
"Dunne border om tekstvlakken (onzichtbaar als niet actief), default active PB kiezen, hover-card voor detail-info, landingpage AI-assisted/AI-driven framing." — verzameling kleinere UI-aanscherpingen, ieder eigen scope.
|
||||
|
||||
### ToDo prioriteit + AI-suggesties (todos `cmohtgdwf`, `cmohswbb9`)
|
||||
|
||||
Twee verwante todo's over de todo-feature uitbreiden. Past bij de strategische richting "AI-driven dev-flow" maar geen v1-blokker.
|
||||
|
||||
---
|
||||
|
||||
## Before launch
|
||||
|
||||
Must-do voor publieke aankondiging, maar mag pas vlak vóór v1.0-tag.
|
||||
|
||||
- [ ] **Smoke-test productie** — checklist klaar in [docs/runbooks/v1-smoke-test.md](../runbooks/v1-smoke-test.md), 11 secties, ~15 min
|
||||
- [ ] **PWA-installatie test** op echt mobiel (Android + iOS) — bevestig manifest landscape, controleer iOS-fallback via CSS-overlay
|
||||
- [x] **Demo-policy regression-pass** — code-side gefixt: 3 gaps gedicht (toggleTodo, archiveCompletedTodos, leaveProduct). Drielaags-block geverifieerd voor alle mutation-actions
|
||||
- [x] **Privacy review** — Sentry sendDefaultPii=false; geen PII in logs; 4 debug-routes nu NODE_ENV-guarded (404 in productie)
|
||||
- [x] **README + Quick start verifiëren** — test-count 69 → 445 gecorrigeerd, env-vars-tabel uitgebreid (CRON_SECRET, Sentry vars), CHANGELOG-link toegevoegd
|
||||
- [x] **CHANGELOG.md** aangemaakt (Keep a Changelog formaat met [Unreleased] + [0.9.0])
|
||||
- [ ] **Bump naar v1.0.0** + GitHub release met release-notes
|
||||
|
||||
---
|
||||
|
||||
## Later
|
||||
|
||||
Bewust uit scope voor v1 (uit functional spec § Expliciet buiten scope) — of grotere domein-uitbreidingen die hun eigen PBI verdienen.
|
||||
|
||||
- **Daily Scrum / Sprint Review / Retrospective**-schermen — v2
|
||||
- **E-mail-uitnodigingsflow voor teams** — nu enkel via username
|
||||
- **Notificaties + reminders** — out of scope
|
||||
- **Native mobile app** — web-first; mobile-shell is genoeg
|
||||
- **Tijdregistratie / burndown-charts** — buiten positionering
|
||||
- **WIA AI agent** (todo `cmog2gzjb`) — eigen project-domein
|
||||
- **Claude-code-integratie via tabel-trigger** (todo `cmohn3728`) — past bij M12-richting maar geen v1
|
||||
- **Inspaningsmonitor-import** (todo `cmohul0ri`) — separate product
|
||||
- **GitHub Issues / Linear / Jira-integratie** — v2
|
||||
|
||||
---
|
||||
|
||||
## Priority order (quick reference)
|
||||
|
||||
```
|
||||
Now: ~~1. Edit-icoon op Product~~ ✅
|
||||
~~2. Sentry/error-monitoring~~ ✅
|
||||
~~3. Rate-limiting op mutation-endpoints~~ ✅
|
||||
4. Accessibility-audit (Lighthouse a11y ≥95)
|
||||
|
||||
Next: 5. Backlog-index.md sync
|
||||
6. Solo observaties (filters/sortering)
|
||||
7. Algemene UI-observaties
|
||||
8. Todo prioriteit + AI-suggesties
|
||||
|
||||
Before launch: 9. Smoke-test productie (desktop + mobile)
|
||||
10. PWA-installatie test op echte mobiel
|
||||
11. Demo-policy regression-pass
|
||||
12. Privacy/PII review
|
||||
13. README quick-start verificatie
|
||||
14. CHANGELOG.md
|
||||
15. Bump → v1.0.0 + release
|
||||
|
||||
Later: (zie sectie hierboven — v2-domein of buiten scope)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
*Updated: 2026-05-04 (na v0.9.0 release). Refresh dit document na elke sprint of major merge.*
|
||||
Loading…
Add table
Add a link
Reference in a new issue