From 913dc99181b87b4ad07101677d3f45c9ce7f54c0 Mon Sep 17 00:00:00 2001 From: Josh Wright Date: Mon, 5 Oct 2026 19:09:04 -0400 Subject: [PATCH] Group server and host details into task sections --- src/lib/server-sections.ts | 13 ++++ src/routes/game-files/+page.svelte | 2 + src/routes/hosts/[id]/+page.svelte | 73 ++++++++++++------ src/routes/servers/[name]/+page.svelte | 102 +++++++++++++++---------- tests/unit/server-sections.test.ts | 19 +++++ 5 files changed, 147 insertions(+), 62 deletions(-) create mode 100644 src/lib/server-sections.ts create mode 100644 tests/unit/server-sections.test.ts diff --git a/src/lib/server-sections.ts b/src/lib/server-sections.ts new file mode 100644 index 0000000..2951bbd --- /dev/null +++ b/src/lib/server-sections.ts @@ -0,0 +1,13 @@ +export type ServerSection = 'live' | 'matches' | 'configuration' | 'diagnostics' | 'history'; + +/** Legacy URLs still select their original content inside the new section. */ +export function serverSection(tab: string | null): ServerSection { + if (tab === 'matches' || tab === 'history' || tab === 'diagnostics') return tab; + if (tab === 'config' || tab === 'inventory' || tab === 'configuration') return 'configuration'; + if (tab === 'logs') return 'diagnostics'; + return 'live'; +} + +export function serverSectionAnchor(tab: string | null): string | null { + return tab === 'chat' || tab === 'logs' || tab === 'inventory' ? tab : null; +} diff --git a/src/routes/game-files/+page.svelte b/src/routes/game-files/+page.svelte index ada224a..6f34059 100644 --- a/src/routes/game-files/+page.svelte +++ b/src/routes/game-files/+page.svelte @@ -1,4 +1,5 @@ {host?.name ?? 'Host'} · Electric Cherry -
← All hosts +
+ {#snippet crumbs()}{/snippet} {#snippet actions()}{#if canManage && host && (!cleaning || host.decommission?.retryable)}{/if}{/snippet} + {#if error}{/if}{#if notice}

{notice}

{/if} {#if loading}

Loading host…

{:else if host} {#if host.decommission}

Host cleanup

{hostDeletionMessage(host)}

{#if host.decommission.error}

{host.decommission.error}

{/if}

Requested {new Date(host.decommission.requestedAt).toLocaleString()}. This page closes only after the cleanup worker confirms removal.

{#if host.decommission.stage === 'cleaned'}

The agent is expected to go offline during uninstall. If final confirmation never arrives, inspect the machine; loss of contact alone does not prove completion.

{/if}
{/if}

{host.connectionState} · {hostReadinessLabel(host.readiness)}

Last heartbeat: {host.lastHeartbeat ? new Date(host.lastHeartbeat).toLocaleString() : 'Never'}
{#if host.readinessReasons.length}
    {#each host.readinessReasons as reason}
  • {reason}
  • {/each}
{:else}

Connected and ready. Each deployment also checks its latest game files, runtime, and ports.

{/if} - {#if canManage && !cleaning}
{/if} -

Draining blocks new deployments and migrates empty instances to other hosts with capacity. The host disables when migration is complete and it is empty. Enabling refreshes the runtime and verifies BO1 and BO2 game files before returning the host to the capacity pool.

+ {#if host.readiness !== 'ready' && tab !== 'setup'}{/if}
+ +
+ {#if tab === 'overview'} +
+

Inventory

+
Hostname
{host.hostname || 'Pending'}
Agent
{host.inventory.agentVersion ?? 'Unknown'}
Platform
{host.inventory.os ?? 'Unknown'} · {host.inventory.architecture ?? 'Unknown'}
CPU
{host.inventory.cpu ?? 'Unknown'} ({host.inventory.cpuCount ?? '?'} cores)
CPU usage
{host.inventory.cpuUsagePercent === undefined ? 'Not reported' : `${host.inventory.cpuUsagePercent.toFixed(1)}%`}{#if !host.connected && host.inventory.cpuUsagePercent !== undefined} (last reported){/if}
Memory available / total
{bytes(host.inventory.memoryAvailable)} / {bytes(host.inventory.memoryTotal)}
Game storage available / total
{bytes(host.inventory.storageAvailable)} / {bytes(host.inventory.storageTotal)}
Docker
{host.inventory.dockerVersion ?? 'Unknown'} · {host.inventory.dockerRunning ? 'Running' : 'Unavailable'}
Current operation
{host.inventory.currentOperation ?? 'None reported'}
+
{#if calibration}

Learned server capacity

+

Allowances update from observed container usage. Hosts reserve 5% CPU and 500 MB of memory. Startup and running usage are tracked separately.

+
{#each ['startup', 'running'] as phase}{@const allowance = calibration[phase as keyof CapacityCalibration]} +
{phase === 'startup' ? 'Starting a server' : 'Running a server'}
+
{allowance.cpuCores.toFixed(2)} CPU cores · {bytes(allowance.memoryBytes)} · {allowance.source === 'host' ? 'Learned on this host' : allowance.source === 'fleet' ? 'Learned from the fleet' : 'Calibrating; temporary allowance'} ({allowance.samples} samples)
+ {/each}
+ {#if host.inventory.instanceResourcesError}

{host.inventory.instanceResourcesError}

{/if} + {#if calibration.startup.source === 'bootstrap' || calibration.running.source === 'bootstrap'}

Use agent 1.4.6 or later for resource sampling. Measurements arrive with agent heartbeats and are combined into one peak sample per container per minute. The first five minutes after a container starts count toward startup; later measurements count toward running. Temporary allowances stay in use until enough measurements are available.

{/if} + {#key host.id}{/key} +
{/if} + {:else if tab === 'servers'}

Running instances · {instances.length}

Current instances on this host, including any that are starting or unavailable.

@@ -84,35 +114,31 @@
{:else}

No instances are currently deployed on this host.

{/if}
+ {:else if tab === 'setup'} {#if !cleaning}

Automatic host setup

{/if} - {#if calibration}

Learned server capacity

-

Allowances update from observed container usage. Hosts reserve 5% CPU and 500 MB of memory. Startup and running usage are tracked separately.

-
{#each ['startup', 'running'] as phase}{@const allowance = calibration[phase as keyof CapacityCalibration]} -
{phase === 'startup' ? 'Starting a server' : 'Running a server'}
-
{allowance.cpuCores.toFixed(2)} CPU cores · {bytes(allowance.memoryBytes)} · {allowance.source === 'host' ? 'Learned on this host' : allowance.source === 'fleet' ? 'Learned from the fleet' : 'Calibrating; temporary allowance'} ({allowance.samples} samples)
- {/each}
- {#if host.inventory.instanceResourcesError}

{host.inventory.instanceResourcesError}

{/if} - {#if calibration.startup.source === 'bootstrap' || calibration.running.source === 'bootstrap'}

Use agent 1.4.6 or later for resource sampling. Measurements arrive with agent heartbeats and are combined into one peak sample per container per minute. The first five minutes after a container starts count toward startup; later measurements count toward running. Temporary allowances stay in use until enough measurements are available.

{/if} - {#key host.id}{/key} -
{/if} -

Inventory

-
Hostname
{host.hostname || 'Pending'}
Agent
{host.inventory.agentVersion ?? 'Unknown'}
Platform
{host.inventory.os ?? 'Unknown'} · {host.inventory.architecture ?? 'Unknown'}
CPU
{host.inventory.cpu ?? 'Unknown'} ({host.inventory.cpuCount ?? '?'} cores)
CPU usage
{host.inventory.cpuUsagePercent === undefined ? 'Not reported' : `${host.inventory.cpuUsagePercent.toFixed(1)}%`}{#if !host.connected && host.inventory.cpuUsagePercent !== undefined} (last reported){/if}
Memory available / total
{bytes(host.inventory.memoryAvailable)} / {bytes(host.inventory.memoryTotal)}
Game storage available / total
{bytes(host.inventory.storageAvailable)} / {bytes(host.inventory.storageTotal)}
Docker
{host.inventory.dockerVersion ?? 'Unknown'} · {host.inventory.dockerRunning ? 'Running' : 'Unavailable'}
Current operation
{host.inventory.currentOperation ?? 'None reported'}
-

Host configuration

{#if canManage}
-
-
{:else}
Friendly name
{host.name}
Game ports
{host.portMin}–{host.portMax}
Local bind address
{host.bindAddress}
Public / NAT address
{host.publicAddress || 'Not configured'}
Minimum free storage
{bytes(host.minimumFreeBytes)}

Host configuration is managed by administrators.

{/if}

Public addressing is separate from local port reservations. Configure your firewall and NAT for the game-port range.

Automatic agent updates · {host.enrolled ? hostAgentUpdateLabel(host.inventory) : 'Awaiting installation'}

Running version
{host.inventory.agentVersion ?? host.inventory.agentUpdate?.currentVersion ?? 'Not reported'}
Target version
{host.inventory.agentUpdate?.targetVersion ?? 'None reported'}
Last update check
{host.inventory.agentUpdate?.checkedAt ? new Date(host.inventory.agentUpdate.checkedAt).toLocaleString() : 'Not reported'}
{#if !host.connected && host.enrolled}

This is the last reported update status. The host is offline.

{/if} {#if host.inventory.agentUpdate?.error}

{host.inventory.agentUpdate.error}

{/if} {#if host.enrolled}

{hostAgentUpdateMessage(host.inventory)}

{:else}

The installer enables automatic updates when this host first enrolls.

{/if} - {#if host.enrolled && !hostAgentUpdatesEnabled(host.inventory)}

{canManage ? 'Generate an installer under Agent access below, run it on this host, and wait for a fresh heartbeat. The installer preserves the existing host identity and game data.' : 'An administrator can generate an installer under Agent access and run it on this host.'}

{:else if hostAgentUpdatesEnabled(host.inventory)}

The host checks this controller about every five minutes. Updates replace agent code between operations; running game containers continue.

{/if} + {#if host.enrolled && !hostAgentUpdatesEnabled(host.inventory)}

{canManage ? 'Generate an installer under Agent access in Settings, run it on this host, and wait for a fresh heartbeat. The installer preserves the existing host identity and game data.' : 'An administrator can generate an installer under Agent access and run it on this host.'}

{:else if hostAgentUpdatesEnabled(host.inventory)}

The host checks this controller about every five minutes. Updates replace agent code between operations; running game containers continue.

{/if}
-

Cached game files and runtimes

{#if host.inventory.libraries?.length}
    {#each host.inventory.libraries as library}
  • {library.id}: {library.state}{library.progress !== undefined ? ` (${library.progress}%)` : ''}
  • {/each}
{:else}

No cached game libraries reported yet.

{/if} +

Cached game files and runtimes

{#if host.inventory.libraries?.length}
    {#each host.inventory.libraries as library}
  • {library.id}: {library.state}{library.progress !== undefined ? ` (${library.progress}%)` : ''}
  • {/each}
{:else}

No cached game libraries reported yet.

{/if}

Cached images: {host.inventory.runtimeImages?.join(', ') || 'None reported yet'}

{#if canManage}
Advanced: custom runtime or manual retry

The configured default runtime is prepared automatically. Use this to prepare another image or retry a pull now.

{ event.preventDefault(); void action('/operations', 'POST', { kind: 'runtime.pull', image: runtimeImage }, 'Runtime download queued.'); }}>
{/if} -

Agent access

Credentials: {host.credentialActive ? 'Active' : 'Not active'}. Enrollment links expire after 15 minutes and work once.

{#if canManage}
+
+
+ {:else if tab === 'settings'} +

Host availability

+ {#if canManage && !cleaning}
{/if} +

Draining blocks new deployments and migrates empty instances to other hosts with capacity. The host disables when migration is complete and it is empty. Enabling refreshes the runtime and verifies BO1 and BO2 game files before returning the host to the capacity pool.

+

Host configuration

{#if canManage}
+
+
{:else}
Friendly name
{host.name}
Game ports
{host.portMin}–{host.portMax}
Local bind address
{host.bindAddress}
Public / NAT address
{host.publicAddress || 'Not configured'}
Minimum free storage
{bytes(host.minimumFreeBytes)}

Host configuration is managed by administrators.

{/if}

Public addressing is separate from local port reservations. Configure your firewall and NAT for the game-port range.

Agent access

Credentials: {host.credentialActive ? 'Active' : 'Not active'}. Enrollment links expire after 15 minutes and work once.

{#if canManage}
{#if enrollment}

Expires {new Date(enrollment.expiresAt).toLocaleString()}

{enrollment.command}
{/if}{:else}

Administrators manage enrollment and agent credentials.

{/if} -
+
+ {/if} + {/if} { if (result.deleted) await goto('/hosts'); else { enrollment = null; await load(); } }} /> @@ -125,5 +151,6 @@ diff --git a/src/routes/servers/[name]/+page.svelte b/src/routes/servers/[name]/+page.svelte index a62ae96..4695b3f 100644 --- a/src/routes/servers/[name]/+page.svelte +++ b/src/routes/servers/[name]/+page.svelte @@ -1,4 +1,6 @@ @@ -289,32 +299,29 @@ {/if}
- +
-
- {#if tabError}{/if} - {#if activeTab === 'scoreboard'} +
+ {#if tabError}{/if} + {#if activeSection === 'live'} + +
{#key name}{/key} - {:else if activeTab === 'chat'} +
+
{#key name}{/key} - {:else if activeTab === 'logs'} - - - - {:else if activeTab === 'config'} +
+ {:else if activeSection === 'configuration'} + +
{#key name}{/key} - {:else if activeTab === 'inventory'} +
+

Installed files

@@ -339,7 +346,10 @@ {/if} - {:else if activeTab === 'diagnostics'} +
+ {:else if activeSection === 'diagnostics'} + +

RCON diagnostic

@@ -414,9 +424,21 @@
{#key name}{/key}
- {:else if activeTab === 'matches'} +
+
+ + + +
+ {:else if activeSection === 'matches'} {#key name}{/key} - {:else if activeTab === 'history'} + {:else if activeSection === 'history'} {#if operations.length} @@ -478,6 +500,8 @@ {/if}