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>
This commit is contained in:
Janpeter Visser 2026-04-28 00:33:38 +02:00
parent 74616432d2
commit ebe153d4e3
2 changed files with 534 additions and 0 deletions

View file

@ -27,6 +27,7 @@ De MVP is klaar wanneer Lars — de primaire persona — de volledige cyclus kan
| M8: Realtime Solo Paneel | Live updates voor stories/tasks via SSE + Postgres LISTEN/NOTIFY | ST-801 ST-806 |
| M9: Actief Product Backlog | Persistente actieve PB-keuze, gesplitste navigatie, disabled-states | ST-901 ST-907 |
| M10: Password-loze inlog via QR-pairing | Mobiel als bevestigingskanaal voor desktop-login zonder wachtwoord | ST-1001 ST-1008 |
| M11: Claude vraagt, gebruiker antwoordt | Persistent vraag-antwoord-kanaal tussen Claude (MCP) en de actieve gebruiker | ST-1101 ST-1108 |
---
## Backlog
@ -654,6 +655,73 @@ Volledige flow + threat-model: `docs/patterns/qr-login.md` (op te leveren in ST-
---
### M11: Claude vraagt, gebruiker antwoordt
**Implementatieplan:** [docs/plans/M11-claude-questions.md](plans/M11-claude-questions.md)
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 (uitgebreid met `entity: 'question'`); de Scrum4Me-app toont een notificatie-badge in de NavBar; iedereen met product-toegang kan antwoorden; Claude leest het antwoord (sync via polling met `wait_seconds`, of in een latere sessie via `get_question_answer`) en gaat door. Eerste concrete uitwerking van de AI-driven dev-flow-richting.
**Beveiligingsuitgangspunt:** atomic answer via `updateMany WHERE status='open'` voorkomt double-submit; demo-blok op zowel MCP-write-tools als Server Action; access-check via `productAccessFilter` in DB-query én SSE-filter; cron-endpoint voor expire-cleanup beveiligd met `Authorization: Bearer ${CRON_SECRET}`-header; logging alleen `question_id` (vraag/antwoord-tekst kan gevoelig materiaal bevatten).
- [ ] **ST-1101** `ClaudeQuestion` schema + Postgres-trigger
- **Schema:** `ClaudeQuestion { id, story_id, task_id?, product_id, asked_by, question, options?: Json, status, answer?, answered_by?, answered_at?, created_at, expires_at }`; relations op `User` (`asked_questions`, `answered_questions`), `Story`, `Task`, `Product`; indexes `(story_id, status)`, `(product_id, status)`, `(status, expires_at)`; `product_id` gedenormaliseerd voor SSE-filter
- **Trigger:** `notify_question_change()` `AFTER INSERT/UPDATE`; emit op `scrum4me_changes`-kanaal met payload `{ op, entity: 'question', id, product_id, story_id, task_id, assignee_id, status }`
- **Migratie:** `prisma migrate dev --name add_claude_questions`
- Done when: migratie slaagt; `psql LISTEN scrum4me_changes` toont nieuwe `entity: 'question'`-payload bij INSERT; bestaande solo-realtime-flow ongewijzigd; submodule sync na merge
- [ ] **ST-1102** MCP-tools voor Claude (in scrum4me-mcp-repo)
- **`ask_user_question`** (write): input `{ story_id, question, options?, task_id?, wait_seconds? }`; insert pairing + optioneel pollen tot `wait_seconds` (max 600); demo-blok via `requireWriteAccess`; access-check via `userCanAccessProduct(story.product_id, ...)`
- **`get_question_answer`** (read): haalt status + antwoord op een specifieke vraag op
- **`list_open_questions`** (read): lijst van eigen vragen (laatste 50, status open of answered)
- **`cancel_question`** (write): asker mag eigen vraag annuleren; status pending→cancelled
- Smoke-test in `scripts/smoke-test.ts`: `ask_user_question` met `wait_seconds=5` + parallel answer roundtrip
- Done when: MCP Inspector toont 4 nieuwe tools; smoke-test groen; demo-token op write-tools krijgt PERMISSION_DENIED; `tsc --noEmit` clean
- [ ] **ST-1103** Server Action `answerQuestion`
- `actions/questions.ts`: `answerQuestion(questionId, answer)` met getSession + Zod + demo-blok + `requireProductWriter` via `question.product_id`; atomic `updateMany WHERE status='open' AND expires_at>now`; `revalidatePath('/', 'layout')` voor badge-refresh
- Bij `count === 0`: disambigueer (al-answered/expired/access-fail) met begrijpelijke foutmelding
- Tests: 6 cases (happy, demo-block, geen access, race, expired, lege answer)
- Done when: `npm test` 6/6; handmatig: open vraag → antwoord → badge-count daalt met 1; demo-toast bij submit
- [ ] **ST-1104** User-scoped SSE-route `/api/realtime/notifications`
- Route Handler `runtime: 'nodejs'`, `maxDuration: 300`; auth via iron-session; **user-scoped** (geen product_id-param); filter `payload.entity === 'question'` én `payload.product_id` in user's accessible-product-ids
- Initial-state-event direct na connect (na LISTEN actief, conform M10 ST-1004 race-fix): summary-array van openstaande vragen voor deze user
- Update solo-route in `app/api/realtime/solo/route.ts`: in `shouldEmit` `if (payload.entity === 'question') return false` toevoegen — anders krijgen solo-clients ongewenst question-events
- Tests: 401 zonder cookie, filter op product-access, geen `entity:'question'`-events op solo-route
- Done when: `curl -N` levert events binnen 1s na INSERT; cross-product-test (user-A ziet user-B's vragen niet)
- [ ] **ST-1105** Notifications-UI (Bell + Sheet + Answer-modal + Zustand-store)
- **`stores/notifications-store.ts`** — Zustand store volgens `solo-store.ts`-patroon: `init`, `add`, `update`, `remove`, `optimisticAnswer`, `rollbackAnswer`; selectors `openCount`, `forYouCount`
- **`lib/realtime/use-notifications-realtime.ts`** — analoog aan `useSoloRealtime`; EventSource op `/api/realtime/notifications` met reconnect-backoff
- **`components/notifications/notifications-bridge.tsx`** — Server Component die initial-data fetcht en aan store geeft; mount in `app/(app)/layout.tsx` naast `<SoloRealtimeBridge />`
- **`components/shared/notifications-bell.tsx`** — Bell-icon (Lucide) met badge in NavBar (links van avatar); MD3-tokens uit `docs/scrum4me-styling.md`
- **`components/notifications/notifications-sheet.tsx`** — shadcn Sheet van rechts; lijst gegroepeerd per product; story-assignee krijgt visuele *"wacht op jou"*-emphase
- **`components/notifications/answer-modal.tsx`** — shadcn Dialog; story-context-link, vraag-tekst, RadioGroup (als options) of Textarea (free-text), submit via `useTransition` + Server Action; demo-blok met tooltip
- Done when: bell + badge zichtbaar; klik opent Sheet met items; submit verwijdert item optimistisch; tweede tab van zelfde user ziet nieuwe vraag binnen 1-2s; demo-modus rendert maar Verstuur disabled
- [ ] **ST-1106** Demo-policy + access-tests
- Demo: Sheet rendert + Modal opent + Verstuur disabled met tooltip
- Access-isolation: cross-product test in `__tests__/api/notifications-stream.test.ts` (al gedeeltelijk in ST-1104)
- Story-assignee-emphase: visueel-only, toegang blijft product-membership-breed
- Done when: 4 access-tests groen; handmatige cross-product-verificatie
- [ ] **ST-1107** Vercel cron `expire-questions`
- **`app/api/cron/expire-questions/route.ts`** — POST handler beveiligd via `Authorization: Bearer ${CRON_SECRET}`; `updateMany WHERE status='open' AND expires_at<now → status='expired'`
- **`vercel.ts`** — `crons` entry: `{ path: '/api/cron/expire-questions', schedule: '0 */6 * * *' }`
- **`lib/env.ts`** + `.env.example``CRON_SECRET` via Zod
- Optioneel: ook M10's `login_pairings`-cleanup in dezelfde route opnemen
- Done when: handmatige `curl -X POST` met secret expireert oude rijen; Vercel-dashboard toont cron-config na deploy; onbevoegde call → 401
- [ ] **ST-1108** Documentatie + acceptatietest
- **`docs/API.md`:** secties "SSE — Notifications" + "Cron — Expire questions" met curl-voorbeelden
- **`docs/scrum4me-architecture.md`:** sectie "Vraag-antwoord-kanaal Claude ↔ user" met Mermaid sequence-diagram + threat-model + "Waarom hergebruik scrum4me_changes-kanaal"
- **`docs/patterns/claude-question-channel.md`:** nieuw herbruikbaar pattern-doc voor toekomstige bidirectionele async-communicatie tussen MCP-agents en interactieve users
- **`CLAUDE.md`:** rij in Implementatiepatronen-tabel voor het nieuwe pattern
- **Acceptatietest** zes scenario's: sync happy (wait_seconds), async happy (geen wait), demo-block, access-isolation, expiry via cron, race op double-submit
- Done when: docs gepubliceerd; alle zes scenario's groen; backlog-parser-self-test toont M11 met ACTIVE-status
---
## v2 Backlog (na MVP)
- [ ] Uitnodigingsflow voor teams — e-mailuitnodiging of link-gebaseerd; nu kunnen alleen admins met toegang tot het systeem Developers toevoegen via gebruikersnaam