579 lines
27 KiB
C++
579 lines
27 KiB
C++
#include "MainWindow.h"
|
||
#include "bank/AccountWindow.h"
|
||
|
||
#include <QLabel>
|
||
#include <QPixmap>
|
||
#include <QResizeEvent>
|
||
#include <QScrollArea>
|
||
#include <QMenuBar>
|
||
#include <QStackedWidget>
|
||
|
||
#include <QMessageBox>
|
||
#include <QStatusBar>
|
||
#include <QThread>
|
||
|
||
#include "TutorialWindow.h"
|
||
#include "bank/BankListWindow.h"
|
||
#include "event/EventWindow.h"
|
||
#include "log/FileLogWindow.h"
|
||
#include "dao/GeneralLogDAO.h"
|
||
#include "dao/SettingsDAO.h"
|
||
#include "net/NetworkService.h"
|
||
#include "db/DeviceInfo.h"
|
||
#include "device/DeviceWidget.h"
|
||
#include "device/WifiConnectDialog.h"
|
||
#include "dao/BankProfileDAO.h"
|
||
#include "DatabaseManager.h"
|
||
#include "dao/DeviceDAO.h"
|
||
#include "dao/MaterialDAO.h"
|
||
#include "dao/SettingsDAO.h"
|
||
#include <QDebug>
|
||
#include <QSharedPointer>
|
||
#include <QTimer>
|
||
#include <QCoreApplication>
|
||
#include <QApplication>
|
||
#include <QProgressDialog>
|
||
#include <QSettings>
|
||
#include "widget/common/FlowLayout.h"
|
||
#include "update/UpdateService.h"
|
||
#include "update/UpdatePause.h"
|
||
|
||
MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) {
|
||
const QSettings settings("config.ini", QSettings::IniFormat);
|
||
const QString version = settings.value("common/version").toString();
|
||
QString title = version.isEmpty() ? "ARC" : QString("ARC v%1").arg(version);
|
||
const QString desktopName = SettingsDAO::get("desktop_unique_name");
|
||
if (!desktopName.isEmpty()) title += QString(" [%1]").arg(desktopName);
|
||
setWindowTitle(title);
|
||
|
||
// Инициализируем таймер обновления
|
||
timer = new QTimer(this);
|
||
connect(timer, &QTimer::timeout, this, &MainWindow::loadDevices);
|
||
|
||
stackedWidget = new QStackedWidget(this);
|
||
setCentralWidget(stackedWidget);
|
||
|
||
createDevicePage();
|
||
stackedWidget->addWidget(m_devicesWindow);
|
||
stackedWidget->setCurrentWidget(m_devicesWindow);
|
||
|
||
// FIXME переделать на ленивое создание
|
||
// m_accountsWindow = new AccountWindow(this);
|
||
// stackedWidget->addWidget(m_accountsWindow);
|
||
// stackedWidget->setCurrentWidget(accountPage);
|
||
|
||
resize(800, 600);
|
||
|
||
// Работа с меню
|
||
QMenu *menu = menuBar()->addMenu("Меню");
|
||
auto *devicesPageAction = new QAction("Активные устройства", this);
|
||
// auto *accountPageAction = new QAction("Аккаунты", this);
|
||
auto *tutorialPageAction = new QAction("Инструкция", this);
|
||
auto *eventAction = new QAction("События", this);
|
||
auto *fileLogAction = new QAction("Логи", this);
|
||
auto *wifiConnectAction = new QAction("Подключить по Wi-Fi", this);
|
||
auto *updateAction = new QAction("Проверить обновления", this);
|
||
m_updateAction = updateAction;
|
||
auto *exitAction = new QAction("Выйти", this);
|
||
menu->addAction(devicesPageAction);
|
||
// menu->addAction(accountPageAction);
|
||
menu->addAction(eventAction);
|
||
menu->addAction(tutorialPageAction);
|
||
menu->addAction(fileLogAction);
|
||
menu->addAction(wifiConnectAction);
|
||
menu->addAction(updateAction);
|
||
menu->addAction(exitAction);
|
||
|
||
connect(devicesPageAction, &QAction::triggered, this, [=]() {
|
||
if (!m_devicesWindow) {
|
||
createDevicePage();
|
||
}
|
||
stackedWidget->setCurrentWidget(m_devicesWindow);
|
||
});
|
||
|
||
// connect(accountPageAction, &QAction::triggered, this, [=]() {
|
||
// stackedWidget->setCurrentWidget(m_accountsWindow);
|
||
// deleteDevicePage(); // освобождаем ресурсы
|
||
// });
|
||
|
||
connect(tutorialPageAction, &QAction::triggered, this, [=]() {
|
||
if (!m_tutorialWindow) {
|
||
m_tutorialWindow = new TutorialWindow(this);
|
||
}
|
||
m_tutorialWindow->show();
|
||
m_tutorialWindow->raise();
|
||
m_tutorialWindow->activateWindow();
|
||
});
|
||
|
||
connect(eventAction, &QAction::triggered, this, [=]() {
|
||
if (!m_eventWindow) {
|
||
m_eventWindow = new EventWindow(this);
|
||
}
|
||
m_eventWindow->show();
|
||
m_eventWindow->raise();
|
||
m_eventWindow->activateWindow();
|
||
});
|
||
|
||
connect(fileLogAction, &QAction::triggered, this, [this]() {
|
||
QMessageBox msgBox(this);
|
||
msgBox.setWindowTitle("Логи");
|
||
msgBox.setText("Отправить логи на сервер?");
|
||
auto *yesBtn = msgBox.addButton("Отправить", QMessageBox::YesRole);
|
||
msgBox.addButton("Отмена", QMessageBox::NoRole);
|
||
msgBox.exec();
|
||
if (msgBox.clickedButton() == yesBtn) {
|
||
if (!m_fileLogWindow) {
|
||
m_fileLogWindow = new FileLogWindow(this);
|
||
}
|
||
m_fileLogWindow->sendLogsToBackend();
|
||
}
|
||
});
|
||
|
||
connect(wifiConnectAction, &QAction::triggered, this, [this]() {
|
||
auto *dlg = new WifiConnectDialog(this);
|
||
dlg->setAttribute(Qt::WA_DeleteOnClose);
|
||
dlg->show();
|
||
});
|
||
|
||
connect(updateAction, &QAction::triggered, this, &MainWindow::checkForUpdates);
|
||
|
||
connect(exitAction, &QAction::triggered, this, [this]() {
|
||
QMessageBox msgBox(this);
|
||
msgBox.setWindowTitle("Выход");
|
||
msgBox.setText("Удалить локальную базу данных и выйти из приложения?");
|
||
auto *yesBtn = msgBox.addButton("Выйти", QMessageBox::YesRole);
|
||
msgBox.addButton("Отмена", QMessageBox::NoRole);
|
||
msgBox.exec();
|
||
if (msgBox.clickedButton() == yesBtn) {
|
||
DatabaseManager::instance().setWipeOnExit(true);
|
||
qApp->quit(); // штатное завершение фоновых потоков (aboutToQuit)
|
||
}
|
||
});
|
||
|
||
if (SettingsDAO::get("tutorial_shown").isEmpty()) {
|
||
QTimer::singleShot(0, this, [this]() {
|
||
m_tutorialWindow = new TutorialWindow(this);
|
||
m_tutorialWindow->show();
|
||
m_tutorialWindow->raise();
|
||
m_tutorialWindow->activateWindow();
|
||
SettingsDAO::set("tutorial_shown", "1");
|
||
});
|
||
}
|
||
|
||
// Sync-health индикатор в статус-баре (правило #9). По умолчанию скрыт —
|
||
// показывается только когда NetworkService сообщил unhealthy.
|
||
m_syncHealthLabel = new QLabel(this);
|
||
m_syncHealthLabel->setVisible(false);
|
||
m_syncHealthLabel->setStyleSheet("color: #d9534f; padding: 0 8px;");
|
||
statusBar()->addPermanentWidget(m_syncHealthLabel);
|
||
}
|
||
|
||
void MainWindow::setSyncHealth(bool healthy, int consecutiveFailures) {
|
||
if (!m_syncHealthLabel) return;
|
||
if (healthy) {
|
||
m_syncHealthLabel->setVisible(false);
|
||
m_syncHealthLabel->setToolTip(QString());
|
||
return;
|
||
}
|
||
m_syncHealthLabel->setText(QStringLiteral("⚠ Нет связи с сервером"));
|
||
m_syncHealthLabel->setToolTip(
|
||
QStringLiteral("Подряд %1 неудачных синхронизаций. Проверьте подключение.")
|
||
.arg(consecutiveFailures));
|
||
m_syncHealthLabel->setVisible(true);
|
||
}
|
||
|
||
void MainWindow::checkForUpdates() {
|
||
if (m_updateAction) m_updateAction->setEnabled(false);
|
||
auto reEnable = [this]() { if (m_updateAction) m_updateAction->setEnabled(true); };
|
||
|
||
// UpdateService живёт в своём потоке (как NetworkService). Сигналы приходят
|
||
// в главный поток через QueuedConnection — диалоги показываем здесь.
|
||
auto *thread = new QThread;
|
||
auto *svc = new UpdateService;
|
||
svc->moveToThread(thread);
|
||
connect(thread, &QThread::started, svc, &UpdateService::checkForUpdates);
|
||
connect(svc, &QObject::destroyed, thread, &QThread::quit);
|
||
connect(thread, &QThread::finished, thread, &QObject::deleteLater);
|
||
|
||
connect(svc, &UpdateService::upToDate, this, [this, svc, reEnable](const QString &ver) {
|
||
QMessageBox::information(this, "Обновление",
|
||
QString("У вас последняя версия (%1).").arg(ver));
|
||
reEnable();
|
||
svc->deleteLater();
|
||
});
|
||
|
||
connect(svc, &UpdateService::checkFailed, this, [this, svc, reEnable](const QString &reason) {
|
||
QMessageBox::warning(this, "Обновление",
|
||
"Не удалось проверить обновления:\n" + reason);
|
||
reEnable();
|
||
svc->deleteLater();
|
||
});
|
||
|
||
connect(svc, &UpdateService::updateAvailable, this,
|
||
[this, svc, reEnable](const QString &ver,
|
||
const QString &zipUrl, const QString &sha) {
|
||
const QString text = QString("Доступна новая версия: %1.\nУстановить сейчас?\n\n"
|
||
"Приложение перезапустится автоматически.").arg(ver);
|
||
// Кнопки задаём вручную — стандартные Yes/No берут английские подписи
|
||
// из локализации Qt (как в остальных диалогах приложения).
|
||
QMessageBox confirmBox(this);
|
||
confirmBox.setWindowTitle("Обновление");
|
||
confirmBox.setText(text);
|
||
confirmBox.setIcon(QMessageBox::Question);
|
||
auto *yesBtn = confirmBox.addButton("Обновить", QMessageBox::YesRole);
|
||
confirmBox.addButton("Позже", QMessageBox::NoRole);
|
||
confirmBox.exec();
|
||
if (confirmBox.clickedButton() != yesBtn) {
|
||
reEnable();
|
||
svc->deleteLater();
|
||
return;
|
||
}
|
||
|
||
// Ставим паузу: EventHandler перестаёт опрашивать задачи, а
|
||
// периодический sync — перепушивать "active" на сервер. Затем сразу
|
||
// выключаем все профили на сервере (на выходе stop() повторит идемпотентно).
|
||
UpdatePause::setPaused(true);
|
||
if (m_network) {
|
||
QMetaObject::invokeMethod(m_network, "disableAllProfilesOnServer",
|
||
Qt::QueuedConnection);
|
||
}
|
||
|
||
auto *progress = new QProgressDialog("Загрузка обновления…", QString(), 0, 100, this);
|
||
progress->setWindowTitle("Обновление");
|
||
progress->setWindowModality(Qt::WindowModal);
|
||
progress->setMinimumDuration(0);
|
||
progress->setAutoClose(false);
|
||
progress->setAutoReset(false);
|
||
progress->setCancelButton(nullptr);
|
||
progress->setValue(0);
|
||
|
||
// Watchdog: страховка от залипания UpdatePause. Если ни stageFailed,
|
||
// ни updateStaged не пришли за дедлайн (download/unpack завис на
|
||
// half-open сокете и т.п.), всё равно снимаем паузу и возвращаем
|
||
// приложение в рабочее состояние — иначе sync и обработка задач
|
||
// молчат до перезапуска. `finished` сериализует терминальные пути,
|
||
// чтобы пауза не снималась дважды и svc не удалялся повторно.
|
||
auto finished = QSharedPointer<bool>::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<int>(r * 100 / t));
|
||
});
|
||
connect(svc, &UpdateService::stageFailed, this,
|
||
[this, svc, progress, reEnable, finished, watchdog](const QString &reason) {
|
||
if (*finished) return;
|
||
*finished = true;
|
||
watchdog->stop();
|
||
progress->close();
|
||
progress->deleteLater();
|
||
// Обновление не удалось — снимаем паузу, приложение продолжает
|
||
// работу. Периодический sync восстановит профили на сервере.
|
||
UpdatePause::setPaused(false);
|
||
QMessageBox::warning(this, "Обновление", "Ошибка установки:\n" + reason);
|
||
reEnable();
|
||
svc->deleteLater();
|
||
});
|
||
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),
|
||
Q_ARG(QString, ver));
|
||
});
|
||
|
||
thread->start();
|
||
}
|
||
|
||
|
||
void MainWindow::createDevicePage() {
|
||
if (m_devicesWindow) {
|
||
return;
|
||
}
|
||
|
||
auto *container = new QWidget(this);
|
||
auto *containerLayout = new QVBoxLayout(container);
|
||
containerLayout->setContentsMargins(0, 0, 0, 0);
|
||
|
||
m_noDevicesLabel = new QLabel(container);
|
||
m_noDevicesPixmap = QPixmap(QCoreApplication::applicationDirPath() + "/no_devices.png");
|
||
m_noDevicesLabel->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Ignored);
|
||
m_noDevicesLabel->setAlignment(Qt::AlignCenter);
|
||
containerLayout->addWidget(m_noDevicesLabel);
|
||
|
||
m_scrollArea = new QScrollArea(container);
|
||
auto *contentWidget = new QWidget;
|
||
flowLayout = new FlowLayout(contentWidget, 0, 0, 0);
|
||
|
||
QVector<DeviceInfo> devices = DeviceDAO::getAllDevices(false);
|
||
for (const auto &device: devices) {
|
||
flowLayout->addWidget(new DeviceWidget(device));
|
||
}
|
||
|
||
contentWidget->setLayout(flowLayout);
|
||
static_cast<QScrollArea*>(m_scrollArea)->setWidget(contentWidget);
|
||
static_cast<QScrollArea*>(m_scrollArea)->setWidgetResizable(true);
|
||
containerLayout->addWidget(m_scrollArea);
|
||
|
||
m_noDevicesLabel->setVisible(devices.isEmpty());
|
||
m_scrollArea->setVisible(!devices.isEmpty());
|
||
|
||
// ОТКЛЮЧЕНО: панель вывода ошибок на главном экране. Она заполнялась через
|
||
// updateLogPanel() каждую секунду (timer 1000ms) запросом getLogs() к general_logs
|
||
// + множеством вложенных DAO-запросов на GUI-потоке. m_logList остаётся nullptr,
|
||
// поэтому updateLogPanel() сразу выходит (см. `if (!m_logList) return;`) — панель
|
||
// не показывается и посекундный запрос к БД больше не выполняется.
|
||
// m_logList = new QListWidget(container);
|
||
// m_logList->setMaximumHeight(120);
|
||
// m_logList->setStyleSheet(
|
||
// "QListWidget { background-color: #1e1e1e; color: #cccccc; font-family: monospace; font-size: 11px; border-top: 1px solid #444; }"
|
||
// "QListWidget::item { padding: 1px 4px; }"
|
||
// );
|
||
// m_logList->setSelectionMode(QAbstractItemView::NoSelection);
|
||
// m_logList->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||
// containerLayout->addWidget(m_logList);
|
||
// updateLogPanel();
|
||
|
||
m_devicesWindow = container;
|
||
stackedWidget->addWidget(m_devicesWindow);
|
||
|
||
timer->start(1000);
|
||
|
||
if (devices.isEmpty()) {
|
||
QTimer::singleShot(0, this, [this]() {
|
||
if (m_noDevicesLabel && !m_noDevicesPixmap.isNull()) {
|
||
m_noDevicesLabel->setPixmap(m_noDevicesPixmap.scaled(
|
||
m_noDevicesLabel->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation));
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
void MainWindow::deleteDevicePage() {
|
||
if (!m_devicesWindow) {
|
||
return;
|
||
}
|
||
|
||
timer->stop();
|
||
|
||
stackedWidget->removeWidget(m_devicesWindow);
|
||
m_devicesWindow->deleteLater();
|
||
m_devicesWindow = nullptr;
|
||
flowLayout = nullptr;
|
||
}
|
||
|
||
/**
|
||
* Однако, в нем вы вызываете delete для элементов, что изменяет память.
|
||
* Для того, чтобы не нарушать константность, этот метод должен быть неконстантным (без const в сигнатуре).
|
||
*/
|
||
void MainWindow::loadDevices() {
|
||
const auto devices = DeviceDAO::getAllDevices(false);
|
||
// Очистить старые виджеты
|
||
QLayoutItem *item;
|
||
while ((item = flowLayout->takeAt(0)) != nullptr) {
|
||
if (item->widget()) {
|
||
item->widget()->deleteLater();
|
||
}
|
||
delete item;
|
||
}
|
||
// Добавляем новые виджеты для каждого устройства
|
||
for (const auto &device: devices) {
|
||
flowLayout->addWidget(new DeviceWidget(device));
|
||
}
|
||
|
||
m_noDevicesLabel->setVisible(devices.isEmpty());
|
||
m_scrollArea->setVisible(!devices.isEmpty());
|
||
|
||
updateLogPanel();
|
||
}
|
||
|
||
void MainWindow::updateLogPanel() {
|
||
if (!m_logList) return;
|
||
|
||
const QList<GeneralLogEntry> logs = GeneralLogDAO::getLogs(0, 5, {"WARNING", "CRITICAL", "FATAL"}, 10);
|
||
m_logList->clear();
|
||
for (const auto &entry : logs) {
|
||
// Преобразуем в человекопонятные сообщения
|
||
QString msg = entry.message;
|
||
|
||
// Извлекаем контекст: банк, устройство
|
||
QString bank;
|
||
QString device;
|
||
{
|
||
// bank=ozon
|
||
QRegularExpression bankRe(R"(bank=(\w+))");
|
||
if (auto m = bankRe.match(msg); m.hasMatch()) {
|
||
const QString b = m.captured(1);
|
||
if (b == "ozon") bank = "Ozon";
|
||
else if (b == "black") bank = "Black";
|
||
else if (b == "dushanbe_city_bank") bank = "Dushanbe";
|
||
else bank = b;
|
||
}
|
||
// Устройство: device=ID или "SERIAL"
|
||
QRegularExpression devEqRe(R"(device=(\S+))");
|
||
if (auto m = devEqRe.match(msg); m.hasMatch()) {
|
||
const QString devId = m.captured(1);
|
||
const DeviceInfo dev = DeviceDAO::getDeviceById(devId);
|
||
if (!dev.name.isEmpty()) device = dev.name;
|
||
else {
|
||
const DeviceInfo dev2 = DeviceDAO::findByAdbSerial(devId);
|
||
device = dev2.name.isEmpty() ? devId.left(10) + "..." : dev2.name;
|
||
}
|
||
}
|
||
if (device.isEmpty()) {
|
||
QRegularExpression devRe(R"x("?([A-Z0-9]{12,})"?)x");
|
||
if (auto m = devRe.match(msg); m.hasMatch()) {
|
||
const DeviceInfo dev = DeviceDAO::findByAdbSerial(m.captured(1));
|
||
device = dev.name.isEmpty() ? m.captured(1).left(8) + "..." : dev.name;
|
||
}
|
||
}
|
||
// UUID в URL — ищем bank_profile или material по ID
|
||
if (bank.isEmpty() || device.isEmpty()) {
|
||
QRegularExpression uuidRe(R"(([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}))");
|
||
auto it = uuidRe.globalMatch(msg);
|
||
while (it.hasNext() && (bank.isEmpty() || device.isEmpty())) {
|
||
const QString uuid = it.next().captured(1);
|
||
if (bank.isEmpty()) {
|
||
const QList<BankProfileInfo> bpList = BankProfileDAO::findAllByBankProfileId(uuid);
|
||
const BankProfileInfo bp = bpList.isEmpty() ? BankProfileInfo() : bpList.first();
|
||
if (bp.id >= 0) {
|
||
if (bank.isEmpty()) {
|
||
const QString b = bp.code;
|
||
if (b == "ozon") bank = "Ozon";
|
||
else if (b == "black") bank = "Black";
|
||
else if (b == "dushanbe_city_bank") bank = "Dushanbe";
|
||
else bank = b;
|
||
}
|
||
if (device.isEmpty() && !bp.deviceId.isEmpty()) {
|
||
const DeviceInfo dev = DeviceDAO::getDeviceById(bp.deviceId);
|
||
device = dev.name.isEmpty() ? bp.deviceId.left(8) + "..." : dev.name;
|
||
}
|
||
}
|
||
}
|
||
if (bank.isEmpty()) {
|
||
const MaterialInfo mat = MaterialDAO::findByMaterialId(uuid);
|
||
if (mat.id >= 0) {
|
||
const QString b = mat.appCode;
|
||
if (b == "ozon") bank = "Ozon";
|
||
else if (b == "black") bank = "Black";
|
||
else if (b == "dushanbe_city_bank") bank = "Dushanbe";
|
||
else bank = b;
|
||
if (device.isEmpty() && !mat.deviceId.isEmpty()) {
|
||
const DeviceInfo dev = DeviceDAO::getDeviceById(mat.deviceId);
|
||
device = dev.name.isEmpty() ? mat.deviceId.left(8) + "..." : dev.name;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
// Формируем префикс [девайс | банк]
|
||
QString ctxPrefix;
|
||
if (!device.isEmpty() || !bank.isEmpty()) {
|
||
QStringList parts;
|
||
if (!device.isEmpty()) parts << device;
|
||
if (!bank.isEmpty()) parts << bank;
|
||
ctxPrefix = "[" + parts.join(" | ") + "] ";
|
||
}
|
||
|
||
if (msg.contains("Failed to send result to server"))
|
||
msg = ctxPrefix + "Не удалось отправить результат на сервер";
|
||
else if (msg.contains("Device is offline"))
|
||
msg = ctxPrefix + "Устройство не подключено";
|
||
else if (msg.contains("bank_transaction_id is empty"))
|
||
msg = ctxPrefix + "Не удалось распознать ID транзакции";
|
||
else if (msg.contains("Transaction not found"))
|
||
msg = ctxPrefix + "Транзакция не найдена";
|
||
else if (msg.contains("Cannot reach home screen") || msg.contains("Cant open the home screen"))
|
||
msg = ctxPrefix + "Не удалось открыть приложение";
|
||
else if (msg.contains("Card input screen not loaded"))
|
||
msg = ctxPrefix + "Экран ввода карты не загрузился";
|
||
else if (msg.contains("Payment result not received"))
|
||
msg = ctxPrefix + "Результат платежа не получен (таймаут)";
|
||
else if (msg.contains("Transfer failed"))
|
||
msg = ctxPrefix + "Перевод не удался";
|
||
else if (msg.contains("PIN code") || msg.contains("Пин-код"))
|
||
msg = ctxPrefix + "Ошибка проверки пин-кода";
|
||
else if (msg.contains("postTaskResult failed"))
|
||
msg = ctxPrefix + "Ошибка отправки результата задачи";
|
||
else if (msg.contains("Network error") || msg.contains("network error")) {
|
||
QRegularExpression urlRe(R"((https?://[^\s\"]+))");
|
||
QRegularExpression codeRe(R"(server replied:\s*(.+?)[\"\n])");
|
||
QString url, code;
|
||
if (auto m = urlRe.match(msg); m.hasMatch()) url = m.captured(1);
|
||
if (auto m = codeRe.match(msg); m.hasMatch()) code = m.captured(1).trimmed();
|
||
|
||
// Преобразуем путь API в читаемое название
|
||
QString endpoint;
|
||
if (url.contains("/bank_profile")) endpoint = "обновление профиля банка";
|
||
else if (url.contains("/material")) endpoint = "обновление карты/счёта";
|
||
else if (url.contains("/task_result")) endpoint = "отправка результата задачи";
|
||
else if (url.contains("/get_tasks")) endpoint = "получение задач";
|
||
else if (url.contains("/device")) endpoint = "обновление устройства";
|
||
else if (url.contains("/auth")) endpoint = "авторизация";
|
||
else if (url.contains("/current_profile")) endpoint = "получение профиля";
|
||
else if (url.contains("/monitoring")) endpoint = "отправка логов";
|
||
else {
|
||
const int pathIdx = url.indexOf('/', url.indexOf("//") + 2);
|
||
endpoint = pathIdx > 0 ? url.mid(pathIdx) : url;
|
||
}
|
||
|
||
msg = "Ошибка: " + endpoint;
|
||
if (!code.isEmpty()) msg += " [" + code + "]";
|
||
msg = ctxPrefix + msg;
|
||
}
|
||
else if (msg.contains("App restarted"))
|
||
msg = ctxPrefix + "Приложение перезапущено, задача отменена";
|
||
else if (msg.contains("Task expired"))
|
||
msg = ctxPrefix + "Задача просрочена";
|
||
else if (msg.contains("Bank profile disabled") || msg.contains("Bank profile is disabled"))
|
||
msg = ctxPrefix + "Банковский профиль выключен";
|
||
else if (msg.contains("Скриншот пустой"))
|
||
msg = ctxPrefix + "Скриншот устройства пустой";
|
||
|
||
const QString time = entry.timestamp.toLocalTime().toString("HH:mm:ss");
|
||
const QString line = QStringLiteral("[%1] %2").arg(time, msg);
|
||
|
||
auto *item = new QListWidgetItem(line, m_logList);
|
||
if (entry.level == "CRITICAL" || entry.level == "FATAL") {
|
||
item->setForeground(QColor("#ff6b6b"));
|
||
} else if (entry.level == "WARNING") {
|
||
item->setForeground(QColor("#ffa94d"));
|
||
} else {
|
||
item->setForeground(QColor("#cccccc"));
|
||
}
|
||
}
|
||
}
|
||
|
||
void MainWindow::resizeEvent(QResizeEvent *event) {
|
||
QMainWindow::resizeEvent(event);
|
||
if (m_noDevicesLabel && m_noDevicesLabel->isVisible() && !m_noDevicesPixmap.isNull()) {
|
||
m_noDevicesLabel->setPixmap(m_noDevicesPixmap.scaled(
|
||
m_noDevicesLabel->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation));
|
||
}
|
||
}
|