Files
navegator_dashboard/main.cpp
T
egutierrez df8c0864f6 fix(layouts): persist panel visibility por layout + e2e tests
Bug: al pulsar un layout guardado el dock layout se aplicaba pero los paneles
ocultos quedaban con dock node vacio. Causa: el INI de ImGui solo guarda
posicion/dock; la visibilidad (`show_*` bools) es estado puro de la app y no
se restauraba al apply.

Fix:
- Tomar control del menu Layouts (cfg.auto_layouts=false). Abrir LayoutStorage
  propio + segunda tabla `panel_visibility` en la misma layouts.db (CREATE
  TABLE IF NOT EXISTS, aditivo, no rompe layouts existentes).
- on_save: capture_panel_state() serializa show_* a JSON y se persiste junto
  al INI bajo el mismo nombre.
- on_apply: marca pending INI + carga state JSON pendiente.
- on_reset: clear INI + open_all_panels (reabre Browsers/Tabs/TabDetail/Network).
- on_delete: borra fila imgui_layouts + sidecar.
- drain_layout_pending() (llamado desde render() cada frame) aplica
  LoadIniSettingsFromMemory + apply_panel_state. Fallback: si layout no tiene
  sidecar (back-compat con layouts antiguos) abre todos los paneles.

Refactor:
- main.cpp: render() ya no es static — necesario para que el test harness
  reuse la misma funcion. int main() guardado tras `#ifndef FN_TEST_BUILD`.
- show_* bools y k_panels movidos al namespace navegator (extern para tests).
- dashboard_state.h: nuevo header expone show_*, setup_layouts(),
  teardown_layouts(), capture/apply_panel_state, open_all_panels y los
  hooks layout_save/apply/delete/reset + drain_layout_pending para tests.

Tests (Dear ImGui Test Engine, opt-in via -DFN_BUILD_TESTS=ON):
  tests/navegator_dashboard_tests.cpp — 6 tests, todos pasan:
  1. panel_state_roundtrip            — capture/apply JSON simetrico.
  2. open_all_panels_marks_main_visible.
  3. save_hide_apply_restores_visibility (FIX BUG).
  4. two_layouts_swap_visibility       — minimal vs full.
  5. reset_opens_all_main_panels.
  6. legacy_layout_fallback_opens_all  — sin sidecar.

Build/run:
  cmake -B cpp/build/windows_tests -S cpp \
        -DCMAKE_TOOLCHAIN_FILE=$(pwd)/cpp/toolchains/mingw-w64.cmake \
        -DFN_BUILD_TESTS=ON
  cmake --build cpp/build/windows_tests --target navegator_dashboard_tests
  Deploy + run via cmd.exe -> 6/6 tests passed.

CMakeLists.txt: añade target navegator_dashboard_tests bajo if(FN_BUILD_TESTS),
linka mismas libs que prod + define FN_TEST_BUILD para que main.cpp no
duplique main(). WIN32_EXECUTABLE FALSE para ver stdout en consola.

Issue 0003 (sub-issue del roadmap navegator_dashboard 0001).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 13:09:50 +02:00

294 lines
9.4 KiB
C++

// 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 <sqlite3.h>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <string>
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);
// ---------- Visibilidad de paneles -----------------------------------------
bool show_browsers = true;
bool show_tabs = true;
bool show_tab_detail = false;
bool show_network = false;
bool show_agent = 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},
};
} // 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<const char*>(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[256];
std::snprintf(buf, sizeof(buf),
"{\"browsers\":%d,\"tabs\":%d,\"tab_detail\":%d,\"network\":%d,\"agent\":%d}",
show_browsers ? 1 : 0,
show_tabs ? 1 : 0,
show_tab_detail ? 1 : 0,
show_network ? 1 : 0,
show_agent ? 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);
}
void open_all_panels() {
show_browsers = show_tabs = show_tab_detail = show_network = true;
// agent es opt-in: ni save/apply ni reset lo abren por defecto.
}
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;
}
};
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;
}
};
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();
};
g_layout_cb.on_reset = []() {
ImGui::LoadIniSettingsFromMemory("", 0);
ImGui::GetIO().WantSaveIniSettings = true;
open_all_panels();
g_layout_cb.active_name.clear();
};
cfg.auto_layouts = false;
cfg.layouts_cb = &g_layout_cb;
}
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;
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;
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 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() {
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) -----------------
void render() {
using namespace navegator;
// Aplica layout pendiente ANTES de abrir Begin() de cualquier panel.
// Si la app gestiona su propio LayoutStorage (cfg.auto_layouts=false), el
// framework no llama apply_pending — lo hacemos aqui.
drain_layout_pending();
ImGui::DockSpaceOverViewport(0, ImGui::GetMainViewport());
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);
}
#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;
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