// odr_console — lanzador GUI de funciones del registry para recolectar datos online. // MVP: panel launcher + placeholders. Ver issue 0066. #include "app_base.h" #include "imgui.h" #include "core/app_menubar.h" #include "core/app_about.h" #include "core/app_settings.h" #include "core/icon_font.h" #include "core/icons_tabler.h" #include "core/tokens.h" #include "core/logger.h" #include "data_registry.h" #include "data_collectors.h" #include "runner.h" #include #include #include #include #include #include #include static OdrRegistry g_registry; static std::string g_db_path; static std::string g_app_dir; // dir de odr_console (collectors/, ops db) static std::string g_python_exe; // resolved al arrancar static std::string g_registry_root; // FN_REGISTRY_ROOT static std::string g_ops_db_path; // /operations.db static char g_search_buf[256] = ""; static std::vector g_results; static int g_selected = -1; static std::vector g_collectors; static int g_coll_selected = -1; static int g_coll_param_limit = 30; static std::string g_last_run_summary; // texto status del ultimo run static bool g_running = false; static void do_search() { g_results.clear(); g_selected = -1; if (g_search_buf[0] == '\0') { registry_list_recent(g_registry, 50, g_results); } else { registry_search(g_registry, g_search_buf, 50, g_results); } } static bool g_show_launcher = true; static bool g_show_collectors = true; static bool g_show_jobs = true; static bool g_show_datasets = true; static void run_selected_collector() { if (g_coll_selected < 0 || g_coll_selected >= (int)g_collectors.size()) return; const Collector& c = g_collectors[g_coll_selected]; if (!c.has_run) return; std::ostringstream ctx; ctx << "{" << "\"ops_db_path\":\"" << g_ops_db_path << "\"," << "\"app_dir\":\"" << g_app_dir << "\"," << "\"registry_root\":\"" << g_registry_root << "\"," << "\"params\":{\"limit\":" << g_coll_param_limit << "}" << "}"; std::string tmp_dir = std::string(fn::local_path("runner_tmp")); g_running = true; auto res = odr::run_collector(g_python_exe, c.run_py, tmp_dir, ctx.str()); g_running = false; std::ostringstream s; s << "exit=" << res.exit_code << " ms=" << res.duration_ms << "\nstdout: " << res.stdout_str << "\nstderr (last):\n"; // Solo ultimas 10 lineas de stderr para no inundar UI. std::vector lines; std::istringstream ss(res.stderr_str); std::string ln; while (std::getline(ss, ln)) lines.push_back(ln); int start = (int)lines.size() - 10; if (start < 0) start = 0; for (int i = start; i < (int)lines.size(); ++i) s << lines[i] << "\n"; g_last_run_summary = s.str(); } static void draw_collectors() { if (!g_show_collectors) return; if (!ImGui::Begin(TI_PLAYER_PLAY " Collectors", &g_show_collectors)) { ImGui::End(); return; } if (ImGui::Button("Refresh")) { std::string root = g_app_dir + "/collectors"; collectors_discover(root, g_collectors); g_coll_selected = -1; } ImGui::SameLine(); ImGui::TextDisabled("%zu collectors", g_collectors.size()); if (g_collectors.empty()) { ImGui::TextDisabled("No collectors found in %s/collectors/", g_app_dir.c_str()); ImGui::End(); return; } if (ImGui::BeginTable("##collectors", 3, ImGuiTableFlags_RowBg | ImGuiTableFlags_Borders | ImGuiTableFlags_Resizable)) { ImGui::TableSetupColumn("ID", ImGuiTableColumnFlags_WidthFixed, 180); ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_WidthFixed, 200); ImGui::TableSetupColumn("Description", ImGuiTableColumnFlags_WidthStretch); ImGui::TableHeadersRow(); for (int i = 0; i < (int)g_collectors.size(); ++i) { const auto& c = g_collectors[i]; ImGui::TableNextRow(); ImGui::TableSetColumnIndex(0); bool sel = (i == g_coll_selected); if (ImGui::Selectable(c.id.c_str(), sel, ImGuiSelectableFlags_SpanAllColumns)) { g_coll_selected = i; } ImGui::TableSetColumnIndex(1); ImGui::TextUnformatted(c.name.c_str()); ImGui::TableSetColumnIndex(2); ImGui::TextUnformatted(c.description.c_str()); } ImGui::EndTable(); } if (g_coll_selected >= 0 && g_coll_selected < (int)g_collectors.size()) { const auto& c = g_collectors[g_coll_selected]; ImGui::Separator(); ImGui::Text("Selected: %s", c.id.c_str()); ImGui::TextDisabled("%s", c.run_py.c_str()); ImGui::PushItemWidth(120); ImGui::InputInt("limit", &g_coll_param_limit); ImGui::PopItemWidth(); if (g_coll_param_limit < 1) g_coll_param_limit = 1; if (g_coll_param_limit > 500) g_coll_param_limit = 500; ImGui::BeginDisabled(g_running || !c.has_run); if (ImGui::Button(g_running ? "Running..." : (TI_PLAYER_PLAY " Run"))) { run_selected_collector(); } ImGui::EndDisabled(); if (!c.has_run) { ImGui::SameLine(); ImGui::TextDisabled("(missing run.py)"); } } if (!g_last_run_summary.empty()) { ImGui::Separator(); ImGui::TextUnformatted("Last run:"); ImGui::BeginChild("##run_summary", ImVec2(0, 200), true); ImGui::TextUnformatted(g_last_run_summary.c_str()); ImGui::EndChild(); } ImGui::End(); } static void draw_launcher() { if (!g_show_launcher) return; if (!ImGui::Begin(TI_SEARCH " Launcher", &g_show_launcher)) { ImGui::End(); return; } ImGui::PushItemWidth(-1); if (ImGui::InputTextWithHint("##search", "Search functions/pipelines (FTS5)...", g_search_buf, sizeof(g_search_buf), ImGuiInputTextFlags_EnterReturnsTrue)) { do_search(); } ImGui::PopItemWidth(); if (ImGui::Button("Search")) do_search(); ImGui::SameLine(); ImGui::TextDisabled("%zu hits", g_results.size()); ImGui::Separator(); if (ImGui::BeginTable("##results", 4, ImGuiTableFlags_RowBg | ImGuiTableFlags_Borders | ImGuiTableFlags_ScrollY | ImGuiTableFlags_Resizable)) { ImGui::TableSetupColumn("ID", ImGuiTableColumnFlags_WidthStretch); ImGui::TableSetupColumn("Kind", ImGuiTableColumnFlags_WidthFixed, 80); ImGui::TableSetupColumn("Domain", ImGuiTableColumnFlags_WidthFixed, 100); ImGui::TableSetupColumn("Description", ImGuiTableColumnFlags_WidthStretch); ImGui::TableHeadersRow(); for (int i = 0; i < (int)g_results.size(); ++i) { const auto& r = g_results[i]; ImGui::TableNextRow(); ImGui::TableSetColumnIndex(0); bool sel = (i == g_selected); if (ImGui::Selectable(r.id.c_str(), sel, ImGuiSelectableFlags_SpanAllColumns)) { g_selected = i; } ImGui::TableSetColumnIndex(1); ImGui::TextUnformatted(r.kind.c_str()); ImGui::TableSetColumnIndex(2); ImGui::TextUnformatted(r.domain.c_str()); ImGui::TableSetColumnIndex(3); ImGui::TextUnformatted(r.description.c_str()); } ImGui::EndTable(); } if (g_selected >= 0 && g_selected < (int)g_results.size()) { ImGui::Separator(); const auto& r = g_results[g_selected]; ImGui::Text("Selected: %s", r.id.c_str()); ImGui::TextWrapped("Signature: %s", r.signature.c_str()); ImGui::Spacing(); ImGui::BeginDisabled(true); ImGui::Button(TI_PLAYER_PLAY " Run"); ImGui::EndDisabled(); ImGui::SameLine(); ImGui::TextDisabled("(jobs system pending — issue 0065)"); } ImGui::End(); } static void draw_jobs() { if (!g_show_jobs) return; if (!ImGui::Begin(TI_LIST " Jobs", &g_show_jobs)) { ImGui::End(); return; } ImGui::TextDisabled("Jobs queue panel — pendiente issue 0065"); ImGui::TextWrapped( "Cuando jobs_pool_cpp_core este extraido del graph_explorer al " "registry, este panel mostrara cola/running/done con live progress. " "Ver dev/issues/0067-odr-osint-prereqs-roadmap.md"); ImGui::End(); } static void draw_datasets() { if (!g_show_datasets) return; if (!ImGui::Begin(TI_DATABASE " Datasets", &g_show_datasets)) { ImGui::End(); return; } ImGui::TextDisabled("DuckDB browser — pendiente fase 2 del MVP"); ImGui::End(); } static void render() { static fn_ui::PanelToggle panels[] = { { "Launcher", nullptr, &g_show_launcher }, { "Collectors", nullptr, &g_show_collectors }, { "Jobs", nullptr, &g_show_jobs }, { "Datasets", nullptr, &g_show_datasets }, }; fn_ui::app_menubar(panels, sizeof(panels) / sizeof(panels[0]), nullptr); draw_launcher(); draw_collectors(); draw_jobs(); draw_datasets(); } int main(int argc, char** argv) { // CLI: opcional, primer arg = path a registry.db (override). if (argc >= 2) { g_db_path = argv[1]; } if (g_db_path.empty()) { const char* env = std::getenv("FN_REGISTRY_DB"); if (env && *env) g_db_path = env; } if (g_db_path.empty()) { const char* root = std::getenv("FN_REGISTRY_ROOT"); if (root && *root) { g_db_path = std::string(root) + "/registry.db"; } } if (g_db_path.empty()) { std::fprintf(stderr, "[odr_console] No registry.db path. Pass as arg or set " "FN_REGISTRY_DB / FN_REGISTRY_ROOT.\n"); } else if (!registry_open(g_registry, g_db_path)) { std::fprintf(stderr, "[odr_console] Failed to open registry.db: %s\n", g_db_path.c_str()); } else { registry_list_recent(g_registry, 50, g_results); } // Resolver registry_root, app_dir, python_exe. if (const char* root = std::getenv("FN_REGISTRY_ROOT"); root && *root) { g_registry_root = root; } else if (!g_db_path.empty()) { // Asume registry.db esta en raiz del registry. std::filesystem::path p(g_db_path); g_registry_root = p.parent_path().string(); } if (const char* d = std::getenv("ODR_APP_DIR"); d && *d) { g_app_dir = d; } else if (!g_registry_root.empty()) { g_app_dir = g_registry_root + "/projects/online_data_recopilation/apps/odr_console"; } g_ops_db_path = g_app_dir + "/operations.db"; if (const char* py = std::getenv("FN_PYTHON"); py && *py) { g_python_exe = py; } else if (!g_registry_root.empty()) { g_python_exe = g_registry_root + "/python/.venv/bin/python3"; } else { g_python_exe = "python3"; } // Discover collectors: prefer app_dir/collectors, fallback a // /assets/collectors/ (Windows distribuible). if (!g_app_dir.empty()) { collectors_discover(g_app_dir + "/collectors", g_collectors); } if (g_collectors.empty()) { const char* assets = fn::asset_dir(); if (assets && *assets) { std::string assets_collectors = std::string(assets) + "/collectors"; collectors_discover(assets_collectors, g_collectors); } } fn::AppConfig cfg; cfg.title = "odr_console — online data recopilation"; cfg.width = 1400; cfg.height = 900; cfg.about = { "odr_console", "0.1.0", "Lanzador GUI de funciones del registry para recolectar " "datos online (APIs, scraping, browser CDP). MVP." }; cfg.log = { "odr_console.log", 1 }; int rc = fn::run_app(cfg, render); registry_close(g_registry); return rc; }