From 934f9bc230bef45a50d72801a552625efb12dd73 Mon Sep 17 00:00:00 2001 From: MHSanaei Date: Sun, 24 May 2026 23:43:45 +0200 Subject: [PATCH] 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 --- web/job/node_traffic_sync_job.go | 8 ++++---- web/job/xray_traffic_job.go | 28 ++++------------------------ 2 files changed, 8 insertions(+), 28 deletions(-) diff --git a/web/job/node_traffic_sync_job.go b/web/job/node_traffic_sync_job.go index 63de5018..1cf32323 100644 --- a/web/job/node_traffic_sync_job.go +++ b/web/job/node_traffic_sync_job.go @@ -101,10 +101,6 @@ func (j *NodeTrafficSyncJob) Run() { j.structural.set() } - if !websocket.HasClients() { - return - } - lastOnline, err := j.inboundService.GetClientsLastOnline() if err != nil { logger.Warning("node traffic sync: get last-online failed:", err) @@ -115,6 +111,10 @@ func (j *NodeTrafficSyncJob) Run() { j.inboundService.RefreshOnlineClientsFromMap(lastOnline) + if !websocket.HasClients() { + return + } + online := j.inboundService.GetOnlineClients() if online == nil { online = []string{} diff --git a/web/job/xray_traffic_job.go b/web/job/xray_traffic_job.go index 7a471b4c..583c5995 100644 --- a/web/job/xray_traffic_job.go +++ b/web/job/xray_traffic_job.go @@ -65,18 +65,6 @@ func (j *XrayTrafficJob) Run() { 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() if err != nil { logger.Warning("get clients last online failed:", err) @@ -84,13 +72,12 @@ func (j *XrayTrafficJob) Run() { if lastOnlineMap == nil { 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) + if !websocket.HasClients() { + return + } + onlineClients := j.inboundService.GetOnlineClients() if onlineClients == nil { onlineClients = []string{} @@ -102,11 +89,6 @@ func (j *XrayTrafficJob) Run() { "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{} if stats, err := j.inboundService.GetAllClientTraffics(); err != nil { logger.Warning("get all client traffics for websocket failed:", err) @@ -122,8 +104,6 @@ func (j *XrayTrafficJob) Run() { 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 { websocket.BroadcastOutbounds(updatedOutbounds) } else if err != nil {