# Журнал: поиск по событиям, ротация, очистка и 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_`, короткий вид — первые 8 символов), `+message TEXT` (человекочитаемое: «Префикс 10.30.0.0/24 создан», для изменений — с перечнем полей); бэкфилл существующих строк. Тип события = `.` (вычисляется, отдельной колонки нет). Актор = `username` для `system`/`anonymous`, иначе `ui:`. - `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 на очистку/настройки не входят.