try{constresponse=awaitfetch('/api/hosts');constbody=awaitresponse.json();if(!response.ok)thrownewError(body.error??'Could not load hosts.');if(sequence===loadSequence){hosts=body.hosts;error='';}}
notice=result.deleted?`${deletingHost?.name??'Host'} deleted.`:`Cleanup requested for ${deletingHost?.name??'host'}. It remains listed until its files and agent are removed.`;
{#ifenrollment}<divclass="enrollment"role="status"><h3>Install the agent</h3><p>This single-use command expires {newDate(enrollment.expiresAt).toLocaleString()}. Run it with administrator access on your game host.</p><pre>{enrollment.command}</pre><ahref={`/hosts/${enrollment.hostId}`}>Openhostsetup→</a></div>{/if}
</section>{:else}<p>Administrators manage host setup, runtime preparation, and credentials.</p>{/if}
{#ifloading}<prole="status">Loading hosts…</p>{:elseif!hosts.length}<p>No hosts connected yet.{canManage?' Add a host to begin.':''}</p>{:else}
constresponse=awaitfetch(endpoint);constbody=awaitresponse.json();if(!response.ok)thrownewError(body.error??body.message??'Could not load host.');host=body.host;operations=body.operations;
constresponse=awaitfetch(endpoint);constbody=awaitresponse.json();if(response.status===404&&host?.decommission){awaitgoto('/hosts');return;}if(!response.ok)thrownewError(body.error??body.message??'Could not load host.');host=body.host;operations=body.operations;
<svelte:head><title>{host?.name??'Host'} · Electric Cherry</title></svelte:head>
<sectionclass="page"><ahref="/hosts">← All hosts</a><SectionHeadertitle={host?.name??'Host'}subtitle="Readiness,inventory,credentials,andoperationhistory.">
{#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 game library, runtime, and ports.</p>{/if}
{#ifhost.readiness==='needs-game-files'}<p><ahref="/game-files">Choose a published game-file version and download it to this host →</a></p>{/if}
<p>Draining prevents new deployments and retires instances as they become safe to remove. Disabled hosts receive no new commands; running game containers continue.</p>
<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>
<divclass="grid"><sectionclass="panel"><h2>Game files and runtime</h2><p><ahref="/game-files">Manage game-file libraries →</a></p>{#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.</p>{/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}onclick={()=>action('/enrollment','POST')}>Generate installer</Button><Buttondisabled={busy||!host.credentialActive||!host.connected}onclick={()=>action('/credentials','POST',undefined,'Credential rotation queued.')}>Rotate credential</Button><Buttondisabled={busy||!host.credentialActive}onclick={()=>action('/credentials','DELETE',undefined,'Host credentials revoked. Generate a new installer to enroll again.')}>Revoke credential</Button></div>
</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>
{#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.