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:
parent
3d52fe4958
commit
864cb81f3c
3 changed files with 168 additions and 142 deletions
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
159
lib/jobs-mapper.ts
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue