Files
electric-cherry/src/lib/server/db/schema.sql
T
joshandClaude Opus 4.7 13a29e5ace 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>
2026-09-13 18:38:42 -04:00

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
);