Files
egutierrez bd5751d30d chore: auto-commit (7 archivos)
- CMakeLists.txt
- app.md
- appicon.ico
- http_client.cpp
- http_client.h
- main.cpp
- vendor/

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 00:31:31 +02:00

1033 lines
38 KiB
C++

// 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 <imgui.h>
#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 <algorithm>
#include <atomic>
#include <chrono>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <deque>
#include <map>
#include <mutex>
#include <string>
#include <thread>
#include <vector>
#ifdef _WIN32
#define popen _popen
#define pclose _pclose
#else
#include <sys/wait.h>
#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<std::string, std::string> 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<AppRow> apps;
std::vector<ModuleRow> modules;
std::string framework_name = "framework";
std::string framework_version = "?";
std::string framework_desc;
std::vector<std::string> framework_members;
std::map<std::string, std::string> 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<bool> 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<ActionEvent> 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<std::chrono::milliseconds>(
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<std::mutex> 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<std::string>();
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<std::string>();
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<std::string>();
}
}
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<std::string>();
}
}
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<long long>();
if (a.contains("win_size") && a["win_size"].is_number())
x.win_size = a["win_size"].get<long long>();
if (a.contains("win_age_sec") && a["win_age_sec"].is_number())
x.win_age_sec = a["win_age_sec"].get<long long>();
if (a.contains("win_build_mtime") && a["win_build_mtime"].is_number())
x.win_build_mtime = a["win_build_mtime"].get<long long>();
if (a.contains("win_needs_deploy") && a["win_needs_deploy"].is_boolean())
x.win_needs_deploy = a["win_needs_deploy"].get<bool>();
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<std::string>());
}
}
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<std::mutex> 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<std::string> owning;
std::vector<const char*> 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<data_table::TableEvent> 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<std::mutex> lk(g_log_mu);
g_action_log.clear();
}
ImGui::BeginChild("##action_log", ImVec2(0, 0), ImGuiChildFlags_Borders);
std::deque<ActionEvent> snap_log;
{
std::lock_guard<std::mutex> 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<std::mutex> 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<std::mutex> 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);
}