import React, { useState, useEffect, useCallback } from 'react';
import { Upload, MessageSquare, RefreshCw, AlertCircle, Loader } from 'lucide-react';
import { useAuth } from '../../contexts/AuthContext';
import ComplianceUploadModal from './ComplianceUploadModal';
import ComplianceDetailPanel from './ComplianceDetailPanel';
const API_BASE = process.env.REACT_APP_API_BASE || 'http://localhost:3001/api';
const TEAL = '#14B8A6';
const TEAMS = ['STEAM', 'ACCESS-ENG'];
// ---------------------------------------------------------------------------
// 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)}%`;
}
// Deduplicate summary entries — one per metric_id for the selected team
// (exclude aggregate "ALL: NTS-AEO" rows)
function teamMetrics(entries, team) {
return entries.filter(e => e.team === team);
}
// ---------------------------------------------------------------------------
// Sub-components
// ---------------------------------------------------------------------------
function MetricHealthCard({ entry, active, onClick }) {
const color = statusColor(entry.status);
const isOk = entry.status === '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() {
const { canWrite } = 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 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);
};
// In-memory filters
const filteredDevices = devices
.filter(d => !metricFilter || d.failing_metrics.some(m => m.metric_id === metricFilter))
.filter(d => !hostSearch || d.hostname.toLowerCase().includes(hostSearch.toLowerCase()));
const metrics = teamMetrics(summary.entries, activeTeam);
const lastUpload = summary.upload;
return (
{/* ── Page header ─────────────────────────────────────────── */}
AEO Compliance
{lastUpload ? (
Last report: {lastUpload.report_date || lastUpload.uploaded_at?.slice(0, 10)}
) : (
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 ──────────────────────────────────── */}
{metrics.length > 0 ? (
Metric Health — click to filter
{metricFilter && (
)}
{metrics.map(entry => (
setMetricFilter(metricFilter === entry.metric_id ? null : entry.metric_id)}
/>
))}
) : lastUpload === null ? (
No compliance data — upload a report to get started
) : null}
{/* ── 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 ? (
) : 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}
/>
)}
{/* ── Upload modal ─────────────────────────────────────────── */}
{showUpload && (
setShowUpload(false)}
onUploadComplete={() => { setShowUpload(false); refresh(); }}
/>
)}
);
}
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 && (
)}
);
}