From e276cac922049e081e9d9aa23d0e86d294c08292 Mon Sep 17 00:00:00 2001 From: Scrum4Me Agent <30029041+madhura68@users.noreply.github.com> Date: Thu, 7 May 2026 18:46:43 +0200 Subject: [PATCH] feat(PBI-59): useJobsRealtime hook met SSE-verbinding en store-updates Co-Authored-By: Claude Sonnet 4.6 --- hooks/use-jobs-realtime.ts | 79 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 hooks/use-jobs-realtime.ts diff --git a/hooks/use-jobs-realtime.ts b/hooks/use-jobs-realtime.ts new file mode 100644 index 0000000..f85b5c5 --- /dev/null +++ b/hooks/use-jobs-realtime.ts @@ -0,0 +1,79 @@ +import { useEffect } from 'react' +import { useJobsStore } from '@/stores/jobs-store' +import type { ClaudeJobStatus } from '@prisma/client' + +interface JobStatusPayload { + job_id: string + kind?: string + status: string + task_id?: string | null + idea_id?: string | null + sprint_run_id?: string | null + branch?: string + pushed_at?: string + pr_url?: string + verify_result?: string + summary?: string + error?: string +} + +export default function useJobsRealtime() { + const initJobs = useJobsStore(s => s.initJobs) + const upsertJob = useJobsStore(s => s.upsertJob) + + useEffect(() => { + let es: EventSource | null = null + let reconnectTimer: ReturnType | null = null + let active = true + + function connect() { + if (!active) return + + es = new EventSource('/api/realtime/jobs') + + es.addEventListener('jobs_initial', (event) => { + try { + const jobs = JSON.parse(event.data) + if (Array.isArray(jobs)) { + initJobs(jobs, useJobsStore.getState().doneJobs) + } + } catch { + // malformed JSON + } + }) + + es.addEventListener('message', (event) => { + try { + const payload = JSON.parse(event.data) as JobStatusPayload + if (!payload.job_id) return + upsertJob({ + id: payload.job_id, + status: payload.status as ClaudeJobStatus, + branch: payload.branch ?? null, + prUrl: payload.pr_url ?? null, + error: payload.error ?? null, + summary: payload.summary ?? null, + }) + } catch { + // malformed JSON + } + }) + + es.onerror = () => { + es?.close() + es = null + if (active) { + reconnectTimer = setTimeout(connect, 3000) + } + } + } + + connect() + + return () => { + active = false + if (reconnectTimer) clearTimeout(reconnectTimer) + es?.close() + } + }, [initJobs, upsertJob]) +}