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>
71 lines
2.3 KiB
TypeScript
71 lines
2.3 KiB
TypeScript
'use client'
|
|
|
|
import type { JobWithRelations } from '@/actions/jobs-page'
|
|
|
|
interface FieldRowProps {
|
|
label: string
|
|
children: React.ReactNode
|
|
}
|
|
|
|
function FieldRow({ label, children }: FieldRowProps) {
|
|
return (
|
|
<div className="flex gap-2 py-1.5 border-b border-border/50 text-sm">
|
|
<span className="w-32 shrink-0 text-muted-foreground">{label}</span>
|
|
<span className="flex-1 min-w-0 font-mono text-xs">{children}</span>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function formatNumber(n: number | null | undefined): string {
|
|
return n != null ? n.toLocaleString('nl-NL') : '—'
|
|
}
|
|
|
|
function formatDuration(start: Date | null, end: Date | null): string {
|
|
if (!start) return '—'
|
|
const endTime = end ? new Date(end).getTime() : Date.now()
|
|
const ms = endTime - new Date(start).getTime()
|
|
if (ms < 0) return '—'
|
|
const sec = Math.floor(ms / 1000)
|
|
if (sec < 60) return `${sec}s`
|
|
const min = Math.floor(sec / 60)
|
|
const remSec = sec % 60
|
|
if (min < 60) return `${min}m ${remSec}s`
|
|
const hr = Math.floor(min / 60)
|
|
const remMin = min % 60
|
|
return `${hr}u ${remMin}m`
|
|
}
|
|
|
|
interface JobUsagePaneProps {
|
|
job: JobWithRelations | null
|
|
}
|
|
|
|
export default function JobUsagePane({ job }: JobUsagePaneProps) {
|
|
if (!job) {
|
|
return (
|
|
<div className="flex items-center justify-center h-full text-sm text-muted-foreground">
|
|
Selecteer een job om gebruik te zien
|
|
</div>
|
|
)
|
|
}
|
|
|
|
const totalTokens =
|
|
(job.inputTokens ?? 0) +
|
|
(job.outputTokens ?? 0) +
|
|
(job.cacheReadTokens ?? 0) +
|
|
(job.cacheWriteTokens ?? 0)
|
|
|
|
const costLabel = job.costUsd != null ? `$${job.costUsd.toFixed(4)}` : '—'
|
|
|
|
return (
|
|
<div className="overflow-y-auto h-full p-4">
|
|
<FieldRow label="Model">{job.modelId ?? '—'}</FieldRow>
|
|
<FieldRow label="Tokens invoer">{formatNumber(job.inputTokens)}</FieldRow>
|
|
<FieldRow label="Tokens uitvoer">{formatNumber(job.outputTokens)}</FieldRow>
|
|
<FieldRow label="Cache read">{formatNumber(job.cacheReadTokens)}</FieldRow>
|
|
<FieldRow label="Cache write">{formatNumber(job.cacheWriteTokens)}</FieldRow>
|
|
<FieldRow label="Tokens totaal">{formatNumber(totalTokens || null)}</FieldRow>
|
|
<FieldRow label="Kosten (USD)">{costLabel}</FieldRow>
|
|
<FieldRow label="Duur">{formatDuration(job.startedAt, job.finishedAt)}</FieldRow>
|
|
</div>
|
|
)
|
|
}
|