diff --git a/update-Nitrov3.sh b/update-Nitrov3.sh index 0581ca4..aaba683 100755 --- a/update-Nitrov3.sh +++ b/update-Nitrov3.sh @@ -11,14 +11,14 @@ set -euo pipefail # ║ ╚═╝ ╚═══╝╚══════╝ ╚═════╝ ╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═════╝ ║ # ║ ║ # ║ NITRO V3 — Enterprise Deployment & Management System ║ -# ║ Version 5.0.0 — Build 20260625 ║ +# ║ Version 6.0.0 — Build 20260625 ║ # ║ ║ # ╚══════════════════════════════════════════════════════════════════════════════╝ # ============================================================================= # CONFIGURATION & CONSTANTS # ============================================================================= -readonly SCRIPT_VERSION="5.0.0" +readonly SCRIPT_VERSION="6.0.0" readonly SCRIPT_BUILD="20260625" readonly SCRIPT_NAME="$(basename "$0")" readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" @@ -32,6 +32,7 @@ if [ -t 1 ] && command -v tput &>/dev/null && [ "$(tput colors 2>/dev/null || ec 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" @@ -45,10 +46,16 @@ if [ -t 1 ] && command -v tput &>/dev/null && [ "$(tput colors 2>/dev/null || ec 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_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_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) ---------------------------------- @@ -57,13 +64,15 @@ if locale charmap 2>/dev/null | grep -qi utf; then 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_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_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 ------------------------------------------------------------------- @@ -110,18 +119,14 @@ 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 + echo -e "${C_CYAN}${C_BOLD}" cat << 'BANNER' ╔══════════════════════════════════════════════════════════════════════╗ @@ -133,11 +138,21 @@ show_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_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 "" } @@ -149,11 +164,11 @@ show_mini_banner() { echo "" } -# --- Box Drawing -------------------------------------------------------------- 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}" @@ -166,11 +181,21 @@ box_top() { box_line() { local text="${1:-}" width="${2:-60}" - local pad=$((width - ${#text} - 4)) + 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}╚" @@ -178,43 +203,26 @@ box_bottom() { echo -e "╝${C_RESET}" } -# --- Step Display ------------------------------------------------------------- +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_BOLD}" - printf " ╔══════════════════════════════════════════════════════════════╗\n" - printf " ║ Step %d/%d │ %-46s║\n" "$STEP_NUM" "$STEP_TOTAL" "$1" - printf " ╚══════════════════════════════════════════════════════════════╝\n" - echo -e "${C_RESET}" + echo -e " ${C_BG_CYAN}${C_WHITE}${C_BOLD} Step ${STEP_NUM}/${STEP_TOTAL} │ $1 ${C_RESET}" + echo "" } -# --- 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" -} +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 "" @@ -226,11 +234,6 @@ die() { 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 @@ -264,10 +267,10 @@ spinner_stop() { 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" ;; + 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 } @@ -283,7 +286,6 @@ progress_bar() { 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]" @@ -294,7 +296,6 @@ confirm() { [[ "$reply" =~ ^[Yy] ]] } -# --- Select Menu -------------------------------------------------------------- select_menu() { local title="$1" shift @@ -328,21 +329,9 @@ notify() { 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 + -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() { @@ -378,10 +367,9 @@ trap 'echo ""; cursor_show; die "Interrupted by user (SIGINT)"' SIGINT trap 'cursor_show; die "Terminated (SIGTERM)"' SIGTERM # ============================================================================= -# PRE-FLIGHT CHECKS +# PREFLIGHT # ============================================================================= 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" @@ -392,8 +380,6 @@ preflight() { [ -z "$NITRO_SITE_URL" ] && die "NITRO_SITE_URL is required" export NITRO_SITE_URL fi - - # Branch — accept any branch name, auto-detect per repo later if [ -z "${NITRO_BRANCH:-}" ]; then read -r -p " Enter branch [main]: " NITRO_BRANCH NITRO_BRANCH="${NITRO_BRANCH:-main}" @@ -402,12 +388,11 @@ preflight() { } # ============================================================================= -# SYSTEM CHECKS +# PRE-FLIGHT 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=() @@ -423,7 +408,6 @@ preflight_checks() { fi ok "All ${#REQUIRED_CMDS[@]} required commands available" - # Required directories info "Checking directories..." REQUIRED_DIRS=( "$EMULATOR_DIR/Emulator" @@ -432,48 +416,33 @@ preflight_checks() { "$NITRO_SRC_DIR" ) for dir in "${REQUIRED_DIRS[@]}"; do - if [ -d "$dir" ]; then - debug " Found: $dir" - else - die "Required directory not found: $dir" - fi + [ -d "$dir" ] || die "Required directory not found: $dir" 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 + [ "$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" - # 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 + [ "$TOTAL_MEM" -gt 0 ] && ok "Memory: ${AVAIL_MEM}MB available / ${TOTAL_MEM}MB total" || warn "Could not determine memory" - # 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..." + 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" @@ -481,74 +450,45 @@ preflight_checks() { 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 + redis-cli ping 2>/dev/null | grep -q PONG && ok "Redis: Connected" || warn "Redis: Not responding" else - warn "redis-cli not found, skipping" + warn "redis-cli not found" 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" + nginx -t 2>/dev/null && ok "Nginx config valid" || warn "Nginx config has issues" fi - # SSL certificate check - if [[ "$NITRO_SITE_URL" == https://* ]]; then + 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 )) + SSL_DAYS_LEFT=$(( (SSL_EXPIRY_EPOCH - $(date +%s)) / 86400 )) if [ "$SSL_DAYS_LEFT" -gt 30 ]; then - ok "SSL certificate valid for $SSL_DAYS_LEFT more days" + ok "SSL valid for $SSL_DAYS_LEFT days" elif [ "$SSL_DAYS_LEFT" -gt 0 ]; then - warn "SSL certificate expires in $SSL_DAYS_LEFT days!" + warn "SSL expires in $SSL_DAYS_LEFT days!" else - fail "SSL certificate has EXPIRED!" + fail "SSL certificate 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" - ) + 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##*:}" + 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 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 + 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 } @@ -559,8 +499,8 @@ preflight_checks() { 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 "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 @@ -572,1117 +512,7 @@ dry_run_display() { } # ============================================================================= -# 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 -} - -detect_branch() { - local repo="$1" preferred="$2" - cd "$repo" 2>/dev/null || { echo "main"; return; } - - # Try exact match first - if git rev-parse --verify "$preferred" &>/dev/null; then - echo "$preferred" - return - fi - - # Try common variants: dev -> Dev -> development -> Development - local variants=("$preferred" "${preferred^}" "development" "Development" "main" "master" "Main" "Master") - for v in "${variants[@]}"; do - if git rev-parse --verify "$v" &>/dev/null; then - echo "$v" - return - fi - done - - # Try remote branches - local remote_branch=$(git branch -r 2>/dev/null | grep -oP "origin/\K${preferred}" | head -1 || echo "") - if [ -n "$remote_branch" ]; then - echo "$remote_branch" - return - fi - - # Last resort: current branch or main - local current=$(git branch --show-current 2>/dev/null || echo "main") - echo "${current:-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 "") - - # Detect best available branch - local resolved_branch - resolved_branch=$(detect_branch "$repo" "$branch") - - if [ "$resolved_branch" != "$branch" ]; then - info "Branch '$branch' not available in $(basename "$repo"), using '$resolved_branch' instead" - fi - - if ! git checkout "$resolved_branch" 2>/dev/null; then - # Fallback: try to find ANY branch - 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" ] -} - -# ============================================================================= -# MAIN INTERACTIVE MENU +# GIT HELPERS # ============================================================================= detect_available_branches() { local repos=("$EMULATOR_DIR/Emulator" "$NITRO_CLIENT" "$NITRO_RENDERER") @@ -1691,11 +521,7 @@ detect_available_branches() { [ -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) + 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 @@ -1708,144 +534,748 @@ detect_available_branches() { printf '%s\n' "${result[@]}" } -cmd_interactive() { - while true; do - show_banner +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")" +} - # Detect available branches from repos - local AVAILABLE_BRANCHES=() - while IFS= read -r b; do - [ -n "$b" ] && AVAILABLE_BRANCHES+=("$b") - done < <(detect_available_branches) +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" ] +} - # Color current branch - local branch_display="${C_CYAN}${C_BOLD}${NITRO_BRANCH:-main}${C_RESET}" - local branch_count="${#AVAILABLE_BRANCHES[@]}" - local branch_list="${AVAILABLE_BRANCHES[*]}" - [ ${#branch_list} -gt 45 ] && branch_list="${branch_list:0:42}..." +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 +} - 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}Current Settings:${C_RESET} ${C_CYAN}║${C_RESET}" - echo -e " ${C_CYAN}║${C_RESET} ${E_GIT} Branch: ${branch_display} ${C_DIM}(${branch_count} branches 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_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_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 & apply to all repos ${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 — create, restore, list ${C_CYAN}║${C_RESET}" - echo -e " ${C_CYAN}║${C_RESET} ${C_YELLOW}${C_BOLD}10)${C_RESET} ${E_WRENCH} Service Manager — start/stop/restart ${C_CYAN}║${C_RESET}" - echo -e " ${C_CYAN}║${C_RESET} ${C_YELLOW}${C_BOLD}11)${C_RESET} ${E_CHART} System Status — dashboard overview ${C_CYAN}║${C_RESET}" - echo -e " ${C_CYAN}║${C_RESET} ${C_YELLOW}${C_BOLD}12)${C_RESET} ${E_HEART} Health Check — comprehensive diagnostics ${C_CYAN}║${C_RESET}" - echo -e " ${C_CYAN}║${C_RESET} ${C_YELLOW}${C_BOLD}13)${C_RESET} ${E_BROOM} Cache Cleanup — yarn, redis, logs, temp ${C_CYAN}║${C_RESET}" - echo -e " ${C_CYAN}║${C_RESET} ${C_YELLOW}${C_BOLD}14)${C_RESET} ${E_EYE} View Logs — browse update history ${C_CYAN}║${C_RESET}" - echo -e " ${C_CYAN}║${C_RESET} ${C_YELLOW}${C_BOLD}15)${C_RESET} ${E_GEAR} Configuration — show all settings ${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}" +# ============================================================================= +# 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 -en " ${C_CYAN}${C_BOLD}Select [0-15]${C_RESET}: " - local choice - read -r choice - - case "$choice" in - 1) - # Quick update — uses current branch - SELECTIVE_UPDATE="" - echo "" - info "Starting update on branch: ${C_BOLD}${NITRO_BRANCH:-main}${C_RESET}" - cmd_update - ;; - 2) - # Full update — ask branch first - 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) - # Switch branch submenu - echo "" - echo -e " ${C_BOLD}${C_CYAN}Switch Branch${C_RESET}" - echo -e " ${C_DIM}Currently: ${NITRO_BRANCH:-main}${C_RESET}" - echo "" - local i=1 - for b in "${AVAILABLE_BRANCHES[@]}"; do - local marker="" - [ "$b" = "${NITRO_BRANCH:-main}" ] && marker=" ${C_GREEN}(active)${C_RESET}" - echo -e " ${C_GREEN}${C_BOLD}$i)${C_RESET} $b$marker" - i=$((i + 1)) - done - echo -e " ${C_GREEN}${C_BOLD}$i)${C_RESET} Type custom branch name" - 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 switched to: ${C_BOLD}$NITRO_BRANCH${C_RESET}" - elif [ "$bc" = "$i" ]; then - echo -en " Enter branch name: " - read -r NITRO_BRANCH - [ -n "$NITRO_BRANCH" ] && ok "Branch set to: ${C_BOLD}$NITRO_BRANCH${C_RESET}" - else - warn "Invalid selection" - fi - ;; - 8) cmd_database ;; - 9) - 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 - ;; - 10) cmd_services ;; - 11) cmd_status ;; - 12) cmd_health ;; - 13) cmd_clean ;; - 14) cmd_logs ;; - 15) cmd_config ;; - 0|q|Q) echo -e "\n ${C_GREEN}Goodbye!${C_RESET}\n"; cursor_show; exit 0 ;; - *) warn "Invalid selection" ;; - esac - + 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 "" - echo -en " ${C_DIM}Press Enter to continue...${C_RESET}" - read -r done } # ============================================================================= -# BRANCH PICKER + UPDATE +# 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}" @@ -1856,120 +1286,184 @@ pick_branch_and_update() { renderer) mode_label="Renderer Only" ;; configs) mode_label="Configs Only" ;; esac - - # Detect available branches local BRANCHES=() - while IFS= read -r b; do - [ -n "$b" ] && BRANCHES+=("$b") - done < <(detect_available_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 "" - - # Branch selection - echo -e " ${C_BOLD}Select branch to update:${C_RESET}" + 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}" + 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} Type custom branch name" + echo -e " ${C_GREEN}${C_BOLD}$i)${C_RESET} Custom branch" echo "" echo -en " Select [1-$i]: " - local bc - read -r bc - + 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 " Enter branch name: " - read -r NITRO_BRANCH - [ -z "$NITRO_BRANCH" ] && NITRO_BRANCH="${NITRO_BRANCH:-main}" + echo -en " Branch name: "; read -r NITRO_BRANCH + [ -z "$NITRO_BRANCH" ] && NITRO_BRANCH="main" else - warn "Invalid selection, using current branch: ${NITRO_BRANCH:-main}" + 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 "Update cancelled." - return 0 - fi - + 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 +} + # ============================================================================= -# COMMAND: HELP +# 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 (use --only for selective)" - echo -e " ${C_GREEN}interactive${C_RESET} Launch interactive TUI control panel (default)" - 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}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 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 -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 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 (any branch, auto-detects per repo)" + 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} Selective update: emulator|client|renderer|configs" + 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 this help" + echo -e " ${C_CYAN}--help, -h${C_RESET} Show help" echo "" - echo -e " ${C_BOLD}EXAMPLES:${C_RESET}" echo -e " ${C_DIM}$SCRIPT_NAME interactive${C_RESET}" echo -e " ${C_DIM}$SCRIPT_NAME update --branch dev --dry-run${C_RESET}" echo -e " ${C_DIM}$SCRIPT_NAME update --only=emulator --force${C_RESET}" echo -e " ${C_DIM}$SCRIPT_NAME backup${C_RESET}" - echo -e " ${C_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 "" } @@ -1978,47 +1472,45 @@ show_help() { # ============================================================================= 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" ;; + 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 for usage)" ;; - *) [ -z "$cmd" ] && cmd="interactive" ;; + --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 - - # 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)" ;; + *) die "Invalid --only: $SELECTIVE_UPDATE" ;; esac fi - - # Default command: interactive menu [ -z "$cmd" ] && cmd="interactive" - echo "$cmd" } @@ -2026,23 +1518,14 @@ parse_args() { # 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}" @@ -2061,14 +1544,16 @@ main() { HEALTH_RETRIES="${NITRO_HEALTH_RETRIES:-12}" HEALTH_INTERVAL="${NITRO_HEALTH_INTERVAL:-5}" - # Derive ws/wss protocol and domain + 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 - # 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:-}}" @@ -2078,33 +1563,32 @@ main() { 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 ;; + 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 ;; - update) cmd_update ;; esac - cursor_show }