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({ onNavigate }) { 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 ? (
{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(); }} /> )}
); } 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 && ( )}
); }