From 1578a9da36e3dd204c8dc9a31de0f61c1ddf2107 Mon Sep 17 00:00:00 2001
From: 3252a8 <3252a8@proton.me>
Date: Sun, 31 May 2026 23:05:29 +0300
Subject: [PATCH] fix: align traffic top-up flows and unlimited overrides
---
backend/bot/app/web/admin_api_impl/users.py | 2 +-
.../subscription_service_impl/tariffs.py | 5 +-
frontend/src/lib/components/ui/dialog.svelte | 2 +-
.../src/lib/components/ui/scroll-area.svelte | 8 +++
frontend/src/lib/webapp/demoDataset.js | 41 +++++++-------
frontend/src/styles/admin.css | 9 ----
locales/en.json | 10 ++--
locales/ru.json | 8 +--
tests/test_admin_traffic_grants.py | 53 +++++++++++++++++++
tests/test_mini_app_url.py | 17 ++++++
tests/test_subscription_service_behavior.py | 4 +-
tests/test_support_notifications.py | 2 +-
tests/test_tariff_worker.py | 25 +++++++++
13 files changed, 139 insertions(+), 47 deletions(-)
diff --git a/backend/bot/app/web/admin_api_impl/users.py b/backend/bot/app/web/admin_api_impl/users.py
index bcbd800..b4ea1df 100644
--- a/backend/bot/app/web/admin_api_impl/users.py
+++ b/backend/bot/app/web/admin_api_impl/users.py
@@ -1031,7 +1031,7 @@ async def admin_user_premium_override_route(request: web.Request) -> web.Respons
async def admin_user_regular_traffic_override_route(request: web.Request) -> web.Response:
- """Main (regular) traffic: unlimited-style ceiling + admin bonus GB."""
+ """Main (regular) traffic: native unlimited panel limit + admin bonus GB."""
actor_id = _require_admin_user_id(request)
target_id = int(request.match_info["user_id"])
settings: Settings = request.app["settings"]
diff --git a/backend/bot/services/subscription_service_impl/tariffs.py b/backend/bot/services/subscription_service_impl/tariffs.py
index 58f4c06..f68e436 100644
--- a/backend/bot/services/subscription_service_impl/tariffs.py
+++ b/backend/bot/services/subscription_service_impl/tariffs.py
@@ -112,15 +112,14 @@ class TariffMixin:
regular_unlimited_override: bool,
traffic_used_bytes: int,
) -> int:
- """Numeric cap sent to the panel; ``regular_unlimited_override`` uses a large practical ceiling.""" # noqa: E501
+ """Numeric cap sent to the panel; Remnawave treats ``0`` as unlimited."""
floor = (
int(tier_baseline_bytes or 0)
+ max(0, int(topup_balance_bytes or 0))
+ max(0, int(regular_bonus_bytes or 0))
)
if regular_unlimited_override:
- used = max(0, int(traffic_used_bytes or 0))
- return max(floor, used + 512 * (1024**3), 1024**5)
+ return 0
return floor
async def premium_access_for_tariff(self, tariff: Optional[Tariff]) -> Dict[str, Any]:
diff --git a/frontend/src/lib/components/ui/dialog.svelte b/frontend/src/lib/components/ui/dialog.svelte
index b716149..2f0ad8f 100644
--- a/frontend/src/lib/components/ui/dialog.svelte
+++ b/frontend/src/lib/components/ui/dialog.svelte
@@ -66,7 +66,7 @@
-
+
diff --git a/frontend/src/lib/components/ui/scroll-area.svelte b/frontend/src/lib/components/ui/scroll-area.svelte
index 33f8364..d941f13 100644
--- a/frontend/src/lib/components/ui/scroll-area.svelte
+++ b/frontend/src/lib/components/ui/scroll-area.svelte
@@ -36,6 +36,10 @@
overflow: hidden;
}
+ :global(.scroll-area--dialog) {
+ overflow: visible;
+ }
+
:global(.scroll-area__viewport) {
width: 100%;
height: 100%;
@@ -56,6 +60,10 @@
width: 10px;
}
+ :global(.scroll-area--dialog .scroll-area__scrollbar[data-orientation="vertical"]) {
+ transform: translateX(12px);
+ }
+
:global(.scroll-area__scrollbar[data-orientation="horizontal"]) {
flex-direction: column;
height: 10px;
diff --git a/frontend/src/lib/webapp/demoDataset.js b/frontend/src/lib/webapp/demoDataset.js
index d70b3d6..d24ddb4 100644
--- a/frontend/src/lib/webapp/demoDataset.js
+++ b/frontend/src/lib/webapp/demoDataset.js
@@ -104957,10 +104957,9 @@ export const DEMO_DATASET = {
updated_by: null,
},
en: {
- base: "Unlimited-style ceiling and a persistent bonus on the main traffic limit.",
+ base: "Unlimited access and a persistent bonus on the main traffic limit.",
fallback: "Режим безлимита и постоянный бонус к лимиту основного трафика.",
- effective:
- "Unlimited-style ceiling and a persistent bonus on the main traffic limit.",
+ effective: "Unlimited access and a persistent bonus on the main traffic limit.",
override: "",
overridden: false,
updated_at: null,
@@ -105241,22 +105240,22 @@ export const DEMO_DATASET = {
audience: "internal",
values: {
ru: {
- base: "Дополнительный лимит основного и премиум-трафика поверх тарифа; безлимит только для премиум-сквадов.",
+ base: "Дополнительный лимит основного и премиум-трафика поверх тарифа; безлимит можно включить для основного трафика или премиум-сквадов.",
fallback:
- "Дополнительный лимит основного и премиум-трафика поверх тарифа; безлимит только для премиум-сквадов.",
+ "Дополнительный лимит основного и премиум-трафика поверх тарифа; безлимит можно включить для основного трафика или премиум-сквадов.",
effective:
- "Дополнительный лимит основного и премиум-трафика поверх тарифа; безлимит только для премиум-сквадов.",
+ "Дополнительный лимит основного и премиум-трафика поверх тарифа; безлимит можно включить для основного трафика или премиум-сквадов.",
override: "",
overridden: false,
updated_at: null,
updated_by: null,
},
en: {
- base: "Extra main or premium traffic limits on top of the tariff; unlimited applies to premium squads only.",
+ base: "Extra main or premium traffic limits on top of the tariff; unlimited can be applied to main traffic or premium squads.",
fallback:
- "Дополнительный лимит основного и премиум-трафика поверх тарифа; безлимит только для премиум-сквадов.",
+ "Дополнительный лимит основного и премиум-трафика поверх тарифа; безлимит можно включить для основного трафика или премиум-сквадов.",
effective:
- "Extra main or premium traffic limits on top of the tariff; unlimited applies to premium squads only.",
+ "Extra main or premium traffic limits on top of the tariff; unlimited can be applied to main traffic or premium squads.",
override: "",
overridden: false,
updated_at: null,
@@ -134112,18 +134111,18 @@ export const DEMO_DATASET = {
audience: "user",
values: {
ru: {
- base: "Докупить премиум-трафик (мини-приложение)",
- fallback: "Докупить премиум-трафик (мини-приложение)",
- effective: "Докупить премиум-трафик (мини-приложение)",
+ base: "Докупить премиум-трафик",
+ fallback: "Докупить премиум-трафик",
+ effective: "Докупить премиум-трафик",
override: "",
overridden: false,
updated_at: null,
updated_by: null,
},
en: {
- base: "Top up premium traffic (mini app)",
- fallback: "Докупить премиум-трафик (мини-приложение)",
- effective: "Top up premium traffic (mini app)",
+ base: "Top up premium traffic",
+ fallback: "Докупить премиум-трафик",
+ effective: "Top up premium traffic",
override: "",
overridden: false,
updated_at: null,
@@ -134136,18 +134135,18 @@ export const DEMO_DATASET = {
audience: "user",
values: {
ru: {
- base: "Докупить трафик (мини-приложение)",
- fallback: "Докупить трафик (мини-приложение)",
- effective: "Докупить трафик (мини-приложение)",
+ base: "Докупить трафик",
+ fallback: "Докупить трафик",
+ effective: "Докупить трафик",
override: "",
overridden: false,
updated_at: null,
updated_by: null,
},
en: {
- base: "Top up traffic (mini app)",
- fallback: "Докупить трафик (мини-приложение)",
- effective: "Top up traffic (mini app)",
+ base: "Top up traffic",
+ fallback: "Докупить трафик",
+ effective: "Top up traffic",
override: "",
overridden: false,
updated_at: null,
diff --git a/frontend/src/styles/admin.css b/frontend/src/styles/admin.css
index 3dd00c4..1a9ac83 100644
--- a/frontend/src/styles/admin.css
+++ b/frontend/src/styles/admin.css
@@ -2760,15 +2760,6 @@
max-height: min(100%, 760px);
}
-.admin-user-dialog .dialog-body-scroll {
- margin-right: -10px;
- padding-right: 10px;
-}
-
-.admin-user-dialog .dialog-body-scroll > .scroll-area__viewport {
- padding-right: 10px;
-}
-
.admin-user-dialog-body {
display: grid;
grid-template-columns: minmax(0, 1fr);
diff --git a/locales/en.json b/locales/en.json
index 2524df2..64be717 100644
--- a/locales/en.json
+++ b/locales/en.json
@@ -374,8 +374,8 @@
"traffic_warning_premium_depleted": "⛔️ The premium server quota for plan {tariff_name} is used up.\n\nCurrent period:\nused — {used}\navailable — {remaining}\nlimit — {limit_total}\n\nThis limit applies to:\n{servers}\n\nAccess may be limited until the period resets or you buy more premium traffic.\n\nUse the button below to top up.",
"traffic_warning_premium_generic_servers": "• servers with premium access under your plan",
"traffic_warning_premium_servers_more": "… and {count} more servers",
- "traffic_warn_btn_topup_webapp_regular": "Top up traffic (mini app)",
- "traffic_warn_btn_topup_webapp_premium": "Top up premium traffic (mini app)",
+ "traffic_warn_btn_topup_webapp_regular": "Top up traffic",
+ "traffic_warn_btn_topup_webapp_premium": "Top up premium traffic",
"traffic_warn_btn_topup_regular": "Top up traffic",
"traffic_warn_btn_topup_premium": "Top up premium traffic",
"log_promo_activation": "🎁 Promo Code Activated\n\n👤 User: {user_display}\n🏷 Code: {promo_code}\n🎯 Bonus: +{bonus_days}d\n🕐 Time: {timestamp}",
@@ -685,7 +685,7 @@
"email_support_row_remaining": "Remaining",
"email_support_row_panel_status": "Panel status",
"email_support_cta_open_ticket": "Open ticket",
- "email_support_cta_open_mini_app": "Open in Mini App",
+ "email_support_cta_open_mini_app": "Open ticket",
"wa_loading": "Loading...",
"wa_back": "Back",
"wa_next": "Next",
@@ -1268,11 +1268,11 @@
"user_premium_override_card_title": "Premium traffic",
"user_premium_override_card_hint": "Unlimited access and extra volume for premium squads on top of the tariff.",
"user_regular_override_card_title": "Main traffic",
- "user_regular_override_card_hint": "Unlimited-style ceiling and a persistent bonus on the main traffic limit.",
+ "user_regular_override_card_hint": "Unlimited access and a persistent bonus on the main traffic limit.",
"user_regular_override_status_unlimited": "Current: unlimited",
"regular_override_saved": "Main traffic override saved",
"user_traffic_override_title": "Traffic overrides",
- "user_traffic_override_hint": "Extra main or premium traffic limits on top of the tariff; unlimited applies to premium squads only.",
+ "user_traffic_override_hint": "Extra main or premium traffic limits on top of the tariff; unlimited can be applied to main traffic or premium squads.",
"user_regular_override_bonus": "Extra main traffic, GB",
"user_regular_override_bonus_hint": "Persistent bonus bytes added to the main traffic limit (not a balance top-up).",
"user_regular_override_status_bonus": "Main traffic now: +{gb} GB",
diff --git a/locales/ru.json b/locales/ru.json
index 674e4eb..650f6e9 100644
--- a/locales/ru.json
+++ b/locales/ru.json
@@ -374,8 +374,8 @@
"traffic_warning_premium_depleted": "⛔️ Лимит премиум-серверов тарифа {tariff_name} израсходован.\n\nТекущий период:\nизрасходовано — {used}\nдоступно — {remaining}\nлимит — {limit_total}\n\nЛимит действует на:\n{servers}\n\nДоступ может быть ограничен до сброса периода или докупки премиум-трафика.\n\nДокупить можно через кнопку ниже.",
"traffic_warning_premium_generic_servers": "• серверы с премиум-доступом по вашему тарифу",
"traffic_warning_premium_servers_more": "… и ещё серверов: {count}",
- "traffic_warn_btn_topup_webapp_regular": "Докупить трафик (мини-приложение)",
- "traffic_warn_btn_topup_webapp_premium": "Докупить премиум-трафик (мини-приложение)",
+ "traffic_warn_btn_topup_webapp_regular": "Докупить трафик",
+ "traffic_warn_btn_topup_webapp_premium": "Докупить премиум-трафик",
"traffic_warn_btn_topup_regular": "Докупить трафик",
"traffic_warn_btn_topup_premium": "Докупить премиум-трафик",
"log_promo_activation": "🎁 Активирован промокод\n\n👤 Пользователь: {user_display}\n🏷 Код: {promo_code}\n🎯 Бонус: +{bonus_days} дн.\n🕐 Время: {timestamp}",
@@ -685,7 +685,7 @@
"email_support_row_remaining": "Осталось",
"email_support_row_panel_status": "Статус в панели",
"email_support_cta_open_ticket": "Открыть тикет",
- "email_support_cta_open_mini_app": "Открыть в Mini App",
+ "email_support_cta_open_mini_app": "Открыть обращение",
"wa_loading": "Загрузка...",
"wa_back": "Назад",
"wa_next": "Далее",
@@ -1272,7 +1272,7 @@
"user_regular_override_status_unlimited": "Сейчас: безлимит",
"regular_override_saved": "Оверрайд основного трафика сохранён",
"user_traffic_override_title": "Оверрайд трафика",
- "user_traffic_override_hint": "Дополнительный лимит основного и премиум-трафика поверх тарифа; безлимит только для премиум-сквадов.",
+ "user_traffic_override_hint": "Дополнительный лимит основного и премиум-трафика поверх тарифа; безлимит можно включить для основного трафика или премиум-сквадов.",
"user_regular_override_bonus": "Доп. основной трафик, GB",
"user_regular_override_bonus_hint": "Постоянный бонус к лимиту основного трафика (не то же самое, что докупка ГБ).",
"user_regular_override_status_bonus": "Основной сейчас: +{gb} GB",
diff --git a/tests/test_admin_traffic_grants.py b/tests/test_admin_traffic_grants.py
index 1bf3934..7b01022 100644
--- a/tests/test_admin_traffic_grants.py
+++ b/tests/test_admin_traffic_grants.py
@@ -133,6 +133,59 @@ class AdminGrantTopupTests(unittest.IsolatedAsyncioTestCase):
self.assertIsNone(await service.admin_grant_topup(AsyncMock(), 1, 0))
self.assertIsNone(await service.admin_grant_topup(AsyncMock(), 1, -10))
+ async def test_regular_unlimited_override_syncs_zero_panel_limit(self):
+ with tempfile.TemporaryDirectory() as tmpdir:
+ settings = _make_settings(_tariffs_config_payload(), tmpdir)
+ panel_service = AsyncMock(spec=PanelApiService)
+ panel_service.update_user_details_on_panel = AsyncMock(return_value={"response": {}})
+ service = SubscriptionService(settings, panel_service)
+
+ db_user = SimpleNamespace(
+ user_id=42,
+ first_name="Tester",
+ last_name=None,
+ username="tester",
+ language_code="ru",
+ panel_user_uuid="panel-uuid",
+ email=None,
+ telegram_id=42,
+ )
+ sub = SimpleNamespace(
+ subscription_id=7,
+ user_id=42,
+ panel_user_uuid="panel-uuid",
+ end_date=datetime.now(timezone.utc) + timedelta(days=10),
+ tariff_key="standard",
+ tier_baseline_bytes=100 * (1024**3),
+ topup_balance_bytes=0,
+ traffic_limit_bytes=105 * (1024**3),
+ traffic_used_bytes=2 * (1024**5),
+ regular_bonus_bytes=0,
+ regular_unlimited_override=True,
+ is_throttled=True,
+ hwid_device_limit=3,
+ extra_hwid_devices=0,
+ premium_is_limited=False,
+ )
+
+ with (
+ patch(
+ "bot.services.subscription_service.user_dal.get_user_by_id",
+ new=AsyncMock(return_value=db_user),
+ ),
+ patch(
+ "bot.services.subscription_service.subscription_dal.get_active_subscription_by_user_id",
+ new=AsyncMock(return_value=sub),
+ ),
+ ):
+ await service.sync_main_traffic_limit_to_panel(AsyncMock(), 42)
+
+ self.assertEqual(sub.traffic_limit_bytes, 0)
+ self.assertFalse(sub.is_throttled)
+ panel_service.update_user_details_on_panel.assert_awaited_once()
+ panel_payload = panel_service.update_user_details_on_panel.await_args.args[1]
+ self.assertEqual(panel_payload["trafficLimitBytes"], 0)
+
async def test_premium_grant_clears_limited_state_when_balance_covers_overuse(self):
with tempfile.TemporaryDirectory() as tmpdir:
settings = _make_settings(_tariffs_config_payload(premium=True), tmpdir)
diff --git a/tests/test_mini_app_url.py b/tests/test_mini_app_url.py
index b137fdf..312b8d4 100644
--- a/tests/test_mini_app_url.py
+++ b/tests/test_mini_app_url.py
@@ -46,6 +46,23 @@ class MiniAppUrlTests(unittest.TestCase):
subscription_mini_app_topup_url(s, "regular"),
"https://app.example.com/webapp?topup=regular",
)
+ self.assertEqual(
+ subscription_mini_app_topup_url(s, "premium"),
+ "https://app.example.com/webapp?topup=premium",
+ )
+
+ def test_subscription_mini_app_topup_url_preserves_existing_query(self):
+ s = Settings(
+ _env_file=None,
+ BOT_TOKEN="x",
+ POSTGRES_USER="u",
+ POSTGRES_PASSWORD="p",
+ SUBSCRIPTION_MINI_APP_URL="https://app.example.com/webapp?lang=ru",
+ )
+ self.assertEqual(
+ subscription_mini_app_topup_url(s, "premium"),
+ "https://app.example.com/webapp?lang=ru&topup=premium",
+ )
def test_subscription_mini_app_path_url(self):
s = Settings(
diff --git a/tests/test_subscription_service_behavior.py b/tests/test_subscription_service_behavior.py
index 7ac0e16..9453289 100644
--- a/tests/test_subscription_service_behavior.py
+++ b/tests/test_subscription_service_behavior.py
@@ -96,7 +96,7 @@ class SubscriptionServiceCalculationTests(unittest.TestCase):
["fallback-a", "fallback-b"],
)
- def test_main_traffic_limit_includes_topup_bonus_and_unlimited_floor(self):
+ def test_main_traffic_limit_includes_topup_bonus_and_unlimited_zero(self):
with tempfile.TemporaryDirectory() as tmpdir:
settings = _make_settings(_tariffs_config_payload(), tmpdir)
service = _make_service(settings)
@@ -117,7 +117,7 @@ class SubscriptionServiceCalculationTests(unittest.TestCase):
regular_unlimited_override=True,
traffic_used_bytes=2 * (1024**5),
)
- self.assertEqual(unlimited_limit, 2 * (1024**5) + 512 * GIB)
+ self.assertEqual(unlimited_limit, 0)
def test_premium_effective_limit_ignores_negative_balances(self):
self.assertEqual(
diff --git a/tests/test_support_notifications.py b/tests/test_support_notifications.py
index 2068380..65b9e4d 100644
--- a/tests/test_support_notifications.py
+++ b/tests/test_support_notifications.py
@@ -41,7 +41,7 @@ def test_support_ticket_closed_email_uses_user_language():
assert ' dict:
class TariffWorkerTests(unittest.IsolatedAsyncioTestCase):
+ def test_topup_webapp_button_labels_do_not_mention_mini_app(self):
+ class I18n:
+ def gettext(self, _lang, key, **_kwargs):
+ return {
+ "traffic_warn_btn_topup_webapp_regular": "Top up traffic",
+ "traffic_warn_btn_topup_webapp_premium": "Top up premium traffic",
+ }.get(key, key)
+
+ worker = TariffTrafficWorker(
+ settings=SimpleNamespace(SUBSCRIPTION_MINI_APP_URL="https://app.example.com"),
+ session_factory=SimpleNamespace(),
+ panel_service=SimpleNamespace(),
+ subscription_service=SimpleNamespace(),
+ bot=SimpleNamespace(),
+ i18n=I18n(),
+ )
+
+ regular = worker._traffic_topup_markup("en", "regular").inline_keyboard[0][0]
+ premium = worker._traffic_topup_markup("en", "premium").inline_keyboard[0][0]
+
+ self.assertEqual(regular.text, "Top up traffic")
+ self.assertEqual(regular.web_app.url, "https://app.example.com?topup=regular")
+ self.assertEqual(premium.text, "Top up premium traffic")
+ self.assertEqual(premium.web_app.url, "https://app.example.com?topup=premium")
+
async def test_db_tick_retries_deadlock_once(self):
class FakeSession:
def __init__(self):