fix(online): refresh online-clients list even when no WS frontend is connected (#4515)

XrayTrafficJob and NodeTrafficSyncJob both gated the entire
post-traffic-write block behind websocket.HasClients() to skip
expensive broadcasts when no browser is open. The block included the
RefreshOnlineClientsFromMap call that keeps the in-memory
p.onlineClients list current.

Several non-WS consumers read that same list:
- Telegram bot (tgbot.go calls p.GetOnlineClients in 3 places)
- REST GET /panel/api/onlines (returned to API callers)
- Internal alerts that check whether a client is online

When no browser was watching the dashboard, the list went stale and
stayed empty, so the bot reported "nobody online" and the onlines API
returned [] even when xray had active sessions.

Move RefreshOnlineClientsFromMap above the HasClients guard so the
in-memory list is always fresh. Only the actual BroadcastTraffic /
BroadcastClientStats / BroadcastOutbounds calls (and the
GetAllClientTraffics / GetInboundsTrafficSummary work that feeds them)
remain gated by HasClients.

Closes #4515
This commit is contained in:
MHSanaei 2026-05-24 23:43:45 +02:00
parent 3df0ed2143
commit 934f9bc230
No known key found for this signature in database
GPG key ID: 7E4060F2FBE5AB7A
2 changed files with 8 additions and 28 deletions

View file

@ -101,10 +101,6 @@ func (j *NodeTrafficSyncJob) Run() {
j.structural.set() j.structural.set()
} }
if !websocket.HasClients() {
return
}
lastOnline, err := j.inboundService.GetClientsLastOnline() lastOnline, err := j.inboundService.GetClientsLastOnline()
if err != nil { if err != nil {
logger.Warning("node traffic sync: get last-online failed:", err) logger.Warning("node traffic sync: get last-online failed:", err)
@ -115,6 +111,10 @@ func (j *NodeTrafficSyncJob) Run() {
j.inboundService.RefreshOnlineClientsFromMap(lastOnline) j.inboundService.RefreshOnlineClientsFromMap(lastOnline)
if !websocket.HasClients() {
return
}
online := j.inboundService.GetOnlineClients() online := j.inboundService.GetOnlineClients()
if online == nil { if online == nil {
online = []string{} online = []string{}

View file

@ -65,18 +65,6 @@ func (j *XrayTrafficJob) Run() {
j.xrayService.SetToNeedRestart() j.xrayService.SetToNeedRestart()
} }
// If no frontend client is connected, skip all WebSocket broadcasting
// routines — including the active-client DB query and JSON marshaling.
if !websocket.HasClients() {
return
}
// Online presence + traffic deltas — small payload, always fits in WS.
// Force non-nil slice/map so JSON marshalling produces [] / {} instead of
// `null` when everyone is offline. The frontend's traffic handler treats
// a missing/null onlineClients field as "no update", so without this the
// "everyone went offline" transition was silently dropped — stale online
// users lingered in the list and the online filter kept showing them.
lastOnlineMap, err := j.inboundService.GetClientsLastOnline() lastOnlineMap, err := j.inboundService.GetClientsLastOnline()
if err != nil { if err != nil {
logger.Warning("get clients last online failed:", err) logger.Warning("get clients last online failed:", err)
@ -84,13 +72,12 @@ func (j *XrayTrafficJob) Run() {
if lastOnlineMap == nil { if lastOnlineMap == nil {
lastOnlineMap = make(map[string]int64) lastOnlineMap = make(map[string]int64)
} }
// Determine online clients from lastOnline timestamps with a 5-second
// grace period instead of just the current 5-second traffic poll. This
// prevents idle-but-connected clients from randomly disappearing from
// the UI between polling windows.
j.inboundService.RefreshOnlineClientsFromMap(lastOnlineMap) j.inboundService.RefreshOnlineClientsFromMap(lastOnlineMap)
if !websocket.HasClients() {
return
}
onlineClients := j.inboundService.GetOnlineClients() onlineClients := j.inboundService.GetOnlineClients()
if onlineClients == nil { if onlineClients == nil {
onlineClients = []string{} onlineClients = []string{}
@ -102,11 +89,6 @@ func (j *XrayTrafficJob) Run() {
"lastOnlineMap": lastOnlineMap, "lastOnlineMap": lastOnlineMap,
}) })
// Full snapshot every cycle: absolute per-client counters and inbound
// totals. Frontend overwrites both in place. The previous delta path
// (activeEmails -> GetActiveClientTraffics) silently omitted the
// clients array whenever nobody moved bytes in the cycle, leaving the
// client rows in the UI stuck at stale traffic/remained/all-time.
clientStatsPayload := map[string]any{} clientStatsPayload := map[string]any{}
if stats, err := j.inboundService.GetAllClientTraffics(); err != nil { if stats, err := j.inboundService.GetAllClientTraffics(); err != nil {
logger.Warning("get all client traffics for websocket failed:", err) logger.Warning("get all client traffics for websocket failed:", err)
@ -122,8 +104,6 @@ func (j *XrayTrafficJob) Run() {
websocket.BroadcastClientStats(clientStatsPayload) websocket.BroadcastClientStats(clientStatsPayload)
} }
// Outbounds list is small (one row per outbound, no per-client expansion)
// so the full snapshot still fits comfortably in WS.
if updatedOutbounds, err := j.outboundService.GetOutboundsTraffic(); err == nil && updatedOutbounds != nil { if updatedOutbounds, err := j.outboundService.GetOutboundsTraffic(); err == nil && updatedOutbounds != nil {
websocket.BroadcastOutbounds(updatedOutbounds) websocket.BroadcastOutbounds(updatedOutbounds)
} else if err != nil { } else if err != nil {