225b59069b
Bug reportado: tabla Network vacia. Causa real: sin actividad de red en la pestaña no hay eventos Network.* — la tabla solo se llena cuando el browser realmente hace peticiones. Faltaba un boton para forzar Page.reload desde la UI y un overview visual de actividad. NetworkSession::reload_page(ignore_cache) — envia Page.reload por la WS CDP activa. Equivalente a F5 / Ctrl+Shift+R. NetworkSession::ws_frames_in/bytes_in/bytes_out — accessors a stats del CDP WebSocket subyacente, expuestos para diagnostico vivo. Network panel toolbar: - Boton "Reload" (TI_REFRESH) — invoca reload_page(). - Checkbox "Bypass cache" — controla el flag ignoreCache. - Toggle "Histogram" (TI_CHART_HISTOGRAM) — muestra/oculta overview. Histograma overview (ImPlot::PlotHistogram): - Eje X: tiempo de inicio (s) desde apertura de la sesion. - Eje Y: requests por bin (30 bins por defecto, AutoFit). - Marcadores TagX: DOMContentLoaded (DCL) y Load (L) tomados de Page.* events. - Altura fija 100px, sin titulo/menu/box-select. Status bar: - Reemplaza placeholder "WS bytes 0/0" por estado real: - "CDP: alive" en verde si frames_in>0, "CDP: no events" en warning si 0. - Cuenta de frames + bytes in/out humanizados. Util para diagnosticar: si "CDP: alive" pero tabla vacia → eventos llegan pero no estan disparando peticiones nuevas → dale a Reload. Si "no events" → WS rota o pestaña no enganchada — investigar la conexion. Tests: 6/6 siguen pasando (no se tocan los hooks de layouts). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1089 lines
42 KiB
C++
1089 lines
42 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).
|
|
|
|
#include "imgui.h"
|
|
#include "implot.h"
|
|
#include "core/icons_tabler.h"
|
|
#include "core/tokens.h"
|
|
|
|
#include "chrome_scanner.h"
|
|
#include "chrome_launcher.h"
|
|
#include "local_api.h"
|
|
#include "cdp_http.h"
|
|
#include "session_state.h"
|
|
|
|
#include <algorithm>
|
|
#include <atomic>
|
|
#include <chrono>
|
|
#include <cstdio>
|
|
#include <cstring>
|
|
#include <cstdlib>
|
|
#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;
|
|
};
|
|
|
|
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 {
|
|
const ImGuiTableFlags flags =
|
|
ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg |
|
|
ImGuiTableFlags_Resizable | ImGuiTableFlags_Sortable;
|
|
if (ImGui::BeginTable("##browsers", 7, flags)) {
|
|
ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed, 22);
|
|
ImGui::TableSetupColumn("PID", ImGuiTableColumnFlags_WidthFixed, 64);
|
|
ImGui::TableSetupColumn("Port", ImGuiTableColumnFlags_WidthFixed, 64);
|
|
ImGui::TableSetupColumn("Profile");
|
|
ImGui::TableSetupColumn("Mode", ImGuiTableColumnFlags_WidthFixed, 80);
|
|
ImGui::TableSetupColumn("user-data-dir");
|
|
ImGui::TableSetupColumn("Actions", ImGuiTableColumnFlags_WidthFixed, 130);
|
|
ImGui::TableHeadersRow();
|
|
|
|
int idx = 0;
|
|
for (const auto& inst : g_browsers.instances) {
|
|
ImGui::TableNextRow();
|
|
ImGui::PushID(idx);
|
|
ImGui::TableNextColumn();
|
|
bool is_sel = (sel_port == inst.port);
|
|
if (is_sel) {
|
|
ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::primary);
|
|
ImGui::TextUnformatted(TI_CHECK);
|
|
ImGui::PopStyleColor();
|
|
} else {
|
|
ImGui::TextUnformatted("");
|
|
}
|
|
|
|
ImGui::TableNextColumn();
|
|
ImGui::Text("%u", inst.pid);
|
|
ImGui::TableNextColumn();
|
|
ImGui::Text("%d", inst.port);
|
|
ImGui::TableNextColumn();
|
|
ImGui::TextUnformatted(inst.profile_name.empty() ? "(none)" : inst.profile_name.c_str());
|
|
ImGui::TableNextColumn();
|
|
if (inst.headless) {
|
|
ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::warning);
|
|
ImGui::TextUnformatted("headless");
|
|
ImGui::PopStyleColor();
|
|
} else {
|
|
ImGui::TextUnformatted("visible");
|
|
}
|
|
ImGui::TableNextColumn();
|
|
ImGui::TextUnformatted(inst.user_data_dir.c_str());
|
|
ImGui::TableNextColumn();
|
|
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::EndTable();
|
|
}
|
|
}
|
|
ImGui::End();
|
|
}
|
|
|
|
// ===========================================================================
|
|
// Tabs panel
|
|
// ===========================================================================
|
|
namespace {
|
|
|
|
struct TabsUiState {
|
|
std::atomic<bool> refreshing{false};
|
|
char new_url_input[1024] = "https://example.com";
|
|
char filter[128] = "";
|
|
};
|
|
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;
|
|
}
|
|
|
|
std::string filter_str = g_tabs_ui.filter;
|
|
std::transform(filter_str.begin(), filter_str.end(), filter_str.begin(), ::tolower);
|
|
|
|
const ImGuiTableFlags flags =
|
|
ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg |
|
|
ImGuiTableFlags_Resizable | ImGuiTableFlags_ScrollY;
|
|
if (ImGui::BeginTable("##tabs", 6, flags, ImVec2(0, 0))) {
|
|
ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed, 22);
|
|
ImGui::TableSetupColumn("Type", ImGuiTableColumnFlags_WidthFixed, 70);
|
|
ImGui::TableSetupColumn("Title");
|
|
ImGui::TableSetupColumn("URL");
|
|
ImGui::TableSetupColumn("Att.", ImGuiTableColumnFlags_WidthFixed, 40);
|
|
ImGui::TableSetupColumn("Actions", ImGuiTableColumnFlags_WidthFixed, 200);
|
|
ImGui::TableHeadersRow();
|
|
|
|
int idx = 0;
|
|
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;
|
|
}
|
|
bool is_sel = (sel_tab_id == t.id);
|
|
ImGui::TableNextRow();
|
|
ImGui::PushID(idx);
|
|
ImGui::TableNextColumn();
|
|
if (is_sel) {
|
|
ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::primary);
|
|
ImGui::TextUnformatted(TI_CHECK);
|
|
ImGui::PopStyleColor();
|
|
}
|
|
ImGui::TableNextColumn();
|
|
ImGui::TextUnformatted(t.type.c_str());
|
|
ImGui::TableNextColumn();
|
|
ImGui::TextUnformatted(t.title.empty() ? "(no title)" : t.title.c_str());
|
|
ImGui::TableNextColumn();
|
|
ImGui::TextUnformatted(t.url.c_str());
|
|
ImGui::TableNextColumn();
|
|
ImGui::TextUnformatted(t.attached ? "yes" : "");
|
|
ImGui::TableNextColumn();
|
|
if (ImGui::SmallButton("Select")) {
|
|
if (!t.ws_url.empty()) g_session().select_tab(t.id, t.ws_url);
|
|
}
|
|
ImGui::SameLine();
|
|
if (ImGui::SmallButton("Focus")) {
|
|
std::string id = t.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 = t.id;
|
|
std::thread([port, id]{
|
|
cdp_close_tab(port, id, nullptr);
|
|
refresh_tabs_async(port);
|
|
}).detach();
|
|
if (is_sel) g_session().select_tab("", "");
|
|
}
|
|
ImGui::PopID();
|
|
++idx;
|
|
}
|
|
ImGui::EndTable();
|
|
}
|
|
|
|
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();
|
|
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;
|
|
};
|
|
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")) {
|
|
if (r.body_fetched && !r.body_text.empty()) {
|
|
if (ImGui::SmallButton("Copy")) copy_to_clipboard(r.body_text);
|
|
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();
|
|
const ImGuiTableFlags f = ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_ScrollY;
|
|
if (ImGui::BeginTable("##wsframes", 4, f, ImVec2(-1, -1))) {
|
|
ImGui::TableSetupColumn("Dir", ImGuiTableColumnFlags_WidthFixed, 30);
|
|
ImGui::TableSetupColumn("Op", ImGuiTableColumnFlags_WidthFixed, 30);
|
|
ImGui::TableSetupColumn("Time", ImGuiTableColumnFlags_WidthFixed, 80);
|
|
ImGui::TableSetupColumn("Payload");
|
|
ImGui::TableHeadersRow();
|
|
for (const auto& wf : r.ws_frames) {
|
|
ImGui::TableNextRow();
|
|
ImGui::TableNextColumn();
|
|
ImGui::TextUnformatted(wf.outgoing ? TI_ARROW_UP : TI_ARROW_DOWN);
|
|
ImGui::TableNextColumn();
|
|
ImGui::Text("%d", wf.opcode);
|
|
ImGui::TableNextColumn();
|
|
ImGui::Text("%.3f", wf.time);
|
|
ImGui::TableNextColumn();
|
|
ImGui::TextWrapped("%s", wf.payload.c_str());
|
|
}
|
|
ImGui::EndTable();
|
|
}
|
|
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);
|
|
{
|
|
const ImGuiTableFlags flags =
|
|
ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg |
|
|
ImGuiTableFlags_Resizable | ImGuiTableFlags_ScrollY |
|
|
ImGuiTableFlags_Sortable;
|
|
if (ImGui::BeginTable("##requests", 8, flags)) {
|
|
ImGui::TableSetupScrollFreeze(0, 1);
|
|
ImGui::TableSetupColumn("Name");
|
|
ImGui::TableSetupColumn("Status", ImGuiTableColumnFlags_WidthFixed, 60);
|
|
ImGui::TableSetupColumn("Method", ImGuiTableColumnFlags_WidthFixed, 70);
|
|
ImGui::TableSetupColumn("Type", ImGuiTableColumnFlags_WidthFixed, 70);
|
|
ImGui::TableSetupColumn("Initiator");
|
|
ImGui::TableSetupColumn("Size", ImGuiTableColumnFlags_WidthFixed, 90);
|
|
ImGui::TableSetupColumn("Time", ImGuiTableColumnFlags_WidthFixed, 70);
|
|
ImGui::TableSetupColumn("Waterfall",ImGuiTableColumnFlags_WidthStretch);
|
|
ImGui::TableHeadersRow();
|
|
|
|
// Para waterfall: necesitamos rango total.
|
|
double t_min = 0.0, t_max = 0.0;
|
|
for (const auto& r : filtered) {
|
|
t_max = std::max(t_max, std::max(r->t_finished, r->t_response));
|
|
}
|
|
if (t_max < 1.0) t_max = 1.0;
|
|
|
|
for (size_t i = 0; i < filtered.size(); ++i) {
|
|
const auto& r = filtered[i];
|
|
ImGui::TableNextRow();
|
|
ImGui::PushID((int)i);
|
|
bool is_sel = (g_net_ui.selected_id == r->id);
|
|
|
|
ImGui::TableNextColumn();
|
|
std::string name = short_name_from_url(r->url);
|
|
if (name.empty()) name = "(empty)";
|
|
if (ImGui::Selectable(name.c_str(), is_sel,
|
|
ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_AllowDoubleClick)) {
|
|
g_net_ui.selected_id = r->id;
|
|
g_net_ui.selected_index = (int)i;
|
|
}
|
|
if (ImGui::BeginPopupContextItem("##rowctx")) {
|
|
if (ImGui::MenuItem("Copy URL")) copy_to_clipboard(r->url);
|
|
if (ImGui::MenuItem("Copy as cURL")) copy_to_clipboard(build_curl(*r));
|
|
if (ImGui::MenuItem("Copy as fetch")) copy_to_clipboard(build_fetch(*r));
|
|
ImGui::Separator();
|
|
if (ImGui::MenuItem("Block URL (TODO)")) {}
|
|
ImGui::EndPopup();
|
|
}
|
|
ImGui::TableNextColumn();
|
|
if (r->status > 0) {
|
|
ImGui::PushStyleColor(ImGuiCol_Text, status_color(r->status));
|
|
ImGui::Text("%d", r->status);
|
|
ImGui::PopStyleColor();
|
|
} else if (r->failed) {
|
|
ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::error);
|
|
ImGui::TextUnformatted("(failed)");
|
|
ImGui::PopStyleColor();
|
|
} else {
|
|
ImGui::TextDisabled("...");
|
|
}
|
|
ImGui::TableNextColumn();
|
|
ImGui::TextUnformatted(r->method.c_str());
|
|
ImGui::TableNextColumn();
|
|
ImGui::TextUnformatted(resource_type_label(r->type));
|
|
ImGui::TableNextColumn();
|
|
if (!r->initiator_url.empty()) {
|
|
ImGui::TextUnformatted(short_name_from_url(r->initiator_url).c_str());
|
|
} else {
|
|
ImGui::TextDisabled("%s", r->initiator_type.c_str());
|
|
}
|
|
ImGui::TableNextColumn();
|
|
if (r->from_cache) {
|
|
ImGui::TextDisabled("(cache)");
|
|
} else {
|
|
ImGui::TextUnformatted(fmt_size(r->encoded_data_length).c_str());
|
|
}
|
|
ImGui::TableNextColumn();
|
|
{
|
|
double dur = (r->t_finished > 0 ? r->t_finished : r->t_response) - r->t_started;
|
|
if (dur < 0) dur = 0;
|
|
ImGui::TextUnformatted(fmt_dur_ms(dur).c_str());
|
|
}
|
|
ImGui::TableNextColumn();
|
|
{
|
|
// mini waterfall bar.
|
|
ImVec2 cmin = ImGui::GetCursorScreenPos();
|
|
ImVec2 avail = ImVec2(ImGui::GetContentRegionAvail().x, ImGui::GetTextLineHeight());
|
|
ImDrawList* dl = ImGui::GetWindowDrawList();
|
|
double a = r->t_started / t_max;
|
|
double b = (r->t_finished > 0 ? r->t_finished : r->t_response) / t_max;
|
|
if (b < a) b = a;
|
|
if (b > 1) b = 1;
|
|
ImVec2 p1(cmin.x + avail.x * (float)a, cmin.y + 2);
|
|
ImVec2 p2(cmin.x + avail.x * (float)b, cmin.y + avail.y - 2);
|
|
if (p2.x < p1.x + 2) p2.x = p1.x + 2;
|
|
ImU32 col = ImGui::ColorConvertFloat4ToU32(
|
|
r->failed ? fn_tokens::colors::error :
|
|
(r->finished ? fn_tokens::colors::primary : fn_tokens::colors::warning));
|
|
dl->AddRectFilled(p1, p2, col, 2.0f);
|
|
ImGui::Dummy(avail);
|
|
}
|
|
ImGui::PopID();
|
|
}
|
|
ImGui::EndTable();
|
|
}
|
|
}
|
|
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
|