Files
ipam_control/docs/changes/003-journal-search-rotation/PLAN.md
T
ayurishchevandClaude Sonnet 5 a846d30872 IPAM Manager: API, UI-админка, журнал аудита
Backend (FastAPI, SQLAlchemy 2, Alembic, PostgreSQL 16):
- организации, VRF, префиксы (дерево, использование, автоназначение),
  адреса, операторы связи, устройства и типы устройств; JWT, роли admin/viewer;
- VRF принадлежит организации (составной FK), смена VRF у префикса
  переносит поддерево, имя VRF уникально в организации;
- журнал аудита: поиск и фильтры, ротация (срок/количество), очистка
  по паролю с блокировкой, IP клиента и метаданные запроса
  (X-Forwarded-For только от TRUSTED_PROXIES).

UI (web/, без сборки): экраны и диалоги по макетам «IPAM Manager»,
кликабельные строки реестров, локальные шрифты IBM Plex,
собственные выпадающие списки.

Окружение: docker-compose (postgres + app), миграции Alembic 0001-0004,
scripts/gen_env.py, scripts/seed_demo.py, 11 автотестов (pytest).
Документация: README.md и docs/changes/001-005 (планы и итоги).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-20 12:27:47 +03:00

75 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Журнал: поиск по событиям, ротация, очистка и 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 на очистку/настройки не входят.