Add V1 report handoff summary
This commit is contained in:
@@ -7,6 +7,13 @@
|
|||||||
|
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## Sprint 23 V1 report handoff summary (2026-06-17)
|
||||||
|
|
||||||
|
- Added V1 readiness summary data to project metadata exports.
|
||||||
|
- Added a V1 Readiness Summary and Known Limitations section to lightweight HTML project reports.
|
||||||
|
- The summary covers project, AOI, dataset readiness, QA/QC and export history using persisted state.
|
||||||
|
- No new report designer, PDF generation, provider fetching, AI inference, migrations or API route changes were introduced.
|
||||||
|
|
||||||
## Sprint 22 V1 workbench status strip (2026-06-17)
|
## Sprint 22 V1 workbench status strip (2026-06-17)
|
||||||
|
|
||||||
- Added a compact frontend status strip for project, AOI, datasets, active map layer, QA/QC and exports.
|
- Added a compact frontend status strip for project, AOI, datasets, active map layer, QA/QC and exports.
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ from typing import Any
|
|||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from app.core.errors import AppError
|
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.schemas.export import ExportContentResponse, ExportCreateResponse, ExportListResponse, ExportRead
|
||||||
from app.services.dataset_service import DatasetService
|
from app.services.dataset_service import DatasetService
|
||||||
from app.services.detection_service import DetectionService
|
from app.services.detection_service import DetectionService
|
||||||
@@ -122,6 +122,7 @@ class ExportService:
|
|||||||
"dataset_count": len(content["datasets"]),
|
"dataset_count": len(content["datasets"]),
|
||||||
"quality_check_count": len(content["quality_checks"]),
|
"quality_check_count": len(content["quality_checks"]),
|
||||||
"export_count": len(content["exports"]),
|
"export_count": len(content["exports"]),
|
||||||
|
"readiness_state": content["readiness_summary"]["overall_state"],
|
||||||
}
|
}
|
||||||
export = ExportService._write_json_export(
|
export = ExportService._write_json_export(
|
||||||
db,
|
db,
|
||||||
@@ -150,6 +151,7 @@ class ExportService:
|
|||||||
"dataset_count": len(summary["datasets"]),
|
"dataset_count": len(summary["datasets"]),
|
||||||
"quality_check_count": len(summary["quality_checks"]),
|
"quality_check_count": len(summary["quality_checks"]),
|
||||||
"export_count": len(summary["exports"]),
|
"export_count": len(summary["exports"]),
|
||||||
|
"readiness_state": summary["readiness_summary"]["overall_state"],
|
||||||
"format": "html",
|
"format": "html",
|
||||||
}
|
}
|
||||||
export = ExportService._write_text_export(
|
export = ExportService._write_text_export(
|
||||||
@@ -282,6 +284,7 @@ class ExportService:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _project_summary(db: Session, project: Project) -> dict[str, Any]:
|
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()
|
datasets = db.query(Dataset).filter(Dataset.project_id == project.id).order_by(Dataset.created_at.desc()).all()
|
||||||
quality_checks = (
|
quality_checks = (
|
||||||
db.query(QualityCheck)
|
db.query(QualityCheck)
|
||||||
@@ -290,7 +293,7 @@ class ExportService:
|
|||||||
.all()
|
.all()
|
||||||
)
|
)
|
||||||
exports = db.query(Export).filter(Export.project_id == project.id).order_by(Export.created_at.desc()).all()
|
exports = db.query(Export).filter(Export.project_id == project.id).order_by(Export.created_at.desc()).all()
|
||||||
return {
|
summary = {
|
||||||
"project": {
|
"project": {
|
||||||
"id": str(project.id),
|
"id": str(project.id),
|
||||||
"name": project.name,
|
"name": project.name,
|
||||||
@@ -298,6 +301,16 @@ class ExportService:
|
|||||||
"region": project.region,
|
"region": project.region,
|
||||||
"status": project.status,
|
"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": [
|
"datasets": [
|
||||||
{
|
{
|
||||||
"id": str(dataset.id),
|
"id": str(dataset.id),
|
||||||
@@ -337,6 +350,77 @@ class ExportService:
|
|||||||
for export in exports
|
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
|
@staticmethod
|
||||||
def _render_project_report_html(summary: dict[str, Any]) -> str:
|
def _render_project_report_html(summary: dict[str, Any]) -> str:
|
||||||
@@ -344,6 +428,17 @@ class ExportService:
|
|||||||
datasets = summary["datasets"]
|
datasets = summary["datasets"]
|
||||||
quality_checks = summary["quality_checks"]
|
quality_checks = summary["quality_checks"]
|
||||||
exports = summary["exports"]
|
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(
|
dataset_rows = "\n".join(
|
||||||
"<tr>"
|
"<tr>"
|
||||||
f"<td>{escape(str(item['name']))}</td>"
|
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, td {{ border: 1px solid #cbd5e1; padding: 0.5rem; text-align: left; }}
|
||||||
th {{ background: #e2e8f0; }}
|
th {{ background: #e2e8f0; }}
|
||||||
.muted {{ color: #475569; }}
|
.muted {{ color: #475569; }}
|
||||||
|
.status-ready {{ color: #166534; font-weight: 700; }}
|
||||||
|
.status-needs_attention {{ color: #92400e; font-weight: 700; }}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -391,6 +488,12 @@ class ExportService:
|
|||||||
<p>Region: {escape(str(project["region"]))}</p>
|
<p>Region: {escape(str(project["region"]))}</p>
|
||||||
<p>Status: {escape(str(project["status"]))}</p>
|
<p>Status: {escape(str(project["status"]))}</p>
|
||||||
<p>Description: {escape(str(project["description"] or "n/a"))}</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>
|
<h2>Datasets ({len(datasets)})</h2>
|
||||||
<table>
|
<table>
|
||||||
<thead><tr><th>Name</th><th>Type</th><th>Role</th><th>Status</th><th>Features</th></tr></thead>
|
<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>
|
<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>
|
<tbody>{export_rows or '<tr><td colspan="3">No exports</td></tr>'}</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
<h2>Known Limitations</h2>
|
||||||
|
<ul>{limitation_items}</ul>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from fastapi.testclient import TestClient
|
|||||||
|
|
||||||
from app.core.errors import AppError
|
from app.core.errors import AppError
|
||||||
from app.main import app
|
from app.main import app
|
||||||
from app.models import Dataset, Export, Project, QualityCheck
|
from app.models import Area, Dataset, Export, Project, QualityCheck
|
||||||
from app.schemas.export import ExportCreateResponse
|
from app.schemas.export import ExportCreateResponse
|
||||||
from app.services.export_service import ExportService
|
from app.services.export_service import ExportService
|
||||||
from app.services.storage_service import StorageService
|
from app.services.storage_service import StorageService
|
||||||
@@ -121,6 +121,7 @@ def test_project_metadata_export_persists_json_summary(tmp_path, monkeypatch) ->
|
|||||||
dataset_id = uuid4()
|
dataset_id = uuid4()
|
||||||
quality_check_id = uuid4()
|
quality_check_id = uuid4()
|
||||||
project = Project(id=project_id, name="Demo", region="Kempen", status="active")
|
project = Project(id=project_id, name="Demo", region="Kempen", status="active")
|
||||||
|
area = Area(id=uuid4(), project_id=project_id, name="Demo AOI", original_crs="EPSG:4326", area_m2=100.0)
|
||||||
dataset = Dataset(
|
dataset = Dataset(
|
||||||
id=dataset_id,
|
id=dataset_id,
|
||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
@@ -154,6 +155,7 @@ def test_project_metadata_export_persists_json_summary(tmp_path, monkeypatch) ->
|
|||||||
db = FakeSession(
|
db = FakeSession(
|
||||||
{
|
{
|
||||||
(Project, project_id): project,
|
(Project, project_id): project,
|
||||||
|
(Area, area.id): area,
|
||||||
(Dataset, dataset_id): dataset,
|
(Dataset, dataset_id): dataset,
|
||||||
(QualityCheck, quality_check_id): quality_check,
|
(QualityCheck, quality_check_id): quality_check,
|
||||||
(Export, previous_export_id): previous_export,
|
(Export, previous_export_id): previous_export,
|
||||||
@@ -166,16 +168,22 @@ def test_project_metadata_export_persists_json_summary(tmp_path, monkeypatch) ->
|
|||||||
payload = json.loads(export_path.read_text(encoding="utf-8"))
|
payload = json.loads(export_path.read_text(encoding="utf-8"))
|
||||||
assert response.export_type == "project_metadata_json"
|
assert response.export_type == "project_metadata_json"
|
||||||
assert payload["project"]["id"] == str(project_id)
|
assert payload["project"]["id"] == str(project_id)
|
||||||
|
assert payload["areas"][0]["name"] == "Demo AOI"
|
||||||
|
assert payload["readiness_summary"]["overall_state"] == "ready"
|
||||||
|
assert payload["readiness_summary"]["counts"]["area_count"] == 1
|
||||||
|
assert payload["known_limitations"]
|
||||||
assert payload["datasets"][0]["id"] == str(dataset_id)
|
assert payload["datasets"][0]["id"] == str(dataset_id)
|
||||||
assert payload["quality_checks"][0]["id"] == str(quality_check_id)
|
assert payload["quality_checks"][0]["id"] == str(quality_check_id)
|
||||||
assert payload["exports"][0]["id"] == str(previous_export_id)
|
assert payload["exports"][0]["id"] == str(previous_export_id)
|
||||||
assert response.metadata_json["export_count"] == 1
|
assert response.metadata_json["export_count"] == 1
|
||||||
|
assert response.metadata_json["readiness_state"] == "ready"
|
||||||
|
|
||||||
|
|
||||||
def test_project_report_export_persists_html_artifact(tmp_path, monkeypatch) -> None:
|
def test_project_report_export_persists_html_artifact(tmp_path, monkeypatch) -> None:
|
||||||
project_id = uuid4()
|
project_id = uuid4()
|
||||||
dataset_id = uuid4()
|
dataset_id = uuid4()
|
||||||
project = Project(id=project_id, name="Demo <Kempen>", description="QA report", region="Kempen", status="active")
|
project = Project(id=project_id, name="Demo <Kempen>", description="QA report", region="Kempen", status="active")
|
||||||
|
area = Area(id=uuid4(), project_id=project_id, name="Demo AOI", original_crs="EPSG:4326", area_m2=100.0)
|
||||||
dataset = Dataset(
|
dataset = Dataset(
|
||||||
id=dataset_id,
|
id=dataset_id,
|
||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
@@ -196,7 +204,24 @@ def test_project_report_export_persists_html_artifact(tmp_path, monkeypatch) ->
|
|||||||
created_at=datetime.now(timezone.utc),
|
created_at=datetime.now(timezone.utc),
|
||||||
)
|
)
|
||||||
export_path = tmp_path / "report.html"
|
export_path = tmp_path / "report.html"
|
||||||
db = FakeSession({(Project, project_id): project, (Dataset, dataset_id): dataset, (Export, previous_export_id): previous_export})
|
quality_check = QualityCheck(
|
||||||
|
id=uuid4(),
|
||||||
|
project_id=project_id,
|
||||||
|
reference_dataset_id=dataset_id,
|
||||||
|
check_type="demo_candidate_vs_reference",
|
||||||
|
status="ok",
|
||||||
|
score=0.5,
|
||||||
|
created_at=datetime.now(timezone.utc),
|
||||||
|
)
|
||||||
|
db = FakeSession(
|
||||||
|
{
|
||||||
|
(Project, project_id): project,
|
||||||
|
(Area, area.id): area,
|
||||||
|
(Dataset, dataset_id): dataset,
|
||||||
|
(QualityCheck, quality_check.id): quality_check,
|
||||||
|
(Export, previous_export_id): previous_export,
|
||||||
|
}
|
||||||
|
)
|
||||||
monkeypatch.setattr(StorageService, "dataset_export_path", lambda *_args: str(export_path))
|
monkeypatch.setattr(StorageService, "dataset_export_path", lambda *_args: str(export_path))
|
||||||
|
|
||||||
response = ExportService.export_project_report(db, project_id)
|
response = ExportService.export_project_report(db, project_id)
|
||||||
@@ -204,8 +229,12 @@ def test_project_report_export_persists_html_artifact(tmp_path, monkeypatch) ->
|
|||||||
html = export_path.read_text(encoding="utf-8")
|
html = export_path.read_text(encoding="utf-8")
|
||||||
assert response.export_type == "project_report_html"
|
assert response.export_type == "project_report_html"
|
||||||
assert response.metadata_json["format"] == "html"
|
assert response.metadata_json["format"] == "html"
|
||||||
|
assert response.metadata_json["readiness_state"] == "ready"
|
||||||
assert "<!doctype html>" in html
|
assert "<!doctype html>" in html
|
||||||
assert "Demo <Kempen>" in html
|
assert "Demo <Kempen>" in html
|
||||||
|
assert "V1 Readiness Summary" in html
|
||||||
|
assert "Overall state:" in html
|
||||||
|
assert "No live GRB/OSM/Sentinel fetching is performed by the report export." in html
|
||||||
assert "reference.geojson" in html
|
assert "reference.geojson" in html
|
||||||
assert "Export History (1)" in html
|
assert "Export History (1)" in html
|
||||||
assert "project_metadata_json" in html
|
assert "project_metadata_json" in html
|
||||||
|
|||||||
@@ -1026,8 +1026,9 @@ Export annotations/detections to YOLO format.
|
|||||||
### POST `/api/v1/exports/report`
|
### POST `/api/v1/exports/report`
|
||||||
|
|
||||||
Creates a lightweight HTML project report artifact from persisted project,
|
Creates a lightweight HTML project report artifact from persisted project,
|
||||||
dataset, QA/QC summary and export history state. This does not create a PDF
|
dataset, V1 readiness summary, QA/QC summary, known limitations and export
|
||||||
and does not introduce a report designer.
|
history state. This does not create a PDF and does not introduce a report
|
||||||
|
designer.
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,3 +1,19 @@
|
|||||||
|
## Sprint 23 V1 report handoff summary (2026-06-17)
|
||||||
|
|
||||||
|
Changed:
|
||||||
|
- Added V1 readiness summary data to project metadata exports.
|
||||||
|
- Added V1 Readiness Summary and Known Limitations sections to lightweight HTML project report exports.
|
||||||
|
- Included persisted AOI, dataset readiness, QA/QC and export-history counts in the handoff summary.
|
||||||
|
- Updated export tests, API contract docs, frontend README, TODO and changelog.
|
||||||
|
|
||||||
|
Tested:
|
||||||
|
- Passed: backend compile, export tests through full readiness, full backend pytest, frontend typecheck/build, Alembic heads, Alembic SQL upgrade and live smoke syntax check.
|
||||||
|
|
||||||
|
Known limitations:
|
||||||
|
- Report export remains a lightweight HTML artifact, not a PDF designer or custom report builder.
|
||||||
|
|
||||||
|
Next recommended pass:
|
||||||
|
- Add optional cleanup tooling for stale demo/export artifacts if repeated smoke runs keep accumulating local artifacts.
|
||||||
## Sprint 22 V1 workbench status strip (2026-06-17)
|
## Sprint 22 V1 workbench status strip (2026-06-17)
|
||||||
|
|
||||||
Changed:
|
Changed:
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ This file now starts with the current implementation status. Older preparation/b
|
|||||||
- [x] Project-scoped QA/QC result listing and frontend QA/QC Results panel.
|
- [x] Project-scoped QA/QC result listing and frontend QA/QC Results panel.
|
||||||
- [x] Persisted export foundation for vector/detection/segmentation GeoJSON and project metadata JSON.
|
- [x] Persisted export foundation for vector/detection/segmentation GeoJSON and project metadata JSON.
|
||||||
- [x] Lightweight HTML project report artifact export.
|
- [x] Lightweight HTML project report artifact export.
|
||||||
|
- [x] V1 readiness handoff summary in project metadata/report exports.
|
||||||
- [x] Browser-facing demo/export workflow smoke script with connected V1 state checks.
|
- [x] Browser-facing demo/export workflow smoke script with connected V1 state checks.
|
||||||
- [x] Compact V1 workbench status strip for project, AOI, datasets, map, QA/QC and exports.
|
- [x] Compact V1 workbench status strip for project, AOI, datasets, map, QA/QC and exports.
|
||||||
- [x] Live Docker/PostGIS validation on Tower/Unraid.
|
- [x] Live Docker/PostGIS validation on Tower/Unraid.
|
||||||
|
|||||||
@@ -164,6 +164,12 @@ React + TypeScript + MapLibre foundation for project/area/dataset workflow.
|
|||||||
- The status strip is implemented in `src/components/WorkbenchStatusStrip.tsx` and remains driven by `App.tsx` orchestration state.
|
- The status strip is implemented in `src/components/WorkbenchStatusStrip.tsx` and remains driven by `App.tsx` orchestration state.
|
||||||
- MapLibre source/layer updates now wait for style readiness to avoid runtime blank-screen failures during first render.
|
- MapLibre source/layer updates now wait for style readiness to avoid runtime blank-screen failures during first render.
|
||||||
|
|
||||||
|
## Sprint 23 additions
|
||||||
|
|
||||||
|
- Project report HTML exports now include a V1 readiness summary.
|
||||||
|
- The report summary covers project, AOI, datasets, QA/QC, exports and known limitations.
|
||||||
|
- Export Center behavior is unchanged; the richer handoff content is produced by the existing project report export action.
|
||||||
|
|
||||||
## Release hardening updates
|
## Release hardening updates
|
||||||
|
|
||||||
- Production builds split application code, React vendor code and MapLibre vendor code into separate chunks.
|
- Production builds split application code, React vendor code and MapLibre vendor code into separate chunks.
|
||||||
|
|||||||
Reference in New Issue
Block a user