pf_app/pf.sh
Paul Trowbridge 0a2f0e50a1 Add pf.sh — interactive deployment and service management script
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-05-02 20:58:39 -04:00

345 lines
9.8 KiB
Bash
Executable File

#!/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" <<EOF
DATABASE_URL=${url}
PORT=${port}
PF_USER=${pf_user}
EOF
chmod 600 "$ENV_FILE"
success ".env written."
if db_ping; then
success "Database connection verified."
else
warn "Could not reach the database — double-check DATABASE_URL."
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 <<EOF
[Unit]
Description=Pivot Forecast App
After=network.target
[Service]
Type=simple
User=${run_user}
WorkingDirectory=${APP_DIR}
EnvironmentFile=${ENV_FILE}
ExecStart=${node_path} ${APP_DIR}/server.js
Restart=on-failure
RestartSec=5
StandardOutput=journal
StandardError=journal
SyslogIdentifier=${SERVICE_NAME}
[Install]
WantedBy=multi-user.target
EOF
$s systemctl daemon-reload
$s systemctl enable "$SERVICE_NAME"
success "Service installed and enabled."
echo " Start now with: ./pf.sh start"
}
cmd_uninstall_service() {
require_systemd
service_installed || { warn "Service not installed."; return; }
echo
warn "This will stop and remove the systemd service (does not touch app files)."
read -rp " Continue? [y/N] " confirm
[[ "$confirm" =~ ^[Yy]$ ]] || { echo "Aborted."; return; }
local s
s=$(sudo_if_needed)
$s systemctl stop "$SERVICE_NAME" 2>/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