Sprint: Jobs scherm (#209)

* refactor(jobs): extraheer job-mapper naar lib/jobs-mapper.ts + voeg breadcrumb-velden toe

Verplaatst JobWithRelations, JOB_INCLUDE, RawJob, PriceRow, pickDescription,
computeCost en mapJob naar lib/jobs-mapper.ts (zonder 'use server'). Voegt
buildPriceMap helper toe en breidt de types uit met productCode, storyCode en
pbiCode via task->story->pbi en product.code includes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(jobs): voeg GET /api/jobs/[id] route toe + tests

* feat(jobs): useJobsRealtime fetch-on-unknown met dedup-Set

Wanneer een SSE-event een onbekend job_id bevat, haalt de hook de volledige
job op via GET /api/jobs/[id] en upsert die in de store. Een inFlight-Set
voorkomt gelijktijdige dubbele fetches voor hetzelfde job_id. Bekende jobs
blijven de bestaande partial-upsert gebruiken. Zelfde logica in jobs_initial.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(jobs): JobCard breadcrumb + datum-fallback per kind

Voeg productCode/pbiCode/storyCode/startedAt/finishedAt toe aan
JobCardProps; bouw breadcrumb per job-kind en toon finishedAt → startedAt
→ createdAt als datum. JobsColumn geeft de nieuwe velden door.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(jobs): JobCard breadcrumb + datum-fallback tests

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Janpeter Visser 2026-05-15 01:25:20 +02:00 committed by GitHub
parent 3d52fe4958
commit 2a6386163c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 605 additions and 156 deletions

View file

@ -24,6 +24,22 @@ export default function useJobsRealtime() {
let es: EventSource | null = null
let reconnectTimer: ReturnType<typeof setTimeout> | null = null
let active = true
const inFlight = new Set<string>()
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
@ -34,20 +50,25 @@ export default function useJobsRealtime() {
// 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.
// 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
upsertJob({
id: p.job_id,
status: p.status as ClaudeJobStatus,
branch: p.branch ?? null,
error: p.error ?? null,
summary: p.summary ?? null,
})
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
@ -58,6 +79,12 @@ export default function useJobsRealtime() {
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,