larxius commited on
Commit
2ddecb9
·
verified ·
1 Parent(s): 9e0fa4c

Update frontend/src/pages/ReportsHistory.jsx

Browse files
Files changed (1) hide show
  1. frontend/src/pages/ReportsHistory.jsx +424 -424
frontend/src/pages/ReportsHistory.jsx CHANGED
@@ -1,424 +1,424 @@
1
- import React, { useState, useEffect } from 'react';
2
- import { useNavigate, Link, useLocation } from 'react-router-dom';
3
- import { useAuth } from '../components/AuthContext';
4
- import { OrganizationSelector } from '../components/OrganizationSelector';
5
-
6
- export const ReportsHistory = () => {
7
- const location = useLocation();
8
- const searchParams = new URLSearchParams(location.search);
9
- const q = searchParams.get('q') || '';
10
-
11
- const [scans, setScans] = useState([]);
12
- const [searchQuery, setSearchQuery] = useState(q);
13
-
14
- useEffect(() => {
15
- setSearchQuery(q);
16
- }, [q]);
17
- const [scanTypeFilter, setScanTypeFilter] = useState('All Types');
18
- const [loading, setLoading] = useState(true);
19
- const [exportingId, setExportingId] = useState(null);
20
- const [copiedId, setCopiedId] = useState(null);
21
- const [error, setError] = useState(null);
22
- const [now, setNow] = useState(new Date());
23
-
24
- const [dateFilter, setDateFilter] = useState('all'); // 'all', '7d', '30d'
25
- const [statusFilter, setStatusFilter] = useState('all'); // 'all', 'completed', 'scanning', 'failed'
26
- const [showFilters, setShowFilters] = useState(false);
27
-
28
- const [sortColumn, setSortColumn] = useState('Date');
29
- const [sortDirection, setSortDirection] = useState('desc');
30
-
31
- const { token } = useAuth();
32
- const navigate = useNavigate();
33
-
34
- // Live clock — updates every second for accurate "time ago" display
35
- useEffect(() => {
36
- const tick = setInterval(() => setNow(new Date()), 1000);
37
- return () => clearInterval(tick);
38
- }, []);
39
-
40
- useEffect(() => {
41
- fetchScanHistory();
42
- // Auto-refresh every 5s so running scans update live
43
- const interval = setInterval(fetchScanHistory, 5000);
44
- return () => clearInterval(interval);
45
- }, [token]);
46
-
47
- const fetchScanHistory = async () => {
48
- try {
49
- const res = await fetch('/api/scans/history', {
50
- headers: { 'Authorization': `Bearer ${token}` }
51
- });
52
- if (res.ok) {
53
- const data = await res.json();
54
- setScans(data.scans || []);
55
- }
56
- } catch (err) {
57
- console.error("Error fetching historical scans", err);
58
- } finally {
59
- setLoading(false);
60
- }
61
- };
62
-
63
- const handlePdfExport = async (e, scanId) => {
64
- e.stopPropagation();
65
- setExportingId(scanId);
66
- try {
67
- const res = await fetch(`/api/reports/${scanId}/pdf`, {
68
- headers: { 'Authorization': `Bearer ${token}` }
69
- });
70
- if (res.ok) {
71
- const blob = await res.blob();
72
- const url = window.URL.createObjectURL(blob);
73
- const a = document.createElement('a');
74
- a.href = url;
75
- a.download = `LarShield_Report_${scanId.substring(0, 8)}.pdf`;
76
- document.body.appendChild(a);
77
- a.click();
78
- a.remove();
79
- window.URL.revokeObjectURL(url);
80
- } else {
81
- setError("Failed to compile PDF Report. Server error.");
82
- }
83
- } catch (err) {
84
- console.error("PDF Export error", err);
85
- } finally {
86
- setExportingId(null);
87
- }
88
- };
89
-
90
- const handleShare = (e, scanId) => {
91
- e.stopPropagation();
92
- const shareUrl = `${window.location.origin}/scans/results?id=${scanId}`;
93
- navigator.clipboard.writeText(shareUrl);
94
- setCopiedId(scanId);
95
- setTimeout(() => setCopiedId(null), 2500);
96
- };
97
-
98
- if (loading) {
99
- return (
100
- <div className="flex items-center justify-center py-2xl font-label-md text-label-md text-on-surface-variant text-left">
101
- <span className="material-symbols-outlined animate-spin mr-sm">sync</span>
102
- Loading Historical Audits...
103
- </div>
104
- );
105
- }
106
-
107
- // Filter & Search logic
108
- const filteredScans = scans.filter((scan) => {
109
- const cleanUrl = scan.target_url.toLowerCase();
110
- const cleanId = scan.id.toLowerCase();
111
- const cleanStatus = scan.status.toLowerCase();
112
- const query = searchQuery.toLowerCase();
113
- const matchesSearch = cleanUrl.includes(query) || cleanId.includes(query) || cleanStatus.includes(query);
114
-
115
- const matchesType = scanTypeFilter === 'All Types' || scan.scan_type === scanTypeFilter || (scanTypeFilter === 'Advanced' && scan.scan_type === 'Standard');
116
- const matchesStatus = statusFilter === 'all' || scan.status === statusFilter;
117
-
118
- let matchesDate = true;
119
- if (dateFilter !== 'all' && scan.started_at) {
120
- const scanDate = new Date(scan.started_at);
121
- const diffDays = (now - scanDate) / (1000 * 60 * 60 * 24);
122
- if (dateFilter === '7d' && diffDays > 7) matchesDate = false;
123
- if (dateFilter === '30d' && diffDays > 30) matchesDate = false;
124
- }
125
-
126
- return matchesSearch && matchesType && matchesStatus && matchesDate;
127
- });
128
-
129
- // Format date in local timezone (IST-aware)
130
- const formatDate = (isoString) => {
131
- if (!isoString) return 'Unknown';
132
- return new Date(isoString).toLocaleString('en-IN', {
133
- day: '2-digit', month: 'short', year: 'numeric',
134
- hour: '2-digit', minute: '2-digit', second: '2-digit',
135
- hour12: true
136
- });
137
- };
138
-
139
- // Live "X ago" helper
140
- const timeAgo = (isoString) => {
141
- if (!isoString) return '';
142
- const diff = Math.floor((now - new Date(isoString)) / 1000);
143
- if (diff < 60) return `${diff}s ago`;
144
- if (diff < 3600) return `${Math.floor(diff / 60)}m ago`;
145
- if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`;
146
- return `${Math.floor(diff / 86400)}d ago`;
147
- };
148
-
149
- const handleSort = (column) => {
150
- if (sortColumn === column) {
151
- setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc');
152
- } else {
153
- setSortColumn(column);
154
- setSortDirection('asc');
155
- }
156
- };
157
-
158
- const getSortedScans = () => {
159
- return [...filteredScans].sort((a, b) => {
160
- let aVal, bVal;
161
- switch (sortColumn) {
162
- case 'Report ID & Date':
163
- case 'Date':
164
- aVal = new Date(a.started_at || 0).getTime(); bVal = new Date(b.started_at || 0).getTime(); break;
165
- case 'Target Host':
166
- aVal = a.target_url || ''; bVal = b.target_url || ''; break;
167
- case 'Engine profile':
168
- aVal = a.scan_type || ''; bVal = b.scan_type || ''; break;
169
- case 'Findings Status':
170
- aVal = a.status || ''; bVal = b.status || ''; break;
171
- default:
172
- return 0;
173
- }
174
- if (aVal < bVal) return sortDirection === 'asc' ? -1 : 1;
175
- if (aVal > bVal) return sortDirection === 'asc' ? 1 : -1;
176
- return 0;
177
- });
178
- };
179
-
180
- return (
181
- <div className="flex flex-col gap-lg text-left w-full">
182
-
183
- {/* Error Toast */}
184
- {error && (
185
- <div className="fixed bottom-10 left-1/2 -translate-x-1/2 z-[100] flex items-center bg-error text-on-error px-md py-sm rounded-lg shadow-xl animate-fade-in gap-sm border border-on-error/20">
186
- <span className="material-symbols-outlined">error</span>
187
- <span className="font-bold text-[14px]">{error}</span>
188
- <button onClick={() => setError(null)} className="ml-md text-on-error/80 hover:text-on-error bg-transparent border-0 cursor-pointer p-0 flex items-center">
189
- <span className="material-symbols-outlined text-[18px]">close</span>
190
- </button>
191
- </div>
192
- )}
193
-
194
- {/* Page Header & Date Actions */}
195
- <div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-md">
196
- <div>
197
- <h2 className="font-display-lg text-display-lg text-on-surface font-bold tracking-tight">Reports &amp; Logs</h2>
198
- <p className="font-body-md text-body-md text-on-surface-variant mt-sm">
199
- View and manage historical security scans and vulnerability logs.
200
- </p>
201
- </div>
202
- <div className="flex items-center gap-sm w-full md:w-auto relative">
203
- <OrganizationSelector />
204
- <select
205
- className="appearance-none flex items-center gap-xs px-md py-sm bg-surface border border-outline-variant rounded-lg text-on-surface font-label-md text-label-md hover:border-primary transition-all cursor-pointer"
206
- value={dateFilter}
207
- onChange={(e) => setDateFilter(e.target.value)}
208
- >
209
- <option value="all">🗓 Date: All Time</option>
210
- <option value="7d">🗓 Last 7 Days</option>
211
- <option value="30d">🗓 Last 30 Days</option>
212
- </select>
213
-
214
- <button
215
- onClick={() => setShowFilters(!showFilters)}
216
- className={`flex items-center gap-xs px-md py-sm bg-surface border ${showFilters ? 'border-primary' : 'border-outline-variant'} rounded-lg text-on-surface font-label-md text-label-md hover:border-primary transition-all cursor-pointer`}
217
- >
218
- <span className="material-symbols-outlined text-[18px]">filter_list</span>
219
- <span>Filter Status</span>
220
- </button>
221
-
222
- {showFilters && (
223
- <div className="absolute top-[110%] right-0 bg-surface border border-outline-variant rounded-lg shadow-lg z-50 flex flex-col min-w-[150px] overflow-hidden">
224
- <button onClick={() => {setStatusFilter('all'); setShowFilters(false)}} className={`text-left px-md py-sm border-b border-outline-variant ${statusFilter === 'all' ? 'bg-primary/10 text-primary' : 'hover:bg-surface-variant text-on-surface'}`}>All Statuses</button>
225
- <button onClick={() => {setStatusFilter('completed'); setShowFilters(false)}} className={`text-left px-md py-sm border-b border-outline-variant ${statusFilter === 'completed' ? 'bg-primary/10 text-primary' : 'hover:bg-surface-variant text-on-surface'}`}>Completed</button>
226
- <button onClick={() => {setStatusFilter('scanning'); setShowFilters(false)}} className={`text-left px-md py-sm border-b border-outline-variant ${statusFilter === 'scanning' ? 'bg-primary/10 text-primary' : 'hover:bg-surface-variant text-on-surface'}`}>Scanning</button>
227
- <button onClick={() => {setStatusFilter('failed'); setShowFilters(false)}} className={`text-left px-md py-sm ${statusFilter === 'failed' ? 'bg-primary/10 text-primary' : 'hover:bg-surface-variant text-on-surface'}`}>Failed</button>
228
- </div>
229
- )}
230
- </div>
231
- </div>
232
-
233
- {/* Search & Toolbar */}
234
- <div className="bg-surface border border-outline-variant rounded-xl p-sm flex flex-col sm:flex-row gap-sm items-center shadow-sm">
235
- <div className="relative flex-grow w-full">
236
- <span className="material-symbols-outlined absolute left-sm top-1/2 -translate-y-1/2 text-on-surface-variant text-[20px]">search</span>
237
- <input
238
- type="text"
239
- className="w-full bg-surface-container-low border border-transparent rounded-lg py-sm pl-xl pr-md text-on-surface font-body-sm text-body-sm placeholder-on-surface-variant focus:outline-none focus:border-primary focus:ring-1 focus:ring-primary transition-all"
240
- placeholder="Search reports by ID, Target, or Status..."
241
- value={searchQuery}
242
- onChange={(e) => setSearchQuery(e.target.value)}
243
- />
244
- </div>
245
- <div className="flex items-center gap-xs w-full sm:w-auto border-t sm:border-t-0 pt-sm sm:pt-0">
246
- <span className="font-label-sm text-label-sm text-on-surface-variant uppercase px-sm font-bold whitespace-nowrap">Scan Profile:</span>
247
- <select
248
- className="bg-surface border border-outline-variant rounded-lg py-sm px-md text-on-surface font-body-sm text-body-sm focus:outline-none focus:border-primary transition-all w-full sm:w-auto cursor-pointer"
249
- value={scanTypeFilter}
250
- onChange={(e) => setScanTypeFilter(e.target.value)}
251
- >
252
- <option value="All Types">All Scan Profiles</option>
253
- <option value="Quick">Quick Scan</option>
254
- <option value="Advanced">Advanced Scan</option>
255
- <option value="Deep">Deep Assessment</option>
256
- </select>
257
- </div>
258
- </div>
259
-
260
- {/* Reports List Canvas Card */}
261
- <div className="bg-surface-container-lowest border border-outline-variant rounded-xl overflow-hidden flex flex-col shadow-sm">
262
-
263
- {/* Table Header */}
264
- <div className="grid grid-cols-12 gap-md px-lg py-sm border-b border-outline-variant bg-surface-container-low items-center hidden md:grid select-none">
265
- <div onClick={() => handleSort('Date')} className="col-span-3 font-label-sm text-label-sm text-on-surface-variant uppercase font-bold cursor-pointer group flex items-center gap-xs">
266
- Report ID &amp; Date
267
- <span className={`material-symbols-outlined text-[16px] opacity-0 group-hover:opacity-50 transition-opacity ${sortColumn === 'Date' ? 'opacity-100 group-hover:opacity-100 text-primary' : ''}`}>
268
- {sortColumn === 'Date' && sortDirection === 'desc' ? 'arrow_downward' : 'arrow_upward'}
269
- </span>
270
- </div>
271
- <div onClick={() => handleSort('Target Host')} className="col-span-3 font-label-sm text-label-sm text-on-surface-variant uppercase font-bold cursor-pointer group flex items-center gap-xs">
272
- Target Host
273
- <span className={`material-symbols-outlined text-[16px] opacity-0 group-hover:opacity-50 transition-opacity ${sortColumn === 'Target Host' ? 'opacity-100 group-hover:opacity-100 text-primary' : ''}`}>
274
- {sortColumn === 'Target Host' && sortDirection === 'desc' ? 'arrow_downward' : 'arrow_upward'}
275
- </span>
276
- </div>
277
- <div onClick={() => handleSort('Engine profile')} className="col-span-2 font-label-sm text-label-sm text-on-surface-variant uppercase font-bold cursor-pointer group flex items-center gap-xs">
278
- Engine profile
279
- <span className={`material-symbols-outlined text-[16px] opacity-0 group-hover:opacity-50 transition-opacity ${sortColumn === 'Engine profile' ? 'opacity-100 group-hover:opacity-100 text-primary' : ''}`}>
280
- {sortColumn === 'Engine profile' && sortDirection === 'desc' ? 'arrow_downward' : 'arrow_upward'}
281
- </span>
282
- </div>
283
- <div onClick={() => handleSort('Findings Status')} className="col-span-2 font-label-sm text-label-sm text-on-surface-variant uppercase font-bold cursor-pointer group flex items-center gap-xs">
284
- Findings Status
285
- <span className={`material-symbols-outlined text-[16px] opacity-0 group-hover:opacity-50 transition-opacity ${sortColumn === 'Findings Status' ? 'opacity-100 group-hover:opacity-100 text-primary' : ''}`}>
286
- {sortColumn === 'Findings Status' && sortDirection === 'desc' ? 'arrow_downward' : 'arrow_upward'}
287
- </span>
288
- </div>
289
- <div className="col-span-2 font-label-sm text-label-sm text-on-surface-variant uppercase text-right font-bold">Actions</div>
290
- </div>
291
-
292
- {/* List Items */}
293
- <div className="flex flex-col divide-y divide-outline-variant">
294
- {filteredScans.length === 0 ? (
295
- <div className="text-center py-2xl text-on-surface-variant font-body-sm">
296
- No historical security audits match your search query.
297
- </div>
298
- ) : (
299
- getSortedScans().map((s) => {
300
- const hasCritical = s.vulnerabilities_count?.critical > 0;
301
- const hasHigh = s.vulnerabilities_count?.high > 0;
302
-
303
- return (
304
- <div
305
- key={s.id}
306
- onClick={() => navigate(`/scans/results?id=${s.id}`)}
307
- className="grid grid-cols-1 md:grid-cols-12 gap-md px-lg py-md hover:bg-surface-container-low transition-colors items-center group cursor-pointer"
308
- >
309
- {/* ID & Date */}
310
- <div className="col-span-1 md:col-span-3 flex flex-col text-left">
311
- <span className="font-label-md text-label-md text-primary font-bold group-hover:underline">
312
- REP-{s.id.substring(0, 8).toUpperCase()}
313
- </span>
314
- <span className="font-body-sm text-body-sm text-on-surface-variant flex items-center gap-xs mt-xs">
315
- <span className="material-symbols-outlined text-[14px]">event</span>
316
- {formatDate(s.started_at)}
317
- </span>
318
- <span className="font-body-sm text-body-sm text-on-surface-variant/60 text-[11px] ml-[18px]">
319
- {timeAgo(s.started_at)}
320
- </span>
321
- </div>
322
-
323
- {/* Target Host */}
324
- <div className="col-span-1 md:col-span-3 text-left">
325
- <span className="font-body-md text-body-md text-on-surface font-semibold truncate block">
326
- {s.target_url.replace("https://", "").replace("http://", "")}
327
- </span>
328
- </div>
329
-
330
- {/* Type */}
331
- <div className="col-span-1 md:col-span-2 text-left">
332
- <span className="inline-flex items-center px-sm py-[2px] rounded bg-surface border border-outline-variant font-label-sm text-label-sm text-on-surface-variant font-bold uppercase tracking-wider">
333
- {s.scan_type} Scan
334
- </span>
335
- </div>
336
-
337
- {/* Status / Findings */}
338
- <div className="col-span-1 md:col-span-2 text-left">
339
- {s.status === 'completed' ? (
340
- (() => {
341
- const counts = s.vulnerabilities_count || {};
342
- const total = (counts.critical || 0) + (counts.high || 0) + (counts.medium || 0) + (counts.low || 0) + (counts.info || 0);
343
- if (total === 0) {
344
- return (
345
- <div className="flex items-center gap-sm">
346
- <div className="w-2.5 h-2.5 rounded-full bg-green-500"></div>
347
- <span className="font-label-md text-label-md font-bold text-green-600">Clean / Safe</span>
348
- </div>
349
- );
350
- }
351
- return (
352
- <div className="grid grid-cols-2 gap-xs w-fit">
353
- {counts.critical > 0 && <span className="text-[11px] font-bold px-1.5 py-0.5 rounded bg-error/10 text-error border border-error/20 text-center" title="Critical">{counts.critical} Crit</span>}
354
- {counts.high > 0 && <span className="text-[11px] font-bold px-1.5 py-0.5 rounded bg-orange-500/10 text-orange-600 border border-orange-500/20 text-center" title="High">{counts.high} High</span>}
355
- {counts.medium > 0 && <span className="text-[11px] font-bold px-1.5 py-0.5 rounded bg-yellow-500/10 text-yellow-600 border border-yellow-500/20 text-center" title="Medium">{counts.medium} Med</span>}
356
- {counts.low > 0 && <span className="text-[11px] font-bold px-1.5 py-0.5 rounded bg-blue-500/10 text-blue-600 border border-blue-500/20 text-center" title="Low">{counts.low} Low</span>}
357
- {counts.info > 0 && <span className="text-[11px] font-bold px-1.5 py-0.5 rounded bg-slate-500/10 text-slate-600 border border-slate-500/20 text-center" title="Info">{counts.info} Info</span>}
358
- </div>
359
- );
360
- })()
361
- ) : s.status === 'scanning' || s.status === 'queued' ? (
362
- <div className="flex items-center gap-sm">
363
- <div className="w-2.5 h-2.5 rounded-full bg-yellow-500 animate-pulse"></div>
364
- <span className="font-label-md text-label-md text-yellow-600 font-bold uppercase">
365
- Running Audit
366
- </span>
367
- </div>
368
- ) : (
369
- <div className="flex items-center gap-sm">
370
- <div className="w-2.5 h-2.5 rounded-full bg-slate-400"></div>
371
- <span className="font-label-md text-label-md text-slate-500 font-bold">
372
- Failed Session
373
- </span>
374
- </div>
375
- )}
376
- </div>
377
-
378
- {/* Actions */}
379
- <div className="col-span-1 md:col-span-2 flex items-center justify-end gap-sm">
380
- <button
381
- onClick={(e) => handlePdfExport(e, s.id)}
382
- disabled={exportingId === s.id}
383
- className="p-xs text-on-surface-variant hover:text-primary transition-colors border-0 bg-transparent cursor-pointer flex items-center justify-center"
384
- title="Download PDF"
385
- >
386
- <span className="material-symbols-outlined text-[20px]">
387
- {exportingId === s.id ? 'sync' : 'picture_as_pdf'}
388
- </span>
389
- </button>
390
- <button
391
- onClick={(e) => handleShare(e, s.id)}
392
- className="p-xs text-on-surface-variant hover:text-primary transition-colors border-0 bg-transparent cursor-pointer flex items-center justify-center relative"
393
- title={copiedId === s.id ? 'Link Copied!' : 'Share Link'}
394
- >
395
- <span className="material-symbols-outlined text-[20px]">
396
- {copiedId === s.id ? 'check_circle' : 'share'}
397
- </span>
398
- </button>
399
- </div>
400
- </div>
401
- );
402
- })
403
- )}
404
- </div>
405
-
406
- {/* Pagination footer */}
407
- <div className="px-lg py-md border-t border-outline-variant bg-surface-container-low flex justify-between items-center">
408
- <span className="font-body-sm text-body-sm text-on-surface-variant">
409
- Showing 1-{filteredScans.length} of {filteredScans.length} completed logs
410
- </span>
411
- <div className="flex items-center gap-sm">
412
- <button className="p-[4px] rounded hover:bg-surface-variant text-on-surface-variant transition-colors disabled:opacity-50 border-0 bg-transparent cursor-pointer flex items-center justify-center" disabled>
413
- <span className="material-symbols-outlined">chevron_left</span>
414
- </button>
415
- <button className="p-[4px] rounded hover:bg-surface-variant text-on-surface-variant transition-colors disabled:opacity-50 border-0 bg-transparent cursor-pointer flex items-center justify-center" disabled>
416
- <span className="material-symbols-outlined">chevron_right</span>
417
- </button>
418
- </div>
419
- </div>
420
-
421
- </div>
422
- </div>
423
- );
424
- };
 
1
+ import React, { useState, useEffect } from 'react';
2
+ import { useNavigate, Link, useLocation } from 'react-router-dom';
3
+ import { useAuth } from '../components/AuthContext';
4
+ import { OrganizationSelector } from '../components/OrganizationSelector';
5
+
6
+ export const ReportsHistory = () => {
7
+ const location = useLocation();
8
+ const searchParams = new URLSearchParams(location.search);
9
+ const q = searchParams.get('q') || '';
10
+
11
+ const [scans, setScans] = useState([]);
12
+ const [searchQuery, setSearchQuery] = useState(q);
13
+
14
+ useEffect(() => {
15
+ setSearchQuery(q);
16
+ }, [q]);
17
+ const [scanTypeFilter, setScanTypeFilter] = useState('All Types');
18
+ const [loading, setLoading] = useState(true);
19
+ const [exportingId, setExportingId] = useState(null);
20
+ const [copiedId, setCopiedId] = useState(null);
21
+ const [error, setError] = useState(null);
22
+ const [now, setNow] = useState(new Date());
23
+
24
+ const [dateFilter, setDateFilter] = useState('all'); // 'all', '7d', '30d'
25
+ const [statusFilter, setStatusFilter] = useState('all'); // 'all', 'completed', 'scanning', 'failed'
26
+ const [showFilters, setShowFilters] = useState(false);
27
+
28
+ const [sortColumn, setSortColumn] = useState('Date');
29
+ const [sortDirection, setSortDirection] = useState('desc');
30
+
31
+ const { token } = useAuth();
32
+ const navigate = useNavigate();
33
+
34
+ // Live clock — updates every second for accurate "time ago" display
35
+ useEffect(() => {
36
+ const tick = setInterval(() => setNow(new Date()), 1000);
37
+ return () => clearInterval(tick);
38
+ }, []);
39
+
40
+ useEffect(() => {
41
+ fetchScanHistory();
42
+ // Auto-refresh every 5s so running scans update live
43
+ const interval = setInterval(fetchScanHistory, 5000);
44
+ return () => clearInterval(interval);
45
+ }, [token]);
46
+
47
+ const fetchScanHistory = async () => {
48
+ try {
49
+ const res = await fetch('/api/scans/history', {
50
+ headers: { 'Authorization': `Bearer ${token}` }
51
+ });
52
+ if (res.ok) {
53
+ const data = await res.json();
54
+ setScans(data.scans || []);
55
+ }
56
+ } catch (err) {
57
+ console.error("Error fetching historical scans", err);
58
+ } finally {
59
+ setLoading(false);
60
+ }
61
+ };
62
+
63
+ const handlePdfExport = async (e, scanId) => {
64
+ e.stopPropagation();
65
+ setExportingId(scanId);
66
+ try {
67
+ const res = await fetch(`/api/reports/${scanId}/pdf`, {
68
+ headers: { 'Authorization': `Bearer ${token}` }
69
+ });
70
+ if (res.ok) {
71
+ const blob = await res.blob();
72
+ const url = window.URL.createObjectURL(blob);
73
+ const a = document.createElement('a');
74
+ a.href = url;
75
+ a.download = `LarShield_Report_${scanId.substring(0, 8)}.pdf`;
76
+ document.body.appendChild(a);
77
+ a.click();
78
+ a.remove();
79
+ window.URL.revokeObjectURL(url);
80
+ } else {
81
+ setError("Failed to compile PDF Report. Server error.");
82
+ }
83
+ } catch (err) {
84
+ console.error("PDF Export error", err);
85
+ } finally {
86
+ setExportingId(null);
87
+ }
88
+ };
89
+
90
+ const handleShare = (e, scanId) => {
91
+ e.stopPropagation();
92
+ const shareUrl = `${window.location.origin}/api/reports/${scanId}/public-pdf`;
93
+ navigator.clipboard.writeText(shareUrl);
94
+ setCopiedId(scanId);
95
+ setTimeout(() => setCopiedId(null), 2500);
96
+ };
97
+
98
+ if (loading) {
99
+ return (
100
+ <div className="flex items-center justify-center py-2xl font-label-md text-label-md text-on-surface-variant text-left">
101
+ <span className="material-symbols-outlined animate-spin mr-sm">sync</span>
102
+ Loading Historical Audits...
103
+ </div>
104
+ );
105
+ }
106
+
107
+ // Filter & Search logic
108
+ const filteredScans = scans.filter((scan) => {
109
+ const cleanUrl = scan.target_url.toLowerCase();
110
+ const cleanId = scan.id.toLowerCase();
111
+ const cleanStatus = scan.status.toLowerCase();
112
+ const query = searchQuery.toLowerCase();
113
+ const matchesSearch = cleanUrl.includes(query) || cleanId.includes(query) || cleanStatus.includes(query);
114
+
115
+ const matchesType = scanTypeFilter === 'All Types' || scan.scan_type === scanTypeFilter || (scanTypeFilter === 'Advanced' && scan.scan_type === 'Standard');
116
+ const matchesStatus = statusFilter === 'all' || scan.status === statusFilter;
117
+
118
+ let matchesDate = true;
119
+ if (dateFilter !== 'all' && scan.started_at) {
120
+ const scanDate = new Date(scan.started_at);
121
+ const diffDays = (now - scanDate) / (1000 * 60 * 60 * 24);
122
+ if (dateFilter === '7d' && diffDays > 7) matchesDate = false;
123
+ if (dateFilter === '30d' && diffDays > 30) matchesDate = false;
124
+ }
125
+
126
+ return matchesSearch && matchesType && matchesStatus && matchesDate;
127
+ });
128
+
129
+ // Format date in local timezone (IST-aware)
130
+ const formatDate = (isoString) => {
131
+ if (!isoString) return 'Unknown';
132
+ return new Date(isoString).toLocaleString('en-IN', {
133
+ day: '2-digit', month: 'short', year: 'numeric',
134
+ hour: '2-digit', minute: '2-digit', second: '2-digit',
135
+ hour12: true
136
+ });
137
+ };
138
+
139
+ // Live "X ago" helper
140
+ const timeAgo = (isoString) => {
141
+ if (!isoString) return '';
142
+ const diff = Math.floor((now - new Date(isoString)) / 1000);
143
+ if (diff < 60) return `${diff}s ago`;
144
+ if (diff < 3600) return `${Math.floor(diff / 60)}m ago`;
145
+ if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`;
146
+ return `${Math.floor(diff / 86400)}d ago`;
147
+ };
148
+
149
+ const handleSort = (column) => {
150
+ if (sortColumn === column) {
151
+ setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc');
152
+ } else {
153
+ setSortColumn(column);
154
+ setSortDirection('asc');
155
+ }
156
+ };
157
+
158
+ const getSortedScans = () => {
159
+ return [...filteredScans].sort((a, b) => {
160
+ let aVal, bVal;
161
+ switch (sortColumn) {
162
+ case 'Report ID & Date':
163
+ case 'Date':
164
+ aVal = new Date(a.started_at || 0).getTime(); bVal = new Date(b.started_at || 0).getTime(); break;
165
+ case 'Target Host':
166
+ aVal = a.target_url || ''; bVal = b.target_url || ''; break;
167
+ case 'Engine profile':
168
+ aVal = a.scan_type || ''; bVal = b.scan_type || ''; break;
169
+ case 'Findings Status':
170
+ aVal = a.status || ''; bVal = b.status || ''; break;
171
+ default:
172
+ return 0;
173
+ }
174
+ if (aVal < bVal) return sortDirection === 'asc' ? -1 : 1;
175
+ if (aVal > bVal) return sortDirection === 'asc' ? 1 : -1;
176
+ return 0;
177
+ });
178
+ };
179
+
180
+ return (
181
+ <div className="flex flex-col gap-lg text-left w-full">
182
+
183
+ {/* Error Toast */}
184
+ {error && (
185
+ <div className="fixed bottom-10 left-1/2 -translate-x-1/2 z-[100] flex items-center bg-error text-on-error px-md py-sm rounded-lg shadow-xl animate-fade-in gap-sm border border-on-error/20">
186
+ <span className="material-symbols-outlined">error</span>
187
+ <span className="font-bold text-[14px]">{error}</span>
188
+ <button onClick={() => setError(null)} className="ml-md text-on-error/80 hover:text-on-error bg-transparent border-0 cursor-pointer p-0 flex items-center">
189
+ <span className="material-symbols-outlined text-[18px]">close</span>
190
+ </button>
191
+ </div>
192
+ )}
193
+
194
+ {/* Page Header & Date Actions */}
195
+ <div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-md">
196
+ <div>
197
+ <h2 className="font-display-lg text-display-lg text-on-surface font-bold tracking-tight">Reports &amp; Logs</h2>
198
+ <p className="font-body-md text-body-md text-on-surface-variant mt-sm">
199
+ View and manage historical security scans and vulnerability logs.
200
+ </p>
201
+ </div>
202
+ <div className="flex items-center gap-sm w-full md:w-auto relative">
203
+ <OrganizationSelector />
204
+ <select
205
+ className="appearance-none flex items-center gap-xs px-md py-sm bg-surface border border-outline-variant rounded-lg text-on-surface font-label-md text-label-md hover:border-primary transition-all cursor-pointer"
206
+ value={dateFilter}
207
+ onChange={(e) => setDateFilter(e.target.value)}
208
+ >
209
+ <option value="all">🗓 Date: All Time</option>
210
+ <option value="7d">🗓 Last 7 Days</option>
211
+ <option value="30d">🗓 Last 30 Days</option>
212
+ </select>
213
+
214
+ <button
215
+ onClick={() => setShowFilters(!showFilters)}
216
+ className={`flex items-center gap-xs px-md py-sm bg-surface border ${showFilters ? 'border-primary' : 'border-outline-variant'} rounded-lg text-on-surface font-label-md text-label-md hover:border-primary transition-all cursor-pointer`}
217
+ >
218
+ <span className="material-symbols-outlined text-[18px]">filter_list</span>
219
+ <span>Filter Status</span>
220
+ </button>
221
+
222
+ {showFilters && (
223
+ <div className="absolute top-[110%] right-0 bg-surface border border-outline-variant rounded-lg shadow-lg z-50 flex flex-col min-w-[150px] overflow-hidden">
224
+ <button onClick={() => {setStatusFilter('all'); setShowFilters(false)}} className={`text-left px-md py-sm border-b border-outline-variant ${statusFilter === 'all' ? 'bg-primary/10 text-primary' : 'hover:bg-surface-variant text-on-surface'}`}>All Statuses</button>
225
+ <button onClick={() => {setStatusFilter('completed'); setShowFilters(false)}} className={`text-left px-md py-sm border-b border-outline-variant ${statusFilter === 'completed' ? 'bg-primary/10 text-primary' : 'hover:bg-surface-variant text-on-surface'}`}>Completed</button>
226
+ <button onClick={() => {setStatusFilter('scanning'); setShowFilters(false)}} className={`text-left px-md py-sm border-b border-outline-variant ${statusFilter === 'scanning' ? 'bg-primary/10 text-primary' : 'hover:bg-surface-variant text-on-surface'}`}>Scanning</button>
227
+ <button onClick={() => {setStatusFilter('failed'); setShowFilters(false)}} className={`text-left px-md py-sm ${statusFilter === 'failed' ? 'bg-primary/10 text-primary' : 'hover:bg-surface-variant text-on-surface'}`}>Failed</button>
228
+ </div>
229
+ )}
230
+ </div>
231
+ </div>
232
+
233
+ {/* Search & Toolbar */}
234
+ <div className="bg-surface border border-outline-variant rounded-xl p-sm flex flex-col sm:flex-row gap-sm items-center shadow-sm">
235
+ <div className="relative flex-grow w-full">
236
+ <span className="material-symbols-outlined absolute left-sm top-1/2 -translate-y-1/2 text-on-surface-variant text-[20px]">search</span>
237
+ <input
238
+ type="text"
239
+ className="w-full bg-surface-container-low border border-transparent rounded-lg py-sm pl-xl pr-md text-on-surface font-body-sm text-body-sm placeholder-on-surface-variant focus:outline-none focus:border-primary focus:ring-1 focus:ring-primary transition-all"
240
+ placeholder="Search reports by ID, Target, or Status..."
241
+ value={searchQuery}
242
+ onChange={(e) => setSearchQuery(e.target.value)}
243
+ />
244
+ </div>
245
+ <div className="flex items-center gap-xs w-full sm:w-auto border-t sm:border-t-0 pt-sm sm:pt-0">
246
+ <span className="font-label-sm text-label-sm text-on-surface-variant uppercase px-sm font-bold whitespace-nowrap">Scan Profile:</span>
247
+ <select
248
+ className="bg-surface border border-outline-variant rounded-lg py-sm px-md text-on-surface font-body-sm text-body-sm focus:outline-none focus:border-primary transition-all w-full sm:w-auto cursor-pointer"
249
+ value={scanTypeFilter}
250
+ onChange={(e) => setScanTypeFilter(e.target.value)}
251
+ >
252
+ <option value="All Types">All Scan Profiles</option>
253
+ <option value="Quick">Quick Scan</option>
254
+ <option value="Advanced">Advanced Scan</option>
255
+ <option value="Deep">Deep Assessment</option>
256
+ </select>
257
+ </div>
258
+ </div>
259
+
260
+ {/* Reports List Canvas Card */}
261
+ <div className="bg-surface-container-lowest border border-outline-variant rounded-xl overflow-hidden flex flex-col shadow-sm">
262
+
263
+ {/* Table Header */}
264
+ <div className="grid grid-cols-12 gap-md px-lg py-sm border-b border-outline-variant bg-surface-container-low items-center hidden md:grid select-none">
265
+ <div onClick={() => handleSort('Date')} className="col-span-3 font-label-sm text-label-sm text-on-surface-variant uppercase font-bold cursor-pointer group flex items-center gap-xs">
266
+ Report ID &amp; Date
267
+ <span className={`material-symbols-outlined text-[16px] opacity-0 group-hover:opacity-50 transition-opacity ${sortColumn === 'Date' ? 'opacity-100 group-hover:opacity-100 text-primary' : ''}`}>
268
+ {sortColumn === 'Date' && sortDirection === 'desc' ? 'arrow_downward' : 'arrow_upward'}
269
+ </span>
270
+ </div>
271
+ <div onClick={() => handleSort('Target Host')} className="col-span-3 font-label-sm text-label-sm text-on-surface-variant uppercase font-bold cursor-pointer group flex items-center gap-xs">
272
+ Target Host
273
+ <span className={`material-symbols-outlined text-[16px] opacity-0 group-hover:opacity-50 transition-opacity ${sortColumn === 'Target Host' ? 'opacity-100 group-hover:opacity-100 text-primary' : ''}`}>
274
+ {sortColumn === 'Target Host' && sortDirection === 'desc' ? 'arrow_downward' : 'arrow_upward'}
275
+ </span>
276
+ </div>
277
+ <div onClick={() => handleSort('Engine profile')} className="col-span-2 font-label-sm text-label-sm text-on-surface-variant uppercase font-bold cursor-pointer group flex items-center gap-xs">
278
+ Engine profile
279
+ <span className={`material-symbols-outlined text-[16px] opacity-0 group-hover:opacity-50 transition-opacity ${sortColumn === 'Engine profile' ? 'opacity-100 group-hover:opacity-100 text-primary' : ''}`}>
280
+ {sortColumn === 'Engine profile' && sortDirection === 'desc' ? 'arrow_downward' : 'arrow_upward'}
281
+ </span>
282
+ </div>
283
+ <div onClick={() => handleSort('Findings Status')} className="col-span-2 font-label-sm text-label-sm text-on-surface-variant uppercase font-bold cursor-pointer group flex items-center gap-xs">
284
+ Findings Status
285
+ <span className={`material-symbols-outlined text-[16px] opacity-0 group-hover:opacity-50 transition-opacity ${sortColumn === 'Findings Status' ? 'opacity-100 group-hover:opacity-100 text-primary' : ''}`}>
286
+ {sortColumn === 'Findings Status' && sortDirection === 'desc' ? 'arrow_downward' : 'arrow_upward'}
287
+ </span>
288
+ </div>
289
+ <div className="col-span-2 font-label-sm text-label-sm text-on-surface-variant uppercase text-right font-bold">Actions</div>
290
+ </div>
291
+
292
+ {/* List Items */}
293
+ <div className="flex flex-col divide-y divide-outline-variant">
294
+ {filteredScans.length === 0 ? (
295
+ <div className="text-center py-2xl text-on-surface-variant font-body-sm">
296
+ No historical security audits match your search query.
297
+ </div>
298
+ ) : (
299
+ getSortedScans().map((s) => {
300
+ const hasCritical = s.vulnerabilities_count?.critical > 0;
301
+ const hasHigh = s.vulnerabilities_count?.high > 0;
302
+
303
+ return (
304
+ <div
305
+ key={s.id}
306
+ onClick={() => navigate(`/scans/results?id=${s.id}`)}
307
+ className="grid grid-cols-1 md:grid-cols-12 gap-md px-lg py-md hover:bg-surface-container-low transition-colors items-center group cursor-pointer"
308
+ >
309
+ {/* ID & Date */}
310
+ <div className="col-span-1 md:col-span-3 flex flex-col text-left">
311
+ <span className="font-label-md text-label-md text-primary font-bold group-hover:underline">
312
+ REP-{s.id.substring(0, 8).toUpperCase()}
313
+ </span>
314
+ <span className="font-body-sm text-body-sm text-on-surface-variant flex items-center gap-xs mt-xs">
315
+ <span className="material-symbols-outlined text-[14px]">event</span>
316
+ {formatDate(s.started_at)}
317
+ </span>
318
+ <span className="font-body-sm text-body-sm text-on-surface-variant/60 text-[11px] ml-[18px]">
319
+ {timeAgo(s.started_at)}
320
+ </span>
321
+ </div>
322
+
323
+ {/* Target Host */}
324
+ <div className="col-span-1 md:col-span-3 text-left">
325
+ <span className="font-body-md text-body-md text-on-surface font-semibold truncate block">
326
+ {s.target_url.replace("https://", "").replace("http://", "")}
327
+ </span>
328
+ </div>
329
+
330
+ {/* Type */}
331
+ <div className="col-span-1 md:col-span-2 text-left">
332
+ <span className="inline-flex items-center px-sm py-[2px] rounded bg-surface border border-outline-variant font-label-sm text-label-sm text-on-surface-variant font-bold uppercase tracking-wider">
333
+ {s.scan_type} Scan
334
+ </span>
335
+ </div>
336
+
337
+ {/* Status / Findings */}
338
+ <div className="col-span-1 md:col-span-2 text-left">
339
+ {s.status === 'completed' ? (
340
+ (() => {
341
+ const counts = s.vulnerabilities_count || {};
342
+ const total = (counts.critical || 0) + (counts.high || 0) + (counts.medium || 0) + (counts.low || 0) + (counts.info || 0);
343
+ if (total === 0) {
344
+ return (
345
+ <div className="flex items-center gap-sm">
346
+ <div className="w-2.5 h-2.5 rounded-full bg-green-500"></div>
347
+ <span className="font-label-md text-label-md font-bold text-green-600">Clean / Safe</span>
348
+ </div>
349
+ );
350
+ }
351
+ return (
352
+ <div className="grid grid-cols-2 gap-xs w-fit">
353
+ {counts.critical > 0 && <span className="text-[11px] font-bold px-1.5 py-0.5 rounded bg-error/10 text-error border border-error/20 text-center" title="Critical">{counts.critical} Crit</span>}
354
+ {counts.high > 0 && <span className="text-[11px] font-bold px-1.5 py-0.5 rounded bg-orange-500/10 text-orange-600 border border-orange-500/20 text-center" title="High">{counts.high} High</span>}
355
+ {counts.medium > 0 && <span className="text-[11px] font-bold px-1.5 py-0.5 rounded bg-yellow-500/10 text-yellow-600 border border-yellow-500/20 text-center" title="Medium">{counts.medium} Med</span>}
356
+ {counts.low > 0 && <span className="text-[11px] font-bold px-1.5 py-0.5 rounded bg-blue-500/10 text-blue-600 border border-blue-500/20 text-center" title="Low">{counts.low} Low</span>}
357
+ {counts.info > 0 && <span className="text-[11px] font-bold px-1.5 py-0.5 rounded bg-slate-500/10 text-slate-600 border border-slate-500/20 text-center" title="Info">{counts.info} Info</span>}
358
+ </div>
359
+ );
360
+ })()
361
+ ) : s.status === 'scanning' || s.status === 'queued' ? (
362
+ <div className="flex items-center gap-sm">
363
+ <div className="w-2.5 h-2.5 rounded-full bg-yellow-500 animate-pulse"></div>
364
+ <span className="font-label-md text-label-md text-yellow-600 font-bold uppercase">
365
+ Running Audit
366
+ </span>
367
+ </div>
368
+ ) : (
369
+ <div className="flex items-center gap-sm">
370
+ <div className="w-2.5 h-2.5 rounded-full bg-slate-400"></div>
371
+ <span className="font-label-md text-label-md text-slate-500 font-bold">
372
+ Failed Session
373
+ </span>
374
+ </div>
375
+ )}
376
+ </div>
377
+
378
+ {/* Actions */}
379
+ <div className="col-span-1 md:col-span-2 flex items-center justify-end gap-sm">
380
+ <button
381
+ onClick={(e) => handlePdfExport(e, s.id)}
382
+ disabled={exportingId === s.id}
383
+ className="p-xs text-on-surface-variant hover:text-primary transition-colors border-0 bg-transparent cursor-pointer flex items-center justify-center"
384
+ title="Download PDF"
385
+ >
386
+ <span className="material-symbols-outlined text-[20px]">
387
+ {exportingId === s.id ? 'sync' : 'picture_as_pdf'}
388
+ </span>
389
+ </button>
390
+ <button
391
+ onClick={(e) => handleShare(e, s.id)}
392
+ className="p-xs text-on-surface-variant hover:text-primary transition-colors border-0 bg-transparent cursor-pointer flex items-center justify-center relative"
393
+ title={copiedId === s.id ? 'Link Copied!' : 'Share Link'}
394
+ >
395
+ <span className="material-symbols-outlined text-[20px]">
396
+ {copiedId === s.id ? 'check_circle' : 'share'}
397
+ </span>
398
+ </button>
399
+ </div>
400
+ </div>
401
+ );
402
+ })
403
+ )}
404
+ </div>
405
+
406
+ {/* Pagination footer */}
407
+ <div className="px-lg py-md border-t border-outline-variant bg-surface-container-low flex justify-between items-center">
408
+ <span className="font-body-sm text-body-sm text-on-surface-variant">
409
+ Showing 1-{filteredScans.length} of {filteredScans.length} completed logs
410
+ </span>
411
+ <div className="flex items-center gap-sm">
412
+ <button className="p-[4px] rounded hover:bg-surface-variant text-on-surface-variant transition-colors disabled:opacity-50 border-0 bg-transparent cursor-pointer flex items-center justify-center" disabled>
413
+ <span className="material-symbols-outlined">chevron_left</span>
414
+ </button>
415
+ <button className="p-[4px] rounded hover:bg-surface-variant text-on-surface-variant transition-colors disabled:opacity-50 border-0 bg-transparent cursor-pointer flex items-center justify-center" disabled>
416
+ <span className="material-symbols-outlined">chevron_right</span>
417
+ </button>
418
+ </div>
419
+ </div>
420
+
421
+ </div>
422
+ </div>
423
+ );
424
+ };