feat: remove legacy frontend, root routing, compliance ZIP with Chromium PDF, chat markdown styling
- Legacy frontend removed: React SPA served at / (no /app/ prefix) - Compliance report ZIP: Chromium headless --print-to-pdf (identical to browser) + CSV findings - Server-side generation state: .compliance_generating marker survives page navigation - RAG enriched: Description + Rationale + Remediation + Audit extracted from CIS vector chunks - Chat markdown: syntax highlighting (Catppuccin), styled headings/lists/tables/blockquotes/code - Default model auto-selected on Chat Agent load
This commit is contained in:
@@ -577,7 +577,6 @@ export default function ReportsPage() {
|
||||
const [showCompliance, setShowCompliance] = useState(false);
|
||||
const [complianceReady, setComplianceReady] = useState(false);
|
||||
const [complianceGenerating, setComplianceGenerating] = useState(false);
|
||||
const compliancePollRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||
|
||||
/* ── Error ── */
|
||||
const [error, setError] = useState('');
|
||||
@@ -625,12 +624,17 @@ export default function ReportsPage() {
|
||||
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]);
|
||||
}, [trackingId, selectedRid]);
|
||||
|
||||
useEffect(() => { loadReports(); }, []); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
@@ -672,14 +676,14 @@ export default function ReportsPage() {
|
||||
Promise.all([
|
||||
reportsApi.summary(selectedRid),
|
||||
reportsApi.files(selectedRid),
|
||||
reportsApi.complianceReportStatus(selectedRid).catch(() => ({ ready: false })),
|
||||
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(false);
|
||||
setComplianceReady(!!compStatus.ready);
|
||||
setComplianceGenerating(!!compStatus.generating);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
@@ -695,43 +699,34 @@ export default function ReportsPage() {
|
||||
return () => { cancelled = true; };
|
||||
}, [selectedRid]);
|
||||
|
||||
/* ── Compliance report polling (useEffect-based, survives re-renders + tab throttle) ── */
|
||||
useEffect(() => {
|
||||
if (!complianceGenerating || !selectedRid) return;
|
||||
let active = true;
|
||||
const check = async () => {
|
||||
/* ── 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 && active) {
|
||||
if (s.ready) {
|
||||
setComplianceReady(true);
|
||||
setComplianceGenerating(false);
|
||||
setShowCompliance(true);
|
||||
return true; // stop polling
|
||||
}
|
||||
} catch { /* keep polling */ }
|
||||
};
|
||||
check();
|
||||
const id = setInterval(check, 3000);
|
||||
compliancePollRef.current = id;
|
||||
const timeout = setTimeout(() => { clearInterval(id); if (active) setComplianceGenerating(false); }, 300000);
|
||||
return () => { active = false; clearInterval(id); clearTimeout(timeout); compliancePollRef.current = null; };
|
||||
}, [complianceGenerating, selectedRid]);
|
||||
|
||||
/* ── Re-check compliance status when section is expanded ── */
|
||||
useEffect(() => {
|
||||
if (!showCompliance || !selectedRid || complianceReady || complianceGenerating) return;
|
||||
reportsApi.complianceReportStatus(selectedRid)
|
||||
.then((s) => { if (s.ready) setComplianceReady(true); })
|
||||
.catch(() => {});
|
||||
}, [showCompliance, selectedRid, complianceReady, complianceGenerating]);
|
||||
return false;
|
||||
},
|
||||
3000,
|
||||
complianceGenerating && !!selectedRid,
|
||||
);
|
||||
|
||||
const startComplianceGeneration = useCallback(async () => {
|
||||
if (!selectedRid) return;
|
||||
setComplianceGenerating(true);
|
||||
if (!selectedRid || complianceGenerating) return;
|
||||
setComplianceReady(false);
|
||||
setComplianceGenerating(true);
|
||||
try {
|
||||
await reportsApi.generateComplianceReport(selectedRid);
|
||||
} catch { /* ignore */ }
|
||||
}, [selectedRid]);
|
||||
// polling is already running via usePolling above
|
||||
}, [selectedRid, complianceGenerating]);
|
||||
|
||||
/* ── Handlers ── */
|
||||
const handleRun = async () => {
|
||||
@@ -1337,7 +1332,14 @@ export default function ReportsPage() {
|
||||
style={{ height: 700, background: '#fff' }}
|
||||
title="LAD A-Team CIS Compliance Report"
|
||||
/>
|
||||
<div className="px-5 py-2 flex justify-end" style={{ borderTop: '1px solid var(--bd)' }}>
|
||||
<div className="px-5 py-2 flex justify-end gap-2" style={{ borderTop: '1px solid var(--bd)' }}>
|
||||
<a
|
||||
href={reportsApi.complianceReportZipUrl(selectedRid)}
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-[.68rem] font-medium cursor-pointer transition-colors no-underline"
|
||||
style={{ background: 'var(--ac)', color: '#fff' }}
|
||||
>
|
||||
<Download size={12} /> Download ZIP
|
||||
</a>
|
||||
<button
|
||||
onClick={startComplianceGeneration}
|
||||
disabled={complianceGenerating}
|
||||
|
||||
Reference in New Issue
Block a user