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
+38 -1
View File
@@ -25,7 +25,10 @@ const SERVERS_COLUMNS: Array<{ name: string; ddl: string }> = [
{ name: 'last_roster', ddl: 'ALTER TABLE servers ADD COLUMN last_roster TEXT' },
{ name: 'last_seen_at', ddl: 'ALTER TABLE servers ADD COLUMN last_seen_at TEXT' },
{ name: 'last_error', ddl: 'ALTER TABLE servers ADD COLUMN last_error TEXT' },
{ name: 'last_reply_snippet', ddl: 'ALTER TABLE servers ADD COLUMN last_reply_snippet TEXT' }
{ name: 'last_reply_snippet', ddl: 'ALTER TABLE servers ADD COLUMN last_reply_snippet TEXT' },
// Script-scope columns — static per server, populated via server-edit UI.
{ name: 'game', ddl: 'ALTER TABLE servers ADD COLUMN game TEXT' },
{ name: 'map', ddl: 'ALTER TABLE servers ADD COLUMN map TEXT' }
];
function migrateServers(handle: Database.Database): void {
@@ -38,6 +41,39 @@ function migrateServers(handle: Database.Database): void {
}
}
// The pre-script-model beta tables had entirely different shapes (slug PK,
// beta_scripts, beta_deploys). schema.sql defines the new shape; CREATE TABLE
// IF NOT EXISTS is a no-op if the old tables already exist, so we have to
// detect and drop them first. Any outstanding rows abort the boot so the
// operator can promote/rollback in the old app before upgrading.
function dropLegacyBetaTables(handle: Database.Database): void {
const legacy = handle
.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name IN ('betas','beta_scripts','beta_deploys')")
.all() as Array<{ name: string }>;
if (legacy.length === 0) return;
const legacyNames = legacy.map((r) => r.name);
// The old `betas` table had a `slug` column; the new one has `id` + `script_id`.
// If we see `slug`, we know it's the old shape.
const betasCols = handle.prepare("PRAGMA table_info(betas)").all() as Array<{ name: string }>;
const isLegacyShape = betasCols.some((c) => c.name === 'slug');
if (!isLegacyShape) return;
for (const table of ['beta_deploys', 'beta_scripts', 'betas']) {
if (!legacyNames.includes(table)) continue;
const row = handle.prepare(`SELECT COUNT(*) AS n FROM ${table}`).get() as { n: number };
if (row.n > 0) {
throw new Error(
`legacy ${table} still has ${row.n} rows — promote or rollback outstanding betas in the previous version before upgrading`
);
}
}
// Drop in FK-safe order.
for (const table of ['beta_deploys', 'beta_scripts', 'betas']) {
if (legacyNames.includes(table)) handle.exec(`DROP TABLE ${table}`);
}
}
export function db(): Database.Database {
if (_db) return _db;
@@ -45,6 +81,7 @@ export function db(): Database.Database {
_db = new Database(config.dbPath);
_db.pragma('journal_mode = WAL');
_db.pragma('foreign_keys = ON');
dropLegacyBetaTables(_db);
_db.exec(schemaSql);
migrateServers(_db);