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:
@@ -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>
|
||||
Reference in New Issue
Block a user