Add fleet and per-host concurrency controls

This commit is contained in:
2026-10-05 22:27:27 -04:00
parent c3b990c5fd
commit 7c98df4fb0
14 changed files with 268 additions and 47 deletions
+22 -25
View File
@@ -2,7 +2,7 @@
import { onMount } from 'svelte';
import SectionHeader from '$ui/components/primitives/SectionHeader.svelte';
import Button from '$ui/components/primitives/Button.svelte';
import type { FleetSettings } from '$lib/fleet-settings.js';
import { defaultFleetSettings, fleetSettingLabels, additionalConcurrencyKeys, concurrencyKeys, retentionKeys, settingHelp, type FleetSettings } from '$lib/fleet-settings.js';
let settings = $state<FleetSettings | null>(null);
let loading = $state(true);
let saving = $state(false);
@@ -20,14 +20,14 @@
finally { loading = false; }
}
async function saveSettings(event: SubmitEvent) {
async function saveSettings(event: SubmitEvent, section: string, keys: (keyof FleetSettings)[]) {
event.preventDefault();
saving = true; error = ''; notice = '';
try {
const response = await fetch('/api/settings', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(settings) });
const response = await fetch('/api/settings', { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ section, values: Object.fromEntries(keys.map(key => [key, settings![key]])) }) });
const result = await response.json();
if (!response.ok) throw new Error(result.error ?? 'Could not save fleet settings.');
settings = result;
for (const key of keys) settings![key] = result[key];
notice = 'Fleet settings saved. Changes apply to the next queue or cleanup check.';
} catch (cause) { error = (cause as Error).message; }
finally { saving = false; }
@@ -45,27 +45,23 @@
{#if loading}
<p role="status">Loading fleet settings…</p>
{:else if settings}
<form onsubmit={saveSettings}>
<fieldset disabled={saving}>
<label for="retention">Campaign history retention (days)</label>
<input id="retention" type="number" min="1" max="36500" step="1" required bind:value={settings.deployKeepDays} aria-describedby="retention-help" />
<p id="retention-help">Default: 7 days. Removes eligible old campaign records and file diffs on the next cleanup check. Pending, failed, held, and health-recovery-linked campaigns are kept. Provisioning history is retained.</p>
<label for="observation-retention">Routine operation history retention (days)</label>
<input id="observation-retention" type="number" min="0" max="36500" step="1" required bind:value={settings.observationKeepDays} aria-describedby="observation-retention-help" />
<p id="observation-retention-help">Default: 1 day. Removes successful monitoring and file-read operation records on the next cleanup check. Failures and actions are retained. Set to 0 to keep these records indefinitely.</p>
<label for="restarts">Concurrent restarts</label>
<input id="restarts" type="number" min="1" step="1" required bind:value={settings.restartConcurrency} aria-describedby="restarts-help" />
<p id="restarts-help">Default: 1 across the fleet. Campaigns, betas, maintenance, and lifecycle actions share recovery slots until container and game health are verified.</p>
<label for="startups">Concurrent server startups</label>
<input id="startups" type="number" min="1" step="1" required bind:value={settings.createConcurrency} aria-describedby="startups-help" />
<p id="startups-help">Default: 1 across collections and individual launches. Each startup holds its slot through container, game health, and selected-map verification.</p>
<label for="destructions">Concurrent server destructions</label>
<input id="destructions" type="number" min="1" step="1" required bind:value={settings.destroyConcurrency} aria-describedby="destructions-help" />
<p id="destructions-help">Default: 1 across individual and fleet removals. Each destruction holds its slot until teardown and local cleanup finish.</p>
<p>Lowering a limit lets active operations finish; queued work waits for capacity. Player safety checks still apply.</p>
<Button type="submit" disabled={saving}>{saving ? 'Saving…' : 'Save fleet settings'}</Button>
</fieldset>
</form>
{#each [{ title: 'Concurrency', keys: concurrencyKeys }, { title: 'Retention', keys: retentionKeys }] as section}
<section class="settings-section" aria-labelledby={section.title}>
<h2 id={section.title}>{section.title}</h2>
<form onsubmit={(event) => saveSettings(event, section.title, section.keys)}>
<fieldset disabled={saving}>
{#each section.keys as key}
<label for={key}>{fleetSettingLabels[key]}{key.endsWith('Days') ? ' (days)' : ''}</label>
<input id={key} type="number" min={key === 'observationKeepDays' || additionalConcurrencyKeys.includes(key) ? 0 : 1}
max={key.endsWith('Days') ? 36500 : undefined} step="1" required bind:value={settings[key]} aria-describedby={`${key}-help`} />
<p id={`${key}-help`}>Default: {defaultFleetSettings[key]}. {settingHelp[key] ?? ''} {additionalConcurrencyKeys.includes(key) ? '0 adds no extra limit.' : ''}</p>
{/each}
{#if section.title === 'Concurrency'}<p>Lowering a limit lets active operations finish; queued work waits for capacity. Player safety checks still apply.</p>{/if}
<Button type="submit" disabled={saving}>{saving ? 'Saving…' : `Save ${section.title.toLowerCase()}`}</Button>
</fieldset>
</form>
</section>
{/each}
{:else}
<Button onclick={loadSettings}>Retry loading</Button>
{/if}
@@ -76,6 +72,7 @@
<style>
.page { display: grid; gap: 20px; }
.guide { padding: 20px; border: 1px solid var(--border-strong); border-radius: var(--radius-md); background: var(--surface); }
.settings-section { margin-top: 24px; padding-top: 24px; border-top: 1px solid var(--border-strong); }
h2 { font-size: var(--text-md); margin: 0; }
p { font-size: var(--text-sm); color: var(--text-secondary); line-height: 1.65; }
fieldset { border: 0; padding: 0; margin: 20px 0 0; min-width: 0; }