diff --git a/app/handlers/admin/monitoring.py b/app/handlers/admin/monitoring.py
index 6efae06c..3c5d28bc 100644
--- a/app/handlers/admin/monitoring.py
+++ b/app/handlers/admin/monitoring.py
@@ -1794,5 +1794,318 @@ async def process_notification_value_input(message: Message, state: FSMContext):
await state.clear()
+# ============== Настройки мониторинга трафика ==============
+
+def _format_traffic_toggle(enabled: bool) -> str:
+ return "🟢 Вкл" if enabled else "🔴 Выкл"
+
+
+def _build_traffic_settings_keyboard() -> InlineKeyboardMarkup:
+ """Строит клавиатуру настроек мониторинга трафика."""
+ fast_enabled = settings.TRAFFIC_FAST_CHECK_ENABLED
+ daily_enabled = settings.TRAFFIC_DAILY_CHECK_ENABLED
+
+ fast_interval = settings.TRAFFIC_FAST_CHECK_INTERVAL_MINUTES
+ fast_threshold = settings.TRAFFIC_FAST_CHECK_THRESHOLD_GB
+ daily_time = settings.TRAFFIC_DAILY_CHECK_TIME
+ daily_threshold = settings.TRAFFIC_DAILY_THRESHOLD_GB
+ cooldown = settings.TRAFFIC_NOTIFICATION_COOLDOWN_MINUTES
+
+ return InlineKeyboardMarkup(inline_keyboard=[
+ [InlineKeyboardButton(
+ text=f"{_format_traffic_toggle(fast_enabled)} Быстрая проверка",
+ callback_data="admin_traffic_toggle_fast"
+ )],
+ [InlineKeyboardButton(
+ text=f"⏱ Интервал: {fast_interval} мин",
+ callback_data="admin_traffic_edit_fast_interval"
+ )],
+ [InlineKeyboardButton(
+ text=f"📊 Порог дельты: {fast_threshold} ГБ",
+ callback_data="admin_traffic_edit_fast_threshold"
+ )],
+ [InlineKeyboardButton(
+ text=f"{_format_traffic_toggle(daily_enabled)} Суточная проверка",
+ callback_data="admin_traffic_toggle_daily"
+ )],
+ [InlineKeyboardButton(
+ text=f"🕐 Время проверки: {daily_time}",
+ callback_data="admin_traffic_edit_daily_time"
+ )],
+ [InlineKeyboardButton(
+ text=f"📈 Суточный порог: {daily_threshold} ГБ",
+ callback_data="admin_traffic_edit_daily_threshold"
+ )],
+ [InlineKeyboardButton(
+ text=f"⏳ Кулдаун: {cooldown} мин",
+ callback_data="admin_traffic_edit_cooldown"
+ )],
+ [InlineKeyboardButton(text="⬅️ Назад", callback_data="admin_monitoring")],
+ ])
+
+
+def _build_traffic_settings_text() -> str:
+ """Строит текст настроек мониторинга трафика."""
+ fast_enabled = settings.TRAFFIC_FAST_CHECK_ENABLED
+ daily_enabled = settings.TRAFFIC_DAILY_CHECK_ENABLED
+
+ fast_status = _format_traffic_toggle(fast_enabled)
+ daily_status = _format_traffic_toggle(daily_enabled)
+
+ text = (
+ "⚙️ Настройки мониторинга трафика\n\n"
+ f"Быстрая проверка: {fast_status}\n"
+ f"• Интервал: {settings.TRAFFIC_FAST_CHECK_INTERVAL_MINUTES} мин\n"
+ f"• Порог дельты: {settings.TRAFFIC_FAST_CHECK_THRESHOLD_GB} ГБ\n\n"
+ f"Суточная проверка: {daily_status}\n"
+ f"• Время: {settings.TRAFFIC_DAILY_CHECK_TIME} UTC\n"
+ f"• Порог: {settings.TRAFFIC_DAILY_THRESHOLD_GB} ГБ\n\n"
+ f"Общие:\n"
+ f"• Кулдаун уведомлений: {settings.TRAFFIC_NOTIFICATION_COOLDOWN_MINUTES} мин\n"
+ )
+
+ # Информация о фильтрах
+ monitored_nodes = settings.get_traffic_monitored_nodes()
+ ignored_nodes = settings.get_traffic_ignored_nodes()
+ excluded_uuids = settings.get_traffic_excluded_user_uuids()
+
+ if monitored_nodes:
+ text += f"• Мониторим только: {len(monitored_nodes)} нод(ы)\n"
+ if ignored_nodes:
+ text += f"• Игнорируем: {len(ignored_nodes)} нод(ы)\n"
+ if excluded_uuids:
+ text += f"• Исключено юзеров: {len(excluded_uuids)}\n"
+
+ return text
+
+
+@router.callback_query(F.data == "admin_mon_traffic_settings")
+@admin_required
+async def admin_traffic_settings(callback: CallbackQuery):
+ """Показывает настройки мониторинга трафика."""
+ try:
+ text = _build_traffic_settings_text()
+ keyboard = _build_traffic_settings_keyboard()
+ await callback.message.edit_text(text, parse_mode="HTML", reply_markup=keyboard)
+ except Exception as e:
+ logger.error(f"Ошибка отображения настроек трафика: {e}")
+ await callback.answer("❌ Ошибка загрузки настроек", show_alert=True)
+
+
+@router.callback_query(F.data == "admin_traffic_toggle_fast")
+@admin_required
+async def toggle_fast_check(callback: CallbackQuery):
+ """Переключает быструю проверку трафика."""
+ try:
+ from app.services.system_settings_service import BotConfigurationService
+
+ current = settings.TRAFFIC_FAST_CHECK_ENABLED
+ new_value = not current
+
+ async with AsyncSessionLocal() as db:
+ await BotConfigurationService.set_value(db, "TRAFFIC_FAST_CHECK_ENABLED", new_value)
+ await db.commit()
+
+ await callback.answer("✅ Включено" if new_value else "⏸️ Отключено")
+
+ # Обновляем отображение
+ text = _build_traffic_settings_text()
+ keyboard = _build_traffic_settings_keyboard()
+ await callback.message.edit_text(text, parse_mode="HTML", reply_markup=keyboard)
+
+ except Exception as e:
+ logger.error(f"Ошибка переключения быстрой проверки: {e}")
+ await callback.answer("❌ Ошибка", show_alert=True)
+
+
+@router.callback_query(F.data == "admin_traffic_toggle_daily")
+@admin_required
+async def toggle_daily_check(callback: CallbackQuery):
+ """Переключает суточную проверку трафика."""
+ try:
+ from app.services.system_settings_service import BotConfigurationService
+
+ current = settings.TRAFFIC_DAILY_CHECK_ENABLED
+ new_value = not current
+
+ async with AsyncSessionLocal() as db:
+ await BotConfigurationService.set_value(db, "TRAFFIC_DAILY_CHECK_ENABLED", new_value)
+ await db.commit()
+
+ await callback.answer("✅ Включено" if new_value else "⏸️ Отключено")
+
+ text = _build_traffic_settings_text()
+ keyboard = _build_traffic_settings_keyboard()
+ await callback.message.edit_text(text, parse_mode="HTML", reply_markup=keyboard)
+
+ except Exception as e:
+ logger.error(f"Ошибка переключения суточной проверки: {e}")
+ await callback.answer("❌ Ошибка", show_alert=True)
+
+
+@router.callback_query(F.data == "admin_traffic_edit_fast_interval")
+@admin_required
+async def edit_fast_interval(callback: CallbackQuery, state: FSMContext):
+ """Начинает редактирование интервала быстрой проверки."""
+ await state.set_state(AdminStates.editing_traffic_setting)
+ await state.update_data(
+ traffic_setting_key="TRAFFIC_FAST_CHECK_INTERVAL_MINUTES",
+ traffic_setting_type="int",
+ settings_message_chat=callback.message.chat.id,
+ settings_message_id=callback.message.message_id,
+ )
+ await callback.answer()
+ await callback.message.answer(
+ "⏱ Введите интервал быстрой проверки в минутах (минимум 1):"
+ )
+
+
+@router.callback_query(F.data == "admin_traffic_edit_fast_threshold")
+@admin_required
+async def edit_fast_threshold(callback: CallbackQuery, state: FSMContext):
+ """Начинает редактирование порога быстрой проверки."""
+ await state.set_state(AdminStates.editing_traffic_setting)
+ await state.update_data(
+ traffic_setting_key="TRAFFIC_FAST_CHECK_THRESHOLD_GB",
+ traffic_setting_type="float",
+ settings_message_chat=callback.message.chat.id,
+ settings_message_id=callback.message.message_id,
+ )
+ await callback.answer()
+ await callback.message.answer(
+ "📊 Введите порог дельты трафика в ГБ (например: 5.0):"
+ )
+
+
+@router.callback_query(F.data == "admin_traffic_edit_daily_time")
+@admin_required
+async def edit_daily_time(callback: CallbackQuery, state: FSMContext):
+ """Начинает редактирование времени суточной проверки."""
+ await state.set_state(AdminStates.editing_traffic_setting)
+ await state.update_data(
+ traffic_setting_key="TRAFFIC_DAILY_CHECK_TIME",
+ traffic_setting_type="time",
+ settings_message_chat=callback.message.chat.id,
+ settings_message_id=callback.message.message_id,
+ )
+ await callback.answer()
+ await callback.message.answer(
+ "🕐 Введите время суточной проверки в формате HH:MM (UTC):\n"
+ "Например: 00:00, 03:00, 12:30"
+ )
+
+
+@router.callback_query(F.data == "admin_traffic_edit_daily_threshold")
+@admin_required
+async def edit_daily_threshold(callback: CallbackQuery, state: FSMContext):
+ """Начинает редактирование суточного порога."""
+ await state.set_state(AdminStates.editing_traffic_setting)
+ await state.update_data(
+ traffic_setting_key="TRAFFIC_DAILY_THRESHOLD_GB",
+ traffic_setting_type="float",
+ settings_message_chat=callback.message.chat.id,
+ settings_message_id=callback.message.message_id,
+ )
+ await callback.answer()
+ await callback.message.answer(
+ "📈 Введите суточный порог трафика в ГБ (например: 50.0):"
+ )
+
+
+@router.callback_query(F.data == "admin_traffic_edit_cooldown")
+@admin_required
+async def edit_cooldown(callback: CallbackQuery, state: FSMContext):
+ """Начинает редактирование кулдауна уведомлений."""
+ await state.set_state(AdminStates.editing_traffic_setting)
+ await state.update_data(
+ traffic_setting_key="TRAFFIC_NOTIFICATION_COOLDOWN_MINUTES",
+ traffic_setting_type="int",
+ settings_message_chat=callback.message.chat.id,
+ settings_message_id=callback.message.message_id,
+ )
+ await callback.answer()
+ await callback.message.answer(
+ "⏳ Введите кулдаун уведомлений в минутах (минимум 1):"
+ )
+
+
+@router.message(AdminStates.editing_traffic_setting)
+async def process_traffic_setting_input(message: Message, state: FSMContext):
+ """Обрабатывает ввод настройки мониторинга трафика."""
+ from app.services.system_settings_service import BotConfigurationService
+
+ data = await state.get_data()
+ if not data:
+ await state.clear()
+ await message.answer("ℹ️ Контекст утерян, попробуйте снова из меню настроек.")
+ return
+
+ raw_value = (message.text or "").strip()
+ setting_key = data.get("traffic_setting_key")
+ setting_type = data.get("traffic_setting_type")
+
+ # Валидация и парсинг значения
+ try:
+ if setting_type == "int":
+ value = int(raw_value)
+ if value < 1:
+ raise ValueError("Значение должно быть >= 1")
+ elif setting_type == "float":
+ value = float(raw_value.replace(",", "."))
+ if value <= 0:
+ raise ValueError("Значение должно быть > 0")
+ elif setting_type == "time":
+ # Валидация формата HH:MM
+ import re
+ if not re.match(r"^\d{1,2}:\d{2}$", raw_value):
+ raise ValueError("Неверный формат времени. Используйте HH:MM")
+ parts = raw_value.split(":")
+ hours, minutes = int(parts[0]), int(parts[1])
+ if hours < 0 or hours > 23 or minutes < 0 or minutes > 59:
+ raise ValueError("Неверное время")
+ value = f"{hours:02d}:{minutes:02d}"
+ else:
+ value = raw_value
+ except ValueError as e:
+ await message.answer(f"❌ {str(e)}")
+ return
+
+ # Сохраняем значение
+ try:
+ async with AsyncSessionLocal() as db:
+ await BotConfigurationService.set_value(db, setting_key, value)
+ await db.commit()
+
+ back_keyboard = InlineKeyboardMarkup(
+ inline_keyboard=[
+ [InlineKeyboardButton(text="⬅️ К настройкам трафика", callback_data="admin_mon_traffic_settings")]
+ ]
+ )
+ await message.answer("✅ Настройка сохранена!", reply_markup=back_keyboard)
+
+ # Обновляем исходное сообщение с настройками
+ chat_id = data.get("settings_message_chat")
+ message_id = data.get("settings_message_id")
+ if chat_id and message_id:
+ try:
+ text = _build_traffic_settings_text()
+ keyboard = _build_traffic_settings_keyboard()
+ await message.bot.edit_message_text(
+ chat_id=chat_id,
+ message_id=message_id,
+ text=text,
+ parse_mode="HTML",
+ reply_markup=keyboard
+ )
+ except Exception:
+ pass # Игнорируем если сообщение уже удалено
+
+ except Exception as e:
+ logger.error(f"Ошибка сохранения настройки трафика: {e}")
+ await message.answer(f"❌ Ошибка сохранения: {str(e)}")
+
+ await state.clear()
+
+
def register_handlers(dp):
dp.include_router(router)
diff --git a/app/keyboards/admin.py b/app/keyboards/admin.py
index 128e91bf..f5c26d68 100644
--- a/app/keyboards/admin.py
+++ b/app/keyboards/admin.py
@@ -1779,6 +1779,10 @@ def get_monitoring_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
InlineKeyboardButton(
text=_t(texts, "ADMIN_MONITORING_TEST_NOTIFICATIONS", "🧪 Тест уведомлений"),
callback_data="admin_mon_test_notifications"
+ ),
+ InlineKeyboardButton(
+ text="⚙️ Настройки трафика",
+ callback_data="admin_mon_traffic_settings"
)
],
[
diff --git a/app/services/system_settings_service.py b/app/services/system_settings_service.py
index 1f1187b0..af4c1583 100644
--- a/app/services/system_settings_service.py
+++ b/app/services/system_settings_service.py
@@ -276,6 +276,18 @@ class BotConfigurationService:
"TRAFFIC_MONITORING_INTERVAL_HOURS": "MONITORING",
"TRAFFIC_MONITORED_NODES": "MONITORING",
"TRAFFIC_SNAPSHOT_TTL_HOURS": "MONITORING",
+ "TRAFFIC_FAST_CHECK_ENABLED": "MONITORING",
+ "TRAFFIC_FAST_CHECK_INTERVAL_MINUTES": "MONITORING",
+ "TRAFFIC_FAST_CHECK_THRESHOLD_GB": "MONITORING",
+ "TRAFFIC_DAILY_CHECK_ENABLED": "MONITORING",
+ "TRAFFIC_DAILY_CHECK_TIME": "MONITORING",
+ "TRAFFIC_DAILY_THRESHOLD_GB": "MONITORING",
+ "TRAFFIC_IGNORED_NODES": "MONITORING",
+ "TRAFFIC_EXCLUDED_USER_UUIDS": "MONITORING",
+ "TRAFFIC_NOTIFICATION_COOLDOWN_MINUTES": "MONITORING",
+ "SUSPICIOUS_NOTIFICATIONS_TOPIC_ID": "MONITORING",
+ "TRAFFIC_CHECK_BATCH_SIZE": "MONITORING",
+ "TRAFFIC_CHECK_CONCURRENCY": "MONITORING",
"ENABLE_LOGO_MODE": "INTERFACE_BRANDING",
"LOGO_FILE": "INTERFACE_BRANDING",
"HIDE_SUBSCRIPTION_LINK": "INTERFACE_SUBSCRIPTION",
@@ -780,6 +792,57 @@ class BotConfigurationService:
),
"dependencies": "TRAFFIC_MONITORING_ENABLED, Redis",
},
+ "TRAFFIC_FAST_CHECK_ENABLED": {
+ "description": (
+ "Включает быструю проверку трафика. "
+ "Система сравнивает текущий трафик со snapshot и уведомляет о превышениях дельты."
+ ),
+ "format": "Булево значение.",
+ "example": "true",
+ "warning": "Требует Redis для хранения snapshot. При отключении проверки не выполняются.",
+ "dependencies": "Redis, TRAFFIC_FAST_CHECK_INTERVAL_MINUTES, TRAFFIC_FAST_CHECK_THRESHOLD_GB",
+ },
+ "TRAFFIC_FAST_CHECK_INTERVAL_MINUTES": {
+ "description": "Интервал быстрой проверки трафика в минутах.",
+ "format": "Целое число минут (минимум 1).",
+ "example": "10",
+ "warning": "Слишком малый интервал создаёт нагрузку на Remnawave API.",
+ "dependencies": "TRAFFIC_FAST_CHECK_ENABLED",
+ },
+ "TRAFFIC_FAST_CHECK_THRESHOLD_GB": {
+ "description": "Порог дельты трафика в ГБ для быстрой проверки. При превышении отправляется уведомление.",
+ "format": "Число с плавающей точкой.",
+ "example": "5.0",
+ "warning": "Слишком низкий порог приведёт к частым уведомлениям.",
+ "dependencies": "TRAFFIC_FAST_CHECK_ENABLED",
+ },
+ "TRAFFIC_DAILY_CHECK_ENABLED": {
+ "description": "Включает суточную проверку трафика через bandwidth-stats API.",
+ "format": "Булево значение.",
+ "example": "true",
+ "warning": "Проверка выполняется в указанное время (TRAFFIC_DAILY_CHECK_TIME).",
+ "dependencies": "TRAFFIC_DAILY_CHECK_TIME, TRAFFIC_DAILY_THRESHOLD_GB",
+ },
+ "TRAFFIC_DAILY_CHECK_TIME": {
+ "description": "Время суточной проверки трафика в формате HH:MM (UTC).",
+ "format": "Строка времени HH:MM.",
+ "example": "00:00",
+ "warning": "Время указывается в UTC.",
+ "dependencies": "TRAFFIC_DAILY_CHECK_ENABLED",
+ },
+ "TRAFFIC_DAILY_THRESHOLD_GB": {
+ "description": "Порог суточного трафика в ГБ. При превышении за 24 часа отправляется уведомление.",
+ "format": "Число с плавающей точкой.",
+ "example": "50.0",
+ "warning": "Учитывается весь трафик за последние 24 часа.",
+ "dependencies": "TRAFFIC_DAILY_CHECK_ENABLED",
+ },
+ "TRAFFIC_NOTIFICATION_COOLDOWN_MINUTES": {
+ "description": "Кулдаун уведомлений по одному пользователю в минутах.",
+ "format": "Целое число минут.",
+ "example": "60",
+ "warning": "Защита от спама уведомлениями по одному и тому же пользователю.",
+ },
}
@classmethod
diff --git a/app/states.py b/app/states.py
index e847e6b4..f06dbe3f 100644
--- a/app/states.py
+++ b/app/states.py
@@ -134,6 +134,7 @@ class AdminStates(StatesGroup):
editing_faq_title = State()
editing_faq_content = State()
editing_notification_value = State()
+ editing_traffic_setting = State()
confirming_sync = State()