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
+163
View File
@@ -0,0 +1,163 @@
import { config, iw4mConfigured, komodoConfigured } from '../config.js';
import { db } from '../db/index.js';
import { emit } from './bus.js';
import { fetchStatus, playersOn } from '../integrations/iw4m.js';
import { komodoRestart } from '../integrations/komodo.js';
import { pipelineReady } from '../integrations/auth-state.js';
interface PendingRow {
deployment_id: string;
server: string;
attempts: number;
}
function nowIso(): string {
return new Date().toISOString();
}
function integrationsOn(): boolean {
return komodoConfigured() && iw4mConfigured();
}
/**
* Re-check every pending/failed deployment_server row, restart the ones with
* zero players, mark the busy ones held. Same-server dedup is per-sweep, so
* one restart resolves every pending row for that server. Ports `deploy_sweep`
* in push_scripts.sh:705.
*/
export async function sweep(): Promise<{ checked: number; restarted: number; held: number; failed: number; frozen: boolean }> {
if (!pipelineReady()) {
emit({ type: 'sweep-tick', frozen: true, reason: 'iw4m token stale' });
return { checked: 0, restarted: 0, held: 0, failed: 0, frozen: true };
}
if (!integrationsOn()) {
// Same behavior as line 714 — mark rows manual so they don't hang
// forever if integrations were never configured.
db()
.prepare(
`UPDATE deployment_servers
SET restart_state = 'manual', checked_at = ?, error = 'integrations not configured'
WHERE restart_state IN ('pending','failed')`
)
.run(nowIso());
return { checked: 0, restarted: 0, held: 0, failed: 0, frozen: false };
}
const pending = db()
.prepare(
`SELECT deployment_id, server, attempts
FROM deployment_servers
WHERE restart_state IN ('pending','failed')`
)
.all() as PendingRow[];
if (pending.length === 0) return { checked: 0, restarted: 0, held: 0, failed: 0, frozen: false };
// Warm the /api/status cache so per-server calls just hit memory
const status = await fetchStatus();
if (!status.ok) {
// Same as line 742 — mark held with the iw4m error
const stmt = db().prepare(
`UPDATE deployment_servers
SET checked_at = ?, error = ?, attempts = attempts + 1
WHERE restart_state IN ('pending','failed')`
);
stmt.run(nowIso(), `iw4m: ${status.message}`);
return { checked: 0, restarted: 0, held: pending.length, failed: 0, frozen: false };
}
// Dedup across deployments: if we resolved server X once this tick,
// reuse the outcome for every other pending row on X.
type Outcome = { state: 'done' | 'failed' | 'pending'; players: number | null; error: string | null };
const seen = new Map<string, Outcome>();
let checked = 0;
let restarted = 0;
let held = 0;
let failed = 0;
const applyOutcome = db().prepare(
`UPDATE deployment_servers
SET restart_state = ?, checked_at = ?, players = ?, error = ?, attempts = attempts + 1,
restarted_at = CASE WHEN ? = 'done' THEN ? ELSE restarted_at END
WHERE deployment_id = ? AND server = ?`
);
for (const row of pending) {
let outcome = seen.get(row.server);
if (!outcome) {
checked++;
const players = await playersOn(row.server);
if (players === null) {
outcome = { state: 'pending', players: null, error: 'no iw4m match' };
held++;
} else if (players > 0) {
outcome = { state: 'pending', players, error: null };
held++;
} else {
const r = await komodoRestart(row.server);
if (r.ok) {
outcome = { state: 'done', players: 0, error: null };
restarted++;
} else if (r.kind === 'no-name') {
outcome = { state: 'failed', players: 0, error: r.message };
failed++;
} else {
outcome = { state: 'failed', players: 0, error: `komodo: ${r.message}` };
failed++;
}
}
seen.set(row.server, outcome);
}
const now = nowIso();
applyOutcome.run(
outcome.state,
now,
outcome.players,
outcome.error,
outcome.state,
now,
row.deployment_id,
row.server
);
emit({
type: 'server-state',
deploymentId: row.deployment_id,
server: row.server,
state: outcome.state,
players: outcome.players,
error: outcome.error
});
}
// Finish deployments where every server is now non-pending
db()
.prepare(
`UPDATE deployments
SET finished_at = ?
WHERE finished_at IS NULL
AND NOT EXISTS (
SELECT 1 FROM deployment_servers
WHERE deployment_id = deployments.id
AND restart_state IN ('pending','failed')
)`
)
.run(nowIso());
// Prune rows older than DEPLOY_KEEP_DAYS that have no pending/failed servers
const cutoff = new Date(Date.now() - config.deployKeepDays * 24 * 60 * 60 * 1000).toISOString();
db()
.prepare(
`DELETE FROM deployments
WHERE started_at < ?
AND NOT EXISTS (
SELECT 1 FROM deployment_servers
WHERE deployment_id = deployments.id
AND restart_state IN ('pending','failed')
)`
)
.run(cutoff);
emit({ type: 'sweep-tick', frozen: false });
return { checked, restarted, held, failed, frozen: false };
}