#!/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="$(dirname -- "$HERE")" # scripts/ lives inside ar-aging-app/ BACKEND_DIR="$APP_DIR/backend" FRONTEND_DIR="$APP_DIR/frontend" VENV_DIR="$APP_DIR/.venv" 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" # Auto-answer prompts when stdin is not a Terminal (CI / Cursor / piped runs). INTERACTIVE=0 [ -t 0 ] && INTERACTIVE=1 ask_yes() { local prompt="$1" if [ "$INTERACTIVE" -eq 0 ]; then printf ' %s Y (non-interactive)\n' "$prompt" return 0 fi printf ' %s [Y/n] ' "$prompt" read -r reply case "${reply:-Y}" in [Nn]*) return 1 ;; *) return 0 ;; esac } # --- 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" if [ "$INTERACTIVE" -eq 1 ]; then printf '%sPress Return to close this window.%s\n' "$DIM" "$R" read -r _ fi 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 inside ar-aging-app/scripts/" 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)" # Isolated venv — required. Global site-packages (e.g. streamlit's Starlette 1.x) break # FastAPI 0.115's Router(on_startup=...) and make "import fastapi" look fine while the app dies. ensure_venv() { if [ ! -x "$VENV_DIR/bin/python" ]; then step "creating project virtualenv at $VENV_DIR…" python3 -m venv "$VENV_DIR" \ || die "Could not create the virtualenv." "python3 -m venv '$VENV_DIR'" good "virtualenv created" else good "virtualenv present" fi PYTHON="$VENV_DIR/bin/python" PIP="$VENV_DIR/bin/pip" } install_python_deps() { step "installing Python packages into the project venv…" "$PIP" install -q --upgrade pip \ || die "pip upgrade failed." "'$PIP' install --upgrade pip" "$PIP" install -q -r "$BACKEND_DIR/requirements.txt" \ || die "pip install failed — see the messages above." \ "'$PIP' install -r '$BACKEND_DIR/requirements.txt'" } backend_imports_ok() { "$PYTHON" -c "import fastapi, uvicorn, sqlalchemy, openpyxl, pymysql, dotenv" >/dev/null 2>&1 } # Prove the pin that Streamlit commonly breaks: Starlette must stay <0.42 for this FastAPI. backend_versions_ok() { "$PYTHON" -c " import fastapi, starlette from packaging.version import Version assert Version(fastapi.__version__) >= Version('0.115.0') assert Version(starlette.__version__) < Version('0.42.0'), starlette.__version__ " >/dev/null 2>&1 } backend_app_loads() { ( cd -- "$BACKEND_DIR" && "$PYTHON" -c "from app.api.main import app" ) >/dev/null 2>&1 } ensure_venv need_install=0 if ! backend_imports_ok; then warn "Python packages are missing or incomplete in the project venv." need_install=1 elif ! backend_versions_ok; then warn "Wrong Starlette/FastAPI versions in the venv (often after a global pip upgrade)." need_install=1 elif ! backend_app_loads; then warn "Backend failed to import — reinstalling pinned dependencies." need_install=1 fi if [ "$need_install" -eq 1 ]; then ask_yes "Install / repair Python packages now?" \ || die "Python dependencies are not installed." \ "'$PIP' install -r '$BACKEND_DIR/requirements.txt'" install_python_deps # packaging is used only for the version check; requirements may not list it. "$PIP" install -q packaging >/dev/null 2>&1 || true backend_imports_ok \ || die "Python packages are still incomplete after installing." "'$PIP' check" backend_versions_ok \ || die "Starlette is still too new for this FastAPI pin." \ "'$PIP' install 'starlette==0.41.3'" good "Python packages installed" else good "Python packages present (pinned versions)" fi # 1b. Which database? SQLite needs nothing; MySQL needs a server and credentials. The app # picks MySQL only when ar-aging-app/.env names a real host, so a machine with no # database installed still runs off the local file instead of refusing to start. ENV_FILE="$APP_DIR/.env" db_backend="$(cd -- "$BACKEND_DIR" && "$PYTHON" -c " from app.config import DB_BACKEND; print(DB_BACKEND)" 2>/dev/null)" db_label="$(cd -- "$BACKEND_DIR" && "$PYTHON" -c " from app.config import database_label; print(database_label())" 2>/dev/null)" if [ "$db_backend" = "mysql" ]; then good "database: $db_label" db_err="$(cd -- "$BACKEND_DIR" && "$PYTHON" -c " import sys, pymysql from app.config import MYSQL_HOST, MYSQL_PORT, MYSQL_USER, MYSQL_PASSWORD try: pymysql.connect(host=MYSQL_HOST, port=MYSQL_PORT, user=MYSQL_USER, password=MYSQL_PASSWORD, connect_timeout=6).close() except Exception as e: sys.stderr.write(str(e)); sys.exit(1) " 2>&1)" || { fail "Cannot reach the MySQL server named in $ENV_FILE" say "" printf ' %s\n' "$(printf '%s' "$db_err" | tail -n 2)" say "" say " Start the server and run this again, or fall back to the local file by setting" say " ${B}AR_DB_BACKEND=sqlite${R} in $ENV_FILE." say "" if [ "$INTERACTIVE" -eq 1 ]; then printf '%sPress Return to close this window.%s\n' "$DIM" "$R" read -r _ fi exit 1 } good "MySQL reachable" else good "database: ${db_label:-SQLite (local file)}" [ -f "$ENV_FILE" ] || warn "no .env — using the local file (fine for a demo or one user)" fi # 1d. Everything above is fine — now prove the app itself loads. if ! import_error="$(cd -- "$BACKEND_DIR" && "$PYTHON" -c "from app.api.main import app" 2>&1)"; then fail "The backend failed to load even though its packages and database are fine:" printf '%s\n' "$import_error" | tail -n 8 | sed 's/^/ /' die "This looks like a code or dependency-version problem." "'$PIP' check" fi good "backend loads cleanly" # 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 || true)" [ -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 ask_yes "Stop it and continue?" \ || die "Port $port is in use, so the dashboard cannot start." \ "quit the other program, or close the old dashboard window" for pid in $pids; do kill "$pid" 2>/dev/null || true; 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 || true)" ] && break done pids="$(lsof -ti tcp:"$port" -sTCP:LISTEN 2>/dev/null || true)" if [ -n "$pids" ]; then for pid in $pids; do kill -9 "$pid" 2>/dev/null || true; done sleep 1 fi [ -n "$(lsof -ti tcp:"$port" -sTCP:LISTEN 2>/dev/null || true)" ] \ && 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 || true [ -n "$BACKEND_PID" ] && kill -- "-$BACKEND_PID" 2>/dev/null || true sleep 0.5 [ -n "$FRONTEND_PID" ] && kill -9 -- "-$FRONTEND_PID" 2>/dev/null || true [ -n "$BACKEND_PID" ] && kill -9 -- "-$BACKEND_PID" 2>/dev/null || true 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)" # Own process group so shutdown() can take down the reloader children too. set -m "$PYTHON" -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 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" 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 " Python ${DIM}$PYTHON${R}" say "" say "${DIM} Keep this window open while you work.${R}" say "${DIM} Press Ctrl-C to stop both servers.${R}" say "" 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