Files
ros_control/docs/changes/017-events-ui-rotation/plan.md
T
ayurishchevandClaude Sonnet 5 26dd1f8be4 Уникальные ID сущностей, журнал событий и его UI с ротацией и очисткой
Глобально уникальные 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>
2026-09-19 17:01:52 +03:00

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); существующие листы не меняются, кроме заметки на холсте о новом пункте меню.

  1. «Журнал» — верхнее меню получает пятый пункт «Журнал» (в макете показан на новых листах; в приложении добавится во все экраны). Заголовок «Журнал», подзаголовок со сводкой («N записей · старейшая … · ротация: 90 дней / 100 000»). Кнопки справа: «Обновить» (secondary), «Настройки» (secondary), «Очистить журнал» (secondary, красный текст — тот же вид «опасного» действия, что «Удалить» на бэкапах). Primary-кнопки на странице нет. Карточка: полоса фильтров (Тип ▾, Актор ▾, Устройство ▾, период с/по, поиск по сообщению или ID, «Сбросить (N)»), таблица: Время (UTC) · Тип (метка по смыслу: ok/warn/bad/run/neutral) · Сущность (вид + короткий ID) · Сообщение · Актор · ID записи (короткий, моноширинный; полный — в подсказке и в окне записи), футер «Показано N из M» и кнопка «Показать ещё» (постраничная подгрузка по курсору).
  2. «Журнал · запись» — окно с полным содержимым записи: evt_…, тип, время, актор, ID сущности, устройство, задача, сообщение, данные (JSON), кнопки «Копировать» у ID; закрывается «Закрыть».
  3. «Журнал · настройки ротации» — окно: «Хранить записи, дней» и «Максимум записей» (0 = не ограничивать), справка («ротация раз в час и при сохранении удаляет самые старые записи и фиксируется записью в журнале»), сводка «сейчас N записей, старейшая …», «Отмена» / «Сохранить» (primary).
  4. «Журнал · очистка» — два состояния рядом: первичное и «неверный пароль». Окно: предупреждение (жёлтая заметка: «будет удалено N записей без возможности восстановления; останется одна запись об очистке»), поле «Пароль пользователя <имя>» (type=password), кнопка «Очистить» (secondary, красный текст) неактивна, пока пароль не введён, «Отмена». Состояние ошибки: красное сообщение «Неверный пароль» и состояние блокировки «Слишком много попыток, повторите через N мин».
  5. Заметка на холсте: пункт «Журнал» добавляется в верхнее меню всех экранов. Результат — ссылка на артефакт и вопросы на ревью; дальше — стоп до вашего ответа.

Этап B — реализация (после одобрения макета)

Данные и настройки.

  • Таблица app_settings(key PK, value JSON, updated_at) (app/models.py, миграция v2 в app/migrations.py через create_all, user_version 1→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/копии тома).