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
View File
@@ -0,0 +1,38 @@
import { resolveLogPath, tailLog } from './logs.js';
// Match markers for T5 Plutonium's `logs/games_sp.log`. Verified against a live
// sample: each game session is bracketed by `InitGame:` and `ShutdownGame:`.
// A REAL completed match always emits `ExitLevel: executed` right before
// `ShutdownGame:`. Idle map cycles print `InitGame:` / `ShutdownGame:` at
// 0:00 with no ExitLevel — those don't count as a played match.
//
// So the reliable discriminator is `ExitLevel: executed`: seeing that line
// after `healthy_at` means a game actually ended normally (no crash — a crash
// would kill the container before ExitLevel fires and take RCON with it).
export const MATCH_START = /InitGame:/;
export const MATCH_END = /ExitLevel:\s*executed/;
/**
* Poll a server's games_sp.log tail for match markers. Returns whether a
* completed match end has been observed since `sinceIso` (the server's
* `healthy_at` timestamp).
*/
export async function scanForMatchEnd(
serverName: string,
sinceIso: string
): Promise<{ ended: boolean; started: boolean; reason?: string }> {
const path = resolveLogPath(serverName, 'games_sp');
if (!path) return { ended: false, started: false, reason: 'log path resolution failed' };
const tail = await tailLog(path, 2000);
if (!tail) return { ended: false, started: false, reason: 'log not written yet' };
if (tail.mtime < sinceIso) return { ended: false, started: false, reason: 'no log activity since healthy' };
let started = false;
let ended = false;
for (const line of tail.lines) {
if (MATCH_START.test(line)) started = true;
if (MATCH_END.test(line)) ended = true;
}
return { ended, started };
}