Files
ipam_control/docs/changes/003-journal-search-rotation/PLAN.md
T

74 lines
11 KiB
Markdown
Raw Normal View History

# Журнал: поиск по событиям, ротация, очистка и 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 на очистку/настройки не входят.