Files
tudo-para-ia-mais-humana-pl…/src/mais_humana/html_export.py
2026-04-30 06:42:00 -03:00

120 lines
4.3 KiB
Python

"""HTML export for quick local review of human reports."""
from __future__ import annotations
from pathlib import Path
from typing import Sequence
from .models import PlatformHumanReport, ReportBundle
from .quality import PlatformQualityReport
def esc(value: object) -> str:
return (
str(value)
.replace("&", "&")
.replace("<", "&lt;")
.replace(">", "&gt;")
.replace('"', "&quot;")
)
def html_shell(title: str, body: str) -> str:
return f"""<!doctype html>
<html lang="pt-BR">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{esc(title)}</title>
<style>
body {{ font-family: Arial, sans-serif; margin: 0; color: #18202a; background: #f8fafc; }}
header {{ padding: 28px 36px; background: #0f172a; color: #f8fafc; }}
main {{ padding: 24px 36px 56px; max-width: 1180px; margin: 0 auto; }}
table {{ border-collapse: collapse; width: 100%; background: white; }}
th, td {{ border: 1px solid #dbe3ef; padding: 8px 10px; text-align: left; vertical-align: top; }}
th {{ background: #e8eef7; }}
.grid {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 12px; }}
.card {{ background: white; border: 1px solid #dbe3ef; border-radius: 8px; padding: 14px; }}
.score {{ font-weight: 700; font-size: 22px; }}
.ok {{ color: #1f7a4d; }}
.warn {{ color: #a16207; }}
.blocker {{ color: #b42318; }}
</style>
</head>
<body>
<header><h1>{esc(title)}</h1></header>
<main>{body}</main>
</body>
</html>
"""
def report_card(report: PlatformHumanReport, quality: PlatformQualityReport | None) -> str:
quality_text = ""
if quality is not None:
status_class = "ok" if quality.human_ready else "blocker" if quality.blocker_count else "warn"
quality_text = (
f'<p class="{status_class}">technical_ready={quality.technical_ready}; '
f"human_ready={quality.human_ready}; blockers={quality.blocker_count}; warnings={quality.warning_count}</p>"
)
gaps = "".join(f"<li>{esc(gap)}</li>" for gap in report.missing_for_humans[:4])
return (
'<section class="card">'
f"<h2>{esc(report.platform.title)}</h2>"
f'<div class="score">{report.average_score}</div>'
f"<p>{esc(report.summary)}</p>"
+ quality_text
+ "<ul>"
+ gaps
+ "</ul>"
"</section>"
)
def index_html(reports: Sequence[PlatformHumanReport], qualities: Sequence[PlatformQualityReport], bundle: ReportBundle | None = None) -> str:
quality_by_platform = {quality.platform_id: quality for quality in qualities}
cards = "".join(report_card(report, quality_by_platform.get(report.platform.platform_id)) for report in reports)
rows = []
for report in sorted(reports, key=lambda item: item.platform.platform_id):
quality = quality_by_platform.get(report.platform.platform_id)
rows.append(
"<tr>"
f"<td>{esc(report.platform.platform_id)}</td>"
f"<td>{report.average_score}</td>"
f"<td>{report.scan.code_lines}</td>"
f"<td>{len(report.scan.evidence)}</td>"
f"<td>{esc(quality.human_ready if quality else 'n/a')}</td>"
f"<td>{esc('; '.join(report.scan.warnings[:3]))}</td>"
"</tr>"
)
intro = ""
if bundle is not None:
intro = (
f"<p>Gerados {len(bundle.generated_files)} arquivos, "
f"{bundle.matrix_cells} celulas de matriz e "
f"{bundle.total_code_lines_analyzed} linhas de codigo analisadas.</p>"
)
body = (
intro
+ '<section class="grid">'
+ cards
+ "</section>"
+ "<h2>Resumo tabular</h2>"
+ "<table><thead><tr><th>Plataforma</th><th>Score</th><th>Linhas</th><th>Evidencias</th><th>Human ready</th><th>Avisos</th></tr></thead><tbody>"
+ "".join(rows)
+ "</tbody></table>"
)
return html_shell("Mais Humana - indice operacional", body)
def write_index_html(
path: Path,
reports: Sequence[PlatformHumanReport],
qualities: Sequence[PlatformQualityReport],
bundle: ReportBundle | None = None,
) -> Path:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(index_html(reports, qualities, bundle), encoding="utf-8")
return path