#!/usr/bin/env bash set -euo pipefail META_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" ROOT="${GOVOPLAN_CORE_ROOT:-$META_ROOT/../govoplan-core}" ROOT="$(cd "$ROOT" && pwd)" PROFILE_ROOT="$META_ROOT/dev/production-like" PROFILE_ENV="${GOVOPLAN_PRODUCTION_LIKE_ENV:-$PROFILE_ROOT/.env}" if [ ! -f "$PROFILE_ENV" ]; then PROFILE_ENV="$PROFILE_ROOT/.env.example" fi WEBUI_ROOT="$ROOT/webui" VENV_ROOT="${GOVOPLAN_VENV_ROOT:-$META_ROOT/.venv}" PYTHON="${PYTHON:-$VENV_ROOT/bin/python}" NODE_BIN="${NODE_BIN:-/home/zemion/.nvm/versions/node/v22.22.3/bin}" NPM="${NPM:-$NODE_BIN/npm}" DOCKER="${DOCKER:-docker}" BACKEND_HOST="${GOVOPLAN_BACKEND_HOST:-127.0.0.1}" BACKEND_PORT="${GOVOPLAN_BACKEND_PORT:-8000}" FRONTEND_HOST="${GOVOPLAN_FRONTEND_HOST:-127.0.0.1}" FRONTEND_PORT="${GOVOPLAN_FRONTEND_PORT:-5173}" OPEN_BROWSER="${OPEN_BROWSER:-1}" STOP_DEPENDENCIES_ON_EXIT="${GOVOPLAN_STOP_PROFILE_DEPENDENCIES_ON_EXIT:-0}" LOG_DIR="$META_ROOT/runtime/production-like/logs" BACKEND_LOG="$LOG_DIR/backend.log" WORKER_LOG="$LOG_DIR/worker.log" FRONTEND_LOG="$LOG_DIR/frontend.log" BACKEND_URL="http://$BACKEND_HOST:$BACKEND_PORT" FRONTEND_URL="http://$FRONTEND_HOST:$FRONTEND_PORT" backend_pid="" worker_pid="" frontend_pid="" fail() { printf 'launch-production-like-dev: %s\n' "$*" >&2 exit 1 } [ -f "$PROFILE_ENV" ] || fail "profile env file not found: $PROFILE_ENV" set -a # shellcheck disable=SC1090 . "$PROFILE_ENV" set +a export APP_ENV="${APP_ENV:-staging}" export GOVOPLAN_INSTALL_PROFILE="${GOVOPLAN_INSTALL_PROFILE:-production-like}" export ENABLED_MODULES="${ENABLED_MODULES:-tenancy,organizations,identity,access,admin,dashboard,policy,audit,campaigns,files,mail,calendar,poll,scheduling,notifications,docs,ops}" export DATABASE_URL="${DATABASE_URL:-${GOVOPLAN_PRODUCTION_LIKE_DATABASE_URL:-postgresql+psycopg://govoplan:govoplan-dev@127.0.0.1:55433/govoplan}}" export GOVOPLAN_DATABASE_URL_PGTOOLS="${GOVOPLAN_DATABASE_URL_PGTOOLS:-${GOVOPLAN_PRODUCTION_LIKE_DATABASE_URL_PGTOOLS:-postgresql://govoplan:govoplan-dev@127.0.0.1:55433/govoplan}}" export REDIS_URL="${REDIS_URL:-${GOVOPLAN_PRODUCTION_LIKE_REDIS_URL:-redis://127.0.0.1:56379/0}}" export CELERY_ENABLED="${CELERY_ENABLED:-true}" export CELERY_QUEUES="${CELERY_QUEUES:-send_email,append_sent,default}" export FILE_STORAGE_BACKEND="${FILE_STORAGE_BACKEND:-local}" export FILE_STORAGE_LOCAL_ROOT="${FILE_STORAGE_LOCAL_ROOT:-$META_ROOT/runtime/production-like/files}" export DEV_AUTO_MIGRATE_ENABLED="${DEV_AUTO_MIGRATE_ENABLED:-false}" export DEV_BOOTSTRAP_ENABLED="${DEV_BOOTSTRAP_ENABLED:-true}" export CORS_ORIGINS="${CORS_ORIGINS:-http://127.0.0.1:$FRONTEND_PORT,http://localhost:$FRONTEND_PORT}" if [ -z "${MASTER_KEY_B64:-}" ]; then MASTER_KEY_B64="$("$PYTHON" - <<'PY' from cryptography.fernet import Fernet print(Fernet.generate_key().decode()) PY )" export MASTER_KEY_B64 fi "$PYTHON" -m govoplan_core.commands.config validate --profile production-like compose() { "$DOCKER" compose --env-file "$PROFILE_ENV" -f "$PROFILE_ROOT/docker-compose.yml" "$@" } port_is_free() { "$PYTHON" - "$1" "$2" <<'PY' import socket import sys host = sys.argv[1] port = int(sys.argv[2]) with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) try: sock.bind((host, port)) except OSError: raise SystemExit(1) PY } wait_for_tcp() { "$PYTHON" - "$1" "$2" <<'PY' import socket import sys import time host = sys.argv[1] port = int(sys.argv[2]) deadline = time.monotonic() + 90 last_error = None while time.monotonic() < deadline: try: with socket.create_connection((host, port), timeout=2): raise SystemExit(0) except OSError as exc: last_error = exc time.sleep(1) print(f"Timed out waiting for {host}:{port}: {last_error}", file=sys.stderr) raise SystemExit(1) PY } wait_for_url() { "$PYTHON" - "$1" <<'PY' import sys import time import urllib.request url = sys.argv[1] deadline = time.monotonic() + 90 last_error = None while time.monotonic() < deadline: try: with urllib.request.urlopen(url, timeout=2) as response: if 200 <= response.status < 500: raise SystemExit(0) except Exception as exc: # noqa: BLE001 - printed only on timeout. last_error = exc time.sleep(1) print(f"Timed out waiting for {url}: {last_error}", file=sys.stderr) raise SystemExit(1) PY } cleanup() { if [ -n "${frontend_pid:-}" ] && kill -0 "$frontend_pid" 2>/dev/null; then kill "$frontend_pid" 2>/dev/null || true fi if [ -n "${backend_pid:-}" ] && kill -0 "$backend_pid" 2>/dev/null; then kill "$backend_pid" 2>/dev/null || true fi if [ -n "${worker_pid:-}" ] && kill -0 "$worker_pid" 2>/dev/null; then kill "$worker_pid" 2>/dev/null || true fi if [ "$STOP_DEPENDENCIES_ON_EXIT" = "1" ]; then compose down >/dev/null 2>&1 || true fi } trap cleanup EXIT INT TERM [ -x "$PYTHON" ] || fail "Python virtualenv not found at $PYTHON. Run: cd $META_ROOT && python3 -m venv .venv && ./.venv/bin/python -m pip install --upgrade pip && ./.venv/bin/python -m pip install -r requirements-dev.txt" [ -x "$NPM" ] || fail "npm not found at $NPM. Set NODE_BIN or NPM to your Node installation." [ -f "$WEBUI_ROOT/package.json" ] || fail "WebUI package.json not found at $WEBUI_ROOT/package.json" [ -d "$WEBUI_ROOT/node_modules" ] || fail "WebUI node_modules missing. Run: cd $WEBUI_ROOT && PATH=$NODE_BIN:\$PATH $NPM install" mkdir -p "$LOG_DIR" "$FILE_STORAGE_LOCAL_ROOT" : > "$BACKEND_LOG" : > "$WORKER_LOG" : > "$FRONTEND_LOG" port_is_free "$BACKEND_HOST" "$BACKEND_PORT" || fail "$BACKEND_URL is already in use" port_is_free "$FRONTEND_HOST" "$FRONTEND_PORT" || fail "$FRONTEND_URL is already in use" printf 'Starting production-like dependencies through Docker Compose\n' compose up -d wait_for_tcp 127.0.0.1 "${GOVOPLAN_PRODUCTION_LIKE_POSTGRES_PORT:-55433}" wait_for_tcp 127.0.0.1 "${GOVOPLAN_PRODUCTION_LIKE_REDIS_PORT:-56379}" printf 'Running migrations and development bootstrap against %s\n' "$DATABASE_URL" ( cd "$ROOT" "$PYTHON" -m govoplan_core.commands.init_db --database-url "$DATABASE_URL" --with-dev-data ) printf 'Starting GovOPlaN backend at %s\n' "$BACKEND_URL" ( cd "$ROOT" "$PYTHON" -m govoplan_core.devserver --host "$BACKEND_HOST" --port "$BACKEND_PORT" ) >"$BACKEND_LOG" 2>&1 & backend_pid="$!" printf 'Waiting for %s/health\n' "$BACKEND_URL" wait_for_url "$BACKEND_URL/health" || { tail -n 80 "$BACKEND_LOG" >&2 || true fail "backend did not become healthy" } printf 'Starting Celery worker for queues %s\n' "$CELERY_QUEUES" ( cd "$ROOT" "$PYTHON" -m celery -A govoplan_core.celery_app:celery worker \ --queues "$CELERY_QUEUES" \ --hostname "govoplan-production-like@%h" \ --loglevel INFO ) >"$WORKER_LOG" 2>&1 & worker_pid="$!" printf 'Starting GovOPlaN WebUI at %s\n' "$FRONTEND_URL" ( cd "$WEBUI_ROOT" PATH="$WEBUI_ROOT/node_modules/.bin:$NODE_BIN:$PATH" \ CHOKIDAR_USEPOLLING="${GOVOPLAN_FRONTEND_USE_POLLING:-1}" \ CHOKIDAR_INTERVAL="${GOVOPLAN_FRONTEND_POLLING_INTERVAL:-500}" \ VITE_API_PROXY_TARGET="$BACKEND_URL" \ "$NPM" run dev -- --force ) >"$FRONTEND_LOG" 2>&1 & frontend_pid="$!" printf 'Waiting for %s\n' "$FRONTEND_URL" wait_for_url "$FRONTEND_URL" || { tail -n 80 "$FRONTEND_LOG" >&2 || true fail "frontend did not become reachable" } if [ "$OPEN_BROWSER" = "1" ] && command -v xdg-open >/dev/null 2>&1; then xdg-open "$FRONTEND_URL" >/dev/null 2>&1 || true fi cat <