Files
tgit/app/bot/menu.py
ElebobaandClaude Opus 5.5 6c6f766abc
CI / ruff · mypy · pytest (pull_request) Successful in 4m49s
feat(alerts): алерты всей команде — детекторы, дедупликация, список (#15)
- детекторы по состоянию после каждого раунда снапшотов: краш, OOM,
  цикл рестартов, unhealthy, перегрузка ≥ 1 мин, мало места, Docker
  недоступен; пороги в config/alerts.yaml
- AlertService: ключ kind+service, повтор в 10 мин — счётчик в исходном
  сообщении, «✅ восстановлено» ответом; разовые события (🛡) со счётчиком
- рассылка всем активным (🛡 — только админам), темп ~25/с, RetryAfter,
  заблокировавший бота пропускается; alert_deliveries для правок и ответов
- «🚨 Алерты»: активные + 7 дней по 10, карточка; кнопки «📜 Логи»,
  «🩺 Статус» под алертом открывают новое сообщение, а не затирают алерт
- 🛡 атаки через агента идут через общий сервис алертов
- убраны заглушки «Раздел в разработке» — у всех разделов свои роутеры

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 15:36:09 +03:00

60 lines
1.8 KiB
Python

"""Main menu (reply keyboard) and in-section navigation (inline keyboard), SPEC §4.0."""
from enum import StrEnum
from aiogram import F
from aiogram.filters.callback_data import CallbackData
from aiogram.types import ReplyKeyboardMarkup
from aiogram.utils.keyboard import ReplyKeyboardBuilder
from magic_filter import MagicFilter
from app.texts import Texts
class Section(StrEnum):
COMMIT = "commit"
STATUS = "status"
LOGS = "logs"
ALERTS = "alerts"
MEMBERS = "members"
class NavAction(StrEnum):
BACK = "back"
CANCEL = "cancel"
class Nav(CallbackData, prefix="nav"):
# Telegram limits callback_data to 64 bytes: only short ids here, never paths (ARCHITECTURE §2.1).
action: NavAction
def section_by_button(texts: Texts) -> dict[str, Section]:
m = texts.menu
return {
m.commit: Section.COMMIT,
m.status: Section.STATUS,
m.logs: Section.LOGS,
m.alerts: Section.ALERTS,
m.members: Section.MEMBERS,
}
def free_input(texts: Texts) -> MagicFilter:
"""Input for a dialog step — anything but a main-menu button or a bot command: those must work from any step,
otherwise «🩺 Статус» pressed while the bot waits for a file name becomes the file name."""
return ~F.text.in_(set(section_by_button(texts))) & ~F.text.regexp(r"^/(start|menu|help)(@\w+)?(\s|$)")
def main_menu(texts: Texts, *, is_admin: bool) -> ReplyKeyboardMarkup:
m = texts.menu
kb = ReplyKeyboardBuilder()
kb.button(text=m.commit)
kb.button(text=m.status)
kb.button(text=m.logs)
kb.button(text=m.alerts)
if is_admin:
kb.button(text=m.members)
kb.adjust(2, 2, 1)
return kb.as_markup(resize_keyboard=True, is_persistent=True, input_field_placeholder=m.placeholder)