1
0
forked from BRT/arc

Add enhanced error handling, logging, and monitoring capabilities across systems. Include archive naming, retry logic for partial PIN entry, API response error dialogs, and support for monitoring tokens.

This commit is contained in:
slava 2026-05-21 13:52:29 +05:00
parent 56ba89522c
commit 07871231a3
11 changed files with 176 additions and 38 deletions

View File

@ -46,6 +46,34 @@ bool CommonScript::inputPinCode(const QString &deviceId, const QString &pinCode)
qWarning() << "[Dushanbe::inputPinCode] pinCode is EMPTY — nothing to enter!"; qWarning() << "[Dushanbe::inputPinCode] pinCode is EMPTY — nothing to enter!";
return false; return false;
} }
// Ждём, пока на экране появятся все уникальные цифры из PIN.
// DC Next на некоторых устройствах успевает показать заголовок
// "Введите код доступа" раньше, чем дорисует кнопочную сетку, —
// если начать тапать в этот момент, часть цифр не найдётся и PIN
// не введётся целиком.
bool seenInPin[10] = {false};
for (const QChar &d : pinCode) {
if (!d.isDigit()) continue;
const int idx = d.digitValue();
if (idx >= 0 && idx <= 9) seenInPin[idx] = true;
}
auto allDigitsPresent = [this, &seenInPin]() {
for (int d = 0; d < 10; ++d) {
if (!seenInPin[d]) continue;
if (xmlScreenParser.findNodeByContentDesc(QString::number(d)).isEmpty()) return false;
}
return true;
};
for (int i = 0; i < 6 && !allDigitsPresent(); ++i) {
QThread::msleep(500);
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
}
if (!allDigitsPresent()) {
qWarning() << "[Dushanbe::inputPinCode] keypad incomplete after wait; aborting";
return false;
}
qDebug() << "[Dushanbe::inputPinCode] entering" << pinCode.length() << "digits"; qDebug() << "[Dushanbe::inputPinCode] entering" << pinCode.length() << "digits";
// DC Next использует кнопочную сетку для ввода PIN (content-desc = "0"-"9") // DC Next использует кнопочную сетку для ввода PIN (content-desc = "0"-"9")
for (const QChar &digit : pinCode) { for (const QChar &digit : pinCode) {
@ -124,10 +152,14 @@ bool CommonScript::runAppAndGoToHomeScreen(
if (!findAndInputPincode && xmlScreenParser.isPinCodeScreen()) { if (!findAndInputPincode && xmlScreenParser.isPinCodeScreen()) {
qDebug() << "[Dushanbe] PIN screen detected on attempt" << counter << "entering PIN"; qDebug() << "[Dushanbe] PIN screen detected on attempt" << counter << "entering PIN";
if (!inputPinCode(deviceId, pinCode)) { if (inputPinCode(deviceId, pinCode)) {
qWarning() << "[Dushanbe] inputPinCode failed on attempt" << counter; findAndInputPincode = true;
} else {
// Клавиатура могла не дорисоваться или PIN ввёлся частично —
// оставляем флаг false, следующая итерация повторит ввод,
// когда кнопки появятся.
qWarning() << "[Dushanbe] inputPinCode failed on attempt" << counter << "— will retry";
} }
findAndInputPincode = true;
QThread::msleep(3000); QThread::msleep(3000);
continue; continue;
} }

View File

@ -20,6 +20,8 @@ pathDeviceScreenshot=/api/v1/device/add_screenshot/
pathMaterial=/api/v1/profile/material pathMaterial=/api/v1/profile/material
pathMonitoringLog=/monitoring/log pathMonitoringLog=/monitoring/log
pathMonitoringDump=/monitoring/dump pathMonitoringDump=/monitoring/dump
monitoringBase=http://142.93.102.40:8080
monitoringToken=509b91592749959b9ff56f9ed9189e5b3e91e1260003f63c477255e1c187298a
[black] [black]
android_package_name=app.blackwallet.ar android_package_name=app.blackwallet.ar

View File

@ -205,6 +205,7 @@ QList<MaterialInfo> MaterialDAO::getAccounts(const QString &deviceId, const QStr
SELECT id, device_id, app_code, last_numbers, card_number, account_number, material_id, amount, update_time, status, description, currency SELECT id, device_id, app_code, last_numbers, card_number, account_number, material_id, amount, update_time, status, description, currency
FROM materials FROM materials
WHERE device_id = :device_id AND app_code = :app_code WHERE device_id = :device_id AND app_code = :app_code
ORDER BY id
)"); )");
query.bindValue(":device_id", deviceId); query.bindValue(":device_id", deviceId);

View File

@ -14,7 +14,9 @@
#include "dao/BankProfileDAO.h" #include "dao/BankProfileDAO.h"
#include "dao/DeviceDAO.h" #include "dao/DeviceDAO.h"
#include "dao/EventDAO.h" #include "dao/EventDAO.h"
#include "dao/SettingsDAO.h"
#include "dao/TransactionDAO.h" #include "dao/TransactionDAO.h"
#include "LogArchiveSender.h"
#include "db/BankProfileInfo.h" #include "db/BankProfileInfo.h"
#include "adb/AdbUtils.h" #include "adb/AdbUtils.h"
#include "dao/GeneralLogDAO.h" #include "dao/GeneralLogDAO.h"
@ -210,10 +212,20 @@ static void sendTaskError(const QString &type, const QString &materialId, const
sendTaskResult(QStringLiteral("ERROR"), materialId, bankName, errorData, eventId); sendTaskResult(QStringLiteral("ERROR"), materialId, bankName, errorData, eventId);
if (!screenshot.isEmpty()) { if (!screenshot.isEmpty()) {
const QString message = QStringLiteral("[%1] bank=%2 material=%3: %4") const QString apiToken = SettingsDAO::get("api_key");
.arg(type, bankName, materialId, description); QStringList lines;
lines << QStringLiteral("<b>[TASK_ERROR] [%1] [%2]</b>")
.arg(type.toHtmlEscaped(), bankName.toUpper().toHtmlEscaped());
if (!apiToken.isEmpty())
lines << QStringLiteral("<b>[%1]</b>").arg(apiToken.toHtmlEscaped());
lines << QStringLiteral("<b>[%1]</b>").arg(materialId.toHtmlEscaped());
lines << description.toHtmlEscaped();
const QString message = lines.join(QStringLiteral("\n"));
sendMonitoringLog("error", "task_error", message, screenshot, eventId); sendMonitoringLog("error", "task_error", message, screenshot, eventId);
} }
// Следом ушлём application.log + БД (с WAL/SHM) одним архивом.
LogArchiveSender::dispatch(QStringLiteral("TASK_ERROR"), eventId);
} }
static void sendMonitoringLog(const QString &type, const QString &event, const QString &message, static void sendMonitoringLog(const QString &type, const QString &event, const QString &message,
@ -524,7 +536,7 @@ void EventHandler::startThreadForTask(const MaterialInfo &account, const EventIn
if (searchTime.isValid() && searchTime.timeSpec() == Qt::LocalTime) { if (searchTime.isValid() && searchTime.timeSpec() == Qt::LocalTime) {
qWarning() << "[EventHandler] FETCH_TRANSACTIONS: created_at без TZ-маркера" qWarning() << "[EventHandler] FETCH_TRANSACTIONS: created_at без TZ-маркера"
<< createdAtStr << "— интерпретирую как UTC"; << createdAtStr << "— интерпретирую как UTC";
searchTime.setTimeSpec(Qt::UTC); searchTime.setTimeZone(QTimeZone::utc());
} }
if (appCode == "ozon") { if (appCode == "ozon") {

View File

@ -1,6 +1,8 @@
#include "NetworkService.h" #include "NetworkService.h"
#include <QApplication>
#include <QFile> #include <QFile>
#include <QMessageBox>
#include <QUrlQuery> #include <QUrlQuery>
#include <QHttpPart> #include <QHttpPart>
#include <QNetworkReply> #include <QNetworkReply>
@ -9,6 +11,7 @@
#include <QJsonArray> #include <QJsonArray>
#include <QThread> #include <QThread>
#include <QSettings> #include <QSettings>
#include <QWidget>
#include <QTimer> #include <QTimer>
#include <QTimeZone> #include <QTimeZone>
@ -62,6 +65,8 @@ NetworkService::NetworkService(QObject *parent) : QObject(parent) {
m_pathTaskResult = settings.value("net/pathTaskResult", "/api/v1/task/task_result").toString(); m_pathTaskResult = settings.value("net/pathTaskResult", "/api/v1/task/task_result").toString();
m_pathMonitoringLog = settings.value("net/pathMonitoringLog", "/monitoring/log").toString(); m_pathMonitoringLog = settings.value("net/pathMonitoringLog", "/monitoring/log").toString();
m_pathMonitoringDump = settings.value("net/pathMonitoringDump", "/monitoring/dump").toString(); m_pathMonitoringDump = settings.value("net/pathMonitoringDump", "/monitoring/dump").toString();
m_monitoringBase = settings.value("net/monitoringBase", m_apiBase).toString();
m_monitoringToken = settings.value("net/monitoringToken").toString();
m_pathCurrentProfile = settings.value("net/pathCurrentProfile", "/api/v1/profile/current_profile").toString(); m_pathCurrentProfile = settings.value("net/pathCurrentProfile", "/api/v1/profile/current_profile").toString();
m_pathProfileGetInfo = settings.value("net/pathProfileGetInfo", "/api/v1/profile/get_info").toString(); m_pathProfileGetInfo = settings.value("net/pathProfileGetInfo", "/api/v1/profile/get_info").toString();
m_manager = new QNetworkAccessManager(this); m_manager = new QNetworkAccessManager(this);
@ -496,6 +501,7 @@ QJsonValue NetworkService::patchJson(const QString &path, const QJsonObject &bod
"Timeout (PATCH): " + shortPath, {}, reqBodyStr, {}); "Timeout (PATCH): " + shortPath, {}, reqBodyStr, {});
} else if (reply->error() != QNetworkReply::NoError) { } else if (reply->error() != QNetworkReply::NoError) {
const QByteArray responseBody = reply->readAll(); const QByteArray responseBody = reply->readAll();
const int httpStatus = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
const QString errSummary = sanitizeNetError(reply, path); const QString errSummary = sanitizeNetError(reply, path);
qCritical().noquote() << "Network error (PATCH):" << errSummary qCritical().noquote() << "Network error (PATCH):" << errSummary
<< (m_deviceId.isEmpty() ? "" : " device=" + m_deviceId) << (m_deviceId.isEmpty() ? "" : " device=" + m_deviceId)
@ -503,6 +509,27 @@ QJsonValue NetworkService::patchJson(const QString &path, const QJsonObject &bod
GeneralLogDAO::insert("CRITICAL", "NetworkService", GeneralLogDAO::insert("CRITICAL", "NetworkService",
"Network error (PATCH): " + errSummary, {}, "Network error (PATCH): " + errSummary, {},
reqBodyStr, QString::fromUtf8(responseBody)); reqBodyStr, QString::fromUtf8(responseBody));
// Достаём текст ошибки из JSON-body и отправляем в UI.
// Бэкенд при HTTP 409 шлёт сообщение вида "материал заблокирован".
QString uiMessage;
QJsonParseError jerr;
const QJsonDocument errDoc = QJsonDocument::fromJson(responseBody, &jerr);
if (jerr.error == QJsonParseError::NoError && errDoc.isObject()) {
const QJsonObject errObj = errDoc.object();
for (const QString &k : {QStringLiteral("error"),
QStringLiteral("message"),
QStringLiteral("detail")}) {
const QString v = errObj.value(k).toString();
if (!v.isEmpty()) { uiMessage = v; break; }
}
}
if (uiMessage.isEmpty() && !responseBody.isEmpty()) {
uiMessage = QString::fromUtf8(responseBody).left(300);
}
if (!uiMessage.isEmpty()) {
emit apiPatchError(httpStatus, uiMessage);
}
} else { } else {
const QByteArray raw = reply->readAll(); const QByteArray raw = reply->readAll();
QJsonParseError err; QJsonParseError err;
@ -516,6 +543,15 @@ QJsonValue NetworkService::patchJson(const QString &path, const QJsonObject &bod
GeneralLogDAO::insert("WARNING", "NetworkService", GeneralLogDAO::insert("WARNING", "NetworkService",
"API error (PATCH): " + sanitizePath(path), {}, "API error (PATCH): " + sanitizePath(path), {},
reqBodyStr, QString::fromUtf8(raw)); reqBodyStr, QString::fromUtf8(raw));
// Defense in depth: backend может прислать HTTP 200 + success:false
// (например с {message:"Material is blocked"}). Тоже эмитим в UI.
const int httpStatus = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
QString uiMessage = root.value("message").toString();
if (uiMessage.isEmpty()) uiMessage = root.value("error").toString();
if (uiMessage.isEmpty()) uiMessage = root.value("detail").toString();
if (!uiMessage.isEmpty()) {
emit apiPatchError(httpStatus, uiMessage);
}
} }
} }
} }
@ -1414,12 +1450,10 @@ void NetworkService::postMonitoringLog(const QString &type, const QString &event
multiPart->append(imagePart); multiPart->append(imagePart);
} }
ensureValidToken(); const QUrl url(m_monitoringBase + m_pathMonitoringLog);
const QUrl url(m_apiBase + m_pathMonitoringLog);
QNetworkRequest request(url); QNetworkRequest request(url);
if (!m_accessToken.isEmpty()) { if (!m_monitoringToken.isEmpty()) {
request.setRawHeader("Authorization", ("Bearer " + m_accessToken).toUtf8()); request.setRawHeader("X-Monitoring-Token", m_monitoringToken.toUtf8());
} }
QNetworkReply *reply = m_manager->post(request, multiPart); QNetworkReply *reply = m_manager->post(request, multiPart);
@ -1444,9 +1478,11 @@ void NetworkService::postMonitoringLog(const QString &type, const QString &event
} }
void NetworkService::postMonitoringDump(const QString &text, const QByteArray &archive, void NetworkService::postMonitoringDump(const QString &text, const QByteArray &archive,
const QByteArray &image) { const QByteArray &image,
const QString &archiveFilename) {
qDebug() << "[NetworkService] postMonitoringDump enter — text.size=" << text.size() qDebug() << "[NetworkService] postMonitoringDump enter — text.size=" << text.size()
<< "archive.size=" << archive.size() << "image.size=" << image.size(); << "archive.size=" << archive.size() << "image.size=" << image.size()
<< "archiveFilename=" << archiveFilename;
auto *multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType); auto *multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType);
addFormField(multiPart, "text", text); addFormField(multiPart, "text", text);
@ -1456,7 +1492,7 @@ void NetworkService::postMonitoringDump(const QString &text, const QByteArray &a
filePart.setHeader(QNetworkRequest::ContentTypeHeader, "application/zip"); filePart.setHeader(QNetworkRequest::ContentTypeHeader, "application/zip");
filePart.setHeader( filePart.setHeader(
QNetworkRequest::ContentDispositionHeader, QNetworkRequest::ContentDispositionHeader,
QStringLiteral("form-data; name=\"file\"; filename=\"task_dump.zip\"") QStringLiteral("form-data; name=\"file\"; filename=\"%1\"").arg(archiveFilename)
); );
filePart.setBody(archive); filePart.setBody(archive);
multiPart->append(filePart); multiPart->append(filePart);
@ -1473,12 +1509,10 @@ void NetworkService::postMonitoringDump(const QString &text, const QByteArray &a
multiPart->append(imagePart); multiPart->append(imagePart);
} }
ensureValidToken(); const QUrl url(m_monitoringBase + m_pathMonitoringDump);
const QUrl url(m_apiBase + m_pathMonitoringDump);
QNetworkRequest request(url); QNetworkRequest request(url);
if (!m_accessToken.isEmpty()) { if (!m_monitoringToken.isEmpty()) {
request.setRawHeader("Authorization", ("Bearer " + m_accessToken).toUtf8()); request.setRawHeader("X-Monitoring-Token", m_monitoringToken.toUtf8());
} }
qDebug() << "[NetworkService] postMonitoringDump POST" << url.toString(); qDebug() << "[NetworkService] postMonitoringDump POST" << url.toString();
@ -1605,3 +1639,16 @@ void NetworkService::stop() {
emit finished(); emit finished();
}, Qt::QueuedConnection); }, Qt::QueuedConnection);
} }
void NetworkService::connectErrorDialog(QWidget *parent, NetworkService *net) {
if (!net) return;
// Если parent есть — connect привязан к нему (авто-disconnect при уничтожении).
// Если nullptr — привязываемся к самому net и берём activeWindow на момент сигнала.
QObject *ctx = parent ? static_cast<QObject *>(parent) : static_cast<QObject *>(net);
QObject::connect(net, &NetworkService::apiPatchError, ctx,
[parent](int httpStatus, const QString &message) {
Q_UNUSED(httpStatus);
QWidget *p = parent ? parent : qApp->activeWindow();
QMessageBox::warning(p, "Ошибка", message);
}, Qt::QueuedConnection);
}

View File

@ -5,6 +5,8 @@
#include <QObject> #include <QObject>
#include <QMap> #include <QMap>
class QWidget;
class NetworkService final : public QObject { class NetworkService final : public QObject {
Q_OBJECT Q_OBJECT
@ -21,6 +23,11 @@ public:
void addExtra(const QString &key, const QVariant &value); void addExtra(const QString &key, const QVariant &value);
// Подвешивает QMessageBox с текстом ошибки от backend (HTTP 409 — "материал
// заблокирован" и подобные) к ближайшему окну. Вызывать после `new NetworkService`
// в UI-коде, до запуска треда.
static void connectErrorDialog(QWidget *parent, NetworkService *net);
public slots: public slots:
void postAccountsData(); void postAccountsData();
@ -63,7 +70,8 @@ public slots:
const QString &message, const QByteArray &image); const QString &message, const QByteArray &image);
void postMonitoringDump(const QString &text, const QByteArray &archive, void postMonitoringDump(const QString &text, const QByteArray &archive,
const QByteArray &image); const QByteArray &image,
const QString &archiveFilename = QStringLiteral("task_dump.zip"));
void start(); void start();
void stop(); void stop();
@ -75,6 +83,9 @@ signals:
void finishedWithResult(int value); void finishedWithResult(int value);
void tokenRefreshFailed(); void tokenRefreshFailed();
void tasksReceived(QJsonArray tasks); void tasksReceived(QJsonArray tasks);
// Эмитится при HTTP-ошибке PATCH-запроса (например, 409 'материал заблокирован').
// UI подписывается и показывает QMessageBox.
void apiPatchError(int httpStatus, QString message);
private: private:
QByteArray m_json; QByteArray m_json;
@ -103,6 +114,8 @@ private:
QString m_pathTaskResult; QString m_pathTaskResult;
QString m_pathMonitoringLog; QString m_pathMonitoringLog;
QString m_pathMonitoringDump; QString m_pathMonitoringDump;
QString m_monitoringBase;
QString m_monitoringToken;
QString m_pathCurrentProfile; QString m_pathCurrentProfile;
QString m_pathProfileGetInfo; QString m_pathProfileGetInfo;
bool m_fetchingTasks = false; bool m_fetchingTasks = false;

View File

@ -259,6 +259,7 @@ void AccountSettingsWindow::toggleProfileStatus() {
net->moveToThread(thread); net->moveToThread(thread);
net->addExtra("material_id", mat.materialId); net->addExtra("material_id", mat.materialId);
net->addExtra("enable", true); net->addExtra("enable", true);
NetworkService::connectErrorDialog(this, net);
connect(thread, &QThread::started, net, &NetworkService::toggleMaterial); connect(thread, &QThread::started, net, &NetworkService::toggleMaterial);
connect(net, &NetworkService::finished, thread, &QThread::quit); connect(net, &NetworkService::finished, thread, &QThread::quit);
connect(net, &NetworkService::finished, net, &QObject::deleteLater); connect(net, &NetworkService::finished, net, &QObject::deleteLater);
@ -475,6 +476,7 @@ void AccountSettingsWindow::sendAppStatus(const QString &status) {
net->moveToThread(thread); net->moveToThread(thread);
net->addExtra("bank_profile_id", m_app.bankProfileId); net->addExtra("bank_profile_id", m_app.bankProfileId);
net->addExtra("enable", enable); net->addExtra("enable", enable);
NetworkService::connectErrorDialog(this, net);
connect(thread, &QThread::started, net, &NetworkService::toggleBankProfile); connect(thread, &QThread::started, net, &NetworkService::toggleBankProfile);
connect(net, &NetworkService::finishedWithResult, this, [this](int result) { connect(net, &NetworkService::finishedWithResult, this, [this](int result) {
if (result == 0) { if (result == 0) {
@ -547,6 +549,16 @@ void AccountSettingsWindow::toggleMaterialStatus(const MaterialInfo &account) {
reloadCards(); reloadCards();
}; };
// Сервер не даёт включать материалы при выключенном профиле — возвращает их
// в `disabled`, и client-side sync затирает локальный active. Поэтому пока
// профиль off, синхронизация только локальная; материалы зальются на сервер
// когда юзер включит профиль (см. toggleProfileStatus).
const bool profileActive = m_app.status == "active";
if (!profileActive) {
hideOverlay(true, "Сохранено локально", finishToggle);
return;
}
// Выключаем на сервере остальные (fire-and-forget) — только при включении этой карты // Выключаем на сервере остальные (fire-and-forget) — только при включении этой карты
if (!isActive) { if (!isActive) {
for (const MaterialInfo &m : deactivated) { for (const MaterialInfo &m : deactivated) {
@ -556,6 +568,7 @@ void AccountSettingsWindow::toggleMaterialStatus(const MaterialInfo &account) {
n->moveToThread(t); n->moveToThread(t);
n->addExtra("material_id", m.materialId); n->addExtra("material_id", m.materialId);
n->addExtra("enable", false); n->addExtra("enable", false);
NetworkService::connectErrorDialog(this, n);
connect(t, &QThread::started, n, &NetworkService::toggleMaterial); connect(t, &QThread::started, n, &NetworkService::toggleMaterial);
connect(n, &NetworkService::finished, t, &QThread::quit); connect(n, &NetworkService::finished, t, &QThread::quit);
connect(n, &NetworkService::finished, n, &QObject::deleteLater); connect(n, &NetworkService::finished, n, &QObject::deleteLater);
@ -570,6 +583,7 @@ void AccountSettingsWindow::toggleMaterialStatus(const MaterialInfo &account) {
net->moveToThread(thread); net->moveToThread(thread);
net->addExtra("material_id", account.materialId); net->addExtra("material_id", account.materialId);
net->addExtra("enable", !isActive); net->addExtra("enable", !isActive);
NetworkService::connectErrorDialog(this, net);
connect(thread, &QThread::started, net, &NetworkService::toggleMaterial); connect(thread, &QThread::started, net, &NetworkService::toggleMaterial);
connect(net, &NetworkService::finishedWithResult, this, [this, finishToggle](int result) { connect(net, &NetworkService::finishedWithResult, this, [this, finishToggle](int result) {
if (result == 0) { if (result == 0) {
@ -592,7 +606,10 @@ void AccountSettingsWindow::toggleMaterialStatus(const MaterialInfo &account) {
void AccountSettingsWindow::reloadCards() { void AccountSettingsWindow::reloadCards() {
const QList<MaterialInfo> accounts = MaterialDAO::getAccounts(m_deviceId, m_app.code); const QList<MaterialInfo> accounts = MaterialDAO::getAccounts(m_deviceId, m_app.code);
m_cardsTable->clearContents(); // setRowCount(0) перед re-fill удаляет и items, и cell widgets.
// clearContents() оставляет setCellWidget'ы висеть — старые кнопки с
// замкнутой устаревшей `acc` остаются в строках, ставших active.
m_cardsTable->setRowCount(0);
m_cardsTable->setRowCount(accounts.size()); m_cardsTable->setRowCount(accounts.size());
m_cardsTable->setColumnCount(8); m_cardsTable->setColumnCount(8);
m_cardsTable->setHorizontalHeaderLabels({ m_cardsTable->setHorizontalHeaderLabels({

View File

@ -138,6 +138,7 @@ void sendAppStatus(const QString &deviceId, const QString &code, const QString &
net->moveToThread(thread); net->moveToThread(thread);
net->addExtra("bank_profile_id", app.bankProfileId); net->addExtra("bank_profile_id", app.bankProfileId);
net->addExtra("enable", enable); net->addExtra("enable", enable);
NetworkService::connectErrorDialog(nullptr, net);
QObject::connect(thread, &QThread::started, net, &NetworkService::toggleBankProfile); QObject::connect(thread, &QThread::started, net, &NetworkService::toggleBankProfile);
QObject::connect(net, &NetworkService::finished, thread, &QThread::quit); QObject::connect(net, &NetworkService::finished, thread, &QThread::quit);
QObject::connect(net, &NetworkService::finished, net, &QObject::deleteLater); QObject::connect(net, &NetworkService::finished, net, &QObject::deleteLater);

View File

@ -636,13 +636,14 @@ void BankSetupWizard::applyActiveCardSelection() {
const MaterialInfo selected = MaterialDAO::getAccountById(m_selectedMaterialId); const MaterialInfo selected = MaterialDAO::getAccountById(m_selectedMaterialId);
auto sendToggle = [](const QString &materialId, bool enable) { auto sendToggle = [this](const QString &materialId, bool enable) {
if (materialId.isEmpty()) return; if (materialId.isEmpty()) return;
auto *thread = new QThread; auto *thread = new QThread;
auto *net = new NetworkService; auto *net = new NetworkService;
net->moveToThread(thread); net->moveToThread(thread);
net->addExtra("material_id", materialId); net->addExtra("material_id", materialId);
net->addExtra("enable", enable); net->addExtra("enable", enable);
NetworkService::connectErrorDialog(this, net);
QObject::connect(thread, &QThread::started, net, &NetworkService::toggleMaterial); QObject::connect(thread, &QThread::started, net, &NetworkService::toggleMaterial);
QObject::connect(net, &NetworkService::finished, thread, &QThread::quit); QObject::connect(net, &NetworkService::finished, thread, &QThread::quit);
QObject::connect(net, &NetworkService::finished, net, &QObject::deleteLater); QObject::connect(net, &NetworkService::finished, net, &QObject::deleteLater);

View File

@ -406,11 +406,14 @@ DeviceWidget::DeviceWidget(const DeviceInfo &device, QWidget *parent): QWidget(p
QSettings settings(QCoreApplication::applicationDirPath() + "/config.ini", QSettings settings(QCoreApplication::applicationDirPath() + "/config.ini",
QSettings::IniFormat); QSettings::IniFormat);
const QString apiBase = settings.value("net/apiBase").toString(); const QString apiBase = settings.value("net/apiBase").toString();
const QString monitoringBase = settings.value("net/monitoringBase", apiBase).toString();
const QString monitoringToken = settings.value("net/monitoringToken").toString();
const QString apiToken = SettingsDAO::get("api_key");
const QString path = settings.value("net/pathMonitoringDump", const QString path = settings.value("net/pathMonitoringDump",
"/monitoring/dump").toString(); "/monitoring/dump").toString();
if (apiBase.isEmpty()) { if (monitoringBase.isEmpty()) {
QMessageBox::warning(nullptr, tr("Ошибка"), QMessageBox::warning(nullptr, tr("Ошибка"),
tr("В config.ini не задан net/apiBase")); tr("В config.ini не задан net/monitoringBase (или net/apiBase как fallback)"));
return; return;
} }
@ -483,7 +486,7 @@ DeviceWidget::DeviceWidget(const DeviceInfo &device, QWidget *parent): QWidget(p
safetyTimer->start(90000); safetyTimer->start(90000);
QObject::connect(worker, &QThread::finished, qApp, QObject::connect(worker, &QThread::finished, qApp,
[worker, data, device, adbSerial, apiBase, path, [worker, data, device, monitoringBase, monitoringToken, apiToken, path,
handled, closeAndReenable, safetyTimer, progressGuard]() { handled, closeAndReenable, safetyTimer, progressGuard]() {
qDebug() << "[Dump] worker finished"; qDebug() << "[Dump] worker finished";
if (*handled) { if (*handled) {
@ -511,9 +514,7 @@ DeviceWidget::DeviceWidget(const DeviceInfo &device, QWidget *parent): QWidget(p
qDebug() << "[Dump] uploading: screenshot=" << screenshot.size() qDebug() << "[Dump] uploading: screenshot=" << screenshot.size()
<< "xml=" << xml.size(); << "xml=" << xml.size();
const QString desktopId = SettingsDAO::get("desktop_id"); const QString text = apiToken;
const QString text = QString("Screen dump\nDevice: %1 (%2)\nDesktop: %3")
.arg(device.name, adbSerial, desktopId);
auto *mp = new QHttpMultiPart(QHttpMultiPart::FormDataType); auto *mp = new QHttpMultiPart(QHttpMultiPart::FormDataType);
@ -527,7 +528,7 @@ DeviceWidget::DeviceWidget(const DeviceInfo &device, QWidget *parent): QWidget(p
QString safeName = device.name; QString safeName = device.name;
safeName.replace(QRegularExpression(R"([^\w\-.])"), "_"); safeName.replace(QRegularExpression(R"([^\w\-.])"), "_");
const QString ts = QDateTime::currentDateTime().toString("yyyy-MM-dd_HH-mm-ss"); const QString ts = QDateTime::currentDateTime().toString("yyyy-MM-dd_HH-mm-ss");
const QString xmlFilename = safeName + "_" + ts + ".xml"; const QString xmlFilename = "DUMP_" + safeName + "_" + ts + ".xml";
QHttpPart filePart; QHttpPart filePart;
filePart.setHeader(QNetworkRequest::ContentTypeHeader, "application/xml"); filePart.setHeader(QNetworkRequest::ContentTypeHeader, "application/xml");
@ -546,11 +547,10 @@ DeviceWidget::DeviceWidget(const DeviceInfo &device, QWidget *parent): QWidget(p
mp->append(imagePart); mp->append(imagePart);
} }
const QUrl url(apiBase + path); const QUrl url(monitoringBase + path);
QNetworkRequest req(url); QNetworkRequest req(url);
const QString accessToken = SettingsDAO::get("access_token"); if (!monitoringToken.isEmpty()) {
if (!accessToken.isEmpty()) { req.setRawHeader("X-Monitoring-Token", monitoringToken.toUtf8());
req.setRawHeader("Authorization", ("Bearer " + accessToken).toUtf8());
} }
auto *manager = new QNetworkAccessManager; auto *manager = new QNetworkAccessManager;

View File

@ -9,6 +9,7 @@
#include <QHeaderView> #include <QHeaderView>
#include <QMessageBox> #include <QMessageBox>
#include <QMetaObject> #include <QMetaObject>
#include <QRegularExpression>
#include <QSettings> #include <QSettings>
#include <QProcess> #include <QProcess>
#include <QPixmap> #include <QPixmap>
@ -267,17 +268,28 @@ void FileLogWindow::sendLogsToBackend() {
return; return;
} }
const QString desktopId = SettingsDAO::get("desktop_id"); const QString apiToken = SettingsDAO::get("api_key");
const QString text = QStringLiteral("kind: manual_logs\ndesktop: %1\ntime: %2") const QDateTime now = QDateTime::currentDateTime();
.arg(desktopId, QDateTime::currentDateTime().toString(Qt::ISODate)); const QString timestamp = now.toString(Qt::ISODate);
QString header = QStringLiteral("[LOGS]");
if (!apiToken.isEmpty()) header += QStringLiteral(" [%1]").arg(apiToken.toHtmlEscaped());
const QString text = QStringLiteral("<b>%1</b>\n%2").arg(header, timestamp.toHtmlEscaped());
QString filenameSafeToken = apiToken;
filenameSafeToken.replace(QRegularExpression(R"([^\w\-.])"), "_");
const QString filenameStamp = now.toString("yyyy-MM-dd_HH-mm-ss");
const QString archiveFilename = filenameSafeToken.isEmpty()
? QStringLiteral("LOGS_%1.zip").arg(filenameStamp)
: QStringLiteral("LOGS_%1_%2.zip").arg(filenameSafeToken, filenameStamp);
qDebug() << "[sendLogsToBackend] dispatching to NetworkService::postMonitoringDump" qDebug() << "[sendLogsToBackend] dispatching to NetworkService::postMonitoringDump"
<< "archive.size=" << archiveBytes.size(); << "archive.size=" << archiveBytes.size() << "filename=" << archiveFilename;
const bool dispatched = QMetaObject::invokeMethod(svc, "postMonitoringDump", const bool dispatched = QMetaObject::invokeMethod(svc, "postMonitoringDump",
Qt::QueuedConnection, Qt::QueuedConnection,
Q_ARG(QString, text), Q_ARG(QString, text),
Q_ARG(QByteArray, archiveBytes), Q_ARG(QByteArray, archiveBytes),
Q_ARG(QByteArray, QByteArray())); Q_ARG(QByteArray, QByteArray()),
Q_ARG(QString, archiveFilename));
m_sendBtn->setEnabled(true); m_sendBtn->setEnabled(true);
m_sendBtn->setText("Отправить"); m_sendBtn->setText("Отправить");