Group server and host details into task sections

This commit is contained in:
2026-10-05 19:09:15 -04:00
parent cd577b646c
commit 913dc99181
5 changed files with 147 additions and 62 deletions
+63 -39
View File
@@ -1,4 +1,6 @@
<script lang="ts">
import { tick } from 'svelte';
import { serverSection, serverSectionAnchor } from '$lib/server-sections.js';
import { contextHref } from '$lib/navigation-context.js';
import BackLink from '$ui/components/BackLink.svelte';
let { data }: { data: import('./$types').PageData } = $props();
@@ -96,11 +98,22 @@
let rconProbe = $state<RconProbeResult | null>(null);
let probingRcon = $state(false);
let destroyOpen = $state(false);
const sectionIds = ['scoreboard', 'matches', 'chat', 'logs', 'config', 'inventory', 'diagnostics', 'history'];
const activeTab = $derived(sectionIds.includes($page.url.searchParams.get('tab') ?? '') ? $page.url.searchParams.get('tab')! : 'scoreboard');
const sectionIds = ['live', 'scoreboard', 'matches', 'chat', 'logs', 'configuration', 'config', 'inventory', 'diagnostics', 'history'];
const activeTab = $derived(sectionIds.includes($page.url.searchParams.get('tab') ?? '') ? $page.url.searchParams.get('tab')! : 'live');
const activeSection = $derived(serverSection(activeTab));
const loadedServerName = $derived(server?.name);
$effect(() => {
const anchor = serverSectionAnchor(activeTab);
const currentName = loadedServerName;
if (!anchor || !currentName) return;
let cancelled = false;
void tick().then(() => { if (!cancelled) document.getElementById(anchor)?.scrollIntoView({ block: 'start' }); });
return () => { cancelled = true; };
});
function changeTab(tab: string) {
const url = new URL($page.url);
url.searchParams.set('tab', tab);
url.hash = '';
void goto(url, { noScroll: true, keepFocus: true });
}
let tabLoading = $state(false);
@@ -112,32 +125,32 @@
let tabSequence = 0;
async function loadTab(currentName: string, tab: string, signal?: AbortSignal) {
if (tab !== 'history' && tab !== 'inventory') return;
if (tab !== 'history' && tab !== 'configuration') return;
const sequence = ++tabSequence;
try {
if (tab === 'history') {
const body = await fetchCampaigns<DeploymentRow>({ server: currentName }, historyLimit, signal);
if (sequence !== tabSequence || name !== currentName || activeTab !== tab || signal?.aborted) return;
if (sequence !== tabSequence || name !== currentName || activeSection !== tab || signal?.aborted) return;
history = body.deployments; historyTotal = body.total;
} else {
const response = await fetch('/api/servers/' + encodeURIComponent(currentName) + '/scripts', { signal });
if (!response.ok) throw new Error('Could not refresh inventory.');
const body = await response.json();
if (sequence !== tabSequence || name !== currentName || activeTab !== tab || signal?.aborted) return;
if (sequence !== tabSequence || name !== currentName || activeSection !== tab || signal?.aborted) return;
inventory = body.scripts;
}
tabError = '';
return true;
} catch (error) {
if (sequence === tabSequence && name === currentName && activeTab === tab && !signal?.aborted) tabError = (error as Error).message;
if (sequence === tabSequence && name === currentName && activeSection === tab && !signal?.aborted) tabError = (error as Error).message;
return false;
} finally { if (sequence === tabSequence && name === currentName && activeTab === tab && !signal?.aborted) tabLoading = false; }
} finally { if (sequence === tabSequence && name === currentName && activeSection === tab && !signal?.aborted) tabLoading = false; }
}
$effect(() => {
const currentName = name;
const tab = activeTab;
const tab = activeSection;
tabError = ''; history = []; inventory = []; historyLimit = 25; inventoryLimit = 25;
tabLoading = tab === 'history' || tab === 'inventory';
tabLoading = tab === 'history' || tab === 'configuration';
return pollVisible(signal => loadTab(currentName, tab, signal), 10000);
});
$effect(() => { name; rconProbe = null; });
@@ -196,14 +209,11 @@
}
// Tab config — Diagnostics gets a warning badge when there's an issue.
let tabs = $derived([
{ id: 'scoreboard', label: 'Scoreboard', badge: null },
{ id: 'chat', label: 'Chat' },
{ id: 'matches', label: 'Matches' },
{ id: 'logs', label: 'Logs' },
{ id: 'config', label: 'Config' },
{ id: 'inventory', label: 'Inventory', badge: inventory.length || null },
{ id: 'diagnostics',label: 'Diagnostics', badge: hasDiagnosticIssue ? '!' : null },
{ id: 'history', label: 'History', badge: history.length || null }
{ id: 'live', label: 'Live' },
{ id: 'matches', label: 'Matches' },
{ id: 'configuration', label: 'Configuration' },
{ id: 'diagnostics', label: 'Diagnostics', badge: hasDiagnosticIssue ? '!' : null },
{ id: 'history', label: 'History' }
]);
</script>
@@ -289,32 +299,29 @@
</section>
{/if}
<div class="tabs-row">
<Tabs {tabs} value={activeTab} onchange={changeTab} ariaLabel="Server sections" idPrefix="server-tabs" panelId="server-panel" />
<Tabs {tabs} value={activeSection} onchange={changeTab} ariaLabel="Server sections" idPrefix="server-tabs" panelId="server-panel" />
</div>
<div class="tab-panel" class:diagnostics={activeTab === 'diagnostics'} class:history-panel={activeTab === 'history'} role="tabpanel" id="server-panel" aria-labelledby={"server-tabs-" + activeTab}>
{#if tabError}<div class="ui-error" role="alert">{tabError}<Button size="sm" onclick={() => loadTab(name, activeTab)}>Retry</Button></div>{/if}
{#if activeTab === 'scoreboard'}
<div class="tab-panel" class:diagnostics={activeSection === 'diagnostics'} class:history-panel={activeSection === 'history'} role="tabpanel" id="server-panel" aria-labelledby={"server-tabs-" + activeSection}>
{#if tabError}<div class="ui-error" role="alert">{tabError}<Button size="sm" onclick={() => loadTab(name, activeSection)}>Retry</Button></div>{/if}
{#if activeSection === 'live'}
<nav class="section-links" aria-label="Live sections"><a href="#scoreboard">Scoreboard</a><a href="#chat">Chat</a></nav>
<section id="scoreboard" aria-label="Scoreboard">
<Card padding="lg">
{#key name}<PlayerScoreboard current={fresh} after={server?.telemetry_after ?? null} report={scoreboard} mapLabel={scoreboard ? fleetIdentity({ ...server, last_map: scoreboard.map }).map : undefined} />{/key}
</Card>
{:else if activeTab === 'chat'}
</section>
<section id="chat" aria-label="Chat">
<Card padding="lg">
{#key name}<ChatPanel server={name} />{/key}
</Card>
{:else if activeTab === 'logs'}
<Card padding="lg">
<LogPanel
server={server.name}
tabs={[
{ key: 'console', label: 'console.log' },
server.game === 't6' ? { key: 'games_zm', label: 'games_zm.log' } : { key: 'games_sp', label: 'games_sp.log' }
]}
/>
</Card>
{:else if activeTab === 'config'}
</section>
{:else if activeSection === 'configuration'}
<nav class="section-links" aria-label="Configuration sections"><a href="#config">Config</a><a href="#inventory">Inventory</a></nav>
<section id="config" aria-label="Config">
{#key name}<ConfigInspector fixedServer={name} />{/key}
{:else if activeTab === 'inventory'}
</section>
<section id="inventory" aria-label="Inventory">
<Card padding="lg">
<div class="panel-head">
<h2>Installed files</h2>
@@ -339,7 +346,10 @@
{/if}
</Card>
<ListFooter total={inventory.length} bind:limit={inventoryLimit} label="files" />
{:else if activeTab === 'diagnostics'}
</section>
{:else if activeSection === 'diagnostics'}
<nav class="section-links" aria-label="Diagnostic sections"><a href="#connection">Connection health</a><a href="#logs">Logs</a></nav>
<section id="connection" aria-label="Connection health">
<Card padding="lg">
<div class="panel-head">
<h2>RCON diagnostic</h2>
@@ -414,9 +424,21 @@
</div>
</Card>
<div id="telemetry">{#key name}<TelemetryDiagnostics server={name} />{/key}</div>
{:else if activeTab === 'matches'}
</section>
<section id="logs" aria-label="Logs">
<Card padding="lg">
<LogPanel
server={server.name}
tabs={[
{ key: 'console', label: 'console.log' },
server.game === 't6' ? { key: 'games_zm', label: 'games_zm.log' } : { key: 'games_sp', label: 'games_sp.log' }
]}
/>
</Card>
</section>
{:else if activeSection === 'matches'}
{#key name}<MatchHistory server={name} />{/key}
{:else if activeTab === 'history'}
{:else if activeSection === '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">
@@ -478,6 +500,8 @@
{/if}
<style>
.section-links { display:flex; gap:20px; flex-wrap:wrap; font-size:var(--text-sm); }
.tab-panel > section { display:grid; gap:16px; min-width:0; scroll-margin-top:80px; }
.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); }
@@ -527,8 +551,8 @@
}
/* Tabs row */
.tab-panel.diagnostics,
.tab-panel.history-panel {
.tab-panel {
min-width: 0;
display: grid;
gap: var(--space-6);
}