feat(PBI-59): add Detail/Usage view-switch on /jobs (#152)
Splits het middenpaneel van de jobs-pagina in twee views (zoals admin/jobs):
- **Detail** — alle metadata (status, kind, product, branch, PR, dates,
errors, summary, verify-result) plus een kind-aware beschrijving:
TASK → implementation_plan, IDEA_GRILL → grill_md, IDEA_MAKE_PLAN →
plan_md, PLAN_CHAT → idea.description.
- **Usage** — model, tokens (in/uit/cache/totaal), berekende kosten in
USD via ModelPrice-tabel, en duur (started→finished).
SprintSubTasksPane blijft als sticky header boven beide views.
Server action `fetchJobsPageData` haalt nu ook ModelPrices op en
selecteert task.{description,implementation_plan} +
idea.{description,grill_md,plan_md} zodat de description en costUsd in
JobWithRelations gevuld kunnen worden.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
a7e9ca1c35
commit
16f01283ef
4 changed files with 207 additions and 22 deletions
|
|
@ -20,10 +20,12 @@ export type JobWithRelations = {
|
|||
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
|
||||
|
|
@ -32,13 +34,13 @@ export type JobWithRelations = {
|
|||
}
|
||||
|
||||
const JOB_INCLUDE = {
|
||||
task: { select: { code: true, title: true } },
|
||||
idea: { select: { code: true, title: true } },
|
||||
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 } } } },
|
||||
} as const
|
||||
|
||||
function mapJob(j: {
|
||||
type RawJob = {
|
||||
id: string
|
||||
kind: ClaudeJobKind
|
||||
status: ClaudeJobStatus
|
||||
|
|
@ -56,11 +58,61 @@ function mapJob(j: {
|
|||
finished_at: Date | null
|
||||
created_at: Date
|
||||
sprint_run_id: string | null
|
||||
task: { code: string | null; title: string } | null
|
||||
idea: { code: string | null; title: 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 } } | null
|
||||
}): JobWithRelations {
|
||||
}
|
||||
|
||||
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,
|
||||
|
|
@ -77,10 +129,12 @@ function mapJob(j: {
|
|||
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,
|
||||
|
|
@ -93,7 +147,7 @@ export async function fetchJobsPageData(): Promise<{ activeJobs: JobWithRelation
|
|||
const session = await getSession()
|
||||
if (!session.userId) return null
|
||||
|
||||
const [active, done] = await Promise.all([
|
||||
const [active, done, prices] = await Promise.all([
|
||||
prisma.claudeJob.findMany({
|
||||
where: { user_id: session.userId, status: { notIn: ['DONE'] } },
|
||||
include: JOB_INCLUDE,
|
||||
|
|
@ -105,10 +159,13 @@ export async function fetchJobsPageData(): Promise<{ activeJobs: JobWithRelation
|
|||
orderBy: { finished_at: 'desc' },
|
||||
take: 100,
|
||||
}),
|
||||
prisma.modelPrice.findMany(),
|
||||
])
|
||||
|
||||
const priceMap = new Map<string, PriceRow>(prices.map((p) => [p.model_id, p as unknown as PriceRow]))
|
||||
|
||||
return {
|
||||
activeJobs: active.map(mapJob),
|
||||
doneJobs: done.map(mapJob),
|
||||
activeJobs: active.map((j) => mapJob(j as RawJob, priceMap)),
|
||||
doneJobs: done.map((j) => mapJob(j as RawJob, priceMap)),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue