Files
restic-manager/web/templates/partials/awaiting_agent.html
T
steve c1f85da55f 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>
2026-05-02 12:59:24 +01:00

60 lines
2.7 KiB
HTML

{{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}}