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

75 lines
3.1 KiB
TypeScript

import Link from "next/link";
import { formatDistanceToNowStrict } from "date-fns";
import { cn } from "@/lib/utils";
import { DELIVERY_STATUS_STYLES } from "@/app/(dashboard)/pipeline/status-colors";
import type { DeliverySummary } from "@/lib/dashboard/ops";
/**
* The last few packages, and whether any of them went wrong.
*
* A failed build is otherwise silent — the exports stay READY_FOR_DELIVERY and
* nothing reaches the delivery volume, but no one finds out until the client
* asks where the shots are.
*/
export function RecentDeliveries({ deliveries }: { deliveries: DeliverySummary[] }) {
const problems = deliveries.filter((d) => d.status === "FAILED" || d.stuck).length;
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">
Recent packages
{problems > 0 && (
<span className="ml-2 text-xs font-normal text-red-400">
{problems} need{problems === 1 ? "s" : ""} attention
</span>
)}
</h3>
<Link href="/pipeline/deliveries" className="text-xs text-zinc-400 hover:text-amber-400">
All deliveries →
</Link>
</div>
{deliveries.length === 0 ? (
<p className="px-4 py-8 text-center text-sm text-zinc-500">No packages built yet.</p>
) : (
deliveries.map((d) => (
<Link
key={d.id}
href="/pipeline/deliveries"
className="flex items-center gap-3 border-b border-zinc-800/60 px-4 py-2.5 last:border-b-0 hover:bg-zinc-900/40"
>
<div className="min-w-0 flex-1">
<div className="truncate text-[13px] text-white">
<span className="font-mono font-medium">#{d.packageNumber}</span>{" "}
<span className="text-zinc-500">
{d.projectCode}
{d.episode ? ` · ${d.episode}` : ""}
{d.kind === "MOV_REVIEW" ? " · review" : ""}
</span>
</div>
<div className="truncate text-xs text-zinc-500" title={d.errorMessage ?? undefined}>
{d.status === "FAILED" && d.errorMessage
? d.errorMessage
: `${d.itemCount} shot${d.itemCount === 1 ? "" : "s"} · ${d.atLabel} ${formatDistanceToNowStrict(d.at)} ago`}
</div>
</div>
<span
className={cn(
"shrink-0 rounded-full border px-2 py-0.5 text-xs whitespace-nowrap",
// A build that never finished reads as a problem, not as work
// still in progress.
d.stuck
? "bg-red-500/15 text-red-400 border-red-500/30"
: DELIVERY_STATUS_STYLES[d.status] ?? "bg-zinc-500/15 text-zinc-300 border-zinc-500/30"
)}
>
{d.stuck ? "STUCK" : d.status}
</span>
</Link>
))
)}
</div>
);
}