From e5819ee079f54fc61d531149079e442238f64732 Mon Sep 17 00:00:00 2001 From: Madhura68 Date: Tue, 28 Apr 2026 01:56:03 +0200 Subject: [PATCH] =?UTF-8?q?docs(ST-1108):=20document=20M11=20question-chan?= =?UTF-8?q?nel=20=E2=80=94=20API=20+=20architecture=20+=20pattern?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- CLAUDE.md | 1 + docs/API.md | 67 +++++++++++ docs/patterns/claude-question-channel.md | 144 +++++++++++++++++++++++ docs/scrum4me-architecture.md | 70 +++++++++++ 4 files changed, 282 insertions(+) create mode 100644 docs/patterns/claude-question-channel.md diff --git a/CLAUDE.md b/CLAUDE.md index 3af09fa..a539598 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -99,6 +99,7 @@ Lees het relevante patroon vóór je begint. Nooit uit het hoofd schrijven. | Float sort_order drag-and-drop | `docs/patterns/sort-order.md` | | Middleware (route protection) | `docs/patterns/middleware.md` | | QR-pairing (unauth-SSE + pre-auth cookie) | `docs/patterns/qr-login.md` | +| Bidirectionele async-comms MCP-agent ↔ user | `docs/patterns/claude-question-channel.md` | | Status-enum mapping (DB ↔ API) | `lib/task-status.ts` | | Client/server module-boundary | `*-server.ts` bevat DB-calls of node-only deps; `*.ts` is pure (client-safe). Nooit `import { ... } from '@/lib/foo-server'` in een client-component, anders krijg je `Module not found: 'dns'`/`'pg'`-style runtime fouten | diff --git a/docs/API.md b/docs/API.md index a4dda0f..a1268a9 100644 --- a/docs/API.md +++ b/docs/API.md @@ -417,6 +417,73 @@ curl -i -X POST -b /tmp/jar -c /tmp/jar \ --- +## Notifications — Vraag-antwoord-kanaal (M11) + +Endpoints voor de Claude vraag-antwoord-flow. De **MCP-tools** in de scrum4me-mcp-repo (`ask_user_question`, `get_question_answer`, `list_open_questions`, `cancel_question`) zijn de primaire schrijf-interface; de endpoints hieronder zijn voor de browser-UI en cron. + +### `GET /api/realtime/notifications` + +Server-Sent Events stream voor de notifications-bell in de NavBar. **User-scoped** — geen `product_id`-param; filtert server-side op alle producten waar de gebruiker eigenaar of teamlid is. + +**Auth:** iron-session cookie. Demo-gebruikers mogen lezen. +**Response:** `text/event-stream`. Stream blijft open tot client sluit of server na 240s een hard-close doet (client herconnect). + +**Events:** +- `event: state` — eenmalig direct na connect, met `{ questions: [...] }` als payload (zelfde shape als de live updates). +- `data: {...}` — bij elke status-overgang in `claude_questions`. Payload-shape: + ```json + { + "op": "I" | "U", + "entity": "question", + "id": "cmoh...", + "product_id": "cmoh...", + "story_id": "cmoh...", + "task_id": "cmoh..." | null, + "assignee_id": "cmoh..." | null, + "status": "open" | "answered" | "cancelled" | "expired" + } + ``` + Het is een delta — voor de volledige vraag-tekst en options reconnect de client (initial-state-event levert ze opnieuw). +- `: heartbeat` — SSE-comment elke 25s. + +**Server-side filter:** +- `payload.entity === 'question'` (`task` en `story` events horen op `/api/realtime/solo`) +- `payload.product_id` zit in de set producten met user-access (productAccessFilter) + +**Voorbeeld:** +```js +const source = new EventSource('/api/realtime/notifications', { withCredentials: true }) +``` + +--- + +## Cron — Expire questions + +### `POST /api/cron/expire-questions` + +Vercel cron handler die elke 6 uur draait. Markeert verlopen open vragen als `expired` en verlopen pending login_pairings als `cancelled`. + +**Auth:** `Authorization: Bearer ${CRON_SECRET}` — header die Vercel automatisch injecteert wanneer de env-var op de project-omgeving staat. Zonder secret of bij mismatch: 401. + +**Schedule:** `0 */6 * * *` (4× per dag). + +**Response 200:** +```json +{ + "expired_questions": 0, + "expired_pairings": 0, + "ran_at": "2026-04-28T00:00:00.000Z" +} +``` + +**Voorbeeld (handmatige trigger):** +```bash +curl -X POST -H "Authorization: Bearer $CRON_SECRET" \ + https://your-app.vercel.app/api/cron/expire-questions +``` + +--- + ## Voorbeeldworkflow voor Claude Code 1. **Probe:** `GET /api/health?db=1` — bevestig dat de service en DB bereikbaar zijn. diff --git a/docs/patterns/claude-question-channel.md b/docs/patterns/claude-question-channel.md new file mode 100644 index 0000000..855f2af --- /dev/null +++ b/docs/patterns/claude-question-channel.md @@ -0,0 +1,144 @@ +# Patroon: Bidirectionele async-comms tussen MCP-agent en interactieve user + +Het M11 vraag-antwoord-kanaal is herbruikbaar voor elke feature waarbij een +**autonome agent** (Claude Code via MCP, een Vercel-job, etc.) iets wil +ophelderen bij de **actieve gebruiker** zonder zelf te blokkeren of te raden. + +> "Agent stelt vraag → vraag wacht persistent → user beantwoordt op een +> moment dat het hem uitkomt → agent leest het antwoord en gaat door." + +Voorbeelden waar dit zou kunnen passen: +- AI-codereview: Claude vraagt om bevestiging op een controversiële refactor +- Background-import: queue-worker vraagt om een keuze (overschrijven/skippen) + per ambigue rij +- Multi-step migratie: scripted task pauseert op een handmatige bevestiging +- Approval-flow voor LLM-actions: user keurt een tool-call goed vóór 't echt + gebeurt + +--- + +## Vier eindpunten + +| Endpoint | Auth | Doel | +|---|---|---| +| MCP `ask_*` (write) | API-token + demo-blok | maakt rij in `*_questions`-tabel; optioneel `wait_seconds` voor sync polling | +| MCP `get_*_answer` (read) | API-token | leest huidige status + antwoord (voor latere session-pickup) | +| `POST /actions/answer` (Server Action) | iron-session + product-membership | atomic `updateMany WHERE status='open'` | +| `GET /api/realtime/` (SSE) | iron-session | user-scoped stream, filter op `entity` + product-access | + +Plus: +- **Postgres-trigger** op de tabel die `pg_notify` doet op een gedeeld + channel met een `{op, entity, id, ...}`-payload +- **Cron-endpoint** dat verlopen rijen markeert (status='expired') zodat + achterstallige queries niet blijven groeien + +--- + +## Vier security-uitgangspunten + +1. **Atomic state-transities.** Antwoord-actie doet één UPDATE met alle + invarianten in de WHERE (status, expiry, owner-check). Concurrent dubbele + submit: PostgreSQL row-locking laat één caller count=1 zien, de rest 0. +2. **Demo-blok op writes.** Agent-side via `requireWriteAccess` (PERMISSION_ + DENIED voor demo-tokens), user-side via early-return op `session.isDemo` + en disabled-submit-knop met tooltip in de UI. +3. **Access-isolation in SSE-filter.** Bij connect: query alle accessible + product-IDs voor deze user → Set. In notification-handler: drop alle + payloads waarvan `product_id ∉ accessibleSet`. Dit is naast de DB-query- + filter; redundant maar voorkomt lekkage als de DB-laag iets doorlaat dat + niet zou moeten. +4. **Geen gevoelige data in logs.** Payload bevat alleen IDs en status — de + tekst van vraag en antwoord komt via een aparte authenticated query. + `console.log` alleen `question_id`, nooit content. + +--- + +## Channel-strategie: hergebruik vs. eigen kanaal + +Twee opties bij meerdere realtime-features: + +| Optie | Voordeel | Nadeel | +|---|---|---| +| **Eigen channel per feature** (M10 `scrum4me_pairing`) | Geen filter-leakage tussen features | 1 LISTEN-connectie per feature; meer DB-resources; meer routes | +| **Gedeeld channel met `entity`-key** (M11 `scrum4me_changes`) | 1 LISTEN per route; nieuwe entity = filter-aanpassing | Vergeten te filteren = leak | + +Voor M11 is gekozen voor **hergebruik**: één channel scaalt beter naar v2 +(comments, mentions, status-updates allemaal op zelfde stream) en de filter- +discipline is enforceable in code-review. Mitigatie voor leak-risico: expliciet +`if (payload.entity === 'X') return false` in elke route die feature X niet +hoort te zien — zoals `app/api/realtime/solo/route.ts` die `entity:'question'` +weert. + +--- + +## TTL-richtlijn + +- **Question lifetime**: 24 u — kort genoeg dat verlaten queries niet + ophopen, lang genoeg dat een gebruiker die afwezig is een nacht heeft om + te antwoorden +- **MCP-tool wait_seconds**: max 600 s — Claude wacht maximaal 10 min op een + antwoord; daarna `status: 'pending'` zodat hij later kan terugkomen via + `get_question_answer` +- **Cron schedule**: `0 */6 * * *` — 4× per dag genoeg om 'expired' op te + ruimen zonder Vercel-cron-budget te belasten + +--- + +## Sjabloon-bestanden + +Specifiek voor M11. Kopieer en pas aan: + +### Database +- `prisma/schema.prisma`: model met `id`, `status`, `expires_at`, denormalized + `product_id` voor SSE-filter, asker/answerer-FKs, json `options?`-veld + voor multiple-choice +- `prisma/migrations//migration.sql`: tabel-DDL + `notify_*_change()`- + functie + `AFTER INSERT/UPDATE`-trigger op gedeeld channel + +### Server (Scrum4Me) +- `actions/questions.ts`: Server Action met getSession + Zod + demo-blok + + productAccessFilter + atomic updateMany +- `app/api/realtime/notifications/route.ts`: user-scoped SSE met + initial-state-event ná LISTEN actief (race-fix conform M10 ST-1004) +- `app/api/cron/expire-questions/route.ts`: Bearer-auth via CRON_SECRET + + updateMany WHERE expires_at>DB: INSERT claude_questions (status=open) + DB->>SC: pg_notify {entity:'question', op:'I', id, ...} + SC->>SSE: notification (filter: question + product-access) + SSE->>U: data event → Zustand store upsert → bell badge + + Note over U: Gebruiker klikt bell → Sheet → Modal + U->>DB: answerQuestion(questionId, answer)
Server Action: atomic updateMany WHERE status='open' + DB->>SC: pg_notify {entity:'question', op:'U', status:'answered'} + SC->>SSE: notification + SSE->>U: data event → store remove → bell badge -1 + + Note over C: Optioneel: ask_user_question(wait_seconds) polt elke 2s + C->>DB: SELECT status FROM claude_questions WHERE id=... + DB-->>C: status='answered', answer='...' + C->>C: gaat door met implementatie +``` + +### Threat-model + +| Aanval | Mitigatie | +|---|---| +| **Race**: dubbele submit op zelfde vraag | Atomic `updateMany WHERE status='open'` — één caller ziet count=1, rest count=0 met disambiguatie via second findFirst | +| **Demo-account misbruik** | `requireWriteAccess` op MCP-write-tools (PERMISSION_DENIED), early-return op `session.isDemo` in answerQuestion Server Action, disabled submit + tooltip in AnswerModal | +| **Cross-product leak** | `productAccessFilter` op DB-query én SSE-server-side-filter (Set met user's accessible product-IDs) | +| **Cron-endpoint misbruik** | `Authorization: Bearer ${CRON_SECRET}` — Vercel injecteert automatisch; faalt 401 als secret niet gezet (geen open endpoint in dev) | +| **Onbeperkte vragen-groei** | `expires_at` 24 u + Vercel cron `0 */6 * * *` markeert `status='expired'` → uit notifications-bell | +| **Gevoelige info in logs** | Logging alleen `question_id`, nooit vraag- of antwoord-tekst | + +### Waarom hergebruik scrum4me_changes-kanaal + +In tegenstelling tot M10 (eigen `scrum4me_pairing`-kanaal) is M11 een uitbreiding van +de bestaande realtime-infra. Voordelen: + +- Eén Postgres-NOTIFY-listener per route i.p.v. twee — minder DB-connecties +- Solo-realtime + notifications kunnen onafhankelijk evolueren via de `entity`-key +- Toekomstige entities (bijv. `entity: 'comment'`, `entity: 'mention'`) hoeven geen + nieuw kanaal — alleen een filter-aanpassing in de route die ze wil ontvangen + +Risico: een nieuwe entity vergeten te filteren leidt tot lekkage. Mitigatie: +expliciet `if (payload.entity === 'X') return false` in elke SSE-route die +betrokken-features niet hoort te zien (zoals de solo-route die `entity:'question'` +weert). + +Dit patroon (notification-channel via een bestaande pg_notify-stream) is +herbruikbaar — zie `docs/patterns/claude-question-channel.md`. + +--- + ## Projectstructuur ```