import { useEffect, useState, type FC } from "react" import { archerAdminApi } from "@/api/archer_admin" import { AdminListArcherJobsStatusEnum, type AdminListArcherJobsRequest, type DtoJob, type DtoPageJob, type DtoQueueInfo, } 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" type JobStatus = AdminListArcherJobsStatusEnum const STATUS: JobStatus[] = [ AdminListArcherJobsStatusEnum.queued, AdminListArcherJobsStatusEnum.running, AdminListArcherJobsStatusEnum.succeeded, AdminListArcherJobsStatusEnum.failed, AdminListArcherJobsStatusEnum.canceled, AdminListArcherJobsStatusEnum.retrying, AdminListArcherJobsStatusEnum.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 ? (status as JobStatus) : undefined, queue: queue || undefined, q: debouncedQ || undefined, page, pageSize, } as AdminListArcherJobsRequest), 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?: object | undefined 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 | undefined const items: DtoJob[] = data?.items ?? [] const total = data?.total ?? 0 const totalPages = Math.max(1, Math.ceil(total / pageSize)) 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 && items.length === 0 && ( No jobs match your filters. )} {items.map((j) => { const jobStatus: JobStatus = (j.status as JobStatus | undefined) ?? AdminListArcherJobsStatusEnum.queued return ( {j.id} {j.queue} {jobStatus} {j.max_attempts ? `${j.attempts}/${j.max_attempts}` : j.attempts} {fmt(j.run_at)} {fmt(j.updated_at ?? j.created_at)}
{(jobStatus === AdminListArcherJobsStatusEnum.failed || jobStatus === AdminListArcherJobsStatusEnum.canceled) && ( )} {(jobStatus === AdminListArcherJobsStatusEnum.queued || jobStatus === AdminListArcherJobsStatusEnum.running || jobStatus === AdminListArcherJobsStatusEnum.scheduled) && ( )}
) })}
{/* Pagination */}
Page {page} of {totalPages} • {total} total
) } function Metric({ label, value }: { label: string; value: number }) { return (
{label}
{value}
) } function DetailsButton({ job }: { job: DtoJob }) { return ( Job {job.id}
{job.last_error && ( Last error
{job.last_error}
)} Payload
                {JSON.stringify(job.payload, null, 2)}
              
) } function EnqueueDialog({ onSubmit, submitting, }: { onSubmit: (body: { queue: string type: string payload?: object | undefined 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" />