#!/usr/bin/env bash set -euo pipefail # --------------------------------------------------------------------------- # pf.sh — Pivot Forecast management script # Usage: ./pf.sh [deploy|start|stop|restart|status|logs|db-setup|config] # ./pf.sh (interactive menu) # --------------------------------------------------------------------------- SERVICE_NAME="pf_app" APP_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SERVICE_FILE="/etc/systemd/system/${SERVICE_NAME}.service" ENV_FILE="${APP_DIR}/.env" MIN_NODE_MAJOR=20 # -- Colors ------------------------------------------------------------------ R='\033[0;31m'; G='\033[0;32m'; Y='\033[1;33m'; B='\033[0;34m'; NC='\033[0m' bold() { echo -e "\033[1m$*\033[0m"; } info() { echo -e "${B}==>${NC} $*"; } success() { echo -e "${G} ✓${NC} $*"; } warn() { echo -e "${Y} !${NC} $*"; } error() { echo -e "${R} ✗${NC} $*" >&2; } die() { error "$*"; exit 1; } # -- Helpers ----------------------------------------------------------------- require_systemd() { systemctl --version &>/dev/null || die "systemd not found on this system." } node_binary() { command -v node 2>/dev/null || true } check_node() { local node node=$(node_binary) [[ -z "$node" ]] && die "node not found. Install Node.js >= ${MIN_NODE_MAJOR}." local ver ver=$(node --version | sed 's/v//') local major="${ver%%.*}" if (( major < MIN_NODE_MAJOR )); then die "Node.js ${ver} found; requires >= ${MIN_NODE_MAJOR}. Please upgrade." fi success "Node.js ${ver}" } require_env() { [[ -f "$ENV_FILE" ]] || die ".env not found. Run: ./pf.sh config" } load_env() { require_env set -a; source "$ENV_FILE"; set +a } sudo_if_needed() { # Returns "sudo" if we're not root, empty string if we are [[ "$EUID" -eq 0 ]] && echo "" || echo "sudo" } service_installed() { [[ -f "$SERVICE_FILE" ]] } require_service() { service_installed || die "systemd service not installed. Run: ./pf.sh install-service" } db_ping() { load_env local url="${DATABASE_URL:-}" [[ -z "$url" ]] && { warn "DATABASE_URL not set in .env"; return 1; } # Use psql if available for a real connectivity check if command -v psql &>/dev/null; then psql "$url" -c "SELECT 1" &>/dev/null && return 0 || return 1 else warn "psql not in PATH — skipping live DB check" return 0 fi } # -- Commands ---------------------------------------------------------------- cmd_deploy() { echo; bold "Deploying Pivot Forecast" echo " App dir: $APP_DIR" echo check_node require_env info "Pulling latest from git…" git -C "$APP_DIR" pull info "Installing server dependencies…" npm --prefix "$APP_DIR" install --omit=dev info "Installing UI dependencies…" npm --prefix "$APP_DIR/ui" install info "Building UI…" npm --prefix "$APP_DIR/ui" run build if service_installed; then info "Restarting service…" cmd_restart else warn "Service not installed — server not started." echo " Run: ./pf.sh install-service" fi success "Deploy complete." } cmd_start() { require_systemd; require_service info "Starting ${SERVICE_NAME}…" $(sudo_if_needed) systemctl start "$SERVICE_NAME" success "Started." } cmd_stop() { require_systemd; require_service info "Stopping ${SERVICE_NAME}…" $(sudo_if_needed) systemctl stop "$SERVICE_NAME" success "Stopped." } cmd_restart() { require_systemd; require_service info "Restarting ${SERVICE_NAME}…" $(sudo_if_needed) systemctl restart "$SERVICE_NAME" success "Restarted." } cmd_status() { require_systemd echo bold "System service" if service_installed; then systemctl status "$SERVICE_NAME" --no-pager -l || true else warn "Service not installed — run: ./pf.sh install-service" fi echo bold "Database" if db_ping; then success "DB reachable" else error "DB not reachable (check DATABASE_URL in .env)" fi echo bold "Git" git -C "$APP_DIR" log -1 --format=" Commit: %h %s (%ar)" local branch branch=$(git -C "$APP_DIR" rev-parse --abbrev-ref HEAD) echo " Branch: $branch" } cmd_logs() { require_systemd; require_service info "Streaming logs (Ctrl-C to exit)…" journalctl -u "$SERVICE_NAME" -f --no-pager } cmd_db_setup() { require_env; load_env local url="${DATABASE_URL:-}" [[ -z "$url" ]] && die "DATABASE_URL not set in .env" command -v psql &>/dev/null || die "psql not found — install postgresql-client" echo bold "DB Setup — will run: setup_sql/01_schema.sql" warn "This creates the pf schema and tables. Safe to re-run (CREATE IF NOT EXISTS)." read -rp " Continue? [y/N] " confirm [[ "$confirm" =~ ^[Yy]$ ]] || { echo "Aborted."; return; } psql "$url" -f "${APP_DIR}/setup_sql/01_schema.sql" success "Schema applied." } cmd_config() { echo bold "Configure .env" echo " File: $ENV_FILE" echo local current_url="" local current_port="" local current_user="" if [[ -f "$ENV_FILE" ]]; then current_url=$(grep -E '^DATABASE_URL=' "$ENV_FILE" | cut -d= -f2- | tr -d '"' || true) current_port=$(grep -E '^PORT=' "$ENV_FILE" | cut -d= -f2- | tr -d '"' || true) current_user=$(grep -E '^PF_USER=' "$ENV_FILE" | cut -d= -f2- | tr -d '"' || true) fi read -rp " DATABASE_URL [${current_url:-not set}]: " input_url local url="${input_url:-$current_url}" [[ -z "$url" ]] && die "DATABASE_URL is required." read -rp " PORT [${current_port:-3010}]: " input_port local port="${input_port:-${current_port:-3010}}" read -rp " PF_USER [${current_user:-$USER}]: " input_user local pf_user="${input_user:-${current_user:-$USER}}" cat > "$ENV_FILE" < /dev/null </dev/null || true $s systemctl disable "$SERVICE_NAME" 2>/dev/null || true $s rm -f "$SERVICE_FILE" $s systemctl daemon-reload success "Service removed." } # -- Interactive menu -------------------------------------------------------- interactive_menu() { while true; do echo bold "Pivot Forecast — Management" echo " 1) deploy pull + install + build + restart" echo " 2) start start server" echo " 3) stop stop server" echo " 4) restart restart server" echo " 5) status service + DB + git info" echo " 6) logs tail journald logs" echo " 7) db-setup apply setup_sql/01_schema.sql" echo " 8) config set DATABASE_URL / PORT / PF_USER" echo " 9) install-service create systemd unit file" echo " 10) uninstall-service remove systemd unit file" echo " q) quit" echo read -rp " Choice: " choice case "$choice" in 1|deploy) cmd_deploy ;; 2|start) cmd_start ;; 3|stop) cmd_stop ;; 4|restart) cmd_restart ;; 5|status) cmd_status ;; 6|logs) cmd_logs ;; 7|db-setup) cmd_db_setup ;; 8|config) cmd_config ;; 9|install-service) cmd_install_service ;; 10|uninstall-service) cmd_uninstall_service ;; q|Q|quit|exit) echo "Bye."; exit 0 ;; *) warn "Unknown option: $choice" ;; esac done } # -- Dispatch ---------------------------------------------------------------- case "${1:-}" in deploy) cmd_deploy ;; start) cmd_start ;; stop) cmd_stop ;; restart) cmd_restart ;; status) cmd_status ;; logs) cmd_logs ;; db-setup) cmd_db_setup ;; config) cmd_config ;; install-service) cmd_install_service ;; uninstall-service) cmd_uninstall_service ;; "") interactive_menu ;; *) die "Unknown command: $1. Valid: deploy start stop restart status logs db-setup config install-service uninstall-service" ;; esac