Files
restic-manager/web/templates/pages/add_host.html
T
steve 8aa635f0c1 P1 polish: Host.default_paths interim + restic env hygiene + job_id JS quoting
Two fixes that close the loop on dashboard run-now and harden the
agent's restic invocation.

Default paths (interim until P2-01 schedules):
  - 0003 migration adds default_paths TEXT NOT NULL DEFAULT '[]'
    to hosts and to enrollment_tokens.
  - Operator types paths in the Add-host form (textarea, one per
    line). They ride on the enrol_token row alongside the
    encrypted creds (paths aren't secret — plain JSON column).
  - On consume, ConsumeEnrollmentToken still just burns the token;
    the new GetEnrollmentTokenAttachments returns both the
    re-bindable creds and the path list in one round trip, the
    handler transfers them onto the new host row inside CreateHost.
  - The dashboard's Run-now and host-detail's "Run backup now"
    button now read Host.DefaultPaths and pass them to dispatchJob.
    A host with no default paths returns 400 with a friendly
    "no paths set" message instead of dispatching a doomed
    `restic backup` with no positional args.
  - Doc comments explicitly call this out as a Phase 1 interim —
    schedules supersede.

Restic env hygiene:
  - envSlice() previously omitted HOME / XDG_CACHE_HOME, which
    bit the smoke runs whenever the agent was launched outside
    systemd (restic refused to start: "neither $XDG_CACHE_HOME
    nor $HOME are defined"). Now both are set explicitly: prefer
    Env.ExtraEnv overrides, fall back to the agent process's own
    HOME, and finally to /var/lib/restic-manager.
  - Comment makes the env policy explicit: parent's RESTIC_* /
    AWS_* / B2_* env is filtered out by design — control-plane
    is the unambiguous source of truth.

JS bug fix in the live log page:
  - {{$job.ID | printf "%q"}} produced a literal-quoted JS string,
    which then went into the WS URL as ".../jobs/"<ID>"/stream"
    → 404. Switched to '{{$job.ID}}' inside the literal so
    html/template's auto-escape does the right thing. Verified
    end-to-end: dashboard "Run now" → live progress + log lines
    arrive over the WS → succeeded pill renders.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 22:35:33 +01:00

189 lines
11 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{{define "title"}}{{.Title}}{{end}}
{{define "content"}}
{{$page := .Page}}
<div class="max-w-[1280px] mx-auto px-8 pt-9 pb-24">
<div class="crumbs"><a href="/">Dashboard</a><span class="sep">/</span><span class="text-ink-mid">Add host</span></div>
{{if eq $page.Token ""}}
{{/* ============================================================
State A · form
============================================================ */}}
<h1 class="text-2xl font-medium tracking-[-0.012em] mt-2.5">Add a host</h1>
<p class="text-pretty text-ink-mute text-[13px] mt-1.5 max-w-[580px]">
Mints a one-time enrolment token (TTL 1 hour) and binds the repo
credentials to it. The token can only be used once — generate a fresh
one if it expires or you typed something wrong.
</p>
{{if $page.Error}}
<div class="mt-6 px-4 py-3 rounded-[5px] text-[13px]"
style="background: color-mix(in oklch, var(--bad), transparent 88%);
border: 1px solid color-mix(in oklch, var(--bad), transparent 70%);
color: oklch(0.85 0.10 25);">
{{$page.Error}}
</div>
{{end}}
<form method="post" action="/hosts/new" class="grid grid-cols-12 gap-8 mt-7">
<div class="col-span-7 panel rounded-[7px] px-8 py-7">
<h3 class="text-[13px] font-semibold uppercase tracking-[0.08em] text-ink-mute mb-4">Host</h3>
<div class="mb-5">
<label class="field-label" for="ah-name">Hostname</label>
<input id="ah-name" name="hostname" type="text" class="field mono" autofocus required value="{{$page.Hostname}}">
<div class="field-help">Becomes the hosts display name. Most operators use the boxs actual hostname so logs line up.</div>
</div>
<div class="mb-7">
<label class="field-label" for="ah-tags">Tags <span class="text-ink-fade font-normal">· optional, comma-separated</span></label>
<input id="ah-tags" name="tags" type="text" class="field mono" placeholder="prod, db" value="{{$page.Tags}}">
<div class="field-help">Free-form. Used for filtering and grouping on the dashboard.</div>
</div>
<h3 class="text-[13px] font-semibold uppercase tracking-[0.08em] text-ink-mute mb-4 pt-6 border-t border-line-soft">Default backup paths</h3>
<div class="mb-7">
<label class="field-label" for="ah-paths">Paths <span class="text-ink-fade font-normal">· one per line</span></label>
<textarea id="ah-paths" name="paths" rows="3" class="field mono"
style="resize: vertical;"
placeholder="/etc&#10;/home&#10;/var/lib/postgresql">{{$page.Paths}}</textarea>
<div class="field-help">
What <span class="mono text-ink-mid">restic backup</span> runs against when an operator hits “Run now”. Until schedules ship in Phase 2, this is the only source of paths for run-now jobs — leave it empty if youll dispatch via the JSON API instead.
</div>
</div>
<h3 class="text-[13px] font-semibold uppercase tracking-[0.08em] text-ink-mute mb-4 pt-6 border-t border-line-soft">Restic repository</h3>
<div class="mb-5">
<label class="field-label" for="ah-url">Repo URL</label>
<input id="ah-url" name="repo_url" type="text" class="field mono" required
placeholder="rest:https://restic.lab/host-name/"
value="{{$page.RepoURL}}">
<div class="field-help">Whatever <span class="mono text-ink-mid">restic -r</span> would accept. Most fleets terminate at a <span class="mono text-ink-mid">restic/rest-server</span>; <span class="mono text-ink-mid">s3:</span> and <span class="mono text-ink-mid">b2:</span> URLs work equally well.</div>
</div>
<div class="mb-5">
<label class="field-label" for="ah-user">Repo username <span class="text-ink-fade font-normal">· optional</span></label>
<input id="ah-user" name="repo_username" type="text" class="field mono" value="{{$page.RepoUsername}}">
<div class="field-help">For <span class="mono text-ink-mid">rest-server</span> with htpasswd, this is the per-host user.</div>
</div>
<div class="mb-7">
<label class="field-label" for="ah-pass">Repo password</label>
<input id="ah-pass" name="repo_password" type="password" class="field" required>
<div class="field-help">Encrypted at rest using the servers AEAD key. Pushed to the agent only over the authenticated WebSocket.</div>
</div>
<div class="flex gap-2 pt-5 border-t border-line-soft">
<button type="submit" class="btn btn-primary btn-lg">Mint token &amp; show install command</button>
<a href="/" class="btn btn-lg">Cancel</a>
</div>
</div>
<aside class="col-span-5">
<div class="text-[11px] uppercase tracking-[0.1em] text-ink-fade mb-3">What happens next</div>
<ol class="list-none p-0 m-0 space-y-4">
<li class="relative pl-9">
<span class="absolute left-0 top-0 w-[22px] h-[22px] border border-line rounded-full text-[11px] leading-[20px] text-center text-ink-mute mono">1</span>
<div class="text-[13px] text-ink font-medium">You get a one-time install command</div>
<div class="text-[12px] text-ink-mute mt-1 leading-[1.55]">A <span class="mono text-ink-mid">curl … | sh</span> snippet with the server URL and a 1h token baked in.</div>
</li>
<li class="relative pl-9">
<span class="absolute left-0 top-0 w-[22px] h-[22px] border border-line rounded-full text-[11px] leading-[20px] text-center text-ink-mute mono">2</span>
<div class="text-[13px] text-ink font-medium">You run it on the box you want to back up</div>
<div class="text-[12px] text-ink-mute mt-1 leading-[1.55]">Installer creates a service user, drops the agent binary, registers a sandboxed systemd unit, and enrols.</div>
</li>
<li class="relative pl-9">
<span class="absolute left-0 top-0 w-[22px] h-[22px] border border-line rounded-full text-[11px] leading-[20px] text-center text-ink-mute mono">3</span>
<div class="text-[13px] text-ink font-medium">The host appears on the dashboard within seconds</div>
<div class="text-[12px] text-ink-mute mt-1 leading-[1.55]">Server pushes the encrypted repo creds over the WS on first <span class="mono text-ink-mid">hello</span>; agent decrypts and persists to <span class="mono text-ink-mid">secrets.enc</span>.</div>
</li>
<li class="relative pl-9">
<span class="absolute left-0 top-0 w-[22px] h-[22px] border border-line rounded-full text-[11px] leading-[20px] text-center text-ink-mute mono">4</span>
<div class="text-[13px] text-ink font-medium">You hit “Run backup now”</div>
<div class="text-[12px] text-ink-mute mt-1 leading-[1.55]">First snapshot lands in the repo. Subsequent ones run on whatever schedule you set (Phase 2).</div>
</li>
</ol>
<div class="mt-8 panel rounded-[6px] px-4 py-3.5">
<div class="text-[11px] uppercase tracking-[0.08em] font-semibold text-warn mb-1.5">Prerequisite</div>
<p class="text-pretty text-[12px] text-ink-mid leading-[1.55]">
<span class="mono text-ink">restic</span> ≥ 0.16 must already be installed on the target host. The agent does not install it for you — different distros, different package managers, too much surface area to maintain.
</p>
</div>
</aside>
</form>
{{else}}
{{/* ============================================================
State B · token minted
============================================================ */}}
<div class="flex items-center gap-3 mt-2.5">
<h1 class="text-2xl font-medium tracking-[-0.012em]">Token minted</h1>
<span class="mono text-[11px] px-2 py-0.5 rounded-[3px]"
style="background: color-mix(in oklch, var(--ok), transparent 88%);
color: var(--ok);
border: 1px solid color-mix(in oklch, var(--ok), transparent 70%);">
expires {{relTime $page.ExpiresAt}}
</span>
</div>
<p class="text-pretty text-ink-mute text-[13px] mt-1.5 max-w-[580px]">
Run the snippet below on the target box. The host will appear on the
dashboard within a few seconds of the agent connecting.
</p>
<div class="snippet mt-6 panel">
<div class="snippet-head">
<span>Install command · paste-and-run</span>
<div class="flex gap-2">
<button type="button" class="btn"
data-snippet="curl -fsSL {{$page.ServerURL}}/install.sh | sudo RM_SERVER={{$page.ServerURL}} RM_TOKEN={{$page.Token}} sh"
onclick="navigator.clipboard.writeText(this.dataset.snippet); this.textContent='Copied'; setTimeout(()=>this.textContent='Copy', 2000);">Copy</button>
</div>
</div>
<pre>curl -fsSL <span class="var">{{$page.ServerURL}}/install.sh</span> | sudo \
RM_SERVER=<span class="var">{{$page.ServerURL}}</span> \
RM_TOKEN=<span class="var">{{$page.Token}}</span> sh</pre>
</div>
<div class="grid grid-cols-12 gap-6 mt-7">
<div class="col-span-7 panel rounded-[7px] px-7 py-6">
<div class="text-[11px] uppercase tracking-[0.1em] text-ink-fade mb-3">Awaiting agent connection</div>
<div class="flex items-center gap-3">
<span class="dot dot-offline pulse"></span>
<span class="mono text-[14px] text-ink">{{if $page.Hostname}}{{$page.Hostname}}{{else}}new host{{end}}</span>
<span class="text-[12px] text-ink-mute">— enrolment will mark this online</span>
</div>
<div class="mt-4 px-3 py-2.5 rounded-[5px] mono text-[11.5px] text-ink-mute leading-[1.7]"
style="background: var(--bg); border: 1px solid var(--line-soft);">
<div>{{$page.ExpiresAt.Format "15:04:05.000"}} <span class="text-ink-mid">server</span> token minted · 1h ttl</div>
<div class="text-ink-fade"> awaiting POST /api/agents/enroll …</div>
</div>
</div>
<aside class="col-span-5">
<div class="text-[11px] uppercase tracking-[0.1em] text-ink-fade mb-3">If the agent doesnt appear</div>
<ul class="list-none p-0 m-0 text-[13px] text-ink-mid leading-[1.55]">
<li class="py-2 border-b border-line-soft text-pretty">Check the box can reach <span class="mono text-ink">{{$page.ServerURL}}</span> over HTTPS.</li>
<li class="py-2 border-b border-line-soft text-pretty">Check <span class="mono text-ink">restic --version</span> ≥ 0.16 — the installer wont bail on this, but backups will fail.</li>
<li class="py-2 border-b border-line-soft text-pretty">Check <span class="mono text-ink">journalctl -u restic-manager-agent -n 50</span> on the target box.</li>
<li class="py-2 text-pretty">Token expired? <a href="/hosts/new" class="underline underline-offset-4 decoration-line">Mint a new one</a> — theyre cheap.</li>
</ul>
</aside>
</div>
<div class="mt-7 flex gap-2">
<a href="/" class="btn btn-lg">← Back to dashboard</a>
<a href="/hosts/new" class="btn btn-lg">Add another host</a>
</div>
{{end}}
</div>
{{end}}