perf: optimize dashboard database queries and remove unused imports

This commit is contained in:
openhands committed 2026-08-25 21:52:18 +02:00
1 parent 3a76dbdb97
commit 416c31643b
4 files changed
+593 -114

No files matched your search

+96
View File
@@ -0,0 +1,96 @@
// Syncs the Nitro/Octane runtime config URLs from environment variables into the
// renderer-config.json / renderer-config.jsonc / ui-config.json files.
//
// Uses jsonc-parser so JSONC (with // and /* */ comments) is handled safely —
// unlike a naive JSON.parse/json.load this never corrupts URLs that contain
// "https://". After this runs the files are written back as strict JSON (which
// is still valid JSONC), so downstream validation passes.
//
// Env:
// NITRO_SRC_DIR e.g. /var/www/Octane/public/configuration
// NITRO_DIST_CONFIG_DIR e.g. /var/www/Octane/dist/configuration
// NITRO_GAMEDATA_CONF_DIR e.g. /var/www/Gamedata/config
// 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
const fs = require("node:fs");
const path = require("node:path");
const { parse, modify } = require("jsonc-parser");
const FILES = ["renderer-config.json", "renderer-config.jsonc", "ui-config.json"];
function env(name) {
return process.env[name] && process.env[name].length ? process.env[name] : null;
}
function applyOverrides(data) {
const image = env("NITRO_IMAGE_LIBRARY_URL");
const hof = env("NITRO_HOF_FURNITURE_URL");
const api = env("NITRO_API_URL");
const socket = env("NITRO_SOCKET_URL");
const gamedata = env("NITRO_GAMEDATA_URL");
const asset = env("NITRO_ASSET_URL");
const icon = env("NITRO_FURNI_ASSET_ICON_URL");
const set = (key, value) => {
if (value && !(key in data)) data[key] = value;
if (value) data[key] = value;
};
set("image.library.url", image);
set("hof.furni.url", hof);
set("api.url", api);
set("socket.url", socket);
set("gamedata.url", gamedata);
set("asset.url", asset);
set("furni.asset.icon.url", icon);
if (gamedata) {
data["radio.url"] = `${gamedata}/config/radio-stations.jsonc?t=%timestamp%`;
data["soundboard.url"] = `${gamedata}/config/soundboard-sounds.jsonc?t=%timestamp%`;
}
if ("show.google.ads" in data) data["show.google.ads"] = false;
return data;
}
const dirs = [
env("NITRO_SRC_DIR"),
env("NITRO_DIST_CONFIG_DIR"),
env("NITRO_GAMEDATA_CONF_DIR"),
].filter(Boolean);
let changed = 0;
for (const dir of dirs) {
if (!fs.existsSync(dir)) continue;
for (const file of FILES) {
const full = path.join(dir, file);
if (!fs.existsSync(full)) continue;
let content;
try {
content = fs.readFileSync(full, "utf-8");
} catch {
continue;
}
let data;
try {
data = parse(content, [], { allowTrailingComma: true, allowEmptyContent: true });
} catch (e) {
console.error(`[sync-nitro-urls] parse error in ${full}: ${e}`);
continue;
}
if (!data || typeof data !== "object") continue;
const before = JSON.stringify(data);
const updated = applyOverrides(data);
if (JSON.stringify(updated) === before) continue;
try {
fs.writeFileSync(full, `${JSON.stringify(updated, null, 4)}\n`);
changed++;
console.log(` [OK] Synced URLs: ${file} (${dir})`);
} catch (e) {
console.error(`[sync-nitro-urls] write error ${full}: ${e}`);
}
}
}
console.log(` [OK] URL sync complete (${changed} file(s) updated)`);
process.exit(0);
+47 -63
View File
@@ -1,4 +1,4 @@
import { and, asc, count, desc, eq, gt, inArray, or } from "drizzle-orm";
import { and, asc, count, desc, eq, gt, or } from "drizzle-orm";
import Image from "next/image";
import Link from "next/link";
import { redirect } from "next/navigation";
@@ -17,7 +17,6 @@ import {
UserReferrals,
UsersBadges,
UsersSettings,
WebsiteLoginLogs,
} from "@/lib/db";
import { avatarImageUrl } from "@/lib/format";
import { resolveHotelName } from "@/lib/hotel-name";
@@ -58,10 +57,7 @@ const ERROR_MESSAGES: Record<string, string> = {
};
function getErrorMessage(error: string): string {
if (error === "not_enough") return ERROR_MESSAGES.not_enough;
if (error === "no_referrals") return ERROR_MESSAGES.no_referrals;
if (error === "bad_config") return ERROR_MESSAGES.bad_config;
return ERROR_MESSAGES.error;
return ERROR_MESSAGES[error] ?? ERROR_MESSAGES.error;
}
export default async function MePage({
@@ -90,10 +86,11 @@ export default async function MePage({
alertRaw,
recentRooms,
badges,
lastWebLoginRows,
userSettingsRows,
friendCountRows,
unreadCountRows,
referralsRows,
friends,
] = await Promise.all([
db
.select({
@@ -139,13 +136,6 @@ export default async function MePage({
.where(and(eq(UsersBadges.userId, userId), gt(UsersBadges.slotId, 0)))
.orderBy(asc(UsersBadges.slotId))
.catch(() => []),
db
.select({ createdAt: WebsiteLoginLogs.createdAt })
.from(WebsiteLoginLogs)
.where(eq(WebsiteLoginLogs.userId, userId))
.orderBy(desc(WebsiteLoginLogs.id))
.limit(1)
.catch(() => []),
db
.select({
achievementScore: UsersSettings.achievementScore,
@@ -170,18 +160,51 @@ export default async function MePage({
.from(MessengerOffline)
.where(eq(MessengerOffline.userId, userId))
.catch(() => [{ value: 0 }]),
db
.select({ referralsTotal: UserReferrals.referralsTotal })
.from(UserReferrals)
.where(eq(UserReferrals.userId, userId))
.orderBy(desc(UserReferrals.id))
.limit(1)
.catch(() => []),
db
.select({
id: User.id,
username: User.username,
look: User.look,
motto: User.motto,
online: User.online,
})
.from(MessengerFriendships)
.innerJoin(
User,
or(
and(
eq(MessengerFriendships.userOneId, userId),
eq(User.id, MessengerFriendships.userTwoId),
),
and(
eq(MessengerFriendships.userTwoId, userId),
eq(User.id, MessengerFriendships.userOneId),
),
),
)
.where(
or(
eq(MessengerFriendships.userOneId, userId),
eq(MessengerFriendships.userTwoId, userId),
),
)
.catch(() => []),
]);
const user = userRows[0] ?? null;
const lastWebLogin = lastWebLoginRows[0] ?? null;
if (!user) throw new Error("user-not-found");
const userSettings = userSettingsRows[0] ?? null;
const friendCount = Number(friendCountRows[0]?.value ?? 0);
const unreadCount = Number(unreadCountRows[0]?.value ?? 0);
void lastWebLogin;
if (!user) throw new Error("user-not-found");
const needed = Number.parseInt(neededRaw ?? "5", 10) || 5;
const rewardAmount = Number.parseInt(rewardAmountRaw ?? "30", 10) || 0;
const rewardCurrency = (
@@ -192,53 +215,12 @@ export default async function MePage({
.trim()
.toLowerCase();
const [referrals] = await db
.select({ referralsTotal: UserReferrals.referralsTotal })
.from(UserReferrals)
.where(eq(UserReferrals.userId, userId))
.orderBy(desc(UserReferrals.id))
.limit(1)
.catch(() => []);
const referralTotal = referrals ? Number(referrals.referralsTotal) : 0;
const referralTotal = referralsRows[0]
? Number(referralsRows[0].referralsTotal)
: 0;
const canClaim = referralTotal >= needed;
const remaining = Math.max(0, needed - referralTotal);
const friendships = await db
.select({
userOneId: MessengerFriendships.userOneId,
userTwoId: MessengerFriendships.userTwoId,
})
.from(MessengerFriendships)
.where(
or(
eq(MessengerFriendships.userOneId, userId),
eq(MessengerFriendships.userTwoId, userId),
),
)
.catch(() => []);
const friendIds = Array.from(
new Set(
friendships
.map((f) => (f.userOneId === userId ? f.userTwoId : f.userOneId))
.filter((id) => id && id !== userId),
),
);
const friends = friendIds.length
? await db
.select({
id: User.id,
username: User.username,
look: User.look,
motto: User.motto,
online: User.online,
})
.from(User)
.where(inArray(User.id, friendIds))
.catch(() => [])
: [];
const onlineFriends = friends.filter((f) => f.online === "1");
const registered = new Date(user.accountCreated * 1000)
@@ -255,6 +237,7 @@ export default async function MePage({
const avatarFull = avatarImageUrl(user.look, {
direction: 2,
});
content = (
<div className="flex flex-col gap-8 pb-8">
{claimed ? (
@@ -298,6 +281,7 @@ export default async function MePage({
/>
<Link
href="/client"
prefetch={false}
className="btn-shine flex items-center justify-center gap-1.5 rounded-xl border py-2 px-4 font-extrabold text-xs transition-all duration-200 hover:scale-[1.02] active:scale-95 shadow-lg"
style={{
background: "var(--color-primary)",
+12 -3
View File
@@ -34,7 +34,13 @@ function ToolbarBtn({
} as const;
if (href) {
return (
<Link href={href} className={cls} style={style} title={title}>
<Link
href={href}
prefetch={false}
className={cls}
style={style}
title={title}
>
{children}
</Link>
);
@@ -377,9 +383,12 @@ export function ClientView({
<iframe
id="nitro"
src={clientSrc}
className="absolute inset-0 w-full h-full border-0 m-0 p-0 overflow-hidden"
className={`absolute inset-0 w-full h-full border-0 m-0 p-0 overflow-hidden ${
dragging ? "pointer-events-none" : "pointer-events-auto"
}`}
title={hotelName}
allow="autoplay; gamepad"
allow="autoplay; gamepad; fullscreen"
loading="eager"
/>
</div>
</>
+438 -48
View File
@@ -1143,10 +1143,17 @@ backup_configs() {
validate_json() {
local file="$1"
if ! python3 -m json.tool "$file" >/dev/null 2>&1; then
return 1
# Strict JSON first...
if python3 -m json.tool "$file" >/dev/null 2>&1; then
return 0
fi
return 0
# ...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() {
@@ -1434,6 +1441,7 @@ cmd_cleanup_old_nitro() {
# =============================================================================
auto_setup_missing_repos() {
local cloned=false
local needs_link=false
if [ ! -d "$NITRO_CLIENT" ] || [ ! -d "$NITRO_CLIENT/.git" ]; then
echo ""
@@ -1443,7 +1451,7 @@ auto_setup_missing_repos() {
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
cloned=true; needs_link=true
else
spinner_stop fail
die "Failed to clone Octane client from %s" "$GIT_REPO_CLIENT"
@@ -1458,32 +1466,75 @@ auto_setup_missing_repos() {
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
cloned=true; needs_link=true
else
spinner_stop fail
die "Failed to clone Octane renderer from %s" "$GIT_REPO_RENDERER"
fi
fi
if [ "$cloned" = true ]; then
echo ""
info "Installing dependencies for cloned repos..."
# 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" --frozen-lockfile --prefer-offline >> "$LOG_FILE" 2>&1 || \
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" --frozen-lockfile --prefer-offline >> "$LOG_FILE" 2>&1 || \
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
@@ -1505,6 +1556,60 @@ auto_setup_missing_repos() {
fi
}
# Copy example config files to their runtime names, handling renamed files
# in the Octane repo (renderer-config..jsonc.example.json etc.)
_setup_config_files() {
local src_dir="$NITRO_SRC_DIR"
[ ! -d "$src_dir" ] && mkdir -p "$src_dir" 2>/dev/null
local copied=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 [ ! -f "$src_dir/$tn" ]; then
cp "$ef" "$src_dir/$tn" 2>/dev/null && 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" ] && [ ! -f "$src_dir/renderer-config.jsonc" ]; then
cp "$rc_jsonc" "$src_dir/renderer-config.jsonc" 2>/dev/null && copied=$((copied + 1))
fi
if [ -f "$rc_json" ] && [ ! -f "$src_dir/renderer-config.json" ]; then
cp "$rc_json" "$src_dir/renderer-config.json" 2>/dev/null && copied=$((copied + 1))
fi
# 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
[ ! -f "$GAMEDATA_CONF_DIR/$fn" ] && cp "$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" "$copied"
}
# =============================================================================
# PRE-FLIGHT CHECKS
# =============================================================================
@@ -1629,6 +1734,10 @@ update_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"
else
info "Renderer: already up to date"
fi
@@ -1670,6 +1779,10 @@ update_client() {
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
spinner_stop ok
@@ -1702,6 +1815,7 @@ sync_configs() {
[ -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}"
@@ -1711,6 +1825,24 @@ sync_configs() {
[ -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
@@ -1736,42 +1868,16 @@ sync_configs() {
fi
info "Applying critical config URLs..."
NITRO_SRC_DIR="$NITRO_SRC_DIR" NITRO_DIST_CONFIG_DIR="$dcd" \
# 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" \
python3 -c '
import json, os
def to_jsonc(value):
if isinstance(value, dict): return {k: to_jsonc(v) for k, v in value.items()}
if isinstance(value, list): return [to_jsonc(v) for v in value]
if isinstance(value, str): return value.replace(".json5", ".jsonc")
return value
paths = [
os.path.join(os.environ.get("NITRO_SRC_DIR", ""), "renderer-config.json"),
os.path.join(os.environ.get("NITRO_DIST_CONFIG_DIR", ""), "renderer-config.json"),
os.path.join(os.environ.get("NITRO_SRC_DIR", ""), "ui-config.json"),
os.path.join(os.environ.get("NITRO_DIST_CONFIG_DIR", ""), "ui-config.json"),
]
for path in paths:
if not os.path.exists(path): continue
with open(path, "r") as f: data = json.load(f)
data = to_jsonc(data)
for key in ["image.library.url", "hof.furni.url", "gamedata.url", "asset.url"]:
env_val = os.environ.get(f"NITRO_{key.upper().replace(chr(46), chr(95))}")
if env_val: data[key] = env_val
for key in ["api.url", "socket.url", "furni.asset.icon.url"]:
if key in data:
env_val = os.environ.get(f"NITRO_{key.upper().replace(chr(46), chr(95))}")
if env_val: data[key] = env_val
if "gamedata.url" in data:
data["radio.url"] = data["gamedata.url"] + "/config/radio-stations.jsonc?t=%timestamp%"
data["soundboard.url"] = data["gamedata.url"] + "/config/soundboard-sounds.jsonc?t=%timestamp%"
if "show.google.ads" in data: data["show.google.ads"] = False
with open(path, "w") as f: json.dump(data, f, indent=(4 if "dist" not in path else None), separators=(",", ":") if "dist" in path else (",", ": "))
print(f" [OK] Fixed: {os.path.basename(path)}")
' && ok "All config URLs synced"
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
@@ -1886,6 +1992,7 @@ ensure_gamedata_redirect() {
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/; }
@@ -1902,7 +2009,7 @@ ensure_gamedata_redirect() {
warn "Could not inject gamedata redirect into %s" "$conf"
continue
fi
bak="$conf.bak-$(date +%s)"
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
@@ -1921,9 +2028,255 @@ ensure_gamedata_redirect() {
return 0
}
# Resolve the emulator's public game (WebSocket) port — the port the Nitro
# client actually connects to (socket.url -> wss://ws.<domain> -> 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.<domain> (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" <<EOF
server {
listen 9443 ssl default_server;
listen [::]:9443 ssl default_server;
server_name _;
ssl_certificate $cert;
ssl_certificate_key $key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers off;
location = /health { return 200 "OK"; }
location / { return 444; }
}
EOF
cat "$conf" >> "$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 8 8 "Restart Services"
ensure_gamedata_redirect
ensure_default_server
ensure_ws_proxy
ensure_api_proxy
if systemctl cat "$EMULATOR_SERVICE" &>/dev/null; then
sudo systemctl restart "$EMULATOR_SERVICE" 2>/dev/null && ok "$EMULATOR_SERVICE restarted" || warn "Restart failed"
if ! wait_for_emulator_health; then
@@ -1959,7 +2312,15 @@ do_restart() {
else
warn "No service manager found"
fi
command -v nginx &>/dev/null && sudo nginx -t 2>/dev/null && sudo systemctl reload nginx 2>/dev/null && ok "Nginx reloaded"
# 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"
}
@@ -1968,10 +2329,7 @@ do_restart() {
emulator_port_open() {
local port="${NITRO_EMULATOR_PORT:-}"
if [ -z "$port" ]; then
port=$(grep -m1 '^game\.port=' "$EMULATOR_MODULE/target/config.ini" 2>/dev/null | cut -d= -f2)
fi
if [ -z "$port" ]; then
port=30000
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)
@@ -2302,9 +2660,29 @@ cmd_flyaway_repair() {
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
@@ -2826,6 +3204,18 @@ main() {
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://}" ;;