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