feat(clients): toolbar sort selector + preserve updated_at on unchanged rows

Frontend
- New Sort dropdown in the clients toolbar covering oldest/newest,
  recently updated, recently online, email A↔Z, most traffic, highest
  remaining, expiring soonest. Default is Oldest first.
- Strip per-column sorter arrows from the Table — all sorting now flows
  through the single dropdown, so the column headers stop competing
  with it.
- Empty state: TeamOutlined icon, t('noData'), text-secondary color
  (matching the inbound/node polish).

Backend
- sortClients: add createdAt, updatedAt and lastOnline cases (with id
  tie-break for stable ordering when timestamps collide).
- Fix Recently updated: SyncInbound was calling tx.Save on every client
  in the inbound, and GORM's autoUpdateTime tag stamped updated_at to
  time.Now() each time — so editing one client bumped ALL of them.
  After the Save, restore each row's preserved updated_at via
  UpdateColumn (skips hooks). The actually-edited client gets its
  fresh stamp from the explicit UpdateColumn at the end of Update().
- Fix periodic updated_at churn: adjustTraffics unconditionally set
  c["updated_at"] = now() for every client in any inbound that had a
  delayed-start expiry, every traffic-stats pass. Turn that into a
  backfill (only when the key is missing), matching the created_at
  treatment one line above.
This commit is contained in:
MHSanaei
2026-05-27 15:07:17 +02:00
parent 6286bb8676
commit 7680e27d1d
5 changed files with 214 additions and 155 deletions
+33 -3
View File
@@ -242,12 +242,19 @@ func (s *ClientService) SyncInbound(tx *gorm.DB, inboundId int, clients []model.
if incoming.CreatedAt > 0 && (row.CreatedAt == 0 || incoming.CreatedAt < row.CreatedAt) {
row.CreatedAt = incoming.CreatedAt
}
if incoming.UpdatedAt > row.UpdatedAt {
row.UpdatedAt = incoming.UpdatedAt
preservedUpdatedAt := row.UpdatedAt
if incoming.UpdatedAt > preservedUpdatedAt {
preservedUpdatedAt = incoming.UpdatedAt
}
row.UpdatedAt = preservedUpdatedAt
if err := tx.Save(row).Error; err != nil {
return err
}
if err := tx.Model(&model.ClientRecord{}).
Where("id = ?", row.Id).
UpdateColumn("updated_at", preservedUpdatedAt).Error; err != nil {
return err
}
}
link := model.ClientInbound{
@@ -648,7 +655,7 @@ func (s *ClientService) Update(inboundSvc *InboundService, id int, updated model
if err := database.GetDB().Model(&model.ClientRecord{}).
Where("id = ?", id).
Update("updated_at", updated.UpdatedAt).Error; err != nil {
UpdateColumn("updated_at", time.Now().UnixMilli()).Error; err != nil {
return needRestart, err
}
return needRestart, nil
@@ -1343,6 +1350,29 @@ func sortClients(rows []ClientWithAttachments, sortKey, order string) {
eb = b.ExpiryTime
}
return ea < eb
case "createdAt":
if a.CreatedAt == b.CreatedAt {
return a.Id < b.Id
}
return a.CreatedAt < b.CreatedAt
case "updatedAt":
if a.UpdatedAt == b.UpdatedAt {
return a.Id < b.Id
}
return a.UpdatedAt < b.UpdatedAt
case "lastOnline":
la := int64(0)
if a.Traffic != nil {
la = a.Traffic.LastOnline
}
lb := int64(0)
if b.Traffic != nil {
lb = b.Traffic.LastOnline
}
if la == lb {
return a.Id < b.Id
}
return la < lb
}
return false
}
+3 -2
View File
@@ -1745,11 +1745,12 @@ func (s *InboundService) adjustTraffics(tx *gorm.DB, dbClientTraffics []*xray.Cl
break
}
}
// Backfill created_at and updated_at
if _, ok := c["created_at"]; !ok {
c["created_at"] = time.Now().Unix() * 1000
}
c["updated_at"] = time.Now().Unix() * 1000
if _, ok := c["updated_at"]; !ok {
c["updated_at"] = time.Now().Unix() * 1000
}
newClients = append(newClients, any(c))
}
settings["clients"] = newClients