// ComplianceChartsPanel.js // Tier-1 time-based compliance charts using Recharts. // Charts rendered: Active Findings Over Time, Change per Cycle, // Team Health, MTTR by Team, Persistent Findings, Archer Pipeline. import React, { useState, useEffect, useMemo } from 'react'; import { LineChart, Line, BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer, } from 'recharts'; import { TrendingUp, ChevronDown, ChevronUp, Loader } from 'lucide-react'; const API_BASE = process.env.REACT_APP_API_BASE || 'http://localhost:3001/api'; const TEAL = '#14B8A6'; const TEAM_COLORS = { 'STEAM': '#0EA5E9', 'ACCESS-ENG': '#F59E0B', 'ACCESS-OPS': '#8B5CF6', 'INTELDEV': '#10B981', }; const ARCHER_STATUS_COLORS = { 'Draft': '#475569', 'Open': '#0EA5E9', 'Under Review': '#F59E0B', 'Accepted': '#10B981', }; // --------------------------------------------------------------------------- // Shared style tokens // --------------------------------------------------------------------------- const AXIS_STYLE = { fontFamily: 'monospace', fontSize: '0.6rem', fill: '#475569' }; const GRID_STYLE = { stroke: 'rgba(255,255,255,0.05)', strokeDasharray: '3 3' }; const LEGEND_STYLE = { fontFamily: 'monospace', fontSize: '0.62rem', color: '#64748B' }; // --------------------------------------------------------------------------- // Custom dark tooltip // --------------------------------------------------------------------------- function DarkTooltip({ active, payload, label }) { if (!active || !payload?.length) return null; return (
{label}
{payload.map(p => (
{p.name} {typeof p.value === 'number' ? Number.isInteger(p.value) ? p.value : p.value.toFixed(1) : p.value}
))}
); } // --------------------------------------------------------------------------- // Chart card wrapper // --------------------------------------------------------------------------- function ChartCard({ title, subtitle, children }) { return (
{title}
{subtitle && (
{subtitle}
)}
{children}
); } // --------------------------------------------------------------------------- // Empty / no-data state // --------------------------------------------------------------------------- function NoData({ msg }) { return (
{msg || 'No data yet — upload compliance reports to populate this chart'}
); } // --------------------------------------------------------------------------- // Shorten a YYYY-MM-DD string to MM/DD/YY // --------------------------------------------------------------------------- function fmtDate(d) { if (!d) return ''; const p = d.split('-'); if (p.length === 3) return `${p[1]}/${p[2]}/${p[0].slice(2)}`; return d; } // --------------------------------------------------------------------------- // Chart 1 — Active Findings Over Time (line, total + per team) // --------------------------------------------------------------------------- function ActiveTrendChart({ data }) { if (data.length < 2) return ; return ( } /> {Object.entries(TEAM_COLORS).map(([team, color]) => ( ))} ); } // --------------------------------------------------------------------------- // Chart 2 — New / Recurring / Resolved per cycle (stacked + grouped bar) // --------------------------------------------------------------------------- function DeltaChart({ data }) { if (data.length === 0) return ; return ( } /> ); } // --------------------------------------------------------------------------- // Chart 3 — Team Health Multi-Line // --------------------------------------------------------------------------- function TeamTrendChart({ data }) { if (data.length < 2) return ; return ( } /> {Object.entries(TEAM_COLORS).map(([team, color]) => ( ))} ); } // --------------------------------------------------------------------------- // Chart 4 — MTTR by Team (horizontal bar) // --------------------------------------------------------------------------- function MttrChart({ data }) { if (data.length === 0) return ; return ( } /> `${v}d` }} /> ); } // --------------------------------------------------------------------------- // Chart 5 — Most Persistent Findings (horizontal bar by seen_count) // --------------------------------------------------------------------------- function RecurringChart({ data }) { if (data.length === 0) return ; const top10 = data.slice(0, 10).map(r => ({ ...r, label: r.metric_id.length > 18 ? r.metric_id.slice(0, 18) + '…' : r.metric_id, })); return ( } formatter={(val, name, props) => [ `${val} cycles (${props.payload.host_count} host${props.payload.host_count !== 1 ? 's' : ''})`, props.payload.team ]} /> `${v}×` }} /> ); } // --------------------------------------------------------------------------- // Chart 6 — Archer Exception Ticket Pipeline (stacked bar by creation date) // --------------------------------------------------------------------------- function ArcherPipelineChart({ data }) { if (data.length === 0) return ; return ( } /> {Object.entries(ARCHER_STATUS_COLORS).map(([status, color], i, arr) => ( ))} ); } // --------------------------------------------------------------------------- // Main panel // --------------------------------------------------------------------------- export default function ComplianceChartsPanel() { const [collapsed, setCollapsed] = useState(false); const [loading, setLoading] = useState(true); const [trends, setTrends] = useState([]); const [mttr, setMttr] = useState([]); const [recurring, setRecurring] = useState([]); const [archerRaw, setArcherRaw] = useState([]); useEffect(() => { let cancelled = false; const load = async () => { setLoading(true); try { const [tRes, mRes, rRes, aRes] = await Promise.all([ fetch(`${API_BASE}/compliance/trends`, { credentials: 'include' }), fetch(`${API_BASE}/compliance/mttr`, { credentials: 'include' }), fetch(`${API_BASE}/compliance/top-recurring`, { credentials: 'include' }), fetch(`${API_BASE}/archer-tickets/status-trend`, { credentials: 'include' }), ]); if (cancelled) return; if (tRes.ok) { const d = await tRes.json(); setTrends(d.trends || []); } if (mRes.ok) { const d = await mRes.json(); setMttr(d.mttr || []); } if (rRes.ok) { const d = await rRes.json(); setRecurring(d.items || []); } if (aRes.ok) { const d = await aRes.json(); setArcherRaw(d.statusTrend || []); } } catch { /* silent — charts will show no-data state */ } finally { if (!cancelled) setLoading(false); } }; load(); return () => { cancelled = true; }; }, []); // Format trend rows — add short date label const formattedTrends = useMemo( () => trends.map(t => ({ ...t, date: fmtDate(t.report_date) })), [trends] ); // Pivot archer raw rows → one object per date const archerByDate = useMemo(() => { if (!archerRaw.length) return []; const map = {}; archerRaw.forEach(r => { if (!map[r.date]) map[r.date] = { date: fmtDate(r.date) }; map[r.date][r.status] = r.count; }); return Object.values(map).sort((a, b) => a.date.localeCompare(b.date)); }, [archerRaw]); return (
{/* ── Section header / collapse toggle ──────────────────────── */} {!collapsed && (
{/* 1. Active findings over time */} {/* 2. New / Recurring / Resolved delta per cycle */} {/* 3. Team health multi-line */} {/* 4. MTTR per team */} {/* 5. Most persistent / recurring findings */} {/* 6. Archer ticket pipeline */}
)}
); }