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):