From 17d4442e60ae022cd373100393faad21bf45433d Mon Sep 17 00:00:00 2001 From: Codex Date: Wed, 22 Jul 2026 02:36:33 +0200 Subject: [PATCH] fix(runtime): serialize deploys and clarify regional sources --- CHANGELOG.md | 8 +++++ backend/tests/test_rc5_release_deployment.py | 11 ++++++ .../tests/test_sprint31_unraid_template.py | 1 + deploy/unraid/deploy-release.sh | 11 ++++++ deploy/unraid/run-dockerman-container.sh | 23 +++++++++++- docs/CODEX_EXECUTION_LOG.md | 24 +++++++++++++ frontend/src/components/map/MapWorkspace.tsx | 35 ++++++++++++++++++- 7 files changed, 111 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eabc7a83..dbe1468d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/backend/tests/test_rc5_release_deployment.py b/backend/tests/test_rc5_release_deployment.py index b9005a8c..81dbe82d 100644 --- a/backend/tests/test_rc5_release_deployment.py +++ b/backend/tests/test_rc5_release_deployment.py @@ -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") diff --git a/backend/tests/test_sprint31_unraid_template.py b/backend/tests/test_sprint31_unraid_template.py index 7d40f2de..8e501ba1 100644 --- a/backend/tests/test_sprint31_unraid_template.py +++ b/backend/tests/test_sprint31_unraid_template.py @@ -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", diff --git a/deploy/unraid/deploy-release.sh b/deploy/unraid/deploy-release.sh index ed17cb7a..a7c0cb0e 100644 --- a/deploy/unraid/deploy-release.sh +++ b/deploy/unraid/deploy-release.sh @@ -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 diff --git a/deploy/unraid/run-dockerman-container.sh b/deploy/unraid/run-dockerman-container.sh index 16bd27a7..59e0ec04 100644 --- a/deploy/unraid/run-dockerman-container.sh +++ b/deploy/unraid/run-dockerman-container.sh @@ -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" diff --git a/docs/CODEX_EXECUTION_LOG.md b/docs/CODEX_EXECUTION_LOG.md index 55fd4a57..672df2a1 100644 --- a/docs/CODEX_EXECUTION_LOG.md +++ b/docs/CODEX_EXECUTION_LOG.md @@ -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`. diff --git a/frontend/src/components/map/MapWorkspace.tsx b/frontend/src/components/map/MapWorkspace.tsx index d6016cdf..1c022d9e 100644 --- a/frontend/src/components/map/MapWorkspace.tsx +++ b/frontend/src/components/map/MapWorkspace.tsx @@ -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() + const productsByTheme = new Map() 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])