From debe26ae359545fead38bf087b069e9defecec26 Mon Sep 17 00:00:00 2001 From: c0mrade Date: Wed, 19 Aug 2026 17:23:30 +0300 Subject: [PATCH] =?UTF-8?q?fix(geocheck):=20=D1=81=D0=BA=D0=B2=D0=BE=D0=B7?= =?UTF-8?q?=D0=BD=D1=8B=D0=B5=20=D0=BA=D0=BB=D0=B8=D0=BA=D0=B8=20=D1=87?= =?UTF-8?q?=D0=B5=D1=80=D0=B5=D0=B7=20=D0=BF=D0=BE=D1=80=D1=82=D0=B0=D0=BB?= =?UTF-8?q?=20=D0=B8=20safe-=D0=B7=D0=BE=D0=BD=D1=8B=20=D0=B2=20=D0=BF?= =?UTF-8?q?=D0=BE=D0=BB=D0=BD=D0=BE=D1=8D=D0=BA=D1=80=D0=B0=D0=BD=D0=BD?= =?UTF-8?q?=D0=BE=D0=BC=20=D1=80=D0=B5=D0=B6=D0=B8=D0=BC=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Три поломки, замеченные на dev. Клики внутри модалки срабатывали и на том, что под ней. Модалка рендерилась внутри `
` карточки ноды: портал уносит её в document.body только по DOM, а события React прогоняет по дереву компонентов, поэтому каждый клик всплывал в обработчик аккордеона. Теперь модалка — сосед кликабельного блока, а не его потомок. Проверено A/B на настоящей модалке: в старой структуре клик по вкладке внутри модалки поднимал счётчик карточки до 1, в новой остаётся 0. По существу правка в 6 строк, остальное в дифе — переотступы от обёртки. В полноэкранном режиме отступы обнулялись целиком, и в Mini App панель инструментов уезжала под шапку Telegram, а кнопки зума — под home indicator. Теперь safe-зоны соблюдаются и в полноэкранном режиме, просто без лишнего сантиметра. Замер при отступах 96/34 на экране 844: диалог занимает 96..810, низ зум-бара на 784 — оба внутри безопасной зоны, было наоборот. Высота диалога считалась от `100dvh` мимо safe-зон, из-за чего он мог вылезти за них даже в обычном режиме. Заменено на `max-h-full`: отступы контейнера уже урезали доступное место. Заодно в полноэкранном режиме шапка ужата в одну строку с именем ноды — вертикальное место ровно то, ради чего его включают. Co-Authored-By: Claude Opus 5 (1M context) --- .../admin/remnawave/GeoCheckModal.tsx | 50 +- src/pages/AdminRemnawave.tsx | 479 +++++++++--------- 2 files changed, 280 insertions(+), 249 deletions(-) diff --git a/src/components/admin/remnawave/GeoCheckModal.tsx b/src/components/admin/remnawave/GeoCheckModal.tsx index a416723..6060216 100644 --- a/src/components/admin/remnawave/GeoCheckModal.tsx +++ b/src/components/admin/remnawave/GeoCheckModal.tsx @@ -65,11 +65,20 @@ export function GeoCheckModal({ node, onClose }: GeoCheckModalProps) { return createPortal(
-
+ {/* В полноэкранном режиме шапка ужимается в одну строку: место по + вертикали — ровно то, ради чего его и включают. */} +
- - + +
-

- {t('admin.remnawave.geoCheck.title', 'GeoCheck')} +

+ {fullscreen ? node.name : t('admin.remnawave.geoCheck.title', 'GeoCheck')}

-

{node.name}

+ {!fullscreen &&

{node.name}

}
{!isRunning && ( diff --git a/src/pages/AdminRemnawave.tsx b/src/pages/AdminRemnawave.tsx index 81f33db..2d1eddf 100644 --- a/src/pages/AdminRemnawave.tsx +++ b/src/pages/AdminRemnawave.tsx @@ -201,261 +201,266 @@ function NodeCard({ node, providerName, realtime, onAction, isLoading }: NodeCar ? 'text-warning-400' : 'text-dark-400'; + // Модалка — сосед кликабельного блока, а не его потомок: портал уносит её + // в document.body только по DOM, а события React прогоняет по дереву + // компонентов, и клики внутри неё всплывали бы в onClick карточки. return ( -
setExpanded((v) => !v) : undefined} - > - {/* Identity + actions */} -
-
- - - - {node.users_online ?? 0} - - - {getCountryFlag(node.country_code)} - -

{node.name}

- {(providerLabel || providerFavicon) && ( - - {providerFavicon && ( - { - e.currentTarget.style.display = 'none'; - }} - /> - )} - {providerLabel && {providerLabel}} + <> +
setExpanded((v) => !v) : undefined} + > + {/* Identity + actions */} +
+
+ + + + {node.users_online ?? 0} - )} -
+ + {getCountryFlag(node.country_code)} + +

{node.name}

+ {(providerLabel || providerFavicon) && ( + + {providerFavicon && ( + { + e.currentTarget.style.display = 'none'; + }} + /> + )} + {providerLabel && {providerLabel}} + + )} +
-
- {canGeoCheck && ( +
+ {canGeoCheck && ( + + )} - )} - - - {hasBreakdown && ( - { + e.stopPropagation(); + onAction(node.uuid, node.is_disabled ? 'enable' : 'disable'); + }} + disabled={isLoading} + className={`rounded-lg p-1.5 transition-colors disabled:opacity-50 ${ + node.is_disabled + ? 'bg-success-500/20 text-success-400 hover:bg-success-500/30' + : 'bg-error-500/20 text-error-400 hover:bg-error-500/30' }`} - /> - )} -
-
- - {/* Address + traffic + uptime */} -
- - - {node.address} - - - {formatBytes(used)} - {trafficPct !== null && ( - - + {node.is_disabled ? ( + + ) : ( + + )} + + {hasBreakdown && ( + + )} +
+
+ + {/* Address + traffic + uptime */} +
+ + + {node.address} + + + {formatBytes(used)} + {trafficPct !== null && ( + + + + )} + / {limit > 0 ? formatBytes(limit) : '∞'} + + {node.xray_uptime > 0 && ( + + + {formatUptime(node.xray_uptime)} )} - / {limit > 0 ? formatBytes(limit) : '∞'} - - {node.xray_uptime > 0 && ( - - - {formatUptime(node.xray_uptime)} - +
+ + {/* Live metrics — mobile: 3 fixed rows so wrapping speeds don't reflow; + desktop (sm+): the original single wrap row, wide enough not to jump. */} + {(ramPct !== null || loadAvg || rx > 0 || tx > 0 || node.versions) && ( + <> + {/* Mobile: processor · traffic · versions */} +
+ {(loadAvg || ramPct !== null) && ( +
+ {loadAvg && ( + + + {loadAvg} + + )} + {ramPct !== null && ( + + + {ramPct}% + + + + + )} +
+ )} + {(rx > 0 || tx > 0) && ( +
+ + + {formatSpeed(rx)} + + + + {formatSpeed(tx)} + +
+ )} + {(node.versions?.node || node.versions?.xray) && ( +
+ {node.versions?.node && ( + + + {node.versions.node} + + )} + {node.versions?.xray && ( + + + {node.versions.xray} + + )} +
+ )} +
+ + {/* Desktop: single wrap row (original) */} +
+ {ramPct !== null && ( + + + {ramPct}% + + + + + )} + {loadAvg && ( + + + {loadAvg} + + )} + + + + {formatSpeed(rx)} + + + + {formatSpeed(tx)} + + + {(node.versions?.node || node.versions?.xray) && ( + + {node.versions?.node && ( + + + {node.versions.node} + + )} + {node.versions?.xray && ( + + + {node.versions.xray} + + )} + + )} +
+ + )} + + {/* Per-node traffic accordion (merged from the former Traffic tab) */} + {expanded && hasBreakdown && ( +
e.stopPropagation()} + > + {inbounds.length > 0 && ( + + )} + {outbounds.length > 0 && ( + + )} +
)}
- {/* Live metrics — mobile: 3 fixed rows so wrapping speeds don't reflow; - desktop (sm+): the original single wrap row, wide enough not to jump. */} - {(ramPct !== null || loadAvg || rx > 0 || tx > 0 || node.versions) && ( - <> - {/* Mobile: processor · traffic · versions */} -
- {(loadAvg || ramPct !== null) && ( -
- {loadAvg && ( - - - {loadAvg} - - )} - {ramPct !== null && ( - - - {ramPct}% - - - - - )} -
- )} - {(rx > 0 || tx > 0) && ( -
- - - {formatSpeed(rx)} - - - - {formatSpeed(tx)} - -
- )} - {(node.versions?.node || node.versions?.xray) && ( -
- {node.versions?.node && ( - - - {node.versions.node} - - )} - {node.versions?.xray && ( - - - {node.versions.xray} - - )} -
- )} -
- - {/* Desktop: single wrap row (original) */} -
- {ramPct !== null && ( - - - {ramPct}% - - - - - )} - {loadAvg && ( - - - {loadAvg} - - )} - - - - {formatSpeed(rx)} - - - - {formatSpeed(tx)} - - - {(node.versions?.node || node.versions?.xray) && ( - - {node.versions?.node && ( - - - {node.versions.node} - - )} - {node.versions?.xray && ( - - - {node.versions.xray} - - )} - - )} -
- - )} - - {/* Per-node traffic accordion (merged from the former Traffic tab) */} - {expanded && hasBreakdown && ( -
e.stopPropagation()} - > - {inbounds.length > 0 && ( - - )} - {outbounds.length > 0 && ( - - )} -
- )} - {geoCheckOpen && setGeoCheckOpen(false)} />} -
+ ); }