mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-06 21:24:10 +00:00
feat: add per-client real-time speed & 24h daily traffic
- Added DailyUp/Down/LastDailyReset to ClientTraffic model - Lazy daily reset at 00:00 local time (no cron) - Non-sortable Speed column using dashboard delta logic - i18n keys added to all locale files
This commit is contained in:
parent
c718e7ca5b
commit
cb33e72925
7 changed files with 136 additions and 1 deletions
2
web/assets/css/custom.min.css
vendored
2
web/assets/css/custom.min.css
vendored
File diff suppressed because one or more lines are too long
|
|
@ -54,6 +54,14 @@
|
|||
</template>
|
||||
</a-popover>
|
||||
</template>
|
||||
<!-- SpeedRenderer: Displays real-time client throughput calculated via delta between WebSocket updates.
|
||||
Calculated client-side to reduce server load and avoid constant re-fetching of stats. -->
|
||||
<template slot="speed" slot-scope="text, client">
|
||||
<a-tag color="cyan" style="cursor: default;">
|
||||
<span v-if="client.email">[[ formatClientSpeed(record, client.email) ]]</span>
|
||||
<span v-else>-</span>
|
||||
</a-tag>
|
||||
</template>
|
||||
<template slot="client" slot-scope="text, client">
|
||||
<a-space direction="horizontal" :size="2">
|
||||
<a-tooltip>
|
||||
|
|
@ -115,6 +123,21 @@
|
|||
<template slot="allTime" slot-scope="text, client">
|
||||
<a-tag>[[ SizeFormatter.sizeFormat(getAllTimeClient(record, client.email)) ]]</a-tag>
|
||||
</template>
|
||||
<!-- DailyTrafficRenderer: Shows cumulative traffic for the current 24h cycle.
|
||||
Includes a popover for granular Up/Down breakdown. -->
|
||||
<template slot="dailyTraffic" slot-scope="text, client">
|
||||
<a-popover :overlay-class-name="themeSwitcher.currentTheme">
|
||||
<template slot="content" v-if="client.email">
|
||||
<table cellpadding="2" width="100%">
|
||||
<tr>
|
||||
<td>↑[[ SizeFormatter.sizeFormat(getDailyUpStats(record, client.email)) ]]</td>
|
||||
<td>↓[[ SizeFormatter.sizeFormat(getDailyDownStats(record, client.email)) ]]</td>
|
||||
</tr>
|
||||
</table>
|
||||
</template>
|
||||
<a-tag>[[ SizeFormatter.sizeFormat(getDailySumStats(record, client.email)) ]]</a-tag>
|
||||
</a-popover>
|
||||
</template>
|
||||
<template slot="expiryTime" slot-scope="text, client, index">
|
||||
<template v-if="client.expiryTime !=0 && client.reset >0">
|
||||
<a-popover :overlay-class-name="themeSwitcher.currentTheme">
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -221,6 +221,8 @@
|
|||
|
||||
[pages.inbounds]
|
||||
"allTimeTraffic" = "Общий трафик"
|
||||
"speed" = "Скорость"
|
||||
"dailyTraffic" = "За сутки"
|
||||
"allTimeTrafficUsage" = "Общее использование за все время"
|
||||
"title" = "Подключения"
|
||||
"totalDownUp" = "Отправлено/получено"
|
||||
|
|
|
|||
|
|
@ -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"`
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue