1
0
forked from BRT/arc
arc/views/log/FileLogWindow.cpp
slava a6509b5653 - Add database migration for screenshot column in general_logs.
- Implement `cleanupOldData` in `DatabaseManager` to remove outdated entries and optimize storage.
- Introduce `unlockScreen` in `AdbUtils` and integrate it into common scripts for screen unlocking.
- Enhance `GeneralLogDAO` to support screenshot storage and retrieval.
- Expand `FileLogWindow` with screenshot viewer and horizontal layout.
- Add device ID migration logic in `BankProfileDAO` and `MaterialDAO`.
- Improve error handling in `EventHandler` by saving and logging screenshots for critical events.
2026-03-31 13:08:02 +07:00

309 lines
12 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 <QProcess>
#include <QTemporaryFile>
#include <QPixmap>
#include <QVBoxLayout>
#include "dao/EventDAO.h"
#include "dao/GeneralLogDAO.h"
#include "dao/SettingsDAO.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_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() {
const QString logPath = QCoreApplication::applicationDirPath() + "/application.log";
if (!QFile::exists(logPath)) {
QMessageBox::warning(this, "Ошибка", "Файл application.log не найден");
return;
}
m_sendBtn->setEnabled(false);
m_sendBtn->setText("Подготовка...");
// Собираем временную папку для архива
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");
// 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();
}
}
// 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;
}
}
// 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;
}
}
// 4. Создаём ZIP
const QString zipPath = QDir::tempPath() + "/arc_logs.zip";
QFile::remove(zipPath);
QProcess zip;
zip.setWorkingDirectory(tmpDir);
zip.start("zip", QStringList() << "-r" << zipPath << ".");
zip.waitForFinished(30000);
// Чистим временную папку
QDir(tmpDir).removeRecursively();
if (zip.exitCode() != 0 || !QFile::exists(zipPath)) {
QMessageBox::warning(this, "Ошибка", "Не удалось создать архив");
m_sendBtn->setEnabled(true);
m_sendBtn->setText("Отправить");
return;
}
m_sendBtn->setText("Отправка...");
QFile *zipFile = new QFile(zipPath);
if (!zipFile->open(QIODevice::ReadOnly)) {
QMessageBox::warning(this, "Ошибка", "Не удалось открыть архив");
delete zipFile;
m_sendBtn->setEnabled(true);
m_sendBtn->setText("Отправить");
return;
}
// 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);
auto *manager = new QNetworkAccessManager(this);
QNetworkReply *reply = manager->post(QNetworkRequest(url), multiPart);
multiPart->setParent(reply);
connect(reply, &QNetworkReply::finished, this, [this, reply, manager, zipPath]() {
reply->deleteLater();
manager->deleteLater();
QFile::remove(zipPath);
m_sendBtn->setEnabled(true);
m_sendBtn->setText("Отправить");
if (reply->error() != QNetworkReply::NoError) {
QMessageBox::warning(this, "Ошибка", "Не удалось отправить: " + reply->errorString());
return;
}
QMessageBox::information(this, "Готово", "Логи отправлены в Telegram");
});
}