Complete the V1 analysis handoff
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1079,6 +1079,8 @@ function App(): JSX.Element {
|
||||
onRunOrthophotoAnalysis={mapOrthophotoAnalysis.run}
|
||||
onSelectOrthophotoProduct={mapOrthophotoAnalysis.setSelectedProductKey}
|
||||
onClearQualityEvidence={clearQualityEvidenceGeoJson}
|
||||
onOpenAssistant={() => setActiveWorkspace('assistant')}
|
||||
onOpenExports={() => setActiveWorkspace('exports')}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
|
||||
@@ -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<boolean>
|
||||
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<DataThemeId>(() => {
|
||||
@@ -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({
|
||||
<strong>{theme.label}</strong>
|
||||
<small>{dataset ? getDatasetSourceDisplayName(dataset) : 'Geen bron gekoppeld'}</small>
|
||||
</span>
|
||||
<span className="geo-theme-result-value">
|
||||
<b>{item ? resultMetricLabel(item.result) : dataset ? 'Niet bevraagd' : 'Bron ontbreekt'}</b>
|
||||
{item?.result.summary ? <small>{item.result.summary.metric_label}</small> : null}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
@@ -1930,8 +1961,28 @@ export function MapWorkspace({
|
||||
|
||||
{analysisMode === 'current' ? (
|
||||
<div className="geo-result-actions">
|
||||
<button className="secondary-action" disabled={!activeSelectionResult || activeThemeDataset?.dataset_type === 'raster'} type="button" onClick={downloadActiveThemeSelection}>Download GeoJSON</button>
|
||||
<button className="secondary-action" disabled={!activeSelectionResult} type="button" onClick={copyActiveThemeSelection}>Kopieer gegevens</button>
|
||||
<button className="secondary-action" disabled={!activeSelectionResult} type="button" onClick={downloadActiveThemeResult}>
|
||||
{activeThemeDataset?.dataset_type === 'raster' ? 'Download analyse' : 'Download GeoJSON'}
|
||||
</button>
|
||||
<button className="secondary-action" disabled={!activeSelectionResult} type="button" onClick={copyActiveThemeResult}>Kopieer gegevens</button>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{analysisMode === 'evolution' && temporalComparison ? (
|
||||
<div className="geo-result-actions">
|
||||
<button className="secondary-action" type="button" onClick={downloadTemporalComparison}>Download vergelijking</button>
|
||||
<button className="secondary-action" type="button" onClick={copyTemporalComparison}>Kopieer vergelijking</button>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{activeSelectionResult || temporalComparison ? (
|
||||
<div className="geo-result-next-actions" aria-label="Volgende stap">
|
||||
<span>
|
||||
<strong>Analyse klaar</strong>
|
||||
<small>Stel een vraag over dit gebied of open je bewaarde resultaten.</small>
|
||||
</span>
|
||||
<button className="primary-action" type="button" onClick={onOpenAssistant}>Stel AI-vraag</button>
|
||||
<button className="secondary-action" type="button" onClick={onOpenExports}>Open downloads</button>
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user