import React, { useState, useEffect, useCallback } from 'react'; import { X, MessageSquare, Send, Loader, AlertCircle, Clock, Shield } from 'lucide-react'; 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 }) { const [detail, setDetail] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [noteText, setNoteText] = useState(''); const [noteMetric, setNoteMetric] = useState(''); const [submitting, setSubmitting] = useState(false); const [noteError, setNoteError] = 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 note metric to first active failing metric const firstActive = (data.metrics || []).find(m => m.status === 'active'); if (firstActive) setNoteMetric(firstActive.metric_id); } catch (err) { setError(err.message); } finally { setLoading(false); } }, [hostname]); useEffect(() => { fetchDetail(); }, [fetchDetail]); const handleAddNote = async () => { if (!noteText.trim() || !noteMetric) 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_id: noteMetric, 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 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 && (
{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
)} {detail.notes.map(n => (
m.metric_id === n.metric_id)?.category || ''} /> {n.created_by && `${n.created_by} · `}{n.created_at?.slice(0, 10)}
{n.note}
))} {/* Add note */}
{activeMetrics.length > 1 && ( )}