diff --git a/docs/dushanbe_screens.md b/docs/dushanbe_screens.md
new file mode 100644
index 0000000..aecd536
--- /dev/null
+++ b/docs/dushanbe_screens.md
@@ -0,0 +1,206 @@
+# Dushanbe — экраны и дампы
+
+Чек-лист экранных дампов для банка **Dushanbe City Bank** (`appCode: dushanbe` /
+`dushanbe_city_bank`). Используется, когда выходит новая версия приложения и нужно
+понять, **какие экраны переснять**.
+
+## Как пользоваться при выходе новой версии
+
+1. Завести папку `assets/dushanbe/<новая_версия>/` (например `assets/dushanbe/3.2.42/`).
+2. Пройти по чек-листу ниже и снять XML+скриншот каждого экрана на устройстве с новой
+ версией. Дампы кладутся в новую папку под теми же именами.
+3. Сверить новый дамп с картинкой ниже (на ней — **самый свежий из имеющихся** дампов).
+4. Если вёрстка/тексты разошлись с парсером — обновить `android/dushanbe/ScreenXmlParser.cpp`
+ и при необходимости завести новый трек в `android/dushanbe/AppVersionTrack.h`.
+
+## Версионирование
+
+Парсер Dushanbe версионируется: `android/dushanbe/AppVersionTrack.h`, треки
+**Legacy** (< 3.2.33) / **V3_2_33** (3.2.33 .. < 3.2.38) / **V3_2_38** (>= 3.2.38, по
+умолчанию). Скрипты выставляют трек один раз: `setVersionTrack(trackForVersion(app.appVersion))`.
+Новый трек добавляется **только** если вёрстка реально разошлась с V3_2_38 — иначе хватает
+свежих эталонных дампов в `assets/dushanbe/<версия>/`.
+
+**Картинка под каждым экраном — самый свежий доступный дамп** (приоритет
+`3.2.41` → `3.2.39` → `3.2.33` → корень `assets/dushanbe/`). Подпись «Текущий дамп»
+показывает, из какой версии реально взята картинка. Если эталонного дампа ещё нет —
+экран помечен ⚠️ и его надо снять в первую очередь.
+
+---
+
+## 1. Мастер установок (`BankSetupWizard`)
+
+Цепочка: `runPinCheck → runProfileCheck → runCardCheck` (`views/bank/BankSetupWizard.cpp`).
+
+### 1.1. Пин-код / вход (`LoginAndCheckAccountsScript`)
+
+#### Экран запуска — `launch_screen`
+Сплэш/загрузка приложения сразу после старта.
+- [ ] Снять под новую версию
+- Текущий дамп: **3.2.33**
+-
+
+#### Экран ввода PIN — `pin_code` ⚠️
+Цифровая клавиатура ввода пин-кода. Детектор `isPinCodeScreen()` ещё не реализован
+(`ScreenXmlParser.h:45` — TODO «после снятия XML дампов экранов»).
+- [ ] **Эталонный дамп ещё не снят — приоритет при следующем съёме**
+- Текущий дамп: _нет_
+
+#### Главный экран — `home_screen`
+«Главная»: нижняя навигация (Переводы/Сервисы/История) + карточка баланса «TJS\n<дата>».
+- [ ] Снять под новую версию
+- Текущий дамп: **3.2.33**
+-
+
+### 1.2. Профиль (`GetProfileInfoScript`)
+
+#### Боковое меню / профиль — `side_menu`
+Шторка профиля: ФИО, телефон, версия/сборка. Из неё парсятся данные профиля.
+- [ ] Снять под новую версию
+- Текущий дамп: **3.2.41**
+-
+
+### 1.3. Карты (`GetCardInfoScript`)
+
+#### Список карт — `cards_screen`
+Экран «Мои карты» со списком карт (last numbers, баланс).
+- [ ] Снять под новую версию
+- Текущий дамп: **3.2.33**
+-
+
+#### Детали карты — `card_detail`
+Заголовок «Карта»: полный номер, баланс, держатель.
+- [ ] Снять под новую версию
+- Текущий дамп: **3.2.33**
+-
+
+#### Детали карты (альт. вид) — `card_detail_2`
+Альтернативное состояние экрана детали карты.
+- [ ] Снять под новую версию
+- Текущий дамп: **корень (база)**
+-
+
+#### Bottom-sheet «Настройки карты» ⚠️
+Шторка с пунктами «Реквизиты карты» / «Заблокировать карту» и т.д. (`isCardSettingsBottomSheet()`).
+- [ ] **Эталонный дамп ещё не снят — приоритет при следующем съёме**
+- Текущий дамп: _нет_
+
+#### Экран «Реквизиты и информация» ⚠️
+Табы «Реквизиты карты» / «Реквизиты счета», номер счёта (`isCardRequisitesScreen()`).
+- [ ] **Эталонный дамп ещё не снят — приоритет при следующем съёме**
+- Текущий дамп: _нет_
+
+---
+
+## 2. fetch_transactions (`EventType::FetchTransactions` → `GetLastTransactionsScript`)
+
+### 2.1. Список истории
+
+#### История операций — `history_screen`
+Табы «Операции» / «Выписка», список транзакций.
+- [ ] Снять под новую версию
+- Текущий дамп: **3.2.33**
+-
+
+#### Выписка — `statement_screen`
+Экран «Выписка» (появился в 3.2.39+).
+- [ ] Снять под новую версию
+- Текущий дамп: **3.2.41**
+-
+
+#### Экран переводов — `transfers_screen`
+Список переводов / вкладка переводов.
+- [ ] Снять под новую версию
+- Текущий дамп: **3.2.33**
+-
+
+### 2.2. Детали транзакции
+
+#### Детали транзакции — `tx_detail`
+«Дата операции:», «Номер операции:», сумма, статус (`isTransactionDetailScreen()`).
+- [ ] Снять под новую версию
+- Текущий дамп: **3.2.33**
+-
+
+#### Детали транзакции перевода — `card_transfer_tx_detail`
+Детали операции, открытые из истории переводов по карте.
+- [ ] Снять под новую версию
+- Текущий дамп: **3.2.33**
+-
+
+---
+
+## 3. create_transaction (`EventType::CreateTransaction`)
+
+### 3.1. Перевод по карте (`PayByCardScript`)
+
+#### Ввод суммы — `card_transfer_input`
+Поле суммы + якорь «Мин. сумма: …» под полем.
+- [ ] Снять под новую версию
+- Текущий дамп: **3.2.33**
+-
+
+#### Подтверждение — `card_transfer_confirm`
+Bottom-sheet подтверждения, оранжевая кнопка «Подтверждаю» (RGB 255,117,40).
+- [ ] Снять под новую версию
+- Текущий дамп: **3.2.33**
+-
+
+#### Подтверждение (детали) — `card_transfer_confirm_detail`
+Развёрнутые детали в шторке подтверждения.
+- [ ] Снять под новую версию
+- Текущий дамп: **корень (база)**
+-
+
+#### Успех — `card_transfer_success`
+Экран «Платёж выполнен», с него снимается ID операции (OCR).
+- [ ] Снять под новую версию
+- Текущий дамп: **3.2.33**
+-
+
+### 3.2. Перевод по телефону (`PayByPhoneScript`)
+
+#### Ввод номера/суммы — `phone_transfer_input`
+Поле номера телефона получателя + сумма.
+- [ ] Снять под новую версию
+- Текущий дамп: **3.2.41**
+-
+
+#### Подтверждение — `phone_transfer_confirm`
+Шторка подтверждения перевода по номеру.
+- [ ] Снять под новую версию
+- Текущий дамп: **3.2.41**
+-
+
+#### Успех — `phone_transfer_success`
+Экран успешного перевода по номеру.
+- [ ] Снять под новую версию
+- Текущий дамп: **3.2.41**
+-
+
+
+
+
+---
+
+## Сводные галереи (только экраны, используемые в коде)
+
+_Эталонные дампы, на которые сейчас нет активного кода (детектор не вызывается), в эти таблицы не включены._
+
+### Задача `fetch_profile` — вход, профиль, карты (`FetchProfile`)
+
+| | | | | | | | |
+|---|---|---|---|---|---|---|---|
+| 
home_screen
3.2.33 | 
side_menu
3.2.41 | 
cards_screen
3.2.33 | 
card_detail
3.2.33 | | | | |
+
+### Задача `fetch_transactions` (`FetchTransactions`)
+
+| | | | | | | | |
+|---|---|---|---|---|---|---|---|
+| 
history_screen
3.2.33 | 
statement_screen
3.2.41 | 
transfers_screen
3.2.33 | 
tx_detail
3.2.33 | 
card_transfer_tx_detail
3.2.33 | | | |
+
+### Задача `create_transaction` (`CreateTransaction`)
+
+| | | | | | | | |
+|---|---|---|---|---|---|---|---|
+| 
card_transfer_input
3.2.33 | 
card_transfer_confirm
3.2.33 | 
card_transfer_success
3.2.33 | 
phone_transfer_input
3.2.41 | 
phone_transfer_confirm
3.2.41 | 
phone_transfer_success
3.2.41 | | |
diff --git a/docs/index.md b/docs/index.md
index d862168..16eb72d 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -13,6 +13,13 @@
| [События и задачи](events.md) | Очередь задач: получение из API, хранение в БД, выполнение скриптов |
| [База данных](database.md) | Схема SQLite, паттерны миграций, что применяется при обновлении |
+### Экраны и дампы
+
+| Раздел | Описание |
+|--------|----------|
+| [Dushanbe — экраны](dushanbe_screens.md) | Чек-лист дампов экранов по задачам: что переснимать при новой версии |
+| [Ozon — экраны](ozon_screens.md) | Чек-лист дампов экранов по задачам: что переснимать при новой версии |
+
### API Backend (`Adb Core Backend v1.0.0`)
| Раздел | Описание |
diff --git a/docs/ozon_screens.md b/docs/ozon_screens.md
new file mode 100644
index 0000000..c2ebee1
--- /dev/null
+++ b/docs/ozon_screens.md
@@ -0,0 +1,321 @@
+# Ozon — экраны и дампы
+
+Чек-лист экранных дампов для банка **Ozon** (`appCode: ozon`). Используется, когда
+выходит новая версия приложения и нужно понять, **какие экраны переснять**.
+
+## Как пользоваться при выходе новой версии
+
+1. Завести папку `assets/ozon/<новая_версия>/` (по образцу `assets/dushanbe/<версия>/`).
+2. Пройти по чек-листу ниже и снять XML+скриншот каждого экрана на устройстве с новой
+ версией. Дампы кладутся под теми же именами (платёжные — в подпапку `pay/`).
+3. Сверить новый дамп с картинкой ниже.
+4. Если вёрстка/тексты разошлись с парсером — обновить `android/ozon/ScreenXmlParser.cpp`.
+
+## Версионирование
+
+Сейчас парсер Ozon **не версионируется** — опирается на текстовые маркеры
+(`android/ozon/ScreenXmlParser.cpp`), а эталонные дампы лежат в корне `assets/ozon/`
+(платёжные — в `assets/ozon/pay/`) без разбивки по версиям.
+
+Ozon **тоже будет версионироваться**: конвенция папок `assets/ozon/<версия>/` вводится
+на будущее по образцу Dushanbe. Когда вёрстка разойдётся между версиями, по аналогии с
+`android/dushanbe/AppVersionTrack.h` понадобится завести `Ozon::AppVersionTrack` и
+вызывать `setVersionTrack(...)` в скриптах.
+
+**Картинка под каждым экраном — текущий дамп из корня.** Когда появятся папки
+`assets/ozon/<версия>/`, применяется тот же fallback по убыванию версии, что у Dushanbe.
+Экраны без эталонного дампа помечены ⚠️ — их надо снять в первую очередь.
+
+---
+
+## 0. Общие баннеры / оверлеи
+
+Всплывают поверх любого экрана и должны закрываться во всех задачах для прохождения флоу.
+
+#### Реклама на главной — `home_ad_1`
+Рекламный bottom-sheet (`tryToFindAdBanner()`, `resource-id design_bottom_sheet`).
+- [ ] Снять под новую версию
+- Текущий дамп: **текущий (без версии)**
+-
+
+#### Обновление Google Play — `google_play`
+In-app update bottom-sheet (`tryToFindGooglePlayBanner()`).
+- [ ] Снять под новую версию
+- Текущий дамп: **текущий (без версии)**
+-
+
+#### Баннер ипотеки — `banner_ipoteka`
+Промо-баннер на главной.
+- [ ] Снять под новую версию
+- Текущий дамп: **текущий (без версии)**
+-
+
+#### Баннер карты — `card_banner`
+Промо-баннер «Заказать карту» на главной.
+- [ ] Снять под новую версию
+- Текущий дамп: **текущий (без версии)**
+-
+
+#### Выбор (bottom-sheet) — `select_bank`
+Шторка выбора (`tryToFindSelectBottomSheet()`).
+- [ ] Снять под новую версию
+- Текущий дамп: **текущий (без версии)**
+-
+
+#### Онбординг «Обновили главную» ⚠️
+`resource-id onboarding_viewpager`, кнопка «Пропустить» (`tryToFindOnboardingBanner()`).
+- [ ] **Эталонный дамп ещё не снят — приоритет при следующем съёме**
+- Текущий дамп: _нет_
+
+#### Баннер безопасности ⚠️
+«Мы заботимся о вашей безопасности» (`tryToFindSecurityBanner()`).
+- [ ] **Эталонный дамп ещё не снят — приоритет при следующем съёме**
+- Текущий дамп: _нет_
+
+#### Системный диалог разрешений ⚠️
+`com.google.android.permissioncontroller`, кнопка «ЗАПРЕТИТЬ» (`tryToFindSystemPermissionDenyButton()`).
+- [ ] **Эталонный дамп ещё не снят — приоритет при следующем съёме**
+- Текущий дамп: _нет_
+
+---
+
+## 1. Мастер установок (`BankSetupWizard`)
+
+Цепочка: `runPinCheck → runProfileCheck → runCardCheck` (`views/bank/BankSetupWizard.cpp`).
+
+### 1.1. Пин-код / вход (`LoginAndCheckAccountsScript`)
+
+#### Главный экран — `home_page`
+Кнопки «Перевести» + «Пополнить» (`isHomeScreen()`).
+- [ ] Снять под новую версию
+- Текущий дамп: **текущий (без версии)**
+-
+
+#### Главный экран (альт. вид) — `home_page_2`
+Альтернативное состояние главной.
+- [ ] Снять под новую версию
+- Текущий дамп: **текущий (без версии)**
+-
+
+#### Ввод PIN — `pin_code`
+Экран ввода пин-кода (`tryToFindPinCode()`).
+- [ ] Снять под новую версию
+- Текущий дамп: **текущий (без версии)**
+-
+
+#### Запрос биометрии — `ask_biometry`
+Диалог предложения включить биометрию.
+- [ ] Снять под новую версию
+- Текущий дамп: **текущий (без версии)**
+-
+
+### 1.2. Профиль (`GetProfileInfoScript`)
+
+#### Профиль / Аккаунт — `profile`
+Заголовок «Аккаунт» + кнопка «Выйти» (`isProfileScreen()`), ФИО и телефон.
+- [ ] Снять под новую версию
+- Текущий дамп: **текущий (без версии)**
+-
+
+### 1.3. Карты (`GetCardInfoScript`)
+
+#### Карта — `card_screen`
+Основное состояние экрана карты.
+- [ ] Снять под новую версию
+- Текущий дамп: **текущий (без версии)**
+-
+
+#### Карта (развёрнута) — `card_screen_open`
+Карта с раскрытыми деталями.
+- [ ] Снять под новую версию
+- Текущий дамп: **текущий (без версии)**
+-
+
+#### Карта (свёрнута) — `card_screen_hide`
+Карта со скрытыми деталями.
+- [ ] Снять под новую версию
+- Текущий дамп: **текущий (без версии)**
+-
+
+#### Реквизиты счёта ⚠️
+Заголовок «Реквизиты счёта» (`isAccountRequisitesScreen()`, `parseAccountNumberFromRequisites()`).
+- [ ] **Эталонный дамп ещё не снят — приоритет при следующем съёме**
+- Текущий дамп: _нет_
+
+---
+
+## 2. fetch_transactions (`EventType::FetchTransactions` → `GetLastTransactionsScript`)
+
+### 2.1. Список истории
+
+#### Страница платежей — `payments_page`
+Раздел платежей/операций.
+- [ ] Снять под новую версию
+- Текущий дамп: **текущий (без версии)**
+-
+
+#### Последние транзакции — `last_transactions`
+Список операций (`isTransactionListScreen()`, `findTransactionButtons()`).
+- [ ] Снять под новую версию
+- Текущий дамп: **текущий (без версии)**
+-
+
+#### Баннер в списке транзакций — `transaction_banner`
+Промо-баннер внутри списка операций.
+- [ ] Снять под новую версию
+- Текущий дамп: **текущий (без версии)**
+-
+
+### 2.2. Детали транзакции
+
+#### Детали транзакции — `transaction_info`
+Дата, отправитель, телефон, сумма (`parseTransactionDetail()`).
+- [ ] Снять под новую версию
+- Текущий дамп: **текущий (без версии)**
+-
+
+#### Детали транзакции (СБП) — `transaction_info_spb`
+Версия деталей для перевода через СБП.
+- [ ] Снять под новую версию
+- Текущий дамп: **текущий (без версии)**
+-
+
+#### Документ операции — `transaction_document`
+Чек/документ по операции.
+- [ ] Снять под новую версию
+- Текущий дамп: **текущий (без версии)**
+-
+
+---
+
+## 3. create_transaction (`EventType::CreateTransaction`)
+
+### 3.1. Платёж по карте (`PayByCardScript`)
+
+#### Перевод по номеру карты — `pay/pay_by_card`
+Экран ввода номера карты получателя (WebView).
+- [ ] Снять под новую версию
+- Текущий дамп: **текущий (без версии)**
+-
+
+#### Ввод суммы — `pay/pay_by_card_enter_sum`
+Поле суммы перевода по карте.
+- [ ] Снять под новую версию
+- Текущий дамп: **текущий (без версии)**
+-
+
+#### Успех — `pay/pay_by_card_success`
+Экран успешного перевода по карте.
+- [ ] Снять под новую версию
+- Текущий дамп: **текущий (без версии)**
+-
+
+### 3.2. Платёж по телефону (`PayByPhoneScript`)
+
+#### Перевод по номеру — `pay/pay_buy_phone`
+Основной экран перевода по номеру телефона.
+- [ ] Снять под новую версию
+- Текущий дамп: **текущий (без версии)**
+-
+
+#### Ввод суммы — `pay/pay_buy_phone_enter_sum`
+Поле суммы перевода по номеру.
+- [ ] Снять под новую версию
+- Текущий дамп: **текущий (без версии)**
+-
+
+#### Выбор банка — `pay/pay_buy_phone_select_bank`
+Список банков получателя.
+- [ ] Снять под новую версию
+- Текущий дамп: **текущий (без версии)**
+-
+
+#### Выбор банка (альт. вид) — `pay/pay_buy_phone_select_bank_1`
+Альтернативное состояние выбора банка.
+- [ ] Снять под новую версию
+- Текущий дамп: **текущий (без версии)**
+-
+
+#### Банк по умолчанию — `pay/pay_buy_phone_default_bank`
+Экран с предустановленным банком получателя.
+- [ ] Снять под новую версию
+- Текущий дамп: **текущий (без версии)**
+-
+
+#### Проверка данных — `pay/pay_buy_phone_check_data`
+Экран сверки данных перед отправкой.
+- [ ] Снять под новую версию
+- Текущий дамп: **текущий (без версии)**
+-
+
+#### Перевод на новый номер — `pay/pay_to_new_number`
+Ввод нового, ранее не использованного номера.
+- [ ] Снять под новую версию
+- Текущий дамп: **текущий (без версии)**
+-
+
+#### Перевод на старый номер — `pay/pay_to_old_number`
+Выбор ранее использованного номера.
+- [ ] Снять под новую версию
+- Текущий дамп: **текущий (без версии)**
+-
+
+#### Отправлено — `pay/pay_buy_phone_sent_success`
+Экран успешной отправки перевода по номеру.
+- [ ] Снять под новую версию
+- Текущий дамп: **текущий (без версии)**
+-
+
+### 3.3. Подтверждение (общее для обоих)
+
+#### Подтверждение платежа — `pay/confirm_payment`
+Экран подтверждения операции.
+- [ ] Снять под новую версию
+- Текущий дамп: **текущий (без версии)**
+-
+
+#### Подтверждение — успех — `pay/confirm_payment_success`
+Успешное подтверждение, с него снимается ID операции.
+- [ ] Снять под новую версию
+- Текущий дамп: **текущий (без версии)**
+-
+
+#### Подтверждение — ошибка — `pay/confirm_payment_error`
+Экран ошибки подтверждения платежа.
+- [ ] Снять под новую версию
+- Текущий дамп: **текущий (без версии)**
+-
+
+
+
+
+---
+
+## Сводные галереи (только экраны, используемые в коде)
+
+_Эталонные дампы, на которые сейчас нет активного кода (детектор не вызывается), в эти таблицы не включены._
+
+### Задача `fetch_profile` — вход, профиль, карты (`FetchProfile`)
+
+| | | | | | | | |
+|---|---|---|---|---|---|---|---|
+| 
home_page
текущий (без версии) | 
profile
текущий (без версии) | | | | | | |
+
+### Задача `fetch_transactions` (`FetchTransactions`)
+
+| | | | | | | | |
+|---|---|---|---|---|---|---|---|
+| 
payments_page
текущий (без версии) | 
last_transactions
текущий (без версии) | 
transaction_info
текущий (без версии) | | | | | |
+
+### Задача `create_transaction` (`CreateTransaction`)
+
+| | | | | | | | |
+|---|---|---|---|---|---|---|---|
+| 
pay_by_card
текущий (без версии) | 
pay_by_card_enter_sum
текущий (без версии) | 
pay_by_card_success
текущий (без версии) | 
pay_buy_phone
текущий (без версии) | 
pay_buy_phone_enter_sum
текущий (без версии) | 
pay_buy_phone_select_bank
текущий (без версии) | 
pay_buy_phone_default_bank
текущий (без версии) | 
pay_to_new_number
текущий (без версии) |
+| 
pay_to_old_number
текущий (без версии) | 
pay_buy_phone_sent_success
текущий (без версии) | 
confirm_payment
текущий (без версии) | 
confirm_payment_success
текущий (без версии) | 
confirm_payment_error
текущий (без версии) | | | |
+
+### Оставшиеся экраны (баннеры/оверлеи, вне серверных задач)
+
+| | | | | | | | |
+|---|---|---|---|---|---|---|---|
+| 
home_ad_1
текущий (без версии) | 
google_play
текущий (без версии) | | | | | | |
diff --git a/services/update/UpdateService.cpp b/services/update/UpdateService.cpp
index 9fc4887..c04f988 100644
--- a/services/update/UpdateService.cpp
+++ b/services/update/UpdateService.cpp
@@ -31,6 +31,10 @@ bool awaitReply(QNetworkReply *reply, int timeoutMs) {
}
constexpr int kManifestTimeoutMs = 15000;
+// Таймаут простоя загрузки zip: если данные перестали течь на это время —
+// reply прерывается с OperationCanceledError, downloadZip вернёт false и
+// downloadAndApply штатно эмитит stageFailed (снимает паузу обновления).
+constexpr int kDownloadStallTimeoutMs = 60000;
// Корень временной папки для стейджинга обновления (вне install-директории).
QString stagingRoot() {
@@ -162,6 +166,11 @@ bool UpdateService::downloadZip(const QString &zipUrl, const QString &destFile)
}
QNetworkRequest req((QUrl(zipUrl)));
+ // Стол-таймаут (Qt 5.15+): сбрасывается, пока данные текут, и срабатывает
+ // только при простое. Без него обрыв сети посреди загрузки оставляет reply
+ // в half-open state — finished не эмитится, loop.exec() висит вечно, а вместе
+ // с ним залипает UpdatePause (глушит периодический sync и обработку задач).
+ req.setTransferTimeout(kDownloadStallTimeoutMs);
QNetworkReply *reply = m_manager->get(req);
QObject::connect(reply, &QNetworkReply::downloadProgress, this,
diff --git a/views/MainWindow.cpp b/views/MainWindow.cpp
index adee150..7498c80 100644
--- a/views/MainWindow.cpp
+++ b/views/MainWindow.cpp
@@ -26,6 +26,8 @@
#include "dao/DeviceDAO.h"
#include "dao/MaterialDAO.h"
#include "dao/SettingsDAO.h"
+#include
+#include
#include
#include
#include
@@ -236,12 +238,26 @@ void MainWindow::checkForUpdates() {
progress->setCancelButton(nullptr);
progress->setValue(0);
+ // Watchdog: страховка от залипания UpdatePause. Если ни stageFailed,
+ // ни updateStaged не пришли за дедлайн (download/unpack завис на
+ // half-open сокете и т.п.), всё равно снимаем паузу и возвращаем
+ // приложение в рабочее состояние — иначе sync и обработка задач
+ // молчат до перезапуска. `finished` сериализует терминальные пути,
+ // чтобы пауза не снималась дважды и svc не удалялся повторно.
+ auto finished = QSharedPointer::create(false);
+ auto *watchdog = new QTimer(this);
+ watchdog->setSingleShot(true);
+ watchdog->setInterval(15 * 60 * 1000);
+
connect(svc, &UpdateService::downloadProgress, progress,
[progress](qint64 r, qint64 t) {
if (t > 0) progress->setValue(static_cast(r * 100 / t));
});
connect(svc, &UpdateService::stageFailed, this,
- [this, svc, progress, reEnable](const QString &reason) {
+ [this, svc, progress, reEnable, finished, watchdog](const QString &reason) {
+ if (*finished) return;
+ *finished = true;
+ watchdog->stop();
progress->close();
progress->deleteLater();
// Обновление не удалось — снимаем паузу, приложение продолжает
@@ -251,11 +267,30 @@ void MainWindow::checkForUpdates() {
reEnable();
svc->deleteLater();
});
- connect(svc, &UpdateService::updateStaged, this, [progress]() {
- progress->setLabelText("Перезапуск…");
- progress->setValue(100);
- qApp->quit(); // штатное завершение: aboutToQuit → stop() → disable
- });
+ connect(svc, &UpdateService::updateStaged, this,
+ [progress, finished, watchdog]() {
+ if (*finished) return;
+ *finished = true;
+ watchdog->stop();
+ progress->setLabelText("Перезапуск…");
+ progress->setValue(100);
+ qApp->quit(); // штатное завершение: aboutToQuit → stop() → disable
+ });
+ connect(watchdog, &QTimer::timeout, this,
+ [this, svc, progress, reEnable, finished]() {
+ if (*finished) return;
+ *finished = true;
+ qWarning() << "[MainWindow] update watchdog fired — forcing pause release";
+ progress->close();
+ progress->deleteLater();
+ UpdatePause::setPaused(false);
+ QMessageBox::warning(this, "Обновление",
+ "Обновление зависло и было прервано.\n"
+ "Приложение продолжит работу.");
+ reEnable();
+ svc->deleteLater();
+ });
+ watchdog->start();
QMetaObject::invokeMethod(svc, "downloadAndApply", Qt::QueuedConnection,
Q_ARG(QString, zipUrl), Q_ARG(QString, sha),