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
+1
-1
@@ -269,7 +269,7 @@ private fun MemberDetailCard(
|
||||
Box(Modifier.size(8.dp).clip(CircleShape).background(dot))
|
||||
Spacer(Modifier.width(6.dp))
|
||||
Text(
|
||||
text = "*[${sk.linksUp}/${sk.linksDown}] ${sk.protocol} ${sk.ifName} ${sk.bindIp}:${sk.port}",
|
||||
text = "[${sk.linksUp}/${sk.linksDown}] ${sk.protocol} ${sk.ifName} ${sk.bindIp}:${sk.port}",
|
||||
fontFamily = FontFamily.Monospace,
|
||||
fontSize = 13.sp,
|
||||
color = Color.DarkGray
|
||||
|
||||
@@ -235,7 +235,7 @@ private fun GeneralSettingsTab(vm: ChatViewModel, dirty: Boolean, onDirty: (Bool
|
||||
Text(s.protocol, style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
if (s.linksUp + s.linksDown > 0) {
|
||||
Spacer(Modifier.width(4.dp))
|
||||
Text("*[${s.linksUp}/${s.linksDown}]", style = MaterialTheme.typography.labelSmall,
|
||||
Text("[${s.linksUp}/${s.linksDown}]", style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include <QWidget>
|
||||
#include <QListView>
|
||||
#include <QTextEdit>
|
||||
#include <QLabel>
|
||||
#include <QTimer>
|
||||
#include <QMenu>
|
||||
#include <QVector>
|
||||
@@ -33,13 +34,15 @@ struct MemberLinkSnap {
|
||||
int family;
|
||||
QByteArray addr;
|
||||
uint16_t port;
|
||||
char sockName[20];
|
||||
};
|
||||
|
||||
struct MemberSockSnap {
|
||||
int sockId;
|
||||
uint16_t port;
|
||||
int linkCount;
|
||||
QString ifname;
|
||||
int linksUp;
|
||||
int linksDown;
|
||||
QString ifname;
|
||||
};
|
||||
|
||||
class AccountList : public QWidget {
|
||||
@@ -51,6 +54,7 @@ public:
|
||||
void setChannel(const QString& channelId);
|
||||
void refresh();
|
||||
void onMemberDetailData(uint64_t nodeId, const uint8_t* data, int len);
|
||||
void onNodeinfoUpdate(const uint8_t* data, int len);
|
||||
|
||||
/* Bridge callbacks (called from GUI thread) */
|
||||
void onMemberList(const QString& chId, int count, const uint8_t* data, int dataLen);
|
||||
@@ -78,7 +82,9 @@ private:
|
||||
QListView *m_listView;
|
||||
MemberListModel *m_model;
|
||||
DbManager *m_db;
|
||||
QTextEdit *m_detailText;
|
||||
QWidget *m_detailPanel;
|
||||
QLabel *m_detailHeader;
|
||||
QTextEdit *m_detailText;
|
||||
QTimer *m_detailTimer;
|
||||
QTimer *m_linkTimer;
|
||||
QString m_channelId;
|
||||
@@ -94,6 +100,10 @@ private:
|
||||
bool m_snapConnUp = false;
|
||||
bool m_snapConnInit = false;
|
||||
int m_snapOwnTcpActive = 0;
|
||||
uint16_t m_snapChainRtt = 0;
|
||||
uint8_t m_snapHopCount = 0;
|
||||
uint16_t m_snapBgpNodes = 0;
|
||||
QString m_bgpText;
|
||||
QVector<MemberLinkSnap> m_snapLinks;
|
||||
QVector<MemberSockSnap> m_snapSocks;
|
||||
};
|
||||
@@ -34,12 +34,13 @@ struct TOPO_GROUP_NODE;
|
||||
#define GUI_EVT_MEMBER_LIST 23 /* data: [ch_id_len:1][ch_id:var][count:2][member:79B]* — полный список мемберов канала */
|
||||
#define GUI_EVT_MEMBER_UPDATED 24 /* data: [ch_id_len:1][ch_id:var][count:1][member:79B] — обновление одного мембера */
|
||||
#define GUI_EVT_MEMBER_REMOVED 25 /* data: [ch_id_len:1][ch_id:var][node_id:8] — мембер удалён из канала */
|
||||
#define GUI_EVT_BGP_INFO 26 /* data: text — BGP hop summary for member detail */
|
||||
|
||||
/* Размеры элементов пакета GUI_EVT_MEMBER_DETAIL (должны совпадать с chat_status.c) */
|
||||
#define MEMBER_DETAIL_LINK_SIZE 43
|
||||
#define MEMBER_DETAIL_SOCK_SIZE 48
|
||||
#define MEMBER_DETAIL_LINK_SIZE 63
|
||||
#define MEMBER_DETAIL_MAX_LINKS 16
|
||||
#define MEMBER_DETAIL_MAX_SOCKS 16
|
||||
#define MEMBER_DETAIL_SOCK_SIZE 48
|
||||
|
||||
/* ── API ── */
|
||||
|
||||
@@ -136,6 +137,11 @@ void gui_bridge_set_member_updated_cb(gui_member_updated_fn cb);
|
||||
typedef void (*gui_member_removed_fn)(const uint8_t* data, int len);
|
||||
void gui_bridge_set_member_removed_cb(gui_member_removed_fn cb);
|
||||
|
||||
/* Callback для BGP hop info текста (вызывается из GUI-потока)
|
||||
data: текстовая строка формата "rtt:12ms hop:3 via bgp: [ABCD] name1 [EF12] name2" */
|
||||
typedef void (*gui_bgp_info_fn)(const uint8_t* data, int len);
|
||||
void gui_bridge_set_bgp_info_cb(gui_bgp_info_fn cb);
|
||||
|
||||
/* uTun nodeinfo callback — registered via utun_add_nodeinfo_cbk, called from uasync thread */
|
||||
void gui_nodeinfo_cb_impl(struct TOPO_GROUP* group, struct TOPO_GROUP_NODE* nq, void* arg);
|
||||
|
||||
|
||||
@@ -44,6 +44,7 @@ static gui_member_detail_fn g_member_detail_cb = nullptr;
|
||||
static gui_member_list_fn g_member_list_cb = nullptr;
|
||||
static gui_member_updated_fn g_member_updated_cb = nullptr;
|
||||
static gui_member_removed_fn g_member_removed_cb = nullptr;
|
||||
static gui_bgp_info_fn g_bgp_info_cb = nullptr;
|
||||
static struct UASYNC* g_ua = nullptr;
|
||||
|
||||
/* ── GuiBridgeReceiver implementation ── */
|
||||
@@ -194,6 +195,9 @@ void GuiBridgeReceiver::processPost(int eventType, QByteArray data) {
|
||||
case GUI_EVT_MEMBER_REMOVED:
|
||||
if (g_member_removed_cb) g_member_removed_cb(d, dlen);
|
||||
break;
|
||||
case GUI_EVT_BGP_INFO:
|
||||
if (g_bgp_info_cb) g_bgp_info_cb(d, dlen);
|
||||
break;
|
||||
default:
|
||||
DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "gui_bridge: unknown event type %d", eventType);
|
||||
break;
|
||||
@@ -311,6 +315,10 @@ void gui_bridge_set_member_removed_cb(gui_member_removed_fn cb) {
|
||||
g_member_removed_cb = cb;
|
||||
}
|
||||
|
||||
void gui_bridge_set_bgp_info_cb(gui_bgp_info_fn cb) {
|
||||
g_bgp_info_cb = cb;
|
||||
}
|
||||
|
||||
} /* extern "C" */
|
||||
|
||||
/* ── uTun nodeinfo callback (called from uasync thread) ── */
|
||||
@@ -319,13 +327,16 @@ extern "C" void gui_nodeinfo_cb_impl(struct TOPO_GROUP* group, struct TOPO_GROUP
|
||||
(void)arg;
|
||||
if (!nq) return;
|
||||
uint16_t best_rtt = topo_get_chain_rtt(nq);
|
||||
uint8_t hop_count = 0;
|
||||
topo_node_best_hop_list(nq, &hop_count, NULL);
|
||||
uint8_t data[16];
|
||||
memcpy(data, &nq->node_id, 8);
|
||||
data[8] = nq->conn_presence;
|
||||
data[9] = nq->conn_up;
|
||||
memcpy(data + 10, &best_rtt, 2);
|
||||
data[12] = hop_count;
|
||||
data[13] = 0;
|
||||
uint16_t bgp_nodes = group ? (uint16_t)queue_entry_count(group->nodes) : 0;
|
||||
memcpy(data + 12, &bgp_nodes, 2);
|
||||
memcpy(data + 14, &bgp_nodes, 2);
|
||||
gui_bridge_post(GUI_EVT_NODEINFO_UPDATE, data, sizeof(data));
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user