HTML 77.2%
TypeScript 10.5%
Python 9.6%
JavaScript 2.5%
1import type { Metadata } from 'next';2import { AdminFilters, AdminTitle, Mono, StatusChip, Trunc } from '@/components/admin/ui';3import { DataTable, EmptyRow, Td, Th } from '@/components/ui/data-table';4import { LiveAgo } from '@/components/ui/live';5import { Unavailable } from '@/components/ui/unavailable';6import { adminApi, load, requireAdmin } from '@/lib/admin/admin-api';7import { fmtDateTime, fmtDuration, fmtInt, num } from '@/lib/format';89export const metadata: Metadata = { title: 'LLM jobs', robots: { index: false, follow: false } };10export const dynamic = 'force-dynamic';1112export default async function AdminLlmJobsPage({ searchParams }: { searchParams: Promise<Record<string, string | undefined>> }) {13 await requireAdmin();14 const sp = await searchParams;15 const limit = Math.min(1000, Math.max(10, Number(sp.limit) || 100));16 const [jobs, health] = await Promise.all([load(adminApi.llmJobs({ limit })), load(adminApi.llmHealth())]);17 const ms = (v: unknown) => (num(v) === null ? '—' : `${fmtInt(Math.round((num(v) ?? 0) / 1000))} s`);18 return (19 <>20 <AdminTitle21 title="LLM jobs"22 count={jobs.ok ? fmtInt(jobs.data.total) : undefined}23 lede={24 health.ok ? (25 <span className="inline-flex flex-wrap items-center gap-x-2">26 <StatusChip value={health.data.reachable === false ? 'failing' : health.data.available ? 'ok' : 'disabled'} />27 <span>engine {health.data.engine ?? '—'}</span>28 {health.data.models && <span>· {Object.entries(health.data.models).map(([k, v]) => `${k}: ${v}`).join(' · ')}</span>}29 {health.data.embedding_model && <span>· embeddings {health.data.embedding_model}</span>}30 </span>31 ) : (32 `LLM health unavailable: ${health.error}`33 )34 }35 />36 <AdminFilters action="/admin/llm-jobs" className="mb-4" fields={[{ kind: 'select', name: 'limit', label: 'Rows', value: String(limit), any: '100', options: ['50', '100', '250', '500', '1000'].map((v) => ({ value: v, label: v })) }]} />37 {!jobs.ok ? (38 <Unavailable what="LLM jobs" reason={jobs.error} />39 ) : (40 <>41 {jobs.data.totals && jobs.data.totals.length > 0 && (42 <section className="mb-6">43 <p className="eyebrow mb-2">Totals by stage · model · status</p>44 <DataTable compact scroll caption="LLM totals">45 <thead>46 <tr>47 <Th>Stage</Th>48 <Th>Model</Th>49 <Th>Status</Th>50 <Th num>n</Th>51 <Th num>Input tokens</Th>52 <Th num>Output tokens</Th>53 <Th num>Avg duration</Th>54 </tr>55 </thead>56 <tbody>57 {jobs.data.totals.map((t, i) => (58 <tr key={i}>59 <Td><Mono className="text-ink">{t.stage}</Mono></Td>60 <Td><Mono>{t.model}</Mono></Td>61 <Td><StatusChip value={t.status} /></Td>62 <Td num className="tnum text-xs">{fmtInt(t.n)}</Td>63 <Td num className="tnum text-xs">{fmtInt(t.input_tokens)}</Td>64 <Td num className="tnum text-xs">{fmtInt(t.output_tokens)}</Td>65 <Td num className="tnum text-xs">{ms(t.avg_ms)}</Td>66 </tr>67 ))}68 </tbody>69 </DataTable>70 </section>71 )}72 <DataTable compact scroll caption="LLM job accounting">73 <thead>74 <tr>75 <Th>Created</Th>76 <Th>Task</Th>77 <Th>Stage</Th>78 <Th>Model</Th>79 <Th>Node</Th>80 <Th>Schema</Th>81 <Th num>In</Th>82 <Th num>Out</Th>83 <Th num>Duration</Th>84 <Th>Status</Th>85 <Th>Error</Th>86 <Th>Snapshot</Th>87 </tr>88 </thead>89 <tbody>90 {jobs.data.items.length === 0 && <EmptyRow cols={12}>No LLM jobs recorded.</EmptyRow>}91 {jobs.data.items.map((j) => (92 <tr key={j.id}>93 <Td className="text-xs" title={fmtDateTime(j.created_at)}><LiveAgo at={j.created_at} /></Td>94 <Td><Mono className="text-ink">{j.task_type ?? '—'}</Mono></Td>95 <Td><Mono>{j.stage ?? '—'}</Mono></Td>96 <Td><Mono>{j.model ?? '—'}</Mono></Td>97 <Td className="text-xs text-ink-2"><Trunc text={j.node} max={24} /></Td>98 <Td><Mono>{j.schema_name ?? '—'}</Mono></Td>99 <Td num className="tnum text-xs">{fmtInt(j.input_tokens)}</Td>100 <Td num className="tnum text-xs">{fmtInt(j.output_tokens)}</Td>101 <Td num className="tnum text-xs">{num(j.duration_ms) === null ? '—' : fmtDuration(Math.round((num(j.duration_ms) ?? 0) / 1000))}</Td>102 <Td><StatusChip value={j.status} /></Td>103 <Td className="text-xs text-danger"><Trunc text={j.error} max={50} /></Td>104 <Td>{j.snapshot_id ? <a href={`/admin/snapshots/${encodeURIComponent(j.snapshot_id)}`} className="mono text-[11.5px] text-ink-2 hover:text-accent">{j.snapshot_id}</a> : <span className="text-ink-3">—</span>}</Td>105 </tr>106 ))}107 </tbody>108 </DataTable>109 </>110 )}111 </>112 );113}114