Files
Atomcms-edit/update-Nitrov3.sh
T
root b512328ac6 fix: restore config URL sync — socket.url now correctly set to wss://ws.epicnabbo.nl
sync_configs() was missing the Python URL fix that patches:
- socket.url → wss://ws.epicnabbo.nl
- api.url, image.library.url, gamedata.url, etc.

This caused renderer-config.json and ui-config.json to have broken wss://ws. values.

Now sync_configs() applies the URL fix after copying .example files.
Verified: socket.url=wss://ws.epicnabbo.nl in both public/ and dist/ configs.
2026-06-25 20:50:26 +02:00

1029 lines
54 KiB
Bash
Executable File

#!/bin/bash
# ╔══════════════════════════════════════════════════════════════════════════════╗
# ║ ║
# ║ ███████╗███╗ ███╗ █████╗ ██████╗ ████████╗ ║
# ║ ██╔════╝████╗ ████║██╔══██╗██╔══██╗╚══██╔══╝ ║
# ║ █████╗ ██╔████╔██║███████║██████╔╝ ██║ ║
# ║ ██╔══╝ ██║╚██╔╝██║██╔══██║██╔══██╗ ██║ ║
# ║ ███████╗██║ ╚═╝ ██║██║ ██║██║ ██║ ██║ ║
# ║ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝ ║
# ║ ║
# ║ Emulator / Nitro-V3 / Nitro-V3-Render ║
# ║ Updater by Remco — epicnabbo.nl ║
# ║ ║
# ╚══════════════════════════════════════════════════════════════════════════════╝
# =============================================================================
# CONSTANTS
# =============================================================================
readonly SCRIPT_VERSION="7.0.0"
readonly SCRIPT_BUILD="20260625"
readonly SCRIPT_NAME="$(basename "$0")"
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly START_TIME=$(date +%s)
# =============================================================================
# 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_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_GOLD="\033[38;5;220m"
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_RED="" C_GREEN="" C_YELLOW="" C_BLUE="" C_MAGENTA="" C_CYAN="" C_WHITE="" C_GOLD=""
C_BG_RED="" C_BG_GREEN="" C_BG_YELLOW="" C_BG_BLUE="" C_BG_MAGENTA="" C_BG_CYAN=""
fi
# =============================================================================
# EMOJIS
# =============================================================================
if locale charmap 2>/dev/null | grep -qi utf; then
E_OK="✔" E_FAIL="✘" E_WARN="⚠" E_ARROW="➜" E_DOT="●"
E_GEAR="⚙" E_ROCKET="🚀" E_SHIELD="🛡" E_DB="🗄" E_GIT="🔀"
E_WRENCH="🔧" E_CHART="📊" E_BOLT="⚡" E_BROOM="🧹" E_EYE="👁"
E_LOCK="🔒" E_HEART="♥" E_CLOUD="☁" E_FIRE="🔥" E_GLOBE="🌐"
E_CONSOLE="🖥" E_CLOCK="⏱"
else
E_OK="[OK]" E_FAIL="[!]" E_WARN="[!]" E_ARROW="->" E_DOT="*"
E_GEAR="[G]" E_ROCKET="[R]" E_SHIELD="[S]" E_DB="[DB]" E_GIT="[G]"
E_WRENCH="[W]" E_CHART="[C]" E_BOLT="[!]" E_BROOM="[C]" E_EYE="[E]"
E_LOCK="[L]" E_HEART="[H]" E_CLOUD="[C]" E_FIRE="[F]" E_GLOBE="[W]"
E_CONSOLE="[M]" E_CLOCK="[T]"
fi
# =============================================================================
# STATE
# =============================================================================
DRY_RUN=false
SELECTIVE_UPDATE=""
HAD_UPDATES=false
NITRO_BUILT=false
ERRORS=0
WARNINGS=0
UPDATED_REPOS=()
LAST_BACKUP=""
ROLLBACK_NEEDED=false
NOTIFY_FAILED=false
# =============================================================================
# LOAD .ENV
# =============================================================================
if [ -f "$SCRIPT_DIR/.env" ]; then set -a; . "$SCRIPT_DIR/.env"; set +a; fi
# =============================================================================
# CONFIG
# =============================================================================
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"
NOTIFY_URL="${NITRO_NOTIFY_URL:-}"
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_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"
# =============================================================================
# UI HELPERS
# =============================================================================
log_write() { printf "[%s] [%s] %s\n" "$(date '+%Y-%m-%d %H:%M:%S')" "$1" "$2" >> "$LOG_FILE" 2>/dev/null || true; }
info() { echo -e " ${C_BLUE}${E_ARROW}${C_RESET} $1"; log_write "INFO" "$1"; }
ok() { echo -e " ${C_GREEN}${E_OK}${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_FAIL}${C_RESET} $1"; log_write "FAIL" "$1"; }
debug() { echo -e " ${C_DIM}${E_DOT} $1${C_RESET}"; log_write "DEBUG" "$1"; }
die() {
echo ""
echo -e " ${C_BG_RED}${C_WHITE}${C_BOLD} ERROR ${C_RESET} ${C_RED}$1${C_RESET}"
log_write "FATAL" "$1"
cleanup_notify_failure "$1"
cursor_show
exit 1
}
cursor_hide() { tput civis 2>/dev/null || true; }
cursor_show() { tput cnorm 2>/dev/null || true; }
clear_line() { printf "\r\033[K"; }
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} %s " "$msg"; i=$(( (i + 1) % ${#frames[@]} )); sleep 0.1; done ) &
SPINNER_PID=$!; disown "$SPINNER_PID" 2>/dev/null || true
}
spinner_stop() {
local st="${1:-ok}"
[ -n "$SPINNER_PID" ] && kill "$SPINNER_PID" 2>/dev/null && wait "$SPINNER_PID" 2>/dev/null; SPINNER_PID=""
clear_line; cursor_show
case "$st" in ok) printf "\r ${C_GREEN}${E_OK}${C_RESET} Done\n" ;; warn) printf "\r ${C_YELLOW}${E_WARN}${C_RESET} Done\n" ;; fail) printf "\r ${C_RED}${E_FAIL}${C_RESET} Failed\n" ;; esac
}
step() {
local num="${1}" total="${2}" title="${3}"
echo ""
echo -e " ${C_BG_CYAN}${C_WHITE}${C_BOLD} Step ${num}/${total}${title} ${C_RESET}"
echo ""
}
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 "
local reply; read -r reply; reply="${reply:-$default}"
[[ "$reply" =~ ^[Yy] ]]
}
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_kv() {
local key="${1}" val="${2}" width="${3:-60}"
local ck=$(echo -e "$key" | sed 's/\x1b\[[0-9;]*m//g')
local cv=$(echo -e "$val" | sed 's/\x1b\[[0-9;]*m//g')
local pad=$((width - ${#ck} - ${#cv} - 6)); [ $pad -lt 0 ] && pad=0
printf "${C_CYAN}${C_RESET} %s%*s%s ${C_CYAN}${C_RESET}\n" "$key" $pad "" "$val"
}
box_line() {
local text="${1:-}" width="${2:-60}"
local ct=$(echo -e "$text" | sed 's/\x1b\[[0-9;]*m//g')
local pad=$((width - ${#ct} - 4)); [ $pad -lt 0 ] && pad=0
printf "${C_CYAN}${C_RESET} %s%*s${C_CYAN}${C_RESET}\n" "$text" $pad ""
}
box_bottom() { local w="${1:-60}"; printf "${C_CYAN}╚"; printf '═%.0s' $(seq 1 $w); echo -e "╝${C_RESET}"; }
# =============================================================================
# NOTIFICATIONS
# =============================================================================
notify() {
local status="$1" message="$2"
[ -z "$NOTIFY_URL" ] && return 0
local elapsed=$(($(date +%s) - START_TIME))
local ef; ef=$(printf '%dm %ds' $((elapsed / 60)) $((elapsed % 60)))
local color="good"; [ "$status" != "SUCCESS" ] && color="danger"
curl -s -o /dev/null -X POST "$NOTIFY_URL" -H "Content-Type: application/json" \
-d "{\"text\":\"[Nitro Update] $status\",\"attachments\":[{\"color\":\"$color\",\"fields\":[{\"title\":\"Status\",\"value\":\"$message\",\"short\":true},{\"title\":\"Duration\",\"value\":\"$ef\",\"short\":true}]}]}" 2>/dev/null || true
}
cleanup_notify_failure() { $NOTIFY_FAILED && return; NOTIFY_FAILED=true; notify "FAILED" "$1"; }
cleanup_on_exit() {
local ec=$?; cursor_show
if [ $ec -ne 0 ] && [ "$ROLLBACK_NEEDED" = true ]; then
echo -e "\n ${C_BG_RED}${C_WHITE}${C_BOLD} ROLLBACK ${C_RESET}"
[ -n "$LAST_BACKUP" ] && [ -f "$LAST_BACKUP" ] && mariadb $MYSQL_CRED "$DB_NAME" < "$LAST_BACKUP" 2>/dev/null && ok "DB restored" || warn "Rollback failed"
fi
[ $ec -ne 0 ] && cleanup_notify_failure "Exit code $ec"
}
trap cleanup_on_exit EXIT
trap 'cursor_show; exit 1' SIGINT SIGTERM
# =============================================================================
# GIT HELPERS
# =============================================================================
detect_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" 2>/dev/null && 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
echo "$result" | grep -qx "$b" 2>/dev/null || result="${result:+$result
}$b"
done <<< "$branches"
done
echo "$result"
}
detect_best_branch() {
local repo="$1" preferred="$2"
cd "$repo" 2>/dev/null || { echo "main"; return; }
for v in "$preferred" "${preferred^}" "main" "master"; do
git rev-parse --verify "$v" &>/dev/null && { echo "$v"; return; }
done
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 rb; rb=$(detect_best_branch "$repo" "$branch")
[ "$rb" != "$branch" ] && info "Branch '$branch' not in $(basename "$repo"), using '$rb'"
git checkout "$rb" 2>/dev/null || { warn "Cannot checkout '$rb' in $(basename "$repo")"; return 1; }
if ! git pull origin "$rb" 2>/dev/null && ! git pull 2>/dev/null; then
warn "Pull failed in $(basename "$repo")"; 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
}
# =============================================================================
# UPDATE FUNCTIONS
# =============================================================================
update_emulator() {
step 2 8 "Update & Build Emulator"
if git_update "$EMULATOR_DIR/Emulator" "$NITRO_BRANCH"; then
HAD_UPDATES=true; ROLLBACK_NEEDED=true; UPDATED_REPOS+=("Emulator")
mkdir -p "$BACKUP_DIR"
local bf="$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" > "$bf" 2>/dev/null; then
LAST_BACKUP="$bf"
local bks=$(stat -c%s "$bf" 2>/dev/null || echo 0)
[ "$bks" -lt 1024 ] && { rm -f "$bf"; spinner_stop fail; die "Backup too small"; }
spinner_stop ok; ok "Backup: $(numfmt --to=iec "$bks")"
else
spinner_stop fail; die "Database backup failed"
fi
if [ -d "$SQL_DIR" ]; then
local sc=0
while IFS= read -r -d '' sf; do info "SQL: $(basename "$sf")"; mariadb $MYSQL_CRED --force "$DB_NAME" < "$sf" 2>/dev/null || warn "SQL failed: $(basename "$sf")"; sc=$((sc+1)); done < <(find "$SQL_DIR" -name '*.sql' -mmin -10 -not -path "$BACKUP_DIR/*" -print0 2>/dev/null)
[ "$sc" -gt 0 ] && ok "$sc SQL file(s) imported"
fi
cd "$EMULATOR_DIR/Emulator"
spinner_start "Building emulator..."
mvn package -q 2>&1 | tail -5 && spinner_stop ok || { spinner_stop fail; die "Maven build failed"; }
local jar=$(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" ] && die "No jar found"
ok "Jar: $jar"
cd target/
cat > emulator << EOJ
#!/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
EOJ
chmod +x emulator; ok "Launch script updated"
ROLLBACK_NEEDED=false
else
info "Emulator: already up to date"
fi
}
update_renderer() {
step 3 8 "Update Nitro_Render_V3"
if git_update "$NITRO_RENDERER" "$NITRO_BRANCH"; then
HAD_UPDATES=true; UPDATED_REPOS+=("Nitro_Render_V3")
spinner_start "Installing deps..."
yarn install --frozen-lockfile 2>&1 || { clean_node_modules && yarn install 2>&1; } || { spinner_stop fail; die "yarn install failed"; }
spinner_stop ok; ok "Renderer dependencies installed"
else
info "Renderer: already up to date"
fi
}
update_client() {
step 4 8 "Update & Build Nitro-V3"
if git_update "$NITRO_CLIENT" "$NITRO_BRANCH"; then
HAD_UPDATES=true; NITRO_BUILT=true; UPDATED_REPOS+=("Nitro-V3")
spinner_start "Installing deps..."
yarn install --frozen-lockfile 2>&1 || { clean_node_modules && yarn install 2>&1; } || { spinner_stop fail; die "yarn install failed"; }
spinner_stop ok
spinner_start "Building frontend..."
yarn build 2>&1 | tail -3 && spinner_stop ok || { spinner_stop fail; die "yarn build failed"; }
else
info "Nitro-V3: already up to date"
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 5 8 "Sync Configurations"
mkdir -p "$GAMEDATA_CONF_DIR"
local ms="$SCRIPT_DIR/scripts/merge-config.cjs"
local dcd="$NITRO_CLIENT/dist/configuration"
for d in "$NITRO_SRC_DIR" "$GAMEDATA_CONF_DIR"; do
[ -d "$d" ] && [ ! -w "$d" ] && sudo chown -R "$(whoami)":"$(whoami)" "$d" 2>/dev/null || true
done
local ec=0
for ef in "$NITRO_SRC_DIR"/*.example; do
[ -f "$ef" ] || continue
local b=$(basename "$ef"); local tn="${b%.example}"
case "$tn" in *.*) ;; *) tn="$tn.json" ;; esac
node "$ms" "$ef" "$NITRO_SRC_DIR/$tn" 2>/dev/null
node "$ms" "$ef" "$GAMEDATA_CONF_DIR/$tn" 2>/dev/null
[ -d "$dcd" ] && cp "$NITRO_SRC_DIR/$tn" "$dcd/$tn" 2>/dev/null || true
ec=$((ec+1))
done
ok "$ec config file(s) synced"
info "Applying critical config URLs..."
NITRO_SRC_DIR="$NITRO_SRC_DIR" NITRO_DIST_CONFIG_DIR="$dcd" \
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 '
import json, 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(chr(46), chr(95))}")
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(chr(46), chr(95))}")
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)}")
' && ok "All config URLs synced"
}
do_cleanup() {
step 6 8 "Cleanup"
local lc=$(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
[ -d "$BACKUP_DIR" ] && 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
yarn cache clean 2>/dev/null || true
rm -rf /tmp/nitro-* 2>/dev/null || true
ok "Cleanup done ($lc logs removed)"
}
do_permissions() {
step 7 8 "Set Permissions"
for d in "$NITRO_CLIENT" "$NITRO_RENDERER" "$EMULATOR_DIR" "$GAMEDATA_CONF_DIR"; do
[ -d "$d" ] && sudo chown -R www-data:www-data "$d" 2>/dev/null || true
done
ok "Permissions set"
}
do_restart() {
step 8 8 "Restart Services"
if systemctl cat "$EMULATOR_SERVICE" &>/dev/null; then
sudo systemctl restart "$EMULATOR_SERVICE" 2>/dev/null && ok "$EMULATOR_SERVICE restarted" || warn "Restart failed"
elif command -v pm2 &>/dev/null; then
pm2 restart all 2>/dev/null && ok "PM2 restarted" || warn "PM2 restart failed"
else
warn "No service manager found"
fi
command -v nginx &>/dev/null && sudo nginx -t 2>/dev/null && sudo systemctl reload nginx 2>/dev/null && ok "Nginx reloaded"
command -v redis-cli &>/dev/null && redis-cli FLUSHALL 2>/dev/null && ok "Redis flushed"
}
show_summary() {
local el=$(($(date +%s) - START_TIME)); local ef; ef=$(printf '%dm %ds' $((el/60)) $((el%60)))
echo ""
echo -e " ${C_CYAN}╔══════════════════════════════════════════════════════════════════╗${C_RESET}"
if [ "$ERRORS" -eq 0 ]; then
echo -e " ${C_CYAN}${C_RESET} ${C_GREEN}${C_BOLD}✔ UPDATE COMPLETED SUCCESSFULLY${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: %-46s${C_CYAN}${C_RESET}\n" "$ef"
printf " ${C_CYAN}${C_RESET} Updates: %-46s${C_CYAN}${C_RESET}\n" "$([ "$HAD_UPDATES" = true ] && echo "Yes" || echo "No")"
printf " ${C_CYAN}${C_RESET} Nitro build: %-46s${C_CYAN}${C_RESET}\n" "$([ "$NITRO_BUILT" = true ] && echo "Yes" || echo "No")"
printf " ${C_CYAN}${C_RESET} Warnings: %-46s${C_CYAN}${C_RESET}\n" "$WARNINGS"
[ ${#UPDATED_REPOS[@]} -gt 0 ] && printf " ${C_CYAN}${C_RESET} Repos: %-46s${C_CYAN}${C_RESET}\n" "${UPDATED_REPOS[*]}"
echo -e " ${C_CYAN}╚══════════════════════════════════════════════════════════════════╝${C_RESET}"
echo ""
notify "SUCCESS" "Completed in $ef (${WARNINGS} warnings)"
}
# =============================================================================
# FULL UPDATE
# =============================================================================
cmd_update() {
echo ""
info "Site: ${NITRO_SITE_URL:-?} | Branch: $NITRO_BRANCH | Mode: ${SELECTIVE_UPDATE:-full}"
info "Log: $LOG_FILE"
step 1 8 "System Diagnostics"
for cmd in git mariadb mariadb-dump mvn node python3 yarn; do
command -v "$cmd" &>/dev/null || die "Missing: $cmd"
done
ok "All commands available"
for d in "$EMULATOR_DIR/Emulator" "$NITRO_RENDERER" "$NITRO_CLIENT" "$NITRO_SRC_DIR"; do
[ -d "$d" ] || die "Missing dir: $d"
done
ok "All directories exist"
local ak=$(df --output=avail "$EMULATOR_DIR" 2>/dev/null | tail -1)
local ag=$((ak / 1024 / 1024))
[ "$ag" -lt "$MIN_DISK_GB" ] && die "Only ${ag}GB free (min ${MIN_DISK_GB}GB)"
ok "${ag}GB disk available"
mariadb $MYSQL_CRED -e "SELECT 1" "$DB_NAME" &>/dev/null || die "DB connection failed"
ok "Database connected"
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
do_cleanup
do_permissions
do_restart
;;
esac
show_summary
}
# =============================================================================
# BACKUP / RESTORE
# =============================================================================
cmd_backup() {
step 1 1 "Database Backup"
mkdir -p "$BACKUP_DIR"
local bf="$BACKUP_DIR/manual_$(date +%Y%m%d_%H%M%S).sql"
spinner_start "Backing up..."
if mariadb-dump $MYSQL_CRED --force --skip-lock-tables --routines --events --triggers "$DB_NAME" > "$bf" 2>/dev/null; then
local sz=$(stat -c%s "$bf" 2>/dev/null || echo 0); spinner_stop ok
ok "Backup: $(numfmt --to=iec "$sz")$(basename "$bf")"
else
spinner_stop fail; die "Backup failed"
fi
}
cmd_restore() {
step 1 1 "Database Restore"
[ ! -d "$BACKUP_DIR" ] && die "No backup dir"
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"
echo ""
echo -e " ${C_BOLD}${C_CYAN}Backups:${C_RESET}"
local i=1
for bk in "${backups[@]}"; do
local sz=$(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" "$(basename "$bk")" "$sz"
i=$((i+1))
done
echo -en "\n Select [1-$((i-1))]: "
local c; read -r c
[[ "$c" =~ ^[0-9]+$ ]] && [ "$c" -ge 1 ] && [ "$c" -le ${#backups[@]} ] || die "Invalid"
local sel="${backups[$((c-1))]}"
if ! confirm "Restore $(basename "$sel")? OVERWRITES database!"; then info "Cancelled"; return; fi
spinner_start "Restoring..."
mariadb $MYSQL_CRED "$DB_NAME" < "$sel" 2>/dev/null && spinner_stop ok && ok "Restored" || { spinner_stop fail; die "Restore failed"; }
}
cmd_backups() {
step 1 1 "Backup Inventory"
[ ! -d "$BACKUP_DIR" ] && { warn "No backup dir"; return; }
echo ""
printf " ${C_DIM}%-4s %-38s %-10s${C_RESET}\n" "#" "FILENAME" "SIZE"
printf " ${C_DIM}%s${C_RESET}\n" "$(printf '%0.s─' $(seq 1 56))"
local t=0 ts=0
while IFS= read -r l; do
t=$((t+1)); local n=$(basename "$l"); local s=$(stat -c%s "$l" 2>/dev/null || echo 0); ts=$((ts+s))
printf " ${C_GREEN}%2d${C_RESET} %-38s ${C_CYAN}%s${C_RESET}\n" "$t" "$n" "$(numfmt --to=iec "$s" 2>/dev/null || echo "?")"
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 56))"
echo -e " ${C_BOLD}Total: $t backups — $(numfmt --to=iec "$ts" 2>/dev/null || echo "?")${C_RESET}\n"
}
# =============================================================================
# STATUS
# =============================================================================
cmd_status() {
step 1 1 "System Status"
echo ""
box_top "SERVER" 56; box_kv "Host:" "$(hostname)" 56
box_kv "OS:" "$(cat /etc/os-release 2>/dev/null | grep PRETTY_NAME | cut -d= -f2 | tr -d '"' || uname -s)" 56
box_kv "Uptime:" "$(uptime -p 2>/dev/null || uptime)" 56; box_bottom 56
echo ""
box_top "RESOURCES" 56
box_kv "CPU:" "$(awk '{printf "%.1f", $1}' /proc/loadavg 2>/dev/null || echo "?") / $(nproc 2>/dev/null || echo '?') cores" 56
box_kv "Memory:" "$(free -m 2>/dev/null | awk '/^Mem:/{printf "%dMB / %dMB", $3, $2}')" 56
box_kv "Disk:" "$(df -h "$SCRIPT_DIR" 2>/dev/null | tail -1 | awk '{printf "%s / %s (%s)", $3, $2, $5}')" 56
local dbs=$(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 dbt=$(mariadb $MYSQL_CRED -N -e "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='$DB_NAME'" "$DB_NAME" 2>/dev/null || echo "?")
box_kv "Database:" "${dbs}MB / ${dbt} 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}"
printf " ${C_CYAN}${C_RESET} %-20s %b${C_CYAN}${C_RESET}\n" "$svc" "$st"
done; box_bottom 56
echo ""
box_top "GIT REPOS" 56
for re in "Emulator:$EMULATOR_DIR/Emulator" "Nitro-V3:$NITRO_CLIENT" "Render:$NITRO_RENDERER"; do
local nm="${re%%:*}" pa="${re##*:}"
[ -d "$pa/.git" ] || continue
local br=$(cd "$pa" && git branch --show-current 2>/dev/null || echo "?")
local cm=$(cd "$pa" && git log --oneline -1 2>/dev/null | cut -c1-30 || echo "?")
printf " ${C_CYAN}${C_RESET} %-12s ${C_CYAN}%-8s${C_RESET} %s\n" "$nm" "$br" "$cm"
done; box_bottom 56
echo ""
}
# =============================================================================
# HEALTH CHECK
# =============================================================================
cmd_health() {
step 1 1 "Health Check"
local t=0 p=0 f=0 w=0
check() { t=$((t+1)); "$@" &>/dev/null && { p=$((p+1)); ok "$1"; } || { f=$((f+1)); fail "$1"; }; }
checkw() { t=$((t+1)); "$@" &>/dev/null && { p=$((p+1)); ok "$1"; } || { w=$((w+1)); warn "$1"; }; }
echo ""
echo -e " ${C_BOLD}System:${C_RESET}"
checkw "Git" command -v git; checkw "Node.js" command -v node; checkw "Yarn" command -v yarn
checkw "Python3" command -v python3; checkw "Java" command -v java; checkw "Maven" command -v mvn
echo -e "\n ${C_BOLD}Directories:${C_RESET}"
checkw "Emulator" test -d "$EMULATOR_DIR/Emulator"; checkw "Nitro-V3" test -d "$NITRO_CLIENT"; checkw "Renderer" test -d "$NITRO_RENDERER"
echo -e "\n ${C_BOLD}Services:${C_RESET}"
checkw "MariaDB" systemctl is-active mariadb; checkw "Nginx" systemctl is-active nginx; checkw "Redis" systemctl is-active redis-server
echo -e "\n ${C_BOLD}Application:${C_RESET}"
checkw "artisan" test -f "$SCRIPT_DIR/artisan"; checkw ".env" test -f "$SCRIPT_DIR/.env"
checkw "Site" timeout 5 curl -sf "${NITRO_SITE_URL:-}" 2>/dev/null
echo -e "\n ${C_BOLD}Resources:${C_RESET}"
local ak=$(df --output=avail "$EMULATOR_DIR" 2>/dev/null | tail -1); local ag=$((ak/1024/1024))
t=$((t+1)); [ "$ag" -ge "$MIN_DISK_GB" ] && { p=$((p+1)); ok "Disk: ${ag}GB"; } || { f=$((f+1)); fail "Disk: ${ag}GB"; }
echo ""
local sc=$((p*100/t))
box_top "SCORE" 50
box_kv "Passed:" "$p / $t" 50
box_kv "Failed:" "$f" 50; box_kv "Warnings:" "$w" 50
if [ "$sc" -ge 90 ]; then box_line "${C_GREEN}${C_BOLD}${sc}% — EXCELLENT${C_RESET}" 50
elif [ "$sc" -ge 70 ]; then box_line "${C_YELLOW}${C_BOLD}${sc}% — GOOD${C_RESET}" 50
else box_line "${C_RED}${C_BOLD}${sc}% — NEEDS ATTENTION${C_RESET}" 50; fi
box_bottom 50; echo ""
}
# =============================================================================
# SERVICES
# =============================================================================
cmd_services() {
step 1 1 "Service Manager"
echo ""
echo -e " ${C_BOLD}1)${C_RESET} Start Emulator ${C_BOLD}2)${C_RESET} Stop Emulator ${C_BOLD}3)${C_RESET} Restart Emulator"
echo -e " ${C_BOLD}4)${C_RESET} Start Nginx ${C_BOLD}5)${C_RESET} Restart Nginx ${C_BOLD}6)${C_RESET} Status All"
echo -e " ${C_BOLD}7)${C_RESET} Emulator Logs ${C_BOLD}0)${C_RESET} Return"
echo -en "\n Select: "; local c; read -r c
case "$c" in
1) sudo systemctl start "$EMULATOR_SERVICE" && ok "Started" || fail "Failed" ;;
2) sudo systemctl stop "$EMULATOR_SERVICE" && ok "Stopped" || fail "Failed" ;;
3) sudo systemctl restart "$EMULATOR_SERVICE" && ok "Restarted" || fail "Failed" ;;
4) sudo systemctl start nginx && ok "Started" || fail "Failed" ;;
5) sudo systemctl restart nginx && ok "Restarted" || fail "Failed" ;;
6) for s in nginx mariadb redis-server "$EMULATOR_SERVICE" php-fpm cron; do
local st="${C_RED}DOWN${C_RESET}"; systemctl is-active "$s" &>/dev/null && st="${C_GREEN}UP${C_RESET}"
printf " %-25s %b\n" "$s" "$st"
done ;;
7) sudo journalctl -u "$EMULATOR_SERVICE" --no-pager -n 30 ;;
esac
}
# =============================================================================
# DATABASE
# =============================================================================
cmd_database() {
step 1 1 "Database Tools"
echo ""
echo -e " ${C_BOLD}1)${C_RESET} DB Info ${C_BOLD}2)${C_RESET} List Tables ${C_BOLD}3)${C_RESET} Table Sizes"
echo -e " ${C_BOLD}4)${C_RESET} Optimize ${C_BOLD}5)${C_RESET} Custom Query ${C_BOLD}6)${C_RESET} Active Users"
echo -e " ${C_BOLD}0)${C_RESET} Return"
echo -en "\n Select: "; local c; read -r c
case "$c" in
1) box_top "DB INFO" 50
local dbs=$(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_kv "Name:" "$DB_NAME" 50; box_kv "Host:" "$DB_HOST:$DB_PORT" 50; box_kv "Size:" "${dbs}MB" 50
box_kv "Tables:" "$(mariadb $MYSQL_CRED -N -e "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='$DB_NAME'" "$DB_NAME" 2>/dev/null || echo '?')" 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' FROM information_schema.tables WHERE table_schema='$DB_NAME' ORDER BY (data_length+index_length) DESC" 2>/dev/null ;;
4) spinner_start "Optimizing..."
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
done; spinner_stop ok; ok "Optimized" ;;
5) echo -en " SQL: "; local q; read -r q; [ -n "$q" ] && mariadb $MYSQL_CRED "$DB_NAME" -e "$q" 2>/dev/null ;;
6) mariadb $MYSQL_CRED -e "SELECT name, motto FROM $DB_NAME.users WHERE online='1'" 2>/dev/null || warn "Could not query" ;;
esac
}
# =============================================================================
# CONFIG
# =============================================================================
cmd_config() {
step 1 1 "Configuration"
echo ""
box_top "CONFIGURATION" 56
box_kv "Site:" "${NITRO_SITE_URL:-?}" 56
box_kv "Branch:" "$NITRO_BRANCH" 56
box_kv "Emulator:" "$EMULATOR_DIR" 56
box_kv "Nitro-V3:" "$NITRO_CLIENT" 56
box_kv "Renderer:" "$NITRO_RENDERER" 56
box_kv "Database:" "$DB_NAME @ $DB_HOST:$DB_PORT" 56
box_kv "Service:" "$EMULATOR_SERVICE" 56
box_kv "Socket:" "${NITRO_SOCKET_URL:-?}" 56
box_kv "API:" "${NITRO_API_URL:-?}" 56
box_kv "Gamedata:" "${NITRO_GAMEDATA_URL:-?}" 56
box_kv "Min Disk:" "${MIN_DISK_GB}GB" 56
box_bottom 56; echo ""
}
# =============================================================================
# LOGS
# =============================================================================
cmd_logs() {
step 1 1 "Log Viewer"
[ ! -d "$LOG_DIR" ] && { warn "No log dir"; return; }
local logs=()
while IFS= read -r l; do logs+=("$l"); 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"; return; }
echo ""
local i=1
for l in "${logs[@]}"; do
local sz=$(numfmt --to=iec "$(stat -c%s "$l" 2>/dev/null || echo 0)" 2>/dev/null || echo "?")
printf " ${C_GREEN}${C_BOLD}%2d)${C_RESET} %-35s ${C_DIM}%s${C_RESET}\n" "$i" "$(basename "$l")" "$sz"
i=$((i+1))
done
echo -en "\n Select [1-$((i-1))]: "; local c; read -r c
if [[ "$c" =~ ^[0-9]+$ ]] && [ "$c" -ge 1 ] && [ "$c" -le ${#logs[@]} ]; then
less -R "${logs[$((c-1))]}" 2>/dev/null || cat "${logs[$((c-1))]}"
fi
}
# =============================================================================
# CLEAN
# =============================================================================
cmd_clean() {
step 1 1 "Cache Cleanup"
echo ""
echo -e " ${C_BOLD}1)${C_RESET} Yarn ${C_BOLD}2)${C_RESET} Laravel ${C_BOLD}3)${C_RESET} Redis"
echo -e " ${C_BOLD}4)${C_RESET} Logs ${C_BOLD}5)${C_RESET} Everything ${C_BOLD}0)${C_RESET} Return"
echo -en "\n Select: "; local c; read -r c
case "$c" in
1) yarn cache clean 2>/dev/null && ok "Yarn cleaned" ;;
2) cd "$SCRIPT_DIR" && php artisan cache:clear 2>/dev/null && php artisan config:clear 2>/dev/null && php artisan view:clear 2>/dev/null && ok "Laravel cleaned" ;;
3) redis-cli FLUSHALL 2>/dev/null && ok "Redis flushed" || warn "Redis N/A" ;;
4) find "$EMULATOR_DIR" -name "*.log" -mtime +14 -exec rm -f {} \; 2>/dev/null; ok "Logs cleaned" ;;
5) yarn cache clean 2>/dev/null; find "$EMULATOR_DIR" -name "*.log" -mtime +14 -exec rm -f {} \; 2>/dev/null; rm -rf /tmp/nitro-* 2>/dev/null; ok "Full cleanup" ;;
esac
}
# =============================================================================
# GIT LOG
# =============================================================================
cmd_gitlog() {
step 1 1 "Git Commit History"
echo ""
echo -e " ${C_BOLD}1)${C_RESET} Emulator ${C_BOLD}2)${C_RESET} Nitro-V3 ${C_BOLD}3)${C_RESET} Renderer"
echo -en "\n Select: "; local c; read -r c
local repo=""
case "$c" in 1) repo="$EMULATOR_DIR/Emulator" ;; 2) repo="$NITRO_CLIENT" ;; 3) repo="$NITRO_RENDERER" ;; *) 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 ""
cd "$repo" && git log --pretty=format:" ${C_CYAN}%h${C_RESET} %C(yellow)%cr${C_RESET} %s" -20 2>/dev/null
echo -e "\n"
}
# =============================================================================
# BRANCH COMPARE
# =============================================================================
cmd_compare() {
step 1 1 "Branch Comparison"
echo ""
local branches=()
while IFS= read -r b; do [ -n "$b" ] && branches+=("$b"); done < <(detect_branches)
[ ${#branches[@]} -lt 2 ] && { warn "Need 2+ branches (found ${#branches[@]})"; return; }
echo -e " ${C_BOLD}Branches:${C_RESET}"
local i=1
for b in "${branches[@]}"; do echo -e " ${C_GREEN}${C_BOLD}$i)${C_RESET} $b"; i=$((i+1)); done
echo -en "\n Branch A: "; local ba; read -r ba
echo -en " Branch B: "; 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"; return; }
local bA="${branches[$((ba-1))]}" bB="${branches[$((bb-1))]}"
echo ""
for repo in "$EMULATOR_DIR/Emulator" "$NITRO_CLIENT" "$NITRO_RENDERER"; do
[ -d "$repo/.git" ] || continue
echo -e " ${C_BOLD}$(basename "$repo"):${C_RESET}"
cd "$repo" 2>/dev/null || continue
if git rev-parse --verify "$bA" &>/dev/null && git rev-parse --verify "$bB" &>/dev/null; then
local ahead=$(git rev-list --count "$bB".."$bA" 2>/dev/null || echo "0")
local behind=$(git rev-list --count "$bA".."$bB" 2>/dev/null || echo "0")
echo -e " $bA is ${C_GREEN}$ahead${C_RESET} ahead, ${C_YELLOW}$behind${C_RESET} behind $bB"
fi
done; echo ""
}
# =============================================================================
# CRON SCHEDULER
# =============================================================================
cmd_schedule() {
step 1 1 "Cron Scheduler"
echo ""
local cc=$(crontab -l 2>/dev/null | grep -F "$SCRIPT_NAME" || echo "")
echo -e " ${C_BOLD}Current:${C_RESET} ${cc:-${C_DIM}none${C_RESET}}"
echo ""
echo -e " ${C_BOLD}1)${C_RESET} Daily 03:00 ${C_BOLD}2)${C_RESET} Daily 04:00 ${C_BOLD}3)${C_RESET} Weekly Sun 03:00"
echo -e " ${C_BOLD}4)${C_RESET} Custom time ${C_BOLD}5)${C_RESET} Remove ${C_BOLD}0)${C_RESET} Return"
echo -en "\n Select: "; local c; read -r c
case "$c" in
1) (crontab -l 2>/dev/null | grep -vF "$SCRIPT_NAME"; echo "0 3 * * * cd $SCRIPT_DIR && ./$SCRIPT_NAME update --branch $NITRO_BRANCH >> /var/log/nitro-cron.log 2>&1") | crontab - && ok "Scheduled daily 03:00" ;;
2) (crontab -l 2>/dev/null | grep -vF "$SCRIPT_NAME"; echo "0 4 * * * cd $SCRIPT_DIR && ./$SCRIPT_NAME update --branch $NITRO_BRANCH >> /var/log/nitro-cron.log 2>&1") | crontab - && ok "Scheduled daily 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 >> /var/log/nitro-cron.log 2>&1") | crontab - && ok "Scheduled weekly" ;;
4) echo -en " Minute [0-59]: "; local m; read -r m
echo -en " Hour [0-23]: "; local h; read -r h
(crontab -l 2>/dev/null | grep -vF "$SCRIPT_NAME"; echo "$m $h * * * cd $SCRIPT_DIR && ./$SCRIPT_NAME update --branch $NITRO_BRANCH >> /var/log/nitro-cron.log 2>&1") | crontab - && ok "Scheduled $m $h * * *" ;;
5) crontab -l 2>/dev/null | grep -vF "$SCRIPT_NAME" | crontab - && ok "Removed" ;;
esac
}
# =============================================================================
# MAIN MENU
# =============================================================================
pick_branch_and_update() {
local mode_label="Full"
case "${SELECTIVE_UPDATE:-full}" in
emulator) mode_label="Emulator Only" ;; client) mode_label="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_branches)
echo ""
echo -e " ${C_BOLD}${C_CYAN}Update: ${C_GREEN}$mode_label${C_RESET}"
echo ""
local i=1
for b in "${branches[@]}"; do
local m=""; [ "$b" = "$NITRO_BRANCH" ] && m=" ${C_GREEN}(current)${C_RESET}"
echo -e " ${C_GREEN}${C_BOLD}$i)${C_RESET} ${C_CYAN}$b${C_RESET}$m"
i=$((i+1))
done
echo -e " ${C_GREEN}${C_BOLD}$i)${C_RESET} Custom"
echo -en "\n Branch [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 " Name: "; read -r NITRO_BRANCH; [ -z "$NITRO_BRANCH" ] && NITRO_BRANCH="main"
else
warn "Invalid, using: $NITRO_BRANCH"
fi
echo -e "\n Branch: ${C_CYAN}${C_BOLD}$NITRO_BRANCH${C_RESET} Mode: ${C_GREEN}$mode_label${C_RESET}"
if ! confirm "Start update?"; then info "Cancelled"; return; fi
cmd_update
}
cmd_interactive() {
while true; do
clear
local branches=()
while IFS= read -r b; do [ -n "$b" ] && branches+=("$b"); done < <(detect_branches)
echo -e "${C_CYAN}"
echo ""
echo " ╔═══════════════════════════════════════════════════════════════════════════╗"
echo " ║ ║"
echo " ║ ███████╗███╗ ███╗ █████╗ ██████╗ ████████╗ ║"
echo " ║ ██╔════╝████╗ ████║██╔══██╗██╔══██╗╚══██╔══╝ ║"
echo " ║ █████╗ ██╔████╔██║███████║██████╔╝ ██║ ║"
echo " ║ ██╔══╝ ██║╚██╔╝██║██╔══██║██╔══██╗ ██║ ║"
echo " ║ ███████╗██║ ╚═╝ ██║██║ ██║██║ ██║ ██║ ║"
echo " ║ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝ ║"
echo " ║ ║"
echo " ╠═══════════════════════════════════════════════════════════════════════════╣"
echo -e " ║ ${C_GOLD}${C_BOLD} Emulator / Nitro-V3 / Nitro-V3-Render${C_CYAN} ║"
echo -e " ║ ${C_WHITE}${C_BOLD} Updater by Remco — epicnabbo.nl${C_CYAN} ║"
echo " ╠═══════════════════════════════════════════════════════════════════════════╣"
echo -e " ║ ${C_DIM}Branch: ${C_CYAN}${NITRO_BRANCH}${C_DIM} (${#branches[@]} detected) │ $(date '+%H:%M')${C_CYAN} ║"
echo " ╚═══════════════════════════════════════════════════════════════════════════╝"
echo -e "${C_RESET}"
echo -e " ${C_BG_GREEN}${C_WHITE}${C_BOLD} UPDATE ${C_RESET}"
echo -e " ${C_GREEN}${C_BOLD}1)${C_RESET} ${E_ROCKET} Quick Update — branch: ${C_CYAN}${NITRO_BRANCH}${C_RESET}"
echo -e " ${C_GREEN}${C_BOLD}2)${C_RESET} ${E_BOLT} Full Update (choose branch)"
echo -e " ${C_GREEN}${C_BOLD}3)${C_RESET} ${E_GEAR} Emulator Only"
echo -e " ${C_GREEN}${C_BOLD}4)${C_RESET} ${E_GEAR} Nitro-V3 Client Only"
echo -e " ${C_GREEN}${C_BOLD}5)${C_RESET} ${E_GEAR} Renderer Only"
echo -e " ${C_GREEN}${C_BOLD}6)${C_RESET} ${E_GEAR} Configs Only"
echo ""
echo -e " ${C_BG_MAGENTA}${C_WHITE}${C_BOLD} TOOLS ${C_RESET}"
echo -e " ${C_MAGENTA}${C_BOLD}7)${C_RESET} ${E_GIT} Switch Branch"
echo -e " ${C_YELLOW}${C_BOLD}8)${C_RESET} ${E_DB} Database"
echo -e " ${C_YELLOW}${C_BOLD}9)${C_RESET} ${E_SHIELD} Backup / Restore"
echo -e " ${C_YELLOW}${C_BOLD}10)${C_RESET} ${E_WRENCH} Services"
echo -e " ${C_YELLOW}${C_BOLD}11)${C_RESET} ${E_CHART} Status"
echo -e " ${C_YELLOW}${C_BOLD}12)${C_RESET} ${E_HEART} Health Check"
echo -e " ${C_YELLOW}${C_BOLD}13)${C_RESET} ${E_BROOM} Clean"
echo -e " ${C_YELLOW}${C_BOLD}14)${C_RESET} ${E_EYE} Logs"
echo -e " ${C_YELLOW}${C_BOLD}15)${C_RESET} ${E_GEAR} Config"
echo ""
echo -e " ${C_BG_BLUE}${C_WHITE}${C_BOLD} ADVANCED ${C_RESET}"
echo -e " ${C_BLUE}${C_BOLD}16)${C_RESET} ${E_CONSOLE} Git Log"
echo -e " ${C_BLUE}${C_BOLD}17)${C_RESET} ${E_GLOBE} Compare Branches"
echo -e " ${C_BLUE}${C_BOLD}18)${C_RESET} ${E_CLOCK} Cron Scheduler"
echo ""
echo -e " ${C_RED}${C_BOLD}0)${C_RESET} Exit"
echo ""
echo -en " ${C_CYAN}${C_BOLD}Select [0-18]${C_RESET}: "
local choice; read -r choice
case "$choice" in
1) SELECTIVE_UPDATE=""; (cmd_update) || warn "Update had errors" ;;
2) SELECTIVE_UPDATE=""; (pick_branch_and_update) || warn "Update had errors" ;;
3) SELECTIVE_UPDATE="emulator"; (pick_branch_and_update) || warn "Update had errors" ;;
4) SELECTIVE_UPDATE="client"; (pick_branch_and_update) || warn "Update had errors" ;;
5) SELECTIVE_UPDATE="renderer"; (pick_branch_and_update) || warn "Update had errors" ;;
6) SELECTIVE_UPDATE="configs"; (pick_branch_and_update) || warn "Update had errors" ;;
7)
echo ""
local i=1
for b in "${branches[@]}"; do
local m=""; [ "$b" = "$NITRO_BRANCH" ] && m=" ${C_GREEN}*${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 -en "\n Branch [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 " Name: "; read -r NITRO_BRANCH
fi
ok "Branch: $NITRO_BRANCH" ;;
8) (cmd_database) ;;
9) (cmd_backup); echo ""; (cmd_backups) ;;
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_schedule) ;;
0|q|Q) echo -e "\n ${C_GREEN}Goodbye!${C_RESET}"; 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() {
echo ""
echo -e " ${C_GOLD}${C_BOLD}Emulator / Nitro-V3 / Nitro-V3-Render Updater${C_RESET}"
echo -e " ${C_DIM}by Remco — epicnabbo.nl v${SCRIPT_VERSION}${C_RESET}"
echo ""
echo -e " ${C_BOLD}USAGE:${C_RESET} $SCRIPT_NAME <command> [options]"
echo ""
echo -e " ${C_BOLD}COMMANDS:${C_RESET}"
echo -e " ${C_GREEN}interactive${C_RESET} Open menu (default)"
echo -e " ${C_GREEN}update${C_RESET} Run update (--only=emulator|client|renderer|configs)"
echo -e " ${C_GREEN}backup${C_RESET} Create backup"
echo -e " ${C_GREEN}restore${C_RESET} Restore backup"
echo -e " ${C_GREEN}backups${C_RESET} List backups"
echo -e " ${C_GREEN}status${C_RESET} System dashboard"
echo -e " ${C_GREEN}health${C_RESET} Health check"
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 config"
echo -e " ${C_GREEN}logs${C_RESET} View logs"
echo -e " ${C_GREEN}clean${C_RESET} Cache cleanup"
echo -e " ${C_GREEN}gitlog${C_RESET} Git history"
echo -e " ${C_GREEN}compare${C_RESET} Branch diff"
echo -e " ${C_GREEN}schedule${C_RESET} Cron setup"
echo -e " ${C_GREEN}help${C_RESET} This help"
echo ""
echo -e " ${C_BOLD}OPTIONS:${C_RESET}"
echo -e " ${C_CYAN}--branch, -b${C_RESET} 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}--dry-run, -n${C_RESET} Preview only"
echo -e " ${C_CYAN}--verbose, -v${C_RESET} Debug output"
echo -e " ${C_CYAN}--help, -h${C_RESET} Help"
echo -e " ${C_CYAN}--version, -V${C_RESET} Version"
echo ""
}
# =============================================================================
# MAIN
# =============================================================================
main() {
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" ;;
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 "v${SCRIPT_VERSION}"; exit 0 ;;
--dry-run|-n) DRY_RUN=true ;;
--verbose|-v) ;; # accepted but not used yet
--branch|-b) shift; NITRO_BRANCH="$1" ;;
--url|-u) shift; NITRO_SITE_URL="$1" ;;
--only=*) SELECTIVE_UPDATE="${1#*=}" ;;
-*) echo "Unknown: $1"; exit 1 ;;
*) [ -z "$cmd" ] && cmd="interactive" ;;
esac
shift
done
[ -z "$cmd" ] && cmd="interactive"
# Log redirect
exec > >(tee -a "$LOG_FILE") 2>&1
case "$cmd" in
interactive) cmd_interactive ;;
update) cmd_update ;;
backup) cmd_backup ;;
restore) cmd_restore ;;
backups) cmd_backups ;;
status) cmd_status ;;
health) cmd_health ;;
services) cmd_services ;;
database) cmd_database ;;
config) cmd_config ;;
logs) cmd_logs ;;
clean) cmd_clean ;;
gitlog) cmd_gitlog ;;
compare) cmd_compare ;;
schedule) cmd_schedule ;;
help) show_help ;;
esac
cursor_show
}
main "$@"