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()
if env.komodo.enabled and target_id:
if target_type == "Stack":
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:stack:{target_id}")
kb.button(text="📜 Логи", callback_data=f"go:log:{target_id}")
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":
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":
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}"
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))
if not buttons:
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
falls = sum(row[2] for row in rows)
if not falls:
return [f"<b>Сервисы</b>: аптайм {pct(uptime)}, падений не было"]
return [f"📋 <b>Сервисы</b>: аптайм {pct(uptime)}, падений не было"]
word = plural(falls, "падение", "падения", "падений")
lines = [
f"<b>Сервисы</b>: аптайм {pct(uptime)}, {falls} {word}, "
f"📋 <b>Сервисы</b>: аптайм {pct(uptime)}, {falls} {word}, "
f"даунтайм {human_duration(lost)}"
]
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(),
)
except KomodoError as exc:
return [f"<b>Флот</b>: {esc(exc)}"]
return [f"🛰 <b>Флот</b>: {esc(exc)}"]
tz = db.settings.get("tz")
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]] = {}
for update in failed:
grouped.setdefault((update.operation, update.target_id), []).append(update)
@@ -93,7 +93,7 @@ async def digest_text() -> str:
blocks = [await _services_block()]
if env.komodo.enabled:
blocks.append(await _fleet_block())
lines = ["<b>Сводка за сутки</b>"]
lines = ["🗞 <b>Сводка за сутки</b>"]
for block in blocks:
if block:
lines += ["", *block]
+39 -34
View File
@@ -19,7 +19,7 @@ from healthbot.actionargs import (
)
from healthbot.filters import Admin
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.runs import track
from healthbot.ui import (
@@ -47,7 +47,8 @@ WIDE_FORM = 6
MAX_LABEL = 40
KIND_TITLE = {"a": "Действия", "p": "Процедуры"}
KIND_ONE = {"a": "Действие", "p": "Процедура"}
KIND_BACK = {"a": "К действию", "p": "К процедуре"}
KIND_BACK = {"a": "⬅️ К действию", "p": "⬅️ К процедуре"}
KIND_ICON = {"a": "🎛", "p": "🧩"}
USAGE_FIELDS = 3
RUN_STATE = {"Ok": "успешно", "Failed": "с ошибкой", "Running": "идёт сейчас"}
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)
nav = pager(kb, f"act:list:{kind}", page, total)
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="menu:main")
kb.button(text="📌 Закреплённые", callback_data="set:pins")
kb.button(text="🏠 Меню", callback_data="menu:main")
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:
text += "\nКлюч бота не видит ни одного. Нужен доступ Execute в Komodo."
return text, kb.as_markup()
@@ -170,7 +172,7 @@ async def card_view(
await state.set_state(None)
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:
lines.append(esc(detail.description))
if detail.stages:
@@ -195,18 +197,18 @@ async def card_view(
for index, key in enumerate(defaults):
value = form["values"].get(key, defaults[key])
kb.button(
text=label(f"{key}: {render(value)}", 40),
text=label(f"✏️ {key}: {render(value)}", 40),
callback_data=f"act:f:{index}:{ref}",
)
width = 2 if len(defaults) > WIDE_FORM else 1
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:pin:{ref}")
kb.button(text="▶️ Запустить", callback_data=f"act:run:{ref}", style=SUCCESS)
kb.button(text="📌 Закрепить", callback_data=f"act:pin:{ref}")
tail = 2
if form["values"]:
kb.button(text="Сбросить", callback_data=f"act:reset:{ref}")
kb.button(text="↩️ Сбросить", callback_data=f"act:reset:{ref}")
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)
return "\n".join(lines), kb.as_markup()
@@ -341,10 +343,10 @@ async def act_field(callback: types.CallbackQuery, state: FSMContext) -> None:
kb = InlineKeyboardBuilder()
if key in form["values"]:
kb.button(
text=label(f"Вернуть {render(default)}", 40),
text=label(f"↩️ Вернуть {render(default)}", 40),
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)
await show(
callback,
@@ -380,7 +382,7 @@ async def on_value(message: types.Message, state: FSMContext) -> None:
await show(
message,
f"<b>{esc(key)}</b>: {esc(exc)}. Пришли ещё раз.",
back_to("Отмена", f"act:back:{_ref(form)}"),
back_to("✖️ Отмена", f"act:back:{_ref(form)}"),
)
return
except KomodoError as exc:
@@ -413,7 +415,10 @@ async def _execute( # noqa: PLR0913
origin,
title or name,
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",
extra=_args_code(args),
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"])
return
kb = InlineKeyboardBuilder()
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:go:{_ref(form)}", style=SUCCESS)
kb.button(text="✖️ Отмена", callback_data=f"act:back:{_ref(form)}")
kb.adjust(2)
changes = f"\nИзменено: {_values_line(form['values'])}" if form["values"] else ""
await show(
@@ -461,9 +466,9 @@ async def act_pin(callback: types.CallbackQuery, state: FSMContext) -> None:
)
kb = InlineKeyboardBuilder()
ref = _ref(form)
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:back:{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:back:{ref}")
kb.adjust(2, 1)
await show(
callback,
@@ -486,17 +491,17 @@ def pin_view(pin: Pin) -> tuple[str, types.InlineKeyboardMarkup]:
values,
]
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
kb.button(
text="Открывать форму" if other is PinMode.FORM else "Запускать сразу",
text="📝 Открывать форму" if other is PinMode.FORM else "⚡ Запускать сразу",
callback_data=f"pin:mode:{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:del:{pin.id}", style=DANGER)
kb.button(text="Закреплённые", callback_data="set:pins")
kb.button(text="Меню", callback_data="menu:main")
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:del:{pin.id}", style=DANGER)
kb.button(text="📌 Закреплённые", callback_data="set:pins")
kb.button(text="🏠 Меню", callback_data="menu:main")
kb.adjust(2, 2, 1, 2)
return "\n".join(lines), kb.as_markup()
@@ -527,12 +532,12 @@ async def pins_view() -> tuple[str, types.InlineKeyboardMarkup]:
pins = await db.pins.all()
kb = InlineKeyboardBuilder()
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():
kb.button(text="Действия", callback_data="act:list:a:0")
kb.button(text="Меню", callback_data="menu:main")
kb.button(text="🎛 Действия", callback_data="act:list:a:0")
kb.button(text="🏠 Меню", callback_data="menu:main")
kb.adjust(*[1] * len(pins), 2)
text = "<b>Закреплённые</b>"
text = "📌 <b>Закреплённые</b>"
if not pins:
text += "\nПока пусто. Открой действие и нажми «Закрепить»."
return text, kb.as_markup()
@@ -630,7 +635,7 @@ async def pin_rename(callback: types.CallbackQuery, state: FSMContext) -> None:
await show(
callback,
f"Пришли новое название для кнопки <b>{esc(pin.label)}</b>.",
back_to("Отмена", f"pin:open:{pin.id}"),
back_to("✖️ Отмена", f"pin:open:{pin.id}"),
)
await callback.answer()
@@ -685,7 +690,7 @@ async def on_command(message: types.Message, state: FSMContext) -> None:
await show(
message,
"Запуск действий командами выключен.",
back_to("Настройки", "set:main"),
back_to("⚙️ Настройки", "set:main"),
)
return
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("За чем следить?")
if path:
kb.button(text="Назад", callback_data="add:up")
kb.button(text="⬆️ Назад", callback_data="add:up")
rows.append(1)
kb.button(text=f"HTTP-статус {data['status']}", callback_data="add:status")
kb.button(text="Текст в ответе", callback_data="add:text")
kb.button(text="Тело целиком", callback_data="add:body")
kb.button(text="Отмена", callback_data="add:cancel")
kb.button(text=f"🔢 HTTP-статус {data['status']}", callback_data="add:status")
kb.button(text="🔤 Текст в ответе", callback_data="add:text")
kb.button(text="📄 Тело целиком", callback_data="add:body")
kb.button(text="✖️ Отмена", callback_data="add:cancel")
kb.adjust(*rows, 2, 2)
return "\n".join(lines), kb.as_markup()
@@ -151,10 +151,10 @@ async def _probe(url: str) -> Probe:
def _unreachable_kb(*, editing: bool) -> types.InlineKeyboardMarkup:
kb = InlineKeyboardBuilder()
kb.button(text="Повторить", callback_data="add:retry")
kb.button(text="🔄 Повторить", callback_data="add:retry")
if not editing:
kb.button(text="Добавить всё равно", callback_data="add:force")
kb.button(text="Отмена", callback_data="add:cancel")
kb.button(text="➕ Добавить всё равно", callback_data="add:force")
kb.button(text="✖️ Отмена", callback_data="add:cancel")
kb.adjust(2, 1)
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]
if 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:
@@ -265,7 +265,7 @@ async def on_new_url(message: types.Message, state: FSMContext) -> None:
await show(message, "Сервис уже удалён.", back_to_menu())
return
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:
await show(message, "Нужна одна ссылка.", back)
return
@@ -292,7 +292,7 @@ async def svc_url(callback: types.CallbackQuery, state: FSMContext) -> None:
callback,
f"Пришли новый адрес для <b>{esc(service.name)}</b>.\n"
f"Сейчас: <code>{esc(service.url)}</code>",
back_to("Отмена", f"svc:open:{service.id}"),
back_to("✖️ Отмена", f"svc:open:{service.id}"),
)
await callback.answer()
@@ -372,7 +372,7 @@ async def on_pick_text(callback: types.CallbackQuery, state: FSMContext) -> None
await show(
callback,
"Пришли текст, который должен быть в ответе.",
back_to("Отмена", "add:cancel"),
back_to("✖️ Отмена", "add:cancel"),
)
await callback.answer()
@@ -385,7 +385,7 @@ async def on_needle(message: types.Message, state: FSMContext) -> None:
await show(
message,
"Сейчас такого текста в ответе нет. Пришли другой.",
back_to("Отмена", "add:cancel"),
back_to("✖️ Отмена", "add:cancel"),
)
return
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),
"sta": ("Start", "StartStack", None),
}
OP_ICONS = {"dep": "🚀", "res": "🔁", "sto": "⏹", "sta": "▶️"}
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()
)
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()
for server in servers:
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]
extra = 0
if broken:
kb.button(text=f"Не работают: {len(broken)}", callback_data="fleet:bad")
kb.button(text=f"⚠️ Не работают: {len(broken)}", callback_data="fleet:bad")
extra += 1
if alerts:
kb.button(text=f"Алерты: {len(alerts)}", callback_data="fleet:alerts")
kb.button(text=f"🔔 Алерты: {len(alerts)}", callback_data="fleet:alerts")
extra += 1
kb.button(text="Журнал", callback_data="fleet:journal")
kb.button(text="Обновить", callback_data="fleet:main")
kb.button(text="Меню", callback_data="menu:main")
kb.button(text="🕘 Журнал", callback_data="fleet:journal")
kb.button(text="🔄 Обновить", callback_data="fleet:main")
kb.button(text="🏠 Меню", callback_data="menu:main")
kb.adjust(*rows, *([extra] if extra else []), 3)
return "\n".join(lines), kb.as_markup()
@@ -122,7 +123,7 @@ async def server_view(
servers, stacks = await asyncio.gather(komodo.servers(), komodo.stacks())
server = next((s for s in servers if s.id == server_id), None)
if server is None:
return "Сервер не найден.", back_to("К флоту", "fleet:main")
return "Сервер не найден.", back_to("⬅️ К флоту", "fleet:main")
lines = [f"{server.mark} <b>{esc(server.name)}</b>"]
if server.state == "NotOk":
@@ -161,8 +162,8 @@ async def server_view(
kb = InlineKeyboardBuilder()
rows = _stack_buttons(kb, mine[page * STACKS_PAGE : (page + 1) * STACKS_PAGE])
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="fleet:main")
kb.button(text="🔄 Обновить", callback_data=f"fleet:srv:{server_id}:{page}")
kb.button(text="⬅️ К флоту", callback_data="fleet:main")
kb.adjust(*rows, *([nav] if nav else []), 2)
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]:
stack = await komodo.stack(stack_id)
if stack is None:
return "Стек не найден, возможно его удалили.", back_to("К флоту", "fleet:main")
return "Стек не найден, возможно его удалили.", back_to(
"⬅️ К флоту", "fleet:main"
)
lines = [
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")
for key in shown:
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(
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 "К флоту",
text="⬅️ К серверу" if stack.server_id else "⬅️ К флоту",
callback_data=f"fleet:srv:{stack.server_id}:0"
if stack.server_id
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),
key=lambda s: (s.server_name, s.name),
)
lines = ["<b>Не работают</b>", ""]
lines = ["⚠️ <b>Не работают</b>", ""]
lines.extend(
f"{s.mark} {esc(s.name)} · {esc(s.server_name)} · {esc(s.state)}"
for s in broken
)
kb = InlineKeyboardBuilder()
rows = _stack_buttons(kb, broken[:40])
kb.button(text="К флоту", callback_data="fleet:main")
kb.button(text="⬅️ К флоту", callback_data="fleet:main")
kb.adjust(*rows, 1)
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]:
tz = db.settings.get("tz")
alerts = await komodo.open_alerts()
lines = ["<b>Открытые алерты</b>"]
lines = ["🔔 <b>Открытые алерты</b>"]
if not alerts:
lines.append("Сейчас ни одного.")
for alert in alerts[:10]:
since = stamp(from_ms(alert.get("ts") or 0), tz)
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")
@@ -350,9 +357,11 @@ async def fleet_ask(callback: types.CallbackQuery) -> None:
return
kb = InlineKeyboardBuilder()
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)
await show(
callback,
@@ -384,8 +393,8 @@ async def fleet_do(callback: types.CallbackQuery) -> None:
f"{text} {name}",
starter,
back=[
("К стеку", f"fleet:stack:{stack_id}"),
("Логи", f"fleet:logs:{stack_id}"),
("⬅️ К стеку", f"fleet:stack:{stack_id}"),
("📜 Логи", f"fleet:logs:{stack_id}"),
],
verbose=False,
)
@@ -402,7 +411,7 @@ async def _log_view(
) -> tuple[str, types.InlineKeyboardMarkup]:
stack = await komodo.stack(stack_id)
if stack is None:
return "Стек не найден.", back_to("К флоту", "fleet:main")
return "Стек не найден.", back_to("⬅️ К флоту", "fleet:main")
services = _picked(stack, index)
log = await komodo.stack_log(stack_id, services, LOG_TAIL)
if services or len(stack.services) == 1:
@@ -410,15 +419,15 @@ async def _log_view(
scope = f" · {esc(services[0])}" if services else ""
body = pre(log, LOG_CHARS) if log else "<i>лог пуст</i>"
kb = InlineKeyboardBuilder()
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:log:{stack_id}:{index}")
kb.button(text="📎 Файлом", callback_data=f"fleet:logf:{stack_id}:{index}")
rows = [2]
if len(stack.services) > 1:
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:logs:{stack_id}")
kb.button(text="⬅️ К стеку", callback_data=f"fleet:stack:{stack_id}")
rows.append(2)
else:
kb.button(text="К стеку", callback_data=f"fleet:stack:{stack_id}")
kb.button(text="⬅️ К стеку", callback_data=f"fleet:stack:{stack_id}")
rows.append(1)
kb.adjust(*rows)
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]:
stack = await komodo.stack(stack_id)
if stack is None:
return "Стек не найден.", back_to("К флоту", "fleet:main")
return "Стек не найден.", back_to("⬅️ К флоту", "fleet:main")
if len(stack.services) <= 1:
return await _log_view(stack_id, ALL_SERVICES)
kb = InlineKeyboardBuilder()
@@ -435,8 +444,10 @@ async def _log_picker(stack_id: str) -> tuple[str, types.InlineKeyboardMarkup]:
kb.button(
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(text="К стеку", callback_data=f"fleet:stack:{stack_id}")
kb.button(
text="🗂 Все сервисы", callback_data=f"fleet:log:{stack_id}:{ALL_SERVICES}"
)
kb.button(text="⬅️ К стеку", callback_data=f"fleet:stack:{stack_id}")
count = len(stack.services)
kb.adjust(*[2] * (count // 2), *[1] * (count % 2), 2)
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 "Стек"
kb = InlineKeyboardBuilder()
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)
body = "\n".join(lines) if lines else "Операций ещё не было."
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)
kb = InlineKeyboardBuilder()
count = _failed_buttons(kb, failed)
kb.button(text="Обновить", callback_data="fleet:journal")
kb.button(text="К флоту", callback_data="fleet:main")
kb.button(text="🔄 Обновить", callback_data="fleet:journal")
kb.button(text="⬅️ К флоту", callback_data="fleet:main")
kb.adjust(*[1] * count, 2)
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")
+5 -5
View File
@@ -78,13 +78,13 @@ async def _services_block() -> list[str]:
works = plural(working, "работает", "работают", "работают")
if not down and not paused:
every = "все " if working > 1 else ""
return [f"<b>Сервисы</b>: {every}{working} {works}"]
return [f"📋 <b>Сервисы</b>: {every}{working} {works}"]
parts = [f"{working} {works}"]
if down:
parts.append(f"{len(down)} {plural(len(down), 'лежит', 'лежат', 'лежат')}")
if 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]])
return lines
@@ -95,11 +95,11 @@ async def _fleet_block() -> list[str]:
komodo.servers(), komodo.stacks(), komodo.open_alerts()
)
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"}
good = sum(s.healthy for s in stacks)
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:
if server.state == "NotOk":
lines.append(f"🔴 {esc(server.name)} не отвечает")
@@ -123,7 +123,7 @@ async def dashboard() -> tuple[str, types.InlineKeyboardMarkup]:
try:
fleet = await asyncio.wait_for(_fleet_block(), FLEET_TIMEOUT)
except TimeoutError:
fleet = ["<b>Флот</b>: Komodo не отвечает"]
fleet = ["🛰 <b>Флот</b>: Komodo не отвечает"]
lines += ["", *fleet]
pins = await db.pins.all() if actions else []
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()
if not update.done:
kb = InlineKeyboardBuilder()
kb.button(text="Проверить", callback_data=f"run:st:{update_id}")
kb.button(text="🔍 Проверить", callback_data=f"run:st:{update_id}")
await show(
callback,
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}"]
if 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"))
@@ -311,7 +311,7 @@ async def svc_incidents(callback: types.CallbackQuery) -> None:
total = downtime(incidents, parse(service.created_at), now())
lines += ["", f"Всего {len(incidents)}, даунтайм {human_duration(total)}"]
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()
@@ -326,7 +326,7 @@ async def svc_rename(callback: types.CallbackQuery, state: FSMContext) -> None:
await show(
callback,
f"Пришли новое имя для <b>{esc(service.name)}</b>.",
back_to("Отмена", f"svc:open:{service.id}"),
back_to("✖️ Отмена", f"svc:open:{service.id}"),
)
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.sort(key=lambda row: row[0])
lines = ["<b>Статистика</b>", ""]
lines = ["📊 <b>Статистика</b>", ""]
for _, service, incidents in rows:
speed = (
f" · {service.last_latency_ms} мс"
+18 -18
View File
@@ -21,9 +21,9 @@ router.message.filter(Admin())
router.callback_query.filter(Admin())
TOGGLES = {
"actions": ("Действия Komodo", "включены", "выключены"),
"commands": ("Действия как команды", "включены", "выключены"),
"confirm": ("Подтверждение запуска", "включено", "выключено"),
"actions": ("🎛 Действия Komodo", "включены", "выключены"),
"commands": ("⌨️ Действия как команды", "включены", "выключены"),
"confirm": ("🛡 Подтверждение запуска", "включено", "выключено"),
}
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]:
tz = db.settings.get("tz")
lines = ["<b>Настройки</b>", await _komodo_line()]
lines = ["⚙️ <b>Настройки</b>", await _komodo_line()]
kb = InlineKeyboardBuilder()
if env.komodo.enabled:
for key, (title, on, off) in TOGGLES.items():
@@ -59,14 +59,14 @@ async def settings_view() -> tuple[str, types.InlineKeyboardMarkup]:
)
if env.web.enabled:
muted = len(db.settings.get("muted"))
kb.button(text=f"Алерты Komodo, скрыто: {muted}", callback_data="set:alerts")
kb.button(text=f"Часовой пояс: {tz}", callback_data="set:tz")
kb.button(text=f"🔔 Алерты Komodo, скрыто: {muted}", callback_data="set:alerts")
kb.button(text=f"🕒 Часовой пояс: {tz}", callback_data="set:tz")
hour = db.settings.get("digest")
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"):
kb.button(text="Закреплённые", callback_data="set:pins")
kb.button(text="Меню", callback_data="menu:main")
kb.button(text="📌 Закреплённые", callback_data="set:pins")
kb.button(text="🏠 Меню", callback_data="menu:main")
kb.adjust(1)
return "\n".join(lines), kb.as_markup()
@@ -106,9 +106,9 @@ def _alerts_view() -> tuple[str, types.InlineKeyboardMarkup]:
for kind in kinds:
mark = "🔕" if kind in muted else "🔔"
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)
return "<b>Алерты Komodo</b>", kb.as_markup()
return "🔔 <b>Алерты Komodo</b>", kb.as_markup()
@router.callback_query(F.data == "set:alerts")
@@ -158,7 +158,7 @@ async def alert_mute(callback: types.CallbackQuery) -> None:
kind = tail_arg(callback.data)
if kind not in db.settings.get("muted"):
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:"))
@@ -166,7 +166,7 @@ async def alert_unmute(callback: types.CallbackQuery) -> None:
kind = tail_arg(callback.data)
if kind in db.settings.get("muted"):
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")
@@ -178,7 +178,7 @@ async def settings_tz(callback: types.CallbackQuery, state: FSMContext) -> None:
callback,
"Пришли часовой пояс, например <code>Europe/Warsaw</code>.\n"
f"Сейчас: {esc(tz)}, на часах {local}",
back_to("Отмена", "set:main"),
back_to("✖️ Отмена", "set:main"),
)
await callback.answer()
@@ -193,7 +193,7 @@ async def on_tz(message: types.Message, state: FSMContext) -> None:
message,
"Такого пояса нет. Нужно имя из базы tz, например "
"<code>Europe/Warsaw</code> или <code>UTC</code>.",
back_to("Отмена", "set:main"),
back_to("✖️ Отмена", "set:main"),
)
return
await state.clear()
@@ -207,12 +207,12 @@ async def settings_digest(callback: types.CallbackQuery) -> None:
kb = InlineKeyboardBuilder()
for hour in DIGEST_HOURS:
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:main")
kb.button(text="🚫 Выключить", callback_data="set:dg:off")
kb.button(text="⬅️ К настройкам", callback_data="set:main")
kb.adjust(4, 3, 2)
await show(
callback,
"<b>Сводка за сутки</b>\n"
"🗞 <b>Сводка за сутки</b>\n"
"Раз в день: аптайм и падения сервисов, операции и алерты флота.\n"
f"Время по поясу {esc(db.settings.get('tz'))}.",
kb.as_markup(),
+37 -29
View File
@@ -1,38 +1,44 @@
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
from aiogram.utils.keyboard import InlineKeyboardBuilder
from healthbot.ui import DANGER, PRIMARY, label, pager, pages
from utils.db.models import Pin, Service
from healthbot.ui import DANGER, PRIMARY, label, pager, pages, with_icon
from utils.db.models import Pin, PinMode, Service
from utils.env import env
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:
kb = InlineKeyboardBuilder()
rows: list[int] = []
for pin in pins:
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)
kb.button(text="Сервисы", callback_data="menu:list:0")
kb.button(text="📋 Сервисы", callback_data="menu:list:0")
if env.komodo.enabled:
kb.button(text="Флот", callback_data="fleet:main")
kb.button(text="🛰 Флот", callback_data="fleet:main")
if actions:
kb.button(text="Действия", callback_data="act:list:a:0")
kb.button(text="Статистика", callback_data="menu:stats")
kb.button(text="🎛 Действия", callback_data="act:list:a:0")
kb.button(text="📊 Статистика", callback_data="menu:stats")
rows += [2, 2]
else:
kb.button(text="Статистика", callback_data="menu:stats")
kb.button(text="📊 Статистика", callback_data="menu:stats")
rows += [2, 1]
else:
kb.button(text="Статистика", callback_data="menu:stats")
kb.button(text="📊 Статистика", callback_data="menu:stats")
rows += [2]
kb.button(text="Добавить сервис", callback_data="menu:add")
kb.button(text="Настройки", callback_data="set:main")
kb.button(text="Обновить", callback_data="menu:main")
kb.button(text="➕ Добавить сервис", callback_data="menu:add")
kb.button(text="⚙️ Настройки", callback_data="set:main")
kb.button(text="🔄 Обновить", callback_data="menu:main")
kb.adjust(*rows, 2, 1)
return kb.as_markup()
@@ -60,33 +66,35 @@ def services_list(services: list[Service], page: int) -> InlineKeyboardMarkup:
callback_data=f"svc:open:{svc.id}",
)
nav = pager(kb, "menu:list", page, total)
kb.button(text="Добавить", callback_data="menu:add")
kb.button(text="Меню", callback_data="menu:main")
kb.button(text="➕ Добавить", callback_data="menu:add")
kb.button(text="🏠 Меню", callback_data="menu:main")
kb.adjust(*[1] * len(chunk), *([nav] if nav else []), 2)
return kb.as_markup()
def service_card(service: Service) -> InlineKeyboardMarkup:
kb = InlineKeyboardBuilder()
kb.button(text="Проверить", callback_data=f"svc:check:{service.id}", style=PRIMARY)
kb.button(text="Инциденты", callback_data=f"svc:inc:{service.id}")
kb.button(
text="🔍 Проверить", callback_data=f"svc:check:{service.id}", style=PRIMARY
)
kb.button(text="🚨 Инциденты", callback_data=f"svc:inc:{service.id}")
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:
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:rename:{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="menu:list:0")
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:rename:{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="menu:list:0")
kb.adjust(2)
return kb.as_markup()
def confirm_delete(service_id: int) -> InlineKeyboardMarkup:
kb = InlineKeyboardBuilder()
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:delyes:{service_id}", style=DANGER)
kb.button(text="✖️ Отмена", callback_data=f"svc:open:{service_id}")
kb.adjust(2)
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:
kb = InlineKeyboardBuilder()
if offer_baseline:
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:open:{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:open:{service_id}")
kb.adjust(*([1] if offer_baseline else []), 2)
return kb.as_markup()
@@ -108,4 +116,4 @@ def back_to(text: str, data: str) -> 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:
kb = InlineKeyboardBuilder()
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:check:{service.id}")
kb.button(text="📂 Открыть", callback_data=f"go:svc:{service.id}")
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)
return kb.as_markup()
def _open_kb(service: Service) -> InlineKeyboardMarkup:
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()
+2 -2
View File
@@ -62,7 +62,7 @@ def result_kb(update: Update | None, back: Back) -> InlineKeyboardMarkup:
kb.button(text=text, callback_data=data)
rows = [len(back)] if back else []
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)
kb.adjust(*rows or [1])
return kb.as_markup()
@@ -119,7 +119,7 @@ async def _track( # noqa: PLR0913
return
check = InlineKeyboardBuilder()
check.button(text="Проверить", callback_data=f"run:st:{started.id}")
check.button(text="🔍 Проверить", callback_data=f"run:st:{started.id}")
try:
update = await komodo.wait(started.id, RUN_TIMEOUT)
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
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:
return max(1, (total + size - 1) // size)
@@ -97,10 +101,10 @@ def pager(kb: InlineKeyboardBuilder, prefix: str, page: int, total_pages: int) -
return 0
count = 0
if page > 0:
kb.button(text="‹ Назад", callback_data=f"{prefix}:{page - 1}")
kb.button(text="◀️ Назад", callback_data=f"{prefix}:{page - 1}")
count += 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
return count