#include // nodespage.cpp — Nodes table + dump in settings #include "nodespage.h" #include "addressprofiletext.h" #include "../db/db_manager.h" #include #include #include #include #include #include #include #include #include #include "../../../lib/platform_compat.h" #include "../../../lib/debug_config.h" static QString hex16(uint64_t v) { return QString("%1").arg(v, 16, 16, QChar('0')).toUpper(); } QString NodesPage::shortenKey(const QByteArray& key) { if (key.size() < 8) return key.toHex(' ').toUpper(); return key.left(4).toHex(' ').toUpper() + ".." + key.right(4).toHex(' ').toUpper(); } QString NodesPage::addrStr(const void* addr, int family) { if (family == 4) { const uint8_t* a = (const uint8_t*)addr; return QString("%1.%2.%3.%4").arg(a[0]).arg(a[1]).arg(a[2]).arg(a[3]); } if (family == 6) { uint16_t a[8]; memcpy(a, addr, sizeof(a)); return QString("%1:%2:%3:%4:%5:%6:%7:%8") .arg(ntohs(a[0]), 0, 16).arg(ntohs(a[1]), 0, 16) .arg(ntohs(a[2]), 0, 16).arg(ntohs(a[3]), 0, 16) .arg(ntohs(a[4]), 0, 16).arg(ntohs(a[5]), 0, 16) .arg(ntohs(a[6]), 0, 16).arg(ntohs(a[7]), 0, 16); } return "?"; } QString NodesPage::protoStr(int protocol) { if (protocol & 0x04) return (protocol & 0x02) ? "TCP+REALITY" : "REALITY"; switch (protocol) { case 1: return "UDP"; case 2: return "TCP"; case 3: return "UDP+TCP"; default: return QString::number(protocol); } } NodesPage::NodesPage(DbManager* db, QWidget* parent) : QWidget(parent) , m_db(db) { auto* layout = new QVBoxLayout(this); layout->setSpacing(8); layout->setContentsMargins(0, 0, 0, 0); auto* title = new QLabel(QCoreApplication::translate("ChatGui", "Known Nodes"), this); title->setStyleSheet("font-weight: bold; font-size: 13px;"); layout->addWidget(title); m_table = new QTableWidget(0, 5, this); m_table->setHorizontalHeaderLabels({QCoreApplication::translate("ChatGui", "Node ID"), QCoreApplication::translate("ChatGui", "Name"), "x25519", "ed25519", QCoreApplication::translate("ChatGui", "Addrs")}); m_table->horizontalHeader()->setStretchLastSection(true); m_table->setSelectionBehavior(QAbstractItemView::SelectRows); m_table->setSelectionMode(QAbstractItemView::SingleSelection); m_table->setEditTriggers(QAbstractItemView::NoEditTriggers); m_table->verticalHeader()->setVisible(false); m_table->setFont(QFont("monospace", 9)); m_table->setStyleSheet( "QTableWidget { border: 1px solid palette(mid); border-radius: 4px;" " gridline-color: palette(midlight); background: palette(base); }" "QHeaderView::section { background: palette(window); padding: 4px;" " font-weight: bold; font-size: 9px; }"); layout->addWidget(m_table, 2); auto* dumpLabel = new QLabel(QCoreApplication::translate("ChatGui", "Details"), this); dumpLabel->setStyleSheet("font-weight: bold; font-size: 12px; margin-top: 8px;"); layout->addWidget(dumpLabel); m_dumpText = new QPlainTextEdit(this); m_dumpText->setReadOnly(true); m_dumpText->setFont(QFont("monospace", 10)); m_dumpText->setStyleSheet( "QPlainTextEdit { border: 1px solid palette(mid); border-radius: 4px;" " padding: 8px; background: palette(base); }"); m_dumpText->setPlaceholderText(QCoreApplication::translate("ChatGui", "Click Refresh to load nodes")); layout->addWidget(m_dumpText, 3); auto* btnLayout = new QHBoxLayout(); btnLayout->addStretch(); m_refreshBtn = new QPushButton(QCoreApplication::translate("ChatGui", "Refresh"), this); m_refreshBtn->setMinimumWidth(90); m_refreshBtn->setStyleSheet( "QPushButton { border: 1px solid palette(mid); border-radius: 4px;" " padding: 6px 16px; font-size: 13px; background: palette(button); }" "QPushButton:hover { background: palette(light); }"); connect(m_refreshBtn, &QPushButton::clicked, this, &NodesPage::onRefreshClicked); btnLayout->addWidget(m_refreshBtn); layout->addLayout(btnLayout); } void NodesPage::onRefreshClicked() { refreshNodes(); } /* Оба представления строятся из одного запроса; при отказе временные данные не попадают в виджеты. */ void NodesPage::refreshNodes() { if (!m_db || !m_db->m_db) { m_table->setRowCount(0); m_dumpText->setPlainText(QCoreApplication::translate("ChatGui", "Database not connected")); DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "Nodes refresh unavailable: manager=%p SQLite handle missing", static_cast(m_db)); return; } sqlite3* d = m_db->m_db; struct NodeSnapshot { quint64 id = 0; QString name, x25519, ed25519, details; int addresses = 0; }; QVector nodes; quint64 currentNode = 0, addressCount = 0; const char* stage = "prepare"; QString failure; sqlite3_stmt* st = nullptr; DEBUG_DEBUG(DEBUG_CATEGORY_CONFIG, "Nodes refresh begin db=%p", static_cast(d)); int rc = sqlite3_prepare_v2(d, "SELECT n.node_id,n.name,n.x25519_pubkey,n.ed25519_pubkey,n.last_seen_at,n.created_at," " a.node_id,a.family,a.protocol,a.address,a.port,a.rtt,a.nat_mapping,a.nat_inbound,a.nat_source" " FROM nodes n LEFT JOIN node_addresses a ON a.node_id=n.node_id ORDER BY n.node_id,a.family,a.port", -1, &st, nullptr); std::unique_ptr statement(st, sqlite3_finalize); if (rc == SQLITE_OK) { stage = "step"; while ((rc = sqlite3_step(st)) == SQLITE_ROW) { currentNode = quint64(sqlite3_column_int64(st, 0)); if (nodes.isEmpty() || nodes.constLast().id != currentNode) { nodes.append(NodeSnapshot{}); auto& node = nodes.last(); node.id = currentNode; node.name = DbManager::colText(st, 1); const auto x25519 = DbManager::colBlob(st, 2), ed25519 = DbManager::colBlob(st, 3); node.x25519 = x25519.isEmpty() ? "-" : shortenKey(x25519); node.ed25519 = ed25519.isEmpty() ? "-" : shortenKey(ed25519); node.details = QCoreApplication::translate("ChatGui", "Node: %1").arg(hex16(node.id)); if (!node.name.isEmpty()) node.details += QString(" (%1)").arg(node.name); node.details += QString(" id=0x%1\n").arg(node.id, 16, 16, QChar('0')); node.details += QString(" x25519: %1\n ed25519: %2\n").arg(node.x25519, node.ed25519); const qint64 lastSeen = sqlite3_column_int64(st, 4), created = sqlite3_column_int64(st, 5); if (lastSeen > 0) node.details += QCoreApplication::translate("ChatGui", " last seen: %1\n") .arg(QDateTime::fromSecsSinceEpoch(lastSeen).toString("yyyy-MM-dd hh:mm:ss")); if (created > 0) node.details += QCoreApplication::translate("ChatGui", " created: %1\n") .arg(QDateTime::fromSecsSinceEpoch(created).toString("yyyy-MM-dd hh:mm:ss")); } if (sqlite3_column_type(st, 6) == SQLITE_NULL) continue; // LEFT JOIN: узел без адресов. const int family = sqlite3_column_int(st, 7), type = sqlite3_column_type(st, 9); const void* address = sqlite3_column_blob(st, 9); const int bytes = sqlite3_column_bytes(st, 9); if (type != SQLITE_BLOB || !address || (family != 4 && family != 6) || bytes != (family == 4 ? 4 : 16)) { stage = "address"; rc = sqlite3_errcode(d) == SQLITE_NOMEM ? SQLITE_NOMEM : SQLITE_CORRUPT; if (rc == SQLITE_CORRUPT) failure = QString("invalid address family=%1 bytes=%2 type=%3").arg(family).arg(bytes).arg(type); break; } auto& node = nodes.last(); if (!node.addresses) node.details += QCoreApplication::translate("ChatGui", " addresses:\n"); const int protocol = sqlite3_column_int(st, 8), port = sqlite3_column_int(st, 10), rtt = sqlite3_column_int(st, 11); const auto profile = addressProfileText(sqlite3_column_int(st, 12), sqlite3_column_int(st, 13), sqlite3_column_int(st, 14)); node.details += QString(" %1:%2 %3 %4 rtt=%5ms\n").arg(addrStr(address, family)).arg(port) .arg(protoStr(protocol), profile, rtt > 0 ? QString::number(rtt / 10) : "-"); ++node.addresses; ++addressCount; } } if (rc != SQLITE_DONE && failure.isEmpty()) failure = QString::fromUtf8(sqlite3_errmsg(d)); const int finalized = sqlite3_finalize(statement.release()); if (rc == SQLITE_DONE && finalized != SQLITE_OK) { stage = "finalize"; failure = QString::fromUtf8(sqlite3_errstr(finalized)); } if (rc != SQLITE_DONE || finalized != SQLITE_OK) { DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "Nodes refresh failed stage=%s rc=%d finalize=%d node=%016llx: %s", stage, rc, finalized, static_cast(currentNode), qPrintable(failure)); m_table->setRowCount(0); m_dumpText->setPlainText(QCoreApplication::translate("ChatGui", "Query error")); return; } QString dump; for (const auto& node : nodes) { if (!dump.isEmpty()) dump += '\n'; dump += node.details; if (!node.addresses) dump += QCoreApplication::translate("ChatGui", " addresses: none\n"); } if (nodes.isEmpty()) dump = QCoreApplication::translate("ChatGui", "No nodes found."); m_table->setRowCount(0); m_table->setRowCount(nodes.size()); for (int row = 0; row < nodes.size(); ++row) { const auto& node = nodes[row]; const QString cells[] = {hex16(node.id), node.name, node.x25519, node.ed25519, QString::number(node.addresses)}; for (int column = 0; column < 5; ++column) m_table->setItem(row, column, new QTableWidgetItem(cells[column])); } m_table->resizeColumnsToContents(); m_dumpText->setPlainText(dump); DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "Nodes refresh complete nodes=%d addresses=%llu", int(nodes.size()), static_cast(addressCount)); }