From 5210ba3333c1f0eb06f0ec7a8fab48f56a453cf1 Mon Sep 17 00:00:00 2001 From: ayurishchev Date: Sun, 27 Sep 2026 08:44:17 +0300 Subject: [PATCH] =?UTF-8?q?README:=20=D0=B0=D0=BA=D1=82=D1=83=D0=B0=D0=BB?= =?UTF-8?q?=D0=B8=D0=B7=D0=B0=D1=86=D0=B8=D1=8F=20=D0=B8=20=D0=BE=D0=BF?= =?UTF-8?q?=D1=82=D0=B8=D0=BC=D0=B8=D0=B7=D0=B0=D1=86=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Текст описывает текущее состояние проекта (конфигурация, архитектура, правила данных, API, безопасность, эксплуатация, UI, тесты) без описаний отдельных доработок; история изменений 001-030 и отчёты ревью вынесены в таблицы со ссылками на docs/changes и docs/reviews. Исправлены устаревшие сведения: структура app/, миграции 0001-0009, команда тестов с проектом ipam_control_006, срок жизни токена. Co-Authored-By: Claude Opus 5.5 --- README.md | 243 +++++++++++++++++++++++++++++++----------------------- 1 file changed, 141 insertions(+), 102 deletions(-) diff --git a/README.md b/README.md index 18afdaa..4f98aaa 100644 --- a/README.md +++ b/README.md @@ -1,134 +1,173 @@ # IPAM Manager -Реестр IP-адресов и адресных префиксов в разрезе организаций. API-first: backend на Python (FastAPI) с PostgreSQL, -UI-админка — отдельный лёгкий SPA (`web/`, без сборки), который только визуализирует ответы API. -Макеты: страница «IPAM Manager» дизайн-канваса ros_control. +Реестр IP-адресов и адресных префиксов в разрезе организаций. Проект построен по принципу API-first: backend на FastAPI и PostgreSQL, +UI-админка — лёгкий SPA без сборки (`web/`), который только отображает ответы API. Макеты — страница «IPAM Manager» дизайн-канваса ros_control. ## Быстрый старт ```bash -python3 scripts/gen_env.py # .env со случайными паролями/секретами (в .gitignore) -docker compose up -d --build # postgres + app; миграции применяются автоматически +python3 scripts/gen_env.py # .env со случайными паролями и секретами (файл в .gitignore) +docker compose up -d --build # postgres + app; миграции применяются при старте python3 -m venv venv && venv/bin/pip install -r requirements-dev.txt -venv/bin/python scripts/seed_demo.py # (по желанию) демо-данные из макетов +venv/bin/python scripts/seed_demo.py # по желанию: демо-данные из макетов ``` -- UI: http://127.0.0.1:8088/ · Swagger: http://127.0.0.1:8088/docs (порт приложения — `APP_PORT` в `.env`, слушает 0.0.0.0; порт БД — только 127.0.0.1) -- Логин/пароль администратора — `ADMIN_USERNAME` / `ADMIN_PASSWORD` из `.env` (создаётся при первом старте; пароль не короче 8 символов). -- `JWT_SECRET` обязателен и не короче 32 символов (заглушки отклоняются): без него приложение не стартует (изменение 017). `scripts/gen_env.py` генерирует корректные значения. -- Порт приложения публикуется на `${APP_BIND:-0.0.0.0}` (в `.env` можно задать `APP_BIND=127.0.0.1` — только loopback, за reverse-proxy с TLS, см. «Публикация»). +- UI: `http://<хост>:8088/`, Swagger: `http://<хост>:8088/docs`. +- Вход: `ADMIN_USERNAME` / `ADMIN_PASSWORD` из `.env`. Администратор создаётся при первом старте на пустой БД. + +## Конфигурация (`.env`) +| Переменная | Назначение | +|---|---| +| `POSTGRES_DB`, `POSTGRES_USER`, `POSTGRES_PASSWORD` | База данных | +| `JWT_SECRET` | Ключ подписи токенов. Обязателен, не короче 32 символов, заглушки отклоняются: без корректного значения приложение не стартует | +| `ADMIN_USERNAME`, `ADMIN_PASSWORD` | Первый администратор; пароль не короче 8 символов | +| `APP_PORT` | Порт UI и API на хосте (8088) | +| `APP_BIND` | Адрес публикации порта: по умолчанию `0.0.0.0`; `127.0.0.1` — только за reverse-proxy | +| `DB_HOST_PORT` | Порт PostgreSQL на хосте, публикуется только на `127.0.0.1` | +| `TRUSTED_PROXIES` | CIDR доверенных прокси через запятую; только от них принимается `X-Forwarded-For` | ## Архитектура | Слой | Технологии | |---|---| -| API | FastAPI, pydantic v2, JWT (argon2), роли `admin` (запись) / `viewer` (чтение) | -| БД | PostgreSQL 16, SQLAlchemy 2, Alembic (`alembic/versions`), типы `CIDR`/`INET` | -| UI | статический SPA (ES-модуль, vanilla JS), раздаётся приложением; шрифты IBM Plex — локально в `web/fonts/` (woff2 latin+cyrillic, лицензия OFL), внешних зависимостей нет | +| API | FastAPI, pydantic v2, JWT (срок 8 ч), пароли в argon2, роли `admin` (запись) и `viewer` (чтение) | +| БД | PostgreSQL 16, SQLAlchemy 2, Alembic (миграции `0001`–`0009`), типы `CIDR`/`INET` | +| UI | Статический SPA (vanilla JS, ES-модуль) раздаётся приложением; шрифты IBM Plex хранятся локально, внешних зависимостей нет | ``` -app/ main.py config.py db.py security.py models.py schemas.py services.py api/v1/{auth,refs,prefixes,overview}.py -web/ index.html styles.css app.js -alembic/ scripts/{gen_env,seed_demo,find_duplicate_addresses,find_unusable_addresses}.py requirements.lock tests/ docs/changes/ docs/reviews/ +app/ main.py config.py db.py security.py models.py schemas.py services.py request_context.py rotation.py +app/api/v1/ auth.py refs.py prefixes.py overview.py journal.py users.py +web/ index.html styles.css app.js fonts/ +alembic/ миграции схемы +scripts/ gen_env.py seed_demo.py find_duplicate_addresses.py find_unusable_addresses.py +tests/ автотесты (pytest) +docs/changes/ планы и итоги доработок docs/reviews/ отчёты ревью ``` -## Модель данных -`organizations` → `vrfs` (по организации, «default» создаётся автоматически) → `prefixes` (дерево через `parent_id`, -вложенность определяется автоматически) → `addresses`; `devices` + `device_types`; `isps` + `isp_networks`; `users`; `audit_log`. -- Ёмкость префикса — размер его собственной подсети (IPv4 без сетевого/broadcast), у листа и у родителя одинаково: после частичного разбиения - (автовыделение, изменение 010) ёмкость родителя не падает до суммы вложенных (изменение 025). `used` считается по адресам всего поддерева того же VRF. -- «Обзор»: `capacity` — сумма ёмкостей «корневых» активных IPv4-префиксов (не вложенных ни в один другой активный IPv4-префикс того же VRF по CIDR, - а не по `parent_id`, чтобы неактивные ветки не влияли на корни); `assigned`/`reserved` — IPv4-адреса этих статусов, лежащие внутри какого-либо корня - (адреса в неактивных ветках не учитываются). «Высокая загрузка» — как раньше, по листовым IPv4-префиксам с ёмкостью > 1 (изменение 025). -- «Свободные» адреса не хранятся, а вычисляются; в списке адресов они показываются для подсетей до /20. Страница `status=free` считается арифметически - (без перебора адресов, `offset` ≤ 10 000 000), остальные фильтры и постраничная выдача — в SQL (изменение 014). -- **IP уникален в пределах VRF и хранится в самом узком префиксе** (изменение 011): `addresses.vrf_id` + составной FK `(prefix_id, vrf_id)` (обновляется каскадом при переносе VRF), - уникальность `(vrf_id, address)`. Адрес из диапазона вложенного префикса в родителе назначить нельзя (422), дубль в VRF → 409; новый вложенный префикс забирает адреса родителя - из своего диапазона (`moved_addresses` в журнале); перенос префикса в VRF с тем же адресом → 409. Миграция 0007 останавливается при дублях — найти: `scripts/find_duplicate_addresses.py`. -- Адрес сети и broadcast (IPv4, префикс ≤ /30) назначить нельзя (422, изменение 015); уже внесённые найдёт `scripts/find_unusable_addresses.py`; загрузка не превышает 100 %. - Перенос адресов при создании вложенного префикса или смене VRF, из-за которого адрес стал бы сетевым/broadcast в новом месте, тоже отклоняется 422 с перечислением адресов и целевых - префиксов (изменение 024): создание префикса откатывается целиком, смена VRF — без частичных изменений. -- Обзор считает использование только по IPv4. -- VRF — часть адресного плана организации: имя уникально **в пределах организации** (без учёта регистра), в разных организациях - имена могут совпадать; в одном VRF может быть много префиксов. Принадлежность VRF организации префикса гарантирует составной FK в БД. -- Смена VRF у префикса (`PATCH /prefixes/{id}` с `vrf_id`) — только среди VRF той же организации; переносится префикс вместе с вложенными, - дубль CIDR в целевом VRF → 409 (без частичных изменений), VRF другой организации → 422. -- Автовыделение вложенного префикса (изменение 010): `POST /prefixes/{id}/subnets/next` с `length` (например 30) создаёт дочерний префикс в первом свободном выровненном блоке родителя (учитываются вложенные префиксы - и адреса родителя); `GET` с тем же путём и `?length=` — предпросмотр. Нет места → 409, недопустимый размер → 422. В UI — пункт «Добавить вложенный (авто)» в меню «⋯» префикса. -- VRF, тип устройства, организация с зависимыми объектами не удаляются (409). У организации без префиксов, устройств и операторов - служебный VRF `default` удаляется вместе с ней (исправлено в изменении 007: раньше такое удаление давало 409). -- Изменения дерева префиксов и раскладки адресов одного VRF (создание/удаление/перенос префикса, автовыделение, назначение адреса) выполняются по одному — - `pg_advisory_xact_lock` по `vrf_id` берётся до чтения дерева; разные VRF друг друга не блокируют, чтения (`GET`) не блокируются (изменение 029, находка №5). +## Модель данных и правила +`organizations` → `vrfs` → `prefixes` (дерево) → `addresses`; `devices` + `device_types`; `isps` + `isp_networks`; `users`; `audit_log`. + +**VRF и префиксы** +- У каждой организации автоматически создаётся VRF `default`. Имя VRF уникально в пределах организации без учёта регистра. +- Префикс принадлежит VRF своей организации; это гарантирует составной FK в БД. +- Вложенность префиксов (`parent_id`) определяется автоматически по CIDR. Смена VRF переносит префикс вместе с вложенными, только в пределах организации. +- Автовыделение вложенного префикса: система выбирает первый свободный выровненный блок заданного размера. +- Изменения дерева одного VRF выполняются по одному (advisory-lock); разные VRF друг друга не блокируют. + +**Адреса** +- IP уникален в пределах VRF и хранится в самом узком содержащем его префиксе. +- Адрес сети и broadcast (IPv4, префикс ≤ /30) назначить нельзя. +- Свободные адреса не хранятся, а вычисляются. В общем списке они показываются только для подсетей до /20. + +**Ёмкость и «Обзор»** +- Ёмкость префикса — размер его подсети (для IPv4 без адреса сети и broadcast). Занятость считается по всему поддереву. +- «Обзор» учитывает только IPv4: ёмкость — сумма корневых активных префиксов, назначенные адреса — адреса внутри них. + +**Удаление** +- Объекты с зависимыми данными не удаляются (409). Отказ фиксируется в журнале с перечнем мешающих объектов. ## API (`/api/v1`) -`POST /auth/login` · `GET /auth/me` · `GET /overview` -CRUD: `/organizations`, `/vrfs`, `/isps`, `/device-types`, `/devices`, `/prefixes`, `/addresses/{id}`, `/users` -Адреса префикса: `GET|POST /prefixes/{id}/addresses` (`status`, `q`, `limit`, `offset`), `POST …/addresses/next` — автоназначение из пула -(первый свободный адрес вне вложенных префиксов, без адреса сети/broadcast; префикс блокируется на время выдачи — изменение 020). -Пагинация во всех списках: `limit` ≥ 1 (и не больше предела эндпоинта), `offset` от 0 до 10 000 000; иначе 422 (изменение 013). -`PATCH`: `null` в текстовом поле очищает его (пустая строка), `null` в `status` адреса → 422, `device_id: null` отвязывает устройство (изменение 018). -`PATCH /devices/{id}` проверяет `name` (hostname/FQDN) и `mac` (формат, нормализация к `AA:BB:CC:DD:EE:FF`) так же, как создание — раньше PATCH пропускал некорректные значения (изменение 024). -Поиск (`q`) экранирует `%` и `_`. Ответы содержат заголовки `Content-Security-Policy` (кроме `/docs`), `X-Content-Type-Options`, `X-Frame-Options`, `Referrer-Policy` (изменение 023). -Ошибки: `{code, message, fields}` (для блокировок/лимитов — дополнительные поля `attempts_left`, `retry_after_seconds`). Каждое изменение пишется в `audit_log`. +| Область | Эндпоинты | +|---|---| +| Вход | `POST /auth/login`, `GET /auth/me` | +| Справочники | `/organizations`, `/vrfs`, `/isps`, `/device-types`, `/devices` | +| Префиксы | `/prefixes`, `GET\|POST /prefixes/{id}/subnets/next` (предпросмотр и автовыделение вложенного) | +| Адреса | `GET\|POST /prefixes/{id}/addresses`, `POST /prefixes/{id}/addresses/next` (автоназначение из пула), `PATCH\|DELETE /addresses/{id}` | +| Пользователи | `/users`, `POST /users/me/password` | +| Журнал | `GET /audit`, `/audit/summary`, `/audit/facets`, `/audit/{uid}`, `GET\|PUT /journal/settings`, `POST /journal/clear` | +| Сводка | `GET /overview` | -### Пользователи и роли -- `GET|POST /users`, `PATCH|DELETE /users/{id}` — управление учётными записями, только `admin`: логин (3–100 символов, латиница, - цифры, `. _ -`), роль, доступ и пароль (при сбросе администратором). Логин после создания не меняется — он же `sub` в токене. -- `POST /users/me/password {current_password, new_password}` — смена своего пароля, доступна любой роли; неверный текущий пароль → 403. Ответ содержит новый `access_token`: - прежние токены пользователя после смены пароля (самим или администратором) недействительны (изменение 023, claim `pv`). -- `PATCH /users/{id}` с `password` для **своей же** учётной записи → 422: свой пароль меняется только через `/users/me/password` (там обязательно подтверждение текущего пароля; изменение 024). -- Занятый логин (в том числе в другом регистре) → 409; служебные логины `system` и `anonymous`, короткий логин или пароль → 422. -- Свою учётную запись нельзя понизить, отключить или удалить, как и последнего активного администратора → 409. -- Отключение действует немедленно (токен проверяется по `users.is_active` на каждом запросе). Роль `viewer` видит реестр и журнал, но любые изменения получает с 403. -- Роль по умолчанию при создании — `viewer` (изменение 016). Логин уникален без учёта регистра и в БД (изменение 021); снятие прав администратора и удаление пользователей идут под advisory-lock. -- Изменения пишутся в журнал: `user.created`, `user.updated`, `user.password_reset`, `user.deleted` (значения паролей не сохраняются). +**Соглашения** +- Ошибки возвращаются в формате `{code, message, fields}`; для лимитов добавляются `retry_after_seconds` и `attempts_left`. +- Пагинация: `limit` ≥ 1, `offset` от 0 до 10 000 000. +- `null` в текстовом поле `PATCH` очищает его. +- Каждое изменение данных пишется в журнал аудита. -### Журнал -- Отказ в удалении по бизнес-правилу (409: организация, VRF, тип устройства, префикс с адресами, пользователь) фиксируется событием `<сущность>.delete_blocked` (изменение 008); - в «Данных» записи — `reason` и `blocked_by` со списками мешающих объектов (`total` и до 20 названий: префиксы, устройства, операторы, адреса). -- `GET /audit` — поиск и фильтры: `q` (сообщение, метка объекта, начало ID записи), `event_type` (`prefix.created`), `entity_type`, `actor` (`ui:admin`, `system`, `anonymous`), `date_from`/`date_to` (UTC), `limit`/`offset`; `GET /audit/summary`, `/audit/facets`, `/audit/{uid}`. -- `GET|PUT /journal/settings` — ротация: `retention_days` (по умолчанию 90) и `max_entries` (100 000), `0` — без ограничения. Ротация идёт раз в час и сразу при сохранении настроек - (advisory-lock защищает от параллельного запуска); каждая ротация с удалениями фиксируется записью `journal.rotated`. Запись — только admin. -- `POST /journal/clear {password}` — очистка с подтверждением пароля текущего пользователя (admin); 5 неверных попыток за 10 минут → блокировка на 10 минут (429). В журнале остаётся запись `journal.cleared`. -- **IP и метаданные запроса:** каждая запись, созданная в рамках HTTP-запроса, хранит `client_ip` и `meta` (`user_agent`, `method`, `path`, `request_id`; ответ содержит `X-Request-ID`); - системные события (ротация) — без IP. Фильтр `GET /audit?client_ip=` принимает IP или подсеть (`192.168.5.0/24`), текстовый поиск `q` ищет и по началу IP. - IP берётся из адреса сокета. `X-Forwarded-For` учитывается только от прокси из `TRUSTED_PROXIES` (CIDR через запятую в `.env`, по умолчанию пусто) — иначе IP можно подделать. - Запросы с самой машины через `127.0.0.1` Docker показывает адресом шлюза сети (`172.x.0.1`); с LAN-адреса и удалённых хостов виден реальный источник. -- В журнал пишутся также входы (`session.login`, `session.failed` — актор `anonymous`, только первая неудача в окне **по этому логину**; `session.locked` — блокировка, `diff.distinct_logins` — - число разных логинов с этого IP в окне, если сработал лимит по IP) и служебные события (`journal.*`, актор `system`; неверный пароль очистки — `journal.clear_failed` / `journal.clear_locked`). - Удаление префикса с `force=true` фиксирует `addresses_deleted`. -- **Вход:** три области лимита за 10 минут (блокировка — окно после последней неудачи): **логин + IP** — 5 неудач блокирует эту пару; **IP** — 20 неудач блокирует - любые логины с этого IP (как раньше); **логин** — 50 неудач со всех IP блокирует логин, но **не для «известных» IP** — тех, с которых этот пользователь уже успешно - входил за последние 30 дней (`known_logins`, обновляется при каждом успешном входе; устаревшие записи удаляет ротация). Так анонимный клиент, знающий логин - (например, `admin`), не может держать пользователя заблокированным с его обычного рабочего места — только с незнакомых IP (изменение 026, находка №2 ревью). 429 содержит - `Retry-After`/`retry_after_seconds`; для несуществующего логина время ответа выравнивается, пароль ≤ 128 символов. - Попытки одного логина сериализованы (`pg_advisory_xact_lock`, изменение 024), затем — попытки одного IP по всем логинам (изменение 027, находка №7: без этого перебор разных - логинов с одного IP проходит проверку лимита по IP параллельно и превышает его на степень параллелизма); порядок всегда «логин, затем IP», взаимная блокировка исключена. - Цена — попытки с одного IP (в том числе за NAT) обрабатываются по одной, время ответа при массовом переборе растёт на время проверки пароля. - При ротации по количеству первыми удаляются `session.failed` и `session.locked`. За reverse-proxy без `TRUSTED_PROXIES` все клиенты делят один IP — лимит по IP заденет всех. +## Безопасность +**Роли и учётные записи** +- `viewer` только читает. Раздел «Пользователи» доступен только `admin`. Роль по умолчанию — `viewer`. +- Логин уникален без учёта регистра. +- Свою учётную запись нельзя понизить, отключить или удалить; то же относится к последнему активному администратору. + +**Пароли и токены** +- Смена пароля отзывает ранее выданные токены. Свой пароль меняется только с подтверждением текущего. + +**Вход** +- Лимит неудачных попыток за 10 минут: + - 5 — на пару логин + IP; + - 20 — на IP; + - 50 — на логин со всех IP, кроме тех, с которых пользователь успешно входил за 30 дней. +- Сверх лимита — 429 с `Retry-After`. + +**Журнал** +- Хранит IP клиента и метаданные запроса. +- Ротация по сроку (90 дней) и по количеству записей (100 000); значения настраиваются. +- Очистка журнала требует пароль. + +**Ответы** +- Заголовки CSP, `X-Frame-Options`, `X-Content-Type-Options`, `Referrer-Policy`. ## Публикация и эксплуатация -- Контейнер приложения работает от непривилегированного пользователя (uid 10001), у сервиса есть healthcheck (`/healthz`), сервисы перезапускаются (`restart: unless-stopped`). -- Миграции при старте выполняются под advisory-lock — параллельные реплики не гоняют их одновременно. Зависимости зафиксированы в `requirements.lock` +- Контейнер приложения работает от непривилегированного пользователя, у него есть healthcheck (`/healthz`) и перезапуск `unless-stopped`. +- Миграции выполняются при старте под advisory-lock. Зависимости зафиксированы в `requirements.lock` (обновление: `venv/bin/pip-compile --strip-extras -o requirements.lock requirements.txt`). -- TLS: приложение отдаёт HTTP; для эксплуатации поставьте reverse-proxy (пример для Caddy) и задайте `APP_BIND=127.0.0.1`, `TRUSTED_PROXIES=<адрес прокси/сеть Docker>`: +- Приложение отдаёт HTTP. Для TLS поставьте reverse-proxy и задайте `APP_BIND=127.0.0.1` и `TRUSTED_PROXIES`. Пример для Caddy: ``` ipam.example.com { reverse_proxy 127.0.0.1:8088 } ``` +- Перед обновлением рабочей БД проверьте данные скриптами только для чтения: `scripts/find_duplicate_addresses.py` и `scripts/find_unusable_addresses.py`. +- Имя compose-проекта задаётся флагом `-p`. Текущий стенд поднят как `ipam_control_006` (`docker compose -p ipam_control_006 …`); без флага команды работают с проектом `ipam_control`. -## Поведение таблиц UI -Администратор может выбирать строки чекбоксами (в шапке — «выбрать все») на экранах «Организации», «Операторы», «Устройства», «Префиксы» (листовые), «Адреса» (кроме «Свободен») и «Пользователи» (кроме себя); -в «Журнале» выбора нет. Панель над таблицей: «Удалить» везде, «Сменить тип» (устройства), «Статус» (префиксы, адреса), «Разрешить/Отключить доступ» (пользователи). Операции идут по одному запросу -на объект (изменение 009): итог «выполнено N из M», отказы (зависимые объекты, свой аккаунт, последний администратор) показаны списком с причиной, попадают в журнал как `*.delete_blocked`, -и остаются выбранными. Префиксы удаляются без `force`: префикс с адресами удаляется из одиночного меню строки. -Строка реестра кликабельна целиком (как в журнале): «Префиксы» — лист открывает адреса подсети, родитель сворачивает/разворачивает ветку; «Организации» — префиксы организации; -«Операторы», «Устройства» и «Адреса» — окно редактирования (свободный адрес — «Назначить адрес» с этим IP). Ссылки, шеврон, меню «⋯» работают как раньше и не запускают действие строки; -Ctrl/Shift+клик и выделение текста тоже игнорируются. -Экран «Пользователи» доступен только администратору: создание, редактирование роли и доступа, удаление на месте, а свой пароль меняется кнопкой «Сменить пароль» в шапке. -Экраны «Префиксы» и «Адреса» догружают все страницы `/prefixes` организации (не только первые 1000), но не больше `PREFIX_UI_CAP` (20 000) префиксов; заголовок, -вкладка «Все» и подвал показывают `total`, а при срабатывании предела над таблицей появляется строка «Загружено X из N…» (изменение 028, находка №3). +## Интерфейс +- Экраны: «Обзор», «Префиксы» (дерево по VRF), «Адреса» подсети, «Организации», «Операторы», «Устройства», «Журнал», «Пользователи» (только admin). +- Строка реестра кликабельна целиком. Действия над строкой — в меню «⋯». +- Групповые операции через чекбоксы (кроме «Журнала»): удаление, смена типа устройств, статус префиксов и адресов, доступ пользователей. +- Экран «Префиксы» загружает до 20 000 префиксов организации и сообщает, если загружены не все. ## Тесты -Идут против приложения в контейнерах, учётные данные берутся из `.env`: +Тесты работают с приложением и БД, заданными в `.env`, то есть с запущенным стендом: они создают и удаляют временные данные. ```bash -docker compose up -d --build && venv/bin/python -m pytest -q +docker compose -p ipam_control_006 up -d --build && venv/bin/python -m pytest -q ``` + +## История изменений +Каждая доработка описана в `docs/changes/<номер>/`: `PLAN.md` — план, `SUMMARY.md` — итог. + +| № | Изменение | Документы | +|---|---|---| +| 001 | Backend и UI-админка | [план](docs/changes/001-ipam-backend/PLAN.md) · [итог](docs/changes/001-ipam-backend/SUMMARY.md) | +| 002 | Смена VRF у префикса, целостность «VRF ⊂ организация» | [план](docs/changes/002-prefix-vrf-change/PLAN.md) · [итог](docs/changes/002-prefix-vrf-change/SUMMARY.md) | +| 003 | Журнал: поиск, ротация, очистка | [план](docs/changes/003-journal-search-rotation/PLAN.md) · [итог](docs/changes/003-journal-search-rotation/SUMMARY.md) | +| 004 | Журнал: IP клиента и метаданные запроса | [план](docs/changes/004-audit-client-ip/PLAN.md) · [итог](docs/changes/004-audit-client-ip/SUMMARY.md) | +| 005 | Кликабельные строки реестров | [план](docs/changes/005-clickable-rows/PLAN.md) · [итог](docs/changes/005-clickable-rows/SUMMARY.md) | +| 006 | Раздел «Пользователи» | [план](docs/changes/006-users-management/PLAN.md) · [итог](docs/changes/006-users-management/SUMMARY.md) | +| 007 | Исправление удаления организации | [план](docs/changes/007-org-delete-fix/PLAN.md) · [итог](docs/changes/007-org-delete-fix/SUMMARY.md) | +| 008 | Журнал отклонённых удалений | [план](docs/changes/008-blocked-delete-audit/PLAN.md) · [итог](docs/changes/008-blocked-delete-audit/SUMMARY.md) | +| 009 | Групповые операции в UI | [план](docs/changes/009-bulk-actions/PLAN.md) · [итог](docs/changes/009-bulk-actions/SUMMARY.md) | +| 010 | Автовыделение вложенного префикса | [план](docs/changes/010-next-free-prefix/PLAN.md) · [итог](docs/changes/010-next-free-prefix/SUMMARY.md) | +| 011 | Уникальность IP в VRF | [план](docs/changes/011-address-unique-in-vrf/PLAN.md) · [итог](docs/changes/011-address-unique-in-vrf/SUMMARY.md) | +| 012 | Ограничение попыток входа | [план](docs/changes/012-login-rate-limit/PLAN.md) · [итог](docs/changes/012-login-rate-limit/SUMMARY.md) | +| 013 | Границы пагинации | [план](docs/changes/013-pagination-bounds/PLAN.md) · [итог](docs/changes/013-pagination-bounds/SUMMARY.md) | +| 014 | Производительность экрана адресов | [план](docs/changes/014-addresses-listing-performance/PLAN.md) · [итог](docs/changes/014-addresses-listing-performance/SUMMARY.md) | +| 015 | Запрет адреса сети и broadcast | [план](docs/changes/015-network-broadcast-addresses/PLAN.md) · [итог](docs/changes/015-network-broadcast-addresses/SUMMARY.md) | +| 016 | Роль по умолчанию — «Просмотр» | [план](docs/changes/016-default-role-viewer/PLAN.md) · [итог](docs/changes/016-default-role-viewer/SUMMARY.md) | +| 017 | Проверка секретов при старте | [план](docs/changes/017-jwt-secret-validation/PLAN.md) · [итог](docs/changes/017-jwt-secret-validation/SUMMARY.md) | +| 018 | `null` в PATCH | [план](docs/changes/018-patch-null-handling/PLAN.md) · [итог](docs/changes/018-patch-null-handling/SUMMARY.md) | +| 019 | Устранение N+1 запросов | [план](docs/changes/019-n-plus-one-queries/PLAN.md) · [итог](docs/changes/019-n-plus-one-queries/SUMMARY.md) | +| 020 | Автоназначение адреса с учётом вложенных префиксов | [план](docs/changes/020-next-free-address/PLAN.md) · [итог](docs/changes/020-next-free-address/SUMMARY.md) | +| 021 | Блокировки: администраторы, регистр логина | [план](docs/changes/021-concurrency-locks/PLAN.md) · [итог](docs/changes/021-concurrency-locks/SUMMARY.md) | +| 022 | Эксплуатация: контейнер, миграции, TLS, зависимости | [план](docs/changes/022-ops-hardening/PLAN.md) · [итог](docs/changes/022-ops-hardening/SUMMARY.md) | +| 023 | Безопасность и журнал: мелкие улучшения | [план](docs/changes/023-minor-hardening/PLAN.md) · [итог](docs/changes/023-minor-hardening/SUMMARY.md) | +| 024 | Исправление находок ревью 011–023 | [план](docs/changes/024-review-fixes-011-023/PLAN.md) · [итог](docs/changes/024-review-fixes-011-023/SUMMARY.md) | +| 025 | Ёмкость частично разбитого префикса | [план](docs/changes/025-prefix-capacity/PLAN.md) · [итог](docs/changes/025-prefix-capacity/SUMMARY.md) | +| 026 | Политика блокировки входа | [план](docs/changes/026-login-lockout-policy/PLAN.md) · [итог](docs/changes/026-login-lockout-policy/SUMMARY.md) | +| 027 | Сериализация попыток входа по IP | [план](docs/changes/027-login-ip-serialization/PLAN.md) · [итог](docs/changes/027-login-ip-serialization/SUMMARY.md) | +| 028 | Экран «Префиксы» без усечения | [план](docs/changes/028-prefixes-ui-pagination/PLAN.md) · [итог](docs/changes/028-prefixes-ui-pagination/SUMMARY.md) | +| 029 | Целостность дерева префиксов | [план](docs/changes/029-prefix-tree-lock/PLAN.md) · [итог](docs/changes/029-prefix-tree-lock/SUMMARY.md) | +| 030 | Исправление замечаний ревью 025–029 | [план](docs/changes/030-review-fixes-025-029/PLAN.md) · [итог](docs/changes/030-review-fixes-025-029/SUMMARY.md) | + +## Отчёты ревью +- [Ревью кодовой базы](docs/reviews/2026-09-26-codebase-review.md) (находки → изменения 011–023) +- [Ревью изменений 011–023](docs/reviews/2026-09-26-changes-011-023-review.md) (→ 024) +- [Повторный анализ кодовой базы](docs/reviews/2026-09-26-codebase-review-2.md) (→ 025–029) +- [Ревью и тестирование изменений 025–029](docs/reviews/2026-09-26-changes-025-029-review.md) (→ 030) +- [Ревью и тестирование изменения 030](docs/reviews/2026-09-27-changes-030-review.md)