Add selectable canary-first fleet updates
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
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
This commit is contained in:
@@ -32,6 +32,7 @@
|
||||
</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>
|
||||
@@ -68,6 +69,12 @@
|
||||
{{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}}
|
||||
@@ -122,11 +129,11 @@
|
||||
{{define "fleet_update_idle_panel"}}
|
||||
{{$page := .}}
|
||||
<div class="panel rounded-[7px] px-5 py-4">
|
||||
{{if eq (len $page.OutOfDateHosts) 0}}
|
||||
{{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">All hosts are up to date.</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>
|
||||
@@ -137,35 +144,48 @@
|
||||
<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:
|
||||
<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>
|
||||
<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>
|
||||
{{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}}
|
||||
|
||||
Reference in New Issue
Block a user