diff --git a/update-Nitrov3.sh b/update-Nitrov3.sh index d908902..66ad40c 100755 --- a/update-Nitrov3.sh +++ b/update-Nitrov3.sh @@ -1,550 +1,250 @@ #!/bin/bash -set -euo pipefail # ╔══════════════════════════════════════════════════════════════════════════════╗ # ║ ║ -# ║ ███╗ ██╗███████╗██╗ ██╗██████╗ ██████╗ ███████╗██╗ ██╗ ║ -# ║ ████╗ ██║██╔════╝██║ ██║██╔══██╗██╔═══██╗██╔════╝██║ ██║ ║ -# ║ ██╔██╗ ██║█████╗ ██║ ██║██████╔╝██║ ██║███████╗██║ ██║ ║ -# ║ ██║╚██╗██║██╔══╝ ██║ ██║██╔══██╗██║ ██║╚════██║██║ ██║ ║ -# ║ ██║ ╚████║███████╗╚██████╔╝██║ ██║╚██████╔╝███████║╚██████╔╝ ║ -# ║ ╚═╝ ╚═══╝╚══════╝ ╚═════╝ ╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═════╝ ║ +# ║ ███████╗███╗ ███╗ █████╗ ██████╗ ████████╗ ║ +# ║ ██╔════╝████╗ ████║██╔══██╗██╔══██╗╚══██╔══╝ ║ +# ║ █████╗ ██╔████╔██║███████║██████╔╝ ██║ ║ +# ║ ██╔══╝ ██║╚██╔╝██║██╔══██║██╔══██╗ ██║ ║ +# ║ ███████╗██║ ╚═╝ ██║██║ ██║██║ ██║ ██║ ║ +# ║ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝ ║ # ║ ║ -# ║ NITRO V3 — Enterprise Deployment & Management System ║ -# ║ Version 6.0.0 — Build 20260625 ║ +# ║ Emulator / Nitro-V3 / Nitro-V3-Render ║ +# ║ Updater by Remco — epicnabbo.nl ║ # ║ ║ # ╚══════════════════════════════════════════════════════════════════════════════╝ # ============================================================================= -# CONFIGURATION & CONSTANTS +# CONSTANTS # ============================================================================= -readonly SCRIPT_VERSION="6.0.0" +readonly SCRIPT_VERSION="7.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" - -# --- 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_ITALIC="\033[3m" - 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" - C_BG_WHITE="\033[47m" - C_GOLD="\033[38;5;220m" - C_ORANGE="\033[38;5;208m" - C_PURPLE="\033[38;5;135m" - C_TEAL="\033[38;5;30m" -else - C_RESET="" C_BOLD="" C_DIM="" C_UNDERLINE="" C_BLINK="" C_ITALIC="" - 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="" C_BG_WHITE="" - C_GOLD="" C_ORANGE="" C_PURPLE="" C_TEAL="" -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="•" E_GLOBE="🌐" - E_CONSOLE="🖥" E_SERVER="🖧" E_DATABASE="💾" E_DIAG="🩺" -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="-" E_GLOBE="[W]" - E_CONSOLE="[M]" E_SERVER="[S]" E_DATABASE="[D]" E_DIAG="[X]" -fi - -# --- Timing ------------------------------------------------------------------- readonly START_TIME=$(date +%s) -# --- State -------------------------------------------------------------------- -STEP_NUM=0 -STEP_TOTAL=8 +# ============================================================================= +# 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_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_GOLD="\033[38;5;220m" + 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_RED="" C_GREEN="" C_YELLOW="" C_BLUE="" C_MAGENTA="" C_CYAN="" C_WHITE="" C_GOLD="" + C_BG_RED="" C_BG_GREEN="" C_BG_YELLOW="" C_BG_BLUE="" C_BG_MAGENTA="" C_BG_CYAN="" +fi + +# ============================================================================= +# EMOJIS +# ============================================================================= +if locale charmap 2>/dev/null | grep -qi utf; then + E_OK="✔" E_FAIL="✘" E_WARN="⚠" E_ARROW="➜" E_DOT="●" + E_GEAR="⚙" E_ROCKET="🚀" E_SHIELD="🛡" E_DB="🗄" E_GIT="🔀" + E_WRENCH="🔧" E_CHART="📊" E_BOLT="⚡" E_BROOM="🧹" E_EYE="👁" + E_LOCK="🔒" E_HEART="♥" E_CLOUD="☁" E_FIRE="🔥" E_GLOBE="🌐" + E_CONSOLE="🖥" E_CLOCK="⏱" +else + E_OK="[OK]" E_FAIL="[!]" E_WARN="[!]" E_ARROW="->" E_DOT="*" + E_GEAR="[G]" E_ROCKET="[R]" E_SHIELD="[S]" E_DB="[DB]" E_GIT="[G]" + E_WRENCH="[W]" E_CHART="[C]" E_BOLT="[!]" E_BROOM="[C]" E_EYE="[E]" + E_LOCK="[L]" E_HEART="[H]" E_CLOUD="[C]" E_FIRE="[F]" E_GLOBE="[W]" + E_CONSOLE="[M]" E_CLOCK="[T]" +fi + +# ============================================================================= +# STATE +# ============================================================================= +DRY_RUN=false +SELECTIVE_UPDATE="" 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=() +LAST_BACKUP="" +ROLLBACK_NEEDED=false +NOTIFY_FAILED=false -# --- Load .env ---------------------------------------------------------------- -if [ -f "$SCRIPT_DIR/.env" ]; then - set -a - . "$SCRIPT_DIR/.env" - set +a -fi +# ============================================================================= +# LOAD .ENV +# ============================================================================= +if [ -f "$SCRIPT_DIR/.env" ]; then set -a; . "$SCRIPT_DIR/.env"; set +a; fi -# --- Logging ------------------------------------------------------------------ +# ============================================================================= +# CONFIG +# ============================================================================= LOG_DIR="${NITRO_LOG_DIR:-$SCRIPT_DIR/storage/logs}" mkdir -p "$LOG_DIR" 2>/dev/null || true LOG_FILE="$LOG_DIR/update-$(date +%Y%m%d-%H%M%S).log" - -# --- Notifications ------------------------------------------------------------ NOTIFY_URL="${NITRO_NOTIFY_URL:-}" +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}" + +if [ -z "${NITRO_SITE_URL:-}" ] && [ -n "${APP_URL:-}" ]; then NITRO_SITE_URL="$APP_URL"; fi +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 +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_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_CRED="-h $DB_HOST -P $DB_PORT -u $DB_USER --ssl-verify-server-cert=OFF" +[ -n "$DB_PASS" ] && export MYSQL_PWD="$DB_PASS" # ============================================================================= -# TERMINAL CAPABILITIES +# UI HELPERS # ============================================================================= -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"; } - -# ============================================================================= -# UI COMPONENTS -# ============================================================================= - -show_banner() { - clear - echo -e "${C_CYAN}" - echo "" - echo " ╔═══════════════════════════════════════════════════════════════════════════╗" - echo " ║ ║" - echo " ║ ███████╗███╗ ███╗ █████╗ ██████╗ ████████╗ ║" - echo " ║ ██╔════╝████╗ ████║██╔══██╗██╔══██╗╚══██╔══╝ ║" - echo " ║ █████╗ ██╔████╔██║███████║██████╔╝ ██║ ║" - echo " ║ ██╔══╝ ██║╚██╔╝██║██╔══██║██╔══██╗ ██║ ║" - echo " ║ ███████╗██║ ╚═╝ ██║██║ ██║██║ ██║ ██║ ║" - echo " ║ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝ ║" - echo " ║ ║" - echo " ╠═══════════════════════════════════════════════════════════════════════════╣" - echo " ║ ║" - echo -e " ║ ${C_GOLD}${C_BOLD} Emulator / Nitro-V3 / Nitro-V3-Render${C_CYAN} ║" - echo " ║ ║" - echo -e " ║ ${C_WHITE}${C_BOLD} Updater by Remco — epicnabbo.nl${C_CYAN} ║" - echo " ║ ║" - echo " ╠═══════════════════════════════════════════════════════════════════════════╣" - echo -e " ║ ${C_DIM}Enterprise Deployment & Management System v${SCRIPT_VERSION}${C_CYAN} ║" - echo " ╚═══════════════════════════════════════════════════════════════════════════╝" - echo -e "${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_RESET}" - echo -e "${C_CYAN}║${C_RESET} ${E_ROCKET} ${C_GOLD}${C_BOLD}Emulator/Nitro-V3/Nitro-V3-Render Updater${C_RESET} ${C_CYAN}║${C_RESET}" - echo -e "${C_CYAN}║${C_RESET} ${C_DIM}by Remco — epicnabbo.nl v${SCRIPT_VERSION}${C_RESET} ${C_CYAN}║${C_RESET}" - echo -e "${C_CYAN}╚══════════════════════════════════════════════════════════════════╝${C_RESET}" - echo "" -} - -box_top() { - local title="${1:-}" width="${2:-60}" - if [ -n "$title" ]; then - local pad=$((width - ${#title} - 6)) - [ $pad -lt 0 ] && pad=0 - 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 clean_text=$(echo -e "$text" | sed 's/\x1b\[[0-9;]*m//g') - local pad=$((width - ${#clean_text} - 4)) - [ $pad -lt 0 ] && pad=0 - printf "${C_CYAN}║${C_RESET} %s%*s${C_CYAN}║${C_RESET}\n" "$text" $pad "" -} - -box_kv() { - local key="${1}" val="${2}" width="${3:-60}" - local clean_key=$(echo -e "$key" | sed 's/\x1b\[[0-9;]*m//g') - local clean_val=$(echo -e "$val" | sed 's/\x1b\[[0-9;]*m//g') - local pad=$((width - ${#clean_key} - ${#clean_val} - 6)) - [ $pad -lt 0 ] && pad=0 - printf "${C_CYAN}║${C_RESET} %s%*s%s ${C_CYAN}║${C_RESET}\n" "$key" $pad "" "$val" -} - -box_bottom() { - local width="${1:-60}" - printf "${C_CYAN}╚" - printf '═%.0s' $(seq 1 $width) - echo -e "╝${C_RESET}" -} - -box_sep() { - local width="${1:-60}" - printf "${C_CYAN}╠${C_RESET}" - printf '═%.0s' $(seq 1 $width) - echo -e "╣${C_RESET}" -} - -step() { - STEP_NUM=$((STEP_NUM + 1)) - echo "" - echo -e " ${C_BG_CYAN}${C_WHITE}${C_BOLD} Step ${STEP_NUM}/${STEP_TOTAL} │ $1 ${C_RESET}" - echo "" -} - -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"; } -debug() { [ "$VERBOSE" = true ] && echo -e " ${C_DIM}${E_DOT} [DEBUG] $1${C_RESET}"; log_write "DEBUG" "$1"; } - -IN_INTERACTIVE=false +log_write() { printf "[%s] [%s] %s\n" "$(date '+%Y-%m-%d %H:%M:%S')" "$1" "$2" >> "$LOG_FILE" 2>/dev/null || true; } +info() { echo -e " ${C_BLUE}${E_ARROW}${C_RESET} $1"; log_write "INFO" "$1"; } +ok() { echo -e " ${C_GREEN}${E_OK}${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_FAIL}${C_RESET} $1"; log_write "FAIL" "$1"; } +debug() { echo -e " ${C_DIM}${E_DOT} $1${C_RESET}"; log_write "DEBUG" "$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}" + echo -e " ${C_BG_RED}${C_WHITE}${C_BOLD} ERROR ${C_RESET} ${C_RED}$1${C_RESET}" log_write "FATAL" "$1" - if [ "$IN_INTERACTIVE" = true ]; then - WARNINGS=$((WARNINGS + 1)) - echo -e " ${C_YELLOW}Returning to menu...${C_RESET}" - return 1 2>/dev/null || true - fi cleanup_notify_failure "$1" cursor_show exit 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 -} +cursor_hide() { tput civis 2>/dev/null || true; } +cursor_show() { tput cnorm 2>/dev/null || true; } +clear_line() { printf "\r\033[K"; } -# --- 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 + ( local i=0; while true; do printf "\r ${C_CYAN}${frames[$i]}${C_RESET} %s " "$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 + local st="${1:-ok}" + [ -n "$SPINNER_PID" ] && kill "$SPINNER_PID" 2>/dev/null && wait "$SPINNER_PID" 2>/dev/null; SPINNER_PID="" + clear_line; cursor_show + case "$st" in ok) printf "\r ${C_GREEN}${E_OK}${C_RESET} Done\n" ;; warn) printf "\r ${C_YELLOW}${E_WARN}${C_RESET} Done\n" ;; fail) printf "\r ${C_RED}${E_FAIL}${C_RESET} Failed\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" +step() { + local num="${1}" total="${2}" title="${3}" + echo "" + echo -e " ${C_BG_CYAN}${C_WHITE}${C_BOLD} Step ${num}/${total} — ${title} ${C_RESET}" + echo "" } confirm() { local msg="${1:-Continue?}" default="${2:-y}" - local hint="[Y/n]" - [ "$default" = "n" ] && hint="[y/N]" + 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}" + local reply; read -r reply; reply="${reply:-$default}" [[ "$reply" =~ ^[Yy] ]] } -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 +box_top() { + local title="${1:-}" width="${2:-60}" + if [ -n "$title" ]; then + local pad=$((width - ${#title} - 6)); [ $pad -lt 0 ] && pad=0 + 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 - return 1 } +box_kv() { + local key="${1}" val="${2}" width="${3:-60}" + local ck=$(echo -e "$key" | sed 's/\x1b\[[0-9;]*m//g') + local cv=$(echo -e "$val" | sed 's/\x1b\[[0-9;]*m//g') + local pad=$((width - ${#ck} - ${#cv} - 6)); [ $pad -lt 0 ] && pad=0 + printf "${C_CYAN}║${C_RESET} %s%*s%s ${C_CYAN}║${C_RESET}\n" "$key" $pad "" "$val" +} +box_line() { + local text="${1:-}" width="${2:-60}" + local ct=$(echo -e "$text" | sed 's/\x1b\[[0-9;]*m//g') + local pad=$((width - ${#ct} - 4)); [ $pad -lt 0 ] && pad=0 + printf "${C_CYAN}║${C_RESET} %s%*s${C_CYAN}║${C_RESET}\n" "$text" $pad "" +} +box_bottom() { local w="${1:-60}"; printf "${C_CYAN}╚"; printf '═%.0s' $(seq 1 $w); echo -e "╝${C_RESET}"; } -# --- Notifications ----------------------------------------------------------- +# ============================================================================= +# NOTIFICATIONS +# ============================================================================= notify() { local status="$1" message="$2" [ -z "$NOTIFY_URL" ] && return 0 local elapsed=$(($(date +%s) - START_TIME)) - local elapsed_fmt - 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 v${SCRIPT_VERSION}] $status\",\"attachments\":[{\"color\":\"$color\",\"fields\":[{\"title\":\"Status\",\"value\":\"$message\",\"short\":true},{\"title\":\"Duration\",\"value\":\"$elapsed_fmt\",\"short\":true},{\"title\":\"Server\",\"value\":\"$(hostname)\",\"short\":true},{\"title\":\"Log\",\"value\":\"$LOG_FILE\",\"short\":false}]}]}" 2>/dev/null || true -} - -cleanup_notify_failure() { - $NOTIFY_FAILED && return - NOTIFY_FAILED=true - notify "FAILED" "$1" -} - -# ============================================================================= -# 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; } - info "Rolling back database from: $LAST_BACKUP" - mariadb $MYSQL_CRED "$DB_NAME" < "$LAST_BACKUP" && ok "Database restored." || warn "Rollback failed." + local ef; ef=$(printf '%dm %ds' $((elapsed / 60)) $((elapsed % 60))) + local color="good"; [ "$status" != "SUCCESS" ] && color="danger" + curl -s -o /dev/null -X POST "$NOTIFY_URL" -H "Content-Type: application/json" \ + -d "{\"text\":\"[Nitro Update] $status\",\"attachments\":[{\"color\":\"$color\",\"fields\":[{\"title\":\"Status\",\"value\":\"$message\",\"short\":true},{\"title\":\"Duration\",\"value\":\"$ef\",\"short\":true}]}]}" 2>/dev/null || true } +cleanup_notify_failure() { $NOTIFY_FAILED && return; NOTIFY_FAILED=true; notify "FAILED" "$1"; } cleanup_on_exit() { - local exit_code=$? - cursor_show - if [ $exit_code -ne 0 ] && [ "$ROLLBACK_NEEDED" = true ]; then - echo "" - echo -e " ${C_BG_RED}${C_WHITE}${C_BOLD} UPDATE FAILED — INITIATING ROLLBACK ${C_RESET}" - echo "" - rollback_db + local ec=$?; cursor_show + if [ $ec -ne 0 ] && [ "$ROLLBACK_NEEDED" = true ]; then + echo -e "\n ${C_BG_RED}${C_WHITE}${C_BOLD} ROLLBACK ${C_RESET}" + [ -n "$LAST_BACKUP" ] && [ -f "$LAST_BACKUP" ] && mariadb $MYSQL_CRED "$DB_NAME" < "$LAST_BACKUP" 2>/dev/null && ok "DB restored" || warn "Rollback failed" fi - [ $exit_code -ne 0 ] && cleanup_notify_failure "Script exited with code $exit_code" + [ $ec -ne 0 ] && cleanup_notify_failure "Exit code $ec" } - trap cleanup_on_exit EXIT -trap 'echo ""; cursor_show; die "Interrupted by user (SIGINT)"' SIGINT -trap 'cursor_show; die "Terminated (SIGTERM)"' SIGTERM - -# ============================================================================= -# PREFLIGHT -# ============================================================================= -preflight() { - 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 - if [ -z "${NITRO_BRANCH:-}" ]; then - read -r -p " Enter branch [main]: " NITRO_BRANCH - NITRO_BRANCH="${NITRO_BRANCH:-main}" - export NITRO_BRANCH - fi -} - -# ============================================================================= -# PRE-FLIGHT CHECKS -# ============================================================================= -preflight_checks() { - step "System Diagnostics" - - 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 - 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" - - 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" - done - ok "All ${#REQUIRED_DIRS[@]} directories exist" - - 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 (min ${MIN_DISK_GB}GB required)" - ok "${AVAIL_GB}GB disk space available" - - 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") - [ "$TOTAL_MEM" -gt 0 ] && ok "Memory: ${AVAIL_MEM}MB available / ${TOTAL_MEM}MB total" || warn "Could not determine memory" - - 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" - - 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" - - info "Checking Laravel..." - 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 - - info "Checking Redis..." - if command -v redis-cli &>/dev/null; then - redis-cli ping 2>/dev/null | grep -q PONG && ok "Redis: Connected" || warn "Redis: Not responding" - else - warn "redis-cli not found" - fi - - info "Checking Nginx..." - if command -v nginx &>/dev/null; then - nginx -t 2>/dev/null && ok "Nginx config valid" || warn "Nginx config has issues" - fi - - 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") - SSL_DAYS_LEFT=$(( (SSL_EXPIRY_EPOCH - $(date +%s)) / 86400 )) - if [ "$SSL_DAYS_LEFT" -gt 30 ]; then - ok "SSL valid for $SSL_DAYS_LEFT days" - elif [ "$SSL_DAYS_LEFT" -gt 0 ]; then - warn "SSL expires in $SSL_DAYS_LEFT days!" - else - fail "SSL certificate EXPIRED!" - ERRORS=$((ERRORS + 1)) - fi - fi - fi - - 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%%:*}" 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-8 || echo "?") - local dirty=$(cd "$path" && git status --porcelain 2>/dev/null | wc -l) - [ "$dirty" -gt 0 ] && warn "$name has $dirty uncommitted changes" || ok "$name is clean ($branch @ $commit)" - fi - done -} - -# ============================================================================= -# 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:-main}" 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." -} +trap 'cursor_show; exit 1' SIGINT SIGTERM # ============================================================================= # GIT HELPERS # ============================================================================= -detect_available_branches() { +detect_branches() { local repos=("$EMULATOR_DIR/Emulator" "$NITRO_CLIENT" "$NITRO_RENDERER") - local result=() + local result="" for repo in "${repos[@]}"; do [ -d "$repo/.git" ] || continue local branches - branches=$(cd "$repo" && git branch -r 2>/dev/null | \ - grep -v '\->' | sed 's/^[[:space:]]*//' | sed 's/^origin\///' | grep -v '^HEAD$' | sort -u) + branches=$(cd "$repo" 2>/dev/null && git branch -r 2>/dev/null | grep -v '\->' | sed 's/^[[:space:]]*//' | sed 's/^origin\///' | grep -v '^HEAD$' | sort -u) while IFS= read -r b; do [ -z "$b" ] && continue - local found=false - for ab in "${result[@]}"; do - [ "$ab" = "$b" ] && found=true && break - done - $found || result+=("$b") + echo "$result" | grep -qx "$b" 2>/dev/null || result="${result:+$result +}$b" done <<< "$branches" done - printf '%s\n' "${result[@]}" + echo "$result" } -detect_branch() { +detect_best_branch() { local repo="$1" preferred="$2" cd "$repo" 2>/dev/null || { echo "main"; return; } - local variants=("$preferred" "${preferred^}" "development" "Development" "main" "master") - for v in "${variants[@]}"; do + for v in "$preferred" "${preferred^}" "main" "master"; do git rev-parse --verify "$v" &>/dev/null && { echo "$v"; return; } done - local remote_branch=$(git branch -r 2>/dev/null | grep -oP "origin/\K${preferred}" | head -1 || echo "") - [ -n "$remote_branch" ] && { echo "$remote_branch"; return; } echo "$(git branch --show-current 2>/dev/null || echo "main")" } @@ -552,871 +252,641 @@ git_update() { local repo="$1" branch="$2" cd "$repo" || { warn "Cannot access $repo"; return 1; } git stash --include-untracked 2>/dev/null || true - local old_head - old_head=$(git rev-parse HEAD 2>/dev/null || echo "") - local resolved_branch - resolved_branch=$(detect_branch "$repo" "$branch") - if [ "$resolved_branch" != "$branch" ]; then - info "Branch '$branch' not in $(basename "$repo"), using '$resolved_branch' instead" - fi - if ! git checkout "$resolved_branch" 2>/dev/null; then - local fallback=$(git branch -r 2>/dev/null | head -1 | sed 's/origin\///' | xargs || echo "main") - warn "Could not checkout '$resolved_branch', trying '$fallback'" - if ! git checkout "$fallback" 2>/dev/null; then - warn "No suitable branch found in $(basename "$repo"), skipping" - return 1 - fi - resolved_branch="$fallback" - fi - if ! git pull origin "$resolved_branch" 2>/dev/null && ! git pull 2>/dev/null; then - warn "Git pull failed in $(basename "$repo") (branch: $resolved_branch)" - return 1 + local old_head; old_head=$(git rev-parse HEAD 2>/dev/null || echo "") + local rb; rb=$(detect_best_branch "$repo" "$branch") + [ "$rb" != "$branch" ] && info "Branch '$branch' not in $(basename "$repo"), using '$rb'" + git checkout "$rb" 2>/dev/null || { warn "Cannot checkout '$rb' in $(basename "$repo")"; return 1; } + if ! git pull origin "$rb" 2>/dev/null && ! git pull 2>/dev/null; then + warn "Pull failed in $(basename "$repo")"; return 1 fi [ "$(git rev-parse HEAD 2>/dev/null)" != "$old_head" ] } 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 } # ============================================================================= # UPDATE FUNCTIONS # ============================================================================= update_emulator() { - step "Update & Build Emulator" + step 2 8 "Update & Build Emulator" if git_update "$EMULATOR_DIR/Emulator" "$NITRO_BRANCH"; then - info "New commits detected" - HAD_UPDATES=true - ROLLBACK_NEEDED=true - UPDATED_REPOS+=("Emulator") - info "Creating database backup..." + HAD_UPDATES=true; ROLLBACK_NEEDED=true; UPDATED_REPOS+=("Emulator") mkdir -p "$BACKUP_DIR" - BACKUP_FILE="$BACKUP_DIR/backup_$(date +%Y%m%d_%H%M%S).sql" + local bf="$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 too small — aborting"; fi - spinner_stop ok - ok "Backup saved: $(numfmt --to=iec "$BK_SIZE") — $(basename "$BACKUP_FILE")" + if mariadb-dump $MYSQL_CRED --force --skip-lock-tables --routines --events --triggers "$DB_NAME" > "$bf" 2>/dev/null; then + LAST_BACKUP="$bf" + local bks=$(stat -c%s "$bf" 2>/dev/null || echo 0) + [ "$bks" -lt 1024 ] && { rm -f "$bf"; spinner_stop fail; die "Backup too small"; } + spinner_stop ok; ok "Backup: $(numfmt --to=iec "$bks")" else - spinner_stop fail - die "Database backup failed" + spinner_stop fail; die "Database backup failed" fi - info "Checking for new SQL files..." if [ -d "$SQL_DIR" ]; then - SQL_COUNT=0 - while IFS= read -r -d '' sql_file; do - info_indent "Importing: $(basename "$sql_file")" - mariadb $MYSQL_CRED --force "$DB_NAME" < "$sql_file" 2>/dev/null || 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" + local sc=0 + while IFS= read -r -d '' sf; do info "SQL: $(basename "$sf")"; mariadb $MYSQL_CRED --force "$DB_NAME" < "$sf" 2>/dev/null || warn "SQL failed: $(basename "$sf")"; sc=$((sc+1)); done < <(find "$SQL_DIR" -name '*.sql' -mmin -10 -not -path "$BACKUP_DIR/*" -print0 2>/dev/null) + [ "$sc" -gt 0 ] && ok "$sc SQL file(s) imported" fi - info "Building emulator..." cd "$EMULATOR_DIR/Emulator" - spinner_start "Compiling Java..." + spinner_start "Building emulator..." mvn package -q 2>&1 | tail -5 && spinner_stop ok || { spinner_stop fail; die "Maven build failed"; } - 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 2>/dev/null || echo "") - [ -z "$JAR_FILE" ] && die "No jar file found" - ok "Jar: $JAR_FILE" - info "Updating launch script..." + local jar=$(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" ] && die "No jar found" + ok "Jar: $jar" cd target/ - cat << EOF > emulator + cat > emulator << EOJ #!/bin/sh file_name_emulator=emulator.log current_time=\$(date "+%H%M_%d-%m-%Y") 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" - echo "$(date +%Y-%m-%d\ %H:%M:%S)|BACKUP|$BACKUP_FILE|$BK_SIZE" >> "$BACKUP_DIR/.history" 2>/dev/null || true +java -Dfile.encoding=UTF8 -Xmx2G -jar $jar +EOJ + chmod +x emulator; ok "Launch script updated" ROLLBACK_NEEDED=false else - info "Already up to date, skipping" + info "Emulator: already up to date" fi } update_renderer() { - step "Update Nitro_Render_V3" + step 3 8 "Update Nitro_Render_V3" 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..."; clean_node_modules && yarn install 2>&1; } || { spinner_stop fail; die "yarn install failed"; } - spinner_stop ok - ok "Dependencies installed" + HAD_UPDATES=true; UPDATED_REPOS+=("Nitro_Render_V3") + spinner_start "Installing deps..." + yarn install --frozen-lockfile 2>&1 || { clean_node_modules && yarn install 2>&1; } || { spinner_stop fail; die "yarn install failed"; } + spinner_stop ok; ok "Renderer dependencies installed" else - info "Already up to date, skipping" + info "Renderer: already up to date" fi } update_client() { - step "Update & Build Nitro-V3" + step 4 8 "Update & Build Nitro-V3" if git_update "$NITRO_CLIENT" "$NITRO_BRANCH"; then - info "New commits detected" - HAD_UPDATES=true - NITRO_BUILT=true - UPDATED_REPOS+=("Nitro-V3") - spinner_start "Installing dependencies..." - yarn install --frozen-lockfile 2>&1 || { info "Retrying..."; clean_node_modules && yarn install 2>&1; } || { spinner_stop fail; die "yarn install failed"; } + HAD_UPDATES=true; NITRO_BUILT=true; UPDATED_REPOS+=("Nitro-V3") + spinner_start "Installing deps..." + yarn install --frozen-lockfile 2>&1 || { clean_node_modules && yarn install 2>&1; } || { spinner_stop fail; die "yarn install failed"; } spinner_stop ok - ok "Dependencies installed" - info "Building Nitro-V3..." spinner_start "Building frontend..." yarn build 2>&1 | tail -3 && spinner_stop ok || { spinner_stop fail; die "yarn build failed"; } - ok "Build complete" else - info "Already up to date, skipping build" + info "Nitro-V3: already up to date" fi mkdir -p "$NITRO_CLIENT/dist/custom-themes" [ ! -f "$NITRO_CLIENT/dist/custom-themes/index.json" ] && echo '{"themes":[]}' > "$NITRO_CLIENT/dist/custom-themes/index.json" } sync_configs() { - step "Sync Configurations" + step 5 8 "Sync Configurations" mkdir -p "$GAMEDATA_CONF_DIR" - local MERGE_SCRIPT="$SCRIPT_DIR/scripts/merge-config.cjs" - local NITRO_DIST_CONFIG_DIR="$NITRO_CLIENT/dist/configuration" - for dir in "$NITRO_SRC_DIR" "$GAMEDATA_CONF_DIR"; do - [ -d "$dir" ] && [ ! -w "$dir" ] && command -v sudo &>/dev/null && sudo chown -R "$(whoami)":"$(whoami)" "$dir" 2>/dev/null || true + local ms="$SCRIPT_DIR/scripts/merge-config.cjs" + local dcd="$NITRO_CLIENT/dist/configuration" + for d in "$NITRO_SRC_DIR" "$GAMEDATA_CONF_DIR"; do + [ -d "$d" ] && [ ! -w "$d" ] && sudo chown -R "$(whoami)":"$(whoami)" "$d" 2>/dev/null || true done - local EXAMPLE_COUNT=0 - for example_file in "$NITRO_SRC_DIR"/*.example; do - [ -f "$example_file" ] || continue - local base=$(basename "$example_file") - local 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 - [ -d "$NITRO_DIST_CONFIG_DIR" ] && cp "$NITRO_SRC_DIR/$target_name" "$NITRO_DIST_CONFIG_DIR/$target_name" 2>/dev/null || true - EXAMPLE_COUNT=$((EXAMPLE_COUNT + 1)) + local ec=0 + for ef in "$NITRO_SRC_DIR"/*.example; do + [ -f "$ef" ] || continue + local b=$(basename "$ef"); local tn="${b%.example}" + case "$tn" in *.*) ;; *) tn="$tn.json" ;; esac + node "$ms" "$ef" "$NITRO_SRC_DIR/$tn" 2>/dev/null + node "$ms" "$ef" "$GAMEDATA_CONF_DIR/$tn" 2>/dev/null + [ -d "$dcd" ] && cp "$NITRO_SRC_DIR/$tn" "$dcd/$tn" 2>/dev/null || true + ec=$((ec+1)) done - ok "$EXAMPLE_COUNT .example file(s) processed" - info "Applying critical config URLs..." - FORCE_CONFIG_SCRIPT=$(cat << 'PYEOF' -import json, sys, os -paths = [ - os.path.join(os.environ.get('NITRO_SRC_DIR', ''), 'renderer-config.json'), - os.path.join(os.environ.get('NITRO_DIST_CONFIG_DIR', ''), 'renderer-config.json'), - os.path.join(os.environ.get('NITRO_SRC_DIR', ''), 'ui-config.json'), - os.path.join(os.environ.get('NITRO_DIST_CONFIG_DIR', ''), 'ui-config.json'), -] -for path in paths: - if not os.path.exists(path): continue - with open(path, 'r') as f: data = json.load(f) - for key in ['image.library.url', 'hof.furni.url', 'gamedata.url', 'asset.url']: - env_val = os.environ.get(f'NITRO_{key.upper().replace(".", "_")}') - if env_val: data[key] = env_val - for key in ['api.url', 'socket.url', 'furni.asset.icon.url']: - if key in data: - env_val = os.environ.get(f'NITRO_{key.upper().replace(".", "_")}') - if env_val: data[key] = env_val - if 'gamedata.url' in data: - data['radio.url'] = data['gamedata.url'] + '/config/radio-stations.json5?t=%timestamp%' - data['soundboard.url'] = data['gamedata.url'] + '/config/soundboard-sounds.json5?t=%timestamp%' - if 'show.google.ads' in data: data['show.google.ads'] = False - with open(path, 'w') as f: json.dump(data, f, indent=(4 if 'dist' not in path else None), separators=(',', ':') if 'dist' in path else (',', ': ')) - print(f' [OK] Fixed: {os.path.basename(path)}') -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" + ok "$ec config file(s) synced" } -cleanup_phase() { - step "Cleanup" - info "Removing old logs..." - local LOG_COUNT=$(find "$EMULATOR_DIR" -name "*.log" -mtime +14 2>/dev/null | wc -l) +do_cleanup() { + step 6 8 "Cleanup" + local lc=$(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(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 - if [ "$HAD_UPDATES" = true ]; then - yarn cache clean 2>/dev/null || true - 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 + [ -d "$BACKUP_DIR" ] && 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 + yarn cache clean 2>/dev/null || true rm -rf /tmp/nitro-* 2>/dev/null || true - ok "Cleanup complete" + ok "Cleanup done ($lc logs removed)" } -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 - [ -d "$dir" ] && sudo chown -R www-data:www-data "$dir" 2>/dev/null || true - done - ok "Permissions set to www-data:www-data" - else - warn "sudo not available, skipping chown" - fi - chmod -R 775 "$LOG_DIR" 2>/dev/null || true - chmod -R 775 "$BACKUP_DIR" 2>/dev/null || true -} - -restart_services() { - step "Restart Services" - if [ "$HAD_UPDATES" = true ]; then - if systemctl cat "$EMULATOR_SERVICE" &>/dev/null && command -v sudo &>/dev/null; then - info "Restarting $EMULATOR_SERVICE..." - spinner_start "Restarting emulator..." - sudo systemctl restart "$EMULATOR_SERVICE" 2>/dev/null && spinner_stop ok || { spinner_stop fail; die "Failed to restart $EMULATOR_SERVICE"; } - elif command -v pm2 &>/dev/null; then - info "Restarting PM2..." - pm2 restart all 2>/dev/null || warn "PM2 restart had issues" - else - warn "No systemd or PM2 found — restart manually" - fi - if command -v nginx &>/dev/null && command -v sudo &>/dev/null; then - sudo nginx -t 2>/dev/null && sudo systemctl reload nginx 2>/dev/null && ok "Nginx reloaded" || warn "Nginx reload failed" - fi - if command -v redis-cli &>/dev/null; then - redis-cli FLUSHALL 2>/dev/null && ok "Redis cache flushed" || warn "Redis flush failed" - fi - else - info "No updates applied, no restart needed" - fi -} - -health_check() { - step "Health Check & Validation" - if [ "$NITRO_BUILT" = true ]; then - if [ -d "$NITRO_CLIENT/dist/assets" ]; then - local ASSET_COUNT=$(find "$NITRO_CLIENT/dist/assets" -type f 2>/dev/null | wc -l) - local ASSET_SIZE=$(du -sh "$NITRO_CLIENT/dist/assets" 2>/dev/null | cut -f1 || echo "?") - ok "Nitro-V3 built: $ASSET_COUNT assets ($ASSET_SIZE)" - else - fail "Nitro-V3 build assets missing!" - ERRORS=$((ERRORS + 1)) - fi - fi - if [ "$HAD_UPDATES" = true ] && systemctl cat "$EMULATOR_SERVICE" &>/dev/null; then - info "Waiting for $EMULATOR_SERVICE..." - HEALTHY=false - for i in $(seq 1 "$HEALTH_RETRIES"); do - sleep "$HEALTH_INTERVAL" - STATUS=$(systemctl is-active "$EMULATOR_SERVICE" 2>/dev/null || echo "unknown") - [ "$STATUS" = "active" ] && { HEALTHY=true; break; } - done - [ "$HEALTHY" = true ] && ok "$EMULATOR_SERVICE is active" || { fail "$EMULATOR_SERVICE did not start"; ERRORS=$((ERRORS + 1)); } - fi - 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") - [ "$OWNER" = "www-data:www-data" ] || [ "$(id -u)" -ne 0 ] && debug "$(basename "$dir"): $OWNER" || sudo chown -R www-data:www-data "$dir" 2>/dev/null || true - fi +do_permissions() { + step 7 8 "Set Permissions" + for d in "$NITRO_CLIENT" "$NITRO_RENDERER" "$EMULATOR_DIR" "$GAMEDATA_CONF_DIR"; do + [ -d "$d" ] && sudo chown -R www-data:www-data "$d" 2>/dev/null || true done - 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 responds: ${NITRO_SITE_URL:-}" + ok "Permissions set" +} + +do_restart() { + step 8 8 "Restart Services" + if systemctl cat "$EMULATOR_SERVICE" &>/dev/null; then + sudo systemctl restart "$EMULATOR_SERVICE" 2>/dev/null && ok "$EMULATOR_SERVICE restarted" || warn "Restart failed" + elif command -v pm2 &>/dev/null; then + pm2 restart all 2>/dev/null && ok "PM2 restarted" || warn "PM2 restart failed" else - warn "Site did not respond: ${NITRO_SITE_URL:-}" + warn "No service manager found" fi + command -v nginx &>/dev/null && sudo nginx -t 2>/dev/null && sudo systemctl reload nginx 2>/dev/null && ok "Nginx reloaded" + command -v redis-cli &>/dev/null && redis-cli FLUSHALL 2>/dev/null && ok "Redis flushed" } show_summary() { - ELAPSED=$(($(date +%s) - START_TIME)) - ELAPSED_FMT=$(printf '%dm %ds' $((ELAPSED / 60)) $((ELAPSED % 60))) + local el=$(($(date +%s) - START_TIME)); local ef; ef=$(printf '%dm %ds' $((el/60)) $((el%60))) echo "" echo -e " ${C_CYAN}╔══════════════════════════════════════════════════════════════════╗${C_RESET}" if [ "$ERRORS" -eq 0 ]; then - echo -e " ${C_CYAN}║${C_RESET} ${C_GREEN}${C_BOLD}✔ UPDATE SUCCESSFULLY COMPLETED${C_RESET} ${C_CYAN}║${C_RESET}" + echo -e " ${C_CYAN}║${C_RESET} ${C_GREEN}${C_BOLD}✔ UPDATE COMPLETED SUCCESSFULLY${C_RESET} ${C_CYAN}║${C_RESET}" else - echo -e " ${C_CYAN}║${C_RESET} ${C_YELLOW}${C_BOLD}⚠ COMPLETED WITH $ERRORS ERROR(S)${C_RESET} ${C_CYAN}║${C_RESET}" + echo -e " ${C_CYAN}║${C_RESET} ${C_YELLOW}${C_BOLD}⚠ COMPLETED WITH $ERRORS ERROR(S)${C_RESET} ${C_CYAN}║${C_RESET}" fi echo -e " ${C_CYAN}╠══════════════════════════════════════════════════════════════════╣${C_RESET}" - printf " ${C_CYAN}║${C_RESET} Duration: %-45s${C_CYAN}║${C_RESET}\n" "$ELAPSED_FMT" - printf " ${C_CYAN}║${C_RESET} Updates: %-45s${C_CYAN}║${C_RESET}\n" "$([ "$HAD_UPDATES" = true ] && echo "Yes" || echo "No")" - printf " ${C_CYAN}║${C_RESET} Nitro build: %-45s${C_CYAN}║${C_RESET}\n" "$([ "$NITRO_BUILT" = true ] && echo "Yes" || echo "No")" - printf " ${C_CYAN}║${C_RESET} Warnings: %-45s${C_CYAN}║${C_RESET}\n" "$WARNINGS" - printf " ${C_CYAN}║${C_RESET} Errors: %-45s${C_CYAN}║${C_RESET}\n" "$ERRORS" - [ ${#UPDATED_REPOS[@]} -gt 0 ] && printf " ${C_CYAN}║${C_RESET} Updated: %-45s${C_CYAN}║${C_RESET}\n" "${UPDATED_REPOS[*]}" - printf " ${C_CYAN}║${C_RESET} Disk free: %-45s${C_CYAN}║${C_RESET}\n" "${AVAIL_GB:-?}GB" - printf " ${C_CYAN}║${C_RESET} Log: %-45s${C_CYAN}║${C_RESET}\n" "$(basename "${LOG_FILE:-?}")" + printf " ${C_CYAN}║${C_RESET} Duration: %-46s${C_CYAN}║${C_RESET}\n" "$ef" + printf " ${C_CYAN}║${C_RESET} Updates: %-46s${C_CYAN}║${C_RESET}\n" "$([ "$HAD_UPDATES" = true ] && echo "Yes" || echo "No")" + printf " ${C_CYAN}║${C_RESET} Nitro build: %-46s${C_CYAN}║${C_RESET}\n" "$([ "$NITRO_BUILT" = true ] && echo "Yes" || echo "No")" + printf " ${C_CYAN}║${C_RESET} Warnings: %-46s${C_CYAN}║${C_RESET}\n" "$WARNINGS" + [ ${#UPDATED_REPOS[@]} -gt 0 ] && printf " ${C_CYAN}║${C_RESET} Repos: %-46s${C_CYAN}║${C_RESET}\n" "${UPDATED_REPOS[*]}" echo -e " ${C_CYAN}╚══════════════════════════════════════════════════════════════════╝${C_RESET}" echo "" - notify "SUCCESS" "Completed in $ELAPSED_FMT (${ERRORS} errors, ${WARNINGS} warnings)" + notify "SUCCESS" "Completed in $ef (${WARNINGS} warnings)" } # ============================================================================= -# COMMANDS +# 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:-main} | Mode: ${SELECTIVE_UPDATE:-full}" + info "Site: ${NITRO_SITE_URL:-?} | Branch: $NITRO_BRANCH | Mode: ${SELECTIVE_UPDATE:-full}" info "Log: $LOG_FILE" + + step 1 8 "System Diagnostics" + for cmd in git mariadb mariadb-dump mvn node python3 yarn; do + command -v "$cmd" &>/dev/null || die "Missing: $cmd" + done + ok "All commands available" + + for d in "$EMULATOR_DIR/Emulator" "$NITRO_RENDERER" "$NITRO_CLIENT" "$NITRO_SRC_DIR"; do + [ -d "$d" ] || die "Missing dir: $d" + done + ok "All directories exist" + + local ak=$(df --output=avail "$EMULATOR_DIR" 2>/dev/null | tail -1) + local ag=$((ak / 1024 / 1024)) + [ "$ag" -lt "$MIN_DISK_GB" ] && die "Only ${ag}GB free (min ${MIN_DISK_GB}GB)" + ok "${ag}GB disk available" + + mariadb $MYSQL_CRED -e "SELECT 1" "$DB_NAME" &>/dev/null || die "DB connection failed" + ok "Database connected" + 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 ;; + *) + update_emulator + update_renderer + update_client + sync_configs + do_cleanup + do_permissions + do_restart + ;; esac + show_summary } +# ============================================================================= +# BACKUP / RESTORE +# ============================================================================= cmd_backup() { - preflight - step "Database Backup" + step 1 1 "Database Backup" mkdir -p "$BACKUP_DIR" - BACKUP_FILE="$BACKUP_DIR/manual_$(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 - 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 + local bf="$BACKUP_DIR/manual_$(date +%Y%m%d_%H%M%S).sql" + spinner_start "Backing up..." + if mariadb-dump $MYSQL_CRED --force --skip-lock-tables --routines --events --triggers "$DB_NAME" > "$bf" 2>/dev/null; then + local sz=$(stat -c%s "$bf" 2>/dev/null || echo 0); spinner_stop ok + ok "Backup: $(numfmt --to=iec "$sz") — $(basename "$bf")" else - spinner_stop fail - die "Backup failed" + spinner_stop fail; die "Backup failed" fi } cmd_restore() { - preflight - step "Database Restore" - [ ! -d "$BACKUP_DIR" ] && die "No backup directory found" + step 1 1 "Database Restore" + [ ! -d "$BACKUP_DIR" ] && die "No backup dir" 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.]* //') - [ ${#backups[@]} -eq 0 ] && die "No backups found" - echo "" - echo -e " ${C_BOLD}${C_CYAN}Available Backups:${C_RESET}" + [ ${#backups[@]} -eq 0 ] && die "No backups" echo "" + echo -e " ${C_BOLD}${C_CYAN}Backups:${C_RESET}" local i=1 for bk in "${backups[@]}"; do - local name=$(basename "$bk") - local size=$(numfmt --to=iec "$(stat -c%s "$bk" 2>/dev/null || echo 0)" 2>/dev/null || echo "?") - printf " ${C_GREEN}${C_BOLD}%2d)${C_RESET} %-40s ${C_DIM}%s${C_RESET}\n" "$i" "$name" "$size" - i=$((i + 1)) + local sz=$(numfmt --to=iec "$(stat -c%s "$bk" 2>/dev/null || echo 0)" 2>/dev/null || echo "?") + printf " ${C_GREEN}${C_BOLD}%2d)${C_RESET} %-40s ${C_DIM}%s${C_RESET}\n" "$i" "$(basename "$bk")" "$sz" + i=$((i+1)) done - echo "" - echo -en " Select [1-$((i-1))]: " - local choice; read -r choice - [[ "$choice" =~ ^[0-9]+$ ]] && [ "$choice" -ge 1 ] && [ "$choice" -le ${#backups[@]} ] || die "Invalid selection" - local selected="${backups[$((choice-1))]}" - if ! confirm "Restore from $(basename "$selected")? This OVERWRITES the database!"; then info "Cancelled."; return 0; fi + echo -en "\n Select [1-$((i-1))]: " + local c; read -r c + [[ "$c" =~ ^[0-9]+$ ]] && [ "$c" -ge 1 ] && [ "$c" -le ${#backups[@]} ] || die "Invalid" + local sel="${backups[$((c-1))]}" + if ! confirm "Restore $(basename "$sel")? OVERWRITES database!"; then info "Cancelled"; return; fi spinner_start "Restoring..." - mariadb $MYSQL_CRED "$DB_NAME" < "$selected" 2>/dev/null && spinner_stop ok && ok "Restored from $(basename "$selected")" || { spinner_stop fail; die "Restore failed"; } + mariadb $MYSQL_CRED "$DB_NAME" < "$sel" 2>/dev/null && spinner_stop ok && ok "Restored" || { spinner_stop fail; die "Restore failed"; } } cmd_backups() { - step "Backup Inventory" - [ ! -d "$BACKUP_DIR" ] && { warn "No backup directory"; return 0; } + step 1 1 "Backup Inventory" + [ ! -d "$BACKUP_DIR" ] && { warn "No backup dir"; return; } echo "" - 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))" - local total=0 total_size=0 - 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 age_days=$(( ($(date +%s) - $(stat -c%Y "$line" 2>/dev/null || echo 0)) / 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" + printf " ${C_DIM}%-4s %-38s %-10s${C_RESET}\n" "#" "FILENAME" "SIZE" + printf " ${C_DIM}%s${C_RESET}\n" "$(printf '%0.s─' $(seq 1 56))" + local t=0 ts=0 + while IFS= read -r l; do + t=$((t+1)); local n=$(basename "$l"); local s=$(stat -c%s "$l" 2>/dev/null || echo 0); ts=$((ts+s)) + printf " ${C_GREEN}%2d${C_RESET} %-38s ${C_CYAN}%s${C_RESET}\n" "$t" "$n" "$(numfmt --to=iec "$s" 2>/dev/null || echo "?")" done < <(find "$BACKUP_DIR" -maxdepth 1 -name '*.sql' -printf '%T@ %p\n' 2>/dev/null | sort -rn | head -20 | sed 's/^[0-9.]* //') - printf " ${C_DIM}%s${C_RESET}\n" "$(printf '%0.s─' $(seq 1 62))" - echo -e " ${C_BOLD}Total: $total backup(s) — $(numfmt --to=iec "$total_size" 2>/dev/null || echo "?")${C_RESET}" - echo "" + printf " ${C_DIM}%s${C_RESET}\n" "$(printf '%0.s─' $(seq 1 56))" + echo -e " ${C_BOLD}Total: $t backups — $(numfmt --to=iec "$ts" 2>/dev/null || echo "?")${C_RESET}\n" } +# ============================================================================= +# STATUS +# ============================================================================= cmd_status() { - step "System Status Dashboard" + step 1 1 "System Status" echo "" - box_top "SERVER" 56 - box_kv "${E_SERVER} Hostname:" "$(hostname)" 56 - box_kv "${E_GLOBE} OS:" "$(cat /etc/os-release 2>/dev/null | grep PRETTY_NAME | cut -d= -f2 | tr -d '"' || uname -s)" 56 - box_kv "${E_GEAR} Kernel:" "$(uname -r)" 56 - box_kv "${E_CLOCK} Uptime:" "$(uptime -p 2>/dev/null || uptime)" 56 - box_bottom 56 + box_top "SERVER" 56; box_kv "Host:" "$(hostname)" 56 + box_kv "OS:" "$(cat /etc/os-release 2>/dev/null | grep PRETTY_NAME | cut -d= -f2 | tr -d '"' || uname -s)" 56 + box_kv "Uptime:" "$(uptime -p 2>/dev/null || uptime)" 56; box_bottom 56 echo "" box_top "RESOURCES" 56 - local mem_info=$(free -m 2>/dev/null | awk '/^Mem:/{printf "%dMB / %dMB (%.1f%%)", $3, $2, $3*100/$2}') - box_kv "${E_LIGHTNING} CPU:" "$(awk '{printf "%.1f", $1}' /proc/loadavg 2>/dev/null || echo "?") / $(nproc 2>/dev/null || echo '?') cores" 56 - box_kv "${E_CHART} Memory:" "$mem_info" 56 - box_kv "${E_FOLDER} Disk:" "$(df -h "$SCRIPT_DIR" 2>/dev/null | tail -1 | awk '{printf "%s / %s (%s)", $3, $2, $5}')" 56 - box_kv "${E_DB} Database:" "${DB_SIZE:-?}MB / ${DB_TABLES:-?} tables" 56 - box_bottom 56 + box_kv "CPU:" "$(awk '{printf "%.1f", $1}' /proc/loadavg 2>/dev/null || echo "?") / $(nproc 2>/dev/null || echo '?') cores" 56 + box_kv "Memory:" "$(free -m 2>/dev/null | awk '/^Mem:/{printf "%dMB / %dMB", $3, $2}')" 56 + box_kv "Disk:" "$(df -h "$SCRIPT_DIR" 2>/dev/null | tail -1 | awk '{printf "%s / %s (%s)", $3, $2, $5}')" 56 + local dbs=$(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 dbt=$(mariadb $MYSQL_CRED -N -e "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='$DB_NAME'" "$DB_NAME" 2>/dev/null || echo "?") + box_kv "Database:" "${dbs}MB / ${dbt} tables" 56; box_bottom 56 echo "" box_top "SERVICES" 56 for svc in nginx mariadb redis-server "$EMULATOR_SERVICE" php-fpm cron; do local st="${C_DIM}N/A${C_RESET}" - systemctl is-active "$svc" &>/dev/null && st="${C_GREEN}ACTIVE${C_RESET}" || systemctl is-enabled "$svc" &>/dev/null 2>&1 && st="${C_YELLOW}INACTIVE${C_RESET}" + systemctl is-active "$svc" &>/dev/null && st="${C_GREEN}ACTIVE${C_RESET}" printf " ${C_CYAN}║${C_RESET} %-20s %b${C_CYAN} ║${C_RESET}\n" "$svc" "$st" - done - box_bottom 56 + done; box_bottom 56 echo "" - box_top "GIT REPOSITORIES" 56 - for repo_entry in "Emulator:$EMULATOR_DIR/Emulator" "Nitro-V3:$NITRO_CLIENT" "Nitro_Render_V3:$NITRO_RENDERER"; do - local name="${repo_entry%%:*}" path="${repo_entry##*:}" - [ -d "$path/.git" ] || continue - 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 " ${C_CYAN}║${C_RESET} %-14s ${C_CYAN}%-8s${C_RESET} %s%b\n" "$name" "$branch" "$commit" "$dirty_str" - done - box_bottom 56 - echo "" - box_top "LOGS" 56 - box_kv "${E_EYE} Total:" "$(find "$LOG_DIR" -name 'update-*.log' 2>/dev/null | wc -l) files" 56 - box_kv "${E_FOLDER} Current:" "$(basename "${LOG_FILE:-?}")" 56 - box_bottom 56 + box_top "GIT REPOS" 56 + for re in "Emulator:$EMULATOR_DIR/Emulator" "Nitro-V3:$NITRO_CLIENT" "Render:$NITRO_RENDERER"; do + local nm="${re%%:*}" pa="${re##*:}" + [ -d "$pa/.git" ] || continue + local br=$(cd "$pa" && git branch --show-current 2>/dev/null || echo "?") + local cm=$(cd "$pa" && git log --oneline -1 2>/dev/null | cut -c1-30 || echo "?") + printf " ${C_CYAN}║${C_RESET} %-12s ${C_CYAN}%-8s${C_RESET} %s\n" "$nm" "$br" "$cm" + done; box_bottom 56 echo "" } +# ============================================================================= +# HEALTH CHECK +# ============================================================================= cmd_health() { - step "System Health Check" - local total=0 passed=0 failed=0 warn_c=0 - check() { total=$((total + 1)); "$@" &>/dev/null && { passed=$((passed + 1)); ok "$1"; } || { failed=$((failed + 1)); fail "$1"; }; } - check_w() { total=$((total + 1)); "$@" &>/dev/null && { passed=$((passed + 1)); ok "$1"; } || { warn_c=$((warn_c + 1)); warn "$1"; }; } + step 1 1 "Health Check" + local t=0 p=0 f=0 w=0 + check() { t=$((t+1)); "$@" &>/dev/null && { p=$((p+1)); ok "$1"; } || { f=$((f+1)); fail "$1"; }; } + checkw() { t=$((t+1)); "$@" &>/dev/null && { p=$((p+1)); ok "$1"; } || { w=$((w+1)); warn "$1"; }; } echo "" echo -e " ${C_BOLD}System:${C_RESET}" - 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 "" - echo -e " ${C_BOLD}Directories:${C_RESET}" - check_w "Emulator dir" test -d "$EMULATOR_DIR/Emulator" - check_w "Nitro-V3 dir" test -d "$NITRO_CLIENT" - check_w "Renderer dir" test -d "$NITRO_RENDERER" - echo "" - 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 "" - echo -e " ${C_BOLD}Application:${C_RESET}" - check_w "artisan exists" test -f "$SCRIPT_DIR/artisan" - check_w ".env exists" test -f "$SCRIPT_DIR/.env" - check_w "Site reachable" timeout 5 curl -sf "${NITRO_SITE_URL:-}" 2>/dev/null - echo "" - 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)) - total=$((total + 1)) - [ "$avail_gb" -ge "$MIN_DISK_GB" ] && { passed=$((passed + 1)); ok "Disk: ${avail_gb}GB free"; } || { failed=$((failed + 1)); fail "Disk: ${avail_gb}GB (min ${MIN_DISK_GB}GB)"; } - echo "" - local score=$((passed * 100 / total)) - box_top "HEALTH SCORE" 50 - box_kv "Total:" "$total" 50 - box_kv "${C_GREEN}Passed:" "$passed" 50 - box_kv "${C_RED}Failed:" "$failed" 50 - box_kv "${C_YELLOW}Warnings:" "$warn_c" 50 - if [ "$score" -ge 90 ]; then - box_line "${C_GREEN}${C_BOLD}Score: ${score}% — EXCELLENT${C_RESET}" 50 - elif [ "$score" -ge 70 ]; then - box_line "${C_YELLOW}${C_BOLD}Score: ${score}% — GOOD${C_RESET}" 50 - else - box_line "${C_RED}${C_BOLD}Score: ${score}% — NEEDS ATTENTION${C_RESET}" 50 - fi - box_bottom 50 + checkw "Git" command -v git; checkw "Node.js" command -v node; checkw "Yarn" command -v yarn + checkw "Python3" command -v python3; checkw "Java" command -v java; checkw "Maven" command -v mvn + echo -e "\n ${C_BOLD}Directories:${C_RESET}" + checkw "Emulator" test -d "$EMULATOR_DIR/Emulator"; checkw "Nitro-V3" test -d "$NITRO_CLIENT"; checkw "Renderer" test -d "$NITRO_RENDERER" + echo -e "\n ${C_BOLD}Services:${C_RESET}" + checkw "MariaDB" systemctl is-active mariadb; checkw "Nginx" systemctl is-active nginx; checkw "Redis" systemctl is-active redis-server + echo -e "\n ${C_BOLD}Application:${C_RESET}" + checkw "artisan" test -f "$SCRIPT_DIR/artisan"; checkw ".env" test -f "$SCRIPT_DIR/.env" + checkw "Site" timeout 5 curl -sf "${NITRO_SITE_URL:-}" 2>/dev/null + echo -e "\n ${C_BOLD}Resources:${C_RESET}" + local ak=$(df --output=avail "$EMULATOR_DIR" 2>/dev/null | tail -1); local ag=$((ak/1024/1024)) + t=$((t+1)); [ "$ag" -ge "$MIN_DISK_GB" ] && { p=$((p+1)); ok "Disk: ${ag}GB"; } || { f=$((f+1)); fail "Disk: ${ag}GB"; } echo "" + local sc=$((p*100/t)) + box_top "SCORE" 50 + box_kv "Passed:" "$p / $t" 50 + box_kv "Failed:" "$f" 50; box_kv "Warnings:" "$w" 50 + if [ "$sc" -ge 90 ]; then box_line "${C_GREEN}${C_BOLD}${sc}% — EXCELLENT${C_RESET}" 50 + elif [ "$sc" -ge 70 ]; then box_line "${C_YELLOW}${C_BOLD}${sc}% — GOOD${C_RESET}" 50 + else box_line "${C_RED}${C_BOLD}${sc}% — NEEDS ATTENTION${C_RESET}" 50; fi + box_bottom 50; echo "" } +# ============================================================================= +# SERVICES +# ============================================================================= cmd_services() { - step "Service Manager" - select_menu "Service Control" "Start Emulator" "Stop Emulator" "Restart Emulator" "Status Emulator" "Start Nginx" "Restart Nginx" "Status All" "Emulator Logs (tail)" "Return" - case "${REPLY:-}" in - 1) sudo systemctl start "$EMULATOR_SERVICE" && ok "$EMULATOR_SERVICE started" || fail "Failed" ;; - 2) sudo systemctl stop "$EMULATOR_SERVICE" && ok "$EMULATOR_SERVICE stopped" || fail "Failed" ;; - 3) sudo systemctl restart "$EMULATOR_SERVICE" && ok "$EMULATOR_SERVICE restarted" || fail "Failed" ;; - 4) systemctl status "$EMULATOR_SERVICE" --no-pager ;; - 5) sudo systemctl start nginx && ok "Nginx started" || fail "Failed" ;; - 6) sudo systemctl restart nginx && ok "Nginx restarted" || fail "Failed" ;; - 7) for svc in nginx mariadb redis-server "$EMULATOR_SERVICE" php-fpm cron; do local st="N/A"; systemctl is-active "$svc" &>/dev/null && st="${C_GREEN}ACTIVE${C_RESET}" || st="${C_RED}INACTIVE${C_RESET}"; printf " %-25s %b\n" "$svc" "$st"; done ;; - 8) sudo journalctl -u "$EMULATOR_SERVICE" --no-pager -n 50 ;; + step 1 1 "Service Manager" + echo "" + echo -e " ${C_BOLD}1)${C_RESET} Start Emulator ${C_BOLD}2)${C_RESET} Stop Emulator ${C_BOLD}3)${C_RESET} Restart Emulator" + echo -e " ${C_BOLD}4)${C_RESET} Start Nginx ${C_BOLD}5)${C_RESET} Restart Nginx ${C_BOLD}6)${C_RESET} Status All" + echo -e " ${C_BOLD}7)${C_RESET} Emulator Logs ${C_BOLD}0)${C_RESET} Return" + echo -en "\n Select: "; local c; read -r c + case "$c" in + 1) sudo systemctl start "$EMULATOR_SERVICE" && ok "Started" || fail "Failed" ;; + 2) sudo systemctl stop "$EMULATOR_SERVICE" && ok "Stopped" || fail "Failed" ;; + 3) sudo systemctl restart "$EMULATOR_SERVICE" && ok "Restarted" || fail "Failed" ;; + 4) sudo systemctl start nginx && ok "Started" || fail "Failed" ;; + 5) sudo systemctl restart nginx && ok "Restarted" || fail "Failed" ;; + 6) for s in nginx mariadb redis-server "$EMULATOR_SERVICE" php-fpm cron; do + local st="${C_RED}DOWN${C_RESET}"; systemctl is-active "$s" &>/dev/null && st="${C_GREEN}UP${C_RESET}" + printf " %-25s %b\n" "$s" "$st" + done ;; + 7) sudo journalctl -u "$EMULATOR_SERVICE" --no-pager -n 30 ;; esac } +# ============================================================================= +# DATABASE +# ============================================================================= cmd_database() { - step "Database Tools" - local choice - choice=$(select_menu "Database Management" "Show DB Info" "List Tables" "Table Sizes" "Optimize Tables" "Check Tables" "Custom Query" "Active Users" "Return") - case "$choice" in - 1) box_top "DATABASE INFO" 50; box_kv "Name:" "$DB_NAME" 50; box_kv "Host:" "$DB_HOST:$DB_PORT" 50; box_kv "User:" "$DB_USER" 50 - box_kv "Size:" "${DB_SIZE:-?}MB" 50; box_kv "Tables:" "${DB_TABLES:-?}" 50; box_bottom 50 ;; + step 1 1 "Database Tools" + echo "" + echo -e " ${C_BOLD}1)${C_RESET} DB Info ${C_BOLD}2)${C_RESET} List Tables ${C_BOLD}3)${C_RESET} Table Sizes" + echo -e " ${C_BOLD}4)${C_RESET} Optimize ${C_BOLD}5)${C_RESET} Custom Query ${C_BOLD}6)${C_RESET} Active Users" + echo -e " ${C_BOLD}0)${C_RESET} Return" + echo -en "\n Select: "; local c; read -r c + case "$c" in + 1) box_top "DB INFO" 50 + local dbs=$(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_kv "Name:" "$DB_NAME" 50; box_kv "Host:" "$DB_HOST:$DB_PORT" 50; box_kv "Size:" "${dbs}MB" 50 + box_kv "Tables:" "$(mariadb $MYSQL_CRED -N -e "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='$DB_NAME'" "$DB_NAME" 2>/dev/null || echo '?')" 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 ;; + 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' FROM information_schema.tables WHERE table_schema='$DB_NAME' ORDER BY (data_length+index_length) DESC" 2>/dev/null ;; 4) spinner_start "Optimizing..." - local count=0 for tbl in $(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); 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 tables" ;; - 5) mariadb $MYSQL_CRED -e "CHECK TABLE $DB_NAME.*" "$DB_NAME" 2>/dev/null ;; - 6) echo -en " SQL: "; local q; read -r q; [ -n "$q" ] && mariadb $MYSQL_CRED "$DB_NAME" -e "$q" 2>/dev/null ;; - 7) mariadb $MYSQL_CRED -e "SELECT name, lookAt, motto FROM $DB_NAME.users WHERE online='1'" 2>/dev/null || warn "Could not query users" ;; + done; spinner_stop ok; ok "Optimized" ;; + 5) echo -en " SQL: "; local q; read -r q; [ -n "$q" ] && mariadb $MYSQL_CRED "$DB_NAME" -e "$q" 2>/dev/null ;; + 6) mariadb $MYSQL_CRED -e "SELECT name, motto FROM $DB_NAME.users WHERE online='1'" 2>/dev/null || warn "Could not query" ;; esac } +# ============================================================================= +# CONFIG +# ============================================================================= cmd_config() { - step "Configuration" - echo "" - box_top "CURRENT CONFIGURATION" 56 - box_kv "${E_CLOUD} Site URL:" "${NITRO_SITE_URL:-?}" 56 - box_kv "${E_GIT} Branch:" "${NITRO_BRANCH:-main}" 56 - box_kv "${E_FOLDER} Emulator:" "$EMULATOR_DIR" 56 - box_kv "${E_FOLDER} Nitro-V3:" "$NITRO_CLIENT" 56 - box_kv "${E_FOLDER} Renderer:" "$NITRO_RENDERER" 56 - box_kv "${E_DB} Database:" "$DB_NAME @ $DB_HOST:$DB_PORT" 56 - box_kv "${E_GEAR} Service:" "$EMULATOR_SERVICE" 56 - box_kv "${E_CLOUD} Socket:" "${NITRO_SOCKET_URL:-?}" 56 - box_kv "${E_CLOUD} API:" "${NITRO_API_URL:-?}" 56 - box_kv "${E_CLOUD} Gamedata:" "${NITRO_GAMEDATA_URL:-?}" 56 - box_kv "${E_LOCK} Min Disk:" "${MIN_DISK_GB}GB" 56 - box_bottom 56 + step 1 1 "Configuration" echo "" + box_top "CONFIGURATION" 56 + box_kv "Site:" "${NITRO_SITE_URL:-?}" 56 + box_kv "Branch:" "$NITRO_BRANCH" 56 + box_kv "Emulator:" "$EMULATOR_DIR" 56 + box_kv "Nitro-V3:" "$NITRO_CLIENT" 56 + box_kv "Renderer:" "$NITRO_RENDERER" 56 + box_kv "Database:" "$DB_NAME @ $DB_HOST:$DB_PORT" 56 + box_kv "Service:" "$EMULATOR_SERVICE" 56 + box_kv "Socket:" "${NITRO_SOCKET_URL:-?}" 56 + box_kv "API:" "${NITRO_API_URL:-?}" 56 + box_kv "Gamedata:" "${NITRO_GAMEDATA_URL:-?}" 56 + box_kv "Min Disk:" "${MIN_DISK_GB}GB" 56 + box_bottom 56; echo "" } +# ============================================================================= +# LOGS +# ============================================================================= cmd_logs() { - step "Log Viewer" - [ ! -d "$LOG_DIR" ] && { warn "No log directory"; return 0; } + step 1 1 "Log Viewer" + [ ! -d "$LOG_DIR" ] && { warn "No log dir"; return; } 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.]* //') - [ ${#logs[@]} -eq 0 ] && { warn "No logs found"; return 0; } - echo "" - echo -e " ${C_BOLD}${C_CYAN}Recent Logs:${C_RESET}" + while IFS= read -r l; do logs+=("$l"); done < <(find "$LOG_DIR" -name 'update-*.log' -printf '%T@ %p\n' 2>/dev/null | sort -rn | head -20 | sed 's/^[0-9.]* //') + [ ${#logs[@]} -eq 0 ] && { warn "No logs"; return; } echo "" local i=1 - for log in "${logs[@]}"; do - local name=$(basename "$log") - local size=$(numfmt --to=iec "$(stat -c%s "$log" 2>/dev/null || echo 0)" 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" "$lines" - i=$((i + 1)) + for l in "${logs[@]}"; do + local sz=$(numfmt --to=iec "$(stat -c%s "$l" 2>/dev/null || echo 0)" 2>/dev/null || echo "?") + printf " ${C_GREEN}${C_BOLD}%2d)${C_RESET} %-35s ${C_DIM}%s${C_RESET}\n" "$i" "$(basename "$l")" "$sz" + i=$((i+1)) done - echo "" - echo -en " Select [1-$((i-1))]: " - local choice; read -r choice - if [[ "$choice" =~ ^[0-9]+$ ]] && [ "$choice" -ge 1 ] && [ "$choice" -le ${#logs[@]} ]; then - echo "" - echo -e " ${C_BOLD}--- $(basename "${logs[$((choice-1))]}") ---${C_RESET}" - echo "" - less -R "${logs[$((choice-1))]}" 2>/dev/null || cat "${logs[$((choice-1))]}" + echo -en "\n Select [1-$((i-1))]: "; local c; read -r c + if [[ "$c" =~ ^[0-9]+$ ]] && [ "$c" -ge 1 ] && [ "$c" -le ${#logs[@]} ]; then + less -R "${logs[$((c-1))]}" 2>/dev/null || cat "${logs[$((c-1))]}" fi } +# ============================================================================= +# CLEAN +# ============================================================================= cmd_clean() { - step "Cache Cleanup" - local choice - choice=$(select_menu "Cleanup" "Yarn Cache" "Laravel Cache" "Redis Cache" "Old Logs (>14d)" "Old Backups (keep 5)" "Temp Files" "Everything" "Return") - case "$choice" in - 1) spinner_start "Cleaning..."; yarn cache clean 2>/dev/null; spinner_stop ok ;; - 2) cd "$SCRIPT_DIR" && php artisan cache:clear 2>/dev/null && php artisan config:clear 2>/dev/null && php artisan route:clear 2>/dev/null && php artisan view:clear 2>/dev/null && ok "Laravel cache cleared" ;; - 3) redis-cli FLUSHALL 2>/dev/null && ok "Redis flushed" || warn "Redis not available" ;; - 4) local c=$(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 $c logs" ;; - 5) local b=$(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; ok "Backups: $b -> $(find "$BACKUP_DIR" -name '*.sql' 2>/dev/null | wc -l)" ;; - 6) rm -rf /tmp/nitro-* 2>/dev/null; ok "Temp cleaned" ;; - 7) yarn cache clean 2>/dev/null; find "$EMULATOR_DIR" -name "*.log" -mtime +14 -exec rm -f {} \; 2>/dev/null; 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; rm -rf /tmp/nitro-* 2>/dev/null; ok "Full cleanup done" ;; + step 1 1 "Cache Cleanup" + echo "" + echo -e " ${C_BOLD}1)${C_RESET} Yarn ${C_BOLD}2)${C_RESET} Laravel ${C_BOLD}3)${C_RESET} Redis" + echo -e " ${C_BOLD}4)${C_RESET} Logs ${C_BOLD}5)${C_RESET} Everything ${C_BOLD}0)${C_RESET} Return" + echo -en "\n Select: "; local c; read -r c + case "$c" in + 1) yarn cache clean 2>/dev/null && ok "Yarn cleaned" ;; + 2) cd "$SCRIPT_DIR" && php artisan cache:clear 2>/dev/null && php artisan config:clear 2>/dev/null && php artisan view:clear 2>/dev/null && ok "Laravel cleaned" ;; + 3) redis-cli FLUSHALL 2>/dev/null && ok "Redis flushed" || warn "Redis N/A" ;; + 4) find "$EMULATOR_DIR" -name "*.log" -mtime +14 -exec rm -f {} \; 2>/dev/null; ok "Logs cleaned" ;; + 5) yarn cache clean 2>/dev/null; find "$EMULATOR_DIR" -name "*.log" -mtime +14 -exec rm -f {} \; 2>/dev/null; rm -rf /tmp/nitro-* 2>/dev/null; ok "Full cleanup" ;; esac } # ============================================================================= -# NEW: GIT LOG VIEWER +# GIT LOG # ============================================================================= cmd_gitlog() { - step "Git Commit History" + step 1 1 "Git Commit History" echo "" - echo -e " ${C_BOLD}Select repository:${C_RESET}" - echo "" - echo -e " ${C_GREEN}1)${C_RESET} Emulator" - echo -e " ${C_GREEN}2)${C_RESET} Nitro-V3" - echo -e " ${C_GREEN}3)${C_RESET} Nitro_Render_V3" - echo "" - echo -en " Select [1-3]: " - local rc; read -r rc + echo -e " ${C_BOLD}1)${C_RESET} Emulator ${C_BOLD}2)${C_RESET} Nitro-V3 ${C_BOLD}3)${C_RESET} Renderer" + echo -en "\n Select: "; local c; read -r c local repo="" - case "$rc" in - 1) repo="$EMULATOR_DIR/Emulator" ;; - 2) repo="$NITRO_CLIENT" ;; - 3) repo="$NITRO_RENDERER" ;; - *) warn "Invalid"; return ;; - esac + case "$c" in 1) repo="$EMULATOR_DIR/Emulator" ;; 2) repo="$NITRO_CLIENT" ;; 3) repo="$NITRO_RENDERER" ;; *) return ;; esac [ ! -d "$repo/.git" ] && { warn "Not a git repo"; return; } echo "" echo -e " ${C_BOLD}Last 20 commits on ${C_CYAN}$(cd "$repo" && git branch --show-current 2>/dev/null)${C_RESET}:" echo "" - printf " ${C_DIM}%-12s %-8s %-50s${C_RESET}\n" "DATE" "HASH" "MESSAGE" - printf " ${C_DIM}%s${C_RESET}\n" "$(printf '%0.s─' $(seq 1 72))" cd "$repo" && git log --pretty=format:" ${C_CYAN}%h${C_RESET} %C(yellow)%cr${C_RESET} %s" -20 2>/dev/null - echo "" - echo "" + echo -e "\n" } # ============================================================================= -# NEW: BRANCH COMPARE +# BRANCH COMPARE # ============================================================================= cmd_compare() { - step "Branch Comparison" - echo "" - local BRANCHES=() - while IFS= read -r b; do [ -n "$b" ] && BRANCHES+=("$b"); done < <(detect_available_branches) - [ ${#BRANCHES[@]} -lt 2 ] && { warn "Need at least 2 branches to compare (found: ${#BRANCHES[@]})"; return 0; } - echo -e " ${C_BOLD}Available branches:${C_RESET}" + step 1 1 "Branch Comparison" echo "" + local branches=() + while IFS= read -r b; do [ -n "$b" ] && branches+=("$b"); done < <(detect_branches) + [ ${#branches[@]} -lt 2 ] && { warn "Need 2+ branches (found ${#branches[@]})"; return; } + echo -e " ${C_BOLD}Branches:${C_RESET}" local i=1 - for b in "${BRANCHES[@]}"; do - echo -e " ${C_GREEN}${C_BOLD}$i)${C_RESET} ${C_CYAN}$b${C_RESET}" - i=$((i + 1)) - done - echo "" - echo -en " Branch A [1-$((i-1))]: " - local ba; read -r ba - echo -en " Branch B [1-$((i-1))]: " - local bb; read -r bb - [[ "$ba" =~ ^[0-9]+$ ]] && [[ "$bb" =~ ^[0-9]+$ ]] && [ "$ba" -ge 1 ] && [ "$bb" -ge 1 ] && [ "$ba" -le ${#BRANCHES[@]} ] && [ "$bb" -le ${#BRANCHES[@]} ] || { warn "Invalid selection"; return 0; } - local branchA="${BRANCHES[$((ba-1))]}" - local branchB="${BRANCHES[$((bb-1))]}" - echo "" - echo -e " ${C_BOLD}Comparing ${C_CYAN}$branchA${C_RESET} vs ${C_CYAN}$branchB${C_RESET}" + for b in "${branches[@]}"; do echo -e " ${C_GREEN}${C_BOLD}$i)${C_RESET} $b"; i=$((i+1)); done + echo -en "\n Branch A: "; local ba; read -r ba + echo -en " Branch B: "; local bb; read -r bb + [[ "$ba" =~ ^[0-9]+$ ]] && [[ "$bb" =~ ^[0-9]+$ ]] && [ "$ba" -ge 1 ] && [ "$bb" -ge 1 ] && [ "$ba" -le ${#branches[@]} ] && [ "$bb" -le ${#branches[@]} ] || { warn "Invalid"; return; } + local bA="${branches[$((ba-1))]}" bB="${branches[$((bb-1))]}" echo "" for repo in "$EMULATOR_DIR/Emulator" "$NITRO_CLIENT" "$NITRO_RENDERER"; do [ -d "$repo/.git" ] || continue - local name=$(basename "$repo") - echo -e " ${C_BOLD}${C_CYAN}$name:${C_RESET}" - local ahead=0 behind=0 + echo -e " ${C_BOLD}$(basename "$repo"):${C_RESET}" cd "$repo" 2>/dev/null || continue - if git rev-parse --verify "$branchA" &>/dev/null && git rev-parse --verify "$branchB" &>/dev/null; then - ahead=$(git rev-list --count "$branchB".."$branchA" 2>/dev/null || echo "0") - behind=$(git rev-list --count "$branchA".."$branchB" 2>/dev/null || echo "0") - local diff_stat=$(git diff --stat "$branchA".."$branchB" 2>/dev/null | tail -1 || echo "") - echo -e " $branchA is ${C_GREEN}$ahead${C_RESET} ahead, ${C_YELLOW}$behind${C_RESET} behind $branchB" - [ -n "$diff_stat" ] && echo -e " ${C_DIM}$diff_stat${C_RESET}" - else - echo -e " ${C_YELLOW}One or both branches not found locally${C_RESET}" + if git rev-parse --verify "$bA" &>/dev/null && git rev-parse --verify "$bB" &>/dev/null; then + local ahead=$(git rev-list --count "$bB".."$bA" 2>/dev/null || echo "0") + local behind=$(git rev-list --count "$bA".."$bB" 2>/dev/null || echo "0") + echo -e " $bA is ${C_GREEN}$ahead${C_RESET} ahead, ${C_YELLOW}$behind${C_RESET} behind $bB" fi - echo "" - done + done; echo "" } # ============================================================================= -# NEW: UPDATE HISTORY -# ============================================================================= -cmd_history() { - step "Update History" - echo "" - local hist_file="$BACKUP_DIR/.history" - if [ ! -f "$hist_file" ]; then - warn "No update history found" - return 0 - fi - echo "" - printf " ${C_DIM}%-20s %-20s %-30s %-10s${C_RESET}\n" "DATE" "TYPE" "FILE" "SIZE" - printf " ${C_DIM}%s${C_RESET}\n" "$(printf '%0.s─' $(seq 1 82))" - local count=0 - while IFS='|' read -r date type file size; do - count=$((count + 1)) - local size_fmt=$(numfmt --to=iec "$size" 2>/dev/null || echo "?") - printf " %-20s ${C_CYAN}%-20s${C_RESET} %-30s ${C_GREEN}%-10s${C_RESET}\n" "$date" "$type" "$(basename "${file:-?}")" "$size_fmt" - [ "$count" -ge 20 ] && break - done < "$hist_file" - [ "$count" -eq 0 ] && echo -e " ${C_DIM}No history entries found${C_RESET}" - echo "" - echo -e " ${C_DIM}Showing last 20 entries${C_RESET}" - echo "" -} - -# ============================================================================= -# NEW: CRON SCHEDULER +# CRON SCHEDULER # ============================================================================= cmd_schedule() { - step "Scheduled Updates (Cron)" + step 1 1 "Cron Scheduler" echo "" - local current_cron=$(crontab -l 2>/dev/null | grep -F "$SCRIPT_NAME" || echo "") - echo -e " ${C_BOLD}Current schedule:${C_RESET}" - if [ -n "$current_cron" ]; then - echo -e " ${C_GREEN}$current_cron${C_RESET}" - else - echo -e " ${C_DIM}No scheduled updates${C_RESET}" - fi + local cc=$(crontab -l 2>/dev/null | grep -F "$SCRIPT_NAME" || echo "") + echo -e " ${C_BOLD}Current:${C_RESET} ${cc:-${C_DIM}none${C_RESET}}" echo "" - echo -e " ${C_BOLD}Options:${C_RESET}" - echo -e " ${C_GREEN}1)${C_RESET} Schedule daily at 03:00" - echo -e " ${C_GREEN}2)${C_RESET} Schedule daily at 04:00" - echo -e " ${C_GREEN}3)${C_RESET} Schedule weekly (Sunday 03:00)" - echo -e " ${C_GREEN}4)${C_RESET} Schedule custom time" - echo -e " ${C_GREEN}5)${C_RESET} Remove schedule" - echo -e " ${C_GREEN}6)${C_RESET} Run scheduled update now (test)" - echo "" - echo -en " Select [1-6]: " - local sc; read -r sc - case "$sc" in - 1) (crontab -l 2>/dev/null | grep -vF "$SCRIPT_NAME"; echo "0 3 * * * cd $SCRIPT_DIR && ./$SCRIPT_NAME update --branch ${NITRO_BRANCH:-main} >> /var/log/nitro-cron.log 2>&1") | crontab - && ok "Scheduled daily at 03:00" ;; - 2) (crontab -l 2>/dev/null | grep -vF "$SCRIPT_NAME"; echo "0 4 * * * cd $SCRIPT_DIR && ./$SCRIPT_NAME update --branch ${NITRO_BRANCH:-main} >> /var/log/nitro-cron.log 2>&1") | crontab - && ok "Scheduled daily at 04:00" ;; - 3) (crontab -l 2>/dev/null | grep -vF "$SCRIPT_NAME"; echo "0 3 * * 0 cd $SCRIPT_DIR && ./$SCRIPT_NAME update --branch ${NITRO_BRANCH:-main} >> /var/log/nitro-cron.log 2>&1") | crontab - && ok "Scheduled weekly (Sunday 03:00)" ;; + echo -e " ${C_BOLD}1)${C_RESET} Daily 03:00 ${C_BOLD}2)${C_RESET} Daily 04:00 ${C_BOLD}3)${C_RESET} Weekly Sun 03:00" + echo -e " ${C_BOLD}4)${C_RESET} Custom time ${C_BOLD}5)${C_RESET} Remove ${C_BOLD}0)${C_RESET} Return" + echo -en "\n Select: "; local c; read -r c + case "$c" in + 1) (crontab -l 2>/dev/null | grep -vF "$SCRIPT_NAME"; echo "0 3 * * * cd $SCRIPT_DIR && ./$SCRIPT_NAME update --branch $NITRO_BRANCH >> /var/log/nitro-cron.log 2>&1") | crontab - && ok "Scheduled daily 03:00" ;; + 2) (crontab -l 2>/dev/null | grep -vF "$SCRIPT_NAME"; echo "0 4 * * * cd $SCRIPT_DIR && ./$SCRIPT_NAME update --branch $NITRO_BRANCH >> /var/log/nitro-cron.log 2>&1") | crontab - && ok "Scheduled daily 04:00" ;; + 3) (crontab -l 2>/dev/null | grep -vF "$SCRIPT_NAME"; echo "0 3 * * 0 cd $SCRIPT_DIR && ./$SCRIPT_NAME update --branch $NITRO_BRANCH >> /var/log/nitro-cron.log 2>&1") | crontab - && ok "Scheduled weekly" ;; 4) echo -en " Minute [0-59]: "; local m; read -r m echo -en " Hour [0-23]: "; local h; read -r h - echo -en " Day [1-31]: "; local d; read -r d - echo -en " Month [1-12]: "; local mo; read -r mo - (crontab -l 2>/dev/null | grep -vF "$SCRIPT_NAME"; echo "$m $h $d $mo * cd $SCRIPT_DIR && ./$SCRIPT_NAME update --branch ${NITRO_BRANCH:-main} >> /var/log/nitro-cron.log 2>&1") | crontab - && ok "Scheduled custom: $m $h $d $mo *" ;; - 5) crontab -l 2>/dev/null | grep -vF "$SCRIPT_NAME" | crontab - && ok "Schedule removed" ;; - 6) info "Running test update..."; cd "$SCRIPT_DIR" && ./"$SCRIPT_NAME" update --branch "${NITRO_BRANCH:-main}" ;; + (crontab -l 2>/dev/null | grep -vF "$SCRIPT_NAME"; echo "$m $h * * * cd $SCRIPT_DIR && ./$SCRIPT_NAME update --branch $NITRO_BRANCH >> /var/log/nitro-cron.log 2>&1") | crontab - && ok "Scheduled $m $h * * *" ;; + 5) crontab -l 2>/dev/null | grep -vF "$SCRIPT_NAME" | crontab - && ok "Removed" ;; esac } # ============================================================================= -# MAIN INTERACTIVE MENU +# MAIN MENU # ============================================================================= pick_branch_and_update() { - local mode="${SELECTIVE_UPDATE:-full}" local mode_label="Full" - case "$mode" in - emulator) mode_label="Emulator Only" ;; - client) mode_label="Nitro-V3 Client Only" ;; - renderer) mode_label="Renderer Only" ;; - configs) mode_label="Configs Only" ;; + case "${SELECTIVE_UPDATE:-full}" in + emulator) mode_label="Emulator Only" ;; client) mode_label="Client Only" ;; + renderer) mode_label="Renderer Only" ;; configs) mode_label="Configs Only" ;; esac - local BRANCHES=() - while IFS= read -r b; do [ -n "$b" ] && BRANCHES+=("$b"); done < <(detect_available_branches) + local branches=() + while IFS= read -r b; do [ -n "$b" ] && branches+=("$b"); done < <(detect_branches) echo "" - echo -e " ${C_BOLD}${C_CYAN}╔═══════════════════════════════════════════════════════╗${C_RESET}" - echo -e " ${C_CYAN}║${C_RESET} ${C_BOLD}Update: ${C_GREEN}$mode_label${C_RESET} ${C_CYAN}║${C_RESET}" - echo -e " ${C_CYAN}╚═══════════════════════════════════════════════════════╝${C_RESET}" - echo "" - echo -e " ${C_BOLD}Select branch:${C_RESET}" + echo -e " ${C_BOLD}${C_CYAN}Update: ${C_GREEN}$mode_label${C_RESET}" echo "" local i=1 - for b in "${BRANCHES[@]}"; do - local marker=""; [ "$b" = "${NITRO_BRANCH:-main}" ] && marker=" ${C_GREEN}(current)${C_RESET}" - echo -e " ${C_GREEN}${C_BOLD}$i)${C_RESET} ${C_CYAN}$b${C_RESET}$marker" - i=$((i + 1)) + for b in "${branches[@]}"; do + local m=""; [ "$b" = "$NITRO_BRANCH" ] && m=" ${C_GREEN}(current)${C_RESET}" + echo -e " ${C_GREEN}${C_BOLD}$i)${C_RESET} ${C_CYAN}$b${C_RESET}$m" + i=$((i+1)) done - echo -e " ${C_GREEN}${C_BOLD}$i)${C_RESET} Custom branch" - echo "" - echo -en " Select [1-$i]: " + echo -e " ${C_GREEN}${C_BOLD}$i)${C_RESET} Custom" + echo -en "\n Branch [1-$i]: " local bc; read -r bc if [[ "$bc" =~ ^[0-9]+$ ]] && [ "$bc" -ge 1 ] && [ "$bc" -lt "$i" ]; then - NITRO_BRANCH="${BRANCHES[$((bc-1))]}" + NITRO_BRANCH="${branches[$((bc-1))]}" elif [ "$bc" = "$i" ]; then - echo -en " Branch name: "; read -r NITRO_BRANCH - [ -z "$NITRO_BRANCH" ] && NITRO_BRANCH="main" + echo -en " Name: "; read -r NITRO_BRANCH; [ -z "$NITRO_BRANCH" ] && NITRO_BRANCH="main" else - warn "Invalid, using: ${NITRO_BRANCH:-main}" + warn "Invalid, using: $NITRO_BRANCH" fi - echo "" - echo -e " ${C_BOLD}Update Summary:${C_RESET}" - echo -e " ${E_GIT} Branch: ${C_CYAN}${C_BOLD}${NITRO_BRANCH}${C_RESET}" - echo -e " ${E_GEAR} Mode: ${C_GREEN}${C_BOLD}${mode_label}${C_RESET}" - echo -e " ${E_CLOUD} Site: ${C_CYAN}${NITRO_SITE_URL:-?}${C_RESET}" - echo "" - if ! confirm "Start update?"; then info "Cancelled."; return 0; fi + echo -e "\n Branch: ${C_CYAN}${C_BOLD}$NITRO_BRANCH${C_RESET} Mode: ${C_GREEN}$mode_label${C_RESET}" + if ! confirm "Start update?"; then info "Cancelled"; return; fi cmd_update } cmd_interactive() { while true; do - show_banner - local AVAILABLE_BRANCHES=() - while IFS= read -r b; do [ -n "$b" ] && AVAILABLE_BRANCHES+=("$b"); done < <(detect_available_branches) - local branch_display="${C_CYAN}${C_BOLD}${NITRO_BRANCH:-main}${C_RESET}" - local branch_count="${#AVAILABLE_BRANCHES[@]}" - echo -e " ${C_CYAN}╔══════════════════════════════════════════════════════════════════╗${C_RESET}" - echo -e " ${C_CYAN}║${C_RESET} ${C_GOLD}${C_BOLD}Emulator/Nitro-V3/Render Updater — $(date '+%H:%M')${C_RESET} ${C_CYAN}║${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_BOLD}Settings:${C_RESET} ${C_CYAN}║${C_RESET}" - echo -e " ${C_CYAN}║${C_RESET} ${E_GIT} Branch: ${branch_display} ${C_DIM}(${branch_count} detected)${C_RESET} ${C_CYAN}║${C_RESET}" - echo -e " ${C_CYAN}║${C_RESET} ${E_CLOUD} Site: ${C_CYAN}${NITRO_SITE_URL:-?}${C_RESET} ${C_CYAN}║${C_RESET}" - echo -e " ${C_CYAN}║${C_RESET} ${C_CYAN}║${C_RESET}" - echo -e " ${C_CYAN}║${C_RESET} ${C_BG_GREEN}${C_WHITE}${C_BOLD} UPDATE ${C_RESET} ${C_CYAN}║${C_RESET}" - echo -e " ${C_CYAN}║${C_RESET} ${C_GREEN}${C_BOLD}1)${C_RESET} ${E_ROCKET} Quick Update — branch: ${branch_display} ${C_CYAN}║${C_RESET}" - echo -e " ${C_CYAN}║${C_RESET} ${C_GREEN}${C_BOLD}2)${C_RESET} ${E_LIGHTNING} Full Update — everything (pull + build + deploy) ${C_CYAN}║${C_RESET}" - echo -e " ${C_CYAN}║${C_RESET} ${C_GREEN}${C_BOLD}3)${C_RESET} ${E_GEAR} Emulator Only — Java backend build & deploy ${C_CYAN}║${C_RESET}" - echo -e " ${C_CYAN}║${C_RESET} ${C_GREEN}${C_BOLD}4)${C_RESET} ${E_GEAR} Nitro-V3 Client Only — frontend build ${C_CYAN}║${C_RESET}" - echo -e " ${C_CYAN}║${C_RESET} ${C_GREEN}${C_BOLD}5)${C_RESET} ${E_GEAR} Renderer Only — Nitro renderer install ${C_CYAN}║${C_RESET}" - echo -e " ${C_CYAN}║${C_RESET} ${C_GREEN}${C_BOLD}6)${C_RESET} ${E_GEAR} Configs Only — URL sync & merge ${C_CYAN}║${C_RESET}" - echo -e " ${C_CYAN}║${C_RESET} ${C_CYAN}║${C_RESET}" - echo -e " ${C_CYAN}║${C_RESET} ${C_BG_MAGENTA}${C_WHITE}${C_BOLD} TOOLS ${C_RESET} ${C_CYAN}║${C_RESET}" - echo -e " ${C_CYAN}║${C_RESET} ${C_MAGENTA}${C_BOLD}7)${C_RESET} ${E_GIT} Switch Branch — select from detected ${C_CYAN}║${C_RESET}" - echo -e " ${C_CYAN}║${C_RESET} ${C_YELLOW}${C_BOLD}8)${C_RESET} ${E_DB} Database Manager — tables, optimize, queries ${C_CYAN}║${C_RESET}" - echo -e " ${C_CYAN}║${C_RESET} ${C_YELLOW}${C_BOLD}9)${C_RESET} ${E_SHIELD} Backup & Restore ${C_CYAN}║${C_RESET}" - echo -e " ${C_CYAN}║${C_RESET} ${C_YELLOW}${C_BOLD}10)${C_RESET} ${E_WRENCH} Service Manager ${C_CYAN}║${C_RESET}" - echo -e " ${C_CYAN}║${C_RESET} ${C_YELLOW}${C_BOLD}11)${C_RESET} ${E_CHART} System Status ${C_CYAN}║${C_RESET}" - echo -e " ${C_CYAN}║${C_RESET} ${C_YELLOW}${C_BOLD}12)${C_RESET} ${E_HEART} Health Check ${C_CYAN}║${C_RESET}" - echo -e " ${C_CYAN}║${C_RESET} ${C_YELLOW}${C_BOLD}13)${C_RESET} ${E_BROOM} Cache Cleanup ${C_CYAN}║${C_RESET}" - echo -e " ${C_CYAN}║${C_RESET} ${C_YELLOW}${C_BOLD}14)${C_RESET} ${E_EYE} View Logs ${C_CYAN}║${C_RESET}" - echo -e " ${C_CYAN}║${C_RESET} ${C_YELLOW}${C_BOLD}15)${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_BG_BLUE}${C_WHITE}${C_BOLD} ADVANCED ${C_RESET} ${C_CYAN}║${C_RESET}" - echo -e " ${C_CYAN}║${C_RESET} ${C_BLUE}${C_BOLD}16)${C_RESET} ${E_CONSOLE} Git Log — commit history per repo ${C_CYAN}║${C_RESET}" - echo -e " ${C_CYAN}║${C_RESET} ${C_BLUE}${C_BOLD}17)${C_RESET} ${E_GLOBE} Compare Branches — diff between branches ${C_CYAN}║${C_RESET}" - echo -e " ${C_CYAN}║${C_RESET} ${C_BLUE}${C_BOLD}18)${C_RESET} ${E_CLOCK} Update History — backup/changelog ${C_CYAN}║${C_RESET}" - echo -e " ${C_CYAN}║${C_RESET} ${C_BLUE}${C_BOLD}19)${C_RESET} ${E_FIRE} Cron Scheduler — auto-update setup ${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}" + clear + local branches=() + while IFS= read -r b; do [ -n "$b" ] && branches+=("$b"); done < <(detect_branches) + + echo -e "${C_CYAN}" echo "" - echo -en " ${C_CYAN}${C_BOLD}Select [0-19]${C_RESET}: " + echo " ╔═══════════════════════════════════════════════════════════════════════════╗" + echo " ║ ║" + echo " ║ ███████╗███╗ ███╗ █████╗ ██████╗ ████████╗ ║" + echo " ║ ██╔════╝████╗ ████║██╔══██╗██╔══██╗╚══██╔══╝ ║" + echo " ║ █████╗ ██╔████╔██║███████║██████╔╝ ██║ ║" + echo " ║ ██╔══╝ ██║╚██╔╝██║██╔══██║██╔══██╗ ██║ ║" + echo " ║ ███████╗██║ ╚═╝ ██║██║ ██║██║ ██║ ██║ ║" + echo " ║ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝ ║" + echo " ║ ║" + echo " ╠═══════════════════════════════════════════════════════════════════════════╣" + echo -e " ║ ${C_GOLD}${C_BOLD} Emulator / Nitro-V3 / Nitro-V3-Render${C_CYAN} ║" + echo -e " ║ ${C_WHITE}${C_BOLD} Updater by Remco — epicnabbo.nl${C_CYAN} ║" + echo " ╠═══════════════════════════════════════════════════════════════════════════╣" + echo -e " ║ ${C_DIM}Branch: ${C_CYAN}${NITRO_BRANCH}${C_DIM} (${#branches[@]} detected) │ $(date '+%H:%M')${C_CYAN} ║" + echo " ╚═══════════════════════════════════════════════════════════════════════════╝" + echo -e "${C_RESET}" + + echo -e " ${C_BG_GREEN}${C_WHITE}${C_BOLD} UPDATE ${C_RESET}" + echo -e " ${C_GREEN}${C_BOLD}1)${C_RESET} ${E_ROCKET} Quick Update — branch: ${C_CYAN}${NITRO_BRANCH}${C_RESET}" + echo -e " ${C_GREEN}${C_BOLD}2)${C_RESET} ${E_BOLT} Full Update (choose branch)" + echo -e " ${C_GREEN}${C_BOLD}3)${C_RESET} ${E_GEAR} Emulator Only" + echo -e " ${C_GREEN}${C_BOLD}4)${C_RESET} ${E_GEAR} Nitro-V3 Client Only" + echo -e " ${C_GREEN}${C_BOLD}5)${C_RESET} ${E_GEAR} Renderer Only" + echo -e " ${C_GREEN}${C_BOLD}6)${C_RESET} ${E_GEAR} Configs Only" + echo "" + echo -e " ${C_BG_MAGENTA}${C_WHITE}${C_BOLD} TOOLS ${C_RESET}" + echo -e " ${C_MAGENTA}${C_BOLD}7)${C_RESET} ${E_GIT} Switch Branch" + echo -e " ${C_YELLOW}${C_BOLD}8)${C_RESET} ${E_DB} Database" + echo -e " ${C_YELLOW}${C_BOLD}9)${C_RESET} ${E_SHIELD} Backup / Restore" + echo -e " ${C_YELLOW}${C_BOLD}10)${C_RESET} ${E_WRENCH} Services" + echo -e " ${C_YELLOW}${C_BOLD}11)${C_RESET} ${E_CHART} Status" + echo -e " ${C_YELLOW}${C_BOLD}12)${C_RESET} ${E_HEART} Health Check" + echo -e " ${C_YELLOW}${C_BOLD}13)${C_RESET} ${E_BROOM} Clean" + echo -e " ${C_YELLOW}${C_BOLD}14)${C_RESET} ${E_EYE} Logs" + echo -e " ${C_YELLOW}${C_BOLD}15)${C_RESET} ${E_GEAR} Config" + echo "" + echo -e " ${C_BG_BLUE}${C_WHITE}${C_BOLD} ADVANCED ${C_RESET}" + echo -e " ${C_BLUE}${C_BOLD}16)${C_RESET} ${E_CONSOLE} Git Log" + echo -e " ${C_BLUE}${C_BOLD}17)${C_RESET} ${E_GLOBE} Compare Branches" + echo -e " ${C_BLUE}${C_BOLD}18)${C_RESET} ${E_CLOCK} Cron Scheduler" + echo "" + echo -e " ${C_RED}${C_BOLD}0)${C_RESET} Exit" + echo "" + echo -en " ${C_CYAN}${C_BOLD}Select [0-18]${C_RESET}: " local choice; read -r choice + case "$choice" in - 1) SELECTIVE_UPDATE=""; IN_INTERACTIVE=true; set +e; info "Starting quick update..."; cmd_update; set -e; IN_INTERACTIVE=false ;; - 2) SELECTIVE_UPDATE=""; IN_INTERACTIVE=true; set +e; pick_branch_and_update; set -e; IN_INTERACTIVE=false ;; - 3) SELECTIVE_UPDATE="emulator"; IN_INTERACTIVE=true; set +e; pick_branch_and_update; set -e; IN_INTERACTIVE=false ;; - 4) SELECTIVE_UPDATE="client"; IN_INTERACTIVE=true; set +e; pick_branch_and_update; set -e; IN_INTERACTIVE=false ;; - 5) SELECTIVE_UPDATE="renderer"; IN_INTERACTIVE=true; set +e; pick_branch_and_update; set -e; IN_INTERACTIVE=false ;; - 6) SELECTIVE_UPDATE="configs"; IN_INTERACTIVE=true; set +e; pick_branch_and_update; set -e; IN_INTERACTIVE=false ;; + 1) SELECTIVE_UPDATE=""; (cmd_update) || warn "Update had errors" ;; + 2) SELECTIVE_UPDATE=""; (pick_branch_and_update) || warn "Update had errors" ;; + 3) SELECTIVE_UPDATE="emulator"; (pick_branch_and_update) || warn "Update had errors" ;; + 4) SELECTIVE_UPDATE="client"; (pick_branch_and_update) || warn "Update had errors" ;; + 5) SELECTIVE_UPDATE="renderer"; (pick_branch_and_update) || warn "Update had errors" ;; + 6) SELECTIVE_UPDATE="configs"; (pick_branch_and_update) || warn "Update had errors" ;; 7) - echo "" - echo -e " ${C_BOLD}Current: ${C_CYAN}${NITRO_BRANCH:-main}${C_RESET}" echo "" local i=1 - for b in "${AVAILABLE_BRANCHES[@]}"; do - local m=""; [ "$b" = "${NITRO_BRANCH:-main}" ] && m=" ${C_GREEN}(active)${C_RESET}" - echo -e " ${C_GREEN}${C_BOLD}$i)${C_RESET} $b$m" - i=$((i + 1)) + for b in "${branches[@]}"; do + local m=""; [ "$b" = "$NITRO_BRANCH" ] && m=" ${C_GREEN}*${C_RESET}" + echo -e " ${C_GREEN}${C_BOLD}$i)${C_RESET} $b$m"; i=$((i+1)) done echo -e " ${C_GREEN}${C_BOLD}$i)${C_RESET} Custom" - echo "" - echo -en " Select [1-$i]: " - local bc; read -r bc + echo -en "\n Branch [1-$i]: "; local bc; read -r bc if [[ "$bc" =~ ^[0-9]+$ ]] && [ "$bc" -ge 1 ] && [ "$bc" -lt "$i" ]; then - NITRO_BRANCH="${AVAILABLE_BRANCHES[$((bc-1))]}" - ok "Branch: $NITRO_BRANCH" - elif [ "$bc" = "$i" ]; then - echo -en " Branch: "; read -r NITRO_BRANCH - [ -n "$NITRO_BRANCH" ] && ok "Branch: $NITRO_BRANCH" - else - warn "Invalid" + NITRO_BRANCH="${branches[$((bc-1))]}" + elif [ "$bc" = "$i" ]; then echo -en " Name: "; read -r NITRO_BRANCH fi - ;; - 8) cmd_database ;; - 9) local sc; sc=$(select_menu "Backup & Restore" "Create Backup" "Restore Backup" "View Backups" "Return"); case "${sc:-}" in 1) cmd_backup;; 2) cmd_restore;; 3) cmd_backups;; esac ;; - 10) cmd_services ;; - 11) cmd_status ;; - 12) cmd_health ;; - 13) cmd_clean ;; - 14) cmd_logs ;; - 15) cmd_config ;; - 16) cmd_gitlog ;; - 17) cmd_compare ;; - 18) cmd_history ;; - 19) cmd_schedule ;; - 0|q|Q) echo -e "\n ${C_GREEN}Goodbye!${C_RESET}\n"; cursor_show; exit 0 ;; + ok "Branch: $NITRO_BRANCH" ;; + 8) (cmd_database) ;; + 9) (cmd_backup); echo ""; (cmd_backups) ;; + 10) (cmd_services) ;; + 11) (cmd_status) ;; + 12) (cmd_health) ;; + 13) (cmd_clean) ;; + 14) (cmd_logs) ;; + 15) (cmd_config) ;; + 16) (cmd_gitlog) ;; + 17) (cmd_compare) ;; + 18) (cmd_schedule) ;; + 0|q|Q) echo -e "\n ${C_GREEN}Goodbye!${C_RESET}"; cursor_show; exit 0 ;; *) warn "Invalid selection" ;; esac + echo "" echo -en " ${C_DIM}Press Enter to continue...${C_RESET}" read -r @@ -1427,55 +897,45 @@ cmd_interactive() { # 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}${C_GOLD}Emulator / Nitro-V3 / Nitro-V3-Render Updater${C_RESET}" - echo -e " ${C_DIM}by Remco — epicnabbo.nl${C_RESET}" + echo -e " ${C_GOLD}${C_BOLD}Emulator / Nitro-V3 / Nitro-V3-Render Updater${C_RESET}" + echo -e " ${C_DIM}by Remco — epicnabbo.nl v${SCRIPT_VERSION}${C_RESET}" + echo "" + echo -e " ${C_BOLD}USAGE:${C_RESET} $SCRIPT_NAME [options]" echo "" echo -e " ${C_BOLD}COMMANDS:${C_RESET}" - echo -e " ${C_GREEN}update${C_RESET} Run full update (--only for selective)" - echo -e " ${C_GREEN}interactive${C_RESET} Launch TUI control panel (default)" - echo -e " ${C_GREEN}status${C_RESET} System status dashboard" - echo -e " ${C_GREEN}health${C_RESET} Health check with scoring" - 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 backups" - echo -e " ${C_GREEN}gitlog${C_RESET} Git commit history" - echo -e " ${C_GREEN}compare${C_RESET} Branch comparison" - echo -e " ${C_GREEN}history${C_RESET} Update history" - echo -e " ${C_GREEN}schedule${C_RESET} Cron scheduler" - echo -e " ${C_GREEN}services${C_RESET} Service manager" - echo -e " ${C_GREEN}database${C_RESET} Database tools" - echo -e " ${C_GREEN}config${C_RESET} Show configuration" - echo -e " ${C_GREEN}logs${C_RESET} View logs" - echo -e " ${C_GREEN}clean${C_RESET} Cache cleanup" - echo -e " ${C_GREEN}help${C_RESET} Show help" + echo -e " ${C_GREEN}interactive${C_RESET} Open menu (default)" + echo -e " ${C_GREEN}update${C_RESET} Run update (--only=emulator|client|renderer|configs)" + echo -e " ${C_GREEN}backup${C_RESET} Create backup" + echo -e " ${C_GREEN}restore${C_RESET} Restore backup" + echo -e " ${C_GREEN}backups${C_RESET} List backups" + echo -e " ${C_GREEN}status${C_RESET} System dashboard" + echo -e " ${C_GREEN}health${C_RESET} Health check" + echo -e " ${C_GREEN}services${C_RESET} Service manager" + echo -e " ${C_GREEN}database${C_RESET} Database tools" + echo -e " ${C_GREEN}config${C_RESET} Show config" + echo -e " ${C_GREEN}logs${C_RESET} View logs" + echo -e " ${C_GREEN}clean${C_RESET} Cache cleanup" + echo -e " ${C_GREEN}gitlog${C_RESET} Git history" + echo -e " ${C_GREEN}compare${C_RESET} Branch diff" + echo -e " ${C_GREEN}schedule${C_RESET} Cron setup" + echo -e " ${C_GREEN}help${C_RESET} This help" echo "" echo -e " ${C_BOLD}OPTIONS:${C_RESET}" - echo -e " ${C_CYAN}--dry-run, -n${C_RESET} Preview only" - echo -e " ${C_CYAN}--force, -f${C_RESET} Force update" - echo -e " ${C_CYAN}--verbose, -v${C_RESET} Debug output" - echo -e " ${C_CYAN}--quiet, -q${C_RESET} Suppress output" - echo -e " ${C_CYAN}--branch, -b${C_RESET} Git branch (auto-detects)" - echo -e " ${C_CYAN}--url, -u${C_RESET} Site URL" - echo -e " ${C_CYAN}--only=${C_RESET} emulator|client|renderer|configs" - echo -e " ${C_CYAN}--version, -V${C_RESET} Show version" - echo -e " ${C_CYAN}--help, -h${C_RESET} Show 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_CYAN}--branch, -b${C_RESET} Branch (auto-detects)" + echo -e " ${C_CYAN}--url, -u${C_RESET} Site URL" + echo -e " ${C_CYAN}--only=${C_RESET} emulator|client|renderer|configs" + echo -e " ${C_CYAN}--dry-run, -n${C_RESET} Preview only" + echo -e " ${C_CYAN}--verbose, -v${C_RESET} Debug output" + echo -e " ${C_CYAN}--help, -h${C_RESET} Help" + echo -e " ${C_CYAN}--version, -V${C_RESET} Version" echo "" } # ============================================================================= -# ARGUMENT PARSING +# MAIN # ============================================================================= -parse_args() { +main() { local cmd="" while [ $# -gt 0 ]; do case "$1" in @@ -1488,7 +948,6 @@ parse_args() { backups|bl) cmd="backups" ;; gitlog|gl) cmd="gitlog" ;; compare|diff) cmd="compare" ;; - history|hist) cmd="history" ;; schedule|cron) cmd="schedule" ;; services|svc) cmd="services" ;; database|db) cmd="database" ;; @@ -1496,104 +955,41 @@ parse_args() { 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 ;; + version|-V|--version) echo "v${SCRIPT_VERSION}"; exit 0 ;; --dry-run|-n) DRY_RUN=true ;; - --force|-f) FORCE=true ;; - --verbose|-v) VERBOSE=true ;; - --quiet|-q) QUIET=true ;; + --verbose|-v) ;; # accepted but not used yet --branch|-b) shift; NITRO_BRANCH="$1" ;; --url|-u) shift; NITRO_SITE_URL="$1" ;; --only=*) SELECTIVE_UPDATE="${1#*=}" ;; - -*) die "Unknown option: $1 (use --help)" ;; + -*) echo "Unknown: $1"; exit 1 ;; *) [ -z "$cmd" ] && cmd="interactive" ;; esac shift done - if [ -n "$SELECTIVE_UPDATE" ]; then - case "$SELECTIVE_UPDATE" in - emulator|client|renderer|configs) ;; - *) die "Invalid --only: $SELECTIVE_UPDATE" ;; - esac - fi [ -z "$cmd" ] && cmd="interactive" - echo "$cmd" -} - -# ============================================================================= -# MAIN -# ============================================================================= -main() { - if [ -z "${_UNBUFFERED:-}" ] && command -v unbuffer &>/dev/null; then - export _UNBUFFERED=1 - exec unbuffer bash "$0" "$@" - fi - LOCKFILE="/tmp/$(basename "$0").lock" - exec 200>"$LOCKFILE" - flock -n 200 2>/dev/null || die "Another instance is already running" - - 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}" - - if [ -z "${NITRO_SITE_URL:-}" ] && [ -n "${APP_URL:-}" ]; then - NITRO_SITE_URL="$APP_URL" - fi - - 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 - - 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_CRED="-h $DB_HOST -P $DB_PORT -u $DB_USER --ssl-verify-server-cert=OFF" - [ -n "$DB_PASS" ] && export MYSQL_PWD="$DB_PASS" + # Log redirect exec > >(tee -a "$LOG_FILE") 2>&1 - local cmd - cmd=$(parse_args "$@") case "$cmd" in interactive) cmd_interactive ;; update) cmd_update ;; - status) cmd_status ;; - health) cmd_health ;; backup) cmd_backup ;; restore) cmd_restore ;; backups) cmd_backups ;; - gitlog) cmd_gitlog ;; - compare) cmd_compare ;; - history) cmd_history ;; - schedule) cmd_schedule ;; + status) cmd_status ;; + health) cmd_health ;; services) cmd_services ;; database) cmd_database ;; config) cmd_config ;; logs) cmd_logs ;; clean) cmd_clean ;; + gitlog) cmd_gitlog ;; + compare) cmd_compare ;; + schedule) cmd_schedule ;; help) show_help ;; esac + cursor_show }