docs(cleanup): archief verouderde plannen, backlog en root-duplicaten (#191)
* docs(cleanup): archief verouderde plannen, backlog en root-duplicaten
- 6 plans naar docs/old/plans/ (PBI-11/75/78, user-settings-store, Local github setup, lees-de-readme — laatste was verkeerde repo)
- docs/backlog/ naar docs/old/backlog/ (pre-MCP statische registry; live werk loopt via Scrum4Me-MCP)
- 6 root-level duplicaten naar docs/old/ (functional, {pbi,story,task}-dialog, product-backlog, backlog)
- 2 landing plans (niet uitgevoerd) krijgen archived: true frontmatter — blijven op plek maar uit INDEX
- scripts/generate-docs-index.mjs: skip docs/old/** + skip archived: true
- CLAUDE.md: rijen docs/backlog/, docs/plans/<key>-*.md, docs/manual/ weg; Track B-sectie verwijderd
- README.md / CHANGELOG.md / docs/plans/v1-readiness.md: link-fixes naar nieuwe locaties
Verify groen (lint + typecheck + 718 tests). docs/INDEX.md geregenereerd.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 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>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
d587be2fb3
commit
b39c3ec2e1
36 changed files with 1068 additions and 49 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
|
||||
371
docs/old/plans/Local github setup.md
Normal file
371
docs/old/plans/Local github setup.md
Normal file
|
|
@ -0,0 +1,371 @@
|
|||
# Advies — Zelf een Git-platform hosten naast of in plaats van GitHub
|
||||
|
||||
## Situatie
|
||||
|
||||
Je wilt onderzoeken of je lokaal of op een eigen server een Git repository-platform kunt draaien vergelijkbaar met GitHub.
|
||||
|
||||
In jouw situatie spelen mee:
|
||||
|
||||
- Next.js/Vercel apps
|
||||
- AI-workers / automation
|
||||
- batch processing
|
||||
- deploy pipelines
|
||||
- private code
|
||||
- mogelijk draaien op NAS of VPS
|
||||
- integratie met Claude Code / Codex / agents
|
||||
|
||||
Het antwoord is: ja, dit kan uitstekend.
|
||||
|
||||
---
|
||||
|
||||
# Architectuur-opties
|
||||
|
||||
## Optie 1 — Alleen een centrale Git remote
|
||||
|
||||
De lichtste oplossing.
|
||||
|
||||
Je draait alleen een zogenaamde "bare repo" op een Linux server.
|
||||
|
||||
### Voordelen
|
||||
|
||||
- extreem simpel
|
||||
- weinig resources
|
||||
- volledige controle
|
||||
- SSH push/pull
|
||||
|
||||
### Nadelen
|
||||
|
||||
- geen webinterface
|
||||
- geen PR’s
|
||||
- geen issues
|
||||
- geen gebruikersbeheer
|
||||
- geen CI/CD UI
|
||||
|
||||
### Setup
|
||||
|
||||
Server:
|
||||
|
||||
```bash
|
||||
mkdir -p /srv/git/myapp.git
|
||||
cd /srv/git/myapp.git
|
||||
git init --bare
|
||||
```
|
||||
|
||||
Client:
|
||||
|
||||
```bash
|
||||
git remote add origin ssh://user@server:/srv/git/myapp.git
|
||||
git push -u origin main
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Optie 2 — Self-hosted GitHub alternatief
|
||||
|
||||
Dit is meestal de beste keuze.
|
||||
|
||||
Software opties:
|
||||
|
||||
| Software | Omschrijving |
|
||||
|---|---|
|
||||
| Gitea | Lichtgewicht GitHub alternatief |
|
||||
| Forgejo | Community fork van Gitea |
|
||||
| GitLab | Zeer compleet maar zwaar |
|
||||
| OneDev | Moderne alles-in-één oplossing |
|
||||
|
||||
---
|
||||
|
||||
# Aanbevolen keuze: Gitea
|
||||
|
||||
## Waarom
|
||||
|
||||
Voor jouw situatie is Gitea waarschijnlijk de beste balans tussen:
|
||||
|
||||
- eenvoud
|
||||
- performance
|
||||
- features
|
||||
- beheerlast
|
||||
|
||||
Je krijgt:
|
||||
|
||||
- Git hosting
|
||||
- web UI
|
||||
- pull requests
|
||||
- issues
|
||||
- SSH support
|
||||
- webhooks
|
||||
- CI integratie
|
||||
- Docker support
|
||||
- private repos
|
||||
- multi-user support
|
||||
|
||||
---
|
||||
|
||||
# Aanbevolen architectuur voor jouw setup
|
||||
|
||||
## Huidige richting
|
||||
|
||||
```text
|
||||
MacBook
|
||||
↓
|
||||
GitHub
|
||||
↓
|
||||
Vercel deploy
|
||||
```
|
||||
|
||||
## Uitgebreide AI workflow
|
||||
|
||||
```text
|
||||
MacBook
|
||||
↓
|
||||
Gitea / GitHub
|
||||
↓ webhook
|
||||
AI Worker Server
|
||||
↓
|
||||
Repo clone
|
||||
↓
|
||||
Code generatie
|
||||
↓
|
||||
Commit + push
|
||||
↓
|
||||
PR creation
|
||||
↓
|
||||
Merge
|
||||
↓
|
||||
Vercel deploy
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Beste strategie voor jouw situatie
|
||||
|
||||
## Advies: hybride model
|
||||
|
||||
Gebruik:
|
||||
|
||||
| Component | Platform |
|
||||
|---|---|
|
||||
| publieke repos | GitHub |
|
||||
| deploys | Vercel |
|
||||
| AI worker orchestration | eigen server |
|
||||
| interne experimenten | Gitea |
|
||||
| automation | self-hosted |
|
||||
|
||||
Waarom:
|
||||
|
||||
- GitHub ecosystem blijft beschikbaar
|
||||
- recruiters herkennen GitHub
|
||||
- Copilot integratie blijft optimaal
|
||||
- minder beheer
|
||||
- sneller stabiel
|
||||
|
||||
---
|
||||
|
||||
# Wanneer volledig self-hosted interessant wordt
|
||||
|
||||
Volledig self-hosted wordt interessant als:
|
||||
|
||||
- privacy belangrijk is
|
||||
- AI agents autonoom moeten kunnen werken
|
||||
- je volledige controle wilt
|
||||
- je GitHub limieten wilt vermijden
|
||||
- je meerdere workers wilt draaien
|
||||
|
||||
Dan bouw je:
|
||||
|
||||
```text
|
||||
Gitea
|
||||
+ Postgres
|
||||
+ Docker Registry
|
||||
+ CI Runners
|
||||
+ Reverse Proxy
|
||||
+ Backups
|
||||
+ Monitoring
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Aanbevolen infrastructuur
|
||||
|
||||
## Lichtgewicht setup
|
||||
|
||||
### Hardware
|
||||
|
||||
- Synology NAS of mini-PC
|
||||
- 8–16 GB RAM
|
||||
- SSD opslag
|
||||
|
||||
### Software stack
|
||||
|
||||
| Component | Advies |
|
||||
|---|---|
|
||||
| OS | Ubuntu Server |
|
||||
| Containers | Docker Compose |
|
||||
| Git platform | Gitea |
|
||||
| Reverse proxy | Traefik |
|
||||
| Database | Postgres |
|
||||
| SSL | Let's Encrypt |
|
||||
| Deploys | Vercel |
|
||||
|
||||
---
|
||||
|
||||
# Docker Compose voorbeeld
|
||||
|
||||
```yaml
|
||||
services:
|
||||
gitea:
|
||||
image: gitea/gitea:latest
|
||||
container_name: gitea
|
||||
|
||||
ports:
|
||||
- "3000:3000"
|
||||
- "222:22"
|
||||
|
||||
volumes:
|
||||
- ./gitea:/data
|
||||
|
||||
restart: always
|
||||
```
|
||||
|
||||
Starten:
|
||||
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
Daarna bereikbaar via:
|
||||
|
||||
```text
|
||||
http://server-ip:3000
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Belangrijke aandachtspunten
|
||||
|
||||
## Backups
|
||||
|
||||
Bij self-hosting moet je zelf regelen:
|
||||
|
||||
- database backups
|
||||
- repo backups
|
||||
- disaster recovery
|
||||
|
||||
---
|
||||
|
||||
## Security
|
||||
|
||||
Je bent zelf verantwoordelijk voor:
|
||||
|
||||
- updates
|
||||
- SSH security
|
||||
- firewall
|
||||
- SSL certificaten
|
||||
- gebruikersbeheer
|
||||
|
||||
---
|
||||
|
||||
## CI/CD
|
||||
|
||||
GitHub Actions vervang je mogelijk door:
|
||||
|
||||
- Gitea Actions
|
||||
- Drone CI
|
||||
- Woodpecker CI
|
||||
- self-hosted runners
|
||||
|
||||
---
|
||||
|
||||
# Integratie met jouw AI-worker ideeën
|
||||
|
||||
Dit sluit zeer goed aan op jouw eerdere ideeën:
|
||||
|
||||
- Neon database events
|
||||
- worker servers
|
||||
- auto-generated PR’s
|
||||
- selective deploys
|
||||
- batch execution
|
||||
|
||||
Je kunt bijvoorbeeld:
|
||||
|
||||
1. story wordt aangemaakt
|
||||
2. worker krijgt event via SSE/webhook
|
||||
3. repo wordt gecloned
|
||||
4. AI implementeert wijziging
|
||||
5. commit + push
|
||||
6. PR automatisch aangemaakt
|
||||
7. review pipeline start
|
||||
8. merge → deploy
|
||||
|
||||
Dit wordt veel eenvoudiger wanneer je volledige controle hebt over de Git infrastructuur.
|
||||
|
||||
---
|
||||
|
||||
# Concrete roadmap
|
||||
|
||||
## Fase 1 — huidige setup stabiliseren
|
||||
|
||||
Hou:
|
||||
|
||||
- GitHub
|
||||
- Vercel
|
||||
- Neon
|
||||
|
||||
Voeg toe:
|
||||
|
||||
- AI worker server
|
||||
- webhooks
|
||||
- automation pipeline
|
||||
|
||||
---
|
||||
|
||||
## Fase 2 — interne Git infrastructuur
|
||||
|
||||
Installeer:
|
||||
|
||||
- Gitea
|
||||
- Docker
|
||||
- Postgres
|
||||
|
||||
Gebruik dit voor:
|
||||
|
||||
- experimenten
|
||||
- AI-generated branches
|
||||
- interne repos
|
||||
- automation testing
|
||||
|
||||
---
|
||||
|
||||
## Fase 3 — geavanceerde automation
|
||||
|
||||
Later toevoegen:
|
||||
|
||||
- self-hosted runners
|
||||
- preview environments
|
||||
- deploy approvals
|
||||
- selective deployments
|
||||
- agent orchestration
|
||||
|
||||
---
|
||||
|
||||
# Eindadvies
|
||||
|
||||
Voor jouw situatie:
|
||||
|
||||
## Niet meteen GitHub vervangen
|
||||
|
||||
Dat levert nu vooral extra beheerlast op.
|
||||
|
||||
## Wel nu al beginnen met:
|
||||
|
||||
- eigen AI worker server
|
||||
- webhook automation
|
||||
- lokale Git orchestration
|
||||
- Gitea testomgeving
|
||||
|
||||
Dat sluit perfect aan op:
|
||||
|
||||
- Scrum4Me
|
||||
- AI-assisted development
|
||||
- batch story execution
|
||||
- autonome pipelines
|
||||
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.
|
||||
198
docs/old/plans/PBI-11-mobile-shell.md
Normal file
198
docs/old/plans/PBI-11-mobile-shell.md
Normal file
|
|
@ -0,0 +1,198 @@
|
|||
# PBI-11 — Mobile-shell met landscape-lock (settings + backlog + solo)
|
||||
|
||||
> **Status:** READY · priority 3 · sort_order 8
|
||||
> **Stories:** ST-1133 (TaskDialog full-screen) · ST-1134 (foundation) · ST-1135 (UA-redirect) · ST-1136 (settings) · ST-1137 (backlog) · ST-1138 (solo) · ST-1139 (docs + E2E)
|
||||
|
||||
## Doel
|
||||
|
||||
Scrum4Me bruikbaar maken op een mobiele telefoon, beperkt tot drie schermen — Settings (account + product-selector + QR-pairing-instructie + logout), Product Backlog (PBI/Story/Task aanmaken), Solo Paneel (voortgang vastleggen). Landscape-orientatie afgedwongen via PWA-manifest + CSS-overlay. App-naam en -icoon onderdrukken op `/m/*`. Desktop-app blijft ongewijzigd.
|
||||
|
||||
## Drie architectuur-beslissingen
|
||||
|
||||
### Beslissing A — gedeelde dialog-classes (raakt ST-1133 + ST-1138)
|
||||
|
||||
Alle entity-dialogen (PbiDialog, StoryDialog, TaskDialog, TaskDetailDialog) delen dezelfde class-string in [components/shared/entity-dialog-layout.ts](../../components/shared/entity-dialog-layout.ts):
|
||||
|
||||
```ts
|
||||
export const entityDialogContentClasses = cn(
|
||||
'flex flex-col p-0 gap-0',
|
||||
'max-h-[90vh] w-full max-w-[calc(100%-2rem)]',
|
||||
'sm:max-w-[90vw] sm:max-h-[85vh]',
|
||||
'lg:max-w-[50vw] lg:min-w-[480px]',
|
||||
)
|
||||
```
|
||||
|
||||
→ Mobile-fullscreen wordt via één edit op deze constant geregeld:
|
||||
|
||||
```ts
|
||||
'max-sm:w-screen max-sm:h-screen max-sm:max-w-none max-sm:rounded-none'
|
||||
```
|
||||
|
||||
**Gevolg voor stories:**
|
||||
- ST-1133 T-317 muteert `entity-dialog-layout.ts`, niet `task-dialog.tsx` rechtstreeks
|
||||
- ST-1138 T-332 vervalt als file-edit — wordt verify-only (controleer dat TaskDetailDialog mee-erft)
|
||||
- PBI/Story-dialogen krijgen mobile-fullscreen "voor niets" (handig voor ST-1137)
|
||||
|
||||
### Beslissing B — eigen route group `app/(mobile)/`
|
||||
|
||||
Parent layout `app/(app)/layout.tsx` rendert NavBar, MinWidthBanner, StatusBar, SoloRealtimeBridge, NotificationsBridge. Een nested layout in `(app)/m/` kan deze parent-output **niet** verwijderen (Next.js layouts erven naar binnen, niet vervangen).
|
||||
|
||||
**Keuze:** verplaats `/m/*` naar een eigen route group `app/(mobile)/m/{settings,pair,products}/...` met eigen `app/(mobile)/layout.tsx`.
|
||||
|
||||
**Auth-guard duplicatie voorkomen** door `getSession()`-check te extraheren naar `lib/auth-guard.ts`:
|
||||
|
||||
```ts
|
||||
// lib/auth-guard.ts
|
||||
export async function requireSession() {
|
||||
const session = await getSession()
|
||||
if (!session.userId) redirect('/login')
|
||||
return session
|
||||
}
|
||||
```
|
||||
|
||||
Beide layouts (`(app)/layout.tsx` en `(mobile)/layout.tsx`) roepen deze helper aan. Bestaande `/m/pair/page.tsx` (M10 QR-pairing) verhuist mee naar `app/(mobile)/m/pair/page.tsx` — geen URL-wijziging, alleen filesystem-move.
|
||||
|
||||
**Gevolg voor stories:**
|
||||
- ST-1134 T-321 schrijft `app/(mobile)/layout.tsx`, niet `app/(app)/m/layout.tsx`
|
||||
- ST-1136 page wordt `app/(mobile)/m/settings/page.tsx`
|
||||
- ST-1137 page wordt `app/(mobile)/m/products/[id]/page.tsx`
|
||||
- ST-1138 page wordt `app/(mobile)/m/products/[id]/solo/page.tsx`
|
||||
- M10's `/m/pair` verhuist naar `app/(mobile)/m/pair/` — URL ongewijzigd, geen redirect-migratie nodig
|
||||
|
||||
### Beslissing C — gescheiden SplitPane cookie-key
|
||||
|
||||
ST-1137 hergebruikt `BacklogSplitPane` (drie panelen). Op mobile rendert die in tab-mode (auto-switch + back-button uit ST-1116). De SplitPane bewaart split-percentages in een cookie.
|
||||
|
||||
**Keuze:** gescheiden cookie-key voor mobile — `split-pane:backlog-3-mobile:<id>` — zodat mobile-gebruikers (die in tab-mode geen split-percentages bewerken maar wel terug kunnen schakelen) de desktop-split niet beïnvloeden.
|
||||
|
||||
**Gevolg voor stories:**
|
||||
- ST-1137 T-328 geeft expliciete `cookieKey`-prop aan `BacklogSplitPane` op de mobile-route
|
||||
|
||||
## Hergebruik (al aanwezig)
|
||||
|
||||
| Wat | Bron |
|
||||
|---|---|
|
||||
| Mobile tab-mode in `SplitPane` (incl. `tabLabels`, `mobileBreakpoint`, `activeTab`) | ST-1116 — [components/split-pane/split-pane.tsx](../../components/split-pane/split-pane.tsx) |
|
||||
| Click-cascade auto-switch in `BacklogSplitPane` | ST-1116 commit `3e86a8d` |
|
||||
| QR-pairing route `/m/pair` | M10 commit `625221f` |
|
||||
| `/m/pair` confirmation page | bestaand |
|
||||
| Functional-spec mobile-tabs sectie | `docs/specs/functional.md:234-235` |
|
||||
|
||||
## Stories
|
||||
|
||||
### ST-1133 — TaskDialog full-screen op mobile (verifieer en fix)
|
||||
|
||||
**Doel:** entity-dialogen renderen 100vw × 100vh op viewport `<640px`.
|
||||
|
||||
**Acceptance:**
|
||||
- `entityDialogContentClasses` in `components/shared/entity-dialog-layout.ts` bevat `max-sm:w-screen max-sm:h-screen max-sm:max-w-none max-sm:rounded-none`
|
||||
- Sticky header en footer blijven bereikbaar; body scrollt
|
||||
- Werkt voor TaskDialog, TaskDetailDialog, PbiDialog, StoryDialog (alle gebruiken de constant)
|
||||
- Tests dekken mobile-render via `window.innerWidth`-mock voor minstens TaskDialog en TaskDetailDialog
|
||||
- Geen regressie op desktop (`sm:max-w-[90vw]` blijft op `>=640px`)
|
||||
|
||||
**Tasks:**
|
||||
- T-316 inventariseer huidige render
|
||||
- T-317 fix de gedeelde constant
|
||||
- T-318 tests
|
||||
|
||||
### ST-1134 — Mobile shell foundation (route group + landscape-guard + tab-bar + manifest)
|
||||
|
||||
**Doel:** route group `(mobile)`, landscape-overlay, bottom tab-bar, PWA-manifest.
|
||||
|
||||
**Acceptance:**
|
||||
- `app/(mobile)/layout.tsx` rendert zonder NavBar / AppIcon / MinWidthBanner / StatusBar
|
||||
- Auth-guard via gedeelde `lib/auth-guard.ts` helper; `(app)/layout.tsx` gebruikt dezelfde helper
|
||||
- `<LandscapeGuard>` toont rotate-overlay in portrait (window.matchMedia)
|
||||
- `<MobileTabBar>` bottom-fixed met 3 lucide-iconen (ListTree, Activity, Settings); tap-targets ≥44×44 px
|
||||
- `public/manifest.json` bevat `"orientation": "landscape"`
|
||||
- M10 `/m/pair` verhuist filesystem-only naar `app/(mobile)/m/pair/` — URL onveranderd
|
||||
- Tests: LandscapeGuard render-states, TabBar route-active, auth-guard helper
|
||||
|
||||
**Tasks:**
|
||||
- T-319 LandscapeGuard
|
||||
- T-320 MobileTabBar
|
||||
- T-321 `(mobile)/layout.tsx` + manifest + auth-guard extractie + filesystem-move van `/m/pair`
|
||||
|
||||
### ST-1135 — Mobile UA-redirect bij login
|
||||
|
||||
**Acceptance:**
|
||||
- `lib/user-agent.ts` exporteert `isPhoneUA(ua: string | null): boolean` op basis van `Mobi`-substring
|
||||
- `actions/auth.ts` `loginAction` redirect bij phone-UA naar `/m/products/[active]/solo`; zonder actief product naar `/m/settings`
|
||||
- Tablet-UA en desktop-UA blijven op `/dashboard`
|
||||
- Demo-user volgt zelfde routing
|
||||
- Tests dekken alle paden (phone met/zonder product, tablet, desktop, null UA, demo)
|
||||
|
||||
**Tasks:** T-322 helper · T-323 loginAction integratie · T-324 tests
|
||||
|
||||
### ST-1136 — Mobile Settings-pagina
|
||||
|
||||
**Acceptance:**
|
||||
- `app/(mobile)/m/settings/page.tsx`
|
||||
- Toont username, isDemo-badge, actief-product-naam
|
||||
- Product-selector — klik → `setActiveProductAction` + redirect `/m/products/[id]/solo`
|
||||
- QR-pairing-instructie — link "Open scrum4me.app/login op je desktop om in te loggen via QR"
|
||||
- Logout-knop met AlertDialog "Uitloggen?" → `logoutAction`
|
||||
- Geen avatar-upload, geen bio-edit
|
||||
- Tests render-states + logout-flow
|
||||
|
||||
**Tasks:** T-325 layout · T-326 logout-flow · T-327 tests
|
||||
|
||||
### ST-1137 — Mobile Product Backlog-pagina
|
||||
|
||||
**Acceptance:**
|
||||
- `app/(mobile)/m/products/[id]/page.tsx` hergebruikt PbiList/StoryPanel/TaskPanel + backlog-store
|
||||
- `BacklogSplitPane` rendert in tab-mode op `<1024px`; auto-switch op selectie blijft werken
|
||||
- TaskDialog-searchParams wiring (`?newTask=`, `?editTask=`, `?storyId=`) werkt
|
||||
- Cookie-key gescheiden: `split-pane:backlog-3-mobile:<id>`
|
||||
- + knoppen voor PBI/Story/Task werken; demo blijft read-only
|
||||
- Tests: page-rendering met initial state, tab-mode, click-cascade-flow
|
||||
|
||||
**Tasks:** T-328 page wrapper + cookie-key · T-329 TaskDialog wiring · T-330 tests
|
||||
|
||||
### ST-1138 — Mobile Solo Paneel
|
||||
|
||||
**Acceptance:**
|
||||
- `app/(mobile)/m/products/[id]/solo/page.tsx` hergebruikt SoloBoard
|
||||
- 3 kanban-kolommen blijven; horizontal scroll
|
||||
- TaskDetailDialog rendert 100vw × 100vh op `<640px` — **gedekt door beslissing A** (entityDialogContentClasses)
|
||||
- "Voer uit"-knop bereikbaar
|
||||
- SSE-stream blijft werken
|
||||
- Tests: solo-page rendert, TaskDetailDialog erft mobile-classes (zonder eigen file-edit)
|
||||
|
||||
**Tasks:**
|
||||
- T-331 page wrapper
|
||||
- T-332 verify-only (geen file-edit; controleer dat shared constant uit ST-1133 doorwerkt)
|
||||
- T-333 tests
|
||||
|
||||
### ST-1139 — Docs sync + end-to-end verificatie
|
||||
|
||||
**Acceptance:**
|
||||
- `docs/specs/functional.md` heeft "Mobile shell"-sectie; desktop-first-clausule herzien
|
||||
- `docs/architecture.md` beschrijft route group `(mobile)`, manifest landscape, UA-redirect, gedeelde auth-guard
|
||||
- `npm run lint && npm test && npm run build` slagen
|
||||
- E2E checklist (11 punten — zie hieronder)
|
||||
- Bekende limiet: iOS Safari PWA-orientation-lock werkt niet 100% — CSS-overlay als fallback
|
||||
|
||||
**Tasks:** T-334 functional-spec · T-335 architecture-doc · T-336 E2E-verificatie
|
||||
|
||||
## Verificatie (E2E checklist uit T-336)
|
||||
|
||||
1. `npm run lint && npm test && npm run build` slagen
|
||||
2. DevTools mobile-emulatie iPhone 12 landscape: `/m/products/[id]` rendert tab-mode, geen NavBar, tab-bar onderaan
|
||||
3. Portrait → rotate-overlay zichtbaar; landscape → overlay verdwijnt
|
||||
4. Tab-bar 3 iconen werken (Backlog/Solo/Settings)
|
||||
5. Login phone-UA → redirect `/m/products/[id]/solo`; desktop-UA → `/dashboard`
|
||||
6. Backlog-flow: + PBI, + Story, + Task in TaskDialog
|
||||
7. Solo-flow: tap task → TaskDetailDialog full-screen, "Voer uit"-knop bereikbaar
|
||||
8. TaskDialog full-screen op `<640px` (via shared constant)
|
||||
9. PWA-installatie test op echte mobile (Android of iOS)
|
||||
10. `/m/pair` QR-flow intact na route-group-verhuizing
|
||||
11. Demo op mobile read-only; logout via `/m/settings` werkt; geen Scrum4Me-tekst of AppIcon op `/m/*`
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Tablets (geen Mobi-UA) blijven desktop-flow gebruiken
|
||||
- iOS PWA full-orientation-lock (CSS-overlay is fallback)
|
||||
- Avatar/bio editor op mobile-settings
|
||||
- 1-koloms-kanban (3-koloms blijft, swipe horizontaal)
|
||||
128
docs/old/plans/PBI-75-sprint-task-edit-store.md
Normal file
128
docs/old/plans/PBI-75-sprint-task-edit-store.md
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
# PBI-75 — Sprint task-edit client-side via workspace-store
|
||||
|
||||
## Context
|
||||
|
||||
In het Sprint-scherm (`/products/<id>/sprint/<sprintId>`) duurt het bewerken van een taak onevenredig lang. Klik op een taakregel of het potlood-icoon roept `router.push(?editTask=<id>)` aan vanuit [`components/sprint/task-list.tsx:226`](../../components/sprint/task-list.tsx). Dat triggert:
|
||||
|
||||
- **Volledige server re-render** van [`app/(app)/products/[id]/sprint/[sprintId]/page.tsx`](../../app/(app)/products/[id]/sprint/[sprintId]/page.tsx) met zware queries (sprint, alle sprintStories+tasks, productMembers, alle PBIs+stories voor het backlog-paneel)
|
||||
- **Tweede Prisma-query** in [`EditTaskLoader`](../../app/_components/tasks/edit-task-loader.tsx) voor task-detail (incl. `implementation_plan`)
|
||||
- **Na save**: `router.push(closePath)` + `revalidatePath` → opnieuw alle queries
|
||||
|
||||
De [`sprint-workspace-store`](../../stores/sprint-workspace/store.ts) (sinds PBI-74 Story 9) bevat al alles voor een client-side edit-flow:
|
||||
|
||||
- [`setActiveTask(taskId)`](../../stores/sprint-workspace/store.ts) (regel 337) — zet `context.activeTaskId` + roept `ensureTaskLoaded()` aan
|
||||
- [`ensureTaskLoaded(taskId)`](../../stores/sprint-workspace/store.ts) (regel 414) — `GET /api/tasks/{id}` → upsert met `_detail: true`
|
||||
- [`selectActiveTask`](../../stores/sprint-workspace/selectors.ts) (regel 91) — bestaat, nog geen consumer
|
||||
- [`applyTaskEvent`](../../stores/sprint-workspace/store.ts) (regel 748) — SSE-events propageren idempotent na server-save
|
||||
- Optimistic-mutation primitives (`applyOptimisticMutation` / `settleMutation` / `rollbackMutation`)
|
||||
|
||||
Het patroon "URL-param → store" bestaat al voor product-workspace in [`components/backlog/url-task-sync.tsx`](../../components/backlog/url-task-sync.tsx) — we volgen dat als precedent.
|
||||
|
||||
**Doel**: klik op een taak opent de edit-dialoog client-side via store-state binnen ~100ms. Geen URL-navigatie, geen server re-render, alleen `GET /api/tasks/{id}` voor het detail.
|
||||
|
||||
## Aanpak
|
||||
|
||||
**Architectuur**: store-mounted dialog + URL-sync component voor deeplinks.
|
||||
|
||||
1. **Klik-flow**: `TaskList.openEditDialog` roept `setActiveTask(taskId)` aan op de store. Geen `router.push`.
|
||||
2. **Render-flow**: nieuwe client-component `SprintTaskDialogMount` zit binnen `SprintHydrationWrapper`, subscribet `selectActiveTask`, en rendert `<TaskDialog>` zodra de active task `_detail === true` is.
|
||||
3. **Save-flow**: `TaskDialog` krijgt optionele `onClose`/`onSaved` callbacks (backwards compatible met bestaande `closePath`). Mount geeft `onClose = () => setActiveTask(null)`. Server action `saveTask` blijft `revalidatePath` doen voor server-cache; SSE-event update store via `applyTaskEvent`.
|
||||
4. **Deeplink-flow**: nieuwe `SprintUrlTaskSync` leest `?editTask=<id>` en roept `setActiveTask(id)` aan (analoog aan `url-task-sync.tsx`).
|
||||
|
||||
## Bestanden + wijzigingen
|
||||
|
||||
### Nieuw — `components/sprint/sprint-task-dialog-mount.tsx`
|
||||
Client component. Subscribet `selectActiveTask` (single-value, geen `useShallow`). Wanneer task aanwezig is en `isDetail(task)` true, mappt naar `TaskDialogTask`-shape:
|
||||
- `status`: via `taskStatusFromApi` uit [`lib/task-status.ts`](../../lib/task-status.ts) (lowercase API → Prisma UPPER_SNAKE)
|
||||
- `implementation_plan: task.implementation_plan ?? null`
|
||||
- `created_at: new Date(task.created_at)`
|
||||
|
||||
Rendert `<TaskDialog task={mapped} productId={productId} onClose={() => setActiveTask(null)} isDemo={isDemo} />`. Geen render tussen `setActiveTask` en `_detail: true` (detail-fetch <100ms).
|
||||
|
||||
### Nieuw — `components/sprint/sprint-url-task-sync.tsx`
|
||||
Kopie van [`components/backlog/url-task-sync.tsx`](../../components/backlog/url-task-sync.tsx) maar tegen `useSprintWorkspaceStore` en `writeTaskHint` uit [`stores/sprint-workspace/restore`](../../stores/sprint-workspace/restore.ts).
|
||||
|
||||
### Wijziging — `components/sprint/task-list.tsx` (regels 225-227)
|
||||
Vervang:
|
||||
```ts
|
||||
function openEditDialog(taskId: string) {
|
||||
router.push(`${pathname}?editTask=${taskId}`)
|
||||
}
|
||||
```
|
||||
door:
|
||||
```ts
|
||||
function openEditDialog(taskId: string) {
|
||||
useSprintWorkspaceStore.getState().setActiveTask(taskId)
|
||||
}
|
||||
```
|
||||
`openCreateDialog` (regel 222) blijft URL-gebaseerd — out-of-scope.
|
||||
|
||||
### Wijziging — `app/(app)/products/[id]/sprint/[sprintId]/page.tsx`
|
||||
- Verwijder `editTask` uit searchParams-destructuring (regel 36)
|
||||
- Verwijder `editTask &&`-block met `<Suspense><EditTaskLoader>` (regels 250-260)
|
||||
- Verwijder ongebruikte imports (`EditTaskLoader`, `TaskDialogSkeleton`, evt. `Suspense`)
|
||||
- Mount binnen `SprintHydrationWrapper`:
|
||||
```tsx
|
||||
<SprintHydrationWrapper ...>
|
||||
<SprintBoardClient ... />
|
||||
<SprintTaskDialogMount productId={id} isDemo={isDemo} />
|
||||
<SprintUrlTaskSync />
|
||||
</SprintHydrationWrapper>
|
||||
```
|
||||
- `newTask`-block (regels 241-248) blijft ongemoeid — out-of-scope.
|
||||
|
||||
### Wijziging — `app/_components/tasks/task-dialog.tsx`
|
||||
Maak `closePath` optioneel + voeg `onClose`/`onSaved` toe (backwards compatible):
|
||||
```ts
|
||||
interface TaskDialogProps {
|
||||
task?: TaskDialogTask
|
||||
storyId?: string
|
||||
productId: string
|
||||
closePath?: string
|
||||
onClose?: () => void
|
||||
onSaved?: (taskId: string) => void
|
||||
isDemo?: boolean
|
||||
}
|
||||
```
|
||||
Refactor de drie `router.push(closePath)`-calls (regels 104, 120, 155) naar één helper:
|
||||
```ts
|
||||
function close() {
|
||||
if (onClose) { onClose(); return }
|
||||
if (closePath) router.push(closePath)
|
||||
}
|
||||
```
|
||||
Bestaande callers (`EditTaskLoader`, mobile, product-page, sprint `newTask`-block) blijven werken via `closePath`. Nieuwe `SprintTaskDialogMount` gebruikt `onClose`.
|
||||
|
||||
### Geen wijziging
|
||||
- `stores/sprint-workspace/selectors.ts` — `selectActiveTask` bestaat al
|
||||
- `app/_components/tasks/edit-task-loader.tsx` — nog gebruikt door product-page en mobile
|
||||
|
||||
## Edge cases
|
||||
|
||||
- **Status-enum mapping**: store API-lowercase → Prisma UPPER_SNAKE via `taskStatusFromApi`, fallback `'TO_DO'`
|
||||
- **`_detail: true` race**: mount rendert pas wanneer `isDetail(task)` true is — geen flash met undefined velden
|
||||
- **Demo-mode**: prop blijft via server doorlopen, dialog respecteert al `isDemo`
|
||||
- **Dirty-close-guard**: ingebouwd in dialog (regels 107, 172) — werkt via `onClose`
|
||||
- **SSE na save**: `applyTaskEvent` updatet store automatisch
|
||||
- **Deeplink + task niet bestaat**: `GET /api/tasks/{id}` 404 → store doet niets, dialog opent niet (huidige `redirect()` verdwijnt — acceptabel)
|
||||
|
||||
## Verificatie
|
||||
|
||||
1. **Browser** (`npm run dev`): klik op task in takenlijst → dialog opent <100ms, geen URL-verandering, alleen `GET /api/tasks/<id>` in Network
|
||||
2. **Save**: wijzig titel → Opslaan → dialog sluit → store toont nieuwe titel via SSE
|
||||
3. **Deeplink**: `?editTask=<id>` → dialog opent via `SprintUrlTaskSync`
|
||||
4. **Bestaande flows ongebroken**: product-page edit, mobile edit, sprint `?newTask=1`
|
||||
5. **`npm run verify && npm run build`**
|
||||
6. **Vitest**: `__tests__/components/sprint/sprint-task-dialog-mount.test.tsx` — hydreer store, mock fetch, `setActiveTask(id)`, assert UPPER_SNAKE status + `onClose` clear
|
||||
|
||||
## Risico's
|
||||
|
||||
- Andere mounts (mobile, product-backlog, sprint `newTask`) blijven URL-gebaseerd — `closePath?` optional houdt ze werkend
|
||||
- Geen `redirect()` bij not-found-deeplink (klein UX-verschil)
|
||||
- SSE-latency 100-500ms na save — eventueel later mitigeren via `applyOptimisticMutation` in `onSaved`-callback
|
||||
|
||||
## Out-of-scope (follow-up PBIs)
|
||||
|
||||
- `?newTask=1`-flow naar store
|
||||
- Mobile + product-backlog mounts
|
||||
- `EditTaskLoader` verwijderen wanneer alle callers over zijn
|
||||
186
docs/old/plans/PBI-78-cost-analysis-widget.md
Normal file
186
docs/old/plans/PBI-78-cost-analysis-widget.md
Normal file
|
|
@ -0,0 +1,186 @@
|
|||
# PBI-78 — Cost-analyse widget op Insights-pagina
|
||||
|
||||
## Context
|
||||
|
||||
De insights-pagina heeft al een `TokenUsageCard` die KPI's + per-job tabel toont, maar **alleen voor de actieve sprint van een gefilterd product**. Daardoor mis je het globale plaatje: hoeveel geef je deze maand uit, welk model is de grootste kostenpost, hoe goed werkt prompt-caching, en welke job-kinds (IDEA_MAKE_PLAN met Opus vs TASK_IMPLEMENTATION met Sonnet) trekken het budget.
|
||||
|
||||
We voegen een nieuwe sectie **"Cost analyse"** toe (tussen Sprint Health en Plan-quality). Eén shared periode-selector (7d/30d/90d/MTD) stuurt vier visualisaties aan op basis van best practices uit de Anthropic Console + LLM-observability tools (Datadog, Portkey):
|
||||
|
||||
1. Trend-chart over tijd
|
||||
2. Breakdown per model
|
||||
3. Breakdown per job-kind
|
||||
4. Cache efficiency
|
||||
|
||||
De bestaande `TokenUsageCard` blijft staan als sprint-detail (top duurste jobs voor de actieve sprint).
|
||||
|
||||
## Bestaande infrastructuur (hergebruik)
|
||||
|
||||
**Reeds aanwezig in DB:**
|
||||
|
||||
- [prisma/schema.prisma](../../prisma/schema.prisma) — `ClaudeJob` heeft `input_tokens`, `output_tokens`, `cache_read_tokens`, `cache_write_tokens`, `actual_thinking_tokens`, `model_id`, `kind`, `finished_at`
|
||||
- `ModelPrice` tabel met prijzen per 1M tokens (input/output/cache_read/cache_write)
|
||||
- Prijzen worden gesynced via [scripts/sync-model-prices.ts](../../scripts/sync-model-prices.ts)
|
||||
|
||||
**Hergebruikbare patronen:**
|
||||
|
||||
- KPI-strip stijl: zie [app/(app)/insights/components/token-usage.tsx](../../app/(app)/insights/components/token-usage.tsx) (regels 43-64)
|
||||
- URL-param-gestuurde filter met `useTransition` + `router.replace`: zie [app/(app)/insights/components/agent-throughput.tsx](../../app/(app)/insights/components/agent-throughput.tsx) (regels 38-62)
|
||||
- Recharts BarChart pattern: zie [app/(app)/insights/components/agent-throughput.tsx](../../app/(app)/insights/components/agent-throughput.tsx) (regels 110-130)
|
||||
- Cost-formule (zelfde overal): zie [lib/insights/token-stats.ts](../../lib/insights/token-stats.ts) (regels 73-79) — input + output + cache_read + cache_write + thinking, allemaal `× price_per_1m / 1_000_000`
|
||||
- Server component → parallel data-fetch via `Promise.all`: zie [app/(app)/insights/page.tsx](../../app/(app)/insights/page.tsx) (regels 46-80)
|
||||
|
||||
## Te bouwen
|
||||
|
||||
### Taak 1 — Data-laag — `lib/insights/cost-analysis.ts` (nieuw)
|
||||
|
||||
Eén bestand met vijf functies, allemaal `(userId, period)` als parameters. Period wordt naar `WHERE cj.finished_at >= NOW() - INTERVAL '<n> days'` vertaald (MTD = `>= date_trunc('month', NOW())`).
|
||||
|
||||
```ts
|
||||
export type Period = '7d' | '30d' | '90d' | 'mtd'
|
||||
|
||||
export interface CostKpi {
|
||||
totalCostUsd: number
|
||||
totalTokens: number
|
||||
jobCount: number
|
||||
avgPerDayUsd: number
|
||||
cacheSavingsUsd: number // (input_price - cache_read_price) × cache_read_tokens
|
||||
topModelId: string | null
|
||||
topModelCostUsd: number
|
||||
}
|
||||
|
||||
export interface CostByDayRow { day: string; costUsd: number }
|
||||
export interface CostByModelRow { modelId: string; costUsd: number; jobCount: number }
|
||||
export interface CostByKindRow { kind: string; costUsd: number; jobCount: number }
|
||||
export interface CacheEfficiency {
|
||||
cacheReadTokens: number
|
||||
uncachedInputTokens: number
|
||||
cacheHitRatio: number // cache_read / (cache_read + input)
|
||||
savingsUsd: number
|
||||
spentOnCacheWriteUsd: number
|
||||
}
|
||||
|
||||
export async function getCostKpi(userId: string, period: Period): Promise<CostKpi>
|
||||
export async function getCostByDay(userId: string, period: Period): Promise<CostByDayRow[]>
|
||||
export async function getCostByModel(userId: string, period: Period): Promise<CostByModelRow[]>
|
||||
export async function getCostByKind(userId: string, period: Period): Promise<CostByKindRow[]>
|
||||
export async function getCacheEfficiency(userId: string, period: Period): Promise<CacheEfficiency>
|
||||
```
|
||||
|
||||
**Belangrijke details:**
|
||||
|
||||
- Alle queries: `WHERE cj.user_id = ${userId} AND cj.status = 'DONE' AND cj.finished_at >= <periodStart>`
|
||||
- Geen `productAccessFilter` nodig — `cj.user_id = ${userId}` filtert al op de eigenaar
|
||||
- `getCostByDay` vult ontbrekende dagen op met `0` (anders breekt de chart-x-as) — vul aan client- of server-side, kies één
|
||||
- Periode → days mapping inline: `7d`→7, `30d`→30, `90d`→90, `mtd`→huidige dag-van-maand
|
||||
- Cache savings: `cache_read_tokens × (input_price - cache_read_price) / 1_000_000` — "wat je betaald zou hebben zonder cache, minus wat je betaalde mét cache"
|
||||
|
||||
### Taak 2 — UI — `app/(app)/insights/components/cost-analysis.tsx` (nieuw)
|
||||
|
||||
Eén client-component die de hele sectie rendert. Structuur:
|
||||
|
||||
```
|
||||
[Period selector rechtsboven]
|
||||
[KPI strip: Totaal | Cache savings | Avg/dag | Top model ($X op claude-opus-4-7)]
|
||||
[grid grid-cols-1 md:grid-cols-2 gap-4]
|
||||
[Daily cost line/bar chart] [Model breakdown - horizontal bar of donut]
|
||||
[Job-kind breakdown - bar] [Cache efficiency - donut + label "X% hit, $Y bespaard"]
|
||||
```
|
||||
|
||||
**Period selector:** kopieer pattern uit [agent-throughput.tsx](../../app/(app)/insights/components/agent-throughput.tsx) (regels 50-61) — `useTransition` + `router.replace` met `?period=` in URL. Default tonen als "30d".
|
||||
|
||||
**Charts:** Recharts (al gebruikt in `BurndownChart`, `AgentThroughputCard`, `VelocityChart`):
|
||||
|
||||
- Daily: `<BarChart>` met één bar (cost in USD), x-as = dag (`MM-DD`), tooltip toont `$X.XXXX`
|
||||
- Model: `<BarChart layout="vertical">` met model_id labels — beperkt tot top 5
|
||||
- Kind: `<BarChart layout="vertical">` met kind labels — beperkt tot top 5
|
||||
- Cache: `<PieChart>` met twee segmenten (cached / uncached input) + tekst "X% cache hit · $Y bespaard"
|
||||
|
||||
**Empty state:** als `kpi.jobCount === 0`: render één regel "Geen jobs in deze periode."
|
||||
|
||||
### Taak 3 — Integratie — `app/(app)/insights/page.tsx` (edit)
|
||||
|
||||
Wijzigingen:
|
||||
|
||||
```diff
|
||||
interface InsightsPageProps {
|
||||
- searchParams: Promise<{ product?: string }>
|
||||
+ searchParams: Promise<{ product?: string; period?: string }>
|
||||
}
|
||||
```
|
||||
|
||||
```diff
|
||||
- const { product: filterProductId } = await searchParams
|
||||
+ const { product: filterProductId, period: rawPeriod } = await searchParams
|
||||
+ const period = (['7d','30d','90d','mtd'].includes(rawPeriod ?? '') ? rawPeriod : '30d') as Period
|
||||
```
|
||||
|
||||
In de `Promise.all`, voeg toe:
|
||||
|
||||
```ts
|
||||
getCostKpi(userId, period),
|
||||
getCostByDay(userId, period),
|
||||
getCostByModel(userId, period),
|
||||
getCostByKind(userId, period),
|
||||
getCacheEfficiency(userId, period),
|
||||
```
|
||||
|
||||
Nieuwe sectie tussen Sprint Health en Plan-quality:
|
||||
|
||||
```tsx
|
||||
<section className="space-y-3">
|
||||
<h2 className="text-lg font-medium text-foreground">Cost analyse</h2>
|
||||
<CostAnalysisCard
|
||||
period={period}
|
||||
kpi={costKpi}
|
||||
byDay={costByDay}
|
||||
byModel={costByModel}
|
||||
byKind={costByKind}
|
||||
cache={cacheEff}
|
||||
/>
|
||||
</section>
|
||||
```
|
||||
|
||||
De bestaande "Token gebruik" sectie blijft staan (sprint-detail tabel).
|
||||
|
||||
## Bestanden
|
||||
|
||||
**Nieuw:**
|
||||
|
||||
- `lib/insights/cost-analysis.ts` — 5 query-functies + types
|
||||
- `app/(app)/insights/components/cost-analysis.tsx` — client-component met period-selector + 4 charts
|
||||
|
||||
**Edit:**
|
||||
|
||||
- `app/(app)/insights/page.tsx` — period uit searchParams, parallel-fetch, nieuwe sectie
|
||||
|
||||
**Geen wijzigingen aan:**
|
||||
|
||||
- Prisma schema (alle data is er al)
|
||||
- MCP server (token-data wordt al weggeschreven via `update_job_status`)
|
||||
- `TokenUsageCard` (blijft als sprint-detail tabel)
|
||||
|
||||
## Verificatie
|
||||
|
||||
```bash
|
||||
npm run verify && npm run build
|
||||
```
|
||||
|
||||
**Handmatig:**
|
||||
|
||||
1. Open `/insights` zonder query — period default `30d`, sectie toont KPI + 4 charts
|
||||
2. Wissel period via selector → URL updatet `?period=7d`, charts laden nieuwe data via `router.replace`
|
||||
3. Check empty state: kies periode zonder jobs → "Geen jobs in deze periode."
|
||||
4. Sanity-check KPI's tegen ruwe DB-query:
|
||||
```sql
|
||||
SELECT SUM(input_tokens * mp.input_price_per_1m / 1e6
|
||||
+ output_tokens * mp.output_price_per_1m / 1e6
|
||||
+ cache_read_tokens * mp.cache_read_price_per_1m / 1e6
|
||||
+ cache_write_tokens * mp.cache_write_price_per_1m / 1e6
|
||||
+ COALESCE(actual_thinking_tokens, 0) * mp.input_price_per_1m / 1e6)
|
||||
FROM claude_jobs cj
|
||||
LEFT JOIN model_prices mp ON mp.model_id = cj.model_id
|
||||
WHERE cj.user_id = '<id>' AND cj.status = 'DONE'
|
||||
AND cj.finished_at >= NOW() - INTERVAL '30 days';
|
||||
```
|
||||
5. Cache savings sanity: `cacheSavingsUsd ≈ cache_read_tokens × 0.9 × input_price / 1M`
|
||||
(cache_read prijs = 0.1× input prijs, dus savings is 90%)
|
||||
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`).
|
||||
205
docs/old/plans/lees-de-readme-md-validated-book.md
Normal file
205
docs/old/plans/lees-de-readme-md-validated-book.md
Normal file
|
|
@ -0,0 +1,205 @@
|
|||
# Scrum4Me-Research — Zustand rearchitecture (reset + execute)
|
||||
|
||||
> **Scope:** dit plan is geschreven voor de research-repo
|
||||
> [`madhura68/Scrum4Me-Research`](https://github.com/madhura68/Scrum4Me-Research),
|
||||
> niet voor dit hoofdproject. Bestandsverwijzingen die naar
|
||||
> `stores/data-store.ts`, `hooks/use-event-stream.ts`,
|
||||
> `components/*-select.tsx` etc. wijzen, bestaan in de research-repo —
|
||||
> niet hier. Ze staan in `code`-tags zodat de doc-link-checker ze niet
|
||||
> probeert te resolven.
|
||||
|
||||
## Context
|
||||
|
||||
Het bestaande [zustand-store-rearchitecture.md](./zustand-store-rearchitecture.md) beschrijft een doel-architectuur (`product-workspace-store` met genormaliseerde entities, race-safe loaders, resync-laag, optimistic mutations). De research-repo is dé plek om dat eerst te testen voordat het in `Scrum4Me/` belandt.
|
||||
|
||||
Probleem nu: de research-repo wijkt af van het hoofdproject. Mijn custom `data-store.ts` lijkt qua vorm op de doel-architectuur, maar springt over de baseline heen. We willen aantonen dat de migratie *vanaf* de huidige Scrum4Me-patronen werkt, niet vanaf een verzonnen tussenvorm.
|
||||
|
||||
Dus: eerst de research-repo terugbrengen naar dezelfde stores/hooks/routes als Scrum4Me nu heeft, dan de rearchitecture daarop uitvoeren.
|
||||
|
||||
## Bron-documenten
|
||||
|
||||
- **Doel-architectuur**: [zustand-store-rearchitecture.md](./zustand-store-rearchitecture.md) (in research-repo). Dit plan voert dat document uit; herhaalt het niet.
|
||||
- **Conventies**: [CLAUDE.md](../../CLAUDE.md) hoofdproject. Taal NL, MD3 tokens, `@base-ui/react` render-prop, `*-server.ts`, enum UPPER_SNAKE↔lowercase via `lib/task-status.ts`.
|
||||
|
||||
## Drie-faseplan
|
||||
|
||||
### Fase A — Reset naar Scrum4Me-patronen
|
||||
|
||||
Doel: onze research-pagina werkt op exact dezelfde store/hook/route-vorm als het hoofdproject, met identiek gedrag.
|
||||
|
||||
**Verwijderen** (research-repo):
|
||||
- `stores/data-store.ts` (research-repo) — mijn megastore
|
||||
- `hooks/use-event-stream.ts` (research-repo) — vervangen door `use-backlog-realtime.ts`
|
||||
- `hooks/use-browser-presence.ts` (research-repo) — niet in main, drop voor reset
|
||||
- `app/api/realtime/events/route.ts` (research-repo) — vervangen door `app/api/realtime/backlog/route.ts`
|
||||
- Mijn custom `loadX/resyncAll`-paden in selectie-componenten
|
||||
|
||||
**Kopiëren uit `/Users/janpetervisser/Development/Scrum4Me/`** (1-op-1 of stripped van auth):
|
||||
| Bron | Doel |
|
||||
|---|---|
|
||||
| `stores/backlog-store.ts` | `stores/backlog-store.ts` (`pbis`, `storiesByPbi`, `tasksByStory`; `setInitialData`, `applyChange`) |
|
||||
| `stores/planner-store.ts` | `stores/planner-store.ts` (DnD-order; voor research nog niet gebruikt maar we zetten 'm klaar) |
|
||||
| `stores/selection-store.ts` | overschrijf bestaand (state: `selectedPbiId`, `selectedStoryId`, geen taskId/productId in main; add `selectedTaskId` + `productId` als research-uitbreiding) |
|
||||
| `stores/product-store.ts` | `stores/product-store.ts` (`currentProduct`) |
|
||||
| `stores/products-store.ts` | `stores/products-store.ts` (lijst, voor pulldown) |
|
||||
| `lib/realtime/use-backlog-realtime.ts` | `lib/realtime/use-backlog-realtime.ts` (SSE-client → `applyChange` op backlog-store) |
|
||||
| `lib/task-status.ts` | `lib/task-status.ts` (enum-converters) |
|
||||
| `app/api/realtime/backlog/route.ts` | `app/api/realtime/backlog/route.ts` (SSE+LISTEN, **research-only: strip auth/session/getAccessibleProduct** — vraagt enkel `product_id` querystring) |
|
||||
|
||||
> ⚠️ **Auth-strip is research-only.** Het hoofdproject MOET sessie + `getAccessibleProduct()`-check op SSE en read-routes behouden. Bij backport vanaf de research-repo nooit de geknipte route 1-op-1 overnemen. Dit geldt voor zowel `app/api/realtime/backlog/route.ts` als alle read-routes onder `app/api/products/...`, `/pbis/...`, `/stories/...`, `/tasks/...`.
|
||||
|
||||
**API-routes (read)** — bestaande paden behouden, alleen `force-dynamic` blijft:
|
||||
- `GET /api/products` (list voor pulldown)
|
||||
- `GET /api/products/[id]/pbis` (open: READY+BLOCKED)
|
||||
- `GET /api/pbis/[id]/stories`
|
||||
- `GET /api/stories/[id]/tasks`
|
||||
- `GET /api/tasks/[id]`
|
||||
|
||||
**Componenten herschrijven**:
|
||||
- `components/product-select.tsx` (research-repo) → leest `useProductsStore`, schrijft naar `useProductStore.setCurrentProduct`
|
||||
- `components/pbi-select.tsx` (research-repo) → leest `useBacklogStore` (filter op currentProduct), `useSelectionStore.selectPbi`. Triggert fetch op product-mount via een `useBacklogLoader`-helper die initial data binnenhaalt.
|
||||
- `components/story-select.tsx` (research-repo) → idem voor stories
|
||||
- `components/tasks-table.tsx` (research-repo) → leest `tasksByStory[selectedStoryId]`. **Max 10 rijen, scrollbaar** (al ingebouwd, behouden)
|
||||
- `components/task-detail-card.tsx` (research-repo) → fetcht task detail apart (geen full-fat backlog veld; matcht main's `tasks/[id]` route)
|
||||
- `components/event-stream-panel.tsx` (research-repo) → blijft bestaan voor research-doel (event-tap), maar luistert nu mee op dezelfde EventSource via `use-backlog-realtime` (of een tweede readonly listener); selecteerbare events met JSON-detail rechts blijven. Twee checkboxes (Postgres / Browser). Truncate met ellipsis in de lijst.
|
||||
|
||||
**Werkwijzen (verifiëren tijdens reset)**:
|
||||
- Comments en UI-tekst NL
|
||||
- Geen `bg-blue-500` etc; enkel MD3 tokens (`bg-primary`, `bg-card`, `bg-status-done`, ...)
|
||||
- shadcn-componenten al `base-nova` style
|
||||
- Server-only files krijgen `*-server.ts` suffix waar van toepassing (in deze fase niet nodig — alle DB-toegang loopt via `lib/prisma.ts` in route handlers)
|
||||
- TaskStatus-mapping via `lib/task-status.ts` als de UI lowercase wil
|
||||
|
||||
**Acceptatie Fase A**:
|
||||
- `npx tsc --noEmit` schoon
|
||||
- Pagina rendert, cascading werkt, tabel toont taken, detail-card vult, events stromen door (preview-verificatie)
|
||||
- Stores matchen het hoofdproject qua vorm (vergelijking via `diff` uitvoerbaar voor backlog-store etc.)
|
||||
|
||||
### Fase B — Rearchitecture uitvoeren
|
||||
|
||||
Volgt de 15 stappen uit [zustand-store-rearchitecture.md](./zustand-store-rearchitecture.md) §Implementatiepad. Concreet voor de research-repo:
|
||||
|
||||
1. **Map** `stores/product-workspace/` aanmaken (factory + provider + selectors).
|
||||
2. **`activeProduct`** wordt nu nog gespiegeld vanuit `useProductStore`; voor de research-pagina geen layout/server-side bepaling — we lezen het uit de pulldown-state.
|
||||
3. **Selection migreren** — `selection-store` → `context.{activePbiId, activeStoryId, activeTaskId}` + `productId`. Setters cascaden de reset naar children (zoals doc beschrijft).
|
||||
4. **Backlog naar entities + relations** — `pbisById`, `storiesById`, `tasksById`, `pbiIds`, `storyIdsByPbi`, `taskIdsByStory`. Selectors:
|
||||
- `selectVisiblePbis(productId)`
|
||||
- `selectStoriesForActivePbi(state)`
|
||||
- `selectTasksForActiveStory(state)`
|
||||
- `selectActivePbi/Story/Task(state)`
|
||||
5. **Planner-state** in dezelfde workspace-store landen (`relations` slice); voor research: niet actief gebruikt, wel structureel meekoppen.
|
||||
6. **Race-safe loaders** — `ensureProductLoaded`, `ensurePbiLoaded`, `ensureStoryLoaded`, `ensureTaskLoaded` met `requestId`-guard. Implementatie:
|
||||
```ts
|
||||
setActivePbi(pbiId) {
|
||||
const requestId = crypto.randomUUID()
|
||||
set({ context: { ..., activePbiId: pbiId, ... }, loading: { ..., activeRequestId: requestId } })
|
||||
void get().ensurePbiLoaded(pbiId, requestId)
|
||||
}
|
||||
// in ensure: if (get().loading.activeRequestId !== requestId) return
|
||||
```
|
||||
7. **localStorage = restore hints** — `lastActivePbiIdByProduct`, `lastActiveStoryIdByProduct`, `lastActiveTaskIdByProduct`. Niet de waarheid, alleen hint die getoetst wordt aan toegankelijkheid.
|
||||
8. **`use-backlog-realtime` dispatcht naar `applyRealtimeEvent`** — store interpreteert pbi/story/task I|U|D events, doet upsert + parent-id move + sort.
|
||||
9. **Hidden tab beleid** — `EventSource` openhouden bij `hidden`. Op `visible` → `resyncActiveScopes('visible')`.
|
||||
10. **Reconnect resync** — bij `ready` na disconnect of na exponential backoff: `resyncActiveScopes('reconnect')`.
|
||||
11. **Unknown-event fallback** — onbekend event met `payload.product_id === activeProductId` → `resyncActiveScopes('unknown-event')`. Dit is wat het "veel events maar geen update" issue oplost.
|
||||
12. **`force-dynamic` + `cache: 'no-store'`** — al gedaan in mijn fixes; behouden bij reset en versterken.
|
||||
13. **Componenten naar selectors** — backlog-componenten lezen via `selectStoriesForActivePbi` etc., niet via raw store-velden.
|
||||
14. **Tests** (Vitest, conform main):
|
||||
- hydrate snapshot
|
||||
- active selectie cascade
|
||||
- race-safe ensure (laat trage promise van oude selectie geen nieuwe data overschrijven)
|
||||
- SSE I|U|D voor pbi/story/task
|
||||
- parent-move (story verandert van pbi)
|
||||
- hidden→visible resync
|
||||
- reconnect resync
|
||||
- unknown-event resync
|
||||
- delete-cleanup van actieve selectie
|
||||
- localStorage restore-hint validatie tegen toegankelijkheid
|
||||
15. **Sprint-workspace** — buiten scope; flag voor latere herhaling.
|
||||
|
||||
**Optimistic mutations** (§Optimistic in doc): voor research geen DnD, dus alleen het patroon dropunten en niet bouwen. Wel: `applyOptimisticMutation`-action wel klaarzetten in de store-API zodat het patroon zichtbaar is.
|
||||
|
||||
### Fase C — Werkwijzen verweven en doortrekken
|
||||
|
||||
**Tijdens Fase A én B respecteren**:
|
||||
1. **Plan mode workflow** — eerst Plan, ExitPlanMode, dan code. Bij grote wendingen opnieuw plannen.
|
||||
2. **TodoWrite** voor multi-step werk; markeer immediate completion.
|
||||
3. **Verify via preview** voor elke observable verandering (de hook reminder doet dit al).
|
||||
4. **`tsc --noEmit`** voor afronden van een stap.
|
||||
5. **Comments/Dutch** consistent. WHY-comments over de invariant; geen WHAT-comments.
|
||||
6. **MD3 tokens** alleen.
|
||||
7. **Geen secrets in chat** — `.env.local` blijft lokaal.
|
||||
8. **Niet schrijven naar shared DB** zonder expliciete user-toestemming (geen `pg_notify` op shared channel).
|
||||
9. **Source of truth = DB**. Zustand is projectie. localStorage = hint.
|
||||
10. **Vóór elke fase**: kort statusrapport in de chat met wat er aankomt en waarom.
|
||||
|
||||
**Doortrekken naar hoofdproject** (out-of-scope deze run, maar geflagd):
|
||||
- Na bewezen werking in research-repo: backport `product-workspace-store` + selectors + realtime-apply + resync-laag naar `Scrum4Me/stores/product-workspace/`.
|
||||
- **Niet backporten**: de auth-stripped routes uit research. Main behoudt iron-session, `getAccessibleProduct()`, en alle product-access/sprint/personal filters in z'n SSE- en read-routes.
|
||||
- **Wel backporten**: store-shape, selectors, race-safe `ensure*Loaded`, hidden-tab beleid, `resyncActiveScopes`, unknown-event fallback, restore-hint patroon, `force-dynamic` + `cache: 'no-store'`.
|
||||
- Migratie main-project zal langer duren (DnD, sprint, jobs, tests). Apart plan.
|
||||
|
||||
## Bestandsmutaties (overzicht)
|
||||
|
||||
### Verwijderen na Fase A
|
||||
- `stores/data-store.ts` (research-repo)
|
||||
- `hooks/use-event-stream.ts` (research-repo)
|
||||
- `hooks/use-browser-presence.ts` (research-repo) — komt deels terug in Fase B als helper voor visibility/online resync trigger
|
||||
- `app/api/realtime/events/route.ts` (research-repo)
|
||||
|
||||
### Toevoegen Fase A (uit Scrum4Me)
|
||||
- `stores/backlog-store.ts`
|
||||
- `stores/planner-store.ts`
|
||||
- `stores/selection-store.ts` (overschrijf)
|
||||
- `stores/product-store.ts`
|
||||
- `stores/products-store.ts`
|
||||
- `lib/realtime/use-backlog-realtime.ts`
|
||||
- `lib/task-status.ts`
|
||||
- `app/api/realtime/backlog/route.ts` (zonder auth)
|
||||
|
||||
### Toevoegen Fase B (nieuw, conform doc)
|
||||
- `stores/product-workspace/store.ts` (zustand factory)
|
||||
- `stores/product-workspace/selectors.ts`
|
||||
- `stores/product-workspace/types.ts`
|
||||
- `stores/product-workspace/restore.ts` (localStorage hints)
|
||||
- `stores/product-workspace/realtime-apply.ts` (SSE event → store)
|
||||
- `stores/product-workspace/resync.ts` (`resyncActiveScopes`, `resyncLoadedScopes`)
|
||||
- `tests/product-workspace/*.test.ts` (Vitest, install vitest als devDep)
|
||||
|
||||
### Te aanpassen in Fase B
|
||||
- Alle `components/*.tsx` (nu shadcn select/table/card panels) → consumeren via selectors uit workspace-store
|
||||
- `lib/realtime/use-backlog-realtime.ts` → dispatcht `applyRealtimeEvent` naar workspace-store i.p.v. `applyChange` naar backlog-store
|
||||
- `event-stream-panel.tsx` → blijft bestaan (research-tap), maar leest events ook uit workspace-store of via een dunne `event-log-store` ernaast (in bounded-context-stijl: aparte log-store voor pure observatie hoort er niet thuis in de workspace-store)
|
||||
|
||||
## Verificatie
|
||||
|
||||
### Na Fase A (baseline)
|
||||
1. `npm run dev` op port 3001
|
||||
2. Pagina laadt, cascading werkt: product → PBI → story → tasks
|
||||
3. Detail-card vult bij klik op task
|
||||
4. Event-paneel toont realtime events (truncate + JSON-detail)
|
||||
5. `npx tsc --noEmit` schoon
|
||||
6. Vergelijk: `diff Scrum4Me/stores/backlog-store.ts Scrum4Me-Research/stores/backlog-store.ts` → identiek (modulo lokale interface-uitbreidingen waar gedocumenteerd)
|
||||
|
||||
### Na Fase B (target)
|
||||
Alle acceptatiecriteria uit [zustand-store-rearchitecture.md §Acceptatiecriteria](./zustand-store-rearchitecture.md):
|
||||
- Eén waarheid per entity in de store ✓
|
||||
- Selectors als enige UI-leesweg ✓
|
||||
- SSE patcht zonder full-page refresh ✓
|
||||
- Hidden→visible herstelt missers binnen één resync-cyclus ✓
|
||||
- Reconnect resync werkt zonder NOTIFY-replay ✓
|
||||
- Directe task-edits zonder `entity:'task'` NOTIFY worden via unknown-event fallback zichtbaar ✓
|
||||
- LocalStorage = hint, geen forced state ✓
|
||||
- `force-dynamic` + `cache: 'no-store'` overal ✓
|
||||
|
||||
### Manuele preview-verificatie (na elke fase)
|
||||
- TODO via TodoWrite tijdens uitvoer; preview-screenshot na grote stappen
|
||||
- Tab-switch test: open page, switch tab, doe een wijziging via een ander mechanisme (psql na user-akkoord, of UI in main-project), keer terug → verwacht: zonder warnings + data gerefresht
|
||||
|
||||
## Open vragen / risico's
|
||||
|
||||
1. **Reset-import** uit hoofdproject: voor de research-repo strippen we auth/session-deps uit de gekopieerde routes (research-repo heeft geen auth-laag, draait lokaal). Belangrijk: **dit is een research-repo-keuze; main behoudt de volledige auth-filters**. Zie de waarschuwing onder "API-routes (read)" hierboven.
|
||||
2. **`use-backlog-realtime` heeft mogelijk auth-headers/session-checks**: bevestigen tijdens copy. Indien zo: research-versie gebruikt geen auth, route is publiek-bereikbaar binnen lokale dev. Geldt alleen lokaal — geen wijziging aan main.
|
||||
3. **Tests-deps** (vitest, @testing-library/react) toevoegen tijdens Fase B. Of pas in Fase B step 14 vanwege scope.
|
||||
4. **Event-paneel toekomst**: blijft het in research-repo of stoten we het af zodra de workspace-store af is? Voorstel: behouden als observatie-tool, maar er aparte `event-log-store` (kleine UI store) voor maken zodat het niet meelift in de workspace-store.
|
||||
5. **README.md** update na Fase B (optioneel) — kort beschrijven dat dit nu het canonical migratie-pad demonstreert.
|
||||
212
docs/old/plans/user-settings-store.md
Normal file
212
docs/old/plans/user-settings-store.md
Normal file
|
|
@ -0,0 +1,212 @@
|
|||
---
|
||||
title: "User-settings store (DB-backed user prefs)"
|
||||
status: draft
|
||||
audience: [contributor, ai-agent]
|
||||
language: nl
|
||||
last_updated: 2026-05-10
|
||||
---
|
||||
|
||||
# User-settings store (DB-backed user prefs)
|
||||
|
||||
> **Locatie na approval:** verhuis dit bestand naar `docs/plans/user-settings-store.md` in de repo.
|
||||
> Trigger voor dit plan: zichtbare hydratie-flits op het sprint-scherm in v1.3.3 ([PR #184](https://github.com/madhura68/Scrum4Me/pull/184)).
|
||||
> De fix daar (useEffect-hydratie + `prefsLoaded`-gate) is een tijdelijke patch; deze migratie elimineert de flits volledig.
|
||||
|
||||
## Context
|
||||
|
||||
Filter- en view-prefs zitten nu verspreid over `localStorage` (en deels cookies).
|
||||
Bij SSR weet de server niets van `localStorage`, dus bij users met saved-state ≠
|
||||
default ontstaat één render-flits direct na hydratie. Daarnaast werken die prefs
|
||||
alleen per browser — geen cross-device, en cross-tab-sync vereist `storage`-events.
|
||||
|
||||
Doel: **één `User.settings` JSON-veld** als single source of truth, met:
|
||||
|
||||
- Server-component leest het veld bij elke page-render → SSR-correct, geen flits
|
||||
- Zustand-store met optimistic updates patroon (zoals `product-workspace-store`)
|
||||
- Cross-tab sync via bestaande `LISTEN/NOTIFY` + SSE-bridge
|
||||
- Cross-device persistence (login op andere browser/laptop ziet zelfde prefs)
|
||||
|
||||
---
|
||||
|
||||
## Scope (gefaseerd)
|
||||
|
||||
### Fase 0 — Infrastructuur
|
||||
|
||||
Aparte PR. Geen UI-wijziging; legt het fundament. Resultaat is een werkende store
|
||||
zonder migraties; bestaande localStorage-flow blijft intact tot Fase 1.
|
||||
|
||||
| # | Bestand | Wat |
|
||||
|---|---|---|
|
||||
| 0.1 | `prisma/schema.prisma` | `settings Json @default("{}")` op `User` model + migration |
|
||||
| 0.2 | `lib/user-settings.ts` | Zod-schema + types + `mergeSettings(prev, patch)` deep-merge helper + defaults |
|
||||
| 0.3 | `actions/user-settings.ts` | `updateUserSettingsAction(patch: Partial<UserSettings>)` — auth-guard, Zod-validate, deep-merge in DB transactie, `NOTIFY scrum4me_changes 'user_settings:${userId}'` |
|
||||
| 0.4 | `stores/user-settings/store.ts` | Zustand met `entities.settings: UserSettings`, `hydrate(initial)`, generieke `setPref(path, value)` met optimistic + rollback. Zelfde mutation-flow als `product-workspace-store` |
|
||||
| 0.5 | `app/api/realtime/user-settings/route.ts` | SSE-route per user, `LISTEN user_settings:${userId}`, push patches |
|
||||
| 0.6 | `components/shared/user-settings-bridge.tsx` | Server reads `prisma.user.findUnique({select:{settings:true}})`, geeft door als prop, client mount roept `store.hydrate()` aan + opent SSE |
|
||||
| 0.7 | Mount in `app/(app)/layout.tsx` | Bridge bovenin de app-layout zodat de store altijd beschikbaar is voor alle authenticated pagina's |
|
||||
| 0.8 | Tests | `__tests__/lib/user-settings.test.ts` (merge-logic), `__tests__/actions/user-settings.test.ts` (auth + validation), `__tests__/stores/user-settings.test.ts` (optimistic flow) |
|
||||
|
||||
**Demo/anon-fallback:** `useUserSettingsStore` detecteert `session.isDemo` of geen `userId`
|
||||
en valt terug op in-memory state (geen server-write). Bridge wordt voor demo niet
|
||||
gemount — defaults blijven actief, geen persistence-verwachting.
|
||||
|
||||
### Fase 1 — Migreer huidige flits-bronnen
|
||||
|
||||
| Component | localStorage-keys | → `settings`-pad |
|
||||
|---|---|---|
|
||||
| `components/sprint/sprint-backlog.tsx` | `scrum4me:sprint_pb_*` (6) | `views.sprintBacklog.{filterPriority,filterStatus,sort,sortDir,collapsedPbis,filterPopoverOpen}` |
|
||||
| `components/backlog/pbi-list.tsx` | `scrum4me:pbi_*` (4) | `views.pbiList.{sort,filterPriority,filterStatus,sortDir}` |
|
||||
| `components/backlog/story-panel.tsx` | `scrum4me:story_sort` (1) | `views.storyPanel.sort` |
|
||||
| `components/jobs/jobs-column.tsx` | `${prefix}_filter_kind`, `${prefix}_filter_status` (2 dyn.) | `views.jobsColumns[prefix].{kinds,statuses}` |
|
||||
| `stores/debug-store.ts` (via `status-bar-debug-toggle`) | `scrum4me:debug-mode` (1) | `devTools.debugMode` |
|
||||
|
||||
Per component:
|
||||
- Verwijder `useState` + `useEffect`-hydratie + `useEffect`-write
|
||||
- Vervang door `useUserSettingsStore(s => s.entities.settings.views.sprintBacklog?.filterStatus ?? 'OPEN')`
|
||||
- Setter wordt `useUserSettingsStore.getState().setPref(['views','sprintBacklog','filterStatus'], value)`
|
||||
- `prefsLoaded`-state en helpers (`readLocalStoragePref`) verdwijnen
|
||||
- `lib/use-local-storage-pref.ts` wordt verwijderd (niet meer in gebruik)
|
||||
|
||||
**Migratie-pad voor bestaande users:** bij eerste mount, voor de eerste `setPref`-call,
|
||||
leest een one-shot `useEffect` de oude localStorage-keys en pusht ze als één bulk-patch
|
||||
naar de server. Daarna `localStorage.removeItem(...)` om geen verwarring te wekken.
|
||||
Idempotent: als `settings.views.sprintBacklog.filterStatus` al gezet is, sla over.
|
||||
|
||||
### Fase 2 — Cookie-consolidatie (optioneel, later PR)
|
||||
|
||||
| Bron | Huidig | → `settings`-pad |
|
||||
|---|---|---|
|
||||
| `components/shared/split-pane.tsx` | `document.cookie` (`sp:` prefix) | `layout.splitPanePositions[cookieKey]` |
|
||||
| `lib/active-sprint.ts` + `actions/active-sprint.ts` | server-side cookie per product | `layout.activeSprints[productId]` |
|
||||
|
||||
Server-component-lezers veranderen — apart traject met meer regression-risico.
|
||||
Niet onderdeel van de eerste user-settings-PR.
|
||||
|
||||
### Fase 3 — Skip / al persistent
|
||||
|
||||
- `idea-md-editor.tsx` drafts — werk-in-progress, geen pref
|
||||
- `iron-session` cookies — auth, andere zorg
|
||||
- `User.active_product_id` — al in DB (kolom op model)
|
||||
- Modal/popover open-state behalve `filterPopoverOpen` — ephemeral
|
||||
|
||||
---
|
||||
|
||||
## JSON-shape (Fase 1)
|
||||
|
||||
```ts
|
||||
// lib/user-settings.ts
|
||||
import { z } from 'zod'
|
||||
|
||||
export const UserSettingsSchema = z.object({
|
||||
views: z.object({
|
||||
sprintBacklog: z.object({
|
||||
filterPriority: z.union([z.number().int().min(1).max(4), z.literal('all')]).optional(),
|
||||
filterStatus: z.enum(['OPEN', 'IN_SPRINT', 'DONE', 'all']).optional(),
|
||||
sort: z.enum(['priority', 'status', 'code']).optional(),
|
||||
sortDir: z.enum(['asc', 'desc']).optional(),
|
||||
collapsedPbis: z.array(z.string()).optional(),
|
||||
filterPopoverOpen: z.boolean().optional(),
|
||||
}).optional(),
|
||||
pbiList: z.object({
|
||||
sort: z.enum(['priority', 'code', 'date']).optional(),
|
||||
filterPriority: z.union([z.number().int().min(1).max(4), z.literal('all')]).optional(),
|
||||
filterStatus: z.enum(['ready', 'blocked', 'done', 'all']).optional(),
|
||||
sortDir: z.enum(['asc', 'desc']).optional(),
|
||||
}).optional(),
|
||||
storyPanel: z.object({
|
||||
sort: z.enum(['priority', 'code', 'date']).optional(),
|
||||
}).optional(),
|
||||
jobsColumns: z.record(z.string(), z.object({
|
||||
kinds: z.array(z.string()),
|
||||
statuses: z.array(z.string()),
|
||||
})).optional(),
|
||||
}).optional(),
|
||||
devTools: z.object({
|
||||
debugMode: z.boolean().optional(),
|
||||
}).optional(),
|
||||
}).strict()
|
||||
|
||||
export type UserSettings = z.infer<typeof UserSettingsSchema>
|
||||
```
|
||||
|
||||
Defaults zijn impliciet (alle keys optioneel). Selectors in de store geven
|
||||
fallback-waardes terug zodat consumers niet `?? 'OPEN'` hoeven te schrijven —
|
||||
maar het mag, geen big deal.
|
||||
|
||||
---
|
||||
|
||||
## Realtime-notificatie
|
||||
|
||||
Bestaand kanaal `scrum4me_changes` blijft. Payload-conventie:
|
||||
|
||||
```json
|
||||
{ "kind": "user_settings", "userId": "...", "patch": { "views": { ... } } }
|
||||
```
|
||||
|
||||
`/api/realtime/user-settings/route.ts` filtert payloads op `userId === session.userId`.
|
||||
Andere tabs van zelfde user krijgen patches binnen, store roept `applyServerPatch(patch)`
|
||||
aan zonder optimistic flow.
|
||||
|
||||
---
|
||||
|
||||
## Verificatie (per fase)
|
||||
|
||||
### Fase 0
|
||||
- [ ] `npm run verify && npm run build` groen
|
||||
- [ ] Migration draait op fresh + bestaande DB zonder data-verlies
|
||||
- [ ] `updateUserSettingsAction` weigert auth-loze calls (test)
|
||||
- [ ] Zod-validatie geeft 422 bij invalid patch (test)
|
||||
- [ ] Optimistic update + rollback gedraagt zich zoals `product-workspace-store` (test)
|
||||
- [ ] SSE-route levert patches alleen aan zelfde user (manueel: open twee tabs als A, schrijf, zie update; tab van user B blijft stil)
|
||||
|
||||
### Fase 1
|
||||
- [ ] Geen `localStorage.getItem` of `localStorage.setItem` meer in de gemigreerde componenten
|
||||
- [ ] Sprint screen: refresh → filter direct correct, geen flits, geen hydration error in console
|
||||
- [ ] Product backlog screen: idem
|
||||
- [ ] Jobs page: idem (per kolom-instance)
|
||||
- [ ] Two-tab test: filter wijzigen in tab A → tab B updatet binnen ~100ms
|
||||
- [ ] Demo-user: filter wijzigen werkt binnen sessie, niet gepersisteerd na refresh (verwacht)
|
||||
- [ ] One-shot localStorage-migratie: bestaande user met oude keys ziet bij eerste login zijn waardes terug; na refresh zijn de localStorage-keys leeg
|
||||
|
||||
### Fase 2
|
||||
- [ ] Split-pane positie persistent en SSR-correct
|
||||
- [ ] Active-sprint per product werkt zonder cookie
|
||||
|
||||
---
|
||||
|
||||
## Schatting
|
||||
|
||||
| Fase | Tijd |
|
||||
|---|---|
|
||||
| 0 — Infra | ~3 uur |
|
||||
| 1 — Migratie | ~2 uur |
|
||||
| 2 — Cookies | ~2 uur (apart) |
|
||||
| Totaal Fase 0 + 1 | **~5 uur**, 1 PR (of 2 als we 0 en 1 splitsen) |
|
||||
|
||||
Aanbevolen: **Fase 0 + 1 in één PR** als de infra klein blijft, anders splitsen
|
||||
per fase. Fase 2 is altijd een aparte PR.
|
||||
|
||||
---
|
||||
|
||||
## Open vragen
|
||||
|
||||
1. **Cross-device merge-conflict.** Twee tabs van zelfde user op verschillende
|
||||
devices wijzigen tegelijk. Server-side: `last-write-wins` of `JSON_PATCH`-merge?
|
||||
Voorstel: deep-merge per top-level path, dus `views.sprintBacklog.filterStatus`
|
||||
en `views.pbiList.sort` botsen niet — laatste schrijver per veld wint.
|
||||
2. **Storage-grens.** PostgreSQL JSON kolom kan ~1GB; we zitten op <5KB per user.
|
||||
Geen concern.
|
||||
3. **Schema-versionering.** Als we het JSON-schema later wijzigen: voorzichtig
|
||||
migreren via Zod `.catch()` voor onbekende keys. Voor v1: start klein.
|
||||
4. **One-shot localStorage-migratie weglaten?** Voor solo-dev-tool kan het
|
||||
acceptabel zijn dat users hun saved filters verliezen bij de migratie. Scheelt
|
||||
~30 minuten implementatie + tests.
|
||||
|
||||
---
|
||||
|
||||
## Eerste stappen na approval
|
||||
|
||||
1. Verhuis dit plan naar `docs/plans/user-settings-store.md` in een nieuwe branch (bv. `feat/user-settings-store`)
|
||||
2. Maak via Scrum4Me-MCP een PBI met story + taken voor Fase 0 (volgens CLAUDE.md werkflow)
|
||||
3. Start met taken in `sort_order`; commit per laag
|
||||
4. Fase 1 als opvolg-PBI (of in dezelfde sprint, los gelabeld)
|
||||
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