1
0
forked from BRT/arc
arc/views/log/FileLogWindow.cpp
slava 9c2ec74f76 - Add findSumEditText to ScreenXmlParser for enhanced sum field detection.
- Update `PayByPhoneScript` and `PayByCardScript` to use `findSumEditText` as a fallback.
- Enhance `FileLogWindow` to export logs and database files to backend via `NetworkService`.
- Add `networkService` getter in `TaskDumpRecorder` for centralizing access to `NetworkService`.
2026-05-07 22:54:06 +07:00

294 lines
11 KiB
C++
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include "FileLogWindow.h"
#include <QCoreApplication>
#include <QDateTime>
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QHBoxLayout>
#include <QHeaderView>
#include <QMessageBox>
#include <QMetaObject>
#include <QSettings>
#include <QProcess>
#include <QPixmap>
#include <QVBoxLayout>
#include "dump/TaskDumpRecorder.h"
#include "dao/GeneralLogDAO.h"
#include "dao/SettingsDAO.h"
#include "log/LogDetailWindow.h"
#include "net/NetworkService.h"
static constexpr int COL_ID = 0;
static constexpr int COL_TIME = 1;
static constexpr int COL_LEVEL = 2;
static constexpr int COL_SOURCE = 3;
static constexpr int COL_MESSAGE = 4;
FileLogWindow::FileLogWindow(QWidget *parent) : QDialog(parent) {
setWindowTitle("Логи");
resize(1200, 650);
m_table = new QTableWidget(this);
m_table->setColumnCount(5);
m_table->setHorizontalHeaderLabels({"#", "Время", "Уровень", "Источник", "Сообщение"});
m_table->setEditTriggers(QAbstractItemView::NoEditTriggers);
m_table->setSelectionBehavior(QAbstractItemView::SelectRows);
m_table->setSelectionMode(QAbstractItemView::SingleSelection);
m_table->verticalHeader()->hide();
m_table->horizontalHeader()->setStretchLastSection(true);
m_table->setColumnWidth(COL_ID, 45);
m_table->setColumnWidth(COL_TIME, 145);
m_table->setColumnWidth(COL_LEVEL, 80);
m_table->setColumnWidth(COL_SOURCE, 250);
// Панель скриншота
m_screenshotLabel = new QLabel(this);
m_screenshotLabel->setAlignment(Qt::AlignCenter);
m_screenshotLabel->setMinimumWidth(300);
m_screenshotLabel->setStyleSheet("background-color: #f0f0f0; border: 1px solid #ccc;");
m_screenshotLabel->setText("Выберите запись\nдля просмотра скриншота");
auto *splitter = new QSplitter(Qt::Horizontal, this);
splitter->addWidget(m_table);
splitter->addWidget(m_screenshotLabel);
splitter->setStretchFactor(0, 3);
splitter->setStretchFactor(1, 1);
m_showAllCheck = new QCheckBox("Показать все (включая DEBUG/INFO)", this);
m_prevBtn = new QPushButton("", this);
m_nextBtn = new QPushButton("", this);
m_pageLabel = new QLabel(this);
m_pageLabel->setAlignment(Qt::AlignCenter);
m_pageLabel->setMinimumWidth(140);
auto *refreshBtn = new QPushButton("Обновить", this);
m_sendBtn = new QPushButton("Отправить", this);
m_sendBtn->setStyleSheet("background-color: #0088cc; color: white; border-radius: 4px; padding: 4px 12px;");
auto *paginationLayout = new QHBoxLayout;
paginationLayout->addWidget(m_prevBtn);
paginationLayout->addWidget(m_pageLabel);
paginationLayout->addWidget(m_nextBtn);
paginationLayout->addStretch();
paginationLayout->addWidget(m_showAllCheck);
paginationLayout->addWidget(refreshBtn);
paginationLayout->addWidget(m_sendBtn);
auto *mainLayout = new QVBoxLayout(this);
mainLayout->addWidget(splitter, 1);
mainLayout->addLayout(paginationLayout);
connect(m_prevBtn, &QPushButton::clicked, this, [this]() {
if (m_page > 0) { --m_page; loadPage(); }
});
connect(m_nextBtn, &QPushButton::clicked, this, [this]() {
if (m_page < m_totalPages - 1) { ++m_page; loadPage(); }
});
connect(refreshBtn, &QPushButton::clicked, this, [this]() {
m_page = 0;
loadPage();
});
connect(m_showAllCheck, &QCheckBox::toggled, this, [this]() {
m_page = 0;
loadPage();
});
connect(m_table, &QTableWidget::cellClicked, this, [this](int row, int) {
showScreenshot(row);
});
connect(m_table, &QTableWidget::cellDoubleClicked, this, [this](int row, int) {
if (row >= 0 && row < m_entries.size()) {
auto *detail = new LogDetailWindow(m_entries[row], this);
detail->setAttribute(Qt::WA_DeleteOnClose);
detail->show();
}
});
connect(m_sendBtn, &QPushButton::clicked, this, &FileLogWindow::sendLogsToBackend);
loadPage();
}
QStringList FileLogWindow::currentLevelFilter() const {
if (m_showAllCheck->isChecked())
return {};
return {"WARNING", "CRITICAL", "FATAL"};
}
void FileLogWindow::loadPage() {
const QStringList levels = currentLevelFilter();
const int total = GeneralLogDAO::getTotalCount(levels);
m_totalPages = qMax(1, (total + m_pageSize - 1) / m_pageSize);
m_page = qBound(0, m_page, m_totalPages - 1);
m_entries = GeneralLogDAO::getLogs(m_page, m_pageSize, levels);
m_table->setRowCount(static_cast<int>(m_entries.size()));
for (int i = 0; i < m_entries.size(); ++i) {
const auto &e = m_entries[i];
m_table->setItem(i, COL_ID, new QTableWidgetItem(QString::number(e.id)));
m_table->setItem(i, COL_TIME, new QTableWidgetItem(
e.timestamp.toLocalTime().toString("dd.MM.yy HH:mm:ss")));
m_table->setItem(i, COL_LEVEL, new QTableWidgetItem(e.level));
m_table->setItem(i, COL_SOURCE, new QTableWidgetItem(e.source));
m_table->setItem(i, COL_MESSAGE, new QTableWidgetItem(e.message));
}
updatePagination();
}
void FileLogWindow::showScreenshot(int row) {
if (row < 0 || row >= m_entries.size()) return;
const QByteArray &data = m_entries[row].screenshot;
if (data.isEmpty()) {
m_screenshotLabel->setText("Нет скриншота");
return;
}
QPixmap pixmap;
pixmap.loadFromData(data);
if (pixmap.isNull()) {
m_screenshotLabel->setText("Ошибка загрузки");
return;
}
m_screenshotLabel->setPixmap(pixmap.scaled(
m_screenshotLabel->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation));
}
void FileLogWindow::updatePagination() {
m_pageLabel->setText(QString("Страница %1 из %2").arg(m_page + 1).arg(m_totalPages));
m_prevBtn->setEnabled(m_page > 0);
m_nextBtn->setEnabled(m_page < m_totalPages - 1);
}
void FileLogWindow::sendLogsToBackend() {
qDebug() << "[sendLogsToBackend] step 1: checking log file";
const QString logPath = QCoreApplication::applicationDirPath() + "/application.log";
if (!QFile::exists(logPath)) {
QMessageBox::warning(this, "Ошибка", "Файл application.log не найден");
return;
}
// Резолвим путь к БД из config.ini (как DatabaseManager): [db]/dbPath,
// с разворотом '~' в домашнюю директорию и приведением к абсолютному пути
// относительно applicationDirPath, чтобы не зависеть от текущего CWD.
const QSettings settings("config.ini", QSettings::IniFormat);
QString dbPath = settings.value("db/dbPath").toString();
if (dbPath.startsWith("~")) {
dbPath.replace(0, 1, QDir::homePath());
}
if (!dbPath.isEmpty() && QFileInfo(dbPath).isRelative()) {
dbPath = QDir(QCoreApplication::applicationDirPath()).absoluteFilePath(dbPath);
}
if (dbPath.isEmpty() || !QFile::exists(dbPath)) {
QMessageBox::warning(this, "Ошибка",
"Файл БД не найден: " + (dbPath.isEmpty() ? QStringLiteral("<пусто>") : dbPath));
return;
}
NetworkService *svc = TaskDumpRecorder::networkService();
if (!svc) {
QMessageBox::warning(this, "Ошибка",
"NetworkService не инициализирован — отправка на бэкенд недоступна.");
return;
}
m_sendBtn->setEnabled(false);
m_sendBtn->setText("Подготовка...");
qDebug() << "[sendLogsToBackend] step 2: creating tmp dir";
const QString tmpDir = QDir::tempPath() + "/arc_logs_export";
QDir(tmpDir).removeRecursively();
QDir().mkpath(tmpDir);
// 1. application.log целиком
QFile::copy(logPath, tmpDir + "/application.log");
// 2. БД + WAL/SHM-сайдкары (если есть) — иначе при включённом WAL
// снимок будет рассогласованным.
const QString dbBaseName = QFileInfo(dbPath).fileName();
QFile::copy(dbPath, tmpDir + "/" + dbBaseName);
for (const QString &suffix : {QStringLiteral("-wal"), QStringLiteral("-shm")}) {
const QString sidecar = dbPath + suffix;
if (QFile::exists(sidecar)) {
QFile::copy(sidecar, tmpDir + "/" + dbBaseName + suffix);
}
}
qDebug() << "[sendLogsToBackend] step 3: creating archive";
const QString archivePath = QDir::tempPath() + "/arc_logs.zip";
QFile::remove(archivePath);
#ifdef Q_OS_WIN
QProcess archiver;
archiver.setWorkingDirectory(tmpDir);
archiver.start("powershell", QStringList()
<< "-NoProfile" << "-Command"
<< QString("Compress-Archive -Path '%1/*' -DestinationPath '%2' -Force").arg(tmpDir, archivePath));
if (!archiver.waitForFinished(30000)) {
qWarning() << "[sendLogsToBackend] powershell archiver timeout/error:" << archiver.errorString();
}
qDebug() << "[sendLogsToBackend] archiver exit code:" << archiver.exitCode()
<< "stdout:" << archiver.readAllStandardOutput()
<< "stderr:" << archiver.readAllStandardError();
#else
QProcess archiver;
archiver.setWorkingDirectory(tmpDir);
archiver.start("zip", QStringList() << "-r" << archivePath << ".");
archiver.waitForFinished(30000);
#endif
QDir(tmpDir).removeRecursively();
if (!QFile::exists(archivePath)) {
QMessageBox::warning(this, "Ошибка", "Не удалось создать архив");
m_sendBtn->setEnabled(true);
m_sendBtn->setText("Отправить");
return;
}
QFile zipFile(archivePath);
if (!zipFile.open(QIODevice::ReadOnly)) {
QMessageBox::warning(this, "Ошибка", "Не удалось открыть архив");
QFile::remove(archivePath);
m_sendBtn->setEnabled(true);
m_sendBtn->setText("Отправить");
return;
}
const QByteArray archiveBytes = zipFile.readAll();
zipFile.close();
QFile::remove(archivePath);
if (archiveBytes.isEmpty()) {
QMessageBox::warning(this, "Ошибка", "Архив пуст — отправлять нечего");
m_sendBtn->setEnabled(true);
m_sendBtn->setText("Отправить");
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));
qDebug() << "[sendLogsToBackend] dispatching to NetworkService::postMonitoringDump"
<< "archive.size=" << archiveBytes.size();
const bool dispatched = QMetaObject::invokeMethod(svc, "postMonitoringDump",
Qt::QueuedConnection,
Q_ARG(QString, text),
Q_ARG(QByteArray, archiveBytes),
Q_ARG(QByteArray, QByteArray()));
m_sendBtn->setEnabled(true);
m_sendBtn->setText("Отправить");
if (!dispatched) {
QMessageBox::warning(this, "Ошибка",
"Не удалось поставить отправку в очередь NetworkService.");
return;
}
QMessageBox::information(this, "Готово",
"Логи поставлены в очередь на отправку на бэкенд.");
}