Files
navegator_dashboard/panels.cpp
T
2026-05-09 18:11:21 +02:00

265 lines
9.0 KiB
C++

// Panels v0:
// - Browsers: scan + spawn + kill (funcional)
// - Tabs / Tab Detail / Network: stubs anunciando v1.
#include "imgui.h"
#include "core/icons_tabler.h"
#include "core/tokens.h"
#include "chrome_scanner.h"
#include "chrome_launcher.h"
#include "local_api.h"
#include <atomic>
#include <chrono>
#include <cstdio>
#include <mutex>
#include <string>
#include <thread>
#include <vector>
#ifdef _WIN32
# define WIN32_LEAN_AND_MEAN
# include <windows.h>
#endif
namespace navegator {
// ---------- Estado compartido del panel Browsers ----------
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};
std::atomic<int> selected{-1};
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) {
// Resolver USERPROFILE si esta disponible.
#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
}
} // namespace
// ---------- Browsers panel ----------
void render_browsers_panel(bool* p_open) {
if (!ImGui::Begin(TI_BROWSER " Browsers", p_open)) {
ImGui::End();
return;
}
// Auto-rescan cada 2s.
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();
}
}
// API status badge.
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());
ImGui::SameLine();
ImGui::TextDisabled("|");
ImGui::SameLine();
} else {
ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::error);
ImGui::TextUnformatted("API: down");
ImGui::PopStyleColor();
ImGui::SameLine();
ImGui::TextDisabled("|");
ImGui::SameLine();
}
// Toolbar.
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;
// Forzar rescan inmediato (con pequeño delay para que Chrome aparezca en CIM).
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();
// Tabla.
std::lock_guard<std::mutex> lk(g_browsers.mu);
// Anti-flicker: solo mostrar "Scanning..." en el primer scan (cuando aun
// no tenemos datos). Una vez tenemos al menos un resultado, mantener el
// empty-state estable; el badge "(reqs:N)" + el rescan async siguen
// corriendo en background sin tocar la UI.
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", 6, flags)) {
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, 110);
ImGui::TableHeadersRow();
int idx = 0;
for (const auto& inst : g_browsers.instances) {
ImGui::TableNextRow();
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();
ImGui::PushID(idx);
if (ImGui::SmallButton("Kill")) {
if (!inst.user_data_dir.empty()) {
kill_chromes_by_userdata(inst.user_data_dir);
}
std::thread([]{
std::this_thread::sleep_for(std::chrono::milliseconds(500));
rescan_async();
}).detach();
}
ImGui::SameLine();
if (ImGui::SmallButton("Inspect")) {
g_browsers.selected = idx;
}
ImGui::PopID();
++idx;
}
ImGui::EndTable();
}
}
ImGui::End();
}
// ---------- Tabs panel (stub) ----------
void render_tabs_panel(bool* p_open) {
if (!ImGui::Begin(TI_LIST " Tabs", p_open)) {
ImGui::End();
return;
}
ImGui::TextDisabled("Coming in v1");
ImGui::TextWrapped("Listara las pestañas de la instancia seleccionada en Browsers via "
"CDP /json y permitira navigate/close/focus.");
ImGui::End();
}
// ---------- Tab Detail panel (stub) ----------
void render_tab_detail_panel(bool* p_open) {
if (!ImGui::Begin(TI_FILE_INFO " Tab Detail", p_open)) {
ImGui::End();
return;
}
ImGui::TextDisabled("Coming in v1");
ImGui::TextWrapped("HTML preview, screenshot live y REPL Runtime.evaluate sobre la pestaña "
"seleccionada.");
ImGui::End();
}
// ---------- Network panel (stub) ----------
void render_network_panel(bool* p_open) {
if (!ImGui::Begin(TI_ACTIVITY " Network", p_open)) {
ImGui::End();
return;
}
ImGui::TextDisabled("Coming in v1");
ImGui::TextWrapped("Log de peticiones HTTP/WS en vivo via CDP Network.* events. "
"Headers, body, timing, filtros.");
ImGui::End();
}
} // namespace navegator