#!/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" } # The keys cmd_config manages; anything else in .env is left alone. ENV_KEYS=(DB_HOST DB_PORT DB_NAME DB_USER DB_PASSWORD PORT) env_get() { [[ -f "$ENV_FILE" ]] || return 0 grep -E "^$1=" "$ENV_FILE" | tail -1 | cut -d= -f2- | tr -d '"' || true } # psql against the DB_* connection in .env; extra args are passed through. run_psql() { PGPASSWORD="${DB_PASSWORD:-}" psql \ -h "${DB_HOST:-localhost}" \ -p "${DB_PORT:-5432}" \ -U "${DB_USER}" \ -d "${DB_NAME}" \ "$@" } db_ping() { load_env if [[ -z "${DB_NAME:-}" || -z "${DB_USER:-}" ]]; then warn "DB_NAME / DB_USER not set in .env" return 1 fi # Use psql if available for a real connectivity check if command -v psql &>/dev/null; then run_psql -tAc "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 [[ -n "${DB_NAME:-}" && -n "${DB_USER:-}" ]] || die "DB_NAME / DB_USER not set in .env — run: ./pf.sh config" 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; } run_psql -v ON_ERROR_STOP=1 -f "${APP_DIR}/setup_sql/01_schema.sql" success "Schema applied." } cmd_config() { echo bold "Configure .env" echo " File: $ENV_FILE" echo local cur_host cur_port cur_name cur_user cur_pass cur_app_port cur_host=$(env_get DB_HOST) cur_port=$(env_get DB_PORT) cur_name=$(env_get DB_NAME) cur_user=$(env_get DB_USER) cur_pass=$(env_get DB_PASSWORD) cur_app_port=$(env_get PORT) local input read -rp " DB_HOST [${cur_host:-localhost}]: " input local host="${input:-${cur_host:-localhost}}" read -rp " DB_PORT [${cur_port:-5432}]: " input local port="${input:-${cur_port:-5432}}" read -rp " DB_NAME [${cur_name:-not set}]: " input local name="${input:-$cur_name}" [[ -z "$name" ]] && die "DB_NAME is required." read -rp " DB_USER [${cur_user:-$USER}]: " input local user="${input:-${cur_user:-$USER}}" if [[ -n "$cur_pass" ]]; then read -rsp " DB_PASSWORD [keep existing]: " input; echo else read -rsp " DB_PASSWORD: " input; echo fi local pass="${input:-$cur_pass}" read -rp " PORT (app) [${cur_app_port:-3010}]: " input local app_port="${input:-${cur_app_port:-3010}}" # Rewrite the managed keys, carrying over any other lines already in .env. local tmp tmp=$(mktemp) cat > "$tmp" <> "$tmp" || true fi mv "$tmp" "$ENV_FILE" chmod 600 "$ENV_FILE" success ".env written." if db_ping; then success "Database connection verified." else warn "Could not reach the database — double-check the DB_* settings." fi } cmd_install_service() { require_systemd require_env local node_path node_path=$(node_binary) [[ -z "$node_path" ]] && die "node not found — install Node.js first." local run_user="$USER" local s s=$(sudo_if_needed) echo bold "Install systemd service" echo " Service file : $SERVICE_FILE" echo " Run as user : $run_user" echo " App dir : $APP_DIR" echo " Node binary : $node_path" echo read -rp " Continue? [y/N] " confirm [[ "$confirm" =~ ^[Yy]$ ]] || { echo "Aborted."; return; } $s tee "$SERVICE_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 DB connection + app PORT" 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