Files
twotalesanimation 6afbe82a29
Deploy / deploy (push) Successful in 6m51s
feat(dashboard): QC queue, deliveries and whose-court panels
Completes the operations dashboard.

QC queue: what is waiting on a person to watch the frames, oldest first.
Every stage before QC is worked by the farm, so this is the only one that
cannot drain itself - depth here is the best early warning that a delivery
date is going to move, and it had no home outside a filter on the queue page.

Deliveries: the last few packages, flagging FAILED and builds stuck in
BUILDING. A failed build is otherwise silent - the exports stay
READY_FOR_DELIVERY, nothing reaches the delivery volume, and nobody finds out
until the client asks where the shots are.

Whose court: versions sent to the client and unanswered, against shots with
unresolved notes. Two numbers rather than one because they call for opposite
responses - a fortnight-old review is a phone call, unactioned notes are a
scheduling problem.

The artist view now scopes to the artist throughout: the blocked list, and
the counters above it, which otherwise quoted a studio-wide figure next to a
personal list.

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

97 lines
3.8 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import Link from "next/link";
import { formatDistanceToNowStrict } from "date-fns";
import { cn } from "@/lib/utils";
import type { QcQueueEntry } from "@/lib/dashboard/ops";
/**
* What is waiting on someone to watch the frames, oldest first.
*
* Everything upstream of QC is worked by the farm; this is the one stage that
* only drains when a person sits down to it, which is why depth here is worth
* a panel of its own rather than a filter on the queue page.
*/
export function QcQueue({ entries, staleHours }: { entries: QcQueueEntry[]; staleHours: number }) {
const oldest = entries[0];
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">
{entries.length === 0
? "QC queue empty"
: `${entries.length} waiting on QC`}
{oldest && (
<span className="ml-2 text-xs font-normal text-zinc-500">
oldest {formatDistanceToNowStrict(oldest.since)}
</span>
)}
</h3>
<Link
href="/pipeline?status=READY_FOR_QC"
className="text-xs text-zinc-400 hover:text-amber-400"
>
Open QC →
</Link>
</div>
{entries.length === 0 ? (
<p className="px-4 py-10 text-center text-sm text-zinc-500">
Nothing is waiting on a QC pass.
</p>
) : (
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-zinc-800 bg-zinc-900/60 text-left text-xs text-zinc-400">
<th className="px-4 py-2.5 font-medium">Shot</th>
<th className="px-4 py-2.5 font-medium">Version</th>
<th className="px-4 py-2.5 font-medium">Waiting</th>
<th className="px-4 py-2.5 font-medium">Frames</th>
<th className="px-4 py-2.5 font-medium">Submitted by</th>
</tr>
</thead>
<tbody>
{entries.map((e) => {
const stale = Date.now() - e.since.getTime() > staleHours * 3600_000;
return (
<tr key={e.exportId} className="border-b border-zinc-800/60 hover:bg-zinc-900/40">
<td className="px-4 py-2.5">
<Link
href={`/pipeline/exports/${e.exportId}`}
className="font-mono text-[13px] font-medium text-white hover:text-amber-400"
>
{e.shotCode}
</Link>
<div className="text-xs text-zinc-500">
{e.projectCode}
{e.episode ? ` · ep ${e.episode}` : ""}
</div>
</td>
<td className="px-4 py-2.5 font-mono text-[13px] text-zinc-300">
{e.versionString}
</td>
<td
className={cn(
"px-4 py-2.5 tabular-nums whitespace-nowrap",
stale ? "text-amber-400" : "text-zinc-300"
)}
>
{formatDistanceToNowStrict(e.since)}
</td>
<td className="px-4 py-2.5 font-mono text-[13px] tabular-nums text-zinc-400 whitespace-nowrap">
{e.frameStart}–{e.frameEnd}
</td>
<td className="px-4 py-2.5 text-zinc-300 whitespace-nowrap">
{e.submittedBy ?? "—"}
</td>
</tr>
);
})}
</tbody>
</table>
</div>
)}
</div>
);
}