chatgui: add Status page in settings (NTP + connections status)

This commit is contained in:
Evgeny committed 2026-07-17 16:25:55 +03:00
1 parent f0bb03c2cf
commit 960cde3eda
15 files changed
+651 -62

No files matched your search

+1
View File
@@ -69,6 +69,7 @@ add_executable(chatgui
src/settingsdialog.cpp
src/networksettingspage.cpp
src/databasesettingspage.cpp
src/statuspage.cpp
transport/utun_node.cpp
transport/node_config.cpp
transport/config_updater.cpp
+7
View File
@@ -2,6 +2,7 @@
#include "settingsdialog.h"
#include "networksettingspage.h"
#include "databasesettingspage.h"
#include "statuspage.h"
#include "../transport/gui_bridge.h"
#include "../../lib/mem.h"
#include "../db/db_manager.h"
@@ -40,6 +41,7 @@ SettingsDialog::SettingsDialog(const QString& configPath, DbManager* db, QWidget
m_categoryList->addItem("Profile");
m_categoryList->addItem("Network");
m_categoryList->addItem("Database");
m_categoryList->addItem("Status");
mainLayout->addWidget(m_categoryList);
// --- Right panel: stacked pages ---
@@ -71,6 +73,10 @@ SettingsDialog::SettingsDialog(const QString& configPath, DbManager* db, QWidget
// Database page
m_databasePage = new DatabaseSettingsPage(db, m_pages);
m_pages->addWidget(m_databasePage);
// Status page
m_statusPage = new StatusPage(m_pages);
m_pages->addWidget(m_statusPage);
rightLayout->addWidget(m_pages, 1);
// --- Bottom buttons ---
@@ -127,6 +133,7 @@ void SettingsDialog::loadProfileFromConfig(const QString& path) {
void SettingsDialog::onCategoryChanged(int row) {
m_pages->setCurrentIndex(row);
if (row == 3) m_statusPage->refreshStatus();
}
void SettingsDialog::onSave() {
+2
View File
@@ -10,6 +10,7 @@
class NetworkSettingsPage;
class DatabaseSettingsPage;
class StatusPage;
class DbManager;
class SettingsDialog : public QDialog {
@@ -32,4 +33,5 @@ private:
QString m_oldNickname;
NetworkSettingsPage* m_networkPage;
DatabaseSettingsPage* m_databasePage;
StatusPage* m_statusPage;
};
+68
View File
@@ -0,0 +1,68 @@
// statuspage.cpp — Status page (NTP + connections) in settings
#include "statuspage.h"
extern "C" {
#include "../transport/gui_bridge.h"
#include "../transport/chat_core.h"
}
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QLabel>
#include <QFont>
static StatusPage* g_activeStatusPage = nullptr;
static void statusRefreshCallback(const char* text, int len) {
if (g_activeStatusPage)
g_activeStatusPage->setText(QString::fromUtf8(text, len));
}
StatusPage::StatusPage(QWidget* parent)
: QWidget(parent)
{
auto* layout = new QVBoxLayout(this);
layout->setSpacing(8);
layout->setContentsMargins(0, 0, 0, 0);
auto* title = new QLabel("System Status", this);
title->setStyleSheet("font-weight: bold; font-size: 13px;");
layout->addWidget(title);
m_textEdit = new QPlainTextEdit(this);
m_textEdit->setReadOnly(true);
m_textEdit->setFont(QFont("monospace", 10));
m_textEdit->setStyleSheet(
"QPlainTextEdit { border: 1px solid palette(mid); border-radius: 4px;"
" padding: 8px; background: palette(base); }");
m_textEdit->setPlaceholderText("Click Refresh to load status");
layout->addWidget(m_textEdit, 1);
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, &StatusPage::onRefreshClicked);
btnLayout->addWidget(m_refreshBtn);
layout->addLayout(btnLayout);
g_activeStatusPage = this;
gui_bridge_set_status_refresh_cb(statusRefreshCallback);
}
StatusPage::~StatusPage() {
if (g_activeStatusPage == this) g_activeStatusPage = nullptr;
}
void StatusPage::refreshStatus() {
m_textEdit->setPlainText("Loading...");
gui_bridge_post_uasync_fn(chat_core_collect_status_trampoline, NULL);
}
void StatusPage::onRefreshClicked() {
refreshStatus();
}
+22
View File
@@ -0,0 +1,22 @@
// statuspage.h — Status page (NTP + connections) in settings
#pragma once
#include <QWidget>
#include <QPlainTextEdit>
#include <QPushButton>
class StatusPage : public QWidget {
Q_OBJECT
public:
explicit StatusPage(QWidget* parent = nullptr);
~StatusPage() override;
void refreshStatus();
void setText(const QString& text) { m_textEdit->setPlainText(text); }
private slots:
void onRefreshClicked();
private:
QPlainTextEdit* m_textEdit;
QPushButton* m_refreshBtn;
};
+143 -7
View File
@@ -307,8 +307,27 @@ void chat_core_update_my_name_trampoline(void* arg) {
u_free(arg);
}
static void chat_core_collect_status(void);
void chat_core_collect_status_trampoline(void* arg) {
(void)arg;
chat_core_collect_status();
}
/* ─── отправка сообщения (GUI → uasync) ─── */
static int msg_get_prev_chain_hash(const char* tbl, uint8_t out[32]) {
char sql[128]; snprintf(sql, sizeof(sql),
"SELECT chain_hash FROM \"%s\" ORDER BY timestamp DESC LIMIT 1", tbl);
sqlite3_stmt* st = NULL;
if (sqlite3_prepare_v2(g_cc.db, sql, -1, &st, NULL) != SQLITE_OK) { memset(out,0,32); return -1; }
if (sqlite3_step(st) == SQLITE_ROW) {
const void* blob = sqlite3_column_blob(st, 0); int blen = sqlite3_column_bytes(st, 0);
if (blob && blen == 32) memcpy(out, blob, 32); else memset(out, 0, 32);
} else { memset(out, 0, 32); }
sqlite3_finalize(st); return 0;
}
void chat_core_submit_message(struct chat_msg_submit* req) {
if (!g_cc.initialized) { DEBUG_ERROR(DEBUG_CATEGORY_CONNECTIVITY, "%s: submit before init", CC_ID); return; }
if (!req) return;
@@ -342,14 +361,26 @@ void chat_core_submit_message(struct chat_msg_submit* req) {
DEBUG_ERROR(DEBUG_CATEGORY_CONNECTIVITY, "%s: db_sync_insert_signed failed ret=%d", CC_ID, ret);
/* Insert directly to msg_ table as fallback (dual-write not triggered by callback) */
char tbl[80]; msg_table_name(req->channel_id, tbl, sizeof(tbl));
char sql[512]; snprintf(sql, sizeof(sql), "INSERT OR IGNORE INTO \"%s\" (node_id,content_type,data,timestamp,signature,is_outgoing,is_read) VALUES(?,?,?,?,?,1,1)", tbl);
/* compute datahash and chain_hash */
uint8_t dh_buf2[32]; SHA256(req->data, req->data_len, dh_buf2);
uint64_t datahash2; memcpy(&datahash2, dh_buf2, 8);
uint8_t prev_ch2[32]; msg_get_prev_chain_hash(tbl, prev_ch2);
uint8_t ch_buf2[48]; memcpy(ch_buf2, prev_ch2, 32); memcpy(ch_buf2+32, &req->timestamp, 8); memcpy(ch_buf2+40, &datahash2, 8);
uint8_t ch_hash2[32]; SHA256(ch_buf2, 48, ch_hash2);
char sql[512]; snprintf(sql, sizeof(sql), "INSERT OR IGNORE INTO \"%s\" (node_id,content_type,data,timestamp,datahash,chain_hash,signature,is_outgoing,is_read) VALUES(?,?,?,?,?,?,?,?,?)", tbl);
sqlite3_stmt* st=NULL;
if (sqlite3_prepare_v2(g_cc.db,sql,-1,&st,NULL)==SQLITE_OK) {
sqlite3_bind_int64(st,1,(sqlite3_int64)g_cc.my_node_id);
sqlite3_bind_text(st,2,req->content_type,-1,SQLITE_STATIC);
sqlite3_bind_blob(st,3,req->data,(int)req->data_len,SQLITE_STATIC);
sqlite3_bind_int64(st,4,(sqlite3_int64)req->timestamp);
sqlite3_bind_blob(st,5,sig,64,SQLITE_STATIC);
sqlite3_bind_int64(st,5,(sqlite3_int64)datahash2);
sqlite3_bind_blob(st,6,ch_hash2,32,SQLITE_STATIC);
sqlite3_bind_blob(st,7,sig,64,SQLITE_STATIC);
sqlite3_bind_int(st,8,1);
sqlite3_bind_int(st,9,1);
sqlite3_step(st); sqlite3_finalize(st);
}
/* notify GUI anyway */
@@ -977,11 +1008,18 @@ void chat_core_ensure_channel_ready(const char* ch_id) {
" content_type TEXT NOT NULL,"
" data BLOB NOT NULL,"
" timestamp INTEGER NOT NULL,"
" datahash INTEGER DEFAULT 0,"
" chain_hash BLOB,"
" signature BLOB,"
" is_outgoing INTEGER DEFAULT 0,"
" is_read INTEGER DEFAULT 0,"
" UNIQUE(timestamp, node_id))", tbl_msg);
db_exec(sql);
/* migration for existing tables without datahash/chain_hash columns */
snprintf(sql, sizeof(sql), "ALTER TABLE \"%s\" ADD COLUMN datahash INTEGER DEFAULT 0", tbl_msg);
sqlite3_exec(g_cc.db, sql, NULL, NULL, NULL);
snprintf(sql, sizeof(sql), "ALTER TABLE \"%s\" ADD COLUMN chain_hash BLOB", tbl_msg);
sqlite3_exec(g_cc.db, sql, NULL, NULL, NULL);
snprintf(sql, sizeof(sql),
"CREATE INDEX IF NOT EXISTS \"idx_%s_ts_node\" ON \"%s\"(timestamp, node_id)",
tbl_msg, tbl_msg);
@@ -1103,6 +1141,7 @@ static void si_register(struct DB_SYNC_INSTANCE* si, const char* ch_id) {
g_cc.si[g_cc.si_count]=si; g_cc.si_ch_id[g_cc.si_count]=u_strdup(ch_id); g_cc.si_count++;
}
static void on_db_sync_insert(struct DB_SYNC_INSTANCE* si, uint64_t record_ts, const char* data, size_t len, uint64_t author, void* arg) {
const char* ch_id = (const char*)arg;
static int insert_count = 0;
@@ -1123,18 +1162,28 @@ static void on_db_sync_insert(struct DB_SYNC_INSTANCE* si, uint64_t record_ts, c
if (!jd_start) { DEBUG_ERROR(DEBUG_CATEGORY_CONNECTIVITY, "%s: on_db_sync_insert JSON parse fail: no '\"d\"' field ch=%s", CC_ID, ch_id); return; }
char tbl[80]; msg_table_name(ch_id, tbl, sizeof(tbl));
/* compute datahash and chain_hash */
uint8_t dh_buf[32]; SHA256((const uint8_t*)jd_start, jd_len, dh_buf);
uint64_t datahash; memcpy(&datahash, dh_buf, 8);
uint8_t prev_ch[32]; msg_get_prev_chain_hash(tbl, prev_ch);
uint8_t ch_buf[48]; memcpy(ch_buf, prev_ch, 32); memcpy(ch_buf+32, &record_ts, 8); memcpy(ch_buf+40, &datahash, 8);
uint8_t ch_hash[32]; SHA256(ch_buf, 48, ch_hash);
char sql[512]; snprintf(sql, sizeof(sql),
"INSERT OR IGNORE INTO \"%s\" (node_id,content_type,data,timestamp,signature,is_outgoing,is_read)"
" VALUES(?,?,?,?,?,?,?)", tbl);
"INSERT OR IGNORE INTO \"%s\" (node_id,content_type,data,timestamp,datahash,chain_hash,signature,is_outgoing,is_read)"
" VALUES(?,?,?,?,?,?,?,?,?)", tbl);
sqlite3_stmt* st=NULL;
if (sqlite3_prepare_v2(g_cc.db,sql,-1,&st,NULL)!=SQLITE_OK) { DEBUG_ERROR(DEBUG_CATEGORY_CONNECTIVITY, "%s: on_db_sync_insert prepare failed ch=%s", CC_ID, ch_id); return; }
sqlite3_bind_int64(st,1,(sqlite3_int64)jn);
sqlite3_bind_text(st,2,jct,(int)jct_len,SQLITE_STATIC);
sqlite3_bind_blob(st,3,jd_start,(int)jd_len,SQLITE_STATIC);
sqlite3_bind_int64(st,4,(sqlite3_int64)record_ts);
{ static const uint8_t z64[64]={0}; sqlite3_bind_blob(st,5,z64,64,SQLITE_STATIC); }
sqlite3_bind_int(st,6,(uint64_t)jn==g_cc.my_node_id?1:0);
sqlite3_bind_int(st,7,1);
sqlite3_bind_int64(st,5,(sqlite3_int64)datahash);
sqlite3_bind_blob(st,6,ch_hash,32,SQLITE_STATIC);
{ static const uint8_t z64[64]={0}; sqlite3_bind_blob(st,7,z64,64,SQLITE_STATIC); }
sqlite3_bind_int(st,8,(uint64_t)jn==g_cc.my_node_id?1:0);
sqlite3_bind_int(st,9,1);
int rc=sqlite3_step(st); sqlite3_finalize(st);
if (rc==SQLITE_DONE) {
insert_count++;
@@ -1152,3 +1201,90 @@ static void on_db_sync_insert(struct DB_SYNC_INSTANCE* si, uint64_t record_ts, c
CC_ID, rc, ch_id);
}
}
/* ─── сбор статуса (NTP + connections) и отправка в GUI ─── */
static const char* conn_state_str(uint8_t state) {
switch (state) { case 0: return "DISCONNECTED"; case 1: return "CONNECTING"; case 2: return "CONNECTED"; default: return "?"; }
}
static const char* conn_type_str(uint8_t t) {
switch (t) { case 0: return "NONE"; case 1: return "DIRECT"; case 2: return "REVERSE"; case 3: return "INDIRECT"; default: return "?"; }
}
static const char* nat_type_str(uint8_t t) {
switch (t) { case 0: return "UNKNOWN"; case 1: return "EIM"; case 2: return "STRICT"; case 3: return "DIRECT"; default: return "?"; }
}
static void chat_core_collect_status(void) {
char buf[8192]; int off = 0;
if (!g_cc.initialized || !g_cc.inst) {
off = snprintf(buf, sizeof(buf), "uTun not initialized\n");
gui_bridge_post(GUI_EVT_STATUS_REFRESH, (const uint8_t*)buf, off);
return;
}
/* ─── NTP ─── */
struct NTP_TIME* ntp = &g_cc.inst->ntp;
off += snprintf(buf + off, sizeof(buf) - off, "=== NTP ===\n");
off += snprintf(buf + off, sizeof(buf) - off, "Enabled: %s\n", ntp->enabled ? "yes" : "no");
off += snprintf(buf + off, sizeof(buf) - off, "Synced: %s\n", ntp->synced ? "yes" : "no");
off += snprintf(buf + off, sizeof(buf) - off, "Offset: %lld us\n", (long long)ntp->offset_us);
if (ntp->server_count > 0 && ntp->servers) {
off += snprintf(buf + off, sizeof(buf) - off, "Servers: %d\n", ntp->server_count);
for (int i = 0; i < ntp->server_count && ntp->servers[i]; i++)
off += snprintf(buf + off, sizeof(buf) - off, " [%d] %s%s\n", i, ntp->servers[i],
i == ntp->server_current ? " (current)" : "");
}
if (ntp->synced && ntp->last_sync_tb > 0) {
uint64_t now_tb = get_time_tb();
uint64_t ago_sec = (now_tb - ntp->last_sync_tb) / 10000;
off += snprintf(buf + off, sizeof(buf) - off, "Last sync: %llu sec ago\n", (unsigned long long)ago_sec);
}
off += snprintf(buf + off, sizeof(buf) - off, "\n");
/* ─── Connections ─── */
int conn_count = 0;
struct ll_entry* entry = g_cc.inst->connections->head;
while (entry) { conn_count++; entry = entry->next; }
off += snprintf(buf + off, sizeof(buf) - off, "=== Connections (%d) ===\n", conn_count);
entry = g_cc.inst->connections->head;
while (entry) {
struct conn_queue_entry* ce = (struct conn_queue_entry*)entry->data;
if (!ce || !ce->conn) { entry = entry->next; continue; }
uint64_t pid = ce->peer_node_id;
struct ETCP_CONN* conn = ce->conn;
uint8_t state = 0, ctype = 0;
conn_mgr_get_status(g_cc.inst->conn_mgr, pid, &state, &ctype);
off += snprintf(buf + off, sizeof(buf) - off,
"Peer 0x%016llx links_up=%d initialized=%d\n",
(unsigned long long)pid, conn->links_up, conn->initialized);
off += snprintf(buf + off, sizeof(buf) - off, " State: %s Type: %s\n",
conn_state_str(state), conn_type_str(ctype));
int link_idx = 0;
struct ETCP_LINK* link = conn->links;
while (link) {
off += snprintf(buf + off, sizeof(buf) - off,
" Link #%d: local_id=%d remote_id=%d status=%s recv=%s remote=%s NAT=%s",
link_idx, link->local_link_id, link->remote_link_id,
link->link_status ? "UP" : "DOWN",
link->recv_keepalive ? "UP" : "DOWN",
link->remote_keepalive ? "UP" : "DOWN",
nat_type_str(link->nat_type));
if (link->rtt_last > 0)
off += snprintf(buf + off, sizeof(buf) - off, " rtt=%ums", link->rtt_last);
if (link->bandwidth > 0)
off += snprintf(buf + off, sizeof(buf) - off, " bw=%uKbps", link->bandwidth);
off += snprintf(buf + off, sizeof(buf) - off, "\n");
link = link->next; link_idx++;
}
entry = entry->next;
}
gui_bridge_post(GUI_EVT_STATUS_REFRESH, (const uint8_t*)buf, off);
}
+3
View File
@@ -92,4 +92,7 @@ void chat_core_update_my_name_trampoline(void* arg);
/* Трамплин для gui_bridge_post_uasync (GUI → uasync) */
void chat_core_submit_trampoline(void* arg);
/* Сбор статуса (NTP + connections) и отправка в GUI */
void chat_core_collect_status_trampoline(void* arg);
#endif /* CHAT_CORE_H */
+5
View File
@@ -21,6 +21,7 @@ struct UASYNC;
#define GUI_EVT_AUTO_CONNECT_STATUS 7 /* data: [status:1][total_tried:2][node_count:2][success:2] */
#define GUI_EVT_CHANNEL_PEERS_ONLINE 8 /* data: [ch_id_len:1][ch_id:var][online_count:2] */
#define GUI_EVT_DB_READY 9 /* data: none — DB is open, tables created */
#define GUI_EVT_STATUS_REFRESH 10 /* data: status text (null-terminated string) */
/* ── API ── */
@@ -72,6 +73,10 @@ void gui_bridge_set_channel_peers_online_cb(gui_channel_peers_online_fn cb);
typedef void (*gui_db_ready_fn)(void);
void gui_bridge_set_db_ready_cb(gui_db_ready_fn cb);
/* Callback для обновления статуса (NTP + connections) — text без \0, вызывается из GUI-потока */
typedef void (*gui_status_refresh_fn)(const char* text, int len);
void gui_bridge_set_status_refresh_cb(gui_status_refresh_fn cb);
#ifdef __cplusplus
}
#endif
@@ -32,6 +32,7 @@ static gui_my_node_id_fn g_my_node_id_cb = nullptr;
static gui_auto_connect_status_fn g_auto_connect_status_cb = nullptr;
static gui_channel_peers_online_fn g_channel_peers_online_cb = nullptr;
static gui_db_ready_fn g_db_ready_cb = nullptr;
static gui_status_refresh_fn g_status_refresh_cb = nullptr;
static struct UASYNC* g_ua = nullptr;
/* ── GuiBridgeReceiver implementation ── */
@@ -126,6 +127,9 @@ void GuiBridgeReceiver::processPost(int eventType, QByteArray data) {
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "gui_bridge: DB_READY");
if (g_db_ready_cb) g_db_ready_cb();
break;
case GUI_EVT_STATUS_REFRESH:
if (g_status_refresh_cb) g_status_refresh_cb((const char*)d, dlen);
break;
default:
DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "gui_bridge: unknown event type %d", eventType);
break;
@@ -204,6 +208,10 @@ void gui_bridge_set_db_ready_cb(gui_db_ready_fn cb) {
g_db_ready_cb = cb;
}
void gui_bridge_set_status_refresh_cb(gui_status_refresh_fn cb) {
g_status_refresh_cb = cb;
}
} /* extern "C" */
#include "gui_bridge_impl.moc"