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)} />} -
+ ); }