diff --git a/CHANGELOG.md b/CHANGELOG.md index a729bb11..a4c492e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ - Added a persisted database layer selector to the Map workspace so users can directly load a ready vector dataset from stored project data. - Added an Operational GIS run panel that reuses AOI or active layer extents to query persisted PostGIS `vector_features` through the existing bbox selection flow. - Added a basemap policy notice when the public OpenStreetMap fallback is active and a guided operational workflow for query, derived dataset, QA/QC and export handoff. +- Added a one-click full GIS workflow action that runs persisted selection, saves the derived dataset, saves a GeoJSON export and optionally runs QA/QC against the selected reference dataset. - Added static regression coverage for the road basemap, attribution, basemap policy notice, database layer selector and persisted operational GIS workflow wiring. ## Sprint 115 QA/QC and Exports usability layout pass (2026-07-04) diff --git a/backend/tests/test_sprint109_map_selection_qa_shortcut.py b/backend/tests/test_sprint109_map_selection_qa_shortcut.py index 12635e0d..d9bda168 100644 --- a/backend/tests/test_sprint109_map_selection_qa_shortcut.py +++ b/backend/tests/test_sprint109_map_selection_qa_shortcut.py @@ -12,7 +12,8 @@ def test_map_selection_qa_shortcut_uses_existing_qa_workflow_contract() -> None: hook = hook_path.read_text(encoding="utf-8") assert "qaApi.runQa" in hook assert "loadQualityChecks" in hook - assert "candidate_dataset_id: latestSelectionDataset.id" in hook + assert "candidateDataset = latestSelectionDataset" in hook + assert "candidate_dataset_id: candidateDataset.id" in hook assert "reference_dataset_id: selectedMapQaReferenceDatasetId" in hook assert "setMapQaResult" in hook assert "useMapSelectionQa" in app diff --git a/backend/tests/test_sprint116_operational_gis_map_workflow.py b/backend/tests/test_sprint116_operational_gis_map_workflow.py index ab364661..8c9c5876 100644 --- a/backend/tests/test_sprint116_operational_gis_map_workflow.py +++ b/backend/tests/test_sprint116_operational_gis_map_workflow.py @@ -34,8 +34,13 @@ def test_map_workspace_can_select_persisted_database_layer_and_run_query() -> No assert "Save result dataset" in map_workspace assert "Save GeoJSON export" in map_workspace assert "Run QA/QC" in map_workspace + assert "Run full GIS workflow" in map_workspace + assert "runFullGisWorkflow" in map_workspace + assert "fullWorkflowStatus" in map_workspace + assert "Query, save, QA and export" in map_workspace assert "selectedMapDatasetId=" in app_shell assert ".basemap-policy-notice" in styles assert ".guided-gis-flow" in styles + assert ".guided-gis-batch-status" in styles assert ".gis-test-run-surface" in styles assert ".gis-test-run-grid" in styles diff --git a/docs/CODEX_EXECUTION_LOG.md b/docs/CODEX_EXECUTION_LOG.md index f998c40c..26f25198 100644 --- a/docs/CODEX_EXECUTION_LOG.md +++ b/docs/CODEX_EXECUTION_LOG.md @@ -6,6 +6,7 @@ Changed: - Added an Operational GIS run panel that reuses the selected AOI bbox or active layer bbox and calls the existing persisted `vector_features` bbox selection workflow. - Added a visible basemap policy notice when the public OpenStreetMap fallback is active. - Added a guided operational workflow that brings query, derived dataset save, GeoJSON export, reference selection, QA/QC run and evidence handoff into the Map workspace. +- Added a one-click full GIS workflow action that runs persisted selection, saves the derived dataset, saves a GeoJSON export and optionally runs QA/QC against the selected reference dataset. - Updated `.env.example`, `CHANGELOG.md`, `docs/TODO.md`, `docs/ENVIRONMENT_SPEC.md` and `frontend/README.md`. - Added regression coverage in `backend/tests/test_sprint116_operational_gis_map_workflow.py`. @@ -14,6 +15,9 @@ Validation: - `python -m pytest backend\tests\test_sprint116_operational_gis_map_workflow.py backend\tests\test_sprint85_map_workspace_density.py backend\tests\test_sprint106_map_bbox_extract.py backend\tests\test_sprint107_map_selection_export.py backend\tests\test_sprint108_map_selection_derived_dataset.py -q` passed: 18 tests. - `python -m pytest backend\tests\test_sprint116_operational_gis_map_workflow.py backend\tests\test_sprint109_map_selection_qa_shortcut.py backend\tests\test_sprint107_map_selection_export.py backend\tests\test_sprint108_map_selection_derived_dataset.py -q` passed: 11 tests. - `python -m pytest backend\tests\test_sprint116_operational_gis_map_workflow.py -q` passed: 2 tests after aligning `.env.example` with the managed-style override policy. +- RED: `python -m pytest backend\tests\test_sprint116_operational_gis_map_workflow.py -q` failed before implementation because `Run full GIS workflow`, `runFullGisWorkflow` and `fullWorkflowStatus` were not present. +- `python -m pytest backend\tests\test_sprint116_operational_gis_map_workflow.py -q` passed: 2 tests after adding the full GIS workflow action. +- `python -m pytest backend\tests\test_sprint116_operational_gis_map_workflow.py backend\tests\test_sprint106_map_bbox_extract.py backend\tests\test_sprint107_map_selection_export.py backend\tests\test_sprint108_map_selection_derived_dataset.py backend\tests\test_sprint109_map_selection_qa_shortcut.py backend\tests\test_sprint110_map_qa_evidence_drilldown.py -q` passed: 18 tests. - `cd frontend && npm run build` passed. - `python -m compileall backend/app` passed. - `cd backend && python -m pytest -q` passed: 364 tests. diff --git a/docs/TODO.md b/docs/TODO.md index eb1a1159..40df0ac4 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -78,6 +78,7 @@ This file now starts with the current implementation status. Older preparation/b - [x] Add selected map feature extraction with highlight, property table, copy and GeoJSON download. - [x] Add operational GIS map workflow with road basemap, persisted database layer selection and AOI/layer `vector_features` query run. - [x] Add basemap policy notice and guided GIS query-to-QA/export workflow in the Map workspace. +- [x] Add one-click full GIS workflow action for query, derived dataset, QA/QC and export handoff. - [x] Add QA/QC workspace result hierarchy and filter density polish. - [x] Add Change Detection panel hierarchy and analysis workspace density polish. - [x] Add calm workbench layout pass to reduce duplicate navigation, heavy card styling and shell density. diff --git a/frontend/README.md b/frontend/README.md index ae7d2ee7..3a53ad76 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -12,7 +12,7 @@ The Map workspace defaults to an OpenStreetMap road basemap with visible attribu When the public OpenStreetMap fallback is active, the Map workspace shows a basemap usage notice. This keeps the local/demo default honest and reminds operators to configure a managed style URL before production or heavier tile traffic. -Operational GIS testing is now available directly in the Map workspace. Users can choose a persisted vector database layer, load it on the map, reuse the selected AOI or active layer extent, run the existing persisted `vector_features` bbox query, save the result as a derived dataset, export the selection GeoJSON, choose a reference dataset and launch QA/QC without creating fake data or a parallel backend path. +Operational GIS testing is now available directly in the Map workspace. Users can choose a persisted vector database layer, load it on the map, reuse the selected AOI or active layer extent, run the existing persisted `vector_features` bbox query, save the result as a derived dataset, export the selection GeoJSON, choose a reference dataset and launch QA/QC without creating fake data or a parallel backend path. The guided workflow also includes a one-click full run action that executes query, derived dataset save, GeoJSON export and optional QA/QC in sequence with visible status. QA/QC and Exports follow the same calmer density model. QA/QC keeps metric evidence, feature ids and raw findings available but compresses provenance and history surfaces so review starts from the selected check and map evidence actions. Exports uses denser handoff cards, latest-artifact cards and history filters so artifact creation and download paths are easier to scan. diff --git a/frontend/src/components/map/MapWorkspace.tsx b/frontend/src/components/map/MapWorkspace.tsx index ecb5fec1..d3976836 100644 --- a/frontend/src/components/map/MapWorkspace.tsx +++ b/frontend/src/components/map/MapWorkspace.tsx @@ -218,12 +218,12 @@ interface MapWorkspaceProps { onSetMapLayerOpacity: (opacity: number) => void onSelectMapFeature: (feature: GeoJSON.Feature | null) => void onSetMapSelectionBbox: (bbox: VectorSelectionBBox | null) => void - onRunMapSelectionExtract: (bbox: VectorSelectionBBox) => void + onRunMapSelectionExtract: (bbox: VectorSelectionBBox) => Promise onClearMapSelectionExtract: () => void - onExportMapSelection: (bbox: VectorSelectionBBox) => void - onDeriveMapSelectionDataset: (bbox: VectorSelectionBBox) => void + onExportMapSelection: (bbox: VectorSelectionBBox) => Promise + onDeriveMapSelectionDataset: (bbox: VectorSelectionBBox) => Promise onSelectMapQaReferenceDataset: (datasetId: string) => void - onRunMapSelectionQa: () => void + onRunMapSelectionQa: (candidateDataset?: DatasetCreateResponse | null) => Promise onOpenMapSelectionQualityEvidence: () => void onClearQualityEvidence?: () => void } @@ -287,6 +287,9 @@ export function MapWorkspace({ const [bboxSelectionMode, setBboxSelectionMode] = useState(false) const [firstSelectionCorner, setFirstSelectionCorner] = useState<[number, number] | null>(null) const [bboxInput, setBboxInput] = useState(bboxToInputState(mapSelectionBbox)) + const [fullWorkflowRunning, setFullWorkflowRunning] = useState(false) + const [fullWorkflowStatus, setFullWorkflowStatus] = useState('Ready to run persisted GIS workflow.') + const [fullWorkflowError, setFullWorkflowError] = useState(null) const selectedMapArea = areas.find((area) => area.id === selectedMapAreaId) const featureProperties = selectedMapFeature?.properties ?? null const featureSummaryEntries = featureProperties @@ -404,6 +407,51 @@ export function MapWorkspace({ onRunMapSelectionExtract(bbox) } + const runFullGisWorkflow = async () => { + const bbox = currentSelectionBbox ?? selectedAreaBbox ?? activeLayerBbox + if (!selectedMapDataset || !bbox) { + setFullWorkflowError('Select a database layer and AOI/layer extent before running the full workflow.') + return + } + + setFullWorkflowRunning(true) + setFullWorkflowError(null) + try { + setFullWorkflowStatus('1/4 Querying persisted vector_features...') + setSelectionBbox(bbox) + const selection = await onRunMapSelectionExtract(bbox) + if (!selection) { + setFullWorkflowError('Persisted vector query did not complete.') + setFullWorkflowStatus('Stopped at query.') + return + } + + setFullWorkflowStatus('2/4 Saving derived result dataset...') + const derived = await onDeriveMapSelectionDataset(bbox) + if (!derived) { + setFullWorkflowError('Derived result dataset was not created.') + setFullWorkflowStatus('Stopped at dataset save.') + return + } + + setFullWorkflowStatus('3/4 Saving GeoJSON export artifact...') + await onExportMapSelection(bbox) + + if (selectedMapQaReferenceDatasetId) { + setFullWorkflowStatus('4/4 Running QA/QC against selected reference...') + const qaResult = await onRunMapSelectionQa(derived) + setFullWorkflowStatus(qaResult ? 'Full GIS workflow complete with QA/QC result.' : 'Dataset/export complete; QA/QC did not complete.') + } else { + setFullWorkflowStatus('Dataset/export complete. Select a reference dataset to add QA/QC.') + } + } catch (error) { + setFullWorkflowError(error instanceof Error ? error.message : 'Full GIS workflow failed.') + setFullWorkflowStatus('Workflow stopped.') + } finally { + setFullWorkflowRunning(false) + } + } + return (
@@ -705,6 +753,18 @@ export function MapWorkspace({
+ +
+ Query, save, QA and export + {fullWorkflowStatus} +
@@ -756,6 +816,7 @@ export function MapWorkspace({ {selectionDatasetError ?

{selectionDatasetError}

: null} {selectionExportError ?

{selectionExportError}

: null} {mapSelectionQaError ?

{mapSelectionQaError}

: null} + {fullWorkflowError ?

{fullWorkflowError}

: null}
@@ -915,7 +976,7 @@ export function MapWorkspace({ className="primary-action" disabled={!selectedMapQaReferenceDatasetId || mapSelectionQaRunning} type="button" - onClick={onRunMapSelectionQa} + onClick={() => onRunMapSelectionQa()} > {mapSelectionQaRunning ? 'Running QA...' : 'Run QA on saved dataset'} diff --git a/frontend/src/hooks/useExportWorkflow.ts b/frontend/src/hooks/useExportWorkflow.ts index 8539c391..135617df 100644 --- a/frontend/src/hooks/useExportWorkflow.ts +++ b/frontend/src/hooks/useExportWorkflow.ts @@ -112,7 +112,7 @@ export function useExportWorkflow({ const exportMapSelectionGeoJson = async (bbox: VectorSelectionBBox) => { if (!selectedDataset || !isVectorDatasetType(selectedDataset.dataset_type)) { setSelectionExportError('Select a vector dataset before saving an area export.') - return + return null } setSelectionExporting(true) setSelectionExportError(null) @@ -127,8 +127,10 @@ export function useExportWorkflow({ setLatestExport(response) setLatestSelectionExport(response) await loadExports(selectedDataset.project_id) + return response } catch (error) { setSelectionExportError(formatError(error, 'Failed to save area export')) + return null } finally { setSelectionExporting(false) } diff --git a/frontend/src/hooks/useMapSelectionDataset.ts b/frontend/src/hooks/useMapSelectionDataset.ts index 920f61f6..f0b61428 100644 --- a/frontend/src/hooks/useMapSelectionDataset.ts +++ b/frontend/src/hooks/useMapSelectionDataset.ts @@ -27,7 +27,7 @@ export function useMapSelectionDataset({ const deriveMapSelectionDataset = async (bbox: VectorSelectionBBox) => { if (!selectedProjectId || !selectedDataset || !isVectorDatasetType(selectedDataset.dataset_type)) { setSelectionDatasetError('Select a vector dataset before saving the area as a dataset.') - return + return null } setSelectionDatasetSaving(true) setSelectionDatasetError(null) @@ -41,8 +41,10 @@ export function useMapSelectionDataset({ await loadProjectData(selectedProjectId) await loadDatasetDetails(selectedProjectId, derived) setMapLayerVisible(true) + return derived } catch (error) { setSelectionDatasetError(formatError(error, 'Failed to save area as dataset')) + return null } finally { setSelectionDatasetSaving(false) } diff --git a/frontend/src/hooks/useMapSelectionExtract.ts b/frontend/src/hooks/useMapSelectionExtract.ts index e667ac29..1b388520 100644 --- a/frontend/src/hooks/useMapSelectionExtract.ts +++ b/frontend/src/hooks/useMapSelectionExtract.ts @@ -28,11 +28,11 @@ export function useMapSelectionExtract({ const runMapSelectionExtract = async (bbox: VectorSelectionBBox) => { if (!selectedProjectId || !selectedDataset) { setMapSelectionError('Open a vector dataset before extracting a map area.') - return + return null } if (!isVectorDatasetType(selectedDataset.dataset_type)) { setMapSelectionError('Area extraction requires an active vector dataset.') - return + return null } setMapSelectionLoading(true) @@ -44,9 +44,11 @@ export function useMapSelectionExtract({ limit: 250, }) setMapSelectionResult(response) + return response } catch (error) { setMapSelectionResult(null) setMapSelectionError(formatError(error, 'Area extraction failed')) + return null } finally { setMapSelectionLoading(false) } diff --git a/frontend/src/hooks/useMapSelectionQa.ts b/frontend/src/hooks/useMapSelectionQa.ts index 3535b168..051880a4 100644 --- a/frontend/src/hooks/useMapSelectionQa.ts +++ b/frontend/src/hooks/useMapSelectionQa.ts @@ -22,56 +22,58 @@ export function useMapSelectionQa({ const [mapSelectionQaResult, setMapQaResult] = useState(null) const [latestMapSelectionQualityCheckId, setLatestMapSelectionQualityCheckId] = useState(null) - const runMapSelectionQa = async () => { + const runMapSelectionQa = async (candidateDataset = latestSelectionDataset) => { if (!selectedProjectId) { setMapSelectionQaError('Select a project before running QA/QC.') - return + return null } - if (!latestSelectionDataset) { + if (!candidateDataset) { setMapSelectionQaError('Save the map selection as a dataset before running QA/QC.') - return + return null } if (!selectedMapQaReferenceDatasetId) { setMapSelectionQaError('Select a reference dataset for QA/QC.') - return + return null } - if (latestSelectionDataset.id === selectedMapQaReferenceDatasetId) { + if (candidateDataset.id === selectedMapQaReferenceDatasetId) { setMapSelectionQaError('Candidate and reference datasets must be different.') - return + return null } setMapSelectionQaRunning(true) setMapSelectionQaError(null) setMapQaResult(null) - setLatestMapSelectionQualityCheckId(null) + setLatestMapSelectionQualityCheckId(null) try { const request: QaComparisonRequest = { - candidate_dataset_id: latestSelectionDataset.id, + candidate_dataset_id: candidateDataset.id, reference_dataset_id: selectedMapQaReferenceDatasetId, iou_threshold: 0.5, - area_id: latestSelectionDataset.area_id ?? null, + area_id: candidateDataset.area_id ?? null, } const job: JobRead = await qaApi.runQa(request) if (job.status === 'failed') { setMapSelectionQaError(job.error_message || 'Map selection QA/QC failed') - return + return null } const payload = job.result_json if (!payload || typeof payload !== 'object') { setMapSelectionQaError('Map selection QA/QC result was not available') - return + return null } const parsed = payload as unknown as QaComparisonResult if (!parsed || typeof parsed.status !== 'string') { setMapSelectionQaError('Map selection QA/QC result format was unexpected') - return + return null } setMapQaResult(parsed) setLatestMapSelectionQualityCheckId(parsed.quality_check_id ?? null) await loadQualityChecks(selectedProjectId) await loadProjectData(selectedProjectId) + return parsed } catch (error) { setMapSelectionQaError(formatError(error, 'Map selection QA/QC failed')) + return null } finally { setMapSelectionQaRunning(false) } diff --git a/frontend/src/styles/app.css b/frontend/src/styles/app.css index ca35a904..b2521dd6 100644 --- a/frontend/src/styles/app.css +++ b/frontend/src/styles/app.css @@ -4873,6 +4873,40 @@ section { font-size: 0.78rem; } +.guided-gis-full-run { + grid-column: span 1; +} + +.guided-gis-batch-status { + display: grid; + gap: 0.12rem; + min-width: 0; + border: 1px solid rgba(15, 118, 110, 0.22); + border-radius: 6px; + padding: 0.44rem 0.52rem; + background: #f8fbf9; +} + +.guided-gis-batch-status strong { + overflow: hidden; + color: #0f766e; + font-size: 0.68rem; + font-weight: 850; + letter-spacing: 0.045em; + text-overflow: ellipsis; + text-transform: uppercase; + white-space: nowrap; +} + +.guided-gis-batch-status span { + overflow: hidden; + color: var(--muted); + font-size: 0.76rem; + line-height: 1.25; + text-overflow: ellipsis; + white-space: nowrap; +} + .bbox-select-surface { border-left-width: 3px; }