feat(monitoring): добавить настройки мониторинга трафика в админку

- Добавлена кнопка "⚙️ Настройки трафика" в меню мониторинга
  - Добавлен UI для управления быстрой и суточной проверками трафика
  - Можно включать/выключать проверки, менять пороги и интервалы
  - Настройки сохраняются в БД через BotConfigurationService
  - Добавлены SETTING_HINTS с описаниями параметров
This commit is contained in:
gy9vin
2026-01-20 17:19:57 +03:00
parent 0a51e4d9ea
commit dff723aede
4 changed files with 381 additions and 0 deletions

View File

@@ -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 = (
"⚙️ <b>Настройки мониторинга трафика</b>\n\n"
f"<b>Быстрая проверка:</b> {fast_status}\n"
f"• Интервал: {settings.TRAFFIC_FAST_CHECK_INTERVAL_MINUTES} мин\n"
f"• Порог дельты: {settings.TRAFFIC_FAST_CHECK_THRESHOLD_GB} ГБ\n\n"
f"<b>Суточная проверка:</b> {daily_status}\n"
f"• Время: {settings.TRAFFIC_DAILY_CHECK_TIME} UTC\n"
f"• Порог: {settings.TRAFFIC_DAILY_THRESHOLD_GB} ГБ\n\n"
f"<b>Общие:</b>\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)

View File

@@ -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"
)
],
[

View File

@@ -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

View File

@@ -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()