Files
vfxreview/components/dashboard/FarmStrip.tsx
T
twotalesanimation f6a7f0ef5a feat(dashboard): show what is blocked and how the farm is doing
The dashboard was a generic task-manager view of a studio that does not work
that way. Two of its five headline numbers counted Task.dueDate, which no
pipeline code writes - the only writer is a manual field on the task detail
page, so those cards read zero by construction. The rest were unscoped and
cumulative ("approved" across every project ever finished), the activity feed
was the notification table, and the shot queue sampled 15 rows by updatedAt,
a column the farm touches constantly. Nothing about failed exports, QC depth
or the render farm appeared at all, so a RENDER_FAILED was only visible if
someone thought to open the queue and filter for it.

Replaced with pipeline state, which the system maintains on its own:

- Blocked queue: the four failed statuses plus exports that stopped
  progressing, each with the reason read off the actual cause - the failing
  validation check, the QC note, the aerender error, or the machine that
  claimed it and went quiet.
- Farm strip: machines, queue depth by job type, oldest unclaimed job, and
  worker-version drift, since a worker's version decides which fixes it has.
- Five counters that can go down, scoped to live productions, each linking to
  the rows it counted.

The queue page ignored ?status=, so the counters had nowhere to link; it now
takes an initial filter from the URL. Stat cards are hidden from CLIENT users
- the old ones were generic version counts, these are internal pipeline state.

Thresholds live in SystemConfig (dashboard.qcStaleHours, .stalledJobHours) so
they can be tuned without a redeploy.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 05:40:40 +02:00

112 lines
4.4 KiB
TypeScript

import Link from "next/link";
import { formatDistanceToNowStrict } from "date-fns";
import { cn } from "@/lib/utils";
import { formatEta } from "@/app/(dashboard)/pipeline/status-colors";
import type { FarmStatus } from "@/lib/dashboard/ops";
/**
* Farm health: who is up, what is waiting, and who is running an old worker.
*
* The version row is not trivia — a worker's version is what decides whether
* it has a given fix, and a machine that quietly stayed behind keeps producing
* the bug everyone else has stopped seeing.
*/
export function FarmStrip({ farm }: { farm: FarmStatus }) {
return (
<div className="rounded-lg border border-zinc-800 bg-card overflow-hidden">
<div className="flex items-center justify-between gap-3 border-b border-zinc-800/60 px-4 py-3">
<h3 className="text-sm font-semibold text-white">
{farm.online} of {farm.total} online
</h3>
<Link href="/pipeline/machines" className="text-xs text-zinc-400 hover:text-amber-400">
Machines →
</Link>
</div>
{farm.machines.length === 0 ? (
<p className="px-4 py-8 text-center text-sm text-zinc-500">
No machines have registered yet.
</p>
) : (
farm.machines.map((m) => (
<div
key={m.id}
className="flex items-center gap-3 border-b border-zinc-800/60 px-4 py-2.5 last:border-b-0"
>
<span
className={cn(
"h-2 w-2 shrink-0 rounded-full",
m.status === "ONLINE"
? "bg-green-500 ring-4 ring-green-500/15"
: m.status === "DISABLED"
? "bg-zinc-600"
: "bg-zinc-500"
)}
title={m.status}
/>
<span className="w-24 shrink-0 font-mono text-[12.5px] text-white">{m.name}</span>
<span className="min-w-0 flex-1 truncate text-[12.5px] text-zinc-400">
{m.currentJob
? `${m.currentJob.shotCode ?? "job"} ${m.currentJob.versionString ?? ""} · ${Math.round(
(m.currentJob.progress ?? 0) * 100
)}% · ETA ${formatEta(m.currentJob.etaSeconds)}`
: m.status === "ONLINE"
? "Idle"
: m.lastSeenAt
? `Offline · last seen ${formatDistanceToNowStrict(new Date(m.lastSeenAt))} ago`
: "Never seen"}
</span>
<span
className={cn(
"shrink-0 font-mono text-[11.5px]",
farm.latestWorkerVersion && m.workerVersion !== farm.latestWorkerVersion
? "text-amber-400"
: "text-zinc-500"
)}
>
{m.workerVersion ?? "—"}
</span>
</div>
))
)}
<div className="grid grid-cols-3 border-t border-zinc-800/60">
<div className="border-r border-zinc-800/60 px-4 py-2.5">
<b className="block font-mono text-lg font-semibold leading-tight text-white">
{farm.queued.render}
</b>
<small className="text-xs text-zinc-500">renders queued</small>
</div>
<div className="border-r border-zinc-800/60 px-4 py-2.5">
<b className="block font-mono text-lg font-semibold leading-tight text-white">
{farm.queued.preview}
</b>
<small className="text-xs text-zinc-500">previews queued</small>
</div>
<div className="px-4 py-2.5">
<b className="block font-mono text-lg font-semibold leading-tight text-white">
{farm.oldestQueuedMs == null
? "—"
: formatDistanceToNowStrict(new Date(Date.now() - farm.oldestQueuedMs))}
</b>
<small className="text-xs text-zinc-500">oldest wait</small>
</div>
</div>
{farm.behindMachines.length > 0 && (
<div className="flex gap-2 border-t border-amber-500/20 bg-amber-500/[0.07] px-4 py-2.5 text-xs text-amber-400">
<span aria-hidden="true">▲</span>
<span>
{farm.behindMachines.length} machine
{farm.behindMachines.length === 1 ? " is" : "s are"} behind{" "}
<span className="font-mono">{farm.latestWorkerVersion}</span> —{" "}
{farm.behindMachines
.map((m) => `${m.name} (${m.workerVersion ?? "unknown"})`)
.join(", ")}
</span>
</div>
)}
</div>
);
}