#include "FileLogWindow.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #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(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 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() << "filename=" << archiveFilename; // Подписываемся на результат ДО диспатча. svc живёт в потоке paymentThread — // соединение автоматически становится QueuedConnection, лямбда исполнится в // UI-потоке. Контекст = this ⇒ при закрытии окна Qt сам отключит слот (без // обращения к висячему указателю). Фильтруем по своему archiveFilename: // синглтон общий, параллельно могут уходить и другие дампы (TaskDumpRecorder). auto handled = std::make_shared(false); auto conn = std::make_shared(); *conn = connect(svc, &NetworkService::monitoringDumpFinished, this, [this, conn, handled, expected = archiveFilename]( bool ok, const QString &detail, const QString &fn) { if (fn != expected || *handled) return; *handled = true; QObject::disconnect(*conn); m_sendBtn->setEnabled(true); m_sendBtn->setText("Отправить"); if (ok) { QMessageBox::information(this, "Готово", "Логи отправлены на бэкенд."); } else { QMessageBox::warning(this, "Ошибка", "Не удалось отправить логи: " + detail); } }); m_sendBtn->setText("Отправка..."); const bool dispatched = QMetaObject::invokeMethod(svc, "postMonitoringDump", Qt::QueuedConnection, Q_ARG(QString, text), Q_ARG(QByteArray, archiveBytes), Q_ARG(QByteArray, QByteArray()), Q_ARG(QString, archiveFilename)); if (!dispatched) { *handled = true; QObject::disconnect(*conn); m_sendBtn->setEnabled(true); m_sendBtn->setText("Отправить"); QMessageBox::warning(this, "Ошибка", "Не удалось поставить отправку в очередь NetworkService."); } }