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

View File

@@ -4272,7 +4272,7 @@ def _search_cis_remediation(title: str, rec_num: str, user_id: str) -> dict:
# Try second chunk if first has no remediation
if not rem and len(matched) > 1:
rem = _extract_remediation_section(matched[1])
return {"description": desc, "remediation": rem or best}
return {"description": desc, "remediation": rem}
except Exception as e:
log.warning(f"CIS remediation search failed for {adb_cfg.get('config_name','?')}: {e}")
return {"description": "", "remediation": ""}
@@ -4323,6 +4323,7 @@ def _build_compliance_html(filtered_rows: list, tenancy_name: str, extract_date:
for sec_name, sd in sections_data.items():
sec_id = f"section-{slug(sec_name)}"
sections_html += f'<div class="detail-section page-break-before" id="{esc(sec_id)}">'
sections_html += '<div class="oracle-brand">ORACLE</div>'
sections_html += f'<h1 class="section-num">{esc(sec_name)}</h1>'
for r in sd["items"]:
@@ -4377,7 +4378,7 @@ def _build_compliance_html(filtered_rows: list, tenancy_name: str, extract_date:
fid = file_id_map.get(csv_filename, "")
if fid and rid:
dl_url = f"/api/reports/{rid}/files/{fid}/download"
findings_file_html = f'<div class="findings-file"><a href="{dl_url}" target="_blank" class="file-link">📄 {esc(csv_filename)}</a><span class="file-hint">Affected resources details</span></div>'
findings_file_html = f'<div class="findings-file"><a href="{dl_url}" target="_blank" class="file-link">[CSV] {esc(csv_filename)}</a><span class="file-hint">Affected resources details</span></div>'
sections_html += f'''
<div class="finding" id="{esc(rec_id)}">
@@ -4427,125 +4428,169 @@ def _build_compliance_html(filtered_rows: list, tenancy_name: str, extract_date:
<meta charset="utf-8"/>
<title>LAD A-Team CIS Compliance Report — {esc(tenancy_name)}</title>
<style>
@page {{ size: A4; margin: 14mm; }}
@media print {{ .no-print {{ display: none !important; }} body {{ background: #fff !important; }} }}
@page {{ size: A4; margin: 0; }}
@media screen {{
.print-header-space, .print-footer-space {{ display: none; }}
}}
@media print {{
.no-print {{ display: none !important; }}
body {{ background: #fff !important; margin: 0; padding: 0; }}
html {{ margin: 0; padding: 0; }}
.wrap {{ margin-left: calc(var(--strip-w) + 10px); padding: 0 14mm 0 14px; max-width: none; }}
.page-strip {{ width: var(--strip-w); }}
/* Table wrapper trick: thead/tfoot repeat on every printed page */
.print-wrapper {{ width: 100%; }}
.print-wrapper > thead {{ display: table-header-group; }}
.print-wrapper > tfoot {{ display: table-footer-group; }}
.print-header-space {{ height: 16mm; display: block; }}
.print-footer-space {{ height: 16mm; display: block; }}
}}
html,body {{ -webkit-print-color-adjust:exact; print-color-adjust:exact; }}
:root {{
--text:#0f172a; --muted:#64748b; --line:#e2e8f0; --soft:#f8fafc;
--oracle-red:#ff0000; --header-blue:#2f5597; --row-alt:#e8f0f7;
--text:#1a1a1a; --muted:#64748b; --line:#d1d5db; --soft:#f8fafc;
--oracle-red:#c74634; --header-green:#4a7c59; --row-alt:#eef3f0;
--fail:#c00000; --pass:#0f7b0f; --bad:#ef4444; --warn:#f59e0b; --ok:#22c55e;
--strip-w:28px;
}}
body {{ margin:0; font-family:Arial,Helvetica,sans-serif; color:var(--text); background:#fff; font-size:12px; line-height:1.5; }}
.wrap {{ max-width:960px; margin:0 auto; padding:0 14px; }}
a {{ color:var(--header-blue); text-decoration:none; }}
body {{ margin:0; font-family:'Oracle Sans','Segoe UI',Arial,Helvetica,sans-serif; color:var(--text); background:#fff; font-size:11.5px; line-height:1.6; }}
/* Decorative left strip on every page */
.page-strip {{ position:fixed; top:0; left:0; width:var(--strip-w); height:100%; z-index:100;
background: linear-gradient(180deg, #6b7f3a 0%, #8a9a4e 8%, #4a6741 16%, #2d3b2a 24%, #c4a94d 32%, #8b6d3b 40%, #3d5c3a 48%, #6b7f3a 56%, #c4a94d 64%, #4a6741 72%, #2d3b2a 80%, #8a9a4e 88%, #6b7f3a 100%);
}}
@media print {{
.page-strip {{ position:fixed; top:0; left:0; width:var(--strip-w); height:100vh; }}
}}
.wrap {{ margin-left:calc(var(--strip-w) + 10px); padding:0 28px 20px 20px; max-width:720px; }}
/* Oracle brand on every page (via running header simulation) */
.oracle-brand {{ font-family:'Oracle Sans',Arial,sans-serif; font-weight:700; letter-spacing:.8px; color:var(--oracle-red); font-size:15px; padding:18px 0 12px; }}
a {{ color:#2f5597; text-decoration:none; }}
a:hover {{ text-decoration:underline; }}
.page-break-before {{ break-before:page; page-break-before:always; }}
/* Cover */
.cover {{ position:relative; min-height:calc(297mm - 28mm); page-break-after:always; }}
.cover-oracle {{ position:absolute; top:0; left:0; font-weight:700; letter-spacing:.6px; color:var(--oracle-red); font-size:18px; }}
.cover-rect {{ position:absolute; top:26mm; left:6mm; right:6mm; height:82mm; border:1px solid #9ca3af; }}
.cover-title {{ position:absolute; left:0; bottom:62mm; font-size:42px; line-height:1.08; font-weight:500; color:#111827; max-width:165mm; }}
.cover-subtitle {{ position:absolute; left:0; bottom:52mm; font-size:12px; color:#111827; }}
.cover-meta {{ position:absolute; left:0; bottom:18mm; font-size:11px; color:#111827; line-height:1.45; }}
.cover {{ position:relative; min-height:calc(297mm - 40mm); page-break-after:always; display:flex; flex-direction:column; }}
.cover-oracle {{ font-weight:700; letter-spacing:.8px; color:var(--oracle-red); font-size:15px; padding-top:18px; }}
.cover-rect {{ display:none; }}
.cover-spacer {{ flex:1; min-height:120px; }}
.cover-title {{ font-size:38px; line-height:1.12; font-weight:400; color:#111827; max-width:500px; margin-bottom:12px; }}
.cover-subtitle {{ font-size:12.5px; color:#374151; margin-bottom:6px; font-style:italic; }}
.cover-meta {{ font-size:10.5px; color:#374151; line-height:1.6; margin-top:8px; }}
.cover-bottom {{ min-height:80px; }}
/* Section page header */
.section-header-brand {{ font-weight:700; letter-spacing:.8px; color:var(--oracle-red); font-size:15px; padding:18px 0 14px; }}
/* Text blocks */
.text-block {{ border:1px solid var(--line); border-radius:10px; padding:14px 18px; margin:0 0 14px; break-inside:avoid; page-break-inside:avoid; }}
.text-block h2 {{ font-size:14px; margin:0 0 8px; color:var(--oracle-red); }}
.text-block p {{ margin:0; font-size:11px; line-height:1.5; white-space:pre-wrap; overflow-wrap:anywhere; text-align:justify; }}
.text-block {{ margin:0 0 20px; }}
.text-block h2 {{ font-size:18px; font-weight:400; margin:0 0 10px; color:var(--oracle-red); }}
.text-block p {{ margin:0 0 10px; font-size:11px; line-height:1.65; white-space:pre-wrap; overflow-wrap:anywhere; text-align:justify; color:#374151; }}
/* TOC */
.toc {{ margin:0 0 14px; }}
.toc-brand {{ color:var(--oracle-red); font-weight:700; letter-spacing:.6px; font-size:16px; margin:0 0 6px; }}
.toc-heading {{ color:var(--oracle-red); font-size:14px; margin:0 0 10px; }}
.toc-rule {{ height:1px; background:#cbd5e1; margin:0 0 10px; }}
.toc-lines {{ display:flex; flex-direction:column; gap:5px; font-size:11px; }}
.toc {{ margin:0 0 20px; }}
.toc-heading {{ color:var(--oracle-red); font-size:18px; font-weight:400; margin:0 0 10px; }}
.toc-rule {{ height:2px; background:var(--oracle-red); margin:0 0 14px; max-width:400px; }}
.toc-lines {{ display:flex; flex-direction:column; gap:3px; font-size:11px; }}
.toc-line {{ display:flex; align-items:baseline; gap:6px; }}
.toc-l0 {{ font-weight:700; }}
.toc-l1 {{ padding-left:16px; font-weight:400; color:#374151; }}
.toc-l0 {{ font-weight:700; padding:2px 0; }}
.toc-l1 {{ padding-left:20px; font-weight:400; color:#374151; }}
.toc-line a {{ flex:1; min-width:0; overflow-wrap:anywhere; }}
.toc-count {{ font-weight:700; color:var(--muted); min-width:20px; text-align:right; }}
/* Summary tables */
.summary-card {{ border:1px solid var(--line); border-radius:10px; padding:14px 18px; margin:0 0 14px; }}
.summary-card h1 {{ font-size:16px; margin:0 0 4px; }}
.summary-card .sub {{ font-size:11px; color:var(--muted); margin:0 0 10px; }}
table.domains {{ width:100%; border-collapse:collapse; border:1px solid #cbd5e1; font-size:11px; }}
table.domains thead th {{ background:var(--header-blue); color:#fff; padding:7px 10px; text-transform:uppercase; letter-spacing:.4px; font-size:10px; text-align:center; border-right:1px solid rgba(255,255,255,.2); }}
.summary-card {{ margin:0 0 20px; }}
.summary-card h1 {{ font-size:18px; font-weight:400; margin:0 0 4px; color:var(--oracle-red); }}
.summary-card .sub {{ font-size:11px; color:var(--muted); margin:0 0 12px; }}
table.domains {{ width:100%; border-collapse:collapse; border:1px solid #bbb; font-size:11px; }}
table.domains thead {{ display:table-header-group; }}
table.domains thead th {{ background:var(--header-green); color:#fff; padding:8px 12px; text-transform:uppercase; letter-spacing:.4px; font-size:10px; text-align:center; border-right:1px solid rgba(255,255,255,.25); }}
table.domains thead th:first-child {{ text-align:left; }}
table.domains thead th:last-child {{ border-right:none; }}
table.domains tbody td {{ padding:6px 10px; border-top:1px solid #cbd5e1; border-right:1px solid #cbd5e1; }}
table.domains tbody td {{ padding:7px 12px; border-top:1px solid #ccc; border-right:1px solid #ddd; }}
table.domains tbody tr {{ break-inside:avoid; page-break-inside:avoid; }}
table.domains tbody td:last-child {{ border-right:none; }}
table.domains tbody tr:nth-child(odd) {{ background:var(--row-alt); }}
.dom {{ font-weight:600; }}
.num {{ text-align:center; font-variant-numeric:tabular-nums; }}
.fail {{ color:var(--fail); font-weight:700; }}
.pass {{ color:var(--pass); font-weight:700; }}
.totals {{ font-size:12px; margin-top:8px; color:var(--muted); }}
.totals {{ font-size:12px; margin-top:10px; color:var(--muted); }}
/* Benchmark summary table */
table.benchmark {{ width:100%; border-collapse:collapse; border:1px solid #cbd5e1; font-size:11px; }}
table.benchmark thead th {{ background:var(--header-blue); color:#fff; padding:7px 10px; text-align:left; font-size:10px; text-transform:uppercase; border-right:1px solid rgba(255,255,255,.2); }}
table.benchmark {{ width:100%; border-collapse:collapse; border:1px solid #bbb; font-size:11px; }}
table.benchmark thead {{ display:table-header-group; }}
table.benchmark thead th {{ background:var(--header-green); color:#fff; padding:8px 12px; text-align:left; font-size:10px; text-transform:uppercase; border-right:1px solid rgba(255,255,255,.25); }}
table.benchmark thead th:last-child {{ text-align:center; border-right:none; }}
table.benchmark tbody td {{ padding:5px 10px; border-top:1px solid #e5e7eb; }}
table.benchmark .section-header td {{ background:#f1f5f9; font-weight:700; font-size:11px; padding:8px 10px; border-top:2px solid #cbd5e1; }}
.rec-num {{ width:40px; font-weight:600; color:var(--header-blue); white-space:nowrap; }}
table.benchmark tbody td {{ padding:6px 12px; border-top:1px solid #ddd; }}
table.benchmark tbody tr {{ break-inside:avoid; page-break-inside:avoid; }}
table.benchmark .section-header {{ break-after:avoid; page-break-after:avoid; }}
table.benchmark .section-header td {{ background:#e8ece8; font-weight:700; font-size:11px; padding:8px 12px; border-top:2px solid #aaa; }}
.rec-num {{ width:42px; font-weight:600; color:#2f5597; white-space:nowrap; }}
.pass-text {{ color:var(--pass); font-weight:700; }}
.fail-text {{ color:var(--fail); font-weight:700; }}
/* Detail sections */
.detail-section {{ margin-top:20px; }}
.detail-section h1.section-num {{ font-size:18px; color:var(--header-blue); border-bottom:2px solid var(--header-blue); padding-bottom:6px; margin:0 0 16px; }}
.finding {{ border:1px solid var(--line); border-radius:10px; padding:14px 18px; margin:0 0 14px; break-inside:avoid; page-break-inside:avoid; }}
.finding h2 {{ font-size:13px; font-weight:700; margin:0 0 8px; color:#111827; line-height:1.35; }}
.detail-section {{ margin-top:0; }}
.detail-section h1.section-num {{ font-size:18px; color:var(--oracle-red); font-weight:400; border-bottom:none; padding-bottom:4px; margin:0 0 16px; }}
.finding {{ border-bottom:1px solid var(--line); padding:14px 0; margin:0; }}
.finding:last-child {{ border-bottom:none; }}
.finding h2 {{ font-size:12.5px; font-weight:700; margin:0 0 8px; color:#111827; line-height:1.4; }}
/* Result box */
.result-box {{ padding:8px 12px; border-radius:8px; font-size:11px; margin:8px 0; }}
.result-box.ok {{ background:#f0fdf4; border:1px solid #bbf7d0; }}
.result-box.fail {{ background:#fef2f2; border:1px solid #fecaca; }}
.result-box {{ padding:8px 12px; border-radius:6px; font-size:11px; margin:8px 0; }}
.result-box.ok {{ background:#f0fdf4; border-left:3px solid var(--pass); }}
.result-box.fail {{ background:#fef2f2; border-left:3px solid var(--fail); }}
/* Findings file link */
.findings-file {{ display:flex; align-items:center; gap:8px; margin:8px 0; padding:8px 12px; background:#fffbeb; border:1px solid #fde68a; border-radius:8px; }}
.file-link {{ font-size:12px; font-weight:700; color:var(--header-blue); text-decoration:none; }}
.findings-file {{ display:flex; align-items:center; gap:8px; margin:8px 0; padding:8px 12px; background:#fffbeb; border-left:3px solid #f59e0b; border-radius:4px; }}
.file-link {{ font-size:11.5px; font-weight:700; color:#2f5597; text-decoration:none; }}
.file-link:hover {{ text-decoration:underline; }}
.file-hint {{ font-size:10px; color:var(--muted); }}
@media print {{ .file-link {{ color:var(--header-blue) !important; }} }}
@media print {{ .file-link {{ color:#2f5597 !important; }} }}
/* Description */
.finding-desc {{ margin:10px 0 0; }}
.finding-desc h4 {{ font-size:12px; margin:0 0 6px; color:var(--header-blue); }}
.finding-desc p {{ font-size:11px; line-height:1.6; text-align:justify; color:#374151; margin:0; padding:10px 14px; background:#f0f4ff; border:1px solid #dbeafe; border-radius:8px; }}
.finding-desc h4 {{ font-size:11.5px; margin:0 0 6px; color:#2f5597; font-weight:700; }}
.finding-desc p {{ font-size:11px; line-height:1.65; text-align:justify; color:#374151; margin:0; padding:10px 14px; background:#f8f9fa; border-left:3px solid #2f5597; }}
/* Remediation */
.remediation {{ margin:10px 0 0; }}
.remediation h4 {{ font-size:12px; margin:0 0 6px; color:var(--oracle-red); }}
.remediation-content {{ font-size:11px; line-height:1.55; }}
.remediation h4 {{ font-size:11.5px; margin:0 0 6px; color:var(--oracle-red); font-weight:700; }}
.remediation-content {{ font-size:11px; line-height:1.6; }}
.rem-step {{ display:flex; gap:6px; padding:3px 0; align-items:flex-start; }}
.rem-step .step-num {{ font-weight:700; color:var(--header-blue); min-width:20px; }}
.rem-step .step-num {{ font-weight:700; color:#2f5597; min-width:20px; }}
.rem-text {{ margin:4px 0; text-align:justify; color:#374151; }}
.code-snippet {{ background:#1e293b; color:#e2e8f0; padding:10px 14px; border-radius:8px; font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace; font-size:11px; line-height:1.5; overflow-x:auto; white-space:pre-wrap; word-break:break-all; margin:6px 0; border-left:3px solid var(--header-blue); }}
.code-snippet {{ background:#1e293b; color:#e2e8f0; padding:10px 14px; border-radius:4px; font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace; font-size:10.5px; line-height:1.5; overflow-x:auto; white-space:pre-wrap; word-break:break-all; margin:6px 0; border-left:3px solid #2f5597; }}
/* Compliance bar */
.compliance-bar {{ display:flex; align-items:center; gap:10px; margin:6px 0 8px; }}
.bar-label {{ font-size:11px; font-weight:700; width:100px; white-space:nowrap; }}
.bar {{ flex:1; height:10px; background:#eef2ff; border:1px solid #e5e7eb; border-radius:999px; overflow:hidden; }}
.bar {{ flex:1; height:8px; background:#eef2ff; border:1px solid #e5e7eb; border-radius:999px; overflow:hidden; }}
.fill {{ height:100%; border-radius:999px; }}
.fill.bad {{ background:var(--bad); }}
.fill.warn {{ background:var(--warn); }}
.fill.ok {{ background:var(--ok); }}
/* Print button */
.print-btn {{ position:fixed; bottom:20px; right:20px; padding:12px 24px; background:var(--header-blue); color:#fff; border:none; border-radius:8px; font-size:14px; font-weight:700; cursor:pointer; box-shadow:0 4px 12px rgba(0,0,0,.3); z-index:999; }}
.print-btn:hover {{ background:#1e3a6e; }}
.print-btn {{ position:fixed; bottom:20px; right:20px; padding:12px 24px; background:var(--oracle-red); color:#fff; border:none; border-radius:6px; font-size:14px; font-weight:700; cursor:pointer; box-shadow:0 4px 12px rgba(0,0,0,.25); z-index:999; }}
.print-btn:hover {{ background:#a83a2c; }}
</style>
</head>
<body>
<!-- Decorative left strip -->
<div class="page-strip"></div>
<table class="print-wrapper"><thead><tr><td><div class="print-header-space"></div></td></tr></thead>
<tfoot><tr><td><div class="print-footer-space"></div></td></tr></tfoot>
<tbody><tr><td>
<div class="wrap">
<!-- COVER -->
<div class="cover">
<div class="cover-oracle">ORACLE</div>
<div class="cover-rect"></div>
<div class="cover-spacer"></div>
<div class="cover-title">Oracle Cloud Security<br/>Assessment</div>
<div class="cover-subtitle">{esc(tenancy_name)} Oracle Cloud Infrastructure</div>
<div class="cover-meta">
@@ -4555,9 +4600,11 @@ table.benchmark .section-header td {{ background:#f1f5f9; font-weight:700; font-
<div>Copyright &copy; {now.year}, Oracle and/or its affiliates</div>
<div>Confidential Oracle Restricted</div>
</div>
<div class="cover-bottom"></div>
</div>
<!-- PURPOSE + DISCLAIMER -->
<div class="oracle-brand">ORACLE</div>
<div class="text-block">
<h2>Purpose Statement</h2>
<p>{esc(PURPOSE_TEXT)}</p>
@@ -4568,45 +4615,57 @@ table.benchmark .section-header td {{ background:#f1f5f9; font-weight:700; font-
</div>
<!-- TOC -->
<div class="toc page-break-before">
<div class="toc-brand">ORACLE</div>
<div class="toc-heading">Table of contents</div>
<div class="toc-rule"></div>
<div class="toc-lines">{toc_lines}</div>
<div class="page-break-before">
<div class="oracle-brand">ORACLE</div>
<div class="toc">
<div class="toc-heading">Table of contents</div>
<div class="toc-rule"></div>
<div class="toc-lines">{toc_lines}</div>
</div>
</div>
<!-- SECURITY OVERVIEW -->
<div class="text-block page-break-before">
<h2>Security Overview</h2>
<p>{SECURITY_OVERVIEW}</p>
<div class="page-break-before">
<div class="oracle-brand">ORACLE</div>
<div class="text-block">
<h2>Security Overview</h2>
<p>{SECURITY_OVERVIEW}</p>
</div>
</div>
<!-- CIS SECURITY ASSESSMENT SUMMARY -->
<div class="summary-card page-break-before" id="cis-summary">
<h1>CIS Security Assessment</h1>
<div class="sub">CIS Oracle Cloud Infrastructure Foundations Benchmark v3.0.0</div>
<p style="margin-bottom:12px">Number of controls per domain and non-compliant controls on tenancy <b>{esc(tenancy_name)}</b>:</p>
<table class="domains">
<thead><tr><th style="text-align:left">DOMAINS</th><th>TOTAL CONTROLS</th><th>FAILED</th><th>PASSED</th></tr></thead>
<tbody>{domains_rows}</tbody>
</table>
<div class="totals">Total: <b>{total_all}</b> &bull; Passed: <b class="pass">{total_passed}</b> &bull; Failed: <b class="fail">{total_failed}</b> &bull; Compliance: <b>{pct_global}%</b></div>
<div class="page-break-before" id="cis-summary">
<div class="oracle-brand">ORACLE</div>
<div class="summary-card">
<h1>CIS Security Assessment</h1>
<div class="sub">CIS Oracle Cloud Infrastructure Foundations Benchmark v3.0.0</div>
<p style="margin-bottom:12px">Number of controls per domain and non-compliant controls on tenancy <b>{esc(tenancy_name)}</b>:</p>
<table class="domains">
<thead><tr><th style="text-align:left">DOMAINS</th><th>TOTAL CONTROLS</th><th>FAILED</th><th>PASSED</th></tr></thead>
<tbody>{domains_rows}</tbody>
</table>
<div class="totals">Total: <b>{total_all}</b> &bull; Passed: <b class="pass">{total_passed}</b> &bull; Failed: <b class="fail">{total_failed}</b> &bull; Compliance: <b>{pct_global}%</b></div>
</div>
</div>
<!-- CIS BENCHMARK RECOMMENDATION SUMMARY -->
<div class="summary-card page-break-before" id="benchmark-table">
<h1>CIS Benchmark Recommendation Summary</h1>
<div class="sub">Status per recommendation</div>
<table class="benchmark">
<thead><tr><th style="width:50px">Item</th><th>Description</th><th style="width:70px;text-align:center">Status</th></tr></thead>
<tbody>{summary_rows}</tbody>
</table>
<div class="page-break-before" id="benchmark-table">
<div class="oracle-brand">ORACLE</div>
<div class="summary-card">
<h1>CIS Benchmark Recommendation Summary</h1>
<div class="sub">Status per recommendation</div>
<table class="benchmark">
<thead><tr><th style="width:50px">Item</th><th>Description</th><th style="width:70px;text-align:center">Status</th></tr></thead>
<tbody>{summary_rows}</tbody>
</table>
</div>
</div>
<!-- DETAILED SECTIONS -->
{sections_html}
</div>
</td></tr></tbody></table>
<!-- Print/PDF button -->
<button class="print-btn no-print" onclick="window.print()">&#128438; Download PDF</button>