Files
cve-dashboard/frontend/src/components/pages/CompliancePage.js
Jordan Ramos e9d6038636 Add Granite Loader to AEO Compliance page with CARD enrichment and pagination
- Add checkbox selection + Granite Loader button to compliance device table
- Integrate LoaderModal for generating loader sheets from compliance devices
- Add direct IP resolve path (resolveAssetId + searchByAssetId) for CARD
  enrichment on compliance devices without Ivanti host IDs
- Add searchByAssetId helper for full enriched record via asset-search endpoint
- Include NTS-AEO-ACCESS-OPS in default enrich-batch team search
- Increase CARD quick-mode timeout from 15s to 30s
- Add timeout vs not-found distinction in enrichment error reporting
- Fix LoaderModal enriching state not resetting on modal reopen
- Add pagination to compliance device table (25/50/100/200 per page)
- Page resets on team, tab, filter, or search change
2026-06-19 13:49:26 -06:00

1186 lines
60 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useState, useEffect, useCallback, useRef } from 'react';
import { Upload, MessageSquare, RefreshCw, AlertCircle, Loader, RotateCcw, Info, FileSpreadsheet } 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 LoaderModal from '../LoaderModal';
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 (
<span style={{
display: 'inline-flex',
alignItems: 'center',
gap: '0.3rem',
padding: '0.15rem 0.45rem',
background: `${color}1F`,
borderRadius: '0.2rem',
border: `1px solid ${color}25`,
fontSize: '0.62rem',
fontFamily: 'monospace',
color: '#CBD5E1',
whiteSpace: 'nowrap',
}}>
{!isOk && (
<span style={{
width: '4px', height: '4px', borderRadius: '50%',
background: color, flexShrink: 0,
boxShadow: `0 0 5px ${color}`,
}} />
)}
{label && <span style={{ color: '#94A3B8' }}>{label}</span>}
<span style={{ color, fontWeight: '600' }}>{pctDisplay(entry.compliance_pct)}</span>
{hasRawCounts && (
<span style={{ color: '#64748B', fontSize: '0.58rem' }}>({entry.compliant}/{entry.total})</span>
)}
</span>
);
}
function MetricHealthCard({ family, active, onClick, onInfoClick, definitionLookup }) {
const color = statusColor(family.worstStatus);
const isOk = family.worstStatus === 'Meets/Exceeds Target';
return (
<button
onClick={onClick}
style={{
background: active
? `${color}18`
: 'linear-gradient(135deg,rgba(30,41,59,0.95) 0%,rgba(15,23,42,0.98) 100%)',
border: `1.5px solid ${active ? color : color + '40'}`,
borderRadius: '0.5rem',
padding: '0.875rem 1rem',
cursor: 'pointer',
textAlign: 'left',
transition: 'all 0.15s',
minWidth: '160px',
flex: '1 1 0',
position: 'relative',
}}
onMouseEnter={e => { if (!active) e.currentTarget.style.borderColor = color + '80'; }}
onMouseLeave={e => { if (!active) e.currentTarget.style.borderColor = active ? color : color + '40'; }}
>
{/* Info icon — top-right */}
<span
onClick={(e) => { e.stopPropagation(); onInfoClick(family.metricId); }}
style={{
position: 'absolute',
top: '0.5rem',
right: '0.5rem',
cursor: 'pointer',
color: '#475569',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: '0.15rem',
borderRadius: '0.2rem',
transition: 'color 0.15s',
}}
onMouseEnter={e => { e.currentTarget.style.color = TEAL; }}
onMouseLeave={e => { e.currentTarget.style.color = '#475569'; }}
>
<Info style={{ width: '13px', height: '13px' }} />
</span>
{/* Metric ID */}
<div style={{ fontFamily: 'monospace', fontSize: '0.95rem', fontWeight: '700', color: active ? color : '#E2E8F0', marginBottom: '0.25rem', paddingRight: '1.25rem' }}>
{family.metricId}
</div>
{/* Category */}
<div style={{ fontSize: '0.65rem', color: '#475569', marginBottom: '0.5rem', textTransform: 'uppercase', letterSpacing: '0.05em' }}>
{family.category}
</div>
{/* Variant pills */}
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.3rem', marginBottom: '0.5rem' }}>
{family.entries.map((entry, i) => {
// Only show a label when there are multiple variants to differentiate
let label = null;
if (family.entries.length > 1) {
label = entry.priority || `#${i + 1}`;
}
return <VariantPill key={entry.metric_id + '-' + i} entry={entry} label={label} />;
})}
</div>
{/* Target */}
<div style={{ fontSize: '0.68rem', color: '#475569', fontFamily: 'monospace', marginBottom: '0.5rem' }}>
target {pctDisplay(family.target)}
</div>
{/* Status pill */}
<div style={{
display: 'inline-flex', alignItems: 'center', gap: '0.3rem',
fontSize: '0.62rem', fontFamily: 'monospace', textTransform: 'uppercase', letterSpacing: '0.04em',
color, padding: '0.2rem 0.5rem',
background: `${color}12`, borderRadius: '999px',
border: `1px solid ${color}30`,
}}>
<span style={{
width: '5px', height: '5px', borderRadius: '50%',
background: color, flexShrink: 0,
...(isOk ? {} : { boxShadow: `0 0 6px ${color}` }),
}} />
{isOk ? 'OK' : family.worstStatus.replace(' of Target', '')}
</div>
</button>
);
}
function MetricBadge({ metricId, category }) {
const color = CATEGORY_COLORS[category] || '#94A3B8';
return (
<span style={{
display: 'inline-flex', alignItems: 'center',
padding: '0.15rem 0.45rem',
background: `${color}15`, border: `1px solid ${color}40`,
borderRadius: '0.2rem', color,
fontSize: '0.65rem', fontFamily: 'monospace', fontWeight: '600',
whiteSpace: 'nowrap',
}}>
{metricId}
</span>
);
}
function SeenBadge({ count }) {
const color = count > 3 ? '#EF4444' : count > 1 ? '#F59E0B' : '#64748B';
return (
<span style={{
fontSize: '0.65rem', fontFamily: 'monospace', fontWeight: '700',
color, padding: '0.15rem 0.4rem',
background: `${color}12`, borderRadius: '0.2rem',
border: `1px solid ${color}30`, whiteSpace: 'nowrap',
}}>
{count}×
</span>
);
}
function FilterChip({ metricId, count, color, active, dimmed, onClick }) {
const label = metricId.length > 24 ? metricId.slice(0, 24) + '…' : metricId;
return (
<button
onClick={onClick}
title={metricId.length > 24 ? metricId : undefined}
style={{
display: 'inline-flex',
alignItems: 'center',
gap: '0.5rem',
padding: '0.375rem 0.75rem',
borderRadius: '999px',
border: `1px solid ${active ? color : color + '40'}`,
background: active ? `${color}1F` : `${color}0A`,
cursor: 'pointer',
opacity: dimmed ? 0.5 : 1,
transition: 'all 0.15s',
}}
onMouseEnter={e => { e.currentTarget.style.borderColor = color + '80'; }}
onMouseLeave={e => { e.currentTarget.style.borderColor = active ? color : color + '40'; }}
>
<span style={{
width: '6px', height: '6px', borderRadius: '50%',
background: color, flexShrink: 0,
}} />
<span style={{ fontSize: '0.72rem', color, fontFamily: 'monospace', fontWeight: '600', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', maxWidth: '150px' }}>
{label}
</span>
<span style={{ fontSize: '0.68rem', fontFamily: 'monospace', color: '#64748B', fontWeight: '700' }}>
{count}
</span>
</button>
);
}
function CategoryFilterBar({ categories, activeFilter, onFilterSelect, onClear, dimmed }) {
if (!categories || categories.length === 0) return null;
return (
<div style={{ marginBottom: '1.5rem', opacity: dimmed ? 0.5 : 1, transition: 'opacity 0.15s' }}>
<div style={{ fontSize: '0.65rem', color: '#334155', fontFamily: 'monospace', textTransform: 'uppercase', letterSpacing: '0.1em', marginBottom: '0.625rem' }}>
Non-Metric Categories
{activeFilter && (
<button onClick={onClear}
style={{ marginLeft: '0.75rem', color: TEAL, background: 'none', border: 'none', cursor: 'pointer', fontSize: '0.65rem', fontFamily: 'monospace' }}>
× clear filter
</button>
)}
</div>
<div style={{ display: 'flex', gap: '0.5rem', flexWrap: 'wrap' }}>
{categories.map(cat => (
<FilterChip
key={cat.metricId}
metricId={cat.metricId}
count={cat.count}
color={cat.color}
active={activeFilter === cat.metricId}
dimmed={false}
onClick={() => onFilterSelect(cat.metricId)}
/>
))}
</div>
</div>
);
}
// ---------------------------------------------------------------------------
// 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 [selectedDevices, setSelectedDevices] = useState(new Set());
const [showLoaderModal, setShowLoaderModal] = useState(false);
const [currentPage, setCurrentPage] = useState(1);
const [pageSize, setPageSize] = useState(50);
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);
setSelectedDevices(new Set());
setCurrentPage(1);
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);
setSelectedDevices(new Set());
setCurrentPage(1);
fetchDevices(activeTeam, activeTab);
}, [activeTab]); // eslint-disable-line react-hooks/exhaustive-deps
// Reset page when filter changes
useEffect(() => { setCurrentPage(1); }, [filterState]);
const refresh = () => {
fetchSummary(activeTeam);
fetchDevices(activeTeam, activeTab);
};
const toggleDeviceSelection = (hostname) => {
setSelectedDevices(prev => {
const next = new Set(prev);
if (next.has(hostname)) next.delete(hostname);
else next.add(hostname);
return next;
});
};
const toggleSelectAll = () => {
if (selectedDevices.size === paginatedDevices.length && paginatedDevices.every(d => selectedDevices.has(d.hostname))) {
setSelectedDevices(new Set());
} else {
setSelectedDevices(new Set(paginatedDevices.map(d => d.hostname)));
}
};
const openGraniteLoader = () => {
setShowLoaderModal(true);
};
const getLoaderDevices = () => {
return filteredDevices
.filter(d => selectedDevices.has(d.hostname))
.map(d => ({
ip_address: d.ip_address || '',
hostname: d.hostname || '',
host_id: null,
}));
};
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()));
// Pagination
const totalPages = Math.max(1, Math.ceil(filteredDevices.length / pageSize));
const safePage = Math.min(currentPage, totalPages);
const paginatedDevices = filteredDevices.slice((safePage - 1) * pageSize, safePage * pageSize);
const families = groupByMetricFamily(summary.entries, activeTeam);
const lastUpload = summary.upload;
return (
<div style={{ paddingBottom: '2rem' }}>
{/* ── Page header ─────────────────────────────────────────── */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: '1.5rem' }}>
<div>
<h2 style={{
fontFamily: 'monospace', fontSize: '1.5rem', fontWeight: '700',
color: TEAL, textTransform: 'uppercase', letterSpacing: '0.1em',
textShadow: `0 0 16px ${TEAL}40`, marginBottom: '0.25rem',
}}>
AEO Compliance
</h2>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.75rem', flexWrap: 'wrap' }}>
{lastUpload ? (
<>
<span style={{ fontSize: '0.72rem', color: '#475569', fontFamily: 'monospace' }}>
Last report: <span style={{ color: '#64748B' }}>{lastUpload.report_date || lastUpload.uploaded_at?.slice(0, 10)}</span>
</span>
{isAdmin() && (
<button
onClick={() => setRollbackConfirm(true)}
title="Rollback last upload"
style={{
background: 'none', border: '1px solid rgba(239,68,68,0.25)',
borderRadius: '0.25rem', padding: '0.15rem 0.4rem',
cursor: 'pointer', color: '#64748B',
display: 'inline-flex', alignItems: 'center', gap: '0.25rem',
fontSize: '0.62rem', fontFamily: 'monospace',
transition: 'all 0.15s',
}}
onMouseEnter={e => { e.currentTarget.style.color = '#EF4444'; e.currentTarget.style.borderColor = 'rgba(239,68,68,0.6)'; }}
onMouseLeave={e => { e.currentTarget.style.color = '#64748B'; e.currentTarget.style.borderColor = 'rgba(239,68,68,0.25)'; }}
>
<RotateCcw style={{ width: '10px', height: '10px' }} />
Rollback
</button>
)}
</>
) : (
<span style={{ fontSize: '0.72rem', color: '#334155', fontFamily: 'monospace' }}>No reports uploaded</span>
)}
{summary.overall_scores?.customer_network != null && (
<span style={{ fontSize: '0.68rem', fontFamily: 'monospace', color: '#64748B' }}>
Network: <span style={{ color: TEAL }}>{pctDisplay(summary.overall_scores.customer_network)}</span>
</span>
)}
{summary.overall_scores?.vertical != null && (
<span style={{ fontSize: '0.68rem', fontFamily: 'monospace', color: '#64748B' }}>
Vertical: <span style={{ color: TEAL }}>{pctDisplay(summary.overall_scores.vertical)}</span>
</span>
)}
</div>
</div>
<div style={{ display: 'flex', gap: '0.5rem', flexShrink: 0 }}>
<button onClick={refresh} title="Refresh"
style={{ background: 'none', border: '1px solid rgba(20,184,166,0.25)', borderRadius: '0.375rem', padding: '0.5rem', cursor: 'pointer', color: '#475569' }}
onMouseEnter={e => { e.currentTarget.style.color = TEAL; e.currentTarget.style.borderColor = `${TEAL}60`; }}
onMouseLeave={e => { e.currentTarget.style.color = '#475569'; e.currentTarget.style.borderColor = 'rgba(20,184,166,0.25)'; }}>
<RefreshCw style={{ width: '16px', height: '16px' }} />
</button>
<button
onClick={() => setVclView(!vclView)}
style={{
background: vclView ? `${TEAL}18` : 'transparent',
border: `1px solid ${vclView ? TEAL : 'rgba(20,184,166,0.25)'}`,
color: vclView ? TEAL : '#475569',
padding: '0.5rem 1rem',
display: 'flex', alignItems: 'center', gap: '0.4rem',
fontFamily: 'monospace', fontSize: '0.75rem', fontWeight: '600',
textTransform: 'uppercase', letterSpacing: '0.05em', cursor: 'pointer',
borderRadius: '0.375rem', transition: 'all 0.15s',
}}
onMouseEnter={e => { if (!vclView) { e.currentTarget.style.color = TEAL; e.currentTarget.style.borderColor = `${TEAL}60`; }}}
onMouseLeave={e => { if (!vclView) { e.currentTarget.style.color = '#475569'; e.currentTarget.style.borderColor = 'rgba(20,184,166,0.25)'; }}}
>
VCL Report
</button>
{canWrite() && (
<button onClick={() => setShowUpload(true)}
className="intel-button"
style={{
background: `${TEAL}18`, border: `1px solid ${TEAL}`,
color: TEAL, padding: '0.5rem 1rem',
display: 'flex', alignItems: 'center', gap: '0.4rem',
fontFamily: 'monospace', fontSize: '0.75rem', fontWeight: '600',
textTransform: 'uppercase', letterSpacing: '0.05em', cursor: 'pointer',
borderRadius: '0.375rem',
}}>
<Upload style={{ width: '14px', height: '14px' }} />
Upload Report
</button>
)}
</div>
</div>
{/* ── VCL Report View ─────────────────────────────────────── */}
{vclView && (
<VCLReportPage />
)}
{/* ── Team tabs ────────────────────────────────────────────── */}
{!vclView && availableTeams.length === 0 && !isAdmin() ? (
<div style={{
padding: '1.5rem', marginBottom: '1.5rem',
borderRadius: '0.5rem', border: '1px solid rgba(245, 158, 11, 0.3)',
background: 'rgba(245, 158, 11, 0.05)',
fontFamily: 'monospace', fontSize: '0.8rem', color: '#F59E0B',
textAlign: 'center'
}}>
No BU teams assigned to your account. Contact an admin to configure your team access.
</div>
) : !vclView && (
<div style={{ display: 'flex', gap: '0.375rem', marginBottom: '1.5rem' }}>
{availableTeams.map(team => {
const isActive = activeTeam === team;
return (
<button key={team} onClick={() => setActiveTeam(team)}
style={{
padding: '0.5rem 1.25rem', cursor: 'pointer',
fontFamily: 'monospace', fontSize: '0.8rem', fontWeight: '600',
textTransform: 'uppercase', letterSpacing: '0.06em',
borderRadius: '0.375rem',
border: isActive ? `1px solid ${TEAL}` : '1px solid rgba(20,184,166,0.2)',
background: isActive ? `${TEAL}18` : 'transparent',
color: isActive ? TEAL : '#475569',
transition: 'all 0.15s',
}}
onMouseEnter={e => { if (!isActive) { e.currentTarget.style.color = '#94A3B8'; e.currentTarget.style.borderColor = 'rgba(20,184,166,0.4)'; }}}
onMouseLeave={e => { if (!isActive) { e.currentTarget.style.color = '#475569'; e.currentTarget.style.borderColor = 'rgba(20,184,166,0.2)'; }}}>
{team}
</button>
);
})}
</div>
)}
{/* ── Metric health cards ──────────────────────────────────── */}
{!vclView && families.length > 0 ? (
<div style={{ marginBottom: '1.5rem' }}>
<div style={{ fontSize: '0.65rem', color: '#334155', fontFamily: 'monospace', textTransform: 'uppercase', letterSpacing: '0.1em', marginBottom: '0.625rem' }}>
Metric Health click to filter
{filterState && (
<button onClick={() => setFilterState(null)}
style={{ marginLeft: '0.75rem', color: TEAL, background: 'none', border: 'none', cursor: 'pointer', fontSize: '0.65rem', fontFamily: 'monospace' }}>
× clear filter
</button>
)}
</div>
<div style={{ display: 'flex', gap: '0.625rem', flexWrap: 'wrap' }}>
{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 (
<div
key={family.metricId}
onMouseEnter={(e) => {
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' }}
>
<MetricHealthCard
family={family}
active={isActive}
onClick={() => setFilterState(isActive ? null : { type: 'metric', ids: familyIds })}
onInfoClick={(metricId) => setInfoMetric(metricId)}
definitionLookup={METRIC_DEFINITIONS}
/>
</div>
);
})}
</div>
{/* 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 (
<div style={{
position: 'fixed',
top: tooltipTop,
left: tooltipLeft,
zIndex: 50,
width: '300px',
background: 'linear-gradient(135deg, rgba(30,41,59,0.98) 0%, rgba(15,23,42,0.99) 100%)',
border: '1px solid rgba(20,184,166,0.25)',
borderRadius: '0.5rem',
boxShadow: '0 8px 32px rgba(0,0,0,0.6)',
padding: '0.75rem 0.875rem',
pointerEvents: 'none',
}}>
<div style={{ fontFamily: 'monospace', fontSize: '0.82rem', fontWeight: '700', color: '#E2E8F0', marginBottom: '0.4rem', lineHeight: 1.3 }}>
{def ? def.metric_title : (family.entries[0]?.description || hoveredMetric)}
</div>
{def && def.business_justification && (
<div style={{ fontSize: '0.72rem', color: '#94A3B8', marginBottom: '0.3rem', lineHeight: 1.4 }}>
{def.business_justification}
</div>
)}
{def && def.data_sources_required && (
<div style={{ fontSize: '0.65rem', color: '#475569', fontFamily: 'monospace' }}>
Sources: {def.data_sources_required}
</div>
)}
{!def && family.entries[0]?.description && (
<div style={{ fontSize: '0.72rem', color: '#94A3B8', lineHeight: 1.4 }}>
{family.entries[0].description}
</div>
)}
</div>
);
})()}
</div>
) : lastUpload === null ? (
<div style={{
marginBottom: '1.5rem', padding: '2rem',
border: '1px dashed rgba(20,184,166,0.2)', borderRadius: '0.5rem',
textAlign: 'center',
}}>
<div style={{ color: '#334155', fontFamily: 'monospace', fontSize: '0.8rem' }}>
No compliance data upload a report to get started
</div>
</div>
) : 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 (
<CategoryFilterBar
categories={nonMetricCategories}
activeFilter={filterState?.type === 'nonmetric' ? filterState.id : null}
onFilterSelect={(metricId) => {
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 && <ComplianceChartsPanel />}
{/* ── Device table ─────────────────────────────────────────── */}
{!vclView && <div style={{
background: 'linear-gradient(135deg,rgba(30,41,59,0.95) 0%,rgba(15,23,42,0.98) 100%)',
border: '1px solid rgba(20,184,166,0.15)', borderRadius: '0.5rem',
overflow: 'hidden',
}}>
{/* Table toolbar */}
<div style={{
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
padding: '0.875rem 1rem', borderBottom: '1px solid rgba(255,255,255,0.05)',
}}>
{/* Active / Resolved tabs */}
<div style={{ display: 'flex', gap: '0.25rem', alignItems: 'center' }}>
{['active', 'resolved'].map(tab => {
const isActive = activeTab === tab;
return (
<button key={tab} onClick={() => setActiveTab(tab)}
style={{
padding: '0.35rem 0.875rem', cursor: 'pointer',
fontFamily: 'monospace', fontSize: '0.72rem', fontWeight: '600',
textTransform: 'uppercase', letterSpacing: '0.04em',
borderRadius: '0.25rem',
border: isActive ? `1px solid ${TEAL}60` : '1px solid transparent',
background: isActive ? `${TEAL}12` : 'transparent',
color: isActive ? TEAL : '#475569',
}}>
{tab}
{isActive && (
<span style={{ marginLeft: '0.4rem', color: '#64748B' }}>
({loading ? '…' : filteredDevices.length})
</span>
)}
</button>
);
})}
{selectedDevices.size > 0 && canWrite() && (
<button
onClick={openGraniteLoader}
title="Generate Granite Loader Sheet from selected devices"
style={{
marginLeft: '0.75rem',
padding: '0.35rem 0.75rem',
display: 'flex', alignItems: 'center', gap: '0.35rem',
background: 'rgba(124, 58, 237, 0.12)',
border: '1px solid rgba(124, 58, 237, 0.5)',
borderRadius: '0.25rem',
color: '#A78BFA',
cursor: 'pointer',
fontFamily: 'monospace', fontSize: '0.72rem', fontWeight: '600',
textTransform: 'uppercase', letterSpacing: '0.04em',
transition: 'all 0.15s',
}}
onMouseEnter={e => { e.currentTarget.style.background = 'rgba(124, 58, 237, 0.2)'; e.currentTarget.style.borderColor = 'rgba(124, 58, 237, 0.8)'; }}
onMouseLeave={e => { e.currentTarget.style.background = 'rgba(124, 58, 237, 0.12)'; e.currentTarget.style.borderColor = 'rgba(124, 58, 237, 0.5)'; }}
>
<FileSpreadsheet style={{ width: '13px', height: '13px' }} />
Granite ({selectedDevices.size})
</button>
)}
</div>
{/* Hostname search */}
<input
value={hostSearch}
onChange={e => { setHostSearch(e.target.value); setCurrentPage(1); }}
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)'}
/>
</div>
{/* Column headers */}
<div style={{
display: 'grid',
gridTemplateColumns: '0.3fr 2fr 1fr 0.8fr 1.8fr 1fr 1.2fr 0.5fr 0.4fr',
padding: '0.5rem 1rem',
borderBottom: '1px solid rgba(255,255,255,0.05)',
fontSize: '0.62rem', color: '#334155',
fontFamily: 'monospace', textTransform: 'uppercase', letterSpacing: '0.08em',
alignItems: 'center',
}}>
<span style={{ display: 'flex', justifyContent: 'center' }}>
<input
type="checkbox"
checked={paginatedDevices.length > 0 && paginatedDevices.every(d => selectedDevices.has(d.hostname))}
onChange={toggleSelectAll}
style={{ cursor: 'pointer', accentColor: TEAL }}
title="Select all on this page"
/>
</span>
<span>Hostname</span>
<span>IP Address</span>
<span>Type</span>
<span>Failing Metrics</span>
<span>Resolution Date</span>
<span>Remediation Plan</span>
<span>Seen</span>
<span></span>
</div>
{/* Rows */}
{loading ? (
<div style={{ padding: '3rem', textAlign: 'center' }}>
<Loader style={{ width: '28px', height: '28px', color: TEAL, margin: '0 auto', animation: 'spin 1s linear infinite' }} />
</div>
) : error ? (
<div style={{ padding: '2rem', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '0.5rem', color: '#F87171', fontSize: '0.8rem' }}>
<AlertCircle style={{ width: '16px', height: '16px' }} />{error}
</div>
) : filteredDevices.length === 0 ? (
<div style={{ padding: '3rem', textAlign: 'center', color: '#334155', fontFamily: 'monospace', fontSize: '0.8rem' }}>
{lastUpload === null ? 'No reports uploaded yet' : filterState ? 'No devices match the selected filter' : activeTab === 'active' ? 'No non-compliant devices' : 'No resolved items'}
</div>
) : (
paginatedDevices.map(device => (
<DeviceRow
key={device.hostname}
device={device}
selected={selectedHost === device.hostname}
checked={selectedDevices.has(device.hostname)}
onCheck={() => toggleDeviceSelection(device.hostname)}
onClick={() => setSelectedHost(selectedHost === device.hostname ? null : device.hostname)}
/>
))
)}
{/* Pagination controls */}
{filteredDevices.length > 0 && (
<div style={{
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
padding: '0.75rem 1rem', borderTop: '1px solid rgba(255,255,255,0.05)',
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
<span style={{ fontSize: '0.68rem', color: '#475569', fontFamily: 'monospace' }}>
Showing {((safePage - 1) * pageSize) + 1}{Math.min(safePage * pageSize, filteredDevices.length)} of {filteredDevices.length}
</span>
<select
value={pageSize}
onChange={e => { setPageSize(Number(e.target.value)); setCurrentPage(1); }}
style={{
background: 'rgba(15,23,42,0.8)', border: '1px solid rgba(20,184,166,0.2)',
borderRadius: '0.25rem', color: '#94A3B8', fontSize: '0.68rem',
fontFamily: 'monospace', padding: '0.2rem 0.4rem', cursor: 'pointer',
}}
>
<option value={25}>25</option>
<option value={50}>50</option>
<option value={100}>100</option>
<option value={200}>200</option>
</select>
<span style={{ fontSize: '0.68rem', color: '#334155', fontFamily: 'monospace' }}>per page</span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.25rem' }}>
<button
onClick={() => setCurrentPage(p => Math.max(1, p - 1))}
disabled={safePage <= 1}
style={{
padding: '0.3rem 0.6rem', borderRadius: '0.25rem',
border: '1px solid rgba(20,184,166,0.2)', background: 'transparent',
color: safePage <= 1 ? '#1E293B' : '#94A3B8', cursor: safePage <= 1 ? 'default' : 'pointer',
fontFamily: 'monospace', fontSize: '0.72rem',
}}
>
</button>
<span style={{ fontSize: '0.68rem', color: '#64748B', fontFamily: 'monospace', padding: '0 0.5rem' }}>
{safePage} / {totalPages}
</span>
<button
onClick={() => setCurrentPage(p => Math.min(totalPages, p + 1))}
disabled={safePage >= totalPages}
style={{
padding: '0.3rem 0.6rem', borderRadius: '0.25rem',
border: '1px solid rgba(20,184,166,0.2)', background: 'transparent',
color: safePage >= totalPages ? '#1E293B' : '#94A3B8', cursor: safePage >= totalPages ? 'default' : 'pointer',
fontFamily: 'monospace', fontSize: '0.72rem',
}}
>
</button>
</div>
</div>
)}
</div>}
{/* ── Detail panel ─────────────────────────────────────────── */}
{selectedHost && (
<ComplianceDetailPanel
hostname={selectedHost}
onClose={() => setSelectedHost(null)}
onNoteAdded={refresh}
onMetadataSaved={refresh}
onNavigate={onNavigate}
/>
)}
{/* ── Upload modal ─────────────────────────────────────────── */}
{showUpload && (
<ComplianceUploadModal
onClose={() => setShowUpload(false)}
onUploadComplete={() => { setShowUpload(false); refresh(); }}
/>
)}
{/* ── Metric info panel ───────────────────────────────────── */}
{infoMetric && (
<MetricInfoPanel
metricId={infoMetric}
definition={METRIC_DEFINITIONS[infoMetric] || null}
summaryEntries={(families.find(f => f.metricId === infoMetric) || {}).entries || []}
onClose={() => setInfoMetric(null)}
/>
)}
{/* ── Rollback confirmation modal ──────────────────────────── */}
{rollbackConfirm && lastUpload && (
<div style={{
position: 'fixed', inset: 0, zIndex: 60,
background: 'rgba(10, 14, 39, 0.95)',
backdropFilter: 'blur(8px)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
padding: '1rem',
}}>
<div style={{
background: 'linear-gradient(135deg, rgba(30,41,59,0.98) 0%, rgba(15,23,42,0.99) 100%)',
border: '1px solid rgba(239,68,68,0.3)',
borderRadius: '0.75rem',
boxShadow: '0 20px 60px rgba(0,0,0,0.7)',
width: '100%', maxWidth: '420px',
padding: '2rem',
}}>
<div style={{ fontFamily: 'monospace', fontSize: '1rem', fontWeight: '700', color: '#EF4444', textTransform: 'uppercase', letterSpacing: '0.1em', marginBottom: '1rem' }}>
Rollback Upload
</div>
<div style={{ fontSize: '0.8rem', color: '#CBD5E1', lineHeight: '1.5', marginBottom: '0.5rem' }}>
This will reverse the most recent upload:
</div>
<div style={{
fontFamily: 'monospace', fontSize: '0.75rem', color: '#94A3B8',
background: 'rgba(15,23,42,0.6)', borderRadius: '0.375rem',
padding: '0.625rem 0.75rem', marginBottom: '1.25rem',
border: '1px solid rgba(239,68,68,0.15)',
}}>
<div><span style={{ color: '#64748B' }}>File:</span> {lastUpload.report_date || 'unknown date'}</div>
<div style={{ marginTop: '0.25rem', fontSize: '0.68rem', color: '#475569' }}>
New items will be deleted, resolved items will be reactivated, and the upload record will be removed.
</div>
</div>
<div style={{ display: 'flex', gap: '0.75rem' }}>
<button
onClick={() => setRollbackConfirm(false)}
style={{
flex: 1, padding: '0.625rem', background: 'transparent',
border: '1px solid rgba(100,116,139,0.4)', borderRadius: '0.375rem',
color: '#64748B', cursor: 'pointer',
fontFamily: 'monospace', fontSize: '0.8rem',
}}
onMouseEnter={e => e.currentTarget.style.borderColor = 'rgba(100,116,139,0.8)'}
onMouseLeave={e => e.currentTarget.style.borderColor = 'rgba(100,116,139,0.4)'}>
Cancel
</button>
<button
onClick={handleRollback}
disabled={rollbackLoading}
style={{
flex: 2, padding: '0.625rem',
background: rollbackLoading ? 'rgba(239,68,68,0.05)' : 'rgba(239,68,68,0.1)',
border: '1px solid #EF4444',
borderRadius: '0.375rem',
color: '#EF4444', cursor: rollbackLoading ? 'wait' : 'pointer',
fontFamily: 'monospace', fontSize: '0.8rem',
fontWeight: '600', textTransform: 'uppercase', letterSpacing: '0.05em',
display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '0.4rem',
opacity: rollbackLoading ? 0.6 : 1,
}}
onMouseEnter={e => { if (!rollbackLoading) e.currentTarget.style.background = 'rgba(239,68,68,0.18)'; }}
onMouseLeave={e => { if (!rollbackLoading) e.currentTarget.style.background = 'rgba(239,68,68,0.1)'; }}>
{rollbackLoading
? <><Loader style={{ width: '14px', height: '14px', animation: 'spin 1s linear infinite' }} /> Rolling back</>
: <><RotateCcw style={{ width: '14px', height: '14px' }} /> Confirm Rollback</>
}
</button>
</div>
</div>
</div>
)}
{/* ── Rollback result toast ────────────────────────────────── */}
{rollbackResult && (
<div style={{
position: 'fixed', bottom: '1.5rem', right: '1.5rem', zIndex: 70,
background: rollbackResult.error
? 'linear-gradient(135deg, rgba(30,41,59,0.98) 0%, rgba(15,23,42,0.99) 100%)'
: 'linear-gradient(135deg, rgba(30,41,59,0.98) 0%, rgba(15,23,42,0.99) 100%)',
border: `1px solid ${rollbackResult.error ? 'rgba(239,68,68,0.4)' : 'rgba(16,185,129,0.4)'}`,
borderRadius: '0.5rem',
boxShadow: '0 8px 32px rgba(0,0,0,0.5)',
padding: '0.875rem 1.25rem',
maxWidth: '360px',
fontFamily: 'monospace', fontSize: '0.75rem',
color: rollbackResult.error ? '#F87171' : '#10B981',
cursor: 'pointer',
}}
onClick={() => setRollbackResult(null)}
>
{rollbackResult.error ? (
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
<AlertCircle style={{ width: '16px', height: '16px', flexShrink: 0 }} />
{rollbackResult.error}
</div>
) : (
<>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', marginBottom: '0.25rem' }}>
<RotateCcw style={{ width: '14px', height: '14px' }} />
{rollbackResult.message}
</div>
{rollbackResult.rolled_back && (
<div style={{ fontSize: '0.68rem', color: '#64748B' }}>
{rollbackResult.rolled_back.items_deleted} items deleted, {rollbackResult.rolled_back.items_reactivated} reactivated
</div>
)}
</>
)}
</div>
)}
{/* ── Granite Loader Modal ─────────────────────────────────── */}
<LoaderModal
isOpen={showLoaderModal}
onClose={() => setShowLoaderModal(false)}
initialDevices={showLoaderModal ? getLoaderDevices() : null}
/>
</div>
);
}
function DeviceRow({ device, selected, checked, onCheck, onClick }) {
const truncateText = (text, maxLen = 80) => {
if (!text) return '—';
return text.length > maxLen ? text.slice(0, maxLen) + '…' : text;
};
return (
<div
onClick={onClick}
style={{
display: 'grid',
gridTemplateColumns: '0.3fr 2fr 1fr 0.8fr 1.8fr 1fr 1.2fr 0.5fr 0.4fr',
padding: '0.625rem 1rem',
borderBottom: '1px solid rgba(255,255,255,0.04)',
cursor: 'pointer',
background: selected ? `${TEAL}08` : 'transparent',
borderLeft: selected ? `2px solid ${TEAL}` : '2px solid transparent',
transition: 'all 0.15s',
alignItems: 'center',
}}
onMouseEnter={e => { if (!selected) e.currentTarget.style.background = 'rgba(255,255,255,0.025)'; }}
onMouseLeave={e => { if (!selected) e.currentTarget.style.background = 'transparent'; }}
>
{/* Checkbox */}
<div style={{ display: 'flex', justifyContent: 'center' }} onClick={e => e.stopPropagation()}>
<input
type="checkbox"
checked={checked}
onChange={onCheck}
style={{ cursor: 'pointer', accentColor: TEAL }}
/>
</div>
{/* Hostname */}
<div style={{ fontFamily: 'monospace', fontSize: '0.78rem', color: selected ? TEAL : '#E2E8F0', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
{device.hostname}
</div>
{/* IP */}
<div style={{ fontFamily: 'monospace', fontSize: '0.72rem', color: '#64748B' }}>
{device.ip_address || '—'}
</div>
{/* Type */}
<div style={{ fontSize: '0.7rem', color: '#475569', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
{device.device_type || '—'}
</div>
{/* Failing metrics */}
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.25rem' }}>
{device.failing_metrics.map(m => (
<MetricBadge key={m.metric_id} metricId={m.metric_id} category={m.category} />
))}
</div>
{/* Resolution Date */}
<div style={{ fontFamily: 'monospace', fontSize: '0.72rem', color: '#94A3B8' }}>
{device.resolution_date ? device.resolution_date.slice(0, 10) : '—'}
</div>
{/* Remediation Plan */}
<div style={{ fontSize: '0.7rem', color: '#94A3B8', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }} title={device.remediation_plan || ''}>
{truncateText(device.remediation_plan)}
</div>
{/* Seen count */}
<div>
<SeenBadge count={device.seen_count} />
</div>
{/* Notes indicator */}
<div style={{ display: 'flex', justifyContent: 'center' }}>
{device.has_notes && (
<MessageSquare style={{ width: '13px', height: '13px', color: TEAL, opacity: 0.7 }} />
)}
</div>
</div>
);
}
// Named exports for testing
export { computeWorstStatus, groupByMetricFamily, deriveNonMetricCategories, CATEGORY_COLORS };