Files
cloud-ip-validator/internal/dashboard/handlers_overview.go
T
ayurishchevandClaude Sonnet 5 0838abc4a2 Add IP search and status filter to the Overview and Registry dashboard pages
Both pages rendered their full lists with no way to narrow them. Adds a
?q=&status= filter (substring match on address, exact match on result
status) applied dashboard-side, in Go, over the already-fetched list —
no control-api/db changes needed.

Overview: the filter form lives outside the polling target (#overview-live)
so the recurring poll never wipes out what's typed/selected; the poll and
both filter inputs share hx-sync="#overview-live:queue last" (the same
fix that resolved the earlier abandoned /ips auto-refresh races) and the
poll now carries hx-include="#overview-filter" so it keeps honoring the
current filter on every tick. Applies uniformly to both the "Текущая
проверка" and "Последние N завершённых" tables, per the confirmed design:
picking a specific status naturally hides in-progress rows, since they
have no result yet.

Registry: no polling exists there, so the filter form reuses the full page
via hx-select/hx-replace-url — simpler than adding a parallel fragment
endpoint, and gives a bookmarkable/shareable filtered URL.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 11:58:53 +03:00

122 lines
3.7 KiB
Go

package dashboard
import (
"net/http"
"sort"
"strings"
)
type overviewData struct {
PageData
Status statusResponse
CurrentItems []ipQueueItem
LastCompleted []ipQueueItem
Breakdown map[string]int
LastN int
PollSeconds int
Query string
StatusFilter string
}
func (s *Server) loadOverview(r *http.Request) (overviewData, error) {
ctx := r.Context()
status, err := s.CA.Status(ctx)
if err != nil {
return overviewData{}, err
}
ips, err := s.CA.ListIPs(ctx)
if err != nil {
return overviewData{}, err
}
q := strings.TrimSpace(r.URL.Query().Get("q"))
resultFilter := r.URL.Query().Get("status")
last := lastCompleted(ips, s.Cfg.LastCompletedCount)
return overviewData{
Status: status,
CurrentItems: filterQueueItems(currentlyChecking(ips), q, resultFilter),
LastCompleted: filterQueueItems(last, q, resultFilter),
Breakdown: resultBreakdown(last),
LastN: s.Cfg.LastCompletedCount,
PollSeconds: s.Cfg.OverviewPollIntervalS,
Query: q,
StatusFilter: resultFilter,
}, nil
}
func (s *Server) handleOverview(w http.ResponseWriter, r *http.Request) {
data, err := s.loadOverview(r)
data.ActiveNav = "overview"
data.Banner = bannerFor(err)
s.renderPage(w, "overview_page", data)
}
func (s *Server) handleOverviewFragment(w http.ResponseWriter, r *http.Request) {
data, err := s.loadOverview(r)
s.renderFragment(w, "overview_fragment", data, err)
}
// currentlyChecking is every IP not yet in a terminal state, ordered by
// queue position — the "текущая проверка" live snapshot. No backend
// concept of a "run" exists; this is computed fresh on every request.
func currentlyChecking(ips []ipQueueItem) []ipQueueItem {
var out []ipQueueItem
for _, ip := range ips {
if ip.State != "done" && ip.State != "failed" {
out = append(out, ip)
}
}
sort.Slice(out, func(i, j int) bool { return out[i].Sequence < out[j].Sequence })
return out
}
// lastCompleted returns the n most recently completed (done/failed) IPs by
// AggregatedAt descending — the "последняя завершённая проверка" summary
// window. This is an operational definition, not a real "batch": resubmit
// n if the window size needs tuning (overview.last_completed_count).
func lastCompleted(ips []ipQueueItem, n int) []ipQueueItem {
var done []ipQueueItem
for _, ip := range ips {
if (ip.State == "done" || ip.State == "failed") && ip.AggregatedAt != nil {
done = append(done, ip)
}
}
sort.Slice(done, func(i, j int) bool { return done[i].AggregatedAt.After(*done[j].AggregatedAt) })
if len(done) > n {
done = done[:n]
}
return done
}
// resultBreakdown counts OverallResult values across exactly the given
// items (normally the output of lastCompleted) — pass/partial/fail/cancelled.
func resultBreakdown(items []ipQueueItem) map[string]int {
out := map[string]int{"pass": 0, "partial": 0, "fail": 0, "cancelled": 0}
for _, ip := range items {
out[ip.OverallResult]++
}
return out
}
// filterQueueItems narrows items to those whose address contains q
// (case-insensitive substring) and, if status is set, whose OverallResult
// matches it exactly. A still-in-progress item always has an empty
// OverallResult, so picking any specific status hides it — the intended
// behavior for "Текущая проверка", which has no verdict yet.
func filterQueueItems(items []ipQueueItem, q, status string) []ipQueueItem {
if q == "" && status == "" {
return items
}
q = strings.ToLower(q)
out := make([]ipQueueItem, 0, len(items))
for _, ip := range items {
if q != "" && !strings.Contains(strings.ToLower(ip.IPAddress), q) {
continue
}
if status != "" && ip.OverallResult != status {
continue
}
out = append(out, ip)
}
return out
}