From 864cb81f3cdaa59095d19e52aece1de8c16bc0bc Mon Sep 17 00:00:00 2001 From: Scrum4Me Agent <30029041+madhura68@users.noreply.github.com> Date: Fri, 15 May 2026 00:48:18 +0200 Subject: [PATCH] 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 --- .../components/jobs/job-detail-pane.test.tsx | 3 + actions/jobs-page.ts | 148 +--------------- lib/jobs-mapper.ts | 159 ++++++++++++++++++ 3 files changed, 168 insertions(+), 142 deletions(-) create mode 100644 lib/jobs-mapper.ts diff --git a/__tests__/components/jobs/job-detail-pane.test.tsx b/__tests__/components/jobs/job-detail-pane.test.tsx index 9e51f87..9a5d0f6 100644 --- a/__tests__/components/jobs/job-detail-pane.test.tsx +++ b/__tests__/components/jobs/job-detail-pane.test.tsx @@ -27,6 +27,9 @@ function makeJob(status: JobWithRelations['status']): JobWithRelations { sprintGoal: null, sprintCode: null, productName: 'Scrum4Me', + productCode: null, + storyCode: null, + pbiCode: null, modelId: null, inputTokens: null, outputTokens: null, diff --git a/actions/jobs-page.ts b/actions/jobs-page.ts index 271a187..22876a5 100644 --- a/actions/jobs-page.ts +++ b/actions/jobs-page.ts @@ -2,146 +2,10 @@ import { prisma } from '@/lib/prisma' 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 = { - 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): 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): 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 type { JobWithRelations } from '@/lib/jobs-mapper' export async function fetchJobsPageData(): Promise<{ activeJobs: JobWithRelations[]; doneJobs: JobWithRelations[] } | null> { const session = await getSession() @@ -162,10 +26,10 @@ export async function fetchJobsPageData(): Promise<{ activeJobs: JobWithRelation prisma.modelPrice.findMany(), ]) - const priceMap = new Map(prices.map((p) => [p.model_id, p as unknown as PriceRow])) + const priceMap = buildPriceMap(prices as unknown as PriceRow[]) return { - activeJobs: active.map((j) => mapJob(j as RawJob, priceMap)), - doneJobs: done.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 unknown as RawJob, priceMap)), } } diff --git a/lib/jobs-mapper.ts b/lib/jobs-mapper.ts new file mode 100644 index 0000000..e9746cd --- /dev/null +++ b/lib/jobs-mapper.ts @@ -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 { + 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): 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): 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, + } +}