diff --git a/bin/SHA256SUMS b/bin/SHA256SUMS index c04d0fa..943718d 100644 --- a/bin/SHA256SUMS +++ b/bin/SHA256SUMS @@ -1,4 +1,4 @@ -1f0f7390b28778530f0d2a3b9d00371f5fd4abd4fa182b47d746ecc2f381aaac control-api -d4075822020e461b3e2adfa2752fb84356c3e922ba758dfb047e7c30e6abc51c validator-agent -abb55560d98b80ecceb31fa46f92140e1196a1c08812b066a4a759b51748a453 prober -fbdb0fe69872bd4fd2d4ec0dcd25b453d7ee92ae4d12eee29d872c883a697eb7 admin-dashboard +194bbd250ad93142eb1eb2d8990292327d48842f851f522ddb31f9ddfa8c554f control-api +43ca6b15a25e8534ae2d833e8d0575a85da036e17db370c797da613bb51a9c29 validator-agent +bde443dd4fd335f9c3c64a2ca04d5db6c9a93fe2f45eb81b1d426581e380cee2 prober +e74de9873e9da1ebfa6beeccd34b6fa7c15bf14c2d8efe13f4de7266af141702 admin-dashboard diff --git a/bin/admin-dashboard b/bin/admin-dashboard index 3c7abfe..d7c46e0 100755 Binary files a/bin/admin-dashboard and b/bin/admin-dashboard differ diff --git a/bin/control-api b/bin/control-api index a2097d6..6d93f55 100755 Binary files a/bin/control-api and b/bin/control-api differ diff --git a/bin/prober b/bin/prober index 2c610ea..9369a69 100755 Binary files a/bin/prober and b/bin/prober differ diff --git a/bin/validator-agent b/bin/validator-agent index 93fd59a..a2cd514 100755 Binary files a/bin/validator-agent and b/bin/validator-agent differ diff --git a/internal/dashboard/handlers_overview.go b/internal/dashboard/handlers_overview.go index 161b7aa..88f7bc0 100644 --- a/internal/dashboard/handlers_overview.go +++ b/internal/dashboard/handlers_overview.go @@ -50,9 +50,19 @@ func (s *Server) handleOverview(w http.ResponseWriter, r *http.Request) { s.renderPage(w, "overview_page", data) } +// handleOverviewFragment serves both the recurring poll and every +// filter-triggered request. Its hx-target is #overview-tables, but the +// stat-grid (#overview-stats) needs to stay in sync too — it lives outside +// #overview-tables in the DOM (see overview.html) so the filter form can +// sit between them, so it's refreshed via an out-of-band swap appended +// after the main content, the same idiom templates/layout.html's +// error_banner already uses for the shared error banner. func (s *Server) handleOverviewFragment(w http.ResponseWriter, r *http.Request) { data, err := s.loadOverview(r) - s.renderFragment(w, "overview_fragment", data, err) + s.renderFragment(w, "overview_tables", data, err) + if tplErr := s.tmpl.ExecuteTemplate(w, "overview_stats_oob", data); tplErr != nil { + s.Log.Error("render overview stats oob", "err", tplErr) + } } // currentlyChecking is every IP not yet in a terminal state, ordered by diff --git a/internal/dashboard/handlers_test.go b/internal/dashboard/handlers_test.go index 771f7b8..3ccfce9 100644 --- a/internal/dashboard/handlers_test.go +++ b/internal/dashboard/handlers_test.go @@ -36,11 +36,44 @@ func TestOverviewFragment(t *testing.T) { } } +// TestOverviewFragmentUpdatesStatsOutOfBand proves /overview/fragment keeps +// refreshing the stat-grid even though #overview-stats lives outside the +// polled #overview-tables div (see overview.html — the stat panel sits +// above the filter form, which must survive polling, so it can't be part +// of the swapped tables block). This guards the DOM layout fix: stats, +// then a gap, then the filter panel, then everything else. +func TestOverviewFragmentUpdatesStatsOutOfBand(t *testing.T) { + fake, caURL := newFakeControlAPI(t) + fake.validators = []validatorDTO{{ValidatorID: "v1", State: "idle"}} + fake.ips = []ipQueueItem{{IPAddress: "1.1.1.1", State: "queued"}} + ts := newTestServer(t, caURL) + + body := get(t, ts, "/overview/fragment") + if !strings.Contains(body, `id="overview-stats" hx-swap-oob="true"`) { + t.Fatalf("expected an out-of-band update for #overview-stats, got:\n%s", body) + } + if !strings.Contains(body, "всего IP") { + t.Fatalf("expected the stat-grid labels in the OOB block, got:\n%s", body) + } + + page := get(t, ts, "/overview") + statsIdx := strings.Index(page, `id="overview-stats"`) + filterIdx := strings.Index(page, `id="overview-filter"`) + tablesIdx := strings.Index(page, `id="overview-tables"`) + if statsIdx < 0 || filterIdx < 0 || tablesIdx < 0 { + t.Fatalf("expected all three sections present, got:\n%s", page) + } + if !(statsIdx < filterIdx && filterIdx < tablesIdx) { + t.Fatalf("expected DOM order stats -> filter -> tables, got indices stats=%d filter=%d tables=%d", + statsIdx, filterIdx, tablesIdx) + } +} + // TestOverviewFilterByQueryAndStatus proves the ?q=&status= params on both // /overview and /overview/fragment narrow the current-checking and // last-completed tables to matching rows only, and that the filter form // echoes the applied values back (so they survive the recurring poll, -// which only re-renders overview_fragment, not the form around it). +// which only re-renders #overview-tables, not the form around it). func TestOverviewFilterByQueryAndStatus(t *testing.T) { fake, caURL := newFakeControlAPI(t) now := time.Now() diff --git a/internal/dashboard/templates/overview.html b/internal/dashboard/templates/overview.html index f904716..4269ef9 100644 --- a/internal/dashboard/templates/overview.html +++ b/internal/dashboard/templates/overview.html @@ -24,20 +24,24 @@