// AnomalyBanner.js // Warning banner for the Vulnerability Triage page. // Fetches the latest sync anomaly summary and displays a dismissible // amber banner when a significant count change is detected. import React, { useState, useEffect } from 'react'; import { AlertTriangle, X, ChevronDown, ChevronUp } from 'lucide-react'; const API_BASE = process.env.REACT_APP_API_BASE || 'http://localhost:3001/api'; // --------------------------------------------------------------------------- // Style constants (inline style objects — matches IvantiCountsChart pattern) // --------------------------------------------------------------------------- const BANNER_CONTAINER = { background: 'rgba(245, 158, 11, 0.15)', border: '1px solid rgba(245, 158, 11, 0.3)', borderRadius: '0.5rem', padding: '0.75rem 1rem', marginBottom: '1.25rem', fontFamily: "'JetBrains Mono', monospace", }; const HEADER_ROW = { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '0.5rem', }; const HEADER_LEFT = { display: 'flex', alignItems: 'center', gap: '0.5rem', flex: 1, minWidth: 0, }; const ICON_STYLE = { width: '16px', height: '16px', color: '#F59E0B', flexShrink: 0, }; const SUMMARY_TEXT = { fontSize: '0.7rem', color: '#FCD34D', fontWeight: '600', lineHeight: '1.4', }; const TOGGLE_BTN = { background: 'none', border: 'none', cursor: 'pointer', padding: '2px', display: 'flex', alignItems: 'center', color: '#F59E0B', opacity: 0.7, }; const DISMISS_BTN = { background: 'none', border: 'none', cursor: 'pointer', padding: '2px', display: 'flex', alignItems: 'center', color: '#94A3B8', opacity: 0.7, flexShrink: 0, }; const DETAIL_SECTION = { marginTop: '0.625rem', paddingTop: '0.5rem', borderTop: '1px solid rgba(245, 158, 11, 0.15)', }; const DETAIL_ROW = { display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '0.2rem 0', fontSize: '0.65rem', color: '#CBD5E1', }; const DETAIL_COUNT = { fontWeight: '700', color: '#FCD34D', }; // --------------------------------------------------------------------------- // Classification labels for display // --------------------------------------------------------------------------- const CLASSIFICATION_LABELS = { bu_reassignment: 'BU reassignment', severity_drift: 'severity drift', closed_on_platform: 'closed on platform', decommissioned: 'decommissioned', }; // --------------------------------------------------------------------------- // Build the summary text from anomaly data // --------------------------------------------------------------------------- function buildSummaryText(anomaly) { const count = anomaly.newly_archived_count || 0; const classification = anomaly.classification || {}; const parts = []; for (const [key, label] of Object.entries(CLASSIFICATION_LABELS)) { const val = classification[key]; if (val && val > 0) { parts.push(`${val} ${label}`); } } const breakdown = parts.length > 0 ? parts.join(', ') : 'unclassified'; return `${count} findings archived — ${breakdown}`; } // --------------------------------------------------------------------------- // Main component // --------------------------------------------------------------------------- export default function AnomalyBanner() { const [anomaly, setAnomaly] = useState(null); const [loading, setLoading] = useState(true); const [dismissed, setDismissed] = useState(false); const [expanded, setExpanded] = useState(false); useEffect(() => { let cancelled = false; const load = async () => { setLoading(true); try { const res = await fetch(`${API_BASE}/ivanti/findings/anomaly/latest`, { credentials: 'include', }); if (res.ok && !cancelled) { const data = await res.json(); setAnomaly(data.anomaly || null); } } catch { /* silent — banner simply won't show */ } finally { if (!cancelled) setLoading(false); } }; load(); return () => { cancelled = true; }; }, []); // Render nothing while loading, if dismissed, or if anomaly is not significant if (loading || dismissed || !anomaly || !anomaly.is_significant) { return null; } const classification = anomaly.classification || {}; return (