f6a7f0ef5a
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>
112 lines
4.4 KiB
TypeScript
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>
|
|
);
|
|
}
|