larxius commited on
Commit
00a396d
·
verified ·
1 Parent(s): bb84c2d

Update frontend/src/pages/Dashboard.jsx

Browse files
Files changed (1) hide show
  1. frontend/src/pages/Dashboard.jsx +679 -668
frontend/src/pages/Dashboard.jsx CHANGED
@@ -1,668 +1,679 @@
1
- import React, { useState, useEffect, useRef, useCallback } from 'react';
2
- import { Link, useNavigate } from 'react-router-dom';
3
- import { useAuth } from '../components/AuthContext';
4
- 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';
5
-
6
- import { Navigate } from 'react-router-dom';
7
- import { OrganizationSelector } from '../components/OrganizationSelector';
8
-
9
- const ACTIVE_SCAN_KEY = 'wss_active_scan'; // localStorage key for persistence
10
-
11
- export const Dashboard = () => {
12
- const { token, refreshAccessToken, user } = useAuth();
13
-
14
- if (user?.role === 'executive_user') {
15
- return <Navigate to="/scans/history" replace />;
16
- }
17
-
18
- const [summary, setSummary] = useState(null);
19
- const [recentScans, setRecentScans] = useState([]);
20
- const [activeScan, setActiveScan] = useState(null);
21
- const [liveLogs, setLiveLogs] = useState([]);
22
- const [loading, setLoading] = useState(true);
23
- const [completedScanId, setCompletedScanId] = useState(null);
24
- const [lastUpdated, setLastUpdated] = useState(null);
25
- const [sortColumn, setSortColumn] = useState('Date');
26
- const [sortDirection, setSortDirection] = useState('desc');
27
-
28
- const logContainerRef = useRef(null);
29
- const logPollRef = useRef(null);
30
- const dashboardPollRef = useRef(null);
31
- const activeScanRef = useRef(null);
32
- // Always read the latest token from localStorage so the poller works even
33
- // after a token refresh (avoids stale closure issues)
34
- const getToken = useCallback(() =>
35
- localStorage.getItem('wss_token') || token
36
- , [token]);
37
-
38
- // Auto-scroll log terminal when new logs arrive
39
- useEffect(() => {
40
- if (logContainerRef.current) {
41
- logContainerRef.current.scrollTop = logContainerRef.current.scrollHeight;
42
- }
43
- }, [liveLogs]);
44
-
45
- // ── Persist active scan to localStorage ─────────────────────
46
- const persistActiveScan = useCallback((scan) => {
47
- if (scan) {
48
- localStorage.setItem(ACTIVE_SCAN_KEY, JSON.stringify({ id: scan.id, target_url: scan.target_url, scan_type: scan.scan_type }));
49
- } else {
50
- localStorage.removeItem(ACTIVE_SCAN_KEY);
51
- }
52
- }, []);
53
-
54
- // ── Log Polling ─────────────────────────────────────────────
55
- const stopLogPolling = useCallback(() => {
56
- if (logPollRef.current) {
57
- clearInterval(logPollRef.current);
58
- logPollRef.current = null;
59
- }
60
- }, []);
61
-
62
- const startLogPolling = useCallback((scan) => {
63
- stopLogPolling();
64
- activeScanRef.current = scan;
65
- persistActiveScan(scan);
66
- let consecutiveErrors = 0;
67
- const MAX_ERRORS = 20; // tolerate up to 20 failures (covers token refresh + brief outages)
68
-
69
- logPollRef.current = setInterval(async () => {
70
- if (!activeScanRef.current) { stopLogPolling(); return; }
71
-
72
- let activeToken = getToken();
73
-
74
- try {
75
- let res = await fetch(`/api/scans/${scan.id}/logs`, {
76
- headers: { 'Authorization': `Bearer ${activeToken}` }
77
- });
78
-
79
- // Token expired — try to refresh it silently
80
- if (res.status === 401) {
81
- const newToken = await refreshAccessToken();
82
- if (newToken) {
83
- activeToken = newToken;
84
- res = await fetch(`/api/scans/${scan.id}/logs`, {
85
- headers: { 'Authorization': `Bearer ${newToken}` }
86
- });
87
- }
88
- }
89
-
90
- if (!res.ok) {
91
- consecutiveErrors++;
92
- if (consecutiveErrors >= MAX_ERRORS) stopLogPolling();
93
- return;
94
- }
95
-
96
- consecutiveErrors = 0;
97
- const data = await res.json();
98
- if (data.logs && data.logs.length > 0) {
99
- setLiveLogs(data.logs);
100
- }
101
-
102
- if (data.status === 'completed' || data.status === 'failed' || data.status === 'terminated') {
103
- stopLogPolling();
104
- setActiveScan(null);
105
- activeScanRef.current = null;
106
- persistActiveScan(null); // clear localStorage
107
- setCompletedScanId(scan.id);
108
- fetchDashboard();
109
- }
110
- } catch (err) {
111
- consecutiveErrors++;
112
- console.error('[Dashboard] Log poll error:', err);
113
- if (consecutiveErrors >= MAX_ERRORS) stopLogPolling();
114
- }
115
- }, 1500);
116
- }, [getToken, refreshAccessToken, stopLogPolling, persistActiveScan]);
117
-
118
- // ── Dashboard Data Fetch ─────────────────────────────────────
119
- const fetchDashboard = useCallback(async () => {
120
- try {
121
- const activeToken = getToken();
122
- const [summaryRes, historyRes] = await Promise.all([
123
- fetch('/api/vulnerabilities/summary', { headers: { 'Authorization': `Bearer ${activeToken}` } }),
124
- fetch('/api/scans/history', { headers: { 'Authorization': `Bearer ${activeToken}` } })
125
- ]);
126
-
127
- if (!summaryRes.ok || !historyRes.ok) return;
128
-
129
- const summaryData = await summaryRes.json();
130
- const historyData = await historyRes.json();
131
-
132
- setSummary(summaryData.summary);
133
- setRecentScans(historyData.scans || []);
134
- setLastUpdated(new Date());
135
-
136
- const running = (historyData.scans || []).find(
137
- s => s.status === 'scanning' || s.status === 'queued'
138
- );
139
-
140
- if (running) {
141
- if (!activeScanRef.current || activeScanRef.current.id !== running.id) {
142
- setActiveScan(running);
143
- setLiveLogs([]);
144
- startLogPolling(running);
145
- }
146
- } else if (!running && activeScanRef.current && !logPollRef.current) {
147
- setActiveScan(null);
148
- activeScanRef.current = null;
149
- persistActiveScan(null);
150
- stopLogPolling();
151
- }
152
- } catch (err) {
153
- console.error('[Dashboard] Fetch error:', err);
154
- } finally {
155
- setLoading(false);
156
- }
157
- }, [getToken, startLogPolling, stopLogPolling, persistActiveScan]);
158
-
159
- // ── Mount — recover active scan from localStorage ─────────────────────────
160
- useEffect(() => {
161
- // 1. Immediately try to restore a previously active scan from localStorage
162
- // so LIVE AUDIT appears instantly even after refresh or re-login.
163
- const stored = localStorage.getItem(ACTIVE_SCAN_KEY);
164
- if (stored && token) {
165
- try {
166
- const storedScan = JSON.parse(stored);
167
- // Validate it's still running before starting the poller
168
- fetch(`/api/scans/${storedScan.id}/logs`, {
169
- headers: { 'Authorization': `Bearer ${getToken()}` }
170
- }).then(async (r) => {
171
- if (r.ok) {
172
- const d = await r.json();
173
- if (d.status === 'scanning' || d.status === 'queued') {
174
- setActiveScan(storedScan);
175
- setLiveLogs(d.logs || []);
176
- startLogPolling(storedScan);
177
- } else {
178
- // Scan already done — clean up localStorage
179
- localStorage.removeItem(ACTIVE_SCAN_KEY);
180
- }
181
- }
182
- }).catch(() => {});
183
- } catch (_) {
184
- localStorage.removeItem(ACTIVE_SCAN_KEY);
185
- }
186
- }
187
-
188
- // 2. Then do the normal full dashboard fetch
189
- fetchDashboard();
190
- dashboardPollRef.current = setInterval(fetchDashboard, 5000);
191
- return () => {
192
- clearInterval(dashboardPollRef.current);
193
- stopLogPolling();
194
- };
195
- }, [fetchDashboard, stopLogPolling]); // intentionally shallow — only run on mount
196
-
197
-
198
- // ── Derived values ───────────────────────────────────────────
199
- if (loading) {
200
- return (
201
- <div className="flex items-center justify-center py-2xl font-label-md text-label-md text-on-surface-variant">
202
- <span className="material-symbols-outlined animate-spin mr-sm">sync</span>
203
- Loading Security Console...
204
- </div>
205
- );
206
- }
207
-
208
- const counts = summary?.vulnerabilities_count || { critical: 0, high: 0, medium: 0, low: 0, total: 0 };
209
-
210
- // Real-time dynamic security score calculation
211
- const score = summary?.average_security_score ?? 100;
212
- const dashOffset = 283 - (283 * score) / 100;
213
-
214
- let scoreLabel = 'Excellent';
215
- let scoreColorClass = 'text-primary';
216
- if (score < 50) { scoreLabel = 'Critical'; scoreColorClass = 'text-error'; }
217
- else if (score < 80) { scoreLabel = 'Warning'; scoreColorClass = 'text-tertiary'; }
218
-
219
- const getRatingGrade = (s) => {
220
- if (s === null || s === undefined) return '--';
221
- if (s >= 90) return 'A'; if (s >= 80) return 'B';
222
- if (s >= 70) return 'C'; if (s >= 50) return 'D'; return 'F';
223
- };
224
- const ratingColor = (g) => ({
225
- A: 'text-green-600', B: 'text-green-500', C: 'text-yellow-600',
226
- D: 'text-orange-600', F: 'text-red-600'
227
- }[g] || 'text-slate-400');
228
-
229
- // Log line coloring — match exactly what backend writes
230
- const getLogColor = (log) => {
231
- if (log.includes('[VULN]')) return 'text-red-400 font-semibold';
232
- if (log.includes('[WARN]')) return 'text-yellow-400';
233
- if (log.includes('[SUCCESS]')) return 'text-green-400 font-semibold';
234
- if (log.includes('[INFO]')) return 'text-blue-300';
235
- if (log.includes('[ERROR]')) return 'text-red-500 font-bold';
236
- return 'text-slate-300';
237
- };
238
-
239
- // Chart: last 7 days line chart data
240
- const buildChart = () => {
241
- const days = ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'];
242
- const today = new Date();
243
- const buckets = Array.from({ length: 7 }, (_, i) => {
244
- const d = new Date(today); d.setDate(today.getDate() - (6 - i));
245
- return { date: d, name: days[d.getDay()], Scans: 0, Threats: 0 };
246
- });
247
- recentScans.forEach(scan => {
248
- if (!scan.started_at) return;
249
- const sd = new Date(scan.started_at);
250
- const b = buckets.find(b => b.date.toDateString() === sd.toDateString());
251
- if (b) {
252
- b.Scans++;
253
- const v = scan.vulnerabilities_count || {};
254
- b.Threats += (v.critical||0) + (v.high||0) + (v.medium||0) + (v.low||0);
255
- }
256
- });
257
- return buckets;
258
- };
259
-
260
- const chartData = buildChart();
261
-
262
- // Derived totals
263
- const totalCounts = counts.critical + counts.high + counts.medium + counts.low;
264
-
265
- const handleSort = (column) => {
266
- if (column === 'Severity' || column === 'Actions') return;
267
- if (sortColumn === column) {
268
- setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc');
269
- } else {
270
- setSortColumn(column);
271
- setSortDirection('asc');
272
- }
273
- };
274
-
275
- const getSortedScans = () => {
276
- return [...recentScans].sort((a, b) => {
277
- let aVal, bVal;
278
- switch (sortColumn) {
279
- case 'Status':
280
- aVal = a.status || ''; bVal = b.status || ''; break;
281
- case 'Target URL':
282
- aVal = a.target_url || ''; bVal = b.target_url || ''; break;
283
- case 'Scan Profile':
284
- aVal = a.scan_type || ''; bVal = b.scan_type || ''; break;
285
- case 'Date':
286
- aVal = new Date(a.started_at || 0).getTime(); bVal = new Date(b.started_at || 0).getTime(); break;
287
- case 'Rating':
288
- aVal = a.security_score || 0; bVal = b.security_score || 0; break;
289
- default:
290
- return 0;
291
- }
292
- if (aVal < bVal) return sortDirection === 'asc' ? -1 : 1;
293
- if (aVal > bVal) return sortDirection === 'asc' ? 1 : -1;
294
- return 0;
295
- });
296
- };
297
-
298
- return (
299
- <div className="flex flex-col gap-gutter text-left">
300
-
301
- {/* Page Header */}
302
- <div className="flex flex-col md:flex-row md:items-end justify-between gap-sm mb-sm">
303
- <div>
304
- <h2 className="font-headline-lg-mobile md:font-headline-lg text-headline-lg-mobile md:text-headline-lg text-on-surface font-bold tracking-tight">
305
- Security Dashboard
306
- </h2>
307
- <p className="font-body-sm text-body-sm text-on-surface-variant mt-xs">
308
- Real-time infrastructure health and vulnerability monitoring.
309
- </p>
310
- </div>
311
- <div className="flex items-center gap-sm">
312
- <OrganizationSelector />
313
- <div className="flex items-center gap-xs text-on-surface-variant bg-surface-container-low px-sm py-xs rounded-md border border-outline-variant">
314
- <span className="material-symbols-outlined" style={{ fontSize: '16px' }}>schedule</span>
315
- <span className="font-label-sm text-label-sm uppercase">
316
- {lastUpdated ? `Updated ${lastUpdated.toLocaleTimeString()}` : 'Loading...'}
317
- </span>
318
- </div>
319
- </div>
320
- </div>
321
-
322
- {/* ── Live Scan Terminal ── */}
323
- {activeScan && (
324
- <div className="w-full bg-[#020617] border border-[#1e293b] rounded-xl overflow-hidden shadow-2xl">
325
- <div className="bg-[#0f172a] px-md py-sm border-b border-[#1e293b] flex items-center justify-between">
326
- <div className="flex items-center gap-sm">
327
- <span className="relative flex h-3 w-3">
328
- <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-red-400 opacity-75"></span>
329
- <span className="relative inline-flex rounded-full h-3 w-3 bg-red-500"></span>
330
- </span>
331
- <span className="font-label-md text-label-md text-white font-bold tracking-tight ml-1">
332
- LIVE AUDIT — {activeScan.target_url}
333
- </span>
334
- </div>
335
- <div className="flex items-center gap-sm">
336
- <span className="font-label-sm text-label-sm text-slate-400 bg-slate-800 px-sm py-[2px] rounded uppercase">
337
- {activeScan.scan_type} Scan
338
- </span>
339
- <span className="font-label-sm text-label-sm text-yellow-400 bg-yellow-400/10 border border-yellow-400/20 px-sm py-[2px] rounded uppercase animate-pulse">
340
- Running
341
- </span>
342
- </div>
343
- </div>
344
-
345
- <div
346
- ref={logContainerRef}
347
- className="p-md font-mono text-[12.5px] leading-relaxed h-56 overflow-y-auto flex flex-col gap-[2px] scroll-smooth"
348
- >
349
- {liveLogs.length === 0 ? (
350
- <div className="text-slate-500 animate-pulse">⏳ Spawning audit worker threads...</div>
351
- ) : (
352
- liveLogs.map((log, i) => (
353
- <div key={i} className={`${getLogColor(log)} leading-snug`}>{log}</div>
354
- ))
355
- )}
356
- </div>
357
-
358
- <div className="bg-[#0a0f1e] border-t border-[#1e293b] px-md py-xs flex items-center justify-between">
359
- <span className="font-label-sm text-label-sm text-slate-500">{liveLogs.length} log entries</span>
360
- <span className="text-slate-400 text-xs animate-pulse">● Scanning in progress...</span>
361
- </div>
362
- </div>
363
- )}
364
-
365
- {/* ── Scan Complete Banner ── */}
366
- {completedScanId && !activeScan && (
367
- (() => {
368
- const scanData = recentScans.find(s => s.id === completedScanId);
369
- const isFailed = scanData?.status === 'failed';
370
- const isTerminated = scanData?.status === 'terminated';
371
-
372
- if (isTerminated) {
373
- return (
374
- <div className="w-full bg-red-50 border border-red-200 rounded-xl p-md flex items-center justify-between shadow-sm">
375
- <div className="flex items-center gap-sm">
376
- <span className="material-symbols-outlined text-red-600 text-[28px]" style={{ fontVariationSettings: "'FILL' 1" }}>error</span>
377
- <div>
378
- <p className="font-label-md text-label-md text-red-900 font-bold">Scanner Terminated</p>
379
- <p className="font-body-sm text-body-sm text-red-700">Sorry, due to some problemes we terminate your scanning and also show that scanner is terminated.</p>
380
- </div>
381
- </div>
382
- <Link
383
- to={`/scans/results?id=${completedScanId}`}
384
- className="bg-red-600 hover:bg-red-700 text-white font-label-md text-label-md px-lg py-sm rounded-lg flex items-center gap-sm transition-colors font-bold border-0"
385
- style={{ textDecoration: 'none' }}
386
- >
387
- <span className="material-symbols-outlined text-[18px]">open_in_new</span>
388
- View Details
389
- </Link>
390
- </div>
391
- );
392
- }
393
-
394
- if (isFailed) {
395
- return (
396
- <div className="w-full bg-red-50 border border-red-200 rounded-xl p-md flex items-center justify-between shadow-sm">
397
- <div className="flex items-center gap-sm">
398
- <span className="material-symbols-outlined text-red-600 text-[28px]" style={{ fontVariationSettings: "'FILL' 1" }}>error</span>
399
- <div>
400
- <p className="font-label-md text-label-md text-red-900 font-bold">Scan Failed!</p>
401
- <p className="font-body-sm text-body-sm text-red-700">The scanner encountered a critical error. View logs for details.</p>
402
- </div>
403
- </div>
404
- <Link
405
- to={`/scans/results?id=${completedScanId}`}
406
- className="bg-red-600 hover:bg-red-700 text-white font-label-md text-label-md px-lg py-sm rounded-lg flex items-center gap-sm transition-colors font-bold border-0"
407
- style={{ textDecoration: 'none' }}
408
- >
409
- <span className="material-symbols-outlined text-[18px]">open_in_new</span>
410
- View Details
411
- </Link>
412
- </div>
413
- );
414
- }
415
-
416
- return (
417
- <div className="w-full bg-green-50 border border-green-200 rounded-xl p-md flex items-center justify-between shadow-sm">
418
- <div className="flex items-center gap-sm">
419
- <span className="material-symbols-outlined text-green-600 text-[28px]" style={{ fontVariationSettings: "'FILL' 1" }}>check_circle</span>
420
- <div>
421
- <p className="font-label-md text-label-md text-green-900 font-bold">Scan Complete!</p>
422
- <p className="font-body-sm text-body-sm text-green-700">Vulnerability analysis finished. View the full report below.</p>
423
- </div>
424
- </div>
425
- <Link
426
- to={`/scans/results?id=${completedScanId}`}
427
- className="bg-green-600 hover:bg-green-700 text-white font-label-md text-label-md px-lg py-sm rounded-lg flex items-center gap-sm transition-colors font-bold border-0"
428
- style={{ textDecoration: 'none' }}
429
- >
430
- <span className="material-symbols-outlined text-[18px]">open_in_new</span>
431
- View Full Report
432
- </Link>
433
- </div>
434
- );
435
- })()
436
- )}
437
-
438
- {/* ── Bento Grid ── */}
439
- <div className="grid grid-cols-1 md:grid-cols-12 gap-gutter">
440
-
441
- {/* Security Score Gauge */}
442
- <div className="md:col-span-6 bg-surface-container-lowest border border-outline-variant rounded-xl p-lg flex flex-col justify-between shadow-sm relative overflow-hidden">
443
- <div className="absolute top-0 left-0 w-full h-full bg-gradient-to-br from-primary/5 to-transparent pointer-events-none"></div>
444
- <div>
445
- <h3 className="font-headline-md text-headline-md text-on-surface tracking-tight">Security Score</h3>
446
- <p className="font-body-sm text-body-sm text-on-surface-variant mt-xs">Overall system resilience</p>
447
- </div>
448
- <div className="flex-grow flex flex-col items-center justify-center py-xl">
449
- <div className="relative w-48 h-48 flex items-center justify-center">
450
- <svg className="w-full h-full absolute transform -rotate-90" viewBox="0 0 100 100">
451
- <circle cx="50" cy="50" fill="none" r="45" stroke="#e5eeff" strokeWidth="8"></circle>
452
- </svg>
453
- <svg className="w-full h-full absolute transform -rotate-90" viewBox="0 0 100 100">
454
- <circle
455
- className="transition-all duration-1000 ease-out"
456
- cx="50" cy="50" fill="none" r="45"
457
- stroke={score < 50 ? '#ba1a1a' : score < 80 ? '#bc4800' : '#004ac6'}
458
- strokeDasharray="283"
459
- strokeDashoffset={dashOffset}
460
- strokeLinecap="round"
461
- strokeWidth="8"
462
- />
463
- </svg>
464
- <div className="text-center flex flex-col items-center z-10">
465
- <span className="font-display-lg text-display-lg text-primary tracking-tighter">{score}</span>
466
- <span className={`font-label-sm text-label-sm uppercase tracking-widest bg-primary/10 px-xs py-[2px] rounded-sm mt-xs ${scoreColorClass}`}>
467
- {scoreLabel}
468
- </span>
469
- <span className="font-label-sm text-label-sm text-on-surface-variant/70 mt-base">
470
- {score >= 80 ? 'System Protected' : 'Remediation Required'}
471
- </span>
472
- </div>
473
- </div>
474
- </div>
475
- <div className="flex items-center justify-between text-body-sm font-body-sm border-t border-outline-variant pt-sm mt-sm">
476
- <span className="text-on-surface-variant">Live security posture</span>
477
- <span className={`font-medium flex items-center ${
478
- score >= 80 ? 'text-green-600' : score >= 50 ? 'text-orange-600' : 'text-error'
479
- }`}>
480
- <span className="material-symbols-outlined text-[16px] mr-[2px]">
481
- {score >= 80 ? 'trending_up' : score >= 50 ? 'trending_flat' : 'trending_down'}
482
- </span>
483
- {score >= 80 ? 'Stable' : score >= 50 ? 'Needs Attention' : 'Critical Risk'}
484
- </span>
485
- </div>
486
- </div>
487
-
488
- {/* Stats + Chart */}
489
- <div className="md:col-span-6 flex flex-col gap-gutter">
490
-
491
- {/* Vulnerability Category Breakdown Bar Chart with Visible Numbers */}
492
- <div className="w-full h-full bg-surface-container-lowest border border-outline-variant rounded-xl p-lg shadow-sm flex flex-col">
493
- <div className="w-full flex justify-between items-center mb-md">
494
- <h3 className="font-headline-md text-headline-md text-on-surface tracking-tight font-bold">Vulnerability Categories</h3>
495
- <span className="text-[12px] font-bold text-on-surface-variant bg-surface-container px-2 py-0.5 rounded border border-outline-variant">
496
- Top Vectors
497
- </span>
498
- </div>
499
- <div className="flex-grow min-h-[260px] w-full">
500
- <ResponsiveContainer width="100%" height="100%">
501
- <BarChart
502
- data={[
503
- { category: 'Critical', count: counts.critical || 0 },
504
- { category: 'High', count: counts.high || 0 },
505
- { category: 'Medium', count: counts.medium || 0 },
506
- { category: 'Low', count: counts.low || 0 }
507
- ]}
508
- margin={{ top: 30, right: 15, left: -10, bottom: 25 }}
509
- >
510
- <defs>
511
- <linearGradient id="colorCritical" x1="0" y1="0" x2="0" y2="1">
512
- <stop offset="0%" stopColor="#ef4444" stopOpacity={1}/>
513
- <stop offset="100%" stopColor="#991b1b" stopOpacity={0.95}/>
514
- </linearGradient>
515
- <linearGradient id="colorHigh" x1="0" y1="0" x2="0" y2="1">
516
- <stop offset="0%" stopColor="#f97316" stopOpacity={1}/>
517
- <stop offset="100%" stopColor="#c2410c" stopOpacity={0.95}/>
518
- </linearGradient>
519
- <linearGradient id="colorMedium" x1="0" y1="0" x2="0" y2="1">
520
- <stop offset="0%" stopColor="#eab308" stopOpacity={1}/>
521
- <stop offset="100%" stopColor="#854d0e" stopOpacity={0.95}/>
522
- </linearGradient>
523
- <linearGradient id="colorLow" x1="0" y1="0" x2="0" y2="1">
524
- <stop offset="0%" stopColor="#3b82f6" stopOpacity={1}/>
525
- <stop offset="100%" stopColor="#1e40af" stopOpacity={0.95}/>
526
- </linearGradient>
527
- </defs>
528
- <CartesianGrid strokeDasharray="3 3" stroke="#334155" vertical={false} />
529
- <XAxis
530
- dataKey="category"
531
- stroke="#475569"
532
- fontSize={12}
533
- tickLine={false}
534
- tick={{ fill: '#000000', fontSize: 12, fontWeight: 'bold' }}
535
- angle={0}
536
- textAnchor="middle"
537
- height={30}
538
- interval={0}
539
- />
540
- <YAxis
541
- stroke="#475569"
542
- fontSize={11}
543
- tickLine={false}
544
- axisLine={false}
545
- allowDecimals={false}
546
- tick={{ fill: '#000000', fontSize: 11, fontWeight: 'bold' }}
547
- />
548
- <RechartsTooltip
549
- contentStyle={{ backgroundColor: '#0f172a', border: '1px solid #334155', borderRadius: '8px', color: '#f8fafc' }}
550
- formatter={(val) => [val, 'Findings']}
551
- />
552
- <Bar dataKey="count" name="Findings" radius={[8, 8, 0, 0]} barSize={45}>
553
- <Cell key="cell-0" fill="url(#colorCritical)" />
554
- <Cell key="cell-1" fill="url(#colorHigh)" />
555
- <Cell key="cell-2" fill="url(#colorMedium)" />
556
- <Cell key="cell-3" fill="url(#colorLow)" />
557
- <LabelList
558
- dataKey="count"
559
- position="top"
560
- fill="#000000"
561
- fontSize={12}
562
- fontWeight="bold"
563
- offset={8}
564
- />
565
- </Bar>
566
- </BarChart>
567
- </ResponsiveContainer>
568
- </div>
569
- </div>
570
- </div>
571
- </div>
572
-
573
- {/* Recent Scans Table */}
574
- <div className="bg-surface-container-lowest border border-outline-variant rounded-xl shadow-sm flex flex-col overflow-hidden">
575
- <div className="p-lg border-b border-outline-variant flex justify-between items-center">
576
- <h3 className="font-headline-md text-headline-md text-on-surface tracking-tight">Configured Target Assets</h3>
577
- <Link className="font-label-md text-label-md text-primary hover:underline" to="/scans/history" style={{ textDecoration: 'none' }}>
578
- View Full Audit Log
579
- </Link>
580
- </div>
581
-
582
- {recentScans.length === 0 ? (
583
- <div className="text-center py-2xl text-on-surface-variant font-body-sm">
584
- No target domains scanned yet. Launch your first website scan under the New Scan tab!
585
- </div>
586
- ) : (
587
- <div className="overflow-x-auto">
588
- <table className="w-full text-left border-collapse min-w-[800px]">
589
- <thead>
590
- <tr className="bg-surface-container-low border-b border-outline-variant">
591
- {['Status','Target URL','Scan Profile','Date','Rating','Severity','Actions'].map((h, i) => (
592
- <th
593
- key={h}
594
- onClick={() => handleSort(h)}
595
- 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' : ''}`}
596
- >
597
- <div className={`flex items-center gap-xs ${i === 6 ? 'justify-end' : ''}`}>
598
- {h}
599
- {(h !== 'Severity' && h !== 'Actions') && (
600
- <span className={`material-symbols-outlined text-[16px] opacity-0 group-hover:opacity-50 transition-opacity ${sortColumn === h ? 'opacity-100 group-hover:opacity-100 text-primary' : ''}`}>
601
- {sortColumn === h && sortDirection === 'desc' ? 'arrow_downward' : 'arrow_upward'}
602
- </span>
603
- )}
604
- </div>
605
- </th>
606
- ))}
607
- </tr>
608
- </thead>
609
- <tbody className="font-body-sm text-body-sm text-on-surface">
610
- {getSortedScans().slice(0, 8).map((scan) => {
611
- let dot = 'bg-primary', statusText = 'Clean', rowBg = '';
612
- if (scan.status === 'completed') {
613
- if (scan.security_score < 60) { dot = 'bg-error animate-pulse'; statusText = 'Critical'; rowBg = 'bg-error/5'; }
614
- else if (scan.security_score < 80) { dot = 'bg-tertiary'; statusText = 'Warning'; }
615
- else { dot = 'bg-green-500'; statusText = 'Secure'; }
616
- } else if (scan.status === 'scanning' || scan.status === 'queued') {
617
- dot = 'bg-yellow-500 animate-pulse'; statusText = 'Scanning';
618
- } else if (scan.status === 'terminated') {
619
- dot = 'bg-slate-400'; statusText = 'Terminated';
620
- } else {
621
- dot = 'bg-slate-400'; statusText = 'Failed';
622
- }
623
- const grade = getRatingGrade(scan.security_score);
624
-
625
- return (
626
- <tr key={scan.id} className={`border-b border-outline-variant/50 hover:bg-surface-bright transition-colors ${rowBg}`}>
627
- <td className="py-md px-lg">
628
- <div className="flex items-center gap-xs">
629
- <div className={`w-2.5 h-2.5 rounded-full ${dot}`}></div>
630
- <span className="font-medium">{statusText}</span>
631
- </div>
632
- </td>
633
- <td className="py-md px-lg font-label-md text-on-surface-variant font-bold max-w-[200px] truncate">{scan.target_url}</td>
634
- <td className="py-md px-lg text-on-surface-variant">{scan.scan_type} Assessment</td>
635
- <td className="py-md px-lg text-on-surface-variant font-medium text-xs">
636
- {scan.started_at ? new Date(scan.started_at).toLocaleString('en-US', {
637
- day: 'numeric', month: 'short', year: 'numeric', hour: '2-digit', minute: '2-digit'
638
- }) : 'Pending'}
639
- </td>
640
- <td className="py-md px-lg">
641
- <span className={`font-mono text-[20px] font-extrabold ${ratingColor(grade)}`}>{grade}</span>
642
- {scan.security_score !== null && <span className="text-on-surface-variant text-xs ml-1 opacity-70">({scan.security_score})</span>}
643
- </td>
644
- <td className="py-md px-lg">
645
- <div className="flex gap-sm flex-wrap">
646
- {(scan.vulnerabilities_count?.critical > 0) && <span className="bg-red-500/10 text-red-600 px-sm py-[2px] rounded text-xs font-bold border border-red-500/20">{scan.vulnerabilities_count.critical} Crit</span>}
647
- {(scan.vulnerabilities_count?.high > 0) && <span className="bg-orange-500/10 text-orange-600 px-sm py-[2px] rounded text-xs font-bold border border-orange-500/20">{scan.vulnerabilities_count.high} High</span>}
648
- {(scan.vulnerabilities_count?.total === 0) && <span className="bg-green-500/10 text-green-600 px-sm py-[2px] rounded text-xs font-bold border border-green-500/20">Clean</span>}
649
- {scan.status === 'scanning' && <span className="bg-yellow-500/10 text-yellow-600 px-sm py-[2px] rounded text-xs font-bold border border-yellow-500/20 animate-pulse">Scanning...</span>}
650
- </div>
651
- </td>
652
- <td className="py-md px-lg text-right">
653
- <Link to={`/scans/results?id=${scan.id}`} className="text-primary hover:text-primary-container font-semibold inline-flex items-center gap-xs" style={{ textDecoration: 'none' }}>
654
- Details <span className="material-symbols-outlined text-[16px]">arrow_forward</span>
655
- </Link>
656
- </td>
657
- </tr>
658
- );
659
- })}
660
- </tbody>
661
- </table>
662
- </div>
663
- )}
664
- </div>
665
-
666
- </div>
667
- );
668
- };
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useEffect, useRef, useCallback } from 'react';
2
+ import { Link, useNavigate } from 'react-router-dom';
3
+ import { useAuth } from '../components/AuthContext';
4
+ 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';
5
+
6
+ import { Navigate } from 'react-router-dom';
7
+ import { OrganizationSelector } from '../components/OrganizationSelector';
8
+
9
+ const ACTIVE_SCAN_KEY = 'wss_active_scan'; // localStorage key for persistence
10
+
11
+ const CustomBarTooltip = ({ active, payload, label }) => {
12
+ if (active && payload && payload.length) {
13
+ return (
14
+ <div className="bg-[#0f172a] border border-[#334155] rounded-lg px-3 py-1.5 shadow-xl text-left pointer-events-none">
15
+ <div className="text-[12px] font-bold text-sky-400 leading-tight">{label}</div>
16
+ <div className="text-[12px] font-semibold text-slate-200 leading-tight mt-1">
17
+ Findings : <span className="font-extrabold text-white">{payload[0].value}</span>
18
+ </div>
19
+ </div>
20
+ );
21
+ }
22
+ return null;
23
+ };
24
+
25
+ export const Dashboard = () => {
26
+ const { token, refreshAccessToken, user } = useAuth();
27
+
28
+ if (user?.role === 'executive_user') {
29
+ return <Navigate to="/scans/history" replace />;
30
+ }
31
+
32
+ const [summary, setSummary] = useState(null);
33
+ const [recentScans, setRecentScans] = useState([]);
34
+ const [activeScan, setActiveScan] = useState(null);
35
+ const [liveLogs, setLiveLogs] = useState([]);
36
+ const [loading, setLoading] = useState(true);
37
+ const [completedScanId, setCompletedScanId] = useState(null);
38
+ const [lastUpdated, setLastUpdated] = useState(null);
39
+ const [sortColumn, setSortColumn] = useState('Date');
40
+ const [sortDirection, setSortDirection] = useState('desc');
41
+
42
+ const logContainerRef = useRef(null);
43
+ const logPollRef = useRef(null);
44
+ const dashboardPollRef = useRef(null);
45
+ const activeScanRef = useRef(null);
46
+ // Always read the latest token from localStorage so the poller works even
47
+ // after a token refresh (avoids stale closure issues)
48
+ const getToken = useCallback(() =>
49
+ localStorage.getItem('wss_token') || token
50
+ , [token]);
51
+
52
+ // Auto-scroll log terminal when new logs arrive
53
+ useEffect(() => {
54
+ if (logContainerRef.current) {
55
+ logContainerRef.current.scrollTop = logContainerRef.current.scrollHeight;
56
+ }
57
+ }, [liveLogs]);
58
+
59
+ // ── Persist active scan to localStorage ─────────────────────
60
+ const persistActiveScan = useCallback((scan) => {
61
+ if (scan) {
62
+ localStorage.setItem(ACTIVE_SCAN_KEY, JSON.stringify({ id: scan.id, target_url: scan.target_url, scan_type: scan.scan_type }));
63
+ } else {
64
+ localStorage.removeItem(ACTIVE_SCAN_KEY);
65
+ }
66
+ }, []);
67
+
68
+ // ── Log Polling ─────────────────────────────────────────────
69
+ const stopLogPolling = useCallback(() => {
70
+ if (logPollRef.current) {
71
+ clearInterval(logPollRef.current);
72
+ logPollRef.current = null;
73
+ }
74
+ }, []);
75
+
76
+ const startLogPolling = useCallback((scan) => {
77
+ stopLogPolling();
78
+ activeScanRef.current = scan;
79
+ persistActiveScan(scan);
80
+ let consecutiveErrors = 0;
81
+ const MAX_ERRORS = 20; // tolerate up to 20 failures (covers token refresh + brief outages)
82
+
83
+ logPollRef.current = setInterval(async () => {
84
+ if (!activeScanRef.current) { stopLogPolling(); return; }
85
+
86
+ let activeToken = getToken();
87
+
88
+ try {
89
+ let res = await fetch(`/api/scans/${scan.id}/logs`, {
90
+ headers: { 'Authorization': `Bearer ${activeToken}` }
91
+ });
92
+
93
+ // Token expired — try to refresh it silently
94
+ if (res.status === 401) {
95
+ const newToken = await refreshAccessToken();
96
+ if (newToken) {
97
+ activeToken = newToken;
98
+ res = await fetch(`/api/scans/${scan.id}/logs`, {
99
+ headers: { 'Authorization': `Bearer ${newToken}` }
100
+ });
101
+ }
102
+ }
103
+
104
+ if (!res.ok) {
105
+ consecutiveErrors++;
106
+ if (consecutiveErrors >= MAX_ERRORS) stopLogPolling();
107
+ return;
108
+ }
109
+
110
+ consecutiveErrors = 0;
111
+ const data = await res.json();
112
+ if (data.logs && data.logs.length > 0) {
113
+ setLiveLogs(data.logs);
114
+ }
115
+
116
+ if (data.status === 'completed' || data.status === 'failed' || data.status === 'terminated') {
117
+ stopLogPolling();
118
+ setActiveScan(null);
119
+ activeScanRef.current = null;
120
+ persistActiveScan(null); // clear localStorage
121
+ setCompletedScanId(scan.id);
122
+ fetchDashboard();
123
+ }
124
+ } catch (err) {
125
+ consecutiveErrors++;
126
+ console.error('[Dashboard] Log poll error:', err);
127
+ if (consecutiveErrors >= MAX_ERRORS) stopLogPolling();
128
+ }
129
+ }, 1500);
130
+ }, [getToken, refreshAccessToken, stopLogPolling, persistActiveScan]);
131
+
132
+ // ── Dashboard Data Fetch ─────────────────────────────────────
133
+ const fetchDashboard = useCallback(async () => {
134
+ try {
135
+ const activeToken = getToken();
136
+ const [summaryRes, historyRes] = await Promise.all([
137
+ fetch('/api/vulnerabilities/summary', { headers: { 'Authorization': `Bearer ${activeToken}` } }),
138
+ fetch('/api/scans/history', { headers: { 'Authorization': `Bearer ${activeToken}` } })
139
+ ]);
140
+
141
+ if (!summaryRes.ok || !historyRes.ok) return;
142
+
143
+ const summaryData = await summaryRes.json();
144
+ const historyData = await historyRes.json();
145
+
146
+ setSummary(summaryData.summary);
147
+ setRecentScans(historyData.scans || []);
148
+ setLastUpdated(new Date());
149
+
150
+ const running = (historyData.scans || []).find(
151
+ s => s.status === 'scanning' || s.status === 'queued'
152
+ );
153
+
154
+ if (running) {
155
+ if (!activeScanRef.current || activeScanRef.current.id !== running.id) {
156
+ setActiveScan(running);
157
+ setLiveLogs([]);
158
+ startLogPolling(running);
159
+ }
160
+ } else if (!running && activeScanRef.current && !logPollRef.current) {
161
+ setActiveScan(null);
162
+ activeScanRef.current = null;
163
+ persistActiveScan(null);
164
+ stopLogPolling();
165
+ }
166
+ } catch (err) {
167
+ console.error('[Dashboard] Fetch error:', err);
168
+ } finally {
169
+ setLoading(false);
170
+ }
171
+ }, [getToken, startLogPolling, stopLogPolling, persistActiveScan]);
172
+
173
+ // ── Mount recover active scan from localStorage ─────────────────────────
174
+ useEffect(() => {
175
+ // 1. Immediately try to restore a previously active scan from localStorage
176
+ // so LIVE AUDIT appears instantly even after refresh or re-login.
177
+ const stored = localStorage.getItem(ACTIVE_SCAN_KEY);
178
+ if (stored && token) {
179
+ try {
180
+ const storedScan = JSON.parse(stored);
181
+ // Validate it's still running before starting the poller
182
+ fetch(`/api/scans/${storedScan.id}/logs`, {
183
+ headers: { 'Authorization': `Bearer ${getToken()}` }
184
+ }).then(async (r) => {
185
+ if (r.ok) {
186
+ const d = await r.json();
187
+ if (d.status === 'scanning' || d.status === 'queued') {
188
+ setActiveScan(storedScan);
189
+ setLiveLogs(d.logs || []);
190
+ startLogPolling(storedScan);
191
+ } else {
192
+ // Scan already done — clean up localStorage
193
+ localStorage.removeItem(ACTIVE_SCAN_KEY);
194
+ }
195
+ }
196
+ }).catch(() => {});
197
+ } catch (_) {
198
+ localStorage.removeItem(ACTIVE_SCAN_KEY);
199
+ }
200
+ }
201
+
202
+ // 2. Then do the normal full dashboard fetch
203
+ fetchDashboard();
204
+ dashboardPollRef.current = setInterval(fetchDashboard, 5000);
205
+ return () => {
206
+ clearInterval(dashboardPollRef.current);
207
+ stopLogPolling();
208
+ };
209
+ }, [fetchDashboard, stopLogPolling]); // intentionally shallow — only run on mount
210
+
211
+
212
+ // ── Derived values ───────────────────────────────────────────
213
+ if (loading) {
214
+ return (
215
+ <div className="flex items-center justify-center py-2xl font-label-md text-label-md text-on-surface-variant">
216
+ <span className="material-symbols-outlined animate-spin mr-sm">sync</span>
217
+ Loading Security Console...
218
+ </div>
219
+ );
220
+ }
221
+
222
+ const counts = summary?.vulnerabilities_count || { critical: 0, high: 0, medium: 0, low: 0, total: 0 };
223
+
224
+ // Real-time dynamic security score calculation
225
+ const score = summary?.average_security_score ?? 100;
226
+ const dashOffset = 283 - (283 * score) / 100;
227
+
228
+ let scoreLabel = 'Excellent';
229
+ let scoreColorClass = 'text-primary';
230
+ if (score < 50) { scoreLabel = 'Critical'; scoreColorClass = 'text-error'; }
231
+ else if (score < 80) { scoreLabel = 'Warning'; scoreColorClass = 'text-tertiary'; }
232
+
233
+ const getRatingGrade = (s) => {
234
+ if (s === null || s === undefined) return '--';
235
+ if (s >= 90) return 'A'; if (s >= 80) return 'B';
236
+ if (s >= 70) return 'C'; if (s >= 50) return 'D'; return 'F';
237
+ };
238
+ const ratingColor = (g) => ({
239
+ A: 'text-green-600', B: 'text-green-500', C: 'text-yellow-600',
240
+ D: 'text-orange-600', F: 'text-red-600'
241
+ }[g] || 'text-slate-400');
242
+
243
+ // Log line coloring match exactly what backend writes
244
+ const getLogColor = (log) => {
245
+ if (log.includes('[VULN]')) return 'text-red-400 font-semibold';
246
+ if (log.includes('[WARN]')) return 'text-yellow-400';
247
+ if (log.includes('[SUCCESS]')) return 'text-green-400 font-semibold';
248
+ if (log.includes('[INFO]')) return 'text-blue-300';
249
+ if (log.includes('[ERROR]')) return 'text-red-500 font-bold';
250
+ return 'text-slate-300';
251
+ };
252
+
253
+ // Chart: last 7 days line chart data
254
+ const buildChart = () => {
255
+ const days = ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'];
256
+ const today = new Date();
257
+ const buckets = Array.from({ length: 7 }, (_, i) => {
258
+ const d = new Date(today); d.setDate(today.getDate() - (6 - i));
259
+ return { date: d, name: days[d.getDay()], Scans: 0, Threats: 0 };
260
+ });
261
+ recentScans.forEach(scan => {
262
+ if (!scan.started_at) return;
263
+ const sd = new Date(scan.started_at);
264
+ const b = buckets.find(b => b.date.toDateString() === sd.toDateString());
265
+ if (b) {
266
+ b.Scans++;
267
+ const v = scan.vulnerabilities_count || {};
268
+ b.Threats += (v.critical||0) + (v.high||0) + (v.medium||0) + (v.low||0);
269
+ }
270
+ });
271
+ return buckets;
272
+ };
273
+
274
+ const chartData = buildChart();
275
+
276
+ // Derived totals
277
+ const totalCounts = counts.critical + counts.high + counts.medium + counts.low;
278
+
279
+ const handleSort = (column) => {
280
+ if (column === 'Severity' || column === 'Actions') return;
281
+ if (sortColumn === column) {
282
+ setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc');
283
+ } else {
284
+ setSortColumn(column);
285
+ setSortDirection('asc');
286
+ }
287
+ };
288
+
289
+ const getSortedScans = () => {
290
+ return [...recentScans].sort((a, b) => {
291
+ let aVal, bVal;
292
+ switch (sortColumn) {
293
+ case 'Status':
294
+ aVal = a.status || ''; bVal = b.status || ''; break;
295
+ case 'Target URL':
296
+ aVal = a.target_url || ''; bVal = b.target_url || ''; break;
297
+ case 'Scan Profile':
298
+ aVal = a.scan_type || ''; bVal = b.scan_type || ''; break;
299
+ case 'Date':
300
+ aVal = new Date(a.started_at || 0).getTime(); bVal = new Date(b.started_at || 0).getTime(); break;
301
+ case 'Rating':
302
+ aVal = a.security_score || 0; bVal = b.security_score || 0; break;
303
+ default:
304
+ return 0;
305
+ }
306
+ if (aVal < bVal) return sortDirection === 'asc' ? -1 : 1;
307
+ if (aVal > bVal) return sortDirection === 'asc' ? 1 : -1;
308
+ return 0;
309
+ });
310
+ };
311
+
312
+ return (
313
+ <div className="flex flex-col gap-gutter text-left">
314
+
315
+ {/* Page Header */}
316
+ <div className="flex flex-col md:flex-row md:items-end justify-between gap-sm mb-sm">
317
+ <div>
318
+ <h2 className="font-headline-lg-mobile md:font-headline-lg text-headline-lg-mobile md:text-headline-lg text-on-surface font-bold tracking-tight">
319
+ Security Dashboard
320
+ </h2>
321
+ <p className="font-body-sm text-body-sm text-on-surface-variant mt-xs">
322
+ Real-time infrastructure health and vulnerability monitoring.
323
+ </p>
324
+ </div>
325
+ <div className="flex items-center gap-sm">
326
+ <OrganizationSelector />
327
+ <div className="flex items-center gap-xs text-on-surface-variant bg-surface-container-low px-sm py-xs rounded-md border border-outline-variant">
328
+ <span className="material-symbols-outlined" style={{ fontSize: '16px' }}>schedule</span>
329
+ <span className="font-label-sm text-label-sm uppercase">
330
+ {lastUpdated ? `Updated ${lastUpdated.toLocaleTimeString()}` : 'Loading...'}
331
+ </span>
332
+ </div>
333
+ </div>
334
+ </div>
335
+
336
+ {/* ── Live Scan Terminal ── */}
337
+ {activeScan && (
338
+ <div className="w-full bg-[#020617] border border-[#1e293b] rounded-xl overflow-hidden shadow-2xl">
339
+ <div className="bg-[#0f172a] px-md py-sm border-b border-[#1e293b] flex items-center justify-between">
340
+ <div className="flex items-center gap-sm">
341
+ <span className="relative flex h-3 w-3">
342
+ <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-red-400 opacity-75"></span>
343
+ <span className="relative inline-flex rounded-full h-3 w-3 bg-red-500"></span>
344
+ </span>
345
+ <span className="font-label-md text-label-md text-white font-bold tracking-tight ml-1">
346
+ LIVE AUDIT — {activeScan.target_url}
347
+ </span>
348
+ </div>
349
+ <div className="flex items-center gap-sm">
350
+ <span className="font-label-sm text-label-sm text-slate-400 bg-slate-800 px-sm py-[2px] rounded uppercase">
351
+ {activeScan.scan_type} Scan
352
+ </span>
353
+ <span className="font-label-sm text-label-sm text-yellow-400 bg-yellow-400/10 border border-yellow-400/20 px-sm py-[2px] rounded uppercase animate-pulse">
354
+ ● Running
355
+ </span>
356
+ </div>
357
+ </div>
358
+
359
+ <div
360
+ ref={logContainerRef}
361
+ className="p-md font-mono text-[12.5px] leading-relaxed h-56 overflow-y-auto flex flex-col gap-[2px] scroll-smooth"
362
+ >
363
+ {liveLogs.length === 0 ? (
364
+ <div className="text-slate-500 animate-pulse">⏳ Spawning audit worker threads...</div>
365
+ ) : (
366
+ liveLogs.map((log, i) => (
367
+ <div key={i} className={`${getLogColor(log)} leading-snug`}>{log}</div>
368
+ ))
369
+ )}
370
+ </div>
371
+
372
+ <div className="bg-[#0a0f1e] border-t border-[#1e293b] px-md py-xs flex items-center justify-between">
373
+ <span className="font-label-sm text-label-sm text-slate-500">{liveLogs.length} log entries</span>
374
+ <span className="text-slate-400 text-xs animate-pulse">● Scanning in progress...</span>
375
+ </div>
376
+ </div>
377
+ )}
378
+
379
+ {/* ── Scan Complete Banner ── */}
380
+ {completedScanId && !activeScan && (
381
+ (() => {
382
+ const scanData = recentScans.find(s => s.id === completedScanId);
383
+ const isFailed = scanData?.status === 'failed';
384
+ const isTerminated = scanData?.status === 'terminated';
385
+
386
+ if (isTerminated) {
387
+ return (
388
+ <div className="w-full bg-red-50 border border-red-200 rounded-xl p-md flex items-center justify-between shadow-sm">
389
+ <div className="flex items-center gap-sm">
390
+ <span className="material-symbols-outlined text-red-600 text-[28px]" style={{ fontVariationSettings: "'FILL' 1" }}>error</span>
391
+ <div>
392
+ <p className="font-label-md text-label-md text-red-900 font-bold">Scanner Terminated</p>
393
+ <p className="font-body-sm text-body-sm text-red-700">Sorry, due to some problemes we terminate your scanning and also show that scanner is terminated.</p>
394
+ </div>
395
+ </div>
396
+ <Link
397
+ to={`/scans/results?id=${completedScanId}`}
398
+ className="bg-red-600 hover:bg-red-700 text-white font-label-md text-label-md px-lg py-sm rounded-lg flex items-center gap-sm transition-colors font-bold border-0"
399
+ style={{ textDecoration: 'none' }}
400
+ >
401
+ <span className="material-symbols-outlined text-[18px]">open_in_new</span>
402
+ View Details
403
+ </Link>
404
+ </div>
405
+ );
406
+ }
407
+
408
+ if (isFailed) {
409
+ return (
410
+ <div className="w-full bg-red-50 border border-red-200 rounded-xl p-md flex items-center justify-between shadow-sm">
411
+ <div className="flex items-center gap-sm">
412
+ <span className="material-symbols-outlined text-red-600 text-[28px]" style={{ fontVariationSettings: "'FILL' 1" }}>error</span>
413
+ <div>
414
+ <p className="font-label-md text-label-md text-red-900 font-bold">Scan Failed!</p>
415
+ <p className="font-body-sm text-body-sm text-red-700">The scanner encountered a critical error. View logs for details.</p>
416
+ </div>
417
+ </div>
418
+ <Link
419
+ to={`/scans/results?id=${completedScanId}`}
420
+ className="bg-red-600 hover:bg-red-700 text-white font-label-md text-label-md px-lg py-sm rounded-lg flex items-center gap-sm transition-colors font-bold border-0"
421
+ style={{ textDecoration: 'none' }}
422
+ >
423
+ <span className="material-symbols-outlined text-[18px]">open_in_new</span>
424
+ View Details
425
+ </Link>
426
+ </div>
427
+ );
428
+ }
429
+
430
+ return (
431
+ <div className="w-full bg-green-50 border border-green-200 rounded-xl p-md flex items-center justify-between shadow-sm">
432
+ <div className="flex items-center gap-sm">
433
+ <span className="material-symbols-outlined text-green-600 text-[28px]" style={{ fontVariationSettings: "'FILL' 1" }}>check_circle</span>
434
+ <div>
435
+ <p className="font-label-md text-label-md text-green-900 font-bold">Scan Complete!</p>
436
+ <p className="font-body-sm text-body-sm text-green-700">Vulnerability analysis finished. View the full report below.</p>
437
+ </div>
438
+ </div>
439
+ <Link
440
+ to={`/scans/results?id=${completedScanId}`}
441
+ className="bg-green-600 hover:bg-green-700 text-white font-label-md text-label-md px-lg py-sm rounded-lg flex items-center gap-sm transition-colors font-bold border-0"
442
+ style={{ textDecoration: 'none' }}
443
+ >
444
+ <span className="material-symbols-outlined text-[18px]">open_in_new</span>
445
+ View Full Report
446
+ </Link>
447
+ </div>
448
+ );
449
+ })()
450
+ )}
451
+
452
+ {/* ── Bento Grid ── */}
453
+ <div className="grid grid-cols-1 md:grid-cols-12 gap-gutter">
454
+
455
+ {/* Security Score Gauge */}
456
+ <div className="md:col-span-6 bg-surface-container-lowest border border-outline-variant rounded-xl p-lg flex flex-col justify-between shadow-sm relative overflow-hidden">
457
+ <div className="absolute top-0 left-0 w-full h-full bg-gradient-to-br from-primary/5 to-transparent pointer-events-none"></div>
458
+ <div>
459
+ <h3 className="font-headline-md text-headline-md text-on-surface tracking-tight">Security Score</h3>
460
+ <p className="font-body-sm text-body-sm text-on-surface-variant mt-xs">Overall system resilience</p>
461
+ </div>
462
+ <div className="flex-grow flex flex-col items-center justify-center py-xl">
463
+ <div className="relative w-48 h-48 flex items-center justify-center">
464
+ <svg className="w-full h-full absolute transform -rotate-90" viewBox="0 0 100 100">
465
+ <circle cx="50" cy="50" fill="none" r="45" stroke="#e5eeff" strokeWidth="8"></circle>
466
+ </svg>
467
+ <svg className="w-full h-full absolute transform -rotate-90" viewBox="0 0 100 100">
468
+ <circle
469
+ className="transition-all duration-1000 ease-out"
470
+ cx="50" cy="50" fill="none" r="45"
471
+ stroke={score < 50 ? '#ba1a1a' : score < 80 ? '#bc4800' : '#004ac6'}
472
+ strokeDasharray="283"
473
+ strokeDashoffset={dashOffset}
474
+ strokeLinecap="round"
475
+ strokeWidth="8"
476
+ />
477
+ </svg>
478
+ <div className="text-center flex flex-col items-center z-10">
479
+ <span className="font-display-lg text-display-lg text-primary tracking-tighter">{score}</span>
480
+ <span className={`font-label-sm text-label-sm uppercase tracking-widest bg-primary/10 px-xs py-[2px] rounded-sm mt-xs ${scoreColorClass}`}>
481
+ {scoreLabel}
482
+ </span>
483
+ <span className="font-label-sm text-label-sm text-on-surface-variant/70 mt-base">
484
+ {score >= 80 ? 'System Protected' : 'Remediation Required'}
485
+ </span>
486
+ </div>
487
+ </div>
488
+ </div>
489
+ <div className="flex items-center justify-between text-body-sm font-body-sm border-t border-outline-variant pt-sm mt-sm">
490
+ <span className="text-on-surface-variant">Live security posture</span>
491
+ <span className={`font-medium flex items-center ${
492
+ score >= 80 ? 'text-green-600' : score >= 50 ? 'text-orange-600' : 'text-error'
493
+ }`}>
494
+ <span className="material-symbols-outlined text-[16px] mr-[2px]">
495
+ {score >= 80 ? 'trending_up' : score >= 50 ? 'trending_flat' : 'trending_down'}
496
+ </span>
497
+ {score >= 80 ? 'Stable' : score >= 50 ? 'Needs Attention' : 'Critical Risk'}
498
+ </span>
499
+ </div>
500
+ </div>
501
+
502
+ {/* Stats + Chart */}
503
+ <div className="md:col-span-6 flex flex-col gap-gutter">
504
+
505
+ {/* Vulnerability Category Breakdown Bar Chart with Visible Numbers */}
506
+ <div className="w-full h-full bg-surface-container-lowest border border-outline-variant rounded-xl p-lg shadow-sm flex flex-col">
507
+ <div className="w-full flex justify-between items-center mb-md">
508
+ <h3 className="font-headline-md text-headline-md text-on-surface tracking-tight font-bold">Vulnerability Categories</h3>
509
+ <span className="text-[12px] font-bold text-on-surface-variant bg-surface-container px-2 py-0.5 rounded border border-outline-variant">
510
+ Top Vectors
511
+ </span>
512
+ </div>
513
+ <div className="flex-grow min-h-[260px] w-full">
514
+ <ResponsiveContainer width="100%" height="100%">
515
+ <BarChart
516
+ data={[
517
+ { category: 'Critical', count: counts.critical || 0 },
518
+ { category: 'High', count: counts.high || 0 },
519
+ { category: 'Medium', count: counts.medium || 0 },
520
+ { category: 'Low', count: counts.low || 0 }
521
+ ]}
522
+ margin={{ top: 30, right: 15, left: -10, bottom: 25 }}
523
+ >
524
+ <defs>
525
+ <linearGradient id="colorCritical" x1="0" y1="0" x2="0" y2="1">
526
+ <stop offset="0%" stopColor="#ef4444" stopOpacity={1}/>
527
+ <stop offset="100%" stopColor="#991b1b" stopOpacity={0.95}/>
528
+ </linearGradient>
529
+ <linearGradient id="colorHigh" x1="0" y1="0" x2="0" y2="1">
530
+ <stop offset="0%" stopColor="#f97316" stopOpacity={1}/>
531
+ <stop offset="100%" stopColor="#c2410c" stopOpacity={0.95}/>
532
+ </linearGradient>
533
+ <linearGradient id="colorMedium" x1="0" y1="0" x2="0" y2="1">
534
+ <stop offset="0%" stopColor="#eab308" stopOpacity={1}/>
535
+ <stop offset="100%" stopColor="#854d0e" stopOpacity={0.95}/>
536
+ </linearGradient>
537
+ <linearGradient id="colorLow" x1="0" y1="0" x2="0" y2="1">
538
+ <stop offset="0%" stopColor="#3b82f6" stopOpacity={1}/>
539
+ <stop offset="100%" stopColor="#1e40af" stopOpacity={0.95}/>
540
+ </linearGradient>
541
+ </defs>
542
+ <CartesianGrid strokeDasharray="3 3" stroke="#334155" vertical={false} />
543
+ <XAxis
544
+ dataKey="category"
545
+ stroke="#475569"
546
+ fontSize={12}
547
+ tickLine={false}
548
+ tick={{ fill: '#000000', fontSize: 12, fontWeight: 'bold' }}
549
+ angle={0}
550
+ textAnchor="middle"
551
+ height={30}
552
+ interval={0}
553
+ />
554
+ <YAxis
555
+ stroke="#475569"
556
+ fontSize={11}
557
+ tickLine={false}
558
+ axisLine={false}
559
+ allowDecimals={false}
560
+ tick={{ fill: '#000000', fontSize: 11, fontWeight: 'bold' }}
561
+ />
562
+ <RechartsTooltip content={<CustomBarTooltip />} />
563
+ <Bar dataKey="count" name="Findings" radius={[8, 8, 0, 0]} barSize={45}>
564
+ <Cell key="cell-0" fill="url(#colorCritical)" />
565
+ <Cell key="cell-1" fill="url(#colorHigh)" />
566
+ <Cell key="cell-2" fill="url(#colorMedium)" />
567
+ <Cell key="cell-3" fill="url(#colorLow)" />
568
+ <LabelList
569
+ dataKey="count"
570
+ position="top"
571
+ fill="#000000"
572
+ fontSize={12}
573
+ fontWeight="bold"
574
+ offset={8}
575
+ />
576
+ </Bar>
577
+ </BarChart>
578
+ </ResponsiveContainer>
579
+ </div>
580
+ </div>
581
+ </div>
582
+ </div>
583
+
584
+ {/* Recent Scans Table */}
585
+ <div className="bg-surface-container-lowest border border-outline-variant rounded-xl shadow-sm flex flex-col overflow-hidden">
586
+ <div className="p-lg border-b border-outline-variant flex justify-between items-center">
587
+ <h3 className="font-headline-md text-headline-md text-on-surface tracking-tight">Configured Target Assets</h3>
588
+ <Link className="font-label-md text-label-md text-primary hover:underline" to="/scans/history" style={{ textDecoration: 'none' }}>
589
+ View Full Audit Log
590
+ </Link>
591
+ </div>
592
+
593
+ {recentScans.length === 0 ? (
594
+ <div className="text-center py-2xl text-on-surface-variant font-body-sm">
595
+ No target domains scanned yet. Launch your first website scan under the New Scan tab!
596
+ </div>
597
+ ) : (
598
+ <div className="overflow-x-auto">
599
+ <table className="w-full text-left border-collapse min-w-[800px]">
600
+ <thead>
601
+ <tr className="bg-surface-container-low border-b border-outline-variant">
602
+ {['Status','Target URL','Scan Profile','Date','Rating','Severity','Actions'].map((h, i) => (
603
+ <th
604
+ key={h}
605
+ onClick={() => handleSort(h)}
606
+ 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' : ''}`}
607
+ >
608
+ <div className={`flex items-center gap-xs ${i === 6 ? 'justify-end' : ''}`}>
609
+ {h}
610
+ {(h !== 'Severity' && h !== 'Actions') && (
611
+ <span className={`material-symbols-outlined text-[16px] opacity-0 group-hover:opacity-50 transition-opacity ${sortColumn === h ? 'opacity-100 group-hover:opacity-100 text-primary' : ''}`}>
612
+ {sortColumn === h && sortDirection === 'desc' ? 'arrow_downward' : 'arrow_upward'}
613
+ </span>
614
+ )}
615
+ </div>
616
+ </th>
617
+ ))}
618
+ </tr>
619
+ </thead>
620
+ <tbody className="font-body-sm text-body-sm text-on-surface">
621
+ {getSortedScans().slice(0, 8).map((scan) => {
622
+ let dot = 'bg-primary', statusText = 'Clean', rowBg = '';
623
+ if (scan.status === 'completed') {
624
+ if (scan.security_score < 60) { dot = 'bg-error animate-pulse'; statusText = 'Critical'; rowBg = 'bg-error/5'; }
625
+ else if (scan.security_score < 80) { dot = 'bg-tertiary'; statusText = 'Warning'; }
626
+ else { dot = 'bg-green-500'; statusText = 'Secure'; }
627
+ } else if (scan.status === 'scanning' || scan.status === 'queued') {
628
+ dot = 'bg-yellow-500 animate-pulse'; statusText = 'Scanning';
629
+ } else if (scan.status === 'terminated') {
630
+ dot = 'bg-slate-400'; statusText = 'Terminated';
631
+ } else {
632
+ dot = 'bg-slate-400'; statusText = 'Failed';
633
+ }
634
+ const grade = getRatingGrade(scan.security_score);
635
+
636
+ return (
637
+ <tr key={scan.id} className={`border-b border-outline-variant/50 hover:bg-surface-bright transition-colors ${rowBg}`}>
638
+ <td className="py-md px-lg">
639
+ <div className="flex items-center gap-xs">
640
+ <div className={`w-2.5 h-2.5 rounded-full ${dot}`}></div>
641
+ <span className="font-medium">{statusText}</span>
642
+ </div>
643
+ </td>
644
+ <td className="py-md px-lg font-label-md text-on-surface-variant font-bold max-w-[200px] truncate">{scan.target_url}</td>
645
+ <td className="py-md px-lg text-on-surface-variant">{scan.scan_type} Assessment</td>
646
+ <td className="py-md px-lg text-on-surface-variant font-medium text-xs">
647
+ {scan.started_at ? new Date(scan.started_at).toLocaleString('en-US', {
648
+ day: 'numeric', month: 'short', year: 'numeric', hour: '2-digit', minute: '2-digit'
649
+ }) : 'Pending'}
650
+ </td>
651
+ <td className="py-md px-lg">
652
+ <span className={`font-mono text-[20px] font-extrabold ${ratingColor(grade)}`}>{grade}</span>
653
+ {scan.security_score !== null && <span className="text-on-surface-variant text-xs ml-1 opacity-70">({scan.security_score})</span>}
654
+ </td>
655
+ <td className="py-md px-lg">
656
+ <div className="flex gap-sm flex-wrap">
657
+ {(scan.vulnerabilities_count?.critical > 0) && <span className="bg-red-500/10 text-red-600 px-sm py-[2px] rounded text-xs font-bold border border-red-500/20">{scan.vulnerabilities_count.critical} Crit</span>}
658
+ {(scan.vulnerabilities_count?.high > 0) && <span className="bg-orange-500/10 text-orange-600 px-sm py-[2px] rounded text-xs font-bold border border-orange-500/20">{scan.vulnerabilities_count.high} High</span>}
659
+ {(scan.vulnerabilities_count?.total === 0) && <span className="bg-green-500/10 text-green-600 px-sm py-[2px] rounded text-xs font-bold border border-green-500/20">Clean</span>}
660
+ {scan.status === 'scanning' && <span className="bg-yellow-500/10 text-yellow-600 px-sm py-[2px] rounded text-xs font-bold border border-yellow-500/20 animate-pulse">Scanning...</span>}
661
+ </div>
662
+ </td>
663
+ <td className="py-md px-lg text-right">
664
+ <Link to={`/scans/results?id=${scan.id}`} className="text-primary hover:text-primary-container font-semibold inline-flex items-center gap-xs" style={{ textDecoration: 'none' }}>
665
+ Details <span className="material-symbols-outlined text-[16px]">arrow_forward</span>
666
+ </Link>
667
+ </td>
668
+ </tr>
669
+ );
670
+ })}
671
+ </tbody>
672
+ </table>
673
+ </div>
674
+ )}
675
+ </div>
676
+
677
+ </div>
678
+ );
679
+ };