fix(runtime): serialize deploys and clarify regional sources
GeoIntel release gates / Compile, test, contracts and builds (push) Canceled after 0s
GeoIntel release gates / Python and npm vulnerability policy (push) Canceled after 0s
GeoIntel release gates / GIS image, SBOM and container scan (push) Canceled after 0s

This commit is contained in:
Codex
2026-07-22 02:36:33 +02:00
parent 0aff8e3b8c
commit 17d4442e60
7 changed files with 111 additions and 2 deletions
+8
View File
@@ -22,6 +22,14 @@
end-user selector. The Detection Lab now states the local Mol/Kempen
validation scope and explicitly warns that the model is not nationally
validated.
- Made national map source presentation zone-aware: broad Belgium work areas
now show a neutral official-per-region source contract instead of presenting
the last Brussels product as if it covered the country. The concrete source
is still resolved from the drawn selection before acquisition.
- Serialized release deployment and DockerMan container replacement with file
locks, and wait for asynchronous container removal to finish before starting
the replacement. This prevents concurrent repo deployments from leaving the
Unraid container in a half-removed state.
- Implemented real local segmentation inference: `YoloSegmentationAdapter` and
`SamSegmentationAdapter` (ultralytics interface) run over existing raster
@@ -36,6 +36,17 @@ def test_release_deploy_preserves_immutable_and_previous_images() -> None:
assert "Deployed immutable image" in script
def test_release_and_container_replacement_are_serialized() -> None:
release_script = (ROOT / "deploy" / "unraid" / "deploy-release.sh").read_text(encoding="utf-8")
run_script = (ROOT / "deploy" / "unraid" / "run-dockerman-container.sh").read_text(encoding="utf-8")
assert "GEOINTEL_DEPLOY_LOCK_FILE" in release_script
assert "flock -n 9" in release_script
assert "GEOINTEL_CONTAINER_LOCK_FILE" in run_script
assert "flock -w 300 8" in run_script
assert "GeoIntel container removal did not complete within 60 seconds" in run_script
def test_runtime_configuration_is_validated_before_container_replacement() -> None:
run_script = (ROOT / "deploy" / "unraid" / "run-dockerman-container.sh").read_text(encoding="utf-8")
@@ -66,6 +66,7 @@ def test_unraid_template_exposes_every_operator_owned_runtime_setting() -> None:
bridged_or_internal = {
"GEOINTEL_FRONTEND_PORT",
"GEOINTEL_BACKUPS_PATH",
"GEOINTEL_CONTAINER_LOCK_FILE",
"GEOINTEL_IMAGE",
"GEOINTEL_MODELS_PATH",
"GEOINTEL_POSTGIS_DATA_PATH",
+11
View File
@@ -4,6 +4,17 @@ set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
cd "$ROOT"
GEOINTEL_DEPLOY_LOCK_FILE="${GEOINTEL_DEPLOY_LOCK_FILE:-/tmp/geointel-release-deploy.lock}"
if ! command -v flock >/dev/null 2>&1; then
echo "GeoIntel release deployment requires flock to prevent concurrent container replacement." >&2
exit 2
fi
exec 9>"$GEOINTEL_DEPLOY_LOCK_FILE"
if ! flock -n 9; then
echo "Another GeoIntel release deployment is already running." >&2
exit 3
fi
if [ -f .env ]; then
set -a
# shellcheck disable=SC1091
+22 -1
View File
@@ -4,6 +4,17 @@ set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
cd "$ROOT"
GEOINTEL_CONTAINER_LOCK_FILE="${GEOINTEL_CONTAINER_LOCK_FILE:-/tmp/geointel-container-replacement.lock}"
if ! command -v flock >/dev/null 2>&1; then
echo "GeoIntel container replacement requires flock to prevent concurrent Docker mutations." >&2
exit 2
fi
exec 8>"$GEOINTEL_CONTAINER_LOCK_FILE"
if ! flock -w 300 8; then
echo "Timed out waiting for another GeoIntel container replacement to finish." >&2
exit 3
fi
if [ -f .env ]; then
set -a
# shellcheck disable=SC1091
@@ -188,7 +199,17 @@ install_dockerman_metadata
docker compose down --remove-orphans || true
if docker ps -a --format '{{.Names}}' | grep -qx geointel; then
docker rm -f geointel
docker rm -f geointel >/dev/null 2>&1 || true
for attempt in $(seq 1 60); do
if ! docker ps -a --format '{{.Names}}' | grep -qx geointel; then
break
fi
if [ "$attempt" -eq 60 ]; then
echo "GeoIntel container removal did not complete within 60 seconds." >&2
exit 1
fi
sleep 1
done
fi
mkdir -p "$GEOINTEL_STORAGE_PATH" "$GEOINTEL_MODELS_PATH" "$GEOINTEL_BACKUPS_PATH" "$GEOINTEL_POSTGIS_DATA_PATH"
+24
View File
@@ -11039,3 +11039,27 @@ Validated during implementation:
- the final repository readiness gate passed with 1,084 backend tests, 31
frontend unit tests, compile, typecheck, production build, Alembic head
`202607160001` and all script syntax/contract checks.
## 2026-07-22 - Live national deployment verification follow-up
Implemented after deploying the national-scope revision:
- reproduced and removed a national UI ambiguity where the Belgium land scope
displayed the final Brussels UrbIS catalog item as the active country-wide
source; multiple applicable regional products are now grouped as an official
source-per-region contract until the actual rectangle resolves its zones;
- added exclusive locks for release deployment and container replacement plus
a bounded wait for Docker's asynchronous removal, after an interrupted Codex
session exposed a concurrent replacement race on Tower.
Live evidence:
- immutable AI image `0aff8e3b8c551a9d1aa29a8495a17e5a858205ab-ai` became healthy on
port 1202 and applied Alembic migrations during startup;
- `scripts/live_migration_smoke.sh` passed against the embedded PostGIS 3.6
runtime with all required tables/indexes and single head `202607160001`;
- the production model catalog exposes exactly the configured approved local
model instead of 25 training artifacts;
- coverage resolution is zone-correct for Brussels, Wallonia and all three
Belgian maritime legal zones. Unimplemented WALOUS, Walloon raster and MDK
acquisition stays explicitly `not_configured`.
+34 -1
View File
@@ -96,6 +96,7 @@ interface OnDemandMapProduct extends MapThemeAcquisition {
availabilityLabel: string
attribution: string
limitationMessage: string
coverageZones: string[]
}
interface PlannedOnDemandMapProduct extends OnDemandMapProduct {
@@ -1051,6 +1052,7 @@ export function MapWorkspace({
availabilityLabel: `${product.native_resolution_m} m · ${product.observation_year} · automatisch bij selectie`,
attribution: product.attribution,
limitationMessage: product.limitation_message,
coverageZones: ['flanders'],
})
}
for (const product of officialMapProducts.grb) {
@@ -1062,6 +1064,7 @@ export function MapWorkspace({
availabilityLabel: 'officiële vectorbron · automatisch bij selectie',
attribution: product.attribution,
limitationMessage: product.limitation_message,
coverageZones: ['flanders'],
})
}
for (const source of officialMapProducts.bathymetry.filter(
@@ -1078,6 +1081,7 @@ export function MapWorkspace({
availabilityLabel: 'historische profielpunten · automatisch bij selectie',
attribution: source.attribution,
limitationMessage: source.limitation_message,
coverageZones: ['flanders'],
})
}
}
@@ -1092,6 +1096,7 @@ export function MapWorkspace({
availabilityLabel: `${product.observation_label} · officiële vectorbron · automatisch bij selectie`,
attribution: product.attribution,
limitationMessage: product.limitation_message,
coverageZones: product.coverage_zones,
})
}
const dhmvProduct = includesFlanders
@@ -1106,6 +1111,7 @@ export function MapWorkspace({
availabilityLabel: `${dhmvProduct.native_resolution_m} m · ${dhmvProduct.acquisition_period} · automatisch bij selectie`,
attribution: dhmvProduct.attribution,
limitationMessage: dhmvProduct.limitation_message,
coverageZones: ['flanders'],
})
}
const floodProduct = includesFlanders
@@ -1122,6 +1128,7 @@ export function MapWorkspace({
availabilityLabel: `${floodProduct.native_resolution_m} m · ${floodProduct.climate_context} · T${floodProduct.return_period_years} · automatisch bij selectie`,
attribution: floodProduct.attribution,
limitationMessage: floodProduct.limitation_message,
coverageZones: ['flanders'],
})
}
return result
@@ -1134,8 +1141,34 @@ export function MapWorkspace({
])
const onDemandProductMap = useMemo(() => {
const result = new Map<DataThemeId, OnDemandMapProduct>()
const productsByTheme = new Map<DataThemeId, OnDemandMapProduct[]>()
for (const product of onDemandProductsForZones(selectedCoverageZones)) {
result.set(product.theme, product)
productsByTheme.set(product.theme, [...(productsByTheme.get(product.theme) ?? []), product])
}
for (const [theme, products] of productsByTheme) {
if (products.length === 1) {
const product = products[0]
const scopeZones = product.coverageZones.filter((zone) => selectedCoverageZones?.includes(zone) ?? true)
result.set(theme, selectedCoverageZones && selectedCoverageZones.length > 1
? {
...product,
availabilityLabel: `${product.availabilityLabel} · alleen ${scopeZones.map(coverageZoneLabel).join(', ')}`,
}
: product)
continue
}
const coverageZones = Array.from(new Set(
products.flatMap((product) => product.coverageZones)
.filter((zone) => selectedCoverageZones?.includes(zone) ?? true),
))
result.set(theme, {
...products[0],
displayName: 'Officiële bron per regio',
availabilityLabel: `${coverageZones.map(coverageZoneLabel).join(', ')} · bron wordt na selectie bepaald`,
attribution: 'Officiële Belgische en gewestelijke databronnen',
limitationMessage: 'GeoIntel bepaalt na de getekende selectie welke regionale bron van toepassing is en voegt alleen semantisch gelijkwaardige resultaten samen.',
coverageZones,
})
}
return result
}, [onDemandProductsForZones, selectedCoverageZones])