From c4d1f984bdf8c2359e351e820410d2a5110d1525 Mon Sep 17 00:00:00 2001 From: Evgeny Date: Thu, 16 Jul 2026 12:46:40 +0300 Subject: [PATCH] chatgui settings: add Database tab with integrity check (quick/full) and optimize --- tools/chatgui/CMakeLists.txt | 1 + tools/chatgui/db/db_manager.cpp | 50 ++++- tools/chatgui/db/db_manager.h | 11 ++ tools/chatgui/src/databasesettingspage.cpp | 204 +++++++++++++++++++++ tools/chatgui/src/databasesettingspage.h | 34 ++++ tools/chatgui/src/mainwindow.cpp | 2 +- tools/chatgui/src/settingsdialog.cpp | 11 +- tools/chatgui/src/settingsdialog.h | 19 +- 8 files changed, 320 insertions(+), 12 deletions(-) create mode 100644 tools/chatgui/src/databasesettingspage.cpp create mode 100644 tools/chatgui/src/databasesettingspage.h diff --git a/tools/chatgui/CMakeLists.txt b/tools/chatgui/CMakeLists.txt index cc3168b7..37bd6546 100644 --- a/tools/chatgui/CMakeLists.txt +++ b/tools/chatgui/CMakeLists.txt @@ -68,6 +68,7 @@ add_executable(chatgui src/joindialog.cpp src/settingsdialog.cpp src/networksettingspage.cpp + src/databasesettingspage.cpp transport/utun_node.cpp transport/node_config.cpp transport/config_updater.cpp diff --git a/tools/chatgui/db/db_manager.cpp b/tools/chatgui/db/db_manager.cpp index 4b2520e9..b341df7a 100644 --- a/tools/chatgui/db/db_manager.cpp +++ b/tools/chatgui/db/db_manager.cpp @@ -4,6 +4,7 @@ #include #include #include +#include /* ── helpers ── */ @@ -443,4 +444,51 @@ QByteArray DbManager::syncListPeers(const QString& chId) const { return out; } -/* ── syncListPeers ── */ +/* ── integrity checks ── */ + +static int integrityCallback(void* data, int nCols, char** values, char** /*names*/) { + auto* result = (IntegrityResult*)data; + for (int i = 0; i < nCols; i++) { + if (values[i]) { + QString line = QString::fromUtf8(values[i]); + if (result->message.isEmpty()) result->message = line; + else if (line != "ok") result->errors.append(line); + } + } + return 0; +} + +static IntegrityResult runPragma(DbManager* self, const char* pragma) { + IntegrityResult result; + if (!self->m_db) { + result.ok = false; + result.message = "Database not open"; + return result; + } + QElapsedTimer timer; + timer.start(); + char* errMsg = nullptr; + int rc = sqlite3_exec(self->m_db, pragma, integrityCallback, &result, &errMsg); + result.elapsedMs = (int)timer.elapsed(); + if (rc != SQLITE_OK) { + result.ok = false; + result.message = QString::fromUtf8(errMsg ? errMsg : "unknown error"); + sqlite3_free(errMsg); + return result; + } + if (errMsg) sqlite3_free(errMsg); + result.ok = (result.message == "ok" && result.errors.isEmpty()); + return result; +} + +IntegrityResult DbManager::integrityCheckQuick() const { + return runPragma(const_cast(this), "PRAGMA quick_check"); +} + +IntegrityResult DbManager::integrityCheckFull() const { + return runPragma(const_cast(this), "PRAGMA integrity_check"); +} + +IntegrityResult DbManager::integrityOptimize() const { + return runPragma(const_cast(this), "PRAGMA optimize"); +} diff --git a/tools/chatgui/db/db_manager.h b/tools/chatgui/db/db_manager.h index adf64169..8fced4ef 100644 --- a/tools/chatgui/db/db_manager.h +++ b/tools/chatgui/db/db_manager.h @@ -62,6 +62,13 @@ struct NodeAddr { bool isNat = false; }; +struct IntegrityResult { + bool ok = false; + QString message; + QStringList errors; + int elapsedMs = 0; +}; + class DbManager : public QObject { Q_OBJECT public: @@ -119,6 +126,10 @@ public: qint64 timestamp, uint64_t datahash); QByteArray getLastChainHash(const QString& chId) const; + IntegrityResult integrityCheckQuick() const; + IntegrityResult integrityCheckFull() const; + IntegrityResult integrityOptimize() const; + sqlite3* m_db = nullptr; quint64 m_myNodeId = 0; diff --git a/tools/chatgui/src/databasesettingspage.cpp b/tools/chatgui/src/databasesettingspage.cpp new file mode 100644 index 00000000..79f6fe9c --- /dev/null +++ b/tools/chatgui/src/databasesettingspage.cpp @@ -0,0 +1,204 @@ +// databasesettingspage.cpp — Database maintenance page in settings +#include "databasesettingspage.h" +#include "../db/db_manager.h" +#include +#include +#include +#include +#include +#include +#include + +DatabaseSettingsPage::DatabaseSettingsPage(DbManager* db, QWidget* parent) + : QWidget(parent) + , m_db(db) +{ + auto* layout = new QVBoxLayout(this); + layout->setSpacing(10); + layout->setContentsMargins(0, 0, 0, 0); + + auto* title = new QLabel("Database Maintenance", this); + title->setStyleSheet("font-weight: bold; font-size: 13px;"); + layout->addWidget(title); + + // --- DB info --- + auto* infoLayout = new QFormLayout(); + infoLayout->setSpacing(4); + + QString dbPath = "—"; + QString dbSize = "—"; + QString walSize = "—"; + if (m_db && m_db->m_db) { + const char* fname = sqlite3_db_filename(m_db->m_db, "main"); + if (fname && fname[0]) { + dbPath = QString::fromUtf8(fname); + QFileInfo fi(dbPath); + if (fi.exists()) { + qint64 sz = fi.size(); + if (sz < 1024) dbSize = QString("%1 B").arg(sz); + else if (sz < 1024 * 1024) dbSize = QString("%1 KB").arg(sz / 1024); + else dbSize = QString("%1 MB").arg(sz / (1024.0 * 1024.0), 0, 'f', 2); + } + QFileInfo walFi(dbPath + "-wal"); + if (walFi.exists()) { + qint64 wsz = walFi.size(); + if (wsz < 1024) walSize = QString("%1 B").arg(wsz); + else if (wsz < 1024 * 1024) walSize = QString("%1 KB").arg(wsz / 1024); + else walSize = QString("%1 MB").arg(wsz / (1024.0 * 1024.0), 0, 'f', 2); + } else { + walSize = "n/a"; + } + } + } + m_filePathLabel = new QLabel(dbPath, this); + m_filePathLabel->setStyleSheet("font-size: 11px; color: gray;"); + m_filePathLabel->setTextInteractionFlags(Qt::TextSelectableByMouse); + infoLayout->addRow("File:", m_filePathLabel); + + m_fileSizeLabel = new QLabel(dbSize, this); + m_fileSizeLabel->setStyleSheet("font-size: 11px;"); + infoLayout->addRow("Size:", m_fileSizeLabel); + + m_walSizeLabel = new QLabel(walSize, this); + m_walSizeLabel->setStyleSheet("font-size: 11px;"); + infoLayout->addRow("WAL:", m_walSizeLabel); + + layout->addLayout(infoLayout); + + // --- Separator --- + auto* sep = new QFrame(this); + sep->setFrameShape(QFrame::HLine); + sep->setFrameShadow(QFrame::Sunken); + layout->addWidget(sep); + + // --- Buttons --- + auto* btnLayout = new QVBoxLayout(); + btnLayout->setSpacing(6); + + auto* quickRow = new QHBoxLayout(); + m_quickBtn = new QPushButton("Quick Check", this); + m_quickBtn->setStyleSheet( + "QPushButton { border: 1px solid palette(mid); border-radius: 4px;" + " padding: 5px 14px; font-size: 12px; background: palette(button); }" + "QPushButton:hover { background: palette(light); }"); + connect(m_quickBtn, &QPushButton::clicked, this, &DatabaseSettingsPage::onQuickCheck); + quickRow->addWidget(m_quickBtn); + auto* quickDesc = new QLabel("Scans structure without verifying indexes (fast)", this); + quickDesc->setStyleSheet("color: gray; font-size: 11px;"); + quickRow->addWidget(quickDesc, 1); + btnLayout->addLayout(quickRow); + + auto* fullRow = new QHBoxLayout(); + m_fullBtn = new QPushButton("Full Check", this); + m_fullBtn->setStyleSheet(m_quickBtn->styleSheet()); + connect(m_fullBtn, &QPushButton::clicked, this, &DatabaseSettingsPage::onFullCheck); + fullRow->addWidget(m_fullBtn); + auto* fullDesc = new QLabel("Verifies all data structures and indexes (thorough)", this); + fullDesc->setStyleSheet("color: gray; font-size: 11px;"); + fullRow->addWidget(fullDesc, 1); + btnLayout->addLayout(fullRow); + + auto* optRow = new QHBoxLayout(); + m_optimizeBtn = new QPushButton("Optimize", this); + m_optimizeBtn->setStyleSheet(m_quickBtn->styleSheet()); + connect(m_optimizeBtn, &QPushButton::clicked, this, &DatabaseSettingsPage::onOptimize); + optRow->addWidget(m_optimizeBtn); + auto* optDesc = new QLabel("Update query planner statistics for better performance", this); + optDesc->setStyleSheet("color: gray; font-size: 11px;"); + optRow->addWidget(optDesc, 1); + btnLayout->addLayout(optRow); + + layout->addLayout(btnLayout); + + // --- Status --- + m_statusLabel = new QLabel("", this); + m_statusLabel->setStyleSheet("font-size: 12px; margin-top: 4px;"); + layout->addWidget(m_statusLabel); + + // --- Results --- + m_resultText = new QPlainTextEdit(this); + m_resultText->setReadOnly(true); + m_resultText->setMaximumHeight(120); + m_resultText->setStyleSheet( + "QPlainTextEdit { font-family: monospace; font-size: 11px;" + " background: palette(base); border: 1px solid palette(mid); border-radius: 4px;" + " padding: 4px; }"); + m_resultText->setPlaceholderText("Run a check to see results..."); + layout->addWidget(m_resultText); + + layout->addStretch(); +} + +void DatabaseSettingsPage::setBusy(bool busy) { + m_quickBtn->setEnabled(!busy); + m_fullBtn->setEnabled(!busy); + m_optimizeBtn->setEnabled(!busy); + if (busy) { + m_statusLabel->setText("Checking..."); + m_statusLabel->setStyleSheet("font-size: 12px; margin-top: 4px; color: gray;"); + } +} + +void DatabaseSettingsPage::onQuickCheck() { + if (!m_db || !m_db->m_db) { + m_statusLabel->setText("Database not open"); + m_statusLabel->setStyleSheet("font-size: 12px; color: red;"); + return; + } + setBusy(true); + QTimer::singleShot(50, this, [this]() { + IntegrityResult r = m_db->integrityCheckQuick(); + showResult("Quick Check", r); + }); +} + +void DatabaseSettingsPage::onFullCheck() { + if (!m_db || !m_db->m_db) { + m_statusLabel->setText("Database not open"); + m_statusLabel->setStyleSheet("font-size: 12px; color: red;"); + return; + } + setBusy(true); + QTimer::singleShot(50, this, [this]() { + IntegrityResult r = m_db->integrityCheckFull(); + showResult("Full Check", r); + }); +} + +void DatabaseSettingsPage::onOptimize() { + if (!m_db || !m_db->m_db) { + m_statusLabel->setText("Database not open"); + m_statusLabel->setStyleSheet("font-size: 12px; color: red;"); + return; + } + setBusy(true); + QTimer::singleShot(50, this, [this]() { + IntegrityResult r = m_db->integrityOptimize(); + showResult("Optimize", r); + }); +} + +void DatabaseSettingsPage::showResult(const QString& checkName, const IntegrityResult& result) { + m_quickBtn->setEnabled(true); + m_fullBtn->setEnabled(true); + m_optimizeBtn->setEnabled(true); + + if (result.ok) { + m_statusLabel->setText(QString("%1: OK (%2 ms)") + .arg(checkName).arg(result.elapsedMs)); + m_statusLabel->setStyleSheet("font-size: 12px; font-weight: bold; color: green;"); + } else { + m_statusLabel->setText(QString("%1: FAILED (%2 ms)") + .arg(checkName).arg(result.elapsedMs)); + m_statusLabel->setStyleSheet("font-size: 12px; font-weight: bold; color: red;"); + } + + if (result.errors.isEmpty()) { + m_resultText->setPlainText(result.message.isEmpty() ? "ok" : result.message); + } else { + QStringList lines; + lines.append(result.message); + lines.append(result.errors); + m_resultText->setPlainText(lines.join('\n')); + } +} diff --git a/tools/chatgui/src/databasesettingspage.h b/tools/chatgui/src/databasesettingspage.h new file mode 100644 index 00000000..b0b1f1f9 --- /dev/null +++ b/tools/chatgui/src/databasesettingspage.h @@ -0,0 +1,34 @@ +// databasesettingspage.h — Database maintenance page in settings +#pragma once + +#include +#include +#include +#include + +class DbManager; + +class DatabaseSettingsPage : public QWidget { + Q_OBJECT +public: + explicit DatabaseSettingsPage(DbManager* db, QWidget* parent = nullptr); + +private slots: + void onQuickCheck(); + void onFullCheck(); + void onOptimize(); + +private: + void showResult(const QString& checkName, const struct IntegrityResult& result); + void setBusy(bool busy); + + DbManager* m_db; + QLabel* m_filePathLabel; + QLabel* m_fileSizeLabel; + QLabel* m_walSizeLabel; + QPushButton* m_quickBtn; + QPushButton* m_fullBtn; + QPushButton* m_optimizeBtn; + QLabel* m_statusLabel; + QPlainTextEdit* m_resultText; +}; diff --git a/tools/chatgui/src/mainwindow.cpp b/tools/chatgui/src/mainwindow.cpp index 4f2bab4e..ddbf6cb2 100644 --- a/tools/chatgui/src/mainwindow.cpp +++ b/tools/chatgui/src/mainwindow.cpp @@ -260,7 +260,7 @@ void MainWindow::showJoinDialog() { } void MainWindow::showSettings() { - SettingsDialog dlg(m_cfgPath, this); + SettingsDialog dlg(m_cfgPath, m_db, this); dlg.exec(); } diff --git a/tools/chatgui/src/settingsdialog.cpp b/tools/chatgui/src/settingsdialog.cpp index 2dbc2dc5..fd6dc177 100644 --- a/tools/chatgui/src/settingsdialog.cpp +++ b/tools/chatgui/src/settingsdialog.cpp @@ -1,8 +1,10 @@ // settingsdialog.cpp — Settings dialog with category list #include "settingsdialog.h" #include "networksettingspage.h" +#include "databasesettingspage.h" #include "../transport/gui_bridge.h" #include "../../lib/mem.h" +#include "../db/db_manager.h" extern "C" { #include "../transport/chat_core.h" } @@ -15,12 +17,12 @@ extern "C" { #include #include -SettingsDialog::SettingsDialog(const QString& configPath, QWidget* parent) +SettingsDialog::SettingsDialog(const QString& configPath, DbManager* db, QWidget* parent) : QDialog(parent) , m_configPath(configPath) { setWindowTitle("Settings"); - setMinimumSize(560, 380); + setMinimumSize(560, 460); setModal(true); auto* mainLayout = new QHBoxLayout(this); @@ -37,6 +39,7 @@ SettingsDialog::SettingsDialog(const QString& configPath, QWidget* parent) "QListWidget::item:selected { background: #3390EC; color: white; }"); m_categoryList->addItem("Profile"); m_categoryList->addItem("Network"); + m_categoryList->addItem("Database"); mainLayout->addWidget(m_categoryList); // --- Right panel: stacked pages --- @@ -64,6 +67,10 @@ SettingsDialog::SettingsDialog(const QString& configPath, QWidget* parent) // Network page m_networkPage = new NetworkSettingsPage(m_pages); m_pages->addWidget(m_networkPage); + + // Database page + m_databasePage = new DatabaseSettingsPage(db, m_pages); + m_pages->addWidget(m_databasePage); rightLayout->addWidget(m_pages, 1); // --- Bottom buttons --- diff --git a/tools/chatgui/src/settingsdialog.h b/tools/chatgui/src/settingsdialog.h index f59e3d2c..edde94a9 100644 --- a/tools/chatgui/src/settingsdialog.h +++ b/tools/chatgui/src/settingsdialog.h @@ -9,11 +9,13 @@ #include class NetworkSettingsPage; +class DatabaseSettingsPage; +class DbManager; class SettingsDialog : public QDialog { Q_OBJECT public: - explicit SettingsDialog(const QString& configPath, QWidget* parent = nullptr); + explicit SettingsDialog(const QString& configPath, DbManager* db, QWidget* parent = nullptr); private slots: void onCategoryChanged(int row); @@ -22,11 +24,12 @@ private slots: private: void loadProfileFromConfig(const QString& path); - QString m_configPath; - QListWidget* m_categoryList; - QStackedWidget* m_pages; - QPushButton* m_saveBtn; - QLineEdit* m_nicknameEdit; - QString m_oldNickname; - NetworkSettingsPage* m_networkPage; + QString m_configPath; + QListWidget* m_categoryList; + QStackedWidget* m_pages; + QPushButton* m_saveBtn; + QLineEdit* m_nicknameEdit; + QString m_oldNickname; + NetworkSettingsPage* m_networkPage; + DatabaseSettingsPage* m_databasePage; };