diff --git a/.env.example b/.env.example
index d7e6c5a..73de37c 100644
--- a/.env.example
+++ b/.env.example
@@ -165,6 +165,7 @@ LOG_LEVEL=INFO #
LOG_STORE_MESSAGE_CONTENT=False # Store message/callback content in DB logs
LOG_STORE_RAW_UPDATES=False # Store raw update payload snippets in DB logs
LOG_EXPORT_INCLUDE_SENSITIVE=False # Include content/raw update columns in admin CSV export
+LOG_ADMIN_HIDE=False # Hide admin actions from "All message logs" UI and CSV export
# Admin Logging Configuration
LOG_CHAT_ID=-1001234567890 # Telegram chat/group ID for admin notifications
diff --git a/README.md b/README.md
index 79cc3b6..2fc3dd7 100644
--- a/README.md
+++ b/README.md
@@ -118,6 +118,18 @@
| `SEVERPAY_LIFETIME_MINUTES` | (Опционально) Время жизни платежной ссылки в минутах (30–4320). |
+
+ Настройки логирования
+
+ | Переменная | Описание | Пример |
+ | --- | --- | --- |
+ | `LOGS_PAGE_SIZE` | Количество записей на странице в разделе админ-логов. | `10` |
+ | `LOG_STORE_MESSAGE_CONTENT` | Сохранять ли содержимое сообщений/колбэков в БД логов (`true`/`false`). | `false` |
+ | `LOG_STORE_RAW_UPDATES` | Сохранять ли превью сырого Telegram update в БД логов (`true`/`false`). | `false` |
+ | `LOG_EXPORT_INCLUDE_SENSITIVE` | Добавлять ли в CSV экспорт чувствительные поля (`content`, `raw_update_preview`). | `false` |
+ | `LOG_ADMIN_HIDE` | Скрывать админские события (`ADMIN_IDS`) в интерфейсе «Все логи сообщений» и в CSV экспорте (`true`/`false`). Логи продолжают записываться в БД. | `true` |
+
+
Настройки подписок
diff --git a/bot/handlers/admin/logs_admin.py b/bot/handlers/admin/logs_admin.py
index 6316d29..fca23d0 100644
--- a/bot/handlers/admin/logs_admin.py
+++ b/bot/handlers/admin/logs_admin.py
@@ -171,9 +171,17 @@ async def view_all_logs_handler(callback: types.CallbackQuery,
await callback.answer("Error processing request.", show_alert=True)
return
+ hide_admin_events = bool(settings.LOG_ADMIN_HIDE)
logs_models = await message_log_dal.get_all_message_logs(
- session, settings.LOGS_PAGE_SIZE, page_idx * settings.LOGS_PAGE_SIZE)
- total_logs_count = await message_log_dal.count_all_message_logs(session)
+ session,
+ settings.LOGS_PAGE_SIZE,
+ page_idx * settings.LOGS_PAGE_SIZE,
+ hide_admin_events=hide_admin_events,
+ )
+ total_logs_count = await message_log_dal.count_all_message_logs(
+ session,
+ hide_admin_events=hide_admin_events,
+ )
await _display_formatted_logs(
target_message=callback.message,
@@ -344,8 +352,12 @@ async def export_logs_csv_handler(callback: types.CallbackQuery,
try:
# Get all logs (limit to 10000 for performance)
logs_models = await message_log_dal.get_all_message_logs(
- session, limit=10000, offset=0)
-
+ session,
+ limit=10000,
+ offset=0,
+ hide_admin_events=bool(settings.LOG_ADMIN_HIDE),
+ )
+
if not logs_models:
await callback.message.answer(_(
"admin_logs_csv_no_data"
@@ -355,7 +367,7 @@ async def export_logs_csv_handler(callback: types.CallbackQuery,
# Create CSV content
csv_buffer = io.StringIO()
csv_writer = csv.writer(csv_buffer, delimiter=',', quotechar='"', quoting=csv.QUOTE_MINIMAL)
-
+
# Write header
headers = [
_("admin_csv_header_log_id"),
@@ -374,16 +386,16 @@ async def export_logs_csv_handler(callback: types.CallbackQuery,
_("admin_csv_header_raw_update_preview"),
])
csv_writer.writerow(headers)
-
+
# Write data rows
for log in logs_models:
# Format timestamp
timestamp_str = log.timestamp.strftime('%Y-%m-%d %H:%M:%S UTC') if log.timestamp else ''
-
+
# Clean content and raw_update_preview (remove newlines and quotes for CSV)
content_clean = (log.content or '').replace('\n', ' ').replace('\r', ' ').strip()
raw_update_clean = (log.raw_update_preview or '').replace('\n', ' ').replace('\r', ' ').strip()
-
+
row = [
log.log_id or '',
timestamp_str,
@@ -400,21 +412,21 @@ async def export_logs_csv_handler(callback: types.CallbackQuery,
raw_update_clean,
])
csv_writer.writerow(row)
-
+
# Create file
csv_content = csv_buffer.getvalue()
csv_buffer.close()
-
+
# Generate filename with current timestamp
now = datetime.now()
filename = f"message_logs_{now.strftime('%Y%m%d_%H%M%S')}.csv"
-
+
# Send as document
csv_file = types.BufferedInputFile(
csv_content.encode('utf-8-sig'), # BOM for Excel compatibility
filename=filename
)
-
+
await callback.message.answer_document(
csv_file,
caption=_(
@@ -423,7 +435,7 @@ async def export_logs_csv_handler(callback: types.CallbackQuery,
date=now.strftime('%Y-%m-%d %H:%M:%S')
)
)
-
+
except Exception as e:
logging.error(f"Error exporting logs to CSV: {e}", exc_info=True)
await callback.message.answer(_(
diff --git a/bot/handlers/user/payment.py b/bot/handlers/user/payment.py
index 0dd571e..53ebdd7 100644
--- a/bot/handlers/user/payment.py
+++ b/bot/handlers/user/payment.py
@@ -645,8 +645,20 @@ async def yookassa_webhook_route(request: web.Request):
yookassa_service,
lknpd_service)
if not processed:
- await session.rollback()
- return web.Response(status=503, text="yookassa_processing_failed_retry")
+ # process_successful_payment uses False for permanent business failures
+ # (e.g. user not found / metadata issues) and may have already updated
+ # the payment status. Commit the status and ACK the webhook to stop
+ # indefinite provider retries.
+ try:
+ await session.commit()
+ except Exception:
+ await session.rollback()
+ logging.exception(
+ "Failed to commit failure status for YooKassa payment %s",
+ payment_dict_for_processing.get("id"),
+ )
+ return web.Response(status=503, text="yookassa_processing_failed_retry")
+ return web.Response(status=200, text="ok")
await session.commit()
else:
logging.warning(
diff --git a/config/settings.py b/config/settings.py
index eea4f32..fb2087f 100644
--- a/config/settings.py
+++ b/config/settings.py
@@ -568,6 +568,11 @@ class Settings(BaseSettings):
description="Include content/raw update fields in admin CSV export",
)
+ LOG_ADMIN_HIDE: bool = Field(
+ default=False,
+ description="Hide admin-generated events from admin logs UI and CSV export",
+ )
+
@field_validator('LOG_LEVEL', mode='before')
@classmethod
def normalize_log_level(cls, v):
diff --git a/db/dal/message_log_dal.py b/db/dal/message_log_dal.py
index 576a1a3..6a0e16b 100644
--- a/db/dal/message_log_dal.py
+++ b/db/dal/message_log_dal.py
@@ -22,16 +22,23 @@ async def create_message_log(session: AsyncSession,
return None
-async def get_all_message_logs(session: AsyncSession, limit: int,
- offset: int) -> List[MessageLog]:
- stmt = select(MessageLog).order_by(
- MessageLog.timestamp.desc()).limit(limit).offset(offset)
+async def get_all_message_logs(session: AsyncSession,
+ limit: int,
+ offset: int,
+ hide_admin_events: bool = False) -> List[MessageLog]:
+ stmt = select(MessageLog)
+ if hide_admin_events:
+ stmt = stmt.where(MessageLog.is_admin_event.is_(False))
+ stmt = stmt.order_by(MessageLog.timestamp.desc()).limit(limit).offset(offset)
result = await session.execute(stmt)
return result.scalars().all()
-async def count_all_message_logs(session: AsyncSession) -> int:
+async def count_all_message_logs(session: AsyncSession,
+ hide_admin_events: bool = False) -> int:
stmt = select(func.count()).select_from(MessageLog)
+ if hide_admin_events:
+ stmt = stmt.where(MessageLog.is_admin_event.is_(False))
result = await session.execute(stmt)
return result.scalar_one()