import { useEffect, useState, type FC } from "react" import { archerAdminApi } from "@/api/archer_admin" import type { AdminListArcherJobsRequest } from "@/sdk" import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query" import { Loader2, Plus, RefreshCw, Search, X } from "lucide-react" import { cn } from "@/lib/utils" import { Badge } from "@/components/ui/badge" import { Button } from "@/components/ui/button" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import { Dialog, DialogClose, DialogContent, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, } from "@/components/ui/dialog" import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select" import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table" import { Textarea } from "@/components/ui/textarea" // Types (align with generated client camelCase) type JobStatus = | "queued" | "running" | "succeeded" | "failed" | "canceled" | "retrying" | "scheduled" type DtoJob = { id: string type: string queue: string status: JobStatus attempts: number maxAttempts?: number createdAt: string updatedAt?: string lastError?: string | null runAt?: string | null payload?: unknown } type DtoPageJob = { items: DtoJob[] total: number page: number pageSize: number } type QueueInfo = { name: string pending: number running: number failed: number scheduled: number } const STATUS: JobStatus[] = [ "queued", "running", "succeeded", "failed", "canceled", "retrying", "scheduled", ] const statusClass: Record = { queued: "bg-amber-100 text-amber-800", running: "bg-sky-100 text-sky-800", succeeded: "bg-emerald-100 text-emerald-800", failed: "bg-red-100 text-red-800", canceled: "bg-zinc-200 text-zinc-700", retrying: "bg-orange-100 text-orange-800", scheduled: "bg-violet-100 text-violet-800", } function fmt(dt?: string | null) { if (!dt) return "—" const d = new Date(dt) return new Intl.DateTimeFormat(undefined, { dateStyle: "medium", timeStyle: "short" }).format(d) } // Small debounce hook for search input function useDebounced(value: T, ms = 300) { const [v, setV] = useState(value) useEffect(() => { const t = setTimeout(() => setV(value), ms) return () => clearTimeout(t) }, [value, ms]) return v } export const JobsPage: FC = () => { const qc = useQueryClient() // Filters const [status, setStatus] = useState("") const [queue, setQueue] = useState("") const [q, setQ] = useState("") const debouncedQ = useDebounced(q, 300) const [page, setPage] = useState(1) const [pageSize, setPageSize] = useState(25) const key = ["archer", "jobs", { status, queue, q: debouncedQ, page, pageSize }] // Jobs query const jobsQ = useQuery({ queryKey: key, queryFn: () => archerAdminApi.listJobs({ status: status, queue: queue || undefined, q: debouncedQ || undefined, page, pageSize, } as AdminListArcherJobsRequest) as Promise, placeholderData: (prev) => prev, staleTime: 10_000, }) // Queues summary (optional header) const queuesQ = useQuery({ queryKey: ["archer", "queues"], queryFn: () => archerAdminApi.listQueues() as Promise, staleTime: 30_000, }) // Mutations const enqueueM = useMutation({ mutationFn: (body: { queue: string; type: string; payload?: unknown; run_at?: string }) => archerAdminApi.enqueue(body), onSuccess: () => qc.invalidateQueries({ queryKey: ["archer", "jobs"] }), }) const retryM = useMutation({ mutationFn: (id: string) => archerAdminApi.retryJob(id), onSuccess: () => qc.invalidateQueries({ queryKey: ["archer", "jobs"] }), }) const cancelM = useMutation({ mutationFn: (id: string) => archerAdminApi.cancelJob(id), onSuccess: () => qc.invalidateQueries({ queryKey: ["archer", "jobs"] }), }) const busy = jobsQ.isFetching const data = jobsQ.data as DtoPageJob const totalPages = data ? Math.max(1, Math.ceil(data.total / data.pageSize)) : 1 return (

Archer Jobs

Inspect, enqueue, retry and cancel background jobs.

enqueueM.mutateAsync(payload)} submitting={enqueueM.isPending} />
{/* Queue metrics (optional) */}
{queuesQ.data?.map((q) => ( {q.name} ))}
{/* Filters */} Filters
{ setQ(e.target.value) setPage(1) }} onKeyDown={(e) => e.key === "Enter" && qc.invalidateQueries({ queryKey: ["archer", "jobs"] }) } /> {q && ( )}
{ setQueue(e.target.value) setPage(1) }} />
{/* Table */} ID Queue Status Attempts Run At Updated Actions {jobsQ.isLoading && ( Loading… )} {jobsQ.isError && ( Failed to load jobs )} {!jobsQ.isLoading && data && data.items.length === 0 && ( No jobs match your filters. )} {data?.items.map((j) => ( {j.id} {j.queue} {j.status} {j.maxAttempts ? `${j.attempts}/${j.maxAttempts}` : j.attempts} {fmt(j.runAt)} {fmt(j.updatedAt ?? j.createdAt)}
{(j.status === "failed" || j.status === "canceled") && ( )} {(j.status === "queued" || j.status === "running" || j.status === "scheduled") && ( )}
))}
{/* Pagination */}
Page {page} of {totalPages} • {data?.total ?? 0} total
) } function Metric({ label, value }: { label: string; value: number }) { return (
{label}
{value}
) } function DetailsButton({ job }: { job: DtoJob }) { return ( Job {job.id}
{job.lastError && ( Last error
{job.lastError}
)} Payload
                {JSON.stringify(job.payload, null, 2)}
              
) } function EnqueueDialog({ onSubmit, submitting, }: { onSubmit: (body: { queue: string type: string payload?: unknown run_at?: string }) => Promise submitting?: boolean }) { const [open, setOpen] = useState(false) const [queue, setQueue] = useState("") const [type, setType] = useState("") const [payload, setPayload] = useState("{}") const [runAt, setRunAt] = useState("") const canSubmit = queue && type && !submitting async function handleSubmit() { const parsed = payload ? JSON.parse(payload) : undefined await onSubmit({ queue, type, payload: parsed, run_at: runAt || undefined }) setOpen(false) setQueue("") setType("") setPayload("{}") setRunAt("") } return ( Enqueue Job
setQueue(e.target.value)} placeholder="e.g. bootstrap_bastion" />
setType(e.target.value)} placeholder="e.g. bootstrap_bastion" />