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>
This commit is contained in:
Scrum4Me Agent 2026-05-15 00:48:18 +02:00
parent 3d52fe4958
commit 864cb81f3c
3 changed files with 168 additions and 142 deletions

View file

@ -27,6 +27,9 @@ function makeJob(status: JobWithRelations['status']): JobWithRelations {
sprintGoal: null, sprintGoal: null,
sprintCode: null, sprintCode: null,
productName: 'Scrum4Me', productName: 'Scrum4Me',
productCode: null,
storyCode: null,
pbiCode: null,
modelId: null, modelId: null,
inputTokens: null, inputTokens: null,
outputTokens: null, outputTokens: null,

View file

@ -2,146 +2,10 @@
import { prisma } from '@/lib/prisma' import { prisma } from '@/lib/prisma'
import { getSession } from '@/lib/auth' import { getSession } from '@/lib/auth'
import type { ClaudeJobKind, ClaudeJobStatus, VerifyResult } from '@prisma/client' import { JOB_INCLUDE, mapJob, buildPriceMap } from '@/lib/jobs-mapper'
import type { RawJob, JobWithRelations, PriceRow } from '@/lib/jobs-mapper'
export type JobWithRelations = { export type { JobWithRelations } from '@/lib/jobs-mapper'
id: string
kind: ClaudeJobKind
status: ClaudeJobStatus
taskCode: string | null
taskTitle: string | null
ideaCode: string | null
ideaTitle: string | null
sprintGoal: string | null
sprintCode: string | null
productName: string
modelId: string | null
inputTokens: number | null
outputTokens: number | null
cacheReadTokens: number | null
cacheWriteTokens: number | null
costUsd: number | null
branch: string | null
prUrl: string | null
error: string | null
summary: string | null
description: string | null
verifyResult: VerifyResult | null
startedAt: Date | null
finishedAt: Date | null
createdAt: Date
sprintRunId: string | null
}
const JOB_INCLUDE = {
task: { select: { code: true, title: true, description: true, implementation_plan: true } },
idea: { select: { code: true, title: true, description: true, grill_md: true, plan_md: true } },
product: { select: { name: true } },
sprint_run: { include: { sprint: { select: { sprint_goal: true, code: true } } } },
} as const
type RawJob = {
id: string
kind: ClaudeJobKind
status: ClaudeJobStatus
model_id: string | null
input_tokens: number | null
output_tokens: number | null
cache_read_tokens: number | null
cache_write_tokens: number | null
branch: string | null
pr_url: string | null
error: string | null
summary: string | null
verify_result: VerifyResult | null
started_at: Date | null
finished_at: Date | null
created_at: Date
sprint_run_id: string | null
task: {
code: string | null
title: string
description: string | null
implementation_plan: string | null
} | null
idea: {
code: string | null
title: string
description: string | null
grill_md: string | null
plan_md: string | null
} | null
product: { name: string }
sprint_run: { sprint: { sprint_goal: string; code: string } } | null
}
type PriceRow = {
model_id: string
input_price_per_1m: { toString: () => string }
output_price_per_1m: { toString: () => string }
cache_read_price_per_1m: { toString: () => string }
cache_write_price_per_1m: { toString: () => string }
}
function pickDescription(j: RawJob): string | null {
switch (j.kind) {
case 'TASK_IMPLEMENTATION':
return j.task?.implementation_plan ?? j.task?.description ?? null
case 'IDEA_GRILL':
return j.idea?.grill_md ?? j.idea?.description ?? null
case 'IDEA_MAKE_PLAN':
return j.idea?.plan_md ?? j.idea?.description ?? null
case 'PLAN_CHAT':
return j.idea?.description ?? null
case 'SPRINT_IMPLEMENTATION':
return null
default:
return null
}
}
function computeCost(j: RawJob, priceMap: Map<string, PriceRow>): number | null {
if (!j.model_id) return null
const p = priceMap.get(j.model_id)
if (!p || j.input_tokens == null) return null
return (
((j.input_tokens ?? 0) * Number(p.input_price_per_1m.toString())) / 1_000_000 +
((j.output_tokens ?? 0) * Number(p.output_price_per_1m.toString())) / 1_000_000 +
((j.cache_read_tokens ?? 0) * Number(p.cache_read_price_per_1m.toString())) / 1_000_000 +
((j.cache_write_tokens ?? 0) * Number(p.cache_write_price_per_1m.toString())) / 1_000_000
)
}
function mapJob(j: RawJob, priceMap: Map<string, PriceRow>): JobWithRelations {
return {
id: j.id,
kind: j.kind,
status: j.status,
taskCode: j.task?.code ?? null,
taskTitle: j.task?.title ?? null,
ideaCode: j.idea?.code ?? null,
ideaTitle: j.idea?.title ?? null,
sprintGoal: j.sprint_run?.sprint.sprint_goal ?? null,
sprintCode: j.sprint_run?.sprint.code ?? null,
productName: j.product.name,
modelId: j.model_id,
inputTokens: j.input_tokens,
outputTokens: j.output_tokens,
cacheReadTokens: j.cache_read_tokens,
cacheWriteTokens: j.cache_write_tokens,
costUsd: computeCost(j, priceMap),
branch: j.branch,
prUrl: j.pr_url,
error: j.error,
summary: j.summary,
description: pickDescription(j),
verifyResult: j.verify_result,
startedAt: j.started_at,
finishedAt: j.finished_at,
createdAt: j.created_at,
sprintRunId: j.sprint_run_id,
}
}
export async function fetchJobsPageData(): Promise<{ activeJobs: JobWithRelations[]; doneJobs: JobWithRelations[] } | null> { export async function fetchJobsPageData(): Promise<{ activeJobs: JobWithRelations[]; doneJobs: JobWithRelations[] } | null> {
const session = await getSession() const session = await getSession()
@ -162,10 +26,10 @@ export async function fetchJobsPageData(): Promise<{ activeJobs: JobWithRelation
prisma.modelPrice.findMany(), prisma.modelPrice.findMany(),
]) ])
const priceMap = new Map<string, PriceRow>(prices.map((p) => [p.model_id, p as unknown as PriceRow])) const priceMap = buildPriceMap(prices as unknown as PriceRow[])
return { return {
activeJobs: active.map((j) => mapJob(j as RawJob, priceMap)), activeJobs: active.map((j) => mapJob(j as unknown as RawJob, priceMap)),
doneJobs: done.map((j) => mapJob(j as RawJob, priceMap)), doneJobs: done.map((j) => mapJob(j as unknown as RawJob, priceMap)),
} }
} }

159
lib/jobs-mapper.ts Normal file
View file

@ -0,0 +1,159 @@
import type { ClaudeJobKind, ClaudeJobStatus, VerifyResult } from '@prisma/client'
export type JobWithRelations = {
id: string
kind: ClaudeJobKind
status: ClaudeJobStatus
taskCode: string | null
taskTitle: string | null
ideaCode: string | null
ideaTitle: string | null
sprintGoal: string | null
sprintCode: string | null
productName: string
productCode: string | null
storyCode: string | null
pbiCode: string | null
modelId: string | null
inputTokens: number | null
outputTokens: number | null
cacheReadTokens: number | null
cacheWriteTokens: number | null
costUsd: number | null
branch: string | null
prUrl: string | null
error: string | null
summary: string | null
description: string | null
verifyResult: VerifyResult | null
startedAt: Date | null
finishedAt: Date | null
createdAt: Date
sprintRunId: string | null
}
export const JOB_INCLUDE = {
task: {
select: {
code: true,
title: true,
description: true,
implementation_plan: true,
story: { select: { code: true, pbi: { select: { code: true } } } },
},
},
idea: { select: { code: true, title: true, description: true, grill_md: true, plan_md: true } },
product: { select: { name: true, code: true } },
sprint_run: { include: { sprint: { select: { sprint_goal: true, code: true } } } },
} as const
export type RawJob = {
id: string
kind: ClaudeJobKind
status: ClaudeJobStatus
model_id: string | null
input_tokens: number | null
output_tokens: number | null
cache_read_tokens: number | null
cache_write_tokens: number | null
branch: string | null
pr_url: string | null
error: string | null
summary: string | null
verify_result: VerifyResult | null
started_at: Date | null
finished_at: Date | null
created_at: Date
sprint_run_id: string | null
task: {
code: string | null
title: string
description: string | null
implementation_plan: string | null
story: { code: string; pbi: { code: string } } | null
} | null
idea: {
code: string | null
title: string
description: string | null
grill_md: string | null
plan_md: string | null
} | null
product: { name: string; code: string | null }
sprint_run: { sprint: { sprint_goal: string; code: string } } | null
}
export type PriceRow = {
model_id: string
input_price_per_1m: { toString: () => string }
output_price_per_1m: { toString: () => string }
cache_read_price_per_1m: { toString: () => string }
cache_write_price_per_1m: { toString: () => string }
}
export function buildPriceMap(prices: PriceRow[]): Map<string, PriceRow> {
return new Map(prices.map((p) => [p.model_id, p]))
}
export function pickDescription(j: RawJob): string | null {
switch (j.kind) {
case 'TASK_IMPLEMENTATION':
return j.task?.implementation_plan ?? j.task?.description ?? null
case 'IDEA_GRILL':
return j.idea?.grill_md ?? j.idea?.description ?? null
case 'IDEA_MAKE_PLAN':
return j.idea?.plan_md ?? j.idea?.description ?? null
case 'PLAN_CHAT':
return j.idea?.description ?? null
case 'SPRINT_IMPLEMENTATION':
return null
default:
return null
}
}
export function computeCost(j: RawJob, priceMap: Map<string, PriceRow>): number | null {
if (!j.model_id) return null
const p = priceMap.get(j.model_id)
if (!p || j.input_tokens == null) return null
return (
((j.input_tokens ?? 0) * Number(p.input_price_per_1m.toString())) / 1_000_000 +
((j.output_tokens ?? 0) * Number(p.output_price_per_1m.toString())) / 1_000_000 +
((j.cache_read_tokens ?? 0) * Number(p.cache_read_price_per_1m.toString())) / 1_000_000 +
((j.cache_write_tokens ?? 0) * Number(p.cache_write_price_per_1m.toString())) / 1_000_000
)
}
export function mapJob(j: RawJob, priceMap: Map<string, PriceRow>): JobWithRelations {
return {
id: j.id,
kind: j.kind,
status: j.status,
taskCode: j.task?.code ?? null,
taskTitle: j.task?.title ?? null,
ideaCode: j.idea?.code ?? null,
ideaTitle: j.idea?.title ?? null,
sprintGoal: j.sprint_run?.sprint.sprint_goal ?? null,
sprintCode: j.sprint_run?.sprint.code ?? null,
productName: j.product.name,
productCode: j.product.code ?? null,
storyCode: j.task?.story?.code ?? null,
pbiCode: j.task?.story?.pbi?.code ?? null,
modelId: j.model_id,
inputTokens: j.input_tokens,
outputTokens: j.output_tokens,
cacheReadTokens: j.cache_read_tokens,
cacheWriteTokens: j.cache_write_tokens,
costUsd: computeCost(j, priceMap),
branch: j.branch,
prUrl: j.pr_url,
error: j.error,
summary: j.summary,
description: pickDescription(j),
verifyResult: j.verify_result,
startedAt: j.started_at,
finishedAt: j.finished_at,
createdAt: j.created_at,
sprintRunId: j.sprint_run_id,
}
}