diff --git a/.env.example b/.env.example index d80a6c69..95b5da12 100644 --- a/.env.example +++ b/.env.example @@ -20,3 +20,13 @@ OSM_OVERPASS_URL=https://overpass-api.de/api/interpreter VITE_API_BASE_URL= VITE_API_PROXY_TARGET=http://localhost:8000 VITE_MAP_STYLE_URL=https://demotiles.maplibre.org/style.json + +# Docker Compose / Unraid +GEOINTEL_FRONTEND_PORT=1202 +GEOINTEL_BACKEND_PORT=8000 +GEOINTEL_STORAGE_PATH=./storage +GEOINTEL_POSTGRES_DB=geointel +GEOINTEL_POSTGRES_USER=geointel +GEOINTEL_POSTGRES_PASSWORD=geointel +GEOINTEL_CORS_ORIGINS=http://localhost:1202,http://127.0.0.1:1202 +GEOINTEL_MAX_UPLOAD_MB=500 diff --git a/CHANGELOG.md b/CHANGELOG.md index d68bdc44..42e884ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,15 @@ # Changelog +## Sprint 31 Unraid deployment template (2026-06-17) + +- Made Docker Compose ports, storage path, PostGIS credentials, CORS origins and upload limit configurable through `.env` defaults. +- Added `deploy/unraid/geointel.env.example` for Unraid/Tower setup. +- Added `deploy/unraid/geointel-unraid-template.xml` documenting editable Unraid settings for the multi-container Compose stack. +- Added GeoIntel SVG icon assets for Unraid/template use and frontend favicon serving. +- Added regression coverage for Compose env defaults, Unraid template settings, README instructions and icon availability. +- No API contracts, backend behavior, migrations, product features, provider fetching or AI behavior were introduced. + ## Sprint 30 workbench component decomposition (2026-06-17) - Moved persisted QA/QC result rendering into `QualityResultsPanel`. diff --git a/README.md b/README.md index cd6ec5ee..595e26bb 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,35 @@ This is a documentation-driven engineering repo. The documentation is not decora make readiness ``` +## Unraid / Tower deployment + +GeoIntel runs on Unraid as a Docker Compose stack with PostGIS, backend and frontend services. The frontend is the browser entrypoint and proxies `/api` and `/health` to the backend. + +Unraid template assets live in: + +- `deploy/unraid/geointel.env.example` +- `deploy/unraid/geointel-unraid-template.xml` +- `deploy/unraid/geointel-icon.svg` + +Copy the Unraid env template to `.env` in the checkout and edit ports there: + +```bash +cd /mnt/user/appdata/geointel +cp deploy/unraid/geointel.env.example .env +nano .env +docker compose up -d --build +``` + +Common editable values: + +```env +GEOINTEL_FRONTEND_PORT=1202 +GEOINTEL_BACKEND_PORT=8000 +GEOINTEL_STORAGE_PATH=/mnt/user/appdata/geointel/storage +``` + +The database port is intentionally not exposed to the LAN. See `deploy/unraid/README.md` for full setup, port-change and cleanup notes. + ## Sprint 2 quick start - Update dependencies: diff --git a/backend/tests/test_docker_runtime_config.py b/backend/tests/test_docker_runtime_config.py index 8b91db91..dd93e643 100644 --- a/backend/tests/test_docker_runtime_config.py +++ b/backend/tests/test_docker_runtime_config.py @@ -41,15 +41,18 @@ def test_compose_does_not_require_missing_root_env_file() -> None: compose = (ROOT / "docker-compose.yml").read_text(encoding="utf-8") assert "env_file:" not in compose - assert "DATABASE_URL: postgresql+psycopg://geointel:geointel@db:5432/geointel" in compose + assert "DATABASE_URL: postgresql+psycopg://${GEOINTEL_POSTGRES_USER:-geointel}" in compose -def test_compose_exposes_frontend_on_host_port_1202_with_cors_origin() -> None: +def test_compose_exposes_frontend_on_configurable_host_port_with_cors_origin() -> None: compose = (ROOT / "docker-compose.yml").read_text(encoding="utf-8") env_example = (ROOT / ".env.example").read_text(encoding="utf-8") - assert '"1202:80"' in compose - assert "CORS_ORIGINS: http://localhost:1202,http://127.0.0.1:1202" in compose + assert '"${GEOINTEL_FRONTEND_PORT:-1202}:80"' in compose + assert '"${GEOINTEL_BACKEND_PORT:-8000}:8000"' in compose + assert "CORS_ORIGINS: ${GEOINTEL_CORS_ORIGINS:-http://localhost:1202,http://127.0.0.1:1202}" in compose + assert "GEOINTEL_FRONTEND_PORT=1202" in env_example + assert "GEOINTEL_BACKEND_PORT=8000" in env_example assert "http://localhost:1202" in env_example assert "http://127.0.0.1:1202" in env_example @@ -92,7 +95,7 @@ def test_compose_does_not_publish_postgis_on_default_host_port() -> None: def test_compose_waits_for_healthy_database_and_applies_migrations() -> None: compose = (ROOT / "docker-compose.yml").read_text(encoding="utf-8") - assert "pg_isready -U geointel -d geointel" in compose + assert "pg_isready -U ${GEOINTEL_POSTGRES_USER:-geointel} -d ${GEOINTEL_POSTGRES_DB:-geointel}" in compose assert "condition: service_healthy" in compose assert "sh /app/docker_start.sh" in compose diff --git a/backend/tests/test_sprint31_unraid_template.py b/backend/tests/test_sprint31_unraid_template.py new file mode 100644 index 00000000..85998e48 --- /dev/null +++ b/backend/tests/test_sprint31_unraid_template.py @@ -0,0 +1,63 @@ +from __future__ import annotations + +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[2] + + +def test_unraid_template_documents_editable_runtime_settings() -> None: + template = (ROOT / "deploy" / "unraid" / "geointel-unraid-template.xml").read_text(encoding="utf-8") + + assert "GeoIntel Kempen" in template + assert "GeoIntel is a multi-container Docker Compose stack" in template + assert "http://[IP]:[PORT:1202]/" in template + assert "http://[IP]:[PORT:1202]/geointel-icon.svg" in template + assert 'Target="GEOINTEL_FRONTEND_PORT"' in template + assert 'Target="GEOINTEL_BACKEND_PORT"' in template + assert 'Target="GEOINTEL_STORAGE_PATH"' in template + assert 'Target="GEOINTEL_POSTGRES_PASSWORD"' in template + assert 'Mask="true">change-me-before-shared-use' in template + + +def test_unraid_env_template_matches_compose_variables() -> None: + compose = (ROOT / "docker-compose.yml").read_text(encoding="utf-8") + env_template = (ROOT / "deploy" / "unraid" / "geointel.env.example").read_text(encoding="utf-8") + + for key in ( + "GEOINTEL_FRONTEND_PORT", + "GEOINTEL_BACKEND_PORT", + "GEOINTEL_STORAGE_PATH", + "GEOINTEL_POSTGRES_DB", + "GEOINTEL_POSTGRES_USER", + "GEOINTEL_POSTGRES_PASSWORD", + "GEOINTEL_CORS_ORIGINS", + "GEOINTEL_MAX_UPLOAD_MB", + ): + assert key in compose + assert f"{key}=" in env_template + + assert "GEOINTEL_FRONTEND_PORT=1202" in env_template + assert "GEOINTEL_STORAGE_PATH=/mnt/user/appdata/geointel/storage" in env_template + + +def test_unraid_readme_explains_port_changes_and_safe_cleanup() -> None: + readme = (ROOT / "deploy" / "unraid" / "README.md").read_text(encoding="utf-8") + + assert "cp deploy/unraid/geointel.env.example .env" in readme + assert "GEOINTEL_FRONTEND_PORT=1203" in readme + assert "docker compose up -d --build" in readme + assert "bash scripts/live_migration_smoke.sh" in readme + assert "docker builder prune -af" in readme + assert "Avoid broad volume pruning" in readme + + +def test_frontend_and_unraid_icon_assets_are_present() -> None: + deploy_icon = (ROOT / "deploy" / "unraid" / "geointel-icon.svg").read_text(encoding="utf-8") + frontend_icon = (ROOT / "frontend" / "public" / "geointel-icon.svg").read_text(encoding="utf-8") + index = (ROOT / "frontend" / "index.html").read_text(encoding="utf-8") + + assert "