- 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.
This commit is contained in:
parent
38f33797c5
commit
292f8b78cf
@ -469,3 +469,33 @@ QSet<QString> AdbUtils::getListApps(const QString &deviceId) {
|
||||
}
|
||||
return apps;
|
||||
}
|
||||
|
||||
void AdbUtils::unlockScreen(const QString &deviceId, const int screenWidth, const int screenHeight) {
|
||||
const QString adb = adbPath();
|
||||
|
||||
// Проверяем, включён ли экран (mWakefulness=Awake)
|
||||
QString powerState;
|
||||
startProcess(adb, {"-s", deviceId, "shell", "dumpsys", "power"}, &powerState);
|
||||
const bool isAwake = powerState.contains("mWakefulness=Awake");
|
||||
|
||||
if (!isAwake) {
|
||||
// Включаем экран
|
||||
startProcess(adb, {"-s", deviceId, "shell", "input", "keyevent", "KEYCODE_WAKEUP"});
|
||||
QThread::msleep(500);
|
||||
qDebug() << "[AdbUtils] unlockScreen: woke up device" << deviceId;
|
||||
}
|
||||
|
||||
// Проверяем, залочен ли экран (mDreamingSleepToken / mShowingLockscreen)
|
||||
QString wmState;
|
||||
startProcess(adb, {"-s", deviceId, "shell", "dumpsys", "window"}, &wmState);
|
||||
const bool isLocked = wmState.contains("mDreamingSleepToken") || wmState.contains("mShowingLockscreen=true")
|
||||
|| wmState.contains("StatusBar");
|
||||
|
||||
if (isLocked) {
|
||||
// Свайп вверх для разблокировки
|
||||
const int x = screenWidth / 2;
|
||||
makeSwipe(deviceId, x, screenHeight * 3 / 4, x, screenHeight / 4);
|
||||
QThread::msleep(500);
|
||||
qDebug() << "[AdbUtils] unlockScreen: swiped to unlock" << deviceId;
|
||||
}
|
||||
}
|
||||
|
||||
@ -44,6 +44,8 @@ public:
|
||||
|
||||
static bool goBack(const QString &deviceId);
|
||||
|
||||
static void unlockScreen(const QString &deviceId, int screenWidth, int screenHeight);
|
||||
|
||||
static QSet<QString> getListApps(const QString &deviceId);
|
||||
|
||||
private:
|
||||
|
||||
@ -67,6 +67,9 @@ bool CommonScript::runAppAndGoToHomeScreen(
|
||||
const int width,
|
||||
const int height
|
||||
) {
|
||||
// Разблокируем экран перед запуском
|
||||
AdbUtils::unlockScreen(deviceId, width, height);
|
||||
|
||||
// Убить приложение если запущено
|
||||
if (const QString pid = AdbUtils::tryToGetRunningAppPid(deviceId, packageName); !pid.isEmpty()) {
|
||||
qDebug() << "[Black] Process already running, pid:" << pid;
|
||||
|
||||
@ -59,6 +59,9 @@ bool CommonScript::runAppAndGoToHomeScreen(
|
||||
const int width,
|
||||
const int height
|
||||
) {
|
||||
// Разблокируем экран перед запуском
|
||||
AdbUtils::unlockScreen(deviceId, width, height);
|
||||
|
||||
// Запустить устройство
|
||||
if (const QString pid = AdbUtils::tryToGetRunningAppPid(deviceId, packageName); !pid.isEmpty()) {
|
||||
qDebug() << "Process already running, pid: " << pid;
|
||||
|
||||
@ -212,6 +212,10 @@ void DatabaseManager::initSchema() {
|
||||
|
||||
q.exec("CREATE INDEX IF NOT EXISTS idx_general_logs_id ON general_logs (id DESC)");
|
||||
|
||||
// Миграция: добавляем колонку screenshot если её нет
|
||||
q.exec("ALTER TABLE general_logs ADD COLUMN screenshot BLOB");
|
||||
// Ошибка = колонка уже существует — это нормально
|
||||
|
||||
q.exec(R"(CREATE TABLE IF NOT EXISTS settings (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT
|
||||
@ -220,6 +224,49 @@ void DatabaseManager::initSchema() {
|
||||
db.commit();
|
||||
}
|
||||
|
||||
void DatabaseManager::cleanupOldData(int days) {
|
||||
QSqlDatabase db = database();
|
||||
QSqlQuery q(db);
|
||||
|
||||
const QString cutoff = QDateTime::currentDateTimeUtc().addDays(-days).toString(Qt::ISODate);
|
||||
|
||||
db.transaction();
|
||||
|
||||
// general_logs
|
||||
q.prepare("DELETE FROM general_logs WHERE timestamp < :cutoff");
|
||||
q.bindValue(":cutoff", cutoff);
|
||||
if (q.exec()) {
|
||||
qDebug() << "[cleanup] general_logs: deleted" << q.numRowsAffected() << "rows older than" << days << "days";
|
||||
}
|
||||
|
||||
// app_logs
|
||||
q.prepare("DELETE FROM app_logs WHERE timestamp < :cutoff");
|
||||
q.bindValue(":cutoff", cutoff);
|
||||
if (q.exec()) {
|
||||
qDebug() << "[cleanup] app_logs: deleted" << q.numRowsAffected() << "rows older than" << days << "days";
|
||||
}
|
||||
|
||||
// events (завершённые/ошибочные)
|
||||
q.prepare("DELETE FROM events WHERE timestamp < :cutoff AND status IN ('complete', 'error')");
|
||||
q.bindValue(":cutoff", cutoff);
|
||||
if (q.exec()) {
|
||||
qDebug() << "[cleanup] events: deleted" << q.numRowsAffected() << "rows older than" << days << "days";
|
||||
}
|
||||
|
||||
// transactions
|
||||
q.prepare("DELETE FROM transactions WHERE timestamp < :cutoff");
|
||||
q.bindValue(":cutoff", cutoff);
|
||||
if (q.exec()) {
|
||||
qDebug() << "[cleanup] transactions: deleted" << q.numRowsAffected() << "rows older than" << days << "days";
|
||||
}
|
||||
|
||||
db.commit();
|
||||
|
||||
// Освобождаем место
|
||||
q.exec("VACUUM");
|
||||
qDebug() << "[cleanup] VACUUM done";
|
||||
}
|
||||
|
||||
void DatabaseManager::closeConnection() {
|
||||
QMutexLocker<QRecursiveMutex> locker(&mutex);
|
||||
if (const QString name = connectionName(); connectionPool.contains(name)) {
|
||||
|
||||
@ -12,6 +12,7 @@ public:
|
||||
|
||||
QSqlDatabase database();
|
||||
void initSchema();
|
||||
void cleanupOldData(int days = 5);
|
||||
void closeConnection();
|
||||
|
||||
private:
|
||||
|
||||
@ -10,22 +10,24 @@ bool BankProfileDAO::upsertApplication(const BankProfileInfo &info) {
|
||||
QSqlQuery query(DatabaseManager::instance().database());
|
||||
|
||||
query.prepare(R"(
|
||||
INSERT INTO bank_profiles (id, device_id, pin_code, name, code, package, status, comment, install, pin_code_checked)
|
||||
VALUES (:id, :device_id, :pin_code, :name, :code, :package, :status, :comment, :install, :pin_code_checked)
|
||||
INSERT INTO bank_profiles (device_id, pin_code, name, code, package, status, comment, install, pin_code_checked,
|
||||
bank_profile_id, full_name, phone)
|
||||
VALUES (:device_id, :pin_code, :name, :code, :package, :status, :comment, :install, :pin_code_checked,
|
||||
:bank_profile_id, :full_name, :phone)
|
||||
ON CONFLICT(device_id, code)
|
||||
DO UPDATE SET
|
||||
device_id = excluded.device_id,
|
||||
pin_code = excluded.pin_code,
|
||||
pin_code = CASE WHEN excluded.pin_code != '' THEN excluded.pin_code ELSE bank_profiles.pin_code END,
|
||||
name = excluded.name,
|
||||
code = excluded.code,
|
||||
package = excluded.package,
|
||||
package = CASE WHEN excluded.package != '' THEN excluded.package ELSE bank_profiles.package END,
|
||||
status = excluded.status,
|
||||
comment = excluded.comment,
|
||||
install = excluded.install,
|
||||
pin_code_checked = excluded.pin_code_checked
|
||||
pin_code_checked = excluded.pin_code_checked,
|
||||
bank_profile_id = CASE WHEN excluded.bank_profile_id != '' THEN excluded.bank_profile_id ELSE bank_profiles.bank_profile_id END,
|
||||
full_name = CASE WHEN excluded.full_name != '' THEN excluded.full_name ELSE bank_profiles.full_name END,
|
||||
phone = CASE WHEN excluded.phone != '' THEN excluded.phone ELSE bank_profiles.phone END
|
||||
)");
|
||||
|
||||
query.bindValue(":id", info.id);
|
||||
query.bindValue(":device_id", info.deviceId);
|
||||
query.bindValue(":pin_code", info.pinCode);
|
||||
query.bindValue(":name", info.name);
|
||||
@ -35,6 +37,9 @@ bool BankProfileDAO::upsertApplication(const BankProfileInfo &info) {
|
||||
query.bindValue(":comment", info.comment);
|
||||
query.bindValue(":install", info.install ? "yes" : "no");
|
||||
query.bindValue(":pin_code_checked", info.pinCodeChecked ? "yes" : "no");
|
||||
query.bindValue(":bank_profile_id", info.bankProfileId);
|
||||
query.bindValue(":full_name", info.fullName);
|
||||
query.bindValue(":phone", info.phone);
|
||||
|
||||
if (!query.exec()) {
|
||||
qWarning() << "Ошибка при вставке/обновлении application:" << query.lastError().text();
|
||||
@ -367,3 +372,16 @@ bool BankProfileDAO::checkInstalledApps(const QString &deviceId, const QSet<QStr
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool BankProfileDAO::migrateDeviceId(const QString &oldDeviceId, const QString &newDeviceId) {
|
||||
QSqlQuery query(DatabaseManager::instance().database());
|
||||
query.prepare("UPDATE bank_profiles SET device_id = :new_id WHERE device_id = :old_id");
|
||||
query.bindValue(":new_id", newDeviceId);
|
||||
query.bindValue(":old_id", oldDeviceId);
|
||||
if (!query.exec()) {
|
||||
qWarning() << "[BankProfileDAO] migrateDeviceId failed:" << query.lastError().text();
|
||||
return false;
|
||||
}
|
||||
qDebug() << "[BankProfileDAO] migrated" << query.numRowsAffected() << "profiles from" << oldDeviceId << "to" << newDeviceId;
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -47,4 +47,6 @@ public:
|
||||
[[nodiscard]] static bool activateAppsForDevice(const QString &deviceId);
|
||||
|
||||
[[nodiscard]] static bool deactivateAppsForDevice(const QString &deviceId);
|
||||
|
||||
[[nodiscard]] static bool migrateDeviceId(const QString &oldDeviceId, const QString &newDeviceId);
|
||||
};
|
||||
|
||||
@ -7,15 +7,21 @@
|
||||
#include "time/DateUtils.h"
|
||||
|
||||
bool GeneralLogDAO::insert(const QString &level, const QString &source, const QString &message) {
|
||||
return insert(level, source, message, {});
|
||||
}
|
||||
|
||||
bool GeneralLogDAO::insert(const QString &level, const QString &source, const QString &message,
|
||||
const QByteArray &screenshot) {
|
||||
QSqlQuery q(DatabaseManager::instance().database());
|
||||
q.prepare(R"(
|
||||
INSERT INTO general_logs (level, source, message, timestamp)
|
||||
VALUES (:level, :source, :message, :timestamp)
|
||||
INSERT INTO general_logs (level, source, message, timestamp, screenshot)
|
||||
VALUES (:level, :source, :message, :timestamp, :screenshot)
|
||||
)");
|
||||
q.bindValue(":level", level);
|
||||
q.bindValue(":source", source);
|
||||
q.bindValue(":message", message);
|
||||
q.bindValue(":timestamp", DateUtils::toUtcString(QDateTime::currentDateTimeUtc()));
|
||||
q.bindValue(":level", level);
|
||||
q.bindValue(":source", source);
|
||||
q.bindValue(":message", message);
|
||||
q.bindValue(":timestamp", DateUtils::toUtcString(QDateTime::currentDateTimeUtc()));
|
||||
q.bindValue(":screenshot", screenshot.isEmpty() ? QVariant(QMetaType(QMetaType::QByteArray)) : screenshot);
|
||||
return q.exec();
|
||||
}
|
||||
|
||||
@ -36,7 +42,7 @@ QList<GeneralLogEntry> GeneralLogDAO::getLogs(int page, int pageSize, const QStr
|
||||
QList<GeneralLogEntry> result;
|
||||
QSqlQuery q(DatabaseManager::instance().database());
|
||||
q.prepare(
|
||||
"SELECT id, level, source, message, timestamp FROM general_logs"
|
||||
"SELECT id, level, source, message, timestamp, screenshot FROM general_logs"
|
||||
+ buildWhereClause(levels)
|
||||
+ " ORDER BY id DESC LIMIT :limit OFFSET :offset"
|
||||
);
|
||||
@ -50,7 +56,8 @@ QList<GeneralLogEntry> GeneralLogDAO::getLogs(int page, int pageSize, const QStr
|
||||
e.level = q.value("level").toString();
|
||||
e.source = q.value("source").toString();
|
||||
e.message = q.value("message").toString();
|
||||
e.timestamp = DateUtils::fromUtcString(q.value("timestamp").toString());
|
||||
e.timestamp = DateUtils::fromUtcString(q.value("timestamp").toString());
|
||||
e.screenshot = q.value("screenshot").toByteArray();
|
||||
result.append(e);
|
||||
}
|
||||
return result;
|
||||
|
||||
@ -5,16 +5,19 @@
|
||||
#include <QDateTime>
|
||||
|
||||
struct GeneralLogEntry {
|
||||
int id = -1;
|
||||
QString level;
|
||||
QString source;
|
||||
QString message;
|
||||
QDateTime timestamp;
|
||||
int id = -1;
|
||||
QString level;
|
||||
QString source;
|
||||
QString message;
|
||||
QDateTime timestamp;
|
||||
QByteArray screenshot;
|
||||
};
|
||||
|
||||
class GeneralLogDAO {
|
||||
public:
|
||||
static bool insert(const QString &level, const QString &source, const QString &message);
|
||||
static bool insert(const QString &level, const QString &source, const QString &message,
|
||||
const QByteArray &screenshot);
|
||||
|
||||
[[nodiscard]] static QList<GeneralLogEntry> getLogs(int page, int pageSize,
|
||||
const QStringList &levels = {});
|
||||
|
||||
@ -19,17 +19,18 @@ bool MaterialDAO::upsertAccount(const MaterialInfo &info) {
|
||||
QSqlQuery query(DatabaseManager::instance().database());
|
||||
|
||||
query.prepare(R"(
|
||||
INSERT INTO materials (device_id, app_code, last_numbers, card_number, amount, update_time, status, description, currency)
|
||||
VALUES (:device_id, :app_code, :last_numbers, :card_number, :amount, :update_time, :status, :description, :currency)
|
||||
INSERT INTO materials (device_id, app_code, last_numbers, card_number, amount, update_time, status, description, currency, material_id)
|
||||
VALUES (:device_id, :app_code, :last_numbers, :card_number, :amount, :update_time, :status, :description, :currency, :material_id)
|
||||
ON CONFLICT(device_id, app_code, last_numbers)
|
||||
DO UPDATE SET
|
||||
amount = excluded.amount,
|
||||
update_time = excluded.update_time,
|
||||
last_numbers = excluded.last_numbers,
|
||||
card_number = excluded.card_number,
|
||||
card_number = CASE WHEN excluded.card_number != '' THEN excluded.card_number ELSE materials.card_number END,
|
||||
status = excluded.status,
|
||||
description = excluded.description,
|
||||
currency = excluded.currency
|
||||
currency = excluded.currency,
|
||||
material_id = CASE WHEN excluded.material_id != '' THEN excluded.material_id ELSE materials.material_id END
|
||||
)");
|
||||
|
||||
query.bindValue(":device_id", info.deviceId);
|
||||
@ -41,6 +42,7 @@ bool MaterialDAO::upsertAccount(const MaterialInfo &info) {
|
||||
query.bindValue(":amount", info.amount);
|
||||
query.bindValue(":update_time", DateUtils::toUtcString(info.updateTime));
|
||||
query.bindValue(":status", materialStatusToString(info.status));
|
||||
query.bindValue(":material_id", info.materialId);
|
||||
|
||||
if (!query.exec()) {
|
||||
qWarning() << "Ошибка при вставке/обновлении account:" << query.lastError().text();
|
||||
@ -289,3 +291,16 @@ MaterialInfo MaterialDAO::findByMaterialId(const QString &materialId) {
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
bool MaterialDAO::migrateDeviceId(const QString &oldDeviceId, const QString &newDeviceId) {
|
||||
QSqlQuery query(DatabaseManager::instance().database());
|
||||
query.prepare("UPDATE materials SET device_id = :new_id WHERE device_id = :old_id");
|
||||
query.bindValue(":new_id", newDeviceId);
|
||||
query.bindValue(":old_id", oldDeviceId);
|
||||
if (!query.exec()) {
|
||||
qWarning() << "[MaterialDAO] migrateDeviceId failed:" << query.lastError().text();
|
||||
return false;
|
||||
}
|
||||
qDebug() << "[MaterialDAO] migrated" << query.numRowsAffected() << "materials from" << oldDeviceId << "to" << newDeviceId;
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -36,4 +36,6 @@ public:
|
||||
[[nodiscard]] static QStringList getActiveMaterialIds();
|
||||
|
||||
[[nodiscard]] static QStringList getActiveMaterialIdsExcludingDevices(const QSet<QString> &excludeDevices);
|
||||
|
||||
[[nodiscard]] static bool migrateDeviceId(const QString &oldDeviceId, const QString &newDeviceId);
|
||||
};
|
||||
|
||||
32
main.cpp
32
main.cpp
@ -268,44 +268,18 @@ int main(int argc, char *argv[]) {
|
||||
qInstallMessageHandler(messageHandler);
|
||||
|
||||
|
||||
// Проверяем наличие файла БД, если нет — предлагаем загрузить
|
||||
// Создаём директорию для БД, если её нет (initSchema создаст файл автоматически)
|
||||
{
|
||||
QSettings settings("config.ini", QSettings::IniFormat);
|
||||
QString dbPath = settings.value("db/dbPath").toString();
|
||||
if (dbPath.startsWith("~"))
|
||||
dbPath.replace(0, 1, QDir::homePath());
|
||||
|
||||
if (!QFile::exists(dbPath)) {
|
||||
QMessageBox msgBox;
|
||||
msgBox.setWindowTitle("База данных не найдена");
|
||||
msgBox.setText("Файл базы данных не найден:\n" + dbPath);
|
||||
QPushButton *selectBtn = msgBox.addButton("Выбрать файл", QMessageBox::AcceptRole);
|
||||
QPushButton *createBtn = msgBox.addButton("Создать новую", QMessageBox::ActionRole);
|
||||
QPushButton *exitBtn = msgBox.addButton("Выход", QMessageBox::RejectRole);
|
||||
msgBox.setDefaultButton(selectBtn);
|
||||
msgBox.exec();
|
||||
|
||||
if (msgBox.clickedButton() == selectBtn) {
|
||||
QString selected = QFileDialog::getOpenFileName(
|
||||
nullptr, "Выберите файл базы данных", QDir::homePath(), "SQLite (*.db *.sqlite);;Все файлы (*)");
|
||||
if (selected.isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
QDir().mkpath(QFileInfo(dbPath).absolutePath());
|
||||
if (!QFile::copy(selected, dbPath)) {
|
||||
QMessageBox::critical(nullptr, "Ошибка", "Не удалось скопировать файл базы данных.");
|
||||
return 1;
|
||||
}
|
||||
} else if (msgBox.clickedButton() == createBtn) {
|
||||
// Просто продолжаем — initSchema создаст новую БД
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
QDir().mkpath(QFileInfo(dbPath).absolutePath());
|
||||
}
|
||||
|
||||
// Миграции БД
|
||||
DatabaseManager::instance().initSchema();
|
||||
DatabaseManager::instance().cleanupOldData(5);
|
||||
dbReady.store(true);
|
||||
|
||||
// 1. Если есть api_key — логинимся и проверяем/создаём desktop
|
||||
|
||||
@ -337,8 +337,16 @@ void DeviceScreener::start() {
|
||||
} else {
|
||||
// Маппинг с восстановленным устройством — только по name (точное совпадение)
|
||||
const DeviceInfo restored = DeviceDAO::findByName(device.name);
|
||||
qDebug() << "[DeviceScreener] findByName(" << device.name << ") →"
|
||||
<< "id:" << restored.id << "apiId:" << restored.apiId
|
||||
<< "match:" << (!restored.id.isEmpty() && restored.id != device.id && !restored.apiId.isEmpty());
|
||||
if (!restored.id.isEmpty() && restored.id != device.id && !restored.apiId.isEmpty()) {
|
||||
device.apiId = restored.apiId;
|
||||
// Сначала создаём новое устройство (FK target), затем мигрируем, затем удаляем старое
|
||||
DeviceDAO::upsertDevice(device);
|
||||
const bool bpOk = BankProfileDAO::migrateDeviceId(restored.id, device.id);
|
||||
const bool matOk = MaterialDAO::migrateDeviceId(restored.id, device.id);
|
||||
qDebug() << "[DeviceScreener] migration result: bp=" << bpOk << "mat=" << matOk;
|
||||
DeviceDAO::deleteDevice(restored.id);
|
||||
qDebug() << "[DeviceScreener] matched restored device:" << restored.id << "->" << device.id
|
||||
<< "apiId:" << device.apiId;
|
||||
|
||||
@ -18,6 +18,7 @@
|
||||
#include "dao/TransactionDAO.h"
|
||||
#include "db/BankProfileInfo.h"
|
||||
#include "adb/AdbUtils.h"
|
||||
#include "dao/GeneralLogDAO.h"
|
||||
#include "net/NetworkService.h"
|
||||
#include "black/GetLastTransactionsScript.h"
|
||||
#include "black/LoginAndCheckAccountsScript.h"
|
||||
@ -256,14 +257,20 @@ void EventHandler::updateEventThread(const QString &key, const EventInfo &event,
|
||||
} else {
|
||||
if (newStatus == EventStatus::Error) {
|
||||
// Сохраняем скриншот устройства при ошибке
|
||||
QByteArray screenshot;
|
||||
if (!event.deviceId.isEmpty()) {
|
||||
const DeviceInfo device = DeviceDAO::getDeviceById(event.deviceId);
|
||||
const QString adbSerial = device.adbSerial.isEmpty() ? event.deviceId : device.adbSerial;
|
||||
const QByteArray screenshot = AdbUtils::captureScreenshotBytes(adbSerial);
|
||||
screenshot = AdbUtils::captureScreenshotBytes(adbSerial);
|
||||
if (!screenshot.isEmpty()) {
|
||||
EventDAO::updateScreenshot(event.id, screenshot);
|
||||
}
|
||||
}
|
||||
// Дублируем ошибку в general_logs со скриншотом
|
||||
const QString logMsg = QString("[%1] %2 bank=%3: %4")
|
||||
.arg(eventTypeToString(event.type), event.externalId, event.bankName, result);
|
||||
GeneralLogDAO::insert("CRITICAL", "EventHandler", logMsg, screenshot);
|
||||
|
||||
sendTaskError(eventTypeToString(event.type), event.externalId, event.bankName, result);
|
||||
EventDAO::markSentToServer(event.id);
|
||||
}
|
||||
|
||||
@ -11,6 +11,7 @@
|
||||
#include <QNetworkReply>
|
||||
#include <QProcess>
|
||||
#include <QTemporaryFile>
|
||||
#include <QPixmap>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#include "dao/EventDAO.h"
|
||||
@ -25,7 +26,7 @@ static constexpr int COL_MESSAGE = 4;
|
||||
|
||||
FileLogWindow::FileLogWindow(QWidget *parent) : QDialog(parent) {
|
||||
setWindowTitle("Логи");
|
||||
resize(1100, 600);
|
||||
resize(1200, 650);
|
||||
|
||||
m_table = new QTableWidget(this);
|
||||
m_table->setColumnCount(5);
|
||||
@ -40,6 +41,19 @@ FileLogWindow::FileLogWindow(QWidget *parent) : QDialog(parent) {
|
||||
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);
|
||||
@ -62,7 +76,7 @@ FileLogWindow::FileLogWindow(QWidget *parent) : QDialog(parent) {
|
||||
paginationLayout->addWidget(m_sendBtn);
|
||||
|
||||
auto *mainLayout = new QVBoxLayout(this);
|
||||
mainLayout->addWidget(m_table, 1);
|
||||
mainLayout->addWidget(splitter, 1);
|
||||
mainLayout->addLayout(paginationLayout);
|
||||
|
||||
connect(m_prevBtn, &QPushButton::clicked, this, [this]() {
|
||||
@ -79,6 +93,9 @@ FileLogWindow::FileLogWindow(QWidget *parent) : QDialog(parent) {
|
||||
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();
|
||||
@ -96,11 +113,11 @@ void FileLogWindow::loadPage() {
|
||||
m_totalPages = qMax(1, (total + m_pageSize - 1) / m_pageSize);
|
||||
m_page = qBound(0, m_page, m_totalPages - 1);
|
||||
|
||||
const QList<GeneralLogEntry> entries = GeneralLogDAO::getLogs(m_page, m_pageSize, levels);
|
||||
m_entries = GeneralLogDAO::getLogs(m_page, m_pageSize, levels);
|
||||
|
||||
m_table->setRowCount(static_cast<int>(entries.size()));
|
||||
for (int i = 0; i < entries.size(); ++i) {
|
||||
const auto &e = entries[i];
|
||||
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")));
|
||||
@ -112,6 +129,25 @@ void FileLogWindow::loadPage() {
|
||||
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);
|
||||
@ -175,6 +211,21 @@ void FileLogWindow::sendLogToTelegram() {
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
|
||||
@ -3,8 +3,11 @@
|
||||
#include <QDialog>
|
||||
#include <QLabel>
|
||||
#include <QPushButton>
|
||||
#include <QSplitter>
|
||||
#include <QTableWidget>
|
||||
|
||||
#include "dao/GeneralLogDAO.h"
|
||||
|
||||
class FileLogWindow final : public QDialog {
|
||||
Q_OBJECT
|
||||
|
||||
@ -13,6 +16,7 @@ public:
|
||||
|
||||
private:
|
||||
QTableWidget *m_table;
|
||||
QLabel *m_screenshotLabel;
|
||||
QLabel *m_pageLabel;
|
||||
QPushButton *m_prevBtn;
|
||||
QPushButton *m_nextBtn;
|
||||
@ -23,8 +27,11 @@ private:
|
||||
int m_pageSize = 20;
|
||||
int m_totalPages = 1;
|
||||
|
||||
QList<GeneralLogEntry> m_entries;
|
||||
|
||||
QStringList currentLevelFilter() const;
|
||||
void loadPage();
|
||||
void updatePagination();
|
||||
void showScreenshot(int row);
|
||||
void sendLogToTelegram();
|
||||
};
|
||||
|
||||
@ -19,10 +19,10 @@ RegistrationWindow::RegistrationWindow(QWidget *parent): QWidget(parent) {
|
||||
|
||||
auto *layout = new QVBoxLayout(this);
|
||||
|
||||
layout->addWidget(new QLabel("Введите API ключ:"));
|
||||
layout->addWidget(new QLabel("Введите ваш идентификатор:"));
|
||||
|
||||
QLineEdit *apiKeyEdit = new QLineEdit();
|
||||
apiKeyEdit->setPlaceholderText("API ключ");
|
||||
apiKeyEdit->setPlaceholderText("Идентификатор");
|
||||
layout->addWidget(apiKeyEdit);
|
||||
|
||||
QHBoxLayout *buttonLayout = new QHBoxLayout();
|
||||
@ -39,7 +39,7 @@ RegistrationWindow::RegistrationWindow(QWidget *parent): QWidget(parent) {
|
||||
connect(loginButton, &QPushButton::clicked, this, [=]() {
|
||||
const QString apiKey = apiKeyEdit->text().trimmed();
|
||||
if (apiKey.isEmpty()) {
|
||||
QMessageBox::warning(this, "Ошибка", "Введите API ключ");
|
||||
QMessageBox::warning(this, "Ошибка", "Введите ваш идентификатор");
|
||||
return;
|
||||
}
|
||||
|
||||
@ -66,7 +66,7 @@ RegistrationWindow::RegistrationWindow(QWidget *parent): QWidget(parent) {
|
||||
emit onRegisteredSuccess();
|
||||
} else if (result == 0) {
|
||||
SettingsDAO::remove("api_key");
|
||||
QMessageBox::critical(this, "Ошибка входа", "Неверный API ключ. Проверьте и попробуйте снова.");
|
||||
QMessageBox::critical(this, "Ошибка входа", "Неверный идентификатор. Проверьте и попробуйте снова.");
|
||||
} else {
|
||||
QMessageBox::critical(this, "Ошибка подключения",
|
||||
"Нет подключения к интернету или сервер не отвечает.\n\nПожалуйста, попробуйте позже.");
|
||||
|
||||
Loading…
Reference in New Issue
Block a user