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
+39 -8
View File
@@ -76,18 +76,36 @@ export interface ServerRow {
last_seen_at: string | null;
last_error: string | null;
last_reply_snippet: string | null;
game_mode: string | null;
map_slug: string | null;
host_dir: string | null;
config_file: string | null;
game_id: string | null;
image: string | null;
created_by: string | null;
destroyed_at: string | null;
created_at: string;
}
export async function seedServerRegistry(): Promise<number> {
const rows = await allServers();
// Under the new naming scheme Komodo entities match EC server names 1:1
// (both underscore-separated), so komodo_name = name — no transformation.
const insert = db().prepare(
'INSERT OR IGNORE INTO servers (name, komodo_type, komodo_name, game, map) VALUES (?, ?, ?, ?, ?)'
`INSERT OR IGNORE INTO servers
(name, komodo_type, komodo_name, game, map, game_mode, map_slug, host_dir)
VALUES (?, 'deployment', ?, ?, ?, ?, ?, ?)`
);
// Also backfill existing rows where game/map were never set (upgrade path
// from before name-based auto-derivation existed).
// Backfill existing rows where derived columns were never set. game_mode
// and map_slug are new; older rows can still surface them from the name.
const backfill = db().prepare(
'UPDATE servers SET game = COALESCE(game, ?), map = COALESCE(map, ?) WHERE name = ? AND (game IS NULL OR map IS NULL)'
`UPDATE servers
SET game = COALESCE(game, ?),
map = COALESCE(map, ?),
game_mode = COALESCE(game_mode, ?),
map_slug = COALESCE(map_slug, ?),
host_dir = COALESCE(host_dir, ?)
WHERE name = ?`
);
let n = 0;
const tx = db().transaction((names: string[]) => {
@@ -95,14 +113,19 @@ export async function seedServerRegistry(): Promise<number> {
const parsed = parseServerName(name);
const game = parsed?.game ?? null;
const map = parsed?.mapCanonical ?? null;
const res = insert.run(name, 'deployment', name.replace(/_/g, '-'), game, map);
const gameMode = parsed?.gameMode ?? null;
const mapSlug = parsed?.mapSlug ?? null;
const hostDir = `${config.serversRoot}/${name}`;
const res = insert.run(name, name, game, map, gameMode, mapSlug, hostDir);
if (res.changes > 0) {
n++;
} else {
backfill.run(game, map, name);
backfill.run(game, map, gameMode, mapSlug, hostDir, name);
}
if (parsed && !parsed.mapCanonical) {
console.warn(`[servers] unknown map alias '${parsed.mapAlias}' for ${name} — add to T5_MAP_ALIASES`);
console.warn(
`[servers] unknown map slug '${parsed.mapSlug}' for ${name} — add to T5_ZOMBIE_MAPS`
);
}
}
});
@@ -138,7 +161,15 @@ export function setServerField(
'last_roster',
'last_seen_at',
'last_error',
'last_reply_snippet'
'last_reply_snippet',
'game_mode',
'map_slug',
'host_dir',
'config_file',
'game_id',
'image',
'created_by',
'destroyed_at'
];
if (!allowed.includes(field)) throw new Error(`refusing to set ${field}`);
db().prepare(`UPDATE servers SET ${field} = ? WHERE name = ?`).run(value, name);