import React, { useState, useEffect, useCallback } from 'react'; import { RefreshCw, Loader, AlertCircle, PieChart, ChevronUp, ChevronDown, ChevronsUpDown } from 'lucide-react'; const API_BASE = process.env.REACT_APP_API_BASE || 'http://localhost:3001/api'; // --------------------------------------------------------------------------- // Column definitions // --------------------------------------------------------------------------- const COLUMNS = [ { key: 'severity', label: 'Severity', accessor: (f) => f.severity, sortable: true }, { key: 'title', label: 'Title', accessor: (f) => f.title, sortable: true }, { key: 'hostName', label: 'Host', accessor: (f) => f.hostName, sortable: true }, { key: 'ipAddress', label: 'IP Address', accessor: (f) => f.ipAddress, sortable: true }, { key: 'dns', label: 'DNS', accessor: (f) => f.dns, sortable: true }, { key: 'slaStatus', label: 'SLA', accessor: (f) => f.slaStatus, sortable: true }, { key: 'discoveredOn',label: 'Discovered', accessor: (f) => f.discoveredOn,sortable: true }, { key: 'lastFoundOn', label: 'Last Found', accessor: (f) => f.lastFoundOn, sortable: true }, { key: 'source', label: 'Source', accessor: (f) => f.source, sortable: true }, { key: 'note', label: 'Notes', accessor: (f) => f.note, sortable: false }, ]; // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- function severityColor(vrrGroup) { switch ((vrrGroup || '').toUpperCase()) { case 'CRITICAL': return { bg: 'rgba(239,68,68,0.15)', border: '#EF4444', text: '#EF4444' }; case 'HIGH': return { bg: 'rgba(245,158,11,0.15)', border: '#F59E0B', text: '#F59E0B' }; case 'MEDIUM': return { bg: 'rgba(234,179,8,0.15)', border: '#EAB308', text: '#EAB308' }; default: return { bg: 'rgba(100,116,139,0.15)', border: '#64748B', text: '#94A3B8' }; } } function slaColor(slaStatus) { switch ((slaStatus || '').toUpperCase()) { case 'OVERDUE': return '#EF4444'; case 'AT_RISK': return '#F59E0B'; case 'OK': return '#10B981'; default: return '#64748B'; } } function SortIcon({ colKey, sort }) { if (sort.field !== colKey) return ; return sort.dir === 'asc' ? : ; } // --------------------------------------------------------------------------- // NoteCell — inline editable, saves on blur // --------------------------------------------------------------------------- function NoteCell({ findingId, initialNote }) { const [value, setValue] = useState(initialNote || ''); const [saving, setSaving] = useState(false); const save = useCallback(async () => { if (value === (initialNote || '')) return; // nothing changed setSaving(true); try { await fetch(`${API_BASE}/ivanti/findings/${encodeURIComponent(findingId)}/note`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ note: value }) }); } catch (e) { console.error('Failed to save note:', e); } finally { setSaving(false); } }, [findingId, value, initialNote]); return (
setValue(e.target.value)} onBlur={save} placeholder="Add note…" style={{ width: '100%', minWidth: '160px', background: 'rgba(14, 165, 233, 0.05)', border: '1px solid rgba(14, 165, 233, 0.2)', borderRadius: '4px', padding: '4px 8px', color: '#CBD5E1', fontSize: '0.75rem', fontFamily: 'inherit', outline: 'none', boxSizing: 'border-box' }} onFocus={(e) => { e.target.style.borderColor = 'rgba(14, 165, 233, 0.6)'; e.target.style.background = 'rgba(14, 165, 233, 0.1)'; }} /> {saving && }
); } // --------------------------------------------------------------------------- // Main ReportingPage component // --------------------------------------------------------------------------- export default function ReportingPage() { const [findings, setFindings] = useState([]); const [total, setTotal] = useState(null); const [syncedAt, setSyncedAt] = useState(null); const [syncStatus, setSyncStatus] = useState(null); const [syncError, setSyncError] = useState(null); const [loading, setLoading] = useState(false); const [syncing, setSyncing] = useState(false); const [sort, setSort] = useState({ field: 'severity', dir: 'desc' }); const applyState = (data) => { setTotal(data.total ?? 0); setFindings(data.findings || []); setSyncedAt(data.synced_at || null); setSyncStatus(data.sync_status || null); setSyncError(data.error_message || null); }; const fetchFindings = async () => { setLoading(true); try { const res = await fetch(`${API_BASE}/ivanti/findings`, { credentials: 'include' }); const data = await res.json(); if (res.ok) applyState(data); } catch (e) { console.error('Error loading findings:', e); } finally { setLoading(false); } }; const syncFindings = async () => { setSyncing(true); try { const res = await fetch(`${API_BASE}/ivanti/findings/sync`, { method: 'POST', credentials: 'include' }); const data = await res.json(); if (res.ok) applyState(data); } catch (e) { console.error('Error syncing findings:', e); } finally { setSyncing(false); } }; useEffect(() => { fetchFindings(); }, []); // eslint-disable-line // Sort findings const sorted = [...findings].sort((a, b) => { const col = COLUMNS.find((c) => c.key === sort.field); if (!col) return 0; const av = col.accessor(a) ?? ''; const bv = col.accessor(b) ?? ''; let cmp = 0; if (typeof av === 'number' && typeof bv === 'number') { cmp = av - bv; } else { cmp = String(av).localeCompare(String(bv), undefined, { numeric: true }); } return sort.dir === 'asc' ? cmp : -cmp; }); const toggleSort = (key) => { setSort((prev) => prev.field === key ? { field: key, dir: prev.dir === 'asc' ? 'desc' : 'asc' } : { field: key, dir: 'asc' } ); }; const syncedDisplay = syncedAt ? new Date(syncedAt.replace(' ', 'T') + 'Z').toLocaleString() : 'Never synced'; // ------------------------------------------------------------------------- // Render // ------------------------------------------------------------------------- return (
{/* ---------------------------------------------------------------- Panel 1 — Metrics placeholder (full width) ---------------------------------------------------------------- */}

Metric Graphs

Pie charts & metrics — coming soon

{/* ---------------------------------------------------------------- Panel 2 — Findings table ---------------------------------------------------------------- */}
{/* Table header row */}

Host Findings

{syncedDisplay} {syncStatus === 'success' && total !== null && ( {total} total findings )}
{/* Error banner */} {syncStatus === 'error' && syncError && (
{syncError}
)} {/* Loading state */} {loading ? (

Loading findings…

) : syncStatus === 'never' ? (

Click Sync to load findings data

) : ( /* Table */
{COLUMNS.map((col) => ( ))} {sorted.map((finding, idx) => { const sc = severityColor(finding.vrrGroup); const rowBg = idx % 2 === 0 ? 'rgba(15,26,46,0.4)' : 'rgba(10,18,32,0.4)'; return ( e.currentTarget.style.background = 'rgba(14,165,233,0.05)'} onMouseLeave={(e) => e.currentTarget.style.background = rowBg} > {/* Severity */} {/* Title */} {/* Host */} {/* IP */} {/* DNS */} {/* SLA */} {/* Discovered */} {/* Last Found */} {/* Source */} {/* Notes */} ); })} {sorted.length === 0 && ( )}
toggleSort(col.key) : undefined} style={{ padding: '0.5rem 0.75rem', textAlign: 'left', fontFamily: 'monospace', fontSize: '0.68rem', fontWeight: '600', color: sort.field === col.key ? '#0EA5E9' : '#64748B', textTransform: 'uppercase', letterSpacing: '0.08em', whiteSpace: 'nowrap', cursor: col.sortable ? 'pointer' : 'default', userSelect: 'none', background: 'rgba(15,26,46,0.6)' }} > {col.label} {col.sortable && }
{finding.severity?.toFixed(2)} {finding.vrrGroup} {finding.title} {finding.hostName || '—'} {finding.ipAddress || '—'} {finding.dns || '—'} {finding.slaStatus || '—'} {finding.discoveredOn || '—'} {finding.lastFoundOn || '—'} {finding.source || '—'}
No findings found
)}
); }