- app/(app)/admin/layout.tsx: admin-sidebar met links (Gebruikers/Claude Jobs/Producten)
- app/(app)/admin/page.tsx: redirect naar /admin/users
- app/(app)/admin/users/page.tsx: server component, query users+roles, geeft userId door
- components/admin/users-table.tsx: client component met UsersTable, RoleBadge,
RolesDialog (checkboxes, eigen ADMIN-rol geblokkeerd), DeleteDialog (confirm),
ResetToggle — alles via useTransition + server actions
Three places linked to \`/products/[id]/backlog#pbi-{pbi_code}\` after
materializing or in the planned-state link-card. That route doesn't
exist (product backlog lives at \`/products/[id]\` directly), and the
hash was double-prefixed (\`#pbi-PBI-32\`) since pbi_code already starts
with PBI-. Result: 404 for the user.
Fix: route to \`/products/[id]\` without anchor. The new PBI is the most
recent so visible near the top. Per-PBI anchor scrolling is a follow-up
once we add \`id="pbi-{id}"\` attributes to pbi-list rows.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
The notifications-realtime hook (PR #92) does close+connect on every
idea-question 'open' event, but the SSE state-event-handler in
/api/realtime/notifications only re-fetched story-questions. Result:
each reconnect wiped idea-questions from the bell within ~500ms, even
though the bridge had loaded them on initial page-render.
Symptom: clicking an idea-question in the bell sometimes hit a
\"question gone\" race because the close+connect after the live event
emptied them out.
Fix: SSE initial-state now does a Promise.all on
- story-questions (productAccessFilter, existing path)
- idea-questions (idea.user_id === session.userId, M12 strict-private)
and sends merged + sorted state with discriminated \`kind\` field.
This mirrors the bridge's own initial fetch (PR #92), so a bridge-mount
and an SSE-reconnect now produce identical state.
Tests: 546/546 still green.
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>