Add-host: durable pending page + polled awaiting-agent panel
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>
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
{{define "awaiting_agent"}}
|
||||
{{$page := .Page}}
|
||||
{{/*
|
||||
Polled status fragment for the Add-host pending page. Wrapper
|
||||
carries the HTMX poll trigger only while State == "awaiting" —
|
||||
state == "connected" or "expired" both stop polling. The wrapper
|
||||
is what HTMX swaps via hx-swap=outerHTML, so the trigger getting
|
||||
removed is what stops the loop.
|
||||
*/}}
|
||||
<div class="col-span-7"
|
||||
{{if eq $page.State "awaiting"}}hx-get="/hosts/pending/{{$page.Token}}/awaiting"
|
||||
hx-trigger="every 2s"
|
||||
hx-swap="outerHTML"{{end}}>
|
||||
|
||||
{{if eq $page.State "connected"}}
|
||||
<div class="panel rounded-[7px] px-7 py-6"
|
||||
style="border-color: color-mix(in oklch, var(--ok), transparent 60%);">
|
||||
<div class="text-[11px] uppercase tracking-[0.1em] font-semibold text-ok mb-3">Agent connected</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="dot dot-online"></span>
|
||||
<span class="mono text-[14px] text-ink">{{$page.HostName}}</span>
|
||||
<span class="text-[12px] text-ink-mute">— enrolled, online{{if $page.LastSeenAt}}, last heartbeat {{relTime $page.LastSeenAt}}{{end}}</span>
|
||||
</div>
|
||||
<div class="mt-4 flex gap-2">
|
||||
<a href="/hosts/{{$page.HostID}}" class="btn btn-primary">Open host →</a>
|
||||
<a href="/hosts/{{$page.HostID}}/schedules" class="btn">View schedules</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{else if eq $page.State "expired"}}
|
||||
<div class="panel rounded-[7px] px-7 py-6"
|
||||
style="border-color: color-mix(in oklch, var(--bad), transparent 60%);">
|
||||
<div class="text-[11px] uppercase tracking-[0.1em] font-semibold text-bad mb-3">Token expired</div>
|
||||
<p class="text-[13px] text-ink-mid leading-[1.55]">
|
||||
The 1-hour window has elapsed without the agent connecting.
|
||||
Mint a fresh token and run the new install command.
|
||||
</p>
|
||||
<div class="mt-4">
|
||||
<a href="/hosts/new" class="btn btn-primary">Mint a new token</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{else}}
|
||||
<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">— polling every 2s; this page redirects to the host detail when enrolment lands.</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>token expires <span class="text-ink-mid">{{relTime $page.ExpiresAt}}</span></div>
|
||||
<div class="text-ink-fade">awaiting POST /api/agents/enroll …</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
</div>
|
||||
{{end}}
|
||||
Reference in New Issue
Block a user