diff --git a/scripts/merge-config.cjs b/scripts/merge-config.cjs index 48c15bd519..9459aa349d 100644 --- a/scripts/merge-config.cjs +++ b/scripts/merge-config.cjs @@ -8,19 +8,30 @@ if (!exampleFile || !targetFile) { process.exit(1); } +function isPlainObject(value) { + return value !== null && typeof value === "object" && !Array.isArray(value); +} + +// Merge the upstream example into the live config with strict "add missing, +// never remove or overwrite" semantics so hotel configs survive every update: +// - every key already present in the live config is PRESERVED (scalars, +// arrays and nested objects always win over the upstream example), +// - nested objects are merged recursively so upstream-only sub-keys are +// added while the hotel's own sub-keys stay untouched, +// - keys that do not exist in the live config yet are ADDED from the example, +// - a source object NEVER replaces an existing scalar/array (would corrupt). function deepMerge(target, source) { const result = { ...target }; for (const key of Object.keys(source)) { - if ( - source[key] !== null && - typeof source[key] === "object" && - !Array.isArray(source[key]) - ) { - result[key] = deepMerge(target[key] || {}, source[key]); - } else { - if (!(key in result)) { - result[key] = source[key]; + if (isPlainObject(source[key])) { + if (isPlainObject(result[key])) { + result[key] = deepMerge(result[key], source[key]); + } else if (!(key in result)) { + result[key] = deepMerge({}, source[key]); } + // Else: live value is a scalar/array/null -> keep it untouched. + } else if (!(key in result)) { + result[key] = source[key]; } } return result; @@ -69,4 +80,32 @@ try { } const merged = deepMerge(current, example); -fs.writeFileSync(targetFile, `${JSON.stringify(merged, null, 4)}\n`); + +// Sanity: the merged result must stay an object and the written file must be +// re-parseable. If anything goes wrong we keep the live file untouched rather +// than shipping a corrupted config to the client. +if (merged === undefined || merged === null || typeof merged !== "object") { + process.stderr.write( + `[merge-config] refused to write non-object result for ${targetFile}\n`, + ); + process.exit(1); +} + +// Verify the result parses round-trip before overwriting the live config. +const serialized = `${JSON.stringify(merged, null, 4)}\n`; +let check; +try { + check = parse(serialized, [], { + allowTrailingComma: true, + allowEmptyContent: true, + }); +} catch { + check = undefined; +} +if (!check || typeof check !== "object" || Array.isArray(check)) { + process.stderr.write( + `[merge-config] refused to write unparseable result for ${targetFile}\n`, + ); + process.exit(1); +} +fs.writeFileSync(targetFile, serialized); diff --git a/scripts/sync-nitro-urls.cjs b/scripts/sync-nitro-urls.cjs index 1b98cb4b69..37b3956cc9 100644 --- a/scripts/sync-nitro-urls.cjs +++ b/scripts/sync-nitro-urls.cjs @@ -13,9 +13,10 @@ // NITRO_IMAGE_LIBRARY_URL, NITRO_HOF_FURNITURE_URL, NITRO_API_URL, // NITRO_SOCKET_URL, NITRO_GAMEDATA_URL, NITRO_ASSET_URL, // NITRO_FURNI_ASSET_ICON_URL +// NITRO_URL_FORCE=1 (optional) overwrite existing values too const fs = require("node:fs"); const path = require("node:path"); -const { parse, modify } = require("jsonc-parser"); +const { parse } = require("jsonc-parser"); const FILES = [ "renderer-config.json", @@ -23,6 +24,22 @@ const FILES = [ "ui-config.json", ]; +const FORCE = process.env.NITRO_URL_FORCE === "1"; + +// Values that must never survive into a production config: placeholder or +// loopback hosts that originate from the upstream example files. When a key +// still holds one of these, it has been newly added by the merge step and has +// to be filled from the environment. A real, already-configured URL (the +// hotel's own domain) is NEVER rewritten, so hotel links stay exactly as the +// hotel owner configured them. +const PLACEHOLDER_RE = + /(localhost|127\.0\.0\.1|0\.0\.0\.0|::1|\.example\.com|nitro\.example\.|hotel\.example\.|example\.org|example\.net|:5173|:2096|\$\{)/i; + +function isPlaceholder(value) { + if (!value) return true; + return PLACEHOLDER_RE.test(value); +} + function env(name) { return process.env[name]?.length ? process.env[name] : null; } @@ -36,9 +53,14 @@ function applyOverrides(data) { const asset = env("NITRO_ASSET_URL"); const icon = env("NITRO_FURNI_ASSET_ICON_URL"); + // Add-only, placeholder-aware URL fill-in. Existing real values are kept, + // crypto.* keys are never touched, and only missing/placeholder keys are + // filled from the environment. NITRO_URL_FORCE=1 opts back into overwriting. const set = (key, value) => { - if (value && !(key in data)) data[key] = value; - if (value) data[key] = value; + if (!value || key.startsWith("crypto.")) return; + if (FORCE || !(key in data) || isPlaceholder(data[key])) { + data[key] = value; + } }; set("image.library.url", image); @@ -49,12 +71,22 @@ function applyOverrides(data) { set("asset.url", asset); set("furni.asset.icon.url", icon); - if (gamedata) { + if ( + gamedata && + (!("radio.url" in data) || isPlaceholder(data["radio.url"])) + ) { data["radio.url"] = `${gamedata}/config/radio-stations.jsonc?t=%timestamp%`; + } + if ( + gamedata && + (!("soundboard.url" in data) || isPlaceholder(data["soundboard.url"])) + ) { data["soundboard.url"] = `${gamedata}/config/soundboard-sounds.jsonc?t=%timestamp%`; } - if ("show.google.ads" in data) data["show.google.ads"] = false; + // Never force ads on/off over a hotel's own choice; only default to "off" + // when the key does not exist yet (prevents external ad scripts from 404ing). + if (!("show.google.ads" in data)) data["show.google.ads"] = false; return data; } diff --git a/update-Nitrov3.sh b/update-Nitrov3.sh index 0a63500342..c388a28828 100755 --- a/update-Nitrov3.sh +++ b/update-Nitrov3.sh @@ -9,8 +9,8 @@ set -Eeo pipefail # ============================================================================= # CONSTANTS # ============================================================================= -readonly SCRIPT_VERSION="8.0.2" -readonly SCRIPT_BUILD="20260803" +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) @@ -216,7 +216,8 @@ load_language() { ["Cleanup old Nitro dirs"]="Oude Nitro mappen opruimen" ["Rebuild Frontend"]="Frontend Herbouwen" ["Rebuild Emulator"]="Emulator Herbouwen" -["Select [0-23]"]="Selecteer [0-23]" +["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 ────────────────────────────────────────────── @@ -531,7 +532,8 @@ load_language() { ["Cleanup old Nitro dirs"]="Nettoyer les anciens répertoires Nitro" ["Rebuild Frontend"]="Reconstruire le Frontend" ["Rebuild Emulator"]="Reconstruire l'Emulateur" -["Select [0-23]"]="Sélectionner [0-23]" +["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 ?" @@ -618,7 +620,8 @@ load_language() { ["Cleanup old Nitro dirs"]="Limpiar directorios Nitro antiguos" ["Rebuild Frontend"]="Reconstruir Frontend" ["Rebuild Emulator"]="Reconstruir Emulador" -["Select [0-23]"]="Seleccionar [0-23]" +["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?" @@ -668,7 +671,8 @@ load_language() { ["Cleanup old Nitro dirs"]="Alte Nitro-Verzeichnisse aufräumen" ["Rebuild Frontend"]="Frontend neu bauen" ["Rebuild Emulator"]="Emulator neu bauen" -["Select [0-23]"]="Auswählen [0-23]" +["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?" @@ -718,7 +722,8 @@ load_language() { ["Cleanup old Nitro dirs"]="Pulisci vecchie cartelle Nitro" ["Rebuild Frontend"]="Ricostruisci Frontend" ["Rebuild Emulator"]="Ricostruisci Emulatore" -["Select [0-23]"]="Seleziona [0-23]" +["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?" @@ -1826,6 +1831,17 @@ update_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++ @@ -1840,6 +1856,20 @@ update_emulator() { 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 @@ -1872,31 +1902,39 @@ update_renderer() { # Let the caller fail the parallel group; the state file records it. warn "Renderer update failed (git error)" return 1 - elif [ $grc -eq 0 ]; then + fi + if [ $grc -eq 0 ]; then HAD_UPDATES=true; UPDATED_REPOS+=("Octane-Renderer") - cd "$NITRO_RENDERER" - spinner_start "Installing deps..." - if ! yarn_install_repo "$NITRO_RENDERER" --frozen-lockfile --prefer-offline; then - warn "Renderer: yarn install failed — cleaning node_modules and retrying" - yarn_reset_repo "$NITRO_RENDERER" - yarn_install_repo "$NITRO_RENDERER" || { spinner_stop fail; git_revert_repo "$NITRO_RENDERER"; die "yarn install failed (see: %s)" "$LOG_FILE"; } - fi - cd "$NITRO_RENDERER" - if [ ! -d node_modules/pixi.js ]; then - warn "Renderer: node_modules incomplete — 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_RENDERER" - yarn_install_repo "$NITRO_RENDERER" || { spinner_stop fail; git_revert_repo "$NITRO_RENDERER"; die "yarn install failed (see: %s)" "$LOG_FILE"; } - fi - spinner_stop ok; ok "Renderer dependencies installed" - # Ensure renderer is globally linked - cd "$NITRO_RENDERER" - yarn link >> "$LOG_FILE" 2>&1 || true - cd "$SCRIPT_DIR" + 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 } @@ -1941,6 +1979,15 @@ update_client() { 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 @@ -1953,6 +2000,7 @@ update_client() { 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 } @@ -2092,11 +2140,9 @@ sync_configs() { # Validate the generated configs validate_configs "$NITRO_SRC_DIR" - # Remove legacy .json5 leftovers — the client only supports JSON/JSONC - for d in "$NITRO_SRC_DIR" "$dcd" "$GAMEDATA_CONF_DIR"; do - [ -d "$d" ] && find "$d" -maxdepth 1 -name '*.json5' -type f -delete 2>/dev/null || true - done - ok "Legacy .json5 files removed" + # 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 @@ -2121,6 +2167,7 @@ sync_configs() { 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 @@ -3343,10 +3390,11 @@ cmd_interactive() { 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-23]")${C_RESET}: " + echo -en " ${C_CYAN}${C_BOLD}$(_t "Select [0-24]")${C_RESET}: " local choice; read -rt 30 choice case "$choice" in @@ -3387,6 +3435,7 @@ cmd_interactive() { 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 @@ -3397,6 +3446,201 @@ cmd_interactive() { 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 # ============================================================================= @@ -3427,6 +3671,7 @@ show_help() { 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)")" @@ -3465,6 +3710,7 @@ main() { 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" ;; @@ -3486,31 +3732,8 @@ main() { info "Auto-detected branch: %s" "$NITRO_BRANCH" fi - if [ -z "${NITRO_SITE_URL:-}" ] && [ -n "${APP_URL:-}" ]; then NITRO_SITE_URL="$APP_URL"; fi - # Auto-detect the site domain from the nginx vhost when no explicit URL is - # configured (APP_URL / NITRO_SITE_URL / --url). This lets the update script - # self-configure the client URLs and the ws. proxy on a fresh hotel - # VPS without manual .env edits. - 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 - case "${NITRO_SITE_URL:-}" in - https://*) NITRO_WS_PROTO="wss://" ; NITRO_DOMAIN="${NITRO_SITE_URL#https://}" ;; - http://*) NITRO_WS_PROTO="ws://" ; NITRO_DOMAIN="${NITRO_SITE_URL#http://}" ;; - *) NITRO_WS_PROTO="wss://" ; NITRO_DOMAIN="${NITRO_SITE_URL:-}" ;; - esac - NITRO_IMAGE_LIBRARY_URL="${NITRO_IMAGE_LIBRARY_URL:-${NITRO_SITE_URL:-}/gamedata/c_images/}" - NITRO_HOF_FURNITURE_URL="${NITRO_HOF_FURNITURE_URL:-${NITRO_SITE_URL:-}/gamedata/icons}" - NITRO_API_URL="${NITRO_API_URL:-${NITRO_SITE_URL:-}}" - NITRO_SOCKET_URL="${NITRO_SOCKET_URL:-${NITRO_WS_PROTO}ws.${NITRO_DOMAIN}}" - NITRO_GAMEDATA_URL="${NITRO_GAMEDATA_URL:-${NITRO_SITE_URL:-}/gamedata}" - NITRO_ASSET_URL="${NITRO_ASSET_URL:-${NITRO_SITE_URL:-}/gamedata/bundled}" - NITRO_FURNI_ASSET_ICON_URL="${NITRO_FURNI_ASSET_ICON_URL:-${NITRO_SITE_URL:-}/gamedata/icons/%libname%%param%_icon.png}" + resolve_site_url + compute_nitro_urls # Validate computed URLs for common misconfigurations. _nitro_url_warn() { @@ -3554,6 +3777,7 @@ main() { repair) cmd_flyaway_repair ;; setup) cmd_setup ;; cleanup-nitro) cmd_cleanup_old_nitro ;; + fixlinks) cmd_fixlinks ;; help) show_help ;; esac