"use client"; import { useState } from "react"; import Link from "next/link"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import { useSession } from "next-auth/react"; import { RefreshCw, RotateCcw, XCircle, CheckCircle2, Server } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Progress } from "@/components/ui/progress"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { useToast } from "@/components/ui/use-toast"; import { cn } from "@/lib/utils"; import { EXPORT_STATUS_STYLES, FAILED_STATUSES, ACTIVE_STATUSES, statusLabel, formatEta, } from "./status-colors"; interface QueueExport { id: string; shotCode: string; episode: string | null; projectName: string; projectCode: string; versionString: string; status: string; statusChangedAt: string; createdAt: string; outputDir: string; job: { id: string; attempt: number; status: string; progress: number; currentFrame: number | null; totalFrames: number | null; etaSeconds: number | null; priority: number; machineName: string | null; errorMessage: string | null; } | null; } interface Props { projects: { id: string; name: string; code: string }[]; } export function PipelineQueueClient({ projects }: Props) { const { data: session } = useSession(); const { toast } = useToast(); const queryClient = useQueryClient(); const [projectId, setProjectId] = useState("all"); const [statusFilter, setStatusFilter] = useState("all"); const isAdmin = ["ADMIN", "PRODUCER", "SUPERVISOR"].includes(session?.user?.role ?? ""); const { data, isLoading, refetch, isFetching } = useQuery({ queryKey: ["pipeline-queue", projectId, statusFilter], queryFn: async () => { const params = new URLSearchParams({ limit: "200" }); if (projectId !== "all") params.set("projectId", projectId); if (statusFilter !== "all") params.set("status", statusFilter); const res = await fetch(`/api/render/queue?${params}`); if (!res.ok) throw new Error("Failed to load queue"); return res.json() as Promise<{ exports: QueueExport[]; pagination: { total: number } }>; }, refetchInterval: 5000, }); const exports = data?.exports ?? []; const counts = { queued: exports.filter((e) => e.status === "QUEUED").length, rendering: exports.filter((e) => e.status === "RENDERING").length, failed: exports.filter((e) => FAILED_STATUSES.includes(e.status)).length, readyForQc: exports.filter((e) => e.status === "READY_FOR_QC").length, readyForDelivery: exports.filter((e) => e.status === "READY_FOR_DELIVERY").length, }; async function action(exportId: string, verb: "retry" | "cancel" | "mark-done") { const res = await fetch(`/api/render/exports/${exportId}/${verb}`, { method: "POST" }); const body = await res.json().catch(() => ({})); if (!res.ok) { toast({ title: `${verb} failed`, description: body.error ?? res.statusText, variant: "destructive" }); } else { toast({ title: `Export ${verb === "mark-done" ? "marked done" : verb === "retry" ? "requeued" : "cancelled"}` }); } queryClient.invalidateQueries({ queryKey: ["pipeline-queue"] }); } const cards: { label: string; value: number; className?: string }[] = [ { label: "Queued", value: counts.queued }, { label: "Rendering", value: counts.rendering, className: "text-blue-400" }, { label: "Failed", value: counts.failed, className: "text-red-400" }, { label: "Ready for QC", value: counts.readyForQc, className: "text-amber-400" }, { label: "Ready for Delivery", value: counts.readyForDelivery, className: "text-green-400" }, ]; return (

Render Queue

Exports queued from the AE panel — live status, retries and history

{/* Summary cards */}
{cards.map((c) => (
{c.value}
{c.label}
))}
{/* Filters */}
{/* Table */}
{isLoading ? ( ) : exports.length === 0 ? ( ) : ( exports.map((e) => ( )) )}
Shot Version Status Progress Machine Attempt Queued Actions
Loading…
No exports yet — queue one from the After Effects panel
{e.shotCode}
{e.projectCode} {e.episode ? ` · ep ${e.episode}` : ""}
{e.versionString} {statusLabel(e.status)} {e.status === "RENDERING" && e.job ? (
{e.job.currentFrame != null && e.job.totalFrames != null ? `frame ${e.job.currentFrame}/${e.job.totalFrames} · ` : ""} ETA {formatEta(e.job.etaSeconds)}
) : ( )}
{e.job?.machineName ?? "—"} {e.job ? `${e.job.attempt}` : "—"} {new Date(e.createdAt).toLocaleString()}
{FAILED_STATUSES.includes(e.status) && ( )} {ACTIVE_STATUSES.includes(e.status) && ( )} {isAdmin && ["QUEUED", "RENDERING"].includes(e.status) && ( )}
); }