# Синхронизация статусов 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` по дизайну — пользователь должен включить его вручную.