fix(runtime): serialize deploys and clarify regional sources
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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])
|
||||
|
||||
Reference in New Issue
Block a user