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
+156 -32
View File
@@ -13,64 +13,188 @@
let order = $state(untrack(() => stableHostOrder([], data.hosts, data.serverNow)));
let clockOffset = untrack(() => data.serverNow - Date.now());
let now = $state(untrack(() => data.serverNow));
let error = $state(''); let addOpen = $state(false);
let search = $state(''); let status = $state('');
let error = $state('');
let addOpen = $state(false);
let search = $state('');
let status = $state('');
let loadSequence = 0;
const canManage = $derived($page.data.user?.role === 'admin');
$effect(() => { if (canManage && $page.url.searchParams.get('action') === 'add') addOpen = true; });
const attention = $derived(hosts.filter(host => ['attention', 'offline'].includes(hostListStatus(host, now).key)).length);
const ready = $derived(hosts.filter(host => hostListStatus(host, now).key === 'ready').length);
$effect(() => {
if (canManage && $page.url.searchParams.get('action') === 'add') addOpen = true;
});
const attention = $derived(
hosts.filter((host) => ['attention', 'offline'].includes(hostListStatus(host, now).key))
.length
);
const ready = $derived(
hosts.filter((host) => hostListStatus(host, now).key === 'ready').length
);
const filtersVisible = $derived(hosts.length > 5 || !!search || !!status);
const visible = $derived.by(() => {
const byId = new Map(hosts.map(host => [host.id, host]));
return order.flatMap(id => {
const byId = new Map(hosts.map((host) => [host.id, host]));
return order.flatMap((id) => {
const host = byId.get(id);
if (!host || !`${host.name} ${host.hostname}`.toLowerCase().includes(search.trim().toLowerCase())) return [];
if (
!host ||
!`${host.name} ${host.hostname}`.toLowerCase().includes(search.trim().toLowerCase())
)
return [];
const state = hostListStatus(host, now).key;
if (status && (status === 'attention' ? !['attention', 'offline'].includes(state) : state !== status)) return [];
if (
status &&
(status === 'attention'
? !['attention', 'offline'].includes(state)
: state !== status)
)
return [];
return [host];
});
});
async function load(signal?: AbortSignal) {
const sequence = ++loadSequence;
try {
const response = await fetch('/api/hosts', { signal: signal ?? AbortSignal.timeout(15000) });
const body = await response.json(); if (!response.ok) throw new Error(body.error ?? 'Could not load hosts.');
if (sequence === loadSequence) { hosts = body.hosts; clockOffset = body.serverNow - Date.now(); now = body.serverNow; order = stableHostOrder(order, hosts, now); error = ''; }
} catch (cause) { if (!signal?.aborted && sequence === loadSequence) error = (cause as Error).message; return false; }
const response = await fetch('/api/hosts', {
signal: signal ?? AbortSignal.timeout(15000)
});
const body = await response.json();
if (!response.ok) throw new Error(body.error ?? 'Could not load hosts.');
if (sequence === loadSequence) {
hosts = body.hosts;
clockOffset = body.serverNow - Date.now();
now = body.serverNow;
order = stableHostOrder(order, hosts, now);
error = '';
}
} catch (cause) {
if (!signal?.aborted && sequence === loadSequence) error = (cause as Error).message;
return false;
}
}
onMount(() => {
const stop = pollVisible(load, 30000, false);
const timer = setInterval(() => now = Date.now() + clockOffset, 15000);
return () => { stop(); clearInterval(timer); };
const timer = setInterval(() => (now = Date.now() + clockOffset), 15000);
return () => {
stop();
clearInterval(timer);
};
});
</script>
<svelte:head><title>Hosts · Electric Cherry</title></svelte:head>
<section class="page">
<SectionHeader title="Hosts">
{#snippet actions()}<Button href="/capacity?rebalance=1">Rebalance</Button>{#if canManage}<Button variant="primary" onclick={() => addOpen = true}>Add host</Button>{/if}{/snippet}
{#snippet actions()}<Button href="/capacity?rebalance=1">Rebalance</Button
>{#if canManage}<Button variant="primary" onclick={() => (addOpen = true)}
>Add host</Button
>{/if}{/snippet}
</SectionHeader>
{#if error}<p role="alert" class="ui-error">{error} Showing the last loaded hosts. <Button variant="ghost" size="sm" onclick={() => load()}>Retry</Button></p>{/if}
{#if error}<p role="alert" class="ui-error">
{error} Showing the last loaded hosts. <Button
variant="ghost"
size="sm"
onclick={() => load()}>Retry</Button
>
</p>{/if}
{#if hosts.length}
<div class="summary"><span>{countLabel(hosts.length, 'host')} · {ready === hosts.length ? 'All ready' : `${ready} ready`}</span>{#if attention}<button onclick={() => { status = 'attention'; search = ''; }}>{attention} {plural(attention, 'needs', 'need')} attention</button>{/if}</div>
<div class="summary">
<span
>{countLabel(hosts.length, 'host')} · {ready === hosts.length
? 'All ready'
: `${ready} ready`}</span
>{#if attention}<button
onclick={() => {
status = 'attention';
search = '';
}}>{attention} {plural(attention, 'needs', 'need')} attention</button
>{/if}
</div>
{#if filtersVisible}<div class="filters">
<input type="search" aria-label="Search hosts" placeholder="Search hosts…" bind:value={search} />
<select aria-label="Filter hosts by status" bind:value={status}><option value="">All statuses</option><option value="attention">Needs attention</option><option value="offline">Offline</option><option value="preparing">Preparing</option><option value="draining">Draining</option><option value="disabled">Disabled</option><option value="deleting">Deleting</option><option value="ready">Ready</option></select>
{#if search || status}<Button variant="ghost" size="sm" onclick={() => { search = ''; status = ''; }}>Clear filters</Button>{/if}
</div>{/if}
{#if visible.length}<HostList hosts={visible} {now} returnTo={$page.url.pathname + $page.url.search} />{:else}<p class="empty">No hosts match these filters.</p>{/if}
{:else}<p class="empty">No hosts connected yet.{canManage ? ' Add a host to begin.' : ''}</p>{/if}
<input
type="search"
aria-label="Search hosts"
placeholder="Search hosts…"
bind:value={search}
/>
<select aria-label="Filter hosts by status" bind:value={status}
><option value="">All statuses</option><option value="attention"
>Needs attention</option
><option value="offline">Offline</option><option value="preparing"
>Preparing</option
><option value="draining">Draining</option><option value="disabled"
>Disabled</option
><option value="deleting">Deleting</option><option value="ready">Ready</option
></select
>
{#if search || status}<Button
variant="ghost"
size="sm"
onclick={() => {
search = '';
status = '';
}}>Clear filters</Button
>{/if}
</div>{/if}
{#if visible.length}<HostList
hosts={visible}
{now}
returnTo={$page.url.pathname + $page.url.search}
/>{:else}<p class="empty">No hosts match these filters.</p>{/if}
{:else}<p class="empty">
No hosts connected yet.{canManage ? ' Add a host to begin.' : ''}
</p>{/if}
</section>
{#if canManage}<AddHostDrawer bind:open={addOpen} onadded={load} />{/if}
<style>
.page { display:grid; gap:20px; }
.summary,.filters { display:flex; align-items:center; flex-wrap:wrap; gap:12px; }
.summary,.empty { color:var(--text-secondary); font-size:var(--text-sm); }
.summary button { background:none; border:0; color:var(--state-danger); cursor:pointer; font:inherit; padding:4px; }
input,select { box-sizing:border-box; padding:10px 12px; color:var(--text-primary); background:var(--surface-sunken); border:1px solid var(--border-control); border-radius:var(--radius-sm); font:var(--text-sm) var(--font-body); }
input { flex:1 1 240px; min-width:0; max-width:440px; }
input:focus-visible,select:focus-visible,button:focus-visible { outline:2px solid var(--accent-primary); outline-offset:3px; }
.empty { padding:24px; text-align:center; border:1px solid var(--border-subtle); border-radius:var(--radius-md); }
.page {
display: grid;
gap: 20px;
}
.summary,
.filters {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 12px;
}
.summary,
.empty {
color: var(--text-secondary);
font-size: var(--text-sm);
}
.summary button {
background: none;
border: 0;
color: var(--state-danger);
cursor: pointer;
font: inherit;
padding: 4px;
}
input,
select {
box-sizing: border-box;
padding: 10px 12px;
color: var(--text-primary);
background: var(--surface-sunken);
border: 1px solid var(--border-control);
border-radius: var(--radius-sm);
font: var(--text-sm) var(--font-body);
}
input {
flex: 1 1 240px;
min-width: 0;
max-width: 440px;
}
input:focus-visible,
select:focus-visible,
button:focus-visible {
outline: 2px solid var(--accent-primary);
outline-offset: 3px;
}
.empty {
padding: 24px;
text-align: center;
border: 1px solid var(--border-subtle);
border-radius: var(--radius-md);
}
</style>