"use client"; import Link from "next/link"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import { ArrowLeft, RotateCcw, XCircle } from "lucide-react"; import { Button } from "@/components/ui/button"; import { useToast } from "@/components/ui/use-toast"; import { cn } from "@/lib/utils"; import { EXPORT_STATUS_STYLES, FAILED_STATUSES, ACTIVE_STATUSES, statusLabel, } from "../../status-colors"; interface ExportDetail { export: { id: string; status: string; versionString: string; statusChangedAt: string; createdAt: string; aepPath: string; compName: string; outputDir: string; outputPattern: string; frameStart: number; frameEnd: number; fps: number; width: number; height: number; colorspace: string | null; exrFileCount: number | null; exrTotalBytes: number | null; vfxScope: string | null; submissionNote: string | null; submittedByName: string | null; shot: { id: string; shotCode: string; episode: string | null; project: { id: string; name: string; code: string }; }; renderJobs: { id: string; attempt: number; status: string; priority: number; progress: number; currentFrame: number | null; totalFrames: number | null; errorMessage: string | null; logTail: string | null; exitCode: number | null; claimedAt: string | null; startedAt: string | null; finishedAt: string | null; machine: { id: string; name: string } | null; }[]; events: { id: string; fromStatus: string | null; toStatus: string; actorType: string; actorId: string | null; note: string | null; createdAt: string; }[]; }; } export function ExportDetailClient({ exportId }: { exportId: string }) { const { toast } = useToast(); const queryClient = useQueryClient(); const { data, isLoading, error } = useQuery({ queryKey: ["pipeline-export", exportId], queryFn: async () => { const res = await fetch(`/api/render/exports/${exportId}`); if (!res.ok) throw new Error((await res.json().catch(() => ({})))?.error ?? "Failed to load export"); return res.json() as Promise; }, refetchInterval: 5000, }); async function action(verb: "retry" | "cancel") { 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" }); } queryClient.invalidateQueries({ queryKey: ["pipeline-export", exportId] }); } if (isLoading) return
Loading…
; if (error || !data) { return (
{(error as Error)?.message ?? "Export not found"}
); } const e = data.export; const manifestRows: [string, string][] = [ ["Project", `${e.shot.project.name} (${e.shot.project.code})`], ["Shot", e.shot.shotCode], ["Comp", e.compName], ["AEP", e.aepPath], ["Output dir", e.outputDir], ["Pattern", e.outputPattern], ["Frames", `${e.frameStart}–${e.frameEnd} @ ${e.fps} fps`], ["Resolution", `${e.width}×${e.height}`], ["Colorspace", e.colorspace ?? "—"], ["EXR files", e.exrFileCount != null ? String(e.exrFileCount) : "—"], ["Submitted by", e.submittedByName ?? "—"], ]; return (

{e.shot.shotCode} · {e.versionString}

{statusLabel(e.status)}
{FAILED_STATUSES.includes(e.status) && ( )} {ACTIVE_STATUSES.includes(e.status) && ( )}
{/* Manifest */}
Manifest
{manifestRows.map(([k, v]) => (
{k}
{v}
))}
{/* Per-submission slate fields */} {(e.vfxScope || e.submissionNote) && (
Submission
{e.vfxScope && (
VFX Scope
{e.vfxScope}
)} {e.submissionNote && (
Submission Note
{e.submissionNote}
)}
)} {/* Render attempts */}
Render attempts
{e.renderJobs.map((j) => (
Attempt {j.attempt} {j.status} {j.machine?.name ?? "unclaimed"} {j.exitCode != null && exit {j.exitCode}} {j.startedAt && ( started {new Date(j.startedAt).toLocaleString()} )} {j.finishedAt && ( finished {new Date(j.finishedAt).toLocaleString()} )}
{j.errorMessage &&
{j.errorMessage}
} {j.logTail && (
Log tail
                    {j.logTail}
                  
)}
))} {e.renderJobs.length === 0 && (
No attempts yet
)}
{/* Event timeline */}
Timeline
    {e.events.map((ev) => (
  1. {new Date(ev.createdAt).toLocaleString()} {ev.fromStatus ? `${statusLabel(ev.fromStatus)} → ` : ""} {statusLabel(ev.toStatus)} · {ev.actorType.toLowerCase()} {ev.note && {ev.note}}
  2. ))}
); }