* 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 (commit0e3228d) — 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 (commitd6e71f9). (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>
12 KiB
CLAUDE.md — Scrum4Me
Dit is het centrale instructiedocument voor Claude Code. Lees dit volledig voordat je iets bouwt.
Wat is Scrum4Me?
Een desktop-first fullstack webapplicatie voor solo developers en kleine Scrum Teams die meerdere softwareprojecten parallel beheren. De app organiseert werk hiërarchisch (product → PBI → story → taak), biedt gesplitste planningsschermen met drag-and-drop, en integreert met Claude Code via een REST API.
Specificatiedocumenten
Lees het relevante document voordat je aan een feature begint. Nooit gokken over requirements.
| Document | Gebruik voor |
|---|---|
docs/scrum4me-functional-spec.md |
Acceptatiecriteria, randgevallen, user flows |
docs/scrum4me-architecture.md |
Stack, datamodel, Prisma schema, Zustand stores |
docs/scrum4me-backlog.md |
Welke task bouwen, volgorde, "done when"-criteria |
docs/scrum4me-personas.md |
Lars (primair), Dina, Remi — gebruik bij UI-beslissingen |
docs/scrum4me-product-backlog.md |
Historische domein-backlog (referentie); seed wordt sinds ST-004 gegenereerd uit scrum4me-backlog.md via prisma/seed-data/parse-backlog.ts |
docs/API.md |
REST-API contract voor Claude Code — endpoints, status-enums, foutcodes, voorbeeld-curls |
docs/scrum4me-styling.md |
Lees dit voor elk component — MD3-kleuren, shadcn patronen |
docs/agent-instruction-audit.md |
Waarom de agent-instructies zijn aangescherpt; checklist voor toekomstige wijzigingen |
docs/plans/<milestone-key>-*.md |
Implementatieplan per milestone — Bestanden, Stappen, Aandachtspunten, Verificatie. Lees vóór je aan een ST begint. Milestone-key matcht backlog-header (M9, M3.5, PBI-9, …). |
madhura68/scrum4me-mcp |
MCP-server repo: native tools voor Claude Code, schema-sync via git submodule |
Waar te beginnen
Volg de backlog strikt op volgorde. Start bij ST-001. Sla geen milestone over.
M0 (ST-001–008) → M1 (ST-101–110) → M2 (ST-201–210)
→ M3 (ST-301–312) → M4 (ST-401–410) → M5 (ST-501–506)
→ M6 (ST-601–612)
Werken aan een task kan via twee tracks. Track A heeft de voorkeur als je in Claude Code zit; Track B is voor Codex of omgevingen zonder MCP.
Track A — via Claude Code MCP (aanbevolen)
- Roep
mcp__scrum4me__implement_next_storyaan metproduct_id(gebruikmcp__scrum4me__list_productsals je het id niet weet) - De prompt orkestreert:
get_claude_context→log_implementation→ per taskupdate_task_status(in_progress)→ bouw →update_task_status(done)→log_test_result→log_commit - Bouw de tasks in volgorde van
sort_order; lees per task de relevante pattern-doc en styling - Verifieer:
npm run lint && npm test && npm run build - Commit per laag (zie Commit Strategy)
Track B — manueel (Codex of zonder MCP)
- Lees de task in
scrum4me-backlog.md - Zoek de bijbehorende feature-spec in
scrum4me-functional-spec.md - Lees het relevante patroon in
docs/patterns/en styling indocs/scrum4me-styling.mdals dat van toepassing is - Bouw — test — verifieer de "Done when"-criteria
- Vraag of de code correct is
- Commit (zie Commit Strategy hieronder)
- Vraag of de volgende taak gedaan moet worden
Tech stack
Next.js 16 (App Router) + React 19
TypeScript strict
Tailwind CSS + shadcn/ui
MD3 kleurensysteem via app/styles/theme.css
Zustand (client state)
dnd-kit (drag-and-drop)
Prisma v7 + PostgreSQL (Neon)
iron-session (auth cookies)
bcryptjs + Zod + Sonner
Sharp (avatarverwerking)
Vercel Analytics (@vercel/analytics/next)
⚠️ Stylingregel: Gebruik nooit
bg-blue-500of willekeurige Tailwind-kleuren. Gebruik altijd semantische MD3-tokens:bg-primary,bg-status-done,bg-priority-critical. Ziescrum4me-styling.mdvoor alle patronen.
⚠️ Next.js-versie: Lees
node_modules/next/dist/docs/bij twijfel — API's kunnen afwijken van trainingsdata.
Implementatiepatronen
Lees het relevante patroon vóór je begint. Nooit uit het hoofd schrijven.
| Patroon | Bestand |
|---|---|
| iron-session (auth cookies) | docs/patterns/iron-session.md |
| Prisma Client singleton | docs/patterns/prisma-client.md |
| Server Action (met auth + Zod) | docs/patterns/server-action.md |
| Route Handler (REST API) | docs/patterns/route-handler.md |
| Zustand optimistische update + rollback | docs/patterns/zustand-optimistic.md |
| 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 |
| 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 |
Env vars
DATABASE_URL="" # postgresql://...
DIRECT_URL="" # alleen bij Neon/cloud
SESSION_SECRET="" # openssl rand -base64 32
Conventies
- Branches:
feat/ST-001-scaffolding - Server Actions: altijd in
actions/[domein].ts, nooit inline in page.tsx - Validatie: altijd Zod, nooit handmatige checks
- Toegangsmodel: product-scoped resources gebruiken
productAccessFilter(userId)tenzij het expliciet een eigenaarsactie is - Bulk-ID's: reorder- en beslissingsacties valideren dat alle meegegeven IDs binnen dezelfde parent-scope vallen voordat er geschreven wordt
- Foreign keys: denormalized keys zoals
story.product_idworden afgeleid uit de database-parent (pbi.product_id), nooit uit client-input - Demo-check: elke Server Action controleert
session.isDemovóór schrijven - Foutberichten: Nederlands voor eindgebruikers — comments in code: Engels
- Dependencies: elke geïmporteerde runtime package staat direct in
dependencies, niet alleen transitief inpackage-lock.json - Docs-sync: elke gedrags-, dependency-, API- of deploymentwijziging werkt README, relevante docs en patterns bij in dezelfde change
- Entity codes: gebruik product/PBI/story-codes in commit-titles wanneer aanwezig (
feat(ST-356.2): ...); branchnaam blijftfeat/ST-XXX-slug - Status-enums op API: lowercase (
todo|in_progress|review|done,open|in_sprint|done); DB houdt UPPER_SNAKE; conversie uitsluitend vialib/task-status.ts-mappers — nooit ad-hoc.toLowerCase()elders - Foutcodes API:
400alleen voor malformed JSON-body (parse-fout viarequest.json());422voor zod-validatie en well-formed-maar-niet-acceptabel;403voor demo-tokens. Documenteer per endpoint indocs/API.md - Tests volgen contract: bij een API-contract-wijziging (status, foutcode, response-shape) MOET in dezelfde commit ook
__tests__/api/mee — een test die rood gaat omdat de oude waarde wordt verwacht is een onvolledige wijziging, niet een "kapotte test" - Dev port:
npm run devdraait altijd op 3000. Eenpredev-hook killt vooraf elk proces op 3000 (stale Next.js dev-server, vorige sessie) zodat sessies, cookies en MCP-config consistent op één poort werken. Wijk hier niet van af — geen-p 3001o.i.d. tenzij je expliciet twee dev-servers naast elkaar wil draaien
Branch & PR Strategy (STRICT — kostenbeheersing)
Core rule: één branch per milestone, PR alleen na gebruikerstest
Elke git push naar een feature-branch triggert een Vercel preview-deployment. Op het huidige Hobby-account zijn die schaars en kosten geld; we minimaliseren preview-builds tot er werkelijk iets te reviewen valt.
Wel doen
- Eén branch voor de hele milestone —
feat/M{N}-{slug}(bv.feat/M10-qr-login); voor losse stories zonder milestone blijftfeat/ST-XXX-{slug}geldig - Commits accumuleren lokaal volgens de Commit Strategy hieronder — één commit per stap, ST-code in de titel
- Pushen + PR openen pas nadat de gebruiker de milestone handmatig heeft getest en goedgekeurd — vraag expliciet om bevestiging vóór
git push - Tussentijdse "klaar voor jouw test"-momenten markeren met een lokale tag of een berichtje in chat, niet met een push
Niet doen
- Pushen na elke story of commit
- Een PR per story openen tijdens de implementatie
- "Just-in-case" pushen om backup te hebben — gebruik
git stash, een lokale tag, of meerdere lokale branches --force-pushom eerdere preview-builds "weg te toveren" (kost dezelfde build opnieuw bij hercreatie)
Uitzonderingen
- Een planning-PR zonder code-wijzigingen (alleen docs in
docs/plans/ofdocs/) mag direct gepusht worden — die triggert geen functional regressie en is goedkoop te bouwen - Een bugfix-hotfix op
mainmet aantoonbare productie-impact mag direct gepusht worden
Wanneer aanpassen
Zodra het Vercel-account naar Pro (of andere omgeving zonder per-build-kosten) gaat: vervang deze regel door "branch + PR per story" zoals oorspronkelijk in dit document stond. Werk deze sectie bij én documenteer de wijziging in docs/agent-instruction-audit.md.
Commit Strategy (STRICT)
Core rule: één commit = één verantwoordelijkheid
Nooit doen
- Database + API + UI in één commit mengen
- Feature + documentatie combineren
- Grote "alles gewijzigd" commits
- Vage berichten zoals "update stuff"
Verplichte structuur
Splits werk op in logische lagen:
- Database / Prisma
- API / server actions
- UI / components
- Config / infra
- Documentatie
Commit-formaat
feat(ST-XXX): korte beschrijving
fix(ST-XXX): korte beschrijving
chore(ST-XXX): korte beschrijving
docs(ST-XXX): korte beschrijving
Voorbeeld (verplicht patroon)
In plaats van:
feat: add profile system
Splits altijd op in:
feat(ST-XXX): add user profile fields to Prisma schema
feat(ST-XXX): add avatar upload endpoint
feat(ST-XXX): add profile editor component
chore(ST-XXX): configure sharp for avatar processing
docs(ST-XXX): document profile feature
Scrum-terminologie
| Correct | Niet gebruiken |
|---|---|
| Product Backlog Item (PBI) | Feature, Epic, Issue |
| Story | User Story, Ticket |
| Sprint Goal | Sprint Objective |
| Scrum Team | Team |
MCP-integratie
Scrum4Me heeft een eigen MCP-server in repo madhura68/scrum4me-mcp die de REST-API als native tools voor Claude Code aanbiedt. Schema's worden gedeeld via een git submodule (vendor/scrum4me), niet gedupliceerd.
Tools beschikbaar in Claude Code
mcp__scrum4me__health— service + DB pingmcp__scrum4me__list_products— producten waar de tokengebruiker toegang tot heeftmcp__scrum4me__get_claude_context— bundled product / actieve sprint / next story (met tasks) / open todosmcp__scrum4me__update_task_status,mcp__scrum4me__update_task_planmcp__scrum4me__log_implementation,mcp__scrum4me__log_test_result,mcp__scrum4me__log_commitmcp__scrum4me__create_todo
Prompt
implement_next_story(arg:product_id) — end-to-end workflow
Schema-drift bewaking
Wekelijks (maandag 08:00 Amsterdam) draait de remote agent trig_015FFUnxjz9WMuhhWNGBQKFD die vendor/scrum4me syncet en prisma:generate + tsc --noEmit uitvoert in scrum4me-mcp. Als die agent drift rapporteert, hoort dat vóór een Scrum4Me-PR met schema-wijziging gemerged kan worden — anders breekt de MCP-server stilletjes op runtime.
Definition of Done (MVP)
M7 (MCP-server) is post-MVP en heeft eigen acceptatie in docs/scrum4me-backlog.md.
- Alle 62 tasks (ST-001 t/m ST-612) afgerond
- Volledige Lars-flow zonder fouten (ST-612)
- Alle 7 API-endpoints werken via curl
- Demo-gebruiker heeft geen schrijfrechten
- App opzetbaar via README zonder extra hulp
- CI/CD actief — falende build blokkeert merge
- Beveiligingsreview API geslaagd (cross-user toegang onmogelijk)
- Documentatie is bijgewerkt voor gewijzigde API's, dependencies, deployment en agent-instructies