import React, { useState, useEffect, useCallback, useRef } from 'react'; import { Upload, MessageSquare, RefreshCw, AlertCircle, Loader, RotateCcw, Info } from 'lucide-react'; import { useAuth } from '../../contexts/AuthContext'; import ComplianceUploadModal from './ComplianceUploadModal'; import ComplianceDetailPanel from './ComplianceDetailPanel'; import ComplianceChartsPanel from './ComplianceChartsPanel'; import MetricInfoPanel from './MetricInfoPanel'; import VCLReportPage from './VCLReportPage'; import metricDefinitionsRaw from '../../data/metricDefinitions.json'; import metricCategoriesConfig from '../../data/complianceCategories.json'; const API_BASE = process.env.REACT_APP_API_BASE || 'http://localhost:3001/api'; const TEAL = '#14B8A6'; // Build definitions lookup map once at module level const METRIC_DEFINITIONS = {}; for (const def of metricDefinitionsRaw) { METRIC_DEFINITIONS[def.metric_id] = def; } // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- const STATUS_COLOR = { 'Meets/Exceeds Target': '#10B981', 'Within 15% of Target': '#F59E0B', 'Below 15% of Target': '#EF4444', }; const CATEGORY_COLORS = { 'Vulnerability Management': '#EF4444', 'Access & MFA': '#F59E0B', 'Logging & Monitoring': '#8B5CF6', 'End-of-Life OS': '#F97316', 'Decommissioned Assets': '#64748B', 'Asset Data Quality': '#64748B', 'Application Security': '#0EA5E9', 'Disaster Recovery': TEAL, 'Endpoint Protection': '#F97316', }; function statusColor(status) { return STATUS_COLOR[status] || '#EF4444'; } function pctDisplay(pct) { return `${Math.round(pct * 100)}%`; } const STATUS_SEVERITY = { 'Below 15% of Target': 0, 'Within 15% of Target': 1, 'Meets/Exceeds Target': 2, }; function computeWorstStatus(statuses) { let worst = 'Meets/Exceeds Target'; let worstSev = 2; for (const s of statuses) { const sev = STATUS_SEVERITY[s] ?? 0; if (sev < worstSev) { worstSev = sev; worst = s; } } return worst; } function groupByMetricFamily(allEntries, team) { const teamEntries = allEntries.filter(e => e.team === team); const familyMap = {}; for (const entry of teamEntries) { const baseId = entry.metric_id; if (!baseId) continue; if (!familyMap[baseId]) { familyMap[baseId] = []; } familyMap[baseId].push(entry); } return Object.entries(familyMap).map(([metricId, entries]) => ({ metricId, entries, category: entries[0].category, target: entries[0].target, worstStatus: computeWorstStatus(entries.map(e => e.status)), })); } // --------------------------------------------------------------------------- // Non-metric category derivation // --------------------------------------------------------------------------- function deriveNonMetricCategories(devices, summaryEntries, categoriesConfig) { const summaryIds = new Set(summaryEntries.map(e => e.metric_id)); const countMap = new Map(); for (const device of devices) { if (!device.failing_metrics) continue; const seen = new Set(); for (const m of device.failing_metrics) { if (!m.metric_id || summaryIds.has(m.metric_id) || seen.has(m.metric_id)) continue; seen.add(m.metric_id); countMap.set(m.metric_id, (countMap.get(m.metric_id) || 0) + 1); } } return [...countMap.entries()] .map(([metricId, count]) => { const categoryName = categoriesConfig[metricId] || null; const color = (categoryName && CATEGORY_COLORS[categoryName]) || '#94A3B8'; return { metricId, count, category: categoryName || 'Unknown', color }; }) .sort((a, b) => a.metricId.localeCompare(b.metricId)); } // --------------------------------------------------------------------------- // Sub-components // --------------------------------------------------------------------------- function VariantPill({ entry, label }) { const color = statusColor(entry.status); const isOk = entry.status === 'Meets/Exceeds Target'; const hasRawCounts = entry.compliant != null && entry.total != null && entry.total > 0; return ( {!isOk && ( )} {label && {label}} {pctDisplay(entry.compliance_pct)} {hasRawCounts && ( ({entry.compliant}/{entry.total}) )} ); } function MetricHealthCard({ family, active, onClick, onInfoClick, definitionLookup }) { const color = statusColor(family.worstStatus); const isOk = family.worstStatus === 'Meets/Exceeds Target'; return ( ); } function MetricBadge({ metricId, category }) { const color = CATEGORY_COLORS[category] || '#94A3B8'; return ( {metricId} ); } function SeenBadge({ count }) { const color = count > 3 ? '#EF4444' : count > 1 ? '#F59E0B' : '#64748B'; return ( {count}× ); } function FilterChip({ metricId, count, color, active, dimmed, onClick }) { const label = metricId.length > 24 ? metricId.slice(0, 24) + '…' : metricId; return ( ); } function CategoryFilterBar({ categories, activeFilter, onFilterSelect, onClear, dimmed }) { if (!categories || categories.length === 0) return null; return (
Non-Metric Categories {activeFilter && ( )}
{categories.map(cat => ( onFilterSelect(cat.metricId)} /> ))}
); } // --------------------------------------------------------------------------- // Main Page // --------------------------------------------------------------------------- export default function CompliancePage({ onNavigate }) { const { canWrite, isAdmin, getAvailableTeams, adminScope } = useAuth(); const availableTeams = getAvailableTeams(); const [activeTeam, setActiveTeam] = useState(() => availableTeams[0] || 'STEAM'); const [activeTab, setActiveTab] = useState('active'); const [vclView, setVclView] = useState(false); const [filterState, setFilterState] = useState(null); const [hostSearch, setHostSearch] = useState(''); const [summary, setSummary] = useState({ entries: [], overall_scores: {}, upload: null }); const [devices, setDevices] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [selectedHost, setSelectedHost] = useState(null); const [showUpload, setShowUpload] = useState(false); const [rollbackConfirm, setRollbackConfirm] = useState(false); const [rollbackLoading, setRollbackLoading] = useState(false); const [rollbackResult, setRollbackResult] = useState(null); const [infoMetric, setInfoMetric] = useState(null); const [hoveredMetric, setHoveredMetric] = useState(null); const hoverTimeoutRef = useRef(null); const hoveredCardRef = useRef(null); const fetchSummary = useCallback(async (team) => { try { const res = await fetch(`${API_BASE}/compliance/summary?team=${team}`, { credentials: 'include' }); const data = await res.json(); setSummary(data); } catch { /* silent */ } }, []); const fetchDevices = useCallback(async (team, tab) => { setLoading(true); setError(null); try { const res = await fetch(`${API_BASE}/compliance/items?team=${team}&status=${tab}`, { credentials: 'include' }); const data = await res.json(); if (!res.ok) throw new Error(data.error || 'Failed to load'); setDevices(data.devices || []); } catch (err) { setError(err.message); setDevices([]); } finally { setLoading(false); } }, []); useEffect(() => { setFilterState(null); setHostSearch(''); setSelectedHost(null); fetchSummary(activeTeam); fetchDevices(activeTeam, activeTab); }, [activeTeam]); // eslint-disable-line react-hooks/exhaustive-deps // When admin scope changes, reset to first available team useEffect(() => { const teams = getAvailableTeams(); if (teams.length > 0 && !teams.includes(activeTeam)) { setActiveTeam(teams[0]); } }, [adminScope]); // eslint-disable-line react-hooks/exhaustive-deps useEffect(() => { setFilterState(null); fetchDevices(activeTeam, activeTab); }, [activeTab]); // eslint-disable-line react-hooks/exhaustive-deps const refresh = () => { fetchSummary(activeTeam); fetchDevices(activeTeam, activeTab); }; const handleRollback = async () => { if (!lastUpload) return; setRollbackLoading(true); try { const res = await fetch(`${API_BASE}/compliance/rollback/${lastUpload.id}`, { method: 'POST', credentials: 'include', }); const data = await res.json(); if (!res.ok) throw new Error(data.error || 'Rollback failed'); setRollbackResult(data); setRollbackConfirm(false); refresh(); // Auto-dismiss result after 4 seconds setTimeout(() => setRollbackResult(null), 4000); } catch (err) { setRollbackResult({ error: err.message }); } finally { setRollbackLoading(false); } }; // In-memory filters const filteredDevices = devices .filter(d => { if (!filterState) return true; if (filterState.type === 'metric') return d.failing_metrics.some(m => filterState.ids.includes(m.metric_id)); if (filterState.type === 'nonmetric') return d.failing_metrics.some(m => m.metric_id === filterState.id); return true; }) .filter(d => !hostSearch || d.hostname.toLowerCase().includes(hostSearch.toLowerCase())); const families = groupByMetricFamily(summary.entries, activeTeam); const lastUpload = summary.upload; return (
{/* ── Page header ─────────────────────────────────────────── */}

AEO Compliance

{lastUpload ? ( <> Last report: {lastUpload.report_date || lastUpload.uploaded_at?.slice(0, 10)} {isAdmin() && ( )} ) : ( No reports uploaded )} {summary.overall_scores?.customer_network != null && ( Network: {pctDisplay(summary.overall_scores.customer_network)} )} {summary.overall_scores?.vertical != null && ( Vertical: {pctDisplay(summary.overall_scores.vertical)} )}
{canWrite() && ( )}
{/* ── VCL Report View ─────────────────────────────────────── */} {vclView && ( )} {/* ── Team tabs ────────────────────────────────────────────── */} {!vclView && availableTeams.length === 0 && !isAdmin() ? (
No BU teams assigned to your account. Contact an admin to configure your team access.
) : !vclView && (
{availableTeams.map(team => { const isActive = activeTeam === team; return ( ); })}
)} {/* ── Metric health cards ──────────────────────────────────── */} {!vclView && families.length > 0 ? (
Metric Health — click to filter {filterState && ( )}
{families.map(family => { const familyIds = family.entries.map(e => e.metric_id); const isActive = filterState?.type === 'metric' && filterState.ids.length === familyIds.length && familyIds.every(id => filterState.ids.includes(id)); return (
{ hoveredCardRef.current = e.currentTarget; if (hoverTimeoutRef.current) clearTimeout(hoverTimeoutRef.current); hoverTimeoutRef.current = setTimeout(() => setHoveredMetric(family.metricId), 300); }} onMouseLeave={() => { if (hoverTimeoutRef.current) clearTimeout(hoverTimeoutRef.current); hoverTimeoutRef.current = null; hoveredCardRef.current = null; setHoveredMetric(null); }} style={{ display: 'flex', flex: '1 1 0', minWidth: '160px', opacity: filterState?.type === 'nonmetric' ? 0.5 : 1, transition: 'opacity 0.15s' }} > setFilterState(isActive ? null : { type: 'metric', ids: familyIds })} onInfoClick={(metricId) => setInfoMetric(metricId)} definitionLookup={METRIC_DEFINITIONS} />
); })}
{/* Hover tooltip */} {hoveredMetric && (() => { const family = families.find(f => f.metricId === hoveredMetric); if (!family) return null; const def = METRIC_DEFINITIONS[hoveredMetric]; const rect = hoveredCardRef.current ? hoveredCardRef.current.getBoundingClientRect() : null; if (!rect) return null; const tooltipTop = Math.min(rect.bottom + 8, window.innerHeight - 180); const tooltipLeft = Math.max(8, Math.min(rect.left, window.innerWidth - 320)); return (
{def ? def.metric_title : (family.entries[0]?.description || hoveredMetric)}
{def && def.business_justification && (
{def.business_justification}
)} {def && def.data_sources_required && (
Sources: {def.data_sources_required}
)} {!def && family.entries[0]?.description && (
{family.entries[0].description}
)}
); })()}
) : lastUpload === null ? (
No compliance data — upload a report to get started
) : null} {/* ── Non-metric category filter bar ─────────────────────── */} {!vclView && !loading && (() => { const nonMetricCategories = deriveNonMetricCategories(devices, summary.entries.filter(e => e.team === activeTeam), metricCategoriesConfig); if (nonMetricCategories.length === 0) return null; return ( { if (filterState?.type === 'nonmetric' && filterState.id === metricId) { setFilterState(null); } else { setFilterState({ type: 'nonmetric', id: metricId }); } }} onClear={() => setFilterState(null)} dimmed={filterState?.type === 'metric'} /> ); })()} {/* ── Historical trend charts ──────────────────────────────── */} {!vclView && } {/* ── Device table ─────────────────────────────────────────── */} {!vclView &&
{/* Table toolbar */}
{/* Active / Resolved tabs */}
{['active', 'resolved'].map(tab => { const isActive = activeTab === tab; return ( ); })}
{/* Hostname search */} setHostSearch(e.target.value)} placeholder="Search hostname…" style={{ background: 'rgba(15,23,42,0.8)', border: '1px solid rgba(20,184,166,0.2)', borderRadius: '0.25rem', color: '#E2E8F0', outline: 'none', padding: '0.35rem 0.625rem', fontSize: '0.75rem', fontFamily: 'monospace', width: '220px', }} onFocus={e => e.target.style.borderColor = `${TEAL}60`} onBlur={e => e.target.style.borderColor = 'rgba(20,184,166,0.2)'} />
{/* Column headers */}
Hostname IP Address Type Failing Metrics Resolution Date Remediation Plan Seen
{/* Rows */} {loading ? (
) : error ? (
{error}
) : filteredDevices.length === 0 ? (
{lastUpload === null ? 'No reports uploaded yet' : filterState ? 'No devices match the selected filter' : activeTab === 'active' ? 'No non-compliant devices' : 'No resolved items'}
) : ( filteredDevices.map(device => ( setSelectedHost(selectedHost === device.hostname ? null : device.hostname)} /> )) )}
} {/* ── Detail panel ─────────────────────────────────────────── */} {selectedHost && ( setSelectedHost(null)} onNoteAdded={refresh} onMetadataSaved={refresh} onNavigate={onNavigate} /> )} {/* ── Upload modal ─────────────────────────────────────────── */} {showUpload && ( setShowUpload(false)} onUploadComplete={() => { setShowUpload(false); refresh(); }} /> )} {/* ── Metric info panel ───────────────────────────────────── */} {infoMetric && ( f.metricId === infoMetric) || {}).entries || []} onClose={() => setInfoMetric(null)} /> )} {/* ── Rollback confirmation modal ──────────────────────────── */} {rollbackConfirm && lastUpload && (
Rollback Upload
This will reverse the most recent upload:
File: {lastUpload.report_date || 'unknown date'}
New items will be deleted, resolved items will be reactivated, and the upload record will be removed.
)} {/* ── Rollback result toast ────────────────────────────────── */} {rollbackResult && (
setRollbackResult(null)} > {rollbackResult.error ? (
{rollbackResult.error}
) : ( <>
{rollbackResult.message}
{rollbackResult.rolled_back && (
{rollbackResult.rolled_back.items_deleted} items deleted, {rollbackResult.rolled_back.items_reactivated} reactivated
)} )}
)}
); } function DeviceRow({ device, selected, onClick }) { const truncateText = (text, maxLen = 80) => { if (!text) return '—'; return text.length > maxLen ? text.slice(0, maxLen) + '…' : text; }; return (
{ if (!selected) e.currentTarget.style.background = 'rgba(255,255,255,0.025)'; }} onMouseLeave={e => { if (!selected) e.currentTarget.style.background = 'transparent'; }} > {/* Hostname */}
{device.hostname}
{/* IP */}
{device.ip_address || '—'}
{/* Type */}
{device.device_type || '—'}
{/* Failing metrics */}
{device.failing_metrics.map(m => ( ))}
{/* Resolution Date */}
{device.resolution_date ? device.resolution_date.slice(0, 10) : '—'}
{/* Remediation Plan */}
{truncateText(device.remediation_plan)}
{/* Seen count */}
{/* Notes indicator */}
{device.has_notes && ( )}
); } // Named exports for testing export { computeWorstStatus, groupByMetricFamily, deriveNonMetricCategories, CATEGORY_COLORS };