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 -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>