import { useState, useMemo } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useTranslation } from 'react-i18next'; import { Link } from 'react-router-dom'; import { adminRemnawaveApi, NodeInfo, SquadWithLocalInfo, SystemStatsResponse, AutoSyncStatus, } from '../api/adminRemnawave'; // ============ Icons ============ const ServerIcon = ({ className = 'w-6 h-6' }: { className?: string }) => ( ); const ChartIcon = ({ className = 'w-5 h-5' }: { className?: string }) => ( ); const GlobeIcon = ({ className = 'w-5 h-5' }: { className?: string }) => ( ); const UsersIcon = ({ className = 'w-5 h-5' }: { className?: string }) => ( ); const SyncIcon = ({ className = 'w-5 h-5' }: { className?: string }) => ( ); const RefreshIcon = ({ className = 'w-4 h-4', spinning = false, }: { className?: string; spinning?: boolean; }) => ( ); const PlayIcon = ({ className = 'w-4 h-4' }: { className?: string }) => ( ); const StopIcon = ({ className = 'w-4 h-4' }: { className?: string }) => ( ); const ArrowPathIcon = ({ className = 'w-4 h-4' }: { className?: string }) => ( ); const ChevronLeftIcon = () => ( ); // ============ 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-emerald-500/20 text-emerald-400', blue: 'bg-blue-500/20 text-blue-400', orange: 'bg-orange-500/20 text-orange-400', red: 'bg-red-500/20 text-red-400', purple: 'bg-purple-500/20 text-purple-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-emerald-400' : 'text-red-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}

{node.users_online ?? 0} online {node.traffic_used_bytes !== undefined && ( {formatBytes(node.traffic_used_bytes)} used )} {node.xray_uptime && Uptime: {node.xray_uptime}}
); } interface SquadCardProps { squad: SquadWithLocalInfo; onSelect: (squad: SquadWithLocalInfo) => void; } function SquadCard({ squad, onSelect }: SquadCardProps) { const { t } = useTranslation(); return (
onSelect(squad)} className="cursor-pointer rounded-xl border border-dark-700 bg-dark-800/50 p-4 transition-colors hover:border-dark-600" >
{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}

{squad.members_count} members {squad.current_users !== undefined && ( {squad.current_users} / {squad.max_users ?? '∞'} )} {squad.inbounds_count} inbounds {squad.is_available !== undefined && ( {squad.is_available ? '✓ Available' : '✗ 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) { 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; onSelect: (squad: SquadWithLocalInfo) => void; onSync: () => void; isSyncing: boolean; } function SquadsTab({ squads, isLoading, onRefresh, onSelect, 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) => ) )}
); } interface SyncTabProps { autoSyncStatus: AutoSyncStatus | undefined; isLoading: boolean; onRunAutoSync: () => void; onSyncFromPanel: () => void; onSyncToPanel: () => void; onValidate: () => void; onCleanup: () => void; onSyncStatuses: () => void; syncResults: Record; loadingStates: Record; } function SyncTab({ autoSyncStatus, isLoading, onRunAutoSync, onSyncFromPanel, onSyncToPanel, onValidate, onCleanup, onSyncStatuses, syncResults, loadingStates, }: SyncTabProps) { const { t } = useTranslation(); if (isLoading) { return (
); } return (
{/* Auto Sync Status */} {autoSyncStatus && (

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

{autoSyncStatus.enabled ? 'Enabled' : 'Disabled'}

Schedule

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

Next Run

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

Last Run

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

Status

{autoSyncStatus.is_running ? 'Running...' : autoSyncStatus.last_run_success ? 'Success' : autoSyncStatus.last_run_error || '—'}

)} {/* Manual Sync Options */}

{t('admin.remnawave.sync.manual', 'Manual Sync')}

{/* Subscriptions */}

{t('admin.remnawave.sync.subscriptions', 'Subscriptions')}

); } // ============ Main Component ============ type TabType = 'overview' | 'nodes' | 'squads' | 'sync'; export default function AdminRemnawave() { const { t } = useTranslation(); const queryClient = useQueryClient(); // State const [activeTab, setActiveTab] = useState('overview'); const [selectedSquad, setSelectedSquad] = useState(null); 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 */}

{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()} onSelect={setSelectedSquad} onSync={handleSyncServers} isSyncing={syncServersMutation.isPending} /> )} {activeTab === 'sync' && ( handleSyncAction('autoSync', adminRemnawaveApi.runAutoSync)} onSyncFromPanel={() => handleSyncAction('fromPanel', () => adminRemnawaveApi.syncFromPanel('all')) } onSyncToPanel={() => handleSyncAction('toPanel', adminRemnawaveApi.syncToPanel)} onValidate={() => handleSyncAction('validate', adminRemnawaveApi.validateSubscriptions)} onCleanup={() => handleSyncAction('cleanup', adminRemnawaveApi.cleanupSubscriptions)} onSyncStatuses={() => handleSyncAction('statuses', adminRemnawaveApi.syncSubscriptionStatuses) } syncResults={syncResults} loadingStates={loadingStates} /> )} {/* Squad Detail Modal */} {selectedSquad && (
setSelectedSquad(null)} >
e.stopPropagation()} > {/* Header */}
{getCountryFlag(selectedSquad.country_code)}

{selectedSquad.display_name || selectedSquad.name}

{/* Content */}

UUID

{selectedSquad.uuid}

Original Name

{selectedSquad.name}

Members

{selectedSquad.members_count}

Inbounds

{selectedSquad.inbounds_count}

{selectedSquad.is_synced && ( <>

Price

{((selectedSquad.price_kopeks ?? 0) / 100).toFixed(2)} ₽

Users

{selectedSquad.current_users ?? 0} / {selectedSquad.max_users ?? '∞'}

Available

{selectedSquad.is_available ? 'Yes' : 'No'}

Trial Eligible

{selectedSquad.is_trial_eligible ? 'Yes' : 'No'}

)}
{/* Inbounds */} {selectedSquad.inbounds.length > 0 && (

Inbounds

{selectedSquad.inbounds.map((inbound: Record, idx) => (
{String(inbound.tag || inbound.uuid || `Inbound ${idx + 1}`)}
))}
)}
{/* Footer */}
)}
); }