Add Prettier and ESLint tooling (Refs #77)
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user