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 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) => { // De server stuurt JobPayload[] (met `job_id`), niet JobWithRelations[]. // Daarom geen initJobs-overwrite — de SSR-fetch heeft de volledige // shape al in de store geplaatst. We reconcileren alleen status/branch // van bekende jobs en pushen onbekende jobs (nieuw aangemaakt tussen // SSR en SSE-connect) als partials. try { const payload = JSON.parse(event.data) if (!Array.isArray(payload)) return for (const p of payload as JobStatusPayload[]) { if (!p.job_id) continue upsertJob({ id: p.job_id, status: p.status as ClaudeJobStatus, branch: p.branch ?? null, error: p.error ?? null, summary: p.summary ?? null, }) } } 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() } }, [upsertJob]) }