// app_gestion — Vista de gestion central de apps + framework + modulos del // registry, conectada a registry_api via HTTP (tiempo real). v0.2.0 — 2026-05-17. #include #include "app_base.h" #include "core/panel_menu.h" #include "core/icons_tabler.h" #include "core/logger.h" #include "data_table/data_table.h" #include "core/data_table_types.h" #include "http_client.h" #include "vendor/nlohmann/json.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #ifdef _WIN32 #define popen _popen #define pclose _pclose #else #include #endif using json = nlohmann::json; namespace { // ------------------------------------------------------------------ // Domain rows. // ------------------------------------------------------------------ struct AppRow { std::string id; std::string name; std::string lang; std::string domain; std::string framework; std::string dir_path; std::string repo_url; std::string tags; std::string description; std::string project_id; std::string uses_modules; // v0.3: drift info (vs registry latest). std::map linked_modules; // name -> version embebida std::string linked_build; // "windows" | "linux" | "" std::string drift_summary; // CSV "fw:1.1.0!=1.0.0,..." bool has_build = false; bool has_drift = false; // v0.4: Windows deploy state. std::string win_status; // "deployed" | "build-only" | "none" | "n/a" std::string win_deployed_exe; std::string win_build_exe; long long win_mtime = 0; long long win_size = 0; long long win_age_sec = 0; long long win_build_mtime = 0; bool win_needs_deploy = false; }; struct ModuleRow { std::string id; std::string name; std::string version; std::string lang; std::string dir_path; std::string description; std::string members; std::string tags; }; struct Snapshot { std::vector apps; std::vector modules; std::string framework_name = "framework"; std::string framework_version = "?"; std::string framework_desc; std::vector framework_members; std::map registry_modules; // name -> version (latest) long long fetched_at_ms = 0; std::string error; }; // ------------------------------------------------------------------ // Global state. // ------------------------------------------------------------------ static std::mutex g_mu; static Snapshot g_snap; static std::atomic g_loading{false}; // API host:port. static char g_host_buf[128] = "127.0.0.1"; static int g_port = 8420; static bool g_auto_refresh = true; static int g_refresh_seconds = 5; static bool g_show_apps = true; static bool g_show_modules = true; static bool g_show_framework = true; static bool g_show_actions = true; static bool g_show_detail = true; struct ActionEvent { long long ts_ms = 0; std::string label; std::string detail; bool ok = true; }; static std::deque g_action_log; static std::mutex g_log_mu; constexpr size_t kLogMax = 80; static char g_selected_app_name[128] = ""; // ------------------------------------------------------------------ // Helpers. // ------------------------------------------------------------------ static long long now_ms() { using clk = std::chrono::steady_clock; return std::chrono::duration_cast( clk::now().time_since_epoch()).count(); } static std::string registry_root() { if (const char* env = std::getenv("FN_REGISTRY_ROOT")) { if (env[0]) return env; } #ifdef _WIN32 return "\\\\wsl.localhost\\Ubuntu\\home\\lucas\\fn_registry"; #else return "/home/lucas/fn_registry"; #endif } static void log_event(const std::string& label, const std::string& detail, bool ok) { ActionEvent ev; ev.ts_ms = now_ms(); ev.label = label; ev.detail = detail; ev.ok = ok; { std::lock_guard lk(g_log_mu); g_action_log.push_front(std::move(ev)); if (g_action_log.size() > kLogMax) g_action_log.pop_back(); } std::string line = "[" + label + "] " + detail; if (ok) fn_log::log_info(line.c_str()); else fn_log::log_warn(line.c_str()); } // ------------------------------------------------------------------ // JSON parsing helpers. // ------------------------------------------------------------------ static std::string j_str(const json& j, const char* key) { if (!j.contains(key) || j[key].is_null()) return ""; if (j[key].is_string()) return j[key].get(); return j[key].dump(); } static std::string j_array_csv(const json& j, const char* key) { if (!j.contains(key) || !j[key].is_array()) return ""; std::string out; for (const auto& el : j[key]) { if (!out.empty()) out += ", "; if (el.is_string()) out += el.get(); else out += el.dump(); } return out; } // ------------------------------------------------------------------ // Snapshot loader via HTTP. // ------------------------------------------------------------------ static void load_snapshot(const std::string& host, int port, Snapshot& out) { out.apps.clear(); out.modules.clear(); out.error.clear(); HttpClient cli(host, port); // /api/status — fail fast if not reachable. { HttpResponse r = cli.get("/api/status"); if (r.status == 0) { out.error = "no se pudo conectar a " + host + ":" + std::to_string(port) + " (registry_api corriendo?)"; return; } if (!r.ok()) { out.error = "GET /api/status -> HTTP " + std::to_string(r.status); return; } } // /api/apps (con linked_modules + registry_modules para drift) { HttpResponse r = cli.get("/api/apps"); if (!r.ok()) { out.error = "GET /api/apps -> HTTP " + std::to_string(r.status); return; } auto j = json::parse(r.body, nullptr, false); if (j.is_discarded() || !j.contains("apps") || !j["apps"].is_array()) { out.error = "respuesta /api/apps invalida"; return; } // Pull registry_modules global. if (j.contains("registry_modules") && j["registry_modules"].is_object()) { for (auto it = j["registry_modules"].begin(); it != j["registry_modules"].end(); ++it) { if (it.value().is_string()) out.registry_modules[it.key()] = it.value().get(); } } for (const auto& a : j["apps"]) { AppRow x; x.id = j_str(a, "id"); x.name = j_str(a, "name"); x.lang = j_str(a, "lang"); x.domain = j_str(a, "domain"); x.framework = j_str(a, "framework"); x.dir_path = j_str(a, "dir_path"); x.repo_url = j_str(a, "repo_url"); x.description = j_str(a, "description"); x.project_id = j_str(a, "project_id"); x.tags = j_array_csv(a, "tags"); x.uses_modules = j_array_csv(a, "uses_modules"); x.linked_build = j_str(a, "linked_build"); if (a.contains("linked_modules") && a["linked_modules"].is_object()) { for (auto it = a["linked_modules"].begin(); it != a["linked_modules"].end(); ++it) { if (it.value().is_string()) x.linked_modules[it.key()] = it.value().get(); } } x.has_build = !x.linked_modules.empty(); // Compute drift vs registry_modules. std::string drift; for (const auto& kv : x.linked_modules) { auto rit = out.registry_modules.find(kv.first); if (rit == out.registry_modules.end()) continue; if (rit->second != kv.second) { if (!drift.empty()) drift += ", "; drift += kv.first + ":" + kv.second + "!=" + rit->second; } } x.drift_summary = drift; x.has_drift = !drift.empty(); // Windows deploy state. x.win_status = j_str(a, "win_status"); x.win_deployed_exe = j_str(a, "win_deployed_exe"); x.win_build_exe = j_str(a, "win_build_exe"); if (a.contains("win_mtime") && a["win_mtime"].is_number()) x.win_mtime = a["win_mtime"].get(); if (a.contains("win_size") && a["win_size"].is_number()) x.win_size = a["win_size"].get(); if (a.contains("win_age_sec") && a["win_age_sec"].is_number()) x.win_age_sec = a["win_age_sec"].get(); if (a.contains("win_build_mtime") && a["win_build_mtime"].is_number()) x.win_build_mtime = a["win_build_mtime"].get(); if (a.contains("win_needs_deploy") && a["win_needs_deploy"].is_boolean()) x.win_needs_deploy = a["win_needs_deploy"].get(); out.apps.push_back(std::move(x)); } } // /api/modules { HttpResponse r = cli.get("/api/modules"); if (!r.ok()) { out.error = "GET /api/modules -> HTTP " + std::to_string(r.status); return; } auto j = json::parse(r.body, nullptr, false); if (j.is_discarded() || !j.contains("modules") || !j["modules"].is_array()) { out.error = "respuesta /api/modules invalida"; return; } for (const auto& m : j["modules"]) { ModuleRow x; x.id = j_str(m, "id"); x.name = j_str(m, "name"); x.version = j_str(m, "version"); x.lang = j_str(m, "lang"); x.dir_path = j_str(m, "dir_path"); x.description = j_str(m, "description"); x.members = j_array_csv(m, "members"); x.tags = j_array_csv(m, "tags"); if (x.name == "framework") { out.framework_version = x.version; out.framework_desc = x.description; if (m.contains("members") && m["members"].is_array()) { for (const auto& el : m["members"]) if (el.is_string()) out.framework_members.push_back(el.get()); } } out.modules.push_back(std::move(x)); } } out.fetched_at_ms = now_ms(); } static void reload_async() { if (g_loading.exchange(true)) return; std::string host = g_host_buf; int port = g_port; std::thread([host, port]() { Snapshot snap; load_snapshot(host, port, snap); { std::lock_guard lk(g_mu); g_snap = std::move(snap); } g_loading = false; }).detach(); } // ------------------------------------------------------------------ // Action runners. // ------------------------------------------------------------------ static void run_cmd_async(const std::string& label, const std::string& cmd) { std::thread([label, cmd]() { std::string out; std::string full = cmd + " 2>&1"; FILE* f = popen(full.c_str(), "r"); if (!f) { log_event(label, "popen failed: " + cmd, false); return; } char buf[1024]; while (fgets(buf, sizeof(buf), f)) { out += buf; if (out.size() > 4096) out.erase(0, out.size() - 4096); } int rc = pclose(f); #ifdef _WIN32 int exit_code = rc; #else int exit_code = WIFEXITED(rc) ? WEXITSTATUS(rc) : -1; #endif if (out.size() > 240) out = "..." + out.substr(out.size() - 240); std::string detail = "rc=" + std::to_string(exit_code); if (!out.empty()) detail += "\n" + out; log_event(label, detail, exit_code == 0); }).detach(); } static void action_rebuild(const AppRow& a) { if (a.name.empty()) return; std::string cmd = "cd " + registry_root() + "/cpp && " "cmake --build build/linux --target " + a.name + " -j"; run_cmd_async("rebuild:" + a.name, cmd); } static void action_redeploy_windows(const AppRow& a) { if (a.name.empty() || a.dir_path.empty()) return; std::string cmd = "cd " + registry_root() + " && " "./fn run redeploy_cpp_app_windows " + a.name + " " + a.dir_path + " --build"; run_cmd_async("redeploy_win:" + a.name, cmd); } static void shell_open(const std::string& target) { #ifdef _WIN32 std::string cmd = "start \"\" \"" + target + "\""; #else std::string cmd = "xdg-open '" + target + "' >/dev/null 2>&1 &"; #endif int srv = std::system(cmd.c_str()); (void)srv; } static void action_open_dir(const AppRow& a) { if (a.dir_path.empty()) return; std::string abs = registry_root() + "/" + a.dir_path; shell_open(abs); log_event("open_dir:" + a.name, abs, true); } static void action_open_repo(const AppRow& a) { if (a.repo_url.empty()) { log_event("open_repo:" + a.name, "(no repo_url)", false); return; } shell_open(a.repo_url); log_event("open_repo:" + a.name, a.repo_url, true); } static const AppRow* find_app_by_name(const Snapshot& s, const char* name) { if (!name || !*name) return nullptr; for (const auto& a : s.apps) { if (a.name == name) return &a; } return nullptr; } // ------------------------------------------------------------------ // Table builders. // ------------------------------------------------------------------ struct TableViewBuffers { std::vector owning; std::vector ptrs; }; static void build_apps_table(const Snapshot& s, TableViewBuffers& buf, data_table::TableInput& tbl) { // Columnas claves: // status — "ok" | "drift" | "no-build" para color rule. // build — windows/linux/(vacio) que indica binario inspeccionado. // fw_v — version del framework embebida (vacio si no hay build). // dt_v — version data_table embebida (si la app la usa). // drift — texto humano "fw:1.0.0!=1.1.0,data_table:1.4.0!=1.5.0". static const char* HEADERS[] = { "status", "name", "lang", "domain", "win", "win_age", "build", "fw_v", "dt_v", "drift", "dir_path", "repo_url", "uses_modules", "tags", "project", "id", }; static const data_table::ColumnType TYPES[] = { data_table::ColumnType::String, data_table::ColumnType::String, data_table::ColumnType::String, data_table::ColumnType::String, data_table::ColumnType::String, data_table::ColumnType::String, data_table::ColumnType::String, data_table::ColumnType::String, data_table::ColumnType::String, data_table::ColumnType::String, data_table::ColumnType::String, data_table::ColumnType::String, data_table::ColumnType::String, data_table::ColumnType::String, data_table::ColumnType::String, data_table::ColumnType::String, }; constexpr int NCOLS = (int)(sizeof(HEADERS) / sizeof(HEADERS[0])); buf.owning.clear(); buf.owning.reserve(s.apps.size() * NCOLS); auto fmt_age = [](long long s) -> std::string { if (s <= 0) return "-"; if (s < 60) return std::to_string(s) + "s"; if (s < 3600) return std::to_string(s/60) + "m"; if (s < 86400) return std::to_string(s/3600) + "h"; return std::to_string(s/86400) + "d"; }; for (const auto& a : s.apps) { std::string status; if (!a.has_build) status = "no-build"; else if (a.has_drift) status = "drift"; else status = "ok"; auto get_v = [&](const char* name) -> std::string { auto it = a.linked_modules.find(name); return it == a.linked_modules.end() ? std::string("") : it->second; }; std::string fw_v = get_v("framework"); std::string dt_v = get_v("data_table"); // win label: status + needs_deploy hint. std::string win_lbl = a.win_status; if (a.win_needs_deploy) win_lbl = "stale-deploy"; std::string win_age = fmt_age(a.win_age_sec); buf.owning.push_back(status); buf.owning.push_back(a.name); buf.owning.push_back(a.lang); buf.owning.push_back(a.domain); buf.owning.push_back(win_lbl); buf.owning.push_back(win_age); buf.owning.push_back(a.linked_build); buf.owning.push_back(fw_v); buf.owning.push_back(dt_v); buf.owning.push_back(a.drift_summary); buf.owning.push_back(a.dir_path); buf.owning.push_back(a.repo_url); buf.owning.push_back(a.uses_modules); buf.owning.push_back(a.tags); buf.owning.push_back(a.project_id); buf.owning.push_back(a.id); } buf.ptrs.clear(); buf.ptrs.reserve(buf.owning.size()); for (const auto& s2 : buf.owning) buf.ptrs.push_back(s2.c_str()); tbl.name = "apps"; tbl.headers.assign(HEADERS, HEADERS + NCOLS); tbl.types.assign(TYPES, TYPES + NCOLS); tbl.cells = buf.ptrs.data(); tbl.rows = (int)s.apps.size(); tbl.cols = NCOLS; } static void build_modules_table(const Snapshot& s, TableViewBuffers& buf, data_table::TableInput& tbl) { static const char* HEADERS[] = { "id", "name", "version", "lang", "dir_path", "members", "description", }; static const data_table::ColumnType TYPES[] = { data_table::ColumnType::String, data_table::ColumnType::String, data_table::ColumnType::String, data_table::ColumnType::String, data_table::ColumnType::String, data_table::ColumnType::String, data_table::ColumnType::String, }; constexpr int NCOLS = (int)(sizeof(HEADERS) / sizeof(HEADERS[0])); buf.owning.clear(); buf.owning.reserve(s.modules.size() * NCOLS); for (const auto& m : s.modules) { buf.owning.push_back(m.id); buf.owning.push_back(m.name); buf.owning.push_back(m.version); buf.owning.push_back(m.lang); buf.owning.push_back(m.dir_path); buf.owning.push_back(m.members); buf.owning.push_back(m.description); } buf.ptrs.clear(); buf.ptrs.reserve(buf.owning.size()); for (const auto& s2 : buf.owning) buf.ptrs.push_back(s2.c_str()); tbl.name = "modules"; tbl.headers.assign(HEADERS, HEADERS + NCOLS); tbl.types.assign(TYPES, TYPES + NCOLS); tbl.cells = buf.ptrs.data(); tbl.rows = (int)s.modules.size(); tbl.cols = NCOLS; } // ------------------------------------------------------------------ // Header bar (host:port + refresh). // ------------------------------------------------------------------ static void draw_header_bar() { ImGui::AlignTextToFramePadding(); ImGui::Text("registry_api: "); ImGui::SameLine(); ImGui::SetNextItemWidth(160.f); ImGui::InputText("##host", g_host_buf, sizeof(g_host_buf)); ImGui::SameLine(); ImGui::SetNextItemWidth(80.f); ImGui::InputInt("##port", &g_port, 0); if (g_port < 1) g_port = 1; if (g_port > 65535) g_port = 65535; ImGui::SameLine(); bool busy = g_loading.load(); // Reload button SIN disabled-toggle: si ya hay un fetch en curso, // reload_async() retorna inmediato. Evita el parpadeo de fondo del // boton entre frames. if (ImGui::Button(TI_REFRESH " Reload")) reload_async(); ImGui::SameLine(); ImGui::Checkbox("Auto", &g_auto_refresh); ImGui::SameLine(); ImGui::SetNextItemWidth(90.f); ImGui::DragInt("interval s", &g_refresh_seconds, 0.2f, 1, 120); // Indicador de actividad SIN layout-shift: punto fijo a la derecha // que cambia alpha segun busy. No aparece/desaparece — fade-in / fade-out. ImGui::SameLine(); static float s_busy_alpha = 0.f; float target = busy ? 1.f : 0.f; float dt = ImGui::GetIO().DeltaTime; float k = busy ? 6.f : 3.f; s_busy_alpha += (target - s_busy_alpha) * std::min(1.f, dt * k); ImVec2 p = ImGui::GetCursorScreenPos(); float h = ImGui::GetTextLineHeight(); ImVec2 center(p.x + h * 0.5f, p.y + h * 0.5f); ImU32 col = IM_COL32((int)(244*1), (int)(192*1), (int)(77*1), (int)(255 * s_busy_alpha)); ImGui::GetWindowDrawList()->AddCircleFilled(center, h * 0.28f, col, 12); // Dummy ocupa el ancho del dot — layout estable haya o no spinner. ImGui::Dummy(ImVec2(h + 6.f, h)); } // ------------------------------------------------------------------ // Panels. // ------------------------------------------------------------------ static void draw_apps_panel(const Snapshot& snap) { if (!ImGui::Begin(TI_DASHBOARD " Apps", &g_show_apps, ImGuiWindowFlags_NoCollapse)) { ImGui::End(); return; } draw_header_bar(); ImGui::SameLine(); int n_ok = 0, n_drift = 0, n_nobuild = 0; int n_deployed = 0, n_stale_deploy = 0, n_build_only = 0, n_no_win = 0; for (const auto& a : snap.apps) { if (!a.has_build) ++n_nobuild; else if (a.has_drift) ++n_drift; else ++n_ok; if (a.win_status == "deployed") { if (a.win_needs_deploy) ++n_stale_deploy; else ++n_deployed; } else if (a.win_status == "build-only") { ++n_build_only; } else if (a.win_status == "none") { ++n_no_win; } } ImGui::Text("| total: %d", (int)snap.apps.size()); ImGui::SameLine(); ImGui::TextColored(ImVec4(0.36f, 0.85f, 0.55f, 1.f), " %d ok", n_ok); ImGui::SameLine(); ImGui::TextColored(ImVec4(0.92f, 0.40f, 0.40f, 1.f), " %d drift", n_drift); ImGui::SameLine(); ImGui::TextDisabled(" %d no-build", n_nobuild); ImGui::SameLine(); ImGui::TextDisabled(" | win:"); ImGui::SameLine(); ImGui::TextColored(ImVec4(0.36f, 0.85f, 0.55f, 1.f), "%d deployed", n_deployed); ImGui::SameLine(); ImGui::TextColored(ImVec4(0.95f, 0.62f, 0.05f, 1.f), "%d stale-dep", n_stale_deploy); ImGui::SameLine(); ImGui::TextColored(ImVec4(0.05f, 0.65f, 0.92f, 1.f), "%d build-only", n_build_only); ImGui::SameLine(); ImGui::TextDisabled("%d none", n_no_win); ImGui::Separator(); static data_table::State st_apps; static TableViewBuffers buf_apps; data_table::TableInput tbl; build_apps_table(snap, buf_apps, tbl); // Badges en columnas claves (indices alineados con HEADERS de // build_apps_table): 0 status, 4 win, 7 fw_v, 8 dt_v. tbl.column_specs.resize(tbl.cols); { data_table::ColumnSpec& cs = tbl.column_specs[0]; // status cs.id = "status"; cs.renderer = data_table::CellRenderer::Badge; cs.badges = { {"ok", "#22c55e", "OK"}, {"drift", "#ef4444", "DRIFT"}, {"no-build", "#64748b", "no-build"}, }; } { data_table::ColumnSpec& cs = tbl.column_specs[4]; // win cs.id = "win"; cs.renderer = data_table::CellRenderer::Badge; cs.badges = { {"deployed", "#22c55e", "deployed"}, {"stale-deploy", "#f59e0b", "stale-deploy"}, {"build-only", "#0ea5e9", "build-only"}, {"none", "#64748b", "none"}, {"n/a", "#1f2937", "n/a"}, }; } { data_table::ColumnSpec& cs = tbl.column_specs[7]; // fw_v cs.id = "fw_v"; cs.renderer = data_table::CellRenderer::Badge; std::string fw_latest = snap.registry_modules.count("framework") ? snap.registry_modules.at("framework") : std::string(); if (!fw_latest.empty()) { cs.badges = { { fw_latest, "#22c55e", "" } }; } } { data_table::ColumnSpec& cs = tbl.column_specs[8]; // dt_v cs.id = "dt_v"; cs.renderer = data_table::CellRenderer::Badge; std::string dt_latest = snap.registry_modules.count("data_table") ? snap.registry_modules.at("data_table") : std::string(); if (!dt_latest.empty()) { cs.badges = { { dt_latest, "#22c55e", "" } }; } } std::vector events; ImGui::BeginChild("##apps_tbl_host", ImVec2(0, 0)); data_table::render("##apps_tbl", { tbl }, st_apps, &events); ImGui::EndChild(); ImGui::End(); // Process events: double-click → select app + open detail/actions. for (const auto& ev : events) { if (ev.kind == data_table::TableEventKind::RowDoubleClick) { if (ev.row >= 0 && ev.row < (int)snap.apps.size()) { const std::string& name = snap.apps[ev.row].name; std::snprintf(g_selected_app_name, sizeof(g_selected_app_name), "%s", name.c_str()); g_show_detail = true; g_show_actions = true; ImGui::SetWindowFocus(TI_INFO_CIRCLE " Detail"); } } } } static void draw_modules_panel(const Snapshot& snap) { if (!ImGui::Begin(TI_PACKAGE " Modules", &g_show_modules, ImGuiWindowFlags_NoCollapse)) { ImGui::End(); return; } ImGui::Text("Total: %d", (int)snap.modules.size()); ImGui::Separator(); static data_table::State st_mods; static TableViewBuffers buf_mods; data_table::TableInput tbl; build_modules_table(snap, buf_mods, tbl); ImGui::BeginChild("##modules_tbl_host", ImVec2(0, 0)); data_table::render("##modules_tbl", { tbl }, st_mods); ImGui::EndChild(); ImGui::End(); } static void draw_framework_panel(const Snapshot& snap) { if (!ImGui::Begin(TI_BUILDING " Framework", &g_show_framework, ImGuiWindowFlags_NoCollapse)) { ImGui::End(); return; } ImGui::Text("Module : %s", snap.framework_name.c_str()); ImGui::Text("Version (api) : %s", snap.framework_version.c_str()); ImGui::Text("Version (link): %s", fn::framework_version()); if (snap.framework_version != fn::framework_version() && snap.framework_version != "?") { ImGui::TextColored(ImVec4(0.95f, 0.75f, 0.30f, 1.f), TI_ALERT_CIRCLE " drift: app linkeada contra %s, registry dice %s", fn::framework_version(), snap.framework_version.c_str()); } ImGui::Separator(); ImGui::TextWrapped("%s", snap.framework_desc.c_str()); ImGui::Separator(); ImGui::TextDisabled("Members (%d):", (int)snap.framework_members.size()); for (const auto& m : snap.framework_members) { ImGui::BulletText("%s", m.c_str()); } ImGui::Separator(); int cpp_count = 0; for (const auto& a : snap.apps) if (a.lang == "cpp") ++cpp_count; ImGui::TextDisabled("Apps cpp que enlazan framework: %d", cpp_count); ImGui::End(); } static void draw_detail_panel(const Snapshot& snap) { if (!ImGui::Begin(TI_INFO_CIRCLE " Detail", &g_show_detail, ImGuiWindowFlags_NoCollapse)) { ImGui::End(); return; } const AppRow* a = find_app_by_name(snap, g_selected_app_name); if (!a) { ImGui::TextDisabled("Doble-click una fila en el panel Apps para ver detalles."); ImGui::End(); return; } // Title + status badge. ImGui::PushFont(nullptr); ImGui::TextUnformatted(a->name.c_str()); ImGui::SameLine(); if (!a->has_build) { ImGui::TextColored(ImVec4(0.55f, 0.60f, 0.66f, 1.f), "[no-build]"); } else if (a->has_drift) { ImGui::TextColored(ImVec4(0.92f, 0.40f, 0.40f, 1.f), "[DRIFT %s]", a->linked_build.c_str()); } else { ImGui::TextColored(ImVec4(0.36f, 0.85f, 0.55f, 1.f), "[ok %s]", a->linked_build.c_str()); } ImGui::PopFont(); ImGui::Separator(); // Description block. ImGui::TextDisabled("Description"); ImGui::TextWrapped("%s", a->description.empty() ? "(sin descripcion)" : a->description.c_str()); ImGui::Separator(); // Meta two-column. ImGui::Columns(2, "##detail_meta", false); ImGui::Text("id : %s", a->id.c_str()); ImGui::Text("lang : %s", a->lang.c_str()); ImGui::Text("domain : %s", a->domain.c_str()); ImGui::Text("project: %s", a->project_id.c_str()); ImGui::NextColumn(); ImGui::Text("dir : %s", a->dir_path.c_str()); ImGui::Text("repo : %s", a->repo_url.c_str()); ImGui::Text("tags : %s", a->tags.c_str()); ImGui::Text("modules: %s", a->uses_modules.c_str()); ImGui::Columns(1); ImGui::Separator(); // Windows deploy section. ImGui::TextDisabled("Windows deploy"); auto fmt_age_d = [](long long s) -> std::string { if (s <= 0) return "-"; if (s < 60) return std::to_string(s) + "s"; if (s < 3600) return std::to_string(s/60) + "m"; if (s < 86400) return std::to_string(s/3600) + "h"; return std::to_string(s/86400) + "d"; }; auto fmt_size = [](long long b) -> std::string { if (b <= 0) return "-"; double mb = (double)b / (1024.0*1024.0); char buf[32]; std::snprintf(buf, sizeof(buf), "%.1f MB", mb); return std::string(buf); }; if (a->win_status == "deployed") { const char* sub = a->win_needs_deploy ? "STALE — build mas reciente" : "actualizado"; ImVec4 col = a->win_needs_deploy ? ImVec4(0.95f, 0.62f, 0.05f, 1.f) : ImVec4(0.36f, 0.85f, 0.55f, 1.f); ImGui::TextColored(col, TI_BRAND_WINDOWS " deployed (%s)", sub); ImGui::Text(" path : %s", a->win_deployed_exe.c_str()); ImGui::Text(" age : %s", fmt_age_d(a->win_age_sec).c_str()); ImGui::Text(" size : %s", fmt_size(a->win_size).c_str()); if (a->win_build_mtime > 0) { long long delta = a->win_build_mtime - a->win_mtime; ImGui::Text(" build vs deployed: %+lld s (%s)", delta, delta > 0 ? "build mas reciente" : (delta < 0 ? "deployed mas reciente" : "iguales")); } } else if (a->win_status == "build-only") { ImGui::TextColored(ImVec4(0.05f, 0.65f, 0.92f, 1.f), TI_HAMMER " build-only — sin desplegar a Desktop"); ImGui::Text(" build exe: %s", a->win_build_exe.c_str()); } else if (a->win_status == "none") { ImGui::TextDisabled("(sin binario Windows — no compilado todavia)"); } else if (a->win_status == "n/a") { ImGui::TextDisabled("(app no-cpp; sin deploy Windows)"); } else { ImGui::TextDisabled("(estado desconocido)"); } ImGui::Separator(); // Linked versions table. ImGui::TextDisabled("Linked versions (binario %s):", a->linked_build.empty() ? "?" : a->linked_build.c_str()); if (a->linked_modules.empty()) { ImGui::TextDisabled("(sin info — la app no se ha buildeado todavia," " o no genera _modules_generated.cpp)"); } else { if (ImGui::BeginTable("##linked_tbl", 4, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) { ImGui::TableSetupColumn("module"); ImGui::TableSetupColumn("linked"); ImGui::TableSetupColumn("registry"); ImGui::TableSetupColumn("status"); ImGui::TableHeadersRow(); for (const auto& kv : a->linked_modules) { ImGui::TableNextRow(); ImGui::TableSetColumnIndex(0); ImGui::TextUnformatted(kv.first.c_str()); ImGui::TableSetColumnIndex(1); ImGui::TextUnformatted(kv.second.c_str()); ImGui::TableSetColumnIndex(2); auto it = snap.registry_modules.find(kv.first); std::string reg_v = (it == snap.registry_modules.end()) ? "?" : it->second; ImGui::TextUnformatted(reg_v.c_str()); ImGui::TableSetColumnIndex(3); if (reg_v == "?") { ImGui::TextDisabled("?"); } else if (reg_v == kv.second) { ImGui::TextColored(ImVec4(0.36f, 0.85f, 0.55f, 1.f), "OK"); } else { ImGui::TextColored(ImVec4(0.92f, 0.40f, 0.40f, 1.f), "STALE"); } } ImGui::EndTable(); } } ImGui::Separator(); // Inline action buttons (same as Actions panel, comoditiy). bool cpp = (a->lang == "cpp"); ImGui::BeginDisabled(!cpp); if (ImGui::Button(TI_HAMMER " Rebuild linux")) action_rebuild(*a); ImGui::SameLine(); if (ImGui::Button(TI_BRAND_WINDOWS " Redeploy Windows")) action_redeploy_windows(*a); ImGui::EndDisabled(); ImGui::SameLine(); if (ImGui::Button(TI_FOLDER " Open dir")) action_open_dir(*a); ImGui::SameLine(); ImGui::BeginDisabled(a->repo_url.empty()); if (ImGui::Button(TI_BRAND_GIT " Open repo")) action_open_repo(*a); ImGui::EndDisabled(); ImGui::End(); } static void draw_actions_panel(const Snapshot& snap) { if (!ImGui::Begin(TI_BOLT " Actions", &g_show_actions, ImGuiWindowFlags_NoCollapse)) { ImGui::End(); return; } ImGui::TextDisabled("Selecciona la app por nombre."); ImGui::SetNextItemWidth(220); ImGui::InputText("##selected_app", g_selected_app_name, sizeof(g_selected_app_name)); ImGui::SameLine(); if (ImGui::BeginCombo("##pick_app", "pick...", ImGuiComboFlags_HeightLarge)) { for (const auto& a : snap.apps) { bool sel = (a.name == g_selected_app_name); if (ImGui::Selectable(a.name.c_str(), sel)) { std::snprintf(g_selected_app_name, sizeof(g_selected_app_name), "%s", a.name.c_str()); } } ImGui::EndCombo(); } const AppRow* a = find_app_by_name(snap, g_selected_app_name); if (!a) { ImGui::TextDisabled("(escribe o elige una app de la lista)"); } else { ImGui::Separator(); ImGui::Text("id : %s", a->id.c_str()); ImGui::Text("lang : %s domain: %s", a->lang.c_str(), a->domain.c_str()); ImGui::Text("dir : %s", a->dir_path.c_str()); ImGui::Text("repo : %s", a->repo_url.c_str()); ImGui::Text("modules : %s", a->uses_modules.c_str()); ImGui::Separator(); bool cpp = (a->lang == "cpp"); ImGui::BeginDisabled(!cpp); if (ImGui::Button(TI_HAMMER " Rebuild linux")) action_rebuild(*a); ImGui::SameLine(); if (ImGui::Button(TI_BRAND_WINDOWS " Redeploy Windows")) action_redeploy_windows(*a); ImGui::EndDisabled(); ImGui::SameLine(); if (ImGui::Button(TI_FOLDER " Open dir")) action_open_dir(*a); ImGui::SameLine(); ImGui::BeginDisabled(a->repo_url.empty()); if (ImGui::Button(TI_BRAND_GIT " Open repo")) action_open_repo(*a); ImGui::EndDisabled(); if (!cpp) ImGui::TextDisabled("Rebuild/redeploy disponibles solo para apps cpp."); } ImGui::Separator(); ImGui::TextDisabled("Action log (max %d):", (int)kLogMax); ImGui::SameLine(); if (ImGui::SmallButton("clear")) { std::lock_guard lk(g_log_mu); g_action_log.clear(); } ImGui::BeginChild("##action_log", ImVec2(0, 0), ImGuiChildFlags_Borders); std::deque snap_log; { std::lock_guard lk(g_log_mu); snap_log = g_action_log; } long long now = now_ms(); for (const auto& ev : snap_log) { ImVec4 col = ev.ok ? ImVec4(0.36f, 0.85f, 0.55f, 1.f) : ImVec4(0.92f, 0.40f, 0.40f, 1.f); long long age_s = (now - ev.ts_ms) / 1000; ImGui::TextColored(col, "[%llds ago] %s", (long long)age_s, ev.label.c_str()); if (!ev.detail.empty()) ImGui::TextWrapped(" %s", ev.detail.c_str()); } ImGui::EndChild(); ImGui::End(); } // ------------------------------------------------------------------ // Frame. // ------------------------------------------------------------------ static void render() { // Auto-refresh. if (g_auto_refresh && !g_loading.load()) { long long age_ms; { std::lock_guard lk(g_mu); age_ms = g_snap.fetched_at_ms == 0 ? 1'000'000LL : (now_ms() - g_snap.fetched_at_ms); } long long interval_ms = (long long)g_refresh_seconds * 1000LL; if (age_ms > interval_ms) reload_async(); } Snapshot snap; { std::lock_guard lk(g_mu); snap = g_snap; } if (!snap.error.empty()) { if (ImGui::Begin(TI_ALERT_CIRCLE " Error")) { ImGui::TextColored(ImVec4(0.92f, 0.40f, 0.40f, 1.f), "%s", snap.error.c_str()); if (ImGui::Button(TI_REFRESH " Retry")) reload_async(); ImGui::Separator(); ImGui::TextWrapped( "Lanza registry_api local:\n" " cd %s/apps/registry_api\n" " ./registry_api -port 8420 &\n" "Por defecto sirve sin auth en 127.0.0.1:8420.", registry_root().c_str()); } ImGui::End(); } if (g_show_apps) draw_apps_panel(snap); if (g_show_detail) draw_detail_panel(snap); if (g_show_modules) draw_modules_panel(snap); if (g_show_framework) draw_framework_panel(snap); if (g_show_actions) draw_actions_panel(snap); } } // namespace int main(int /*argc*/, char** /*argv*/) { static fn_ui::PanelToggle panels[] = { { "Apps", nullptr, &g_show_apps }, { "Detail", nullptr, &g_show_detail }, { "Modules", nullptr, &g_show_modules }, { "Framework", nullptr, &g_show_framework }, { "Actions", nullptr, &g_show_actions }, }; fn::AppConfig cfg; cfg.title = "app_gestion"; cfg.about = { "app_gestion", "0.4.0", "Gestion central de apps + framework + modulos via registry_api (HTTP), con drift + estado Windows deploy." }; cfg.log = { "app_gestion.log", 1 }; cfg.panels = panels; cfg.panel_count = sizeof(panels) / sizeof(panels[0]); reload_async(); return fn::run_app(cfg, render); }