Compare commits
6 Commits
v1.2.0
...
3ccefc254a
| Author | SHA1 | Date | |
|---|---|---|---|
| 3ccefc254a | |||
| 4ff6c7296d | |||
| bc5c99898c | |||
| 80dac33f0a | |||
| facef5b254 | |||
| 6374201f6c |
@@ -6,6 +6,15 @@ and the project follows [Semantic Versioning](https://semver.org/).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [1.2.1] - 2026-08-22
|
||||
|
||||
### Fixed
|
||||
|
||||
- Made the Agent updates host-selection screen stable and responsive: removed
|
||||
the redundant typed-count confirmation, corrected name/version/eligibility
|
||||
filtering, and stopped replacing idle or terminal controls through polling.
|
||||
Progress polling now runs only while a rollout is actively running. ([#50])
|
||||
|
||||
## [1.2.0] - 2026-08-22
|
||||
|
||||
### Added
|
||||
|
||||
@@ -6,7 +6,9 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
stdhttp "net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -19,6 +21,67 @@ import (
|
||||
"gitea.dcglab.co.uk/steve/restic-manager/internal/version"
|
||||
)
|
||||
|
||||
func TestFleetUpdateIdlePageDoesNotPollOrRequireTypedConfirmation(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"`,
|
||||
"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")
|
||||
}
|
||||
if strings.Contains(body, `hx-trigger="every 3s`) {
|
||||
t.Error("idle selection page must not poll and replace operator input")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFleetUpdateRunningPagePollsForProgress(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, baseURL, st := newTestServerWithUI(t)
|
||||
cookie, userID := loginAsAdminWithID(t, st)
|
||||
hostID := makeHost(t, st, "fleet-running-host")
|
||||
if err := st.CreateFleetUpdate(context.Background(), store.FleetUpdate{
|
||||
ID: ulid.Make().String(), StartedByUserID: userID, TargetVersion: version.Version,
|
||||
}, []string{hostID}); err != nil {
|
||||
t.Fatalf("create fleet update: %v", err)
|
||||
}
|
||||
|
||||
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()
|
||||
raw, _ := io.ReadAll(res.Body)
|
||||
if body := string(raw); !strings.Contains(body, `hx-trigger="every 3s`) {
|
||||
t.Error("running rollout page must poll for progress")
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
@@ -28,5 +28,15 @@
|
||||
|
||||
{{template "fleet_update_inner" .}}
|
||||
|
||||
<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}
|
||||
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}
|
||||
if(document.getElementById('fleet-selected-count'))fleetReview()
|
||||
</script>
|
||||
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
@@ -7,11 +7,11 @@
|
||||
*/}}
|
||||
{{define "fleet_update_inner"}}
|
||||
{{$page := .Page}}
|
||||
<div id="fleet-update-panel" class="mt-5"
|
||||
<div id="fleet-update-panel" class="mt-5"{{if and $page.Active (eq $page.Active.Status "running")}}
|
||||
hx-get="{{$page.PollURL}}"
|
||||
hx-trigger="every 3s [document.visibilityState==='visible']"
|
||||
hx-select="#fleet-update-panel"
|
||||
hx-swap="outerHTML">
|
||||
hx-swap="outerHTML"{{end}}>
|
||||
|
||||
{{if and $page.Active (eq $page.Active.Status "running")}}
|
||||
|
||||
@@ -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}}
|
||||
|
||||
Reference in New Issue
Block a user