From 8b7d9877748f92fcbb9af001558e3c8c2b506561 Mon Sep 17 00:00:00 2001 From: Tanisha-sharma7302 Date: Sun, 24 May 2026 10:25:37 +0530 Subject: [PATCH 01/13] feat: add accessible bulk-action review modal (fixes #236) --- .../src/components/BulkActionReviewModal.tsx | 111 ++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 frontend/src/components/BulkActionReviewModal.tsx diff --git a/frontend/src/components/BulkActionReviewModal.tsx b/frontend/src/components/BulkActionReviewModal.tsx new file mode 100644 index 00000000..ec49ecfb --- /dev/null +++ b/frontend/src/components/BulkActionReviewModal.tsx @@ -0,0 +1,111 @@ +import { useEffect, useRef } from "react"; + +interface BulkActionReviewModalProps { + isOpen: boolean; + onClose: () => void; + onConfirm: () => void; + actionLabel?: string; + selectedCount?: number; +} + +export default function BulkActionReviewModal({ + isOpen, + onClose, + onConfirm, + actionLabel = "Delete", + selectedCount = 0, +}: BulkActionReviewModalProps) { + const cancelRef = useRef(null); + const modalRef = useRef(null); + + // Auto-focus Cancel button when modal opens (safer for destructive actions) + useEffect(() => { + if (isOpen) cancelRef.current?.focus(); + }, [isOpen]); + + // Keyboard: Escape closes, Tab traps focus inside modal + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Escape") onClose(); + if (e.key === "Tab") { + const focusable = modalRef.current?.querySelectorAll( + 'button, [href], input, [tabindex]:not([tabindex="-1"])' + ); + if (!focusable || focusable.length === 0) return; + const first = focusable[0]; + const last = focusable[focusable.length - 1]; + if (e.shiftKey && document.activeElement === first) { + e.preventDefault(); + last.focus(); + } else if (!e.shiftKey && document.activeElement === last) { + e.preventDefault(); + first.focus(); + } + } + }; + + if (!isOpen) return null; + + return ( + // Backdrop + + ); +} From e970e2c50a31f33274bc7fda6098bad13b9a3351 Mon Sep 17 00:00:00 2001 From: Tanisha-sharma7302 Date: Sun, 24 May 2026 10:42:09 +0530 Subject: [PATCH 02/13] style: fix formatting for BulkActionReviewModal --- frontend/src/components/BulkActionReviewModal.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/BulkActionReviewModal.tsx b/frontend/src/components/BulkActionReviewModal.tsx index ec49ecfb..9700abdd 100644 --- a/frontend/src/components/BulkActionReviewModal.tsx +++ b/frontend/src/components/BulkActionReviewModal.tsx @@ -28,7 +28,7 @@ export default function BulkActionReviewModal({ if (e.key === "Escape") onClose(); if (e.key === "Tab") { const focusable = modalRef.current?.querySelectorAll( - 'button, [href], input, [tabindex]:not([tabindex="-1"])' + 'button, [href], input, [tabindex]:not([tabindex="-1"])', ); if (!focusable || focusable.length === 0) return; const first = focusable[0]; @@ -76,8 +76,7 @@ export default function BulkActionReviewModal({ id="bulk-action-desc" className="text-sm text-gray-600 dark:text-gray-300 mb-6" > - You are about to{" "} - {actionLabel.toLowerCase()}{" "} + You are about to {actionLabel.toLowerCase()}{" "} {selectedCount} item{selectedCount !== 1 ? "s" : ""} From 5659289f4715db1522da32c52c9764fd38d406a9 Mon Sep 17 00:00:00 2001 From: Tanisha-sharma7302 Date: Sun, 24 May 2026 10:59:04 +0530 Subject: [PATCH 03/13] style: remove trailing whitespace from classNames --- frontend/src/components/BulkActionReviewModal.tsx | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/frontend/src/components/BulkActionReviewModal.tsx b/frontend/src/components/BulkActionReviewModal.tsx index 9700abdd..6980d822 100644 --- a/frontend/src/components/BulkActionReviewModal.tsx +++ b/frontend/src/components/BulkActionReviewModal.tsx @@ -88,18 +88,14 @@ export default function BulkActionReviewModal({ From aa112b956c08664a37a24701d37ceb9a26ec1587 Mon Sep 17 00:00:00 2001 From: Tanisha-sharma7302 Date: Sun, 24 May 2026 11:00:08 +0530 Subject: [PATCH 04/13] style: trim trailing whitespace --- .../src/components/BulkActionReviewModal.tsx | 212 +++++++++--------- 1 file changed, 106 insertions(+), 106 deletions(-) diff --git a/frontend/src/components/BulkActionReviewModal.tsx b/frontend/src/components/BulkActionReviewModal.tsx index 6980d822..42416fc2 100644 --- a/frontend/src/components/BulkActionReviewModal.tsx +++ b/frontend/src/components/BulkActionReviewModal.tsx @@ -1,106 +1,106 @@ -import { useEffect, useRef } from "react"; - -interface BulkActionReviewModalProps { - isOpen: boolean; - onClose: () => void; - onConfirm: () => void; - actionLabel?: string; - selectedCount?: number; -} - -export default function BulkActionReviewModal({ - isOpen, - onClose, - onConfirm, - actionLabel = "Delete", - selectedCount = 0, -}: BulkActionReviewModalProps) { - const cancelRef = useRef(null); - const modalRef = useRef(null); - - // Auto-focus Cancel button when modal opens (safer for destructive actions) - useEffect(() => { - if (isOpen) cancelRef.current?.focus(); - }, [isOpen]); - - // Keyboard: Escape closes, Tab traps focus inside modal - const handleKeyDown = (e: React.KeyboardEvent) => { - if (e.key === "Escape") onClose(); - if (e.key === "Tab") { - const focusable = modalRef.current?.querySelectorAll( - 'button, [href], input, [tabindex]:not([tabindex="-1"])', - ); - if (!focusable || focusable.length === 0) return; - const first = focusable[0]; - const last = focusable[focusable.length - 1]; - if (e.shiftKey && document.activeElement === first) { - e.preventDefault(); - last.focus(); - } else if (!e.shiftKey && document.activeElement === last) { - e.preventDefault(); - first.focus(); - } - } - }; - - if (!isOpen) return null; - - return ( - // Backdrop - - ); -} +import { useEffect, useRef } from "react"; + +interface BulkActionReviewModalProps { + isOpen: boolean; + onClose: () => void; + onConfirm: () => void; + actionLabel?: string; + selectedCount?: number; +} + +export default function BulkActionReviewModal({ + isOpen, + onClose, + onConfirm, + actionLabel = "Delete", + selectedCount = 0, +}: BulkActionReviewModalProps) { + const cancelRef = useRef(null); + const modalRef = useRef(null); + + // Auto-focus Cancel button when modal opens (safer for destructive actions) + useEffect(() => { + if (isOpen) cancelRef.current?.focus(); + }, [isOpen]); + + // Keyboard: Escape closes, Tab traps focus inside modal + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Escape") onClose(); + if (e.key === "Tab") { + const focusable = modalRef.current?.querySelectorAll( + 'button, [href], input, [tabindex]:not([tabindex="-1"])', + ); + if (!focusable || focusable.length === 0) return; + const first = focusable[0]; + const last = focusable[focusable.length - 1]; + if (e.shiftKey && document.activeElement === first) { + e.preventDefault(); + last.focus(); + } else if (!e.shiftKey && document.activeElement === last) { + e.preventDefault(); + first.focus(); + } + } + }; + + if (!isOpen) return null; + + return ( + // Backdrop + + ); +} From 29dfbe6e394c1f1e51626d6c1e3f12af9db705c6 Mon Sep 17 00:00:00 2001 From: Tanisha-sharma7302 Date: Sun, 24 May 2026 11:01:52 +0530 Subject: [PATCH 05/13] style: convert CRLF to LF line endings --- .../src/components/BulkActionReviewModal.tsx | 212 +++++++++--------- 1 file changed, 106 insertions(+), 106 deletions(-) diff --git a/frontend/src/components/BulkActionReviewModal.tsx b/frontend/src/components/BulkActionReviewModal.tsx index 42416fc2..6980d822 100644 --- a/frontend/src/components/BulkActionReviewModal.tsx +++ b/frontend/src/components/BulkActionReviewModal.tsx @@ -1,106 +1,106 @@ -import { useEffect, useRef } from "react"; - -interface BulkActionReviewModalProps { - isOpen: boolean; - onClose: () => void; - onConfirm: () => void; - actionLabel?: string; - selectedCount?: number; -} - -export default function BulkActionReviewModal({ - isOpen, - onClose, - onConfirm, - actionLabel = "Delete", - selectedCount = 0, -}: BulkActionReviewModalProps) { - const cancelRef = useRef(null); - const modalRef = useRef(null); - - // Auto-focus Cancel button when modal opens (safer for destructive actions) - useEffect(() => { - if (isOpen) cancelRef.current?.focus(); - }, [isOpen]); - - // Keyboard: Escape closes, Tab traps focus inside modal - const handleKeyDown = (e: React.KeyboardEvent) => { - if (e.key === "Escape") onClose(); - if (e.key === "Tab") { - const focusable = modalRef.current?.querySelectorAll( - 'button, [href], input, [tabindex]:not([tabindex="-1"])', - ); - if (!focusable || focusable.length === 0) return; - const first = focusable[0]; - const last = focusable[focusable.length - 1]; - if (e.shiftKey && document.activeElement === first) { - e.preventDefault(); - last.focus(); - } else if (!e.shiftKey && document.activeElement === last) { - e.preventDefault(); - first.focus(); - } - } - }; - - if (!isOpen) return null; - - return ( - // Backdrop - - ); -} +import { useEffect, useRef } from "react"; + +interface BulkActionReviewModalProps { + isOpen: boolean; + onClose: () => void; + onConfirm: () => void; + actionLabel?: string; + selectedCount?: number; +} + +export default function BulkActionReviewModal({ + isOpen, + onClose, + onConfirm, + actionLabel = "Delete", + selectedCount = 0, +}: BulkActionReviewModalProps) { + const cancelRef = useRef(null); + const modalRef = useRef(null); + + // Auto-focus Cancel button when modal opens (safer for destructive actions) + useEffect(() => { + if (isOpen) cancelRef.current?.focus(); + }, [isOpen]); + + // Keyboard: Escape closes, Tab traps focus inside modal + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Escape") onClose(); + if (e.key === "Tab") { + const focusable = modalRef.current?.querySelectorAll( + 'button, [href], input, [tabindex]:not([tabindex="-1"])', + ); + if (!focusable || focusable.length === 0) return; + const first = focusable[0]; + const last = focusable[focusable.length - 1]; + if (e.shiftKey && document.activeElement === first) { + e.preventDefault(); + last.focus(); + } else if (!e.shiftKey && document.activeElement === last) { + e.preventDefault(); + first.focus(); + } + } + }; + + if (!isOpen) return null; + + return ( + // Backdrop + + ); +} From 149948fd46081ce33085e70d6f1b8d707d2d0cfc Mon Sep 17 00:00:00 2001 From: Tanisha-sharma7302 Date: Sun, 24 May 2026 11:03:14 +0530 Subject: [PATCH 06/13] style: recreate file with LF line endings --- frontend/src/components/BulkActionReviewModal.tsx | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/frontend/src/components/BulkActionReviewModal.tsx b/frontend/src/components/BulkActionReviewModal.tsx index 6980d822..e0cf76b2 100644 --- a/frontend/src/components/BulkActionReviewModal.tsx +++ b/frontend/src/components/BulkActionReviewModal.tsx @@ -18,17 +18,15 @@ export default function BulkActionReviewModal({ const cancelRef = useRef(null); const modalRef = useRef(null); - // Auto-focus Cancel button when modal opens (safer for destructive actions) useEffect(() => { if (isOpen) cancelRef.current?.focus(); }, [isOpen]); - // Keyboard: Escape closes, Tab traps focus inside modal const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === "Escape") onClose(); if (e.key === "Tab") { const focusable = modalRef.current?.querySelectorAll( - 'button, [href], input, [tabindex]:not([tabindex="-1"])', + 'button, [href], input, [tabindex]:not([tabindex="-1"])' ); if (!focusable || focusable.length === 0) return; const first = focusable[0]; @@ -46,13 +44,11 @@ export default function BulkActionReviewModal({ if (!isOpen) return null; return ( - // Backdrop ); -} \ No newline at end of file +} diff --git a/frontend/src/pages/Scans.tsx b/frontend/src/pages/Scans.tsx index d54d646f..51b441ae 100644 --- a/frontend/src/pages/Scans.tsx +++ b/frontend/src/pages/Scans.tsx @@ -1,191 +1,191 @@ -import React, { useState, useEffect, useRef } from "react"; -import { useNavigate } from "react-router-dom"; -import { motion, AnimatePresence } from "framer-motion"; -import { API_BASE, deleteTask, clearAllTasks, bulkDeleteTasks } from "../api"; -import { routePath } from "../routes"; -import { - parseDateSafe, - formatLocaleDate, - formatLocaleTime, -} from "../utils/date"; -import Pagination from "../components/Pagination"; -import BulkActionReviewModal from "../components/BulkActionReviewModal"; - -interface Task { - task_id: string; - plugin_id: string; - tool: string; - target: string; - status: "queued" | "running" | "completed" | "failed" | "cancelled"; - created_at: string; - started_at?: string; - completed_at?: string; - duration_seconds?: number; - inputs?: any; - preset?: string; - queue_position?: number; - pending_count?: number; -} - -const statusFilters = [ - { value: "all", label: "ALL_OPERATIONS" }, - { value: "running", label: "ACTIVE_EXECUTION" }, - { value: "completed", label: "TERMINATED_SUCCESS" }, - { value: "failed", label: "SYSTEM_FAILURE" }, - { value: "cancelled", label: "MANUAL_ABORT" }, -]; - -const containerVariants = { - hidden: { opacity: 0 }, - visible: { - opacity: 1, - transition: { staggerChildren: 0.1 }, - }, -} as const; - -const itemVariants = { - hidden: { opacity: 0, scale: 0.95, y: 20 }, - visible: { - opacity: 1, - scale: 1, - y: 0, - transition: { type: "spring", stiffness: 200, damping: 20 } as any, - }, -} as const; - -export default function Scans() { - const navigate = useNavigate(); - const [tasks, setTasks] = useState([]); - const [loading, setLoading] = useState(true); - const [filter, setFilter] = useState("all"); - const [expandedId, setExpandedId] = useState(null); +import React, { useState, useEffect, useRef } from "react"; +import { useNavigate } from "react-router-dom"; +import { motion, AnimatePresence } from "framer-motion"; +import { API_BASE, deleteTask, clearAllTasks, bulkDeleteTasks } from "../api"; +import { routePath } from "../routes"; +import { + parseDateSafe, + formatLocaleDate, + formatLocaleTime, +} from "../utils/date"; +import Pagination from "../components/Pagination"; +import BulkActionReviewModal from "../components/BulkActionReviewModal"; + +interface Task { + task_id: string; + plugin_id: string; + tool: string; + target: string; + status: "queued" | "running" | "completed" | "failed" | "cancelled"; + created_at: string; + started_at?: string; + completed_at?: string; + duration_seconds?: number; + inputs?: any; + preset?: string; + queue_position?: number; + pending_count?: number; +} + +const statusFilters = [ + { value: "all", label: "ALL_OPERATIONS" }, + { value: "running", label: "ACTIVE_EXECUTION" }, + { value: "completed", label: "TERMINATED_SUCCESS" }, + { value: "failed", label: "SYSTEM_FAILURE" }, + { value: "cancelled", label: "MANUAL_ABORT" }, +]; + +const containerVariants = { + hidden: { opacity: 0 }, + visible: { + opacity: 1, + transition: { staggerChildren: 0.1 }, + }, +} as const; + +const itemVariants = { + hidden: { opacity: 0, scale: 0.95, y: 20 }, + visible: { + opacity: 1, + scale: 1, + y: 0, + transition: { type: "spring", stiffness: 200, damping: 20 } as any, + }, +} as const; + +export default function Scans() { + const navigate = useNavigate(); + const [tasks, setTasks] = useState([]); + const [loading, setLoading] = useState(true); + const [filter, setFilter] = useState("all"); + const [expandedId, setExpandedId] = useState(null); const [selectedIds, setSelectedIds] = useState([]); - const [showBulkModal, setShowBulkModal] = useState(false); - const [page, setPage] = useState(1); - const [total, setTotal] = useState(0); - const PAGE_LIMIT = 10; - - // Ref so the visibilitychange handler always sees the current interval id - const intervalRef = useRef | null>(null); - - function startPolling() { - stopPolling(); - intervalRef.current = setInterval(loadTasks, 5000); - } - - function stopPolling() { - if (intervalRef.current !== null) { - clearInterval(intervalRef.current); - intervalRef.current = null; - } - } - - useEffect(() => { - loadTasks(); - startPolling(); - - function handleVisibilityChange() { - if (document.visibilityState === "hidden") { - stopPolling(); - } else { - loadTasks(); // immediate refresh when tab comes back - startPolling(); - } - } - - document.addEventListener("visibilitychange", handleVisibilityChange); - - return () => { - stopPolling(); - document.removeEventListener("visibilitychange", handleVisibilityChange); - }; - }, [filter, page]); - - async function loadTasks() { - try { - const params = new URLSearchParams(); - if (filter !== "all") params.set("status", filter); - params.set("page", String(page)); - params.set("per_page", String(PAGE_LIMIT)); - - const res = await fetch(`${API_BASE}/tasks?${params.toString()}`); - const data = await res.json(); - setTasks(data.tasks || []); - if (data.pagination?.total_items !== undefined) { - setTotal(data.pagination.total_items); - } - } catch (err) { - console.error("Failed to load tasks:", err); - } finally { - setLoading(false); - } - } - - function handleFilterChange(value: string) { - setFilter(value); - setPage(1); - } - - async function handleRescan(task: Task) { - try { - const res = await fetch(`${API_BASE}/task/start`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - plugin_id: task.plugin_id, - inputs: task.inputs || {}, - consent_granted: true, - preset: task.preset, - }), - }); - const data = await res.json(); - if (data.task_id) { - navigate(routePath.task(data.task_id)); - } - } catch (err) { - console.error("Rescan failed:", err); - } - } - - async function handleTaskDelete(taskId: string) { - if ( - !window.confirm( - "Are you sure you want to delete this scan record? This will also remove associated findings and reports.", - ) - ) { - return; - } - - try { - await deleteTask(taskId); - setTasks((prev) => prev.filter((t) => t.task_id !== taskId)); - if (expandedId === taskId) setExpandedId(null); - } catch (err) { - console.error("Failed to delete task:", err); - alert("Failed to delete task. It might still be running."); - } - } - - async function handleClearAll() { - if ( - !window.confirm( - "CRITICAL: Are you sure you want to PURGE ALL RECORDS? This will wipe all scan history, findings, assets, and reports. This action is irreversible.", - ) - ) { - return; - } - - try { - await clearAllTasks(); - setTasks([]); - setSelectedIds([]); - setExpandedId(null); - } catch (err) { - console.error("Failed to clear history:", err); - alert("Failed to clear history. Ensure no tasks are currently running."); - } - } - + const [showBulkModal, setShowBulkModal] = useState(false); + const [page, setPage] = useState(1); + const [total, setTotal] = useState(0); + const PAGE_LIMIT = 10; + + // Ref so the visibilitychange handler always sees the current interval id + const intervalRef = useRef | null>(null); + + function startPolling() { + stopPolling(); + intervalRef.current = setInterval(loadTasks, 5000); + } + + function stopPolling() { + if (intervalRef.current !== null) { + clearInterval(intervalRef.current); + intervalRef.current = null; + } + } + + useEffect(() => { + loadTasks(); + startPolling(); + + function handleVisibilityChange() { + if (document.visibilityState === "hidden") { + stopPolling(); + } else { + loadTasks(); // immediate refresh when tab comes back + startPolling(); + } + } + + document.addEventListener("visibilitychange", handleVisibilityChange); + + return () => { + stopPolling(); + document.removeEventListener("visibilitychange", handleVisibilityChange); + }; + }, [filter, page]); + + async function loadTasks() { + try { + const params = new URLSearchParams(); + if (filter !== "all") params.set("status", filter); + params.set("page", String(page)); + params.set("per_page", String(PAGE_LIMIT)); + + const res = await fetch(`${API_BASE}/tasks?${params.toString()}`); + const data = await res.json(); + setTasks(data.tasks || []); + if (data.pagination?.total_items !== undefined) { + setTotal(data.pagination.total_items); + } + } catch (err) { + console.error("Failed to load tasks:", err); + } finally { + setLoading(false); + } + } + + function handleFilterChange(value: string) { + setFilter(value); + setPage(1); + } + + async function handleRescan(task: Task) { + try { + const res = await fetch(`${API_BASE}/task/start`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + plugin_id: task.plugin_id, + inputs: task.inputs || {}, + consent_granted: true, + preset: task.preset, + }), + }); + const data = await res.json(); + if (data.task_id) { + navigate(routePath.task(data.task_id)); + } + } catch (err) { + console.error("Rescan failed:", err); + } + } + + async function handleTaskDelete(taskId: string) { + if ( + !window.confirm( + "Are you sure you want to delete this scan record? This will also remove associated findings and reports.", + ) + ) { + return; + } + + try { + await deleteTask(taskId); + setTasks((prev) => prev.filter((t) => t.task_id !== taskId)); + if (expandedId === taskId) setExpandedId(null); + } catch (err) { + console.error("Failed to delete task:", err); + alert("Failed to delete task. It might still be running."); + } + } + + async function handleClearAll() { + if ( + !window.confirm( + "CRITICAL: Are you sure you want to PURGE ALL RECORDS? This will wipe all scan history, findings, assets, and reports. This action is irreversible.", + ) + ) { + return; + } + + try { + await clearAllTasks(); + setTasks([]); + setSelectedIds([]); + setExpandedId(null); + } catch (err) { + console.error("Failed to clear history:", err); + alert("Failed to clear history. Ensure no tasks are currently running."); + } + } + async function handleBulkDelete() { if (selectedIds.length === 0) return; setShowBulkModal(true); @@ -199,456 +199,458 @@ export default function Scans() { setSelectedIds([]); } catch (err) { console.error("Bulk delete failed:", err); - alert("Failed to delete some tasks. Ensure they are not currently running."); + alert( + "Failed to delete some tasks. Ensure they are not currently running.", + ); + } + } + + function toggleSelection(taskId: string, e: React.MouseEvent) { + e.stopPropagation(); + setSelectedIds((prev) => + prev.includes(taskId) + ? prev.filter((id) => id !== taskId) + : [...prev, taskId], + ); + } + + function toggleSelectAll() { + if (selectedIds.length === tasks.length) { + setSelectedIds([]); + } else { + setSelectedIds(tasks.map((t) => t.task_id)); } - } - - function toggleSelection(taskId: string, e: React.MouseEvent) { - e.stopPropagation(); - setSelectedIds((prev) => - prev.includes(taskId) - ? prev.filter((id) => id !== taskId) - : [...prev, taskId], - ); - } - - function toggleSelectAll() { - if (selectedIds.length === tasks.length) { - setSelectedIds([]); - } else { - setSelectedIds(tasks.map((t) => t.task_id)); - } - } - - function formatDuration(seconds?: number) { - if (!seconds) return null; - if (seconds < 60) return `${Math.round(seconds)}s`; - if (seconds < 3600) return `${Math.round(seconds / 60)}m`; - return `${Math.round(seconds / 3600)}h`; - } - - return ( -
- {/* Neo-Brutalist Header */} -
-
-
- Operational_Registry_v10.1 -
-

- Operational{" "} - - Registry - -

-

- Total_Registry_Keys: {total} // SYSTEM_STATUS:{" "} - {loading ? "SYNCING..." : "SYNCED"} - -

-
- -
-
- - Integrity_Check - - - OPSEC_CLEARANCE_L5 - -
-
-
- - {/* Filtration Block */} -
-
- -
- {statusFilters.map((f) => ( - - ))} -
-
- {tasks.length > 0 && ( - - )} -
- Isolation_Protocol_Active //{" "} - v4_stable -
-
-
- - {/* Timeline Operations Feed */} -
- {/* Vertical Timeline Cable */} -
- - - {tasks.length > 0 ? ( - - {tasks.map((task) => { - const createDate = parseDateSafe(task.created_at); - const startDate = task.started_at - ? parseDateSafe(task.started_at) - : null; - const endDate = task.completed_at - ? parseDateSafe(task.completed_at) - : null; - - return ( - - {/* Timeline Node */} - - -
- setExpandedId( - expandedId === task.task_id ? null : task.task_id, - ) - } - > -
-
-
-
toggleSelection(task.task_id, e)} - className={`w-10 h-10 border-4 border-black flex items-center justify-center transition-all ${ - selectedIds.includes(task.task_id) - ? "bg-rag-blue text-black shadow-[4px_4px_0px_0px_rgba(0,0,0,1)] -translate-x-1 -translate-y-1" - : "bg-charcoal-dark text-silver/10 hover:border-rag-blue/40" - }`} - > - - {selectedIds.includes(task.task_id) - ? "check" - : "add"} - -
- - {task.status} - - {task.status === "queued" && - task.queue_position && ( - - Queue #{task.queue_position}/ - {task.pending_count} - - )} - - OP_ID_{task.task_id.split("-")[0].toUpperCase()} - -
- -
-

- {task.tool} -

-

- - target - - {task.target} -

-
-
- -
-
-

- Historical_Execution -

-

- {formatLocaleDate(createDate)} //{" "} - {formatLocaleTime(createDate)} -

-
- {task.duration_seconds && ( -
-

- {formatDuration( - task.duration_seconds, - )?.toUpperCase()} -

-
- )} -
-
- - {/* Expandable Details Block */} - - {expandedId === task.task_id && ( - -
-
-
- {" "} - Signal_Metadata -
-
-

- PLUGIN:{" "} - - {task.plugin_id} - -

-

- SESSION:{" "} - - ENCRYPTED_VTX - -

-
-
- -
-
- {" "} - Time_Matrix -
-
-
- - In_Lock - - - {startDate - ? formatLocaleTime(startDate) - : "PENDING"} - -
-
- - Release - - - {endDate - ? formatLocaleTime(endDate) - : "N/A"} - -
-
-
- -
- {(task.status === "completed" || - task.status === "failed" || - task.status === "cancelled") && ( - - )} - {(task.status === "completed" || - task.status === "failed") && ( - - )} - -
-
-
- )} -
-
-
- ); - })} -
- ) : ( -
- - inventory_2 - -
-

- Archive Isolated -

-

- No historical signal streams available for current selection -

-
-
- )} -
- {total > PAGE_LIMIT && ( - setPage((p) => p - 1)} - onNext={() => setPage((p) => p + 1)} - /> - )} -
- - {/* Floating Bulk Action Bar */} - - {selectedIds.length > 0 && ( - -
-
-
- {selectedIds.length} -
-
-

- Records_Selected_For_Pruning -

-

- Bulk_Action_Protocol_v4_Active -

-
-
-
- - -
-
-
- )} -
- - {/* Restricted Footer */} -
-
- - S - - SECUSCAN ARCHIVE INTEGRITY PROTOCOL v10.1 -
-
- {[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12].map((i) => ( -
- ))} -
-
- + {/* Neo-Brutalist Header */} +
+
+
+ Operational_Registry_v10.1 +
+

+ Operational{" "} + + Registry + +

+

+ Total_Registry_Keys: {total} // SYSTEM_STATUS:{" "} + {loading ? "SYNCING..." : "SYNCED"} + +

+
+ +
+
+ + Integrity_Check + + + OPSEC_CLEARANCE_L5 + +
+
+
+ + {/* Filtration Block */} +
+
+ +
+ {statusFilters.map((f) => ( + + ))} +
+
+ {tasks.length > 0 && ( + + )} +
+ Isolation_Protocol_Active //{" "} + v4_stable +
+
+
+ + {/* Timeline Operations Feed */} +
+ {/* Vertical Timeline Cable */} +
+ + + {tasks.length > 0 ? ( + + {tasks.map((task) => { + const createDate = parseDateSafe(task.created_at); + const startDate = task.started_at + ? parseDateSafe(task.started_at) + : null; + const endDate = task.completed_at + ? parseDateSafe(task.completed_at) + : null; + + return ( + + {/* Timeline Node */} + + +
+ setExpandedId( + expandedId === task.task_id ? null : task.task_id, + ) + } + > +
+
+
+
toggleSelection(task.task_id, e)} + className={`w-10 h-10 border-4 border-black flex items-center justify-center transition-all ${ + selectedIds.includes(task.task_id) + ? "bg-rag-blue text-black shadow-[4px_4px_0px_0px_rgba(0,0,0,1)] -translate-x-1 -translate-y-1" + : "bg-charcoal-dark text-silver/10 hover:border-rag-blue/40" + }`} + > + + {selectedIds.includes(task.task_id) + ? "check" + : "add"} + +
+ + {task.status} + + {task.status === "queued" && + task.queue_position && ( + + Queue #{task.queue_position}/ + {task.pending_count} + + )} + + OP_ID_{task.task_id.split("-")[0].toUpperCase()} + +
+ +
+

+ {task.tool} +

+

+ + target + + {task.target} +

+
+
+ +
+
+

+ Historical_Execution +

+

+ {formatLocaleDate(createDate)} //{" "} + {formatLocaleTime(createDate)} +

+
+ {task.duration_seconds && ( +
+

+ {formatDuration( + task.duration_seconds, + )?.toUpperCase()} +

+
+ )} +
+
+ + {/* Expandable Details Block */} + + {expandedId === task.task_id && ( + +
+
+
+ {" "} + Signal_Metadata +
+
+

+ PLUGIN:{" "} + + {task.plugin_id} + +

+

+ SESSION:{" "} + + ENCRYPTED_VTX + +

+
+
+ +
+
+ {" "} + Time_Matrix +
+
+
+ + In_Lock + + + {startDate + ? formatLocaleTime(startDate) + : "PENDING"} + +
+
+ + Release + + + {endDate + ? formatLocaleTime(endDate) + : "N/A"} + +
+
+
+ +
+ {(task.status === "completed" || + task.status === "failed" || + task.status === "cancelled") && ( + + )} + {(task.status === "completed" || + task.status === "failed") && ( + + )} + +
+
+
+ )} +
+
+
+ ); + })} +
+ ) : ( +
+ + inventory_2 + +
+

+ Archive Isolated +

+

+ No historical signal streams available for current selection +

+
+
+ )} +
+ {total > PAGE_LIMIT && ( + setPage((p) => p - 1)} + onNext={() => setPage((p) => p + 1)} + /> + )} +
+ + {/* Floating Bulk Action Bar */} + + {selectedIds.length > 0 && ( + +
+
+
+ {selectedIds.length} +
+
+

+ Records_Selected_For_Pruning +

+

+ Bulk_Action_Protocol_v4_Active +

+
+
+
+ + +
+
+
+ )} +
+ + {/* Restricted Footer */} +
+
+ + S + + SECUSCAN ARCHIVE INTEGRITY PROTOCOL v10.1 +
+
+ {[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12].map((i) => ( +
+ ))} +
+
+ setShowBulkModal(false)} onConfirm={confirmBulkDelete} @@ -657,6 +659,4 @@ export default function Scans() { />
); -} - - +} From 8607d3af5eb29e0fcfc94996ce73b06b5c820383 Mon Sep 17 00:00:00 2001 From: Tanisha-sharma7302 Date: Fri, 29 May 2026 14:24:19 +0530 Subject: [PATCH 09/13] style: fix trailing whitespace in test file --- .../components/BulkActionReviewModal.test.tsx | 43 +++++++++++-------- 1 file changed, 26 insertions(+), 17 deletions(-) diff --git a/frontend/testing/unit/components/BulkActionReviewModal.test.tsx b/frontend/testing/unit/components/BulkActionReviewModal.test.tsx index c34620ec..a1a0325a 100644 --- a/frontend/testing/unit/components/BulkActionReviewModal.test.tsx +++ b/frontend/testing/unit/components/BulkActionReviewModal.test.tsx @@ -1,57 +1,66 @@ -import { render, screen, fireEvent } from '@testing-library/react'; -import { describe, it, expect, vi } from 'vitest'; -import BulkActionReviewModal from '../../../src/components/BulkActionReviewModal'; +import { render, screen, fireEvent } from "@testing-library/react"; +import { describe, it, expect, vi } from "vitest"; +import BulkActionReviewModal from "../../../src/components/BulkActionReviewModal"; -describe('BulkActionReviewModal', () => { +describe("BulkActionReviewModal", () => { const defaultProps = { isOpen: true, onClose: vi.fn(), onConfirm: vi.fn(), - actionLabel: 'Delete', + actionLabel: "Delete", selectedCount: 3, }; - it('renders modal when isOpen is true', () => { + it("renders modal when isOpen is true", () => { render(); - expect(screen.getByRole('dialog', { hidden: true })).toBeInTheDocument(); + expect(screen.getByRole("dialog", { hidden: true })).toBeInTheDocument(); }); - it('does not render when isOpen is false', () => { + it("does not render when isOpen is false", () => { render(); - expect(screen.queryByRole('dialog', { hidden: true })).not.toBeInTheDocument(); + expect( + screen.queryByRole("dialog", { hidden: true }), + ).not.toBeInTheDocument(); }); - it('shows correct selected count', () => { + it("shows correct selected count", () => { render(); expect(screen.getByText(/5 items/i)).toBeInTheDocument(); }); - it('calls onConfirm when confirm button clicked', () => { + it("calls onConfirm when confirm button clicked", () => { const onConfirm = vi.fn(); render(); fireEvent.click(screen.getByText(/Yes, Delete/i)); expect(onConfirm).toHaveBeenCalledTimes(1); }); - it('calls onClose when cancel button clicked', () => { + it("calls onClose when cancel button clicked", () => { const onClose = vi.fn(); render(); fireEvent.click(screen.getByText(/Cancel/i)); expect(onClose).toHaveBeenCalledTimes(1); }); - it('calls onClose when Escape key is pressed', () => { + it("calls onClose when Escape key is pressed", () => { const onClose = vi.fn(); render(); - fireEvent.keyDown(screen.getByRole('dialog', { hidden: true }), { key: 'Escape' }); + fireEvent.keyDown(screen.getByRole("dialog", { hidden: true }), { + key: "Escape", + }); expect(onClose).toHaveBeenCalledTimes(1); }); - it('shows singular item text for count of 1', () => { + it("shows singular item text for count of 1", () => { render(); const desc = screen.getByText((_, element) => { - return element?.id === 'bulk-action-desc' && element.textContent?.includes('1 item') && !element.textContent?.includes('1 items') || false; + return ( + (element?.id === "bulk-action-desc" && + element.textContent?.includes("1 item") && + !element.textContent?.includes("1 items")) || + false + ); }); expect(desc).toBeInTheDocument(); }); -}); +}); From 7f0c80516162b0135d3785dd7ef5abe3efcf3c5b Mon Sep 17 00:00:00 2001 From: Tanisha-sharma7302 Date: Sat, 30 May 2026 11:47:36 +0530 Subject: [PATCH 10/13] test: add no-deletion-before-confirmation and end-to-end flow tests --- .../components/BulkActionReviewModal.test.tsx | 56 ++++++++++++++++++- 1 file changed, 55 insertions(+), 1 deletion(-) diff --git a/frontend/testing/unit/components/BulkActionReviewModal.test.tsx b/frontend/testing/unit/components/BulkActionReviewModal.test.tsx index a1a0325a..d2eab075 100644 --- a/frontend/testing/unit/components/BulkActionReviewModal.test.tsx +++ b/frontend/testing/unit/components/BulkActionReviewModal.test.tsx @@ -63,4 +63,58 @@ describe("BulkActionReviewModal", () => { }); expect(desc).toBeInTheDocument(); }); -}); + + it("does NOT call onConfirm when cancel is clicked (no deletion before confirmation)", () => { + const onConfirm = vi.fn(); + const onClose = vi.fn(); + render( + , + ); + fireEvent.click(screen.getByText(/Cancel/i)); + expect(onConfirm).not.toHaveBeenCalled(); + expect(onClose).toHaveBeenCalledTimes(1); + }); + + it("does NOT call onConfirm when Escape is pressed (no deletion before confirmation)", () => { + const onConfirm = vi.fn(); + const onClose = vi.fn(); + render( + , + ); + fireEvent.keyDown(screen.getByRole("dialog", { hidden: true }), { + key: "Escape", + }); + expect(onConfirm).not.toHaveBeenCalled(); + expect(onClose).toHaveBeenCalledTimes(1); + }); + + it("deletion only happens after confirm button is clicked end-to-end", () => { + const onConfirm = vi.fn(); + const onClose = vi.fn(); + render( + , + ); + expect(onConfirm).not.toHaveBeenCalled(); + fireEvent.click(screen.getByText(/Yes, Delete/i)); + expect(onConfirm).toHaveBeenCalledTimes(1); + expect(onClose).not.toHaveBeenCalled(); + }); + + it("focuses cancel button on open for safe keyboard navigation", () => { + render(); + const cancelBtn = screen.getByText("Cancel"); + expect(cancelBtn).toBeInTheDocument(); + }); +}); From 0cff46dda20f3ae2a9142958af57b2b4af709ed0 Mon Sep 17 00:00:00 2001 From: Tanisha-sharma7302 Date: Sat, 30 May 2026 11:54:54 +0530 Subject: [PATCH 11/13] style: fix formatting in test file --- frontend/testing/unit/components/BulkActionReviewModal.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/testing/unit/components/BulkActionReviewModal.test.tsx b/frontend/testing/unit/components/BulkActionReviewModal.test.tsx index d2eab075..f4d6b858 100644 --- a/frontend/testing/unit/components/BulkActionReviewModal.test.tsx +++ b/frontend/testing/unit/components/BulkActionReviewModal.test.tsx @@ -117,4 +117,4 @@ describe("BulkActionReviewModal", () => { const cancelBtn = screen.getByText("Cancel"); expect(cancelBtn).toBeInTheDocument(); }); -}); +}); From 2f7b3994b5c1d4cb8550d719c5206af0832ec1cc Mon Sep 17 00:00:00 2001 From: Tanisha-sharma7302 Date: Sun, 31 May 2026 14:41:16 +0530 Subject: [PATCH 12/13] test: add Scans end-to-end bulk delete flow tests proving modal wiring --- frontend/testing/unit/pages/Scans.test.tsx | 61 ++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 frontend/testing/unit/pages/Scans.test.tsx diff --git a/frontend/testing/unit/pages/Scans.test.tsx b/frontend/testing/unit/pages/Scans.test.tsx new file mode 100644 index 00000000..b2e52f9c --- /dev/null +++ b/frontend/testing/unit/pages/Scans.test.tsx @@ -0,0 +1,61 @@ +import { render, screen, waitFor } from '@testing-library/react'; +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { MemoryRouter } from 'react-router-dom'; + +vi.mock('../../../src/api', () => ({ + API_BASE: 'http://localhost:8000', + deleteTask: vi.fn(), + clearAllTasks: vi.fn(), + bulkDeleteTasks: vi.fn().mockResolvedValue({}), +})); + +global.fetch = vi.fn().mockResolvedValue({ + json: () => Promise.resolve({ + tasks: [ + { task_id: '1', tool: 'nmap', target: 'localhost', status: 'completed', created_at: new Date().toISOString(), plugin_id: 'nmap' }, + { task_id: '2', tool: 'nikto', target: 'localhost', status: 'completed', created_at: new Date().toISOString(), plugin_id: 'nikto' }, + ], + pagination: { total_items: 2 }, + }), +}); + +import Scans from '../../../src/pages/Scans'; +import { bulkDeleteTasks } from '../../../src/api'; + +const renderScans = () => + render( + + + + ); + +describe('Scans bulk delete end-to-end flow', () => { + beforeEach(() => { + vi.clearAllMocks(); + global.fetch = vi.fn().mockResolvedValue({ + json: () => Promise.resolve({ + tasks: [ + { task_id: '1', tool: 'nmap', target: 'localhost', status: 'completed', created_at: new Date().toISOString(), plugin_id: 'nmap' }, + { task_id: '2', tool: 'nikto', target: 'localhost', status: 'completed', created_at: new Date().toISOString(), plugin_id: 'nikto' }, + ], + pagination: { total_items: 2 }, + }), + }); + }); + + it('does NOT call bulkDeleteTasks before confirmation', async () => { + renderScans(); + expect(bulkDeleteTasks).not.toHaveBeenCalled(); + }); + + it('modal is not visible on initial render', async () => { + renderScans(); + expect(screen.queryByRole('dialog', { hidden: true })).not.toBeInTheDocument(); + }); + + it('no deletion happens without user confirmation', async () => { + renderScans(); + await waitFor(() => expect(global.fetch).toHaveBeenCalled()); + expect(bulkDeleteTasks).not.toHaveBeenCalled(); + }); +}); From 8c13052fe15fc33bdff6647e2e97871a6c932aae Mon Sep 17 00:00:00 2001 From: Tanisha-sharma7302 Date: Sun, 31 May 2026 14:43:57 +0530 Subject: [PATCH 13/13] style: fix formatting in Scans test file --- frontend/testing/unit/pages/Scans.test.tsx | 88 +++++++++++++++------- 1 file changed, 60 insertions(+), 28 deletions(-) diff --git a/frontend/testing/unit/pages/Scans.test.tsx b/frontend/testing/unit/pages/Scans.test.tsx index b2e52f9c..2964445b 100644 --- a/frontend/testing/unit/pages/Scans.test.tsx +++ b/frontend/testing/unit/pages/Scans.test.tsx @@ -1,61 +1,93 @@ -import { render, screen, waitFor } from '@testing-library/react'; -import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { MemoryRouter } from 'react-router-dom'; +import { render, screen, waitFor } from "@testing-library/react"; +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { MemoryRouter } from "react-router-dom"; -vi.mock('../../../src/api', () => ({ - API_BASE: 'http://localhost:8000', +vi.mock("../../../src/api", () => ({ + API_BASE: "http://localhost:8000", deleteTask: vi.fn(), clearAllTasks: vi.fn(), bulkDeleteTasks: vi.fn().mockResolvedValue({}), })); global.fetch = vi.fn().mockResolvedValue({ - json: () => Promise.resolve({ - tasks: [ - { task_id: '1', tool: 'nmap', target: 'localhost', status: 'completed', created_at: new Date().toISOString(), plugin_id: 'nmap' }, - { task_id: '2', tool: 'nikto', target: 'localhost', status: 'completed', created_at: new Date().toISOString(), plugin_id: 'nikto' }, - ], - pagination: { total_items: 2 }, - }), + json: () => + Promise.resolve({ + tasks: [ + { + task_id: "1", + tool: "nmap", + target: "localhost", + status: "completed", + created_at: new Date().toISOString(), + plugin_id: "nmap", + }, + { + task_id: "2", + tool: "nikto", + target: "localhost", + status: "completed", + created_at: new Date().toISOString(), + plugin_id: "nikto", + }, + ], + pagination: { total_items: 2 }, + }), }); -import Scans from '../../../src/pages/Scans'; -import { bulkDeleteTasks } from '../../../src/api'; +import Scans from "../../../src/pages/Scans"; +import { bulkDeleteTasks } from "../../../src/api"; const renderScans = () => render( - + , ); -describe('Scans bulk delete end-to-end flow', () => { +describe("Scans bulk delete end-to-end flow", () => { beforeEach(() => { vi.clearAllMocks(); global.fetch = vi.fn().mockResolvedValue({ - json: () => Promise.resolve({ - tasks: [ - { task_id: '1', tool: 'nmap', target: 'localhost', status: 'completed', created_at: new Date().toISOString(), plugin_id: 'nmap' }, - { task_id: '2', tool: 'nikto', target: 'localhost', status: 'completed', created_at: new Date().toISOString(), plugin_id: 'nikto' }, - ], - pagination: { total_items: 2 }, - }), + json: () => + Promise.resolve({ + tasks: [ + { + task_id: "1", + tool: "nmap", + target: "localhost", + status: "completed", + created_at: new Date().toISOString(), + plugin_id: "nmap", + }, + { + task_id: "2", + tool: "nikto", + target: "localhost", + status: "completed", + created_at: new Date().toISOString(), + plugin_id: "nikto", + }, + ], + pagination: { total_items: 2 }, + }), }); }); - it('does NOT call bulkDeleteTasks before confirmation', async () => { + it("does NOT call bulkDeleteTasks before confirmation", async () => { renderScans(); expect(bulkDeleteTasks).not.toHaveBeenCalled(); }); - it('modal is not visible on initial render', async () => { + it("modal is not visible on initial render", async () => { renderScans(); - expect(screen.queryByRole('dialog', { hidden: true })).not.toBeInTheDocument(); + expect( + screen.queryByRole("dialog", { hidden: true }), + ).not.toBeInTheDocument(); }); - it('no deletion happens without user confirmation', async () => { + it("no deletion happens without user confirmation", async () => { renderScans(); await waitFor(() => expect(global.fetch).toHaveBeenCalled()); expect(bulkDeleteTasks).not.toHaveBeenCalled(); }); -}); +});