<svelte:head><title>Game Files | Electric Cherry</title></svelte:head>
<svelte:head><title>Game Files | Electric Cherry</title></svelte:head>
<divclass="page">
<divclass="page">
<SectionHeadertitle="Game Files"subtitle="Upload your T5 and T6 game folders. Hosts automatically keep a verified copy of the latest files for each game."/>
<SectionHeadertitle="Game Files"subtitle="Upload your T5 and T6 game folders. Hosts automatically keep a verified copy of the latest files for each game."/>
import BackLink from '$ui/components/BackLink.svelte';
import Breadcrumbs from '$ui/components/primitives/Breadcrumbs.svelte';
import Tabs from '$ui/components/primitives/Tabs.svelte';
import {contextHref} from '$lib/navigation-context.js';
import {contextHref} from '$lib/navigation-context.js';
import {untrack} from 'svelte';
import {untrack} from 'svelte';
import {pollVisible} from '$ui/poll.js';
import {pollVisible} from '$ui/poll.js';
@@ -24,6 +27,14 @@
let drainOpen = $state(false);
let drainOpen = $state(false);
let name = $state(''); let portMin = $state(28960); let portMax = $state(28980); let bindAddress = $state('0.0.0.0'); let publicAddress = $state(''); let minimumFreeGB = $state(1);
let name = $state(''); let portMin = $state(28960); let portMax = $state(28980); let bindAddress = $state('0.0.0.0'); let publicAddress = $state(''); let minimumFreeGB = $state(1);
let runtimeImage = $state('ghcr.io/ayymoss/plutainer:latest'); let enrollment = $state<{command: string;expiresAt: string} | null>(null);
let runtimeImage = $state('ghcr.io/ayymoss/plutainer:latest'); let enrollment = $state<{command: string;expiresAt: string} | null>(null);
return pollVisible(async signal => {constresult=awaitload(initialize,signal);if(result!==false)initialize=false;returnresult;}, 15000);
return pollVisible(async signal => {constresult=awaitload(initialize,signal);if(result!==false)initialize=false;returnresult;}, 15000);
});
});
@@ -57,17 +69,35 @@
</script>
</script>
<svelte:head><title>{host?.name??'Host'} · Electric Cherry</title></svelte:head>
<svelte:head><title>{host?.name??'Host'} · Electric Cherry</title></svelte:head>
<sectionclass="page"><ahref="/hosts">← All hosts</a><SectionHeadertitle={host?.name??'Host'}subtitle="Runninginstances,readiness,inventory,settings,andcredentials.">
{#ifhost.decommission}<sectionclass="panel"aria-live="polite"><h2>Host cleanup</h2><p>{hostDeletionMessage(host)}</p>{#ifhost.decommission.error}<pclass="ui-error">{host.decommission.error}</p>{/if}<p>Requested {newDate(host.decommission.requestedAt).toLocaleString()}. This page closes only after the cleanup worker confirms removal.</p>{#ifhost.decommission.stage==='cleaned'}<p>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.</p>{/if}</section>{/if}
{#ifhost.decommission}<sectionclass="panel"aria-live="polite"><h2>Host cleanup</h2><p>{hostDeletionMessage(host)}</p>{#ifhost.decommission.error}<pclass="ui-error">{host.decommission.error}</p>{/if}<p>Requested {newDate(host.decommission.requestedAt).toLocaleString()}. This page closes only after the cleanup worker confirms removal.</p>{#ifhost.decommission.stage==='cleaned'}<p>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.</p>{/if}</section>{/if}
{#ifhost.readinessReasons.length}<ul>{#eachhost.readinessReasonsasreason}<li>{reason}</li>{/each}</ul>{:else}<p>Connected and ready. Each deployment also checks its latest game files, runtime, and ports.</p>{/if}
{#ifhost.readinessReasons.length}<ul>{#eachhost.readinessReasonsasreason}<li>{reason}</li>{/each}</ul>{:else}<p>Connected and ready. Each deployment also checks its latest game files, runtime, and ports.</p>{/if}
<p>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.</p>
<dt>{phase==='startup'?'Starting a server':'Running a server'}</dt>
<dd>{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)</dd>
{#ifcalibration.startup.source==='bootstrap'||calibration.running.source==='bootstrap'}<p>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.</p>{/if}
<dt>{phase==='startup'?'Starting a server':'Running a server'}</dt>
<dd>{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)</dd>
{#ifcalibration.startup.source==='bootstrap'||calibration.running.source==='bootstrap'}<p>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.</p>{/if}
<label>Friendly name<inputrequiredmaxlength="200"bind:value={name}/></label><divclass="fields"><label>First game port<inputtype="number"min="1"max="65535"requiredbind:value={portMin}/></label><label>Last game port<inputtype="number"min={portMin}max="65535"requiredbind:value={portMax}/></label></div><label>Local bind address<inputrequiredbind:value={bindAddress}/></label><label>Public / NAT address<inputbind:value={publicAddress}placeholder="Optionalpublichostnameoraddress"/></label><label>Minimum free game storage (GiB)<inputtype="number"min="0"step="0.1"requiredbind:value={minimumFreeGB}/></label><Buttontype="submit"disabled={busy}>Savesettings</Button>
</fieldset></form>{:else}<dl><dt>Friendly name</dt><dd>{host.name}</dd><dt>Game ports</dt><dd>{host.portMin}–{host.portMax}</dd><dt>Local bind address</dt><dd>{host.bindAddress}</dd><dt>Public / NAT address</dt><dd>{host.publicAddress||'Not configured'}</dd><dt>Minimum free storage</dt><dd>{bytes(host.minimumFreeBytes)}</dd></dl><p>Host configuration is managed by administrators.</p>{/if}<p>Public addressing is separate from local port reservations. Configure your firewall and NAT for the game-port range.</p></section></div>
{#ifhost.enrolled}<p>{hostAgentUpdateMessage(host.inventory)}</p>{:else}<p>The installer enables automatic updates when this host first enrolls.</p>{/if}
{#ifhost.enrolled}<p>{hostAgentUpdateMessage(host.inventory)}</p>{:else}<p>The installer enables automatic updates when this host first enrolls.</p>{/if}
{#ifhost.enrolled&&!hostAgentUpdatesEnabled(host.inventory)}<p>{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.'}</p>{:elseifhostAgentUpdatesEnabled(host.inventory)}<p>The host checks this controller about every five minutes. Updates replace agent code between operations; running game containers continue.</p>{/if}
{#ifhost.enrolled&&!hostAgentUpdatesEnabled(host.inventory)}<p>{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.'}</p>{:elseifhostAgentUpdatesEnabled(host.inventory)}<p>The host checks this controller about every five minutes. Updates replace agent code between operations; running game containers continue.</p>{/if}
</section>
</section>
<divclass="grid"><sectionclass="panel"><h2>Cached game files and runtimes</h2>{#ifhost.inventory.libraries?.length}<ul>{#eachhost.inventory.librariesaslibrary}<li>{library.id}: {library.state}{library.progress!==undefined?` (${library.progress}%)` : ''}</li>{/each}</ul>{:else}<p>No cached game libraries reported yet.</p>{/if}
<sectionclass="panel"><h2>Cached game files and runtimes</h2>{#ifhost.inventory.libraries?.length}<ul>{#eachhost.inventory.librariesaslibrary}<li>{library.id}: {library.state}{library.progress!==undefined?` (${library.progress}%)` : ''}</li>{/each}</ul>{:else}<p>No cached game libraries reported yet.</p>{/if}
{#ifcanManage}<details><summary>Advanced: custom runtime or manual retry</summary><p>The configured default runtime is prepared automatically. Use this to prepare another image or retry a pull now.</p><formonsubmit={(event)=>{event.preventDefault();voidaction('/operations','POST',{kind:'runtime.pull',image: runtimeImage},'Runtime download queued.');}}><fieldsetdisabled={busy||cleaning}><label>Plutainer image<inputrequiredbind:value={runtimeImage}/></label><Buttontype="submit"disabled={busy||!host.connected}>Pullruntimeimage</Button></fieldset></form></details>{/if}
{#ifcanManage}<details><summary>Advanced: custom runtime or manual retry</summary><p>The configured default runtime is prepared automatically. Use this to prepare another image or retry a pull now.</p><formonsubmit={(event)=>{event.preventDefault();voidaction('/operations','POST',{kind:'runtime.pull',image: runtimeImage},'Runtime download queued.');}}><fieldsetdisabled={busy||cleaning}><label>Plutainer image<inputrequiredbind:value={runtimeImage}/></label><Buttontype="submit"disabled={busy||!host.connected}>Pullruntimeimage</Button></fieldset></form></details>{/if}
</section><sectionclass="panel"><h2>Agent access</h2><p>Credentials: {host.credentialActive?'Active':'Not active'}. Enrollment links expire after 15 minutes and work once.</p>{#ifcanManage}<divclass="actions"><Buttondisabled={busy||(cleaning&&!host.decommission?.retryable)}onclick={()=>action('/enrollment','POST')}>Generate installer</Button><Buttondisabled={busy||cleaning||!host.credentialActive||!host.connected}onclick={()=>action('/credentials','POST',undefined,'Credential rotation queued.')}>Rotate credential</Button><Buttondisabled={busy||cleaning||!host.credentialActive}onclick={()=>action('/credentials','DELETE',undefined,'Host credentials revoked. Generate a new installer to enroll again.')}>Revoke credential</Button></div>
<p>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.</p></section>
<label>Friendly name<inputrequiredmaxlength="200"bind:value={name}/></label><divclass="fields"><label>First game port<inputtype="number"min="1"max="65535"requiredbind:value={portMin}/></label><label>Last game port<inputtype="number"min={portMin}max="65535"requiredbind:value={portMax}/></label></div><label>Local bind address<inputrequiredbind:value={bindAddress}/></label><label>Public / NAT address<inputbind:value={publicAddress}placeholder="Optionalpublichostnameoraddress"/></label><label>Minimum free game storage (GiB)<inputtype="number"min="0"step="0.1"requiredbind:value={minimumFreeGB}/></label><Buttontype="submit"disabled={busy}>Savesettings</Button>
</fieldset></form>{:else}<dl><dt>Friendly name</dt><dd>{host.name}</dd><dt>Game ports</dt><dd>{host.portMin}–{host.portMax}</dd><dt>Local bind address</dt><dd>{host.bindAddress}</dd><dt>Public / NAT address</dt><dd>{host.publicAddress||'Not configured'}</dd><dt>Minimum free storage</dt><dd>{bytes(host.minimumFreeBytes)}</dd></dl><p>Host configuration is managed by administrators.</p>{/if}<p>Public addressing is separate from local port reservations. Configure your firewall and NAT for the game-port range.</p></section><sectionclass="panel"id="agent-access"><h2>Agent access</h2><p>Credentials: {host.credentialActive?'Active':'Not active'}. Enrollment links expire after 15 minutes and work once.</p>{#ifcanManage}<divclass="actions"><Buttondisabled={busy||(cleaning&&!host.decommission?.retryable)}onclick={()=>action('/enrollment','POST')}>Generate installer</Button><Buttondisabled={busy||cleaning||!host.credentialActive||!host.connected}onclick={()=>action('/credentials','POST',undefined,'Credential rotation queued.')}>Rotate credential</Button><Buttondisabled={busy||cleaning||!host.credentialActive}onclick={()=>action('/credentials','DELETE',undefined,'Host credentials revoked. Generate a new installer to enroll again.')}>Revoke credential</Button></div>
{#ifenrollment}<p>Expires {newDate(enrollment.expiresAt).toLocaleString()}</p><pre>{enrollment.command}</pre>{/if}{:else}<p>Administrators manage enrollment and agent credentials.</p>{/if}
{#ifenrollment}<p>Expires {newDate(enrollment.expiresAt).toLocaleString()}</p><pre>{enrollment.command}</pre>{/if}{:else}<p>Administrators manage enrollment and agent credentials.</p>{/if}
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.