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 const inFlight = new Set() async function fetchAndUpsert(jobId: string) { if (inFlight.has(jobId)) return inFlight.add(jobId) try { const res = await fetch(`/api/jobs/${jobId}`) if (!res.ok) return const job = await res.json() if (active) upsertJob(job) } catch { // netwerk-/parse-fout: stil } finally { inFlight.delete(jobId) } } 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 fetchen onbekende jobs volledig via REST. try { const payload = JSON.parse(event.data) if (!Array.isArray(payload)) return const { activeJobs, doneJobs } = useJobsStore.getState() for (const p of payload as JobStatusPayload[]) { if (!p.job_id) continue const known = activeJobs.some(j => j.id === p.job_id) || doneJobs.some(j => j.id === p.job_id) if (!known) { void fetchAndUpsert(p.job_id) } else { 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 const { activeJobs, doneJobs } = useJobsStore.getState() const known = activeJobs.some(j => j.id === payload.job_id) || doneJobs.some(j => j.id === payload.job_id) if (!known) { void fetchAndUpsert(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]) }