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 metricDefinitionsRaw from '../../data/metricDefinitions.json'; const API_BASE = process.env.REACT_APP_API_BASE || 'http://localhost:3001/api'; const TEAL = '#14B8A6'; const TEAMS = ['STEAM', 'ACCESS-ENG']; // 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)), })); } // --------------------------------------------------------------------------- // Sub-components // --------------------------------------------------------------------------- function VariantPill({ entry }) { const color = statusColor(entry.status); const isOk = entry.status === 'Meets/Exceeds Target'; return ( {!isOk && ( )} {entry.description || entry.team} {pctDisplay(entry.compliance_pct)} ); } 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}× ); } // --------------------------------------------------------------------------- // Main Page // --------------------------------------------------------------------------- export default function CompliancePage({ onNavigate }) { const { canWrite, isAdmin } = useAuth(); const [activeTeam, setActiveTeam] = useState('STEAM'); const [activeTab, setActiveTab] = useState('active'); const [metricFilter, setMetricFilter] = 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(() => { setMetricFilter(null); setHostSearch(''); setSelectedHost(null); fetchSummary(activeTeam); fetchDevices(activeTeam, activeTab); }, [activeTeam]); // eslint-disable-line react-hooks/exhaustive-deps useEffect(() => { setMetricFilter(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 => !metricFilter || d.failing_metrics.some(m => metricFilter.includes(m.metric_id))) .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() && ( )}
{/* ── Team tabs ────────────────────────────────────────────── */}
{TEAMS.map(team => { const isActive = activeTeam === team; return ( ); })}
{/* ── Metric health cards ──────────────────────────────────── */} {families.length > 0 ? (
Metric Health — click to filter {metricFilter && ( )}
{families.map(family => { const familyIds = family.entries.map(e => e.metric_id); const isActive = metricFilter !== null && metricFilter.length === familyIds.length && familyIds.every(id => metricFilter.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' }} > setMetricFilter(isActive ? null : 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} {/* ── Historical trend charts ──────────────────────────────── */} {/* ── Device table ─────────────────────────────────────────── */}
{/* 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 Seen
{/* Rows */} {loading ? (
) : error ? (
{error}
) : filteredDevices.length === 0 ? (
{lastUpload === null ? 'No reports uploaded yet' : 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} 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 }) { 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 => ( ))}
{/* Seen count */}
{/* Notes indicator */}
{device.has_notes && ( )}
); } // Named exports for testing export { computeWorstStatus, groupByMetricFamily };