import React, { useState, useEffect } from 'react'; import { useNavigate, Link, useLocation } from 'react-router-dom'; import { useAuth } from '../components/AuthContext'; import { OrganizationSelector } from '../components/OrganizationSelector'; export const ReportsHistory = () => { const location = useLocation(); const searchParams = new URLSearchParams(location.search); const q = searchParams.get('q') || ''; const [scans, setScans] = useState([]); const [searchQuery, setSearchQuery] = useState(q); useEffect(() => { setSearchQuery(q); }, [q]); const [scanTypeFilter, setScanTypeFilter] = useState('All Types'); const [loading, setLoading] = useState(true); const [exportingId, setExportingId] = useState(null); const [copiedId, setCopiedId] = useState(null); const [error, setError] = useState(null); const [now, setNow] = useState(new Date()); const [dateFilter, setDateFilter] = useState('all'); // 'all', '7d', '30d' const [statusFilter, setStatusFilter] = useState('all'); // 'all', 'completed', 'scanning', 'failed' const [showFilters, setShowFilters] = useState(false); const [sortColumn, setSortColumn] = useState('Date'); const [sortDirection, setSortDirection] = useState('desc'); const [currentPage, setCurrentPage] = useState(1); const [itemsPerPage, setItemsPerPage] = useState(15); useEffect(() => { setCurrentPage(1); }, [searchQuery, scanTypeFilter, dateFilter, statusFilter]); const { token } = useAuth(); const navigate = useNavigate(); // Live clock — updates every second for accurate "time ago" display useEffect(() => { const tick = setInterval(() => setNow(new Date()), 1000); return () => clearInterval(tick); }, []); useEffect(() => { fetchScanHistory(); // Auto-refresh every 5s so running scans update live const interval = setInterval(fetchScanHistory, 5000); return () => clearInterval(interval); }, [token]); const fetchScanHistory = async () => { try { const res = await fetch('/api/scans/history?limit=100', { headers: { 'Authorization': `Bearer ${token}` } }); if (res.ok) { const data = await res.json(); setScans(data.scans || []); } } catch (err) { console.error("Error fetching historical scans", err); } finally { setLoading(false); } }; const handlePdfExport = async (e, scanId) => { e.stopPropagation(); setExportingId(scanId); try { const res = await fetch(`/api/reports/${scanId}/pdf`, { headers: { 'Authorization': `Bearer ${token}` } }); if (res.ok) { const blob = await res.blob(); const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; let filename = ''; const disposition = res.headers.get('Content-Disposition'); if (disposition && disposition.includes('filename=')) { const match = disposition.match(/filename="?([^";]+)"?/); if (match && match[1]) { filename = match[1]; } } if (!filename) { const scanItem = scans.find(s => s.id === scanId); const orgName = scanItem?.org_name || scanItem?.organization_name || 'Global'; const cleanOrg = orgName.replace(/[^\w]/g, '') || 'Organization'; const dateObj = new Date(scanItem?.completed_at || scanItem?.started_at || Date.now()); const day = String(dateObj.getDate()).padStart(2, '0'); const month = String(dateObj.getMonth() + 1).padStart(2, '0'); const year = dateObj.getFullYear(); filename = `LarShield_${cleanOrg}_Report_${day}${month}${year}.pdf`; } a.download = filename; document.body.appendChild(a); a.click(); a.remove(); setTimeout(() => window.URL.revokeObjectURL(url), 100); } else { setError("Failed to compile PDF Report. Server error."); } } catch (err) { console.error("PDF Export error", err); } finally { setExportingId(null); } }; const handleShare = (e, scanId) => { e.stopPropagation(); const shareUrl = `${window.location.origin}/api/reports/${scanId}/public-pdf`; navigator.clipboard.writeText(shareUrl); setCopiedId(scanId); setTimeout(() => setCopiedId(null), 2500); }; if (loading) { return (
sync Loading Historical Audits...
); } // Filter & Search logic const filteredScans = scans.filter((scan) => { const cleanUrl = scan.target_url.toLowerCase(); const cleanId = scan.id.toLowerCase(); const cleanStatus = scan.status.toLowerCase(); const query = searchQuery.toLowerCase(); const matchesSearch = cleanUrl.includes(query) || cleanId.includes(query) || cleanStatus.includes(query); const matchesType = scanTypeFilter === 'All Types' || scan.scan_type === scanTypeFilter || (scanTypeFilter === 'Advanced' && scan.scan_type === 'Standard'); const matchesStatus = statusFilter === 'all' || scan.status === statusFilter; let matchesDate = true; if (dateFilter !== 'all' && scan.started_at) { const scanDate = new Date(scan.started_at); const diffDays = (now - scanDate) / (1000 * 60 * 60 * 24); if (dateFilter === '7d' && diffDays > 7) matchesDate = false; if (dateFilter === '30d' && diffDays > 30) matchesDate = false; } return matchesSearch && matchesType && matchesStatus && matchesDate; }); // Format date in local timezone (IST-aware) const formatDate = (isoString) => { if (!isoString) return 'Unknown'; return new Date(isoString).toLocaleString('en-IN', { day: '2-digit', month: 'short', year: 'numeric', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: true }); }; // Live "X ago" helper const timeAgo = (isoString) => { if (!isoString) return ''; const diff = Math.floor((now - new Date(isoString)) / 1000); if (diff < 60) return `${diff}s ago`; if (diff < 3600) return `${Math.floor(diff / 60)}m ago`; if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`; return `${Math.floor(diff / 86400)}d ago`; }; const handleSort = (column) => { if (sortColumn === column) { setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc'); } else { setSortColumn(column); setSortDirection('asc'); } }; const getSortedScans = () => { return [...filteredScans].sort((a, b) => { let aVal, bVal; switch (sortColumn) { case 'Report ID & Date': case 'Date': aVal = new Date(a.started_at || 0).getTime(); bVal = new Date(b.started_at || 0).getTime(); break; case 'Target Host': aVal = a.target_url || ''; bVal = b.target_url || ''; break; case 'Engine profile': aVal = a.scan_type || ''; bVal = b.scan_type || ''; break; case 'Findings Status': aVal = a.status || ''; bVal = b.status || ''; break; default: return 0; } if (aVal < bVal) return sortDirection === 'asc' ? -1 : 1; if (aVal > bVal) return sortDirection === 'asc' ? 1 : -1; return 0; }); }; // Calculate Pagination (15 items per page) const sortedScans = getSortedScans(); const totalItems = sortedScans.length; const totalPages = Math.ceil(totalItems / itemsPerPage) || 1; const startIndex = (currentPage - 1) * itemsPerPage; const endIndex = Math.min(startIndex + itemsPerPage, totalItems); const paginatedScans = sortedScans.slice(startIndex, endIndex); return (
{/* Error Toast */} {error && (
error {error}
)} {/* Page Header & Date Actions */}

Reports & Logs

View and manage historical security scans and vulnerability logs.

{showFilters && (
)}
{/* Search & Toolbar */}
search setSearchQuery(e.target.value)} />
Scan Profile:
{/* Reports List Canvas Card */}
{/* Table Header */}
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"> Report ID & Date {sortColumn === 'Date' && sortDirection === 'desc' ? 'arrow_downward' : 'arrow_upward'}
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"> Target Host {sortColumn === 'Target Host' && sortDirection === 'desc' ? 'arrow_downward' : 'arrow_upward'}
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"> Engine profile {sortColumn === 'Engine profile' && sortDirection === 'desc' ? 'arrow_downward' : 'arrow_upward'}
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"> Findings Status {sortColumn === 'Findings Status' && sortDirection === 'desc' ? 'arrow_downward' : 'arrow_upward'}
Actions
{/* List Items */}
{paginatedScans.length === 0 ? (
No historical security audits match your search query.
) : ( paginatedScans.map((s) => { const hasCritical = s.vulnerabilities_count?.critical > 0; const hasHigh = s.vulnerabilities_count?.high > 0; return (
navigate(`/scans/results?id=${s.id}`)} 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" > {/* ID & Date */}
REP-{s.id.substring(0, 8).toUpperCase()} event {formatDate(s.started_at)} {timeAgo(s.started_at)}
{/* Target Host */}
{s.target_url.replace("https://", "").replace("http://", "")}
{/* Type */}
{s.scan_type} Scan
{/* Status / Findings */}
{s.status === 'completed' ? ( (() => { const counts = s.vulnerabilities_count || {}; const total = (counts.critical || 0) + (counts.high || 0) + (counts.medium || 0) + (counts.low || 0) + (counts.info || 0); if (total === 0) { return (
Clean / Safe
); } return (
{counts.critical > 0 && {counts.critical} Crit} {counts.high > 0 && {counts.high} High} {counts.medium > 0 && {counts.medium} Med} {counts.low > 0 && {counts.low} Low} {counts.info > 0 && {counts.info} Info}
); })() ) : s.status === 'scanning' || s.status === 'queued' ? (
Running Audit
) : (
Failed Session
)}
{/* Actions */}
); }) )}
{/* Standard Pagination footer */}
Rows per page: {totalItems > 0 ? `${startIndex + 1} - ${endIndex} of ${totalItems} records` : `0 of 0 records` }
{Array.from({ length: totalPages }, (_, i) => i + 1) .filter(p => p === 1 || p === totalPages || Math.abs(p - currentPage) <= 1) .map((page, idx, arr) => { const prev = arr[idx - 1]; return ( {prev && page - prev > 1 && ...} ); })}
); };