From c1c219639ad34590db2915732a9a0204ebb56087 Mon Sep 17 00:00:00 2001 From: Janpeter Visser Date: Mon, 27 Apr 2026 17:37:06 +0200 Subject: [PATCH] =?UTF-8?q?feat(M8):=20Realtime=20Solo=20Paneel=20?= =?UTF-8?q?=E2=80=94=20SSE-afronding=20en=20cleanup=20(ST-801..ST-806)=20(?= =?UTF-8?q?#9)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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) * 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) * 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) * 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 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) * 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-` (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) * 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) * 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) * 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) * 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) * 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 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) * 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) * 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) * 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) --------- Co-authored-by: Claude Opus 4.7 (1M context)