Пункт 18 ревью 2026-09-28 21:32 (docs/changes/027): после отката ROS
плата остаётся на более новой прошивке, а приложение сравнивало версии
на неравенство и показывало откат как обновление.
- devices.fw_state: unknown/update/downgrade/current; has_fw_update и
фильтры не считают откат обновлением; метка «в ROS: X».
- upgrade_firmware не понижает прошивку; общая запись — _flash_firmware.
- Задача fw_downgrade: проверка → обязательный бэкап → запись →
перезагрузка; API POST /devices/{id}/firmware/downgrade и
/batch/fw_downgrade (target_version обязателен).
- UI: «Откатить прошивку…» (строка и группа), общее окно отката с kind.
Тесты: 39 из 39. Стенд: временное недоступное устройство — 422/202,
задачи failed на проверке, бэкапов нет. Реальная запись прошивки при
откате и ручная проверка UI не выполнялись.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
227 lines
29 KiB
Markdown
227 lines
29 KiB
Markdown
# ros_control
|
||
|
||
Централизованное управление парком MikroTik RouterOS: статус и мониторинг устройств, бэкапы в S3, обновление и откат ROS,
|
||
обновление прошивки, группы устройств и журнал событий. Backend на FastAPI и SQLite, WEB UI (Jinja2 + HTMX) и JSON API используют
|
||
один сервисный слой. Дизайн и требования — `ros_control.svg`, утверждённый макет интерфейса — [изменение 006](docs/changes/006-ui-redesign/plan.md).
|
||
|
||
```
|
||
Admin Dashboard ⇄ Control Server ⇄ RouterOS REST (на каждом устройстве)
|
||
⇅ ⇅
|
||
Metadata DB S3 (Yandex Object Storage)
|
||
```
|
||
|
||
## Быстрый старт
|
||
```bash
|
||
cp .env.example .env # заполнить SECRET_KEY, SESSION_SECRET, ADMIN_PASSWORD, API_TOKEN, S3_* (см. «Конфигурация»)
|
||
docker compose up -d --build # БД — в томе ros_data, схема обновляется при старте
|
||
```
|
||
- UI: `http://<хост>:${APP_PORT:-8000}/`, OpenAPI: `http://<хост>:${APP_PORT:-8000}/docs`.
|
||
- Вход в UI: `ADMIN_USER` / `ADMIN_PASSWORD` из `.env`; API — заголовок `Authorization: Bearer <API_TOKEN>`.
|
||
- Генерация секретов: `python -c "import secrets; print(secrets.token_urlsafe(32))"`;
|
||
ключ Fernet: `python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"`.
|
||
|
||
## Конфигурация (`.env`)
|
||
| Переменная | По умолчанию | Назначение |
|
||
|---|---|---|
|
||
| `APP_PORT` | `8000` | Порт хоста, публикуемый `docker-compose.yml` (читает только compose, не приложение) |
|
||
| `APP_BIND` | `0.0.0.0` | Адрес публикации порта; `127.0.0.1` — только за reverse-proxy на том же хосте |
|
||
| `SECRET_KEY` | — | Ключ Fernet для паролей устройств. Обязателен; потеря ключа — потеря доступа к сохранённым паролям |
|
||
| `SESSION_SECRET` | — | Подпись cookie-сессии UI. Обязателен, ≥ 32 символов |
|
||
| `SESSION_COOKIE_SECURE` | `false` | Флаг Secure у cookie сессии; включить, когда приложение работает за TLS |
|
||
| `ADMIN_USER`, `ADMIN_PASSWORD` | `admin`, — | Вход в UI. Пароль обязателен, ≥ 12 символов |
|
||
| `API_TOKEN` | — | Bearer-токен API. Обязателен, ≥ 32 символов |
|
||
| `TRUSTED_PROXIES` | пусто | Доверенные прокси (CIDR через запятую). Только от них принимается `X-Forwarded-For` для IP клиента; пусто — заголовок всегда игнорируется |
|
||
| `DATABASE_URL` | `sqlite:///./data/ros_control.db` | База метаданных (в compose — том `ros_data`) |
|
||
| `S3_ENDPOINT`, `S3_REGION`, `S3_BUCKET`, `S3_ACCESS_KEY`, `S3_SECRET_KEY`, `S3_PREFIX` | Yandex Object Storage, `backups` | Бакет резервных копий |
|
||
| `BACKUPS_CACHE_TTL` | `60` | Время жизни кэша списка бакета, с; `0` — без кэша |
|
||
| `MAX_CONCURRENCY` | `10` | Одновременные обращения к устройствам |
|
||
| `ROS_CONNECT_TIMEOUT` | `4` | Установление соединения, с — скорость обнаружения недоступности |
|
||
| `ROS_TIMEOUT` | `30` | Ответ устройства, с — долгие операции |
|
||
| `POLL_INTERVAL` | `30` | Период фонового опроса, с; `0` — выключить |
|
||
| `UPDATE_CHECK_INTERVAL` | `1800` | Как часто опрос проверяет обновления ROS, с |
|
||
| `EVENTS_RETENTION_DAYS`, `EVENTS_MAX_ROWS` | `90`, `100000` | Ротация журнала по умолчанию; действующие значения меняются в UI; `0` — без ограничения |
|
||
|
||
Пустые, равные `change-me` или слишком короткие секреты и невалидный `SECRET_KEY` — отказ старта (`RuntimeError` без значений секретов).
|
||
|
||
## Архитектура
|
||
| Слой | Технологии |
|
||
|---|---|
|
||
| API | FastAPI, pydantic v2, Bearer-токен; долгие операции — фоновые задачи с ограничением параллелизма |
|
||
| UI | Jinja2 + HTMX, ванильный JS без сборки; сессия в cookie; шрифты IBM Plex локально (OFL), внешних ресурсов нет |
|
||
| БД | SQLite (WAL), SQLAlchemy 2; версия схемы — `PRAGMA user_version`, миграции в `app/migrations.py` |
|
||
| Устройства и S3 | RouterOS REST API через httpx; S3 — boto3 |
|
||
|
||
```
|
||
app/ main.py config.py db.py migrations.py models.py ids.py security.py process_lock.py s3.py
|
||
app/ros/ client.py (REST-клиент) operations.py (статус, бэкап, обновления, откат)
|
||
app/services/ devices groups backups ops jobs poller events settings rotation
|
||
app/api/ v1.py (JSON API)
|
||
app/ui/ routes.py templates/ static/ (стили, app.js, шрифты)
|
||
tests/ автотесты (pytest) — по модулям: test_security test_devices test_operations test_backups
|
||
test_events test_ids_migrations test_architecture; helpers.py — общие хелперы, conftest.py — фикстуры
|
||
docs/changes/ планы и итоги доработок docs/reviews/ отчёты ревью
|
||
```
|
||
|
||
## Функциональность и правила
|
||
**Устройства**
|
||
- Добавление, изменение, удаление; пароль хранится зашифрованным (Fernet) и в ответы API не попадает.
|
||
- Имя задаётся только при создании и не меняется: оно входит в ключи бэкапов в S3. Клик по имени открывает окно изменения.
|
||
- Примечание до 500 символов — подсказкой при наведении на имя.
|
||
- Статус: online/offline (с причиной недоступности), модель, канал обновлений, версии ROS и FW, uptime, время запроса бэкапа.
|
||
- Мониторинг: опрос каждые `POLL_INTERVAL` секунд, недоступность определяется за ~`ROS_CONNECT_TIMEOUT` секунд; страница обновляет статусы сама.
|
||
|
||
**Колонка «Upgrade ROS»**
|
||
| Состояние | Отображение |
|
||
|---|---|
|
||
| На канале есть более новая версия | «↑ X» |
|
||
| Установлена версия канала | «актуально» |
|
||
| Версия канала старше установленной (например, после перехода на `long-term`) | «канал: X» — не обновление; откат — отдельной операцией |
|
||
| Последняя проверка обновлений завершилась ошибкой | «проверка не удалась», текст ошибки в подсказке |
|
||
| Проверка ещё не выполнялась (или у устройства нет интернета) | «—» |
|
||
|
||
**Колонка «Upgrade FW»** (изменение 027)
|
||
| Состояние | Отображение |
|
||
|---|---|
|
||
| В установленную ROS встроена более новая прошивка платы | «↑ X» |
|
||
| Записана версия, встроенная в ROS | «актуально» |
|
||
| Прошивка платы новее встроенной в ROS (например, плата не откатилась вместе с ROS — 023) | «в ROS: X» — не обновление; откат — отдельной операцией |
|
||
| Нет RouterBOARD firmware (CHR) или устройство не опрошено | «—» |
|
||
|
||
Прошивка сравнивается с версией, встроенной в установленную ROS (`upgrade-firmware`), а не с серверами MikroTik: `/system/routerboard/upgrade` только записывает то, что уже скачано вместе с ROS.
|
||
|
||
**Группы и фильтры**
|
||
- Устройство — в одной группе или «Без группы». Вкладки групп со счётчиками, страница «Группы»; группу можно создать при добавлении устройства.
|
||
- Фильтры устройств: поиск (имя, адрес, модель), статус, наличие обновлений, канал. Фильтры бэкапов: группа, устройство, тип файла, период, имя файла. Фильтры сохраняются в URL.
|
||
|
||
**Операции и задачи**
|
||
- Действия по устройству — меню «⋯» строки; над выбранными — полоса инструментов; через API — и над целой группой.
|
||
- Бэкап, обновление ROS и FW, смена канала для группы, откат ROS, откат FW (027) выполняются задачами (карточка «Задачи», `/api/v1/jobs`).
|
||
- Обновление ROS: устройство перезагружается после скачивания. Обновление FW: перезагрузка, как только в журнале устройства появилась запись «Firmware upgraded successfully…».
|
||
- Штатное обновление не выполняет откат: версия канала (или встроенной прошивки) старше установленной/записанной — «Обновление не требуется», с подсказкой на соответствующий откат.
|
||
|
||
**Откат ROS до версии канала / откат FW до версии ROS**
|
||
- Только осознанно: окно с вводом целевой версии, кнопка «Откатить» активна после ввода. Одно окно на оба вида отката (параметр `kind`): для ROS — «Откатить ROS…» в меню «⋯» (виден только в состоянии «канал: X») и пункт в меню «Обновление»; для FW (изменение 027) — «Откатить прошивку…» (виден в состоянии «в ROS: X») и «Откатить прошивку до версии ROS…» там же. Устройства, которые откатывать нельзя, показаны списком «Не будут затронуты».
|
||
- Задача: проверка на устройстве (целевая версия совпадает с введённой и старше записанной) → **обязательный бэкап** (сбой прерывает) → повторная проверка и запись. Для FW прошивка берётся из установленной ROS, а не с серверов MikroTik.
|
||
|
||
**Бэкапы**
|
||
- Пара файлов: `.backup` (без шифрования) и `.rsc` (с `show-sensitive` — пароли и ключи). Сервер создаёт их через REST, скачивает, загружает в S3 и удаляет с устройства.
|
||
- Список бакета, скачивание по временной ссылке, удаление одного файла или выбранных (до 500 за раз).
|
||
|
||
**Идентификаторы**
|
||
- `<префикс>_<uuid7>`: `dev_` устройство, `grp_` группа, `job_` задача, `bkp_` резервная копия, `evt_` запись журнала. Типы не пересекаются, ID сортируются по времени и не переиспользуются. ID чужого типа или неверного формата — 404.
|
||
- Резервная копия — пара файлов с одним `bkp_` ID; у файлов в S3 метаданные `backup-id`/`device-id` (Yandex отдаёт `Backup-Id`/`Device-Id`). Копия без файлов в бакете помечается удалённой и остаётся в истории.
|
||
|
||
**Журнал событий**
|
||
- Создание, изменение и удаление устройств и групп, перенос устройств, задачи и бэкапы, online/offline (только смены), вход в UI, ротация и очистка журнала. Запись: `evt_` ID, ID сущности, актор (`ui:<пользователь>`, `api`, `poller`, `system`, `anonymous`), данные.
|
||
- Страница «Журнал»: фильтры по типу, актору, устройству, периоду и тексту; «Показать ещё 100»; окно записи с полными ID и данными.
|
||
- Ротация по сроку и числу записей (настройки в UI, хранятся в БД): при старте, раз в час и после сохранения; оставляет запись `journal.rotated`.
|
||
- Очистка — только в UI, через окно с паролем пользователя; остаётся запись `journal.cleared`. Через API — только чтение журнала и настроек.
|
||
|
||
## Требования к устройствам
|
||
- RouterBOARD и виртуальные **CHR** (у CHR нет `/system/routerboard`: FW не показывается, обновление FW пропускается).
|
||
- RouterOS **7.1+**, сервис `www-ssl` (REST, порт 443; **не** `api-ssl` 8729 и не `api` 8728). Для доверенной сети допустим `www` (порт 80) со снятым флагом «HTTPS» — логин и пароль идут открытым текстом.
|
||
- Пользователь с политиками `read`, `write`, `policy`, `test`, `ftp`, `reboot`, `sensitive` (проще — группа `full`). Группа `write` не подходит: бэкап падает с `not enough permissions`. После смены прав пересоздайте подключение к устройству.
|
||
- Доступ устройств к S3 не нужен: файлы загружает Control Server (нужен доступ к `storage.yandexcloud.net:443`).
|
||
|
||
## API (`/api/v1`)
|
||
| Область | Эндпоинты |
|
||
|---|---|
|
||
| Устройства | `GET\|POST /devices` (фильтры `group`, `q`, `status`, `updates`, `channel`), `GET\|PATCH\|DELETE /devices/{id}` (смена имени — 400), `POST /devices/refresh`, `POST /devices/{id}/refresh` |
|
||
| Операции | `POST /devices/{id}/backups`, `PUT /devices/{id}/update/channel`, `POST /devices/{id}/update/install`, `POST /devices/{id}/firmware/upgrade`, `POST /devices/{id}/update/downgrade`, `POST /devices/{id}/firmware/downgrade` (027) — оба отката `{"target_version"}` обязателен |
|
||
| Групповые | `POST /batch/{backup\|ros_update\|fw_update}`, `PUT /batch/channel` (`channel`), `POST /batch/ros_downgrade`, `POST /batch/fw_downgrade` (027) — оба `target_version` — тело `{"device_ids": [...]}` и/или `{"group_id": "grp_…"}` |
|
||
| Группы | `GET\|POST /groups`, `PATCH\|DELETE /groups/{id}` (устройства остаются без группы) |
|
||
| Бэкапы | `GET /backups` (фильтры `device_id`, `group`, `kind`, `date_from`, `date_to`, `q`; `refresh=1` — минуя кэш), `GET /backups/download?key=`, `DELETE /backups?key=`, `POST /backups/delete` (`{"keys": [...]}` → `{"deleted", "failed"}`) |
|
||
| Задачи | `GET /jobs`, `GET /jobs/{id}` |
|
||
| Журнал | `GET /events` (фильтры `entity_id`, `type`, `device_id`, `job_id`, `actor`, `date_from`, `date_to`, `q`; `before=<evt_…>`, `limit`), `GET /events/{id}`, `GET\|PUT /events/settings` |
|
||
|
||
**Соглашения**
|
||
- Все ID — строки с префиксом типа; `group_id: null` — без группы; пароль устройства передаётся только при записи.
|
||
- Операции над устройствами (бэкап, обновления, откат, групповые) отвечают `202 {"job_ids": [...]}`; результат — в задаче. Смена канала одного устройства и `refresh` — синхронные.
|
||
- Ошибки: 400 — неверные данные, 401 — нет или неверный токен, 404 — нет объекта или ID чужого типа, 422 — нарушение схемы, 502 — ошибка S3.
|
||
|
||
```bash
|
||
curl -s -H "Authorization: Bearer $API_TOKEN" http://localhost:8000/api/v1/devices | python3 -m json.tool
|
||
```
|
||
Справочники: [RouterOS REST API](https://help.mikrotik.com/docs/spaces/ROS/pages/47579162/REST+API#RESTAPI-HTTPMethods),
|
||
[Yandex Object Storage S3 API](https://yandex.cloud/ru/docs/storage/s3/api-ref/).
|
||
|
||
## Безопасность
|
||
**Секреты**
|
||
- Требования — в «Конфигурации»; с небезопасными значениями приложение не стартует.
|
||
- Пароли устройств шифруются `SECRET_KEY`; секреты не пишутся ни в журнал, ни в сообщения об ошибках.
|
||
|
||
**Вход в UI**
|
||
- 5 неверных попыток за 10 минут с одного IP → IP заблокирован на 10 минут (429, пароль не проверяется); неверный пароль — 401.
|
||
- Блокировка по IP, а не по имени: единственного администратора нельзя заблокировать чужими попытками. За reverse-proxy без `TRUSTED_PROXIES` все клиенты видны с IP прокси — одна блокировка на всех.
|
||
- `TRUSTED_PROXIES` (CIDR через запятую) включает разбор `X-Forwarded-For`: только когда адрес соединения (peer) сам входит в доверенную сеть, заголовок берётся в расчёт — цепочка разбирается справа налево, первый адрес не из доверенной сети становится IP клиента (блокировка входа, `data.ip` в `auth.*`). Пустой список (по умолчанию) или недоверенный peer — заголовок полностью игнорируется, подделать IP нельзя. Доверяя сети Docker-моста, вы делаете доверенными и процессы хоста (не только сам reverse-proxy) — используйте узкий CIDR.
|
||
- Очистка журнала требует пароль; 5 неверных за 10 минут блокируют её на 10 минут.
|
||
- Редиректы после форм — только на локальный путь.
|
||
|
||
**Данные**
|
||
- `.rsc` содержит пароли и ключи, `.backup` не зашифрован: ограничьте доступ к бакету и ссылкам скачивания.
|
||
- Порт приложения не открывайте в недоверенную сеть; за TLS включите `SESSION_COOKIE_SECURE=true`.
|
||
|
||
## Эксплуатация
|
||
- Контейнер работает от непривилегированного пользователя (uid 10001), перезапуск `unless-stopped`.
|
||
- БД (SQLite) — в томе `ros_data`, переживает пересборку; `docker compose down -v` удаляет её. Схема обновляется при старте; перед миграцией ID создаётся копия `ros_control.db.bak-<метка>`.
|
||
- **Один процесс на БД**: файловая блокировка `<файл БД>.lock`; второй процесс (`--workers 2+`, вторая копия контейнера на том же томе) не стартует. В памяти процесса — семафор задач, счётчики неудачных паролей, кэш бакета.
|
||
- **Кэш бакета**: страница «Бэкапы» и `GET /api/v1/backups` перечитывают бакет не чаще `BACKUPS_CACHE_TTL`; собственные изменения сбрасывают кэш, внешние видны по «Обновить список».
|
||
- **Копия БД**: SQLite работает в режиме WAL — файл БД без `-wal` может быть неполным. Копировать через `sqlite3 <БД> ".backup <копия>"` или вместе с `-wal`/`-shm`.
|
||
- Текущий стенд опубликован на порту **8001** — задан `APP_PORT=8001` в `.env` стенда (порт 8000 на хосте занят другим процессом); override-файл не нужен, `docker compose up -d` берёт порт из `.env`.
|
||
|
||
## Интерфейс
|
||
- Экраны: «Устройства» (вкладки групп, фильтры, таблица, карточка «Задачи»), «Бэкапы», «Группы», «Журнал».
|
||
- Пока строки не выбраны, полоса инструментов показывает фильтры; при выборе — действия над выбранными.
|
||
- Добавление и изменение устройств, группы, откат ROS, запись журнала — в окнах поверх страницы; запасные страницы `/devices/new`, `/devices/{id}/edit` работают без JavaScript.
|
||
- Меню и выпадающие списки в едином стиле «⋯», не обрезаются таблицей и раскрываются вверх у нижнего края. Списки строятся поверх скрытого `<select>`: без JavaScript работает стандартный.
|
||
- Светлая и тёмная темы — по настройке системы.
|
||
|
||
## Тесты
|
||
39 тестов по модулям предметных областей (`tests/test_*.py`), фоновый опрос выключен; стенд не нужен (временная SQLite,
|
||
RouterOS и S3 — заглушки). Тест-линтер (`test_architecture.py`) не допускает синхронных обращений к БД в `async`-коде.
|
||
```bash
|
||
python3 -m venv venv && venv/bin/pip install -r requirements.txt
|
||
venv/bin/python -m pytest -q # все тесты
|
||
venv/bin/python -m pytest -q tests/test_backups.py # один модуль
|
||
```
|
||
Локальный запуск без Docker: `set -a; . ./.env; set +a; venv/bin/uvicorn app.main:app --reload`.
|
||
|
||
## История изменений
|
||
Каждая доработка описана в `docs/changes/<номер>/`: `plan.md` — план, `summary.md` — итог.
|
||
|
||
| № | Изменение | Документы |
|
||
|---|---|---|
|
||
| 001 | Первая версия | [план](docs/changes/001-initial-implementation/plan.md) · [итог](docs/changes/001-initial-implementation/summary.md) |
|
||
| 002 | HTTP-подключение, исправления по тесту на устройстве | [план](docs/changes/002-http-support-and-fixes/plan.md) · [итог](docs/changes/002-http-support-and-fixes/summary.md) |
|
||
| 003 | Бэкап: скачивание через REST, загрузка в S3 сервером | [план](docs/changes/003-download-via-rest-upload-from-server/plan.md) · [итог](docs/changes/003-download-via-rest-upload-from-server/summary.md) |
|
||
| 004 | Колонки Upgrade ROS/FW, меню действий «⋯» | [план](docs/changes/004-upgrade-columns-and-actions-menu/plan.md) · [итог](docs/changes/004-upgrade-columns-and-actions-menu/summary.md) |
|
||
| 005 | Группы устройств, фильтры устройств и бэкапов | [план](docs/changes/005-groups-and-filters/plan.md) · [итог](docs/changes/005-groups-and-filters/summary.md) |
|
||
| 006 | Новый интерфейс по утверждённому макету | [план](docs/changes/006-ui-redesign/plan.md) · [итог](docs/changes/006-ui-redesign/summary.md) |
|
||
| 007 | Фоновый опрос, быстрое обнаружение недоступности | [план](docs/changes/007-fast-offline-detection/plan.md) · [итог](docs/changes/007-fast-offline-detection/summary.md) |
|
||
| 008 | Примечание к устройству | [план](docs/changes/008-device-note/plan.md) · [итог](docs/changes/008-device-note/summary.md) |
|
||
| 009 | Выравнивание полей в окне устройства | [план](docs/changes/009-dialog-label-alignment/plan.md) · [итог](docs/changes/009-dialog-label-alignment/summary.md) |
|
||
| 010 | `.rsc` с `show-sensitive` | [план](docs/changes/010-export-show-sensitive/plan.md) · [итог](docs/changes/010-export-show-sensitive/summary.md) |
|
||
| 011 | Выпадающие меню не обрезаются | [план](docs/changes/011-dropdown-clipping/plan.md) · [итог](docs/changes/011-dropdown-clipping/summary.md) |
|
||
| 012 | Клик по имени устройства открывает окно изменения | [план](docs/changes/012-clickable-device-name/plan.md) · [итог](docs/changes/012-clickable-device-name/summary.md) |
|
||
| 013 | Групповое удаление бэкапов | [план](docs/changes/013-bulk-delete-backups/plan.md) · [итог](docs/changes/013-bulk-delete-backups/summary.md) |
|
||
| 014 | Поддержка CHR, строгое сравнение версий ROS | [план](docs/changes/014-chr-support/plan.md) · [итог](docs/changes/014-chr-support/summary.md) |
|
||
| 015 | Имя устройства задаётся только при создании | [план](docs/changes/015-immutable-device-name/plan.md) · [итог](docs/changes/015-immutable-device-name/summary.md) |
|
||
| 016 | Уникальные ID сущностей, журнал событий | [план](docs/changes/016-unique-ids-and-event-log/plan.md) · [итог](docs/changes/016-unique-ids-and-event-log/summary.md) |
|
||
| 017 | Журнал в UI: ротация, очистка с паролем | [план](docs/changes/017-events-ui-rotation/plan.md) · [итог](docs/changes/017-events-ui-rotation/summary.md) |
|
||
| 018 | Корректность: удаление бэкапа, миграции, групповая смена канала задачами | [план](docs/changes/018-correctness-consistency/plan.md) · [итог](docs/changes/018-correctness-consistency/summary.md) |
|
||
| 019 | Производительность: кэш бакета, БД вне event loop, один процесс на БД | [план](docs/changes/019-performance-scaling/plan.md) · [итог](docs/changes/019-performance-scaling/summary.md) |
|
||
| 020 | Выпадающие списки в стиле меню «⋯» | [план](docs/changes/020-custom-select-menus/plan.md) · [итог](docs/changes/020-custom-select-menus/summary.md) |
|
||
| 021 | Безопасность: секреты, блокировка входа, Secure-cookie, редиректы | [план](docs/changes/021-security-hardening/plan.md) · [итог](docs/changes/021-security-hardening/summary.md) |
|
||
| 022 | Остаток синхронной БД в async-коде, тест-линтер | [план](docs/changes/022-async-db-remainder/plan.md) · [итог](docs/changes/022-async-db-remainder/summary.md) |
|
||
| 023 | Состояния «Upgrade ROS», откат ROS до версии канала | [план](docs/changes/023-ros-downgrade/plan.md) · [итог](docs/changes/023-ros-downgrade/summary.md) |
|
||
| 024 | Оптимизация README | [план](docs/changes/024-readme-restructure/plan.md) · [итог](docs/changes/024-readme-restructure/summary.md) |
|
||
| 025 | Доверенные прокси (реальный IP клиента), параметризация порта | [план](docs/changes/025-trusted-proxies-port/plan.md) · [итог](docs/changes/025-trusted-proxies-port/summary.md) |
|
||
| 026 | Тесты по модулям предметных областей | [план](docs/changes/026-split-tests/plan.md) · [итог](docs/changes/026-split-tests/summary.md) |
|
||
| 027 | Состояния «Upgrade FW», откат прошивки RouterBOARD до версии ROS | [план](docs/changes/027-fw-downgrade/plan.md) · [итог](docs/changes/027-fw-downgrade/summary.md) |
|
||
|
||
## Отчёты ревью
|
||
- [Ревью кодовой базы 2026-09-27](docs/reviews/2026-09-27-codebase-review.md) (→ 018–021)
|
||
- [Повторное ревью 2026-09-28 12:43](docs/reviews/2026-09-28-1243-codebase-review.md) (→ 022, 024)
|
||
- [Третье ревью 2026-09-28 17:35](docs/reviews/2026-09-28-1735-codebase-review.md) (→ 025, 026)
|
||
- [Четвёртое ревью 2026-09-28 21:32](docs/reviews/2026-09-28-2132-codebase-review.md) (открыты п. 11, 13, 15, 19; п. 18 отложен)
|