// nodespage.cpp — Nodes table + dump in settings #include "nodespage.h" #include "../db/db_manager.h" #include #include #include #include #include #include #include #include static QString hex4(uint32_t v) { return QString("%1").arg(v, 4, 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) { const uint16_t* a = (const uint16_t*)addr; 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) { switch (protocol) { case 1: return "UDP"; case 2: return "TCP"; case 3: return "UDP+TCP"; default: return QString::number(protocol); } } static QString addrTypeStr(int t) { switch (t) { case 0: return "NETIF"; case 1: return "DIRECT"; case 2: return "NAT_EIM"; case 3: return "NAT_STRICT"; default: return QString("?%1").arg(t); } } 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("Known Nodes", this); title->setStyleSheet("font-weight: bold; font-size: 13px;"); layout->addWidget(title); m_table = new QTableWidget(0, 6, this); m_table->setHorizontalHeaderLabels({"Node ID", "Name", "Online", "x25519", "ed25519", "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("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("Click Refresh to load nodes"); layout->addWidget(m_dumpText, 3); auto* btnLayout = new QHBoxLayout(); btnLayout->addStretch(); m_refreshBtn = new QPushButton("Refresh", this); m_refreshBtn->setFixedWidth(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) return; sqlite3* d = m_db->m_db; /* ── table ── */ m_table->setRowCount(0); sqlite3_stmt* st = nullptr; if (sqlite3_prepare_v2(d, "SELECT node_id, name, online, x25519_pubkey, ed25519_pubkey," " last_seen_at, created_at FROM nodes ORDER BY node_id", -1, &st, nullptr) != SQLITE_OK) return; int row = 0; while (sqlite3_step(st) == SQLITE_ROW) { m_table->insertRow(row); quint64 nid = (quint64)sqlite3_column_int64(st, 0); auto setCell = [&](int col, const QString& v) { m_table->setItem(row, col, new QTableWidgetItem(v)); }; setCell(0, hex4((uint32_t)(nid & 0xFFFF))); setCell(1, DbManager::colText(st, 1)); setCell(2, sqlite3_column_int(st, 2) ? "yes" : "no"); QByteArray x25 = DbManager::colBlob(st, 3); QByteArray ed = DbManager::colBlob(st, 4); setCell(3, x25.isEmpty() ? "-" : shortenKey(x25)); setCell(4, ed.isEmpty() ? "-" : shortenKey(ed)); sqlite3_stmt* ac = nullptr; sqlite3_prepare_v2(d, "SELECT COUNT(*) FROM node_addresses WHERE node_id=?", -1, &ac, nullptr); if (ac) { sqlite3_bind_int64(ac, 1, (sqlite3_int64)nid); setCell(5, sqlite3_step(ac) == SQLITE_ROW ? QString::number(sqlite3_column_int(ac, 0)) : "0"); sqlite3_finalize(ac); } else { setCell(5, "0"); } row++; } sqlite3_finalize(st); m_table->resizeColumnsToContents(); /* ── text dump ── */ QString dump; st = nullptr; sqlite3_prepare_v2(d, "SELECT node_id, name, online, x25519_pubkey, ed25519_pubkey," " last_seen_at, created_at FROM nodes ORDER BY node_id", -1, &st, nullptr); if (!st) { m_dumpText->setPlainText("Query error"); return; } int nodeCount = 0; while (sqlite3_step(st) == SQLITE_ROW) { if (nodeCount > 0) dump += "\n"; nodeCount++; quint64 nid = (quint64)sqlite3_column_int64(st, 0); QString name = DbManager::colText(st, 1); int online = sqlite3_column_int(st, 2); QByteArray x25 = DbManager::colBlob(st, 3); QByteArray ed = DbManager::colBlob(st, 4); qint64 last_seen = sqlite3_column_int64(st, 5); qint64 created = sqlite3_column_int64(st, 6); dump += QString("Node: %1").arg(hex4((uint32_t)(nid & 0xFFFF))); if (!name.isEmpty()) dump += QString(" (%1)").arg(name); dump += QString(" id=0x%1\n").arg(nid, 16, 16, QChar('0')); dump += QString(" online: %1\n").arg(online ? "yes" : "no"); dump += QString(" x25519: %1\n").arg(x25.isEmpty() ? "-" : shortenKey(x25)); dump += QString(" ed25519: %1\n").arg(ed.isEmpty() ? "-" : shortenKey(ed)); if (last_seen > 0) dump += QString(" last_seen: %1\n").arg(QDateTime::fromSecsSinceEpoch(last_seen).toString("yyyy-MM-dd hh:mm:ss")); if (created > 0) dump += QString(" created: %1\n").arg(QDateTime::fromSecsSinceEpoch(created).toString("yyyy-MM-dd hh:mm:ss")); sqlite3_stmt* na = nullptr; sqlite3_prepare_v2(d, "SELECT family, protocol, address, port, rtt, addr_type" " FROM node_addresses WHERE node_id=? ORDER BY family, port", -1, &na, nullptr); if (na) { sqlite3_bind_int64(na, 1, (sqlite3_int64)nid); int ac = 0; while (sqlite3_step(na) == SQLITE_ROW) { if (ac == 0) dump += " addresses:\n"; int family = sqlite3_column_int(na, 0); int proto = sqlite3_column_int(na, 1); const void* addr = sqlite3_column_blob(na, 2); int addrSz = sqlite3_column_bytes(na, 2); int port = sqlite3_column_int(na, 3); int rtt = sqlite3_column_int(na, 4); int addrType = sqlite3_column_int(na, 5); if (addr && addrSz > 0) { dump += QString(" %1:%2 %3 %4 rtt=%5ms\n") .arg(addrStr(addr, family)) .arg(port) .arg(protoStr(proto)) .arg(addrTypeStr(addrType)) .arg(rtt > 0 ? QString::number(rtt) : "-"); } ac++; } if (ac == 0) dump += " addresses: none\n"; sqlite3_finalize(na); } else { dump += " addresses: none\n"; } } sqlite3_finalize(st); if (nodeCount == 0) dump = "No nodes found."; m_dumpText->setPlainText(dump); }