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 ? (
) : 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.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 };