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>
120 lines
3.6 KiB
Go
120 lines
3.6 KiB
Go
package dashboard
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
)
|
|
|
|
type ipsPageData struct {
|
|
PageData
|
|
Items []ipQueueItem
|
|
FIPSettleSeconds int
|
|
}
|
|
|
|
type ipDetailData struct {
|
|
PageData
|
|
Detail ipDetailResponse
|
|
}
|
|
|
|
func (s *Server) handleIPsPage(w http.ResponseWriter, r *http.Request) {
|
|
items, err := s.CA.ListIPs(r.Context())
|
|
settings, settingsErr := s.CA.GetOrchestratorSettings(r.Context())
|
|
if err == nil {
|
|
err = settingsErr
|
|
}
|
|
data := ipsPageData{Items: items, FIPSettleSeconds: settings.FIPSettleSeconds}
|
|
data.ActiveNav = "ips"
|
|
data.Banner = bannerFor(err)
|
|
s.renderPage(w, "ips_page", data)
|
|
}
|
|
|
|
func (s *Server) handleIPDetail(w http.ResponseWriter, r *http.Request) {
|
|
ip := r.PathValue("ip")
|
|
detail, err := s.CA.GetIP(r.Context(), ip)
|
|
data := ipDetailData{Detail: detail}
|
|
data.ActiveNav = "ips"
|
|
data.Banner = bannerFor(err)
|
|
s.renderPage(w, "ip_detail_page", data)
|
|
}
|
|
|
|
// renderIPsTable re-fetches the current queue and renders the ips_table
|
|
// fragment, tagging actionErr (if any) on the shared error banner. Called
|
|
// after every mutating /ips/* request so the table always reflects true
|
|
// current state regardless of whether the mutation itself succeeded.
|
|
func (s *Server) renderIPsTable(w http.ResponseWriter, r *http.Request, actionErr error) {
|
|
items, listErr := s.CA.ListIPs(r.Context())
|
|
if actionErr == nil {
|
|
actionErr = listErr
|
|
}
|
|
settings, settingsErr := s.CA.GetOrchestratorSettings(r.Context())
|
|
if actionErr == nil {
|
|
actionErr = settingsErr
|
|
}
|
|
s.renderFragment(w, "ips_table", ipsPageData{Items: items, FIPSettleSeconds: settings.FIPSettleSeconds}, actionErr)
|
|
}
|
|
|
|
func (s *Server) handleIPsSubmit(w http.ResponseWriter, r *http.Request) {
|
|
if err := r.ParseForm(); err != nil {
|
|
s.renderIPsTable(w, r, fmt.Errorf("invalid form: %w", err))
|
|
return
|
|
}
|
|
addresses := splitList(r.PostFormValue("addresses"))
|
|
if len(addresses) == 0 {
|
|
s.renderIPsTable(w, r, &apiErr{Status: http.StatusBadRequest, Message: "укажите хотя бы один адрес"})
|
|
return
|
|
}
|
|
_, err := s.CA.SubmitIPs(r.Context(), addresses)
|
|
s.renderIPsTable(w, r, err)
|
|
}
|
|
|
|
func (s *Server) handleIPRecheck(w http.ResponseWriter, r *http.Request) {
|
|
ip := r.PathValue("ip")
|
|
_, err := s.CA.SubmitIPs(r.Context(), []string{ip})
|
|
s.renderIPsTable(w, r, err)
|
|
}
|
|
|
|
func (s *Server) handleIPCancel(w http.ResponseWriter, r *http.Request) {
|
|
ip := r.PathValue("ip")
|
|
err := s.CA.CancelIP(r.Context(), ip)
|
|
s.renderIPsTable(w, r, err)
|
|
}
|
|
|
|
func (s *Server) handleIPDelete(w http.ResponseWriter, r *http.Request) {
|
|
ip := r.PathValue("ip")
|
|
err := s.CA.DeleteIP(r.Context(), ip)
|
|
s.renderIPsTable(w, r, err)
|
|
}
|
|
|
|
func (s *Server) handleIPsDeleteSelected(w http.ResponseWriter, r *http.Request) {
|
|
if err := r.ParseForm(); err != nil {
|
|
s.renderIPsTable(w, r, fmt.Errorf("invalid form: %w", err))
|
|
return
|
|
}
|
|
addresses := r.Form["addresses"]
|
|
if len(addresses) == 0 {
|
|
s.renderIPsTable(w, r, &apiErr{Status: http.StatusBadRequest, Message: "ничего не выбрано"})
|
|
return
|
|
}
|
|
_, err := s.CA.DeleteIPs(r.Context(), addresses)
|
|
s.renderIPsTable(w, r, err)
|
|
}
|
|
|
|
func (s *Server) handleIPsRecheckSelected(w http.ResponseWriter, r *http.Request) {
|
|
if err := r.ParseForm(); err != nil {
|
|
s.renderIPsTable(w, r, fmt.Errorf("invalid form: %w", err))
|
|
return
|
|
}
|
|
addresses := r.Form["addresses"]
|
|
if len(addresses) == 0 {
|
|
s.renderIPsTable(w, r, &apiErr{Status: http.StatusBadRequest, Message: "ничего не выбрано"})
|
|
return
|
|
}
|
|
_, err := s.CA.SubmitIPs(r.Context(), addresses)
|
|
s.renderIPsTable(w, r, err)
|
|
}
|
|
|
|
func (s *Server) handleIPsClear(w http.ResponseWriter, r *http.Request) {
|
|
_, err := s.CA.ClearQueue(r.Context())
|
|
s.renderIPsTable(w, r, err)
|
|
}
|