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 */}
{/* 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 (
);
}
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' ? (
) : (
)}
{/* 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 && ...}
);
})}
);
};