|
@@ -12,6 +12,8 @@ import { SlimInboundListSchema, LastOnlineMapSchema, InboundDetailSchema } from
|
|
|
import { OnlinesSchema, OnlineByNodeSchema, ActiveInboundsByNodeSchema } from '@/schemas/client';
|
|
import { OnlinesSchema, OnlineByNodeSchema, ActiveInboundsByNodeSchema } from '@/schemas/client';
|
|
|
import { DefaultsPayloadSchema, type DefaultsPayload } from '@/schemas/defaults';
|
|
import { DefaultsPayloadSchema, type DefaultsPayload } from '@/schemas/defaults';
|
|
|
|
|
|
|
|
|
|
+import type { InboundSpeedEntry } from './list/types';
|
|
|
|
|
+
|
|
|
export interface SubSettings {
|
|
export interface SubSettings {
|
|
|
enable: boolean;
|
|
enable: boolean;
|
|
|
subTitle: string;
|
|
subTitle: string;
|
|
@@ -26,6 +28,17 @@ export interface SubSettings {
|
|
|
|
|
|
|
|
type DBInboundInstance = InstanceType<typeof DBInbound>;
|
|
type DBInboundInstance = InstanceType<typeof DBInbound>;
|
|
|
|
|
|
|
|
|
|
+// Server-side traffic polling interval in seconds. XrayTrafficJob broadcasts
|
|
|
|
|
+// deltas accumulated over this window, so dividing by it yields bytes/sec.
|
|
|
|
|
+const TRAFFIC_POLL_INTERVAL_S = 5;
|
|
|
|
|
+
|
|
|
|
|
+interface TrafficDelta {
|
|
|
|
|
+ Tag: string;
|
|
|
|
|
+ Up: number;
|
|
|
|
|
+ Down: number;
|
|
|
|
|
+ IsInbound?: boolean;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
interface ClientRollup {
|
|
interface ClientRollup {
|
|
|
clients: number;
|
|
clients: number;
|
|
|
active: string[];
|
|
active: string[];
|
|
@@ -179,6 +192,8 @@ export function useInbounds() {
|
|
|
const [clientCount, setClientCount] = useState<Record<number, ClientRollup>>({});
|
|
const [clientCount, setClientCount] = useState<Record<number, ClientRollup>>({});
|
|
|
const [statsVersion, setStatsVersion] = useState(0);
|
|
const [statsVersion, setStatsVersion] = useState(0);
|
|
|
|
|
|
|
|
|
|
+ const [inboundSpeed, setInboundSpeed] = useState<Record<number, InboundSpeedEntry>>({});
|
|
|
|
|
+
|
|
|
const [onlineClients, setOnlineClients] = useState<string[]>([]);
|
|
const [onlineClients, setOnlineClients] = useState<string[]>([]);
|
|
|
const onlineClientsRef = useRef<string[]>([]);
|
|
const onlineClientsRef = useRef<string[]>([]);
|
|
|
onlineClientsRef.current = onlineClients;
|
|
onlineClientsRef.current = onlineClients;
|
|
@@ -383,7 +398,13 @@ export function useInbounds() {
|
|
|
const applyTrafficEvent = useCallback(
|
|
const applyTrafficEvent = useCallback(
|
|
|
(payload: unknown) => {
|
|
(payload: unknown) => {
|
|
|
if (!payload || typeof payload !== 'object') return;
|
|
if (!payload || typeof payload !== 'object') return;
|
|
|
- const p = payload as { onlineClients?: string[]; onlineByGuid?: Record<string, string[]>; activeInbounds?: Record<string, string[]>; lastOnlineMap?: Record<string, number> };
|
|
|
|
|
|
|
+ const p = payload as {
|
|
|
|
|
+ traffics?: TrafficDelta[];
|
|
|
|
|
+ onlineClients?: string[];
|
|
|
|
|
+ onlineByGuid?: Record<string, string[]>;
|
|
|
|
|
+ activeInbounds?: Record<string, string[]>;
|
|
|
|
|
+ lastOnlineMap?: Record<string, number>;
|
|
|
|
|
+ };
|
|
|
if (Array.isArray(p.onlineClients)) {
|
|
if (Array.isArray(p.onlineClients)) {
|
|
|
onlineClientsRef.current = p.onlineClients;
|
|
onlineClientsRef.current = p.onlineClients;
|
|
|
setOnlineClients(p.onlineClients);
|
|
setOnlineClients(p.onlineClients);
|
|
@@ -397,6 +418,26 @@ export function useInbounds() {
|
|
|
if (p.lastOnlineMap && typeof p.lastOnlineMap === 'object') {
|
|
if (p.lastOnlineMap && typeof p.lastOnlineMap === 'object') {
|
|
|
setLastOnlineMap((prev) => ({ ...prev, ...p.lastOnlineMap! }));
|
|
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)) {
|
|
|
|
|
+ const byTag = new Map<string, TrafficDelta>();
|
|
|
|
|
+ for (const tr of p.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);
|
|
|
|
|
+ }
|
|
|
rebuildClientCount();
|
|
rebuildClientCount();
|
|
|
},
|
|
},
|
|
|
[rebuildClientCount],
|
|
[rebuildClientCount],
|
|
@@ -481,6 +522,7 @@ export function useInbounds() {
|
|
|
clientCount,
|
|
clientCount,
|
|
|
onlineClients,
|
|
onlineClients,
|
|
|
lastOnlineMap,
|
|
lastOnlineMap,
|
|
|
|
|
+ inboundSpeed,
|
|
|
statsVersion,
|
|
statsVersion,
|
|
|
totals,
|
|
totals,
|
|
|
expireDiff,
|
|
expireDiff,
|