"use client"; import Link from "next/link"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import { useSession } from "next-auth/react"; import { ArrowLeft, Zap, Power, Cpu, HardDrive, MemoryStick } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Progress } from "@/components/ui/progress"; import { useToast } from "@/components/ui/use-toast"; import { cn } from "@/lib/utils"; interface MachineRow { id: string; name: string; hostname: string; enabled: boolean; status: "ONLINE" | "OFFLINE" | "DISABLED"; lastSeenAt: string | null; workerVersion: string | null; aeVersion: string | null; availability: { mode?: string } | null; renderNowUntil: string | null; latestHeartbeat: { createdAt: string; cpuPercent: number | null; memPercent: number | null; diskFreeGb: number | null; } | null; currentJob: { id: string; exportId: string | null; shotCode: string | null; versionString: string | null; progress: number; etaSeconds: number | null; } | null; } const STATUS_STYLES: Record = { ONLINE: "bg-green-500/15 text-green-400 border-green-500/30", OFFLINE: "bg-zinc-500/15 text-zinc-400 border-zinc-500/30", DISABLED: "bg-red-500/15 text-red-400 border-red-500/30", }; export function MachinesClient() { const { data: session } = useSession(); const { toast } = useToast(); const queryClient = useQueryClient(); const isAdmin = ["ADMIN", "PRODUCER", "SUPERVISOR"].includes(session?.user?.role ?? ""); const { data, isLoading } = useQuery({ queryKey: ["pipeline-machines"], queryFn: async () => { const res = await fetch("/api/machines"); if (!res.ok) throw new Error("Failed to load machines"); return res.json() as Promise<{ machines: MachineRow[] }>; }, refetchInterval: 10000, }); async function patch(machineId: string, body: Record, okMsg: string) { const res = await fetch(`/api/machines/${machineId}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), }); const resBody = await res.json().catch(() => ({})); if (!res.ok) { toast({ title: "Update failed", description: resBody.error ?? res.statusText, variant: "destructive" }); } else { toast({ title: okMsg }); } queryClient.invalidateQueries({ queryKey: ["pipeline-machines"] }); } const machines = data?.machines ?? []; return (

Render Machines

Worker status, render windows and the Render Now override

{isLoading ? (
Loading…
) : machines.length === 0 ? (
No machines registered yet — install the RenderWorker service on a workstation and it will appear here after its first registration.
) : (
{machines.map((m) => { const renderNowActive = m.renderNowUntil && new Date(m.renderNowUntil) > new Date(); return (
{m.name}
{m.hostname}
{m.status}
Worker {m.workerVersion ?? "?"} · AE {m.aeVersion ?? "?"}
Availability: {m.availability?.mode ?? "ALWAYS"} {renderNowActive && ( {" "}· Render Now until {new Date(m.renderNowUntil!).toLocaleTimeString()} )}
{m.lastSeenAt &&
Last seen {new Date(m.lastSeenAt).toLocaleString()}
}
{m.latestHeartbeat && (
{m.latestHeartbeat.cpuPercent != null ? `${Math.round(m.latestHeartbeat.cpuPercent)}%` : "—"} {m.latestHeartbeat.memPercent != null ? `${Math.round(m.latestHeartbeat.memPercent)}%` : "—"} {m.latestHeartbeat.diskFreeGb != null ? `${Math.round(m.latestHeartbeat.diskFreeGb)} GB free` : "—"}
)} {m.currentJob ? (
Rendering{" "} {m.currentJob.exportId ? ( {m.currentJob.shotCode} {m.currentJob.versionString} ) : ( "job" )}
) : (
Idle
)}
{isAdmin && ( )}
); })}
)}
); }