* ST-cmovs79lt: Schema + migratie PushSubscription model
Voeg PushSubscription model toe aan prisma/schema.prisma met
snake_case-conventie, relation field op User, en bijbehorende
migratie (push_subscriptions tabel, FK + index op user_id).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* ST-cmovs7e3o: web-push dependency + VAPID env vars feature-gated
Voeg web-push + @types/web-push toe aan package.json.
Registreer NEXT_PUBLIC_VAPID_PUBLIC_KEY, VAPID_PRIVATE_KEY,
VAPID_SUBJECT en INTERNAL_PUSH_SECRET als .optional() in lib/env.ts.
Documenteer alle vier in .env.example en README.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* ST-cmovs7jgr: lib/push-server.ts met sendPushToUser + stale-cleanup
Server-only push-lib met VAPID feature-gate, send naar alle
subscriptions van een user, en automatische cleanup bij 404/410.
Unit tests: success-pad, 410 verwijdert sub, 404 verwijdert sub,
andere errors loggen zonder delete.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* ST-cmovs7ouz: lib/push-client.ts client-side push helpers + stub actions/push.ts
Client-side helpers: isPushSupported, isIOSSafari, isStandalonePWA,
urlBase64ToUint8Array, subscribeToPush, unsubscribeFromPush.
Stub actions/push.ts zodat imports resolven (implementatie volgt
in volgende taak). Unit tests voor urlBase64ToUint8Array.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* ST-cmovs7ut4: actions/push.ts subscribeToPushAction + unsubscribeFromPushAction
Vervangt stub met volledige implementatie: requireUser via getSession,
demo-block, Zod-validatie, upsert met user_id-scoping en user-scoped
deleteMany. Tests (8): idempotentie, demo-block, unauthenticated, invalid input.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* ST-cmovs80c1: POST /api/internal/push/send met constant-time Bearer check
Route: 503 als INTERNAL_PUSH_SECRET uitstaat, 401 bij verkeerd secret
(timingSafeEqual), 400 bij invalid JSON, 422 bij Zod-fout, 204 bij succes.
push-server.ts: env-import vervangen door process.env om SESSION_SECRET
validatie tijdens build te omzeilen. Tests aangepast.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* ST-cmovs862j: Admin test-send route + public/sw.js service worker
POST /api/internal/push/test-send: requireAdmin check (redirect bij
niet-admin), optioneel body met defaults, roept sendPushToUser aan, 204.
public/sw.js: push-handler met showNotification, notificationclick met
same-origin guard, focus bestaand venster of openWindow.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* ST-cmovs8jvq: PushToggle component met 3 states + iOS-banner
Client component met states loading/unsupported/ios-needs-install/
denied/subscribed/unsubscribed. useEffect detecteert initial status,
permission-prompt alleen via user-click. iOS-banner NL, denied-uitleg,
subscribe/unsubscribe knoppen met sonner-toasts.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* ST-cmovs8psg: notifications-sheet + iOS meta-tags in layout
notifications-sheet.tsx: PushToggle onderin met sectie
'Notificatie-instellingen' en visuele scheidslijn.
app/layout.tsx: appleWebApp.capable, statusBarStyle en
mobile-web-app-capable meta-tags toegevoegd via Next.js Metadata API.
manifest.json had al display: standalone.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* ST-cmovs8vxj: docs/patterns/web-push.md pattern-documentatie
Architectuur-diagram, payload-shape, foutcodes, VAPID-config,
iOS-quirks, demo-users blokkade, trigger-voorbeelden (server +
HTTP) en admin-testroute curl-voorbeeld.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
§ 4a beschrijft hybrid detail+inline-edit dialogen met dynamische
footer en blur-save: bv. TaskDetailDialog. Maakt expliciet wanneer je
deze variant kiest, welke § 4-eisen blijven gelden en welke vervallen
(geen dirty-guard, geen Cmd+Enter, geen full-record schema).
Profiel docs/specs/dialogs/task-detail.md verwijst nu naar § 4a en
documenteert de layout-keuzes (sticky header, scrollable body,
flex-wrap footer met job-status, plan-textarea max-h-[40vh]).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 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>
* docs(naming): drop scrum4me- prefix from doc filenames
Rename 10 docs/scrum4me-*.md files to unprefixed kebab-case names.
Update every internal link in docs/, CLAUDE.md, AGENTS.md, README.md.
* docs(naming): lowercase API.md and MD3 filenames
Rename docs/API.md → docs/api.md and
docs/MD3_Color_Scheme_Documentation.md → docs/md3-color-scheme.md.
Update all internal links across 7 files.
* docs(naming): rename plan file to kebab-case ASCII
Rename "docs/plans/Tweede Claude Agent — Planning Agent.md"
→ docs/plans/tweede-claude-agent-planning.md. No external links needed updating.
* docs(naming): rename middleware.md to proxy.md (next 16)
docs/patterns/middleware.md → docs/patterns/proxy.md following
the Next.js 16 proxy.ts rename. Update link in CLAUDE.md.
* docs(naming): polish CLAUDE.md doc-index after renames
Fix doubled scrum4me-scrum4me-mcp repo references (cascade from
prior sed) in CLAUDE.md, docs/architecture.md, backlog.md,
agent-instruction-audit.md, and plans/ST-1109. Update
'Middleware' label to 'Proxy middleware' in patterns table.
Documents the mandatory 3-task pattern (Helper / Component / Integration)
for stories introducing UI components. Cites the 2026-05-02 Velocity story
as the anti-pattern and the Foundation Sprint Health story as the blueprint.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* 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
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 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>
* chore(M11): swap demo-active sprint from M10 to M11
M10 is gemerged en afgesloten — M11 wordt de nieuwe demo-actieve milestone
zodat get_claude_context (via MCP) ST-1101 als next-story teruggeeft.
Drie maps in parse-backlog.ts uitgebreid: M11 priority=4, goal omschrijving,
sprint_status='ACTIVE'. M10 → COMPLETED.
Vereist npx prisma db seed na deze commit zodat de live DB de nieuwe
sprint-state weerspiegelt.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(ST-1108): add F-11b — Claude question-channel to functional spec
Voegt feature-omschrijving toe naast bestaande F-11 (Claude Code REST API).
Beschrijft het verloop (Claude → MCP-tool → DB → trigger → SSE → user → answer
→ trigger → Claude polls), acceptatiecriteria (8 items), randgevallen (offline-
Claude, assignee-change, expiry, abuse) en datamodel (claude_questions tabel).
Persona Lars als primair, Dina secundair voor klant-werk.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(M11): drop parser ACTIVE-flip; sprint goes via UI from now on
Bij M9/M10 hebben we de seed-flip (MILESTONE_SPRINT_STATUS pivot) gebruikt om
nieuwe stories als IN_SPRINT in een verse sprint te krijgen. Dat werkt maar
is fragiel:
- npm run seed wist user-data
- de "sprint" die de seed maakt is geen echte planning-actie
- bij multi-product scenario's breekt het model
Vanaf M11 gebruiken we de bestaande Sprint-creatie-UI van Scrum4Me. Stories
voor M11 worden via scripts/insert-milestone.ts (idempotent insert, geen
seed-reset) aan de DB toegevoegd; de gebruiker maakt zelf een Sprint aan in
/products/[scrum4me]/sprint en sleept ST-1101..1108 ernaartoe.
Parser-map M11 dus terug naar COMPLETED zodat een eventuele re-seed niet meer
een fake sprint aanmaakt voor M11-stories.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(ST-1101): add ClaudeQuestion model + notify_question_change trigger
Schema (prisma/schema.prisma):
- Nieuw model ClaudeQuestion: id (cuid), story_id (FK Cascade), task_id?
(FK SetNull), product_id (FK Cascade — gedenormaliseerd uit story.product_id
voor SSE-filter zonder join), asked_by (FK Restrict — Claude-token-houder),
question (Text), options (Json? — string[] voor multi-choice), status
('open'|'answered'|'cancelled'|'expired'), answer (Text?), answered_by
(FK SetNull), answered_at?, created_at, expires_at
- Indexes: (story_id, status), (product_id, status), (status, expires_at)
- Back-relations: User.asked_questions (ClaudeQuestionAsker),
User.answered_questions (ClaudeQuestionAnswerer), Story.claude_questions,
Task.claude_questions, Product.claude_questions
Migratie (20260427224849_add_claude_questions):
- Prisma-gegenereerde DDL voor claude_questions + indexes + 5 FK's
- Toegevoegde notify_question_change() functie + claude_questions_notify trigger
op AFTER INSERT/UPDATE
- Emit op BESTAANDE scrum4me_changes-channel met entity:'question' (i.t.t. M10
dat eigen scrum4me_pairing-channel kreeg) — solo-route in ST-1104 moet
entity='question' wegfilteren om regressie op solo-board te voorkomen
- Trigger leest story.assignee_id voor "wacht op jou"-emphase in payload
- DELETE niet ondersteund — questions gaan naar answered/cancelled/expired
Verification: Node pg-client roundtrip via DATABASE_URL toonde correcte payloads
bij INSERT (op=I, status=open) en UPDATE (op=U, status=answered) met alle FK-IDs
en assignee_id correct uit story-join.
Volgende stap M11: ST-1102 — vier MCP-tools in scrum4me-mcp-repo
(ask_user_question, get_question_answer, list_open_questions, cancel_question).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(ST-1103): add answerQuestion server action
actions/questions.ts:
- answerQuestion(questionId, answer) — auth + Zod + demo-blok + access-check
via productAccessFilter (anyone met product-membership mag antwoorden,
consistent met Scrum self-organizing — niet alleen story-assignee)
- Atomic prisma.claudeQuestion.updateMany WHERE id + status='open' +
expires_at>now → status='answered'; concurrent dubbele submit: één wint
(count=1), rest count=0 met disambiguatie via second findFirst
- revalidatePath('/', 'layout') refresh't NavBar bell-count voor SSR-paths;
realtime updates voor andere clients gaan via SSE in ST-1104/1105
- Begrijpelijke NL-foutmeldingen voor elk faalpad
Tests __tests__/actions/questions.test.ts (6 cases):
- happy: status update + revalidatePath called
- demo-block: error + geen DB-call + geen revalidate
- geen access: error + geen update
- al-answered: race-error 'is al answered'
- expired: race-error 'is verlopen'
- lege answer: Zod-validatie
Quality gates: lint 0 errors, tsc clean, vitest 145/145 (17 files).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(ST-1104): add user-scoped /api/realtime/notifications + filter solo-route
Twee delen:
1. Solo-route filter (1-regel-fix in app/api/realtime/solo/route.ts):
- NotifyPayload uitgebreid met entity:'question'
- shouldEmit returnt direct false bij entity='question'
Voorkomt dat solo-clients M11 question-events ontvangen (geen lekkage naar
het Solo-bord; geen onnodig netwerk-verkeer; loose coupling tussen features).
2. Nieuwe SSE-route app/api/realtime/notifications/route.ts:
- User-scoped (geen ?product_id=); query alle accessible product-IDs één keer
bij connect via productAccessFilter
- LISTEN scrum4me_changes; filter entity='question' && product_id ∈ accessible
- Initial-state-event NA LISTEN actief (race-fix conform M10 ST-1004):
query open vragen voor deze user's accessible products, stuur als event:state
met summary (id, story_code/title, assignee_id, question, options, expires_at)
- Hergebruikt het pg.Client + ReadableStream + heartbeat 25s + hard-close 240s +
abort-cleanup-pattern uit solo-route
Tests __tests__/api/notifications-stream.test.ts:
- 401 zonder iron-session cookie (en geen DB-call)
- Solo-route filter wordt visueel/E2E gedekt in ST-1108-acceptatie
Quality gates: lint 0 errors, tsc clean, vitest 146/146 (18 files).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(ST-1105): add NavBar bell + sheet + answer-modal + Zustand store + SSE hook
UI-volledig voor de Claude vraag-antwoord-flow (M11). Bel-icon links van avatar
in NavBar; klik opent slide-over rechts met openstaande vragen; klik op een vraag
opent een modal voor antwoord. Story-assignee = current user krijgt visuele
"voor jou"-emphase met primary-container accent en error-color badge-ring.
Bestanden:
- stores/notifications-store.ts — Zustand store met init/upsert/remove +
openCount/forYouCount selectors (vereenvoudigd vs solo-store: geen pendingOps,
geen optimistic-echo-onderdrukking)
- lib/realtime/use-notifications-realtime.ts — EventSource hook met state-
event en message-event handling, exponential-backoff reconnect, Page
Visibility pause-resume
- components/notifications/notifications-bridge.tsx — Server Component die
initial open-questions fetcht via productAccessFilter
- components/notifications/notifications-realtime-mount.tsx — tiny client
island dat de store hydrateert + de hook activeert
- components/notifications/notifications-sheet.tsx — shadcn Sheet met item-
lijst, "voor jou"-accent voor assignee-vragen, lege staat
- components/notifications/answer-modal.tsx — Dialog met options-radio of
free-text Textarea (max 4000), char-counter, demo-blok via Tooltip; bij
succes optimistisch remove + sheet blijft open zodat meerdere vragen
achter elkaar te beantwoorden zijn
- components/shared/notifications-bell.tsx — Bell-icon met badge (count >9 → "9+"),
ring-accent als forYouCount > 0, ARIA-label voor screenreaders
Wiring:
- components/shared/nav-bar.tsx — <NotificationsBell /> rechts naast <UserMenu>
- app/(app)/layout.tsx — <NotificationsBridge /> naast <SoloRealtimeBridge />,
user.id (server-side) als prop
base-ui-aanpassingen: SheetTrigger/TooltipTrigger gebruiken render-prop ipv
asChild (geen Radix).
Quality gates: lint 0 errors, tsc clean, vitest 146/146, npm run build groen.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(ST-1106): add cross-product access-isolation test for notifications SSE
Demo-policy + assignee-emphase zaten al in eerdere stories:
- answerQuestion demo-blok in actions/questions.test.ts (ST-1103)
- AnswerModal demo-tooltip in components/notifications/answer-modal.tsx (ST-1105)
- requireWriteAccess in MCP write-tools (ST-1102)
Deze story voegt expliciet een access-isolation-test toe op de notifications-
SSE-route: productAccessFilter wordt met de echte userId aangeroepen, en
prisma.product.findMany filter't op archived=false + user_id-scope. Dat
garandeert dat een gebruiker geen question-events ontvangt voor producten waar
hij geen membership op heeft.
Story-assignee-emphase blijft visueel-only (NotificationsBell ring-accent +
Sheet primary-container) — toegang werkt product-membership-breed zodat een
team-lid kan invallen als de assignee niet beschikbaar is.
Quality gates: lint 0 errors, tsc clean, vitest 147/147 (was 146).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(ST-1107): add Vercel cron expire-questions (+ M10 pairing cleanup)
POST /api/cron/expire-questions:
- Auth via Authorization: Bearer ${CRON_SECRET} (Vercel injecteert dit
automatisch wanneer de env-var op de project-omgeving staat); 401 als secret
niet matcht of niet is gezet (faal-veilig — geen open endpoint in dev)
- updateMany op claude_questions WHERE status='open' AND expires_at<now →
'expired'
- Bonus: zelfde route ruimt M10 login_pairings op (status='pending' AND
expires_at<now → 'cancelled'). Eén cron-job is goedkoper qua Vercel-budget
en houdt cleanup-strategie centraal — opvolg-actie uit M10 dat geparkeerd was.
Config:
- vercel.json: crons-entry { path: '/api/cron/expire-questions', schedule: '0 */6 * * *' } (4x/dag)
- lib/env.ts: CRON_SECRET als optional in Zod-schema
- .env.example: documentatie + openssl rand-tip
Tests __tests__/api/cron-expire-questions.test.ts (4 cases):
- 401 zonder Authorization-header
- 401 met verkeerde secret
- 401 als CRON_SECRET niet is gezet (faal-veilig)
- 200 met juiste secret: response { expired_questions, expired_pairings, ran_at }
+ beide updateMany WHERE/data correct
Quality gates: lint 0 errors, tsc clean, vitest 151/151.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(ST-1108): document M11 question-channel — API + architecture + pattern
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) <noreply@anthropic.com>
* fix(ST-1107): cron schedule daily — Vercel Hobby allows only 1 run/day
Vercel deploy faalde met:
> Hobby accounts are limited to daily cron jobs.
> This cron expression (0 */6 * * *) would run more than once per day.
Schedule van 4×/dag (0 */6 * * *) naar 1×/dag (0 4 * * * — 04:00 UTC, rustig
tijdstip). Functioneel acceptabel: ClaudeQuestion TTL is 24u, dus daily
cleanup pakt alles dat in de afgelopen 24u verlopen is. Login-pairings TTL
is 2 min — die zijn al onbruikbaar zodra ze expiren, cron is alleen voor
status-housekeeping.
Schedule-referenties consistent bijgewerkt in docs (API.md, architecture,
backlog M11-sectie, plan-doc, pattern-doc) + comment in route.ts. Vermelding
overal dat dit een Hobby-plan-beperking is en Pro fijnmaziger ondersteunt.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(M10): swap demo-active sprint from M3.5 to M10
M3.5 was de demo-actieve sprint zolang er geen recentere milestone in progress
was. Nu M10 het actieve werk is, willen we dat get_claude_context (en
implement_next_story) ST-1001 als next-story teruggeven i.p.v. ST-350.
Vereist een herhaling van npx prisma db seed na deze commit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(M10): gate Solo demo-stories on M3.5 active milestone
De hardcoded Solo Paneel-demoset uit M3.5 (priority=2) schreeuwt over de
parser-driven M10-stories heen (priority=4) en laat get_claude_context op
"Gebruikersauthenticatie opzetten" wijzen i.p.v. ST-1001.
Sluit het blok nu alleen open als de actieve sprint van het Scrum4Me-product
M3.5 betreft. Voor M10+ leveren de parser-stories zelf de bord-content; de
demo-set blijft beschikbaar als M3.5 ooit weer ACTIVE wordt voor demo-doeleinden.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(M10): drop hardcoded Solo Paneel demo data from seed
DB wordt voortaan leidend voor de werkstaat; testdata voor andere projecten /
demo-scenario's komt elders. Deze hardgecodeerde set was specifiek gemaakt voor
de M3.5 Solo Paneel-demo en raakt nu het next_story-resultaat: priority=2 won
van de M10 parser-stories (priority=4) waardoor get_claude_context op
'Gebruikersauthenticatie opzetten' bleef hangen i.p.v. ST-1001.
Vervangt de eerdere M3.5-gating-aanpak (commit 0e3228d) — schoner om het
helemaal weg te halen dan met een conditional aanwezig te houden.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(M10): add npm run seed shortcut
Wrapt prisma db seed (die de bestaande prisma.seed-config in package.json gebruikt)
zodat re-seeden één korte invocatie wordt zonder de prisma-CLI-syntaxis te onthouden.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(ST-1001): add LoginPairing model + pg_notify trigger via migration
Schema (prisma/schema.prisma):
- model LoginPairing met id (cuid), secret_hash + desktop_token_hash (beide NOT
NULL — scheiden mobiel- en desktop-bewijs), status (pending|approved|consumed
|cancelled), optionele user_id met onDelete: SetNull, desktop_ua VarChar(255),
desktop_ip VarChar(45) voor IPv6, created_at + expires_at + approved_at +
consumed_at, indexes op (expires_at) en (status, expires_at)
- back-relation login_pairings LoginPairing[] op User
Migratie (20260427200734_add_login_pairing):
- Prisma-gegenereerde DDL voor login_pairings + indexes + FK
- Toegevoegde notify_pairing_change() functie + login_pairings_notify trigger
op AFTER INSERT/UPDATE; emit pg_notify('scrum4me_pairing', payload) met
{ op: 'I'|'U', pairing_id, status }
- DELETE niet ondersteund — pairings gaan naar consumed/cancelled, niet weg
- Channel naam analoog aan scrum4me_changes uit ST-801
Verification: Node pg-client roundtrip-test via DATABASE_URL toonde notifies bij
INSERT (op=I) en UPDATE (op=U) met correcte payload-shape.
Bouwt voort op M8 LISTEN/NOTIFY-infra. SSE-route /api/auth/pair/stream/[id] in
ST-1004 abonneert hierop.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(ST-1002): add pairing helpers, pre-auth cookie + paired-session guard
lib/auth/pairing.ts: pure crypto-helpers voor de QR-pairing flow.
- generateMobileSecret() / generateDesktopToken() — beide 32 bytes base64url, los
zodat ze elkaar niet onthullen
- hashToken(t) — sha256-hex
- verifyToken(t, hash) — timingSafeEqual met length-guard
- isPairedSessionExpired(session) — geëxtraheerde helper zodat de Server-
Component-render Date.now() niet rechtstreeks aanroept (React Compiler-flag)
lib/auth/pair-cookie.ts: HttpOnly pre-auth cookie helpers (s4m_pair).
- Path=/api/auth/pair, Max-Age=120s (gelijk aan pending-TTL pairing),
SameSite=Lax, Secure in productie
lib/session.ts: SessionData uitgebreid met optionele paired + pairedExpiresAt.
app/(app)/layout.tsx: guard die paired-sessies vernietigt zodra
pairedExpiresAt verstreken is en redirect naar /login.
Tests: 14 unit-tests in __tests__/lib/auth/pairing.test.ts dekken hash-
determinisme, timing-safe verify (true/false/length-mismatch), generator-
uniciteit en vier expiry-scenario's voor isPairedSessionExpired.
Quality gates: npm run lint (0 errors), tsc --noEmit clean, vitest 111/111.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(ST-1003): add /api/auth/pair/start with rate-limit + pre-auth cookie
POST /api/auth/pair/start (anon, runtime: 'nodejs'):
- Geen authenticateApiRequest — desktop heeft nog geen sessie
- Genereert los mobileSecret + desktopToken via lib/auth/pairing
- Persisteert alleen sha256-hashes in login_pairings; status='pending', expires_at = now + 2 min
- Slaat user-agent + best-effort IP op (afgekapt op kolom-grootte)
- Set-Cookie via setPairCookie helper: HttpOnly, Path=/api/auth/pair, Max-Age=120, SameSite=Lax
- Response body: { pairingId, mobileSecret, expiresAt, qrUrl } met qrUrl = origin/m/pair#id=…&s=…
→ secret reist alleen via fragment (#…), nooit in querystring of access logs
Rate-limit: 'pair-start' expliciet aan lib/rate-limit.ts CONFIGS toegevoegd
voor self-documentatie (10/min, gelijk aan login).
Tests __tests__/api/pair-start.test.ts (6 cases):
- 200 met body-shape (pairingId, mobileSecret 43-char base64url, qrUrl met
fragment, expiresAt ISO)
- alleen hashes in DB, geen plaintext
- cookie set met juiste opties
- UA + IP afgekapt op kolom-grootte
- IP=null als x-forwarded-for ontbreekt
- 11e POST levert 429 met NL foutmelding
Quality gates: lint 0 errors, tsc clean (na prisma generate), vitest 117/117.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(ST-1004): add SSE /api/auth/pair/stream with cookie auth
GET /api/auth/pair/stream/[pairingId]:
- runtime: 'nodejs', maxDuration: 300, dynamic: 'force-dynamic'
- Auth via s4m_pair HttpOnly cookie (readPairCookie + verifyToken tegen
desktop_token_hash); 401 zonder cookie of bij hash-mismatch, 404 als pairing
onbekend, 410 als verlopen — geen geheim materiaal in URL of querystring
- Hergebruikt LISTEN/NOTIFY-pattern uit app/api/realtime/solo/route.ts:
ReadableStream + dedicated pg.Client + heartbeat 25s + hard-close 240s
- Channel: scrum4me_pairing; filter notifies op pairing_id-match
- Initial 'state'-event direct na connect met huidige status (voorkomt race
waarbij approve net vóór SSE-open landt — desktop ziet 'm alsnog)
- Auto-close zodra status consumed/cancelled binnenkomt
- Fallback DIRECT_URL → DATABASE_URL (de eerste staat lokaal op een placeholder)
Tests __tests__/api/pair-stream.test.ts (4 cases — auth-paden):
- 401 zonder cookie (en geen DB-call gedaan)
- 404 op onbekende pairingId
- 410 op verlopen pairing
- 401 op cookie/hash-mismatch
Full-stream-test (LISTEN+notify-roundtrip) is een handmatige acceptatietest in
ST-1008 — niet zinvol te mocken voor v1.
Quality gates: lint 0 errors, tsc clean, vitest 121/121.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(ST-1005): add pairing server actions + mobile confirmation page
actions/pairing.ts (Server Actions, volgt docs/patterns/server-action.md):
- getPairingForApproval(pairingId, mobileSecret): auth + Zod + lookup + status
+ expiry + verifyToken-check; retourneert UA/IP/username voor de
bevestigingspagina. Demo MAG aanroepen (read-only).
- approvePairing: zelfde checks PLUS demo-blokkade (session.isDemo). Update
status pending→approved, zet user_id + approved_at, bumpt expires_at +5min.
Postgres-trigger emit pg_notify automatisch — desktop-SSE pikt het op.
- cancelPairing: status pending→cancelled. Demo mag annuleren.
- Tagged-union return-type uit loadPendingPairing voor schone discriminatie.
app/(app)/m/pair/page.tsx (Server Component, achter (app)/layout-guard):
- Geen searchParams uitlezen — page leest URL niet. Alleen statische uitleg +
PairConfirmation client-island.
app/(app)/m/pair/pair-confirmation.tsx (Client Component):
- useEffect parseert window.location.hash voor #id=…&s=… (server ziet de
fragment nooit)
- Roept getPairingForApproval om UA/IP/username op te halen
- Toont kaart "Inloggen als <username> op dit apparaat?" met UA + IP +
expliciete waarschuwing tegen phishing-QR; Bevestig/Annuleer-knoppen
- Na approve: window.history.replaceState wist de hash zodat back/forward de
secret niet meer onthult; transitioneert naar success-state
- queueMicrotask voor synchrone setState om React-Compiler "cascading renders"
warning te vermijden
Tests __tests__/actions/pairing.test.ts (11 cases):
- getPairingForApproval: ok + 5 fail-paths (geen sessie, approved, verlopen,
verkeerd secret, ongeldige cuid)
- approvePairing: happy + demo-block + verkeerd secret (geen DB-write)
- cancelPairing: happy + demo mag annuleren
Quality gates: lint 0 errors, tsc clean, vitest 132/132.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(ST-1006): add /api/auth/pair/claim with atomic consume + iron-session
POST /api/auth/pair/claim (cookie-auth, runtime: 'nodejs'):
- Auth via s4m_pair HttpOnly cookie alleen — body bevat enkel pairingId, geen
secret. Het cookie-token is het bewijs.
- Atomic state-transitie via prisma.loginPairing.updateMany met composite
WHERE (id + status='approved' + desktop_token_hash + expires_at > now);
PostgreSQL row-locking garandeert dat concurrent dubbele claims slechts één
count=1 zien — de rest 410.
- Bij geen rij geüpdate: tweede findFirst om te disambigueren tussen 401
(cookie matcht geen pairing) en 410 (al consumed/cancelled). Cookie altijd
gecleared bij faalpaden om herhaalde verwerking te voorkomen.
- Bij succes: getIronSession schrijft scrum4me-session-cookie met userId +
isDemo (uit user-record als vangnet) + paired=true + pairedExpiresAt = now+8h
(kortere TTL voor publieke desktops). s4m_pair wordt gecleared.
- Logging onder NODE_ENV !== 'production' alleen pairingId, nooit cookie of
mobileSecret.
Tests __tests__/api/pair-claim.test.ts (7 cases):
- 200 happy: updateMany met juiste WHERE, iron-session payload (userId, isDemo,
paired, pairedExpiresAt ~8h), save() called, s4m_pair cleared
- demo-vangnet: isDemo=true wordt doorgezet
- 401 zonder cookie (geen DB-call)
- 400 op malformed body
- 400 zonder pairingId
- 410 op tweede claim (al consumed, cookie cleared, geen session.save)
- 401 op cookie/hash-mismatch (cookie cleared)
Quality gates: lint 0 errors, tsc clean, vitest 139/139.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(M10): bump pending-TTL to 5min + repair MD3 contrast on pair page
TTL: 2 min was te kort voor handmatig curl-paste-confirm-testen — gebruiker
zag 'Pairing verlopen' voor hij kon bevestigen. Bumpt naar 5 min (gelijk aan
approved-TTL): nog steeds tight voor security, ruim voor menselijke reactie.
- app/api/auth/pair/start/route.ts: PENDING_TTL_MS 120s → 300s
- lib/auth/pair-cookie.ts: MAX_AGE_SECONDS 120 → 300
- __tests__/api/pair-start.test.ts: maxAge en expires_at-window meegegroeid
Kleuren: bevestigingspagina gebruikte bg-destructive/10 + text-destructive-
foreground — beide lichte kleuren, te weinig contrast. Vervangen door MD3
container-tokens (zelfde patroon als components/auth/auth-form.tsx):
- error-state: bg-error-container + text-error-container-foreground + border-l-4 border-error
- approved-state: bg-success-container + foreground + accent-border
- cancelled-state: bg-surface-container-high + neutral foreground
Quality gates: lint 0 errors, tsc clean, vitest 139/139.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(ST-1007): add QR login button on /login with SSE listener
Voltooit de desktop-zijde van de QR-pairing-flow. Gebruiker klikt "Inloggen
via mobiel" naast het wachtwoord-formulier → krijgt een QR-code → telefoon
scant en bevestigt → desktop wordt automatisch ingelogd zonder dat er ooit
een wachtwoord is getypt op het publieke apparaat.
app/(auth)/login/qr-login-button.tsx (Client Component):
- Phase-state: idle | starting | showing | expired | claiming
- klik → POST /api/auth/pair/start (credentials:'same-origin' voor s4m_pair)
- QRCodeSVG met fragment-URL als value (level=M, 200px); aria-label
- EventSource('/api/auth/pair/stream/<id>', { withCredentials: true })
vereist voor cookie-auth — standaard verstuurt EventSource geen credentials
- bij data.status === 'approved': es.close → POST /pair/claim → router.push('/dashboard')
- aftellende timer (mm:ss); bij 0s → 'expired' state met Vernieuwen-knop
- cleanup bij unmount: removeEventListener + close
- A11y: <details> sectie toont fragment-URL als kopieerbare tekst voor screenreaders en gebruikers zonder camera
app/(auth)/login/page.tsx: QrLoginButton onder het bestaande wachtwoord-form
met "of"-divider, achter de bestaande surface-container-low styling.
Dependency: qrcode.react ^4.2.0 (client-side SVG; geen extra round-trip;
mobileSecret blijft op desktop in JS-geheugen).
Quality gates: lint 0 errors, tsc clean, vitest 139/139, next build slaagt
(login-route static, m/pair en pair/* dynamic).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(ST-1008): document QR-pairing endpoints, flow, threat-model + pattern
docs/API.md — nieuwe sectie 'Auth — QR-pairing (M10)' met alle drie endpoints
(start, stream, claim), cookie-mechaniek, foutcodes (400/401/410/429),
curl-voorbeelden inclusief --cookie-jar.
docs/scrum4me-architecture.md — sectie 'QR-pairing flow' met:
- Mermaid sequence-diagram (start → QR → scan → approve → claim)
- Threat-model (replay, phishing-QR, demo-block, rate-limit, secret-leak,
long-lived sessie) met expliciete mitigaties
- TTL-rationale voor de drie tijden (5min pending / +5min approved / 8u paired)
- Subsectie 'Waarom geen secret in URL' — fragment-eigenschap + HttpOnly
cookie + twee gescheiden hashes
docs/patterns/qr-login.md — herbruikbaar pattern 'QR-pairing via unauth-SSE +
pre-auth cookie' met de drie endpoints, vier security-uitgangspunten,
sjabloon-bestanden, TTL-richtlijn, en wanneer NIET te gebruiken.
CLAUDE.md — extra rij in Implementatiepatronen-tabel die naar het nieuwe
pattern-doc verwijst.
Acceptatie ST-1008 (zeven scenario's):
- Happy path: gedekt door manuele E2E in vorige stories (gebruiker bevestigde
dat M10-stories op Solo bord verschijnen + curl-roundtrip werkt)
- Demo-block: actions/pairing.test.ts → approvePairing demo → Niet beschikbaar
- Replay: pair-claim.test.ts → 410 op tweede claim
- Expiry tijdens pending: pair-stream.test.ts + pairing.test.ts → 410/error
- Expiry tussen approve+claim: pair-claim.test.ts → 410
- Cookie-mismatch op SSE/claim: pair-stream.test.ts + pair-claim.test.ts → 401
- Secret niet in URL/logs: per ontwerp — fragment + cookie reizen niet via
URL-paden of querystrings (gedocumenteerd in architecture.md)
Quality gates: lint 0 errors, tsc clean, vitest 139/139 (16 files).
M10 is hiermee compleet — feat/M10-qr-login bevat 13 commits klaar voor
gebruiker-acceptatie en PR.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix: move logout form outside DropdownMenuContent so requestSubmit fires
UserMenu's hidden logout-form zat binnen <DropdownMenuContent>. Wanneer een
DropdownMenuItem onSelect vuurt, sluit base-ui de menu en unmount het
content-portal in dezelfde tick — waardoor de form verdwijnt voordat
requestSubmit() wordt aangeroepen, en logoutFormRef.current null is.
Form naar top-level van het component verplaatsen (als sibling van DropdownMenu,
binnen Fragment) houdt de ref geldig. Geen DOM-side-effecten — form is hidden,
zat nooit visueel in het menu.
Quality gates: lint 0 errors, tsc clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix: call logoutAction directly via useTransition instead of form-ref submit
De form-ref-dance werkte niet betrouwbaar in de huidige base-ui:
- onSelect vuurde requestSubmit() op een hidden form
- Form zat eerst binnen DropdownMenuContent (form geunmount → ref null)
- Form daarna naar top-level verplaatst — vuurde nog steeds geen request af,
vermoedelijk doordat onSelect in deze base-ui-build niet (consistent) een
click-event genereerde dat de form-API trigger'de
Vervang door directe call: Server Actions kunnen sinds Next.js 14 als async
functie worden aangeroepen vanuit Client Components. useTransition voorkomt
dat de UI bevriest tijdens de redirect.
Naast onSelect ook onClick als veiligheid voor het geval base-ui later weer
van event-prop wisselt — beide handlers wijzen naar dezelfde idempotente
function (handleLogout via startTransition).
Pendingstate ('Uitloggen…' label, disabled item) zodat dubbele klikken niet
dubbele logoutAction-calls afvuren.
Quality gates: lint 0 errors, tsc clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(ST-1007): listen for SSE 'state' event so approve-during-connect resolves
De SSE-route in ST-1004 stuurt de catch-up payload als `event: state\ndata: …`
om een race te dichten: tussen pair/start en SSE-open kan de mobiel approven,
de pg_notify fired vóór onze LISTEN actief is en gaat verloren (Postgres
queuet niet). De server compenseert door direct na connect een `state`-event
te sturen met de huidige status uit de DB.
Maar de client luisterde alleen op 'message'. EventSource routeert events met
`event: <name>` enkel naar listeners voor die exacte naam — het catch-up event
werd dus genegeerd. Gevolg bij een (zeldzame) race: QR blijft hangen tot
expiry omdat noch de notify noch de catch-up doorkomt.
Fix: dezelfde onMessage-handler ook aan 'state' binden (en netjes
unsubscriben bij cleanup). Geen server-side wijziging nodig — protocol bleef
bewust om de semantische scheiding 'initial state' vs 'live notify' te
behouden voor toekomstige clients die er onderscheid in willen maken.
Severity: middel-laag — kleine race-window, geen data/security-impact, alleen
"QR doet niks" tot user op Vernieuwen klikt.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(M10): close pair/stream race + demo-block on cancelPairing
Twee P1's uit code-review:
(1) pair/stream race: de findUnique die de pairing-status leest gebeurde vóór
LISTEN actief was. Als de mobiel approvet tussen die query en LISTEN: pg_notify
fired in dat venster gaat verloren (Postgres queuet niet voor abonnees die
nog niet listen) én was de eerder gelezen status stale. De catch-up state-
event emitte dus 'pending' terwijl de DB inmiddels 'approved' was, en de
desktop bleef hangen tot expiry.
Tweede findUnique toegevoegd ná LISTEN actief is: het venster sluit, omdat
elke approve na dat punt via de notify-handler doorkomt. Aanvullend op de
eerdere client-side fix die 'state' events nu ook routeert (commit d6e71f9).
(2) cancelPairing demo-block: cancel was een DB-write zonder demo-guard,
in tegenspraak met de "demo = 403 op writes"-regel. Demo-blokkade
toegevoegd; bestaande test omgedraaid naar 'wordt geblokkeerd, geen DB-write'.
Quality gates: lint 0 errors, tsc clean, vitest 139/139.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>