feat: RCON API transport, full furnidata translation with LibreTranslate fallback, remove organize feature, bump per-page limit to 500
- Add EMULATOR_MODE=api option with HTTP transport mirroring RCON payload - buildLocalizedFurniDataFiles now falls back to LibreTranslate for customs (incl. pl/ru/ar/ja) - Remove Organiseer import button from studio and Reorganize Catalog from import page - Delete organize API route and PUT handler; remove organizeCatalogAll from catalog-repair.ts - Increase imported-furni list perPage from 50 to 500
This commit is contained in:
1 parent
5668493485
commit
85b5792d40
8 files changed
+146
-768
No files matched your search
@@ -153,17 +153,101 @@ export class RconClient {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP transport for emulators that expose an API instead of RCON's raw-JSON
|
||||
* TCP protocol. It POSTs the exact same `{"key":...,"data":...}` payload the
|
||||
* TCP transport writes, so the RconClient command shapes stay identical.
|
||||
*
|
||||
* Mirrors the TCP transport's best-effort semantics: a 2xx response is treated
|
||||
* as delivered (`true`); any non-2xx, network error, or timeout is logged and
|
||||
* treated as not delivered (`false`).
|
||||
*/
|
||||
export function httpTransport(opts: {
|
||||
url: string;
|
||||
timeoutMs: number;
|
||||
apiKey?: string;
|
||||
apiKeyHeader?: string;
|
||||
maxRetries: number;
|
||||
}): RconTransport {
|
||||
const sendOnce = (payload: RconPayload): Promise<boolean> =>
|
||||
new Promise((resolve) => {
|
||||
const body = JSON.stringify(payload);
|
||||
const headers: Record<string, string> = {
|
||||
"content-type": "application/json",
|
||||
};
|
||||
if (opts.apiKey) {
|
||||
const header = opts.apiKeyHeader ?? "Authorization";
|
||||
// Accept either a raw token or a full scheme (e.g. "Bearer xyz").
|
||||
headers[header] = /\s/.test(opts.apiKey)
|
||||
? opts.apiKey
|
||||
: `Bearer ${opts.apiKey}`;
|
||||
}
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(() => controller.abort(), opts.timeoutMs);
|
||||
fetch(opts.url, {
|
||||
method: "POST",
|
||||
headers,
|
||||
body,
|
||||
signal: controller.signal,
|
||||
})
|
||||
.then((res) => {
|
||||
clearTimeout(timer);
|
||||
resolve(res.ok);
|
||||
})
|
||||
.catch(() => {
|
||||
clearTimeout(timer);
|
||||
resolve(false);
|
||||
});
|
||||
});
|
||||
|
||||
return async (payload) => {
|
||||
for (let attempt = 0; attempt < opts.maxRetries; attempt++) {
|
||||
if (await sendOnce(payload)) return true;
|
||||
if (attempt < opts.maxRetries - 1) {
|
||||
await new Promise((r) => setTimeout(r, 200 * 2 ** attempt));
|
||||
}
|
||||
}
|
||||
logger.error("Emulator API delivery failed after retries", {
|
||||
module: "emulator-api",
|
||||
key: payload.key,
|
||||
url: opts.url,
|
||||
});
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the configured emulator transport based on `EMULATOR_MODE`:
|
||||
* - "rcon" (default): raw-JSON TCP transport.
|
||||
* - "api": HTTP transport posting the RCON payload to `EMULATOR_API_URL`.
|
||||
*
|
||||
* Throws at construction time when api mode is selected without a URL so the
|
||||
* misconfiguration surfaces immediately instead of on first command.
|
||||
*/
|
||||
export function createEmulatorTransport(): RconTransport {
|
||||
if (env.EMULATOR_MODE === "api") {
|
||||
if (!env.EMULATOR_API_URL) {
|
||||
throw new Error("EMULATOR_MODE=api requires EMULATOR_API_URL to be set");
|
||||
}
|
||||
return httpTransport({
|
||||
url: env.EMULATOR_API_URL,
|
||||
timeoutMs: env.RCON_TIMEOUT_MS,
|
||||
apiKey: env.EMULATOR_API_KEY,
|
||||
apiKeyHeader: env.EMULATOR_API_KEY_HEADER,
|
||||
maxRetries: env.RCON_MAX_RETRIES,
|
||||
});
|
||||
}
|
||||
return tcpTransport({
|
||||
host: env.RCON_HOST,
|
||||
port: env.RCON_PORT,
|
||||
timeoutMs: env.RCON_TIMEOUT_MS,
|
||||
maxRetries: env.RCON_MAX_RETRIES,
|
||||
});
|
||||
}
|
||||
|
||||
const globalForRcon = globalThis as unknown as { rcon?: RconClient };
|
||||
|
||||
export const rcon =
|
||||
globalForRcon.rcon ??
|
||||
new RconClient(
|
||||
tcpTransport({
|
||||
host: env.RCON_HOST,
|
||||
port: env.RCON_PORT,
|
||||
timeoutMs: env.RCON_TIMEOUT_MS,
|
||||
maxRetries: env.RCON_MAX_RETRIES,
|
||||
}),
|
||||
);
|
||||
globalForRcon.rcon ?? new RconClient(createEmulatorTransport());
|
||||
|
||||
if (env.NODE_ENV !== "production") globalForRcon.rcon = rcon;
|
||||
Reference in new issue
Block a user