74 lines
11 KiB
Markdown
74 lines
11 KiB
Markdown
# Журнал: поиск по событиям, ротация, очистка и UI (изменение 003)
|
||||
|
|
|
|||
|
|
## Context
|
|||
|
|
Сейчас `audit_log` — сырая таблица (`ts, username, entity_type, entity_id, entity_label, action, diff`), экран «Журнал» — простая таблица
|
|||
|
|
без фильтров и без управления размером; журнал растёт бесконечно. В макетах канваса (страница «ROS Manager», листы Journal / JournalEntry /
|
|||
|
|
JournalSettings / JournalClear) уже нарисован целевой журнал: фильтры и поиск, окно записи, настройки ротации, очистка с паролем.
|
|||
|
|
Цель — реализовать это для IPAM (backend + UI), адаптировав поля под IPAM. Решения из блока «Журнал: вопросы для ревью» канваса берём как
|
|||
|
|
принятые: короткий ID 8 символов; «Показать ещё 100»; ротация по умолчанию 90 дней и 100 000 записей (0 — без ограничения);
|
|||
|
|
очистка по паролю пользователя, 5 неверных попыток за 10 минут → блокировка на 10 минут; настройки ротации сохраняются без пароля;
|
|||
|
|
строка таблицы целиком кликабельна и открывает окно записи; «Очистить журнал» — белая кнопка с красным текстом.
|
|||
|
|
|
|||
|
|
## Модель данных (миграция 0003)
|
|||
|
|
- `audit_log`: `+uid UUID` (default `gen_random_uuid()`, уникальный; в UI `evt_<uuid>`, короткий вид — первые 8 символов),
|
|||
|
|
`+message TEXT` (человекочитаемое: «Префикс 10.30.0.0/24 создан», для изменений — с перечнем полей); бэкфилл существующих строк.
|
|||
|
|
Тип события = `<entity_type>.<action>` (вычисляется, отдельной колонки нет). Актор = `username` для `system`/`anonymous`, иначе `ui:<username>`.
|
|||
|
|
- `app_settings(key PK, value JSONB)` — ключ `journal` = `{retention_days: 90, max_entries: 100000}` (создаётся с дефолтами).
|
|||
|
|
- `clear_attempts(id, user_id FK, ts)` — неудачные попытки подтверждения пароля при очистке (счётчик и блокировка в БД, переживают рестарт).
|
|||
|
|
- Индексы: `(entity_type, action)`; поиск по сообщению/ID — `ILIKE` (при лимите 100 000 записей достаточно; `pg_trgm` — при необходимости позже).
|
|||
|
|
|
|||
|
|
## Backend
|
|||
|
|
Новый `app/api/v1/journal.py` (перенос `/audit` из `overview.py`), `app/rotation.py`.
|
|||
|
|
- `GET /audit` — фильтры `event_type`, `entity_type`, `actor`, `date_from`, `date_to` (UTC, включительно), `q` (message / entity_label / начало uid),
|
|||
|
|
`limit`, `offset`; сортировка `id DESC`. Ответ: `items[{uid, short_id, ts, event_type, entity_type, entity_id, entity_label, actor, message, diff}]`, `total`.
|
|||
|
|
- `GET /audit/summary` — `total`, `oldest_ts`, `retention_days`, `max_entries` (для строки «1 284 записи · старейшая … · ротация …»).
|
|||
|
|
- `GET /audit/facets` — списки значений для фильтров: типы событий, акторы, сущности.
|
|||
|
|
- `GET /audit/{uid}` — запись целиком (окно записи).
|
|||
|
|
- `GET|PUT /journal/settings` — чтение всем, запись только admin; валидация: целые ≥ 0 (дни ≤ 3650, записей ≤ 10 000 000); после сохранения — немедленная ротация.
|
|||
|
|
- `POST /journal/clear {password}` — только admin: проверка пароля текущего пользователя (argon2); неверный → 403 `{attempts_left}`;
|
|||
|
|
5 неверных за 10 минут → 429 `{retry_after_seconds}`; успех: удалить все записи и в той же транзакции записать `journal.cleared` (кто, сколько удалено).
|
|||
|
|
Обработчик ошибок расширяется: `detail` может быть dict (доп. поля в тело ответа).
|
|||
|
|
- Ротация (`rotate(db)`): удаляет записи старше `retention_days` и самые старые сверх `max_entries`; при удалении >0 пишет `journal.rotated`
|
|||
|
|
(`system`, кол-во и причина). Запуск: фоновая задача раз в час (asyncio в `lifespan`, работа в потоке) и при сохранении настроек;
|
|||
|
|
защита от параллельного запуска в нескольких репликах — `pg_try_advisory_lock`.
|
|||
|
|
- Новые события в журнал: `auth.login`, `auth.failed` (`anonymous`), `journal.settings_updated`, `journal.cleared`, `journal.rotated`;
|
|||
|
|
`services.audit()` получает генерацию `message` и `uid`.
|
|||
|
|
- Обратная совместимость: поля `/audit`, используемые Обзором (`entity_label`, `action`, `username`, `ts`), сохраняются.
|
|||
|
|
|
|||
|
|
## UI (`web/app.js`, `web/styles.css`) — по макетам Journal*
|
|||
|
|
- Экран «Журнал»: заголовок + строка «N записей · старейшая <дата> · ротация: 90 дней / 100 000 записей»; кнопки «Обновить», «Настройки»,
|
|||
|
|
«Очистить журнал» (admin; для viewer скрыты/неактивны). Фильтры: «Тип», «Актор», «Сущность» (выпадающие списки в стиле меню — уже есть `filterBtn`),
|
|||
|
|
период «с … по …» (`input type=date`), поиск «Сообщение или ID» (debounce, как в остальных экранах). Таблица: Время (UTC, с секундами), Тип (бейдж),
|
|||
|
|
Сущность (тип · метка), Сообщение, Актор, ID записи (8 симв., моно); «Показать ещё 100», «Показано X из N записей».
|
|||
|
|
- Окно записи (640 px): ID с кнопкой «Копировать» (fallback через `execCommand`, т.к. по http `navigator.clipboard` недоступен), время, тип, актор, сущность, сообщение, блок «Данные» (JSON `diff`).
|
|||
|
|
- Окно «Настройки журнала» (560 px): текущее число записей и старейшая, поля «Хранить записи, дней» и «Максимум записей», пояснение о ротации.
|
|||
|
|
- Окно «Очистить журнал» (540 px): предупреждение, поле пароля, кнопка неактивна до ввода; состояния «Неверный пароль. Осталось попыток: N» и блокировка «Повторите через N мин.» (поле отключено).
|
|||
|
|
- Бейджи типов: created — синий, assigned — зелёный, updated — янтарный, deleted/auth.failed — красный, остальные — серый.
|
|||
|
|
|
|||
|
|
## Порядок работ
|
|||
|
|
0. Скопировать этот план в `docs/changes/003-journal-search-rotation/PLAN.md` (требование проекта).
|
|||
|
|
1. Миграция 0003 + модели; бэкфилл `message`/`uid`.
|
|||
|
|
2. `services.audit()` (message, uid) и новые события (`auth.*`).
|
|||
|
|
3. `journal.py`: список/поиск/summary/facets/запись; перенос `/audit`.
|
|||
|
|
4. Настройки, ротация (`rotation.py`, фоновая задача, advisory lock), очистка с блокировкой.
|
|||
|
|
5. UI: экран, окна записи/настроек/очистки.
|
|||
|
|
6. Тесты, прогон сценария UI, сверка окон с макетами, README, SUMMARY.
|
|||
|
|
|
|||
|
|
## Тесты (+3, всего 9; против контейнеров, свои учётные данные из `.env`)
|
|||
|
|
1. Поиск и фильтры: создать префикс → находится по `q` (метка, начало uid), по `event_type`, `actor`; `date_from` в будущем → пусто; `/audit/{uid}`.
|
|||
|
|
2. Ротация: запись с `ts` старше срока (вставка через БД по DSN из `.env`) и превышение `max_entries` удаляются при `PUT /journal/settings`;
|
|||
|
|
появляется `journal.rotated`; настройки в конце восстанавливаются к 90/100 000.
|
|||
|
|
3. Очистка: отдельный временный пользователь (вставка в БД, удаляется после теста, чтобы не блокировать `admin`): неверный пароль → 403 с `attempts_left`,
|
|||
|
|
5 неверных → 429; успешную очистку в общей базе не гоняем — она проверяется вручную в сценарии UI на сбрасываемой базе.
|
|||
|
|
|
|||
|
|
## Проверка end-to-end
|
|||
|
|
`docker compose up -d --build` (миграции автоматически) → `pytest` → сценарий в браузере: создать/изменить объекты → найти событие по тексту,
|
|||
|
|
типу, актору и периоду → открыть запись и скопировать ID → сменить ротацию на малый лимит и увидеть усечение и запись `journal.rotated` →
|
|||
|
|
очистка: неверный пароль (счётчик попыток), блокировка, успешная очистка → в журнале одна запись `journal.cleared`; после этого база пересоздаётся и заполняется `seed_demo.py`.
|
|||
|
|
Сверка окон записи/настроек/очистки с макетами Journal* (отличия шапки и названий — из-за другого приложения, ROS Manager).
|
|||
|
|
|
|||
|
|
## Допущения (скажите, если не так)
|
|||
|
|
- Запись `journal.rotated` создаётся только когда что-то удалено (иначе каждый час по записи).
|
|||
|
|
- Смена регистра/формата: поиск `q` без учёта регистра, подстрока в сообщении и метке, префикс для ID.
|
|||
|
|
- Записи `auth.failed` не ограничиваются отдельно; рост от подбора паролей сдерживает ротация.
|
|||
|
|
- Экспорт журнала (CSV) и права viewer на очистку/настройки не входят.
|