From dff723aede6bf0e63effbe902dd10ca68a1df7d9 Mon Sep 17 00:00:00 2001 From: gy9vin Date: Tue, 20 Jan 2026 17:19:57 +0300 Subject: [PATCH] =?UTF-8?q?feat(monitoring):=20=D0=B4=D0=BE=D0=B1=D0=B0?= =?UTF-8?q?=D0=B2=D0=B8=D1=82=D1=8C=20=D0=BD=D0=B0=D1=81=D1=82=D1=80=D0=BE?= =?UTF-8?q?=D0=B9=D0=BA=D0=B8=20=D0=BC=D0=BE=D0=BD=D0=B8=D1=82=D0=BE=D1=80?= =?UTF-8?q?=D0=B8=D0=BD=D0=B3=D0=B0=20=D1=82=D1=80=D0=B0=D1=84=D0=B8=D0=BA?= =?UTF-8?q?=D0=B0=20=D0=B2=20=D0=B0=D0=B4=D0=BC=D0=B8=D0=BD=D0=BA=D1=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Добавлена кнопка "⚙️ Настройки трафика" в меню мониторинга - Добавлен UI для управления быстрой и суточной проверками трафика - Можно включать/выключать проверки, менять пороги и интервалы - Настройки сохраняются в БД через BotConfigurationService - Добавлены SETTING_HINTS с описаниями параметров --- app/handlers/admin/monitoring.py | 313 ++++++++++++++++++++++++ app/keyboards/admin.py | 4 + app/services/system_settings_service.py | 63 +++++ app/states.py | 1 + 4 files changed, 381 insertions(+) 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()