8a05969953
Two issues from a smoke session:
1. The awaiting-agent panel never refreshed — operator had to go
back to the dashboard to see the host had connected.
2. Generated passwords were displayed only on the POST response.
Navigating away (or even an accidental tab close) lost them
permanently, so the operator couldn't update the rest-server's
htpasswd.
Both are the same fix: convert the POST-rendered transient
"result state" into a durable GET page at /hosts/pending/{token}.
* New route GET /hosts/pending/{token} renders the install-command +
htpasswd snippet view. Password is decrypted from the (still-
encrypted-at-rest) token row on every render — operator can
refresh, bookmark, navigate away and come back. Once the agent
enrols, the page redirects to /hosts/{id}; once the token
expires, redirect to /hosts/new.
* New route GET /hosts/pending/{token}/awaiting returns a polled
HTML fragment that the pending page swaps in every 2s via HTMX.
States: awaiting (keep polling) | connected (show "Open host →"
+ "View schedules" CTAs, polling stops) | expired (mint-new
link, polling stops). Polling stops naturally because only the
awaiting state's wrapper carries the hx-trigger attribute.
* POST /hosts/new now 303-redirects to /hosts/pending/{token}
on success; validation errors keep re-rendering the form with
banner.
Supporting changes:
* New store helper Store.GetEnrollmentTokenStatus(tokenHash) for
the polling endpoint — returns {expires_at, consumed_at,
consumed_host} in one round-trip without dragging in the
attachments-decryption path.
* New ui.Renderer.RenderPartial(w, name, data) for HTMX fragment
responses (no layout wrap). Picks an arbitrary page's template
set as the lookup point — every page parses the full common-
paths list, so they all see every partial.
* add_host.html stripped to form-only; pending_host.html owns the
result-state UI; awaiting_agent.html is the polled partial.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
99 lines
5.3 KiB
HTML
99 lines
5.3 KiB
HTML
{{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>
|
|
<a href="/hosts/new">Add host</a><span class="sep">/</span>
|
|
<span class="text-ink-mid">pending</span>
|
|
</div>
|
|
|
|
<div class="flex items-center gap-3 mt-2.5">
|
|
<h1 class="text-2xl font-medium tracking-[-0.012em]">Pending host</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-[680px]">
|
|
Token's still alive — refresh the page or come back later until the
|
|
agent enrols. Credentials are decrypted from the (still-encrypted-at-rest)
|
|
token row each render, so you can recover them if you've already lost
|
|
the snippets below. Once the agent connects this page redirects to
|
|
the host detail.
|
|
</p>
|
|
|
|
{{if and $page.RepoUsername $page.RepoPassword}}
|
|
<div class="snippet mt-6 panel" style="border-color: color-mix(in oklch, var(--warn), transparent 60%);">
|
|
<div class="snippet-head" style="background: color-mix(in oklch, var(--warn), transparent 92%);">
|
|
<span>
|
|
Run on the rest-server box first
|
|
<span class="text-ink-fade ml-2">· paste-and-run after replacing the htpasswd path</span>
|
|
</span>
|
|
<div class="flex gap-2">
|
|
<button type="button" class="btn"
|
|
data-snippet="echo '{{$page.RepoPassword}}' | sudo htpasswd -B -i /path/to/htpasswd {{$page.RepoUsername}}"
|
|
onclick="navigator.clipboard.writeText(this.dataset.snippet); this.textContent='Copied'; setTimeout(()=>this.textContent='Copy', 2000);">Copy</button>
|
|
</div>
|
|
</div>
|
|
<pre>echo '<span class="var">{{$page.RepoPassword}}</span>' | sudo htpasswd -B -i <span class="var">/path/to/htpasswd</span> <span class="var">{{$page.RepoUsername}}</span></pre>
|
|
<div class="px-4 pt-1 pb-3 text-[12px] text-ink-mute leading-[1.55]">
|
|
Replace <span class="mono text-ink-mid">/path/to/htpasswd</span> with whatever your <span class="mono text-ink-mid">restic/rest-server</span> reads (typically the file passed via <span class="mono text-ink-mid">--htpasswd-file</span>, or <span class="mono text-ink-mid">/data/.htpasswd</span> in the official Docker image). The <span class="mono text-ink-mid">-i</span> flag reads the password from stdin so it never appears in your shell's process list. Then either send <span class="mono text-ink-mid">SIGHUP</span> to the rest-server process or restart the container to pick up the new entry.
|
|
</div>
|
|
</div>
|
|
{{end}}
|
|
|
|
<div class="snippet mt-4 panel">
|
|
<div class="snippet-head">
|
|
<span>Install command · paste-and-run on the host you're backing up</span>
|
|
<div class="flex gap-2">
|
|
<button type="button" class="btn"
|
|
data-snippet="curl -fsSL {{$page.ServerURL}}/install/install.sh | sudo RM_SERVER={{$page.ServerURL}} RM_TOKEN={{$page.Token}} bash"
|
|
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/install.sh</span> | sudo \
|
|
RM_SERVER=<span class="var">{{$page.ServerURL}}</span> \
|
|
RM_TOKEN=<span class="var">{{$page.Token}}</span> bash</pre>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-12 gap-6 mt-7">
|
|
|
|
<div class="col-span-7"
|
|
hx-get="/hosts/pending/{{$page.Token}}/awaiting"
|
|
hx-trigger="load, every 2s"
|
|
hx-swap="outerHTML">
|
|
{{/* Fallback content; HTMX swaps this out within 2s of load. */}}
|
|
<div class="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="text-[12px] text-ink-mute">checking…</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<aside class="col-span-5">
|
|
<div class="text-[11px] uppercase tracking-[0.1em] text-ink-fade mb-3">If the agent doesn't 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>.</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 won't 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> — they're 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>
|
|
|
|
</div>
|
|
{{end}}
|