383bdb7d36
CI / Test (rest) (pull_request) Successful in 39s
CI / Test (store) (pull_request) Successful in 42s
CI / Lint (pull_request) Successful in 11s
CI / Build (windows/amd64) (pull_request) Successful in 7s
CI / Build (linux/amd64) (pull_request) Successful in 8s
CI / Build (linux/arm64) (pull_request) Successful in 8s
CI / Test (server-http) (pull_request) Successful in 1m45s
e2e / Playwright vs docker-compose (pull_request) Successful in 1m17s
192 lines
10 KiB
HTML
192 lines
10 KiB
HTML
{{/*
|
||
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>
|
||
{{if $page.Active.PauseAfterFirst}} · canary-first{{end}}
|
||
· 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}}
|
||
{{if eq $page.Active.Status "halted"}}
|
||
<div class="mt-3 flex gap-2 text-[12px]">
|
||
<a class="btn" href="#fleet-update-start-form">Resume remaining as new rollout</a>
|
||
{{range $page.ActiveRows}}{{if eq .Status "failed"}}<button class="btn" type="button" onclick="fleetSelectOnly('{{.HostID}}');document.getElementById('fleet-update-start-form').scrollIntoView()">Retry {{.HostName}}</button>{{end}}{{end}}
|
||
</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.Candidates) 0}}
|
||
<div class="flex items-center gap-3">
|
||
<span class="dot dot-online"></span>
|
||
<div>
|
||
<div class="text-ink text-[14px] font-medium">No hosts enrolled.</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>
|
||
<div class="mt-3 flex flex-wrap gap-2">
|
||
<input id="fleet-filter-name" class="field text-[12px]" placeholder="Filter name or tag" oninput="fleetFilter()">
|
||
<input id="fleet-filter-version" class="field text-[12px] mono" placeholder="Version" oninput="fleetFilter()">
|
||
<select id="fleet-filter-state" class="field text-[12px]" onchange="fleetFilter()">
|
||
<option value="all">All states</option><option value="eligible">Eligible</option><option value="excluded">Excluded</option>
|
||
</select>
|
||
<button type="button" class="btn" onclick="fleetSelectVisible(true)">Select visible eligible</button>
|
||
<button type="button" class="btn" onclick="fleetSelectVisible(false)">Clear selection</button>
|
||
</div>
|
||
<div class="panel mt-3 rounded-[7px] overflow-hidden">
|
||
{{range $page.Candidates}}
|
||
<label class="fleet-candidate grid items-center px-3 py-2 hairline text-[12px]"
|
||
data-name="{{.Host.Name}} {{range .Host.Tags}}{{.}} {{end}}" data-version="{{.Host.AgentVersion}}" data-state="{{if .Eligible}}eligible{{else}}excluded{{end}}"
|
||
style="grid-template-columns: 28px 1.4fr .8fr .8fr 1.2fr;gap:12px">
|
||
<input class="fleet-host" type="checkbox" value="{{.Host.ID}}" {{if .Eligible}}checked onchange="fleetReview()"{{else}}disabled{{end}}>
|
||
<span class="mono">{{.Host.Name}}</span>
|
||
<span class="mono text-ink-mute">{{if .Host.AgentVersion}}{{.Host.AgentVersion}}{{else}}unknown{{end}}</span>
|
||
<span class="mono text-ink-mute">{{$page.TargetVersion}}</span>
|
||
<span class="text-ink-mute">{{if .Eligible}}eligible{{else}}{{.Reason}}{{end}}</span>
|
||
</label>
|
||
{{end}}
|
||
</div>
|
||
<div class="mt-4 text-[12px] text-ink-mute">
|
||
<span id="fleet-selected-count">{{len $page.OutOfDateHosts}}</span> selected · sequential, halts on first failure · worst-case
|
||
<span id="fleet-timeout" class="mono">{{len $page.OutOfDateHosts}} × 95s</span>
|
||
</div>
|
||
<form class="mt-3 flex items-center gap-3" onsubmit="return fleetStart(event)">
|
||
<label class="text-[11.5px] text-ink-mute"><input id="fleet-canary" type="checkbox" checked> Pause after first host for canary review</label>
|
||
<label class="text-[11.5px] text-ink-mute">Type selected count to confirm:</label>
|
||
<input id="fleet-update-confirm" class="field mono text-[12.5px]" style="width:80px;padding:5px 8px" oninput="fleetReview()" autocomplete="off">
|
||
<button id="fleet-update-start-btn" class="btn btn-amber" disabled>Start agent update</button>
|
||
<span id="fleet-start-error" class="text-bad text-[12px]"></span>
|
||
</form>
|
||
<script>
|
||
function fleetBoxes(){return Array.from(document.querySelectorAll('.fleet-host:not(:disabled)'))}
|
||
function fleetReview(){const n=fleetBoxes().filter(x=>x.checked).length;document.getElementById('fleet-selected-count').textContent=n;document.getElementById('fleet-timeout').textContent=n+' × 95s';document.getElementById('fleet-update-start-btn').disabled=n===0||document.getElementById('fleet-update-confirm').value!==String(n)}
|
||
function fleetFilter(){const q=document.getElementById('fleet-filter-name').value.toLowerCase(),v=document.getElementById('fleet-filter-version').value.toLowerCase(),s=document.getElementById('fleet-filter-state').value;document.querySelectorAll('.fleet-candidate').forEach(r=>r.hidden=!(r.dataset.name.toLowerCase().includes(q)&&r.dataset.version.toLowerCase().includes(v)&&(s==='all'||r.dataset.state===s)))}
|
||
function fleetSelectVisible(on){document.querySelectorAll('.fleet-candidate:not([hidden]) .fleet-host:not(:disabled)').forEach(x=>x.checked=on);fleetReview()}
|
||
function fleetSelectOnly(id){fleetBoxes().forEach(x=>x.checked=x.value===id);fleetReview()}
|
||
async function fleetStart(e){e.preventDefault();const ids=fleetBoxes().filter(x=>x.checked).map(x=>x.value),out=document.getElementById('fleet-start-error');out.textContent='';const res=await fetch('/api/fleet/update',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({host_ids:ids,canary_first:document.getElementById('fleet-canary').checked})});if(res.ok){location.reload();return false}const body=await res.json();out.textContent=body.message||body.code||'Unable to start';return false}
|
||
fleetReview()
|
||
</script>
|
||
{{end}}
|
||
</div>
|
||
{{end}}
|