diff --git a/backend/app/main.py b/backend/app/main.py index f81f9dd..d7d7629 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -44,6 +44,7 @@ ) from .models import ( Finding, + FindingStatusUpdate, FixRequest, FixResponse, Location, @@ -828,6 +829,45 @@ async def get_findings(job_id: str): } +@app.patch("/findings/{finding_id}/status") +async def update_finding_status(finding_id: str, payload: FindingStatusUpdate): + if payload.status not in ("open", "accepted", "ignored"): + raise HTTPException( + status_code=400, + detail="Invalid status value. Must be 'open', 'accepted', or 'ignored'.", + ) + + db = await get_db() + try: + cur = await db.execute("SELECT id FROM findings WHERE id = ?", (finding_id,)) + if not await cur.fetchone(): + raise HTTPException( + status_code=404, detail=f"Finding '{finding_id}' not found." + ) + try: + await db.execute( + "UPDATE findings SET status = ? WHERE id = ?", + (payload.status, finding_id), + ) + await db.commit() + except Exception as e: + if "no such column: status" in str(e).lower(): + await db.execute( + "ALTER TABLE findings ADD COLUMN status TEXT DEFAULT 'open'" + ) + await db.execute( + "UPDATE findings SET status = ? WHERE id = ?", + (payload.status, finding_id), + ) + await db.commit() + else: + raise e + finally: + await db.close() + + return {"id": finding_id, "status": payload.status} + + @app.get("/jobs/{job_id}/verify") async def get_verify(job_id: str): db = await get_db() diff --git a/backend/app/models.py b/backend/app/models.py index 6aca77b..7b7a4e3 100644 --- a/backend/app/models.py +++ b/backend/app/models.py @@ -16,6 +16,12 @@ class Reachability(BaseModel): evidence: Optional[str] = None +class FindingStatusUpdate(BaseModel): + status: str = Field( + ..., description="The new status: 'open', 'accepted', or 'ignored'" + ) + + class Finding(BaseModel): id: str category: str diff --git a/frontend/src/app/lib/api.ts b/frontend/src/app/lib/api.ts index 9bd4f69..62a9d2d 100644 --- a/frontend/src/app/lib/api.ts +++ b/frontend/src/app/lib/api.ts @@ -104,6 +104,17 @@ export async function getJobFindings(jobId: string): Promise { return (await res.json()) as BackendFinding[]; } +export async function updateFindingStatus(findingId: string, status: "open" | "accepted" | "ignored") { + const res = await fetch(`${API_BASE}/findings/${findingId}/status`, { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ status }), + }); + + if (!res.ok) throw new Error(await res.text()); + return res.json(); +} + export async function fix(jobId: string, findingIds: string[]) { const res = await fetch(`${API_BASE}/fix`, { method: "POST", diff --git a/frontend/src/app/pages/findings.tsx b/frontend/src/app/pages/findings.tsx index fd90b00..82f1c70 100644 --- a/frontend/src/app/pages/findings.tsx +++ b/frontend/src/app/pages/findings.tsx @@ -28,6 +28,7 @@ import { SheetContent, SheetHeader, SheetTitle, + SheetDescription, } from "../components/ui/sheet"; import { Tabs, @@ -41,7 +42,7 @@ import { CodeBlock } from "../components/code-block"; import { FilterChips } from "../components/filter-chips"; import type { Finding } from "../data/sample-data"; import { loadLastScan } from "../lib/scan-store"; -import { getJobFindings } from "../lib/api"; +import { getJobFindings, updateFindingStatus } from "../lib/api"; import { mapBackendFindingToUi } from "../lib/mappers"; import { cn } from "../components/ui/utils"; @@ -140,6 +141,24 @@ export function Findings() { const [selectedFindings, setSelectedFindings] = useState>(new Set()); const [detailFinding, setDetailFinding] = useState(null); + const [isUpdatingStatus, setIsUpdatingStatus] = useState(false); + + const handleStatusUpdate = async (findingId: string, newStatus: "open" | "accepted" | "ignored") => { + setIsUpdatingStatus(true); + try { + await updateFindingStatus(findingId, newStatus); + setFindings((prev) => + prev.map((f) => f.id === findingId ? { ...f, status: newStatus } : f) + ); + if (detailFinding && detailFinding.id === findingId) { + setDetailFinding({ ...detailFinding, status: newStatus }); + } + } catch (err) { + console.error("Failed to update status", err); + } finally { + setIsUpdatingStatus(false); + } + }; const [searchParams, setSearchParams] = useSearchParams(); @@ -329,7 +348,7 @@ export function Findings() { - {filteredFindings.map((finding) => ( + {filteredFindings.slice(0, 150).map((finding) => (
- {filteredFindings.map((finding) => ( + {filteredFindings.slice(0, 150).map((finding) => ( {detailFinding.title} - {/* 🚨 CRITICAL ACCESSIBILITY FIX: Added SheetDescription to prevent Radix layout crash */} -
+ Finding ID: {detailFinding.id} -
+
-
+
@@ -541,16 +559,51 @@ export function Findings() {
- {/* Status Buttons - Pushed cleanly to the bottom */} + {/* Status Buttons - Dynamic and clickable */}
- - + {detailFinding.status === 'accepted' ? ( + + ) : ( + + )} + + {detailFinding.status === 'ignored' ? ( + + ) : ( + + )}
)}