Админка стала удобной с телефона

Навигация сворачивается в бургер-меню, второстепенные колонки таблиц скрываются на узких экранах, формы и тулбар редактора нормально складываются в одну колонку. Основные действия и ссылки остаются доступны на ширине 360–390 px.
This commit is contained in:
MpakobecEx 2026-10-25 21:47:06 +05:00
parent 1c85590f6f
commit cfa0e29bbf
9 changed files with 66 additions and 40 deletions

View File

@ -96,6 +96,9 @@ ID пользователя ведёт в его карточку. ClickHouse ч
код, блок кода, цитата, спойлер и ссылка; есть предпросмотр и счётчик символов код, блок кода, цитата, спойлер и ссылка; есть предпросмотр и счётчик символов
(1024 для подписи к фото, 4096 для обычного сообщения). (1024 для подписи к фото, 4096 для обычного сообщения).
Интерфейс адаптирован под мобильные: бургер-меню, второстепенные колонки таблиц
скрываются на узких экранах, формы складываются в одну колонку.
## Аналитика (ClickHouse) ## Аналитика (ClickHouse)
Все значимые действия бота пишутся в ClickHouse в единую таблицу `analytics.events`. Все значимые действия бота пишутся в ClickHouse в единую таблицу `analytics.events`.
@ -158,7 +161,7 @@ make backup E=test # на домашнем сервере: ./scripts/clickho
## Версионирование ## Версионирование
- Единый источник версии — `common/version.py` (`__version__`), текущая — `0.5.2`. - Единый источник версии — `common/version.py` (`__version__`), текущая — `0.5.3`.
- Схема — SemVer-подобная (`MAJOR.MINOR.PATCH`); git-тег = `v` + версия (`v0.1`). - Схема — SemVer-подобная (`MAJOR.MINOR.PATCH`); git-тег = `v` + версия (`v0.1`).
- Docker-образы тегируются версией (Блок 2), события ClickHouse получают `bot_version` (Блок 3). - Docker-образы тегируются версией (Блок 2), события ClickHouse получают `bot_version` (Блок 3).
- Выпуск: изменить `common/version.py` → коммит → аннотированный тег `vX.Y` → `git push --tags`. - Выпуск: изменить `common/version.py` → коммит → аннотированный тег `vX.Y` → `git push --tags`.

View File

@ -81,7 +81,7 @@
<th class="px-4 py-3 font-medium">Username</th> <th class="px-4 py-3 font-medium">Username</th>
<th class="px-4 py-3 font-medium">Роль</th> <th class="px-4 py-3 font-medium">Роль</th>
<th class="px-4 py-3 font-medium">Доступ</th> <th class="px-4 py-3 font-medium">Доступ</th>
<th class="px-4 py-3 font-medium">Последний вход</th> <th class="hidden px-4 py-3 font-medium lg:table-cell">Последний вход</th>
<th class="px-4 py-3 font-medium"></th> <th class="px-4 py-3 font-medium"></th>
</tr> </tr>
</thead> </thead>
@ -116,7 +116,7 @@
<span class="rounded bg-slate-100 px-2 py-0.5 text-xs text-slate-500">выключен</span> <span class="rounded bg-slate-100 px-2 py-0.5 text-xs text-slate-500">выключен</span>
{% endif %} {% endif %}
</td> </td>
<td class="px-4 py-3 text-slate-600"> <td class="hidden px-4 py-3 text-slate-600 lg:table-cell">
{{ admin.last_login_at.strftime("%d.%m.%Y %H:%M") if admin.last_login_at else "—" }} {{ admin.last_login_at.strftime("%d.%m.%Y %H:%M") if admin.last_login_at else "—" }}
</td> </td>
<td class="px-4 py-3"> <td class="px-4 py-3">

View File

@ -9,25 +9,38 @@
</head> </head>
<body class="min-h-screen bg-slate-100 text-slate-900"> <body class="min-h-screen bg-slate-100 text-slate-900">
<header class="border-b border-slate-200 bg-white"> <header class="border-b border-slate-200 bg-white">
<div class="mx-auto flex max-w-5xl items-center justify-between px-4 py-4"> <div class="mx-auto max-w-5xl px-4 py-3">
<a href="/" class="text-lg font-semibold">DispelEvilBot</a> <div class="flex flex-wrap items-center justify-between gap-3">
<nav class="flex items-center gap-4 text-sm"> <a href="/" class="text-lg font-semibold">DispelEvilBot</a>
<a href="/" class="text-slate-600 hover:text-slate-900">Главная</a> <button
<a href="/messages" class="text-slate-600 hover:text-slate-900">Сообщения</a> id="nav-toggle"
<a href="/sequences" class="text-slate-600 hover:text-slate-900">Последовательности</a> type="button"
<a href="/schedules" class="text-slate-600 hover:text-slate-900">Расписания</a> class="rounded-lg border border-slate-300 px-3 py-1.5 text-sm text-slate-600 sm:hidden"
<a href="/users" class="text-slate-600 hover:text-slate-900">Пользователи</a> aria-expanded="false"
<a href="/admins" class="text-slate-600 hover:text-slate-900">Админы</a> >
<a href="/logs" class="text-slate-600 hover:text-slate-900">Логи</a> Меню
{% if request.session.get('admin_id') %} </button>
<span class="text-slate-500">{{ request.session.get('admin_username') or 'админ' }}</span> <nav
<form method="post" action="/logout"> id="nav-menu"
<input type="hidden" name="csrf_token" value="{{ request.session.get('csrf', '') }}" /> class="hidden w-full flex-col gap-2 border-t border-slate-100 pt-2 text-sm sm:flex sm:w-auto sm:flex-row sm:items-center sm:gap-4 sm:border-0 sm:pt-0"
<button type="submit" class="text-slate-600 hover:text-slate-900">Выйти</button> >
</form> <a href="/" class="py-1 text-slate-600 hover:text-slate-900">Главная</a>
{% endif %} <a href="/messages" class="py-1 text-slate-600 hover:text-slate-900">Сообщения</a>
<span class="text-slate-500">админка · v{{ version }}</span> <a href="/sequences" class="py-1 text-slate-600 hover:text-slate-900">Последовательности</a>
</nav> <a href="/schedules" class="py-1 text-slate-600 hover:text-slate-900">Расписания</a>
<a href="/users" class="py-1 text-slate-600 hover:text-slate-900">Пользователи</a>
<a href="/admins" class="py-1 text-slate-600 hover:text-slate-900">Админы</a>
<a href="/logs" class="py-1 text-slate-600 hover:text-slate-900">Логи</a>
{% if request.session.get('admin_id') %}
<span class="py-1 text-slate-500">{{ request.session.get('admin_username') or 'админ' }}</span>
<form method="post" action="/logout" class="py-1">
<input type="hidden" name="csrf_token" value="{{ request.session.get('csrf', '') }}" />
<button type="submit" class="text-slate-600 hover:text-slate-900">Выйти</button>
</form>
{% endif %}
<span class="py-1 text-slate-500">админка · v{{ version }}</span>
</nav>
</div>
</div> </div>
</header> </header>
@ -39,6 +52,16 @@
<script> <script>
document.addEventListener("DOMContentLoaded", function () { document.addEventListener("DOMContentLoaded", function () {
var navToggle = document.getElementById("nav-toggle");
var navMenu = document.getElementById("nav-menu");
if (navToggle && navMenu) {
navToggle.addEventListener("click", function () {
var hidden = navMenu.classList.toggle("hidden");
navMenu.classList.toggle("flex", !hidden);
navToggle.setAttribute("aria-expanded", String(!hidden));
});
}
var tags = { var tags = {
b: ["<b>", "</b>"], b: ["<b>", "</b>"],
i: ["<i>", "</i>"], i: ["<i>", "</i>"],

View File

@ -72,8 +72,8 @@
<th class="px-4 py-3 font-medium">Время</th> <th class="px-4 py-3 font-medium">Время</th>
<th class="px-4 py-3 font-medium">Событие</th> <th class="px-4 py-3 font-medium">Событие</th>
<th class="px-4 py-3 font-medium">Пользователь</th> <th class="px-4 py-3 font-medium">Пользователь</th>
<th class="px-4 py-3 font-medium">Чат</th> <th class="hidden px-4 py-3 font-medium lg:table-cell">Чат</th>
<th class="px-4 py-3 font-medium">Версия</th> <th class="hidden px-4 py-3 font-medium xl:table-cell">Версия</th>
<th class="px-4 py-3 font-medium">Payload</th> <th class="px-4 py-3 font-medium">Payload</th>
</tr> </tr>
</thead> </thead>
@ -93,8 +93,8 @@
<span class="text-slate-400">—</span> <span class="text-slate-400">—</span>
{% endif %} {% endif %}
</td> </td>
<td class="px-4 py-3 font-mono text-xs text-slate-500">{{ event.chat_id or "—" }}</td> <td class="hidden px-4 py-3 font-mono text-xs text-slate-500 lg:table-cell">{{ event.chat_id or "—" }}</td>
<td class="px-4 py-3 text-xs text-slate-500">{{ event.bot_version }}</td> <td class="hidden px-4 py-3 text-xs text-slate-500 xl:table-cell">{{ event.bot_version }}</td>
<td class="px-4 py-3"> <td class="px-4 py-3">
{% if event.payload_pretty %} {% if event.payload_pretty %}
<details> <details>

View File

@ -29,8 +29,8 @@
<tr> <tr>
<th class="px-4 py-3 font-medium">Ключ</th> <th class="px-4 py-3 font-medium">Ключ</th>
<th class="px-4 py-3 font-medium">Описание</th> <th class="px-4 py-3 font-medium">Описание</th>
<th class="px-4 py-3 font-medium">Длина</th> <th class="hidden px-4 py-3 font-medium sm:table-cell">Длина</th>
<th class="px-4 py-3 font-medium">Изменён</th> <th class="hidden px-4 py-3 font-medium md:table-cell">Изменён</th>
</tr> </tr>
</thead> </thead>
<tbody class="divide-y divide-slate-100"> <tbody class="divide-y divide-slate-100">
@ -40,8 +40,8 @@
<a href="/messages/{{ template.key }}" class="font-mono text-sky-700 hover:underline">{{ template.key }}</a> <a href="/messages/{{ template.key }}" class="font-mono text-sky-700 hover:underline">{{ template.key }}</a>
</td> </td>
<td class="px-4 py-3 text-slate-700">{{ template.description }}</td> <td class="px-4 py-3 text-slate-700">{{ template.description }}</td>
<td class="px-4 py-3 text-slate-500">{{ template.text_length }}</td> <td class="hidden px-4 py-3 text-slate-500 sm:table-cell">{{ template.text_length }}</td>
<td class="px-4 py-3 text-slate-500">{{ template.updated_at.strftime("%d.%m.%Y %H:%M") }}</td> <td class="hidden px-4 py-3 text-slate-500 md:table-cell">{{ template.updated_at.strftime("%d.%m.%Y %H:%M") }}</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>

View File

@ -20,9 +20,9 @@
<thead class="bg-slate-50 text-left text-slate-600"> <thead class="bg-slate-50 text-left text-slate-600">
<tr> <tr>
<th class="px-4 py-3 font-medium">Время</th> <th class="px-4 py-3 font-medium">Время</th>
<th class="px-4 py-3 font-medium">Дни</th> <th class="hidden px-4 py-3 font-medium sm:table-cell">Дни</th>
<th class="px-4 py-3 font-medium">Шаблон</th> <th class="px-4 py-3 font-medium">Шаблон</th>
<th class="px-4 py-3 font-medium">Таймзона</th> <th class="hidden px-4 py-3 font-medium lg:table-cell">Таймзона</th>
<th class="px-4 py-3 font-medium">Статус</th> <th class="px-4 py-3 font-medium">Статус</th>
<th class="px-4 py-3 font-medium"></th> <th class="px-4 py-3 font-medium"></th>
</tr> </tr>
@ -31,12 +31,12 @@
{% for schedule in schedules %} {% for schedule in schedules %}
<tr class="hover:bg-slate-50"> <tr class="hover:bg-slate-50">
<td class="px-4 py-3 font-mono">{{ "%02d:%02d" | format(schedule.hour, schedule.minute) }}</td> <td class="px-4 py-3 font-mono">{{ "%02d:%02d" | format(schedule.hour, schedule.minute) }}</td>
<td class="px-4 py-3">{{ format_weekdays(schedule.weekdays) }}</td> <td class="hidden px-4 py-3 sm:table-cell">{{ format_weekdays(schedule.weekdays) }}</td>
<td class="px-4 py-3"> <td class="px-4 py-3">
<div class="font-mono text-sky-700">{{ schedule.template_key }}</div> <div class="font-mono text-sky-700">{{ schedule.template_key }}</div>
<div class="text-xs text-slate-500">{{ schedule.description or schedule.template_description }}</div> <div class="text-xs text-slate-500">{{ schedule.description or schedule.template_description }}</div>
</td> </td>
<td class="px-4 py-3 text-slate-600">{{ schedule.timezone }}</td> <td class="hidden px-4 py-3 text-slate-600 lg:table-cell">{{ schedule.timezone }}</td>
<td class="px-4 py-3"> <td class="px-4 py-3">
{% if schedule.enabled %} {% if schedule.enabled %}
<span class="rounded bg-emerald-100 px-2 py-0.5 text-xs text-emerald-800">включено</span> <span class="rounded bg-emerald-100 px-2 py-0.5 text-xs text-emerald-800">включено</span>

View File

@ -35,9 +35,9 @@
<tr> <tr>
<th class="px-4 py-3 font-medium">Ник</th> <th class="px-4 py-3 font-medium">Ник</th>
<th class="px-4 py-3 font-medium">Telegram</th> <th class="px-4 py-3 font-medium">Telegram</th>
<th class="px-4 py-3 font-medium">Уровень</th> <th class="hidden px-4 py-3 font-medium sm:table-cell">Уровень</th>
<th class="px-4 py-3 font-medium">Баланс</th> <th class="px-4 py-3 font-medium">Баланс</th>
<th class="px-4 py-3 font-medium">Сообщения</th> <th class="hidden px-4 py-3 font-medium md:table-cell">Сообщения</th>
<th class="px-4 py-3 font-medium">Подписка</th> <th class="px-4 py-3 font-medium">Подписка</th>
</tr> </tr>
</thead> </thead>
@ -54,9 +54,9 @@
{% if user.username %}<div>@{{ user.username }}</div>{% endif %} {% if user.username %}<div>@{{ user.username }}</div>{% endif %}
<div class="font-mono text-xs text-slate-500">{{ user.id_user }}</div> <div class="font-mono text-xs text-slate-500">{{ user.id_user }}</div>
</td> </td>
<td class="px-4 py-3">{{ user.lvl }}</td> <td class="hidden px-4 py-3 sm:table-cell">{{ user.lvl }}</td>
<td class="px-4 py-3">{{ user.balance }}</td> <td class="px-4 py-3">{{ user.balance }}</td>
<td class="px-4 py-3">{{ user.msg_count }}</td> <td class="hidden px-4 py-3 md:table-cell">{{ user.msg_count }}</td>
<td class="px-4 py-3"> <td class="px-4 py-3">
{% if user.subscribed %} {% if user.subscribed %}
<span class="rounded bg-emerald-100 px-2 py-0.5 text-xs text-emerald-800">да</span> <span class="rounded bg-emerald-100 px-2 py-0.5 text-xs text-emerald-800">да</span>

View File

@ -7,4 +7,4 @@
from __future__ import annotations from __future__ import annotations
__version__ = "0.5.2" __version__ = "0.5.3"

View File

@ -7,4 +7,4 @@ from common import __version__
def test_version_is_set(): def test_version_is_set():
assert __version__ assert __version__
assert __version__ == "0.5.2" assert __version__ == "0.5.3"