From 4a0914cb1e271ab4f076cb1bd68c9f07cc025e92 Mon Sep 17 00:00:00 2001
From: Ali Golzar <57574919+aliglzr@users.noreply.github.com>
Date: Sun, 31 Aug 2025 20:03:50 +0330
Subject: [PATCH] feat: add "Last Online" column to client list and modal
(Closes #3402) (#3405)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* feat: persist client last online and expose API
* feat(ui): show client last online in table and info modal
* i18n: add “Last Online” across locales
* chore: format timestamps as HH:mm:ss
---
web/assets/js/util/date-util.js | 2 +-
web/controller/api.go | 1 +
web/controller/inbound.go | 5 +++++
web/html/component/aClientTable.html | 17 +++++++++++------
web/html/inbounds.html | 18 ++++++++++++++++++
web/html/modals/inbound_info_modal.html | 6 ++++++
web/service/inbound.go | 15 +++++++++++++++
web/translation/translate.ar_EG.toml | 1 +
web/translation/translate.en_US.toml | 1 +
web/translation/translate.es_ES.toml | 1 +
web/translation/translate.fa_IR.toml | 1 +
web/translation/translate.id_ID.toml | 1 +
web/translation/translate.ja_JP.toml | 1 +
web/translation/translate.pt_BR.toml | 1 +
web/translation/translate.ru_RU.toml | 1 +
web/translation/translate.tr_TR.toml | 1 +
web/translation/translate.uk_UA.toml | 1 +
web/translation/translate.vi_VN.toml | 1 +
web/translation/translate.zh_CN.toml | 1 +
web/translation/translate.zh_TW.toml | 1 +
xray/client_traffic.go | 1 +
21 files changed, 71 insertions(+), 7 deletions(-)
diff --git a/web/assets/js/util/date-util.js b/web/assets/js/util/date-util.js
index 9b4b0f81..bbca1272 100644
--- a/web/assets/js/util/date-util.js
+++ b/web/assets/js/util/date-util.js
@@ -134,7 +134,7 @@ class DateUtil {
}
static formatMillis(millis) {
- return moment(millis).format('YYYY-M-D H:m:s');
+ return moment(millis).format('YYYY-M-D HH:mm:ss');
}
static firstDayOfMonth() {
diff --git a/web/controller/api.go b/web/controller/api.go
index 636035ba..32af934e 100644
--- a/web/controller/api.go
+++ b/web/controller/api.go
@@ -47,6 +47,7 @@ func (a *APIController) initRouter(g *gin.RouterGroup) {
{"POST", "/resetAllClientTraffics/:id", a.inboundController.resetAllClientTraffics},
{"POST", "/delDepletedClients/:id", a.inboundController.delDepletedClients},
{"POST", "/onlines", a.inboundController.onlines},
+ {"POST", "/lastOnline", a.inboundController.lastOnline},
{"POST", "/updateClientTraffic/:email", a.inboundController.updateClientTraffic},
}
diff --git a/web/controller/inbound.go b/web/controller/inbound.go
index 851b4b6f..9ff2f302 100644
--- a/web/controller/inbound.go
+++ b/web/controller/inbound.go
@@ -340,6 +340,11 @@ func (a *InboundController) onlines(c *gin.Context) {
jsonObj(c, a.inboundService.GetOnlineClients(), nil)
}
+func (a *InboundController) lastOnline(c *gin.Context) {
+ data, err := a.inboundService.GetClientsLastOnline()
+ jsonObj(c, data, err)
+}
+
func (a *InboundController) updateClientTraffic(c *gin.Context) {
email := c.Param("email")
diff --git a/web/html/component/aClientTable.html b/web/html/component/aClientTable.html
index 53ec27a3..a7279e50 100644
--- a/web/html/component/aClientTable.html
+++ b/web/html/component/aClientTable.html
@@ -33,12 +33,17 @@
-
- {{ i18n "online" }}
-
-
- {{ i18n "offline" }}
-
+
+
+ {{ i18n "lastOnline" }}: [[ formatLastOnline(client.email) ]]
+
+
+ {{ i18n "online" }}
+
+
+ {{ i18n "offline" }}
+
+
diff --git a/web/html/inbounds.html b/web/html/inbounds.html
index 1621807e..dfccdd70 100644
--- a/web/html/inbounds.html
+++ b/web/html/inbounds.html
@@ -807,6 +807,7 @@
defaultKey: '',
clientCount: [],
onlineClients: [],
+ lastOnlineMap: {},
isRefreshEnabled: localStorage.getItem("isRefreshEnabled") === "true" ? true : false,
refreshing: false,
refreshInterval: Number(localStorage.getItem("refreshInterval")) || 5000,
@@ -835,6 +836,7 @@
return;
}
+ await this.getLastOnlineMap();
await this.getOnlineUsers();
this.setInbounds(msg.obj);
@@ -849,6 +851,11 @@
}
this.onlineClients = msg.obj != null ? msg.obj : [];
},
+ async getLastOnlineMap() {
+ const msg = await HttpUtil.post('/panel/api/inbounds/lastOnline');
+ if (!msg.success || !msg.obj) return;
+ this.lastOnlineMap = msg.obj || {}
+ },
async getDefaultSettings() {
const msg = await HttpUtil.post('/panel/setting/defaultSettings');
if (!msg.success) {
@@ -1493,6 +1500,17 @@
isClientOnline(email) {
return this.onlineClients.includes(email);
},
+ getLastOnline(email) {
+ return this.lastOnlineMap[email] || null
+ },
+ formatLastOnline(email) {
+ const ts = this.getLastOnline(email)
+ if (!ts) return '-'
+ if (this.datepicker === 'gregorian') {
+ return DateUtil.formatMillis(ts)
+ }
+ return DateUtil.convertToJalalian(moment(ts))
+ },
isRemovable(dbInboundId) {
return this.getInboundClients(this.dbInbounds.find(row => row.id === dbInboundId)).length > 1;
},
diff --git a/web/html/modals/inbound_info_modal.html b/web/html/modals/inbound_info_modal.html
index fe7d7a82..a15172f3 100644
--- a/web/html/modals/inbound_info_modal.html
+++ b/web/html/modals/inbound_info_modal.html
@@ -217,6 +217,12 @@
+
+ {{ i18n "lastOnline" }} |
+
+ [[ app.formatLastOnline(infoModal.clientSettings && infoModal.clientSettings.email ? infoModal.clientSettings.email : '') ]]
+ |
+
{{ i18n "comment" }} |
diff --git a/web/service/inbound.go b/web/service/inbound.go
index 0621cdea..b494d502 100644
--- a/web/service/inbound.go
+++ b/web/service/inbound.go
@@ -967,6 +967,7 @@ func (s *InboundService) addClientTraffic(tx *gorm.DB, traffics []*xray.ClientTr
// Add user in onlineUsers array on traffic
if traffics[traffic_index].Up+traffics[traffic_index].Down > 0 {
onlineClients = append(onlineClients, traffics[traffic_index].Email)
+ dbClientTraffics[dbTraffic_index].LastOnline = time.Now().UnixMilli()
}
break
}
@@ -2187,6 +2188,20 @@ func (s *InboundService) GetOnlineClients() []string {
return p.GetOnlineClients()
}
+func (s *InboundService) GetClientsLastOnline() (map[string]int64, error) {
+ db := database.GetDB()
+ var rows []xray.ClientTraffic
+ err := db.Model(&xray.ClientTraffic{}).Select("email, last_online").Find(&rows).Error
+ if err != nil && err != gorm.ErrRecordNotFound {
+ return nil, err
+ }
+ result := make(map[string]int64, len(rows))
+ for _, r := range rows {
+ result[r.Email] = r.LastOnline
+ }
+ return result, nil
+}
+
func (s *InboundService) FilterAndSortClientEmails(emails []string) ([]string, []string, error) {
db := database.GetDB()
diff --git a/web/translation/translate.ar_EG.toml b/web/translation/translate.ar_EG.toml
index bbf68822..dd5618cb 100644
--- a/web/translation/translate.ar_EG.toml
+++ b/web/translation/translate.ar_EG.toml
@@ -50,6 +50,7 @@
"fail" = "فشل"
"comment" = "تعليق"
"success" = "تم بنجاح"
+"lastOnline" = "آخر متصل"
"getVersion" = "جيب النسخة"
"install" = "تثبيت"
"clients" = "عملاء"
diff --git a/web/translation/translate.en_US.toml b/web/translation/translate.en_US.toml
index 1531fe30..89f127a8 100644
--- a/web/translation/translate.en_US.toml
+++ b/web/translation/translate.en_US.toml
@@ -50,6 +50,7 @@
"fail" = "Failed"
"comment" = "Comment"
"success" = "Successfully"
+"lastOnline" = "Last Online"
"getVersion" = "Get Version"
"install" = "Install"
"clients" = "Clients"
diff --git a/web/translation/translate.es_ES.toml b/web/translation/translate.es_ES.toml
index 6a2b8958..070b6b57 100644
--- a/web/translation/translate.es_ES.toml
+++ b/web/translation/translate.es_ES.toml
@@ -50,6 +50,7 @@
"fail" = "Falló"
"comment" = "Comentario"
"success" = "Éxito"
+"lastOnline" = "Última conexión"
"getVersion" = "Obtener versión"
"install" = "Instalar"
"clients" = "Clientes"
diff --git a/web/translation/translate.fa_IR.toml b/web/translation/translate.fa_IR.toml
index fb6d9f02..5c949928 100644
--- a/web/translation/translate.fa_IR.toml
+++ b/web/translation/translate.fa_IR.toml
@@ -50,6 +50,7 @@
"fail" = "ناموفق"
"comment" = "توضیحات"
"success" = "موفق"
+"lastOnline" = "آخرین فعالیت"
"getVersion" = "دریافت نسخه"
"install" = "نصب"
"clients" = "کاربران"
diff --git a/web/translation/translate.id_ID.toml b/web/translation/translate.id_ID.toml
index d0d77dc3..4dc8e378 100644
--- a/web/translation/translate.id_ID.toml
+++ b/web/translation/translate.id_ID.toml
@@ -50,6 +50,7 @@
"fail" = "Gagal"
"comment" = "Komentar"
"success" = "Berhasil"
+"lastOnline" = "Terakhir online"
"getVersion" = "Dapatkan Versi"
"install" = "Instal"
"clients" = "Klien"
diff --git a/web/translation/translate.ja_JP.toml b/web/translation/translate.ja_JP.toml
index 3f89cf0c..54479232 100644
--- a/web/translation/translate.ja_JP.toml
+++ b/web/translation/translate.ja_JP.toml
@@ -50,6 +50,7 @@
"fail" = "失敗"
"comment" = "コメント"
"success" = "成功"
+"lastOnline" = "最終オンライン"
"getVersion" = "バージョン取得"
"install" = "インストール"
"clients" = "クライアント"
diff --git a/web/translation/translate.pt_BR.toml b/web/translation/translate.pt_BR.toml
index 3755c61e..a3aac778 100644
--- a/web/translation/translate.pt_BR.toml
+++ b/web/translation/translate.pt_BR.toml
@@ -50,6 +50,7 @@
"fail" = "Falhou"
"comment" = "Comentário"
"success" = "Com Sucesso"
+"lastOnline" = "Última vez online"
"getVersion" = "Obter Versão"
"install" = "Instalar"
"clients" = "Clientes"
diff --git a/web/translation/translate.ru_RU.toml b/web/translation/translate.ru_RU.toml
index 8efc4673..718edb51 100644
--- a/web/translation/translate.ru_RU.toml
+++ b/web/translation/translate.ru_RU.toml
@@ -50,6 +50,7 @@
"fail" = "Ошибка"
"comment" = "Комментарий"
"success" = "Успешно"
+"lastOnline" = "Был(а) в сети"
"getVersion" = "Узнать версию"
"install" = "Установка"
"clients" = "Клиенты"
diff --git a/web/translation/translate.tr_TR.toml b/web/translation/translate.tr_TR.toml
index a298dd30..047d9d57 100644
--- a/web/translation/translate.tr_TR.toml
+++ b/web/translation/translate.tr_TR.toml
@@ -50,6 +50,7 @@
"fail" = "Başarısız"
"comment" = "Yorum"
"success" = "Başarılı"
+"lastOnline" = "Son çevrimiçi"
"getVersion" = "Sürümü Al"
"install" = "Yükle"
"clients" = "Müşteriler"
diff --git a/web/translation/translate.uk_UA.toml b/web/translation/translate.uk_UA.toml
index 02ed7352..3dc5b3e7 100644
--- a/web/translation/translate.uk_UA.toml
+++ b/web/translation/translate.uk_UA.toml
@@ -50,6 +50,7 @@
"fail" = "Помилка"
"comment" = "Коментар"
"success" = "Успішно"
+"lastOnline" = "Був(ла) онлайн"
"getVersion" = "Отримати версію"
"install" = "Встановити"
"clients" = "Клієнти"
diff --git a/web/translation/translate.vi_VN.toml b/web/translation/translate.vi_VN.toml
index 3f61874f..aa0009eb 100644
--- a/web/translation/translate.vi_VN.toml
+++ b/web/translation/translate.vi_VN.toml
@@ -50,6 +50,7 @@
"fail" = "Thất bại"
"comment" = "Bình luận"
"success" = "Thành công"
+"lastOnline" = "Lần online gần nhất"
"getVersion" = "Lấy phiên bản"
"install" = "Cài đặt"
"clients" = "Các khách hàng"
diff --git a/web/translation/translate.zh_CN.toml b/web/translation/translate.zh_CN.toml
index bf19cfdb..01844f13 100644
--- a/web/translation/translate.zh_CN.toml
+++ b/web/translation/translate.zh_CN.toml
@@ -50,6 +50,7 @@
"fail" = "失败"
"comment" = "评论"
"success" = "成功"
+"lastOnline" = "上次在线"
"getVersion" = "获取版本"
"install" = "安装"
"clients" = "客户端"
diff --git a/web/translation/translate.zh_TW.toml b/web/translation/translate.zh_TW.toml
index dfd284d2..f3121c69 100644
--- a/web/translation/translate.zh_TW.toml
+++ b/web/translation/translate.zh_TW.toml
@@ -50,6 +50,7 @@
"fail" = "失敗"
"comment" = "評論"
"success" = "成功"
+"lastOnline" = "上次上線"
"getVersion" = "獲取版本"
"install" = "安裝"
"clients" = "客戶端"
diff --git a/xray/client_traffic.go b/xray/client_traffic.go
index 883de2cc..fe527d55 100644
--- a/xray/client_traffic.go
+++ b/xray/client_traffic.go
@@ -11,4 +11,5 @@ type ClientTraffic struct {
ExpiryTime int64 `json:"expiryTime" form:"expiryTime"`
Total int64 `json:"total" form:"total"`
Reset int `json:"reset" form:"reset" gorm:"default:0"`
+ LastOnline int64 `json:"lastOnline" form:"lastOnline" gorm:"default:0"`
}
|