import React, { useState, useEffect, useCallback } from 'react';
import { X, MessageSquare, Send, Loader, AlertCircle, Clock, Shield, Trash2 } from 'lucide-react';
import ConfirmModal from '../ConfirmModal';
const API_BASE = process.env.REACT_APP_API_BASE || 'http://localhost:3001/api';
const TEAL = '#14B8A6';
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 categoryColor(category) {
return CATEGORY_COLORS[category] || '#94A3B8';
}
function MetricChip({ metricId, category, status }) {
const color = status === 'resolved' ? '#64748B' : categoryColor(category);
return (
{metricId}
);
}
export default function ComplianceDetailPanel({ hostname, onClose, onNoteAdded, onNavigate }) {
const [detail, setDetail] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [noteText, setNoteText] = useState('');
const [selectedMetrics, setSelectedMetrics] = useState([]);
const [submitting, setSubmitting] = useState(false);
const [noteError, setNoteError] = useState(null);
const [pendingConfirm, setPendingConfirm] = useState(null);
const fetchDetail = useCallback(async () => {
setLoading(true);
setError(null);
try {
const res = await fetch(`${API_BASE}/compliance/items/${encodeURIComponent(hostname)}`, { credentials: 'include' });
const data = await res.json();
if (!res.ok) throw new Error(data.error || 'Failed to load device');
setDetail(data);
// Default selected metrics to first active failing metric
const firstActive = (data.metrics || []).find(m => m.status === 'active');
if (firstActive) setSelectedMetrics([firstActive.metric_id]);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
}, [hostname]);
useEffect(() => { fetchDetail(); }, [fetchDetail]);
const handleAddNote = async () => {
if (!noteText.trim() || selectedMetrics.length === 0) return;
setSubmitting(true);
setNoteError(null);
try {
const res = await fetch(`${API_BASE}/compliance/notes`, {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ hostname, metric_ids: selectedMetrics, note: noteText.trim() }),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error || 'Failed to save note');
setNoteText('');
await fetchDetail();
if (onNoteAdded) onNoteAdded();
} catch (err) {
setNoteError(err.message);
} finally {
setSubmitting(false);
}
};
const handleDeleteNote = async (noteId, hasGroup) => {
setPendingConfirm({
title: 'Delete Note',
message: 'Delete this note?',
confirmText: 'Delete',
onConfirm: async () => {
setPendingConfirm(null);
try {
const url = hasGroup
? `${API_BASE}/compliance/notes/${noteId}?group=true`
: `${API_BASE}/compliance/notes/${noteId}`;
const res = await fetch(url, { method: 'DELETE', credentials: 'include' });
const data = await res.json();
if (!res.ok) throw new Error(data.error || 'Failed to delete note');
await fetchDetail();
if (onNoteAdded) onNoteAdded();
} catch (err) {
setNoteError(err.message);
}
},
});
};
const activeMetrics = detail?.metrics?.filter(m => m.status === 'active') || [];
const resolvedMetrics = detail?.metrics?.filter(m => m.status === 'resolved') || [];
return (
<>
{/* Backdrop */}
{/* Panel */}
{/* Header */}
{hostname}
{detail && (
{detail.ip_address && (
{detail.ip_address}
)}
{detail.device_type && (
· {detail.device_type}
)}
· {detail.team}
)}
{loading && (
)}
{error && (
)}
{!loading && !error && detail && (
{/* Active failing metrics */}
{activeMetrics.length > 0 && (
}>
{activeMetrics.map(m => (
))}
)}
{/* Resolved metrics */}
{resolvedMetrics.length > 0 && (
{resolvedMetrics.map(m => (
))}
)}
{/* Upload history summary */}
{activeMetrics.length > 0 && (
}>
{activeMetrics.map(m => (
2 ? '#F59E0B' : '#94A3B8' }}>
{m.seen_count}× seen
{m.first_seen && since {m.first_seen}}
))}
)}
{/* Notes */}
} grow>
{detail.notes.length === 0 && (
No notes yet
)}
{(() => {
// Build a lookup map for metric categories (active + resolved)
const metricMap = {};
(detail.metrics || []).forEach(m => { metricMap[m.metric_id] = m.category; });
// Group notes by group_id, preserving reverse chronological order
const grouped = [];
const seen = new Set();
// detail.notes is already sorted newest-first from the API
for (const n of detail.notes) {
const gid = n.group_id;
if (!gid) {
// Legacy note without group_id — render individually
grouped.push({ key: `note-${n.id}`, notes: [n], note: n.note, created_by: n.created_by, created_at: n.created_at });
} else if (!seen.has(gid)) {
seen.add(gid);
const group = detail.notes.filter(x => x.group_id === gid);
grouped.push({ key: `group-${gid}`, notes: group, note: group[0].note, created_by: group[0].created_by, created_at: group[0].created_at });
}
}
return grouped.map(g => (
{g.notes.map(n => (
))}
{g.created_by && `${g.created_by} · `}{g.created_at?.slice(0, 10)}
{g.note}
));
})()}
{/* Add note */}
{activeMetrics.length > 1 && (() => {
const allSelected = activeMetrics.length > 0 && activeMetrics.every(m => selectedMetrics.includes(m.metric_id));
return (
Metrics
{activeMetrics.map(m => {
const isSelected = selectedMetrics.includes(m.metric_id);
const color = categoryColor(m.category);
return (
);
})}
);
})()}
{noteError &&
{noteError}
}
)}
{/* Confirmation Modal */}
setPendingConfirm(null)}
/>
>
);
}
function Section({ title, icon, children, muted, grow }) {
return (
{icon && {icon}}
{title}
{children}
);
}
function MetricRow({ metric, resolved, onNavigate }) {
const color = resolved ? '#475569' : categoryColor(metric.category);
const extra = metric.extra || {};
const ivantiId = (!resolved && metric.metric_id?.startsWith('2.3'))
? (extra['Ivanti_Vulnerability_ID'] || null)
: null;
// Surface the most useful extra fields per metric type
const highlights = [];
if (extra['CVEs_Associated']) highlights.push({ label: 'CVEs', value: extra['CVEs_Associated'] });
if (extra['SLA_Status']) highlights.push({ label: 'SLA', value: extra['SLA_Status'] });
if (extra['Due_Date']) highlights.push({ label: 'Due', value: extra['Due_Date'] });
if (extra['Normalized - Operating System'])
highlights.push({ label: 'OS', value: `${extra['Normalized - Operating System']} ${extra['Normalized - Operating System Version'] || ''}`.trim() });
if (extra['EOS - End of Service Life'])
highlights.push({ label: 'EoL', value: extra['EOS - End of Service Life'] });
if (extra['Splunk - Last Seen']) highlights.push({ label: 'Splunk', value: extra['Splunk - Last Seen'] });
if (extra['MFA - Software']) highlights.push({ label: 'MFA SW', value: extra['MFA - Software'] });
return (
{resolved && resolved {metric.resolved_on || ''}}
{metric.metric_desc && (
{metric.metric_desc.length > 100 ? metric.metric_desc.slice(0, 100) + '…' : metric.metric_desc}
)}
{ivantiId && (
Ivanti ID
{ivantiId}
{onNavigate && (
)}
)}
{highlights.map(h => (
{h.label}
{String(h.value).length > 80 ? String(h.value).slice(0, 80) + '…' : h.value}
))}
);
}