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="p-4 bg-dark-800/50 rounded-xl border border-dark-700 hover:border-dark-600 transition-colors cursor-pointer" >
{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>({}) 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 (error) { 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 */}
)}
) }