Skip to content

Commit 814f550

Browse files
authored
feat(web-ui): link failed tasks to PROOF9 gate failure for root cause (#474)
## Summary - TaskDetailModal FAILED state: guidance panel + 'View PROOF9 Gates' (routes to /proof?gate=<detected> from obligation data, fallback /proof) + 'Reset to Ready' buttons - proof/page.tsx: ?gate= filter using useSearchParams — filters table by obligation.gate match (pytest/ruff/build/custom) - Filter badge with clear link (aria-labeled) + empty-state row - Wrapped in Suspense for useSearchParams compatibility ## Validation - Review feedback: 2 items fixed (aria-label for accessibility, gate-aware routing using requirementsMap) - Demo: All 5 acceptance criteria verified via screenshot - CI: All checks green Closes #474
1 parent 7f6ded0 commit 814f550

2 files changed

Lines changed: 95 additions & 6 deletions

File tree

web-ui/src/app/proof/page.tsx

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
'use client';
22

3-
import { useState, useEffect } from 'react';
3+
import { useState, useEffect, Suspense } from 'react';
44
import Link from 'next/link';
5+
import { useSearchParams } from 'next/navigation';
56
import useSWR from 'swr';
67
import { InformationCircleIcon } from '@hugeicons/react';
78
import {
@@ -117,10 +118,12 @@ function WaiveDialog({
117118
);
118119
}
119120

120-
export default function ProofPage() {
121+
function ProofPageContent() {
121122
const [workspacePath, setWorkspacePath] = useState<string | null>(null);
122123
const [workspaceReady, setWorkspaceReady] = useState(false);
123124
const [waivedReq, setWaivedReq] = useState<ProofRequirement | null>(null);
125+
const searchParams = useSearchParams();
126+
const gateFilter = searchParams.get('gate')?.toLowerCase() ?? null;
124127

125128
useEffect(() => {
126129
setWorkspacePath(getSelectedWorkspacePath());
@@ -184,13 +187,26 @@ export default function ProofPage() {
184187
</div>
185188
)}
186189

187-
{data && data.total > 0 && (
190+
{data && data.total > 0 && (() => {
191+
const visibleReqs = gateFilter
192+
? data.requirements.filter((r) =>
193+
r.obligations.some((o) => o.gate.toLowerCase() === gateFilter)
194+
)
195+
: data.requirements;
196+
197+
return (
188198
<>
189-
<div className="mb-4 flex gap-4 text-sm text-muted-foreground">
199+
<div className="mb-4 flex flex-wrap items-center gap-4 text-sm text-muted-foreground">
190200
<span>{data.by_status?.open ?? 0} open</span>
191201
<span>{data.by_status?.satisfied ?? 0} satisfied</span>
192202
<span>{data.by_status?.waived ?? 0} waived</span>
193203
<span className="font-medium text-foreground">{data.total} total</span>
204+
{gateFilter && (
205+
<span className="flex items-center gap-1.5 rounded-full border bg-muted px-2.5 py-0.5 text-xs font-medium text-foreground">
206+
Filtered by gate: {gateFilter}
207+
<Link href="/proof" aria-label={`Clear gate filter ${gateFilter}`} className="text-muted-foreground hover:text-foreground"></Link>
208+
</span>
209+
)}
194210
</div>
195211

196212
{/* Status legend */}
@@ -260,7 +276,15 @@ export default function ProofPage() {
260276
</tr>
261277
</thead>
262278
<tbody>
263-
{data.requirements.map((req) => (
279+
{visibleReqs.length === 0 && (
280+
<tr>
281+
<td colSpan={8} className="px-4 py-8 text-center text-sm text-muted-foreground">
282+
No requirements match gate &quot;{gateFilter}&quot;.{' '}
283+
<Link href="/proof" className="text-primary hover:underline">Clear filter</Link>
284+
</td>
285+
</tr>
286+
)}
287+
{visibleReqs.map((req) => (
264288
<tr key={req.id} className="border-b last:border-0 hover:bg-muted/30">
265289
<td className="px-4 py-3 font-mono text-xs">
266290
<Link href={`/proof/${encodeURIComponent(req.id)}`} className="text-primary hover:underline">
@@ -300,7 +324,8 @@ export default function ProofPage() {
300324
</table>
301325
</div>
302326
</>
303-
)}
327+
);
328+
})()}
304329

305330
{waivedReq && (
306331
<WaiveDialog
@@ -370,3 +395,11 @@ export default function ProofPage() {
370395
</TooltipProvider>
371396
);
372397
}
398+
399+
export default function ProofPage() {
400+
return (
401+
<Suspense>
402+
<ProofPageContent />
403+
</Suspense>
404+
);
405+
}

web-ui/src/components/tasks/TaskDetailModal.tsx

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import {
1111
Time01Icon,
1212
ViewIcon,
1313
BookOpen01Icon,
14+
Alert02Icon,
15+
CheckListIcon,
1416
} from '@hugeicons/react';
1517
import {
1618
Dialog,
@@ -221,6 +223,24 @@ export function TaskDetailModal({
221223
</div>
222224
)}
223225

226+
{/* FAILED-state guidance panel */}
227+
{task.status === 'FAILED' && (
228+
<div className="rounded-md border border-destructive/40 bg-destructive/5 px-3 py-2.5">
229+
<div className="flex items-start gap-2">
230+
<Alert02Icon className="mt-0.5 h-4 w-4 shrink-0 text-destructive" />
231+
<div className="space-y-0.5">
232+
<p className="text-xs font-medium text-destructive">Task failed during execution</p>
233+
<p className="text-xs text-muted-foreground">
234+
Check PROOF9 gates to identify which quality requirements need attention.
235+
{(task.requirement_ids ?? []).length === 0 && (
236+
<> Use the button below to view all gates.</>
237+
)}
238+
</p>
239+
</div>
240+
</div>
241+
</div>
242+
)}
243+
224244
<DialogFooter>
225245
{task.status === 'BACKLOG' && (
226246
<Button
@@ -258,6 +278,42 @@ export function TaskDetailModal({
258278
Execute
259279
</Button>
260280
)}
281+
{task.status === 'FAILED' && (() => {
282+
// Derive the best proof link from requirement obligations if available
283+
let proofLink = '/proof';
284+
for (const reqId of (task.requirement_ids ?? [])) {
285+
const req = requirementsMap.get(reqId);
286+
const gate = req?.obligations?.[0]?.gate?.toLowerCase();
287+
if (gate) { proofLink = `/proof?gate=${encodeURIComponent(gate)}`; break; }
288+
}
289+
return (
290+
<>
291+
<Button
292+
variant="outline"
293+
size="sm"
294+
disabled={isUpdating}
295+
onClick={handleMarkReady}
296+
>
297+
{isUpdating ? (
298+
<Loading03Icon className="mr-1.5 h-3.5 w-3.5 animate-spin" />
299+
) : (
300+
<CheckmarkCircle01Icon className="mr-1.5 h-3.5 w-3.5" />
301+
)}
302+
Reset to Ready
303+
</Button>
304+
<Button
305+
size="sm"
306+
onClick={() => {
307+
onClose();
308+
router.push(proofLink);
309+
}}
310+
>
311+
<CheckListIcon className="mr-1.5 h-3.5 w-3.5" />
312+
View PROOF9 Gates
313+
</Button>
314+
</>
315+
);
316+
})()}
261317
</DialogFooter>
262318
</>
263319
)}

0 commit comments

Comments
 (0)