Promote as a real deployment; version state and scope dropdowns

Turns promotion into a first-class deployment (action='promote') so it lands
in the deployments UI with before/after content, per-server restart tracking,
and one-click rollback. Non-beta matching-scope servers get restart_state=
pending so the existing sweep drains and restarts them the same way drift does.

Also adds a per-version state (untested/tested/test-failed) so the upload
flow can visibly signal that a beta must land before promotion, and swaps
the scope-value text box for game/map dropdowns populated from the fleet.
Beta rollback at ready-promote is now labelled Reject and marks the version
test-failed.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-09-12 14:24:35 -04:00
co-authored by Claude Opus 4.7
parent 3f934fd579
commit 1afdfddc87
16 changed files with 1014 additions and 41 deletions
+30
View File
@@ -13,6 +13,8 @@ export interface ScriptRow {
created_at: string;
}
export type ScriptVersionState = 'untested' | 'tested' | 'test-failed';
export interface ScriptVersionRow {
id: number;
script_id: number;
@@ -21,6 +23,7 @@ export interface ScriptVersionRow {
size: number;
content: Buffer;
notes: string | null;
state: ScriptVersionState;
uploaded_at: string;
}
@@ -233,3 +236,30 @@ export function serversMatchingScope(scopeType: ScopeType | null, scopeValue: st
.all(scopeValue) as Array<{ name: string }>;
return rows.map((r) => r.name);
}
// Distinct game IDs present in the registered fleet. Powers the scope=game
// dropdown so operators can only pick values that will match at least one
// server.
export function listDistinctGames(): string[] {
const rows = db()
.prepare(`SELECT DISTINCT game FROM servers WHERE game IS NOT NULL AND game <> '' ORDER BY game`)
.all() as Array<{ game: string }>;
return rows.map((r) => r.game);
}
// Distinct maps grouped by their game. Scope=map is chained (pick game → pick
// map) so the UI can filter map options to those valid under the selected game.
export function listMapsByGame(): Record<string, string[]> {
const rows = db()
.prepare(
`SELECT DISTINCT game, map FROM servers
WHERE game IS NOT NULL AND game <> '' AND map IS NOT NULL AND map <> ''
ORDER BY game, map`
)
.all() as Array<{ game: string; map: string }>;
const out: Record<string, string[]> = {};
for (const r of rows) {
(out[r.game] ??= []).push(r.map);
}
return out;
}