#!/bin/bash # # Amazon A/R Aging Dashboard — double-click launcher (macOS) # # Starts the FastAPI backend and the Vite frontend, waits until both are actually # answering, then opens the dashboard in your browser. Leave this Terminal window open # while you work; press Ctrl-C (or just close the window) to stop both servers. # # Everything runs on this machine. No transaction data leaves your computer. set -u -o pipefail # --- resolve our own directory ------------------------------------------------------ # The project path contains spaces and an "&", so every path stays quoted throughout. HERE="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)" cd -- "$HERE" || exit 1 APP_DIR="$HERE/ar-aging-app" BACKEND_DIR="$APP_DIR/backend" FRONTEND_DIR="$APP_DIR/frontend" LOG_DIR="$APP_DIR/backend/data/logs" BACKEND_LOG="$LOG_DIR/backend.log" FRONTEND_LOG="$LOG_DIR/frontend.log" BACKEND_PORT=8000 # the Vite proxy hard-codes localhost:8000 — don't change one without the other FRONTEND_PORT=5173 DASHBOARD_URL="http://localhost:$FRONTEND_PORT" # --- Finder gives a minimal PATH; put the usual tool locations back ------------------ # Double-clicking does not run your shell profile, so node/npm installed under # ~/.local/bin or Homebrew would otherwise be "command not found". export PATH="$HOME/.local/bin:/opt/homebrew/bin:/usr/local/bin:/Library/Frameworks/Python.framework/Versions/3.11/bin:$PATH" # --- pretty output ------------------------------------------------------------------ if [ -t 1 ]; then B=$'\033[1m'; DIM=$'\033[2m'; R=$'\033[0m' OK=$'\033[32m'; WARN=$'\033[33m'; ERR=$'\033[31m'; ACCENT=$'\033[35m' else B=""; DIM=""; R=""; OK=""; WARN=""; ERR=""; ACCENT="" fi say() { printf '%s\n' "$*"; } step() { printf ' %s→%s %s\n' "$DIM" "$R" "$*"; } good() { printf ' %s✓%s %s\n' "$OK" "$R" "$*"; } warn() { printf ' %s!%s %s\n' "$WARN" "$R" "$*"; } fail() { printf ' %s✗%s %s\n' "$ERR" "$R" "$*"; } die() { printf '\n%s%sCould not start the dashboard.%s\n\n' "$ERR" "$B" "$R" printf ' %s\n\n' "$1" [ $# -gt 1 ] && printf ' Try: %s%s%s\n\n' "$B" "$2" "$R" printf '%sPress Return to close this window.%s\n' "$DIM" "$R" read -r _ exit 1 } say "" say "${ACCENT}${B} Amazon A/R Aging — Month-End Closing${R}" say "${DIM} $HERE${R}" say "" # --- 1. prerequisites --------------------------------------------------------------- say "${B}1. Checking prerequisites${R}" [ -d "$BACKEND_DIR" ] || die "Backend folder not found at: $BACKEND_DIR" \ "keep start.command in the same folder as ar-aging-app/" command -v python3 >/dev/null 2>&1 || die "python3 was not found." \ "install Python 3.11+ from python.org" command -v npm >/dev/null 2>&1 || die "npm was not found." \ "install Node.js 20+ from nodejs.org" good "python $(python3 -V 2>&1 | awk '{print $2}') · node $(node -v 2>/dev/null) · npm $(npm -v 2>/dev/null)" # Python packages — actually IMPORT the app rather than checking a few package names. # "Installed" is not the same as "compatible": an unpinned starlette upgrade once satisfied # every import check while breaking the app at load time. Importing proves it will boot. import_error="" if ! import_error="$(cd -- "$BACKEND_DIR" && python3 -c "from app.api.main import app" 2>&1)"; then warn "the backend could not be loaded:" printf '%s\n' "$import_error" | tail -n 3 | sed 's/^/ /' printf ' Install/repair Python packages now? [Y/n] ' read -r reply case "${reply:-Y}" in [Nn]*) die "The backend cannot start with the current Python packages." \ "python3 -m pip install -r '$BACKEND_DIR/requirements.txt'" ;; esac step "installing Python packages (this can take a minute)…" python3 -m pip install -q -r "$BACKEND_DIR/requirements.txt" \ || die "pip install failed — see the messages above." \ "python3 -m pip install -r '$BACKEND_DIR/requirements.txt'" if ! import_error="$(cd -- "$BACKEND_DIR" && python3 -c "from app.api.main import app" 2>&1)"; then printf '%s\n' "$import_error" | tail -n 5 | sed 's/^/ /' die "The backend still cannot be loaded after installing packages." \ "python3 -m pip check" fi good "Python packages repaired" else good "Python packages present and compatible" fi # Frontend packages — safe to install unattended, they're local to the project. if [ ! -d "$FRONTEND_DIR/node_modules" ]; then step "installing frontend packages (first run only, ~1 minute)…" ( cd -- "$FRONTEND_DIR" && npm install --silent ) \ || die "npm install failed — see the messages above." \ "cd '$FRONTEND_DIR' && npm install" good "frontend packages installed" else good "frontend packages present" fi # --- 2. ports ----------------------------------------------------------------------- # A leftover server from a previous run holds the port and the launch silently fails. # The Vite proxy points at a fixed localhost:8000, so we can't just pick another port. free_port() { local port="$1" label="$2" pids pids="$(lsof -ti tcp:"$port" -sTCP:LISTEN 2>/dev/null)" [ -z "$pids" ] && { good "port $port free ($label)"; return 0; } warn "port $port is already in use ($label):" local pid for pid in $pids; do printf ' pid %-7s %s\n' "$pid" "$(ps -p "$pid" -o command= 2>/dev/null | cut -c1-88)" done printf ' Stop it and continue? [Y/n] ' read -r reply case "${reply:-Y}" in [Nn]*) die "Port $port is in use, so the dashboard cannot start." \ "quit the other program, or close the old dashboard window" ;; esac for pid in $pids; do kill "$pid" 2>/dev/null; done for _ in 1 2 3 4 5 6 7 8 9 10; do sleep 0.3 [ -z "$(lsof -ti tcp:"$port" -sTCP:LISTEN 2>/dev/null)" ] && break done # Still holding on? Escalate once. pids="$(lsof -ti tcp:"$port" -sTCP:LISTEN 2>/dev/null)" if [ -n "$pids" ]; then for pid in $pids; do kill -9 "$pid" 2>/dev/null; done sleep 1 fi [ -n "$(lsof -ti tcp:"$port" -sTCP:LISTEN 2>/dev/null)" ] \ && die "Port $port is still in use after trying to stop it." \ "restart your Mac, or find the process with: lsof -i :$port" good "port $port freed" } say "" say "${B}2. Checking ports${R}" free_port "$BACKEND_PORT" "backend" free_port "$FRONTEND_PORT" "frontend" # --- 3. start the servers ----------------------------------------------------------- mkdir -p "$LOG_DIR" BACKEND_PID="" FRONTEND_PID="" shutdown() { printf '\n%sStopping…%s\n' "$DIM" "$R" # Kill the whole process group of each server: uvicorn --reload and vite both fork. [ -n "$FRONTEND_PID" ] && kill -- "-$FRONTEND_PID" 2>/dev/null [ -n "$BACKEND_PID" ] && kill -- "-$BACKEND_PID" 2>/dev/null sleep 0.5 [ -n "$FRONTEND_PID" ] && kill -9 -- "-$FRONTEND_PID" 2>/dev/null [ -n "$BACKEND_PID" ] && kill -9 -- "-$BACKEND_PID" 2>/dev/null printf '%sBoth servers stopped.%s\n\n' "$OK" "$R" exit 0 } trap shutdown INT TERM say "" say "${B}3. Starting servers${R}" step "backend (FastAPI on :$BACKEND_PORT)" # setsid-style: run in its own process group so shutdown() can take down the reloader too. set -m python3 -m uvicorn app.api.main:app \ --app-dir "$BACKEND_DIR" \ --host 127.0.0.1 --port "$BACKEND_PORT" \ >"$BACKEND_LOG" 2>&1 & BACKEND_PID=$! set +m # Wait for it to actually answer — "process started" is not the same as "server ready". backend_ready="" for _ in $(seq 1 60); do if ! kill -0 "$BACKEND_PID" 2>/dev/null; then say "" tail -n 25 "$BACKEND_LOG" die "The backend exited during startup (log above)." "full log: $BACKEND_LOG" fi if curl -fsS "http://127.0.0.1:$BACKEND_PORT/api/health" >/dev/null 2>&1; then backend_ready=1; break fi sleep 0.5 done [ -n "$backend_ready" ] || { tail -n 25 "$BACKEND_LOG"; \ die "The backend did not respond within 30 seconds." "full log: $BACKEND_LOG"; } good "backend ready" step "frontend (Vite on :$FRONTEND_PORT)" set -m npm --prefix "$FRONTEND_DIR" run dev >"$FRONTEND_LOG" 2>&1 & FRONTEND_PID=$! set +m frontend_ready="" for _ in $(seq 1 60); do if ! kill -0 "$FRONTEND_PID" 2>/dev/null; then say "" tail -n 25 "$FRONTEND_LOG" die "The frontend exited during startup (log above)." "full log: $FRONTEND_LOG" fi if curl -fsS -o /dev/null "$DASHBOARD_URL" 2>/dev/null; then frontend_ready=1; break fi sleep 0.5 done [ -n "$frontend_ready" ] || { tail -n 25 "$FRONTEND_LOG"; \ die "The frontend did not respond within 30 seconds." "full log: $FRONTEND_LOG"; } good "frontend ready" # End-to-end check: the browser reaches the API *through* the Vite proxy, not directly. if curl -fsS -o /dev/null "$DASHBOARD_URL/api/sessions" 2>/dev/null; then good "dashboard is talking to the API" else warn "the API proxy did not answer — the dashboard may show loading errors" fi # --- 4. open it --------------------------------------------------------------------- say "" say "${B}4. Opening the dashboard${R}" open "$DASHBOARD_URL" 2>/dev/null && good "$DASHBOARD_URL" \ || warn "could not open a browser — go to $DASHBOARD_URL yourself" say "" say "${OK}${B} Dashboard is running.${R}" say "" say " Dashboard ${B}$DASHBOARD_URL${R}" say " API docs ${DIM}http://localhost:$BACKEND_PORT/docs${R}" say " Logs ${DIM}$LOG_DIR${R}" say "" say "${DIM} Keep this window open while you work.${R}" say "${DIM} Press Ctrl-C to stop both servers.${R}" say "" # Stay alive until a server dies or the user interrupts. while kill -0 "$BACKEND_PID" 2>/dev/null && kill -0 "$FRONTEND_PID" 2>/dev/null; do sleep 1 done say "" fail "A server stopped unexpectedly. Last lines of each log:" say "" say "${DIM}--- backend ---${R}"; tail -n 12 "$BACKEND_LOG" 2>/dev/null say "${DIM}--- frontend ---${R}"; tail -n 12 "$FRONTEND_LOG" 2>/dev/null shutdown