#!/bin/bash set -euo pipefail # ╔══════════════════════════════════════════════════════════════════════════════╗ # ║ ║ # ║ ███╗ ██╗███████╗██╗ ██╗██████╗ ██████╗ ███████╗██╗ ██╗ ║ # ║ ████╗ ██║██╔════╝██║ ██║██╔══██╗██╔═══██╗██╔════╝██║ ██║ ║ # ║ ██╔██╗ ██║█████╗ ██║ ██║██████╔╝██║ ██║███████╗██║ ██║ ║ # ║ ██║╚██╗██║██╔══╝ ██║ ██║██╔══██╗██║ ██║╚════██║██║ ██║ ║ # ║ ██║ ╚████║███████╗╚██████╔╝██║ ██║╚██████╔╝███████║╚██████╔╝ ║ # ║ ╚═╝ ╚═══╝╚══════╝ ╚═════╝ ╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═════╝ ║ # ║ ║ # ║ NITRO V3 — Enterprise Deployment & Management System ║ # ║ Version 6.0.0 — Build 20260625 ║ # ║ ║ # ╚══════════════════════════════════════════════════════════════════════════════╝ # ============================================================================= # CONFIGURATION & CONSTANTS # ============================================================================= readonly SCRIPT_VERSION="6.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 HAD_UPDATES=false NITRO_BUILT=false DRY_RUN=false VERBOSE=false QUIET=false FORCE=false SELECTIVE_UPDATE="" ROLLBACK_NEEDED=false LAST_BACKUP="" NOTIFY_FAILED=false ERRORS=0 WARNINGS=0 UPDATED_REPOS=() # --- Load .env ---------------------------------------------------------------- if [ -f "$SCRIPT_DIR/.env" ]; then set -a . "$SCRIPT_DIR/.env" set +a fi # --- Logging ------------------------------------------------------------------ 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:-}" # ============================================================================= # TERMINAL CAPABILITIES # ============================================================================= TERM_COLS=$(tput cols 2>/dev/null || echo 80) TERM_LINES=$(tput lines 2>/dev/null || echo 24) cursor_hide() { tput civis 2>/dev/null || true; } cursor_show() { tput cnorm 2>/dev/null || true; } clear_line() { printf "\r\033[K"; } # ============================================================================= # UI COMPONENTS # ============================================================================= show_banner() { clear echo -e "${C_CYAN}${C_BOLD}" cat << 'BANNER' ╔══════════════════════════════════════════════════════════════════════╗ ║ ║ ║ ███╗ ██╗███████╗██╗ ██╗██████╗ ██████╗ ███████╗██╗ ██╗ ║ ║ ████╗ ██║██╔════╝██║ ██║██╔══██╗██╔═══██╗██╔════╝██║ ██║ ║ ║ ██╔██╗ ██║█████╗ ██║ ██║██████╔╝██║ ██║███████╗██║ ██║ ║ ║ ██║╚██╗██║██╔══╝ ██║ ██║██╔══██╗██║ ██║╚════██║██║ ██║ ║ ║ ██║ ╚████║███████╗╚██████╔╝██║ ██║╚██████╔╝███████║╚██████╔╝ ║ ║ ╚═╝ ╚═══╝╚══════╝ ╚═════╝ ╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═════╝ ║ ║ ║ ╚══════════════════════════════════════════════════════════════════════╝ BANNER echo -e "${C_RESET}" echo -e " ${C_GOLD}${C_BOLD} ██████╗ ██╗ ██╗██╗███╗ ██╗███████╗ ████████╗███████╗██████╗ ███╗ ███╗${C_RESET}" echo -e " ${C_GOLD}${C_BOLD} ██╔══██╗██║ ██║██║████╗ ██║██╔════╝ ╚══██╔══╝██╔════╝██╔══██╗████╗ ████║${C_RESET}" echo -e " ${C_GOLD}${C_BOLD} ██████╔╝██║ ██║██║██╔██╗ ██║███████╗ ██║ █████╗ ██████╔╝██╔████╔██║${C_RESET}" echo -e " ${C_GOLD}${C_BOLD} ██╔══██╗██║ ██║██║██║╚██╗██║╚════██║ ██║ ██╔══╝ ██╔══██╗██║╚██╔╝██║${C_RESET}" echo -e " ${C_GOLD}${C_BOLD} ██║ ██║╚██████╔╝██║██║ ╚████║███████║ ██║ ███████╗██║ ██║██║ ╚═╝ ██║${C_RESET}" echo -e " ${C_GOLD}${C_BOLD} ╚═╝ ╚═╝ ╚═════╝ ╚═╝╚═╝ ╚═══╝╚══════╝ ╚═╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝${C_RESET}" echo "" echo -e " ${C_CYAN} ════════════════════════════════════════════════════════════════${C_RESET}" echo -e " ${C_WHITE}${C_BOLD} Enterprise Deployment & Management System${C_RESET} ${C_DIM}v${SCRIPT_VERSION}${C_RESET}" echo -e " ${C_CYAN} ════════════════════════════════════════════════════════════════${C_RESET}" echo "" echo -e " ${C_DIM} $(date '+%A, %d %B %Y — %H:%M:%S')${C_RESET}" echo "" } show_mini_banner() { echo -e "${C_CYAN}${C_BOLD}╔══════════════════════════════════════════════════════════════╗${C_RESET}" echo -e "${C_CYAN}${C_BOLD}║ ${E_ROCKET} Nitro V3 Update System v${SCRIPT_VERSION} ║${C_RESET}" echo -e "${C_CYAN}${C_BOLD}╚══════════════════════════════════════════════════════════════╝${C_RESET}" echo "" } box_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"; } die() { echo "" echo -e " ${C_BG_RED}${C_WHITE}${C_BOLD} FATAL ERROR ${C_RESET}" echo -e " ${C_RED}${E_CROSS} $1${C_RESET}" log_write "FATAL" "$1" cleanup_notify_failure "$1" cursor_show exit 1 } log_write() { local level="$1" msg="$2" printf "[%s] [%s] %s\n" "$(date '+%Y-%m-%d %H:%M:%S')" "$level" "$msg" >> "$LOG_FILE" 2>/dev/null || true } # --- Spinner ------------------------------------------------------------------ SPINNER_PID="" spinner_start() { local msg="${1:-Working...}" local frames=("⠋" "⠙" "⠹" "⠸" "⠼" "⠴" "⠦" "⠧" "⠇" "⠏") cursor_hide ( local i=0 while true; do printf "\r ${C_CYAN}${frames[$i]}${C_RESET} ${C_BOLD}%s${C_RESET} " "$msg" i=$(( (i + 1) % ${#frames[@]} )) sleep 0.1 done ) & SPINNER_PID=$! disown "$SPINNER_PID" 2>/dev/null || true } spinner_stop() { local status="${1:-ok}" if [ -n "$SPINNER_PID" ] && kill -0 "$SPINNER_PID" 2>/dev/null; then kill "$SPINNER_PID" 2>/dev/null || true wait "$SPINNER_PID" 2>/dev/null || true SPINNER_PID="" fi clear_line cursor_show case "$status" in ok) printf "\r ${C_GREEN}${E_CHECK}${C_RESET} Done\n" ;; warn) printf "\r ${C_YELLOW}${E_WARN}${C_RESET} Done with warnings\n" ;; fail) printf "\r ${C_RED}${E_CROSS}${C_RESET} Failed\n" ;; skip) printf "\r ${C_DIM}— Skipped${C_RESET}\n" ;; esac } # --- Progress Bar ------------------------------------------------------------- progress_bar() { local current="${1:-0}" total="${2:-100}" width="${3:-40}" local pct=$((current * 100 / total)) local filled=$((current * width / total)) local empty=$((width - filled)) printf "\r ${C_CYAN}[${C_RESET}" printf '%0.s█' $(seq 1 $filled 2>/dev/null || echo 1) printf '%0.s░' $(seq 1 $empty 2>/dev/null || echo 1) printf "${C_CYAN}]${C_RESET} ${C_BOLD}%3d%%${C_RESET}" "$pct" } confirm() { local msg="${1:-Continue?}" default="${2:-y}" local hint="[Y/n]" [ "$default" = "n" ] && hint="[y/N]" echo -en " ${C_YELLOW}${E_ARROW}${C_RESET} ${C_BOLD}$msg${C_RESET} $hint " read -r reply reply="${reply:-$default}" [[ "$reply" =~ ^[Yy] ]] } select_menu() { local title="$1" shift local options=("$@") echo "" echo -e " ${C_BOLD}${C_CYAN}$title${C_RESET}" echo "" local i=1 for opt in "${options[@]}"; do echo -e " ${C_GREEN}${C_BOLD}$i)${C_RESET} $opt" i=$((i + 1)) done echo "" echo -en " ${C_CYAN}Select [1-$((i-1))]${C_RESET}: " local choice read -r choice if [[ "$choice" =~ ^[0-9]+$ ]] && [ "$choice" -ge 1 ] && [ "$choice" -lt "$i" ]; then echo "$choice" return 0 fi return 1 } # --- Notifications ----------------------------------------------------------- notify() { 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." } 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 fi [ $exit_code -ne 0 ] && cleanup_notify_failure "Script exited with code $exit_code" } 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." } # ============================================================================= # GIT HELPERS # ============================================================================= detect_available_branches() { local repos=("$EMULATOR_DIR/Emulator" "$NITRO_CLIENT" "$NITRO_RENDERER") 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) 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") done <<< "$branches" done printf '%s\n' "${result[@]}" } detect_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 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")" } 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 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" 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..." mkdir -p "$BACKUP_DIR" BACKUP_FILE="$BACKUP_DIR/backup_$(date +%Y%m%d_%H%M%S).sql" spinner_start "Backing up database..." if mariadb-dump $MYSQL_CRED --force --skip-lock-tables --routines --events --triggers "$DB_NAME" > "$BACKUP_FILE" 2>/dev/null; then LAST_BACKUP="$BACKUP_FILE" BK_SIZE=$(stat -c%s "$BACKUP_FILE" 2>/dev/null || echo 0) if [ "$BK_SIZE" -lt 1024 ]; then rm -f "$BACKUP_FILE"; spinner_stop fail; die "Backup too small — aborting"; fi spinner_stop ok ok "Backup saved: $(numfmt --to=iec "$BK_SIZE") — $(basename "$BACKUP_FILE")" else spinner_stop fail die "Database backup failed" fi 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" fi info "Building emulator..." cd "$EMULATOR_DIR/Emulator" spinner_start "Compiling Java..." 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..." cd target/ cat << EOF > emulator #!/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 ROLLBACK_NEEDED=false else info "Already up to date, skipping" fi } update_renderer() { step "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" else info "Already up to date, skipping" fi } update_client() { step "Update & Build Nitro-V3" if git_update "$NITRO_CLIENT" "$NITRO_BRANCH"; then info "New commits detected" HAD_UPDATES=true NITRO_BUILT=true 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"; } 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" 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" 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 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)) 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" } cleanup_phase() { step "Cleanup" info "Removing old logs..." local LOG_COUNT=$(find "$EMULATOR_DIR" -name "*.log" -mtime +14 2>/dev/null | wc -l) find "$EMULATOR_DIR" -name "*.log" -mtime +14 -exec rm -f {} \; 2>/dev/null || true ok "Removed $LOG_COUNT old log(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 rm -rf /tmp/nitro-* 2>/dev/null || true ok "Cleanup complete" } 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 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:-}" else warn "Site did not respond: ${NITRO_SITE_URL:-}" fi } show_summary() { ELAPSED=$(($(date +%s) - START_TIME)) ELAPSED_FMT=$(printf '%dm %ds' $((ELAPSED / 60)) $((ELAPSED % 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}" else 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:-?}")" echo -e " ${C_CYAN}╚══════════════════════════════════════════════════════════════════╝${C_RESET}" echo "" notify "SUCCESS" "Completed in $ELAPSED_FMT (${ERRORS} errors, ${WARNINGS} warnings)" } # ============================================================================= # COMMANDS # ============================================================================= 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 "Log: $LOG_FILE" case "${SELECTIVE_UPDATE:-full}" in emulator) update_emulator ;; renderer) update_renderer ;; client) update_client ;; configs) sync_configs ;; *) update_emulator; update_renderer; update_client; sync_configs; cleanup_phase; set_permissions; restart_services; health_check ;; esac show_summary } cmd_backup() { preflight step "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 else spinner_stop fail die "Backup failed" fi } cmd_restore() { preflight step "Database Restore" [ ! -d "$BACKUP_DIR" ] && die "No backup directory found" 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}" echo "" 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)) 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 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"; } } cmd_backups() { step "Backup Inventory" [ ! -d "$BACKUP_DIR" ] && { warn "No backup directory"; return 0; } 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" 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 "" } cmd_status() { step "System Status Dashboard" 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 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 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}" printf " ${C_CYAN}║${C_RESET} %-20s %b${C_CYAN} ║${C_RESET}\n" "$svc" "$st" 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 echo "" } 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"; }; } 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 echo "" } 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 ;; esac } 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 ;; 2) mariadb $MYSQL_CRED -e "SHOW TABLES FROM $DB_NAME" 2>/dev/null ;; 3) mariadb $MYSQL_CRED -e "SELECT table_name AS 'Table', ROUND(data_length/1024/1024,2) AS 'Data MB', ROUND(index_length/1024/1024,2) AS 'Index MB', ROUND((data_length+index_length)/1024/1024,2) AS 'Total MB' FROM information_schema.tables WHERE table_schema='$DB_NAME' ORDER BY (data_length+index_length) DESC" 2>/dev/null ;; 4) spinner_start "Optimizing..." 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" ;; esac } 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 echo "" } cmd_logs() { step "Log Viewer" [ ! -d "$LOG_DIR" ] && { warn "No log directory"; return 0; } 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}" 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)) 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))]}" fi } 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" ;; esac } # ============================================================================= # NEW: GIT LOG VIEWER # ============================================================================= cmd_gitlog() { step "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 local repo="" case "$rc" in 1) repo="$EMULATOR_DIR/Emulator" ;; 2) repo="$NITRO_CLIENT" ;; 3) repo="$NITRO_RENDERER" ;; *) warn "Invalid"; 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 "" } # ============================================================================= # NEW: 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}" echo "" 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}" 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 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}" fi echo "" done } # ============================================================================= # 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 # ============================================================================= cmd_schedule() { step "Scheduled Updates (Cron)" 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 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)" ;; 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}" ;; esac } # ============================================================================= # MAIN INTERACTIVE 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" ;; esac local BRANCHES=() while IFS= read -r b; do [ -n "$b" ] && BRANCHES+=("$b"); done < <(detect_available_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 "" 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)) done echo -e " ${C_GREEN}${C_BOLD}$i)${C_RESET} Custom branch" echo "" echo -en " Select [1-$i]: " local bc; read -r bc if [[ "$bc" =~ ^[0-9]+$ ]] && [ "$bc" -ge 1 ] && [ "$bc" -lt "$i" ]; then NITRO_BRANCH="${BRANCHES[$((bc-1))]}" elif [ "$bc" = "$i" ]; then echo -en " Branch name: "; read -r NITRO_BRANCH [ -z "$NITRO_BRANCH" ] && NITRO_BRANCH="main" else warn "Invalid, using: ${NITRO_BRANCH:-main}" 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 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_BOLD}${E_ROCKET} NITRO V3 CONTROL PANEL — $(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}" echo "" echo -en " ${C_CYAN}${C_BOLD}Select [0-19]${C_RESET}: " local choice; read -r choice case "$choice" in 1) SELECTIVE_UPDATE=""; info "Starting quick update..."; cmd_update ;; 2) SELECTIVE_UPDATE=""; pick_branch_and_update ;; 3) SELECTIVE_UPDATE="emulator"; pick_branch_and_update ;; 4) SELECTIVE_UPDATE="client"; pick_branch_and_update ;; 5) SELECTIVE_UPDATE="renderer"; pick_branch_and_update ;; 6) SELECTIVE_UPDATE="configs"; pick_branch_and_update ;; 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)) done echo -e " ${C_GREEN}${C_BOLD}$i)${C_RESET} Custom" echo "" echo -en " Select [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" 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 ;; *) warn "Invalid selection" ;; esac echo "" echo -en " ${C_DIM}Press Enter to continue...${C_RESET}" read -r done } # ============================================================================= # HELP # ============================================================================= show_help() { show_mini_banner echo -e " ${C_BOLD}USAGE:${C_RESET}" echo -e " $SCRIPT_NAME ${C_CYAN}${C_RESET} [${C_DIM}options${C_RESET}]" echo "" echo -e " ${C_BOLD}COMMANDS:${C_RESET}" echo -e " ${C_GREEN}update${C_RESET} Run full update (--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 "" 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 "" } # ============================================================================= # ARGUMENT PARSING # ============================================================================= parse_args() { local cmd="" while [ $# -gt 0 ]; do case "$1" in update|up) cmd="update" ;; interactive|menu|ui) cmd="interactive" ;; status|st) cmd="status" ;; health|check) cmd="health" ;; backup|bk) cmd="backup" ;; restore|rs) cmd="restore" ;; backups|bl) cmd="backups" ;; gitlog|gl) cmd="gitlog" ;; compare|diff) cmd="compare" ;; history|hist) cmd="history" ;; schedule|cron) cmd="schedule" ;; services|svc) cmd="services" ;; database|db) cmd="database" ;; config|cfg) cmd="config" ;; logs|log) cmd="logs" ;; clean|cl) cmd="clean" ;; help|-h|--help) cmd="help" ;; version|-V|--version) echo "Nitro V3 Update System v${SCRIPT_VERSION} (build ${SCRIPT_BUILD})"; exit 0 ;; --dry-run|-n) DRY_RUN=true ;; --force|-f) FORCE=true ;; --verbose|-v) VERBOSE=true ;; --quiet|-q) QUIET=true ;; --branch|-b) shift; NITRO_BRANCH="$1" ;; --url|-u) shift; NITRO_SITE_URL="$1" ;; --only=*) SELECTIVE_UPDATE="${1#*=}" ;; -*) die "Unknown option: $1 (use --help)" ;; *) [ -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" 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 ;; services) cmd_services ;; database) cmd_database ;; config) cmd_config ;; logs) cmd_logs ;; clean) cmd_clean ;; help) show_help ;; esac cursor_show } main "$@"