192 lines
6.4 KiB
Svelte
192 lines
6.4 KiB
Svelte
<script lang="ts">
|
|
import Input from '$ui/components/primitives/Input.svelte';
|
|
import Field from '$ui/components/primitives/Field.svelte';
|
|
import { timeFormat } from '$ui/stores/time-zone.js';
|
|
import Dialog from './primitives/Dialog.svelte';
|
|
import Button from './primitives/Button.svelte';
|
|
let { open = $bindable(false), onadded }: { open?: boolean; onadded: () => Promise<unknown> } =
|
|
$props();
|
|
let name = $state('');
|
|
let portMin = $state(28960);
|
|
let portMax = $state(28980);
|
|
let busy = $state(false);
|
|
let error = $state('');
|
|
let copied = $state(false);
|
|
let enrollment = $state<{ command: string; expiresAt: string; hostId: string } | null>(null);
|
|
async function add(event: SubmitEvent) {
|
|
event.preventDefault();
|
|
if (busy) return;
|
|
busy = true;
|
|
error = '';
|
|
copied = false;
|
|
try {
|
|
const response = await fetch('/api/hosts', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ name, portMin, portMax })
|
|
});
|
|
const body = await response.json();
|
|
if (!response.ok) throw new Error(body.error ?? 'Could not add host.');
|
|
enrollment = body.enrollment;
|
|
await onadded();
|
|
} catch (cause) {
|
|
error = (cause as Error).message;
|
|
} finally {
|
|
busy = false;
|
|
}
|
|
}
|
|
async function copy() {
|
|
if (!enrollment) return;
|
|
try {
|
|
await navigator.clipboard.writeText(enrollment.command);
|
|
copied = true;
|
|
error = '';
|
|
} catch {
|
|
error = 'Could not copy automatically. Select and copy the command below.';
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<Dialog bind:open title={enrollment ? 'Install the host agent' : 'Add host'} {busy} drawer>
|
|
<div class="content">
|
|
{#if error}<p role="alert" class="ui-error">{error}</p>{/if}
|
|
{#if enrollment}
|
|
<p role="status">
|
|
{name} created. Run this command with administrator access on that machine.
|
|
</p>
|
|
<p>
|
|
This command downloads the installer from the public Electric Cherry repository. The
|
|
host automatically fetches the publisher's public signing key from that repository
|
|
during installation and before every update check.
|
|
</p>
|
|
<textarea
|
|
readonly
|
|
aria-label="Host installation command"
|
|
rows="6"
|
|
value={enrollment.command}
|
|
></textarea>
|
|
<div class="actions">
|
|
<Button onclick={copy}>{copied ? 'Copied' : 'Copy command'}</Button><Button
|
|
variant="primary"
|
|
href={`/hosts/${enrollment.hostId}?tab=setup`}>Open host setup</Button
|
|
>
|
|
</div>
|
|
<p class="muted">
|
|
This single-use command expires {$timeFormat.dateTime(enrollment.expiresAt)}.
|
|
Generate a new installer from host settings if it expires.
|
|
</p>
|
|
<Button
|
|
variant="ghost"
|
|
onclick={() => {
|
|
enrollment = null;
|
|
name = '';
|
|
copied = false;
|
|
error = '';
|
|
}}>Add another host</Button
|
|
>
|
|
{:else}
|
|
<p>
|
|
Use a Debian 12 or 13 amd64 machine. The agent connects over HTTPS and prepares the
|
|
runtime and game files automatically.
|
|
</p>
|
|
<form onsubmit={add}>
|
|
<fieldset disabled={busy}>
|
|
<Field label="Friendly name">
|
|
<Input
|
|
required
|
|
maxlength={200}
|
|
bind:value={name}
|
|
placeholder="Game host 1"
|
|
/></Field
|
|
>
|
|
<details>
|
|
<summary>Advanced settings</summary>
|
|
<div class="ports">
|
|
<Field label="First game port">
|
|
<Input
|
|
type="number"
|
|
required
|
|
min="1"
|
|
max="65535"
|
|
bind:value={portMin}
|
|
/></Field
|
|
>
|
|
<Field label="Last game port">
|
|
<Input
|
|
type="number"
|
|
required
|
|
min={portMin}
|
|
max="65535"
|
|
bind:value={portMax}
|
|
/></Field
|
|
>
|
|
</div>
|
|
</details>
|
|
<Button type="submit" variant="primary" disabled={busy}
|
|
>{busy ? 'Creating…' : 'Add host'}</Button
|
|
>
|
|
</fieldset>
|
|
</form>
|
|
{/if}
|
|
</div>
|
|
</Dialog>
|
|
|
|
<style>
|
|
.content,
|
|
fieldset {
|
|
display: grid;
|
|
gap: 20px;
|
|
}
|
|
fieldset {
|
|
border: 0;
|
|
padding: 0;
|
|
margin: 0;
|
|
min-width: 0;
|
|
}
|
|
p {
|
|
margin: 0;
|
|
color: var(--text-secondary);
|
|
font-size: var(--text-sm);
|
|
line-height: 1.6;
|
|
}
|
|
.muted {
|
|
font-size: var(--text-xs);
|
|
}
|
|
|
|
textarea {
|
|
box-sizing: border-box;
|
|
width: 100%;
|
|
min-width: 0;
|
|
padding: 10px;
|
|
color: var(--text-primary);
|
|
background: var(--surface-sunken);
|
|
border: 1px solid var(--border-control);
|
|
border-radius: var(--radius-sm);
|
|
}
|
|
textarea {
|
|
font: var(--text-xs)/1.6 var(--font-mono);
|
|
resize: vertical;
|
|
}
|
|
summary {
|
|
cursor: pointer;
|
|
color: var(--text-secondary);
|
|
font-size: var(--text-sm);
|
|
}
|
|
.ports {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 16px;
|
|
margin-top: 16px;
|
|
}
|
|
.actions {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 12px;
|
|
}
|
|
textarea:focus-visible,
|
|
summary:focus-visible {
|
|
outline: 2px solid var(--accent-primary);
|
|
outline-offset: 3px;
|
|
}
|
|
</style>
|