Commit graph

79 commits

Author SHA1 Message Date
Janpeter Visser
4a9db57e94
feat(PBI-63): meerdere sprints per product + EXCLUDED + sprint-switcher (#161)
- Sprint lifecycle: ACTIVE→OPEN, COMPLETED→CLOSED, +ARCHIVED (FAILED behouden)
- TaskStatus: +EXCLUDED (overgeslagen door agent-loop via bestaande TO_DO filter)
- Cookie-gebaseerde actieve sprint per product (lib/active-sprint.ts)
- Route splitsen: /products/[id]/sprint/[sprintId] + /sprint redirect-page
- NavBar: gestapelde product/sprint dropdowns + BUILDING-badge derivatie
- Backlog selectie-modus + nieuwe-sprint-dialog (createSprintWithPbisAction)
- Migratie 20260507210000_sprint_lifecycle: ALTER TYPE RENAME (geen data-rewrite)
- Version bump 1.0.0 → 1.2.0

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 00:15:04 +02:00
Janpeter Visser
7ae8a24372
Sprint: pbi-55 (#156)
* 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>
2026-05-07 21:46:01 +02:00
Janpeter Visser
25bd59c0b9
fix(PBI-59): jobs sorted newest-first, unified on created_at (#157)
- actions/jobs-page.ts: beide kolommen orderBy created_at desc
- stores/jobs-store.ts: nieuwe actieve jobs unshift (top) i.p.v. push (bottom)

Hiermee komen nieuw aangemaakte QUEUED/CLAIMED jobs bovenaan in de
linker kolom, in plaats van onderaan waar ze buiten het scrollbare deel
kunnen vallen.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 20:58:27 +02:00
Janpeter Visser
a268df3680
feat(PBI-59): Sprint.code (SP-N sequentieel per product) (#153)
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>
2026-05-07 20:10:16 +02:00
Janpeter Visser
16f01283ef
feat(PBI-59): add Detail/Usage view-switch on /jobs (#152)
Splits het middenpaneel van de jobs-pagina in twee views (zoals admin/jobs):

- **Detail** — alle metadata (status, kind, product, branch, PR, dates,
  errors, summary, verify-result) plus een kind-aware beschrijving:
  TASK → implementation_plan, IDEA_GRILL → grill_md, IDEA_MAKE_PLAN →
  plan_md, PLAN_CHAT → idea.description.
- **Usage** — model, tokens (in/uit/cache/totaal), berekende kosten in
  USD via ModelPrice-tabel, en duur (started→finished).

SprintSubTasksPane blijft als sticky header boven beide views.

Server action `fetchJobsPageData` haalt nu ook ModelPrices op en
selecteert task.{description,implementation_plan} +
idea.{description,grill_md,plan_md} zodat de description en costUsd in
JobWithRelations gevuld kunnen worden.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 19:51:53 +02:00
Janpeter Visser
a7e9ca1c35
fix(PBI-59): drop invalid Sprint.code select in fetchJobsPageData (#151)
Sprint heeft geen `code` veld; de query crashte met
PrismaClientValidationError zodra /jobs werd geopend. sprintCode blijft
in JobWithRelations als string|null voor UI-compat (JobCard.titleText
fallback) maar is nu altijd null.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 19:24:21 +02:00
Janpeter Visser
4a63b4b01f
Sprint: UI taken/ (#149)
* feat(PBI-58): Vitest-tests voor SoloTaskCard veldmapping en 4-regels layout

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(PBI-59): server action fetchJobsPageData voor jobs-pagina

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(PBI-59): SSE-route /api/realtime/jobs voor user-scoped job-events

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(PBI-59): JobCard component voor jobs-pagina

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(PBI-59): JobDetailPane component voor jobs-pagina

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(PBI-59): API route GET /api/jobs/[id]/sub-tasks voor sprint task executions

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-07 18:41:19 +02:00
Janpeter Visser
94f4f6ffd8
feat(PBI-33): chat-kanaal UI + lint cleanup (#145)
* feat(PBI-33): chat-kanaal UI — IdeaTimeline merge + UserChatInput

Voltooit de UI-laag van PLAN_CHAT (gebruikersvragen over plan, Claude
antwoordt async). Backend (UserQuestion model, createUserQuestionAction,
SSE-handling, server-side prop-passing) was al aanwezig — alleen de
UI-koppeling ontbrak waardoor userQuestions ongebruikt bleven.

- IdeaDetailLayout geeft userQuestions/planMd/ideaId/isDemo door aan
  IdeaTimeline en telt user-questions mee in de tab-count
- IdeaTimeline mergt user-questions chronologisch met logs+questions,
  rendert ze met MessageCircle-icoon en pending/answered status, en
  toont onderaan UserChatInput wanneer plan_md aanwezig is
- UserChatInput nieuw component met textarea + verzend-knop dat
  createUserQuestionAction aanroept en op success router.refresh()
  triggert zodat SSE de pending-state oppikt
- useNotificationsRealtime: router toegevoegd aan useEffect-deps zodat
  router.refresh() op user_question/idea-job events werkt zonder
  stale-closure waarschuwing

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

* fix(lint): unused vars/imports + react-hook-form watch incompatibility

Resolves de overige lint-warnings van de gefaalde sprint-build die los
staan van PBI-33. Eslint-config staat unused vars/args toe als ze met
'_' prefixen, dus required interface-params krijgen een prefix terwijl
losse dode constantes/imports verwijderd worden.

- sprint-header: productId is required prop maar nog niet gebruikt
  → prefix _productId i.p.v. verwijderen (caller passeert het door)
- agent-throughput: STATUSES-constante was dood — verwijderd, queries
  gebruiken hardcoded status-velden in de perDay-loop
- claude-jobs: productAccessFilter en enforceUserRateLimit waren
  dode imports — verwijderd
- story-log.test: ongebruikte 'data' binding vervangen door bare
  await res.json() zodat de stream nog wel geconsumeerd wordt
- product-dialog: form.watch('auto_pr') vervangen door useWatch met
  control-prop — useWatch is veilig voor React Compiler memoization

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 16:04:53 +02:00
Janpeter Visser
5cb3abbd3d
Sprint: Idee regril mogelijkheid (#144)
* feat(ST-cmovhveef): add PLANNED to GRILL_TRIGGERABLE_FROM and PLANNED→GRILLING transition

- GRILL_TRIGGERABLE_FROM now includes 'PLANNED' in actions/ideas.ts
- ALLOWED_TRANSITIONS PLANNED entry extended with 'GRILLING' in lib/idea-status.ts
- Updated canTransition test to reflect the new re-grill-from-PLANNED behavior

* test(ST-cmovhvef3): add exhaustive re-grill canTransition test covering PLANNED

Adds a loop test that asserts canTransition(status, 'GRILLING') for all
statuses in GRILL_TRIGGERABLE_FROM that support the transition, explicitly
documenting PLANNED as a valid re-grill entry point.

* feat(ST-cmovhvegf): add existingPbi pre-check in materializeIdeaPlanAction

- Adds options.allowAlongside parameter to control behaviour when a PBI
  with executed tasks already exists.
- Returns 409 PBI_HAS_ACTIVE_TASKS:<code> when tasks are DONE/IN_PROGRESS
  and allowAlongside is not set.
- Auto-deletes the old PBI inside the transaction when no tasks have been
  executed (atomic replace).
- Alongside mode (allowAlongside=true) skips deletion and creates a new PBI.

* test(ST-cmovhveh3): add pre-check integration tests for materializeIdeaPlanAction

Three new scenarios in ideas-crud.test.ts:
- auto-vervang: old PBI deleted in transaction when no executed tasks
- conflict-409: returns PBI_HAS_ACTIVE_TASKS:<code> with active tasks
- alongside: skips delete and creates new PBI when allowAlongside=true
Also adds task.count, pbi.findUnique, pbi.delete to prisma mock.

* feat(ST-cmovhveih): remove PLANNED-blokkering in idea-row-actions, add inline Bekijk-PBI button

- Removed grillBlockedReason guard for status==='planned', enabling re-grill from PLANNED
- Removed the early return for PLANNED that hid all standard buttons
- Added conditional 'Bekijk <code>' button at the start of the standard button set,
  visible only when status==='planned' and PBI + product_id are present

* feat(ST-cmovhvej7): add PBI_HAS_ACTIVE_TASKS alongside-dialoog in materialize handler

When materializeIdeaPlanAction returns code 409 with PBI_HAS_ACTIVE_TASKS:<code>,
a confirm dialog offers the user a choice: create new PBI alongside the existing one
or cancel. Alongside=true retries the action; cancel leaves the idea in PLAN_READY.
2026-05-07 15:27:43 +02:00
Janpeter Visser
07749ad9fb
PBI-50: SPRINT_IMPLEMENTATION single-session sprint runner (Scrum4Me-side) (#139)
* 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>
2026-05-07 13:05:02 +02:00
Janpeter Visser
e6dcc91383
PBI-49 P0: resumePausedSprintRunAction — DONE bij scope-completed (#138)
A STORY-mode MERGE_CONFLICT triggers AFTER all tasks are already DONE
(storyBecameDone is what produces the conflict event). The previous resume
logic put the SprintRun back to QUEUED, but there was no QUEUED job left for
a worker to claim — the run would hang forever.

Now: when both QUEUED and CLAIMED/RUNNING counts are zero, transition straight
to DONE (with finished_at set). The dev resolved the conflict manually and
the PR is theirs to merge. Existing behaviour preserved when active claims
or queued work remain.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 11:11:45 +02:00
Janpeter Visser
d3e79021c1
PBI-47: schema, pause_context Zod, resumePausedSprintRunAction, PAUSED-banner UI (#137)
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>
2026-05-06 22:17:11 +02:00
Janpeter Visser
77617e89ac
PBI-46: Sprint-niveau jobflow met cascade-FAIL (F1/F2/F4 Scrum4Me) (#136)
* 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>
2026-05-06 16:43:57 +02:00
Janpeter Visser
c18d17108c
ST-1240: Verwijder backend todo-code (server actions + API route) (#135)
* 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]

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-06 12:24:48 +02:00
Janpeter Visser
f09f5a2a06
feat(ST-9pobw4w6): setAllSprintTasksDoneAction — alle sprint-taken atomair op DONE (#120)
Voegt setAllSprintTasksDoneAction toe aan actions/sprints.ts. De actie haalt
alle taken op voor een sprint (via sprint_id + product_id), zet ze via een
interactieve Prisma-transactie op DONE met updateTaskStatusWithStoryPromotion,
en promoot parent-stories automatisch als alle taken DONE zijn.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-06 06:35:56 +02:00
Janpeter Visser
555ed8fe89
feat(ST-qfpqpxzy): DB schema + settings-UI voor min_quota_pct worker-drempel (#118)
- User.min_quota_pct Int @default(20) + ClaudeWorker.last_quota_pct/last_quota_check_at
- Migratie add_worker_quota_gate
- lib/schemas/user.ts: minQuotaPctSchema (int, 1-100)
- actions/settings.ts: updateMinQuotaPctAction met auth/demo/zod-guard
- MinQuotaEditor component met numeric input en DemoTooltip
- Settings-pagina: Worker-instellingen sectie

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-06 03:45:59 +02:00
Scrum4Me Agent
a5afb8c5fd feat(ideas): updateSecondaryProductsAction — atomisch vervangen secundaire producten
Voegt server action toe die secondary_products voor een idee atomisch
vervangt: primair product gefilterd, toegankelijkheid gevalideerd via
productAccessFilter, deleteMany + createMany in één transactie.
Demo-geblokkeerd, Zod-gevalideerd.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-06 02:23:00 +02:00
deb70a9e20 feat(T-572): map SKIPPED in lib/job-status + alle terminal-checks
- lib/job-status.ts: SKIPPED ↔ 'skipped' mapping in beide richtingen
- components/shared/job-status.ts: label "Overgeslagen" + neutrale italic styling
- actions/admin/jobs.ts: cancel-guard erkent SKIPPED als eindstatus
- app/api/cron/cleanup-agent-artifacts: SKIPPED ook opruimen na 7d
- lib/insights/agent-throughput: SKIPPED telt mee als terminal

ACTIVE_JOB_STATUSES bewust ongewijzigd — SKIPPED is afgerond.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 23:10:14 +02:00
fbf58d4e44 fix: admin-navigatie zichtbaar voor ADMIN-rol gebruikers
- requireAdmin() checkt nu de database i.p.v. session.isAdmin (was altijd undefined)
- loginAction stelt session.isAdmin in op basis van UserRole in de DB
- registerAction stelt session.isAdmin = false expliciet in
- NavBar toont 'Admin'-link conditioneel als roles.includes('ADMIN')
- UserMenu ROLE_LABELS uitgebreid met ADMIN → 'Admin'
- Tests aangepast: prismaUserRole.findFirst mock toegevoegd

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-05 20:46:27 +02:00
Scrum4Me Agent
1067167611 feat(ST-p6d1odh0): createUserQuestionAction — UserQuestion + PLAN_CHAT job queuing
- Nieuwe server action in actions/user-questions.ts
- Aanmaken UserQuestion + ClaudeJob PLAN_CHAT in transactie
- Blokkeert als idea.plan_md null is of product ontbreekt
- Idempotency-check: geen dubbele PLAN_CHAT per idee
- pg_notify claude_job_enqueued event voor SSE-realtime
- Rate-limit config uitgebreid met create-user-question

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-05 17:30:00 +02:00
Scrum4Me Agent
b9e6e725b6 feat(ST-abeu63oz): admin products-actions (create, update, archive, delete, addMember, removeMember)
- Zod-schema adminProductSchema (name, description, repo_url, definition_of_done, auto_pr, owner_user_id)
- adminCreateProductAction: owner-validatie, prisma.product.create
- adminUpdateProductAction: zelfde schema zonder owner_user_id
- adminArchiveProductAction: toggle archived-vlag
- adminDeleteProductAction: hard delete (cascade)
- adminAddMemberAction: upsert ProductMember
- adminRemoveMemberAction: deleteMany ProductMember
2026-05-05 14:54:08 +02:00
Janpeter Visser
9861495dbd
Merge pull request #100 from madhura68/feat/story-xmwvqru1
ST-1206: Admin: ClaudeJobs beheer (/admin/jobs)
2026-05-05 14:48:30 +02:00
Scrum4Me Agent
788920b790 feat(ST-xmwvqru1): admin jobs-actions (cancelJob, deleteJob)
- lib/session.ts: isAdmin: boolean toegevoegd
- lib/auth-guard.ts: requireAdmin() toegevoegd
- actions/admin/jobs.ts: cancelJobAction (CUID-validatie, eindstatus-check → CANCELLED),
  deleteJobAction (hard delete) — beide 'use server', revalidatePath('/admin/jobs')
2026-05-05 14:47:11 +02:00
Janpeter Visser
384a7ecd4a
Merge pull request #99 from madhura68/feat/story-111ci8t4
ST-1205: Admin: gebruikersbeheer (/admin/users)
2026-05-05 14:44:55 +02:00
Scrum4Me Agent
5fd56e3f67 feat(ST-111ci8t4): admin user-actions (delete, updateRoles, setMustResetPassword)
- lib/session.ts: isAdmin: boolean toegevoegd aan SessionData
- lib/auth-guard.ts: requireAdmin() toegevoegd (redirect /dashboard bij !isAdmin)
- actions/admin/users.ts: deleteUserAction (zelfbescherming), updateUserRolesAction
  (Zod z.nativeEnum, eigen ADMIN-rol-beveiliging, transactie), setMustResetPasswordAction
  — alle drie 'use server', revalidatePath('/admin/users')
2026-05-05 14:38:42 +02:00
Scrum4Me Agent
a19ae89e37 feat(ST-l9kkxh2m): /reset-password pagina + resetPasswordAction + hashPassword
- lib/auth.ts: hashPassword(password) geëxporteerd (bcrypt, rounds=12)
- actions/auth.ts: resetPasswordAction met Zod-validatie (min 8, superRefine gelijkheid),
  prisma.user.update (password_hash + must_reset_password=false), redirect /dashboard
- app/(auth)/reset-password/page.tsx: server guard (userId check + must_reset_password check)
- app/(auth)/reset-password/reset-form.tsx: client form (nieuw wachtwoord + bevestiging)
- __tests__/actions/auth.test.ts: 3 tests voor resetPasswordAction
2026-05-05 14:30:59 +02:00
9e8f33b96e fix(m12): user can answer idea-questions — inline + bell support
Two gaps discovered during the first live grill-session of IDEA-002:
the agent posted a question, but the user had no UI to answer it.
1. Idea-questions only appeared on the Timeline-tab as read-only entries
2. Notifications-bell fetched + handled story-questions only

This fix:

**Inline answer-form in IdeaTimeline** (components/ideas/idea-timeline.tsx)
- Open questions now render an AnswerForm directly under the question text
- Multi-choice options become clickable buttons (one-click submit); free-text
  fallback via collapsed details/textarea
- Plain free-text questions render textarea + Verzend
- Calls existing answerQuestion server-action; toast + router.refresh on success

**Notifications-bell extended for idea-questions**
- stores/notifications-store.ts: NotificationQuestion → discriminated union
  (kind: 'story' | 'idea'); forYouCount treats idea-questions as always-for-you
  (idea is strictly user_id-only — only the owner sees them)
- components/notifications/notifications-bridge.tsx: parallel fetch of
  story-questions (productAccessFilter) + idea-questions (idea.user_id ===
  session.userId); merged + sorted by created_at
- components/notifications/notifications-sheet.tsx: renders idea_code/title
  for kind='idea'
- components/notifications/answer-modal.tsx: header + open-link branch on
  kind (idea → /ideas/[id]?tab=timeline; story → existing /sprint link)
- lib/realtime/use-notifications-realtime.ts: idea-question events also
  trigger close+reconnect on 'open' (loads fresh detail) and remove(id) on
  non-open — same pattern story-questions already use
- components/shared/notifications-bell.tsx: badge counts idea-questions as
  for-you regardless of assignee

**Security gap closed (actions/questions.ts answerQuestion)**
Before: accepted any answer if user has product-access.
After: idea-questions require idea.user_id === session.userId; story-
questions keep the existing productAccessFilter path. (Prisma 7 rejects
\`{ not: null }\` in WHERE; routing happens app-level after a single fetch.)

Tests: 546/546 still green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 13:05:39 +02:00
492b71beb9 fix: drop \__test__\ export from actions/ideas.ts (use-server-only-fns)
Next.js 'use server' files only allow exports of async functions. The
\`export const __test__ = { canTransition }\` line at the bottom of
actions/ideas.ts threw a runtime error on every page load that imported
the file.

Tests already import canTransition directly from lib/idea-status; the
__test__ helper was vestigial. Removed.

Tests: 546/546 still green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 21:54:10 +02:00
6904de9f2b actions: promoteTodoToIdeaAction (M12 T-499)
actions/todos.ts:
- promoteTodoToIdeaAction(todoId): auth + demo + scope + already-archived
  guards. Atomic \$transaction creates DRAFT Idea (with auto IDEA-NNN code)
  and archives source Todo + IdeaLog{NOTE}.
- Anders dan Todo→PBI/Story (die de todo deleten): we ARCHIVEREN. De idea
  wordt het nieuwe planningsartifact; de archived todo bewaart het
  vertrekpunt (zie M12 grill-keuze 12).

Tests: 5 cases — happy, auth-401, demo-403, scope-404, already-archived-422.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 19:52:37 +02:00
6fee0394c5 actions: materializeIdeaPlanAction + relinkIdeaPlanAction (M12 T-498)
actions/ideas.ts:
- materializeIdeaPlanAction(id):
  - guard: status===PLAN_READY, plan_md present, product linked, demo-403
  - parsePlanMd → 422 with line-info on fail
  - Prisma.\$transaction:
    - SELECT max(code) for PBI/Story/Task within product
    - INSERT PBI with sort_order = lastPbi+1 within priority
    - per story: INSERT (sequential ST-NNN), per task: INSERT (T-N)
    - UPDATE idea SET pbi_id, status=PLANNED
    - INSERT IdeaLog{PLAN_RESULT, metadata}
  - returns 409 on P2002 (concurrent-materialize race)
- relinkIdeaPlanAction(id):
  - guard: status===PLANNED && pbi_id===null (PBI manually deleted via SetNull FK)
  - reverts to PLAN_READY + IdeaLog{NOTE}

Tests: 39 cases total (8 new for materialize + relink): happy creates entities,
status-mismatch-422, parse-fail-422 with details, demo-403, P2002→409,
relink happy + invalid-precondition guards.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 19:51:18 +02:00
33cbb6c2f4 actions: idea-job triggers + cancel (M12 T-497)
actions/ideas.ts:
- startGrillJobAction(id) — DRAFT/GRILLED/GRILL_FAILED/PLAN_READY → GRILLING;
  validates product+repo_url, idempotency check (active job 409),
  worker-count check (15s freshness), atomic $transaction creates ClaudeJob
  + flips idea.status + IdeaLog{JOB_EVENT}, manual pg_notify
- startMakePlanJobAction(id) — GRILLED/PLAN_FAILED/PLAN_READY → PLANNING;
  same shape via shared startIdeaJob helper
- cancelIdeaJobAction(id) — finds active QUEUED|CLAIMED|RUNNING job for idea,
  reverts status: grill→DRAFT/GRILLED based on grill_md presence;
  plan→GRILLED/PLAN_READY based on plan_md presence

Tests: 31 cases incl. happy path, demo-403, no-product/no-repo-422,
no-worker-422, idempotency-409, status-mismatch-422, cancel revert paths,
404 no-active-job.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 19:49:27 +02:00
5f410d3b10 actions: ideas CRUD + grill_md/plan_md edit + download (M12 T-496)
actions/ideas.ts (strikt user_id-only, geen productAccessFilter):
- createIdeaAction(input) — atomic nextIdeaCode + idea.create in $transaction
- updateIdeaAction(id, input) — guards on isIdeaEditable
- archiveIdeaAction / unarchiveIdeaAction
- deleteIdeaAction — refuses when pbi_id linked
- updateGrillMdAction — only in GRILLED|PLAN_READY; logs IdeaLog{NOTE}
- updatePlanMdAction — only in PLAN_READY; runs parsePlanMd; 422 with details on fail
- downloadIdeaMdAction — read-only, demo allowed

Added rate-limit configs: create-idea, edit-idea-md, start-idea-job,
materialize-idea.

Tests: 19 cases covering auth (401), demo (403), zod (422), status guards
(422), 404 cross-user-scope, plan-md parse-fail with details.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 19:47:30 +02:00
95eff4087c fix(demo): close 3 demo-policy gaps in mutation actions (before-launch)
Audit van alle Server Actions revealed drie mutation-paden zonder
isDemo-check, terwijl de demo-policy zegt "demo-user is read-only":

- toggleTodoAction: demo kon eigen todos done/undone toggelen
- archiveCompletedTodosAction: demo kon todos archiveren (bulk)
- leaveProductAction: demo kon productMembership verlaten

Fix: standaard `if (session.isDemo) return { error: 'Niet beschikbaar in
demo-modus' }` toegevoegd, conform de andere mutation-actions.

Andere claim/unclaim/reassign/updateTaskPlan-actions zijn al gedekt via
requireProductWriter() → requireWriter() → demo-throw — nu code-side
geverifieerd voor de hele actions/-tree.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 14:14:01 +02:00
a0a10001d5 feat(rate-limit): per-user mutation-rate-limiting (v1-readiness #3)
lib/rate-limit.ts: 11 nieuwe scope-configs + enforceUserRateLimit(scope, userId)
helper. Returnt { error, code: 429 } shape voor consistent foutbeleid.

Toegepast op de high-value mutation-paths:
- actions/pbis.ts createPbiAction
- actions/stories.ts createStoryAction
- actions/tasks.ts saveTask (alleen create-path) + createTaskAction
- actions/todos.ts createTodoAction
- actions/sprints.ts createSprintAction
- actions/products.ts createProductAction + createProductFormAction
- actions/api-tokens.ts createApiTokenAction
- actions/questions.ts answerQuestion
- actions/claude-jobs.ts enqueueClaudeJobAction + enqueueClaudeJobsBatchAction
- app/api/profile/avatar/route.ts POST
- app/api/stories/[id]/log/route.ts POST

Limits zijn ruim genoeg voor normaal gebruik, eng genoeg voor abuse-loops:
create-task 100/min, create-todo 60/min, create-pbi 30/min, create-product
5/min, create-token 10/uur, etc. Per-user scope (geen globale block).

Niet aangeraakt: reorder/status-toggle (intra-session frequent, lage abuse),
update/delete (laag-volume), cron-routes (CRON_SECRET-gated).

Consumer-tweaks: 'success' in result narrowing waar TS de bredere union niet
meer accepteerde. Tests: 9 nieuwe op rate-limit-helper.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 13:48:59 +02:00
13ab53ab8d feat(ST-1135): UA-redirect bij login — phone naar /m/* (T-322/T-323/T-324)
- lib/user-agent.ts (nieuw): isPhoneUA() — Mobi-substring heuristiek
  (telefoons hebben Mobi, tablets/desktop niet)
- actions/auth.ts loginAction: leest user-agent header na session.save();
  phone-UA + actief product → /m/products/[id]/solo, zonder → /m/settings;
  tablet/desktop/null-UA → /dashboard (ongewijzigd)
- Tests: 7 helper-cases + 6 loginAction-paden incl. demo-user

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 10:09:09 +02:00
7c82a736f5 feat(codes): server actions + seed/scripts gebruiken code overal
- actions/tasks.ts: saveTask + createTaskAction (legacy form) gebruiken
  createWithCodeRetry + generateNextTaskCode; persisten product_id
  denormalisatie. P2002 op user-supplied code wordt 422 met fieldError
- actions/pbis.ts + stories.ts: insert-helpers nemen verplichte string;
  update laat code-veld weg uit data wanneer null (kan niet meer leeg
  worden gemaakt nu DB NOT NULL is)
- actions/todos.ts: promoteTodoToPbi/Story genereren expliciet een code
  voor het transactiestart (kan niet binnen $transaction array retryen)
- prisma/seed.ts: per-product task counter geeft elke task een T-N code
- scripts/insert-milestone.ts: createMany berekent maxN voor product
  en assigneert T-{maxN+i+1} per nieuwe task

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 08:36:41 +02:00
4b0ab8e4b2 feat(answer-modal): conform aan dialog-pattern + entity-profile
Story 7 van PBI "Alle dialogen conform docs/patterns/dialog.md".

- lib/schemas/question-answer.ts — gedeeld zod-schema +
  ANSWER_MAX_CHARS constant
- actions/questions.ts gebruikt het gedeelde schema
- AnswerModal: entityDialog* layout-classes, useDirtyCloseGuard,
  useDialogSubmitShortcut, DemoTooltip rond submit + multiple-choice
  knoppen
- docs/specs/dialogs/answer-modal.md — entity-profile

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 07:34:56 +02:00
784791d8f9 feat(sprint-dialogs): conform aan dialog-pattern + entity-profile
Story 5 van PBI "Alle dialogen conform docs/patterns/dialog.md".

- lib/schemas/sprint.ts — gedeelde zod-schemas (create/dates/goal)
- actions/sprints.ts — code+fieldErrors voor 422; code: 403 voor
  auth/demo errors
- StartSprintButton dialog: useDirtyCloseGuard, useDialogSubmitShortcut,
  entityDialog* layout-classes; DemoTooltip op trigger; veld-niveau
  errors via fieldErrors
- SprintHeader's date- en complete-dialogen: zelfde behandeling; date-
  dialog krijgt dirty-guard, complete-dialog krijgt DemoTooltip op
  bevestigen
- docs/specs/dialogs/sprint.md — entity-profile dat alle drie de modes
  documenteert; consolidatie naar één SprintDialog component bewust
  uitgesteld
- Sprint-dates tests aangepast aan nieuwe action-shape

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 07:30:46 +02:00
01e77fc560 feat(story-dialog): conform aan dialog-pattern + AlertDialog delete
Story 4 van PBI "Alle dialogen conform docs/patterns/dialog.md".

- lib/schemas/story.ts — gedeeld zod-schema
- actions/stories.ts — code+fieldErrors voor 422; code: 403 voor auth/demo
- StoryDialog adopt useDirtyCloseGuard, useDialogSubmitShortcut,
  entityDialog* layout-classes
- Inline delete-confirm vervangen door AlertDialog (§10.4)
- docs/specs/dialogs/story.md — gaps weggewerkt; alleen bewuste
  afwijkingen blijven (header met badges, geen char-counter)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 07:26:56 +02:00
97dc4ee553 feat(pbi-dialog): conform aan dialog-pattern + DemoTooltip + dirty-guard
Story 3 van PBI "Alle dialogen conform docs/patterns/dialog.md".

- lib/schemas/pbi.ts — gedeeld zod-schema (createPbiSchema/updatePbiSchema)
- actions/pbis.ts — returnen nu code+fieldErrors (422) en code: 403 voor
  auth/demo errors
- PbiDialog adopt useDirtyCloseGuard, useDialogSubmitShortcut,
  entityDialog* layout-classes; submit-knop + Annuleren in DemoTooltip
- isDemo-prop toegevoegd, pbi-list geeft 'm door
- docs/specs/dialogs/pbi.md — "Bekende gaps" weggewerkt; alleen bewuste
  uitsluitingen blijven

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 07:23:14 +02:00
03a248b0fb feat(product-dialog): conform aan dialog-pattern + entity-profile
Story 2 van PBI "Alle dialogen conform docs/patterns/dialog.md".

- lib/schemas/product.ts — gedeeld zod-schema (Dialog API)
- actions/products.ts — createProductAction/updateProductAction returnen
  nu code+fieldErrors voor 422-validatie en code: 403 voor demo/auth
- ProductDialog adopt useDirtyCloseGuard, useDialogSubmitShortcut,
  entityDialog* layout-classes; 422-fieldErrors mappen naar form.setError
- docs/specs/dialogs/product.md — entity-profile

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 07:18:39 +02:00
Janpeter Visser
0ee03c6b72
ProductDialog: create + edit met alle velden (#68)
* feat(ST-?): createProductAction + updateProductAction (data-object API)

Voegt data-object-gebaseerde createProductAction(data) en
updateProductAction(id, data) toe aan actions/products.ts voor gebruik
door ProductDialog. Bevat Zod-validatie (incl. github-regex op repo_url),
productAccessFilter voor update, pg_notify bij update, en productMember-
aanleg bij create. FormData-varianten hernoemd naar ...FormAction; callers
bijgewerkt. 9 nieuwe tests groen.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(ST-?): ProductDialog component (create + edit modes)

Voegt components/dialogs/product-dialog.tsx toe op basis van het
entity-dialog-patroon. Gebruikt react-hook-form + zodResolver voor
client-side validatie. Roept createProductAction/updateProductAction
aan en werkt stores/products-store.ts optimistisch bij. Demo-modus
disabled alle velden + submit-knop via DemoTooltip. 7 tests groen.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(ST-?): UI triggers voor ProductDialog op dashboard en product-detail

Voegt NewProductButton toe op het dashboard (vervangt de /products/new
link) en EditProductButton op de product-detail pagina. Bewerken-knop
is alleen zichtbaar voor de product-eigenaar en verborgen in demo-modus.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(test): cast toast via unknown to satisfy strict TS

`toast as { success, error }` direct-cast faalt omdat sonner's toast een
callable + properties is. TS2352. Cast via unknown lost het op zonder
gedrag te wijzigen.

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-03 17:56:33 +02:00
Janpeter Visser
0ce6076a5c
Solo batch-enqueue: per-PBI volgorde + blocker-dialog (#65)
* feat(solo): orderBy taken per PBI-hiërarchie

Voeg pbi.priority en pbi.sort_order toe aan de task.findMany orderBy in de solo-page query zodat taken per PBI gegroepeerd worden vóór story- en task-volgorde.

* feat(solo): previewEnqueueAllAction met blocker-detectie

Voeg previewEnqueueAllAction toe aan actions/claude-jobs.ts: haalt taken op in PBI-volgorde, filtert actieve jobs, detecteert eerste blocker (REVIEW taak of BLOCKED PBI). Retourneert tasks[], blockerIndex en blockerReason. Tests: 7 nieuwe cases voor alle blocker-scenario's en demo-blokkering.

* feat(solo): enqueueClaudeJobsBatchAction met IDOR-check

Voeg enqueueClaudeJobsBatchAction toe: accepteert expliciete taskIds[], verifieert dat alle IDs bij de ingelogde gebruiker horen (IDOR-preventie), slaat taken met actieve jobs over (idempotent), en maakt jobs aan in transactie in opgegeven volgorde. 6 nieuwe tests.

* feat(solo): BatchEnqueueBlockerDialog component

Nieuw dialoogvenster dat gebruiker waarschuwt bij gedetecteerde blocker: toont blockerReason in NL, prefixCount taken vóór blokkade, confirm-knop (disabled met tooltip bij count=0) en annuleer-knop. 7 tests voor rendering, click-handlers en disabled-state.

* feat(solo): preview-then-confirm flow in SoloBoard Voer-alle-uit

Vervang directe enqueueAllTodoJobsAction door previewEnqueueAllAction + BatchEnqueueBlockerDialog. Geen blocker → enqueueClaudeJobsBatchAction direct. Wel blocker → dialog met prefix-enqueue of annuleer. Loading-state op knop tijdens preview en confirm. 5 integratie-tests.

* test(solo): uitgebreide batch-preflight tests met 2 PBI's en 4 taken

Nieuw claude-jobs-batch.test.ts: 10 gevallen voor previewEnqueueAllAction (PBI-volgorde, REVIEW/BLOCKED-detectie, active-job-skip met blockerIndex-shift) en enqueueClaudeJobsBatchAction (happy path, IDOR, active-job-skip, demo).
2026-05-03 13:55:13 +02:00
ce94fb48c3
Foundation: route, recharts, sprint-dates migration, chart-colors helper (#46)
* feat(ST-1201): add Sprint start_date/end_date + claude_jobs index migration

- Sprint model: optionele start_date en end_date (DATE) voor burndown x-as
- CREATE INDEX claude_jobs(status, finished_at) voor agent-throughput-queries
- Bestaande sprints houden NULL; burndown skipt die

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(ST-1202): add lib/chart-colors.ts + vitest coverage

MD3-token-to-CSS-var mappings for STATUS, PRIORITY, VERIFY, JOB_STATUS
and SERIES_COLORS; all 5 tests pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(ST-1203): add Insights link to NavBar

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(ST-1204): move Insights NavBar link between Solo and Todo's

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(ST-1205): add sprint start_date/end_date UI + server actions

- createSprintAction + updateSprintDatesAction: Zod date validation
  with end_date >= start_date cross-check
- start-sprint-button: date inputs in create dialog
- sprint-header: date display button + edit dialog with updateSprintDatesAction
- sprint page: select start_date/end_date for SprintHeader prop
- Demo blokkade via bestaande isDemo checks
- 6 tests groen (validation + demo guard)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-02 15:58:15 +02:00
9794a9baef
M13: Veilige Claude-agent-workflow (Scrum4Me-side) (#26)
* 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>

* feat: GitHub-link op DONE-card + pushed_at doorvoer

- lib/job-status-url.ts: getBranchUrl(repoUrl, branch) → GitHub tree URL
- JobState + ClaudeJobEvent: pushed_at? veld toegevoegd
- realtime/solo/route.ts: pushed_at in Prisma-select, JobPayload en mapping
- SoloBoardProps + TaskDetailDialog: repoUrl prop doorgevoerd
- task-detail-dialog: "Open op GitHub"-link als done + pushed_at + branch + repoUrl
- 3 unit-tests voor getBranchUrl; totaal 261 tests groen

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat: add VerifyResult enum, verify_only on Task, verify_result on ClaudeJob

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat: add verify_result+pushed_at to JobState, VerifyResultApi type, SSE payload

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat: verify_only field on SoloTask, PATCH route saves verify_only

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat: TaskDetailDialog — verify_result display + verify_only checkbox

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test: verify_only PATCH + verify_result dialog render + store fix

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* docs: document VerifyResult enum, verify_only task field, pushed_at in architecture

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(M13): cron /api/cron/cleanup-agent-artifacts — hard-delete FAILED/CANCELLED jobs >7 days

* feat(M13): add auto_pr field to Product schema + migration

* feat(M13): auto_pr toggle in product settings — server action + UI component + tests

* feat(M13): add pr_url to ClaudeJob schema + migration

* feat(M13): UI — 'Open PR' link on DONE-card; pr_url in JobState + SSE + task-dialog

* feat(M13): add retry_count migration + regen erd

- Migration ALTER TABLE claude_jobs ADD COLUMN retry_count INT DEFAULT 0
  (schema.prisma was reeds bijgewerkt in eerdere commits)
- docs/erd.svg geregenereerd voor de complete M13-schema-wijzigingen
  (verify_result, verify_only, pushed_at, pr_url, auto_pr, retry_count)

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-01 13:42:18 +02:00
3bb87f17ba
Solo Paneel header refactor + agent-workflow hardening (#24)
* feat: SoloBoard layout naar SplitPane met cookie-persistentie en tab-collapse

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat: verplaats Live + agent-status indicators naar NavBar

Live-dot (SSE-status) en "Agent verbonden / Geen agent" indicator zijn
verhuisd van de SoloBoard-header naar de NavBar (rechts, voor de
notifications-bell). Data blijft uit useSoloStore komen, gevoed door
SoloRealtimeBridge in de (app)-layout. Indicators tonen alleen op
/products/[id]/solo — buiten die route is de SSE-stream inactief.

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

* feat: open SoloRealtimeBridge globaal voor active product

SoloRealtimeBridge gated nu op active-product i.p.v. /solo-pad. Live-dot
en worker-presence werken daardoor op alle (app)-pagina's
(Producten/PB/Sprint/Solo/Todo's). Buiten /solo is de solo-store leeg en
zijn task-events no-ops, dus de stream gedraagt zich automatisch als
lichte presence-stream tot SoloBoard mount.

- realtime-bridge: productId-prop i.p.v. usePathname
- (app)/layout: activeProduct?.id doorgegeven aan bridge
- nav-status-indicators: pathname-check vervangen door hasActiveProduct prop
- nav-bar: hasActiveProduct={!!activeProduct} doorgegeven
- architecture-doc: realtime connection lifecycle bijgewerkt

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

* feat: enqueueAllTodoJobsAction voor batch-queueing van TO_DO-taken

Nieuwe Server Action die alle TO_DO-taken van een product zonder
actieve ClaudeJob in één $transaction als QUEUED jobs aanmaakt en
voor elk een pg_notify('claude_job_enqueued') stuurt zodat de SSE-
stream de UI live bijwerkt.

- Auth + demo-blokkade + product-access via productAccessFilter
- Idempotent: tasks met status QUEUED/CLAIMED/RUNNING worden overgeslagen
- 4 nieuwe tests (happy path, count=0, demo-blokkade, geen toegang)

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

* feat: 'Start agents (n)'-knop in Solo header, productname weg

SoloBoard-header toont nu een primary button die het aantal queueable
TO_DO-taken telt (TO_DO zonder actieve ClaudeJob via
claudeJobsByTaskId-store) en bij klik de nieuwe
enqueueAllTodoJobsAction aanroept. Toast geeft het aantal gestarte
agents terug.

- productname-h1 verwijderd (staat al in NavBar-dropdown, dubbel)
- sprintdoel blijft naast de knop
- 'Toon openstaande stories'-link blijft rechts
- demo-modus disabled met DemoTooltip
- batch-pending state voorkomt dubbele klikken
- productName-prop weg uit SoloBoard + page.tsx (was alleen voor h1)

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

* fix: scope enqueueAllTodoJobsAction op actieve sprint + assignee

De action queue'de eerder ALLE TO_DO-taken van een product, ongeacht
sprint of assignee — terwijl de 'Start agents (n)'-knop in de UI
alleen de taken telt die de gebruiker ziet (actieve sprint, eigen
stories). Daardoor kreeg een klik op de knop veel meer jobs aangemaakt
dan de count suggereerde (62 i.p.v. de getoonde n).

Server-filter komt nu overeen met page.tsx solo-query:
  story: { sprint_id: <activeSprint>, assignee_id: userId }

Edge case: geen actieve sprint → success met count=0 (geen error).

Tests aangepast + nieuwe test voor 'geen actieve sprint'-pad.

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

* feat(db): trigger sync_task_status_from_claude_job promote task naar DONE

Postgres AFTER-trigger op claude_jobs.status zet de bijbehorende
task automatisch op DONE zodra de job DONE wordt — werkt ongeacht
welke client de update doet (MCP-server, Server Action, raw SQL).

Idempotent: WHERE status <> 'DONE' voorkomt no-op updates die de
bestaande notify_task_change-trigger zouden doen vuren. Die laatste
verzorgt de pg_notify naar /api/realtime/solo zodat de UI synct.

- migration: prisma/migrations/20260501110000_sync_task_status_from_claude_job
- doc: nieuwe sectie 'Auto-promote task naar DONE' in architecture.md

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

* fix(ui): vul SoloColumn-kolommen volledige paneelhoogte

Buitenste flex-container van SoloColumn miste h-full, waardoor het
kader op content-hoogte bleef hangen i.p.v. de hele pane (binnen
SplitPane) te vullen. Drop-target was daardoor ook beperkt tot het
kleine kader bovenin een lege kolom.

Auto-toegepast door een ClaudeJob-agent op task
cmomoayt10002bortgp27jwma; co-auteurschap hieronder.

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

* docs: agent-batch-loop verplichte flow in CLAUDE.md

Na een 'pak de volgende job'-instructie liep de agent één job en sloot
de turn af, waardoor de gebruiker handmatig opnieuw 'wait_for_job'
moest aanroepen voor elke volgende job in de queue.

Voeg een expliciete loop-instructie toe onder de MCP-tools-sectie:
na elke update_job_status moet de agent opnieuw wait_for_job
aanroepen, totdat die na de full block-time terugkomt zonder claim.

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-01 11:41:35 +02:00
6cd98129f2
M14: TaskDialog (create/edit) + story auto-promotion (#21)
* chore(ST-1112): add deps for task dialog

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(ST-1112): add shared zod schema for task dialog

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(ST-1112): add missing MD3 tokens for task dialog

outline-variant, on-error-container, status-review (light + dark)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(ST-1112): add saveTask and deleteTask server actions for TaskDialog

Unified create/edit action (saveTask) replaces separate formData-based
actions for the new TaskDialog. Uses shared zod schema, structured
SaveTaskResult union type, and context-aware revalidatePath for both
sprint and backlog routes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(ST-1112): add TaskDialog component (create & edit mode)

Builds the full TaskDialog on top of the existing @base-ui/react
Dialog primitive. Covers create mode, edit mode (status field +
created_at metadata + delete), dirty-check AlertDialog, delete
confirm AlertDialog, Cmd+Enter submit, and per-field char counters.
Uses react-hook-form + zodResolver against the shared taskSchema.
Priority and status are extracted to PrioritySegmented and
StatusSelect sub-components using MD3 tokens throughout.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(ST-1112): refactor task-list to open TaskDialog via URL params

Replaces inline create/edit forms with router.push navigation:
- Clicking a task row → ?editTask=<id>
- "+ Taak" button → ?newTask=1&storyId=<storyId>
Removes CreateTaskForm, EditSubmitButton, updateTaskAction, and
createTaskAction from the component. Status toggle and DnD remain
unchanged. Rows now have cursor-pointer and keyboard a11y.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(ST-1112): wire TaskDialog into sprint page via searchParams

Sprint page now reads ?newTask, ?storyId, and ?editTask query params.
For edit mode: fetches the task server-side with productAccessFilter
scope (invalid/foreign IDs redirect to closePath). Renders TaskDialog
when either param is present. closePath is the sprint route without
query params.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(ST-1112): add Suspense skeleton for edit-mode task loading

Extracts task fetch into EditTaskLoader (async server component) so
the sprint board renders immediately while the task loads.
TaskDialogSkeleton shows 3 grey bars during the fetch. Invalid or
out-of-scope task IDs redirect to closePath.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(ST-1112): render description as markdown in task-detail-dialog

Solo task detail now renders description via react-markdown +
remark-gfm with prose styling. Sanitizes script/iframe elements.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(ST-1112): add saveTask/deleteTask server action tests

Covers all three demo-policy layers and cross-tenant scope:
demo blocked (403), unauthenticated blocked, validation 422,
edit cross-tenant forbidden, create cross-tenant forbidden,
and happy-path for both edit and create.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(ST-1112): add updateTaskStatusWithStoryPromotion helper

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(ST-1112): wire story-promotion into saveTask and PATCH /api/tasks/:id

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* docs(ST-1112): add task-dialog doc and architecture note

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore: extend allowed tools in settings.local.json

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(ST-1113): add 200ms animation-delay to TaskDialogSkeleton to prevent flicker

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(ST-1114): add DirtyCloseGuard reusable component for dirty-form close confirmation

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(ST-1114): add shared Markdown wrapper, apply to task-detail and story-dialog

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore: allow grep -E pattern in settings.local.json

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-30 16:55:20 +02:00
73087e9705
M13: Claude job queue — 'Voer uit'-knop + worker presence (ST-1111) (#18)
* 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>
2026-04-29 19:51:48 +02:00
8a9fb9d32b
M12 / ST-1109: PBI krijgt een status (Ready / Blocked / Done) (#16)
* 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>
2026-04-29 17:52:34 +02:00
9587ff4ff3
M11: Claude vraagt, gebruiker antwoordt (ST-1101..ST-1108) (#13)
* 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>
2026-04-29 11:38:23 +02:00