#pragma once // Only active when KPN_WEB_DEBUG is defined. #ifdef KPN_WEB_DEBUG #include "diagnostics.hpp" #include "web_debug.hpp" #include #include #include #include #include #include namespace kpn { // ── Hub HTML ────────────────────────────────────────────────────────────────── // Multi-tab UI: one tab per registered network (force-directed graph) + // an "All Networks" tab showing shared resource cards and a cross-network // node table. static const char* HUB_HTML = R"html( KPN++ Debug Hub

KPN++ Debug Hub

connecting…
)html"; // ── DebugHub ────────────────────────────────────────────────────────────────── class DebugHub { public: explicit DebugHub(uint16_t port = 9090) : port_(port) {} ~DebugHub() { stop(); } DebugHub(const DebugHub&) = delete; DebugHub& operator=(const DebugHub&) = delete; // Register a network. Disables that network's own web server so the hub // becomes the single debug endpoint. Call before network.start(). template void register_network(const std::string& name, Net& net) { net.disable_web_server(); networks_.push_back({name, [&net, name] { auto s = net.network_snapshot(); s.name = name; return s; }}); } // Register a shared resource — appears in the "All Networks" resource panel. void register_resource(const std::string& name, IResourceProbe* probe) { resources_.emplace_back(name, probe); } void start() { server_ = std::make_unique( port_, [this] { return build_json(); }, HUB_HTML); server_->start(); std::cerr << "[kpn] hub debug UI: http://localhost:" << port_ << "\n"; } void stop() { if (server_) server_->stop(); } private: // Serialise nodes array for one network snapshot static void write_nodes(std::ostream& o, const NetworkSnapshot& s) { o << "["; for (std::size_t i = 0; i < s.nodes.size(); ++i) { const auto& n = s.nodes[i]; if (i) o << ','; o << "{\"id\":\"" << web_debug::escape_json(n.name) << "\"" << ",\"frames\":" << n.frames_processed << ",\"ema_exec_ms\":" << n.ema_exec_ms << ",\"max_exec_ms\":" << n.max_exec_ms << ",\"blocked_ms\":" << n.total_blocked_ms << ",\"fps\":" << n.throughput_fps << ",\"total_cpu_ms\":" << n.total_cpu_ms << ",\"cpu_util_pct\":" << n.cpu_util_pct << "}"; } o << "]"; } // Serialise edges array for one network snapshot static void write_edges(std::ostream& o, const NetworkSnapshot& s) { o << "["; for (std::size_t i = 0; i < s.channels.size(); ++i) { const auto& c = s.channels[i]; if (i) o << ','; auto [src, dst] = web_debug::parse_edge_name(c.name); o << "{\"name\":\"" << web_debug::escape_json(c.name) << "\"" << ",\"source\":\"" << web_debug::escape_json(src) << "\"" << ",\"target\":\"" << web_debug::escape_json(dst) << "\"" << ",\"capacity\":" << c.capacity << ",\"current\":" << c.current_fill << ",\"fill_pct\":" << c.fill_pct() << ",\"peak_pct\":" << c.peak_pct() << ",\"pushes\":" << c.pushes << ",\"drops\":" << c.drops << ",\"overflows\":" << c.overflows << ",\"item_bytes\":" << c.item_bytes << ",\"bw_mbs\":" << c.bandwidth_mbs(s.elapsed_s) << "}"; } o << "]"; } std::string build_json() const { std::ostringstream o; o << std::fixed; o.precision(2); o << "{\"networks\":["; for (std::size_t i = 0; i < networks_.size(); ++i) { if (i) o << ','; auto s = networks_[i].fn(); o << "{\"name\":\"" << web_debug::escape_json(networks_[i].name) << "\"" << ",\"nodes\":"; write_nodes(o, s); o << ",\"edges\":"; write_edges(o, s); o << "}"; } o << "],\"resources\":["; for (std::size_t i = 0; i < resources_.size(); ++i) { if (i) o << ','; const auto r = resources_[i].second->snapshot(resources_[i].first); o << "{\"name\":\"" << web_debug::escape_json(r.name) << "\"" << ",\"acquisitions\":" << r.acquisitions << ",\"avg_wait_ms\":" << r.avg_wait_ms << ",\"peak_waiters\":" << r.peak_waiters << ",\"current_waiters\":" << r.current_waiters << ",\"held\":" << (r.held ? "true" : "false") << "}"; } o << "]}"; return o.str(); } struct Entry { std::string name; std::function fn; }; uint16_t port_; std::vector networks_; std::vector> resources_; std::unique_ptr server_; }; } // namespace kpn #endif // KPN_WEB_DEBUG