feat(ui): emoji icons on menu buttons and screen titles

This commit is contained in:
hh committed 2026-10-12 23:50:09 +02:00
1 parent a471b1dfe2
commit 6ef321dc9f
13 files changed
+187 -159

No files matched your search

+6 -6
View File
@@ -178,17 +178,17 @@ def alert_keyboard(payload: dict) -> InlineKeyboardMarkup | None:
kb = InlineKeyboardBuilder() kb = InlineKeyboardBuilder()
if env.komodo.enabled and target_id: if env.komodo.enabled and target_id:
if target_type == "Stack": if target_type == "Stack":
kb.button(text="Стек", callback_data=f"go:stack:{target_id}") kb.button(text="📦 Стек", callback_data=f"go:stack:{target_id}")
kb.button(text="Логи", callback_data=f"go:log:{target_id}") kb.button(text="📜 Логи", callback_data=f"go:log:{target_id}")
elif target_type == "Server": elif target_type == "Server":
kb.button(text="Сервер", callback_data=f"go:srv:{target_id}") kb.button(text="🖥 Сервер", callback_data=f"go:srv:{target_id}")
elif target_type == "Action": elif target_type == "Action":
kb.button(text="Действие", callback_data=f"go:act:a:{target_id}") kb.button(text="🎛 Действие", callback_data=f"go:act:a:{target_id}")
elif target_type == "Procedure": elif target_type == "Procedure":
kb.button(text="Процедура", callback_data=f"go:act:p:{target_id}") kb.button(text="🧩 Процедура", callback_data=f"go:act:p:{target_id}")
mute = f"al:mute:{kind}" mute = f"al:mute:{kind}"
if kind != "Test" and len(mute) <= CALLBACK_LIMIT: if kind != "Test" and len(mute) <= CALLBACK_LIMIT:
kb.button(text="Не присылать такие", callback_data=mute) kb.button(text="🔕 Не присылать такие", callback_data=mute)
buttons = len(list(kb.buttons)) buttons = len(list(kb.buttons))
if not buttons: if not buttons:
return None return None
+5 -5
View File
@@ -38,10 +38,10 @@ async def _services_block() -> list[str]:
uptime = (window - lost) / window * 100 if window else None uptime = (window - lost) / window * 100 if window else None
falls = sum(row[2] for row in rows) falls = sum(row[2] for row in rows)
if not falls: if not falls:
return [f"<b>Сервисы</b>: аптайм {pct(uptime)}, падений не было"] return [f"📋 <b>Сервисы</b>: аптайм {pct(uptime)}, падений не было"]
word = plural(falls, "падение", "падения", "падений") word = plural(falls, "падение", "падения", "падений")
lines = [ lines = [
f"<b>Сервисы</b>: аптайм {pct(uptime)}, {falls} {word}, " f"📋 <b>Сервисы</b>: аптайм {pct(uptime)}, {falls} {word}, "
f"даунтайм {human_duration(lost)}" f"даунтайм {human_duration(lost)}"
] ]
for down, service, count in sorted(rows, key=lambda row: -row[0])[:SHOWN]: for down, service, count in sorted(rows, key=lambda row: -row[0])[:SHOWN]:
@@ -71,10 +71,10 @@ async def _fleet_block() -> list[str]:
komodo.names(), komodo.names(),
) )
except KomodoError as exc: except KomodoError as exc:
return [f"<b>Флот</b>: {esc(exc)}"] return [f"🛰 <b>Флот</b>: {esc(exc)}"]
tz = db.settings.get("tz") tz = db.settings.get("tz")
failed = [u for u in updates if u.done and not u.success] failed = [u for u in updates if u.done and not u.success]
lines = [f"<b>Флот</b>: операций {len(updates)}, неудачных {len(failed)}"] lines = [f"🛰 <b>Флот</b>: операций {len(updates)}, неудачных {len(failed)}"]
grouped: dict[tuple[str, str], list[Update]] = {} grouped: dict[tuple[str, str], list[Update]] = {}
for update in failed: for update in failed:
grouped.setdefault((update.operation, update.target_id), []).append(update) grouped.setdefault((update.operation, update.target_id), []).append(update)
@@ -93,7 +93,7 @@ async def digest_text() -> str:
blocks = [await _services_block()] blocks = [await _services_block()]
if env.komodo.enabled: if env.komodo.enabled:
blocks.append(await _fleet_block()) blocks.append(await _fleet_block())
lines = ["<b>Сводка за сутки</b>"] lines = ["🗞 <b>Сводка за сутки</b>"]
for block in blocks: for block in blocks:
if block: if block:
lines += ["", *block] lines += ["", *block]
+39 -34
View File
@@ -19,7 +19,7 @@ from healthbot.actionargs import (
) )
from healthbot.filters import Admin from healthbot.filters import Admin
from healthbot.handlers.menu import sync_commands from healthbot.handlers.menu import sync_commands
from healthbot.keyboards import back_to, back_to_menu from healthbot.keyboards import back_to, back_to_menu, pin_text
from healthbot.komodo import Detail, KomodoError, Resource, Update, komodo from healthbot.komodo import Detail, KomodoError, Resource, Update, komodo
from healthbot.runs import track from healthbot.runs import track
from healthbot.ui import ( from healthbot.ui import (
@@ -47,7 +47,8 @@ WIDE_FORM = 6
MAX_LABEL = 40 MAX_LABEL = 40
KIND_TITLE = {"a": "Действия", "p": "Процедуры"} KIND_TITLE = {"a": "Действия", "p": "Процедуры"}
KIND_ONE = {"a": "Действие", "p": "Процедура"} KIND_ONE = {"a": "Действие", "p": "Процедура"}
KIND_BACK = {"a": "К действию", "p": "К процедуре"} KIND_BACK = {"a": "⬅️ К действию", "p": "⬅️ К процедуре"}
KIND_ICON = {"a": "🎛", "p": "🧩"}
USAGE_FIELDS = 3 USAGE_FIELDS = 3
RUN_STATE = {"Ok": "успешно", "Failed": "с ошибкой", "Running": "идёт сейчас"} RUN_STATE = {"Ok": "успешно", "Failed": "с ошибкой", "Running": "идёт сейчас"}
MODE_TEXT = {PinMode.INSTANT: "запускается сразу", PinMode.FORM: "открывает форму"} MODE_TEXT = {PinMode.INSTANT: "запускается сразу", PinMode.FORM: "открывает форму"}
@@ -96,12 +97,13 @@ async def list_view(kind: str, page: int) -> tuple[str, types.InlineKeyboardMark
rows = [2] * (len(chunk) // 2) + [1] * (len(chunk) % 2) rows = [2] * (len(chunk) // 2) + [1] * (len(chunk) % 2)
nav = pager(kb, f"act:list:{kind}", page, total) nav = pager(kb, f"act:list:{kind}", page, total)
kb.button( kb.button(
text=f"{KIND_TITLE[other]}: {other_count}", callback_data=f"act:list:{other}:0" text=f"{KIND_ICON[other]} {KIND_TITLE[other]}: {other_count}",
callback_data=f"act:list:{other}:0",
) )
kb.button(text="Закреплённые", callback_data="set:pins") kb.button(text="📌 Закреплённые", callback_data="set:pins")
kb.button(text="Меню", callback_data="menu:main") kb.button(text="🏠 Меню", callback_data="menu:main")
kb.adjust(*rows, *([nav] if nav else []), 2, 1) kb.adjust(*rows, *([nav] if nav else []), 2, 1)
text = f"<b>{KIND_TITLE[kind]}</b>: {len(items)}" text = f"{KIND_ICON[kind]} <b>{KIND_TITLE[kind]}</b>: {len(items)}"
if not items: if not items:
text += "\nКлюч бота не видит ни одного. Нужен доступ Execute в Komodo." text += "\nКлюч бота не видит ни одного. Нужен доступ Execute в Komodo."
return text, kb.as_markup() return text, kb.as_markup()
@@ -170,7 +172,7 @@ async def card_view(
await state.set_state(None) await state.set_state(None)
await state.update_data(form=form) await state.update_data(form=form)
lines = [f"<b>{esc(detail.name)}</b>"] lines = [f"{KIND_ICON[kind]} <b>{esc(detail.name)}</b>"]
if detail.description: if detail.description:
lines.append(esc(detail.description)) lines.append(esc(detail.description))
if detail.stages: if detail.stages:
@@ -195,18 +197,18 @@ async def card_view(
for index, key in enumerate(defaults): for index, key in enumerate(defaults):
value = form["values"].get(key, defaults[key]) value = form["values"].get(key, defaults[key])
kb.button( kb.button(
text=label(f"{key}: {render(value)}", 40), text=label(f"✏️ {key}: {render(value)}", 40),
callback_data=f"act:f:{index}:{ref}", callback_data=f"act:f:{index}:{ref}",
) )
width = 2 if len(defaults) > WIDE_FORM else 1 width = 2 if len(defaults) > WIDE_FORM else 1
rows = [width] * (len(defaults) // width) + [1] * (len(defaults) % width) rows = [width] * (len(defaults) // width) + [1] * (len(defaults) % width)
kb.button(text="Запустить", callback_data=f"act:run:{ref}", style=SUCCESS) kb.button(text="▶️ Запустить", callback_data=f"act:run:{ref}", style=SUCCESS)
kb.button(text="Закрепить", callback_data=f"act:pin:{ref}") kb.button(text="📌 Закрепить", callback_data=f"act:pin:{ref}")
tail = 2 tail = 2
if form["values"]: if form["values"]:
kb.button(text="Сбросить", callback_data=f"act:reset:{ref}") kb.button(text="↩️ Сбросить", callback_data=f"act:reset:{ref}")
tail = 3 tail = 3
kb.button(text="К списку", callback_data=f"act:list:{kind}:0") kb.button(text="⬅️ К списку", callback_data=f"act:list:{kind}:0")
kb.adjust(*rows, tail, 1) kb.adjust(*rows, tail, 1)
return "\n".join(lines), kb.as_markup() return "\n".join(lines), kb.as_markup()
@@ -341,10 +343,10 @@ async def act_field(callback: types.CallbackQuery, state: FSMContext) -> None:
kb = InlineKeyboardBuilder() kb = InlineKeyboardBuilder()
if key in form["values"]: if key in form["values"]:
kb.button( kb.button(
text=label(f"Вернуть {render(default)}", 40), text=label(f"↩️ Вернуть {render(default)}", 40),
callback_data=f"act:fd:{_ref(form)}", callback_data=f"act:fd:{_ref(form)}",
) )
kb.button(text="Отмена", callback_data=f"act:back:{_ref(form)}") kb.button(text="✖️ Отмена", callback_data=f"act:back:{_ref(form)}")
kb.adjust(1) kb.adjust(1)
await show( await show(
callback, callback,
@@ -380,7 +382,7 @@ async def on_value(message: types.Message, state: FSMContext) -> None:
await show( await show(
message, message,
f"<b>{esc(key)}</b>: {esc(exc)}. Пришли ещё раз.", f"<b>{esc(key)}</b>: {esc(exc)}. Пришли ещё раз.",
back_to("Отмена", f"act:back:{_ref(form)}"), back_to("✖️ Отмена", f"act:back:{_ref(form)}"),
) )
return return
except KomodoError as exc: except KomodoError as exc:
@@ -413,7 +415,10 @@ async def _execute( # noqa: PLR0913
origin, origin,
title or name, title or name,
starter, starter,
back=[(KIND_BACK[kind], f"act:open:{kind}:{target_id}"), ("Меню", "menu:main")], back=[
(KIND_BACK[kind], f"act:open:{kind}:{target_id}"),
("🏠 Меню", "menu:main"),
],
verbose=kind == "a", verbose=kind == "a",
extra=_args_code(args), extra=_args_code(args),
new=new, new=new,
@@ -430,8 +435,8 @@ async def act_run(callback: types.CallbackQuery, state: FSMContext) -> None:
await _execute(callback, form["kind"], form["id"], form["name"], form["values"]) await _execute(callback, form["kind"], form["id"], form["name"], form["values"])
return return
kb = InlineKeyboardBuilder() kb = InlineKeyboardBuilder()
kb.button(text="Запустить", callback_data=f"act:go:{_ref(form)}", style=SUCCESS) kb.button(text="▶️ Запустить", callback_data=f"act:go:{_ref(form)}", style=SUCCESS)
kb.button(text="Отмена", callback_data=f"act:back:{_ref(form)}") kb.button(text="✖️ Отмена", callback_data=f"act:back:{_ref(form)}")
kb.adjust(2) kb.adjust(2)
changes = f"\nИзменено: {_values_line(form['values'])}" if form["values"] else "" changes = f"\nИзменено: {_values_line(form['values'])}" if form["values"] else ""
await show( await show(
@@ -461,9 +466,9 @@ async def act_pin(callback: types.CallbackQuery, state: FSMContext) -> None:
) )
kb = InlineKeyboardBuilder() kb = InlineKeyboardBuilder()
ref = _ref(form) ref = _ref(form)
kb.button(text="Запускать сразу", callback_data=f"act:pinmode:instant:{ref}") kb.button(text="⚡ Запускать сразу", callback_data=f"act:pinmode:instant:{ref}")
kb.button(text="Открывать форму", callback_data=f"act:pinmode:form:{ref}") kb.button(text="📝 Открывать форму", callback_data=f"act:pinmode:form:{ref}")
kb.button(text="Отмена", callback_data=f"act:back:{ref}") kb.button(text="✖️ Отмена", callback_data=f"act:back:{ref}")
kb.adjust(2, 1) kb.adjust(2, 1)
await show( await show(
callback, callback,
@@ -486,17 +491,17 @@ def pin_view(pin: Pin) -> tuple[str, types.InlineKeyboardMarkup]:
values, values,
] ]
kb = InlineKeyboardBuilder() kb = InlineKeyboardBuilder()
kb.button(text="Переименовать", callback_data=f"pin:ren:{pin.id}") kb.button(text="✏️ Переименовать", callback_data=f"pin:ren:{pin.id}")
other = PinMode.FORM if pin.mode is PinMode.INSTANT else PinMode.INSTANT other = PinMode.FORM if pin.mode is PinMode.INSTANT else PinMode.INSTANT
kb.button( kb.button(
text="Открывать форму" if other is PinMode.FORM else "Запускать сразу", text="📝 Открывать форму" if other is PinMode.FORM else "⚡ Запускать сразу",
callback_data=f"pin:mode:{pin.id}", callback_data=f"pin:mode:{pin.id}",
) )
kb.button(text="Выше", callback_data=f"pin:up:{pin.id}") kb.button(text="⬆️ Выше", callback_data=f"pin:up:{pin.id}")
kb.button(text="Ниже", callback_data=f"pin:down:{pin.id}") kb.button(text="⬇️ Ниже", callback_data=f"pin:down:{pin.id}")
kb.button(text="Открепить", callback_data=f"pin:del:{pin.id}", style=DANGER) kb.button(text="🗑 Открепить", callback_data=f"pin:del:{pin.id}", style=DANGER)
kb.button(text="Закреплённые", callback_data="set:pins") kb.button(text="📌 Закреплённые", callback_data="set:pins")
kb.button(text="Меню", callback_data="menu:main") kb.button(text="🏠 Меню", callback_data="menu:main")
kb.adjust(2, 2, 1, 2) kb.adjust(2, 2, 1, 2)
return "\n".join(lines), kb.as_markup() return "\n".join(lines), kb.as_markup()
@@ -527,12 +532,12 @@ async def pins_view() -> tuple[str, types.InlineKeyboardMarkup]:
pins = await db.pins.all() pins = await db.pins.all()
kb = InlineKeyboardBuilder() kb = InlineKeyboardBuilder()
for pin in pins: for pin in pins:
kb.button(text=label(pin.label, 40), callback_data=f"pin:open:{pin.id}") kb.button(text=label(pin_text(pin), 40), callback_data=f"pin:open:{pin.id}")
if enabled(): if enabled():
kb.button(text="Действия", callback_data="act:list:a:0") kb.button(text="🎛 Действия", callback_data="act:list:a:0")
kb.button(text="Меню", callback_data="menu:main") kb.button(text="🏠 Меню", callback_data="menu:main")
kb.adjust(*[1] * len(pins), 2) kb.adjust(*[1] * len(pins), 2)
text = "<b>Закреплённые</b>" text = "📌 <b>Закреплённые</b>"
if not pins: if not pins:
text += "\nПока пусто. Открой действие и нажми «Закрепить»." text += "\nПока пусто. Открой действие и нажми «Закрепить»."
return text, kb.as_markup() return text, kb.as_markup()
@@ -630,7 +635,7 @@ async def pin_rename(callback: types.CallbackQuery, state: FSMContext) -> None:
await show( await show(
callback, callback,
f"Пришли новое название для кнопки <b>{esc(pin.label)}</b>.", f"Пришли новое название для кнопки <b>{esc(pin.label)}</b>.",
back_to("Отмена", f"pin:open:{pin.id}"), back_to("✖️ Отмена", f"pin:open:{pin.id}"),
) )
await callback.answer() await callback.answer()
@@ -685,7 +690,7 @@ async def on_command(message: types.Message, state: FSMContext) -> None:
await show( await show(
message, message,
"Запуск действий командами выключен.", "Запуск действий командами выключен.",
back_to("Настройки", "set:main"), back_to("⚙️ Настройки", "set:main"),
) )
return return
if resource is None and not (enabled() and db.settings.get("commands")): if resource is None and not (enabled() and db.settings.get("commands")):
+13 -13
View File
@@ -134,12 +134,12 @@ def _choose_view(data: dict) -> tuple[str, types.InlineKeyboardMarkup]:
lines.append("За чем следить?") lines.append("За чем следить?")
if path: if path:
kb.button(text="Назад", callback_data="add:up") kb.button(text="⬆️ Назад", callback_data="add:up")
rows.append(1) rows.append(1)
kb.button(text=f"HTTP-статус {data['status']}", callback_data="add:status") kb.button(text=f"🔢 HTTP-статус {data['status']}", callback_data="add:status")
kb.button(text="Текст в ответе", callback_data="add:text") kb.button(text="🔤 Текст в ответе", callback_data="add:text")
kb.button(text="Тело целиком", callback_data="add:body") kb.button(text="📄 Тело целиком", callback_data="add:body")
kb.button(text="Отмена", callback_data="add:cancel") kb.button(text="✖️ Отмена", callback_data="add:cancel")
kb.adjust(*rows, 2, 2) kb.adjust(*rows, 2, 2)
return "\n".join(lines), kb.as_markup() return "\n".join(lines), kb.as_markup()
@@ -151,10 +151,10 @@ async def _probe(url: str) -> Probe:
def _unreachable_kb(*, editing: bool) -> types.InlineKeyboardMarkup: def _unreachable_kb(*, editing: bool) -> types.InlineKeyboardMarkup:
kb = InlineKeyboardBuilder() kb = InlineKeyboardBuilder()
kb.button(text="Повторить", callback_data="add:retry") kb.button(text="🔄 Повторить", callback_data="add:retry")
if not editing: if not editing:
kb.button(text="Добавить всё равно", callback_data="add:force") kb.button(text="➕ Добавить всё равно", callback_data="add:force")
kb.button(text="Отмена", callback_data="add:cancel") kb.button(text="✖️ Отмена", callback_data="add:cancel")
kb.adjust(2, 1) kb.adjust(2, 1)
return kb.as_markup() return kb.as_markup()
@@ -227,7 +227,7 @@ async def _add_many(message: types.Message, urls: list[str]) -> None:
lines = [f"Добавил {len(added)} из {len(urls)}, слежу за HTTP-статусом.", *added] lines = [f"Добавил {len(added)} из {len(urls)}, слежу за HTTP-статусом.", *added]
if skipped: if skipped:
lines += ["", "Пропустил:", *skipped] lines += ["", "Пропустил:", *skipped]
await show(message, "\n".join(lines), back_to("Сервисы", "menu:list:0")) await show(message, "\n".join(lines), back_to("📋 Сервисы", "menu:list:0"))
async def _on_urls(message: types.Message, state: FSMContext, urls: list[str]) -> None: async def _on_urls(message: types.Message, state: FSMContext, urls: list[str]) -> None:
@@ -265,7 +265,7 @@ async def on_new_url(message: types.Message, state: FSMContext) -> None:
await show(message, "Сервис уже удалён.", back_to_menu()) await show(message, "Сервис уже удалён.", back_to_menu())
return return
urls = extract_urls(message.text) urls = extract_urls(message.text)
back = back_to("Отмена", f"svc:open:{service.id}") back = back_to("✖️ Отмена", f"svc:open:{service.id}")
if urls is None or len(urls) != 1: if urls is None or len(urls) != 1:
await show(message, "Нужна одна ссылка.", back) await show(message, "Нужна одна ссылка.", back)
return return
@@ -292,7 +292,7 @@ async def svc_url(callback: types.CallbackQuery, state: FSMContext) -> None:
callback, callback,
f"Пришли новый адрес для <b>{esc(service.name)}</b>.\n" f"Пришли новый адрес для <b>{esc(service.name)}</b>.\n"
f"Сейчас: <code>{esc(service.url)}</code>", f"Сейчас: <code>{esc(service.url)}</code>",
back_to("Отмена", f"svc:open:{service.id}"), back_to("✖️ Отмена", f"svc:open:{service.id}"),
) )
await callback.answer() await callback.answer()
@@ -372,7 +372,7 @@ async def on_pick_text(callback: types.CallbackQuery, state: FSMContext) -> None
await show( await show(
callback, callback,
"Пришли текст, который должен быть в ответе.", "Пришли текст, который должен быть в ответе.",
back_to("Отмена", "add:cancel"), back_to("✖️ Отмена", "add:cancel"),
) )
await callback.answer() await callback.answer()
@@ -385,7 +385,7 @@ async def on_needle(message: types.Message, state: FSMContext) -> None:
await show( await show(
message, message,
"Сейчас такого текста в ответе нет. Пришли другой.", "Сейчас такого текста в ответе нет. Пришли другой.",
back_to("Отмена", "add:cancel"), back_to("✖️ Отмена", "add:cancel"),
) )
return return
await _save(message, state, match_type=MatchType.CONTAINS, needle=needle) await _save(message, state, match_type=MatchType.CONTAINS, needle=needle)
+47 -36
View File
@@ -39,6 +39,7 @@ OPS: dict[str, tuple[str, str, str | None]] = {
"sto": ("Stop", "StopStack", DANGER), "sto": ("Stop", "StopStack", DANGER),
"sta": ("Start", "StartStack", None), "sta": ("Start", "StartStack", None),
} }
OP_ICONS = {"dep": "🚀", "res": "🔁", "sto": "⏹", "sta": "▶️"}
async def _guard(callback: types.CallbackQuery) -> bool: async def _guard(callback: types.CallbackQuery) -> bool:
@@ -71,7 +72,7 @@ async def fleet_view() -> tuple[str, types.InlineKeyboardMarkup]:
komodo.servers(), komodo.stacks(), komodo.open_alerts() komodo.servers(), komodo.stacks(), komodo.open_alerts()
) )
servers.sort(key=lambda s: s.name) servers.sort(key=lambda s: s.name)
lines = [f"<b>Флот</b>: серверов {len(servers)}, стеков {len(stacks)}", ""] lines = [f"🛰 <b>Флот</b>: серверов {len(servers)}, стеков {len(stacks)}", ""]
kb = InlineKeyboardBuilder() kb = InlineKeyboardBuilder()
for server in servers: for server in servers:
mine = [s for s in stacks if s.server_id == server.id] mine = [s for s in stacks if s.server_id == server.id]
@@ -95,14 +96,14 @@ async def fleet_view() -> tuple[str, types.InlineKeyboardMarkup]:
broken = [s for s in stacks if not s.healthy] broken = [s for s in stacks if not s.healthy]
extra = 0 extra = 0
if broken: if broken:
kb.button(text=f"Не работают: {len(broken)}", callback_data="fleet:bad") kb.button(text=f"⚠️ Не работают: {len(broken)}", callback_data="fleet:bad")
extra += 1 extra += 1
if alerts: if alerts:
kb.button(text=f"Алерты: {len(alerts)}", callback_data="fleet:alerts") kb.button(text=f"🔔 Алерты: {len(alerts)}", callback_data="fleet:alerts")
extra += 1 extra += 1
kb.button(text="Журнал", callback_data="fleet:journal") kb.button(text="🕘 Журнал", callback_data="fleet:journal")
kb.button(text="Обновить", callback_data="fleet:main") kb.button(text="🔄 Обновить", callback_data="fleet:main")
kb.button(text="Меню", callback_data="menu:main") kb.button(text="🏠 Меню", callback_data="menu:main")
kb.adjust(*rows, *([extra] if extra else []), 3) kb.adjust(*rows, *([extra] if extra else []), 3)
return "\n".join(lines), kb.as_markup() return "\n".join(lines), kb.as_markup()
@@ -122,7 +123,7 @@ async def server_view(
servers, stacks = await asyncio.gather(komodo.servers(), komodo.stacks()) servers, stacks = await asyncio.gather(komodo.servers(), komodo.stacks())
server = next((s for s in servers if s.id == server_id), None) server = next((s for s in servers if s.id == server_id), None)
if server is None: if server is None:
return "Сервер не найден.", back_to("К флоту", "fleet:main") return "Сервер не найден.", back_to("⬅️ К флоту", "fleet:main")
lines = [f"{server.mark} <b>{esc(server.name)}</b>"] lines = [f"{server.mark} <b>{esc(server.name)}</b>"]
if server.state == "NotOk": if server.state == "NotOk":
@@ -161,8 +162,8 @@ async def server_view(
kb = InlineKeyboardBuilder() kb = InlineKeyboardBuilder()
rows = _stack_buttons(kb, mine[page * STACKS_PAGE : (page + 1) * STACKS_PAGE]) rows = _stack_buttons(kb, mine[page * STACKS_PAGE : (page + 1) * STACKS_PAGE])
nav = pager(kb, f"fleet:srv:{server_id}", page, total_pages) nav = pager(kb, f"fleet:srv:{server_id}", page, total_pages)
kb.button(text="Обновить", callback_data=f"fleet:srv:{server_id}:{page}") kb.button(text="🔄 Обновить", callback_data=f"fleet:srv:{server_id}:{page}")
kb.button(text="К флоту", callback_data="fleet:main") kb.button(text="⬅️ К флоту", callback_data="fleet:main")
kb.adjust(*rows, *([nav] if nav else []), 2) kb.adjust(*rows, *([nav] if nav else []), 2)
return "\n".join(lines), kb.as_markup() return "\n".join(lines), kb.as_markup()
@@ -170,7 +171,9 @@ async def server_view(
async def stack_view(stack_id: str) -> tuple[str, types.InlineKeyboardMarkup]: async def stack_view(stack_id: str) -> tuple[str, types.InlineKeyboardMarkup]:
stack = await komodo.stack(stack_id) stack = await komodo.stack(stack_id)
if stack is None: if stack is None:
return "Стек не найден, возможно его удалили.", back_to("К флоту", "fleet:main") return "Стек не найден, возможно его удалили.", back_to(
"⬅️ К флоту", "fleet:main"
)
lines = [ lines = [
f"{stack.mark} <b>{esc(stack.name)}</b>", f"{stack.mark} <b>{esc(stack.name)}</b>",
@@ -202,12 +205,16 @@ async def stack_view(stack_id: str) -> tuple[str, types.InlineKeyboardMarkup]:
shown = ("dep", "res", "sto") if stack.state != "down" else ("dep", "sta") shown = ("dep", "res", "sto") if stack.state != "down" else ("dep", "sta")
for key in shown: for key in shown:
text, _, style = OPS[key] text, _, style = OPS[key]
kb.button(text=text, callback_data=f"fleet:ask:{key}:{stack.id}", style=style)
kb.button(text="Логи", callback_data=f"fleet:logs:{stack.id}")
kb.button(text="История", callback_data=f"fleet:hist:{stack.id}")
kb.button(text="Обновить", callback_data=f"fleet:stack:{stack.id}")
kb.button( kb.button(
text="К серверу" if stack.server_id else "К флоту", text=f"{OP_ICONS[key]} {text}",
callback_data=f"fleet:ask:{key}:{stack.id}",
style=style,
)
kb.button(text="📜 Логи", callback_data=f"fleet:logs:{stack.id}")
kb.button(text="🕘 История", callback_data=f"fleet:hist:{stack.id}")
kb.button(text="🔄 Обновить", callback_data=f"fleet:stack:{stack.id}")
kb.button(
text="⬅️ К серверу" if stack.server_id else "⬅️ К флоту",
callback_data=f"fleet:srv:{stack.server_id}:0" callback_data=f"fleet:srv:{stack.server_id}:0"
if stack.server_id if stack.server_id
else "fleet:main", else "fleet:main",
@@ -296,14 +303,14 @@ async def _bad_view() -> tuple[str, types.InlineKeyboardMarkup]:
(s for s in await komodo.stacks() if not s.healthy), (s for s in await komodo.stacks() if not s.healthy),
key=lambda s: (s.server_name, s.name), key=lambda s: (s.server_name, s.name),
) )
lines = ["<b>Не работают</b>", ""] lines = ["⚠️ <b>Не работают</b>", ""]
lines.extend( lines.extend(
f"{s.mark} {esc(s.name)} · {esc(s.server_name)} · {esc(s.state)}" f"{s.mark} {esc(s.name)} · {esc(s.server_name)} · {esc(s.state)}"
for s in broken for s in broken
) )
kb = InlineKeyboardBuilder() kb = InlineKeyboardBuilder()
rows = _stack_buttons(kb, broken[:40]) rows = _stack_buttons(kb, broken[:40])
kb.button(text="К флоту", callback_data="fleet:main") kb.button(text="⬅️ К флоту", callback_data="fleet:main")
kb.adjust(*rows, 1) kb.adjust(*rows, 1)
return "\n".join(lines), kb.as_markup() return "\n".join(lines), kb.as_markup()
@@ -318,13 +325,13 @@ async def fleet_bad(callback: types.CallbackQuery) -> None:
async def _alerts_view() -> tuple[str, types.InlineKeyboardMarkup]: async def _alerts_view() -> tuple[str, types.InlineKeyboardMarkup]:
tz = db.settings.get("tz") tz = db.settings.get("tz")
alerts = await komodo.open_alerts() alerts = await komodo.open_alerts()
lines = ["<b>Открытые алерты</b>"] lines = ["🔔 <b>Открытые алерты</b>"]
if not alerts: if not alerts:
lines.append("Сейчас ни одного.") lines.append("Сейчас ни одного.")
for alert in alerts[:10]: for alert in alerts[:10]:
since = stamp(from_ms(alert.get("ts") or 0), tz) since = stamp(from_ms(alert.get("ts") or 0), tz)
lines += ["", format_alert(alert), f"с {since}"] lines += ["", format_alert(alert), f"с {since}"]
return "\n".join(lines), back_to("К флоту", "fleet:main") return "\n".join(lines), back_to("⬅️ К флоту", "fleet:main")
@router.callback_query(F.data == "fleet:alerts") @router.callback_query(F.data == "fleet:alerts")
@@ -350,9 +357,11 @@ async def fleet_ask(callback: types.CallbackQuery) -> None:
return return
kb = InlineKeyboardBuilder() kb = InlineKeyboardBuilder()
kb.button( kb.button(
text=text, callback_data=f"fleet:do:{key}:{stack_id}", style=style or PRIMARY text=f"{OP_ICONS[key]} {text}",
callback_data=f"fleet:do:{key}:{stack_id}",
style=style or PRIMARY,
) )
kb.button(text="Отмена", callback_data=f"fleet:stack:{stack_id}") kb.button(text="✖️ Отмена", callback_data=f"fleet:stack:{stack_id}")
kb.adjust(2) kb.adjust(2)
await show( await show(
callback, callback,
@@ -384,8 +393,8 @@ async def fleet_do(callback: types.CallbackQuery) -> None:
f"{text} {name}", f"{text} {name}",
starter, starter,
back=[ back=[
("К стеку", f"fleet:stack:{stack_id}"), ("⬅️ К стеку", f"fleet:stack:{stack_id}"),
("Логи", f"fleet:logs:{stack_id}"), ("📜 Логи", f"fleet:logs:{stack_id}"),
], ],
verbose=False, verbose=False,
) )
@@ -402,7 +411,7 @@ async def _log_view(
) -> tuple[str, types.InlineKeyboardMarkup]: ) -> tuple[str, types.InlineKeyboardMarkup]:
stack = await komodo.stack(stack_id) stack = await komodo.stack(stack_id)
if stack is None: if stack is None:
return "Стек не найден.", back_to("К флоту", "fleet:main") return "Стек не найден.", back_to("⬅️ К флоту", "fleet:main")
services = _picked(stack, index) services = _picked(stack, index)
log = await komodo.stack_log(stack_id, services, LOG_TAIL) log = await komodo.stack_log(stack_id, services, LOG_TAIL)
if services or len(stack.services) == 1: if services or len(stack.services) == 1:
@@ -410,15 +419,15 @@ async def _log_view(
scope = f" · {esc(services[0])}" if services else "" scope = f" · {esc(services[0])}" if services else ""
body = pre(log, LOG_CHARS) if log else "<i>лог пуст</i>" body = pre(log, LOG_CHARS) if log else "<i>лог пуст</i>"
kb = InlineKeyboardBuilder() kb = InlineKeyboardBuilder()
kb.button(text="Обновить", callback_data=f"fleet:log:{stack_id}:{index}") kb.button(text="🔄 Обновить", callback_data=f"fleet:log:{stack_id}:{index}")
kb.button(text="Файлом", callback_data=f"fleet:logf:{stack_id}:{index}") kb.button(text="📎 Файлом", callback_data=f"fleet:logf:{stack_id}:{index}")
rows = [2] rows = [2]
if len(stack.services) > 1: if len(stack.services) > 1:
kb.button(text="Другой сервис", callback_data=f"fleet:logs:{stack_id}") kb.button(text="🔀 Другой сервис", callback_data=f"fleet:logs:{stack_id}")
kb.button(text="К стеку", callback_data=f"fleet:stack:{stack_id}") kb.button(text="⬅️ К стеку", callback_data=f"fleet:stack:{stack_id}")
rows.append(2) rows.append(2)
else: else:
kb.button(text="К стеку", callback_data=f"fleet:stack:{stack_id}") kb.button(text="⬅️ К стеку", callback_data=f"fleet:stack:{stack_id}")
rows.append(1) rows.append(1)
kb.adjust(*rows) kb.adjust(*rows)
return f"<b>{esc(stack.name)}</b>{scope}\n{body}", kb.as_markup() return f"<b>{esc(stack.name)}</b>{scope}\n{body}", kb.as_markup()
@@ -427,7 +436,7 @@ async def _log_view(
async def _log_picker(stack_id: str) -> tuple[str, types.InlineKeyboardMarkup]: async def _log_picker(stack_id: str) -> tuple[str, types.InlineKeyboardMarkup]:
stack = await komodo.stack(stack_id) stack = await komodo.stack(stack_id)
if stack is None: if stack is None:
return "Стек не найден.", back_to("К флоту", "fleet:main") return "Стек не найден.", back_to("⬅️ К флоту", "fleet:main")
if len(stack.services) <= 1: if len(stack.services) <= 1:
return await _log_view(stack_id, ALL_SERVICES) return await _log_view(stack_id, ALL_SERVICES)
kb = InlineKeyboardBuilder() kb = InlineKeyboardBuilder()
@@ -435,8 +444,10 @@ async def _log_picker(stack_id: str) -> tuple[str, types.InlineKeyboardMarkup]:
kb.button( kb.button(
text=label(service, 28), callback_data=f"fleet:log:{stack_id}:{index}" text=label(service, 28), callback_data=f"fleet:log:{stack_id}:{index}"
) )
kb.button(text="Все сервисы", callback_data=f"fleet:log:{stack_id}:{ALL_SERVICES}") kb.button(
kb.button(text="К стеку", callback_data=f"fleet:stack:{stack_id}") text="🗂 Все сервисы", callback_data=f"fleet:log:{stack_id}:{ALL_SERVICES}"
)
kb.button(text="⬅️ К стеку", callback_data=f"fleet:stack:{stack_id}")
count = len(stack.services) count = len(stack.services)
kb.adjust(*[2] * (count // 2), *[1] * (count % 2), 2) kb.adjust(*[2] * (count // 2), *[1] * (count % 2), 2)
return f"<b>{esc(stack.name)}</b>: чьи логи?", kb.as_markup() return f"<b>{esc(stack.name)}</b>: чьи логи?", kb.as_markup()
@@ -559,7 +570,7 @@ async def _history_view(stack_id: str) -> tuple[str, types.InlineKeyboardMarkup]
title = esc(stack.name) if stack else "Стек" title = esc(stack.name) if stack else "Стек"
kb = InlineKeyboardBuilder() kb = InlineKeyboardBuilder()
count = _failed_buttons(kb, failed) count = _failed_buttons(kb, failed)
kb.button(text="К стеку", callback_data=f"fleet:stack:{stack_id}") kb.button(text="⬅️ К стеку", callback_data=f"fleet:stack:{stack_id}")
kb.adjust(*[1] * count, 1) kb.adjust(*[1] * count, 1)
body = "\n".join(lines) if lines else "Операций ещё не было." body = "\n".join(lines) if lines else "Операций ещё не было."
return f"<b>{title}</b>: операции\n\n{body}", kb.as_markup() return f"<b>{title}</b>: операции\n\n{body}", kb.as_markup()
@@ -578,11 +589,11 @@ async def _journal_view() -> tuple[str, types.InlineKeyboardMarkup]:
lines, failed = await _update_lines(updates, JOURNAL, with_target=True) lines, failed = await _update_lines(updates, JOURNAL, with_target=True)
kb = InlineKeyboardBuilder() kb = InlineKeyboardBuilder()
count = _failed_buttons(kb, failed) count = _failed_buttons(kb, failed)
kb.button(text="Обновить", callback_data="fleet:journal") kb.button(text="🔄 Обновить", callback_data="fleet:journal")
kb.button(text="К флоту", callback_data="fleet:main") kb.button(text="⬅️ К флоту", callback_data="fleet:main")
kb.adjust(*[1] * count, 2) kb.adjust(*[1] * count, 2)
body = "\n".join(lines) if lines else "Операций ещё не было." body = "\n".join(lines) if lines else "Операций ещё не было."
return f"<b>Журнал</b>\n\n{body}", kb.as_markup() return f"🕘 <b>Журнал</b>\n\n{body}", kb.as_markup()
@router.callback_query(F.data == "fleet:journal") @router.callback_query(F.data == "fleet:journal")
+5 -5
View File
@@ -78,13 +78,13 @@ async def _services_block() -> list[str]:
works = plural(working, "работает", "работают", "работают") works = plural(working, "работает", "работают", "работают")
if not down and not paused: if not down and not paused:
every = "все " if working > 1 else "" every = "все " if working > 1 else ""
return [f"<b>Сервисы</b>: {every}{working} {works}"] return [f"📋 <b>Сервисы</b>: {every}{working} {works}"]
parts = [f"{working} {works}"] parts = [f"{working} {works}"]
if down: if down:
parts.append(f"{len(down)} {plural(len(down), 'лежит', 'лежат', 'лежат')}") parts.append(f"{len(down)} {plural(len(down), 'лежит', 'лежат', 'лежат')}")
if paused: if paused:
parts.append(f"{paused} на паузе") parts.append(f"{paused} на паузе")
lines = [f"<b>Сервисы</b>: {', '.join(parts)}"] lines = [f"📋 <b>Сервисы</b>: {', '.join(parts)}"]
lines.extend([await _down_line(s) for s in down[:SHOWN_PROBLEMS]]) lines.extend([await _down_line(s) for s in down[:SHOWN_PROBLEMS]])
return lines return lines
@@ -95,11 +95,11 @@ async def _fleet_block() -> list[str]:
komodo.servers(), komodo.stacks(), komodo.open_alerts() komodo.servers(), komodo.stacks(), komodo.open_alerts()
) )
except KomodoError as exc: except KomodoError as exc:
return [f"<b>Флот</b>: {esc(exc)}"] return [f"🛰 <b>Флот</b>: {esc(exc)}"]
reachable = {s.id for s in servers if s.state == "Ok"} reachable = {s.id for s in servers if s.state == "Ok"}
good = sum(s.healthy for s in stacks) good = sum(s.healthy for s in stacks)
summary = "все работают" if good == len(stacks) else f"работают {good}" summary = "все работают" if good == len(stacks) else f"работают {good}"
lines = [f"<b>Флот</b>: серверов {len(servers)}, стеков {len(stacks)}, {summary}"] lines = [f"🛰 <b>Флот</b>: серверов {len(servers)}, стеков {len(stacks)}, {summary}"]
for server in servers: for server in servers:
if server.state == "NotOk": if server.state == "NotOk":
lines.append(f"🔴 {esc(server.name)} не отвечает") lines.append(f"🔴 {esc(server.name)} не отвечает")
@@ -123,7 +123,7 @@ async def dashboard() -> tuple[str, types.InlineKeyboardMarkup]:
try: try:
fleet = await asyncio.wait_for(_fleet_block(), FLEET_TIMEOUT) fleet = await asyncio.wait_for(_fleet_block(), FLEET_TIMEOUT)
except TimeoutError: except TimeoutError:
fleet = ["<b>Флот</b>: Komodo не отвечает"] fleet = ["🛰 <b>Флот</b>: Komodo не отвечает"]
lines += ["", *fleet] lines += ["", *fleet]
pins = await db.pins.all() if actions else [] pins = await db.pins.all() if actions else []
return "\n".join(lines), main_menu(pins, actions=bool(actions)) return "\n".join(lines), main_menu(pins, actions=bool(actions))
+1 -1
View File
@@ -32,7 +32,7 @@ async def run_status(callback: types.CallbackQuery) -> None:
await callback.answer() await callback.answer()
if not update.done: if not update.done:
kb = InlineKeyboardBuilder() kb = InlineKeyboardBuilder()
kb.button(text="Проверить", callback_data=f"run:st:{update_id}") kb.button(text="🔍 Проверить", callback_data=f"run:st:{update_id}")
await show( await show(
callback, callback,
f"⏳ <b>{esc(title)}</b> идёт уже {clock(update.seconds)}", f"⏳ <b>{esc(title)}</b> идёт уже {clock(update.seconds)}",
+4 -4
View File
@@ -54,7 +54,7 @@ async def list_view(page: int = 0) -> tuple[str, types.InlineKeyboardMarkup]:
parts = [f"🟢 {up}", f"🔴 {len(watched) - up}"] parts = [f"🟢 {up}", f"🔴 {len(watched) - up}"]
if paused: if paused:
parts.append(f"⏸ {paused}") parts.append(f"⏸ {paused}")
return f"<b>Сервисы</b> {' '.join(parts)}", services_list(services, page) return f"📋 <b>Сервисы</b> {' '.join(parts)}", services_list(services, page)
@router.message(Command("list")) @router.message(Command("list"))
@@ -311,7 +311,7 @@ async def svc_incidents(callback: types.CallbackQuery) -> None:
total = downtime(incidents, parse(service.created_at), now()) total = downtime(incidents, parse(service.created_at), now())
lines += ["", f"Всего {len(incidents)}, даунтайм {human_duration(total)}"] lines += ["", f"Всего {len(incidents)}, даунтайм {human_duration(total)}"]
await show( await show(
callback, "\n".join(lines), back_to("К сервису", f"svc:open:{service.id}") callback, "\n".join(lines), back_to("⬅️ К сервису", f"svc:open:{service.id}")
) )
await callback.answer() await callback.answer()
@@ -326,7 +326,7 @@ async def svc_rename(callback: types.CallbackQuery, state: FSMContext) -> None:
await show( await show(
callback, callback,
f"Пришли новое имя для <b>{esc(service.name)}</b>.", f"Пришли новое имя для <b>{esc(service.name)}</b>.",
back_to("Отмена", f"svc:open:{service.id}"), back_to("✖️ Отмена", f"svc:open:{service.id}"),
) )
await callback.answer() await callback.answer()
@@ -378,7 +378,7 @@ async def stats_view() -> tuple[str, types.InlineKeyboardMarkup]:
rows.append((uptime(service, incidents, month) or 0.0, service, incidents)) rows.append((uptime(service, incidents, month) or 0.0, service, incidents))
rows.sort(key=lambda row: row[0]) rows.sort(key=lambda row: row[0])
lines = ["<b>Статистика</b>", ""] lines = ["📊 <b>Статистика</b>", ""]
for _, service, incidents in rows: for _, service, incidents in rows:
speed = ( speed = (
f" · {service.last_latency_ms} мс" f" · {service.last_latency_ms} мс"
+18 -18
View File
@@ -21,9 +21,9 @@ router.message.filter(Admin())
router.callback_query.filter(Admin()) router.callback_query.filter(Admin())
TOGGLES = { TOGGLES = {
"actions": ("Действия Komodo", "включены", "выключены"), "actions": ("🎛 Действия Komodo", "включены", "выключены"),
"commands": ("Действия как команды", "включены", "выключены"), "commands": ("⌨️ Действия как команды", "включены", "выключены"),
"confirm": ("Подтверждение запуска", "включено", "выключено"), "confirm": ("🛡 Подтверждение запуска", "включено", "выключено"),
} }
DIGEST_HOURS = (7, 8, 9, 10, 12, 18, 21) DIGEST_HOURS = (7, 8, 9, 10, 12, 18, 21)
@@ -43,7 +43,7 @@ async def _komodo_line() -> str:
async def settings_view() -> tuple[str, types.InlineKeyboardMarkup]: async def settings_view() -> tuple[str, types.InlineKeyboardMarkup]:
tz = db.settings.get("tz") tz = db.settings.get("tz")
lines = ["<b>Настройки</b>", await _komodo_line()] lines = ["⚙️ <b>Настройки</b>", await _komodo_line()]
kb = InlineKeyboardBuilder() kb = InlineKeyboardBuilder()
if env.komodo.enabled: if env.komodo.enabled:
for key, (title, on, off) in TOGGLES.items(): for key, (title, on, off) in TOGGLES.items():
@@ -59,14 +59,14 @@ async def settings_view() -> tuple[str, types.InlineKeyboardMarkup]:
) )
if env.web.enabled: if env.web.enabled:
muted = len(db.settings.get("muted")) muted = len(db.settings.get("muted"))
kb.button(text=f"Алерты Komodo, скрыто: {muted}", callback_data="set:alerts") kb.button(text=f"🔔 Алерты Komodo, скрыто: {muted}", callback_data="set:alerts")
kb.button(text=f"Часовой пояс: {tz}", callback_data="set:tz") kb.button(text=f"🕒 Часовой пояс: {tz}", callback_data="set:tz")
hour = db.settings.get("digest") hour = db.settings.get("digest")
digest = "выключена" if hour is None else f"в {hour:02d}:00" digest = "выключена" if hour is None else f"в {hour:02d}:00"
kb.button(text=f"Сводка за сутки: {digest}", callback_data="set:digest") kb.button(text=f"🗞 Сводка за сутки: {digest}", callback_data="set:digest")
if env.komodo.enabled and db.settings.get("actions"): if env.komodo.enabled and db.settings.get("actions"):
kb.button(text="Закреплённые", callback_data="set:pins") kb.button(text="📌 Закреплённые", callback_data="set:pins")
kb.button(text="Меню", callback_data="menu:main") kb.button(text="🏠 Меню", callback_data="menu:main")
kb.adjust(1) kb.adjust(1)
return "\n".join(lines), kb.as_markup() return "\n".join(lines), kb.as_markup()
@@ -106,9 +106,9 @@ def _alerts_view() -> tuple[str, types.InlineKeyboardMarkup]:
for kind in kinds: for kind in kinds:
mark = "🔕" if kind in muted else "🔔" mark = "🔕" if kind in muted else "🔔"
kb.button(text=f"{mark} {kind}", callback_data=f"set:mute:{kind}") kb.button(text=f"{mark} {kind}", callback_data=f"set:mute:{kind}")
kb.button(text="К настройкам", callback_data="set:main") kb.button(text="⬅️ К настройкам", callback_data="set:main")
kb.adjust(1) kb.adjust(1)
return "<b>Алерты Komodo</b>", kb.as_markup() return "🔔 <b>Алерты Komodo</b>", kb.as_markup()
@router.callback_query(F.data == "set:alerts") @router.callback_query(F.data == "set:alerts")
@@ -158,7 +158,7 @@ async def alert_mute(callback: types.CallbackQuery) -> None:
kind = tail_arg(callback.data) kind = tail_arg(callback.data)
if kind not in db.settings.get("muted"): if kind not in db.settings.get("muted"):
await _flip_mute(kind) await _flip_mute(kind)
await _swap_alert_button(callback, f"{kind} скрыт, вернуть", f"al:show:{kind}") await _swap_alert_button(callback, f"🔔 {kind} скрыт, вернуть", f"al:show:{kind}")
@router.callback_query(F.data.startswith("al:show:")) @router.callback_query(F.data.startswith("al:show:"))
@@ -166,7 +166,7 @@ async def alert_unmute(callback: types.CallbackQuery) -> None:
kind = tail_arg(callback.data) kind = tail_arg(callback.data)
if kind in db.settings.get("muted"): if kind in db.settings.get("muted"):
await _flip_mute(kind) await _flip_mute(kind)
await _swap_alert_button(callback, "Не присылать такие", f"al:mute:{kind}") await _swap_alert_button(callback, "🔕 Не присылать такие", f"al:mute:{kind}")
@router.callback_query(F.data == "set:tz") @router.callback_query(F.data == "set:tz")
@@ -178,7 +178,7 @@ async def settings_tz(callback: types.CallbackQuery, state: FSMContext) -> None:
callback, callback,
"Пришли часовой пояс, например <code>Europe/Warsaw</code>.\n" "Пришли часовой пояс, например <code>Europe/Warsaw</code>.\n"
f"Сейчас: {esc(tz)}, на часах {local}", f"Сейчас: {esc(tz)}, на часах {local}",
back_to("Отмена", "set:main"), back_to("✖️ Отмена", "set:main"),
) )
await callback.answer() await callback.answer()
@@ -193,7 +193,7 @@ async def on_tz(message: types.Message, state: FSMContext) -> None:
message, message,
"Такого пояса нет. Нужно имя из базы tz, например " "Такого пояса нет. Нужно имя из базы tz, например "
"<code>Europe/Warsaw</code> или <code>UTC</code>.", "<code>Europe/Warsaw</code> или <code>UTC</code>.",
back_to("Отмена", "set:main"), back_to("✖️ Отмена", "set:main"),
) )
return return
await state.clear() await state.clear()
@@ -207,12 +207,12 @@ async def settings_digest(callback: types.CallbackQuery) -> None:
kb = InlineKeyboardBuilder() kb = InlineKeyboardBuilder()
for hour in DIGEST_HOURS: for hour in DIGEST_HOURS:
kb.button(text=f"{hour:02d}:00", callback_data=f"set:dg:{hour}") kb.button(text=f"{hour:02d}:00", callback_data=f"set:dg:{hour}")
kb.button(text="Выключить", callback_data="set:dg:off") kb.button(text="🚫 Выключить", callback_data="set:dg:off")
kb.button(text="К настройкам", callback_data="set:main") kb.button(text="⬅️ К настройкам", callback_data="set:main")
kb.adjust(4, 3, 2) kb.adjust(4, 3, 2)
await show( await show(
callback, callback,
"<b>Сводка за сутки</b>\n" "🗞 <b>Сводка за сутки</b>\n"
"Раз в день: аптайм и падения сервисов, операции и алерты флота.\n" "Раз в день: аптайм и падения сервисов, операции и алерты флота.\n"
f"Время по поясу {esc(db.settings.get('tz'))}.", f"Время по поясу {esc(db.settings.get('tz'))}.",
kb.as_markup(), kb.as_markup(),
+37 -29
View File
@@ -1,38 +1,44 @@
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
from aiogram.utils.keyboard import InlineKeyboardBuilder from aiogram.utils.keyboard import InlineKeyboardBuilder
from healthbot.ui import DANGER, PRIMARY, label, pager, pages from healthbot.ui import DANGER, PRIMARY, label, pager, pages, with_icon
from utils.db.models import Pin, Service from utils.db.models import Pin, PinMode, Service
from utils.env import env from utils.env import env
SERVICES_PAGE = 12 SERVICES_PAGE = 12
def pin_text(pin: Pin) -> str:
return with_icon(pin.label, "⚡" if pin.mode is PinMode.INSTANT else "📝")
def main_menu(pins: list[Pin], *, actions: bool) -> InlineKeyboardMarkup: def main_menu(pins: list[Pin], *, actions: bool) -> InlineKeyboardMarkup:
kb = InlineKeyboardBuilder() kb = InlineKeyboardBuilder()
rows: list[int] = [] rows: list[int] = []
for pin in pins: for pin in pins:
kb.button( kb.button(
text=label(pin.label, 30), callback_data=f"pin:run:{pin.id}", style=PRIMARY text=label(pin_text(pin), 30),
callback_data=f"pin:run:{pin.id}",
style=PRIMARY,
) )
rows += [2] * (len(pins) // 2) + [1] * (len(pins) % 2) rows += [2] * (len(pins) // 2) + [1] * (len(pins) % 2)
kb.button(text="Сервисы", callback_data="menu:list:0") kb.button(text="📋 Сервисы", callback_data="menu:list:0")
if env.komodo.enabled: if env.komodo.enabled:
kb.button(text="Флот", callback_data="fleet:main") kb.button(text="🛰 Флот", callback_data="fleet:main")
if actions: if actions:
kb.button(text="Действия", callback_data="act:list:a:0") kb.button(text="🎛 Действия", callback_data="act:list:a:0")
kb.button(text="Статистика", callback_data="menu:stats") kb.button(text="📊 Статистика", callback_data="menu:stats")
rows += [2, 2] rows += [2, 2]
else: else:
kb.button(text="Статистика", callback_data="menu:stats") kb.button(text="📊 Статистика", callback_data="menu:stats")
rows += [2, 1] rows += [2, 1]
else: else:
kb.button(text="Статистика", callback_data="menu:stats") kb.button(text="📊 Статистика", callback_data="menu:stats")
rows += [2] rows += [2]
kb.button(text="Добавить сервис", callback_data="menu:add") kb.button(text="➕ Добавить сервис", callback_data="menu:add")
kb.button(text="Настройки", callback_data="set:main") kb.button(text="⚙️ Настройки", callback_data="set:main")
kb.button(text="Обновить", callback_data="menu:main") kb.button(text="🔄 Обновить", callback_data="menu:main")
kb.adjust(*rows, 2, 1) kb.adjust(*rows, 2, 1)
return kb.as_markup() return kb.as_markup()
@@ -60,33 +66,35 @@ def services_list(services: list[Service], page: int) -> InlineKeyboardMarkup:
callback_data=f"svc:open:{svc.id}", callback_data=f"svc:open:{svc.id}",
) )
nav = pager(kb, "menu:list", page, total) nav = pager(kb, "menu:list", page, total)
kb.button(text="Добавить", callback_data="menu:add") kb.button(text="➕ Добавить", callback_data="menu:add")
kb.button(text="Меню", callback_data="menu:main") kb.button(text="🏠 Меню", callback_data="menu:main")
kb.adjust(*[1] * len(chunk), *([nav] if nav else []), 2) kb.adjust(*[1] * len(chunk), *([nav] if nav else []), 2)
return kb.as_markup() return kb.as_markup()
def service_card(service: Service) -> InlineKeyboardMarkup: def service_card(service: Service) -> InlineKeyboardMarkup:
kb = InlineKeyboardBuilder() kb = InlineKeyboardBuilder()
kb.button(text="Проверить", callback_data=f"svc:check:{service.id}", style=PRIMARY) kb.button(
kb.button(text="Инциденты", callback_data=f"svc:inc:{service.id}") text="🔍 Проверить", callback_data=f"svc:check:{service.id}", style=PRIMARY
)
kb.button(text="🚨 Инциденты", callback_data=f"svc:inc:{service.id}")
if service.is_paused: if service.is_paused:
kb.button(text="Возобновить", callback_data=f"svc:resume:{service.id}") kb.button(text="▶️ Возобновить", callback_data=f"svc:resume:{service.id}")
else: else:
kb.button(text="Пауза", callback_data=f"svc:pause:{service.id}") kb.button(text="⏸ Пауза", callback_data=f"svc:pause:{service.id}")
kb.button(text="Правило", callback_data=f"svc:rule:{service.id}") kb.button(text="🎯 Правило", callback_data=f"svc:rule:{service.id}")
kb.button(text="Имя", callback_data=f"svc:rename:{service.id}") kb.button(text="✏️ Имя", callback_data=f"svc:rename:{service.id}")
kb.button(text="Адрес", callback_data=f"svc:url:{service.id}") kb.button(text="🔗 Адрес", callback_data=f"svc:url:{service.id}")
kb.button(text="Удалить", callback_data=f"svc:del:{service.id}") kb.button(text="🗑 Удалить", callback_data=f"svc:del:{service.id}")
kb.button(text="К списку", callback_data="menu:list:0") kb.button(text="⬅️ К списку", callback_data="menu:list:0")
kb.adjust(2) kb.adjust(2)
return kb.as_markup() return kb.as_markup()
def confirm_delete(service_id: int) -> InlineKeyboardMarkup: def confirm_delete(service_id: int) -> InlineKeyboardMarkup:
kb = InlineKeyboardBuilder() kb = InlineKeyboardBuilder()
kb.button(text="Удалить", callback_data=f"svc:delyes:{service_id}", style=DANGER) kb.button(text="🗑 Удалить", callback_data=f"svc:delyes:{service_id}", style=DANGER)
kb.button(text="Отмена", callback_data=f"svc:open:{service_id}") kb.button(text="✖️ Отмена", callback_data=f"svc:open:{service_id}")
kb.adjust(2) kb.adjust(2)
return kb.as_markup() return kb.as_markup()
@@ -94,9 +102,9 @@ def confirm_delete(service_id: int) -> InlineKeyboardMarkup:
def after_check(service_id: int, *, offer_baseline: bool) -> InlineKeyboardMarkup: def after_check(service_id: int, *, offer_baseline: bool) -> InlineKeyboardMarkup:
kb = InlineKeyboardBuilder() kb = InlineKeyboardBuilder()
if offer_baseline: if offer_baseline:
kb.button(text="Принять как эталон", callback_data=f"svc:base:{service_id}") kb.button(text="✅ Принять как эталон", callback_data=f"svc:base:{service_id}")
kb.button(text="Ещё раз", callback_data=f"svc:check:{service_id}") kb.button(text="🔄 Ещё раз", callback_data=f"svc:check:{service_id}")
kb.button(text="К сервису", callback_data=f"svc:open:{service_id}") kb.button(text="⬅️ К сервису", callback_data=f"svc:open:{service_id}")
kb.adjust(*([1] if offer_baseline else []), 2) kb.adjust(*([1] if offer_baseline else []), 2)
return kb.as_markup() return kb.as_markup()
@@ -108,4 +116,4 @@ def back_to(text: str, data: str) -> InlineKeyboardMarkup:
def back_to_menu() -> InlineKeyboardMarkup: def back_to_menu() -> InlineKeyboardMarkup:
return back_to("Меню", "menu:main") return back_to("🏠 Меню", "menu:main")
+4 -4
View File
@@ -30,17 +30,17 @@ def _head(service: Service, icon: str, tail: str) -> str:
def _down_kb(service: Service, incident_id: int | None) -> InlineKeyboardMarkup: def _down_kb(service: Service, incident_id: int | None) -> InlineKeyboardMarkup:
kb = InlineKeyboardBuilder() kb = InlineKeyboardBuilder()
kb.button(text="Проверить", callback_data=f"go:check:{service.id}") kb.button(text="🔍 Проверить", callback_data=f"go:check:{service.id}")
kb.button(text="Открыть", callback_data=f"go:svc:{service.id}") kb.button(text="📂 Открыть", callback_data=f"go:svc:{service.id}")
if incident_id is not None: if incident_id is not None:
kb.button(text="Не напоминать", callback_data=f"svc:mute:{incident_id}") kb.button(text="🔕 Не напоминать", callback_data=f"svc:mute:{incident_id}")
kb.adjust(2, 1) kb.adjust(2, 1)
return kb.as_markup() return kb.as_markup()
def _open_kb(service: Service) -> InlineKeyboardMarkup: def _open_kb(service: Service) -> InlineKeyboardMarkup:
kb = InlineKeyboardBuilder() kb = InlineKeyboardBuilder()
kb.button(text="Открыть", callback_data=f"go:svc:{service.id}") kb.button(text="📂 Открыть", callback_data=f"go:svc:{service.id}")
return kb.as_markup() return kb.as_markup()
+2 -2
View File
@@ -62,7 +62,7 @@ def result_kb(update: Update | None, back: Back) -> InlineKeyboardMarkup:
kb.button(text=text, callback_data=data) kb.button(text=text, callback_data=data)
rows = [len(back)] if back else [] rows = [len(back)] if back else []
if update is not None and len(output_of(update, only_failed=False)) > OUTPUT_LIMIT: if update is not None and len(output_of(update, only_failed=False)) > OUTPUT_LIMIT:
kb.button(text="Весь вывод файлом", callback_data=f"run:file:{update.id}") kb.button(text="📎 Весь вывод файлом", callback_data=f"run:file:{update.id}")
rows.append(1) rows.append(1)
kb.adjust(*rows or [1]) kb.adjust(*rows or [1])
return kb.as_markup() return kb.as_markup()
@@ -119,7 +119,7 @@ async def _track( # noqa: PLR0913
return return
check = InlineKeyboardBuilder() check = InlineKeyboardBuilder()
check.button(text="Проверить", callback_data=f"run:st:{started.id}") check.button(text="🔍 Проверить", callback_data=f"run:st:{started.id}")
try: try:
update = await komodo.wait(started.id, RUN_TIMEOUT) update = await komodo.wait(started.id, RUN_TIMEOUT)
except KomodoError as exc: except KomodoError as exc:
+6 -2
View File
@@ -88,6 +88,10 @@ def plural(count: int, one: str, few: str, many: str) -> str:
return many return many
def with_icon(text: str, icon: str) -> str:
return f"{icon} {text}" if text[:1].isalnum() else text
def pages(total: int, size: int) -> int: def pages(total: int, size: int) -> int:
return max(1, (total + size - 1) // size) return max(1, (total + size - 1) // size)
@@ -97,10 +101,10 @@ def pager(kb: InlineKeyboardBuilder, prefix: str, page: int, total_pages: int) -
return 0 return 0
count = 0 count = 0
if page > 0: if page > 0:
kb.button(text="‹ Назад", callback_data=f"{prefix}:{page - 1}") kb.button(text="◀️ Назад", callback_data=f"{prefix}:{page - 1}")
count += 1 count += 1
if page < total_pages - 1: if page < total_pages - 1:
kb.button(text="Дальше ›", callback_data=f"{prefix}:{page + 1}") kb.button(text="Дальше ▶️", callback_data=f"{prefix}:{page + 1}")
count += 1 count += 1
return count return count