Add deploy + destroy server flow; migrate to t5_<mode>_<slug>_<n> naming

Full server lifecycle management from Electric Cherry: pick game/mode/map/pluto
key in the Fleet page, EC provisions the deployment via Komodo, allocates a
port in 28960-28980, creates the 0o777 host dir, and starts the container.
Destroy button tears everything down (Komodo Destroy+Delete, rm -rf host dir,
transactional DB cleanup) and surfaces any partial-failure warnings inline.

Same phase cuts over to the new naming scheme (t5_zombie_cosmodrome_1 etc.).
Komodo entity names now match EC server names 1:1 (no more _/- transform), and
the map catalog carries friendly/slug/canonical for each of the 11 t5 zombie
maps. Server rows gain game_mode, map_slug, host_dir, config_file, game_id,
image, created_by, and destroyed_at columns for provisioning + audit.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-09-13 18:38:42 -04:00
co-authored by Claude Opus 4.7
parent 8c15cae80d
commit 13a29e5ace
25 changed files with 1103 additions and 123 deletions
+12 -12
View File
@@ -8,13 +8,13 @@ let fixture: Fixture;
beforeEach(async () => {
vi.resetModules();
fixture = await newFixture(['t5_kino_1']);
fixture = await newFixture(['t5_zombie_theater_1']);
delete process.env.KOMODO_ADDRESS;
delete process.env.RCON_PASSWORD;
// Ensure the three per-kind destinations exist under the fixture tree.
await mkdir(join(fixture.t5, 't5_kino_1', 'runtime/plutonium/storage/t5/raw/scripts/sp'), { recursive: true });
await mkdir(join(fixture.t5, 't5_kino_1', 'configs'), { recursive: true });
await mkdir(join(fixture.t5, 't5_kino_1', 'runtime/plutonium/plugins'), { recursive: true });
await mkdir(join(fixture.t5, 't5_zombie_theater_1', 'runtime/plutonium/storage/t5/raw/scripts/sp'), { recursive: true });
await mkdir(join(fixture.t5, 't5_zombie_theater_1', 'configs'), { recursive: true });
await mkdir(join(fixture.t5, 't5_zombie_theater_1', 'runtime/plutonium/plugins'), { recursive: true });
});
afterEach(async () => {
@@ -28,7 +28,7 @@ describe('drift scanning', () => {
await seedServerRegistry();
// Drop one stray file in each managed destination.
const server = 't5_kino_1';
const server = 't5_zombie_theater_1';
await writeFile(
join(fixture.t5, server, 'runtime/plutonium/storage/t5/raw/scripts/sp', 'stray.gsc'),
'main(){}\n'
@@ -84,7 +84,7 @@ async function promoteAndOrphan(
const ver = getLatestVersion(created.script.id)!;
const { betaId } = await openBeta({
scripts: [{ scriptId: created.script.id, scriptVersionId: ver.id }],
servers: ['t5_kino_1'],
servers: ['t5_zombie_theater_1'],
waitForHealth: false,
waitForMatch: false
});
@@ -121,7 +121,7 @@ describe('missing artifact detection', () => {
const targetPath = join(
fixture.t5,
't5_kino_1',
't5_zombie_theater_1',
'runtime/plutonium/storage/t5/raw/scripts/sp',
'hud.gsc'
);
@@ -131,13 +131,13 @@ describe('missing artifact detection', () => {
// Simulate someone deleting the file out-of-band.
await rm(targetPath, { force: true });
const drift = await scanServerDrift('t5_kino_1');
const drift = await scanServerDrift('t5_zombie_theater_1');
const missing = drift.filter((d) => d.driftKind === 'missing' && d.kind === 'script');
expect(missing.length).toBe(1);
expect(missing[0].relPath).toBe('hud.gsc');
const result = await runDriftDeployment({
servers: ['t5_kino_1'],
servers: ['t5_zombie_theater_1'],
dryRun: false,
triggeredBy: 'ui'
});
@@ -169,7 +169,7 @@ describe('missing artifact detection', () => {
const bVer = getLatestVersion(baseline.script.id)!;
const { betaId } = await openBeta({
scripts: [{ scriptId: baseline.script.id, scriptVersionId: bVer.id }],
servers: ['t5_kino_1'],
servers: ['t5_zombie_theater_1'],
waitForHealth: false,
waitForMatch: false
});
@@ -179,11 +179,11 @@ describe('missing artifact detection', () => {
db().prepare(`UPDATE betas SET state = 'ready-promote' WHERE id = ?`).run(betaId);
await promoteBeta(betaId);
const cfgPath = join(fixture.t5, 't5_kino_1', 'configs', 'dedicated_sp.cfg');
const cfgPath = join(fixture.t5, 't5_zombie_theater_1', 'configs', 'dedicated_sp.cfg');
expect(existsSync(cfgPath)).toBe(true);
await rm(cfgPath, { force: true });
const drift = await scanServerDrift('t5_kino_1');
const drift = await scanServerDrift('t5_zombie_theater_1');
const missingCfg = drift.filter((d) => d.driftKind === 'missing' && d.kind === 'config');
expect(missingCfg.length).toBe(1);
expect(missingCfg[0].relPath).toBe('dedicated_sp.cfg');