From e529aa23ec8421f932e1a6d56bd858107ffef825 Mon Sep 17 00:00:00 2001 From: shats Date: Sun, 15 Mar 2026 23:29:24 +0700 Subject: [PATCH] refactor(tests): update package.json scripts for linting and testing; enhance CI workflows for linting and testing integration --- ...тимизация_router_lists_ui_4a198973.plan.md | 248 ++++++++++++++++++ .gitea/workflows/docker-publish-fast.yml | 27 ++ .gitea/workflows/docker-publish.yml | 26 +- .gitea/workflows/frontend-ci.yml | 63 +++++ frontend/eslint.config.js | 5 +- frontend/src/ASNsNewManager.jsx | 12 +- frontend/src/AutoUrlManager.jsx | 4 +- frontend/src/BillingManager.jsx | 20 +- frontend/src/CommunitiesManager.jsx | 4 +- frontend/src/Dashboard.jsx | 8 +- frontend/src/DomainsNewManager.jsx | 16 +- frontend/src/EasySwitchManager.jsx | 8 +- frontend/src/FilterManager.jsx | 22 +- frontend/src/FirewallPage.jsx | 4 +- frontend/src/GraphView.jsx | 13 +- frontend/src/IPRangesManager.jsx | 42 +-- frontend/src/InterfaceSpeedTest.jsx | 2 +- frontend/src/MikrotikTools.jsx | 8 +- frontend/src/NetworkConfigManager.jsx | 32 +-- frontend/src/NetworkMapDashboard.jsx | 2 +- frontend/src/NetworkMapUnifi.jsx | 12 +- frontend/src/OspfToolsPage.jsx | 4 +- frontend/src/PingServicesManager.jsx | 4 +- frontend/src/ServerManager.jsx | 12 +- frontend/src/UptimeMonitorPage.jsx | 4 +- frontend/src/components/BulkActionsBar.jsx | 6 +- frontend/src/components/DataTable.jsx | 2 +- frontend/src/components/ErrorBoundary.jsx | 4 +- frontend/src/components/HistoryModal.jsx | 2 +- frontend/src/components/IconPicker.jsx | 1 + frontend/src/components/MobileCardView.jsx | 4 +- frontend/src/components/NotifyProvider.jsx | 3 +- frontend/src/components/SavedFilters.jsx | 2 +- frontend/src/components/SettingsModal.jsx | 2 +- frontend/src/components/ToastContainer.jsx | 1 + frontend/src/components/TopNStats.jsx | 2 +- frontend/src/components/ValidatedInput.jsx | 4 +- frontend/src/components/VirtualizedTable.jsx | 2 +- frontend/src/components/WsUpdateModal.jsx | 6 +- .../src/components/server/ServerSidebar.jsx | 4 +- frontend/src/contexts/AlertsContext.jsx | 1 + frontend/src/contexts/PingContext.jsx | 5 +- .../src/examples/ErrorHandlingExample.jsx | 2 +- frontend/src/lib/api.js | 4 +- frontend/src/lib/asn.js | 2 +- package.json | 4 +- 46 files changed, 519 insertions(+), 146 deletions(-) create mode 100644 .cursor/plans/анализ_и_оптимизация_router_lists_ui_4a198973.plan.md create mode 100644 .gitea/workflows/frontend-ci.yml diff --git a/.cursor/plans/анализ_и_оптимизация_router_lists_ui_4a198973.plan.md b/.cursor/plans/анализ_и_оптимизация_router_lists_ui_4a198973.plan.md new file mode 100644 index 0000000..b19ef19 --- /dev/null +++ b/.cursor/plans/анализ_и_оптимизация_router_lists_ui_4a198973.plan.md @@ -0,0 +1,248 @@ +--- +name: Анализ и оптимизация Router Lists UI +overview: Комплексный анализ проекта Router Lists UI (RouterOS Manager) — управление BGP-списками, MikroTik и S3. План включает предложения по новым фичам и оптимизациям архитектуры, производительности и качества кода. +todos: [] +isProject: false +--- + +# Анализ проекта Router Lists UI + +## Текущее состояние проекта + +**Router Lists UI** — fullstack-приложение для управления BGP-списками (домены, IP-диапазоны, ASN), фильтрами и конфигурациями MikroTik на базе S3 (Yandex Object Storage). + +### Архитектура + +```mermaid +flowchart TB + subgraph Frontend [Frontend - Vite + React] + App[App.jsx] + Pages[20+ страниц] + Hooks[useApiQuery, useErrorHandler] + API[api.js + axios] + end + + subgraph Backend [Backend - Express] + Server[server.js] + Routes[30+ роутов] + S3Service[s3Service] + Schedulers[4 планировщика] + end + + subgraph External [Внешние сервисы] + S3[S3 / Yandex Object Storage] + MikroTik[MikroTik RouterOS API] + BGP[BGP Background Service] + end + + App --> Pages + Pages --> Hooks + Hooks --> API + API -->|REST| Server + Server --> Routes + Routes --> S3Service + S3Service --> S3 + Routes --> MikroTik + Server --> Schedulers +``` + + + +### Технологический стек + + +| Слой | Технологии | +| -------- | ------------------------------------------------------------------------------- | +| Frontend | React 19, Vite 7, Tabler, TanStack Query, React Router, Recharts, @xyflow/react | +| Backend | Express, AWS SDK (S3), Pino, Prometheus, Helmet, Rate Limit | +| Инфра | Docker, Gitea Actions, S3 | + + +### Сильные стороны + +- Продуманная обработка ошибок (retry, ErrorBoundary, NetworkErrorHandler) +- ETag/If-None-Match для условных GET и кеширования +- Soft-locks для предотвращения гонок при редактировании +- Prometheus-метрики, health/ready endpoints +- i18n (RU/EN), темы (light/dark), два варианта layout +- Command Palette, горячие клавиши +- Типы в `frontend/src/types/index.ts` (частичная типизация) + +--- + +## Рекомендуемые фичи + +### 1. Аутентификация и авторизация (высокий приоритет) + +**Проблема:** API открыт без авторизации — любой с доступом к сети может изменять данные. + +**Решение:** + +- JWT или session-based auth (express-session + passport) +- Роли: `viewer` (только чтение), `editor` (CRUD), `admin` (всё + настройки) +- Middleware на backend для проверки токена/сессии +- Страница логина, защищённые роуты на frontend + +### 2. Аудит-лог изменений + +- Логирование всех мутаций (кто, когда, что изменил) +- Хранение в S3 (`audit/YYYY-MM-DD.jsonl`) или отдельная таблица +- UI: страница «История изменений» с фильтрами по ресурсу, пользователю, дате + +### 3. Уведомления и алерты + +- Расширить [AlertsContext](frontend/src/contexts/AlertsContext.jsx): email/Telegram при критичных событиях +- Настраиваемые триггеры: сервер недоступен, превышение лимита ресурсов +- Интеграция с webhook (например, Slack/Discord) + +### 4. Экспорт/импорт в разных форматах + +- Экспорт доменов/ASN/IP в CSV, JSON, plain text +- Импорт из CSV с маппингом колонок +- Bulk-операции: «заменить community для выбранных» уже есть — добавить «импорт из файла» + +### 5. Расширенный поиск и фильтры + +- Полнотекстовый поиск по доменам (при больших объёмах — индексация на backend) +- Фильтры: по community, по дате добавления (если добавить метаданные) +- Сохранённые фильтры (уже есть [SavedFilters](frontend/src/components/SavedFilters.jsx)) — синхронизация с backend + +### 6. Дашборд и отчёты + +- Кастомные виджеты на Dashboard (drag-and-drop) +- Отчёты: топ доменов по community, динамика изменений +- Scheduled reports (PDF/email по расписанию) + +### 7. API-документация + +- OpenAPI/Swagger для backend API +- Интерактивная документация (Swagger UI) на `/api-docs` +- Генерация клиента для внешних интеграций + +### 8. Мобильная версия (PWA) + +- Service Worker для офлайн-просмотра кешированных данных +- Адаптивные таблицы (уже используется Tabler) +- Установка как PWA (manifest.json) + +--- + +## Оптимизации проекта + +### Frontend + +#### 1. Code splitting и lazy loading + +**Проблема:** Все 20+ страниц импортируются синхронно в [App.jsx](frontend/src/App.jsx) — большой начальный бандл. + +**Решение:** + +```javascript +// Вместо: import Dashboard from './Dashboard'; +const Dashboard = lazy(() => import('./Dashboard')); +// Обернуть Routes в }> +``` + +Применить для тяжёлых страниц: `TrafficDashboard`, `NetworkMapDashboard`, `RouteOptimizerPage`, `FilterManager`, `MikrotikTools`, `OspfToolsPage`. + +#### 2. Виртуализация таблиц + +- Используется `@tanstack/react-virtual` — проверить, что все большие списки (domains, ip-ranges, asns) используют виртуализацию +- В [DomainsNewManager](frontend/src/DomainsNewManager.jsx) — пагинация на backend (`offset`/`limit`), но при `limit=0` загружаются все данные — рассмотреть серверную пагинацию для больших объёмов + +#### 3. Оптимизация API-запросов + +- На Easy Switch при открытии идёт множество параллельных `POST /mikrotik/ping` — добавить batch endpoint `POST /mikrotik/ping-batch` для снижения нагрузки +- Prefetch при наведении на пункты меню (React Query `prefetchQuery`) +- Дедупликация запросов к `/communities` — уже кешируется в React Query, но вызывается из многих компонентов + +#### 4. Унификация работы с данными + +- Часть страниц использует [useApiQuery](frontend/src/hooks/useApiQuery.js) (domains, ipRanges, asns, servers, communities), часть — ручные `useState` + `api.get/post` +- Мигрировать `DomainsNewManager`, `IPRangesManager`, `ASNsNewManager` на единые хуки из `useApiQuery` для консистентности и оптимистичных обновлений + +#### 5. Миграция на TypeScript + +- Есть [types/index.ts](frontend/src/types/index.ts), но большинство компонентов — `.jsx` +- Поэтапная миграция: сначала `api.js`, `useApiQuery.js`, затем страницы +- Включить `strict` в tsconfig для новых файлов + +### Backend + +#### 1. Кеширование S3-ответов + +- Добавить in-memory cache (например, `node-cache`) для часто читаемых объектов (`communities`, `servers`, `filters`) с TTL 30–60 сек +- Уменьшит количество обращений к S3 при активной работе + +#### 2. Пул соединений и таймауты + +- Проверить настройки AWS SDK: connection pooling, таймауты для S3 +- Для долгих операций (address-lists, speed-test) — рассмотреть фоновые задачи (очередь jobs) + +#### 3. Валидация и санитизация + +- Использовать единую схему валидации (Ajv) для всех JSON-эндпоинтов +- Добавить rate limit на чувствительные операции (apply config, run script) + +#### 4. Структура роутов + +- [server.js](backend/server.js) — ~450 строк, много роутов в одном файле +- Вынести группы роутов в отдельные файлы: `textDataRoutes`, `jsonDataRoutes`, `mikrotikRoutes` и подключать через `app.use('/api/...', router)` + +### Инфраструктура и DevOps + +#### 1. Тестирование + +- **Сейчас:** `npm test` — заглушка, vitest упоминается в tsconfig, но тестов нет +- Добавить unit-тесты для: `validators`, `apiErrorHandler`, `s3Helpers`, `mikrotikConfig` +- E2E (Playwright) для критичных сценариев: логин, добавление домена, применение фильтра + +#### 2. CI/CD + +- Добавить шаг `npm run lint` и `npm run test` в Gitea Actions перед сборкой Docker +- Отдельный workflow для frontend: `npm run build` + проверка размера бандла + +#### 3. Мониторинг + +- Prometheus уже есть — добавить алерты (например, в Alertmanager) на `http_errors_total`, `http_request_duration_seconds` +- Health check: проверка доступности S3 в `/ready` + +#### 4. Безопасность + +- Регулярное обновление зависимостей (`npm audit`) +- Секреты (ENCRYPTION_KEY, AWS keys) — только через env, не в коде +- Рассмотреть Vault или аналог для production + +### Документация и DX + +- README — хороший, но API описан текстом; добавить ссылку на OpenAPI +- CONTRIBUTING.md с правилами коммитов, кодстайла +- Архитектурная диаграмма в репозитории (C4 или подобная) + +--- + +## Приоритизация + + +| Категория | Действие | Приоритет | +| ------------------ | -------------------------- | --------- | +| Безопасность | Аутентификация | Высокий | +| Производительность | Lazy loading страниц | Высокий | +| Производительность | Batch ping endpoint | Средний | +| Качество | Unit-тесты | Высокий | +| Качество | Миграция на TS | Средний | +| UX | Аудит-лог | Средний | +| UX | Расширенный экспорт/импорт | Низкий | +| Инфра | CI: lint + test | Высокий | +| Инфра | S3 in-memory cache | Средний | + + +--- + +## Следующие шаги + +1. Выбрать 2–3 пункта из плана для первой итерации +2. Для аутентификации — определиться со стратегией (JWT vs session, хранение пользователей) +3. Для lazy loading — создать `PageSkeleton` и обновить роуты в App.jsx +4. Для тестов — настроить Vitest, написать первые тесты для `validators` и `apiErrorHandler` + diff --git a/.gitea/workflows/docker-publish-fast.yml b/.gitea/workflows/docker-publish-fast.yml index 290e7d3..65803fa 100644 --- a/.gitea/workflows/docker-publish-fast.yml +++ b/.gitea/workflows/docker-publish-fast.yml @@ -8,7 +8,34 @@ on: - v4 jobs: + lint-and-test: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + + - name: Install dependencies + run: | + npm ci + cd frontend && npm ci + + - name: Lint + run: npm run lint + + - name: Test (frontend build) + run: npm run test + build-and-push-fast: + runs-on: ubuntu-latest + needs: lint-and-test runs-on: ubuntu-latest timeout-minutes: 15 steps: diff --git a/.gitea/workflows/docker-publish.yml b/.gitea/workflows/docker-publish.yml index a5dd25a..03b1f29 100644 --- a/.gitea/workflows/docker-publish.yml +++ b/.gitea/workflows/docker-publish.yml @@ -6,8 +6,32 @@ on: - main jobs: + lint-and-test: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + + - name: Install dependencies + run: | + npm ci + cd frontend && npm ci + + - name: Lint + run: npm run lint + + - name: Test (frontend build) + run: npm run test + build-and-push: runs-on: ubuntu-latest + needs: lint-and-test steps: - name: Checkout repository uses: actions/checkout@v4 @@ -29,7 +53,7 @@ jobs: uses: docker/build-push-action@v5 with: context: . - file: ./Dockerfile + file: ./Dockerfile.fast push: true platforms: linux/amd64 cache-from: type=gha diff --git a/.gitea/workflows/frontend-ci.yml b/.gitea/workflows/frontend-ci.yml new file mode 100644 index 0000000..36937f6 --- /dev/null +++ b/.gitea/workflows/frontend-ci.yml @@ -0,0 +1,63 @@ +name: Frontend CI + +on: + push: + branches: + - main + - tabler + - v3 + - v4 + paths: + - 'frontend/**' + - '.gitea/workflows/frontend-ci.yml' + pull_request: + branches: + - main + - tabler + - v3 + - v4 + paths: + - 'frontend/**' + - '.gitea/workflows/frontend-ci.yml' + +jobs: + frontend: + runs-on: ubuntu-latest + defaults: + run: + working-directory: frontend + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + cache-dependency-path: frontend/package-lock.json + + - name: Install dependencies + run: npm ci + + - name: Lint + run: npm run lint + + - name: Build + run: npm run build + + - name: Check bundle size + id: bundle-size + run: | + SIZE_BYTES=$(du -sb dist | cut -f1) + SIZE_MB=$(awk "BEGIN {printf \"%.2f\", $SIZE_BYTES / 1048576}") + echo "size_bytes=$SIZE_BYTES" >> $GITHUB_OUTPUT + echo "size_mb=$SIZE_MB" >> $GITHUB_OUTPUT + echo "## Bundle size: ${SIZE_MB} MB" >> $GITHUB_STEP_SUMMARY + # Fail if total bundle exceeds 5MB + BUNDLE_LIMIT_BYTES=5242880 + if [ "$SIZE_BYTES" -gt "$BUNDLE_LIMIT_BYTES" ]; then + echo "::error::Bundle size (${SIZE_MB} MB) exceeds 5MB limit" + exit 1 + fi diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js index cee1e2c..a081853 100644 --- a/frontend/eslint.config.js +++ b/frontend/eslint.config.js @@ -15,7 +15,7 @@ export default defineConfig([ ], languageOptions: { ecmaVersion: 2020, - globals: globals.browser, + globals: { ...globals.browser, process: 'readonly' }, parserOptions: { ecmaVersion: 'latest', ecmaFeatures: { jsx: true }, @@ -23,7 +23,8 @@ export default defineConfig([ }, }, rules: { - 'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }], + 'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]', argsIgnorePattern: '^_', caughtErrorsIgnorePattern: '^_' }], + 'no-empty': ['error', { allowEmptyCatch: true }], }, }, ]) diff --git a/frontend/src/ASNsNewManager.jsx b/frontend/src/ASNsNewManager.jsx index 41ccdc5..035963a 100644 --- a/frontend/src/ASNsNewManager.jsx +++ b/frontend/src/ASNsNewManager.jsx @@ -51,13 +51,13 @@ function ASNsNewManager() { const [originalItems, setOriginalItems] = useState([]); const [etag, setEtag] = useState(''); const [lastModified, setLastModified] = useState(''); - const [contentLength, setContentLength] = useState(null); + const [, setContentLength] = useState(null); const [newItem, setNewItem] = useState({ asn: '', community: '' }); - const [newInvalid, setNewInvalid] = useState({ asn: false, community: false }); + const [, setNewInvalid] = useState({ asn: false, community: false }); const [error, setError] = useState(''); const [success, setSuccess] = useState(''); const [searchTerm, setSearchTerm] = useState(''); - const searchTimer = useRef(null); + const _searchTimer = useRef(null); const didInit = useRef(false); const [editingValue, setEditingValue] = useState(''); const [editModalOpen, setEditModalOpen] = useState(false); @@ -82,7 +82,7 @@ function ASNsNewManager() { try { const res = await api.get(`/communities`); setCommunities(Array.isArray(res.data) ? res.data : []); - } catch (e) { + } catch (_e) { // тихо игнорируем } })(); @@ -298,7 +298,7 @@ function ASNsNewManager() { return { added, removed, changed }; }; - const [showDiff, setShowDiff] = useState(false); + const [_showDiff, setShowDiff] = useState(false); const [diff, setDiff] = useState({ added: [], removed: [], changed: [] }); const [confirmSaveOpen, setConfirmSaveOpen] = useState(false); const [historyOpen, setHistoryOpen] = useState(false); @@ -401,7 +401,7 @@ function ASNsNewManager() { }; // Drag&Drop импорт (быстрый путь) - const onDropImport = async (e) => { + const _onDropImport = async (e) => { e.preventDefault(); const file = e.dataTransfer?.files?.[0]; if (!file) return; diff --git a/frontend/src/AutoUrlManager.jsx b/frontend/src/AutoUrlManager.jsx index d994cb8..ce352bc 100644 --- a/frontend/src/AutoUrlManager.jsx +++ b/frontend/src/AutoUrlManager.jsx @@ -158,7 +158,7 @@ function AutoUrlManager() { } setSuccess('Пример формата скопирован'); setTimeout(() => setSuccess(''), 2000); - } catch (e) { + } catch (_e) { setError('Не удалось скопировать пример'); setTimeout(() => setError(''), 2000); } @@ -619,7 +619,7 @@ function AutoUrlManager() { - {filteredAndSortedUrls.map((url, displayIndex) => { + {filteredAndSortedUrls.map((url, _displayIndex) => { const originalIndex = urls.findIndex(u => u === url); const v = getRowValidity(url); const isValid = v.url && v.community; diff --git a/frontend/src/BillingManager.jsx b/frontend/src/BillingManager.jsx index 52e5e4e..fa90420 100644 --- a/frontend/src/BillingManager.jsx +++ b/frontend/src/BillingManager.jsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useRef } from 'react'; +import { useState, useEffect } from 'react'; import api from './lib/api.js'; import axios from 'axios'; import FormModal from './components/FormModal.jsx'; @@ -51,7 +51,7 @@ function BillingManager() { const [currentPage, setCurrentPage] = useState(1); const [sortField, setSortField] = useState('nextPaymentDate'); const [sortOrder, setSortOrder] = useState('asc'); - const [filterStatus, setFilterStatus] = useState(''); + const [filterStatus, _setFilterStatus] = useState(''); const [filterUrgency, setFilterUrgency] = useState(''); const [viewMode, setViewMode] = useState('cards'); // 'cards' | 'table' const [activeTab, setActiveTab] = useState('subscriptions'); // 'subscriptions' | 'payments' @@ -76,14 +76,14 @@ function BillingManager() { const [editingPayment, setEditingPayment] = useState(null); const [showDeletePaymentModal, setShowDeletePaymentModal] = useState(false); const [paymentToDelete, setPaymentToDelete] = useState(null); - const [futureDateConfirm, setFutureDateConfirm] = useState(false); + const [_futureDateConfirm, _setFutureDateConfirm] = useState(false); // История платежей - const [paymentSearchTerm, setPaymentSearchTerm] = useState(''); - const [paymentSortField, setPaymentSortField] = useState('date'); - const [paymentSortOrder, setPaymentSortOrder] = useState('desc'); - const [selectedPayments, setSelectedPayments] = useState(new Set()); - const [paymentPage, setPaymentPage] = useState(1); + const [paymentSearchTerm, _setPaymentSearchTerm] = useState(''); + const [_paymentSortField, _setPaymentSortField] = useState('date'); + const [_paymentSortOrder, _setPaymentSortOrder] = useState('desc'); + const [_selectedPayments, _setSelectedPayments] = useState(new Set()); + const [_paymentPage, _setPaymentPage] = useState(1); const paymentPageSize = 10; // Курсы валют @@ -92,7 +92,7 @@ function BillingManager() { EUR: 1, RUB: 1 }); - const [ratesLoading, setRatesLoading] = useState(false); + const [_ratesLoading, setRatesLoading] = useState(false); // Новый элемент const [newItem, setNewItem] = useState({ @@ -273,7 +273,7 @@ function BillingManager() { setLoading(true); try { // Убираем служебное поле _external перед сохранением - const payload = billingData.map(({ _external, ...rest }) => rest); + const payload = billingData.map(({ _external: _ext, ...rest }) => rest); await api.post(`/billing`, { domains: payload }); // После сохранения снимаем флаг _external со всех записей setBillingData(prev => prev.map(item => ({ ...item, _external: false }))); diff --git a/frontend/src/CommunitiesManager.jsx b/frontend/src/CommunitiesManager.jsx index 4c180b2..87ddcc9 100644 --- a/frontend/src/CommunitiesManager.jsx +++ b/frontend/src/CommunitiesManager.jsx @@ -190,7 +190,7 @@ function CommunitiesManager() { setLoading(true); try { // не отправляем служебное поле _external - const payload = data.map(({ _external, ...rest }) => rest); + const payload = data.map(({ _external: _, ...rest }) => rest); await api.post(`/communities`, { communities: payload }); setSuccess('Справочник сохранён!'); setTimeout(() => setSuccess(''), 3000); @@ -302,7 +302,7 @@ function CommunitiesManager() { }); return Array.from(map.values()); }); - } catch (e) { + } catch (_e) { setError('Неверный JSON.'); } }; diff --git a/frontend/src/Dashboard.jsx b/frontend/src/Dashboard.jsx index 6860509..b8e6c13 100644 --- a/frontend/src/Dashboard.jsx +++ b/frontend/src/Dashboard.jsx @@ -33,12 +33,12 @@ import Tooltip from './components/Tooltip.jsx'; import Sparkline from './components/Sparkline.jsx'; import { useAlerts } from './contexts/AlertsContext.jsx'; -function StatCard({ icon: Icon, color, value, title, subtitle, to, trend, previousValue }) { +function StatCard({ icon: _Icon, color, value, title, subtitle, to, trend, previousValue }) { return (
- + <_Icon size={32} />
@@ -64,12 +64,12 @@ function StatCard({ icon: Icon, color, value, title, subtitle, to, trend, previo ); } -function MetricCard({ title, value, icon: Icon, color, description }) { +function MetricCard({ title, value, icon: _Icon, color, description }) { return (
- + <_Icon size={32} />
diff --git a/frontend/src/DomainsNewManager.jsx b/frontend/src/DomainsNewManager.jsx index 2db018f..e9fd4a6 100644 --- a/frontend/src/DomainsNewManager.jsx +++ b/frontend/src/DomainsNewManager.jsx @@ -58,13 +58,13 @@ function DomainsNewManager() { const [originalItems, setOriginalItems] = useState([]); const [etag, setEtag] = useState(''); const [lastModified, setLastModified] = useState(''); - const [contentLength, setContentLength] = useState(null); + const [, setContentLength] = useState(null); const [newItem, setNewItem] = useState({ domain: '', community: '' }); - const [newInvalid, setNewInvalid] = useState({ domain: false, community: false }); + const [, setNewInvalid] = useState({ domain: false, community: false }); const [error, setError] = useState(''); const [success, setSuccess] = useState(''); const [searchTerm, setSearchTerm] = useState(''); - const searchTimer = useRef(null); + const _searchTimer = useRef(null); const didInit = useRef(false); const [editingValue, setEditingValue] = useState(''); const [editModalOpen, setEditModalOpen] = useState(false); @@ -92,16 +92,16 @@ function DomainsNewManager() { try { const res = await api.get(`/communities`, { signal }); setCommunities(Array.isArray(res.data) ? res.data : []); - } catch (e) { - if (e?.name === 'CanceledError' || e?.name === 'AbortError' || e?.code === 'ERR_CANCELED') return; + } catch (err) { + if (err?.name === 'CanceledError' || err?.name === 'AbortError' || err?.code === 'ERR_CANCELED') return; } })(); (async () => { try { const r = await api.get('/ws/url', { signal }); setWsUrl(String(r.data?.url || '')); - } catch (e) { - if (e?.name === 'CanceledError' || e?.name === 'AbortError' || e?.code === 'ERR_CANCELED') return; + } catch (err) { + if (err?.name === 'CanceledError' || err?.name === 'AbortError' || err?.code === 'ERR_CANCELED') return; } })(); return () => controller.abort(); @@ -339,7 +339,7 @@ function DomainsNewManager() { }; // Drag&Drop импорт - const onDropImport = async (e) => { + const _onDropImport = async (e) => { e.preventDefault(); const file = e.dataTransfer?.files?.[0]; if (!file) return; diff --git a/frontend/src/EasySwitchManager.jsx b/frontend/src/EasySwitchManager.jsx index 285fe6f..064fedd 100644 --- a/frontend/src/EasySwitchManager.jsx +++ b/frontend/src/EasySwitchManager.jsx @@ -42,7 +42,7 @@ function EasySwitchManager() { // Поиск и фильтры const [searchQuery, setSearchQuery] = useState(''); - const [selectedRule, setSelectedRule] = useState('all'); // all | by-community + const [_selectedRule, _setSelectedRule] = useState('all'); // all | by-community const [activeTab, setActiveTab] = useState('proxies'); // proxies | providers const [showOnlyConfigured, setShowOnlyConfigured] = useState(true); // Показывать только настроенные @@ -275,7 +275,7 @@ function EasySwitchManager() { try { console.log('[EasySwitch] serversWithGateways:', serversWithGateways); console.log('[EasySwitch] communitiesDirectory:', communities); - } catch (_) {} + } catch { /* no-op */ } // Автоматически разворачиваем первый сервер if (serversWithGateways.length > 0 && expandedServers.size === 0) { @@ -300,7 +300,7 @@ function EasySwitchManager() { try { console.log('[EasySwitch] initialActiveGateways:', initialActive); - } catch (_) {} + } catch { /* no-op */ } } catch (err) { if (err?.name === 'CanceledError' || err?.name === 'AbortError' || err?.code === 'ERR_CANCELED') return; @@ -381,7 +381,7 @@ function EasySwitchManager() { const currentFilters = Array.isArray(response.data) ? response.data : []; // Создаем Set существующих communities в фильтрах - const existingCommunitiesSet = new Set(currentFilters.map(f => f.community)); + const _existingCommunitiesSet = new Set(currentFilters.map(f => f.community)); // Обновляем существующие фильтры и добавляем новые const updatedFilters = []; diff --git a/frontend/src/FilterManager.jsx b/frontend/src/FilterManager.jsx index 679bed9..b8125a5 100644 --- a/frontend/src/FilterManager.jsx +++ b/frontend/src/FilterManager.jsx @@ -79,7 +79,7 @@ function CommunityAutocomplete({ label, value, onChange, communities = [], requi // 3. Если ввели "65001:120", ищем "120" в справочнике if (search.includes(':')) { const searchParts = search.split(':'); - const searchAS = searchParts[0]; + const _searchAS = searchParts[0]; const searchNum = searchParts[1]; // Точное совпадение числовой части @@ -246,7 +246,7 @@ function CommunityAutocomplete({ label, value, onChange, communities = [], requi {Object.entries(groupedCommunities).map(([category, items]) => (
{category}
- {items.map((c, idx) => { + {items.map((c, _idx) => { const globalIdx = filteredCommunities.indexOf(c); return (
setSuccess(''), 3000); return; } - } catch (legacyError) { + } catch (_legacyError) { // Фильтров по legacyId тоже нет - это нормально console.log('No legacy filters found'); } @@ -1165,7 +1165,7 @@ function FilterManager() { } }; - const handleLoadServerConfig = async (serverId, legacyId = null) => { + const _handleLoadServerConfig = async (serverId, legacyId = null) => { try { const response = await api.get(`/server-configs/${serverId}`); const config = response.data?.config; @@ -1192,7 +1192,7 @@ function FilterManager() { setTimeout(() => setSuccess(''), 3000); return; } - } catch (legacyError) { + } catch (_legacyError) { console.log('No legacy config found'); } } @@ -1299,7 +1299,7 @@ function FilterManager() { } }; - const handleDeployToAllServers = async () => { + const _handleDeployToAllServers = async () => { if (!selectedServer) { setError('Сначала выберите сервер с фильтрами для развертывания.'); return; @@ -1521,7 +1521,7 @@ function FilterManager() { setEditServerModalOpen(false); setSuccess('Сервер успешно обновлён!'); setTimeout(() => setSuccess(''), 3000); - } catch (error) { + } catch (_error) { setEditServerError('Не удалось сохранить изменения.'); } }; @@ -1576,7 +1576,7 @@ function FilterManager() { await navigator.clipboard.writeText(config); setSuccess('Конфигурация скопирована в буфер обмена!'); setTimeout(() => setSuccess(''), 3000); - } catch (err) { + } catch (_err) { setError('Не удалось скопировать конфигурацию'); } } else { @@ -1590,7 +1590,7 @@ function FilterManager() { document.execCommand('copy'); setSuccess('Конфигурация скопирована в буфер обмена!'); setTimeout(() => setSuccess(''), 3000); - } catch (err) { + } catch (_err) { setError('Не удалось скопировать конфигурацию'); } document.body.removeChild(textArea); @@ -1598,7 +1598,7 @@ function FilterManager() { }; // Функция для экспорта конфигурации в S3 - const exportConfigToS3 = async () => { + const _exportConfigToS3 = async () => { setLoading(true); try { const response = await api.post(`/filters/export-config`); @@ -2561,7 +2561,7 @@ function FilterManager() { try { await api.post(`/server-configs`, { servers: updatedServers }); setServers(updatedServers); - } catch (error) { + } catch (_error) { setError('Не удалось обновить статус сервера'); } }} diff --git a/frontend/src/FirewallPage.jsx b/frontend/src/FirewallPage.jsx index 6ba84ef..ad281a8 100644 --- a/frontend/src/FirewallPage.jsx +++ b/frontend/src/FirewallPage.jsx @@ -250,7 +250,7 @@ export default function FirewallPage() { const res = await api.get('/servers'); const list = Array.isArray(res.data) ? res.data : []; if (!cancelled) setServers(list); - } catch (e) { + } catch (_e) { if (!cancelled) setError('Не удалось загрузить список серверов.'); } finally { if (!cancelled) setLoadingServers(false); @@ -319,7 +319,7 @@ export default function FirewallPage() { [s.subnet]: { org: info.org, country: info.country, city: info.city }, })); } - } catch (_) { + } catch { if (!cancelled) setIpInfoMap((prev) => ({ ...prev, [s.subnet]: null })); } if (i < slice.length - 1) await new Promise((r) => setTimeout(r, 180)); diff --git a/frontend/src/GraphView.jsx b/frontend/src/GraphView.jsx index 08eea2c..c5fa7cc 100644 --- a/frontend/src/GraphView.jsx +++ b/frontend/src/GraphView.jsx @@ -5,7 +5,6 @@ import { MiniMap, useEdgesState, useNodesState, - addEdge, MarkerType, Handle, Position, @@ -224,7 +223,7 @@ function GraphView({ servers, connections, onCreateConnection, pingMap }) { }); setSelectedNodeId(nodeId); setTimeout(() => setSelectedNodeId(null), 2000); - } catch {} + } catch { /* no-op */ } }, []); // Сброс раскладки: очищаем localStorage и пересоздаём ноды @@ -232,7 +231,7 @@ function GraphView({ servers, connections, onCreateConnection, pingMap }) { if (typeof window !== 'undefined') { try { window.localStorage.removeItem(STORAGE_KEY); - } catch {} + } catch { /* no-op */ } } setNodes((current) => { return current.map((n, i) => ({ @@ -244,7 +243,7 @@ function GraphView({ servers, connections, onCreateConnection, pingMap }) { if (instanceRef.current) { try { instanceRef.current.fitView({ padding: 0.2 }); - } catch {} + } catch { /* no-op */ } } }, 100); }, [setNodes, computeGridPosition]); @@ -378,7 +377,7 @@ function GraphView({ servers, connections, onCreateConnection, pingMap }) { requestAnimationFrame(() => { try { inst.fitView({ padding: 0.2, includeHiddenNodes: true, minZoom: 0.2, maxZoom: 2 }); - } catch {} + } catch { /* no-op */ } }); }, []); @@ -389,7 +388,7 @@ function GraphView({ servers, connections, onCreateConnection, pingMap }) { const t = setTimeout(() => { try { i.fitView({ padding: 0.2, includeHiddenNodes: true, minZoom: 0.2, maxZoom: 2 }); - } catch {} + } catch { /* no-op */ } }, 50); return () => clearTimeout(t); }, [servers, connections]); @@ -403,7 +402,7 @@ function GraphView({ servers, connections, onCreateConnection, pingMap }) { setTimeout(() => { try { instanceRef.current.fitView({ padding: 0.2 }); - } catch {} + } catch { /* no-op */ } }, 60); } }; diff --git a/frontend/src/IPRangesManager.jsx b/frontend/src/IPRangesManager.jsx index 5701671..59f1619 100644 --- a/frontend/src/IPRangesManager.jsx +++ b/frontend/src/IPRangesManager.jsx @@ -52,13 +52,13 @@ function IPRangesManager() { const [originalItems, setOriginalItems] = useState([]); const [etag, setEtag] = useState(''); const [lastModified, setLastModified] = useState(''); - const [contentLength, setContentLength] = useState(null); + const [, setContentLength] = useState(null); const [newItem, setNewItem] = useState({ ipRange: '', community: '' }); - const [newInvalid, setNewInvalid] = useState({ ipRange: false, community: false }); + const [, setNewInvalid] = useState({ ipRange: false, community: false }); const [error, setError] = useState(''); const [success, setSuccess] = useState(''); const [searchTerm, setSearchTerm] = useState(''); - const searchTimer = useRef(null); + const _searchTimer = useRef(null); const didInit = useRef(false); const [editingValue, setEditingValue] = useState(''); const [editModalOpen, setEditModalOpen] = useState(false); @@ -82,7 +82,7 @@ function IPRangesManager() { try { const res = await api.get(`/communities`); setCommunities(Array.isArray(res.data) ? res.data : []); - } catch (e) { + } catch (_e) { // тихо игнорируем } })(); @@ -268,7 +268,7 @@ function IPRangesManager() { }); }; - const duplicateItem = (item) => { + const _duplicateItem = (item) => { setItems([...items, { ...item, ipRange: `${item.ipRange.split('/')[0]}/24` }]); window.notify?.success?.('Запись продублирована'); }; @@ -316,19 +316,19 @@ function IPRangesManager() { return { added, removed, changed }; }; - const [showDiff, setShowDiff] = useState(false); + const [_showDiff, setShowDiff] = useState(false); const [diff, setDiff] = useState({ added: [], removed: [], changed: [] }); const [confirmSaveOpen, setConfirmSaveOpen] = useState(false); const [historyOpen, setHistoryOpen] = useState(false); const [clearCommunitiesOpen, setClearCommunitiesOpen] = useState(false); // Аналитика - const [analyzeOpen, setAnalyzeOpen] = useState(false); - const [analyzeConfirmOpen, setAnalyzeConfirmOpen] = useState(false); - const [analysisResultsOpen, setAnalysisResultsOpen] = useState(false); + const [_analyzeOpen, _setAnalyzeOpen] = useState(false); + const [_analyzeConfirmOpen, _setAnalyzeConfirmOpen] = useState(false); + const [_analysisResultsOpen, setAnalysisResultsOpen] = useState(false); const [analysisPreview, setAnalysisPreview] = useState(null); - const [overwriteConfirmOpen, setOverwriteConfirmOpen] = useState(false); - const [analyzeFilters, setAnalyzeFilters] = useState({ community: '', minMask: 0, maxMask: 32, type: 'any', supernet16: false }); + const [_overwriteConfirmOpen, setOverwriteConfirmOpen] = useState(false); + const [analyzeFilters, _setAnalyzeFilters] = useState({ community: '', minMask: 0, maxMask: 32, type: 'any', supernet16: false }); const handlePreviewDiff = () => { const valid = items.filter(i => i != null && isValidCidr(i.ipRange) && isValidCommunity(i.community)) @@ -426,7 +426,7 @@ function IPRangesManager() { }; // Drag&Drop импорт (оставим как быстрый путь) - const onDropImport = async (e) => { + const _onDropImport = async (e) => { e.preventDefault(); const file = e.dataTransfer?.files?.[0]; if (!file) return; @@ -582,7 +582,7 @@ function IPRangesManager() { return 'public'; }; - const formatBigInt = (n) => { + const _formatBigInt = (n) => { try { const s = (n ?? 0n).toString(); return s.replace(/\B(?=(\d{3})+(?!\d))/g, ' '); @@ -591,7 +591,7 @@ function IPRangesManager() { } }; - const buildAnalysisConfirmText = () => { + const _buildAnalysisConfirmText = () => { const parts = []; parts.push(`Community: ${analyzeFilters.community || 'не выбрано'}`); parts.push(`Маска: /${analyzeFilters.minMask}..../${analyzeFilters.maxMask}`); @@ -705,7 +705,7 @@ function IPRangesManager() { return Array.from(new Set(out)); }; - const runAnalysis = () => { + const _runAnalysis = () => { const community = String(analyzeFilters.community || '').trim(); if (!community) { window.notify?.error?.('Выберите community'); return; } // исходные для community @@ -744,7 +744,7 @@ function IPRangesManager() { setAnalysisResultsOpen(true); }; - const applyAnalysisToItems = () => { + const _applyAnalysisToItems = () => { if (!analysisPreview) return; const { community, afterCidrs, unchangedCidrs } = analysisPreview; const keptOthers = items.filter(i => i != null && i.community !== community); @@ -770,10 +770,14 @@ function IPRangesManager() { window.notify?.success?.('Диапазоны перезаписаны по результатам анализа. Не забудьте сохранить изменения.'); }; - const exportAnalysisCsv = () => { + const _exportAnalysisCsv = () => { + if (!analysisPreview) return; + const { community, afterCidrs } = analysisPreview; + const totalAddr = afterCidrs?.reduce((s, c) => s + countAddresses(parseMask(c)), 0n) ?? 0n; const header = ['community', 'ranges', 'totalAddresses']; - const lines = [header, ...analysisResult.map(r => [r.community, r.ranges, r.totalAddresses.toString()])] - .map(r => r.map(x => `"${(x ?? '').toString().replace(/"/g, '""')}"`).join(',')) + const rows = [[community, String(afterCidrs?.length ?? 0), totalAddr.toString()]]; + const lines = [header, ...rows] + .map(r => (Array.isArray(r) ? r : []).map(x => `"${(x ?? '').toString().replace(/"/g, '""')}"`).join(',')) .join('\n'); const blob = new Blob([lines], { type: 'text/csv;charset=utf-8;' }); const url = URL.createObjectURL(blob); diff --git a/frontend/src/InterfaceSpeedTest.jsx b/frontend/src/InterfaceSpeedTest.jsx index 6cee9cf..eb4aad9 100644 --- a/frontend/src/InterfaceSpeedTest.jsx +++ b/frontend/src/InterfaceSpeedTest.jsx @@ -83,7 +83,7 @@ export default function InterfaceSpeedTest() { return () => controller.abort(); }, [notify]); - const gateways = useMemo( + const _gateways = useMemo( () => networkConfig?.gateways && Array.isArray(networkConfig.gateways) ? networkConfig.gateways diff --git a/frontend/src/MikrotikTools.jsx b/frontend/src/MikrotikTools.jsx index 12aa5a8..25f6f58 100644 --- a/frontend/src/MikrotikTools.jsx +++ b/frontend/src/MikrotikTools.jsx @@ -82,7 +82,7 @@ function MikrotikTools() { [networkConfig] ); - const handleSelectGatewayMeta = (meta) => { + const _handleSelectGatewayMeta = (meta) => { setGatewayMeta(meta); // Если цель не задана — подставляем IP gateway как target if (!target && meta && meta.ip) { @@ -214,7 +214,7 @@ function MikrotikTools() { [jumphostServers, serverId] ); - const interfacesForServer = useMemo(() => { + const _interfacesForServer = useMemo(() => { if (!serverId || !interfaces.length) return []; const ids = new Set( [serverId, currentServer?.id, currentServer?.ip, currentServer?.dns] @@ -391,7 +391,7 @@ function MikrotikTools() { {/* Цели — карточки в стиле сервера/gateway */}
- {POPULAR_TARGETS.map(({ id: tid, name, target: tgt, Icon, color }) => { + {POPULAR_TARGETS.map(({ id: tid, name, target: tgt, Icon: _Icon, color }) => { const isSelected = target === tgt; const bgClass = color === 'red' ? 'bg-red-lt' : color === 'blue' ? 'bg-blue-lt' : 'bg-orange-lt'; const textClass = color === 'red' ? 'text-red' : color === 'blue' ? 'text-blue' : 'text-orange'; @@ -404,7 +404,7 @@ function MikrotikTools() { >
- + <_Icon size={20} stroke={1.5} />
{name}
diff --git a/frontend/src/NetworkConfigManager.jsx b/frontend/src/NetworkConfigManager.jsx index 5dc500b..fa9baca 100644 --- a/frontend/src/NetworkConfigManager.jsx +++ b/frontend/src/NetworkConfigManager.jsx @@ -468,7 +468,7 @@ function NetworkConfigManager() { if (searchTerm) { const term = searchTerm.toLowerCase(); result = result.filter(p => { - const server = getServerInfo(p.serverId); + const _server = getServerInfo(p.serverId); return ( p.name?.toLowerCase().includes(term) || p.cidr?.toLowerCase().includes(term) || @@ -913,7 +913,7 @@ function NetworkConfigManager() { return; } - const invalidPairs = templateServerPairs.filter((pair, index) => { + const invalidPairs = templateServerPairs.filter((pair, _index) => { if (!pair.server1 || !pair.server2) { return true; } @@ -1274,7 +1274,7 @@ function NetworkConfigManager() { }; // === Генерация IP из пула === - const generateIpFromPool = (cidr, usedIps, excludeIp = null, forRemote = false, pairedLocalIp = null) => { + const generateIpFromPool = (cidr, usedIps, _excludeIp = null, forRemote = false, pairedLocalIp = null) => { // Парсим CIDR (например, "10.10.0.0/24") const [network, prefixLength] = cidr.split('/'); if (!network || !prefixLength) { @@ -1650,7 +1650,7 @@ function NetworkConfigManager() { }; // === CRUD для IPSec Passwords === - const handleAddIpsecPassword = () => { + const _handleAddIpsecPassword = () => { setEditingIpsecPassword({ name: '', password: '', description: '' }); setIpsecPasswordModalMode('add'); setIpsecPasswordModalOpen(true); @@ -2021,7 +2021,7 @@ function NetworkConfigManager() { }; // === Генерация кода MikroTik для рекурсивных маршрутов === - const generateMikrotikRecursiveRoutes = async () => { + const _generateMikrotikRecursiveRoutes = async () => { const recursiveGateways = (config.gateways || []).filter(gw => gw.type === 'recursive'); const blocks = []; @@ -2073,7 +2073,7 @@ function NetworkConfigManager() { } // Обрабатываем каждый родительский gateway - parentGatewaysList.forEach((parentRef, parentIndex) => { + parentGatewaysList.forEach((parentRef, _parentIndex) => { const parent = getParentGateway(parentRef.id); if (!parent) { @@ -2264,7 +2264,7 @@ function NetworkConfigManager() { if (obj.t === 'log') console.log('[MikroTik apply]', obj.msg, obj); else if (obj.t === 'result') finalData = obj; else if (obj.t === 'error') finalData = { ok: false, error: obj.error, mikrotikRequests: obj.mikrotikRequests }; - } catch (_) {} + } catch { /* no-op */ } } } if (buffer.trim()) { @@ -2272,7 +2272,7 @@ function NetworkConfigManager() { const obj = JSON.parse(buffer); if (obj.t === 'result') finalData = obj; else if (obj.t === 'error') finalData = { ok: false, error: obj.error, mikrotikRequests: obj.mikrotikRequests }; - } catch (_) {} + } catch { /* no-op */ } } data = finalData || {}; } else { @@ -2403,7 +2403,7 @@ function NetworkConfigManager() { let validRoutesCount = 0; - parentGatewaysList.forEach((parentRef, parentIndex) => { + parentGatewaysList.forEach((parentRef, _parentIndex) => { const parent = getParentGateway(parentRef.id); if (!parent) { @@ -2542,7 +2542,7 @@ function NetworkConfigManager() { try { document.execCommand('copy'); notify.success('Код скопирован в буфер обмена!'); - } catch (err) { + } catch (_err) { notify.error('Не удалось скопировать код'); } document.body.removeChild(textArea); @@ -2589,7 +2589,7 @@ function NetworkConfigManager() { }, [generatedMikrotikCode]); // === Получение названия типа блока === - const getBlockTypeLabel = (type) => { + const _getBlockTypeLabel = (type) => { switch (type) { case 'recursive-routes': return 'Рекурсивные маршруты'; @@ -2967,11 +2967,11 @@ function NetworkConfigManager() { // === Рендер карточки Gateway === const renderGatewayCard = (gateway) => { const server = getServerInfo(gateway.serverId); - const provider = server?.provider || ''; + const _provider = server?.provider || ''; const serverCountry = server?.country || ''; const gatewayCountry = gateway.country || ''; const displayCountry = gatewayCountry || serverCountry; - const gatewayType = GATEWAY_TYPES.find(t => t.value === gateway.type) || GATEWAY_TYPES[0]; + const _gatewayType = GATEWAY_TYPES.find(t => t.value === gateway.type) || GATEWAY_TYPES[0]; // Поддержка старого формата (parentGatewayId) и нового (parentGateways) const parentGatewaysList = gateway.type === 'recursive' @@ -3135,7 +3135,7 @@ function NetworkConfigManager() { const interfaceTypeColor = getInterfaceTypeColor(iface.type); // Определяем отображаемый IP для зеленого блока (remote IP другого сервера) - const displayIp = displayRemoteIp !== '—' ? displayRemoteIp : displayLocalIp; + const _displayIp = displayRemoteIp !== '—' ? displayRemoteIp : displayLocalIp; return (
@@ -5531,7 +5531,7 @@ function NetworkConfigManager() {
{GATEWAY_TEMPLATES.map(template => { // Генерируем предпросмотр IP адресов если есть базовый IP и сервер - const previewIps = template.gateways.map((gw, idx) => { + const previewIps = template.gateways.map((gw, _idx) => { // Для прямых gateways показываем IP сервера, если выбран if (gw.type === 'direct' && templateServerId) { const server = getServerInfo(templateServerId); @@ -5709,7 +5709,7 @@ function NetworkConfigManager() {
{(gw.parentGateways || []).map((parent, parentIndex) => { - const parentInfo = getParentGateway(parent.id, templateGateways); + const _parentInfo = getParentGateway(parent.id, templateGateways); return (
diff --git a/frontend/src/NetworkMapDashboard.jsx b/frontend/src/NetworkMapDashboard.jsx index 033ce8d..6ec3df0 100644 --- a/frontend/src/NetworkMapDashboard.jsx +++ b/frontend/src/NetworkMapDashboard.jsx @@ -309,7 +309,7 @@ export default function NetworkMapDashboard() { * Если передать force=true, принудительно выполняет новый speed-test (игнорируя кеш на бэкенде). */ const requestSpeeds = useCallback(async (options = {}) => { - const { connections: targetConnections, force = false } = options; + const { connections: targetConnections, force: _force = false } = options; const sourceConnections = Array.isArray(targetConnections) && targetConnections.length > 0 ? targetConnections : connections; const withSpeed = sourceConnections.filter( diff --git a/frontend/src/NetworkMapUnifi.jsx b/frontend/src/NetworkMapUnifi.jsx index 40dd66a..3e56d44 100644 --- a/frontend/src/NetworkMapUnifi.jsx +++ b/frontend/src/NetworkMapUnifi.jsx @@ -213,10 +213,10 @@ export default function NetworkMapUnifi({ try { const raw = window.localStorage.getItem(STORAGE_KEY); if (raw) saved = JSON.parse(raw) || {}; - } catch {} + } catch { /* no-op */ } const initial = computeInitialLayout(servers, size); const merged = {}; - servers.forEach((s, i) => { + servers.forEach((s, _i) => { const ip = String(s.ip); if (saved[ip] && typeof saved[ip].x === 'number' && typeof saved[ip].y === 'number') { merged[ip] = { x: saved[ip].x, y: saved[ip].y }; @@ -227,11 +227,11 @@ export default function NetworkMapUnifi({ setPositions(merged); }, [servers.length, size.w, size.h]); - const savePositions = useCallback((next) => { + const _savePositions = useCallback((next) => { setPositions(next); try { window.localStorage.setItem(STORAGE_KEY, JSON.stringify(next)); - } catch {} + } catch { /* no-op */ } }, []); const getSvgCoords = useCallback((clientX, clientY) => { @@ -255,7 +255,7 @@ export default function NetworkMapUnifi({ const next = { ...prev, [drag.nodeId]: { x: drag.startX + (p.x - drag.mouseX), y: drag.startY + (p.y - drag.mouseY) } }; try { window.localStorage.setItem(STORAGE_KEY, JSON.stringify(next)); - } catch {} + } catch { /* no-op */ } return next; }); }; @@ -273,7 +273,7 @@ export default function NetworkMapUnifi({ setPositions(initial); try { window.localStorage.setItem(STORAGE_KEY, JSON.stringify(initial)); - } catch {} + } catch { /* no-op */ } }, [servers, size]); const onNodeMouseDown = useCallback( diff --git a/frontend/src/OspfToolsPage.jsx b/frontend/src/OspfToolsPage.jsx index eb4099b..ed8b81c 100644 --- a/frontend/src/OspfToolsPage.jsx +++ b/frontend/src/OspfToolsPage.jsx @@ -68,7 +68,7 @@ function buildOspfTemplatesFromRouterResults(routerResults = [], servers = []) { interfaceName, area, cost, - active: !Boolean(tpl?.disabled), + active: !tpl?.disabled, }); }); }); @@ -116,7 +116,7 @@ function buildOptimizerProbabilityMap(routeOptimizerData, servers = []) { uniqueIface.push(candidate); }); - uniqueIface.forEach((candidate, idx) => { + uniqueIface.forEach((candidate, _idx) => { const iface = String(candidate?.interfaceName || '').trim(); const hintKey = `${identity.key}::${iface.toUpperCase()}`; const probabilityOptimal = Number(candidate?.probabilityOptimal || 0); diff --git a/frontend/src/PingServicesManager.jsx b/frontend/src/PingServicesManager.jsx index 218dd98..c11a60d 100644 --- a/frontend/src/PingServicesManager.jsx +++ b/frontend/src/PingServicesManager.jsx @@ -308,12 +308,12 @@ export default function PingServicesManager() { ) : ( filtered.map((item) => { - const iconInfo = getIconById(item.icon); + const { Icon } = getIconById(item.icon); return ( - + {item.id} diff --git a/frontend/src/ServerManager.jsx b/frontend/src/ServerManager.jsx index 4ecccab..f6b92eb 100644 --- a/frontend/src/ServerManager.jsx +++ b/frontend/src/ServerManager.jsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useRef, useMemo } from 'react'; +import { useState, useEffect, useMemo } from 'react'; import api from './lib/api.js'; import ConfirmDialog from './components/ConfirmDialog.jsx'; import ConfirmModal from './components/ConfirmModal.jsx'; @@ -501,7 +501,7 @@ function ServerManager() { return `${urlSettings.baseUrl}?${params.toString()}`; }; - const handleGenerateLink = (server) => { + const _handleGenerateLink = (server) => { setLinkGeneratorServer(server); setLinkGeneratorOpen(true); }; @@ -511,7 +511,7 @@ function ServerManager() { setLinkGeneratorServer(null); }; - const applyPreset = (name) => { + const _applyPreset = (name) => { const p = presets[name]; if (!p) return; setUrlSettings(prev => ({ ...prev, ...p })); @@ -529,7 +529,7 @@ function ServerManager() { try { document.execCommand('copy'); notify.success('Скопировано в буфер обмена!'); - } catch (err) { + } catch (_err) { notify.error('Не удалось скопировать'); } document.body.removeChild(textArea); @@ -541,14 +541,14 @@ function ServerManager() { await navigator.clipboard.writeText(text); notify.success('Скопировано в буфер обмена!'); return; - } catch (err) { + } catch (_err) { // fallback } } fallbackCopyToClipboard(text); }; - const quickCopyLink = async (server) => { + const _quickCopyLink = async (server) => { const url = generateServerUrl(server); await copyToClipboard(url); }; diff --git a/frontend/src/UptimeMonitorPage.jsx b/frontend/src/UptimeMonitorPage.jsx index d641b91..f129ac9 100644 --- a/frontend/src/UptimeMonitorPage.jsx +++ b/frontend/src/UptimeMonitorPage.jsx @@ -34,14 +34,14 @@ function loadHistoryFromStorage() { if (!raw) return {}; const parsed = JSON.parse(raw); if (parsed && typeof parsed === 'object') return parsed; - } catch (_) {} + } catch { /* no-op */ } return {}; } function saveHistoryToStorage(historyMap) { try { localStorage.setItem(STORAGE_KEY, JSON.stringify(historyMap)); - } catch (_) {} + } catch { /* no-op */ } } /** Группировка истории по дням: массив { day, dayLabel, incidents } */ diff --git a/frontend/src/components/BulkActionsBar.jsx b/frontend/src/components/BulkActionsBar.jsx index b9a2e1b..e6d59d3 100644 --- a/frontend/src/components/BulkActionsBar.jsx +++ b/frontend/src/components/BulkActionsBar.jsx @@ -8,14 +8,14 @@ import FloatingBulkActionsBar from './FloatingBulkActionsBar.jsx'; */ function BulkActionsBar({ selectedCount = 0, - totalCount = 0, - onSelectAll = () => {}, + totalCount: _totalCount = 0, + onSelectAll: _onSelectAll = () => {}, onDeselectAll = () => {}, onDelete = null, onEdit = null, onExport = null, customActions = [], - className = '', + className: _className = '', }) { const actions = []; diff --git a/frontend/src/components/DataTable.jsx b/frontend/src/components/DataTable.jsx index c8e88c2..c648c0e 100644 --- a/frontend/src/components/DataTable.jsx +++ b/frontend/src/components/DataTable.jsx @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import React from 'react'; import { IconArrowUp, IconArrowDown, IconEdit, IconTrash, IconCopy, IconCheck, IconX } from '@tabler/icons-react'; import TableSkeleton, { TableEmpty } from './TableSkeleton.jsx'; import EmptyState from './EmptyState.jsx'; diff --git a/frontend/src/components/ErrorBoundary.jsx b/frontend/src/components/ErrorBoundary.jsx index 08112f7..df02074 100644 --- a/frontend/src/components/ErrorBoundary.jsx +++ b/frontend/src/components/ErrorBoundary.jsx @@ -16,7 +16,7 @@ class ErrorBoundary extends Component { }; } - static getDerivedStateFromError(error) { + static getDerivedStateFromError(_error) { return { hasError: true }; } @@ -93,7 +93,7 @@ class ErrorBoundary extends Component { {this.state.error?.message || 'Что-то пошло не так. Попробуйте обновить страницу.'}

- {process.env.NODE_ENV === 'development' && this.state.errorInfo && ( + {import.meta.env.DEV && this.state.errorInfo && (

Детали ошибки (только в dev режиме)

diff --git a/frontend/src/components/HistoryModal.jsx b/frontend/src/components/HistoryModal.jsx index 4c67888..74b8098 100644 --- a/frontend/src/components/HistoryModal.jsx +++ b/frontend/src/components/HistoryModal.jsx @@ -40,7 +40,7 @@ export default function HistoryModal({ resource, show, onClose, onRolledBack }) notify.success('Откат выполнен успешно'); onRolledBack?.(res.data || {}); onClose?.(); - } catch (e) { + } catch (_e) { notify.error('Не удалось выполнить откат'); } finally { setLoading(false); diff --git a/frontend/src/components/IconPicker.jsx b/frontend/src/components/IconPicker.jsx index 856ae5c..8ebfaab 100644 --- a/frontend/src/components/IconPicker.jsx +++ b/frontend/src/components/IconPicker.jsx @@ -1,3 +1,4 @@ +/* eslint-disable react-refresh/only-export-components */ import { useState, useMemo, useRef, useEffect } from 'react'; import BRAND_ICONS, { getIconById } from '../lib/brandIcons.js'; import { IconSearch, IconMinus } from '@tabler/icons-react'; diff --git a/frontend/src/components/MobileCardView.jsx b/frontend/src/components/MobileCardView.jsx index d8b9f7e..d4ed9db 100644 --- a/frontend/src/components/MobileCardView.jsx +++ b/frontend/src/components/MobileCardView.jsx @@ -19,12 +19,12 @@ function MobileCardView({ // минимальная дистанция свайпа в px const minSwipeDistance = 50 - const onTouchStart = (e, item) => { + const onTouchStart = (e, _item) => { setTouchEnd(null) setTouchStart(e.targetTouches[0].clientX) } - const onTouchMove = (e, item) => { + const onTouchMove = (e, _item) => { setTouchEnd(e.targetTouches[0].clientX) } diff --git a/frontend/src/components/NotifyProvider.jsx b/frontend/src/components/NotifyProvider.jsx index 975d860..e0ee3bd 100644 --- a/frontend/src/components/NotifyProvider.jsx +++ b/frontend/src/components/NotifyProvider.jsx @@ -1,3 +1,4 @@ +/* eslint-disable react-refresh/only-export-components */ import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'; import { IconCheck, IconAlertTriangle, IconInfoCircle, IconX } from '@tabler/icons-react'; @@ -98,7 +99,7 @@ export function notifyMutationSuccess(message, details) { if (forceToast && typeof window !== 'undefined' && window.notify?.success) { window.notify.success(text, details); } - } catch {} + } catch { /* no-op */ } } function colorByType(type) { diff --git a/frontend/src/components/SavedFilters.jsx b/frontend/src/components/SavedFilters.jsx index 1baaded..c418950 100644 --- a/frontend/src/components/SavedFilters.jsx +++ b/frontend/src/components/SavedFilters.jsx @@ -14,7 +14,7 @@ function SavedFilters({ const [savedFilters, setSavedFilters] = useState([]) const [showSaveDialog, setShowSaveDialog] = useState(false) const [filterName, setFilterName] = useState('') - const [showList, setShowList] = useState(false) + const [_showList, setShowList] = useState(false) const storageKey = `savedFilters_${pageKey}` diff --git a/frontend/src/components/SettingsModal.jsx b/frontend/src/components/SettingsModal.jsx index 9407e90..70fa3d4 100644 --- a/frontend/src/components/SettingsModal.jsx +++ b/frontend/src/components/SettingsModal.jsx @@ -96,7 +96,7 @@ export default function SettingsModal({ open, onClose }) { const e = settingsRes?.headers?.etag || settingsRes?.headers?.ETag || ''; setEtag(e ? String(e) : ''); setServersList(Array.isArray(serversRes?.data) ? serversRes.data : []); - } catch (e) { + } catch (_e) { setError('Не удалось загрузить настройки'); } finally { setLoading(false); diff --git a/frontend/src/components/ToastContainer.jsx b/frontend/src/components/ToastContainer.jsx index 7bc1adf..09ad5e0 100644 --- a/frontend/src/components/ToastContainer.jsx +++ b/frontend/src/components/ToastContainer.jsx @@ -1,3 +1,4 @@ +/* eslint-disable react-refresh/only-export-components */ import { useState, useEffect, createContext, useContext } from 'react' import { IconCheck, diff --git a/frontend/src/components/TopNStats.jsx b/frontend/src/components/TopNStats.jsx index fa9a236..b60971d 100644 --- a/frontend/src/components/TopNStats.jsx +++ b/frontend/src/components/TopNStats.jsx @@ -7,7 +7,7 @@ import { IconTrophy, IconMapPin, IconCloud, IconHash, IconChartBar } from '@tabl function TopNStats({ data, loading }) { const [topCountries, setTopCountries] = useState([]); const [topProviders, setTopProviders] = useState([]); - const [topCommunities, setTopCommunities] = useState([]); + const [_topCommunities, _setTopCommunities] = useState([]); useEffect(() => { if (!data || !Array.isArray(data.servers)) return; diff --git a/frontend/src/components/ValidatedInput.jsx b/frontend/src/components/ValidatedInput.jsx index ffb22f4..6379734 100644 --- a/frontend/src/components/ValidatedInput.jsx +++ b/frontend/src/components/ValidatedInput.jsx @@ -58,7 +58,7 @@ function ValidatedInput({ message: validationResult.message || '', isValidating: false }) - } catch (error) { + } catch (_error) { setValidationState({ valid: false, message: 'Ошибка валидации', @@ -84,7 +84,7 @@ function ValidatedInput({ // Запускаем валидацию сразу при потере фокуса if (validate && !validateOnChange) { const result = validate(value) - const validationResult = result instanceof Promise ? result.then(r => { + const _validationResult = result instanceof Promise ? result.then(r => { setValidationState({ valid: r.valid, message: r.message || '', diff --git a/frontend/src/components/VirtualizedTable.jsx b/frontend/src/components/VirtualizedTable.jsx index da252be..f81b634 100644 --- a/frontend/src/components/VirtualizedTable.jsx +++ b/frontend/src/components/VirtualizedTable.jsx @@ -1,4 +1,4 @@ -import { useRef, useMemo } from 'react'; +import { useRef } from 'react'; import { useVirtualizer } from '@tanstack/react-virtual'; import { IconArrowUp, IconArrowDown, IconEdit, IconTrash, IconCopy } from '@tabler/icons-react'; import TableSkeleton from './TableSkeleton.jsx'; diff --git a/frontend/src/components/WsUpdateModal.jsx b/frontend/src/components/WsUpdateModal.jsx index c49bbdf..9e47ed5 100644 --- a/frontend/src/components/WsUpdateModal.jsx +++ b/frontend/src/components/WsUpdateModal.jsx @@ -64,12 +64,12 @@ function WsUpdateModal({ show, url, onClose }) { ws.onerror = () => setStatus('error'); ws.onclose = () => setStatus('closed'); - } catch (e) { + } catch (_e) { setStatus('error'); } return () => { - try { wsRef.current?.close(); } catch {} + try { wsRef.current?.close(); } catch { /* no-op */ } wsRef.current = null; }; }, [show, url]); @@ -96,7 +96,7 @@ function WsUpdateModal({ show, url, onClose }) { const copyLog = async () => { try { await navigator.clipboard.writeText(plainLog); - } catch {} + } catch { /* no-op */ } }; const clearLog = () => { diff --git a/frontend/src/components/server/ServerSidebar.jsx b/frontend/src/components/server/ServerSidebar.jsx index 563782c..fa99f98 100644 --- a/frontend/src/components/server/ServerSidebar.jsx +++ b/frontend/src/components/server/ServerSidebar.jsx @@ -27,7 +27,7 @@ function countryToFlag(isoCode) { return code.replace(/./g, char => String.fromCodePoint(127397 + char.charCodeAt())); } -function FilterSection({ title, icon: Icon, iconColor, expanded, onToggle, children }) { +function FilterSection({ title, icon: _Icon, iconColor, expanded, onToggle, children }) { return (