diff --git a/bot/handlers/admin/promo_codes.py b/bot/handlers/admin/promo_codes.py
index b230ec8..a22cc7d 100644
--- a/bot/handlers/admin/promo_codes.py
+++ b/bot/handlers/admin/promo_codes.py
@@ -266,7 +266,8 @@ async def process_promo_set_validity(callback: types.CallbackQuery,
async def process_promo_validity_days_handler(message: types.Message,
state: FSMContext,
i18n_data: dict,
- settings: Settings):
+ settings: Settings,
+ session: AsyncSession):
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
if not i18n:
diff --git a/bot/handlers/admin/statistics.py b/bot/handlers/admin/statistics.py
index d7c1af4..cfe3279 100644
--- a/bot/handlers/admin/statistics.py
+++ b/bot/handlers/admin/statistics.py
@@ -142,56 +142,53 @@ async def show_statistics_handler(callback: types.CallbackQuery,
stats_text_parts.append(f"\n🖥 {_('admin_panel_stats_header', default='Статистика панели')}")
try:
- panel_service = PanelApiService(settings)
-
- # Get panel system statistics
- logging.info("Fetching panel statistics...")
- panel_stats = await panel_service.get_panel_statistics()
- nodes_stats = await panel_service.get_nodes_statistics()
- online_count = await panel_service.get_online_users_count()
- users_activity = await panel_service.get_users_activity_stats()
-
- logging.info(f"Panel stats response: panel_stats={panel_stats}, nodes_stats={nodes_stats}, online_count={online_count}, users_activity={users_activity}")
-
- # Online users
- if online_count is not None:
- stats_text_parts.append(f"🟢 Онлайн сейчас: {online_count}")
- else:
- stats_text_parts.append(f"🟢 Онлайн сейчас: N/A")
-
- # Users activity
- if users_activity:
- today_connected = users_activity.get('today_connected', 'N/A')
- week_connected = users_activity.get('week_connected', 'N/A')
- never_connected = users_activity.get('never_connected', 'N/A')
- stats_text_parts.append(f"📅 Подключались сегодня: {today_connected}")
- stats_text_parts.append(f"📅 Подключались за неделю: {week_connected}")
- stats_text_parts.append(f"❌ Никогда не подключались: {never_connected}")
- else:
- stats_text_parts.append(f"📅 Активность пользователей: N/A")
-
- # Nodes statistics
- if nodes_stats:
- active_nodes = len([node for node in nodes_stats if node.get('status') == 'active'])
- total_nodes = len(nodes_stats)
- stats_text_parts.append(f"🔗 Активных нод: {active_nodes}/{total_nodes}")
- else:
- stats_text_parts.append(f"🔗 Ноды: N/A")
-
- # System statistics
- if panel_stats:
- system_info = panel_stats.get('system', {})
- if system_info:
- memory_usage = system_info.get('memory_usage_percent', 'N/A')
- cpu_usage = system_info.get('cpu_usage_percent', 'N/A')
- stats_text_parts.append(f"💾 Использование RAM: {memory_usage}%")
- stats_text_parts.append(f"🔄 Загрузка CPU: {cpu_usage}%")
+ async with PanelApiService(settings) as panel_service:
+ # Get panel system statistics
+ logging.info("Fetching panel statistics...")
+ panel_stats = await panel_service.get_panel_statistics()
+ nodes_stats = await panel_service.get_nodes_statistics()
+ online_count = await panel_service.get_online_users_count()
+ users_activity = await panel_service.get_users_activity_stats()
+
+ logging.info(f"Panel stats response: panel_stats={panel_stats}, nodes_stats={nodes_stats}, online_count={online_count}, users_activity={users_activity}")
+
+ # Online users
+ if online_count is not None:
+ stats_text_parts.append(f"🟢 Онлайн сейчас: {online_count}")
else:
- stats_text_parts.append(f"💾 Системная информация: N/A")
- else:
- stats_text_parts.append(f"💾 Системная статистика: N/A")
-
- await panel_service.close()
+ stats_text_parts.append(f"🟢 Онлайн сейчас: N/A")
+
+ # Users activity
+ if users_activity:
+ today_connected = users_activity.get('today_connected', 'N/A')
+ week_connected = users_activity.get('week_connected', 'N/A')
+ never_connected = users_activity.get('never_connected', 'N/A')
+ stats_text_parts.append(f"📅 Подключались сегодня: {today_connected}")
+ stats_text_parts.append(f"📅 Подключались за неделю: {week_connected}")
+ stats_text_parts.append(f"❌ Никогда не подключались: {never_connected}")
+ else:
+ stats_text_parts.append(f"📅 Активность пользователей: N/A")
+
+ # Nodes statistics
+ if nodes_stats:
+ active_nodes = len([node for node in nodes_stats if node.get('status') == 'active'])
+ total_nodes = len(nodes_stats)
+ stats_text_parts.append(f"🔗 Активных нод: {active_nodes}/{total_nodes}")
+ else:
+ stats_text_parts.append(f"🔗 Ноды: N/A")
+
+ # System statistics
+ if panel_stats:
+ system_info = panel_stats.get('system', {})
+ if system_info:
+ memory_usage = system_info.get('memory_usage_percent', 'N/A')
+ cpu_usage = system_info.get('cpu_usage_percent', 'N/A')
+ stats_text_parts.append(f"💾 Использование RAM: {memory_usage}%")
+ stats_text_parts.append(f"🔄 Загрузка CPU: {cpu_usage}%")
+ else:
+ stats_text_parts.append(f"💾 Системная информация: N/A")
+ else:
+ stats_text_parts.append(f"💾 Системная статистика: N/A")
except Exception as e:
logging.error(f"Failed to fetch panel statistics: {e}", exc_info=True)
diff --git a/bot/handlers/admin/user_management.py b/bot/handlers/admin/user_management.py
index 65daa9d..747c8e7 100644
--- a/bot/handlers/admin/user_management.py
+++ b/bot/handlers/admin/user_management.py
@@ -113,27 +113,28 @@ async def format_user_card(user: User, session: AsyncSession,
card_parts.append(f"👤 {_('admin_user_card_title', default='Карточка пользователя')}\n")
# User details
- user_name = user.first_name or "N/A"
- username_display = f"@{user.username}" if user.username else "N/A"
- registration_date = user.registration_date.strftime('%Y-%m-%d %H:%M') if user.registration_date else "N/A"
+ na_value = _("admin_user_na_value", default="N/A")
+ user_name = user.first_name or na_value
+ username_display = f"@{user.username}" if user.username else na_value
+ registration_date = user.registration_date.strftime('%Y-%m-%d %H:%M') if user.registration_date else na_value
- card_parts.append(f"🆔 ID: {hcode(str(user.user_id))}")
- card_parts.append(f"👤 Имя: {hcode(user_name)}")
- card_parts.append(f"📱 Username: {hcode(username_display)}")
- card_parts.append(f"🌍 Язык: {hcode(user.language_code or 'N/A')}")
- card_parts.append(f"📅 Регистрация: {hcode(registration_date)}")
+ card_parts.append(f"{_('admin_user_id_label', default='🆔 ID:')} {hcode(str(user.user_id))}")
+ card_parts.append(f"{_('admin_user_name_label', default='👤 Имя:')} {hcode(user_name)}")
+ card_parts.append(f"{_('admin_user_username_label', default='📱 Username:')} {hcode(username_display)}")
+ card_parts.append(f"{_('admin_user_language_label', default='🌍 Язык:')} {hcode(user.language_code or na_value)}")
+ card_parts.append(f"{_('admin_user_registration_label', default='📅 Регистрация:')} {hcode(registration_date)}")
# Ban status
- ban_status = "🚫 Заблокирован" if user.is_banned else "✅ Активен"
- card_parts.append(f"🛡 Статус: {ban_status}")
+ ban_status = _("admin_user_status_banned", default="🚫 Заблокирован") if user.is_banned else _("admin_user_status_active", default="✅ Активен")
+ card_parts.append(f"{_('admin_user_status_label', default='🛡 Статус:')} {ban_status}")
# Referral info
if user.referred_by_id:
- card_parts.append(f"🎁 Привлечен по реферальной программе от: {hcode(str(user.referred_by_id))}")
+ card_parts.append(f"{_('admin_user_referral_label', default='🎁 Привлечен по реферальной программе от:')} {hcode(str(user.referred_by_id))}")
# Panel info
if user.panel_user_uuid:
- card_parts.append(f"🔗 Panel UUID: {hcode(user.panel_user_uuid[:8] + '...' if len(user.panel_user_uuid) > 8 else user.panel_user_uuid)}")
+ card_parts.append(f"{_('admin_user_panel_uuid_label', default='🔗 Panel UUID:')} {hcode(user.panel_user_uuid[:8] + '...' if len(user.panel_user_uuid) > 8 else user.panel_user_uuid)}")
card_parts.append("") # Empty line
@@ -146,33 +147,33 @@ async def format_user_card(user: User, session: AsyncSession,
end_date = subscription_details.get('end_date')
if end_date:
end_date_str = end_date.strftime('%Y-%m-%d %H:%M') if isinstance(end_date, datetime) else str(end_date)
- card_parts.append(f"⏰ Действует до: {hcode(end_date_str)}")
+ card_parts.append(f"{_('admin_user_subscription_active_until', default='⏰ Действует до:')} {hcode(end_date_str)}")
status = subscription_details.get('status_from_panel', 'UNKNOWN')
- card_parts.append(f"📊 Статус на панели: {hcode(status)}")
+ card_parts.append(f"{_('admin_user_panel_status_label', default='📊 Статус на панели:')} {hcode(status)}")
traffic_limit = subscription_details.get('traffic_limit_bytes')
traffic_used = subscription_details.get('traffic_used_bytes')
if traffic_limit and traffic_used is not None:
traffic_limit_gb = traffic_limit / (1024**3)
traffic_used_gb = traffic_used / (1024**3)
- card_parts.append(f"📊 Трафик: {hcode(f'{traffic_used_gb:.2f}GB / {traffic_limit_gb:.2f}GB')}")
+ card_parts.append(f"{_('admin_user_traffic_label', default='📊 Трафик:')} {hcode(f'{traffic_used_gb:.2f}GB / {traffic_limit_gb:.2f}GB')}")
else:
- card_parts.append(f"💳 Подписка: {hcode('Отсутствует')}")
+ card_parts.append(f"{_('admin_user_subscription_label', default='💼 Подписка:')} {hcode(_('admin_user_subscription_none', default='Нет активной подписки'))}")
except Exception as e:
logging.error(f"Error getting subscription details for user {user.user_id}: {e}")
- card_parts.append(f"💳 Подписка: {hcode('Ошибка загрузки')}")
+ card_parts.append(f"{_('admin_user_subscription_label', default='💼 Подписка:')} {hcode(_('admin_user_subscription_error', default='Ошибка загрузки'))}")
# Statistics
try:
# Count user logs
logs_count = await message_log_dal.count_user_message_logs(session, user.user_id)
- card_parts.append(f"📜 Всего действий: {hcode(str(logs_count))}")
+ card_parts.append(f"{_('admin_user_actions_count_label', default='📜 Всего действий:')} {hcode(str(logs_count))}")
# Check if user had any subscriptions
had_subscriptions = await subscription_service.has_had_any_subscription(session, user.user_id)
- trial_status = "Использовал" if had_subscriptions else "Не использовал"
- card_parts.append(f"🆓 Триал: {hcode(trial_status)}")
+ trial_status = _("admin_user_trial_used", default="Использовал") if had_subscriptions else _("admin_user_trial_not_used", default="Не использовал")
+ card_parts.append(f"{_('admin_user_trial_label', default='🏡 Триал:')} {hcode(trial_status)}")
except Exception as e:
logging.error(f"Error getting user statistics for {user.user_id}: {e}")
@@ -353,7 +354,7 @@ async def handle_toggle_ban(callback: types.CallbackQuery, user: User,
await session.commit()
- status_text = "заблокирован" if new_ban_status else "разблокирован"
+ status_text = _("admin_user_ban_action_banned", default="заблокирован") if new_ban_status else _("admin_user_ban_action_unbanned", default="разблокирован")
await callback.answer(_(
"admin_user_ban_toggle_success",
default="✅ Пользователь {status}",
@@ -365,9 +366,9 @@ async def handle_toggle_ban(callback: types.CallbackQuery, user: User,
from config.settings import Settings
from bot.services.panel_api_service import PanelApiService
settings = Settings()
- panel_service = PanelApiService(settings)
- subscription_service = SubscriptionService(settings, panel_service)
- await handle_refresh_user_card(callback, user, subscription_service, session, i18n_instance, lang)
+ async with PanelApiService(settings) as panel_service:
+ subscription_service = SubscriptionService(settings, panel_service)
+ await handle_refresh_user_card(callback, user, subscription_service, session, i18n_instance, lang)
except Exception as e:
logging.error(f"Error toggling ban for user {user.user_id}: {e}")
@@ -631,16 +632,16 @@ async def process_direct_message_handler(message: types.Message, state: FSMConte
# Show user card again
from bot.services.panel_api_service import PanelApiService
- panel_service = PanelApiService(settings)
- subscription_service = SubscriptionService(settings, panel_service)
- user_card_text = await format_user_card(target_user, session, subscription_service, i18n, current_lang)
- keyboard = get_user_card_keyboard(target_user.user_id, i18n, current_lang)
-
- await message.answer(
- user_card_text,
- reply_markup=keyboard.as_markup(),
- parse_mode="HTML"
- )
+ async with PanelApiService(settings) as panel_service:
+ subscription_service = SubscriptionService(settings, panel_service)
+ user_card_text = await format_user_card(target_user, session, subscription_service, i18n, current_lang)
+ keyboard = get_user_card_keyboard(target_user.user_id, i18n, current_lang)
+
+ await message.answer(
+ user_card_text,
+ reply_markup=keyboard.as_markup(),
+ parse_mode="HTML"
+ )
except Exception as e:
logging.error(f"Error sending direct message to user {target_user_id}: {e}")
diff --git a/bot/handlers/user/payment.py b/bot/handlers/user/payment.py
index bc54a9c..a8c2463 100644
--- a/bot/handlers/user/payment.py
+++ b/bot/handlers/user/payment.py
@@ -20,7 +20,7 @@ from bot.services.panel_api_service import PanelApiService
from bot.services.yookassa_service import YooKassaService
from bot.middlewares.i18n import JsonI18n
from config.settings import Settings
-from bot.services.notification_service import notify_admin_new_payment, NotificationService
+from bot.services.notification_service import NotificationService
from bot.keyboards.inline.user_keyboards import get_connect_and_main_keyboard
payment_processing_lock = asyncio.Lock()
@@ -209,16 +209,6 @@ async def process_successful_payment(session: AsyncSession, bot: Bot,
)
except Exception as e:
logging.error(f"Failed to send payment notification: {e}")
-
- # Legacy notification for backwards compatibility
- await notify_admin_new_payment(
- bot,
- settings,
- i18n,
- user_id,
- subscription_months,
- payment_value,
- )
except Exception as e_process:
logging.error(
diff --git a/bot/handlers/user/start.py b/bot/handlers/user/start.py
index 996e7de..ab9c19e 100644
--- a/bot/handlers/user/start.py
+++ b/bot/handlers/user/start.py
@@ -237,7 +237,6 @@ async def start_command_handler(message: types.Message,
config_link = active.get("config_link") if active else None
config_link = config_link or _("config_link_not_available")
- from datetime import datetime
new_end_date = result if isinstance(result, datetime) else None
promo_success_text = _(
diff --git a/bot/services/crypto_pay_service.py b/bot/services/crypto_pay_service.py
index b2a40a9..8c67195 100644
--- a/bot/services/crypto_pay_service.py
+++ b/bot/services/crypto_pay_service.py
@@ -14,7 +14,7 @@ from bot.middlewares.i18n import JsonI18n
from bot.services.subscription_service import SubscriptionService
from bot.services.referral_service import ReferralService
from bot.keyboards.inline.user_keyboards import get_connect_and_main_keyboard
-from bot.services.notification_service import notify_admin_new_payment
+from bot.services.notification_service import NotificationService
from db.dal import payment_dal, user_dal
@@ -194,15 +194,20 @@ class CryptoPayService:
except Exception as e:
logging.error(f"Failed to send CryptoPay success message: {e}")
- await notify_admin_new_payment(
- bot,
- settings,
- i18n,
- user_id,
- months,
- float(invoice.amount),
- currency=invoice.asset or settings.DEFAULT_CURRENCY_SYMBOL,
- )
+ # Send notification about payment
+ try:
+ notification_service = NotificationService(bot, settings, i18n)
+ user = await user_dal.get_user_by_id(session, user_id)
+ await notification_service.notify_payment_received(
+ user_id=user_id,
+ amount=float(invoice.amount),
+ currency=invoice.asset or settings.DEFAULT_CURRENCY_SYMBOL,
+ months=months,
+ payment_provider="crypto_pay",
+ username=user.username if user else None
+ )
+ except Exception as e:
+ logging.error(f"Failed to send crypto_pay payment notification: {e}")
async def webhook_route(self, request: web.Request) -> web.Response:
if not self.configured or not self.client:
diff --git a/bot/services/notification_service.py b/bot/services/notification_service.py
index a8166dc..ee9e963 100644
--- a/bot/services/notification_service.py
+++ b/bot/services/notification_service.py
@@ -220,20 +220,7 @@ async def notify_admin_new_trial(bot: Bot, settings: Settings, i18n: JsonI18n,
await notification_service.notify_trial_activation(user_id, end_date)
-async def notify_admin_new_payment(bot: Bot, settings: Settings, i18n: JsonI18n,
- user_id: int, months: int, amount: float,
- currency: str | None = None) -> None:
- currency_symbol = currency or settings.DEFAULT_CURRENCY_SYMBOL
- await notify_admins(
- bot,
- settings,
- i18n,
- "admin_new_payment_notification",
- user_id=user_id,
- months=months,
- amount=f"{amount:.2f}",
- currency=currency_symbol,
- )
+
async def notify_admin_promo_activation(bot: Bot, settings: Settings,
diff --git a/bot/services/panel_api_service.py b/bot/services/panel_api_service.py
index a09f499..582ad55 100644
--- a/bot/services/panel_api_service.py
+++ b/bot/services/panel_api_service.py
@@ -21,6 +21,14 @@ class PanelApiService:
self.api_key = settings.PANEL_API_KEY
self._session: Optional[aiohttp.ClientSession] = None
self.default_client_ip = "127.0.0.1"
+
+ async def __aenter__(self):
+ """Context manager entry"""
+ return self
+
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
+ """Context manager exit - automatically close session"""
+ await self.close_session()
async def _get_session(self) -> aiohttp.ClientSession:
if self._session is None or self._session.closed:
@@ -32,7 +40,7 @@ class PanelApiService:
if self._session and not self._session.closed:
await self._session.close()
self._session = None
- logging.info("Panel API service HTTP session closed.")
+ logging.debug("Panel API service HTTP session closed.")
async def close(self):
"""Alias for close_session for API consistency."""
diff --git a/bot/services/stars_service.py b/bot/services/stars_service.py
index 524547b..548babe 100644
--- a/bot/services/stars_service.py
+++ b/bot/services/stars_service.py
@@ -10,7 +10,7 @@ from db.dal import payment_dal, user_dal
from .subscription_service import SubscriptionService
from .referral_service import ReferralService
from bot.middlewares.i18n import JsonI18n
-from .notification_service import notify_admin_new_payment
+from .notification_service import NotificationService
from bot.keyboards.inline.user_keyboards import get_connect_and_main_keyboard
@@ -153,13 +153,18 @@ class StarsService:
logging.error(
f"Failed to send stars payment success message: {e_send}")
- await notify_admin_new_payment(
- self.bot,
- self.settings,
- self.i18n,
- message.from_user.id,
- months,
- float(stars_amount),
- currency="XTR",
- )
+ # Send notification about payment
+ try:
+ notification_service = NotificationService(self.bot, self.settings, self.i18n)
+ user = await user_dal.get_user_by_id(session, message.from_user.id)
+ await notification_service.notify_payment_received(
+ user_id=message.from_user.id,
+ amount=float(stars_amount),
+ currency="XTR",
+ months=months,
+ payment_provider="stars",
+ username=user.username if user else None
+ )
+ except Exception as e:
+ logging.error(f"Failed to send stars payment notification: {e}")
diff --git a/bot/services/tribute_service.py b/bot/services/tribute_service.py
index c9d6882..f310f2d 100644
--- a/bot/services/tribute_service.py
+++ b/bot/services/tribute_service.py
@@ -13,7 +13,7 @@ from bot.middlewares.i18n import JsonI18n
from bot.services.subscription_service import SubscriptionService
from bot.services.panel_api_service import PanelApiService
from bot.services.referral_service import ReferralService
-from .notification_service import notify_admin_new_payment
+from .notification_service import NotificationService
from bot.keyboards.inline.user_keyboards import get_connect_and_main_keyboard
from db.dal import payment_dal, user_dal, subscription_dal
@@ -187,15 +187,20 @@ class TributeService:
logging.error(
f"Failed to send Tribute payment success message to user {user_id}: {e}")
- await notify_admin_new_payment(
- bot,
- settings,
- i18n,
- user_id,
- months,
- float(price_rub),
- currency="RUB",
- )
+ # Send notification about payment
+ try:
+ notification_service = NotificationService(bot, settings, i18n)
+ user = await user_dal.get_user_by_id(session, user_id)
+ await notification_service.notify_payment_received(
+ user_id=user_id,
+ amount=float(price_rub),
+ currency="RUB",
+ months=months,
+ payment_provider="tribute",
+ username=user.username if user else None
+ )
+ except Exception as e:
+ logging.error(f"Failed to send tribute payment notification: {e}")
else:
await session.commit()
return web.Response(status=200, text="ok")
diff --git a/locales/en.json b/locales/en.json
index 0705c2d..612a39c 100644
--- a/locales/en.json
+++ b/locales/en.json
@@ -234,7 +234,7 @@
"subscription_expired_yesterday_notification": "👋 Hi, {user_name}!\n\n⏳ Your VPN subscription expired yesterday ({end_date}).\n\nPlease renew it using the button below.",
"admin_new_trial_notification": "\ud83c\udf21 User {user_id} activated a free trial until {end_date}.",
- "admin_new_payment_notification": "\ud83d\udcb3 Payment received from user {user_id}: {months} mo. for {amount} {currency}.",
+
"admin_promo_activation_notification": "\ud83c\udf81 Promo code {code} activated by user {user_id} (+{bonus_days}d).",
"error_unknown": "An unknown error occurred.",
@@ -362,5 +362,41 @@
"admin_promo_creation_failed": "❌ Error creating promo code: {error}",
"admin_panel_back_button": "⬅️ Back",
- "admin_bulk_promo_creation_failed": "❌ Error creating promo codes: {error}"
+ "admin_bulk_promo_creation_failed": "❌ Error creating promo codes: {error}",
+
+ "admin_user_id_label": "🆔 ID:",
+ "admin_user_name_label": "👤 Name:",
+ "admin_user_username_label": "📱 Username:",
+ "admin_user_language_label": "🌍 Language:",
+ "admin_user_registration_label": "📅 Registration:",
+ "admin_user_status_label": "🛡 Status:",
+ "admin_user_referral_label": "🎁 Referred by:",
+ "admin_user_panel_uuid_label": "🔗 Panel UUID:",
+ "admin_user_panel_status_label": "📊 Panel Status:",
+ "admin_user_traffic_label": "📊 Traffic:",
+ "admin_user_subscription_label": "💼 Subscription:",
+ "admin_user_trial_label": "🏡 Trial:",
+
+ "admin_user_status_banned": "🚫 Banned",
+ "admin_user_status_active": "✅ Active",
+ "admin_user_trial_used": "Used",
+ "admin_user_trial_not_used": "Not used",
+ "admin_user_ban_action_banned": "banned",
+ "admin_user_ban_action_unbanned": "unbanned",
+ "admin_user_na_value": "N/A",
+
+ "admin_user_subscription_active": "Active until {end_date}",
+ "admin_user_subscription_expired": "Expired {end_date}",
+ "admin_user_subscription_none": "No active subscription",
+
+ "admin_user_logs_title": "📋 User logs {user_id}",
+ "admin_user_logs_empty": "No logs available",
+ "admin_user_logs_count": "Total entries: {count}",
+ "admin_user_logs_entry": "📅 {timestamp}\n📝 {event_type}: {content}",
+
+ "admin_user_actions_count_label": "📜 Total actions:",
+ "admin_user_subscription_active_until": "⏰ Active until:",
+ "admin_user_subscription_error": "Loading error",
+
+ "admin_bulk_promo_unique_generation_failed": "Failed to create unique promo code"
}
diff --git a/locales/ru.json b/locales/ru.json
index 15211e8..70e4bc8 100644
--- a/locales/ru.json
+++ b/locales/ru.json
@@ -234,7 +234,7 @@
"subscription_expired_yesterday_notification": "👋 Привет, {user_name}!\n\n⏳ Ваша подписка на VPN истекла сутки назад ({end_date}).\n\nПродлите её по кнопке ниже.",
"admin_new_trial_notification": "\ud83c\udf21 Пользователь {user_id} активировал пробный период до {end_date}.",
- "admin_new_payment_notification": "\ud83d\udcb3 Получен платеж от пользователя {user_id}: {months} мес. за {amount} {currency}.",
+
"admin_promo_activation_notification": "\ud83c\udf81 Пользователь {user_id} активировал промокод {code} (+{bonus_days} дн.)",
"error_unknown": "Произошла неизвестная ошибка.",
@@ -362,5 +362,41 @@
"admin_promo_creation_failed": "❌ Ошибка создания промокода: {error}",
"admin_panel_back_button": "⬅️ Назад",
- "admin_bulk_promo_creation_failed": "❌ Ошибка создания промокодов: {error}"
+ "admin_bulk_promo_creation_failed": "❌ Ошибка создания промокодов: {error}",
+
+ "admin_user_id_label": "🆔 ID:",
+ "admin_user_name_label": "👤 Имя:",
+ "admin_user_username_label": "📱 Username:",
+ "admin_user_language_label": "🌍 Язык:",
+ "admin_user_registration_label": "📅 Регистрация:",
+ "admin_user_status_label": "🛡 Статус:",
+ "admin_user_referral_label": "🎁 Привлечен по реферальной программе от:",
+ "admin_user_panel_uuid_label": "🔗 Panel UUID:",
+ "admin_user_panel_status_label": "📊 Статус на панели:",
+ "admin_user_traffic_label": "📊 Трафик:",
+ "admin_user_subscription_label": "💼 Подписка:",
+ "admin_user_trial_label": "🏡 Триал:",
+
+ "admin_user_status_banned": "🚫 Заблокирован",
+ "admin_user_status_active": "✅ Активен",
+ "admin_user_trial_used": "Использовал",
+ "admin_user_trial_not_used": "Не использовал",
+ "admin_user_ban_action_banned": "заблокирован",
+ "admin_user_ban_action_unbanned": "разблокирован",
+ "admin_user_na_value": "N/A",
+
+ "admin_user_subscription_active": "Активна до {end_date}",
+ "admin_user_subscription_expired": "Истекла {end_date}",
+ "admin_user_subscription_none": "Нет активной подписки",
+
+ "admin_user_logs_title": "📋 Логи пользователя {user_id}",
+ "admin_user_logs_empty": "Логи отсутствуют",
+ "admin_user_logs_count": "Всего записей: {count}",
+ "admin_user_logs_entry": "📅 {timestamp}\n📝 {event_type}: {content}",
+
+ "admin_user_actions_count_label": "📜 Всего действий:",
+ "admin_user_subscription_active_until": "⏰ Действует до:",
+ "admin_user_subscription_error": "Ошибка загрузки",
+
+ "admin_bulk_promo_unique_generation_failed": "Не удалось создать уникальный промокод"
}