Show running instances on host pages
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
import type { ServerObservation } from '../../server-observation.js';
|
||||
import { db } from '../db/index.js';
|
||||
import type { ServerRow } from '../fleet/servers.js';
|
||||
import { observationReader } from '../fleet/status.js';
|
||||
|
||||
/** Current instances remain visible when their host or game stops reporting. */
|
||||
export function hostInstances(hostId: string): ServerObservation[] {
|
||||
const servers = db().prepare(`SELECT s.* FROM servers s
|
||||
JOIN native_instances i ON i.server = s.name
|
||||
WHERE i.host_id = ? AND i.retired_at IS NULL AND s.destroyed_at IS NULL
|
||||
ORDER BY s.name`).all(hostId) as ServerRow[];
|
||||
if (!servers.length) return [];
|
||||
const observe = observationReader();
|
||||
return servers.map(observe);
|
||||
}
|
||||
@@ -4,10 +4,11 @@ import { deleteHost, getHost, hostDeletionStatus, HostDeletionError, updateHost
|
||||
import { hostApiError, objectBody, requireManagementOrigin } from '$lib/server/hosts/http.js';
|
||||
import { presentHostPreparation } from '$lib/server/hosts/preparation.js';
|
||||
import { config } from '$lib/server/config.js';
|
||||
import { hostInstances } from '$lib/server/hosts/instances.js';
|
||||
type Event = { request: Request; params: { id: string } };
|
||||
export function GET({ request, params }: Event) {
|
||||
requireManagementOrigin(request); const host = getHost(params.id); if (!host) error(404, 'Host not found.');
|
||||
return json({ host: presentHostPreparation(host), calibration: capacityCalibration(host.id), defaultRuntimeImage: config.komodo.defaultImage, deletion: hostDeletionStatus(host.id) });
|
||||
return json({ host: presentHostPreparation(host), instances: hostInstances(host.id), serverNow: Date.now(), calibration: capacityCalibration(host.id), defaultRuntimeImage: config.komodo.defaultImage, deletion: hostDeletionStatus(host.id) });
|
||||
}
|
||||
export async function PATCH({ request, params }: Event) {
|
||||
requireManagementOrigin(request);
|
||||
|
||||
@@ -11,7 +11,11 @@
|
||||
import DeleteHostDialog from '$ui/components/DeleteHostDialog.svelte';
|
||||
import HostPreparation from '$ui/components/HostPreparation.svelte';
|
||||
import CapacitySamples from '$ui/components/CapacitySamples.svelte';
|
||||
import ServerCard from '$ui/components/ServerCard.svelte';
|
||||
import type { ServerObservation } from '$lib/server-observation.js';
|
||||
import { syncServerTime } from '$ui/stores/servers.js';
|
||||
let host = $state<Host | null>(null);
|
||||
let instances = $state<ServerObservation[]>([]);
|
||||
let calibration = $state<CapacityCalibration | null>(null);
|
||||
let loading = $state(true); let busy = $state(false); let error = $state(''); let notice = $state('');
|
||||
let deleteOpen = $state(false);
|
||||
@@ -24,7 +28,7 @@
|
||||
async function load(initialize = false, signal?: AbortSignal) {
|
||||
const target = endpoint; const sequence = ++requestSequence;
|
||||
try {
|
||||
const response = await fetch(target, { signal: signal ?? AbortSignal.timeout(15000) }); const body = await response.json(); if (signal?.aborted || target !== endpoint || sequence !== requestSequence) return; if (response.status === 404 && host?.decommission) { await goto('/hosts'); return; } if (!response.ok) throw new Error(body.error ?? body.message ?? 'Could not load host.'); host = body.host; calibration = body.calibration ?? null; error = '';
|
||||
const response = await fetch(target, { signal: signal ?? AbortSignal.timeout(15000) }); const body = await response.json(); if (signal?.aborted || target !== endpoint || sequence !== requestSequence) return; if (response.status === 404 && host?.decommission) { await goto('/hosts'); return; } if (!response.ok) throw new Error(body.error ?? body.message ?? 'Could not load host.'); host = body.host; instances = body.instances ?? []; syncServerTime(body.serverNow); calibration = body.calibration ?? null; error = '';
|
||||
if (initialize && host) { name = host.name; portMin = host.portMin; portMax = host.portMax; bindAddress = host.bindAddress; publicAddress = host.publicAddress; minimumFreeGB = host.minimumFreeBytes / 1073741824; runtimeImage = body.defaultRuntimeImage ?? runtimeImage; }
|
||||
} catch (cause) { if (!signal?.aborted && sequence === requestSequence) error = (cause as Error).message; return false; } finally { if (sequence === requestSequence) loading = false; }
|
||||
}
|
||||
@@ -45,7 +49,7 @@
|
||||
</script>
|
||||
|
||||
<svelte:head><title>{host?.name ?? 'Host'} · Electric Cherry</title></svelte:head>
|
||||
<section class="page"><a href="/hosts">← All hosts</a><SectionHeader title={host?.name ?? 'Host'} subtitle="Readiness, inventory, settings, and credentials.">
|
||||
<section class="page"><a href="/hosts">← All hosts</a><SectionHeader title={host?.name ?? 'Host'} subtitle="Running instances, readiness, inventory, settings, and credentials.">
|
||||
{#snippet actions()}<Button href="/hosts/operations">Operation history</Button>{#if canManage && host && (!cleaning || host.decommission?.retryable)}<Button variant="danger" disabled={busy} onclick={() => deleteOpen = true}>{cleaning ? 'Retry cleanup' : 'Delete host'}</Button>{/if}{/snippet}
|
||||
</SectionHeader>
|
||||
{#if error}<p class="ui-error" role="alert">{error}</p>{/if}{#if notice}<p role="status">{notice}</p>{/if}
|
||||
@@ -56,6 +60,22 @@
|
||||
{#if canManage && !cleaning}<div class="actions"><Button disabled={busy || host.mode === 'active'} onclick={() => mode('active')}>Enable</Button><Button disabled={busy || host.mode === 'draining'} onclick={() => mode('draining')}>Drain host</Button><Button disabled={busy || host.mode === 'disabled'} onclick={() => mode('disabled')}>Disable host</Button></div>{/if}
|
||||
<p>Draining prevents new deployments and retires instances as they become safe to remove. Disabled hosts receive no new commands; running game containers continue.</p>
|
||||
</section>
|
||||
<section class="panel" aria-labelledby="running-instances-heading">
|
||||
<h2 id="running-instances-heading">Running instances · {instances.length}</h2>
|
||||
<p>Current instances on this host, including any that are starting or unavailable.</p>
|
||||
{#if !host.connected && instances.length}<p role="status">The host is offline. Instance details reflect the latest reports.</p>{/if}
|
||||
{#if instances.length}
|
||||
<div class="instances">
|
||||
{#each instances as instance (instance.name)}
|
||||
<ServerCard name={instance.name} health={instance.health} players={instance.last_players}
|
||||
map={instance.last_map} gametype={instance.last_gametype} roster={instance.roster}
|
||||
lastSeenAt={instance.last_seen_at} lastError={instance.last_error} telemetry={instance.telemetry}
|
||||
activeSince={instance.active_since} inactiveSince={instance.inactive_since}
|
||||
gameStartedAt={instance.game_started_at} gameEndedAt={instance.game_ended_at} />
|
||||
{/each}
|
||||
</div>
|
||||
{:else}<p>No instances are currently deployed on this host.</p>{/if}
|
||||
</section>
|
||||
{#if !cleaning}<section class="panel"><h2>Automatic host setup</h2><HostPreparation {host} /></section>{/if}
|
||||
{#if calibration}<section class="panel"><h2>Learned server capacity</h2>
|
||||
<p>Allowances update from observed container usage. Hosts reserve 5% CPU and 500 MB of memory. Startup and running usage are tracked separately.</p>
|
||||
@@ -90,5 +110,6 @@
|
||||
<DeleteHostDialog bind:open={deleteOpen} {host} ondeleted={async (result) => { if (result.deleted) await goto('/hosts'); else { enrollment = null; await load(); } }} />
|
||||
|
||||
<style>
|
||||
.instances { display:grid; grid-template-columns:repeat(auto-fit,minmax(min(100%,300px),1fr)); gap:16px; }
|
||||
.page { display:grid; gap:24px; } .panel { border:1px solid var(--border-strong); border-radius:var(--radius-md); background:var(--surface); padding:24px; min-width:0; } h2 { margin:0 0 16px; font-size:var(--text-md); text-transform:capitalize; } p, li, dd, dt, span { font-size:var(--text-sm); line-height:1.65; } p,li,dt,span { color:var(--text-secondary); } .grid { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:24px; } .row,.actions,.fields { display:flex; gap:12px; flex-wrap:wrap; } .row { justify-content:space-between; } .actions { margin-top:20px; } dl { display:grid; grid-template-columns:1fr 1.3fr; gap:12px; margin:0; } dd { margin:0; overflow-wrap:anywhere; } fieldset { display:grid; gap:16px; border:0; padding:0; margin:0; } label { display:grid; gap:8px; font-size:var(--text-sm); } input { width:100%; min-width:0; padding:10px; color:var(--text-primary); background:var(--surface-sunken); border:1px solid var(--border-control); border-radius:var(--radius-sm); } .fields>* { flex:1; min-width:120px; } pre { white-space:pre-wrap; overflow-wrap:anywhere; padding:16px; background:var(--surface-sunken); font-size:var(--text-xs); } @media(max-width:900px) { .grid { grid-template-columns:1fr; } }
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user