// navegator_dashboard — cuadro de mandos para gestionar instancias Chrome con CDP. // // v0.3.x: Browsers + Tabs + Tab Detail (placeholder) + Network (DevTools-like) + Agent. // El menu Layouts persiste imgui.ini + visibilidad de paneles por nombre. // Ver projects/navegator/apps/navegator_dashboard/app.md. #include "app_base.h" #include "core/icons_tabler.h" #include "core/layout_storage.h" #include "core/layouts_menu.h" #include "core/panel_menu.h" #include "imgui.h" #include "local_api.h" #include "agent.h" #include "dashboard_state.h" #include #include #include #include #include namespace navegator { void render_browsers_panel(bool* p_open); void render_tabs_panel(bool* p_open); void render_tab_detail_panel(bool* p_open); void render_network_panel(bool* p_open); void render_autoextract_panel(bool* p_open); void render_recipes_panel(bool* p_open); // ---------- Visibilidad de paneles ----------------------------------------- bool show_browsers = true; bool show_tabs = true; bool show_tab_detail = false; bool show_network = false; bool show_agent = false; bool show_autoextract = false; bool show_recipes = false; namespace { constexpr fn_ui::PanelToggle k_panels[] = { {"Browsers", "Ctrl+1", &show_browsers}, {"Tabs", "Ctrl+2", &show_tabs}, {"Tab Detail", "Ctrl+3", &show_tab_detail}, {"Network", "Ctrl+4", &show_network}, {"Agent", "Ctrl+5", &show_agent}, {"AutoExtract", "Ctrl+6", &show_autoextract}, {"Recipes", "Ctrl+7", &show_recipes}, }; } // anon // ---------- Layouts: storage + sidecar de visibilidad ---------------------- namespace { sqlite3* g_extra_db = nullptr; fn_ui::LayoutStorage* g_layouts = nullptr; fn_ui::LayoutCallbacks g_layout_cb; std::string g_pending_panel_state; bool extra_open(const char* db_path) { if (sqlite3_open(db_path, &g_extra_db) != SQLITE_OK) { if (g_extra_db) { sqlite3_close(g_extra_db); g_extra_db = nullptr; } return false; } const char* sql = "CREATE TABLE IF NOT EXISTS panel_visibility (" " layout_name TEXT PRIMARY KEY," " state_json TEXT NOT NULL," " updated_at INTEGER NOT NULL" ");"; char* err = nullptr; int rc = sqlite3_exec(g_extra_db, sql, nullptr, nullptr, &err); if (err) sqlite3_free(err); return rc == SQLITE_OK; } bool extra_save(const std::string& name, const std::string& json) { if (!g_extra_db) return false; const char* sql = "INSERT INTO panel_visibility (layout_name, state_json, updated_at) " "VALUES (?, ?, strftime('%s','now')) " "ON CONFLICT(layout_name) DO UPDATE SET " " state_json = excluded.state_json, " " updated_at = excluded.updated_at;"; sqlite3_stmt* st = nullptr; if (sqlite3_prepare_v2(g_extra_db, sql, -1, &st, nullptr) != SQLITE_OK) return false; sqlite3_bind_text(st, 1, name.c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_text(st, 2, json.c_str(), -1, SQLITE_TRANSIENT); int rc = sqlite3_step(st); sqlite3_finalize(st); return rc == SQLITE_DONE; } std::string extra_load(const std::string& name) { std::string out; if (!g_extra_db) return out; const char* sql = "SELECT state_json FROM panel_visibility WHERE layout_name = ?;"; sqlite3_stmt* st = nullptr; if (sqlite3_prepare_v2(g_extra_db, sql, -1, &st, nullptr) != SQLITE_OK) return out; sqlite3_bind_text(st, 1, name.c_str(), -1, SQLITE_TRANSIENT); if (sqlite3_step(st) == SQLITE_ROW) { const unsigned char* t = sqlite3_column_text(st, 0); if (t) out = reinterpret_cast(t); } sqlite3_finalize(st); return out; } void extra_del(const std::string& name) { if (!g_extra_db) return; const char* sql = "DELETE FROM panel_visibility WHERE layout_name = ?;"; sqlite3_stmt* st = nullptr; if (sqlite3_prepare_v2(g_extra_db, sql, -1, &st, nullptr) != SQLITE_OK) return; sqlite3_bind_text(st, 1, name.c_str(), -1, SQLITE_TRANSIENT); sqlite3_step(st); sqlite3_finalize(st); } } // anon // ---------- API publica para tests + main ---------------------------------- std::string capture_panel_state() { char buf[384]; std::snprintf(buf, sizeof(buf), "{\"browsers\":%d,\"tabs\":%d,\"tab_detail\":%d,\"network\":%d,\"agent\":%d," "\"autoextract\":%d,\"recipes\":%d}", show_browsers ? 1 : 0, show_tabs ? 1 : 0, show_tab_detail ? 1 : 0, show_network ? 1 : 0, show_agent ? 1 : 0, show_autoextract ? 1 : 0, show_recipes ? 1 : 0); return buf; } void apply_panel_state(const std::string& json) { auto pull = [&](const char* key, bool def) -> bool { std::string needle = std::string("\"") + key + "\":"; auto p = json.find(needle); if (p == std::string::npos) return def; p += needle.size(); if (p >= json.size()) return def; return json[p] == '1' || (json.compare(p, 4, "true") == 0); }; show_browsers = pull("browsers", true); show_tabs = pull("tabs", true); show_tab_detail = pull("tab_detail", true); show_network = pull("network", true); show_agent = pull("agent", false); show_autoextract = pull("autoextract", false); show_recipes = pull("recipes", false); } void open_all_panels() { show_browsers = show_tabs = show_tab_detail = show_network = true; // agent / autoextract / recipes son opt-in: no se reabren con Reset. } void setup_layouts(fn::AppConfig& cfg) { if (g_layouts) return; // idempotente g_layouts = fn_ui::layout_storage_open(fn::local_path("layouts.db")); extra_open(fn::local_path("layouts.db")); if (!g_layouts) return; fn_ui::layout_storage_make_callbacks(g_layouts, g_layout_cb); g_layout_cb.on_save = [](const std::string& name) { if (fn_ui::layout_storage_save(g_layouts, name)) { extra_save(name, capture_panel_state()); g_layout_cb.active_name = name; fn_ui::layout_storage_set_last_active(g_layouts, name); } }; g_layout_cb.on_apply = [](const std::string& name) { if (fn_ui::layout_storage_apply(g_layouts, name)) { g_pending_panel_state = extra_load(name); g_layout_cb.active_name = name; fn_ui::layout_storage_set_last_active(g_layouts, name); } }; g_layout_cb.on_delete = [](const std::string& name) { fn_ui::layout_storage_delete(g_layouts, name); extra_del(name); if (g_layout_cb.active_name == name) { g_layout_cb.active_name.clear(); fn_ui::layout_storage_set_last_active(g_layouts, ""); } }; g_layout_cb.on_reset = []() { ImGui::LoadIniSettingsFromMemory("", 0); ImGui::GetIO().WantSaveIniSettings = true; open_all_panels(); g_layout_cb.active_name.clear(); fn_ui::layout_storage_set_last_active(g_layouts, ""); }; cfg.auto_layouts = false; cfg.layouts_cb = &g_layout_cb; // pre_frame se ejecuta entre NewFrame y menubar/auto-dockspace. Es el // punto correcto para LoadIniSettingsFromMemory (apply_pending llama a // esa funcion). Si lo hacemos mid-frame dentro de render_fn los paneles // docked aparecen flotantes hasta el siguiente ciclo. cfg.pre_frame = []() { drain_layout_pending(); }; // Restore-on-open: si hay layout activo persistido y el sidecar de // visibilidad existe, lo dejamos pendiente. drain_layout_pending lo // recoge en el primer frame. std::string last = fn_ui::layout_storage_get_last_active(g_layouts); if (!last.empty() && fn_ui::layout_storage_apply(g_layouts, last)) { g_pending_panel_state = extra_load(last); g_layout_cb.active_name = last; } } bool layout_save(const std::string& name) { if (!g_layouts) return false; if (!fn_ui::layout_storage_save(g_layouts, name)) return false; extra_save(name, capture_panel_state()); g_layout_cb.active_name = name; fn_ui::layout_storage_set_last_active(g_layouts, name); return true; } bool layout_apply(const std::string& name) { if (!g_layouts) return false; if (!fn_ui::layout_storage_apply(g_layouts, name)) return false; g_pending_panel_state = extra_load(name); g_layout_cb.active_name = name; fn_ui::layout_storage_set_last_active(g_layouts, name); return true; } bool layout_delete(const std::string& name) { if (!g_layouts) return false; bool ok = fn_ui::layout_storage_delete(g_layouts, name); extra_del(name); if (g_layout_cb.active_name == name) g_layout_cb.active_name.clear(); return ok; } void layout_reset() { ImGui::LoadIniSettingsFromMemory("", 0); ImGui::GetIO().WantSaveIniSettings = true; open_all_panels(); g_layout_cb.active_name.clear(); } std::string active_layout_name() { return g_layout_cb.active_name; } std::string drain_layout_pending() { if (!g_layouts) return ""; std::string applied = fn_ui::layout_storage_apply_pending(g_layouts); if (!applied.empty()) { g_layout_cb.active_name = applied; if (!g_pending_panel_state.empty()) { apply_panel_state(g_pending_panel_state); g_pending_panel_state.clear(); } else { open_all_panels(); } ImGui::GetIO().WantSaveIniSettings = true; } return applied; } void teardown_layouts() { // teardown_layouts corre tras fn::run_app, cuando ImGui::DestroyContext // ya se ejecuto. SaveIniSettingsToMemory aqui crashea, asi que NO se hace // save-on-close. La consistencia reapertura→layout activo se mantiene via // restore-on-open: setup_layouts lee last_active y reaplica el blob del // slot. Si el usuario quiere capturar tweaks puntuales debe hacer "Save". if (g_extra_db) { sqlite3_close(g_extra_db); g_extra_db = nullptr; } if (g_layouts) { fn_ui::layout_storage_close(g_layouts); g_layouts = nullptr; } g_pending_panel_state.clear(); g_layout_cb = {}; } } // namespace navegator // ---------- Render principal (linkable desde test harness) ----------------- // // drain_layout_pending() se cablea via cfg.pre_frame (ver setup_layouts) — se // ejecuta entre NewFrame y app_menubar/auto-dockspace. ImGui requiere que // LoadIniSettingsFromMemory ocurra ANTES de cualquier Begin() del frame para // restaurar dock state correctamente; mid-frame las ventanas docked vuelven // a aparecer flotantes. // // Auto-dockspace lo provee el framework (cfg.auto_dockspace=true por default) // — no llamamos DockSpaceOverViewport aqui para no duplicar. void render() { using namespace navegator; if (show_browsers) render_browsers_panel(&show_browsers); if (show_tabs) render_tabs_panel(&show_tabs); if (show_tab_detail) render_tab_detail_panel(&show_tab_detail); if (show_network) render_network_panel(&show_network); if (show_agent) app_agent::chat_render(&show_agent); if (show_autoextract) render_autoextract_panel(&show_autoextract); if (show_recipes) render_recipes_panel(&show_recipes); } #ifndef FN_TEST_BUILD int main() { fn::AppConfig cfg; cfg.title = "Navegator Dashboard"; cfg.about = { "Navegator Dashboard", "0.3.1", "Cuadro de mandos Chrome (CDP) — Browsers + Tabs + Network DevTools-like + agente." }; cfg.panels = navegator::k_panels; cfg.panel_count = sizeof(navegator::k_panels) / sizeof(navegator::k_panels[0]); cfg.init_gl_loader = false; cfg.log = {"navegator_dashboard.log", 1}; navegator::setup_layouts(cfg); navegator::start_api_server(19333); std::string app_dir = fn::exe_dir(); std::string fake_app_db = std::string(fn::local_dir()) + "/_chat.db"; app_agent::chat_init("", fake_app_db.c_str(), app_dir.c_str()); int rc = fn::run_app(cfg, render); navegator::teardown_layouts(); return rc; } #endif