mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-09-13 16:23:08 +00:00
feat(remnawave): GeoCheck ноды в админке — запуск проверки и просмотр отчёта
Кнопка в карточке ноды открывает модалку: выбор маршрута (по умолчанию / IP-адрес / интерфейс) с подсказками из данных ноды, ожидание, затем отчёт картинкой либо тем же отчётом в JSON. Есть копирование JSON, скачивание SVG, повтор проверки и полноэкранный режим. Кнопка показывается только для узлов 3.3.0+ (versions.node) и только для подключённых — иначе админ упирался бы в ошибку панели. Отчёт вставляется как <img src="data:...">, а не сырым SVG в разметку: он несёт встроенный моноширинный шрифт и <style>, на которых держится выравнивание колонок, и санитайзер их выбрасывает. В <img> SVG рисуется в изолированном контексте без скриптов и внешних запросов — и безопаснее, и вернее по виду. Зум и панорамирование — react-zoom-pan-pinch с той же конфигурацией, что в самой панели Remnawave. Своя реализация обязательна: index.html задаёт user-scalable=no, поэтому браузерного пинча нет ни в мобильном вебе, ни в Mini App. Колесо намеренно не масштабирует, иначе прокрутка над отчётом превращается в зум. В отличие от панели стартовый масштаб считается от читаемой ширины: на телефоне «вписано по ширине» превращает отчёт в серую рябь. На 390px это 2.42x с панорамированием, на десктопе — 1x. При смене ширины (полный экран, поворот) масштаб пересчитывается. Опрос задачи с потолком в три минуты — иначе забытая открытой модалка опрашивала бы панель бесконечно. Пока проверка идёт, модалка не закрывается: задача уже поставлена, терять её результат молча хуже. Заодно в AdminRemnawave.tsx осели автоправки biome (import type, Math.pow -> **) — файл всё равно правился, они механические. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
55
src/components/admin/remnawave/geoCheckRoute.ts
Normal file
55
src/components/admin/remnawave/geoCheckRoute.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import type { GeoCheckRequest, NodeInfo } from '@/api/adminRemnawave';
|
||||
|
||||
/** Откуда гнать проверку. */
|
||||
export type GeoCheckRouteMode = 'default' | 'ip' | 'interface';
|
||||
|
||||
/**
|
||||
* Те же правила, что и на бэкенде (`GeocheckRequest`): значение уезжает в
|
||||
* панель и дальше на узел, поэтому форма не должна пропускать мусор. Здесь
|
||||
* проверка нужна не ради безопасности — она ради того, чтобы админ увидел
|
||||
* ошибку сразу, а не через полминуты ожидания.
|
||||
*/
|
||||
const IPV4_RE = /^(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)$/;
|
||||
const IPV6_RE = /^[0-9a-fA-F:]+$/;
|
||||
const INTERFACE_RE = /^[A-Za-z0-9][A-Za-z0-9_.@-]{0,31}$/;
|
||||
|
||||
export function isValidIpAddress(value: string): boolean {
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) return false;
|
||||
if (IPV4_RE.test(trimmed)) return true;
|
||||
// IPv6 без полного разбора: достаточно набора символов и одного «::»-стиля
|
||||
// разделителя. Точный разбор делает панель, здесь важно отсечь опечатки.
|
||||
return trimmed.includes(':') && IPV6_RE.test(trimmed) && !trimmed.includes(':::');
|
||||
}
|
||||
|
||||
export function isValidInterfaceName(value: string): boolean {
|
||||
return INTERFACE_RE.test(value.trim());
|
||||
}
|
||||
|
||||
/** Готово ли значение к отправке в выбранном режиме. */
|
||||
export function isRouteReady(mode: GeoCheckRouteMode, value: string): boolean {
|
||||
if (mode === 'default') return true;
|
||||
if (mode === 'ip') return isValidIpAddress(value);
|
||||
return isValidInterfaceName(value);
|
||||
}
|
||||
|
||||
/** Тело запроса для выбранного режима. */
|
||||
export function buildGeoCheckRequest(mode: GeoCheckRouteMode, value: string): GeoCheckRequest {
|
||||
const trimmed = value.trim();
|
||||
if (mode === 'ip' && trimmed) return { ip: trimmed };
|
||||
if (mode === 'interface' && trimmed) return { interface: trimmed };
|
||||
return {};
|
||||
}
|
||||
|
||||
/** Адреса узла, которые имеет смысл предлагать как исходные. */
|
||||
export function suggestedIps(node: NodeInfo): string[] {
|
||||
const fromPanel = (node.ips ?? []).filter((entry) => entry.status !== 'BLOCKED').map((e) => e.ip);
|
||||
// Адрес самой ноды панель в `ips` не всегда повторяет, а он самый ожидаемый.
|
||||
const all = [node.address, ...fromPanel].filter(Boolean);
|
||||
return Array.from(new Set(all));
|
||||
}
|
||||
|
||||
/** Сетевые интерфейсы узла; служебный loopback предлагать бессмысленно. */
|
||||
export function suggestedInterfaces(node: NodeInfo): string[] {
|
||||
return (node.system?.info?.networkInterfaces ?? []).filter((name) => name !== 'lo');
|
||||
}
|
||||
Reference in New Issue
Block a user