[feat] display client version and platform

This commit is contained in:
dijunkun committed 2026-08-24 09:46:51 +08:00
1 parent f1807f4c2b
commit 695193ab3c
8 files changed
+119 -3

No files matched your search

+2
View File
@@ -610,6 +610,8 @@ AdminHttpResponse AdminController::HandleOverview(
devices.push_back({{"id", device.device_id},
{"online", device.online},
{"kind", ClientKind(device.device_id)},
{"client_version", device.client_version},
{"client_platform", device.client_platform},
{"updated_at", device.updated_at},
{"last_online_at",
device.online ? 0 : device.updated_at},
+4
View File
@@ -82,11 +82,15 @@
.sessions-table td:nth-child(1), .sessions-table td:nth-child(2) { word-break: break-all; }
.device-id { font-weight: 600; word-break: break-all; }
.subline { display: block; color: #667085; font-size: 12px; margin-top: 3px; }
.client-meta { align-items: center; display: flex; flex-wrap: wrap; gap: 6px; margin-top: 3px; }
.client-meta .subline { margin-top: 0; }
.badge { border-radius: 999px; display: inline-flex; align-items: center; justify-content: center; font-size: 12px; font-weight: 700; line-height: 1; padding: 5px 8px; white-space: nowrap; }
.badge.online { background: #dcfae6; color: #067647; }
.badge.offline { background: #f2f4f7; color: #475467; }
.badge.active { background: #fef0c7; color: #b54708; }
.badge.web { background: #e0f2fe; color: #026aa2; }
.badge.platform { background: #ecfdf3; color: #027a48; }
.badge.version { background: #eef4ff; color: #3538cd; }
.presence-table td:nth-child(2) { display: grid; gap: 4px; justify-items: start; }
.presence-table td:nth-child(2) .badge { min-width: 58px; }
.details-row td { background: #f9fafb; }
+17 -1
View File
@@ -559,6 +559,13 @@
return Number(activeCount) > 0 ? formatDuration(value) : '-';
}
function platformLabel(platform) {
if (platform === 'windows') return 'Windows';
if (platform === 'macos') return 'macOS';
if (platform === 'linux') return 'Linux';
return platform || '';
}
function appendDetailItem(parent, label, value, className, dataset) {
const item = document.createElement('div');
appendText(item, 'span', label);
@@ -590,7 +597,16 @@
const clientCell = document.createElement('td');
labelCell(clientCell, 'Client');
appendText(clientCell, 'div', device.id, 'device-id');
appendText(clientCell, 'span', device.kind === 'web' ? 'web client' : 'PC client', 'subline');
const clientMeta = document.createElement('div');
clientMeta.className = 'client-meta';
appendText(clientMeta, 'span', device.kind === 'web' ? 'web client' : 'PC client', 'subline');
if (device.kind !== 'web' && device.client_platform) {
appendBadge(clientMeta, platformLabel(device.client_platform), 'platform');
}
if (device.kind !== 'web' && device.client_version) {
appendBadge(clientMeta, device.client_version, 'version');
}
clientCell.appendChild(clientMeta);
row.appendChild(clientCell);
const statusCell = document.createElement('td');
+42 -2
View File
@@ -426,6 +426,8 @@ void DeviceDBManager::InitDB() {
EnsureTextColumn(db_, "device_presence", "geo_region");
EnsureTextColumn(db_, "device_presence", "geo_city");
EnsureTextColumn(db_, "device_presence", "geo_location");
EnsureTextColumn(db_, "device_presence", "client_version");
EnsureTextColumn(db_, "device_presence", "client_platform");
const char* sql_presence_backfill =
"UPDATE device_presence SET online_since = updated_at "
@@ -1040,6 +1042,38 @@ bool DeviceDBManager::UpdateDeviceNetworkInfo(
return ok;
}
bool DeviceDBManager::UpdateDeviceClientInfo(
const std::string& device_id, const std::string& client_version,
const std::string& client_platform) {
std::lock_guard<std::recursive_mutex> lock(db_mutex_);
if (db_ == nullptr) {
LOG_ERROR("Database is not initialized in UpdateDeviceClientInfo.");
return false;
}
if (device_id.empty()) {
return false;
}
const char* sql =
"INSERT INTO device_presence "
"(device_id, online, updated_at, client_version, client_platform) "
"VALUES (?, 0, CAST(strftime('%s','now') AS INTEGER), ?, ?) "
"ON CONFLICT(device_id) DO UPDATE SET "
"client_version=excluded.client_version, "
"client_platform=excluded.client_platform;";
sqlite3_stmt* stmt = nullptr;
if (sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr) != SQLITE_OK) {
return false;
}
sqlite3_bind_text(stmt, 1, device_id.c_str(), -1, SQLITE_TRANSIENT);
sqlite3_bind_text(stmt, 2, client_version.c_str(), -1, SQLITE_TRANSIENT);
sqlite3_bind_text(stmt, 3, client_platform.c_str(), -1, SQLITE_TRANSIENT);
const bool ok = sqlite3_step(stmt) == SQLITE_DONE;
sqlite3_finalize(stmt);
return ok;
}
bool DeviceDBManager::StartRemoteControlSession(
const std::string& transmission_id, const std::string& host_id,
const std::string& guest_id) {
@@ -1668,7 +1702,8 @@ std::vector<OnlineDeviceInfo> DeviceDBManager::ListOnlineDevices(
"FROM remote_control_sessions "
"WHERE normalized_host_id = device_presence.device_id), 0) "
"AS total_controlled_seconds, "
"client_ip, geo_country, geo_region, geo_city, geo_location "
"client_ip, geo_country, geo_region, geo_city, geo_location, "
"client_version, client_platform "
"FROM device_presence "
"WHERE online = 1 "
"AND device_id NOT LIKE 'web-%' "
@@ -1707,6 +1742,8 @@ std::vector<OnlineDeviceInfo> DeviceDBManager::ListOnlineDevices(
info.region = ColumnText(stmt, 10);
info.city = ColumnText(stmt, 11);
info.location = ColumnText(stmt, 12);
info.client_version = ColumnText(stmt, 13);
info.client_platform = ColumnText(stmt, 14);
result.push_back(info);
}
sqlite3_finalize(stmt);
@@ -1771,7 +1808,8 @@ std::vector<OnlineDeviceInfo> DeviceDBManager::ListDevicePresence(
"FROM remote_control_sessions "
"WHERE normalized_host_id = device_presence.device_id), '') "
"AS active_controlled_by, "
"client_ip, geo_country, geo_region, geo_city, geo_location "
"client_ip, geo_country, geo_region, geo_city, geo_location, "
"client_version, client_platform "
"FROM device_presence "
"WHERE " +
DevicePresenceFilterClause(filter, kind);
@@ -1816,6 +1854,8 @@ std::vector<OnlineDeviceInfo> DeviceDBManager::ListDevicePresence(
info.region = ColumnText(stmt, 16);
info.city = ColumnText(stmt, 17);
info.location = ColumnText(stmt, 18);
info.client_version = ColumnText(stmt, 19);
info.client_platform = ColumnText(stmt, 20);
result.push_back(info);
}
sqlite3_finalize(stmt);
@@ -42,6 +42,8 @@ struct OnlineDeviceInfo {
std::string region;
std::string city;
std::string location;
std::string client_version;
std::string client_platform;
};
struct ClientNetworkInfo {
@@ -113,6 +115,9 @@ class DeviceDBManager {
bool SetDeviceOnline(const std::string& device_id, bool online);
bool UpdateDeviceNetworkInfo(const std::string& device_id,
const ClientNetworkInfo& network_info);
bool UpdateDeviceClientInfo(const std::string& device_id,
const std::string& client_version,
const std::string& client_platform);
bool RecordRuntimeHeartbeat();
bool StartRemoteControlSession(const std::string& transmission_id,
const std::string& host_id,
+45
View File
@@ -18,6 +18,11 @@ bool GetStringField(const json& j, const char* key, std::string& value) {
return true;
}
bool ShouldTrackClientInfo(const std::string& user_id) {
return !user_id.empty() && user_id.rfind("web-", 0) != 0 &&
user_id.rfind("C-", 0) != 0;
}
std::string PasswordFingerprint(const std::string& password) {
unsigned char digest[SHA256_DIGEST_LENGTH];
SHA256(reinterpret_cast<const unsigned char*>(password.data()),
@@ -121,6 +126,10 @@ bool SignalNegotiation::login_user(websocketpp::connection_hdl hdl,
transmission_manager_->BindHostToTransmission(ret_host_id, ret_host_id);
if (success) {
if (ShouldTrackClientInfo(ret_host_id) &&
!device_db_manager_->UpdateDeviceClientInfo(ret_host_id, "", "")) {
LOG_WARN("Failed to clear client information for [{}]", ret_host_id);
}
json message = {{"type", "login"},
{"user_id", return_host_id},
{"status", "success"}};
@@ -151,6 +160,42 @@ bool SignalNegotiation::login_user(websocketpp::connection_hdl hdl,
return true;
}
bool SignalNegotiation::client_info(websocketpp::connection_hdl hdl,
const json& j) {
std::string version;
std::string platform;
if (!GetStringField(j, "version", version) || version.empty() ||
version.size() > 64) {
LOG_ERROR("client_info missing or invalid field: version");
return false;
}
if (!GetStringField(j, "platform", platform) || platform.empty() ||
platform.size() > 32) {
LOG_ERROR("client_info missing or invalid field: platform");
return false;
}
const std::string user_id = transmission_manager_->GetUserId(hdl);
if (user_id.empty()) {
LOG_WARN("Ignore client_info from unauthenticated connection");
return false;
}
if (!ShouldTrackClientInfo(user_id)) {
return true;
}
if (!device_db_manager_->UpdateDeviceClientInfo(user_id, version,
platform)) {
LOG_ERROR("Failed to store client information for [{}]", user_id);
return false;
}
LOG_INFO("Client [{}] reports version [{}] on [{}]", user_id, version,
platform);
return true;
}
bool SignalNegotiation::leave_transmission(websocketpp::connection_hdl hdl,
const json& j) {
std::string transmission_id;
+1
View File
@@ -42,6 +42,7 @@ class SignalNegotiation {
bool new_candidate_mid(websocketpp::connection_hdl hdl, const json& j);
bool change_password(websocketpp::connection_hdl hdl, const json& j);
bool turn_credentials(websocketpp::connection_hdl hdl, const json& j);
bool client_info(websocketpp::connection_hdl hdl, const json& j);
void OnWebClientDisconnect(const std::string& user_id);
private:
+3
View File
@@ -852,6 +852,9 @@ void SignalServer::OnMessage(websocketpp::connection_hdl hdl,
case "turn_credentials"_H:
signal_negotiation_->turn_credentials(hdl, j);
break;
case "client_info"_H:
signal_negotiation_->client_info(hdl, j);
break;
case "recent_connections_presence"_H: {
std::string user_id;
if (!j.contains("user_id") || !j["user_id"].is_string()) {