Add V1 report handoff summary
GeoIntel CI / docs-smoke (push) Has been cancelled
GeoIntel CI / contract-smoke (push) Has been cancelled

This commit is contained in:
Codex
2026-06-17 01:30:53 +02:00
parent 014d088866
commit a4bcbea9f1
7 changed files with 171 additions and 6 deletions
+107 -2
View File
@@ -10,7 +10,7 @@ from typing import Any
from sqlalchemy.orm import Session
from app.core.errors import AppError
from app.models import AnalysisRun, Dataset, Export, Project, QualityCheck
from app.models import AnalysisRun, Area, Dataset, Export, Project, QualityCheck
from app.schemas.export import ExportContentResponse, ExportCreateResponse, ExportListResponse, ExportRead
from app.services.dataset_service import DatasetService
from app.services.detection_service import DetectionService
@@ -122,6 +122,7 @@ class ExportService:
"dataset_count": len(content["datasets"]),
"quality_check_count": len(content["quality_checks"]),
"export_count": len(content["exports"]),
"readiness_state": content["readiness_summary"]["overall_state"],
}
export = ExportService._write_json_export(
db,
@@ -150,6 +151,7 @@ class ExportService:
"dataset_count": len(summary["datasets"]),
"quality_check_count": len(summary["quality_checks"]),
"export_count": len(summary["exports"]),
"readiness_state": summary["readiness_summary"]["overall_state"],
"format": "html",
}
export = ExportService._write_text_export(
@@ -282,6 +284,7 @@ class ExportService:
@staticmethod
def _project_summary(db: Session, project: Project) -> dict[str, Any]:
areas = db.query(Area).filter(Area.project_id == project.id).order_by(Area.created_at.desc()).all()
datasets = db.query(Dataset).filter(Dataset.project_id == project.id).order_by(Dataset.created_at.desc()).all()
quality_checks = (
db.query(QualityCheck)
@@ -290,7 +293,7 @@ class ExportService:
.all()
)
exports = db.query(Export).filter(Export.project_id == project.id).order_by(Export.created_at.desc()).all()
return {
summary = {
"project": {
"id": str(project.id),
"name": project.name,
@@ -298,6 +301,16 @@ class ExportService:
"region": project.region,
"status": project.status,
},
"areas": [
{
"id": str(area.id),
"name": area.name,
"original_crs": area.original_crs,
"area_m2": area.area_m2,
"created_at": area.created_at.isoformat() if area.created_at else None,
}
for area in areas
],
"datasets": [
{
"id": str(dataset.id),
@@ -337,6 +350,77 @@ class ExportService:
for export in exports
],
}
summary["readiness_summary"] = ExportService._build_readiness_summary(summary)
summary["known_limitations"] = [
"Report artifact is a lightweight HTML handoff, not a PDF designer.",
"No live GRB/OSM/Sentinel fetching is performed by the report export.",
"AI detections or segmentations are included only when they already exist as persisted records/exports.",
]
return summary
@staticmethod
def _build_readiness_summary(summary: dict[str, Any]) -> dict[str, Any]:
project = summary["project"]
areas = summary["areas"]
datasets = summary["datasets"]
quality_checks = summary["quality_checks"]
exports = summary["exports"]
ready_datasets = [dataset for dataset in datasets if dataset["status"] == "ready"]
vector_datasets = [dataset for dataset in datasets if dataset["dataset_type"] in {"vector", "geojson"}]
raster_datasets = [dataset for dataset in datasets if dataset["dataset_type"] == "raster"]
reference_datasets = [dataset for dataset in datasets if dataset["dataset_role"] == "reference"]
items = [
{
"key": "project",
"label": "Project",
"state": "ready" if project["status"] != "deleted" else "blocked",
"detail": f"{project['name']} ({project['region']})",
},
{
"key": "aoi",
"label": "AOI",
"state": "ready" if areas else "waiting",
"detail": f"{len(areas)} area{'s' if len(areas) != 1 else ''}",
},
{
"key": "datasets",
"label": "Datasets",
"state": "ready" if datasets and len(ready_datasets) == len(datasets) else "waiting" if not datasets else "warning",
"detail": (
f"{len(ready_datasets)}/{len(datasets)} ready; "
f"{len(vector_datasets)} vector, {len(raster_datasets)} raster, {len(reference_datasets)} reference"
),
},
{
"key": "qa",
"label": "QA/QC",
"state": "ready" if quality_checks else "waiting",
"detail": f"{len(quality_checks)} persisted check{'s' if len(quality_checks) != 1 else ''}",
},
{
"key": "exports",
"label": "Exports",
"state": "ready" if exports else "waiting",
"detail": f"{len(exports)} previous export{'s' if len(exports) != 1 else ''}",
},
]
overall_state = "ready" if all(item["state"] == "ready" for item in items) else "needs_attention"
return {
"overall_state": overall_state,
"items": items,
"counts": {
"area_count": len(areas),
"dataset_count": len(datasets),
"ready_dataset_count": len(ready_datasets),
"vector_dataset_count": len(vector_datasets),
"raster_dataset_count": len(raster_datasets),
"reference_dataset_count": len(reference_datasets),
"quality_check_count": len(quality_checks),
"export_count": len(exports),
},
}
@staticmethod
def _render_project_report_html(summary: dict[str, Any]) -> str:
@@ -344,6 +428,17 @@ class ExportService:
datasets = summary["datasets"]
quality_checks = summary["quality_checks"]
exports = summary["exports"]
readiness_summary = summary["readiness_summary"]
known_limitations = summary["known_limitations"]
readiness_rows = "\n".join(
"<tr>"
f"<td>{escape(str(item['label']))}</td>"
f"<td>{escape(str(item['state']))}</td>"
f"<td>{escape(str(item['detail']))}</td>"
"</tr>"
for item in readiness_summary["items"]
)
limitation_items = "\n".join(f"<li>{escape(str(item))}</li>" for item in known_limitations)
dataset_rows = "\n".join(
"<tr>"
f"<td>{escape(str(item['name']))}</td>"
@@ -383,6 +478,8 @@ class ExportService:
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; }}
</style>
</head>
<body>
@@ -391,6 +488,12 @@ class ExportService:
<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>
@@ -406,6 +509,8 @@ class ExportService:
<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>
</body>
</html>
"""