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 ────────────────────────────────────────────── */}