Fix Agent updates filters and refresh state #51

Merged
steve merged 1 commits from fix-issue-50-agent-update-filters into main 2026-08-22 12:06:45 +01:00
3 changed files with 70 additions and 12 deletions
Showing only changes of commit 6374201f6c - Show all commits
+39
View File
@@ -6,7 +6,9 @@ import (
"bytes"
"context"
"encoding/json"
"io"
stdhttp "net/http"
"strings"
"sync"
"testing"
"time"
@@ -19,6 +21,43 @@ import (
"gitea.dcglab.co.uk/steve/restic-manager/internal/version"
)
func TestFleetUpdatePagePreservesFiltersWithoutTypedConfirmation(t *testing.T) {
t.Parallel()
_, baseURL, st := newTestServerWithUI(t)
cookie := loginAsAdmin(t, st)
_ = makeHost(t, st, "fleet-filter-host")
req, _ := stdhttp.NewRequest("GET", baseURL+"/settings/fleet-update", nil)
req.AddCookie(cookie)
res, err := stdhttp.DefaultClient.Do(req)
if err != nil {
t.Fatalf("get fleet update page: %v", err)
}
defer res.Body.Close()
if res.StatusCode != stdhttp.StatusOK {
t.Fatalf("status: got %d, want 200", res.StatusCode)
}
raw, _ := io.ReadAll(res.Body)
body := string(raw)
for _, want := range []string{
`id="fleet-filter-name"`,
`id="fleet-filter-version"`,
`id="fleet-filter-state"`,
"htmx:beforeSwap",
"fleetCaptureUIState()",
"fleetRestoreUIState()",
"r.style.display=",
} {
if !strings.Contains(body, want) {
t.Errorf("page missing %q", want)
}
}
if strings.Contains(body, "Type selected count to confirm") || strings.Contains(body, `id="fleet-update-confirm"`) {
t.Error("page still renders the typed-count confirmation")
}
}
// fakeFleetWorker stands in for *fleetupdate.Worker in HTTP tests.
// It records what was passed to Start/Cancel and lets tests inject
// canned errors. Satisfies the FleetWorker interface in
+30
View File
@@ -28,5 +28,35 @@
{{template "fleet_update_inner" .}}
<script>
let fleetUpdateUIState = null
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}
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.style.display=r.dataset.name.toLowerCase().includes(q)&&r.dataset.version.toLowerCase().includes(v)&&(s==='all'||r.dataset.state===s)?'':'none')}
function fleetSelectVisible(on){document.querySelectorAll('.fleet-candidate').forEach(r=>{const x=r.querySelector('.fleet-host:not(:disabled)');if(x&&r.style.display!=='none')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}
function fleetCaptureUIState(){
const name=document.getElementById('fleet-filter-name'),version=document.getElementById('fleet-filter-version'),state=document.getElementById('fleet-filter-state'),canary=document.getElementById('fleet-canary')
if(!name)return
fleetUpdateUIState={name:name.value,version:version.value,state:state.value,canary:canary.checked,selected:fleetBoxes().filter(x=>x.checked).map(x=>x.value)}
}
function fleetRestoreUIState(){
if(!fleetUpdateUIState||!document.getElementById('fleet-filter-name'))return
document.getElementById('fleet-filter-name').value=fleetUpdateUIState.name
document.getElementById('fleet-filter-version').value=fleetUpdateUIState.version
document.getElementById('fleet-filter-state').value=fleetUpdateUIState.state
document.getElementById('fleet-canary').checked=fleetUpdateUIState.canary
const selected=new Set(fleetUpdateUIState.selected)
fleetBoxes().forEach(x=>x.checked=selected.has(x.value))
fleetFilter();fleetReview()
}
document.body.addEventListener('htmx:beforeSwap',e=>{if(e.detail.target.id==='fleet-update-panel')fleetCaptureUIState()})
document.body.addEventListener('htmx:afterSwap',e=>{if(e.detail.target.id==='fleet-update-panel')fleetRestoreUIState()})
if(document.getElementById('fleet-selected-count'))fleetReview()
</script>
</div>
{{end}}
+1 -12
View File
@@ -172,20 +172,9 @@
</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>
<button id="fleet-update-start-btn" class="btn btn-amber">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}}