chore: sync from fn-registry agent
This commit is contained in:
+264
@@ -0,0 +1,264 @@
|
||||
// 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
|
||||
Reference in New Issue
Block a user