From e964a763188f6e86830d328ef4420eb1d12c71c0 Mon Sep 17 00:00:00 2001 From: Madhura68 Date: Mon, 27 Apr 2026 10:02:22 +0200 Subject: [PATCH] chore(debug): add /debug-realtime page + bare SSE endpoint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- app/api/debug/realtime-stream/route.ts | 114 +++++++++++++++++++++++++ app/debug-realtime/client.tsx | 104 ++++++++++++++++++++++ app/debug-realtime/page.tsx | 23 +++++ 3 files changed, 241 insertions(+) create mode 100644 app/api/debug/realtime-stream/route.ts create mode 100644 app/debug-realtime/client.tsx create mode 100644 app/debug-realtime/page.tsx diff --git a/app/api/debug/realtime-stream/route.ts b/app/api/debug/realtime-stream/route.ts new file mode 100644 index 0000000..e909bfc --- /dev/null +++ b/app/api/debug/realtime-stream/route.ts @@ -0,0 +1,114 @@ +// TIJDELIJKE debug-endpoint voor M8-acceptance. +// Geen auth, geen filtering — alle pg_notify-events op `scrum4me_changes` +// stromen rauw door naar de browser. Bedoeld om te isoleren of de +// SSE + LISTEN-pipe op Vercel werkt, los van iron-session, productfilter +// of solo-store. +// +// VERWIJDEREN VOOR M8 OUT-OF-DRAFT. + +import { NextRequest } from 'next/server' +import { Client } from 'pg' + +export const runtime = 'nodejs' +export const dynamic = 'force-dynamic' +export const maxDuration = 300 + +const CHANNEL = 'scrum4me_changes' + +export async function GET(request: NextRequest) { + const directUrl = process.env.DIRECT_URL ?? process.env.DATABASE_URL + if (!directUrl) { + return Response.json({ error: 'DIRECT_URL/DATABASE_URL niet gezet' }, { status: 500 }) + } + + const isPooled = directUrl.includes('pooler.') + const hostHint = directUrl.match(/@([^/]+)/)?.[1] ?? 'unknown-host' + console.log(`[debug-realtime] connecting (${isPooled ? 'POOLED' : 'direct'}) host=${hostHint}`) + + const encoder = new TextEncoder() + const pgClient = new Client({ connectionString: directUrl }) + let closed = false + let heartbeatTimer: ReturnType | null = null + + const stream = new ReadableStream({ + async start(controller) { + const enqueue = (chunk: string) => { + if (closed) return + try { + controller.enqueue(encoder.encode(chunk)) + } catch { + // already closed + } + } + + const cleanup = async (reason: string) => { + if (closed) return + closed = true + if (heartbeatTimer) clearInterval(heartbeatTimer) + try { + await pgClient.end() + } catch { + // ignore + } + try { + controller.close() + } catch { + // already closed + } + console.log(`[debug-realtime] closed: ${reason}`) + } + + try { + await pgClient.connect() + await pgClient.query(`LISTEN ${CHANNEL}`) + console.log('[debug-realtime] LISTEN ready') + } catch (err) { + console.error('[debug-realtime] pg connect/listen failed:', err) + enqueue(`event: error\ndata: ${JSON.stringify({ message: String(err) })}\n\n`) + await cleanup('pg connect failed') + return + } + + pgClient.on('notification', (msg) => { + console.log(`[debug-realtime] RAW notification length=${msg.payload?.length ?? 0}`) + if (!msg.payload) return + enqueue(`data: ${msg.payload}\n\n`) + }) + + pgClient.on('error', async (err) => { + console.error('[debug-realtime] pg client error:', err) + await cleanup('pg error') + }) + + pgClient.on('end', () => { + console.log('[debug-realtime] pg client end') + }) + + enqueue( + `event: ready\ndata: ${JSON.stringify({ + host: hostHint, + pooled: isPooled, + channel: CHANNEL, + time: new Date().toISOString(), + })}\n\n`, + ) + + heartbeatTimer = setInterval(() => { + enqueue(`: heartbeat ${new Date().toISOString()}\n\n`) + }, 25_000) + + request.signal.addEventListener('abort', () => { + cleanup('client aborted') + }) + }, + }) + + return new Response(stream, { + headers: { + 'Content-Type': 'text/event-stream; charset=utf-8', + 'Cache-Control': 'no-cache, no-transform', + Connection: 'keep-alive', + 'X-Accel-Buffering': 'no', + }, + }) +} diff --git a/app/debug-realtime/client.tsx b/app/debug-realtime/client.tsx new file mode 100644 index 0000000..20618b8 --- /dev/null +++ b/app/debug-realtime/client.tsx @@ -0,0 +1,104 @@ +'use client' + +import { useEffect, useRef, useState } from 'react' + +interface Row { + receivedAt: string + type: 'ready' | 'message' | 'error' + raw: string +} + +const MAX_ROWS = 200 + +export function DebugRealtimeClient() { + const [rows, setRows] = useState([]) + const [status, setStatus] = useState<'connecting' | 'open' | 'closed' | 'error'>('connecting') + const sourceRef = useRef(null) + + useEffect(() => { + const source = new EventSource('/api/debug/realtime-stream') + sourceRef.current = source + + const append = (row: Row) => { + setRows((prev) => [row, ...prev].slice(0, MAX_ROWS)) + } + + source.addEventListener('ready', (e) => { + setStatus('open') + const data = (e as MessageEvent).data ?? '' + append({ receivedAt: new Date().toISOString(), type: 'ready', raw: data }) + }) + + source.addEventListener('error', (e) => { + setStatus('error') + const data = (e as MessageEvent).data ?? '(no data)' + append({ receivedAt: new Date().toISOString(), type: 'error', raw: data }) + }) + + source.onmessage = (e) => { + append({ receivedAt: new Date().toISOString(), type: 'message', raw: e.data ?? '' }) + } + + source.onerror = () => { + setStatus('error') + } + + return () => { + source.close() + sourceRef.current = null + } + }, []) + + function statusColor() { + switch (status) { + case 'open': + return 'green' + case 'error': + return 'red' + case 'closed': + return 'gray' + default: + return 'orange' + } + } + + return ( +
+
+ Status:{' '} + {status} · totaal{' '} + {rows.length} entries +
+
+ + + + + + + + + {rows.length === 0 ? ( + + + + ) : ( + rows.map((row, idx) => ( + + + + + + )) + )} + +
received_attypepayload
+ Wachten op events… trigger een mutatie via UI of script. +
+ {row.receivedAt} + {row.type} + {row.raw} +
+ + ) +} diff --git a/app/debug-realtime/page.tsx b/app/debug-realtime/page.tsx new file mode 100644 index 0000000..4dc28f3 --- /dev/null +++ b/app/debug-realtime/page.tsx @@ -0,0 +1,23 @@ +// TIJDELIJKE debug-pagina voor M8-acceptance. +// Geen auth, geen styling — toont alle inkomende pg_notify-events op +// `scrum4me_changes` in een tabel zodat we kunnen zien of de SSE + LISTEN- +// pipe überhaupt events doorstroomt op Vercel. +// +// VERWIJDEREN VOOR M8 OUT-OF-DRAFT. + +import { DebugRealtimeClient } from './client' + +export const dynamic = 'force-dynamic' + +export default function DebugRealtimePage() { + return ( +
+

Realtime debug — scrum4me_changes

+

+ Live SSE-stream rechtstreeks van Postgres LISTEN op channel{' '} + scrum4me_changes. Geen auth, geen filtering. Verwijderen na M8 acceptance. +

+ +
+ ) +}