Rework as scripts-centric, upload-only, gated beta lifecycle

Scripts become first-class: named, versioned, uploaded via the UI, optionally
scoped to a game or map. Any change that would land on scope-matched servers
(marking-required or new version of a scoped script) must first pass a beta.
The beta runs a per-server state machine — deploy -beta.gsc → wait empty →
Komodo restart → wait healthy → optionally wait for a real match end
(games_sp.log 'ExitLevel: executed') — before it's eligible for promotion.
Rollback is always available and restores from .OLD.

The shared/scripts mirror and its betas/ staging folder are gone. Every
server now has a tracked script inventory (server_scripts table), populated
only by explicit upload flows.

New: /scripts, /scripts/[id], /betas/[id] UI; /api/scripts + /api/betas +
/api/servers/[name]/scripts endpoints; scripts.ts, inventory.ts, beta.ts
(rewritten), beta-runner.ts, match-log.ts, komodoInspectHealthy().
Removed: push.ts, /api/push, BetaPanel, legacy importer.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-09-12 13:25:06 -04:00
co-authored by Claude Opus 4.7
parent 47051dc79a
commit 93d5a8e2e5
52 changed files with 2605 additions and 1689 deletions
+70 -29
View File
@@ -1,5 +1,7 @@
-- Electric Cherry schema. Idempotent — safe to run at every boot via migrate.ts.
-- Mirrors the shapes tracked in the legacy script_metadata.json but in real SQL.
-- 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;
@@ -11,6 +13,8 @@ CREATE TABLE IF NOT EXISTS servers (
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
@@ -22,37 +26,74 @@ CREATE TABLE IF NOT EXISTS servers (
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,
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 (
slug TEXT PRIMARY KEY,
name TEXT NOT NULL,
folder TEXT NOT NULL,
opened_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
revision INTEGER NOT NULL DEFAULT 1
id INTEGER PRIMARY KEY AUTOINCREMENT,
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, -- 'mark-required' | 'version-update' | 'unscoped'
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_scripts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
beta_slug TEXT NOT NULL REFERENCES betas(slug) ON DELETE CASCADE,
staged_as TEXT NOT NULL,
target TEXT NOT NULL,
beta_file TEXT NOT NULL,
backup TEXT NOT NULL,
sha256 TEXT NOT NULL,
revision INTEGER NOT NULL DEFAULT 1
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,
prior_version_id INTEGER REFERENCES script_versions(id),
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_scripts_slug ON beta_scripts(beta_slug);
CREATE TABLE IF NOT EXISTS beta_deploys (
id INTEGER PRIMARY KEY AUTOINCREMENT,
beta_script_id INTEGER NOT NULL REFERENCES beta_scripts(id) ON DELETE CASCADE,
server TEXT NOT NULL REFERENCES servers(name),
had_original INTEGER NOT NULL,
UNIQUE(beta_script_id, server)
);
CREATE INDEX IF NOT EXISTS idx_beta_deploys_server ON beta_deploys(server);
CREATE INDEX IF NOT EXISTS idx_beta_servers_stage ON beta_servers(stage);
CREATE TABLE IF NOT EXISTS deployments (
id TEXT PRIMARY KEY,
@@ -83,7 +124,7 @@ 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 | held
verb TEXT NOT NULL, -- added | updated | removed
rel_path TEXT NOT NULL,
sha256_before TEXT,
sha256_after TEXT,