diff --git a/src/api/banSystem.ts b/src/api/banSystem.ts index 18137fd..af6e98d 100644 --- a/src/api/banSystem.ts +++ b/src/api/banSystem.ts @@ -176,6 +176,97 @@ export interface BanTrafficViolationsResponse { total: number } +export interface BanTrafficTopItem { + username: string + bytes_total: number + bytes_limit: number | null + over_limit: boolean +} + +export interface BanTrafficResponse { + enabled: boolean + stats: Record | null + top_users: BanTrafficTopItem[] + recent_violations: BanTrafficViolationItem[] +} + +// === Settings Types === + +export interface BanSettingDefinition { + key: string + value: unknown + type: string + min_value: number | null + max_value: number | null + editable: boolean + description: string | null + category: string | null +} + +export interface BanSettingsResponse { + settings: BanSettingDefinition[] +} + +export interface BanWhitelistRequest { + username: string +} + +// === Report Types === + +export interface BanReportTopViolator { + username: string + count: number +} + +export interface BanReportResponse { + period_hours: number + current_users: number + current_ips: number + punishment_stats: Record | null + top_violators: BanReportTopViolator[] +} + +// === Health Types === + +export interface BanHealthComponent { + name: string + status: string + message: string | null + details: Record | null +} + +export interface BanHealthResponse { + status: string + uptime: number | null + components: BanHealthComponent[] +} + +export interface BanHealthDetailedResponse { + status: string + uptime: number | null + components: Record +} + +// === Agent History Types === + +export interface BanAgentHistoryItem { + timestamp: string + sent_total: number + dropped_total: number + queue_size: number + batches_total: number +} + +export interface BanAgentHistoryResponse { + node: string + hours: number + records: number + delta: Record | null + first: Record | null + last: Record | null + history: BanAgentHistoryItem[] +} + // === API === export const banSystemApi = { @@ -269,4 +360,86 @@ export const banSystemApi = { }) return response.data }, + + // Full Traffic + getTraffic: async (): Promise => { + const response = await apiClient.get('/cabinet/admin/ban-system/traffic') + return response.data + }, + + getTrafficTop: async (limit: number = 20): Promise => { + const response = await apiClient.get('/cabinet/admin/ban-system/traffic/top', { + params: { limit } + }) + return response.data + }, + + // Settings + getSettings: async (): Promise => { + const response = await apiClient.get('/cabinet/admin/ban-system/settings') + return response.data + }, + + getSetting: async (key: string): Promise => { + const response = await apiClient.get(`/cabinet/admin/ban-system/settings/${key}`) + return response.data + }, + + setSetting: async (key: string, value: string): Promise => { + const response = await apiClient.post(`/cabinet/admin/ban-system/settings/${key}`, null, { + params: { value } + }) + return response.data + }, + + toggleSetting: async (key: string): Promise => { + const response = await apiClient.post(`/cabinet/admin/ban-system/settings/${key}/toggle`) + return response.data + }, + + // Whitelist + whitelistAdd: async (username: string): Promise => { + const response = await apiClient.post('/cabinet/admin/ban-system/settings/whitelist/add', { username }) + return response.data + }, + + whitelistRemove: async (username: string): Promise => { + const response = await apiClient.post('/cabinet/admin/ban-system/settings/whitelist/remove', { username }) + return response.data + }, + + // Reports + getReport: async (hours: number = 24): Promise => { + const response = await apiClient.get('/cabinet/admin/ban-system/report', { + params: { hours } + }) + return response.data + }, + + // Health + getHealth: async (): Promise => { + const response = await apiClient.get('/cabinet/admin/ban-system/health') + return response.data + }, + + getHealthDetailed: async (): Promise => { + const response = await apiClient.get('/cabinet/admin/ban-system/health/detailed') + return response.data + }, + + // Agent History + getAgentHistory: async (nodeName: string, hours: number = 24): Promise => { + const response = await apiClient.get(`/cabinet/admin/ban-system/agents/${encodeURIComponent(nodeName)}/history`, { + params: { hours } + }) + return response.data + }, + + // User Punishment History + getUserHistory: async (email: string, limit: number = 20): Promise => { + const response = await apiClient.get(`/cabinet/admin/ban-system/users/${encodeURIComponent(email)}/history`, { + params: { limit } + }) + return response.data + }, } diff --git a/src/locales/en.json b/src/locales/en.json index bfa40cd..1152287 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -18,7 +18,8 @@ "and": "and", "edit": "Edit", "delete": "Delete", - "currency": "$" + "currency": "$", + "refresh": "Refresh" }, "nav": { "dashboard": "Dashboard", @@ -758,7 +759,11 @@ "punishments": "Bans", "nodes": "Nodes", "agents": "Agents", - "violations": "Violations" + "violations": "Violations", + "traffic": "Traffic", + "reports": "Reports", + "settings": "Settings", + "health": "Health" }, "stats": { "activeUsers": "Active Users", @@ -939,6 +944,45 @@ "noViolations": "No violations found", "yes": "Yes", "no": "No" + }, + "traffic": { + "title": "Traffic Statistics", + "enabled": "Traffic Monitoring", + "topUsers": "Top by Traffic", + "username": "User", + "bytesTotal": "Total", + "bytesLimit": "Limit", + "status": "Status", + "overLimit": "Over Limit", + "ok": "OK", + "recentViolations": "Recent Violations" + }, + "reports": { + "title": "Period Reports", + "period": "Period", + "currentUsers": "Active Users", + "currentIps": "Unique IPs", + "topViolators": "Top Violators", + "username": "User", + "count": "Violations" + }, + "settings": { + "title": "System Settings", + "general": "General", + "limits": "Limits", + "notifications": "Notifications", + "whitelist": "Whitelist", + "saved": "Setting saved", + "error": "Error saving" + }, + "health": { + "title": "System Health", + "systemStatus": "System Status", + "healthy": "Healthy", + "degraded": "Degraded", + "unhealthy": "Unhealthy", + "components": "Components", + "uptime": "Uptime" } }, "profile": { diff --git a/src/locales/ru.json b/src/locales/ru.json index 4de17cd..ac4ce57 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -18,7 +18,8 @@ "and": "и", "edit": "Редактировать", "delete": "Удалить", - "currency": "₽" + "currency": "₽", + "refresh": "Обновить" }, "nav": { "dashboard": "Главная", @@ -758,7 +759,11 @@ "punishments": "Баны", "nodes": "Ноды", "agents": "Агенты", - "violations": "Нарушения" + "violations": "Нарушения", + "traffic": "Трафик", + "reports": "Отчёты", + "settings": "Настройки", + "health": "Здоровье" }, "stats": { "activeUsers": "Активных пользователей", @@ -939,6 +944,45 @@ "noViolations": "Нарушений не обнаружено", "yes": "Да", "no": "Нет" + }, + "traffic": { + "title": "Статистика трафика", + "enabled": "Мониторинг трафика", + "topUsers": "Топ по трафику", + "username": "Пользователь", + "bytesTotal": "Всего", + "bytesLimit": "Лимит", + "status": "Статус", + "overLimit": "Превышен", + "ok": "OK", + "recentViolations": "Последние нарушения" + }, + "reports": { + "title": "Отчёты за период", + "period": "Период", + "currentUsers": "Активных пользователей", + "currentIps": "Уникальных IP", + "topViolators": "Топ нарушителей", + "username": "Пользователь", + "count": "Нарушений" + }, + "settings": { + "title": "Настройки системы", + "general": "Общие", + "limits": "Лимиты", + "notifications": "Уведомления", + "whitelist": "Белый список", + "saved": "Настройка сохранена", + "error": "Ошибка сохранения" + }, + "health": { + "title": "Состояние системы", + "systemStatus": "Статус системы", + "healthy": "Здорова", + "degraded": "Деградация", + "unhealthy": "Проблема", + "components": "Компоненты", + "uptime": "Аптайм" } }, "profile": { diff --git a/src/pages/AdminBanSystem.tsx b/src/pages/AdminBanSystem.tsx index 05869e4..594834a 100644 --- a/src/pages/AdminBanSystem.tsx +++ b/src/pages/AdminBanSystem.tsx @@ -10,6 +10,11 @@ import { type BanNodesListResponse, type BanAgentsListResponse, type BanTrafficViolationsResponse, + type BanSettingsResponse, + type BanSettingDefinition, + type BanTrafficResponse, + type BanReportResponse, + type BanHealthResponse, } from '../api/banSystem' // Icons @@ -67,7 +72,32 @@ const SearchIcon = () => ( ) -type TabType = 'dashboard' | 'users' | 'punishments' | 'nodes' | 'agents' | 'violations' +const SettingsIcon = () => ( + + + + +) + +const TrafficIcon = () => ( + + + +) + +const ReportIcon = () => ( + + + +) + +const HealthIcon = () => ( + + + +) + +type TabType = 'dashboard' | 'users' | 'punishments' | 'nodes' | 'agents' | 'violations' | 'settings' | 'traffic' | 'reports' | 'health' interface StatCardProps { title: string @@ -111,6 +141,12 @@ export default function AdminBanSystem() { 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) const [error, setError] = useState(null) const [searchQuery, setSearchQuery] = useState('') @@ -171,6 +207,22 @@ export default function AdminBanSystem() { 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')) @@ -219,6 +271,48 @@ export default function AdminBanSystem() { } } + 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) @@ -242,6 +336,10 @@ export default function AdminBanSystem() { { 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) { @@ -648,6 +746,296 @@ export default function AdminBanSystem() { )} )} + + {/* 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 = {} + settings.settings.forEach((s) => { + const cat = s.category || 'general' + if (!grouped[cat]) grouped[cat] = [] + grouped[cat].push(s) + }) + + return Object.entries(grouped).map(([category, items]) => ( +
+
+

{category}

+
+
+ {items.map((setting) => ( +
+
+
{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.join(', ') : String(setting.value)} +
+ ) : ( +
{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}
+ )} +
+ ))} +
+ )} +
+ )} )}