#!/bin/bash set -Eeo pipefail # ╔══════════════════════════════════════════════════════════════════════════════╗ # ║ SMaRT — Emulator / Octane / Octane-Renderer ║ # ║ Updater & Manager by Remco ║ # ╚══════════════════════════════════════════════════════════════════════════════╝ # ============================================================================= # CONSTANTS # ============================================================================= readonly SCRIPT_VERSION="8.0.4" readonly SCRIPT_BUILD="20260830" readonly SCRIPT_NAME="$(basename "$0")" readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" readonly START_TIME=$(date +%s) readonly LOCK_FILE="/tmp/${SCRIPT_NAME%.sh}.lock" # ============================================================================= # 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_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_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="⏱" E_DOCKER="🐳" E_CHECK="🔍" 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]" E_DOCKER="[D]" E_CHECK="[C]" fi # ============================================================================= # STATE # ============================================================================= SELECTIVE_UPDATE="" DRY_RUN=false HAD_UPDATES=false NITRO_BUILT=false EMULATOR_BUILT=false CMS_BUILT=false NGINX_CHANGED=false ERRORS=0 WARNINGS=0 UPDATED_REPOS=() LAST_BACKUP="" ROLLBACK_NEEDED=false BINARY_BACKUP_DIR="" NOTIFY_FAILED=false CONFIG_BACKUP_DIR="" MAVEN_OPTS="${MAVEN_OPTS:--Xmx2G}" # ============================================================================= # LOAD .ENV # ============================================================================= if [ -f "$SCRIPT_DIR/.env" ]; then set -a; . "$SCRIPT_DIR/.env"; set +a; fi if [ -z "${NITRO_DB_NAME:-}" ] && [ -n "${DATABASE_URL:-}" ]; then eval "$( python3 -c " import os, urllib.parse url = os.environ['DATABASE_URL'] parsed = urllib.parse.urlparse(url) auth = parsed.netloc.rsplit('@', 1)[0] if '@' in parsed.netloc else '' user, pw = (auth.split(':', 1) + ['', ''])[:2] host = parsed.hostname or '127.0.0.1' port = parsed.port or 3306 name = parsed.path.lstrip('/').split('?')[0] print(f'DB_USER={user!r}') print(f'DB_PASS={urllib.parse.unquote(pw)!r}') print(f'DB_HOST={host!r}') print(f'DB_PORT={port!r}') print(f'DB_NAME={name!r}') " 2>/dev/null || true )" 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:-${DB_NAME:-habbo}}" DB_HOST="${NITRO_DB_HOST:-${DB_HOST:-127.0.0.1}}" DB_PORT="${NITRO_DB_PORT:-${DB_PORT:-3306}}" DB_USER="${NITRO_DB_USER:-${DB_USER:-root}}" DB_PASS="${NITRO_DB_PASS:-${DB_PASS:-}}" EMULATOR_SERVICE="${NITRO_EMULATOR_SERVICE:-emulator}" EMULATOR_DIR="${NITRO_EMULATOR_PATH:-/var/www/emulator}" # Polaris-Emulator: the git repository root is $EMULATOR_DIR itself, while the # Maven project (pom.xml + target/) lives in the $EMULATOR_DIR/Emulator submodule. EMULATOR_REPO="${NITRO_EMULATOR_REPO:-$EMULATOR_DIR}" EMULATOR_MODULE="${NITRO_EMULATOR_MODULE:-$EMULATOR_DIR/Emulator}" SQL_DIR="${NITRO_SQL_DIR:-$EMULATOR_DIR/Database/Database Updates}" GAMEDATA_CONF_DIR="${NITRO_GAMEDATA_DIR:-/var/www/Gamedata/config}" NITRO_SRC_DIR="${NITRO_CLIENT_DIR:-/var/www/Octane/public/configuration}" BACKUP_DIR="${NITRO_BACKUP_DIR:-$EMULATOR_DIR/Database/Database Updates/backups}" NITRO_CLIENT="${NITRO_CLIENT_SRC:-/var/www/Octane}" NITRO_RENDERER="${NITRO_RENDERER_SRC:-/var/www/Octane-Renderer}" NITRO_BRANCH="${NITRO_BRANCH:-}" # CMS (EpicNextCMS / atom-nexst) — the Next.js backend that serves the hotel. # It lives next to this script. We never git-stash it automatically (it may # carry uncommitted local work); see NITRO_CMS_PULL to allow opt-in pulls. CMS_DIR="${NITRO_CMS_DIR:-$SCRIPT_DIR}" CMS_PM2_NAME="${NITRO_CMS_PM2_NAME:-epic-next-app}" CMS_PORT="${NITRO_CMS_PORT:-3002}" CMS_INSTALL_CMD="${NITRO_CMS_INSTALL_CMD:-pnpm install --frozen-lockfile}" CMS_BUILD_CMD="${NITRO_CMS_BUILD_CMD:-pnpm build}" CMS_HEALTH_RETRIES="${NITRO_CMS_HEALTH_RETRIES:-18}" CMS_HEALTH_INTERVAL="${NITRO_CMS_HEALTH_INTERVAL:-5}" # 1 = also git-pull the CMS before building (stashes local work if dirty). CMS_PULL="${NITRO_CMS_PULL:-0}" # Dedicated per-repo yarn cache folders. The renderer + client installs run in # parallel and yarn 1.22 has no cache locking, so a single shared cache gets # corrupted and node_modules is left incomplete (vite/pixi.js missing) while # yarn still exits 0. Isolating each repo's cache via --cache-folder prevents # that race entirely. # GitHub repository URLs for auto-setup (used when repos are missing) GIT_REPO_CLIENT="${NITRO_GIT_CLIENT:-https://github.com/duckietm/Octane.git}" GIT_REPO_RENDERER="${NITRO_GIT_RENDERER:-https://github.com/duckietm/Octane-Renderer.git}" YARN_CACHE_DIR="${NITRO_YARN_CACHE_DIR:-/var/www/.yarn-cache}" mkdir -p "$YARN_CACHE_DIR" 2>/dev/null || true MIN_DISK_GB="${NITRO_MIN_DISK_GB:-5}" HEALTH_RETRIES="${NITRO_HEALTH_RETRIES:-12}" HEALTH_INTERVAL="${NITRO_HEALTH_INTERVAL:-5}" NITRO_SSL_VERIFY="${NITRO_SSL_VERIFY:-OFF}" CONFIG_BACKUP_DIR="${SCRIPT_DIR}/storage/config-backups" mkdir -p "$CONFIG_BACKUP_DIR" 2>/dev/null || true mkdir -p "$BACKUP_DIR" 2>/dev/null || true NITRO_LANG="${NITRO_LANG:-en}" # Nginx site config(s) that serve /gamedata/. After every build/rebuild the # update script (re)adds an exact-match redirect so /gamedata/ points at # /gamedata/config/ instead of the raw directory listing. NGINX_SITE_CONFS=( "${NITRO_NGINX_SITE_CONF:-/etc/nginx/sites-enabled/cms.conf}" "${NITRO_NGINX_SITE_CONF_AVAILABLE:-/etc/nginx/sites-available/cms.conf}" ) MYSQL_CRED="-h $DB_HOST -P $DB_PORT -u $DB_USER --ssl-verify-server-cert=${NITRO_SSL_VERIFY}" [ -n "$DB_PASS" ] && MYSQL_CRED="$MYSQL_CRED --password=$DB_PASS" if command -v mariadb &>/dev/null; then MYSQL_CLIENT="mariadb"; MYSQL_DUMP="mariadb-dump" elif command -v mysql &>/dev/null; then MYSQL_CLIENT="mysql"; MYSQL_DUMP="mysqldump" else MYSQL_CLIENT="mariadb"; MYSQL_DUMP="mariadb-dump" fi # ============================================================================= # LANGUAGE / INTERNATIONALIZATION # ============================================================================= declare -A _T _t() { local key="$1"; shift local text="${_T[$key]:-$key}" for arg in "$@"; do text="${text/\%s/$arg}" done echo "$text" } load_language() { local lang="${NITRO_LANG:-en}" case "${lang,,}" in nl) _T=( # ── MENU ────────────────────────────────────────────── ["UPDATE"]="UPDATE" ["TOOLS"]="TOOLS" ["ADVANCED"]="ADVANCED" ["Quick Update — current branch"]="Snelle Update — huidige branch" ["Full Update (choose branch)"]="Volledige Update (kies branch)" ["Emulator Only"]="Alleen Emulator" ["Octane Client Only"]="Alleen Octane Client" ["Renderer Only"]="Alleen Renderer" ["Configs Only"]="Alleen Configuraties" ["Dev Mode (skip builds, only configs)"]="Dev Modus (skip builds, alleen configs)" ["Switch Branch"]="Wissel Branch" ["Database"]="Database" ["Backup / Restore"]="Backup / Herstel" ["Services"]="Services" ["Status"]="Status" ["Health Check"]="Gezondheidscheck" ["Clean"]="Opschonen" ["Logs"]="Logboeken" ["Config"]="Configuratie" ["Git Log"]="Git Logboek" ["Compare Branches"]="Vergelijk Branches" ["Cron Scheduler"]="Cron Planner" ["View Git Stashes"]="Bekijk Git Stashes" ["Exit"]="Afsluiten" ["Flyaway Repair"]="Vliegende Reparatie" ["Auto-setup missing repos"]="Automatisch ontbrekende repos installeren" ["Cleanup old Nitro dirs"]="Oude Nitro mappen opruimen" ["Rebuild Frontend"]="Frontend Herbouwen" ["Rebuild Emulator"]="Emulator Herbouwen" ["Select [0-24]"]="Selecteer [0-24]" ["Fix Client Links (icons/furniture/gamedata)"]="Fixeer Client Links (icons/meubels/gamedata)" ["Press Enter to continue..."]="Druk op Enter om door te gaan..." ["Goodbye!"]="Tot ziens!" # ── HELP ────────────────────────────────────────────── ["SMaRT — Emulator / Octane / Octane-Renderer Updater & Manager"]="SMaRT — Emulator / Octane / Octane-Renderer Updater & Beheerder" ["USAGE:"]="GEBRUIK:" ["COMMANDS:"]="COMMANDO'S:" ["Open menu (default)"]="Open menu (standaard)" ["Run update"]="Voer update uit" ["Create database backup"]="Maak databasebackup" ["Restore from backup"]="Herstel vanuit backup" ["List backups"]="Toon backups" ["System dashboard"]="Systeemdashboard" ["Health check"]="Gezondheidscheck" ["Service manager"]="Servicebeheerder" ["Database tools"]="Database tools" ["Show config"]="Toon configuratie" ["View logs"]="Bekijk logboeken" ["Cache cleanup"]="Cache opschonen" ["Git history"]="Git geschiedenis" ["Branch diff"]="Branch vergelijking" ["Cron setup"]="Cron instellen" ["View git stashes"]="Bekijk git stashes" ["Branch (auto-detects)"]="Branch (auto-detectie)" ["Site URL"]="Site URL" ["emulator|client|renderer|configs|dev"]="emulator|client|renderer|configs|dev" ["Preview only (no changes)"]="Alleen voorbeeld (geen wijzigingen)" ["Show this help"]="Toon deze hulp" ["Show version"]="Toon versie" ["OPTIONS:"]="OPTIES:" ["Unknown:"]="Onbekend:" # ── STEP HEADERS ────────────────────────────────────── ["Update & Build Emulator"]="Update & Bouw Emulator" ["Update Octane-Renderer"]="Update Octane-Renderer" ["Update & Build Octane"]="Update & Bouw Octane" ["Sync Configurations"]="Synchroniseer Configuraties" ["Cleanup"]="Opschonen" ["Set Permissions"]="Stel Permissies in" ["Restart Services"]="Herstart Services" ["System Diagnostics"]="Systeemdiagnose" ["Database Backup"]="Databasebackup" ["Database Restore"]="Database Herstel" ["Backup Inventory"]="Backup Inventaris" ["System Status"]="Systeemstatus" ["Health Check"]="Gezondheidscheck" ["Service Manager"]="Servicebeheer" ["Database Tools"]="Database Tools" ["Configuration"]="Configuratie" ["Log Viewer"]="Logboekweergave" ["Cache Cleanup"]="Cache Opschonen" ["Git Commit History"]="Git Commit Geschiedenis" ["Branch Comparison"]="Branch Vergelijking" ["Cron Scheduler"]="Cron Planner" ["Git Stashes"]="Git Stashes" # ── PROMPTS / CONFIRM ───────────────────────────────── ["Start update?"]="Update starten?" ["Continue?"]="Doorgaan?" ["Restore %s? OVERWRITES database!"]="Herstel %s? OVERSCHRIJFT database!" ["Select [1-%s]"]="Selecteer [1-%s]" ["Branch [1-%s]"]="Branch [1-%s]" ["Name:"]="Naam:" ["Custom"]="Aangepast" ["Select:"]="Selecteer:" ["SQL:"]="SQL:" ["Minute [0-59]:"]="Minuut [0-59]:" ["Hour [0-23]:"]="Uur [0-23]:" ["Branch A:"]="Branch A:" ["Branch B:"]="Branch B:" ["(current)"]="(huidig)" # ── INFO / OK / WARN / FAIL ────────────────────────── ["All commands available"]="Alle commando's beschikbaar" ["All directories exist"]="Alle mappen bestaan" ["Database connected"]="Database verbonden" ["Emulator: already up to date"]="Emulator: al up-to-date" ["Renderer: already up to date"]="Renderer: al up-to-date" ["Octane: already up to date"]="Octane: al up-to-date" ["Emulator JAR backed up"]="Emulator JAR geback-upt" ["Octane dist backed up"]="Octane dist geback-upt" ["Emulator JAR restored"]="Emulator JAR hersteld" ["Octane dist restored"]="Octane dist hersteld" ["Renderer dependencies installed"]="Renderer afhankelijkheden geïnstalleerd" ["No repo updates; skipping parallel builds"]="Geen repo-updates; parallel bouwen overgeslagen" ["Dev mode: skipping builds, syncing configs only"]="Dev modus: builds overgeslagen, alleen configs synchroniseren" ["Config back-up: %s"]="Config-backup: %s" ["%s config file(s) synced"]="%s configuratiebestand(en) gesynchroniseerd" ["All config files valid"]="Alle configuratiebestanden geldig" ["%s config(s) invalid"]="%s configuratie(s) ongeldig" ["All config URLs synced"]="Alle config-URL's gesynchroniseerd" ["Redis cache flushed"]="Redis cache geleegd" ["Permissions set"]="Permissies ingesteld" ["Jar: %s"]="Jar: %s" ["Launch script updated"]="Startscript bijgewerkt" ["%s restarted"]="%s herstart" ["Emulator health check passed (service up + port listening)"]="Emulator gezondheidscheck geslaagd (service actief + poort luistert)" ["Emulator not healthy after %s retries"]="Emulator niet gezond na %s pogingen" ["Nginx reloaded"]="Nginx herladen" ["Redis flushed"]="Redis geleegd" ["Cleanup done (%s logs removed)"]="Opschonen voltooid (%s logs verwijderd)" ["Old config backups pruned"]="Oude config-backups opgeruimd" ["Dry-run: would update repos, run DB backups, build, sync configs, cleanup, restart services"]="Proefrun: zou repos updaten, DB-backups maken, bouwen, configs synchroniseren, opschonen, services herstarten" ["Backup: %s — %s"]="Backup: %s — %s" ["Restored"]="Hersteld" ["Cancelled"]="Geannuleerd" ["Started"]="Gestart" ["Stopped"]="Gestopt" ["Restarted"]="Herstart" ["Yarn cleaned"]="Yarn opgeschoond" ["Next.js cache cleared"]="Next.js cache gewist" ["Logs cleaned"]="Logs opgeschoond" ["Full cleanup"]="Volledig opgeschoond" ["Docker pruned"]="Docker opgeruimd" ["Optimized"]="Geoptimaliseerd" ["Old Maven artifacts cleaned"]="Oude Maven artefacten opgeruimd" ["Scheduled daily 03:00"]="Ingesteld dagelijks 03:00" ["Scheduled daily 04:00"]="Ingesteld dagelijks 04:00" ["Scheduled weekly"]="Ingesteld wekelijks" ["Scheduled %s"]="Ingesteld %s" ["Removed"]="Verwijderd" ["Branch: %s"]="Branch: %s" ["No backup dir"]="Geen backup-map" ["No backups"]="Geen backups" ["No logs"]="Geen logboeken" ["No log dir"]="Geen logboek-map" ["No stashes found"]="Geen stashes gevonden" ["Not a git repo"]="Geen git-repository" ["Invalid"]="Ongeldig" ["Invalid selection"]="Ongeldige selectie" ["%s updates, %s warnings"]="%s updates, %s waarschuwingen" ["Update had errors"]="Update had fouten" ["Dev mode had errors"]="Dev modus had fouten" ["Invalid, using: %s"]="Ongeldig, gebruik: %s" ["Backup: %s — %s"]="Backup: %s — %s" ["Script:"]="Script:" ["Bash syntax"]="Bash syntax" ["Configs:"]="Configuraties:" ["Config dir"]="Config map" ["All JSON valid"]="Alle JSON geldig" ["JSON errors:"]="JSON fouten:" ["Gamedata config"]="Gamedata config" ["Backup dir"]="Backup map" ["Network:"]="Netwerk:" ["Nginx config"]="Nginx configuratie" ["Redis PING"]="Redis PING" ["Site reachable"]="Site bereikbaar" ["Emulator port open"]="Emulator poort open" ["Emulator port closed"]="Emulator poort gesloten" ["Databases:"]="Databases:" ["MySQL connect"]="MySQL verbinding" ["Disk:"]="Schijf:" ["Memory:"]="Geheugen:" ["Load:"]="Belasting:" ["Commands:"]="Commando's:" ["Bashisms"]="Bashisms" ["Tables:"]="Tabellen:" # ── DIE MESSAGES ────────────────────────────────────── ["Another instance is already running (lock: %s)"]="Er draait al een andere instantie (lock: %s)" ["Required commands missing"]="Vereiste commando's ontbreken" ["Missing: %s"]="Ontbreekt: %s" ["Missing dir: %s"]="Map ontbreekt: %s" ["Only %sGB free (min %sGB)"]="Slechts %sGB vrij (min %sGB)" ["Low disk: %sGB free"]="Weinig schijfruimte: %sGB vrij" ["%sGB disk available"]="%sGB schijf beschikbaar" ["DB connection failed"]="Databaseverbinding mislukt" ["Maven build failed (see: %s)"]="Maven build mislukt (zie: %s)" ["No jar found"]="Geen jar gevonden" ["yarn install failed"]="yarn install mislukt" ["yarn build failed (see: %s)"]="yarn build mislukt (zie: %s)" ["Build failure in: %s"]="Buildfout in: %s" ["Backup failed"]="Backup mislukt" ["Restore failed"]="Herstel mislukt" # ── SECTIONS ────────────────────────────────────────── [" UPDATE "]=" UPDATE " [" TOOLS "]=" TOOLS " [" ADVANCED "]=" GEAVANCEERD " # ── SUBMENUS ────────────────────────────────────────── ["Start Emulator"]="Start Emulator" ["Stop Emulator"]="Stop Emulator" ["Restart Emulator"]="Herstart Emulator" ["Start Nginx"]="Start Nginx" ["Restart Nginx"]="Herstart Nginx" ["Status All"]="Status Alles" ["Emulator Logs"]="Emulator Logs" ["Emulator Journal"]="Emulator Journal" ["Return"]="Terug" ["DB Info"]="DB Info" ["List Tables"]="Toon Tabellen" ["Table Sizes"]="Tabelgroottes" ["Optimize"]="Optimaliseer" ["Custom Query"]="Eigen Query" ["Active Users"]="Actieve Gebruikers" ["Processlist"]="Proceslijst" ["Yarn"]="Yarn" ["Next.js"]="Next.js" ["Redis"]="Redis" ["Everything"]="Alles" ["Docker"]="Docker" ["Maven"]="Maven" ["Daily 03:00"]="Dagelijks 03:00" ["Daily 04:00"]="Dagelijks 04:00" ["Weekly Sun 03:00"]="Wekelijks Zo 03:00" ["Custom time"]="Eigen tijd" ["Remove"]="Verwijderen" ["Last 20 commits on %s (ahead: %s):"]="Laatste 20 commits op %s (vooruit: %s):" ["%s is %s ahead, %s behind %s"]="%s is %s voor, %s achter op %s" ["Current:"]="Huidig:" ["none"]="geen" # ── BOX LABELS ──────────────────────────────────────── ["SERVER"]="SERVER" ["RESOURCES"]="BRONNEN" ["SERVICES"]="SERVICES" ["GIT REPOS"]="GIT REPOS" ["SCORE"]="SCORE" ["DB INFO"]="DB INFO" ["CONFIGURATION"]="CONFIGURATIE" ["Host:"]="Host:" ["OS:"]="Besturingssysteem:" ["Uptime:"]="Uptime:" ["CPU:"]="CPU:" ["Memory:"]="Geheugen:" ["Disk:"]="Schijf:" ["Database:"]="Database:" ["Passed:"]="Geslaagd:" ["Failed:"]="Mislukt:" ["Warnings:"]="Waarschuwingen:" ["Name:"]="Naam:" ["Size:"]="Grootte:" ["Tables:"]="Tabellen:" ["Site:"]="Site:" ["Branch:"]="Branch:" ["Emulator:"]="Emulator:" ["Octane:"]="Octane:" ["Renderer:"]="Renderer:" ["Service:"]="Service:" ["Socket:"]="Socket:" ["API:"]="API:" ["Gamedata:"]="Gamedata:" ["Min Disk:"]="Min Schijf:" # ── SPINNER ─────────────────────────────────────────── ["Building emulator (parallel)..."]="Emulator aan het bouwen (parallel)..." ["Installing deps..."]="Afhankelijkheden installeren..." ["Building frontend..."]="Frontend aan het bouwen..." ["Backing up..."]="Backup aan het maken..." ["Restoring..."]="Aan het herstellen..." ["Optimizing..."]="Aan het optimaliseren..." # ── NOTIFY ────────────────────────────────────────── ["SUCCESS"]="GELUKT" ["FAILED"]="MISLUKT" ["Completed in %s (%s warnings)"]="Voltooid in %s (%s waarschuwingen)" # ── OTHER ─────────────────────────────────────────── ["Total: %s backups — %s"]="Totaal: %s backups — %s" ["FILENAME"]="BESTANDSNAAM" ["SIZE"]="GROOTTE" ["All config files valid"]="Alle configuratiebestanden geldig" # ── SUMMARY ────────────────────────────────────────── ["UPDATE COMPLETED SUCCESSFULLY"]="UPDATE SUCCESVOL VOLTOOID" ["COMPLETED WITH %s ERROR(S)"]="VOLTOOID MET %s FOUT(EN)" ["Duration:"]="Duur:" ["Updates:"]="Updates:" ["Octane build:"]="Octane build:" ["Warnings:"]="Waarschuwingen:" ["Repos:"]="Repos:" ["Yes"]="Ja" ["No"]="Nee" # ── NEW (v8.1) ──────────────────────────────────────── ["Update & Build CMS"]="Update & Bouw CMS" ["Environment %s is up to date"]="Omgeving %s is up-to-date" ["%s new variable(s) added to %s"]="%s nieuwe variabele(n) toegevoegd aan %s" ["No .env.example in %s — skipping env sync"]="Geen .env.example in %s — env-sync overgeslagen" ["No .env file in %s"]="Geen .env-bestand in %s" ["Duplicate key(s) in %s: %s"]="Dubbele sleutel(s) in %s: %s" ["Missing variable(s) in %s: %s"]="Ontbrekende variabele(n) in %s: %s" ["Invalid .env — fix before next startup"]="Ongeldig .env — corrigeer vóór de volgende start" ["CMS build complete"]="CMS-build voltooid" ["CMS: already up to date"]="CMS: al up-to-date" ["CMS health check passed (port %s listening)"]="CMS gezondheidscheck geslaagd (poort %s luistert)" ["CMS not healthy after %s retries (port %s)"]="CMS niet gezond na %s pogingen (poort %s)" ["%s restarted"]="%s herstart" ["%s already healthy — no restart needed"]="%s al gezond — herstart niet nodig" ["CMS unhealthy — restarting %s"]="CMS niet gezond — %s opnieuw starten" ["No %s/package.json — skipping CMS"]="Geen %s/package.json — CMS overgeslagen" ["CMS git pull disabled (NITRO_CMS_PULL=0) — building from current tree"]="CMS git-pull uitgeschakeld (NITRO_CMS_PULL=0) — bouwen vanuit huidige boom" ["Environment sync & validation..."]="Omgeving synchroniseren & valideren..." ["No .env.example in"]="Geen .env.example in" ) ;; fr) _T=( # ── MENU ────────────────────────────────────────────── ["UPDATE"]="MISE À JOUR" ["TOOLS"]="OUTILS" ["ADVANCED"]="AVANCÉ" ["Quick Update — current branch"]="Mise à jour rapide — branche actuelle" ["Full Update (choose branch)"]="Mise à jour complète (choisir branche)" ["Emulator Only"]="Émulateur uniquement" ["Octane Client Only"]="Client Octane uniquement" ["Renderer Only"]="Rendu uniquement" ["Configs Only"]="Configs uniquement" ["Dev Mode (skip builds, only configs)"]="Mode Dev (sans builds, configs seulement)" ["Switch Branch"]="Changer de branche" ["Database"]="Base de données" ["Backup / Restore"]="Sauvegarde / Restauration" ["Services"]="Services" ["Status"]="Statut" ["Health Check"]="Vérification de santé" ["Clean"]="Nettoyer" ["Logs"]="Journaux" ["Config"]="Configuration" ["Git Log"]="Journal Git" ["Compare Branches"]="Comparer les branches" ["Cron Scheduler"]="Planificateur Cron" ["View Git Stashes"]="Voir les stashs Git" ["Exit"]="Quitter" ["Flyaway Repair"]="Réparation Flyaway" ["Auto-setup missing repos"]="Installation auto des repos manquants" ["Cleanup old Nitro dirs"]="Nettoyer les anciens répertoires Nitro" ["Rebuild Frontend"]="Reconstruire le Frontend" ["Rebuild Emulator"]="Reconstruire l'Emulateur" ["Select [0-24]"]="Sélectionner [0-24]" ["Fix Client Links (icons/furniture/gamedata)"]="Corriger les liens Client (icônes/meubles/gamedata)" ["Press Enter to continue..."]="Appuyez sur Entrée pour continuer..." ["Goodbye!"]="Au revoir !" ["Start update?"]="Lancer la mise à jour ?" ["Continue?"]="Continuer ?" # ── HELP ────────────────────────────────────────────── ["USAGE:"]="UTILISATION :" ["COMMANDS:"]="COMMANDES :" ["OPTIONS:"]="OPTIONS :" # ── STEP HEADERS ────────────────────────────────────── ["Update & Build Emulator"]="Mise à jour & Construction Émulateur" ["Update Octane-Renderer"]="Mise à jour Octane-Renderer" ["Update & Build Octane"]="Mise à jour & Construction Octane" ["Sync Configurations"]="Synchroniser les configurations" ["Cleanup"]="Nettoyage" ["Set Permissions"]="Définir les permissions" ["Restart Services"]="Redémarrer les services" ["System Diagnostics"]="Diagnostics système" ["Database Backup"]="Sauvegarde base de données" ["Database Restore"]="Restauration base de données" ["Backup Inventory"]="Inventaire des sauvegardes" ["System Status"]="Statut du système" ["Health Check"]="Vérification de santé" ["Service Manager"]="Gestionnaire de services" ["Database Tools"]="Outils base de données" ["Configuration"]="Configuration" ["Log Viewer"]="Visualisateur de journaux" ["Cache Cleanup"]="Nettoyage du cache" ["Git Commit History"]="Historique des commits Git" ["Branch Comparison"]="Comparaison de branches" ["Cron Scheduler"]="Planificateur Cron" ["Git Stashes"]="Stashs Git" ["Missing: %s"]="Manquant : %s" ["Missing dir: %s"]="Répertoire manquant : %s" ["All commands available"]="Toutes les commandes disponibles" ["All directories exist"]="Tous les répertoires existent" ["Database connected"]="Base de données connectée" ["Maven build failed (see: %s)"]="Build Maven échoué (voir : %s)" ["yarn install failed"]="yarn install échoué" ["yarn build failed (see: %s)"]="yarn build échoué (voir : %s)" ["Jar: %s"]="Jar : %s" ["Backup: %s — %s"]="Sauvegarde : %s — %s" ["Restored"]="Restauré" ["Cancelled"]="Annulé" ["Invalid"]="Invalide" ["Invalid selection"]="Sélection invalide" ["Scheduled daily 03:00"]="Planifié quotidien 03:00" ["Scheduled daily 04:00"]="Planifié quotidien 04:00" ["Scheduled weekly"]="Planifié hebdomadaire" ["Removed"]="Supprimé" ["Return"]="Retour" ["none"]="aucun" ["Yes"]="Oui" ["No"]="Non" ["Goodbye!"]="Au revoir !" ) ;; es) _T=( # ── MENU ────────────────────────────────────────────── ["UPDATE"]="ACTUALIZAR" ["TOOLS"]="HERRAMIENTAS" ["ADVANCED"]="AVANZADO" ["Quick Update — current branch"]="Actualización rápida — rama actual" ["Full Update (choose branch)"]="Actualización completa (elegir rama)" ["Emulator Only"]="Solo Emulador" ["Octane Client Only"]="Solo Cliente Octane" ["Renderer Only"]="Solo Renderizador" ["Configs Only"]="Solo Configuraciones" ["Dev Mode (skip builds, only configs)"]="Modo Dev (sin builds, solo configs)" ["Switch Branch"]="Cambiar Rama" ["Database"]="Base de Datos" ["Backup / Restore"]="Respaldo / Restaurar" ["Services"]="Servicios" ["Status"]="Estado" ["Health Check"]="Verificación de Salud" ["Clean"]="Limpiar" ["Logs"]="Registros" ["Config"]="Configuración" ["Git Log"]="Registro Git" ["Compare Branches"]="Comparar Ramas" ["Cron Scheduler"]="Programador Cron" ["View Git Stashes"]="Ver Stashes Git" ["Exit"]="Salir" ["Flyaway Repair"]="Reparación Flyaway" ["Auto-setup missing repos"]="Auto-instalar repos faltantes" ["Cleanup old Nitro dirs"]="Limpiar directorios Nitro antiguos" ["Rebuild Frontend"]="Reconstruir Frontend" ["Rebuild Emulator"]="Reconstruir Emulador" ["Select [0-24]"]="Seleccionar [0-24]" ["Fix Client Links (icons/furniture/gamedata)"]="Arreglar enlaces del Cliente (iconos/muebles/gamedata)" ["Press Enter to continue..."]="Presione Enter para continuar..." ["Goodbye!"]="¡Hasta luego!" ["Start update?"]="¿Iniciar actualización?" ["Continue?"]="¿Continuar?" ["Missing: %s"]="Falta: %s" ["Missing dir: %s"]="Directorio faltante: %s" ["All commands available"]="Todos los comandos disponibles" ["All directories exist"]="Todos los directorios existen" ["Database connected"]="Base de datos conectada" ["Maven build failed (see: %s)"]="Build Maven falló (ver: %s)" ["yarn install failed"]="yarn install falló" ["yarn build failed (see: %s)"]="yarn build falló (ver: %s)" ["Goodbye!"]="¡Hasta luego!" ["Return"]="Volver" ["none"]="ninguno" ["Yes"]="Sí" ["No"]="No" ) ;; de) _T=( # ── MENU ────────────────────────────────────────────── ["UPDATE"]="AKTUALISIEREN" ["TOOLS"]="WERKZEUGE" ["ADVANCED"]="ERWEITERT" ["Quick Update — current branch"]="Schnellupdate — aktueller Branch" ["Full Update (choose branch)"]="Vollständiges Update (Branch wählen)" ["Emulator Only"]="Nur Emulator" ["Octane Client Only"]="Nur Octane Client" ["Renderer Only"]="Nur Renderer" ["Configs Only"]="Nur Konfigurationen" ["Dev Mode (skip builds, only configs)"]="Dev-Modus (ohne Builds, nur Konfigs)" ["Switch Branch"]="Branch wechseln" ["Database"]="Datenbank" ["Backup / Restore"]="Sicherung / Wiederherstellung" ["Services"]="Dienste" ["Status"]="Status" ["Health Check"]="Gesundheitsprüfung" ["Clean"]="Bereinigen" ["Logs"]="Protokolle" ["Config"]="Konfiguration" ["Git Log"]="Git-Verlauf" ["Compare Branches"]="Branches vergleichen" ["Cron Scheduler"]="Cron-Planer" ["View Git Stashes"]="Git-Stashes anzeigen" ["Exit"]="Beenden" ["Flyaway Repair"]="Flyaway-Reparatur" ["Auto-setup missing repos"]="Fehlende Repos automatisch einrichten" ["Cleanup old Nitro dirs"]="Alte Nitro-Verzeichnisse aufräumen" ["Rebuild Frontend"]="Frontend neu bauen" ["Rebuild Emulator"]="Emulator neu bauen" ["Select [0-24]"]="Auswählen [0-24]" ["Fix Client Links (icons/furniture/gamedata)"]="Client-Links reparieren (Icons/Möbel/Gamedata)" ["Press Enter to continue..."]="Drücken Sie Enter zum Fortfahren..." ["Goodbye!"]="Auf Wiedersehen!" ["Start update?"]="Update starten?" ["Continue?"]="Fortfahren?" ["Missing: %s"]="Fehlt: %s" ["Missing dir: %s"]="Verzeichnis fehlt: %s" ["All commands available"]="Alle Befehle verfügbar" ["All directories exist"]="Alle Verzeichnisse vorhanden" ["Database connected"]="Datenbank verbunden" ["Maven build failed (see: %s)"]="Maven-Build fehlgeschlagen (siehe: %s)" ["yarn install failed"]="yarn install fehlgeschlagen" ["yarn build failed (see: %s)"]="yarn build fehlgeschlagen (siehe: %s)" ["Goodbye!"]="Auf Wiedersehen!" ["Return"]="Zurück" ["none"]="keine" ["Yes"]="Ja" ["No"]="Nein" ) ;; it) _T=( # ── MENU ────────────────────────────────────────────── ["UPDATE"]="AGGIORNA" ["TOOLS"]="STRUMENTI" ["ADVANCED"]="AVANZATE" ["Quick Update — current branch"]="Aggiornamento rapido — ramo attuale" ["Full Update (choose branch)"]="Aggiornamento completo (scegli ramo)" ["Emulator Only"]="Solo Emulatore" ["Octane Client Only"]="Solo Client Octane" ["Renderer Only"]="Solo Renderer" ["Configs Only"]="Solo Configurazioni" ["Dev Mode (skip builds, only configs)"]="Modalità Dev (senza build, solo configs)" ["Switch Branch"]="Cambia Ramo" ["Database"]="Database" ["Backup / Restore"]="Backup / Ripristino" ["Services"]="Servizi" ["Status"]="Stato" ["Health Check"]="Controllo Salute" ["Clean"]="Pulisci" ["Logs"]="Registri" ["Config"]="Configurazione" ["Git Log"]="Registro Git" ["Compare Branches"]="Confronta Rami" ["Cron Scheduler"]="Pianificatore Cron" ["View Git Stashes"]="Vedi Stash Git" ["Exit"]="Esci" ["Flyaway Repair"]="Riparazione Flyaway" ["Auto-setup missing repos"]="Auto-installazione repo mancanti" ["Cleanup old Nitro dirs"]="Pulisci vecchie cartelle Nitro" ["Rebuild Frontend"]="Ricostruisci Frontend" ["Rebuild Emulator"]="Ricostruisci Emulatore" ["Select [0-24]"]="Seleziona [0-24]" ["Fix Client Links (icons/furniture/gamedata)"]="Correggi link Client (icone/mobili/gamedata)" ["Press Enter to continue..."]="Premi Invio per continuare..." ["Goodbye!"]="Arrivederci!" ["Start update?"]="Avviare aggiornamento?" ["Continue?"]="Continuare?" ["Missing: %s"]="Mancante: %s" ["Missing dir: %s"]="Directory mancante: %s" ["All commands available"]="Tutti i comandi disponibili" ["All directories exist"]="Tutte le directory esistono" ["Database connected"]="Database connesso" ["Maven build failed (see: %s)"]="Build Maven fallita (vedi: %s)" ["yarn install failed"]="yarn install fallito" ["yarn build failed (see: %s)"]="yarn build fallito (vedi: %s)" ["Goodbye!"]="Arrivederci!" ["Return"]="Indietro" ["none"]="nessuno" ["Yes"]="Sì" ["No"]="No" ) ;; esac } # ============================================================================= # LOCKFILE # ============================================================================= acquire_lock() { exec 9>"$LOCK_FILE" if ! flock -n 9; then die "Another instance is already running (lock: %s)" "$LOCK_FILE" fi } release_lock() { # Parallel job subshells inherit fd 9; releasing it here would drop the # main process's lock while the other jobs are still running. [ -n "$PARALLEL_JOB" ] && return 0 flock -u 9 2>/dev/null || true rm -f "$LOCK_FILE" 2>/dev/null || true } # ============================================================================= # 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() { local m; m=$(_t "$1" "${@:2}"); echo -e " ${C_BLUE}${E_ARROW}${C_RESET} $m"; log_write "INFO" "$m"; } ok() { local m; m=$(_t "$1" "${@:2}"); echo -e " ${C_GREEN}${E_OK}${C_RESET} $m"; log_write "OK" "$m"; } warn() { local m; m=$(_t "$1" "${@:2}"); echo -e " ${C_YELLOW}${E_WARN}${C_RESET} $m"; WARNINGS=$((WARNINGS + 1)); log_write "WARN" "$m"; } fail() { local m; m=$(_t "$1" "${@:2}"); echo -e " ${C_RED}${E_FAIL}${C_RESET} $m"; log_write "FAIL" "$m"; } die() { local m; m=$(_t "$1" "${@:2}") echo "" echo -e " ${C_BG_RED}${C_WHITE}${C_BOLD} $(_t "ERROR") ${C_RESET} ${C_RED}$m${C_RESET}" log_write "FATAL" "$m" cleanup_notify_failure "$m" release_lock 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"; } repeat() { local ch="${1:-=}" n="${2:-1}"; printf '%*s' "$n" '' | tr ' ' "$ch"; } list_sorted() { local d="$1" p="$2" m="${3:-20}"; find "$d" -name "$p" -printf '%T@ %p\n' 2>/dev/null | sort -rn | head -n "$m" | sed 's/^[0-9.]* //'; } service_active() { local svc="$1" if command -v systemctl &>/dev/null; then systemctl is-active "$svc" &>/dev/null elif command -v service &>/dev/null; then service "$svc" status &>/dev/null else pgrep -x "$svc" &>/dev/null fi } # Redis protocol server name: classic redis-server or Dragonfly (drop-in # replacement that also speaks the redis protocol, e.g. in production hotels). redis_backend() { if service_active redis-server; then echo redis-server elif service_active dragonfly; then echo dragonfly else echo redis-server; fi } redis_active() { service_active redis-server || service_active dragonfly } format_size() { local bytes="${1:-0}" if command -v numfmt &>/dev/null; then numfmt --to=iec "$bytes" 2>/dev/null || echo "${bytes}B" else echo "${bytes}B" fi } 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 " "$(_t "$msg")" i=$(( (i + 1) % ${#frames[@]} )) sleep 0.1 done ) & SPINNER_PID=$! } spinner_stop() { local st="${1:-ok}" if [ -n "$SPINNER_PID" ]; then kill "$SPINNER_PID" 2>/dev/null || true wait "$SPINNER_PID" 2>/dev/null || true SPINNER_PID="" fi 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} — $(_t "$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}$(_t "$msg")${C_RESET} $hint " local reply; read -rt 30 reply; reply="${reply:-$default}" [[ "$reply" =~ ^[Yy] ]] } dry_run() { if [ "$DRY_RUN" = true ]; then echo -e " ${C_DIM}[DRY-RUN]${C_RESET} $*" return 0 fi return 1 } box_top() { local title="${1:-}" width="${2:-60}" if [ -n "$title" ]; then local tt; tt=$(_t "$title") local pad=$((width - ${#tt} - 6)); [ $pad -lt 0 ] && pad=0 printf "${C_CYAN}╔═══ ${C_BOLD}%s${C_RESET}${C_CYAN}" "$tt" repeat '═' "$pad" echo -e "╗${C_RESET}" else printf "${C_CYAN}╔"; repeat '═' "$width"; echo -e "╗${C_RESET}" fi } box_kv() { local key="${1}" val="${2}" width="${3:-60}" local kk; kk=$(_t "$key") local ck=$(echo -e "$kk" | 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" "$kk" $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}╚"; repeat '═' "$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))) N_STATUS="$status" N_MESSAGE="$message" N_DURATION="$ef" \ python3 -c " import json, os payload = { 'text': '[Octane Update] ' + os.environ['N_STATUS'], 'attachments': [{ 'color': 'good' if os.environ['N_STATUS'] == 'SUCCESS' else 'danger', 'fields': [ {'title': 'Status', 'value': os.environ['N_MESSAGE'], 'short': True}, {'title': 'Duration', 'value': os.environ['N_DURATION'], 'short': True} ] }] } print(json.dumps(payload)) " | curl -s -o /dev/null -X POST "$NOTIFY_URL" -H "Content-Type: application/json" -d @- 2>/dev/null || true } cleanup_notify_failure() { $NOTIFY_FAILED && return; NOTIFY_FAILED=true; notify "FAILED" "$1"; } cleanup_on_exit() { local ec=$? # Parallel job subshells must never run the full cleanup (rollback, # lock release, notify); only the main process owns those. Their status # is captured by parallel_wait instead. if [ -n "$PARALLEL_JOB" ]; then return $ec; fi cursor_show; release_lock if [ $ec -ne 0 ] && [ "$ROLLBACK_NEEDED" = true ]; then echo -e "\n ${C_BG_RED}${C_WHITE}${C_BOLD} $(_t "ROLLBACK") ${C_RESET}" echo -e " $(_t "Restoring config back-ups from") $CONFIG_BACKUP_DIR/rollback..." if [ -d "$CONFIG_BACKUP_DIR/rollback" ]; then cp -r "$CONFIG_BACKUP_DIR/rollback/"* "$NITRO_SRC_DIR/" 2>/dev/null || true ok "Config files restored from rollback" fi restore_binaries fi # Preserve the real exit status: the last command in an EXIT trap defines # the script's exit code, so never let a guard/notify call overwrite it. if [ $ec -ne 0 ]; then cleanup_notify_failure "Exit code $ec" return $ec fi return 0 } trap cleanup_on_exit EXIT trap 'release_lock; cursor_show; exit 1' SIGINT SIGTERM # ============================================================================= # GIT HELPERS # ============================================================================= detect_branches() { local repos=("$EMULATOR_REPO" "$NITRO_CLIENT" "$NITRO_RENDERER") for repo in "${repos[@]}"; do [ -d "$repo/.git" ] || continue ( cd "$repo" 2>/dev/null || exit git branch -r 2>/dev/null | grep -v '\->' | sed 's/^[[:space:]]*//; s/^origin\///' | grep -v '^HEAD$' ) done | sort -u } load_branches() { local -n _arr="$1" readarray -t _arr < <(detect_branches) } detect_best_branch() { local repo="$1" preferred="$2" ( cd "$repo" 2>/dev/null || { echo "main"; exit 0; } local cur; cur=$(git branch --show-current 2>/dev/null || echo "") for v in "$preferred" "${preferred^}" "$cur" "main" "master"; do [ -n "$v" ] && git rev-parse --verify "$v" &>/dev/null && { echo "$v"; exit 0; } done echo "${cur:-main}" ) } git_update() { local repo="$1" branch="$2" local name; name=$(basename "$repo") local rc=1 ( cd "$repo" || { warn "Cannot access $repo"; exit 2; } # Only stash TRACKED changes (never untracked files — runtime config # data in public/configuration/ must stay in place) and only when the # working tree is dirty, to keep the pull fast-forwardable. The stash is # left in the repo (recoverable via `git stash list`), never dropped. if ! git diff --quiet 2>/dev/null || ! git diff --cached --quiet 2>/dev/null; then git stash push -m "auto-update $(date +%F-%T)" 2>/dev/null \ && warn "Local tracked changes in $(basename "$repo") stashed (see 'git stash list')" fi 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")"; exit 2; } local pulled=1 for attempt in 1 2 3; do if git pull --ff-only origin "$rb" 2>/dev/null; then pulled=0; break elif git pull --ff-only 2>/dev/null; then pulled=0; break fi warn "Pull attempt %s failed in %s, retrying..." "$attempt" "$(basename "$repo")" >&2 sleep 2 done if [ "$pulled" -ne 0 ]; then warn "Pull failed in $(basename "$repo")" echo "git error: pull failed" > "$PARALLEL_STATE_DIR/giterror-$name" exit 2 fi # Remember the pre-update commit so a later build failure can reset the # repo back to it; otherwise the next run would see HEAD==origin and # silently skip the never-built change. echo "$old_head" > "$PARALLEL_STATE_DIR/oldhead-$name" [ "$(git rev-parse HEAD 2>/dev/null)" != "$old_head" ] && exit 0 exit 1 ) rc=$? # Return 0 = new commits fetched, 1 = nothing to do, 2 = real git error. return $rc } # Reverse a successful pull after a failed build, so the change is picked up # again on the next run instead of being silently skipped (HEAD would otherwise # equal origin and git_update would report "already up to date"). git_revert_repo() { local repo="$1" local name; name=$(basename "$repo") [ -f "$PARALLEL_STATE_DIR/oldhead-$name" ] || return 0 local prev; prev=$(cat "$PARALLEL_STATE_DIR/oldhead-$name" 2>/dev/null) [ -n "$prev" ] || return 0 if git -C "$repo" reset --hard "$prev" >/dev/null 2>&1; then warn "Reverted $(basename "$repo") to $prev (build failed)" else warn "Could not revert $(basename "$repo") to $prev" fi rm -f "$PARALLEL_STATE_DIR/oldhead-$name" 2>/dev/null || true } # Run a yarn command inside a repo that pins an unavailable Yarn Berry version. # Upstream package.json now declares e.g. "packageManager": "yarn@4.18.0", # which the system Yarn 1 refuses to run ("current global version is 1.22.22") # and which corepack cannot fetch (yarn 4 is not published on the registry). # Drop the field for the duration of the command, then restore the pristine # file so the repo stays clean for the next git pull. Runs inside the repo and # always returns the caller to SCRIPT_DIR so cwd never leaks between repos. yarn_run() { local repo="$1"; shift cd "$repo" 2>/dev/null || { warn "Cannot cd into %s" "$repo"; return 1; } local modified=false if python3 -c " import json, sys p = 'package.json' d = json.load(open(p)) pm = d.get('packageManager', '') if pm.startswith('yarn@') and not pm.startswith('yarn@1.'): d.pop('packageManager', None) json.dump(d, open(p, 'w'), indent=2) open(p, 'a').write(chr(10)) sys.exit(0) sys.exit(1) " 2>/dev/null; then modified=true fi yarn "$@" local rc=$? if [ "$modified" = true ]; then git checkout -- package.json 2>/dev/null || true fi cd "$SCRIPT_DIR" 2>/dev/null || true return $rc } # yarn install scoped to a per-repo cache folder. Parallel installs (renderer + # client) must never share a cache — yarn 1 corrupts the shared cache, leaving # node_modules incomplete (vite/pixi.js missing) while still exiting 0. # $1 = repo dir; remaining args forwarded to yarn. yarn_install_repo() { local repo="$1"; shift local cache_dir="$YARN_CACHE_DIR/$(basename "$repo")" mkdir -p "$cache_dir" 2>/dev/null || true # NODE_ENV may be exported as "production" via .env (the CMS runs in prod), # which makes yarn 1 skip devDependencies — vite is a devDep, so builds # would fail with "vite: not found". Force a non-production env so the full # dependency tree (including dev deps) is always installed. NODE_ENV=development yarn_run "$repo" install --cache-folder "$cache_dir" "$@" >> "$LOG_FILE" 2>&1 } # Nuke a repo's own yarn cache and node_modules for a truly clean reinstall. # Parallel installs may have corrupted the cache, so it must be discarded too. yarn_reset_repo() { local repo="$1" rm -rf "$repo/node_modules" "$YARN_CACHE_DIR/$(basename "$repo")" 2>/dev/null || true mkdir -p "$YARN_CACHE_DIR/$(basename "$repo")" 2>/dev/null || true } # ============================================================================= # PARALLEL HELPERS # ============================================================================= PARALLEL_PIDS=() PARALLEL_NAMES=() PARALLEL_FAILED=() PARALLEL_RESULTS=() # Each parallel job runs in a subshell, so variable changes (HAD_UPDATES, # UPDATED_REPOS, NITRO_BUILT) made inside update_renderer/update_client would # otherwise be lost. Jobs persist their state to a per-job file which # parallel_wait re-reads back into the parent shell. PARALLEL_STATE_DIR="${TMPDIR:-/tmp}/nitro-parallel-$$" mkdir -p "$PARALLEL_STATE_DIR" 2>/dev/null || true parallel_run() { local name="$1"; shift PARALLEL_NAMES+=("$name") ( # This subshell inherits the parent's EXIT trap and lock fd 9. Mark it # so cleanup_on_exit/release_lock become no-ops here: the main process # owns rollback, lock release and notifications. PARALLEL_JOB=1 # Snapshot inherited state so we only persist the delta back: the # emulator (run before the parallel phase) may already be in # UPDATED_REPOS, so emitting the full array would duplicate it. local _orig="${UPDATED_REPOS[*]:-}" "$@" >/dev/null 2>&1 local rc=$? { # Note: exit status is captured via `wait`; only fold state vars back. echo "HAD_UPDATES=$HAD_UPDATES" echo "NITRO_BUILT=$NITRO_BUILT" for r in "${UPDATED_REPOS[@]:-}"; do case " $_orig " in *" $r "*) ;; *) printf 'UPDATED_REPOS+=(\047%s\047)\n' "$r" ;; esac done } > "$PARALLEL_STATE_DIR/$name" exit $rc ) & PARALLEL_PIDS+=($!) } parallel_wait() { # Collect per-job pass/fail based on the real exit status of each job. PARALLEL_RESULTS=() PARALLEL_FAILED=() local i=0 for pid in "${PARALLEL_PIDS[@]}"; do local name="${PARALLEL_NAMES[$i]}" if wait "$pid" 2>/dev/null; then PARALLEL_RESULTS+=("__PARALLEL_OK__${name}") else PARALLEL_RESULTS+=("__PARALLEL_FAIL__${name}") PARALLEL_FAILED+=("$name") fi # Fold the job's persisted state back into the parent shell so the # summary reflects renderer/client updates, not just the emulator. if [ -f "$PARALLEL_STATE_DIR/$name" ]; then . "$PARALLEL_STATE_DIR/$name" rm -f "$PARALLEL_STATE_DIR/$name" fi i=$((i+1)) done PARALLEL_PIDS=() PARALLEL_NAMES=() rm -rf "$PARALLEL_STATE_DIR" 2>/dev/null || true } parallel_failed() { # Returns 0 (success) when no backgrounded job failed. [ ${#PARALLEL_FAILED[@]} -eq 0 ] } # ============================================================================= # CONFIG BACKUP & VALIDATION # ============================================================================= backup_configs() { local ts=$(date +%Y%m%d-%H%M%S) local bdir="$CONFIG_BACKUP_DIR/$ts" mkdir -p "$bdir" if [ -d "$NITRO_SRC_DIR" ]; then cp -r "$NITRO_SRC_DIR"/*.json "$bdir/" 2>/dev/null || true fi # Keep a rolling 'rollback' pointer rm -rf "$CONFIG_BACKUP_DIR/rollback" cp -r "$bdir" "$CONFIG_BACKUP_DIR/rollback" 2>/dev/null || true echo "$bdir" } validate_json() { local file="$1" # Strict JSON first... if python3 -m json.tool "$file" >/dev/null 2>&1; then return 0 fi # ...otherwise fall back to JSONC (the renderer configs legitimately carry # // and /* */ comments — that is not an error, it is by design). if command -v node &>/dev/null && \ node -e "require('jsonc-parser').parse(require('fs').readFileSync(process.argv[1],'utf-8'),[],{allowTrailingComma:true,allowEmptyContent:true});" "$file" >/dev/null 2>&1; then return 0 fi return 1 } validate_configs() { local dir="$1" local errors=0 for f in "$dir"/*.json; do [ -f "$f" ] || continue case "$(basename "$f")" in *jsonc*|*.example*) continue ;; esac if ! validate_json "$f"; then warn "Invalid JSON: %s" "$(basename "$f")" errors=$((errors + 1)) fi done [ "$errors" -eq 0 ] && ok "All config files valid" || warn "$errors config(s) invalid" } # ============================================================================= # ENV FILE SYNC & VALIDATION # ============================================================================= # Merge any keys documented in /.env.example but missing from the live # /.env so new options introduced upstream are filled in with their # default values on every update. Existing values are ALWAYS preserved. sync_env_file() { local base_dir="${1:-$SCRIPT_DIR}" local ex="$base_dir/.env.example" en="$base_dir/.env" [ -f "$ex" ] || { info "No .env.example in %s — skipping env sync" "$(basename "$base_dir")"; return 0; } local tmp; tmp=$(mktemp) env_example="$ex" env_file="$en" env_added="$tmp" python3 -c ' import os, re ex, en, out = os.environ["env_example"], os.environ["env_file"], os.environ["env_added"] seen = set() if os.path.exists(en): with open(en, encoding="utf-8", errors="replace") as f: for line in f: m = re.match(r"^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=", line) if m: seen.add(m.group(1)) additions = [] with open(ex, encoding="utf-8", errors="replace") as f: for line in f: m = re.match(r"^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$", line) if not m: continue key = m.group(1) if key in seen: continue seen.add(key) value = m.group(2).strip() if value and not (value[0] in "\"\x27" or value.startswith("${")): value = "\"" + value + "\"" additions.append(f"{key}={value}\n") with open(out, "w", encoding="utf-8") as f: f.writelines(additions) ' 2>/dev/null || true if [ -s "$tmp" ]; then if { printf "\n# --- Auto-added by %s on %s ---\n" "$SCRIPT_NAME" "$(date +%F)" cat "$tmp" } >> "$en" 2>/dev/null; then # Re-load new values so the rest of the run sees them. set -a; . "$en"; set +a ok "%s new variable(s) added to %s" "$(wc -l < "$tmp")" "$(basename "$en")" else warn "Cannot update %s (no write permission) — add the %s variable(s) manually" \ "$(basename "$en")" "$(wc -l < "$tmp")" fi else ok "Environment %s is up to date" "$(basename "$en")" fi rm -f "$tmp" 2>/dev/null || true } # Sanity-check a .env file for the failure modes that actually break a deploy: # duplicate keys, and a value concatenated onto another key (missing newline), # e.g. BYPARR_URL=http://localhost:8191AUTH_TRUST_HOST="..." validate_env_file() { local base_dir="${1:-$SCRIPT_DIR}" local en="$base_dir/.env" [ -f "$en" ] || { warn "No .env file in %s" "$(basename "$base_dir")"; return 1; } local issues; issues=$(python3 - "$en" <<'PY' import sys, re path = sys.argv[1] out = [] with open(path, encoding="utf-8", errors="replace") as f: for i, line in enumerate(f, 1): s = line.strip() if not s or s.startswith("#") or s.startswith("export "): continue m = re.match(r"^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$", s) if not m: out.append(f"line {i}: unparseable {s[:60]}") continue val = m.group(2) # URL immediately followed by an uppercase KEY= (missing newline). if re.search(r"https?://[^\s\"']+?[A-Z][A-Z0-9_]{2,40}\s*=", val): out.append(f"line {i}: concatenated value (missing newline) {s[:90]}") PY ) if [ -n "$issues" ]; then while IFS= read -r line; do warn "%s" "$line"; done <<< "$issues" fail "Invalid .env — fix before next startup" return 1 fi local dup; dup=$(grep -oE '^[A-Za-z_][A-Za-z0-9_]*=' "$en" 2>/dev/null | sort | uniq -d | tr '\n' ' ') if [ -n "$dup" ]; then warn "Duplicate key(s) in %s: %s" "$(basename "$en")" "$dup" fi # Missing critical vars commonly needed for a healthy boot. local req=(DATABASE_URL REDIS_URL PORT HOTEL_NAME) local miss=() for r in "${req[@]}"; do grep -qE "^[[:space:]]*${r}[[:space:]]*=" "$en" || miss+=("$r") done [ ${#miss[@]} -gt 0 ] && warn "Missing variable(s) in %s: %s" "$(basename "$en")" "${miss[*]}" return 0 } # ============================================================================= # BINARY BACKUP & ROLLBACK # ============================================================================= backup_binaries() { [ -n "$BINARY_BACKUP_DIR" ] && return BINARY_BACKUP_DIR="$CONFIG_BACKUP_DIR/binaries-$(date +%Y%m%d-%H%M%S)" mkdir -p "$BINARY_BACKUP_DIR" local jar_dir="$EMULATOR_MODULE/target" if [ -d "$jar_dir" ] && ls "$jar_dir"/Polaris-*-jar-with-dependencies.jar &>/dev/null 2>&1; then cp "$jar_dir"/Polaris-*-jar-with-dependencies.jar "$BINARY_BACKUP_DIR/" 2>/dev/null || true ok "Emulator JAR backed up" fi local client_dist="$NITRO_CLIENT/dist" if [ -d "$client_dist" ]; then cp -r "$client_dist" "$BINARY_BACKUP_DIR/client-dist" 2>/dev/null || true ok "Octane dist backed up" fi } restore_binaries() { [ -z "$BINARY_BACKUP_DIR" ] && return [ ! -d "$BINARY_BACKUP_DIR" ] && return echo "" echo -e " ${C_BG_RED}${C_WHITE}${C_BOLD} $(_t "RESTORE BINARIES") ${C_RESET}" if ls "$BINARY_BACKUP_DIR"/Polaris-*-jar-with-dependencies.jar &>/dev/null 2>&1; then local jar_dir="$EMULATOR_MODULE/target" mkdir -p "$jar_dir" cp "$BINARY_BACKUP_DIR"/Polaris-*-jar-with-dependencies.jar "$jar_dir/" 2>/dev/null || true ok "Emulator JAR restored" fi if [ -d "$BINARY_BACKUP_DIR/client-dist" ]; then local client_dist="$NITRO_CLIENT/dist" rm -rf "$client_dist" 2>/dev/null || true cp -r "$BINARY_BACKUP_DIR/client-dist" "$client_dist" 2>/dev/null || true ok "Octane dist restored" fi } # ============================================================================= # OLD NITRO CLEANUP # ============================================================================= cmd_cleanup_old_nitro() { step 1 1 "Cleanup Old Nitro" echo "" local www_dir="/var/www" local old_dirs=() local current_client="$NITRO_CLIENT" local current_renderer="$NITRO_RENDERER" # Scan for old Nitro directories that are NOT the current active ones for candidate in "$www_dir"/Nitro-V3 "$www_dir"/Nitro_Render_V3 "$www_dir"/Nitro_Render "$www_dir"/Nitro "$www_dir"/Nitro-V3-*; do [ -d "$candidate" ] || continue [ "$(realpath "$candidate" 2>/dev/null)" = "$(realpath "$current_client" 2>/dev/null)" ] && continue [ "$(realpath "$candidate" 2>/dev/null)" = "$(realpath "$current_renderer" 2>/dev/null)" ] && continue old_dirs+=("$candidate") done if [ ${#old_dirs[@]} -eq 0 ]; then ok "No old Nitro directories found" else echo -e " ${C_YELLOW}${C_BOLD}Found ${#old_dirs[@]} old Nitro director(y/ies):${C_RESET}" echo "" local total_size=0 for d in "${old_dirs[@]}"; do local sz=$(du -sh "$d" 2>/dev/null | cut -f1) local files=$(find "$d" -type f 2>/dev/null | wc -l) echo -e " ${C_RED}✗${C_RESET} ${C_BOLD}$(basename "$d")${C_RESET} ${C_DIM}($sz, $files files)${C_RESET}" local bytes=$(du -sb "$d" 2>/dev/null | cut -f1) total_size=$((total_size + bytes)) done echo "" echo -e " ${C_CYAN}Total space: $(numfmt --to=iec "$total_size" 2>/dev/null || echo "${total_size}B")${C_RESET}" echo "" # ── STEP 1: Migrate symlinks pointing to old dirs ── echo -e " ${C_BOLD}Checking symlinks...${C_RESET}" local symlinks_fixed=0 local root_cmd="" [ "$(id -u)" -eq 0 ] || root_cmd="sudo -n" for old_dir in "${old_dirs[@]}"; do local old_name=$(basename "$old_dir") # Determine the correct new target local new_target="" case "$old_name" in Nitro-V3|Nitro-V3-*) new_target="$current_client" ;; Nitro_Render_V3|Nitro_Render) new_target="$current_renderer" ;; esac [ -z "$new_target" ] && continue # Find all symlinks on the system pointing to the old dir while IFS= read -r link; do [ -z "$link" ] && continue local link_target=$(readlink -f "$link" 2>/dev/null) case "$link_target" in *"$old_name"*|*"$old_dir"*) echo -e " ${C_GREEN}${E_ARROW}${C_RESET} $link → $new_target" $root_cmd ln -sfn "$new_target" "$link" 2>/dev/null && symlinks_fixed=$((symlinks_fixed + 1)) || warn "Could not update symlink: %s" "$link" ;; esac done < <(find /var/www /etc/nginx /etc/systemd /opt -maxdepth 4 -type l 2>/dev/null) done [ "$symlinks_fixed" -gt 0 ] && ok "Updated %s symlink(s)" "$symlinks_fixed" || ok "No symlinks to update" # ── STEP 2: Update nginx configs referencing old paths ── echo "" echo -e " ${C_BOLD}Checking nginx configs...${C_RESET}" local nginx_fixed=0 for conf in /etc/nginx/sites-enabled/*.conf /etc/nginx/sites-available/*.conf /etc/nginx/conf.d/*.conf; do [ -f "$conf" ] || continue local needs_update=false for old_dir in "${old_dirs[@]}"; do grep -q "$old_dir" "$conf" 2>/dev/null && needs_update=true && break done if [ "$needs_update" = true ]; then local bak="$conf.bak-nitro-cleanup-$(date +%s)" $root_cmd cp "$conf" "$bak" 2>/dev/null || true for old_dir in "${old_dirs[@]}"; do local old_name=$(basename "$old_dir") local new_target="" case "$old_name" in Nitro-V3|Nitro-V3-*) new_target="$current_client" ;; Nitro_Render_V3|Nitro_Render) new_target="$current_renderer" ;; esac [ -z "$new_target" ] && continue $root_cmd sed -i "s|$old_dir|$new_target|g" "$conf" 2>/dev/null && nginx_fixed=$((nginx_fixed + 1)) done echo -e " ${C_GREEN}${E_ARROW}${C_RESET} Updated: $(basename "$conf")" fi done [ "$nginx_fixed" -gt 0 ] && ok "Updated %s nginx reference(s) (backups created)" "$nginx_fixed" || ok "No nginx configs to update" # ── STEP 3: Update systemd service files referencing old paths ── echo "" echo -e " ${C_BOLD}Checking systemd services...${C_RESET}" local svc_fixed=0 for svc_file in /etc/systemd/system/*.service /etc/systemd/system/emulator.service.d/*.conf; do [ -f "$svc_file" ] || continue local needs_update=false for old_dir in "${old_dirs[@]}"; do grep -q "$old_dir" "$svc_file" 2>/dev/null && needs_update=true && break done if [ "$needs_update" = true ]; then local bak="$svc_file.bak-nitro-cleanup-$(date +%s)" $root_cmd cp "$svc_file" "$bak" 2>/dev/null || true for old_dir in "${old_dirs[@]}"; do local old_name=$(basename "$old_dir") local new_target="" case "$old_name" in Nitro-V3|Nitro-V3-*) new_target="$current_client" ;; Nitro_Render_V3|Nitro_Render) new_target="$current_renderer" ;; esac [ -z "$new_target" ] && continue $root_cmd sed -i "s|$old_dir|$new_target|g" "$svc_file" 2>/dev/null && svc_fixed=$((svc_fixed + 1)) done echo -e " ${C_GREEN}${E_ARROW}${C_RESET} Updated: $(basename "$svc_file")" fi done [ "$svc_fixed" -gt 0 ] && $root_cmd systemctl daemon-reload 2>/dev/null [ "$svc_fixed" -gt 0 ] && ok "Updated %s service reference(s)" "$svc_fixed" || ok "No systemd services to update" # ── STEP 4: Check for any other file references ── echo "" echo -e " ${C_BOLD}Checking other file references...${C_RESET}" local other_refs=0 for old_dir in "${old_dirs[@]}"; do local old_name=$(basename "$old_dir") local refs=$(grep -rl "$old_name" /var/www/atom-nexst/*.sh /var/www/atom-nexst/.env 2>/dev/null | grep -v "update-Nitrov3.sh" | wc -l) other_refs=$((other_refs + refs)) if [ "$refs" -gt 0 ]; then echo -e " ${C_YELLOW}${E_WARN}${C_RESET} $refs file(s) still reference ${C_BOLD}$old_name${C_RESET}" fi done [ "$other_refs" -gt 0 ] && warn "Some files still reference old names — check manually" || ok "No other references found" # ── STEP 5: Remove old directories ── echo "" echo -e " ${C_BOLD}Summary:${C_RESET}" echo -e " Symlinks updated: ${C_GREEN}$symlinks_fixed${C_RESET}" echo -e " Nginx refs fixed: ${C_GREEN}$nginx_fixed${C_RESET}" echo -e " Service refs fixed: ${C_GREEN}$svc_fixed${C_RESET}" echo -e " Other refs: ${C_YELLOW}$other_refs${C_RESET}" echo "" if confirm "Now remove the old directories?"; then for d in "${old_dirs[@]}"; do local name=$(basename "$d") spinner_start "Removing %s..." "$name" if rm -rf "$d" 2>/dev/null; then spinner_stop ok ok "Removed %s" "$name" else spinner_stop fail warn "Failed to remove %s" "$name" fi done ok "Old Nitro cleanup completed" else info "Skipped — old directories kept" fi fi # Also clean up old /tmp nitro files local tmp_count=$(find /tmp -maxdepth 1 -name 'nitro-*' -o -name 'epicnext-*' 2>/dev/null | wc -l) if [ "$tmp_count" -gt 0 ]; then echo "" echo -e " ${C_YELLOW}Found $tmp_count old temp file(s) in /tmp${C_RESET}" if confirm "Clean /tmp nitro/epicnext files?"; then rm -rf /tmp/nitro-* /tmp/epicnext-* 2>/dev/null || true ok "Temp files cleaned" fi fi # Clean up old log files (older than 30 days) local old_logs=0 if [ -d "$LOG_DIR" ]; then old_logs=$(find "$LOG_DIR" -name 'update-*.log' -mtime +30 2>/dev/null | wc -l) if [ "$old_logs" -gt 0 ]; then echo "" echo -e " ${C_YELLOW}Found $old_logs old log file(s) (>30 days)${C_RESET}" if confirm "Clean old logs?"; then find "$LOG_DIR" -name 'update-*.log' -mtime +30 -exec rm -f {} \; 2>/dev/null || true ok "Old logs cleaned" fi fi fi # Clean up old config backups (keep last 5) if [ -d "$CONFIG_BACKUP_DIR" ]; then local backup_count=$(find "$CONFIG_BACKUP_DIR" -maxdepth 1 -type d ! -name 'rollback' ! -name 'config-backups' 2>/dev/null | wc -l) if [ "$backup_count" -gt 5 ]; then local to_remove=$((backup_count - 5)) echo "" echo -e " ${C_YELLOW}Found $backup_count config backups (keeping 5, removing $to_remove)${C_RESET}" if confirm "Prune old config backups?"; then find "$CONFIG_BACKUP_DIR" -maxdepth 1 -type d ! -name 'rollback' ! -name 'config-backups' -printf '%T@ %p\n' 2>/dev/null | sort -rn | tail -n +6 | sed 's/^[0-9.]* //' | xargs -r rm -rf || true ok "Old config backups pruned" fi fi fi # Clean up old SQL backups (keep last 5) if [ -d "$BACKUP_DIR" ]; then local sql_count=$(find "$BACKUP_DIR" -maxdepth 1 -name '*.sql' 2>/dev/null | wc -l) if [ "$sql_count" -gt 5 ]; then local to_remove=$((sql_count - 5)) echo "" echo -e " ${C_YELLOW}Found $sql_count SQL backups (keeping 5, removing $to_remove)${C_RESET}" if confirm "Prune old SQL backups?"; then find "$BACKUP_DIR" -maxdepth 1 -name '*.sql' -printf '%T@ %p\n' 2>/dev/null | sort -rn | tail -n +6 | sed 's/^[0-9.]* //' | xargs -r rm -f || true ok "Old SQL backups pruned" fi fi fi echo "" ok "Cleanup check completed" } # ============================================================================= # AUTO-SETUP (clone missing repos) # ============================================================================= auto_setup_missing_repos() { local cloned=false local needs_link=false if [ ! -d "$NITRO_CLIENT" ] || [ ! -d "$NITRO_CLIENT/.git" ]; then echo "" echo -e " ${C_YELLOW}${E_WARN}${C_RESET} ${C_BOLD}Octane client not found at $NITRO_CLIENT${C_RESET}" info "Cloning Octane client from %s..." "$GIT_REPO_CLIENT" spinner_start "Cloning Octane client..." if git clone --depth 1 "$GIT_REPO_CLIENT" "$NITRO_CLIENT" >> "$LOG_FILE" 2>&1; then spinner_stop ok ok "Octane client cloned successfully" cloned=true; needs_link=true else spinner_stop fail die "Failed to clone Octane client from %s" "$GIT_REPO_CLIENT" fi fi if [ ! -d "$NITRO_RENDERER" ] || [ ! -d "$NITRO_RENDERER/.git" ]; then echo "" echo -e " ${C_YELLOW}${E_WARN}${C_RESET} ${C_BOLD}Octane renderer not found at $NITRO_RENDERER${C_RESET}" info "Cloning Octane renderer from %s..." "$GIT_REPO_RENDERER" spinner_start "Cloning Octane renderer..." if git clone --depth 1 "$GIT_REPO_RENDERER" "$NITRO_RENDERER" >> "$LOG_FILE" 2>&1; then spinner_stop ok ok "Octane renderer cloned successfully" cloned=true; needs_link=true else spinner_stop fail die "Failed to clone Octane renderer from %s" "$GIT_REPO_RENDERER" fi fi # Also detect when either repo exists but was never linked if [ "$needs_link" = false ]; then if [ -d "$NITRO_RENDERER" ] && [ -d "$NITRO_RENDERER/.git" ]; then # Check if yarn global link is registered — if not, we need to re-link if [ ! -d "$(yarn global dir 2>/dev/null)/node_modules/@nitrots/nitro-renderer" ] 2>/dev/null && \ [ ! -L "$(yarn global dir 2>/dev/null)/node_modules/@nitrots/nitro-renderer" ] 2>/dev/null; then needs_link=true fi fi fi if [ "$cloned" = true ] || [ "$needs_link" = true ]; then echo "" info "Installing dependencies..." # Step 1: Install renderer deps if [ -d "$NITRO_RENDERER" ] && [ -f "$NITRO_RENDERER/package.json" ]; then spinner_start "Installing renderer deps..." yarn_install_repo "$NITRO_RENDERER" --prefer-offline >> "$LOG_FILE" 2>&1 || \ yarn_install_repo "$NITRO_RENDERER" >> "$LOG_FILE" 2>&1 || warn "Renderer yarn install failed" spinner_stop ok ok "Renderer dependencies installed" # Step 2: Register renderer as a global yarn link spinner_start "Linking renderer package..." cd "$NITRO_RENDERER" yarn link >> "$LOG_FILE" 2>&1 || warn "yarn link in renderer failed (may already be linked)" cd "$SCRIPT_DIR" spinner_stop ok ok "Renderer linked globally" fi # Step 3: Install client deps if [ -d "$NITRO_CLIENT" ] && [ -f "$NITRO_CLIENT/package.json" ]; then spinner_start "Installing client deps..." yarn_install_repo "$NITRO_CLIENT" --prefer-offline >> "$LOG_FILE" 2>&1 || \ yarn_install_repo "$NITRO_CLIENT" >> "$LOG_FILE" 2>&1 || warn "Client yarn install failed" spinner_stop ok ok "Client dependencies installed" # Step 4: Link renderer into client spinner_start "Linking renderer to client..." cd "$NITRO_CLIENT" yarn link "@nitrots/nitro-renderer" >> "$LOG_FILE" 2>&1 || warn "yarn link to renderer failed (may already be linked)" cd "$SCRIPT_DIR" spinner_stop ok ok "Renderer linked to client" fi # Step 5: Setup config files from examples echo "" info "Setting up configuration files..." _setup_config_files # Step 6: Create .nitro-build.json for JSONC mode if [ ! -f "$NITRO_CLIENT/.nitro-build.json" ]; then echo '{"jsonMode":"jsonc"}' > "$NITRO_CLIENT/.nitro-build.json" ok "Created .nitro-build.json (jsonc mode)" fi # Step 7: Build client frontend if [ -d "$NITRO_CLIENT" ] && [ -f "$NITRO_CLIENT/package.json" ]; then spinner_start "Building client frontend..." if yarn_run "$NITRO_CLIENT" build >> "$LOG_FILE" 2>&1; then spinner_stop ok ok "Client frontend built" else spinner_stop fail warn "Client build failed — run update to retry" fi fi mkdir -p "$NITRO_SRC_DIR" 2>/dev/null || true mkdir -p "$NITRO_CLIENT/dist/custom-themes" 2>/dev/null || true [ ! -f "$NITRO_CLIENT/dist/custom-themes/index.json" ] && echo '{"themes":[]}' > "$NITRO_CLIENT/dist/custom-themes/index.json" fix_perms echo "" ok "Auto-setup completed — all repos ready" else ok "All repos already present" fi } # Copy example config files to their runtime names, handling renamed files # in the Octane repo (renderer-config..jsonc.example.json etc.). Existing # runtime files are MERGED via merge-config.cjs (new keys filled in, local # values preserved) instead of being skipped — this is what keeps configs # "gecompleteerd" when upstream adds new options. _setup_config_files() { local src_dir="$NITRO_SRC_DIR" [ ! -d "$src_dir" ] && mkdir -p "$src_dir" 2>/dev/null local copied=0 local ms="$SCRIPT_DIR/scripts/merge-config.cjs" local merge_or_copy merge_or_copy() { local ex="$1" tgt="$2" if [ -f "$tgt" ]; then node "$ms" "$ex" "$tgt" 2>/dev/null || true else cp "$ex" "$tgt" 2>/dev/null || return 1 fi return 0 } # Standard *.example files (ui-config, client-mode, etc.) for ef in "$src_dir"/*.example; do [ -f "$ef" ] || continue local b=$(basename "$ef"); local tn="${b%.example}" case "$tn" in *.*) ;; *) tn="$tn.json" ;; esac if merge_or_copy "$ef" "$src_dir/$tn"; then copied=$((copied + 1)); fi done # Handle renamed renderer config files # Repo has: renderer-config..jsonc.example.json / renderer-config.json.example.json # Runtime needs: renderer-config.jsonc (JSONC) or renderer-config.json local rc_jsonc="$src_dir/renderer-config..jsonc.example.json" local rc_json="$src_dir/renderer-config.json.example.json" if [ -f "$rc_jsonc" ]; then if merge_or_copy "$rc_jsonc" "$src_dir/renderer-config.jsonc"; then copied=$((copied + 1)); fi fi if [ -f "$rc_json" ]; then if merge_or_copy "$rc_json" "$src_dir/renderer-config.json"; then copied=$((copied + 1)); fi fi # Merge/copy to gamedata config dir too if [ -d "$GAMEDATA_CONF_DIR" ]; then for f in "$src_dir"/*.json "$src_dir"/*.jsonc; do [ -f "$f" ] || continue local fn=$(basename "$f") # Skip example/source files case "$fn" in *.example*|*.example) continue ;; esac merge_or_copy "$f" "$GAMEDATA_CONF_DIR/$fn" 2>/dev/null || true done fi # Copy to dist/configuration if it exists local dcd="$NITRO_CLIENT/dist/configuration" if [ -d "$dcd" ]; then for f in "$src_dir"/*.json "$src_dir"/*.jsonc; do [ -f "$f" ] || continue local fn=$(basename "$f") case "$fn" in *.example*|*.example) continue ;; esac cp "$f" "$dcd/$fn" 2>/dev/null || true done fi ok "%s config file(s) copied/synced" "$copied" } # ============================================================================= # PRE-FLIGHT CHECKS # ============================================================================= preflight_check() { local fatal=false for cmd in git mvn node python3 yarn; do command -v "$cmd" &>/dev/null || { fail "Missing: $cmd"; fatal=true; } done command -v "$MYSQL_CLIENT" &>/dev/null || { fail "Missing: $MYSQL_CLIENT"; fatal=true; } command -v "$MYSQL_DUMP" &>/dev/null || { fail "Missing: $MYSQL_DUMP"; fatal=true; } $fatal && die "Required commands missing" ok "All commands available" # Keep .env complete: fill in any new variables documented in # .env.example and validate the result before touching anything. info "Environment sync & validation..." sync_env_file "$SCRIPT_DIR" || true validate_env_file "$SCRIPT_DIR" || fatal=true $fatal && die "Invalid .env — fix before next startup" # Auto-clone missing repos instead of dying local missing_repos=false for d in "$NITRO_RENDERER" "$NITRO_CLIENT"; do [ -d "$d" ] && [ -d "$d/.git" ] || missing_repos=true done if [ "$missing_repos" = true ]; then info "Missing repos detected — running auto-setup..." auto_setup_missing_repos fi for d in "$EMULATOR_REPO" "$EMULATOR_MODULE" "$NITRO_RENDERER" "$NITRO_CLIENT" "$NITRO_SRC_DIR"; do [ -d "$d" ] || die "Missing dir: $d" done ok "All directories exist" local total_kb=$(df --output=avail "$EMULATOR_DIR" 2>/dev/null | tail -1) local total_gb=$((total_kb / 1024 / 1024)) [ "$total_gb" -lt "$MIN_DISK_GB" ] && die "Only ${total_gb}GB free (min ${MIN_DISK_GB}GB)" if [ "$total_gb" -lt $((MIN_DISK_GB * 2)) ]; then warn "Low disk: %sGB free" "$total_gb" else ok "%sGB disk available" "$total_gb" fi $MYSQL_CLIENT $MYSQL_CRED -e "SELECT 1" "$DB_NAME" &>/dev/null || die "DB connection failed" ok "Database connected" } # ============================================================================= # UPDATE FUNCTIONS # ============================================================================= update_emulator() { step 2 9 "Update & Build Emulator" local grc=1 if git_update "$EMULATOR_REPO" "$NITRO_BRANCH"; then grc=0 else grc=$? fi if [ $grc -eq 2 ]; then die "Emulator update failed (git error)" elif [ $grc -eq 0 ]; then HAD_UPDATES=true; UPDATED_REPOS+=("Emulator") cd "$EMULATOR_MODULE" backup_binaries ROLLBACK_NEEDED=true # The emulator reads its live config from target/config.ini (gitignored, # so git pull never touches it). Guard it anyway: if the Maven build ever # rebuilds target/ without it, the live config is restored verbatim so # the DB/hotel settings survive every update untouched. local emu_cfg="$EMULATOR_MODULE/target/config.ini" local emu_cfg_bak="" if [ -f "$emu_cfg" ]; then emu_cfg_bak="$(mktemp -d)/config.ini" cp "$emu_cfg" "$emu_cfg_bak" 2>/dev/null && info "Emulator config.ini backed up" \ || { rm -rf "${emu_cfg_bak%/config.ini}" 2>/dev/null || true; emu_cfg_bak=""; } fi spinner_start "Building emulator (parallel)..." # Prefer offline mode when the local Maven repo is warm; fall back to # online if offline resolution fails. ccache speeds up repeated JNI/C++ # native compiles if present. command -v ccache >/dev/null 2>&1 && export CC="ccache gcc" CXX="ccache g++" if mvn package -q -T 1C -o -Dmaven.test.skip=true >> "$LOG_FILE" 2>&1 || \ mvn package -q -T 1C -Dmaven.test.skip=true >> "$LOG_FILE" 2>&1; then spinner_stop ok else spinner_stop fail tail -30 "$LOG_FILE" | grep -E '(ERROR|FAILURE|BUILD)' || true git_revert_repo "$EMULATOR_REPO" die "Maven build failed (see: %s)" "$LOG_FILE" fi # Restore the live config if the build produced a target/ without it. if [ ! -f "$emu_cfg" ]; then if [ -n "$emu_cfg_bak" ] && [ -f "$emu_cfg_bak" ]; then mkdir -p "$EMULATOR_MODULE/target" cp "$emu_cfg_bak" "$emu_cfg" 2>/dev/null && ok "Emulator config.ini restored (preserved across build)" fi else info "Emulator config.ini preserved" fi if [ ! -f "$emu_cfg" ]; then fail "No target/config.ini — emulator would start with defaults!" ERRORS=$((ERRORS + 1)) fi [ -n "$emu_cfg_bak" ] && rm -rf "${emu_cfg_bak%/config.ini}" 2>/dev/null || true local jar=$(find target -maxdepth 1 -name 'Polaris-*-jar-with-dependencies.jar' -printf '%T@ %p\n' 2>/dev/null | sort -rn | sed -n '1s/^[0-9.]* //p' | xargs -r basename 2>/dev/null || echo "") [ -z "$jar" ] && die "No jar found" EMULATOR_BUILT=true ok "Jar: %s" "$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 -Xmx4G -jar $jar EOJ chmod +x emulator; ok "Launch script updated" cd "$SCRIPT_DIR" else info "Emulator: already up to date" fi } update_renderer() { step 3 9 "Update Octane-Renderer" local grc=1 if git_update "$NITRO_RENDERER" "$NITRO_BRANCH"; then grc=0 else grc=$? fi if [ $grc -eq 2 ]; then # Let the caller fail the parallel group; the state file records it. warn "Renderer update failed (git error)" return 1 fi if [ $grc -eq 0 ]; then HAD_UPDATES=true; UPDATED_REPOS+=("Octane-Renderer") info "Renderer pulled (%s)" "$(git -C "$NITRO_RENDERER" log --oneline -1 2>/dev/null || true)" else info "Renderer: already up to date" fi # Renderer is consumed by the Octane client build through the global # `@nitrots/nitro-renderer` link — it needs its SOURCE updated (git pull # above) and its node_modules present, nothing more. We deliberately do NOT # rebuild or reinstall a working node_modules: repeated parallel yarn # installs corrupt the cache and are how the client build silently breaks. if [ -d "$NITRO_RENDERER/node_modules" ]; then info "Renderer node_modules present — skipping yarn install" else spinner_start "Installing renderer deps (first run)..." if yarn_install_repo "$NITRO_RENDERER" --frozen-lockfile --prefer-offline; then spinner_stop ok; ok "Renderer dependencies installed" elif yarn_install_repo "$NITRO_RENDERER"; then spinner_stop ok; ok "Renderer dependencies installed (online)" else spinner_stop fail warn "Renderer: node_modules still missing — Octane build may fail" fi fi # Idempotent global registration (no install involved) so the client build # can still `yarn link "@nitrots/nitro-renderer"`. Not fatal when absent. if [ ! -e "$HOME/.config/yarn/link/@nitrots/nitro-renderer" ] \ && [ ! -e "/usr/local/share/.config/yarn/link/@nitrots/nitro-renderer" ]; then cd "$NITRO_RENDERER" yarn link >> "$LOG_FILE" 2>&1 || warn "Renderer global link not registered" cd "$SCRIPT_DIR" fi fix_perms return 0 } update_client() { step 4 9 "Update & Build Octane" local grc=1 if git_update "$NITRO_CLIENT" "$NITRO_BRANCH"; then grc=0 else grc=$? fi if [ $grc -eq 2 ]; then warn "Octane update failed (git error)" return 1 elif [ $grc -eq 0 ]; then HAD_UPDATES=true; NITRO_BUILT=true; UPDATED_REPOS+=("Octane") cd "$NITRO_CLIENT" backup_binaries ROLLBACK_NEEDED=true spinner_start "Installing deps..." # yarn 1 can "succeed" while leaving node_modules incomplete, so verify # that the vite build binary actually exists before attempting a build. if ! yarn_install_repo "$NITRO_CLIENT" --frozen-lockfile --prefer-offline; then warn "Octane: yarn install failed — cleaning node_modules and retrying" yarn_reset_repo "$NITRO_CLIENT" yarn_install_repo "$NITRO_CLIENT" || { spinner_stop fail; git_revert_repo "$NITRO_CLIENT"; die "yarn install failed (see: %s)" "$LOG_FILE"; } fi cd "$NITRO_CLIENT" if [ ! -x node_modules/.bin/vite ]; then warn "Octane: vite missing after install — resetting repo cache and doing full install" # Parallel yarn installs can corrupt a shared cache; resetting the # repo's own cache folder guarantees a clean fetch from the registry. yarn_reset_repo "$NITRO_CLIENT" yarn_install_repo "$NITRO_CLIENT" || { spinner_stop fail; git_revert_repo "$NITRO_CLIENT"; die "yarn install failed (see: %s)" "$LOG_FILE"; } cd "$NITRO_CLIENT" fi spinner_stop ok # Ensure renderer is linked into client cd "$NITRO_CLIENT" yarn link "@nitrots/nitro-renderer" >> "$LOG_FILE" 2>&1 || true cd "$SCRIPT_DIR" spinner_start "Building frontend..." if yarn_run "$NITRO_CLIENT" build >> "$LOG_FILE" 2>&1; then # Hard guarantee: the served entrypoint must exist, otherwise the # client is broken. Revert the repo and restore the previous dist # so the hotel keeps serving a working client. if [ ! -f "$NITRO_CLIENT/dist/index.html" ]; then spinner_stop fail tail -30 "$LOG_FILE" || true git_revert_repo "$NITRO_CLIENT" die "Octane build produced no dist/index.html — reverted (see: %s)" "$LOG_FILE" fi spinner_stop ok else spinner_stop fail tail -30 "$LOG_FILE" || true git_revert_repo "$NITRO_CLIENT" die "yarn build failed (see: %s)" "$LOG_FILE" fi else info "Octane: 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" precompress_assets fix_perms return 0 } # Does the CMS need a rebuild? True when the build output is missing or when # any source/package file is newer than the last successful build. needs_cms_build() { local bid="$CMS_DIR/.next/BUILD_ID" [ -f "$bid" ] || return 0 local newer newer=$(find "$CMS_DIR/src" "$CMS_DIR/package.json" "$CMS_DIR/pnpm-lock.yaml" \ "$CMS_DIR/next.config.ts" "$CMS_DIR/next.config.mjs" "$CMS_DIR/next.config.js" \ "$CMS_DIR/tsconfig.json" -newer "$bid" 2>/dev/null | head -1) [ -n "$newer" ] } # Build (and optionally pull) the CMS. Unlike the git repos this NEVER stashes # local work: the CMS may carry uncommitted files that are intentionally live. # Set NITRO_CMS_PULL=1 to also run a safe `git pull --ff-only` first. update_cms() { step 5 9 "Update & Build CMS" [ -f "$CMS_DIR/package.json" ] || { info "No %s/package.json — skipping CMS" "$(basename "$CMS_DIR")"; return 0; } local needs=false if [ "$CMS_PULL" = "1" ] && [ -d "$CMS_DIR/.git" ]; then local grc=1 if git_update "$CMS_DIR" "$NITRO_BRANCH"; then grc=0 else grc=$? fi if [ $grc -eq 2 ]; then warn "CMS git update failed" return 1 fi [ $grc -eq 0 ] && needs=true elif [ "$CMS_PULL" != "1" ]; then info "CMS git pull disabled (NITRO_CMS_PULL=0) — building from current tree" fi needs_cms_build && needs=true if [ "$needs" = true ]; then # Build into a staging dir and swap atomically: the live CMS keeps # serving from .next the whole time, and a failed or interrupted # build can never break the running site. local staging="$CMS_DIR/.next-staging" rm -rf "$staging" 2>/dev/null || true spinner_start "Building CMS..." local rc=0 ( cd "$CMS_DIR" && NEXT_DIST_DIR=".next-staging" eval "$CMS_INSTALL_CMD" >> "$LOG_FILE" 2>&1 \ && NEXT_DIST_DIR=".next-staging" eval "$CMS_BUILD_CMD" >> "$LOG_FILE" 2>&1 ) || rc=$? if [ $rc -eq 0 ] && [ -s "$staging/BUILD_ID" ]; then rm -rf "$CMS_DIR/.next" 2>/dev/null || true mv "$staging" "$CMS_DIR/.next" 2>/dev/null if [ -s "$CMS_DIR/.next/BUILD_ID" ]; then spinner_stop ok ok "CMS build complete" CMS_BUILT=true else rm -rf "$staging" 2>/dev/null || true spinner_stop fail warn "CMS build swap failed — live build untouched" fi else rm -rf "$staging" 2>/dev/null || true spinner_stop fail tail -40 "$LOG_FILE" | grep -E '(error|Error|ERROR|Failed|BUILD)' || true warn "CMS build failed — live build untouched, see %s" "$LOG_FILE" fi else info "CMS: already up to date" fi return 0 } # Wait for the CMS (Next.js on port $CMS_PORT) to accept connections. wait_for_cms_health() { local hc=0 while [ "$hc" -lt "$CMS_HEALTH_RETRIES" ]; do if command -v nc &>/dev/null && nc -z 127.0.0.1 "$CMS_PORT" -w 3 2>/dev/null; then ok "CMS health check passed (port %s listening)" "$CMS_PORT" return 0 fi if timeout 3 bash -c "exec 3<>/dev/tcp/127.0.0.1/$CMS_PORT" 2>/dev/null; then ok "CMS health check passed (port %s listening)" "$CMS_PORT" return 0 fi hc=$((hc+1)) [ "$hc" -lt "$CMS_HEALTH_RETRIES" ] && sleep "$CMS_HEALTH_INTERVAL" done warn "CMS not healthy after %s retries (port %s)" "$CMS_HEALTH_RETRIES" "$CMS_PORT" return 1 } sync_configs() { step 6 9 "Sync Configurations" mkdir -p "$GAMEDATA_CONF_DIR" # Backup current configs before overwriting local cb=$(backup_configs) info "Config back-up: %s" "$(basename "$cb")" 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 # Standard *.example files (ui-config, client-mode, etc.) 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 # Repo ships renamed examples that don't match the standard *.example glob: # renderer-config.json.example.json -> renderer-config.json # renderer-config..jsonc.example.json -> renderer-config.jsonc # (the ".." in that second name is a quirk of the upstream repo). Map them # explicitly so we never emit garbage like renderer-config.jsonc.json. local -A rc_map=( ["renderer-config.json.example.json"]="renderer-config.json" ["renderer-config..jsonc.example.json"]="renderer-config.jsonc" ) for ex in "${!rc_map[@]}"; do local src="$NITRO_SRC_DIR/$ex" [ -f "$src" ] || continue local tgt="${rc_map[$ex]}" node "$ms" "$src" "$NITRO_SRC_DIR/$tgt" 2>/dev/null || true node "$ms" "$src" "$GAMEDATA_CONF_DIR/$tgt" 2>/dev/null || true [ -d "$dcd" ] && cp "$NITRO_SRC_DIR/$tgt" "$dcd/$tgt" 2>/dev/null || true ec=$((ec+1)) done ok "%s config file(s) synced" "$ec" # Validate the generated configs validate_configs "$NITRO_SRC_DIR" # NOTE: legacy *.json5 leftovers in the config dirs are intentionally left # in place — this updater never deletes user files, it only adds what is # missing from the upstream examples. # Pre-flight: warn if NITRO_IMAGE_LIBRARY_URL looks wrong before applying configs if [ -n "$NITRO_IMAGE_LIBRARY_URL" ]; then case "$NITRO_IMAGE_LIBRARY_URL" in *://*/nitro-client*|*://*/nitro-client/) fail "NITRO_IMAGE_LIBRARY_URL points to the Nitro client path instead of /gamedata/c_images/ — images will 404" ;; esac case "$NITRO_IMAGE_LIBRARY_URL" in */c_images/) ;; *://*) warn "NITRO_IMAGE_LIBRARY_URL does not contain /c_images/ — verify it points to the c_images directory" ;; esac fi info "Applying critical config URLs..." # Use the node-based sync so JSONC configs (which contain // comments and # URLs like https://) are parsed and rewritten safely. The previous Python # json.load choked on the comments and silently skipped the URL injection, # leaving the example.com placeholders in place. NITRO_SRC_DIR="$NITRO_SRC_DIR" NITRO_DIST_CONFIG_DIR="$dcd" NITRO_GAMEDATA_CONF_DIR="$GAMEDATA_CONF_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" \ NITRO_URL_FORCE="${NITRO_URL_FORCE:-0}" \ node "$SCRIPT_DIR/scripts/sync-nitro-urls.cjs" 2>/dev/null && ok "All config URLs synced" || warn "Config URL sync failed" # Flush Redis so new config is picked up immediately if command -v redis-cli &>/dev/null; then redis-cli FLUSHALL 2>/dev/null && ok "Redis cache flushed" || true fi fix_perms } do_cleanup() { step 7 9 "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 # Clean old config backups (keep 10 most recent) local cb_count=$(find "$CONFIG_BACKUP_DIR" -maxdepth 1 -type d ! -name 'rollback' 2>/dev/null | wc -l) if [ "$cb_count" -gt 10 ]; then find "$CONFIG_BACKUP_DIR" -maxdepth 1 -type d ! -name 'rollback' -printf '%T@ %p\n' 2>/dev/null | sort -rn | tail -n +11 | sed 's/^[0-9.]* //' | xargs -r rm -rf || true ok "Old config backups pruned" fi [ -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 # NOTE: the per-repo yarn cache ($YARN_CACHE_DIR) is intentionally kept # between updates — it is what makes parallel installs fast and reliable. # It is only removed on an explicit "Clean" (menu) action. yarn cache clean 2>/dev/null || true rm -rf /tmp/nitro-* /tmp/epicnext-* 2>/dev/null || true # Clean old Maven build artifacts (older than 7 days) find "$EMULATOR_MODULE" -maxdepth 3 -name '*.jar' -mtime +7 2>/dev/null -exec rm -f {} \; || true ok "Cleanup done (%s logs removed)" "$lc" } fix_perms() { # Make build output web-servable no matter which user ran the build. # Running as root we chown directly; otherwise use `sudo -n` (no TTY, so it # also works from cron). Never silently swallow failures. local runner="" [ "$(id -u)" -eq 0 ] || runner="sudo -n" local changed=false for d in "$NITRO_CLIENT" "$NITRO_RENDERER" "$GAMEDATA_CONF_DIR"; do [ -d "$d" ] || continue if $runner chown -R www-data:www-data "$d" 2>/dev/null; then changed=true else warn "Could not chown %s to www-data:www-data" "$d" fi done [ "$changed" = true ] && ok "Permissions set" } do_permissions() { step 8 9 "Set Permissions" fix_perms } # Wait for the emulator service to be active and its game port to be listening. wait_for_emulator_health() { local hc=0 while [ "$hc" -lt "$HEALTH_RETRIES" ]; do if service_active "$EMULATOR_SERVICE" && emulator_port_open; then ok "Emulator health check passed (service up + port listening)" return 0 fi hc=$((hc+1)) [ "$hc" -lt "$HEALTH_RETRIES" ] && sleep "$HEALTH_INTERVAL" done return 1 } # Resolve the newest built emulator jar filename inside the target directory. emulator_jar_name() { find "$EMULATOR_MODULE/target" -maxdepth 1 -name 'Polaris-*-jar-with-dependencies.jar' \ -printf '%T@ %p\n' 2>/dev/null | sort -rn | sed -n '1s/^[0-9.]* //p' | xargs -r basename 2>/dev/null } # Run the emulator's built-in Flyway repair against the database. This realigns # flyway_schema_history with the packaged migrations (e.g. clearing "applied # migration not resolved locally" drift) without touching hotel data. flyaway_repair_db() { local jar; jar=$(emulator_jar_name) if [ -z "$jar" ]; then fail "No emulator jar found in %s" "$EMULATOR_MODULE/target" return 1 fi command -v java >/dev/null 2>&1 || { fail "Missing: java"; return 1; } cd "$EMULATOR_MODULE/target" spinner_start "Running Flyway repair..." if timeout 300 java -Dfile.encoding=UTF8 -jar "$jar" --migrations=repair >> "$LOG_FILE" 2>&1; then spinner_stop ok ok "Flyway schema history repaired (see: %s)" "$LOG_FILE" cd "$SCRIPT_DIR" return 0 fi spinner_stop fail tail -30 "$LOG_FILE" | grep -iE 'repair|migrat|error|exception' || true cd "$SCRIPT_DIR" return 1 } # After a build/rebuild, ensure nginx redirects /gamedata/ (and /gamedata) # to /gamedata/config/ instead of serving the raw directory listing. # Idempotent: injects exact-match location blocks in front of the /gamedata/ # prefix block, validates with `nginx -t` (restoring the pre-edit copy on # failure) and keeps a timestamped backup. The actual reload happens at the # end of do_restart. ensure_gamedata_redirect() { local root_cmd="" [ "$(id -u)" -eq 0 ] || root_cmd="sudo -n" local changed=false local conf tmp snippet bak for conf in "${NGINX_SITE_CONFS[@]}"; do [ -f "$conf" ] || continue if grep -qs "location = /gamedata" "$conf" 2>/dev/null; then ok "Gamedata redirect already present in %s" "$(basename "$conf")" continue fi tmp=$(mktemp) mkdir -p /var/backups/nginx-cms 2>/dev/null || true snippet=" # Redirect /gamedata/ to /gamedata/config/ location = /gamedata { return 301 /gamedata/config/; } location = /gamedata/ { return 301 /gamedata/config/; } " awk -v s="$snippet" ' /^[[:space:]]*location \/gamedata\/ \{/ && !done { printf "%s", s done = 1 } { print } ' "$conf" > "$tmp" 2>/dev/null if [ ! -s "$tmp" ] || ! grep -qs "location = /gamedata" "$tmp" 2>/dev/null; then rm -f "$tmp" 2>/dev/null || true warn "Could not inject gamedata redirect into %s" "$conf" continue fi bak="/var/backups/nginx-cms/$(basename "$conf").bak-$(date +%s)" $root_cmd cp "$conf" "$bak" 2>/dev/null || { rm -f "$tmp"; warn "Cannot backup %s" "$conf"; continue; } if ! $root_cmd install -m 640 "$tmp" "$conf" 2>/dev/null; then rm -f "$tmp" 2>/dev/null || true warn "Cannot update %s" "$conf" continue fi rm -f "$tmp" 2>/dev/null || true if ! sudo nginx -t >/dev/null 2>&1; then $root_cmd cp "$bak" "$conf" 2>/dev/null || true warn "nginx -t failed — reverted gamedata redirect in %s" "$conf" continue fi changed=true ok "Added /gamedata/ → /gamedata/config/ redirect to %s" "$(basename "$conf")" done return 0 } # Resolve the emulator's public game (WebSocket) port — the port the Nitro # client actually connects to (socket.url -> wss://ws. -> here). # # NOTE: Emulator config.ini ships a stale `game.port=3000` that does NOT speak # the game WebSocket (it is an unused legacy listener); the real game socket # listens on 2096. We therefore do NOT trust config.ini and instead: # 1. explicit NITRO_EMULATOR_GAME_PORT override # 2. auto-detect a listening port of the emulator java process that is not a # well-known non-game port (3000/3001/3002/3030/3306/5432/6379/80/443/...). # This reliably yields 2096. # 3. fall back to 2096 (the conventional Nitro/Polaris game port). emulator_game_port() { [ -n "${NITRO_EMULATOR_GAME_PORT:-}" ] && { echo "$NITRO_EMULATOR_GAME_PORT"; return; } local pid pid=$(pgrep -f 'jar.*[Pp]olaris' 2>/dev/null | head -1) [ -z "$pid" ] && pid=$(pgrep -x java 2>/dev/null | head -1) if [ -n "$pid" ] && command -v ss >/dev/null 2>&1; then local ports ports=$(ss -ltnp 2>/dev/null | grep -E "pid=$pid," | grep -oE ':[0-9]+ ' | tr -d ' :' | sort -n -u) for cand in $ports; do case "$cand" in 80|443|3000|3001|3002|3030|3306|5432|6379|8080|8081|8422|9443|9444|5000|22|53|38939) continue ;; *) echo "$cand"; return ;; esac done fi echo 2096 } # Ensure nginx terminates the game WebSocket. The client connects to # wss://ws. (port 443) and that has to be proxied to the emulator's # game port, otherwise the client loads but never joins the server. Idempotent: # injects a server block (reusing the existing SSL cert), validates with # `nginx -t`, and reloads. Derives the domain automatically so this self-heals # on any hotel VPS. ensure_ws_proxy() { [ ${#NGINX_SITE_CONFS[@]} -eq 0 ] && return 0 local domain="${NITRO_DOMAIN:-}" if [ -z "$domain" ]; then for conf in "${NGINX_SITE_CONFS[@]}"; do [ -f "$conf" ] || continue domain=$(grep -oE 'server_name[^;]+;' "$conf" 2>/dev/null | head -1 \ | tr -s ' ' '\n' | grep -vE 'server_name|;|127\.0\.0\.1|172\.|10\.|192\.168|localhost|\[?' | head -1) [ -n "$domain" ] && break done fi [ -z "$domain" ] && { warn "Could not derive domain for ws proxy — skipping"; return 0; } local ws_name="ws.${domain}" local gport; gport=$(emulator_game_port) local root_cmd="" [ "$(id -u)" -eq 0 ] || root_cmd="sudo -n" mkdir -p /var/backups/nginx-cms 2>/dev/null || true local cert="" key="" for conf in "${NGINX_SITE_CONFS[@]}"; do [ -f "$conf" ] || continue cert=$(grep -m1 'ssl_certificate[[:space:]]' "$conf" 2>/dev/null | awk '{print $2}' | tr -d ';') key=$(grep -m1 'ssl_certificate_key[[:space:]]' "$conf" 2>/dev/null | awk '{print $2}' | tr -d ';') [ -n "$cert" ] && [ -n "$key" ] && break done [ -z "$cert" ] && cert="/etc/ssl/epicnabbo-backend.pem" [ -z "$key" ] && key="/etc/ssl/epicnabbo-backend.key" local conf for conf in "${NGINX_SITE_CONFS[@]}"; do [ -f "$conf" ] || continue if grep -qs "$ws_name" "$conf" 2>/dev/null; then ok "Game WebSocket proxy already present for %s" "$ws_name" continue fi local tmp; tmp=$(mktemp) local snippet snippet="server { listen 9443 ssl; listen [::]:9443 ssl; http2 on; server_name $ws_name; ssl_certificate $cert; ssl_certificate_key $key; ssl_protocols TLSv1.2 TLSv1.3; ssl_prefer_server_ciphers off; location / { proxy_pass http://127.0.0.1:$gport; proxy_http_version 1.1; proxy_set_header Upgrade \$http_upgrade; proxy_set_header Connection \"upgrade\"; proxy_set_header Host \$host; proxy_set_header X-Real-IP \$remote_addr; proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto \$scheme; proxy_read_timeout 86400; } } " printf "%s\n" "$snippet" > "$tmp" cat "$conf" >> "$tmp" 2>/dev/null local bak="/var/backups/nginx-cms/$(basename "$conf").bak-ws-$(date +%s)" $root_cmd cp "$conf" "$bak" 2>/dev/null || { rm -f "$tmp"; warn "Cannot backup %s" "$conf"; continue; } if ! $root_cmd install -m 640 "$tmp" "$conf" 2>/dev/null; then rm -f "$tmp" 2>/dev/null || true warn "Cannot update %s" "$conf" continue fi rm -f "$tmp" 2>/dev/null || true if ! sudo nginx -t >/dev/null 2>&1; then $root_cmd cp "$bak" "$conf" 2>/dev/null || true warn "nginx -t failed — reverted ws proxy in %s" "$conf" continue fi ok "Added game WebSocket proxy %s -> 127.0.0.1:%s" "$ws_name" "$gport" done } # Ensure the Octane client's emulator game API is reachable same-origin. The # client calls /api/auth/* (login, server-key, sso-token, register, change-*, # remember, refresh, ...) and /api/badges/custom via api.url (same origin = # the CMS domain). The Polaris game server serves these on its game port (2096); # the CMS does NOT. Proxying them here keeps api.url same-origin (no CORS) while # still reaching the emulator. Inserted before the generic `location /api/` # (which sends everything else to the CMS on :3002) inside the HTTPS vhost that # listens on 9443. Idempotent: skips when the block is already present. ensure_api_proxy() { [ ${#NGINX_SITE_CONFS[@]} -eq 0 ] && return 0 local gport; gport=$(emulator_game_port) local root_cmd="" [ "$(id -u)" -eq 0 ] || root_cmd="sudo -n" mkdir -p /var/backups/nginx-cms 2>/dev/null || true local snippet=" # --- Nitro/Emulator game API (managed by update-Nitrov3.sh: ensure_api_proxy) --- # Octane client calls /api/auth/* and /api/badges/custom via api.url (same # origin). The emulator serves these on its game port; proxy them so the # client stays same-origin (no CORS). Inserted before the generic /api/. location /api/auth/ { proxy_pass http://127.0.0.1:${gport}; proxy_http_version 1.1; proxy_set_header Host \$host; proxy_set_header X-Real-IP \$remote_addr; proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto \$scheme; add_header Cache-Control \"no-cache, no-store, must-revalidate\"; } location /api/badges/custom { proxy_pass http://127.0.0.1:${gport}; proxy_http_version 1.1; proxy_set_header Host \$host; proxy_set_header X-Real-IP \$remote_addr; proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto \$scheme; add_header Cache-Control \"no-cache, no-store, must-revalidate\"; } " local conf for conf in "${NGINX_SITE_CONFS[@]}"; do [ -f "$conf" ] || continue if grep -qs "location /api/auth/" "$conf" 2>/dev/null; then ok "Emulator /api/auth proxy already present in %s" "$(basename "$conf")" continue fi local bak="/var/backups/nginx-cms/$(basename "$conf").bak-api-$(date +%s)" $root_cmd cp "$conf" "$bak" 2>/dev/null || { warn "Cannot backup %s" "$conf"; continue; } # Insert the snippet before the first `location /api/ {` inside the # server block that listens on 9443 (the HTTPS client vhost). if ! SNIPPET="$snippet" $root_cmd perl -i -pe ' BEGIN { $https=0; $inserted=0; $snip=$ENV{SNIPPET}; } if (/^\s*server\s*\{/) { $https=0; } if (/^\s*listen\s+9443\b/) { $https=1; } if ($https && !$inserted && /^\s*location\s+\/api\/\s*\{/) { $_ = $snip . $_; $inserted=1; } ' "$conf" 2>/dev/null; then $root_cmd cp "$bak" "$conf" 2>/dev/null || true warn "Failed to insert emulator /api proxy into %s" "$(basename "$conf")" continue fi if ! sudo nginx -t >/dev/null 2>&1; then $root_cmd cp "$bak" "$conf" 2>/dev/null || true warn "nginx -t failed — reverted emulator /api proxy in %s" "$(basename "$conf")" continue fi ok "Added emulator /api proxy to %s" "$(basename "$conf")" done } # Ensure a catch-all default_server exists on the nginx vhost port. L7 proxies # such as Traefik health-check the upstream using the IP as the Host header, # which matches no real vhost; without this block those checks hit the first # server block (e.g. the ws proxy) and get a 400, marking the site unhealthy # (HTTP 503). Answering /health with 200 keeps proxy health checks green. ensure_default_server() { [ ${#NGINX_SITE_CONFS[@]} -eq 0 ] && return 0 local found=false for conf in "${NGINX_SITE_CONFS[@]}"; do [ -f "$conf" ] || continue if grep -qs "listen.*default_server" "$conf" 2>/dev/null; then found=true; break; fi done $found && { ok "Default-server health block already present"; return 0; } local root_cmd="" [ "$(id -u)" -eq 0 ] || root_cmd="sudo -n" local cert="" key="" for conf in "${NGINX_SITE_CONFS[@]}"; do [ -f "$conf" ] || continue cert=$(grep -m1 'ssl_certificate[[:space:]]' "$conf" 2>/dev/null | awk '{print $2}' | tr -d ';') key=$(grep -m1 'ssl_certificate_key[[:space:]]' "$conf" 2>/dev/null | awk '{print $2}' | tr -d ';') [ -n "$cert" ] && [ -n "$key" ] && break done [ -z "$cert" ] && cert="/etc/ssl/epicnabbo-backend.pem" [ -z "$key" ] && key="/etc/ssl/epicnabbo-backend.key" local conf for conf in "${NGINX_SITE_CONFS[@]}"; do [ -f "$conf" ] || continue local tmp; tmp=$(mktemp) mkdir -p /var/backups/nginx-cms 2>/dev/null || true cat > "$tmp" <> "$tmp" 2>/dev/null local bak="/var/backups/nginx-cms/$(basename "$conf").bak-default-$(date +%s)" $root_cmd cp "$conf" "$bak" 2>/dev/null || { rm -f "$tmp"; warn "Cannot backup %s" "$conf"; continue; } if ! $root_cmd install -m 640 "$tmp" "$conf" 2>/dev/null; then rm -f "$tmp" 2>/dev/null || true warn "Cannot update %s" "$conf" continue fi rm -f "$tmp" 2>/dev/null || true if ! sudo nginx -t >/dev/null 2>&1; then $root_cmd cp "$bak" "$conf" 2>/dev/null || true warn "nginx -t failed — reverted default-server block in %s" "$conf" continue fi ok "Added default-server /health block to %s" "$(basename "$conf")" done } do_restart() { step 9 9 "Restart Services" ensure_gamedata_redirect ensure_default_server ensure_ws_proxy ensure_api_proxy if systemctl cat "$EMULATOR_SERVICE" &>/dev/null; then # Only bounce the emulator when it was genuinely rebuilt this run, or # when it is not actually serving — "nothing changed" stays online. if [ "$EMULATOR_BUILT" = true ] || ! emulator_port_open; then sudo systemctl restart "$EMULATOR_SERVICE" 2>/dev/null && ok "$EMULATOR_SERVICE restarted" || warn "Restart failed" else ok "$EMULATOR_SERVICE already healthy — no restart needed" fi if ! wait_for_emulator_health; then warn "Emulator not healthy after %s retries" "$HEALTH_RETRIES" sudo journalctl -u "$EMULATOR_SERVICE" --no-pager -n 20 2>/dev/null || true # Flyway schema drift (an applied migration no longer resolvable # locally) aborts startup. Repair the history table and retry once # before giving up, so updates survive upstream migration renames. local flyway_drift=false if sudo journalctl -u "$EMULATOR_SERVICE" --no-pager -n 300 2>/dev/null \ | grep -qiE 'applied migration not resolved locally|FlywayValidateException|Migrations have failed validation'; then flyway_drift=true warn "Flyway schema drift detected — running repair and retrying" # Halt the restart loop so repair cannot race migrate on the history table sudo systemctl stop "$EMULATOR_SERVICE" 2>/dev/null || true if flyaway_repair_db; then sudo systemctl start "$EMULATOR_SERVICE" 2>/dev/null || true if wait_for_emulator_health; then ok "Emulator healthy after Flyway repair" flyway_drift=false fi else sudo systemctl start "$EMULATOR_SERVICE" 2>/dev/null || true fi fi if [ "$flyway_drift" = true ]; then ERRORS=$((ERRORS + 1)) ROLLBACK_NEEDED=true fi fi 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 # CMS (Next.js under PM2): restart only when it was rebuilt this run, or # when it is not answering — keep an already-online CMS untouched. if command -v pm2 &>/dev/null && [ -f "$CMS_DIR/package.json" ]; then local cms_restarted=false if [ "$CMS_BUILT" = true ]; then if pm2 restart "$CMS_PM2_NAME" 2>/dev/null; then ok "$CMS_PM2_NAME restarted" cms_restarted=true elif pm2 restart all 2>/dev/null; then ok "PM2 restarted" cms_restarted=true else warn "PM2 restart failed for %s" "$CMS_PM2_NAME" fi elif ! wait_for_cms_health; then warn "CMS unhealthy — restarting %s" "$CMS_PM2_NAME" pm2 restart "$CMS_PM2_NAME" 2>/dev/null && ok "$CMS_PM2_NAME restarted" && cms_restarted=true || warn "PM2 restart failed" else ok "$CMS_PM2_NAME already healthy — no restart needed" fi if [ "$cms_restarted" = true ]; then wait_for_cms_health || ERRORS=$((ERRORS + 1)) fi fi # Reload nginx. Prefer `nginx -s reload` (the running master picks it up # immediately); fall back to systemctl for hosts where nginx is a unit. if command -v nginx &>/dev/null && sudo nginx -t >/dev/null 2>&1; then if sudo nginx -s reload >/dev/null 2>&1 || sudo systemctl reload nginx >/dev/null 2>&1; then ok "Nginx reloaded" else warn "Nginx reload failed" fi fi command -v redis-cli &>/dev/null && redis-cli FLUSHALL 2>/dev/null && ok "Redis flushed" } # Probe the emulator's configured TCP port to confirm it is actually serving, # not merely that systemd reports the unit as active. emulator_port_open() { local port="${NITRO_EMULATOR_PORT:-}" if [ -z "$port" ]; then port=$(emulator_game_port) fi if command -v redis-cli >/dev/null 2>&1; then local p; p=$(redis-cli GET nitro:emulator:port 2>/dev/null) [ -n "$p" ] && port="$p" fi if command -v nc >/dev/null 2>&1; then nc -z 127.0.0.1 "$port" -w 3 2>/dev/null && return 0 fi if command -v ss >/dev/null 2>&1; then ss -ltn 2>/dev/null | grep -q ":$port " && return 0 fi timeout 3 bash -c "exec 3<>/dev/tcp/127.0.0.1/$port" 2>/dev/null && return 0 return 1 } 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}✔ $(_t "UPDATE COMPLETED SUCCESSFULLY")${C_RESET} ${C_CYAN}║${C_RESET}" else echo -e " ${C_CYAN}║${C_RESET} ${C_YELLOW}${C_BOLD}⚠ $(_t "COMPLETED WITH") $ERRORS $(_t "ERROR(S)")${C_RESET} ${C_CYAN}║${C_RESET}" fi echo -e " ${C_CYAN}╠══════════════════════════════════════════════════════════════════╣${C_RESET}" printf " ${C_CYAN}║${C_RESET} $(_t "Duration:") %-46s${C_CYAN}║${C_RESET}\n" "$ef" printf " ${C_CYAN}║${C_RESET} $(_t "Updates:") %-46s${C_CYAN}║${C_RESET}\n" "$([ "$HAD_UPDATES" = true ] && echo "$(_t "Yes")" || echo "$(_t "No")")" printf " ${C_CYAN}║${C_RESET} $(_t "Octane build:") %-46s${C_CYAN}║${C_RESET}\n" "$([ "$NITRO_BUILT" = true ] && echo "$(_t "Yes")" || echo "$(_t "No")")" printf " ${C_CYAN}║${C_RESET} $(_t "Warnings:") %-46s${C_CYAN}║${C_RESET}\n" "$WARNINGS" [ ${#UPDATED_REPOS[@]} -gt 0 ] && printf " ${C_CYAN}║${C_RESET} $(_t "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 "" acquire_lock DRY_RUN=${DRY_RUN:-false} local mode_label="${SELECTIVE_UPDATE:-full}" [ "$DRY_RUN" = true ] && mode_label="$mode_label [DRY-RUN]" info "Site: %s | Branch: %s | Mode: %s" "${NITRO_SITE_URL:-?}" "$NITRO_BRANCH" "$mode_label" info "Log: %s" "$LOG_FILE" if [ "$DRY_RUN" = true ]; then info "Dry-run: would update repos, run DB backups, build, sync configs, cleanup, restart services" show_summary release_lock return fi step 1 9 "System Diagnostics" preflight_check case "${SELECTIVE_UPDATE:-full}" in emulator) update_emulator # A freshly rebuilt jar must be loaded — bounce the service and # wait for it to come back healthy. if [ "$EMULATOR_BUILT" = true ] && systemctl cat "$EMULATOR_SERVICE" &>/dev/null; then sudo systemctl restart "$EMULATOR_SERVICE" 2>/dev/null && ok "$EMULATOR_SERVICE restarted" wait_for_emulator_health || warn "Emulator not healthy after %s retries" "$HEALTH_RETRIES" fi ;; renderer) update_renderer ;; client) update_client ;; configs) sync_configs ;; dev) # Quick dev mode: skip builds, just sync configs and reload info "Dev mode: skipping builds, syncing configs only" sync_configs do_restart ;; *) RUN_ROLLBACK=true update_emulator # Always attempt the renderer + client builds in parallel. Each # update_* function runs git_update itself and skips when its repo # is already up to date, so gating these on the emulator's update # status would wrongly skip them when only client/renderer changed. parallel_run "renderer" update_renderer parallel_run "client" update_client parallel_wait if ! parallel_failed; then warn "Parallel build(s) failed: %s — aborting before restart" "${PARALLEL_FAILED[*]}" ROLLBACK_NEEDED=true die "Build failure in: %s" "${PARALLEL_FAILED[*]}" fi update_cms sync_configs do_cleanup do_permissions do_restart RUN_ROLLBACK=false [ -n "$BINARY_BACKUP_DIR" ] && rm -rf "$BINARY_BACKUP_DIR" 2>/dev/null || true ;; esac show_summary fix_perms release_lock } # ============================================================================= # 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 $MYSQL_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: %s — %s" "$(format_size "$sz")" "$(basename "$bf")" else # The DB user may lack routine/trigger/event privileges (common on # restricted MySQL accounts). Retry data-only instead of failing, and # never leave a half-written dump behind. rm -f "$bf" 2>/dev/null || true if $MYSQL_DUMP $MYSQL_CRED --force --skip-lock-tables "$DB_NAME" > "$bf" 2>/dev/null; then local sz2=$(stat -c%s "$bf" 2>/dev/null || echo 0); spinner_stop ok warn "Routines/triggers/events skipped (insufficient privileges)" ok "Backup: %s — %s" "$(format_size "$sz2")" "$(basename "$bf")" else rm -f "$bf" 2>/dev/null || true spinner_stop fail; die "Backup failed" fi 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 < <(list_sorted "$BACKUP_DIR" '*.sql') [ ${#backups[@]} -eq 0 ] && die "No backups" echo "" echo -e " ${C_BOLD}${C_CYAN}$(_t "Backups:")${C_RESET}" local i=1 for bk in "${backups[@]}"; do local sz=$(format_size "$(stat -c%s "$bk" 2>/dev/null || echo 0)") 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 $(_t "Select") [1-$((i-1))]: " local c; read -rt 30 c [[ "$c" =~ ^[0-9]+$ ]] && [ "$c" -ge 1 ] && [ "$c" -le ${#backups[@]} ] || die "$(_t "Invalid")" local sel="${backups[$((c-1))]}" if ! confirm "Restore $(basename "$sel")? OVERWRITES database!"; then info "Cancelled"; return; fi spinner_start "Restoring..." $MYSQL_CLIENT $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" "$(repeat '─' 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" "$(format_size "$s")" done < <(list_sorted "$BACKUP_DIR" '*.sql') printf " ${C_DIM}%s${C_RESET}\n" "$(repeat '─' 56)" echo -e " ${C_BOLD}$(_t "Total:") $t $(_t "backups") — $(format_size "$ts")${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=$($MYSQL_CLIENT $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=$($MYSQL_CLIENT $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_backend)" "$EMULATOR_SERVICE" php-fpm cron; do local st="${C_DIM}N/A${C_RESET}" service_active "$svc" && 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_REPO" "Octane:$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 "?") local ah=$(cd "$pa" && git rev-list --count "origin/$(git branch --show-current 2>/dev/null)"..HEAD 2>/dev/null || echo "0") local bh=$(cd "$pa" && git rev-list --count HEAD.."origin/$(git branch --show-current 2>/dev/null)" 2>/dev/null || echo "0") printf " ${C_CYAN}║${C_RESET} %-12s ${C_CYAN}%-8s${C_RESET} %s" "$nm" "$br" "$cm" [ "$ah" -gt 0 ] && printf " ${C_RED}+${ah}${C_RESET}" || printf " " [ "$bh" -gt 0 ] && printf " ${C_YELLOW}-${bh}${C_RESET}" || printf " " printf " ${C_CYAN}║${C_RESET}\n" 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)); local lbl="$1"; shift; "$@" &>/dev/null && { p=$((p+1)); ok "$(_t "$lbl")"; } || { f=$((f+1)); fail "$(_t "$lbl")"; }; } checkw() { t=$((t+1)); local lbl="$1"; shift; "$@" &>/dev/null && { p=$((p+1)); ok "$(_t "$lbl")"; } || { w=$((w+1)); warn "$(_t "$lbl")"; }; } echo "" echo -e " ${C_BOLD}$(_t "Commands:")${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 checkw "MariaDB" command -v "$MYSQL_CLIENT" echo -e "\n ${C_BOLD}$(_t "Script:")${C_RESET}" checkw "$(_t "Bash syntax")" bash -n "$SCRIPT_DIR/$SCRIPT_NAME" if command -v shellcheck &>/dev/null; then checkw "ShellCheck" shellcheck --quiet "$SCRIPT_DIR/$SCRIPT_NAME"; fi if command -v checkbashisms &>/dev/null; then checkw "$(_t "Bashisms")" checkbashisms "$SCRIPT_DIR/$SCRIPT_NAME"; fi echo -e "\n ${C_BOLD}$(_t "Configs:")${C_RESET}" checkw "$(_t "Config dir")" test -d "$NITRO_SRC_DIR" local cerr=0 for cf in "$NITRO_SRC_DIR"/*.json; do [ -f "$cf" ] || continue case "$(basename "$cf")" in *jsonc*|*.example*) continue ;; esac python3 -m json.tool "$cf" >/dev/null 2>&1 || cerr=$((cerr+1)) done [ "$cerr" -eq 0 ] && ok "$(_t "All JSON valid")" || warn "$(_t "JSON errors:") $cerr" echo -e "\n ${C_BOLD}$(_t "Config URLs:")${C_RESET}" for key in NITRO_SITE_URL NITRO_API_URL NITRO_SOCKET_URL NITRO_GAMEDATA_URL NITRO_IMAGE_LIBRARY_URL; do local val="${!key}" if [ -z "$val" ]; then warn "$key" else case "$key" in NITRO_IMAGE_LIBRARY_URL) case "$val" in */c_images/) ok "$key" ;; *) warn "$key missing /c_images/ in path: $val" ;; esac ;; NITRO_SITE_URL) case "$val" in */nitro-client*|*/nitro-client) warn "$key should be the site URL, not the Nitro client path" ;; *) ok "$key" ;; esac ;; *) ok "$key" ;; esac fi done echo -e "\n ${C_BOLD}$(_t "Directories:")${C_RESET}" checkw "Emulator" test -d "$EMULATOR_MODULE"; checkw "Octane" test -d "$NITRO_CLIENT"; checkw "Renderer" test -d "$NITRO_RENDERER" checkw "$(_t "Gamedata config")" test -d "$GAMEDATA_CONF_DIR"; checkw "$(_t "Backup dir")" test -d "$BACKUP_DIR" echo -e "\n ${C_BOLD}$(_t "Services:")${C_RESET}" checkw "MariaDB" service_active mariadb; checkw "Redis" redis_active; checkw "Nginx" service_active nginx checkw "$EMULATOR_SERVICE" service_active "$EMULATOR_SERVICE" echo -e "\n ${C_BOLD}$(_t "Network:")${C_RESET}" checkw "$(_t "Nginx config")" nginx -t 2>/dev/null if command -v redis-cli &>/dev/null; then checkw "Redis PING" redis-cli PING 2>/dev/null; fi checkw "$(_t "Site reachable")" timeout 5 curl -sf "${NITRO_SITE_URL:-}" 2>/dev/null if emulator_port_open; then ok "$(_t "Emulator port open")"; else warn "$(_t "Emulator port closed")"; fi echo -e "\n ${C_BOLD}$(_t "Databases:")${C_RESET}" checkw "$(_t "MySQL connect")" $MYSQL_CLIENT $MYSQL_CRED -e "SELECT 1" "$DB_NAME" 2>/dev/null local dbt=$($MYSQL_CLIENT $MYSQL_CRED -N -e "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='$DB_NAME'" "$DB_NAME" 2>/dev/null || echo "?") t=$((t+1)); [ "$dbt" != "?" ] && { p=$((p+1)); ok "$(_t "Tables:") $dbt"; } || { f=$((f+1)); fail "$(_t "Tables:") ?"; } echo -e "\n ${C_BOLD}$(_t "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 "$(_t "Disk:") ${ag}GB"; } || { f=$((f+1)); fail "$(_t "Disk:") ${ag}GB"; } local mem=$(free -m 2>/dev/null | awk '/^Mem:/{printf "%dMB / %dMB", $3, $2}') t=$((t+1)); ok "$(_t "Memory:") $mem" local load=$(awk '{printf "%.1f", $1}' /proc/loadavg 2>/dev/null || echo "?") local cores=$(nproc 2>/dev/null || echo "?") t=$((t+1)); ok "$(_t "Load:") $load / $cores cores" 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}% — $(_t "EXCELLENT")${C_RESET}" 50 elif [ "$sc" -ge 70 ]; then box_line "${C_YELLOW}${C_BOLD}${sc}% — $(_t "GOOD")${C_RESET}" 50 else box_line "${C_RED}${C_BOLD}${sc}% — $(_t "NEEDS ATTENTION")${C_RESET}" 50; fi box_bottom 50; echo "" } # ============================================================================= # FLYAWAY REPAIR # ============================================================================= cmd_flyaway_repair() { echo "" acquire_lock DRY_RUN=${DRY_RUN:-false} info "Site: %s | Branch: %s | Mode: %s" "${NITRO_SITE_URL:-?}" "$NITRO_BRANCH" "flyaway-repair" info "Log: %s" "$LOG_FILE" if [ "$DRY_RUN" = true ]; then info "Dry-run: would check repos, reinstall deps, fix permissions, sync configs, restart services" show_summary release_lock return fi step 1 7 "System Diagnostics" preflight_check step 2 7 "$(_t "Flyaway Repair")" local repaired=0 for repo in "$EMULATOR_REPO" "$NITRO_CLIENT" "$NITRO_RENDERER"; do [ -d "$repo/.git" ] || continue local name; name=$(basename "$repo") info "Checking %s..." "$name" # Reset any dirty/uncommitted changes that may cause issues cd "$repo" if ! git diff --quiet 2>/dev/null || ! git diff --cached --quiet 2>/dev/null; then warn "Dirty working tree in %s — stashing changes" "$name" git stash --include-untracked 2>/dev/null || true fi # Reset to clean latest of the current branch local branch; branch=$(git branch --show-current 2>/dev/null || echo "main") if git checkout "$branch" 2>/dev/null; then git checkout . 2>/dev/null || true git clean -fd 2>/dev/null || true ok "Cleaned %s working tree" "$name" fi cd "$SCRIPT_DIR" # Reinstall dependencies if package.json exists if [ -f "$repo/package.json" ]; then spinner_start "Installing deps for %s..." "$name" if yarn_install_repo "$repo" --frozen-lockfile --prefer-offline 2>/dev/null; then spinner_stop ok ok "Deps reinstalled for %s" "$name" else spinner_stop fail warn "yarn install failed for %s — resetting repo and retrying" "$name" yarn_reset_repo "$repo" yarn_install_repo "$repo" --frozen-lockfile --prefer-offline 2>/dev/null || warn "yarn install still failed for %s" "$name" fi fi # Re-link renderer: register global link + link into client if [ "$repo" = "$NITRO_RENDERER" ] && [ -d "$repo" ]; then spinner_start "Re-linking renderer..." cd "$NITRO_RENDERER" yarn link >> "$LOG_FILE" 2>&1 || warn "yarn link in renderer failed" cd "$SCRIPT_DIR" cd "$NITRO_CLIENT" yarn link "@nitrots/nitro-renderer" >> "$LOG_FILE" 2>&1 || warn "yarn link to renderer failed" cd "$SCRIPT_DIR" spinner_stop ok ok "Renderer re-linked" fi repaired=$((repaired + 1)) done # Ensure .nitro-build.json exists if [ ! -f "$NITRO_CLIENT/.nitro-build.json" ]; then echo '{"jsonMode":"jsonc"}' > "$NITRO_CLIENT/.nitro-build.json" ok "Created .nitro-build.json (jsonc mode)" fi # Realign Flyway schema history with the packaged migrations step 3 7 "$(_t "Flyway DB Repair")" if flyaway_repair_db; then ok "$(_t "Flyway schema history repaired")" else fail "$(_t "Flyway DB repair failed — the emulator will retry on restart")" fi # Fix permissions step 4 7 "$(_t "Set Permissions")" fix_perms ok "Permissions fixed" # Sync / validate configs step 5 7 "$(_t "Sync Configurations")" sync_configs # Rebuild frontend if dist is missing or empty if [ ! -d "$NITRO_CLIENT/dist" ] || [ -z "$(ls -A "$NITRO_CLIENT/dist" 2>/dev/null)" ]; then step 6 7 "$(_t "Rebuild Frontend")" spinner_start "$(_t "Building frontend...")" cd "$NITRO_CLIENT" if yarn_run "$NITRO_CLIENT" build >> "$LOG_FILE" 2>&1; then spinner_stop ok ok "Frontend rebuilt" else spinner_stop fail fail "Frontend rebuild failed" fi cd "$SCRIPT_DIR" else ok "Frontend dist intact — skip rebuild" fi # Rebuild emulator if jar is missing if [ ! -d "$EMULATOR_MODULE/target" ] || ! ls "$EMULATOR_MODULE"/target/Polaris-*-jar-with-dependencies.jar &>/dev/null 2>&1; then step 7 7 "$(_t "Rebuild Emulator")" cd "$EMULATOR_MODULE" backup_binaries ROLLBACK_NEEDED=true spinner_start "$(_t "Building emulator (parallel)...")" command -v ccache >/dev/null 2>&1 && export CC="ccache gcc" CXX="ccache g++" if mvn package -q -T 1C -Dmaven.test.skip=true >> "$LOG_FILE" 2>&1; then spinner_stop ok ok "Emulator rebuilt" else spinner_stop fail fail "Emulator rebuild failed" fi cd "$SCRIPT_DIR" else ok "Emulator jar intact — skip rebuild" fi # Restart services do_restart show_summary fix_perms release_lock } # ============================================================================= # AUTO-SETUP COMMAND # ============================================================================= cmd_setup() { step 1 1 "Setup" echo "" acquire_lock info "Site: %s | Branch: %s | Mode: %s" "${NITRO_SITE_URL:-?}" "$NITRO_BRANCH" "auto-setup" info "Log: %s" "$LOG_FILE" auto_setup_missing_repos release_lock } # ============================================================================= # SERVICES # ============================================================================= cmd_services() { step 1 1 "Service Manager" echo "" echo -e " ${C_BOLD}1)${C_RESET} $(_t "Start Emulator") ${C_BOLD}2)${C_RESET} $(_t "Stop Emulator") ${C_BOLD}3)${C_RESET} $(_t "Restart Emulator")" echo -e " ${C_BOLD}4)${C_RESET} $(_t "Start Nginx") ${C_BOLD}5)${C_RESET} $(_t "Restart Nginx") ${C_BOLD}6)${C_RESET} $(_t "Status All")" echo -e " ${C_BOLD}7)${C_RESET} $(_t "Emulator Logs") ${C_BOLD}8)${C_RESET} $(_t "Emulator Journal") ${C_BOLD}0)${C_RESET} $(_t "Return")" echo -en "\n $(_t "Select:") "; local c; read -rt 30 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}$(_t "DOWN")${C_RESET}"; service_active "$s" && st="${C_GREEN}$(_t "UP")${C_RESET}" printf " %-25s %b\n" "$s" "$st" done ;; 7) sudo journalctl -u "$EMULATOR_SERVICE" --no-pager -n 30 ;; 8) sudo journalctl -u "$EMULATOR_SERVICE" --no-pager -n 100 --output=short-full ;; esac } # ============================================================================= # DATABASE # ============================================================================= cmd_database() { step 1 1 "Database Tools" echo "" echo -e " ${C_BOLD}1)${C_RESET} $(_t "DB Info") ${C_BOLD}2)${C_RESET} $(_t "List Tables") ${C_BOLD}3)${C_RESET} $(_t "Table Sizes")" echo -e " ${C_BOLD}4)${C_RESET} $(_t "Optimize") ${C_BOLD}5)${C_RESET} $(_t "Custom Query") ${C_BOLD}6)${C_RESET} $(_t "Active Users")" echo -e " ${C_BOLD}7)${C_RESET} $(_t "Processlist") ${C_BOLD}0)${C_RESET} $(_t "Return")" echo -en "\n $(_t "Select:") "; local c; read -rt 30 c case "$c" in 1) box_top "DB INFO" 50 local dbs=$($MYSQL_CLIENT $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:" "$($MYSQL_CLIENT $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) $MYSQL_CLIENT $MYSQL_CRED -e "SHOW TABLES FROM $DB_NAME" 2>/dev/null ;; 3) $MYSQL_CLIENT $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 $($MYSQL_CLIENT $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 $MYSQL_CLIENT $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 -rt 30 q; [ -n "$q" ] && $MYSQL_CLIENT $MYSQL_CRED "$DB_NAME" -e "$q" 2>/dev/null ;; 6) $MYSQL_CLIENT $MYSQL_CRED -e "SELECT name, motto FROM $DB_NAME.users WHERE online='1'" 2>/dev/null || warn "Could not query" ;; 7) $MYSQL_CLIENT $MYSQL_CRED -e "SHOW FULL PROCESSLIST" 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 "Octane:" "$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 < <(list_sorted "$LOG_DIR" 'update-*.log') [ ${#logs[@]} -eq 0 ] && { warn "No logs"; return; } echo "" local i=1 for l in "${logs[@]}"; do local sz=$(format_size "$(stat -c%s "$l" 2>/dev/null || echo 0)") 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 $(_t "Select") [1-$((i-1))]: "; local c; read -rt 30 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} $(_t "Yarn") ${C_BOLD}2)${C_RESET} $(_t "Next.js") ${C_BOLD}3)${C_RESET} $(_t "Redis")" echo -e " ${C_BOLD}4)${C_RESET} $(_t "Logs") ${C_BOLD}5)${C_RESET} $(_t "Everything") ${C_BOLD}6)${C_RESET} $(_t "Docker")" echo -e " ${C_BOLD}7)${C_RESET} $(_t "Maven") ${C_BOLD}0)${C_RESET} $(_t "Return")" echo -en "\n $(_t "Select:") "; local c; read -rt 30 c case "$c" in 1) yarn cache clean 2>/dev/null; rm -rf "$YARN_CACHE_DIR" 2>/dev/null; ok "Yarn cleaned" ;; 2) cd "$SCRIPT_DIR" && rm -rf .next 2>/dev/null && ok "Next.js cache cleared" ;; 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; rm -rf "$YARN_CACHE_DIR" 2>/dev/null; find "$EMULATOR_DIR" -name "*.log" -mtime +14 -exec rm -f {} \; 2>/dev/null; rm -rf /tmp/nitro-* /tmp/epicnext-* 2>/dev/null; ok "Full cleanup" ;; 6) command -v docker &>/dev/null && docker system prune -f 2>/dev/null && ok "Docker pruned" || warn "Docker N/A" ;; 7) find "$EMULATOR_MODULE" -maxdepth 3 -name '*.jar' -mtime +7 -exec rm -f {} \; 2>/dev/null; ok "Old Maven artifacts cleaned" ;; esac } # ============================================================================= # GIT LOG # ============================================================================= cmd_gitlog() { step 1 1 "Git Commit History" echo "" echo -e " ${C_BOLD}1)${C_RESET} $(_t "Emulator") ${C_BOLD}2)${C_RESET} $(_t "Octane") ${C_BOLD}3)${C_RESET} $(_t "Renderer")" echo -en "\n $(_t "Select:") "; local c; read -rt 30 c local repo="" case "$c" in 1) repo="$EMULATOR_REPO" ;; 2) repo="$NITRO_CLIENT" ;; 3) repo="$NITRO_RENDERER" ;; *) return ;; esac [ ! -d "$repo/.git" ] && { warn "Not a git repo"; return; } echo "" local branch branch=$(cd "$repo" && git branch --show-current 2>/dev/null || echo "?") local ahead ahead=$(cd "$repo" && git rev-list --count "origin/${branch}"..HEAD 2>/dev/null || echo "0") echo -e " ${C_BOLD}$(_t "Last 20 commits on") ${C_CYAN}${branch}${C_RESET} ($(_t "ahead:") ${C_RED}${ahead}${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=() load_branches branches [ ${#branches[@]} -lt 2 ] && { warn "Need 2+ branches (found ${#branches[@]})"; return; } echo -e " ${C_BOLD}$(_t "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 $(_t "Branch A:") "; local ba; read -rt 30 ba echo -en " $(_t "Branch B:") "; local bb; read -rt 30 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_REPO" "$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 $(_t "is") ${C_GREEN}$ahead${C_RESET} $(_t "ahead,") ${C_YELLOW}$behind${C_RESET} $(_t "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}$(_t "Current:")${C_RESET} ${cc:-${C_DIM}$(_t "none")${C_RESET}}" echo "" echo -e " ${C_BOLD}1)${C_RESET} $(_t "Daily 03:00") ${C_BOLD}2)${C_RESET} $(_t "Daily 04:00") ${C_BOLD}3)${C_RESET} $(_t "Weekly Sun 03:00")" echo -e " ${C_BOLD}4)${C_RESET} $(_t "Custom time") ${C_BOLD}5)${C_RESET} $(_t "Remove") ${C_BOLD}0)${C_RESET} $(_t "Return")" echo -en "\n $(_t "Select:") "; local c; read -rt 30 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 " $(_t "Minute") [0-59]: "; local m; read -rt 30 m echo -en " $(_t "Hour") [0-23]: "; local h; read -rt 30 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 } # ============================================================================= # GIT STASH VIEWER # ============================================================================= cmd_stash() { step 1 1 "Git Stashes" local found=false for repo in "$EMULATOR_REPO" "$NITRO_CLIENT" "$NITRO_RENDERER"; do [ -d "$repo/.git" ] || continue local list list=$(cd "$repo" && git stash list 2>/dev/null) if [ -n "$list" ]; then found=true echo "" echo -e " ${C_BOLD}$(basename "$repo"):${C_RESET}" echo "$list" | while IFS= read -r line; do echo -e " ${C_CYAN}→${C_RESET} $line"; done fi done $found || warn "No stashes found" echo "" } # ============================================================================= # 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" ;; dev) mode_label="Dev Mode" ;; esac local branches=() load_branches branches echo "" echo -e " ${C_BOLD}${C_CYAN}$(_t "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} $(_t "Custom")" echo -en "\n $(_t "Branch") [1-$i]: " local bc; read -rt 30 bc if [[ "$bc" =~ ^[0-9]+$ ]] && [ "$bc" -ge 1 ] && [ "$bc" -lt "$i" ]; then NITRO_BRANCH="${branches[$((bc-1))]}" elif [ "$bc" = "$i" ]; then echo -en " $(_t "Name:") "; read -rt 30 NITRO_BRANCH; [ -z "$NITRO_BRANCH" ] && NITRO_BRANCH="main" else warn "Invalid, using: %s" "$NITRO_BRANCH" fi echo -e "\n $(_t "Branch:") ${C_CYAN}${C_BOLD}$NITRO_BRANCH${C_RESET} $(_t "Mode:") ${C_GREEN}$mode_label${C_RESET}" if ! confirm "Start update?"; then info "Cancelled"; return; fi cmd_update } cmd_interactive() { local branches=() load_branches branches while true; do clear echo -e "${C_CYAN}" echo "" echo " ╔═══════════════════════════════════════════════════════════════════════════╗" echo " ║ ║" echo " ║ ██████╗ ██████╗ ██████╗ ████████╗██╗ ██╗ ██████╗ ██████╗ ║" echo " ║ ██╔══██╗██╔══██╗██╔══██╗╚══██╔══╝██║ ██║██╔═══██╗██╔══██╗ ║" echo " ║ ██████╔╝██████╔╝██████╔╝ ██║ ███████║██║ ██║██████╔╝ ║" echo " ║ ██╔═══╝ ██╔══██╗██╔══██╗ ██║ ██╔══██║██║ ██║██╔══██╗ ║" echo " ║ ██║ ██║ ██║██████╔╝ ██║ ██║ ██║╚██████╔╝██║ ██║ ║" echo " ║ ╚═╝ ╚═╝ ╚═╝╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ║" echo " ║ ║" echo " ╠═══════════════════════════════════════════════════════════════════════════╣" echo -e " ║ ${C_GOLD}${C_BOLD} SMaRT v${SCRIPT_VERSION} — Emulator / Octane / Octane-Renderer${C_CYAN} ║" echo -e " ║ ${C_WHITE}${C_BOLD} Updater & Manager by Remco — epicnabbo.nl${C_CYAN} ║" echo " ╠═══════════════════════════════════════════════════════════════════════════╣" echo -e " ║ ${C_DIM}$(_t "Branch:") ${C_CYAN}${NITRO_BRANCH}${C_DIM} (${#branches[@]} $(_t "branches")) │ $(date '+%H:%M')${C_CYAN} ║" echo " ╚═══════════════════════════════════════════════════════════════════════════╝" echo -e "${C_RESET}" echo -e " ${C_BG_GREEN}${C_WHITE}${C_BOLD} $(_t "UPDATE") ${C_RESET}" echo -e " ${C_GREEN}${C_BOLD}1)${C_RESET} ${E_ROCKET} $(_t "Quick Update — current branch")" echo -e " ${C_GREEN}${C_BOLD}2)${C_RESET} ${E_BOLT} $(_t "Full Update (choose branch)")" echo -e " ${C_GREEN}${C_BOLD}3)${C_RESET} ${E_GEAR} $(_t "Emulator Only")" echo -e " ${C_GREEN}${C_BOLD}4)${C_RESET} ${E_GEAR} $(_t "Octane Client Only")" echo -e " ${C_GREEN}${C_BOLD}5)${C_RESET} ${E_GEAR} $(_t "Renderer Only")" echo -e " ${C_GREEN}${C_BOLD}6)${C_RESET} ${E_GEAR} $(_t "Configs Only")" echo -e " ${C_GREEN}${C_BOLD}19)${C_RESET} ${E_BOLT} $(_t "Dev Mode (skip builds, only configs)")" echo "" echo -e " ${C_BG_MAGENTA}${C_WHITE}${C_BOLD} $(_t "TOOLS") ${C_RESET}" echo -e " ${C_MAGENTA}${C_BOLD}7)${C_RESET} ${E_GIT} $(_t "Switch Branch")" echo -e " ${C_YELLOW}${C_BOLD}8)${C_RESET} ${E_DB} $(_t "Database")" echo -e " ${C_YELLOW}${C_BOLD}9)${C_RESET} ${E_SHIELD} $(_t "Backup / Restore")" echo -e " ${C_YELLOW}${C_BOLD}10)${C_RESET} ${E_WRENCH} $(_t "Services")" echo -e " ${C_YELLOW}${C_BOLD}11)${C_RESET} ${E_CHART} $(_t "Status")" echo -e " ${C_YELLOW}${C_BOLD}12)${C_RESET} ${E_HEART} $(_t "Health Check")" echo -e " ${C_YELLOW}${C_BOLD}13)${C_RESET} ${E_BROOM} $(_t "Clean")" echo -e " ${C_YELLOW}${C_BOLD}14)${C_RESET} ${E_EYE} $(_t "Logs")" echo -e " ${C_YELLOW}${C_BOLD}15)${C_RESET} ${E_GEAR} $(_t "Config")" echo "" echo -e " ${C_BG_BLUE}${C_WHITE}${C_BOLD} $(_t "ADVANCED") ${C_RESET}" echo -e " ${C_BLUE}${C_BOLD}16)${C_RESET} ${E_CONSOLE} $(_t "Git Log")" echo -e " ${C_BLUE}${C_BOLD}17)${C_RESET} ${E_GLOBE} $(_t "Compare Branches")" echo -e " ${C_BLUE}${C_BOLD}18)${C_RESET} ${E_CLOCK} $(_t "Cron Scheduler")" echo -e " ${C_BLUE}${C_BOLD}20)${C_RESET} ${E_GIT} $(_t "View Git Stashes")" echo -e " ${C_RED}${C_BOLD}21)${C_RESET} ${E_WRENCH} $(_t "Flyaway Repair")" echo -e " ${C_GREEN}${C_BOLD}22)${C_RESET} ${E_ROCKET} $(_t "Auto-setup missing repos")" echo -e " ${C_YELLOW}${C_BOLD}23)${C_RESET} ${E_BROOM} $(_t "Cleanup old Nitro dirs")" echo -e " ${C_GREEN}${C_BOLD}24)${C_RESET} ${E_GLOBE} $(_t "Fix Client Links (icons/furniture/gamedata)")" echo "" echo -e " ${C_RED}${C_BOLD}0)${C_RESET} $(_t "Exit")" echo "" echo -en " ${C_CYAN}${C_BOLD}$(_t "Select [0-24]")${C_RESET}: " local choice; read -rt 30 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} $(_t "Custom")" echo -en "\n $(_t "Branch") [1-$i]: "; local bc; read -rt 30 bc if [[ "$bc" =~ ^[0-9]+$ ]] && [ "$bc" -ge 1 ] && [ "$bc" -lt "$i" ]; then NITRO_BRANCH="${branches[$((bc-1))]}" elif [ "$bc" = "$i" ]; then echo -en " $(_t "Name:") "; read -rt 30 NITRO_BRANCH fi load_branches branches ok "Branch: %s" "$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) ;; 19) SELECTIVE_UPDATE="dev"; (pick_branch_and_update) || warn "Dev mode had errors" ;; 20) (cmd_stash) ;; 21) (cmd_flyaway_repair) ;; 22) (cmd_setup) ;; 23) (cmd_cleanup_old_nitro) ;; 24) (cmd_fixlinks) ;; 0|q|Q) echo -e "\n ${C_GREEN}$(_t "Goodbye!")${C_RESET}"; cursor_show; exit 0 ;; *) warn "Invalid selection" ;; esac echo "" echo -en " ${C_DIM}$(_t "Press Enter to continue...")${C_RESET}" read -rt 30 done } # ============================================================================= # CLIENT LINK FIXER (URLs / icons / furniture / gamedata) # ============================================================================= # Resolve the public site URL the client configs must point at. Order: # explicit NITRO_SITE_URL / --url > APP_URL > nginx server_name vhost. This # lets the script self-configure on ANY hotel VPS without manual .env edits. resolve_site_url() { if [ -z "${NITRO_SITE_URL:-}" ] && [ -n "${APP_URL:-}" ]; then NITRO_SITE_URL="$APP_URL"; fi if [ -z "${NITRO_SITE_URL:-}" ]; then for _c in "${NGINX_SITE_CONFS[@]}"; do [ -f "$_c" ] || continue NITRO_SITE_URL=$(grep -oE 'server_name[^;]+;' "$_c" 2>/dev/null | head -1 \ | tr -s ' ' '\n' | grep -vE 'server_name|;|127\.0\.0\.1|172\.|10\.|192\.168|localhost|\[?' | head -1) [ -n "$NITRO_SITE_URL" ] && NITRO_SITE_URL="https://$NITRO_SITE_URL" && break done fi NITRO_SITE_URL="${NITRO_SITE_URL%/}" 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 } # Derive every client-facing URL (icons, furniture/gamedata configs, bundled # assets, websocket...) from the detected site. Explicit NITRO_*_URL values in # .env win; everything else is computed and sanitized so the client never ends # up pointed at /nitro-client/ or a path missing its trailing slash. compute_nitro_urls() { [ -n "${NITRO_SITE_URL:-}" ] || resolve_site_url 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}" # Normalize slashes. NITRO_API_URL="${NITRO_API_URL%/}" NITRO_GAMEDATA_URL="${NITRO_GAMEDATA_URL%/}" NITRO_ASSET_URL="${NITRO_ASSET_URL%/}" NITRO_HOF_FURNITURE_URL="${NITRO_HOF_FURNITURE_URL%/}" NITRO_FURNI_ASSET_ICON_URL="${NITRO_FURNI_ASSET_ICON_URL%/}" NITRO_IMAGE_LIBRARY_URL="${NITRO_IMAGE_LIBRARY_URL%/}" [ -n "$NITRO_IMAGE_LIBRARY_URL" ] && NITRO_IMAGE_LIBRARY_URL="$NITRO_IMAGE_LIBRARY_URL/" # Never let these point at the client's own path. case "$NITRO_IMAGE_LIBRARY_URL" in *"/nitro-client"*) NITRO_IMAGE_LIBRARY_URL="${NITRO_SITE_URL:-}/gamedata/c_images/"; warn "Corrected NITRO_IMAGE_LIBRARY_URL (was /nitro-client/)" ;; esac case "$NITRO_GAMEDATA_URL" in *"/nitro-client"*) NITRO_GAMEDATA_URL="${NITRO_SITE_URL:-}/gamedata"; warn "Corrected NITRO_GAMEDATA_URL (was /nitro-client/)" ;; esac } # HTTP answer code for a URL (000 = unreachable/curl failed). Never aborts the # caller, even under `set -e` — a failed probe is reported as 000, not an error. http_status() { local code code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 8 --max-redirs 4 -k -L "$1" 2>/dev/null) || code=000 if [ -n "$code" ]; then echo "$code"; else echo 000; fi } # Re-generate .gz sidecars for the heavy static assets so nginx `gzip_static` # serves them instantly (no per-request compression). Also restores any loose # original that was left behind as only the .gz sidecar — a state that would # otherwise make the origin 404 on those files. precompress_assets() { local root f base for root in "$(dirname "${GAMEDATA_CONF_DIR:-/var/www/Gamedata/config}")" "${NITRO_CLIENT:-/var/www/Octane}/dist"; do [ -d "$root" ] || continue while IFS= read -r f; do base="${f%.gz}" [ -f "$base" ] || { gunzip -k "$f" && warn "Restored missing original: %s" "$base"; } done < <(find "$root" -name "*.gz" -type f 2>/dev/null) find "$root" -type f \( -name "*.js" -o -name "*.css" -o -name "*.wasm" -o -name "*.json" -o -name "*.jsonc" \) -size +20k -print0 2>/dev/null \ | while IFS= read -r -d '' f; do gzip -k -f -6 "$f"; done done ok "$(_t "Assets compressed for fast serving")" } # TCP reachability probe for the websocket endpoint (curl cannot talk `wss://`). check_socket() { local url="$1" hostport host port [ -n "$url" ] || { warn "$(_t "Socket not configured")"; bad_urls=$((bad_urls + 1)); return 0; } hostport="${url#*://}"; hostport="${hostport%%/*}" case "$hostport" in *:*) host="${hostport%%:*}"; port="${hostport##*:}" ;; *) host="$hostport"; port="443" ;; esac if timeout 5 bash -c "exec 3<>/dev/tcp/'$host'/'$port'" 2>/dev/null; then ok "$(_t "Socket reachable") (%s:%s)" "$host" "$port" ok_urls=$((ok_urls + 1)) else fail "$(_t "Socket unreachable") (%s:%s)" "$host" "$port" bad_urls=$((bad_urls + 1)) fi } # Recompute + force-write every client-facing URL into renderer-config and # ui-config (source, dist mirror and gamedata mirror), verify the on-disk asset # directories exist and that the live URLs actually answer, then reload nginx # and flush Redis so the client picks everything up. Self-heals stale or # placeholder links (icons, furniture/gamedata, bundled assets, socket) on any # system — crypto.* settings are still never touched. cmd_fixlinks() { acquire_lock step 1 5 "$(_t "Detect Site & URLs")" echo "" resolve_site_url compute_nitro_urls if [ -z "$NITRO_SITE_URL" ]; then fail "$(_t "No site URL detected (set APP_URL or NITRO_SITE_URL)")" release_lock return 1 fi echo -e " ${C_CYAN}$(_t "Site:")${C_RESET} ${C_GREEN}${NITRO_SITE_URL}${C_RESET}" echo -e " ${C_CYAN}$(_t "Socket:")${C_RESET} ${C_GREEN}${NITRO_SOCKET_URL}${C_RESET}" echo -e " ${C_CYAN}$(_t "Gamedata:")${C_RESET} ${C_GREEN}${NITRO_GAMEDATA_URL}${C_RESET}" echo -e " ${C_CYAN}$(_t "Images:")${C_RESET} ${C_GREEN}${NITRO_IMAGE_LIBRARY_URL}${C_RESET}" step 2 5 "$(_t "Verify on-disk asset paths")" echo "" local misses=0 local gamedata_root; gamedata_root=$(dirname "$GAMEDATA_CONF_DIR") for d in "$gamedata_root/c_images" "$gamedata_root/bundled" "$gamedata_root/icons" "$GAMEDATA_CONF_DIR"; do if [ -d "$d" ]; then ok "%s" "$d"; else fail "Missing dir: %s" "$d"; misses=$((misses + 1)); fi done if [ -f "$GAMEDATA_CONF_DIR/FurnitureData.json" ]; then ok "FurnitureData.json present" else fail "Missing: %s/FurnitureData.json" "$GAMEDATA_CONF_DIR" misses=$((misses + 1)) fi if [ "$misses" -gt 0 ]; then warn "%s path(s) missing — some assets will 404" "$misses"; fi step 3 5 "$(_t "Write corrected links (forced)")" echo "" local dcd="$NITRO_CLIENT/dist/configuration" mkdir -p "$dcd" "$NITRO_SRC_DIR" "$GAMEDATA_CONF_DIR" 2>/dev/null || true if NITRO_SRC_DIR="$NITRO_SRC_DIR" NITRO_DIST_CONFIG_DIR="$dcd" NITRO_GAMEDATA_CONF_DIR="$GAMEDATA_CONF_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" \ NITRO_URL_FORCE=1 \ node "$SCRIPT_DIR/scripts/sync-nitro-urls.cjs" 2>/dev/null; then ok "$(_t "Links written to configs")" else fail "$(_t "Could not write links")" fi validate_configs "$NITRO_SRC_DIR" step 4 5 "$(_t "Validate links over HTTP")" echo "" if [ "$misses" -gt 0 ]; then warn "$(_t "Skipping some URL checks — on-disk paths missing")"; fi local chk ok_urls=0 bad_urls=0 check_url() { local label="$1" url="$2" msg="" [ -n "$url" ] || { return 0; } chk=$(http_status "$url") msg="${label}: ${url} (HTTP ${chk})" case "$chk" in 200) ok "%s" "$msg"; ok_urls=$((ok_urls + 1)) ;; [23][0-9][0-9]|40[013]) ok "%s $(_t "reachable")" "$msg"; ok_urls=$((ok_urls + 1)) ;; 404) fail "%s (404)" "$msg"; bad_urls=$((bad_urls + 1)) ;; 000) fail "%s ($(_t "unreachable"))" "$msg"; bad_urls=$((bad_urls + 1)) ;; *) warn "%s" "$msg"; bad_urls=$((bad_urls + 1)) ;; esac } check_url "$(_t "Site")" "$NITRO_API_URL" check_url "$(_t "FurnitureData")" "$NITRO_GAMEDATA_URL/config/FurnitureData.json" check_url "$(_t "Images")" "${NITRO_IMAGE_LIBRARY_URL}reception/AIRpromo_background_left.png" check_url "$(_t "Icons")" "${NITRO_HOF_FURNITURE_URL}/00011_icon.png" check_url "$(_t "Bundled assets")" "$NITRO_ASSET_URL/figure/Hair_U_leet4.nitro" check_socket "$NITRO_SOCKET_URL" if [ "$ok_urls" -gt 0 ]; then ok "%s $(_t "URL(s) OK")" "$ok_urls"; fi if [ "$bad_urls" -gt 0 ]; then warn "%s $(_t "URL(s) need attention")" "$bad_urls"; fi step 5 5 "$(_t "Apply (nginx + redis)")" echo "" ensure_gamedata_redirect ensure_ws_proxy if command -v nginx &>/dev/null && sudo nginx -t >/dev/null 2>&1; then sudo nginx -s reload >/dev/null 2>&1 || sudo systemctl reload nginx >/dev/null 2>&1 || true ok "Nginx reloaded" fi command -v redis-cli &>/dev/null && redis-cli FLUSHALL 2>/dev/null && ok "Redis flushed" || true fix_perms precompress_assets info "$(_t "Done — links fixed. Test the client now.")" release_lock return 0 } # ============================================================================= # HELP # ============================================================================= show_help() { echo "" echo -e " ${C_GOLD}${C_BOLD}$(_t "SMaRT — Emulator / Octane / Octane-Renderer Updater & Manager")${C_RESET}" echo -e " ${C_DIM}by Remco — epicnabbo.nl v${SCRIPT_VERSION} (build ${SCRIPT_BUILD})${C_RESET}" echo "" echo -e " ${C_BOLD}$(_t "USAGE:")${C_RESET} $SCRIPT_NAME [options]" echo "" echo -e " ${C_BOLD}$(_t "COMMANDS:")${C_RESET}" echo -e " ${C_GREEN}interactive${C_RESET} $(_t "Open menu (default)")" echo -e " ${C_GREEN}update${C_RESET} $(_t "Run update")" echo -e " ${C_GREEN}backup${C_RESET} $(_t "Create database backup")" echo -e " ${C_GREEN}restore${C_RESET} $(_t "Restore from backup")" echo -e " ${C_GREEN}backups${C_RESET} $(_t "List backups")" echo -e " ${C_GREEN}status${C_RESET} $(_t "System dashboard")" echo -e " ${C_GREEN}health${C_RESET} $(_t "Health check")" echo -e " ${C_GREEN}services${C_RESET} $(_t "Service manager")" echo -e " ${C_GREEN}database${C_RESET} $(_t "Database tools")" echo -e " ${C_GREEN}config${C_RESET} $(_t "Show config")" echo -e " ${C_GREEN}logs${C_RESET} $(_t "View logs")" echo -e " ${C_GREEN}clean${C_RESET} $(_t "Cache cleanup")" echo -e " ${C_GREEN}gitlog${C_RESET} $(_t "Git history")" echo -e " ${C_GREEN}compare${C_RESET} $(_t "Branch diff")" echo -e " ${C_GREEN}schedule${C_RESET} $(_t "Cron setup")" echo -e " ${C_GREEN}stash${C_RESET} $(_t "View git stashes")" echo -e " ${C_GREEN}repair${C_RESET} $(_t "Flyaway Repair")" echo -e " ${C_GREEN}setup${C_RESET} $(_t "Auto-setup missing repos")" echo -e " ${C_GREEN}cleanup-nitro${C_RESET} $(_t "Cleanup old Nitro dirs")" echo -e " ${C_GREEN}fixlinks${C_RESET} $(_t "Fix client links (icons/furniture/gamedata)")" echo "" echo -e " ${C_BOLD}$(_t "OPTIONS:")${C_RESET}" echo -e " ${C_CYAN}--branch, -b${C_RESET} $(_t "Branch (auto-detects)")" echo -e " ${C_CYAN}--url, -u${C_RESET} $(_t "Site URL")" echo -e " ${C_CYAN}--only=${C_RESET} $(_t "emulator|client|renderer|configs|dev")" echo -e " ${C_CYAN}--dry-run, -n${C_RESET} $(_t "Preview only (no changes)")" echo -e " ${C_CYAN}--help, -h${C_RESET} $(_t "Show this help")" echo -e " ${C_CYAN}--version, -V${C_RESET} $(_t "Show version")" echo "" } # ============================================================================= # MAIN # ============================================================================= main() { load_language 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" ;; stash|stashes) cmd="stash" ;; repair|flyaway) cmd="repair" ;; setup|init) cmd="setup" ;; cleanup-nitro|oldnitro) cmd="cleanup-nitro" ;; fixlinks|fix-links|links) cmd="fixlinks" ;; help|-h|--help) cmd="help" ;; version|-V|--version) echo "v${SCRIPT_VERSION} (build ${SCRIPT_BUILD})"; exit 0 ;; --branch|-b) shift; NITRO_BRANCH="$1" ;; --url|-u) shift; NITRO_SITE_URL="$1" ;; --only=*) SELECTIVE_UPDATE="${1#*=}" ;; --dry-run|-n) DRY_RUN=true ;; -*) echo "$(_t "Unknown:") $1"; exit 1 ;; *) [ -z "$cmd" ] && cmd="interactive" ;; esac shift done [ -z "$cmd" ] && cmd="interactive" # Auto-detect the branch from the emulator repo when none was configured, # so "Quick Update — current branch" actually updates the current branch. if [ -z "${NITRO_BRANCH:-}" ]; then NITRO_BRANCH="$(cd "$EMULATOR_REPO" 2>/dev/null && git branch --show-current 2>/dev/null || echo "main")" [ -z "$NITRO_BRANCH" ] && NITRO_BRANCH="main" info "Auto-detected branch: %s" "$NITRO_BRANCH" fi resolve_site_url compute_nitro_urls # Validate computed URLs for common misconfigurations. _nitro_url_warn() { local _key="$1" _val="$2" _hint="$3" case "$_val" in "") warn "$_key not set ($_hint)" ;; */nitro-client/*|*/nitro-client) fail "$_key points at the Nitro client path. It must point to the gamedata/c_images directory." ;; *://*/nitro-client/) fail "$_key points at the Nitro client URL. Set NITRO_IMAGE_LIBRARY_URL=/gamedata/c_images/ or correct NITRO_SITE_URL." ;; esac case "$_key" in NITRO_IMAGE_LIBRARY_URL) case "$_val" in */c_images/) ok "$_key correctly includes /c_images/" ;; *://*) warn "$_key does not end with /c_images/ — current: $_val" ;; esac ;; esac } _nitro_url_warn "NITRO_SITE_URL" "$NITRO_SITE_URL" "site URL" _nitro_url_warn "NITRO_IMAGE_LIBRARY_URL" "$NITRO_IMAGE_LIBRARY_URL" "image library URL" _nitro_url_warn "NITRO_GAMEDATA_URL" "$NITRO_GAMEDATA_URL" "gamedata URL" 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 ;; stash) cmd_stash ;; repair) cmd_flyaway_repair ;; setup) cmd_setup ;; cleanup-nitro) cmd_cleanup_old_nitro ;; fixlinks) cmd_fixlinks ;; help) show_help ;; esac cursor_show } main "$@"