diff --git a/DELIVERY_WORKFLOW.md b/DELIVERY_WORKFLOW.md index d304490..e6e675d 100644 --- a/DELIVERY_WORKFLOW.md +++ b/DELIVERY_WORKFLOW.md @@ -27,6 +27,12 @@ An Export reaches `READY_FOR_DELIVERY` only by passing QC — from the AE panel or the web. That is the single gate; there is no way to package an export that has not been QC'd. +A render that failed automated validation can still get there: **Override +validation** on the Export detail page (admin/producer/supervisor, reason +required) resumes the pipeline at the preview stage, so the shot still goes +through QC like any other. The failed checks stay on record and keep appearing +as warnings in the QC queue, so whoever QCs it knows what was waved through. + Two rules worth knowing: - **Newest version per shot only.** If a shot has two exports sitting ready, the diff --git a/RenderPipeline2.md b/RenderPipeline2.md index 0e5aa04..0ce817d 100644 --- a/RenderPipeline2.md +++ b/RenderPipeline2.md @@ -1085,6 +1085,7 @@ The offset term matters — a shot may render a sub-range of a long plate (obser - Any FAIL → whole run FAIL → Export = VALIDATION_FAILED, pipeline stops, exact reasons in `ValidationResult` rows (E12 example above shows a missing-frame report). - WARN never stops the pipeline but is shown in QC and delivery UIs. +- **Manual override.** A check can fail on a render that is genuinely fine — an unreachable plate, an expectation that does not apply to this shot — so a VALIDATION_FAILED export is not a dead end. An admin/producer/supervisor can override it from the Export detail page with a **required reason**, and the pipeline resumes exactly where a pass would have handed off: preview, then READY_FOR_QC. **An override never skips QC and never delivers directly** — a human still looks at the frames. The failed `ValidationResult` rows are left untouched, so the QC queue keeps listing them as warnings, and the reason plus the checks waved through are recorded on the Export's timeline. - Checksum: xxHash64 per file, sequence digest = xxHash64 of sorted per-file digests; stored on `Export.checksum` and reused by the delivery builder for copy verification (MD5 available for the client-facing manifest per 18.1-Q13). --- diff --git a/app/(dashboard)/pipeline/exports/[exportId]/ExportDetailClient.tsx b/app/(dashboard)/pipeline/exports/[exportId]/ExportDetailClient.tsx index 8b67811..23cccb7 100644 --- a/app/(dashboard)/pipeline/exports/[exportId]/ExportDetailClient.tsx +++ b/app/(dashboard)/pipeline/exports/[exportId]/ExportDetailClient.tsx @@ -1,8 +1,9 @@ "use client"; +import { useState } from "react"; import Link from "next/link"; import { useQuery, useQueryClient } from "@tanstack/react-query"; -import { ArrowLeft, RotateCcw, XCircle } from "lucide-react"; +import { ArrowLeft, RotateCcw, XCircle, ShieldAlert } from "lucide-react"; import { Button } from "@/components/ui/button"; import { useToast } from "@/components/ui/use-toast"; import { cn } from "@/lib/utils"; @@ -105,6 +106,9 @@ const CHECK_STYLES: Record = { export function ExportDetailClient({ exportId }: { exportId: string }) { const { toast } = useToast(); const queryClient = useQueryClient(); + const [overrideOpen, setOverrideOpen] = useState(false); + const [overrideReason, setOverrideReason] = useState(""); + const [overriding, setOverriding] = useState(false); const { data, isLoading, error } = useQuery({ queryKey: ["pipeline-export", exportId], @@ -125,6 +129,31 @@ export function ExportDetailClient({ exportId }: { exportId: string }) { queryClient.invalidateQueries({ queryKey: ["pipeline-export", exportId] }); } + async function overrideValidation() { + setOverriding(true); + try { + const res = await fetch(`/api/render/exports/${exportId}/override-validation`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ reason: overrideReason }), + }); + const body = await res.json().catch(() => ({})); + if (!res.ok) { + toast({ title: "Override failed", description: body.error ?? res.statusText, variant: "destructive" }); + return; + } + toast({ + title: "Validation overridden", + description: `${(body.overridden ?? []).join(", ") || "No failed checks recorded"} — continuing to preview and QC`, + }); + setOverrideOpen(false); + setOverrideReason(""); + queryClient.invalidateQueries({ queryKey: ["pipeline-export", exportId] }); + } finally { + setOverriding(false); + } + } + if (isLoading) return
Loading…
; if (error || !data) { return ( @@ -178,6 +207,11 @@ export function ExportDetailClient({ exportId }: { exportId: string }) { Retry )} + {e.status === "VALIDATION_FAILED" && !overrideOpen && ( + + )} {ACTIVE_STATUSES.includes(e.status) && (