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
+95
View File
@@ -0,0 +1,95 @@
<script lang="ts">
interface Props {
before: string | null;
after: string | null;
verb: string;
relPath: string;
}
let { before, after, verb, relPath }: Props = $props();
// A tiny line-by-line diff. Not LCS — just presence-based. For GSC scripts
// this reads fine; if we want proper Myers diff later, swap in `diff` npm
// package and render the same rows.
interface Row {
kind: 'ctx' | 'add' | 'del';
text: string;
}
let rows = $derived(computeRows(before, after));
function computeRows(a: string | null, b: string | null): Row[] {
const alines = (a ?? '').split(/\r?\n/);
const blines = (b ?? '').split(/\r?\n/);
const asetBase: Row[] = alines.map((t) => ({ kind: 'del' as const, text: t }));
const bsetBase: Row[] = blines.map((t) => ({ kind: 'add' as const, text: t }));
if (a === null) return bsetBase;
if (b === null) return asetBase;
// interleave shared-prefix / shared-suffix trimming for readability
let i = 0;
while (i < alines.length && i < blines.length && alines[i] === blines[i]) i++;
let j = 0;
while (
j < alines.length - i &&
j < blines.length - i &&
alines[alines.length - 1 - j] === blines[blines.length - 1 - j]
)
j++;
const prefix = alines.slice(0, i).map((t) => ({ kind: 'ctx' as const, text: t }));
const suffix = alines.slice(alines.length - j).map((t) => ({ kind: 'ctx' as const, text: t }));
const midA = alines.slice(i, alines.length - j).map((t) => ({ kind: 'del' as const, text: t }));
const midB = blines.slice(i, blines.length - j).map((t) => ({ kind: 'add' as const, text: t }));
return [...prefix, ...midA, ...midB, ...suffix];
}
</script>
<div class="diff">
<header>
<span class="verb verb-{verb}">{verb}</span>
<span class="path">{relPath}</span>
</header>
<pre>
{#each rows as r, idx (idx)}<span
class="ln ln-{r.kind}">{r.kind === 'add' ? '+' : r.kind === 'del' ? '-' : ' '} {r.text}
</span>{/each}</pre>
</div>
<style>
.diff {
background: #0f0f0f;
border: 1px solid #333;
border-radius: 4px;
margin: 8px 0;
overflow: hidden;
}
header {
display: flex;
gap: 8px;
padding: 6px 10px;
background: #1a1a1a;
border-bottom: 1px solid #333;
font-size: 12px;
}
.verb {
padding: 1px 6px;
border-radius: 3px;
font-family: ui-monospace, monospace;
}
.verb-added { background: #2d4a2d; color: #a8e6a8; }
.verb-updated { background: #2d3a4a; color: #a8c8e6; }
.verb-removed { background: #4a2d2d; color: #ff8080; }
.verb-held { background: #3a2f00; color: #ffcc55; }
.path { color: #ccc; font-family: ui-monospace, monospace; }
pre {
margin: 0;
padding: 8px 10px;
font-family: ui-monospace, monospace;
font-size: 12px;
line-height: 1.5;
color: #ccc;
max-height: 500px;
overflow: auto;
}
.ln { display: block; }
.ln-add { background: rgba(45, 74, 45, 0.4); color: #a8e6a8; }
.ln-del { background: rgba(74, 45, 45, 0.4); color: #ff8080; }
</style>