#!/usr/bin/env sh set -eu if (set -o pipefail) >/dev/null 2>&1; then set -o pipefail fi export MSYS_NO_PATHCONV=1 SCRIPT_PATH="$0" case "$SCRIPT_PATH" in /*) ;; *) SCRIPT_PATH="$(pwd)/$SCRIPT_PATH" ;; esac SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$SCRIPT_PATH")" && pwd)" ENV_FILE="$SCRIPT_DIR/.env" ENV_EXAMPLE_FILE="$SCRIPT_DIR/.env.example" ADMIN_SECRET_FILE="$SCRIPT_DIR/server/.secrets/admin.json" MODE="${1:-all}" RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' NC='\033[0m' info() { printf '%b\n' "${GREEN}[INFO]${NC} $*"; } warn() { printf '%b\n' "${YELLOW}[WARN]${NC} $*"; } error() { printf '%b\n' "${RED}[ERROR]${NC} $*"; exit 1; } is_container() { [ -f "/.dockerenv" ] && return 0 if [ -r /proc/1/cgroup ] && grep -Eq "(docker|containerd|kubepods)" /proc/1/cgroup 2>/dev/null; then return 0 fi return 1 } if [ ! -f "$ENV_FILE" ]; then if [ -f "$ENV_EXAMPLE_FILE" ]; then warn ".env not found. Creating it from .env.example" cp "$ENV_EXAMPLE_FILE" "$ENV_FILE" else error ".env and .env.example are both missing." fi fi ENV_OVERRIDE_WEB_HOST_SET=false ENV_OVERRIDE_SERVER_HOST_SET=false ENV_OVERRIDE_ONLYOFFICE_ENABLED_SET=false ENV_OVERRIDE_ONLYOFFICE_PUBLIC_URL_SET=false ENV_OVERRIDE_ONLYOFFICE_BACKEND_URL_SET=false ENV_OVERRIDE_ONLYOFFICE_JWT_SECRET_SET=false PREFER_ENV_FILE_FOR_ONLYOFFICE=false case "${X_FINANCIAL_PREFER_ENV_FILE:-false}" in 1|true|TRUE|yes|YES|on|ON) PREFER_ENV_FILE_FOR_ONLYOFFICE=true ;; esac if [ "${WEB_HOST+x}" = x ]; then ENV_OVERRIDE_WEB_HOST_SET=true ENV_OVERRIDE_WEB_HOST="$WEB_HOST" fi if [ "${SERVER_HOST+x}" = x ]; then ENV_OVERRIDE_SERVER_HOST_SET=true ENV_OVERRIDE_SERVER_HOST="$SERVER_HOST" fi if [ "$PREFER_ENV_FILE_FOR_ONLYOFFICE" != true ] && [ "${ONLYOFFICE_ENABLED+x}" = x ]; then ENV_OVERRIDE_ONLYOFFICE_ENABLED_SET=true ENV_OVERRIDE_ONLYOFFICE_ENABLED="$ONLYOFFICE_ENABLED" fi if [ "$PREFER_ENV_FILE_FOR_ONLYOFFICE" != true ] && [ "${ONLYOFFICE_PUBLIC_URL+x}" = x ]; then ENV_OVERRIDE_ONLYOFFICE_PUBLIC_URL_SET=true ENV_OVERRIDE_ONLYOFFICE_PUBLIC_URL="$ONLYOFFICE_PUBLIC_URL" fi if [ "$PREFER_ENV_FILE_FOR_ONLYOFFICE" != true ] && [ "${ONLYOFFICE_BACKEND_URL+x}" = x ]; then ENV_OVERRIDE_ONLYOFFICE_BACKEND_URL_SET=true ENV_OVERRIDE_ONLYOFFICE_BACKEND_URL="$ONLYOFFICE_BACKEND_URL" fi if [ "$PREFER_ENV_FILE_FOR_ONLYOFFICE" != true ] && [ "${ONLYOFFICE_JWT_SECRET+x}" = x ]; then ENV_OVERRIDE_ONLYOFFICE_JWT_SECRET_SET=true ENV_OVERRIDE_ONLYOFFICE_JWT_SECRET="$ONLYOFFICE_JWT_SECRET" fi set -a . "$ENV_FILE" set +a if [ "$ENV_OVERRIDE_WEB_HOST_SET" = true ]; then WEB_HOST="$ENV_OVERRIDE_WEB_HOST" fi if [ "$ENV_OVERRIDE_SERVER_HOST_SET" = true ]; then SERVER_HOST="$ENV_OVERRIDE_SERVER_HOST" fi if [ "$ENV_OVERRIDE_ONLYOFFICE_ENABLED_SET" = true ]; then ONLYOFFICE_ENABLED="$ENV_OVERRIDE_ONLYOFFICE_ENABLED" fi if [ "$ENV_OVERRIDE_ONLYOFFICE_PUBLIC_URL_SET" = true ]; then ONLYOFFICE_PUBLIC_URL="$ENV_OVERRIDE_ONLYOFFICE_PUBLIC_URL" fi if [ "$ENV_OVERRIDE_ONLYOFFICE_BACKEND_URL_SET" = true ]; then ONLYOFFICE_BACKEND_URL="$ENV_OVERRIDE_ONLYOFFICE_BACKEND_URL" fi if [ "$ENV_OVERRIDE_ONLYOFFICE_JWT_SECRET_SET" = true ]; then ONLYOFFICE_JWT_SECRET="$ENV_OVERRIDE_ONLYOFFICE_JWT_SECRET" fi SERVER_STARTUP_TIMEOUT="${SERVER_STARTUP_TIMEOUT:-300}" SERVER_SMOKE_CHECK_ENABLED="${SERVER_SMOKE_CHECK_ENABLED:-false}" SETUP_COMPLETED="${SETUP_COMPLETED:-false}" APP_DEBUG="${APP_DEBUG:-true}" APP_ENV="${APP_ENV:-local}" SERVER_RELOAD="${SERVER_RELOAD:-}" DEFAULT_SERVER_RELOAD="false" case "$APP_ENV" in local|dev|development) DEFAULT_SERVER_RELOAD="true" ;; esac if [ "$APP_DEBUG" = "true" ]; then DEFAULT_SERVER_RELOAD="true" fi if is_container; then DEFAULT_SERVER_RELOAD="false" fi EFFECTIVE_SERVER_RELOAD="${SERVER_RELOAD:-$DEFAULT_SERVER_RELOAD}" setup_ready() { [ "$SETUP_COMPLETED" = "true" ] && [ -f "$ADMIN_SECRET_FILE" ] } server_probe_host() { case "${SERVER_HOST:-127.0.0.1}" in 0.0.0.0|::) echo "127.0.0.1" ;; *) echo "${SERVER_HOST:-127.0.0.1}" ;; esac } server_probe_url() { echo "http://$(server_probe_host):${SERVER_PORT:-8000}${API_V1_PREFIX:-/api/v1}/health" } server_smoke_url() { echo "http://$(server_probe_host):${SERVER_PORT:-8000}${API_V1_PREFIX:-/api/v1}/employees/meta" } server_probe_python() { if [ -x "$SCRIPT_DIR/server/.venv/Scripts/python.exe" ]; then echo "$SCRIPT_DIR/server/.venv/Scripts/python.exe" return 0 fi if [ -x "$SCRIPT_DIR/server/.venv/bin/python" ]; then echo "$SCRIPT_DIR/server/.venv/bin/python" return 0 fi return 1 } probe_server_health() { _probe_url="${1:-$(server_probe_url)}" _probe_python="" if _probe_python="$(server_probe_python)"; then "$_probe_python" -c "import json, sys, urllib.request; data = json.load(urllib.request.urlopen(sys.argv[1], timeout=2)); raise SystemExit(0 if data.get('status') == 'ok' else 1)" "$_probe_url" >/dev/null 2>&1 return $? fi if command -v curl >/dev/null 2>&1; then curl --silent --fail --max-time 2 "$_probe_url" | grep -q '"status"[[:space:]]*:[[:space:]]*"ok"' return $? fi return 1 } probe_server_smoke() { _probe_url="${1:-$(server_smoke_url)}" _probe_python="" if _probe_python="$(server_probe_python)"; then "$_probe_python" -c "import sys, urllib.request; response = urllib.request.urlopen(sys.argv[1], timeout=3); raise SystemExit(0 if response.status == 200 else 1)" "$_probe_url" >/dev/null 2>&1 return $? fi if command -v curl >/dev/null 2>&1; then curl --silent --fail --max-time 3 "$_probe_url" >/dev/null 2>&1 return $? fi return 1 } probe_server_port_open() { _probe_python="" _probe_host="$(server_probe_host)" _probe_port="${SERVER_PORT:-8000}" if _probe_python="$(server_probe_python)"; then "$_probe_python" -c "import socket, sys; sock = socket.socket(); sock.settimeout(1.5); result = sock.connect_ex((sys.argv[1], int(sys.argv[2]))); sock.close(); raise SystemExit(0 if result == 0 else 1)" "$_probe_host" "$_probe_port" >/dev/null 2>&1 return $? fi if command -v ss >/dev/null 2>&1; then ss -ltn | grep -q "[.:]${_probe_port}[[:space:]]" return $? fi return 1 } probe_server_ready() { _health_url="${1:-$(server_probe_url)}" _smoke_url="${2:-$(server_smoke_url)}" probe_server_health "$_health_url" || return 1 case "$SERVER_SMOKE_CHECK_ENABLED" in 1|true|TRUE|yes|YES|on|ON) probe_server_smoke "$_smoke_url" return $? ;; esac return 0 } prepare_web() { info "Preparing web dependencies..." ( cd "$SCRIPT_DIR/web" ./web_start.sh deps ) } prepare_server() { info "Preparing server dependencies..." ( cd "$SCRIPT_DIR/server" ./server_start.sh deps ) } start_web() { prepare_web cd "$SCRIPT_DIR/web" exec ./web_start.sh start } start_server() { prepare_server cd "$SCRIPT_DIR/server" exec ./server_start.sh start } start_setup_web() { warn "Initial setup is not completed. Starting web only." warn "Setup requires both .env completion and server/.secrets/admin.json." warn "Finish the setup form first; the web setup bridge will start FastAPI after saving." prepare_web cd "$SCRIPT_DIR/web" export X_FINANCIAL_FORCE_SETUP=true exec ./web_start.sh start } start_all() { server_pid="" started_server=false probe_url="" smoke_url="" prepare_server cleanup() { if [ "$started_server" = true ] && [ -n "$server_pid" ] && kill -0 "$server_pid" 2>/dev/null; then warn "Stopping FastAPI server..." kill "$server_pid" 2>/dev/null || true wait "$server_pid" 2>/dev/null || true fi } trap cleanup EXIT INT TERM probe_url="$(server_probe_url)" smoke_url="$(server_smoke_url)" if probe_server_ready "$probe_url" "$smoke_url"; then warn "FastAPI is already ready at $probe_url. Reusing the existing backend process." if [ "$APP_DEBUG" = "true" ] && [ "$EFFECTIVE_SERVER_RELOAD" != "true" ]; then warn "This backend may be stale because SERVER_RELOAD is disabled. If new API routes are missing, stop the old backend process and rerun ./start.sh." fi elif probe_server_health "$probe_url"; then error "An existing backend process is responding at $probe_url, but the smoke check failed at $smoke_url. Stop the old FastAPI process and rerun ./start.sh." elif probe_server_port_open; then error "Port ${SERVER_PORT:-8000} is already occupied, but FastAPI health checks are not passing at $probe_url. Stop the stale backend process and rerun ./start.sh." else info "Starting FastAPI server..." ( cd "$SCRIPT_DIR/server" ./server_start.sh start ) & server_pid=$! started_server=true fi wait_for_server_ready() { attempt=1 max_attempts="$SERVER_STARTUP_TIMEOUT" info "Waiting for FastAPI readiness before starting the web frontend..." while [ "$attempt" -le "$max_attempts" ]; do if probe_server_ready "$probe_url" "$smoke_url"; then info "FastAPI is ready. Starting web frontend next." return 0 fi if [ "$started_server" = true ] && ! kill -0 "$server_pid" 2>/dev/null; then if probe_server_ready "$probe_url" "$smoke_url"; then warn "FastAPI is already available at $probe_url. Continuing with the existing process." started_server=false server_pid="" return 0 fi wait "$server_pid" 2>/dev/null || true error "FastAPI process exited before becoming ready. Run ./server/server_start.sh start directly to inspect the backend error." fi sleep 1 attempt=$((attempt + 1)) done if probe_server_health "$probe_url"; then error "FastAPI answered health checks at $probe_url, but the smoke check failed at $smoke_url. The running backend is stale or incompatible." fi error "FastAPI did not become ready within ${SERVER_STARTUP_TIMEOUT}s. Inspect server/logs/app.log." } wait_for_server_ready prepare_web info "Starting web frontend..." cd "$SCRIPT_DIR/web" ./web_start.sh start } case "$MODE" in web) start_web ;; server) start_server ;; all) if setup_ready; then start_all else start_setup_web fi ;; *) error "Unknown mode: $MODE. Use one of: web, server, all" ;; esac