Глобально уникальные ID (docs/changes/016): - у устройств, групп, задач, резервных копий и записей журнала ID вида <префикс>_<uuid7> (dev_, grp_, job_, bkp_, evt_): типы не пересекаются, внутри типа ID не повторяются и сортируются по времени; - миграция БД v1 заменяет числовые ID с пересчётом ссылок (копия файла БД перед миграцией, сверка числа строк, одна транзакция); - резервная копия = пара файлов с одним bkp_ ID, файлы в S3 получают метаданные backup-id/device-id; синхронизация метаданных с бакетом; - журнал событий в БД (events): создание/изменение/удаление, задачи, бэкапы, смена online/offline, вход в UI; API чтения GET /api/v1/events. Журнал в UI, ротация и очистка (docs/changes/017): - страница «Журнал»: фильтры, подгрузка «Показать ещё», окно записи; - настройки ротации (срок и максимум записей) хранятся в БД (схема v2), ротация при старте, раз в час и после сохранения настроек; - очистка журнала только через окно с паролем пользователя, блокировка после 5 неверных попыток, остаётся запись о факте очистки; через API очистки нет. Тесты: 20 из 20. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
12 KiB
План: 017 — журнал событий в UI, ротация и защищённая очистка
Статус: выполнено — макет утверждён (этап A), реализация выполнена (этап B), итоги — в summary.md.
Context
Журнал событий (events, 016) сейчас читается только через API. Нужно: (1) страница «Журнал» в UI; (2) настройки ротации; (3) кнопка очистки журнала, которая срабатывает только через модальное окно с вводом пароля пользователя.
Решения (согласованы): сначала макет на ревью, реализация после его одобрения; настройки ротации — в БД, редактируются из UI (значения по умолчанию из .env); ротация по возрасту и по числу записей; очистка удаляет всё и оставляет запись о факте очистки.
Работа идёт в два этапа. Этап A (макет) выполняется сразу после одобрения этого плана и заканчивается остановкой на ваше ревью. Этап B (код) — только после того, как макет утверждён.
Этап A — макет (артефакт claude.ai/artifact/QdhgQWbQEWRYtBoAWMjGq6, новые листы)
Оформление — тем же генератором и компонентами, что и утверждённые листы (h36 r8, метки h22 r6, карточки r12, IBM Plex); существующие листы не меняются, кроме заметки на холсте о новом пункте меню.
- «Журнал» — верхнее меню получает пятый пункт «Журнал» (в макете показан на новых листах; в приложении добавится во все экраны). Заголовок «Журнал», подзаголовок со сводкой («N записей · старейшая … · ротация: 90 дней / 100 000»). Кнопки справа: «Обновить» (secondary), «Настройки» (secondary), «Очистить журнал» (secondary, красный текст — тот же вид «опасного» действия, что «Удалить» на бэкапах). Primary-кнопки на странице нет. Карточка: полоса фильтров (Тип ▾, Актор ▾, Устройство ▾, период с/по, поиск по сообщению или ID, «Сбросить (N)»), таблица: Время (UTC) · Тип (метка по смыслу: ok/warn/bad/run/neutral) · Сущность (вид + короткий ID) · Сообщение · Актор · ID записи (короткий, моноширинный; полный — в подсказке и в окне записи), футер «Показано N из M» и кнопка «Показать ещё» (постраничная подгрузка по курсору).
- «Журнал · запись» — окно с полным содержимым записи:
evt_…, тип, время, актор, ID сущности, устройство, задача, сообщение, данные (JSON), кнопки «Копировать» у ID; закрывается «Закрыть». - «Журнал · настройки ротации» — окно: «Хранить записи, дней» и «Максимум записей» (0 = не ограничивать), справка («ротация раз в час и при сохранении удаляет самые старые записи и фиксируется записью в журнале»), сводка «сейчас N записей, старейшая …», «Отмена» / «Сохранить» (primary).
- «Журнал · очистка» — два состояния рядом: первичное и «неверный пароль». Окно: предупреждение (жёлтая заметка: «будет удалено N записей без возможности восстановления; останется одна запись об очистке»), поле «Пароль пользователя <имя>» (type=password), кнопка «Очистить» (secondary, красный текст) неактивна, пока пароль не введён, «Отмена». Состояние ошибки: красное сообщение «Неверный пароль» и состояние блокировки «Слишком много попыток, повторите через N мин».
- Заметка на холсте: пункт «Журнал» добавляется в верхнее меню всех экранов. Результат — ссылка на артефакт и вопросы на ревью; дальше — стоп до вашего ответа.
Этап B — реализация (после одобрения макета)
Данные и настройки.
- Таблица
app_settings(key PK, value JSON, updated_at)(app/models.py, миграция v2 вapp/migrations.pyчерезcreate_all,user_version1→2). Ключи:events.retention_days,events.max_rows. app/services/settings.py(новый): чтение/запись с валидацией (целые,0= без ограничения, верхние границы), значения по умолчанию изSettings(EVENTS_RETENTION_DAYS=90,EVENTS_MAX_ROWS=100000,app/config.py,.env.example); изменение пишет событиеjournal.settings_changed(старое/новое).
Ротация (app/services/events.py, app/services/rotation.py — новый).
rotate(): удаляет записи старше срока и, если записей больше лимита, самые старые сверх лимита (по возрастанию ID — ID сортируется по времени); пакетами, чтобы не блокировать БД; если что-то удалено — одна записьjournal.rotated(сколько, по какой причине, действующие настройки).- Запуск: при старте, сразу после сохранения настроек и раз в час фоновой задачей в
lifespan(app/main.py, рядом с опросом; акторsystem).
Очистка (events.clear(user); маршрут только в UI).
POST /events/clear(нужна сессия): пароль сверяется с паролем пользователя сессии (security.verify_admin_password,hmac.compare_digest; логин берётся из сессии, а не из формы). Неверный пароль → окно остаётся открытым с ошибкой, событиеjournal.clear_denied(без пароля), счётчик попыток; после 5 неверных за 10 минут — блокировка на 10 минут (в памяти, по пользователю) с сообщением о времени ожидания.- Успех: в одной транзакции удаляются все записи и добавляется
journal.cleared(кто, сколько удалено); окно закрывается, страница обновляется (HX-Refresh). - Через API очистки нет (только чтение журнала и настроек) — иначе токен обходил бы окно с паролем; прямой POST без пароля отклоняется.
UI (app/ui/routes.py, шаблоны events.html, _events_rows.html, _event_dialog.html, _events_settings.html, _events_clear.html, base.html — пункт меню, static/style.css — только новые классы на существующих токенах, static/app.js — кнопка «Копировать»).
GET /events— страница с фильтрами (GET-форма, как «Бэкапы»: тип/группа типов, актор, устройство, период, поиск; закладки), «Показать ещё» — HTMX-подгрузка следующих 100 по курсоруbefore=<evt_…>(тот жеevents.list_events, к нему добавляются фильтры по актору, периоду и тексту).- Окна через существующую инфраструктуру
<dialog>+ HTMX:/ui/dialog/event/{id},/ui/dialog/events-settings,/ui/dialog/events-clear; ошибки — внутри окна, успех —HX-Refresh(как у окон устройства/групп). - Кнопка «Очистить» в окне неактивна, пока поле пароля пусто (JS); проверка — на сервере.
API (app/api/v1.py): GET/PUT /api/v1/events/settings (настройки ротации и сводка); фильтры actor, date_from, date_to, q в GET /api/v1/events. Очистки нет.
Документы: docs/changes/017-events-ui-rotation/{plan,summary}.md; README (раздел «Журнал», настройки .env, история).
Проверка
- Макет (этап A): публикация листов; сверка при ревью — на вашей стороне.
- Тесты (
pytest, сейчас 17; +3): (1) ротация: по возрасту, по числу,0= без ограничения, записьjournal.rotated; (2) очистка: неверный пароль не удаляет и пишетjournal.clear_denied, верный удаляет всё и оставляет однуjournal.cleared, блокировка после 5 неверных,DELETE /api/v1/eventsне существует, POST без пароля отклонён; (3) страница/events: фильтры, курсор «Показать ещё», окно записи, сохранение настроек с валидацией. - Браузер (Playwright) на копии боевой БД — второй экземпляр приложения на другом порту с копией файла БД и выключенным опросом, чтобы не стереть ваш реальный журнал: страница и фильтры, окно записи, настройки (сохранение, ошибка валидации), очистка (неверный пароль → ошибка, блокировка, верный → пустой журнал с одной записью об очистке), ротация по срокам на искусственно состаренных записях, тёмная тема и узкое окно (меню/окна не обрезаются). На боевом журнале очистку не запускаю без вашей команды.
- Регрессия: весь прежний набор тестов и сценарий интерфейса (устройства, группы, бэкапы) без ошибок.
Риски и оговорки
- Изменение настроек (уменьшение срока/лимита) сразу удаляет старые записи — это фиксируется в журнале (
journal.settings_changed,journal.rotated); пароль для смены настроек не требуется (по вашему ТЗ он нужен только для очистки). - Пароль пользователя — единственный (
ADMIN_PASSWORDиз.env); счётчик блокировки хранится в памяти процесса и сбрасывается перезапуском. - Журнал не заменяет резервное копирование БД: очистка необратима (кроме восстановления из
.bak/копии тома).