import { useState, useEffect, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { useQuery } from '@tanstack/react-query'; import { AdminBackButton } from '../components/admin/AdminBackButton'; import { useFocusTrap } from '../hooks/useFocusTrap'; import { banSystemApi, type BanSystemStatus, type BanSystemStats, type BanUsersListResponse, type BanUserDetailResponse, type BanPunishmentsListResponse, type BanNodesListResponse, type BanAgentsListResponse, type BanTrafficViolationsResponse, type BanSettingsResponse, type BanSettingDefinition, type BanTrafficResponse, type BanReportResponse, type BanHealthResponse, } from '../api/banSystem'; import { ShieldIcon, UsersIcon, BanIcon, ServerIcon, AgentIcon, WarningIcon, RefreshIcon, ChartIcon, SearchIcon, SettingsIcon, TrafficIcon, ReportIcon, HealthIcon, ExclamationIcon, BackIcon, XIcon, } from '@/components/icons'; type TabType = | 'dashboard' | 'users' | 'punishments' | 'nodes' | 'agents' | 'violations' | 'settings' | 'traffic' | 'reports' | 'health'; interface StatCardProps { title: string; value: string | number; subtitle?: string; icon: React.ReactNode; color: 'accent' | 'success' | 'warning' | 'error' | 'info'; } function StatCard({ title, value, subtitle, icon, color }: StatCardProps) { const colorClasses = { accent: 'bg-accent-500/20 text-accent-400', success: 'bg-success-500/20 text-success-400', warning: 'bg-warning-500/20 text-warning-400', error: 'bg-error-500/20 text-error-400', info: 'bg-info-500/20 text-info-400', }; return (
{icon}
{value}
{title}
{subtitle &&
{subtitle}
}
); } export default function AdminBanSystem() { const { t } = useTranslation(); const [activeTab, setActiveTab] = useState('dashboard'); const [status, setStatus] = useState(null); const [stats, setStats] = useState(null); const [users, setUsers] = useState(null); const [selectedUser, setSelectedUser] = useState(null); const userDetailRef = useFocusTrap(selectedUser !== null, { onEscape: () => setSelectedUser(null), }); const [punishments, setPunishments] = useState(null); const [nodes, setNodes] = useState(null); const [agents, setAgents] = useState(null); const [violations, setViolations] = useState(null); const [settings, setSettings] = useState(null); const [traffic, setTraffic] = useState(null); const [report, setReport] = useState(null); const [health, setHealth] = useState(null); const [reportHours, setReportHours] = useState(24); const [settingLoading, setSettingLoading] = useState(null); const [loading, setLoading] = useState(true); // Format snake_case to readable label const formatSettingKey = useCallback( (key: string): string => { // Try translation first const translated = t(`banSystem.settings.${key}`, { defaultValue: '' }); if (translated && translated !== `banSystem.settings.${key}`) { return translated; } // Fallback: convert snake_case to Title Case return key .split('_') .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) .join(' '); }, [t], ); const formatCategory = useCallback( (category: string): string => { const translated = t(`banSystem.settings.categories.${category}`, { defaultValue: '' }); if (translated && translated !== `banSystem.settings.categories.${category}`) { return translated; } return category.charAt(0).toUpperCase() + category.slice(1).replace(/_/g, ' '); }, [t], ); const [error, setError] = useState(null); const [searchQuery, setSearchQuery] = useState(''); const [actionLoading, setActionLoading] = useState(null); // React Query: status once at mount; each tab fetches lazily via `enabled`. // Caching means switching tabs returns to cached data instantly (with background revalidate). const statusQuery = useQuery({ queryKey: ['ban-status'] as const, queryFn: () => banSystemApi.getStatus(), }); const isReady = !!(status?.enabled && status?.configured); const dashboardQuery = useQuery({ queryKey: ['ban-stats'] as const, queryFn: () => banSystemApi.getStats(), enabled: isReady && activeTab === 'dashboard', }); const usersQuery = useQuery({ queryKey: ['ban-users'] as const, queryFn: () => banSystemApi.getUsers({ limit: 50 }), enabled: isReady && activeTab === 'users', }); const punishmentsQuery = useQuery({ queryKey: ['ban-punishments'] as const, queryFn: () => banSystemApi.getPunishments(), enabled: isReady && activeTab === 'punishments', }); const nodesQuery = useQuery({ queryKey: ['ban-nodes'] as const, queryFn: () => banSystemApi.getNodes(), enabled: isReady && activeTab === 'nodes', }); const agentsQuery = useQuery({ queryKey: ['ban-agents'] as const, queryFn: () => banSystemApi.getAgents(), enabled: isReady && activeTab === 'agents', }); const violationsQuery = useQuery({ queryKey: ['ban-violations'] as const, queryFn: () => banSystemApi.getTrafficViolations(), enabled: isReady && activeTab === 'violations', }); const settingsQuery = useQuery({ queryKey: ['ban-settings'] as const, queryFn: () => banSystemApi.getSettings(), enabled: isReady && activeTab === 'settings', }); const trafficQuery = useQuery({ queryKey: ['ban-traffic'] as const, queryFn: () => banSystemApi.getTraffic(), enabled: isReady && activeTab === 'traffic', }); const reportsQuery = useQuery({ queryKey: ['ban-report', reportHours] as const, queryFn: () => banSystemApi.getReport(reportHours), enabled: isReady && activeTab === 'reports', }); const healthQuery = useQuery({ queryKey: ['ban-health'] as const, queryFn: () => banSystemApi.getHealth(), enabled: isReady && activeTab === 'health', }); // Sync query data into the existing state vars so the JSX + handlers stay unchanged // (handleSearch overrides `users` with search results; useEffect re-syncs on next refetch). useEffect(() => { if (statusQuery.data) { setStatus(statusQuery.data); if (!statusQuery.data.enabled || !statusQuery.data.configured) { setError(t('banSystem.notConfigured')); } } if (statusQuery.isError) setError(t('banSystem.loadError')); }, [statusQuery.data, statusQuery.isError, t]); useEffect(() => { if (dashboardQuery.data) setStats(dashboardQuery.data); }, [dashboardQuery.data]); useEffect(() => { if (usersQuery.data) setUsers(usersQuery.data); }, [usersQuery.data]); useEffect(() => { if (punishmentsQuery.data) setPunishments(punishmentsQuery.data); }, [punishmentsQuery.data]); useEffect(() => { if (nodesQuery.data) setNodes(nodesQuery.data); }, [nodesQuery.data]); useEffect(() => { if (agentsQuery.data) setAgents(agentsQuery.data); }, [agentsQuery.data]); useEffect(() => { if (violationsQuery.data) setViolations(violationsQuery.data); }, [violationsQuery.data]); useEffect(() => { if (settingsQuery.data) setSettings(settingsQuery.data); }, [settingsQuery.data]); useEffect(() => { if (trafficQuery.data) setTraffic(trafficQuery.data); }, [trafficQuery.data]); useEffect(() => { if (reportsQuery.data) setReport(reportsQuery.data); }, [reportsQuery.data]); useEffect(() => { if (healthQuery.data) setHealth(healthQuery.data); }, [healthQuery.data]); // Map activeTab → its query (used for `loading` derivation and refetchActiveTab below). const activeTabQuery = activeTab === 'dashboard' ? dashboardQuery : activeTab === 'users' ? usersQuery : activeTab === 'punishments' ? punishmentsQuery : activeTab === 'nodes' ? nodesQuery : activeTab === 'agents' ? agentsQuery : activeTab === 'violations' ? violationsQuery : activeTab === 'settings' ? settingsQuery : activeTab === 'traffic' ? trafficQuery : activeTab === 'reports' ? reportsQuery : healthQuery; // Derive `loading` from status + active tab query. useEffect(() => { setLoading(statusQuery.isLoading || activeTabQuery.isFetching); if (activeTabQuery.isError) setError(t('banSystem.loadError')); }, [statusQuery.isLoading, activeTabQuery.isFetching, activeTabQuery.isError, t]); const refetchActiveTab = useCallback(() => { void activeTabQuery.refetch(); }, [activeTabQuery]); const handleSearch = async () => { if (!searchQuery.trim()) { void usersQuery.refetch(); return; } try { setLoading(true); const data = await banSystemApi.searchUsers(searchQuery); setUsers(data); } catch { setError(t('banSystem.loadError')); } finally { setLoading(false); } }; const handleViewUser = async (email: string) => { try { setActionLoading(email); const data = await banSystemApi.getUser(email); setSelectedUser(data); } catch { setError(t('banSystem.loadError')); } finally { setActionLoading(null); } }; const handleUnban = async (userId: string) => { try { setActionLoading(userId); await banSystemApi.unbanUser(userId); void punishmentsQuery.refetch(); } catch { setError(t('banSystem.loadError')); } finally { setActionLoading(null); } }; const handleToggleSetting = async (key: string) => { try { setSettingLoading(key); await banSystemApi.toggleSetting(key); void settingsQuery.refetch(); } catch { setError(t('banSystem.loadError')); } finally { setSettingLoading(null); } }; const handleSetSetting = async (key: string, value: string) => { try { setSettingLoading(key); await banSystemApi.setSetting(key, value); void settingsQuery.refetch(); } catch { setError(t('banSystem.loadError')); } finally { setSettingLoading(null); } }; const handleReportPeriodChange = (hours: number) => { setReportHours(hours); }; // (reports query auto-refetches when reportHours changes — it's in the queryKey) const formatBytes = (bytes: number) => { if (bytes === 0) return '0 B'; const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB']; const i = Math.min(Math.floor(Math.log(bytes) / Math.log(k)), sizes.length - 1); return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`; }; const formatUptime = (seconds: number | null) => { if (!seconds) return '-'; const hours = Math.floor(seconds / 3600); const minutes = Math.floor((seconds % 3600) / 60); if (hours > 24) { const days = Math.floor(hours / 24); return `${days}d ${hours % 24}h`; } return `${hours}h ${minutes}m`; }; const formatDate = (dateStr: string | null) => { if (!dateStr) return '-'; return new Date(dateStr).toLocaleString(); }; const tabs = [ { id: 'dashboard' as TabType, label: t('banSystem.tabs.dashboard'), icon: , }, { id: 'users' as TabType, label: t('banSystem.tabs.users'), icon: }, { id: 'punishments' as TabType, label: t('banSystem.tabs.punishments'), icon: }, { id: 'nodes' as TabType, label: t('banSystem.tabs.nodes'), icon: }, { id: 'agents' as TabType, label: t('banSystem.tabs.agents'), icon: }, { id: 'violations' as TabType, label: t('banSystem.tabs.violations'), icon: }, { id: 'traffic' as TabType, label: t('banSystem.tabs.traffic'), icon: }, { id: 'reports' as TabType, label: t('banSystem.tabs.reports'), icon: }, { id: 'settings' as TabType, label: t('banSystem.tabs.settings'), icon: }, { id: 'health' as TabType, label: t('banSystem.tabs.health'), icon: }, ]; if (loading && !status) { return (
); } if (error && !status?.enabled) { return (
{/* Card */}
{/* Icon */}
{/* Title */}

{t('banSystem.title')}

{/* Error message */}

{error}

{/* Hint */}

{t('banSystem.configureHint')}

{/* Buttons */}
{/* Telegram Button */} {t('banSystem.contactTelegram')} {/* Back Button */}
{/* Decorative elements */}
); } return (
{/* Header */}

{t('banSystem.title')}

{t('banSystem.subtitle')}

{/* Tabs */}
{tabs.map((tab) => ( ))}
{/* Content */} {loading ? (
) : error ? (
{error}
) : ( <> {/* Dashboard Tab */} {activeTab === 'dashboard' && stats && (
} color="success" /> } color="warning" /> } color="error" /> } color="accent" /> } color="info" /> } color="accent" /> } color={stats.panel_connected ? 'success' : 'error'} /> } color="info" />
)} {/* Users Tab */} {activeTab === 'users' && (
{/* Search */}
setSearchQuery(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && handleSearch()} placeholder={t('banSystem.users.searchPlaceholder')} className="input pl-10" />
{/* Users Table */}
{users?.users.map((user) => ( ))}
{t('banSystem.users.email')} {t('banSystem.users.ipCount')} {t('banSystem.users.limit')} {t('banSystem.users.status')} {t('banSystem.users.bans')} {t('common.actions')}
{user.email} {user.unique_ip_count} {user.limit ?? '-'} {user.is_over_limit ? t('banSystem.users.overLimit') : t('banSystem.users.ok')} {user.blocked_count}
{(!users?.users || users.users.length === 0) && (
{t('common.noData')}
)}
)} {/* Punishments Tab */} {activeTab === 'punishments' && (
{punishments?.punishments.map((p) => ( ))}
{t('banSystem.punishments.user')} {t('banSystem.punishments.reason')} {t('banSystem.punishments.ipCount')} {t('banSystem.punishments.limit')} {t('banSystem.punishments.bannedAt')} {t('banSystem.punishments.enableAt')} {t('common.actions')}
{p.username}
{p.user_id}
{p.reason || '-'} {p.ip_count} {p.limit} {formatDate(p.punished_at)} {formatDate(p.enable_at)}
{(!punishments?.punishments || punishments.punishments.length === 0) && (
{t('banSystem.punishments.noBans')}
)}
)} {/* Nodes Tab */} {activeTab === 'nodes' && (
{nodes?.nodes.map((node) => (
{node.name}
{node.address || '-'}
{t('banSystem.nodes.status')}
{node.is_connected ? t('banSystem.nodes.online') : t('banSystem.nodes.offline')}
{t('banSystem.nodes.users')}
{node.users_count}
))} {(!nodes?.nodes || nodes.nodes.length === 0) && (
{t('banSystem.nodes.noNodes')}
)}
)} {/* Agents Tab */} {activeTab === 'agents' && (
{/* Summary */} {agents?.summary && (
} color="success" /> } color="accent" /> } color="warning" /> } color="info" />
)} {/* Agents List */}
{agents?.agents.map((agent) => ( ))}
{t('banSystem.agents.node')} {t('banSystem.agents.status')} {t('banSystem.agents.health')} {t('banSystem.agents.sent')} {t('banSystem.agents.dropped')} {t('banSystem.agents.queue')}
{agent.node_name} {agent.is_online ? t('banSystem.agents.online') : t('banSystem.agents.offline')} {agent.health} {agent.sent_total.toLocaleString()} {agent.dropped_total.toLocaleString()} {agent.queue_size}/{agent.queue_max}
{(!agents?.agents || agents.agents.length === 0) && (
{t('banSystem.agents.noAgents')}
)}
)} {/* Violations Tab */} {activeTab === 'violations' && (
{violations?.violations.map((v, idx) => ( ))}
{t('banSystem.violations.user')} {t('banSystem.violations.type')} {t('banSystem.violations.description')} {t('banSystem.violations.detectedAt')} {t('banSystem.violations.status')}
{v.username}
{v.email || '-'}
{v.violation_type} {v.description || '-'} {formatDate(v.detected_at)} {v.resolved ? t('banSystem.violations.resolved') : t('banSystem.violations.active')}
{(!violations?.violations || violations.violations.length === 0) && (
{t('banSystem.violations.noViolations')}
)}
)} {/* Traffic Tab */} {activeTab === 'traffic' && traffic && (
{/* Traffic Stats */}
} color={traffic.enabled ? 'success' : 'warning'} />
{/* Top Users by Traffic */} {traffic.top_users && traffic.top_users.length > 0 && (

{t('banSystem.traffic.topUsers')}

{traffic.top_users.map((user, idx) => ( ))}
{t('banSystem.traffic.username')} {t('banSystem.traffic.bytesTotal')} {t('banSystem.traffic.bytesLimit')} {t('banSystem.traffic.status')}
{user.username} {formatBytes(user.bytes_total)} {user.bytes_limit ? formatBytes(user.bytes_limit) : '-'} {user.over_limit ? t('banSystem.traffic.overLimit') : t('banSystem.traffic.ok')}
)} {/* Recent Violations */} {traffic.recent_violations && traffic.recent_violations.length > 0 && (

{t('banSystem.traffic.recentViolations')}

{traffic.recent_violations.map((v, idx) => ( ))}
{t('banSystem.violations.user')} {t('banSystem.violations.type')} {t('banSystem.violations.detectedAt')}
{v.username} {v.violation_type} {formatDate(v.detected_at)}
)} {(!traffic.top_users || traffic.top_users.length === 0) && (!traffic.recent_violations || traffic.recent_violations.length === 0) && (
{t('common.noData')}
)}
)} {/* Reports Tab */} {activeTab === 'reports' && (
{/* Period Selector */}
{t('banSystem.reports.period')}:
{[6, 12, 24, 48, 72].map((hours) => ( ))}
{report && ( <> {/* Report Stats */}
} color="accent" /> } color="info" />
{/* Top Violators */} {report.top_violators && report.top_violators.length > 0 && (

{t('banSystem.reports.topViolators')}

{report.top_violators.map((v, idx) => ( ))}
{t('banSystem.reports.username')} {t('banSystem.reports.count')}
{v.username} {v.count}
)} )}
)} {/* Settings Tab */} {activeTab === 'settings' && settings && (
{/* Group settings by category */} {(() => { const grouped: Record = {}; // Smart categorization: use API category or infer from key prefix const inferCategory = (key: string, apiCategory: string | null): string => { if (apiCategory) return apiCategory; if (key.startsWith('punishment_') || key.startsWith('progressive_ban')) return 'punishment'; if (key.startsWith('traffic_')) return 'traffic'; if (key.startsWith('network_')) return 'network'; if (key.startsWith('rate_limit_')) return 'rate_limit'; if (key.startsWith('notify_') || key.startsWith('daily_report')) return 'notifications'; return 'general'; }; settings.settings.forEach((s) => { const cat = inferCategory(s.key, s.category); if (!grouped[cat]) grouped[cat] = []; grouped[cat].push(s); }); // Sort categories in logical order const categoryOrder = [ 'general', 'punishment', 'progressive_bans', 'traffic', 'network', 'notifications', 'rate_limit', ]; const sortedCategories = Object.keys(grouped).sort((a, b) => { const aIdx = categoryOrder.indexOf(a); const bIdx = categoryOrder.indexOf(b); if (aIdx === -1 && bIdx === -1) return a.localeCompare(b); if (aIdx === -1) return 1; if (bIdx === -1) return -1; return aIdx - bIdx; }); return sortedCategories.map((category) => (

{formatCategory(category)}

{grouped[category].map((setting) => (
{formatSettingKey(setting.key)}
{setting.description && (
{setting.description}
)}
{setting.type === 'bool' ? ( ) : setting.type === 'int' ? ( handleSetSetting(setting.key, e.target.value)} min={setting.min_value ?? undefined} max={setting.max_value ?? undefined} disabled={!setting.editable || settingLoading === setting.key} className="input w-24" /> ) : setting.type === 'list' ? (
{Array.isArray(setting.value) && setting.value.length > 0 ? ( setting.value.map((item, idx) => ( {String(item)} )) ) : ( {t('common.noData')} )} {setting.editable && nodes && setting.key.includes('nodes') && ( )}
) : (
{String(setting.value)}
)}
))}
)); })()}
)} {/* Health Tab */} {activeTab === 'health' && health && (
{/* Overall Status */}
{t('banSystem.health.systemStatus')}
{health.status.toUpperCase()}
{health.uptime !== null && (
{t('banSystem.stats.uptime')}
{formatUptime(health.uptime)}
)}
{/* Components Status */} {health.components && health.components.length > 0 && (
{health.components.map((comp, idx) => (
{comp.name}
{comp.status}
{comp.message && (
{comp.message}
)}
))}
)}
)} )} {/* User Detail Modal */} {selectedUser && (
setSelectedUser(null)} >
e.stopPropagation()} >

{t('banSystem.userDetail.title')}

{/* User Info */}
{t('banSystem.users.email')}
{selectedUser.email}
{t('banSystem.users.limit')}
{selectedUser.limit ?? '-'}
{t('banSystem.users.ipCount')}
{selectedUser.unique_ip_count}
{t('banSystem.users.networkType')}
{selectedUser.network_type || '-'}
{/* IP History */}

{t('banSystem.userDetail.ipHistory')}

{selectedUser.ips.map((ip, idx) => ( ))}
{t('banSystem.userDetail.ip')} {t('banSystem.userDetail.country')} {t('banSystem.userDetail.node')} {t('banSystem.userDetail.requests')}
{ip.ip} {ip.country_name || ip.country_code || '-'} {ip.node || '-'} {ip.request_count}
{selectedUser.ips.length === 0 && (
{t('common.noData')}
)}
)}
); }