From a9ae15828dd0e46b2a1c3eb9129ca3ed83bc840e Mon Sep 17 00:00:00 2001 From: Zeltarox Date: Fri, 29 May 2026 23:49:56 +0530 Subject: [PATCH 1/9] feat(ui): accessible confirmation modal replacing window.confirm - Add ConfirmModal component with full accessibility support - Focus trapping, ARIA labels, keyboard navigation - Replace 5 window.confirm dialogs (Scans: 3, Settings: 2) - Add comprehensive unit tests for accessibility features Closes #33 --- backend/package-lock.json | 6 + frontend/package-lock.json | 33 +- frontend/src/components/ConfirmModal.tsx | 155 +++++ frontend/src/pages/Scans.tsx | 134 ++-- frontend/src/pages/Scans.tsx.backup | 625 ++++++++++++++++++ frontend/src/pages/Scans.tsx:Zone.Identifier | 3 + frontend/src/pages/Settings.tsx | 134 ++-- .../unit/components/ConfirmModal.test.tsx | 67 ++ 8 files changed, 1037 insertions(+), 120 deletions(-) create mode 100644 backend/package-lock.json create mode 100644 frontend/src/components/ConfirmModal.tsx create mode 100644 frontend/src/pages/Scans.tsx.backup create mode 100644 frontend/src/pages/Scans.tsx:Zone.Identifier create mode 100644 frontend/testing/unit/components/ConfirmModal.test.tsx diff --git a/backend/package-lock.json b/backend/package-lock.json new file mode 100644 index 00000000..dfb18f11 --- /dev/null +++ b/backend/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "backend", + "lockfileVersion": 3, + "requires": true, + "packages": {} +} diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 90a66524..6267a517 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -143,7 +143,6 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -505,7 +504,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=20.19.0" }, @@ -554,7 +552,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=20.19.0" } @@ -564,7 +561,6 @@ "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.4.0.tgz", "integrity": "sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw==", "license": "MIT", - "peer": true, "dependencies": { "@emotion/memoize": "^0.9.0" } @@ -1670,7 +1666,8 @@ "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@types/babel__core": { "version": "7.20.5", @@ -1793,7 +1790,6 @@ "integrity": "sha512-z+pdZyxE+RTQE9AcboAZCb4otwcrvgHD+GlBpPgn0emDVt0ohrTMhAwlr2Wd9nZ+nihhYFxO2pThz3C5qSu2Eg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.21.0" } @@ -1824,7 +1820,6 @@ "integrity": "sha512-WeqMfGJLGuLCqHGYRGHxnKrXcTitc6L/nBUWfWPcTarG3t9PsquqUMuVeXZeca+mglY4Vo5GZjCi0A3Or2lnxA==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -1836,7 +1831,6 @@ "integrity": "sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/react": "*" } @@ -1967,6 +1961,7 @@ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=8" } @@ -1977,6 +1972,7 @@ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=10" }, @@ -2147,7 +2143,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", @@ -2642,7 +2637,8 @@ "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/dompurify": { "version": "3.4.2", @@ -3090,7 +3086,6 @@ "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", "dev": true, "license": "MIT", - "peer": true, "bin": { "jiti": "bin/jiti.js" } @@ -3259,6 +3254,7 @@ "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "dev": true, "license": "MIT", + "peer": true, "bin": { "lz-string": "bin/bin.js" } @@ -3551,7 +3547,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -3699,6 +3694,7 @@ "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", @@ -3713,7 +3709,8 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/punycode": { "version": "2.3.1", @@ -3761,7 +3758,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -3774,7 +3770,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -3804,7 +3799,6 @@ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", "license": "MIT", - "peer": true, "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" @@ -3935,8 +3929,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/redux-thunk": { "version": "3.1.0", @@ -4432,7 +4425,6 @@ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -4657,7 +4649,6 @@ "integrity": "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", @@ -5294,7 +5285,6 @@ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -5846,7 +5836,6 @@ "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", diff --git a/frontend/src/components/ConfirmModal.tsx b/frontend/src/components/ConfirmModal.tsx new file mode 100644 index 00000000..8396de36 --- /dev/null +++ b/frontend/src/components/ConfirmModal.tsx @@ -0,0 +1,155 @@ +import React, { useEffect, useRef } from 'react'; + +interface ConfirmModalProps { + isOpen: boolean; + title: string; + message: string; + onConfirm: () => void; + onCancel: () => void; + confirmText?: string; + cancelText?: string; + type?: 'danger' | 'warning' | 'info'; +} + +export const ConfirmModal: React.FC = ({ + isOpen, + title, + message, + onConfirm, + onCancel, + confirmText = 'Confirm', + cancelText = 'Cancel', + type = 'warning', +}) => { + const modalRef = useRef(null); + const confirmButtonRef = useRef(null); + const cancelButtonRef = useRef(null); + const previousFocusRef = useRef(null); + + useEffect(() => { + if (!isOpen) return; + + // Store previously focused element + previousFocusRef.current = document.activeElement as HTMLElement; + + // Focus the confirm button + confirmButtonRef.current?.focus(); + + // Hide background from screen readers + const mainContent = document.getElementById('root'); + if (mainContent) { + mainContent.setAttribute('aria-hidden', 'true'); + } + + // Handle keyboard events + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Escape') { + onCancel(); + } else if (e.key === 'Enter' && !e.shiftKey) { + // Only confirm if focus is not on a button (to avoid double trigger) + const activeElement = document.activeElement; + if (activeElement?.tagName !== 'BUTTON') { + e.preventDefault(); + onConfirm(); + } + } else if (e.key === 'Tab') { + // Trap focus within modal + const focusableElements = modalRef.current?.querySelectorAll( + 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])' + ); + + if (focusableElements && focusableElements.length > 0) { + const firstElement = focusableElements[0] as HTMLElement; + const lastElement = focusableElements[focusableElements.length - 1] as HTMLElement; + + if (e.shiftKey && document.activeElement === firstElement) { + e.preventDefault(); + lastElement.focus(); + } else if (!e.shiftKey && document.activeElement === lastElement) { + e.preventDefault(); + firstElement.focus(); + } + } + } + }; + + document.addEventListener('keydown', handleKeyDown); + + return () => { + document.removeEventListener('keydown', handleKeyDown); + + // Restore background + if (mainContent) { + mainContent.setAttribute('aria-hidden', 'false'); + } + + // Restore focus + previousFocusRef.current?.focus(); + }; + }, [isOpen, onConfirm, onCancel]); + + if (!isOpen) return null; + + const getButtonStyle = () => { + switch (type) { + case 'danger': return 'bg-rag-red hover:bg-rag-red/80'; + case 'warning': return 'bg-rag-amber hover:bg-rag-amber/80'; + default: return 'bg-rag-blue hover:bg-rag-blue/80'; + } + }; + + return ( + <> + {/* Backdrop */} + + + {/* Confirm Modal */} + setModalState(prev => ({ ...prev, isOpen: false }))} + type={modalState.type} + /> ); -} +} \ No newline at end of file diff --git a/frontend/src/pages/Scans.tsx.backup b/frontend/src/pages/Scans.tsx.backup new file mode 100644 index 00000000..48a07f0b --- /dev/null +++ b/frontend/src/pages/Scans.tsx.backup @@ -0,0 +1,625 @@ +import React, { useState, useEffect } 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"; + +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 [page, setPage] = useState(1); + const [total, setTotal] = useState(0); + const PAGE_LIMIT = 10; + + useEffect(() => { + loadTasks(); + const interval = setInterval(loadTasks, 5000); + return () => clearInterval(interval); + }, [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; + if ( + !window.confirm( + `Are you sure you want to delete ${selectedIds.length} selected scan records?`, + ) + ) { + return; + } + + try { + await bulkDeleteTasks(selectedIds); + setTasks((prev) => prev.filter((t) => !selectedIds.includes(t.task_id))); + setSelectedIds([]); + } catch (err) { + console.error("Bulk delete failed:", err); + 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 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) => ( +
+ ))} +
+
+
+ ); +} diff --git a/frontend/src/pages/Scans.tsx:Zone.Identifier b/frontend/src/pages/Scans.tsx:Zone.Identifier new file mode 100644 index 00000000..0a1446be --- /dev/null +++ b/frontend/src/pages/Scans.tsx:Zone.Identifier @@ -0,0 +1,3 @@ +[ZoneTransfer] +ZoneId=3 +HostUrl=https://chat.deepseek.com/ diff --git a/frontend/src/pages/Settings.tsx b/frontend/src/pages/Settings.tsx index 14061158..0d6381c1 100644 --- a/frontend/src/pages/Settings.tsx +++ b/frontend/src/pages/Settings.tsx @@ -2,11 +2,12 @@ import React, { useState, useEffect } from 'react' import { motion, AnimatePresence } from 'framer-motion' import { useTheme } from '../components/ThemeContext' import { useToast } from '../components/ToastContext' +import { ConfirmModal } from '../components/ConfirmModal' const itemVariants = { hidden: { opacity: 0, y: 20 }, - visible: { - opacity: 1, + visible: { + opacity: 1, y: 0, transition: { type: 'spring', stiffness: 200, damping: 25 } } @@ -34,7 +35,7 @@ const DEFAULT_CONFIG = { export default function Settings() { const { theme, setTheme } = useTheme() const { addToast } = useToast() - + const [config, setConfig] = useState(() => { const saved = localStorage.getItem('secuscan-config') if (saved) { @@ -48,6 +49,21 @@ export default function Settings() { }) const [systemTimezone, setSystemTimezone] = useState('Detecting...') + + // Modal state for confirm dialogs + const [modalState, setModalState] = useState<{ + isOpen: boolean; + title: string; + message: string; + onConfirm: () => void; + type: "danger" | "warning" | "info"; + }>({ + isOpen: false, + title: "", + message: "", + onConfirm: () => {}, + type: "warning", + }) useEffect(() => { try { @@ -66,11 +82,32 @@ export default function Settings() { } const handleReset = () => { - if (window.confirm("Restore engine to factory specifications? All API keys and custom rules will be cleared.")) { - setConfig(DEFAULT_CONFIG) - localStorage.setItem('secuscan-config', JSON.stringify(DEFAULT_CONFIG)) - addToast("Engine parameters reset to factory defaults", "info") - } + setModalState({ + isOpen: true, + title: "Engine Reset", + message: "Restore engine to factory specifications? All API keys and custom rules will be cleared.", + type: "warning", + onConfirm: () => { + setConfig(DEFAULT_CONFIG) + localStorage.setItem('secuscan-config', JSON.stringify(DEFAULT_CONFIG)) + addToast("Engine parameters reset to factory defaults", "info") + setModalState(prev => ({ ...prev, isOpen: false })) + } + }) + } + + const handleNuclearPurge = () => { + setModalState({ + isOpen: true, + title: "NUCLEAR PURGE", + message: "CRITICAL: THIS WILL PURGE ALL HISTORY AND ASSETS. PROCEED?", + type: "danger", + onConfirm: () => { + localStorage.clear() + window.location.reload() + setModalState(prev => ({ ...prev, isOpen: false })) + } + }) } const handleExport = () => { @@ -90,7 +127,7 @@ export default function Settings() {

{description}

- onChange(type === 'number' ? parseInt(e.target.value) || 0 : e.target.value)} @@ -106,7 +143,7 @@ export default function Settings() {

{description}

-