README по образцу ipam_control
README переверстан (docs/changes/024): быстрый старт, конфигурация одной таблицей, архитектура с деревом каталогов, функциональность и правила по разделам (таблица состояний «Upgrade ROS»), API по областям с соглашениями, безопасность, эксплуатация (в т.ч. копия БД в режиме WAL, стенд на порту 8001), интерфейс, тесты, история изменений таблицей со ссылками на план и итог, отчёты ревью. Повторы убраны. Все 51 ссылка на документы проверены; переменные, эндпоинты и число тестов сверены с кодом. Код не менялся. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ae826fc562
commit
7c830be721
3 files changed
+227
-145
No files matched your search
@@ -1,7 +1,8 @@
|
|||||||
# ros_control
|
# ros_control
|
||||||
|
|
||||||
Централизованное управление парком MikroTik RouterOS: WEB UI и JSON API.
|
Централизованное управление парком MikroTik RouterOS: статус и мониторинг устройств, бэкапы в S3, обновление и откат ROS,
|
||||||
Дизайн и требования — `ros_control.svg`; макет интерфейса — утверждён и перенесён в приложение (см. `docs/changes/006-ui-redesign`).
|
обновление прошивки, группы устройств и журнал событий. 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 (на каждом устройстве)
|
Admin Dashboard ⇄ Control Server ⇄ RouterOS REST (на каждом устройстве)
|
||||||
@@ -9,166 +10,197 @@ Admin Dashboard ⇄ Control Server ⇄ RouterOS REST (на каждом устр
|
|||||||
Metadata DB S3 (Yandex Object Storage)
|
Metadata DB S3 (Yandex Object Storage)
|
||||||
```
|
```
|
||||||
|
|
||||||
## Возможности
|
## Быстрый старт
|
||||||
|
|
||||||
**Устройства**
|
|
||||||
- Список устройств: добавление, изменение, удаление; пароли хранятся зашифрованно (Fernet). Имя устройства в таблице кликабельно — открывает окно изменения; **само имя задаётся только при создании и не меняется** (оно входит в ключи бэкапов).
|
|
||||||
- К устройству можно добавить **примечание** (до 500 символов): видно подсказкой при наведении на имя.
|
|
||||||
- Статус: online/offline, модель, канал обновлений, версии ROS и FW, uptime, время запроса бэкапа. Колонки **Upgrade ROS** и **Upgrade FW** показывают версию, до которой можно обновиться (устройство проверяет обновления на серверах MikroTik; без интернета на устройстве — «—»).
|
|
||||||
- Колонка **Upgrade ROS** — пять состояний: обновление доступно («↑ версия»), актуально, **«канал: версия»** — версия канала старше установленной (например, после переключения на `long-term`: это не обновление, а осознанный откат), «проверка не удалась» — последняя проверка обновлений завершилась ошибкой, «—» — проверка ещё не выполнялась.
|
|
||||||
- **Мониторинг доступности**: сервер опрашивает устройства каждые 30 с (`POLL_INTERVAL`), недоступность определяется за ~4 с на устройство (`ROS_CONNECT_TIMEOUT`); страница обновляет статусы сама, без перезагрузки.
|
|
||||||
|
|
||||||
**Группы и фильтры**
|
|
||||||
- Устройство относится к одной группе: вкладки групп со счётчиками, страница «Группы» (создание, переименование, удаление), перенос отмеченных устройств в группу. При добавлении устройства группу можно создать «на месте» (пункт «+ Новая группа…»).
|
|
||||||
- Фильтры устройств: поиск (имя, адрес, модель), статус, наличие обновлений, канал. Фильтры бэкапов: группа, устройство, тип файла, период, имя файла. Фильтры попадают в URL.
|
|
||||||
|
|
||||||
**Операции**
|
|
||||||
- Действия по устройству — в меню «⋯» строки; действия над выбранными устройствами (бэкап, статус, обновление ROS/FW, канал, перенос в группу) появляются в полосе инструментов при выборе строк; через API их можно запускать и над всей группой.
|
|
||||||
- **Бэкап**: `.backup` (без шифрования) и `.rsc` (с `show-sensitive`, т.е. с паролями и ключами — из него можно восстановить все сущности, использующие пароль). Сервер создаёт файлы через REST, скачивает во временную папку, загружает в S3 и удаляет файлы с устройства.
|
|
||||||
- Список бэкапов в бакете, скачивание (временная ссылка), удаление одного файла или **группы выбранных** (чекбоксы, кнопка «Удалить» с подтверждением).
|
|
||||||
- Канал обновлений; обновление ROS (устройство перезагружается после скачивания); обновление FW (перезагрузка сразу после появления в журнале устройства записи «Firmware upgraded successfully…»).
|
|
||||||
- **Откат ROS до версии канала** (состояние «канал: X»): окно с вводом целевой версии (кнопка активна только после ввода, сервер сверяет версию с кэшем статуса) — для одного устройства (пункт «Откатить ROS…» в меню «⋯», виден только в состоянии отката) и для группы (пункт «Откатить ROS до версии канала…» в меню «Обновление» с выбранными устройствами; устройства, которые откатывать нельзя, показаны отдельным списком «Не будут затронуты»). Перед откатом — обязательный бэкап: не удался — откат не запускается. Реальный откат на устройстве запускает пользователь в UI.
|
|
||||||
- Долгие и групповые операции выполняются задачами (карточка «Задачи», состояние — в UI и через API).
|
|
||||||
|
|
||||||
## Идентификаторы и журнал событий
|
|
||||||
|
|
||||||
У каждой сущности глобально уникальный ID вида `<префикс>_<uuid7>`: `dev_…` устройство, `grp_…` группа, `job_…` задача, `bkp_…` резервная копия, `evt_…` запись журнала. Префикс исключает пересечение типов, UUIDv7 не повторяется и сортируется по времени создания; ID не переиспользуются после удаления. Числовых ID нет: `GET /api/v1/devices/dev_…`, ID чужого типа или неверного формата — 404.
|
|
||||||
Резервная копия — пара файлов (`.backup` + `.rsc`) с одним `bkp_` ID; файлы в бакете имеют метаданные `backup-id`/`device-id` (Yandex возвращает их как `Backup-Id`/`Device-Id`), в списке файлов API есть `backup_id`; копия, у которой пропали файлы, помечается удалённой, но остаётся в истории.
|
|
||||||
**Журнал событий** пишется в БД: создание, изменение и удаление устройств и групп, перенос устройств, жизненный цикл задач и бэкапов, смена online/offline, вход в UI, ротация и очистка журнала. Каждая запись содержит `evt_` ID, ID сущности, актора (`ui:<пользователь>`, `api`, `poller`, `system`) и данные.
|
|
||||||
В UI — страница «Журнал» (фильтры по типу, актору, устройству, периоду и поиску по сообщению/ID; подгрузка «Показать ещё 100»; клик по строке открывает запись с полными ID и данными).
|
|
||||||
- **Ротация**: «Журнал → Настройки» — срок хранения (дней) и максимум записей, `0` — без ограничения. Настройки хранятся в БД (значения по умолчанию — `EVENTS_RETENTION_DAYS=90`, `EVENTS_MAX_ROWS=100000` в `.env`); ротация выполняется при старте, раз в час и сразу после сохранения настроек и оставляет запись `journal.rotated`.
|
|
||||||
- **Очистка**: «Журнал → Очистить журнал» открывает окно с вводом пароля пользователя. Неверный пароль журнал не трогает (`journal.clear_denied`), 5 неверных попыток за 10 минут блокируют очистку на 10 минут. После успешной очистки остаётся одна запись `journal.cleared` (кто и сколько удалил). Очистки через API нет — только через это окно; API даёт чтение журнала и настроек.
|
|
||||||
|
|
||||||
## Интерфейс
|
|
||||||
|
|
||||||
Экран строится сверху вниз: приложение (навигация) → страница (заголовок, счётчики, главное действие) → вкладки групп → полоса инструментов таблицы → данные; «Задачи» — отдельная карточка. Пока ничего не выбрано, полоса показывает фильтры; при выборе строк — действия над выбранными.
|
|
||||||
Добавление и изменение устройств, создание и переименование групп, откат ROS — в окнах поверх страницы (запасные страницы `/devices/new`, `/devices/{id}/edit` работают без JavaScript). Выпадающие меню не обрезаются таблицей и раскрываются вверх, если снизу нет места. Выпадающие списки (фильтры устройств/бэкапов/журнала, поле «Группа») оформлены как меню действий «⋯»: список строит JS поверх обычного `<select>`, который остаётся в разметке скрытым — без JavaScript работает стандартный выбор браузера. Светлая и тёмная темы переключаются по настройке системы. Шрифты IBM Plex лежат в `app/ui/static/fonts` (лицензия OFL), внешние ресурсы не загружаются.
|
|
||||||
|
|
||||||
## Требования к устройствам
|
|
||||||
|
|
||||||
- Поддерживаются и RouterBOARD, и виртуальные **CHR** (у них нет `/system/routerboard`: версия FW не показывается, обновление FW пропускается).
|
|
||||||
- RouterOS **7.1+**, включённый сервис `www-ssl` (REST API, порт 443; **не** `api-ssl` 8729 и не `api` 8728). Если `www-ssl` недоступен, для доверенной сети можно указать сервис `www` (порт 80) и снять флаг «HTTPS» у устройства — логин и пароль пойдут открытым текстом.
|
|
||||||
- Пользователь с политиками `read`, `write`, `policy`, `test`, `ftp`, `reboot`, `sensitive` (проще всего — группа `full`). Встроенная группа `write` с запретом `ftp`/`policy` не подходит: бэкап падает с `not enough permissions`. После смены прав пересоздайте подключение к устройству.
|
|
||||||
- Доступ устройств к S3 не нужен: файлы забирает и загружает Control Server (ему нужен доступ к `storage.yandexcloud.net:443`).
|
|
||||||
|
|
||||||
## Безопасность
|
|
||||||
|
|
||||||
- **Секреты обязательны:** `API_TOKEN`, `SESSION_SECRET` (≥ 32 символов) и `ADMIN_PASSWORD` (≥ 12 символов) не могут быть пустыми или равными `change-me`; `SECRET_KEY` должен быть валидным ключом Fernet. С небезопасными значениями приложение не стартует (`RuntimeError` при старте, без секретов в тексте ошибки и в логе) — сгенерировать: `python -c "import secrets; print(secrets.token_urlsafe(32))"`. Порт 8000 не стоит открывать в недоверенную сеть.
|
|
||||||
- **Вход в UI ограничен по IP клиента**: 5 неверных попыток за 10 минут блокируют IP на 10 минут (в блокировке пароль не проверяется); единственного администратора нельзя заблокировать чужими попытками, т.к. блокировка не привязана к имени пользователя. Ограничение достоверно только пока перед приложением нет reverse-proxy — иначе все запросы приходят с одного IP прокси, и понадобится доверенный `X-Forwarded-For`.
|
|
||||||
- **Cookie сессии UI**: `SESSION_COOKIE_SECURE=false` по умолчанию (UI по HTTP продолжает работать); включите `true`, когда приложение работает за TLS.
|
|
||||||
- Пароли устройств шифруются ключом `SECRET_KEY` (Fernet); потеря ключа = потеря доступа к сохранённым паролям.
|
|
||||||
- **Секреты в бэкапах:** `.rsc` создаётся с `show-sensitive`, а `.backup` — без шифрования, поэтому файлы содержат пароли и ключи открытым текстом. Ограничьте доступ к бакету и ссылкам скачивания.
|
|
||||||
|
|
||||||
## Ограничения
|
|
||||||
|
|
||||||
- **Один процесс на БД**: сервер держит файловую блокировку `<файл БД>.lock` рядом с БД (снимается при остановке); второй процесс на той же БД (`--workers 2+`, вторая копия контейнера на том же томе) не стартует — понятная ошибка вместо молчаливой порчи данных. В памяти процесса (не переживает перезапуск и не разделяется между процессами) — семафор фоновых задач, блокировка синхронизации бэкапов, счётчики неудачных попыток входа в UI и очистки журнала, кэш списка бакета.
|
|
||||||
- **Кэш списка бакета**: страница «Бэкапы» и `GET /api/v1/backups` не перечитывают бакет на каждый просмотр — список живёт `BACKUPS_CACHE_TTL` секунд (по умолчанию 60; `0` — кэш выключен). Изменения бакета, сделанные не через это приложение, видны не позже TTL или сразу — кнопкой «Обновить список» (`refresh=1`). Собственные изменения (бэкап, удаление) сбрасывают кэш сами.
|
|
||||||
|
|
||||||
## Запуск
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cp .env.example .env # заполнить SECRET_KEY, SESSION_SECRET, ADMIN_PASSWORD, API_TOKEN, S3_*
|
cp .env.example .env # заполнить SECRET_KEY, SESSION_SECRET, ADMIN_PASSWORD, API_TOKEN, S3_* (см. «Конфигурация»)
|
||||||
docker compose up -d --build # UI: http://localhost:8000, OpenAPI: /docs
|
docker compose up -d --build # БД — в томе ros_data, схема обновляется при старте
|
||||||
```
|
```
|
||||||
|
- UI: `http://<хост>:8000/`, OpenAPI: `http://<хост>: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())"`.
|
||||||
|
|
||||||
БД (SQLite) хранится в Docker-томе `ros_data` (`docker volume inspect ros_control_ros_data`), переживает пересборку контейнера; схема обновляется автоматически при старте (перед миграцией ID создаётся копия `ros_control.db.bak-<метка>` рядом с БД). Остановка: `docker compose down` (том сохраняется; `down -v` удалит БД).
|
## Конфигурация (`.env`)
|
||||||
|
|
||||||
Ключ Fernet: `python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"`.
|
|
||||||
|
|
||||||
### Настройки (`.env`)
|
|
||||||
|
|
||||||
| Переменная | По умолчанию | Назначение |
|
| Переменная | По умолчанию | Назначение |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `SECRET_KEY` | — | ключ Fernet для паролей устройств (обязателен) |
|
| `SECRET_KEY` | — | Ключ Fernet для паролей устройств. Обязателен; потеря ключа — потеря доступа к сохранённым паролям |
|
||||||
| `SESSION_SECRET` | — | подпись cookie-сессии UI (обязателен, ≥ 32 символов) |
|
| `SESSION_SECRET` | — | Подпись cookie-сессии UI. Обязателен, ≥ 32 символов |
|
||||||
| `SESSION_COOKIE_SECURE` | `false` | Secure-флаг cookie сессии; включить за TLS |
|
| `SESSION_COOKIE_SECURE` | `false` | Флаг Secure у cookie сессии; включить, когда приложение работает за TLS |
|
||||||
| `ADMIN_USER` / `ADMIN_PASSWORD` | `admin` / — | вход в UI (`ADMIN_PASSWORD` обязателен, ≥ 12 символов) |
|
| `ADMIN_USER`, `ADMIN_PASSWORD` | `admin`, — | Вход в UI. Пароль обязателен, ≥ 12 символов |
|
||||||
| `API_TOKEN` | — | Bearer-токен API (обязателен, ≥ 32 символов) |
|
| `API_TOKEN` | — | Bearer-токен API. Обязателен, ≥ 32 символов |
|
||||||
| `DATABASE_URL` | `sqlite:///./data/ros_control.db` | база метаданных (в compose — том) |
|
| `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` | бакет для резервных копий |
|
| `S3_ENDPOINT`, `S3_REGION`, `S3_BUCKET`, `S3_ACCESS_KEY`, `S3_SECRET_KEY`, `S3_PREFIX` | Yandex Object Storage, `backups` | Бакет резервных копий |
|
||||||
| `BACKUPS_CACHE_TTL` | `60` | кэш списка бакета, с; `0` — выключить (см. «Ограничения») |
|
| `BACKUPS_CACHE_TTL` | `60` | Время жизни кэша списка бакета, с; `0` — без кэша |
|
||||||
| `MAX_CONCURRENCY` | `10` | одновременные обращения к устройствам |
|
| `MAX_CONCURRENCY` | `10` | Одновременные обращения к устройствам |
|
||||||
| `ROS_CONNECT_TIMEOUT` | `4` | секунд на установление соединения (скорость обнаружения недоступности) |
|
| `ROS_CONNECT_TIMEOUT` | `4` | Установление соединения, с — скорость обнаружения недоступности |
|
||||||
| `ROS_TIMEOUT` | `30` | секунд на ответ устройства (долгие операции) |
|
| `ROS_TIMEOUT` | `30` | Ответ устройства, с — долгие операции |
|
||||||
| `POLL_INTERVAL` | `30` | период фонового опроса, с; `0` — выключить |
|
| `POLL_INTERVAL` | `30` | Период фонового опроса, с; `0` — выключить |
|
||||||
| `UPDATE_CHECK_INTERVAL` | `1800` | как часто опрос проверяет обновления ROS, с |
|
| `UPDATE_CHECK_INTERVAL` | `1800` | Как часто опрос проверяет обновления ROS, с |
|
||||||
| `EVENTS_RETENTION_DAYS` / `EVENTS_MAX_ROWS` | `90` / `100000` | ротация журнала по умолчанию (действующие значения меняются в UI); `0` — без ограничения |
|
| `EVENTS_RETENTION_DAYS`, `EVENTS_MAX_ROWS` | `90`, `100000` | Ротация журнала по умолчанию; действующие значения меняются в UI; `0` — без ограничения |
|
||||||
|
|
||||||
### Разработка
|
Пустые, равные `change-me` или слишком короткие секреты и невалидный `SECRET_KEY` — отказ старта (`RuntimeError` без значений секретов).
|
||||||
|
|
||||||
```bash
|
## Архитектура
|
||||||
python3 -m venv venv && ./venv/bin/pip install -r requirements.txt
|
| Слой | Технологии |
|
||||||
set -a; . ./.env; set +a
|
|---|---|
|
||||||
./venv/bin/uvicorn app.main:app --reload
|
| API | FastAPI, pydantic v2, Bearer-токен; долгие операции — фоновые задачи с ограничением параллелизма |
|
||||||
./venv/bin/python -m pytest # 33 теста, фоновый опрос в тестах выключен
|
| 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)
|
||||||
|
docs/changes/ планы и итоги доработок docs/reviews/ отчёты ревью
|
||||||
```
|
```
|
||||||
|
|
||||||
## API v1
|
## Функциональность и правила
|
||||||
|
**Устройства**
|
||||||
|
- Добавление, изменение, удаление; пароль хранится зашифрованным (Fernet) и в ответы API не попадает.
|
||||||
|
- Имя задаётся только при создании и не меняется: оно входит в ключи бэкапов в S3. Клик по имени открывает окно изменения.
|
||||||
|
- Примечание до 500 символов — подсказкой при наведении на имя.
|
||||||
|
- Статус: online/offline (с причиной недоступности), модель, канал обновлений, версии ROS и FW, uptime, время запроса бэкапа.
|
||||||
|
- Мониторинг: опрос каждые `POLL_INTERVAL` секунд, недоступность определяется за ~`ROS_CONNECT_TIMEOUT` секунд; страница обновляет статусы сама.
|
||||||
|
|
||||||
Заголовок `Authorization: Bearer <API_TOKEN>`. Интерактивная документация — `/docs`. Пример:
|
**Колонка «Upgrade ROS»**
|
||||||
|
| Состояние | Отображение |
|
||||||
|
|---|---|
|
||||||
|
| На канале есть более новая версия | «↑ X» |
|
||||||
|
| Установлена версия канала | «актуально» |
|
||||||
|
| Версия канала старше установленной (например, после перехода на `long-term`) | «канал: X» — не обновление; откат — отдельной операцией |
|
||||||
|
| Последняя проверка обновлений завершилась ошибкой | «проверка не удалась», текст ошибки в подсказке |
|
||||||
|
| Проверка ещё не выполнялась (или у устройства нет интернета) | «—» |
|
||||||
|
|
||||||
|
«Upgrade FW» — версия прошивки для обновления или «актуально». Устройство проверяет обновления на серверах MikroTik.
|
||||||
|
|
||||||
|
**Группы и фильтры**
|
||||||
|
- Устройство — в одной группе или «Без группы». Вкладки групп со счётчиками, страница «Группы»; группу можно создать при добавлении устройства.
|
||||||
|
- Фильтры устройств: поиск (имя, адрес, модель), статус, наличие обновлений, канал. Фильтры бэкапов: группа, устройство, тип файла, период, имя файла. Фильтры сохраняются в URL.
|
||||||
|
|
||||||
|
**Операции и задачи**
|
||||||
|
- Действия по устройству — меню «⋯» строки; над выбранными — полоса инструментов; через API — и над целой группой.
|
||||||
|
- Бэкап, обновление ROS и FW, смена канала для группы, откат ROS выполняются задачами (карточка «Задачи», `/api/v1/jobs`).
|
||||||
|
- Обновление ROS: устройство перезагружается после скачивания. Обновление FW: перезагрузка, как только в журнале устройства появилась запись «Firmware upgraded successfully…».
|
||||||
|
- Штатное обновление не выполняет откат: версия канала старше установленной — «Обновление не требуется».
|
||||||
|
|
||||||
|
**Откат ROS до версии канала**
|
||||||
|
- Только осознанно: окно с вводом целевой версии, кнопка «Откатить» активна после ввода. Для одного устройства — «Откатить ROS…» в меню «⋯» (виден только в состоянии «канал: X»), для группы — пункт в меню «Обновление»; устройства, которые откатывать нельзя, показаны списком «Не будут затронуты».
|
||||||
|
- Задача: проверка на устройстве (версия канала совпадает с введённой и старше установленной) → **обязательный бэкап** (сбой прерывает) → повторная проверка и установка.
|
||||||
|
|
||||||
|
**Бэкапы**
|
||||||
|
- Пара файлов: `.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` (`{"target_version"}` обязателен) |
|
||||||
|
| Групповые | `POST /batch/{backup\|ros_update\|fw_update}`, `PUT /batch/channel` (`channel`), `POST /batch/ros_downgrade` (`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
|
```bash
|
||||||
curl -s -H "Authorization: Bearer $API_TOKEN" http://localhost:8000/api/v1/devices | python3 -m json.tool
|
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),
|
||||||
Все `id` — строки (`dev_…`, `grp_…`, `job_…`); `group_id` — `grp_…`. Устройство: `name`, `host`, `port`, `username`, `password` (только при записи), `verify_tls`, `use_tls` (`false` — HTTP), `group_id` (`null` — без группы), `note`.
|
|
||||||
|
|
||||||
| Метод | Путь | Назначение |
|
|
||||||
|---|---|---|
|
|
||||||
| GET/POST | `/api/v1/devices` | список (фильтры `group`, `q`, `status`, `updates`, `channel`) / добавить |
|
|
||||||
| GET/PATCH/DELETE | `/api/v1/devices/{id}` | получить / изменить (имя менять нельзя — 400) / удалить |
|
|
||||||
| POST | `/api/v1/devices/refresh`, `/devices/{id}/refresh` | обновить статус |
|
|
||||||
| POST | `/api/v1/devices/{id}/backups` | бэкап (задача) |
|
|
||||||
| PUT | `/api/v1/devices/{id}/update/channel` | `{"channel": "stable\|long-term\|testing\|development"}` |
|
|
||||||
| POST | `/api/v1/devices/{id}/update/install` | обновление ROS (задача) |
|
|
||||||
| POST | `/api/v1/devices/{id}/update/downgrade` | `{"target_version": "7.23.7"}` (обязательно) — откат ROS до версии канала: бэкап, затем откат (задача `ros_downgrade`); версия сверяется на устройстве, не совпала — задача завершается ошибкой |
|
|
||||||
| POST | `/api/v1/devices/{id}/firmware/upgrade` | обновление FW (задача) |
|
|
||||||
| GET/POST | `/api/v1/groups` | группы (с числом устройств) / создать |
|
|
||||||
| PATCH/DELETE | `/api/v1/groups/{id}` | переименовать / удалить (устройства остаются без группы) |
|
|
||||||
| POST | `/api/v1/batch/{backup\|ros_update\|fw_update}` | `{"device_ids": [...]}` и/или `{"group_id": N}` — групповая операция (задача) |
|
|
||||||
| PUT | `/api/v1/batch/channel` | `{"device_ids": [...] или "group_id": N, "channel": "..."}` — групповая смена канала (задача `set_channel`) → 202 `{"job_ids": [...]}` |
|
|
||||||
| POST | `/api/v1/batch/ros_downgrade` | `{"device_ids": [...] или "group_id": N, "target_version": "7.23.7"}` — групповой откат ROS (задача `ros_downgrade`); отдельный эндпоинт — `ros_downgrade` в `Literal` `/batch/{action}` не входит, версия там обязательна |
|
|
||||||
| GET/DELETE | `/api/v1/backups`, `/backups/download?key=` | бэкапы в бакете (фильтры `device_id`, `group`, `kind`, `date_from`, `date_to`, `q`; `refresh=1` — минуя кэш) |
|
|
||||||
| POST | `/api/v1/backups/delete` | групповое удаление файлов: `{"keys": [...]}` → `{"deleted": N, "failed": M}` |
|
|
||||||
| GET | `/api/v1/jobs`, `/jobs/{id}` | состояние задач |
|
|
||||||
| GET | `/api/v1/events`, `/events/{id}` | журнал событий (фильтры `entity_id`, `type`, `device_id`, `job_id`, `actor`, `date_from`, `date_to`, `q`; постранично `before=<evt_…>`, `limit`) |
|
|
||||||
| GET/PUT | `/api/v1/events/settings` | настройки ротации журнала `{"retention_days", "max_rows"}` и сводка (очистки журнала через API нет) |
|
|
||||||
|
|
||||||
Внешние справочники: [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/).
|
[Yandex Object Storage S3 API](https://yandex.cloud/ru/docs/storage/s3/api-ref/).
|
||||||
|
|
||||||
## Структура
|
## Безопасность
|
||||||
|
**Секреты**
|
||||||
|
- Требования — в «Конфигурации»; с небезопасными значениями приложение не стартует.
|
||||||
|
- Пароли устройств шифруются `SECRET_KEY`; секреты не пишутся ни в журнал, ни в сообщения об ошибках.
|
||||||
|
|
||||||
- `app/ros` — клиент RouterOS REST и операции (статус, бэкап, скачивание файлов, обновления)
|
**Вход в UI**
|
||||||
- `app/s3.py` — бакет; `app/security.py` — шифрование и авторизация; `app/models.py`, `app/db.py` — БД и миграции
|
- 5 неверных попыток за 10 минут с одного IP → IP заблокирован на 10 минут (429, пароль не проверяется); неверный пароль — 401.
|
||||||
- `app/services` — устройства и фильтры, группы, бэкапы, операции, задачи, фоновый опрос (`poller.py`)
|
- Блокировка по IP, а не по имени: единственного администратора нельзя заблокировать чужими попытками. За reverse-proxy все клиенты видны с IP прокси — нужен доверенный `X-Forwarded-For` (сейчас не поддерживается).
|
||||||
- `app/api` — JSON API; `app/ui` — WEB UI (Jinja2 + HTMX, `static/` со стилями, скриптом и шрифтами)
|
- Очистка журнала требует пароль; 5 неверных за 10 минут блокируют её на 10 минут.
|
||||||
- `tests` — минимальный набор; `docs/changes` — планы и итоги каждого изменения
|
- Редиректы после форм — только на локальный путь.
|
||||||
- `docs/reviews` — ревью кодовой базы (замечания и приоритеты исправлений)
|
|
||||||
|
**Данные**
|
||||||
|
- `.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** через override-файл вне репозитория: порт 8000 на хосте занят другим процессом.
|
||||||
|
|
||||||
|
## Интерфейс
|
||||||
|
- Экраны: «Устройства» (вкладки групп, фильтры, таблица, карточка «Задачи»), «Бэкапы», «Группы», «Журнал».
|
||||||
|
- Пока строки не выбраны, полоса инструментов показывает фильтры; при выборе — действия над выбранными.
|
||||||
|
- Добавление и изменение устройств, группы, откат ROS, запись журнала — в окнах поверх страницы; запасные страницы `/devices/new`, `/devices/{id}/edit` работают без JavaScript.
|
||||||
|
- Меню и выпадающие списки в едином стиле «⋯», не обрезаются таблицей и раскрываются вверх у нижнего края. Списки строятся поверх скрытого `<select>`: без JavaScript работает стандартный.
|
||||||
|
- Светлая и тёмная темы — по настройке системы.
|
||||||
|
|
||||||
|
## Тесты
|
||||||
|
33 теста, фоновый опрос выключен; стенд не нужен (временная SQLite, RouterOS и S3 — заглушки). Тест-линтер не допускает синхронных обращений к БД в `async`-коде.
|
||||||
|
```bash
|
||||||
|
python3 -m venv venv && venv/bin/pip install -r requirements.txt
|
||||||
|
venv/bin/python -m pytest -q
|
||||||
|
```
|
||||||
|
Локальный запуск без Docker: `set -a; . ./.env; set +a; venv/bin/uvicorn app.main:app --reload`.
|
||||||
|
|
||||||
## История изменений
|
## История изменений
|
||||||
|
Каждая доработка описана в `docs/changes/<номер>/`: `plan.md` — план, `summary.md` — итог.
|
||||||
|
|
||||||
Планы и итоги — в `docs/changes/`:
|
| № | Изменение | Документы |
|
||||||
- `001-initial-implementation` — первая версия.
|
|---|---|---|
|
||||||
- `002-http-support-and-fixes` — HTTP-подключение, исправления по итогам теста на реальном устройстве.
|
| 001 | Первая версия | [план](docs/changes/001-initial-implementation/plan.md) · [итог](docs/changes/001-initial-implementation/summary.md) |
|
||||||
- `003-download-via-rest-upload-from-server` — бэкап: скачивание файлов через REST, загрузка в S3 сервером.
|
| 002 | HTTP-подключение, исправления по тесту на устройстве | [план](docs/changes/002-http-support-and-fixes/plan.md) · [итог](docs/changes/002-http-support-and-fixes/summary.md) |
|
||||||
- `004-upgrade-columns-and-actions-menu` — колонки Upgrade ROS/FW, меню действий «⋯».
|
| 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) |
|
||||||
- `005-groups-and-filters` — группы устройств, фильтры устройств и бэкапов.
|
| 004 | Колонки Upgrade ROS/FW, меню действий «⋯» | [план](docs/changes/004-upgrade-columns-and-actions-menu/plan.md) · [итог](docs/changes/004-upgrade-columns-and-actions-menu/summary.md) |
|
||||||
- `006-ui-redesign` — новый интерфейс по утверждённому макету; исправлены перезагрузка после обновления FW и привязка группы при добавлении устройства.
|
| 005 | Группы устройств, фильтры устройств и бэкапов | [план](docs/changes/005-groups-and-filters/plan.md) · [итог](docs/changes/005-groups-and-filters/summary.md) |
|
||||||
- `007-fast-offline-detection` — фоновый опрос устройств (30 с), быстрый таймаут соединения, автообновление страницы.
|
| 006 | Новый интерфейс по утверждённому макету | [план](docs/changes/006-ui-redesign/plan.md) · [итог](docs/changes/006-ui-redesign/summary.md) |
|
||||||
- `008-device-note` — примечание к устройству.
|
| 007 | Фоновый опрос, быстрое обнаружение недоступности | [план](docs/changes/007-fast-offline-detection/plan.md) · [итог](docs/changes/007-fast-offline-detection/summary.md) |
|
||||||
- `009-dialog-label-alignment` — выравнивание полей в окне устройства.
|
| 008 | Примечание к устройству | [план](docs/changes/008-device-note/plan.md) · [итог](docs/changes/008-device-note/summary.md) |
|
||||||
- `010-export-show-sensitive` — `.rsc` создаётся с `show-sensitive` (секреты в файле).
|
| 009 | Выравнивание полей в окне устройства | [план](docs/changes/009-dialog-label-alignment/plan.md) · [итог](docs/changes/009-dialog-label-alignment/summary.md) |
|
||||||
- `011-dropdown-clipping` — выпадающие меню не обрезаются в узком окне.
|
| 010 | `.rsc` с `show-sensitive` | [план](docs/changes/010-export-show-sensitive/plan.md) · [итог](docs/changes/010-export-show-sensitive/summary.md) |
|
||||||
- `012-clickable-device-name` — клик по имени устройства открывает окно изменения.
|
| 011 | Выпадающие меню не обрезаются | [план](docs/changes/011-dropdown-clipping/plan.md) · [итог](docs/changes/011-dropdown-clipping/summary.md) |
|
||||||
- `013-bulk-delete-backups` — выбор файлов чекбоксами и групповое удаление бэкапов.
|
| 012 | Клик по имени устройства открывает окно изменения | [план](docs/changes/012-clickable-device-name/plan.md) · [итог](docs/changes/012-clickable-device-name/summary.md) |
|
||||||
- `014-chr-support` — поддержка CHR (нет `/system/routerboard`), строгое сравнение версий ROS, причина недоступности в таблице.
|
| 013 | Групповое удаление бэкапов | [план](docs/changes/013-bulk-delete-backups/plan.md) · [итог](docs/changes/013-bulk-delete-backups/summary.md) |
|
||||||
- `015-immutable-device-name` — имя устройства задаётся только при создании.
|
| 014 | Поддержка CHR, строгое сравнение версий ROS | [план](docs/changes/014-chr-support/plan.md) · [итог](docs/changes/014-chr-support/summary.md) |
|
||||||
- `016-unique-ids-and-event-log` — глобально уникальные ID (префикс + UUIDv7) для всех сущностей, журнал событий в БД, миграция числовых ID.
|
| 015 | Имя устройства задаётся только при создании | [план](docs/changes/015-immutable-device-name/plan.md) · [итог](docs/changes/015-immutable-device-name/summary.md) |
|
||||||
- `017-events-ui-rotation` — страница «Журнал» в UI, настройки ротации, очистка журнала с подтверждением паролем.
|
| 016 | Уникальные ID сущностей, журнал событий | [план](docs/changes/016-unique-ids-and-event-log/plan.md) · [итог](docs/changes/016-unique-ids-and-event-log/summary.md) |
|
||||||
- `018-correctness-consistency` — одиночное удаление бэкапа в UI через общий сервис `delete_many`, единая система миграций (колонки старой схемы — в `migrations.run` до миграции ID), групповая смена канала фоновыми задачами (`set_channel`).
|
| 017 | Журнал в UI: ротация, очистка с паролем | [план](docs/changes/017-events-ui-rotation/plan.md) · [итог](docs/changes/017-events-ui-rotation/summary.md) |
|
||||||
- `019-performance-scaling` — кэш списка бакета (`BACKUPS_CACHE_TTL`, «Обновить список»); обработчики без обращений к event loop — обычные функции (пул потоков FastAPI), запись статуса и тяжёлые операции с БД в фоне — через `asyncio.to_thread`; SQLite — WAL и `busy_timeout`; файловая блокировка БД — один процесс на БД.
|
| 018 | Корректность: удаление бэкапа, миграции, групповая смена канала задачами | [план](docs/changes/018-correctness-consistency/plan.md) · [итог](docs/changes/018-correctness-consistency/summary.md) |
|
||||||
- `020-custom-select-menus` — выпадающие списки (фильтры, «Группа») в стиле меню действий «⋯»: прогрессивное улучшение в JS, нативный `select` остаётся в разметке и работает без JavaScript.
|
| 019 | Производительность: кэш бакета, БД вне event loop, один процесс на БД | [план](docs/changes/019-performance-scaling/plan.md) · [итог](docs/changes/019-performance-scaling/summary.md) |
|
||||||
- `021-security-hardening` — отказ старта при небезопасных секретах (`API_TOKEN`/`SESSION_SECRET`/`ADMIN_PASSWORD`/`SECRET_KEY`), блокировка входа в UI по IP клиента, `SESSION_COOKIE_SECURE`, безопасный `next` в редиректах (`/ui/move`, `/backups/delete-many`).
|
| 020 | Выпадающие списки в стиле меню «⋯» | [план](docs/changes/020-custom-select-menus/plan.md) · [итог](docs/changes/020-custom-select-menus/summary.md) |
|
||||||
- `022-async-db-remainder` — остаток п. 9 ревью: оставшиеся синхронные обращения к БД в async-обработчиках (API, UI, `ops`, `backups.search`) — через `asyncio.to_thread`; `jobs.start_jobs` разделён на синхронную `_create_jobs` (в потоке) и `async start_jobs` (создаёт задачи и планирует их в event loop); регрессионный тест-линтер (AST-обход `app/`) не даёт синхронным обращениям к БД вернуться в async-код.
|
| 021 | Безопасность: секреты, блокировка входа, Secure-cookie, редиректы | [план](docs/changes/021-security-hardening/plan.md) · [итог](docs/changes/021-security-hardening/summary.md) |
|
||||||
- `023-ros-downgrade` — пять состояний колонки «Upgrade ROS» (`update`/`current`/`downgrade`/`check_error`/`unknown`; строгое сравнение версий больше не путает откат канала с «актуально»); осознанный откат ROS до версии канала с обязательным бэкапом — для одного устройства и группы, в UI (окно с подтверждением версии) и API (`/update/downgrade`, `/batch/ros_downgrade`).
|
| 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) |
|
||||||
|
|
||||||
|
## Отчёты ревью
|
||||||
|
- [Ревью кодовой базы 2026-09-27](docs/reviews/2026-09-27-codebase-review.md) (→ 018–021)
|
||||||
|
- [Повторное ревью 2026-09-28](docs/reviews/2026-09-28-1243-codebase-review.md) (→ 022; открыты п. 11, 12, 14–16)
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
# План: 024 — оптимизация README по образцу ipam_control
|
||||||
|
|
||||||
|
## Context
|
||||||
|
README вырос за 23 изменения: длинные абзацы, повторы (требования к секретам — в трёх разделах, генерация ключей — в двух),
|
||||||
|
история изменений — список с длинными описаниями без ссылок на документы, нет раздела об отчётах ревью, эксплуатационные сведения
|
||||||
|
(том, миграции, один процесс, WAL) разбросаны. Пользователь просит переверстать README по образцу `/opt/lvraid/claude/ipam_control/README.md`.
|
||||||
|
|
||||||
|
## Изменения (только `README.md`)
|
||||||
|
Структура образца:
|
||||||
|
1. Заголовок и абзац о проекте (+ схема компонентов).
|
||||||
|
2. **Быстрый старт** — команды и адреса UI/OpenAPI, вход.
|
||||||
|
3. **Конфигурация (`.env`)** — одна таблица «Переменная | По умолчанию | Назначение», требования к секретам — только здесь и в «Безопасности».
|
||||||
|
4. **Архитектура** — таблица слоёв и дерево каталогов.
|
||||||
|
5. **Функциональность и правила** — полужирные подзаголовки: устройства, состояния «Upgrade ROS», группы и фильтры, операции и задачи,
|
||||||
|
бэкапы, откат ROS, идентификаторы, журнал событий.
|
||||||
|
6. **Требования к устройствам**.
|
||||||
|
7. **API (`/api/v1`)** — таблица по областям + «Соглашения».
|
||||||
|
8. **Безопасность** — секреты, вход, cookie, шифрование, секреты в бэкапах.
|
||||||
|
9. **Эксплуатация** — контейнер и том, миграции, один процесс, кэш бакета, копирование БД в режиме WAL (замечание 13 повторного ревью — только документация), текущий стенд на порту 8001.
|
||||||
|
10. **Интерфейс** — короткий список.
|
||||||
|
11. **Тесты** — команда и число тестов.
|
||||||
|
12. **История изменений** — таблица «№ | Изменение | Документы» со ссылками на `plan.md`/`summary.md` (001–024).
|
||||||
|
13. **Отчёты ревью** — ссылки на `docs/reviews/` с указанием, в какие изменения они вылились.
|
||||||
|
|
||||||
|
Содержание не теряется: каждое утверждение текущего README переносится в свой раздел (сверка списком фактов при ревью).
|
||||||
|
|
||||||
|
## Исполнение
|
||||||
|
Только документация — оркестратор сам (навык change-flow: документация без исполнителя). Код не меняется, стенд не пересобирается.
|
||||||
|
|
||||||
|
## Проверка
|
||||||
|
- Все ссылки на `docs/changes/*/plan.md|summary.md` и `docs/reviews/*.md` указывают на существующие файлы (скрипт).
|
||||||
|
- Сверка фактов: переменные `.env` из `app/config.py`, эндпоинты из `app/api/v1.py`, число тестов из `pytest --collect-only`.
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Итоги: 024 — оптимизация README по образцу ipam_control
|
||||||
|
|
||||||
|
## Сделано
|
||||||
|
- README переверстан по структуре `/opt/lvraid/claude/ipam_control/README.md`: «Быстрый старт», «Конфигурация» (одна таблица),
|
||||||
|
«Архитектура» (таблица слоёв и дерево каталогов), «Функциональность и правила» (полужирные подзаголовки, таблица состояний «Upgrade ROS»),
|
||||||
|
«Требования к устройствам», «API» (таблица по областям и «Соглашения»), «Безопасность», «Эксплуатация», «Интерфейс», «Тесты»,
|
||||||
|
«История изменений» (таблица со ссылками на `plan.md`/`summary.md` 001–024), «Отчёты ревью».
|
||||||
|
- Убраны повторы: требования к секретам и команды генерации — по одному месту; длинные описания истории изменений — в документах изменений.
|
||||||
|
- Добавлено: раздел «Отчёты ревью», копирование БД в режиме WAL (замечание 13 повторного ревью, только документация), текущий стенд на порту 8001,
|
||||||
|
коды ошибок API (включая 401), синхронность смены канала одного устройства и `refresh`.
|
||||||
|
|
||||||
|
## Проверено
|
||||||
|
- Все 51 ссылка (49 на `docs/changes/*`, 2 на `docs/reviews/*`) указывают на существующие файлы.
|
||||||
|
- Переменные `.env` сверены с `app/config.py`, эндпоинты — с `app/api/v1.py`, число тестов (33) — с `pytest --collect-only`.
|
||||||
|
- Код не менялся, стенд не пересобирался.
|
||||||
|
|
||||||
|
## Оговорки
|
||||||
|
- Документы изменений в этом проекте названы `plan.md`/`summary.md` (в образце — `PLAN.md`/`SUMMARY.md`); регистр сохранён как есть.
|
||||||
Reference in new issue
Block a user