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
+10 -2
View File
@@ -38,7 +38,7 @@
"edit": "Edit",
"delete": "Delete",
"reset": "Reset",
"noData": "No data.",
"noData": "Nothing here yet",
"copySuccess": "Copied successfully",
"sure": "Sure",
"encryption": "Encryption",
@@ -461,6 +461,15 @@
"searchPlaceholder": "Search email, comment, sub ID, UUID, password, auth…",
"filterTitle": "Filter clients",
"clearAllFilters": "Clear all",
"sortOldest": "Oldest first",
"sortNewest": "Newest first",
"sortRecentlyUpdated": "Recently updated",
"sortRecentlyOnline": "Recently online",
"sortEmailAZ": "Email A→Z",
"sortEmailZA": "Email Z→A",
"sortMostTraffic": "Most traffic",
"sortHighestRemaining": "Highest remaining",
"sortExpiringSoonest": "Expiring soonest",
"has": "Has",
"hasNot": "Doesn't have",
"title": "Clients",
@@ -496,7 +505,6 @@
"resetAllTraffics": "Reset all client traffic",
"resetAllTrafficsTitle": "Reset all client traffic?",
"resetAllTrafficsContent": "Every client's up/down counter drops to zero. Quotas and expiry are not affected. This cannot be undone.",
"empty": "No clients yet — add one to get started.",
"deleteConfirmTitle": "Delete client {email}?",
"deleteConfirmContent": "This removes the client from every attached inbound and drops its traffic record. This cannot be undone.",
"deleteSelected": "Delete ({count})",