diff --git a/CHANGELOG.md b/CHANGELOG.md index 382895a6..e791e5aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,21 @@ # Changelog +## Sprint 232 V1 user-flow completion (2026-07-17) + +- Completed the primary end-user handoff from map analysis to a usable result, + local AI question and downloads without exposing technical project or model + setup in the normal path. +- Added explicit metric names to the all-theme summary so values such as a + station water level are no longer shown without their measured meaning. +- Kept vector selections downloadable as GeoJSON and added JSON downloads for + governed raster analyses and complete historical comparisons. +- Added compact post-analysis actions for the local grounded Ollama assistant + and the existing Downloads workspace, preserving the active area context. +- Replaced the accumulated TODO lists as the product roadmap with one current + V1 completion board; historical sprint checklists remain retained as + implementation evidence. + ## Sprint 231 Governed orthophoto release promotion (2026-07-17) - Added an operator-only `plan -> stage -> review -> apply` coordinator for one diff --git a/backend/tests/test_sprint186_map_first_geographic_explorer.py b/backend/tests/test_sprint186_map_first_geographic_explorer.py index a63a9b8e..35ebfa62 100644 --- a/backend/tests/test_sprint186_map_first_geographic_explorer.py +++ b/backend/tests/test_sprint186_map_first_geographic_explorer.py @@ -24,7 +24,7 @@ def test_map_first_explorer_is_the_default_product_flow() -> None: assert "useMapThemeSelectionInsights" in workspace assert "datasetsApi.selectVectorFeatures" in read("frontend/src/hooks/useMapThemeSelectionInsights.ts") assert "activeSelectionResult" in workspace - assert "downloadActiveThemeSelection" in workspace + assert "downloadActiveThemeResult" in workspace assert "disabled={!activeSelectionResult}" in workspace assert "bboxesEqual(mapSelectionBbox, selectedAreaBbox)" in workspace diff --git a/backend/tests/test_sprint232_v1_completion_flow.py b/backend/tests/test_sprint232_v1_completion_flow.py new file mode 100644 index 00000000..426678e7 --- /dev/null +++ b/backend/tests/test_sprint232_v1_completion_flow.py @@ -0,0 +1,43 @@ +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_theme_overview_names_the_metric_instead_of_showing_a_bare_value() -> None: + workspace = read("frontend/src/components/map/MapWorkspace.tsx") + styles = read("frontend/src/styles/app.css") + + assert 'className="geo-theme-result-value"' in workspace + assert "item.result.summary.metric_label" in workspace + assert ".geo-theme-result-value" in styles + + +def test_current_and_historical_results_are_downloadable() -> None: + workspace = read("frontend/src/components/map/MapWorkspace.tsx") + + assert "activeTheme.id}-analysis.json" in workspace + assert "activeTheme.id}-selection.geojson" in workspace + assert "application/geo+json" in workspace + assert "downloadTemporalComparison" in workspace + assert "Download vergelijking" in workspace + assert "Kopieer vergelijking" in workspace + + +def test_completed_analysis_hands_off_to_ai_and_downloads_responsively() -> None: + app = read("frontend/src/App.tsx") + workspace = read("frontend/src/components/map/MapWorkspace.tsx") + styles = read("frontend/src/styles/app.css") + + assert "onOpenAssistant: () => void" in workspace + assert "onOpenExports: () => void" in workspace + assert "Stel AI-vraag" in workspace + assert "Open downloads" in workspace + assert "onOpenAssistant={() => setActiveWorkspace('assistant')}" in app + assert "onOpenExports={() => setActiveWorkspace('exports')}" in app + assert ".geo-result-next-actions" in styles + assert ".geo-results-panel > .geo-result-next-actions" in styles diff --git a/docs/CODEX_EXECUTION_LOG.md b/docs/CODEX_EXECUTION_LOG.md index e3171040..193c830d 100644 --- a/docs/CODEX_EXECUTION_LOG.md +++ b/docs/CODEX_EXECUTION_LOG.md @@ -9839,3 +9839,41 @@ Next: workspace so official/local edition, review identity, Dataset ID and hash integrity can be inspected without shell access. Keep all refresh actions operator-only and outside the browser. + +## Sprint 232 - V1 user-flow completion (2026-07-17) + +Implemented: +- Re-audited the live platform from the end-user entry point instead of + continuing operator release work. The normal path is now explicitly Map -> + area -> measurements/evolution -> AI question or download. +- Added the semantic metric label to every row in the 15-theme result summary. + This distinguishes, for example, a Waterinfo station level from mapped + surface-water area or watercourse length. +- Kept vector output as GeoJSON and added complete JSON downloads for governed + raster analyses and historical comparisons. Copy actions now copy the full + analysis response rather than an empty raster GeoJSON shell. +- Added direct post-analysis handoff buttons to `AI-vragen` and `Downloads`. + The selected Area or drawn bbox remains in shared App state. +- Added one current V1 completion board to `docs/TODO.md`; legacy checklists + remain preserved but no longer define product readiness. + +Validation so far: +- Live pre-change acceptance selected the persisted Mol municipality Area and + completed all 15 PostGIS-backed themes. Representative results included + 502.14 ha building footprint, 3,626.56 ha forest/green, 36,783 inhabitants, + 3,223.5 ha agricultural use and 1,015.51 km roads. +- Live historical acceptance compared the official modern forest series from + 2013 to 2025 over Mol: five observations, 3,723.17 ha to 3,626.56 ha, + absolute change -96.6 ha and relative change -2.6%. +- Ollama status and installed-model discovery were live. A grounded assistant + query returned HTTP 200; a separate `qwen3.5:4b` request completed in 13.01 + seconds. Model answers remain advisory and source evidence remains primary. +- Focused end-user, temporal/Ollama and Sprint 232 tests pass: 38 tests. +- Frontend typecheck and production build pass. +- Complete readiness passes with 881 backend tests, 110 documented routes, + one Alembic head `202607160001`, frontend typecheck and production build. + +Next: +- Run the complete release gate, deploy to Tower and repeat the same user flow + with visual checks of metric labels, raster/evolution downloads and both + post-analysis handoffs. diff --git a/docs/TODO.md b/docs/TODO.md index 8894c0b9..6a3824da 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -1,5 +1,40 @@ # GeoIntel TODO +## Actuele V1-productstatus + +Dit is het enige actuele afwerkingsbord. De lange sprint- en +voorbereidingslijsten verderop blijven bewaard als historisch bewijs, maar zijn +geen open productroadmap meer. + +- [x] Open automatisch de volledige Kempen-werkruimte met Mol als snel + selecteerbaar werkgebied; een technische project- of regioselectie is niet + vereist. +- [x] Kies een begrijpbaar datathema, teken een rechthoek of gebruik het + volledige werkgebied en analyseer alle 15 beschikbare thema's uit PostGIS. +- [x] Toon betekenisvolle eenheden en metrieklabels voor oppervlakte, lengte, + inwoners, hoogte, scenario's en stationsmetingen; objectaantallen zijn + ondersteunend. +- [x] Vergelijk beschikbare historische meetmomenten met een volledige + tijdreeks, absolute wijziging, procentuele wijziging en bronbeperking. +- [x] Download vectorresultaten als GeoJSON, rasteranalyses als JSON en + historische vergelijkingen als JSON; kopieren blijft voor alle drie mogelijk. +- [x] Ga vanuit een afgeronde analyse rechtstreeks naar de lokale + brongebonden Ollama-assistent of naar Downloads. +- [x] Gebruik de operationele lokale YOLO/PyTorch-keten alleen met persisted + detecties, GRB-QA en expliciete controlewaarschuwingen. +- [x] Draai de volledige applicatie in de beheersbare Unraid-container op poort + 1202 met PostGIS, backend, frontend, Ollama-koppeling en modelconfiguratie. + +Bewuste, niet-blokkerende grenzen: + +- Watervolume blijft niet beschikbaar zolang geen gekoppelde bathymetrie, + bodemhoogte en onzekerheidsmodel bestaan. Dit is geospatiale correctheid, + geen onafgewerkte UI. +- Gebouwdetectie is operationeel maar blijft een controlekandidaat; verdere + training gebeurt pas na nieuwe, onafhankelijke reviewdata. +- Een echt SAM/YOLO-seg-model en een trainingsstudio blijven post-V1. De + persistente segmentatie- en QA-fundering blijft beschikbaar voor later. + ## Map-first Mol explorer - [x] Make the Mol geographic explorer the default workflow. diff --git a/frontend/README.md b/frontend/README.md index 5e54679c..d41a48f6 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -71,6 +71,14 @@ but is not required for the primary choose-theme, draw-area, read-result flow. The primary workflow is deliberately short: choose a municipality or the complete region, choose a data theme, drag a rectangle on the MapLibre map and read the resulting PostGIS evidence. Releasing the drag runs the active theme query and every other available theme query for the same EPSG:4326 bbox. The result panel shows selection area, exact intersection totals, active-theme density, source identity and bounded feature properties. Map rendering remains capped at 1,000 features while `total_feature_count` reports the exact database count. +Every cross-theme result now names the measured quantity next to the value, so +a station water level, mapped area and line length cannot appear as an +unexplained bare number. Current vector selections download as GeoJSON, +current raster analyses download as JSON and historical comparisons download +as JSON with their complete timeline and provenance. After a successful +analysis, compact next actions carry the same spatial context directly into +`AI-vragen` or `Downloads`. + For a rectangle in `Laatste toestand`, `Herken gebouwen` runs the complete operational image path without opening the technical AI screen: bounded official orthophoto acquisition, raster persistence, safe tiling, the active diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index ddbe41d9..a1088e20 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1079,6 +1079,8 @@ function App(): JSX.Element { onRunOrthophotoAnalysis={mapOrthophotoAnalysis.run} onSelectOrthophotoProduct={mapOrthophotoAnalysis.setSelectedProductKey} onClearQualityEvidence={clearQualityEvidenceGeoJson} + onOpenAssistant={() => setActiveWorkspace('assistant')} + onOpenExports={() => setActiveWorkspace('exports')} /> ) : null} diff --git a/frontend/src/components/map/MapWorkspace.tsx b/frontend/src/components/map/MapWorkspace.tsx index 549a84a7..5cbe01d1 100644 --- a/frontend/src/components/map/MapWorkspace.tsx +++ b/frontend/src/components/map/MapWorkspace.tsx @@ -625,8 +625,8 @@ function copyText(text: string): void { fallbackCopyText(text) } -function downloadJsonFile(filename: string, payload: unknown): void { - const blob = new Blob([JSON.stringify(payload, null, 2)], { type: 'application/geo+json' }) +function downloadJsonFile(filename: string, payload: unknown, contentType = 'application/json'): void { + const blob = new Blob([JSON.stringify(payload, null, 2)], { type: contentType }) const url = URL.createObjectURL(blob) const link = document.createElement('a') link.href = url @@ -716,6 +716,8 @@ interface MapWorkspaceProps { onRunOrthophotoAnalysis: (bbox: VectorSelectionBBox) => Promise onSelectOrthophotoProduct: (productKey: string) => void onClearQualityEvidence?: () => void + onOpenAssistant: () => void + onOpenExports: () => void } export function MapWorkspace({ @@ -797,6 +799,8 @@ export function MapWorkspace({ onRunOrthophotoAnalysis, onSelectOrthophotoProduct, onClearQualityEvidence, + onOpenAssistant, + onOpenExports, }: MapWorkspaceProps): JSX.Element { const [advancedMode, setAdvancedMode] = useState(false) const [activeThemeId, setActiveThemeId] = useState(() => { @@ -1135,7 +1139,7 @@ export function MapWorkspace({ if (!selectedFeatureGeoJson) { return } - downloadJsonFile(selectedFeatureFilename, selectedFeatureGeoJson) + downloadJsonFile(selectedFeatureFilename, selectedFeatureGeoJson, 'application/geo+json') } const copySelectedMapFeatureProperties = () => { @@ -1192,22 +1196,46 @@ export function MapWorkspace({ if (!mapSelectionResult) { return } - downloadJsonFile(DEFAULT_AREA_SELECTION_FILENAME, mapSelectionResult.geojson) + downloadJsonFile(DEFAULT_AREA_SELECTION_FILENAME, mapSelectionResult.geojson, 'application/geo+json') } const copyAreaSelection = () => { copyText(JSON.stringify(mapSelectionResult?.geojson ?? { type: 'FeatureCollection', features: [] }, null, 2)) } - const downloadActiveThemeSelection = () => { + const downloadActiveThemeResult = () => { if (!activeSelectionResult) { return } - downloadJsonFile(`${activeTheme.id}-selection.geojson`, activeSelectionResult.geojson) + if (activeThemeDataset?.dataset_type === 'raster') { + downloadJsonFile(`${activeTheme.id}-analysis.json`, { + project_id: selectedProjectId, + area_id: selectedMapArea?.id ?? null, + area_name: selectedMapArea?.name ?? null, + theme: activeTheme, + dataset_id: activeThemeDataset.id, + dataset_name: activeThemeDataset.name, + source_name: activeThemeDataset.source_name, + result: activeSelectionResult, + }) + return + } + downloadJsonFile(`${activeTheme.id}-selection.geojson`, activeSelectionResult.geojson, 'application/geo+json') } - const copyActiveThemeSelection = () => { - copyText(JSON.stringify(activeSelectionResult?.geojson ?? { type: 'FeatureCollection', features: [] }, null, 2)) + const copyActiveThemeResult = () => { + copyText(JSON.stringify(activeSelectionResult ?? {}, null, 2)) + } + + const downloadTemporalComparison = () => { + if (!temporalComparison) { + return + } + downloadJsonFile(`${activeTheme.id}-evolution-${temporalComparison.earlier.observed_at.slice(0, 10)}-${temporalComparison.later.observed_at.slice(0, 10)}.json`, temporalComparison) + } + + const copyTemporalComparison = () => { + copyText(JSON.stringify(temporalComparison ?? {}, null, 2)) } const saveAreaSelectionExport = () => { @@ -1888,7 +1916,10 @@ export function MapWorkspace({ {theme.label} {dataset ? getDatasetSourceDisplayName(dataset) : 'Geen bron gekoppeld'} - {item ? resultMetricLabel(item.result) : dataset ? 'Niet bevraagd' : 'Bron ontbreekt'} + + {item ? resultMetricLabel(item.result) : dataset ? 'Niet bevraagd' : 'Bron ontbreekt'} + {item?.result.summary ? {item.result.summary.metric_label} : null} + ) })} @@ -1930,8 +1961,28 @@ export function MapWorkspace({ {analysisMode === 'current' ? (
- - + + +
+ ) : null} + + {analysisMode === 'evolution' && temporalComparison ? ( +
+ + +
+ ) : null} + + {activeSelectionResult || temporalComparison ? ( +
+ + Analyse klaar + Stel een vraag over dit gebied of open je bewaarde resultaten. + + +
) : null} diff --git a/frontend/src/styles/app.css b/frontend/src/styles/app.css index efcb7a03..a97ab48d 100644 --- a/frontend/src/styles/app.css +++ b/frontend/src/styles/app.css @@ -6459,6 +6459,17 @@ section { text-align: right; } +.geo-theme-result-value { + display: grid; + justify-items: end; + min-width: 8.5rem; + text-align: right; +} + +.geo-theme-result-value small { + max-width: 13rem; +} + .geo-data-notice { margin: 0; border-left: 3px solid #b17a31; @@ -6542,6 +6553,27 @@ section { margin-top: auto; } +.geo-result-next-actions { + display: grid; + grid-template-columns: minmax(0, 1fr) auto auto; + gap: 0.45rem; + align-items: center; + border-top: 1px solid #dfe7e3; + padding-top: 0.65rem; +} + +.geo-result-next-actions > span { + display: grid; + gap: 0.12rem; + min-width: 0; +} + +.geo-result-next-actions small { + color: #687570; + font-size: 0.66rem; + line-height: 1.35; +} + .geo-explorer-footer { display: flex; flex-wrap: wrap; @@ -6581,7 +6613,8 @@ section { .geo-results-panel > .geo-panel-heading, .geo-results-panel > .error, .geo-results-panel > .geo-data-notice, - .geo-results-panel > .geo-result-actions { + .geo-results-panel > .geo-result-actions, + .geo-results-panel > .geo-result-next-actions { grid-column: 1 / -1; } @@ -6661,7 +6694,8 @@ section { } .geo-map-actions, - .geo-result-actions { + .geo-result-actions, + .geo-result-next-actions { display: grid; grid-template-columns: 1fr; }