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 -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;
}