From 76ac1252d269e16306450d2ee4211683ed508893 Mon Sep 17 00:00:00 2001 From: Johnathan Corgan Date: Mon, 9 Mar 2026 01:25:34 +0000 Subject: [PATCH] Enhance fipstop peers display with transport, direction, and tree roles Add full npub column, transport type/address, direction (in/out), goodput, and LQI-ascending sort to peers table. Colorize spanning tree parent (magenta) and children (cyan). Add tree relationship and transport info to show_peers JSON output. --- src/bin/fipstop/ui/peers.rs | 128 +++++++++++++++++++++++++----------- src/control/queries.rs | 22 +++++++ 2 files changed, 111 insertions(+), 39 deletions(-) diff --git a/src/bin/fipstop/ui/peers.rs b/src/bin/fipstop/ui/peers.rs index 5da31893..5ec7a1cc 100644 --- a/src/bin/fipstop/ui/peers.rs +++ b/src/bin/fipstop/ui/peers.rs @@ -1,7 +1,7 @@ use ratatui::Frame; use ratatui::layout::{Constraint, Layout, Rect}; use ratatui::style::{Color, Modifier, Style}; -use ratatui::text::{Line, Span}; +use ratatui::text::Line; use ratatui::widgets::{Block, Borders, Cell, Paragraph, Row, Scrollbar, ScrollbarOrientation, ScrollbarState, Table}; use crate::app::{App, Tab}; @@ -9,7 +9,7 @@ use crate::app::{App, Tab}; use super::helpers; pub fn draw(frame: &mut Frame, app: &mut App, area: Rect) { - let peers = get_peers(app); + let peers = get_peers_sorted(app); let row_count = peers.len(); if app.detail_view.is_some() { @@ -27,24 +27,39 @@ pub fn draw(frame: &mut Frame, app: &mut App, area: Rect) { } } -fn get_peers(app: &App) -> Vec { - app.data +/// Get peers sorted by LQI ascending (best first). Peers without LQI sort last. +fn get_peers_sorted(app: &App) -> Vec { + let mut peers = app.data .get(&Tab::Peers) .and_then(|v| v.get("peers")) .and_then(|v| v.as_array()) .cloned() - .unwrap_or_default() + .unwrap_or_default(); + + peers.sort_by(|a, b| { + let lqi_a = a.get("mmp").and_then(|m| m.get("lqi")).and_then(|v| v.as_f64()); + let lqi_b = b.get("mmp").and_then(|m| m.get("lqi")).and_then(|v| v.as_f64()); + match (lqi_a, lqi_b) { + (Some(a), Some(b)) => a.partial_cmp(&b).unwrap_or(std::cmp::Ordering::Equal), + (Some(_), None) => std::cmp::Ordering::Less, + (None, Some(_)) => std::cmp::Ordering::Greater, + (None, None) => std::cmp::Ordering::Equal, + } + }); + + peers } fn draw_table(frame: &mut Frame, app: &mut App, area: Rect, peers: &[serde_json::Value], row_count: usize) { let header = Row::new(vec![ Cell::from("Name"), - Cell::from("Address"), - Cell::from("Conn"), - Cell::from("Depth"), + Cell::from("Npub"), + Cell::from("Transport"), + Cell::from("Dir"), Cell::from("SRTT"), Cell::from("Loss"), Cell::from("LQI"), + Cell::from("Goodput"), Cell::from("Pkts Tx"), Cell::from("Pkts Rx"), ]) @@ -58,43 +73,73 @@ fn draw_table(frame: &mut Frame, app: &mut App, area: Rect, peers: &[serde_json: .iter() .map(|peer| { let name = helpers::str_field(peer, "display_name"); - let addr = helpers::truncate_hex(helpers::str_field(peer, "ipv6_addr"), 10); - let conn = helpers::str_field(peer, "connectivity"); - let depth = peer - .get("tree_depth") - .and_then(|v| v.as_u64()) - .map(|n| n.to_string()) - .unwrap_or_else(|| "-".into()); + let npub = helpers::str_field(peer, "npub"); + let is_parent = peer.get("is_parent").and_then(|v| v.as_bool()).unwrap_or(false); + let is_child = peer.get("is_child").and_then(|v| v.as_bool()).unwrap_or(false); + + // Transport: "type addr" (e.g., "udp 1.2.3.4:2121") + let transport = { + let t_type = peer.get("transport_type").and_then(|v| v.as_str()).unwrap_or(""); + let t_addr = peer.get("transport_addr").and_then(|v| v.as_str()).unwrap_or(""); + if t_type.is_empty() && t_addr.is_empty() { + "-".to_string() + } else if t_type.is_empty() { + t_addr.to_string() + } else if t_addr.is_empty() { + t_type.to_string() + } else { + format!("{t_type}/{t_addr}") + } + }; + + let dir = peer.get("direction").and_then(|v| v.as_str()).map(|d| match d { + "inbound" => "in", + "outbound" => "out", + other => other, + }).unwrap_or("-"); let srtt = helpers::nested_f64(peer, "mmp", "srtt_ms", 1); let loss = helpers::nested_f64_prefer(peer, "mmp", "smoothed_loss", "loss_rate", 3); let lqi = helpers::nested_f64(peer, "mmp", "lqi", 2); + let goodput = helpers::nested_throughput(peer, "mmp", "goodput_bps"); let pkts_tx = helpers::nested_u64(peer, "stats", "packets_sent"); let pkts_rx = helpers::nested_u64(peer, "stats", "packets_recv"); + // Tree role colorization + let row_style = if is_parent { + Style::default().fg(Color::Magenta) + } else if is_child { + Style::default().fg(Color::Cyan) + } else { + Style::default() + }; + Row::new(vec![ Cell::from(name.to_string()), - Cell::from(addr), - Cell::from(connectivity_styled(conn)), - Cell::from(depth), + Cell::from(npub.to_string()), + Cell::from(transport), + Cell::from(dir.to_string()), Cell::from(srtt), Cell::from(loss), Cell::from(lqi), + Cell::from(goodput), Cell::from(pkts_tx), Cell::from(pkts_rx), ]) + .style(row_style) }) .collect(); let widths = [ - Constraint::Min(12), - Constraint::Length(13), - Constraint::Length(8), - Constraint::Length(5), - Constraint::Length(8), - Constraint::Length(7), - Constraint::Length(6), - Constraint::Length(9), - Constraint::Length(9), + Constraint::Min(12), // Name + Constraint::Length(67), // Npub (full bech32) + Constraint::Min(20), // Transport + Constraint::Length(4), // Dir + Constraint::Length(8), // SRTT + Constraint::Length(7), // Loss + Constraint::Length(6), // LQI + Constraint::Length(10), // Goodput + Constraint::Length(9), // Pkts Tx + Constraint::Length(9), // Pkts Rx ]; let table = Table::new(rows, widths) @@ -146,6 +191,16 @@ fn draw_detail(frame: &mut Frame, app: &App, area: Rect, peers: &[serde_json::Va let has_tree = peer.get("has_tree_position").and_then(|v| v.as_bool()).unwrap_or(false); let has_bloom = peer.get("has_bloom_filter").and_then(|v| v.as_bool()).unwrap_or(false); + let is_parent = peer.get("is_parent").and_then(|v| v.as_bool()).unwrap_or(false); + let is_child = peer.get("is_child").and_then(|v| v.as_bool()).unwrap_or(false); + + let tree_role = if is_parent { + "parent" + } else if is_child { + "child" + } else { + "peer" + }; let mut lines: Vec = vec![ // Identity @@ -154,20 +209,24 @@ fn draw_detail(frame: &mut Frame, app: &App, area: Rect, peers: &[serde_json::Va helpers::kv_line("Node Addr", helpers::str_field(peer, "node_addr")), helpers::kv_line("Npub", helpers::str_field(peer, "npub")), helpers::kv_line("IPv6 Addr", helpers::str_field(peer, "ipv6_addr")), + helpers::kv_line("Tree Role", tree_role), Line::from(""), // Connection helpers::section_header("Connection"), helpers::kv_line("Connectivity", helpers::str_field(peer, "connectivity")), helpers::kv_line("Link ID", &helpers::u64_field(peer, "link_id")), + helpers::kv_line("Direction", peer.get("direction").and_then(|v| v.as_str()).unwrap_or("-")), ]; if let Some(addr) = peer.get("transport_addr").and_then(|v| v.as_str()) { lines.push(helpers::kv_line("Transport Addr", addr)); } + if let Some(t_type) = peer.get("transport_type").and_then(|v| v.as_str()) { + lines.push(helpers::kv_line("Transport Type", t_type)); + } // Link details (cross-referenced) let link_id = peer.get("link_id").and_then(|v| v.as_u64()); let link = lookup_link(app, link_id); if let Some(ref link) = link { - lines.push(helpers::kv_line("Direction", helpers::str_field(link, "direction"))); lines.push(helpers::kv_line("Link State", helpers::str_field(link, "state"))); } lines.extend([ @@ -180,7 +239,7 @@ fn draw_detail(frame: &mut Frame, app: &App, area: Rect, peers: &[serde_json::Va Line::from(""), ]); - // Transport info (cross-referenced from link → transport) + // Transport info (cross-referenced from link -> transport) if let Some(transport) = link.as_ref().and_then(|l| lookup_transport(app, l)) { lines.push(helpers::section_header("Transport")); lines.push(helpers::kv_line("Type", helpers::str_field(&transport, "type"))); @@ -250,7 +309,7 @@ fn lookup_link(app: &App, link_id: Option) -> Option { .cloned() } -/// Look up transport info by chaining: link → transport_id → transport. +/// Look up transport info by chaining: link -> transport_id -> transport. fn lookup_transport(app: &App, link: &serde_json::Value) -> Option { let transport_id = link.get("transport_id").and_then(|v| v.as_u64())?; let transports = app.data.get(&Tab::Transports)?; @@ -262,12 +321,3 @@ fn lookup_transport(app: &App, link: &serde_json::Value) -> Option Span<'static> { - let color = match conn { - "Full" => Color::Green, - "Partial" => Color::Yellow, - _ => Color::Red, - }; - Span::styled(conn.to_string(), Style::default().fg(color)) -} - diff --git a/src/control/queries.rs b/src/control/queries.rs index 1624555a..9db51ac9 100644 --- a/src/control/queries.rs +++ b/src/control/queries.rs @@ -65,10 +65,20 @@ pub fn show_status(node: &Node) -> Value { /// `show_peers` — Authenticated peers. pub fn show_peers(node: &Node) -> Value { + let tree = node.tree_state(); + let my_addr = *tree.my_node_addr(); + let parent_id = *tree.my_declaration().parent_id(); + let is_root = tree.is_root(); + let peers: Vec = node.peers().map(|peer| { let node_addr = *peer.node_addr(); let addr_hex = hex::encode(node_addr.as_bytes()); + // Determine tree relationship + let is_parent = !is_root && node_addr == parent_id; + let is_child = tree.peer_declaration(&node_addr) + .is_some_and(|decl| *decl.parent_id() == my_addr); + let mut peer_json = json!({ "node_addr": addr_hex, "npub": peer.npub(), @@ -81,6 +91,8 @@ pub fn show_peers(node: &Node) -> Value { "has_tree_position": peer.has_tree_position(), "has_bloom_filter": peer.filter_sequence() > 0, "filter_sequence": peer.filter_sequence(), + "is_parent": is_parent, + "is_child": is_child, }); // Add transport address if available @@ -88,6 +100,16 @@ pub fn show_peers(node: &Node) -> Value { peer_json["transport_addr"] = json!(format!("{}", addr)); } + // Add link info (direction, transport type) + let link_id = peer.link_id(); + if let Some(link) = node.get_link(&link_id) { + peer_json["direction"] = json!(format!("{}", link.direction())); + let transport_id = link.transport_id(); + if let Some(handle) = node.get_transport(&transport_id) { + peer_json["transport_type"] = json!(handle.transport_type().name); + } + } + // Add tree depth if available if let Some(coords) = peer.coords() { peer_json["tree_depth"] = json!(coords.depth());