diff --git a/web/job/node_traffic_sync_job.go b/web/job/node_traffic_sync_job.go index a978d767..c2f5fa6a 100644 --- a/web/job/node_traffic_sync_job.go +++ b/web/job/node_traffic_sync_job.go @@ -87,10 +87,6 @@ func (j *NodeTrafficSyncJob) Run() { return } - online := j.inboundService.GetOnlineClients() - if online == nil { - online = []string{} - } lastOnline, err := j.inboundService.GetClientsLastOnline() if err != nil { logger.Warning("node traffic sync: get last-online failed:", err) @@ -98,6 +94,13 @@ func (j *NodeTrafficSyncJob) Run() { if lastOnline == nil { lastOnline = map[string]int64{} } + + j.inboundService.RefreshOnlineClientsFromMap(lastOnline) + + online := j.inboundService.GetOnlineClients() + if online == nil { + online = []string{} + } websocket.BroadcastTraffic(map[string]any{ "onlineClients": online, "lastOnlineMap": lastOnline, diff --git a/web/job/xray_traffic_job.go b/web/job/xray_traffic_job.go index 96539986..7a471b4c 100644 --- a/web/job/xray_traffic_job.go +++ b/web/job/xray_traffic_job.go @@ -77,10 +77,6 @@ func (j *XrayTrafficJob) Run() { // 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. - onlineClients := j.inboundService.GetOnlineClients() - if onlineClients == nil { - onlineClients = []string{} - } lastOnlineMap, err := j.inboundService.GetClientsLastOnline() if err != nil { logger.Warning("get clients last online failed:", err) @@ -88,6 +84,17 @@ 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) + + onlineClients := j.inboundService.GetOnlineClients() + if onlineClients == nil { + onlineClients = []string{} + } websocket.BroadcastTraffic(map[string]any{ "traffics": traffics, "clientTraffics": clientTraffics, diff --git a/web/service/inbound.go b/web/service/inbound.go index 4025659e..866a765c 100644 --- a/web/service/inbound.go +++ b/web/service/inbound.go @@ -1539,6 +1539,13 @@ func (s *InboundService) UpdateInboundClient(data *model.Inbound, clientId strin const resetGracePeriodMs int64 = 30000 +// onlineGracePeriodMs must comfortably exceed the 5s traffic-poll interval — +// Xray's stats counters often report a zero delta for an active session across +// a single poll, so a 5s grace would still drop the client on the next tick. +// ~4 polls of slack keeps idle-but-connected clients visible without lingering +// long after a real disconnect. +const onlineGracePeriodMs int64 = 20000 + func (s *InboundService) SetRemoteTraffic(nodeID int, snap *runtime.TrafficSnapshot) (bool, error) { var structuralChange bool err := submitTrafficWrite(func() error { @@ -1880,15 +1887,9 @@ func (s *InboundService) addInboundTraffic(tx *gorm.DB, traffics []*xray.Traffic func (s *InboundService) addClientTraffic(tx *gorm.DB, traffics []*xray.ClientTraffic) (err error) { if len(traffics) == 0 { - // Empty onlineUsers - if p != nil { - p.SetOnlineClients(make([]string, 0)) - } return nil } - onlineClients := make([]string, 0) - emails := make([]string, 0, len(traffics)) for _, traffic := range traffics { emails = append(emails, traffic.Email) @@ -1931,14 +1932,10 @@ func (s *InboundService) addClientTraffic(tx *gorm.DB, traffics []*xray.ClientTr dbClientTraffics[dbTraffic_index].Down += t.Down dbClientTraffics[dbTraffic_index].AllTime += t.Up + t.Down if t.Up+t.Down > 0 { - onlineClients = append(onlineClients, t.Email) dbClientTraffics[dbTraffic_index].LastOnline = now } } - // Set onlineUsers - p.SetOnlineClients(onlineClients) - err = tx.Save(dbClientTraffics).Error if err != nil { logger.Warning("AddClientTraffic update data ", err) @@ -3764,6 +3761,19 @@ func (s *InboundService) GetClientsLastOnline() (map[string]int64, error) { return result, nil } +func (s *InboundService) RefreshOnlineClientsFromMap(lastOnlineMap map[string]int64) { + now := time.Now().UnixMilli() + newOnlineClients := make([]string, 0, len(lastOnlineMap)) + for email, lastOnline := range lastOnlineMap { + if now-lastOnline < onlineGracePeriodMs { + newOnlineClients = append(newOnlineClients, email) + } + } + if p != nil { + p.SetOnlineClients(newOnlineClients) + } +} + func (s *InboundService) FilterAndSortClientEmails(emails []string) ([]string, []string, error) { db := database.GetDB()