refactor(traffic): drop all-time traffic tracking

Removes the AllTime field from Inbound and ClientTraffic and migrates
existing DBs by dropping the all_time columns on startup. The counter
duplicated up+down without adding signal, and the per-event accumulator
ran on every traffic write.

Frontend: drop the All-time column from the inbound list and the
client-row table, the All-time row from the client info modal, and the
All-Time Total Usage tile from the inbounds summary card. The
allTimeTraffic/allTimeTrafficUsage i18n keys are removed across every
locale.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
MHSanaei 2026-05-17 09:01:04 +02:00
parent 8a4101a96b
commit f315ed269e
No known key found for this signature in database
GPG key ID: 7E4060F2FBE5AB7A
22 changed files with 24 additions and 130 deletions

View file

@ -49,7 +49,6 @@ type Inbound struct {
Up int64 `json:"up" form:"up"` // Upload traffic in bytes
Down int64 `json:"down" form:"down"` // Download traffic in bytes
Total int64 `json:"total" form:"total"` // Total traffic limit in bytes
AllTime int64 `json:"allTime" form:"allTime" gorm:"default:0"` // All-time traffic usage
Remark string `json:"remark" form:"remark"` // Human-readable remark
Enable bool `json:"enable" form:"enable" gorm:"index:idx_enable_traffic_reset,priority:1"` // Whether the inbound is enabled
ExpiryTime int64 `json:"expiryTime" form:"expiryTime"` // Expiration timestamp

View file

@ -10,7 +10,6 @@ export class DBInbound {
this.up = 0;
this.down = 0;
this.total = 0;
this.allTime = 0;
this.remark = "";
this.enable = true;
this.expiryTime = 0;

View file

@ -203,13 +203,6 @@ function close() {
</td>
</tr>
<tr>
<td>{{ t('pages.inbounds.allTimeTraffic') || 'All-time' }}</td>
<td>
<a-tag>{{ SizeFormatter.sizeFormat(traffic?.allTime || used) }}</a-tag>
</td>
</tr>
<tr>
<td>{{ t('pages.inbounds.expireDate') || 'Expiry' }}</td>
<td>

View file

@ -86,14 +86,6 @@ function getRem(email) {
const r = s.total - s.up - s.down;
return r > 0 ? r : 0;
}
function getAllTime(email) {
const s = statsFor(email);
if (!s) return 0;
// allTime is the cumulative-historical counter; never let it dip
// below up+down (manual edits / partial migrations can push it under).
const current = (s.up || 0) + (s.down || 0);
return s.allTime > current ? s.allTime : current;
}
function isClientDepleted(email) {
const s = statsFor(email);
if (!s) return false;
@ -286,7 +278,6 @@ function confirmBulkDelete() {
<div class="cell cell-client">{{ t('pages.inbounds.client') }}</div>
<div class="cell cell-traffic">{{ t('pages.inbounds.traffic') }}</div>
<div class="cell cell-remained">{{ t('remained') }}</div>
<div class="cell cell-alltime">{{ t('pages.inbounds.allTimeTraffic') }}</div>
<div class="cell cell-expiry">{{ t('pages.inbounds.expireDate') }}</div>
</div>
@ -388,10 +379,6 @@ function confirmBulkDelete() {
</a-tag>
</div>
<div class="cell cell-alltime">
<a-tag>{{ SizeFormatter.sizeFormat(getAllTime(client.email)) }}</a-tag>
</div>
<div class="cell cell-expiry">
<template v-if="client.expiryTime !== 0 && client.reset > 0">
<a-popover>
@ -499,10 +486,6 @@ function confirmBulkDelete() {
{{ SizeFormatter.sizeFormat(getRem(statsClient.email)) }}
</a-tag>
</div>
<div class="stat-row">
<span class="stat-label">{{ t('pages.inbounds.allTimeTraffic') }}</span>
<a-tag>{{ SizeFormatter.sizeFormat(getAllTime(statsClient.email)) }}</a-tag>
</div>
<div class="stat-row">
<span class="stat-label">{{ t('online') }}</span>
<a-tag v-if="statsClient.enable && isClientOnline(statsClient.email)" color="green">{{ t('online')
@ -571,8 +554,6 @@ function confirmBulkDelete() {
minmax(160px, 2fr)
/* traffic */
130px
/* all-time */
130px
/* remained */
140px;
/* expiry */
@ -597,8 +578,6 @@ function confirmBulkDelete() {
minmax(160px, 2fr)
/* traffic */
130px
/* all-time */
130px
/* remained */
140px;
/* expiry */
@ -628,7 +607,6 @@ function confirmBulkDelete() {
.cell-actions,
.cell-enable,
.cell-online,
.cell-alltime,
.cell-remained {
text-align: center;
display: inline-flex;

View file

@ -189,7 +189,6 @@ const sortFns = {
port: (a, b) => a.port - b.port,
protocol: (a, b) => a.protocol.localeCompare(b.protocol),
traffic: (a, b) => (a.up + a.down) - (b.up + b.down),
allTimeInbound: (a, b) => (a.allTime || 0) - (b.allTime || 0),
expiryTime: (a, b) => (a.expiryTime || Infinity) - (b.expiryTime || Infinity),
node: (a, b) => {
const nameA = props.nodesById.get(a.nodeId)?.name ?? (a.nodeId == null ? '\uffff' : `node #${a.nodeId}`);
@ -244,7 +243,6 @@ const desktopColumns = computed(() => {
sortableCol({ title: t('pages.inbounds.protocol'), key: 'protocol', align: 'left', width: 130 }, 'protocol'),
sortableCol({ title: t('clients'), key: 'clients', align: 'left', width: 50 }, 'clients'),
sortableCol({ title: t('pages.inbounds.traffic'), key: 'traffic', align: 'center', width: 90 }, 'traffic'),
sortableCol({ title: t('pages.inbounds.allTimeTraffic'), key: 'allTimeInbound', align: 'center', width: 95 }, 'allTimeInbound'),
sortableCol({ title: t('pages.inbounds.expireDate'), key: 'expiryTime', align: 'center', width: 40 }, 'expiryTime'),
);
return cols;
@ -515,10 +513,6 @@ function showQrCodeMenu(dbInbound) {
<InfinityIcon v-else />
</a-tag>
</div>
<div class="stat-row">
<span class="stat-label">{{ t('pages.inbounds.allTimeTraffic') }}</span>
<a-tag>{{ SizeFormatter.sizeFormat(statsRecord.allTime || 0) }}</a-tag>
</div>
<div v-if="clientCount[statsRecord.id]" class="stat-row">
<span class="stat-label">{{ t('clients') }}</span>
<a-tag color="green" class="client-count-tag">{{ clientCount[statsRecord.id].clients }}</a-tag>
@ -735,11 +729,6 @@ function showQrCodeMenu(dbInbound) {
</a-popover>
</template>
<!-- ============== All-time inbound traffic ============== -->
<template v-else-if="column.key === 'allTimeInbound'">
<a-tag>{{ SizeFormatter.sizeFormat(record.allTime || 0) }}</a-tag>
</template>
<!-- ============== Expiry ============== -->
<template v-else-if="column.key === 'expiryTime'">
<a-popover v-if="record.expiryTime > 0">

View file

@ -5,7 +5,6 @@ import { Modal, message } from 'ant-design-vue';
import {
SwapOutlined,
PieChartOutlined,
HistoryOutlined,
BarsOutlined,
TeamOutlined,
} from '@ant-design/icons-vue';
@ -582,14 +581,6 @@ function onRowAction({ key, dbInbound }) {
</template>
</CustomStatistic>
</a-col>
<a-col :xs="12" :sm="12" :md="5">
<CustomStatistic :title="t('pages.inbounds.allTimeTrafficUsage')"
:value="SizeFormatter.sizeFormat(totals.allTime)">
<template #prefix>
<HistoryOutlined />
</template>
</CustomStatistic>
</a-col>
<a-col :xs="12" :sm="12" :md="5">
<CustomStatistic :title="t('pages.inbounds.inboundCount')" :value="String(dbInbounds.length)">
<template #prefix>

View file

@ -195,7 +195,6 @@ export function useInbounds() {
if (!upd) continue;
if (typeof upd.up === 'number') ib.up = upd.up;
if (typeof upd.down === 'number') ib.down = upd.down;
if (typeof upd.allTime === 'number') ib.allTime = upd.allTime;
if (typeof upd.total === 'number') ib.total = upd.total;
if (typeof upd.enable === 'boolean') ib.enable = upd.enable;
touched = true;
@ -216,7 +215,6 @@ export function useInbounds() {
if (typeof upd.up === 'number') stat.up = upd.up;
if (typeof upd.down === 'number') stat.down = upd.down;
if (typeof upd.total === 'number') stat.total = upd.total;
if (typeof upd.allTime === 'number') stat.allTime = upd.allTime;
if (typeof upd.expiryTime === 'number') stat.expiryTime = upd.expiryTime;
if (typeof upd.enable === 'boolean') stat.enable = upd.enable;
touched = true;
@ -283,12 +281,9 @@ export function useInbounds() {
}
}
// Aggregate totals shown in the dashboard summary card. allTime falls
// back to up+down when the per-inbound counter isn't populated yet.
const totals = computed(() => {
let up = 0;
let down = 0;
let allTime = 0;
let clients = 0;
const deactive = [];
const depleted = [];
@ -297,7 +292,6 @@ export function useInbounds() {
for (const ib of dbInbounds.value) {
up += ib.up || 0;
down += ib.down || 0;
allTime += ib.allTime || (ib.up + ib.down) || 0;
const c = clientCount.value[ib.id];
if (c) {
clients += c.clients;
@ -307,7 +301,7 @@ export function useInbounds() {
online.push(...c.online);
}
}
return { up, down, allTime, clients, deactive, depleted, expiring, online };
return { up, down, clients, deactive, depleted, expiring, online };
});
// ObjectUtil reference is wired at module load — keeping a no-op import

View file

@ -1700,7 +1700,6 @@ func (s *InboundService) setRemoteTrafficLocked(nodeID int, snap *runtime.Traffi
ExpiryTime: snapIb.ExpiryTime,
Up: snapIb.Up,
Down: snapIb.Down,
AllTime: snapIb.AllTime,
}
if err := tx.Create(&newIb).Error; err != nil {
logger.Warning("setRemoteTraffic: create central inbound for tag", snapIb.Tag, "failed:", err)
@ -1730,9 +1729,6 @@ func (s *InboundService) setRemoteTrafficLocked(nodeID int, snap *runtime.Traffi
updates["up"] = snapIb.Up
updates["down"] = snapIb.Down
}
if snapIb.AllTime > c.AllTime {
updates["all_time"] = snapIb.AllTime
}
if c.Settings != snapIb.Settings ||
c.Remark != snapIb.Remark ||
@ -1792,7 +1788,6 @@ func (s *InboundService) setRemoteTrafficLocked(nodeID int, snap *runtime.Traffi
Reset: cs.Reset,
Up: cs.Up,
Down: cs.Down,
AllTime: cs.AllTime,
LastOnline: cs.LastOnline,
}).Error; err != nil {
return false, err
@ -1808,17 +1803,12 @@ func (s *InboundService) setRemoteTrafficLocked(nodeID int, snap *runtime.Traffi
structuralChange = true
}
allTime := existing.AllTime
if cs.AllTime > allTime {
allTime = cs.AllTime
}
if inGrace && cs.Up+cs.Down > 0 {
if err := tx.Exec(
`UPDATE client_traffics
SET enable = ?, total = ?, expiry_time = ?, reset = ?, all_time = ?
SET enable = ?, total = ?, expiry_time = ?, reset = ?
WHERE inbound_id = ? AND email = ?`,
cs.Enable, cs.Total, cs.ExpiryTime, cs.Reset, allTime, c.Id, cs.Email,
cs.Enable, cs.Total, cs.ExpiryTime, cs.Reset, c.Id, cs.Email,
).Error; err != nil {
return false, err
}
@ -1828,9 +1818,9 @@ func (s *InboundService) setRemoteTrafficLocked(nodeID int, snap *runtime.Traffi
if err := tx.Exec(
`UPDATE client_traffics
SET up = ?, down = ?, enable = ?, total = ?, expiry_time = ?, reset = ?,
all_time = ?, last_online = MAX(last_online, ?)
last_online = MAX(last_online, ?)
WHERE inbound_id = ? AND email = ?`,
cs.Up, cs.Down, cs.Enable, cs.Total, cs.ExpiryTime, cs.Reset, allTime,
cs.Up, cs.Down, cs.Enable, cs.Total, cs.ExpiryTime, cs.Reset,
cs.LastOnline, c.Id, cs.Email,
).Error; err != nil {
return false, err
@ -1930,9 +1920,8 @@ func (s *InboundService) addInboundTraffic(tx *gorm.DB, traffics []*xray.Traffic
if traffic.IsInbound {
err = tx.Model(&model.Inbound{}).Where("tag = ? AND node_id IS NULL", traffic.Tag).
Updates(map[string]any{
"up": gorm.Expr("up + ?", traffic.Up),
"down": gorm.Expr("down + ?", traffic.Down),
"all_time": gorm.Expr("COALESCE(all_time, 0) + ?", traffic.Up+traffic.Down),
"up": gorm.Expr("up + ?", traffic.Up),
"down": gorm.Expr("down + ?", traffic.Down),
}).Error
if err != nil {
return err
@ -1987,7 +1976,6 @@ 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
if t.Up+t.Down > 0 {
dbClientTraffics[dbTraffic_index].LastOnline = now
}
@ -3449,19 +3437,18 @@ func (s *InboundService) GetAllClientTraffics() ([]*xray.ClientTraffic, error) {
}
type InboundTrafficSummary struct {
Id int `json:"id"`
Up int64 `json:"up"`
Down int64 `json:"down"`
Total int64 `json:"total"`
AllTime int64 `json:"allTime"`
Enable bool `json:"enable"`
Id int `json:"id"`
Up int64 `json:"up"`
Down int64 `json:"down"`
Total int64 `json:"total"`
Enable bool `json:"enable"`
}
func (s *InboundService) GetInboundsTrafficSummary() ([]InboundTrafficSummary, error) {
db := database.GetDB()
var summaries []InboundTrafficSummary
if err := db.Model(&model.Inbound{}).
Select("id, up, down, total, all_time, enable").
Select("id, up, down, total, enable").
Find(&summaries).Error; err != nil {
return nil, err
}
@ -3489,9 +3476,8 @@ func (s *InboundService) UpdateClientTrafficByEmail(email string, upload int64,
err := db.Model(xray.ClientTraffic{}).
Where("email = ?", email).
Updates(map[string]any{
"up": upload,
"down": download,
"all_time": gorm.Expr("CASE WHEN COALESCE(all_time, 0) < ? THEN ? ELSE all_time END", upload+download, upload+download),
"up": upload,
"down": download,
}).Error
if err != nil {
logger.Warningf("Error updating ClientTraffic with email %s: %v", email, err)
@ -3664,23 +3650,15 @@ func (s *InboundService) MigrationRequirements() {
}
}()
// Calculate and backfill all_time from up+down for inbounds and clients
err = tx.Exec(`
UPDATE inbounds
SET all_time = IFNULL(up, 0) + IFNULL(down, 0)
WHERE IFNULL(all_time, 0) = 0 AND (IFNULL(up, 0) + IFNULL(down, 0)) > 0
`).Error
if err != nil {
return
if tx.Migrator().HasColumn(&model.Inbound{}, "all_time") {
if err = tx.Migrator().DropColumn(&model.Inbound{}, "all_time"); err != nil {
return
}
}
err = tx.Exec(`
UPDATE client_traffics
SET all_time = IFNULL(up, 0) + IFNULL(down, 0)
WHERE IFNULL(all_time, 0) = 0 AND (IFNULL(up, 0) + IFNULL(down, 0)) > 0
`).Error
if err != nil {
return
if tx.Migrator().HasColumn(&xray.ClientTraffic{}, "all_time") {
if err = tx.Migrator().DropColumn(&xray.ClientTraffic{}, "all_time"); err != nil {
return
}
}
// Fix inbounds based problems

View file

@ -237,8 +237,6 @@
"customGeoEmpty": "لا توجد مصادر geo مخصصة بعد — انقر على «إضافة» لإنشاء واحد"
},
"inbounds": {
"allTimeTraffic": "إجمالي حركة المرور",
"allTimeTrafficUsage": "إجمالي الاستخدام طوال الوقت",
"title": "الإدخالات",
"totalDownUp": "إجمالي المرسل/المستقبل",
"totalUsage": "إجمالي الاستخدام",

View file

@ -238,8 +238,6 @@
"getConfigError": "An error occurred while retrieving the config file."
},
"inbounds": {
"allTimeTraffic": "All-time Traffic",
"allTimeTrafficUsage": "All-Time Total Usage",
"title": "Inbounds",
"totalDownUp": "Total Sent/Received",
"totalUsage": "Total Usage",

View file

@ -237,8 +237,6 @@
"customGeoEmpty": "Aún no hay fuentes geo personalizadas — haz clic en Añadir para crear una"
},
"inbounds": {
"allTimeTraffic": "Tráfico Total",
"allTimeTrafficUsage": "Uso de datos histórico",
"title": "Entradas",
"totalDownUp": "Subidas/Descargas Totales",
"totalUsage": "Uso Total",

View file

@ -240,8 +240,6 @@
"node": "نود",
"deployTo": "استقرار روی",
"localPanel": "پنل لوکال",
"allTimeTraffic": "کل ترافیک",
"allTimeTrafficUsage": "کل استفاده در تمام مدت",
"title": "کاربران",
"totalDownUp": "دریافت/ارسال کل",
"totalUsage": "‌‌‌مصرف کل",

View file

@ -237,8 +237,6 @@
"customGeoEmpty": "Belum ada sumber geo kustom — klik Tambah untuk membuatnya"
},
"inbounds": {
"allTimeTraffic": "Total Lalu Lintas",
"allTimeTrafficUsage": "Total Penggunaan Sepanjang Waktu",
"title": "Masuk",
"totalDownUp": "Total Terkirim/Diterima",
"totalUsage": "Penggunaan Total",

View file

@ -237,8 +237,6 @@
"customGeoEmpty": "カスタム geo ソースはまだありません — 「追加」をクリックして作成してください"
},
"inbounds": {
"allTimeTraffic": "総トラフィック",
"allTimeTrafficUsage": "これまでの総使用量",
"title": "インバウンド一覧",
"totalDownUp": "総アップロード / ダウンロード",
"totalUsage": "総使用量",

View file

@ -237,8 +237,6 @@
"customGeoEmpty": "Ainda não há fontes geo personalizadas — clique em Adicionar para criar uma"
},
"inbounds": {
"allTimeTraffic": "Tráfego Total",
"allTimeTrafficUsage": "Uso total de todos os tempos",
"title": "Inbounds",
"totalDownUp": "Total Enviado/Recebido",
"totalUsage": "Uso Total",

View file

@ -237,8 +237,6 @@
"getConfigError": "Произошла ошибка при получении конфигурационного файла"
},
"inbounds": {
"allTimeTraffic": "Общий трафик",
"allTimeTrafficUsage": "Общее использование за все время",
"title": "Подключения",
"totalDownUp": "Отправлено/получено",
"totalUsage": "Всего трафика",

View file

@ -237,8 +237,6 @@
"customGeoEmpty": "Henüz özel geo kaynağı yok — oluşturmak için Ekle'ye tıklayın"
},
"inbounds": {
"allTimeTraffic": "Toplam Trafik",
"allTimeTrafficUsage": "Tüm Zamanların Toplam Kullanımı",
"title": "Gelenler",
"totalDownUp": "Toplam Gönderilen/Alınan",
"totalUsage": "Toplam Kullanım",

View file

@ -237,8 +237,6 @@
"customGeoEmpty": "Користувацьких джерел geo поки немає — натисніть «Додати», щоб створити"
},
"inbounds": {
"allTimeTraffic": "Загальний трафік",
"allTimeTrafficUsage": "Загальне використання за весь час",
"title": "Вхідні",
"totalDownUp": "Всього надісланих/отриманих",
"totalUsage": "Всього використанно",

View file

@ -237,8 +237,6 @@
"customGeoEmpty": "Chưa có nguồn geo tùy chỉnh nào — nhấp Thêm để tạo"
},
"inbounds": {
"allTimeTraffic": "Tổng Lưu Lượng",
"allTimeTrafficUsage": "Tổng mức sử dụng mọi lúc",
"title": "Điểm vào (Inbounds)",
"totalDownUp": "Tổng tải lên/tải xuống",
"totalUsage": "Tổng sử dụng",

View file

@ -237,8 +237,6 @@
"customGeoEmpty": "暂无自定义 geo 源 — 点击「添加」以创建"
},
"inbounds": {
"allTimeTraffic": "累计总流量",
"allTimeTrafficUsage": "所有时间总使用量",
"title": "入站列表",
"totalDownUp": "总上传 / 下载",
"totalUsage": "总用量",

View file

@ -237,8 +237,6 @@
"customGeoEmpty": "尚無自訂 geo 來源 — 點擊「新增」以建立"
},
"inbounds": {
"allTimeTraffic": "累計總流量",
"allTimeTrafficUsage": "所有时间总使用量",
"title": "入站列表",
"totalDownUp": "總上傳 / 下載",
"totalUsage": "總用量",

View file

@ -11,7 +11,6 @@ type ClientTraffic struct {
SubId string `json:"subId" form:"subId" gorm:"-"`
Up int64 `json:"up" form:"up"`
Down int64 `json:"down" form:"down"`
AllTime int64 `json:"allTime" form:"allTime"`
ExpiryTime int64 `json:"expiryTime" form:"expiryTime"`
Total int64 `json:"total" form:"total"`
Reset int `json:"reset" form:"reset" gorm:"default:0"`