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) => (
| 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'}
)}
|
))}
{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 (
|
|
{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
|
);
})}
)}
);
};