"use client"; /** * T-176 — Jobs tab: per-queue BullMQ state + the recent failed jobs (DLQ view) * with an audited re-run action. * * BullMQ is CLUSTER-wide (every org shares the seven worker queues), so the * counts are reported `scope: "cluster"` — the org-admin gate is the access * control, and the label keeps the UI honest (it never pretends the numbers * are org-scoped). */ import { useState } from "react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { ListChecks, RefreshCw } from "lucide-react"; import { orgAdminApi, type AdminQueueName, type OrgFailedJob } from "@/lib/org-admin"; import { isApiError } from "@/lib/api"; import { toast } from "@/stores/ui-store"; import { Section, inputCls } from "@/components/settings/settings-shell"; const QUEUES: AdminQueueName[] = [ "files", "search-index", "webhooks", "automations", "email", "ai", "maintenance" ]; function fmtTime(iso: string | null): string { if (!iso) return "—"; return new Date(iso).toLocaleString(); } export function AdminJobsTab({ orgId }: { orgId: string }) { const qc = useQueryClient(); const [queue, setQueue] = useState(""); const overview = useQuery({ queryKey: ["orgs", orgId, "admin", "jobs"], queryFn: () => orgAdminApi.jobsOverview(orgId), retry: false, }); const failed = useQuery({ queryKey: ["orgs", orgId, "admin", "jobs", "failed", queue], queryFn: () => orgAdminApi.jobsFailed(orgId, { queue: queue || undefined, limit: 20 }), retry: false, }); const requeue = useMutation({ mutationFn: (v: { queue: AdminQueueName; jobId: string }) => orgAdminApi.jobsRequeue(orgId, v), onSuccess: () => { toast("success", "Job requeued"); qc.invalidateQueries({ queryKey: ["orgs", orgId, "admin", "jobs"] }); qc.invalidateQueries({ queryKey: ["orgs", orgId, "admin", "jobs", "failed"] }); }, onError: (e) => toast("error", "Could not requeue the job", isApiError(e) ? e.message : undefined) }); if (overview.isLoading) { return

Loading jobs…

; } if (overview.isError) { return

Could not load the job state.

; } const counts = overview.data!.queues; const jobs = failed.data?.jobs ?? []; return (
{counts.map((c) => (
{c.name}
{c.waiting} wait {c.active} active 0 ? "font-medium text-destructive" : "font-medium text-foreground"}> {c.failed} {" "} fail
))}
{failed.isLoading ? (

Loading failed jobs…

) : failed.isError ? (

Could not load failed jobs.

) : jobs.length === 0 ? (

No failed jobs{queue ? ` on ${queue}` : ""}.

) : (
    {jobs.map((j) => ( requeue.mutate({ queue: j.queue, jobId: j.jobId! })} busy={requeue.isPending} /> ))}
)}
); } function JobRow({ job, onRequeue, busy }: { job: OrgFailedJob; onRequeue: () => void; busy: boolean }) { return (
  • {job.name} {job.queue} {job.attemptsMade} attempts {fmtTime(job.failedAt)}
    {job.failedReason && (

    {job.failedReason}

    )} {job.payload && (
                {job.payload}
              
    )}
  • ); }