fix(online): scope online status per node instead of a global union

The inbounds page and Nodes page checked each client's email against a
single deduped union of every node's online clients, so a client connected
to one node showed as online on every inbound across every node. The local
online set was also derived from the email-keyed client_traffics.last_online
column, which remote-node syncs bump too, leaking remote-only clients onto
local inbounds.

Track online clients per node: the local panel's own xray clients under key
0 (derived from live traffic-poll deltas via RefreshLocalOnline, kept in
memory and independent of the shared last_online column) and each remote
node under its id. Add GetOnlineClientsByNode plus a /clients/onlinesByNode
endpoint and onlineByNode WS field; node.go and the inbounds rollup now scope
online by node. The flat GetOnlineClients union is kept for client-centric and
total-count views (Clients page, dashboard, telegram).

Closes #4809
This commit is contained in:
MHSanaei
2026-06-02 18:33:21 +02:00
parent 6f6c7fc17a
commit 3af2da0142
12 changed files with 334 additions and 31 deletions
+5 -1
View File
@@ -109,7 +109,10 @@ func (j *NodeTrafficSyncJob) Run() {
lastOnline = map[string]int64{}
}
j.inboundService.RefreshOnlineClientsFromMap(lastOnline)
// Prune stale local-online entries (no local active emails to add here —
// only the local xray poll feeds those) so a stopped local xray's clients
// still age out between traffic polls.
j.inboundService.RefreshLocalOnlineClients(nil)
if !websocket.HasClients() {
return
@@ -121,6 +124,7 @@ func (j *NodeTrafficSyncJob) Run() {
}
websocket.BroadcastTraffic(map[string]any{
"onlineClients": online,
"onlineByNode": j.inboundService.GetOnlineClientsByNode(),
"lastOnlineMap": lastOnline,
})
+12 -1
View File
@@ -72,7 +72,17 @@ func (j *XrayTrafficJob) Run() {
if lastOnlineMap == nil {
lastOnlineMap = make(map[string]int64)
}
j.inboundService.RefreshOnlineClientsFromMap(lastOnlineMap)
// Derive the local online set from this poll's per-email deltas rather
// than the shared last_online column, which remote-node syncs also bump
// and would otherwise make a client active only on a remote node appear
// online on local inbounds.
activeEmails := make([]string, 0, len(clientTraffics))
for _, ct := range clientTraffics {
if ct != nil && ct.Up+ct.Down > 0 {
activeEmails = append(activeEmails, ct.Email)
}
}
j.inboundService.RefreshLocalOnlineClients(activeEmails)
if !websocket.HasClients() {
return
@@ -86,6 +96,7 @@ func (j *XrayTrafficJob) Run() {
"traffics": traffics,
"clientTraffics": clientTraffics,
"onlineClients": onlineClients,
"onlineByNode": j.inboundService.GetOnlineClientsByNode(),
"lastOnlineMap": lastOnlineMap,
})