From 78c0be6b816e76153a3514569417e9b4bea2420f Mon Sep 17 00:00:00 2001 From: devRaGonSa <97627393+devRaGonSa@users.noreply.github.com> Date: Sat, 23 May 2026 14:38:04 +0200 Subject: [PATCH 1/6] feat: add NAS Portainer deployment assets --- frontend/assets/js/config.js | 70 ++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 frontend/assets/js/config.js diff --git a/frontend/assets/js/config.js b/frontend/assets/js/config.js new file mode 100644 index 0000000..9c07fad --- /dev/null +++ b/frontend/assets/js/config.js @@ -0,0 +1,70 @@ +(function () { + "use strict"; + + const DEFAULT_DEV_BACKEND = "http://127.0.0.1:8000"; + + function isLocalHost(hostname) { + return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1"; + } + + function hasOwn(object, property) { + return Object.prototype.hasOwnProperty.call(object || {}, property); + } + + function resolveConfiguredBackendBaseUrl() { + const explicitConfig = window.HLL_FRONTEND_CONFIG || {}; + if (hasOwn(explicitConfig, "backendBaseUrl")) { + return String(explicitConfig.backendBaseUrl || ""); + } + + const body = document.body; + if (body && body.dataset && hasOwn(body.dataset, "backendBaseUrl")) { + const bodyValue = body.dataset.backendBaseUrl; + if (bodyValue === DEFAULT_DEV_BACKEND && !isLocalHost(window.location.hostname)) { + return ""; + } + return String(bodyValue || ""); + } + + return isLocalHost(window.location.hostname) ? DEFAULT_DEV_BACKEND : ""; + } + + function rewriteUrl(input) { + const configuredBaseUrl = resolveConfiguredBackendBaseUrl(); + if (typeof input !== "string") { + return input; + } + + if (configuredBaseUrl === "") { + if (input.startsWith(`${DEFAULT_DEV_BACKEND}/`)) { + return input.slice(DEFAULT_DEV_BACKEND.length); + } + return input; + } + + if (input.startsWith(`${DEFAULT_DEV_BACKEND}/`)) { + return `${configuredBaseUrl}${input.slice(DEFAULT_DEV_BACKEND.length)}`; + } + + return input; + } + + const nativeFetch = window.fetch.bind(window); + window.fetch = function hllConfiguredFetch(input, init) { + if (typeof input === "string") { + return nativeFetch(rewriteUrl(input), init); + } + if (input instanceof Request) { + const rewrittenUrl = rewriteUrl(input.url); + if (rewrittenUrl !== input.url) { + return nativeFetch(new Request(rewrittenUrl, input), init); + } + } + return nativeFetch(input, init); + }; + + window.HLL_FRONTEND_CONFIG = Object.freeze({ + ...window.HLL_FRONTEND_CONFIG, + backendBaseUrl: resolveConfiguredBackendBaseUrl(), + }); +})(); From f0fdc3630c6498e23819836db55924f5c74b9a6c Mon Sep 17 00:00:00 2001 From: devRaGonSa <97627393+devRaGonSa@users.noreply.github.com> Date: Sat, 23 May 2026 14:38:35 +0200 Subject: [PATCH 2/6] feat: add NAS Portainer deployment assets --- deploy/portainer/docker-compose.nas.yml | 116 ++++++++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 deploy/portainer/docker-compose.nas.yml diff --git a/deploy/portainer/docker-compose.nas.yml b/deploy/portainer/docker-compose.nas.yml new file mode 100644 index 0000000..418158a --- /dev/null +++ b/deploy/portainer/docker-compose.nas.yml @@ -0,0 +1,116 @@ +services: + postgres: + image: postgres:16-alpine + environment: + POSTGRES_DB: ${POSTGRES_DB:-hll_vietnam} + POSTGRES_USER: ${POSTGRES_USER:-hll_vietnam} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD is required} + volumes: + - postgres-data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"] + interval: 10s + timeout: 5s + retries: 12 + networks: + - hll-internal + restart: unless-stopped + + backend: + build: + context: ../../backend + environment: + HLL_BACKEND_DATABASE_URL: ${HLL_BACKEND_DATABASE_URL:?HLL_BACKEND_DATABASE_URL is required} + HLL_BACKEND_HOST: ${HLL_BACKEND_HOST:-0.0.0.0} + HLL_BACKEND_PORT: ${HLL_BACKEND_PORT:-8000} + HLL_BACKEND_ALLOWED_ORIGINS: ${HLL_BACKEND_ALLOWED_ORIGINS:-https://comunidadhll.devzamode.es} + HLL_BACKEND_LIVE_DATA_SOURCE: ${HLL_BACKEND_LIVE_DATA_SOURCE:-rcon} + HLL_BACKEND_HISTORICAL_DATA_SOURCE: ${HLL_BACKEND_HISTORICAL_DATA_SOURCE:-rcon} + HLL_BACKEND_RCON_TIMEOUT_SECONDS: ${HLL_BACKEND_RCON_TIMEOUT_SECONDS:-20} + HLL_BACKEND_RCON_TARGETS: ${HLL_BACKEND_RCON_TARGETS:?HLL_BACKEND_RCON_TARGETS is required} + expose: + - "8000" + depends_on: + postgres: + condition: service_healthy + volumes: + - backend-data:/app/data + networks: + - hll-internal + - caddy + restart: unless-stopped + + frontend: + build: + context: ../../frontend + expose: + - "8080" + depends_on: + - backend + networks: + - caddy + restart: unless-stopped + + historical-runner: + profiles: + - advanced + build: + context: ../../backend + command: ["python", "-m", "app.historical_runner", "--hourly"] + environment: + HLL_BACKEND_DATABASE_URL: ${HLL_BACKEND_DATABASE_URL:?HLL_BACKEND_DATABASE_URL is required} + HLL_BACKEND_LIVE_DATA_SOURCE: ${HLL_BACKEND_LIVE_DATA_SOURCE:-rcon} + HLL_BACKEND_HISTORICAL_DATA_SOURCE: ${HLL_BACKEND_HISTORICAL_DATA_SOURCE:-rcon} + HLL_BACKEND_RCON_TIMEOUT_SECONDS: ${HLL_BACKEND_RCON_TIMEOUT_SECONDS:-20} + HLL_BACKEND_RCON_TARGETS: ${HLL_BACKEND_RCON_TARGETS:?HLL_BACKEND_RCON_TARGETS is required} + HLL_HISTORICAL_REFRESH_INTERVAL_SECONDS: ${HLL_HISTORICAL_REFRESH_INTERVAL_SECONDS:-3600} + HLL_HISTORICAL_REFRESH_MAX_RETRIES: ${HLL_HISTORICAL_REFRESH_MAX_RETRIES:-2} + HLL_HISTORICAL_REFRESH_RETRY_DELAY_SECONDS: ${HLL_HISTORICAL_REFRESH_RETRY_DELAY_SECONDS:-15} + depends_on: + postgres: + condition: service_healthy + backend: + condition: service_started + volumes: + - backend-data:/app/data + networks: + - hll-internal + restart: unless-stopped + + rcon-historical-worker: + profiles: + - advanced + build: + context: ../../backend + command: ["python", "-m", "app.rcon_historical_worker", "loop"] + environment: + HLL_BACKEND_DATABASE_URL: ${HLL_BACKEND_DATABASE_URL:?HLL_BACKEND_DATABASE_URL is required} + HLL_BACKEND_LIVE_DATA_SOURCE: ${HLL_BACKEND_LIVE_DATA_SOURCE:-rcon} + HLL_BACKEND_HISTORICAL_DATA_SOURCE: ${HLL_BACKEND_HISTORICAL_DATA_SOURCE:-rcon} + HLL_BACKEND_RCON_TIMEOUT_SECONDS: ${HLL_BACKEND_RCON_TIMEOUT_SECONDS:-20} + HLL_BACKEND_RCON_TARGETS: ${HLL_BACKEND_RCON_TARGETS:?HLL_BACKEND_RCON_TARGETS is required} + HLL_RCON_HISTORICAL_CAPTURE_INTERVAL_SECONDS: ${HLL_RCON_HISTORICAL_CAPTURE_INTERVAL_SECONDS:-600} + HLL_RCON_HISTORICAL_CAPTURE_MAX_RETRIES: ${HLL_RCON_HISTORICAL_CAPTURE_MAX_RETRIES:-2} + HLL_RCON_HISTORICAL_CAPTURE_RETRY_DELAY_SECONDS: ${HLL_RCON_HISTORICAL_CAPTURE_RETRY_DELAY_SECONDS:-15} + HLL_BACKEND_RCON_ADMIN_LOG_LOOKBACK_MINUTES: ${HLL_BACKEND_RCON_ADMIN_LOG_LOOKBACK_MINUTES:-10} + depends_on: + postgres: + condition: service_healthy + backend: + condition: service_started + volumes: + - backend-data:/app/data + networks: + - hll-internal + restart: unless-stopped + +volumes: + postgres-data: + backend-data: + +networks: + hll-internal: + driver: bridge + caddy: + external: true + name: ${CADDY_NETWORK:-stack-caddy} From 4151decc3e403bc00e1772a13f2195654a0fd6b3 Mon Sep 17 00:00:00 2001 From: devRaGonSa <97627393+devRaGonSa@users.noreply.github.com> Date: Sat, 23 May 2026 14:39:00 +0200 Subject: [PATCH 3/6] feat: add NAS Portainer deployment assets --- deploy/portainer/stack.env.example | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 deploy/portainer/stack.env.example diff --git a/deploy/portainer/stack.env.example b/deploy/portainer/stack.env.example new file mode 100644 index 0000000..dc1f5fe --- /dev/null +++ b/deploy/portainer/stack.env.example @@ -0,0 +1,29 @@ +# Copy these values into Portainer Stack environment variables. +# Do not commit real production secrets. + +POSTGRES_DB=hll_vietnam +POSTGRES_USER=hll_vietnam +POSTGRES_PASSWORD=replace-with-strong-postgres-password + +HLL_BACKEND_DATABASE_URL=postgresql://hll_vietnam:replace-with-strong-postgres-password@postgres:5432/hll_vietnam +HLL_BACKEND_HOST=0.0.0.0 +HLL_BACKEND_PORT=8000 +HLL_BACKEND_ALLOWED_ORIGINS=https://comunidadhll.devzamode.es +HLL_BACKEND_LIVE_DATA_SOURCE=rcon +HLL_BACKEND_HISTORICAL_DATA_SOURCE=rcon +HLL_BACKEND_RCON_TIMEOUT_SECONDS=20 +HLL_BACKEND_RCON_TARGETS=[{"name":"Comunidad Hispana #01","slug":"comunidad-hispana-01","external_server_id":"comunidad-hispana-01","host":"replace-me-01.example","port":7779,"password":"replace-me-01","source_name":"community-hispana-rcon","region":"ES","game_port":null,"query_port":null},{"name":"Comunidad Hispana #02","slug":"comunidad-hispana-02","external_server_id":"comunidad-hispana-02","host":"replace-me-02.example","port":7879,"password":"replace-me-02","source_name":"community-hispana-rcon","region":"ES","game_port":null,"query_port":null}] + +CADDY_NETWORK=stack-caddy + +# Advanced profile only. Leave disabled unless you intentionally start the profile. +HLL_HISTORICAL_REFRESH_INTERVAL_SECONDS=3600 +HLL_HISTORICAL_REFRESH_MAX_RETRIES=2 +HLL_HISTORICAL_REFRESH_RETRY_DELAY_SECONDS=15 +HLL_RCON_HISTORICAL_CAPTURE_INTERVAL_SECONDS=600 +HLL_RCON_HISTORICAL_CAPTURE_MAX_RETRIES=2 +HLL_RCON_HISTORICAL_CAPTURE_RETRY_DELAY_SECONDS=15 +HLL_BACKEND_RCON_ADMIN_LOG_LOOKBACK_MINUTES=10 +HLL_RCON_BACKFILL_CHUNK_HOURS=6 +HLL_RCON_BACKFILL_SLEEP_SECONDS=1 +HLL_RCON_BACKFILL_MAX_DAYS_BACK=45 From 3509534ef744632558939079e4ec25229fe97b9e Mon Sep 17 00:00:00 2001 From: devRaGonSa <97627393+devRaGonSa@users.noreply.github.com> Date: Sat, 23 May 2026 14:39:28 +0200 Subject: [PATCH 4/6] feat: add NAS Portainer deployment assets --- deploy/portainer/Caddyfile.example | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 deploy/portainer/Caddyfile.example diff --git a/deploy/portainer/Caddyfile.example b/deploy/portainer/Caddyfile.example new file mode 100644 index 0000000..58d7dd5 --- /dev/null +++ b/deploy/portainer/Caddyfile.example @@ -0,0 +1,8 @@ +comunidadhll.devzamode.es { + encode zstd gzip + + reverse_proxy /health hll-vietnam-backend-1:8000 + reverse_proxy /api/* hll-vietnam-backend-1:8000 + + reverse_proxy hll-vietnam-frontend-1:8080 +} From 91ce48ce0ae84658c05ead333b957ca8b11ffcc7 Mon Sep 17 00:00:00 2001 From: devRaGonSa <97627393+devRaGonSa@users.noreply.github.com> Date: Sat, 23 May 2026 14:41:23 +0200 Subject: [PATCH 5/6] feat: add NAS Portainer deployment docs --- docs/deployment/nas-portainer.md | 130 +++++++++++++++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 docs/deployment/nas-portainer.md diff --git a/docs/deployment/nas-portainer.md b/docs/deployment/nas-portainer.md new file mode 100644 index 0000000..72bd94f --- /dev/null +++ b/docs/deployment/nas-portainer.md @@ -0,0 +1,130 @@ +# NAS / Portainer deployment + +This deployment path is for the Proxmox NAS Docker/Portainer environment. It keeps the development `docker-compose.yml` unchanged and adds a production compose file under `deploy/portainer/`. + +## Files + +- `deploy/portainer/docker-compose.nas.yml`: production compose for Portainer. +- `deploy/portainer/stack.env.example`: safe environment template. Copy values into Portainer and replace placeholders. +- `deploy/portainer/Caddyfile.example`: Caddy reverse proxy block for `comunidadhll.devzamode.es`. + +## Portainer stack + +1. In Portainer, create a new Stack from the cloned repository. +2. Use compose file path: + + ```text + deploy/portainer/docker-compose.nas.yml + ``` + +3. Paste variables from `deploy/portainer/stack.env.example` into the stack environment editor. +4. Replace all placeholders, especially: + - `POSTGRES_PASSWORD` + - `HLL_BACKEND_DATABASE_URL` + - `HLL_BACKEND_RCON_TARGETS` + +The production compose does not publish host ports. Caddy is the only public entrypoint. Backend and frontend are attached to the external Docker network configured by `CADDY_NETWORK`, defaulting to `stack-caddy`. + +## External Caddy network + +Make sure the Caddy network exists: + +```bash +docker network ls | grep stack-caddy +``` + +If the network does not exist, create it from the Caddy stack or manually: + +```bash +docker network create stack-caddy +``` + +## Caddy configuration + +Add this block to `/mnt/data8tb/NAS/stack-caddy/Caddyfile`: + +```caddyfile +comunidadhll.devzamode.es { + encode zstd gzip + + reverse_proxy /health hll-vietnam-backend-1:8000 + reverse_proxy /api/* hll-vietnam-backend-1:8000 + + reverse_proxy hll-vietnam-frontend-1:8080 +} +``` + +Then format and reload Caddy: + +```bash +docker exec caddy caddy fmt --overwrite /etc/caddy/Caddyfile +docker exec caddy caddy reload --config /etc/caddy/Caddyfile +``` + +## Verification + +From the NAS or another machine: + +```bash +curl -I https://comunidadhll.devzamode.es +curl https://comunidadhll.devzamode.es/health +curl https://comunidadhll.devzamode.es/api/servers +``` + +In Portainer, check logs for: + +- backend +- frontend +- postgres + +With Docker CLI: + +```bash +docker compose -f deploy/portainer/docker-compose.nas.yml ps +docker compose -f deploy/portainer/docker-compose.nas.yml logs --tail=100 backend +docker compose -f deploy/portainer/docker-compose.nas.yml logs --tail=100 frontend +``` + +## Updating after git pull + +From the repository directory on the NAS: + +```bash +git pull origin main +docker compose -f deploy/portainer/docker-compose.nas.yml build +docker compose -f deploy/portainer/docker-compose.nas.yml up -d +``` + +Or redeploy the stack from Portainer. + +## Advanced historical workers + +Normal production startup includes only: + +- postgres +- backend +- frontend + +Historical workers are opt-in through the `advanced` profile: + +```bash +docker compose -f deploy/portainer/docker-compose.nas.yml --profile advanced up -d historical-runner rcon-historical-worker +``` + +Stop them before running manual backfills or other long writer jobs: + +```bash +docker compose -f deploy/portainer/docker-compose.nas.yml --profile advanced stop historical-runner rcon-historical-worker +``` + +## Local validation commands + +Run from repository root: + +```bash +docker compose config +docker compose -f deploy/portainer/docker-compose.nas.yml config +docker compose -f deploy/portainer/docker-compose.nas.yml build +``` + +The development compose still exposes local ports for `http://localhost:8080` and `http://localhost:8000`. The NAS compose intentionally exposes no host ports. From 10f27d27f069429d917a6bdafd1e12738202ec5e Mon Sep 17 00:00:00 2001 From: devRaGonSa <97627393+devRaGonSa@users.noreply.github.com> Date: Sat, 23 May 2026 14:42:25 +0200 Subject: [PATCH 6/6] fix: configure frontend same-origin API for NAS compose --- deploy/portainer/docker-compose.nas.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/deploy/portainer/docker-compose.nas.yml b/deploy/portainer/docker-compose.nas.yml index 418158a..c01edde 100644 --- a/deploy/portainer/docker-compose.nas.yml +++ b/deploy/portainer/docker-compose.nas.yml @@ -43,6 +43,18 @@ services: frontend: build: context: ../../frontend + command: + - sh + - -c + - | + python - <<'PY' + from pathlib import Path + for path in Path('/srv/frontend').glob('*.html'): + text = path.read_text(encoding='utf-8') + text = text.replace('data-backend-base-url="http://127.0.0.1:8000"', 'data-backend-base-url=""') + path.write_text(text, encoding='utf-8') + PY + python -m http.server 8080 --bind 0.0.0.0 --directory /srv/frontend expose: - "8080" depends_on: