mirror of
https://github.com/wweir/sower.git
synced 2026-10-19 16:14:33 +00:00
fix(admin): keep client chips stable under the client filter
The clients aggregate was built only from domains surviving the client filter, so selecting a client chip collapsed every other chip's count to the few shared domains (or made it vanish), and the chips row hid itself entirely when the filter matched no domains - stranding the user with no way to clear the filter. - Snapshot now aggregates chips over the source-filtered view before the client filter is applied; chips stay globally counted and stable. - Traffic page keeps the toolbar and chips visible when the filtered domain list is empty; the empty state only replaces the table. - Regression test pins chips stability, including clients that share no domain with the selected one.
This commit is contained in:
+18
-12
@@ -323,6 +323,9 @@ func (s *Stats) BindConn(conn net.Conn, domain string) {
|
||||
// domain ordering, source restricts domains to one entry point, and client
|
||||
// restricts them to one client IP; invalid values fall back to
|
||||
// DomainSortBytes and SourceAll, and an empty client disables the filter.
|
||||
// Clients aggregates per-client counters over the source-filtered view and
|
||||
// is intentionally unaffected by the client filter, so the console's client
|
||||
// chips stay stable while one is selected.
|
||||
func (s *Stats) Snapshot(sort DomainSort, source Source, client string) TrafficSnapshot {
|
||||
s.sample()
|
||||
|
||||
@@ -371,18 +374,9 @@ func (s *Stats) Snapshot(sort DomainSort, source Source, client string) TrafficS
|
||||
ds.BytesDown = src.bytesDown
|
||||
ds.LastSeen = src.lastSeen
|
||||
}
|
||||
if client != "" {
|
||||
dc, ok := d.byClient[client] // nil map read is safe
|
||||
if !ok || (dc.conns == 0 && dc.bytesUp == 0 && dc.bytesDown == 0) {
|
||||
continue
|
||||
}
|
||||
ds.Conns = dc.conns
|
||||
ds.BytesUp = dc.bytesUp
|
||||
ds.BytesDown = dc.bytesDown
|
||||
ds.LastSeen = dc.lastSeen
|
||||
}
|
||||
snap.Domains = append(snap.Domains, ds)
|
||||
|
||||
// Aggregate the client chips over the source-filtered view but before
|
||||
// the client filter: the chip row is a navigation device and must stay
|
||||
// stable (and globally counted) while a client filter is active.
|
||||
for ip, c := range d.byClient {
|
||||
agg := clientAgg[ip]
|
||||
if agg == nil {
|
||||
@@ -396,6 +390,18 @@ func (s *Stats) Snapshot(sort DomainSort, source Source, client string) TrafficS
|
||||
agg.LastSeen = c.lastSeen
|
||||
}
|
||||
}
|
||||
|
||||
if client != "" {
|
||||
dc, ok := d.byClient[client] // nil map read is safe
|
||||
if !ok || (dc.conns == 0 && dc.bytesUp == 0 && dc.bytesDown == 0) {
|
||||
continue
|
||||
}
|
||||
ds.Conns = dc.conns
|
||||
ds.BytesUp = dc.bytesUp
|
||||
ds.BytesDown = dc.bytesDown
|
||||
ds.LastSeen = dc.lastSeen
|
||||
}
|
||||
snap.Domains = append(snap.Domains, ds)
|
||||
}
|
||||
sortDomains(snap.Domains, sort)
|
||||
if len(snap.Domains) > snapshotTopN {
|
||||
|
||||
@@ -433,6 +433,12 @@ func TestStatsSnapshotClientFilter(t *testing.T) {
|
||||
"192.168.1.10": {conns: 2, bytesUp: 200, bytesDown: 200, lastSeen: now},
|
||||
},
|
||||
}
|
||||
s.domains["c.example"] = &domainStat{
|
||||
conns: 7, bytesUp: 700, bytesDown: 700, lastSeen: now,
|
||||
byClient: map[string]*clientStat{
|
||||
"192.168.1.20": {conns: 7, bytesUp: 700, bytesDown: 700, lastSeen: now},
|
||||
},
|
||||
}
|
||||
s.mu.Unlock()
|
||||
|
||||
// filtered by client: only that client's domains, with that client's values
|
||||
@@ -452,6 +458,19 @@ func TestStatsSnapshotClientFilter(t *testing.T) {
|
||||
if len(empty.Domains) != 0 {
|
||||
t.Fatalf("expected no domains for unknown client, got %+v", empty.Domains)
|
||||
}
|
||||
|
||||
// the client chips are a navigation device: they must stay stable and
|
||||
// globally counted regardless of the active client filter, including
|
||||
// clients that share no domain with the selected one
|
||||
if len(view.Clients) != 2 {
|
||||
t.Fatalf("expected both clients in chips under filter, got %+v", view.Clients)
|
||||
}
|
||||
if view.Clients[0].IP != "192.168.1.20" || view.Clients[0].Conns != 8 {
|
||||
t.Fatalf("client .20 must keep its global conns (8) under filter, got %+v", view.Clients[0])
|
||||
}
|
||||
if len(empty.Clients) != 2 {
|
||||
t.Fatalf("chips must survive an unknown-client filter, got %+v", empty.Clients)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStatsSnapshotClientsAggregate(t *testing.T) {
|
||||
|
||||
Vendored
+1233
File diff suppressed because one or more lines are too long
Vendored
-1233
File diff suppressed because one or more lines are too long
Vendored
+1
-1
@@ -5,7 +5,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="color-scheme" content="light" />
|
||||
<title>Sower Admin</title>
|
||||
<script type="module" crossorigin src="/assets/index-DYU-q3xW.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-CCzmNAbo.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-C8oAZ1sn.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -57,11 +57,6 @@
|
||||
|
||||
{#if !traffic}
|
||||
<Loading />
|
||||
{:else if (traffic.domains ?? []).length === 0}
|
||||
<div class="flex flex-col items-center gap-2 py-10 text-sm text-muted-foreground">
|
||||
<Inbox class="size-5" aria-hidden="true" />
|
||||
<p>暂无{sourceLabel}记录。</p>
|
||||
</div>
|
||||
{:else}
|
||||
{#if !live.connected}
|
||||
<p class="mb-3 rounded-md border border-destructive/40 bg-destructive/5 px-3 py-2 text-xs text-destructive">
|
||||
@@ -132,7 +127,13 @@
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
<Card.Card class="max-h-[70vh] overflow-auto">
|
||||
{#if (traffic.domains ?? []).length === 0}
|
||||
<div class="flex flex-col items-center gap-2 rounded-lg border border-dashed py-10 text-sm text-muted-foreground">
|
||||
<Inbox class="size-5" aria-hidden="true" />
|
||||
<p>暂无{sourceLabel}记录。</p>
|
||||
</div>
|
||||
{:else}
|
||||
<Card.Card class="max-h-[70vh] overflow-auto">
|
||||
<Table.Table>
|
||||
<Table.TableHeader>
|
||||
<Table.TableRow>
|
||||
@@ -172,7 +173,8 @@
|
||||
{/each}
|
||||
</Table.TableBody>
|
||||
</Table.Table>
|
||||
</Card.Card>
|
||||
</Card.Card>
|
||||
{/if}
|
||||
<p class="mt-2 text-xs text-muted-foreground">
|
||||
按{activeSortLabel}排序的前 100 个{isDNSView ? 'DNS 查询域名' : sourceLabel + '域名'},实时推送。
|
||||
</p>
|
||||
|
||||
Reference in New Issue
Block a user