Use current promoted files for blueprint launches

This commit is contained in:
2026-09-19 19:58:30 -04:00
parent 41891b4209
commit 345839fbd4
15 changed files with 236 additions and 103 deletions
+15 -9
View File
@@ -167,7 +167,9 @@ export function previewCreation(input: { game?: unknown; mapSlug?: unknown; blue
if (typeof input.deploymentId !== 'string') throw new Error('Invalid deployment.');
const source = recipeForDeployment(input.deploymentId);
if (!source) throw new Error('This deployment has no recorded launch recipe.');
({ recipe, blueprintName } = source);
blueprintName = source.blueprintName;
// History describes the previous launch; only its map selection is reusable.
recipe = captureRecipe(source.recipe.game, source.recipe.mapSlug);
} else {
if ((input.game !== 't5' && input.game !== 't6') || typeof input.mapSlug !== 'string') throw new Error('Choose a supported game and map.');
recipe = captureRecipe(input.game, input.mapSlug);
@@ -176,13 +178,12 @@ export function previewCreation(input: { game?: unknown; mapSlug?: unknown; blue
const name = lobby === null ? null : `${recipe.game}_zombie_${recipe.mapSlug}_${lobby}`;
const port = nextAvailablePort();
const configured = !!(config.komodo.address && config.komodo.apiKey && config.komodo.apiSecret && config.komodo.targetServer);
const compatible = recipe.configFile === configFileForGame(recipe.game);
const checks: LaunchPreview['checks'] = [
{ label: 'Komodo settings', state: configured ? 'pass' : 'blocked', detail: configured ? 'Connection settings and target host are configured.' : 'Configure Komodo credentials and a target host before launching.' },
{ label: 'Health verification', state: config.rcon.password ? 'pass' : 'blocked', detail: config.rcon.password ? 'RCON is configured. Container, RCON, and map checks run after launch.' : 'Configure RCON_PASSWORD so the new server can be verified.' },
{ label: 'Lobby', state: name ? 'pass' : 'blocked', detail: name ?? 'All 20 lobbies for this map are occupied or reserved.' },
{ label: 'Game port', state: port === null ? 'blocked' : 'pass', detail: port === null ? 'No free game port. Free capacity before launching.' : `${port} / UDP and TCP. Availability is checked again at launch.` },
{ label: 'Configuration layout', state: compatible ? 'pass' : 'blocked', detail: compatible ? recipe.configSource : 'The configured filename has changed since this recipe was saved. Create a fresh recipe.' },
{ label: 'Promoted configuration', state: 'pass', detail: recipe.configSource },
{ label: 'Game files and host readiness', state: 'info', detail: 'The host must have the selected game and DLC files. This preview does not probe volumes, images, credentials, or network access.' }
];
const id = randomUUID();
@@ -208,15 +209,16 @@ export function queueCreateServer(input: CreateServerInput): CreateServerResult
if (preview && (!preview.server || !preview.port || preview.server !== nextName(input) || preview.port !== nextAvailablePort())) {
return { ok: false, kind: 'no-slot', message: 'Available lobby or port changed. Refresh the preview and review the new server key.' };
}
if (recipe && (recipe.configFile !== configFileForGame(recipe.game) || !config.rcon.password)) {
return { ok: false, kind: 'unconfigured', message: 'Configuration or health-check settings changed. Refresh the preview.' };
if (recipe && !config.rcon.password) {
return { ok: false, kind: 'unconfigured', message: 'Health-check settings changed. Refresh the preview.' };
}
const prepared = prepareCreation(input);
if (!prepared.ok) return prepared;
const frozenRecipe = recipe ?? captureRecipe(input.game ?? 't5', prepared.mapEntry.slug);
// A preview records choices, not version pins. Promotions remain authoritative.
const currentRecipe = captureRecipe(input.game ?? 't5', prepared.mapEntry.slug);
const id = beginDeployment(prepared.name, 'create', input.createdBy);
db().prepare('INSERT INTO server_launch_recipes (deployment_id, recipe, blueprint_name) VALUES (?, ?, ?)')
.run(id, JSON.stringify(frozenRecipe), preview?.blueprint_name ?? null);
.run(id, JSON.stringify(currentRecipe), preview?.blueprint_name ?? null);
if (preview) db().prepare('UPDATE server_launch_previews SET deployment_id = ? WHERE id = ?').run(id, preview.id);
db().prepare("UPDATE server_deployments SET state = 'queued' WHERE id = ?").run(id);
db().prepare('INSERT INTO server_creation_jobs (deployment_id, server, port, game_mode, map_slug, expected_map) VALUES (?, ?, ?, ?, ?, ?)')
@@ -293,9 +295,13 @@ function prepareCreation(input: CreateServerInput): PreparedCreation | Extract<C
async function createServerUnlocked(input: CreateServerInput, prepared: PreparedCreation, operation: string): Promise<CreateServerResult> {
const { name, port, plutoKey, mapEntry } = prepared;
const game = gameOfServer(name);
const recipe = recipeForDeployment(operation)?.recipe ?? captureRecipe(game, mapEntry.slug);
// Resolve once more at provisioning so promotions made while queued also apply.
// Persist the exact attempted files for history, never as input to future launches.
const recipe = captureRecipe(game, mapEntry.slug);
db().prepare(`INSERT INTO server_launch_recipes (deployment_id, recipe) VALUES (?, ?)
ON CONFLICT(deployment_id) DO UPDATE SET recipe = excluded.recipe`)
.run(operation, JSON.stringify(recipe));
const configFile = recipe.configFile;
if (configFile !== configFileForGame(game)) return { ok: false, kind: 'bad-request', message: 'Server config filename changed after this launch was queued.' };
deploymentStage(operation, 'Preparing server files');
const hostDir = serverDir(name);