ui: fleet update page + endpoints
- POST /api/fleet/update, POST /api/fleet-updates/{id}/cancel,
GET /api/fleet-updates/{id} (admin-only).
- GET /settings/fleet-update + /partial for htmx polling.
- Renders idle / running / terminal states with per-host progress.
- Tests cover happy path, derive-host-ids, conflict, cancel, get,
and RBAC.
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
{{define "title"}}Fleet update · restic-manager{{end}}
|
||||
|
||||
{{define "content"}}
|
||||
{{$page := .Page}}
|
||||
<div class="max-w-[1280px] mx-auto px-8 pb-14">
|
||||
|
||||
{{/* breadcrumbs */}}
|
||||
<div class="crumbs pt-6">
|
||||
<a href="/">Dashboard</a><span class="sep">/</span>
|
||||
<a href="/settings">Settings</a><span class="sep">/</span>
|
||||
<span class="text-ink-mid">fleet update</span>
|
||||
</div>
|
||||
|
||||
{{/* page header */}}
|
||||
<div class="flex items-baseline justify-between mt-3.5">
|
||||
<div>
|
||||
<h1 class="text-[22px] font-medium tracking-[-0.005em]">
|
||||
Fleet update
|
||||
<span class="text-ink-fade font-normal text-[14px] ml-2 mono">target {{$page.TargetVersion}}</span>
|
||||
</h1>
|
||||
<p class="text-ink-mute text-[12px] mt-1 max-w-[760px] leading-[1.55]">
|
||||
Rolling, sequential agent self-update. One host at a time, halts on first failure,
|
||||
cancellable mid-roll. Only online hosts whose <span class="mono">agent_version</span>
|
||||
differs from the server are eligible.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{template "fleet_update_inner" .}}
|
||||
|
||||
</div>
|
||||
{{end}}
|
||||
@@ -0,0 +1,171 @@
|
||||
{{/*
|
||||
fleet_update_inner — inner panel for /settings/fleet-update.
|
||||
Rendered both as part of the full page and as the htmx polling
|
||||
fragment via /settings/fleet-update/partial.
|
||||
|
||||
Expects .Page to be a fleetUpdatePage struct (see fleet_update.go).
|
||||
*/}}
|
||||
{{define "fleet_update_inner"}}
|
||||
{{$page := .Page}}
|
||||
<div id="fleet-update-panel" class="mt-5"
|
||||
hx-get="{{$page.PollURL}}"
|
||||
hx-trigger="every 3s [document.visibilityState==='visible']"
|
||||
hx-select="#fleet-update-panel"
|
||||
hx-swap="outerHTML">
|
||||
|
||||
{{if and $page.Active (eq $page.Active.Status "running")}}
|
||||
|
||||
{{/* ---------- running state ---------- */}}
|
||||
<div class="panel rounded-[7px] px-5 py-4">
|
||||
<div class="flex items-baseline justify-between">
|
||||
<div>
|
||||
<span class="mono text-[12px] text-ink-fade">fleet update</span>
|
||||
<span class="mono text-[12px] text-accent ml-2">running</span>
|
||||
<span class="mono text-[11px] text-ink-fade ml-2">{{$page.Active.ID}}</span>
|
||||
</div>
|
||||
<form hx-post="/api/fleet-updates/{{$page.Active.ID}}/cancel" hx-swap="none">
|
||||
<button class="btn btn-danger" type="submit"
|
||||
onclick="return confirm('Cancel this fleet update? Hosts already updated stay updated; pending hosts will be skipped.');">
|
||||
Cancel
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="text-[11.5px] text-ink-mute mt-1">
|
||||
target <span class="mono text-ink-mid">{{$page.Active.TargetVersion}}</span>
|
||||
· started <span class="mono text-ink-mid">{{relTime $page.Active.StartedAt}}</span>
|
||||
{{if $page.Active.CurrentHostID}}
|
||||
· waiting on <span class="mono text-ink-mid">{{index $page.HostNames $page.Active.CurrentHostID}}</span>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{template "fleet_update_rows" $page}}
|
||||
|
||||
{{else if $page.Active}}
|
||||
|
||||
{{/* ---------- terminal state (completed / halted / cancelled) ---------- */}}
|
||||
<div class="panel rounded-[7px] px-5 py-4">
|
||||
<div class="flex items-baseline justify-between">
|
||||
<div>
|
||||
<span class="mono text-[12px] text-ink-fade">last fleet update</span>
|
||||
{{if eq $page.Active.Status "completed"}}
|
||||
<span class="mono text-[12px] text-ok ml-2">completed</span>
|
||||
{{else if eq $page.Active.Status "halted"}}
|
||||
<span class="mono text-[12px] text-bad ml-2">halted</span>
|
||||
{{else if eq $page.Active.Status "cancelled"}}
|
||||
<span class="mono text-[12px] text-warn ml-2">cancelled</span>
|
||||
{{else}}
|
||||
<span class="mono text-[12px] text-ink-mid ml-2">{{$page.Active.Status}}</span>
|
||||
{{end}}
|
||||
<span class="mono text-[11px] text-ink-fade ml-2">{{$page.Active.ID}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-[11.5px] text-ink-mute mt-1">
|
||||
target <span class="mono text-ink-mid">{{$page.Active.TargetVersion}}</span>
|
||||
· started <span class="mono text-ink-mid">{{relTime $page.Active.StartedAt}}</span>
|
||||
{{if $page.Active.CompletedAt}} · finished <span class="mono text-ink-mid">{{relTime $page.Active.CompletedAt}}</span>{{end}}
|
||||
</div>
|
||||
{{if $page.Active.HaltedReason}}
|
||||
<div class="text-[12px] text-bad mt-2">{{$page.Active.HaltedReason}}</div>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
{{template "fleet_update_rows" $page}}
|
||||
|
||||
{{if gt (len $page.OutOfDateHosts) 0}}
|
||||
<div class="mt-5">
|
||||
{{template "fleet_update_idle_panel" $page}}
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{else}}
|
||||
|
||||
{{template "fleet_update_idle_panel" $page}}
|
||||
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{define "fleet_update_rows"}}
|
||||
{{$page := .}}
|
||||
<div class="panel mt-3 rounded-[7px] overflow-hidden">
|
||||
<div class="hairline grid items-baseline px-4 py-2.5 text-[11px] text-ink-fade uppercase tracking-[0.08em]"
|
||||
style="grid-template-columns: 0.4fr 1.5fr 0.8fr 1.2fr 1.5fr; column-gap: 18px;">
|
||||
<div>#</div>
|
||||
<div>Host</div>
|
||||
<div>Status</div>
|
||||
<div>Job</div>
|
||||
<div>Detail</div>
|
||||
</div>
|
||||
{{range $page.ActiveRows}}
|
||||
<div class="grid items-center px-4 py-2.5 text-[12.5px] hairline"
|
||||
style="grid-template-columns: 0.4fr 1.5fr 0.8fr 1.2fr 1.5fr; column-gap: 18px;">
|
||||
<div class="mono text-ink-fade">{{.Position}}</div>
|
||||
<div class="mono text-ink">{{if .HostName}}{{.HostName}}{{else}}{{.HostID}}{{end}}</div>
|
||||
<div>
|
||||
{{if eq .Status "pending"}}<span class="text-ink-fade">pending</span>
|
||||
{{else if eq .Status "running"}}<span class="text-accent">running…</span>
|
||||
{{else if eq .Status "succeeded"}}<span class="text-ok">succeeded</span>
|
||||
{{else if eq .Status "failed"}}<span class="text-bad font-medium">failed</span>
|
||||
{{else if eq .Status "skipped"}}<span class="text-ink-mute">skipped</span>
|
||||
{{else}}<span class="text-ink-mute">{{.Status}}</span>{{end}}
|
||||
</div>
|
||||
<div>
|
||||
{{if .JobID}}<a class="link mono text-[11.5px]" href="/jobs/{{.JobID}}">{{.JobID}}</a>{{else}}<span class="text-ink-fade">—</span>{{end}}
|
||||
</div>
|
||||
<div class="mono text-[11.5px] text-ink-mute truncate" title="{{.FailedReason}}">{{.FailedReason}}</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{define "fleet_update_idle_panel"}}
|
||||
{{$page := .}}
|
||||
<div class="panel rounded-[7px] px-5 py-4">
|
||||
{{if eq (len $page.OutOfDateHosts) 0}}
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="dot dot-online"></span>
|
||||
<div>
|
||||
<div class="text-ink text-[14px] font-medium">All hosts are up to date.</div>
|
||||
<div class="text-ink-mute text-[12px] mt-0.5">
|
||||
Every online agent matches server version <span class="mono">{{$page.TargetVersion}}</span>.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="flex items-baseline justify-between">
|
||||
<h2 class="text-[14px] font-medium">{{len $page.OutOfDateHosts}} host{{if ne (len $page.OutOfDateHosts) 1}}s{{end}} out of date</h2>
|
||||
<span class="mono text-[11px] text-ink-fade">target {{$page.TargetVersion}}</span>
|
||||
</div>
|
||||
<ul class="mt-3 space-y-1 text-[12px]">
|
||||
{{range $page.OutOfDateHosts}}
|
||||
<li class="flex items-center gap-3">
|
||||
<span class="dot dot-online"></span>
|
||||
<span class="mono text-ink">{{.Name}}</span>
|
||||
<span class="mono text-ink-mute">{{if .AgentVersion}}{{.AgentVersion}}{{else}}—{{end}} → {{$page.TargetVersion}}</span>
|
||||
</li>
|
||||
{{end}}
|
||||
</ul>
|
||||
|
||||
<form id="fleet-update-start-form" class="mt-4 flex items-center gap-3"
|
||||
hx-post="/api/fleet/update"
|
||||
hx-headers='{"Content-Type":"application/json"}'
|
||||
hx-vals='{}'
|
||||
hx-swap="none"
|
||||
hx-on::after-request="if(event.detail.successful) location.reload()">
|
||||
<label class="text-[11.5px] text-ink-mute">
|
||||
Type the count
|
||||
<span class="mono text-ink-mid">({{len $page.OutOfDateHosts}})</span>
|
||||
to enable Start:
|
||||
</label>
|
||||
<input type="text" id="fleet-update-confirm" class="field mono text-[12.5px]"
|
||||
style="width: 80px; padding: 5px 8px;"
|
||||
oninput="document.getElementById('fleet-update-start-btn').disabled = (this.value !== '{{len $page.OutOfDateHosts}}');"
|
||||
autocomplete="off" />
|
||||
<button type="submit" id="fleet-update-start-btn" class="btn btn-amber" disabled>
|
||||
Start fleet update
|
||||
</button>
|
||||
</form>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
Reference in New Issue
Block a user