feat(PBI-59): Jobs-pagina UI (vervolg na #149) (#150)

* 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>
This commit is contained in:
Janpeter Visser 2026-05-07 19:16:20 +02:00 committed by GitHub
parent 4a63b4b01f
commit f166186374
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 327 additions and 0 deletions

View file

@ -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<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])
}