Add one-click GIS workflow run
GeoIntel CI / docs-smoke (push) Has been cancelled
GeoIntel CI / contract-smoke (push) Has been cancelled

This commit is contained in:
Codex
2026-07-05 00:11:03 +02:00
parent a9c108a4d4
commit 533135885c
12 changed files with 140 additions and 25 deletions
+1
View File
@@ -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)
@@ -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
@@ -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
+4
View File
@@ -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.
+1
View File
@@ -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.
+1 -1
View File
@@ -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.
+67 -6
View File
@@ -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<VectorSelectionResponse | null>
onClearMapSelectionExtract: () => void
onExportMapSelection: (bbox: VectorSelectionBBox) => void
onDeriveMapSelectionDataset: (bbox: VectorSelectionBBox) => void
onExportMapSelection: (bbox: VectorSelectionBBox) => Promise<unknown>
onDeriveMapSelectionDataset: (bbox: VectorSelectionBBox) => Promise<DatasetCreateResponse | null>
onSelectMapQaReferenceDataset: (datasetId: string) => void
onRunMapSelectionQa: () => void
onRunMapSelectionQa: (candidateDataset?: DatasetCreateResponse | null) => Promise<QaComparisonResult | null>
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<string | null>(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 (
<section className="map-workspace-shell" data-testid="map-workspace">
<div className="panel-title-row">
@@ -705,6 +753,18 @@ export function MapWorkspace({
</div>
</div>
<div className="guided-gis-actions">
<button
className="primary-action guided-gis-full-run"
disabled={!selectedMapDataset || !mapFeatureCollection || (!currentSelectionBbox && !selectedAreaBbox && !activeLayerBbox) || fullWorkflowRunning}
type="button"
onClick={runFullGisWorkflow}
>
{fullWorkflowRunning ? 'Running full workflow...' : 'Run full GIS workflow'}
</button>
<div className="guided-gis-batch-status" aria-live="polite">
<strong>Query, save, QA and export</strong>
<span>{fullWorkflowStatus}</span>
</div>
<button
className="secondary-action"
disabled={!mapSelectionResult || !currentSelectionBbox || selectionDatasetSaving}
@@ -740,7 +800,7 @@ export function MapWorkspace({
className="primary-action"
disabled={!latestSelectionDatasetName || !selectedMapQaReferenceDatasetId || mapSelectionQaRunning}
type="button"
onClick={onRunMapSelectionQa}
onClick={() => onRunMapSelectionQa()}
>
{mapSelectionQaRunning ? 'Running QA...' : 'Run QA/QC'}
</button>
@@ -756,6 +816,7 @@ export function MapWorkspace({
{selectionDatasetError ? <p className="error">{selectionDatasetError}</p> : null}
{selectionExportError ? <p className="error">{selectionExportError}</p> : null}
{mapSelectionQaError ? <p className="error">{mapSelectionQaError}</p> : null}
{fullWorkflowError ? <p className="error">{fullWorkflowError}</p> : null}
</div>
</div>
<div className="bbox-select-surface" aria-label="Area selection and extract">
@@ -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'}
</button>
+3 -1
View File
@@ -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)
}
+3 -1
View File
@@ -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)
}
+4 -2
View File
@@ -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)
}
+15 -13
View File
@@ -22,56 +22,58 @@ export function useMapSelectionQa({
const [mapSelectionQaResult, setMapQaResult] = useState<QaComparisonResult | null>(null)
const [latestMapSelectionQualityCheckId, setLatestMapSelectionQualityCheckId] = useState<string | null>(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)
}
+34
View File
@@ -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;
}