From 9cad8d4af6b7629dc7d9a06a5e4e71935f449328 Mon Sep 17 00:00:00 2001 From: Codex Date: Sun, 5 Jul 2026 00:53:21 +0200 Subject: [PATCH] Set writable YOLO config directory --- .env.example | 1 + CHANGELOG.md | 1 + backend/README.md | 4 ++++ backend/tests/test_docker_runtime_config.py | 17 +++++++++++++++++ deploy/unraid/README.md | 2 ++ deploy/unraid/all-in-one-start.sh | 3 ++- deploy/unraid/geointel.env.example | 1 + deploy/unraid/run-dockerman-container.sh | 2 ++ docker-compose.unraid.yml | 1 + docker-compose.yml | 1 + docs/CODEX_EXECUTION_LOG.md | 3 +++ 11 files changed, 35 insertions(+), 1 deletion(-) diff --git a/.env.example b/.env.example index b86fd5c4..c3b6f264 100644 --- a/.env.example +++ b/.env.example @@ -10,6 +10,7 @@ YOLO_MODEL_PATH= YOLO_MODEL_ID=yolo-configured YOLO_MODEL_DISPLAY_NAME=Configured YOLO detector YOLO_MODEL_VERSION= +YOLO_CONFIG_DIR=./storage/ultralytics YOLO_DEVICE=cpu YOLO_IMAGE_SIZE=640 YOLO_MAX_TILES=100 diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d5a97ff..e702dd7a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ - Added a full-workflow run mode selector so repeated Map QA/QC runs can reuse the latest saved derived dataset instead of creating duplicate dataset/export artifacts. - Added an opt-in Docker/Unraid AI build path (`GEOINTEL_INSTALL_AI=true`) for installing optional PyTorch/Ultralytics dependencies while keeping the default GIS runtime lightweight and import-safe. - Hardened the AI Docker runtime with OpenCV native libraries required by Ultralytics and made YOLO dependency detection use real imports instead of optimistic module discovery. +- Added a writable `YOLO_CONFIG_DIR` default under application storage so Ultralytics does not fall back to root user config paths in Docker/Unraid. - Added static regression coverage for the road basemap, attribution, basemap policy notice, database layer selector and persisted operational GIS workflow wiring. ## Sprint 115 QA/QC and Exports usability layout pass (2026-07-04) diff --git a/backend/README.md b/backend/README.md index 287f4dad..4dd1ef23 100644 --- a/backend/README.md +++ b/backend/README.md @@ -231,6 +231,9 @@ AI-enabled Docker images include the native OpenCV runtime libraries required by Ultralytics. Dependency availability is checked with real `torch` and `ultralytics` imports, so missing shared libraries are reported as `dependency_unavailable` instead of being treated as configured. +Docker/Unraid runtimes set `YOLO_CONFIG_DIR` to a writable storage path so +Ultralytics does not attempt to write settings under the root user config +directory. Configured YOLO requires: @@ -269,6 +272,7 @@ Optional tuning: YOLO_MODEL_ID=yolo-configured YOLO_MODEL_DISPLAY_NAME="Configured YOLO detector" YOLO_MODEL_VERSION=local-v1 +YOLO_CONFIG_DIR=/app/storage/ultralytics YOLO_DEVICE=cpu YOLO_IMAGE_SIZE=640 YOLO_MAX_TILES=100 diff --git a/backend/tests/test_docker_runtime_config.py b/backend/tests/test_docker_runtime_config.py index 451b57ce..7c252c6c 100644 --- a/backend/tests/test_docker_runtime_config.py +++ b/backend/tests/test_docker_runtime_config.py @@ -81,6 +81,7 @@ def test_env_example_uses_runtime_env_names_read_by_backend_and_frontend() -> No assert "GEOINTEL_INSTALL_AI=false" in env_example assert "YOLO_ENABLED=false" in env_example assert "YOLO_MODEL_PATH=" in env_example + assert "YOLO_CONFIG_DIR=./storage/ultralytics" in env_example assert "YOLO_MAX_TILES=100" in env_example assert "ENABLE_YOLO" not in env_example assert "ENABLE_SAM" not in env_example @@ -154,6 +155,22 @@ def test_backend_docker_start_script_waits_for_sql_connection_before_migrations( assert "uvicorn app.main:app --host 0.0.0.0 --port 8000" in script +def test_runtime_sets_writable_ultralytics_config_directory() -> None: + compose = (ROOT / "docker-compose.yml").read_text(encoding="utf-8") + unraid_compose = (ROOT / "docker-compose.unraid.yml").read_text(encoding="utf-8") + start_script = (ROOT / "deploy" / "unraid" / "all-in-one-start.sh").read_text(encoding="utf-8") + run_script = (ROOT / "deploy" / "unraid" / "run-dockerman-container.sh").read_text(encoding="utf-8") + unraid_env = (ROOT / "deploy" / "unraid" / "geointel.env.example").read_text(encoding="utf-8") + + assert "YOLO_CONFIG_DIR: ${YOLO_CONFIG_DIR:-/app/storage/ultralytics}" in compose + assert "YOLO_CONFIG_DIR: ${YOLO_CONFIG_DIR:-/app/storage/ultralytics}" in unraid_compose + assert 'export YOLO_CONFIG_DIR="${YOLO_CONFIG_DIR:-$STORAGE_ROOT/ultralytics}"' in start_script + assert 'mkdir -p "$PGDATA" "$STORAGE_ROOT" "$YOLO_CONFIG_DIR"' in start_script + assert 'YOLO_CONFIG_DIR="${YOLO_CONFIG_DIR:-/app/storage/ultralytics}"' in run_script + assert '-e YOLO_CONFIG_DIR="$YOLO_CONFIG_DIR"' in run_script + assert "YOLO_CONFIG_DIR=/app/storage/ultralytics" in unraid_env + + def test_docker_build_contexts_exclude_vendor_build_and_cache_outputs() -> None: required_patterns = { "node_modules", diff --git a/deploy/unraid/README.md b/deploy/unraid/README.md index c2bbd96d..556e1555 100644 --- a/deploy/unraid/README.md +++ b/deploy/unraid/README.md @@ -88,6 +88,8 @@ GIS-only image. Set `GEOINTEL_INSTALL_AI=true`, mount models through `YOLO_MODEL_PATH=/app/models/.pt` only when you have a local model file. The AI-enabled image installs PyTorch/Ultralytics plus the native OpenCV runtime libraries needed for Ultralytics imports; it still never downloads model weights. +`YOLO_CONFIG_DIR` defaults to `/app/storage/ultralytics`, a writable persistent +path, so Ultralytics settings do not fall back to root user config directories. Validate: diff --git a/deploy/unraid/all-in-one-start.sh b/deploy/unraid/all-in-one-start.sh index adb7f35d..090f9300 100644 --- a/deploy/unraid/all-in-one-start.sh +++ b/deploy/unraid/all-in-one-start.sh @@ -9,8 +9,9 @@ export STORAGE_ROOT="${STORAGE_ROOT:-${GEOINTEL_STORAGE_ROOT:-/app/storage}}" export DATABASE_URL="${DATABASE_URL:-postgresql+psycopg://${POSTGRES_USER}:${POSTGRES_PASSWORD}@127.0.0.1:5432/${POSTGRES_DB}}" export CORS_ORIGINS="${GEOINTEL_CORS_ORIGINS:-${CORS_ORIGINS:-http://localhost:1202,http://127.0.0.1:1202}}" export MAX_UPLOAD_MB="${GEOINTEL_MAX_UPLOAD_MB:-${MAX_UPLOAD_MB:-500}}" +export YOLO_CONFIG_DIR="${YOLO_CONFIG_DIR:-$STORAGE_ROOT/ultralytics}" -mkdir -p "$PGDATA" "$STORAGE_ROOT" /run/nginx /var/log/nginx +mkdir -p "$PGDATA" "$STORAGE_ROOT" "$YOLO_CONFIG_DIR" /run/nginx /var/log/nginx chown -R postgres:postgres "$PGDATA" postgres_pid="" diff --git a/deploy/unraid/geointel.env.example b/deploy/unraid/geointel.env.example index c09ea416..d6c12774 100644 --- a/deploy/unraid/geointel.env.example +++ b/deploy/unraid/geointel.env.example @@ -31,6 +31,7 @@ YOLO_MODEL_PATH= YOLO_MODEL_ID=yolo-configured YOLO_MODEL_DISPLAY_NAME=Configured YOLO detector YOLO_MODEL_VERSION= +YOLO_CONFIG_DIR=/app/storage/ultralytics YOLO_DEVICE=cpu YOLO_IMAGE_SIZE=640 YOLO_MAX_TILES=100 diff --git a/deploy/unraid/run-dockerman-container.sh b/deploy/unraid/run-dockerman-container.sh index 68f179c5..ac3ff243 100644 --- a/deploy/unraid/run-dockerman-container.sh +++ b/deploy/unraid/run-dockerman-container.sh @@ -25,6 +25,7 @@ YOLO_MODEL_PATH="${YOLO_MODEL_PATH:-}" YOLO_MODEL_ID="${YOLO_MODEL_ID:-yolo-configured}" YOLO_MODEL_DISPLAY_NAME="${YOLO_MODEL_DISPLAY_NAME:-Configured YOLO detector}" YOLO_MODEL_VERSION="${YOLO_MODEL_VERSION:-}" +YOLO_CONFIG_DIR="${YOLO_CONFIG_DIR:-/app/storage/ultralytics}" YOLO_DEVICE="${YOLO_DEVICE:-cpu}" YOLO_IMAGE_SIZE="${YOLO_IMAGE_SIZE:-640}" YOLO_MAX_TILES="${YOLO_MAX_TILES:-100}" @@ -83,6 +84,7 @@ docker run -d \ -e YOLO_MODEL_ID="$YOLO_MODEL_ID" \ -e YOLO_MODEL_DISPLAY_NAME="$YOLO_MODEL_DISPLAY_NAME" \ -e YOLO_MODEL_VERSION="$YOLO_MODEL_VERSION" \ + -e YOLO_CONFIG_DIR="$YOLO_CONFIG_DIR" \ -e YOLO_DEVICE="$YOLO_DEVICE" \ -e YOLO_IMAGE_SIZE="$YOLO_IMAGE_SIZE" \ -e YOLO_MAX_TILES="$YOLO_MAX_TILES" \ diff --git a/docker-compose.unraid.yml b/docker-compose.unraid.yml index b0d8c31c..cfa42ad9 100644 --- a/docker-compose.unraid.yml +++ b/docker-compose.unraid.yml @@ -23,6 +23,7 @@ services: YOLO_MODEL_ID: ${YOLO_MODEL_ID:-yolo-configured} YOLO_MODEL_DISPLAY_NAME: ${YOLO_MODEL_DISPLAY_NAME:-Configured YOLO detector} YOLO_MODEL_VERSION: ${YOLO_MODEL_VERSION:-} + YOLO_CONFIG_DIR: ${YOLO_CONFIG_DIR:-/app/storage/ultralytics} YOLO_DEVICE: ${YOLO_DEVICE:-cpu} YOLO_IMAGE_SIZE: ${YOLO_IMAGE_SIZE:-640} YOLO_MAX_TILES: ${YOLO_MAX_TILES:-100} diff --git a/docker-compose.yml b/docker-compose.yml index 4231bf96..7d457113 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -28,6 +28,7 @@ services: YOLO_MODEL_ID: ${YOLO_MODEL_ID:-yolo-configured} YOLO_MODEL_DISPLAY_NAME: ${YOLO_MODEL_DISPLAY_NAME:-Configured YOLO detector} YOLO_MODEL_VERSION: ${YOLO_MODEL_VERSION:-} + YOLO_CONFIG_DIR: ${YOLO_CONFIG_DIR:-/app/storage/ultralytics} YOLO_DEVICE: ${YOLO_DEVICE:-cpu} YOLO_IMAGE_SIZE: ${YOLO_IMAGE_SIZE:-640} YOLO_MAX_TILES: ${YOLO_MAX_TILES:-100} diff --git a/docs/CODEX_EXECUTION_LOG.md b/docs/CODEX_EXECUTION_LOG.md index 2e47af25..5a7c0fc3 100644 --- a/docs/CODEX_EXECUTION_LOG.md +++ b/docs/CODEX_EXECUTION_LOG.md @@ -6,6 +6,7 @@ Changed: - Added opt-in Docker and Unraid AI build support through `GEOINTEL_INSTALL_AI=true`; default builds still install only the GIS runtime. - Passed YOLO runtime environment variables and a `/app/models` volume into the all-in-one Unraid container so local PyTorch/Ultralytics models can be mounted explicitly. - Hardened the AI image path after Tower validation showed `torch` imported but `ultralytics` failed on a missing OpenCV native library. The Dockerfiles now include the required OpenCV runtime shared libraries and YOLO dependency detection performs real imports instead of `find_spec` checks. +- Added a writable `YOLO_CONFIG_DIR` default under application storage after Tower validation showed Ultralytics otherwise falls back to `/tmp` because root config is not writable in the container. - Updated `.env.example`, `backend/README.md`, `frontend/README.md`, `scripts/README.md`, `docs/AI_PIPELINES.md`, `docs/TODO.md` and `CHANGELOG.md`. - Added regression coverage in `backend/tests/test_sprint116_operational_gis_map_workflow.py`, `backend/tests/test_sprint8b_yolo_foundation.py` and `backend/tests/test_docker_runtime_config.py`. @@ -19,6 +20,8 @@ Validation: - `python -m pytest backend\tests\test_sprint31_unraid_template.py backend\tests\test_docker_runtime_config.py -q` passed: 27 tests. - RED: `python -m pytest backend\tests\test_sprint8b_yolo_foundation.py backend\tests\test_docker_runtime_config.py -q` failed before the runtime hardening because YOLO dependency detection still used `find_spec` and the Dockerfiles lacked OpenCV native runtime libraries. - `python -m pytest backend\tests\test_sprint8b_yolo_foundation.py backend\tests\test_docker_runtime_config.py -q` passed: 30 tests. +- RED: `python -m pytest backend\tests\test_docker_runtime_config.py -q` failed before `YOLO_CONFIG_DIR` wiring because the Compose, Unraid and startup paths did not define a writable Ultralytics config directory. +- `python -m pytest backend\tests\test_docker_runtime_config.py -q` passed: 21 tests. - `python -m compileall backend/app` passed after the AI runtime hardening. - `cd backend && python -m pytest -q` passed: 367 tests. - `cd frontend && npm run typecheck` passed.