Files
navegator_dashboard/panels.cpp
T

1393 lines
54 KiB
C++

// Panels v1+v2:
// - Browsers : scan + spawn + kill + seleccion (click selecciona instancia)
// - Tabs : lista pestañas via CDP HTTP /json + Focus/Close/New/Select
// - Tab Detail : Runtime.evaluate REPL minimo (placeholder upgradeable)
// - Network : panel DevTools-like — tabla + filtros + detalle por tab
//
// Estado cross-panel via g_session() (session_state.h).
// Issue 0081-J: tablas ##browsers, ##tabs, ##wsframes, ##requests migradas a
// data_table::render() con CellRenderers declarativos (Badge, Duration).
#include "imgui.h"
#include "implot.h"
#include "core/icons_tabler.h"
#include "core/tokens.h"
#include "core/data_table_types.h"
#include "data_table/data_table.h"
#include "chrome_scanner.h"
#include "chrome_launcher.h"
#include "local_api.h"
#include "cdp_http.h"
#include "session_state.h"
#include "picker_state.h"
#include "py_subprocess.h"
#include <algorithm>
#include <atomic>
#include <chrono>
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <fstream>
#include <map>
#include <mutex>
#include <sstream>
#include <string>
#include <thread>
#include <vector>
#include <ctime>
#ifdef _WIN32
# define WIN32_LEAN_AND_MEAN
# include <windows.h>
#endif
namespace navegator {
// ===========================================================================
// Browsers panel
// ===========================================================================
namespace {
struct BrowsersState {
std::mutex mu;
std::vector<ChromeInstance> instances;
std::chrono::steady_clock::time_point last_scan;
std::atomic<bool> scanning{false};
std::atomic<bool> ever_scanned{false};
char new_profile[128] = "default";
int new_port = 19222;
bool new_headless = false;
std::string last_error;
// data_table state (persists between frames).
data_table::State dt_state;
};
BrowsersState g_browsers;
void rescan_async() {
if (g_browsers.scanning.exchange(true)) return;
std::thread([]{
auto v = scan_chrome_instances();
{
std::lock_guard<std::mutex> lk(g_browsers.mu);
g_browsers.instances = std::move(v);
g_browsers.last_scan = std::chrono::steady_clock::now();
}
g_browsers.ever_scanned.store(true);
g_browsers.scanning.store(false);
}).detach();
}
std::string default_user_data_dir(const std::string& profile) {
#ifdef _WIN32
char buf[MAX_PATH] = {0};
DWORD n = GetEnvironmentVariableA("USERPROFILE", buf, sizeof(buf));
std::string base = (n > 0 && n < sizeof(buf)) ? buf : "C:\\Users\\Public";
return base + "\\AppData\\Local\\navegator_profiles\\" + profile;
#else
return std::string("/tmp/navegator_profiles/") + profile;
#endif
}
} // anon
void render_browsers_panel(bool* p_open) {
if (!ImGui::Begin(TI_BROWSER " Browsers", p_open)) {
ImGui::End();
return;
}
auto now = std::chrono::steady_clock::now();
{
std::lock_guard<std::mutex> lk(g_browsers.mu);
if (now - g_browsers.last_scan > std::chrono::seconds(2) && !g_browsers.scanning.load()) {
rescan_async();
}
}
if (g_api_running.load()) {
ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::success);
ImGui::Text("API: 127.0.0.1:%d", g_api_port.load());
ImGui::PopStyleColor();
ImGui::SameLine();
ImGui::TextDisabled("(reqs: %d)", g_api_request_count.load());
} else {
ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::error);
ImGui::TextUnformatted("API: down");
ImGui::PopStyleColor();
}
ImGui::Separator();
if (ImGui::Button(TI_REFRESH " Rescan")) rescan_async();
ImGui::SameLine();
ImGui::TextDisabled("|");
ImGui::SameLine();
ImGui::TextUnformatted("New profile:");
ImGui::SameLine();
ImGui::SetNextItemWidth(160);
ImGui::InputText("##profile", g_browsers.new_profile, sizeof(g_browsers.new_profile));
ImGui::SameLine();
ImGui::TextUnformatted("Port:");
ImGui::SameLine();
ImGui::SetNextItemWidth(80);
ImGui::InputInt("##port", &g_browsers.new_port, 0, 0);
ImGui::SameLine();
ImGui::Checkbox("Headless", &g_browsers.new_headless);
ImGui::SameLine();
if (ImGui::Button(TI_PLAYER_PLAY " Launch")) {
LaunchOpts o;
o.port = g_browsers.new_port;
o.headless = g_browsers.new_headless;
std::string profile = g_browsers.new_profile;
if (profile.empty()) profile = "default";
o.user_data_dir = default_user_data_dir(profile);
auto r = launch_chrome(o);
g_browsers.last_error = r.ok ? "" : r.error;
std::thread([]{
std::this_thread::sleep_for(std::chrono::milliseconds(800));
rescan_async();
}).detach();
}
ImGui::SameLine();
ImGui::TextDisabled("|");
ImGui::SameLine();
if (ImGui::Button(TI_X " Kill all navegator")) {
kill_chromes_by_userdata("navegator_profiles");
std::thread([]{
std::this_thread::sleep_for(std::chrono::milliseconds(500));
rescan_async();
}).detach();
}
if (!g_browsers.last_error.empty()) {
ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::error);
ImGui::TextWrapped("Error: %s", g_browsers.last_error.c_str());
ImGui::PopStyleColor();
}
ImGui::Separator();
int sel_port = 0;
{
std::lock_guard<std::mutex> lk(g_session().mu);
sel_port = g_session().selected_port;
}
std::lock_guard<std::mutex> lk(g_browsers.mu);
if (!g_browsers.ever_scanned.load() && g_browsers.instances.empty()) {
ImGui::TextUnformatted("Scanning...");
} else if (g_browsers.instances.empty()) {
ImGui::TextDisabled("No Chrome instances with --remote-debugging-port detected.");
ImGui::TextDisabled("Lanza una con el formulario de arriba o con scripts/start.sh.");
} else {
// --- Build TableInput (6 data cols, no Actions) ---
// Cols: Sel, PID, Port, Profile, Mode, user-data-dir
static std::vector<std::string> cell_backing;
const int NCOLS = 6;
const int nrows = (int)g_browsers.instances.size();
cell_backing.clear();
cell_backing.reserve((size_t)(nrows * NCOLS));
for (const auto& inst : g_browsers.instances) {
cell_backing.push_back(sel_port == inst.port ? TI_CHECK : "");
cell_backing.push_back(std::to_string(inst.pid));
cell_backing.push_back(std::to_string(inst.port));
cell_backing.push_back(inst.profile_name.empty() ? "(none)" : inst.profile_name);
cell_backing.push_back(inst.headless ? "headless" : "visible");
cell_backing.push_back(inst.user_data_dir);
}
static std::vector<const char*> cell_ptrs;
cell_ptrs.clear();
cell_ptrs.reserve(cell_backing.size());
for (const auto& s : cell_backing) cell_ptrs.push_back(s.c_str());
data_table::TableInput tbl;
tbl.name = "browsers";
tbl.headers = {"", "PID", "Port", "Profile", "Mode", "user-data-dir"};
tbl.types = {
data_table::ColumnType::String,
data_table::ColumnType::Int,
data_table::ColumnType::Int,
data_table::ColumnType::String,
data_table::ColumnType::String,
data_table::ColumnType::String
};
tbl.cells = cell_ptrs.data();
tbl.rows = nrows;
tbl.cols = NCOLS;
// Declarative renderers.
tbl.column_specs.resize((size_t)NCOLS);
// Col 4: Mode -> Badge
{
data_table::ColumnSpec& cs = tbl.column_specs[4];
cs.id = "mode";
cs.renderer = data_table::CellRenderer::Badge;
cs.badges = {
data_table::BadgeRule{"headless", "#f59e0b", "headless"},
data_table::BadgeRule{"visible", "#22c55e", "visible"},
};
}
std::vector<data_table::TableEvent> dt_events;
data_table::render("##dt_browsers", {tbl}, g_browsers.dt_state, &dt_events, /*show_chrome=*/false);
for (auto& ev : dt_events) {
if (ev.kind == data_table::TableEventKind::RowDoubleClick &&
ev.row >= 0 && ev.row < static_cast<int>(g_browsers.instances.size())) {
g_session().select_browser(g_browsers.instances[ev.row].port);
}
}
// --- Actions: inline button list per row (no BeginTable — Button renderer not Fase-1) ---
ImGui::Separator();
ImGui::TextDisabled("Actions:");
int idx = 0;
for (const auto& inst : g_browsers.instances) {
bool is_sel = (sel_port == inst.port);
ImGui::PushID(idx);
ImGui::Text(":%d", inst.port); ImGui::SameLine();
if (ImGui::SmallButton(is_sel ? "Selected" : "Select")) {
g_session().select_browser(inst.port);
}
ImGui::SameLine();
if (ImGui::SmallButton("Kill")) {
if (!inst.user_data_dir.empty()) {
kill_chromes_by_userdata(inst.user_data_dir);
}
if (sel_port == inst.port) g_session().clear_selection();
std::thread([]{
std::this_thread::sleep_for(std::chrono::milliseconds(500));
rescan_async();
}).detach();
}
ImGui::PopID();
++idx;
}
}
ImGui::End();
}
// ===========================================================================
// Tabs panel
// ===========================================================================
namespace {
struct TabsUiState {
std::atomic<bool> refreshing{false};
char new_url_input[1024] = "https://example.com";
char filter[128] = "";
// data_table state (persists between frames).
data_table::State dt_state;
};
TabsUiState g_tabs_ui;
void refresh_tabs_async(int port) {
if (port <= 0) return;
if (g_tabs_ui.refreshing.exchange(true)) return;
std::thread([port]{
std::vector<CdpTab> v;
std::string err;
bool ok = cdp_list_tabs(port, v, &err);
{
std::lock_guard<std::mutex> lk(g_session().mu);
if (g_session().selected_port == port) {
g_session().tabs = std::move(v);
g_session().tabs_error = ok ? "" : err;
g_session().last_tabs_refresh = std::chrono::steady_clock::now();
}
}
g_tabs_ui.refreshing.store(false);
}).detach();
}
} // anon
void render_tabs_panel(bool* p_open) {
if (!ImGui::Begin(TI_LIST " Tabs", p_open)) {
ImGui::End();
return;
}
int port = 0;
std::string sel_tab_id;
{
std::lock_guard<std::mutex> lk(g_session().mu);
port = g_session().selected_port;
sel_tab_id = g_session().selected_tab_id;
}
if (port <= 0) {
ImGui::TextDisabled("Select a browser in the Browsers panel.");
ImGui::End();
return;
}
auto now = std::chrono::steady_clock::now();
{
std::lock_guard<std::mutex> lk(g_session().mu);
if (now - g_session().last_tabs_refresh > std::chrono::seconds(2) &&
!g_tabs_ui.refreshing.load()) {
refresh_tabs_async(port);
}
}
ImGui::Text("Browser :%d", port);
ImGui::SameLine();
ImGui::TextDisabled("|");
ImGui::SameLine();
if (ImGui::Button(TI_REFRESH " Refresh")) refresh_tabs_async(port);
ImGui::SameLine();
ImGui::TextDisabled("|");
ImGui::SameLine();
ImGui::SetNextItemWidth(280);
ImGui::InputTextWithHint("##new_url", "https://...", g_tabs_ui.new_url_input, sizeof(g_tabs_ui.new_url_input));
ImGui::SameLine();
if (ImGui::Button(TI_PLUS " New tab")) {
std::string url = g_tabs_ui.new_url_input;
std::thread([port, url]{
CdpTab t; std::string err;
cdp_new_tab(port, url, t, &err);
refresh_tabs_async(port);
}).detach();
}
ImGui::SameLine();
ImGui::TextDisabled("|");
ImGui::SameLine();
ImGui::SetNextItemWidth(200);
ImGui::InputTextWithHint("##filter", "filter title/url", g_tabs_ui.filter, sizeof(g_tabs_ui.filter));
{
std::lock_guard<std::mutex> lk(g_session().mu);
if (!g_session().tabs_error.empty()) {
ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::error);
ImGui::TextWrapped("Error: %s", g_session().tabs_error.c_str());
ImGui::PopStyleColor();
}
}
ImGui::Separator();
std::vector<CdpTab> tabs_copy;
{
std::lock_guard<std::mutex> lk(g_session().mu);
tabs_copy = g_session().tabs;
}
if (tabs_copy.empty()) {
ImGui::TextDisabled("No tabs (or CDP HTTP not reachable on :%d).", port);
ImGui::End();
return;
}
// Apply filter to get visible tabs.
std::string filter_str = g_tabs_ui.filter;
std::transform(filter_str.begin(), filter_str.end(), filter_str.begin(), ::tolower);
std::vector<const CdpTab*> visible_tabs;
visible_tabs.reserve(tabs_copy.size());
for (const auto& t : tabs_copy) {
if (!filter_str.empty()) {
std::string lt = t.title; std::transform(lt.begin(), lt.end(), lt.begin(), ::tolower);
std::string lu = t.url; std::transform(lu.begin(), lu.end(), lu.begin(), ::tolower);
if (lt.find(filter_str) == std::string::npos &&
lu.find(filter_str) == std::string::npos) continue;
}
visible_tabs.push_back(&t);
}
// --- Build TableInput (5 data cols, no Actions) ---
// Cols: Sel, Type, Title, URL, Attached
static std::vector<std::string> tab_cell_backing;
const int NCOLS = 5;
const int nrows = (int)visible_tabs.size();
tab_cell_backing.clear();
tab_cell_backing.reserve((size_t)(nrows * NCOLS));
for (const CdpTab* tp : visible_tabs) {
tab_cell_backing.push_back(sel_tab_id == tp->id ? TI_CHECK : "");
tab_cell_backing.push_back(tp->type);
tab_cell_backing.push_back(tp->title.empty() ? "(no title)" : tp->title);
tab_cell_backing.push_back(tp->url);
tab_cell_backing.push_back(tp->attached ? "yes" : "no");
}
static std::vector<const char*> tab_cell_ptrs;
tab_cell_ptrs.clear();
tab_cell_ptrs.reserve(tab_cell_backing.size());
for (const auto& s : tab_cell_backing) tab_cell_ptrs.push_back(s.c_str());
data_table::TableInput tbl;
tbl.name = "tabs";
tbl.headers = {"", "Type", "Title", "URL", "Attached"};
tbl.types = {
data_table::ColumnType::String,
data_table::ColumnType::String,
data_table::ColumnType::String,
data_table::ColumnType::String,
data_table::ColumnType::String
};
tbl.cells = tab_cell_ptrs.data();
tbl.rows = nrows;
tbl.cols = NCOLS;
// Declarative renderers.
tbl.column_specs.resize((size_t)NCOLS);
// Col 1: Type -> Badge
{
data_table::ColumnSpec& cs = tbl.column_specs[1];
cs.id = "type";
cs.renderer = data_table::CellRenderer::Badge;
cs.badges = {
data_table::BadgeRule{"page", "#3b82f6", "page"},
data_table::BadgeRule{"iframe", "#8b5cf6", "iframe"},
data_table::BadgeRule{"service_worker", "#f59e0b", "service_worker"},
data_table::BadgeRule{"worker", "#f59e0b", "worker"},
};
}
// Col 4: Attached -> Badge
{
data_table::ColumnSpec& cs = tbl.column_specs[4];
cs.id = "attached";
cs.renderer = data_table::CellRenderer::Badge;
cs.badges = {
data_table::BadgeRule{"yes", "#22c55e", "yes"},
data_table::BadgeRule{"no", "#6b7280", "no"},
};
}
std::vector<data_table::TableEvent> dt_events;
data_table::render("##dt_tabs", {tbl}, g_tabs_ui.dt_state, &dt_events, /*show_chrome=*/false);
for (auto& ev : dt_events) {
if (ev.kind == data_table::TableEventKind::RowDoubleClick &&
ev.row >= 0 && ev.row < static_cast<int>(visible_tabs.size())) {
const CdpTab* tp = visible_tabs[ev.row];
if (tp && !tp->ws_url.empty()) {
g_session().select_tab(tp->id, tp->ws_url);
}
}
}
// --- Actions: inline button list per visible row (no BeginTable — Button renderer not Fase-1) ---
ImGui::Separator();
ImGui::TextDisabled("Actions:");
{
int idx = 0;
for (const CdpTab* tp : visible_tabs) {
bool is_sel = (sel_tab_id == tp->id);
ImGui::PushID(idx);
std::string short_title = tp->title.empty() ? "(no title)" : tp->title;
if (short_title.size() > 32) short_title = short_title.substr(0, 29) + "...";
ImGui::TextUnformatted(short_title.c_str()); ImGui::SameLine();
if (ImGui::SmallButton(is_sel ? "Sel*" : "Select")) {
if (!tp->ws_url.empty()) g_session().select_tab(tp->id, tp->ws_url);
}
ImGui::SameLine();
if (ImGui::SmallButton("Focus")) {
std::string id = tp->id;
std::thread([port, id]{
cdp_activate_tab(port, id, nullptr);
refresh_tabs_async(port);
}).detach();
}
ImGui::SameLine();
if (ImGui::SmallButton("Close")) {
std::string id = tp->id;
bool is_s = is_sel;
std::thread([port, id]{
cdp_close_tab(port, id, nullptr);
refresh_tabs_async(port);
}).detach();
if (is_s) g_session().select_tab("", "");
}
ImGui::PopID();
++idx;
}
}
ImGui::End();
}
// ===========================================================================
// Tab Detail panel (placeholder funcional)
// ===========================================================================
namespace {
struct TabDetailUiState {
char repl_input[4096] = "1+1";
std::string repl_output;
std::mutex mu;
};
TabDetailUiState g_tab_detail_ui;
void tab_detail_eval_async(const std::string& expr) {
NetworkSession* net = nullptr;
{
std::lock_guard<std::mutex> lk(g_session().mu);
net = g_session().net.get();
if (!net) return;
}
// No tenemos acceso directo al CdpWs desde NetworkSession publicamente.
// Para v1.5 — Tab Detail dedicado abrira su propio CdpWs. De momento,
// el panel solo muestra info estatica + tip.
(void)expr;
}
} // anon
void render_tab_detail_panel(bool* p_open) {
if (!ImGui::Begin(TI_FILE_INFO " Tab Detail", p_open)) {
ImGui::End();
return;
}
std::string sel_id, sel_ws;
int port = 0;
{
std::lock_guard<std::mutex> lk(g_session().mu);
port = g_session().selected_port;
sel_id = g_session().selected_tab_id;
sel_ws = g_session().selected_tab_ws_url;
}
if (sel_id.empty()) {
ImGui::TextDisabled("Select a tab in the Tabs panel.");
ImGui::End();
return;
}
ImGui::Text("Browser :%d", port);
ImGui::Text("Tab id %s", sel_id.c_str());
ImGui::TextWrapped("WS %s", sel_ws.c_str());
ImGui::Separator();
// --- Pick element ---
bool active = picker_is_active();
if (active) ImGui::PushStyleColor(ImGuiCol_Button, fn_tokens::colors::primary);
if (ImGui::Button(active ? (TI_FLASK " Picking... (click to stop)")
: (TI_FLASK " Pick element"))) {
if (active) {
picker_stop();
} else {
std::string err = picker_start(port, sel_id, sel_ws);
if (!err.empty()) {
ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::error);
ImGui::TextWrapped("Pick error: %s", err.c_str());
ImGui::PopStyleColor();
}
}
}
if (active) ImGui::PopStyleColor();
ImGui::SameLine();
ImGui::TextDisabled("(injects functions/browser/cdp_pick_element_js.js via CDP)");
PickedElement last = picker_last();
if (last.valid) {
ImGui::Separator();
ImGui::TextDisabled("Last picked:");
if (ImGui::BeginChild("##picked_card", ImVec2(0, 110), true)) {
ImGui::Text("tag: %s", last.tag.c_str());
ImGui::TextWrapped("selector: %s", last.selector.c_str());
ImGui::TextWrapped("xpath: %s", last.xpath.c_str());
std::string short_text = last.text;
if (short_text.size() > 200) short_text = short_text.substr(0, 200) + "...";
ImGui::TextWrapped("text: %s", short_text.c_str());
}
ImGui::EndChild();
if (ImGui::SmallButton("Copy selector")) {
ImGui::SetClipboardText(last.selector.c_str());
}
ImGui::SameLine();
if (ImGui::SmallButton("Save to recipe (new)")) {
// Placeholder: futura integracion para crear recipe nueva con un
// unico field a partir del selector. Por ahora se copia.
ImGui::SetClipboardText(last.selector.c_str());
}
ImGui::SameLine();
if (ImGui::SmallButton("Clear")) picker_clear_last();
} else {
ImGui::TextDisabled("(no picked element yet — click 'Pick element' and click on the page)");
}
ImGui::Separator();
ImGui::TextWrapped(
"Tab Detail (HTML preview + screenshot + Runtime.evaluate REPL) llega "
"en v1.5 (issue 0003). El WebSocket esta vivo via Network panel — el "
"REPL re-utilizara la misma conexion en una proxima iteracion.");
ImGui::End();
}
// ===========================================================================
// Network panel (DevTools-like)
// ===========================================================================
namespace {
// Filtros chips (tipo recurso). Bitmask sobre ResourceType.
struct NetUiState {
char filter_text[256] = "";
bool invert_filter = false;
bool hide_data_urls = true;
bool only_blocked = false;
// chips mask. true = mostrar este tipo. start: All on.
bool type_doc = true;
bool type_css = true;
bool type_js = true;
bool type_xhr = true; // XHR + Fetch
bool type_img = true;
bool type_media = true;
bool type_font = true;
bool type_ws = true;
bool type_other = true;
bool all_types = true;
bool paused = false;
int selected_index = -1; // index en snapshot filtrado
std::string selected_id; // requestId estable
int detail_tab = 0; // 0 headers, 1 payload, 2 response, 3 cookies, 4 timing, 5 ws
// Histograma overview (request starts/s).
bool show_histogram = true;
int histogram_bins = 30;
bool reload_ignore_cache = false;
// data_table state for ##requests (persists between frames).
data_table::State dt_state;
};
NetUiState g_net_ui;
bool type_passes(const NetUiState& s, ResourceType t) {
if (s.all_types) return true;
switch (t) {
case ResourceType::Document: return s.type_doc;
case ResourceType::Stylesheet: return s.type_css;
case ResourceType::Script: return s.type_js;
case ResourceType::Image: return s.type_img;
case ResourceType::Media: return s.type_media;
case ResourceType::Font: return s.type_font;
case ResourceType::XHR:
case ResourceType::Fetch: return s.type_xhr;
case ResourceType::WebSocket:
case ResourceType::EventSource: return s.type_ws;
default: return s.type_other;
}
}
ImVec4 status_color(int status) {
if (status == 0) return fn_tokens::colors::text_muted;
if (status >= 500) return fn_tokens::colors::error;
if (status >= 400) return fn_tokens::colors::warning;
if (status >= 300) return fn_tokens::colors::info;
return fn_tokens::colors::success;
}
std::string short_name_from_url(const std::string& url) {
if (url.empty()) return "";
size_t scheme_end = url.find("://");
size_t path_start = (scheme_end == std::string::npos) ? 0 : scheme_end + 3;
size_t qmark = url.find('?', path_start);
std::string path = url.substr(path_start, qmark == std::string::npos ? std::string::npos : qmark - path_start);
size_t slash = path.find('/');
if (slash == std::string::npos) return path;
std::string after = path.substr(slash);
size_t last = after.find_last_of('/');
if (last == std::string::npos || last == after.size() - 1) {
// ends with "/" -> use host
return path.substr(0, slash);
}
return after.substr(last + 1);
}
std::string fmt_size(int64_t b) {
char buf[64];
if (b < 1024) std::snprintf(buf, sizeof(buf), "%lld B", (long long)b);
else if (b < 1024 * 1024) std::snprintf(buf, sizeof(buf), "%.1f kB", b / 1024.0);
else std::snprintf(buf, sizeof(buf), "%.2f MB", b / (1024.0 * 1024.0));
return buf;
}
std::string fmt_dur_ms(double s) {
char buf[32];
if (s <= 0) return "";
if (s < 1.0) std::snprintf(buf, sizeof(buf), "%.0f ms", s * 1000.0);
else std::snprintf(buf, sizeof(buf), "%.2f s", s);
return buf;
}
void copy_to_clipboard(const std::string& s) {
ImGui::SetClipboardText(s.c_str());
}
std::string build_curl(const NetworkRequest& r) {
std::ostringstream os;
os << "curl -X " << (r.method.empty() ? "GET" : r.method) << " '" << r.url << "'";
for (const auto& h : r.request_headers) {
if (h.name.size() >= 1 && h.name[0] == ':') continue; // pseudo h2
os << " -H '" << h.name << ": " << h.value << "'";
}
if (r.has_post_data && !r.post_data.empty()) {
os << " --data-raw '" << r.post_data << "'";
}
return os.str();
}
std::string build_fetch(const NetworkRequest& r) {
std::ostringstream os;
os << "fetch('" << r.url << "', { method: '" << (r.method.empty() ? "GET" : r.method) << "', headers: {";
bool first = true;
for (const auto& h : r.request_headers) {
if (h.name.size() >= 1 && h.name[0] == ':') continue;
if (!first) os << ", ";
first = false;
os << "'" << h.name << "': '" << h.value << "'";
}
os << "}";
if (r.has_post_data && !r.post_data.empty()) {
os << ", body: '" << r.post_data << "'";
}
os << "})";
return os.str();
}
void draw_filter_chips() {
auto chip = [](const char* label, bool* state, ImVec4 color) {
if (*state) {
ImGui::PushStyleColor(ImGuiCol_Button, color);
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, color);
}
if (ImGui::SmallButton(label)) *state = !*state;
if (*state) ImGui::PopStyleColor(2);
ImGui::SameLine();
};
if (ImGui::SmallButton(g_net_ui.all_types ? "All*" : "All")) {
g_net_ui.all_types = !g_net_ui.all_types;
}
ImGui::SameLine();
ImGui::TextDisabled("|"); ImGui::SameLine();
chip("Doc", &g_net_ui.type_doc, fn_tokens::colors::primary);
chip("CSS", &g_net_ui.type_css, fn_tokens::colors::primary);
chip("JS", &g_net_ui.type_js, fn_tokens::colors::primary);
chip("XHR", &g_net_ui.type_xhr, fn_tokens::colors::primary);
chip("Img", &g_net_ui.type_img, fn_tokens::colors::primary);
chip("Media", &g_net_ui.type_media, fn_tokens::colors::primary);
chip("Font", &g_net_ui.type_font, fn_tokens::colors::primary);
chip("WS", &g_net_ui.type_ws, fn_tokens::colors::primary);
chip("Other", &g_net_ui.type_other, fn_tokens::colors::primary);
ImGui::NewLine();
}
void draw_request_detail(const NetworkRequest& r, NetworkSession* net) {
if (ImGui::BeginTabBar("##req_detail_tabs")) {
if (ImGui::BeginTabItem("Headers")) {
ImGui::TextDisabled("General");
ImGui::Text("URL: %s", r.url.c_str());
ImGui::Text("Method: %s", r.method.c_str());
ImGui::Text("Status: %d %s", r.status, r.status_text.c_str());
ImGui::Text("Remote: %s:%d", r.remote_ip.c_str(), r.remote_port);
ImGui::Text("Protocol: %s", r.protocol.c_str());
if (r.failed) {
ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::error);
ImGui::Text("Error: %s%s", r.error_text.c_str(), r.canceled ? " (canceled)" : "");
ImGui::PopStyleColor();
}
ImGui::Separator();
ImGui::TextDisabled("Request headers (%d)", (int)r.request_headers.size());
for (const auto& h : r.request_headers) {
ImGui::Text("%s:", h.name.c_str());
ImGui::SameLine();
ImGui::TextWrapped("%s", h.value.c_str());
}
ImGui::Separator();
ImGui::TextDisabled("Response headers (%d)", (int)r.response_headers.size());
for (const auto& h : r.response_headers) {
ImGui::Text("%s:", h.name.c_str());
ImGui::SameLine();
ImGui::TextWrapped("%s", h.value.c_str());
}
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("Payload")) {
if (!r.has_post_data) {
ImGui::TextDisabled("(no request body)");
} else {
if (ImGui::SmallButton("Copy")) copy_to_clipboard(r.post_data);
ImGui::Separator();
ImGui::InputTextMultiline("##postdata", (char*)r.post_data.c_str(), r.post_data.size() + 1,
ImVec2(-1, -1), ImGuiInputTextFlags_ReadOnly);
}
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("Response")) {
// Detect JSON response (content-type: application/json).
bool is_json = false;
for (const auto& h : r.response_headers) {
std::string n = h.name; std::transform(n.begin(), n.end(), n.begin(), ::tolower);
if (n == "content-type" && h.value.find("application/json") != std::string::npos) {
is_json = true; break;
}
}
if (r.body_fetched && !r.body_text.empty()) {
if (ImGui::SmallButton("Copy")) copy_to_clipboard(r.body_text);
if (is_json) {
ImGui::SameLine();
if (ImGui::SmallButton(TI_LIST_DETAILS " Parse")) {
// Llama infer_json_rows_schema via subprocess.
static std::string g_parsed; // sticky entre frames
g_parsed.clear();
const char* code = R"PY(
import sys, os, json, traceback
root = os.environ.get('FN_REGISTRY_ROOT', '')
if not root:
print(json.dumps({"error":"FN_REGISTRY_ROOT not set"})); sys.exit(2)
for sub in ('core',):
sys.path.insert(0, os.path.join(root, 'python', 'functions', sub))
try:
from infer_json_rows_schema import infer_json_rows_schema
body = sys.stdin.read()
obj = json.loads(body)
res = infer_json_rows_schema(obj)
print(json.dumps(res if isinstance(res, dict) else {"result": res}))
except Exception as e:
print(json.dumps({"error": str(e), "trace": traceback.format_exc()})); sys.exit(1)
)PY";
std::vector<std::string> argv;
argv.push_back(py_resolve_interpreter());
argv.push_back("-c");
argv.push_back(code);
// Lanza un thread y deja log en g_net_ui.* via clipboard (simple).
std::string body = r.body_text;
std::thread([argv, body]() {
(void)argv; (void)body;
// py_run no soporta stdin todavia; usamos un archivo temporal.
// Para mantener el patch minimo: escribimos body a archivo temp,
// y pasamos su path como argv extra; el script lo lee.
char tmp[256];
std::snprintf(tmp, sizeof(tmp), "%s%snav_body_%lld.json",
#ifdef _WIN32
getenv("TEMP") ? getenv("TEMP") : ".", "\\",
#else
"/tmp", "/",
#endif
(long long)std::time(nullptr));
{
std::ofstream f(tmp, std::ios::binary);
if (f) f.write(body.data(), body.size());
}
const char* code2 = R"PY(
import sys, os, json, traceback
root = os.environ.get('FN_REGISTRY_ROOT', '')
if not root:
print(json.dumps({"error":"FN_REGISTRY_ROOT not set"})); sys.exit(2)
for sub in ('core',):
sys.path.insert(0, os.path.join(root, 'python', 'functions', sub))
try:
from infer_json_rows_schema import infer_json_rows_schema
with open(sys.argv[1], 'rb') as f: body = f.read().decode('utf-8','replace')
obj = json.loads(body)
res = infer_json_rows_schema(obj)
print(json.dumps(res if isinstance(res, dict) else {"result": res}))
except Exception as e:
print(json.dumps({"error": str(e), "trace": traceback.format_exc()})); sys.exit(1)
)PY";
std::vector<std::string> a2 = {
py_resolve_interpreter(), "-c", code2, tmp
};
PyResult pr = py_run(a2, 30000);
ImGui::SetClipboardText(pr.stdout_data.c_str());
std::remove(tmp);
}).detach();
}
ImGui::SameLine();
ImGui::TextDisabled("(result -> clipboard)");
}
ImGui::Separator();
ImGui::InputTextMultiline("##body", (char*)r.body_text.c_str(), r.body_text.size() + 1,
ImVec2(-1, -1), ImGuiInputTextFlags_ReadOnly);
} else {
if (ImGui::Button("Fetch response body")) {
if (net) net->request_body(r.id);
}
ImGui::TextDisabled("(body lazy-loaded via Network.getResponseBody)");
ImGui::TextDisabled("Limitacion conocida: matching id->requestId pendiente — body llega via WS pero no se pinta hasta v1.5.");
}
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("Cookies")) {
// Buscar Cookie / Set-Cookie en headers.
ImGui::TextDisabled("Sent (Cookie):");
for (const auto& h : r.request_headers) {
if (h.name == "Cookie" || h.name == "cookie") {
ImGui::TextWrapped("%s", h.value.c_str());
}
}
ImGui::Separator();
ImGui::TextDisabled("Set (Set-Cookie):");
for (const auto& h : r.response_headers) {
if (h.name == "Set-Cookie" || h.name == "set-cookie") {
ImGui::TextWrapped("%s", h.value.c_str());
}
}
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("Timing")) {
double dur = (r.t_finished > 0 ? r.t_finished : r.t_response) - r.t_started;
ImGui::Text("Started: %.3f s", r.t_started);
ImGui::Text("Response: %.3f s", r.t_response);
ImGui::Text("Finished: %.3f s", r.t_finished);
ImGui::Text("Total: %s", fmt_dur_ms(dur).c_str());
ImGui::Separator();
ImGui::Text("CDP timestamps");
ImGui::Text(" requestWillBeSent: %.6f", r.ts_request_will_be_sent);
ImGui::Text(" responseReceived: %.6f", r.ts_response_received);
ImGui::Text(" loadingFinished: %.6f", r.ts_loading_finished);
ImGui::Text(" loadingFailed: %.6f", r.ts_loading_failed);
ImGui::EndTabItem();
}
if (r.type == ResourceType::WebSocket && ImGui::BeginTabItem("Messages")) {
ImGui::Text("Frames: %d", (int)r.ws_frames.size());
ImGui::Separator();
// --- data_table::render for ##wsframes ---
// Cols: Dir, Op, Time, Payload
static std::vector<std::string> ws_cell_backing;
static data_table::State g_dt_wsframes;
const int WS_COLS = 4;
const int ws_rows = (int)r.ws_frames.size();
ws_cell_backing.clear();
ws_cell_backing.reserve((size_t)(ws_rows * WS_COLS));
for (const auto& wf : r.ws_frames) {
ws_cell_backing.push_back(wf.outgoing ? "send" : "recv");
// Op: opcode as string label
switch (wf.opcode) {
case 1: ws_cell_backing.push_back("text"); break;
case 2: ws_cell_backing.push_back("binary"); break;
case 8: ws_cell_backing.push_back("close"); break;
case 9: ws_cell_backing.push_back("ping"); break;
case 10: ws_cell_backing.push_back("pong"); break;
default: {
char buf[16]; std::snprintf(buf, sizeof(buf), "%d", wf.opcode);
ws_cell_backing.push_back(buf);
}
}
{ char buf[32]; std::snprintf(buf, sizeof(buf), "%.3f", wf.time);
ws_cell_backing.push_back(buf); }
ws_cell_backing.push_back(wf.payload);
}
static std::vector<const char*> ws_cell_ptrs;
ws_cell_ptrs.clear();
ws_cell_ptrs.reserve(ws_cell_backing.size());
for (const auto& s : ws_cell_backing) ws_cell_ptrs.push_back(s.c_str());
data_table::TableInput ws_tbl;
ws_tbl.name = "wsframes";
ws_tbl.headers = {"Dir", "Op", "Time", "Payload"};
ws_tbl.types = {
data_table::ColumnType::String,
data_table::ColumnType::String,
data_table::ColumnType::Float,
data_table::ColumnType::String
};
ws_tbl.cells = ws_rows > 0 ? ws_cell_ptrs.data() : nullptr;
ws_tbl.rows = ws_rows;
ws_tbl.cols = WS_COLS;
// Declarative renderers.
ws_tbl.column_specs.resize((size_t)WS_COLS);
// Col 0: Dir -> Badge
{
data_table::ColumnSpec& cs = ws_tbl.column_specs[0];
cs.id = "dir";
cs.renderer = data_table::CellRenderer::Badge;
cs.badges = {
data_table::BadgeRule{"send", "#3b82f6", "send"},
data_table::BadgeRule{"recv", "#22c55e", "recv"},
};
}
// Col 1: Op -> Badge
{
data_table::ColumnSpec& cs = ws_tbl.column_specs[1];
cs.id = "op";
cs.renderer = data_table::CellRenderer::Badge;
cs.badges = {
data_table::BadgeRule{"text", "#3b82f6", "text"},
data_table::BadgeRule{"binary", "#8b5cf6", "binary"},
data_table::BadgeRule{"close", "#ef4444", "close"},
data_table::BadgeRule{"ping", "#6b7280", "ping"},
data_table::BadgeRule{"pong", "#6b7280", "pong"},
};
}
data_table::render("##dt_wsframes", {ws_tbl}, g_dt_wsframes, false);
ImGui::EndTabItem();
}
ImGui::EndTabBar();
}
}
void render_network_toolbar(NetworkSession* net) {
// Reload page (Page.reload via CDP). Si no hay sesion, deshabilita.
if (!net) ImGui::BeginDisabled();
if (ImGui::Button(TI_REFRESH " Reload")) {
if (net) net->reload_page(g_net_ui.reload_ignore_cache);
}
if (!net) ImGui::EndDisabled();
ImGui::SameLine();
ImGui::Checkbox("Bypass cache", &g_net_ui.reload_ignore_cache);
ImGui::SameLine();
ImGui::TextDisabled("|");
ImGui::SameLine();
if (ImGui::Button(TI_TRASH " Clear")) {
if (net) net->clear_log();
g_net_ui.selected_id.clear();
g_net_ui.selected_index = -1;
}
ImGui::SameLine();
if (ImGui::Button(g_net_ui.paused ? (TI_PLAYER_PLAY " Resume") : (TI_PLAYER_PAUSE " Pause"))) {
g_net_ui.paused = !g_net_ui.paused;
}
ImGui::SameLine();
ImGui::Checkbox(TI_CHART_HISTOGRAM " Histogram", &g_net_ui.show_histogram);
ImGui::SameLine();
ImGui::TextDisabled("|");
ImGui::SameLine();
bool preserve = net ? net->preserve_log() : true;
if (ImGui::Checkbox("Preserve log", &preserve)) {
if (net) net->set_preserve_log(preserve);
}
ImGui::SameLine();
bool cache_disabled = net ? net->cache_disabled() : false;
if (ImGui::Checkbox("Disable cache", &cache_disabled)) {
if (net) net->set_cache_disabled(cache_disabled);
}
ImGui::SameLine();
ImGui::Checkbox("Hide data:", &g_net_ui.hide_data_urls);
ImGui::SameLine();
ImGui::Checkbox("Only failed", &g_net_ui.only_blocked);
ImGui::SameLine();
ImGui::TextDisabled("|");
ImGui::SameLine();
ImGui::SetNextItemWidth(220);
ImGui::InputTextWithHint("##netfilter", "filter (regex-like substring)",
g_net_ui.filter_text, sizeof(g_net_ui.filter_text));
ImGui::SameLine();
ImGui::Checkbox("Invert", &g_net_ui.invert_filter);
ImGui::SameLine();
ImGui::TextDisabled("|");
ImGui::SameLine();
if (ImGui::Button(TI_DOWNLOAD " Export HAR")) {
if (net) {
std::string har = net->export_har_json();
// Escribir junto al exe.
char path[1024];
std::snprintf(path, sizeof(path), "navegator_har_%lld.har",
(long long)std::time(nullptr));
FILE* f = std::fopen(path, "w");
if (f) { std::fwrite(har.data(), 1, har.size(), f); std::fclose(f); }
}
}
}
} // anon
void render_network_panel(bool* p_open) {
if (!ImGui::Begin(TI_ACTIVITY " Network", p_open, ImGuiWindowFlags_MenuBar)) {
ImGui::End();
return;
}
NetworkSession* net = nullptr;
int port = 0;
std::string sel_tab_id;
std::string net_err;
{
std::lock_guard<std::mutex> lk(g_session().mu);
net = g_session().net.get();
port = g_session().selected_port;
sel_tab_id = g_session().selected_tab_id;
net_err = g_session().net_error;
}
if (!net) {
if (sel_tab_id.empty()) {
ImGui::TextDisabled("Select a tab in the Tabs panel to capture network.");
} else {
ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::error);
ImGui::TextWrapped("Network session not open: %s", net_err.c_str());
ImGui::PopStyleColor();
}
ImGui::End();
return;
}
// Drenar eventos cada frame (a menos que pause).
if (!g_net_ui.paused) net->pump();
render_network_toolbar(net);
draw_filter_chips();
ImGui::Separator();
// Snapshot + filtrado.
auto reqs = net->snapshot();
// ---------- Histograma overview ----------
if (g_net_ui.show_histogram) {
std::vector<double> starts;
starts.reserve(reqs.size());
double t_max = 1.0;
for (const auto& r : reqs) {
if (r->t_started >= 0.0) {
starts.push_back(r->t_started);
if (r->t_started > t_max) t_max = r->t_started;
}
}
// Tamaño bin dinamico — bins fijo 30, rango 0..t_max.
if (ImPlot::BeginPlot("##req_histogram", ImVec2(-1, 100),
ImPlotFlags_NoTitle | ImPlotFlags_NoMouseText |
ImPlotFlags_NoBoxSelect | ImPlotFlags_NoMenus |
ImPlotFlags_NoFrame)) {
ImPlot::SetupAxes("t (s)", "reqs/bin",
ImPlotAxisFlags_NoMenus,
ImPlotAxisFlags_AutoFit | ImPlotAxisFlags_NoMenus);
ImPlot::SetupAxisLimits(ImAxis_X1, 0.0, t_max + 0.001, ImPlotCond_Always);
if (!starts.empty()) {
ImPlot::PlotHistogram("Requests/bin",
starts.data(), (int)starts.size(),
g_net_ui.histogram_bins, 1.0,
ImPlotRange(0.0, t_max + 0.001));
}
// Marcadores DOMContentLoaded / Load.
auto stats = net->stats();
if (stats.dom_content_loaded > 0) {
double x = stats.dom_content_loaded;
ImPlot::TagX(x, fn_tokens::colors::info, "DCL");
}
if (stats.load_event > 0) {
double x = stats.load_event;
ImPlot::TagX(x, fn_tokens::colors::success, "L");
}
ImPlot::EndPlot();
}
ImGui::Separator();
}
// ---------- /histograma ----------
std::string filt = g_net_ui.filter_text;
std::string filt_lower = filt;
std::transform(filt_lower.begin(), filt_lower.end(), filt_lower.begin(), ::tolower);
std::vector<std::shared_ptr<NetworkRequest>> filtered;
filtered.reserve(reqs.size());
for (auto& r : reqs) {
if (g_net_ui.hide_data_urls && r->url.compare(0, 5, "data:") == 0) continue;
if (g_net_ui.only_blocked && !r->failed) continue;
if (!type_passes(g_net_ui, r->type)) continue;
if (!filt_lower.empty()) {
std::string lu = r->url; std::transform(lu.begin(), lu.end(), lu.begin(), ::tolower);
bool match = (lu.find(filt_lower) != std::string::npos);
if (g_net_ui.invert_filter) match = !match;
if (!match) continue;
}
filtered.push_back(r);
}
// Layout: split top (table) / bottom (detail) cuando hay seleccion.
bool has_sel = !g_net_ui.selected_id.empty();
float avail_h = ImGui::GetContentRegionAvail().y;
float status_bar_h = ImGui::GetTextLineHeightWithSpacing() + 4.0f;
float top_h = has_sel ? std::max(80.0f, (avail_h - status_bar_h) * 0.55f)
: (avail_h - status_bar_h);
ImGui::BeginChild("##nettable", ImVec2(0, top_h), true);
{
// --- data_table::render for ##requests ---
// Cols: Name, Status, Method, Type, Initiator, Size, Time
// (Waterfall col omitted: requires custom ImDrawList rendering — not a data_table renderer)
static std::vector<std::string> req_cell_backing;
const int REQ_COLS = 7;
const int req_rows = (int)filtered.size();
req_cell_backing.clear();
req_cell_backing.reserve((size_t)(req_rows * REQ_COLS));
for (const auto& r : filtered) {
// Name
std::string name = short_name_from_url(r->url);
if (name.empty()) name = "(empty)";
req_cell_backing.push_back(std::move(name));
// Status: bucket string for Badge + numeric for display
if (r->status > 0) {
req_cell_backing.push_back(std::to_string(r->status));
} else if (r->failed) {
req_cell_backing.push_back("failed");
} else {
req_cell_backing.push_back("...");
}
// Method
req_cell_backing.push_back(r->method.empty() ? "GET" : r->method);
// Type
req_cell_backing.push_back(resource_type_label(r->type));
// Initiator
if (!r->initiator_url.empty()) {
req_cell_backing.push_back(short_name_from_url(r->initiator_url));
} else {
req_cell_backing.push_back(r->initiator_type);
}
// Size
if (r->from_cache) {
req_cell_backing.push_back("(cache)");
} else {
req_cell_backing.push_back(fmt_size(r->encoded_data_length));
}
// Time (duration_ms as float string for Duration renderer)
{
double dur = (r->t_finished > 0 ? r->t_finished : r->t_response) - r->t_started;
if (dur < 0) dur = 0;
char buf[32]; std::snprintf(buf, sizeof(buf), "%.3f", dur * 1000.0);
req_cell_backing.push_back(buf);
}
}
static std::vector<const char*> req_cell_ptrs;
req_cell_ptrs.clear();
req_cell_ptrs.reserve(req_cell_backing.size());
for (const auto& s : req_cell_backing) req_cell_ptrs.push_back(s.c_str());
data_table::TableInput req_tbl;
req_tbl.name = "requests";
req_tbl.headers = {"Name", "Status", "Method", "Type", "Initiator", "Size", "Time (ms)"};
req_tbl.types = {
data_table::ColumnType::String,
data_table::ColumnType::String,
data_table::ColumnType::String,
data_table::ColumnType::String,
data_table::ColumnType::String,
data_table::ColumnType::String,
data_table::ColumnType::Float,
};
req_tbl.cells = req_rows > 0 ? req_cell_ptrs.data() : nullptr;
req_tbl.rows = req_rows;
req_tbl.cols = REQ_COLS;
// Declarative renderers.
req_tbl.column_specs.resize((size_t)REQ_COLS);
// Col 1: Status -> Badge by bucket
{
data_table::ColumnSpec& cs = req_tbl.column_specs[1];
cs.id = "status";
cs.renderer = data_table::CellRenderer::Badge;
cs.badges = {
// 2xx — success green
data_table::BadgeRule{"200", "#22c55e", "200"},
data_table::BadgeRule{"201", "#22c55e", "201"},
data_table::BadgeRule{"204", "#22c55e", "204"},
data_table::BadgeRule{"206", "#22c55e", "206"},
// 3xx — info blue
data_table::BadgeRule{"301", "#3b82f6", "301"},
data_table::BadgeRule{"302", "#3b82f6", "302"},
data_table::BadgeRule{"304", "#3b82f6", "304"},
// 4xx — warning yellow
data_table::BadgeRule{"400", "#f59e0b", "400"},
data_table::BadgeRule{"401", "#f59e0b", "401"},
data_table::BadgeRule{"403", "#f59e0b", "403"},
data_table::BadgeRule{"404", "#f59e0b", "404"},
data_table::BadgeRule{"429", "#f59e0b", "429"},
// 5xx — error red
data_table::BadgeRule{"500", "#ef4444", "500"},
data_table::BadgeRule{"502", "#ef4444", "502"},
data_table::BadgeRule{"503", "#ef4444", "503"},
data_table::BadgeRule{"504", "#ef4444", "504"},
// special
data_table::BadgeRule{"failed", "#ef4444", "failed"},
data_table::BadgeRule{"...", "#6b7280", "..."},
};
}
// Col 2: Method -> Badge
{
data_table::ColumnSpec& cs = req_tbl.column_specs[2];
cs.id = "method";
cs.renderer = data_table::CellRenderer::Badge;
cs.badges = {
data_table::BadgeRule{"GET", "#22c55e", "GET"},
data_table::BadgeRule{"POST", "#3b82f6", "POST"},
data_table::BadgeRule{"PUT", "#f59e0b", "PUT"},
data_table::BadgeRule{"DELETE", "#ef4444", "DELETE"},
data_table::BadgeRule{"PATCH", "#8b5cf6", "PATCH"},
data_table::BadgeRule{"HEAD", "#6b7280", "HEAD"},
data_table::BadgeRule{"OPTIONS","#6b7280", "OPTIONS"},
};
}
// Col 6: Time -> Duration (value already in ms)
{
data_table::ColumnSpec& cs = req_tbl.column_specs[6];
cs.id = "time_ms";
cs.renderer = data_table::CellRenderer::Duration;
cs.duration_warn_ms = 1000.0f;
cs.duration_error_ms = 5000.0f;
}
std::vector<data_table::TableEvent> dt_events;
data_table::render("##dt_requests", {req_tbl}, g_net_ui.dt_state, &dt_events, /*show_chrome=*/true);
for (auto& ev : dt_events) {
if (ev.kind == data_table::TableEventKind::RowDoubleClick &&
ev.row >= 0 && ev.row < static_cast<int>(filtered.size())) {
g_net_ui.selected_id = filtered[ev.row]->id;
g_net_ui.selected_index = ev.row;
}
}
// Context menu + selection: track clicked row by matching Name col
// (handled inside data_table via row-click; URL copy available via right-click
// on cell in data_table chrome).
}
ImGui::EndChild();
// Detail pane (selected request).
if (has_sel) {
ImGui::BeginChild("##netdetail", ImVec2(0, 0), true);
std::shared_ptr<NetworkRequest> sel;
for (auto& r : filtered) if (r->id == g_net_ui.selected_id) { sel = r; break; }
if (!sel) {
for (auto& r : reqs) if (r->id == g_net_ui.selected_id) { sel = r; break; }
}
if (sel) {
draw_request_detail(*sel, net);
} else {
ImGui::TextDisabled("(request gone — log was cleared)");
}
ImGui::EndChild();
}
// Status bar
auto stats = net->stats();
ImGui::Separator();
ImGui::Text("%d requests", stats.total_requests);
ImGui::SameLine(); ImGui::TextDisabled("|"); ImGui::SameLine();
ImGui::Text("%s transferred", fmt_size(stats.transferred).c_str());
ImGui::SameLine(); ImGui::TextDisabled("|"); ImGui::SameLine();
ImGui::Text("%s resources", fmt_size(stats.resources).c_str());
ImGui::SameLine(); ImGui::TextDisabled("|"); ImGui::SameLine();
if (stats.finish_time > 0) ImGui::Text("Finish: %.2f s", stats.finish_time);
else ImGui::TextDisabled("Finish: —");
if (stats.dom_content_loaded > 0) {
ImGui::SameLine(); ImGui::TextDisabled("|"); ImGui::SameLine();
ImGui::Text("DCL: %.2f", stats.dom_content_loaded);
}
if (stats.load_event > 0) {
ImGui::SameLine();
ImGui::Text("L: %.2f", stats.load_event);
}
ImGui::SameLine(); ImGui::TextDisabled("|"); ImGui::SameLine();
{
uint64_t fin = net->ws_frames_in();
uint64_t bin = net->ws_bytes_in();
uint64_t bout = net->ws_bytes_out();
bool alive = (fin > 0);
ImGui::PushStyleColor(ImGuiCol_Text,
alive ? fn_tokens::colors::success : fn_tokens::colors::warning);
ImGui::Text("CDP: %s", alive ? "alive" : "no events");
ImGui::PopStyleColor();
ImGui::SameLine();
ImGui::TextDisabled("(%llu frames, in %s / out %s)",
(unsigned long long)fin,
fmt_size((int64_t)bin).c_str(),
fmt_size((int64_t)bout).c_str());
}
ImGui::End();
}
} // namespace navegator