#!/usr/bin/env bash # Start all Padelnomics dev processes with colored, labeled output. # # Usage: ./scripts/dev_run.sh # # On each start: resets the DB, runs migrations, seeds data, builds CSS, # optionally starts ngrok for Paddle webhook forwarding, then starts # app (port 5000), background worker, and CSS watcher. # Ctrl-C stops everything cleanly. set -euo pipefail cd "$(dirname "$0")/../.." # -- Colors & helpers -------------------------------------------------------- RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' BOLD='\033[1m' NC='\033[0m' COLOR_APP='\033[0;36m' # cyan COLOR_WORKER='\033[0;33m' # yellow COLOR_CSS='\033[0;35m' # magenta COLOR_NGROK='\033[0;32m' # green info() { echo -e "${BLUE}==>${NC} ${BOLD}$1${NC}"; } ok() { echo -e "${GREEN} ✓${NC} $1"; } warn() { echo -e "${YELLOW} !${NC} $1"; } fail() { echo -e "${RED} ✗${NC} $1"; exit 1; } # -- Preflight --------------------------------------------------------------- if [ ! -f .env ]; then fail ".env not found. Run ./scripts/dev_setup.sh first." fi # Load config from .env (|| true prevents set -e from aborting on empty values) DATABASE_PATH=$(grep '^DATABASE_PATH=' .env 2>/dev/null | cut -d= -f2- || true) DATABASE_PATH=${DATABASE_PATH:-data/app.db} PADDLE_API_KEY=$(grep '^PADDLE_API_KEY=' .env 2>/dev/null | cut -d= -f2- || true) PADDLE_NOTIFICATION_SETTING_ID=$(grep '^PADDLE_NOTIFICATION_SETTING_ID=' .env 2>/dev/null | cut -d= -f2- || true) PADDLE_ENVIRONMENT=$(grep '^PADDLE_ENVIRONMENT=' .env 2>/dev/null | cut -d= -f2- || true) PADDLE_ENVIRONMENT=${PADDLE_ENVIRONMENT:-sandbox} # -- Preparation ------------------------------------------------------------- info "Resetting database" rm -f "$DATABASE_PATH" ok "Removed $DATABASE_PATH" info "Running migrations" uv run python -m padelnomics.migrations.migrate ok "Migrations applied" info "Seeding development data" uv run python -m padelnomics.scripts.seed_dev_data ok "Dev data seeded" if [ -n "$PADDLE_API_KEY" ]; then info "Syncing Paddle products to DB" uv run python -m padelnomics.scripts.setup_paddle --sync ok "Paddle products synced" fi info "Building CSS" make css-build ok "CSS built" # -- Process management ------------------------------------------------------ PIDS=() cleanup() { echo "" echo -e "${BOLD}Stopping all processes...${NC}" for pid in "${PIDS[@]}"; do # Kill children first (e.g. make → tailwind), then the process itself pkill -P "$pid" 2>/dev/null || true kill "$pid" 2>/dev/null || true done wait 2>/dev/null || true echo "Done." exit 0 } trap cleanup SIGINT SIGTERM # Prefix each line of a command's output with a colored label. # Uses process substitution so $! is the actual command PID (not the formatter). run_with_label() { local color="$1" label="$2" shift 2 "$@" > >(while IFS= read -r line; do echo -e "${color}[${label}]${NC} ${line}"; done) 2>&1 & PIDS+=($!) } # -- Ngrok tunnel (if Paddle is configured) ---------------------------------- TUNNEL_URL="" if [ -n "$PADDLE_API_KEY" ] && [ -n "$PADDLE_NOTIFICATION_SETTING_ID" ]; then if command -v ngrok >/dev/null 2>&1; then info "Starting ngrok tunnel for Paddle webhooks" ngrok http 5000 --log=stdout --log-level=warn > /tmp/padelnomics-ngrok.log 2>&1 & NGROK_PID=$! PIDS+=($NGROK_PID) # Wait for ngrok to be ready (up to 10 seconds) WAIT_SECONDS=10 for i in $(seq 1 $WAIT_SECONDS); do TUNNEL_URL=$(curl -s http://localhost:4040/api/tunnels 2>/dev/null \ | python3 -c "import sys,json; print(json.load(sys.stdin)['tunnels'][0]['public_url'])" 2>/dev/null) \ && break sleep 1 done if [ -n "$TUNNEL_URL" ]; then ok "ngrok tunnel: $TUNNEL_URL" # Update Paddle notification destination with tunnel URL WEBHOOK_URL="${TUNNEL_URL}/billing/webhook/paddle" info "Updating Paddle webhook destination → $WEBHOOK_URL" uv run python -c " import os from dotenv import load_dotenv load_dotenv() from paddle_billing import Client, Environment, Options from paddle_billing.Resources.NotificationSettings.Operations import UpdateNotificationSetting env = Environment.SANDBOX if '${PADDLE_ENVIRONMENT}' == 'sandbox' else Environment.PRODUCTION paddle = Client('${PADDLE_API_KEY}', options=Options(env)) paddle.notification_settings.update( '${PADDLE_NOTIFICATION_SETTING_ID}', UpdateNotificationSetting(destination='${WEBHOOK_URL}'), ) print(' Updated.') " ok "Paddle webhooks → $WEBHOOK_URL" else warn "ngrok started but tunnel URL not available — webhooks won't reach localhost" fi else warn "ngrok not installed — Paddle webhooks won't reach localhost" warn "Install: https://ngrok.com/download (or brew install ngrok)" fi elif [ -n "$PADDLE_API_KEY" ]; then warn "PADDLE_NOTIFICATION_SETTING_ID not set — run setup_paddle to create webhook destination" fi # -- Start processes --------------------------------------------------------- echo "" echo -e "${BOLD}Starting Padelnomics dev environment${NC}" echo "" echo " app: http://localhost:5000" echo " admin: http://localhost:5000/admin" echo " login: http://localhost:5000/auth/dev-login?email=dev@localhost" if [ -n "$TUNNEL_URL" ]; then echo " tunnel: $TUNNEL_URL" fi echo "" echo "Press Ctrl-C to stop all processes." echo "" run_with_label "$COLOR_APP" "app " uv run granian --interface asgi --host 127.0.0.1 --port 5000 --reload --reload-paths web/src padelnomics.app:app run_with_label "$COLOR_WORKER" "worker" uv run python -u -m padelnomics.worker run_with_label "$COLOR_CSS" "css " make css-watch # Open a private/incognito browser window once the server is ready. # Polls /auth/dev-login until it responds, then launches the browser. ( DEV_URL="http://localhost:5000/auth/dev-login?email=dev@localhost" for i in $(seq 1 20); do sleep 0.5 if curl -s -o /dev/null -w "%{http_code}" "$DEV_URL" 2>/dev/null | grep -qE "^[23]"; then break fi done if command -v google-chrome >/dev/null 2>&1; then google-chrome --incognito "$DEV_URL" &>/dev/null & elif command -v chromium >/dev/null 2>&1; then chromium --incognito "$DEV_URL" &>/dev/null & elif command -v chromium-browser >/dev/null 2>&1; then chromium-browser --incognito "$DEV_URL" &>/dev/null & elif command -v firefox >/dev/null 2>&1; then firefox --private-window "$DEV_URL" &>/dev/null & fi ) & wait