diff --git a/web/html/component/aClientTable.html b/web/html/component/aClientTable.html index c7ac0513..e214f656 100644 --- a/web/html/component/aClientTable.html +++ b/web/html/component/aClientTable.html @@ -54,6 +54,14 @@ + + + + [[ formatClientSpeed(record, client.email) ]] + - + + @@ -115,6 +123,21 @@ [[ SizeFormatter.sizeFormat(getAllTimeClient(record, client.email)) ]] + + + + + + + ↑[[ SizeFormatter.sizeFormat(getDailyUpStats(record, client.email)) ]] + ↓[[ SizeFormatter.sizeFormat(getDailyDownStats(record, client.email)) ]] + + + + [[ SizeFormatter.sizeFormat(getDailySumStats(record, client.email)) ]] + + diff --git a/web/html/inbounds.html b/web/html/inbounds.html index 2c81e0da..9559b675 100644 --- a/web/html/inbounds.html +++ b/web/html/inbounds.html @@ -992,8 +992,12 @@ { title: '{{ i18n "pages.inbounds.operate" }}', width: 140, scopedSlots: { customRender: 'actions' } }, { title: '{{ i18n "pages.inbounds.enable" }}', width: 60, scopedSlots: { customRender: 'enable' } }, { title: '{{ i18n "online" }}', width: 80, scopedSlots: { customRender: 'online' } }, + // Speed column is intentionally non-sortable to prevent UI jitter caused by volatile real-time values. + { title: '{{ i18n "pages.inbounds.speed" }}', width: 90, align: 'center', scopedSlots: { customRender: 'speed' } }, { title: '{{ i18n "pages.inbounds.client" }}', width: 160, scopedSlots: { customRender: 'client' } }, { title: '{{ i18n "pages.inbounds.traffic" }}', width: 200, align: 'center', scopedSlots: { customRender: 'traffic' } }, + // DynamicMetricsColumns: Adds Speed and Daily Traffic visualization to the table. + { title: '{{ i18n "pages.inbounds.dailyTraffic" }}', width: 100, align: 'center', scopedSlots: { customRender: 'dailyTraffic' } }, { title: '{{ i18n "pages.inbounds.allTimeTraffic" }}', width: 110, align: 'center', scopedSlots: { customRender: 'allTime' } }, { title: '{{ i18n "pages.inbounds.expireDate" }}', width: 180, align: 'center', scopedSlots: { customRender: 'expiryTime' } }, ]; @@ -1064,6 +1068,10 @@ showAlert: false, ipLimitEnable: false, pageSize: 0, + // clientSpeedCache stores the previous traffic snapshot (total bytes, timestamp) + // for each client. Used to calculate instantaneous speed on the client-side + // without additional API requests. + clientSpeedCache: {}, }, methods: { loading(spinning = true) { @@ -1857,6 +1865,88 @@ const allTime = clientStats.allTime || 0; return allTime > current ? allTime : current; }, + + // DailyTrafficGetters & SpeedCalculator: Exposes 24h traffic counters (Up/Down/Sum) + // and calculates real-time client throughput. Speed computation uses a delta-caching + // strategy (1s interval) to prevent UI jitter during high-frequency WebSocket updates. + getDailyUpStats(dbInbound, email) { + if (!email || email.length == 0) return 0; + let clientStats = this.getClientStats(dbInbound, email); + return clientStats ? (clientStats.dailyUp || 0) : 0; + }, + getDailyDownStats(dbInbound, email) { + if (!email || email.length == 0) return 0; + let clientStats = this.getClientStats(dbInbound, email); + return clientStats ? (clientStats.dailyDown || 0) : 0; + }, + getDailySumStats(dbInbound, email) { + if (!email || email.length == 0) return 0; + let clientStats = this.getClientStats(dbInbound, email); + if (!clientStats) return 0; + return (clientStats.dailyUp || 0) + (clientStats.dailyDown || 0); + }, + // getClientSpeed returns the current speed (bytes/sec) for a client + // Calculated from the delta between last known stats and current stats + // Uses same logic as dashboard total speed calculation + formatClientSpeed(dbInbound, email) { + const speed = this.getClientSpeed(dbInbound, email); + if (speed > 0) { + return SizeFormatter.sizeFormat(speed) + '/s'; + } + return '-'; + }, + getClientSpeed(dbInbound, email) { + if (!email || !email.length) return 0; + if (!dbInbound || !dbInbound.id) return 0; + + const clientStats = this.getClientStats(dbInbound, email); + if (!clientStats) return 0; + + const now = Date.now(); + const currentUp = clientStats.up || 0; + const currentDown = clientStats.down || 0; + const currentTotal = currentUp + currentDown; + const key = `${dbInbound.id}:${email}`; + + // Initialize cache entry if not exists + if (!this.clientSpeedCache[key]) { + this.$set(this.clientSpeedCache, key, { + up: currentUp, + down: currentDown, + total: currentTotal, + time: now, + speed: 0 + }); + return 0; + } + + const cache = this.clientSpeedCache[key]; + const timeDiff = (now - cache.time) / 1000; + + // Only recalculate speed if enough time passed (1 second for stability) + if (timeDiff >= 1.0) { + const upDelta = currentUp - cache.up; + const downDelta = currentDown - cache.down; + const totalDelta = upDelta + downDelta; + + // If traffic increased (positive delta), calculate speed + if (totalDelta >= 0 && timeDiff > 0) { + cache.speed = totalDelta / timeDiff; + } else { + // Traffic reset or counter overflow - keep last known speed + cache.speed = cache.speed || 0; + } + + // Update cache with current values + cache.up = currentUp; + cache.down = currentDown; + cache.total = currentTotal; + cache.time = now; + } + + return cache.speed || 0; + }, + getRemStats(dbInbound, email) { if (!email || email.length == 0) return 0; let clientStats = this.getClientStats(dbInbound, email); diff --git a/web/service/inbound.go b/web/service/inbound.go index b3a6b945..1155b916 100644 --- a/web/service/inbound.go +++ b/web/service/inbound.go @@ -1408,6 +1408,9 @@ func (s *InboundService) addClientTraffic(tx *gorm.DB, traffics []*xray.ClientTr } } now := time.Now().UnixMilli() + // Calculate start of today in local timezone (Unix milliseconds) + nowTime := time.Now() + today := time.Date(nowTime.Year(), nowTime.Month(), nowTime.Day(), 0, 0, 0, 0, nowTime.Location()).UnixMilli() for dbTraffic_index := range dbClientTraffics { t, ok := trafficByEmail[dbClientTraffics[dbTraffic_index].Email] if !ok { @@ -1416,6 +1419,15 @@ func (s *InboundService) addClientTraffic(tx *gorm.DB, traffics []*xray.ClientTr dbClientTraffics[dbTraffic_index].Up += t.Up dbClientTraffics[dbTraffic_index].Down += t.Down dbClientTraffics[dbTraffic_index].AllTime += t.Up + t.Down + // Check if daily reset is needed (lazy reset at first traffic after 00:00) + if dbClientTraffics[dbTraffic_index].LastDailyReset < today { + dbClientTraffics[dbTraffic_index].DailyUp = 0 + dbClientTraffics[dbTraffic_index].DailyDown = 0 + dbClientTraffics[dbTraffic_index].LastDailyReset = today + } + // Add current traffic to daily counters + dbClientTraffics[dbTraffic_index].DailyUp += t.Up + dbClientTraffics[dbTraffic_index].DailyDown += t.Down if t.Up+t.Down > 0 { onlineClients = append(onlineClients, t.Email) dbClientTraffics[dbTraffic_index].LastOnline = now diff --git a/web/translation/translate.en_US.toml b/web/translation/translate.en_US.toml index 25343fe4..683b18fc 100644 --- a/web/translation/translate.en_US.toml +++ b/web/translation/translate.en_US.toml @@ -221,6 +221,8 @@ [pages.inbounds] "allTimeTraffic" = "All-time Traffic" +"speed" = "Speed" +"dailyTraffic" = "24h Traffic" "allTimeTrafficUsage" = "All Time Total Usage" "title" = "Inbounds" "totalDownUp" = "Total Sent/Received" diff --git a/web/translation/translate.ru_RU.toml b/web/translation/translate.ru_RU.toml index 299cc019..bcd3b373 100644 --- a/web/translation/translate.ru_RU.toml +++ b/web/translation/translate.ru_RU.toml @@ -221,6 +221,8 @@ [pages.inbounds] "allTimeTraffic" = "Общий трафик" +"speed" = "Скорость" +"dailyTraffic" = "За сутки" "allTimeTrafficUsage" = "Общее использование за все время" "title" = "Подключения" "totalDownUp" = "Отправлено/получено" diff --git a/xray/client_traffic.go b/xray/client_traffic.go index fcb2585e..506d39f0 100644 --- a/xray/client_traffic.go +++ b/xray/client_traffic.go @@ -16,4 +16,10 @@ type ClientTraffic struct { Total int64 `json:"total" form:"total"` Reset int `json:"reset" form:"reset" gorm:"default:0"` LastOnline int64 `json:"lastOnline" form:"lastOnline" gorm:"default:0"` + // DailyTrafficModelExtension: Adds persistent fields for 24h cycle accounting (DailyUp/Down) + // and a timezone-aware reset checkpoint. Enables "lazy reset" logic during traffic updates, + // eliminating the need for background cron jobs or scheduled tasks. + DailyUp int64 `json:"dailyUp" form:"dailyUp"` + DailyDown int64 `json:"dailyDown" form:"dailyDown"` + LastDailyReset int64 `json:"lastDailyReset" form:"lastDailyReset" gorm:"default:0"` }