feat: simplify assurance reports with progressive disclosure
This commit is contained in:
@@ -656,6 +656,31 @@ def report_html(report: dict[str, Any], kind: str) -> str:
|
||||
("Data quality", report.get("data_quality", {}).get("status")),
|
||||
]
|
||||
findings = report.get("findings", [])
|
||||
statuses = report.get("details", {}).get("by_status", [])
|
||||
maximum = max(
|
||||
[float(item.get("count", 0) or 0) for item in statuses] or [1]
|
||||
)
|
||||
total_runs = float(summary.get("runs", 0) or 0)
|
||||
outcome_rows = "".join(
|
||||
'<li><div><span><strong>%s</strong><small>%s%% of selected runs</small></span><b>%s</b></div>'
|
||||
'<i><em class="%s" style="width:%s%%"></em></i></li>' % (
|
||||
escape(_text(item.get("status"), "unknown").replace("_", " ")),
|
||||
round((float(item.get("count", 0) or 0) / total_runs) * 100)
|
||||
if total_runs else 0,
|
||||
escape(str(int(float(item.get("count", 0) or 0)))),
|
||||
"fail" if item.get("status") == "failed" else
|
||||
"attention" if item.get("status") == "degraded" else "pass",
|
||||
max((float(item.get("count", 0) or 0) / maximum) * 100, 3)
|
||||
if float(item.get("count", 0) or 0) else 0,
|
||||
)
|
||||
for item in statuses
|
||||
)
|
||||
outcomes = (
|
||||
'<section class="panel"><h2>Run distribution</h2>'
|
||||
'<p class="sub">Canonical runtime status values, normalized against the largest observed group.</p>'
|
||||
f'<ol class="distribution">{outcome_rows}</ol></section>'
|
||||
if outcome_rows else ""
|
||||
)
|
||||
else:
|
||||
summary = report.get("summary", {})
|
||||
metrics = [
|
||||
@@ -668,6 +693,7 @@ def report_html(report: dict[str, Any], kind: str) -> str:
|
||||
{"code": gate.get("title"), "severity": gate.get("status"), "message": gate.get("reason")}
|
||||
for gate in report.get("gates", [])
|
||||
]
|
||||
outcomes = ""
|
||||
cards = "".join(
|
||||
f'<div class="metric"><span>{escape(str(label))}</span><strong>{escape(str(value if value is not None else "Unavailable"))}</strong></div>'
|
||||
for label, value in metrics
|
||||
@@ -692,10 +718,11 @@ main{{max-width:1060px;margin:auto;padding:44px 24px 72px}}header{{border-radius
|
||||
.grid{{display:grid;grid-template-columns:repeat(4,1fr);gap:14px;margin:18px 0}}.metric,.panel{{border:1px solid var(--line);border-radius:18px;background:white;padding:20px;box-shadow:0 10px 30px #10182808}}
|
||||
.metric span{{display:block;color:var(--muted)}}.metric strong{{display:block;margin-top:16px;font-size:24px;letter-spacing:-.03em}}.panel{{margin-top:18px;padding:26px}}h2{{margin:0 0 6px;font-size:20px}}.sub{{margin:0 0 20px;color:var(--muted);font-size:14px}}
|
||||
.finding{{border-left:4px solid var(--cyan);border-radius:10px;background:#f8fafc;padding:14px 16px;margin-top:10px}}.finding div{{display:flex;justify-content:space-between;gap:16px}}code{{font-weight:800}}em{{color:var(--muted);font-size:11px;text-transform:uppercase}}.finding p{{margin:8px 0 0;color:#344054;font-size:14px;line-height:1.55}}.clear{{border-radius:12px;background:#ecfdf3;color:var(--green);padding:16px}}
|
||||
.distribution{{display:grid;gap:16px;margin:20px 0 0;padding:0;list-style:none}}.distribution li>div{{display:flex;align-items:end;justify-content:space-between;gap:16px}}.distribution span strong,.distribution span small{{display:block}}.distribution span strong{{font-size:13px;text-transform:capitalize}}.distribution span small{{margin-top:4px;color:var(--muted);font-size:11px}}.distribution b{{font:700 12px ui-monospace,monospace}}.distribution i{{display:block;overflow:hidden;height:8px;margin-top:8px;border-radius:99px;background:#eaecf0}}.distribution em{{display:block;height:100%;border-radius:inherit;background:var(--cyan)}}.distribution em.pass{{background:#12b76a}}.distribution em.attention{{background:#f79009}}.distribution em.fail{{background:#f04438}}
|
||||
details{{margin-top:18px}}summary{{cursor:pointer;font-weight:700}}pre{{overflow:auto;max-height:720px;border-radius:14px;background:#101828;color:#d0d5dd;padding:20px;font:11px/1.6 ui-monospace,monospace}}
|
||||
footer{{margin-top:22px;color:var(--muted);font-size:11px}}@media(max-width:760px){{h1{{font-size:32px}}.grid{{grid-template-columns:1fr 1fr}}header{{padding:28px}}}}@media print{{body{{background:white}}main{{max-width:none;padding:0}}header,.panel,.metric{{box-shadow:none}}details{{break-before:page}}}}
|
||||
</style></head><body><main><header><div class="eyebrow">CASAN · Independent assurance dossier</div><h1>{escape(str(title))}</h1>
|
||||
<div class="meta"><span>{escape(str(report.get("report_id", "")))}</span><span>Generated {escape(str(report.get("generated_at", "")))}</span></div><div class="badge">Verdict · {escape(verdict)}</div></header>
|
||||
<section class="grid">{cards}</section><section class="panel"><h2>Decision evidence</h2><p class="sub">Threshold findings and control outcomes from canonical local evidence.</p>{rows}</section>
|
||||
<section class="grid">{cards}</section><section class="panel"><h2>Decision evidence</h2><p class="sub">Threshold findings and control outcomes from canonical local evidence.</p>{rows}</section>{outcomes}
|
||||
<section class="panel"><h2>Machine-auditable contract</h2><p class="sub">The complete sanitized report payload used to render this dossier.</p><details><summary>Open JSON evidence</summary><pre>{contract}</pre></details></section>
|
||||
<footer>Generated on demand by CASAN Core Local Assurance Viewer · read-only · offline-capable</footer></main></body></html>"""
|
||||
|
||||
Reference in New Issue
Block a user