* feat(PBI-58): Vitest-tests voor SoloTaskCard veldmapping en 4-regels layout Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(PBI-59): server action fetchJobsPageData voor jobs-pagina Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(PBI-59): SSE-route /api/realtime/jobs voor user-scoped job-events Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(PBI-59): JobCard component voor jobs-pagina Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(PBI-59): JobDetailPane component voor jobs-pagina Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(PBI-59): API route GET /api/jobs/[id]/sub-tasks voor sprint task executions Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(PBI-59): SprintSubTasksPane component voor jobs-pagina Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(PBI-59): Zustand store useJobsStore voor jobs-pagina Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(PBI-59): useJobsRealtime hook met SSE-verbinding en store-updates Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(PBI-59): JobsBoard 3-kolom SplitPane client component Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(PBI-59): /jobs server page met JobsBoard Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(PBI-59): Jobs nav-link toevoegen aan NavBar Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
79 lines
1.9 KiB
TypeScript
79 lines
1.9 KiB
TypeScript
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<typeof setTimeout> | 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])
|
|
}
|