diff --git a/handlers/admin/clusters/cluster_manage.py b/handlers/admin/clusters/cluster_manage.py
index 9c4fc540..89ee3c74 100644
--- a/handlers/admin/clusters/cluster_manage.py
+++ b/handlers/admin/clusters/cluster_manage.py
@@ -147,46 +147,99 @@ async def handle_days_input(message: Message, state: FSMContext, session: AsyncS
await state.clear()
return
- for key in keys:
- new_expiry = key.expiry_time + add_ms
+ servers = await get_servers(session=session)
+ cluster_servers = servers.get(cluster_name, [])
+
+ if not cluster_servers:
+ await message.answer("❌ Не найдены серверы в кластере.")
+ await state.clear()
+ return
- traffic_limit = 0
- device_limit = 0
- key_subgroup = None
- if key.tariff_id:
- result = await session.execute(
- select(Tariff.traffic_limit, Tariff.device_limit, Tariff.subgroup_title).where(
- Tariff.id == key.tariff_id,
- Tariff.is_active.is_(True),
- )
- )
- tariff = result.first()
- if tariff:
- traffic_limit = int(tariff[0]) if tariff[0] is not None else 0
- device_limit = int(tariff[1]) if tariff[1] is not None else 0
- key_subgroup = tariff[2]
-
- await renew_key_in_cluster(
- cluster_name,
- email=key.email,
- client_id=key.client_id,
- new_expiry_time=new_expiry,
- total_gb=traffic_limit,
- session=session,
- hwid_device_limit=device_limit,
- reset_traffic=False,
- target_subgroup=key_subgroup,
- old_subgroup=key_subgroup,
- plan=key.tariff_id,
- )
- await update_key_expiry(session, key.client_id, new_expiry)
-
- logger.info(f"[Cluster Extend] {key.email} +{days}д → {datetime.utcfromtimestamp(new_expiry / 1000)}")
-
- await message.answer(
- f"✅ Время подписки продлено на {days} дней всем пользователям в кластере {cluster_name}."
+ is_full_remnawave = all(
+ str(s.get("panel_type", "")).lower() == "remnawave"
+ for s in cluster_servers
)
+ if is_full_remnawave:
+ uuids = [key.client_id for key in keys if key.client_id]
+
+ if not uuids:
+ await message.answer("❌ Нет валидных подписок для продления.")
+ await state.clear()
+ return
+
+ api_url = cluster_servers[0].get("api_url", "")
+ if not api_url:
+ await message.answer("❌ Не найден URL панели для кластера.")
+ await state.clear()
+ return
+
+ from panels.remnawave import RemnawaveAPI
+ remna = RemnawaveAPI(api_url)
+
+ try:
+ result_bulk = await remna.bulk_extend_expiration_date(uuids, days)
+ finally:
+ await remna.aclose()
+
+ if result_bulk is None:
+ await message.answer("❌ Ошибка при обращении к API панели.")
+ await state.clear()
+ return
+
+ affected = result_bulk.get("affectedRows", 0)
+ logger.info(f"[Cluster Extend] Bulk API: продлено {affected} подписок на {days} дней")
+
+ for key in keys:
+ new_expiry = key.expiry_time + add_ms
+ await update_key_expiry(session, key.client_id, new_expiry)
+
+ await session.commit()
+
+ await message.answer(
+ f"✅ Время подписки продлено на {days} дней для {affected} пользователей в кластере {cluster_name}."
+ )
+ else:
+ for key in keys:
+ new_expiry = key.expiry_time + add_ms
+
+ traffic_limit = 0
+ device_limit = 0
+ key_subgroup = None
+ if key.tariff_id:
+ tariff_result = await session.execute(
+ select(Tariff.traffic_limit, Tariff.device_limit, Tariff.subgroup_title).where(
+ Tariff.id == key.tariff_id,
+ Tariff.is_active.is_(True),
+ )
+ )
+ tariff = tariff_result.first()
+ if tariff:
+ traffic_limit = int(tariff[0]) if tariff[0] is not None else 0
+ device_limit = int(tariff[1]) if tariff[1] is not None else 0
+ key_subgroup = tariff[2]
+
+ await renew_key_in_cluster(
+ cluster_name,
+ email=key.email,
+ client_id=key.client_id,
+ new_expiry_time=new_expiry,
+ total_gb=traffic_limit,
+ session=session,
+ hwid_device_limit=device_limit,
+ reset_traffic=False,
+ target_subgroup=key_subgroup,
+ old_subgroup=key_subgroup,
+ plan=key.tariff_id,
+ )
+ await update_key_expiry(session, key.client_id, new_expiry)
+
+ logger.info(f"[Cluster Extend] {key.email} +{days}д → {datetime.utcfromtimestamp(new_expiry / 1000)}")
+
+ await message.answer(
+ f"✅ Время подписки продлено на {days} дней всем пользователям в кластере {cluster_name}."
+ )
+
except ValueError:
await message.answer("❌ Введите корректное число дней.")
except Exception as e:
diff --git a/handlers/keys/operations/renewal.py b/handlers/keys/operations/renewal.py
index 098282b5..71c3bcff 100644
--- a/handlers/keys/operations/renewal.py
+++ b/handlers/keys/operations/renewal.py
@@ -58,6 +58,7 @@ async def renew_on_remnawave(
reset_traffic: bool,
target_server_name: str | None = None,
external_squad_uuid: str | None = None,
+ old_device_limit: int | None = None,
) -> bool:
remnawave_nodes = [
s for s in cluster if str(s.get("panel_type", "3x-ui")).lower() == "remnawave" and s.get("inbound_id")
@@ -74,6 +75,13 @@ async def renew_on_remnawave(
logger.error(f"{PANEL_REMNA} Не удалось войти в Remnawave API")
return False
+ if old_device_limit is not None and hwid_device_limit < old_device_limit:
+ try:
+ await remna.clear_all_hwid_devices(client_id)
+ logger.info(f"{PANEL_REMNA} HWID устройства сброшены для {client_id} (лимит {old_device_limit} → {hwid_device_limit})")
+ except Exception as e:
+ logger.warning(f"{PANEL_REMNA} Ошибка сброса HWID: {e}")
+
expire_iso = datetime.utcfromtimestamp(new_expiry_time // 1000).isoformat() + "Z"
traffic_limit_bytes = total_gb * 1024 * 1024 * 1024 if total_gb else 0
active_inbounds = [s["inbound_id"] for s in remnawave_nodes]
@@ -203,6 +211,12 @@ async def renew_key_in_cluster(
tg_id = int(kd["tg_id"])
server_id = kd["server_id"]
+ old_device_limit = kd.get("current_device_limit") or kd.get("selected_device_limit")
+ if old_device_limit is None and kd.get("tariff_id"):
+ old_tariff = await get_tariff_by_id(session, kd["tariff_id"])
+ if old_tariff:
+ old_device_limit = old_tariff.get("device_limit")
+
single_server = None
if servers_map.get(server_id):
cluster = servers_map[server_id]
@@ -330,6 +344,7 @@ async def renew_key_in_cluster(
reset_traffic=reset_traffic,
target_server_name=server_id if single_server else None,
external_squad_uuid=external_squad_uuid,
+ old_device_limit=old_device_limit,
)
succeeded, _ = await renew_on_3xui(
diff --git a/panels/remnawave.cpython-312-x86_64-linux-gnu.so b/panels/remnawave.cpython-312-x86_64-linux-gnu.so
index 4dcb0b83..64db91e5 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