HTML 77.2%
TypeScript 10.5%
Python 9.6%
JavaScript 2.5%
1import type { Metadata } from 'next';2import { ActionButton, AdminFilters, AdminTitle, Mono, Notice, 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 { requeueDeadAction, retryJobAction } from '@/lib/admin/actions';7import { adminApi, load, requireAdmin } from '@/lib/admin/admin-api';8import { fmtDateTime, fmtInt } from '@/lib/format';910export const metadata: Metadata = { title: 'Jobs', robots: { index: false, follow: false } };11export const dynamic = 'force-dynamic';1213const STATUSES = ['queued', 'running', 'done', 'failed', 'dead'];1415function payloadSummary(p: Record<string, unknown>): string {16 return Object.entries(p ?? {})17 .filter(([, v]) => v !== null && v !== undefined && v !== '')18 .map(([k, v]) => `${k}=${typeof v === 'object' ? JSON.stringify(v) : String(v)}`)19 .join(' · ');20}2122export default async function AdminJobsPage({ searchParams }: { searchParams: Promise<Record<string, string | undefined>> }) {23 await requireAdmin();24 const sp = await searchParams;25 const ret = `/admin/jobs${sp.status || sp.kind ? `?${new URLSearchParams(Object.fromEntries(Object.entries({ status: sp.status, kind: sp.kind }).filter(([, v]) => v)) as Record<string, string>).toString()}` : ''}`;26 const res = await load(adminApi.jobs({ status: sp.status, kind: sp.kind, limit: 200 }));27 const kinds = res.ok ? Object.keys(res.data.depth ?? {}) : [];28 return (29 <>30 <AdminTitle title="Jobs" count={res.ok ? fmtInt(res.data.total) : undefined}>31 <form action={requeueDeadAction}>32 <input type="hidden" name="return" value={ret} />33 <ActionButton tone="accent" title="Move every dead job back to the queue">Requeue dead</ActionButton>34 </form>35 </AdminTitle>36 <Notice notice={sp.notice} level={sp.level} />37 {res.ok && res.data.depth && Object.keys(res.data.depth).length > 0 && (38 <ul className="mb-4 flex flex-wrap gap-x-4 gap-y-1 text-xs">39 {Object.entries(res.data.depth).map(([kind, byStatus]) => (40 <li key={kind} className="flex flex-wrap items-center gap-1.5">41 <Mono className="text-ink">{kind}</Mono>42 {Object.entries(byStatus).map(([st, n]) => (43 <a key={st} href={`/admin/jobs?kind=${encodeURIComponent(kind)}&status=${encodeURIComponent(st)}`} className="inline-flex items-center gap-1 hover:underline">44 <StatusChip value={st} /> <span className="tnum text-ink-2">{fmtInt(n)}</span>45 </a>46 ))}47 </li>48 ))}49 </ul>50 )}51 <AdminFilters52 action="/admin/jobs"53 className="mb-4"54 fields={[55 { kind: 'select', name: 'kind', label: 'Kind', value: sp.kind, options: (kinds.length ? kinds : ['llm_extract', 'reprocess_snapshot']).map((k) => ({ value: k, label: k })) },56 { kind: 'select', name: 'status', label: 'Status', value: sp.status, options: STATUSES.map((s) => ({ value: s, label: s })) },57 ]}58 />59 {!res.ok ? (60 <Unavailable what="Jobs" reason={res.error} />61 ) : (62 <DataTable compact scroll caption="Job queue">63 <thead>64 <tr>65 <Th>Job</Th>66 <Th>Kind</Th>67 <Th>Status</Th>68 <Th num>Prio</Th>69 <Th num>Attempts</Th>70 <Th>Run after</Th>71 <Th>Locked by</Th>72 <Th>Started</Th>73 <Th>Finished</Th>74 <Th>Error</Th>75 <Th>Action</Th>76 </tr>77 </thead>78 <tbody>79 {res.data.items.length === 0 && <EmptyRow cols={11}>No jobs match.</EmptyRow>}80 {res.data.items.map((j) => (81 <tr key={j.id}>82 <Td><Mono title={payloadSummary(j.payload)}>{j.id}</Mono></Td>83 <Td><Mono className="text-ink">{j.kind}</Mono></Td>84 <Td><StatusChip value={j.status} /></Td>85 <Td num className="tnum text-xs">{fmtInt(j.priority)}</Td>86 <Td num className="tnum text-xs">{fmtInt(j.attempts)} / {fmtInt(j.max_attempts)}</Td>87 <Td className="text-xs text-ink-2" title={fmtDateTime(j.run_after)}>{j.run_after ? <LiveAgo at={j.run_after} /> : '—'}</Td>88 <Td className="text-xs text-ink-2"><Trunc text={j.locked_by} max={28} /></Td>89 <Td className="text-xs" title={fmtDateTime(j.started_at)}>{j.started_at ? <LiveAgo at={j.started_at} /> : <span className="text-ink-3">—</span>}</Td>90 <Td className="text-xs" title={fmtDateTime(j.finished_at)}>{j.finished_at ? <LiveAgo at={j.finished_at} /> : <span className="text-ink-3">—</span>}</Td>91 <Td className="text-xs text-danger"><Trunc text={j.error} max={60} /></Td>92 <Td>93 {(j.status === 'failed' || j.status === 'dead') && (94 <form action={retryJobAction}>95 <input type="hidden" name="id" value={j.id} />96 <input type="hidden" name="return" value={ret} />97 <ActionButton tone="accent">Retry</ActionButton>98 </form>99 )}100 </Td>101 </tr>102 ))}103 </tbody>104 </DataTable>105 )}106 </>107 );108}109