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:
ayurishchevandClaude Sonnet 5 committed 2026-09-18 19:29:26 +03:00
1 parent 5a53705217
commit 78b20fa5be
6 files changed
+15 -43

No files matched your search

+1 -1
View File
@@ -1,4 +1,4 @@
dcb137ae6a3f6a59465f2772215d48f98fd303dd1c266645b20ec70194ab7915 control-api
b7a6068db1d095ae7b73cbd9b7273d6a1629ae017e9c3c9601e103432894a247 validator-agent
be8e9576c6fbe5e5798b5d4b758a5617b63bea6abe80e326998d10583edbbf5c prober
cec482fb92246210108261cfc02c10ef9829ce3c7d7c174cf935976baa42fd2c admin-dashboard
61d1df7aa84528e8fd9b5e700ac6a9b77e45763b5c5d938913dbce25d0ad9940 admin-dashboard
Binary file not shown.
+1 -10
View File
@@ -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)
+11 -29
View File
@@ -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)
}
}
-1
View File
@@ -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)
+2 -2
View File
@@ -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>