Add Prettier and ESLint tooling (Refs #77)

This commit is contained in:
2026-10-09 15:56:13 -04:00
parent 8ed059b8d3
commit 16469e2d43
711 changed files with 99737 additions and 28286 deletions
+172 -44
View File
@@ -10,8 +10,21 @@
import Dialog from './primitives/Dialog.svelte';
import Button from './primitives/Button.svelte';
let { open = $bindable(false), initialServer = '', initialAction = 'start', serverContext = null, players = null, round = null }:
{ open?: boolean; initialServer?: string; initialAction?: RuntimeAction; serverContext?: FleetServer | null; players?: number | null; round?: number | null } = $props();
let {
open = $bindable(false),
initialServer = '',
initialAction = 'start',
serverContext = null,
players = null,
round = null
}: {
open?: boolean;
initialServer?: string;
initialAction?: RuntimeAction;
serverContext?: FleetServer | null;
players?: number | null;
round?: number | null;
} = $props();
let servers = $state<FleetServer[]>([]);
let server = $state(untrack(() => initialServer));
let action = $state<RuntimeAction | 'destroy'>(untrack(() => initialAction));
@@ -19,15 +32,35 @@
let loading = $state(false);
let error = $state('');
let confirmed = $state(false);
const selected = $derived(serverContext?.name === server ? serverContext : servers.find(item => item.name === server));
const selected = $derived(
serverContext?.name === server
? serverContext
: servers.find((item) => item.name === server)
);
const available = $derived(selected ? applicableRuntimeActions(selected, $uiNow) : []);
const applicable = $derived(action === 'destroy' || available.includes(action));
const destructive = $derived(action === 'rebuild' || action === 'destroy');
const health = $derived(selected ? serverHealth(selected, $uiNow) : null);
const impactPlayers = $derived(initialServer ? players : selected && health?.state === 'online' && freshServer(selected, $uiNow) ? selected.last_players : null);
const impactRound = $derived(initialServer ? round : selected ? currentRound(selected, selected.telemetry, $uiNow) : null);
const title = $derived(initialServer ? `${actionLabels[action]} ${serverTitle(initialServer)}?` : 'Manage server');
const submitLabel = $derived(action === 'start' ? 'Start server' : action === 'destroy' ? 'Destroy server when empty' : `${actionLabels[action]} when empty`);
const impactPlayers = $derived(
initialServer
? players
: selected && health?.state === 'online' && freshServer(selected, $uiNow)
? selected.last_players
: null
);
const impactRound = $derived(
initialServer ? round : selected ? currentRound(selected, selected.telemetry, $uiNow) : null
);
const title = $derived(
initialServer ? `${actionLabels[action]} ${serverTitle(initialServer)}?` : 'Manage server'
);
const submitLabel = $derived(
action === 'start'
? 'Start server'
: action === 'destroy'
? 'Destroy server when empty'
: `${actionLabels[action]} when empty`
);
$effect(() => {
if (!open) return;
const fixedServer = initialServer;
@@ -36,86 +69,181 @@
// Live context and match reports update the copy and eligibility without
// reopening the dialog or clearing a deliberate confirmation.
untrack(() => {
server = fixedServer; action = requestedAction; confirmed = false; error = ''; servers = [];
server = fixedServer;
action = requestedAction;
confirmed = false;
error = '';
servers = [];
loading = false;
if (!fixedServer || serverContext?.name !== fixedServer) void load(fixedServer, controller.signal);
if (!fixedServer || serverContext?.name !== fixedServer)
void load(fixedServer, controller.signal);
});
return () => controller.abort();
});
async function load(fixedServer: string, signal: AbortSignal) {
loading = true;
try {
const response = await fetch(fixedServer ? `/api/servers/${encodeURIComponent(fixedServer)}` : '/api/servers', { signal });
const response = await fetch(
fixedServer ? `/api/servers/${encodeURIComponent(fixedServer)}` : '/api/servers',
{ signal }
);
if (!response.ok) throw new Error('Could not load servers.');
const body = await response.json();
if (signal.aborted) return;
servers = body.servers;
syncServerTime(body.serverNow);
} catch (err) { if (!signal.aborted) error = (err as Error).message; }
finally { if (!signal.aborted) loading = false; }
} catch (err) {
if (!signal.aborted) error = (err as Error).message;
} finally {
if (!signal.aborted) loading = false;
}
}
function chooseServer(event: Event & { currentTarget: EventTarget & HTMLSelectElement }) {
server = event.currentTarget.value;
confirmed = false; error = '';
const item = servers.find(item => item.name === server);
confirmed = false;
error = '';
const item = servers.find((item) => item.name === server);
const actions = item ? applicableRuntimeActions(item, serverTimeNow()) : [];
action = actions[0] ?? 'destroy';
}
async function submit() {
if (busy || !server || (destructive && !confirmed)) return;
if (action !== 'destroy' && (!selected || !applicableRuntimeActions(selected, serverTimeNow()).includes(action))) {
error = 'The server status has changed. Close this dialog and choose an action from its current status.';
if (
action !== 'destroy' &&
(!selected || !applicableRuntimeActions(selected, serverTimeNow()).includes(action))
) {
error =
'The server status has changed. Close this dialog and choose an action from its current status.';
return;
}
busy = true; error = '';
busy = true;
error = '';
try {
const response = action === 'destroy'
? await fetch(`/api/servers/${encodeURIComponent(server)}`, { method: 'DELETE' })
: await fetch('/api/provisioning', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ server, action }) });
const response =
action === 'destroy'
? await fetch(`/api/servers/${encodeURIComponent(server)}`, {
method: 'DELETE'
})
: await fetch('/api/provisioning', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ server, action })
});
const body = await response.json();
if (!response.ok || !body.deploymentId) throw new Error(body.error ?? 'Could not queue this operation.');
if (!response.ok || !body.deploymentId)
throw new Error(body.error ?? 'Could not queue this operation.');
await goto(`/provisioning/${encodeURIComponent(body.deploymentId)}`);
open = false;
} catch (err) { error = (err as Error).message; }
finally { busy = false; }
} catch (err) {
error = (err as Error).message;
} finally {
busy = false;
}
}
</script>
<Dialog bind:open {title} {busy}>
<div class="fields">
{#if !initialServer}
<label>Server <select bind:value={server} disabled={busy || loading} onchange={chooseServer}>
<option value="">{loading ? 'Loading servers…' : 'Choose a server'}</option>
{#each servers as item (item.name)}<option value={item.name}>{serverTitle(item.name)}</option>{/each}
</select></label>
<label>Action <select bind:value={action} disabled={busy || !server || loading} onchange={() => { confirmed = false; error = ''; }}>
{#if !server}<option value="start">Choose a server first</option>{/if}
{#if server && !applicable}<option value={action} disabled>{actionLabels[action]} unavailable</option>{/if}
{#each available as value}<option value={value}>{actionLabels[value]}</option>{/each}
{#if server}<option value="destroy">Destroy server</option>{/if}
</select></label>
<label
>Server <select
bind:value={server}
disabled={busy || loading}
onchange={chooseServer}
>
<option value="">{loading ? 'Loading servers…' : 'Choose a server'}</option>
{#each servers as item (item.name)}<option value={item.name}
>{serverTitle(item.name)}</option
>{/each}
</select></label
>
<label
>Action <select
bind:value={action}
disabled={busy || !server || loading}
onchange={() => {
confirmed = false;
error = '';
}}
>
{#if !server}<option value="start">Choose a server first</option>{/if}
{#if server && !applicable}<option value={action} disabled
>{actionLabels[action]} unavailable</option
>{/if}
{#each available as value}<option {value}>{actionLabels[value]}</option>{/each}
{#if server}<option value="destroy">Destroy server</option>{/if}
</select></label
>
{/if}
{#if loading}<p role="status">Checking the latest server status…</p>
{:else if server}
{#if applicable}
<p class="impact">{lifecycleImpact(action, impactPlayers, impactRound)}</p>
<p>{action === 'destroy' ? 'Remove the container, its writable server files, and registration after the server is empty. Shared game libraries, recorded history, and the key saved separately in Keys are retained.' : runtimeDescriptions[action]}</p>
{#if action !== 'start'}<p class="help">Follow progress or cancel the waiting request on the provisioning page.</p>{/if}
{:else}<p role="status">{actionLabels[action]} is unavailable. {health?.reason ?? 'The current server status could not be verified.'} Close this dialog and choose an action from its current status.</p>{/if}
{#if applicable && destructive}<label class="confirmation"><input type="checkbox" bind:checked={confirmed} disabled={busy} /> I understand that this instance’s writable files and data will be permanently deleted.</label>{/if}
<p>
{action === 'destroy'
? 'Remove the container, its writable server files, and registration after the server is empty. Shared game libraries, recorded history, and the key saved separately in Keys are retained.'
: runtimeDescriptions[action]}
</p>
{#if action !== 'start'}<p class="help">
Follow progress or cancel the waiting request on the provisioning page.
</p>{/if}
{:else}<p role="status">
{actionLabels[action]} is unavailable. {health?.reason ??
'The current server status could not be verified.'} Close this dialog and choose
an action from its current status.
</p>{/if}
{#if applicable && destructive}<label class="confirmation"
><input type="checkbox" bind:checked={confirmed} disabled={busy} /> I understand that
this instance’s writable files and data will be permanently deleted.</label
>{/if}
{/if}
{#if error}<p class="ui-error" role="alert">{error}</p>{/if}
</div>
{#snippet actions()}
<Button disabled={busy} onclick={() => open = false}>Cancel</Button>
<Button variant={destructive ? 'danger' : 'primary'} disabled={busy || loading || !server || !applicable || (destructive && !confirmed)} loading={busy} onclick={submit}>{submitLabel}</Button>
<Button disabled={busy} onclick={() => (open = false)}>Cancel</Button>
<Button
variant={destructive ? 'danger' : 'primary'}
disabled={busy || loading || !server || !applicable || (destructive && !confirmed)}
loading={busy}
onclick={submit}>{submitLabel}</Button
>
{/snippet}
</Dialog>
<style>
.fields { display: grid; gap: 16px; } label { display: grid; gap: 8px; }
select { padding: 10px; border: 1px solid var(--border-strong); border-radius: var(--radius-sm); background: var(--surface); color: var(--text-primary); }
p { margin: 0; line-height: 1.6; color: var(--text-secondary); } .confirmation { display: flex; align-items: flex-start; gap: 12px; line-height: 1.5; }
.impact { padding: var(--space-4); border-radius: var(--radius-md); background: var(--surface-raised); color: var(--text-primary); }
.help { font-size: var(--text-sm); }
.fields {
display: grid;
gap: 16px;
}
label {
display: grid;
gap: 8px;
}
select {
padding: 10px;
border: 1px solid var(--border-strong);
border-radius: var(--radius-sm);
background: var(--surface);
color: var(--text-primary);
}
p {
margin: 0;
line-height: 1.6;
color: var(--text-secondary);
}
.confirmation {
display: flex;
align-items: flex-start;
gap: 12px;
line-height: 1.5;
}
.impact {
padding: var(--space-4);
border-radius: var(--radius-md);
background: var(--surface-raised);
color: var(--text-primary);
}
.help {
font-size: var(--text-sm);
}
</style>