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
@@ -0,0 +1,41 @@
import { json } from '@sveltejs/kit';
import { getScript, setScope, ScriptError, type ScopeType } from '$lib/server/fleet/scripts.js';
import { unsetScopeAndCleanup } from '$lib/server/fleet/beta.js';
function parseId(raw: string): number | null {
const n = Number.parseInt(raw, 10);
return Number.isFinite(n) ? n : null;
}
// POST body: { scopeType: 'game' | 'map' | null, scopeValue?: string }
// Setting/changing scope on a script with a current_version_id returns a
// 409 with kind='scope-requires-beta'; unsetting cleans the file off matching
// servers.
export async function POST({ params, request }: { params: { id: string }; request: Request }) {
const scriptId = parseId(params.id);
if (scriptId === null) return json({ ok: false, error: 'bad id' }, { status: 400 });
const script = getScript(scriptId);
if (!script) return json({ ok: false, error: 'not found' }, { status: 404 });
const body = (await request.json().catch(() => ({}))) as {
scopeType?: ScopeType | null;
scopeValue?: string | null;
};
const scopeType = body.scopeType ?? null;
const scopeValue = body.scopeValue ?? null;
try {
if (scopeType === null) {
const cleaned = await unsetScopeAndCleanup(scriptId);
return json({ ok: true, cleaned });
}
const updated = setScope({ scriptId, scopeType, scopeValue });
return json({ ok: true, script: updated });
} catch (err) {
if (err instanceof ScriptError) {
return json({ ok: false, error: err.message, kind: err.kind }, { status: 409 });
}
throw err;
}
}