Files

387 lines
13 KiB
C++

// 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_table/data_table.h"
#include "data_registry.h"
#include "data_collectors.h"
#include "runner.h"
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <filesystem>
#include <sstream>
#include <string>
#include <vector>
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; // <app_dir>/operations.db
static char g_search_buf[256] = "";
static std::vector<RegistryRow> g_results;
static int g_selected = -1;
static std::vector<Collector> 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;
// data_table::State persistentes (issue 0081-J: migration from inline BeginTable).
static data_table::State g_dt_collectors;
static data_table::State g_dt_results;
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<std::string> 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;
}
// Build flat cells array for data_table::render (row-major, 3 cols).
{
static std::vector<std::string> coll_cell_backing;
static std::vector<const char*> coll_cells;
coll_cell_backing.clear();
coll_cell_backing.reserve(g_collectors.size() * 3);
for (const auto& c : g_collectors) {
coll_cell_backing.push_back(c.id);
coll_cell_backing.push_back(c.name);
coll_cell_backing.push_back(c.description);
}
coll_cells.resize(coll_cell_backing.size());
for (size_t i = 0; i < coll_cell_backing.size(); ++i)
coll_cells[i] = coll_cell_backing[i].c_str();
data_table::TableInput tbl;
tbl.name = "collectors";
tbl.headers = {"ID", "Name", "Description"};
tbl.types = {data_table::ColumnType::String,
data_table::ColumnType::String,
data_table::ColumnType::String};
tbl.cells = coll_cells.empty() ? nullptr : coll_cells.data();
tbl.rows = (int)g_collectors.size();
tbl.cols = 3;
// All columns use default Text renderer — no column_specs needed.
ImGui::BeginChild("##collectors_dt", ImVec2(0, 200), false);
data_table::render("##dt_collectors", {tbl}, g_dt_collectors);
ImGui::EndChild();
}
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();
// Build flat cells array for data_table::render (row-major, 4 cols).
{
static std::vector<std::string> res_cell_backing;
static std::vector<const char*> res_cells;
res_cell_backing.clear();
res_cell_backing.reserve(g_results.size() * 4);
for (const auto& r : g_results) {
res_cell_backing.push_back(r.id);
res_cell_backing.push_back(r.kind);
res_cell_backing.push_back(r.domain);
res_cell_backing.push_back(r.description);
}
res_cells.resize(res_cell_backing.size());
for (size_t i = 0; i < res_cell_backing.size(); ++i)
res_cells[i] = res_cell_backing[i].c_str();
// Column spec for Kind (col 1): Badge per kind value.
data_table::ColumnSpec cs_kind;
cs_kind.id = "kind";
cs_kind.renderer = data_table::CellRenderer::Badge;
cs_kind.badges = {
data_table::BadgeRule{"function", "#3b82f6", ""},
data_table::BadgeRule{"pipeline", "#8b5cf6", ""},
data_table::BadgeRule{"component", "#f59e0b", ""},
};
data_table::TableInput tbl;
tbl.name = "results";
tbl.headers = {"ID", "Kind", "Domain", "Description"};
tbl.types = {data_table::ColumnType::String,
data_table::ColumnType::String,
data_table::ColumnType::String,
data_table::ColumnType::String};
tbl.cells = res_cells.empty() ? nullptr : res_cells.data();
tbl.rows = (int)g_results.size();
tbl.cols = 4;
tbl.column_specs.resize(4); // default Text for all
tbl.column_specs[1] = cs_kind; // Badge for Kind
ImGui::BeginChild("##results_dt", ImVec2(0, -1), false);
data_table::render("##dt_results", {tbl}, g_dt_results);
ImGui::EndChild();
}
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() {
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
// <exe_dir>/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);
}
}
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::AppConfig cfg;
cfg.title = "odr_console — online data recopilation";
cfg.width = 1400;
cfg.height = 900;
cfg.panels = panels;
cfg.panel_count = sizeof(panels) / sizeof(panels[0]);
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;
}