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:
@@ -64,6 +64,8 @@
|
||||
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;
|
||||
@@ -74,7 +76,15 @@
|
||||
health: Health;
|
||||
endpoint: RconEndpoint | null;
|
||||
replySnippet: string | null;
|
||||
held: Array<{ target: string; slug: string }>;
|
||||
held: Array<{ target: string; betaId: number }>;
|
||||
}
|
||||
interface InventoryEntry {
|
||||
relPath: string;
|
||||
scriptVersionId: number | null;
|
||||
sha256: string;
|
||||
size: number;
|
||||
updatedAt: string;
|
||||
source: string | null;
|
||||
}
|
||||
interface Integrations {
|
||||
rcon: boolean;
|
||||
@@ -93,6 +103,7 @@
|
||||
let server = $state<Server | null>(null);
|
||||
let integrations = $state<Integrations | null>(null);
|
||||
let history = $state<DeploymentRow[]>([]);
|
||||
let inventory = $state<InventoryEntry[]>([]);
|
||||
let name = $derived($page.params.name ?? '');
|
||||
let komodoProbe = $state<KomodoProbeResult | null>(null);
|
||||
let probing = $state(false);
|
||||
@@ -102,12 +113,20 @@
|
||||
let probingRcon = $state(false);
|
||||
|
||||
onMount(async () => {
|
||||
const [srvRes, depRes] = await Promise.all([fetch('/api/servers'), fetch('/api/deployments')]);
|
||||
const [srvRes, depRes, invRes] = await Promise.all([
|
||||
fetch('/api/servers'),
|
||||
fetch('/api/deployments'),
|
||||
fetch(`/api/servers/${encodeURIComponent(name)}/scripts`)
|
||||
]);
|
||||
const srvBody = (await srvRes.json()) as { servers: Server[]; integrations: Integrations };
|
||||
const depBody = (await depRes.json()) as { deployments: DeploymentRow[] };
|
||||
server = srvBody.servers.find((s) => s.name === name) ?? null;
|
||||
integrations = srvBody.integrations;
|
||||
history = depBody.deployments.filter((d) => d.servers.some((s) => s.server === name));
|
||||
if (invRes.ok) {
|
||||
const invBody = (await invRes.json()) as { scripts: InventoryEntry[] };
|
||||
inventory = invBody.scripts;
|
||||
}
|
||||
});
|
||||
|
||||
let live = $derived($statusByServer.get(name));
|
||||
@@ -420,15 +439,41 @@
|
||||
<div class="k">game_port</div>
|
||||
<div class="v mono">{server.game_port ?? '(unresolved)'}</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="k">game</div>
|
||||
<div class="v mono">{server.game ?? '(unset)'}</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="k">map</div>
|
||||
<div class="v mono">{server.map ?? '(unset)'}</div>
|
||||
</div>
|
||||
{#if server.held.length > 0}
|
||||
<div class="stat">
|
||||
<div class="k">held by beta</div>
|
||||
<div class="v mono">{server.held.map((h) => h.target).join(', ')}</div>
|
||||
<div class="v mono">{server.held.map((h) => `${h.target} (beta ${h.betaId})`).join(', ')}</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<h2>Scripts on this server ({inventory.filter((f) => !f.relPath.endsWith('.OLD') && !/-beta\./.test(f.relPath)).length})</h2>
|
||||
{#if inventory.length === 0}
|
||||
<p class="empty">no scripts tracked — upload one from the Scripts page and open a beta to deploy it here</p>
|
||||
{:else}
|
||||
<div class="inv-list">
|
||||
{#each inventory as f (f.relPath)}
|
||||
<div class="inv-row inv-{f.relPath.endsWith('.OLD') ? 'old' : /-beta\./.test(f.relPath) ? 'beta' : 'live'}">
|
||||
<span class="rel mono">{f.relPath}</span>
|
||||
<span class="size mono">{f.size}B</span>
|
||||
<span class="src mono">{f.source ?? '—'}</span>
|
||||
<span class="sha mono">{f.sha256.slice(0, 12)}</span>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<h2>Deployment history</h2>
|
||||
{#if history.length === 0}
|
||||
@@ -843,4 +888,22 @@
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--text-base);
|
||||
}
|
||||
|
||||
.inv-list { display: flex; flex-direction: column; gap: 4px; }
|
||||
.inv-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 90px 160px 140px;
|
||||
gap: 14px;
|
||||
padding: 8px 12px;
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--r-sm);
|
||||
font-size: var(--text-mono-sm);
|
||||
align-items: center;
|
||||
}
|
||||
.inv-row.inv-old { opacity: 0.55; border-left: 3px solid var(--ink-500); }
|
||||
.inv-row.inv-beta { border-left: 3px solid var(--amber-500); }
|
||||
.inv-row.inv-live { border-left: 3px solid var(--spark-500); }
|
||||
.inv-row .rel { color: var(--cream); word-break: break-all; }
|
||||
.inv-row .size, .inv-row .src, .inv-row .sha { color: var(--ink-500); }
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user