Allow deleting settled autoscaling policies

This commit is contained in:
2026-10-06 23:41:37 -04:00
parent 285279ee6d
commit 34648b34e0
7 changed files with 131 additions and 5 deletions
+6 -1
View File
@@ -1,5 +1,5 @@
import { json } from '@sveltejs/kit';
import { previewScaling, saveScaling, scalingReport, setScalingGlobalPause, ScalingError } from '$lib/server/autoscaling/index.js';
import { previewScaling, saveScaling, deleteScaling, scalingReport, setScalingGlobalPause, ScalingError } from '$lib/server/autoscaling/index.js';
export function GET() { return json(scalingReport()); }
export async function POST({ request }: { request: Request }) {
@@ -9,6 +9,11 @@ export async function POST({ request }: { request: Request }) {
if (body.id !== undefined && typeof body.id !== 'string') throw new ScalingError('Invalid policy identity.');
if (body.action === 'preview') return json(previewScaling(body.settings, body.id, body.revision));
if (body.action === 'save') return json({ policy: saveScaling(body.settings, body.id, body.revision, body.token) });
if (body.action === 'delete') {
if (typeof body.id !== 'string' || !body.id || !Number.isInteger(body.revision) || body.revision < 1) throw new ScalingError('Provide the policy identity and current revision.');
deleteScaling(body.id, body.revision);
return json({ ok: true });
}
if (body.action === 'global-pause' && typeof body.paused === 'boolean') { setScalingGlobalPause(body.paused); return json({ ok: true }); }
throw new ScalingError('Choose a supported policy action.');
} catch (error) { return json({ error: (error as Error).message }, { status: error instanceof ScalingError ? error.status : 400 }); }
+16 -2
View File
@@ -1,6 +1,7 @@
<script lang="ts">
import { onMount } from 'svelte';
import { page } from '$app/state';
import { confirmAction } from '$ui/confirm.js';
import { pollVisible } from '$ui/poll.js';
import { timeFormat } from '$ui/stores/time-zone.js';
import SectionHeader from '$ui/components/primitives/SectionHeader.svelte';
@@ -48,6 +49,19 @@
} catch (error) { actionError = (error as Error).message; preview = null; }
finally { busy = false; }
}
async function remove(policy: ScalingPolicy) {
const scope = policy.mapSlug === '*' ? `every map in ${policy.game.toUpperCase()}` : `${zombieMapBySlug(policy.game, policy.mapSlug)?.friendly ?? policy.mapSlug} · ${policy.game.toUpperCase()}`;
if (!await confirmAction({ title: 'Delete autoscaling policy?', message: `Delete the policy for ${scope} and its scaling history? Existing lobbies, saved keys, and Provisioning history will be kept.`, confirmLabel: 'Delete policy', danger: true })) return;
busy = true; actionError = ''; notice = ''; ++generation;
try {
await post({ action: 'delete', id: policy.id, revision: policy.revision });
if (editing?.id === policy.id) { editing = null; editorOpen = false; preview = null; }
snapshot = { ...report, policies: report.policies.filter(view => view.policy.id !== policy.id) };
notice = 'Policy deleted.';
await refresh();
} catch (error) { actionError = (error as Error).message; }
finally { busy = false; }
}
async function pauseAll() {
busy = true; actionError = ''; ++generation;
try { await post({ action: 'global-pause', paused: !report.globalPaused }); notice = report.globalPaused ? 'Autoscaling resumed.' : 'Autoscaling paused. Review any unsettled operations below.'; await refresh(); }
@@ -102,9 +116,9 @@
{#if !report.policies.length}<p class="panel">No autoscaling policies. Add a policy to observe a map or a whole game in dry run.</p>{/if}
{#each report.policies as view (view.policy.id)}
<section class="panel" id={view.policy.id} aria-labelledby={`title-${view.policy.id}`}>
<div class="heading"><h2 id={`title-${view.policy.id}`}>{view.policy.mapSlug === '*' ? 'Every map' : zombieMapBySlug(view.policy.game, view.policy.mapSlug)?.friendly ?? view.policy.mapSlug} · {view.policy.game.toUpperCase()}</h2><span>{modeLabel(view.policy.mode)}</span>{#if canOperate}<Button disabled={busy} onclick={() => edit(view.policy)}>Edit or pause</Button>{/if}</div>
<div class="heading"><h2 id={`title-${view.policy.id}`}>{view.policy.mapSlug === '*' ? 'Every map' : zombieMapBySlug(view.policy.game, view.policy.mapSlug)?.friendly ?? view.policy.mapSlug} · {view.policy.game.toUpperCase()}</h2><span>{modeLabel(view.policy.mode)}</span>{#if canOperate}<Button disabled={busy} onclick={() => edit(view.policy)}>Edit or pause</Button><Button variant="danger" disabled={busy || view.unsettled.length > 0} title={view.unsettled.length ? 'Pause the policy and settle outstanding work before deleting it.' : undefined} onclick={() => remove(view.policy)}>Delete policy</Button>{/if}</div>
<p class="counts">{view.decision.occupied} occupied · {view.decision.empty} empty and ready · {view.decision.pending} pending · {view.decision.unknown} unknown or stopped</p>
{#if view.unsettled.length}<div class="banner"><strong>Work is still settling.</strong>{#each view.unsettled as operation}<p><a href={`/provisioning/${operation.id}`}>{operation.action} · {operation.state}</a> — {operation.stage}</p>{/each}{#if view.ownsMap}<p>This policy retains ownership of its scope until its outstanding work settles.</p>{/if}</div>{/if}
{#if view.unsettled.length}<div class="banner"><strong>Work is still settling.</strong><p>Pause this policy and settle its outstanding work before deleting it.</p>{#each view.unsettled as operation}<p><a href={`/provisioning/${operation.id}`}>{operation.action} · {operation.state}</a> — {operation.stage}</p>{/each}{#if view.ownsMap}<p>This policy retains ownership of its scope until its outstanding work settles.</p>{/if}</div>{/if}
<p>{view.policy.mapSlug === '*' ? 'Per map: ' : ''}Minimum {view.policy.minimum} · Maximum {view.policy.maximum} · Spare target {view.policy.spares} · {view.snapshot.keyedNames.length} saved keys</p>
<p><strong>{view.policy.mode === 'dry-run' ? 'Hypothetical decision: ' : 'Current decision: '}</strong>{view.decision.action}{view.decision.server ? ` ${view.decision.server}` : ''}. {view.decision.reason}</p>
{#if view.policy.mode === 'create-only' && view.decision.action === 'destroy'}<p>Removal is advisory in Create only mode.</p>{/if}