8357774b86
- CMakeLists.txt - app.md - main.cpp - panels.cpp - appicon.ico - autoextract_panel.cpp - picker_state.cpp - picker_state.h - py_subprocess.cpp - py_subprocess.h - ... Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
342 lines
12 KiB
C++
342 lines
12 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);
|
|
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<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[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
|