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
@@ -6,8 +6,12 @@
let { hosts, returnTo }: { hosts: CapacityHost[]; returnTo: string } = $props();
let expanded = $state<string[]>([]);
const numbers = new Intl.NumberFormat('en-US', { maximumFractionDigits: 1 });
function count(value: number | null) { return value === null ? '—' : numbers.format(value); }
function memory(value: number | null) { return value === null ? '—' : `${numbers.format(value / 1024 ** 3)} GiB`; }
function count(value: number | null) {
return value === null ? '—' : numbers.format(value);
}
function memory(value: number | null) {
return value === null ? '—' : `${numbers.format(value / 1024 ** 3)} GiB`;
}
function restriction(host: CapacityHost) {
if (!host.connected) return 'Offline';
return host.readiness === 'ready' ? 'Not eligible' : hostReadinessLabel(host.readiness);
@@ -15,50 +19,220 @@
</script>
<!-- svelte-ignore a11y_no_noninteractive_tabindex (Scrollable tables must be reachable by keyboard.) -->
<div class="table-wrap" role="region" aria-label="Host capacity comparison" tabindex="0"><table>
<thead><tr><th>Host</th><th>Additional servers</th><th>Limited by</th><th>CPU available</th><th>Memory available</th><th>Free ports</th><th><span class="sr-only">Details</span></th></tr></thead>
<tbody>{#each hosts as host (host.id)}
<tr>
<td><a class="name" href={contextHref(`/hosts/${host.id}`, returnTo)}>{host.name}</a></td>
<td class="estimate">{count(host.estimatedServers)}</td>
<td class="limitation">{#if host.eligible}<strong>{host.limitingResource ?? 'Measurements pending'}</strong>{:else}<span>{restriction(host)}</span>{#if host.reasons[0]}<a class="reason" href={contextHref(`/hosts/${host.id}?tab=setup`, returnTo)}>{host.reasons[0]}</a>{/if}{/if}</td>
<td>{count(host.cpuCoresAvailable)}{host.cpuCoresAvailable === null ? '' : ' cores'}</td>
<td>{memory(host.memoryBytesAvailable)}</td>
<td>{host.portsAvailable}</td>
<td><button aria-label={`Details for ${host.name}`} aria-expanded={expanded.includes(host.id)} aria-controls={`capacity-host-${host.id}`} onclick={() => expanded = expanded.includes(host.id) ? expanded.filter(id => id !== host.id) : [...expanded, host.id]}>Details</button></td>
</tr>
<tr hidden={!expanded.includes(host.id)} id={`capacity-host-${host.id}`}><td colspan="7" class="detail-cell">
<div class="detail-content">
{#if host.reasons.length}<ul>{#each host.reasons as reason}<li>{reason}</li>{/each}</ul>{/if}
<p>Games ready: {host.games.length ? host.games.map(game => game === 't5' ? 'Black Ops I' : 'Black Ops II').join(', ') : 'None'} · {host.portsAvailable} of {host.totalPorts} ports free.</p>
<p>Server limits: CPU {count(host.limits.cpu)} · Memory {count(host.limits.memory)} · Ports {host.limits.ports}.</p>
<dl>{#each [{ label: 'Running server', value: host.calibration.running }, { label: 'Starting server', value: host.calibration.startup }] as allowance}<div>
<dt>{allowance.label}</dt><dd>{count(allowance.value.cpuCores)} cores · {memory(allowance.value.memoryBytes)}</dd>
<dd class="secondary">{allowance.value.source === 'bootstrap' ? 'Default allowance' : `${allowance.value.source === 'host' ? 'Host' : 'Fleet'} observations · ${allowance.value.samples} samples`}</dd>
</div>{/each}</dl>
<p>Starting servers may need more resources. The controller checks current capacity before reserving a host and port.</p>
<p>{host.updatedAt ? `Reported ${$timeFormat.dateTime(host.updatedAt)}` : 'No host report yet'}</p>
</div>
</td></tr>
{/each}</tbody>
</table></div>
<div class="table-wrap" role="region" aria-label="Host capacity comparison" tabindex="0">
<table>
<thead
><tr
><th>Host</th><th>Additional servers</th><th>Limited by</th><th>CPU available</th
><th>Memory available</th><th>Free ports</th><th
><span class="sr-only">Details</span></th
></tr
></thead
>
<tbody
>{#each hosts as host (host.id)}
<tr>
<td
><a class="name" href={contextHref(`/hosts/${host.id}`, returnTo)}
>{host.name}</a
></td
>
<td class="estimate">{count(host.estimatedServers)}</td>
<td class="limitation"
>{#if host.eligible}<strong
>{host.limitingResource ?? 'Measurements pending'}</strong
>{:else}<span>{restriction(host)}</span>{#if host.reasons[0]}<a
class="reason"
href={contextHref(`/hosts/${host.id}?tab=setup`, returnTo)}
>{host.reasons[0]}</a
>{/if}{/if}</td
>
<td
>{count(host.cpuCoresAvailable)}{host.cpuCoresAvailable === null
? ''
: ' cores'}</td
>
<td>{memory(host.memoryBytesAvailable)}</td>
<td>{host.portsAvailable}</td>
<td
><button
aria-label={`Details for ${host.name}`}
aria-expanded={expanded.includes(host.id)}
aria-controls={`capacity-host-${host.id}`}
onclick={() =>
(expanded = expanded.includes(host.id)
? expanded.filter((id) => id !== host.id)
: [...expanded, host.id])}>Details</button
></td
>
</tr>
<tr hidden={!expanded.includes(host.id)} id={`capacity-host-${host.id}`}
><td colspan="7" class="detail-cell">
<div class="detail-content">
{#if host.reasons.length}<ul>
{#each host.reasons as reason}<li>{reason}</li>{/each}
</ul>{/if}
<p>
Games ready: {host.games.length
? host.games
.map((game) =>
game === 't5' ? 'Black Ops I' : 'Black Ops II'
)
.join(', ')
: 'None'} · {host.portsAvailable} of {host.totalPorts} ports free.
</p>
<p>
Server limits: CPU {count(host.limits.cpu)} · Memory {count(
host.limits.memory
)} · Ports {host.limits.ports}.
</p>
<dl>
{#each [{ label: 'Running server', value: host.calibration.running }, { label: 'Starting server', value: host.calibration.startup }] as allowance}<div
>
<dt>{allowance.label}</dt>
<dd>
{count(allowance.value.cpuCores)} cores · {memory(
allowance.value.memoryBytes
)}
</dd>
<dd class="secondary">
{allowance.value.source === 'bootstrap'
? 'Default allowance'
: `${allowance.value.source === 'host' ? 'Host' : 'Fleet'} observations · ${allowance.value.samples} samples`}
</dd>
</div>{/each}
</dl>
<p>
Starting servers may need more resources. The controller checks
current capacity before reserving a host and port.
</p>
<p>
{host.updatedAt
? `Reported ${$timeFormat.dateTime(host.updatedAt)}`
: 'No host report yet'}
</p>
</div>
</td></tr
>
{/each}</tbody
>
</table>
</div>
<style>
.table-wrap { position:relative; overflow-x:auto; border:1px solid var(--border-subtle); border-radius:var(--radius-md); }
.table-wrap:focus-visible { outline:2px solid var(--accent-primary); outline-offset:3px; }
table { width:100%; min-width:800px; border-collapse:collapse; text-align:left; font-size:var(--text-sm); }
th { padding:12px 16px; color:var(--text-secondary); font-size:var(--text-xs); font-weight:500; background:var(--surface-sunken); }
td { padding:14px 16px; border-top:1px solid var(--border-subtle); background:var(--surface); font-variant-numeric:tabular-nums; }
tr:hover>td { background:var(--surface-raised); }
a { color:var(--accent-primary-hover); } .name { color:var(--text-primary); font-weight:600; text-decoration:none; }
.name:hover { color:var(--accent-primary-hover); text-decoration:underline; }
.estimate { font-weight:600; } .limitation { max-width:280px; } .limitation strong { font-weight:500; }
.reason { display:block; font-size:var(--text-xs); color:var(--text-secondary); line-height:1.5; margin-top:4px; }
button { border:0; background:none; color:var(--accent-primary-hover); font:inherit; cursor:pointer; padding:4px; }
button:focus-visible,a:focus-visible { outline:2px solid var(--accent-primary); outline-offset:3px; }
.detail-cell { background:var(--surface-sunken); } .detail-content { display:grid; gap:12px; font-size:var(--text-xs); color:var(--text-secondary); }
p,dl,ul { margin:0; } ul { padding-left:18px; line-height:1.7; }
dl { display:flex; gap:40px; flex-wrap:wrap; } dt { color:var(--text-primary); font-weight:500; } dd { margin:6px 0 0; }
.secondary { color:var(--text-tertiary); }
.sr-only { position:absolute; width:1px; height:1px; overflow:hidden; clip-path:inset(50%); white-space:nowrap; }
.table-wrap {
position: relative;
overflow-x: auto;
border: 1px solid var(--border-subtle);
border-radius: var(--radius-md);
}
.table-wrap:focus-visible {
outline: 2px solid var(--accent-primary);
outline-offset: 3px;
}
table {
width: 100%;
min-width: 800px;
border-collapse: collapse;
text-align: left;
font-size: var(--text-sm);
}
th {
padding: 12px 16px;
color: var(--text-secondary);
font-size: var(--text-xs);
font-weight: 500;
background: var(--surface-sunken);
}
td {
padding: 14px 16px;
border-top: 1px solid var(--border-subtle);
background: var(--surface);
font-variant-numeric: tabular-nums;
}
tr:hover > td {
background: var(--surface-raised);
}
a {
color: var(--accent-primary-hover);
}
.name {
color: var(--text-primary);
font-weight: 600;
text-decoration: none;
}
.name:hover {
color: var(--accent-primary-hover);
text-decoration: underline;
}
.estimate {
font-weight: 600;
}
.limitation {
max-width: 280px;
}
.limitation strong {
font-weight: 500;
}
.reason {
display: block;
font-size: var(--text-xs);
color: var(--text-secondary);
line-height: 1.5;
margin-top: 4px;
}
button {
border: 0;
background: none;
color: var(--accent-primary-hover);
font: inherit;
cursor: pointer;
padding: 4px;
}
button:focus-visible,
a:focus-visible {
outline: 2px solid var(--accent-primary);
outline-offset: 3px;
}
.detail-cell {
background: var(--surface-sunken);
}
.detail-content {
display: grid;
gap: 12px;
font-size: var(--text-xs);
color: var(--text-secondary);
}
p,
dl,
ul {
margin: 0;
}
ul {
padding-left: 18px;
line-height: 1.7;
}
dl {
display: flex;
gap: 40px;
flex-wrap: wrap;
}
dt {
color: var(--text-primary);
font-weight: 500;
}
dd {
margin: 6px 0 0;
}
.secondary {
color: var(--text-tertiary);
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
}
</style>