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
+123
View File
@@ -0,0 +1,123 @@
<script lang="ts">
import { onMount } from 'svelte';
import ServerCard from '$ui/components/ServerCard.svelte';
import PipelineTimeline from '$ui/components/PipelineTimeline.svelte';
import { statusByServer } from '$ui/stores/servers.js';
interface HeldRow { target: string; slug: string; }
interface ServerRow {
name: string;
komodo_type: string;
komodo_name: string;
iw4m_match: string | null;
iw4m_value: string | null;
last_players: number | null;
held: HeldRow[];
}
let servers = $state<ServerRow[]>([]);
let pushing = $state(false);
let flash = $state<string | null>(null);
onMount(async () => {
const res = await fetch('/api/servers');
const body = (await res.json()) as { servers: ServerRow[] };
servers = body.servers;
});
async function push(dryRun: boolean) {
pushing = true;
flash = null;
const res = await fetch('/api/push', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ patterns: ['*'], dryRun, create: false, delete: false })
});
pushing = false;
const body = (await res.json()) as { totalChangedServers?: number; totalFiles?: number; deploymentId?: string | null };
if (!res.ok) {
flash = `push failed: HTTP ${res.status}`;
return;
}
flash = dryRun
? `dry run: ${body.totalChangedServers ?? 0} servers would change, ${body.totalFiles ?? 0} files`
: `pushed to ${body.totalChangedServers ?? 0} servers · ${body.totalFiles ?? 0} files`;
}
function playersFor(name: string): number | null {
const s = $statusByServer.get(name);
return s?.players ?? null;
}
</script>
<div class="grid">
<section class="left">
<header class="hdr">
<h1>Fleet</h1>
<div class="actions">
<button onclick={() => push(true)} disabled={pushing}>dry run</button>
<button class="primary" onclick={() => push(false)} disabled={pushing}>push all</button>
</div>
</header>
{#if flash}<p class="flash">{flash}</p>{/if}
{#if servers.length === 0}
<p class="empty">no servers registered yet — check SERVERS_ROOT</p>
{:else}
<div class="cards">
{#each servers as s (s.name)}
<ServerCard
name={s.name}
players={playersFor(s.name) ?? s.last_players}
held={s.held}
/>
{/each}
</div>
{/if}
</section>
<aside class="right">
<PipelineTimeline />
</aside>
</div>
<style>
.grid {
display: grid;
grid-template-columns: 1fr 380px;
gap: 20px;
}
@media (max-width: 900px) {
.grid { grid-template-columns: 1fr; }
}
.hdr {
display: flex;
justify-content: space-between;
align-items: baseline;
margin-bottom: 16px;
}
h1 {
margin: 0;
font-size: 22px;
color: #ff6060;
letter-spacing: 0.5px;
}
.actions { display: flex; gap: 8px; }
button {
background: #222;
color: #ddd;
border: 1px solid #444;
border-radius: 4px;
padding: 8px 14px;
cursor: pointer;
font-size: 13px;
}
button.primary { background: #ff3030; color: #fff; border-color: #ff3030; font-weight: 600; }
button:disabled { opacity: 0.5; cursor: default; }
.cards {
display: grid;
gap: 10px;
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
}
.empty { color: #666; }
.flash { color: #aaa; margin: 0 0 12px; font-family: ui-monospace, monospace; font-size: 12px; }
</style>