2026-04-22 18:30:59 +00:00
|
|
|
|
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
|
|
|
|
|
import { Upload, MessageSquare, RefreshCw, AlertCircle, Loader, RotateCcw, Info } from 'lucide-react';
|
feat(compliance): add AEO compliance frontend
- CompliancePage: team tabs (STEAM/ACCESS-ENG), metric health cards with
click-to-filter, device table with Active/Resolved tabs, hostname search,
seen-count badges, notes indicator, empty/loading/error states
- ComplianceUploadModal: phased flow (idle→upload→preview→commit→done),
drag-and-drop xlsx drop zone, diff summary before commit
- ComplianceDetailPanel: slide-out panel with failing metrics, surfaced
extra fields (CVEs, SLA, OS, Splunk), upload history, notes timeline,
per-metric note add with Ctrl+Enter submit
- NavDrawer: add Compliance nav item (teal, ShieldCheck icon)
- App.js: import and render CompliancePage on compliance route
- Fix SQL join bug in compliance route (lu ON upload_id = lu.id)
- Fix groupByHostname to use max last_seen across all metric rows
2026-03-31 15:14:51 -06:00
|
|
|
|
import { useAuth } from '../../contexts/AuthContext';
|
|
|
|
|
|
import ComplianceUploadModal from './ComplianceUploadModal';
|
|
|
|
|
|
import ComplianceDetailPanel from './ComplianceDetailPanel';
|
2026-04-02 09:49:32 -06:00
|
|
|
|
import ComplianceChartsPanel from './ComplianceChartsPanel';
|
2026-04-22 18:30:59 +00:00
|
|
|
|
import MetricInfoPanel from './MetricInfoPanel';
|
2026-05-11 15:48:10 -06:00
|
|
|
|
import VCLReportPage from './VCLReportPage';
|
2026-04-22 18:30:59 +00:00
|
|
|
|
import metricDefinitionsRaw from '../../data/metricDefinitions.json';
|
feat(compliance): add AEO compliance frontend
- CompliancePage: team tabs (STEAM/ACCESS-ENG), metric health cards with
click-to-filter, device table with Active/Resolved tabs, hostname search,
seen-count badges, notes indicator, empty/loading/error states
- ComplianceUploadModal: phased flow (idle→upload→preview→commit→done),
drag-and-drop xlsx drop zone, diff summary before commit
- ComplianceDetailPanel: slide-out panel with failing metrics, surfaced
extra fields (CVEs, SLA, OS, Splunk), upload history, notes timeline,
per-metric note add with Ctrl+Enter submit
- NavDrawer: add Compliance nav item (teal, ShieldCheck icon)
- App.js: import and render CompliancePage on compliance route
- Fix SQL join bug in compliance route (lu ON upload_id = lu.id)
- Fix groupByHostname to use max last_seen across all metric rows
2026-03-31 15:14:51 -06:00
|
|
|
|
|
|
|
|
|
|
const API_BASE = process.env.REACT_APP_API_BASE || 'http://localhost:3001/api';
|
|
|
|
|
|
const TEAL = '#14B8A6';
|
|
|
|
|
|
|
2026-04-22 18:30:59 +00:00
|
|
|
|
// Build definitions lookup map once at module level
|
|
|
|
|
|
const METRIC_DEFINITIONS = {};
|
|
|
|
|
|
for (const def of metricDefinitionsRaw) {
|
|
|
|
|
|
METRIC_DEFINITIONS[def.metric_id] = def;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
feat(compliance): add AEO compliance frontend
- CompliancePage: team tabs (STEAM/ACCESS-ENG), metric health cards with
click-to-filter, device table with Active/Resolved tabs, hostname search,
seen-count badges, notes indicator, empty/loading/error states
- ComplianceUploadModal: phased flow (idle→upload→preview→commit→done),
drag-and-drop xlsx drop zone, diff summary before commit
- ComplianceDetailPanel: slide-out panel with failing metrics, surfaced
extra fields (CVEs, SLA, OS, Splunk), upload history, notes timeline,
per-metric note add with Ctrl+Enter submit
- NavDrawer: add Compliance nav item (teal, ShieldCheck icon)
- App.js: import and render CompliancePage on compliance route
- Fix SQL join bug in compliance route (lu ON upload_id = lu.id)
- Fix groupByHostname to use max last_seen across all metric rows
2026-03-31 15:14:51 -06:00
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
// 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)}%`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-22 18:30:59 +00:00
|
|
|
|
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)),
|
|
|
|
|
|
}));
|
feat(compliance): add AEO compliance frontend
- CompliancePage: team tabs (STEAM/ACCESS-ENG), metric health cards with
click-to-filter, device table with Active/Resolved tabs, hostname search,
seen-count badges, notes indicator, empty/loading/error states
- ComplianceUploadModal: phased flow (idle→upload→preview→commit→done),
drag-and-drop xlsx drop zone, diff summary before commit
- ComplianceDetailPanel: slide-out panel with failing metrics, surfaced
extra fields (CVEs, SLA, OS, Splunk), upload history, notes timeline,
per-metric note add with Ctrl+Enter submit
- NavDrawer: add Compliance nav item (teal, ShieldCheck icon)
- App.js: import and render CompliancePage on compliance route
- Fix SQL join bug in compliance route (lu ON upload_id = lu.id)
- Fix groupByHostname to use max last_seen across all metric rows
2026-03-31 15:14:51 -06:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
// Sub-components
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
2026-04-22 18:37:54 +00:00
|
|
|
|
function VariantPill({ entry, label }) {
|
feat(compliance): add AEO compliance frontend
- CompliancePage: team tabs (STEAM/ACCESS-ENG), metric health cards with
click-to-filter, device table with Active/Resolved tabs, hostname search,
seen-count badges, notes indicator, empty/loading/error states
- ComplianceUploadModal: phased flow (idle→upload→preview→commit→done),
drag-and-drop xlsx drop zone, diff summary before commit
- ComplianceDetailPanel: slide-out panel with failing metrics, surfaced
extra fields (CVEs, SLA, OS, Splunk), upload history, notes timeline,
per-metric note add with Ctrl+Enter submit
- NavDrawer: add Compliance nav item (teal, ShieldCheck icon)
- App.js: import and render CompliancePage on compliance route
- Fix SQL join bug in compliance route (lu ON upload_id = lu.id)
- Fix groupByHostname to use max last_seen across all metric rows
2026-03-31 15:14:51 -06:00
|
|
|
|
const color = statusColor(entry.status);
|
2026-04-22 18:30:59 +00:00
|
|
|
|
const isOk = entry.status === 'Meets/Exceeds Target';
|
2026-05-26 11:48:53 -06:00
|
|
|
|
const hasRawCounts = entry.compliant != null && entry.total != null && entry.total > 0;
|
2026-04-22 18:30:59 +00:00
|
|
|
|
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}`,
|
|
|
|
|
|
}} />
|
|
|
|
|
|
)}
|
2026-04-22 18:37:54 +00:00
|
|
|
|
{label && <span style={{ color: '#94A3B8' }}>{label}</span>}
|
2026-04-22 18:30:59 +00:00
|
|
|
|
<span style={{ color, fontWeight: '600' }}>{pctDisplay(entry.compliance_pct)}</span>
|
2026-05-26 11:48:53 -06:00
|
|
|
|
{hasRawCounts && (
|
|
|
|
|
|
<span style={{ color: '#64748B', fontSize: '0.58rem' }}>({entry.compliant}/{entry.total})</span>
|
|
|
|
|
|
)}
|
2026-04-22 18:30:59 +00:00
|
|
|
|
</span>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function MetricHealthCard({ family, active, onClick, onInfoClick, definitionLookup }) {
|
|
|
|
|
|
const color = statusColor(family.worstStatus);
|
|
|
|
|
|
const isOk = family.worstStatus === 'Meets/Exceeds Target';
|
feat(compliance): add AEO compliance frontend
- CompliancePage: team tabs (STEAM/ACCESS-ENG), metric health cards with
click-to-filter, device table with Active/Resolved tabs, hostname search,
seen-count badges, notes indicator, empty/loading/error states
- ComplianceUploadModal: phased flow (idle→upload→preview→commit→done),
drag-and-drop xlsx drop zone, diff summary before commit
- ComplianceDetailPanel: slide-out panel with failing metrics, surfaced
extra fields (CVEs, SLA, OS, Splunk), upload history, notes timeline,
per-metric note add with Ctrl+Enter submit
- NavDrawer: add Compliance nav item (teal, ShieldCheck icon)
- App.js: import and render CompliancePage on compliance route
- Fix SQL join bug in compliance route (lu ON upload_id = lu.id)
- Fix groupByHostname to use max last_seen across all metric rows
2026-03-31 15:14:51 -06:00
|
|
|
|
|
|
|
|
|
|
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',
|
2026-04-22 18:30:59 +00:00
|
|
|
|
position: 'relative',
|
feat(compliance): add AEO compliance frontend
- CompliancePage: team tabs (STEAM/ACCESS-ENG), metric health cards with
click-to-filter, device table with Active/Resolved tabs, hostname search,
seen-count badges, notes indicator, empty/loading/error states
- ComplianceUploadModal: phased flow (idle→upload→preview→commit→done),
drag-and-drop xlsx drop zone, diff summary before commit
- ComplianceDetailPanel: slide-out panel with failing metrics, surfaced
extra fields (CVEs, SLA, OS, Splunk), upload history, notes timeline,
per-metric note add with Ctrl+Enter submit
- NavDrawer: add Compliance nav item (teal, ShieldCheck icon)
- App.js: import and render CompliancePage on compliance route
- Fix SQL join bug in compliance route (lu ON upload_id = lu.id)
- Fix groupByHostname to use max last_seen across all metric rows
2026-03-31 15:14:51 -06:00
|
|
|
|
}}
|
|
|
|
|
|
onMouseEnter={e => { if (!active) e.currentTarget.style.borderColor = color + '80'; }}
|
2026-04-22 18:30:59 +00:00
|
|
|
|
onMouseLeave={e => { if (!active) e.currentTarget.style.borderColor = active ? color : color + '40'; }}
|
feat(compliance): add AEO compliance frontend
- CompliancePage: team tabs (STEAM/ACCESS-ENG), metric health cards with
click-to-filter, device table with Active/Resolved tabs, hostname search,
seen-count badges, notes indicator, empty/loading/error states
- ComplianceUploadModal: phased flow (idle→upload→preview→commit→done),
drag-and-drop xlsx drop zone, diff summary before commit
- ComplianceDetailPanel: slide-out panel with failing metrics, surfaced
extra fields (CVEs, SLA, OS, Splunk), upload history, notes timeline,
per-metric note add with Ctrl+Enter submit
- NavDrawer: add Compliance nav item (teal, ShieldCheck icon)
- App.js: import and render CompliancePage on compliance route
- Fix SQL join bug in compliance route (lu ON upload_id = lu.id)
- Fix groupByHostname to use max last_seen across all metric rows
2026-03-31 15:14:51 -06:00
|
|
|
|
>
|
2026-04-22 18:30:59 +00:00
|
|
|
|
{/* 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>
|
|
|
|
|
|
|
feat(compliance): add AEO compliance frontend
- CompliancePage: team tabs (STEAM/ACCESS-ENG), metric health cards with
click-to-filter, device table with Active/Resolved tabs, hostname search,
seen-count badges, notes indicator, empty/loading/error states
- ComplianceUploadModal: phased flow (idle→upload→preview→commit→done),
drag-and-drop xlsx drop zone, diff summary before commit
- ComplianceDetailPanel: slide-out panel with failing metrics, surfaced
extra fields (CVEs, SLA, OS, Splunk), upload history, notes timeline,
per-metric note add with Ctrl+Enter submit
- NavDrawer: add Compliance nav item (teal, ShieldCheck icon)
- App.js: import and render CompliancePage on compliance route
- Fix SQL join bug in compliance route (lu ON upload_id = lu.id)
- Fix groupByHostname to use max last_seen across all metric rows
2026-03-31 15:14:51 -06:00
|
|
|
|
{/* Metric ID */}
|
2026-04-22 18:30:59 +00:00
|
|
|
|
<div style={{ fontFamily: 'monospace', fontSize: '0.95rem', fontWeight: '700', color: active ? color : '#E2E8F0', marginBottom: '0.25rem', paddingRight: '1.25rem' }}>
|
|
|
|
|
|
{family.metricId}
|
feat(compliance): add AEO compliance frontend
- CompliancePage: team tabs (STEAM/ACCESS-ENG), metric health cards with
click-to-filter, device table with Active/Resolved tabs, hostname search,
seen-count badges, notes indicator, empty/loading/error states
- ComplianceUploadModal: phased flow (idle→upload→preview→commit→done),
drag-and-drop xlsx drop zone, diff summary before commit
- ComplianceDetailPanel: slide-out panel with failing metrics, surfaced
extra fields (CVEs, SLA, OS, Splunk), upload history, notes timeline,
per-metric note add with Ctrl+Enter submit
- NavDrawer: add Compliance nav item (teal, ShieldCheck icon)
- App.js: import and render CompliancePage on compliance route
- Fix SQL join bug in compliance route (lu ON upload_id = lu.id)
- Fix groupByHostname to use max last_seen across all metric rows
2026-03-31 15:14:51 -06:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Category */}
|
2026-04-22 18:30:59 +00:00
|
|
|
|
<div style={{ fontSize: '0.65rem', color: '#475569', marginBottom: '0.5rem', textTransform: 'uppercase', letterSpacing: '0.05em' }}>
|
|
|
|
|
|
{family.category}
|
feat(compliance): add AEO compliance frontend
- CompliancePage: team tabs (STEAM/ACCESS-ENG), metric health cards with
click-to-filter, device table with Active/Resolved tabs, hostname search,
seen-count badges, notes indicator, empty/loading/error states
- ComplianceUploadModal: phased flow (idle→upload→preview→commit→done),
drag-and-drop xlsx drop zone, diff summary before commit
- ComplianceDetailPanel: slide-out panel with failing metrics, surfaced
extra fields (CVEs, SLA, OS, Splunk), upload history, notes timeline,
per-metric note add with Ctrl+Enter submit
- NavDrawer: add Compliance nav item (teal, ShieldCheck icon)
- App.js: import and render CompliancePage on compliance route
- Fix SQL join bug in compliance route (lu ON upload_id = lu.id)
- Fix groupByHostname to use max last_seen across all metric rows
2026-03-31 15:14:51 -06:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-04-22 18:30:59 +00:00
|
|
|
|
{/* Variant pills */}
|
|
|
|
|
|
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.3rem', marginBottom: '0.5rem' }}>
|
2026-04-22 18:37:54 +00:00
|
|
|
|
{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} />;
|
|
|
|
|
|
})}
|
feat(compliance): add AEO compliance frontend
- CompliancePage: team tabs (STEAM/ACCESS-ENG), metric health cards with
click-to-filter, device table with Active/Resolved tabs, hostname search,
seen-count badges, notes indicator, empty/loading/error states
- ComplianceUploadModal: phased flow (idle→upload→preview→commit→done),
drag-and-drop xlsx drop zone, diff summary before commit
- ComplianceDetailPanel: slide-out panel with failing metrics, surfaced
extra fields (CVEs, SLA, OS, Splunk), upload history, notes timeline,
per-metric note add with Ctrl+Enter submit
- NavDrawer: add Compliance nav item (teal, ShieldCheck icon)
- App.js: import and render CompliancePage on compliance route
- Fix SQL join bug in compliance route (lu ON upload_id = lu.id)
- Fix groupByHostname to use max last_seen across all metric rows
2026-03-31 15:14:51 -06:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Target */}
|
2026-04-22 18:30:59 +00:00
|
|
|
|
<div style={{ fontSize: '0.68rem', color: '#475569', fontFamily: 'monospace', marginBottom: '0.5rem' }}>
|
|
|
|
|
|
target {pctDisplay(family.target)}
|
feat(compliance): add AEO compliance frontend
- CompliancePage: team tabs (STEAM/ACCESS-ENG), metric health cards with
click-to-filter, device table with Active/Resolved tabs, hostname search,
seen-count badges, notes indicator, empty/loading/error states
- ComplianceUploadModal: phased flow (idle→upload→preview→commit→done),
drag-and-drop xlsx drop zone, diff summary before commit
- ComplianceDetailPanel: slide-out panel with failing metrics, surfaced
extra fields (CVEs, SLA, OS, Splunk), upload history, notes timeline,
per-metric note add with Ctrl+Enter submit
- NavDrawer: add Compliance nav item (teal, ShieldCheck icon)
- App.js: import and render CompliancePage on compliance route
- Fix SQL join bug in compliance route (lu ON upload_id = lu.id)
- Fix groupByHostname to use max last_seen across all metric rows
2026-03-31 15:14:51 -06:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Status pill */}
|
|
|
|
|
|
<div style={{
|
2026-04-22 18:30:59 +00:00
|
|
|
|
display: 'inline-flex', alignItems: 'center', gap: '0.3rem',
|
feat(compliance): add AEO compliance frontend
- CompliancePage: team tabs (STEAM/ACCESS-ENG), metric health cards with
click-to-filter, device table with Active/Resolved tabs, hostname search,
seen-count badges, notes indicator, empty/loading/error states
- ComplianceUploadModal: phased flow (idle→upload→preview→commit→done),
drag-and-drop xlsx drop zone, diff summary before commit
- ComplianceDetailPanel: slide-out panel with failing metrics, surfaced
extra fields (CVEs, SLA, OS, Splunk), upload history, notes timeline,
per-metric note add with Ctrl+Enter submit
- NavDrawer: add Compliance nav item (teal, ShieldCheck icon)
- App.js: import and render CompliancePage on compliance route
- Fix SQL join bug in compliance route (lu ON upload_id = lu.id)
- Fix groupByHostname to use max last_seen across all metric rows
2026-03-31 15:14:51 -06:00
|
|
|
|
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}` }),
|
|
|
|
|
|
}} />
|
2026-04-22 18:30:59 +00:00
|
|
|
|
{isOk ? 'OK' : family.worstStatus.replace(' of Target', '')}
|
feat(compliance): add AEO compliance frontend
- CompliancePage: team tabs (STEAM/ACCESS-ENG), metric health cards with
click-to-filter, device table with Active/Resolved tabs, hostname search,
seen-count badges, notes indicator, empty/loading/error states
- ComplianceUploadModal: phased flow (idle→upload→preview→commit→done),
drag-and-drop xlsx drop zone, diff summary before commit
- ComplianceDetailPanel: slide-out panel with failing metrics, surfaced
extra fields (CVEs, SLA, OS, Splunk), upload history, notes timeline,
per-metric note add with Ctrl+Enter submit
- NavDrawer: add Compliance nav item (teal, ShieldCheck icon)
- App.js: import and render CompliancePage on compliance route
- Fix SQL join bug in compliance route (lu ON upload_id = lu.id)
- Fix groupByHostname to use max last_seen across all metric rows
2026-03-31 15:14:51 -06:00
|
|
|
|
</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>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
// Main Page
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
2026-04-01 09:20:30 -06:00
|
|
|
|
export default function CompliancePage({ onNavigate }) {
|
2026-05-05 11:04:53 -06:00
|
|
|
|
const { canWrite, isAdmin, getAvailableTeams, adminScope } = useAuth();
|
feat(compliance): add AEO compliance frontend
- CompliancePage: team tabs (STEAM/ACCESS-ENG), metric health cards with
click-to-filter, device table with Active/Resolved tabs, hostname search,
seen-count badges, notes indicator, empty/loading/error states
- ComplianceUploadModal: phased flow (idle→upload→preview→commit→done),
drag-and-drop xlsx drop zone, diff summary before commit
- ComplianceDetailPanel: slide-out panel with failing metrics, surfaced
extra fields (CVEs, SLA, OS, Splunk), upload history, notes timeline,
per-metric note add with Ctrl+Enter submit
- NavDrawer: add Compliance nav item (teal, ShieldCheck icon)
- App.js: import and render CompliancePage on compliance route
- Fix SQL join bug in compliance route (lu ON upload_id = lu.id)
- Fix groupByHostname to use max last_seen across all metric rows
2026-03-31 15:14:51 -06:00
|
|
|
|
|
2026-05-05 11:04:53 -06:00
|
|
|
|
const availableTeams = getAvailableTeams();
|
|
|
|
|
|
const [activeTeam, setActiveTeam] = useState(() => availableTeams[0] || 'STEAM');
|
feat(compliance): add AEO compliance frontend
- CompliancePage: team tabs (STEAM/ACCESS-ENG), metric health cards with
click-to-filter, device table with Active/Resolved tabs, hostname search,
seen-count badges, notes indicator, empty/loading/error states
- ComplianceUploadModal: phased flow (idle→upload→preview→commit→done),
drag-and-drop xlsx drop zone, diff summary before commit
- ComplianceDetailPanel: slide-out panel with failing metrics, surfaced
extra fields (CVEs, SLA, OS, Splunk), upload history, notes timeline,
per-metric note add with Ctrl+Enter submit
- NavDrawer: add Compliance nav item (teal, ShieldCheck icon)
- App.js: import and render CompliancePage on compliance route
- Fix SQL join bug in compliance route (lu ON upload_id = lu.id)
- Fix groupByHostname to use max last_seen across all metric rows
2026-03-31 15:14:51 -06:00
|
|
|
|
const [activeTab, setActiveTab] = useState('active');
|
2026-05-11 15:48:10 -06:00
|
|
|
|
const [vclView, setVclView] = useState(false);
|
feat(compliance): add AEO compliance frontend
- CompliancePage: team tabs (STEAM/ACCESS-ENG), metric health cards with
click-to-filter, device table with Active/Resolved tabs, hostname search,
seen-count badges, notes indicator, empty/loading/error states
- ComplianceUploadModal: phased flow (idle→upload→preview→commit→done),
drag-and-drop xlsx drop zone, diff summary before commit
- ComplianceDetailPanel: slide-out panel with failing metrics, surfaced
extra fields (CVEs, SLA, OS, Splunk), upload history, notes timeline,
per-metric note add with Ctrl+Enter submit
- NavDrawer: add Compliance nav item (teal, ShieldCheck icon)
- App.js: import and render CompliancePage on compliance route
- Fix SQL join bug in compliance route (lu ON upload_id = lu.id)
- Fix groupByHostname to use max last_seen across all metric rows
2026-03-31 15:14:51 -06:00
|
|
|
|
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);
|
2026-04-20 20:12:12 +00:00
|
|
|
|
const [rollbackConfirm, setRollbackConfirm] = useState(false);
|
|
|
|
|
|
const [rollbackLoading, setRollbackLoading] = useState(false);
|
|
|
|
|
|
const [rollbackResult, setRollbackResult] = useState(null);
|
2026-04-22 18:30:59 +00:00
|
|
|
|
const [infoMetric, setInfoMetric] = useState(null);
|
|
|
|
|
|
const [hoveredMetric, setHoveredMetric] = useState(null);
|
|
|
|
|
|
const hoverTimeoutRef = useRef(null);
|
|
|
|
|
|
const hoveredCardRef = useRef(null);
|
feat(compliance): add AEO compliance frontend
- CompliancePage: team tabs (STEAM/ACCESS-ENG), metric health cards with
click-to-filter, device table with Active/Resolved tabs, hostname search,
seen-count badges, notes indicator, empty/loading/error states
- ComplianceUploadModal: phased flow (idle→upload→preview→commit→done),
drag-and-drop xlsx drop zone, diff summary before commit
- ComplianceDetailPanel: slide-out panel with failing metrics, surfaced
extra fields (CVEs, SLA, OS, Splunk), upload history, notes timeline,
per-metric note add with Ctrl+Enter submit
- NavDrawer: add Compliance nav item (teal, ShieldCheck icon)
- App.js: import and render CompliancePage on compliance route
- Fix SQL join bug in compliance route (lu ON upload_id = lu.id)
- Fix groupByHostname to use max last_seen across all metric rows
2026-03-31 15:14:51 -06:00
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
2026-05-05 11:04:53 -06:00
|
|
|
|
// 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
|
|
|
|
|
|
|
feat(compliance): add AEO compliance frontend
- CompliancePage: team tabs (STEAM/ACCESS-ENG), metric health cards with
click-to-filter, device table with Active/Resolved tabs, hostname search,
seen-count badges, notes indicator, empty/loading/error states
- ComplianceUploadModal: phased flow (idle→upload→preview→commit→done),
drag-and-drop xlsx drop zone, diff summary before commit
- ComplianceDetailPanel: slide-out panel with failing metrics, surfaced
extra fields (CVEs, SLA, OS, Splunk), upload history, notes timeline,
per-metric note add with Ctrl+Enter submit
- NavDrawer: add Compliance nav item (teal, ShieldCheck icon)
- App.js: import and render CompliancePage on compliance route
- Fix SQL join bug in compliance route (lu ON upload_id = lu.id)
- Fix groupByHostname to use max last_seen across all metric rows
2026-03-31 15:14:51 -06:00
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
setMetricFilter(null);
|
|
|
|
|
|
fetchDevices(activeTeam, activeTab);
|
|
|
|
|
|
}, [activeTab]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
|
|
|
|
|
|
|
|
|
|
const refresh = () => {
|
|
|
|
|
|
fetchSummary(activeTeam);
|
|
|
|
|
|
fetchDevices(activeTeam, activeTab);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-04-20 20:12:12 +00:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
feat(compliance): add AEO compliance frontend
- CompliancePage: team tabs (STEAM/ACCESS-ENG), metric health cards with
click-to-filter, device table with Active/Resolved tabs, hostname search,
seen-count badges, notes indicator, empty/loading/error states
- ComplianceUploadModal: phased flow (idle→upload→preview→commit→done),
drag-and-drop xlsx drop zone, diff summary before commit
- ComplianceDetailPanel: slide-out panel with failing metrics, surfaced
extra fields (CVEs, SLA, OS, Splunk), upload history, notes timeline,
per-metric note add with Ctrl+Enter submit
- NavDrawer: add Compliance nav item (teal, ShieldCheck icon)
- App.js: import and render CompliancePage on compliance route
- Fix SQL join bug in compliance route (lu ON upload_id = lu.id)
- Fix groupByHostname to use max last_seen across all metric rows
2026-03-31 15:14:51 -06:00
|
|
|
|
// In-memory filters
|
|
|
|
|
|
const filteredDevices = devices
|
2026-04-22 18:30:59 +00:00
|
|
|
|
.filter(d => !metricFilter || d.failing_metrics.some(m => metricFilter.includes(m.metric_id)))
|
feat(compliance): add AEO compliance frontend
- CompliancePage: team tabs (STEAM/ACCESS-ENG), metric health cards with
click-to-filter, device table with Active/Resolved tabs, hostname search,
seen-count badges, notes indicator, empty/loading/error states
- ComplianceUploadModal: phased flow (idle→upload→preview→commit→done),
drag-and-drop xlsx drop zone, diff summary before commit
- ComplianceDetailPanel: slide-out panel with failing metrics, surfaced
extra fields (CVEs, SLA, OS, Splunk), upload history, notes timeline,
per-metric note add with Ctrl+Enter submit
- NavDrawer: add Compliance nav item (teal, ShieldCheck icon)
- App.js: import and render CompliancePage on compliance route
- Fix SQL join bug in compliance route (lu ON upload_id = lu.id)
- Fix groupByHostname to use max last_seen across all metric rows
2026-03-31 15:14:51 -06:00
|
|
|
|
.filter(d => !hostSearch || d.hostname.toLowerCase().includes(hostSearch.toLowerCase()));
|
|
|
|
|
|
|
2026-04-22 18:30:59 +00:00
|
|
|
|
const families = groupByMetricFamily(summary.entries, activeTeam);
|
feat(compliance): add AEO compliance frontend
- CompliancePage: team tabs (STEAM/ACCESS-ENG), metric health cards with
click-to-filter, device table with Active/Resolved tabs, hostname search,
seen-count badges, notes indicator, empty/loading/error states
- ComplianceUploadModal: phased flow (idle→upload→preview→commit→done),
drag-and-drop xlsx drop zone, diff summary before commit
- ComplianceDetailPanel: slide-out panel with failing metrics, surfaced
extra fields (CVEs, SLA, OS, Splunk), upload history, notes timeline,
per-metric note add with Ctrl+Enter submit
- NavDrawer: add Compliance nav item (teal, ShieldCheck icon)
- App.js: import and render CompliancePage on compliance route
- Fix SQL join bug in compliance route (lu ON upload_id = lu.id)
- Fix groupByHostname to use max last_seen across all metric rows
2026-03-31 15:14:51 -06:00
|
|
|
|
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 ? (
|
2026-04-20 20:12:12 +00:00
|
|
|
|
<>
|
|
|
|
|
|
<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>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</>
|
feat(compliance): add AEO compliance frontend
- CompliancePage: team tabs (STEAM/ACCESS-ENG), metric health cards with
click-to-filter, device table with Active/Resolved tabs, hostname search,
seen-count badges, notes indicator, empty/loading/error states
- ComplianceUploadModal: phased flow (idle→upload→preview→commit→done),
drag-and-drop xlsx drop zone, diff summary before commit
- ComplianceDetailPanel: slide-out panel with failing metrics, surfaced
extra fields (CVEs, SLA, OS, Splunk), upload history, notes timeline,
per-metric note add with Ctrl+Enter submit
- NavDrawer: add Compliance nav item (teal, ShieldCheck icon)
- App.js: import and render CompliancePage on compliance route
- Fix SQL join bug in compliance route (lu ON upload_id = lu.id)
- Fix groupByHostname to use max last_seen across all metric rows
2026-03-31 15:14:51 -06:00
|
|
|
|
) : (
|
|
|
|
|
|
<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>
|
2026-05-11 15:48:10 -06:00
|
|
|
|
<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>
|
feat(compliance): add AEO compliance frontend
- CompliancePage: team tabs (STEAM/ACCESS-ENG), metric health cards with
click-to-filter, device table with Active/Resolved tabs, hostname search,
seen-count badges, notes indicator, empty/loading/error states
- ComplianceUploadModal: phased flow (idle→upload→preview→commit→done),
drag-and-drop xlsx drop zone, diff summary before commit
- ComplianceDetailPanel: slide-out panel with failing metrics, surfaced
extra fields (CVEs, SLA, OS, Splunk), upload history, notes timeline,
per-metric note add with Ctrl+Enter submit
- NavDrawer: add Compliance nav item (teal, ShieldCheck icon)
- App.js: import and render CompliancePage on compliance route
- Fix SQL join bug in compliance route (lu ON upload_id = lu.id)
- Fix groupByHostname to use max last_seen across all metric rows
2026-03-31 15:14:51 -06:00
|
|
|
|
{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>
|
|
|
|
|
|
|
2026-05-11 15:48:10 -06:00
|
|
|
|
{/* ── VCL Report View ─────────────────────────────────────── */}
|
|
|
|
|
|
{vclView && (
|
|
|
|
|
|
<VCLReportPage />
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
feat(compliance): add AEO compliance frontend
- CompliancePage: team tabs (STEAM/ACCESS-ENG), metric health cards with
click-to-filter, device table with Active/Resolved tabs, hostname search,
seen-count badges, notes indicator, empty/loading/error states
- ComplianceUploadModal: phased flow (idle→upload→preview→commit→done),
drag-and-drop xlsx drop zone, diff summary before commit
- ComplianceDetailPanel: slide-out panel with failing metrics, surfaced
extra fields (CVEs, SLA, OS, Splunk), upload history, notes timeline,
per-metric note add with Ctrl+Enter submit
- NavDrawer: add Compliance nav item (teal, ShieldCheck icon)
- App.js: import and render CompliancePage on compliance route
- Fix SQL join bug in compliance route (lu ON upload_id = lu.id)
- Fix groupByHostname to use max last_seen across all metric rows
2026-03-31 15:14:51 -06:00
|
|
|
|
{/* ── Team tabs ────────────────────────────────────────────── */}
|
2026-05-11 15:48:10 -06:00
|
|
|
|
{!vclView && availableTeams.length === 0 && !isAdmin() ? (
|
2026-05-05 11:04:53 -06:00
|
|
|
|
<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>
|
2026-05-11 15:48:10 -06:00
|
|
|
|
) : !vclView && (
|
feat(compliance): add AEO compliance frontend
- CompliancePage: team tabs (STEAM/ACCESS-ENG), metric health cards with
click-to-filter, device table with Active/Resolved tabs, hostname search,
seen-count badges, notes indicator, empty/loading/error states
- ComplianceUploadModal: phased flow (idle→upload→preview→commit→done),
drag-and-drop xlsx drop zone, diff summary before commit
- ComplianceDetailPanel: slide-out panel with failing metrics, surfaced
extra fields (CVEs, SLA, OS, Splunk), upload history, notes timeline,
per-metric note add with Ctrl+Enter submit
- NavDrawer: add Compliance nav item (teal, ShieldCheck icon)
- App.js: import and render CompliancePage on compliance route
- Fix SQL join bug in compliance route (lu ON upload_id = lu.id)
- Fix groupByHostname to use max last_seen across all metric rows
2026-03-31 15:14:51 -06:00
|
|
|
|
<div style={{ display: 'flex', gap: '0.375rem', marginBottom: '1.5rem' }}>
|
2026-05-05 11:04:53 -06:00
|
|
|
|
{availableTeams.map(team => {
|
feat(compliance): add AEO compliance frontend
- CompliancePage: team tabs (STEAM/ACCESS-ENG), metric health cards with
click-to-filter, device table with Active/Resolved tabs, hostname search,
seen-count badges, notes indicator, empty/loading/error states
- ComplianceUploadModal: phased flow (idle→upload→preview→commit→done),
drag-and-drop xlsx drop zone, diff summary before commit
- ComplianceDetailPanel: slide-out panel with failing metrics, surfaced
extra fields (CVEs, SLA, OS, Splunk), upload history, notes timeline,
per-metric note add with Ctrl+Enter submit
- NavDrawer: add Compliance nav item (teal, ShieldCheck icon)
- App.js: import and render CompliancePage on compliance route
- Fix SQL join bug in compliance route (lu ON upload_id = lu.id)
- Fix groupByHostname to use max last_seen across all metric rows
2026-03-31 15:14:51 -06:00
|
|
|
|
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>
|
2026-05-05 11:04:53 -06:00
|
|
|
|
)}
|
feat(compliance): add AEO compliance frontend
- CompliancePage: team tabs (STEAM/ACCESS-ENG), metric health cards with
click-to-filter, device table with Active/Resolved tabs, hostname search,
seen-count badges, notes indicator, empty/loading/error states
- ComplianceUploadModal: phased flow (idle→upload→preview→commit→done),
drag-and-drop xlsx drop zone, diff summary before commit
- ComplianceDetailPanel: slide-out panel with failing metrics, surfaced
extra fields (CVEs, SLA, OS, Splunk), upload history, notes timeline,
per-metric note add with Ctrl+Enter submit
- NavDrawer: add Compliance nav item (teal, ShieldCheck icon)
- App.js: import and render CompliancePage on compliance route
- Fix SQL join bug in compliance route (lu ON upload_id = lu.id)
- Fix groupByHostname to use max last_seen across all metric rows
2026-03-31 15:14:51 -06:00
|
|
|
|
|
|
|
|
|
|
{/* ── Metric health cards ──────────────────────────────────── */}
|
2026-05-11 15:48:10 -06:00
|
|
|
|
{!vclView && families.length > 0 ? (
|
feat(compliance): add AEO compliance frontend
- CompliancePage: team tabs (STEAM/ACCESS-ENG), metric health cards with
click-to-filter, device table with Active/Resolved tabs, hostname search,
seen-count badges, notes indicator, empty/loading/error states
- ComplianceUploadModal: phased flow (idle→upload→preview→commit→done),
drag-and-drop xlsx drop zone, diff summary before commit
- ComplianceDetailPanel: slide-out panel with failing metrics, surfaced
extra fields (CVEs, SLA, OS, Splunk), upload history, notes timeline,
per-metric note add with Ctrl+Enter submit
- NavDrawer: add Compliance nav item (teal, ShieldCheck icon)
- App.js: import and render CompliancePage on compliance route
- Fix SQL join bug in compliance route (lu ON upload_id = lu.id)
- Fix groupByHostname to use max last_seen across all metric rows
2026-03-31 15:14:51 -06:00
|
|
|
|
<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
|
|
|
|
|
|
{metricFilter && (
|
|
|
|
|
|
<button onClick={() => setMetricFilter(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' }}>
|
2026-04-22 18:30:59 +00:00
|
|
|
|
{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 (
|
|
|
|
|
|
<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' }}
|
|
|
|
|
|
>
|
|
|
|
|
|
<MetricHealthCard
|
|
|
|
|
|
family={family}
|
|
|
|
|
|
active={isActive}
|
|
|
|
|
|
onClick={() => setMetricFilter(isActive ? null : familyIds)}
|
|
|
|
|
|
onInfoClick={(metricId) => setInfoMetric(metricId)}
|
|
|
|
|
|
definitionLookup={METRIC_DEFINITIONS}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
})}
|
feat(compliance): add AEO compliance frontend
- CompliancePage: team tabs (STEAM/ACCESS-ENG), metric health cards with
click-to-filter, device table with Active/Resolved tabs, hostname search,
seen-count badges, notes indicator, empty/loading/error states
- ComplianceUploadModal: phased flow (idle→upload→preview→commit→done),
drag-and-drop xlsx drop zone, diff summary before commit
- ComplianceDetailPanel: slide-out panel with failing metrics, surfaced
extra fields (CVEs, SLA, OS, Splunk), upload history, notes timeline,
per-metric note add with Ctrl+Enter submit
- NavDrawer: add Compliance nav item (teal, ShieldCheck icon)
- App.js: import and render CompliancePage on compliance route
- Fix SQL join bug in compliance route (lu ON upload_id = lu.id)
- Fix groupByHostname to use max last_seen across all metric rows
2026-03-31 15:14:51 -06:00
|
|
|
|
</div>
|
2026-04-22 18:30:59 +00:00
|
|
|
|
|
|
|
|
|
|
{/* 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>
|
|
|
|
|
|
);
|
|
|
|
|
|
})()}
|
feat(compliance): add AEO compliance frontend
- CompliancePage: team tabs (STEAM/ACCESS-ENG), metric health cards with
click-to-filter, device table with Active/Resolved tabs, hostname search,
seen-count badges, notes indicator, empty/loading/error states
- ComplianceUploadModal: phased flow (idle→upload→preview→commit→done),
drag-and-drop xlsx drop zone, diff summary before commit
- ComplianceDetailPanel: slide-out panel with failing metrics, surfaced
extra fields (CVEs, SLA, OS, Splunk), upload history, notes timeline,
per-metric note add with Ctrl+Enter submit
- NavDrawer: add Compliance nav item (teal, ShieldCheck icon)
- App.js: import and render CompliancePage on compliance route
- Fix SQL join bug in compliance route (lu ON upload_id = lu.id)
- Fix groupByHostname to use max last_seen across all metric rows
2026-03-31 15:14:51 -06:00
|
|
|
|
</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}
|
|
|
|
|
|
|
2026-04-02 09:49:32 -06:00
|
|
|
|
{/* ── Historical trend charts ──────────────────────────────── */}
|
2026-05-11 15:48:10 -06:00
|
|
|
|
{!vclView && <ComplianceChartsPanel />}
|
2026-04-02 09:49:32 -06:00
|
|
|
|
|
feat(compliance): add AEO compliance frontend
- CompliancePage: team tabs (STEAM/ACCESS-ENG), metric health cards with
click-to-filter, device table with Active/Resolved tabs, hostname search,
seen-count badges, notes indicator, empty/loading/error states
- ComplianceUploadModal: phased flow (idle→upload→preview→commit→done),
drag-and-drop xlsx drop zone, diff summary before commit
- ComplianceDetailPanel: slide-out panel with failing metrics, surfaced
extra fields (CVEs, SLA, OS, Splunk), upload history, notes timeline,
per-metric note add with Ctrl+Enter submit
- NavDrawer: add Compliance nav item (teal, ShieldCheck icon)
- App.js: import and render CompliancePage on compliance route
- Fix SQL join bug in compliance route (lu ON upload_id = lu.id)
- Fix groupByHostname to use max last_seen across all metric rows
2026-03-31 15:14:51 -06:00
|
|
|
|
{/* ── Device table ─────────────────────────────────────────── */}
|
2026-05-11 15:48:10 -06:00
|
|
|
|
{!vclView && <div style={{
|
feat(compliance): add AEO compliance frontend
- CompliancePage: team tabs (STEAM/ACCESS-ENG), metric health cards with
click-to-filter, device table with Active/Resolved tabs, hostname search,
seen-count badges, notes indicator, empty/loading/error states
- ComplianceUploadModal: phased flow (idle→upload→preview→commit→done),
drag-and-drop xlsx drop zone, diff summary before commit
- ComplianceDetailPanel: slide-out panel with failing metrics, surfaced
extra fields (CVEs, SLA, OS, Splunk), upload history, notes timeline,
per-metric note add with Ctrl+Enter submit
- NavDrawer: add Compliance nav item (teal, ShieldCheck icon)
- App.js: import and render CompliancePage on compliance route
- Fix SQL join bug in compliance route (lu ON upload_id = lu.id)
- Fix groupByHostname to use max last_seen across all metric rows
2026-03-31 15:14:51 -06:00
|
|
|
|
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' }}>
|
|
|
|
|
|
{['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>
|
|
|
|
|
|
);
|
|
|
|
|
|
})}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Hostname search */}
|
|
|
|
|
|
<input
|
|
|
|
|
|
value={hostSearch}
|
|
|
|
|
|
onChange={e => 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)'}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Column headers */}
|
|
|
|
|
|
<div style={{
|
|
|
|
|
|
display: 'grid',
|
2026-05-11 15:48:10 -06:00
|
|
|
|
gridTemplateColumns: '2fr 1fr 0.8fr 1.8fr 1fr 1.2fr 0.5fr 0.4fr',
|
feat(compliance): add AEO compliance frontend
- CompliancePage: team tabs (STEAM/ACCESS-ENG), metric health cards with
click-to-filter, device table with Active/Resolved tabs, hostname search,
seen-count badges, notes indicator, empty/loading/error states
- ComplianceUploadModal: phased flow (idle→upload→preview→commit→done),
drag-and-drop xlsx drop zone, diff summary before commit
- ComplianceDetailPanel: slide-out panel with failing metrics, surfaced
extra fields (CVEs, SLA, OS, Splunk), upload history, notes timeline,
per-metric note add with Ctrl+Enter submit
- NavDrawer: add Compliance nav item (teal, ShieldCheck icon)
- App.js: import and render CompliancePage on compliance route
- Fix SQL join bug in compliance route (lu ON upload_id = lu.id)
- Fix groupByHostname to use max last_seen across all metric rows
2026-03-31 15:14:51 -06:00
|
|
|
|
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',
|
|
|
|
|
|
}}>
|
|
|
|
|
|
<span>Hostname</span>
|
|
|
|
|
|
<span>IP Address</span>
|
|
|
|
|
|
<span>Type</span>
|
|
|
|
|
|
<span>Failing Metrics</span>
|
2026-05-11 15:48:10 -06:00
|
|
|
|
<span>Resolution Date</span>
|
|
|
|
|
|
<span>Remediation Plan</span>
|
feat(compliance): add AEO compliance frontend
- CompliancePage: team tabs (STEAM/ACCESS-ENG), metric health cards with
click-to-filter, device table with Active/Resolved tabs, hostname search,
seen-count badges, notes indicator, empty/loading/error states
- ComplianceUploadModal: phased flow (idle→upload→preview→commit→done),
drag-and-drop xlsx drop zone, diff summary before commit
- ComplianceDetailPanel: slide-out panel with failing metrics, surfaced
extra fields (CVEs, SLA, OS, Splunk), upload history, notes timeline,
per-metric note add with Ctrl+Enter submit
- NavDrawer: add Compliance nav item (teal, ShieldCheck icon)
- App.js: import and render CompliancePage on compliance route
- Fix SQL join bug in compliance route (lu ON upload_id = lu.id)
- Fix groupByHostname to use max last_seen across all metric rows
2026-03-31 15:14:51 -06:00
|
|
|
|
<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' : activeTab === 'active' ? 'No non-compliant devices' : 'No resolved items'}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
filteredDevices.map(device => (
|
|
|
|
|
|
<DeviceRow
|
|
|
|
|
|
key={device.hostname}
|
|
|
|
|
|
device={device}
|
|
|
|
|
|
selected={selectedHost === device.hostname}
|
|
|
|
|
|
onClick={() => setSelectedHost(selectedHost === device.hostname ? null : device.hostname)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
))
|
|
|
|
|
|
)}
|
2026-05-11 15:48:10 -06:00
|
|
|
|
</div>}
|
feat(compliance): add AEO compliance frontend
- CompliancePage: team tabs (STEAM/ACCESS-ENG), metric health cards with
click-to-filter, device table with Active/Resolved tabs, hostname search,
seen-count badges, notes indicator, empty/loading/error states
- ComplianceUploadModal: phased flow (idle→upload→preview→commit→done),
drag-and-drop xlsx drop zone, diff summary before commit
- ComplianceDetailPanel: slide-out panel with failing metrics, surfaced
extra fields (CVEs, SLA, OS, Splunk), upload history, notes timeline,
per-metric note add with Ctrl+Enter submit
- NavDrawer: add Compliance nav item (teal, ShieldCheck icon)
- App.js: import and render CompliancePage on compliance route
- Fix SQL join bug in compliance route (lu ON upload_id = lu.id)
- Fix groupByHostname to use max last_seen across all metric rows
2026-03-31 15:14:51 -06:00
|
|
|
|
|
|
|
|
|
|
{/* ── Detail panel ─────────────────────────────────────────── */}
|
|
|
|
|
|
{selectedHost && (
|
|
|
|
|
|
<ComplianceDetailPanel
|
|
|
|
|
|
hostname={selectedHost}
|
|
|
|
|
|
onClose={() => setSelectedHost(null)}
|
|
|
|
|
|
onNoteAdded={refresh}
|
2026-04-01 09:20:30 -06:00
|
|
|
|
onNavigate={onNavigate}
|
feat(compliance): add AEO compliance frontend
- CompliancePage: team tabs (STEAM/ACCESS-ENG), metric health cards with
click-to-filter, device table with Active/Resolved tabs, hostname search,
seen-count badges, notes indicator, empty/loading/error states
- ComplianceUploadModal: phased flow (idle→upload→preview→commit→done),
drag-and-drop xlsx drop zone, diff summary before commit
- ComplianceDetailPanel: slide-out panel with failing metrics, surfaced
extra fields (CVEs, SLA, OS, Splunk), upload history, notes timeline,
per-metric note add with Ctrl+Enter submit
- NavDrawer: add Compliance nav item (teal, ShieldCheck icon)
- App.js: import and render CompliancePage on compliance route
- Fix SQL join bug in compliance route (lu ON upload_id = lu.id)
- Fix groupByHostname to use max last_seen across all metric rows
2026-03-31 15:14:51 -06:00
|
|
|
|
/>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* ── Upload modal ─────────────────────────────────────────── */}
|
|
|
|
|
|
{showUpload && (
|
|
|
|
|
|
<ComplianceUploadModal
|
|
|
|
|
|
onClose={() => setShowUpload(false)}
|
|
|
|
|
|
onUploadComplete={() => { setShowUpload(false); refresh(); }}
|
|
|
|
|
|
/>
|
|
|
|
|
|
)}
|
2026-04-20 20:12:12 +00:00
|
|
|
|
|
2026-04-22 18:30:59 +00:00
|
|
|
|
{/* ── Metric info panel ───────────────────────────────────── */}
|
|
|
|
|
|
{infoMetric && (
|
|
|
|
|
|
<MetricInfoPanel
|
|
|
|
|
|
metricId={infoMetric}
|
|
|
|
|
|
definition={METRIC_DEFINITIONS[infoMetric] || null}
|
|
|
|
|
|
summaryEntries={(families.find(f => f.metricId === infoMetric) || {}).entries || []}
|
|
|
|
|
|
onClose={() => setInfoMetric(null)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
2026-04-20 20:12:12 +00:00
|
|
|
|
{/* ── 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>
|
|
|
|
|
|
)}
|
feat(compliance): add AEO compliance frontend
- CompliancePage: team tabs (STEAM/ACCESS-ENG), metric health cards with
click-to-filter, device table with Active/Resolved tabs, hostname search,
seen-count badges, notes indicator, empty/loading/error states
- ComplianceUploadModal: phased flow (idle→upload→preview→commit→done),
drag-and-drop xlsx drop zone, diff summary before commit
- ComplianceDetailPanel: slide-out panel with failing metrics, surfaced
extra fields (CVEs, SLA, OS, Splunk), upload history, notes timeline,
per-metric note add with Ctrl+Enter submit
- NavDrawer: add Compliance nav item (teal, ShieldCheck icon)
- App.js: import and render CompliancePage on compliance route
- Fix SQL join bug in compliance route (lu ON upload_id = lu.id)
- Fix groupByHostname to use max last_seen across all metric rows
2026-03-31 15:14:51 -06:00
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function DeviceRow({ device, selected, onClick }) {
|
2026-05-11 15:48:10 -06:00
|
|
|
|
const truncateText = (text, maxLen = 80) => {
|
|
|
|
|
|
if (!text) return '—';
|
|
|
|
|
|
return text.length > maxLen ? text.slice(0, maxLen) + '…' : text;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
feat(compliance): add AEO compliance frontend
- CompliancePage: team tabs (STEAM/ACCESS-ENG), metric health cards with
click-to-filter, device table with Active/Resolved tabs, hostname search,
seen-count badges, notes indicator, empty/loading/error states
- ComplianceUploadModal: phased flow (idle→upload→preview→commit→done),
drag-and-drop xlsx drop zone, diff summary before commit
- ComplianceDetailPanel: slide-out panel with failing metrics, surfaced
extra fields (CVEs, SLA, OS, Splunk), upload history, notes timeline,
per-metric note add with Ctrl+Enter submit
- NavDrawer: add Compliance nav item (teal, ShieldCheck icon)
- App.js: import and render CompliancePage on compliance route
- Fix SQL join bug in compliance route (lu ON upload_id = lu.id)
- Fix groupByHostname to use max last_seen across all metric rows
2026-03-31 15:14:51 -06:00
|
|
|
|
return (
|
|
|
|
|
|
<div
|
|
|
|
|
|
onClick={onClick}
|
|
|
|
|
|
style={{
|
|
|
|
|
|
display: 'grid',
|
2026-05-11 15:48:10 -06:00
|
|
|
|
gridTemplateColumns: '2fr 1fr 0.8fr 1.8fr 1fr 1.2fr 0.5fr 0.4fr',
|
feat(compliance): add AEO compliance frontend
- CompliancePage: team tabs (STEAM/ACCESS-ENG), metric health cards with
click-to-filter, device table with Active/Resolved tabs, hostname search,
seen-count badges, notes indicator, empty/loading/error states
- ComplianceUploadModal: phased flow (idle→upload→preview→commit→done),
drag-and-drop xlsx drop zone, diff summary before commit
- ComplianceDetailPanel: slide-out panel with failing metrics, surfaced
extra fields (CVEs, SLA, OS, Splunk), upload history, notes timeline,
per-metric note add with Ctrl+Enter submit
- NavDrawer: add Compliance nav item (teal, ShieldCheck icon)
- App.js: import and render CompliancePage on compliance route
- Fix SQL join bug in compliance route (lu ON upload_id = lu.id)
- Fix groupByHostname to use max last_seen across all metric rows
2026-03-31 15:14:51 -06:00
|
|
|
|
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'; }}
|
|
|
|
|
|
>
|
|
|
|
|
|
{/* 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>
|
|
|
|
|
|
|
2026-05-11 15:48:10 -06:00
|
|
|
|
{/* Resolution Date */}
|
|
|
|
|
|
<div style={{ fontFamily: 'monospace', fontSize: '0.72rem', color: '#94A3B8' }}>
|
|
|
|
|
|
{device.resolution_date || '—'}
|
|
|
|
|
|
</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>
|
|
|
|
|
|
|
feat(compliance): add AEO compliance frontend
- CompliancePage: team tabs (STEAM/ACCESS-ENG), metric health cards with
click-to-filter, device table with Active/Resolved tabs, hostname search,
seen-count badges, notes indicator, empty/loading/error states
- ComplianceUploadModal: phased flow (idle→upload→preview→commit→done),
drag-and-drop xlsx drop zone, diff summary before commit
- ComplianceDetailPanel: slide-out panel with failing metrics, surfaced
extra fields (CVEs, SLA, OS, Splunk), upload history, notes timeline,
per-metric note add with Ctrl+Enter submit
- NavDrawer: add Compliance nav item (teal, ShieldCheck icon)
- App.js: import and render CompliancePage on compliance route
- Fix SQL join bug in compliance route (lu ON upload_id = lu.id)
- Fix groupByHostname to use max last_seen across all metric rows
2026-03-31 15:14:51 -06:00
|
|
|
|
{/* 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>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
2026-04-22 18:30:59 +00:00
|
|
|
|
|
|
|
|
|
|
// Named exports for testing
|
|
|
|
|
|
export { computeWorstStatus, groupByMetricFamily };
|