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>
11 KiB
Журнал: поиск по событиям, ротация, очистка и 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(defaultgen_random_uuid(), уникальный; в UIevt_<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, т.к. по httpnavigator.clipboardнедоступен), время, тип, актор, сущность, сообщение, блок «Данные» (JSONdiff). - Окно «Настройки журнала» (560 px): текущее число записей и старейшая, поля «Хранить записи, дней» и «Максимум записей», пояснение о ротации.
- Окно «Очистить журнал» (540 px): предупреждение, поле пароля, кнопка неактивна до ввода; состояния «Неверный пароль. Осталось попыток: N» и блокировка «Повторите через N мин.» (поле отключено).
- Бейджи типов: created — синий, assigned — зелёный, updated — янтарный, deleted/auth.failed — красный, остальные — серый.
Порядок работ
- Скопировать этот план в
docs/changes/003-journal-search-rotation/PLAN.md(требование проекта). - Миграция 0003 + модели; бэкфилл
message/uid. services.audit()(message, uid) и новые события (auth.*).journal.py: список/поиск/summary/facets/запись; перенос/audit.- Настройки, ротация (
rotation.py, фоновая задача, advisory lock), очистка с блокировкой. - UI: экран, окна записи/настроек/очистки.
- Тесты, прогон сценария UI, сверка окон с макетами, README, SUMMARY.
Тесты (+3, всего 9; против контейнеров, свои учётные данные из .env)
- Поиск и фильтры: создать префикс → находится по
q(метка, начало uid), поevent_type,actor;date_fromв будущем → пусто;/audit/{uid}. - Ротация: запись с
tsстарше срока (вставка через БД по DSN из.env) и превышениеmax_entriesудаляются приPUT /journal/settings; появляетсяjournal.rotated; настройки в конце восстанавливаются к 90/100 000. - Очистка: отдельный временный пользователь (вставка в БД, удаляется после теста, чтобы не блокировать
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 на очистку/настройки не входят.