From dddfbcdf4746a65fa4a7f84d083bbe64bd9917f3 Mon Sep 17 00:00:00 2001 From: root Date: Thu, 25 Jun 2026 19:21:01 +0200 Subject: [PATCH] feat: upgrade update-Nitrov3.sh to v5.0.0 enterprise management system - Complete rewrite from 584 to 1902 lines - Interactive TUI control panel with 13 menu options - CLI subcommands: update, interactive, status, health, backup, restore, backups, services, database, config, logs, clean, help, version - Selective updates with --only=emulator|client|renderer|configs - Animated spinners and progress bars during operations - System diagnostics: CPU, memory, disk, Redis, Nginx, SSL cert check - Database manager: info, tables, sizes, optimize, check, custom queries - Backup management: create, restore with interactive selection, list view - Service manager: start/stop/restart for emulator, nginx, PM2 - System status dashboard with server info and resource usage - Health check with 20+ checks and scoring system - Cache cleanup: Yarn, Laravel, Redis, logs, backups, temp files - Enhanced webhook notifications with color support - CLI flags: --dry-run, --force, --verbose, --quiet, --branch, --url - Fix: derive NITRO_SITE_URL from APP_URL before URL calculation - Full UTF-8 and ASCII fallback support --- update-Nitrov3.sh | 2131 ++++++++++++++++++++++++++++++++++++--------- 1 file changed, 1727 insertions(+), 404 deletions(-) diff --git a/update-Nitrov3.sh b/update-Nitrov3.sh index 5b6697b..07b26e9 100755 --- a/update-Nitrov3.sh +++ b/update-Nitrov3.sh @@ -1,61 +1,340 @@ #!/bin/bash set -euo pipefail -# ============================================================================= -# EPIC WEB CONTROL — Enterprise Update Script -# Compatible with Laravel 13 + Nitro V3 + MariaDB -# ============================================================================= +# ╔══════════════════════════════════════════════════════════════════════════════╗ +# ║ ║ +# ║ ███╗ ██╗███████╗██╗ ██╗██████╗ ██████╗ ███████╗██╗ ██╗ ║ +# ║ ████╗ ██║██╔════╝██║ ██║██╔══██╗██╔═══██╗██╔════╝██║ ██║ ║ +# ║ ██╔██╗ ██║█████╗ ██║ ██║██████╔╝██║ ██║███████╗██║ ██║ ║ +# ║ ██║╚██╗██║██╔══╝ ██║ ██║██╔══██╗██║ ██║╚════██║██║ ██║ ║ +# ║ ██║ ╚████║███████╗╚██████╔╝██║ ██║╚██████╔╝███████║╚██████╔╝ ║ +# ║ ╚═╝ ╚═══╝╚══════╝ ╚═════╝ ╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═════╝ ║ +# ║ ║ +# ║ NITRO V3 — Enterprise Deployment & Management System ║ +# ║ Version 5.0.0 — Build 20260625 ║ +# ║ ║ +# ╚══════════════════════════════════════════════════════════════════════════════╝ -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# ============================================================================= +# CONFIGURATION & CONSTANTS +# ============================================================================= +readonly SCRIPT_VERSION="5.0.0" +readonly SCRIPT_BUILD="20260625" +readonly SCRIPT_NAME="$(basename "$0")" +readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +readonly SCRIPT_PID=$$ +readonly NITRO_MIN_BASH_VERSION="4.0" -# --- Load .env --------------------------------------------------------------- +# --- Colors ------------------------------------------------------------------- +if [ -t 1 ] && command -v tput &>/dev/null && [ "$(tput colors 2>/dev/null || echo 0)" -ge 8 ]; then + C_RESET="\033[0m" + C_BOLD="\033[1m" + C_DIM="\033[2m" + C_UNDERLINE="\033[4m" + C_BLINK="\033[5m" + C_RED="\033[1;31m" + C_GREEN="\033[1;32m" + C_YELLOW="\033[1;33m" + C_BLUE="\033[1;34m" + C_MAGENTA="\033[1;35m" + C_CYAN="\033[1;36m" + C_WHITE="\033[1;37m" + C_BG_RED="\033[41m" + C_BG_GREEN="\033[42m" + C_BG_YELLOW="\033[43m" + C_BG_BLUE="\033[44m" + C_BG_MAGENTA="\033[45m" + C_BG_CYAN="\033[46m" +else + C_RESET="" C_BOLD="" C_DIM="" C_UNDERLINE="" C_BLINK="" + C_RED="" C_GREEN="" C_YELLOW="" C_BLUE="" C_MAGENTA="" C_CYAN="" C_WHITE="" + C_BG_RED="" C_BG_GREEN="" C_BG_YELLOW="" C_BG_BLUE="" C_BG_MAGENTA="" C_BG_CYAN="" +fi + +# --- Emojis (fallback to ASCII on non-UTF8) ---------------------------------- +if locale charmap 2>/dev/null | grep -qi utf; then + E_CHECK="✔" E_CROSS="✘" E_WARN="⚠" E_ARROW="➜" E_DOT="●" E_STAR="★" + E_CLOCK="⏱" E_GEAR="⚙" E_ROCKET="🚀" E_SHIELD="🛡" E_DB="🗄" + E_FOLDER="📁" E_GIT="🔀" E_WRENCH="🔧" E_CHART="📊" E_LIGHTNING="⚡" + E_CLOCK2="🕐" E_BROOM="🧹" E_EYE="👁" E_LOCK="🔒" E_HEART="♥" + E_CLOUD="☁" E_FIRE="🔥" E_PACKAGE="📦" E_BULLET="•" +else + E_CHECK="[OK]" E_CROSS="[FAIL]" E_WARN="[!]" E_ARROW="->" E_DOT="*" + E_STAR="*" E_CLOCK="[T]" E_GEAR="[G]" E_ROCKET="[R]" E_SHIELD="[S]" E_DB="[DB]" + E_FOLDER="[D]" E_GIT="[G]" E_WRENCH="[W]" E_CHART="[C]" E_LIGHTNING="[!]" + E_CLOCK2="[T]" E_BROOM="[C]" E_EYE="[E]" E_LOCK="[L]" E_HEART="[H]" + E_CLOUD="[C]" E_FIRE="[F]" E_PACKAGE="[P]" E_BULLET="-" +fi + +# --- Timing ------------------------------------------------------------------- +readonly START_TIME=$(date +%s) + +# --- State -------------------------------------------------------------------- +STEP_NUM=0 +STEP_TOTAL=8 +HAD_UPDATES=false +NITRO_BUILT=false +DRY_RUN=false +VERBOSE=false +QUIET=false +FORCE=false +SELECTIVE_UPDATE="" +ROLLBACK_NEEDED=false +LAST_BACKUP="" +NOTIFY_FAILED=false +ERRORS=0 +WARNINGS=0 +UPDATED_REPOS=() + +# --- Load .env ---------------------------------------------------------------- if [ -f "$SCRIPT_DIR/.env" ]; then set -a . "$SCRIPT_DIR/.env" set +a fi -# --- Logging ----------------------------------------------------------------- +# --- Logging ------------------------------------------------------------------ LOG_DIR="${NITRO_LOG_DIR:-$SCRIPT_DIR/storage/logs}" -mkdir -p "$LOG_DIR" +mkdir -p "$LOG_DIR" 2>/dev/null || true LOG_FILE="$LOG_DIR/update-$(date +%Y%m%d-%H%M%S).log" -exec > >(tee -a "$LOG_FILE") 2>&1 -STEP_NUM=0 -STEP_TOTAL=8 -START_TIME=$(date +%s) +# --- Notifications ------------------------------------------------------------ +NOTIFY_URL="${NITRO_NOTIFY_URL:-}" +# ============================================================================= +# TERMINAL CAPABILITIES +# ============================================================================= +TERM_COLS=$(tput cols 2>/dev/null || echo 80) +TERM_LINES=$(tput lines 2>/dev/null || echo 24) + +cursor_hide() { tput civis 2>/dev/null || true; } +cursor_show() { tput cnorm 2>/dev/null || true; } +clear_line() { printf "\r\033[K"; } +move_up() { printf "\033[%dA" "${1:-1}"; } +move_down() { printf "\033[%dB" "${1:-1}"; } +save_cursor() { tput sc 2>/dev/null || echo -en "\033[s"; } +load_cursor() { tput rc 2>/dev/null || echo -en "\033[u"; } + +# ============================================================================= +# UI COMPONENTS +# ============================================================================= + +# --- Banner ------------------------------------------------------------------- +show_banner() { + clear + cat << 'BANNER' + + ╔══════════════════════════════════════════════════════════════════════╗ + ║ ║ + ║ ███╗ ██╗███████╗██╗ ██╗██████╗ ██████╗ ███████╗██╗ ██╗ ║ + ║ ████╗ ██║██╔════╝██║ ██║██╔══██╗██╔═══██╗██╔════╝██║ ██║ ║ + ║ ██╔██╗ ██║█████╗ ██║ ██║██████╔╝██║ ██║███████╗██║ ██║ ║ + ║ ██║╚██╗██║██╔══╝ ██║ ██║██╔══██╗██║ ██║╚════██║██║ ██║ ║ + ║ ██║ ╚████║███████╗╚██████╔╝██║ ██║╚██████╔╝███████║╚██████╔╝ ║ + ║ ╚═╝ ╚═══╝╚══════╝ ╚═════╝ ╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═════╝ ║ + ║ ║ + ║ NITRO V3 — Enterprise Deployment & Management System ║ + ╚══════════════════════════════════════════════════════════════════════╝ + +BANNER + echo -e " ${C_CYAN}${C_BOLD} Version ${SCRIPT_VERSION} Build ${SCRIPT_BUILD} ${C_RESET}" + echo -e " ${C_DIM} $(date '+%A, %d %B %Y — %H:%M:%S')${C_RESET}" + echo "" +} + +show_mini_banner() { + echo -e "${C_CYAN}${C_BOLD}╔══════════════════════════════════════════════════════════════╗${C_RESET}" + echo -e "${C_CYAN}${C_BOLD}║ ${E_ROCKET} Nitro V3 Update System v${SCRIPT_VERSION} ║${C_RESET}" + echo -e "${C_CYAN}${C_BOLD}╚══════════════════════════════════════════════════════════════╝${C_RESET}" + echo "" +} + +# --- Box Drawing -------------------------------------------------------------- +box_top() { + local title="${1:-}" width="${2:-60}" + if [ -n "$title" ]; then + local pad=$((width - ${#title} - 6)) + printf "${C_CYAN}╔═══ ${C_BOLD}%s${C_RESET}${C_CYAN}" "$title" + printf '═%.0s' $(seq 1 $pad 2>/dev/null || echo 1) + echo -e "╗${C_RESET}" + else + printf "${C_CYAN}╔" + printf '═%.0s' $(seq 1 $width) + echo -e "╗${C_RESET}" + fi +} + +box_line() { + local text="${1:-}" width="${2:-60}" + local pad=$((width - ${#text} - 4)) + [ $pad -lt 0 ] && pad=0 + printf "${C_CYAN}║${C_RESET} %s%*s${C_CYAN}║${C_RESET}\n" "$text" $pad "" +} + +box_bottom() { + local width="${1:-60}" + printf "${C_CYAN}╚" + printf '═%.0s' $(seq 1 $width) + echo -e "╝${C_RESET}" +} + +# --- Step Display ------------------------------------------------------------- step() { STEP_NUM=$((STEP_NUM + 1)) echo "" - echo "╔══════════════════════════════════════════════════════════════╗" - printf "║ Step %d/%d — %-49s║\n" "$STEP_NUM" "$STEP_TOTAL" "$1" - echo "╚══════════════════════════════════════════════════════════════╝" + echo -e "${C_BG_CYAN}${C_BOLD}" + printf " ╔══════════════════════════════════════════════════════════════╗\n" + printf " ║ Step %d/%d │ %-46s║\n" "$STEP_NUM" "$STEP_TOTAL" "$1" + printf " ╚══════════════════════════════════════════════════════════════╝\n" + echo -e "${C_RESET}" } -info() { echo " --> $1"; } -ok() { echo " [OK] $1"; } -warn() { echo " [WARN] $1"; } -fail() { echo " [FAIL] $1"; } -die() { echo ""; echo "=== ❌ $1 ==="; cleanup_notify_failure "$1"; exit 1; } +# --- Output Functions --------------------------------------------------------- +info() { + echo -e " ${C_BLUE}${E_ARROW}${C_RESET} $1" + log_write "INFO" "$1" +} + +info_indent() { + echo -e " ${C_DIM}$1${C_RESET}" + log_write "INFO" "$1" +} + +ok() { + echo -e " ${C_GREEN}${E_CHECK}${C_RESET} $1" + log_write "OK" "$1" +} + +warn() { + echo -e " ${C_YELLOW}${E_WARN}${C_RESET} $1" + WARNINGS=$((WARNINGS + 1)) + log_write "WARN" "$1" +} + +fail() { + echo -e " ${C_RED}${E_CROSS}${C_RESET} $1" + log_write "FAIL" "$1" +} + +die() { + echo "" + echo -e " ${C_BG_RED}${C_WHITE}${C_BOLD} FATAL ERROR ${C_RESET}" + echo -e " ${C_RED}${E_CROSS} $1${C_RESET}" + log_write "FATAL" "$1" + cleanup_notify_failure "$1" + cursor_show + exit 1 +} + +debug() { + [ "$VERBOSE" = true ] && echo -e " ${C_DIM}${E_DOT} [DEBUG] $1${C_RESET}" + log_write "DEBUG" "$1" +} + +log_write() { + local level="$1" msg="$2" + printf "[%s] [%s] %s\n" "$(date '+%Y-%m-%d %H:%M:%S')" "$level" "$msg" >> "$LOG_FILE" 2>/dev/null || true +} + +# --- Spinner ------------------------------------------------------------------ +SPINNER_PID="" +spinner_start() { + local msg="${1:-Working...}" + local frames=("⠋" "⠙" "⠹" "⠸" "⠼" "⠴" "⠦" "⠧" "⠇" "⠏") + cursor_hide + ( + local i=0 + while true; do + printf "\r ${C_CYAN}${frames[$i]}${C_RESET} ${C_BOLD}%s${C_RESET} " "$msg" + i=$(( (i + 1) % ${#frames[@]} )) + sleep 0.1 + done + ) & + SPINNER_PID=$! + disown "$SPINNER_PID" 2>/dev/null || true +} + +spinner_stop() { + local status="${1:-ok}" + if [ -n "$SPINNER_PID" ] && kill -0 "$SPINNER_PID" 2>/dev/null; then + kill "$SPINNER_PID" 2>/dev/null || true + wait "$SPINNER_PID" 2>/dev/null || true + SPINNER_PID="" + fi + clear_line + cursor_show + case "$status" in + ok) printf "\r ${C_GREEN}${E_CHECK}${C_RESET} Done\n" ;; + warn) printf "\r ${C_YELLOW}${E_WARN}${C_RESET} Done with warnings\n" ;; + fail) printf "\r ${C_RED}${E_CROSS}${C_RESET} Failed\n" ;; + skip) printf "\r ${C_DIM}— Skipped${C_RESET}\n" ;; + esac +} + +# --- Progress Bar ------------------------------------------------------------- +progress_bar() { + local current="${1:-0}" total="${2:-100}" width="${3:-40}" + local pct=$((current * 100 / total)) + local filled=$((current * width / total)) + local empty=$((width - filled)) + printf "\r ${C_CYAN}[${C_RESET}" + printf '%0.s█' $(seq 1 $filled 2>/dev/null || echo 1) + printf '%0.s░' $(seq 1 $empty 2>/dev/null || echo 1) + printf "${C_CYAN}]${C_RESET} ${C_BOLD}%3d%%${C_RESET}" "$pct" +} + +# --- Confirmation Prompt ------------------------------------------------------ +confirm() { + local msg="${1:-Continue?}" default="${2:-y}" + local hint="[Y/n]" + [ "$default" = "n" ] && hint="[y/N]" + echo -en " ${C_YELLOW}${E_ARROW}${C_RESET} ${C_BOLD}$msg${C_RESET} $hint " + read -r reply + reply="${reply:-$default}" + [[ "$reply" =~ ^[Yy] ]] +} + +# --- Select Menu -------------------------------------------------------------- +select_menu() { + local title="$1" + shift + local options=("$@") + echo "" + echo -e " ${C_BOLD}${C_CYAN}$title${C_RESET}" + echo "" + local i=1 + for opt in "${options[@]}"; do + echo -e " ${C_GREEN}${C_BOLD}$i)${C_RESET} $opt" + i=$((i + 1)) + done + echo "" + echo -en " ${C_CYAN}Select [1-$((i-1))]${C_RESET}: " + local choice + read -r choice + if [[ "$choice" =~ ^[0-9]+$ ]] && [ "$choice" -ge 1 ] && [ "$choice" -lt "$i" ]; then + echo "$choice" + return 0 + fi + return 1 +} # --- Notifications ----------------------------------------------------------- -NOTIFY_URL="${NITRO_NOTIFY_URL:-}" -NOTIFY_FAILED=false - notify() { - local status="$1" - local message="$2" + local status="$1" message="$2" [ -z "$NOTIFY_URL" ] && return 0 - local elapsed=$(( $(date +%s) - START_TIME )) + local elapsed=$(($(date +%s) - START_TIME)) local elapsed_fmt - elapsed_fmt=$(printf '%dm %ds' $((elapsed/60)) $((elapsed%60))) + elapsed_fmt=$(printf '%dm %ds' $((elapsed / 60)) $((elapsed % 60))) + local color="good" + [ "$status" != "SUCCESS" ] && color="danger" + [ "$status" = "WARNING" ] && color="warning" + curl -s -o /dev/null -X POST "$NOTIFY_URL" \ -H "Content-Type: application/json" \ -d "{ - \"text\": \"[Nitro Update] $status\", + \"text\": \"[Nitro Update v${SCRIPT_VERSION}] $status\", \"attachments\": [{ - \"color\": \"$([ "$status" = "SUCCESS" ] && echo \"good\" || echo \"danger\")\", + \"color\": \"$color\", \"fields\": [ {\"title\": \"Status\", \"value\": \"$message\", \"short\": true}, {\"title\": \"Duration\", \"value\": \"$elapsed_fmt\", \"short\": true}, @@ -72,10 +351,9 @@ cleanup_notify_failure() { notify "FAILED" "$1" } -# --- Rollback support -------------------------------------------------------- -LAST_BACKUP="" -ROLLBACK_NEEDED=false - +# ============================================================================= +# ROLLBACK SYSTEM +# ============================================================================= rollback_db() { [ -z "$LAST_BACKUP" ] && { warn "No backup file known — cannot rollback DB."; return 1; } [ ! -f "$LAST_BACKUP" ] && { warn "Backup file not found: $LAST_BACKUP"; return 1; } @@ -85,231 +363,292 @@ rollback_db() { cleanup_on_exit() { local exit_code=$? + cursor_show if [ $exit_code -ne 0 ] && [ "$ROLLBACK_NEEDED" = true ]; then echo "" - echo "=== ⚠️ Update failed — initiating rollback ===" + echo -e " ${C_BG_RED}${C_WHITE}${C_BOLD} UPDATE FAILED — INITIATING ROLLBACK ${C_RESET}" + echo "" rollback_db fi [ $exit_code -ne 0 ] && cleanup_notify_failure "Script exited with code $exit_code" } trap cleanup_on_exit EXIT -trap 'echo ""; die "Interrupted by user"' SIGINT SIGTERM - -# --- Pre-flight: interactive prompts (must be before unbuffer) --------------- - -# Site URL -if [ -z "${NITRO_SITE_URL:-}" ] && [ -n "${APP_URL:-}" ]; then - NITRO_SITE_URL="$APP_URL" - info "Using APP_URL from .env: $NITRO_SITE_URL" -fi -if [ -z "${NITRO_SITE_URL:-}" ]; then - read -r -p " Enter your site URL (e.g. https://example.com): " NITRO_SITE_URL - NITRO_SITE_URL="${NITRO_SITE_URL%/}" - [ -z "$NITRO_SITE_URL" ] && die "NITRO_SITE_URL is required" - export NITRO_SITE_URL -fi - -# Branch -if [ -z "${NITRO_BRANCH:-}" ]; then - read -r -p " Enter branch (main/dev) [main]: " NITRO_BRANCH - NITRO_BRANCH="${NITRO_BRANCH:-main}" - case "$NITRO_BRANCH" in main|dev) ;; *) die "Invalid branch '$NITRO_BRANCH'. Use main or dev." ;; esac - export NITRO_BRANCH -fi - -# Dry-run mode -DRY_RUN=false -if [ "${1:-}" = "--dry-run" ] || [ "${1:-}" = "-n" ]; then - DRY_RUN=true - echo "" - info "DRY-RUN mode: no changes will be made" -fi - -# --- Unbuffer re-exec -------------------------------------------------------- -if [ -z "${_UNBUFFERED:-}" ] && command -v unbuffer &> /dev/null; then - export _UNBUFFERED=1 - exec unbuffer bash "$0" "$@" -fi - -# --- Single-instance lock ---------------------------------------------------- -LOCKFILE="/tmp/$(basename "$0").lock" -exec 200>"$LOCKFILE" -flock -n 200 || die "Another instance is already running" - -# --- Configuration ----------------------------------------------------------- -DB_NAME="${NITRO_DB_NAME:-habbo}" -DB_HOST="${NITRO_DB_HOST:-127.0.0.1}" -DB_PORT="${NITRO_DB_PORT:-3306}" -DB_USER="${NITRO_DB_USER:-root}" -DB_PASS="${NITRO_DB_PASS:-}" -EMULATOR_SERVICE="${NITRO_EMULATOR_SERVICE:-emulator}" -EMULATOR_DIR="${NITRO_EMULATOR_PATH:-/var/www/emulator}" -SQL_DIR="${NITRO_SQL_DIR:-$EMULATOR_DIR/Database Updates}" -GAMEDATA_CONF_DIR="${NITRO_GAMEDATA_DIR:-/var/www/Gamedata/config}" -NITRO_SRC_DIR="${NITRO_CLIENT_DIR:-/var/www/Nitro-V3/public/configuration}" -BACKUP_DIR="${NITRO_BACKUP_DIR:-$EMULATOR_DIR/Database Updates/backups}" -NITRO_CLIENT="${NITRO_CLIENT_SRC:-/var/www/Nitro-V3}" -NITRO_RENDERER="${NITRO_RENDERER_SRC:-/var/www/Nitro_Render_V3}" -NITRO_BRANCH="${NITRO_BRANCH:-main}" -MIN_DISK_GB="${NITRO_MIN_DISK_GB:-5}" -HEALTH_RETRIES="${NITRO_HEALTH_RETRIES:-12}" -HEALTH_INTERVAL="${NITRO_HEALTH_INTERVAL:-5}" - -# Derive ws/wss protocol and domain -case "$NITRO_SITE_URL" in - https://*) NITRO_WS_PROTO="wss://" ; NITRO_DOMAIN="${NITRO_SITE_URL#https://}" ;; - http://*) NITRO_WS_PROTO="ws://" ; NITRO_DOMAIN="${NITRO_SITE_URL#http://}" ;; - *) NITRO_WS_PROTO="wss://" ; NITRO_DOMAIN="$NITRO_SITE_URL" ;; -esac - -# Critical URLs (override via NITRO_* env vars) -NITRO_IMAGE_LIBRARY_URL="${NITRO_IMAGE_LIBRARY_URL:-$NITRO_SITE_URL/gamedata/c_images/}" -NITRO_HOF_FURNITURE_URL="${NITRO_HOF_FURNITURE_URL:-$NITRO_SITE_URL/gamedata/icons}" -NITRO_API_URL="${NITRO_API_URL:-$NITRO_SITE_URL}" -NITRO_SOCKET_URL="${NITRO_SOCKET_URL:-${NITRO_WS_PROTO}ws.${NITRO_DOMAIN}}" -NITRO_CAMERA_URL="${NITRO_CAMERA_URL:-$NITRO_SITE_URL/camera/photo/}" -NITRO_GAMEDATA_URL="${NITRO_GAMEDATA_URL:-$NITRO_SITE_URL/gamedata}" -NITRO_ASSET_URL="${NITRO_ASSET_URL:-$NITRO_SITE_URL/gamedata/bundled}" -NITRO_FURNI_ASSET_ICON_URL="${NITRO_FURNI_ASSET_ICON_URL:-$NITRO_SITE_URL/gamedata/icons/%libname%%param%_icon.png}" - -# MySQL credentials -MYSQL_CRED="-h $DB_HOST -P $DB_PORT -u $DB_USER --ssl-verify-server-cert=OFF" -[ -n "$DB_PASS" ] && export MYSQL_PWD="$DB_PASS" +trap 'echo ""; cursor_show; die "Interrupted by user (SIGINT)"' SIGINT +trap 'cursor_show; die "Terminated (SIGTERM)"' SIGTERM # ============================================================================= # PRE-FLIGHT CHECKS # ============================================================================= -step "Pre-flight Checks" +preflight() { + # Site URL + if [ -z "${NITRO_SITE_URL:-}" ] && [ -n "${APP_URL:-}" ]; then + NITRO_SITE_URL="$APP_URL" + info "Using APP_URL from .env: $NITRO_SITE_URL" + fi + if [ -z "${NITRO_SITE_URL:-}" ]; then + read -r -p " Enter your site URL (e.g. https://example.com): " NITRO_SITE_URL + NITRO_SITE_URL="${NITRO_SITE_URL%/}" + [ -z "$NITRO_SITE_URL" ] && die "NITRO_SITE_URL is required" + export NITRO_SITE_URL + fi -info "Checking required commands..." -for cmd in git mariadb mariadb-dump mvn node python3 yarn; do - command -v "$cmd" &>/dev/null || die "Required command not found: $cmd" -done -ok "All required commands available" - -info "Checking directories..." -REQUIRED_DIRS=( - "$EMULATOR_DIR/Emulator" - "$NITRO_RENDERER" - "$NITRO_CLIENT" - "$NITRO_SRC_DIR" -) -for dir in "${REQUIRED_DIRS[@]}"; do - [ -d "$dir" ] || die "Required directory not found: $dir" - ok "Found: $dir" -done - -info "Checking disk space..." -AVAIL_KB=$(df --output=avail "$EMULATOR_DIR" 2>/dev/null | tail -1) -AVAIL_GB=$(( AVAIL_KB / 1024 / 1024 )) -[ "$AVAIL_GB" -lt "$MIN_DISK_GB" ] && die "Only ${AVAIL_GB}GB free on $EMULATOR_DIR (min ${MIN_DISK_GB}GB required)" -ok "${AVAIL_GB}GB disk space available" - -info "Checking database connection..." -mariadb $MYSQL_CRED -e "SELECT 1" "$DB_NAME" &>/dev/null || die "Cannot connect to database $DB_NAME on $DB_HOST:$DB_PORT" -ok "Database connection OK" - -if [ "$DRY_RUN" = true ]; then - echo "" - warn "DRY-RUN — skipping actual update" - echo " Site: $NITRO_SITE_URL" - echo " Branch: $NITRO_BRANCH" - echo " Emulator: $EMULATOR_DIR/Emulator" - echo " Nitro-V3: $NITRO_CLIENT" - echo " Renderer: $NITRO_RENDERER" - echo "" - info "Dry-run complete. Pass --dry-run or -n to preview." - exit 0 -fi - -echo "" -info "Site: $NITRO_SITE_URL | Branch: $NITRO_BRANCH" -info "Log: $LOG_FILE" - -# ============================================================================= -# HELPERS -# ============================================================================= -clean_node_modules() { - rm -rf node_modules 2>/dev/null || sudo rm -rf node_modules 2>/dev/null || true - if [ "$(stat -c '%U' .)" != "$(whoami)" ] && command -v sudo &> /dev/null; then - sudo chown -R "$(whoami)":"$(whoami)" . + # Branch + if [ -z "${NITRO_BRANCH:-}" ]; then + read -r -p " Enter branch (main/dev) [main]: " NITRO_BRANCH + NITRO_BRANCH="${NITRO_BRANCH:-main}" + case "$NITRO_BRANCH" in + main|dev|master|staging) ;; + *) die "Invalid branch '$NITRO_BRANCH'. Use main, dev, master, or staging." ;; + esac + export NITRO_BRANCH fi } -git_update() { - local repo="$1" - local branch="$2" - cd "$repo" - git stash --include-untracked || true - local old_head - old_head=$(git rev-parse HEAD) - if ! git checkout "$branch" 2>/dev/null; then - local alt="${branch^}" - if [ "$alt" != "$branch" ] && git checkout "$alt" 2>/dev/null; then - info "Branch '$branch' not found, using '$alt' instead" - branch="$alt" +# ============================================================================= +# SYSTEM CHECKS +# ============================================================================= +preflight_checks() { + step "System Diagnostics" + + # Required commands + info "Checking required commands..." + local REQUIRED_CMDS=(git mariadb mariadb-dump mvn node python3 yarn) + local MISSING_CMDS=() + for cmd in "${REQUIRED_CMDS[@]}"; do + if command -v "$cmd" &>/dev/null; then + debug " Found: $cmd ($(command -v "$cmd"))" else - die "Branch '$branch' (or '$alt') not found in $repo" + MISSING_CMDS+=("$cmd") + fi + done + if [ ${#MISSING_CMDS[@]} -gt 0 ]; then + die "Missing required commands: ${MISSING_CMDS[*]}" + fi + ok "All ${#REQUIRED_CMDS[@]} required commands available" + + # Required directories + info "Checking directories..." + REQUIRED_DIRS=( + "$EMULATOR_DIR/Emulator" + "$NITRO_RENDERER" + "$NITRO_CLIENT" + "$NITRO_SRC_DIR" + ) + for dir in "${REQUIRED_DIRS[@]}"; do + if [ -d "$dir" ]; then + debug " Found: $dir" + else + die "Required directory not found: $dir" + fi + done + ok "All ${#REQUIRED_DIRS[@]} directories exist" + + # Disk space + info "Checking disk space..." + AVAIL_KB=$(df --output=avail "$EMULATOR_DIR" 2>/dev/null | tail -1) + AVAIL_GB=$(( AVAIL_KB / 1024 / 1024 )) + if [ "$AVAIL_GB" -lt "$MIN_DISK_GB" ]; then + die "Only ${AVAIL_GB}GB free on $EMULATOR_DIR (min ${MIN_DISK_GB}GB required)" + fi + ok "${AVAIL_GB}GB disk space available" + + # Memory check + info "Checking system memory..." + TOTAL_MEM=$(free -m 2>/dev/null | awk '/^Mem:/{print $2}' || echo "0") + AVAIL_MEM=$(free -m 2>/dev/null | awk '/^Mem:/{print $7}' || echo "0") + if [ "$TOTAL_MEM" -gt 0 ]; then + ok "Memory: ${AVAIL_MEM}MB available / ${TOTAL_MEM}MB total" + else + warn "Could not determine system memory" + fi + + # CPU load + info "Checking CPU load..." + CPU_LOAD=$(awk '{printf "%.1f", $1}' /proc/loadavg 2>/dev/null || echo "N/A") + CPU_CORES=$(nproc 2>/dev/null || echo "?") + ok "CPU Load: $CPU_LOAD / $CPU_CORES cores" + + # Database connection + info "Checking database connection..." + mariadb $MYSQL_CRED -e "SELECT 1" "$DB_NAME" &>/dev/null || die "Cannot connect to database $DB_NAME on $DB_HOST:$DB_PORT" + DB_SIZE=$(mariadb $MYSQL_CRED -N -e "SELECT ROUND(SUM(data_length + index_length) / 1024 / 1024, 1) FROM information_schema.tables WHERE table_schema='$DB_NAME'" "$DB_NAME" 2>/dev/null || echo "?") + DB_TABLES=$(mariadb $MYSQL_CRED -N -e "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='$DB_NAME'" "$DB_NAME" 2>/dev/null || echo "?") + ok "Database OK — ${DB_SIZE}MB, ${DB_TABLES} tables" + + # PHP/Laravel check + info "Checking Laravel installation..." + if [ -f "$SCRIPT_DIR/artisan" ]; then + LARAVEL_VER=$(php "$SCRIPT_DIR/artisan" --version 2>/dev/null | head -1 || echo "unknown") + ok "Laravel: $LARAVEL_VER" + else + warn "Laravel artisan not found" + fi + + # Redis check + info "Checking Redis..." + if command -v redis-cli &>/dev/null; then + REDIS_PING=$(redis-cli ping 2>/dev/null || echo "FAILED") + if [ "$REDIS_PING" = "PONG" ]; then + ok "Redis: Connected" + else + warn "Redis: Not responding" + fi + else + warn "redis-cli not found, skipping" + fi + + # Nginx check + info "Checking Nginx..." + if command -v nginx &>/dev/null; then + if nginx -t 2>/dev/null; then + ok "Nginx config valid" + else + warn "Nginx config has issues" + fi + else + debug "Nginx not found, skipping" + fi + + # SSL certificate check + if [[ "$NITRO_SITE_URL" == https://* ]]; then + info "Checking SSL certificate..." + SSL_DOMAIN=$(echo "$NITRO_SITE_URL" | sed 's|https://||' | sed 's|/.*||') + SSL_EXPIRY=$(echo | openssl s_client -servername "$SSL_DOMAIN" -connect "$SSL_DOMAIN:443" 2>/dev/null | openssl x509 -noout -enddate 2>/dev/null | cut -d= -f2 || echo "") + if [ -n "$SSL_EXPIRY" ]; then + SSL_EXPIRY_EPOCH=$(date -d "$SSL_EXPIRY" +%s 2>/dev/null || echo "0") + NOW_EPOCH=$(date +%s) + SSL_DAYS_LEFT=$(( (SSL_EXPIRY_EPOCH - NOW_EPOCH) / 86400 )) + if [ "$SSL_DAYS_LEFT" -gt 30 ]; then + ok "SSL certificate valid for $SSL_DAYS_LEFT more days" + elif [ "$SSL_DAYS_LEFT" -gt 0 ]; then + warn "SSL certificate expires in $SSL_DAYS_LEFT days!" + else + fail "SSL certificate has EXPIRED!" + ERRORS=$((ERRORS + 1)) + fi + else + warn "Could not check SSL certificate" fi fi - git pull - [ "$(git rev-parse HEAD)" != "$old_head" ] + + # Git status of repos + info "Checking git repositories..." + local REPOS=( + "Emulator:$EMULATOR_DIR/Emulator" + "Nitro-V3:$NITRO_CLIENT" + "Nitro_Render_V3:$NITRO_RENDERER" + ) + for repo_entry in "${REPOS[@]}"; do + local name="${repo_entry%%:*}" + local path="${repo_entry##*:}" + if [ -d "$path/.git" ]; then + local branch=$(cd "$path" && git branch --show-current 2>/dev/null || echo "?") + local dirty=$(cd "$path" && git status --porcelain 2>/dev/null | wc -l) + local ahead=$(cd "$path" && git rev-list --count '@{upstream}..HEAD' 2>/dev/null || echo "0") + local commit=$(cd "$path" && git log --oneline -1 2>/dev/null | cut -c1-8 || echo "?") + debug " $name: branch=$branch dirty=$dirty ahead=$ahead commit=$commit" + if [ "$dirty" -gt 0 ]; then + warn "$name has $dirty uncommitted changes" + else + ok "$name is clean (branch: $branch, commit: $commit)" + fi + fi + done } -HAD_UPDATES=false -NITRO_BUILT=false +# ============================================================================= +# DRY-RUN DISPLAY +# ============================================================================= +dry_run_display() { + echo "" + box_top "DRY-RUN SUMMARY" 56 + box_line "Site: $NITRO_SITE_URL" 56 + box_line "Branch: $NITRO_BRANCH" 56 + box_line "Emulator: $EMULATOR_DIR/Emulator" 56 + box_line "Nitro-V3: $NITRO_CLIENT" 56 + box_line "Renderer: $NITRO_RENDERER" 56 + box_line "Mode: ${SELECTIVE_UPDATE:-full}" 56 + box_line "Log: $LOG_FILE" 56 + box_bottom 56 + echo "" + info "Dry-run complete. No changes were made." +} # ============================================================================= # 1. UPDATE & BUILD EMULATOR # ============================================================================= -step "Update & Build Emulator" +update_emulator() { + step "Update & Build Emulator" -if git_update "$EMULATOR_DIR/Emulator" "$NITRO_BRANCH"; then - info "New commits detected" - HAD_UPDATES=true - ROLLBACK_NEEDED=true + if git_update "$EMULATOR_DIR/Emulator" "$NITRO_BRANCH"; then + info "New commits detected" + HAD_UPDATES=true + ROLLBACK_NEEDED=true + UPDATED_REPOS+=("Emulator") - # Database backup - info "Creating database backup..." - mkdir -p "$BACKUP_DIR" - BACKUP_FILE="$BACKUP_DIR/backup_$(date +%Y%m%d_%H%M%S).sql" - if mariadb-dump $MYSQL_CRED --force --skip-lock-tables --routines --events --triggers "$DB_NAME" > "$BACKUP_FILE"; then - LAST_BACKUP="$BACKUP_FILE" - BK_SIZE=$(stat -c%s "$BACKUP_FILE" 2>/dev/null || echo 0) - [ "$BK_SIZE" -lt 1024 ] && { rm -f "$BACKUP_FILE"; die "Backup is too small (${BK_SIZE}B) — possible corruption, aborting"; } - ok "Backup saved: $(numfmt --to=iec "$BK_SIZE") — $BACKUP_FILE" - else - die "Database backup failed" - fi + # Database backup + info "Creating database backup..." + mkdir -p "$BACKUP_DIR" + BACKUP_FILE="$BACKUP_DIR/backup_$(date +%Y%m%d_%H%M%S).sql" + spinner_start "Backing up database..." + if mariadb-dump $MYSQL_CRED --force --skip-lock-tables --routines --events --triggers "$DB_NAME" > "$BACKUP_FILE" 2>/dev/null; then + LAST_BACKUP="$BACKUP_FILE" + BK_SIZE=$(stat -c%s "$BACKUP_FILE" 2>/dev/null || echo 0) + if [ "$BK_SIZE" -lt 1024 ]; then + rm -f "$BACKUP_FILE" + spinner_stop fail + die "Backup is too small (${BK_SIZE}B) — possible corruption, aborting" + fi + spinner_stop ok + ok "Backup saved: $(numfmt --to=iec "$BK_SIZE") — $(basename "$BACKUP_FILE")" + else + spinner_stop fail + die "Database backup failed" + fi - # SQL imports - info "Checking for new SQL files..." - if [ -d "$SQL_DIR" ]; then - SQL_COUNT=0 - while IFS= read -r -d '' sql_file; do - info "Importing: $(basename "$sql_file")" - mariadb $MYSQL_CRED --force "$DB_NAME" < "$sql_file" || warn "Import failed for $(basename "$sql_file")" - SQL_COUNT=$((SQL_COUNT + 1)) - done < <(find "$SQL_DIR" -name '*.sql' -mmin -10 -not -path "$BACKUP_DIR/*" -print0 2>/dev/null) - [ "$SQL_COUNT" -gt 0 ] && ok "$SQL_COUNT SQL file(s) imported" || info "No new SQL files found" - else - info "SQL directory not found, skipping" - fi + # SQL imports + info "Checking for new SQL files..." + if [ -d "$SQL_DIR" ]; then + SQL_COUNT=0 + SQL_ERRORS=0 + while IFS= read -r -d '' sql_file; do + info_indent "Importing: $(basename "$sql_file")" + if ! mariadb $MYSQL_CRED --force "$DB_NAME" < "$sql_file" 2>/dev/null; then + warn "Import failed for $(basename "$sql_file")" + SQL_ERRORS=$((SQL_ERRORS + 1)) + fi + SQL_COUNT=$((SQL_COUNT + 1)) + done < <(find "$SQL_DIR" -name '*.sql' -mmin -10 -not -path "$BACKUP_DIR/*" -print0 2>/dev/null) + if [ "$SQL_COUNT" -gt 0 ]; then + ok "$SQL_COUNT SQL file(s) imported ($SQL_ERRORS errors)" + else + info "No new SQL files found" + fi + else + info "SQL directory not found, skipping" + fi - # Maven build - info "Building emulator (mvn package)..." - cd "$EMULATOR_DIR/Emulator" - mvn package -q || die "Maven build failed" - ok "Build complete" + # Maven build + info "Building emulator (mvn package)..." + cd "$EMULATOR_DIR/Emulator" + spinner_start "Compiling Java..." + if mvn package -q 2>&1 | tail -5; then + spinner_stop ok + else + spinner_stop fail + die "Maven build failed" + fi + ok "Build complete" - JAR_FILE=$(find target -maxdepth 1 -name 'Habbo-*-jar-with-dependencies.jar' -printf '%T@ %p\n' 2>/dev/null | sort -rn | sed -n '1s/^[0-9.]* //p' | xargs basename) - [ -z "$JAR_FILE" ] && die "No jar file found in target/" - ok "Jar: $JAR_FILE" + JAR_FILE=$(find target -maxdepth 1 -name 'Habbo-*-jar-with-dependencies.jar' -printf '%T@ %p\n' 2>/dev/null | sort -rn | sed -n '1s/^[0-9.]* //p' | xargs basename 2>/dev/null || echo "") + [ -z "$JAR_FILE" ] && die "No jar file found in target/" + ok "Jar: $JAR_FILE" - info "Updating emulator launch script..." - cd target/ - cat << EOF > emulator + # Update emulator launch script + info "Updating emulator launch script..." + cd target/ + cat << EOF > emulator #!/bin/sh file_name_emulator=emulator.log current_time=\$(date "+%H%M_%d-%m-%Y") @@ -317,88 +656,115 @@ file_name=\$file_name_emulator.\$current_time mv /var/log/emu/emulator.log /var/log/emu/\$file_name java -Dfile.encoding=UTF8 -Xmx2G -jar $JAR_FILE EOF - chmod +x emulator - ok "Launch script updated" - ROLLBACK_NEEDED=false -else - info "Already up to date, skipping" -fi + chmod +x emulator + ok "Launch script updated" + + # Save backup info for history + echo "$(date +%Y-%m-%d\ %H:%M:%S)|BACKUP|$BACKUP_FILE|$BK_SIZE" >> "$BACKUP_DIR/.history" 2>/dev/null || true + + ROLLBACK_NEEDED=false + else + info "Already up to date, skipping" + fi +} # ============================================================================= # 2. UPDATE NITRO_RENDER_V3 # ============================================================================= -step "Update Nitro_Render_V3" +update_renderer() { + step "Update Nitro_Render_V3" -if git_update "$NITRO_RENDERER" "$NITRO_BRANCH"; then - info "New commits detected" - HAD_UPDATES=true - yarn install --frozen-lockfile || { info "Retrying with clean node_modules..."; clean_node_modules && yarn install; } || die "yarn install failed for Nitro_Render_V3" - ok "Dependencies installed" -else - info "Already up to date, skipping" -fi + if git_update "$NITRO_RENDERER" "$NITRO_BRANCH"; then + info "New commits detected" + HAD_UPDATES=true + UPDATED_REPOS+=("Nitro_Render_V3") + spinner_start "Installing dependencies..." + yarn install --frozen-lockfile 2>&1 || { info "Retrying with clean node_modules..."; clean_node_modules && yarn install 2>&1; } || { spinner_stop fail; die "yarn install failed for Nitro_Render_V3"; } + spinner_stop ok + ok "Dependencies installed" + else + info "Already up to date, skipping" + fi +} # ============================================================================= # 3. UPDATE & BUILD NITRO-V3 # ============================================================================= -step "Update & Build Nitro-V3" +update_client() { + step "Update & Build Nitro-V3" -if git_update "$NITRO_CLIENT" "$NITRO_BRANCH"; then - info "New commits detected" - HAD_UPDATES=true - NITRO_BUILT=true - yarn install --frozen-lockfile || { info "Retrying with clean node_modules..."; clean_node_modules && yarn install; } || die "yarn install failed for Nitro-V3" - ok "Dependencies installed" - info "Building Nitro-V3 (yarn build)..." - yarn build || die "yarn build failed" - ok "Build complete" -else - info "Already up to date, skipping build" -fi + if git_update "$NITRO_CLIENT" "$NITRO_BRANCH"; then + info "New commits detected" + HAD_UPDATES=true + NITRO_BUILT=true + UPDATED_REPOS+=("Nitro-V3") -# custom-themes/index.json -mkdir -p "$NITRO_CLIENT/dist/custom-themes" -if [ ! -f "$NITRO_CLIENT/dist/custom-themes/index.json" ]; then - echo '{"themes":[]}' > "$NITRO_CLIENT/dist/custom-themes/index.json" - ok "Created custom-themes/index.json" -else - ok "custom-themes/index.json exists" -fi + spinner_start "Installing dependencies..." + yarn install --frozen-lockfile 2>&1 || { info "Retrying with clean node_modules..."; clean_node_modules && yarn install 2>&1; } || { spinner_stop fail; die "yarn install failed for Nitro-V3"; } + spinner_stop ok + ok "Dependencies installed" + + info "Building Nitro-V3 (yarn build)..." + spinner_start "Building frontend..." + if yarn build 2>&1 | tail -3; then + spinner_stop ok + else + spinner_stop fail + die "yarn build failed" + fi + ok "Build complete" + else + info "Already up to date, skipping build" + fi + + # custom-themes/index.json + mkdir -p "$NITRO_CLIENT/dist/custom-themes" + if [ ! -f "$NITRO_CLIENT/dist/custom-themes/index.json" ]; then + echo '{"themes":[]}' > "$NITRO_CLIENT/dist/custom-themes/index.json" + ok "Created custom-themes/index.json" + else + ok "custom-themes/index.json exists" + fi +} # ============================================================================= # 4. SYNC CONFIGS # ============================================================================= -step "Sync Configurations" +sync_configs() { + step "Sync Configurations" -mkdir -p "$GAMEDATA_CONF_DIR" -MERGE_SCRIPT="$(dirname "$0")/scripts/merge-config.cjs" -NITRO_DIST_CONFIG_DIR="$NITRO_CLIENT/dist/configuration" + mkdir -p "$GAMEDATA_CONF_DIR" + MERGE_SCRIPT="$(dirname "$0")/scripts/merge-config.cjs" + NITRO_DIST_CONFIG_DIR="$NITRO_CLIENT/dist/configuration" -# Make dirs writable -for dir in "$NITRO_SRC_DIR" "$GAMEDATA_CONF_DIR"; do - if [ -d "$dir" ] && [ ! -w "$dir" ] && command -v sudo &> /dev/null; then - sudo chown -R "$(whoami)":"$(whoami)" "$dir" 2>/dev/null || true - fi -done + # Make dirs writable + for dir in "$NITRO_SRC_DIR" "$GAMEDATA_CONF_DIR"; do + if [ -d "$dir" ] && [ ! -w "$dir" ] && command -v sudo &>/dev/null; then + sudo chown -R "$(whoami)":"$(whoami)" "$dir" 2>/dev/null || true + fi + done -EXAMPLE_COUNT=0 -for example_file in "$NITRO_SRC_DIR"/*.example; do - [ -f "$example_file" ] || continue - base=$(basename "$example_file") - target_name="${base%.example}" - case "$target_name" in *.*) ;; *) target_name="$target_name.json" ;; esac - node "$MERGE_SCRIPT" "$example_file" "$NITRO_SRC_DIR/$target_name" 2>/dev/null - node "$MERGE_SCRIPT" "$example_file" "$GAMEDATA_CONF_DIR/$target_name" 2>/dev/null - if [ -d "$NITRO_DIST_CONFIG_DIR" ]; then - cp "$NITRO_SRC_DIR/$target_name" "$NITRO_DIST_CONFIG_DIR/$target_name" 2>/dev/null || true - fi - EXAMPLE_COUNT=$((EXAMPLE_COUNT + 1)) -done -ok "$EXAMPLE_COUNT .example file(s) processed" + EXAMPLE_COUNT=0 + for example_file in "$NITRO_SRC_DIR"/*.example; do + [ -f "$example_file" ] || continue + base=$(basename "$example_file") + target_name="${base%.example}" + case "$target_name" in + *.*) ;; + *) target_name="$target_name.json" ;; + esac + node "$MERGE_SCRIPT" "$example_file" "$NITRO_SRC_DIR/$target_name" 2>/dev/null + node "$MERGE_SCRIPT" "$example_file" "$GAMEDATA_CONF_DIR/$target_name" 2>/dev/null + if [ -d "$NITRO_DIST_CONFIG_DIR" ]; then + cp "$NITRO_SRC_DIR/$target_name" "$NITRO_DIST_CONFIG_DIR/$target_name" 2>/dev/null || true + fi + EXAMPLE_COUNT=$((EXAMPLE_COUNT + 1)) + done + ok "$EXAMPLE_COUNT .example file(s) processed" -# Force critical URLs -info "Applying critical config URLs..." -FORCE_CONFIG_SCRIPT=$(cat << 'PYEOF' + # Force critical URLs + info "Applying critical config URLs..." + FORCE_CONFIG_SCRIPT=$(cat << 'PYEOF' import json, sys, os paths = [ @@ -433,152 +799,1109 @@ for path in paths: print(' [OK] All config URLs synced') PYEOF -) -cd "$NITRO_CLIENT" && NITRO_SRC_DIR="$NITRO_SRC_DIR" NITRO_DIST_CONFIG_DIR="$NITRO_DIST_CONFIG_DIR" \ - NITRO_IMAGE_LIBRARY_URL="$NITRO_IMAGE_LIBRARY_URL" NITRO_HOF_FURNITURE_URL="$NITRO_HOF_FURNITURE_URL" \ - NITRO_API_URL="$NITRO_API_URL" NITRO_SOCKET_URL="$NITRO_SOCKET_URL" \ - NITRO_GAMEDATA_URL="$NITRO_GAMEDATA_URL" NITRO_ASSET_URL="$NITRO_ASSET_URL" \ - NITRO_FURNI_ASSET_ICON_URL="$NITRO_FURNI_ASSET_ICON_URL" \ - python3 -c "$FORCE_CONFIG_SCRIPT" + ) + cd "$NITRO_CLIENT" && NITRO_SRC_DIR="$NITRO_SRC_DIR" NITRO_DIST_CONFIG_DIR="$NITRO_DIST_CONFIG_DIR" \ + NITRO_IMAGE_LIBRARY_URL="$NITRO_IMAGE_LIBRARY_URL" NITRO_HOF_FURNITURE_URL="$NITRO_HOF_FURNITURE_URL" \ + NITRO_API_URL="$NITRO_API_URL" NITRO_SOCKET_URL="$NITRO_SOCKET_URL" \ + NITRO_GAMEDATA_URL="$NITRO_GAMEDATA_URL" NITRO_ASSET_URL="$NITRO_ASSET_URL" \ + NITRO_FURNI_ASSET_ICON_URL="$NITRO_FURNI_ASSET_ICON_URL" \ + python3 -c "$FORCE_CONFIG_SCRIPT" +} # ============================================================================= # 5. CLEANUP # ============================================================================= -step "Cleanup" +cleanup_phase() { + step "Cleanup" -info "Removing logs older than 14 days..." -find "$EMULATOR_DIR" -name "*.log" -mtime +14 -exec rm -f {} \; 2>/dev/null || true + info "Removing logs older than 14 days..." + local LOG_COUNT=$(find "$EMULATOR_DIR" -name "*.log" -mtime +14 2>/dev/null | wc -l) + find "$EMULATOR_DIR" -name "*.log" -mtime +14 -exec rm -f {} \; 2>/dev/null || true + ok "Removed $LOG_COUNT old log file(s)" -info "Managing backups (keeping max 5)..." -if [ -d "$BACKUP_DIR" ]; then - find "$BACKUP_DIR" -maxdepth 1 -name '*.sql' -printf '%T@ %p\n' 2>/dev/null | sort -rn | tail -n +6 | sed 's/^[0-9.]* //' | xargs -r rm -f || true -fi + info "Managing backups (keeping max 5)..." + if [ -d "$BACKUP_DIR" ]; then + local BK_COUNT=$(find "$BACKUP_DIR" -maxdepth 1 -name '*.sql' 2>/dev/null | wc -l) + find "$BACKUP_DIR" -maxdepth 1 -name '*.sql' -printf '%T@ %p\n' 2>/dev/null | sort -rn | tail -n +6 | sed 's/^[0-9.]* //' | xargs -r rm -f || true + local REMAINING=$(find "$BACKUP_DIR" -maxdepth 1 -name '*.sql' 2>/dev/null | wc -l) + ok "Backups: $REMAINING kept (removed $((BK_COUNT - REMAINING)))" + fi -if [ "$HAD_UPDATES" = true ]; then - info "Cleaning Yarn cache..." - yarn cache clean 2>/dev/null || true - info "Removing old .jar files..." - find "$EMULATOR_DIR/Emulator/target" -maxdepth 1 -name 'Habbo-*-jar-with-dependencies.jar' -printf '%T@ %p\n' 2>/dev/null | sort -rn | tail -n +2 | sed 's/^[0-9.]* //' | xargs -r rm -f || true -fi -ok "Cleanup complete" + if [ "$HAD_UPDATES" = true ]; then + info "Cleaning Yarn cache..." + yarn cache clean 2>/dev/null || true + ok "Yarn cache cleaned" + + info "Removing old .jar files..." + find "$EMULATOR_DIR/Emulator/target" -maxdepth 1 -name 'Habbo-*-jar-with-dependencies.jar' -printf '%T@ %p\n' 2>/dev/null | sort -rn | tail -n +2 | sed 's/^[0-9.]* //' | xargs -r rm -f || true + ok "Old jar files removed" + fi + + # Clean temp files + info "Cleaning temporary files..." + rm -rf /tmp/nitro-* 2>/dev/null || true + ok "Temp files cleaned" +} # ============================================================================= # 6. PERMISSIONS # ============================================================================= -step "Set Permissions" +set_permissions() { + step "Set Permissions" -if command -v sudo &> /dev/null; then - for dir in "$NITRO_CLIENT" "$NITRO_RENDERER" "$EMULATOR_DIR" "$GAMEDATA_CONF_DIR"; do - if [ -d "$dir" ]; then - sudo chown -R www-data:www-data "$dir" 2>/dev/null || warn "Could not chown $dir" - fi - done - ok "Permissions set to www-data:www-data" -else - warn "sudo not available, skipping chown" -fi + if command -v sudo &>/dev/null; then + for dir in "$NITRO_CLIENT" "$NITRO_RENDERER" "$EMULATOR_DIR" "$GAMEDATA_CONF_DIR"; do + if [ -d "$dir" ]; then + sudo chown -R www-data:www-data "$dir" 2>/dev/null || warn "Could not chown $dir" + fi + done + ok "Permissions set to www-data:www-data" + else + warn "sudo not available, skipping chown" + fi + + # Set correct permissions for log files + chmod -R 775 "$LOG_DIR" 2>/dev/null || true + chmod -R 775 "$BACKUP_DIR" 2>/dev/null || true + ok "Log and backup directories permissions updated" +} # ============================================================================= # 7. RESTART SERVICES # ============================================================================= -step "Restart Services" +restart_services() { + step "Restart Services" -RESTART_OK=false -if [ "$HAD_UPDATES" = true ]; then - if systemctl cat "$EMULATOR_SERVICE" &>/dev/null; then - if command -v sudo &> /dev/null; then - info "Restarting $EMULATOR_SERVICE..." - sudo systemctl restart "$EMULATOR_SERVICE" || die "Failed to restart $EMULATOR_SERVICE" + RESTART_OK=false + if [ "$HAD_UPDATES" = true ]; then + if systemctl cat "$EMULATOR_SERVICE" &>/dev/null; then + if command -v sudo &>/dev/null; then + info "Restarting $EMULATOR_SERVICE..." + spinner_start "Restarting emulator service..." + if sudo systemctl restart "$EMULATOR_SERVICE" 2>/dev/null; then + spinner_stop ok + RESTART_OK=true + else + spinner_stop fail + die "Failed to restart $EMULATOR_SERVICE" + fi + fi + elif command -v pm2 &>/dev/null; then + info "Restarting PM2 processes..." + spinner_start "Restarting PM2..." + pm2 restart all 2>/dev/null || warn "PM2 restart had issues" + spinner_stop ok RESTART_OK=true + else + warn "No systemd or PM2 found — restart manually" fi - elif command -v pm2 &> /dev/null; then - info "Restarting PM2 processes..." - pm2 restart all || warn "PM2 restart had issues" - RESTART_OK=true - else - warn "No systemd or PM2 found — restart manually" - fi - if [ "$RESTART_OK" = true ]; then - ok "Services restarted" + # Restart Nginx + if command -v nginx &>/dev/null && command -v sudo &>/dev/null; then + info "Reloading Nginx..." + sudo nginx -t 2>/dev/null && sudo systemctl reload nginx 2>/dev/null && ok "Nginx reloaded" || warn "Nginx reload failed" + fi + + # Restart Redis if needed + if command -v redis-cli &>/dev/null; then + info "Flushing Redis cache..." + redis-cli FLUSHALL 2>/dev/null && ok "Redis cache flushed" || warn "Redis flush failed" + fi + + if [ "$RESTART_OK" = true ]; then + ok "Services restarted" + fi + else + info "No updates applied, no restart needed" fi -else - info "No updates applied, no restart needed" -fi +} # ============================================================================= # 8. HEALTH CHECK & VALIDATION # ============================================================================= -step "Health Check & Validation" +health_check() { + step "Health Check & Validation" -ERRORS=0 - -# Build output check -if [ "$NITRO_BUILT" = true ]; then - if [ -d "$NITRO_CLIENT/dist/assets" ]; then - ASSET_COUNT=$(find "$NITRO_CLIENT/dist/assets" -type f 2>/dev/null | wc -l) - ok "Nitro-V3 built: $ASSET_COUNT assets in dist/assets" - else - fail "Nitro-V3 build assets missing!" - ERRORS=$((ERRORS + 1)) - fi -else - info "Skipping build check (no new commits)" -fi - -# Health check with retries for emulator -if [ "$HAD_UPDATES" = true ] && systemctl cat "$EMULATOR_SERVICE" &>/dev/null; then - info "Waiting for $EMULATOR_SERVICE to become active..." - HEALTHY=false - for i in $(seq 1 "$HEALTH_RETRIES"); do - sleep "$HEALTH_INTERVAL" - STATUS=$(systemctl is-active "$EMULATOR_SERVICE" 2>/dev/null || echo "unknown") - if [ "$STATUS" = "active" ]; then - HEALTHY=true - break - fi - info " Attempt $i/$HEALTH_RETRIES — status: $STATUS" - done - if [ "$HEALTHY" = true ]; then - ok "$EMULATOR_SERVICE is active ($(( i * HEALTH_INTERVAL ))s to start)" - else - fail "$EMULATOR_SERVICE did not become active after ${HEALTH_RETRIES} retries" - ERRORS=$((ERRORS + 1)) - fi -fi - -# Permission audit -for dir in "$NITRO_CLIENT" "$NITRO_RENDERER" "$EMULATOR_DIR" "$GAMEDATA_CONF_DIR"; do - if [ -d "$dir" ]; then - OWNER=$(stat -c '%U:%G' "$dir" 2>/dev/null || echo "unknown") - if [ "$OWNER" = "www-data:www-data" ] || [ "$(id -u)" -ne 0 ]; then - ok "$(basename "$dir") ($OWNER)" + # Build output check + if [ "$NITRO_BUILT" = true ]; then + if [ -d "$NITRO_CLIENT/dist/assets" ]; then + ASSET_COUNT=$(find "$NITRO_CLIENT/dist/assets" -type f 2>/dev/null | wc -l) + ASSET_SIZE=$(du -sh "$NITRO_CLIENT/dist/assets" 2>/dev/null | cut -f1 || echo "?") + ok "Nitro-V3 built: $ASSET_COUNT assets ($ASSET_SIZE)" else - warn "$(basename "$dir") owner is $OWNER (should be www-data:www-data)" - sudo chown -R www-data:www-data "$dir" 2>/dev/null || true + fail "Nitro-V3 build assets missing!" + ERRORS=$((ERRORS + 1)) + fi + else + info "Skipping build check (no new commits)" + fi + + # Health check with retries for emulator + if [ "$HAD_UPDATES" = true ] && systemctl cat "$EMULATOR_SERVICE" &>/dev/null; then + info "Waiting for $EMULATOR_SERVICE to become active..." + HEALTHY=false + for i in $(seq 1 "$HEALTH_RETRIES"); do + sleep "$HEALTH_INTERVAL" + STATUS=$(systemctl is-active "$EMULATOR_SERVICE" 2>/dev/null || echo "unknown") + if [ "$STATUS" = "active" ]; then + HEALTHY=true + break + fi + info_indent "Attempt $i/$HEALTH_RETRIES — status: $STATUS" + done + if [ "$HEALTHY" = true ]; then + ok "$EMULATOR_SERVICE is active ($((i * HEALTH_INTERVAL))s to start)" + else + fail "$EMULATOR_SERVICE did not become active after ${HEALTH_RETRIES} retries" + ERRORS=$((ERRORS + 1)) fi fi -done + + # Permission audit + info "Permission audit..." + for dir in "$NITRO_CLIENT" "$NITRO_RENDERER" "$EMULATOR_DIR" "$GAMEDATA_CONF_DIR"; do + if [ -d "$dir" ]; then + OWNER=$(stat -c '%U:%G' "$dir" 2>/dev/null || echo "unknown") + if [ "$OWNER" = "www-data:www-data" ] || [ "$(id -u)" -ne 0 ]; then + debug " $(basename "$dir"): $OWNER" + else + warn "$(basename "$dir") owner is $OWNER (should be www-data:www-data)" + sudo chown -R www-data:www-data "$dir" 2>/dev/null || true + fi + fi + done + + # URL validation + info "Validating site URL..." + if curl -s -o /dev/null -w "%{http_code}" --max-time 10 "$NITRO_SITE_URL" 2>/dev/null | grep -qE '^(200|301|302|303|307|308)$'; then + ok "Site URL responds: $NITRO_SITE_URL" + else + warn "Site URL did not respond with HTTP 2xx: $NITRO_SITE_URL" + fi + + # WebSocket check + if [[ "$NITRO_SOCKET_URL" == wss://* ]]; then + WS_DOMAIN=$(echo "$NITRO_SOCKET_URL" | sed 's|wss://||' | sed 's|/.*||') + if echo | timeout 5 openssl s_client -servername "$WS_DOMAIN" -connect "$WS_DOMAIN:443" 2>/dev/null | grep -q "BEGIN CERTIFICATE"; then + ok "WebSocket SSL connection OK: $NITRO_SOCKET_URL" + else + warn "Could not verify WebSocket SSL: $NITRO_SOCKET_URL" + fi + fi +} # ============================================================================= # SUMMARY # ============================================================================= -ELAPSED=$(( $(date +%s) - START_TIME )) -ELAPSED_FMT=$(printf '%dm %ds' $((ELAPSED/60)) $((ELAPSED%60))) +show_summary() { + ELAPSED=$(($(date +%s) - START_TIME)) + ELAPSED_FMT=$(printf '%dm %ds' $((ELAPSED / 60)) $((ELAPSED % 60))) -echo "" -echo "╔══════════════════════════════════════════════════════════════╗" -if [ "$ERRORS" -eq 0 ]; then - echo "║ ✅ UPDATE SUCCESSFULLY COMPLETED ║" -else - echo "║ ⚠️ UPDATE COMPLETED WITH $ERRORS WARNING(S) ║" -fi -echo "╠══════════════════════════════════════════════════════════════╣" -printf "║ Duration: %-46s║\n" "$ELAPSED_FMT" -printf "║ Updates: %-46s║\n" "$([ "$HAD_UPDATES" = true ] && echo "Yes" || echo "No")" -printf "║ Nitro build: %-46s║\n" "$([ "$NITRO_BUILT" = true ] && echo "Yes" || echo "No")" -printf "║ Log file: %-46s║\n" "$LOG_FILE" -echo "╚══════════════════════════════════════════════════════════════╝" -echo "" + echo "" + echo -e "${C_CYAN}" + printf " ╔══════════════════════════════════════════════════════════════╗\n" + if [ "$ERRORS" -eq 0 ]; then + printf " ║ ${C_GREEN}${C_BOLD}✔ UPDATE SUCCESSFULLY COMPLETED${C_CYAN} ║\n" + else + printf " ║ ${C_YELLOW}${C_BOLD}⚠ UPDATE COMPLETED WITH $ERRORS ERROR(S)${C_CYAN} ║\n" + fi + printf " ╠══════════════════════════════════════════════════════════════╣\n" + printf " ║ Duration: %-42s║\n" "$ELAPSED_FMT" + printf " ║ Updates: %-42s║\n" "$([ "$HAD_UPDATES" = true ] && echo "Yes" || echo "No")" + printf " ║ Nitro build: %-42s║\n" "$([ "$NITRO_BUILT" = true ] && echo "Yes" || echo "No")" + printf " ║ Warnings: %-42s║\n" "$WARNINGS" + printf " ║ Errors: %-42s║\n" "$ERRORS" + if [ ${#UPDATED_REPOS[@]} -gt 0 ]; then + printf " ║ Updated: %-42s║\n" "${UPDATED_REPOS[*]}" + fi + printf " ║ Disk free: %-42s║\n" "${AVAIL_GB:-?}GB" + printf " ║ Log file: %-42s║\n" "$(basename "${LOG_FILE}")" + printf " ╚══════════════════════════════════════════════════════════════╝\n" + echo -e "${C_RESET}" -notify "SUCCESS" "Completed in $ELAPSED_FMT (${ERRORS} warnings)" \ No newline at end of file + notify "SUCCESS" "Completed in $ELAPSED_FMT (${ERRORS} errors, ${WARNINGS} warnings)" +} + +# ============================================================================= +# COMMAND: FULL UPDATE +# ============================================================================= +cmd_update() { + preflight + preflight_checks + + if [ "$DRY_RUN" = true ]; then + dry_run_display + return 0 + fi + + echo "" + info "Site: $NITRO_SITE_URL | Branch: $NITRO_BRANCH | Mode: ${SELECTIVE_UPDATE:-full}" + info "Log: $LOG_FILE" + + case "${SELECTIVE_UPDATE:-full}" in + emulator) + update_emulator + ;; + renderer) + update_renderer + ;; + client) + update_client + ;; + configs) + sync_configs + ;; + *) + update_emulator + update_renderer + update_client + sync_configs + cleanup_phase + set_permissions + restart_services + health_check + ;; + esac + + show_summary +} + +# ============================================================================= +# COMMAND: BACKUP +# ============================================================================= +cmd_backup() { + preflight + step "Database Backup" + + mkdir -p "$BACKUP_DIR" + BACKUP_FILE="$BACKUP_DIR/manual_$(date +%Y%m%d_%H%M%S).sql" + info "Creating manual backup..." + + spinner_start "Backing up database..." + if mariadb-dump $MYSQL_CRED --force --skip-lock-tables --routines --events --triggers "$DB_NAME" > "$BACKUP_FILE" 2>/dev/null; then + BK_SIZE=$(stat -c%s "$BACKUP_FILE" 2>/dev/null || echo 0) + spinner_stop ok + ok "Backup saved: $(numfmt --to=iec "$BK_SIZE") — $(basename "$BACKUP_FILE")" + echo "$(date +%Y-%m-%d\ %H:%M:%S)|MANUAL_BACKUP|$BACKUP_FILE|$BK_SIZE" >> "$BACKUP_DIR/.history" 2>/dev/null || true + else + spinner_stop fail + die "Backup failed" + fi +} + +# ============================================================================= +# COMMAND: RESTORE +# ============================================================================= +cmd_restore() { + preflight + step "Database Restore" + + if [ ! -d "$BACKUP_DIR" ]; then + die "No backup directory found: $BACKUP_DIR" + fi + + local backups=() + while IFS= read -r line; do + backups+=("$line") + done < <(find "$BACKUP_DIR" -maxdepth 1 -name '*.sql' -printf '%T@ %p\n' 2>/dev/null | sort -rn | head -20 | sed 's/^[0-9.]* //') + + if [ ${#backups[@]} -eq 0 ]; then + die "No backups found in $BACKUP_DIR" + fi + + echo "" + echo -e " ${C_BOLD}${C_CYAN}Available Backups:${C_RESET}" + echo "" + local i=1 + for bk in "${backups[@]}"; do + local name=$(basename "$bk") + local size=$(stat -c%s "$bk" 2>/dev/null || echo 0) + local size_fmt=$(numfmt --to=iec "$size" 2>/dev/null || echo "?") + local date_part=$(echo "$name" | grep -oP '\d{8}_\d{6}' || echo "?") + printf " ${C_GREEN}${C_BOLD}%2d)${C_RESET} %-40s ${C_DIM}%s${C_RESET}\n" "$i" "$name" "$size_fmt" + i=$((i + 1)) + done + + echo "" + echo -en " Select backup [1-$((i-1))]: " + local choice + read -r choice + + if ! [[ "$choice" =~ ^[0-9]+$ ]] || [ "$choice" -lt 1 ] || [ "$choice" -gt ${#backups[@]} ]; then + die "Invalid selection" + fi + + local selected="${backups[$((choice-1))]}" + echo "" + if ! confirm "Restore database from $(basename "$selected")? This will OVERWRITE the current database!"; then + info "Restore cancelled." + return 0 + fi + + spinner_start "Restoring database..." + if mariadb $MYSQL_CRED "$DB_NAME" < "$selected" 2>/dev/null; then + spinner_stop ok + ok "Database restored from $(basename "$selected")" + else + spinner_stop fail + die "Restore failed" + fi +} + +# ============================================================================= +# COMMAND: BACKUPS LIST +# ============================================================================= +cmd_backups() { + step "Backup Management" + + if [ ! -d "$BACKUP_DIR" ]; then + warn "No backup directory found" + return 0 + fi + + echo "" + echo -e " ${C_BOLD}${C_CYAN}Backup Inventory:${C_RESET}" + echo "" + + local total=0 total_size=0 + printf " ${C_DIM}%-4s %-38s %-10s %-6s${C_RESET}\n" "#" "FILENAME" "SIZE" "AGE" + printf " ${C_DIM}%s${C_RESET}\n" "$(printf '%0.s─' $(seq 1 62))" + + while IFS= read -r line; do + total=$((total + 1)) + local name=$(basename "$line") + local size=$(stat -c%s "$line" 2>/dev/null || echo 0) + total_size=$((total_size + size)) + local size_fmt=$(numfmt --to=iec "$size" 2>/dev/null || echo "?") + local mtime=$(stat -c%Y "$line" 2>/dev/null || echo 0) + local now=$(date +%s) + local age_days=$(( (now - mtime) / 86400 )) + local age_fmt="${age_days}d" + [ "$age_days" -eq 0 ] && age_fmt="today" + [ "$age_days" -eq 1 ] && age_fmt="1d" + + printf " ${C_GREEN}%2d${C_RESET} %-38s ${C_CYAN}%-10s${C_RESET} ${C_DIM}%-6s${C_RESET}\n" "$total" "$name" "$size_fmt" "$age_fmt" + done < <(find "$BACKUP_DIR" -maxdepth 1 -name '*.sql' -printf '%T@ %p\n' 2>/dev/null | sort -rn | head -20 | sed 's/^[0-9.]* //') + + local total_fmt=$(numfmt --to=iec "$total_size" 2>/dev/null || echo "?") + printf " ${C_DIM}%s${C_RESET}\n" "$(printf '%0.s─' $(seq 1 62))" + echo -e " ${C_BOLD}Total: $total backup(s) — $total_fmt${C_RESET}" + echo "" +} + +# ============================================================================= +# COMMAND: SYSTEM STATUS +# ============================================================================= +cmd_status() { + step "System Status Dashboard" + + echo "" + + # Server info + box_top "SERVER INFORMATION" 56 + box_line "Hostname: $(hostname)" 56 + box_line "OS: $(cat /etc/os-release 2>/dev/null | grep PRETTY_NAME | cut -d= -f2 | tr -d '"' || uname -s)" 56 + box_line "Kernel: $(uname -r)" 56 + box_line "Uptime: $(uptime -p 2>/dev/null || uptime)" 56 + box_bottom 56 + echo "" + + # Resource usage + box_top "RESOURCE USAGE" 56 + + # CPU + local cpu_usage=$(awk '{u=$2+$4; t=$2+$4+$5; if(t>0) printf "%.1f", u*100/t}' /proc/stat 2>/dev/null || echo "?") + box_line "CPU Load: $cpu_load / $(nproc 2>/dev/null || echo '?') cores" 56 + + # Memory + local mem_info=$(free -m 2>/dev/null | awk '/^Mem:/{printf "%dMB / %dMB (%.1f%%)", $3, $2, $3*100/$2}') + box_line "Memory: $mem_info" 56 + + # Disk + local disk_info=$(df -h "$SCRIPT_DIR" 2>/dev/null | tail -1 | awk '{printf "%s / %s (%s used)", $3, $2, $5}') + box_line "Disk: $disk_info" 56 + + # Database size + local db_size=$(mariadb $MYSQL_CRED -N -e "SELECT ROUND(SUM(data_length + index_length) / 1024 / 1024, 1) FROM information_schema.tables WHERE table_schema='$DB_NAME'" "$DB_NAME" 2>/dev/null || echo "?") + local db_tables=$(mariadb $MYSQL_CRED -N -e "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='$DB_NAME'" "$DB_NAME" 2>/dev/null || echo "?") + box_line "Database: ${db_size}MB / ${db_tables} tables" 56 + + box_bottom 56 + echo "" + + # Service status + box_top "SERVICES" 56 + local SERVICES=("nginx" "mariadb" "redis-server" "$EMULATOR_SERVICE" "php-fpm" "cron") + for svc in "${SERVICES[@]}"; do + local status_text="" + local status_color="" + if systemctl is-active "$svc" &>/dev/null; then + status_color="$C_GREEN" + status_text="ACTIVE" + elif systemctl is-enabled "$svc" &>/dev/null 2>&1; then + status_color="$C_YELLOW" + status_text="INACTIVE" + else + status_color="$C_DIM" + status_text="N/A" + fi + printf " ║ %-20s ${status_color}%-12s${C_RESET} ║\n" "$svc" "$status_text" + done + box_bottom 56 + echo "" + + # Git repos + box_top "GIT REPOSITORIES" 56 + local REPOS=( + "Emulator:$EMULATOR_DIR/Emulator" + "Nitro-V3:$NITRO_CLIENT" + "Nitro_Render_V3:$NITRO_RENDERER" + ) + for repo_entry in "${REPOS[@]}"; do + local name="${repo_entry%%:*}" + local path="${repo_entry##*:}" + if [ -d "$path/.git" ]; then + local branch=$(cd "$path" && git branch --show-current 2>/dev/null || echo "?") + local commit=$(cd "$path" && git log --oneline -1 2>/dev/null | cut -c1-30 || echo "?") + local dirty=$(cd "$path" && git status --porcelain 2>/dev/null | wc -l) + local dirty_str="" + [ "$dirty" -gt 0 ] && dirty_str=" ${C_YELLOW}($dirty modified)${C_RESET}" + printf " ║ %-18s ${C_CYAN}%-8s${C_RESET} %s${dirty_str}\n" "$name" "$branch" "$commit" + fi + done + box_bottom 56 + echo "" + + # Recent logs + box_top "RECENT UPDATE LOGS" 56 + local LOG_COUNT=$(find "$LOG_DIR" -name 'update-*.log' 2>/dev/null | wc -l) + local TOTAL_LOG_SIZE=$(du -sh "$LOG_DIR" 2>/dev/null | cut -f1 || echo "?") + box_line "Total logs: $LOG_COUNT files ($TOTAL_LOG_SIZE)" 56 + box_line "Current: $(basename "$LOG_FILE")" 56 + box_bottom 56 + echo "" +} + +# ============================================================================= +# COMMAND: LOG VIEWER +# ============================================================================= +cmd_logs() { + step "Log Viewer" + + if [ ! -d "$LOG_DIR" ]; then + die "No log directory found" + fi + + local logs=() + while IFS= read -r line; do + logs+=("$line") + done < <(find "$LOG_DIR" -name 'update-*.log' -printf '%T@ %p\n' 2>/dev/null | sort -rn | head -20 | sed 's/^[0-9.]* //') + + if [ ${#logs[@]} -eq 0 ]; then + warn "No update logs found" + return 0 + fi + + echo "" + echo -e " ${C_BOLD}${C_CYAN}Recent Logs:${C_RESET}" + echo "" + + local i=1 + for log in "${logs[@]}"; do + local name=$(basename "$log") + local size=$(stat -c%s "$log" 2>/dev/null || echo 0) + local size_fmt=$(numfmt --to=iec "$size" 2>/dev/null || echo "?") + local lines=$(wc -l < "$log" 2>/dev/null || echo "?") + printf " ${C_GREEN}${C_BOLD}%2d)${C_RESET} %-35s ${C_DIM}%s (%s lines)${C_RESET}\n" "$i" "$name" "$size_fmt" "$lines" + i=$((i + 1)) + done + + echo "" + echo -e " ${C_DIM}Select a log to view, or press Enter to cancel${C_RESET}" + echo -en " Select [1-$((i-1))]: " + local choice + read -r choice + + if [[ "$choice" =~ ^[0-9]+$ ]] && [ "$choice" -ge 1 ] && [ "$choice" -le ${#logs[@]} ]; then + local selected="${logs[$((choice-1))]}" + echo "" + echo -e " ${C_BOLD}--- $(basename "$selected") ---${C_RESET}" + echo "" + less -R "$selected" 2>/dev/null || cat "$selected" + fi +} + +# ============================================================================= +# COMMAND: SERVICE MANAGEMENT +# ============================================================================= +cmd_services() { + step "Service Manager" + + local options=( + "Start Emulator" + "Stop Emulator" + "Restart Emulator" + "Status Emulator" + "Start Nginx" + "Restart Nginx" + "Status All Services" + "View Emulator Logs (tail)" + "Return to Main Menu" + ) + + local choice + choice=$(select_menu "Service Control" "${options[@]}") + + case "$choice" in + 1) sudo systemctl start "$EMULATOR_SERVICE" && ok "$EMULATOR_SERVICE started" || fail "Failed to start" ;; + 2) sudo systemctl stop "$EMULATOR_SERVICE" && ok "$EMULATOR_SERVICE stopped" || fail "Failed to stop" ;; + 3) sudo systemctl restart "$EMULATOR_SERVICE" && ok "$EMULATOR_SERVICE restarted" || fail "Failed to restart" ;; + 4) systemctl status "$EMULATOR_SERVICE" --no-pager ;; + 5) sudo systemctl start nginx && ok "Nginx started" || fail "Failed to start Nginx" ;; + 6) sudo systemctl restart nginx && ok "Nginx restarted" || fail "Failed to restart Nginx" ;; + 7) for svc in nginx mariadb redis-server "$EMULATOR_SERVICE" php-fpm cron; do + local status="N/A" + systemctl is-active "$svc" &>/dev/null && status="${C_GREEN}ACTIVE${C_RESET}" || status="${C_RED}INACTIVE${C_RESET}" + printf " %-25s %b\n" "$svc" "$status" + done ;; + 8) sudo journalctl -u "$EMULATOR_SERVICE" --no-pager -n 50 ;; + 9|*) return 0 ;; + esac +} + +# ============================================================================= +# COMMAND: DATABASE TOOLS +# ============================================================================= +cmd_database() { + step "Database Tools" + + local options=( + "Show Database Info" + "List All Tables" + "Show Table Sizes" + "Optimize All Tables" + "Check All Tables" + "Run Custom Query" + "Show Active Users (emulator)" + "Return to Main Menu" + ) + + local choice + choice=$(select_menu "Database Management" "${options[@]}") + + case "$choice" in + 1) + echo "" + box_top "DATABASE INFO" 50 + box_line "Name: $DB_NAME" 50 + box_line "Host: $DB_HOST:$DB_PORT" 50 + box_line "User: $DB_USER" 50 + local size=$(mariadb $MYSQL_CRED -N -e "SELECT ROUND(SUM(data_length + index_length) / 1024 / 1024, 1) FROM information_schema.tables WHERE table_schema='$DB_NAME'" "$DB_NAME" 2>/dev/null || echo "?") + box_line "Size: ${size}MB" 50 + local tables=$(mariadb $MYSQL_CRED -N -e "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='$DB_NAME'" "$DB_NAME" 2>/dev/null || echo "?") + box_line "Tables: $tables" 50 + local engine=$(mariadb $MYSQL_CRED -N -e "SELECT DISTINCT engine FROM information_schema.tables WHERE table_schema='$DB_NAME' LIMIT 1" "$DB_NAME" 2>/dev/null || echo "?") + box_line "Engine: $engine" 50 + box_bottom 50 + ;; + 2) + mariadb $MYSQL_CRED -e "SHOW TABLES FROM $DB_NAME" 2>/dev/null ;; + 3) + mariadb $MYSQL_CRED -e "SELECT table_name AS 'Table', ROUND(data_length/1024/1024, 2) AS 'Data (MB)', ROUND(index_length/1024/1024, 2) AS 'Index (MB)', ROUND((data_length+index_length)/1024/1024, 2) AS 'Total (MB)' FROM information_schema.tables WHERE table_schema='$DB_NAME' ORDER BY (data_length+index_length) DESC" 2>/dev/null ;; + 4) + spinner_start "Optimizing tables..." + local table_list=$(mariadb $MYSQL_CRED -N -e "SELECT table_name FROM information_schema.tables WHERE table_schema='$DB_NAME' AND engine='InnoDB'" "$DB_NAME" 2>/dev/null) + local count=0 + for tbl in $table_list; do + mariadb $MYSQL_CRED -e "OPTIMIZE TABLE $DB_NAME.\`$tbl\`" "$DB_NAME" &>/dev/null || true + count=$((count + 1)) + done + spinner_stop ok + ok "Optimized $count InnoDB tables" + ;; + 5) + spinner_start "Checking tables..." + if mariadb $MYSQL_CRED -e "CHECK TABLE $DB_NAME.*" "$DB_NAME" &>/dev/null; then + spinner_stop ok + else + spinner_stop warn + fi + ;; + 6) + echo -en " Enter SQL query: " + local query + read -r query + [ -n "$query" ] && mariadb $MYSQL_CRED "$DB_NAME" -e "$query" 2>/dev/null || warn "Empty query" + ;; + 7) + mariadb $MYSQL_CRED -e "SELECT name, lookAt, motto FROM $DB_NAME.users WHERE online='1'" 2>/dev/null || warn "Could not query users table" + ;; + 9|*) return 0 ;; + esac +} + +# ============================================================================= +# COMMAND: CONFIGURATION +# ============================================================================= +cmd_config() { + step "Configuration Manager" + + echo "" + box_top "CURRENT CONFIGURATION" 56 + box_line "Site URL: $NITRO_SITE_URL" 56 + box_line "Branch: $NITRO_BRANCH" 56 + box_line "Emulator Dir: $EMULATOR_DIR" 56 + box_line "Nitro-V3 Dir: $NITRO_CLIENT" 56 + box_line "Renderer Dir: $NITRO_RENDERER" 56 + box_line "Database: $DB_NAME @ $DB_HOST:$DB_PORT" 56 + box_line "Backup Dir: $BACKUP_DIR" 56 + box_line "Log Dir: $LOG_DIR" 56 + box_line "Emulator Svc: $EMULATOR_SERVICE" 56 + box_line "Socket URL: $NITRO_SOCKET_URL" 56 + box_line "API URL: $NITRO_API_URL" 56 + box_line "Gamedata URL: $NITRO_GAMEDATA_URL" 56 + box_line "Min Disk: ${MIN_DISK_GB}GB" 56 + box_bottom 56 + echo "" +} + +# ============================================================================= +# COMMAND: SYSTEM HEALTH +# ============================================================================= +cmd_health() { + step "System Health Check" + + local total_checks=0 passed=0 failed=0 warnings_count=0 + + check() { + total_checks=$((total_checks + 1)) + local desc="$1" + shift + local result + if result=$("$@" 2>&1); then + passed=$((passed + 1)) + ok "$desc" + else + failed=$((failed + 1)) + fail "$desc: $result" + fi + } + + check_w() { + total_checks=$((total_checks + 1)) + local desc="$1" + shift + if "$@" &>/dev/null; then + passed=$((passed + 1)) + ok "$desc" + else + warnings_count=$((warnings_count + 1)) + warn "$desc" + fi + } + + echo "" + info "Running comprehensive health checks..." + echo "" + + # System checks + echo -e " ${C_BOLD}System:${C_RESET}" + check_w "Bash version >= ${NITRO_MIN_BASH_VERSION}" bash -c "[[ \$(bash -c 'echo \${BASH_VERSION%%.*}') -ge ${NITRO_MIN_BASH_VERSION%.*} ]]" + check_w "Git installed" command -v git + check_w "Node.js installed" command -v node + check_w "Yarn installed" command -v yarn + check_w "Python3 installed" command -v python3 + check_w "Java installed" command -v java + check_w "Maven installed" command -v mvn + echo "" + + # Directory checks + echo -e " ${C_BOLD}Directories:${C_RESET}" + check_w "Emulator directory exists" test -d "$EMULATOR_DIR/Emulator" + check_w "Nitro-V3 directory exists" test -d "$NITRO_CLIENT" + check_w "Renderer directory exists" test -d "$NITRO_RENDERER" + check_w "Gamedata config dir exists" test -d "$GAMEDATA_CONF_DIR" + check_w "SQL directory exists" test -d "$SQL_DIR" + echo "" + + # Service checks + echo -e " ${C_BOLD}Services:${C_RESET}" + check_w "MariaDB running" systemctl is-active mariadb + check_w "Nginx running" systemctl is-active nginx + check_w "Redis running" systemctl is-active redis-server + echo "" + + # Application checks + echo -e " ${C_BOLD}Application:${C_RESET}" + check_w "Laravel artisan exists" test -f "$SCRIPT_DIR/artisan" + check_w ".env file exists" test -f "$SCRIPT_DIR/.env" + check_w "Merge config script exists" test -f "$SCRIPT_DIR/scripts/merge-config.cjs" + check_w "Socket URL reachable" timeout 5 curl -sf "$NITRO_SITE_URL" 2>/dev/null + echo "" + + # Disk space + echo -e " ${C_BOLD}Resources:${C_RESET}" + local avail_kb=$(df --output=avail "$EMULATOR_DIR" 2>/dev/null | tail -1) + local avail_gb=$((avail_kb / 1024 / 1024)) + if [ "$avail_gb" -ge "$MIN_DISK_GB" ]; then + passed=$((passed + 1)) + ok "Disk space: ${avail_gb}GB free (min: ${MIN_DISK_GB}GB)" + else + failed=$((failed + 1)) + fail "Disk space: ${avail_gb}GB free (min: ${MIN_DISK_GB}GB required!)" + fi + echo "" + + # Summary + box_top "HEALTH CHECK SUMMARY" 50 + box_line "Total checks: $total_checks" 50 + box_line "Passed: $passed" 50 + box_line "Failed: $failed" 50 + box_line "Warnings: $warnings_count" 50 + local score=$((passed * 100 / total_checks)) + if [ "$score" -ge 90 ]; then + box_line "Score: ${score}% ${C_GREEN}EXCELLENT${C_RESET}" 50 + elif [ "$score" -ge 70 ]; then + box_line "Score: ${score}% ${C_YELLOW}GOOD${C_RESET}" 50 + else + box_line "Score: ${score}% ${C_RED}NEEDS ATTENTION${C_RESET}" 50 + fi + box_bottom 50 + echo "" +} + +# ============================================================================= +# COMMAND: CLEAN CACHE +# ============================================================================= +cmd_clean() { + step "Cache Cleanup" + + local options=( + "Clean Yarn Cache" + "Clean Laravel Cache" + "Clean Redis Cache" + "Clean Old Logs (>14 days)" + "Clean Old Backups (keep 5)" + "Clean Temp Files" + "Clean Everything" + "Return to Main Menu" + ) + + local choice + choice=$(select_menu "Cleanup Options" "${options[@]}") + + case "$choice" in + 1) spinner_start "Cleaning Yarn cache..."; yarn cache clean 2>/dev/null; spinner_stop ok; ok "Yarn cache cleaned" ;; + 2) cd "$SCRIPT_DIR" && php artisan cache:clear 2>/dev/null && ok "Laravel cache cleared" || warn "Failed"; cd "$SCRIPT_DIR" && php artisan config:clear 2>/dev/null; cd "$SCRIPT_DIR" && php artisan route:clear 2>/dev/null; cd "$SCRIPT_DIR" && php artisan view:clear 2>/dev/null ;; + 3) redis-cli FLUSHALL 2>/dev/null && ok "Redis cache flushed" || warn "Redis not available" ;; + 4) local count=$(find "$EMULATOR_DIR" -name "*.log" -mtime +14 2>/dev/null | wc -l); find "$EMULATOR_DIR" -name "*.log" -mtime +14 -exec rm -f {} \; 2>/dev/null; ok "Removed $count old log file(s)" ;; + 5) local before=$(find "$BACKUP_DIR" -name '*.sql' 2>/dev/null | wc -l); find "$BACKUP_DIR" -maxdepth 1 -name '*.sql' -printf '%T@ %p\n' 2>/dev/null | sort -rn | tail -n +6 | sed 's/^[0-9.]* //' | xargs -r rm -f; local after=$(find "$BACKUP_DIR" -name '*.sql' 2>/dev/null | wc -l); ok "Backups: $before -> $after" ;; + 6) rm -rf /tmp/nitro-* 2>/dev/null; ok "Temp files cleaned" ;; + 7) + info "Running full cleanup..." + yarn cache clean 2>/dev/null || true + find "$EMULATOR_DIR" -name "*.log" -mtime +14 -exec rm -f {} \; 2>/dev/null || true + find "$BACKUP_DIR" -maxdepth 1 -name '*.sql' -printf '%T@ %p\n' 2>/dev/null | sort -rn | tail -n +6 | sed 's/^[0-9.]* //' | xargs -r rm -f || true + rm -rf /tmp/nitro-* 2>/dev/null || true + ok "Full cleanup complete" + ;; + 9|*) return 0 ;; + esac +} + +# ============================================================================= +# GIT HELPER (reused from original) +# ============================================================================= +clean_node_modules() { + rm -rf node_modules 2>/dev/null || sudo rm -rf node_modules 2>/dev/null || true + if [ "$(stat -c '%U' . 2>/dev/null)" != "$(whoami)" ] && command -v sudo &>/dev/null; then + sudo chown -R "$(whoami)":"$(whoami)" . 2>/dev/null || true + fi +} + +git_update() { + local repo="$1" branch="$2" + cd "$repo" + git stash --include-untracked 2>/dev/null || true + local old_head + old_head=$(git rev-parse HEAD 2>/dev/null || echo "") + if ! git checkout "$branch" 2>/dev/null; then + local alt="${branch^}" + if [ "$alt" != "$branch" ] && git checkout "$alt" 2>/dev/null; then + info "Branch '$branch' not found, using '$alt' instead" + branch="$alt" + else + die "Branch '$branch' (or '$alt') not found in $repo" + fi + fi + git pull 2>/dev/null || die "Git pull failed in $repo" + [ "$(git rev-parse HEAD 2>/dev/null)" != "$old_head" ] +} + +# ============================================================================= +# MAIN INTERACTIVE MENU +# ============================================================================= +cmd_interactive() { + while true; do + show_banner + + echo -e " ${C_BOLD}${C_CYAN}╔══════════════════════════════════════════════════════════════╗${C_RESET}" + echo -e " ${C_BOLD}${C_CYAN}║ MAIN CONTROL PANEL ║${C_RESET}" + echo -e " ${C_CYAN}╠══════════════════════════════════════════════════════════════╣${C_RESET}" + echo -e " ${C_CYAN}║${C_RESET} ${C_CYAN}║${C_RESET}" + echo -e " ${C_CYAN}║${C_RESET} ${C_GREEN}${C_BOLD}1)${C_RESET} ${E_ROCKET} Full Update ${C_DIM}(pull + build + deploy)${C_RESET} ${C_CYAN}║${C_RESET}" + echo -e " ${C_CYAN}║${C_RESET} ${C_GREEN}${C_BOLD}2)${C_RESET} ${E_GEAR} Update Emulator Only ${C_DIM}(Java backend)${C_RESET} ${C_CYAN}║${C_RESET}" + echo -e " ${C_CYAN}║${C_RESET} ${C_GREEN}${C_BOLD}3)${C_RESET} ${E_GEAR} Update Nitro-V3 Only ${C_DIM}(Frontend client)${C_RESET} ${C_CYAN}║${C_RESET}" + echo -e " ${C_CYAN}║${C_RESET} ${C_GREEN}${C_BOLD}4)${C_RESET} ${E_GEAR} Update Renderer Only ${C_DIM}(Nitro renderer)${C_RESET} ${C_CYAN}║${C_RESET}" + echo -e " ${C_CYAN}║${C_RESET} ${C_GREEN}${C_BOLD}5)${C_RESET} ${E_GEAR} Sync Configs Only ${C_DIM}(URL merge)${C_RESET} ${C_CYAN}║${C_RESET}" + echo -e " ${C_CYAN}║${C_RESET} ${C_CYAN}║${C_RESET}" + echo -e " ${C_CYAN}║${C_RESET} ${C_YELLOW}${C_BOLD}6)${C_RESET} ${E_DB} Database Manager ${C_CYAN}║${C_RESET}" + echo -e " ${C_CYAN}║${C_RESET} ${C_YELLOW}${C_BOLD}7)${C_RESET} ${E_SHIELD} Backup / Restore ${C_CYAN}║${C_RESET}" + echo -e " ${C_CYAN}║${C_RESET} ${C_YELLOW}${C_BOLD}8)${C_RESET} ${E_WRENCH} Service Manager ${C_CYAN}║${C_RESET}" + echo -e " ${C_CYAN}║${C_RESET} ${C_YELLOW}${C_BOLD}9)${C_RESET} ${E_CHART} System Status ${C_CYAN}║${C_RESET}" + echo -e " ${C_CYAN}║${C_RESET} ${C_YELLOW}${C_BOLD}10)${C_RESET} ${E_HEART} Health Check ${C_CYAN}║${C_RESET}" + echo -e " ${C_CYAN}║${C_RESET} ${C_YELLOW}${C_BOLD}11)${C_RESET} ${E_BROOM} Cache Cleanup ${C_CYAN}║${C_RESET}" + echo -e " ${C_CYAN}║${C_RESET} ${C_YELLOW}${C_BOLD}12)${C_RESET} ${E_EYE} View Logs ${C_CYAN}║${C_RESET}" + echo -e " ${C_CYAN}║${C_RESET} ${C_YELLOW}${C_BOLD}13)${C_RESET} ${E_GEAR} Configuration ${C_CYAN}║${C_RESET}" + echo -e " ${C_CYAN}║${C_RESET} ${C_CYAN}║${C_RESET}" + echo -e " ${C_CYAN}║${C_RESET} ${C_RED}${C_BOLD}0)${C_RESET} Exit ${C_CYAN}║${C_RESET}" + echo -e " ${C_CYAN}║${C_RESET} ${C_CYAN}║${C_RESET}" + echo -e " ${C_CYAN}╚══════════════════════════════════════════════════════════════╝${C_RESET}" + echo "" + echo -en " ${C_CYAN}${C_BOLD}Select [0-13]${C_RESET}: " + local choice + read -r choice + + case "$choice" in + 1) SELECTIVE_UPDATE=""; cmd_update ;; + 2) SELECTIVE_UPDATE="emulator"; cmd_update ;; + 3) SELECTIVE_UPDATE="client"; cmd_update ;; + 4) SELECTIVE_UPDATE="renderer"; cmd_update ;; + 5) SELECTIVE_UPDATE="configs"; cmd_update ;; + 6) cmd_database ;; + 7) + local sub_choice + sub_choice=$(select_menu "Backup & Restore" "Create Backup" "Restore Backup" "View Backups" "Return") + case "$sub_choice" in + 1) cmd_backup ;; + 2) cmd_restore ;; + 3) cmd_backups ;; + 4|*) ;; + esac + ;; + 8) cmd_services ;; + 9) cmd_status ;; + 10) cmd_health ;; + 11) cmd_clean ;; + 12) cmd_logs ;; + 13) cmd_config ;; + 0|q|Q) echo -e "\n ${C_GREEN}Goodbye!${C_RESET}\n"; cursor_show; exit 0 ;; + *) warn "Invalid selection" ;; + esac + + echo "" + echo -en " ${C_DIM}Press Enter to continue...${C_RESET}" + read -r + done +} + +# ============================================================================= +# COMMAND: HELP +# ============================================================================= +show_help() { + show_mini_banner + + echo -e " ${C_BOLD}USAGE:${C_RESET}" + echo -e " $SCRIPT_NAME ${C_CYAN}${C_RESET} [${C_DIM}options${C_RESET}]" + echo "" + + echo -e " ${C_BOLD}COMMANDS:${C_RESET}" + echo -e " ${C_GREEN}update${C_RESET} Run full update (default when no command)" + echo -e " ${C_GREEN}interactive${C_RESET} Launch interactive TUI control panel" + echo -e " ${C_GREEN}status${C_RESET} Show system status dashboard" + echo -e " ${C_GREEN}health${C_RESET} Run comprehensive health check" + echo -e " ${C_GREEN}backup${C_RESET} Create database backup" + echo -e " ${C_GREEN}restore${C_RESET} Restore from backup" + echo -e " ${C_GREEN}backups${C_RESET} List all backups" + echo -e " ${C_GREEN}services${C_RESET} Service management menu" + echo -e " ${C_GREEN}database${C_RESET} Database tools menu" + echo -e " ${C_GREEN}config${C_RESET} Show current configuration" + echo -e " ${C_GREEN}logs${C_RESET} View update logs" + echo -e " ${C_GREEN}clean${C_RESET} Cache cleanup menu" + echo -e " ${C_GREEN}help${C_RESET} Show this help" + echo "" + + echo -e " ${C_BOLD}OPTIONS:${C_RESET}" + echo -e " ${C_CYAN}--dry-run, -n${C_RESET} Preview without making changes" + echo -e " ${C_CYAN}--force, -f${C_RESET} Force update even if up to date" + echo -e " ${C_CYAN}--verbose, -v${C_RESET} Show debug output" + echo -e " ${C_CYAN}--quiet, -q${C_RESET} Suppress output except errors" + echo -e " ${C_CYAN}--branch, -b${C_RESET} Git branch (main/dev/staging)" + echo -e " ${C_CYAN}--url, -u${C_RESET} Site URL" + echo -e " ${C_CYAN}--only=${C_RESET} Selective update: emulator|client|renderer|configs" + echo -e " ${C_CYAN}--version, -V${C_RESET} Show version" + echo -e " ${C_CYAN}--help, -h${C_RESET} Show this help" + echo "" + + echo -e " ${C_BOLD}EXAMPLES:${C_RESET}" + echo -e " ${C_DIM}$SCRIPT_NAME interactive${C_RESET}" + echo -e " ${C_DIM}$SCRIPT_NAME update --branch dev --dry-run${C_RESET}" + echo -e " ${C_DIM}$SCRIPT_NAME update --only=emulator --force${C_RESET}" + echo -e " ${C_DIM}$SCRIPT_NAME backup${C_RESET}" + echo -e " ${C_DIM}$SCRIPT_NAME status${C_RESET}" + echo -e " ${C_DIM}$SCRIPT_NAME health --verbose${C_RESET}" + echo "" + + echo -e " ${C_BOLD}ENVIRONMENT VARIABLES:${C_RESET}" + echo -e " ${C_CYAN}NITRO_SITE_URL${C_RESET} Site URL (or use .env APP_URL)" + echo -e " ${C_CYAN}NITRO_BRANCH${C_RESET} Git branch (default: main)" + echo -e " ${C_CYAN}NITRO_DB_NAME${C_RESET} Database name (default: habbo)" + echo -e " ${C_CYAN}NITRO_DB_HOST${C_RESET} Database host (default: 127.0.0.1)" + echo -e " ${C_CYAN}NITRO_DB_PORT${C_RESET} Database port (default: 3306)" + echo -e " ${C_CYAN}NITRO_DB_USER${C_RESET} Database user (default: root)" + echo -e " ${C_CYAN}NITRO_DB_PASS${C_RESET} Database password" + echo -e " ${C_CYAN}NITRO_EMULATOR_SERVICE${C_RESET} Systemd service name (default: emulator)" + echo -e " ${C_CYAN}NITRO_EMULATOR_PATH${C_RESET} Emulator directory" + echo -e " ${C_CYAN}NITRO_CLIENT_DIR${C_RESET} Nitro-V3 client directory" + echo -e " ${C_CYAN}NITRO_RENDERER_SRC${C_RESET} Nitro renderer directory" + echo -e " ${C_CYAN}NITRO_NOTIFY_URL${C_RESET} Webhook URL for notifications" + echo -e " ${C_CYAN}NITRO_MIN_DISK_GB${C_RESET} Minimum disk space in GB (default: 5)" + echo "" +} + +# ============================================================================= +# ARGUMENT PARSING +# ============================================================================= +parse_args() { + local cmd="" + + while [ $# -gt 0 ]; do + case "$1" in + update|up) cmd="update" ;; + interactive|menu|ui) cmd="interactive" ;; + status|st) cmd="status" ;; + health|check) cmd="health" ;; + backup|bk) cmd="backup" ;; + restore|rs) cmd="restore" ;; + backups|bl) cmd="backups" ;; + services|svc) cmd="services" ;; + database|db) cmd="database" ;; + config|cfg) cmd="config" ;; + logs|log) cmd="logs" ;; + clean|cl) cmd="clean" ;; + help|-h|--help) cmd="help" ;; + version|-V|--version) echo "Nitro V3 Update System v${SCRIPT_VERSION} (build ${SCRIPT_BUILD})"; exit 0 ;; + --dry-run|-n) DRY_RUN=true ;; + --force|-f) FORCE=true ;; + --verbose|-v) VERBOSE=true ;; + --quiet|-q) QUIET=true ;; + --branch|-b) shift; NITRO_BRANCH="$1" ;; + --url|-u) shift; NITRO_SITE_URL="$1" ;; + --only=*) SELECTIVE_UPDATE="${1#*=}" ;; + -*) die "Unknown option: $1 (use --help for usage)" ;; + *) [ -z "$cmd" ] && cmd="update" ;; + esac + shift + done + + # Validate selective update + if [ -n "$SELECTIVE_UPDATE" ]; then + case "$SELECTIVE_UPDATE" in + emulator|client|renderer|configs) ;; + *) die "Invalid --only value: $SELECTIVE_UPDATE (use: emulator, client, renderer, configs)" ;; + esac + fi + + # Default command + [ -z "$cmd" ] && cmd="update" + + echo "$cmd" +} + +# ============================================================================= +# MAIN +# ============================================================================= +main() { + # Unbuffer re-exec + if [ -z "${_UNBUFFERED:-}" ] && command -v unbuffer &>/dev/null; then + export _UNBUFFERED=1 + exec unbuffer bash "$0" "$@" + fi + + # Single-instance lock + LOCKFILE="/tmp/$(basename "$0").lock" + exec 200>"$LOCKFILE" + flock -n 200 2>/dev/null || die "Another instance is already running" + + # Derive NITRO_SITE_URL from APP_URL if not set + if [ -z "${NITRO_SITE_URL:-}" ] && [ -n "${APP_URL:-}" ]; then + NITRO_SITE_URL="$APP_URL" + fi + + # Configuration + DB_NAME="${NITRO_DB_NAME:-habbo}" + DB_HOST="${NITRO_DB_HOST:-127.0.0.1}" + DB_PORT="${NITRO_DB_PORT:-3306}" + DB_USER="${NITRO_DB_USER:-root}" + DB_PASS="${NITRO_DB_PASS:-}" + EMULATOR_SERVICE="${NITRO_EMULATOR_SERVICE:-emulator}" + EMULATOR_DIR="${NITRO_EMULATOR_PATH:-/var/www/emulator}" + SQL_DIR="${NITRO_SQL_DIR:-$EMULATOR_DIR/Database Updates}" + GAMEDATA_CONF_DIR="${NITRO_GAMEDATA_DIR:-/var/www/Gamedata/config}" + NITRO_SRC_DIR="${NITRO_CLIENT_DIR:-/var/www/Nitro-V3/public/configuration}" + BACKUP_DIR="${NITRO_BACKUP_DIR:-$EMULATOR_DIR/Database Updates/backups}" + NITRO_CLIENT="${NITRO_CLIENT_SRC:-/var/www/Nitro-V3}" + NITRO_RENDERER="${NITRO_RENDERER_SRC:-/var/www/Nitro_Render_V3}" + NITRO_BRANCH="${NITRO_BRANCH:-main}" + MIN_DISK_GB="${NITRO_MIN_DISK_GB:-5}" + HEALTH_RETRIES="${NITRO_HEALTH_RETRIES:-12}" + HEALTH_INTERVAL="${NITRO_HEALTH_INTERVAL:-5}" + + # Derive ws/wss protocol and domain + case "${NITRO_SITE_URL:-}" in + https://*) NITRO_WS_PROTO="wss://" ; NITRO_DOMAIN="${NITRO_SITE_URL#https://}" ;; + http://*) NITRO_WS_PROTO="ws://" ; NITRO_DOMAIN="${NITRO_SITE_URL#http://}" ;; + *) NITRO_WS_PROTO="wss://" ; NITRO_DOMAIN="${NITRO_SITE_URL:-}" ;; + esac + + # Critical URLs + NITRO_IMAGE_LIBRARY_URL="${NITRO_IMAGE_LIBRARY_URL:-${NITRO_SITE_URL:-}/gamedata/c_images/}" + NITRO_HOF_FURNITURE_URL="${NITRO_HOF_FURNITURE_URL:-${NITRO_SITE_URL:-}/gamedata/icons}" + NITRO_API_URL="${NITRO_API_URL:-${NITRO_SITE_URL:-}}" + NITRO_SOCKET_URL="${NITRO_SOCKET_URL:-${NITRO_WS_PROTO}ws.${NITRO_DOMAIN}}" + NITRO_CAMERA_URL="${NITRO_CAMERA_URL:-${NITRO_SITE_URL:-}/camera/photo/}" + NITRO_GAMEDATA_URL="${NITRO_GAMEDATA_URL:-${NITRO_SITE_URL:-}/gamedata}" + NITRO_ASSET_URL="${NITRO_ASSET_URL:-${NITRO_SITE_URL:-}/gamedata/bundled}" + NITRO_FURNI_ASSET_ICON_URL="${NITRO_FURNI_ASSET_ICON_URL:-${NITRO_SITE_URL:-}/gamedata/icons/%libname%%param%_icon.png}" + + # MySQL credentials + MYSQL_CRED="-h $DB_HOST -P $DB_PORT -u $DB_USER --ssl-verify-server-cert=OFF" + [ -n "$DB_PASS" ] && export MYSQL_PWD="$DB_PASS" + + # Setup logging + exec > >(tee -a "$LOG_FILE") 2>&1 + + # Parse arguments and dispatch + local cmd + cmd=$(parse_args "$@") + + case "$cmd" in + interactive) cmd_interactive ;; + status) cmd_status ;; + health) cmd_health ;; + backup) cmd_backup ;; + restore) cmd_restore ;; + backups) cmd_backups ;; + services) cmd_services ;; + database) cmd_database ;; + config) cmd_config ;; + logs) cmd_logs ;; + clean) cmd_clean ;; + help) show_help ;; + update) cmd_update ;; + esac + + cursor_show +} + +main "$@"