#!/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)" WEBUI_ROOT="${GOVOPLAN_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}" NPM_USERCONFIG="$(mktemp "${TMPDIR:-/tmp}/govoplan-npmrc.XXXXXXXX")" 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}" FRONTEND_FORCE_RELOAD="${GOVOPLAN_FRONTEND_FORCE_RELOAD:-1}" FRONTEND_CLEAR_VITE_CACHE="${GOVOPLAN_FRONTEND_CLEAR_VITE_CACHE:-1}" FRONTEND_USE_POLLING="${GOVOPLAN_FRONTEND_USE_POLLING:-1}" FRONTEND_POLLING_INTERVAL="${GOVOPLAN_FRONTEND_POLLING_INTERVAL:-500}" AUTO_SYNC_PYTHON="${GOVOPLAN_AUTO_SYNC_PYTHON:-1}" DEV_DATABASE_BACKEND="${GOVOPLAN_DEV_DATABASE_BACKEND:-postgres}" LOG_DIR="${GOVOPLAN_DEV_LOG_DIR:-$META_ROOT/runtime/dev-launcher}" BACKEND_LOG="$LOG_DIR/backend.log" FRONTEND_LOG="$LOG_DIR/frontend.log" BACKEND_URL="http://$BACKEND_HOST:$BACKEND_PORT" FRONTEND_URL="http://$FRONTEND_HOST:$FRONTEND_PORT" backend_pid="" frontend_pid="" run_grouped_pid="" fail() { printf 'launch-dev: %s\n' "$*" >&2 exit 1 } run_grouped() { local cwd="$1" local log_file="$2" shift 2 if command -v setsid >/dev/null 2>&1; then setsid sh -c 'cd "$1" || exit 127; shift; exec "$@"' sh "$cwd" "$@" >"$log_file" 2>&1 & else ( cd "$cwd" exec "$@" ) >"$log_file" 2>&1 & fi run_grouped_pid="$!" } terminate_process() { local pid="${1:-}" local pgid="" [ -n "$pid" ] || return 0 kill -0 "$pid" 2>/dev/null || return 0 pgid="$(ps -o pgid= -p "$pid" 2>/dev/null | tr -d ' ' || true)" if [ "$pgid" = "$pid" ]; then kill -TERM "-$pid" 2>/dev/null || true else kill "$pid" 2>/dev/null || true fi sleep 1 kill -0 "$pid" 2>/dev/null || return 0 if [ "$pgid" = "$pid" ]; then kill -KILL "-$pid" 2>/dev/null || true else kill -KILL "$pid" 2>/dev/null || true fi } case "$DEV_DATABASE_BACKEND" in postgres) export DATABASE_URL="${DATABASE_URL:-postgresql+psycopg://govoplan_dev@127.0.0.1:5432/govoplan_dev}" export GOVOPLAN_DATABASE_URL_PGTOOLS="${GOVOPLAN_DATABASE_URL_PGTOOLS:-postgresql://govoplan_dev@127.0.0.1:5432/govoplan_dev}" ;; sqlite) mkdir -p "$META_ROOT/runtime" export DATABASE_URL="${DATABASE_URL:-sqlite:///$META_ROOT/runtime/govoplan-dev.db}" ;; *) fail "Unsupported GOVOPLAN_DEV_DATABASE_BACKEND=$DEV_DATABASE_BACKEND. Use postgres or sqlite." ;; esac export DEV_BOOTSTRAP_ENABLED="${DEV_BOOTSTRAP_ENABLED:-true}" 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_url() { "$PYTHON" - "$1" <<'PY' import sys import time import urllib.request url = sys.argv[1] deadline = time.monotonic() + 60 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() { terminate_process "$frontend_pid" terminate_process "$backend_pid" rm -f "$NPM_USERCONFIG" } 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" if [ "$AUTO_SYNC_PYTHON" = "1" ]; then "$PYTHON" "$META_ROOT/tools/repo/sync-python-environment.py" \ --requirements "$META_ROOT/requirements-dev.txt" \ --python "$PYTHON" \ --stamp "$VENV_ROOT/.govoplan-sync-requirements-dev.json" \ || fail "Python environment synchronization failed" fi ( cd "$META_ROOT" "$PYTHON" - <<'PY' import importlib importlib.import_module("govoplan_core.devserver") PY ) || fail "Python environment at $PYTHON cannot import govoplan_core.devserver. Run: cd $META_ROOT && ./.venv/bin/python -m pip install -r requirements-dev.txt" mkdir -p "$LOG_DIR" : > "$BACKEND_LOG" : > "$FRONTEND_LOG" if [ "$FRONTEND_CLEAR_VITE_CACHE" = "1" ]; then rm -rf "$WEBUI_ROOT/node_modules/.vite" "$WEBUI_ROOT/node_modules/.vite-temp" fi 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 GovOPlaN backend at %s\n' "$BACKEND_URL" run_grouped "$ROOT" "$BACKEND_LOG" "$PYTHON" -m govoplan_core.devserver --host "$BACKEND_HOST" --port "$BACKEND_PORT" backend_pid="$run_grouped_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 GovOPlaN WebUI at %s\n' "$FRONTEND_URL" frontend_args=(run dev -- --host "$FRONTEND_HOST" --port "$FRONTEND_PORT" --strictPort) if [ "$FRONTEND_FORCE_RELOAD" = "1" ]; then frontend_args+=(--force) fi run_grouped "$WEBUI_ROOT" "$FRONTEND_LOG" \ env \ -u npm_config_tmp \ -u NPM_CONFIG_TMP \ "PATH=$WEBUI_ROOT/node_modules/.bin:$NODE_BIN:$PATH" \ "NPM_CONFIG_USERCONFIG=$NPM_USERCONFIG" \ "CHOKIDAR_USEPOLLING=$FRONTEND_USE_POLLING" \ "CHOKIDAR_INTERVAL=$FRONTEND_POLLING_INTERVAL" \ "VITE_API_PROXY_TARGET=$BACKEND_URL" \ "$NPM" "${frontend_args[@]}" frontend_pid="$run_grouped_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 <