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
+159
View File
@@ -0,0 +1,159 @@
import { readdir, mkdir, stat } from 'node:fs/promises';
import { existsSync } from 'node:fs';
import { config, destOf } from '../config.js';
import { db } from '../db/index.js';
/**
* All server folders under SERVERS_ROOT. Mirrors `all_servers` in
* push_scripts.sh:430.
*/
export async function allServers(): Promise<string[]> {
let entries;
try {
entries = await readdir(config.serversRoot, { withFileTypes: true });
} catch {
return [];
}
return entries
.filter((e) => e.isDirectory())
.map((e) => e.name)
.sort();
}
/**
* Match a server name against a list of glob-like patterns (supports `*` and
* `?` and comma-separated lists). Ports `match_servers` in
* push_scripts.sh:438.
*/
export function matchServers(all: string[], patterns: string[]): string[] {
if (patterns.length === 0 || (patterns.length === 1 && patterns[0] === '*')) {
return [...all];
}
const regexes = patterns
.flatMap((p) => p.split(',').map((s) => s.trim()).filter(Boolean))
.map((p) => new RegExp('^' + p.replace(/[.+^$()|{}\\]/g, '\\$&').replace(/\*/g, '.*').replace(/\?/g, '.') + '$'));
return all.filter((name) => regexes.some((r) => r.test(name)));
}
/**
* Ensure a server has an sp/ destination. Mirrors `ensure_dest` in
* push_scripts.sh:466. Returns `{ ok: true }` when the dir exists (or was
* just created with `create: true`), or `{ ok: false, reason }` when it can't
* be used.
*/
export async function ensureDest(
server: string,
create: boolean
): Promise<{ ok: true; path: string } | { ok: false; reason: string }> {
const dest = destOf(server);
if (existsSync(dest)) {
try {
const s = await stat(dest);
if (!s.isDirectory()) return { ok: false, reason: 'dest is not a directory' };
} catch (err) {
return { ok: false, reason: `stat failed: ${(err as Error).message}` };
}
return { ok: true, path: dest };
}
if (!create) return { ok: false, reason: 'no sp folder — use create=true' };
await mkdir(dest, { recursive: true });
return { ok: true, path: dest };
}
// -- server registry (was .servers map in script_metadata.json) --
export interface ServerRow {
name: string;
komodo_type: 'deployment' | 'stack' | 'container';
komodo_name: string;
komodo_server: string | null;
iw4m_match: 'port' | 'id' | 'name' | null;
iw4m_value: string | null;
last_players: number | null;
last_checked_at: string | null;
created_at: string;
}
/**
* Seed the servers table with rows for any folder under SERVERS_ROOT that
* isn't yet tracked. Uses folder-name-with-dashes as the default komodo_name,
* mirroring `server_reg_seed` in push_scripts.sh:564.
*/
export async function seedServerRegistry(): Promise<number> {
const rows = await allServers();
const insert = db().prepare(
'INSERT OR IGNORE INTO servers (name, komodo_type, komodo_name) VALUES (?, ?, ?)'
);
let n = 0;
const tx = db().transaction((names: string[]) => {
for (const name of names) {
const res = insert.run(name, 'deployment', name.replace(/_/g, '-'));
if (res.changes > 0) n++;
}
});
tx(rows);
return n;
}
export function getServer(name: string): ServerRow | undefined {
return db().prepare('SELECT * FROM servers WHERE name = ?').get(name) as ServerRow | undefined;
}
export function listServers(): ServerRow[] {
return db().prepare('SELECT * FROM servers ORDER BY name').all() as ServerRow[];
}
export function setServerField(
name: string,
field: keyof ServerRow,
value: string | number | null
): void {
// whitelist columns — never interpolate arbitrary strings into SQL
const allowed: Array<keyof ServerRow> = [
'komodo_type',
'komodo_name',
'komodo_server',
'iw4m_match',
'iw4m_value',
'last_players',
'last_checked_at'
];
if (!allowed.includes(field)) throw new Error(`refusing to set ${field}`);
db().prepare(`UPDATE servers SET ${field} = ? WHERE name = ?`).run(value, name);
}
/**
* Which relative-path targets are currently held by an open beta on this
* server? Returns `Map<relPath, betaSlug>`. Mirrors `held_by_beta` in
* push_scripts.sh:378.
*/
export function heldByBeta(server: string): Map<string, string> {
const rows = db()
.prepare(
`SELECT bs.target AS target, bs.beta_slug AS slug
FROM beta_deploys bd
JOIN beta_scripts bs ON bs.id = bd.beta_script_id
WHERE bd.server = ?`
)
.all(server) as Array<{ target: string; slug: string }>;
const map = new Map<string, string>();
for (const r of rows) map.set(r.target, r.slug);
return map;
}
/**
* Which beta (if any) owns this target on this server?
* Mirrors `owner_of` in push_scripts.sh:366.
*/
export function ownerOf(server: string, target: string): string | null {
const row = db()
.prepare(
`SELECT bs.beta_slug AS slug
FROM beta_deploys bd
JOIN beta_scripts bs ON bs.id = bd.beta_script_id
WHERE bd.server = ? AND bs.target = ?
LIMIT 1`
)
.get(server, target) as { slug: string } | undefined;
return row?.slug ?? null;
}