Polish project report artifact
GeoIntel CI / docs-smoke (push) Has been cancelled
GeoIntel CI / contract-smoke (push) Has been cancelled

This commit is contained in:
Codex
2026-06-18 23:44:36 +02:00
parent 61b833f119
commit 688c3434fc
6 changed files with 376 additions and 37 deletions
+204 -37
View File
@@ -437,10 +437,28 @@ class ExportService:
exports = summary["exports"]
readiness_summary = summary["readiness_summary"]
known_limitations = summary["known_limitations"]
counts = readiness_summary["counts"]
overall_state = str(readiness_summary["overall_state"])
overall_state_class = ExportService._html_class_token(overall_state)
generated_context = "Generated from persisted GeoIntel state"
scorecards = [
("Areas", counts.get("area_count", 0)),
("Datasets", f"{counts.get('ready_dataset_count', 0)}/{counts.get('dataset_count', 0)} ready"),
("Reference", counts.get("reference_dataset_count", 0)),
("QA/QC", counts.get("quality_check_count", 0)),
("Exports", counts.get("export_count", 0)),
]
scorecard_html = "\n".join(
"<div class=\"scorecard\">"
f"<span>{escape(str(label))}</span>"
f"<strong>{escape(str(value))}</strong>"
"</div>"
for label, value in scorecards
)
readiness_rows = "\n".join(
"<tr>"
f"<td>{escape(str(item['label']))}</td>"
f"<td>{escape(str(item['state']))}</td>"
f"<td><span class=\"readiness-pill readiness-{ExportService._html_class_token(str(item['state']))}\">{escape(str(item['state']))}</span></td>"
f"<td>{escape(str(item['detail']))}</td>"
"</tr>"
for item in readiness_summary["items"]
@@ -453,6 +471,8 @@ class ExportService:
f"<td>{escape(str(item['dataset_role']))}</td>"
f"<td>{escape(str(item['status']))}</td>"
f"<td>{escape(str(item['feature_count'] if item['feature_count'] is not None else 'n/a'))}</td>"
f"<td>{escape(str(item.get('source_name') or 'n/a'))}</td>"
f"<td>{escape(str(item.get('crs') or 'n/a'))}</td>"
"</tr>"
for item in datasets
)
@@ -477,51 +497,198 @@ class ExportService:
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>GeoIntel Project Report - {escape(str(project["name"]))}</title>
<style>
body {{ font-family: Arial, sans-serif; color: #0f172a; margin: 2rem; }}
h1, h2 {{ margin-bottom: 0.4rem; }}
table {{ width: 100%; border-collapse: collapse; margin: 1rem 0 2rem; }}
th, td {{ border: 1px solid #cbd5e1; padding: 0.5rem; text-align: left; }}
th {{ background: #e2e8f0; }}
.muted {{ color: #475569; }}
.status-ready {{ color: #166534; font-weight: 700; }}
.status-needs_attention {{ color: #92400e; font-weight: 700; }}
:root {{
--ink: #132018;
--muted: #5f6f67;
--line: #cbd8d0;
--soft: #f3f8f5;
--accent: #0f766e;
--accent-soft: #e3f4ef;
--warning: #b45309;
--danger: #991b1b;
}}
* {{ box-sizing: border-box; }}
body {{
margin: 0;
background: #eef4f1;
color: var(--ink);
font-family: Inter, "Segoe UI", Arial, sans-serif;
line-height: 1.45;
}}
.report-shell {{
width: min(1120px, calc(100% - 2rem));
margin: 0 auto;
padding: 1.25rem 0 2rem;
}}
.report-hero,
.report-section {{
page-break-inside: avoid;
border: 1px solid var(--line);
border-radius: 10px;
background: #fff;
box-shadow: 0 10px 28px rgba(33, 48, 41, 0.08);
}}
.report-hero {{
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 1rem;
align-items: end;
padding: 1.2rem;
background: linear-gradient(135deg, #ffffff, var(--accent-soft));
}}
h1, h2, h3, p {{ margin-top: 0; }}
h1 {{ margin-bottom: 0.35rem; font-size: 2rem; line-height: 1.05; }}
h2 {{ margin-bottom: 0.65rem; font-size: 1.2rem; }}
p {{ margin-bottom: 0.55rem; }}
.muted {{ color: var(--muted); }}
.section-kicker {{
margin: 0 0 0.22rem;
color: var(--muted);
font-size: 0.72rem;
font-weight: 800;
letter-spacing: 0.08em;
text-transform: uppercase;
}}
.readiness-pill {{
display: inline-flex;
align-items: center;
justify-content: center;
border: 1px solid var(--line);
border-radius: 999px;
padding: 0.2rem 0.58rem;
background: #fff;
font-size: 0.78rem;
font-weight: 800;
line-height: 1.2;
white-space: nowrap;
}}
.readiness-ready {{ border-color: rgba(21, 128, 61, 0.28); background: #ecfdf3; color: #166534; }}
.readiness-needs_attention,
.readiness-warning,
.readiness-waiting {{ border-color: rgba(180, 83, 9, 0.28); background: #fffbeb; color: var(--warning); }}
.readiness-blocked {{ border-color: rgba(153, 27, 27, 0.28); background: #fff1f2; color: var(--danger); }}
.report-scorecards {{
display: grid;
grid-template-columns: repeat(5, minmax(0, 1fr));
gap: 0.65rem;
margin: 1rem 0;
}}
.scorecard {{
border: 1px solid var(--line);
border-radius: 8px;
padding: 0.7rem;
background: #fff;
}}
.scorecard span {{
display: block;
color: var(--muted);
font-size: 0.72rem;
font-weight: 800;
letter-spacing: 0.05em;
text-transform: uppercase;
}}
.scorecard strong {{ display: block; margin-top: 0.25rem; font-size: 1.05rem; }}
.report-section {{ margin-top: 1rem; padding: 1rem; overflow: hidden; }}
.table-wrap {{ width: 100%; overflow-x: auto; }}
table {{ width: 100%; border-collapse: collapse; min-width: 42rem; }}
th, td {{ border-bottom: 1px solid var(--line); padding: 0.55rem; text-align: left; vertical-align: top; }}
th {{
background: var(--soft);
color: var(--muted);
font-size: 0.72rem;
letter-spacing: 0.05em;
text-transform: uppercase;
}}
ul {{ margin: 0; padding-left: 1.2rem; }}
li + li {{ margin-top: 0.35rem; }}
@media (max-width: 760px) {{
.report-shell {{ width: min(100% - 1rem, 1120px); }}
.report-hero {{ grid-template-columns: 1fr; }}
.report-scorecards {{ grid-template-columns: repeat(2, minmax(0, 1fr)); }}
}}
@media print {{
body {{ background: #fff; }}
.report-shell {{ width: 100%; padding: 0; }}
.report-hero,
.report-section {{ box-shadow: none; border-color: #94a3b8; page-break-inside: avoid; }}
.table-wrap {{ overflow: visible; }}
table {{ min-width: 0; font-size: 0.82rem; }}
}}
</style>
</head>
<body>
<h1>{escape(str(project["name"]))}</h1>
<p class="muted">GeoIntel project report artifact</p>
<p>Region: {escape(str(project["region"]))}</p>
<p>Status: {escape(str(project["status"]))}</p>
<p>Description: {escape(str(project["description"] or "n/a"))}</p>
<h2>V1 Readiness Summary</h2>
<p>Overall state: <span class="status-{escape(str(readiness_summary['overall_state']))}">{escape(str(readiness_summary["overall_state"]))}</span></p>
<table>
<thead><tr><th>Area</th><th>State</th><th>Detail</th></tr></thead>
<tbody>{readiness_rows}</tbody>
</table>
<h2>Datasets ({len(datasets)})</h2>
<table>
<thead><tr><th>Name</th><th>Type</th><th>Role</th><th>Status</th><th>Features</th></tr></thead>
<tbody>{dataset_rows or '<tr><td colspan="5">No datasets</td></tr>'}</tbody>
</table>
<h2>QA/QC Results ({len(quality_checks)})</h2>
<table>
<thead><tr><th>Check</th><th>Status</th><th>Score</th><th>Reference dataset</th></tr></thead>
<tbody>{quality_rows or '<tr><td colspan="4">No QA/QC results</td></tr>'}</tbody>
</table>
<h2>Export History ({len(exports)})</h2>
<table>
<thead><tr><th>Type</th><th>Storage path</th><th>Created</th></tr></thead>
<tbody>{export_rows or '<tr><td colspan="3">No exports</td></tr>'}</tbody>
</table>
<h2>Known Limitations</h2>
<ul>{limitation_items}</ul>
<main class="report-shell">
<section class="report-hero">
<div>
<p class="section-kicker">GeoIntel project report artifact</p>
<h1>{escape(str(project["name"]))}</h1>
<p class="muted">{generated_context}</p>
<p>Region: {escape(str(project["region"]))} · Status: {escape(str(project["status"]))}</p>
<p>Description: {escape(str(project["description"] or "n/a"))}</p>
</div>
<span class="readiness-pill readiness-{overall_state_class}">{escape(overall_state)}</span>
</section>
<div class="report-scorecards">{scorecard_html}</div>
<section class="report-section">
<p class="section-kicker">Release handoff</p>
<h2>V1 Readiness Summary</h2>
<p>Overall state: <span class="readiness-pill readiness-{overall_state_class}">{escape(overall_state)}</span></p>
<div class="table-wrap">
<table>
<thead><tr><th>Area</th><th>State</th><th>Detail</th></tr></thead>
<tbody>{readiness_rows}</tbody>
</table>
</div>
</section>
<section class="report-section">
<p class="section-kicker">Data handoff</p>
<h2>Dataset inventory ({len(datasets)})</h2>
<div class="table-wrap">
<table>
<thead><tr><th>Name</th><th>Type</th><th>Role</th><th>Status</th><th>Features</th><th>Source</th><th>CRS</th></tr></thead>
<tbody>{dataset_rows or '<tr><td colspan="7">No datasets</td></tr>'}</tbody>
</table>
</div>
</section>
<section class="report-section">
<p class="section-kicker">Quality handoff</p>
<h2>QA/QC evidence ({len(quality_checks)})</h2>
<div class="table-wrap">
<table>
<thead><tr><th>Check</th><th>Status</th><th>Score</th><th>Reference dataset</th></tr></thead>
<tbody>{quality_rows or '<tr><td colspan="4">No QA/QC results</td></tr>'}</tbody>
</table>
</div>
</section>
<section class="report-section">
<p class="section-kicker">Artifact handoff</p>
<h2>Artifact history ({len(exports)})</h2>
<p class="muted">Export History ({len(exports)})</p>
<div class="table-wrap">
<table>
<thead><tr><th>Type</th><th>Storage path</th><th>Created</th></tr></thead>
<tbody>{export_rows or '<tr><td colspan="3">No exports</td></tr>'}</tbody>
</table>
</div>
</section>
<section class="report-section">
<p class="section-kicker">Scope guardrails</p>
<h2>Known Limitations</h2>
<ul>{limitation_items}</ul>
</section>
</main>
</body>
</html>
"""
@staticmethod
def _html_class_token(value: str) -> str:
token = re.sub(r"[^a-zA-Z0-9_-]+", "_", value.strip().lower()).strip("_")
return token or "unknown"
@staticmethod
def _create_response(export: Export) -> ExportCreateResponse:
return ExportCreateResponse(