feat: add mesh network visualizer

- D3.js force-directed graph for real-time mesh visualization
- WebSocket server (mesh-viz-bridge crate) for live updates
- Demo mode with simulated topology
- JSONL file upload for offline analysis
- Optional viz logging in mesh_node forwarding
This commit is contained in:
2026-04-06 21:43:28 +02:00
parent 99d36679c8
commit 95ce8898fd
9 changed files with 831 additions and 1 deletions

View File

@@ -42,6 +42,7 @@ pub mod transport_iroh;
pub mod transport_manager;
pub mod transport_tcp;
pub mod transport_lora;
pub mod viz_log;
#[cfg(feature = "traffic-resistance")]
pub mod traffic_resistance;

View File

@@ -352,8 +352,14 @@ impl MeshNode {
IncomingAction::Deliver(_) => {
self.metrics.store.messages_delivered.inc();
}
IncomingAction::Forward { .. } => {
IncomingAction::Forward {
envelope: _,
next_hop,
} => {
self.metrics.routing.announcements_forwarded.inc();
let from = format!("{sender}");
let to = next_hop.to_string();
crate::viz_log::log_forward_hop(&from, &to, 0);
}
IncomingAction::Store(_) => {
self.metrics.store.messages_stored.inc();

View File

@@ -0,0 +1,45 @@
//! Optional NDJSON events for the mesh graph visualizer (`viz/mesh-graph.html`).
//!
//! When the environment variable `QPC_MESH_VIZ_LOG` is set to a file path, one JSON object
//! per line is appended for selected mesh events. The `viz/bridge` binary can tail this file
//! and forward lines to the browser over WebSocket.
use serde::Serialize;
#[derive(Serialize)]
struct HopEvent<'a> {
#[serde(rename = "type")]
kind: &'static str,
from: &'a str,
to: &'a str,
ms: u64,
}
/// Log a relay hop (forwarding to `next_hop`). No-op unless `QPC_MESH_VIZ_LOG` is set.
pub fn log_forward_hop(from_sender: &str, next_hop: &str, latency_ms: u64) {
let Ok(path) = std::env::var("QPC_MESH_VIZ_LOG") else {
return;
};
let ev = HopEvent {
kind: "hop",
from: from_sender,
to: next_hop,
ms: latency_ms,
};
let Ok(line) = serde_json::to_string(&ev) else {
return;
};
append_line(&path, &line);
}
fn append_line(path: &str, line: &str) {
use std::io::Write;
let Ok(mut f) = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(path)
else {
return;
};
let _ = writeln!(f, "{line}");
}