diff --git a/handlers/admin/clusters/cluster_sync.py b/handlers/admin/clusters/cluster_sync.py
index 65fcf066..4c258380 100644
--- a/handlers/admin/clusters/cluster_sync.py
+++ b/handlers/admin/clusters/cluster_sync.py
@@ -12,7 +12,6 @@ from sqlalchemy.ext.asyncio import AsyncSession
from config import (
ADMIN_PASSWORD,
ADMIN_USERNAME,
- HAPP_CRYPTOLINK,
REMNAWAVE_LOGIN,
REMNAWAVE_PASSWORD,
USE_COUNTRY_SELECTION,
diff --git a/handlers/admin/users/users_hwid.py b/handlers/admin/users/users_hwid.py
index 97c7d7f2..fbfa4e72 100644
--- a/handlers/admin/users/users_hwid.py
+++ b/handlers/admin/users/users_hwid.py
@@ -52,12 +52,53 @@ async def handle_hwid_menu(
await callback_query.message.edit_text("❌ Ошибка авторизации в Remnawave.")
return
+ user_info = await api.get_user_by_uuid(client_id)
devices = await api.get_user_hwid_devices(client_id)
- if not devices:
- text = "💻 HWID устройства\n\n🔌 Нет привязанных устройств."
+ status_emoji = "🟢"
+ status_text = "Онлайн"
+ online_at_str = "—"
+ first_connected_str = "—"
+ last_node_uuid = "—"
+
+ if not user_info:
+ status_emoji = "⚪️"
+ status_text = "Не найден"
else:
- text = f"💻 HWID устройства\n\nПривязано: {len(devices)}\n\n"
+ is_online = bool(user_info.get("isOnline"))
+ status_emoji = "🟢" if is_online else "⚪️"
+ status_text = "Онлайн" if is_online else "Офлайн"
+
+ online_at = user_info.get("onlineAt")
+ if online_at:
+ online_at_str = online_at[:19].replace("T", " ")
+
+ first_connected_at = user_info.get("firstConnectedAt")
+ if first_connected_at:
+ first_connected_str = first_connected_at[:19].replace("T", " ")
+
+ last_node_uuid_val = user_info.get("lastConnectedNodeUuid")
+ if last_node_uuid_val:
+ last_node_uuid = last_node_uuid_val
+
+ if not devices:
+ text = (
+ "💻 HWID устройства\n\n"
+ f"{status_emoji} Статус: {status_text}\n"
+ f"└ 🕓 Онлайн был: {online_at_str}\n"
+ f"└ 🚀 Первое подключение: {first_connected_str}\n"
+ f"└ 🛰 Нода последнего подключения: {last_node_uuid}\n\n"
+ "🔌 Нет привязанных устройств."
+ )
+ else:
+ text = (
+ "💻 HWID устройства\n\n"
+ f"{status_emoji} Статус: {status_text}\n"
+ f"└ 🕓 Онлайн был: {online_at_str}\n"
+ f"└ 🚀 Первое подключение: {first_connected_str}\n"
+ f"└ 🛰 Нода последнего подключения: {last_node_uuid}\n\n"
+ f"🔗 Привязано устройств: {len(devices)}\n\n"
+ )
for idx, device in enumerate(devices, 1):
created = device.get("createdAt", "")[:19].replace("T", " ")
updated = device.get("updatedAt", "")[:19].replace("T", " ")
diff --git a/handlers/keys/operations/aggregated_links.py b/handlers/keys/operations/aggregated_links.py
index c4b7b093..9481c9b2 100644
--- a/handlers/keys/operations/aggregated_links.py
+++ b/handlers/keys/operations/aggregated_links.py
@@ -23,7 +23,7 @@ async def _is_vless_tariff(session: AsyncSession, email: str) -> bool:
return is_plan_vless(tariff)
-async def _try_build_remna_vless(servers: list, email: str) -> tuple[str | None, str | None]:
+async def _try_build_remna_vless(servers: list, email: str) -> tuple[str | None, str | None, str | None]:
happ_cryptolink_enabled = bool(MODES_CONFIG.get("HAPP_CRYPTOLINK_ENABLED", HAPP_CRYPTOLINK))
si = servers[0]
@@ -31,33 +31,28 @@ async def _try_build_remna_vless(servers: list, email: str) -> tuple[str | None,
ok = await remna.login(REMNAWAVE_LOGIN, REMNAWAVE_PASSWORD)
if not ok:
logger.warning("[Remnawave] login failed")
- return None, None
+ return None, None, None
data = await remna.get_subscription_by_username(email)
if not data:
logger.warning("[Remnawave] by-username empty")
- return None, None
+ return None, None, None
- links = data.get("links") or []
- best = None
- if links:
- best = max(links, key=score_vless_url)
- if score_vless_url(best) < 0:
- best = None
+ sub_url = data.get("subscriptionUrl") or None
+
+ best_vless = None
+ if sub_url and sub_url.lower().startswith("vless://"):
+ best_vless = sub_url
happ_link = None
- try:
- happ = data.get("happ") or {}
- if isinstance(happ, dict):
- happ_link = happ.get("cryptoLink") or happ.get("link")
- except Exception:
- pass
+ if happ_cryptolink_enabled and sub_url:
+ try:
+ happ_link = await remna.encrypt_happ_crypto_link(sub_url)
+ except Exception as e:
+ logger.warning(f"[Remnawave] happ encrypt failed: {e}")
+ happ_link = None
- if happ_cryptolink_enabled and happ_link:
- return best, happ_link
-
- sub_url = data.get("subscriptionUrl")
- return best, sub_url
+ return best_vless, sub_url, happ_link
async def _try_build_3xui_vless(servers: list, email: str) -> str | None:
@@ -144,7 +139,7 @@ async def make_aggregated_link(
logger.info("[agg_link] choose 3x-ui VLESS")
return xui_link
if remna:
- best_vless, sub_url = await _try_build_remna_vless(remna, email)
+ best_vless, sub_url, happ_link = await _try_build_remna_vless(remna, email)
if best_vless:
logger.info("[agg_link] choose Remnawave VLESS")
return best_vless
@@ -156,8 +151,11 @@ async def make_aggregated_link(
if stored and str(stored).lower().startswith("vless://"):
logger.info("[agg_link] choose stored Remnawave VLESS")
return stored
+ if happ_link:
+ logger.info("[agg_link] choose Remnawave cryptoLink (vless)")
+ return happ_link
if sub_url:
- logger.info("[agg_link] choose Remnawave subscriptionUrl")
+ logger.info("[agg_link] choose Remnawave subscriptionUrl (vless)")
return sub_url
logger.info("[agg_link] fallback base link")
return f"{base}/{email}/{tg_id}"
@@ -166,15 +164,15 @@ async def make_aggregated_link(
if legacy_links_enabled:
logger.info("[agg_link] LEGACY non-vless -> base link")
return f"{base}/{email}/{tg_id}"
- best_vless, sub_url = await _try_build_remna_vless(remna, email)
+ best_vless, sub_url, happ_link = await _try_build_remna_vless(remna, email)
if remna_link_override and (
remna_link_override.lower().startswith("vless://") or remna_link_override.startswith(("http", "happ://"))
):
logger.info("[agg_link] choose override Remnawave (non-vless)")
return remna_link_override
- if sub_url and sub_url.startswith("happ://"):
+ if happ_link:
logger.info("[agg_link] choose Remnawave cryptoLink (non-vless)")
- return sub_url
+ return happ_link
kd = await get_key_details(session, email)
stored = kd.get("remnawave_link") if kd else None
if stored:
diff --git a/handlers/keys/operations/subgroup_migration.py b/handlers/keys/operations/subgroup_migration.py
index cb8a1325..ac6b37ed 100644
--- a/handlers/keys/operations/subgroup_migration.py
+++ b/handlers/keys/operations/subgroup_migration.py
@@ -52,6 +52,20 @@ async def ensure_on_remnawave(
traffic_bytes = bytes_from_gb(total_gb)
use_crypto_link = bool(MODES_CONFIG.get("HAPP_CRYPTOLINK_ENABLED", HAPP_CRYPTOLINK))
+ async def _build_link_from_subscription(sub_url: str | None) -> str | None:
+ if not sub_url:
+ return None
+ remna_link = None
+ if use_crypto_link:
+ try:
+ remna_link = await api.encrypt_happ_crypto_link(sub_url)
+ except Exception as e:
+ logger.error(f"{PANEL_REMNA} ошибка шифрования happ-ссылки: {e}")
+ remna_link = None
+ if not remna_link:
+ remna_link = sub_url
+ return remna_link
+
async def do_update():
try:
updated = await api.update_user(
@@ -61,16 +75,30 @@ async def ensure_on_remnawave(
traffic_limit_bytes=traffic_bytes,
hwid_device_limit=hwid_device_limit,
)
- if updated:
- if reset_traffic:
- try:
- await api.reset_user_traffic(client_id)
- except Exception:
- pass
- return client_id, None
- except Exception:
+ if not updated:
+ return None, None
+
+ if reset_traffic:
+ try:
+ await api.reset_user_traffic(client_id)
+ except Exception:
+ pass
+
+ remna_link = None
+ try:
+ sub_data = await api.get_subscription_by_username(email)
+ except Exception as e:
+ logger.error(f"{PANEL_REMNA} get_subscription_by_username при update: {e}")
+ sub_data = None
+
+ if sub_data:
+ sub_url = sub_data.get("subscriptionUrl")
+ remna_link = await _build_link_from_subscription(sub_url)
+
+ return client_id, remna_link
+ except Exception as e:
+ logger.error(f"{PANEL_REMNA} update_user не удалось: {e}")
return None, None
- return None, None
async def do_create():
try:
@@ -84,19 +112,18 @@ async def ensure_on_remnawave(
}
if traffic_bytes > 0:
payload["trafficLimitBytes"] = traffic_bytes
-
payload["hwidDeviceLimit"] = hwid_device_limit
created = await api.create_user(payload)
- new_uuid = created.get("uuid") if isinstance(created, dict) else None
- remna_link = None
- if isinstance(created, dict):
- if use_crypto_link:
- remna_link = (
- created.get("happ", {}).get("cryptoLink") if isinstance(created.get("happ"), dict) else None
- )
- if not remna_link:
- remna_link = created.get("subscriptionUrl")
+ if not created:
+ return None, None
+
+ user = created.get("user") or {}
+ new_uuid = user.get("uuid") or client_id
+
+ sub_url = created.get("subscriptionUrl")
+ remna_link = await _build_link_from_subscription(sub_url)
+
return new_uuid, remna_link
except Exception as e:
logger.error(f"{PANEL_REMNA} создание не удалось: {e}")
diff --git a/hooks/constants.py b/hooks/constants.py
index c59d449e..699e01a3 100644
--- a/hooks/constants.py
+++ b/hooks/constants.py
@@ -1 +1 @@
-DEFAULT_HOOK_TIMEOUT = 2.0
\ No newline at end of file
+DEFAULT_HOOK_TIMEOUT = 4.0
\ No newline at end of file
diff --git a/panels/remnawave.cpython-312-x86_64-linux-gnu.so b/panels/remnawave.cpython-312-x86_64-linux-gnu.so
index 73be5853..e4341e00 100644
Binary files a/panels/remnawave.cpython-312-x86_64-linux-gnu.so and b/panels/remnawave.cpython-312-x86_64-linux-gnu.so differ