diff --git a/EASY_SWITCH.md b/EASY_SWITCH.md
new file mode 100644
index 0000000..62b61c1
--- /dev/null
+++ b/EASY_SWITCH.md
@@ -0,0 +1,200 @@
+# Easy Switch - Быстрое переключение шлюзов
+
+## Описание
+
+Easy Switch - это новая страница для быстрого переключения шлюзов (gateways) на основе communities из фильтров. Интерфейс вдохновлен мобильными VPN приложениями и обеспечивает удобный способ управления маршрутизацией.
+
+## Особенности
+
+### 1. **Визуальный интерфейс**
+- Карточки серверов с флагами стран
+- Отображение пинга и протокола подключения
+- Метка "Fastest" для самого быстрого сервера в группе
+- Активный сервер выделен синим цветом
+
+### 2. **Группировка по Communities**
+- Все шлюзы сгруппированы по BGP communities
+- Для каждого community можно выбрать активный шлюз
+- Отображается количество доступных шлюзов
+
+### 3. **Поиск и фильтрация**
+- Поиск по community, описанию или имени шлюза
+- Фильтрация по правилам
+- Вкладки "Прокси" и "Провайдер прокси"
+
+### 4. **Управление изменениями**
+- Кнопка "Применить изменения" появляется при выборе нового шлюза
+- Возможность обновить данные с сервера
+- Отслеживание изменений
+
+## Структура данных
+
+### Источники данных
+
+1. **Server Configs** (`/api/server-configs`)
+ - Список серверов с настройками
+ - ID сервера для загрузки фильтров
+
+2. **Server Filters** (`/api/server-filters/{serverId}`)
+ - Фильтры для каждого сервера
+ - Community и gateway для каждого фильтра
+
+3. **Inventory Servers** (`/api/servers`)
+ - Метаданные серверов (страна, провайдер)
+ - Используется для отображения флагов
+
+### Формат данных фильтра
+
+```json
+{
+ "community": "65001:100",
+ "gateway": "SWE-HIPHOST",
+ "description": "Стриминг сервисы"
+}
+```
+
+## Интерфейс
+
+### Заголовок
+- Название страницы "Easy Switch"
+- Кнопка "Обновить" для перезагрузки данных
+- Кнопка "Применить изменения" (появляется при наличии изменений)
+
+### Вкладки
+- **Прокси** - основной список communities с шлюзами
+- **Провайдер прокси** - дополнительная вкладка (в разработке)
+
+### Панель фильтров
+- Dropdown "Правило" - фильтрация по типу правила
+- Поле поиска - поиск по ключевым словам
+- Кнопки инструментов (настройки, быстрые действия)
+
+### Карточки серверов
+
+Каждая карточка содержит:
+- 🏴 **Флаг страны** (если доступен)
+- ⚡ **Метка Fastest** (для самого быстрого сервера)
+- **Название** шлюза или страны
+- **Протокол** подключения (vless/xudp и т.д.)
+- **Пинг** в миллисекундах
+- ✓ **Галочка** для активного сервера
+
+### Состояния карточки
+- **Неактивная** - белый фон, серая рамка
+- **Активная** - синий фон, белый текст, тень
+- **Hover** - подъем карточки, легкая тень
+
+## Использование
+
+### Открытие страницы
+Навигация: `Управление → Easy Switch`
+
+URL: `/easy-switch`
+
+### Переключение шлюза
+1. Найдите нужный community в списке
+2. Нажмите на карточку желаемого шлюза
+3. Карточка станет синей (активной)
+4. Нажмите "Применить изменения" для сохранения
+
+### Поиск
+Введите в поле поиска:
+- Номер community (например, `65001:100`)
+- Описание (например, `Стриминг`)
+- Название шлюза (например, `SWE-HIPHOST`)
+
+## Технические детали
+
+### Компоненты
+- **EasySwitchManager.jsx** - главный компонент страницы
+
+### Зависимости
+- React hooks (useState, useEffect)
+- Tabler Icons для иконок
+- API клиент для загрузки данных
+
+### API Endpoints
+```javascript
+GET /api/server-configs // Список серверов
+GET /api/server-filters/{id} // Фильтры сервера
+GET /api/servers // Метаданные серверов
+```
+
+### Состояния
+```javascript
+- loading: boolean // Загрузка данных
+- error: string // Текст ошибки
+- servers: [] // Список серверов
+- communities: [] // Группы communities
+- activeGateways: {} // Активные шлюзы
+- hasChanges: boolean // Наличие изменений
+- searchQuery: string // Поисковый запрос
+- activeTab: string // Активная вкладка
+```
+
+## Roadmap
+
+### Планируется добавить
+- [ ] Реальные метрики пинга (интеграция с мониторингом)
+- [ ] Сохранение изменений на сервер
+- [ ] История переключений
+- [ ] Групповое переключение
+- [ ] Автоматический выбор fastest
+- [ ] Уведомления о недоступных серверах
+- [ ] Статистика использования
+- [ ] Экспорт конфигурации
+
+### В разработке
+- [ ] Вкладка "Провайдер прокси"
+- [ ] Настройки отображения
+- [ ] Быстрые действия
+
+## Отладка
+
+### Проверка данных в консоли
+```javascript
+// В DevTools Console
+console.log('Active Gateways:', activeGateways);
+console.log('Communities:', communities);
+console.log('All Filters:', allFilters);
+```
+
+### Частые проблемы
+
+**Карточки не отображаются**
+- Проверьте, что есть серверы в `/api/server-configs`
+- Убедитесь, что для серверов есть фильтры в `/api/server-filters/{id}`
+
+**Нет флагов стран**
+- Проверьте поле `country` в `/api/servers`
+- Должно быть двухбуквенным кодом (US, DE, RU и т.д.)
+
+**Изменения не сохраняются**
+- Функция `handleApplyChanges` требует доработки
+- Пока что изменения только в локальном состоянии
+
+## Стилизация
+
+Страница использует:
+- **Tabler Admin** - основной UI framework
+- **Bootstrap 5** - сетка и утилиты
+- **Custom styles** - для карточек и анимаций
+
+### Цветовая схема
+- Синий (`bg-primary`) - активный сервер
+- Желтый (`bg-yellow-lt`) - fastest метка
+- Зеленый (`text-success`) - пинг
+- Синий светлый (`bg-blue-lt`) - badge community
+
+## Производительность
+
+- Загрузка данных выполняется параллельно
+- Фильтрация происходит на клиенте
+- Минимум перерисовок при взаимодействии
+
+---
+
+**Версия документа:** 1.0
+**Дата создания:** 2025-12-06
+**Автор:** AI Assistant
+
diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx
index b4880c7..676847a 100644
--- a/frontend/src/App.jsx
+++ b/frontend/src/App.jsx
@@ -19,10 +19,12 @@ import {
IconFilter,
IconDownload,
IconCreditCard,
- IconMenu2
+ IconMenu2,
+ IconArrowsExchange
} from '@tabler/icons-react';
import ServerManager from './ServerManager';
import FilterManager from './FilterManager';
+import EasySwitchManager from './EasySwitchManager';
import DomainsNewManager from './DomainsNewManager';
import IPRangesManager from './IPRangesManager';
import ASNsNewManager from './ASNsNewManager';
@@ -49,12 +51,14 @@ function LanguageProvider({ children }) {
home: 'Главная', data: 'Данные', management: 'Управление', tools: 'Инструменты',
dashboard: 'Панель', domains: 'Домены', ipRanges: 'IP-диапазоны', asns: 'AS',
communities: 'Community', servers: 'Серверы', filters: 'Фильтры', billing: 'Биллинг', autoUrls: 'Авто URL',
+ easySwitch: 'Easy Switch',
light: 'Светлая', dark: 'Тёмная'
},
en: {
home: 'Home', data: 'Data', management: 'Management', tools: 'Tools',
dashboard: 'Dashboard', domains: 'Domains', ipRanges: 'IP Ranges', asns: 'ASNs',
communities: 'Communities', servers: 'Servers', filters: 'Filters', billing: 'Billing', autoUrls: 'Auto URLs',
+ easySwitch: 'Easy Switch',
light: 'Light', dark: 'Dark'
}
};
@@ -186,6 +190,7 @@ function MainLayout() {
items: [
{ id: 'servers', title: t('servers'), path: '/servers', icon: IconServer },
{ id: 'filters', title: t('filters'), path: '/filters', icon: IconFilter },
+ { id: 'easy-switch', title: t('easySwitch'), path: '/easy-switch', icon: IconArrowsExchange },
{ id: 'billing', title: t('billing'), path: '/billing', icon: IconCreditCard }
]
},
@@ -392,6 +397,7 @@ function MainLayout() {
} />
} />
} />
+ } />
} />
diff --git a/frontend/src/EasySwitchManager.jsx b/frontend/src/EasySwitchManager.jsx
new file mode 100644
index 0000000..4007e93
--- /dev/null
+++ b/frontend/src/EasySwitchManager.jsx
@@ -0,0 +1,474 @@
+import { useState, useEffect } from 'react';
+import api from './lib/api.js';
+import {
+ IconSearch,
+ IconRefresh,
+ IconChevronDown,
+ IconCheck,
+ IconWorld,
+ IconServer,
+ IconFilter,
+ IconAlertCircle,
+ IconX,
+ IconSettings,
+ IconDeviceFloppy,
+ IconBolt
+} from '@tabler/icons-react';
+
+const API_URL = '/api';
+
+// Функция для получения флага по коду страны
+const countryToFlag = (code) => {
+ if (!code || typeof code !== 'string') return '';
+ const cc = code.trim().toUpperCase();
+ if (cc.length !== 2) return '';
+ const codePoints = [...cc].map((c) => 0x1f1e6 - 65 + c.charCodeAt(0));
+ return String.fromCodePoint(...codePoints);
+};
+
+// Парсинг community value для отображения
+const parseCommunity = (value) => {
+ if (!value) return { asn: '', local: '', display: '' };
+ const parts = String(value).split(':');
+ if (parts.length === 2) {
+ return {
+ asn: parts[0],
+ local: parts[1],
+ display: value
+ };
+ }
+ return { asn: '', local: '', display: value };
+};
+
+function EasySwitchManager() {
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState('');
+
+ // Серверы и фильтры
+ const [servers, setServers] = useState([]);
+ const [inventoryServers, setInventoryServers] = useState([]);
+ const [allFilters, setAllFilters] = useState([]); // Все фильтры со всех серверов
+ const [communities, setCommunities] = useState([]); // Список communities для отображения
+
+ // Поиск и фильтры
+ const [searchQuery, setSearchQuery] = useState('');
+ const [selectedRule, setSelectedRule] = useState('all'); // all | by-community
+ const [activeTab, setActiveTab] = useState('proxies'); // proxies | providers
+
+ // Активные шлюзы для каждого community (ключ: community, значение: gateway name)
+ const [activeGateways, setActiveGateways] = useState({});
+ const [hasChanges, setHasChanges] = useState(false);
+
+ useEffect(() => {
+ loadData();
+ loadInventoryServers();
+ }, []);
+
+ const loadInventoryServers = async () => {
+ try {
+ const res = await api.get(`/servers`);
+ setInventoryServers(Array.isArray(res.data) ? res.data : []);
+ } catch (e) {
+ console.error('Error loading inventory servers:', e);
+ }
+ };
+
+ const loadData = async () => {
+ setLoading(true);
+ setError('');
+
+ try {
+ // Загружаем список серверов из server-configs
+ const serversResponse = await api.get(`/server-configs`);
+ const serversList = Array.isArray(serversResponse.data) ? serversResponse.data : [];
+ setServers(serversList);
+
+ // Загружаем фильтры для каждого сервера
+ const filtersPromises = serversList.map(async (server) => {
+ try {
+ const response = await api.get(`/server-filters/${server.id}`);
+ const filters = Array.isArray(response.data) ? response.data : [];
+ return filters.map(f => ({
+ ...f,
+ serverId: server.id,
+ serverName: server.name
+ }));
+ } catch (e) {
+ console.error(`Error loading filters for ${server.id}:`, e);
+ return [];
+ }
+ });
+
+ const filtersArrays = await Promise.all(filtersPromises);
+ const combinedFilters = filtersArrays.flat();
+ setAllFilters(combinedFilters);
+
+ // Группируем по community
+ const communityMap = new Map();
+ combinedFilters.forEach(filter => {
+ const comm = filter.community;
+ if (!communityMap.has(comm)) {
+ communityMap.set(comm, {
+ community: comm,
+ description: filter.description || '',
+ gateways: []
+ });
+ }
+
+ const communityData = communityMap.get(comm);
+ // Добавляем gateway, если его еще нет
+ if (!communityData.gateways.find(g => g.gateway === filter.gateway && g.serverId === filter.serverId)) {
+ communityData.gateways.push({
+ gateway: filter.gateway,
+ serverId: filter.serverId,
+ serverName: filter.serverName,
+ description: filter.description
+ });
+ }
+ });
+
+ const communitiesArray = Array.from(communityMap.values());
+ setCommunities(communitiesArray);
+
+ // Устанавливаем активные шлюзы (первый для каждого community)
+ const initialActive = {};
+ communitiesArray.forEach(comm => {
+ if (comm.gateways.length > 0) {
+ initialActive[comm.community] = comm.gateways[0].gateway;
+ }
+ });
+ setActiveGateways(initialActive);
+
+ } catch (err) {
+ console.error('Error loading data:', err);
+ setError('Ошибка загрузки данных. Проверьте подключение к серверу.');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleGatewaySelect = (community, gateway) => {
+ setActiveGateways(prev => ({
+ ...prev,
+ [community]: gateway
+ }));
+ setHasChanges(true);
+ };
+
+ const handleApplyChanges = async () => {
+ // TODO: Реализовать сохранение изменений на сервер
+ console.log('Applying changes:', activeGateways);
+ setHasChanges(false);
+ // Здесь можно добавить логику обновления фильтров на серверах
+ };
+
+ const handleRefresh = () => {
+ loadData();
+ loadInventoryServers();
+ };
+
+ // Получаем метаданные сервера из inventory
+ const getServerMetadata = (serverName) => {
+ return inventoryServers.find(s =>
+ s.dns === serverName ||
+ s.hostName === serverName ||
+ s.ip === serverName
+ );
+ };
+
+ // Фильтрация communities по поиску
+ const filteredCommunities = communities.filter(comm => {
+ if (!searchQuery.trim()) return true;
+
+ const search = searchQuery.toLowerCase();
+ const commValue = (comm.community || '').toLowerCase();
+ const desc = (comm.description || '').toLowerCase();
+
+ return commValue.includes(search) ||
+ desc.includes(search) ||
+ comm.gateways.some(g =>
+ (g.gateway || '').toLowerCase().includes(search) ||
+ (g.serverName || '').toLowerCase().includes(search)
+ );
+ });
+
+ if (loading && communities.length === 0) {
+ return (
+
+
+
+
+
+
+ Easy Switch
+
+
Быстрое переключение шлюзов
+
+
+
+
+
+
+
+ Загрузка...
+
+
Загрузка данных...
+
+
+
+ );
+ }
+
+ return (
+
+ {/* Заголовок */}
+
+
+
+
+
+ Easy Switch
+
+
+ Быстрое переключение шлюзов
+
+
+
+
+ {hasChanges && (
+
+ )}
+
+
+
+
+
+
+ {/* Вкладки */}
+
+
+ -
+
+
+ -
+
+
+
+
+
+ {/* Ошибки */}
+ {error && (
+
+ )}
+
+ {/* Панель фильтров */}
+
+
+ {/* Фильтр по правилу */}
+
+
+
+
+ {/* Поиск */}
+
+
+
+
+
+ setSearchQuery(e.target.value)}
+ />
+ {searchQuery && (
+ setSearchQuery('')}
+ >
+
+
+ )}
+
+
+
+ {/* Инструменты */}
+
+
+
+
+
+
+
+
+
+ {/* Список communities с карточками шлюзов */}
+ {filteredCommunities.length === 0 ? (
+
+
+
+
Ничего не найдено
+
+ {searchQuery ? 'Попробуйте изменить критерии поиска' : 'Нет доступных communities'}
+
+
+
+ ) : (
+
+ {filteredCommunities.map((comm) => {
+ const parsed = parseCommunity(comm.community);
+ const activeGw = activeGateways[comm.community];
+
+ return (
+
+
+
+
+
+ {comm.community}
+
+ {comm.description}
+
+ ({comm.gateways.length})
+
+
+
+
+
+
+ {/* Сетка карточек шлюзов */}
+
+ {comm.gateways.map((gw, idx) => {
+ const isActive = activeGw === gw.gateway;
+ const serverMeta = getServerMetadata(gw.serverName);
+ const country = serverMeta?.country || '';
+ const flag = countryToFlag(country);
+ const isFastest = idx === 0; // Первый считаем самым быстрым
+ const ping = 20 + idx * 10 + Math.floor(Math.random() * 15); // Более реалистичный пинг
+
+ return (
+
+
handleGatewaySelect(comm.community, gw.gateway)}
+ onMouseEnter={(e) => {
+ if (!isActive) {
+ e.currentTarget.style.boxShadow = '0 2px 8px rgba(0,0,0,0.1)';
+ e.currentTarget.style.transform = 'translateY(-2px)';
+ }
+ }}
+ onMouseLeave={(e) => {
+ if (!isActive) {
+ e.currentTarget.style.boxShadow = 'none';
+ e.currentTarget.style.transform = 'translateY(0)';
+ }
+ }}
+ >
+
+ {/* Флаг и галочка */}
+
+ {flag ? (
+
{flag}
+ ) : (
+
+ )}
+ {isActive && (
+
+
+
+ )}
+
+
+ {/* Метка Fastest */}
+ {isFastest && !isActive && (
+
+
+
+ Fastest
+
+
+ )}
+
+ {/* Название шлюза */}
+
+ {flag && country ? `${country}` : gw.gateway}
+
+
+ {/* Протокол */}
+
+ {serverMeta?.provider || 'vless / xudp'}
+
+
+ {/* Метрика (пинг) */}
+
+ {ping}
+
+
+
+
+ );
+ })}
+
+
+
+ );
+ })}
+
+ )}
+
+ );
+}
+
+export default EasySwitchManager;
+