import { randomUUID } from 'node:crypto'; import { db } from '../db/index.js'; import { recipeForDeployment, summarizeRecipe } from './launch-recipes.js'; export function beginDeployment(server: string, action: 'create' | 'destroy', triggeredBy = 'ui'): string { const id = randomUUID(); db().prepare(`INSERT INTO server_deployments (id, server, action, state, stage, started_at, triggered_by) VALUES (?, ?, ?, 'running', 'Starting', ?, ?)`).run(id, server, action, new Date().toISOString(), triggeredBy); recordDeploymentEvent(id, action === 'create' ? 'Server creation requested' : 'Server destruction requested'); return id; } export function deploymentStage(id: string, stage: string): void { const previous = db().prepare('SELECT stage FROM server_deployments WHERE id = ?').get(id) as { stage: string } | undefined; db().prepare('UPDATE server_deployments SET stage = ? WHERE id = ?').run(stage, id); if (previous && previous.stage !== stage) recordDeploymentEvent(id, stage); } export function recordDeploymentEvent(id: string, message: string): void { db().prepare('INSERT INTO server_deployment_events (deployment_id, created_at, message) VALUES (?, ?, ?)') .run(id, new Date().toISOString(), message); } export function getDeployment(id: string) { const deployment = db().prepare(`SELECT d.*, j.expected_map, EXISTS(SELECT 1 FROM servers s WHERE s.name = d.server) AS server_exists FROM server_deployments d LEFT JOIN server_creation_jobs j ON j.deployment_id = d.id WHERE d.id = ?`).get(id); if (!deployment) return null; const events = db().prepare('SELECT id, created_at, message FROM server_deployment_events WHERE deployment_id = ? ORDER BY id').all(id); const launch = recipeForDeployment(id); return { deployment, events, recipe: launch ? summarizeRecipe(launch.recipe) : null, blueprintName: launch?.blueprintName ?? null, canResumeVerification: canResumeVerification(id) }; } export function canResumeVerification(id: string): boolean { return !!db().prepare(`SELECT d.id FROM server_deployments d JOIN server_creation_jobs j ON j.deployment_id = d.id JOIN servers s ON s.name = j.server AND s.game_port = j.port WHERE d.id = ? AND d.action = 'create' AND d.state IN ('failed', 'interrupted') AND NOT EXISTS (SELECT 1 FROM server_deployments newer WHERE newer.server = d.server AND newer.rowid > d.rowid)`) .get(id); } /** Resume observation only. Never repeat a container creation or restart. */ export function resumeVerification(id: string): boolean { return db().transaction(() => { if (!canResumeVerification(id)) return false; db().prepare("UPDATE server_deployments SET state = 'awaiting-health', finished_at = NULL, error = NULL WHERE id = ?").run(id); recordDeploymentEvent(id, 'Resumed verification of the existing server'); deploymentStage(id, 'Waiting for Komodo health'); return true; })(); } export function finishDeployment(id: string, result: { ok: boolean; message?: string; warnings?: string[] }): void { const state = !result.ok ? 'failed' : result.warnings?.length ? 'warning' : 'done'; db().prepare(`UPDATE server_deployments SET state = ?, finished_at = ?, error = ?, warnings = ? WHERE id = ?`) .run(state, new Date().toISOString(), result.message ?? null, JSON.stringify(result.warnings ?? []), id); for (const warning of result.warnings ?? []) recordDeploymentEvent(id, warning); recordDeploymentEvent(id, result.ok ? (state === 'warning' ? 'Completed with warnings' : 'Completed') : `Failed: ${result.message ?? 'Unknown error'}`); } export function listDeployments() { return db().prepare(`SELECT d.*, EXISTS(SELECT 1 FROM servers s WHERE s.name = d.server) AS server_exists FROM server_deployments d ORDER BY started_at DESC, rowid DESC`).all(); } export function interruptDeployments(): void { const running = db().prepare("SELECT id FROM server_deployments WHERE state = 'running'").all() as { id: string }[]; db().prepare(`UPDATE server_deployments SET state = 'interrupted', finished_at = ?, error = 'The application restarted before this operation finished. Check the server before retrying.' WHERE state = 'running'`).run(new Date().toISOString()); for (const { id } of running) recordDeploymentEvent(id, 'Interrupted by application restart. Check the server before retrying.'); }