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:
evgeny committed 2026-08-11 16:05:35 +03:00
1 parent 5a42741d06
commit fec3ea38a7
8 files changed
+217 -77

No files matched your search

+116 -64
View File
@@ -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("&nbsp;%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() {