prospector/scripts/app.sh
Natalie b08a521b96 fix(app): reach panel via the bind host (mesh --host=IP support)
A specific --host=ADDR binds only that address, so localhost health-check and
window-open failed. Derive REACH_HOST from the bind (localhost for unset/0.0.0.0,
else the bound IP) and use it for web_up + PANEL_URL. Verified: gate works with a
real passcode (401 unauth, 200 with cookie).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 10:04:19 -04:00

152 lines
5.7 KiB
Bash
Executable file

#!/usr/bin/env bash
# Launch the prospector operator app locally and open it as a Chrome app window
# (the containerless PWA experience). Starts the NestJS API + the vite preview
# front door (serves the built web/ assets and injects the bearer token), waits
# for health, then opens Chrome --app at the panel URL.
#
# scripts/app.sh launch in the foreground (Ctrl-C stops it)
# scripts/app.sh --build rebuild backend + web first
# scripts/app.sh --detach start in the background and return (tray/menubar)
# ./run app (preferred entrypoint)
#
# If the app is already running, just opens the window. Foreground mode stops
# both processes on Ctrl-C; detached mode leaves them running (stop with
# './run stop'). Only the PIDs we started are killed — never a blanket node kill.
set -euo pipefail
. "$(dirname "${BASH_SOURCE[0]}")/lib.sh"
load_env
API_PORT="${PROSPECTOR_API_PORT:-3210}"
WEB_PORT="${PROSPECTOR_WEB_PORT:-4173}"
HEALTH_URL="http://127.0.0.1:${API_PORT}/health"
RUN_DIR="$REPO_ROOT/.run"
PIDFILE="$RUN_DIR/app.pids"
BUILD=0 DETACH=0
WEB_HOST="${PROSPECTOR_WEB_HOST:-}" # empty = localhost only (default)
for a in "$@"; do
case "$a" in
--build) BUILD=1 ;;
--detach) DETACH=1 ;;
--host) WEB_HOST="0.0.0.0" ;;
--host=*) WEB_HOST="${a#--host=}" ;;
*) die "unknown flag: $a (use --build / --detach / --host[=addr])" ;;
esac
done
# Binding the panel beyond localhost exposes an app with no login of its own —
# require a passcode (the vite preview gate) before doing so.
HOST_ARGS=()
if [[ -n "$WEB_HOST" ]]; then
HOST_ARGS=(--host "$WEB_HOST")
if [[ -z "$(grep -sE '^PROSPECTOR_PANEL_PASSCODE=.+' "$REPO_ROOT/web/.env.local")" ]]; then
warn "exposing the panel on $WEB_HOST with NO passcode — anyone who can reach :$WEB_PORT gets full operator control."
warn "set PROSPECTOR_PANEL_PASSCODE in web/.env.local first (then it prompts for a passcode)."
fi
fi
# Host used to reach the panel from this machine (health-check + opening the
# window). A specific --host binds only that address; unset/0.0.0.0 still answer
# on localhost.
if [[ -z "$WEB_HOST" || "$WEB_HOST" == "0.0.0.0" || "$WEB_HOST" == "::" ]]; then
REACH_HOST="localhost"
else
REACH_HOST="$WEB_HOST"
fi
PANEL_URL="http://${REACH_HOST}:${WEB_PORT}/#/markets"
api_up() { curl -fsS "$HEALTH_URL" >/dev/null 2>&1; }
# Any HTTP response = up (the passcode gate answers 401 to non-HTML requests, so
# don't use -f here — a 401 still means the preview server is listening).
web_up() { curl -sS -o /dev/null "http://${REACH_HOST}:${WEB_PORT}" 2>/dev/null; }
open_window() {
info "opening $PANEL_URL"
if [[ -d "/Applications/Google Chrome.app" ]]; then
open -na "Google Chrome" --args --app="$PANEL_URL" || open "$PANEL_URL"
else
warn "Google Chrome not found — opening in your default browser"
open "$PANEL_URL" 2>/dev/null || true
fi
}
if [[ "$BUILD" == "1" ]]; then
info "building backend"; ( cd "$REPO_ROOT" && npm run build )
info "building web"; ( cd "$REPO_ROOT/web" && npm run build )
fi
[[ -f "$REPO_ROOT/dist/main.js" ]] || die "backend not built — run 'scripts/app.sh --build' or './run install'"
[[ -d "$REPO_ROOT/web/dist" ]] || die "web not built — run 'scripts/app.sh --build' or './run install'"
mkdir -p "$RUN_DIR"
BACK_PID="" WEB_PID=""
# Reuse whatever is already up (idempotent); only start what's down. This avoids
# colliding with an existing instance on the same ports.
if api_up; then
ok "API already running on :$API_PORT — reusing"
else
info "starting API on :$API_PORT${DETACH:+ (detached)}"
if [[ "$DETACH" == "1" ]]; then
( cd "$REPO_ROOT" && exec node dist/main.js ) >"$RUN_DIR/api.log" 2>&1 &
else
( cd "$REPO_ROOT" && exec node dist/main.js ) &
fi
BACK_PID=$!
info "waiting for API health"
for _ in $(seq 1 40); do
kill -0 "$BACK_PID" 2>/dev/null || die "API exited during startup — port :$API_PORT in use or DB unreachable? (see $RUN_DIR/api.log)"
api_up && { ok "API healthy"; break; }
sleep 0.5
done
api_up || die "API did not become healthy at $HEALTH_URL"
fi
if web_up; then
ok "web front door already running on :$WEB_PORT — reusing"
else
info "starting web front door on :$WEB_PORT${DETACH:+ (detached)}"
if [[ "$DETACH" == "1" ]]; then
( cd "$REPO_ROOT/web" && exec npx vite preview ${HOST_ARGS[@]+"${HOST_ARGS[@]}"} --port "$WEB_PORT" --strictPort ) >"$RUN_DIR/web.log" 2>&1 &
else
( cd "$REPO_ROOT/web" && exec npx vite preview ${HOST_ARGS[@]+"${HOST_ARGS[@]}"} --port "$WEB_PORT" --strictPort ) &
fi
WEB_PID=$!
for _ in $(seq 1 40); do
kill -0 "$WEB_PID" 2>/dev/null || die "web preview exited during startup — port :$WEB_PORT in use? (see $RUN_DIR/web.log)"
web_up && break
sleep 0.5
done
web_up || die "web preview did not come up on :$WEB_PORT"
fi
# Record only the PIDs we started, so 'stop' touches nothing it didn't launch.
: > "$PIDFILE"
[[ -n "$BACK_PID" ]] && echo "$BACK_PID" >> "$PIDFILE"
[[ -n "$WEB_PID" ]] && echo "$WEB_PID" >> "$PIDFILE"
open_window
if [[ "$DETACH" == "1" ]]; then
ok "prospector running. API :$API_PORT · panel :$WEB_PORT · stop with './run stop'"
exit 0
fi
# Foreground: clean up only what we started; if we reused both, just exit.
if [[ -z "$BACK_PID" && -z "$WEB_PID" ]]; then
ok "prospector already running — opened window. API :$API_PORT · panel :$WEB_PORT"
exit 0
fi
cleanup() {
[[ -n "$WEB_PID" ]] && kill "$WEB_PID" 2>/dev/null || true
[[ -n "$BACK_PID" ]] && kill "$BACK_PID" 2>/dev/null || true
rm -f "$PIDFILE"
}
trap cleanup EXIT INT TERM
ok "prospector running. API :$API_PORT · panel :$WEB_PORT"
echo " Tip: in the Chrome window, ⋮ → Save and Share → Install Prospector for a dock icon."
echo " Press Ctrl-C to stop."
wait "${BACK_PID:-$WEB_PID}"