fix(inbounds): refresh client rows live over websocket

Two bugs combined to leave per-client traffic / remained / all-time
columns stuck at stale numbers while only the inbound-level row and
the online badge refreshed:

1. Backend (xray + node sync traffic jobs) only included the per-client
   array in the client_stats broadcast when activeEmails / touched
   was non-empty. Cycles with no client deltas — or any node sync that
   failed to fetch a snapshot — shipped only the inbound summary, so
   the frontend had nothing to merge for clients. Replaced both code
   paths with a single GetAllClientTraffics() snapshot per cycle; the
   broadcast now always carries the full client list.

2. Frontend mutated dbInbound.clientStats[i] in place. DBInbound is a
   plain class instance (not wrapped in reactive()), so Vue could not
   see the field-level changes and ClientRowTable's statsMap computed
   stayed cached forever. Added a statsVersion tick bumped on every
   merge and read inside statsMap so the computed re-evaluates and the
   template pulls fresh up/down/allTime/expiryTime each push.

Removed the now-dead emailSet helper from node_traffic_sync_job and
the activeEmails filter from xray_traffic_job.
This commit is contained in:
MHSanaei
2026-05-14 01:31:49 +02:00
parent ce4c42e09c
commit 2551a673c3
7 changed files with 49 additions and 85 deletions

View File

@@ -33,6 +33,7 @@ const props = defineProps({
isDarkTheme: { type: Boolean, default: false },
pageSize: { type: Number, default: 0 },
totalClientCount: { type: Number, default: 0 },
statsVersion: { type: Number, default: 0 },
});
const emit = defineEmits([
@@ -63,7 +64,11 @@ watch([clients, () => props.pageSize], () => {
});
// === Per-client stats lookup =======================================
// statsVersion bumps on every ws merge so this computed re-evaluates
// (DBInbound isn't reactive — the in-place stat mutations alone don't
// trigger Vue's tracking).
const statsMap = computed(() => {
void props.statsVersion;
const m = new Map();
for (const cs of (props.dbInbound.clientStats || [])) m.set(cs.email, cs);
return m;

View File

@@ -50,6 +50,7 @@ const props = defineProps({
// inbound row can render its node name without an extra fetch.
nodesById: { type: Map, default: () => new Map() },
hasActiveNode: { type: Boolean, default: false },
statsVersion: { type: Number, default: 0 },
});
const emit = defineEmits([
@@ -468,6 +469,7 @@ function showQrCodeMenu(dbInbound) {
<ClientRowTable :db-inbound="record" :is-mobile="true" :traffic-diff="trafficDiff" :expire-diff="expireDiff"
:online-clients="onlineClients" :last-online-map="lastOnlineMap" :is-dark-theme="isDarkTheme"
:page-size="pageSize" :total-client-count="clientCount[record.id]?.clients || 0"
:stats-version="statsVersion"
@edit-client="(p) => emit('edit-client', p)" @qrcode-client="(p) => emit('qrcode-client', p)"
@info-client="(p) => emit('info-client', p)"
@reset-traffic-client="(p) => emit('reset-traffic-client', p)"
@@ -557,6 +559,7 @@ function showQrCodeMenu(dbInbound) {
:traffic-diff="trafficDiff" :expire-diff="expireDiff" :online-clients="onlineClients"
:last-online-map="lastOnlineMap" :is-dark-theme="isDarkTheme" :page-size="pageSize"
:total-client-count="clientCount[record.id]?.clients || 0"
:stats-version="statsVersion"
@edit-client="(p) => emit('edit-client', p)"
@qrcode-client="(p) => emit('qrcode-client', p)" @info-client="(p) => emit('info-client', p)"
@reset-traffic-client="(p) => emit('reset-traffic-client', p)"

View File

@@ -45,6 +45,7 @@ const {
ipLimitEnable,
remarkModel,
lastOnlineMap,
statsVersion,
refresh,
fetchDefaultSettings,
applyTrafficEvent,
@@ -648,6 +649,7 @@ function onRowAction({ key, dbInbound }) {
:last-online-map="lastOnlineMap" :is-dark-theme="themeState.isDark" :expire-diff="expireDiff"
:traffic-diff="trafficDiff" :page-size="pageSize" :is-mobile="isMobile"
:sub-enable="subSettings.enable" :nodes-by-id="nodesById" :has-active-node="hasActiveNode"
:stats-version="statsVersion"
@refresh="refresh"
@add-inbound="onAddInbound" @general-action="onGeneralAction" @row-action="onRowAction"
@edit-client="onEditClient" @qrcode-client="onQrcodeClient" @info-client="onInfoClient"

View File

@@ -23,6 +23,11 @@ export function useInbounds() {
const clientCount = ref({});
const onlineClients = ref([]);
const lastOnlineMap = ref({});
// Bumps on every client_stats merge so the per-inbound ClientRowTable
// child can re-render. DBInbound is a plain class instance, not reactive,
// so the in-place mutations on its clientStats array are invisible to
// Vue's tracking unless something else (this tick) signals the change.
const statsVersion = ref(0);
// Default-settings sidecar fields the table needs for color/expiry math.
const expireDiff = ref(0);
@@ -173,9 +178,9 @@ export function useInbounds() {
rebuildClientCount();
}
// The client_stats payload carries absolute traffic counters for the
// clients that had activity in the latest window plus per-inbound
// totals. Both are absolute (not deltas), so we overwrite in place.
// The client_stats payload carries absolute traffic counters for every
// client + per-inbound totals (full snapshot, not deltas). Both are
// overwritten in place.
function applyClientStatsEvent(payload) {
if (!payload || typeof payload !== 'object') return;
let touched = false;
@@ -220,6 +225,7 @@ export function useInbounds() {
}
if (touched) {
statsVersion.value++;
dbInbounds.value = [...dbInbounds.value];
rebuildClientCount();
}
@@ -315,6 +321,7 @@ export function useInbounds() {
clientCount,
onlineClients,
lastOnlineMap,
statsVersion,
totals,
expireDiff,
trafficDiff,