CI / ruff · mypy · pytest (pull_request) Successful in 9m40s
- app/logs/grep.py: подмножество grep (шаблон, -i -v -E -c -C N), разбор shlex без исполнения, BRE → regex, таймаут на строку и бюджет на поиск, лимиты 200 символов / 500 строк вывода - app/logs/journal.py: каждый поиск в log_queries (L-6) - бот: выбор контейнера, хвост 50/200 строк или 5 мин/1 ч/24 ч, свой grep по 24 ч, длинный результат — файлом .log (L-5) - логи Docker: не больше 50 000 строк и 5 МБ, сохраняются новейшие - fix: кнопки меню и /menu, /help больше не проглатываются шагами ввода (имя файла, содержимое, сообщение коммита, grep) — фильтр free_input Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
67 lines
2.1 KiB
Python
67 lines
2.1 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 InlineKeyboardMarkup, ReplyKeyboardMarkup
|
|
from aiogram.utils.keyboard import InlineKeyboardBuilder, 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)
|
|
|
|
|
|
def section_nav(texts: Texts) -> InlineKeyboardMarkup:
|
|
kb = InlineKeyboardBuilder()
|
|
kb.button(text=texts.menu.back, callback_data=Nav(action=NavAction.BACK))
|
|
kb.button(text=texts.menu.cancel, callback_data=Nav(action=NavAction.CANCEL))
|
|
return kb.as_markup()
|