#!/usr/bin/env bash set -euo pipefail ROOT="${GOVOPLAN_CORE_ROOT:-/mnt/DATA/git/govoplan-core}" WEBUI_ROOT="$ROOT/webui" PYTHON="${PYTHON:-$ROOT/.venv/bin/python}" NODE_BIN="${NODE_BIN:-/home/zemion/.nvm/versions/node/v22.22.3/bin}" NPM="${NPM:-$NODE_BIN/npm}" 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}" LOG_DIR="$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="" fail() { printf 'launch-dev: %s\n' "$*" >&2 exit 1 } 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() { 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 } trap cleanup EXIT INT TERM [ -x "$PYTHON" ] || fail "Python virtualenv not found at $PYTHON. Run: cd $ROOT && ./.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" : > "$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" ( 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 GovOPlaN WebUI at %s\n' "$FRONTEND_URL" ( cd "$WEBUI_ROOT" frontend_args=(run dev) if [ "$FRONTEND_FORCE_RELOAD" = "1" ]; then frontend_args+=(-- --force) fi PATH="$WEBUI_ROOT/node_modules/.bin:$NODE_BIN:$PATH" \ CHOKIDAR_USEPOLLING="$FRONTEND_USE_POLLING" \ CHOKIDAR_INTERVAL="$FRONTEND_POLLING_INTERVAL" \ VITE_API_PROXY_TARGET="$BACKEND_URL" \ "$NPM" "${frontend_args[@]}" ) >"$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 <