Commit graph

64 commits

Author SHA1 Message Date
3a90fa9d13 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>
2026-04-27 23:21:10 +02:00
c48e30df1f 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>
2026-04-27 23:15:02 +02:00
5c4ee150ea 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>
2026-04-27 22:58:17 +02:00
625221f9ee 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>
2026-04-27 22:50:42 +02:00
2a0c6a512d 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>
2026-04-27 22:42:07 +02:00
e0bec8c55c 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>
2026-04-27 22:34:49 +02:00
b4813e6e54 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>
2026-04-27 22:23:00 +02:00
9941d96846 feat(landing): highlight realtime updates and beta/desktop-first notice 2026-04-27 20:29:51 +02:00
aa040ffd48 feat(landing): add MCP integration section, API curl example, GitHub link 2026-04-27 20:28:32 +02:00
88dca4102c
feat(M9): active product backlog — persistent active PB, NavBar splits, sprint card styling (#10)
* feat(tooling): extend backlog parser to support PBI-x milestone headers

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore(backlog): mark ST-801–806 as done

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(backlog): sorteer PBI's en stories op prio/code/datum, onthoud keuze in localStorage; vergroot sprint-afronden dialoog

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(ST-901): add user.active_product_id with FK to Product

- Nullable relation User → Product with onDelete: SetNull
- Index on active_product_id for join performance
- Migration: 20260427165329_add_user_active_product_id
- Install @tanstack/react-table (was missing from node_modules)
- Fix PRIORITY_COLORS ref removed in earlier refactor
- Note: User schema change affects vendor/scrum4me-mcp submodule — run prisma generate + tsc --noEmit there after merge

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: restore priority color on PBI filter pill

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(ST-902): add setActiveProduct + clearActiveProduct server actions

- actions/active-product.ts: setActiveProductAction validates access via
  productAccessFilter, rejects archived products and demo users
- archiveProductAction: clears active_product_id for all affected users in transaction
- removeProductMemberAction: clears active_product_id for removed member
- leaveProductAction: clears active_product_id for leaving user

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(ST-903): load active product in layout, replace cookie with DB lookup in solo

- layout.tsx: fetch active_product_id, resolve product, clear stale ref server-side
- NavBar: add activeProduct prop (rendering changes in ST-904)
- solo/page.tsx: redirect via user.active_product_id instead of lastProductId cookie
- proxy.ts: remove lastProductId cookie logic
- lib/cookies.ts: deleted (no longer used)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(ST-904): split NavBar into 5 tabs with disabled-states and product-switcher dropdown

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(ST-905): add Activeer button per product row in dashboard and product header

* feat(ST-906): redirect to dashboard with toast when active product becomes inaccessible

* feat(ST-907): tests for active-product actions and functional spec update for M9

* docs(M9): add implementation plan document and link from backlog

* feat: active PB indicator, Maak actief button and new product link in settings

* feat: apply priority-color card style to sprint story rows

* fix: move add-to-sprint click from entire card to + Toevoegen button

* feat: apply priority-color card style to sprint task rows

* fix(sprint-backlog): prevent text selection on PBI collapse button

* chore: bump version to 0.4.0 (M9 active product backlog)

* fix(landing): align logged-in nav left to match app NavBar

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-27 20:25:13 +02:00
2460eae9df
feat(M8): Realtime Solo Paneel via Postgres LISTEN/NOTIFY (ST-801..ST-806) (#8)
* feat(ST-801): pg_notify triggers on tasks and stories

Add notify_task_change() and notify_story_change() PL/pgSQL functions
plus AFTER INSERT/UPDATE/DELETE triggers on tasks and stories. Each
write emits a JSONB payload on the 'scrum4me_changes' channel with
op, entity, id, product_id, sprint_id, assignee_id and (for UPDATE)
the list of changed columns. Tasks resolve product/sprint/assignee
via their parent story so the SSE handler can filter without an extra
DB roundtrip.

The migration is a side-effect-only change (no Prisma model/schema
diff) so the Prisma Client and TypeScript types are unaffected.

Verified locally with a node-pg LISTEN client: both task and story
mutations produce the expected payload within milliseconds.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(ST-802): SSE route /api/realtime/solo for Solo Paneel updates

Node.js-runtime route handler die een dedicated pg-Client opent op
DIRECT_URL en LISTEN't op het scrum4me_changes-kanaal. Per inkomende
NOTIFY-payload filtert het server-side op product (uit query-param),
sprint (active sprint van het product) en persoonlijke relevantie
(assignee_id == userId, of assignee_id IS NULL voor stories voor de
claim-lijst).

Auth via iron-session cookie:
- 401 als sessie ontbreekt
- 400 als product_id query-param ontbreekt
- 403 als de user geen toegang heeft tot het product
- demo-tokens mogen lezen (geen write-tools op deze route)

Stream-bouw:
- text/event-stream met juiste headers (no-cache, no-transform,
  X-Accel-Buffering: no voor proxy-vrije buffering)
- ready-event bij connect met product_id en active sprint_id
- heartbeat (SSE-comment) elke 25s
- hard-close na 240s als safety-net onder Vercel maxDuration; client
  herconnect via EventSource
- cleanup op request.signal abort (tab dicht / refresh)

Cleanup-pad sluit de pg-client en de stream-controller idempotent.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(ST-803): useSoloRealtime hook with EventSource lifecycle

Client-only hook die de SSE-stream van /api/realtime/solo opent en
voor de UI twee statussen exposed:
  - status: 'connecting' | 'open' | 'disconnected'
  - showConnectingIndicator: pas true als status !== 'open' >2s duurt
    (ST-805 animatie B — voorkomt flikker bij microscopische
    disconnects)

Lifecycle-gedrag:
- Reconnect met exponential backoff start 1s, plafond 30s, reset op
  'ready'-event
- Page Visibility API: bij hidden sluit de hook de connectie en stopt
  reconnect-pogingen; bij visible reopent direct
- onerror van EventSource wordt onderschept zodat we eigen backoff
  kunnen voeren ipv de browser-default
- Volledige cleanup op unmount

Voert events naar useSoloStore.getState().handleRealtimeEvent — de
echte dispatch-logica met pendingOps en gedifferentieerde
apply{Task,Story}{Update,Create,Delete} landt in ST-804. Hier is dat
nog een stub zodat dit zelf-staand kan compileren.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(ST-804): solo-store realtime dispatch + pendingOps

Wire de SSE-events uit /api/realtime/solo door naar de Zustand-store
zodat het Solo Paneel zonder refresh meebeweegt met DB-mutaties uit
welke bron dan ook (web, REST, MCP).

Migratie 20260427000216_extend_realtime_payload: voegt new-state
velden aan de pg_notify-payload toe (task_status, task_sort_order,
task_title, story_status, story_sort_order, story_title, story_code)
zodat de client geen extra fetch nodig heeft per event.

Store-uitbreiding (stores/solo-store.ts):
- pendingOps: Set<task-id> die optimistic-writes markeert; realtime
  echos voor die ids worden onderdrukt zodat eigen UI-mutaties niet
  twee keer toegepast worden of door een latere echo overschreven
- handleRealtimeEvent: dispatch op entity + op
  - task UPDATE/INSERT: bestaande tasks krijgen status/title/sort_order
    bijgewerkt; onbekende tasks worden genegeerd (story-context
    ontbreekt — gebruiker ziet ze pas na refresh)
  - task DELETE: verwijdert uit store
  - story UPDATE: werkt story_title/story_code bij op alle child-tasks
    in de store
  - story DELETE: verwijdert alle child-tasks (cascade reflectie)

Unit-test: 7 scenario's (status update, pendingOps echo-suppression,
DELETE, story-rename cascade, story-delete cascade, unknown task
no-op).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(ST-805): wire useSoloRealtime + live indicator + column-move animatie

- SoloBoard roept useSoloRealtime(productId) aan, zo komt elke task/
  story-mutatie uit web/REST/MCP binnen via SSE en wordt door de
  store-dispatcher (ST-804) verwerkt
- markPending/clearPending rond de drag-drop optimistic write zodat
  de echo van de eigen Server Action de store niet dubbel beweegt
- RealtimeIndicator: kleine status-dot in de header
  - groen ('Live') wanneer SSE-stream open OF tijdens de eerste 2s
    grace-period (animatie B in de hook — voorkomt micro-flikker)
  - grijs ('Verbinden…') na 2s in connecting-state
  - rood ('Verbroken') na 2s in disconnected-state
- Animatie A (kolom-move): bij task UPDATE-events wikkelt de hook de
  store-dispatch in document.startViewTransition + flushSync. SoloTask-
  cards krijgen view-transition-name `solo-task-<id>` (alleen wanneer
  niet aan het draggen) zodat de browser de positie-shift soepel
  animeert van bezig naar klaar (en omgekeerd)

Bestaande 89 tests blijven groen.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: pin dev to port 3000 + predev hook to clear stale processes

Voorkomt dat een stale next-dev op 3000 ervoor zorgt dat een tweede
'npm run dev' op 3001 start — wat sessies, cookies en MCP-config
inconsistent maakt.

- dev: '-p 3000' expliciet
- predev: lsof/kill alles op 3000 (idempotent — falen is ok)
- CLAUDE.md: regel toegevoegd onder Conventies

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(M8): make SSE-stream survive Solo Paneel mutations

Symptoom op feat/ST-801-realtime-triggers initial implementation:
elke task-update sloot de open SSE-stream af en triggerde een
herverbinding met backoff. In de tussentijd gemiste events.

Oorzaak: Server Actions in App Router doen een impliciete
route-tree refresh die client components remount; daarmee killt
React de useEffect die de EventSource beheert.

Fix in twee delen:

1. Hef de realtime-hook op naar de (app)-layout via een nieuwe
   `SoloRealtimeBridge`-component. Layouts overleven Server-
   Action-refreshes beter dan pages, en de bridge leest het
   product-id uit de URL via usePathname. Connection-status
   (status, showConnectingIndicator) gaat naar de solo-store
   zodat SoloBoard 'm uit een gedeelde plek kan lezen.

2. Vervang updateTaskStatusAction en updateTaskPlanAction in de
   Solo-componenten door fetch naar de bestaande Route Handler
   `PATCH /api/tasks/[id]`. Route Handlers triggeren geen
   page-refresh, dus de SSE-stream blijft staan. lib/api-auth.ts
   accepteert nu naast Bearer-tokens ook iron-session cookies
   zodat browser-fetches zonder token werken.

Bijkomend: actions/tasks.ts laat /solo bewust niet meer
revalideren (wordt nu via realtime gedekt). Sprint/planning blijft
wel revalidaten — geen realtime daar.

Toegevoegd:
- components/solo/realtime-bridge.tsx — mount in (app) layout
- scripts/realtime-mutate.ts — handige test-helper voor externe
  mutaties (alsof MCP/REST schrijft) tijdens acceptance

Debug-logs in app/api/realtime/solo/route.ts staan nog aan voor
ST-806 acceptance; worden later gestript.

Bekend issue: Chrome op localhost (HTTP/1.1) cycle't EventSource
om de paar seconden vanwege de 6-connectie-limiet en retry-
heuristiek. Safari werkt stabiel. Productie op Vercel (HTTP/2
multiplexing) zou beide browsers stabiel moeten houden — Vercel
preview test is volgende stap.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(dev): disable reactStrictMode for stable SSE testing locally

Strict Mode dubbel-mount maakt langlopende connecties tijdens
ontwikkeling moeilijk te observeren — de mount/unmount cycles in
dev rondom Hot Reload + Turbopack triggeren herhaalde EventSource-
reconnects die verbergen of de productie-flow stabiel is.

Productie kent dit gedrag niet (Strict Mode is dev-only). Heroverwogen
als M8 acceptance rond is — kan dan weer aan voor andere effects-
discipline.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(M8): unguard debug logs in /api/realtime/solo for Vercel diagnose

Maakt de connect/listen/notify-logs zichtbaar in Vercel function-logs
op preview zodat we kunnen zien waarom events niet doorkomen ondanks
DIRECT_URL-fix. Logt ook hostname (gemaskeerd) + sprint id bij connect,
en pg client errors + end-events voor closed connections.

Wordt gestript in ST-806 voordat de PR uit draft gaat.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(debug): add /debug-realtime page + bare SSE endpoint

Tijdelijke debug-tooling voor M8-acceptance op Vercel preview.

- app/api/debug/realtime-stream/route.ts — geen auth, geen filtering;
  dropt elke pg_notify-event op scrum4me_changes rauw door als SSE
- app/debug-realtime/page.tsx — open zonder login op de root, toont
  binnenkomende events in een simpele <table>

Doel: isoleren of de SSE + Postgres LISTEN-pipe op Vercel überhaupt
events laat zien, los van iron-session, productfilter of solo-store.
Als ook deze niets binnen krijgt: probleem zit in pg connection of
Vercel function lifecycle. Als deze wel events toont: probleem zit
hoger in de stack (filter, store, hook).

VERWIJDEREN voordat de PR uit draft gaat.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(debug): extend /debug-realtime with stats, emit-button and filters

Bouwt de basale luister-tabel uit met diagnostische tooling om de
SSE+LISTEN-pipe stress-vrij te kunnen valideren.

Toegevoegd:
- POST /api/debug/emit-test-notify — vuurt een handmatige pg_notify
  op scrum4me_changes met een synthetic payload (debug:true) zonder
  een echte DB-UPDATE te doen. Isoleert de SSE-route van Prisma/triggers.
- DebugRealtimeClient: stats-grid (status, reconnects, total events,
  since last event met >30s rood-warning, largest gap, first-event-
  time), emit-button, reset-stats, filters op type en entity
  (incl. "debug only").
- Type/entity kolom in de tabel met kleuring per type.

Geen impact op productie- of solo-flow. Tijdelijke testtooling;
verwijderen wanneer we deze pagina niet meer nodig hebben.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(debug): add Layer 2 — mini Zustand-store + dispatch toggles

Test of SSE-event → store → render-pipeline werkt buiten de Solo
Paneel context. Mirrort het patroon van solo-store maar minimaal.

- debug-store.ts: kleine Zustand-store met tasks + applyEvent +
  applyCount/skipCount-tellers
- store-panel.tsx: rendert store-state in een tabel met statuskleuring
- client.tsx: drie layer-toggles (dispatch / flushSync / startView-
  Transition) + lift dispatch in onmessage. Zo kunnen we elke
  combinatie isoleren

Bevestigd: alle drie de toggles werken op het bare /debug-realtime
endpoint. Volgende laag is Server Action revalidation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(ST-806): cleanup debug-pages, document realtime, restore strict mode

M8 acceptance heeft de end-to-end pipeline bevestigd: trigger → NOTIFY →
SSE → store → View Transition. De cycling-symptomen waren een artefact
van testen via terminal (alt-tab triggert visibility-pause-by-design),
geen bug. Tijd om de tijdelijke instrumentatie en debug-pagina's weg te
halen en de architectuur op te schrijven.

- Verwijder /debug-realtime, /(app)/debug-realtime-app, /api/debug/*
- Strip debug-logs uit /api/realtime/solo (closed-reden alleen in dev)
- reactStrictMode weer aan
- CONNECTING_INDICATOR_DELAY_MS 2s → 4s (minder flikker bij micro-disconnects)
- Nieuwe sectie "Realtime updates (M8)" in scrum4me-architecture.md:
  diagram, NOTIFY-bron, server-filter, connection lifecycle inclusief
  visibility-pause + bekende beperking, animatie, auth
- DIRECT_URL env-rij uitgebreid met realtime-doel
- GET /api/realtime/solo gedocumenteerd in API.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 13:59:32 +02:00
8afbc4f6c4 chore(debug): add /debug-env page to verify Vercel env-var presence
Server-side gerenderde tabel die per env-var (DATABASE_URL, DIRECT_URL,
SESSION_SECRET) toont of ie gezet is, lengte, hostname (van DB-URLs)
en of het de Neon-pooler is. Geen secrets in de output.

Doel: bevestigen dat onze Vercel-env-config correct werd opgeslagen na
de eerdere ENOTFOUND 'base'-error. Tijdelijk; verwijderen zodra de
config is gevalideerd.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 10:37:46 +02:00
43a4294424
Todo description, entity codes, REST API extensions and Claude Code hardening (ST-509/511/512/513) (#2)
* docs(ST-511): add backlog entry for entity codes feature

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(ST-511): add createWithCodeRetry helper to handle P2002 race on auto codes

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(ST-511): retry on auto-code unique conflict in story and pbi create

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(ST-511): surface field errors for code and title in PBI dialog

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(ST-511): read create-state errors in Story dialog fieldError

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(ST-512): add backlog entry for REST API code/description/implementation_plan extensions; mark ST-511 done

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(ST-512): extend REST API with code, description and implementation_plan

- GET /api/products returns code, description and definition_of_done
- GET /api/products/:id/next-story returns story.code and per-task code + implementation_plan
- GET /api/sprints/:id/tasks returns description, implementation_plan, story_code and derived per-task code
- POST /api/todos accepts and returns optional description (max 2000)

All changes are additive — existing clients ignore unknown keys.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(ST-512): mark ST-512 as done

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(ST-513): add backlog entry for API hardening for Claude Code

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(ST-513): add task and story status mappers for API boundary

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(ST-513): expose lowercase status on API and accept lowercase in PATCH /api/tasks

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(ST-513): add metadata JSONB column to StoryLog

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(ST-513): accept optional metadata in story log and switch validation errors to 422

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(ST-513): add GET /api/health endpoint with optional db ping

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(ST-513): add GET /api/products/:id/claude-context bundled endpoint

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(ST-513): add docs/API.md and link from CLAUDE.md specs table

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(ST-513): mark ST-513 as done

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(ST-513): split 400 (malformed JSON) from 422 (validation), reject 'review'

Codex review on PR #2:

- P2.1: routes treated JSON parse failures as 422 instead of 400, breaking
  the contract in docs/API.md. Replace `request.json().catch(() => null)`
  with try/catch in 4 routes (tasks, reorder, todos, story-log) so a
  malformed body returns 400 and only well-formed-but-invalid bodies
  return 422.

- P2.2: PATCH /api/tasks/:id accepted `status: "review"`, but the sprint
  task list UI does not render REVIEW (no label/color, the cycle helper
  falls back to TO_DO). Reject `review` at the API until the sprint UI
  is extended; document the subset in docs/API.md.

Tests in __tests__/api updated for the new contract (29 assertions:
zod-failures now expect 422, status payloads use lowercase API values,
sprint-tasks mocks include the new story relation).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 23:40:54 +02:00
b71eb53fa8 feat(ST-507): show code badges on cards, lists and dialogs across the app
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 20:36:59 +02:00
66063f035a feat(ST-507): add code input to Product, Pbi and Story forms
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 20:36:47 +02:00
c45ba1d0c2 feat(ST-509): add description textarea to Todo create and edit cards
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 19:59:55 +02:00
0dc907b75c feat(ST-507): add email input to settings and surface in user menu
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 19:37:51 +02:00
e887f796c7 feat(ST-507): replace navbar roles/settings/logout with avatar user menu
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 19:31:21 +02:00
8a5076a5ed feat(ST-611): add tour screenshots to public/screenshots
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 19:22:52 +02:00
3977b89ca7 feat(ST-611): add tour with screenshots, Solo Paneel and Sprint Board to landing page
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 19:04:57 +02:00
29ed4f2773 feat: show active product name in navbar, links to product page
Sub-layout sets product in Zustand store; NavBar reads it.
getAccessibleProduct wrapped with React cache to avoid double DB call.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 17:56:50 +02:00
d684732ec8 feat: show tasks with priority and status in unassigned stories sheet
Tasks are always visible under each story. Click a task to expand
its description.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 17:49:19 +02:00
1abbd4e5e4 fix: set lastProductId cookie in proxy instead of Server Component
Cookies can only be written in Server Actions or Route Handlers.
Moved the write to proxy.ts where NextResponse.cookies.set is allowed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 17:25:17 +02:00
d27c55c7fc fix: sprint board accessible to product members, not only owners
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 17:25:10 +02:00
af4a357709 fix: product backlog page accessible to members, not only owners
Replaced owner-only query (user_id = session.userId) with
getAccessibleProduct which also accepts product members.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 17:15:26 +02:00
928328b380 fix(ST-102): show products via membership on dashboard, not just owned
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 17:12:42 +02:00
89e5164a28 feat(ST-358): add unassigned stories sheet with claim-on-click
- UnassignedStoriesSheet: slide-in sheet listing unassigned sprint stories
- ClaimStoryRow: form action + ClaimButton with useFormStatus pending state
- Successful claim removes story from local list and shows success toast
- Empty state: "Geen ongeclaimde stories. Lekker bezig!"
- Demo: DemoTooltip wraps Pak op button, claim button disabled
- Page now fetches stories with _count.tasks instead of just count
- claimStoryAction also revalidates /products/[id]/solo path

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 16:55:55 +02:00
ca631e71ec feat(ST-355): add /solo route, cookie helper, product picker, and solo product page
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 16:29:31 +02:00
358c88a9d9 feat(ST-353): add assignee chip and claim/reassign dropdown to sprint story card
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 16:24:34 +02:00
ef5c7c7675 feat(ST-351): add UserAvatar component and /api/users/[id]/avatar route
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 16:15:56 +02:00
0a27be4886 feat(ST-313): merge sprint board into single three-panel view
- TriplePane component with two resizable dividers, localStorage persistence, mobile tabs
- SprintBoardClient replaces SprintBacklogClient + PlanningRightClient
- Left panel: Product Backlog (PBIs with stories to add to sprint)
- Middle panel: Sprint Backlog (stories in sprint, click to select, sortable)
- Right panel: TaskList for selected story
- /sprint/planning redirects to /sprint
- Remove PlanningLeft, PlanningRightClient, SprintBacklogClient

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 22:53:39 +02:00
4df83dcdbb feat(ST-108/ST-208): replace inline forms with PBI and story dialogs
- PbiDialog: create/edit with priority select and optional description
- StoryDialog: create/edit with priority, description, acceptance criteria, activity log, and delete
- PrioritySelect: reusable controlled select component
- Edit icons always visible on PBI rows and story blocks
- Dialog backdrop uses 40% opacity blur

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 22:53:26 +02:00
02096196ab feat(ST-601): add loading skeletons for dashboard, settings and todos
De drie zware split-pane routes hadden al loading.tsx; dashboard,
settings en todos kregen elk een passende animate-pulse skeleton.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 20:09:48 +02:00
0595a2a5d9 feat(ST-506): toon gebruikersrollen in navigatiebalk
Haalt rollen op in AppLayout en geeft ze door aan NavBar.
NavBar toont afkortingen (PO · SM · Dev) rechts van de navigatie.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 20:09:42 +02:00
e991f4f185 feat(todos): include team-member products in todos page query
Use productAccessFilter so users who are product members (not just
owners) see those products in the todo form and promote dialogs.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 19:17:05 +02:00
2deea9e841 Added Beta development banner to the app. This will be removed once we are ready for a public release. 2026-04-25 17:26:35 +02:00
4ec0683f88 Document Prisma ERD generation 2026-04-25 17:08:55 +02:00
b5e967d8d3 Add analytics and documentation updates 2026-04-25 15:11:51 +02:00
e0efb65efb Fix scoped access checks 2026-04-25 14:36:55 +02:00
d90a8fd560 fix: PATCH /api/tasks/:id geeft 403 bij cross-user toegang
Vervang productAccessFilter in de WHERE clause door een expliciete
toegangscheck na het ophalen. findFirst haalt de taak op met product
en members (gefilterd op auth.userId); daarna wordt eigenaarschap of
teamlidmaatschap gecontroleerd en 403 teruggegeven bij geen toegang.

Dit herstelt het onderscheid 404 (taak bestaat niet) vs 403 (taak
bestaat maar geen toegang), zoals de beveiligingstest verwacht.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 13:44:51 +02:00
e94959c5bc feat: PB-overzicht in instellingen + documentatie bijgewerkt
Settings:
- "Mijn teams" vervangen door gecombineerde "Product Backlogs" sectie
- Toont eigen producten (badge Eigenaar) en team-lidmaatschappen (badge Developer)
- Productnaam is klikbaar naar de product backlog
- "Verlaten"-knop met bevestiging alleen voor Developer-lidmaatschappen
- Lege staat met link naar product aanmaken

Docs:
- architecture.md: users tabel aangevuld met bio/bio_detail/avatar_data;
  Prisma schema excerpt bijgewerkt; projectstructuur bijgewerkt (profile route, ProfileEditor)
- functional-spec.md: F-02b gebruikersprofiel en F-02c PB-overzicht toegevoegd;
  datamodel users rij bijgewerkt; settings route bijgewerkt
- backlog.md: ST-507 profiel en ST-508 PB-overzicht toegevoegd als afgerond

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 13:40:47 +02:00
ec9de5a962 fix: header en footer sticky op landingspagina
Zelfde patroon als (app) layout: h-screen overflow-hidden op de wrapper,
flex-1 overflow-y-auto op main, shrink-0 op footer.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 13:32:32 +02:00
1ff894a6c0 feat: gebruikersprofiel met avatar, bio en uitgebreide beschrijving
- Schema: bio (160), bio_detail (2000) en avatar_data (bytea) op User
- POST /api/profile/avatar: validatie MIME-type + max 12 MB vóór verwerking,
  Sharp resize naar max 700x700 (fit inside), output WebP q85, opgeslagen als bytea in Neon
- GET /api/profile/avatar: serveert avatar met Cache-Control private 1u
- updateProfileAction: slaat bio en bio_detail op via Server Action + Zod
- ProfileEditor client component: avatar preview, upload met client-side validatie,
  bio-velden met tekenlimieten
- Settings page: profiel-sectie bovenaan, uitgeschakeld voor demo-gebruiker
- next.config: sharp als serverExternalPackage

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 13:30:38 +02:00
357b1e32e8 feat: ProductMember — team management for product backlogs
- Add ProductMember model (many-to-many User ↔ Product)
- Add productAccessFilter helper (owner OR member OR clause)
- Replace all ownership checks across actions and API routes
- Add addProductMemberAction / removeProductMemberAction / leaveProductAction
- Add TeamManager component in product settings (owner adds/removes Developers)
- Add LeaveProductButton in user settings (member leaves a product team)
- Regenerate Prisma Client after schema migration

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 13:09:44 +02:00
fc12e3cc64 feat: landingspagina met app-uitleg, Scrum-samenvatting en gebruikershandleiding
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 12:47:32 +02:00
76d6f0a09d fix: nav en statusbar vast op top/bottom via h-screen layout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 12:40:05 +02:00
cb7eb36fbb feat: Todo altijd gekoppeld aan product backlog
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 12:35:40 +02:00
b4371f5afb chore: documentatie naar docs/, iconen bijgewerkt, theme.css verplaatst
- scrum4me-*.md en MD3_Color_Scheme_Documentation.md verplaatst naar docs/
- Srum4MeIcons.html verplaatst naar docs/icons.html
- theme.css verplaatst van root naar app/styles/theme.css
- Import in globals.css bijgewerkt
- Alle app-iconen vervangen door nieuw logo (icon-master-light.svg)
- AppIcon component bijgewerkt met nieuw SVG
- CLAUDE.md verwijzingen bijgewerkt

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-24 22:51:42 +02:00
71e4ebc22d feat: statusbalk met versie en builddatum
Voegt sticky statusbalk toe aan de onderkant van de app met:
- Copyright melding
- App-versie uit package.json (NEXT_PUBLIC_APP_VERSION)
- Builddatum ingesteld op het moment van de build (NEXT_PUBLIC_BUILD_DATE)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-24 17:12:20 +02:00
f2698d98d8 fix: function props vanuit server component verplaatst naar client wrapper 2026-04-24 16:51:11 +02:00