import { useState, useMemo } from 'react'; import { useNavigate } from 'react-router'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useTranslation } from 'react-i18next'; import { adminRemnawaveApi, NodeInfo, SquadWithLocalInfo, SystemStatsResponse, AutoSyncStatus, } from '../api/adminRemnawave'; import { usePlatform } from '../platform/hooks/usePlatform'; import { ServerIcon, ChartIcon, GlobeIcon, UsersIcon, SyncIcon, RefreshIcon, PlayIcon, StopIcon, ArrowPathIcon, RemnawaveIcon, } from '../components/icons'; // ============ Icons ============ const BackIcon = () => ( ); // ============ Helpers ============ const formatBytes = (bytes: number): string => { if (bytes === 0) return '0 B'; const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; }; const formatUptime = (seconds: number): string => { const days = Math.floor(seconds / 86400); const hours = Math.floor((seconds % 86400) / 3600); const minutes = Math.floor((seconds % 3600) / 60); if (days > 0) return `${days}d ${hours}h`; if (hours > 0) return `${hours}h ${minutes}m`; return `${minutes}m`; }; const getCountryFlag = (code: string | null | undefined): string => { if (!code) return '🌍'; const codeMap: Record = { RU: '🇷🇺', US: '🇺🇸', DE: '🇩🇪', NL: '🇳🇱', GB: '🇬🇧', UK: '🇬🇧', FR: '🇫🇷', FI: '🇫🇮', SE: '🇸🇪', NO: '🇳🇴', PL: '🇵🇱', TR: '🇹🇷', JP: '🇯🇵', SG: '🇸🇬', HK: '🇭🇰', KR: '🇰🇷', AU: '🇦🇺', CA: '🇨🇦', CH: '🇨🇭', AT: '🇦🇹', IT: '🇮🇹', ES: '🇪🇸', BR: '🇧🇷', IN: '🇮🇳', AE: '🇦🇪', IL: '🇮🇱', KZ: '🇰🇿', UA: '🇺🇦', CZ: '🇨🇿', RO: '🇷🇴', LV: '🇱🇻', LT: '🇱🇹', EE: '🇪🇪', BG: '🇧🇬', HU: '🇭🇺', MD: '🇲🇩', }; return codeMap[code.toUpperCase()] || code; }; // ============ Sub-Components ============ interface StatCardProps { label: string; value: string | number; icon: React.ReactNode; color?: string; subValue?: string; } function StatCard({ label, value, icon, color = 'accent', subValue }: StatCardProps) { const colorClasses: Record = { accent: 'bg-accent-500/20 text-accent-400', green: 'bg-success-500/20 text-success-400', blue: 'bg-accent-500/20 text-accent-400', orange: 'bg-warning-500/20 text-warning-400', red: 'bg-error-500/20 text-error-400', purple: 'bg-accent-500/20 text-accent-400', }; return (
{icon}

{label}

{value}

{subValue &&

{subValue}

}
); } interface NodeCardProps { node: NodeInfo; onAction: (uuid: string, action: 'enable' | 'disable' | 'restart') => void; isLoading?: boolean; } function NodeCard({ node, onAction, isLoading }: NodeCardProps) { const { t } = useTranslation(); const statusColor = node.is_disabled ? 'text-dark-500' : node.is_connected && node.is_node_online ? 'text-success-400' : 'text-error-400'; const statusText = node.is_disabled ? t('admin.remnawave.nodes.disabled', 'Disabled') : node.is_connected && node.is_node_online ? t('admin.remnawave.nodes.online', 'Online') : t('admin.remnawave.nodes.offline', 'Offline'); return (
{getCountryFlag(node.country_code)}

{node.name}

{statusText}

{node.address}

{t('admin.remnawave.nodes.usersOnlineCount', '{{count}} online', { count: node.users_online ?? 0, })} {node.traffic_used_bytes !== undefined && ( {formatBytes(node.traffic_used_bytes)}{' '} {t('admin.remnawave.nodes.trafficUsed', 'used')} )} {node.xray_uptime && ( {t('admin.remnawave.nodes.uptimeLabel', 'Uptime')}: {node.xray_uptime} )}
); } interface SquadCardProps { squad: SquadWithLocalInfo; onClick: () => void; } function SquadCard({ squad, onClick }: SquadCardProps) { const { t } = useTranslation(); return (
{getCountryFlag(squad.country_code)}

{squad.display_name || squad.name}

{squad.is_synced ? ( {t('admin.remnawave.squads.synced', 'Synced')} ) : ( {t('admin.remnawave.squads.notSynced', 'Not synced')} )}

{squad.name}

{t('admin.remnawave.squads.membersCount', '{{count}} members', { count: squad.members_count, })} {squad.current_users !== undefined && ( {squad.current_users} / {squad.max_users ?? '∞'} )} {t('admin.remnawave.squads.inboundsCount', '{{count}} inbounds', { count: squad.inbounds_count, })} {squad.is_available !== undefined && ( {squad.is_available ? `✓ ${t('admin.remnawave.squads.available', 'Available')}` : `✗ ${t('admin.remnawave.squads.unavailable', 'Unavailable')}`} )}
); } interface SyncCardProps { title: string; description: string; onAction: () => void; isLoading?: boolean; lastResult?: { success: boolean; message?: string } | null; } function SyncCard({ title, description, onAction, isLoading, lastResult }: SyncCardProps) { const { t } = useTranslation(); return (

{title}

{description}

{lastResult && (

{lastResult.message}

)}
); } // ============ Tab Components ============ interface OverviewTabProps { stats: SystemStatsResponse | undefined; isLoading: boolean; onRefresh: () => void; } function OverviewTab({ stats, isLoading, onRefresh }: OverviewTabProps) { const { t } = useTranslation(); if (isLoading) { return (
); } if (!stats) { return (

{t('admin.remnawave.noData', 'Failed to load data')}

); } const memoryUsedPercent = stats.server_info.memory_total > 0 ? Math.round((stats.server_info.memory_used / stats.server_info.memory_total) * 100) : 0; return (
{/* System Stats */}

{t('admin.remnawave.overview.system', 'System')}

} color="green" /> } color="blue" /> } color="purple" /> } color="orange" />
{/* Bandwidth */}

{t('admin.remnawave.overview.bandwidth', 'Realtime Bandwidth')}

↓} color="green" /> ↑} color="blue" /> ⇅} color="purple" />
{/* Server Info */}

{t('admin.remnawave.overview.server', 'Server')}

⚡} color="accent" /> 💾} color={memoryUsedPercent > 80 ? 'red' : memoryUsedPercent > 60 ? 'orange' : 'green'} /> ⏱️} color="blue" />
{/* Traffic Periods */}

{t('admin.remnawave.overview.traffic', 'Traffic Statistics')}

📊} color="accent" /> 📊} color="blue" /> 📊} color="green" /> 📊} color="purple" /> 📊} color="orange" />
{/* Users by Status */}

{t('admin.remnawave.overview.usersByStatus', 'Users by Status')}

{Object.entries(stats.users_by_status).map(([status, count]) => ( } color={status === 'ACTIVE' ? 'green' : status === 'DISABLED' ? 'red' : 'accent'} /> ))}
); } interface NodesTabProps { nodes: NodeInfo[]; isLoading: boolean; onRefresh: () => void; onAction: (uuid: string, action: 'enable' | 'disable' | 'restart') => void; onRestartAll: () => void; isActionLoading: boolean; } function NodesTab({ nodes, isLoading, onRefresh, onAction, onRestartAll, isActionLoading, }: NodesTabProps) { const { t } = useTranslation(); const stats = useMemo(() => { const total = nodes.length; const online = nodes.filter((n) => n.is_connected && n.is_node_online && !n.is_disabled).length; const offline = nodes.filter( (n) => (!n.is_connected || !n.is_node_online) && !n.is_disabled, ).length; const disabled = nodes.filter((n) => n.is_disabled).length; const totalUsers = nodes.reduce((acc, n) => acc + (n.users_online ?? 0), 0); return { total, online, offline, disabled, totalUsers }; }, [nodes]); if (isLoading) { return (
); } return (
{/* Stats */}
} color="accent" /> } color="green" /> } color="red" /> } color="accent" /> } color="blue" />
{/* Actions */}
{/* Nodes List */}
{nodes.length === 0 ? (

{t('admin.remnawave.nodes.noNodes', 'No nodes found')}

) : ( nodes.map((node) => ( )) )}
); } interface SquadsTabProps { squads: SquadWithLocalInfo[]; isLoading: boolean; onRefresh: () => void; onNavigate: (uuid: string) => void; onSync: () => void; isSyncing: boolean; } function SquadsTab({ squads, isLoading, onRefresh, onNavigate, onSync, isSyncing, }: SquadsTabProps) { const { t } = useTranslation(); const stats = useMemo(() => { const total = squads.length; const synced = squads.filter((s) => s.is_synced).length; const available = squads.filter((s) => s.is_available).length; const totalMembers = squads.reduce((acc, s) => acc + s.members_count, 0); return { total, synced, available, totalMembers }; }, [squads]); if (isLoading) { return (
); } return (
{/* Stats */}
} color="accent" /> } color="green" /> } color="blue" /> } color="purple" />
{/* Actions */}
{/* Squads List */}
{squads.length === 0 ? (

{t('admin.remnawave.squads.noSquads', 'No squads found')}

) : ( squads.map((squad) => ( onNavigate(squad.uuid)} /> )) )}
); } interface SyncTabProps { autoSyncStatus: AutoSyncStatus | undefined; isLoading: boolean; onRunAutoSync: () => void; onSyncFromPanel: () => void; onSyncToPanel: () => void; syncResults: Record; loadingStates: Record; } function SyncTab({ autoSyncStatus, isLoading, onRunAutoSync, onSyncFromPanel, onSyncToPanel, syncResults, loadingStates, }: SyncTabProps) { const { t } = useTranslation(); if (isLoading) { return (
); } return (
{/* Auto Sync Status */} {autoSyncStatus && (

{t('admin.remnawave.sync.autoSync', 'Auto Sync')}

{autoSyncStatus.enabled ? t('admin.remnawave.sync.enabled', 'Enabled') : t('admin.remnawave.sync.disabled', 'Disabled')}

{t('admin.remnawave.sync.schedule', 'Schedule')}

{autoSyncStatus.times.length > 0 ? autoSyncStatus.times.join(', ') : '—'}

{t('admin.remnawave.sync.status', 'Status')}

{autoSyncStatus.is_running ? t('admin.remnawave.sync.running', 'Running...') : autoSyncStatus.last_run_success ? t('admin.remnawave.sync.success', 'Success') : autoSyncStatus.last_run_error || '—'}

{t('admin.remnawave.sync.lastRun', 'Last Run')}

{autoSyncStatus.last_run_finished_at ? new Date(autoSyncStatus.last_run_finished_at).toLocaleString() : '—'}

{t('admin.remnawave.sync.nextRun', 'Next Run')}

{autoSyncStatus.next_run ? new Date(autoSyncStatus.next_run).toLocaleString() : '—'}

)} {/* Manual Sync */}
); } // ============ Main Component ============ type TabType = 'overview' | 'nodes' | 'squads' | 'sync'; export default function AdminRemnawave() { const { t } = useTranslation(); const navigate = useNavigate(); const queryClient = useQueryClient(); const { capabilities } = usePlatform(); // State const [activeTab, setActiveTab] = useState('overview'); const [syncResults, setSyncResults] = useState< Record >({}); const [loadingStates, setLoadingStates] = useState>({}); // Queries const { data: status } = useQuery({ queryKey: ['admin-remnawave-status'], queryFn: adminRemnawaveApi.getStatus, }); const { data: systemStats, isLoading: isLoadingStats, refetch: refetchStats, } = useQuery({ queryKey: ['admin-remnawave-system'], queryFn: adminRemnawaveApi.getSystemStats, enabled: activeTab === 'overview', refetchInterval: 30000, }); const { data: nodesData, isLoading: isLoadingNodes, refetch: refetchNodes, } = useQuery({ queryKey: ['admin-remnawave-nodes'], queryFn: adminRemnawaveApi.getNodes, enabled: activeTab === 'nodes', refetchInterval: 15000, }); const { data: squadsData, isLoading: isLoadingSquads, refetch: refetchSquads, } = useQuery({ queryKey: ['admin-remnawave-squads'], queryFn: adminRemnawaveApi.getSquads, enabled: activeTab === 'squads', }); const { data: autoSyncStatus, isLoading: isLoadingAutoSync } = useQuery({ queryKey: ['admin-remnawave-autosync'], queryFn: adminRemnawaveApi.getAutoSyncStatus, enabled: activeTab === 'sync', refetchInterval: 10000, }); // Mutations const nodeActionMutation = useMutation({ mutationFn: ({ uuid, action }: { uuid: string; action: 'enable' | 'disable' | 'restart' }) => adminRemnawaveApi.nodeAction(uuid, action), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['admin-remnawave-nodes'] }); }, }); const restartAllMutation = useMutation({ mutationFn: adminRemnawaveApi.restartAllNodes, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['admin-remnawave-nodes'] }); }, }); const syncServersMutation = useMutation({ mutationFn: adminRemnawaveApi.syncServers, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['admin-remnawave-squads'] }); }, }); // Handlers const handleNodeAction = (uuid: string, action: 'enable' | 'disable' | 'restart') => { nodeActionMutation.mutate({ uuid, action }); }; const handleRestartAll = () => { if ( confirm( t('admin.remnawave.nodes.confirmRestartAll', 'Are you sure you want to restart all nodes?'), ) ) { restartAllMutation.mutate(); } }; const handleSyncServers = () => { syncServersMutation.mutate(); }; const handleSyncAction = async ( key: string, action: () => Promise<{ success?: boolean; started?: boolean; message?: string }>, ) => { setLoadingStates((prev) => ({ ...prev, [key]: true })); try { const result = await action(); setSyncResults((prev) => ({ ...prev, [key]: { success: result.success ?? result.started ?? false, message: result.message }, })); } catch { setSyncResults((prev) => ({ ...prev, [key]: { success: false, message: 'Failed' } })); } finally { setLoadingStates((prev) => ({ ...prev, [key]: false })); } }; const tabs = [ { id: 'overview' as const, label: t('admin.remnawave.tabs.overview', 'Overview'), icon: , }, { id: 'nodes' as const, label: t('admin.remnawave.tabs.nodes', 'Nodes'), icon: }, { id: 'squads' as const, label: t('admin.remnawave.tabs.squads', 'Squads'), icon: , }, { id: 'sync' as const, label: t('admin.remnawave.tabs.sync', 'Sync'), icon: }, ]; const isConfigured = status?.is_configured; return (
{/* Header */}
{/* Show back button only on web, not in Telegram Mini App */} {!capabilities.hasBackButton && ( )}

{t('admin.remnawave.title', 'RemnaWave')}

{t('admin.remnawave.subtitle', 'Panel management and statistics')}

{/* Connection Status Badge */}
{isConfigured ? t('admin.remnawave.connected', 'Connected') : t('admin.remnawave.disconnected', 'Not configured')}
{/* Configuration Error */} {status?.configuration_error && (

{status.configuration_error}

)} {/* Tabs */}
{tabs.map((tab) => ( ))}
{/* Tab Content */} {activeTab === 'overview' && ( refetchStats()} /> )} {activeTab === 'nodes' && ( refetchNodes()} onAction={handleNodeAction} onRestartAll={handleRestartAll} isActionLoading={nodeActionMutation.isPending || restartAllMutation.isPending} /> )} {activeTab === 'squads' && ( refetchSquads()} onNavigate={(uuid) => navigate(`/admin/remnawave/squads/${uuid}`)} onSync={handleSyncServers} isSyncing={syncServersMutation.isPending} /> )} {activeTab === 'sync' && ( handleSyncAction('autoSync', adminRemnawaveApi.runAutoSync)} onSyncFromPanel={() => handleSyncAction('fromPanel', () => adminRemnawaveApi.syncFromPanel('all')) } onSyncToPanel={() => handleSyncAction('toPanel', adminRemnawaveApi.syncToPanel)} syncResults={syncResults} loadingStates={loadingStates} /> )}
); }