From 50e7d0163b99bca54f2a424560bef41dda90638a Mon Sep 17 00:00:00 2001 From: Codex Date: Tue, 14 Jul 2026 13:32:47 +0200 Subject: [PATCH] fix: keep latest map dataset context --- CHANGELOG.md | 1 + ...sprint186_map_first_geographic_explorer.py | 10 ++++++ docs/CODEX_EXECUTION_LOG.md | 11 +++--- frontend/src/hooks/useDatasetWorkflow.ts | 34 ++++++++++++++++--- 4 files changed, 47 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 52cdff14..46c88c58 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ - Added true drag-to-select behavior in MapLibre and exact total intersection counts alongside the bounded 1,000-feature map preview. - Made the official full Mol municipality area and the largest authoritative building layer the initial map context. - Added an idempotent operator provisioner for official Mol GRB roads, water and parcels through the existing API/DatasetService/PostGIS persistence flow. +- Prevented stale asynchronous dataset-detail responses from replacing the latest selected map layer and its visible context. ## Sprint 185 Coverage-aware Mol operational benchmark (2026-07-14) diff --git a/backend/tests/test_sprint186_map_first_geographic_explorer.py b/backend/tests/test_sprint186_map_first_geographic_explorer.py index f1fcf2bc..bcf30782 100644 --- a/backend/tests/test_sprint186_map_first_geographic_explorer.py +++ b/backend/tests/test_sprint186_map_first_geographic_explorer.py @@ -40,6 +40,16 @@ def test_map_rectangle_drag_is_wired_to_automatic_analysis() -> None: assert "onMapBboxSelectRef.current?.(bbox)" in geomap +def test_dataset_detail_responses_cannot_overwrite_the_latest_map_layer() -> None: + workflow = read("frontend/src/hooks/useDatasetWorkflow.ts") + + assert "const datasetDetailRequestSequence = useRef(0)" in workflow + assert "const detailRequestId = ++datasetDetailRequestSequence.current" in workflow + assert "detailRequestId !== datasetDetailRequestSequence.current" in workflow + assert "detailRequestId === datasetDetailRequestSequence.current" in workflow + assert "datasetDetailRequestSequence.current += 1" in workflow + + def test_selection_contract_reports_total_intersections_separately_from_preview() -> None: schema = read("backend/app/schemas/operations.py") service = read("backend/app/services/vector_feature_service.py") diff --git a/docs/CODEX_EXECUTION_LOG.md b/docs/CODEX_EXECUTION_LOG.md index 7bdac94c..75fccc16 100644 --- a/docs/CODEX_EXECUTION_LOG.md +++ b/docs/CODEX_EXECUTION_LOG.md @@ -7752,15 +7752,18 @@ Changed: - Added exact `total_feature_count` to the vector bbox-selection response while retaining the existing 1,000-feature geometry cap. - Added `provision_mol_context_layers.py` for official GRB roads (`Wegsegment`), water (`WTZ`, `WLAS`, `WGR`) and parcels (`ADP`) clipped to NIS 13025 and imported through the public dataset API. -Validated so far: +Validated: - Frontend typecheck and production build passed. - Focused map, orchestration and new explorer tests passed. - Official fetch-only smoke produced 8,444 Mol road features, 3,668 water features and 32,961 parcels with complete pagination and no truncation. -- Full backend suite reached 516 tests; one legacy component-boundary guard initially failed and was resolved by moving theme API orchestration into `useMapThemeSelectionInsights`. +- Full backend suite passed 517 tests; one legacy component-boundary guard initially failed and was resolved by moving theme API orchestration into `useMapThemeSelectionInsights`. +- Tower was rebuilt from `main`; live PostGIS 3.6 connectivity, Alembic head `202606120900`, required geometry tables/indexes and browser proxy health all passed. +- The official provisioner persisted 8,444 roads, 3,668 water features and 32,961 parcels alongside the existing 36,941 GRB buildings for the complete Mol municipality. +- In-app browser validation of `Volledige gemeente` reported 114.55 km2, 36,941 buildings and a building density of 322.5/km2; the bounded preview clearly reported 1,000 of 36,941 features. +- Dataset-detail loading now ignores stale asynchronous responses, preventing a slower municipality-boundary request from overwriting the currently selected building layer or its map context. Limitations: - Population and forest/green remain unavailable rather than simulated until suitable authoritative sources and semantics are selected. -- Live Tower import and browser verification follow after the full readiness gate and deployment. Next: -- Deploy, import the three official Mol context layers, validate rectangle analysis in the in-app browser and then define authoritative population and land-cover source adapters. +- Define authoritative population and land-cover source adapters, then reuse the proven municipality provisioner and bbox analysis flow for the complete Kempen. diff --git a/frontend/src/hooks/useDatasetWorkflow.ts b/frontend/src/hooks/useDatasetWorkflow.ts index b39adea8..b229df30 100644 --- a/frontend/src/hooks/useDatasetWorkflow.ts +++ b/frontend/src/hooks/useDatasetWorkflow.ts @@ -1,4 +1,4 @@ -import { FormEvent, useEffect, useMemo, useState } from 'react' +import { FormEvent, useEffect, useMemo, useRef, useState } from 'react' import { datasetsApi, jobsApi } from '../services/api' import type { AreaRead, @@ -99,6 +99,7 @@ export function useDatasetWorkflow({ const [ndbiNirBand, setNdbiNirBand] = useState(4) const [loadingDatasetDetails, setLoadingDatasetDetails] = useState(false) const [datasetDetailError, setDatasetDetailError] = useState(null) + const datasetDetailRequestSequence = useRef(0) const [datasetForm, setDatasetForm] = useState({ datasetType: 'vector', source: 'user_upload', @@ -139,8 +140,11 @@ export function useDatasetWorkflow({ } }, [datasets, isVectorDatasetType, selectedDatasetId, selectedProjectId]) - const loadDatasetJobs = async (projectId: string, datasetId: string) => { + const loadDatasetJobs = async (projectId: string, datasetId: string, detailRequestId?: number) => { const response = await jobsApi.list(projectId, { dataset_id: datasetId, limit: 20, offset: 0 }) + if (detailRequestId != null && detailRequestId !== datasetDetailRequestSequence.current) { + return + } setJobs(response.items) const latestRasterTileJob = response.items.find( (job) => job.job_type === 'raster.tile' && extractRasterTileManifestPath(job), @@ -152,6 +156,7 @@ export function useDatasetWorkflow({ } const loadDatasetDetails = async (projectId: string, dataset: DatasetCreateResponse) => { + const detailRequestId = ++datasetDetailRequestSequence.current setLoadingDatasetDetails(true) setDatasetDetailError(null) setSelectedDataset(dataset) @@ -167,26 +172,42 @@ export function useDatasetWorkflow({ try { if (isVectorDatasetType(dataset.dataset_type)) { const summary = await datasetsApi.vectorSummary(projectId, dataset.id) + if (detailRequestId !== datasetDetailRequestSequence.current) { + return + } setSelectedDatasetSummary(summary) const featureCount = summary.feature_count ?? dataset.feature_count if (featureCount == null || featureCount <= VECTOR_VIEWPORT_FEATURE_THRESHOLD) { const content = await datasetsApi.getContent(projectId, dataset.id) + if (detailRequestId !== datasetDetailRequestSequence.current) { + return + } setDatasetContent(content) } } else if (dataset.dataset_type === 'raster') { try { const rasterInspection = await datasetsApi.rasterInspect(projectId, dataset.id) + if (detailRequestId !== datasetDetailRequestSequence.current) { + return + } setSelectedRasterMetadata(toRasterMetadata(rasterInspection.metadata)) } catch (error) { + if (detailRequestId !== datasetDetailRequestSequence.current) { + return + } setSelectedRasterMetadata(null) setDatasetDetailError(formatError(error, 'Raster metadata unavailable')) } } - await loadDatasetJobs(projectId, dataset.id) + await loadDatasetJobs(projectId, dataset.id, detailRequestId) } catch (error) { - setDatasetDetailError(formatError(error, 'Unable to load dataset detail')) + if (detailRequestId === datasetDetailRequestSequence.current) { + setDatasetDetailError(formatError(error, 'Unable to load dataset detail')) + } } finally { - setLoadingDatasetDetails(false) + if (detailRequestId === datasetDetailRequestSequence.current) { + setLoadingDatasetDetails(false) + } } } @@ -497,6 +518,7 @@ export function useDatasetWorkflow({ } const resetDatasetForProject = () => { + datasetDetailRequestSequence.current += 1 setSelectedDatasetId(null) setSelectedDataset(null) setSelectedDatasetSummary(null) @@ -507,6 +529,8 @@ export function useDatasetWorkflow({ setLatestRasterTileManifestPath('') setLatestRasterTileManifest(null) setJobs([]) + setLoadingDatasetDetails(false) + setDatasetDetailError(null) } return {