105 lines
11 KiB
Markdown
105 lines
11 KiB
Markdown
# Синхронизация статусов bank_profile и material
|
||
|
||
## Определения
|
||
|
||
### Локальные статусы (SQLite)
|
||
| Таблица | Поле | Значения |
|
||
|---|---|---|
|
||
| `bank_profiles` | `status` | `"active"` \| `"off"` |
|
||
| `materials` | `status` | `"active"` \| `"off"` (enum `MaterialStatus::Active` / `MaterialStatus::Off`) |
|
||
|
||
### Серверные статусы (REST API)
|
||
| Сущность | Поле | Значения |
|
||
|---|---|---|
|
||
| bank_profile | `state` | `"enabled"` \| `"disabled"` |
|
||
| material | `state` | `"enabled"` \| `"disabled"` |
|
||
|
||
### Маппинг
|
||
| Локально | Сервер |
|
||
|---|---|
|
||
| `active` | `enabled` |
|
||
| `off` | `disabled` |
|
||
|
||
---
|
||
|
||
## Инвариант
|
||
**Локальное состояние — единственный источник истины.** Сервер всегда отражает локаль. При любых расхождениях побеждает локальная БД, и сервер дотягивается до неё (а не наоборот).
|
||
|
||
Pull с сервера (`syncBankProfilesFromServer`) используется только для **создания новых записей** (новые bank_profile / новые материалы) и привязки серверных UUID. Статусы существующих записей с сервера в локаль никогда не пишутся.
|
||
|
||
---
|
||
|
||
## Основные правила
|
||
|
||
### 1. Смена онлайн-статуса устройства
|
||
- **Device → OFFLINE:** bank_profile этого устройства выключается **и локально, и на сервере** (`PATCH bank_profile/{id}/false`). Материалы локально не трогаем — они остаются в том состоянии, в котором были (см. правило 4-А ниже).
|
||
- **Device → ONLINE:** локальный статус **не меняется**. На сервер пушится текущее локальное состояние bank_profile и всех его материалов, если оно расходится с серверным.
|
||
|
||
### 2. Периодическая синхронизация
|
||
- Каждые **60 секунд** запускается фоновая задача, которая сравнивает локальные статусы bank_profile и материалов с серверными и отправляет `PATCH` только для расходящихся.
|
||
- Локаль не трогается. Цель — компенсировать сетевые сбои, перезапуски сервера и т.п.
|
||
|
||
### 3. Обработка ошибок при включении
|
||
- Когда пользователь включает bank_profile или material и `PATCH .../true` падает (network error / HTTP не-2xx / `success:false`):
|
||
- локально оставляем `off`;
|
||
- показываем пользователю сообщение об ошибке (см. правило 6 про UI).
|
||
- При неудачном **выключении** локально оставляем `off`; рассинхрон с сервером устранит минутный таймер из правила 2.
|
||
|
||
### 4. Реконсиляция материалов после включения профиля
|
||
- Срабатывает **только при включении** профиля (`PATCH bank_profile/{id}/true`), при выключении — нет (см. правило 4-B про гард ниже).
|
||
- Отдельной реконсиляции после PATCH не делаем — расхождения материалов подхватит 60s-таймер из правила 2 (в худшем случае — через минуту).
|
||
- Допустима оптимизация: «дёрнуть» периодический sync сразу после успешного включения профиля, чтобы не ждать.
|
||
|
||
---
|
||
|
||
## Дополнительные правила и инварианты
|
||
|
||
### 4-A. Материалы при device → OFFLINE
|
||
- При переходе device → OFFLINE правило 1 выключает только профиль. Материалы локально не трогаем — после повторного включения профиля они вернутся к работе без лишних кликов пользователя.
|
||
- В UI материалы при `bank_profile.status == "off"` отображаются **приглушённо** или с пометкой «профиль выключен», но их `status` в БД остаётся как есть.
|
||
|
||
### 4-B. Гард на пуш материала при выключенном профиле
|
||
- На сервере действует инвариант: `material.state ≤ bank_profile.state`. Если профиль `disabled`, сервер принудительно держит материалы в `disabled`.
|
||
- Чтобы не получить бесконечный diff-цикл, **при любом пуше статуса материала** (toggle в UI, периодический sync, follow-up после toggle профиля) проверяем локальный `bank_profile.status`. Если профиль `off` — `PATCH` материала **не отправляем**, локальный статус не меняем.
|
||
- Этот гард уже частично реализован в `AccountSettingsWindow.cpp:556`; нужно поднять его на уровень `NetworkService`, чтобы покрыть все пути пуша.
|
||
|
||
### 5. Дебаунс ADB-флапа
|
||
- ADB может мерцать (USB-хаб, спящий экран, Doze). Без дебаунса каждое мерцание сработает как device → OFFLINE.
|
||
- **Online — мгновенно** (0 секунд). Как только устройство видно — сразу триггерим логику device → ONLINE.
|
||
- **Offline — с задержкой 30 секунд.** Устройство считается офлайн только после того, как ADB **не** показывает его как `device` непрерывно в течение 30 секунд. Если за этот период оно вернулось — считаем что флапа не было.
|
||
|
||
### 6. Стартовый порядок (избегание гонки)
|
||
- `DeviceScreener` эмитит сигналы `deviceWentOnline(deviceId)` / `deviceWentOffline(deviceId)` **только при смене** статуса. На «никогда не видел в этой сессии» никаких событий нет.
|
||
- `NetworkService` стартует 60s-таймер только **после первого успешного прохода** `DeviceScreener` (сигнал `firstPassDone`). До этого никаких offline-эвентов не обрабатывает.
|
||
- Это исключает ситуацию «на старте все устройства выглядят офлайн → массово выключаем профили».
|
||
|
||
### 7. Сериализация PATCH-ов
|
||
- Mutex на пару `(deviceId, bankProfileId)`: внутри одного устройства+профиля PATCH-операции последовательны, между разными — могут идти параллельно.
|
||
- Защищает от гонки: одновременно прилетели device-event, юзер-toggle и периодический sync для одного профиля → они выстраиваются в очередь.
|
||
|
||
### 8. UI-обработка ошибок (overlay-pattern)
|
||
- При любом PATCH статуса (профиля или материала) UI показывает оверлей «Сохранение…» поверх тоггла.
|
||
- Тоггл **двигается только после ответа сервера**:
|
||
- успех → переходит в новое положение, оверлей скрывается;
|
||
- ошибка → остаётся в исходном положении, оверлей сменяется сообщением «Ошибка: …», локальный статус не меняется.
|
||
- Сейчас частично реализовано через `hideOverlay` в `AccountSettingsWindow.cpp:559`. Применить тот же pattern в `DeviceWidget`, `BankSetupWizard` и всех других местах с toggle.
|
||
|
||
### 9. Индикация неработающей синхронизации
|
||
- В `NetworkService` ведём счётчик `m_consecutiveSyncFailures`, инкрементируем при каждом проваленном тике (правило 2) или провале `GET /current_profile`. Сбрасываем при первом успехе.
|
||
- При превышении порога (например, **3 тика подряд**) эмитим сигнал в `MainWindow`, который показывает индикатор «Нет связи с сервером, последняя успешная синхронизация: HH:MM:SS».
|
||
- При следующем успехе индикатор скрывается.
|
||
|
||
### 10. Параллелизм и ограничения нагрузки
|
||
- В одном тике периодического sync может образоваться большой объём расхождений (5 устройств × 6 профилей × 4 материала = до 120 PATCH).
|
||
- Параллелизм с **лимитом 4-8 одновременных PATCH**; mutex из правила 7 соблюдается.
|
||
- Флаг `m_syncInProgress`: если предыдущий тик ещё не завершился, новый тик **пропускается**. Это предотвращает накопление очереди.
|
||
|
||
### 11. Поведение при провале `GET /current_profile`
|
||
- Если периодический `GET /current_profile` упал (таймаут/сеть/5xx) — тик **пропускается тихо**, локаль не трогаем, никакие PATCH не шлём.
|
||
- Сам факт неудачи учитывается в счётчике из правила 9.
|
||
|
||
---
|
||
|
||
## Связанные существующие правила (контекст)
|
||
- Toggle материала, когда `bank_profile.status == "off"`, **не идёт на сервер** — сохраняется только локально (см. правило 4-B). Серверная синхронизация произойдёт после включения профиля и следующего 60s-тика.
|
||
- После `BankSetupWizard` `bank_profile` остаётся `off` по дизайну — пользователь должен включить его вручную. |