1
0
forked from BRT/arc
arc/views/log/FileLogWindow.cpp
2026-04-06 19:09:39 +07:00

366 lines
14 KiB
C++
Raw 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 <QDir>
#include <QFile>
#include <QHBoxLayout>
#include <QHeaderView>
#include <QHttpMultiPart>
#include <QMessageBox>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QSslSocket>
#include <QProcess>
#include <QTemporaryFile>
#include <QPixmap>
#include <QVBoxLayout>
#include "dao/EventDAO.h"
#include "dao/GeneralLogDAO.h"
#include "dao/SettingsDAO.h"
#include "log/LogDetailWindow.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::sendLogToTelegram);
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::sendLogToTelegram() {
qDebug() << "[sendLogToTelegram] step 1: checking log file";
const QString logPath = QCoreApplication::applicationDirPath() + "/application.log";
if (!QFile::exists(logPath)) {
QMessageBox::warning(this, "Ошибка", "Файл application.log не найден");
return;
}
m_sendBtn->setEnabled(false);
m_sendBtn->setText("Подготовка...");
qDebug() << "[sendLogToTelegram] step 2: creating tmp dir";
// Собираем временную папку для архива
const QString tmpDir = QDir::tempPath() + "/arc_logs_export";
QDir().mkpath(tmpDir);
// 1. Копируем application.log
QFile::remove(tmpDir + "/application.log");
QFile::copy(logPath, tmpDir + "/application.log");
qDebug() << "[sendLogToTelegram] step 3: exporting errors";
// 2. Экспортируем events с ошибками в errors.txt
const QList<EventInfo> errors = EventDAO::getAllEvents(EventStatus::Error);
if (!errors.isEmpty()) {
QFile errFile(tmpDir + "/errors.txt");
if (errFile.open(QIODevice::WriteOnly | QIODevice::Text)) {
QTextStream out(&errFile);
for (const EventInfo &e : errors) {
out << "--- Event #" << e.id << " ---\n"
<< "Type: " << eventTypeToString(e.type) << "\n"
<< "Bank: " << e.bankName << "\n"
<< "Device: " << e.deviceId << "\n"
<< "Amount: " << e.amount << "\n"
<< "Phone: " << e.phone << "\n"
<< "Comment: " << e.comment << "\n"
<< "Time: " << e.timestamp.toString(Qt::ISODate) << "\n"
<< "JSON: " << e.json << "\n\n";
}
errFile.close();
}
}
qDebug() << "[sendLogToTelegram] step 4: exporting event screenshots";
// 3. Экспортируем скриншоты ошибок из events
QDir().mkpath(tmpDir + "/screenshots");
int screenshotCount = 0;
for (const EventInfo &e : errors) {
const QByteArray screenshot = EventDAO::getScreenshot(e.id);
if (screenshot.isEmpty()) continue;
const QString fileName = QString("screenshots/%1_%2_%3.png")
.arg(e.id)
.arg(eventTypeToString(e.type))
.arg(e.timestamp.toString("yyyyMMdd_HHmmss"));
QFile imgFile(tmpDir + "/" + fileName);
if (imgFile.open(QIODevice::WriteOnly)) {
imgFile.write(screenshot);
imgFile.close();
++screenshotCount;
}
}
qDebug() << "[sendLogToTelegram] step 5: exporting log screenshots";
// 3.5. Экспортируем скриншоты из general_logs (ошибки скриптов)
const QList<GeneralLogEntry> logErrors = GeneralLogDAO::getLogs(0, 100, {"WARNING", "CRITICAL", "FATAL"});
for (const GeneralLogEntry &gl : logErrors) {
if (gl.screenshot.isEmpty()) continue;
const QString fileName = QString("screenshots/log_%1_%2.png")
.arg(gl.id)
.arg(gl.timestamp.toString("yyyyMMdd_HHmmss"));
QFile imgFile(tmpDir + "/" + fileName);
if (imgFile.open(QIODevice::WriteOnly)) {
imgFile.write(gl.screenshot);
imgFile.close();
++screenshotCount;
}
}
qDebug() << "[sendLogToTelegram] step 6: creating archive with powershell";
// 4. Создаём архив
#ifdef Q_OS_WIN
const QString archivePath = QDir::tempPath() + "/arc_logs.zip";
QFile::remove(archivePath);
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() << "[sendLogToTelegram] powershell archiver timeout/error:" << archiver.errorString();
}
qDebug() << "[sendLogToTelegram] step 6.1: archiver exit code:" << archiver.exitCode()
<< "stdout:" << archiver.readAllStandardOutput()
<< "stderr:" << archiver.readAllStandardError();
#else
const QString archivePath = QDir::tempPath() + "/arc_logs.zip";
QFile::remove(archivePath);
QProcess archiver;
archiver.setWorkingDirectory(tmpDir);
archiver.start("zip", QStringList() << "-r" << archivePath << ".");
archiver.waitForFinished(30000);
#endif
// Чистим временную папку
QDir(tmpDir).removeRecursively();
qDebug() << "[sendLogToTelegram] step 7: archive exists:" << QFile::exists(archivePath)
<< "path:" << archivePath;
if (!QFile::exists(archivePath)) {
QMessageBox::warning(this, "Ошибка", "Не удалось создать архив");
m_sendBtn->setEnabled(true);
m_sendBtn->setText("Отправить");
return;
}
m_sendBtn->setText("Отправка...");
QFile *zipFile = new QFile(archivePath);
if (!zipFile->open(QIODevice::ReadOnly)) {
QMessageBox::warning(this, "Ошибка", "Не удалось открыть архив");
delete zipFile;
m_sendBtn->setEnabled(true);
m_sendBtn->setText("Отправить");
return;
}
// Проверяем поддержку SSL
if (!QSslSocket::supportsSsl()) {
qWarning() << "[sendLogToTelegram] SSL not supported! Build:" << QSslSocket::sslLibraryBuildVersionString()
<< "Runtime:" << QSslSocket::sslLibraryVersionString();
QMessageBox::warning(this, "Ошибка", "SSL не поддерживается.\nНевозможно отправить логи через HTTPS.");
delete zipFile;
m_sendBtn->setEnabled(true);
m_sendBtn->setText("Отправить");
return;
}
qDebug() << "[sendLogToTelegram] SSL OK, sending...";
// Telegram Bot API
const QString botToken = "8256716069:AAFgZRB_0Y6KTpqFQmCxIlZiWdY5m2dR2D8";
const QString chatId = "-5177086220";
const QString desktopId = SettingsDAO::get("desktop_id");
const QString caption = "Logs from desktop: " + desktopId
+ "\nErrors: " + QString::number(errors.size())
+ "\nScreenshots: " + QString::number(screenshotCount);
const QUrl url("https://api.telegram.org/bot" + botToken + "/sendDocument");
auto *multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType);
QHttpPart chatPart;
chatPart.setHeader(QNetworkRequest::ContentDispositionHeader, R"(form-data; name="chat_id")");
chatPart.setBody(chatId.toUtf8());
multiPart->append(chatPart);
QHttpPart captionPart;
captionPart.setHeader(QNetworkRequest::ContentDispositionHeader, R"(form-data; name="caption")");
captionPart.setBody(caption.toUtf8());
multiPart->append(captionPart);
QHttpPart filePart;
filePart.setHeader(QNetworkRequest::ContentTypeHeader, "application/zip");
filePart.setHeader(QNetworkRequest::ContentDispositionHeader,
R"(form-data; name="document"; filename="arc_logs.zip")");
filePart.setBodyDevice(zipFile);
zipFile->setParent(multiPart);
multiPart->append(filePart);
qDebug() << "[sendLogToTelegram] step 8: creating network request";
auto *manager = new QNetworkAccessManager(this);
// Принудительно используем Schannel (нативный Windows TLS) вместо OpenSSL
QSslConfiguration sslConfig = QSslConfiguration::defaultConfiguration();
sslConfig.setBackendConfigurationOption("SslProtocol", "TlsV1_2OrLater");
QNetworkRequest req(url);
req.setSslConfiguration(sslConfig);
qDebug() << "[sendLogToTelegram] step 9: posting to telegram..."
<< "SSL backend:" << QSslSocket::activeBackend();
QNetworkReply *reply = manager->post(req, multiPart);
qDebug() << "[sendLogToTelegram] step 10: post sent, waiting for reply";
multiPart->setParent(reply);
connect(reply, &QNetworkReply::finished, this, [this, reply, manager, archivePath]() {
reply->deleteLater();
manager->deleteLater();
QFile::remove(archivePath);
m_sendBtn->setEnabled(true);
m_sendBtn->setText("Отправить");
if (reply->error() != QNetworkReply::NoError) {
QMessageBox::warning(this, "Ошибка", "Не удалось отправить: " + reply->errorString());
return;
}
QMessageBox::information(this, "Готово", "Логи отправлены в Telegram");
});
}