Files
3x-ui/internal/web/job/sidecar_traffic.go
T
Kuzz007 1aa81428b8 fix(traffic): show live Speed for AmneziaWG and MTProto inbounds/clients
The Speed column showed "--" for AmneziaWG (and MTProto, which has the
identical gap) even while cumulative traffic totals were correct.
XrayTrafficJob drives live speed by querying xray-core's own stats API
and broadcasting the delta over websocket -- but AmneziaWG/MTProto never
run inside xray-core's own runtime inbounds, so they're invisible to
that API. Their own jobs already compute the same per-poll delta shape
(that's what keeps cumulative totals correct) but never broadcast it.

Reusing the existing "traffics"/"clientTraffics" broadcast would have
two real bugs: the frontend's existing scope/replace logic would let
each side clobber the other's speed on its next unrelated tick, and the
websocket hub's per-message-type throttle is keyed only by message type,
not caller -- since both sidecar jobs run on identical "@every 10s"
grids registered milliseconds apart, one would silently lose almost
every broadcast if both protocols were ever configured together.

Fixed with a small unthrottled broadcast path (both sidecar jobs are
already self-rate-limited by their own cron cadence) and protocol-
namespaced wire keys, tracked in their own frontend state and merged
into the existing inboundSpeed/clientSpeed only at read time -- so every
existing consumer needs zero changes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-26 18:27:23 +03:00

44 lines
2.2 KiB
Go

package job
import (
"github.com/mhsanaei/3x-ui/v3/internal/web/websocket"
"github.com/mhsanaei/3x-ui/v3/internal/xray"
)
// sidecarTrafficPayload builds the websocket broadcast map for one poll of a
// sidecar protocol's traffic delta. "Sidecar" means a protocol that never
// runs inside xray-core's own inbounds -- AmneziaWG's kernel tunnel and
// MTProto's mtg process are the only two today (both explicitly skipped from
// claiming an xray-core tag in GenXrayInboundConfig). XrayTrafficJob's own 5s
// broadcast (xray_traffic_job.go) carries "traffics"/"clientTraffics" for
// xray-native inbounds/clients and never mentions sidecar tags, so reusing
// those keys would let each side's broadcast clobber the other's speed on
// its very next, unrelated tick. protocol namespaces the keys instead:
// "amneziawg" produces "amneziawgTraffics"/"amneziawgClientTraffics",
// "mtproto" produces "mtprotoTraffics"/"mtprotoClientTraffics" -- distinct
// pairs per protocol, so the frontend tracks each independently and never
// has to reconcile two protocols within one map (see useInbounds.ts /
// useClients.ts).
func sidecarTrafficPayload(protocol string, traffics []*xray.Traffic, clientTraffics []*xray.ClientTraffic) map[string]any {
return map[string]any{
protocol + "Traffics": traffics,
protocol + "ClientTraffics": clientTraffics,
}
}
// broadcastSidecarTraffic pushes this poll's live-speed snapshot for a
// sidecar protocol ("amneziawg" or "mtproto") over the websocket, so its
// inbound/client rows show a live Speed value the same way xray-native rows
// already do. Call this every tick -- even with empty slices -- so a peer
// that just went idle clears to no-speed on the frontend instead of sticking
// at its last nonzero reading; only the AddTraffic/RefreshLocalOnlineClients
// calls around it are conditioned on non-empty data, since cumulative-totals
// accounting doesn't need this signal. Uses BroadcastSidecarTraffic (not
// BroadcastTraffic) -- see its doc comment for why.
func broadcastSidecarTraffic(protocol string, traffics []*xray.Traffic, clientTraffics []*xray.ClientTraffic) {
if !websocket.HasClients() {
return
}
websocket.BroadcastSidecarTraffic(sidecarTrafficPayload(protocol, traffics, clientTraffics))
}