ros_control: централизованное управление парком MikroTik RouterOS
Control Server (FastAPI) с WEB UI (Jinja2 + HTMX) и JSON API для группового администрирования устройств RouterOS 7 через REST. - Устройства: список, статус (модель, канал, версии ROS/FW, uptime, доступные обновления), примечания, пароли шифруются (Fernet); фоновый опрос каждые 30 с и быстрое обнаружение недоступности (таймаут соединения 4 с). - Группы устройств и фильтры (устройства, резервные копии). - Резервные копии: .backup и .rsc (show-sensitive) создаются через REST, скачиваются сервером и загружаются в S3 (Yandex Object Storage). - Обновление ROS/FW и выбор канала, групповые операции задачами. - Интерфейс по утверждённому макету: светлая/тёмная темы, окна, шрифты IBM Plex локально. Docker Compose, SQLite в томе, минимальный набор тестов. - Планы и итоги каждого изменения — в docs/changes/. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
commit
4c1841b61b
85 files changed
+3579
No files matched your search
@@ -0,0 +1,76 @@
|
||||
# План: ros_control — централизованное управление парком MikroTik RouterOS
|
||||
|
||||
## Context
|
||||
|
||||
Нужно приложение для группового администрирования множества устройств MikroTik RouterOS через API и WEB UI. Источник требований — `ros_control.svg` (draw.io): архитектура, список функций, ссылки на API.
|
||||
|
||||
Из схемы:
|
||||
- **Control Server** связан двусторонне с **Local Metadata DB** (DB transactions), **S3 Bucket** (Yandex Object Storage: `ListObjectsV2`, `GetObject`, `PutObject`, `DeleteObject`) и **Admin Dashboard**.
|
||||
- Dashboard ↔ Control Server: JSON payload, UI Forms (ввод), JSON payload render (вывод).
|
||||
- Control Server ↔ **Endpoint API Server (on device)** = RouterOS REST API (`GET/PATCH/PUT/POST/DELETE`, JSON), N устройств.
|
||||
|
||||
Функции dashboard: список устройств и управление списком (добавить, изменить, удалить); статус (модель, канал обновлений, версия ROS, версия FW, uptime, время запроса бэкапа); бэкап по каждому устройству (бинарный `.backup` без шифрования + текстовый `.rsc`, загрузка в S3, удаление с устройства, скачивание из бакета, список бэкапов в бакете); выбор канала обновлений; обновление ROS до актуальной версии канала (перезагрузка после скачивания); обновление FW (перезагрузка сразу после обновления).
|
||||
|
||||
Принятые решения: Python FastAPI + Jinja2/HTMX, SQLite (SQLAlchemy), пароли устройств шифруются (Fernet), вход в UI по логину/паролю, API — Bearer-токен, Docker Compose для развёртывания + `venv` в корне проекта для разработки.
|
||||
|
||||
## Артефакты процесса (по CLAUDE.md проекта)
|
||||
|
||||
- `docs/changes/001-initial-implementation/plan.md` — копия этого плана (создаётся первым шагом реализации)
|
||||
- `docs/changes/001-initial-implementation/summary.md` — итоги по выполненному (создаётся последним шагом)
|
||||
- `README.md` — описание, запуск, конфигурация, ссылки на API; обновляется при каждом изменении
|
||||
- Тесты — минимальный набор (см. Verification)
|
||||
|
||||
## Структура проекта
|
||||
|
||||
```
|
||||
app/
|
||||
main.py # FastAPI, роутинг, lifespan
|
||||
config.py # настройки из env (pydantic-settings)
|
||||
db.py, models.py # SQLAlchemy + SQLite: users, devices, backups, jobs
|
||||
security.py # Fernet для паролей устройств, хэш пароля админа, API-токен
|
||||
ros/client.py # httpx-клиент RouterOS REST (basic auth, verify_tls per device)
|
||||
ros/operations.py # status, backup, update channel/install, firmware upgrade
|
||||
s3.py # boto3: list/get(presigned)/put(presigned)/delete, endpoint Yandex
|
||||
services/ # devices, backups, updates, jobs (фоновые задачи + batch)
|
||||
api/v1.py # JSON API
|
||||
ui/routes.py, templates/, static/ # Jinja2 + HTMX
|
||||
tests/ # минимальный набор
|
||||
Dockerfile, docker-compose.yml, requirements.txt, .env.example, README.md
|
||||
docs/changes/001-initial-implementation/{plan,summary}.md
|
||||
```
|
||||
|
||||
## Ключевые технические решения
|
||||
|
||||
1. **RouterOS REST** (`https://<host>/rest/...`, ROS ≥ 7.1, сервис `www-ssl`). Для самоподписанных сертификатов — флаг `verify_tls` на устройство. Ограничение: только ROS 7.
|
||||
2. **Статус** — параллельный опрос (`asyncio.gather` + семафор): `/rest/system/resource` (модель, версия ROS, uptime), `/rest/system/routerboard` (current/upgrade firmware), `/rest/system/package/update` (channel, installed/latest version). Недоступное устройство отображается как offline, не ломает список.
|
||||
3. **Бэкап**:
|
||||
- `POST /rest/system/backup/save` (`dont-encrypt=yes`) и `POST /rest/export` (`.rsc`);
|
||||
- REST не отдаёт содержимое крупных файлов, поэтому **устройство само загружает файлы в S3** через `/tool/fetch` (`http-method=put upload=yes src-path=…`) по **presigned PUT URL**, выданному Control Server. Ключи S3: `backups/<device>/<timestamp>.backup|.rsc`;
|
||||
- после успешной загрузки — `DELETE /rest/file/<id>` на устройстве; время запроса пишется в БД;
|
||||
- список — `ListObjectsV2` по префиксу, скачивание — presigned GET (редирект).
|
||||
4. **Обновление ROS**: `POST /rest/system/package/update/set {channel}` → `check-for-updates` → `install` (устройство скачивает пакет и перезагружается само — соответствует требованию «перезагрузка после скачивания»).
|
||||
5. **Обновление FW**: `POST /rest/system/routerboard/upgrade` → опрос до `current-firmware == upgrade-firmware` → `POST /rest/system/reboot`.
|
||||
6. **Долгие операции** — таблица `jobs` + фоновые задачи; UI опрашивает статус через HTMX. Групповые операции: эндпоинты принимают список `device_ids`, выполняются параллельно с ограничением конкурентности.
|
||||
7. **API v1**: `/api/v1/devices` (CRUD), `/devices/{id}/status`, `/devices/{id}/backups` (POST/GET), `/backups/download`, `/devices/{id}/update/channel` (PUT), `/devices/{id}/update/install`, `/devices/{id}/firmware/upgrade` (POST), `/jobs/{id}`. UI использует те же сервисы.
|
||||
8. **Безопасность**: пароли устройств не возвращаются в API/UI; ключ шифрования и S3-ключи — только из env; UI — сессия после логина; API — Bearer-токен.
|
||||
|
||||
## Этапы реализации
|
||||
|
||||
1. Артефакты: `plan.md`, каркас проекта, `venv`, `requirements.txt`, `.env.example`.
|
||||
2. Модели БД, security (Fernet, логин, токен), конфиг.
|
||||
3. RouterOS-клиент и операции статуса; CRUD устройств; список со статусами (API + UI).
|
||||
4. S3-модуль; бэкап (2 формата, загрузка с устройства, очистка), список и скачивание.
|
||||
5. Каналы обновлений, обновление ROS, обновление FW; jobs и групповые операции.
|
||||
6. Docker/compose, README, минимальные тесты, `summary.md`.
|
||||
|
||||
## Риски / проверить на реальном устройстве
|
||||
|
||||
- `/tool/fetch` с `http-method=put upload=yes` на целевой версии ROS (основной вариант). Запасной: устройство отправляет файл на эндпоинт Control Server.
|
||||
- Синхронные REST-вызовы (`fetch`, `install`) могут упереться в таймаут — использовать `duration`/асинхронные задачи и опрос состояния.
|
||||
- Сертификат `www-ssl` на устройствах и доступность 443 от Control Server; доступность S3 (`storage.yandexcloud.net`) с устройств.
|
||||
|
||||
## Verification
|
||||
|
||||
- **Автотесты (минимум, ~4)**: шифрование/расшифровка пароля; разбор статуса RouterOS (mock httpx); сценарий бэкапа с мокнутыми RouterOS и S3 (порядок: save → export → fetch → delete file → запись в БД); авторизация API (401 без токена).
|
||||
- **Ручная проверка**: RouterOS CHR (ROS 7) в лаборатории + тестовый бакет Yandex Object Storage: добавить устройство → статус в списке → бэкап (оба файла в бакете, на устройстве удалены) → скачивание → смена канала → обновление ROS → обновление FW.
|
||||
- `docker compose up` — приложение поднимается, UI на `:8000`, `/docs` показывает OpenAPI.
|
||||
@@ -0,0 +1,32 @@
|
||||
# Итоги: 001 — первая версия ros_control
|
||||
|
||||
## Сделано
|
||||
|
||||
- Control Server на FastAPI: JSON API `/api/v1` (Bearer-токен) и WEB UI (Jinja2 + HTMX, вход по логину/паролю).
|
||||
- Local Metadata DB (SQLite): устройства с зашифрованными (Fernet) паролями, кэш статуса, бэкапы, задачи.
|
||||
- RouterOS REST-клиент: статус (модель, канал, ROS, FW, uptime), бэкап, смена канала, обновление ROS и FW.
|
||||
- Бэкап: `.backup` (без шифрования) + `.rsc` → устройство само загружает в S3 по presigned PUT → файлы удаляются с устройства.
|
||||
- S3 (Yandex Object Storage): список, скачивание (presigned GET), удаление.
|
||||
- Долгие и групповые операции через таблицу `jobs` с ограничением параллелизма (`MAX_CONCURRENCY`).
|
||||
- Docker Compose, `venv` для разработки, README, 4 автотеста.
|
||||
|
||||
## Проверено
|
||||
|
||||
- `pytest`: 4 теста (шифрование, разбор статуса, порядок шагов бэкапа с моками RouterOS/S3, авторизация API и отсутствие пароля в ответах) — пройдены.
|
||||
- Дымовой прогон сервера: редирект на логин, вход, добавление недоступного устройства (offline + причина), задача бэкапа завершается `failed` с понятной ошибкой, страница бэкапов открывается.
|
||||
|
||||
## НЕ проверено (нужно устройство и бакет)
|
||||
|
||||
- Реальная работа с RouterOS 7 и Yandex Object Storage. Главный риск — `/tool/fetch` с `http-method=put upload=yes` на целевой версии ROS.
|
||||
- Обновления ROS/FW (поведение при обрыве соединения на перезагрузке реализовано по документации).
|
||||
- Сборка и запуск Docker-образа не выполнялись.
|
||||
|
||||
## Отклонения от плана
|
||||
|
||||
- Администратор UI берётся из переменных окружения (`ADMIN_USER`/`ADMIN_PASSWORD`), таблицы `users` нет — достаточно для одного администратора.
|
||||
- Кнопки в строке устройства и групповые кнопки используют `hx-include` вместо общей формы (иначе значения каналов разных устройств смешивались).
|
||||
|
||||
## Ограничения
|
||||
|
||||
- Только RouterOS 7.x. Один администратор. HTMX-скрипт лежит локально (`app/ui/static`), внешние CDN не нужны.
|
||||
- Долгие задачи выполняются внутри процесса сервера; при перезапуске незавершённые помечаются `failed`.
|
||||
@@ -0,0 +1,15 @@
|
||||
# План: 002 — HTTP-подключение к устройствам и исправления по итогам первого теста
|
||||
|
||||
## Контекст
|
||||
Тест на реальном RouterOS (RB4011, ROS 7.24.4): статус не считывался, бэкап не создавался (`ConnectError`).
|
||||
Причины: у устройства включён только `www` (порт 80, HTTP), а `www-ssl` (443) выключен; в приложении был указан порт 8729 (`api-ssl` — бинарный протокол, не REST).
|
||||
|
||||
## Решение (выбрано пользователем)
|
||||
Поддержка HTTP на уровне устройства: флаг `use_tls` (по умолчанию `true`; `false` = HTTP, только для доверенной сети).
|
||||
|
||||
## Шаги
|
||||
1. Поле `use_tls` в модели `Device` + миграция существующей БД (`ALTER TABLE`).
|
||||
2. `RosClient`: схема `http/https` по флагу; в ошибку подключения добавлен URL и причина.
|
||||
3. API (`DeviceIn/Patch/Out`) и UI-форма: флаг «HTTPS» + предупреждение о пароле открытым текстом.
|
||||
4. Исправление: RouterOS может отдавать имена файлов не в UTF-8 — декодирование с заменой вместо падения.
|
||||
5. Перевод тестового устройства на порт 80/HTTP, проверка статуса и бэкапа, диагностика прав пользователя.
|
||||
@@ -0,0 +1,19 @@
|
||||
# Итоги: 002 — HTTP-подключение и исправления
|
||||
|
||||
## Сделано
|
||||
- Флаг `use_tls` у устройства (API, UI, БД + автомиграция колонки). HTTP — только для доверенной сети.
|
||||
- Сообщения об ошибках подключения содержат URL и тип ошибки (раньше — пустое `ConnectError:`).
|
||||
- Исправлено падение на не-UTF-8 ответах RouterOS (`GET /file`).
|
||||
- Устройство `msk-home` переведено на порт 80/HTTP: статус считывается (модель, канал, ROS, FW, uptime).
|
||||
|
||||
## Найдено при тесте
|
||||
- Порт 8729 — `api-ssl`, не REST. REST: `www-ssl` (443) или `www` (80).
|
||||
- Пользователь `apicontrol` в группе `write` с `!ftp,!policy`: `backup/save`, `export` → `not enough permissions (9)`;
|
||||
`/tool/fetch` → `cannot open file: permission denied`. Нужна политика `ftp` (при необходимости `policy`).
|
||||
- `ros_latest`/`ros_update_status` пусты, пока не выполнена проверка обновлений (`check-for-updates`).
|
||||
|
||||
## Не проверено
|
||||
Бэкап целиком (ждёт выдачи прав пользователю), загрузка в S3 через `/tool/fetch`, обновления ROS/FW.
|
||||
|
||||
## Дополнение
|
||||
Права `apicontrol` были причиной ошибок `not enough permissions`: после выдачи `full` и пересоздания подключения `backup/save` заработал (см. 003). Загрузка через `/tool/fetch` оказалась неприменима — заменена в 003.
|
||||
@@ -0,0 +1,16 @@
|
||||
# План: 003 — скачивание бэкапов через REST, загрузка в S3 сервером
|
||||
|
||||
## Контекст
|
||||
Схема 001 (устройство само грузит в S3 по presigned PUT через `/tool/fetch`) не работает: на ROS 7.24 `fetch` поддерживает upload только по FTP/SFTP.
|
||||
Решение пользователя: Control Server сам скачивает файлы с устройства **через REST-канал** и сам загружает их в S3. Устройство S3 не касается.
|
||||
|
||||
## Проверено на устройстве (RB4011, ROS 7.24.4)
|
||||
- `GET /rest/file/<id>` для файлов >4 КБ не отдаёт `contents` (231 КБ .backup, 75 КБ .rsc).
|
||||
- `POST /rest/file/read {file, offset, chunk-size}` возвращает блок `data` — подходит для .rsc.
|
||||
- `POST /rest/execute {script, as-string}` с `:convert to=base64` возвращает блок бинарного файла без искажений в JSON.
|
||||
|
||||
## Шаги
|
||||
1. `ros/operations.py`: `download_file()` — чтение блоками (32 КБ) через `execute` + base64 в файл, проверка итогового размера; убрать `upload_file()` (fetch).
|
||||
2. `s3.py`: `upload_file()` (PutObject, boto3 в потоке); убрать presigned PUT.
|
||||
3. `services/ops.py::run_backup`: создать файлы → скачать во временный каталог → загрузить в S3 → удалить файлы с устройства (в т.ч. при сбое); временный каталог всегда удаляется.
|
||||
4. Тест бэкапа, README, summary; проверка на `msk-home`.
|
||||
@@ -0,0 +1,25 @@
|
||||
# Итоги: 003 — скачивание через REST, загрузка в S3 сервером
|
||||
|
||||
## Сделано
|
||||
- Бэкап: Control Server создаёт `.backup` и `.rsc` через REST → скачивает их блоками по 32 КБ во временный каталог
|
||||
(`POST /rest/execute` + `:convert to=base64`) → загружает в S3 (`PutObject`) → удаляет файлы с устройства
|
||||
(в том числе при сбое). Временный каталог удаляется всегда. Устройство S3 не касается.
|
||||
- Удалены `/tool/fetch`-загрузка и presigned PUT. Проверка целостности: сравнение размера на устройстве и скачанного.
|
||||
- Экспорт: пример из документации (`compact`) на ROS 7 даёт побайтно тот же результат, поэтому запрос остался без `compact`.
|
||||
|
||||
## Проверено на RB4011 (ROS 7.24.4) и бакете Yandex
|
||||
- Бэкап `MSK_Home`: `.backup` 231 237 Б и `.rsc` 74 924 Б в бакете, размеры совпадают с устройством,
|
||||
сигнатура `.backup` = `88 AC A1 B1`, `.rsc` начинается с заголовка экспорта; на устройстве `ros_control-*` не осталось.
|
||||
- Скачивание из бакета через API (`/backups/download`) работает. `pytest`: 4 из 4.
|
||||
|
||||
## Причина прежних ошибок доступа (002)
|
||||
Права применились только после пересоздания подключения к устройству (пользователь `full`);
|
||||
сама смена политик группы без этого не помогала.
|
||||
|
||||
## Ограничения
|
||||
- `.rsc` создаётся без `show-sensitive`: пароли и ключи в нём скрыты (для полного восстановления служит `.backup`).
|
||||
- Скорость скачивания ограничена размером блока и задержкой REST (около 8 запросов на 230 КБ); для очень больших файлов
|
||||
можно увеличить `chunk_size`.
|
||||
|
||||
## Не проверено
|
||||
Обновление ROS и FW (перезагрузка устройства), групповые операции над несколькими устройствами.
|
||||
@@ -0,0 +1,46 @@
|
||||
# План: 004 — колонки Upgrade ROS / Upgrade FW и меню действий «⋯» в UI
|
||||
|
||||
## Context
|
||||
|
||||
Дашборд показывает только текущие версии; доступные для обновления версии видны лишь мелким текстом под ROS/FW и для ROS почти никогда не заполнены. Причины:
|
||||
- `latest-version` в `/rest/system/package/update` появляется только после `check-for-updates` — приложение его при опросе статуса не вызывает (`app/ros/operations.py::get_status`).
|
||||
- `ros_version` из `system/resource` имеет вид `7.24.4 (stable)`, а `latest-version` — `7.24.4`; прямое сравнение в шаблоне всегда даёт «отличаются».
|
||||
- В строке устройства пять кнопок в ряд (Статус, Бэкап, ROS, FW, Изменить) — таблица перегружена.
|
||||
|
||||
Цель: отдельные колонки **Upgrade ROS** и **Upgrade FW** с версией, до которой можно обновиться, и все действия строки — в выпадающем меню под иконкой «⋯».
|
||||
|
||||
## Изменения
|
||||
|
||||
**1. Статус: проверка обновлений** — `app/ros/operations.py::get_status`
|
||||
- Перед чтением `system/package/update` выполнять `POST system/package/update/check-for-updates` (таймаут ~30 с). Ошибка проверки (нет интернета на устройстве, таймаут) не ломает опрос: остаётся то, что вернул `GET`, в статус пишется `ros_check_error`.
|
||||
- В статус добавить `ros_installed` (`installed-version`, без суффикса «(stable)») — именно с ним сравнивается `ros_latest`.
|
||||
- `get_status(c, check_updates=True)`: параметр нужен только чтобы тест мог отключить проверку.
|
||||
- `set_channel` уже вызывает `refresh_status`, значит после смены канала доступная версия пересчитается сама. Инструмент `install_ros_update` (`check-for-updates` + `install`) не меняется.
|
||||
|
||||
**2. Шаблон `app/ui/templates/_devices.html`**
|
||||
- Колонки: `…, ROS, Upgrade ROS, FW, Upgrade FW, …`. Убрать мелкие подсказки «доступна X» из колонок ROS/FW.
|
||||
- Upgrade ROS: `ros_latest`, если он есть и `!= ros_installed` → выделенный бейдж с версией; равен → «✓ актуально»; нет данных → «—» (в `title` — `ros_check_error`).
|
||||
- Upgrade FW: `fw_upgrade`, если `!= fw_current` → бейдж; равен → «✓ актуально»; нет RouterBOARD (CHR) → «—».
|
||||
- Колонка «Действия» → компактная колонка с кнопкой `⋯`; меню на `<details class="menu"><summary aria-label="Действия">⋯</summary>…</details>` (без внешних библиотек). Пункты те же, что были кнопками: Обновить статус, Бэкап, Обновить ROS, Обновить FW, Изменить. Атрибуты `hx-post` / `hx-target` / `hx-confirm` переносятся как есть.
|
||||
- `colspan` пустой строки 9 → 10.
|
||||
|
||||
**3. Стили и поведение** — `app/ui/static/style.css`, `app/ui/templates/base.html`
|
||||
- CSS: `.menu` (relative), `.menu-list` (absolute, справа, тень, границы под тёмную и светлую темы, `z-index`), пункты-кнопки на всю ширину, опасные (ROS/FW) — акцентным цветом; `.badge` для Upgrade-версий (цвет `--warn`), «актуально» — `--ok`.
|
||||
- ~6 строк JS в `base.html`: закрывать открытое меню по клику вне его, по `Esc` и после выбора пункта (нужно, потому что действия с `hx-target="#jobs"` таблицу не перерисовывают и меню иначе останется открытым).
|
||||
|
||||
**4. Артефакты проекта (CLAUDE.md)**
|
||||
- `docs/changes/004-upgrade-columns-and-actions-menu/plan.md` (копия этого плана) и `summary.md` в конце.
|
||||
- `README.md`: описание колонок и меню, строка в «Историю изменений».
|
||||
- Тесты: без новых; в существующем `test_status_parsing` (`tests/test_app.py`) добавить обработку `POST …/check-for-updates` в моке и проверку `ros_installed`/`ros_latest`.
|
||||
|
||||
## Проверка
|
||||
|
||||
- `pytest` (4 теста).
|
||||
- `docker compose up -d --build --force-recreate`, затем `POST /api/v1/devices/1/refresh`: в статусе появились `ros_installed`, `ros_latest`; время опроса с проверкой обновлений замерить — при заметном замедлении оставить `check_updates` только в кнопке «Обновить статус», а не при каждом опросе.
|
||||
- Визуально в браузере (`http://localhost:8000`): колонки Upgrade ROS/FW, меню «⋯» открывается/закрывается (клик вне, Esc, выбор пункта), пункты работают (Статус, Бэкап, ROS/FW — с подтверждением, Изменить), светлая и тёмная тема, узкое окно.
|
||||
- Обновления ROS/FW фактически **не запускаются** (перезагрузка устройства); проверяется только отображение и подтверждение.
|
||||
|
||||
## Риски
|
||||
|
||||
- `check-for-updates` ходит с устройства на серверы MikroTik: без интернета вернётся ошибка/таймаут → «—» с подсказкой, статус остаётся `online`.
|
||||
- Проверка добавляет секунды к опросу каждого устройства; параллелизм ограничен `MAX_CONCURRENCY` (см. пункт проверки выше).
|
||||
@@ -0,0 +1,19 @@
|
||||
# Итоги: 004 — колонки Upgrade ROS / Upgrade FW и меню действий «⋯»
|
||||
|
||||
## Сделано
|
||||
- Статус устройства теперь включает проверку обновлений (`check-for-updates`): `ros_latest`, `ros_installed`, `ros_check_error`.
|
||||
Сбой проверки (нет интернета на устройстве) не делает устройство offline — в колонке «—» с подсказкой.
|
||||
- UI: колонки **Upgrade ROS** и **Upgrade FW** — бейдж `↑ версия` (есть обновление), `✓ актуально`, либо `—` (нет данных / нет RouterBOARD).
|
||||
- Исправлено сравнение версий ROS: раньше `7.24.4 (stable)` (resource) сравнивалось с `7.24.4` (latest) и всегда «отличалось».
|
||||
Теперь используется `installed-version`.
|
||||
- UI: пять кнопок строки заменены выпадающим меню под иконкой «⋯» (Обновить статус, Создать бэкап, Обновить ROS, Обновить FW, Изменить).
|
||||
Меню закрывается по клику вне, по Esc и после выбора пункта; без внешних библиотек.
|
||||
|
||||
## Проверено
|
||||
- `pytest`: 4 из 4 (тест статуса дополнен проверкой `check-for-updates` и `ros_installed`).
|
||||
- На `msk-home` (ROS 7.24.4): опрос с проверкой обновлений 1,2–1,5 с; `ros_latest = ros_installed = 7.24.4`, `fw_current = fw_upgrade = 7.24.4` → в обеих колонках «✓ актуально».
|
||||
- Рендер шаблона на тестовых данных: ветки «есть обновление ROS и FW», «нет интернета на устройстве», «CHR без RouterBOARD» отображаются верно.
|
||||
|
||||
## Не проверено
|
||||
- Внешний вид и поведение меню в браузере (на машине разработки браузера нет): позиционирование, светлая/тёмная тема, узкое окно.
|
||||
- Фактический запуск обновлений ROS/FW не выполнялся (перезагрузка устройства) — проверялось только отображение.
|
||||
@@ -0,0 +1,56 @@
|
||||
# План: 005 — группы устройств и фильтры (список устройств, список бэкапов)
|
||||
|
||||
## Context
|
||||
|
||||
Парк устройств растёт, а дашборд показывает всё одним списком, и в бакете бэкапы тоже идут одним списком. Нужно:
|
||||
1. **Группы** — новая сущность: устройство относится к одной группе (или ни к одной); таблица показывает устройства выбранной группы.
|
||||
2. **Фильтры** на странице устройств и на странице бэкапов.
|
||||
|
||||
Принятые решения (по формулировке «относятся к одной группе»): у устройства **одна** группа (`group_id`, может быть пустой = «Без группы»); переключение групп — вкладки над таблицей; по умолчанию — «Все». Фильтры работают на серверной стороне.
|
||||
|
||||
## Изменения
|
||||
|
||||
**1. Модель и БД** — `app/models.py`, `app/db.py::_migrate`
|
||||
- Таблица `groups` (`id`, `name` уникальное, ≤64 символов, кириллица и пробелы допустимы — в ключи S3 имя группы не попадает).
|
||||
- `devices.group_id` (nullable). Миграция существующей БД: `ALTER TABLE devices ADD COLUMN group_id INTEGER` рядом с миграцией `use_tls`.
|
||||
- Внешние ключи в SQLite не включены, поэтому при удалении группы устройства открепляются явно (`UPDATE devices SET group_id = NULL`), устройства не удаляются.
|
||||
|
||||
**2. Сервисы**
|
||||
- `app/services/groups.py` (новый): `list_groups` (с числом устройств), `create/rename/delete_group`.
|
||||
- `app/services/devices.py`: `group_id` в `create_device`/`update_device` (для открепления допустимо явное `None`); `filter_devices(devices, group, q, status, updates, channel)` — общий для UI и API. `group`: пусто = все, `none` = без группы, иначе id. `q` — по имени, адресу, модели. `status`: online/offline. `updates`: `ros` / `fw` / `any` / `none` (по `ros_latest != ros_installed`, `fw_upgrade != fw_current` из кэша статуса). `channel` — канал обновлений.
|
||||
- `app/services/backups.py` (новый): `list_backups(device, group, kind, date_from, date_to, q)` поверх `s3.list_backups` (`app/s3.py:48`); имя устройства берётся из ключа `backups/<устройство>/<файл>`, группа — по имени устройства; добавляет поля `device`, `group`, `kind` (`backup`/`rsc`). Бэкапы удалённых устройств остаются в «Все», группа у них «—». Даты — по `LastModified`, UTC, включительно.
|
||||
|
||||
**2. API** — `app/api/v1.py`
|
||||
- `GET/POST /api/v1/groups`, `PATCH/DELETE /api/v1/groups/{id}`.
|
||||
- `group_id` в `DeviceIn`/`DevicePatch`/`DeviceOut`; `GET /devices?group=&q=&status=&updates=&channel=`.
|
||||
- `GET /backups?device_id=&group=&kind=&date_from=&date_to=&q=` через `services/backups.py`.
|
||||
- Групповые операции: `BatchIn` принимает `device_ids` **или** `group_id` (операция над всей группой).
|
||||
|
||||
**3. UI — устройства** — `app/ui/routes.py`, `dashboard.html`, `_devices.html`
|
||||
- Вкладки групп над таблицей: «Все (N)», «Без группы (N)», по каждой группе (N) — обычные ссылки `/?group=…` с сохранением остальных фильтров (полная перезагрузка, URL можно сохранить в закладки; счётчики всегда актуальны).
|
||||
- Форма фильтров `#dev-filters` (поля с префиксом `f_`, чтобы не пересекаться с `channel` у селектов канала): поиск `f_q`, статус, обновления, канал + скрытое `f_group`. `hx-get="/ui/devices"`, `hx-trigger="input delay:300ms, change"`, обновляет `#devices`; ответ содержит заголовок `HX-Push-Url` с адресом страницы, поэтому фильтр переживает перезагрузку. Кнопка «Сбросить».
|
||||
- Фильтр сохраняется после действий, возвращающих таблицу (обновить статус, смена канала, групповые): у этих кнопок `hx-include="#dev-filters"`, маршруты разбирают `f_*` из формы. Чекбокс «выбрать все» затрагивает только видимые строки.
|
||||
- Перемещение в группу: отдельная форма `#move-form` (обычный POST + редирект, поэтому счётчики вкладок обновляются) — выпадающий список групп + кнопка для отмеченных устройств (чекбоксы связаны атрибутом `form="move-form"`). Плюс поле «Группа» в форме создания/изменения устройства (`device_form.html`).
|
||||
- Страница **«Группы»** (`/groups`, пункт в навигации `base.html`): список с числом устройств, создание, переименование, удаление (с подтверждением; устройства остаются, становятся «Без группы»).
|
||||
|
||||
**4. UI — бэкапы** — `backups.html`, маршрут `/backups`
|
||||
- Обычная GET-форма (закладки, без HTMX; список всё равно запрашивается из S3): группа, устройство, тип (`.backup`/`.rsc`), дата с/по, поиск по имени файла. Селекты и даты отправляются по `change`, поиск — по Enter; кнопка «Сбросить».
|
||||
- Новые колонки: Устройство, Группа, Тип.
|
||||
|
||||
**5. Артефакты проекта (CLAUDE.md)**
|
||||
- `docs/changes/005-groups-and-filters/plan.md` (копия) и `summary.md`; README: группы, фильтры, новые API-маршруты, строка в «Историю изменений».
|
||||
- Тесты (минимум, +2): (а) группы + `filter_devices`: фильтр по группе/«без группы»/статусу/обновлениям, удаление группы открепляет устройства; (б) `services/backups.list_backups` с подменённым `s3.list_backups`: фильтры по группе, типу и датам.
|
||||
|
||||
## Проверка
|
||||
|
||||
- `pytest` (4 существующих + 2 новых).
|
||||
- `docker compose up -d --build --force-recreate` (миграция БД должна сохранить `msk-home`).
|
||||
- API (curl): создать группу, привязать `msk-home`, `GET /devices?group=…`, `?group=none`, `?status=offline`; `GET /backups?group=…&kind=rsc`; групповой бэкап по `group_id` **не запускать без вашего подтверждения** (создаёт файлы на устройстве) — проверить только валидацию.
|
||||
- UI (curl с cookie): `/?f_group=…&f_q=…` даёт нужные строки и вкладки со счётчиками; запрос с заголовком `HX-Request` к `/ui/devices` возвращает таблицу и заголовок `HX-Push-Url`; `/backups?…` фильтрует реальные файлы в бакете.
|
||||
- **Ограничение:** на машине нет браузера, живое поведение HTMX (обновление по вводу, вид вкладок) увижу только по серверным ответам; внешний вид и живой фильтр нужно посмотреть вам.
|
||||
|
||||
## Риски
|
||||
|
||||
- Без FK-каскадов целостность групп обеспечивается кодом (`delete_group`), не БД.
|
||||
- Фильтры «обновления» опираются на кэш статуса: у не опрошенных устройств поля пусты — они не попадают в «есть обновление».
|
||||
- Имя устройства берётся из ключа S3: устройство, переименованное после бэкапа, потеряет связь с группой в списке бэкапов (старые файлы останутся под прежним именем).
|
||||
@@ -0,0 +1,29 @@
|
||||
# Итоги: 005 — группы устройств и фильтры
|
||||
|
||||
## Сделано
|
||||
- **Группы**: таблица `device_groups`, у устройства одна группа (`group_id`, пусто = «Без группы»). Миграция существующей БД автоматическая.
|
||||
Удаление группы не удаляет устройства — они открепляются (FK в SQLite не включены, откреплением занимается код).
|
||||
- **UI, устройства**: вкладки групп со счётчиками («Все», «Без группы», каждая группа) — таблица показывает устройства выбранной группы;
|
||||
колонка «Группа»; фильтры: поиск (имя/адрес/модель), статус, обновления (ROS/FW/любое/актуально), канал.
|
||||
Фильтр применяется на лету (HTMX), URL страницы обновляется (`HX-Push-Url`), фильтр сохраняется после действий над таблицей.
|
||||
«Обновить статус» без выбора теперь обновляет устройства текущего представления, а не весь парк.
|
||||
- **UI, группы**: страница «Группы» (создать, переименовать, удалить); перенос отмеченных устройств в группу; поле «Группа» в форме устройства.
|
||||
- **UI, бэкапы**: фильтры — группа, устройство, тип (`.backup`/`.rsc`), период дат, поиск по имени файла; колонки «Устройство», «Группа», «Тип».
|
||||
- **API**: `/api/v1/groups` (CRUD); `group_id` у устройства; фильтры `GET /devices?group&q&status&updates&channel`
|
||||
и `GET /backups?device_id&group&kind&date_from&date_to&q`; групповые операции по `group_id` в `/batch/*`.
|
||||
|
||||
## Проверено
|
||||
- `pytest`: 6 из 6 (добавлено 2: группы + фильтры устройств, фильтры бэкапов).
|
||||
- Миграция на существующей БД: `MSK_Home` сохранился. API: создание/переименование/удаление групп, дубликат (400), несуществующая группа (404),
|
||||
все фильтры устройств и бэкапов на реальном бакете, валидация батча по группе (запуск реальных операций не выполнялся).
|
||||
- UI (curl): вкладки и счётчики, фильтры, заголовок `HX-Push-Url`, перенос устройства между группами, защита от open redirect в `next`,
|
||||
страницы «Группы», «Бэкапы» с фильтрами.
|
||||
- Тестовые группы («Москва», «Резерв») после проверки удалены, устройство осталось без группы.
|
||||
|
||||
## Не проверено
|
||||
- Живое поведение в браузере (на машине разработки браузера нет): обновление таблицы по вводу, вид вкладок и форм фильтров.
|
||||
|
||||
## Ограничения
|
||||
- Группа определяется по имени устройства в ключе S3 (`backups/<устройство>/…`): бэкапы удалённого или переименованного устройства
|
||||
отображаются с группой «—» и попадают в «Без группы».
|
||||
- Фильтр по обновлениям использует кэш статуса; у ещё не опрошенных устройств поля пусты.
|
||||
@@ -0,0 +1,33 @@
|
||||
# План: 006 — редизайн UI (макет на ревью)
|
||||
|
||||
Статус: **макет принят заказчиком, внедрено** (см. `summary.md`). Ниже — исходное предложение. Макет: https://claude.ai/artifact/QdhgQWbQEWRYtBoAWMjGq6 (7 листов: разбор текущего UI, компоненты и уровни, «Устройства» в трёх состояниях, «Бэкапы», «Группы»).
|
||||
|
||||
## Проблемы текущего UI
|
||||
1. Над таблицей три-четыре панели подряд (вкладки групп, фильтры, действия над выбранными, перенос в группу) выглядят одинаково — уровни не отделены.
|
||||
2. Разные формы контролов: вкладки-пилюли (r16), кнопки и поля (r5), бейджи (r10), пунктирная «вкладка».
|
||||
3. Действия над выбранными видны всегда; «Применить канал» и «Переместить» — двухшаговые.
|
||||
4. 12 колонок, выпадающий список канала в каждой строке.
|
||||
|
||||
## Предложение
|
||||
- **Одна форма контролов**: высота 36, радиус 8; три вида кнопок (primary — одна на экран, secondary, ghost); список-фильтр выглядит как secondary с шевроном; активный фильтр — контур и тон акцента. Метки: высота 22, радиус 6. Вкладки — подчёркивание, не кнопки.
|
||||
- **Уровни экрана**: 1 приложение (навигация) → 2 страница (заголовок, счётчики, главное действие) → 3 область (вкладки групп) → 4 инструменты таблицы → 5 данные → задачи отдельной карточкой.
|
||||
- **Инструменты таблицы — одна полоса двух состояний**: нет выбора — фильтры; выбрано N — действия над выбранными (Создать бэкап, Обновить статус, Обновление ▾, Канал ▾, В группу ▾, Снять выбор). ROS/FW — в меню с пояснением про перезагрузку.
|
||||
- **Таблица**: модель уходит второй строкой в ячейку устройства; канал только показывается (смена — через «Канал ▾» и меню строки); меню «⋯» сгруппировано разделителями.
|
||||
- «Управление группами» — ссылка справа от вкладок и пункт верхнего меню (не вкладка). Бэкапы: та же полоса фильтров, действия в строке — «Скачать» (иконка) и «⋯».
|
||||
|
||||
## Вопросы к ревью
|
||||
1. Убрать выбор канала из строк? 2. Слить «Модель» в ячейку устройства? 3. Действия заменяют фильтры при выборе — ок?
|
||||
4. «Управление группами» в двух местах — оставить оба? 5. Подтверждение обновлений: окно браузера или модальное со списком устройств? 6. Тёмная тема — сохраняем (палитра на тех же токенах).
|
||||
|
||||
## Внедрение (после утверждения макета)
|
||||
1. Токены и компоненты в `app/ui/static/style.css` (кнопки, поля, метки, вкладки, карточка, меню), убрать разнобой радиусов.
|
||||
2. `base.html`, `dashboard.html`: заголовок страницы, вкладки-подчёркивание, единая полоса инструментов (фильтры ⇄ действия при выборе — HTMX + небольшой JS для счётчика выбора).
|
||||
3. `_devices.html`: слияние колонки «Модель», канал — текстом, сгруппированное меню строки; меню «Обновление / Канал / В группу» вместо отдельных форм (маршрут `/ui/move` сохраняется).
|
||||
4. `backups.html`, `groups.html`, `device_form.html` — на тех же компонентах.
|
||||
5. Проверка: pytest, рендер шаблонов и `curl`; внешний вид — на ревью у заказчика (браузера на машине разработки нет). Итоги в `summary.md`, README — в «Историю изменений».
|
||||
|
||||
|
||||
## Принято к внедрению (дополнение)
|
||||
- Стиль макета переносится в точности; окна добавления/изменения устройства и групп проектируются в том же стиле (листы «Окно: …» в макете).
|
||||
- Два бага, найденные заказчиком: (1) перезагрузка после обновления FW должна идти по записи в журнале «Firmware upgraded successfully, please reboot for changes to take effect!»; (2) при добавлении устройства нельзя привязать его к группе.
|
||||
- Причины и подробный план внедрения — `docs/changes/006-ui-redesign/summary.md` (раздел «Баги») и утверждённый план в истории работ.
|
||||
@@ -0,0 +1,36 @@
|
||||
# Итоги: 006 — перенос утверждённого макета в UI и два бага
|
||||
|
||||
## Баги
|
||||
1. **Прошивка не перезагружала устройство.** `upgrade_firmware` ждал `current-firmware == upgrade-firmware`, но `current-firmware`
|
||||
меняется только после перезагрузки (на `OZ_Dacha`: `7.22.3` при `upgrade 7.24.4`, задача №12 упала по таймауту).
|
||||
Теперь триггер — новая запись журнала «Firmware upgraded successfully, please reboot for changes to take effect!»
|
||||
(журнал читается целиком и фильтруется на сервере: REST-фильтр `?message=` по точному тексту ничего не вернул).
|
||||
Если запись новее последней загрузки (по `system/clock` − `uptime`) уже есть, то повторная команда не нужна — сразу перезагрузка.
|
||||
2. **Нельзя привязать создаваемое устройство к группе.** Форма и маршрут работали, но групп в экземпляре не было (тестовые я удалил
|
||||
после проверки), а создать группу из формы было нельзя. Теперь в окне устройства в списке «Группа» есть пункт «+ Новая группа…»:
|
||||
группа создаётся и привязывается в одной транзакции (при ошибке лишней группы не остаётся). Если сценарий был другим — сообщить.
|
||||
|
||||
## Интерфейс (перенос макета)
|
||||
- Дизайн-система в `app/ui/static/style.css`: токены и размеры макета 1:1 (контролы h36 r8, метки h22 r6, панели r12), тёмная тема на тех же токенах.
|
||||
Шрифты IBM Plex Sans/Mono лежат локально (`static/fonts`, лицензия OFL) — внешний CDN не нужен.
|
||||
- Страницы: «Устройства» (заголовок со счётчиками, вкладки групп, карточка с полосой «фильтры ⇄ действия над выбранными», таблица, задачи),
|
||||
«Группы», «Резервные копии» (фильтры на `.field`, «Скачать» и «⋯» в строке), вход.
|
||||
- Окна (`<dialog>` + HTMX): добавление/изменение устройства, создание/переименование группы; ошибки — внутри окна, успех — обновление страницы.
|
||||
Запасные страницы `/devices/new`, `/devices/{id}/edit` сохранены.
|
||||
- Логика выбора строк, меню, окон и фильтров — `static/app.js` (без библиотек, кроме htmx).
|
||||
|
||||
## Проверено
|
||||
- `pytest`: 8 из 8 (добавлены: перезагрузка по записи журнала — оба сценария; добавление устройства из окна с группой и «новой группой», окна групп).
|
||||
- Реальный браузер (headless Chromium): вход → окно «Новое устройство» → «Новая группа…» → вкладка «QA группа 1» → выбор строк и панель действий,
|
||||
меню «Обновление / В группу», меню строки с подменю каналов, живой фильтр без перезагрузки и «Сбросить (N)», страницы «Группы» и «Бэкапы»;
|
||||
ошибок JS/консоли нет; тестовые данные удалены (в БД остались `MSK_Home`, `OZ_Dacha`, групп нет).
|
||||
- По скриншотам исправлены расхождения с макетом: пробел в «Выбрано: N», двойные границы карточек, «системная» за краем таблицы,
|
||||
закрытие подменю каналов, склейка «Сбросить (N)», серые «—» у offline-устройства, знак «+» в списке групп.
|
||||
|
||||
## Не проверено
|
||||
- Реальная перезагрузка по «Обновить FW» на `OZ_Dacha` — нужно подтверждение (перезагрузит роутер); логика проверена на моках и на реальном журнале (чтение).
|
||||
- Соответствие макету на других размерах окна и в тёмной теме (проверялось при 1440 px в светлой).
|
||||
|
||||
## Отличия от макета
|
||||
- Календарь в фильтре дат — нативный `<input type="date">`: формат зависит от локали браузера (в макете ISO).
|
||||
- Фильтры вкладок и списков в приложении работают; в макете они статичны.
|
||||
@@ -0,0 +1,14 @@
|
||||
# План: 007 — быстрое обнаружение недоступности устройств
|
||||
|
||||
## Контекст
|
||||
Раньше опроса по расписанию не было: статус обновлялся только по действию пользователя, а недоступное (не отвечающее) устройство определялось по общему таймауту 30 с.
|
||||
|
||||
## Решение (согласовано: оба пункта, интервал 30 с)
|
||||
1. **Раздельные таймауты**: соединение — 4 с (`ROS_CONNECT_TIMEOUT`), чтение — 30 с (долгие операции не затронуты).
|
||||
2. **Фоновый опрос** каждые 30 с (`POLL_INTERVAL`, 0 — выключить): параллельно, не более `MAX_CONCURRENCY`.
|
||||
- Лёгкий опрос (один запрос `system/resource`): online/offline, uptime, версия ROS.
|
||||
- Полный опрос: если устройство было offline или не опрошено (версии могли измениться), и раз в `UPDATE_CHECK_INTERVAL` (30 мин) — с проверкой обновлений.
|
||||
3. **UI**: таблица и счётчики в заголовке обновляются сами каждые 15 с (только чтение БД); пауза при выбранных строках, открытом меню/окне, фокусе в фильтрах и скрытой вкладке; история браузера не засоряется.
|
||||
|
||||
## Шаги
|
||||
Клиент RouterOS (connect-таймаут) → `ops.poll_device` → `services/poller.py` и запуск в `lifespan` → автообновление в `app.js` (OOB-подзаголовок) → тест, README, итоги.
|
||||
@@ -0,0 +1,19 @@
|
||||
# Итоги: 007 — быстрое обнаружение недоступности
|
||||
|
||||
## Сделано
|
||||
- Таймаут соединения 4 с отдельно от таймаута чтения 30 с (`RosClient`, `ROS_CONNECT_TIMEOUT`).
|
||||
- Фоновый опрос (`app/services/poller.py`, запускается в `lifespan`): период `POLL_INTERVAL=30` с, лёгкий и полный режимы
|
||||
(`ops.poll_device`); сбой одного устройства не останавливает остальных, при остановке приложения задача корректно отменяется.
|
||||
- Автообновление страницы «Устройства» каждые 15 с без перезагрузки и без потери выбора/открытых меню (`static/app.js`, `HX-Push-Url` при автообновлении не отправляется).
|
||||
- Новые настройки в `.env.example`: `ROS_CONNECT_TIMEOUT`, `POLL_INTERVAL`, `UPDATE_CHECK_INTERVAL` (значения по умолчанию заданы в коде — существующий `.env` менять не нужно).
|
||||
|
||||
## Проверено
|
||||
- `pytest`: 9 из 9 (добавлен тест опроса: недоступно → возврат с полным опросом → лёгкий опрос обновляет uptime, остальные поля сохраняются; в тестах опрос выключен).
|
||||
- Вживую: статусы обновляются каждые 30 с без обращений пользователя; адрес без ответа определяется за 4,0 с (было до 30 с);
|
||||
новое недоступное устройство помечается offline за ≤24 с без ручного опроса; в браузере страница сама показала «1 offline» через 15 с,
|
||||
история не засорена, ошибок JS нет. Тестовые устройства удалены.
|
||||
|
||||
## Ограничения
|
||||
- Худшее время обнаружения: `POLL_INTERVAL` + `ROS_CONNECT_TIMEOUT` (≈34 с) плюс до 15 с до обновления страницы.
|
||||
- Во время перезагрузки устройства (обновление ROS/FW) оно кратковременно отображается как offline — это ожидаемо.
|
||||
- Интервал 30 с при большом парке даёт по одному лёгкому запросу на устройство в 30 с; нагрузку ограничивает `MAX_CONCURRENCY`.
|
||||
@@ -0,0 +1,8 @@
|
||||
# План: 008 — примечание к устройству
|
||||
|
||||
Запрос: комментарий владельца к листу «Окно: добавление устройства» в макете — «добавь место, где можно будет написать примечание».
|
||||
|
||||
## Решение
|
||||
- Макет: в окне устройства под полем «Название» — многострочное поле «Примечание» (необязательно). Прод повторяет макет.
|
||||
- Приложение: `devices.note` (TEXT, до 500 символов, пробелы по краям обрезаются, пусто = нет), миграция БД, поле в форме окна и запасной страницы, `note` в API (`POST/PATCH/GET /api/v1/devices`), примечание показывается подсказкой (`title`) над именем устройства в таблице — внешний вид таблицы не меняется.
|
||||
- Тест: примечание через окно (обрезка пробелов, пусто → null).
|
||||
@@ -0,0 +1,14 @@
|
||||
# Итоги: 008 — примечание к устройству
|
||||
|
||||
## Сделано
|
||||
- Макет (лист «Окно: добавление устройства», версия 4) и приложение: поле «Примечание» под названием устройства (textarea, до 500 символов).
|
||||
- БД: колонка `devices.note` (автомиграция), сервис (`_clean_note`: обрезка, лимит, пусто → `null`), API (`note` в создании, правке и ответах; ошибка 400 при превышении лимита), UI (окно и запасная страница).
|
||||
- В таблице примечание видно во всплывающей подсказке над именем устройства.
|
||||
|
||||
## Проверено
|
||||
- `pytest`: 9 из 9 (тест создания устройства из окна расширен проверкой примечания).
|
||||
- В браузере окно с полем «Примечание» выглядит как в макете; примечание сохраняется, видно в окне изменения и подсказке; через API правится, очищается пустой строкой, длиннее 500 символов отклоняется (400).
|
||||
- Временное устройство удалено; данные пользователя (группа PRIVATE, `MSK_Home`, `OZ_Dacha`) не затронуты.
|
||||
|
||||
## Не проверено / замечания
|
||||
- Примечание не выводится в самой таблице (чтобы не менять утверждённый вид) — только подсказка; если нужно видеть его постоянно (например, отдельной строкой или колонкой), это отдельное решение по макету.
|
||||
@@ -0,0 +1,5 @@
|
||||
# План: 009 — выравнивание полей в окне изменения устройства
|
||||
|
||||
Баг (скриншот `alignment_ui_device_edit_bug.png`): в окне изменения поле «Пароль» ниже поля «Пользователь».
|
||||
Причина: подсказка «пусто — не менять» была отдельным flex-элементом и добавляла строку только над полем пароля. В макете подсказка стоит в той же строке, что и подпись.
|
||||
Решение: подпись и подсказка в одном `<span>` (`_device_form.html`), `.lbl small { font-size: inherit }` (`style.css`).
|
||||
@@ -0,0 +1,5 @@
|
||||
# Итоги: 009 — выравнивание полей в окне устройства
|
||||
|
||||
- Подсказки у «Название», «Примечание», «Пароль» теперь в строке подписи (как в макете); поля в рядах выровнены.
|
||||
- Проверено в браузере: верх полей «Пользователь» и «Пароль» совпадает (488/488), «Адрес» и «Порт» тоже (413/413); в окнах добавления и изменения.
|
||||
- Побочно: меню строки корректно открывается и при узком окне (1000 px), клип в прокручиваемой таблице не возникает.
|
||||
@@ -0,0 +1,4 @@
|
||||
# План: 010 — экспорт .rsc с show-sensitive
|
||||
|
||||
Запрос: добавить `show-sensitive` в создание .rsc, чтобы из копии восстанавливались все сущности, где используется пароль.
|
||||
Решение: `POST /rest/export {"file": …, "show-sensitive": ""}` (`ros/operations.py::create_backup_files`). Проверено на устройстве до внедрения: без параметра в .rsc 0 секретов, с параметром — 23 (password, secret, preshared-key, private-key). Требуется политика `sensitive` у пользователя устройства (при `full` есть). Тест: запрос экспорта содержит `show-sensitive`; README — предупреждение о секретах в бакете.
|
||||
@@ -0,0 +1,7 @@
|
||||
# Итоги: 010 — экспорт .rsc с show-sensitive
|
||||
|
||||
- `.rsc` создаётся с `show-sensitive`: пароли, ключи и секреты попадают в файл.
|
||||
- Проверено на RB4011 (ROS 7.24.4): без параметра 0 секретных полей, с параметром 23; пробные файлы удалены с устройства. `pytest`: 9 из 9.
|
||||
- Ранее созданные `.rsc` в бакете — без секретов; новые бэкапы — с секретами.
|
||||
- Безопасность: файлы `.rsc` (как и `.backup` без шифрования) теперь содержат секреты открытым текстом — доступ к бакету и ссылкам скачивания должен быть ограничен.
|
||||
- Полный бэкап через S3 после изменения не запускался (файл с секретами не создавался в бакете); логика проверена на устройстве и моках.
|
||||
@@ -0,0 +1,4 @@
|
||||
# План: 011 — обрезание выпадающих меню в узком окне
|
||||
|
||||
Баг (скриншот `dropdown-menu_ui_bug.png`): при узком окне таблица получает горизонтальную прокрутку (`.table-wrap { overflow-x: auto }`), и меню «⋯» обрезается её границей.
|
||||
Решение (`static/app.js`): при открытии список позиционируется от кнопки в координатах окна (`position: fixed`), не выходит за края окна и раскрывается вверх, если снизу нет места; при прокрутке и изменении размера следует за кнопкой, а если кнопка ушла за пределы окна — закрывается. Действует для всех меню (строка, групповые действия, группы, бэкапы).
|
||||
@@ -0,0 +1,6 @@
|
||||
# Итоги: 011 — обрезание выпадающих меню
|
||||
|
||||
- Меню не обрезаются прокручиваемой таблицей; при нехватке места снизу раскрываются вверх; остаются в пределах окна.
|
||||
- Проверено в браузере при 1440×900, 620×800 и 620×430: панель целиком в окне, четыре контрольные точки принадлежат панели (не обрезана), в низком окне раскрытие вверх; после прокрутки меню остаётся открытым у кнопки. Полный сценарий интерфейса (окна, выбор строк, групповые меню, фильтры, страницы) проходит без ошибок JS.
|
||||
- Во время отладки исправлена собственная ошибка: первый вариант закрывал меню по любой прокрутке, а браузер сдвигает таблицу при фокусе на кнопке — меню закрывалось сразу после открытия в узком окне.
|
||||
- Автотеста нет (поведение проверяется только в браузере).
|
||||
@@ -0,0 +1,4 @@
|
||||
# План: 012 — кликабельное имя устройства
|
||||
|
||||
Запрос: по клику на имя устройства открывается модальное окно изменения устройства.
|
||||
Решение: имя в таблице — ссылка `<a class="dev-link" href="/devices/{id}/edit" hx-get="/ui/dialog/device/{id}">`: с JS открывает окно (то же, что пункт меню «Изменить устройство»), без JS — запасную страницу. Внешне имя остаётся прежним (600, цвет текста); при наведении — цвет акцента и подчёркивание. Подсказка: примечание, а если его нет — «Изменить устройство».
|
||||
@@ -0,0 +1,5 @@
|
||||
# Итоги: 012 — кликабельное имя устройства
|
||||
|
||||
- Имя устройства открывает окно изменения; работает мышью и с клавиатуры (Tab → Enter), окно закрывается по Esc; адрес страницы не меняется. Пункт «Изменить устройство» в меню «⋯» сохранён.
|
||||
- Автообновление таблицы не мешает: пока окно открыто, оно на паузе.
|
||||
- Проверено в браузере (окно открывается с данными нужного устройства, Enter, Esc, ошибок JS нет); `pytest`: 9 из 9 (в существующий тест добавлена проверка ссылки).
|
||||
Reference in new issue
Block a user