84 lines
5.2 KiB
Svelte
84 lines
5.2 KiB
Svelte
<script lang="ts">
|
|
import { onMount } from 'svelte';
|
|
import SectionHeader from '$ui/components/primitives/SectionHeader.svelte';
|
|
import RetentionTools from '$ui/components/RetentionTools.svelte';
|
|
import Button from '$ui/components/primitives/Button.svelte';
|
|
import { defaultFleetSettings, fleetSettingLabels, additionalConcurrencyKeys, concurrencyKeys, retentionKeys, settingHelp, settingMinimum, type FleetSettings } from '$lib/fleet-settings.js';
|
|
let settings = $state<FleetSettings | null>(null);
|
|
let loading = $state(true);
|
|
let saving = $state(false);
|
|
let error = $state('');
|
|
let notice = $state('');
|
|
onMount(() => { 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, section: string, keys: (keyof FleetSettings)[]) {
|
|
event.preventDefault();
|
|
saving = true; error = ''; notice = '';
|
|
try {
|
|
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.');
|
|
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; }
|
|
}
|
|
</script>
|
|
|
|
<svelte:head><title>Settings · Electric Cherry</title></svelte:head>
|
|
<section class="page">
|
|
<SectionHeader title="Settings" subtitle="Manage shared fleet limits and history retention." />
|
|
<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}
|
|
{#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={settingMinimum(key)}
|
|
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.' : key === 'versionKeepDays' ? '0 disables age expiry; the version count policy may still apply.' : key.endsWith('Days') && key !== 'gameFileGraceDays' ? '0 keeps indefinitely.' : ''}</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>
|
|
{#if section.title === 'Retention'}<RetentionTools {settings} />{/if}
|
|
</section>
|
|
{/each}
|
|
{:else}
|
|
<Button onclick={loadSettings}>Retry loading</Button>
|
|
{/if}
|
|
</section>
|
|
|
|
</section>
|
|
|
|
<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; }
|
|
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>
|