SPB Git
2.9 KB · 21 lines tsx
1import { sql } from "@/db";2import { stats } from "@/lib/jobs/queue";3import { fmtDateTime } from "@/lib/utils";4import { EnqueueButton } from "@/components/admin/enqueue-button";5import { JobRowActions } from "@/components/admin/job-actions";6export default async function Jobs({ searchParams }: { searchParams: Promise<{ status?: string }> }) {7  const { status } = await searchParams;8  const rows = await sql`SELECT id, kind, payload, status, attempts, max_attempts AS "maxAttempts", run_at AS "runAt", finished_at AS "finishedAt", last_error AS "lastError", result FROM jobs ${status ? sql`WHERE status=${status}` : sql``} ORDER BY id DESC LIMIT 150`;9  const st = await stats();10  const [lastBackup] = await sql`SELECT finished_at AS "finishedAt", result FROM jobs WHERE kind='backup' AND status='succeeded' ORDER BY finished_at DESC LIMIT 1`;11  return (12    <div className="space-y-6">13      <div><h1 className="display h1">Jobs & crawler</h1><p className="mt-1 text-sm text-fg-2">Durable queue in PostgreSQL. Schedule (America/Toronto): link sweep 03:10 daily · freshness 03:40 · embeddings 04:00 (if a provider is configured) · encrypted backup 04:20. Retries use exponential backoff; after the maximum attempts a job is marked dead.</p></div>14      <div className="flex flex-wrap gap-2"><EnqueueButton kind="link_sweep" label="Link sweep" /><EnqueueButton kind="freshness_sweep" label="Freshness sweep" /><EnqueueButton kind="embed_sweep" label="Embedding sweep" /><EnqueueButton kind="backup" label="Backup now" /></div>15      <p className="text-sm text-fg-2">Last successful backup: {lastBackup ? `${fmtDateTime(lastBackup.finishedAt)} · ${(lastBackup.result as any)?.bytes ?? "?"} bytes${(lastBackup.result as any)?.remote ? ` · remote ${(lastBackup.result as any).remote}` : ""}` : "none recorded"}</p>16      <div className="flex flex-wrap gap-2 text-xs">{st.map((s) => <span key={s.status + s.kind} className="chip">{s.kind} · {s.status}: {s.n}</span>)}</div>17      <div className="overflow-x-auto rounded-2xl border border-line bg-bg-2"><table className="table"><thead><tr><th>#</th><th>Kind</th><th>Payload</th><th>Status</th><th>Attempts</th><th>Run at</th><th>Finished</th><th>Result / error</th><th></th></tr></thead><tbody>{rows.map((j) => <tr key={j.id}><td>{j.id}</td><td>{j.kind}</td><td className="max-w-[12rem] truncate font-mono text-xs">{JSON.stringify(j.payload)}</td><td><span className={`chip ${j.status === "dead" ? "border-bad/40 text-bad" : j.status === "succeeded" ? "border-ok/40 text-ok" : ""}`}>{j.status}</span></td><td>{j.attempts}/{j.maxAttempts}</td><td className="text-fg-3">{fmtDateTime(j.runAt)}</td><td className="text-fg-3">{fmtDateTime(j.finishedAt)}</td><td className="max-w-md truncate font-mono text-xs" title={j.lastError ?? JSON.stringify(j.result)}>{j.lastError ? j.lastError.split("\n")[0] : JSON.stringify(j.result)}</td><td><JobRowActions id={j.id} status={j.status} /></td></tr>)}</tbody></table></div>18    </div>19  );20}21
Files · main
Menu

Theme