feat: bulletproof updater + fixes for client links (icons/furniture/gamedata)
CI / check (push) Failing after 8s
CI / release (push) Skipped
CI / deploy (push) Skipped

This commit is contained in:
openhands committed 2026-08-30 18:19:46 +02:00
1 parent f2ac4745d4
commit 678d22ceab
3 files changed
+346 -74

No files matched your search

+49 -10
View File
@@ -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);
+34 -5
View File
@@ -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,19 @@ 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;
}
+263 -59
View File
@@ -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
@@ -2092,11 +2139,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 +2166,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 +3389,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 +3434,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 +3445,182 @@ 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
}
# 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
info "$(_t "Done — links fixed. Test the client now.")"
release_lock
return 0
}
# =============================================================================
# HELP
# =============================================================================
@@ -3427,6 +3651,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 +3690,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 +3712,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.<domain> 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 +3757,7 @@ main() {
repair) cmd_flyaway_repair ;;
setup) cmd_setup ;;
cleanup-nitro) cmd_cleanup_old_nitro ;;
fixlinks) cmd_fixlinks ;;
help) show_help ;;
esac