Move fleet retention and concurrency controls into Settings

Persist settings in SQLite, import legacy environment values once, and apply changes to cleanup and deployment queues without restarting.

Closes #51
This commit is contained in:
2026-09-27 16:11:45 -04:00
parent fc46cb9da4
commit 4fe8613678
20 changed files with 334 additions and 54 deletions
+12
View File
@@ -0,0 +1,12 @@
import { json } from '@sveltejs/kit';
import { validateFleetSettings } from '$lib/fleet-settings.js';
import { fleetSettings, saveFleetSettings } from '$lib/server/settings.js';
export function GET() { return json(fleetSettings()); }
export async function PUT({ request }: { request: Request }) {
let settings;
try { settings = validateFleetSettings(await request.json()); }
catch (error) { return json({ error: (error as Error).message }, { status: 400 }); }
return json(saveFleetSettings(settings));
}
+62 -2
View File
@@ -3,14 +3,71 @@
import { availableTimeZones, DEFAULT_TIME_ZONE } from '$lib/time-zone.js';
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';
let exampleDate = $state<Date | null>(null);
let settings = $state<FleetSettings | null>(null);
let loading = $state(true);
let saving = $state(false);
let error = $state('');
let notice = $state('');
const zones = $derived([...new Set([...availableTimeZones(), $timeZone])].sort());
onMount(() => { exampleDate = new Date(); });
onMount(() => { exampleDate = new Date(); void loadSettings(); });
async function loadSettings() {
loading = true; error = '';
try {
const response = await fetch('/api/settings');
if (!response.ok) throw new Error('Could not load fleet settings.');
settings = await response.json();
} catch (cause) { error = (cause as Error).message; }
finally { loading = false; }
}
async function saveSettings(event: SubmitEvent) {
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 result = await response.json();
if (!response.ok) throw new Error(result.error ?? 'Could not save fleet settings.');
settings = result;
notice = 'Fleet settings saved. Changes apply to the next queue or cleanup check.';
} catch (cause) { error = (cause as Error).message; }
finally { saving = false; }
}
</script>
<svelte:head><title>Settings · Electric Cherry</title></svelte:head>
<section class="page">
<SectionHeader title="Settings" subtitle="Choose how dates and times appear across Electric Cherry." />
<SectionHeader title="Settings" subtitle="Manage fleet limits, history retention, and display preferences." />
<section class="guide" aria-labelledby="fleet-heading">
<h2 id="fleet-heading">Fleet settings</h2>
<p>Saved for everyone using this Electric Cherry instance. Changes apply without restarting the app.</p>
{#if error}<p class="ui-error" role="alert">{error}</p>{/if}
{#if notice}<p role="status">{notice}</p>{/if}
{#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="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>
<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>
{:else}
<Button onclick={loadSettings}>Retry loading</Button>
{/if}
</section>
<section class="guide" aria-labelledby="display-heading">
<h2 id="display-heading">Display preferences</h2>
<label class="timezone">Timezone
@@ -32,4 +89,7 @@
p { font-size: var(--text-sm); color: var(--text-secondary); line-height: 1.65; }
.timezone { display: grid; gap: 8px; margin-top: 16px; font-size: var(--text-sm); max-width: 420px; }
.timezone select { width: 100%; padding: 10px 12px; font: inherit; color: var(--text-primary); background: var(--surface-sunken); border: 1px solid var(--border-control); border-radius: var(--radius-sm); }
fieldset { border: 0; padding: 0; margin: 20px 0 0; min-width: 0; }
fieldset label { display: block; margin: 20px 0 8px; font-size: var(--text-sm); }
fieldset input { width: 160px; padding: 10px 12px; font: inherit; color: var(--text-primary); background: var(--surface-sunken); border: 1px solid var(--border-control); border-radius: var(--radius-sm); }
</style>