2

Refine the dashboard and requests log UX

This commit is contained in:
2026-03-12 17:40:13 +01:00
parent 0a14dd1df9
commit 0dfa30973e
8 changed files with 423 additions and 180 deletions

View File

@@ -236,6 +236,16 @@ func knownBotExistsClause(ipExpression string) string {
)`
}
func anyBotExistsClause(ipExpression string) string {
return `EXISTS (
SELECT 1
FROM ip_investigations i
WHERE i.ip = ` + ipExpression + `
AND json_valid(i.payload_json)
AND json_type(i.payload_json, '$.bot') IS NOT NULL
)`
}
func overviewFilterQueryParts(options model.OverviewOptions) (joins []string, clauses []string) {
if !options.ShowAllowed {
joins = append(joins, `LEFT JOIN ip_state s ON s.ip = e.client_ip`)
@@ -443,11 +453,27 @@ func (s *Store) GetOverview(ctx context.Context, since time.Time, limit int, opt
if err != nil {
return model.Overview{}, err
}
topIPsByEvents, err := s.listTopIPRows(ctx, since, limit, "events", options)
topIPsByEvents, err := s.listTopIPRows(ctx, since, limit, "events", options, "all")
if err != nil {
return model.Overview{}, err
}
topIPsByTraffic, err := s.listTopIPRows(ctx, since, limit, "traffic", options)
topBotIPsByEvents, err := s.listTopIPRows(ctx, since, limit, "events", options, "bots")
if err != nil {
return model.Overview{}, err
}
topNonBotIPsByEvents, err := s.listTopIPRows(ctx, since, limit, "events", options, "non-bots")
if err != nil {
return model.Overview{}, err
}
topIPsByTraffic, err := s.listTopIPRows(ctx, since, limit, "traffic", options, "all")
if err != nil {
return model.Overview{}, err
}
topBotIPsByTraffic, err := s.listTopIPRows(ctx, since, limit, "traffic", options, "bots")
if err != nil {
return model.Overview{}, err
}
topNonBotIPsByTraffic, err := s.listTopIPRows(ctx, since, limit, "traffic", options, "non-bots")
if err != nil {
return model.Overview{}, err
}
@@ -477,17 +503,27 @@ func (s *Store) GetOverview(ctx context.Context, since time.Time, limit int, opt
overview.Methods = methods
overview.Bots = bots
overview.TopIPsByEvents = topIPsByEvents
overview.TopBotIPsByEvents = topBotIPsByEvents
overview.TopNonBotIPsByEvents = topNonBotIPsByEvents
overview.TopIPsByTraffic = topIPsByTraffic
overview.TopBotIPsByTraffic = topBotIPsByTraffic
overview.TopNonBotIPsByTraffic = topNonBotIPsByTraffic
overview.TopSources = topSources
overview.TopURLs = topURLs
return overview, nil
}
func (s *Store) listTopIPRows(ctx context.Context, since time.Time, limit int, orderBy string, options model.OverviewOptions) ([]model.TopIPRow, error) {
func (s *Store) listTopIPRows(ctx context.Context, since time.Time, limit int, orderBy string, options model.OverviewOptions, botScope string) ([]model.TopIPRow, error) {
if limit <= 0 {
limit = 10
}
joins, clauses := overviewFilterQueryParts(options)
switch botScope {
case "bots":
clauses = append(clauses, anyBotExistsClause(`e.client_ip`))
case "non-bots":
clauses = append(clauses, `NOT `+anyBotExistsClause(`e.client_ip`))
}
query := fmt.Sprintf(`
SELECT e.client_ip,
COUNT(*) AS event_count,
@@ -797,6 +833,9 @@ func (s *Store) ListEvents(ctx context.Context, since time.Time, limit int, opti
if limit <= 0 {
limit = 100
}
if options.Offset < 0 {
options.Offset = 0
}
joins, clauses := eventFilterQueryParts(options)
query := `
SELECT e.id, e.source_name, e.profile_name, e.occurred_at, e.remote_ip, e.client_ip, e.host,
@@ -815,8 +854,8 @@ func (s *Store) ListEvents(ctx context.Context, since time.Time, limit int, opti
if len(clauses) > 0 {
query += ` WHERE ` + strings.Join(clauses, ` AND `)
}
query += ` ORDER BY e.occurred_at DESC, e.id DESC LIMIT ?`
args = append(args, limit)
query += ` ORDER BY e.occurred_at DESC, e.id DESC LIMIT ? OFFSET ?`
args = append(args, limit, options.Offset)
rows, err := s.db.QueryContext(ctx, query, args...)
if err != nil {

View File

@@ -109,6 +109,12 @@ func TestStoreRecordsEventsAndState(t *testing.T) {
if len(overview.TopIPsByEvents) != 1 || overview.TopIPsByEvents[0].IP != event.ClientIP {
t.Fatalf("unexpected top ips by events: %+v", overview.TopIPsByEvents)
}
if len(overview.TopBotIPsByEvents) != 0 {
t.Fatalf("expected no bot top ips by events before investigation, got %+v", overview.TopBotIPsByEvents)
}
if len(overview.TopNonBotIPsByEvents) != 1 || overview.TopNonBotIPsByEvents[0].IP != event.ClientIP {
t.Fatalf("unexpected top non-bot ips by events: %+v", overview.TopNonBotIPsByEvents)
}
if len(overview.TopSources) != 1 || overview.TopSources[0].SourceName != event.SourceName {
t.Fatalf("unexpected top sources: %+v", overview.TopSources)
}
@@ -252,6 +258,9 @@ func TestStoreOverviewLeaderboardsUseTrafficFromRawJSON(t *testing.T) {
if overview.TopIPsByEvents[0].IP != "203.0.113.10" || overview.TopIPsByEvents[0].Events != 2 || overview.TopIPsByEvents[0].TrafficBytes != 3072 {
t.Fatalf("unexpected top IP by events row: %+v", overview.TopIPsByEvents[0])
}
if len(overview.TopNonBotIPsByEvents) < 2 || overview.TopNonBotIPsByEvents[0].IP != "203.0.113.10" || overview.TopNonBotIPsByEvents[0].Events != 2 {
t.Fatalf("unexpected top non-bot IP by events rows: %+v", overview.TopNonBotIPsByEvents)
}
if len(overview.TopIPsByTraffic) < 2 {
t.Fatalf("expected at least 2 top IP rows by traffic, got %+v", overview.TopIPsByTraffic)
}
@@ -272,6 +281,22 @@ func TestStoreOverviewLeaderboardsUseTrafficFromRawJSON(t *testing.T) {
}); err != nil {
t.Fatalf("save top bot investigation: %v", err)
}
refreshedOverview, err := db.GetOverview(ctx, baseTime.Add(-time.Minute), 10, model.OverviewOptions{ShowKnownBots: true, ShowAllowed: true})
if err != nil {
t.Fatalf("get refreshed overview: %v", err)
}
if len(refreshedOverview.TopBotIPsByEvents) == 0 || refreshedOverview.TopBotIPsByEvents[0].IP != "203.0.113.10" {
t.Fatalf("unexpected top bot IPs by events after investigation: %+v", refreshedOverview.TopBotIPsByEvents)
}
if len(refreshedOverview.TopNonBotIPsByEvents) == 0 || refreshedOverview.TopNonBotIPsByEvents[0].IP != "203.0.113.20" {
t.Fatalf("unexpected top non-bot IPs by events after investigation: %+v", refreshedOverview.TopNonBotIPsByEvents)
}
if len(refreshedOverview.TopBotIPsByTraffic) == 0 || refreshedOverview.TopBotIPsByTraffic[0].IP != "203.0.113.10" {
t.Fatalf("unexpected top bot IPs by traffic after investigation: %+v", refreshedOverview.TopBotIPsByTraffic)
}
if len(refreshedOverview.TopNonBotIPsByTraffic) == 0 || refreshedOverview.TopNonBotIPsByTraffic[0].IP != "203.0.113.20" {
t.Fatalf("unexpected top non-bot IPs by traffic after investigation: %+v", refreshedOverview.TopNonBotIPsByTraffic)
}
if _, err := db.SetManualOverride(ctx, "203.0.113.20", model.ManualOverrideForceAllow, model.IPStateAllowed, "manual allow"); err != nil {
t.Fatalf("set manual override for filter test: %v", err)
}
@@ -283,9 +308,21 @@ func TestStoreOverviewLeaderboardsUseTrafficFromRawJSON(t *testing.T) {
if len(filtered.TopIPsByEvents) != 0 {
t.Fatalf("expected filtered top IPs by events to be empty, got %+v", filtered.TopIPsByEvents)
}
if len(filtered.TopBotIPsByEvents) != 0 {
t.Fatalf("expected filtered top bot IPs by events to be empty, got %+v", filtered.TopBotIPsByEvents)
}
if len(filtered.TopNonBotIPsByEvents) != 0 {
t.Fatalf("expected filtered top non-bot IPs by events to be empty, got %+v", filtered.TopNonBotIPsByEvents)
}
if len(filtered.TopIPsByTraffic) != 0 {
t.Fatalf("expected filtered top IPs by traffic to be empty, got %+v", filtered.TopIPsByTraffic)
}
if len(filtered.TopBotIPsByTraffic) != 0 {
t.Fatalf("expected filtered top bot IPs by traffic to be empty, got %+v", filtered.TopBotIPsByTraffic)
}
if len(filtered.TopNonBotIPsByTraffic) != 0 {
t.Fatalf("expected filtered top non-bot IPs by traffic to be empty, got %+v", filtered.TopNonBotIPsByTraffic)
}
if len(filtered.TopSources) != 0 {
t.Fatalf("expected filtered top sources to be empty, got %+v", filtered.TopSources)
}