bd5751d30d
- 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>
1033 lines
38 KiB
C++
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);
|
|
}
|