mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-08-16 00:01:02 +00:00
feat(nodes): show live speed for node-hosted inbounds
Inbound speed is computed on the dashboard from a 'traffics' delta feed, which only the local Xray poll produced — so node-hosted inbounds showed no speed. The node sync now diffs successive per-inbound cumulative totals (it polls @5s, same as the local poll) and broadcasts the byte deltas as a separate 'nodeTraffics' field, keyed by the central tag the dashboard already matches. The frontend applies 'traffics' to local inbounds and 'nodeTraffics' to node inbounds within their own scope, so the two 5s polls don't clobber each other and idle inbounds still clear. Deltas clamp to 0 on a reset; a node that fails to sync keeps a stale total so its delta is 0 (no phantom speed).
This commit is contained in:
@@ -399,6 +399,7 @@ export function useInbounds() {
|
||||
if (!payload || typeof payload !== 'object') return;
|
||||
const p = payload as {
|
||||
traffics?: TrafficDelta[];
|
||||
nodeTraffics?: TrafficDelta[];
|
||||
onlineClients?: string[];
|
||||
onlineByGuid?: Record<string, string[]>;
|
||||
activeInbounds?: Record<string, string[]>;
|
||||
@@ -417,26 +418,40 @@ export function useInbounds() {
|
||||
if (p.lastOnlineMap && typeof p.lastOnlineMap === 'object') {
|
||||
setLastOnlineMap((prev) => ({ ...prev, ...p.lastOnlineMap! }));
|
||||
}
|
||||
// Full-replace each poll so idle inbounds (and an empty array after an
|
||||
// Xray stat reset) clear their speed instead of showing a stale value.
|
||||
if (Array.isArray(p.traffics)) {
|
||||
// Speed arrives from two independent 5s polls: the local Xray poll sends
|
||||
// `traffics` (local inbounds) and the node sync sends `nodeTraffics` (node
|
||||
// inbounds). Each replaces speed only within its own scope so the two don't
|
||||
// clobber each other; an idle in-scope inbound — absent from its payload —
|
||||
// clears instead of showing a stale value.
|
||||
const applyTraffics = (
|
||||
traffics: TrafficDelta[],
|
||||
inScope: (ib: DBInboundInstance) => boolean,
|
||||
) => {
|
||||
const byTag = new Map<string, TrafficDelta>();
|
||||
for (const tr of p.traffics) {
|
||||
for (const tr of traffics) {
|
||||
if (!tr || typeof tr.Tag !== 'string') continue;
|
||||
if (tr.IsInbound === false) continue;
|
||||
byTag.set(tr.Tag, tr);
|
||||
}
|
||||
const nextSpeed: Record<number, InboundSpeedEntry> = {};
|
||||
for (const ib of dbInboundsRef.current) {
|
||||
const delta = byTag.get(ib.tag);
|
||||
if (!delta) continue;
|
||||
nextSpeed[ib.id] = {
|
||||
up: (delta.Up || 0) / TRAFFIC_POLL_INTERVAL_S,
|
||||
down: (delta.Down || 0) / TRAFFIC_POLL_INTERVAL_S,
|
||||
};
|
||||
}
|
||||
setInboundSpeed(nextSpeed);
|
||||
}
|
||||
setInboundSpeed((prev) => {
|
||||
const next = { ...prev };
|
||||
for (const ib of dbInboundsRef.current) {
|
||||
if (!inScope(ib)) continue;
|
||||
const delta = byTag.get(ib.tag);
|
||||
if (delta) {
|
||||
next[ib.id] = {
|
||||
up: (delta.Up || 0) / TRAFFIC_POLL_INTERVAL_S,
|
||||
down: (delta.Down || 0) / TRAFFIC_POLL_INTERVAL_S,
|
||||
};
|
||||
} else {
|
||||
delete next[ib.id];
|
||||
}
|
||||
}
|
||||
return next;
|
||||
});
|
||||
};
|
||||
if (Array.isArray(p.traffics)) applyTraffics(p.traffics, (ib) => ib.nodeId == null);
|
||||
if (Array.isArray(p.nodeTraffics)) applyTraffics(p.nodeTraffics, (ib) => ib.nodeId != null);
|
||||
rebuildClientCount();
|
||||
},
|
||||
[rebuildClientCount],
|
||||
|
||||
Reference in New Issue
Block a user