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>
224 lines
12 KiB
SQL
224 lines
12 KiB
SQL
-- Electric Cherry schema. Idempotent — safe to run at every boot via migrate.ts.
|
|
-- The old betas/beta_scripts/beta_deploys tables are dropped in-code before this
|
|
-- runs (see dropLegacyBetaTables in db/index.ts); the new script-centric model
|
|
-- lives here.
|
|
|
|
PRAGMA journal_mode = WAL;
|
|
PRAGMA foreign_keys = ON;
|
|
|
|
CREATE TABLE IF NOT EXISTS servers (
|
|
name TEXT PRIMARY KEY,
|
|
komodo_type TEXT NOT NULL DEFAULT 'deployment', -- deployment|stack|container
|
|
komodo_name TEXT NOT NULL,
|
|
komodo_server TEXT,
|
|
game_port INTEGER, -- UDP RCON port; lazy-filled from Komodo
|
|
rcon_host TEXT, -- optional per-server override of RCON_HOST
|
|
game TEXT, -- 't5' etc.; static per-server (no map rotation)
|
|
map TEXT, -- 'zombie_theater' etc.; static per-server
|
|
last_players INTEGER,
|
|
last_checked_at TEXT,
|
|
last_map TEXT, -- from RCON getstatus mapname
|
|
last_gametype TEXT, -- from RCON getstatus g_gametype
|
|
last_roster TEXT, -- JSON array of RconPlayer
|
|
last_seen_at TEXT, -- ISO ts of last successful RCON reply
|
|
last_error TEXT, -- last RCON failure reason (cleared on success)
|
|
last_reply_snippet TEXT, -- hex+ASCII of last bad-reply UDP payload
|
|
-- Provisioning fields — set when Electric Cherry creates the deployment.
|
|
game_mode TEXT, -- 'zombie' | 'mp' | 'sp' (currently zombie only)
|
|
map_slug TEXT, -- 'cosmodrome' — short slug used in server name
|
|
host_dir TEXT, -- absolute host path bind-mounted into the container
|
|
config_file TEXT, -- e.g. 'dedicated_sp.cfg' (PLUTAINER_CONFIG_FILE)
|
|
game_id TEXT, -- e.g. 't5sp' (PLUTAINER_GAME)
|
|
image TEXT, -- e.g. 'ghcr.io/ayymoss/plutainer:latest'
|
|
created_by TEXT, -- 'ui' | operator identifier
|
|
destroyed_at TEXT, -- ISO ts when tombstoned (row usually deleted; kept as safety net)
|
|
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now'))
|
|
);
|
|
|
|
-- One row per artifact (a logical thing, e.g. "HUD tweaks"). Name is unique and
|
|
-- doubles as the slug in URLs. The `kind` column discriminates:
|
|
-- 'script' — GSC files deployed to runtime/plutonium/storage/t5/raw/scripts/sp
|
|
-- 'plugin' — .dll files deployed to runtime/plutonium/plugins
|
|
-- 'config' — cfg overlay (set K "V" lines) layered onto the baseline
|
|
-- 'baseline' — full dedicated_sp.cfg; exactly one per scope
|
|
-- Table + column names keep the "scripts" prefix for continuity; treat them
|
|
-- polymorphically at the app layer.
|
|
CREATE TABLE IF NOT EXISTS scripts (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
name TEXT NOT NULL UNIQUE,
|
|
target_rel_path TEXT NOT NULL, -- 'maps/mp_kino/hud.gsc' or 'hud.gsc'
|
|
scope_type TEXT, -- NULL | 'game' | 'map'
|
|
scope_value TEXT, -- NULL | 't5' | 'zombie_theater'
|
|
current_version_id INTEGER, -- FK → script_versions(id) after first promote
|
|
kind TEXT NOT NULL DEFAULT 'script', -- 'script' | 'config' | 'plugin' | 'baseline'
|
|
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now'))
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_scripts_scope ON scripts(scope_type, scope_value);
|
|
-- kind-referencing indexes are created in db/index.ts AFTER migrateTable adds
|
|
-- the column to legacy DBs. Putting them here would fail with "no such column:
|
|
-- kind" on an existing pre-kind DB, breaking boot.
|
|
|
|
CREATE TABLE IF NOT EXISTS script_versions (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
script_id INTEGER NOT NULL REFERENCES scripts(id) ON DELETE CASCADE,
|
|
version INTEGER NOT NULL,
|
|
sha256 TEXT NOT NULL,
|
|
size INTEGER NOT NULL,
|
|
content BLOB NOT NULL,
|
|
notes TEXT,
|
|
state TEXT NOT NULL DEFAULT 'untested', -- 'untested' | 'tested' | 'test-failed'
|
|
kind TEXT NOT NULL DEFAULT 'script', -- denormalized from scripts.kind for join-free filtering
|
|
uploaded_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')),
|
|
UNIQUE(script_id, version)
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_script_versions_script ON script_versions(script_id);
|
|
|
|
CREATE TABLE IF NOT EXISTS server_scripts (
|
|
server TEXT NOT NULL REFERENCES servers(name) ON DELETE CASCADE,
|
|
rel_path TEXT NOT NULL,
|
|
script_version_id INTEGER REFERENCES script_versions(id) ON DELETE SET NULL,
|
|
sha256 TEXT NOT NULL,
|
|
size INTEGER NOT NULL,
|
|
updated_at TEXT NOT NULL,
|
|
source TEXT, -- 'beta:<id>' | 'promote:<id>' | 'rollback:<id>'
|
|
PRIMARY KEY (server, rel_path)
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_server_scripts_server ON server_scripts(server);
|
|
|
|
CREATE TABLE IF NOT EXISTS betas (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
-- DEPRECATED: multi-script betas are represented by beta_scripts. These
|
|
-- three columns are populated from the first bundled script to satisfy
|
|
-- NOT NULL on legacy schemas; the fleet code doesn't read them anymore.
|
|
script_id INTEGER NOT NULL REFERENCES scripts(id) ON DELETE CASCADE,
|
|
script_version_id INTEGER NOT NULL REFERENCES script_versions(id) ON DELETE CASCADE,
|
|
trigger_kind TEXT NOT NULL,
|
|
wait_for_health INTEGER NOT NULL DEFAULT 1,
|
|
wait_for_match INTEGER NOT NULL DEFAULT 0,
|
|
state TEXT NOT NULL, -- 'running' | 'ready-promote' | 'promoted' | 'rolled-back'
|
|
opened_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')),
|
|
closed_at TEXT,
|
|
notes TEXT
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_betas_state ON betas(state);
|
|
CREATE INDEX IF NOT EXISTS idx_betas_script ON betas(script_id);
|
|
|
|
CREATE TABLE IF NOT EXISTS beta_servers (
|
|
beta_id INTEGER NOT NULL REFERENCES betas(id) ON DELETE CASCADE,
|
|
server TEXT NOT NULL REFERENCES servers(name),
|
|
had_original INTEGER NOT NULL, -- deprecated; see beta_server_scripts
|
|
prior_version_id INTEGER REFERENCES script_versions(id), -- deprecated; see beta_server_scripts
|
|
stage TEXT NOT NULL, -- 'deploying'|'awaiting-empty'|'restarting'|'awaiting-health'|'awaiting-match'|'ready'|'failed'
|
|
error TEXT,
|
|
deployed_at TEXT,
|
|
restarted_at TEXT,
|
|
healthy_at TEXT,
|
|
match_seen_at TEXT,
|
|
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')),
|
|
PRIMARY KEY (beta_id, server)
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_beta_servers_stage ON beta_servers(stage);
|
|
|
|
-- Many-to-many: which scripts (and pinned versions) are bundled into a beta.
|
|
-- Replaces the deprecated betas.script_id / betas.script_version_id columns.
|
|
CREATE TABLE IF NOT EXISTS beta_scripts (
|
|
beta_id INTEGER NOT NULL REFERENCES betas(id) ON DELETE CASCADE,
|
|
script_id INTEGER NOT NULL REFERENCES scripts(id) ON DELETE CASCADE,
|
|
script_version_id INTEGER NOT NULL REFERENCES script_versions(id) ON DELETE CASCADE,
|
|
kind TEXT NOT NULL DEFAULT 'script', -- denormalized from scripts.kind
|
|
PRIMARY KEY (beta_id, script_id)
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_beta_scripts_beta ON beta_scripts(beta_id);
|
|
CREATE INDEX IF NOT EXISTS idx_beta_scripts_script ON beta_scripts(script_id);
|
|
|
|
-- Per (beta, server, script) prior-file state. A server row in beta_servers
|
|
-- covers restart progress; this table covers whether each script's target
|
|
-- existed on that server before the beta overwrote it — snapshot content
|
|
-- lives in beta_file_snapshots.
|
|
CREATE TABLE IF NOT EXISTS beta_server_scripts (
|
|
beta_id INTEGER NOT NULL REFERENCES betas(id) ON DELETE CASCADE,
|
|
server TEXT NOT NULL REFERENCES servers(name),
|
|
script_id INTEGER NOT NULL REFERENCES scripts(id) ON DELETE CASCADE,
|
|
had_original INTEGER NOT NULL,
|
|
prior_version_id INTEGER REFERENCES script_versions(id),
|
|
PRIMARY KEY (beta_id, server, script_id)
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_beta_server_scripts_beta ON beta_server_scripts(beta_id);
|
|
|
|
-- Pre-beta bytes of every target file the beta overwrote on each server.
|
|
-- Absence of a row for (beta, server, rel_path) means the file did not exist
|
|
-- before the beta wrote it — rollback deletes the file rather than restoring.
|
|
-- Handles scripts, plugins, and the composed dedicated_sp.cfg uniformly.
|
|
CREATE TABLE IF NOT EXISTS beta_file_snapshots (
|
|
beta_id INTEGER NOT NULL REFERENCES betas(id) ON DELETE CASCADE,
|
|
server TEXT NOT NULL REFERENCES servers(name) ON DELETE CASCADE,
|
|
rel_path TEXT NOT NULL,
|
|
prior_content BLOB NOT NULL,
|
|
prior_sha256 TEXT NOT NULL,
|
|
prior_size INTEGER NOT NULL,
|
|
PRIMARY KEY (beta_id, server, rel_path)
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_beta_file_snapshots_beta ON beta_file_snapshots(beta_id);
|
|
|
|
CREATE TABLE IF NOT EXISTS deployments (
|
|
id TEXT PRIMARY KEY,
|
|
action TEXT NOT NULL, -- 'drift' | 'promote' | 'rollback'
|
|
started_at TEXT NOT NULL,
|
|
finished_at TEXT,
|
|
triggered_by TEXT,
|
|
script_id INTEGER REFERENCES scripts(id), -- promote/rollback only
|
|
script_version_id INTEGER REFERENCES script_versions(id), -- the version this deployment lands / restores
|
|
prior_version_id INTEGER REFERENCES script_versions(id), -- current_version_id before this promote
|
|
beta_id INTEGER REFERENCES betas(id), -- promote only
|
|
rolled_back_by TEXT REFERENCES deployments(id) -- set on the promote row when a rollback reverses it
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_deployments_started ON deployments(started_at DESC);
|
|
|
|
CREATE TABLE IF NOT EXISTS deployment_servers (
|
|
deployment_id TEXT NOT NULL REFERENCES deployments(id) ON DELETE CASCADE,
|
|
server TEXT NOT NULL REFERENCES servers(name),
|
|
files_changed INTEGER NOT NULL,
|
|
restart_state TEXT NOT NULL, -- pending | done | failed | manual | held
|
|
players INTEGER,
|
|
checked_at TEXT,
|
|
restarted_at TEXT,
|
|
attempts INTEGER NOT NULL DEFAULT 0,
|
|
error TEXT,
|
|
PRIMARY KEY (deployment_id, server)
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_deployment_servers_state ON deployment_servers(restart_state);
|
|
|
|
CREATE TABLE IF NOT EXISTS deployment_files (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
deployment_id TEXT NOT NULL REFERENCES deployments(id) ON DELETE CASCADE,
|
|
server TEXT NOT NULL,
|
|
verb TEXT NOT NULL, -- added | updated | removed
|
|
rel_path TEXT NOT NULL,
|
|
sha256_before TEXT,
|
|
sha256_after TEXT,
|
|
content_before BLOB,
|
|
content_after BLOB
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_deployment_files_dep ON deployment_files(deployment_id);
|
|
|
|
-- Per-script rows on aggregate promote/rollback deployments. For legacy
|
|
-- single-script deployments this may also be backfilled from the deprecated
|
|
-- deployments.script_id / script_version_id / prior_version_id columns.
|
|
CREATE TABLE IF NOT EXISTS deployment_scripts (
|
|
deployment_id TEXT NOT NULL REFERENCES deployments(id) ON DELETE CASCADE,
|
|
script_id INTEGER NOT NULL REFERENCES scripts(id) ON DELETE CASCADE,
|
|
script_version_id INTEGER NOT NULL REFERENCES script_versions(id) ON DELETE CASCADE,
|
|
prior_version_id INTEGER REFERENCES script_versions(id),
|
|
kind TEXT NOT NULL DEFAULT 'script', -- denormalized from scripts.kind
|
|
PRIMARY KEY (deployment_id, script_id)
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_deployment_scripts_dep ON deployment_scripts(deployment_id);
|
|
|
|
CREATE TABLE IF NOT EXISTS settings (
|
|
key TEXT PRIMARY KEY,
|
|
value TEXT NOT NULL
|
|
);
|