#!/bin/bash set -euo pipefail SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) ROOT_DIR=$(cd "${SCRIPT_DIR}/.." && pwd) load_env_file() { local env_file=$1 if [[ -f ${env_file} ]]; then printf "[hot-dev] Loading %s\n" "${env_file}" set +u set -a # shellcheck disable=SC1090 source "${env_file}" set +a set -u fi } load_env_file "${ROOT_DIR}/.env" load_env_file "${ROOT_DIR}/.env.local" load_env_file "${ROOT_DIR}/.env.dev" FRONTEND_PORT=${FRONTEND_PORT:-${PORT:-7655}} PORT=${PORT:-${FRONTEND_PORT}} FRONTEND_DEV_HOST=${FRONTEND_DEV_HOST:-0.0.0.0} FRONTEND_DEV_PORT=${FRONTEND_DEV_PORT:-${FRONTEND_PORT}} PULSE_DEV_API_HOST=${PULSE_DEV_API_HOST:-127.0.0.1} PULSE_DEV_API_PORT=${PULSE_DEV_API_PORT:-7656} if [[ -z ${PULSE_DEV_API_URL:-} ]]; then PULSE_DEV_API_URL="http://${PULSE_DEV_API_HOST}:${PULSE_DEV_API_PORT}" fi if [[ -z ${PULSE_DEV_WS_URL:-} ]]; then if [[ ${PULSE_DEV_API_URL} == http://* ]]; then PULSE_DEV_WS_URL="ws://${PULSE_DEV_API_URL#http://}" elif [[ ${PULSE_DEV_API_URL} == https://* ]]; then PULSE_DEV_WS_URL="wss://${PULSE_DEV_API_URL#https://}" else PULSE_DEV_WS_URL=${PULSE_DEV_API_URL} fi fi export FRONTEND_PORT PORT export FRONTEND_DEV_HOST FRONTEND_DEV_PORT export PULSE_DEV_API_HOST PULSE_DEV_API_PORT PULSE_DEV_API_URL PULSE_DEV_WS_URL # Auto-detect pulse-sensor-proxy socket if available HOST_PROXY_SOCKET="/mnt/pulse-proxy/pulse-sensor-proxy.sock" CONTAINER_PROXY_SOCKET="/run/pulse-sensor-proxy/pulse-sensor-proxy.sock" if [[ -z ${PULSE_SENSOR_PROXY_SOCKET:-} ]]; then if [[ -S "${HOST_PROXY_SOCKET}" ]]; then export PULSE_SENSOR_PROXY_SOCKET="${HOST_PROXY_SOCKET}" printf "[hot-dev] Detected pulse-sensor-proxy socket at %s\n" "${PULSE_SENSOR_PROXY_SOCKET}" elif [[ -S "${CONTAINER_PROXY_SOCKET}" ]]; then export PULSE_SENSOR_PROXY_SOCKET="${CONTAINER_PROXY_SOCKET}" printf "[hot-dev] WARNING: Using container-local pulse-sensor-proxy socket at %s\n" "${PULSE_SENSOR_PROXY_SOCKET}" printf "[hot-dev] WARNING: Host proxy is missing; temperatures will not reach Pulse until it is reinstalled.\n" else printf "[hot-dev] WARNING: No pulse-sensor-proxy socket detected. Temperatures will be unavailable.\n" fi else if [[ ! -S "${PULSE_SENSOR_PROXY_SOCKET}" ]]; then printf "[hot-dev] WARNING: Configured pulse-sensor-proxy socket not found at %s\n" "${PULSE_SENSOR_PROXY_SOCKET}" elif [[ "${PULSE_SENSOR_PROXY_SOCKET}" == "${CONTAINER_PROXY_SOCKET}" && ! -S "${HOST_PROXY_SOCKET}" ]]; then printf "[hot-dev] WARNING: Using container-local proxy socket; reinstall host pulse-sensor-proxy for real telemetry.\n" fi fi EXTRA_CLEANUP_PORT=$((PULSE_DEV_API_PORT + 1)) cat </dev/null | awk 'NR>1 {print $2}' | xargs -r kill -9 2>/dev/null || true } printf "[hot-dev] Cleaning up existing processes...\n" # Don't stop ourselves if we're running under systemd if [[ -z "${INVOCATION_ID:-}" ]]; then # Not running under systemd, safe to stop the service sudo systemctl stop pulse-hot-dev 2>/dev/null || true fi sudo systemctl stop pulse-backend 2>/dev/null || true sudo systemctl stop pulse 2>/dev/null || true sudo systemctl stop pulse-frontend 2>/dev/null || true pkill -f "backend-watch.sh" 2>/dev/null || true pkill -f vite 2>/dev/null || true pkill -f "npm run dev" 2>/dev/null || true pkill -f "npm exec" 2>/dev/null || true pkill -x "pulse" 2>/dev/null || true sleep 1 pkill -9 -x "pulse" 2>/dev/null || true kill_port "${FRONTEND_DEV_PORT}" kill_port "${PULSE_DEV_API_PORT}" kill_port "${EXTRA_CLEANUP_PORT}" sleep 3 # Temporarily disable pipefail for port checks (lsof returns 1 when port is free) set +o pipefail if lsof -i :"${FRONTEND_DEV_PORT}" 2>/dev/null | grep -q LISTEN; then echo "ERROR: Port ${FRONTEND_DEV_PORT} is still in use after cleanup!" kill_port "${FRONTEND_DEV_PORT}" sleep 2 if lsof -i :"${FRONTEND_DEV_PORT}" 2>/dev/null | grep -q LISTEN; then echo "FATAL: Cannot free port ${FRONTEND_DEV_PORT}. Please manually kill the process:" lsof -i :"${FRONTEND_DEV_PORT}" exit 1 fi fi if lsof -i :"${PULSE_DEV_API_PORT}" 2>/dev/null | grep -q LISTEN; then echo "ERROR: Port ${PULSE_DEV_API_PORT} is still in use after cleanup!" kill_port "${PULSE_DEV_API_PORT}" sleep 2 if lsof -i :"${PULSE_DEV_API_PORT}" 2>/dev/null | grep -q LISTEN; then echo "FATAL: Cannot free port ${PULSE_DEV_API_PORT}. Please manually kill the process:" lsof -i :"${PULSE_DEV_API_PORT}" exit 1 fi fi # Re-enable pipefail set -o pipefail echo "Ports are clean!" if [[ -f "${ROOT_DIR}/mock.env" ]]; then load_env_file "${ROOT_DIR}/mock.env" # Load local overrides if they exist if [[ -f "${ROOT_DIR}/mock.env.local" ]]; then load_env_file "${ROOT_DIR}/mock.env.local" echo "[hot-dev] Loaded mock.env.local overrides" fi if [[ ${PULSE_MOCK_MODE:-false} == "true" ]]; then TOTAL_GUESTS=$((PULSE_MOCK_NODES * (PULSE_MOCK_VMS_PER_NODE + PULSE_MOCK_LXCS_PER_NODE))) echo "Mock mode ENABLED with ${PULSE_MOCK_NODES} nodes (${TOTAL_GUESTS} total guests)" else # Sync production config when not in mock mode echo "Syncing production configuration..." "${ROOT_DIR}/scripts/sync-production-config.sh" fi fi if [[ -f /etc/pulse/.env ]] && [[ -r /etc/pulse/.env ]]; then set +u # shellcheck disable=SC1091 source /etc/pulse/.env 2>/dev/null || true set -u echo "Auth configuration loaded from /etc/pulse/.env" fi printf "[hot-dev] Starting backend on port %s...\n" "${PULSE_DEV_API_PORT}" cd "${ROOT_DIR}" go build -o pulse ./cmd/pulse # CRITICAL: Export all required environment variables for the backend # Mock variables already exported via load_env_file (set -a) # But we must explicitly export PULSE_DATA_DIR to ensure dev mode uses correct config FRONTEND_PORT=${PULSE_DEV_API_PORT} PORT=${PULSE_DEV_API_PORT} export FRONTEND_PORT PULSE_DEV_API_PORT PORT # Set data directory strategy for the backend if [[ ${PULSE_MOCK_MODE:-false} == "true" ]]; then export PULSE_DATA_DIR=/opt/pulse/tmp/mock-data mkdir -p "$PULSE_DATA_DIR" echo "[hot-dev] Mock mode: Using isolated data directory: ${PULSE_DATA_DIR}" else if [[ -n ${PULSE_DATA_DIR:-} ]]; then echo "[hot-dev] Using preconfigured data directory: ${PULSE_DATA_DIR}" elif [[ ${HOT_DEV_USE_PROD_DATA:-false} == "true" ]]; then export PULSE_DATA_DIR=/etc/pulse echo "[hot-dev] HOT_DEV_USE_PROD_DATA=true – using production data directory: ${PULSE_DATA_DIR}" else DEV_CONFIG_DIR="${ROOT_DIR}/tmp/dev-config" mkdir -p "$DEV_CONFIG_DIR" export PULSE_DATA_DIR="${DEV_CONFIG_DIR}" echo "[hot-dev] Production mode: Using dev config directory: ${PULSE_DATA_DIR}" fi # Attempt to load encryption key automatically when not explicitly provided if [[ -z ${PULSE_ENCRYPTION_KEY:-} ]]; then if [[ -f "${PULSE_DATA_DIR}/.encryption.key" ]]; then export PULSE_ENCRYPTION_KEY="$(<"${PULSE_DATA_DIR}/.encryption.key")" echo "[hot-dev] Loaded encryption key from ${PULSE_DATA_DIR}/.encryption.key" elif [[ ${PULSE_DATA_DIR} == "${ROOT_DIR}/tmp/dev-config" ]]; then DEV_KEY_FILE="${PULSE_DATA_DIR}/.encryption.key" if [[ ! -f "${DEV_KEY_FILE}" ]]; then openssl rand -hex 32 > "${DEV_KEY_FILE}" chmod 600 "${DEV_KEY_FILE}" echo "[hot-dev] Generated dev encryption key at ${DEV_KEY_FILE}" fi export PULSE_ENCRYPTION_KEY="$(<"${DEV_KEY_FILE}")" else echo "[hot-dev] WARNING: No encryption key found for ${PULSE_DATA_DIR}. Encrypted config may fail to load." fi fi fi ./pulse & BACKEND_PID=$! sleep 2 if ! kill -0 "${BACKEND_PID}" 2>/dev/null; then echo "ERROR: Backend failed to start!" exit 1 fi # Start backend file watcher in background printf "[hot-dev] Starting backend file watcher...\n" ( cd "${ROOT_DIR}" while true; do # Watch for changes to .go files (excluding vendor and node_modules) inotifywait -r -e modify,create,delete,move \ --exclude '(vendor/|node_modules/|\.git/|\.swp$|\.tmp$|~$)' \ --format '%e %w%f' \ "${ROOT_DIR}/cmd" "${ROOT_DIR}/internal" "${ROOT_DIR}/pkg" 2>/dev/null | \ while read -r event changed_file; do # Rebuild on any .go file change OR any create/delete event (catches new files) if [[ "$changed_file" == *.go ]] || [[ "$event" =~ CREATE|DELETE|MOVED ]]; then echo "" echo "[hot-dev] 🔄 Change detected: $event $(basename "$changed_file")" echo "[hot-dev] Rebuilding backend..." # Rebuild the binary if go build -o pulse ./cmd/pulse 2>&1 | grep -v "^#"; then echo "[hot-dev] ✓ Build successful, restarting backend..." # Find and kill old backend OLD_PID=$(pgrep -f "^\./pulse$" || true) if [[ -n "$OLD_PID" ]]; then kill "$OLD_PID" 2>/dev/null || true sleep 1 if kill -0 "$OLD_PID" 2>/dev/null; then kill -9 "$OLD_PID" 2>/dev/null || true fi fi # Start new backend with same environment FRONTEND_PORT=${PULSE_DEV_API_PORT} PORT=${PULSE_DEV_API_PORT} PULSE_DATA_DIR=${PULSE_DATA_DIR} ./pulse & NEW_PID=$! sleep 1 if kill -0 "$NEW_PID" 2>/dev/null; then echo "[hot-dev] ✓ Backend restarted (PID: $NEW_PID)" else echo "[hot-dev] ✗ Backend failed to start!" fi else echo "[hot-dev] ✗ Build failed!" fi echo "[hot-dev] Watching for changes..." fi done done ) & WATCHER_PID=$! cleanup() { echo "" echo "Stopping services..." if [[ -n ${WATCHER_PID:-} ]] && kill -0 "${WATCHER_PID}" 2>/dev/null; then kill "${WATCHER_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 sleep 1 if kill -0 "${BACKEND_PID}" 2>/dev/null; then echo "Backend not responding to SIGTERM, force killing..." kill -9 "${BACKEND_PID}" 2>/dev/null || true fi fi pkill -f vite 2>/dev/null || true pkill -f "npm run dev" 2>/dev/null || true pkill -9 -x "pulse" 2>/dev/null || true pkill -f "inotifywait.*pulse" 2>/dev/null || true echo "Hot-dev stopped. To restart normal service, run: sudo systemctl start pulse" echo "(Legacy installs may use: sudo systemctl start pulse-backend)" } trap cleanup INT TERM EXIT printf "[hot-dev] Starting frontend with hot-reload on port %s...\n" "${FRONTEND_DEV_PORT}" echo "If this fails, port ${FRONTEND_DEV_PORT} is still in use!" cd "${ROOT_DIR}/frontend-modern" npx vite --config vite.config.ts --host "${FRONTEND_DEV_HOST}" --port "${FRONTEND_DEV_PORT}" --clearScreen false echo "ERROR: Vite exited unexpectedly!" echo "Dev mode will auto-restart in 5 seconds via systemd..." cleanup