@@ -292,6 +292,172 @@ namespace vix::p2p_http
292292 " tracked_endpoints" , (long long )st.connect .tracked_endpoints
293293 })); });
294294
295+ #if defined(VIX_P2P_HTTP_WITH_MIDDLEWARE)
296+ {
297+ vix::p2p_http::RouteOptions ro;
298+ ro.heavy = false ;
299+ ro.require_auth = false ;
300+ install_route_middlewares (app, path, ro, opt);
301+ }
302+ #endif
303+ }
304+
305+ // GET /p2p/peers (multi-peer view for dashboard)
306+ if (opt.enable_peers )
307+ {
308+ const std::string path = join_prefix (base, " /peers" );
309+
310+ app.get (path, [&runtime](vix::vhttp::Request &, vix::vhttp::ResponseWrapper &res)
311+ {
312+ auto node = runtime.node ();
313+ if (!node)
314+ {
315+ res.status (503 ).json (J::obj ({
316+ " ok" , false ,
317+ " error" , " p2p_node_unavailable"
318+ }));
319+ return ;
320+ }
321+
322+ const auto snap = node->peers_snapshot ();
323+
324+ // Make output stable: sort by peer_id
325+ std::vector<std::pair<vix::p2p::PeerId, vix::p2p::Peer>> items;
326+ items.reserve (snap.size ());
327+ for (const auto &kv : snap)
328+ items.push_back (kv);
329+
330+ std::sort (items.begin (), items.end (),
331+ [](const auto &a, const auto &b)
332+ {
333+ return a.first < b.first ;
334+ });
335+
336+ auto state_to_string = [](vix::p2p::PeerState s) -> const char *
337+ {
338+ switch (s)
339+ {
340+ case vix::p2p::PeerState::Disconnected: return " disconnected" ;
341+ case vix::p2p::PeerState::Connecting: return " connecting" ;
342+ case vix::p2p::PeerState::Handshaking: return " handshaking" ;
343+ case vix::p2p::PeerState::Connected: return " connected" ;
344+ case vix::p2p::PeerState::Stale: return " stale" ;
345+ case vix::p2p::PeerState::Closed: return " closed" ;
346+ default : return " unknown" ;
347+ }
348+ };
349+
350+ auto hs_stage_to_string = [](vix::p2p::HandshakeState::Stage s) -> const char *
351+ {
352+ switch (s)
353+ {
354+ case vix::p2p::HandshakeState::Stage::None: return " none" ;
355+ case vix::p2p::HandshakeState::Stage::HelloSent: return " hello_sent" ;
356+ case vix::p2p::HandshakeState::Stage::HelloReceived: return " hello_received" ;
357+ case vix::p2p::HandshakeState::Stage::AckSent: return " ack_sent" ;
358+ case vix::p2p::HandshakeState::Stage::AckReceived: return " ack_received" ;
359+ case vix::p2p::HandshakeState::Stage::Finished: return " finished" ;
360+ default : return " unknown" ;
361+ }
362+ };
363+
364+ auto endpoint_to_string = [](const std::optional<vix::p2p::PeerEndpoint> &ep) -> std::string
365+ {
366+ if (!ep)
367+ return " " ;
368+
369+ const std::string scheme = (ep->scheme .empty () ? " tcp" : ep->scheme );
370+ return scheme + " ://" + ep->host + " :" + std::to_string (ep->port );
371+ };
372+
373+ const auto now = std::chrono::steady_clock::now ();
374+
375+ std::vector<J::token> peers_arr;
376+ peers_arr.reserve (items.size ());
377+
378+ for (const auto &[peer_id, p] : items)
379+ {
380+ const std::string ep_str = endpoint_to_string (p.endpoint );
381+
382+ long long last_seen_ms_ago = -1 ;
383+ if (p.meta .last_seen .time_since_epoch ().count () != 0 )
384+ {
385+ const auto diff = now - p.meta .last_seen ;
386+ last_seen_ms_ago =
387+ (long long )std::chrono::duration_cast<std::chrono::milliseconds>(diff).count ();
388+ }
389+
390+ const bool secure = p.meta .secure ;
391+ const long long public_key_len = (long long )p.meta .public_key .size ();
392+ const long long session_key_len = (long long )p.meta .session_key_32 .size ();
393+ const long long capabilities_count = (long long )p.meta .capabilities .size ();
394+
395+ // Handshake block (optional)
396+ const bool has_hs = p.handshake .has_value ();
397+ const char *hs_stage = " none" ;
398+ long long hs_age_ms = -1 ;
399+ long long hs_nonce_a = 0 ;
400+ long long hs_nonce_b = 0 ;
401+ long long hs_ts_ms = 0 ;
402+
403+ if (has_hs)
404+ {
405+ hs_stage = hs_stage_to_string (p.handshake ->stage );
406+
407+ if (p.handshake ->started_at .time_since_epoch ().count () != 0 )
408+ {
409+ const auto diff = now - p.handshake ->started_at ;
410+ hs_age_ms =
411+ (long long )std::chrono::duration_cast<std::chrono::milliseconds>(diff).count ();
412+ }
413+
414+ hs_nonce_a = (long long )p.handshake ->nonce_a ;
415+ hs_nonce_b = (long long )p.handshake ->nonce_b ;
416+ hs_ts_ms = (long long )p.handshake ->ts_ms ;
417+ }
418+
419+ // Endpoint split (optional)
420+ const bool has_ep = p.endpoint .has_value ();
421+ const std::string ep_scheme = (has_ep ? (p.endpoint ->scheme .empty () ? " tcp" : p.endpoint ->scheme ) : " " );
422+ const std::string ep_host = (has_ep ? p.endpoint ->host : " " );
423+ const long long ep_port = (has_ep ? (long long )p.endpoint ->port : 0 );
424+
425+ // Final peer object
426+ peers_arr.push_back (J::obj ({
427+ " peer_id" , peer_id,
428+ " state" , state_to_string (p.state ),
429+
430+ " endpoint" , ep_str,
431+ " has_endpoint" , has_ep,
432+ " scheme" , ep_scheme,
433+ " host" , ep_host,
434+ " port" , ep_port,
435+
436+ " secure" , secure,
437+ " capabilities_count" , capabilities_count,
438+ " public_key_len" , public_key_len,
439+ " session_key_len" , session_key_len,
440+
441+ " last_seen_ms_ago" , (long long )last_seen_ms_ago,
442+
443+ " has_handshake" , has_hs,
444+ " handshake_stage" , hs_stage,
445+ " handshake_age_ms" , (long long )hs_age_ms,
446+
447+ // debug-friendly (safe, no secrets)
448+ " nonce_a" , (long long )hs_nonce_a,
449+ " nonce_b" , (long long )hs_nonce_b,
450+ " ts_ms" , (long long )hs_ts_ms
451+ }));
452+ }
453+
454+ res.json (J::obj ({
455+ " ok" , true ,
456+ " module" , " p2p_http" ,
457+ " total" , (long long )peers_arr.size (),
458+ " peers" , J::array (std::move (peers_arr))
459+ })); });
460+
295461#if defined(VIX_P2P_HTTP_WITH_MIDDLEWARE)
296462 {
297463 vix::p2p_http::RouteOptions ro;
0 commit comments