Repository Public
1import { AutoRefresh } from "@/components/studio/AutoRefresh";2import { A, PageHead, Panel, Status, ago, usd } from "@/components/studio/ui";3import { requireRole } from "@/lib/auth";4import { sql } from "@/lib/db";5import { queueDepth } from "@/lib/queue";67export const metadata = { title: "Jobs" };8export default async function Jobs() {9 await requireRole("viewer");10 const [depth, runs, jobs] = await Promise.all([11 queueDepth(),12 sql<{ id: string; project_id: string; title: string; stage: string; status: string; attempts: number; error: string | null; queued_at: Date; started_at: Date | null; finished_at: Date | null }[]>`13 SELECT r.*, p.title FROM stage_runs r JOIN projects p ON p.id = r.project_id ORDER BY r.queued_at DESC LIMIT 60`,14 sql<{ id: string; project_id: string | null; title: string | null; stage: string; kind: string; model: string; status: string; attempts: number; error: string | null; est_usd: number | null; cost_usd: number | null; request_id: string | null; created_at: Date; started_at: Date | null; finished_at: Date | null }[]>`15 SELECT j.*, p.title FROM provider_jobs j LEFT JOIN projects p ON p.id = j.project_id ORDER BY j.created_at DESC LIMIT 120`,16 ]);17 const live = runs.some((r) => r.status === "queued" || r.status === "running");18 const dur = (a: Date | null, b: Date | null) => (a && b ? `${((b.getTime() - a.getTime()) / 1000).toFixed(1)} s` : "—");19 return (20 <>21 <AutoRefresh seconds={5} active={live} />22 <PageHead title="Jobs" sub={depth ? `Queue: ${depth.active} active · ${depth.waiting} waiting · ${depth.delayed} delayed (retry back-off) · ${depth.failed} failed kept for inspection.` : "The queue (Redis) is unreachable."} />23 <Panel title="Pipeline stages (durable queue)">24 <div className="overflow-x-auto"><table className="studio-table"><thead><tr><th>Queued</th><th>Project</th><th>Stage</th><th>State</th><th>Attempts</th><th>Duration</th><th>Error</th></tr></thead><tbody>25 {runs.map((r) => <tr key={r.id}><td className="whitespace-nowrap text-[var(--color-muted)]">{ago(r.queued_at)}</td><td><A href={`/studio/projects/${r.project_id}`}>{r.title}</A></td><td>{r.stage}</td><td><Status value={r.status} /></td><td className="tabular-nums">{r.attempts || "—"}</td><td className="tabular-nums">{dur(r.started_at, r.finished_at)}</td><td className="max-w-md break-words text-xs text-[var(--color-bad)]">{r.error}</td></tr>)}26 {!runs.length && <tr><td colSpan={7} className="text-[var(--color-faint)]">No stage has run yet.</td></tr>}27 </tbody></table></div>28 </Panel>29 <div className="mt-5"><Panel title="Provider calls">30 <div className="overflow-x-auto"><table className="studio-table"><thead><tr><th>Created</th><th>Project</th><th>Stage</th><th>Kind · model</th><th>State</th><th>Tries</th><th>Duration</th><th>Est.</th><th>Actual</th><th>Error</th></tr></thead><tbody>31 {jobs.map((x) => <tr key={x.id}><td className="whitespace-nowrap text-[var(--color-muted)]">{ago(x.created_at)}</td><td>{x.project_id ? <A href={`/studio/projects/${x.project_id}`}>{x.title ?? "project"}</A> : "—"}</td><td>{x.stage}</td><td className="mono">{x.kind} · {x.model}</td><td><Status value={x.status} /></td><td className="tabular-nums">{x.attempts}</td><td className="tabular-nums">{dur(x.started_at, x.finished_at)}</td><td className="tabular-nums">{usd(x.est_usd, 3)}</td><td className="tabular-nums">{x.cost_usd === null ? "—" : usd(x.cost_usd, 4)}</td><td className="max-w-xs break-words text-xs text-[var(--color-bad)]">{x.error}</td></tr>)}32 {!jobs.length && <tr><td colSpan={10} className="text-[var(--color-faint)]">No provider call yet.</td></tr>}33 </tbody></table></div>34 </Panel></div>35 </>36 );37}38