Files
bedolaga-cabinet/src/components/admin/remnawave/geoCheckRoute.ts
c0mrade b4e1de6152 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>
2026-08-19 16:01:05 +03:00

56 lines
2.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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');
}