Files
Atomcms-edit/update-Nitrov3.sh
T
root ec81179376 fix: flexible branch handling - works with any branch name
- Add detect_branch() function with smart fallback per repo
- Auto-detect branches: tries exact match, capitalization variants,
  development/Development, main/master, remote branches
- Never crashes on missing branch - warns and skips instead
- Remove hardcoded branch validation (now accepts any branch name)
- Add branch switcher to interactive menu (option 6)
- Shows current branch and site URL in menu header
- git_update() now uses git pull origin instead of generic pull
- Update help text for --branch option
2026-06-25 19:34:02 +02:00

1974 lines
81 KiB
Bash
Executable File

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