fix: compliance report Oracle design, polling robustness, print margins

- Oracle PDF style: decorative left strip, ORACLE brand header, red section titles, green table headers
- Cover page: removed rectangle, clean layout matching Oracle reference PDF
- Print: @page margin:0 + thead/tfoot table trick for per-page header/footer spacing without browser URL/date
- RAG remediation: fixed fallback dumping entire CIS chunk instead of just remediation section
- Findings: removed break-inside:avoid that caused empty pages, emoji replaced with [CSV] text
- Tables: break-inside:avoid on rows, section headers break-after:avoid, thead repeats on page break
- Compliance polling: useEffect-based with cleanup, auto-expand on ready, re-check on section toggle
- Explorer: silent polling during start/stop (no flickering)
- HTML report starts minimized, auto-expands on fresh generation
- Include dist/ in git for deployment
This commit is contained in:
nogueiraguh
2026-03-19 11:34:31 -03:00
parent 442e710b9f
commit 5028f632a6
8 changed files with 365 additions and 108 deletions

23
frontend-react/.gitignore vendored Normal file
View File

@@ -0,0 +1,23 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

22
frontend-react/dist/index.html vendored Normal file
View File

@@ -0,0 +1,22 @@
<!doctype html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/app/logo.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>AI Agent - Infrastructure & Security Engineer</title>
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<script type="module" crossorigin src="/app/assets/index-BCYp2Z2l.js"></script>
<link rel="stylesheet" crossorigin href="/app/assets/index-D9WRGO1k.css">
</head>
<body>
<div id="root"></div>
<script>
// Restore theme before React hydration to avoid flash
(function() {
var t = localStorage.getItem('theme') || 'light';
document.documentElement.className = t;
})();
</script>
</body>
</html>

21
frontend-react/dist/logo.svg vendored Normal file
View File

@@ -0,0 +1,21 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36" width="128" height="128">
<!-- Oracle-style rounded rectangle -->
<rect x="2" y="5" width="32" height="26" rx="13" ry="13" fill="rgba(199,70,52,0.08)" stroke="#C74634" stroke-width="2"/>
<!-- Robot head -->
<rect x="11" y="11" width="14" height="14" rx="4" fill="#fff" stroke="#C74634" stroke-width="0.5"/>
<!-- Eyes -->
<circle cx="15" cy="17" r="2" fill="#C74634"/>
<circle cx="21" cy="17" r="2" fill="#C74634"/>
<circle cx="15" cy="16.7" r="0.7" fill="#fff"/>
<circle cx="21" cy="16.7" r="0.7" fill="#fff"/>
<!-- Mouth -->
<rect x="14" y="21" width="8" height="1.5" rx="0.75" fill="#C74634" opacity="0.6"/>
<!-- Antenna -->
<line x1="18" y1="11" x2="18" y2="6" stroke="#C74634" stroke-width="1.2" stroke-linecap="round"/>
<circle cx="18" cy="5.5" r="1.5" fill="#C74634" opacity="0.3" stroke="#C74634" stroke-width="0.8"/>
<!-- Ears/signal -->
<line x1="11" y1="18" x2="6" y2="18" stroke="#C74634" stroke-width="1" stroke-linecap="round" opacity="0.5"/>
<line x1="25" y1="18" x2="30" y2="18" stroke="#C74634" stroke-width="1" stroke-linecap="round" opacity="0.5"/>
<circle cx="5.5" cy="18" r="1" fill="#C74634" opacity="0.4"/>
<circle cx="30.5" cy="18" r="1" fill="#C74634" opacity="0.4"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -577,6 +577,7 @@ 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('');
@@ -645,6 +646,7 @@ export default function ReportsPage() {
await loadReports();
if (p.status === 'completed') {
setSelectedRid(trackingId);
setShowIframe(true);
}
setTrackingId(null);
return true; // stop polling
@@ -693,6 +695,44 @@ 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 () => {
try {
const s = await reportsApi.complianceReportStatus(selectedRid);
if (s.ready && active) {
setComplianceReady(true);
setComplianceGenerating(false);
setShowCompliance(true);
}
} 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]);
const startComplianceGeneration = useCallback(async () => {
if (!selectedRid) return;
setComplianceGenerating(true);
setComplianceReady(false);
try {
await reportsApi.generateComplianceReport(selectedRid);
} catch { /* ignore */ }
}, [selectedRid]);
/* ── Handlers ── */
const handleRun = async () => {
if (!ociVal) {
@@ -1299,20 +1339,7 @@ export default function ReportsPage() {
/>
<div className="px-5 py-2 flex justify-end" style={{ borderTop: '1px solid var(--bd)' }}>
<button
onClick={async () => {
setComplianceGenerating(true);
setComplianceReady(false);
try {
await reportsApi.generateComplianceReport(selectedRid);
} catch { /* ignore */ }
const poll = setInterval(async () => {
try {
const s = await reportsApi.complianceReportStatus(selectedRid);
if (s.ready) { clearInterval(poll); setComplianceReady(true); setComplianceGenerating(false); }
} catch { /* keep polling */ }
}, 3000);
setTimeout(() => { clearInterval(poll); setComplianceGenerating(false); }, 300000);
}}
onClick={startComplianceGeneration}
disabled={complianceGenerating}
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-[.68rem] font-medium cursor-pointer transition-colors"
style={{ background: 'var(--bg3)', color: 'var(--t2)', border: '1px solid var(--bd)' }}
@@ -1332,19 +1359,7 @@ export default function ReportsPage() {
<ShieldCheck size={32} style={{ color: 'var(--t4)', opacity: 0.5 }} />
<p className="text-[.78rem]" style={{ color: 'var(--t3)' }}>Report ainda não gerado</p>
<button
onClick={async () => {
setComplianceGenerating(true);
try {
await reportsApi.generateComplianceReport(selectedRid);
} catch { /* ignore */ }
const poll = setInterval(async () => {
try {
const s = await reportsApi.complianceReportStatus(selectedRid);
if (s.ready) { clearInterval(poll); setComplianceReady(true); setComplianceGenerating(false); }
} catch { /* keep polling */ }
}, 3000);
setTimeout(() => { clearInterval(poll); setComplianceGenerating(false); }, 300000);
}}
onClick={startComplianceGeneration}
className="flex items-center gap-2 px-4 py-2 rounded-lg text-[.76rem] font-semibold cursor-pointer"
style={{ background: 'var(--gn)', color: '#fff' }}
>