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** +- launch_screen + +#### Экран ввода PIN — `pin_code` ⚠️ +Цифровая клавиатура ввода пин-кода. Детектор `isPinCodeScreen()` ещё не реализован +(`ScreenXmlParser.h:45` — TODO «после снятия XML дампов экранов»). +- [ ] **Эталонный дамп ещё не снят — приоритет при следующем съёме** +- Текущий дамп: _нет_ + +#### Главный экран — `home_screen` +«Главная»: нижняя навигация (Переводы/Сервисы/История) + карточка баланса «TJS\n<дата>». +- [ ] Снять под новую версию +- Текущий дамп: **3.2.33** +- home_screen + +### 1.2. Профиль (`GetProfileInfoScript`) + +#### Боковое меню / профиль — `side_menu` +Шторка профиля: ФИО, телефон, версия/сборка. Из неё парсятся данные профиля. +- [ ] Снять под новую версию +- Текущий дамп: **3.2.41** +- side_menu + +### 1.3. Карты (`GetCardInfoScript`) + +#### Список карт — `cards_screen` +Экран «Мои карты» со списком карт (last numbers, баланс). +- [ ] Снять под новую версию +- Текущий дамп: **3.2.33** +- cards_screen + +#### Детали карты — `card_detail` +Заголовок «Карта»: полный номер, баланс, держатель. +- [ ] Снять под новую версию +- Текущий дамп: **3.2.33** +- card_detail + +#### Детали карты (альт. вид) — `card_detail_2` +Альтернативное состояние экрана детали карты. +- [ ] Снять под новую версию +- Текущий дамп: **корень (база)** +- card_detail_2 + +#### Bottom-sheet «Настройки карты» ⚠️ +Шторка с пунктами «Реквизиты карты» / «Заблокировать карту» и т.д. (`isCardSettingsBottomSheet()`). +- [ ] **Эталонный дамп ещё не снят — приоритет при следующем съёме** +- Текущий дамп: _нет_ + +#### Экран «Реквизиты и информация» ⚠️ +Табы «Реквизиты карты» / «Реквизиты счета», номер счёта (`isCardRequisitesScreen()`). +- [ ] **Эталонный дамп ещё не снят — приоритет при следующем съёме** +- Текущий дамп: _нет_ + +--- + +## 2. fetch_transactions (`EventType::FetchTransactions` → `GetLastTransactionsScript`) + +### 2.1. Список истории + +#### История операций — `history_screen` +Табы «Операции» / «Выписка», список транзакций. +- [ ] Снять под новую версию +- Текущий дамп: **3.2.33** +- history_screen + +#### Выписка — `statement_screen` +Экран «Выписка» (появился в 3.2.39+). +- [ ] Снять под новую версию +- Текущий дамп: **3.2.41** +- statement_screen + +#### Экран переводов — `transfers_screen` +Список переводов / вкладка переводов. +- [ ] Снять под новую версию +- Текущий дамп: **3.2.33** +- transfers_screen + +### 2.2. Детали транзакции + +#### Детали транзакции — `tx_detail` +«Дата операции:», «Номер операции:», сумма, статус (`isTransactionDetailScreen()`). +- [ ] Снять под новую версию +- Текущий дамп: **3.2.33** +- tx_detail + +#### Детали транзакции перевода — `card_transfer_tx_detail` +Детали операции, открытые из истории переводов по карте. +- [ ] Снять под новую версию +- Текущий дамп: **3.2.33** +- card_transfer_tx_detail + +--- + +## 3. create_transaction (`EventType::CreateTransaction`) + +### 3.1. Перевод по карте (`PayByCardScript`) + +#### Ввод суммы — `card_transfer_input` +Поле суммы + якорь «Мин. сумма: …» под полем. +- [ ] Снять под новую версию +- Текущий дамп: **3.2.33** +- card_transfer_input + +#### Подтверждение — `card_transfer_confirm` +Bottom-sheet подтверждения, оранжевая кнопка «Подтверждаю» (RGB 255,117,40). +- [ ] Снять под новую версию +- Текущий дамп: **3.2.33** +- card_transfer_confirm + +#### Подтверждение (детали) — `card_transfer_confirm_detail` +Развёрнутые детали в шторке подтверждения. +- [ ] Снять под новую версию +- Текущий дамп: **корень (база)** +- card_transfer_confirm_detail + +#### Успех — `card_transfer_success` +Экран «Платёж выполнен», с него снимается ID операции (OCR). +- [ ] Снять под новую версию +- Текущий дамп: **3.2.33** +- card_transfer_success + +### 3.2. Перевод по телефону (`PayByPhoneScript`) + +#### Ввод номера/суммы — `phone_transfer_input` +Поле номера телефона получателя + сумма. +- [ ] Снять под новую версию +- Текущий дамп: **3.2.41** +- phone_transfer_input + +#### Подтверждение — `phone_transfer_confirm` +Шторка подтверждения перевода по номеру. +- [ ] Снять под новую версию +- Текущий дамп: **3.2.41** +- phone_transfer_confirm + +#### Успех — `phone_transfer_success` +Экран успешного перевода по номеру. +- [ ] Снять под новую версию +- Текущий дамп: **3.2.41** +- phone_transfer_success + + + + +--- + +## Сводные галереи (только экраны, используемые в коде) + +_Эталонные дампы, на которые сейчас нет активного кода (детектор не вызывается), в эти таблицы не включены._ + +### Задача `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`). +- [ ] Снять под новую версию +- Текущий дамп: **текущий (без версии)** +- home_ad_1 + +#### Обновление Google Play — `google_play` +In-app update bottom-sheet (`tryToFindGooglePlayBanner()`). +- [ ] Снять под новую версию +- Текущий дамп: **текущий (без версии)** +- google_play + +#### Баннер ипотеки — `banner_ipoteka` +Промо-баннер на главной. +- [ ] Снять под новую версию +- Текущий дамп: **текущий (без версии)** +- banner_ipoteka + +#### Баннер карты — `card_banner` +Промо-баннер «Заказать карту» на главной. +- [ ] Снять под новую версию +- Текущий дамп: **текущий (без версии)** +- card_banner + +#### Выбор (bottom-sheet) — `select_bank` +Шторка выбора (`tryToFindSelectBottomSheet()`). +- [ ] Снять под новую версию +- Текущий дамп: **текущий (без версии)** +- select_bank + +#### Онбординг «Обновили главную» ⚠️ +`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 + +#### Главный экран (альт. вид) — `home_page_2` +Альтернативное состояние главной. +- [ ] Снять под новую версию +- Текущий дамп: **текущий (без версии)** +- home_page_2 + +#### Ввод PIN — `pin_code` +Экран ввода пин-кода (`tryToFindPinCode()`). +- [ ] Снять под новую версию +- Текущий дамп: **текущий (без версии)** +- pin_code + +#### Запрос биометрии — `ask_biometry` +Диалог предложения включить биометрию. +- [ ] Снять под новую версию +- Текущий дамп: **текущий (без версии)** +- ask_biometry + +### 1.2. Профиль (`GetProfileInfoScript`) + +#### Профиль / Аккаунт — `profile` +Заголовок «Аккаунт» + кнопка «Выйти» (`isProfileScreen()`), ФИО и телефон. +- [ ] Снять под новую версию +- Текущий дамп: **текущий (без версии)** +- profile + +### 1.3. Карты (`GetCardInfoScript`) + +#### Карта — `card_screen` +Основное состояние экрана карты. +- [ ] Снять под новую версию +- Текущий дамп: **текущий (без версии)** +- card_screen + +#### Карта (развёрнута) — `card_screen_open` +Карта с раскрытыми деталями. +- [ ] Снять под новую версию +- Текущий дамп: **текущий (без версии)** +- card_screen_open + +#### Карта (свёрнута) — `card_screen_hide` +Карта со скрытыми деталями. +- [ ] Снять под новую версию +- Текущий дамп: **текущий (без версии)** +- card_screen_hide + +#### Реквизиты счёта ⚠️ +Заголовок «Реквизиты счёта» (`isAccountRequisitesScreen()`, `parseAccountNumberFromRequisites()`). +- [ ] **Эталонный дамп ещё не снят — приоритет при следующем съёме** +- Текущий дамп: _нет_ + +--- + +## 2. fetch_transactions (`EventType::FetchTransactions` → `GetLastTransactionsScript`) + +### 2.1. Список истории + +#### Страница платежей — `payments_page` +Раздел платежей/операций. +- [ ] Снять под новую версию +- Текущий дамп: **текущий (без версии)** +- payments_page + +#### Последние транзакции — `last_transactions` +Список операций (`isTransactionListScreen()`, `findTransactionButtons()`). +- [ ] Снять под новую версию +- Текущий дамп: **текущий (без версии)** +- last_transactions + +#### Баннер в списке транзакций — `transaction_banner` +Промо-баннер внутри списка операций. +- [ ] Снять под новую версию +- Текущий дамп: **текущий (без версии)** +- transaction_banner + +### 2.2. Детали транзакции + +#### Детали транзакции — `transaction_info` +Дата, отправитель, телефон, сумма (`parseTransactionDetail()`). +- [ ] Снять под новую версию +- Текущий дамп: **текущий (без версии)** +- transaction_info + +#### Детали транзакции (СБП) — `transaction_info_spb` +Версия деталей для перевода через СБП. +- [ ] Снять под новую версию +- Текущий дамп: **текущий (без версии)** +- transaction_info_spb + +#### Документ операции — `transaction_document` +Чек/документ по операции. +- [ ] Снять под новую версию +- Текущий дамп: **текущий (без версии)** +- transaction_document + +--- + +## 3. create_transaction (`EventType::CreateTransaction`) + +### 3.1. Платёж по карте (`PayByCardScript`) + +#### Перевод по номеру карты — `pay/pay_by_card` +Экран ввода номера карты получателя (WebView). +- [ ] Снять под новую версию +- Текущий дамп: **текущий (без версии)** +- pay_by_card + +#### Ввод суммы — `pay/pay_by_card_enter_sum` +Поле суммы перевода по карте. +- [ ] Снять под новую версию +- Текущий дамп: **текущий (без версии)** +- pay_by_card_enter_sum + +#### Успех — `pay/pay_by_card_success` +Экран успешного перевода по карте. +- [ ] Снять под новую версию +- Текущий дамп: **текущий (без версии)** +- pay_by_card_success + +### 3.2. Платёж по телефону (`PayByPhoneScript`) + +#### Перевод по номеру — `pay/pay_buy_phone` +Основной экран перевода по номеру телефона. +- [ ] Снять под новую версию +- Текущий дамп: **текущий (без версии)** +- pay_buy_phone + +#### Ввод суммы — `pay/pay_buy_phone_enter_sum` +Поле суммы перевода по номеру. +- [ ] Снять под новую версию +- Текущий дамп: **текущий (без версии)** +- pay_buy_phone_enter_sum + +#### Выбор банка — `pay/pay_buy_phone_select_bank` +Список банков получателя. +- [ ] Снять под новую версию +- Текущий дамп: **текущий (без версии)** +- pay_buy_phone_select_bank + +#### Выбор банка (альт. вид) — `pay/pay_buy_phone_select_bank_1` +Альтернативное состояние выбора банка. +- [ ] Снять под новую версию +- Текущий дамп: **текущий (без версии)** +- pay_buy_phone_select_bank_1 + +#### Банк по умолчанию — `pay/pay_buy_phone_default_bank` +Экран с предустановленным банком получателя. +- [ ] Снять под новую версию +- Текущий дамп: **текущий (без версии)** +- pay_buy_phone_default_bank + +#### Проверка данных — `pay/pay_buy_phone_check_data` +Экран сверки данных перед отправкой. +- [ ] Снять под новую версию +- Текущий дамп: **текущий (без версии)** +- pay_buy_phone_check_data + +#### Перевод на новый номер — `pay/pay_to_new_number` +Ввод нового, ранее не использованного номера. +- [ ] Снять под новую версию +- Текущий дамп: **текущий (без версии)** +- pay_to_new_number + +#### Перевод на старый номер — `pay/pay_to_old_number` +Выбор ранее использованного номера. +- [ ] Снять под новую версию +- Текущий дамп: **текущий (без версии)** +- pay_to_old_number + +#### Отправлено — `pay/pay_buy_phone_sent_success` +Экран успешной отправки перевода по номеру. +- [ ] Снять под новую версию +- Текущий дамп: **текущий (без версии)** +- pay_buy_phone_sent_success + +### 3.3. Подтверждение (общее для обоих) + +#### Подтверждение платежа — `pay/confirm_payment` +Экран подтверждения операции. +- [ ] Снять под новую версию +- Текущий дамп: **текущий (без версии)** +- confirm_payment + +#### Подтверждение — успех — `pay/confirm_payment_success` +Успешное подтверждение, с него снимается ID операции. +- [ ] Снять под новую версию +- Текущий дамп: **текущий (без версии)** +- confirm_payment_success + +#### Подтверждение — ошибка — `pay/confirm_payment_error` +Экран ошибки подтверждения платежа. +- [ ] Снять под новую версию +- Текущий дамп: **текущий (без версии)** +- 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),