Scrum4Me/components/jobs/job-usage-pane.tsx
Janpeter Visser 16f01283ef
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>
2026-05-07 19:51:53 +02:00

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>
)
}