-- 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 created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')) ); -- One row per script (a logical thing, e.g. "HUD tweaks"). Name is unique and -- doubles as the slug in URLs. 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 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); 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' 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:' | 'promote:' | 'rollback:' 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, 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 what each script's target -- looked like on that server before the beta wrote its -beta.gsc. 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); 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), 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 );