// recipes_panel — Listado de recetas (YAML) en // projects/navegator/profiles/default/recipes/*.yaml. // // Acciones por fila: // Run -> subprocess Python con cdp_extract_recipe (record_run=True). // Edit -> abre InputTextMultiline con el YAML; "Save" reescribe. // Delete -> rm + refresh list. // Open in data_factory -> noop (placeholder; mostraria link/cmd). #include "imgui.h" #include "core/icons_tabler.h" #include "core/tokens.h" #include "data_table/data_table.h" #include "core/data_table_types.h" #include "py_subprocess.h" #include "session_state.h" #include #include #include #include #include #include #include #include #include #include #include #ifdef _WIN32 # define WIN32_LEAN_AND_MEAN # include #else # include # include #endif namespace navegator { namespace { struct RecipeRow { std::string name; std::string url_pattern; std::string yaml_path; std::string last_run_status; std::string last_run_at; int rows_last_run = 0; }; struct RecipesState { std::mutex mu; std::vector rows; std::string status; std::string last_error; std::atomic busy{false}; int editing_idx = -1; std::string edit_buf; char edit_textarea[16384] = {0}; }; RecipesState g_rs; std::string recipes_dir() { std::string root = py_resolve_registry_root(); if (root.empty()) return ""; #ifdef _WIN32 return root + "\\projects\\navegator\\profiles\\default\\recipes"; #else return root + "/projects/navegator/profiles/default/recipes"; #endif } std::string slurp(const std::string& path) { std::ifstream f(path, std::ios::binary); if (!f) return ""; std::ostringstream ss; ss << f.rdbuf(); return ss.str(); } // Mini-parser YAML especifico: solo extrae name + url_pattern. void parse_recipe_min(const std::string& body, RecipeRow& r) { std::stringstream ss(body); std::string line; while (std::getline(ss, line)) { auto strip = [](std::string s){ size_t a = s.find_first_not_of(" \t"); size_t b = s.find_last_not_of(" \t\r"); return (a == std::string::npos) ? std::string() : s.substr(a, b - a + 1); }; if (line.rfind("name:", 0) == 0) { r.name = strip(line.substr(5)); if (!r.name.empty() && (r.name.front()=='"' || r.name.front()=='\'')) { r.name = r.name.substr(1, r.name.size() - 2); } } else if (line.rfind("url_pattern:", 0) == 0) { r.url_pattern = strip(line.substr(12)); if (!r.url_pattern.empty() && (r.url_pattern.front()=='"' || r.url_pattern.front()=='\'')) { r.url_pattern = r.url_pattern.substr(1, r.url_pattern.size() - 2); } } } } std::vector list_yaml_files(const std::string& dir) { std::vector out; #ifdef _WIN32 std::string pattern = dir + "\\*.yaml"; WIN32_FIND_DATAA fd; HANDLE h = FindFirstFileA(pattern.c_str(), &fd); if (h == INVALID_HANDLE_VALUE) return out; do { out.push_back(dir + "\\" + fd.cFileName); } while (FindNextFileA(h, &fd)); FindClose(h); #else DIR* d = opendir(dir.c_str()); if (!d) return out; while (auto e = readdir(d)) { std::string n = e->d_name; if (n.size() > 5 && n.substr(n.size() - 5) == ".yaml") { out.push_back(dir + "/" + n); } } closedir(d); #endif std::sort(out.begin(), out.end()); return out; } void refresh_list() { std::string dir = recipes_dir(); if (dir.empty()) { std::lock_guard lk(g_rs.mu); g_rs.last_error = "FN_REGISTRY_ROOT not set"; return; } auto files = list_yaml_files(dir); std::vector rows; for (const auto& f : files) { RecipeRow r; r.yaml_path = f; std::string body = slurp(f); parse_recipe_min(body, r); if (r.name.empty()) { // fallback al basename sin ext size_t p1 = f.find_last_of("/\\"); std::string base = (p1 == std::string::npos) ? f : f.substr(p1 + 1); if (base.size() > 5) base = base.substr(0, base.size() - 5); r.name = base; } rows.push_back(std::move(r)); } // Anota last_run_* desde data_factory.runs (subprocess sqlite3 best-effort). // Lo dejamos como TODO — la primera version queda con campos vacios. std::lock_guard lk(g_rs.mu); g_rs.rows = std::move(rows); g_rs.last_error.clear(); g_rs.status = "Listed " + std::to_string(g_rs.rows.size()) + " recipes"; } void run_recipe_async(const std::string& yaml_path) { if (g_rs.busy.exchange(true)) return; { std::lock_guard lk(g_rs.mu); g_rs.status = "Running " + yaml_path; } std::thread([yaml_path]() { const char* code = R"PY( import sys, os, json, traceback root = os.environ.get('FN_REGISTRY_ROOT', '') if not root: print(json.dumps({"error":"FN_REGISTRY_ROOT not set"})); sys.exit(2) for sub in ('pipelines','core','infra'): sys.path.insert(0, os.path.join(root, 'python', 'functions', sub)) try: from cdp_extract_recipe import cdp_extract_recipe path = sys.argv[1] res = cdp_extract_recipe(path, debug_port=9222, record_run=True) print(json.dumps(res if isinstance(res, dict) else {"result": res})) except Exception as e: print(json.dumps({"error": str(e), "trace": traceback.format_exc()})); sys.exit(1) )PY"; std::vector argv; argv.push_back(py_resolve_interpreter()); argv.push_back("-c"); argv.push_back(code); argv.push_back(yaml_path); PyResult r = py_run(argv, 120000); { std::lock_guard lk(g_rs.mu); if (r.exit_code != 0) { g_rs.last_error = r.error.empty() ? "python exited non-zero" : r.error; g_rs.status = "Run failed"; } else { g_rs.status = "Run OK: " + r.stdout_data.substr(0, 200); } } g_rs.busy.store(false); refresh_list(); }).detach(); } void delete_recipe(const std::string& path) { std::remove(path.c_str()); refresh_list(); } } // anon void render_recipes_panel(bool* p_open) { if (!ImGui::Begin(TI_LIST_DETAILS " Recipes", p_open)) { ImGui::End(); return; } if (ImGui::Button(TI_REFRESH " Refresh")) refresh_list(); ImGui::SameLine(); { std::lock_guard lk(g_rs.mu); if (!g_rs.status.empty()) ImGui::Text("%s", g_rs.status.c_str()); if (!g_rs.last_error.empty()) { ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::error); ImGui::TextWrapped("Error: %s", g_rs.last_error.c_str()); ImGui::PopStyleColor(); } } ImGui::Separator(); std::vector rows_copy; int editing_idx = -1; { std::lock_guard lk(g_rs.mu); rows_copy = g_rs.rows; editing_idx = g_rs.editing_idx; } if (rows_copy.empty()) { ImGui::TextDisabled("No recipes in projects/navegator/profiles/default/recipes/."); ImGui::TextDisabled("Use AutoExtract panel to create one."); } else { // Tabla de recetas — migrado a data_table::render (issue 0107g, Patron B). // Las acciones Run/Edit/Delete/Open se mapean a columnas Button con action_id. // ev.row indexa rows_copy directamente para recuperar yaml_path. static data_table::State g_st_recipes; static std::vector g_back_recipes; static std::vector g_ptrs_recipes; g_back_recipes.clear(); for (const auto& r : rows_copy) { g_back_recipes.push_back(r.name); g_back_recipes.push_back(r.url_pattern); g_back_recipes.push_back(r.last_run_status.empty() ? "-" : r.last_run_status); g_back_recipes.push_back(r.last_run_at.empty() ? "-" : r.last_run_at); g_back_recipes.push_back(std::to_string(r.rows_last_run)); g_back_recipes.push_back("Run"); // col 5: accion run g_back_recipes.push_back("Edit"); // col 6: accion edit g_back_recipes.push_back("Delete"); // col 7: accion delete g_back_recipes.push_back("Open"); // col 8: accion open_df } g_ptrs_recipes.clear(); for (const auto& s : g_back_recipes) g_ptrs_recipes.push_back(s.c_str()); data_table::TableInput tbl; tbl.name = "recipes_tbl"; tbl.headers = {"name", "url_pattern", "last_status", "last_at", "rows", "run", "edit", "delete", "open"}; tbl.types = { data_table::ColumnType::String, data_table::ColumnType::String, data_table::ColumnType::String, data_table::ColumnType::String, data_table::ColumnType::Int, data_table::ColumnType::String, data_table::ColumnType::String, data_table::ColumnType::String, data_table::ColumnType::String, }; tbl.cells = g_ptrs_recipes.data(); tbl.rows = (int)rows_copy.size(); tbl.cols = 9; tbl.column_specs.resize(tbl.cols); for (int i = 0; i < tbl.cols; i++) tbl.column_specs[i].id = tbl.headers[i]; // last_status → CategoricalChip tbl.column_specs[2].renderer = data_table::CellRenderer::CategoricalChip; tbl.column_specs[2].chips = { {"ok", "#22c55e"}, {"success", "#22c55e"}, {"error", "#ef4444"}, {"failed", "#ef4444"}, {"-", "#a3a3a3"}, }; // Botones de accion tbl.column_specs[5].renderer = data_table::CellRenderer::Button; tbl.column_specs[5].button_action = "run_recipe"; tbl.column_specs[5].button_label = "Run"; tbl.column_specs[5].button_color_hex = "#3b82f6"; tbl.column_specs[6].renderer = data_table::CellRenderer::Button; tbl.column_specs[6].button_action = "edit_recipe"; tbl.column_specs[6].button_label = "Edit"; tbl.column_specs[7].renderer = data_table::CellRenderer::Button; tbl.column_specs[7].button_action = "delete_recipe"; tbl.column_specs[7].button_label = "Delete"; tbl.column_specs[7].button_color_hex = "#ef4444"; tbl.column_specs[8].renderer = data_table::CellRenderer::Button; tbl.column_specs[8].button_action = "open_df"; tbl.column_specs[8].button_label = "Open"; std::vector rec_events; ImGui::BeginChild("##recipes_tbl_host", ImVec2(-1, 300)); data_table::render("##recipes_dt", {tbl}, g_st_recipes, &rec_events); ImGui::EndChild(); for (const auto& ev : rec_events) { if (ev.kind != data_table::TableEventKind::ButtonClick) continue; if (ev.row < 0 || ev.row >= (int)rows_copy.size()) continue; const RecipeRow& r = rows_copy[ev.row]; if (ev.action_id == "run_recipe") { run_recipe_async(r.yaml_path); } else if (ev.action_id == "edit_recipe") { std::string body = slurp(r.yaml_path); std::lock_guard lk(g_rs.mu); g_rs.editing_idx = ev.row; g_rs.edit_buf = body; std::snprintf(g_rs.edit_textarea, sizeof(g_rs.edit_textarea), "%s", body.c_str()); } else if (ev.action_id == "delete_recipe") { delete_recipe(r.yaml_path); } else if (ev.action_id == "open_df") { std::lock_guard lk(g_rs.mu); g_rs.status = "open in data_factory: " + r.name + " (not wired)"; } } } if (editing_idx >= 0 && editing_idx < (int)rows_copy.size()) { ImGui::Separator(); ImGui::Text("Editing: %s", rows_copy[editing_idx].yaml_path.c_str()); ImGui::InputTextMultiline("##rec_edit", g_rs.edit_textarea, sizeof(g_rs.edit_textarea), ImVec2(-1, 220)); if (ImGui::Button(TI_DEVICE_FLOPPY " Save")) { std::ofstream f(rows_copy[editing_idx].yaml_path, std::ios::binary); if (f) { f << g_rs.edit_textarea; f.close(); std::lock_guard lk(g_rs.mu); g_rs.status = "Saved " + rows_copy[editing_idx].yaml_path; g_rs.editing_idx = -1; } refresh_list(); } ImGui::SameLine(); if (ImGui::Button("Cancel")) { std::lock_guard lk(g_rs.mu); g_rs.editing_idx = -1; } } ImGui::End(); } } // namespace navegator