From 329bef4b953dcfc4e5fd162353ffd9da1f100c1b Mon Sep 17 00:00:00 2001 From: ANU-2524 Date: Sun, 24 May 2026 00:13:22 +0530 Subject: [PATCH 1/5] feat(frontend): add local preferences layer and refine tool configuration --- backend/secuscan/executor.py | 10 ++++ backend/secuscan/models.py | 5 ++ backend/secuscan/validation.py | 19 ++++++ frontend/src/api.ts | 6 ++ frontend/src/pages/Findings.tsx | 48 +++++++++++++-- frontend/src/pages/Reports.tsx | 7 ++- frontend/src/pages/Scans.tsx | 7 ++- frontend/src/pages/ToolConfig.tsx | 31 +++++++++- frontend/src/utils/preferences.ts | 35 +++++++++++ .../unit/pages/ReportsPreference.test.tsx | 60 +++++++++++++++++++ .../unit/pages/ToolConfigDynamic.test.tsx | 31 +++++++++- .../testing/unit/utils/preferences.test.ts | 54 +++++++++++++++++ testing/backend/unit/test_validation.py | 13 +++- 13 files changed, 317 insertions(+), 9 deletions(-) create mode 100644 frontend/src/utils/preferences.ts create mode 100644 frontend/testing/unit/pages/ReportsPreference.test.tsx create mode 100644 frontend/testing/unit/utils/preferences.test.ts diff --git a/backend/secuscan/executor.py b/backend/secuscan/executor.py index 7fd178f0..afd269ca 100644 --- a/backend/secuscan/executor.py +++ b/backend/secuscan/executor.py @@ -18,6 +18,7 @@ from .database import get_db from .plugins import get_plugin_manager from .models import TaskStatus +from .validation import validate_timeout # Modular Scanners from .scanners.port_scanner import PortScanner @@ -104,6 +105,15 @@ async def create_task( # Merge preset with user inputs (user inputs take precedence) inputs = {**preset_values, **inputs} + # Validate timeout if provided and supported + if "timeout" in inputs and plugin.timeout_config: + timeout_val = int(inputs["timeout"]) + min_t = plugin.timeout_config.get("min", 10) + max_t = plugin.timeout_config.get("max", 3600) + is_valid, err = validate_timeout(timeout_val, min_t, max_t) + if not is_valid: + raise ValueError(err) + # Store task in database db = await get_db() await db.execute( diff --git a/backend/secuscan/models.py b/backend/secuscan/models.py index 264363e5..bbd95a5a 100644 --- a/backend/secuscan/models.py +++ b/backend/secuscan/models.py @@ -66,6 +66,11 @@ class PluginMetadata(BaseModel): fields: List[PluginField] presets: Dict[str, Dict[str, Any]] + timeout_config: Optional[Dict[str, Any]] = Field( + default=None, + description="Configuration for timeout behavior: { 'enabled': bool, 'min': int, 'max': int, 'default': int }" + ) + output: Dict[str, Any] safety: Dict[str, Any] learning: Optional[Dict[str, Any]] = None diff --git a/backend/secuscan/validation.py b/backend/secuscan/validation.py index cf6874aa..92978466 100644 --- a/backend/secuscan/validation.py +++ b/backend/secuscan/validation.py @@ -90,6 +90,25 @@ def validate_target(target: str, safe_mode: bool = True) -> Tuple[bool, str]: return True, "" +def validate_timeout(timeout: int, min_val: int = 10, max_val: int = 3600) -> Tuple[bool, str]: + """ + Validate scan timeout value. + + Args: + timeout: Timeout in seconds + min_val: Minimum allowed timeout + max_val: Maximum allowed timeout + + Returns: + Tuple of (is_valid, error_message) + """ + if timeout < min_val: + return False, f"Timeout must be at least {min_val} seconds" + if timeout > max_val: + return False, f"Timeout cannot exceed {max_val} seconds" + return True, "" + + def validate_port(port: int) -> Tuple[bool, str]: """ Validate port number. diff --git a/frontend/src/api.ts b/frontend/src/api.ts index 37942a58..01317d44 100644 --- a/frontend/src/api.ts +++ b/frontend/src/api.ts @@ -74,6 +74,12 @@ export interface PluginSchemaResponse { fields: PluginFieldSchema[] presets: Record> safety: Record + timeout_config?: { + enabled: boolean + min: number + max: number + default: number + } } export interface TaskStartResponse { diff --git a/frontend/src/pages/Findings.tsx b/frontend/src/pages/Findings.tsx index 74e7e811..18bc5aee 100644 --- a/frontend/src/pages/Findings.tsx +++ b/frontend/src/pages/Findings.tsx @@ -2,6 +2,7 @@ import React, { useEffect, useMemo, useState } from 'react' import { motion } from 'framer-motion' import { getFindings } from '../api' import { formatLocaleDate } from '../utils/date' +import { getPreference, setPreference } from '../utils/preferences' type Finding = { id: string @@ -88,7 +89,8 @@ export default function Findings() { const [findings, setFindings] = useState([]) const [loading, setLoading] = useState(true) const [searchQuery, setSearchQuery] = useState('') - const [filterSeverity, setFilterSeverity] = useState('all') + const [filterSeverity, setFilterSeverity] = useState(() => getPreference('findings-severity-filter', 'all')) + const [sortBy, setSortBy] = useState<'severity' | 'date'>(() => getPreference('findings-sort-by', 'severity')) const [selectedFindingId, setSelectedFindingId] = useState(null) const [reviewState, setReviewState] = useState({}) const [copiedFindingId, setCopiedFindingId] = useState(null) @@ -119,6 +121,14 @@ export default function Findings() { localStorage.setItem('secuscan-finding-review-state', JSON.stringify(reviewState)) }, [reviewState]) + useEffect(() => { + setPreference('findings-severity-filter', filterSeverity) + }, [filterSeverity]) + + useEffect(() => { + setPreference('findings-sort-by', sortBy) + }, [sortBy]) + const enrichedFindings = useMemo( () => findings.map((finding) => ({ @@ -132,7 +142,7 @@ export default function Findings() { const filteredFindings = useMemo(() => { const query = searchQuery.trim().toLowerCase() - return enrichedFindings.filter((finding) => { + const list = enrichedFindings.filter((finding) => { const matchesSeverity = filterSeverity === 'all' || finding.severity === filterSeverity const haystack = [ finding.title, @@ -148,7 +158,17 @@ export default function Findings() { return matchesSeverity && haystack.includes(query) }) - }, [enrichedFindings, filterSeverity, searchQuery]) + + if (sortBy === 'date') { + return list.sort((a, b) => new Date(b.discovered_at).getTime() - new Date(a.discovered_at).getTime()) + } + + return list.sort((a, b) => { + const diff = (b.cvss || 0) - (a.cvss || 0) + if (diff !== 0) return diff + return new Date(b.discovered_at).getTime() - new Date(a.discovered_at).getTime() + }) + }, [enrichedFindings, filterSeverity, searchQuery, sortBy]) const groupedFindings = useMemo( () => @@ -257,7 +277,7 @@ export default function Findings() {
-
+
+ +
+ +
+ + +
+
diff --git a/frontend/src/pages/Reports.tsx b/frontend/src/pages/Reports.tsx index fe5f8cce..c42b14c8 100644 --- a/frontend/src/pages/Reports.tsx +++ b/frontend/src/pages/Reports.tsx @@ -17,6 +17,7 @@ import { } from '@hugeicons/core-free-icons' import { getDashboardSummary, getReports, API_BASE } from '../api' import { formatDateLong } from '../utils/date' +import { getPreference, setPreference } from '../utils/preferences' type Report = { id: string @@ -64,7 +65,7 @@ export default function Reports() { const navigate = useNavigate() const [reports, setReports] = useState([]) const [summary, setSummary] = useState({ total_findings: 0, total_assets: 0, critical_findings: 0, high_findings: 0, total_attack_surface: 0 }) - const [selectedType, setSelectedType] = useState('all') + const [selectedType, setSelectedType] = useState(() => getPreference('reports-type-filter', 'all')) const fetchReports = () => { Promise.all([getReports(), getDashboardSummary()]).then(([reportData, summaryData]: any) => { @@ -77,6 +78,10 @@ export default function Reports() { fetchReports() }, []) + useEffect(() => { + setPreference('reports-type-filter', selectedType) + }, [selectedType]) + const filteredReports = reports.filter((report) => selectedType === 'all' || report.type === selectedType) // Check if any report with status 'ready' exists diff --git a/frontend/src/pages/Scans.tsx b/frontend/src/pages/Scans.tsx index 6328a061..c10798e7 100644 --- a/frontend/src/pages/Scans.tsx +++ b/frontend/src/pages/Scans.tsx @@ -4,6 +4,7 @@ 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 { getPreference, setPreference } from '../utils/preferences' interface Task { task_id: string @@ -49,7 +50,7 @@ export default function Scans() { const navigate = useNavigate() const [tasks, setTasks] = useState([]) const [loading, setLoading] = useState(true) - const [filter, setFilter] = useState('all') + const [filter, setFilter] = useState(() => getPreference('scans-status-filter', 'all')) const [expandedId, setExpandedId] = useState(null) const [selectedIds, setSelectedIds] = useState([]) @@ -59,6 +60,10 @@ export default function Scans() { return () => clearInterval(interval) }, [filter]) + useEffect(() => { + setPreference('scans-status-filter', filter) + }, [filter]) + async function loadTasks() { try { const url = filter === 'all' diff --git a/frontend/src/pages/ToolConfig.tsx b/frontend/src/pages/ToolConfig.tsx index 2ac40f7a..386c0d8f 100644 --- a/frontend/src/pages/ToolConfig.tsx +++ b/frontend/src/pages/ToolConfig.tsx @@ -194,11 +194,16 @@ export default function ToolConfig() { return } + const taskInputs = { ...inputs } + if (schema.timeout_config?.enabled && taskInputs.timeout === undefined) { + taskInputs.timeout = schema.timeout_config.default + } + try { setSubmitting(true) const task = await startTask( plugin.id, - inputs, + taskInputs, plugin.requires_consent ? consentGranted : true, selectedPreset || undefined, ) @@ -412,6 +417,30 @@ export default function ToolConfig() {
+ {schema.timeout_config?.enabled && ( +
+
+

Execution_Timeout

+ {inputs.timeout || schema.timeout_config.default}s +
+
+ setInputs(prev => ({ ...prev, timeout: parseInt(e.target.value) }))} + className="w-full h-2 bg-black rounded-none appearance-none cursor-pointer accent-rag-blue" + /> +
+ Min: {schema.timeout_config.min || 10}s + Max: {schema.timeout_config.max || 300}s +
+
+
+ )} + + - {/* Ledger Section */} -
+ {/* Ledger Section */} +
-

Historical_Ledger

-
- {filteredReports.length} ENTRIES_LOCATED +

Historical_Ledger

+
+ {filteredReports.length} ENTRIES_LOCATED
- - {filteredReports.map((report) => ( - - {/* Status Top Bar */} -
+ + {filteredReports.length === 0 && reports.length > 0 ? ( +
+ filter_alt_off +
+

+ No Matching Reports +

+

+ No reports match the current filter. Adjust classification to view additional dossiers. +

+
+
+ ) : ( + filteredReports.map((report) => ( + + {/* Status Top Bar */} +
-
-
- - {report.type}_TYPE - -
+
+
+ + {report.type}_TYPE + + +
-
-

- {report.name} -

-
-
+
+

+ {report.name} +

+
+
-
-
- Findings - {report.findings.toString().padStart(3, '0')} -
-
- Assets - {report.assets.toString().padStart(3, '0')} -
-
- Pages - {report.pages.toString().padStart(3, '0')} -
-
+
+
+ Findings + {report.findings.toString().padStart(3, '0')} +
+
+ Assets + {report.assets.toString().padStart(3, '0')} +
+
+ Pages + {report.pages.toString().padStart(3, '0')} +
+
-
-
-

TIMESTAMP

-

{formatDateLong(report.generated_at)}

-
-
- - {[...exportFormats].sort((a, b) => - a === preferred ? -1 : b === preferred ? 1 : 0 - ).map((format) => ( - - ))} -
-
-
+
+
+

TIMESTAMP

+

{formatDateLong(report.generated_at)}

+
+
+ + +
+
+
- {/* Background Hover Icon */} -
-
-
-
-
- ))} + {/* Background Hover Icon */} +
+
+ +
+
+
+ )) + )} - {filteredReports.length === 0 && ( -
-
- )} -
+ {reports.length === 0 && ( +
+ +
+

Archive Isolated

+

System buffer awaiting briefing generation protocols

+
+
+ )} +
-
- - - )} +
+ {/* Tactical Footer */}
-
-
- RESTRICTED_ACCESS_ENCLAVE // SYSTEM_ARCHIVE_DAEMON // {new Date().getFullYear()} -
-
- {[1, 2, 3, 4, 5, 6, 7, 8].map(i =>
)} -
+
+
+ RESTRICTED_ACCESS_ENCLAVE // SYSTEM_ARCHIVE_DAEMON // {new Date().getFullYear()} +
+
+ {[1,2,3,4,5,6,7,8].map(i =>
)} +
) diff --git a/frontend/src/pages/Scans.tsx b/frontend/src/pages/Scans.tsx index 2280f334..6328a061 100644 --- a/frontend/src/pages/Scans.tsx +++ b/frontend/src/pages/Scans.tsx @@ -1,73 +1,55 @@ - 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 { getPreference, setPreference } from '../utils/preferences' - -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; + 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 } 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" }, -]; + { 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; + transition: { staggerChildren: 0.1 } + } +} as const const itemVariants = { hidden: { opacity: 0, scale: 0.95, y: 20 }, - visible: { - opacity: 1, - scale: 1, + visible: { + opacity: 1, + scale: 1, y: 0, - transition: { type: "spring", stiffness: 200, damping: 20 } as any, - }, -} as const; + 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(() => getPreference('scans-status-filter', 'all')) + const [filter, setFilter] = useState('all') const [expandedId, setExpandedId] = useState(null) const [selectedIds, setSelectedIds] = useState([]) @@ -77,10 +59,6 @@ export default function Scans() { return () => clearInterval(interval) }, [filter]) - useEffect(() => { - setPreference('scans-status-filter', filter) - }, [filter]) - async function loadTasks() { try { const url = filter === 'all' @@ -95,575 +73,405 @@ export default function Scans() { } finally { setLoading(false) } - - 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 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; - } + 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."); + 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; - } + 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."); + 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; - } + 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.", - ); + 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 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 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`; - } + 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"} - -

-
+ return ( +
+ + {/* Neo-Brutalist Header */} +
+
+
+ Operational_Registry_v10.1 +
+

+ Operational Registry +

+

+ Total_Registry_Keys: {tasks.length} // 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, - ) - } +
+
+ 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} + + + 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

- )}
-
- - {/* 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"} - -
+ )} + + + + {/* Floating Bulk Action Bar */} + + {selectedIds.length > 0 && ( + +
+
+
{selectedIds.length}
+
+

Records_Selected_For_Pruning

+

Bulk_Action_Protocol_v4_Active

-
- -
- {(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} +
+ + )} + + + {/* Restricted Footer */} +
+
+ S + SECUSCAN ARCHIVE INTEGRITY PROTOCOL v10.1
-
-

- Records_Selected_For_Pruning -

-

- Bulk_Action_Protocol_v4_Active -

+
+ {[1,2,3,4,5,6,7,8,9,10,11,12].map(i =>
)}
-
-
- - -
-
- - )} - - - {/* 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/ToolConfig.tsx b/frontend/src/pages/ToolConfig.tsx index 9a480eae..1ccb2827 100644 --- a/frontend/src/pages/ToolConfig.tsx +++ b/frontend/src/pages/ToolConfig.tsx @@ -395,7 +395,7 @@ export default function ToolConfig() { ) })}
- + {schema.timeout_config?.enabled && (
@@ -421,7 +421,6 @@ export default function ToolConfig() {
)} -