Group server and host details into task sections
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user