import { useState, useEffect, useRef, useCallback } from 'react'; import { Link, useNavigate } from 'react-router-dom'; import { useAuth } from '../components/AuthContext'; import { LabelList, ComposedChart, RadialBarChart, RadialBar, RadarChart, Radar, PolarGrid, PolarAngleAxis, PolarRadiusAxis, AreaChart, Area, LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip as RechartsTooltip, ResponsiveContainer, PieChart, Pie, Cell, Legend, BarChart, Bar } from 'recharts'; import { Navigate } from 'react-router-dom'; import { OrganizationSelector } from '../components/OrganizationSelector'; const ACTIVE_SCAN_KEY = 'wss_active_scan'; // localStorage key for persistence const CustomBarTooltip = ({ active, payload, label }) => { if (active && payload && payload.length) { return (
{label}
Findings : {payload[0].value}
); } return null; }; const getCleanDomain = (url) => { if (!url) return ''; try { const u = new URL(url.startsWith('http') ? url : `https://${url}`); return u.hostname || url; } catch (_) { return url.replace(/^https?:\/\//, '').split('/')[0]; } }; export const Dashboard = () => { const { token, refreshAccessToken, user } = useAuth(); const navigate = useNavigate(); useEffect(() => { if (user?.role === 'executive_user') { navigate('/scans/history', { replace: true }); } }, [user?.role, navigate]); const [summary, setSummary] = useState(null); const [recentScans, setRecentScans] = useState([]); const [activeScan, setActiveScan] = useState(null); const [liveLogs, setLiveLogs] = useState([]); const [loading, setLoading] = useState(true); const [completedScanId, setCompletedScanId] = useState(null); const [lastUpdated, setLastUpdated] = useState(null); const [sortColumn, setSortColumn] = useState('Date'); const [sortDirection, setSortDirection] = useState('desc'); const logContainerRef = useRef(null); const logPollRef = useRef(null); const dashboardPollRef = useRef(null); const activeScanRef = useRef(null); const completedScanIdRef = useRef(null); const liveScanIdsRef = useRef(new Set()); const markScanAsCompleted = useCallback((id) => { completedScanIdRef.current = id; setCompletedScanId(id); }, []); // Always read the latest token from localStorage so the poller works even // after a token refresh (avoids stale closure issues) const getToken = useCallback(() => localStorage.getItem('wss_token') || token , [token]); // Auto-scroll log terminal when new logs arrive useEffect(() => { if (logContainerRef.current) { logContainerRef.current.scrollTop = logContainerRef.current.scrollHeight; } }, [liveLogs]); // ── Persist active scan to localStorage ───────────────────── const persistActiveScan = useCallback((scan) => { if (scan) { localStorage.setItem(ACTIVE_SCAN_KEY, JSON.stringify({ id: scan.id, target_url: scan.target_url, scan_type: scan.scan_type })); } else { localStorage.removeItem(ACTIVE_SCAN_KEY); } }, []); // ── Log Polling ───────────────────────────────────────────── const stopLogPolling = useCallback(() => { if (logPollRef.current) { clearInterval(logPollRef.current); logPollRef.current = null; } }, []); const startLogPolling = useCallback((scan, isLive = true) => { stopLogPolling(); activeScanRef.current = scan; persistActiveScan(scan); if (isLive && scan?.id) { liveScanIdsRef.current.add(scan.id); } let consecutiveErrors = 0; const MAX_ERRORS = 20; // tolerate up to 20 failures (covers token refresh + brief outages) const pollLogs = async () => { const currentScan = activeScanRef.current || scan; if (!currentScan) { stopLogPolling(); return; } let activeToken = getToken(); try { let res = await fetch(`/api/scans/${currentScan.id}/logs`, { headers: { 'Authorization': `Bearer ${activeToken}` } }); // Token expired — try to refresh it silently if (res.status === 401) { const newToken = await refreshAccessToken(); if (newToken) { activeToken = newToken; res = await fetch(`/api/scans/${currentScan.id}/logs`, { headers: { 'Authorization': `Bearer ${newToken}` } }); } } if (!res.ok) { consecutiveErrors++; if (consecutiveErrors >= MAX_ERRORS) stopLogPolling(); return; } consecutiveErrors = 0; const data = await res.json(); if (data.logs) { setLiveLogs(data.logs); } if (data.status === 'completed' || data.status === 'failed' || data.status === 'terminated') { stopLogPolling(); setActiveScan(null); activeScanRef.current = null; persistActiveScan(null); // clear localStorage if (liveScanIdsRef.current.has(currentScan.id)) { markScanAsCompleted(currentScan.id); liveScanIdsRef.current.delete(currentScan.id); } fetchDashboard(); } } catch (err) { consecutiveErrors++; console.error('[Dashboard] Log poll error:', err); if (consecutiveErrors >= MAX_ERRORS) stopLogPolling(); } }; // Execute immediately on start for instant response without 1.5s delay pollLogs(); logPollRef.current = setInterval(pollLogs, 1500); }, [getToken, refreshAccessToken, stopLogPolling, persistActiveScan, markScanAsCompleted]); // ── Dashboard Data Fetch ───────────────────────────────────── const fetchDashboard = useCallback(async () => { try { const activeToken = getToken(); const [summaryRes, historyRes] = await Promise.all([ fetch('/api/vulnerabilities/summary', { headers: { 'Authorization': `Bearer ${activeToken}` } }), fetch('/api/scans/history?limit=100', { headers: { 'Authorization': `Bearer ${activeToken}` } }) ]); if (!summaryRes.ok || !historyRes.ok) return; const summaryData = await summaryRes.json(); const historyData = await historyRes.json(); setSummary(summaryData.summary); setRecentScans(historyData.scans || []); setLastUpdated(new Date()); // Filter all currently active or queued scans (excluding completed ones) const runningScans = (historyData.scans || []).filter( s => (s.status === 'scanning' || s.status === 'queued') && s.id !== completedScanIdRef.current ); // Sort by status ('scanning' first) and then earliest creation time (FIFO order) runningScans.sort((a, b) => { if (a.status === 'scanning' && b.status !== 'scanning') return -1; if (b.status === 'scanning' && a.status !== 'scanning') return 1; return new Date(a.started_at || a.created_at || 0) - new Date(b.started_at || b.created_at || 0); }); if (runningScans.length > 0) { runningScans.forEach(s => liveScanIdsRef.current.add(s.id)); // If we are ALREADY tracking an active scan that is still in progress, STICK WITH IT! const currentStillRunning = activeScanRef.current ? runningScans.find(s => s.id === activeScanRef.current.id) : null; if (currentStillRunning) { // Keep current active scan without resetting logs or switching setActiveScan(currentStillRunning); activeScanRef.current = currentStillRunning; } else { // No active scan tracked currently — pick the earliest scan in queue const nextScan = runningScans[0]; setActiveScan(nextScan); activeScanRef.current = nextScan; setLiveLogs([]); startLogPolling(nextScan, true); } } else { // No active scans remaining localStorage.removeItem(ACTIVE_SCAN_KEY); if (activeScanRef.current) { setActiveScan(null); activeScanRef.current = null; persistActiveScan(null); stopLogPolling(); } } } catch (err) { console.error('[Dashboard] Fetch error:', err); } finally { setLoading(false); } }, [getToken, startLogPolling, stopLogPolling, persistActiveScan]); // ── Mount — recover active scan from localStorage ───────────────────────── useEffect(() => { // 1. Immediately try to restore a previously active scan from localStorage // so LIVE AUDIT appears instantly even after refresh or re-login. const stored = localStorage.getItem(ACTIVE_SCAN_KEY); if (stored && token) { try { const storedScan = JSON.parse(stored); if (storedScan && storedScan.id) { setActiveScan(storedScan); activeScanRef.current = storedScan; startLogPolling(storedScan, false); // isLive = false, prevent stale error banner } } catch (_) { localStorage.removeItem(ACTIVE_SCAN_KEY); } } // 2. Then do the normal full dashboard fetch fetchDashboard(); dashboardPollRef.current = setInterval(fetchDashboard, 5000); return () => { clearInterval(dashboardPollRef.current); stopLogPolling(); }; }, [fetchDashboard, stopLogPolling]); // intentionally shallow — only run on mount // ── Derived values ─────────────────────────────────────────── if (loading) { return (
sync Loading Security Console...
); } const counts = summary?.vulnerabilities_count || { critical: 0, high: 0, medium: 0, low: 0, total: 0 }; // Real-time dynamic security score calculation const rawScore = summary?.average_security_score; const hasCompletedScans = (summary?.scans_count > 0 || recentScans.some(s => s.status === 'completed')) && rawScore !== null && rawScore !== undefined; const score = hasCompletedScans ? Math.round(rawScore) : null; const dashOffset = hasCompletedScans ? (283 - (283 * score) / 100) : 283; let scoreDisplay = hasCompletedScans ? score : 'N/A'; let scoreLabel = hasCompletedScans ? 'Excellent' : 'Not Tested Yet'; let scoreColorClass = hasCompletedScans ? 'text-primary bg-primary/10' : 'text-slate-400 bg-slate-500/10 border border-slate-500/20'; let scoreSubtext = hasCompletedScans ? (score >= 80 ? 'System Protected' : 'Remediation Required') : 'No Scans Performed Yet'; if (hasCompletedScans) { if (score < 50) { scoreLabel = 'Critical'; scoreColorClass = 'text-error bg-error/10'; } else if (score < 80) { scoreLabel = 'Warning'; scoreColorClass = 'text-tertiary bg-tertiary/10'; } } const getRatingGrade = (s) => { if (s === null || s === undefined) return '--'; if (s >= 90) return 'A'; if (s >= 80) return 'B'; if (s >= 70) return 'C'; if (s >= 50) return 'D'; return 'F'; }; const ratingColor = (g) => ({ A: 'text-green-600', B: 'text-green-500', C: 'text-yellow-600', D: 'text-orange-600', F: 'text-red-600' }[g] || 'text-slate-400'); // Log line coloring — match exactly what backend writes const getLogColor = (log) => { if (log.includes('[VULN]')) return 'text-red-400 font-semibold'; if (log.includes('[WARN]')) return 'text-yellow-400'; if (log.includes('[SUCCESS]')) return 'text-green-400 font-semibold'; if (log.includes('[INFO]')) return 'text-blue-300'; if (log.includes('[ERROR]')) return 'text-red-500 font-bold'; return 'text-slate-300'; }; // Chart: last 7 days line chart data const buildChart = () => { const days = ['Sun','Mon','Tue','Wed','Thu','Fri','Sat']; const today = new Date(); const buckets = Array.from({ length: 7 }, (_, i) => { const d = new Date(today); d.setDate(today.getDate() - (6 - i)); return { date: d, name: days[d.getDay()], Scans: 0, Threats: 0 }; }); recentScans.forEach(scan => { if (!scan.started_at) return; const sd = new Date(scan.started_at); const b = buckets.find(b => b.date.toDateString() === sd.toDateString()); if (b) { b.Scans++; const v = scan.vulnerabilities_count || {}; b.Threats += (v.critical||0) + (v.high||0) + (v.medium||0) + (v.low||0); } }); return buckets; }; const chartData = buildChart(); // Derived totals const totalCounts = counts.critical + counts.high + counts.medium + counts.low; const handleSort = (column) => { if (column === 'Severity' || column === 'Actions') return; if (sortColumn === column) { setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc'); } else { setSortColumn(column); setSortDirection('asc'); } }; const getSortedScans = () => { return [...recentScans].sort((a, b) => { let aVal, bVal; switch (sortColumn) { case 'Status': aVal = a.status || ''; bVal = b.status || ''; break; case 'Target URL': aVal = a.target_url || ''; bVal = b.target_url || ''; break; case 'Scan Profile': aVal = a.scan_type || ''; bVal = b.scan_type || ''; break; case 'Date': aVal = new Date(a.started_at || 0).getTime(); bVal = new Date(b.started_at || 0).getTime(); break; case 'Rating': aVal = a.security_score || 0; bVal = b.security_score || 0; break; default: return 0; } if (aVal < bVal) return sortDirection === 'asc' ? -1 : 1; if (aVal > bVal) return sortDirection === 'asc' ? 1 : -1; return 0; }); }; if (user?.role === 'executive_user') { return null; } return (
{/* Page Header */}

Security Dashboard

Real-time infrastructure health and vulnerability monitoring.

schedule {lastUpdated ? `Updated ${lastUpdated.toLocaleTimeString()}` : 'Loading...'}
{/* ── Live Scan Terminal ── */} {activeScan && ( (() => { const runningScansList = recentScans.filter( s => (s.status === 'scanning' || s.status === 'queued') && s.id !== completedScanIdRef.current ); return (
{/* Left Section: Live Audit Title + Scrollable Scan Tabs */}
LIVE AUDIT — {activeScan.target_url}
{/* Interactive Scan Tabs when multiple scans are active or queued (Scrollable horizontally) */} {runningScansList.length > 1 && (
{runningScansList.map((s) => { const isSelected = activeScan.id === s.id; const domainName = getCleanDomain(s.target_url); return ( ); })}
)}
{/* Right Section: QUICK SCAN, +N QUEUED NEXT, ● RUNNING (Pinned strictly to the right side) */}
{activeScan.scan_type} Scan {/* Clickable +N Queued Next button to switch to the next queued scan */} {runningScansList.filter(s => s.id !== activeScan.id).length > 0 && ( )} ● {activeScan.status === 'scanning' ? 'Running' : 'Queued'}
{liveLogs.length === 0 ? (
{activeScan.status === 'queued' ? '⏳ Waiting in execution queue (prior scan running)...' : '⏳ Spawning audit worker threads...'}
) : ( liveLogs.map((log, i) => (
{log}
)) )}
{liveLogs.length} log entries ● {activeScan.status === 'scanning' ? 'Scanning in progress...' : 'Queued — awaiting execution...'}
); })() )} {/* ── Scan Complete Banner ── */} {completedScanId && !activeScan && ( (() => { const scanData = recentScans.find(s => s.id === completedScanId); const isFailed = scanData?.status === 'failed'; const isTerminated = scanData?.status === 'terminated'; const handleDismiss = () => { setCompletedScanId(null); completedScanIdRef.current = null; localStorage.removeItem(ACTIVE_SCAN_KEY); }; if (isTerminated) { return (
error

Scanner Terminated

Sorry, due to some problemes we terminate your scanning and also show that scanner is terminated.

open_in_new View Details
); } if (isFailed) { return (
error

Scan Failed!

The scanner encountered a critical error. View logs for details.

open_in_new View Details
); } return (
check_circle

Scan Complete!

Vulnerability analysis finished. View the full report below.

open_in_new View Full Report
); })() )} {/* ── Bento Grid ── */}
{/* Security Score Gauge */}

Security Score

Overall system resilience

{!hasCompletedScans ? (
{/* Track Circle */} {/* Sleek Dashed Radar Ring */} {/* Inner Content */}
shield
N/A NOT TESTED YET
{/* Subtext and Action */}

No security audit performed on this workspace yet.

) : (
{score} {scoreLabel} {score >= 80 ? 'System Protected' : 'Remediation Required'}
)}
Live security posture = 80 ? 'text-green-600' : score >= 50 ? 'text-orange-600' : 'text-error' }`}> {!hasCompletedScans ? 'hourglass_empty' : score >= 80 ? 'trending_up' : score >= 50 ? 'trending_flat' : 'trending_down'} {!hasCompletedScans ? 'Pending Initial Scan' : score >= 80 ? 'Stable' : score >= 50 ? 'Needs Attention' : 'Critical Risk'}
{/* Stats + Chart */}
{/* Vulnerability Category Breakdown Bar Chart with Visible Numbers */}

Vulnerability Categories

Top Vectors
} />
{/* Recent Scans Table */}

Configured Target Assets

View Full Audit Log
{recentScans.length === 0 ? (
No target domains scanned yet. Launch your first website scan under the New Scan tab!
) : (
{['Status','Target URL','Scan Profile','Date','Rating','Severity','Actions'].map((h, i) => ( ))} {getSortedScans().slice(0, 8).map((scan) => { let dot = 'bg-primary', statusText = 'Clean', rowBg = ''; if (scan.status === 'completed') { if (scan.security_score < 60) { dot = 'bg-error animate-pulse'; statusText = 'Critical'; rowBg = 'bg-error/5'; } else if (scan.security_score < 80) { dot = 'bg-tertiary'; statusText = 'Warning'; } else { dot = 'bg-green-500'; statusText = 'Secure'; } } else if (scan.status === 'scanning' || scan.status === 'queued') { dot = 'bg-yellow-500 animate-pulse'; statusText = 'Scanning'; } else if (scan.status === 'terminated') { dot = 'bg-slate-400'; statusText = 'Terminated'; } else { dot = 'bg-slate-400'; statusText = 'Failed'; } const grade = getRatingGrade(scan.security_score); return ( ); })}
handleSort(h)} className={`py-sm px-lg font-label-sm text-label-sm text-on-surface-variant uppercase tracking-wider font-medium ${i === 6 ? 'text-right' : ''} ${(h !== 'Severity' && h !== 'Actions') ? 'cursor-pointer hover:bg-surface-container-high transition-colors select-none group' : ''}`} >
{h} {(h !== 'Severity' && h !== 'Actions') && ( {sortColumn === h && sortDirection === 'desc' ? 'arrow_downward' : 'arrow_upward'} )}
{statusText}
{scan.target_url} {scan.scan_type} Assessment {scan.started_at ? new Date(scan.started_at).toLocaleString('en-US', { day: 'numeric', month: 'short', year: 'numeric', hour: '2-digit', minute: '2-digit' }) : 'Pending'} {grade} {scan.security_score !== null && ({scan.security_score})}
{(scan.vulnerabilities_count?.critical > 0) && {scan.vulnerabilities_count.critical} Crit} {(scan.vulnerabilities_count?.high > 0) && {scan.vulnerabilities_count.high} High} {(scan.vulnerabilities_count?.total === 0) && Clean} {scan.status === 'scanning' && Scanning...}
Details arrow_forward
)}
); };