* feat(ideas): upload-plan knop — short-circuit van Make-Plan AI-flow
Voegt een 'Upload plan' knop toe in idea-row-actions (verschijnt in zowel
list als idea-detail). Klik → file picker → kies .md → server-side parse +
opslaan; idea-status springt naar PLAN_READY. Vandaaruit de bestaande
'Maak PBI' knop voor materialize.
Server (uploadPlanMdAction):
- Toegestaan vanuit DRAFT, GRILLED, PLAN_FAILED, PLAN_READY
- DRAFT → skip-grill: status gaat direct naar PLAN_READY
- PLAN_READY overschrijft het bestaande plan (consistent met
updatePlanMdAction, geen confirmation)
- Geblokkeerd in GRILLING/PLANNING (job loopt), PLANNED (al gematerialiseerd)
- Parse-failure → 422 + details (NIET opslaan, zodat een onparseerbaar plan
nooit in de DB belandt)
- Empty / >100k chars → 422
- Schrijft IdeaLog NOTE met from_status + length
- Rate-limit + demo-guard + ownership-check via loadOwnedIdea (zelfde
patroon als updatePlanMdAction)
UI (idea-row-actions.tsx):
- Hidden <input type=file accept=".md,.markdown,text/markdown,text/plain">
- FileReader → text → action
- Toast bij success + router.refresh()
- Blocked-tooltip in andere statussen
Tests: 10 nieuwe in __tests__/actions/ideas-crud.test.ts dekkend voor:
happy paths (DRAFT/GRILLED/PLAN_READY-overwrite/PLAN_FAILED), blocks
(PLANNED/GRILLING), validation (empty/oversized/parse-fail), 404.
Full suite groen: 849/849.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Add reviews for Bootstrap-wizard plans v3.2 to v3.4
- Review v3.2: Addressed executor model, fire-and-forget issues, and PAT handling.
- Review v3.3: Improved transaction handling, stale recovery, and ID generation.
- Review v3.4: Finalized GitHub permissions, catalog versioning, and E2E verification queries.
- Updated recommendations for each version to enhance implementation readiness.
* docs(plans): M8 bootstrap-wizard upload-variant v1.4 — backtick-paden
Upload-variant van het volledige technische plan (docs/plans/M8-bootstrap-wizard.md),
bedoeld voor de "Upload plan"-functie. Genereert 1 PBI + 4 Stories + 22 Tasks
via materializeIdeaPlanAction.
v1.4-aanpassingen tov eerdere generatie-iteratie:
- Alle bestandspaden in implementation_plan in backticks (path-extractor matchen)
- Expliciete "Bestanden:" blok per task vóór de stappen
- Alle tasks op verify_required: ALIGNED_OR_PARTIAL (was deels ALIGNED — te strict
voor ADR-stubs en multi-file edits)
Fixt forward-only: T-963 cancelled_by_self door DIVERGENT verifier-verdict.
Re-upload van dit bestand produceert tasks die door verify_task_against_plan
als ALIGNED of PARTIAL geclassificeerd kunnen worden.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* PBI-67: Add review-plan support to Idea model and job config
- Add plan_review_log and reviewed_at fields to Idea model
- Add REVIEWING_PLAN, PLAN_REVIEW_FAILED, PLAN_REVIEWED to IdeaStatus enum
- Add IDEA_REVIEW_PLAN to ClaudeJobKind enum
- Add IDEA_REVIEW_PLAN config to job-config.ts with model=opus, thinking_budget=6000
- Create migration record for schema changes (applied via db push)
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
* PBI-67 Phase 2: Add update-idea-plan-reviewed MCP tool
- Create src/tools/update-idea-plan-reviewed.ts: saves review-log and transitions idea status to PLAN_REVIEWED
- Add PLAN_REVIEW_RESULT to IdeaLogType enum (both repos)
- Register tool in src/index.ts
- Update Prisma schemas (both repos): add plan_review_log and reviewed_at fields to Idea model
- Add REVIEWING_PLAN, PLAN_REVIEW_FAILED, PLAN_REVIEWED to IdeaStatus enum (MCP schema)
- Add IDEA_REVIEW_PLAN to ClaudeJobKind enum (MCP schema)
- Tool includes transaction safety and convergence metrics logging
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
* feat(PBI-67): IDEA_REVIEW_PLAN Phases 3-6 — server actions, UI components, prompt & tests
- Phase 3: startReviewPlanJobAction, cancelIdeaJobAction, status transitions
(REVIEWING_PLAN / PLAN_REVIEWED / PLAN_REVIEW_FAILED), status colors,
job-card/jobs-column filters, idea-list status tabs
- Phase 4: review-plan-job.md prompt (multi-model orchestration with codex
injection + active plan revision via update_idea_plan_md after each round),
runbook, 13 unit tests
- Phase 5: ReviewLogViewer component (rounds, convergence, approval, issues),
idea-detail integration, proper ReviewLog TypeScript types exported from component
- Phase 6.1: wait-for-job discriminator wired (IDEA_REVIEW_PLAN), plan-revision
step made mandatory in prompt (was previously optional/missing)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(PBI-76): plan for user-settings DB-store
Persists view/filter prefs in User.settings (Json) instead of
localStorage. SSR-correct hydration, cross-tab sync via
LISTEN/NOTIFY + SSE, cross-device persistence.
Phased: 0=infra, 1=migrate flicker sources, 2=cookie consolidation.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(PBI-76): User.settings json column + migration
Adds JSONB column to users table for persistent user prefs.
Idempotent SQL — safe on databases where column already exists.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(PBI-76): user-settings types and merge helpers
Zod schema for User.settings shape (views/devTools), deep-merge
helper that replaces arrays and merges nested objects, and a
safe parser that returns defaults on invalid input.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(PBI-76): updateUserSettingsAction with notify
Validates patch via Zod, deep-merges with current settings in
a transaction, persists to DB, and emits pg_notify on
scrum4me_changes for cross-tab/cross-device sync. Demo accounts
get 403, unauthenticated 401, invalid input 422.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(PBI-76): user-settings zustand store with optimistic flow
Hydrate from prop (SSR-correct), setPref via path with optimistic
update + rollback on server error, applyServerPatch for SSE-driven
cross-tab updates. Demo accounts skip server-write entirely.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(PBI-76): SSE route for user-settings
User-scoped /api/realtime/user-settings stream that filters
scrum4me_changes notifications on kind=user_settings and matching
userId. Forwards the patch as a data: event so other tabs can
applyServerPatch without re-fetching settings.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(PBI-76): user-settings bridge mounted in app layout
Hydrates the zustand store with the user's persisted settings via
prop (SSR-correct, no flicker). Opens an EventSource to
/api/realtime/user-settings so changes from other tabs/devices
flow into the same store. Demo accounts skip the SSE subscription.
Layout now selects user.settings alongside the other user fields,
no extra DB roundtrip.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(PBI-76): user-settings lib/action/store coverage
22 vitest cases covering merge semantics (no mutation, array
replace, nested merge), Zod schema strictness, server action
auth/demo/validation paths, and the optimistic store flow
including rollback and demo-mode skip.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(PBI-76): sync package-lock to v1.3.3
Lockfile drifted after @prisma/client reinstall during the
schema regenerate. No dependency changes — just the version
field tracking package.json bumped in #184.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Vercel detecteert @prisma/client en runt automatisch `prisma generate`
zonder --generator filter. Daardoor probeerde de erd-generator op Vercel
te draaien en faalde op libnss3.so (puppeteer/Chrome niet beschikbaar in
de build container). Cascading: de Prisma-client werd niet ge-update,
runtime kreeg oude enum-waarden (ACTIVE i.p.v. OPEN).
ERD is dev-only documentatie en niet meer in productie nodig. Generator
+ dependency + npm scripts + de gegenereerde svg verwijderd. README,
prisma-client pattern en architecture docs bijgewerkt.
Build script blijft `prisma generate && next build` zodat de client ook
bij Vercel build-cache-hits opnieuw wordt gegenereerd.
* feat(PBI-67/ST-1297): datamodel-velden voor job-model-selectie
Voegt 8 nieuwe optionele velden toe verspreid over Product, Task en
ClaudeJob ten dienste van de override-cascade:
task.requires_opus → job.requested_* → product.preferred_* → kind-default
Bestaande rijen krijgen NULL (Product/ClaudeJob) of false (Task) en
vallen daarmee terug op de kind-defaults uit de resolver (ST-1298).
Migration is additief: alleen ALTER TABLE ADD COLUMN, geen RENAME of
DROP. Bestaande factories en seed-script blijven werken zonder
aanpassing omdat alle nieuwe velden default-waardes hebben.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(PBI-67/ST-1299): job-config snapshot bij enqueue + worker-flag-runbook
T-789: Snapshot van resolved JobConfig in ClaudeJob.requested_*
bij elke job-creatie. Helper in lib/job-config-snapshot.ts laadt
product (preferred_*) en task (requires_opus) en draait de resolver
uit lib/job-config.ts (mirror van scrum4me-mcp/src/lib/job-config.ts —
zelfde matrix, sync-comment in bestand). Toegepast op alle 5
enqueue-locaties:
- actions/user-questions.ts (PLAN_CHAT)
- actions/sprint-runs.ts × 3 (SPRINT_IMPLEMENTATION x2,
TASK_IMPLEMENTATION loop)
- actions/ideas.ts (IDEA_GRILL / IDEA_MAKE_PLAN)
Test-mocks uitgebreid met product.findUnique en task.findUnique zodat
de helper bij unit tests veilig terugvalt op kind-defaults (alle 563
tests groen).
T-790: Sectie 'Config doorgeven aan Claude Code' toegevoegd aan
docs/runbooks/worker-idempotency.md met CLI-flag-mapping en de
verwachte aanroep per kind. Forward-link naar
docs/runbooks/job-model-selection.md (volgt in T-794).
Plus: docs/plans/job-model-selection.md (de approved plan-doc).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(PBI-67/ST-1300): cost-attribution voor thinking-tokens + admin UI
T-792: token-stats + token-history rekenen actual_thinking_tokens nu
mee in de totale kosten (tegen input-rate, conform Anthropic billing).
COALESCE-veilig zodat oude rijen 0 bijdragen i.p.v. NaN. Nieuwe export
`getTokenStatsByKind` aggregeert tokens en kosten per ClaudeJob.kind
zodat we relatieve uitgaven van IDEA_GRILL/IDEA_MAKE_PLAN/PLAN_CHAT/
TASK_IMPLEMENTATION/SPRINT_IMPLEMENTATION kunnen zien.
T-793: admin/jobs Kosten-tabel toont:
- Nieuwe kolom 'Thinking' (aantal verbruikte thinking-tokens)
- Mismatch-marker (rood) als requested_model afwijkt van actuele
model_id — duidt op een worker die de CLI-flag niet doorgaf.
Tooltip toont aangevraagd model. Geen Sentry/log-noise.
Page-level cost-berekening volgt dezelfde formule (input_price ×
thinking_tokens). 563 tests groen.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(PBI-67/ST-1301): runbook + CLAUDE.md updates voor model/mode-selectie
T-794: Nieuwe runbook docs/runbooks/job-model-selection.md met
override-cascade, kind-default-matrix, override-voorbeelden,
auditspoor en cost-attribution-formule. 107 regels.
T-795: CLAUDE.md hardstop-bullet voor 'Model/mode per ClaudeJob'
(verwijst naar nieuwe runbook) + patterns-quickref-rij voor
job-config resolver. CLAUDE.md blijft 139 regels (≤ 150).
T-796: docs:check-links groen — 108 files, geen broken links. Twee
externe-repo verwijzingen (scrum4me-mcp/...) ge-de-linked tot plain
text omdat de check-links script de zustertree niet traverseert; de
referenties blijven leesbaar.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ST-cmovs79lt: Schema + migratie PushSubscription model
Voeg PushSubscription model toe aan prisma/schema.prisma met
snake_case-conventie, relation field op User, en bijbehorende
migratie (push_subscriptions tabel, FK + index op user_id).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* ST-cmovs7e3o: web-push dependency + VAPID env vars feature-gated
Voeg web-push + @types/web-push toe aan package.json.
Registreer NEXT_PUBLIC_VAPID_PUBLIC_KEY, VAPID_PRIVATE_KEY,
VAPID_SUBJECT en INTERNAL_PUSH_SECRET als .optional() in lib/env.ts.
Documenteer alle vier in .env.example en README.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* ST-cmovs7jgr: lib/push-server.ts met sendPushToUser + stale-cleanup
Server-only push-lib met VAPID feature-gate, send naar alle
subscriptions van een user, en automatische cleanup bij 404/410.
Unit tests: success-pad, 410 verwijdert sub, 404 verwijdert sub,
andere errors loggen zonder delete.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* ST-cmovs7ouz: lib/push-client.ts client-side push helpers + stub actions/push.ts
Client-side helpers: isPushSupported, isIOSSafari, isStandalonePWA,
urlBase64ToUint8Array, subscribeToPush, unsubscribeFromPush.
Stub actions/push.ts zodat imports resolven (implementatie volgt
in volgende taak). Unit tests voor urlBase64ToUint8Array.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* ST-cmovs7ut4: actions/push.ts subscribeToPushAction + unsubscribeFromPushAction
Vervangt stub met volledige implementatie: requireUser via getSession,
demo-block, Zod-validatie, upsert met user_id-scoping en user-scoped
deleteMany. Tests (8): idempotentie, demo-block, unauthenticated, invalid input.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* ST-cmovs80c1: POST /api/internal/push/send met constant-time Bearer check
Route: 503 als INTERNAL_PUSH_SECRET uitstaat, 401 bij verkeerd secret
(timingSafeEqual), 400 bij invalid JSON, 422 bij Zod-fout, 204 bij succes.
push-server.ts: env-import vervangen door process.env om SESSION_SECRET
validatie tijdens build te omzeilen. Tests aangepast.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* ST-cmovs862j: Admin test-send route + public/sw.js service worker
POST /api/internal/push/test-send: requireAdmin check (redirect bij
niet-admin), optioneel body met defaults, roept sendPushToUser aan, 204.
public/sw.js: push-handler met showNotification, notificationclick met
same-origin guard, focus bestaand venster of openWindow.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* ST-cmovs8jvq: PushToggle component met 3 states + iOS-banner
Client component met states loading/unsupported/ios-needs-install/
denied/subscribed/unsubscribed. useEffect detecteert initial status,
permission-prompt alleen via user-click. iOS-banner NL, denied-uitleg,
subscribe/unsubscribe knoppen met sonner-toasts.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* ST-cmovs8psg: notifications-sheet + iOS meta-tags in layout
notifications-sheet.tsx: PushToggle onderin met sectie
'Notificatie-instellingen' en visuele scheidslijn.
app/layout.tsx: appleWebApp.capable, statusBarStyle en
mobile-web-app-capable meta-tags toegevoegd via Next.js Metadata API.
manifest.json had al display: standalone.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* ST-cmovs8vxj: docs/patterns/web-push.md pattern-documentatie
Architectuur-diagram, payload-shape, foutcodes, VAPID-config,
iOS-quirks, demo-users blokkade, trigger-voorbeelden (server +
HTTP) en admin-testroute curl-voorbeeld.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Voegt een verplicht code-veld toe aan Sprint, sequentieel per product
(consistent met PBI-N, ST-NNN, T-N).
- **Schema** — `Sprint.code String @db.VarChar(30)` + `@@unique([product_id, code])`
- **Migratie** — voegt kolom toe als nullable, backfillt bestaande sprints
via `ROW_NUMBER() OVER (PARTITION BY product_id ORDER BY created_at)`
als `SP-N`, en zet daarna NOT NULL + UNIQUE.
- **Generator** — `generateNextSprintCode(productId)` in lib/code-server.ts
volgt het patroon van story/pbi/task; createSprintAction gebruikt
`createWithCodeRetry` voor race-bescherming.
- **Seed** — sprint-counter per product (`SP-1`, `SP-2`, ...).
Zichtbaar in:
- Sprint-header (`Product › Sprint actief · SP-3`)
- JobCard + JobDetailPane voor SPRINT_IMPLEMENTATION jobs
- Insights: VelocityChart x-axis (compacter dan goal-truncated),
AlignmentTrend tooltip, SprintInfoStrip
- actions/jobs-page.ts: `sprintCode` is weer een echte code i.p.v. null
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* PBI-50 F1: SPRINT_BATCH execution-strategy + cross-repo blocker + branch-resume
Schema-migratie + Scrum4Me-side wiring voor de nieuwe SPRINT_IMPLEMENTATION-flow:
- prisma: PrStrategy ADD VALUE 'SPRINT_BATCH'; ClaudeJobKind ADD VALUE
'SPRINT_IMPLEMENTATION'; nieuwe enum SprintTaskExecutionStatus; ClaudeJob.lease_until
+ status_lease_until index; SprintRun.previous_run_id (self-relation
SprintRunChain) voor branch-hergebruik bij resume; nieuwe sprint_task_executions
tabel met frozen plan_snapshot + verify_required_snapshot per task in scope.
- actions/sprint-runs.ts startSprintRunCore: nieuwe blocker-type 'task_cross_repo'
voor SPRINT_BATCH (pre-flight rejecteert sprints met cross-repo task_url).
Bij SPRINT_BATCH: één SPRINT_IMPLEMENTATION ClaudeJob (geen per-task loop).
- actions/sprint-runs.ts resumePausedSprintRunAction: SPRINT_BATCH-pad met
remaining-execution-check; bij onafgemaakt werk → nieuwe SprintRun met
previous_run_id + run.branch hergebruikt + nieuwe SPRINT_IMPLEMENTATION-job.
Oude SprintRun → CANCELLED. Bestaande PBI-49 P0 scope-DONE pad ongewijzigd.
- actions/products.ts updatePrStrategyAction: accepteert SPRINT_BATCH.
- components/products/pr-strategy-select.tsx: drie opties met helptekst,
gebruikt @prisma/client PrStrategy ipv lokaal type.
- components/sprint/sprint-run-controls.tsx: BLOCKER_LABELS + blockerHref
voor task_cross_repo.
Migratie applied op Neon. Type-check + 532 tests groen.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* PBI-50 F5: cross-repo blocker test voor SPRINT_BATCH
- task_cross_repo blocker fires bij task.repo_url ≠ product.repo_url
- happy path: tasks zonder repo_url-override of met match → één
SPRINT_IMPLEMENTATION-job (niet per-task).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* PBI-50 F5: docs/architecture/sprint-execution-modes.md
Vergelijking PER_TASK vs SPRINT_BATCH met trade-offs, datamodel-
toevoegingen (SprintTaskExecution, lease_until, SprintRunChain) en
MCP-tools-matrix per modus. Toegevoegd aan breadcrumb in
docs/architecture.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Scrum4Me-side counterpart of scrum4me-mcp@f7f5a48 (PBI-9 + PBI-47):
- prisma migration: ClaudeJob.{base_sha,head_sha} + SprintRun.pause_context
- lib/pause-context.ts: Zod schema + parsePauseContext + pauseReasonLabel
helper; single source of truth for the JSON pause_context shape produced
by the mcp sprint-run flow (MERGE_CONFLICT pause)
- actions/sprint-runs.ts: resumePausedSprintRunAction — separate from the
existing FAILED-resume flow, requires SprintRun.status === PAUSED, closes
the linked ClaudeQuestion, clears pause_context, sets RUNNING/QUEUED based
on whether a claim is still active
- components/sprint/sprint-run-controls.tsx: PAUSED banner with reason label,
PR link, conflict-files list (max 5 + "+N more"), Resume button with
confirm() guard
- app/(app)/products/[id]/sprint/page.tsx: load pause_context from active
SprintRun and pass through to SprintRunControls
All MD3 tokens (warning-container, on-warning-container, primary). No raw
Tailwind utility colours.
Tests: 532 passing across 72 files (Scrum4Me side).
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ST-1243: F1 schema + propagateStatusUpwards-helper voor sprint-flow
Schema-uitbreidingen voor de sprint-niveau jobflow (PBI-46):
- TaskStatus, StoryStatus, PbiStatus, SprintStatus krijgen FAILED
- Nieuwe enums: SprintRunStatus, PrStrategy
- Nieuw SprintRun-model dat per-task ClaudeJobs groepeert
- ClaudeJob.sprint_run_id koppeling + index
- Product.pr_strategy (default SPRINT)
- Bijhorende Prisma-migratie
propagateStatusUpwards vervangt updateTaskStatusWithStoryPromotion en
herevalueert de keten Task → Story → PBI → Sprint → SprintRun bij elke
task-statuswijziging. Bij FAILED cancelt het sibling-jobs in dezelfde
SprintRun. PBI-status BLOCKED blijft handmatig en wordt niet overschreven.
Status-mappers + theme krijgen failed-token + label-uitbreidingen.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ST-1244: F2 sprint-runs actions + deprecate per-task enqueues
actions/sprint-runs.ts (nieuw):
- startSprintRunAction met pre-flight (impl_plan / open ClaudeQuestion / PBI BLOCKED|FAILED)
- Maakt SprintRun + ClaudeJobs in PBI→Story→Task volgorde
- resumeSprintAction zet FAILED tasks/stories/PBIs terug en start nieuwe SprintRun
- cancelSprintRunAction breekt lopende SprintRun af zonder cascade
actions/claude-jobs.ts:
- enqueueClaudeJobAction, enqueueAllTodoJobsAction, previewEnqueueAllAction,
enqueueClaudeJobsBatchAction nu deprecation-stubs (UI-cleanup volgt in F4)
- cancelClaudeJobAction blijft beschikbaar voor losse jobs
Tests bijgewerkt: 11 nieuwe sprint-runs tests, claude-jobs(-batch) tests
herzien naar deprecation-asserties.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ST-1246: F4 UI Start/Resume/Cancel sprint + pr_strategy dropdown
- components/sprint/sprint-run-controls.tsx: knoppen Start Sprint
(sprintStatus=ACTIVE), Hervat sprint (sprintStatus=FAILED) en
Annuleer sprint-run (lopende run). Pre-flight blocker-modal toont
blockers met directe links naar de relevante pagina's.
- components/products/pr-strategy-select.tsx: dropdown SPRINT|STORY in
product-settings, met optimistic update + sonner-toast op fail.
- actions/products.ts: updatePrStrategyAction (eigenaar-only, demo-block).
- Sprint-page: query op actieve SprintRun + tonen van controls-balk.
Live cascade-visualisatie (T-634) staat als follow-up genoteerd —
huidige sprint-board statusbadges volstaan voor MVP. De Solo-board
"Voer uit"-knoppen zijn niet expliciet verwijderd; ze tonen nu de
deprecation-error van de gestubde actions tot de Solo-flow opnieuw
ontworpen wordt.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(cleanup): verwijder Todo's navlink en todo-referenties uit marketing page [cmotto5ia000nx3178lq6xk8d]
- nav-bar.tsx: Todo's navLink verwijderd; Ideas-link blijft staan
- app/page.tsx: /todos quick-access link, feature-entry en /api/todos API-doc verwijderd
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(cleanup): verwijder app/(app)/todos/ en components/todos/ [cmottjvzo000cx3172472cu4g]
* test(cleanup): verwijder POST /api/todos import en describe-block uit security.test.ts [cmotto5jn000px317kjqlba89]
- Import 'POST as postTodo' uit verwijderde todos-route verwijderd
- describe('POST /api/todos') sectie (3 tests) verwijderd
- 73 testfiles / 561 tests groen
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test(cleanup): verwijder __tests__/api/todos.test.ts en __tests__/actions/todos-promote-idea.test.ts [cmottjw1u000fx317igq27mh5]
* feat(cleanup): verwijder actions/todos.ts en app/api/todos/route.ts; verplaats updateRolesAction naar actions/settings.ts [cmottjvy9000ax3173sgfjcqs]
* feat(db): migratie todos→ideas, counters bijwerken, todos dropt [cmotto5fh000jx317r7c5srvb]
Nieuwe Prisma-migratie die in één transactie actieve todos omzet naar
DRAFT-ideas met unieke IDEA-NNN codes, idea_code_counter per user
bijwerkt, en de todos-tabel dropt.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(schema): verwijder Todo model en relaties uit prisma/schema.prisma [cmottjvwu0008x317fwwodg3i]
* feat(cleanup): vervang open_todos door open_ideas in /api/products/:id/claude-context
Laatste prisma.todo-referentie verwijderd. Endpoint geeft nu open_ideas terug
(ideeën van de gebruiker voor dit product die niet gearchiveerd zijn en nog
niet status PLANNED hebben). Docs bijgewerkt in docs/api.md en
docs/api/rest-contract.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
- Voeg model_id, input_tokens, output_tokens, cache_read_tokens, cache_write_tokens (nullable) toe aan ClaudeJob
- Voeg nieuw ModelPrice model toe met per-1M prijsvelden en currency default USD
- Migratie 20260506010013_add_token_usage_fields aangemaakt en toegepast
- Seed uitgebreid met standaardprijzen voor claude-opus-4-7, claude-sonnet-4-6, claude-haiku-4-5-20251001
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Voegt IdeaProduct model toe met idea_id/product_id/created_at,
unique constraint op (idea_id, product_id), cascade-deletes.
Breidt Product.idea_products en Idea.secondary_products relaties uit.
Migratie 20260506010000_add_idea_product_secondary aangemaakt en toegepast.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Reactie op PBI-33 batch waar worker correct detecteerde dat werk al
gemerged was, maar geen passende status had om dat uit te drukken.
SKIPPED is bedoeld voor jobs met verify=EMPTY/DIVERGENT waar de
diff t.o.v. origin/main leeg is — geen FAILED (geen fout), geen DONE
(geen netto-output).
Migratie: ALTER TYPE ClaudeJobStatus ADD VALUE 'SKIPPED'.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Voeg PLAN_CHAT toe aan ClaudeJobKind enum
- Voeg UserQuestionStatus enum toe (pending, answered)
- Voeg UserQuestion model toe met idea_id, user_id, question, answer, status
- Koppel user_questions relatie aan Idea model
- Migratie: 20260505120000_add_user_question_plan_chat
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- new enums IdeaStatus, ClaudeJobKind, IdeaLogType
- new models Idea (with @@unique([user_id, code]) + pbi_id @unique) and IdeaLog
- User.idea_code_counter Int @default(0) for IDEA-{nnn} code generation
- ClaudeJob.task_id nullable; new idea_id + kind fields + index
- ClaudeQuestion.story_id nullable; new idea_id field + index
- existing call sites narrowed to story-questions / task-jobs (idea-paths come in T-502+)
- includes the M12 plan doc copied from /Users/janpetervisser/.claude/plans
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Pbi.code en Story.code worden NOT NULL (tot dusver optional)
- Task krijgt code String + product_id String denorm + @@unique([product_id, code])
- Product krijgt back-relation tasks Task[]
- Migratie backfillt bestaande NULL-rijen via PL/pgSQL:
PBI-N (per product), ST-N (3-digit padded met GREATEST om
truncatie van LPAD bij 4-digit nummers te voorkomen),
T-N voor alle tasks
- Codes zijn stabiele identifiers (Jira-stijl flat-per-product),
zodat re-parenting de code niet muteert
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Voeg twee optionele velden toe aan model Pbi voor het opslaan van de
PR-link en merge-status. Migratie 20260503145506_add_pbi_pr_link past
de database aan; Prisma-client is hergenereerd.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
When a task targets a different repo than its parent product (e.g. an
MCP-server task tracked under the main product's PBI), product.repo_url
points to the wrong place. Result observed in story 'Verify-gate'
batch (2 May): agent's gate-task work failed to push because product
was Scrum4Me but the code lives in scrum4me-mcp.
Add optional `Task.repo_url` (TEXT, nullable). scrum4me-mcp's
`resolveRepoRoot` will read this in a follow-up PR — null falls back
to product.repo_url, preserving current behaviour for the 99% case.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(schema): add Task.verify_required enum (ALIGNED / ALIGNED_OR_PARTIAL / ANY)
Adds VerifyRequired enum and verify_required field (default ALIGNED_OR_PARTIAL)
to the Task model. Also declares the claude_jobs_status_finished_at_idx index
in the schema to match the live DB. Applied via db execute + migrate resolve.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(ui): add verify_required select to TaskDetailDialog
SoloTask interface, solo page mapping, solo store, PATCH route handler
and TaskDetailDialog all updated to expose the three-level verify gate
(ALIGNED / ALIGNED_OR_PARTIAL / ANY) as a native select. Disabled with
DemoTooltip in demo mode.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat: add pushed_at field to ClaudeJob schema
Nullable DateTime column to record when the agent's feature branch was
pushed to origin. Enables the UI to show a 'pushed' state independently
of DONE status.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(db): promote task naar IN_PROGRESS bij ClaudeJob CLAIMED/RUNNING
Solo-kaart bleef in 'Te doen'-kolom staan terwijl de agent al bezig
was — alleen DONE was via een trigger gekoppeld (vorige migration).
Nieuwe Postgres-trigger claude_job_claim_to_task: bij INSERT of
UPDATE OF status naar CLAIMED|RUNNING promoot de bijbehorende task
van TO_DO naar IN_PROGRESS. Forceert niet vanuit andere status —
handmatige overrides (REVIEW, DONE) blijven staan.
De bestaande notify_task_change-trigger op tasks zorgt automatisch
voor de pg_notify zodat de Solo-paneel-UI direct synct.
- migration: 20260501130000_promote_task_to_in_progress_on_claim
- doc: architecture.md sectie 'Auto-promote task-status op job-overgangen' uitgebreid
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat: add plan_snapshot field to ClaudeJob schema
Nullable String? column on claude_jobs captures the task's
implementation_plan at claim time — immutable baseline for drift detection.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* docs: update ClaudeJob lifecycle with plan_snapshot
Document state machine snapshot capture/reset, plan_snapshot field
rationale, and drift-detection baseline semantics.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor: remove duplicate header labels on backlog page
Both the product H1 + description in the page header and the
"Product Backlog" panel-title in the PBI panel duplicated info
already visible in the NavBar. Removed both, keeping the right-aligned
action bars (activate/sprint/settings, plus filters/+PBI) intact.
PanelNavBar component is unchanged — Stories and Taken panels keep
their titles.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(ST-1111.1): add ClaudeJob model and state-machine enum
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(ST-1111.2): add ClaudeJob status API mappers
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(ST-1111.3): add enqueue/cancel ClaudeJob server actions with idempotency + NOTIFY
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(ST-1111.4): forward ClaudeJob events on solo SSE stream + initial state
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(ST-1111.6): add 'Voer uit' + cancel buttons to task detail dialog
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(ST-1111.7): add job status pill with spinner on solo task cards
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test(ST-1111.8): cover job-status mappers and enqueue/cancel actions
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* docs(ST-1111.9): document Claude job queue architecture and agent flow
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(ST-1111.10a): add ClaudeWorker presence model
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(ST-1111.10c): forward worker presence events on solo SSE + initial count
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(ST-1111.10d): show worker presence indicator and gate 'Voer uit' on connected workers
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(ST-1109.2): add PbiStatus enum and status field to Pbi model
- New PbiStatus enum (READY/BLOCKED/DONE) for PBI lifecycle tracking
- Pbi.status PbiStatus @default(READY)
- Index on (product_id, status) for filter queries
- Migration: 20260429150643_add_pbi_status
- ERD regenerated via prisma generate
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(ST-1109.3): add PBI status API mappers
- pbiStatusToApi / pbiStatusFromApi following same pattern as task/story
- PbiStatusApi type derived from PBI_DB_TO_API
- PBI_STATUS_API_VALUES export for downstream Zod schemas
- Lowercase API surface (ready/blocked/done), DB stays UPPER_SNAKE
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(ST-1109.4): support status in PBI create/update actions
- Optional status field in Zod schemas (lowercase API: ready/blocked/done)
- pbiStatusFromApi() maps to DB enum before persistence
- Status omitted on create => Prisma @default(READY) takes effect
- Update preserves existing status when not provided
- Demo-check unchanged
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(ST-1109.5): auto-mark PBI as DONE when all its stories are DONE on sprint close
Extends completeSprintAction's $transaction with PBI status cascade:
- Pre-transaction: identify PBIs touched by this close (via stories.pbi_id),
fetch each with all its stories
- Skip PBIs already DONE; skip PBIs with 0 stories
- Mark PBI DONE only when every story (post-decision) is DONE — stories
outside the sprint are evaluated against their current DB status
- Promote-only: never demotes a PBI that becomes "incomplete" again
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(ST-1109.6): add Popover primitive (base-ui wrapper)
- Mirrors the Tooltip pattern: render-prop composition, data-slot attrs
- Exports Popover (Root), PopoverTrigger, PopoverContent (Portal+Positioner+Popup)
- MD3 popover/popover-foreground tokens, animated open/close states
- Will be used to consolidate the backlog filter UI in ST-1109.8
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(ST-1109.7): add status select to PBI dialog
- New components/shared/pbi-status-select.tsx mirrors PrioritySelect:
PBI_STATUS_LABELS (NL), PBI_STATUS_COLORS, PbiStatusSelect component
- Reuses existing --status-todo/blocked/done MD3 tokens
- PbiDialog: status state with sync-on-open; default 'ready' for create,
pbi.status for edit; hidden input submits lowercase API value
- Priority + Status sit side-by-side in 2-col grid
- PbiDialogPbi.status is optional; pbi-list.tsx will populate in ST-1109.8
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(ST-1109.8): show PBI status badge and consolidate filters into popover
- Pbi.status (lowercase API) flows from page.tsx via pbiStatusToApi
- Status badge rendered in BacklogCard's badge slot using PBI_STATUS_COLORS
- Two old Select dropdowns replaced by single Popover with three pill-button
sections (Sorteren, Prioriteit, Status) and a "Wis filters" footer
- Filter trigger shows active count "(n)" badge in label
- Active priority/status filters still surface as dismissable chips next to
the trigger for at-a-glance feedback
- onEdit passes the full Pbi (incl. status) so the dialog opens with the
correct current status — closes the data flow loop opened in ST-1109.7
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(ST-1109.9): cover PBI status mappers and sprint-close cascade
- __tests__/lib/task-status.test.ts: 11 cases incl. round-trip + invalid
input for task/story/pbi mappers; verifies PBI_STATUS_API_VALUES shape
- __tests__/actions/sprints-cascade.test.ts: 8 cases for completeSprintAction:
promote on all-DONE, no promote on partial OPEN, respect out-of-sprint
story status, skip already-DONE PBIs, multi-PBI cascade, 0-story guard,
demo-user block
- Full vitest run: 170/170 green across 21 files
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(ST-1109.10): document PbiStatus enum, sprint-close cascade, and filter UI
- docs/scrum4me-architecture.md: pbis-table updated with status column +
index; PbiStatus enum + Pbi model in the Prisma schema sample;
cascade-on-sprint-close rule documented inline
- docs/scrum4me-styling.md: short note pointing to PBI_STATUS_LABELS /
PBI_STATUS_COLORS in components/shared/pbi-status-select.tsx so future
components don't ad-hoc-copy the color map
- docs/plans/ST-1109-pbi-status.md: in-repo mirror of the approved plan
(per feedback_plan_location memory) with cascade pseudo-code and
end-to-end verification checklist
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(ST-1109.11): persist backlog filters in localStorage
Filters reset op reload was verwarrend. Nu net als sortMode:
- scrum4me:pbi_filter_priority — 'all' | '1' | '2' | '3' | '4'
- scrum4me:pbi_filter_status — 'all' | 'ready' | 'blocked' | 'done'
useState-init met SSR-guard; ongeldige waarden vallen terug op 'all'.
Wis filters reset alle drie de keys correct (sortMode -> 'priority',
beide filters -> 'all'), waardoor de localStorage-staat consistent wordt.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(ST-1101..1108): add M11 — Claude question-channel milestone to backlog
Plant acht stories ST-1101..ST-1108 voor het persistente vraag-antwoord-kanaal
tussen Claude (MCP) en de actieve gebruiker. Eerste concrete uitwerking van
de AI-driven dev-flow-richting (strategisch besluit "B" uit overleg na M10).
Beveiligingsuitgangspunt: atomic answer via updateMany WHERE status='open',
demo-blok op write-tools, access-check via productAccessFilter in DB-query én
SSE-filter, cron-endpoint via Bearer-secret, geen vraag/antwoord-tekst in logs.
Hergebruikt bestaande scrum4me_changes-channel (uitgebreid met entity:'question')
en het LISTEN/NOTIFY+ReadableStream-pattern uit M8/M10. Nieuw: user-scoped SSE
op /api/realtime/notifications zodat de bell globaal werkt over producten heen.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(M11): swap demo-active sprint from M10 to M11
M10 is gemerged en afgesloten — M11 wordt de nieuwe demo-actieve milestone
zodat get_claude_context (via MCP) ST-1101 als next-story teruggeeft.
Drie maps in parse-backlog.ts uitgebreid: M11 priority=4, goal omschrijving,
sprint_status='ACTIVE'. M10 → COMPLETED.
Vereist npx prisma db seed na deze commit zodat de live DB de nieuwe
sprint-state weerspiegelt.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(ST-1108): add F-11b — Claude question-channel to functional spec
Voegt feature-omschrijving toe naast bestaande F-11 (Claude Code REST API).
Beschrijft het verloop (Claude → MCP-tool → DB → trigger → SSE → user → answer
→ trigger → Claude polls), acceptatiecriteria (8 items), randgevallen (offline-
Claude, assignee-change, expiry, abuse) en datamodel (claude_questions tabel).
Persona Lars als primair, Dina secundair voor klant-werk.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(M11): drop parser ACTIVE-flip; sprint goes via UI from now on
Bij M9/M10 hebben we de seed-flip (MILESTONE_SPRINT_STATUS pivot) gebruikt om
nieuwe stories als IN_SPRINT in een verse sprint te krijgen. Dat werkt maar
is fragiel:
- npm run seed wist user-data
- de "sprint" die de seed maakt is geen echte planning-actie
- bij multi-product scenario's breekt het model
Vanaf M11 gebruiken we de bestaande Sprint-creatie-UI van Scrum4Me. Stories
voor M11 worden via scripts/insert-milestone.ts (idempotent insert, geen
seed-reset) aan de DB toegevoegd; de gebruiker maakt zelf een Sprint aan in
/products/[scrum4me]/sprint en sleept ST-1101..1108 ernaartoe.
Parser-map M11 dus terug naar COMPLETED zodat een eventuele re-seed niet meer
een fake sprint aanmaakt voor M11-stories.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(ST-1101): add ClaudeQuestion model + notify_question_change trigger
Schema (prisma/schema.prisma):
- Nieuw model ClaudeQuestion: id (cuid), story_id (FK Cascade), task_id?
(FK SetNull), product_id (FK Cascade — gedenormaliseerd uit story.product_id
voor SSE-filter zonder join), asked_by (FK Restrict — Claude-token-houder),
question (Text), options (Json? — string[] voor multi-choice), status
('open'|'answered'|'cancelled'|'expired'), answer (Text?), answered_by
(FK SetNull), answered_at?, created_at, expires_at
- Indexes: (story_id, status), (product_id, status), (status, expires_at)
- Back-relations: User.asked_questions (ClaudeQuestionAsker),
User.answered_questions (ClaudeQuestionAnswerer), Story.claude_questions,
Task.claude_questions, Product.claude_questions
Migratie (20260427224849_add_claude_questions):
- Prisma-gegenereerde DDL voor claude_questions + indexes + 5 FK's
- Toegevoegde notify_question_change() functie + claude_questions_notify trigger
op AFTER INSERT/UPDATE
- Emit op BESTAANDE scrum4me_changes-channel met entity:'question' (i.t.t. M10
dat eigen scrum4me_pairing-channel kreeg) — solo-route in ST-1104 moet
entity='question' wegfilteren om regressie op solo-board te voorkomen
- Trigger leest story.assignee_id voor "wacht op jou"-emphase in payload
- DELETE niet ondersteund — questions gaan naar answered/cancelled/expired
Verification: Node pg-client roundtrip via DATABASE_URL toonde correcte payloads
bij INSERT (op=I, status=open) en UPDATE (op=U, status=answered) met alle FK-IDs
en assignee_id correct uit story-join.
Volgende stap M11: ST-1102 — vier MCP-tools in scrum4me-mcp-repo
(ask_user_question, get_question_answer, list_open_questions, cancel_question).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(ST-1103): add answerQuestion server action
actions/questions.ts:
- answerQuestion(questionId, answer) — auth + Zod + demo-blok + access-check
via productAccessFilter (anyone met product-membership mag antwoorden,
consistent met Scrum self-organizing — niet alleen story-assignee)
- Atomic prisma.claudeQuestion.updateMany WHERE id + status='open' +
expires_at>now → status='answered'; concurrent dubbele submit: één wint
(count=1), rest count=0 met disambiguatie via second findFirst
- revalidatePath('/', 'layout') refresh't NavBar bell-count voor SSR-paths;
realtime updates voor andere clients gaan via SSE in ST-1104/1105
- Begrijpelijke NL-foutmeldingen voor elk faalpad
Tests __tests__/actions/questions.test.ts (6 cases):
- happy: status update + revalidatePath called
- demo-block: error + geen DB-call + geen revalidate
- geen access: error + geen update
- al-answered: race-error 'is al answered'
- expired: race-error 'is verlopen'
- lege answer: Zod-validatie
Quality gates: lint 0 errors, tsc clean, vitest 145/145 (17 files).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(ST-1104): add user-scoped /api/realtime/notifications + filter solo-route
Twee delen:
1. Solo-route filter (1-regel-fix in app/api/realtime/solo/route.ts):
- NotifyPayload uitgebreid met entity:'question'
- shouldEmit returnt direct false bij entity='question'
Voorkomt dat solo-clients M11 question-events ontvangen (geen lekkage naar
het Solo-bord; geen onnodig netwerk-verkeer; loose coupling tussen features).
2. Nieuwe SSE-route app/api/realtime/notifications/route.ts:
- User-scoped (geen ?product_id=); query alle accessible product-IDs één keer
bij connect via productAccessFilter
- LISTEN scrum4me_changes; filter entity='question' && product_id ∈ accessible
- Initial-state-event NA LISTEN actief (race-fix conform M10 ST-1004):
query open vragen voor deze user's accessible products, stuur als event:state
met summary (id, story_code/title, assignee_id, question, options, expires_at)
- Hergebruikt het pg.Client + ReadableStream + heartbeat 25s + hard-close 240s +
abort-cleanup-pattern uit solo-route
Tests __tests__/api/notifications-stream.test.ts:
- 401 zonder iron-session cookie (en geen DB-call)
- Solo-route filter wordt visueel/E2E gedekt in ST-1108-acceptatie
Quality gates: lint 0 errors, tsc clean, vitest 146/146 (18 files).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(ST-1105): add NavBar bell + sheet + answer-modal + Zustand store + SSE hook
UI-volledig voor de Claude vraag-antwoord-flow (M11). Bel-icon links van avatar
in NavBar; klik opent slide-over rechts met openstaande vragen; klik op een vraag
opent een modal voor antwoord. Story-assignee = current user krijgt visuele
"voor jou"-emphase met primary-container accent en error-color badge-ring.
Bestanden:
- stores/notifications-store.ts — Zustand store met init/upsert/remove +
openCount/forYouCount selectors (vereenvoudigd vs solo-store: geen pendingOps,
geen optimistic-echo-onderdrukking)
- lib/realtime/use-notifications-realtime.ts — EventSource hook met state-
event en message-event handling, exponential-backoff reconnect, Page
Visibility pause-resume
- components/notifications/notifications-bridge.tsx — Server Component die
initial open-questions fetcht via productAccessFilter
- components/notifications/notifications-realtime-mount.tsx — tiny client
island dat de store hydrateert + de hook activeert
- components/notifications/notifications-sheet.tsx — shadcn Sheet met item-
lijst, "voor jou"-accent voor assignee-vragen, lege staat
- components/notifications/answer-modal.tsx — Dialog met options-radio of
free-text Textarea (max 4000), char-counter, demo-blok via Tooltip; bij
succes optimistisch remove + sheet blijft open zodat meerdere vragen
achter elkaar te beantwoorden zijn
- components/shared/notifications-bell.tsx — Bell-icon met badge (count >9 → "9+"),
ring-accent als forYouCount > 0, ARIA-label voor screenreaders
Wiring:
- components/shared/nav-bar.tsx — <NotificationsBell /> rechts naast <UserMenu>
- app/(app)/layout.tsx — <NotificationsBridge /> naast <SoloRealtimeBridge />,
user.id (server-side) als prop
base-ui-aanpassingen: SheetTrigger/TooltipTrigger gebruiken render-prop ipv
asChild (geen Radix).
Quality gates: lint 0 errors, tsc clean, vitest 146/146, npm run build groen.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(ST-1106): add cross-product access-isolation test for notifications SSE
Demo-policy + assignee-emphase zaten al in eerdere stories:
- answerQuestion demo-blok in actions/questions.test.ts (ST-1103)
- AnswerModal demo-tooltip in components/notifications/answer-modal.tsx (ST-1105)
- requireWriteAccess in MCP write-tools (ST-1102)
Deze story voegt expliciet een access-isolation-test toe op de notifications-
SSE-route: productAccessFilter wordt met de echte userId aangeroepen, en
prisma.product.findMany filter't op archived=false + user_id-scope. Dat
garandeert dat een gebruiker geen question-events ontvangt voor producten waar
hij geen membership op heeft.
Story-assignee-emphase blijft visueel-only (NotificationsBell ring-accent +
Sheet primary-container) — toegang werkt product-membership-breed zodat een
team-lid kan invallen als de assignee niet beschikbaar is.
Quality gates: lint 0 errors, tsc clean, vitest 147/147 (was 146).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(ST-1107): add Vercel cron expire-questions (+ M10 pairing cleanup)
POST /api/cron/expire-questions:
- Auth via Authorization: Bearer ${CRON_SECRET} (Vercel injecteert dit
automatisch wanneer de env-var op de project-omgeving staat); 401 als secret
niet matcht of niet is gezet (faal-veilig — geen open endpoint in dev)
- updateMany op claude_questions WHERE status='open' AND expires_at<now →
'expired'
- Bonus: zelfde route ruimt M10 login_pairings op (status='pending' AND
expires_at<now → 'cancelled'). Eén cron-job is goedkoper qua Vercel-budget
en houdt cleanup-strategie centraal — opvolg-actie uit M10 dat geparkeerd was.
Config:
- vercel.json: crons-entry { path: '/api/cron/expire-questions', schedule: '0 */6 * * *' } (4x/dag)
- lib/env.ts: CRON_SECRET als optional in Zod-schema
- .env.example: documentatie + openssl rand-tip
Tests __tests__/api/cron-expire-questions.test.ts (4 cases):
- 401 zonder Authorization-header
- 401 met verkeerde secret
- 401 als CRON_SECRET niet is gezet (faal-veilig)
- 200 met juiste secret: response { expired_questions, expired_pairings, ran_at }
+ beide updateMany WHERE/data correct
Quality gates: lint 0 errors, tsc clean, vitest 151/151.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(ST-1108): document M11 question-channel — API + architecture + pattern
docs/API.md — twee nieuwe secties:
- 'Notifications' met /api/realtime/notifications SSE-endpoint (event-shapes,
filter-rules, voorbeeld)
- 'Cron — Expire questions' met /api/cron/expire-questions (Bearer-auth,
schedule, response-shape, manual curl)
docs/scrum4me-architecture.md — nieuw hoofdstuk 'Vraag-antwoord-kanaal Claude
↔ user' tussen QR-pairing-flow en Projectstructuur:
- Mermaid sequence-diagram (Claude → DB → trigger → SSE → user → answer →
trigger → Claude polls)
- Threat-model-tabel (race, demo-misbruik, cross-product leak, cron-misbruik,
growth, log-leakage)
- Subsectie 'Waarom hergebruik scrum4me_changes-kanaal' met trade-off vs M10's
eigen-kanaal-aanpak
docs/patterns/claude-question-channel.md — herbruikbaar pattern 'Bidirectionele
async-comms tussen MCP-agent en interactieve user' met de vier eindpunten,
vier security-uitgangspunten, channel-strategie-tabel, TTL-richtlijn, en
sjabloon-bestanden per laag (DB / server / client / MCP-tools).
CLAUDE.md — extra rij in Implementatiepatronen-tabel die naar het nieuwe
pattern-doc verwijst.
Acceptatie 6 scenario's:
1. Sync happy path (MCP wait_seconds + UI submit) — handmatig getest tijdens
ST-1105 acceptance-loop met de q-test injection
2. Async happy path — gedekt door get_question_answer-tool in ST-1102 +
list_open_questions
3. Demo-block — actions/questions.test.ts (case 2: demo-user) + AnswerModal
tooltip (visueel)
4. Access-isolation — notifications-stream.test.ts (case 'access-isolation')
5. Expiry — cron-expire-questions.test.ts (case '200 met juiste secret')
6. Race — actions/questions.test.ts (case 'al-answered' via atomic updateMany)
Quality gates: lint 0 errors, tsc clean, vitest 151/151 (19 files), npm run
build groen.
M11 is hiermee feature-compleet. feat/M11-claude-questions heeft 12 commits
lokaal, klaar voor user-acceptatie en PR.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(ST-1107): cron schedule daily — Vercel Hobby allows only 1 run/day
Vercel deploy faalde met:
> Hobby accounts are limited to daily cron jobs.
> This cron expression (0 */6 * * *) would run more than once per day.
Schedule van 4×/dag (0 */6 * * *) naar 1×/dag (0 4 * * * — 04:00 UTC, rustig
tijdstip). Functioneel acceptabel: ClaudeQuestion TTL is 24u, dus daily
cleanup pakt alles dat in de afgelopen 24u verlopen is. Login-pairings TTL
is 2 min — die zijn al onbruikbaar zodra ze expiren, cron is alleen voor
status-housekeeping.
Schedule-referenties consistent bijgewerkt in docs (API.md, architecture,
backlog M11-sectie, plan-doc, pattern-doc) + comment in route.ts. Vermelding
overal dat dit een Hobby-plan-beperking is en Pro fijnmaziger ondersteunt.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(M10): swap demo-active sprint from M3.5 to M10
M3.5 was de demo-actieve sprint zolang er geen recentere milestone in progress
was. Nu M10 het actieve werk is, willen we dat get_claude_context (en
implement_next_story) ST-1001 als next-story teruggeven i.p.v. ST-350.
Vereist een herhaling van npx prisma db seed na deze commit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(M10): gate Solo demo-stories on M3.5 active milestone
De hardcoded Solo Paneel-demoset uit M3.5 (priority=2) schreeuwt over de
parser-driven M10-stories heen (priority=4) en laat get_claude_context op
"Gebruikersauthenticatie opzetten" wijzen i.p.v. ST-1001.
Sluit het blok nu alleen open als de actieve sprint van het Scrum4Me-product
M3.5 betreft. Voor M10+ leveren de parser-stories zelf de bord-content; de
demo-set blijft beschikbaar als M3.5 ooit weer ACTIVE wordt voor demo-doeleinden.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(M10): drop hardcoded Solo Paneel demo data from seed
DB wordt voortaan leidend voor de werkstaat; testdata voor andere projecten /
demo-scenario's komt elders. Deze hardgecodeerde set was specifiek gemaakt voor
de M3.5 Solo Paneel-demo en raakt nu het next_story-resultaat: priority=2 won
van de M10 parser-stories (priority=4) waardoor get_claude_context op
'Gebruikersauthenticatie opzetten' bleef hangen i.p.v. ST-1001.
Vervangt de eerdere M3.5-gating-aanpak (commit 0e3228d) — schoner om het
helemaal weg te halen dan met een conditional aanwezig te houden.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(M10): add npm run seed shortcut
Wrapt prisma db seed (die de bestaande prisma.seed-config in package.json gebruikt)
zodat re-seeden één korte invocatie wordt zonder de prisma-CLI-syntaxis te onthouden.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(ST-1001): add LoginPairing model + pg_notify trigger via migration
Schema (prisma/schema.prisma):
- model LoginPairing met id (cuid), secret_hash + desktop_token_hash (beide NOT
NULL — scheiden mobiel- en desktop-bewijs), status (pending|approved|consumed
|cancelled), optionele user_id met onDelete: SetNull, desktop_ua VarChar(255),
desktop_ip VarChar(45) voor IPv6, created_at + expires_at + approved_at +
consumed_at, indexes op (expires_at) en (status, expires_at)
- back-relation login_pairings LoginPairing[] op User
Migratie (20260427200734_add_login_pairing):
- Prisma-gegenereerde DDL voor login_pairings + indexes + FK
- Toegevoegde notify_pairing_change() functie + login_pairings_notify trigger
op AFTER INSERT/UPDATE; emit pg_notify('scrum4me_pairing', payload) met
{ op: 'I'|'U', pairing_id, status }
- DELETE niet ondersteund — pairings gaan naar consumed/cancelled, niet weg
- Channel naam analoog aan scrum4me_changes uit ST-801
Verification: Node pg-client roundtrip-test via DATABASE_URL toonde notifies bij
INSERT (op=I) en UPDATE (op=U) met correcte payload-shape.
Bouwt voort op M8 LISTEN/NOTIFY-infra. SSE-route /api/auth/pair/stream/[id] in
ST-1004 abonneert hierop.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(ST-1002): add pairing helpers, pre-auth cookie + paired-session guard
lib/auth/pairing.ts: pure crypto-helpers voor de QR-pairing flow.
- generateMobileSecret() / generateDesktopToken() — beide 32 bytes base64url, los
zodat ze elkaar niet onthullen
- hashToken(t) — sha256-hex
- verifyToken(t, hash) — timingSafeEqual met length-guard
- isPairedSessionExpired(session) — geëxtraheerde helper zodat de Server-
Component-render Date.now() niet rechtstreeks aanroept (React Compiler-flag)
lib/auth/pair-cookie.ts: HttpOnly pre-auth cookie helpers (s4m_pair).
- Path=/api/auth/pair, Max-Age=120s (gelijk aan pending-TTL pairing),
SameSite=Lax, Secure in productie
lib/session.ts: SessionData uitgebreid met optionele paired + pairedExpiresAt.
app/(app)/layout.tsx: guard die paired-sessies vernietigt zodra
pairedExpiresAt verstreken is en redirect naar /login.
Tests: 14 unit-tests in __tests__/lib/auth/pairing.test.ts dekken hash-
determinisme, timing-safe verify (true/false/length-mismatch), generator-
uniciteit en vier expiry-scenario's voor isPairedSessionExpired.
Quality gates: npm run lint (0 errors), tsc --noEmit clean, vitest 111/111.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(ST-1003): add /api/auth/pair/start with rate-limit + pre-auth cookie
POST /api/auth/pair/start (anon, runtime: 'nodejs'):
- Geen authenticateApiRequest — desktop heeft nog geen sessie
- Genereert los mobileSecret + desktopToken via lib/auth/pairing
- Persisteert alleen sha256-hashes in login_pairings; status='pending', expires_at = now + 2 min
- Slaat user-agent + best-effort IP op (afgekapt op kolom-grootte)
- Set-Cookie via setPairCookie helper: HttpOnly, Path=/api/auth/pair, Max-Age=120, SameSite=Lax
- Response body: { pairingId, mobileSecret, expiresAt, qrUrl } met qrUrl = origin/m/pair#id=…&s=…
→ secret reist alleen via fragment (#…), nooit in querystring of access logs
Rate-limit: 'pair-start' expliciet aan lib/rate-limit.ts CONFIGS toegevoegd
voor self-documentatie (10/min, gelijk aan login).
Tests __tests__/api/pair-start.test.ts (6 cases):
- 200 met body-shape (pairingId, mobileSecret 43-char base64url, qrUrl met
fragment, expiresAt ISO)
- alleen hashes in DB, geen plaintext
- cookie set met juiste opties
- UA + IP afgekapt op kolom-grootte
- IP=null als x-forwarded-for ontbreekt
- 11e POST levert 429 met NL foutmelding
Quality gates: lint 0 errors, tsc clean (na prisma generate), vitest 117/117.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(ST-1004): add SSE /api/auth/pair/stream with cookie auth
GET /api/auth/pair/stream/[pairingId]:
- runtime: 'nodejs', maxDuration: 300, dynamic: 'force-dynamic'
- Auth via s4m_pair HttpOnly cookie (readPairCookie + verifyToken tegen
desktop_token_hash); 401 zonder cookie of bij hash-mismatch, 404 als pairing
onbekend, 410 als verlopen — geen geheim materiaal in URL of querystring
- Hergebruikt LISTEN/NOTIFY-pattern uit app/api/realtime/solo/route.ts:
ReadableStream + dedicated pg.Client + heartbeat 25s + hard-close 240s
- Channel: scrum4me_pairing; filter notifies op pairing_id-match
- Initial 'state'-event direct na connect met huidige status (voorkomt race
waarbij approve net vóór SSE-open landt — desktop ziet 'm alsnog)
- Auto-close zodra status consumed/cancelled binnenkomt
- Fallback DIRECT_URL → DATABASE_URL (de eerste staat lokaal op een placeholder)
Tests __tests__/api/pair-stream.test.ts (4 cases — auth-paden):
- 401 zonder cookie (en geen DB-call gedaan)
- 404 op onbekende pairingId
- 410 op verlopen pairing
- 401 op cookie/hash-mismatch
Full-stream-test (LISTEN+notify-roundtrip) is een handmatige acceptatietest in
ST-1008 — niet zinvol te mocken voor v1.
Quality gates: lint 0 errors, tsc clean, vitest 121/121.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(ST-1005): add pairing server actions + mobile confirmation page
actions/pairing.ts (Server Actions, volgt docs/patterns/server-action.md):
- getPairingForApproval(pairingId, mobileSecret): auth + Zod + lookup + status
+ expiry + verifyToken-check; retourneert UA/IP/username voor de
bevestigingspagina. Demo MAG aanroepen (read-only).
- approvePairing: zelfde checks PLUS demo-blokkade (session.isDemo). Update
status pending→approved, zet user_id + approved_at, bumpt expires_at +5min.
Postgres-trigger emit pg_notify automatisch — desktop-SSE pikt het op.
- cancelPairing: status pending→cancelled. Demo mag annuleren.
- Tagged-union return-type uit loadPendingPairing voor schone discriminatie.
app/(app)/m/pair/page.tsx (Server Component, achter (app)/layout-guard):
- Geen searchParams uitlezen — page leest URL niet. Alleen statische uitleg +
PairConfirmation client-island.
app/(app)/m/pair/pair-confirmation.tsx (Client Component):
- useEffect parseert window.location.hash voor #id=…&s=… (server ziet de
fragment nooit)
- Roept getPairingForApproval om UA/IP/username op te halen
- Toont kaart "Inloggen als <username> op dit apparaat?" met UA + IP +
expliciete waarschuwing tegen phishing-QR; Bevestig/Annuleer-knoppen
- Na approve: window.history.replaceState wist de hash zodat back/forward de
secret niet meer onthult; transitioneert naar success-state
- queueMicrotask voor synchrone setState om React-Compiler "cascading renders"
warning te vermijden
Tests __tests__/actions/pairing.test.ts (11 cases):
- getPairingForApproval: ok + 5 fail-paths (geen sessie, approved, verlopen,
verkeerd secret, ongeldige cuid)
- approvePairing: happy + demo-block + verkeerd secret (geen DB-write)
- cancelPairing: happy + demo mag annuleren
Quality gates: lint 0 errors, tsc clean, vitest 132/132.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(ST-1006): add /api/auth/pair/claim with atomic consume + iron-session
POST /api/auth/pair/claim (cookie-auth, runtime: 'nodejs'):
- Auth via s4m_pair HttpOnly cookie alleen — body bevat enkel pairingId, geen
secret. Het cookie-token is het bewijs.
- Atomic state-transitie via prisma.loginPairing.updateMany met composite
WHERE (id + status='approved' + desktop_token_hash + expires_at > now);
PostgreSQL row-locking garandeert dat concurrent dubbele claims slechts één
count=1 zien — de rest 410.
- Bij geen rij geüpdate: tweede findFirst om te disambigueren tussen 401
(cookie matcht geen pairing) en 410 (al consumed/cancelled). Cookie altijd
gecleared bij faalpaden om herhaalde verwerking te voorkomen.
- Bij succes: getIronSession schrijft scrum4me-session-cookie met userId +
isDemo (uit user-record als vangnet) + paired=true + pairedExpiresAt = now+8h
(kortere TTL voor publieke desktops). s4m_pair wordt gecleared.
- Logging onder NODE_ENV !== 'production' alleen pairingId, nooit cookie of
mobileSecret.
Tests __tests__/api/pair-claim.test.ts (7 cases):
- 200 happy: updateMany met juiste WHERE, iron-session payload (userId, isDemo,
paired, pairedExpiresAt ~8h), save() called, s4m_pair cleared
- demo-vangnet: isDemo=true wordt doorgezet
- 401 zonder cookie (geen DB-call)
- 400 op malformed body
- 400 zonder pairingId
- 410 op tweede claim (al consumed, cookie cleared, geen session.save)
- 401 op cookie/hash-mismatch (cookie cleared)
Quality gates: lint 0 errors, tsc clean, vitest 139/139.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(M10): bump pending-TTL to 5min + repair MD3 contrast on pair page
TTL: 2 min was te kort voor handmatig curl-paste-confirm-testen — gebruiker
zag 'Pairing verlopen' voor hij kon bevestigen. Bumpt naar 5 min (gelijk aan
approved-TTL): nog steeds tight voor security, ruim voor menselijke reactie.
- app/api/auth/pair/start/route.ts: PENDING_TTL_MS 120s → 300s
- lib/auth/pair-cookie.ts: MAX_AGE_SECONDS 120 → 300
- __tests__/api/pair-start.test.ts: maxAge en expires_at-window meegegroeid
Kleuren: bevestigingspagina gebruikte bg-destructive/10 + text-destructive-
foreground — beide lichte kleuren, te weinig contrast. Vervangen door MD3
container-tokens (zelfde patroon als components/auth/auth-form.tsx):
- error-state: bg-error-container + text-error-container-foreground + border-l-4 border-error
- approved-state: bg-success-container + foreground + accent-border
- cancelled-state: bg-surface-container-high + neutral foreground
Quality gates: lint 0 errors, tsc clean, vitest 139/139.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(ST-1007): add QR login button on /login with SSE listener
Voltooit de desktop-zijde van de QR-pairing-flow. Gebruiker klikt "Inloggen
via mobiel" naast het wachtwoord-formulier → krijgt een QR-code → telefoon
scant en bevestigt → desktop wordt automatisch ingelogd zonder dat er ooit
een wachtwoord is getypt op het publieke apparaat.
app/(auth)/login/qr-login-button.tsx (Client Component):
- Phase-state: idle | starting | showing | expired | claiming
- klik → POST /api/auth/pair/start (credentials:'same-origin' voor s4m_pair)
- QRCodeSVG met fragment-URL als value (level=M, 200px); aria-label
- EventSource('/api/auth/pair/stream/<id>', { withCredentials: true })
vereist voor cookie-auth — standaard verstuurt EventSource geen credentials
- bij data.status === 'approved': es.close → POST /pair/claim → router.push('/dashboard')
- aftellende timer (mm:ss); bij 0s → 'expired' state met Vernieuwen-knop
- cleanup bij unmount: removeEventListener + close
- A11y: <details> sectie toont fragment-URL als kopieerbare tekst voor screenreaders en gebruikers zonder camera
app/(auth)/login/page.tsx: QrLoginButton onder het bestaande wachtwoord-form
met "of"-divider, achter de bestaande surface-container-low styling.
Dependency: qrcode.react ^4.2.0 (client-side SVG; geen extra round-trip;
mobileSecret blijft op desktop in JS-geheugen).
Quality gates: lint 0 errors, tsc clean, vitest 139/139, next build slaagt
(login-route static, m/pair en pair/* dynamic).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(ST-1008): document QR-pairing endpoints, flow, threat-model + pattern
docs/API.md — nieuwe sectie 'Auth — QR-pairing (M10)' met alle drie endpoints
(start, stream, claim), cookie-mechaniek, foutcodes (400/401/410/429),
curl-voorbeelden inclusief --cookie-jar.
docs/scrum4me-architecture.md — sectie 'QR-pairing flow' met:
- Mermaid sequence-diagram (start → QR → scan → approve → claim)
- Threat-model (replay, phishing-QR, demo-block, rate-limit, secret-leak,
long-lived sessie) met expliciete mitigaties
- TTL-rationale voor de drie tijden (5min pending / +5min approved / 8u paired)
- Subsectie 'Waarom geen secret in URL' — fragment-eigenschap + HttpOnly
cookie + twee gescheiden hashes
docs/patterns/qr-login.md — herbruikbaar pattern 'QR-pairing via unauth-SSE +
pre-auth cookie' met de drie endpoints, vier security-uitgangspunten,
sjabloon-bestanden, TTL-richtlijn, en wanneer NIET te gebruiken.
CLAUDE.md — extra rij in Implementatiepatronen-tabel die naar het nieuwe
pattern-doc verwijst.
Acceptatie ST-1008 (zeven scenario's):
- Happy path: gedekt door manuele E2E in vorige stories (gebruiker bevestigde
dat M10-stories op Solo bord verschijnen + curl-roundtrip werkt)
- Demo-block: actions/pairing.test.ts → approvePairing demo → Niet beschikbaar
- Replay: pair-claim.test.ts → 410 op tweede claim
- Expiry tijdens pending: pair-stream.test.ts + pairing.test.ts → 410/error
- Expiry tussen approve+claim: pair-claim.test.ts → 410
- Cookie-mismatch op SSE/claim: pair-stream.test.ts + pair-claim.test.ts → 401
- Secret niet in URL/logs: per ontwerp — fragment + cookie reizen niet via
URL-paden of querystrings (gedocumenteerd in architecture.md)
Quality gates: lint 0 errors, tsc clean, vitest 139/139 (16 files).
M10 is hiermee compleet — feat/M10-qr-login bevat 13 commits klaar voor
gebruiker-acceptatie en PR.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix: move logout form outside DropdownMenuContent so requestSubmit fires
UserMenu's hidden logout-form zat binnen <DropdownMenuContent>. Wanneer een
DropdownMenuItem onSelect vuurt, sluit base-ui de menu en unmount het
content-portal in dezelfde tick — waardoor de form verdwijnt voordat
requestSubmit() wordt aangeroepen, en logoutFormRef.current null is.
Form naar top-level van het component verplaatsen (als sibling van DropdownMenu,
binnen Fragment) houdt de ref geldig. Geen DOM-side-effecten — form is hidden,
zat nooit visueel in het menu.
Quality gates: lint 0 errors, tsc clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix: call logoutAction directly via useTransition instead of form-ref submit
De form-ref-dance werkte niet betrouwbaar in de huidige base-ui:
- onSelect vuurde requestSubmit() op een hidden form
- Form zat eerst binnen DropdownMenuContent (form geunmount → ref null)
- Form daarna naar top-level verplaatst — vuurde nog steeds geen request af,
vermoedelijk doordat onSelect in deze base-ui-build niet (consistent) een
click-event genereerde dat de form-API trigger'de
Vervang door directe call: Server Actions kunnen sinds Next.js 14 als async
functie worden aangeroepen vanuit Client Components. useTransition voorkomt
dat de UI bevriest tijdens de redirect.
Naast onSelect ook onClick als veiligheid voor het geval base-ui later weer
van event-prop wisselt — beide handlers wijzen naar dezelfde idempotente
function (handleLogout via startTransition).
Pendingstate ('Uitloggen…' label, disabled item) zodat dubbele klikken niet
dubbele logoutAction-calls afvuren.
Quality gates: lint 0 errors, tsc clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(ST-1007): listen for SSE 'state' event so approve-during-connect resolves
De SSE-route in ST-1004 stuurt de catch-up payload als `event: state\ndata: …`
om een race te dichten: tussen pair/start en SSE-open kan de mobiel approven,
de pg_notify fired vóór onze LISTEN actief is en gaat verloren (Postgres
queuet niet). De server compenseert door direct na connect een `state`-event
te sturen met de huidige status uit de DB.
Maar de client luisterde alleen op 'message'. EventSource routeert events met
`event: <name>` enkel naar listeners voor die exacte naam — het catch-up event
werd dus genegeerd. Gevolg bij een (zeldzame) race: QR blijft hangen tot
expiry omdat noch de notify noch de catch-up doorkomt.
Fix: dezelfde onMessage-handler ook aan 'state' binden (en netjes
unsubscriben bij cleanup). Geen server-side wijziging nodig — protocol bleef
bewust om de semantische scheiding 'initial state' vs 'live notify' te
behouden voor toekomstige clients die er onderscheid in willen maken.
Severity: middel-laag — kleine race-window, geen data/security-impact, alleen
"QR doet niks" tot user op Vernieuwen klikt.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(M10): close pair/stream race + demo-block on cancelPairing
Twee P1's uit code-review:
(1) pair/stream race: de findUnique die de pairing-status leest gebeurde vóór
LISTEN actief was. Als de mobiel approvet tussen die query en LISTEN: pg_notify
fired in dat venster gaat verloren (Postgres queuet niet voor abonnees die
nog niet listen) én was de eerder gelezen status stale. De catch-up state-
event emitte dus 'pending' terwijl de DB inmiddels 'approved' was, en de
desktop bleef hangen tot expiry.
Tweede findUnique toegevoegd ná LISTEN actief is: het venster sluit, omdat
elke approve na dat punt via de notify-handler doorkomt. Aanvullend op de
eerdere client-side fix die 'state' events nu ook routeert (commit d6e71f9).
(2) cancelPairing demo-block: cancel was een DB-write zonder demo-guard,
in tegenspraak met de "demo = 403 op writes"-regel. Demo-blokkade
toegevoegd; bestaande test omgedraaid naar 'wordt geblokkeerd, geen DB-write'.
Quality gates: lint 0 errors, tsc clean, vitest 139/139.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(tooling): extend backlog parser to support PBI-x milestone headers
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore(backlog): mark ST-801–806 as done
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(backlog): sorteer PBI's en stories op prio/code/datum, onthoud keuze in localStorage; vergroot sprint-afronden dialoog
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(ST-901): add user.active_product_id with FK to Product
- Nullable relation User → Product with onDelete: SetNull
- Index on active_product_id for join performance
- Migration: 20260427165329_add_user_active_product_id
- Install @tanstack/react-table (was missing from node_modules)
- Fix PRIORITY_COLORS ref removed in earlier refactor
- Note: User schema change affects vendor/scrum4me-mcp submodule — run prisma generate + tsc --noEmit there after merge
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: restore priority color on PBI filter pill
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(ST-902): add setActiveProduct + clearActiveProduct server actions
- actions/active-product.ts: setActiveProductAction validates access via
productAccessFilter, rejects archived products and demo users
- archiveProductAction: clears active_product_id for all affected users in transaction
- removeProductMemberAction: clears active_product_id for removed member
- leaveProductAction: clears active_product_id for leaving user
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(ST-903): load active product in layout, replace cookie with DB lookup in solo
- layout.tsx: fetch active_product_id, resolve product, clear stale ref server-side
- NavBar: add activeProduct prop (rendering changes in ST-904)
- solo/page.tsx: redirect via user.active_product_id instead of lastProductId cookie
- proxy.ts: remove lastProductId cookie logic
- lib/cookies.ts: deleted (no longer used)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(ST-904): split NavBar into 5 tabs with disabled-states and product-switcher dropdown
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(ST-905): add Activeer button per product row in dashboard and product header
* feat(ST-906): redirect to dashboard with toast when active product becomes inaccessible
* feat(ST-907): tests for active-product actions and functional spec update for M9
* docs(M9): add implementation plan document and link from backlog
* feat: active PB indicator, Maak actief button and new product link in settings
* feat: apply priority-color card style to sprint story rows
* fix: move add-to-sprint click from entire card to + Toevoegen button
* feat: apply priority-color card style to sprint task rows
* fix(sprint-backlog): prevent text selection on PBI collapse button
* chore: bump version to 0.4.0 (M9 active product backlog)
* fix(landing): align logged-in nav left to match app NavBar
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* docs(ST-511): add backlog entry for entity codes feature
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(ST-511): add createWithCodeRetry helper to handle P2002 race on auto codes
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(ST-511): retry on auto-code unique conflict in story and pbi create
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(ST-511): surface field errors for code and title in PBI dialog
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(ST-511): read create-state errors in Story dialog fieldError
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(ST-512): add backlog entry for REST API code/description/implementation_plan extensions; mark ST-511 done
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(ST-512): extend REST API with code, description and implementation_plan
- GET /api/products returns code, description and definition_of_done
- GET /api/products/:id/next-story returns story.code and per-task code + implementation_plan
- GET /api/sprints/:id/tasks returns description, implementation_plan, story_code and derived per-task code
- POST /api/todos accepts and returns optional description (max 2000)
All changes are additive — existing clients ignore unknown keys.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(ST-512): mark ST-512 as done
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(ST-513): add backlog entry for API hardening for Claude Code
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(ST-513): add task and story status mappers for API boundary
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(ST-513): expose lowercase status on API and accept lowercase in PATCH /api/tasks
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(ST-513): add metadata JSONB column to StoryLog
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(ST-513): accept optional metadata in story log and switch validation errors to 422
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(ST-513): add GET /api/health endpoint with optional db ping
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(ST-513): add GET /api/products/:id/claude-context bundled endpoint
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(ST-513): add docs/API.md and link from CLAUDE.md specs table
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(ST-513): mark ST-513 as done
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(ST-513): split 400 (malformed JSON) from 422 (validation), reject 'review'
Codex review on PR #2:
- P2.1: routes treated JSON parse failures as 422 instead of 400, breaking
the contract in docs/API.md. Replace `request.json().catch(() => null)`
with try/catch in 4 routes (tasks, reorder, todos, story-log) so a
malformed body returns 400 and only well-formed-but-invalid bodies
return 422.
- P2.2: PATCH /api/tasks/:id accepted `status: "review"`, but the sprint
task list UI does not render REVIEW (no label/color, the cycle helper
falls back to TO_DO). Reject `review` at the API until the sprint UI
is extended; document the subset in docs/API.md.
Tests in __tests__/api updated for the new contract (29 assertions:
zod-failures now expect 422, status payloads use lowercase API values,
sprint-tasks mocks include the new story relation).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Todos can now exist without a product link. Changed relation from
Cascade to SetNull so deleting a product doesn't delete unlinked todos.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Aligns schema with the API and server action, both of which already
require product_id. Changes onDelete from SetNull to Cascade — deleting
a product now also removes its todos.
Run: npx prisma db push
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Schema: bio (160), bio_detail (2000) en avatar_data (bytea) op User
- POST /api/profile/avatar: validatie MIME-type + max 12 MB vóór verwerking,
Sharp resize naar max 700x700 (fit inside), output WebP q85, opgeslagen als bytea in Neon
- GET /api/profile/avatar: serveert avatar met Cache-Control private 1u
- updateProfileAction: slaat bio en bio_detail op via Server Action + Zod
- ProfileEditor client component: avatar preview, upload met client-side validatie,
bio-velden met tekenlimieten
- Settings page: profiel-sectie bovenaan, uitgeschakeld voor demo-gebruiker
- next.config: sharp als serverExternalPackage
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add ProductMember model (many-to-many User ↔ Product)
- Add productAccessFilter helper (owner OR member OR clause)
- Replace all ownership checks across actions and API routes
- Add addProductMemberAction / removeProductMemberAction / leaveProductAction
- Add TeamManager component in product settings (owner adds/removes Developers)
- Add LeaveProductButton in user settings (member leaves a product team)
- Regenerate Prisma Client after schema migration
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Prisma schema: sqlite → postgresql provider met directUrl
- prisma.config.ts: directUrl toegevoegd
- seed.ts: dynamisch SQLite of PostgreSQL adapter op basis van DATABASE_URL
- ci.yml: preview deploy op PR, productie deploy op push naar main
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>