Initial commit — SvelteKit fleet manager for T5 Plutonium servers

Ports push_scripts.sh (1497 lines of bash) to a SvelteKit + SQLite web app
that manages a fleet of T5 Plutonium servers running on Komodo. Same push
and beta workflow as the shell script, with a live SSE pipeline view, a
!rt-token-gated auth modal that hard-pauses deployments on stale tokens,
per-file audit diffs, and a Komodo-container deployment target.

- Push/beta engines port the exact filter, held-by-beta, conflict, and
  stale-backup rules from the shell script
- SQLite (better-sqlite3) replaces script_metadata.json; a one-shot
  importer moves legacy state over on first boot
- 16 vitest tests cover push add/update/skip, beta open→rollback,
  open→promote, conflict guards, sweep frozen/manual branches, and IW4M
  login/status parsing
- docker/ + komodo/stack.toml deploy as a managed container on the same
  host as the game servers, /root/zombies bind-mounted at the same path
  the shell script used
This commit is contained in:
2026-09-12 00:23:51 -04:00
commit d99711563e
67 changed files with 9415 additions and 0 deletions
+81
View File
@@ -0,0 +1,81 @@
import { config, komodoConfigured } from '../config.js';
import { getServer } from '../fleet/servers.js';
// Ports `komodo_restart` in push_scripts.sh:622. Three shapes depending on
// the tracked komodo_type: deployment | container | stack.
export type KomodoRestartResult =
| { ok: true }
| { ok: false; kind: 'unconfigured' | 'no-name' | 'http-error' | 'unreachable'; message: string };
async function komodoCall(path: string, body: unknown): Promise<Response> {
const url = `${config.komodo.address.replace(/\/+$/, '')}/${path}`;
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), config.httpTimeoutMs);
try {
return await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Api-Key': config.komodo.apiKey,
'X-Api-Secret': config.komodo.apiSecret
},
body: JSON.stringify(body),
signal: controller.signal
});
} finally {
clearTimeout(timer);
}
}
export async function komodoRestart(serverName: string): Promise<KomodoRestartResult> {
if (!komodoConfigured()) {
return { ok: false, kind: 'unconfigured', message: 'komodo not configured' };
}
const row = getServer(serverName);
if (!row?.komodo_name) return { ok: false, kind: 'no-name', message: 'no komodo_name set for server' };
const type = row.komodo_type ?? 'deployment';
let path: string;
let body: Record<string, string>;
switch (type) {
case 'stack':
path = 'execute/RestartStack';
body = { stack: row.komodo_name };
break;
case 'container':
if (!row.komodo_server) return { ok: false, kind: 'no-name', message: 'container type needs komodo_server' };
path = 'execute/RestartContainer';
body = { server: row.komodo_server, container: row.komodo_name };
break;
case 'deployment':
default:
path = 'execute/RestartDeployment';
body = { deployment: row.komodo_name };
break;
}
let res: Response;
try {
res = await komodoCall(path, body);
} catch (err) {
return { ok: false, kind: 'unreachable', message: (err as Error).message };
}
if (!res.ok) {
return { ok: false, kind: 'http-error', message: `HTTP ${res.status}` };
}
// The script also checks the response body: if it's an object with
// .success === false, treat as failure. If .success is absent, trust the
// HTTP status (line 646).
try {
const json = await res.json();
if (json && typeof json === 'object' && 'success' in json && json.success === false) {
return { ok: false, kind: 'http-error', message: 'komodo returned success=false' };
}
} catch {
// Non-JSON body is fine — HTTP 2xx already indicates success.
}
return { ok: true };
}