Files
Atomcms-edit/update-Nitrov3.sh
T
root e8d7ffe656 feat: complete rewrite to v6.0.0 — enterprise-grade management system
NEW FEATURES:
- Premium dual-line ASCII art banner (NITRO + V3)
- Git Log Viewer (option 16) — last 20 commits per repo
- Branch Compare (option 17) — diff between any 2 branches
- Update History (option 18) — backup/changelog viewer
- Cron Scheduler (option 19) — auto-update setup (daily/weekly/custom)
- Box key-value display (box_kv) for clean data presentation
- box_sep for section dividers
- Gold/orange/purple/teal color themes
- Active emoji set expanded (globe, console, server, database, diagnostics)

IMPROVEMENTS:
- Menu restructured: UPDATE / TOOLS / ADVANCED sections
- 19 interactive options total
- Health check with percentage scoring
- Git commit history with date, hash, message columns
- Branch comparison shows ahead/behind counts + diff stat
- Cron setup with test-run option
- Cleaner code organization — removed duplication
- All update functions extracted to standalone functions
- Box drawing with proper UTF-8 padding

FIXES:
- Branch detection properly filters HEAD and arrow entries
- Default command is now interactive (shows menu)
- NITRO_SITE_URL derived from APP_URL before URL calculation
2026-06-25 20:04:34 +02:00

1596 lines
81 KiB
Bash
Executable File

#!/bin/bash
set -euo pipefail
# ╔══════════════════════════════════════════════════════════════════════════════╗
# ║ ║
# ║ ███╗ ██╗███████╗██╗ ██╗██████╗ ██████╗ ███████╗██╗ ██╗ ║
# ║ ████╗ ██║██╔════╝██║ ██║██╔══██╗██╔═══██╗██╔════╝██║ ██║ ║
# ║ ██╔██╗ ██║█████╗ ██║ ██║██████╔╝██║ ██║███████╗██║ ██║ ║
# ║ ██║╚██╗██║██╔══╝ ██║ ██║██╔══██╗██║ ██║╚════██║██║ ██║ ║
# ║ ██║ ╚████║███████╗╚██████╔╝██║ ██║╚██████╔╝███████║╚██████╔╝ ║
# ║ ╚═╝ ╚═══╝╚══════╝ ╚═════╝ ╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═════╝ ║
# ║ ║
# ║ NITRO V3 — Enterprise Deployment & Management System ║
# ║ Version 6.0.0 — Build 20260625 ║
# ║ ║
# ╚══════════════════════════════════════════════════════════════════════════════╝
# =============================================================================
# CONFIGURATION & CONSTANTS
# =============================================================================
readonly SCRIPT_VERSION="6.0.0"
readonly SCRIPT_BUILD="20260625"
readonly SCRIPT_NAME="$(basename "$0")"
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly SCRIPT_PID=$$
readonly NITRO_MIN_BASH_VERSION="4.0"
# --- Colors -------------------------------------------------------------------
if [ -t 1 ] && command -v tput &>/dev/null && [ "$(tput colors 2>/dev/null || echo 0)" -ge 8 ]; then
C_RESET="\033[0m"
C_BOLD="\033[1m"
C_DIM="\033[2m"
C_UNDERLINE="\033[4m"
C_BLINK="\033[5m"
C_ITALIC="\033[3m"
C_RED="\033[1;31m"
C_GREEN="\033[1;32m"
C_YELLOW="\033[1;33m"
C_BLUE="\033[1;34m"
C_MAGENTA="\033[1;35m"
C_CYAN="\033[1;36m"
C_WHITE="\033[1;37m"
C_BG_RED="\033[41m"
C_BG_GREEN="\033[42m"
C_BG_YELLOW="\033[43m"
C_BG_BLUE="\033[44m"
C_BG_MAGENTA="\033[45m"
C_BG_CYAN="\033[46m"
C_BG_WHITE="\033[47m"
C_GOLD="\033[38;5;220m"
C_ORANGE="\033[38;5;208m"
C_PURPLE="\033[38;5;135m"
C_TEAL="\033[38;5;30m"
else
C_RESET="" C_BOLD="" C_DIM="" C_UNDERLINE="" C_BLINK="" C_ITALIC=""
C_RED="" C_GREEN="" C_YELLOW="" C_BLUE="" C_MAGENTA="" C_CYAN="" C_WHITE=""
C_BG_RED="" C_BG_GREEN="" C_BG_YELLOW="" C_BG_BLUE="" C_BG_MAGENTA="" C_BG_CYAN="" C_BG_WHITE=""
C_GOLD="" C_ORANGE="" C_PURPLE="" C_TEAL=""
fi
# --- Emojis (fallback to ASCII on non-UTF8) ----------------------------------
if locale charmap 2>/dev/null | grep -qi utf; then
E_CHECK="✔" E_CROSS="✘" E_WARN="⚠" E_ARROW="➜" E_DOT="●" E_STAR="★"
E_CLOCK="⏱" E_GEAR="⚙" E_ROCKET="🚀" E_SHIELD="🛡" E_DB="🗄"
E_FOLDER="📁" E_GIT="🔀" E_WRENCH="🔧" E_CHART="📊" E_LIGHTNING="⚡"
E_CLOCK2="🕐" E_BROOM="🧹" E_EYE="👁" E_LOCK="🔒" E_HEART="♥"
E_CLOUD="☁" E_FIRE="🔥" E_PACKAGE="📦" E_BULLET="•" E_GLOBE="🌐"
E_CONSOLE="🖥" E_SERVER="🖧" E_DATABASE="💾" E_DIAG="🩺"
else
E_CHECK="[OK]" E_CROSS="[FAIL]" E_WARN="[!]" E_ARROW="->" E_DOT="*"
E_STAR="*" E_CLOCK="[T]" E_GEAR="[G]" E_ROCKET="[R]" E_SHIELD="[S]" E_DB="[DB]"
E_FOLDER="[D]" E_GIT="[G]" E_WRENCH="[W]" E_CHART="[C]" E_LIGHTNING="[!]"
E_CLOCK2="[T]" E_BROOM="[C]" E_EYE="[E]" E_LOCK="[L]" E_HEART="[H]"
E_CLOUD="[C]" E_FIRE="[F]" E_PACKAGE="[P]" E_BULLET="-" E_GLOBE="[W]"
E_CONSOLE="[M]" E_SERVER="[S]" E_DATABASE="[D]" E_DIAG="[X]"
fi
# --- Timing -------------------------------------------------------------------
readonly START_TIME=$(date +%s)
# --- State --------------------------------------------------------------------
STEP_NUM=0
STEP_TOTAL=8
HAD_UPDATES=false
NITRO_BUILT=false
DRY_RUN=false
VERBOSE=false
QUIET=false
FORCE=false
SELECTIVE_UPDATE=""
ROLLBACK_NEEDED=false
LAST_BACKUP=""
NOTIFY_FAILED=false
ERRORS=0
WARNINGS=0
UPDATED_REPOS=()
# --- Load .env ----------------------------------------------------------------
if [ -f "$SCRIPT_DIR/.env" ]; then
set -a
. "$SCRIPT_DIR/.env"
set +a
fi
# --- Logging ------------------------------------------------------------------
LOG_DIR="${NITRO_LOG_DIR:-$SCRIPT_DIR/storage/logs}"
mkdir -p "$LOG_DIR" 2>/dev/null || true
LOG_FILE="$LOG_DIR/update-$(date +%Y%m%d-%H%M%S).log"
# --- Notifications ------------------------------------------------------------
NOTIFY_URL="${NITRO_NOTIFY_URL:-}"
# =============================================================================
# TERMINAL CAPABILITIES
# =============================================================================
TERM_COLS=$(tput cols 2>/dev/null || echo 80)
TERM_LINES=$(tput lines 2>/dev/null || echo 24)
cursor_hide() { tput civis 2>/dev/null || true; }
cursor_show() { tput cnorm 2>/dev/null || true; }
clear_line() { printf "\r\033[K"; }
# =============================================================================
# UI COMPONENTS
# =============================================================================
show_banner() {
clear
echo -e "${C_CYAN}${C_BOLD}"
cat << 'BANNER'
╔══════════════════════════════════════════════════════════════════════╗
║ ║
║ ███╗ ██╗███████╗██╗ ██╗██████╗ ██████╗ ███████╗██╗ ██╗ ║
║ ████╗ ██║██╔════╝██║ ██║██╔══██╗██╔═══██╗██╔════╝██║ ██║ ║
║ ██╔██╗ ██║█████╗ ██║ ██║██████╔╝██║ ██║███████╗██║ ██║ ║
║ ██║╚██╗██║██╔══╝ ██║ ██║██╔══██╗██║ ██║╚════██║██║ ██║ ║
║ ██║ ╚████║███████╗╚██████╔╝██║ ██║╚██████╔╝███████║╚██████╔╝ ║
║ ╚═╝ ╚═══╝╚══════╝ ╚═════╝ ╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═════╝ ║
║ ║
╚══════════════════════════════════════════════════════════════════════╝
BANNER
echo -e "${C_RESET}"
echo -e " ${C_GOLD}${C_BOLD} ██████╗ ██╗ ██╗██╗███╗ ██╗███████╗ ████████╗███████╗██████╗ ███╗ ███╗${C_RESET}"
echo -e " ${C_GOLD}${C_BOLD} ██╔══██╗██║ ██║██║████╗ ██║██╔════╝ ╚══██╔══╝██╔════╝██╔══██╗████╗ ████║${C_RESET}"
echo -e " ${C_GOLD}${C_BOLD} ██████╔╝██║ ██║██║██╔██╗ ██║███████╗ ██║ █████╗ ██████╔╝██╔████╔██║${C_RESET}"
echo -e " ${C_GOLD}${C_BOLD} ██╔══██╗██║ ██║██║██║╚██╗██║╚════██║ ██║ ██╔══╝ ██╔══██╗██║╚██╔╝██║${C_RESET}"
echo -e " ${C_GOLD}${C_BOLD} ██║ ██║╚██████╔╝██║██║ ╚████║███████║ ██║ ███████╗██║ ██║██║ ╚═╝ ██║${C_RESET}"
echo -e " ${C_GOLD}${C_BOLD} ╚═╝ ╚═╝ ╚═════╝ ╚═╝╚═╝ ╚═══╝╚══════╝ ╚═╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝${C_RESET}"
echo ""
echo -e " ${C_CYAN} ════════════════════════════════════════════════════════════════${C_RESET}"
echo -e " ${C_WHITE}${C_BOLD} Enterprise Deployment & Management System${C_RESET} ${C_DIM}v${SCRIPT_VERSION}${C_RESET}"
echo -e " ${C_CYAN} ════════════════════════════════════════════════════════════════${C_RESET}"
echo ""
echo -e " ${C_DIM} $(date '+%A, %d %B %Y — %H:%M:%S')${C_RESET}"
echo ""
}
show_mini_banner() {
echo -e "${C_CYAN}${C_BOLD}╔══════════════════════════════════════════════════════════════╗${C_RESET}"
echo -e "${C_CYAN}${C_BOLD}${E_ROCKET} Nitro V3 Update System v${SCRIPT_VERSION}${C_RESET}"
echo -e "${C_CYAN}${C_BOLD}╚══════════════════════════════════════════════════════════════╝${C_RESET}"
echo ""
}
box_top() {
local title="${1:-}" width="${2:-60}"
if [ -n "$title" ]; then
local pad=$((width - ${#title} - 6))
[ $pad -lt 0 ] && pad=0
printf "${C_CYAN}╔═══ ${C_BOLD}%s${C_RESET}${C_CYAN}" "$title"
printf '═%.0s' $(seq 1 $pad 2>/dev/null || echo 1)
echo -e "╗${C_RESET}"
else
printf "${C_CYAN}╔"
printf '═%.0s' $(seq 1 $width)
echo -e "╗${C_RESET}"
fi
}
box_line() {
local text="${1:-}" width="${2:-60}"
local clean_text=$(echo -e "$text" | sed 's/\x1b\[[0-9;]*m//g')
local pad=$((width - ${#clean_text} - 4))
[ $pad -lt 0 ] && pad=0
printf "${C_CYAN}${C_RESET} %s%*s${C_CYAN}${C_RESET}\n" "$text" $pad ""
}
box_kv() {
local key="${1}" val="${2}" width="${3:-60}"
local clean_key=$(echo -e "$key" | sed 's/\x1b\[[0-9;]*m//g')
local clean_val=$(echo -e "$val" | sed 's/\x1b\[[0-9;]*m//g')
local pad=$((width - ${#clean_key} - ${#clean_val} - 6))
[ $pad -lt 0 ] && pad=0
printf "${C_CYAN}${C_RESET} %s%*s%s ${C_CYAN}${C_RESET}\n" "$key" $pad "" "$val"
}
box_bottom() {
local width="${1:-60}"
printf "${C_CYAN}╚"
printf '═%.0s' $(seq 1 $width)
echo -e "╝${C_RESET}"
}
box_sep() {
local width="${1:-60}"
printf "${C_CYAN}${C_RESET}"
printf '═%.0s' $(seq 1 $width)
echo -e "╣${C_RESET}"
}
step() {
STEP_NUM=$((STEP_NUM + 1))
echo ""
echo -e " ${C_BG_CYAN}${C_WHITE}${C_BOLD} Step ${STEP_NUM}/${STEP_TOTAL}$1 ${C_RESET}"
echo ""
}
info() { echo -e " ${C_BLUE}${E_ARROW}${C_RESET} $1"; log_write "INFO" "$1"; }
info_indent() { echo -e " ${C_DIM}$1${C_RESET}"; log_write "INFO" "$1"; }
ok() { echo -e " ${C_GREEN}${E_CHECK}${C_RESET} $1"; log_write "OK" "$1"; }
warn() { echo -e " ${C_YELLOW}${E_WARN}${C_RESET} $1"; WARNINGS=$((WARNINGS + 1)); log_write "WARN" "$1"; }
fail() { echo -e " ${C_RED}${E_CROSS}${C_RESET} $1"; log_write "FAIL" "$1"; }
debug() { [ "$VERBOSE" = true ] && echo -e " ${C_DIM}${E_DOT} [DEBUG] $1${C_RESET}"; log_write "DEBUG" "$1"; }
die() {
echo ""
echo -e " ${C_BG_RED}${C_WHITE}${C_BOLD} FATAL ERROR ${C_RESET}"
echo -e " ${C_RED}${E_CROSS} $1${C_RESET}"
log_write "FATAL" "$1"
cleanup_notify_failure "$1"
cursor_show
exit 1
}
log_write() {
local level="$1" msg="$2"
printf "[%s] [%s] %s\n" "$(date '+%Y-%m-%d %H:%M:%S')" "$level" "$msg" >> "$LOG_FILE" 2>/dev/null || true
}
# --- Spinner ------------------------------------------------------------------
SPINNER_PID=""
spinner_start() {
local msg="${1:-Working...}"
local frames=("⠋" "⠙" "⠹" "⠸" "⠼" "⠴" "⠦" "⠧" "⠇" "⠏")
cursor_hide
(
local i=0
while true; do
printf "\r ${C_CYAN}${frames[$i]}${C_RESET} ${C_BOLD}%s${C_RESET} " "$msg"
i=$(( (i + 1) % ${#frames[@]} ))
sleep 0.1
done
) &
SPINNER_PID=$!
disown "$SPINNER_PID" 2>/dev/null || true
}
spinner_stop() {
local status="${1:-ok}"
if [ -n "$SPINNER_PID" ] && kill -0 "$SPINNER_PID" 2>/dev/null; then
kill "$SPINNER_PID" 2>/dev/null || true
wait "$SPINNER_PID" 2>/dev/null || true
SPINNER_PID=""
fi
clear_line
cursor_show
case "$status" in
ok) printf "\r ${C_GREEN}${E_CHECK}${C_RESET} Done\n" ;;
warn) printf "\r ${C_YELLOW}${E_WARN}${C_RESET} Done with warnings\n" ;;
fail) printf "\r ${C_RED}${E_CROSS}${C_RESET} Failed\n" ;;
skip) printf "\r ${C_DIM}— Skipped${C_RESET}\n" ;;
esac
}
# --- Progress Bar -------------------------------------------------------------
progress_bar() {
local current="${1:-0}" total="${2:-100}" width="${3:-40}"
local pct=$((current * 100 / total))
local filled=$((current * width / total))
local empty=$((width - filled))
printf "\r ${C_CYAN}[${C_RESET}"
printf '%0.s█' $(seq 1 $filled 2>/dev/null || echo 1)
printf '%0.s░' $(seq 1 $empty 2>/dev/null || echo 1)
printf "${C_CYAN}]${C_RESET} ${C_BOLD}%3d%%${C_RESET}" "$pct"
}
confirm() {
local msg="${1:-Continue?}" default="${2:-y}"
local hint="[Y/n]"
[ "$default" = "n" ] && hint="[y/N]"
echo -en " ${C_YELLOW}${E_ARROW}${C_RESET} ${C_BOLD}$msg${C_RESET} $hint "
read -r reply
reply="${reply:-$default}"
[[ "$reply" =~ ^[Yy] ]]
}
select_menu() {
local title="$1"
shift
local options=("$@")
echo ""
echo -e " ${C_BOLD}${C_CYAN}$title${C_RESET}"
echo ""
local i=1
for opt in "${options[@]}"; do
echo -e " ${C_GREEN}${C_BOLD}$i)${C_RESET} $opt"
i=$((i + 1))
done
echo ""
echo -en " ${C_CYAN}Select [1-$((i-1))]${C_RESET}: "
local choice
read -r choice
if [[ "$choice" =~ ^[0-9]+$ ]] && [ "$choice" -ge 1 ] && [ "$choice" -lt "$i" ]; then
echo "$choice"
return 0
fi
return 1
}
# --- Notifications -----------------------------------------------------------
notify() {
local status="$1" message="$2"
[ -z "$NOTIFY_URL" ] && return 0
local elapsed=$(($(date +%s) - START_TIME))
local elapsed_fmt
elapsed_fmt=$(printf '%dm %ds' $((elapsed / 60)) $((elapsed % 60)))
local color="good"
[ "$status" != "SUCCESS" ] && color="danger"
[ "$status" = "WARNING" ] && color="warning"
curl -s -o /dev/null -X POST "$NOTIFY_URL" \
-H "Content-Type: application/json" \
-d "{\"text\":\"[Nitro Update v${SCRIPT_VERSION}] $status\",\"attachments\":[{\"color\":\"$color\",\"fields\":[{\"title\":\"Status\",\"value\":\"$message\",\"short\":true},{\"title\":\"Duration\",\"value\":\"$elapsed_fmt\",\"short\":true},{\"title\":\"Server\",\"value\":\"$(hostname)\",\"short\":true},{\"title\":\"Log\",\"value\":\"$LOG_FILE\",\"short\":false}]}]}" 2>/dev/null || true
}
cleanup_notify_failure() {
$NOTIFY_FAILED && return
NOTIFY_FAILED=true
notify "FAILED" "$1"
}
# =============================================================================
# ROLLBACK SYSTEM
# =============================================================================
rollback_db() {
[ -z "$LAST_BACKUP" ] && { warn "No backup file known — cannot rollback DB."; return 1; }
[ ! -f "$LAST_BACKUP" ] && { warn "Backup file not found: $LAST_BACKUP"; return 1; }
info "Rolling back database from: $LAST_BACKUP"
mariadb $MYSQL_CRED "$DB_NAME" < "$LAST_BACKUP" && ok "Database restored." || warn "Rollback failed."
}
cleanup_on_exit() {
local exit_code=$?
cursor_show
if [ $exit_code -ne 0 ] && [ "$ROLLBACK_NEEDED" = true ]; then
echo ""
echo -e " ${C_BG_RED}${C_WHITE}${C_BOLD} UPDATE FAILED — INITIATING ROLLBACK ${C_RESET}"
echo ""
rollback_db
fi
[ $exit_code -ne 0 ] && cleanup_notify_failure "Script exited with code $exit_code"
}
trap cleanup_on_exit EXIT
trap 'echo ""; cursor_show; die "Interrupted by user (SIGINT)"' SIGINT
trap 'cursor_show; die "Terminated (SIGTERM)"' SIGTERM
# =============================================================================
# PREFLIGHT
# =============================================================================
preflight() {
if [ -z "${NITRO_SITE_URL:-}" ] && [ -n "${APP_URL:-}" ]; then
NITRO_SITE_URL="$APP_URL"
info "Using APP_URL from .env: $NITRO_SITE_URL"
fi
if [ -z "${NITRO_SITE_URL:-}" ]; then
read -r -p " Enter your site URL (e.g. https://example.com): " NITRO_SITE_URL
NITRO_SITE_URL="${NITRO_SITE_URL%/}"
[ -z "$NITRO_SITE_URL" ] && die "NITRO_SITE_URL is required"
export NITRO_SITE_URL
fi
if [ -z "${NITRO_BRANCH:-}" ]; then
read -r -p " Enter branch [main]: " NITRO_BRANCH
NITRO_BRANCH="${NITRO_BRANCH:-main}"
export NITRO_BRANCH
fi
}
# =============================================================================
# PRE-FLIGHT CHECKS
# =============================================================================
preflight_checks() {
step "System Diagnostics"
info "Checking required commands..."
local REQUIRED_CMDS=(git mariadb mariadb-dump mvn node python3 yarn)
local MISSING_CMDS=()
for cmd in "${REQUIRED_CMDS[@]}"; do
if command -v "$cmd" &>/dev/null; then
debug " Found: $cmd ($(command -v "$cmd"))"
else
MISSING_CMDS+=("$cmd")
fi
done
if [ ${#MISSING_CMDS[@]} -gt 0 ]; then
die "Missing required commands: ${MISSING_CMDS[*]}"
fi
ok "All ${#REQUIRED_CMDS[@]} required commands available"
info "Checking directories..."
REQUIRED_DIRS=(
"$EMULATOR_DIR/Emulator"
"$NITRO_RENDERER"
"$NITRO_CLIENT"
"$NITRO_SRC_DIR"
)
for dir in "${REQUIRED_DIRS[@]}"; do
[ -d "$dir" ] || die "Required directory not found: $dir"
done
ok "All ${#REQUIRED_DIRS[@]} directories exist"
info "Checking disk space..."
AVAIL_KB=$(df --output=avail "$EMULATOR_DIR" 2>/dev/null | tail -1)
AVAIL_GB=$(( AVAIL_KB / 1024 / 1024 ))
[ "$AVAIL_GB" -lt "$MIN_DISK_GB" ] && die "Only ${AVAIL_GB}GB free (min ${MIN_DISK_GB}GB required)"
ok "${AVAIL_GB}GB disk space available"
info "Checking system memory..."
TOTAL_MEM=$(free -m 2>/dev/null | awk '/^Mem:/{print $2}' || echo "0")
AVAIL_MEM=$(free -m 2>/dev/null | awk '/^Mem:/{print $7}' || echo "0")
[ "$TOTAL_MEM" -gt 0 ] && ok "Memory: ${AVAIL_MEM}MB available / ${TOTAL_MEM}MB total" || warn "Could not determine memory"
info "Checking CPU load..."
CPU_LOAD=$(awk '{printf "%.1f", $1}' /proc/loadavg 2>/dev/null || echo "N/A")
CPU_CORES=$(nproc 2>/dev/null || echo "?")
ok "CPU Load: $CPU_LOAD / $CPU_CORES cores"
info "Checking database connection..."
mariadb $MYSQL_CRED -e "SELECT 1" "$DB_NAME" &>/dev/null || die "Cannot connect to database $DB_NAME on $DB_HOST:$DB_PORT"
DB_SIZE=$(mariadb $MYSQL_CRED -N -e "SELECT ROUND(SUM(data_length + index_length) / 1024 / 1024, 1) FROM information_schema.tables WHERE table_schema='$DB_NAME'" "$DB_NAME" 2>/dev/null || echo "?")
DB_TABLES=$(mariadb $MYSQL_CRED -N -e "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='$DB_NAME'" "$DB_NAME" 2>/dev/null || echo "?")
ok "Database OK — ${DB_SIZE}MB, ${DB_TABLES} tables"
info "Checking Laravel..."
if [ -f "$SCRIPT_DIR/artisan" ]; then
LARAVEL_VER=$(php "$SCRIPT_DIR/artisan" --version 2>/dev/null | head -1 || echo "unknown")
ok "Laravel: $LARAVEL_VER"
else
warn "Laravel artisan not found"
fi
info "Checking Redis..."
if command -v redis-cli &>/dev/null; then
redis-cli ping 2>/dev/null | grep -q PONG && ok "Redis: Connected" || warn "Redis: Not responding"
else
warn "redis-cli not found"
fi
info "Checking Nginx..."
if command -v nginx &>/dev/null; then
nginx -t 2>/dev/null && ok "Nginx config valid" || warn "Nginx config has issues"
fi
if [[ "${NITRO_SITE_URL:-}" == https://* ]]; then
info "Checking SSL certificate..."
SSL_DOMAIN=$(echo "$NITRO_SITE_URL" | sed 's|https://||' | sed 's|/.*||')
SSL_EXPIRY=$(echo | openssl s_client -servername "$SSL_DOMAIN" -connect "$SSL_DOMAIN:443" 2>/dev/null | openssl x509 -noout -enddate 2>/dev/null | cut -d= -f2 || echo "")
if [ -n "$SSL_EXPIRY" ]; then
SSL_EXPIRY_EPOCH=$(date -d "$SSL_EXPIRY" +%s 2>/dev/null || echo "0")
SSL_DAYS_LEFT=$(( (SSL_EXPIRY_EPOCH - $(date +%s)) / 86400 ))
if [ "$SSL_DAYS_LEFT" -gt 30 ]; then
ok "SSL valid for $SSL_DAYS_LEFT days"
elif [ "$SSL_DAYS_LEFT" -gt 0 ]; then
warn "SSL expires in $SSL_DAYS_LEFT days!"
else
fail "SSL certificate EXPIRED!"
ERRORS=$((ERRORS + 1))
fi
fi
fi
info "Checking git repositories..."
local REPOS=("Emulator:$EMULATOR_DIR/Emulator" "Nitro-V3:$NITRO_CLIENT" "Nitro_Render_V3:$NITRO_RENDERER")
for repo_entry in "${REPOS[@]}"; do
local name="${repo_entry%%:*}" path="${repo_entry##*:}"
if [ -d "$path/.git" ]; then
local branch=$(cd "$path" && git branch --show-current 2>/dev/null || echo "?")
local commit=$(cd "$path" && git log --oneline -1 2>/dev/null | cut -c1-8 || echo "?")
local dirty=$(cd "$path" && git status --porcelain 2>/dev/null | wc -l)
[ "$dirty" -gt 0 ] && warn "$name has $dirty uncommitted changes" || ok "$name is clean ($branch @ $commit)"
fi
done
}
# =============================================================================
# DRY-RUN DISPLAY
# =============================================================================
dry_run_display() {
echo ""
box_top "DRY-RUN SUMMARY" 56
box_line "Site: ${NITRO_SITE_URL:-?}" 56
box_line "Branch: ${NITRO_BRANCH:-main}" 56
box_line "Emulator: $EMULATOR_DIR/Emulator" 56
box_line "Nitro-V3: $NITRO_CLIENT" 56
box_line "Renderer: $NITRO_RENDERER" 56
box_line "Mode: ${SELECTIVE_UPDATE:-full}" 56
box_line "Log: $LOG_FILE" 56
box_bottom 56
echo ""
info "Dry-run complete. No changes were made."
}
# =============================================================================
# GIT HELPERS
# =============================================================================
detect_available_branches() {
local repos=("$EMULATOR_DIR/Emulator" "$NITRO_CLIENT" "$NITRO_RENDERER")
local result=()
for repo in "${repos[@]}"; do
[ -d "$repo/.git" ] || continue
local branches
branches=$(cd "$repo" && git branch -r 2>/dev/null | \
grep -v '\->' | sed 's/^[[:space:]]*//' | sed 's/^origin\///' | grep -v '^HEAD$' | sort -u)
while IFS= read -r b; do
[ -z "$b" ] && continue
local found=false
for ab in "${result[@]}"; do
[ "$ab" = "$b" ] && found=true && break
done
$found || result+=("$b")
done <<< "$branches"
done
printf '%s\n' "${result[@]}"
}
detect_branch() {
local repo="$1" preferred="$2"
cd "$repo" 2>/dev/null || { echo "main"; return; }
local variants=("$preferred" "${preferred^}" "development" "Development" "main" "master")
for v in "${variants[@]}"; do
git rev-parse --verify "$v" &>/dev/null && { echo "$v"; return; }
done
local remote_branch=$(git branch -r 2>/dev/null | grep -oP "origin/\K${preferred}" | head -1 || echo "")
[ -n "$remote_branch" ] && { echo "$remote_branch"; return; }
echo "$(git branch --show-current 2>/dev/null || echo "main")"
}
git_update() {
local repo="$1" branch="$2"
cd "$repo" || { warn "Cannot access $repo"; return 1; }
git stash --include-untracked 2>/dev/null || true
local old_head
old_head=$(git rev-parse HEAD 2>/dev/null || echo "")
local resolved_branch
resolved_branch=$(detect_branch "$repo" "$branch")
if [ "$resolved_branch" != "$branch" ]; then
info "Branch '$branch' not in $(basename "$repo"), using '$resolved_branch' instead"
fi
if ! git checkout "$resolved_branch" 2>/dev/null; then
local fallback=$(git branch -r 2>/dev/null | head -1 | sed 's/origin\///' | xargs || echo "main")
warn "Could not checkout '$resolved_branch', trying '$fallback'"
if ! git checkout "$fallback" 2>/dev/null; then
warn "No suitable branch found in $(basename "$repo"), skipping"
return 1
fi
resolved_branch="$fallback"
fi
if ! git pull origin "$resolved_branch" 2>/dev/null && ! git pull 2>/dev/null; then
warn "Git pull failed in $(basename "$repo") (branch: $resolved_branch)"
return 1
fi
[ "$(git rev-parse HEAD 2>/dev/null)" != "$old_head" ]
}
clean_node_modules() {
rm -rf node_modules 2>/dev/null || sudo rm -rf node_modules 2>/dev/null || true
if [ "$(stat -c '%U' . 2>/dev/null)" != "$(whoami)" ] && command -v sudo &>/dev/null; then
sudo chown -R "$(whoami)":"$(whoami)" . 2>/dev/null || true
fi
}
# =============================================================================
# UPDATE FUNCTIONS
# =============================================================================
update_emulator() {
step "Update & Build Emulator"
if git_update "$EMULATOR_DIR/Emulator" "$NITRO_BRANCH"; then
info "New commits detected"
HAD_UPDATES=true
ROLLBACK_NEEDED=true
UPDATED_REPOS+=("Emulator")
info "Creating database backup..."
mkdir -p "$BACKUP_DIR"
BACKUP_FILE="$BACKUP_DIR/backup_$(date +%Y%m%d_%H%M%S).sql"
spinner_start "Backing up database..."
if mariadb-dump $MYSQL_CRED --force --skip-lock-tables --routines --events --triggers "$DB_NAME" > "$BACKUP_FILE" 2>/dev/null; then
LAST_BACKUP="$BACKUP_FILE"
BK_SIZE=$(stat -c%s "$BACKUP_FILE" 2>/dev/null || echo 0)
if [ "$BK_SIZE" -lt 1024 ]; then rm -f "$BACKUP_FILE"; spinner_stop fail; die "Backup too small — aborting"; fi
spinner_stop ok
ok "Backup saved: $(numfmt --to=iec "$BK_SIZE")$(basename "$BACKUP_FILE")"
else
spinner_stop fail
die "Database backup failed"
fi
info "Checking for new SQL files..."
if [ -d "$SQL_DIR" ]; then
SQL_COUNT=0
while IFS= read -r -d '' sql_file; do
info_indent "Importing: $(basename "$sql_file")"
mariadb $MYSQL_CRED --force "$DB_NAME" < "$sql_file" 2>/dev/null || warn "Import failed for $(basename "$sql_file")"
SQL_COUNT=$((SQL_COUNT + 1))
done < <(find "$SQL_DIR" -name '*.sql' -mmin -10 -not -path "$BACKUP_DIR/*" -print0 2>/dev/null)
[ "$SQL_COUNT" -gt 0 ] && ok "$SQL_COUNT SQL file(s) imported" || info "No new SQL files"
fi
info "Building emulator..."
cd "$EMULATOR_DIR/Emulator"
spinner_start "Compiling Java..."
mvn package -q 2>&1 | tail -5 && spinner_stop ok || { spinner_stop fail; die "Maven build failed"; }
ok "Build complete"
JAR_FILE=$(find target -maxdepth 1 -name 'Habbo-*-jar-with-dependencies.jar' -printf '%T@ %p\n' 2>/dev/null | sort -rn | sed -n '1s/^[0-9.]* //p' | xargs basename 2>/dev/null || echo "")
[ -z "$JAR_FILE" ] && die "No jar file found"
ok "Jar: $JAR_FILE"
info "Updating launch script..."
cd target/
cat << EOF > emulator
#!/bin/sh
file_name_emulator=emulator.log
current_time=\$(date "+%H%M_%d-%m-%Y")
file_name=\$file_name_emulator.\$current_time
mv /var/log/emu/emulator.log /var/log/emu/\$file_name
java -Dfile.encoding=UTF8 -Xmx2G -jar $JAR_FILE
EOF
chmod +x emulator
ok "Launch script updated"
echo "$(date +%Y-%m-%d\ %H:%M:%S)|BACKUP|$BACKUP_FILE|$BK_SIZE" >> "$BACKUP_DIR/.history" 2>/dev/null || true
ROLLBACK_NEEDED=false
else
info "Already up to date, skipping"
fi
}
update_renderer() {
step "Update Nitro_Render_V3"
if git_update "$NITRO_RENDERER" "$NITRO_BRANCH"; then
info "New commits detected"
HAD_UPDATES=true
UPDATED_REPOS+=("Nitro_Render_V3")
spinner_start "Installing dependencies..."
yarn install --frozen-lockfile 2>&1 || { info "Retrying..."; clean_node_modules && yarn install 2>&1; } || { spinner_stop fail; die "yarn install failed"; }
spinner_stop ok
ok "Dependencies installed"
else
info "Already up to date, skipping"
fi
}
update_client() {
step "Update & Build Nitro-V3"
if git_update "$NITRO_CLIENT" "$NITRO_BRANCH"; then
info "New commits detected"
HAD_UPDATES=true
NITRO_BUILT=true
UPDATED_REPOS+=("Nitro-V3")
spinner_start "Installing dependencies..."
yarn install --frozen-lockfile 2>&1 || { info "Retrying..."; clean_node_modules && yarn install 2>&1; } || { spinner_stop fail; die "yarn install failed"; }
spinner_stop ok
ok "Dependencies installed"
info "Building Nitro-V3..."
spinner_start "Building frontend..."
yarn build 2>&1 | tail -3 && spinner_stop ok || { spinner_stop fail; die "yarn build failed"; }
ok "Build complete"
else
info "Already up to date, skipping build"
fi
mkdir -p "$NITRO_CLIENT/dist/custom-themes"
[ ! -f "$NITRO_CLIENT/dist/custom-themes/index.json" ] && echo '{"themes":[]}' > "$NITRO_CLIENT/dist/custom-themes/index.json"
}
sync_configs() {
step "Sync Configurations"
mkdir -p "$GAMEDATA_CONF_DIR"
local MERGE_SCRIPT="$SCRIPT_DIR/scripts/merge-config.cjs"
local NITRO_DIST_CONFIG_DIR="$NITRO_CLIENT/dist/configuration"
for dir in "$NITRO_SRC_DIR" "$GAMEDATA_CONF_DIR"; do
[ -d "$dir" ] && [ ! -w "$dir" ] && command -v sudo &>/dev/null && sudo chown -R "$(whoami)":"$(whoami)" "$dir" 2>/dev/null || true
done
local EXAMPLE_COUNT=0
for example_file in "$NITRO_SRC_DIR"/*.example; do
[ -f "$example_file" ] || continue
local base=$(basename "$example_file")
local target_name="${base%.example}"
case "$target_name" in *.*) ;; *) target_name="$target_name.json" ;; esac
node "$MERGE_SCRIPT" "$example_file" "$NITRO_SRC_DIR/$target_name" 2>/dev/null
node "$MERGE_SCRIPT" "$example_file" "$GAMEDATA_CONF_DIR/$target_name" 2>/dev/null
[ -d "$NITRO_DIST_CONFIG_DIR" ] && cp "$NITRO_SRC_DIR/$target_name" "$NITRO_DIST_CONFIG_DIR/$target_name" 2>/dev/null || true
EXAMPLE_COUNT=$((EXAMPLE_COUNT + 1))
done
ok "$EXAMPLE_COUNT .example file(s) processed"
info "Applying critical config URLs..."
FORCE_CONFIG_SCRIPT=$(cat << 'PYEOF'
import json, sys, os
paths = [
os.path.join(os.environ.get('NITRO_SRC_DIR', ''), 'renderer-config.json'),
os.path.join(os.environ.get('NITRO_DIST_CONFIG_DIR', ''), 'renderer-config.json'),
os.path.join(os.environ.get('NITRO_SRC_DIR', ''), 'ui-config.json'),
os.path.join(os.environ.get('NITRO_DIST_CONFIG_DIR', ''), 'ui-config.json'),
]
for path in paths:
if not os.path.exists(path): continue
with open(path, 'r') as f: data = json.load(f)
for key in ['image.library.url', 'hof.furni.url', 'gamedata.url', 'asset.url']:
env_val = os.environ.get(f'NITRO_{key.upper().replace(".", "_")}')
if env_val: data[key] = env_val
for key in ['api.url', 'socket.url', 'furni.asset.icon.url']:
if key in data:
env_val = os.environ.get(f'NITRO_{key.upper().replace(".", "_")}')
if env_val: data[key] = env_val
if 'gamedata.url' in data:
data['radio.url'] = data['gamedata.url'] + '/config/radio-stations.json5?t=%timestamp%'
data['soundboard.url'] = data['gamedata.url'] + '/config/soundboard-sounds.json5?t=%timestamp%'
if 'show.google.ads' in data: data['show.google.ads'] = False
with open(path, 'w') as f: json.dump(data, f, indent=(4 if 'dist' not in path else None), separators=(',', ':') if 'dist' in path else (',', ': '))
print(f' [OK] Fixed: {os.path.basename(path)}')
print(' [OK] All config URLs synced')
PYEOF
)
cd "$NITRO_CLIENT" && NITRO_SRC_DIR="$NITRO_SRC_DIR" NITRO_DIST_CONFIG_DIR="$NITRO_DIST_CONFIG_DIR" \
NITRO_IMAGE_LIBRARY_URL="$NITRO_IMAGE_LIBRARY_URL" NITRO_HOF_FURNITURE_URL="$NITRO_HOF_FURNITURE_URL" \
NITRO_API_URL="$NITRO_API_URL" NITRO_SOCKET_URL="$NITRO_SOCKET_URL" \
NITRO_GAMEDATA_URL="$NITRO_GAMEDATA_URL" NITRO_ASSET_URL="$NITRO_ASSET_URL" \
NITRO_FURNI_ASSET_ICON_URL="$NITRO_FURNI_ASSET_ICON_URL" \
python3 -c "$FORCE_CONFIG_SCRIPT"
}
cleanup_phase() {
step "Cleanup"
info "Removing old logs..."
local LOG_COUNT=$(find "$EMULATOR_DIR" -name "*.log" -mtime +14 2>/dev/null | wc -l)
find "$EMULATOR_DIR" -name "*.log" -mtime +14 -exec rm -f {} \; 2>/dev/null || true
ok "Removed $LOG_COUNT old log(s)"
info "Managing backups (keeping max 5)..."
if [ -d "$BACKUP_DIR" ]; then
find "$BACKUP_DIR" -maxdepth 1 -name '*.sql' -printf '%T@ %p\n' 2>/dev/null | sort -rn | tail -n +6 | sed 's/^[0-9.]* //' | xargs -r rm -f || true
fi
if [ "$HAD_UPDATES" = true ]; then
yarn cache clean 2>/dev/null || true
find "$EMULATOR_DIR/Emulator/target" -maxdepth 1 -name 'Habbo-*-jar-with-dependencies.jar' -printf '%T@ %p\n' 2>/dev/null | sort -rn | tail -n +2 | sed 's/^[0-9.]* //' | xargs -r rm -f || true
fi
rm -rf /tmp/nitro-* 2>/dev/null || true
ok "Cleanup complete"
}
set_permissions() {
step "Set Permissions"
if command -v sudo &>/dev/null; then
for dir in "$NITRO_CLIENT" "$NITRO_RENDERER" "$EMULATOR_DIR" "$GAMEDATA_CONF_DIR"; do
[ -d "$dir" ] && sudo chown -R www-data:www-data "$dir" 2>/dev/null || true
done
ok "Permissions set to www-data:www-data"
else
warn "sudo not available, skipping chown"
fi
chmod -R 775 "$LOG_DIR" 2>/dev/null || true
chmod -R 775 "$BACKUP_DIR" 2>/dev/null || true
}
restart_services() {
step "Restart Services"
if [ "$HAD_UPDATES" = true ]; then
if systemctl cat "$EMULATOR_SERVICE" &>/dev/null && command -v sudo &>/dev/null; then
info "Restarting $EMULATOR_SERVICE..."
spinner_start "Restarting emulator..."
sudo systemctl restart "$EMULATOR_SERVICE" 2>/dev/null && spinner_stop ok || { spinner_stop fail; die "Failed to restart $EMULATOR_SERVICE"; }
elif command -v pm2 &>/dev/null; then
info "Restarting PM2..."
pm2 restart all 2>/dev/null || warn "PM2 restart had issues"
else
warn "No systemd or PM2 found — restart manually"
fi
if command -v nginx &>/dev/null && command -v sudo &>/dev/null; then
sudo nginx -t 2>/dev/null && sudo systemctl reload nginx 2>/dev/null && ok "Nginx reloaded" || warn "Nginx reload failed"
fi
if command -v redis-cli &>/dev/null; then
redis-cli FLUSHALL 2>/dev/null && ok "Redis cache flushed" || warn "Redis flush failed"
fi
else
info "No updates applied, no restart needed"
fi
}
health_check() {
step "Health Check & Validation"
if [ "$NITRO_BUILT" = true ]; then
if [ -d "$NITRO_CLIENT/dist/assets" ]; then
local ASSET_COUNT=$(find "$NITRO_CLIENT/dist/assets" -type f 2>/dev/null | wc -l)
local ASSET_SIZE=$(du -sh "$NITRO_CLIENT/dist/assets" 2>/dev/null | cut -f1 || echo "?")
ok "Nitro-V3 built: $ASSET_COUNT assets ($ASSET_SIZE)"
else
fail "Nitro-V3 build assets missing!"
ERRORS=$((ERRORS + 1))
fi
fi
if [ "$HAD_UPDATES" = true ] && systemctl cat "$EMULATOR_SERVICE" &>/dev/null; then
info "Waiting for $EMULATOR_SERVICE..."
HEALTHY=false
for i in $(seq 1 "$HEALTH_RETRIES"); do
sleep "$HEALTH_INTERVAL"
STATUS=$(systemctl is-active "$EMULATOR_SERVICE" 2>/dev/null || echo "unknown")
[ "$STATUS" = "active" ] && { HEALTHY=true; break; }
done
[ "$HEALTHY" = true ] && ok "$EMULATOR_SERVICE is active" || { fail "$EMULATOR_SERVICE did not start"; ERRORS=$((ERRORS + 1)); }
fi
for dir in "$NITRO_CLIENT" "$NITRO_RENDERER" "$EMULATOR_DIR" "$GAMEDATA_CONF_DIR"; do
if [ -d "$dir" ]; then
OWNER=$(stat -c '%U:%G' "$dir" 2>/dev/null || echo "unknown")
[ "$OWNER" = "www-data:www-data" ] || [ "$(id -u)" -ne 0 ] && debug "$(basename "$dir"): $OWNER" || sudo chown -R www-data:www-data "$dir" 2>/dev/null || true
fi
done
if curl -s -o /dev/null -w "%{http_code}" --max-time 10 "${NITRO_SITE_URL:-}" 2>/dev/null | grep -qE '^(200|301|302|303|307|308)$'; then
ok "Site responds: ${NITRO_SITE_URL:-}"
else
warn "Site did not respond: ${NITRO_SITE_URL:-}"
fi
}
show_summary() {
ELAPSED=$(($(date +%s) - START_TIME))
ELAPSED_FMT=$(printf '%dm %ds' $((ELAPSED / 60)) $((ELAPSED % 60)))
echo ""
echo -e " ${C_CYAN}╔══════════════════════════════════════════════════════════════════╗${C_RESET}"
if [ "$ERRORS" -eq 0 ]; then
echo -e " ${C_CYAN}${C_RESET} ${C_GREEN}${C_BOLD}✔ UPDATE SUCCESSFULLY COMPLETED${C_RESET} ${C_CYAN}${C_RESET}"
else
echo -e " ${C_CYAN}${C_RESET} ${C_YELLOW}${C_BOLD}⚠ COMPLETED WITH $ERRORS ERROR(S)${C_RESET} ${C_CYAN}${C_RESET}"
fi
echo -e " ${C_CYAN}╠══════════════════════════════════════════════════════════════════╣${C_RESET}"
printf " ${C_CYAN}${C_RESET} Duration: %-45s${C_CYAN}${C_RESET}\n" "$ELAPSED_FMT"
printf " ${C_CYAN}${C_RESET} Updates: %-45s${C_CYAN}${C_RESET}\n" "$([ "$HAD_UPDATES" = true ] && echo "Yes" || echo "No")"
printf " ${C_CYAN}${C_RESET} Nitro build: %-45s${C_CYAN}${C_RESET}\n" "$([ "$NITRO_BUILT" = true ] && echo "Yes" || echo "No")"
printf " ${C_CYAN}${C_RESET} Warnings: %-45s${C_CYAN}${C_RESET}\n" "$WARNINGS"
printf " ${C_CYAN}${C_RESET} Errors: %-45s${C_CYAN}${C_RESET}\n" "$ERRORS"
[ ${#UPDATED_REPOS[@]} -gt 0 ] && printf " ${C_CYAN}${C_RESET} Updated: %-45s${C_CYAN}${C_RESET}\n" "${UPDATED_REPOS[*]}"
printf " ${C_CYAN}${C_RESET} Disk free: %-45s${C_CYAN}${C_RESET}\n" "${AVAIL_GB:-?}GB"
printf " ${C_CYAN}${C_RESET} Log: %-45s${C_CYAN}${C_RESET}\n" "$(basename "${LOG_FILE:-?}")"
echo -e " ${C_CYAN}╚══════════════════════════════════════════════════════════════════╝${C_RESET}"
echo ""
notify "SUCCESS" "Completed in $ELAPSED_FMT (${ERRORS} errors, ${WARNINGS} warnings)"
}
# =============================================================================
# COMMANDS
# =============================================================================
cmd_update() {
preflight
preflight_checks
if [ "$DRY_RUN" = true ]; then dry_run_display; return 0; fi
echo ""
info "Site: ${NITRO_SITE_URL:-?} | Branch: ${NITRO_BRANCH:-main} | Mode: ${SELECTIVE_UPDATE:-full}"
info "Log: $LOG_FILE"
case "${SELECTIVE_UPDATE:-full}" in
emulator) update_emulator ;;
renderer) update_renderer ;;
client) update_client ;;
configs) sync_configs ;;
*) update_emulator; update_renderer; update_client; sync_configs; cleanup_phase; set_permissions; restart_services; health_check ;;
esac
show_summary
}
cmd_backup() {
preflight
step "Database Backup"
mkdir -p "$BACKUP_DIR"
BACKUP_FILE="$BACKUP_DIR/manual_$(date +%Y%m%d_%H%M%S).sql"
spinner_start "Backing up database..."
if mariadb-dump $MYSQL_CRED --force --skip-lock-tables --routines --events --triggers "$DB_NAME" > "$BACKUP_FILE" 2>/dev/null; then
BK_SIZE=$(stat -c%s "$BACKUP_FILE" 2>/dev/null || echo 0)
spinner_stop ok
ok "Backup saved: $(numfmt --to=iec "$BK_SIZE")$(basename "$BACKUP_FILE")"
echo "$(date +%Y-%m-%d\ %H:%M:%S)|MANUAL_BACKUP|$BACKUP_FILE|$BK_SIZE" >> "$BACKUP_DIR/.history" 2>/dev/null || true
else
spinner_stop fail
die "Backup failed"
fi
}
cmd_restore() {
preflight
step "Database Restore"
[ ! -d "$BACKUP_DIR" ] && die "No backup directory found"
local backups=()
while IFS= read -r line; do backups+=("$line"); done < <(find "$BACKUP_DIR" -maxdepth 1 -name '*.sql' -printf '%T@ %p\n' 2>/dev/null | sort -rn | head -20 | sed 's/^[0-9.]* //')
[ ${#backups[@]} -eq 0 ] && die "No backups found"
echo ""
echo -e " ${C_BOLD}${C_CYAN}Available Backups:${C_RESET}"
echo ""
local i=1
for bk in "${backups[@]}"; do
local name=$(basename "$bk")
local size=$(numfmt --to=iec "$(stat -c%s "$bk" 2>/dev/null || echo 0)" 2>/dev/null || echo "?")
printf " ${C_GREEN}${C_BOLD}%2d)${C_RESET} %-40s ${C_DIM}%s${C_RESET}\n" "$i" "$name" "$size"
i=$((i + 1))
done
echo ""
echo -en " Select [1-$((i-1))]: "
local choice; read -r choice
[[ "$choice" =~ ^[0-9]+$ ]] && [ "$choice" -ge 1 ] && [ "$choice" -le ${#backups[@]} ] || die "Invalid selection"
local selected="${backups[$((choice-1))]}"
if ! confirm "Restore from $(basename "$selected")? This OVERWRITES the database!"; then info "Cancelled."; return 0; fi
spinner_start "Restoring..."
mariadb $MYSQL_CRED "$DB_NAME" < "$selected" 2>/dev/null && spinner_stop ok && ok "Restored from $(basename "$selected")" || { spinner_stop fail; die "Restore failed"; }
}
cmd_backups() {
step "Backup Inventory"
[ ! -d "$BACKUP_DIR" ] && { warn "No backup directory"; return 0; }
echo ""
printf " ${C_DIM}%-4s %-38s %-10s %-6s${C_RESET}\n" "#" "FILENAME" "SIZE" "AGE"
printf " ${C_DIM}%s${C_RESET}\n" "$(printf '%0.s─' $(seq 1 62))"
local total=0 total_size=0
while IFS= read -r line; do
total=$((total + 1))
local name=$(basename "$line")
local size=$(stat -c%s "$line" 2>/dev/null || echo 0)
total_size=$((total_size + size))
local size_fmt=$(numfmt --to=iec "$size" 2>/dev/null || echo "?")
local age_days=$(( ($(date +%s) - $(stat -c%Y "$line" 2>/dev/null || echo 0)) / 86400 ))
local age_fmt="${age_days}d"; [ "$age_days" -eq 0 ] && age_fmt="today"; [ "$age_days" -eq 1 ] && age_fmt="1d"
printf " ${C_GREEN}%2d${C_RESET} %-38s ${C_CYAN}%-10s${C_RESET} ${C_DIM}%-6s${C_RESET}\n" "$total" "$name" "$size_fmt" "$age_fmt"
done < <(find "$BACKUP_DIR" -maxdepth 1 -name '*.sql' -printf '%T@ %p\n' 2>/dev/null | sort -rn | head -20 | sed 's/^[0-9.]* //')
printf " ${C_DIM}%s${C_RESET}\n" "$(printf '%0.s─' $(seq 1 62))"
echo -e " ${C_BOLD}Total: $total backup(s) — $(numfmt --to=iec "$total_size" 2>/dev/null || echo "?")${C_RESET}"
echo ""
}
cmd_status() {
step "System Status Dashboard"
echo ""
box_top "SERVER" 56
box_kv "${E_SERVER} Hostname:" "$(hostname)" 56
box_kv "${E_GLOBE} OS:" "$(cat /etc/os-release 2>/dev/null | grep PRETTY_NAME | cut -d= -f2 | tr -d '"' || uname -s)" 56
box_kv "${E_GEAR} Kernel:" "$(uname -r)" 56
box_kv "${E_CLOCK} Uptime:" "$(uptime -p 2>/dev/null || uptime)" 56
box_bottom 56
echo ""
box_top "RESOURCES" 56
local mem_info=$(free -m 2>/dev/null | awk '/^Mem:/{printf "%dMB / %dMB (%.1f%%)", $3, $2, $3*100/$2}')
box_kv "${E_LIGHTNING} CPU:" "$(awk '{printf "%.1f", $1}' /proc/loadavg 2>/dev/null || echo "?") / $(nproc 2>/dev/null || echo '?') cores" 56
box_kv "${E_CHART} Memory:" "$mem_info" 56
box_kv "${E_FOLDER} Disk:" "$(df -h "$SCRIPT_DIR" 2>/dev/null | tail -1 | awk '{printf "%s / %s (%s)", $3, $2, $5}')" 56
box_kv "${E_DB} Database:" "${DB_SIZE:-?}MB / ${DB_TABLES:-?} tables" 56
box_bottom 56
echo ""
box_top "SERVICES" 56
for svc in nginx mariadb redis-server "$EMULATOR_SERVICE" php-fpm cron; do
local st="${C_DIM}N/A${C_RESET}"
systemctl is-active "$svc" &>/dev/null && st="${C_GREEN}ACTIVE${C_RESET}" || systemctl is-enabled "$svc" &>/dev/null 2>&1 && st="${C_YELLOW}INACTIVE${C_RESET}"
printf " ${C_CYAN}${C_RESET} %-20s %b${C_CYAN}${C_RESET}\n" "$svc" "$st"
done
box_bottom 56
echo ""
box_top "GIT REPOSITORIES" 56
for repo_entry in "Emulator:$EMULATOR_DIR/Emulator" "Nitro-V3:$NITRO_CLIENT" "Nitro_Render_V3:$NITRO_RENDERER"; do
local name="${repo_entry%%:*}" path="${repo_entry##*:}"
[ -d "$path/.git" ] || continue
local branch=$(cd "$path" && git branch --show-current 2>/dev/null || echo "?")
local commit=$(cd "$path" && git log --oneline -1 2>/dev/null | cut -c1-30 || echo "?")
local dirty=$(cd "$path" && git status --porcelain 2>/dev/null | wc -l)
local dirty_str=""; [ "$dirty" -gt 0 ] && dirty_str=" ${C_YELLOW}($dirty modified)${C_RESET}"
printf " ${C_CYAN}${C_RESET} %-14s ${C_CYAN}%-8s${C_RESET} %s%b\n" "$name" "$branch" "$commit" "$dirty_str"
done
box_bottom 56
echo ""
box_top "LOGS" 56
box_kv "${E_EYE} Total:" "$(find "$LOG_DIR" -name 'update-*.log' 2>/dev/null | wc -l) files" 56
box_kv "${E_FOLDER} Current:" "$(basename "${LOG_FILE:-?}")" 56
box_bottom 56
echo ""
}
cmd_health() {
step "System Health Check"
local total=0 passed=0 failed=0 warn_c=0
check() { total=$((total + 1)); "$@" &>/dev/null && { passed=$((passed + 1)); ok "$1"; } || { failed=$((failed + 1)); fail "$1"; }; }
check_w() { total=$((total + 1)); "$@" &>/dev/null && { passed=$((passed + 1)); ok "$1"; } || { warn_c=$((warn_c + 1)); warn "$1"; }; }
echo ""
echo -e " ${C_BOLD}System:${C_RESET}"
check_w "Git installed" command -v git
check_w "Node.js installed" command -v node
check_w "Yarn installed" command -v yarn
check_w "Python3 installed" command -v python3
check_w "Java installed" command -v java
check_w "Maven installed" command -v mvn
echo ""
echo -e " ${C_BOLD}Directories:${C_RESET}"
check_w "Emulator dir" test -d "$EMULATOR_DIR/Emulator"
check_w "Nitro-V3 dir" test -d "$NITRO_CLIENT"
check_w "Renderer dir" test -d "$NITRO_RENDERER"
echo ""
echo -e " ${C_BOLD}Services:${C_RESET}"
check_w "MariaDB running" systemctl is-active mariadb
check_w "Nginx running" systemctl is-active nginx
check_w "Redis running" systemctl is-active redis-server
echo ""
echo -e " ${C_BOLD}Application:${C_RESET}"
check_w "artisan exists" test -f "$SCRIPT_DIR/artisan"
check_w ".env exists" test -f "$SCRIPT_DIR/.env"
check_w "Site reachable" timeout 5 curl -sf "${NITRO_SITE_URL:-}" 2>/dev/null
echo ""
echo -e " ${C_BOLD}Resources:${C_RESET}"
local avail_kb=$(df --output=avail "$EMULATOR_DIR" 2>/dev/null | tail -1)
local avail_gb=$((avail_kb / 1024 / 1024))
total=$((total + 1))
[ "$avail_gb" -ge "$MIN_DISK_GB" ] && { passed=$((passed + 1)); ok "Disk: ${avail_gb}GB free"; } || { failed=$((failed + 1)); fail "Disk: ${avail_gb}GB (min ${MIN_DISK_GB}GB)"; }
echo ""
local score=$((passed * 100 / total))
box_top "HEALTH SCORE" 50
box_kv "Total:" "$total" 50
box_kv "${C_GREEN}Passed:" "$passed" 50
box_kv "${C_RED}Failed:" "$failed" 50
box_kv "${C_YELLOW}Warnings:" "$warn_c" 50
if [ "$score" -ge 90 ]; then
box_line "${C_GREEN}${C_BOLD}Score: ${score}% — EXCELLENT${C_RESET}" 50
elif [ "$score" -ge 70 ]; then
box_line "${C_YELLOW}${C_BOLD}Score: ${score}% — GOOD${C_RESET}" 50
else
box_line "${C_RED}${C_BOLD}Score: ${score}% — NEEDS ATTENTION${C_RESET}" 50
fi
box_bottom 50
echo ""
}
cmd_services() {
step "Service Manager"
select_menu "Service Control" "Start Emulator" "Stop Emulator" "Restart Emulator" "Status Emulator" "Start Nginx" "Restart Nginx" "Status All" "Emulator Logs (tail)" "Return"
case "${REPLY:-}" in
1) sudo systemctl start "$EMULATOR_SERVICE" && ok "$EMULATOR_SERVICE started" || fail "Failed" ;;
2) sudo systemctl stop "$EMULATOR_SERVICE" && ok "$EMULATOR_SERVICE stopped" || fail "Failed" ;;
3) sudo systemctl restart "$EMULATOR_SERVICE" && ok "$EMULATOR_SERVICE restarted" || fail "Failed" ;;
4) systemctl status "$EMULATOR_SERVICE" --no-pager ;;
5) sudo systemctl start nginx && ok "Nginx started" || fail "Failed" ;;
6) sudo systemctl restart nginx && ok "Nginx restarted" || fail "Failed" ;;
7) for svc in nginx mariadb redis-server "$EMULATOR_SERVICE" php-fpm cron; do local st="N/A"; systemctl is-active "$svc" &>/dev/null && st="${C_GREEN}ACTIVE${C_RESET}" || st="${C_RED}INACTIVE${C_RESET}"; printf " %-25s %b\n" "$svc" "$st"; done ;;
8) sudo journalctl -u "$EMULATOR_SERVICE" --no-pager -n 50 ;;
esac
}
cmd_database() {
step "Database Tools"
local choice
choice=$(select_menu "Database Management" "Show DB Info" "List Tables" "Table Sizes" "Optimize Tables" "Check Tables" "Custom Query" "Active Users" "Return")
case "$choice" in
1) box_top "DATABASE INFO" 50; box_kv "Name:" "$DB_NAME" 50; box_kv "Host:" "$DB_HOST:$DB_PORT" 50; box_kv "User:" "$DB_USER" 50
box_kv "Size:" "${DB_SIZE:-?}MB" 50; box_kv "Tables:" "${DB_TABLES:-?}" 50; box_bottom 50 ;;
2) mariadb $MYSQL_CRED -e "SHOW TABLES FROM $DB_NAME" 2>/dev/null ;;
3) mariadb $MYSQL_CRED -e "SELECT table_name AS 'Table', ROUND(data_length/1024/1024,2) AS 'Data MB', ROUND(index_length/1024/1024,2) AS 'Index MB', ROUND((data_length+index_length)/1024/1024,2) AS 'Total MB' FROM information_schema.tables WHERE table_schema='$DB_NAME' ORDER BY (data_length+index_length) DESC" 2>/dev/null ;;
4) spinner_start "Optimizing..."
local count=0
for tbl in $(mariadb $MYSQL_CRED -N -e "SELECT table_name FROM information_schema.tables WHERE table_schema='$DB_NAME' AND engine='InnoDB'" "$DB_NAME" 2>/dev/null); do
mariadb $MYSQL_CRED -e "OPTIMIZE TABLE $DB_NAME.\`$tbl\`" "$DB_NAME" &>/dev/null || true
count=$((count + 1))
done
spinner_stop ok; ok "Optimized $count tables" ;;
5) mariadb $MYSQL_CRED -e "CHECK TABLE $DB_NAME.*" "$DB_NAME" 2>/dev/null ;;
6) echo -en " SQL: "; local q; read -r q; [ -n "$q" ] && mariadb $MYSQL_CRED "$DB_NAME" -e "$q" 2>/dev/null ;;
7) mariadb $MYSQL_CRED -e "SELECT name, lookAt, motto FROM $DB_NAME.users WHERE online='1'" 2>/dev/null || warn "Could not query users" ;;
esac
}
cmd_config() {
step "Configuration"
echo ""
box_top "CURRENT CONFIGURATION" 56
box_kv "${E_CLOUD} Site URL:" "${NITRO_SITE_URL:-?}" 56
box_kv "${E_GIT} Branch:" "${NITRO_BRANCH:-main}" 56
box_kv "${E_FOLDER} Emulator:" "$EMULATOR_DIR" 56
box_kv "${E_FOLDER} Nitro-V3:" "$NITRO_CLIENT" 56
box_kv "${E_FOLDER} Renderer:" "$NITRO_RENDERER" 56
box_kv "${E_DB} Database:" "$DB_NAME @ $DB_HOST:$DB_PORT" 56
box_kv "${E_GEAR} Service:" "$EMULATOR_SERVICE" 56
box_kv "${E_CLOUD} Socket:" "${NITRO_SOCKET_URL:-?}" 56
box_kv "${E_CLOUD} API:" "${NITRO_API_URL:-?}" 56
box_kv "${E_CLOUD} Gamedata:" "${NITRO_GAMEDATA_URL:-?}" 56
box_kv "${E_LOCK} Min Disk:" "${MIN_DISK_GB}GB" 56
box_bottom 56
echo ""
}
cmd_logs() {
step "Log Viewer"
[ ! -d "$LOG_DIR" ] && { warn "No log directory"; return 0; }
local logs=()
while IFS= read -r line; do logs+=("$line"); done < <(find "$LOG_DIR" -name 'update-*.log' -printf '%T@ %p\n' 2>/dev/null | sort -rn | head -20 | sed 's/^[0-9.]* //')
[ ${#logs[@]} -eq 0 ] && { warn "No logs found"; return 0; }
echo ""
echo -e " ${C_BOLD}${C_CYAN}Recent Logs:${C_RESET}"
echo ""
local i=1
for log in "${logs[@]}"; do
local name=$(basename "$log")
local size=$(numfmt --to=iec "$(stat -c%s "$log" 2>/dev/null || echo 0)" 2>/dev/null || echo "?")
local lines=$(wc -l < "$log" 2>/dev/null || echo "?")
printf " ${C_GREEN}${C_BOLD}%2d)${C_RESET} %-35s ${C_DIM}%s (%s lines)${C_RESET}\n" "$i" "$name" "$size" "$lines"
i=$((i + 1))
done
echo ""
echo -en " Select [1-$((i-1))]: "
local choice; read -r choice
if [[ "$choice" =~ ^[0-9]+$ ]] && [ "$choice" -ge 1 ] && [ "$choice" -le ${#logs[@]} ]; then
echo ""
echo -e " ${C_BOLD}--- $(basename "${logs[$((choice-1))]}") ---${C_RESET}"
echo ""
less -R "${logs[$((choice-1))]}" 2>/dev/null || cat "${logs[$((choice-1))]}"
fi
}
cmd_clean() {
step "Cache Cleanup"
local choice
choice=$(select_menu "Cleanup" "Yarn Cache" "Laravel Cache" "Redis Cache" "Old Logs (>14d)" "Old Backups (keep 5)" "Temp Files" "Everything" "Return")
case "$choice" in
1) spinner_start "Cleaning..."; yarn cache clean 2>/dev/null; spinner_stop ok ;;
2) cd "$SCRIPT_DIR" && php artisan cache:clear 2>/dev/null && php artisan config:clear 2>/dev/null && php artisan route:clear 2>/dev/null && php artisan view:clear 2>/dev/null && ok "Laravel cache cleared" ;;
3) redis-cli FLUSHALL 2>/dev/null && ok "Redis flushed" || warn "Redis not available" ;;
4) local c=$(find "$EMULATOR_DIR" -name "*.log" -mtime +14 2>/dev/null | wc -l); find "$EMULATOR_DIR" -name "*.log" -mtime +14 -exec rm -f {} \; 2>/dev/null; ok "Removed $c logs" ;;
5) local b=$(find "$BACKUP_DIR" -name '*.sql' 2>/dev/null | wc -l); find "$BACKUP_DIR" -maxdepth 1 -name '*.sql' -printf '%T@ %p\n' 2>/dev/null | sort -rn | tail -n +6 | sed 's/^[0-9.]* //' | xargs -r rm -f; ok "Backups: $b -> $(find "$BACKUP_DIR" -name '*.sql' 2>/dev/null | wc -l)" ;;
6) rm -rf /tmp/nitro-* 2>/dev/null; ok "Temp cleaned" ;;
7) yarn cache clean 2>/dev/null; find "$EMULATOR_DIR" -name "*.log" -mtime +14 -exec rm -f {} \; 2>/dev/null; find "$BACKUP_DIR" -maxdepth 1 -name '*.sql' -printf '%T@ %p\n' 2>/dev/null | sort -rn | tail -n +6 | sed 's/^[0-9.]* //' | xargs -r rm -f; rm -rf /tmp/nitro-* 2>/dev/null; ok "Full cleanup done" ;;
esac
}
# =============================================================================
# NEW: GIT LOG VIEWER
# =============================================================================
cmd_gitlog() {
step "Git Commit History"
echo ""
echo -e " ${C_BOLD}Select repository:${C_RESET}"
echo ""
echo -e " ${C_GREEN}1)${C_RESET} Emulator"
echo -e " ${C_GREEN}2)${C_RESET} Nitro-V3"
echo -e " ${C_GREEN}3)${C_RESET} Nitro_Render_V3"
echo ""
echo -en " Select [1-3]: "
local rc; read -r rc
local repo=""
case "$rc" in
1) repo="$EMULATOR_DIR/Emulator" ;;
2) repo="$NITRO_CLIENT" ;;
3) repo="$NITRO_RENDERER" ;;
*) warn "Invalid"; return ;;
esac
[ ! -d "$repo/.git" ] && { warn "Not a git repo"; return; }
echo ""
echo -e " ${C_BOLD}Last 20 commits on ${C_CYAN}$(cd "$repo" && git branch --show-current 2>/dev/null)${C_RESET}:"
echo ""
printf " ${C_DIM}%-12s %-8s %-50s${C_RESET}\n" "DATE" "HASH" "MESSAGE"
printf " ${C_DIM}%s${C_RESET}\n" "$(printf '%0.s─' $(seq 1 72))"
cd "$repo" && git log --pretty=format:" ${C_CYAN}%h${C_RESET} %C(yellow)%cr${C_RESET} %s" -20 2>/dev/null
echo ""
echo ""
}
# =============================================================================
# NEW: BRANCH COMPARE
# =============================================================================
cmd_compare() {
step "Branch Comparison"
echo ""
local BRANCHES=()
while IFS= read -r b; do [ -n "$b" ] && BRANCHES+=("$b"); done < <(detect_available_branches)
[ ${#BRANCHES[@]} -lt 2 ] && { warn "Need at least 2 branches to compare (found: ${#BRANCHES[@]})"; return 0; }
echo -e " ${C_BOLD}Available branches:${C_RESET}"
echo ""
local i=1
for b in "${BRANCHES[@]}"; do
echo -e " ${C_GREEN}${C_BOLD}$i)${C_RESET} ${C_CYAN}$b${C_RESET}"
i=$((i + 1))
done
echo ""
echo -en " Branch A [1-$((i-1))]: "
local ba; read -r ba
echo -en " Branch B [1-$((i-1))]: "
local bb; read -r bb
[[ "$ba" =~ ^[0-9]+$ ]] && [[ "$bb" =~ ^[0-9]+$ ]] && [ "$ba" -ge 1 ] && [ "$bb" -ge 1 ] && [ "$ba" -le ${#BRANCHES[@]} ] && [ "$bb" -le ${#BRANCHES[@]} ] || { warn "Invalid selection"; return 0; }
local branchA="${BRANCHES[$((ba-1))]}"
local branchB="${BRANCHES[$((bb-1))]}"
echo ""
echo -e " ${C_BOLD}Comparing ${C_CYAN}$branchA${C_RESET} vs ${C_CYAN}$branchB${C_RESET}"
echo ""
for repo in "$EMULATOR_DIR/Emulator" "$NITRO_CLIENT" "$NITRO_RENDERER"; do
[ -d "$repo/.git" ] || continue
local name=$(basename "$repo")
echo -e " ${C_BOLD}${C_CYAN}$name:${C_RESET}"
local ahead=0 behind=0
cd "$repo" 2>/dev/null || continue
if git rev-parse --verify "$branchA" &>/dev/null && git rev-parse --verify "$branchB" &>/dev/null; then
ahead=$(git rev-list --count "$branchB".."$branchA" 2>/dev/null || echo "0")
behind=$(git rev-list --count "$branchA".."$branchB" 2>/dev/null || echo "0")
local diff_stat=$(git diff --stat "$branchA".."$branchB" 2>/dev/null | tail -1 || echo "")
echo -e " $branchA is ${C_GREEN}$ahead${C_RESET} ahead, ${C_YELLOW}$behind${C_RESET} behind $branchB"
[ -n "$diff_stat" ] && echo -e " ${C_DIM}$diff_stat${C_RESET}"
else
echo -e " ${C_YELLOW}One or both branches not found locally${C_RESET}"
fi
echo ""
done
}
# =============================================================================
# NEW: UPDATE HISTORY
# =============================================================================
cmd_history() {
step "Update History"
echo ""
local hist_file="$BACKUP_DIR/.history"
if [ ! -f "$hist_file" ]; then
warn "No update history found"
return 0
fi
echo ""
printf " ${C_DIM}%-20s %-20s %-30s %-10s${C_RESET}\n" "DATE" "TYPE" "FILE" "SIZE"
printf " ${C_DIM}%s${C_RESET}\n" "$(printf '%0.s─' $(seq 1 82))"
local count=0
while IFS='|' read -r date type file size; do
count=$((count + 1))
local size_fmt=$(numfmt --to=iec "$size" 2>/dev/null || echo "?")
printf " %-20s ${C_CYAN}%-20s${C_RESET} %-30s ${C_GREEN}%-10s${C_RESET}\n" "$date" "$type" "$(basename "${file:-?}")" "$size_fmt"
[ "$count" -ge 20 ] && break
done < "$hist_file"
[ "$count" -eq 0 ] && echo -e " ${C_DIM}No history entries found${C_RESET}"
echo ""
echo -e " ${C_DIM}Showing last 20 entries${C_RESET}"
echo ""
}
# =============================================================================
# NEW: CRON SCHEDULER
# =============================================================================
cmd_schedule() {
step "Scheduled Updates (Cron)"
echo ""
local current_cron=$(crontab -l 2>/dev/null | grep -F "$SCRIPT_NAME" || echo "")
echo -e " ${C_BOLD}Current schedule:${C_RESET}"
if [ -n "$current_cron" ]; then
echo -e " ${C_GREEN}$current_cron${C_RESET}"
else
echo -e " ${C_DIM}No scheduled updates${C_RESET}"
fi
echo ""
echo -e " ${C_BOLD}Options:${C_RESET}"
echo -e " ${C_GREEN}1)${C_RESET} Schedule daily at 03:00"
echo -e " ${C_GREEN}2)${C_RESET} Schedule daily at 04:00"
echo -e " ${C_GREEN}3)${C_RESET} Schedule weekly (Sunday 03:00)"
echo -e " ${C_GREEN}4)${C_RESET} Schedule custom time"
echo -e " ${C_GREEN}5)${C_RESET} Remove schedule"
echo -e " ${C_GREEN}6)${C_RESET} Run scheduled update now (test)"
echo ""
echo -en " Select [1-6]: "
local sc; read -r sc
case "$sc" in
1) (crontab -l 2>/dev/null | grep -vF "$SCRIPT_NAME"; echo "0 3 * * * cd $SCRIPT_DIR && ./$SCRIPT_NAME update --branch ${NITRO_BRANCH:-main} >> /var/log/nitro-cron.log 2>&1") | crontab - && ok "Scheduled daily at 03:00" ;;
2) (crontab -l 2>/dev/null | grep -vF "$SCRIPT_NAME"; echo "0 4 * * * cd $SCRIPT_DIR && ./$SCRIPT_NAME update --branch ${NITRO_BRANCH:-main} >> /var/log/nitro-cron.log 2>&1") | crontab - && ok "Scheduled daily at 04:00" ;;
3) (crontab -l 2>/dev/null | grep -vF "$SCRIPT_NAME"; echo "0 3 * * 0 cd $SCRIPT_DIR && ./$SCRIPT_NAME update --branch ${NITRO_BRANCH:-main} >> /var/log/nitro-cron.log 2>&1") | crontab - && ok "Scheduled weekly (Sunday 03:00)" ;;
4) echo -en " Minute [0-59]: "; local m; read -r m
echo -en " Hour [0-23]: "; local h; read -r h
echo -en " Day [1-31]: "; local d; read -r d
echo -en " Month [1-12]: "; local mo; read -r mo
(crontab -l 2>/dev/null | grep -vF "$SCRIPT_NAME"; echo "$m $h $d $mo * cd $SCRIPT_DIR && ./$SCRIPT_NAME update --branch ${NITRO_BRANCH:-main} >> /var/log/nitro-cron.log 2>&1") | crontab - && ok "Scheduled custom: $m $h $d $mo *" ;;
5) crontab -l 2>/dev/null | grep -vF "$SCRIPT_NAME" | crontab - && ok "Schedule removed" ;;
6) info "Running test update..."; cd "$SCRIPT_DIR" && ./"$SCRIPT_NAME" update --branch "${NITRO_BRANCH:-main}" ;;
esac
}
# =============================================================================
# MAIN INTERACTIVE MENU
# =============================================================================
pick_branch_and_update() {
local mode="${SELECTIVE_UPDATE:-full}"
local mode_label="Full"
case "$mode" in
emulator) mode_label="Emulator Only" ;;
client) mode_label="Nitro-V3 Client Only" ;;
renderer) mode_label="Renderer Only" ;;
configs) mode_label="Configs Only" ;;
esac
local BRANCHES=()
while IFS= read -r b; do [ -n "$b" ] && BRANCHES+=("$b"); done < <(detect_available_branches)
echo ""
echo -e " ${C_BOLD}${C_CYAN}╔═══════════════════════════════════════════════════════╗${C_RESET}"
echo -e " ${C_CYAN}${C_RESET} ${C_BOLD}Update: ${C_GREEN}$mode_label${C_RESET} ${C_CYAN}${C_RESET}"
echo -e " ${C_CYAN}╚═══════════════════════════════════════════════════════╝${C_RESET}"
echo ""
echo -e " ${C_BOLD}Select branch:${C_RESET}"
echo ""
local i=1
for b in "${BRANCHES[@]}"; do
local marker=""; [ "$b" = "${NITRO_BRANCH:-main}" ] && marker=" ${C_GREEN}(current)${C_RESET}"
echo -e " ${C_GREEN}${C_BOLD}$i)${C_RESET} ${C_CYAN}$b${C_RESET}$marker"
i=$((i + 1))
done
echo -e " ${C_GREEN}${C_BOLD}$i)${C_RESET} Custom branch"
echo ""
echo -en " Select [1-$i]: "
local bc; read -r bc
if [[ "$bc" =~ ^[0-9]+$ ]] && [ "$bc" -ge 1 ] && [ "$bc" -lt "$i" ]; then
NITRO_BRANCH="${BRANCHES[$((bc-1))]}"
elif [ "$bc" = "$i" ]; then
echo -en " Branch name: "; read -r NITRO_BRANCH
[ -z "$NITRO_BRANCH" ] && NITRO_BRANCH="main"
else
warn "Invalid, using: ${NITRO_BRANCH:-main}"
fi
echo ""
echo -e " ${C_BOLD}Update Summary:${C_RESET}"
echo -e " ${E_GIT} Branch: ${C_CYAN}${C_BOLD}${NITRO_BRANCH}${C_RESET}"
echo -e " ${E_GEAR} Mode: ${C_GREEN}${C_BOLD}${mode_label}${C_RESET}"
echo -e " ${E_CLOUD} Site: ${C_CYAN}${NITRO_SITE_URL:-?}${C_RESET}"
echo ""
if ! confirm "Start update?"; then info "Cancelled."; return 0; fi
cmd_update
}
cmd_interactive() {
while true; do
show_banner
local AVAILABLE_BRANCHES=()
while IFS= read -r b; do [ -n "$b" ] && AVAILABLE_BRANCHES+=("$b"); done < <(detect_available_branches)
local branch_display="${C_CYAN}${C_BOLD}${NITRO_BRANCH:-main}${C_RESET}"
local branch_count="${#AVAILABLE_BRANCHES[@]}"
echo -e " ${C_CYAN}╔══════════════════════════════════════════════════════════════════╗${C_RESET}"
echo -e " ${C_CYAN}${C_RESET} ${C_BOLD}${E_ROCKET} NITRO V3 CONTROL PANEL — $(date '+%H:%M')${C_RESET} ${C_CYAN}${C_RESET}"
echo -e " ${C_CYAN}╠══════════════════════════════════════════════════════════════════╣${C_RESET}"
echo -e " ${C_CYAN}${C_RESET} ${C_CYAN}${C_RESET}"
echo -e " ${C_CYAN}${C_RESET} ${C_BOLD}Settings:${C_RESET} ${C_CYAN}${C_RESET}"
echo -e " ${C_CYAN}${C_RESET} ${E_GIT} Branch: ${branch_display} ${C_DIM}(${branch_count} detected)${C_RESET} ${C_CYAN}${C_RESET}"
echo -e " ${C_CYAN}${C_RESET} ${E_CLOUD} Site: ${C_CYAN}${NITRO_SITE_URL:-?}${C_RESET} ${C_CYAN}${C_RESET}"
echo -e " ${C_CYAN}${C_RESET} ${C_CYAN}${C_RESET}"
echo -e " ${C_CYAN}${C_RESET} ${C_BG_GREEN}${C_WHITE}${C_BOLD} UPDATE ${C_RESET} ${C_CYAN}${C_RESET}"
echo -e " ${C_CYAN}${C_RESET} ${C_GREEN}${C_BOLD}1)${C_RESET} ${E_ROCKET} Quick Update — branch: ${branch_display} ${C_CYAN}${C_RESET}"
echo -e " ${C_CYAN}${C_RESET} ${C_GREEN}${C_BOLD}2)${C_RESET} ${E_LIGHTNING} Full Update — everything (pull + build + deploy) ${C_CYAN}${C_RESET}"
echo -e " ${C_CYAN}${C_RESET} ${C_GREEN}${C_BOLD}3)${C_RESET} ${E_GEAR} Emulator Only — Java backend build & deploy ${C_CYAN}${C_RESET}"
echo -e " ${C_CYAN}${C_RESET} ${C_GREEN}${C_BOLD}4)${C_RESET} ${E_GEAR} Nitro-V3 Client Only — frontend build ${C_CYAN}${C_RESET}"
echo -e " ${C_CYAN}${C_RESET} ${C_GREEN}${C_BOLD}5)${C_RESET} ${E_GEAR} Renderer Only — Nitro renderer install ${C_CYAN}${C_RESET}"
echo -e " ${C_CYAN}${C_RESET} ${C_GREEN}${C_BOLD}6)${C_RESET} ${E_GEAR} Configs Only — URL sync & merge ${C_CYAN}${C_RESET}"
echo -e " ${C_CYAN}${C_RESET} ${C_CYAN}${C_RESET}"
echo -e " ${C_CYAN}${C_RESET} ${C_BG_MAGENTA}${C_WHITE}${C_BOLD} TOOLS ${C_RESET} ${C_CYAN}${C_RESET}"
echo -e " ${C_CYAN}${C_RESET} ${C_MAGENTA}${C_BOLD}7)${C_RESET} ${E_GIT} Switch Branch — select from detected ${C_CYAN}${C_RESET}"
echo -e " ${C_CYAN}${C_RESET} ${C_YELLOW}${C_BOLD}8)${C_RESET} ${E_DB} Database Manager — tables, optimize, queries ${C_CYAN}${C_RESET}"
echo -e " ${C_CYAN}${C_RESET} ${C_YELLOW}${C_BOLD}9)${C_RESET} ${E_SHIELD} Backup & Restore ${C_CYAN}${C_RESET}"
echo -e " ${C_CYAN}${C_RESET} ${C_YELLOW}${C_BOLD}10)${C_RESET} ${E_WRENCH} Service Manager ${C_CYAN}${C_RESET}"
echo -e " ${C_CYAN}${C_RESET} ${C_YELLOW}${C_BOLD}11)${C_RESET} ${E_CHART} System Status ${C_CYAN}${C_RESET}"
echo -e " ${C_CYAN}${C_RESET} ${C_YELLOW}${C_BOLD}12)${C_RESET} ${E_HEART} Health Check ${C_CYAN}${C_RESET}"
echo -e " ${C_CYAN}${C_RESET} ${C_YELLOW}${C_BOLD}13)${C_RESET} ${E_BROOM} Cache Cleanup ${C_CYAN}${C_RESET}"
echo -e " ${C_CYAN}${C_RESET} ${C_YELLOW}${C_BOLD}14)${C_RESET} ${E_EYE} View Logs ${C_CYAN}${C_RESET}"
echo -e " ${C_CYAN}${C_RESET} ${C_YELLOW}${C_BOLD}15)${C_RESET} ${E_GEAR} Configuration ${C_CYAN}${C_RESET}"
echo -e " ${C_CYAN}${C_RESET} ${C_CYAN}${C_RESET}"
echo -e " ${C_CYAN}${C_RESET} ${C_BG_BLUE}${C_WHITE}${C_BOLD} ADVANCED ${C_RESET} ${C_CYAN}${C_RESET}"
echo -e " ${C_CYAN}${C_RESET} ${C_BLUE}${C_BOLD}16)${C_RESET} ${E_CONSOLE} Git Log — commit history per repo ${C_CYAN}${C_RESET}"
echo -e " ${C_CYAN}${C_RESET} ${C_BLUE}${C_BOLD}17)${C_RESET} ${E_GLOBE} Compare Branches — diff between branches ${C_CYAN}${C_RESET}"
echo -e " ${C_CYAN}${C_RESET} ${C_BLUE}${C_BOLD}18)${C_RESET} ${E_CLOCK} Update History — backup/changelog ${C_CYAN}${C_RESET}"
echo -e " ${C_CYAN}${C_RESET} ${C_BLUE}${C_BOLD}19)${C_RESET} ${E_FIRE} Cron Scheduler — auto-update setup ${C_CYAN}${C_RESET}"
echo -e " ${C_CYAN}${C_RESET} ${C_CYAN}${C_RESET}"
echo -e " ${C_CYAN}${C_RESET} ${C_RED}${C_BOLD}0)${C_RESET} Exit ${C_CYAN}${C_RESET}"
echo -e " ${C_CYAN}${C_RESET} ${C_CYAN}${C_RESET}"
echo -e " ${C_CYAN}╚══════════════════════════════════════════════════════════════════╝${C_RESET}"
echo ""
echo -en " ${C_CYAN}${C_BOLD}Select [0-19]${C_RESET}: "
local choice; read -r choice
case "$choice" in
1) SELECTIVE_UPDATE=""; info "Starting quick update..."; cmd_update ;;
2) SELECTIVE_UPDATE=""; pick_branch_and_update ;;
3) SELECTIVE_UPDATE="emulator"; pick_branch_and_update ;;
4) SELECTIVE_UPDATE="client"; pick_branch_and_update ;;
5) SELECTIVE_UPDATE="renderer"; pick_branch_and_update ;;
6) SELECTIVE_UPDATE="configs"; pick_branch_and_update ;;
7)
echo ""
echo -e " ${C_BOLD}Current: ${C_CYAN}${NITRO_BRANCH:-main}${C_RESET}"
echo ""
local i=1
for b in "${AVAILABLE_BRANCHES[@]}"; do
local m=""; [ "$b" = "${NITRO_BRANCH:-main}" ] && m=" ${C_GREEN}(active)${C_RESET}"
echo -e " ${C_GREEN}${C_BOLD}$i)${C_RESET} $b$m"
i=$((i + 1))
done
echo -e " ${C_GREEN}${C_BOLD}$i)${C_RESET} Custom"
echo ""
echo -en " Select [1-$i]: "
local bc; read -r bc
if [[ "$bc" =~ ^[0-9]+$ ]] && [ "$bc" -ge 1 ] && [ "$bc" -lt "$i" ]; then
NITRO_BRANCH="${AVAILABLE_BRANCHES[$((bc-1))]}"
ok "Branch: $NITRO_BRANCH"
elif [ "$bc" = "$i" ]; then
echo -en " Branch: "; read -r NITRO_BRANCH
[ -n "$NITRO_BRANCH" ] && ok "Branch: $NITRO_BRANCH"
else
warn "Invalid"
fi
;;
8) cmd_database ;;
9) local sc; sc=$(select_menu "Backup & Restore" "Create Backup" "Restore Backup" "View Backups" "Return"); case "${sc:-}" in 1) cmd_backup;; 2) cmd_restore;; 3) cmd_backups;; esac ;;
10) cmd_services ;;
11) cmd_status ;;
12) cmd_health ;;
13) cmd_clean ;;
14) cmd_logs ;;
15) cmd_config ;;
16) cmd_gitlog ;;
17) cmd_compare ;;
18) cmd_history ;;
19) cmd_schedule ;;
0|q|Q) echo -e "\n ${C_GREEN}Goodbye!${C_RESET}\n"; cursor_show; exit 0 ;;
*) warn "Invalid selection" ;;
esac
echo ""
echo -en " ${C_DIM}Press Enter to continue...${C_RESET}"
read -r
done
}
# =============================================================================
# HELP
# =============================================================================
show_help() {
show_mini_banner
echo -e " ${C_BOLD}USAGE:${C_RESET}"
echo -e " $SCRIPT_NAME ${C_CYAN}<command>${C_RESET} [${C_DIM}options${C_RESET}]"
echo ""
echo -e " ${C_BOLD}COMMANDS:${C_RESET}"
echo -e " ${C_GREEN}update${C_RESET} Run full update (--only for selective)"
echo -e " ${C_GREEN}interactive${C_RESET} Launch TUI control panel (default)"
echo -e " ${C_GREEN}status${C_RESET} System status dashboard"
echo -e " ${C_GREEN}health${C_RESET} Health check with scoring"
echo -e " ${C_GREEN}backup${C_RESET} Create database backup"
echo -e " ${C_GREEN}restore${C_RESET} Restore from backup"
echo -e " ${C_GREEN}backups${C_RESET} List backups"
echo -e " ${C_GREEN}gitlog${C_RESET} Git commit history"
echo -e " ${C_GREEN}compare${C_RESET} Branch comparison"
echo -e " ${C_GREEN}history${C_RESET} Update history"
echo -e " ${C_GREEN}schedule${C_RESET} Cron scheduler"
echo -e " ${C_GREEN}services${C_RESET} Service manager"
echo -e " ${C_GREEN}database${C_RESET} Database tools"
echo -e " ${C_GREEN}config${C_RESET} Show configuration"
echo -e " ${C_GREEN}logs${C_RESET} View logs"
echo -e " ${C_GREEN}clean${C_RESET} Cache cleanup"
echo -e " ${C_GREEN}help${C_RESET} Show help"
echo ""
echo -e " ${C_BOLD}OPTIONS:${C_RESET}"
echo -e " ${C_CYAN}--dry-run, -n${C_RESET} Preview only"
echo -e " ${C_CYAN}--force, -f${C_RESET} Force update"
echo -e " ${C_CYAN}--verbose, -v${C_RESET} Debug output"
echo -e " ${C_CYAN}--quiet, -q${C_RESET} Suppress output"
echo -e " ${C_CYAN}--branch, -b${C_RESET} Git branch (auto-detects)"
echo -e " ${C_CYAN}--url, -u${C_RESET} Site URL"
echo -e " ${C_CYAN}--only=${C_RESET} emulator|client|renderer|configs"
echo -e " ${C_CYAN}--version, -V${C_RESET} Show version"
echo -e " ${C_CYAN}--help, -h${C_RESET} Show help"
echo ""
echo -e " ${C_BOLD}EXAMPLES:${C_RESET}"
echo -e " ${C_DIM}$SCRIPT_NAME interactive${C_RESET}"
echo -e " ${C_DIM}$SCRIPT_NAME update --branch dev --dry-run${C_RESET}"
echo -e " ${C_DIM}$SCRIPT_NAME update --only=emulator --force${C_RESET}"
echo -e " ${C_DIM}$SCRIPT_NAME backup${C_RESET}"
echo ""
}
# =============================================================================
# ARGUMENT PARSING
# =============================================================================
parse_args() {
local cmd=""
while [ $# -gt 0 ]; do
case "$1" in
update|up) cmd="update" ;;
interactive|menu|ui) cmd="interactive" ;;
status|st) cmd="status" ;;
health|check) cmd="health" ;;
backup|bk) cmd="backup" ;;
restore|rs) cmd="restore" ;;
backups|bl) cmd="backups" ;;
gitlog|gl) cmd="gitlog" ;;
compare|diff) cmd="compare" ;;
history|hist) cmd="history" ;;
schedule|cron) cmd="schedule" ;;
services|svc) cmd="services" ;;
database|db) cmd="database" ;;
config|cfg) cmd="config" ;;
logs|log) cmd="logs" ;;
clean|cl) cmd="clean" ;;
help|-h|--help) cmd="help" ;;
version|-V|--version) echo "Nitro V3 Update System v${SCRIPT_VERSION} (build ${SCRIPT_BUILD})"; exit 0 ;;
--dry-run|-n) DRY_RUN=true ;;
--force|-f) FORCE=true ;;
--verbose|-v) VERBOSE=true ;;
--quiet|-q) QUIET=true ;;
--branch|-b) shift; NITRO_BRANCH="$1" ;;
--url|-u) shift; NITRO_SITE_URL="$1" ;;
--only=*) SELECTIVE_UPDATE="${1#*=}" ;;
-*) die "Unknown option: $1 (use --help)" ;;
*) [ -z "$cmd" ] && cmd="interactive" ;;
esac
shift
done
if [ -n "$SELECTIVE_UPDATE" ]; then
case "$SELECTIVE_UPDATE" in
emulator|client|renderer|configs) ;;
*) die "Invalid --only: $SELECTIVE_UPDATE" ;;
esac
fi
[ -z "$cmd" ] && cmd="interactive"
echo "$cmd"
}
# =============================================================================
# MAIN
# =============================================================================
main() {
if [ -z "${_UNBUFFERED:-}" ] && command -v unbuffer &>/dev/null; then
export _UNBUFFERED=1
exec unbuffer bash "$0" "$@"
fi
LOCKFILE="/tmp/$(basename "$0").lock"
exec 200>"$LOCKFILE"
flock -n 200 2>/dev/null || die "Another instance is already running"
DB_NAME="${NITRO_DB_NAME:-habbo}"
DB_HOST="${NITRO_DB_HOST:-127.0.0.1}"
DB_PORT="${NITRO_DB_PORT:-3306}"
DB_USER="${NITRO_DB_USER:-root}"
DB_PASS="${NITRO_DB_PASS:-}"
EMULATOR_SERVICE="${NITRO_EMULATOR_SERVICE:-emulator}"
EMULATOR_DIR="${NITRO_EMULATOR_PATH:-/var/www/emulator}"
SQL_DIR="${NITRO_SQL_DIR:-$EMULATOR_DIR/Database Updates}"
GAMEDATA_CONF_DIR="${NITRO_GAMEDATA_DIR:-/var/www/Gamedata/config}"
NITRO_SRC_DIR="${NITRO_CLIENT_DIR:-/var/www/Nitro-V3/public/configuration}"
BACKUP_DIR="${NITRO_BACKUP_DIR:-$EMULATOR_DIR/Database Updates/backups}"
NITRO_CLIENT="${NITRO_CLIENT_SRC:-/var/www/Nitro-V3}"
NITRO_RENDERER="${NITRO_RENDERER_SRC:-/var/www/Nitro_Render_V3}"
NITRO_BRANCH="${NITRO_BRANCH:-main}"
MIN_DISK_GB="${NITRO_MIN_DISK_GB:-5}"
HEALTH_RETRIES="${NITRO_HEALTH_RETRIES:-12}"
HEALTH_INTERVAL="${NITRO_HEALTH_INTERVAL:-5}"
if [ -z "${NITRO_SITE_URL:-}" ] && [ -n "${APP_URL:-}" ]; then
NITRO_SITE_URL="$APP_URL"
fi
case "${NITRO_SITE_URL:-}" in
https://*) NITRO_WS_PROTO="wss://" ; NITRO_DOMAIN="${NITRO_SITE_URL#https://}" ;;
http://*) NITRO_WS_PROTO="ws://" ; NITRO_DOMAIN="${NITRO_SITE_URL#http://}" ;;
*) NITRO_WS_PROTO="wss://" ; NITRO_DOMAIN="${NITRO_SITE_URL:-}" ;;
esac
NITRO_IMAGE_LIBRARY_URL="${NITRO_IMAGE_LIBRARY_URL:-${NITRO_SITE_URL:-}/gamedata/c_images/}"
NITRO_HOF_FURNITURE_URL="${NITRO_HOF_FURNITURE_URL:-${NITRO_SITE_URL:-}/gamedata/icons}"
NITRO_API_URL="${NITRO_API_URL:-${NITRO_SITE_URL:-}}"
NITRO_SOCKET_URL="${NITRO_SOCKET_URL:-${NITRO_WS_PROTO}ws.${NITRO_DOMAIN}}"
NITRO_CAMERA_URL="${NITRO_CAMERA_URL:-${NITRO_SITE_URL:-}/camera/photo/}"
NITRO_GAMEDATA_URL="${NITRO_GAMEDATA_URL:-${NITRO_SITE_URL:-}/gamedata}"
NITRO_ASSET_URL="${NITRO_ASSET_URL:-${NITRO_SITE_URL:-}/gamedata/bundled}"
NITRO_FURNI_ASSET_ICON_URL="${NITRO_FURNI_ASSET_ICON_URL:-${NITRO_SITE_URL:-}/gamedata/icons/%libname%%param%_icon.png}"
MYSQL_CRED="-h $DB_HOST -P $DB_PORT -u $DB_USER --ssl-verify-server-cert=OFF"
[ -n "$DB_PASS" ] && export MYSQL_PWD="$DB_PASS"
exec > >(tee -a "$LOG_FILE") 2>&1
local cmd
cmd=$(parse_args "$@")
case "$cmd" in
interactive) cmd_interactive ;;
update) cmd_update ;;
status) cmd_status ;;
health) cmd_health ;;
backup) cmd_backup ;;
restore) cmd_restore ;;
backups) cmd_backups ;;
gitlog) cmd_gitlog ;;
compare) cmd_compare ;;
history) cmd_history ;;
schedule) cmd_schedule ;;
services) cmd_services ;;
database) cmd_database ;;
config) cmd_config ;;
logs) cmd_logs ;;
clean) cmd_clean ;;
help) show_help ;;
esac
cursor_show
}
main "$@"