The /ideas/[id]?tab=timeline page is server-rendered: questions are a
prop snapshot. Without a router.refresh, new questions only show after a
manual page reload — and during a grill-session that's every ~20s.
Fix: in use-notifications-realtime, after dispatching idea-question
events to idea-store + re-syncing the bell, call \`router.refresh()\`.
This re-runs the server-component for whichever page the user is on; on
/ideas/[id] it pulls the new question. On other pages it's a no-op (no
visible state change).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
\`new Date(...).toLocaleString()\` zonder expliciete locale gebruikt de
default-locale van runtime: server (Node) levert nl-NL formaat
(\`05/05/2026, 13:21:51\`), browser CSR levert en-US (\`5/5/2026, 1:21:51 PM\`).
React detecteert dat als hydration-mismatch en regenereert de tree.
Fix: pass \`'nl-NL'\` met expliciete date/time-style. Server en client
produceren nu identieke output.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
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>
docs/backlog/index.md:
- New M12 row in milestone-overview
- Full M12 section with 10 stories (8 done, ST-1197 extern + ST-1201 in
progress); each story lists its task IDs
docs/runbooks/mcp-integration.md:
- wait_for_job payload contract documented per kind discriminator
(TASK_IMPLEMENTATION vs IDEA_GRILL vs IDEA_MAKE_PLAN)
- Per-kind agent behavior table
- 5 new MCP-tools documented: get_idea_context, update_idea_grill_md,
update_idea_plan_md, log_idea_decision; plus extended ask_user_question
contract (story_id|idea_id xor)
- Batch-loop step 2 expanded to switch on kind
docs/INDEX.md auto-regenerated (83 docs).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
components/shared/nav-bar.tsx:
- New nav-link to /ideas with active-state on pathname.startsWith('/ideas')
- Placement: between Insights and Todo's — matches the M12 plan
("direct boven Todo's")
- No icon (existing nav uses text-only links; deviation from plan's
Lightbulb spec for visual consistency with the rest of the nav)
Tests: 546/546 still green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
components/todos/todo-list.tsx:
- TodoCard: new "→ Idee" button next to "→ PBI" + "→ Story" (only shown
for non-demo)
- PromoteIdeaDialog: confirmation modal — no extra inputs needed since
promoteTodoToIdeaAction takes only todoId; title/description carry
over from the todo, status starts as DRAFT
- onPromoteIdea callback wired through TodoCard props
- On success: navigates to /ideas/{new-id} so user lands on the fresh
idea-detail page
Tests: 546/546 still green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
components/ideas/idea-md-editor.tsx:
- Textarea-based editor with monospace styling for grill_md / plan_md
- kind='plan': live yaml-frontmatter validation as derived state via
useMemo (no setState-in-effect); inline errors with line numbers
- kind='grill': free markdown, no validation
- localStorage draft per (ideaId, kind) — lazy initial-value seeded on
mount; toast notice if drift from server
- Cmd/Ctrl+S keyboard shortcut to save
- Server-action 422 details surface as separate submitErrors state
components/ideas/idea-detail-layout.tsx:
- Grill/Plan tabs flip into edit-mode via "Bewerk" button when:
- grill: status in [GRILLED, PLAN_READY] (M12 grill-keuze 12)
- plan: status === PLAN_READY
- Empty-state offers "Schrijf zelf" when md is null + editable
- Demo always read-only
Tests: 546/546 still green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
components/ideas/idea-row-actions.tsx — replaces T-507 placeholder:
- Grill Me: disabled in GRILLING/PLANNING/PLANNED, requires
product-with-repo + connectedWorkers > 0; tooltip shows specific reason
("Grill loopt al", "Idee heeft een product met repo nodig", "Geen
Claude-worker actief")
- Make Plan: enabled only in GRILLED/PLAN_FAILED/PLAN_READY; same
prerequisites as Grill
- Materialiseer: enabled only in PLAN_READY (no worker needed — synchrone
server-side parser); confirm-dialog before action; navigates to product
backlog PBI anchor on success
- *_FAILED: dedicated "Probeer opnieuw" rotate-icon button
- PLANNED: replaces all three with "Bekijk {PBI-code}" link + open-detail
- Demo: every mutating button wrapped in DemoTooltip with disabled state
- connectedWorkers read directly via useSoloStore (per M12 grill-keuze 16)
Tests: 546/546 still green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
app/(app)/ideas/page.tsx (server-component):
- user_id-only fetch (no productAccessFilter — Idee is privé)
- products fetched with productAccessFilter for filter-dropdown + create-form
components/ideas/idea-list.tsx (client-component):
- Search by title, product-dropdown filter, status multi-chip filter
- Inline create form with title/description/product (optional)
- Native shadcn Table + status badge via getIdeaStatusBadge (T-509)
- Row click navigates to /ideas/[id]
- Sonner toasts for success/error; router.refresh() after mutations
- DemoTooltip + disabled on Nieuw + Archive
- Empty-state + filtered-empty messaging
components/ideas/idea-row-actions.tsx (placeholder for T-508):
- "Open" navigation + "Archive" button only — Grill / Make Plan /
Materialiseer come in T-508 with full disabled-rules
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Idea-jobs and idea-questions are user-private (M12 grill-keuze 8) — they
flow through /api/realtime/notifications, not /api/realtime/solo.
app/api/realtime/notifications/route.ts:
- Pre-fetch user's idea-ids → accessibleIdeaIds Set (avoids per-event DB lookup)
- New IdeaJobPayload type (claude_job_enqueued/_status with kind=IDEA_*)
- New QuestionPayload narrows: story_id and idea_id mutually exclusive (DB
check-constraint enforces it)
- Routing: idea-jobs filtered on user_id; idea-questions on accessibleIdeaIds;
story-questions on accessibleProductIds (existing path)
app/api/realtime/solo/route.ts:
- JobPayload extended with optional kind + idea_id
- shouldEmit filters out kind=IDEA_GRILL/IDEA_MAKE_PLAN — they don't belong
on the product/sprint Solo Paneel
Tests: 539/539 green; notifications-stream test mock updated for idea.findMany.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- proxy.ts: /ideas added to protectedRoutes — unauthenticated users get
redirected to /login when navigating to /ideas or /ideas/[id]
- existing demo-guard catch-all (\`/api/* + non-GET\`) already blocks
POST/PATCH/DELETE /api/ideas* with 403 — confirmed via 3 new tests
- server-action endpoints (start-grill / start-make-plan / materialize /
promote-to-idea) carry their own \`session.isDemo\` checks inside
actions/ideas.ts and actions/todos.ts (defense in depth)
Tests: 9/9 in proxy demo-guard suite (added 3 idea cases).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- app/api/ideas/route.ts: GET (list with archived/product_id/status filters,
user_id-scope), POST (creates DRAFT with auto IDEA-NNN code, 201)
- app/api/ideas/[id]/route.ts: GET (idea + recent logs), PATCH
(ideaUpdateSchema, isIdeaEditable guard)
- lib/idea-dto.ts: API projection — converts Prisma row → DTO with
lowercase status + has_grill_md/has_plan_md flags (md content excluded
from list payloads, fetch via dedicated download action)
Auth: session OR API-token via authenticateApiRequest. Strict user_id
scope (no productAccessFilter — Idee is privé per Q8). 404 (not 403) for
foreign-user reads to prevent enumeration.
Tests: 13 cases (auth-401, demo-403, validation-422, malformed-400,
not-found-404, status-mismatch-422, filter param round-trip, DTO shape).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
PrismaClientValidationError ('Argument \`not\` must not be null') hit at
runtime when notifications-bridge mounted post-M12 schema change.
Although StringNullableFilter typings allow \`not: null\`, the v7 query
engine rejects it.
Removed the WHERE-side filter in 3 places — null-narrowing already
happens client-side via flatMap / Boolean filter:
- components/notifications/notifications-bridge.tsx
- app/api/realtime/notifications/route.ts
- lib/insights/verify-stats.ts (task_id filter)
Idea-questions / idea-jobs will be routed via separate channels in
T-502 + T-507; for now, story-question + task-job paths simply ignore
NULL rows in their post-fetch mapping.
Tests: 479/479 green; tsc clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- new tables ideas + idea_logs with FKs (User/Product/Pbi cascade rules per plan)
- claude_jobs.task_id nullable; new idea_id FK + kind enum + index
+ check-constraint: exactly_one(task_id, idea_id)
- claude_questions.story_id nullable; new idea_id FK + index
+ check-constraint: exactly_one(story_id, idea_id)
- notify_question_change trigger: handles null story_id; idea_id added to payload
Verified against dev DB: tables created, both check-constraints active
(neither-set insert correctly rejected with errcode 23514),
trigger replaced.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add FAQ subsection explaining that stories within the same batch don't
conflict (linear commits on shared branch), while parallel batches may
require rebase or serial PRs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- new tables ideas + idea_logs with FKs (User/Product/Pbi cascade rules per plan)
- claude_jobs.task_id nullable; new idea_id FK + kind enum + index
+ check-constraint: exactly_one(task_id, idea_id)
- claude_questions.story_id nullable; new idea_id FK + index
+ check-constraint: exactly_one(story_id, idea_id)
- notify_question_change trigger: handles null story_id; idea_id added to payload
Verified against dev DB: tables created, both check-constraints active
(neither-set insert correctly rejected with errcode 23514),
trigger replaced.
Co-Authored-By: Claude Opus 4.7 (1M context) <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>
docs/runbooks/v1-smoke-test.md (NIEUW): 11-secties handmatige checklist
voor de v1.0-pre-launch verificatie — auth, mobile UA-redirect, happy-path
flow, mobile shell, edit-flows, demo-policy, rate-limiting steekproef,
realtime, debug-routes 404 in productie, Lighthouse a11y per pagina,
rollback-trigger.
v1-readiness.md: 4 Before-launch items afgevinkt (demo-policy, privacy,
README, CHANGELOG); smoke-test verwijst nu naar de checklist; PWA-test
en v1.0.0-bump zijn de twee resterende handmatige items.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Privacy/PII review-pass van Server Actions, API-routes, debug-paths en
Sentry config:
✅ Sentry sendDefaultPii: false in alle drie configs (server/edge/client)
✅ Geen wachtwoord/email/token in console-logs
✅ Pair-id-logs zijn metadata-only (5-min TTL, geen secret)
⚠️ Vier debug-routes hadden geen auth-guard:
- /api/debug/realtime-stream — rauwe pg_notify-stream zonder filtering
- /api/debug/emit-test-notify — anonieme test-emit op het kanaal
- /debug-env — lekt env-var-metadata (hostnames, lengtes, pooled-flag)
- /debug-realtime — UI op dezelfde rauwe pg_notify-stream
Allemaal gemarkeerd als TIJDELIJK met VERWIJDEREN-comments uit M8.
Voor v1 launch: NODE_ENV-guard die in productie 404 retourneert. Lokaal
dev blijft alles werken voor debugging.
Toekomstige cleanup: kunnen worden verwijderd zodra M8-realtime stabiel
draait in productie en niemand ze meer nodig heeft.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
CHANGELOG.md: Keep-a-Changelog formaat met [Unreleased], [0.9.0]-release,
en compact-historie. Klaar voor v1.0.0 release-notes.
README:
- Test-count 69 → 445 (was outdated)
- Quick-start claim over auto-erd-watch in `npm run dev` corrigeren
(npm run db:erd:watch is optioneel, niet automatisch)
- Env-vars-tabel uitgebreid: CRON_SECRET (productie), Sentry DSN +
source-map vars (optioneel)
- CHANGELOG-link in Documentation-sectie
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Lighthouse-audit op /products/[id] flagde drie issues; fix in deze PR:
1. **[aria-*] attributes do not match their roles** — pbi-list.tsx had
aria-selected={isSelected} op role="button". aria-selected is alleen
geldig op tab/option/treeitem etc. Voor toggle-buttons is aria-pressed
de juiste attribute.
2. **Touch targets do not have sufficient size** — drie offenders op het
product-backlog scherm (PBI ✎/× iconen, Story ✎ icoon) hadden
~16-18×18px tap-targets via px-1.5/p-0.5. Lighthouse minimum is 24×24
en WCAG AA streeft 44×44. Fix: inline-flex + min-h-7 min-w-7 (28×28px)
met behoud van het kleine icoon — wel grotere clickable area.
3. Dashboard product-card pencil-icoon kreeg dezelfde fix preventief.
Sprint-backlog heeft hetzelfde patroon op meer plekken; bewust nu niet
aangeraakt om PR scope te beperken tot de ge-auditeerde route. Vervolg-PR
indien sprint-page-audit ook flagt.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Statische audit op happy-path-code; 4 categorieën gefixt vóór de Lighthouse-
verificatie die de gebruiker handmatig draait:
1. <main>-landmark op /login en /register (waren <div>); auth-pages krijgen
nu een correcte landmark zodat screen-readers ze kunnen overslaan/nav
2. solo-task-card.tsx: agent-status-pill had role="button" + aria-label maar
GEEN tabIndex en GEEN onKeyDown — keyboard-onbereikbaar. Nu compleet:
tabIndex={0} + Enter/Space-handler
3. Form-label-associaties via htmlFor + id-pairs:
- story-dialog (5): code, title, description, acceptance + priority via labelledby
- task-dialog (3): title, description, implementation_plan
- todo-list PromotePbi/PromoteStory dialogs (6): title, product, pbi, priority
Lighthouse a11y "form-field-multiple-labels" en "label" rules worden
hierdoor groen.
Niet aangeraakt:
- pbi-dialog: htmlFor was al goed gewired
- auth-form: htmlFor was al goed gewired
- Color-contrast: gebruikt MD3-tokens; theoretisch correct (verifieer in
Lighthouse run)
- Heading-hierarchy: nog niet gescand — kan in vervolgronde
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Vier config-files volgens Next.js 15+ conventie:
- instrumentation.ts (root) → koppelt server/edge config aan runtime-hook
- instrumentation-client.ts → client-init + onRouterTransitionStart
- sentry.server.config.ts → node-runtime
- sentry.edge.config.ts → edge-runtime (proxy.ts)
next.config.ts gewrapped met withSentryConfig:
- Source-map-upload ALLEEN als SENTRY_AUTH_TOKEN gezet is
- Tunnel /monitoring omzeilt ad-blockers (*.sentry.io)
- Silent buiten CI
SDK is no-op zonder NEXT_PUBLIC_SENTRY_DSN — geen network/overhead in
dev of bij ontbrekende creds. Sample-rates conservatief: errors 100%,
performance 10% in productie / 100% in dev. Geen Replay (privacy-review
nodig + overkill voor MVP). sendDefaultPii uit.
.env.example gedocumenteerd; architectuur-doc bijgewerkt met nieuwe
sleutelbeslissing en file-tree-aanvulling. v1-readiness #1 verschoven
naar 'done', #2 hiermee in flight.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Acceptatiecriteria vroeg om 'flow per scherm' beschrijving in de
Mobile shell sectie. Toegevoegd: stap-voor-stap flow voor Settings,
Backlog en Solo schermen.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>