From 0a9054c9ec1cd261aac0cd8f65eef791cb00cb46 Mon Sep 17 00:00:00 2001 From: Codex Date: Sun, 21 Jun 2026 13:08:42 +0200 Subject: [PATCH] Polish export handoff artifacts --- CHANGELOG.md | 8 ++ ...test_sprint93_export_handoff_completion.py | 45 ++++++++++ docs/CODEX_EXECUTION_LOG.md | 27 ++++++ docs/TODO.md | 1 + frontend/README.md | 1 + .../src/components/exports/ExportCenter.tsx | 82 +++++++++++++++++++ frontend/src/styles/app.css | 68 +++++++++++++++ 7 files changed, 232 insertions(+) create mode 100644 backend/tests/test_sprint93_export_handoff_completion.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 138c2226..edd70b84 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,14 @@ # Changelog +## Sprint 93 export handoff artifact polish (2026-06-21) + +- Added a latest handoff artifacts section to the Export Center for project reports, project metadata, dataset GeoJSON, detection GeoJSON and segmentation GeoJSON. +- Reused existing preview/download export actions from each latest artifact card without changing export API contracts or persistence. +- Added responsive styling for latest artifact cards, empty artifact states and compact artifact actions. +- Added regression coverage for grouped latest artifact surfaces and preserved preview/download controls. +- No API contracts, migrations, provider fetching or AI/model behavior changed. + ## Sprint 92 workflow rail interaction polish (2026-06-21) - Audited the live Overview workflow rail click path from Overview to Data, Map, QA/QC and Exports. diff --git a/backend/tests/test_sprint93_export_handoff_completion.py b/backend/tests/test_sprint93_export_handoff_completion.py new file mode 100644 index 00000000..57140b6d --- /dev/null +++ b/backend/tests/test_sprint93_export_handoff_completion.py @@ -0,0 +1,45 @@ +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[2] + + +def test_export_center_groups_latest_handoff_artifacts_by_type() -> None: + export_center = (ROOT / "frontend" / "src" / "components" / "exports" / "ExportCenter.tsx").read_text( + encoding="utf-8" + ) + + assert "latestHandoffArtifacts" in export_center + assert "Latest handoff artifacts" in export_center + assert "Project report" in export_center + assert "Project metadata" in export_center + assert "Dataset GeoJSON" in export_center + assert "Detection GeoJSON" in export_center + assert "Segmentation GeoJSON" in export_center + assert "latest-artifact-grid" in export_center + assert "latest-artifact-card" in export_center + assert "No artifact yet" in export_center + + +def test_export_center_latest_artifact_cards_keep_existing_actions() -> None: + export_center = (ROOT / "frontend" / "src" / "components" / "exports" / "ExportCenter.tsx").read_text( + encoding="utf-8" + ) + + assert "getLatestExportByType" in export_center + assert "const item = artifact.item" in export_center + assert "canPreviewJson(item)" in export_center + assert "onPreviewContent(item.id)" in export_center + assert "onDownload(item.id)" in export_center + assert "compactPath(item.storage_path)" in export_center + assert "formatExportCreated(item.created_at)" in export_center + + +def test_export_center_latest_artifact_styles_are_responsive() -> None: + css = (ROOT / "frontend" / "src" / "styles" / "app.css").read_text(encoding="utf-8") + + assert ".latest-artifact-grid" in css + assert ".latest-artifact-card" in css + assert ".latest-artifact-card-empty" in css + assert ".latest-artifact-actions" in css + assert "grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr));" in css diff --git a/docs/CODEX_EXECUTION_LOG.md b/docs/CODEX_EXECUTION_LOG.md index 46d8bf31..7c175f61 100644 --- a/docs/CODEX_EXECUTION_LOG.md +++ b/docs/CODEX_EXECUTION_LOG.md @@ -3342,3 +3342,30 @@ Limitations: Next recommended pass: - After deploy and live validation, continue with export artifact action ergonomics or QA result drill-down, depending on the next live friction point. + +## Sprint 93 export handoff artifact polish (2026-06-21) + +Changed: +- Added a `Latest handoff artifacts` section to `frontend/src/components/exports/ExportCenter.tsx`. +- Grouped newest persisted artifacts for project report, project metadata, dataset GeoJSON, detection GeoJSON and segmentation GeoJSON. +- Reused existing JSON preview and artifact download callbacks from latest artifact cards. +- Added responsive latest artifact card styling in `frontend/src/styles/app.css`. +- Added `backend/tests/test_sprint93_export_handoff_completion.py` to lock the grouped handoff surface, actions and responsive CSS. +- Updated `CHANGELOG.md`, `docs/TODO.md` and `frontend/README.md`. + +Tested: +- Red step: `python -m pytest backend\tests\test_sprint93_export_handoff_completion.py -q` failed on missing latest artifact grouping/actions/styles. +- `python -m pytest backend\tests\test_sprint93_export_handoff_completion.py -q` (`3 passed`) +- `python -m pytest backend\tests\test_sprint93_export_handoff_completion.py backend\tests\test_sprint64_export_handoff_polish.py backend\tests\test_sprint89_export_system_density.py backend\tests\test_sprint51_quality_export_polish.py backend\tests\test_sprint78_export_preview_readability.py -q` (`13 passed`) +- `cd frontend && npm run typecheck` +- `cd frontend && npm run build` +- `bash scripts/run_readiness_check.sh` (`311 passed`; frontend typecheck/build passed; Alembic head `202606120900`; live smoke syntax passed) + +Open: +- Tower deploy and live browser validation still pending for this pass. + +Limitations: +- Export Center frontend handoff polish only; no API contract, persistence, migration, provider fetching or AI/model behavior changes. + +Next recommended pass: +- After deploy and live validation, continue with QA/QC drilldown and map evidence layers. diff --git a/docs/TODO.md b/docs/TODO.md index 89f9def2..69f5926f 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -364,3 +364,4 @@ This file now starts with the current implementation status. Older preparation/b - [x] Add Overview workflow guidance for the V1 project -> data -> map -> QA/AI -> export path. - [x] Audit populated demo workflow and tighten complete-state Overview guidance copy. - [x] Make Overview workflow rail Map/Export clicks preserve useful dataset context. +- [x] Add latest handoff artifact cards to the Export Center for report, metadata and GeoJSON outputs. diff --git a/frontend/README.md b/frontend/README.md index bd9af2fc..ed810be8 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -253,6 +253,7 @@ Detection Lab and Segmentation Lab now share the same AI workspace hierarchy: mo - Overview rail Map and Export clicks reuse the first ready vector/GeoJSON dataset when no layer or dataset is active, keeping the happy path connected without adding new API calls. - The Data workspace surfaces selected project, AOI and dataset context before creation/upload forms, then separates form and catalog/list regions for faster scanning. - The Export Center includes a handoff readiness summary, grouped artifact actions and provenance-rich export cards so report/GeoJSON handoff stays understandable in long-running demo projects. +- The Export Center highlights the latest project report, project metadata, dataset GeoJSON, detection GeoJSON and segmentation GeoJSON artifacts with direct preview/download actions. ## Workbench shell refactor diff --git a/frontend/src/components/exports/ExportCenter.tsx b/frontend/src/components/exports/ExportCenter.tsx index 13f79c23..a44e0fb0 100644 --- a/frontend/src/components/exports/ExportCenter.tsx +++ b/frontend/src/components/exports/ExportCenter.tsx @@ -51,6 +51,17 @@ function formatExportType(value: string): string { .join(' ') } +function formatExportCreated(value?: string | null): string { + if (!value) { + return 'created time unavailable' + } + return value +} + +function getLatestExportByType(exports: ExportRead[], exportType: string): ExportRead | null { + return exports.find((item) => item.export_type === exportType) ?? null +} + function exportMatchesSearch(item: ExportRead, query: string): boolean { const normalizedQuery = query.trim().toLowerCase() if (!normalizedQuery) { @@ -90,6 +101,41 @@ export function ExportCenter({ const handoffStatus = selectedProjectId ? 'project selected' : 'select project' const exportTypes = useMemo(() => Array.from(new Set(exports.map((item) => item.export_type))).sort(), [exports]) const exportStatuses = useMemo(() => Array.from(new Set(exports.map((item) => item.status))).sort(), [exports]) + const latestHandoffArtifacts = useMemo( + () => [ + { + key: 'project_report_html', + label: 'Project report', + detail: 'HTML report for human review', + item: getLatestExportByType(exports, 'project_report_html'), + }, + { + key: 'project_metadata_json', + label: 'Project metadata', + detail: 'Machine-readable project inventory', + item: getLatestExportByType(exports, 'project_metadata_json'), + }, + { + key: 'dataset_geojson', + label: 'Dataset GeoJSON', + detail: 'Selected vector/reference layer', + item: getLatestExportByType(exports, 'dataset_geojson'), + }, + { + key: 'detection_geojson', + label: 'Detection GeoJSON', + detail: 'Persisted detection geometries', + item: getLatestExportByType(exports, 'detection_geojson'), + }, + { + key: 'segmentation_geojson', + label: 'Segmentation GeoJSON', + detail: 'Persisted segmentation polygons', + item: getLatestExportByType(exports, 'segmentation_geojson'), + }, + ], + [exports], + ) const filteredExports = useMemo( () => exports.filter((item) => { @@ -161,6 +207,42 @@ export function ExportCenter({ +
+
+

Latest handoff artifacts

+

Open the newest artifact of each handoff type without searching the full export history.

+
+
+ {latestHandoffArtifacts.map((artifact) => { + const item = artifact.item + return ( +
+ {artifact.label} + {item ? item.status : 'No artifact yet'} +

{item ? compactPath(item.storage_path) : artifact.detail}

+ {item ? ( + <> + {formatExportCreated(item.created_at)} +
+ {canPreviewJson(item) ? ( + + ) : ( + download-only + )} + +
+ + ) : null} +
+ ) + })} +
+
+
diff --git a/frontend/src/styles/app.css b/frontend/src/styles/app.css index 59d998c4..b8312d38 100644 --- a/frontend/src/styles/app.css +++ b/frontend/src/styles/app.css @@ -1831,6 +1831,7 @@ button.entity-card { .export-summary-surface, .export-handoff-surface, +.latest-artifact-surface, .export-actions-surface, .export-history-surface, .system-provider-capability-surface { @@ -1849,6 +1850,10 @@ button.entity-card { background: #ffffff; } +.latest-artifact-surface { + background: #ffffff; +} + .export-center-shell .quality-summary-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(8rem, 1fr)); @@ -1977,6 +1982,69 @@ button.entity-card { margin-top: 0; } +.latest-artifact-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr)); + gap: 0.6rem; +} + +.latest-artifact-card { + display: grid; + min-width: 0; + gap: 0.36rem; + border: 1px solid rgba(15, 118, 110, 0.2); + border-radius: 8px; + padding: 0.72rem; + background: linear-gradient(180deg, #f8fcfa, #ffffff); +} + +.latest-artifact-card-empty { + border-style: dashed; + border-color: var(--line); + background: var(--panel-soft); +} + +.latest-artifact-card span { + color: var(--muted); + font-size: 0.68rem; + font-weight: 850; + letter-spacing: 0.05em; + text-transform: uppercase; +} + +.latest-artifact-card strong { + overflow: hidden; + font-size: 0.9rem; + line-height: 1.24; + text-overflow: ellipsis; + white-space: nowrap; +} + +.latest-artifact-card p, +.latest-artifact-card small { + margin: 0; + color: var(--muted); + font-size: 0.8rem; + line-height: 1.35; + overflow-wrap: anywhere; +} + +.latest-artifact-actions { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 0.45rem; + align-items: center; + margin-top: 0.18rem; +} + +.latest-artifact-actions button, +.latest-artifact-actions .status-badge { + width: 100%; + min-height: 2.1rem; + justify-content: center; + text-align: center; +} + .export-type-badge { display: inline-flex; max-width: 100%;