Clarify server actions and group maintenance failures
This commit is contained in:
@@ -15,9 +15,13 @@
|
||||
import SectionHeader from '$ui/components/primitives/SectionHeader.svelte';
|
||||
import Tabs from '$ui/components/primitives/Tabs.svelte';
|
||||
import Badge from '$ui/components/primitives/Badge.svelte';
|
||||
import StatusBadge from '$ui/components/primitives/StatusBadge.svelte';
|
||||
import StatusPip from '$ui/components/primitives/StatusPip.svelte';
|
||||
import Button from '$ui/components/primitives/Button.svelte';
|
||||
import Freshness from '$ui/components/Freshness.svelte';
|
||||
import EmptyState from '$ui/components/primitives/EmptyState.svelte';
|
||||
import ListFooter from '$ui/components/primitives/ListFooter.svelte';
|
||||
import { groupMaintenanceScanErrors, groupMaintenanceVersions } from '$ui/maintenance-presentation.js';
|
||||
|
||||
let data = $state<MaintenanceOverview | null>(null);
|
||||
let error = $state('');
|
||||
@@ -34,6 +38,7 @@
|
||||
let scheduleLoaded = false;
|
||||
let displayedZone = $state('');
|
||||
let request = 0;
|
||||
let checkedAt = $state<number | null>(null);
|
||||
const tabs = [{ id: 'overview', label: 'Overview' }, { id: 'policies', label: 'Policies & schedules' }, { id: 'activity', label: 'Activity' }];
|
||||
const policies: { id: MaintenancePolicy; title: string; description: string }[] = [
|
||||
{ id: 'health', title: 'Health recovery', description: 'Restart unhealthy servers, then rebuild once if recovery fails. Rebuilding deletes instance files and installs the latest promoted setup.' },
|
||||
@@ -44,6 +49,9 @@
|
||||
const outdated = $derived(versions.filter(v => v.update_status === 'outdated'));
|
||||
const unknown = $derived(versions.filter(v => v.update_status === 'unknown'));
|
||||
const activeJobs = $derived(data?.runtime.jobs.filter(j => !j.server_deleted_at && ['queued', 'restarting'].includes(j.state)) ?? []);
|
||||
const scanErrors = $derived(groupMaintenanceScanErrors(data?.recovery.scanError, data?.runtime.servers.map(version => version.server) ?? []));
|
||||
const versionGroups = $derived(groupMaintenanceVersions(versions, activeJobs));
|
||||
const updatableServers = $derived(versionGroups.flatMap(group => group.updatable));
|
||||
const activities = $derived([
|
||||
...(data?.recovery.incidents ?? []).map(i => ({
|
||||
key: i.id, kind: i.kind, server: i.server, server_deleted_at: i.server_deleted_at, at: i.first_seen_at,
|
||||
@@ -92,6 +100,7 @@
|
||||
}
|
||||
function receive(body: MaintenanceOverview) {
|
||||
data = body;
|
||||
checkedAt = Date.now();
|
||||
if (!scheduleLoaded) { scheduleEnabled = body.runtime.schedule.enabled; scheduleLoaded = true; }
|
||||
}
|
||||
async function load() {
|
||||
@@ -129,8 +138,9 @@
|
||||
{/snippet}
|
||||
</SectionHeader>
|
||||
{#if error}<p class="ui-error" role="alert">{error} {data ? 'Showing the last successful refresh.' : ''} <Button size="sm" onclick={load} disabled={busy}>Retry refresh</Button></p>{/if}
|
||||
{#if !error}<Freshness updatedAt={checkedAt} intervalLabel="Updates every 5 seconds" />{/if}
|
||||
{#if notice}<p role="status">{notice}</p>{/if}
|
||||
{#if data?.policies.paused}<div class="paused"><strong>All maintenance is paused.</strong> Checks and verification continue. New actions, scheduled runs, and queued maintenance are held. Submitted operations finish recovery. Campaigns, betas, and manual Provisioning actions continue.</div>{/if}
|
||||
{#if data?.policies.paused}<div class="paused"><strong>All maintenance is paused.</strong> Checks and verification continue. New actions, scheduled runs, and queued maintenance are held. Submitted operations finish recovery. Campaigns, betas, and manual server operations continue.</div>{/if}
|
||||
{#if $page.url.searchParams.has('returnTo')}<BackLink fallback="/provisioning" />{/if}
|
||||
<Tabs {tabs} bind:value={view} onchange={() => saveContext()} ariaLabel="Maintenance views" idPrefix="maintenance-tabs" panelId="maintenance-panel" />
|
||||
<div id="maintenance-panel" role="tabpanel" aria-labelledby={`maintenance-tabs-${view}`} class="panels">
|
||||
@@ -159,10 +169,21 @@
|
||||
</section>
|
||||
{:else}
|
||||
{#if view === 'overview'}
|
||||
<div class="summary"><span><strong>{attention}</strong> need attention</span><span><strong>{waiting}</strong> active</span><span><strong>{outdated.length}</strong> outdated</span><span><strong>{unknown.length}</strong> versions unverified</span></div>
|
||||
<div class="summary"><span><strong>{attention}</strong> {attention === 1 ? 'needs' : 'need'} attention</span><span><strong>{waiting}</strong> active</span><span><strong>{outdated.length}</strong> outdated</span><span><strong>{unknown.length}</strong> {unknown.length === 1 ? 'version' : 'versions'} unverified</span></div>
|
||||
{#if data.runtime.schedule.nextAt}<p class="muted">Next scheduled restart: {$timeFormat.dateTime(data.runtime.schedule.nextAt)} · <a href="/maintenance?view=policies#schedule">Manage schedule</a></p>{/if}
|
||||
{#if data.recovery.lastScanAt}<p class="muted">Health and drift checked {$timeFormat.dateTime(data.recovery.lastScanAt)}</p>{/if}
|
||||
{#if data.recovery.scanError}<p class="ui-error" role="alert">{data.recovery.scanError}</p>{/if}
|
||||
{#if scanErrors.length}
|
||||
<section class="scan-errors" aria-labelledby="scan-errors-heading">
|
||||
<div class="row-heading"><h2 id="scan-errors-heading">Checks need attention</h2><Button size="sm" disabled={busy} onclick={() => act({ action: 'scan' })}>Retry checks</Button></div>
|
||||
<ul class="error-groups">{#each scanErrors as group (group.key)}
|
||||
<li><h3><StatusPip tone="warning" />{group.title}{group.servers.length ? ` · ${group.servers.length} ${group.servers.length === 1 ? 'server' : 'servers'}` : ''}</h3>
|
||||
<p>{group.description}</p>
|
||||
{#if group.servers.length}<ul class="affected-servers">{#each group.servers as name}<li><a href={contextHref(`/servers/${encodeURIComponent(name)}?tab=diagnostics`, $page.url)}>{serverTitle(name)}</a></li>{/each}</ul>{/if}
|
||||
<details><summary>Technical details</summary><ul class="technical-details">{#each group.details as detail}<li>{#if detail.server}<strong>{serverTitle(detail.server)}</strong>{/if}<pre>{detail.message}</pre></li>{/each}</ul></details>
|
||||
</li>
|
||||
{/each}</ul>
|
||||
</section>
|
||||
{/if}
|
||||
{/if}
|
||||
<div class="filters">
|
||||
<label>Capability<select bind:value={kind} onchange={() => saveContext()}><option value="all">All capabilities</option><option value="health">Health recovery</option><option value="drift">Configuration drift</option><option value="updates">Plutonium updates</option><option value="schedule">Scheduled restarts</option></select></label>
|
||||
@@ -171,19 +192,34 @@
|
||||
</div>
|
||||
{#if view === 'overview' && ['all', 'updates'].includes(kind)}
|
||||
<section class="card" id="updates">
|
||||
<div class="row-heading"><h2>Plutonium updates</h2><Button disabled={busy || data.policies.paused || !outdated.some(v => !activeJobs.some(j => j.server === v.server))} onclick={() => act({ action: 'update', servers: outdated.map(v => v.server) })}>Update outdated servers on restart</Button></div>
|
||||
<div class="row-heading"><h2>Plutonium updates</h2>{#if updatableServers.length}<Button disabled={busy || data.policies.paused} onclick={() => act({ action: 'update', servers: updatableServers })}>Update outdated servers on restart</Button>{/if}</div>
|
||||
<p>Published revision: <strong>{data.runtime.release.revision ? `r${data.runtime.release.revision}` : 'Unknown'}</strong> · <a href={PLUTONIUM_MANIFEST_URL} target="_blank" rel="noreferrer">Production manifest</a> · <a href="https://plutonium.pw/docs/changelog/" target="_blank" rel="noreferrer">Changelog</a></p>
|
||||
{#if data.runtime.release.checkedAt}<p class="muted">Checked {$timeFormat.dateTime(data.runtime.release.checkedAt)}</p>{/if}
|
||||
{#if data.runtime.release.error}<p class="ui-error" role="alert">{data.runtime.release.error} Displayed release information is cached.</p>{/if}
|
||||
<p class="muted">Plutainer applies the available release at startup when auto-update is enabled. Maintenance verifies the running revision after recovery. Unknown versions require a successful check before automatic updates.</p>
|
||||
<div class="table-wrap"><table><thead><tr><th>Server</th><th>Running revision</th><th>Status</th><th>Action</th></tr></thead><tbody>
|
||||
{#each versions as version (version.server)}
|
||||
{@const pending = activeJobs.find(j => j.server === version.server)}
|
||||
<tr><td><a href={`/servers/${encodeURIComponent(version.server)}`}><ServerIdentity name={version.server} /></a></td><td>{version.revision ? `r${version.revision}` : 'Unknown'}{#if version.checked_at}<small>{$timeFormat.dateTime(version.checked_at)}</small>{/if}{#if version.error}<small>{version.error}</small>{/if}</td>
|
||||
<td>{version.update_status === 'outdated' ? 'Update available' : version.update_status === 'current' ? 'Up to date' : version.update_status === 'ahead' ? 'Ahead of published release' : version.update_status === 'stopped' ? 'Intentionally stopped' : 'Unverified'}</td>
|
||||
<td>{#if pending}<a href={pending.provisioning_id ? `/provisioning/${encodeURIComponent(pending.provisioning_id)}` : '#activity'}>Restart pending</a>{:else if version.update_status === 'outdated'}<Button size="sm" disabled={busy || data.policies.paused} onclick={() => act({ action: 'update', servers: [version.server] })}>Update on restart</Button>{/if}</td></tr>
|
||||
{:else}<tr><td colspan="4">No matching fleet servers.</td></tr>{/each}
|
||||
</tbody></table></div>
|
||||
<div class="version-groups">{#each versionGroups as group (group.key)}
|
||||
<article class="version-group">
|
||||
<div class="row-heading">
|
||||
<div class="version-summary"><strong>{group.versions.length} {group.versions.length === 1 ? 'server' : 'servers'} · {group.revision ? `reported r${group.revision}` : 'revision unknown'}</strong><Badge variant={group.tone}><StatusPip tone={group.tone} size="sm" />{group.label}</Badge></div>
|
||||
<div class="version-actions">
|
||||
{#if group.pending.length}<span>{group.pending.length} {group.pending.length === 1 ? 'restart' : 'restarts'} pending</span>{/if}
|
||||
{#if group.status === 'unknown'}<Button size="sm" disabled={busy} onclick={() => act({ action: 'scan' })}>Retry version check</Button>
|
||||
{:else if group.updatable.length}<Button size="sm" disabled={busy || data.policies.paused} onclick={() => act({ action: 'update', servers: group.updatable })}>Update {group.updatable.length === 1 ? 'server' : `${group.updatable.length} servers`} on restart</Button>{/if}
|
||||
</div>
|
||||
</div>
|
||||
<details><summary>{group.versions.length === 1 ? 'Server and check details' : `Show ${group.versions.length} servers and check details`}</summary>
|
||||
<ul class="version-members">{#each group.versions as version (version.server)}
|
||||
{@const pending = group.pending.find(job => job.server === version.server)}
|
||||
<li><div class="row-heading"><a href={contextHref(`/servers/${encodeURIComponent(version.server)}`, $page.url)}><ServerIdentity name={version.server} /></a>
|
||||
{#if pending}<span class="pending-job"><StatusBadge state={pending.state} />{#if pending.provisioning_id}<a href={contextHref(`/provisioning/${encodeURIComponent(pending.provisioning_id)}`, $page.url)}>View restart</a>{:else}<a href="#activity">View pending restart</a>{/if}</span>{/if}
|
||||
</div>
|
||||
{#if version.checked_at}<small>Last checked <time datetime={version.checked_at}>{$timeFormat.dateTime(version.checked_at)}</time></small>{/if}
|
||||
{#if version.error}<pre class="version-error">{version.error}</pre>{/if}
|
||||
</li>
|
||||
{/each}</ul>
|
||||
</details>
|
||||
</article>
|
||||
{:else}<EmptyState title="No matching servers" description="Choose another server name to see its reported Plutonium revision." />{/each}</div>
|
||||
</section>
|
||||
{/if}
|
||||
<section id="activity">
|
||||
@@ -215,11 +251,29 @@
|
||||
.summary, .paused { padding: 20px; background: var(--surface-raised); border: 1px solid var(--border-strong); border-radius: var(--radius-md); }
|
||||
.paused { line-height: 1.7; }
|
||||
h2 { font-size: var(--text-md); } h3 { font-size: var(--text-sm); } h2, h3, p { margin: 0; }
|
||||
p, label, td, th { font-size: var(--text-sm); line-height: 1.6; }
|
||||
p, label { font-size: var(--text-sm); line-height: 1.6; }
|
||||
.muted, small { color: var(--text-secondary); } small { display: block; }
|
||||
a { color: var(--accent-primary-hover); } label { display: grid; gap: 6px; } .toggle { display: flex; align-items: center; }
|
||||
input:not([type='checkbox']), select { color: inherit; background: var(--surface); padding: 9px; border: 1px solid var(--border-strong); border-radius: var(--radius-sm); max-width: 100%; }
|
||||
.table-wrap { overflow-x: auto; } table { width: 100%; border-collapse: collapse; text-align: left; } td, th { padding: 12px 8px; border-bottom: 1px solid var(--border-subtle); }
|
||||
input:not([type='checkbox']), select { color: inherit; background: var(--surface-sunken); padding: 9px; border: 1px solid var(--border-control); border-radius: var(--radius-sm); max-width: 100%; }
|
||||
.scan-errors { padding: var(--space-4); border: 1px solid color-mix(in srgb, var(--state-warning) 35%, var(--border-subtle)); border-radius: var(--radius-md); background: var(--surface); }
|
||||
.error-groups, .technical-details, .version-members { list-style: none; padding: 0; margin: 0; }
|
||||
.error-groups { display: grid; gap: var(--space-4); }
|
||||
.error-groups > li + li { border-top: 1px solid var(--border-subtle); padding-top: var(--space-4); }
|
||||
.error-groups h3 { display: flex; align-items: center; gap: var(--space-2); margin-bottom: var(--space-2); }
|
||||
.error-groups p { color: var(--text-secondary); }
|
||||
.affected-servers { display: flex; flex-wrap: wrap; gap: var(--space-2) var(--space-4); padding: 0; margin: var(--space-3) 0; list-style: none; font-size: var(--text-sm); }
|
||||
details { margin-top: var(--space-2); min-width: 0; }
|
||||
summary { cursor: pointer; color: var(--text-secondary); font-size: var(--text-xs); }
|
||||
.technical-details > li { margin-top: var(--space-3); font-size: var(--text-xs); }
|
||||
pre { max-width: 100%; white-space: pre-wrap; overflow-wrap: anywhere; font-family: var(--font-mono); font-size: var(--text-xs); line-height: var(--leading-mono); color: var(--text-secondary); margin: var(--space-2) 0 0; }
|
||||
.version-groups { display: grid; gap: var(--space-2); }
|
||||
.version-group { min-width: 0; padding: var(--space-3) var(--space-4); border: 1px solid var(--border-subtle); border-radius: var(--radius-md); }
|
||||
.version-summary, .version-actions, .pending-job { display: flex; align-items: center; flex-wrap: wrap; gap: var(--space-2); font-size: var(--text-sm); }
|
||||
.version-summary strong { font-weight: var(--weight-medium); font-variant-numeric: tabular-nums; }
|
||||
.version-actions { font-size: var(--text-xs); color: var(--text-secondary); }
|
||||
.version-members > li { padding: var(--space-3) 0; }
|
||||
.version-members > li + li { border-top: 1px solid var(--border-subtle); }
|
||||
.version-members small { margin-top: var(--space-1); font-size: var(--text-xs); }
|
||||
.activity article, section { scroll-margin-top: 100px; }
|
||||
@media (max-width: 640px) { .card { padding: 18px; } }
|
||||
</style>
|
||||
|
||||
@@ -158,7 +158,7 @@
|
||||
<Button onclick={() => lifecycleOpen = true}>Server actions</Button><Button variant="primary" onclick={() => open = true}>Create server</Button>
|
||||
<details class="fleet-actions" bind:open={fleetActionsOpen}><summary>Fleet actions</summary><div class="fleet-menu">
|
||||
{#if active.some(row => row.canCancel)}<Button disabled={cancelling} onclick={() => cancelRequests(active.filter(row => row.canCancel && !row.server_deleted_at), true)}>Cancel all active operations</Button>{/if}
|
||||
<Button variant="danger" onclick={() => { fleetActionsOpen = false; housePartyOpen = true; }}>Destroy fleet…</Button>
|
||||
<Button variant="danger" onclick={() => { fleetActionsOpen = false; housePartyOpen = true; }}>Remove fleet servers…</Button>
|
||||
</div></details>
|
||||
{/snippet}
|
||||
</SectionHeader>
|
||||
|
||||
@@ -23,9 +23,11 @@
|
||||
import { page } from '$app/stores';
|
||||
import { goto } from '$app/navigation';
|
||||
import LifecycleDialog from '$ui/components/LifecycleDialog.svelte';
|
||||
import { runtimeActions, actionLabels, type RuntimeAction } from '$lib/provisioning.js';
|
||||
import { actionLabels, type RuntimeAction } from '$lib/provisioning.js';
|
||||
let lifecycleOpen = $state(false);
|
||||
let lifecycleAction = $state<RuntimeAction>('start');
|
||||
import { applicableRuntimeActions } from '$ui/lifecycle-actions.js';
|
||||
import { serverHealth } from '$ui/server-health.js';
|
||||
import DestroyServerModal from '$ui/components/DestroyServerModal.svelte';
|
||||
import { telemetryByServer, uiNow } from '$ui/stores/servers.js';
|
||||
import type { RosterPlayer, Health, RconEndpoint, RconFailure } from '$ui/stores/servers.js';
|
||||
@@ -39,7 +41,6 @@
|
||||
import Badge from '$ui/components/primitives/Badge.svelte';
|
||||
import Button from '$ui/components/primitives/Button.svelte';
|
||||
import Tabs from '$ui/components/primitives/Tabs.svelte';
|
||||
import StatusPip from '$ui/components/primitives/StatusPip.svelte';
|
||||
import SectionHeader from '$ui/components/primitives/SectionHeader.svelte';
|
||||
import EmptyState from '$ui/components/primitives/EmptyState.svelte';
|
||||
|
||||
@@ -91,7 +92,24 @@
|
||||
});
|
||||
const snapshot = $derived($fleet.receivedAt ? $fleet : data.serverSnapshot);
|
||||
const visibleFleet = $derived($fleet.receivedAt ? $fleetServers : data.serverSnapshot.servers);
|
||||
let server = $derived((visibleFleet.find(s => s.name === name) as Server | undefined) ?? null);
|
||||
const serverContext = $derived(visibleFleet.find(s => s.name === name) ?? null);
|
||||
let server = $derived((serverContext as Server | null));
|
||||
const health = $derived(serverContext ? serverHealth(serverContext, $uiNow) : null);
|
||||
const canOperate = $derived($page.data.user?.role === 'admin' || $page.data.user?.role === 'operator');
|
||||
const availableActions = $derived(serverContext ? applicableRuntimeActions(serverContext, $uiNow) : []);
|
||||
const primaryAction = $derived(availableActions.includes('restart') ? 'restart' : availableActions.includes('start') ? 'start' : null);
|
||||
function openAction(action: RuntimeAction) { if (canOperate && availableActions.includes(action)) { lifecycleAction = action; lifecycleOpen = true; } }
|
||||
let consumedAction = '';
|
||||
$effect(() => {
|
||||
const requested = $page.url.searchParams.get('action');
|
||||
const key = `${name}:${requested}`;
|
||||
if (requested !== 'start') { consumedAction = ''; return; }
|
||||
if (requested === 'start' && canOperate && availableActions.includes('start') && consumedAction !== key) {
|
||||
consumedAction = key; openAction('start');
|
||||
const url = new URL($page.url); url.searchParams.delete('action');
|
||||
void goto(url, { replaceState: true, noScroll: true, keepFocus: true });
|
||||
}
|
||||
});
|
||||
const native = $derived(!!server?.native_instance_id);
|
||||
let telemetryOpen = $state(false);
|
||||
let history = $state<DeploymentRow[]>([]);
|
||||
@@ -159,7 +177,7 @@
|
||||
$effect(() => { name; rconProbe = null; });
|
||||
const fresh = $derived(server ? freshServer(server, $uiNow) : false);
|
||||
|
||||
let effectivePlayers = $derived(fresh ? server?.last_players ?? null : null);
|
||||
let effectivePlayers = $derived(fresh && health?.state === 'online' ? server?.last_players ?? null : null);
|
||||
const scoreboard = $derived(server?.telemetry ?? $telemetryByServer.get(name));
|
||||
let effectiveHealth = $derived<Health>(server?.health === 'online' && !fresh ? 'stale' : server?.health ?? 'offline');
|
||||
let effectiveSeen = $derived(server?.last_seen_at ?? null);
|
||||
@@ -182,7 +200,8 @@
|
||||
let gameInProgress = $derived(!!effectiveGameStartedAt && !effectiveGameEndedAt && !!effectiveActiveSince);
|
||||
let gameElapsed = $derived(fresh && gameInProgress ? elapsedSince(effectiveGameStartedAt, $uiNow) : null);
|
||||
|
||||
let hasDiagnosticIssue = $derived(effectiveHealth !== 'online' && effectiveHealth !== 'starting' && effectiveHealth !== 'provisioning');
|
||||
const currentRound = $derived(effectivePlayers !== null && scoreboard && 'round' in scoreboard && freshTelemetry(scoreboard, $uiNow, server?.telemetry_after) ? scoreboard.round : null);
|
||||
let hasDiagnosticIssue = $derived(!!health && !['online', 'starting', 'provisioning', 'stopped'].includes(health.state));
|
||||
|
||||
function ago(iso: string | null, now: number): string {
|
||||
if (!iso) return 'never';
|
||||
@@ -218,30 +237,33 @@
|
||||
</script>
|
||||
|
||||
<svelte:head><title>{serverTitle(name)} · {activeTab.charAt(0).toUpperCase() + activeTab.slice(1)} · Electric Cherry</title></svelte:head>
|
||||
<LifecycleDialog bind:open={lifecycleOpen} initialServer={name} initialAction={lifecycleAction} />
|
||||
<DestroyServerModal bind:open={destroyOpen} {name} players={effectivePlayers} />
|
||||
<LifecycleDialog bind:open={lifecycleOpen} initialServer={name} initialAction={lifecycleAction} {serverContext} players={effectivePlayers} round={currentRound} />
|
||||
<DestroyServerModal bind:open={destroyOpen} {name} players={effectivePlayers} round={currentRound} />
|
||||
|
||||
{#if server}
|
||||
<div class="server-page" style="view-transition-name: server-{name.replace(/[^a-z0-9_-]/gi, '-')};">
|
||||
<header class="instance-header">
|
||||
<Breadcrumbs items={[{ label: 'Fleet', href: '/' }, { label: serverTitle(name) }]} />
|
||||
<Breadcrumbs items={[{ label: 'Servers', href: '/' }, { label: serverTitle(name) }]} />
|
||||
<SectionHeader title={serverTitle(server.name)}>
|
||||
{#snippet actions()}
|
||||
<details class="server-actions"><summary>Instance actions</summary><div class="action-menu">{#each runtimeActions as action}<Button onclick={() => { lifecycleAction = action; lifecycleOpen = true; }}>{actionLabels[action]}</Button>{/each}<Button variant="danger" onclick={() => destroyOpen = true}>Queue instance removal</Button></div></details>
|
||||
{#if canOperate}
|
||||
{#if primaryAction}<Button onclick={() => openAction(primaryAction)}>{actionLabels[primaryAction]} server</Button>{/if}
|
||||
<details class="server-actions"><summary>More actions</summary><div class="action-menu">{#each availableActions.filter(action => action !== primaryAction) as action}<Button onclick={() => openAction(action)}>{actionLabels[action]} server</Button>{/each}<Button variant="danger" onclick={() => destroyOpen = true}>Remove server</Button></div></details>
|
||||
{/if}
|
||||
{/snippet}
|
||||
</SectionHeader>
|
||||
<div class="instance-meta">
|
||||
<span class="health"><StatusPip health={effectiveHealth} size="md" />{effectiveHealth === 'online' ? 'Online' : effectiveHealth === 'starting' ? 'Starting' : effectiveHealth === 'provisioning' ? 'Provisioning' : effectiveHealth === 'stale' ? 'Stale' : 'Offline'}</span>
|
||||
<StatusBadge state={health?.state ?? 'unknown'} />
|
||||
{#if server.host_id}<a href={contextHref(`/hosts/${server.host_id}`, $page.url)}>{server.host?.name ?? server.host_id}</a>{/if}
|
||||
<details><summary>Details</summary><div class="instance-details"><CopyValue value={name} label="Instance ID" /><RuntimeVersion {name} version={server.plutonium} after={server.telemetry_after} ready={effectiveHealth === 'online'} /></div></details>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{#if $fleetError}<div class="ui-error" role="alert">{$fleetError}<Button size="sm" onclick={() => refreshFleet(true)}>Retry</Button></div>{/if}
|
||||
{#if hasDiagnosticIssue}
|
||||
{#if hasDiagnosticIssue && health}
|
||||
<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={contextHref(`/hosts/${server.host_id}`, $page.url)}>Check host</Button>{:else}<Button size="sm" href="/settings">Check system setup</Button>{/if}</div>
|
||||
<div><strong>{health.label}</strong><p>{health.reason} {health.since ? `Last evidence ${ago(health.since, $uiNow)}.` : 'No current report is available.'}</p></div>
|
||||
<div class="issue-actions">{#if health.state === 'host-offline' && server.host_id}<Button size="sm" href={contextHref(`/hosts/${server.host_id}`, $page.url)}>Check host</Button>{:else}<Button size="sm" onclick={() => changeTab('diagnostics')}>Open diagnostics</Button>{/if}<Button size="sm" variant="ghost" onclick={() => changeTab('logs')}>Open logs</Button></div>
|
||||
</section>
|
||||
{/if}
|
||||
<div class="tabs-row">
|
||||
@@ -259,7 +281,7 @@
|
||||
</section>
|
||||
<section id="chat" aria-label="Chat">
|
||||
<Card>
|
||||
{#key `${name}:${scoreboard?.timing?.matchId ?? ''}:${effectiveGameStartedAt ?? ''}`}<ChatPanel server={name} />{/key}
|
||||
{#key `${name}:${scoreboard?.timing?.matchId ?? ''}:${effectiveGameStartedAt ?? ''}`}<ChatPanel server={name} hostName={server.host?.name} hostOffline={health?.state === 'host-offline'} />{/key}
|
||||
</Card>
|
||||
</section>
|
||||
{:else if activeSection === 'configuration'}
|
||||
@@ -529,8 +551,7 @@
|
||||
|
||||
.live-context { margin: 0; font-size: var(--text-sm); color: var(--text-secondary); }
|
||||
.instance-header { display: grid; gap: 12px; }
|
||||
.instance-meta, .health { display: flex; align-items: center; flex-wrap: wrap; gap: 12px; font-size: var(--text-sm); }
|
||||
.health { gap: 8px; }
|
||||
.instance-meta { display: flex; align-items: center; flex-wrap: wrap; gap: 12px; font-size: var(--text-sm); }
|
||||
.instance-details { display: grid; gap: 8px; padding: 12px 0; }
|
||||
details > summary { cursor: pointer; color: var(--text-secondary); font-size: var(--text-sm); }
|
||||
.server-actions { position: relative; }
|
||||
|
||||
Reference in New Issue
Block a user