From a36edbc26b4a3d2c9f32175eda34e199d43e0dbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9B=D1=83=D1=86=D0=B5=D0=BD=D0=BA=D0=BE=20=D0=90=D0=BD?= =?UTF-8?q?=D0=B4=D1=80=D0=B5=D0=B9=20=D0=90=D0=BD=D0=B0=D1=82=D0=BE=D0=BB?= =?UTF-8?q?=D1=8C=D0=B5=D0=B2=D0=B8=D1=87?= Date: Fri, 10 Apr 2026 15:35:45 +1000 Subject: [PATCH] =?UTF-8?q?fix(api):=20SNMP/hosts=20=E2=80=94=20=D0=BE?= =?UTF-8?q?=D0=B4=D0=BD=D0=B0=20=D1=81=D1=82=D1=80=D0=BE=D0=BA=D0=B0=20?= =?UTF-8?q?=D0=BD=D0=B0=20IP=20(DISTINCT=20ON),=20=D0=BB=D0=B8=D0=BC=D0=B8?= =?UTF-8?q?=D1=82=D1=8B=20=D0=B4=D0=BB=D1=8F=20=D0=B1=D0=BE=D0=BB=D1=8C?= =?UTF-8?q?=D1=88=D0=B8=D1=85=20=D1=81=D0=BA=D0=B0=D0=BD=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Made-with: Cursor --- internal/api/handler.go | 12 +++--- internal/scans/postgres_store.go | 21 +++++++--- internal/scans/store.go | 68 +++++++++++++++++++++----------- internal/webui/index.html | 4 +- 4 files changed, 69 insertions(+), 36 deletions(-) diff --git a/internal/api/handler.go b/internal/api/handler.go index 2685f16..5bda749 100644 --- a/internal/api/handler.go +++ b/internal/api/handler.go @@ -181,11 +181,11 @@ func (h *Handler) getScanHosts(w http.ResponseWriter, r *http.Request) { return } - limit := 500 + limit := 200000 if raw := r.URL.Query().Get("limit"); raw != "" { n, err := strconv.Atoi(raw) - if err != nil || n <= 0 || n > 5000 { - writeError(w, http.StatusBadRequest, "limit must be between 1 and 5000") + if err != nil || n <= 0 || n > 500000 { + writeError(w, http.StatusBadRequest, "limit must be between 1 and 500000") return } limit = n @@ -233,11 +233,11 @@ func (h *Handler) getScanSNMP(w http.ResponseWriter, r *http.Request) { return } - limit := 1000 + limit := 200000 if raw := r.URL.Query().Get("limit"); raw != "" { n, err := strconv.Atoi(raw) - if err != nil || n <= 0 || n > 10000 { - writeError(w, http.StatusBadRequest, "limit must be between 1 and 10000") + if err != nil || n <= 0 || n > 500000 { + writeError(w, http.StatusBadRequest, "limit must be between 1 and 500000") return } limit = n diff --git a/internal/scans/postgres_store.go b/internal/scans/postgres_store.go index 1c13e53..02536f5 100644 --- a/internal/scans/postgres_store.go +++ b/internal/scans/postgres_store.go @@ -291,12 +291,17 @@ values ($1, $2, $3, $4)` func (s *PostgresStore) ListHostResults(scanID string, limit int) []HostResult { if limit <= 0 { - limit = 500 + limit = 200000 } + // Одна актуальная строка на IP (иначе при limit по времени часть хостов без SNMP в UI). query := ` select ip::text, is_up, checked_at -from scan_hosts -where scan_id = $1 +from ( + select distinct on (ip) ip, is_up, checked_at + from scan_hosts + where scan_id = $1 + order by ip asc, checked_at desc +) t order by checked_at desc limit $2` rows, err := s.db.QueryContext(context.Background(), query, scanID, limit) @@ -380,13 +385,17 @@ values ($1, $2, $3, $4, $5, $6, $7, $8)` func (s *PostgresStore) ListSNMPResults(scanID string, limit int) []SNMPResult { if limit <= 0 { - limit = 1000 + limit = 200000 } query := ` select ip::text, success, coalesce(sys_name, ''), coalesce(sys_descr, ''), coalesce(sys_object_id, ''), coalesce(error_text, ''), checked_at -from scan_snmp -where scan_id = $1 +from ( + select distinct on (ip) ip, success, sys_name, sys_descr, sys_object_id, error_text, checked_at + from scan_snmp + where scan_id = $1 + order by ip asc, checked_at desc +) t order by checked_at desc limit $2` rows, err := s.db.QueryContext(context.Background(), query, scanID, limit) diff --git a/internal/scans/store.go b/internal/scans/store.go index 07e9674..642bb17 100644 --- a/internal/scans/store.go +++ b/internal/scans/store.go @@ -221,21 +221,11 @@ func (s *MemoryStore) SaveHostResult(scanID string, host HostResult) error { } func (s *MemoryStore) ListHostResults(scanID string, limit int) []HostResult { - if limit <= 0 { - limit = 500 - } s.mu.RLock() defer s.mu.RUnlock() items := s.hosts[scanID] - if len(items) <= limit { - out := make([]HostResult, len(items)) - copy(out, items) - return out - } - out := make([]HostResult, limit) - copy(out, items[:limit]) - return out + return dedupeHostsLatest(items, limit) } func (s *MemoryStore) SaveOpenPortResult(scanID string, result OpenPortResult) error { @@ -279,21 +269,11 @@ func (s *MemoryStore) SaveSNMPResult(scanID string, result SNMPResult) error { } func (s *MemoryStore) ListSNMPResults(scanID string, limit int) []SNMPResult { - if limit <= 0 { - limit = 1000 - } s.mu.RLock() defer s.mu.RUnlock() items := s.snmp[scanID] - if len(items) <= limit { - out := make([]SNMPResult, len(items)) - copy(out, items) - return out - } - out := make([]SNMPResult, limit) - copy(out, items[:limit]) - return out + return dedupeSNMPLatest(items, limit) } func (s *MemoryStore) SaveLLDPResult(scanID string, result LLDPResult) error { @@ -367,3 +347,47 @@ func newID() (string, error) { } return hex.EncodeToString(b[:]), nil } + +func dedupeHostsLatest(items []HostResult, limit int) []HostResult { + if limit <= 0 { + limit = 200000 + } + m := make(map[string]HostResult, len(items)) + for _, h := range items { + cur, ok := m[h.IP] + if !ok || h.CheckedAt.After(cur.CheckedAt) { + m[h.IP] = h + } + } + out := make([]HostResult, 0, len(m)) + for _, h := range m { + out = append(out, h) + } + sort.Slice(out, func(i, j int) bool { return out[i].CheckedAt.After(out[j].CheckedAt) }) + if len(out) > limit { + out = out[:limit] + } + return out +} + +func dedupeSNMPLatest(items []SNMPResult, limit int) []SNMPResult { + if limit <= 0 { + limit = 200000 + } + m := make(map[string]SNMPResult, len(items)) + for _, r := range items { + cur, ok := m[r.IP] + if !ok || r.CheckedAt.After(cur.CheckedAt) { + m[r.IP] = r + } + } + out := make([]SNMPResult, 0, len(m)) + for _, r := range m { + out = append(out, r) + } + sort.Slice(out, func(i, j int) bool { return out[i].CheckedAt.After(out[j].CheckedAt) }) + if len(out) > limit { + out = out[:limit] + } + return out +} diff --git a/internal/webui/index.html b/internal/webui/index.html index d62b4be..2b53efd 100644 --- a/internal/webui/index.html +++ b/internal/webui/index.html @@ -418,8 +418,8 @@ deviceTableBody.innerHTML = 'Загрузка…'; try { const [hRes, sRes, lRes] = await Promise.all([ - fetch(`/api/scans/${encodeURIComponent(scanId)}/hosts?limit=5000`), - fetch(`/api/scans/${encodeURIComponent(scanId)}/snmp?limit=5000`), + fetch(`/api/scans/${encodeURIComponent(scanId)}/hosts?limit=200000`), + fetch(`/api/scans/${encodeURIComponent(scanId)}/snmp?limit=200000`), fetch(`/api/scans/${encodeURIComponent(scanId)}/lldp?limit=10000`), ]); if (!hRes.ok) throw new Error(await hRes.text());