fix: clear stale regional map results
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 20:50:45 +02:00
parent 7af47abe2a
commit 7997a7220a
9 changed files with 131 additions and 10 deletions
+9
View File
@@ -7,6 +7,13 @@
# Changelog
## Sprint 192 Regional map state correctness (2026-07-14)
- Fixed the map-first work-area selector so changing from the complete Kempen region to a municipality clears the old bbox, theme totals and result geometry before switching Areas.
- Invalidated outstanding single-theme and multi-theme selection requests on reset, preventing a slow previous-area response from restoring stale results after a scope change.
- Replaced the hardcoded viewport instruction for `buildings` with the active dataset's reference layer name.
- Added focused state/race/status regressions and passed the complete 556-test release gate plus frontend typecheck/build.
## Sprint 191 Regional Kempen GRB context foundation (2026-07-14)
- Added one explicit regional operator for current GRB roads, water and parcels, with independent resumable municipality partitions and one normal PostGIS dataset per theme.
@@ -16,6 +23,8 @@
- Added truncation guards, atomic manifests, checksum reuse, duplicate rejection, Docker packaging, readiness compilation and focused geometry/persistence tests.
- Documented source semantics honestly: road objects are not traffic data, heterogeneous water objects are not volume metrics and ADP is not a legal cadastral survey.
- Kept all source access operator-only; the browser, startup path and public `not_configured` GRB provider perform no external fetch.
- Provisioned the live Tower snapshot: 84,504 roads, 88,332 water objects and 415,288 parcels across 28 complete partitions per theme; immediate repeat runs reused all artifacts and datasets.
- Verified exact manifest/PostGIS/distinct-ID parity, valid non-empty EPSG:4326 geometries, one immutable DatasetVersion per snapshot and canonical full-region/Mol Area selection responses.
## Sprint 190 Regional Kempen GRB buildings (2026-07-14)
@@ -0,0 +1,36 @@
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
def read(path: str) -> str:
return (ROOT / path).read_text(encoding="utf-8")
def test_work_area_change_clears_stale_spatial_results_before_switching_area() -> None:
workspace = read("frontend/src/components/map/MapWorkspace.tsx")
assert "const handleSelectMapArea = (areaId: string) => {" in workspace
assert "clearAreaSelection()\n onSelectMapArea(areaId)" in workspace
assert workspace.count("handleSelectMapArea(event.target.value)") == 2
def test_cancelled_selection_requests_cannot_restore_stale_results() -> None:
selection_hook = read("frontend/src/hooks/useMapSelectionExtract.ts")
themes_hook = read("frontend/src/hooks/useMapThemeSelectionInsights.ts")
assert "const requestSequence = useRef(0)" in selection_hook
assert "requestSequence.current += 1\n setMapSelectionBbox(null)" in selection_hook
assert "if (requestSequence.current !== sequence)" in selection_hook
assert "const requestSequence = useRef(0)" in themes_hook
assert "requestSequence.current += 1\n setThemeInsights([])" in themes_hook
assert "if (requestSequence.current !== sequence)" in themes_hook
def test_viewport_status_names_the_active_reference_layer() -> None:
hook = read("frontend/src/hooks/useViewportVectorLayer.ts")
assert "load buildings from PostGIS" not in hook
assert "selectedDataset?.reference_layer_name?.trim() || 'features'" in hook
assert "to load ${layerLabel} from PostGIS" in hook
+24 -1
View File
@@ -11,8 +11,31 @@ Tested before deployment:
- `python -m pytest backend/tests/test_sprint191_regional_grb_context.py backend/tests/test_sprint190_regional_grb_buildings.py backend/tests/test_sprint106_map_bbox_extract.py -q` (`19 passed`).
- `bash scripts/run_readiness_check.sh` (`553 passed`; one Alembic head; frontend typecheck/build and script syntax gates passed).
Runtime evidence:
- Pushed commits `0ed214d` and `7af47ab`, rebuilt the AI-enabled all-in-one Tower runtime and passed PostGIS 3.6/Alembic head `202607140001` plus browser-proxy verification.
- Roads: 84,504 unique `Wegsegment` features, 124,721,658-byte artifact, Dataset `715b24f1-148a-4d50-9c40-d5468f670ffb`.
- Water: 88,332 unique features (25,648 `WTZ`, 4,605 `WLAS`, 58,079 `WGR`), 122,737,017-byte artifact, Dataset `689bb65d-0b03-4c5c-8f42-b5d2d899d8ca`.
- Parcels: 415,288 unique `ADP` features, 525,243,364-byte artifact, Dataset `3cf5e8da-9ab0-49ef-96e4-f74b7557e0a4`.
- All three repeat runs returned `reused=true`; manifest, metadata, PostGIS row and distinct source-ID counts match exactly. There are no missing IDs, invalid/empty geometries or non-4326 rows, and every dataset has one immutable version.
- Canonical Area selections returned the full regional counts and Mol counts of 8,444 roads, 3,669 water objects and 33,038 parcels. The browser exposed all four available regional themes and honestly left population and forest unavailable.
Next:
- Pass the complete release gate, deploy the operator and provision all three live regional snapshots before claiming runtime readiness.
- Extend official population and modern land-use time series from Mol to the approved regional scope, retaining source-specific methodology and bounded municipality partitions.
## Sprint 192 Regional map state correctness (2026-07-14)
Changed:
- Cleared spatial selection and multi-theme result state before changing the selected work Area.
- Invalidated in-flight single-theme and multi-theme selection requests when their context is reset.
- Made large-vector viewport guidance name the active reference layer instead of always saying `buildings`.
Tested:
- `python -m pytest backend/tests/test_sprint192_regional_map_state.py backend/tests/test_sprint186_map_first_geographic_explorer.py backend/tests/test_sprint190_regional_grb_buildings.py -q` (`14 passed`).
- Frontend typecheck and production build passed.
- `bash scripts/run_readiness_check.sh` (`556 passed`; one Alembic head; frontend typecheck/build and script syntax gates passed).
Next:
- Deploy and repeat the exact regional full-Area analysis followed by a Mol Area switch; no stale regional result or hardcoded building guidance may remain.
## Sprint 169 Filtered YOLO candidate gate and operator hardening (2026-07-12)
+2 -1
View File
@@ -10,7 +10,8 @@
- [x] Select, validate and provision official Departement Omgeving 2013-2025 forest snapshots from the 10 m land-use map.
- [x] Define and provision the official 28-municipality Vervoerregio Kempen as the explicit operational regional scope.
- [x] Partition and provision the complete current GRB building theme for all 28 Kempen transport-region municipalities, with resumable source artifacts and one queryable regional PostGIS dataset.
- [ ] Provision the remaining regional context themes in bounded partitions: roads, water and parcels, followed by population and land-use time series.
- [x] Provision the remaining regional GRB context themes in bounded partitions: roads, water and parcels.
- [ ] Extend official population and land-use time series from Mol to the approved 28-municipality regional scope.
This file now starts with the current implementation status. Older preparation/backlog sections are preserved below as historical planning context and should not be treated as the live sprint board without checking `docs/CODEX_EXECUTION_LOG.md`.
+7 -2
View File
@@ -744,6 +744,11 @@ export function MapWorkspace({
onClearMapSelectionExtract()
}
const handleSelectMapArea = (areaId: string) => {
clearAreaSelection()
onSelectMapArea(areaId)
}
const downloadAreaSelection = () => {
if (!mapSelectionResult) {
return
@@ -1061,7 +1066,7 @@ export function MapWorkspace({
<label className="geo-scope-select">
Werkgebied
<select aria-label="Werkgebied" value={selectedMapAreaId} onChange={(event) => onSelectMapArea(event.target.value)} disabled={areas.length === 0}>
<select aria-label="Werkgebied" value={selectedMapAreaId} onChange={(event) => handleSelectMapArea(event.target.value)} disabled={areas.length === 0}>
<option value="">Geen werkgebied</option>
{areas.map((area) => (
<option key={area.id} value={area.id}>{area.name}</option>
@@ -1389,7 +1394,7 @@ export function MapWorkspace({
<select
aria-label="Werkgebied"
value={selectedMapAreaId}
onChange={(event) => onSelectMapArea(event.target.value)}
onChange={(event) => handleSelectMapArea(event.target.value)}
disabled={areas.length === 0}
data-testid="map-area-select"
>
+19 -2
View File
@@ -1,4 +1,4 @@
import { useEffect, useState } from 'react'
import { useEffect, useRef, useState } from 'react'
import { datasetsApi } from '../services/api'
import { formatError } from '../lib/formatError'
import type { DatasetCreateResponse, VectorSelectionBBox, VectorSelectionResponse } from '../types'
@@ -18,14 +18,19 @@ export function useMapSelectionExtract({
const [mapSelectionResult, setMapSelectionResult] = useState<VectorSelectionResponse | null>(null)
const [mapSelectionLoading, setMapSelectionLoading] = useState(false)
const [mapSelectionError, setMapSelectionError] = useState<string | null>(null)
const requestSequence = useRef(0)
useEffect(() => {
requestSequence.current += 1
setMapSelectionBbox(null)
setMapSelectionLoading(false)
}, [selectedProjectId])
useEffect(() => {
requestSequence.current += 1
setMapSelectionResult(null)
setMapSelectionError(null)
setMapSelectionLoading(false)
}, [selectedProjectId, selectedDataset?.id])
const runMapSelectionExtract = async (bbox: VectorSelectionBBox, areaId?: string) => {
@@ -38,6 +43,8 @@ export function useMapSelectionExtract({
return null
}
const sequence = requestSequence.current + 1
requestSequence.current = sequence
setMapSelectionLoading(true)
setMapSelectionError(null)
setMapSelectionBbox(bbox)
@@ -47,21 +54,31 @@ export function useMapSelectionExtract({
area_id: areaId,
limit: 1000,
})
if (requestSequence.current !== sequence) {
return null
}
setMapSelectionResult(response)
return response
} catch (error) {
if (requestSequence.current !== sequence) {
return null
}
setMapSelectionResult(null)
setMapSelectionError(formatError(error, 'Area extraction failed'))
return null
} finally {
setMapSelectionLoading(false)
if (requestSequence.current === sequence) {
setMapSelectionLoading(false)
}
}
}
const resetMapSelectionExtract = () => {
requestSequence.current += 1
setMapSelectionBbox(null)
setMapSelectionResult(null)
setMapSelectionError(null)
setMapSelectionLoading(false)
}
return {
@@ -1,4 +1,4 @@
import { useEffect, useState } from 'react'
import { useEffect, useRef, useState } from 'react'
import { formatError } from '../lib/formatError'
import { datasetsApi } from '../services/api/datasets'
import type { DatasetCreateResponse, VectorSelectionBBox, VectorSelectionResponse } from '../types'
@@ -18,15 +18,20 @@ export function useMapThemeSelectionInsights<TThemeId extends string>(
const [themeInsights, setThemeInsights] = useState<Array<MapThemeInsight<TThemeId>>>([])
const [themeInsightsLoading, setThemeInsightsLoading] = useState(false)
const [themeInsightsError, setThemeInsightsError] = useState<string | null>(null)
const requestSequence = useRef(0)
useEffect(() => {
requestSequence.current += 1
setThemeInsights([])
setThemeInsightsError(null)
setThemeInsightsLoading(false)
}, [selectedProjectId])
const clearThemeInsights = () => {
requestSequence.current += 1
setThemeInsights([])
setThemeInsightsError(null)
setThemeInsightsLoading(false)
}
const loadThemeInsights = async (
@@ -40,6 +45,8 @@ export function useMapThemeSelectionInsights<TThemeId extends string>(
return []
}
const sequence = requestSequence.current + 1
requestSequence.current = sequence
setThemeInsightsLoading(true)
setThemeInsightsError(null)
try {
@@ -56,6 +63,9 @@ export function useMapThemeSelectionInsights<TThemeId extends string>(
)
const successful = settled.flatMap((item) => (item.status === 'fulfilled' ? [item.value] : []))
const failureCount = settled.length - successful.length
if (requestSequence.current !== sequence) {
return []
}
setThemeInsights(successful)
if (failureCount > 0) {
setThemeInsightsError(
@@ -64,11 +74,16 @@ export function useMapThemeSelectionInsights<TThemeId extends string>(
}
return successful
} catch (error) {
if (requestSequence.current !== sequence) {
return []
}
setThemeInsights([])
setThemeInsightsError(formatError(error, 'De gebiedsanalyse is mislukt.'))
return []
} finally {
setThemeInsightsLoading(false)
if (requestSequence.current === sequence) {
setThemeInsightsLoading(false)
}
}
}
+3 -2
View File
@@ -38,6 +38,7 @@ export function useViewportVectorLayer({
)
const zoom = viewport?.zoom ?? null
const zoomRequired = enabled && (zoom === null || zoom < VECTOR_VIEWPORT_MIN_ZOOM)
const layerLabel = selectedDataset?.reference_layer_name?.trim() || 'features'
useEffect(() => {
requestSequence.current += 1
@@ -103,7 +104,7 @@ export function useViewportVectorLayer({
return null
}
if (zoomRequired) {
return `Zoom in to level ${VECTOR_VIEWPORT_MIN_ZOOM} to load buildings from PostGIS.`
return `Zoom in to level ${VECTOR_VIEWPORT_MIN_ZOOM} to load ${layerLabel} from PostGIS.`
}
if (loading) {
return 'Loading visible features from PostGIS...'
@@ -115,7 +116,7 @@ export function useViewportVectorLayer({
return `${loadedFeatureCount.toLocaleString()} visible features loaded; zoom in further because this view exceeds the ${VECTOR_VIEWPORT_FEATURE_LIMIT.toLocaleString()} feature limit.`
}
return `${loadedFeatureCount.toLocaleString()} visible of ${(featureCount ?? 0).toLocaleString()} total features loaded from PostGIS.`
}, [enabled, error, featureCount, loadedFeatureCount, loading, truncated, zoomRequired])
}, [enabled, error, featureCount, layerLabel, loadedFeatureCount, loading, truncated, zoomRequired])
return {
enabled,
+14
View File
@@ -1357,6 +1357,20 @@ objects rather than a volume metric; `ADP` is the graphical presumed cadastral
parcel location rather than a legal survey. The public GRB provider remains
`not_configured`, and no fetch runs from the browser or during startup.
The verified Tower snapshot for `2026-07-14` contains:
- roads: 84,504 unique `Wegsegment` objects in Dataset
`715b24f1-148a-4d50-9c40-d5468f670ffb`;
- water: 88,332 unique objects (25,648 `WTZ`, 4,605 `WLAS` and
58,079 `WGR`) in Dataset `689bb65d-0b03-4c5c-8f42-b5d2d899d8ca`;
- parcels: 415,288 unique `ADP` objects in Dataset
`3cf5e8da-9ab0-49ef-96e4-f74b7557e0a4`.
All three manifests report `reference_truncated=false`. PostGIS row counts
and distinct source IDs match the manifests exactly, all geometries are
non-empty and valid in EPSG:4326, and immediate repeat runs reuse the same
artifact and Dataset.
## Tower deployment
Push the local branch to Gitea, then rebuild the Unraid/Tower Docker runtime: