|
@@ -9,7 +9,7 @@ import { isSSMultiUser } from '@/lib/xray/protocol-capabilities';
|
|
|
import { setDatepicker } from '@/hooks/useDatepicker';
|
|
import { setDatepicker } from '@/hooks/useDatepicker';
|
|
|
import { keys } from '@/api/queryKeys';
|
|
import { keys } from '@/api/queryKeys';
|
|
|
import { SlimInboundListSchema, LastOnlineMapSchema, InboundDetailSchema } from '@/schemas/inbound';
|
|
import { SlimInboundListSchema, LastOnlineMapSchema, InboundDetailSchema } from '@/schemas/inbound';
|
|
|
-import { OnlinesSchema } from '@/schemas/client';
|
|
|
|
|
|
|
+import { OnlinesSchema, OnlineByNodeSchema } from '@/schemas/client';
|
|
|
import { DefaultsPayloadSchema, type DefaultsPayload } from '@/schemas/defaults';
|
|
import { DefaultsPayloadSchema, type DefaultsPayload } from '@/schemas/defaults';
|
|
|
|
|
|
|
|
export interface SubSettings {
|
|
export interface SubSettings {
|
|
@@ -54,6 +54,25 @@ async function fetchOnlineClients(): Promise<string[]> {
|
|
|
return Array.isArray(validated.obj) ? validated.obj : [];
|
|
return Array.isArray(validated.obj) ? validated.obj : [];
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+// Online emails grouped by node id (local panel = key 0), used to scope the
|
|
|
|
|
+// per-inbound online rollup so a client online on one node is not shown
|
|
|
|
|
+// online on every node's inbounds.
|
|
|
|
|
+async function fetchOnlineClientsByNode(): Promise<Record<string, string[]>> {
|
|
|
|
|
+ const msg = await HttpUtil.post('/panel/api/clients/onlinesByNode', undefined, { silent: true });
|
|
|
|
|
+ if (!msg?.success) throw new Error(msg?.msg || 'Failed to fetch onlinesByNode');
|
|
|
|
|
+ const validated = parseMsg(msg, OnlineByNodeSchema, 'clients/onlinesByNode');
|
|
|
|
|
+ return (validated.obj && typeof validated.obj === 'object') ? (validated.obj as Record<string, string[]>) : {};
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function toNodeOnlineMap(data: Record<string, string[]>): Map<number, Set<string>> {
|
|
|
|
|
+ const map = new Map<number, Set<string>>();
|
|
|
|
|
+ for (const [key, emails] of Object.entries(data)) {
|
|
|
|
|
+ if (!Array.isArray(emails)) continue;
|
|
|
|
|
+ map.set(Number(key), new Set(emails));
|
|
|
|
|
+ }
|
|
|
|
|
+ return map;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
async function fetchLastOnlineMap(): Promise<Record<string, number>> {
|
|
async function fetchLastOnlineMap(): Promise<Record<string, number>> {
|
|
|
const msg = await HttpUtil.post('/panel/api/clients/lastOnline', undefined, { silent: true });
|
|
const msg = await HttpUtil.post('/panel/api/clients/lastOnline', undefined, { silent: true });
|
|
|
if (!msg?.success) throw new Error(msg?.msg || 'Failed to fetch lastOnline');
|
|
if (!msg?.success) throw new Error(msg?.msg || 'Failed to fetch lastOnline');
|
|
@@ -83,6 +102,12 @@ export function useInbounds() {
|
|
|
staleTime: Infinity,
|
|
staleTime: Infinity,
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
|
|
+ const onlinesByNodeQuery = useQuery({
|
|
|
|
|
+ queryKey: keys.clients.onlinesByNode(),
|
|
|
|
|
+ queryFn: fetchOnlineClientsByNode,
|
|
|
|
|
+ staleTime: Infinity,
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
const lastOnlineQuery = useQuery({
|
|
const lastOnlineQuery = useQuery({
|
|
|
queryKey: keys.clients.lastOnline(),
|
|
queryKey: keys.clients.lastOnline(),
|
|
|
queryFn: fetchLastOnlineMap,
|
|
queryFn: fetchLastOnlineMap,
|
|
@@ -135,6 +160,10 @@ export function useInbounds() {
|
|
|
const onlineClientsRef = useRef<string[]>([]);
|
|
const onlineClientsRef = useRef<string[]>([]);
|
|
|
onlineClientsRef.current = onlineClients;
|
|
onlineClientsRef.current = onlineClients;
|
|
|
|
|
|
|
|
|
|
+ // Online emails keyed by node id (local inbounds = key 0). The rollup
|
|
|
|
|
+ // reads this so each inbound only counts clients online on its own node.
|
|
|
|
|
+ const onlineByNodeRef = useRef<Map<number, Set<string>>>(new Map());
|
|
|
|
|
+
|
|
|
const [lastOnlineMap, setLastOnlineMap] = useState<Record<string, number>>({});
|
|
const [lastOnlineMap, setLastOnlineMap] = useState<Record<string, number>>({});
|
|
|
|
|
|
|
|
const rollupClients = useCallback(
|
|
const rollupClients = useCallback(
|
|
@@ -151,12 +180,14 @@ export function useInbounds() {
|
|
|
const comments = new Map<string, string>();
|
|
const comments = new Map<string, string>();
|
|
|
const now = Date.now();
|
|
const now = Date.now();
|
|
|
|
|
|
|
|
|
|
+ const nodeOnline = onlineByNodeRef.current.get(dbInbound.nodeId ?? 0);
|
|
|
|
|
+
|
|
|
if (dbInbound.enable) {
|
|
if (dbInbound.enable) {
|
|
|
for (const client of clients) {
|
|
for (const client of clients) {
|
|
|
if (client.comment && client.email) comments.set(client.email, client.comment);
|
|
if (client.comment && client.email) comments.set(client.email, client.comment);
|
|
|
if (client.enable) {
|
|
if (client.enable) {
|
|
|
if (client.email) active.push(client.email);
|
|
if (client.email) active.push(client.email);
|
|
|
- if (client.email && onlineClientsRef.current.includes(client.email)) online.push(client.email);
|
|
|
|
|
|
|
+ if (client.email && nodeOnline?.has(client.email)) online.push(client.email);
|
|
|
} else if (client.email) {
|
|
} else if (client.email) {
|
|
|
deactive.push(client.email);
|
|
deactive.push(client.email);
|
|
|
}
|
|
}
|
|
@@ -237,6 +268,13 @@ export function useInbounds() {
|
|
|
}
|
|
}
|
|
|
}, [onlinesQuery.data]);
|
|
}, [onlinesQuery.data]);
|
|
|
|
|
|
|
|
|
|
+ useEffect(() => {
|
|
|
|
|
+ if (onlinesByNodeQuery.data) {
|
|
|
|
|
+ onlineByNodeRef.current = toNodeOnlineMap(onlinesByNodeQuery.data);
|
|
|
|
|
+ rebuildClientCount();
|
|
|
|
|
+ }
|
|
|
|
|
+ }, [onlinesByNodeQuery.data, rebuildClientCount]);
|
|
|
|
|
+
|
|
|
useEffect(() => {
|
|
useEffect(() => {
|
|
|
if (lastOnlineQuery.data) setLastOnlineMap(lastOnlineQuery.data);
|
|
if (lastOnlineQuery.data) setLastOnlineMap(lastOnlineQuery.data);
|
|
|
}, [lastOnlineQuery.data]);
|
|
}, [lastOnlineQuery.data]);
|
|
@@ -255,6 +293,7 @@ export function useInbounds() {
|
|
|
await Promise.all([
|
|
await Promise.all([
|
|
|
queryClient.invalidateQueries({ queryKey: keys.inbounds.root() }),
|
|
queryClient.invalidateQueries({ queryKey: keys.inbounds.root() }),
|
|
|
queryClient.invalidateQueries({ queryKey: keys.clients.onlines() }),
|
|
queryClient.invalidateQueries({ queryKey: keys.clients.onlines() }),
|
|
|
|
|
+ queryClient.invalidateQueries({ queryKey: keys.clients.onlinesByNode() }),
|
|
|
queryClient.invalidateQueries({ queryKey: keys.clients.lastOnline() }),
|
|
queryClient.invalidateQueries({ queryKey: keys.clients.lastOnline() }),
|
|
|
queryClient.invalidateQueries({ queryKey: keys.xray.config() }),
|
|
queryClient.invalidateQueries({ queryKey: keys.xray.config() }),
|
|
|
]);
|
|
]);
|
|
@@ -284,11 +323,14 @@ 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[]; lastOnlineMap?: Record<string, number> };
|
|
|
|
|
|
|
+ const p = payload as { onlineClients?: string[]; onlineByNode?: 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);
|
|
|
}
|
|
}
|
|
|
|
|
+ if (p.onlineByNode && typeof p.onlineByNode === 'object') {
|
|
|
|
|
+ onlineByNodeRef.current = toNodeOnlineMap(p.onlineByNode);
|
|
|
|
|
+ }
|
|
|
if (p.lastOnlineMap && typeof p.lastOnlineMap === 'object') {
|
|
if (p.lastOnlineMap && typeof p.lastOnlineMap === 'object') {
|
|
|
setLastOnlineMap((prev) => ({ ...prev, ...p.lastOnlineMap! }));
|
|
setLastOnlineMap((prev) => ({ ...prev, ...p.lastOnlineMap! }));
|
|
|
}
|
|
}
|