+ {/* 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 (
+
+ {/* 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 (
+
+ {/* 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