Retain host and server scope during investigations
This commit is contained in:
@@ -113,6 +113,15 @@ export interface HostOperationSummary extends Omit<HostOperation, 'payload' | 'r
|
||||
routine: boolean;
|
||||
}
|
||||
export type HostOperationFilter = 'important' | 'routine' | 'all';
|
||||
export interface HostOperationScope { host?: string; server?: string }
|
||||
export function hostOperationScope(params: URLSearchParams): HostOperationScope {
|
||||
const scope: HostOperationScope = {};
|
||||
for (const key of ['host', 'server'] as const) {
|
||||
const value = params.get(key);
|
||||
if (value && value.length <= 512 && !/[\x00-\x1f\x7f]/.test(value)) scope[key] = value;
|
||||
}
|
||||
return scope;
|
||||
}
|
||||
export function isHostOperationFilter(value: unknown): value is HostOperationFilter {
|
||||
return typeof value === 'string' && ['important', 'routine', 'all'].includes(value);
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import { parseHostInventory } from './request-context.js';
|
||||
import { fleetSettings } from '../settings.js';
|
||||
import { recordInstanceResources, validateInstanceResources } from './calibration.js';
|
||||
import { refreshHostAvailability, scheduleAvailabilityRefresh } from './availability.js';
|
||||
import type { Host, HostDecommission, HostDeletion, HostInventory, HostMode, HostOperation, HostOperationFilter, HostOperationHistoryPage, HostOperationState } from '../../hosts.js';
|
||||
import type { Host, HostDecommission, HostDeletion, HostInventory, HostMode, HostOperation, HostOperationFilter, HostOperationHistoryPage, HostOperationScope, HostOperationState } from '../../hosts.js';
|
||||
|
||||
export const AGENT_PROTOCOL_VERSION = 1;
|
||||
export const HEARTBEAT_MAX_AGE_MS = 90_000;
|
||||
@@ -395,12 +395,16 @@ export function listHostOperations(hostId: string, limit = 100, summary = false)
|
||||
.all(hostId, Math.min(500, Math.max(1, limit))) as OperationRow[]).map(operationFromRow);
|
||||
}
|
||||
/** Browse retained history without loading command payloads or credential-bearing results. */
|
||||
export function listHostOperationHistory(page = 1, filter: HostOperationFilter = 'important'): HostOperationHistoryPage {
|
||||
export function listHostOperationHistory(page = 1, filter: HostOperationFilter = 'important', scope: HostOperationScope = {}): HostOperationHistoryPage {
|
||||
const handle = database();
|
||||
const pageSize = 25;
|
||||
const where = filter === 'all' ? '' : `WHERE ${filter === 'routine' ? 'o.routine=1' : importantOperationSql}`;
|
||||
const conditions = filter === 'all' ? [] : [filter === 'routine' ? 'o.routine=1' : `(${importantOperationSql})`];
|
||||
const parameters: string[] = [];
|
||||
if (scope.host) { conditions.push('o.host_id=?'); parameters.push(scope.host); }
|
||||
if (scope.server) { conditions.push('o.server_name=?'); parameters.push(scope.server); }
|
||||
const where = conditions.length ? `WHERE ${conditions.join(' AND ')}` : '';
|
||||
return handle.transaction((): HostOperationHistoryPage => {
|
||||
const { total } = handle.prepare(`SELECT COUNT(*) AS total FROM ec_host_operations o ${where}`).get() as { total: number };
|
||||
const { total } = handle.prepare(`SELECT COUNT(*) AS total FROM ec_host_operations o ${where}`).get(...parameters) as { total: number };
|
||||
const totalPages = Math.max(1, Math.ceil(total / pageSize));
|
||||
const currentPage = Math.min(totalPages, Number.isSafeInteger(page) && page > 0 ? page : 1);
|
||||
const operations = handle.prepare(`SELECT o.id,o.host_id AS hostId,h.name AS hostName,o.kind,
|
||||
@@ -411,7 +415,7 @@ export function listHostOperationHistory(page = 1, filter: HostOperationFilter =
|
||||
FROM ec_host_operations o LEFT JOIN ec_hosts h ON h.id=o.host_id AND h.deleted_at IS NULL
|
||||
${where}
|
||||
ORDER BY o.created_at DESC,o.rowid DESC LIMIT ? OFFSET ?`)
|
||||
.all(pageSize, (currentPage - 1) * pageSize) as HostOperationHistoryPage['operations'];
|
||||
.all(...parameters, pageSize, (currentPage - 1) * pageSize) as HostOperationHistoryPage['operations'];
|
||||
return { operations: operations.map(operation => ({ ...operation, routine: !!operation.routine,
|
||||
targetPath: safeOperationTarget(operation.kind, operation.targetPath) })), filter,
|
||||
page: currentPage, pageSize, total, totalPages };
|
||||
|
||||
@@ -110,6 +110,8 @@ export function ensureHostSchema(handle: Database.Database): void {
|
||||
})();
|
||||
}
|
||||
handle.exec(`CREATE INDEX IF NOT EXISTS ec_host_operations_routine_history ON ec_host_operations(routine,created_at);
|
||||
CREATE INDEX IF NOT EXISTS ec_host_operations_host_history ON ec_host_operations(host_id,created_at);
|
||||
CREATE INDEX IF NOT EXISTS ec_host_operations_server_history ON ec_host_operations(server_name,created_at);
|
||||
CREATE INDEX IF NOT EXISTS ec_host_operations_important_history ON ec_host_operations(created_at)
|
||||
WHERE ${importantOperationSql.replaceAll('o.', '')}`);
|
||||
handle.exec('CREATE TABLE IF NOT EXISTS ec_host_schema_migrations (name TEXT PRIMARY KEY)');
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { error, json } from '@sveltejs/kit';
|
||||
import { listHostOperationHistory } from '$lib/server/hosts/index.js';
|
||||
import { requireManagementOrigin } from '$lib/server/hosts/http.js';
|
||||
import { isHostOperationFilter } from '$lib/hosts.js';
|
||||
import { hostOperationScope, isHostOperationFilter } from '$lib/hosts.js';
|
||||
|
||||
export function GET({ request }: { request: Request }) {
|
||||
requireManagementOrigin(request);
|
||||
@@ -10,5 +10,9 @@ export function GET({ request }: { request: Request }) {
|
||||
const filter = params.get('filter') ?? 'important';
|
||||
if (!Number.isSafeInteger(page) || page < 1) error(400, 'Invalid operation history page.');
|
||||
if (!isHostOperationFilter(filter)) error(400, 'Invalid operation history filter.');
|
||||
return json(listHostOperationHistory(page, filter), { headers: { 'Cache-Control': 'no-store' } });
|
||||
const scope = hostOperationScope(params);
|
||||
for (const key of ['host', 'server'] as const) {
|
||||
if (params.has(key) && params.get(key) !== scope[key]) error(400, 'Invalid operation history scope.');
|
||||
}
|
||||
return json(listHostOperationHistory(page, filter, scope), { headers: { 'Cache-Control': 'no-store' } });
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import BackLink from '$ui/components/BackLink.svelte';
|
||||
import { contextHref } from '$lib/navigation-context.js';
|
||||
import { onMount, untrack } from 'svelte';
|
||||
import { beforeNavigate, afterNavigate, goto } from '$app/navigation';
|
||||
@@ -36,7 +37,12 @@
|
||||
if (query.hostId) params.set('host', query.hostId);
|
||||
return params;
|
||||
}
|
||||
function scopeUrl(change: Partial<CapacityQuery>) { return `/capacity?${queryParams({ ...report.query, ...change })}`; }
|
||||
function scopeUrl(change: Partial<CapacityQuery>) {
|
||||
const params = queryParams({ ...report.query, ...change });
|
||||
const returnTo = page.url.searchParams.get('returnTo');
|
||||
if (returnTo) params.set('returnTo', returnTo);
|
||||
return `/capacity?${params}`;
|
||||
}
|
||||
function cancel() { sequence++; request?.abort(); request = null; refreshing = false; }
|
||||
async function refresh(signal?: AbortSignal) {
|
||||
if (navigating || refreshing) return;
|
||||
@@ -72,6 +78,7 @@
|
||||
<SectionHeader title="Capacity" subtitle="See what your hosts can run, what is limiting them, and how headroom changes.">
|
||||
{#snippet actions()}<Button href="/hosts">Manage hosts</Button><Button disabled={refreshing || navigating} onclick={() => void refresh()}>{refreshing ? 'Refreshing…' : 'Refresh'}</Button>{/snippet}
|
||||
</SectionHeader>
|
||||
{#if page.url.searchParams.has('returnTo')}<BackLink fallback="/hosts" />{/if}
|
||||
{#if refreshError || navigationError}<div class="refresh-error" role="alert"><strong>{navigationError || refreshError}</strong><span>Showing the last successfully loaded capacity report.</span></div>{/if}
|
||||
<div class="toolbar">
|
||||
<div class="filters"><Field label="Host"><Select value={report.query.hostId ?? ''} options={hostOptions} disabled={navigating} onchange={event => void changeScope({ hostId: (event.currentTarget as HTMLSelectElement).value || null })} /></Field>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { contextHref } from '$lib/navigation-context.js';
|
||||
import { untrack } from 'svelte';
|
||||
import { pollVisible } from '$ui/poll.js';
|
||||
let requestSequence = 0;
|
||||
@@ -57,7 +58,7 @@
|
||||
|
||||
<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="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}
|
||||
{#snippet actions()}<Button href={contextHref(`/capacity?host=${encodeURIComponent($page.params.id ?? "")}`, $page.url)}>Capacity</Button><Button href={contextHref(`/hosts/operations?host=${encodeURIComponent($page.params.id ?? "")}`, $page.url)}>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}
|
||||
{#if loading}<p role="status">Loading host…</p>{:else if host}
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import { listHostOperationHistory } from '$lib/server/hosts/index.js';
|
||||
import { isHostOperationFilter } from '$lib/hosts.js';
|
||||
import { getHost, listHostOperationHistory } from '$lib/server/hosts/index.js';
|
||||
import { hostOperationScope, isHostOperationFilter } from '$lib/hosts.js';
|
||||
import type { PageServerLoad } from './$types';
|
||||
|
||||
export const load: PageServerLoad = ({ url }) => {
|
||||
const requestedPage = url.searchParams.get('page');
|
||||
const requestedFilter = url.searchParams.get('filter');
|
||||
const filter = isHostOperationFilter(requestedFilter) ? requestedFilter : 'important';
|
||||
const history = listHostOperationHistory(Number(requestedPage ?? 1), filter);
|
||||
const scope = hostOperationScope(url.searchParams);
|
||||
const history = listHostOperationHistory(Number(requestedPage ?? 1), filter, scope);
|
||||
if ((requestedPage !== null && requestedPage !== String(history.page))
|
||||
|| (requestedFilter !== null && requestedFilter !== filter)) {
|
||||
const params = new URLSearchParams();
|
||||
const params = new URLSearchParams(url.searchParams);
|
||||
params.delete('page');
|
||||
if (requestedFilter !== null) params.set('filter', filter);
|
||||
if (history.page !== 1) params.set('page', String(history.page));
|
||||
redirect(307, `/hosts/operations${params.size ? `?${params}` : ''}`);
|
||||
}
|
||||
return { history };
|
||||
return { history, scope, hostName: scope.host ? getHost(scope.host)?.name ?? scope.host : null };
|
||||
};
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
<script lang="ts">
|
||||
import { onMount, untrack } from 'svelte';
|
||||
import { beforeNavigate } from '$app/navigation';
|
||||
import { navigating } from '$app/stores';
|
||||
import { navigating, page } from '$app/stores';
|
||||
import type { HostOperationFilter, HostOperationHistoryPage } from '$lib/hosts.js';
|
||||
import type { PageData } from './$types';
|
||||
import { pollVisible } from '$ui/poll.js';
|
||||
import { timeFormat } from '$ui/stores/time-zone.js';
|
||||
import BackLink from '$ui/components/BackLink.svelte';
|
||||
import { contextHref } from '$lib/navigation-context.js';
|
||||
import SectionHeader from '$ui/components/primitives/SectionHeader.svelte';
|
||||
import Button from '$ui/components/primitives/Button.svelte';
|
||||
|
||||
@@ -25,9 +27,10 @@
|
||||
{ id: 'all', label: 'All operations', description: 'Every recorded remote operation.', empty: 'No remote operations yet.' }
|
||||
];
|
||||
const selectedFilter = $derived(filters.find(filter => filter.id === history.filter)!);
|
||||
function pageHref(page: number, filter: HostOperationFilter = history.filter) {
|
||||
const params = new URLSearchParams({ filter });
|
||||
if (page !== 1) params.set('page', String(page));
|
||||
function pageHref(pageNumber: number, filter: HostOperationFilter = history.filter) {
|
||||
const params = new URLSearchParams($page.url.searchParams);
|
||||
params.set('filter', filter); params.delete('page');
|
||||
if (pageNumber !== 1) params.set('page', String(pageNumber));
|
||||
return `/hosts/operations?${params}`;
|
||||
}
|
||||
function cancel() { sequence++; request?.abort(); request = null; refreshing = false; }
|
||||
@@ -40,7 +43,7 @@
|
||||
request = controller;
|
||||
refreshing = true;
|
||||
try {
|
||||
const params = new URLSearchParams({ page: String(history.page), filter: history.filter });
|
||||
const params = new URLSearchParams({ ...data.scope, page: String(history.page), filter: history.filter });
|
||||
const response = await fetch(`/api/hosts/operations?${params}`, {
|
||||
signal: AbortSignal.any([controller.signal, AbortSignal.timeout(15000), ...(signal ? [signal] : [])])
|
||||
});
|
||||
@@ -60,12 +63,14 @@
|
||||
onMount(() => { const stop = pollVisible(refresh, 15000, false); return () => { stop(); cancel(); }; });
|
||||
</script>
|
||||
|
||||
<svelte:head><title>Operation history · Hosts · Electric Cherry</title></svelte:head>
|
||||
<svelte:head><title>Host operation history · Electric Cherry</title></svelte:head>
|
||||
|
||||
<section class="page" aria-busy={changingPage}>
|
||||
<SectionHeader title="Operation history" eyebrow="Hosts" subtitle="Remote operations across all hosts, newest first.">
|
||||
<SectionHeader title="Host operation history" eyebrow="Operations" subtitle="Remote operations, newest first. Filtered views retain their host and server context.">
|
||||
{#snippet actions()}<Button disabled={refreshing || changingPage} onclick={() => void refresh()}>{refreshing ? 'Refreshing…' : 'Refresh'}</Button>{/snippet}
|
||||
</SectionHeader>
|
||||
{#if $page.url.searchParams.has('returnTo')}<BackLink fallback="/hosts" />{/if}
|
||||
{#if data.scope.host || data.scope.server}<div class="scope" role="status"><span>Showing {data.hostName ? `host: ${data.hostName}` : ''}{data.scope.host && data.scope.server ? ' · ' : ''}{data.scope.server ? `server: ${data.scope.server}` : ''}</span><a href={`/hosts/operations?filter=${history.filter}`}>Clear scope</a></div>{/if}
|
||||
<div class="history-filters">
|
||||
<nav class="filter-links" aria-label="Operation history filters">
|
||||
{#each filters as filter}
|
||||
@@ -86,7 +91,7 @@
|
||||
<tbody>{#each history.operations as operation (operation.id)}
|
||||
<tr>
|
||||
<td class="operation">{operation.kind}<small>{operation.id}</small></td>
|
||||
<td>{#if operation.hostName !== null}<a href={`/hosts/${operation.hostId}`}>{operation.hostName}</a>{:else}Deleted host<small>{operation.hostId}</small>{/if}</td>
|
||||
<td>{#if operation.hostName !== null}<a href={contextHref(`/hosts/${operation.hostId}`, $page.url)}>{operation.hostName}</a>{:else}Deleted host<small>{operation.hostId}</small>{/if}</td>
|
||||
<td class="target">
|
||||
<span>{operation.serverName ?? operation.instanceId ?? 'Host-level'}</span>
|
||||
{#if operation.targetPath}<small class="target-path">{operation.targetPath}</small>{/if}
|
||||
@@ -112,6 +117,7 @@
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.scope { display:flex; flex-wrap:wrap; gap:16px; color:var(--text-secondary); font-size:var(--text-sm); }
|
||||
.page { display: grid; gap: 24px; min-width: 0; }
|
||||
.history-filters { min-width: 0; }
|
||||
.history-filters p { margin: 10px 0 0; color: var(--text-secondary); font-size: var(--text-sm); }
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import BackLink from '$ui/components/BackLink.svelte';
|
||||
import { timeFormat } from '$ui/stores/time-zone.js';
|
||||
import { onMount } from 'svelte';
|
||||
import { page } from '$app/stores';
|
||||
@@ -112,6 +113,7 @@
|
||||
<SectionHeader title="Provisioning" subtitle="Create, start, restart, rebuild, stop, and destroy server instances.">
|
||||
{#snippet actions()}<Button onclick={() => lifecycleOpen = true}>Manage server</Button><Button variant="primary" onclick={() => open = true}>Create server</Button><details class="fleet-actions"><summary>Fleet actions</summary><Button variant="danger" onclick={() => housePartyOpen = true}>Destroy fleet…</Button></details>{/snippet}
|
||||
</SectionHeader>
|
||||
{#if $page.url.searchParams.has('returnTo')}<BackLink fallback="/" />{/if}
|
||||
<Tabs {tabs} bind:value={tab} onchange={() => saveContext(true)} ariaLabel="Provisioning views" idPrefix="deployment-tabs" panelId="deployment-panel" />
|
||||
<LifecycleDialog bind:open={lifecycleOpen} />
|
||||
<DeployServerDialog bind:open />
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { safeReturnHref, returnLabel } from '$lib/navigation-context.js';
|
||||
import { safeReturnHref, returnLabel, contextHref } from '$lib/navigation-context.js';
|
||||
import { onDestroy, onMount, untrack } from 'svelte';
|
||||
import { page } from '$app/stores';
|
||||
import type { PageData } from './$types';
|
||||
@@ -145,7 +145,7 @@
|
||||
</dl>
|
||||
{#if detail.agentOperation.error}<p>{detail.agentOperation.error}</p>{/if}
|
||||
{#if outcome.active && detail.agentOperation.state === 'completed'}<p>The agent finished its command. EC is verifying readiness.</p>{/if}
|
||||
<a href="/hosts/operations">View host operation history</a>
|
||||
<a href={contextHref(`/hosts/operations?server=${encodeURIComponent(deployment.server)}`, $page.url)}>View host operation history</a>
|
||||
</div></Card>
|
||||
{/if}
|
||||
{#if outcome.active && detail.workerActivity}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { contextHref } from '$lib/navigation-context.js';
|
||||
import BackLink from '$ui/components/BackLink.svelte';
|
||||
let { data }: { data: import('./$types').PageData } = $props();
|
||||
import RuntimeVersion from '$ui/components/RuntimeVersion.svelte';
|
||||
@@ -284,7 +285,7 @@
|
||||
{#if hasDiagnosticIssue}
|
||||
<section class="connection-issue" aria-label="Server connection issue">
|
||||
<div><strong>{effectiveHealth === 'stale' ? 'Server report is outdated' : 'Server is not responding'}</strong><p>Player count is unknown. {effectiveSeen ? `Last successful report ${ago(effectiveSeen, $uiNow)}.` : 'No successful report has been received.'} {native && !server.host?.connected ? 'The host agent is offline; the game may still be running.' : ''}</p></div>
|
||||
<div class="issue-actions"><Button size="sm" onclick={() => changeTab('diagnostics')}>Diagnose connection</Button><Button size="sm" variant="ghost" onclick={() => changeTab('logs')}>Open logs</Button>{#if native && server.host_id}<Button size="sm" href={`/hosts/${server.host_id}`}>Check host</Button>{:else}<Button size="sm" href="/settings">Check system setup</Button>{/if}</div>
|
||||
<div class="issue-actions"><Button size="sm" onclick={() => changeTab('diagnostics')}>Diagnose connection</Button><Button size="sm" variant="ghost" onclick={() => changeTab('logs')}>Open logs</Button>{#if native && server.host_id}<Button size="sm" href={contextHref(`/hosts/${server.host_id}`, $page.url)}>Check host</Button>{:else}<Button size="sm" href="/settings">Check system setup</Button>{/if}</div>
|
||||
</section>
|
||||
{/if}
|
||||
<div class="tabs-row">
|
||||
@@ -384,7 +385,7 @@
|
||||
{native ? 'Probe RCON through agent' : 'Probe RCON'}
|
||||
</Button>
|
||||
{/if}
|
||||
{#if server.host_id}<Button variant="secondary" size="sm" href={`/hosts/${server.host_id}`}>Inspect host</Button>{/if}
|
||||
{#if server.host_id}<Button variant="secondary" size="sm" href={contextHref(`/hosts/${server.host_id}`, $page.url)}>Inspect host</Button>{/if}
|
||||
</div>
|
||||
|
||||
{#if rconProbe?.ok}
|
||||
@@ -402,7 +403,7 @@
|
||||
<h2>Configuration</h2>
|
||||
</div>
|
||||
<div class="diag-grid">
|
||||
{#if native}<div class="kv"><span class="k">Host</span><span class="v"><a href={`/hosts/${server.host_id}`}>{server.host?.name ?? server.host_id}</a></span></div><div class="kv"><span class="k">Instance</span><span class="v mono">{server.native_instance_id}</span></div><div class="kv"><span class="k">Generation</span><span class="v mono">{server.instance_generation}</span></div><div class="kv"><span class="k">Game-file version</span><span class="v mono"><a href="/game-files">{server.game_library_id}</a></span></div>{#if server.network}<div class="kv"><span class="k">Local UDP binding</span><span class="v mono">{server.network.bindAddress}:{server.network.localPort}</span></div><div class="kv"><span class="k">Public / NAT endpoint</span><span class="v mono">{server.network.publicAddress || server.host?.publicAddress || server.host?.hostname || 'Not configured'}:{server.network.publicPort ?? server.network.localPort}</span></div>{/if}
|
||||
{#if native}<div class="kv"><span class="k">Host</span><span class="v"><a href={contextHref(`/hosts/${server.host_id}`, $page.url)}>{server.host?.name ?? server.host_id}</a></span></div><div class="kv"><span class="k">Instance</span><span class="v mono">{server.native_instance_id}</span></div><div class="kv"><span class="k">Generation</span><span class="v mono">{server.instance_generation}</span></div><div class="kv"><span class="k">Game-file version</span><span class="v mono"><a href="/game-files">{server.game_library_id}</a></span></div>{#if server.network}<div class="kv"><span class="k">Local UDP binding</span><span class="v mono">{server.network.bindAddress}:{server.network.localPort}</span></div><div class="kv"><span class="k">Public / NAT endpoint</span><span class="v mono">{server.network.publicAddress || server.host?.publicAddress || server.host?.hostname || 'Not configured'}:{server.network.publicPort ?? server.network.localPort}</span></div>{/if}
|
||||
{/if}
|
||||
<div class="kv"><span class="k">Game port</span><span class="v mono">{server.game_port ?? '(unresolved)'}</span></div>
|
||||
<div class="kv"><span class="k">Game</span><span class="v mono">{server.game ?? '(unset)'}</span></div>
|
||||
@@ -416,6 +417,7 @@
|
||||
{:else if activeTab === 'matches'}
|
||||
{#key name}<MatchHistory server={name} />{/key}
|
||||
{:else if activeTab === 'history'}
|
||||
<div class="history-links"><Button href={contextHref(`/provisioning?view=history&server=${encodeURIComponent(name)}`, $page.url)}>Provisioning history</Button><Button href={contextHref(`/hosts/operations?server=${encodeURIComponent(name)}`, $page.url)}>Host operation history</Button></div>
|
||||
{#if operations.length}
|
||||
<Card padding="lg">
|
||||
<div class="panel-head">
|
||||
@@ -424,7 +426,7 @@
|
||||
</div>
|
||||
<div class="server-operations">
|
||||
{#each operations as operation (operation.key)}
|
||||
<a href={operation.href}>
|
||||
<a href={contextHref(operation.href, $page.url)}>
|
||||
<strong>{operation.label}</strong>
|
||||
<span>{operation.detail}</span>
|
||||
<StatusBadge state={operation.state} />
|
||||
@@ -447,7 +449,7 @@
|
||||
<div class="history">
|
||||
{#each history.slice(0,historyLimit) as d (d.id)}
|
||||
{@const mine = d.servers.find((s) => s.server === name)!}
|
||||
<a class="hist-row s-{mine.restart_state}" href="/campaigns/{d.id}">
|
||||
<a class="hist-row s-{mine.restart_state}" href={contextHref(`/campaigns/${d.id}`, $page.url)}>
|
||||
<span class="hist-action">{d.action}</span>
|
||||
<span class="hist-state">
|
||||
<StatusBadge state={mine.restart_state} />
|
||||
@@ -476,6 +478,7 @@
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.history-links { display:flex; flex-wrap:wrap; gap:12px; }
|
||||
.server-actions { text-align: right; }
|
||||
.server-actions summary { cursor: pointer; padding: 9px 12px; border: 1px solid var(--border-strong); border-radius: var(--radius-md); font-size: var(--text-sm); }
|
||||
.server-actions[open] summary { margin-bottom: 10px; }
|
||||
|
||||
Reference in New Issue
Block a user