diff --git a/src/api/adminRemnawave.ts b/src/api/adminRemnawave.ts index fb2c41a..dad19cf 100644 --- a/src/api/adminRemnawave.ts +++ b/src/api/adminRemnawave.ts @@ -159,6 +159,25 @@ export interface NodeActionResponse { is_disabled?: boolean; } +// Realtime Traffic +export interface InboundTraffic { + tag: string; + downloadBytes: number; + uploadBytes: number; + totalBytes: number; +} + +export interface NodeRealtimeStats { + nodeUuid: string; + nodeName: string; + downloadBytes: number; + uploadBytes: number; + totalBytes: number; + usersOnline: number; + inbounds: InboundTraffic[]; + outbounds: InboundTraffic[]; +} + // Squads export interface SquadWithLocalInfo { uuid: string; @@ -282,7 +301,7 @@ export const adminRemnawaveApi = { return response.data; }, - getNodesRealtime: async (): Promise[]> => { + getNodesRealtime: async (): Promise => { const response = await apiClient.get('/cabinet/admin/remnawave/nodes/realtime'); return response.data; }, diff --git a/src/pages/AdminRemnawave.tsx b/src/pages/AdminRemnawave.tsx index 9e742cf..13f9b0e 100644 --- a/src/pages/AdminRemnawave.tsx +++ b/src/pages/AdminRemnawave.tsx @@ -5,6 +5,7 @@ import { useTranslation } from 'react-i18next'; import { adminRemnawaveApi, NodeInfo, + NodeRealtimeStats, SquadWithLocalInfo, SystemStatsResponse, AutoSyncStatus, @@ -851,7 +852,133 @@ function SyncTab({ ); } -type TabType = 'overview' | 'nodes' | 'squads' | 'sync'; +interface TrafficTabProps { + data: NodeRealtimeStats[] | undefined; + isLoading: boolean; + onRefresh: () => void; +} + +function TrafficTab({ data, isLoading, onRefresh }: TrafficTabProps) { + const { t } = useTranslation(); + + if (isLoading) { + return ( +
+
+
+ ); + } + + if (!data || data.length === 0) { + return ( +
+

+ {t('admin.remnawave.traffic.noData', 'No traffic data available')} +

+ +
+ ); + } + + const totalDownload = data.reduce((acc, n) => acc + n.downloadBytes, 0); + const totalUpload = data.reduce((acc, n) => acc + n.uploadBytes, 0); + + return ( +
+ {/* Totals */} +
+ ↓} + color="green" + /> + ↑} + color="blue" + /> + ⇅} + color="purple" + /> +
+ + {/* Per-node inbound breakdown */} + {data.map((node) => ( +
+
+
+ +

{node.nodeName}

+ + {node.usersOnline} {t('admin.remnawave.traffic.online', 'online')} + +
+ {formatBytes(node.totalBytes)} +
+ + {node.inbounds.length > 0 && ( +
+

+ {t('admin.remnawave.traffic.inbounds', 'Inbounds')} +

+ {node.inbounds + .sort((a, b) => b.totalBytes - a.totalBytes) + .map((ib) => ( +
+ {ib.tag} +
+ ↓ {formatBytes(ib.downloadBytes)} + ↑ {formatBytes(ib.uploadBytes)} + + {formatBytes(ib.totalBytes)} + +
+
+ ))} +
+ )} + + {node.outbounds.length > 0 && ( +
+

+ {t('admin.remnawave.traffic.outbounds', 'Outbounds')} +

+ {node.outbounds + .sort((a, b) => b.totalBytes - a.totalBytes) + .map((ob) => ( +
+ {ob.tag} +
+ ↓ {formatBytes(ob.downloadBytes)} + ↑ {formatBytes(ob.uploadBytes)} + + {formatBytes(ob.totalBytes)} + +
+
+ ))} +
+ )} +
+ ))} +
+ ); +} + +type TabType = 'overview' | 'nodes' | 'traffic' | 'squads' | 'sync'; export default function AdminRemnawave() { const { t } = useTranslation(); @@ -904,6 +1031,17 @@ export default function AdminRemnawave() { enabled: activeTab === 'squads', }); + const { + data: realtimeData, + isLoading: isLoadingRealtime, + refetch: refetchRealtime, + } = useQuery({ + queryKey: ['admin-remnawave-realtime'], + queryFn: adminRemnawaveApi.getNodesRealtime, + enabled: activeTab === 'traffic', + refetchInterval: 10000, + }); + const { data: autoSyncStatus, isLoading: isLoadingAutoSync } = useQuery({ queryKey: ['admin-remnawave-autosync'], queryFn: adminRemnawaveApi.getAutoSyncStatus, @@ -978,6 +1116,11 @@ export default function AdminRemnawave() { icon: , }, { id: 'nodes' as const, label: t('admin.remnawave.tabs.nodes', 'Nodes'), icon: }, + { + id: 'traffic' as const, + label: t('admin.remnawave.tabs.traffic', 'Traffic'), + icon: , + }, { id: 'squads' as const, label: t('admin.remnawave.tabs.squads', 'Squads'), @@ -1075,6 +1218,14 @@ export default function AdminRemnawave() { /> )} + {activeTab === 'traffic' && ( + refetchRealtime()} + /> + )} + {activeTab === 'squads' && (