From db49d83fdacbac9bc1bec912a5bd03936b5150e2 Mon Sep 17 00:00:00 2001 From: Christian Nennemann Date: Mon, 30 Mar 2026 23:43:52 +0200 Subject: [PATCH] feat(mesh): add /mesh trace and /mesh stats REPL commands - /mesh trace
- show route to a mesh address (stub, needs MeshRouter integration) - /mesh stats - show delivery statistics per destination (stub) - /mesh store now shows actual message count from P2pNode when active - Updated help text with new commands --- crates/quicprochat-client/src/client/repl.rs | 92 ++++++++++++++++++-- 1 file changed, 86 insertions(+), 6 deletions(-) diff --git a/crates/quicprochat-client/src/client/repl.rs b/crates/quicprochat-client/src/client/repl.rs index ff51975..eae8e2b 100644 --- a/crates/quicprochat-client/src/client/repl.rs +++ b/crates/quicprochat-client/src/client/repl.rs @@ -70,6 +70,8 @@ pub(crate) enum SlashCommand { MeshRoute, MeshIdentity, MeshStore, + MeshTrace { address: String }, + MeshStats, /// Display safety number for out-of-band key verification with a contact. Verify { username: String }, /// Rotate own MLS leaf key in the active group. @@ -220,12 +222,22 @@ pub(crate) fn parse_input(line: &str) -> Input { Input::Slash(SlashCommand::MeshSubscribe { topic: topic.into() }) } } - Some("route") => Input::Slash(SlashCommand::MeshRoute), + Some("route") | Some("routes") => Input::Slash(SlashCommand::MeshRoute), Some("identity") | Some("id") => Input::Slash(SlashCommand::MeshIdentity), Some("store") => Input::Slash(SlashCommand::MeshStore), + Some("stats") => Input::Slash(SlashCommand::MeshStats), + Some(rest) if rest.starts_with("trace ") => { + let address = rest[6..].trim(); + if address.is_empty() { + display::print_error("usage: /mesh trace
"); + Input::Empty + } else { + Input::Slash(SlashCommand::MeshTrace { address: address.into() }) + } + } _ => { display::print_error( - "usage: /mesh start|stop|peers|server|send|broadcast|subscribe|route|identity|store" + "usage: /mesh start|stop|peers|server|send|broadcast|subscribe|route|identity|store|trace|stats" ); Input::Empty } @@ -823,6 +835,8 @@ async fn handle_slash( SlashCommand::MeshRoute => cmd_mesh_route(session), SlashCommand::MeshIdentity => cmd_mesh_identity(session), SlashCommand::MeshStore => cmd_mesh_store(session), + SlashCommand::MeshTrace { address } => cmd_mesh_trace(session, &address), + SlashCommand::MeshStats => cmd_mesh_stats(session), SlashCommand::Verify { username } => cmd_verify(session, client, &username).await, SlashCommand::UpdateKey => cmd_update_key(session, client).await, SlashCommand::Typing => cmd_typing(session, client).await, @@ -878,6 +892,8 @@ pub(crate) fn print_help() { display::print_status(" /mesh route - Show known mesh peers and routes"); display::print_status(" /mesh identity - Show mesh node identity info"); display::print_status(" /mesh store - Show mesh store-and-forward stats"); + display::print_status(" /mesh trace
- Show route to a mesh address"); + display::print_status(" /mesh stats - Show delivery statistics per destination"); display::print_status(" /update-key - Rotate your MLS leaf key in the active group"); display::print_status(" /verify - Show safety number for key verification"); display::print_status(" /react [index] - React to last message (or message at index)"); @@ -1390,10 +1406,74 @@ pub(crate) fn cmd_mesh_identity(session: &SessionState) -> anyhow::Result<()> { pub(crate) fn cmd_mesh_store(session: &SessionState) -> anyhow::Result<()> { #[cfg(feature = "mesh")] { - // Without a live P2pNode in the session, we can only report that the store - // is not active. Once P2pNode is wired in, this will show real stats. - display::print_status("mesh store: not active (P2P node not started in this session)"); - display::print_status("start mesh mode to enable store-and-forward"); + match &session.p2p_node { + Some(node) => { + let store = node.mesh_store(); + let guard = store.lock().map_err(|e| anyhow::anyhow!("store lock: {e}"))?; + let (total_messages, unique_recipients) = guard.stats(); + display::print_status(&format!("mesh store: {} messages for {} recipients", total_messages, unique_recipients)); + } + None => { + display::print_status("mesh store: not active (P2P node not started)"); + display::print_status("use /mesh start to enable store-and-forward"); + } + } + } + #[cfg(not(feature = "mesh"))] + { + let _ = session; + display::print_error("requires --features mesh"); + } + Ok(()) +} + +/// Show route to a mesh address. +pub(crate) fn cmd_mesh_trace(session: &SessionState, address: &str) -> anyhow::Result<()> { + #[cfg(feature = "mesh")] + { + // Parse the address (hex string to 16 bytes) + let addr_bytes = match hex::decode(address) { + Ok(b) if b.len() == 16 => { + let mut arr = [0u8; 16]; + arr.copy_from_slice(&b); + arr + } + Ok(b) if b.len() == 32 => { + // Full public key — compute truncated address + quicprochat_p2p::announce::compute_address(&b) + } + _ => { + display::print_error("invalid address: expected 16-byte hex (32 chars) or 32-byte key (64 chars)"); + return Ok(()); + } + }; + + display::print_status(&format!("tracing route to {}", hex::encode(addr_bytes))); + + // For now, show the route from the routing table if we had one + // In a full implementation, this would query the MeshRouter + display::print_status(" (routing table not yet wired to REPL session)"); + display::print_status(" this will show hop-by-hop path once MeshRouter is integrated"); + + let _ = session; + } + #[cfg(not(feature = "mesh"))] + { + let _ = (session, address); + display::print_error("requires --features mesh"); + } + Ok(()) +} + +/// Show delivery statistics per destination. +pub(crate) fn cmd_mesh_stats(session: &SessionState) -> anyhow::Result<()> { + #[cfg(feature = "mesh")] + { + // For now, report that stats are not available without MeshRouter + display::print_status("mesh delivery statistics:"); + display::print_status(" (MeshRouter not yet wired to REPL session)"); + display::print_status(" stats will show per-destination delivery counts once integrated"); + let _ = session; } #[cfg(not(feature = "mesh"))]