mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-05-17 00:05:56 +03:00
fix: prevent online clients from randomly disappearing from panel UI (#4387)
* fix: prevent online clients from randomly disappearing from panel UI Online status was determined solely by whether a client transferred bytes in the current 5-second polling window. The online list was completely replaced each cycle, so idle-but-connected clients with no traffic delta in that window were dropped from the UI. Now online status is computed from lastOnline DB timestamps with a 5-second grace period via RefreshOnlineClientsFromMap(), so clients remain visible across idle polling windows. Closes #4384 * fix: extend online client grace period to survive idle poll cycles The 5s grace period equalled the traffic-poll interval, so a client whose Xray stats reported a zero delta for one cycle was still dropped on the very next tick. Bump to 20s (~4 polls) so idle-but-connected sessions stay visible across momentary counter gaps without lingering long after a real disconnect. Refs #4384 --------- Co-authored-by: MHSanaei <ho3ein.sanaei@gmail.com>
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user