Our Info panel, hole punch race fix, NAT profiles in relay introduction

- Network Diagnostics: "Our Info" button shows addresses with NAT status,
  device role, UPnP, HTTP capability. Addresses stacked for mobile.
- Hole punch race: re-check for existing connection before and after relay
  introduction to avoid wasting minutes on redundant punch attempts.
- Relay introduction now carries requester/target NAT mapping+filtering
  so hole punch strategy uses fresh profiles instead of stale stored ones.
  Critical for phones that switch between WiFi/cellular/VPN.
- STUN fix: filter DNS results to IPv4 (was resolving to IPv6 first on
  dual-stack, causing silent send failure and "NAT unknown").
- Welcome screen: Ready button with loading bar for instant feed access.
- LAN addresses show just "LAN" (no misleading punchability label).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Scott Reimers 2026-04-05 17:57:41 -04:00
parent e354ccc388
commit be253e8001
7 changed files with 266 additions and 9 deletions

6
Cargo.lock generated
View file

@ -2732,7 +2732,7 @@ checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
[[package]] [[package]]
name = "itsgoin-cli" name = "itsgoin-cli"
version = "0.3.0" version = "0.5.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"hex", "hex",
@ -2744,7 +2744,7 @@ dependencies = [
[[package]] [[package]]
name = "itsgoin-core" name = "itsgoin-core"
version = "0.3.0" version = "0.5.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"base64 0.22.1", "base64 0.22.1",
@ -2767,7 +2767,7 @@ dependencies = [
[[package]] [[package]]
name = "itsgoin-desktop" name = "itsgoin-desktop"
version = "0.4.4" version = "0.5.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"base64 0.22.1", "base64 0.22.1",

View file

@ -3868,12 +3868,15 @@ impl ConnectionManager {
} }
} }
let our_profile = self.our_nat_profile();
let payload = RelayIntroducePayload { let payload = RelayIntroducePayload {
intro_id, intro_id,
target: *target, target: *target,
requester: self.our_node_id, requester: self.our_node_id,
requester_addresses: our_addrs, requester_addresses: our_addrs,
ttl, ttl,
nat_mapping: Some(our_profile.mapping.to_string()),
nat_filtering: Some(our_profile.filtering.to_string()),
}; };
let (mut send, mut recv) = pc.connection.open_bi().await?; let (mut send, mut recv) = pc.connection.open_bi().await?;
@ -3909,6 +3912,8 @@ impl ConnectionManager {
target_addresses: vec![], target_addresses: vec![],
relay_available: false, relay_available: false,
reject_reason: Some("duplicate intro".to_string()), reject_reason: Some("duplicate intro".to_string()),
nat_mapping: None,
nat_filtering: None,
}; };
write_typed_message(&mut send, MessageType::RelayIntroduceResult, &result).await?; write_typed_message(&mut send, MessageType::RelayIntroduceResult, &result).await?;
send.finish()?; send.finish()?;
@ -3932,12 +3937,15 @@ impl ConnectionManager {
} }
} }
let our_profile = self.our_nat_profile();
let result = RelayIntroduceResultPayload { let result = RelayIntroduceResultPayload {
intro_id: payload.intro_id, intro_id: payload.intro_id,
accepted: true, accepted: true,
target_addresses: our_addrs, target_addresses: our_addrs,
relay_available: false, relay_available: false,
reject_reason: None, reject_reason: None,
nat_mapping: Some(our_profile.mapping.to_string()),
nat_filtering: Some(our_profile.filtering.to_string()),
}; };
write_typed_message(&mut send, MessageType::RelayIntroduceResult, &result).await?; write_typed_message(&mut send, MessageType::RelayIntroduceResult, &result).await?;
send.finish()?; send.finish()?;
@ -3963,7 +3971,20 @@ impl ConnectionManager {
let our_http_capable = self.http_capable; let our_http_capable = self.http_capable;
let our_http_addr = self.http_addr.clone(); let our_http_addr = self.http_addr.clone();
let our_nat_profile = self.our_nat_profile(); let our_nat_profile = self.our_nat_profile();
let peer_nat_profile = { // Prefer fresh NAT profile from relay payload over stale stored profile
let peer_nat_profile = if payload.nat_mapping.is_some() || payload.nat_filtering.is_some() {
let mapping = payload.nat_mapping.as_deref()
.map(crate::types::NatMapping::from_str_label)
.unwrap_or(crate::types::NatMapping::Unknown);
let filtering = payload.nat_filtering.as_deref()
.map(crate::types::NatFiltering::from_str_label)
.unwrap_or(crate::types::NatFiltering::Unknown);
let fresh = crate::types::NatProfile::new(mapping, filtering);
// Update storage with fresh profile
let s = self.storage.get().await;
let _ = s.set_peer_nat_profile(&requester, &fresh);
fresh
} else {
let s = self.storage.get().await; let s = self.storage.get().await;
s.get_peer_nat_profile(&requester) s.get_peer_nat_profile(&requester)
}; };
@ -4081,6 +4102,7 @@ impl ConnectionManager {
target_addresses: vec![], target_addresses: vec![],
relay_available: false, relay_available: false,
reject_reason: Some(format!("relay forward failed: {}", e)), reject_reason: Some(format!("relay forward failed: {}", e)),
nat_mapping: None, nat_filtering: None,
}; };
write_typed_message(&mut send, MessageType::RelayIntroduceResult, &result).await?; write_typed_message(&mut send, MessageType::RelayIntroduceResult, &result).await?;
send.finish()?; send.finish()?;
@ -4155,6 +4177,7 @@ impl ConnectionManager {
target_addresses: vec![], target_addresses: vec![],
relay_available: false, relay_available: false,
reject_reason: Some(format!("relay forward to session failed: {}", e)), reject_reason: Some(format!("relay forward to session failed: {}", e)),
nat_mapping: None, nat_filtering: None,
}; };
write_typed_message(&mut send, MessageType::RelayIntroduceResult, &result).await?; write_typed_message(&mut send, MessageType::RelayIntroduceResult, &result).await?;
send.finish()?; send.finish()?;
@ -4194,6 +4217,8 @@ impl ConnectionManager {
requester: payload.requester, requester: payload.requester,
requester_addresses: req_addrs, requester_addresses: req_addrs,
ttl: payload.ttl - 1, ttl: payload.ttl - 1,
nat_mapping: payload.nat_mapping.clone(),
nat_filtering: payload.nat_filtering.clone(),
}; };
let forward_result = async { let forward_result = async {
@ -4246,6 +4271,7 @@ impl ConnectionManager {
target_addresses: vec![], target_addresses: vec![],
relay_available: false, relay_available: false,
reject_reason: Some("target not reachable through relay".to_string()), reject_reason: Some("target not reachable through relay".to_string()),
nat_mapping: None, nat_filtering: None,
}; };
write_typed_message(&mut send, MessageType::RelayIntroduceResult, &result).await?; write_typed_message(&mut send, MessageType::RelayIntroduceResult, &result).await?;
send.finish()?; send.finish()?;
@ -4273,6 +4299,7 @@ impl ConnectionManager {
target_addresses: vec![], target_addresses: vec![],
relay_available: false, relay_available: false,
reject_reason: Some("relay at capacity".to_string()), reject_reason: Some("relay at capacity".to_string()),
nat_mapping: None, nat_filtering: None,
}; };
write_typed_message(&mut requester_send, MessageType::RelayIntroduceResult, &result).await?; write_typed_message(&mut requester_send, MessageType::RelayIntroduceResult, &result).await?;
requester_send.finish()?; requester_send.finish()?;
@ -4293,6 +4320,7 @@ impl ConnectionManager {
target_addresses: vec![], target_addresses: vec![],
relay_available: false, relay_available: false,
reject_reason: Some("target not connected to relay".to_string()), reject_reason: Some("target not connected to relay".to_string()),
nat_mapping: None, nat_filtering: None,
}; };
write_typed_message(&mut requester_send, MessageType::RelayIntroduceResult, &result).await?; write_typed_message(&mut requester_send, MessageType::RelayIntroduceResult, &result).await?;
requester_send.finish()?; requester_send.finish()?;
@ -5811,6 +5839,7 @@ impl ConnectionManager {
let result = RelayIntroduceResultPayload { let result = RelayIntroduceResultPayload {
intro_id: payload.intro_id, accepted: false, target_addresses: vec![], intro_id: payload.intro_id, accepted: false, target_addresses: vec![],
relay_available: false, reject_reason: Some("duplicate intro".to_string()), relay_available: false, reject_reason: Some("duplicate intro".to_string()),
nat_mapping: None, nat_filtering: None,
}; };
write_typed_message(&mut send, MessageType::RelayIntroduceResult, &result).await?; write_typed_message(&mut send, MessageType::RelayIntroduceResult, &result).await?;
send.finish()?; send.finish()?;
@ -5836,7 +5865,22 @@ impl ConnectionManager {
let our_http_capable = cm.http_capable; let our_http_capable = cm.http_capable;
let our_http_addr = cm.http_addr.clone(); let our_http_addr = cm.http_addr.clone();
let our_nat_profile = cm.our_nat_profile(); let our_nat_profile = cm.our_nat_profile();
let peer_nat_profile = { let s = cm.storage.get().await; s.get_peer_nat_profile(&payload.requester) }; // Prefer fresh NAT profile from relay payload over stale stored profile
let peer_nat_profile = if payload.nat_mapping.is_some() || payload.nat_filtering.is_some() {
let mapping = payload.nat_mapping.as_deref()
.map(crate::types::NatMapping::from_str_label)
.unwrap_or(crate::types::NatMapping::Unknown);
let filtering = payload.nat_filtering.as_deref()
.map(crate::types::NatFiltering::from_str_label)
.unwrap_or(crate::types::NatFiltering::Unknown);
let fresh = crate::types::NatProfile::new(mapping, filtering);
let s = cm.storage.get().await;
let _ = s.set_peer_nat_profile(&payload.requester, &fresh);
fresh
} else {
let s = cm.storage.get().await;
s.get_peer_nat_profile(&payload.requester)
};
Some(RelayGathered::WeAreTarget { our_addrs, endpoint, storage, our_node_id, our_nat_type, our_http_capable, our_http_addr, our_nat_profile, peer_nat_profile }) Some(RelayGathered::WeAreTarget { our_addrs, endpoint, storage, our_node_id, our_nat_type, our_http_capable, our_http_addr, our_nat_profile, peer_nat_profile })
} else { } else {
// We are relay — gather target connection, requester observed addr, etc. // We are relay — gather target connection, requester observed addr, etc.
@ -5866,6 +5910,8 @@ impl ConnectionManager {
let result = RelayIntroduceResultPayload { let result = RelayIntroduceResultPayload {
intro_id: payload.intro_id, accepted: true, target_addresses: our_addrs, intro_id: payload.intro_id, accepted: true, target_addresses: our_addrs,
relay_available: false, reject_reason: None, relay_available: false, reject_reason: None,
nat_mapping: Some(our_nat_profile.mapping.to_string()),
nat_filtering: Some(our_nat_profile.filtering.to_string()),
}; };
write_typed_message(&mut send, MessageType::RelayIntroduceResult, &result).await?; write_typed_message(&mut send, MessageType::RelayIntroduceResult, &result).await?;
send.finish()?; send.finish()?;
@ -5954,6 +6000,7 @@ impl ConnectionManager {
let result = RelayIntroduceResultPayload { let result = RelayIntroduceResultPayload {
intro_id: payload.intro_id, accepted: false, target_addresses: vec![], intro_id: payload.intro_id, accepted: false, target_addresses: vec![],
relay_available: false, reject_reason: Some(format!("relay forward failed: {}", e_msg)), relay_available: false, reject_reason: Some(format!("relay forward failed: {}", e_msg)),
nat_mapping: None, nat_filtering: None,
}; };
write_typed_message(&mut send, MessageType::RelayIntroduceResult, &result).await?; write_typed_message(&mut send, MessageType::RelayIntroduceResult, &result).await?;
send.finish()?; send.finish()?;

View file

@ -349,6 +349,11 @@ impl Network {
self.device_role self.device_role
} }
/// Get the explicit bind address (from --bind flag), if any.
pub fn bind_addr(&self) -> Option<std::net::SocketAddr> {
self.bind_addr
}
/// Whether this node can serve HTTP (has TCP reachability). /// Whether this node can serve HTTP (has TCP reachability).
pub fn is_http_capable(&self) -> bool { pub fn is_http_capable(&self) -> bool {
self.has_upnp_tcp || self.has_public_v6 || self.bind_addr.is_some() self.has_upnp_tcp || self.has_public_v6 || self.bind_addr.is_some()
@ -1952,7 +1957,13 @@ impl Network {
} }
} }
// 4. Try relay introduction + hole punch (no lock during I/O) // 4. Re-check connection — peer may have connected to us while we were trying direct
if let Some(conn) = self.conn_handle.get_any_connection(peer_id).await {
debug!(peer = hex::encode(peer_id), "Peer connected to us while we were trying — using existing connection");
return Ok(conn);
}
// 5. Try relay introduction + hole punch (no lock during I/O)
let relay_candidates = self.conn_handle.find_relays_for(peer_id).await; let relay_candidates = self.conn_handle.find_relays_for(peer_id).await;
if relay_candidates.is_empty() { if relay_candidates.is_empty() {
@ -1975,10 +1986,28 @@ impl Network {
self.send_relay_introduce_standalone(relay_peer, peer_id, *ttl), self.send_relay_introduce_standalone(relay_peer, peer_id, *ttl),
).await; ).await;
// Re-check: peer may have connected while relay intro was in flight
if let Some(conn) = self.conn_handle.get_any_connection(peer_id).await {
debug!(peer = hex::encode(peer_id), "Peer connected during relay intro — using existing");
return Ok(conn);
}
match intro_result { match intro_result {
Ok(Ok(result)) if result.accepted => { Ok(Ok(ref result)) if result.accepted => {
let our_profile = self.conn_handle.our_nat_profile().await; let our_profile = self.conn_handle.our_nat_profile().await;
let peer_profile = { // Prefer fresh NAT profile from relay response over stale stored profile
let peer_profile = if result.nat_mapping.is_some() || result.nat_filtering.is_some() {
let mapping = result.nat_mapping.as_deref()
.map(crate::types::NatMapping::from_str_label)
.unwrap_or(crate::types::NatMapping::Unknown);
let filtering = result.nat_filtering.as_deref()
.map(crate::types::NatFiltering::from_str_label)
.unwrap_or(crate::types::NatFiltering::Unknown);
let fresh = crate::types::NatProfile::new(mapping, filtering);
let s = self.storage.get().await;
let _ = s.set_peer_nat_profile(peer_id, &fresh);
fresh
} else {
let s = self.storage.get().await; let s = self.storage.get().await;
s.get_peer_nat_profile(peer_id) s.get_peer_nat_profile(peer_id)
}; };
@ -2274,12 +2303,15 @@ impl Network {
} }
} }
let our_profile = self.conn_handle.our_nat_profile().await;
let payload = crate::protocol::RelayIntroducePayload { let payload = crate::protocol::RelayIntroducePayload {
intro_id, intro_id,
target: *target, target: *target,
requester: self.our_node_id, requester: self.our_node_id,
requester_addresses: our_addrs, requester_addresses: our_addrs,
ttl, ttl,
nat_mapping: Some(our_profile.mapping.to_string()),
nat_filtering: Some(our_profile.filtering.to_string()),
}; };
let (mut send, mut recv) = conn.open_bi().await?; let (mut send, mut recv) = conn.open_bi().await?;

View file

@ -461,6 +461,12 @@ pub struct RelayIntroducePayload {
pub requester_addresses: Vec<String>, pub requester_addresses: Vec<String>,
/// Max forwarding hops remaining (0 = relay must know target directly) /// Max forwarding hops remaining (0 = relay must know target directly)
pub ttl: u8, pub ttl: u8,
/// Requester's current NAT mapping type (for hole punch strategy)
#[serde(default, skip_serializing_if = "Option::is_none")]
pub nat_mapping: Option<String>,
/// Requester's current NAT filtering type
#[serde(default, skip_serializing_if = "Option::is_none")]
pub nat_filtering: Option<String>,
} }
/// Target's response to a relay introduction (bi-stream response) /// Target's response to a relay introduction (bi-stream response)
@ -472,6 +478,12 @@ pub struct RelayIntroduceResultPayload {
/// Relay is willing to serve as stream relay fallback /// Relay is willing to serve as stream relay fallback
pub relay_available: bool, pub relay_available: bool,
pub reject_reason: Option<String>, pub reject_reason: Option<String>,
/// Target's current NAT mapping type (for hole punch strategy)
#[serde(default, skip_serializing_if = "Option::is_none")]
pub nat_mapping: Option<String>,
/// Target's current NAT filtering type
#[serde(default, skip_serializing_if = "Option::is_none")]
pub nat_filtering: Option<String>,
} }
/// Open a relay pipe — intermediary splices two bi-streams (bi-stream) /// Open a relay pipe — intermediary splices two bi-streams (bi-stream)
@ -858,6 +870,8 @@ mod tests {
requester: [2u8; 32], requester: [2u8; 32],
requester_addresses: vec!["10.0.0.2:4433".to_string()], requester_addresses: vec!["10.0.0.2:4433".to_string()],
ttl: 1, ttl: 1,
nat_mapping: Some("eim".to_string()),
nat_filtering: Some("open".to_string()),
}; };
let json = serde_json::to_string(&payload).unwrap(); let json = serde_json::to_string(&payload).unwrap();
let decoded: RelayIntroducePayload = serde_json::from_str(&json).unwrap(); let decoded: RelayIntroducePayload = serde_json::from_str(&json).unwrap();
@ -876,6 +890,8 @@ mod tests {
target_addresses: vec!["10.0.0.1:4433".to_string(), "192.168.1.1:4433".to_string()], target_addresses: vec!["10.0.0.1:4433".to_string(), "192.168.1.1:4433".to_string()],
relay_available: true, relay_available: true,
reject_reason: None, reject_reason: None,
nat_mapping: Some("eim".to_string()),
nat_filtering: Some("open".to_string()),
}; };
let json = serde_json::to_string(&payload).unwrap(); let json = serde_json::to_string(&payload).unwrap();
let decoded: RelayIntroduceResultPayload = serde_json::from_str(&json).unwrap(); let decoded: RelayIntroduceResultPayload = serde_json::from_str(&json).unwrap();
@ -892,6 +908,8 @@ mod tests {
target_addresses: vec![], target_addresses: vec![],
relay_available: false, relay_available: false,
reject_reason: Some("target not reachable".to_string()), reject_reason: Some("target not reachable".to_string()),
nat_mapping: None,
nat_filtering: None,
}; };
let json2 = serde_json::to_string(&rejected).unwrap(); let json2 = serde_json::to_string(&rejected).unwrap();
let decoded2: RelayIntroduceResultPayload = serde_json::from_str(&json2).unwrap(); let decoded2: RelayIntroduceResultPayload = serde_json::from_str(&json2).unwrap();

View file

@ -103,8 +103,9 @@ fn parse_xor_mapped_address(resp: &[u8], txn_id: &[u8; 12]) -> Option<SocketAddr
/// Query a single STUN server and return the mapped address. /// Query a single STUN server and return the mapped address.
async fn stun_query(sock: &UdpSocket, server: &str) -> Option<SocketAddr> { async fn stun_query(sock: &UdpSocket, server: &str) -> Option<SocketAddr> {
use std::net::ToSocketAddrs; use std::net::ToSocketAddrs;
// Filter for IPv4 since our socket is bound to 0.0.0.0 (IPv4)
let server_addr = match server.to_socket_addrs() { let server_addr = match server.to_socket_addrs() {
Ok(mut addrs) => addrs.next()?, Ok(addrs) => addrs.filter(|a| a.is_ipv4()).next()?,
Err(e) => { Err(e) => {
debug!(server, error = %e, "STUN DNS resolution failed"); debug!(server, error = %e, "STUN DNS resolution failed");
return None; return None;

View file

@ -1623,6 +1623,120 @@ async fn get_network_summary(state: State<'_, AppNode>) -> Result<NetworkSummary
}) })
} }
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct OurInfoDto {
node_id: String,
addresses: Vec<AddressInfoDto>,
nat_type: String,
device_role: String,
upnp: bool,
http_capable: bool,
http_addr: Option<String>,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct AddressInfoDto {
addr: String,
family: String, // "IPv4" or "IPv6"
status: String, // "Public", "NAT (easy)", "NAT (hard)", "UPnP", "LAN", "Server"
}
#[tauri::command]
async fn get_our_info(state: State<'_, AppNode>) -> Result<OurInfoDto, String> {
let node = get_node(&state).await;
let net = &node.network;
let nat_type = net.conn_handle().nat_type().await;
let has_upnp = net.has_upnp();
let _has_public_v6 = net.has_public_v6();
let bind_addr = net.bind_addr();
let mut addresses = Vec::new();
// Collect bound socket addresses (these are our local interfaces)
let bound: std::collections::HashSet<String> = net.bound_sockets()
.iter()
.map(|s| s.to_string())
.collect();
// Gather bound (local) addresses with classification
for sock in net.bound_sockets() {
if sock.ip().is_loopback() || sock.ip().is_unspecified() { continue; }
let family = if sock.ip().is_ipv4() { "IPv4" } else { "IPv6" };
let status = classify_addr(&sock, &nat_type, has_upnp, bind_addr.is_some(), false);
addresses.push(AddressInfoDto {
addr: sock.to_string(),
family: family.to_string(),
status,
});
}
// Add UPnP external address if different from bound
if let Some(ref mapping) = net.upnp_mapping() {
let ext = mapping.external_addr;
if !addresses.iter().any(|a| a.addr == ext.to_string()) {
addresses.insert(0, AddressInfoDto {
addr: ext.to_string(),
family: if ext.ip().is_ipv4() { "IPv4" } else { "IPv6" }.to_string(),
status: "UPnP external".to_string(),
});
}
}
// Add iroh-discovered addresses not already listed (STUN-observed externals)
for sock in net.endpoint_addr().ip_addrs() {
if sock.ip().is_loopback() || sock.ip().is_unspecified() { continue; }
let s = sock.to_string();
if addresses.iter().any(|a| a.addr == s) { continue; }
let family = if sock.ip().is_ipv4() { "IPv4" } else { "IPv6" };
let is_observed = !bound.contains(&s);
let status = classify_addr(sock, &nat_type, has_upnp, bind_addr.is_some(), is_observed);
addresses.push(AddressInfoDto {
addr: s,
family: family.to_string(),
status,
});
}
Ok(OurInfoDto {
node_id: hex::encode(net.node_id_bytes()),
addresses,
nat_type: nat_type.to_string(),
device_role: format!("{:?}", net.device_role()),
upnp: has_upnp,
http_capable: net.is_http_capable(),
http_addr: net.http_addr(),
})
}
fn classify_addr(sock: &std::net::SocketAddr, nat_type: &itsgoin_core::types::NatType, has_upnp: bool, is_server: bool, is_observed: bool) -> String {
use std::net::IpAddr;
let ip = sock.ip();
let is_v6 = ip.is_ipv6();
let is_pub = match ip {
IpAddr::V4(v4) => !v4.is_private() && !v4.is_loopback() && !v4.is_link_local(),
IpAddr::V6(v6) => !v6.is_loopback() && {
let seg = v6.segments();
seg[0] & 0xfe00 != 0xfe00 && seg[0] & 0xffc0 != 0xfe80
},
};
if is_server { return "Server".to_string(); }
if is_pub {
if is_observed && !is_v6 {
return match nat_type {
itsgoin_core::types::NatType::Public |
itsgoin_core::types::NatType::Easy => "External · easy punch".to_string(),
itsgoin_core::types::NatType::Hard => "External · hard punch (scan)".to_string(),
itsgoin_core::types::NatType::Unknown => "External".to_string(),
};
}
return "Public".to_string();
}
if is_v6 { return "Link-local".to_string(); }
"LAN".to_string()
}
#[derive(Serialize)] #[derive(Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
struct ActivityEventDto { struct ActivityEventDto {
@ -2387,6 +2501,7 @@ pub fn run() {
sync_all, sync_all,
sync_from_peer, sync_from_peer,
get_network_summary, get_network_summary,
get_our_info,
get_activity_log, get_activity_log,
trigger_rebalance, trigger_rebalance,
request_referrals, request_referrals,

View file

@ -2986,9 +2986,14 @@ function openDiagnostics() {
<button id="request-referrals-btn" class="btn btn-ghost btn-sm">Request Referrals</button> <button id="request-referrals-btn" class="btn btn-ghost btn-sm">Request Referrals</button>
</div> </div>
<div style="display:flex;gap:0.5rem;margin-top:0.5rem;flex-wrap:wrap;justify-content:center"> <div style="display:flex;gap:0.5rem;margin-top:0.5rem;flex-wrap:wrap;justify-content:center">
<button id="show-ourinfo-btn" class="btn btn-ghost btn-sm">Our Info</button>
<button id="show-connections-btn" class="btn btn-ghost btn-sm">Show Connections</button> <button id="show-connections-btn" class="btn btn-ghost btn-sm">Show Connections</button>
<button id="show-anchors-btn" class="btn btn-ghost btn-sm">Stored Anchors</button> <button id="show-anchors-btn" class="btn btn-ghost btn-sm">Stored Anchors</button>
</div> </div>
<div id="ourinfo-section" class="hidden">
<h4 class="subsection-title">Our Info</h4>
<div id="ourinfo-content"></div>
</div>
<div id="connections-section" class="hidden"> <div id="connections-section" class="hidden">
<h4 class="subsection-title">Mesh &amp; Session Connections</h4> <h4 class="subsection-title">Mesh &amp; Session Connections</h4>
<div id="connections-list"></div> <div id="connections-list"></div>
@ -3006,6 +3011,45 @@ function openDiagnostics() {
networkSummaryEl = $('#network-summary'); networkSummaryEl = $('#network-summary');
connectionsList = $('#connections-list'); connectionsList = $('#connections-list');
peersList = null; // Known peers removed peersList = null; // Known peers removed
// Wire Our Info toggle
$('#show-ourinfo-btn').addEventListener('click', async () => {
const section = $('#ourinfo-section');
const btn = $('#show-ourinfo-btn');
if (section.classList.contains('hidden')) {
section.classList.remove('hidden');
btn.textContent = 'Hide Our Info';
try {
const info = await invoke('get_our_info');
const httpVal = info.httpAddr || 'No';
let html = `<div class="diag-grid" style="margin-bottom:0.5rem">
<div class="diag-item"><span class="diag-label">NAT Type</span><span class="diag-value" style="font-size:0.85rem">${info.natType}</span></div>
<div class="diag-item"><span class="diag-label">Role</span><span class="diag-value" style="font-size:0.85rem">${info.deviceRole}</span></div>
<div class="diag-item"><span class="diag-label">UPnP</span><span class="diag-value" style="font-size:0.85rem">${info.upnp ? 'Yes' : 'No'}</span></div>
</div>
<div style="text-align:center;margin-bottom:0.75rem">
<span style="color:#888;font-size:0.75rem">HTTP</span>
<span style="color:#ccc;font-size:0.8rem;margin-left:0.5rem">${httpVal}</span>
</div>`;
html += '<div style="font-size:0.8rem">';
for (const a of info.addresses) {
const color = a.status.includes('Public') || a.status.includes('punchable') || a.status.includes('Server') ? '#7fdbca' :
a.status.includes('UPnP') || a.status.includes('External') ? '#5b8def' : '#888';
html += `<div style="padding:0.3rem 0;border-bottom:1px solid #1a1a2e">
<div style="color:#ccc;font-family:monospace;font-size:0.7rem;word-break:break-all">${a.addr}</div>
<div style="color:${color};font-size:0.7rem;margin-top:0.1rem">${a.family} · ${a.status}</div>
</div>`;
}
html += '</div>';
html += `<p style="color:#555;font-size:0.7rem;text-align:center;margin-top:0.5rem;word-break:break-all">Node: ${info.nodeId.substring(0, 16)}…</p>`;
$('#ourinfo-content').innerHTML = html;
} catch (e) {
$('#ourinfo-content').innerHTML = `<p class="empty-hint">Failed to load: ${e}</p>`;
}
} else {
section.classList.add('hidden');
btn.textContent = 'Our Info';
}
});
// Wire connections toggle // Wire connections toggle
$('#show-connections-btn').addEventListener('click', () => { $('#show-connections-btn').addEventListener('click', () => {
const section = $('#connections-section'); const section = $('#connections-section');