import { useState, useEffect, useRef, useCallback, useMemo } from 'react'; import { useAppStore } from '@/stores/app'; import { useI18n } from '@/i18n'; import { usePolling } from '@/hooks/usePolling'; import { reportsApi, type Report, type ReportProgress, type ReportSummary, type ReportFile, } from '@/api/endpoints/reports'; import { BarChart3, RefreshCw, Play, XCircle, ChevronDown, ChevronRight, Search, X, Loader2, AlertCircle, CheckCircle2, Clock, FileText, Download, ShieldCheck, ShieldAlert, Activity, Eye, EyeOff, Trash2, Database, Lock, Globe, Server, Package, Dna, Check, } from 'lucide-react'; import { PieChart, Pie, Cell, BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, } from 'recharts'; /* ── Fallback Regions ── */ const FALLBACK_REGIONS: Record = { 'Americas': [ 'us-ashburn-1', 'us-phoenix-1', 'us-sanjose-1', 'us-chicago-1', 'ca-toronto-1', 'ca-montreal-1', 'sa-saopaulo-1', 'sa-vinhedo-1', 'sa-santiago-1', 'sa-bogota-1', 'mx-queretaro-1', 'mx-monterrey-1', ], 'Europe': [ 'eu-frankfurt-1', 'eu-amsterdam-1', 'eu-zurich-1', 'eu-madrid-1', 'eu-marseille-1', 'eu-milan-1', 'eu-stockholm-1', 'eu-paris-1', 'uk-london-1', 'uk-cardiff-1', 'eu-jovanovac-1', ], 'Asia Pacific': [ 'ap-tokyo-1', 'ap-osaka-1', 'ap-seoul-1', 'ap-sydney-1', 'ap-melbourne-1', 'ap-mumbai-1', 'ap-hyderabad-1', 'ap-singapore-1', ], 'Middle East & Africa': [ 'me-jeddah-1', 'me-dubai-1', 'me-abudhabi-1', 'af-johannesburg-1', 'il-jerusalem-1', ], }; /* ── Progress estimation ── */ function estimateProgress(lines: string[]): number { if (!lines.length) return 0; const last = lines[lines.length - 1].toLowerCase(); // Walk backwards to find the most advanced keyword for (let i = lines.length - 1; i >= 0; i--) { const l = lines[i].toLowerCase(); if (l.includes('finished')) return 100; if (l.includes('best practices')) return 85; if (l.includes('writing cis reports') || l.includes('writing reports')) return 75; if (l.includes('cis summary report') || l.includes('summary report')) return 60; if (l.includes('processing regional')) return 35; if (l.includes('processing home region')) return 20; if (l.includes('identity domains') || l.includes('identity')) return 10; } // If there are lines but no keyword matched, at least show minimal progress if (last) return 5; return 0; } function scoreColor(score: number): string { if (score >= 80) return 'var(--gn)'; if (score >= 60) return '#f59e0b'; return 'var(--rd)'; } function statusBadge(status: string) { const t = useI18n.getState().t; const map: Record = { running: { bg: 'color-mix(in srgb, var(--bl) 12%, transparent)', fg: 'var(--bl)', label: t('rpt.inExecution') }, completed: { bg: 'var(--gnl)', fg: 'var(--gn)', label: t('rpt.completed') }, failed: { bg: 'var(--rdl)', fg: 'var(--rd)', label: t('rpt.failed') }, cancelled: { bg: 'color-mix(in srgb, var(--t4) 12%, transparent)', fg: 'var(--t4)', label: t('rpt.cancelled') }, }; const s = map[status] || { bg: 'var(--bg2)', fg: 'var(--t4)', label: status }; return ( {status === 'running' && } {status === 'completed' && } {status === 'failed' && } {status === 'cancelled' && } {s.label} ); } /* ── File Grouping Helpers ── */ function extractSection(name: string): string { const m = name.match(/^(?:cis|obp)_([A-Za-z_]+?)_\d/); if (m) return m[1].replace(/_/g, ' '); if (name.includes('summary')) return 'Summary'; if (name.includes('error')) return 'Error'; return 'Other'; } function formatFileSize(bytes: number): string { if (bytes < 1024) return `${bytes} B`; if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; } function fileExt(name: string): string { return (name.split('.').pop() || '').toLowerCase(); } const extColors: Record = { html: '#e44d26', csv: '#217346', json: '#f5a623', xlsx: '#217346', txt: '#6b7280', pdf: '#dc2626', }; const sectionIcons: Record = { 'Identity and Access Management': Lock, 'Networking': Globe, 'Compute': Server, 'Logging and Monitoring': Activity, 'Storage Object Storage': Database, 'Storage Block Volumes': Database, 'Storage File Storage Service': Database, 'Asset Management': Package, 'Summary': BarChart3, 'Error': AlertCircle, 'Other': FileText, }; function sectionSort(a: string, b: string): number { if (a === 'Summary') return -1; if (b === 'Summary') return 1; if (a === 'Other') return 1; if (b === 'Other') return -1; return a.localeCompare(b); } function groupBySection(files: ReportFile[]): Record { const map: Record = {}; for (const f of files) { const sec = extractSection(f.file_name); (map[sec] ||= []).push(f); } return map; } /* ── SVG Gauge ── */ function ComplianceGauge({ score }: { score: number }) { const radius = 54; const stroke = 10; const circumference = 2 * Math.PI * radius; const pct = Math.min(Math.max(score, 0), 100); const offset = circumference - (pct / 100) * circumference; const color = scoreColor(pct); return ( {pct.toFixed(1)}% Compliance ); } /* ── Region Multi-Select ── */ function RegionMultiSelect({ selected, onChange, regionsMap, }: { selected: string[]; onChange: (regions: string[]) => void; regionsMap: Record; }) { const { t } = useI18n(); const [open, setOpen] = useState(false); const [filter, setFilter] = useState(''); const ref = useRef(null); useEffect(() => { const handler = (e: MouseEvent) => { if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false); }; document.addEventListener('mousedown', handler); return () => document.removeEventListener('mousedown', handler); }, []); const f = filter.toLowerCase(); const toggle = (r: string) => { if (selected.includes(r)) { onChange(selected.filter((s) => s !== r)); } else { onChange([...selected, r]); } }; const removeTag = (r: string) => onChange(selected.filter((s) => s !== r)); return (
setOpen(!open)} className="flex items-center flex-wrap gap-1.5 cursor-pointer px-3 py-2 rounded-lg min-h-[36px]" style={{ background: 'var(--bg2)', border: '1.5px solid var(--bd)' }} > {selected.length === 0 ? ( {t('rpt.allRegionsDefault')} ) : ( selected.map((r) => ( {r} { e.stopPropagation(); removeTag(r); }} /> )) )}
{open && (
setFilter(e.target.value)} onClick={(e) => e.stopPropagation()} className="w-full border-none outline-none text-xs py-1.5" style={{ background: 'transparent', color: 'var(--t1)' }} autoFocus />
{Object.entries(regionsMap).map(([grp, regs]) => { const filtered = regs.filter((r) => !f || r.includes(f)); if (!filtered.length) return null; return (
{grp}
{filtered.map((r) => { const isSelected = selected.includes(r); return (
toggle(r)} className="flex items-center gap-2 px-3 py-1.5 text-xs cursor-pointer transition-colors" style={{ color: isSelected ? 'var(--ac)' : 'var(--t2)', background: isSelected ? 'color-mix(in srgb, var(--ac) 8%, transparent)' : undefined, }} onMouseEnter={(e) => { if (!isSelected) (e.currentTarget.style.background = 'var(--bg2)'); }} onMouseLeave={(e) => { if (!isSelected) (e.currentTarget.style.background = ''); }} >
{isSelected && }
{r}
); })}
); })}
)}
); } /* ── OCI Config Dropdown ── */ function ConfigDropdown({ configs, value, onChange, }: { configs: { id: string; tenancy_name: string; region: string }[]; value: string; onChange: (id: string) => void; }) { const { t } = useI18n(); const [open, setOpen] = useState(false); const [filter, setFilter] = useState(''); const ref = useRef(null); useEffect(() => { const handler = (e: MouseEvent) => { if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false); }; document.addEventListener('mousedown', handler); return () => document.removeEventListener('mousedown', handler); }, []); const f = filter.toLowerCase(); const selected = configs.find((c) => c.id === value); const filtered = configs.filter( (c) => !f || c.tenancy_name.toLowerCase().includes(f) || c.region.toLowerCase().includes(f), ); return (
setOpen(!open)} className="flex items-center justify-between cursor-pointer px-3 py-2 rounded-lg text-[.78rem]" style={{ background: 'var(--bg2)', border: '1.5px solid var(--bd)', minHeight: 36 }} > {selected ? `${selected.tenancy_name} (${selected.region})` : t('rpt.selectOciCred')}
{open && (
setFilter(e.target.value)} onClick={(e) => e.stopPropagation()} className="w-full border-none outline-none text-xs py-1.5" style={{ background: 'transparent', color: 'var(--t1)' }} autoFocus />
{filtered.length === 0 ? (
{t('rpt.noCredentials')}
) : ( filtered.map((c) => (
{ onChange(c.id); setOpen(false); setFilter(''); }} className="px-3 py-2 text-xs cursor-pointer transition-colors" style={{ color: value === c.id ? 'var(--ac)' : 'var(--t2)', background: value === c.id ? 'color-mix(in srgb, var(--ac) 8%, transparent)' : undefined, }} onMouseEnter={(e) => { if (value !== c.id) (e.currentTarget.style.background = 'var(--bg2)'); }} onMouseLeave={(e) => { if (value !== c.id) (e.currentTarget.style.background = ''); }} >
{c.tenancy_name}
{c.region}
)) )}
)}
); } /* ── Report Selector ── */ function ReportSelector({ reports, value, onChange, }: { reports: Report[]; value: string; onChange: (id: string) => void; }) { const { t } = useI18n(); const [open, setOpen] = useState(false); const [filter, setFilter] = useState(''); const ref = useRef(null); useEffect(() => { const handler = (e: MouseEvent) => { if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false); }; document.addEventListener('mousedown', handler); return () => document.removeEventListener('mousedown', handler); }, []); const f = filter.toLowerCase(); const completed = reports.filter((r) => r.status === 'completed'); const selected = completed.find((r) => r.id === value); const filtered = completed.filter( (c) => !f || c.tenancy_name.toLowerCase().includes(f) || c.created_at.toLowerCase().includes(f), ); return (
setOpen(!open)} className="flex items-center justify-between cursor-pointer px-3 py-2 rounded-lg text-[.78rem]" style={{ background: 'var(--bg2)', border: '1.5px solid var(--bd)', minHeight: 36 }} > {selected ? `${selected.tenancy_name} — Level ${selected.level || 2} — ${selected.created_at}` : t('rpt.selectCompletedReport')}
{open && (
setFilter(e.target.value)} onClick={(e) => e.stopPropagation()} className="w-full border-none outline-none text-xs py-1.5" style={{ background: 'transparent', color: 'var(--t1)' }} autoFocus />
{filtered.length === 0 ? (
{t('rpt.noReports')}
) : ( filtered.map((r) => (
{ onChange(r.id); setOpen(false); setFilter(''); }} className="px-3 py-2 text-xs cursor-pointer transition-colors" style={{ color: value === r.id ? 'var(--ac)' : 'var(--t2)', background: value === r.id ? 'color-mix(in srgb, var(--ac) 8%, transparent)' : undefined, }} onMouseEnter={(e) => { if (value !== r.id) (e.currentTarget.style.background = 'var(--bg2)'); }} onMouseLeave={(e) => { if (value !== r.id) (e.currentTarget.style.background = ''); }} >
{r.tenancy_name}
Level {r.level || 2} | {r.created_at}
)) )}
)}
); } /* ── Recharts Custom Tooltip ── */ function CustomTooltip({ active, payload, label }: { active?: boolean; payload?: { value: number; name: string; fill?: string }[]; label?: string }) { if (!active || !payload?.length) return null; return (
{label &&
{label}
} {payload.map((p, i) => (
{p.name}: {p.value}
))}
); } /* ── Main Component ── */ export default function ReportsPage() { const { t } = useI18n(); const store = useAppStore(); const ociCfg = store.ociCfg; const ociRegions = store.ociRegions; const adbCfg = store.adbCfg; /* ── Reports list ── */ const [reports, setReports] = useState([]); const [reportsLoading, setReportsLoading] = useState(true); /* ── Form state (from store) ── */ const formOpen = store.rptFormOpen; const setFormOpen = store.setRptFormOpen; const ociVal = store.rptOciVal; const setOciVal = store.setRptOciVal; const level = store.rptLevel; const setLevel = store.setRptLevel; const selectedRegions = store.rptSelectedRegions; const setSelectedRegions = store.setRptSelectedRegions; const obp = store.rptObp; const setObp = store.setRptObp; const raw = store.rptRaw; const setRaw = store.setRptRaw; const redact = store.rptRedact; const setRedact = store.setRptRedact; /* ── Tracking / progress (from store) ── */ const trackingId = store.rptTrackingId; const setTrackingId = store.setRptTrackingId; const [progress, setProgress] = useState(null); const [logExpanded, setLogExpanded] = useState(false); /* ── Selected report + KPI (from store) ── */ const selectedRid = store.rptSelectedRid; const setSelectedRid = store.setRptSelectedRid; const [kpi, setKpi] = useState(null); const [kpiLoading, setKpiLoading] = useState(false); /* ── Report files ── */ const [files, setFiles] = useState([]); /* ── Embedding state (from store) ── */ const embedAdb = store.rptEmbedAdb; const setEmbedAdb = store.setRptEmbedAdb; const embedTable = store.rptEmbedTable; const setEmbedTable = store.setRptEmbedTable; const [embedLoading, setEmbedLoading] = useState(''); // '' = idle, 'all' = full embed, section name = section embed const [embedMsg, setEmbedMsg] = useState<{ type: 's' | 'e'; text: string } | null>(null); /* ── HTML iframe (from store) ── */ const showIframe = store.rptShowIframe; const setShowIframe = store.setRptShowIframe; const showCompliance = store.rptShowCompliance; const setShowCompliance = store.setRptShowCompliance; const [complianceReady, setComplianceReady] = useState(false); const [complianceGenerating, setComplianceGenerating] = useState(false); /* ── Error ── */ const [error, setError] = useState(''); /* ── Regions map ── */ const regionsMap = useMemo>(() => { if (ociRegions && typeof ociRegions === 'object') { const entries = Object.entries(ociRegions); if (entries.length > 0) { const first = entries[0][1]; if (Array.isArray(first)) return ociRegions as unknown as Record; const flat = entries.map(([, v]) => v as unknown as string); if (flat.length > 0) return { [t('rpt.regions')]: flat }; } } return FALLBACK_REGIONS; }, [ociRegions]); /* ── Init embed ADB ── */ useEffect(() => { if (!embedAdb && adbCfg.length) { const active = adbCfg.find((c) => c.is_active); const cfg = active || adbCfg[0]; setEmbedAdb(cfg.id); const tables = (cfg.tables || []).filter((t) => t.is_active); setEmbedTable(tables[0]?.table_name || ''); } }, [adbCfg, embedAdb]); /* ── Update table when ADB changes ── */ const embedAdbTables = useMemo(() => { const cfg = adbCfg.find((c) => c.id === embedAdb); return (cfg?.tables || []).filter((t) => t.is_active); }, [adbCfg, embedAdb]); /* ── Load reports ── */ const loadReports = useCallback(async () => { setReportsLoading(true); setError(''); try { const data = await reportsApi.list(); setReports(data); // Auto-track if there's a running report const running = data.find((r) => r.status === 'running'); if (running && !trackingId) { setTrackingId(running.id); } // Auto-select most recent completed report if none selected if (!selectedRid && data.length > 0) { const completed = data.find((r) => r.status === 'completed'); if (completed) setSelectedRid(completed.id); } } catch (err) { setError(err instanceof Error ? err.message : t('rpt.errorLoadReports')); } finally { setReportsLoading(false); } }, [trackingId, selectedRid]); useEffect(() => { loadReports(); }, []); // eslint-disable-line react-hooks/exhaustive-deps /* ── Poll progress ── */ usePolling( async () => { if (!trackingId) return true; try { const p = await reportsApi.progress(trackingId); setProgress(p); if (p.status === 'completed' || p.status === 'failed' || p.status === 'cancelled') { // Refresh reports list await loadReports(); if (p.status === 'completed') { setSelectedRid(trackingId); setShowIframe(true); } setTrackingId(null); return true; // stop polling } } catch { // Continue polling on transient errors } return false; }, 3000, !!trackingId, ); /* ── Load KPI when selected report changes ── */ useEffect(() => { if (!selectedRid) { setKpi(null); setFiles([]); return; } let cancelled = false; setKpiLoading(true); Promise.all([ reportsApi.summary(selectedRid), reportsApi.files(selectedRid), reportsApi.complianceReportStatus(selectedRid).catch(() => ({ ready: false, generating: false } as { ready: boolean; generating: boolean })), ]) .then(([summaryData, filesData, compStatus]) => { if (!cancelled) { setKpi(summaryData); setFiles(filesData); setComplianceReady(!!compStatus.ready); setComplianceGenerating(!!compStatus.generating); } }) .catch(() => { if (!cancelled) { setKpi(null); setFiles([]); setComplianceReady(false); } }) .finally(() => { if (!cancelled) setKpiLoading(false); }); return () => { cancelled = true; }; }, [selectedRid]); /* ── Compliance report polling (uses same usePolling as CIS report) ── */ usePolling( async () => { if (!complianceGenerating || !selectedRid) return false; try { const s = await reportsApi.complianceReportStatus(selectedRid); if (s.ready) { setComplianceReady(true); setComplianceGenerating(false); setShowCompliance(true); return true; // stop polling } } catch { /* keep polling */ } return false; }, 3000, complianceGenerating && !!selectedRid, ); const startComplianceGeneration = useCallback(async () => { if (!selectedRid || complianceGenerating) return; setComplianceReady(false); setComplianceGenerating(true); try { await reportsApi.generateComplianceReport(selectedRid); } catch { /* ignore */ } // polling is already running via usePolling above }, [selectedRid, complianceGenerating]); /* ── Handlers ── */ const handleRun = async () => { if (!ociVal) { setError(t('rpt.selectOciCredError')); return; } if (trackingId) { setError(t('rpt.alreadyRunning')); return; } setError(''); setProgress(null); setLogExpanded(false); try { const result = await reportsApi.run({ config_id: ociVal, regions: selectedRegions.length > 0 ? selectedRegions : undefined, level, obp, raw, redact_output: redact, }); setTrackingId(result.report_id); setFormOpen(false); await loadReports(); } catch (err) { setError(err instanceof Error ? err.message : t('rpt.errorStartScan')); } }; const handleCancel = async () => { if (!trackingId) return; try { await reportsApi.cancel(trackingId); } catch { // ignore } }; const pollEmbeddingStatus = useCallback(async (taskId: string) => { setEmbedMsg({ type: 's', text: 'Embedding iniciado...' }); const poll = setInterval(async () => { try { const s = await reportsApi.embeddingStatus(taskId); if (s.status === 'running') { setEmbedMsg({ type: 's', text: s.message }); } else if (s.status === 'done') { clearInterval(poll); setEmbedMsg({ type: 's', text: s.message }); setEmbedLoading(''); } else if (s.status === 'error') { clearInterval(poll); setEmbedMsg({ type: 'e', text: s.message }); setEmbedLoading(''); } } catch { /* keep polling */ } }, 2000); setTimeout(() => { clearInterval(poll); setEmbedLoading(''); }, 300000); }, []); const handleEmbed = async (rid: string) => { const adbId = embedAdb || (adbCfg.length > 0 ? adbCfg[0].id : ''); if (!adbId) { setEmbedMsg({ type: 'e', text: t('rpt.selectAdb') }); return; } setEmbedLoading('all'); setEmbedMsg(null); try { const r = await reportsApi.embedReport(rid, adbId); if (r.task_id) { pollEmbeddingStatus(r.task_id); } else { setEmbedMsg({ type: 's', text: r.message }); setEmbedLoading(''); } } catch (err) { setEmbedMsg({ type: 'e', text: err instanceof Error ? err.message : t('rpt.errorEmbedding') }); setEmbedLoading(''); } }; const handleEmbedFile = async (rid: string, fid: string) => { if (!embedAdb) { setEmbedMsg({ type: 'e', text: t('rpt.selectAdb') }); return; } if (!embedTable) { setEmbedMsg({ type: 'e', text: t('rpt.selectTable') }); return; } setEmbedLoading('all'); setEmbedMsg(null); try { const r = await reportsApi.embedFile(rid, fid, embedAdb, embedTable); if (r.task_id) { pollEmbeddingStatus(r.task_id); } else { setEmbedMsg({ type: 's', text: r.message }); setEmbedLoading(''); } } catch (err) { setEmbedMsg({ type: 'e', text: err instanceof Error ? err.message : t('rpt.errorEmbedding') }); setEmbedLoading(''); } }; const handleEmbedSection = async (secName: string, sectionFiles: ReportFile[]) => { const csvFiles = sectionFiles.filter((f) => f.file_name.endsWith('.csv')); if (!csvFiles.length) return; const adbId = embedAdb || (adbCfg.length > 0 ? adbCfg[0].id : ''); if (!adbId) { setEmbedMsg({ type: 'e', text: t('rpt.selectAdb') }); return; } setEmbedLoading(secName); setEmbedMsg(null); try { const r = await reportsApi.embedSection(selectedRid, csvFiles.map((f) => f.file_name), adbId); if (r.task_id) { pollEmbeddingStatus(r.task_id); } else { setEmbedMsg({ type: 's', text: r.message }); setEmbedLoading(''); } } catch (err) { setEmbedMsg({ type: 'e', text: err instanceof Error ? err.message : t('rpt.errorEmbedding') }); setEmbedLoading(''); } }; const handleRefresh = () => { loadReports(); }; /* ── Derived data ── */ const isRunning = !!trackingId; const pct = progress ? estimateProgress(progress.progress) : 0; const lastLine = progress?.progress?.length ? progress.progress[progress.progress.length - 1] : ''; /* ── KPI chart data ── */ const pieData = kpi ? [ { name: t('rpt.compliant'), value: kpi.passed, fill: 'var(--gn)' }, { name: t('rpt.nonCompliant'), value: kpi.failed, fill: 'var(--rd)' }, ] : []; const barData = kpi ? Object.entries(kpi.sections) .map(([name, s]) => ({ name: name.length > 25 ? name.slice(0, 25) + '...' : name, fullName: name, failed: s.failed, passed: s.passed })) .sort((a, b) => b.failed - a.failed) : []; /* ── Style helpers ── */ const cardStyle = { background: 'var(--bg1)', border: '1px solid var(--bd)' }; const labelCls = "block text-[.72rem] font-semibold mb-1"; const labelStyle = { color: 'var(--t3)' }; return (
{/* ═══ Header ═══ */}

{t('rpt.pageTitle')}

{t('rpt.pageSubtitle')}

{/* ═══ Error ═══ */} {error && (
{error} setError('')} />
)} {/* ═══ Run Report Form (collapsible) ═══ */}
setFormOpen(!formOpen)} > {t('rpt.runScanCis')} {isRunning && ( {t('rpt.inExecution')} )}
{formOpen && (

{t('rpt.configParams')}

{/* Row 1: Config + Level */}
{([1, 2] as const).map((l) => ( ))}

{level === 1 ? t('rpt.level1Desc') : t('rpt.level2Desc')}

{/* Row 2: Regions */}

{t('rpt.leaveEmpty')}

{/* Row 3: Checkboxes */}
{[ { id: 'obp', label: 'OCI Best Practices', checked: obp, set: setObp }, { id: 'raw', label: 'Raw Data', checked: raw, set: setRaw }, { id: 'redact', label: 'Redact OCIDs', checked: redact, set: setRedact }, ].map(({ id, label, checked, set }) => ( ))}
{/* Execute button */}
)}
{/* ═══ Progress Card ═══ */} {(isRunning || (progress && progress.status !== 'completed')) && progress && (
{t('rpt.scanProgress')} {pct}%
{/* Progress bar */}
{/* Last status line */} {lastLine && (
{lastLine}
)} {/* Collapsible log */}
{logExpanded && (
{progress.progress.map((line, i) => (
{line}
))}
)}
{/* Cancel button */} {isRunning && ( )} {/* Error message for failed */} {progress.status === 'failed' && progress.error_msg && (
{progress.error_msg}
)}
)} {/* ═══ Report Selector ═══ */}
{t('rpt.selectReport')}
{/* ═══ KPI Dashboard ═══ */} {kpiLoading && (
{t('rpt.loadingDashboard')}
)} {kpi && !kpiLoading && ( <> {/* KPI Cards */}
{/* Score Gauge */}
{t('rpt.complianceScore')}
{/* Compliant */}
{t('rpt.compliant')}
{kpi.passed}
{kpi.total > 0 ? ((kpi.passed / kpi.total) * 100).toFixed(1) : 0}% {t('rpt.ofTotal')}
{kpi.total_compliant} {t('rpt.compliantItems')}
{/* Non-Compliant */}
{t('rpt.nonCompliant')}
{kpi.failed}
{kpi.total_findings} {t('rpt.totalFindings')}
{/* Total Evaluated */}
{t('rpt.evaluated')}
{kpi.total}
{t('rpt.controlsVerified')}
Level {kpi.level} | {kpi.regions.length} {t('rpt.regionsCount')}
{/* Charts Row */}
{/* Donut Chart */}

{t('rpt.complianceDist')}

{pieData.map((entry, i) => ( ))} } />
{t('rpt.compliant')} ({kpi.passed})
{t('rpt.nonCompliant')} ({kpi.failed})
{/* Horizontal Bar Chart */}

{t('rpt.findingsBySection')}

} />
{/* Summary info */}
{t('rpt.tenancy')}: {kpi.tenancy} {t('rpt.level')}: {kpi.level} {t('rpt.regions')}: {kpi.regions.join(', ')} {t('rpt.generatedAt')}: {kpi.generated_at}
)} {/* ═══ HTML Report Iframe ═══ */} {selectedRid && (
setShowIframe(!showIframe)} >
{showIframe ? : } {t('rpt.htmlReport')}
{showIframe && (