{value.address};
+}
+
function PathDetails({ title, path }) {
if (!path?.available) return VPN не запущен.
IPv4: {path.ipv4.addresses.join(', ') || 'нет'}
-IPv6: {path.ipv6 || 'нет'}
{path.sites.map((site) => (
{site.label}: {site.stage}{site.httpStatus ? ` · HTTP ${site.httpStatus}` : ''}
@@ -53,12 +80,25 @@ export function ConnectivityDiagnosticsPanel({ open, panelRef, closeRef, onClose
const [result, setResult] = useState(null);
const [status, setStatus] = useState('idle');
const [error, setError] = useState(null);
+ const [customServices, setCustomServices] = useState(readCustomServices);
+ const [adding, setAdding] = useState(false);
+ const [serviceName, setServiceName] = useState('');
+ const [serviceUrl, setServiceUrl] = useState('');
+ const [formError, setFormError] = useState('');
+
+ useEffect(() => {
+ try {
+ localStorage.setItem(CUSTOM_SERVICES_KEY, JSON.stringify(customServices));
+ } catch {
+ // The service still works for this session when browser storage is unavailable.
+ }
+ }, [customServices]);
async function run() {
setStatus('running');
setError(null);
try {
- setResult(await api.diagnostics.connectivity());
+ setResult(await api.diagnostics.connectivity(customServices));
setStatus('ready');
} catch (requestError) {
setError(requestError);
@@ -66,6 +106,28 @@ export function ConnectivityDiagnosticsPanel({ open, panelRef, closeRef, onClose
}
}
+ function addService(event) {
+ event.preventDefault();
+ try {
+ if (customServices.length >= MAX_CUSTOM_DIAGNOSTIC_SERVICES) return;
+ const parsed = new URL(serviceUrl.trim());
+ if (parsed.protocol !== 'https:') throw new Error('Нужен публичный HTTPS-адрес.');
+ setCustomServices((services) => [...services, {
+ id: `custom-${globalThis.crypto?.randomUUID?.() || Date.now()}`,
+ label: serviceName.trim() || parsed.hostname,
+ url: parsed.href,
+ }]);
+ setServiceName('');
+ setServiceUrl('');
+ setFormError('');
+ setAdding(false);
+ } catch (validationError) {
+ setFormError(validationError.message || 'Проверьте адрес.');
+ }
+ }
+
+ const pending = status === 'running';
+ const sites = [...CONNECTIVITY_SITES, ...customServices];
const summary = SUMMARY_COPY[result?.assessment?.summary] || SUMMARY_COPY.inconclusive;
const checkedAt = result?.checkedAt
? new Date(result.checkedAt).toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit', second: '2-digit' })
@@ -90,69 +152,127 @@ export function ConnectivityDiagnosticsPanel({ open, panelRef, closeRef, onClose
>×
Сравнивает внешний IP и доступность сайтов напрямую и через выбранный VPN, не меняя правила. Результаты появятся здесь после ручного запуска.Проверка маршрутов
- Маршруты
+
+
+ Проверить маршруты
+
+
+
+
+ {CONNECTIVITY_IP_SOURCES.map((source) => Источник
+ Напрямую
+ VPN{result?.vpn?.server?.label ? ` · ${result.vpn.server.label}` : ''}
+
+ )}
+ {source.label}
+
+
+ {ipText(result.direct)}
-
- {result.vpn.available ? ipText(result.vpn) : 'VPN выключен'}
-
-
+
+
+
+ {result &&
+
+ {sites.map((site) => {
+ const direct = result?.direct?.sites?.find((item) => item.id === site.id);
+ const vpn = result?.vpn?.sites?.find((item) => item.id === site.id);
+ const custom = site.id.startsWith('custom-');
+ return Сервис
+ Напрямую
+ VPN
+
+ ;
+ })}
+
+ {site.label}
+ {custom && }
+
+
+
+ Технические детали
+
-
: {title}
}
-
+ >{device.ip} : !hasName && Неизвестное устройство}
+
+ {!hasName &&
{
assert.match(rule('.client-state-detail'), /min-height:\s*44px/);
assert.match(rule('.client-duration-toggle'), /min-height:\s*44px/);