Уникальные ID сущностей, журнал событий и его UI с ротацией и очисткой

Глобально уникальные ID (docs/changes/016):
- у устройств, групп, задач, резервных копий и записей журнала ID вида
  <префикс>_<uuid7> (dev_, grp_, job_, bkp_, evt_): типы не пересекаются,
  внутри типа ID не повторяются и сортируются по времени;
- миграция БД v1 заменяет числовые ID с пересчётом ссылок (копия файла БД
  перед миграцией, сверка числа строк, одна транзакция);
- резервная копия = пара файлов с одним bkp_ ID, файлы в S3 получают
  метаданные backup-id/device-id; синхронизация метаданных с бакетом;
- журнал событий в БД (events): создание/изменение/удаление, задачи, бэкапы,
  смена online/offline, вход в UI; API чтения GET /api/v1/events.

Журнал в UI, ротация и очистка (docs/changes/017):
- страница «Журнал»: фильтры, подгрузка «Показать ещё», окно записи;
- настройки ротации (срок и максимум записей) хранятся в БД (схема v2),
  ротация при старте, раз в час и после сохранения настроек;
- очистка журнала только через окно с паролем пользователя, блокировка
  после 5 неверных попыток, остаётся запись о факте очистки; через API
  очистки нет.

Тесты: 20 из 20.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
ayurishchevandClaude Sonnet 5 committed 2026-09-19 17:01:52 +03:00
1 parent 1e5fb17755
commit 26dd1f8be4
43 files changed
+1677 -157

No files matched your search

@@ -0,0 +1,27 @@
# Итоги: 016 — глобально уникальные ID и журнал событий
## Сделано
- **ID** (`app/ids.py`): у каждой сущности — `<префикс>_<uuid7>`: `dev_` устройство, `grp_` группа, `job_` задача, `bkp_` резервная копия, `evt_` запись журнала.
Типы не пересекаются по построению (префикс); внутри типа UUIDv7 = время (мс) + 12-битный счётчик + 62 случайных бита, строго возрастает внутри процесса, сортировка по ID = по времени создания;
ID не переиспользуются (в отличие от прежних `max(id)+1`); PRIMARY KEY в каждой таблице; ID чужого типа в пути/запросе → 404 с пояснением, числовые ID больше не существуют.
- **Журнал событий** (`events`, `app/services/events.py`): каждая запись со своим `evt_` ID, ссылкой на ID сущности, актором (`ui:<пользователь>` / `api` / `poller` / `system`) и данными.
Типы: `device.created|updated|deleted|online|offline`, `group.created|renamed|deleted`, `devices.moved`, `job.created|started|done|failed`, `backup.created|done|failed|imported|deleted`, `auth.login|failed`, `system.migrated`.
В `online/offline` пишутся только смены состояния; пароли в журнал не попадают. API: `GET /api/v1/events` (фильтры `entity_id`, `type`, `device_id`, `job_id`, `before`, `limit`), `GET /api/v1/events/{id}`. UI-страницы журнала нет (нужен макет).
- **Резервные копии**: одна копия = пара файлов с одним `bkp_` ID; строка хранит `job_id` задачи и снимок имени устройства; файлы загружаются в S3 с метаданными `backup-id` и `device-id`;
файлы бакета без строки получают ID автоматически (при старте и при просмотре списка), копии без файлов помечаются удалёнными (`deleted_at`), строка и ID остаются в истории. В API списка файлов есть `backup_id`, в UI ID виден в подсказке.
- **Миграция** (`app/migrations.py`, `PRAGMA user_version` 0→1): числовые ID заменены везде с пересчётом ссылок, порядок записей сохранён (время в ID берётся из `created_at`), выполняется одной транзакцией, сверяет число строк, перед этим создаёт копию БД (`ros_control.db.bak-<метка>` в томе).
- **API/UI**: все ID — строки; в таблице «Задачи» колонка «ID» показывает последние 8 символов (полный — в подсказке) — единственное видимое отличие от утверждённого макета.
## Проверено
- `pytest`: 17 из 17 (12 прежних обновлены, 5 новых: уникальность 50 тыс. ID и типы, миграция из старой схемы, журнал событий, API журнала и валидация ID, связь бэкапов с бакетом).
- Репетиция на копии боевой БД, затем боевая миграция: 4 группы, 6 устройств, 32 задачи, 23 бэкапа — число строк, связи «устройство → группа → задачи → бэкапы» и порядок сохранены; 70 ID, все уникальны; повторный запуск ничего не меняет.
- Сверка с бакетом: 4 копии (08:26–09:38), чьих файлов уже нет в бакете, помечены удалёнными; все 24 файла в бакете связаны с 12 активными копиями.
- Живой журнал: создание тестового устройства (актор `api`), уход в offline (актор `poller`), удаление; числовой ID и ID чужого типа → 404. Полный сценарий в браузере на мигрированных данных без ошибок JS.
- S3: объект загружается с метаданными, значения совпадают. Yandex Object Storage возвращает ключи в виде `Backup-Id`, `Device-Id` — при чтении сравнивать без учёта регистра. Тестовый объект удалён.
## Оговорки
- **Ломающее изменение**: старые curl-скрипты с числовыми ID не работают. Откат — из `ros_control.db.bak-*` в томе и предыдущего коммита.
- Уже лежащие в бакете объекты метаданных S3 не получили (переписывать объекты не стали): связь с ними — через БД.
- Полный бэкап на реальном устройстве после изменения не запускался (создаёт файлы, `.rsc` с секретами) — метаданные S3 проверены отдельным тестовым объектом; первый штатный бэкап проверит их на реальных файлах.
- Журнал не ротируется (растёт только при изменениях, опрос пишет лишь смены состояния); срок хранения/очистка — при необходимости отдельной доработкой.
- Пробные записи проверки (создание/удаление тестового устройства и группы) остались в журнале как обычные события.