From 4926e89e46c2d492e0ad3591f4a6eb83fa146c5e Mon Sep 17 00:00:00 2001 From: domverse Date: Fri, 27 Feb 2026 16:33:45 +0100 Subject: [PATCH] feat: re-run and delete scan from detail page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Backend: - DELETE /api/v1/scans/{id} — 204 on success, 404 if missing, 409 if pending/running; CASCADE removes routes and flights Frontend (api.ts): - scanApi.delete(id) Frontend (ScanDetails.tsx): - Re-run button: derives window_months from stored dates, detects country vs airports mode via comma in scan.country, creates new scan and navigates to it; disabled while scan is active - Delete button: inline two-step confirm (no modal), navigates to dashboard on success; disabled while scan is active Co-Authored-By: Claude Sonnet 4.6 --- flight-comparator/api_server.py | 36 ++++++++ flight-comparator/frontend/src/api.ts | 2 + .../frontend/src/pages/ScanDetails.tsx | 87 +++++++++++++++++++ 3 files changed, 125 insertions(+) diff --git a/flight-comparator/api_server.py b/flight-comparator/api_server.py index 71c3322..e43b049 100644 --- a/flight-comparator/api_server.py +++ b/flight-comparator/api_server.py @@ -1337,6 +1337,42 @@ async def get_scan_status(scan_id: int): ) +@router_v1.delete("/scans/{scan_id}", status_code=204) +async def delete_scan(scan_id: int): + """ + Delete a scan and all its associated routes and flights (CASCADE). + Returns 409 if the scan is currently running or pending. + """ + try: + conn = get_connection() + cursor = conn.cursor() + + cursor.execute("SELECT status FROM scans WHERE id = ?", (scan_id,)) + row = cursor.fetchone() + + if not row: + conn.close() + raise HTTPException(status_code=404, detail=f"Scan not found: {scan_id}") + + if row[0] in ('pending', 'running'): + conn.close() + raise HTTPException( + status_code=409, + detail="Cannot delete a scan that is currently pending or running." + ) + + cursor.execute("DELETE FROM scans WHERE id = ?", (scan_id,)) + conn.commit() + conn.close() + + logging.info(f"Scan {scan_id} deleted") + + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=f"Failed to delete scan: {str(e)}") + + @router_v1.get("/scans/{scan_id}/routes", response_model=PaginatedResponse[Route]) async def get_scan_routes( scan_id: int, diff --git a/flight-comparator/frontend/src/api.ts b/flight-comparator/frontend/src/api.ts index 7b9e1f4..2e736bc 100644 --- a/flight-comparator/frontend/src/api.ts +++ b/flight-comparator/frontend/src/api.ts @@ -123,6 +123,8 @@ export const scanApi = { if (destination) params.destination = destination; return api.get>(`/scans/${id}/flights`, { params }); }, + + delete: (id: number) => api.delete(`/scans/${id}`), }; export const airportApi = { diff --git a/flight-comparator/frontend/src/pages/ScanDetails.tsx b/flight-comparator/frontend/src/pages/ScanDetails.tsx index e2fd98b..94aa88a 100644 --- a/flight-comparator/frontend/src/pages/ScanDetails.tsx +++ b/flight-comparator/frontend/src/pages/ScanDetails.tsx @@ -13,6 +13,8 @@ import { MapPin, AlertCircle, Loader2, + RotateCcw, + Trash2, } from 'lucide-react'; import { scanApi } from '../api'; import type { Scan, Route, Flight } from '../api'; @@ -45,6 +47,9 @@ export default function ScanDetails() { const [expandedRoute, setExpandedRoute] = useState(null); const [flightsByDest, setFlightsByDest] = useState>({}); const [loadingFlights, setLoadingFlights] = useState(null); + const [rerunning, setRerunning] = useState(false); + const [confirmDelete, setConfirmDelete] = useState(false); + const [deleting, setDeleting] = useState(false); useEffect(() => { if (id) loadScanDetails(); @@ -110,6 +115,45 @@ export default function ScanDetails() { } }; + const handleRerun = async () => { + if (!scan) return; + setRerunning(true); + try { + // Compute window from stored dates so the new scan covers the same span + const ms = new Date(scan.end_date).getTime() - new Date(scan.start_date).getTime(); + const window_months = Math.max(1, Math.round(ms / (1000 * 60 * 60 * 24 * 30))); + + // country column holds either "IT" or "BRI,BDS" + const isAirports = scan.country.includes(','); + const resp = await scanApi.create({ + origin: scan.origin, + window_months, + seat_class: scan.seat_class as 'economy' | 'premium' | 'business' | 'first', + adults: scan.adults, + ...(isAirports + ? { destinations: scan.country.split(',') } + : { country: scan.country }), + }); + navigate(`/scans/${resp.data.id}`); + } catch { + // silently fall through — the navigate won't happen + } finally { + setRerunning(false); + } + }; + + const handleDelete = async () => { + if (!scan) return; + setDeleting(true); + try { + await scanApi.delete(scan.id); + navigate('/'); + } catch { + setDeleting(false); + setConfirmDelete(false); + } + }; + const SortIcon = ({ field }: { field: typeof sortField }) => { if (sortField !== field) return ; return sortDirection === 'asc' @@ -203,6 +247,49 @@ export default function ScanDetails() { Created {formatDate(scan.created_at)}

)} + + {/* Row 4: actions */} +
+ {/* Re-run */} + + + {/* Delete — inline confirm */} + {confirmDelete ? ( +
+ Delete this scan? + + +
+ ) : ( + + )} +
{/* ── Stat cards ────────────────────────────────────────────── */}