Remove IPs page auto-refresh; clear checkboxes after bulk recheck
The 5s auto-refresh (ec44d54) and the hx-sync fix on top of it (5a53705) didn't resolve the issues seen in manual testing. Rather than keep debugging htmx's polling/preserve/sync interaction, drop auto-refresh entirely: handleIPsFragment, GET /ips/fragment, and the poll attributes/PollSeconds plumbing are all removed. The table now only updates when a button action re-renders it, as it did before auto-refresh was added — the bulk-recheck feature itself (handleIPsRecheckSelected, POST /ips/recheck, "Перепроверить выбранные") is untouched. Also drops hx-preserve/id from the row checkboxes: it existed solely to survive the auto-poll wiping a selection mid-task, so it has no purpose left, and it was actively wrong for one case — after a successful "Перепроверить выбранные", it kept the just-submitted addresses checked instead of clearing them. Since the checkbox's checked state was never server-rendered to begin with, removing hx-preserve alone makes every table swap (including the recheck button's own) render fresh, unchecked boxes, which is exactly the desired "selection clears once the action has been applied" behavior. Left hx-sync="#ips-table-wrap:queue last" on the action buttons/form — still cheap protection against a double-click race between two real user actions, independent of the now-removed polling. Rebuilt bin/admin-dashboard and bin/SHA256SUMS per docs/SETUP.md. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
5a53705217
commit
78b20fa5be
6 files changed
+15
-43
No files matched your search
@@ -9,7 +9,6 @@ type ipsPageData struct {
|
||||
PageData
|
||||
Items []ipQueueItem
|
||||
FIPSettleSeconds int
|
||||
PollSeconds int
|
||||
}
|
||||
|
||||
type ipDetailData struct {
|
||||
@@ -23,20 +22,12 @@ func (s *Server) handleIPsPage(w http.ResponseWriter, r *http.Request) {
|
||||
if err == nil {
|
||||
err = settingsErr
|
||||
}
|
||||
data := ipsPageData{Items: items, FIPSettleSeconds: settings.FIPSettleSeconds, PollSeconds: s.Cfg.OverviewPollIntervalS}
|
||||
data := ipsPageData{Items: items, FIPSettleSeconds: settings.FIPSettleSeconds}
|
||||
data.ActiveNav = "ips"
|
||||
data.Banner = bannerFor(err)
|
||||
s.renderPage(w, "ips_page", data)
|
||||
}
|
||||
|
||||
// handleIPsFragment re-renders just the ips_table fragment, for the
|
||||
// periodic auto-refresh poll (see ips_content's hx-trigger="every Ns") —
|
||||
// identical to a mutating action's post-update render, just with no
|
||||
// actionErr of its own.
|
||||
func (s *Server) handleIPsFragment(w http.ResponseWriter, r *http.Request) {
|
||||
s.renderIPsTable(w, r, nil)
|
||||
}
|
||||
|
||||
func (s *Server) handleIPDetail(w http.ResponseWriter, r *http.Request) {
|
||||
ip := r.PathValue("ip")
|
||||
detail, err := s.CA.GetIP(r.Context(), ip)
|
||||
|
||||
@@ -155,42 +155,24 @@ func TestIPsDeleteSelected(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestIPsPagePollsForFragment(t *testing.T) {
|
||||
// TestIPsRecheckSelectedClearsCheckboxes proves a successful bulk recheck
|
||||
// re-renders the table with plain, unchecked checkboxes (no hx-preserve),
|
||||
// so a user sees their selection cleared once the action has been applied
|
||||
// rather than it silently staying checked.
|
||||
func TestIPsRecheckSelectedClearsCheckboxes(t *testing.T) {
|
||||
fake, caURL := newFakeControlAPI(t)
|
||||
now := time.Now()
|
||||
fake.ips = []ipQueueItem{
|
||||
{IPAddress: "1.1.1.1", State: "done", OverallResult: "pass", AggregatedAt: &now, UpdatedAt: now, CreatedAt: now},
|
||||
{IPAddress: "9.9.9.9", State: "done", OverallResult: "pass", AttemptNumber: 1, AggregatedAt: &now, UpdatedAt: now, CreatedAt: now},
|
||||
}
|
||||
ts := newTestServer(t, caURL)
|
||||
|
||||
body := get(t, ts, "/ips")
|
||||
if !strings.Contains(body, `hx-get="/ips/fragment"`) || !strings.Contains(body, `hx-trigger="every 5s"`) {
|
||||
t.Fatalf("expected ips-table-wrap to auto-poll every 5s, got:\n%s", body)
|
||||
body := postForm(t, ts, "POST", "/ips/recheck", map[string][]string{"addresses": {"9.9.9.9"}})
|
||||
if strings.Contains(body, "hx-preserve") {
|
||||
t.Fatalf("expected no hx-preserve on checkboxes after recheck, got:\n%s", body)
|
||||
}
|
||||
// hx-sync must coordinate the poll with every action that also swaps
|
||||
// #ips-table-wrap (bulk buttons, per-row buttons, the add-address
|
||||
// form) so a slow mutation and a poll tick can never race and let a
|
||||
// stale response clobber a just-applied action (recheck-selected
|
||||
// appearing to silently fail with many rows selected) — 1 wrapper div
|
||||
// + 1 add-address form + 3 bulk buttons + 2 per-row buttons (one
|
||||
// terminal row: "Перепроверить" + "Удалить") = 7.
|
||||
if got := strings.Count(body, `hx-sync="#ips-table-wrap:queue last"`); got != 7 {
|
||||
t.Fatalf("expected 7 elements to declare hx-sync, got %d in:\n%s", got, body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIPsFragment(t *testing.T) {
|
||||
fake, caURL := newFakeControlAPI(t)
|
||||
now := time.Now()
|
||||
fake.ips = []ipQueueItem{
|
||||
{IPAddress: "1.1.1.1", State: "queued", UpdatedAt: now, CreatedAt: now},
|
||||
{IPAddress: "2.2.2.2", State: "checking", UpdatedAt: now, CreatedAt: now},
|
||||
}
|
||||
ts := newTestServer(t, caURL)
|
||||
|
||||
body := get(t, ts, "/ips/fragment")
|
||||
if !strings.Contains(body, "1.1.1.1") || !strings.Contains(body, "2.2.2.2") {
|
||||
t.Fatalf("expected both addresses in fragment, got:\n%s", body)
|
||||
if !strings.Contains(body, `<input type="checkbox" name="addresses" value="9.9.9.9">`) {
|
||||
t.Fatalf("expected a plain, unchecked checkbox for 9.9.9.9 after recheck, got:\n%s", body)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ func (s *Server) routes(mux *http.ServeMux) {
|
||||
mux.HandleFunc("GET /overview/fragment", s.handleOverviewFragment)
|
||||
|
||||
mux.HandleFunc("GET /ips", s.handleIPsPage)
|
||||
mux.HandleFunc("GET /ips/fragment", s.handleIPsFragment)
|
||||
mux.HandleFunc("GET /ips/{ip}", s.handleIPDetail)
|
||||
mux.HandleFunc("POST /ips", s.handleIPsSubmit)
|
||||
mux.HandleFunc("POST /ips/{ip}/recheck", s.handleIPRecheck)
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="ips-table-wrap" hx-get="/ips/fragment" hx-trigger="every {{.PollSeconds}}s" hx-swap="innerHTML" hx-sync="#ips-table-wrap:queue last">
|
||||
<div id="ips-table-wrap">
|
||||
{{template "ips_table" .}}
|
||||
</div>
|
||||
{{end}}
|
||||
@@ -59,7 +59,7 @@
|
||||
{{$b := ipBadge .State .OverallResult .FIPAssociatedAt $.FIPSettleSeconds}}
|
||||
{{$terminal := or (eq .State "done") (eq .State "failed") (eq .State "occupied")}}
|
||||
<tr>
|
||||
<td data-label=""><input type="checkbox" name="addresses" value="{{.IPAddress}}" id="ip-select-{{.IPAddress}}" hx-preserve="true"></td>
|
||||
<td data-label=""><input type="checkbox" name="addresses" value="{{.IPAddress}}"></td>
|
||||
<td class="addr" data-label="Адрес"><a href="/ips/{{.IPAddress}}">{{.IPAddress}}</a></td>
|
||||
<td data-label="Состояние"><span class="pill {{$b.Class}}">{{$b.Label}}</span></td>
|
||||
<td data-label="Валидатор">{{deref .OwnerValidatorID}}</td>
|
||||
|
||||
Reference in new issue
Block a user