a73x

f984021a

fix(web): the join flow hands you the bundle your host actually takes

a73x   2026-08-11 15:15

Commit message
fix(web): the join flow hands you the bundle your host actually takes

The console printed one recipe — eitri-server_<v>_linux_amd64 — under the
words "the host bundle", and left the reader to reconcile them. On a Linux
host the artifact is right and the name is the confusing part: the tarball is
named for the eitri-server it also carries, and a box that only runs VMs
installs the eitri-agent inside it. On an arm64 box it was the wrong file, and
on a Mac it was a file that cannot enrol anything — a Mac's agent ships in
eitri-agent_<v>_darwin_arm64, with a LaunchAgent instead of a unit, no sudo
anywhere, vfkit from Homebrew first, and shasum because macOS has no
sha256sum.

So the flow asks which machine this is and prints that machine's recipe,
naming the artifact and saying what it is. The rules live in fleet.svelte.ts
under test, next to the release they describe; the recipe itself is one
component, where the fleet page had two copies of it.

A plane that has not read the release manifest yet no longer prints
eitri-server_latest_linux_amd64.tar.gz, which is a URL that 404s: the recipe
reads the version out of SHA256SUMS, as the docs have always done.

docs/quickstart.md
Old New
@@ -30,6 +30,10 @@ tar xzf "eitri-server_${V}_linux_amd64.tar.gz" && cd "eitri-server_${V}_linux_am
30 `/dl/latest/` is the current release. Its filenames carry the version, so 30 `/dl/latest/` is the current release. Its filenames carry the version, so
31 `SHA256SUMS`—which you need to verify anyway—is where you read it from. 31 `SHA256SUMS`—which you need to verify anyway—is where you read it from.
32 32
33 The Linux host bundle is named for the `eitri-server` it also carries: a box
34 that only runs VMs installs the `eitri-agent` inside it and nothing else. On an
35 arm64 host, `_arm64` replaces `_amd64` throughout.
36
33 Then click **+ Add host** in the console and run the command it prints 37 Then click **+ Add host** in the console and run the command it prints
34 from the unpacked bundle—it installs the agent and joins this machine 38 from the unpacked bundle—it installs the agent and joins this machine
35 to your tenant: 39 to your tenant:
@@ -60,8 +64,20 @@ here: vfkit only works carrying Apple's virtualization entitlement, and an
60 entitlement lives in a code signature. Homebrew's copy is signed. Without it, 64 entitlement lives in a code signature. Homebrew's copy is signed. Without it,
61 VMs placed on this host fail at once, saying so. 65 VMs placed on this host fail at once, saying so.
62 66
63 Take the darwin bundle instead of the linux one. Nothing here needs `sudo`, and 67 Take the darwin bundle instead of the linux one:
64 nothing goes in a system directory: 68 `eitri-agent_<version>_darwin_arm64.tar.gz`, the agent and its LaunchAgent
69 installer, no server. macOS ships no `sha256sum`, so `shasum` checks the one
70 line of `SHA256SUMS` that names it:
71
72 ```sh
73 curl -fsSLO https://eitri.sh/dl/latest/SHA256SUMS
74 V=$(sed -n 's/.*eitri-agent_\(v[^_]*\)_darwin_arm64\.tar\.gz$/\1/p' SHA256SUMS)
75 curl -fsSLO "https://eitri.sh/dl/latest/eitri-agent_${V}_darwin_arm64.tar.gz"
76 grep " eitri-agent_${V}_darwin_arm64.tar.gz$" SHA256SUMS | shasum -a 256 -c -
77 tar xzf "eitri-agent_${V}_darwin_arm64.tar.gz" && cd "eitri-agent_${V}_darwin_arm64"
78 ```
79
80 Nothing here needs `sudo`, and nothing goes in a system directory:
65 81
66 ```sh 82 ```sh
67 mkdir -p ~/.local/bin && cp eitri-agent ~/.local/bin/ 83 mkdir -p ~/.local/bin && cp eitri-agent ~/.local/bin/
@@ -213,9 +229,12 @@ guest firmware itself on first start, sha-verified against the release. To
213 manage them by hand instead, disable it in `/etc/default/eitri-agent`: 229 manage them by hand instead, disable it in `/etc/default/eitri-agent`:
214 `EITRI_AGENT_FLAGS="--bootstrap-url="`. 230 `EITRI_AGENT_FLAGS="--bootstrap-url="`.
215 231
216 Tarballs live at <https://eitri.sh/dl/latest/>. The host bundle 232 Tarballs live at <https://eitri.sh/dl/latest/>. The Linux host bundle
217 (`eitri-server_<version>_linux_amd64.tar.gz`) has `eitri-server`, `eitri-agent`, and 233 (`eitri-server_<version>_linux_amd64.tar.gz`) has `eitri-server`, `eitri-agent`, and
218 their systemd units. The issuer bundle 234 their systemd units—a box that only runs VMs takes the same tarball and
235 installs just the agent from it. A Mac host has a bundle of its own
236 (`eitri-agent_<version>_darwin_arm64.tar.gz`): the agent and its LaunchAgent
237 installer, no server. The issuer bundle
219 (`eitri-oidc_<version>_linux_amd64.tar.gz`) has `eitri-oidc`—the bundled 238 (`eitri-oidc_<version>_linux_amd64.tar.gz`) has `eitri-oidc`—the bundled
220 sign-in provider—and its unit. The client bundle 239 sign-in provider—and its unit. The client bundle
221 (`eitri-cli_<version>_<os>_<arch>.tar.gz`) is the single `eitri` binary for 240 (`eitri-cli_<version>_<os>_<arch>.tar.gz`) is the single `eitri` binary for
@@ -391,8 +410,9 @@ sudo systemctl enable --now eitri-agent
391 The host goes **online** in the console. Logs: 410 The host goes **online** in the console. Logs:
392 `journalctl -u eitri-agent -f`. 411 `journalctl -u eitri-agent -f`.
393 412
394 A Mac joins the same fleet from the `darwin_arm64` bundle—`brew install vfkit` 413 A Mac joins the same fleet from its own bundle,
395 first, then `eitri-agent join`, then `eitri-agent-launchagent.sh install` in 414 `eitri-agent_<version>_darwin_arm64.tar.gz`—`brew install vfkit` first, then
415 `eitri-agent join`, then `eitri-agent-launchagent.sh install` in
396 place of the systemd unit. The differences are the same ones the hosted section covers above, and 416 place of the systemd unit. The differences are the same ones the hosted section covers above, and
397 the one extra thing self-hosting asks of you is an `arm64` entry in the 417 the one extra thing self-hosting asks of you is an `arm64` entry in the
398 server's `default_images`. 418 server's `default_images`.
docs/upgrade.md
Old New
@@ -63,7 +63,8 @@ keeps, so rollback is the same either way.
63 63
64 On a Mac the binary lives in the running account's own space (e.g. 64 On a Mac the binary lives in the running account's own space (e.g.
65 `~/.local/bin/eitri-agent`). Overwrite it with `eitri-agent` from the darwin 65 `~/.local/bin/eitri-agent`). Overwrite it with `eitri-agent` from the darwin
66 bundle, then restart the LaunchAgent: 66 bundle (`eitri-agent_<version>_darwin_arm64.tar.gz`), then restart the
67 LaunchAgent:
67 68
68 ```sh 69 ```sh
69 launchctl kickstart -k gui/$(id -u)/sh.eitri.agent 70 launchctl kickstart -k gui/$(id -u)/sh.eitri.agent
web/src/lib/JoinHost.svelte
Old New
@@ -0,0 +1,72 @@
1 <script lang="ts">
2 // The enrolment recipe for one join token. The token is the same word on
3 // every platform; nothing else about joining is, so the flow asks which
4 // machine this is rather than printing one recipe and hoping — the rules
5 // themselves live in fleet.svelte.ts, next to the release they describe.
6 import { HOST_PLATFORMS, hostBundle, joinCommands, type HostPlatform } from '$lib/fleet.svelte';
7
8 let { blob, version }: { blob: string; version: string } = $props();
9
10 // Linux amd64 leads because most hosts are one, and because the server's own
11 // box — the first host of most fleets — is whatever the server runs on.
12 let platform = $state<HostPlatform>('linux/amd64');
13 const mac = $derived(platform === 'darwin/arm64');
14 const bundle = $derived(hostBundle(platform, version));
15 const commands = $derived(joinCommands(platform, version, blob));
16 </script>
17
18 <div class="join">
19 <label>
20 Host platform
21 <select bind:value={platform}>
22 {#each HOST_PLATFORMS as p (p.id)}
23 <option value={p.id}>{p.label}</option>
24 {/each}
25 </select>
26 </label>
27
28 <p>
29 Run these on the new host.
30 {#if mac}
31 <code>{bundle.file}</code> is the macOS host bundle: the agent and its LaunchAgent installer, no
32 server. Apple silicon only, and nothing here needs <code>sudo</code>.
33 {:else}
34 <code>{bundle.file}</code> is the Linux host bundle. It is named for the
35 <code>eitri-server</code> it also carries; a box that only runs VMs installs the
36 <code>eitri-agent</code> inside it and nothing else.
37 {/if}
38 </p>
39
40 <div class="enroll">
41 {#each commands as c (c)}
42 <code>{c}</code>
43 {/each}
44 </div>
45 </div>
46
47 <style>
48 /* Top margin only: this sits under a line of prose in both callers, and the
49 box below it is the last thing in the onboarding card. */
50 .join {
51 margin-top: 0.5em;
52 }
53 label {
54 display: flex;
55 align-items: center;
56 gap: 0.75em;
57 }
58 .join p {
59 margin: 0.5em 0;
60 }
61 /* One command per line, unwrapped: a join blob is one long word and breaking
62 it mid-token would be a command nobody can paste. */
63 .enroll {
64 border: 1px solid var(--hairline);
65 padding: 1em;
66 overflow-x: auto;
67 }
68 .enroll code {
69 display: block;
70 word-break: break-all;
71 }
72 </style>
web/src/lib/fleet.svelte.ts
Old New
@@ -387,6 +387,94 @@ export function vmsForHost(id: string): VM[] {
387 return fleet.vms.filter((v) => v.host_id === id); 387 return fleet.vms.filter((v) => v.host_id === id);
388 } 388 }
389 389
390 // Host enrolment — which release artifact a new host takes, and what it runs.
391
392 /** HostPlatform is a machine a host can enrol on, spelled the way
393 * scripts/release.sh spells the artifact for it. darwin/amd64 is absent
394 * because no release carries an agent for it: shipping one would put an
395 * enrolment recipe in front of an Intel Mac that has never booted a guest. */
396 export type HostPlatform = 'linux/amd64' | 'linux/arm64' | 'darwin/arm64';
397
398 /** HOST_PLATFORMS is what the join flow offers, in the order it offers it. */
399 export const HOST_PLATFORMS: { id: HostPlatform; label: string }[] = [
400 { id: 'linux/amd64', label: 'Linux · amd64' },
401 { id: 'linux/arm64', label: 'Linux · arm64' },
402 { id: 'darwin/arm64', label: 'macOS · Apple silicon' }
403 ];
404
405 /** HostBundle is the one release artifact a host of a given platform enrols
406 * from: the tarball to fetch and the directory it unpacks into. */
407 export type HostBundle = { file: string; dir: string };
408
409 /** hostBundle names that artifact.
410 *
411 * A Linux host takes `eitri-server_<v>_linux_<arch>.tar.gz`. The tarball is
412 * named for the server it also carries, and that is the whole of the trap
413 * this names its way out of: the `eitri-agent` inside it is the binary a host
414 * installs, whether or not that box ever runs a server. A Mac takes
415 * `eitri-agent_<v>_darwin_arm64.tar.gz`, which carries no server at all —
416 * a Mac joins a fleet only as a host.
417 *
418 * An empty version is a plane that has not read the release manifest yet. The
419 * names then carry `${V}`, a shell variable the recipe reads out of
420 * SHA256SUMS, because the release's own filenames are the only place the
421 * version is written: `/dl/latest/` is a directory alias, and
422 * `eitri-server_latest_linux_amd64.tar.gz` is a URL that 404s. */
423 export function hostBundle(platform: HostPlatform, version: string): HostBundle {
424 const arch = platform.split('/')[1];
425 const v = version || '${V}';
426 const dir =
427 platform === 'darwin/arm64'
428 ? `eitri-agent_${v}_darwin_arm64`
429 : `eitri-server_${v}_linux_${arch}`;
430 return { file: `${dir}.tar.gz`, dir };
431 }
432
433 /** joinCommands is the enrolment recipe for one join token on one platform:
434 * fetch, verify, install, join, start. It is the same sequence docs/quickstart
435 * prints, with this fleet's release in it.
436 *
437 * The two platforms differ in more than a filename, which is why the flow asks
438 * rather than guessing. A Linux host installs into system paths under sudo and
439 * runs the agent under systemd; a Mac installs into the running account's own
440 * space with no sudo anywhere, runs it as a LaunchAgent, and needs vfkit from
441 * Homebrew first — the agent bootstraps cloud-hypervisor for itself but cannot
442 * do the same for vfkit, which only works carrying Apple's virtualization
443 * entitlement. macOS also ships no `sha256sum`, so verification there is
444 * `shasum` over the one line of SHA256SUMS that names this bundle. */
445 export function joinCommands(platform: HostPlatform, version: string, blob: string): string[] {
446 const { file, dir } = hostBundle(platform, version);
447 const base = `https://eitri.sh/dl/${version || 'latest'}`;
448 const mac = platform === 'darwin/arm64';
449 const stem = mac ? 'eitri-agent' : 'eitri-server';
450 const suffix = mac ? 'darwin_arm64' : `linux_${platform.split('/')[1]}`;
451
452 const cmds = mac ? ['brew install vfkit'] : [];
453 cmds.push(`curl -fsSLO ${base}/SHA256SUMS`);
454 if (!version) {
455 cmds.push(`V=$(sed -n 's/.*${stem}_\\(v[^_]*\\)_${suffix}\\.tar\\.gz$/\\1/p' SHA256SUMS)`);
456 }
457 cmds.push(`curl -fsSLO "${base}/${file}"`);
458 cmds.push(
459 mac
460 ? `grep " ${file}$" SHA256SUMS | shasum -a 256 -c -`
461 : 'sha256sum -c SHA256SUMS --ignore-missing'
462 );
463 cmds.push(`tar xzf ${file} && cd ${dir}`);
464 if (mac) {
465 cmds.push('mkdir -p ~/.local/bin && cp eitri-agent ~/.local/bin/');
466 cmds.push(`~/.local/bin/eitri-agent join ${blob}`);
467 cmds.push('./eitri-agent-launchagent.sh install ~/.local/bin/eitri-agent');
468 } else {
469 cmds.push('sudo install -m 0755 eitri-agent /usr/local/bin/eitri-agent');
470 cmds.push('sudo install -m 0644 eitri-agent.service /etc/systemd/system/eitri-agent.service');
471 cmds.push(`sudo eitri-agent --state-dir /var/lib/eitri-agent join ${blob}`);
472 cmds.push('sudo systemctl daemon-reload');
473 cmds.push('sudo systemctl enable --now eitri-agent');
474 }
475 return cmds;
476 }
477
390 // VM display derivations — the canonical "what phase / power / address is this 478 // VM display derivations — the canonical "what phase / power / address is this
391 // VM" rules, shared by every view so they cannot drift. 479 // VM" rules, shared by every view so they cannot drift.
392 480
web/src/lib/fleet.test.ts
Old New
@@ -2,6 +2,8 @@ import { beforeEach, describe, expect, test } from 'vitest';
2 import { 2 import {
3 capacityReading, 3 capacityReading,
4 fleet, 4 fleet,
5 hostBundle,
6 joinCommands,
5 upgradeAge, 7 upgradeAge,
6 upgradeStuck, 8 upgradeStuck,
7 upgradeStuckNote, 9 upgradeStuckNote,
@@ -244,3 +246,91 @@ describe('capacityReading', () => {
244 expect(capacityReading(4, 0)).toEqual({ pct: 0, free: 0, over: 0, level: 'ok' }); 246 expect(capacityReading(4, 0)).toEqual({ pct: 0, free: 0, over: 0, level: 'ok' });
245 }); 247 });
246 }); 248 });
249
250 describe('hostBundle', () => {
251 test('a Linux host takes the tarball named for the server it also carries', () => {
252 expect(hostBundle('linux/amd64', 'v0.0.6')).toEqual({
253 file: 'eitri-server_v0.0.6_linux_amd64.tar.gz',
254 dir: 'eitri-server_v0.0.6_linux_amd64'
255 });
256 expect(hostBundle('linux/arm64', 'v0.0.6').file).toBe(
257 'eitri-server_v0.0.6_linux_arm64.tar.gz'
258 );
259 });
260
261 test('a Mac takes the agent bundle—no release carries a server for it', () => {
262 expect(hostBundle('darwin/arm64', 'v0.0.6')).toEqual({
263 file: 'eitri-agent_v0.0.6_darwin_arm64.tar.gz',
264 dir: 'eitri-agent_v0.0.6_darwin_arm64'
265 });
266 });
267
268 test('an unknown release leaves the version to the shell, never the word latest', () => {
269 const b = hostBundle('linux/amd64', '');
270 expect(b.file).toBe('eitri-server_${V}_linux_amd64.tar.gz');
271 expect(b.file).not.toContain('latest');
272 });
273 });
274
275 describe('joinCommands', () => {
276 const blob = 'eitri_join_abc123';
277
278 test('a Linux recipe fetches its bundle, verifies it, and starts the unit', () => {
279 const cmds = joinCommands('linux/amd64', 'v0.0.6', blob);
280 expect(cmds).toContain(
281 'curl -fsSLO "https://eitri.sh/dl/v0.0.6/eitri-server_v0.0.6_linux_amd64.tar.gz"'
282 );
283 expect(cmds).toContain('sha256sum -c SHA256SUMS --ignore-missing');
284 expect(cmds).toContain(`sudo eitri-agent --state-dir /var/lib/eitri-agent join ${blob}`);
285 expect(cmds).toContain('sudo systemctl enable --now eitri-agent');
286 expect(cmds.some((c) => c.includes('SHA256SUMS'))).toBe(true);
287 });
288
289 test('SHA256SUMS is fetched before the bundle it verifies', () => {
290 const cmds = joinCommands('linux/amd64', 'v0.0.6', blob);
291 expect(cmds.findIndex((c) => c.endsWith('SHA256SUMS'))).toBeLessThan(
292 cmds.findIndex((c) => c.includes('.tar.gz"'))
293 );
294 });
295
296 test('a Mac recipe takes the darwin agent bundle and asks for sudo nowhere', () => {
297 const cmds = joinCommands('darwin/arm64', 'v0.0.6', blob);
298 expect(cmds.join('\n')).toContain('eitri-agent_v0.0.6_darwin_arm64.tar.gz');
299 expect(cmds.join('\n')).not.toContain('eitri-server');
300 expect(cmds.join('\n')).not.toContain('sudo');
301 expect(cmds).toContain('brew install vfkit');
302 expect(cmds).toContain('./eitri-agent-launchagent.sh install ~/.local/bin/eitri-agent');
303 });
304
305 test('a Mac verifies with shasum—macOS ships no sha256sum', () => {
306 const cmds = joinCommands('darwin/arm64', 'v0.0.6', blob);
307 expect(cmds).toContain(
308 'grep " eitri-agent_v0.0.6_darwin_arm64.tar.gz$" SHA256SUMS | shasum -a 256 -c -'
309 );
310 expect(cmds.join('\n')).not.toContain('sha256sum');
311 });
312
313 test('no systemd on a Mac, and no LaunchAgent on Linux', () => {
314 expect(joinCommands('darwin/arm64', 'v0.0.6', blob).join('\n')).not.toContain('systemctl');
315 expect(joinCommands('linux/amd64', 'v0.0.6', blob).join('\n')).not.toContain('launchagent');
316 });
317
318 test('an unknown release reads the version out of SHA256SUMS, per platform', () => {
319 const linux = joinCommands('linux/arm64', '', blob);
320 expect(linux).toContain(
321 "V=$(sed -n 's/.*eitri-server_\\(v[^_]*\\)_linux_arm64\\.tar\\.gz$/\\1/p' SHA256SUMS)"
322 );
323 expect(linux).toContain('curl -fsSLO https://eitri.sh/dl/latest/SHA256SUMS');
324 const mac = joinCommands('darwin/arm64', '', blob);
325 expect(mac).toContain(
326 "V=$(sed -n 's/.*eitri-agent_\\(v[^_]*\\)_darwin_arm64\\.tar\\.gz$/\\1/p' SHA256SUMS)"
327 );
328 });
329
330 test('a known release is fetched from its own immutable directory', () => {
331 const cmds = joinCommands('linux/amd64', 'v0.0.6', blob).join('\n');
332 expect(cmds).toContain('https://eitri.sh/dl/v0.0.6/');
333 expect(cmds).not.toContain('/dl/latest/');
334 expect(cmds).not.toContain('${V}');
335 });
336 });
web/src/routes/+page.svelte
Old New
@@ -22,6 +22,7 @@
22 type CreateVMRequest, 22 type CreateVMRequest,
23 type VM 23 type VM
24 } from '$lib/fleet.svelte'; 24 } from '$lib/fleet.svelte';
25 import JoinHost from '$lib/JoinHost.svelte';
25 26
26 let showCreate = $state(false); 27 let showCreate = $state(false);
27 let advanced = $state(false); 28 let advanced = $state(false);
@@ -32,9 +33,6 @@
32 // Case-insensitive substring match over the row's visible fields, so a 33 // Case-insensitive substring match over the row's visible fields, so a
33 // large fleet can be narrowed live from one box. 34 // large fleet can be narrowed live from one box.
34 const needle = $derived(filter.trim().toLowerCase()); 35 const needle = $derived(filter.trim().toLowerCase());
35 // The /dl/ path for the printed join instructions: the fleet's known release,
36 // falling back to the stable latest alias before the manifest is fetched.
37 const dlVersion = $derived(fleet.latest_version || 'latest');
38 const shownHosts = $derived( 36 const shownHosts = $derived(
39 fleet.hosts.filter( 37 fleet.hosts.filter(
40 (h) => 38 (h) =>
@@ -193,41 +191,19 @@
193 <div class="onboard"> 191 <div class="onboard">
194 <h3>Add your first host</h3> 192 <h3>Add your first host</h3>
195 <p class="hint"> 193 <p class="hint">
196 A host is any Linux box that runs your VMs—the server's own box counts. Mint a one-time 194 A host is any Linux box or Mac that runs your VMs—the server's own box counts. Mint a
197 join token, then run the printed command on the box; it comes online here the moment it 195 one-time join token, then run the printed command on the box; it comes online here the
198 enrolls. 196 moment it enrolls.
199 </p> 197 </p>
200 {#if joinBlob} 198 {#if joinBlob}
201 <p>Download and verify the host bundle, then run on the new host:</p> 199 <JoinHost blob={joinBlob} version={fleet.latest_version} />
202 <div class="enroll">
203 <code>curl -fsSLO "https://eitri.sh/dl/{dlVersion}/eitri-server_{dlVersion}_linux_amd64.tar.gz"</code>
204 <code>curl -fsSLO "https://eitri.sh/dl/{dlVersion}/SHA256SUMS"</code>
205 <code>sha256sum -c SHA256SUMS --ignore-missing</code>
206 <code>tar xzf eitri-server_{dlVersion}_linux_amd64.tar.gz && cd eitri-server_{dlVersion}_linux_amd64</code>
207 <code>sudo install -m 0755 eitri-agent /usr/local/bin/eitri-agent</code>
208 <code>sudo install -m 0644 eitri-agent.service /etc/systemd/system/eitri-agent.service</code>
209 <code>sudo eitri-agent --state-dir /var/lib/eitri-agent join {joinBlob}</code>
210 <code>sudo systemctl daemon-reload</code>
211 <code>sudo systemctl enable --now eitri-agent</code>
212 </div>
213 {:else} 200 {:else}
214 <button onclick={addHost}>Add your first host</button> 201 <button onclick={addHost}>Add your first host</button>
215 {/if} 202 {/if}
216 </div> 203 </div>
217 {:else if joinBlob || shownHosts.length === 0} 204 {:else if joinBlob || shownHosts.length === 0}
218 {#if joinBlob} 205 {#if joinBlob}
219 <div class="enroll"> 206 <JoinHost blob={joinBlob} version={fleet.latest_version} />
220 Download and verify the host bundle, then run on the new host:
221 <code>curl -fsSLO "https://eitri.sh/dl/{dlVersion}/eitri-server_{dlVersion}_linux_amd64.tar.gz"</code>
222 <code>curl -fsSLO "https://eitri.sh/dl/{dlVersion}/SHA256SUMS"</code>
223 <code>sha256sum -c SHA256SUMS --ignore-missing</code>
224 <code>tar xzf eitri-server_{dlVersion}_linux_amd64.tar.gz && cd eitri-server_{dlVersion}_linux_amd64</code>
225 <code>sudo install -m 0755 eitri-agent /usr/local/bin/eitri-agent</code>
226 <code>sudo install -m 0644 eitri-agent.service /etc/systemd/system/eitri-agent.service</code>
227 <code>sudo eitri-agent --state-dir /var/lib/eitri-agent join {joinBlob}</code>
228 <code>sudo systemctl daemon-reload</code>
229 <code>sudo systemctl enable --now eitri-agent</code>
230 </div>
231 {/if} 207 {/if}
232 {#if shownHosts.length === 0} 208 {#if shownHosts.length === 0}
233 <p class="hint">No hosts match “{filter}”.</p> 209 <p class="hint">No hosts match “{filter}”.</p>
@@ -522,16 +498,6 @@
522 display: flex; 498 display: flex;
523 gap: 0.5em; 499 gap: 0.5em;
524 } 500 }
525 .enroll {
526 border: 1px solid var(--hairline);
527 padding: 1em;
528 margin: 0.5em 0;
529 overflow-x: auto;
530 }
531 .enroll code {
532 display: block;
533 word-break: break-all;
534 }
535 .update-banner { 501 .update-banner {
536 display: inline-block; 502 display: inline-block;
537 border: 1px solid var(--hairline); 503 border: 1px solid var(--hairline);
@@ -617,7 +583,4 @@
617 .onboard p { 583 .onboard p {
618 margin-bottom: 0.5em; 584 margin-bottom: 0.5em;
619 } 585 }
620 .onboard .enroll {
621 margin-bottom: 0;
622 }
623 </style> 586 </style>