Files
electric-cherry/src/lib/server/db/schema.sql
T
2026-09-17 16:57:08 -04:00

279 lines
15 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)
-- Occupancy + match timers. active_since / inactive_since are mutually
-- exclusive: exactly one is non-null while a server is reachable. RCON
-- outages do NOT clear either — timers survive transient unreachability
-- and resume from the same anchor when the server comes back.
active_since TEXT, -- ISO ts of last 0 -> >0 player transition
inactive_since TEXT, -- ISO ts of last >0 -> 0 player transition
game_started_at TEXT, -- wall-clock ISO when we first saw a J; line for the current match cycle
game_ended_at TEXT, -- wall-clock ISO when we first saw ExitLevel/ShutdownGame end it
game_cycle_key TEXT, -- opaque marker for the currently-tracked cycle (the first J; line after the latest InitGame); lets us detect same-players game restarts
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);
-- Fleet-wide restart slots shared by deployments and betas. A successful API
-- response does not release a slot: the server must recover first. Persisting
-- these rows also protects the fleet across Electric Cherry process restarts.
CREATE TABLE IF NOT EXISTS restart_rollout (
server TEXT PRIMARY KEY REFERENCES servers(name) ON DELETE CASCADE,
requested_at TEXT NOT NULL,
checked_at TEXT,
healthy_at TEXT,
players INTEGER,
error TEXT
);
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);
-- Deliberately independent of servers: credentials survive server destruction.
CREATE TABLE IF NOT EXISTS server_keys (
server_id TEXT PRIMARY KEY,
pluto_key TEXT NOT NULL,
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS settings (
key TEXT PRIMARY KEY,
value TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS server_telemetry (
server TEXT PRIMARY KEY REFERENCES servers(name) ON DELETE CASCADE,
round INTEGER NOT NULL,
players INTEGER NOT NULL,
received_at TEXT NOT NULL
);
-- Lifecycle history deliberately survives removal of the server registry row.
CREATE TABLE IF NOT EXISTS server_deployments (
id TEXT PRIMARY KEY,
server TEXT NOT NULL,
action TEXT NOT NULL CHECK (action IN ('create', 'destroy')),
state TEXT NOT NULL,
stage TEXT NOT NULL,
started_at TEXT NOT NULL,
finished_at TEXT,
triggered_by TEXT NOT NULL,
error TEXT,
warnings TEXT NOT NULL DEFAULT '[]'
);
CREATE TABLE IF NOT EXISTS server_deployment_events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
deployment_id TEXT NOT NULL REFERENCES server_deployments(id) ON DELETE CASCADE,
created_at TEXT NOT NULL,
message TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_server_deployment_events ON server_deployment_events(deployment_id, id);