* docs(dialog-pattern): add generic entity-dialog spec Introduceert docs/patterns/dialog.md als bron-of-truth voor elke create/edit/detail-dialog in Scrum4Me, ongeacht het achterliggende dataobject. Bevat 14 secties: uitgangspunten, stack, component- architectuur, layout, validatie, drielaagse demo-policy, submission, dialog-gedrag, theming, footer, triggers/URL-state, per-entiteit profile-template, out-of-scope, en een verificatie-checklist. Registreert het patroon in CLAUDE.md "Implementatiepatronen"-tabel zodat Claude (en mensen) de spec verplicht raadplegen voor elke nieuwe dialog. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(dialog-pattern): convert task spec + add pbi/story entity-profiles Reduceert docs/scrum4me-task-dialog.md van 507 naar ~140 regels: alle gedeelde regels verhuisd naar docs/patterns/dialog.md, dit document bevat nu alleen Task-specifieke velden, URL-pattern, status-veld, server actions, triggers en bewuste out-of-scope-keuzes. Voegt twee nieuwe entity-profielen toe voor bestaande dialogen: - docs/scrum4me-pbi-dialog.md (PbiDialog: state-based, code+title-rij, PbiStatusSelect, geen delete in v1) - docs/scrum4me-story-dialog.md (StoryDialog: state-based, header met status/priority badges, inline activity-log, demo-readonly-fallback, inline-delete-confirm i.p.v. AlertDialog) Beide profielen documenteren expliciet de "Bekende gaps t.o.v. generieke spec" zodat opvolgende PR's de afwijkingen kunnen rechtzetten of bewust kunnen accorderen. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Added pdevelopment docs * docs(plans): add docs-restructure plan for AI-optimized lookup Audit of existing 39 doc files (~10.700 lines) and a phased restructure proposal aimed at minimising the tokens an AI agent has to read to find the right reference. Captures resolved decisions on language (English), ADR template (Nygard default with MADR escape-hatch), index generator (node script), and folder taxonomy. Proposal status — fase 1 to follow. * docs(adr): add ADR scaffolding (templates, README, meta-ADR) Set up docs/adr/ as the canonical home for architecture decisions: - templates/nygard.md — default four-section format (Status, Context, Decision, Consequences) for one-way-door decisions. - templates/madr.md — MADR v4 with YAML front-matter and explicit Considered Options for decisions where rejected alternatives matter. - README.md — naming convention (NNNN-kebab-case), template-selection guidance (Nygard default; MADR for auth, queue mechanics, agent integration), status lifecycle, and ADR roster. - 0000-record-architecture-decisions.md — meta-ADR establishing the practice itself, in Nygard format. Backfilling existing implicit decisions (base-ui-over-radix, float sort_order, demo-user three-layer policy, etc.) is fase 6 of the docs-restructure plan. * feat(docs): add docs index generator + initial INDEX.md scripts/generate-docs-index.mjs walks docs/**/*.md, parses YAML front-matter (or first H1 fallback) and a Nygard-style ## Status section, then writes docs/INDEX.md with grouped tables for ADRs, Specs, Plans (with archive subsection), Patterns, and Other. Pure Node 20 (no external deps); idempotent — running it twice produces byte-identical output. Excludes adr/templates/, the ADR README, INDEX.md itself, and any *_*.md sidecar file. Wire-up: - package.json: docs:index → node scripts/generate-docs-index.mjs Initial run indexed 35 docs across the existing structure; the generated INDEX.md is committed so the table is reviewable in the PR before hooking generation into a pre-commit step. * chore: ignore Obsidian vault and personal sidecar files Add .obsidian/ (Obsidian vault config) and _*.md (personal sidecar notes) to .gitignore so the docs/ tree can serve as canonical source of truth while still being usable as an Obsidian vault for personal authoring. The docs index generator already excludes the same _*.md pattern from INDEX.md. * docs(plans): add PBI bulk-create spec for docs-restructure Machine-parseable spec for an executor that calls the scrum4me MCP (create_pbi → create_story → create_task) to seed the docs-restructure work into the DB. - Section 1 (Context) is the PBI description; serves as task-context via mcp__scrum4me__get_claude_context. - Section 2 lists the 6 resolved decisions (English, MD3+styling merged, solo-paneel merged, .Plans archived, Nygard ADR default, node index script). - Section 3 records what already shipped on this branch so the executor doesn't duplicate the ADR scaffolding or index generator. - Section 4 carries the structured YAML graph: 1 PBI, 8 stories (one per phase), 39 tasks. product_id is REPLACE_ME — fill before running. - YAML validated with PyYAML; field schema sanity-checked. * docs(junk-cleanup): remove stub patterns/test.md * docs(junk-cleanup): archive .Plans/ to docs/plans/archive/ * docs(front-matter): add YAML front-matter to docs/ root * docs(front-matter): add YAML front-matter to patterns/ * docs(front-matter): add YAML front-matter to plans + agent files * docs(index): regenerate INDEX.md after front-matter pass * docs(naming): drop scrum4me- prefix from doc filenames * docs(naming): lowercase API.md and MD3 filenames * docs(naming): rename plan file to kebab-case ASCII * docs(naming): rename middleware.md to proxy.md (next 16) * docs(naming): polish CLAUDE.md doc-index after renames * docs(taxonomy): scaffold topical folders under docs/ * docs(taxonomy): move spec files into docs/specs/ * docs(taxonomy): move design/api/qa/backlog/assets into folders * docs(taxonomy): move agent-instruction-audit into decisions/ * docs(split): break architecture.md into 6 topical files * docs(split): merge solo-paneel-spec into specs/functional.md * docs(split): merge md3-color-scheme into design/styling * docs(trim): extract branch/commit rules into runbook * docs(trim): extract MCP integration into runbook * docs(adr): add 0001-base-ui-over-radix * docs(adr): add 0002-float-sort-order * docs(adr): add 0003-one-branch-per-milestone * docs(adr): add 0004-status-enum-mapping * docs(adr): add 0005-iron-session-over-nextauth * docs(adr): add 0006-demo-user-three-layer-policy * docs(adr): add 0007-claude-question-channel-design * docs(adr): add 0008-agent-instructions-in-claude-md + update README index * docs(index): regenerate after ADR 0001-0008 * docs(glossary): add docs/glossary.md * chore(docs): regenerate INDEX.md in pre-commit hook * docs(readme): link INDEX + glossary + agent instructions * feat(docs): add doc-link checker script * chore(docs): wire docs:check-links and docs npm scripts * ci(docs): block merge on broken doc links * docs(links): fix broken cross-references after restructure --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
24 KiB
| title | status | audience | language | last_updated | applies_to | |||
|---|---|---|---|---|---|---|---|---|
| M11 — Claude vraagt, gebruiker antwoordt | active |
|
nl | 2026-05-03 |
|
M11 — Claude vraagt, gebruiker antwoordt
Persistent vraag-antwoord-kanaal tussen Claude Code (via MCP) en de actieve Scrum4Me-gebruiker. Claude schrijft een vraag naar claude_questions als hij vastloopt op een keuze; een Postgres-trigger emit op het bestaande scrum4me_changes-kanaal; de app toont een notificatie-badge; iedereen met product-toegang kan antwoorden; Claude leest het antwoord (sync via polling of in latere sessie via get_question_answer) en gaat door.
Eerste concrete uitwerking van strategische richting B (verdiepen van de unieke AI-driven dev-flow).
Backlog-entries: zie backlog.md § M11 (op te leveren in ST-1108).
Beveiligingsuitgangspunten:
- Atomic answer via
updateMany WHERE status='open'— concurrent dubbele submit kan niet - Demo-blok op
ask_user_question(MCP) enanswerQuestion(Server Action) - Access-check via
productAccessFilterin DB-query én SSE-filter; vraag-tekst en antwoord komen pas via een aparte authenticated query - Cron-endpoint beveiligd via
Authorization: Bearer ${CRON_SECRET} - Logging: alleen
question_id, nooit vraag/antwoord-tekst (kan gevoelige info bevatten)
Gekozen kaders (uit overleg):
- Sync-model: default async —
ask_user_questionretourneert direct metquestion_id; optionelewait_seconds(max 600) voor polling tot het antwoord er is - Answer-policy: iedereen met product-toegang mag antwoorden; story-assignee krijgt visuele "wacht op jou"-emphase
- Realtime: hergebruik
scrum4me_changes-kanaal (uitgebreid metentity: 'question'); aparte user-scoped SSE-route/api/realtime/notificationszodat solo-board-SSE product-scoped blijft
ST-1101 — ClaudeQuestion schema + Postgres-trigger
Bestanden
prisma/schema.prisma— modelClaudeQuestion+ relations opUser/Story/Task/Productprisma/migrations/<ts>_add_claude_questions/migration.sql— table-DDL + triggervendor/scrum4me-submodule inmcp— schema-sync ná merge
Stappen
-
Schema-uitbreiding:
model ClaudeQuestion { id String @id @default(cuid()) story_id String story Story @relation(fields: [story_id], references: [id], onDelete: Cascade) task_id String? task Task? @relation(fields: [task_id], references: [id], onDelete: SetNull) product_id String // gedenormaliseerd voor SSE-filter product Product @relation(fields: [product_id], references: [id], onDelete: Cascade) asked_by String asker User @relation("ClaudeQuestionAsker", fields: [asked_by], references: [id]) question String @db.Text options Json? // string[] voor multi-choice; null voor free-text status String // 'open' | 'answered' | 'cancelled' | 'expired' answer String? @db.Text answered_by String? answerer User? @relation("ClaudeQuestionAnswerer", fields: [answered_by], references: [id]) answered_at DateTime? created_at DateTime @default(now()) expires_at DateTime @@index([story_id, status]) @@index([product_id, status]) @@index([status, expires_at]) @@map("claude_questions") }Plus op
User:asked_questions ClaudeQuestion[] @relation("ClaudeQuestionAsker")enanswered_questions ClaudeQuestion[] @relation("ClaudeQuestionAnswerer"). -
Migratie-SQL voegt naast tabel + indexes ook trigger toe (mirror van
notify_pairing_changeuit M10 ST-1001):CREATE OR REPLACE FUNCTION notify_question_change() RETURNS trigger AS $$ DECLARE story_row record; payload jsonb; BEGIN SELECT assignee_id INTO story_row FROM stories WHERE id = NEW.story_id; payload := jsonb_build_object( 'op', CASE TG_OP WHEN 'INSERT' THEN 'I' ELSE 'U' END, 'entity', 'question', 'id', NEW.id, 'product_id', NEW.product_id, 'story_id', NEW.story_id, 'task_id', NEW.task_id, 'assignee_id', story_row.assignee_id, 'status', NEW.status ); PERFORM pg_notify('scrum4me_changes', payload::text); RETURN NEW; END; $$ LANGUAGE plpgsql; CREATE TRIGGER claude_questions_notify AFTER INSERT OR UPDATE ON claude_questions FOR EACH ROW EXECUTE FUNCTION notify_question_change(); -
npx prisma migrate dev --name add_claude_questions
Aandachtspunten
entity: 'question'is een nieuwe waarde naast bestaande'task'/'story'. Solo-route in M8 filter't viapayload.entity— moet'question'-events expliciet wegfilteren (niet emitten naar solo-clients)product_idop de question is gedenormaliseerd uitstory.product_id— voorkomt extra join in SSE-filter (zelfde keuze alsstory.product_idin M3)vendor/scrum4me-submodule sync vereist na merge (drift-checktrig_015FFUnxjz9WMuhhWNGBQKFD)
Verificatie
npx prisma migrate devslaagt;npx prisma validatecleanpsql $DIRECT_URL -c "LISTEN scrum4me_changes;"toont payload metentity: 'question'bij INSERT- Bestaande solo-flow nog steeds werkend (regressie-check)
ST-1102 — MCP-tools (in mcp-repo)
Bestanden
mcp/src/tools/ask-user-question.ts— nieuwmcp/src/tools/get-question-answer.ts— nieuwmcp/src/tools/list-open-questions.ts— nieuwmcp/src/tools/cancel-question.ts— nieuwmcp/src/index.ts— register de vier toolsmcp/scripts/smoke-test.ts— uitbreiden met question-roundtripmcp/README.md— tool-tabel uitbreiden
Stappen
-
ask_user_question(write-tool, sjablooncreate-todo.ts):- Input:
{ story_id, question, options?, task_id?, wait_seconds? }—wait_seconds0–600 (Zod.min(0).max(600)) requireWriteAccess(demo-blok)- Access-check:
userCanAccessProduct(story.product_id, auth.userId) - Insert met
expires_at = now() + 24h,status = 'open' - Als
wait_seconds === 0(default): return{ question_id, status: 'open' } - Als
wait_seconds > 0: poll elke 2s totstatus !== 'open'of timeout. Bij answered: return{ question_id, status, answer, answered_by, answered_at }. Bij timeout: return{ question_id, status: 'pending' }zodat Claude metget_question_answerlater kan ophalen - Polling-implementatie:
setIntervalmetPromiseen abort-signal voor schone cleanup
- Input:
-
get_question_answer(read-tool):- Input:
{ question_id } - Access-check via
userCanAccessProduct(question.product_id, auth.userId) - Output: full row (
status,answer,answered_by,answered_at,expires_at)
- Input:
-
list_open_questions(read-tool):- Input:
{ story_id? }(optionele filter) - Output: array van eigen vragen (
asked_by === auth.userId) met status open of answered, max 50, geordend opcreated_at desc - Bedoeld voor Claude om bij begin van een sessie te zien of eerdere vragen inmiddels beantwoord zijn
- Input:
-
cancel_question(write-tool):- Input:
{ question_id } - Alleen de asker mag cancelen;
requireWriteAccessvoor demo-blok - Atomic
updateMany WHERE id=… AND status='open' AND asked_by=… - Bedoeld voor wanneer Claude zelf de oplossing vindt en de vraag overbodig wordt
- Input:
-
Smoke-test in
scripts/smoke-test.ts:ask_user_questionmetwait_seconds=5+ parallelanswerQuestion(via REST of direct DB-write) → verifieer dat de tool het antwoord retourneert binnen het venster
Aandachtspunten
wait_secondspolling moet aborten als de MCP-cliënt disconnect (signal-check) — anders blijft de Node-process hangen op een dood socketoptions-veld accepteert string-array; in zod alsz.array(z.string()).optional()- Als
wait_seconds> 300 raakt 'm Vercel-deploy onmogelijk (Vercel-functies cap op 300s) — maar de MCP-server draait lokaal bij Claude Code, dus 600s mag
Verificatie
- MCP Inspector toont 4 nieuwe tools (totaal 13)
- Smoke-test groen: ask + answer roundtrip binnen 5s
- Demo-token op
ask_user_questionofcancel_questiongeeftPERMISSION_DENIED tsc --noEmitclean opmcp
ST-1103 — Server Actions voor de browser-UI
Bestanden
actions/questions.ts— nieuw__tests__/actions/questions.test.ts— nieuw
Stappen
-
answerQuestion(questionId, answer)(volgtdocs/patterns/server-action.md):getSession+requireUser; demo-blok viaif (session.isDemo) return { ok: false, error: 'Niet beschikbaar in demo-modus' }- Zod-input:
{ questionId: cuid, answer: string.min(1).max(4000) } - Lookup question + access-check via
userCanAccessProduct(question.product_id, userId) - Atomic
updateMany WHERE id=… AND status='open' AND expires_at > now()metdata: { status: 'answered', answer, answered_by: userId, answered_at: now } - Bij
count === 0: disambigueer (al-answered → 'al beantwoord', expired → 'verlopen', geen access → 'geen toegang') revalidatePath('/', 'layout')zodat badge-count overal updatet
-
cancelQuestionByAnswerer(questionId)— uitgesteld naar v2. Voor v1 alleen Claude (asker) kan annuleren via MCP. Als de UI later een dismiss-functie krijgt, komt het hier. -
Tests
__tests__/actions/questions.test.ts(6 cases):- happy answer → status='answered',
revalidatePathaangeroepen - demo-user → error + geen DB-write
- user zonder product-access → error
- already-answered → race-error (
updateMany count=0met status='answered' fallback) - expired → error
- empty answer → Zod-validatie
- happy answer → status='answered',
Aandachtspunten
revalidatePath('/', 'layout')is correct (zelfde keuze als M9setActiveProductAction) — badge zit in app-layout- Geen
revalidatePathop/sprintof/solonodig — die zien de question niet - Bij multi-tab: na answer in tab-1 verdwijnt het item in tab-2 via SSE-event, niet via revalidate. revalidate is voor de SSR-render-na-navigatie
Verificatie
npm test6/6 voor questions- Handmatig: open vraag in browser, antwoord, badge-count zakt met 1
- Demo-test: log in als demo, klik antwoord → toast "Niet beschikbaar in demo-modus"
ST-1104 — User-scoped SSE-route /api/realtime/notifications
Bestanden
app/api/realtime/notifications/route.ts— nieuwapp/api/realtime/solo/route.ts— uitbreiden omentity: 'question'te filteren (anders krijgt solo-client question-events ongewenst door)__tests__/api/notifications-stream.test.ts— nieuw (auth-cases)
Stappen
-
Route Handler — sjabloon uit
app/api/realtime/solo/route.ts:runtime: 'nodejs',maxDuration: 300,dynamic: 'force-dynamic'- Auth via iron-session cookie; 401 zonder
- User-scoped (geen
?product_id=-param). Bij connect: queryproductAccessFilter(userId)om alle accessible product-IDs te krijgen - LISTEN op
scrum4me_changes; filter:payload.entity === 'question'(anders skip)payload.product_id IN accessibleProductIds
- Initial-state-event direct na connect, na LISTEN actief: query
claude_questionsmetstatus='open'voor deze user's accessible products. Stuur alsevent: state\ndata: [{...question-summary...}]. Voorkomt race tussen connect en LISTEN (zelfde fix als M10 ST-1004) - Auto-close bij hard-close 240s; client herconnect
-
Solo-route bijwerken: in
shouldEmittoevoegenif (payload.entity === 'question') return false -
Tests (auth-paden, full-stream blijft handmatig):
- 401 zonder iron-session cookie
- Bij connect met sessie: list van accessible products correct gefilterd
- Question-event op een product zonder access → niet doorgegeven
Aandachtspunten
- Twee parallelle SSE-streams in browser (solo-route op product-pagina + notifications-route in app-layout) — netwerk-overhead aanvaardbaar; Vercel rekent per-actieve-functie ongeacht aantal streams
- Initial-state event content: een kleine summary (id, story_code, question, options?) per open vraag — voorkomt dat de bridge eerst een aparte fetch moet doen voor de initial badge-count
- Path expliciet maken in een client
useNotificationsRealtime-hook (volgtuseSoloRealtime-pattern)
Verificatie
curl -N --cookie session-jar /api/realtime/notificationsblijft openstaan, levertevent: statedirect- INSERT op
claude_questionsvoor een toegankelijk product → event binnen 1s - INSERT voor een ontoegankelijk product → geen event
- Solo-route op
/api/realtime/solo?product_id=…levert geen question-events meer
ST-1105 — Notifications-UI (Bell + Sheet + Answer-modal)
Bestanden
components/shared/notifications-bell.tsx— nieuwcomponents/notifications/notifications-sheet.tsx— nieuwcomponents/notifications/answer-modal.tsx— nieuwcomponents/notifications/notifications-bridge.tsx— nieuw, hookt SSE-listener aan storestores/notifications-store.ts— nieuwlib/realtime/use-notifications-realtime.ts— nieuwcomponents/shared/nav-bar.tsx—<NotificationsBell />toevoegen rechts (links van<UserMenu>)app/(app)/layout.tsx—<NotificationsBridge />mounten (analoog aan<SoloRealtimeBridge />)
Stappen
-
stores/notifications-store.ts— Zustand store; volgtstores/solo-store.ts-pattern:- State:
{ questions: Question[], pendingAnswerIds: Set<string> } - Actions:
init(q[]),add(q),update(q),remove(id),optimisticAnswer(id),rollbackAnswer(id, q) - Selectors:
openCount(userId),forYouCount(userId)(waar story-assignee = userId)
- State:
-
lib/realtime/use-notifications-realtime.ts— analoog aanuseSoloRealtime. EventSource opent op/api/realtime/notifications, dispatchtstate/message-events naar store viaadd/update/remove. Reconnect met exponential backoff. -
<NotificationsBridge />— Server Component die initial questions ophaalt en aan de store geeft viainit-prop. Mount in(app)/layout.tsxzodat de bridge altijd actief is wanneer user is ingelogd. -
<NotificationsBell />— Client Component:- Lucide
Bell-icon met badge:openCount(totaal) + accent-dot alsforYouCount > 0 - Klik:
setOpen(true)op de Sheet - Geen badge als count === 0
- Lucide
-
<NotificationsSheet />— shadcnSheetvan rechts:- Header: "Vragen van Claude (N)"
- Lijst gegroepeerd op product (analoog aan M5 todo-data-table-styling), elk item: story-code + truncated title, vraag-preview (line-clamp-2), assignee-emphase als forYou, "Beantwoord" knop opent answer-modal
- Lege staat: "Geen openstaande vragen. Lekker bezig!"
-
<AnswerModal />— shadcnDialog:- Story-context-link bovenaan (kleine kaart)
- Volledige vraag-tekst
- Als
options:<RadioGroup>met opties; geen vrije tekst - Anders:
<Textarea>(max 4000 chars, char-counter) - "Verstuur" + "Annuleer" knoppen; submit roept
answerQuestion-action viauseTransition - Demo-modus: knop disabled met tooltip
-
NavBar-edit:
<NotificationsBell />rechts naast de huidige avatar-trigger. Nieuwe gap-spacing in NavBar's right-section.
Aandachtspunten
- Bell-icon en avatar moeten visueel balanceren — hoogte/padding gelijktrekken
- MD3-tokens uit
docs/design/styling.md: badgebg-error text-error-foregroundvoor critical-count,bg-primaryvoor neutraal. Geen willekeurige Tailwind-kleuren - Optimistic-answer in store: voor het Server Action-resultaat zet item op pending; bij error rollback met sonner-error-toast
- Sheet-content blijft open zodat de user meerdere vragen achter elkaar kan beantwoorden (zelfde patroon als ST-358 openstaande-stories-sheet)
- ARIA: bell-icon heeft
aria-label="Notificaties — N open vragen", badgerole="status"
Verificatie
- Bell verschijnt in NavBar links van avatar; badge count = open question count
- Klik opent Sheet; lijst rendert correct met assignee-emphase
- Submit schiet event door — in tweede tab van zelfde user verdwijnt item binnen 1-2s
- Demo-modus: Sheet rendert, Modal opent, "Verstuur" disabled
- E2E-flow: Claude
ask_user_question→ bell-badge wordt 1 → klik → modal → submit → badge wordt 0 → Claude'sget_question_answerlevert antwoord
ST-1106 — Demo-policy + access-rules + tests
Bestanden
__tests__/actions/questions.test.ts— uitbreiden met access-cases (al opgezet in ST-1103)__tests__/api/notifications-stream.test.ts— access-cases- Documentatie-aanpassingen in
actions/questions.tsen SSE-route met expliciete demo-blok-comment
Stappen
- Verifieer dat
requireProductWriteralle Server-Action-mutaties al dekt (zou moeten — uit M3.5) - Voeg expliciete demo-test toe: demo-user opent answer-modal → Verstuur disabled met tooltip
- Voeg access-test toe: user-A heeft geen access tot product van user-B → user-A's notification-stream krijgt geen events voor user-B's questions
Aandachtspunten
- Story-assignee-emphase is alleen visueel — toegang is product-membership-breed. Dit is bewust: als de assignee niet beschikbaar is moet een andere member kunnen invallen
- Demo kan een vraag wel lezen (transparantie over hoe de feature werkt) — alleen niet beantwoorden
Verificatie
- 6+ tests groen (al gedekt in ST-1103/1104)
- Handmatige cross-product-test met 2 users + 2 producten
ST-1107 — Auto-expire + Vercel cron-cleanup
Bestanden
app/api/cron/expire-questions/route.ts— nieuwvercel.ts—crons-entry toevoegenlib/env.ts—CRON_SECRETtoevoegen aan Zod-schema.env.example—CRON_SECRETdocumenteren
Stappen
-
Cron-handler:
export const runtime = 'nodejs' export async function POST(request: Request) { const auth = request.headers.get('authorization') if (auth !== `Bearer ${process.env.CRON_SECRET}`) { return Response.json({ error: 'Unauthorized' }, { status: 401 }) } const result = await prisma.claudeQuestion.updateMany({ where: { status: 'open', expires_at: { lt: new Date() } }, data: { status: 'expired' }, }) // Optioneel: ook M10 login_pairings cleanup hier (eerder geparkeerd) return Response.json({ expired: result.count }) } -
vercel.ts:export const config: VercelConfig = { // ... bestaande config crons: [{ path: '/api/cron/expire-questions', schedule: '0 4 * * *' }], } -
Documenteer in
.env.example:CRON_SECRET=<openssl rand -base64 32>
Aandachtspunten
- Vercel cron POST't standaard zonder body; auth is alleen via header
- Op Hobby-plan zijn crons beperkt — check budget. M11 cron is 4x/dag, prima
- Cron-trigger update't
claude_questions→ trigger fired → SSE-events → UI updates badge real-time. Geen extra plumbing nodig
Verificatie
- Handmatig:
curl -X POST -H "Authorization: Bearer ${CRON_SECRET}" /api/cron/expire-questionsmet een vraag waarexpires_at < now→ response{expired: 1}, vraag verdwijnt uit notifications - Onbevoegde call zonder secret → 401
- Vercel dashboard toont cron-config na deploy
ST-1108 — Documentatie + acceptatietest
Bestanden
docs/api/rest-contract.md— secties "SSE — Notifications" + "Cron — Expire questions"docs/architecture.md— sectie "Vraag-antwoord-kanaal" met sequence-diagramdocs/patterns/claude-question-channel.md— herbruikbaar pattern-docdocs/backlog/index.md— M11-tabel-rij + M11-sectieprisma/seed-data/parse-backlog.ts—M11: 'ACTIVE',M10: 'COMPLETED',M3.5: 'COMPLETED'CLAUDE.md— pattern-doc verwijzing in Implementatiepatronen-tabel
Stappen
-
Backlog-tabel-rij + M11-sectie in
docs/backlog/index.md(mirror M10-format met Implementatieplan: verwijzing naar dit doc) -
docs/architecture.md§ "Vraag-antwoord-kanaal":- Mermaid sequence-diagram: Claude → MCP → DB → trigger → SSE → user → Server Action → DB → trigger → polling-tool
- Threat-model-tabel (replay, demo-block, access-leak, expiry, race)
- "Waarom hergebruik scrum4me_changes-kanaal" sub-sectie
-
docs/patterns/claude-question-channel.md— generiek pattern voor toekomstige bidirectionele async-communicatie tussen MCP-agents en interactieve users -
Parser-flip: M11 wordt nieuwe ACTIVE-milestone, M10 → COMPLETED. (Zelfde patroon als bij M10-start: chore-commit met vlag-flip + re-seed.)
-
Acceptatie-scenario's (zes, deels door unit-tests gedekt):
- Sync happy path: Claude
ask_user_question(wait_seconds=300)→ user antwoordt binnen 30s → MCP-tool retourneert het antwoord ✅ - Async happy path:
ask_user_question(wait_seconds=0)→ tool returnt direct → user antwoordt later → Claudeget_question_answer→ ziet antwoord ✅ - Demo-block: demo-user opent vraag → kan inhoud lezen → "Verstuur" disabled (UI + Server Action ✅)
- Access-isolation: vraag op product zonder access → onzichtbaar in andere user's notifications-bell (SSE-filter ✅)
- Expiry: vraag met
expires_at < now→ na cron-run niet meer in badge-count ✅ - Race: concurrent answer-poging op al-beantwoorde vraag → schone foutmelding (atomic
updateMany count=0✅)
- Sync happy path: Claude
Aandachtspunten
- Acceptatie-scenario's 1-2 zijn handmatig (full Claude+browser cyclus); 3-6 worden in unit-tests vastgelegd
- Pattern-doc moet ook beschrijven wanneer NIET te gebruiken (bv. wanneer een gewone API-call met sessie volstaat)
Verificatie
- Alle docs gepubliceerd in repo
- Backlog-parser-self-test:
npx tsx prisma/seed-data/parse-backlog.tstoont M11 metpriority=4 sprint=ACTIVE - 6/6 acceptatie-scenario's groen
npm run lint && npx tsc --noEmit && npm test && npm run buildcleanvendor/scrum4me-submodule sync in mcp na merge
Branch- en commit-strategie
Per Branch & PR Strategy:
- Eén branch op Scrum4Me:
feat/M11-claude-questionsafgesplitst vanmainná M10-merge - Aparte branch op mcp:
feat/M11-question-tools - Commits chronologisch per stap met ST-code in titel:
chore(M11): swap demo-active sprint from M10 to M11
feat(ST-1101): add ClaudeQuestion model + notify_question_change trigger
feat(ST-1102): add 4 MCP question tools (in mcp)
feat(ST-1103): add answerQuestion server action
feat(ST-1104): add /api/realtime/notifications user-scoped SSE
feat(ST-1104): filter entity='question' from solo-realtime stream
feat(ST-1105): add Zustand notifications-store + realtime hook
feat(ST-1105): add NotificationsBridge in app layout
feat(ST-1105): add NotificationsBell + Sheet + AnswerModal
chore(ST-1107): add CRON_SECRET to env schema
feat(ST-1107): add /api/cron/expire-questions handler
feat(ST-1107): wire vercel.ts cron entry
docs(ST-1108): document notifications SSE + cron in api.md
docs(ST-1108): add vraag-antwoord-kanaal flow to architecture
docs(ST-1108): add claude-question-channel pattern doc
chore(ST-1108): backlog M11 + parser ACTIVE-flip
Push + PR pas na handmatige acceptatie van scenario 1 (sync happy path) + 3 (demo-block) op localhost.
MCP-PR pas mergen ná Scrum4Me-PR + submodule-sync — anders wijzen MCP-tools naar een schema-tabel die op main nog niet bestaat.
Reseed-stap (eenmalig vóór ST-1101-implementatie)
Backlog-markdown moet eerst de M11-stories bevatten en de parser moet M11 als ACTIVE-milestone kennen voordat mcp__scrum4me__get_claude_context ze als next-story kan teruggeven. Workflow:
- Doe ST-1108 backlog-edit + parser-flip eerst (commit
chore(M11): swap demo-active sprint from M10 to M11+ de backlog-uitbreiding) npm run seed— re-seed met M11=ACTIVEmcp__scrum4me__get_claude_contextlevert nu ST-1101 als next-story- Verder met ST-1101-implementatie
Let op: seed wist user-data. Doe dit op een dev-DB.
Buiten scope (volgende milestones)
- AI-suggested antwoorden — Claude leest de codebase en stelt 3 mogelijke antwoorden voor; user kiest. Vereist tweede LLM-call per vraag.
- Mobile-push notifications — bouwt op M10 paired-flow + service-worker. v3.
- Question-templates — "ambiguous-naming"-vraag, "missing-test-case"-vraag etc. voor consistentie.
- Threading — vervolgvraag op een antwoord. v1 is single-shot Q&A.
- File-uploads als antwoord — bv. een screenshot.
- Stats/dashboard — gemiddelde antwoord-tijd, meest-gestelde-vraagsoorten.
- Dismiss-per-user — een member negeert een vraag voor zichzelf zonder 'm te beantwoorden.