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:
@@ -4,8 +4,8 @@ import { config, destOf } from '../config.js';
|
||||
import { db } from '../db/index.js';
|
||||
|
||||
/**
|
||||
* All server folders under SERVERS_ROOT. Mirrors `all_servers` in
|
||||
* push_scripts.sh:430.
|
||||
* All server folders under SERVERS_ROOT. Used by seedServerRegistry to backfill
|
||||
* the servers table for any on-disk server directory that isn't yet tracked.
|
||||
*/
|
||||
export async function allServers(): Promise<string[]> {
|
||||
let entries;
|
||||
@@ -22,8 +22,7 @@ export async function allServers(): Promise<string[]> {
|
||||
|
||||
/**
|
||||
* Match a server name against a list of glob-like patterns (supports `*` and
|
||||
* `?` and comma-separated lists). Ports `match_servers` in
|
||||
* push_scripts.sh:438.
|
||||
* `?` and comma-separated lists).
|
||||
*/
|
||||
export function matchServers(all: string[], patterns: string[]): string[] {
|
||||
if (patterns.length === 0 || (patterns.length === 1 && patterns[0] === '*')) {
|
||||
@@ -36,10 +35,7 @@ export function matchServers(all: string[], patterns: string[]): string[] {
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure a server has an sp/ destination. Mirrors `ensure_dest` in
|
||||
* push_scripts.sh:466. Returns `{ ok: true }` when the dir exists (or was
|
||||
* just created with `create: true`), or `{ ok: false, reason }` when it can't
|
||||
* be used.
|
||||
* Ensure a server has an sp/ destination. Creates it when create=true.
|
||||
*/
|
||||
export async function ensureDest(
|
||||
server: string,
|
||||
@@ -60,7 +56,7 @@ export async function ensureDest(
|
||||
return { ok: true, path: dest };
|
||||
}
|
||||
|
||||
// -- server registry (was .servers map in script_metadata.json) --
|
||||
// -- server registry --
|
||||
|
||||
export interface ServerRow {
|
||||
name: string;
|
||||
@@ -69,22 +65,19 @@ export interface ServerRow {
|
||||
komodo_server: string | null;
|
||||
game_port: number | null;
|
||||
rcon_host: string | null;
|
||||
game: string | null;
|
||||
map: string | null;
|
||||
last_players: number | null;
|
||||
last_checked_at: string | null;
|
||||
last_map: string | null;
|
||||
last_gametype: string | null;
|
||||
last_roster: string | null; // JSON array of RconPlayer, or null
|
||||
last_seen_at: string | null; // last successful RCON reply
|
||||
last_error: string | null; // cleared on success
|
||||
last_reply_snippet: string | null; // hex+ASCII of last bad-reply payload
|
||||
last_roster: string | null;
|
||||
last_seen_at: string | null;
|
||||
last_error: string | null;
|
||||
last_reply_snippet: string | null;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Seed the servers table with rows for any folder under SERVERS_ROOT that
|
||||
* isn't yet tracked. Uses folder-name-with-dashes as the default komodo_name,
|
||||
* mirroring `server_reg_seed` in push_scripts.sh:564.
|
||||
*/
|
||||
export async function seedServerRegistry(): Promise<number> {
|
||||
const rows = await allServers();
|
||||
const insert = db().prepare(
|
||||
@@ -114,13 +107,14 @@ export function setServerField(
|
||||
field: keyof ServerRow,
|
||||
value: string | number | null
|
||||
): void {
|
||||
// whitelist columns — never interpolate arbitrary strings into SQL
|
||||
const allowed: Array<keyof ServerRow> = [
|
||||
'komodo_type',
|
||||
'komodo_name',
|
||||
'komodo_server',
|
||||
'game_port',
|
||||
'rcon_host',
|
||||
'game',
|
||||
'map',
|
||||
'last_players',
|
||||
'last_checked_at',
|
||||
'last_map',
|
||||
@@ -135,37 +129,38 @@ export function setServerField(
|
||||
}
|
||||
|
||||
/**
|
||||
* Which relative-path targets are currently held by an open beta on this
|
||||
* server? Returns `Map<relPath, betaSlug>`. Mirrors `held_by_beta` in
|
||||
* push_scripts.sh:378.
|
||||
* Which target rel_paths on this server are currently held by a running beta?
|
||||
* Returns Map<relPath, betaId>. A target held by beta X blocks other betas AND
|
||||
* blocks unrelated writes to that same target on that server.
|
||||
*/
|
||||
export function heldByBeta(server: string): Map<string, string> {
|
||||
export function heldByBeta(server: string): Map<string, number> {
|
||||
const rows = db()
|
||||
.prepare(
|
||||
`SELECT bs.target AS target, bs.beta_slug AS slug
|
||||
FROM beta_deploys bd
|
||||
JOIN beta_scripts bs ON bs.id = bd.beta_script_id
|
||||
WHERE bd.server = ?`
|
||||
`SELECT s.target_rel_path AS target, b.id AS beta_id
|
||||
FROM beta_servers bs
|
||||
JOIN betas b ON b.id = bs.beta_id
|
||||
JOIN scripts s ON s.id = b.script_id
|
||||
WHERE bs.server = ? AND b.state = 'running'`
|
||||
)
|
||||
.all(server) as Array<{ target: string; slug: string }>;
|
||||
const map = new Map<string, string>();
|
||||
for (const r of rows) map.set(r.target, r.slug);
|
||||
.all(server) as Array<{ target: string; beta_id: number }>;
|
||||
const map = new Map<string, number>();
|
||||
for (const r of rows) map.set(r.target, r.beta_id);
|
||||
return map;
|
||||
}
|
||||
|
||||
/**
|
||||
* Which beta (if any) owns this target on this server?
|
||||
* Mirrors `owner_of` in push_scripts.sh:366.
|
||||
* Which running beta (if any) owns a specific target rel_path on a server?
|
||||
*/
|
||||
export function ownerOf(server: string, target: string): string | null {
|
||||
export function ownerOf(server: string, target: string): number | null {
|
||||
const row = db()
|
||||
.prepare(
|
||||
`SELECT bs.beta_slug AS slug
|
||||
FROM beta_deploys bd
|
||||
JOIN beta_scripts bs ON bs.id = bd.beta_script_id
|
||||
WHERE bd.server = ? AND bs.target = ?
|
||||
`SELECT b.id AS beta_id
|
||||
FROM beta_servers bs
|
||||
JOIN betas b ON b.id = bs.beta_id
|
||||
JOIN scripts s ON s.id = b.script_id
|
||||
WHERE bs.server = ? AND s.target_rel_path = ? AND b.state = 'running'
|
||||
LIMIT 1`
|
||||
)
|
||||
.get(server, target) as { slug: string } | undefined;
|
||||
return row?.slug ?? null;
|
||||
.get(server, target) as { beta_id: number } | undefined;
|
||||
return row?.beta_id ?? null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user