From 688c3434fc81af0471d4e9ae981e76a629e547cd Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 18 Jun 2026 23:44:36 +0200 Subject: [PATCH] Polish project report artifact --- CHANGELOG.md | 8 + backend/README.md | 11 + backend/app/services/export_service.py | 241 +++++++++++++++--- .../test_sprint65_project_report_polish.py | 125 +++++++++ docs/CODEX_EXECUTION_LOG.md | 27 ++ docs/TODO.md | 1 + 6 files changed, 376 insertions(+), 37 deletions(-) create mode 100644 backend/tests/test_sprint65_project_report_polish.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 994c3377..a8850a65 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -703,3 +703,11 @@ Added: - Added clearer export history provenance with formatted export-type badges, analysis-run ids and created timestamps when available. - Added regression coverage for the Export Center handoff structure and responsive styling. - No API contracts, migrations, product capabilities, live provider fetching or AI/model dependency changes were introduced. + +## Sprint 65 project report readability polish (2026-06-18) + +- Reworked the lightweight HTML project report template into a self-contained handoff layout with hero, readiness pill, scorecards and sectioned tables. +- Added print-friendly CSS and scroll-safe table wrappers while preserving the existing `project_report_html` export type and download behavior. +- Added source/CRS columns to the dataset inventory section and clearer "Dataset inventory", "QA/QC evidence" and "Artifact history" report headings. +- Added regression coverage for report layout markers, print CSS and HTML escaping. +- No API contracts, migrations, product capabilities, PDF/report-designer functionality, live provider fetching or AI/model dependency changes were introduced. diff --git a/backend/README.md b/backend/README.md index 066c6782..ec4a66e7 100644 --- a/backend/README.md +++ b/backend/README.md @@ -563,6 +563,17 @@ If `rasterio` is unavailable: - raster metadata responses return `503` with `RASTER_PROCESSING_UNAVAILABLE` - raster clip/tile endpoints return explicit unavailable responses +## Export report artifact + +`POST /api/v1/exports/report` creates the existing lightweight +`project_report_html` artifact. The report is a self-contained HTML handoff +view rendered from persisted project, dataset, QA/QC and export-history state. +It includes readiness scorecards, dataset inventory, QA/QC evidence, artifact +history, known limitations and print-friendly CSS. + +This remains a simple HTML export. It does not add a PDF designer, report +builder, live provider fetching or new analysis behavior. + ## Helpful repository scripts - `bash scripts/backend_install.sh` diff --git a/backend/app/services/export_service.py b/backend/app/services/export_service.py index bd039d0d..2e21a95b 100644 --- a/backend/app/services/export_service.py +++ b/backend/app/services/export_service.py @@ -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( + "
" + f"{escape(str(label))}" + f"{escape(str(value))}" + "
" + for label, value in scorecards + ) readiness_rows = "\n".join( "" f"{escape(str(item['label']))}" - f"{escape(str(item['state']))}" + f"{escape(str(item['state']))}" f"{escape(str(item['detail']))}" "" for item in readiness_summary["items"] @@ -453,6 +471,8 @@ class ExportService: f"{escape(str(item['dataset_role']))}" f"{escape(str(item['status']))}" f"{escape(str(item['feature_count'] if item['feature_count'] is not None else 'n/a'))}" + f"{escape(str(item.get('source_name') or 'n/a'))}" + f"{escape(str(item.get('crs') or 'n/a'))}" "" for item in datasets ) @@ -477,51 +497,198 @@ class ExportService: + GeoIntel Project Report - {escape(str(project["name"]))} -

{escape(str(project["name"]))}

-

GeoIntel project report artifact

-

Region: {escape(str(project["region"]))}

-

Status: {escape(str(project["status"]))}

-

Description: {escape(str(project["description"] or "n/a"))}

-

V1 Readiness Summary

-

Overall state: {escape(str(readiness_summary["overall_state"]))}

- - - {readiness_rows} -
AreaStateDetail
-

Datasets ({len(datasets)})

- - - {dataset_rows or ''} -
NameTypeRoleStatusFeatures
No datasets
-

QA/QC Results ({len(quality_checks)})

- - - {quality_rows or ''} -
CheckStatusScoreReference dataset
No QA/QC results
-

Export History ({len(exports)})

- - - {export_rows or ''} -
TypeStorage pathCreated
No exports
-

Known Limitations

- +
+
+
+

GeoIntel project report artifact

+

{escape(str(project["name"]))}

+

{generated_context}

+

Region: {escape(str(project["region"]))} ยท Status: {escape(str(project["status"]))}

+

Description: {escape(str(project["description"] or "n/a"))}

+
+ {escape(overall_state)} +
+
{scorecard_html}
+
+

Release handoff

+

V1 Readiness Summary

+

Overall state: {escape(overall_state)}

+
+ + + {readiness_rows} +
AreaStateDetail
+
+
+
+

Data handoff

+

Dataset inventory ({len(datasets)})

+
+ + + {dataset_rows or ''} +
NameTypeRoleStatusFeaturesSourceCRS
No datasets
+
+
+
+

Quality handoff

+

QA/QC evidence ({len(quality_checks)})

+
+ + + {quality_rows or ''} +
CheckStatusScoreReference dataset
No QA/QC results
+
+
+
+

Artifact handoff

+

Artifact history ({len(exports)})

+

Export History ({len(exports)})

+
+ + + {export_rows or ''} +
TypeStorage pathCreated
No exports
+
+
+
+

Scope guardrails

+

Known Limitations

+
    {limitation_items}
+
+
""" + @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( diff --git a/backend/tests/test_sprint65_project_report_polish.py b/backend/tests/test_sprint65_project_report_polish.py new file mode 100644 index 00000000..3a77a5bc --- /dev/null +++ b/backend/tests/test_sprint65_project_report_polish.py @@ -0,0 +1,125 @@ +from __future__ import annotations + +from app.services.export_service import ExportService + + +def test_project_report_html_uses_handoff_layout_and_print_styles() -> None: + summary = { + "project": { + "id": "project-1", + "name": "Demo ", + "description": "QA handoff", + "region": "Kempen", + "status": "active", + }, + "datasets": [ + { + "id": "dataset-1", + "name": "reference.geojson", + "dataset_type": "vector", + "dataset_role": "reference", + "source_name": "fixture", + "reference_layer_name": "buildings", + "status": "ready", + "crs": "EPSG:4326", + "bounds_json": None, + "feature_count": 2, + } + ], + "quality_checks": [ + { + "id": "quality-1", + "analysis_run_id": None, + "candidate_dataset_id": "candidate-1", + "reference_dataset_id": "dataset-1", + "check_type": "demo_candidate_vs_reference", + "status": "ok", + "score": 0.75, + } + ], + "exports": [ + { + "id": "export-1", + "analysis_run_id": None, + "export_type": "project_metadata_json", + "storage_path": "storage/exports/metadata.json", + "metadata_json": {"readiness_state": "ready"}, + "created_at": "2026-06-18T10:00:00+00:00", + } + ], + "readiness_summary": { + "overall_state": "ready", + "items": [ + {"key": "project", "label": "Project", "state": "ready", "detail": "Demo (Kempen)"}, + {"key": "datasets", "label": "Datasets", "state": "ready", "detail": "1/1 ready"}, + ], + "counts": { + "area_count": 1, + "dataset_count": 1, + "ready_dataset_count": 1, + "vector_dataset_count": 1, + "raster_dataset_count": 0, + "reference_dataset_count": 1, + "quality_check_count": 1, + "export_count": 1, + }, + }, + "known_limitations": ["No live GRB/OSM/Sentinel fetching is performed by the report export."], + } + + html = ExportService._render_project_report_html(summary) + + assert 'class="report-shell"' in html + assert 'class="report-hero"' in html + assert 'class="report-scorecards"' in html + assert 'class="readiness-pill readiness-ready"' in html + assert 'class="section-kicker"' in html + assert "@media print" in html + assert "page-break-inside: avoid" in html + assert "Generated from persisted GeoIntel state" in html + assert "Dataset inventory" in html + assert "QA/QC evidence" in html + assert "Artifact history" in html + assert "Demo <Kempen>" in html + + +def test_project_report_html_escapes_table_values_in_polished_layout() -> None: + summary = { + "project": { + "id": "project-1", + "name": "", + "description": "unsafe", + "region": "Kempen", + "status": "active", + }, + "datasets": [], + "quality_checks": [], + "exports": [], + "readiness_summary": { + "overall_state": "needs_attention", + "items": [ + {"key": "project", "label": "", "state": "waiting", "detail": ""}, + ], + "counts": { + "area_count": 0, + "dataset_count": 0, + "ready_dataset_count": 0, + "vector_dataset_count": 0, + "raster_dataset_count": 0, + "reference_dataset_count": 0, + "quality_check_count": 0, + "export_count": 0, + }, + }, + "known_limitations": [""], + } + + html = ExportService._render_project_report_html(summary) + + assert "" not in html + assert "<script>alert(1)</script>" in html + assert "unsafe" not in html + assert "<b>unsafe</b>" in html + assert "<Project>" in html + assert "<unsafe limitation>" in html + assert 'class="readiness-pill readiness-needs_attention"' in html diff --git a/docs/CODEX_EXECUTION_LOG.md b/docs/CODEX_EXECUTION_LOG.md index 91ffa491..4fabea30 100644 --- a/docs/CODEX_EXECUTION_LOG.md +++ b/docs/CODEX_EXECUTION_LOG.md @@ -2489,3 +2489,30 @@ Limitations: Next recommended pass: - Run a live browser smoke across Exports and Overview after deployment, then continue with report artifact readability if the exported HTML itself needs visual polish. + +## Sprint 65 Project report readability polish (2026-06-18) + +Changed: +- Reworked the lightweight `project_report_html` renderer into a self-contained handoff layout with hero, readiness pill, scorecards and sectioned report content. +- Added print-friendly CSS and scroll-safe table wrappers to the HTML artifact. +- Added source and CRS columns to the dataset inventory section. +- Preserved existing export type, endpoint behavior, download behavior and storage flow. +- Added `backend/tests/test_sprint65_project_report_polish.py`. +- Updated `backend/README.md`, `docs/TODO.md` and `CHANGELOG.md`. + +Tested: +- Red step: `python -m pytest backend/tests/test_sprint65_project_report_polish.py -q` failed on missing report shell, scorecards, print styles and readiness pill classes. +- `python -m pytest backend/tests/test_sprint65_project_report_polish.py backend/tests/test_sprint17_export_foundation.py -q` (`12 passed`) +- `python -m compileall backend/app` +- `cd frontend && npm run typecheck` +- `cd frontend && npm run build` +- `bash scripts/run_readiness_check.sh` (`235 passed`) + +Open: +- Run full readiness, deploy Tower and smoke the HTML report download visually. + +Limitations: +- This remains a lightweight HTML handoff artifact. It does not add PDF generation, a report designer, new endpoints, live provider fetching or AI/model behavior. + +Next recommended pass: +- Verify the generated report artifact through the live export workflow, then continue with a full-workspace browser smoke if the runtime stays green. diff --git a/docs/TODO.md b/docs/TODO.md index cee7a964..e64c5c50 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -337,3 +337,4 @@ This file now starts with the current implementation status. Older preparation/b - [x] Add workbench visual polish pass for command bar, panel surfaces, empty states and mobile nav density. - [x] Add map/result overlay ergonomics for active layer provenance and feature property summaries. - [x] Add export/report handoff polish for artifact readiness, action grouping and export provenance. +- [x] Polish lightweight HTML project report readability, print styling and handoff sections.