member detail: structured display with addresses, connections, BGP summary and socket names
- Extend MEMBER_DETAIL_LINK_SIZE to 63B with 20B sock_name per link - Replace link_count with links_up/links_down (two uint16) in sock snap - Add BGP hop list collection via topo_node_best_hop_list, post as CHAT_EVT_BGP_INFO - Add direct RTT summary text via same event - Rework AccountList detail panel: header + QTextEdit with monospace layout - Display protocol (TCP/UDP), socket name line, colored dots for connections - Show [up/down] link counts for self listen sockets - Remove * prefix from link count display in Android
This commit is contained in:
1 parent
5a42741d06
commit
fec3ea38a7
8 files changed
+217
-77
No files matched your search
@@ -64,7 +64,7 @@ AccountList::AccountList(DbManager* db, QWidget *parent)
|
||||
, m_listView(new QListView(this))
|
||||
, m_model(new MemberListModel(this))
|
||||
, m_db(db)
|
||||
, m_detailText(nullptr)
|
||||
, m_detailPanel(nullptr)
|
||||
{
|
||||
auto *layout = new QVBoxLayout(this);
|
||||
layout->setContentsMargins(0, 0, 0, 0);
|
||||
@@ -83,17 +83,25 @@ AccountList::AccountList(DbManager* db, QWidget *parent)
|
||||
m_listView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||
m_listView->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
|
||||
m_listView->setContextMenuPolicy(Qt::CustomContextMenu);
|
||||
m_detailText = new QTextEdit(this);
|
||||
m_detailPanel = new QWidget(this);
|
||||
auto* detailLayout = new QVBoxLayout(m_detailPanel);
|
||||
detailLayout->setContentsMargins(0, 0, 0, 0);
|
||||
detailLayout->setSpacing(0);
|
||||
|
||||
m_detailHeader = new QLabel("Select a member", m_detailPanel);
|
||||
m_detailHeader->setWordWrap(true);
|
||||
m_detailHeader->setStyleSheet("font-weight: bold; padding: 4px 8px; background: palette(window);");
|
||||
detailLayout->addWidget(m_detailHeader);
|
||||
|
||||
m_detailText = new QTextEdit(m_detailPanel);
|
||||
m_detailText->setReadOnly(true);
|
||||
m_detailText->setFont(QFont("monospace", 10));
|
||||
m_detailText->setStyleSheet(
|
||||
"QTextEdit { border-top: 1px solid palette(mid); border-bottom: none;"
|
||||
" border-left: none; border-right: none; padding: 6px; background: palette(base); }");
|
||||
m_detailText->setPlaceholderText("Select a member");
|
||||
m_detailText->setFont(QFont("monospace", 9));
|
||||
m_detailText->setStyleSheet("QTextEdit { border: none; padding: 4px; background: palette(base); }");
|
||||
detailLayout->addWidget(m_detailText, 1);
|
||||
|
||||
auto *memberSplitter = new QSplitter(Qt::Vertical, this);
|
||||
memberSplitter->addWidget(m_listView);
|
||||
memberSplitter->addWidget(m_detailText);
|
||||
memberSplitter->addWidget(m_detailPanel);
|
||||
memberSplitter->setStretchFactor(0, 3);
|
||||
memberSplitter->setStretchFactor(1, 2);
|
||||
memberSplitter->setSizes({360, 240});
|
||||
@@ -119,6 +127,19 @@ AccountList::AccountList(DbManager* db, QWidget *parent)
|
||||
|
||||
g_detailTarget = this;
|
||||
gui_bridge_set_member_detail_cb(memberDetailCb);
|
||||
|
||||
static bool nodeinfoRegistered = false;
|
||||
if (!nodeinfoRegistered) {
|
||||
gui_bridge_set_nodeinfo_update_cb([](const uint8_t* data, int len) {
|
||||
if (g_detailTarget) g_detailTarget->onNodeinfoUpdate(data, len);
|
||||
});
|
||||
gui_bridge_set_bgp_info_cb([](const uint8_t* data, int len) {
|
||||
if (!g_detailTarget) return;
|
||||
g_detailTarget->m_bgpText = QString::fromUtf8((const char*)data, len);
|
||||
g_detailTarget->updateDetailDisplay();
|
||||
});
|
||||
nodeinfoRegistered = true;
|
||||
}
|
||||
}
|
||||
|
||||
AccountList::~AccountList() {
|
||||
@@ -131,6 +152,7 @@ void AccountList::setChannel(const QString& channelId) {
|
||||
m_detailTimer->stop();
|
||||
m_linkTimer->stop();
|
||||
m_detailNodeId = 0;
|
||||
m_detailHeader->setText("Select a member");
|
||||
m_detailText->clear();
|
||||
}
|
||||
|
||||
@@ -157,6 +179,10 @@ void AccountList::showMemberDetail(quint64 nodeId) {
|
||||
m_snapConnUp = false;
|
||||
m_snapConnInit = false;
|
||||
m_snapOwnTcpActive = 0;
|
||||
m_snapChainRtt = 0;
|
||||
m_snapHopCount = 0;
|
||||
m_snapBgpNodes = 0;
|
||||
m_bgpText.clear();
|
||||
m_snapLinks.clear();
|
||||
m_snapSocks.clear();
|
||||
|
||||
@@ -266,6 +292,7 @@ void AccountList::onMemberDetailData(uint64_t nodeId, const uint8_t* data, int l
|
||||
lk.family = p[24];
|
||||
lk.addr = QByteArray((const char*)p + 25, 16);
|
||||
memcpy(&lk.port, p + 41, 2);
|
||||
memcpy(lk.sockName, p + 43, 20); lk.sockName[19] = '\0';
|
||||
p += MEMBER_DETAIL_LINK_SIZE;
|
||||
m_snapLinks.append(lk);
|
||||
}
|
||||
@@ -278,7 +305,8 @@ void AccountList::onMemberDetailData(uint64_t nodeId, const uint8_t* data, int l
|
||||
MemberSockSnap s;
|
||||
memcpy(&s.sockId, p, 4);
|
||||
memcpy(&s.port, p + 4, 2);
|
||||
memcpy(&s.linkCount, p + 6, 4);
|
||||
uint16_t lu = 0, ld = 0; memcpy(&lu, p + 6, 2); memcpy(&ld, p + 8, 2);
|
||||
s.linksUp = lu; s.linksDown = ld;
|
||||
s.ifname = QString::fromUtf8((const char*)p + 10).trimmed();
|
||||
p += MEMBER_DETAIL_SOCK_SIZE;
|
||||
m_snapSocks.append(s);
|
||||
@@ -302,68 +330,92 @@ void AccountList::onMemberDetailData(uint64_t nodeId, const uint8_t* data, int l
|
||||
}
|
||||
|
||||
void AccountList::updateDetailDisplay() {
|
||||
bool isSelf = m_db ? (m_detailNodeId == m_db->myNodeId()) : false;
|
||||
QString nodeHex = QString("0x%1").arg(m_detailNodeId, 16, 16, QChar('0'));
|
||||
|
||||
/* ── Header ── */
|
||||
QString dotColor = m_detailOnline ? "#4CAF50" : "#9E9E9E";
|
||||
QStringList hdr;
|
||||
hdr.append(QString("<span style='color:%1;'>●</span> %2 %3")
|
||||
.arg(dotColor, m_detailName.toHtmlEscaped(), nodeHex));
|
||||
if (m_detailLastSeen > 0)
|
||||
hdr.append(QString("seen %1").arg(relativeTimeStr(m_detailLastSeen)));
|
||||
if (m_detailCreated > 0)
|
||||
hdr.append(QString("created %1").arg(relativeTimeStr(m_detailCreated)));
|
||||
m_detailHeader->setText(hdr.join(" "));
|
||||
|
||||
/* ── Body ── */
|
||||
QStringList lines;
|
||||
|
||||
lines.append(QString("ID:0x%1").arg(m_detailNodeId, 16, 16, QChar('0')));
|
||||
|
||||
QString dotColor = m_detailOnline ? "#4CAF50" : "#9E9E9E";
|
||||
lines.append(QString("<span style='color:%1;'>●</span> %2")
|
||||
.arg(dotColor, m_detailName.toHtmlEscaped()));
|
||||
|
||||
if (m_detailLastSeen > 0)
|
||||
lines.append(QString("⌛ %1").arg(relativeTimeStr(m_detailLastSeen)));
|
||||
|
||||
if (m_detailCreated > 0)
|
||||
lines.append(QString("📅 %1").arg(relativeTimeStr(m_detailCreated)));
|
||||
|
||||
bool isSelf = m_db ? (m_detailNodeId == m_db->myNodeId()) : false;
|
||||
|
||||
if (isSelf) {
|
||||
if (!m_detailAddrs.isEmpty()) {
|
||||
lines.append("── Addresses ──");
|
||||
for (const auto& a : m_detailAddrs) {
|
||||
QString proto;
|
||||
switch (a.protocol) { case 1: proto = "UDP"; break; case 2: proto = "TCP"; break; default: proto = "UDP/TCP"; break; }
|
||||
int lc = 0;
|
||||
if (a.protocol == 1) { for (const auto& s : m_snapSocks) { if (s.port == a.port) { lc = s.linkCount; break; } } }
|
||||
else if (a.protocol == 2) lc = m_snapOwnTcpActive;
|
||||
QString prefix = lc > 0 ? QString("[%1] ").arg(lc) : "";
|
||||
if (!a.ifname.isEmpty())
|
||||
lines.append(QString("%1● %2 %3").arg(prefix, proto, a.ifname));
|
||||
else
|
||||
lines.append(QString("%1● %2:%3:%4").arg(prefix, proto, addrStr(a.addr.constData(), a.family)).arg(a.port));
|
||||
}
|
||||
} else {
|
||||
/* all live links with arrows */
|
||||
for (const auto& lk : m_snapLinks) {
|
||||
QString proto = lk.isTcp ? "TCP" : "UDP";
|
||||
QString lc = (lk.linkState == 3 && lk.linkStatus == 1) ? "#4CAF50" : "#E53935";
|
||||
QString left = QString("%1:%2").arg(addrStr(lk.localAddr.constData(), lk.localFamily)).arg(lk.localPort);
|
||||
QString right = QString("%1:%2").arg(addrStr(lk.addr.constData(), lk.family)).arg(lk.port);
|
||||
const char* arrow = lk.isServer
|
||||
? "<b style='color:#FF9800;font-size:13pt;'>◀</b>"
|
||||
: "<b style='color:#2196F3;font-size:13pt;'>▶</b>";
|
||||
if (lk.localFamily == 6 || lk.family == 6) {
|
||||
lines.append(QString("<span style='color:%1;'>●</span> %2 %3:%4").arg(lc, proto, addrStr(lk.localAddr.constData(), lk.localFamily)).arg(lk.localPort));
|
||||
lines.append(QString(" %1 %2:%3").arg(arrow, addrStr(lk.addr.constData(), lk.family)).arg(lk.port));
|
||||
} else {
|
||||
lines.append(QString("<span style='color:%1;'>●</span> %2 %3 %4 %5")
|
||||
.arg(lc, proto, left, arrow, right));
|
||||
}
|
||||
}
|
||||
/* DB addresses without live link */
|
||||
for (const auto& a : m_detailAddrs) {
|
||||
bool matched = false;
|
||||
for (const auto& lk : m_snapLinks) {
|
||||
if (lk.family == a.family && lk.port == a.port && lk.addr == a.addr) { matched = true; break; }
|
||||
}
|
||||
if (matched) continue;
|
||||
QString proto;
|
||||
switch (a.protocol) { case 1: proto = "UDP"; break; case 2: proto = "TCP"; break; default: proto = "UDP/TCP"; break; }
|
||||
lines.append(QString("<span style='color:#9E9E9E;'>●</span> %1 %2:%3")
|
||||
.arg(proto, addrStr(a.addr.constData(), a.family)).arg(a.port));
|
||||
switch (a.protocol) { case 1: proto = "UDP"; break; case 2: proto = "TCP"; break; default: proto = "?"; break; }
|
||||
lines.append(QString(" %1 %2 %3:%4")
|
||||
.arg(proto)
|
||||
.arg(a.ifname)
|
||||
.arg(addrStr(a.addr.constData(), a.family))
|
||||
.arg(a.port));
|
||||
}
|
||||
}
|
||||
|
||||
m_detailText->setHtml(lines.join("<br>"));
|
||||
if (isSelf) {
|
||||
lines.append("── Listen ──");
|
||||
for (const auto& s : m_snapSocks) {
|
||||
const DetailAddress* a = nullptr;
|
||||
for (const auto& da : m_detailAddrs) {
|
||||
if (da.socket_id == s.sockId) { a = &da; break; }
|
||||
}
|
||||
QString proto = a ? (a->protocol == 2 ? "TCP" : "UDP") : "?";
|
||||
QString ip = a ? addrStr(a->addr.constData(), a->family) : "?";
|
||||
lines.append(QString(" [%1/%2] %3:%4 %5")
|
||||
.arg(s.linksUp).arg(s.linksDown)
|
||||
.arg(ip).arg(s.port)
|
||||
.arg(proto));
|
||||
}
|
||||
} else {
|
||||
if (!m_snapLinks.isEmpty()) {
|
||||
lines.append("── Connections ──");
|
||||
for (const auto& lk : m_snapLinks) {
|
||||
bool up = (lk.linkState == 3 && lk.linkStatus == 1);
|
||||
QString c = up ? "#4CAF50" : "#E53935";
|
||||
lines.append(QString("<span style='color:%1;'>●</span> %2 %3:%4 %5 %6:%7")
|
||||
.arg(c)
|
||||
.arg(lk.isTcp ? "TCP" : "UDP")
|
||||
.arg(addrStr(lk.localAddr.constData(), lk.localFamily)).arg(lk.localPort)
|
||||
.arg(lk.isServer ? "\u25c0" : "\u25b6")
|
||||
.arg(addrStr(lk.addr.constData(), lk.family)).arg(lk.port));
|
||||
if (lk.sockName[0])
|
||||
lines.append(QString(" %1").arg(lk.sockName));
|
||||
}
|
||||
}
|
||||
/* summary */
|
||||
if (!m_bgpText.isEmpty()) {
|
||||
lines.append(m_bgpText);
|
||||
} else if (m_snapConnUp) {
|
||||
lines.append("UP");
|
||||
}
|
||||
}
|
||||
|
||||
if (lines.isEmpty())
|
||||
m_detailText->clear();
|
||||
else
|
||||
m_detailText->setHtml(QString("<pre style='margin:0;'>") + lines.join("\n") + "</pre>");
|
||||
}
|
||||
|
||||
void AccountList::onNodeinfoUpdate(const uint8_t* data, int len) {
|
||||
if (len < 16 || m_detailNodeId == 0) return;
|
||||
uint64_t nodeId; memcpy(&nodeId, data, 8);
|
||||
if (nodeId != m_detailNodeId) return;
|
||||
m_snapConnPresent = (data[8] & 1) != 0;
|
||||
m_snapConnUp = (data[9] & 1) != 0;
|
||||
memcpy(&m_snapChainRtt, data + 10, 2);
|
||||
m_snapHopCount = data[12];
|
||||
memcpy(&m_snapBgpNodes, data + 14, 2);
|
||||
/* data[13] is reserved/unused */
|
||||
if (m_snapConnPresent)
|
||||
updateDetailDisplay();
|
||||
}
|
||||
|
||||
void AccountList::onDetailTimerTick() {
|
||||
|
||||
Reference in new issue
Block a user