feat(mesh): add /mesh trace and /mesh stats REPL commands

- /mesh trace <address> - 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
This commit is contained in:
2026-03-30 23:43:52 +02:00
parent 9b09f09892
commit db49d83fda

View File

@@ -70,6 +70,8 @@ pub(crate) enum SlashCommand {
MeshRoute, MeshRoute,
MeshIdentity, MeshIdentity,
MeshStore, MeshStore,
MeshTrace { address: String },
MeshStats,
/// Display safety number for out-of-band key verification with a contact. /// Display safety number for out-of-band key verification with a contact.
Verify { username: String }, Verify { username: String },
/// Rotate own MLS leaf key in the active group. /// 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() }) 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("identity") | Some("id") => Input::Slash(SlashCommand::MeshIdentity),
Some("store") => Input::Slash(SlashCommand::MeshStore), 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 <address>");
Input::Empty
} else {
Input::Slash(SlashCommand::MeshTrace { address: address.into() })
}
}
_ => { _ => {
display::print_error( 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 Input::Empty
} }
@@ -823,6 +835,8 @@ async fn handle_slash(
SlashCommand::MeshRoute => cmd_mesh_route(session), SlashCommand::MeshRoute => cmd_mesh_route(session),
SlashCommand::MeshIdentity => cmd_mesh_identity(session), SlashCommand::MeshIdentity => cmd_mesh_identity(session),
SlashCommand::MeshStore => cmd_mesh_store(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::Verify { username } => cmd_verify(session, client, &username).await,
SlashCommand::UpdateKey => cmd_update_key(session, client).await, SlashCommand::UpdateKey => cmd_update_key(session, client).await,
SlashCommand::Typing => cmd_typing(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 route - Show known mesh peers and routes");
display::print_status(" /mesh identity - Show mesh node identity info"); 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 store - Show mesh store-and-forward stats");
display::print_status(" /mesh trace <address> - 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(" /update-key - Rotate your MLS leaf key in the active group");
display::print_status(" /verify <username> - Show safety number for key verification"); display::print_status(" /verify <username> - Show safety number for key verification");
display::print_status(" /react <emoji> [index] - React to last message (or message at index)"); display::print_status(" /react <emoji> [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<()> { pub(crate) fn cmd_mesh_store(session: &SessionState) -> anyhow::Result<()> {
#[cfg(feature = "mesh")] #[cfg(feature = "mesh")]
{ {
// Without a live P2pNode in the session, we can only report that the store match &session.p2p_node {
// is not active. Once P2pNode is wired in, this will show real stats. Some(node) => {
display::print_status("mesh store: not active (P2P node not started in this session)"); let store = node.mesh_store();
display::print_status("start mesh mode to enable store-and-forward"); 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; let _ = session;
} }
#[cfg(not(feature = "mesh"))] #[cfg(not(feature = "mesh"))]