fix: isolate regional project context
GeoIntel CI / docs-smoke (push) Canceled after 0s
GeoIntel CI / contract-smoke (push) Canceled after 0s

This commit is contained in:
Codex
2026-07-14 18:41:05 +02:00
parent bb7310e015
commit 9fff0c5706
6 changed files with 46 additions and 7 deletions
+1
View File
@@ -13,6 +13,7 @@
- Added a central operator scope registry with current municipality names and NIS codes, including Mol `13025` and Nijlen `12026`. - Added a central operator scope registry with current municipality names and NIS codes, including Mol `13025` and Nijlen `12026`.
- Added an explicit, idempotent VRBG scope provisioner that creates one regional boundary, 28 member boundaries, one regional Area, 28 municipality Areas and canonical source datasets through the existing API. - Added an explicit, idempotent VRBG scope provisioner that creates one regional boundary, 28 member boundaries, one regional Area, 28 municipality Areas and canonical source datasets through the existing API.
- Added a compact Mol/Kempen region selector to the map-first explorer and made its heading and full-area action scope-neutral. - Added a compact Mol/Kempen region selector to the map-first explorer and made its heading and full-area action scope-neutral.
- Made region switches clear all project-bound state, ignore stale project-data responses and prefer the regional boundary/AOI, preventing Mol context from leaking into the Kempen workbench.
- Kept thematic regional ingestion separate from the boundary pass so large GRB/WCS sources can be partitioned and validated without hidden startup fetches or truncated datasets. - Kept thematic regional ingestion separate from the boundary pass so large GRB/WCS sources can be partitioned and validated without hidden startup fetches or truncated datasets.
## Sprint 188 Official modern Mol land-use series (2026-07-14) ## Sprint 188 Official modern Mol land-use series (2026-07-14)
@@ -132,3 +132,17 @@ def test_kempen_scope_operator_is_packaged_and_exposed_in_map_flow() -> None:
assert 'aria-label="Regio"' in workspace assert 'aria-label="Regio"' in workspace
assert "projects={projects}" in app assert "projects={projects}" in app
assert "onSelectProject={selectProject}" in app assert "onSelectProject={selectProject}" in app
def test_project_switches_reset_scoped_state_and_prefer_the_regional_context() -> None:
bootstrap = (ROOT / "frontend/src/hooks/useWorkbenchBootstrap.ts").read_text(encoding="utf-8")
project_workspace = (ROOT / "frontend/src/hooks/useProjectWorkspace.ts").read_text(encoding="utf-8")
map_state = (ROOT / "frontend/src/hooks/useMapWorkspaceState.ts").read_text(encoding="utf-8")
dataset_workflow = (ROOT / "frontend/src/hooks/useDatasetWorkflow.ts").read_text(encoding="utf-8")
selected_project_branch = bootstrap.split("if (!selectedProjectId)", maxsplit=1)[1]
assert "resetProjectData()" in selected_project_branch
assert "resetDatasetForProject()" in selected_project_branch
assert "projectDataRequestSequence" in project_workspace
assert "vervoerregio|operationele grens" in map_state
assert "datasets.find(isOperationalScopeBoundaryDataset)" in dataset_workflow
+9
View File
@@ -30,6 +30,14 @@ function toRasterMetadata(metadata: Record<string, unknown> | null | undefined):
return metadata as unknown as RasterMetadataResponse return metadata as unknown as RasterMetadataResponse
} }
function isOperationalScopeBoundaryDataset(dataset: DatasetCreateResponse): boolean {
return (
dataset.status === 'ready' &&
(dataset.dataset_type === 'vector' || dataset.dataset_type === 'geojson') &&
dataset.source_metadata?.layer_type === 'regional_boundary'
)
}
function extractRasterTileManifestPath(job: JobRead | null | undefined): string { function extractRasterTileManifestPath(job: JobRead | null | undefined): string {
const manifest = toRasterTileHandoff(job) const manifest = toRasterTileHandoff(job)
return manifest?.manifest_path ?? '' return manifest?.manifest_path ?? ''
@@ -131,6 +139,7 @@ export function useDatasetWorkflow({
} }
// Auto-open the first usable dataset so Data, Map and Exports start with real context. // Auto-open the first usable dataset so Data, Map and Exports start with real context.
const defaultDataset = const defaultDataset =
datasets.find(isOperationalScopeBoundaryDataset) ??
datasets.find(isPrimaryFocusMunicipalityBoundaryDataset) ?? datasets.find(isPrimaryFocusMunicipalityBoundaryDataset) ??
datasets.find((dataset) => isVectorDatasetType(dataset.dataset_type) && dataset.status === 'ready') ?? datasets.find((dataset) => isVectorDatasetType(dataset.dataset_type) && dataset.status === 'ready') ??
datasets.find((dataset) => dataset.status === 'ready') ?? datasets.find((dataset) => dataset.status === 'ready') ??
+3 -3
View File
@@ -31,9 +31,9 @@ export function useMapWorkspaceState({
if (areas.length === 0) { if (areas.length === 0) {
setSelectedMapAreaId('') setSelectedMapAreaId('')
} else if (!selectedMapAreaId || !areas.some((area) => area.id === selectedMapAreaId)) { } else if (!selectedMapAreaId || !areas.some((area) => area.id === selectedMapAreaId)) {
const municipalityArea = areas.find((area) => /gemeente mol|municipality/i.test(area.name)) const operationalScopeArea = areas.find((area) => /vervoerregio|operationele grens/i.test(area.name))
if (municipalityArea) { if (operationalScopeArea) {
setSelectedMapAreaId(municipalityArea.id) setSelectedMapAreaId(operationalScopeArea.id)
} else { } else {
const largestArea = [...areas].sort((left, right) => (right.area_m2 ?? 0) - (left.area_m2 ?? 0))[0] const largestArea = [...areas].sort((left, right) => (right.area_m2 ?? 0) - (left.area_m2 ?? 0))[0]
if (largestArea?.area_m2) { if (largestArea?.area_m2) {
+11 -1
View File
@@ -1,4 +1,4 @@
import { FormEvent, useMemo, useState } from 'react' import { FormEvent, useMemo, useRef, useState } from 'react'
import { import {
PRIMARY_FOCUS_AREA_GEOJSON, PRIMARY_FOCUS_AREA_GEOJSON,
PRIMARY_FOCUS_AREA_NAME, PRIMARY_FOCUS_AREA_NAME,
@@ -46,6 +46,7 @@ export function useProjectWorkspace() {
const [loadingAreas, setLoadingAreas] = useState(false) const [loadingAreas, setLoadingAreas] = useState(false)
const [loadingDatasets, setLoadingDatasets] = useState(false) const [loadingDatasets, setLoadingDatasets] = useState(false)
const [errorMessage, setErrorMessage] = useState<string | null>(null) const [errorMessage, setErrorMessage] = useState<string | null>(null)
const projectDataRequestSequence = useRef(0)
const [projectForm, setProjectForm] = useState<ProjectCreate>({ const [projectForm, setProjectForm] = useState<ProjectCreate>({
name: '', name: '',
@@ -145,22 +146,30 @@ export function useProjectWorkspace() {
} }
const loadProjectData = async (projectId: string) => { const loadProjectData = async (projectId: string) => {
const requestId = ++projectDataRequestSequence.current
setLoadingAreas(true) setLoadingAreas(true)
setLoadingDatasets(true) setLoadingDatasets(true)
setErrorMessage(null) setErrorMessage(null)
try { try {
const projectData = await fetchProjectData(projectId) const projectData = await fetchProjectData(projectId)
if (requestId !== projectDataRequestSequence.current) {
return null
}
setAreas(projectData.areas) setAreas(projectData.areas)
setDatasets(projectData.datasets) setDatasets(projectData.datasets)
return projectData return projectData
} catch (error) { } catch (error) {
if (requestId === projectDataRequestSequence.current) {
setErrorMessage(error instanceof Error ? error.message : 'Failed to load project data') setErrorMessage(error instanceof Error ? error.message : 'Failed to load project data')
}
return null return null
} finally { } finally {
if (requestId === projectDataRequestSequence.current) {
setLoadingAreas(false) setLoadingAreas(false)
setLoadingDatasets(false) setLoadingDatasets(false)
} }
} }
}
const createProject = async (event: FormEvent) => { const createProject = async (event: FormEvent) => {
event.preventDefault() event.preventDefault()
@@ -209,6 +218,7 @@ export function useProjectWorkspace() {
} }
const resetProjectData = () => { const resetProjectData = () => {
projectDataRequestSequence.current += 1
setAreas([]) setAreas([])
setDatasets([]) setDatasets([])
} }
@@ -70,6 +70,11 @@ export function useWorkbenchBootstrap({
resetExportsForProject() resetExportsForProject()
return return
} }
resetProjectData()
resetDatasetForProject()
resetDetectionForProject()
resetSegmentationForProject()
resetExportsForProject()
loadProjectData(selectedProjectId).catch(() => null) loadProjectData(selectedProjectId).catch(() => null)
loadDetectionRuns(selectedProjectId).catch(() => null) loadDetectionRuns(selectedProjectId).catch(() => null)
loadSegmentationRuns(selectedProjectId).catch(() => null) loadSegmentationRuns(selectedProjectId).catch(() => null)