import { useState, useEffect, useCallback } from 'react' import { useTranslation } from 'react-i18next' 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' // Icons const ShieldIcon = () => ( ) const UsersIcon = () => ( ) const BanIcon = () => ( ) const ServerIcon = () => ( ) const AgentIcon = () => ( ) const WarningIcon = () => ( ) const RefreshIcon = () => ( ) const ChartIcon = () => ( ) const SearchIcon = () => ( ) const SettingsIcon = () => ( ) const TrafficIcon = () => ( ) const ReportIcon = () => ( ) const HealthIcon = () => ( ) 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 [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) useEffect(() => { loadStatus() }, []) useEffect(() => { if (status?.enabled && status?.configured) { loadTabData(activeTab) } }, [activeTab, status]) const loadStatus = async () => { try { setLoading(true) const data = await banSystemApi.getStatus() setStatus(data) if (!data.enabled || !data.configured) { setError(t('banSystem.notConfigured')) } } catch { setError(t('banSystem.loadError')) } finally { setLoading(false) } } const loadTabData = async (tab: TabType) => { try { setLoading(true) setError(null) switch (tab) { case 'dashboard': { const statsData = await banSystemApi.getStats() setStats(statsData) break } case 'users': { const usersData = await banSystemApi.getUsers({ limit: 50 }) setUsers(usersData) break } case 'punishments': { const punishmentsData = await banSystemApi.getPunishments() setPunishments(punishmentsData) break } case 'nodes': { const nodesData = await banSystemApi.getNodes() setNodes(nodesData) break } case 'agents': { const agentsData = await banSystemApi.getAgents() setAgents(agentsData) break } case 'violations': { const violationsData = await banSystemApi.getTrafficViolations() setViolations(violationsData) break } case 'settings': { const settingsData = await banSystemApi.getSettings() setSettings(settingsData) break } case 'traffic': { const trafficData = await banSystemApi.getTraffic() setTraffic(trafficData) break } case 'reports': { const reportData = await banSystemApi.getReport(reportHours) setReport(reportData) break } case 'health': { const healthData = await banSystemApi.getHealth() setHealth(healthData) break } } } catch { setError(t('banSystem.loadError')) } finally { setLoading(false) } } const handleSearch = async () => { if (!searchQuery.trim()) { loadTabData('users') 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) loadTabData('punishments') } catch { setError(t('banSystem.loadError')) } finally { setActionLoading(null) } } const handleToggleSetting = async (key: string) => { try { setSettingLoading(key) await banSystemApi.toggleSetting(key) loadTabData('settings') } catch { setError(t('banSystem.loadError')) } finally { setSettingLoading(null) } } const handleSetSetting = async (key: string, value: string) => { try { setSettingLoading(key) await banSystemApi.setSetting(key, value) loadTabData('settings') } catch { setError(t('banSystem.loadError')) } finally { setSettingLoading(null) } } const handleReportPeriodChange = (hours: number) => { setReportHours(hours) } useEffect(() => { if (activeTab === 'reports' && status?.enabled) { loadTabData('reports') } }, [reportHours]) const formatBytes = (bytes: number) => { 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 | 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', { defaultValue: 'Написать в Telegram' })} {/* 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="w-full pl-10 pr-4 py-2 bg-dark-800 border border-dark-700 rounded-lg text-dark-100 placeholder-dark-500 focus:outline-none focus:border-accent-500" />
{/* 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="w-24 px-3 py-1.5 bg-dark-900 border border-dark-700 rounded-lg text-dark-100 text-sm focus:outline-none focus:border-accent-500 disabled:opacity-50" /> ) : 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')}
)}
)}
) }