Add Reprocess to retry failed IC reviews without the create wizard.

Successful reviews are kept; skipped ICs such as a DeepSeek 400 are run again. Reprocess all re-reviews every IC while still using the library cache.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-28 02:37:53 +02:00
co-authored by Cursor
parent 34bfe33cdf
commit e84418f975
9 changed files with 429 additions and 36 deletions
+78 -12
View File
@@ -16,6 +16,7 @@ import {
removeCollaborator,
makeCollaboratorOwner,
startPipeline,
reprocessPipeline,
resumePipeline,
fetchPipelineEstimate,
} from "@/lib/api";
@@ -24,6 +25,7 @@ import type { Project, SkippedComponent, ApiLogEntry, BomSummaryRow, DeratingRow
import {
ArrowRight,
Play,
RotateCcw,
AlertTriangle,
ExternalLink,
X,
@@ -106,7 +108,14 @@ export default function ProjectDetailPage({
const [estimate, setEstimate] = useState<CostEstimate | null>(null);
const canRun = Boolean(project?.hasBom && project?.hasNetlist);
const canStartFresh = project?.status !== "complete" && project?.status !== "paused_insufficient_credits";
const canReprocess =
project?.status === "complete" ||
project?.status === "error" ||
project?.status === "cancelled";
const canStartFresh =
project?.status !== "complete" &&
project?.status !== "paused_insufficient_credits" &&
!canReprocess;
// Pull a cost estimate when the project is ready to run, so we can show
// the "~X credits" hint under the button. No blocking — the user can
@@ -157,6 +166,20 @@ export default function ProjectDetailPage({
}
};
const handleReprocess = async (mode: "failed" | "all") => {
if (!canRun) return;
setStarting(true);
try {
await reprocessPipeline(id, mode);
router.push(`/project/${id}/progress`);
} catch (e) {
setStarting(false);
alert(e instanceof Error ? e.message : "Failed to reprocess");
}
};
const hasFailedReviews = Boolean(hasSkipped);
return (
<div className="flex-1 p-6 max-w-4xl mx-auto w-full space-y-6">
<div className="flex items-center justify-between">
@@ -190,16 +213,38 @@ export default function ProjectDetailPage({
<Card>
<CardContent className="py-3">
{project.status === "complete" ? (
<div className="flex items-center gap-3">
<div className="flex flex-col gap-3 sm:flex-row sm:items-center">
<span className="text-sm text-emerald-600 dark:text-emerald-400">
Validation complete
</span>
<Link href={`/project/${id}/report`} className="ml-auto">
<Button size="sm">
View Report
<ArrowRight className="h-4 w-4 ml-1" />
<div className="flex flex-wrap items-center gap-2 sm:ml-auto">
{hasFailedReviews && (
<Button
size="sm"
variant="outline"
disabled={starting}
onClick={() => handleReprocess("failed")}
>
<RotateCcw className="h-4 w-4 mr-1" />
{starting ? "Starting..." : "Retry failed reviews"}
</Button>
)}
<Button
size="sm"
variant="outline"
disabled={starting}
onClick={() => handleReprocess("all")}
>
<RotateCcw className="h-4 w-4 mr-1" />
Reprocess all
</Button>
</Link>
<Link href={`/project/${id}/report`}>
<Button size="sm">
View Report
<ArrowRight className="h-4 w-4 ml-1" />
</Button>
</Link>
</div>
</div>
) : isPaused ? (
<span className="text-sm text-amber-600 dark:text-amber-400">
@@ -207,11 +252,32 @@ export default function ProjectDetailPage({
</span>
) : (
<div className="flex flex-col items-start gap-2">
<div className="flex items-center gap-3">
<Button size="sm" disabled={!canRun || starting} onClick={handleRunPipeline}>
<Play className="h-4 w-4 mr-1" />
{starting ? "Starting..." : "Run Pipeline"}
</Button>
<div className="flex flex-wrap items-center gap-3">
{(project.status === "error" || project.status === "cancelled") && canRun ? (
<>
<Button
size="sm"
disabled={starting}
onClick={() => handleReprocess("failed")}
>
<RotateCcw className="h-4 w-4 mr-1" />
{starting ? "Starting..." : "Reprocess"}
</Button>
<Button
size="sm"
variant="outline"
disabled={starting}
onClick={() => handleReprocess("all")}
>
Reprocess all
</Button>
</>
) : (
<Button size="sm" disabled={!canRun || starting} onClick={handleRunPipeline}>
<Play className="h-4 w-4 mr-1" />
{starting ? "Starting..." : "Run Pipeline"}
</Button>
)}
{!canRun && (
<span className="text-xs text-muted-foreground">
Upload BOM and netlist to enable
@@ -20,7 +20,7 @@ import {
import { PipelineStepper } from "@/components/progress/pipeline-stepper";
import { PausedRunBanner } from "@/components/billing/paused-run-banner";
import { usePipelineProgress } from "@/hooks/use-pipeline-progress";
import { cancelPipeline, fetchProject, resumePipeline } from "@/lib/api";
import { cancelPipeline, fetchProject, resumePipeline, reprocessPipeline } from "@/lib/api";
import type { PauseCheckpoint } from "@/lib/types";
import {
AlertTriangle,
@@ -31,6 +31,7 @@ import {
Loader2,
OctagonX,
Ban,
RotateCcw,
} from "lucide-react";
export default function ProgressPage({
@@ -51,6 +52,18 @@ export default function ProgressPage({
const [projectPaused, setProjectPaused] = useState(false);
const [projectCheckpoint, setProjectCheckpoint] = useState<PauseCheckpoint | null>(null);
const [resuming, setResuming] = useState(false);
const [reprocessing, setReprocessing] = useState(false);
async function handleReprocess(mode: "failed" | "all") {
setReprocessing(true);
try {
await reprocessPipeline(id, mode);
window.location.reload();
} catch (e) {
setReprocessing(false);
alert(e instanceof Error ? e.message : "Failed to reprocess");
}
}
// Fetch project name + initial paused state. The SSE stream only reports
// `pipeline_paused` if the page is open when it fires; landing on the
@@ -333,6 +346,15 @@ export default function ProgressPage({
<ArrowRight className="h-4 w-4 ml-1" />
</Button>
</Link>
<Button
size="sm"
variant="outline"
disabled={reprocessing}
onClick={() => handleReprocess("failed")}
>
<RotateCcw className="h-4 w-4 mr-1" />
{reprocessing ? "Starting..." : "Reprocess"}
</Button>
</div>
)}
@@ -348,12 +370,28 @@ export default function ProgressPage({
Back to Dashboard
</Button>
</Link>
<Button
size="sm"
disabled={reprocessing}
onClick={() => handleReprocess("failed")}
>
<RotateCcw className="h-4 w-4 mr-1" />
{reprocessing ? "Starting..." : "Reprocess"}
</Button>
</div>
)}
{done && error && !cancelled && (
<div className="flex items-center gap-3 p-4 rounded-lg border border-rose-500/30 bg-rose-500/5">
<span className="text-sm text-rose-600 dark:text-rose-400">{error}</span>
<span className="text-sm text-rose-600 dark:text-rose-400 flex-1">{error}</span>
<Button
size="sm"
disabled={reprocessing}
onClick={() => handleReprocess("failed")}
>
<RotateCcw className="h-4 w-4 mr-1" />
{reprocessing ? "Starting..." : "Reprocess"}
</Button>
</div>
)}
</div>
@@ -1,7 +1,8 @@
"use client";
import { use, useState, useCallback, useEffect, useLayoutEffect, useMemo, useRef, Suspense } from "react";
import { Download } from "lucide-react";
import { Download, RotateCcw } from "lucide-react";
import { useRouter } from "next/navigation";
import { useOptionalUser } from "@/hooks/use-optional-auth";
import { useReport } from "@/hooks/use-report";
import { useReviewedFindings } from "@/hooks/use-reviewed-findings";
@@ -12,7 +13,7 @@ import { Button } from "@/components/ui/button";
import { Skeleton } from "@/components/ui/skeleton";
import { Toast, useToast } from "@/components/ui/toast";
import { FeedbackDialog } from "@/components/feedback/feedback-dialog";
import { fetchCollaborators, fetchProject, fetchMyFeedback } from "@/lib/api";
import { fetchCollaborators, fetchProject, fetchMyFeedback, reprocessPipeline } from "@/lib/api";
import { exportReportToExcel } from "@/lib/report-export";
import { cn, getFindingKey } from "@/lib/utils";
import type { Finding, FindingComment, Collaborator } from "@/lib/types";
@@ -26,6 +27,7 @@ interface FocusState {
}
function ReportContent({ projectId }: { projectId: string }) {
const router = useRouter();
const { report, graph, loading, error } = useReport(projectId);
const { user } = useOptionalUser();
const [focus, setFocus] = useState<FocusState | null>(null);
@@ -39,6 +41,18 @@ function ReportContent({ projectId }: { projectId: string }) {
const [feedbackFinding, setFeedbackFinding] = useState<Finding | null>(null);
const [feedbackOpen, setFeedbackOpen] = useState(false);
const [reportedFindingIds, setReportedFindingIds] = useState<Set<string>>(new Set());
const [reprocessing, setReprocessing] = useState(false);
async function handleReprocessFailed() {
setReprocessing(true);
try {
await reprocessPipeline(projectId, "failed");
router.push(`/project/${projectId}/progress`);
} catch (e) {
setReprocessing(false);
alert(e instanceof Error ? e.message : "Failed to reprocess");
}
}
useEffect(() => {
fetchMyFeedback()
@@ -211,14 +225,25 @@ function ReportContent({ projectId }: { projectId: string }) {
{new Date(report.timestamp).toLocaleDateString()}
</p>
</div>
<Button
variant="outline"
size="sm"
onClick={() => exportReportToExcel(report, graph, projectName)}
disabled={report.findings.length === 0}
>
<Download /> Export Excel
</Button>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
disabled={reprocessing}
onClick={handleReprocessFailed}
>
<RotateCcw />
{reprocessing ? "Starting..." : "Retry failed reviews"}
</Button>
<Button
variant="outline"
size="sm"
onClick={() => exportReportToExcel(report, graph, projectName)}
disabled={report.findings.length === 0}
>
<Download /> Export Excel
</Button>
</div>
</div>
<ReportSummary
summary={report.summary}
@@ -231,7 +256,8 @@ function ReportContent({ projectId }: { projectId: string }) {
{Object.keys(report.review_errors).length} IC review{Object.keys(report.review_errors).length === 1 ? "" : "s"} failed
</p>
<p className="text-xs text-muted-foreground">
These ICs could not be reviewed against their datasheets. Re-run the pipeline to retry; if the failure repeats, share the error with support.
These ICs could not be reviewed against their datasheets. Use Retry
failed reviews to run them again without repeating ICs that already succeeded.
</p>
<ul className="space-y-1 text-xs">
{Object.entries(report.review_errors).map(([ref, err]) => (
+16 -1
View File
@@ -63,6 +63,7 @@ function mapProject(p: Record<string, unknown>): Project {
hasBom: p.has_bom as boolean,
datasheetCount: p.datasheet_count as number,
skippedComponents: (p.skipped_components as SkippedComponent[] | null) ?? undefined,
completedReviewRefs: (p.completed_review_refs as string[] | null) ?? undefined,
userId: p.user_id as string | undefined,
collaborators: (p.collaborators as string[] | null) ?? undefined,
creditsSpent: (p.credits_spent as number | undefined) ?? undefined,
@@ -439,7 +440,21 @@ export async function fetchLibraryDatasheetUrl(mpn: string): Promise<string | nu
// --- Pipeline ---
export async function startPipeline(projectId: string) {
export async function reprocessPipeline(
projectId: string,
mode: "failed" | "all" = "failed",
) {
const res = await authFetch(`${BASE}/api/pipeline/${projectId}/reprocess`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ mode }),
});
if (!res.ok) {
const err = await res.json().catch(() => ({ detail: "Failed to reprocess" }));
throw new Error(err.detail || "Failed to reprocess pipeline");
}
return res.json();
}
const res = await authFetch(`${BASE}/api/pipeline/${projectId}/start`, {
method: "POST",
});
+1
View File
@@ -227,6 +227,7 @@ export interface Project {
hasBom: boolean;
datasheetCount: number;
skippedComponents?: SkippedComponent[];
completedReviewRefs?: string[];
userId?: string;
collaborators?: string[];
creditsSpent?: number;