From e8562d40180e77de6201b8c1b54422b82db4e680 Mon Sep 17 00:00:00 2001 From: Janpeter Visser <30029041+madhura68@users.noreply.github.com> Date: Thu, 7 May 2026 16:09:17 +0200 Subject: [PATCH] Sprint: inzicht jobs (#146) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(admin/jobs): select token-velden en bereken kostprijs server-side Voegt model_id, input_tokens, output_tokens, cache_read_tokens en cache_write_tokens toe aan de ClaudeJob-query en berekent cost_usd per job via een ModelPrice-lookup. Jobs zonder prijs-entry of zonder input_tokens krijgen cost_usd: null. * feat(admin/jobs-table): toggle-buttons en view-state voor status/kosten-weergave Voegt useState toe, breidt Job-type uit met model_id en cost_usd, extraheert huidige tabellogica naar StatusTable en voegt CostsTable-stub + toggle-knoppen toe aan JobsTable. * feat(admin/jobs-table): CostRow en CostsTable voor kosten-view Voegt CostRow toe met kolommen ID/Gebruiker/Product/Type/Model/Kosten(USD)/ Aangemaakt/Acties en vervangt de CostsTable-stub door een volledige tabel. Kostprijs geformatteerd als "$0.0042"; ontbrekende prijs toont "—". --- app/(app)/admin/jobs/page.tsx | 21 +++++++- components/admin/jobs-table.tsx | 89 ++++++++++++++++++++++++++++++++- 2 files changed, 107 insertions(+), 3 deletions(-) diff --git a/app/(app)/admin/jobs/page.tsx b/app/(app)/admin/jobs/page.tsx index b1c9920..76d0825 100644 --- a/app/(app)/admin/jobs/page.tsx +++ b/app/(app)/admin/jobs/page.tsx @@ -16,15 +16,34 @@ export default async function AdminJobsPage() { branch: true, pr_url: true, error: true, + model_id: true, + input_tokens: true, + output_tokens: true, + cache_read_tokens: true, + cache_write_tokens: true, user: { select: { username: true } }, product: { select: { name: true } }, }, }) + const prices = await prisma.modelPrice.findMany() + const priceMap = new Map(prices.map((p) => [p.model_id, p])) + + const jobsWithCost = jobs.map((job) => { + const p = job.model_id ? priceMap.get(job.model_id) : undefined + if (!p || job.input_tokens == null) return { ...job, cost_usd: null } + const cost = + (job.input_tokens ?? 0) * Number(p.input_price_per_1m) / 1_000_000 + + (job.output_tokens ?? 0) * Number(p.output_price_per_1m) / 1_000_000 + + (job.cache_read_tokens ?? 0) * Number(p.cache_read_price_per_1m) / 1_000_000 + + (job.cache_write_tokens ?? 0) * Number(p.cache_write_price_per_1m) / 1_000_000 + return { ...job, cost_usd: cost } + }) + return (

Claude Jobs

- +
) } diff --git a/components/admin/jobs-table.tsx b/components/admin/jobs-table.tsx index a241549..3b1c312 100644 --- a/components/admin/jobs-table.tsx +++ b/components/admin/jobs-table.tsx @@ -1,6 +1,6 @@ 'use client' -import { useTransition } from 'react' +import { useState, useTransition } from 'react' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' import { @@ -23,6 +23,8 @@ type Job = { branch: string | null pr_url: string | null error: string | null + model_id: string | null + cost_usd: number | null } const STATUS_CLASS: Record = { @@ -92,7 +94,7 @@ function JobRow({ job }: { job: Job }) { ) } -export function JobsTable({ jobs }: { jobs: Job[] }) { +function StatusTable({ jobs }: { jobs: Job[] }) { return ( @@ -123,3 +125,86 @@ export function JobsTable({ jobs }: { jobs: Job[] }) {
) } + +function CostRow({ job }: { job: Job }) { + const [pending, startTransition] = useTransition() + function handleCancel() { startTransition(() => cancelJobAction(job.id)) } + function handleDelete() { startTransition(() => deleteJobAction(job.id)) } + const costLabel = job.cost_usd != null ? `$${job.cost_usd.toFixed(4)}` : '—' + return ( + + {job.id.slice(0, 8)} + {job.user.username} + {job.product.name} + {KIND_LABEL[job.kind] ?? job.kind} + {job.model_id ?? '—'} + {costLabel} + + {new Date(job.created_at).toLocaleString('nl-NL', { dateStyle: 'short', timeStyle: 'short' })} + + +
+ {ACTIVE_STATUSES.has(job.status) && ( + + )} + +
+
+
+ ) +} + +function CostsTable({ jobs }: { jobs: Job[] }) { + return ( + + + + ID + Gebruiker + Product + Type + Model + Kosten (USD) + Aangemaakt + Acties + + + + {jobs.length === 0 && ( + + + Geen jobs gevonden + + + )} + {jobs.map((job) => )} + +
+ ) +} + +export function JobsTable({ jobs }: { jobs: Job[] }) { + const [view, setView] = useState<'status' | 'costs'>('status') + + return ( +
+
+ + +
+ {view === 'status' ? : } +
+ ) +}