ff67e4e069
- app.md - appicon.ico - views.cpp - work_tab.cpp Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1830 lines
78 KiB
C++
1830 lines
78 KiB
C++
#include "views.h"
|
|
#include "data_http.h"
|
|
#include "work_tab.h"
|
|
#include <filesystem>
|
|
#include "imgui.h"
|
|
#include "implot.h"
|
|
|
|
#include "viz/kpi_card.h"
|
|
#include "viz/bar_chart.h"
|
|
#include "viz/pie_chart.h"
|
|
#include "viz/table_view.h"
|
|
#include "viz/sparkline.h"
|
|
#include "data_table/data_table.h"
|
|
#include "core/data_table_types.h"
|
|
#include "core/icons_tabler.h"
|
|
#include "core/dashboard_panel.h"
|
|
#include "core/dashboard_grid.h"
|
|
#include "core/fullscreen_window.h"
|
|
#include "core/tokens.h"
|
|
#include "core/page_header.h"
|
|
#include "core/empty_state.h"
|
|
#include "core/badge.h"
|
|
#include "core/button.h"
|
|
#include "core/icon_button.h"
|
|
#include "core/toolbar.h"
|
|
#include "core/modal_dialog.h"
|
|
#include "core/text_input.h"
|
|
#include "core/select.h"
|
|
#include "core/toast.h"
|
|
#include "core/process_runner.h"
|
|
#include "core/tree_view.h"
|
|
#include "core/selectable_text.h"
|
|
|
|
#include <cstdio>
|
|
#include <cstring>
|
|
#include <cctype>
|
|
#include <ctime>
|
|
#include <cmath>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Shared state
|
|
// ---------------------------------------------------------------------------
|
|
|
|
static std::string g_api_url;
|
|
static fn_ui::ProcessRunner g_reindex_runner;
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Monitor (issue 0086) — local state
|
|
// ---------------------------------------------------------------------------
|
|
// Presets de ventana temporal: 1h, 24h, 7d, 30d, All. Indice por defecto = 24h.
|
|
static const char* kMonitorWindowLabels[] = {"1h", "24h", "7d", "30d", "All"};
|
|
static const int kMonitorWindowSecs[] = {3600, 86400, 604800, 2592000, 0};
|
|
static int g_monitor_window_idx = 1; // 24h por defecto
|
|
static bool g_monitor_reload_request = false;
|
|
static bool g_monitor_ws_connected = false;
|
|
static long long g_monitor_last_event_ts = 0;
|
|
|
|
// Scatter: ventana solo del eje X. NO afecta KPIs ni queries — solo al plot.
|
|
static const char* kScatterWindowLabels[] = {"1m", "5m", "15m", "1h", "6h"};
|
|
static const int kScatterWindowSecs[] = {60, 300, 900, 3600, 21600};
|
|
static int g_scatter_window_idx = 1; // 5m por defecto
|
|
|
|
// Recent Executions: si true, oculta filas con function_id vacio.
|
|
static bool g_recent_only_registry = false;
|
|
|
|
bool monitor_consume_reload_request() {
|
|
bool r = g_monitor_reload_request;
|
|
g_monitor_reload_request = false;
|
|
return r;
|
|
}
|
|
|
|
void monitor_set_ws_state(bool connected, long long last_event_ts) {
|
|
g_monitor_ws_connected = connected;
|
|
if (last_event_ts > 0) g_monitor_last_event_ts = last_event_ts;
|
|
}
|
|
|
|
// Formatea un epoch ts en "YYYY-MM-DD HH:MM:SS" local. Si ts == 0 -> "-".
|
|
static std::string format_ts(long long ts) {
|
|
if (ts <= 0) return "-";
|
|
std::time_t t = static_cast<std::time_t>(ts);
|
|
std::tm tm_buf{};
|
|
#if defined(_WIN32)
|
|
localtime_s(&tm_buf, &t);
|
|
#else
|
|
localtime_r(&t, &tm_buf);
|
|
#endif
|
|
char buf[32];
|
|
std::strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", &tm_buf);
|
|
return std::string(buf);
|
|
}
|
|
|
|
// Formatea ts relativo a now: "3s", "2m", "1h", "4d". Para "live indicator".
|
|
static std::string format_ts_relative(long long ts) {
|
|
if (ts <= 0) return "never";
|
|
std::time_t now = std::time(nullptr);
|
|
long long diff = static_cast<long long>(now) - ts;
|
|
if (diff < 0) diff = 0;
|
|
char buf[32];
|
|
if (diff < 60) std::snprintf(buf, sizeof(buf), "%llds ago", (long long)diff);
|
|
else if (diff < 3600) std::snprintf(buf, sizeof(buf), "%lldm ago", (long long)(diff / 60));
|
|
else if (diff < 86400) std::snprintf(buf, sizeof(buf), "%lldh ago", (long long)(diff / 3600));
|
|
else std::snprintf(buf, sizeof(buf), "%lldd ago", (long long)(diff / 86400));
|
|
return std::string(buf);
|
|
}
|
|
static fn_ui::ProcessRunner g_add_runner;
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// data_table::State — persistent per panel (issue 0081-J)
|
|
// ---------------------------------------------------------------------------
|
|
// One State per data-table panel. Must NOT be stack-local (see data_table.md
|
|
// Gotchas: "State no stack-local").
|
|
static data_table::State g_dt_recent_funcs;
|
|
static data_table::State g_dt_apps;
|
|
static data_table::State g_dt_analysis;
|
|
static data_table::State g_dt_types;
|
|
static data_table::State g_dt_vaults;
|
|
static data_table::State g_dt_top_fn;
|
|
static data_table::State g_dt_violations;
|
|
static data_table::State g_dt_copies;
|
|
static data_table::State g_dt_monitor_recent; // issue 0081-J
|
|
static data_table::State g_dt_monitor_failed; // issue 0081-J
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Helpers: build a data_table::TableInput from registry row vectors
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// Build a flat cells vector (row-major) + keep string backing alive.
|
|
// Returns backing storage; `ti` is populated in-place.
|
|
static std::vector<std::string> make_cells_recent_funcs(
|
|
const std::vector<FunctionRow>& funcs,
|
|
data_table::TableInput& ti)
|
|
{
|
|
ti.name = "functions";
|
|
ti.headers = {"Name", "Lang", "Domain", "Kind", "Purity", "Tested", "Created"};
|
|
ti.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::Date,
|
|
};
|
|
ti.rows = static_cast<int>(funcs.size());
|
|
ti.cols = static_cast<int>(ti.headers.size());
|
|
|
|
std::vector<std::string> backing;
|
|
backing.reserve(funcs.size() * ti.cols);
|
|
for (const auto& f : funcs) {
|
|
backing.push_back(f.name);
|
|
backing.push_back(f.lang);
|
|
backing.push_back(f.domain);
|
|
backing.push_back(f.kind);
|
|
backing.push_back(f.purity);
|
|
backing.push_back(f.tested ? "yes" : "no");
|
|
backing.push_back(f.created_at.substr(0, 10));
|
|
}
|
|
return backing;
|
|
}
|
|
|
|
static std::vector<std::string> make_cells_apps(
|
|
const std::vector<AppRow>& apps,
|
|
data_table::TableInput& ti)
|
|
{
|
|
ti.name = "apps";
|
|
ti.headers = {"Name", "Lang", "Domain", "Framework", "Git", "Description"};
|
|
ti.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,
|
|
};
|
|
ti.rows = static_cast<int>(apps.size());
|
|
ti.cols = static_cast<int>(ti.headers.size());
|
|
|
|
std::vector<std::string> backing;
|
|
backing.reserve(apps.size() * ti.cols);
|
|
for (const auto& a : apps) {
|
|
std::string git_status;
|
|
if (!a.repo_url.empty()) {
|
|
git_status = "remote";
|
|
} else if (!a.dir_path.empty()) {
|
|
std::error_code ec;
|
|
if (std::filesystem::exists(
|
|
std::filesystem::path(a.dir_path) / ".git", ec))
|
|
git_status = "local";
|
|
else
|
|
git_status = "-";
|
|
} else {
|
|
git_status = "-";
|
|
}
|
|
backing.push_back(a.name);
|
|
backing.push_back(a.lang);
|
|
backing.push_back(a.domain);
|
|
backing.push_back(a.framework);
|
|
backing.push_back(git_status);
|
|
backing.push_back(a.description);
|
|
}
|
|
return backing;
|
|
}
|
|
|
|
static std::vector<std::string> make_cells_analysis(
|
|
const std::vector<AnalysisRow>& analyses,
|
|
data_table::TableInput& ti)
|
|
{
|
|
ti.name = "analysis";
|
|
ti.headers = {"Name", "Lang", "Domain", "Description"};
|
|
ti.types = {
|
|
data_table::ColumnType::String,
|
|
data_table::ColumnType::String,
|
|
data_table::ColumnType::String,
|
|
data_table::ColumnType::String,
|
|
};
|
|
ti.rows = static_cast<int>(analyses.size());
|
|
ti.cols = static_cast<int>(ti.headers.size());
|
|
|
|
std::vector<std::string> backing;
|
|
backing.reserve(analyses.size() * ti.cols);
|
|
for (const auto& a : analyses) {
|
|
backing.push_back(a.name);
|
|
backing.push_back(a.lang);
|
|
backing.push_back(a.domain);
|
|
backing.push_back(a.description);
|
|
}
|
|
return backing;
|
|
}
|
|
|
|
static std::vector<std::string> make_cells_types(
|
|
const std::vector<TypeRow>& types,
|
|
data_table::TableInput& ti)
|
|
{
|
|
ti.name = "types";
|
|
ti.headers = {"Name", "Lang", "Domain", "Algebraic", "Description"};
|
|
ti.types = {
|
|
data_table::ColumnType::String,
|
|
data_table::ColumnType::String,
|
|
data_table::ColumnType::String,
|
|
data_table::ColumnType::String,
|
|
data_table::ColumnType::String,
|
|
};
|
|
ti.rows = static_cast<int>(types.size());
|
|
ti.cols = static_cast<int>(ti.headers.size());
|
|
|
|
std::vector<std::string> backing;
|
|
backing.reserve(types.size() * ti.cols);
|
|
for (const auto& t : types) {
|
|
backing.push_back(t.name);
|
|
backing.push_back(t.lang);
|
|
backing.push_back(t.domain);
|
|
backing.push_back(t.algebraic);
|
|
backing.push_back(t.description);
|
|
}
|
|
return backing;
|
|
}
|
|
|
|
static std::vector<std::string> make_cells_vaults(
|
|
const std::vector<VaultRow>& vaults,
|
|
data_table::TableInput& ti)
|
|
{
|
|
ti.name = "vaults";
|
|
ti.headers = {"Name", "Path", "Symlink", "Description"};
|
|
ti.types = {
|
|
data_table::ColumnType::String,
|
|
data_table::ColumnType::String,
|
|
data_table::ColumnType::String,
|
|
data_table::ColumnType::String,
|
|
};
|
|
ti.rows = static_cast<int>(vaults.size());
|
|
ti.cols = static_cast<int>(ti.headers.size());
|
|
|
|
std::vector<std::string> backing;
|
|
backing.reserve(vaults.size() * ti.cols);
|
|
for (const auto& v : vaults) {
|
|
backing.push_back(v.name);
|
|
backing.push_back(v.path);
|
|
backing.push_back(v.symlink ? "yes" : "no");
|
|
backing.push_back(v.description);
|
|
}
|
|
return backing;
|
|
}
|
|
|
|
// Converts backing vector to flat const char* array for TableInput.cells.
|
|
// `ptrs` must outlive the render() call.
|
|
static void cells_to_ptrs(const std::vector<std::string>& backing,
|
|
std::vector<const char*>& ptrs)
|
|
{
|
|
ptrs.resize(backing.size());
|
|
for (size_t i = 0; i < backing.size(); i++)
|
|
ptrs[i] = backing[i].c_str();
|
|
}
|
|
|
|
// Add modal state
|
|
enum class AddKind : int { App = 0, Analysis = 1, Vault = 2 };
|
|
static bool g_show_add = false;
|
|
static int g_add_kind_idx = static_cast<int>(AddKind::App);
|
|
static int g_add_project_idx = -1; // -1 = (none / sin project)
|
|
static int g_add_lang_idx = 0;
|
|
static int g_add_domain_idx = 0;
|
|
static char g_add_name[128] = {};
|
|
static char g_add_desc[256] = {};
|
|
static char g_add_packages[256] = {};
|
|
static char g_add_vault_path[512] = {};
|
|
|
|
void views_set_api_url(const std::string& url) { g_api_url = url; }
|
|
|
|
static std::vector<const char*> to_cstr(const std::vector<std::string>& v) {
|
|
std::vector<const char*> out;
|
|
out.reserve(v.size());
|
|
for (auto& s : v) out.push_back(s.c_str());
|
|
return out;
|
|
}
|
|
|
|
// Cache del Explorer: forward declarado aqui para que trigger_reload pueda
|
|
// invalidarlo (la lista completa de funciones se cachea hasta que el usuario
|
|
// dispara Reload o Reindex).
|
|
static std::vector<FunctionRow> g_explorer_funcs;
|
|
static bool g_explorer_loaded = false;
|
|
static std::string g_explorer_selected_id;
|
|
static FunctionDetail g_explorer_detail;
|
|
static std::vector<UnitTestRow> g_explorer_tests;
|
|
static int g_explorer_test_idx = 0;
|
|
static char g_explorer_filter[128] = {};
|
|
static int g_explorer_lang_idx = 0;
|
|
static int g_explorer_domain_idx = 0;
|
|
// Caches del tab Dependencies — se recomputan en explorer_select() para no
|
|
// reparsar JSON cada frame. uses_funcs/uses_types: parse de d.uses_functions
|
|
// y d.uses_types. used_by: reverse lookup cliente-side sobre g_explorer_funcs.
|
|
static std::vector<std::string> g_explorer_uses_funcs;
|
|
static std::vector<std::string> g_explorer_uses_types;
|
|
static std::vector<std::string> g_explorer_used_by;
|
|
|
|
// Parser tolerante de arrays JSON de strings (`["a","b"]`). Las entradas de
|
|
// la BD son IDs `[a-z0-9_]+` sin escapes, asi que un walker simple basta y
|
|
// evita anadir dependencia a nlohmann/json (que vive solo en data_http.cpp).
|
|
static std::vector<std::string> parse_string_array_json(const std::string& json_str) {
|
|
std::vector<std::string> out;
|
|
if (json_str.empty() || json_str == "[]") return out;
|
|
size_t i = 0, n = json_str.size();
|
|
while (i < n) {
|
|
while (i < n && json_str[i] != '"') i++;
|
|
if (i >= n) break;
|
|
i++; // past opening "
|
|
size_t start = i;
|
|
while (i < n && json_str[i] != '"') i++;
|
|
if (i > start) out.emplace_back(json_str.substr(start, i - start));
|
|
if (i < n) i++; // past closing "
|
|
}
|
|
return out;
|
|
}
|
|
|
|
static void trigger_reload() {
|
|
ImGui::GetIO().UserData = reinterpret_cast<void*>(1);
|
|
g_explorer_loaded = false;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// KPI row
|
|
// ---------------------------------------------------------------------------
|
|
|
|
void draw_kpi_row(const RegistryData& data) {
|
|
const RegistryStats& stats = data.stats;
|
|
float tested_pct = stats.total_functions > 0
|
|
? 100.0f * stats.tested_functions / stats.total_functions : 0.0f;
|
|
float pure_pct = stats.total_functions > 0
|
|
? 100.0f * stats.pure_functions / stats.total_functions : 0.0f;
|
|
|
|
const ImGuiTableFlags flags = ImGuiTableFlags_SizingStretchSame
|
|
| ImGuiTableFlags_NoPadOuterX;
|
|
|
|
// Sparkline shared: ultimos 30 dias de creacion de funciones (timeline real
|
|
// del registry). Si no hay datos cargados, queda vacio y el card mostrara
|
|
// solo valor + delta placeholder.
|
|
const float* spark_data = data.date_values.empty() ? nullptr : data.date_values.data();
|
|
const int spark_count = static_cast<int>(data.date_values.size());
|
|
|
|
// LAYOUT-TABLE — KPI/form/splitter, no data; keep BeginTable inline.
|
|
if (ImGui::BeginTable("##kpi_grid", 4, flags)) {
|
|
struct KPI { const char* label; float value; const char* fmt; const char* icon; };
|
|
const KPI cards[8] = {
|
|
{"Functions", static_cast<float>(stats.total_functions), "%.0f", TI_FUNCTION},
|
|
{"Types", static_cast<float>(stats.total_types), "%.0f", TI_HEXAGON},
|
|
{"Apps", static_cast<float>(stats.total_apps), "%.0f", TI_APPS},
|
|
{"Analysis", static_cast<float>(stats.total_analysis), "%.0f", TI_FLASK},
|
|
{"Unit Tests", static_cast<float>(stats.total_unit_tests), "%.0f", TI_TEST_PIPE},
|
|
{"Proposals", static_cast<float>(stats.total_proposals), "%.0f", TI_BULB},
|
|
{"Tested", tested_pct, "%.0f%%", TI_CIRCLE_CHECK},
|
|
{"Pure", pure_pct, "%.0f%%", TI_HEART},
|
|
};
|
|
for (int i = 0; i < 8; i++) {
|
|
if (i % 4 == 0) ImGui::TableNextRow();
|
|
ImGui::TableSetColumnIndex(i % 4);
|
|
kpi_card(cards[i].label, cards[i].value, 0.0f,
|
|
spark_data, spark_count,
|
|
cards[i].fmt, cards[i].icon);
|
|
}
|
|
ImGui::EndTable();
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Charts
|
|
// ---------------------------------------------------------------------------
|
|
|
|
static bool chart_panel_begin(const char* title, const ImVec2& size) {
|
|
using namespace fn_tokens;
|
|
ImGui::PushStyleColor(ImGuiCol_ChildBg, colors::surface);
|
|
ImGui::PushStyleColor(ImGuiCol_Border, colors::border);
|
|
ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, radius::md);
|
|
ImGui::PushStyleVar(ImGuiStyleVar_ChildBorderSize, 1.0f);
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(spacing::md, spacing::md));
|
|
|
|
ImGui::BeginChild(title, size, ImGuiChildFlags_Borders,
|
|
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Text, colors::text_muted);
|
|
ImGui::TextUnformatted(title);
|
|
ImGui::PopStyleColor();
|
|
ImGui::Separator();
|
|
return true;
|
|
}
|
|
|
|
static void chart_panel_end() {
|
|
ImGui::EndChild();
|
|
ImGui::PopStyleVar(3);
|
|
ImGui::PopStyleColor(2);
|
|
}
|
|
|
|
void draw_charts(RegistryData& data, float height) {
|
|
const ImGuiTableFlags flags = ImGuiTableFlags_SizingStretchSame
|
|
| ImGuiTableFlags_NoPadOuterX;
|
|
const float plot_h = height - 48.0f;
|
|
|
|
// LAYOUT-TABLE — KPI/form/splitter, no data; keep BeginTable inline.
|
|
if (ImGui::BeginTable("##chart_grid", 4, flags)) {
|
|
ImGui::TableNextRow();
|
|
|
|
ImGui::TableSetColumnIndex(0);
|
|
{
|
|
ImVec2 sz(ImGui::GetContentRegionAvail().x, height);
|
|
chart_panel_begin("By Language", sz);
|
|
auto labels = to_cstr(data.lang_labels);
|
|
if (!labels.empty())
|
|
bar_chart("##lang", labels.data(), data.lang_values.data(),
|
|
static_cast<int>(labels.size()), 0.67f, plot_h);
|
|
chart_panel_end();
|
|
}
|
|
ImGui::TableSetColumnIndex(1);
|
|
{
|
|
ImVec2 sz(ImGui::GetContentRegionAvail().x, height);
|
|
chart_panel_begin("By Domain", sz);
|
|
auto labels = to_cstr(data.domain_labels);
|
|
if (!labels.empty())
|
|
bar_chart("##domain", labels.data(), data.domain_values.data(),
|
|
static_cast<int>(labels.size()), 0.67f, plot_h);
|
|
chart_panel_end();
|
|
}
|
|
ImGui::TableSetColumnIndex(2);
|
|
{
|
|
ImVec2 sz(ImGui::GetContentRegionAvail().x, height);
|
|
chart_panel_begin("Purity", sz);
|
|
const char* labels[] = {"Pure", "Impure"};
|
|
float values[] = {static_cast<float>(data.stats.pure_functions),
|
|
static_cast<float>(data.stats.impure_functions)};
|
|
pie_chart("##purity", labels, values, 2, 0.0f, plot_h);
|
|
chart_panel_end();
|
|
}
|
|
ImGui::TableSetColumnIndex(3);
|
|
{
|
|
ImVec2 sz(ImGui::GetContentRegionAvail().x, height);
|
|
chart_panel_begin("Kind", sz);
|
|
auto labels = to_cstr(data.kind_labels);
|
|
if (!labels.empty())
|
|
pie_chart("##kind", labels.data(), data.kind_values.data(),
|
|
static_cast<int>(labels.size()), 0.0f, plot_h);
|
|
chart_panel_end();
|
|
}
|
|
ImGui::EndTable();
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Tables
|
|
// ---------------------------------------------------------------------------
|
|
|
|
void draw_recent_functions(const std::vector<FunctionRow>& funcs) {
|
|
if (funcs.empty()) {
|
|
empty_state("( no data )", "No functions yet",
|
|
"Run 'fn index' to populate the registry");
|
|
return;
|
|
}
|
|
data_table::TableInput ti;
|
|
auto backing = make_cells_recent_funcs(funcs, ti);
|
|
std::vector<const char*> ptrs;
|
|
cells_to_ptrs(backing, ptrs);
|
|
ti.cells = ptrs.data();
|
|
|
|
ImGui::BeginChild("##recent_dt_wrap", ImVec2(-1, -1));
|
|
data_table::render("##dt_recent_funcs", {ti}, g_dt_recent_funcs);
|
|
ImGui::EndChild();
|
|
}
|
|
|
|
void draw_apps_list(const std::vector<AppRow>& apps) {
|
|
if (apps.empty()) {
|
|
empty_state("( no data )", "No apps registered",
|
|
"Use the + Add button above or run 'fn sync'");
|
|
return;
|
|
}
|
|
data_table::TableInput ti;
|
|
auto backing = make_cells_apps(apps, ti);
|
|
std::vector<const char*> ptrs;
|
|
cells_to_ptrs(backing, ptrs);
|
|
ti.cells = ptrs.data();
|
|
|
|
ImGui::BeginChild("##apps_dt_wrap", ImVec2(-1, -1));
|
|
data_table::render("##dt_apps", {ti}, g_dt_apps);
|
|
ImGui::EndChild();
|
|
}
|
|
|
|
void draw_analysis_list(const std::vector<AnalysisRow>& analyses) {
|
|
if (analyses.empty()) {
|
|
empty_state("( no data )", "No analysis yet",
|
|
"Use the + Add button above with kind = Analysis");
|
|
return;
|
|
}
|
|
data_table::TableInput ti;
|
|
auto backing = make_cells_analysis(analyses, ti);
|
|
std::vector<const char*> ptrs;
|
|
cells_to_ptrs(backing, ptrs);
|
|
ti.cells = ptrs.data();
|
|
|
|
ImGui::BeginChild("##analysis_dt_wrap", ImVec2(-1, -1));
|
|
data_table::render("##dt_analysis", {ti}, g_dt_analysis);
|
|
ImGui::EndChild();
|
|
}
|
|
|
|
void draw_types_list(const std::vector<TypeRow>& types) {
|
|
if (types.empty()) {
|
|
empty_state("( no data )", "No types yet",
|
|
"Types are indexed from the registry alongside functions");
|
|
return;
|
|
}
|
|
data_table::TableInput ti;
|
|
auto backing = make_cells_types(types, ti);
|
|
std::vector<const char*> ptrs;
|
|
cells_to_ptrs(backing, ptrs);
|
|
ti.cells = ptrs.data();
|
|
|
|
ImGui::BeginChild("##types_dt_wrap", ImVec2(-1, -1));
|
|
data_table::render("##dt_types", {ti}, g_dt_types);
|
|
ImGui::EndChild();
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Monitor tab (issue 0086) — reads from ops:call_monitor.
|
|
// Pestana principal del dashboard. Bucle reactivo: construir / ejecutar /
|
|
// recopilar / analizar / mejorar lo vigila desde aqui.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
static void draw_monitor_toolbar(RegistryData& data) {
|
|
fn_ui::toolbar_begin();
|
|
|
|
// Window preset selector. Si cambia, marcamos reload_request para que
|
|
// main.cpp recargue solo claude (no toca registry entero).
|
|
ImGui::TextUnformatted("Window:");
|
|
ImGui::SameLine();
|
|
ImGui::SetNextItemWidth(110.0f);
|
|
if (ImGui::BeginCombo("##monitor_window", kMonitorWindowLabels[g_monitor_window_idx])) {
|
|
const int n = (int)(sizeof(kMonitorWindowLabels) / sizeof(kMonitorWindowLabels[0]));
|
|
for (int i = 0; i < n; i++) {
|
|
const bool selected = (i == g_monitor_window_idx);
|
|
if (ImGui::Selectable(kMonitorWindowLabels[i], selected)) {
|
|
if (i != g_monitor_window_idx) {
|
|
g_monitor_window_idx = i;
|
|
data.claude.window_secs = kMonitorWindowSecs[i];
|
|
g_monitor_reload_request = true;
|
|
}
|
|
}
|
|
if (selected) ImGui::SetItemDefaultFocus();
|
|
}
|
|
ImGui::EndCombo();
|
|
}
|
|
|
|
// Scatter axis window — separado de la ventana de KPIs.
|
|
ImGui::SameLine();
|
|
ImGui::TextUnformatted("Scatter:");
|
|
ImGui::SameLine();
|
|
ImGui::SetNextItemWidth(90.0f);
|
|
if (ImGui::BeginCombo("##scatter_window", kScatterWindowLabels[g_scatter_window_idx])) {
|
|
const int n = (int)(sizeof(kScatterWindowLabels) / sizeof(kScatterWindowLabels[0]));
|
|
for (int i = 0; i < n; i++) {
|
|
const bool selected = (i == g_scatter_window_idx);
|
|
if (ImGui::Selectable(kScatterWindowLabels[i], selected)) {
|
|
g_scatter_window_idx = i;
|
|
}
|
|
if (selected) ImGui::SetItemDefaultFocus();
|
|
}
|
|
ImGui::EndCombo();
|
|
}
|
|
|
|
ImGui::SameLine();
|
|
if (fn_ui::button("Refresh", fn_ui::ButtonVariant::Subtle)) {
|
|
g_monitor_reload_request = true;
|
|
}
|
|
|
|
// Live LED: verde si WS conectado, gris si caido. Ts ultimo evento.
|
|
ImGui::SameLine();
|
|
ImGui::Dummy(ImVec2(fn_tokens::spacing::lg, 0));
|
|
ImGui::SameLine();
|
|
const ImVec4 dot_col = g_monitor_ws_connected
|
|
? ImVec4(0.30f, 0.85f, 0.40f, 1.0f)
|
|
: ImVec4(0.55f, 0.55f, 0.55f, 1.0f);
|
|
ImGui::TextColored(dot_col, "%s", g_monitor_ws_connected ? TI_POINT : TI_CIRCLE_DOTTED);
|
|
ImGui::SameLine();
|
|
ImGui::TextUnformatted(g_monitor_ws_connected ? "live" : "offline");
|
|
if (g_monitor_last_event_ts > 0) {
|
|
ImGui::SameLine();
|
|
ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::text_muted);
|
|
const std::string rel = format_ts_relative(g_monitor_last_event_ts);
|
|
ImGui::Text("(last event: %s)", rel.c_str());
|
|
ImGui::PopStyleColor();
|
|
}
|
|
|
|
fn_ui::toolbar_end();
|
|
}
|
|
|
|
void draw_monitor(RegistryData& data) {
|
|
auto& cu = data.claude;
|
|
|
|
// Toolbar siempre visible (date filter + LED) incluso si call_monitor caido.
|
|
draw_monitor_toolbar(data);
|
|
ImGui::Dummy(ImVec2(0, fn_tokens::spacing::sm));
|
|
|
|
if (!cu.available) {
|
|
ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::text_muted);
|
|
ImGui::TextWrapped("%s call_monitor.operations.db no esta accesible.", TI_ALERT_CIRCLE);
|
|
ImGui::TextWrapped("Inicializa con: ./projects/fn_monitoring/apps/call_monitor/call_monitor init");
|
|
ImGui::TextWrapped("Despues `systemctl --user restart sqlite_api` para que el datasource ops:call_monitor sea descubierto.");
|
|
ImGui::PopStyleColor();
|
|
return;
|
|
}
|
|
|
|
// 7 KPI cards: Calls / MCP / Reg% / Errors / Violations / Copies / Versions
|
|
// "MCP" = calls Claude lanza via tools registry-aware (mcp / fn_cli_run /
|
|
// heredoc). "Reg %" = porcentaje del total con function_id no vacio.
|
|
// LAYOUT-TABLE — KPI/form/splitter, no data; keep BeginTable inline.
|
|
const ImGuiTableFlags flags = ImGuiTableFlags_SizingStretchSame | ImGuiTableFlags_NoPadOuterX;
|
|
if (ImGui::BeginTable("##monitor_kpi", 7, flags)) {
|
|
struct KPI { const char* label; float value; const char* icon; const char* fmt; };
|
|
const KPI cards[7] = {
|
|
{"Calls", static_cast<float>(cu.total_calls), TI_ACTIVITY, "%.0f"},
|
|
{"MCP", static_cast<float>(cu.total_mcp), TI_PLUG_CONNECTED, "%.0f"},
|
|
{"Reg %", static_cast<float>(cu.registry_pct), TI_PERCENTAGE, "%.1f%%"},
|
|
{"Errors", static_cast<float>(cu.total_errors), TI_ALERT_TRIANGLE, "%.0f"},
|
|
{"Violations", static_cast<float>(cu.total_violations), TI_ALERT_CIRCLE, "%.0f"},
|
|
{"Copies", static_cast<float>(cu.total_copies), TI_COPY, "%.0f"},
|
|
{"Versions", static_cast<float>(cu.total_versions), TI_HISTORY, "%.0f"},
|
|
};
|
|
ImGui::TableNextRow();
|
|
for (int i = 0; i < 7; i++) {
|
|
ImGui::TableSetColumnIndex(i);
|
|
kpi_card(cards[i].label, cards[i].value, 0.0f, nullptr, 0,
|
|
cards[i].fmt, cards[i].icon);
|
|
}
|
|
ImGui::EndTable();
|
|
}
|
|
ImGui::Dummy(ImVec2(0, fn_tokens::spacing::md));
|
|
|
|
// Live scatter — duracion (ms) por ejecucion, X = ts en tiempo real.
|
|
// Ventana del eje X configurable via combo "Scatter" del toolbar (default
|
|
// 5 min). Eje Y dinamico: 0..max_dentro_de_ventana + 500ms — asi siempre
|
|
// hay headroom y el plot se reescala con picos sin perder la base.
|
|
// ImPlot auto-scrollea fijando el limite derecho a `now` cada frame.
|
|
// Dos series (ok/error) para colorear sin getter custom.
|
|
{
|
|
const double now = static_cast<double>(std::time(nullptr));
|
|
const int win_s = kScatterWindowSecs[g_scatter_window_idx];
|
|
const double left = now - static_cast<double>(win_s);
|
|
|
|
// Reuse buffers entre frames; reset cada frame es O(N) sobre <=200 filas.
|
|
static std::vector<double> ok_x, ok_y, err_x, err_y;
|
|
ok_x.clear(); ok_y.clear(); err_x.clear(); err_y.clear();
|
|
ok_x.reserve(cu.recent_executions.size());
|
|
ok_y.reserve(cu.recent_executions.size());
|
|
double y_max_in_window = 0.0;
|
|
for (const auto& r : cu.recent_executions) {
|
|
double x = static_cast<double>(r.ts);
|
|
double y = static_cast<double>(r.duration_ms);
|
|
// Solo considera y_max sobre puntos visibles para que el rescale
|
|
// no se quede pillado en un pico antiguo fuera de ventana.
|
|
if (x >= left && y > y_max_in_window) y_max_in_window = y;
|
|
if (r.success) { ok_x.push_back(x); ok_y.push_back(y); }
|
|
else { err_x.push_back(x); err_y.push_back(y); }
|
|
}
|
|
const double y_top = y_max_in_window + 500.0;
|
|
|
|
// Forzar localtime en ImPlot — por defecto formatea como UTC y se
|
|
// desincroniza con el resto de la app que usa hora local.
|
|
ImPlot::GetStyle().UseLocalTime = true;
|
|
|
|
if (ImPlot::BeginPlot("##monitor_scatter", ImVec2(-1, 200),
|
|
ImPlotFlags_NoTitle | ImPlotFlags_NoMouseText)) {
|
|
// NoHighlight evita el efecto de iluminacion del fondo del eje
|
|
// cuando el cursor pasa por encima (era visualmente ruidoso).
|
|
ImPlot::SetupAxis(ImAxis_X1, "time",
|
|
ImPlotAxisFlags_NoGridLines | ImPlotAxisFlags_NoHighlight);
|
|
ImPlot::SetupAxisScale(ImAxis_X1, ImPlotScale_Time);
|
|
ImPlot::SetupAxisLimits(ImAxis_X1, left, now + 5.0, ImPlotCond_Always);
|
|
ImPlot::SetupAxis(ImAxis_Y1, "duration (ms)",
|
|
ImPlotAxisFlags_NoHighlight);
|
|
ImPlot::SetupAxisLimits(ImAxis_Y1, 0.0, y_top, ImPlotCond_Always);
|
|
|
|
// Marcadores. Verde = ok, rojo = error. ImPlot v1.0+ usa ImPlotSpec
|
|
// en lugar de SetNextMarkerStyle.
|
|
if (!ok_x.empty()) {
|
|
ImPlotSpec spec_ok;
|
|
spec_ok.Marker = ImPlotMarker_Circle;
|
|
spec_ok.MarkerSize = 4.0f;
|
|
spec_ok.MarkerFillColor = ImVec4(0.30f, 0.85f, 0.40f, 0.85f);
|
|
spec_ok.MarkerLineColor = ImVec4(0.30f, 0.85f, 0.40f, 1.0f);
|
|
spec_ok.LineColor = ImVec4(0.30f, 0.85f, 0.40f, 1.0f);
|
|
ImPlot::PlotScatter("ok", ok_x.data(), ok_y.data(),
|
|
static_cast<int>(ok_x.size()), spec_ok);
|
|
}
|
|
if (!err_x.empty()) {
|
|
ImPlotSpec spec_err;
|
|
spec_err.Marker = ImPlotMarker_Cross;
|
|
spec_err.MarkerSize = 5.0f;
|
|
spec_err.MarkerFillColor = ImVec4(0.95f, 0.35f, 0.30f, 0.95f);
|
|
spec_err.MarkerLineColor = ImVec4(0.95f, 0.35f, 0.30f, 1.0f);
|
|
spec_err.LineColor = ImVec4(0.95f, 0.35f, 0.30f, 1.0f);
|
|
ImPlot::PlotScatter("error", err_x.data(), err_y.data(),
|
|
static_cast<int>(err_x.size()), spec_err);
|
|
}
|
|
|
|
// Hover tooltip: localiza el punto mas cercano al cursor (en
|
|
// pixel-space, no plot-space, para que la tolerancia sea uniforme
|
|
// sin importar la escala del eje Y) y muestra function/tool/ms.
|
|
if (ImPlot::IsPlotHovered() && !cu.recent_executions.empty()) {
|
|
const ImVec2 mp_px = ImGui::GetIO().MousePos;
|
|
const double kHitRadiusPx = 14.0;
|
|
double best_dist = kHitRadiusPx;
|
|
const RecentExecutionRow* best = nullptr;
|
|
for (const auto& r : cu.recent_executions) {
|
|
double x = static_cast<double>(r.ts);
|
|
double y = static_cast<double>(r.duration_ms);
|
|
if (x < left || x > now + 5.0) continue;
|
|
ImVec2 px = ImPlot::PlotToPixels(x, y);
|
|
double dx = px.x - mp_px.x;
|
|
double dy = px.y - mp_px.y;
|
|
double d = std::sqrt(dx * dx + dy * dy);
|
|
if (d < best_dist) {
|
|
best_dist = d;
|
|
best = &r;
|
|
}
|
|
}
|
|
if (best != nullptr) {
|
|
ImGui::BeginTooltip();
|
|
ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::text_muted);
|
|
ImGui::TextUnformatted(format_ts(best->ts).c_str());
|
|
ImGui::PopStyleColor();
|
|
ImGui::Separator();
|
|
ImGui::Text("Function: %s", best->function_id.empty()
|
|
? "-" : best->function_id.c_str());
|
|
ImGui::Text("Tool: %s", best->tool_used.c_str());
|
|
ImGui::Text("Duration: %d ms", best->duration_ms);
|
|
if (!best->success) {
|
|
ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::error);
|
|
ImGui::Text("Error: %s", best->error_class.c_str());
|
|
ImGui::PopStyleColor();
|
|
}
|
|
ImGui::EndTooltip();
|
|
}
|
|
}
|
|
|
|
ImPlot::EndPlot();
|
|
}
|
|
}
|
|
ImGui::Dummy(ImVec2(0, fn_tokens::spacing::md));
|
|
|
|
// Sub-tabs: Recent Executions (primera) / Top Functions / Violations / Copies
|
|
if (ImGui::BeginTabBar("##monitor_sub_tabs")) {
|
|
if (ImGui::BeginTabItem("Recent Executions")) {
|
|
// Filtro: solo calls a funciones del registry (function_id no vacio).
|
|
ImGui::Checkbox("Only registry functions", &g_recent_only_registry);
|
|
ImGui::SameLine();
|
|
ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::text_muted);
|
|
int total_shown = 0, total_all = 0;
|
|
for (const auto& r : cu.recent_executions) {
|
|
total_all++;
|
|
if (!g_recent_only_registry || !r.function_id.empty()) total_shown++;
|
|
}
|
|
ImGui::Text("(%d/%d)", total_shown, total_all);
|
|
ImGui::PopStyleColor();
|
|
ImGui::Dummy(ImVec2(0, fn_tokens::spacing::xs));
|
|
|
|
if (cu.recent_executions.empty()) {
|
|
ImGui::TextDisabled("No executions in this window. Try widening (7d/30d/All) or wait for the next call.");
|
|
} else {
|
|
// Build TableInput for data_table::render (issue 0081-J).
|
|
// Columns: When | Function | Tool | Duration(ms) | Status
|
|
// Status = "ok" | "error" (from r.success) → Badge renderer.
|
|
// Duration → Duration renderer (warn=500ms, error=2000ms).
|
|
data_table::TableInput ti;
|
|
ti.name = "monitor_recent";
|
|
ti.headers = {"When", "Function", "Tool", "Duration", "Status"};
|
|
ti.types = {
|
|
data_table::ColumnType::String,
|
|
data_table::ColumnType::String,
|
|
data_table::ColumnType::String,
|
|
data_table::ColumnType::Float,
|
|
data_table::ColumnType::String,
|
|
};
|
|
|
|
// column_specs: parallel to headers (5 specs)
|
|
ti.column_specs.resize(5);
|
|
// col 0 When: Text (default)
|
|
ti.column_specs[0].renderer = data_table::CellRenderer::Text;
|
|
// col 1 Function: Text (default)
|
|
ti.column_specs[1].renderer = data_table::CellRenderer::Text;
|
|
// col 2 Tool: Text (default)
|
|
ti.column_specs[2].renderer = data_table::CellRenderer::Text;
|
|
// col 3 Duration: Duration renderer
|
|
ti.column_specs[3].renderer = data_table::CellRenderer::Duration;
|
|
ti.column_specs[3].duration_warn_ms = 500.0f;
|
|
ti.column_specs[3].duration_error_ms = 2000.0f;
|
|
// col 4 Status: Badge renderer
|
|
ti.column_specs[4].renderer = data_table::CellRenderer::Badge;
|
|
ti.column_specs[4].badges = {
|
|
{"ok", "#22c55e", "OK"},
|
|
{"error", "#ef4444", "Error"},
|
|
{"running", "#3b82f6", "Running"},
|
|
{"timeout", "#f59e0b", "Timeout"},
|
|
};
|
|
|
|
std::vector<std::string> backing;
|
|
backing.reserve(cu.recent_executions.size() * 5);
|
|
char dur_buf[24];
|
|
for (const auto& r : cu.recent_executions) {
|
|
if (g_recent_only_registry && r.function_id.empty()) continue;
|
|
// When
|
|
backing.push_back(format_ts(r.ts));
|
|
// Function — registry call or $ snippet or "-"
|
|
if (!r.function_id.empty()) {
|
|
backing.push_back(r.function_id);
|
|
} else if (!r.command_snippet.empty()) {
|
|
char sbuf[88];
|
|
std::snprintf(sbuf, sizeof(sbuf), "$ %.80s%s",
|
|
r.command_snippet.c_str(),
|
|
r.command_snippet.size() > 80 ? "..." : "");
|
|
backing.push_back(sbuf);
|
|
} else {
|
|
backing.push_back("-");
|
|
}
|
|
// Tool
|
|
backing.push_back(r.tool_used);
|
|
// Duration (as numeric string for Float column)
|
|
std::snprintf(dur_buf, sizeof(dur_buf), "%d", r.duration_ms);
|
|
backing.push_back(dur_buf);
|
|
// Status
|
|
backing.push_back(r.success ? "ok" : "error");
|
|
}
|
|
|
|
int nrows = 0;
|
|
for (const auto& r : cu.recent_executions)
|
|
if (!g_recent_only_registry || !r.function_id.empty()) nrows++;
|
|
ti.rows = nrows;
|
|
ti.cols = static_cast<int>(ti.headers.size());
|
|
|
|
std::vector<const char*> ptrs;
|
|
cells_to_ptrs(backing, ptrs);
|
|
ti.cells = ptrs.data();
|
|
|
|
ImGui::BeginChild("##monitor_recent_wrap", ImVec2(-1, -1));
|
|
data_table::render("##dt_monitor_recent", {ti}, g_dt_monitor_recent);
|
|
ImGui::EndChild();
|
|
}
|
|
ImGui::EndTabItem();
|
|
}
|
|
if (ImGui::BeginTabItem("Top Functions")) {
|
|
if (cu.top_functions.empty()) {
|
|
ImGui::TextDisabled("No function calls recorded yet. Hook fires on next session.");
|
|
} else {
|
|
// Build TableInput for data_table::render
|
|
data_table::TableInput ti;
|
|
ti.name = "top_functions";
|
|
ti.headers = {"Function ID", "Calls", "7d", "Errors", "Error %", "Mean ms"};
|
|
ti.types = {
|
|
data_table::ColumnType::String,
|
|
data_table::ColumnType::Int,
|
|
data_table::ColumnType::Int,
|
|
data_table::ColumnType::Int,
|
|
data_table::ColumnType::Float,
|
|
data_table::ColumnType::Float,
|
|
};
|
|
ti.rows = static_cast<int>(cu.top_functions.size());
|
|
ti.cols = static_cast<int>(ti.headers.size());
|
|
|
|
std::vector<std::string> backing;
|
|
backing.reserve(cu.top_functions.size() * 6);
|
|
char buf[32];
|
|
for (const auto& r : cu.top_functions) {
|
|
backing.push_back(r.function_id);
|
|
std::snprintf(buf, sizeof(buf), "%d", r.calls_total);
|
|
backing.push_back(buf);
|
|
std::snprintf(buf, sizeof(buf), "%d", r.calls_7d);
|
|
backing.push_back(buf);
|
|
std::snprintf(buf, sizeof(buf), "%d", r.errors_total);
|
|
backing.push_back(buf);
|
|
std::snprintf(buf, sizeof(buf), "%.1f%%", r.error_rate * 100.0);
|
|
backing.push_back(buf);
|
|
std::snprintf(buf, sizeof(buf), "%.0f", r.mean_duration_ms);
|
|
backing.push_back(buf);
|
|
}
|
|
std::vector<const char*> ptrs;
|
|
cells_to_ptrs(backing, ptrs);
|
|
ti.cells = ptrs.data();
|
|
|
|
ImGui::BeginChild("##top_fn_dt_wrap", ImVec2(-1, -1));
|
|
data_table::render("##dt_top_fn", {ti}, g_dt_top_fn);
|
|
ImGui::EndChild();
|
|
}
|
|
ImGui::EndTabItem();
|
|
}
|
|
if (ImGui::BeginTabItem("Violations")) {
|
|
if (cu.recent_violations.empty()) {
|
|
ImGui::TextDisabled("No antipattern violations detected.");
|
|
} else {
|
|
data_table::TableInput ti;
|
|
ti.name = "violations";
|
|
ti.headers = {"When", "Rule", "Severity", "Function", "Snippet"};
|
|
ti.types = {
|
|
data_table::ColumnType::String,
|
|
data_table::ColumnType::String,
|
|
data_table::ColumnType::String,
|
|
data_table::ColumnType::String,
|
|
data_table::ColumnType::String,
|
|
};
|
|
ti.rows = static_cast<int>(cu.recent_violations.size());
|
|
ti.cols = static_cast<int>(ti.headers.size());
|
|
|
|
std::vector<std::string> backing;
|
|
backing.reserve(cu.recent_violations.size() * 5);
|
|
for (const auto& r : cu.recent_violations) {
|
|
backing.push_back(format_ts(r.ts));
|
|
backing.push_back(r.rule_id);
|
|
backing.push_back(r.severity);
|
|
backing.push_back(r.function_id);
|
|
backing.push_back(r.command_snippet);
|
|
}
|
|
std::vector<const char*> ptrs;
|
|
cells_to_ptrs(backing, ptrs);
|
|
ti.cells = ptrs.data();
|
|
|
|
ImGui::BeginChild("##viol_dt_wrap", ImVec2(-1, -1));
|
|
data_table::render("##dt_violations", {ti}, g_dt_violations);
|
|
ImGui::EndChild();
|
|
}
|
|
ImGui::EndTabItem();
|
|
}
|
|
if (ImGui::BeginTabItem("Failed Functions")) {
|
|
// Subset de recent_executions: solo calls que golpean una funcion
|
|
// del registry Y fallaron. Util para diagnostico: cuales funciones
|
|
// del registry rompen mas + bug analysis cuando objetivo 1+2 caen.
|
|
std::vector<const RecentExecutionRow*> failed;
|
|
for (const auto& r : cu.recent_executions) {
|
|
if (!r.success && !r.function_id.empty()) failed.push_back(&r);
|
|
}
|
|
if (failed.empty()) {
|
|
ImGui::TextDisabled("No registry-function failures in this window. Healthy.");
|
|
} else {
|
|
// Build TableInput for data_table::render (issue 0081-J).
|
|
// Columns: When | Function | Tool | Error Class | Snippet
|
|
// Error Class → Badge renderer with common error class colors.
|
|
data_table::TableInput ti;
|
|
ti.name = "monitor_failed";
|
|
ti.headers = {"When", "Function", "Tool", "Error Class", "Snippet"};
|
|
ti.types = {
|
|
data_table::ColumnType::String,
|
|
data_table::ColumnType::String,
|
|
data_table::ColumnType::String,
|
|
data_table::ColumnType::String,
|
|
data_table::ColumnType::String,
|
|
};
|
|
|
|
// column_specs: parallel to headers (5 specs)
|
|
ti.column_specs.resize(5);
|
|
// col 0 When: Text (default)
|
|
ti.column_specs[0].renderer = data_table::CellRenderer::Text;
|
|
// col 1 Function: Text (default) — already in error context, no extra badge needed
|
|
ti.column_specs[1].renderer = data_table::CellRenderer::Text;
|
|
// col 2 Tool: Text (default)
|
|
ti.column_specs[2].renderer = data_table::CellRenderer::Text;
|
|
// col 3 Error Class: Badge renderer
|
|
ti.column_specs[3].renderer = data_table::CellRenderer::Badge;
|
|
ti.column_specs[3].badges = {
|
|
{"sqlite", "#f59e0b", "SQLite"},
|
|
{"network", "#3b82f6", "Network"},
|
|
{"timeout", "#f59e0b", "Timeout"},
|
|
{"not_found", "#8b5cf6", "Not Found"},
|
|
{"auth", "#ef4444", "Auth"},
|
|
{"parse", "#ec4899", "Parse"},
|
|
{"io", "#06b6d4", "I/O"},
|
|
{"permission", "#ef4444", "Permission"},
|
|
{"unknown", "#6b7280", "Unknown"},
|
|
};
|
|
// col 4 Snippet: Text (default)
|
|
ti.column_specs[4].renderer = data_table::CellRenderer::Text;
|
|
|
|
std::vector<std::string> backing;
|
|
backing.reserve(failed.size() * 5);
|
|
for (const auto* p : failed) {
|
|
const auto& r = *p;
|
|
backing.push_back(format_ts(r.ts));
|
|
backing.push_back(r.function_id);
|
|
backing.push_back(r.tool_used);
|
|
// error_class: normalize empty to "unknown" for badge matching
|
|
backing.push_back(r.error_class.empty() ? "unknown" : r.error_class);
|
|
// snippet: truncate to ~110 chars for table display
|
|
if (r.error_snippet.empty()) {
|
|
backing.push_back("-");
|
|
} else {
|
|
char sbuf[120];
|
|
std::snprintf(sbuf, sizeof(sbuf), "%.110s%s",
|
|
r.error_snippet.c_str(),
|
|
r.error_snippet.size() > 110 ? "..." : "");
|
|
backing.push_back(sbuf);
|
|
}
|
|
}
|
|
|
|
ti.rows = static_cast<int>(failed.size());
|
|
ti.cols = static_cast<int>(ti.headers.size());
|
|
|
|
std::vector<const char*> ptrs;
|
|
cells_to_ptrs(backing, ptrs);
|
|
ti.cells = ptrs.data();
|
|
|
|
ImGui::BeginChild("##monitor_failed_wrap", ImVec2(-1, -1));
|
|
data_table::render("##dt_monitor_failed", {ti}, g_dt_monitor_failed);
|
|
ImGui::EndChild();
|
|
}
|
|
ImGui::EndTabItem();
|
|
}
|
|
if (ImGui::BeginTabItem("Copied Code")) {
|
|
if (cu.copies.empty()) {
|
|
ImGui::TextDisabled("No copied code detected. Run `fn doctor copied-code` or `call_monitor copied-code`.");
|
|
} else {
|
|
data_table::TableInput ti;
|
|
ti.name = "copies";
|
|
ti.headers = {"Kind", "Sim", "App File", "App Function", "Registry ID"};
|
|
ti.types = {
|
|
data_table::ColumnType::String,
|
|
data_table::ColumnType::Float,
|
|
data_table::ColumnType::String,
|
|
data_table::ColumnType::String,
|
|
data_table::ColumnType::String,
|
|
};
|
|
ti.rows = static_cast<int>(cu.copies.size());
|
|
ti.cols = static_cast<int>(ti.headers.size());
|
|
|
|
std::vector<std::string> backing;
|
|
backing.reserve(cu.copies.size() * 5);
|
|
char buf[32];
|
|
for (const auto& r : cu.copies) {
|
|
backing.push_back(r.kind);
|
|
std::snprintf(buf, sizeof(buf), "%.2f", r.similarity);
|
|
backing.push_back(buf);
|
|
backing.push_back(r.app_file);
|
|
backing.push_back(r.app_function);
|
|
backing.push_back(r.registry_id);
|
|
}
|
|
std::vector<const char*> ptrs;
|
|
cells_to_ptrs(backing, ptrs);
|
|
ti.cells = ptrs.data();
|
|
|
|
ImGui::BeginChild("##copies_dt_wrap", ImVec2(-1, -1));
|
|
data_table::render("##dt_copies", {ti}, g_dt_copies);
|
|
ImGui::EndChild();
|
|
}
|
|
ImGui::EndTabItem();
|
|
}
|
|
ImGui::EndTabBar();
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Projects view
|
|
// ---------------------------------------------------------------------------
|
|
|
|
static std::string g_selected_project_id = "";
|
|
static ProjectDetail g_project_detail;
|
|
|
|
static void refresh_project_detail() {
|
|
g_project_detail = ProjectDetail{};
|
|
if (g_selected_project_id.empty() || g_api_url.empty()) return;
|
|
load_project_detail_http(g_api_url, g_selected_project_id, g_project_detail);
|
|
}
|
|
|
|
void draw_projects_list(RegistryData& data) {
|
|
if (data.projects.empty() && data.orphan_apps == 0
|
|
&& data.orphan_analyses == 0 && data.orphan_vaults == 0) {
|
|
empty_state("( no data )", "No projects yet",
|
|
"Create a project under projects/{name}/ with project.md and reindex");
|
|
return;
|
|
}
|
|
|
|
// Dos columnas: izquierda arbol, derecha detalle.
|
|
// LAYOUT-TABLE — KPI/form/splitter, no data; keep BeginTable inline.
|
|
const ImGuiTableFlags flags = ImGuiTableFlags_Resizable
|
|
| ImGuiTableFlags_SizingStretchProp;
|
|
if (!ImGui::BeginTable("##proj_layout", 2, flags)) return;
|
|
|
|
ImGui::TableSetupColumn("tree", ImGuiTableColumnFlags_WidthStretch, 1.0f);
|
|
ImGui::TableSetupColumn("detail", ImGuiTableColumnFlags_WidthStretch, 2.5f);
|
|
ImGui::TableNextRow();
|
|
|
|
// --- Left column: tree ---
|
|
ImGui::TableSetColumnIndex(0);
|
|
ImGui::BeginChild("##proj_tree", ImVec2(0, 0), ImGuiChildFlags_Borders);
|
|
for (const auto& p : data.projects) {
|
|
bool sel = (g_selected_project_id == p.id);
|
|
char label[256];
|
|
std::snprintf(label, sizeof(label), "%s [%d/%d/%d]",
|
|
p.name.c_str(), p.apps_count, p.analyses_count, p.vaults_count);
|
|
fn_ui::tree_leaf(p.id.c_str(), label, sel);
|
|
if (fn_ui::tree_node_clicked()) {
|
|
g_selected_project_id = p.id;
|
|
refresh_project_detail();
|
|
}
|
|
}
|
|
// Orphans
|
|
if (data.orphan_apps + data.orphan_analyses + data.orphan_vaults > 0) {
|
|
bool sel = (g_selected_project_id == "orphans");
|
|
char label[128];
|
|
std::snprintf(label, sizeof(label), "(orphans) [%d/%d/%d]",
|
|
data.orphan_apps, data.orphan_analyses, data.orphan_vaults);
|
|
fn_ui::tree_leaf("orphans", label, sel);
|
|
if (fn_ui::tree_node_clicked()) {
|
|
g_selected_project_id = "orphans";
|
|
refresh_project_detail();
|
|
}
|
|
}
|
|
ImGui::EndChild();
|
|
|
|
// --- Right column: detail ---
|
|
ImGui::TableSetColumnIndex(1);
|
|
ImGui::BeginChild("##proj_detail", ImVec2(0, 0), ImGuiChildFlags_Borders);
|
|
if (g_selected_project_id.empty()) {
|
|
empty_state("\xe2\x86\x90", "Select a project",
|
|
"Click a project on the left to see its apps, analyses and vaults");
|
|
} else {
|
|
const auto& d = g_project_detail;
|
|
ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::text);
|
|
if (!d.name.empty())
|
|
ImGui::TextUnformatted(d.name.c_str());
|
|
else
|
|
ImGui::TextUnformatted(g_selected_project_id.c_str());
|
|
ImGui::PopStyleColor();
|
|
if (!d.description.empty()) {
|
|
ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::text_muted);
|
|
ImGui::TextWrapped("%s", d.description.c_str());
|
|
ImGui::PopStyleColor();
|
|
}
|
|
ImGui::Separator();
|
|
|
|
if (ImGui::BeginTabBar("##proj_tabs")) {
|
|
char tab_apps[64]; std::snprintf(tab_apps, sizeof(tab_apps), "Apps (%zu)", d.apps.size());
|
|
char tab_analyses[64]; std::snprintf(tab_analyses, sizeof(tab_analyses), "Analysis (%zu)", d.analyses.size());
|
|
char tab_vaults[64]; std::snprintf(tab_vaults, sizeof(tab_vaults), "Vaults (%zu)", d.vaults.size());
|
|
|
|
if (ImGui::BeginTabItem(tab_apps)) {
|
|
draw_apps_list(d.apps);
|
|
ImGui::EndTabItem();
|
|
}
|
|
if (ImGui::BeginTabItem(tab_analyses)) {
|
|
draw_analysis_list(d.analyses);
|
|
ImGui::EndTabItem();
|
|
}
|
|
if (ImGui::BeginTabItem(tab_vaults)) {
|
|
if (d.vaults.empty()) {
|
|
empty_state("( no data )", "No vaults in this project",
|
|
"Use the + Add button with kind = Vault (requires a project)");
|
|
} else {
|
|
data_table::TableInput ti;
|
|
auto backing = make_cells_vaults(d.vaults, ti);
|
|
std::vector<const char*> ptrs;
|
|
cells_to_ptrs(backing, ptrs);
|
|
ti.cells = ptrs.data();
|
|
ImGui::BeginChild("##vaults_dt_wrap", ImVec2(-1, -1));
|
|
data_table::render("##dt_vaults", {ti}, g_dt_vaults);
|
|
ImGui::EndChild();
|
|
}
|
|
ImGui::EndTabItem();
|
|
}
|
|
ImGui::EndTabBar();
|
|
}
|
|
}
|
|
ImGui::EndChild();
|
|
|
|
ImGui::EndTable();
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Actions bar (Reindex + Add button) + Add modal
|
|
// ---------------------------------------------------------------------------
|
|
|
|
static const char* kLangs[] = {"go", "py", "ts", "sh", "cpp"};
|
|
static const char* kDomains[] = {"core", "infra", "finance", "datascience",
|
|
"cybersecurity", "shell", "tui", "pipelines",
|
|
"browser", "viz", "gfx", "notebook"};
|
|
|
|
static void submit_add() {
|
|
if (g_api_url.empty()) {
|
|
fn_ui::toast_push(fn_ui::ToastKind::Error, "API URL not set");
|
|
return;
|
|
}
|
|
std::string name = g_add_name;
|
|
std::string desc = g_add_desc;
|
|
std::string project;
|
|
if (g_add_project_idx >= 0) {
|
|
// g_add_project_idx indexa la lista live de projects (resolved en el modal)
|
|
// guardada en g_project_ids_cache — ver draw_add_modal
|
|
}
|
|
|
|
// Resolvemos project en draw_add_modal y lo pasamos via g_add_project_resolved
|
|
extern std::string g_add_project_resolved;
|
|
project = g_add_project_resolved;
|
|
|
|
AddKind kind = static_cast<AddKind>(g_add_kind_idx);
|
|
std::string url = g_api_url;
|
|
|
|
fn_ui::runner_trigger(g_add_runner,
|
|
[kind, url, name, desc, project,
|
|
lang_idx = g_add_lang_idx, domain_idx = g_add_domain_idx,
|
|
packages = std::string(g_add_packages),
|
|
vault_path = std::string(g_add_vault_path)
|
|
](std::string& out) -> bool {
|
|
std::string body;
|
|
bool ok = false;
|
|
switch (kind) {
|
|
case AddKind::App:
|
|
ok = http_post_add_app(url, name, kLangs[lang_idx],
|
|
kDomains[domain_idx], project, desc, body);
|
|
break;
|
|
case AddKind::Analysis:
|
|
ok = http_post_add_analysis(url, name, project, packages, desc, body);
|
|
break;
|
|
case AddKind::Vault:
|
|
ok = http_post_add_vault(url, name, project, vault_path, desc, body);
|
|
break;
|
|
}
|
|
out = body;
|
|
return ok;
|
|
});
|
|
}
|
|
|
|
std::string g_add_project_resolved = "";
|
|
|
|
static void draw_add_modal(RegistryData& data) {
|
|
if (!fn_ui::modal_dialog_begin("Add...", &g_show_add, ImVec2(460, 0))) {
|
|
fn_ui::modal_dialog_end();
|
|
return;
|
|
}
|
|
|
|
// Kind selector
|
|
const char* kinds[] = {"App", "Analysis", "Vault"};
|
|
fn_ui::select("Kind", &g_add_kind_idx, kinds, 3);
|
|
|
|
// Project selector (del registro vivo)
|
|
std::vector<std::string> proj_labels;
|
|
std::vector<std::string> proj_ids;
|
|
for (auto& p : data.projects) {
|
|
proj_labels.push_back(p.name);
|
|
proj_ids.push_back(p.id);
|
|
}
|
|
std::vector<const char*> proj_cstr;
|
|
for (auto& s : proj_labels) proj_cstr.push_back(s.c_str());
|
|
|
|
AddKind kind = static_cast<AddKind>(g_add_kind_idx);
|
|
fn_ui::select("Project", &g_add_project_idx, proj_cstr.data(),
|
|
static_cast<int>(proj_cstr.size()),
|
|
kind != AddKind::Vault /* vault obliga a project */);
|
|
g_add_project_resolved = (g_add_project_idx >= 0 && g_add_project_idx < (int)proj_ids.size())
|
|
? proj_ids[g_add_project_idx] : "";
|
|
|
|
fn_ui::text_input("Name", g_add_name, sizeof(g_add_name),
|
|
"snake_case, a-z0-9_");
|
|
fn_ui::text_input("Description", g_add_desc, sizeof(g_add_desc));
|
|
|
|
// Campos especificos segun kind
|
|
if (kind == AddKind::App) {
|
|
fn_ui::select("Lang", &g_add_lang_idx, kLangs, 5);
|
|
fn_ui::select("Domain", &g_add_domain_idx, kDomains, 12);
|
|
} else if (kind == AddKind::Analysis) {
|
|
fn_ui::text_input("Packages (CSV)", g_add_packages, sizeof(g_add_packages),
|
|
"polars,scikit-learn,torch");
|
|
} else if (kind == AddKind::Vault) {
|
|
fn_ui::text_input("Path (abs, opcional)", g_add_vault_path, sizeof(g_add_vault_path),
|
|
"/home/lucas/vaults/my_data");
|
|
}
|
|
|
|
ImGui::Dummy(ImVec2(0, fn_tokens::spacing::sm));
|
|
|
|
// Status del runner en curso
|
|
fn_ui::runner_status(g_add_runner, "Creating...");
|
|
|
|
// Detectar transicion running->done para cerrar y notificar
|
|
static fn_ui::RunnerState last_state = fn_ui::RunnerState::Idle;
|
|
fn_ui::RunnerState now = g_add_runner.state();
|
|
if (last_state == fn_ui::RunnerState::Running
|
|
&& (now == fn_ui::RunnerState::Success || now == fn_ui::RunnerState::Error)) {
|
|
const bool ok = (now == fn_ui::RunnerState::Success);
|
|
fn_ui::toast_push(ok ? fn_ui::ToastKind::Success : fn_ui::ToastKind::Error,
|
|
ok ? "Created OK — reloading" : g_add_runner.message().c_str());
|
|
if (ok) {
|
|
g_show_add = false;
|
|
g_add_name[0] = '\0';
|
|
g_add_desc[0] = '\0';
|
|
g_add_packages[0] = '\0';
|
|
g_add_vault_path[0] = '\0';
|
|
trigger_reload();
|
|
}
|
|
}
|
|
last_state = now;
|
|
|
|
ImGui::Separator();
|
|
if (fn_ui::button("Cancel", fn_ui::ButtonVariant::Subtle)) g_show_add = false;
|
|
ImGui::SameLine();
|
|
const bool disabled = g_add_runner.is_busy() || g_add_name[0] == '\0';
|
|
if (disabled) ImGui::BeginDisabled();
|
|
if (fn_ui::button("Create", fn_ui::ButtonVariant::Primary)) {
|
|
submit_add();
|
|
}
|
|
if (disabled) ImGui::EndDisabled();
|
|
|
|
fn_ui::modal_dialog_end();
|
|
}
|
|
|
|
static void draw_actions_bar() {
|
|
if (g_api_url.empty()) return;
|
|
|
|
fn_ui::toolbar_begin();
|
|
if (fn_ui::button("Reindex", fn_ui::ButtonVariant::Primary)
|
|
&& !g_reindex_runner.is_busy()) {
|
|
const std::string url = g_api_url;
|
|
fn_ui::runner_trigger(g_reindex_runner, [url](std::string& out) -> bool {
|
|
return http_post_reindex(url, out);
|
|
});
|
|
}
|
|
ImGui::SameLine();
|
|
if (fn_ui::button("+ Add", fn_ui::ButtonVariant::Secondary)) {
|
|
g_show_add = true;
|
|
g_add_runner.reset();
|
|
}
|
|
ImGui::SameLine();
|
|
if (fn_ui::button("Reload", fn_ui::ButtonVariant::Subtle)) {
|
|
trigger_reload();
|
|
}
|
|
ImGui::SameLine();
|
|
fn_ui::toast_inbox_button("##inbox");
|
|
fn_ui::toolbar_end();
|
|
|
|
// Status del reindex debajo del toolbar
|
|
static fn_ui::RunnerState last_reindex_state = fn_ui::RunnerState::Idle;
|
|
fn_ui::RunnerState now = g_reindex_runner.state();
|
|
if (now != fn_ui::RunnerState::Idle) {
|
|
fn_ui::runner_status(g_reindex_runner, "Reindexing...");
|
|
}
|
|
if (last_reindex_state == fn_ui::RunnerState::Running
|
|
&& (now == fn_ui::RunnerState::Success || now == fn_ui::RunnerState::Error)) {
|
|
const bool ok = (now == fn_ui::RunnerState::Success);
|
|
fn_ui::toast_push(ok ? fn_ui::ToastKind::Success : fn_ui::ToastKind::Error,
|
|
g_reindex_runner.message().c_str());
|
|
if (ok) trigger_reload();
|
|
}
|
|
last_reindex_state = now;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Functions Explorer
|
|
// ---------------------------------------------------------------------------
|
|
// Pestana con dos columnas: a la izquierda lista filtrable de funciones del
|
|
// registry; a la derecha codigo + documentacion + metadata de la funcion
|
|
// seleccionada. La lista se carga en el primer render (lazy) y se cachea —
|
|
// el boton "Reload" del toolbar fuerza recarga al disparar trigger_reload
|
|
// (que pone g_explorer_loaded = false).
|
|
|
|
static const char* kExplorerLangs[] = {"all", "go", "py", "ts", "sh", "cpp"};
|
|
static const char* kExplorerDomains[] = {"all", "core", "infra", "finance",
|
|
"datascience", "cybersecurity",
|
|
"shell", "tui", "pipelines",
|
|
"browser", "viz", "gfx", "notebook"};
|
|
|
|
static bool str_contains_ci(const std::string& haystack, const char* needle) {
|
|
if (!needle || !*needle) return true;
|
|
std::string h = haystack;
|
|
std::string n = needle;
|
|
for (auto& c : h) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
|
for (auto& c : n) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
|
return h.find(n) != std::string::npos;
|
|
}
|
|
|
|
static void explorer_select(const std::string& id) {
|
|
g_explorer_selected_id = id;
|
|
g_explorer_detail = FunctionDetail{};
|
|
g_explorer_tests.clear();
|
|
g_explorer_test_idx = 0;
|
|
g_explorer_uses_funcs.clear();
|
|
g_explorer_uses_types.clear();
|
|
g_explorer_used_by.clear();
|
|
if (!g_api_url.empty() && !id.empty()) {
|
|
load_function_detail_http(g_api_url, id, g_explorer_detail);
|
|
load_unit_tests_http(g_api_url, id, g_explorer_tests);
|
|
// Caches del tab Dependencies — forward + reverse.
|
|
g_explorer_uses_funcs = parse_string_array_json(g_explorer_detail.uses_functions);
|
|
g_explorer_uses_types = parse_string_array_json(g_explorer_detail.uses_types);
|
|
for (const auto& f : g_explorer_funcs) {
|
|
if (f.id == id) continue;
|
|
auto deps = parse_string_array_json(f.uses_functions);
|
|
for (const auto& dep : deps) {
|
|
if (dep == id) { g_explorer_used_by.push_back(f.id); break; }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
static void explorer_reload_list() {
|
|
g_explorer_funcs.clear();
|
|
g_explorer_loaded = false;
|
|
if (g_api_url.empty()) return;
|
|
if (load_all_functions_http(g_api_url, g_explorer_funcs)) {
|
|
g_explorer_loaded = true;
|
|
}
|
|
}
|
|
|
|
void draw_functions_explorer() {
|
|
if (!g_explorer_loaded) {
|
|
explorer_reload_list();
|
|
}
|
|
|
|
if (g_explorer_funcs.empty()) {
|
|
empty_state("( no data )", "No functions to explore",
|
|
"Run 'fn index' or check the API connection");
|
|
return;
|
|
}
|
|
|
|
// LAYOUT-TABLE — KPI/form/splitter, no data; keep BeginTable inline.
|
|
const ImGuiTableFlags flags = ImGuiTableFlags_Resizable
|
|
| ImGuiTableFlags_SizingStretchProp;
|
|
if (!ImGui::BeginTable("##explorer_layout", 2, flags)) return;
|
|
|
|
ImGui::TableSetupColumn("list", ImGuiTableColumnFlags_WidthStretch, 1.0f);
|
|
ImGui::TableSetupColumn("detail", ImGuiTableColumnFlags_WidthStretch, 2.4f);
|
|
ImGui::TableNextRow();
|
|
|
|
// --- Left: filter + list ---
|
|
ImGui::TableSetColumnIndex(0);
|
|
ImGui::BeginChild("##explorer_left", ImVec2(0, 0), ImGuiChildFlags_Borders);
|
|
|
|
fn_ui::text_input("Search", g_explorer_filter, sizeof(g_explorer_filter),
|
|
"name or description");
|
|
|
|
ImGui::PushItemWidth(120.0f);
|
|
ImGui::Combo("##lang", &g_explorer_lang_idx, kExplorerLangs,
|
|
IM_ARRAYSIZE(kExplorerLangs));
|
|
ImGui::SameLine();
|
|
ImGui::Combo("##domain", &g_explorer_domain_idx, kExplorerDomains,
|
|
IM_ARRAYSIZE(kExplorerDomains));
|
|
ImGui::PopItemWidth();
|
|
ImGui::Spacing();
|
|
|
|
// Conteo de resultados visibles + lista
|
|
int visible = 0;
|
|
ImGui::BeginChild("##explorer_list", ImVec2(0, 0));
|
|
for (const auto& f : g_explorer_funcs) {
|
|
if (g_explorer_lang_idx > 0
|
|
&& f.lang != kExplorerLangs[g_explorer_lang_idx]) continue;
|
|
if (g_explorer_domain_idx > 0
|
|
&& f.domain != kExplorerDomains[g_explorer_domain_idx]) continue;
|
|
if (g_explorer_filter[0] != '\0'
|
|
&& !str_contains_ci(f.name, g_explorer_filter)
|
|
&& !str_contains_ci(f.description, g_explorer_filter)) continue;
|
|
|
|
visible++;
|
|
bool sel = (g_explorer_selected_id == f.id);
|
|
char label[256];
|
|
std::snprintf(label, sizeof(label), "%s [%s/%s]",
|
|
f.name.c_str(), f.lang.c_str(), f.domain.c_str());
|
|
if (ImGui::Selectable(label, sel, ImGuiSelectableFlags_AllowOverlap)) {
|
|
explorer_select(f.id);
|
|
}
|
|
}
|
|
ImGui::EndChild();
|
|
(void)visible;
|
|
|
|
ImGui::EndChild();
|
|
|
|
// --- Right: detail ---
|
|
ImGui::TableSetColumnIndex(1);
|
|
ImGui::BeginChild("##explorer_right", ImVec2(0, 0), ImGuiChildFlags_Borders);
|
|
|
|
if (g_explorer_selected_id.empty()) {
|
|
empty_state("\xe2\x86\x90", "Select a function",
|
|
"Click a function on the left to see its code and documentation");
|
|
} else {
|
|
const auto& d = g_explorer_detail;
|
|
|
|
// Header: nombre + signature
|
|
ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::text);
|
|
ImGui::TextUnformatted(d.name.empty()
|
|
? g_explorer_selected_id.c_str() : d.name.c_str());
|
|
ImGui::PopStyleColor();
|
|
|
|
// Badges meta (lang/domain/kind/purity/tested)
|
|
ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::text_muted);
|
|
ImGui::Text("%s · %s · %s · %s%s",
|
|
d.lang.c_str(), d.domain.c_str(),
|
|
d.kind.c_str(), d.purity.c_str(),
|
|
d.tested ? " · tested" : "");
|
|
ImGui::PopStyleColor();
|
|
|
|
if (!d.description.empty()) {
|
|
ImGui::Spacing();
|
|
fn_ui::selectable_text_wrapped(d.description.c_str());
|
|
}
|
|
|
|
if (!d.signature.empty()) {
|
|
ImGui::Spacing();
|
|
ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::text_muted);
|
|
ImGui::TextUnformatted("Signature:");
|
|
ImGui::PopStyleColor();
|
|
fn_ui::selectable_text_wrapped(d.signature.c_str());
|
|
}
|
|
|
|
ImGui::Separator();
|
|
|
|
// Tabs Code / Documentation / Notes / Example
|
|
if (ImGui::BeginTabBar("##fn_detail_tabs")) {
|
|
if (ImGui::BeginTabItem("Code")) {
|
|
if (d.code.empty()) {
|
|
ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::text_dim);
|
|
ImGui::TextUnformatted("(no code stored)");
|
|
ImGui::PopStyleColor();
|
|
} else {
|
|
// InputTextMultiline read-only — permite seleccion y copia,
|
|
// y maneja scroll horizontal/vertical para codigo largo.
|
|
ImVec2 sz = ImGui::GetContentRegionAvail();
|
|
ImGui::InputTextMultiline("##code",
|
|
const_cast<char*>(d.code.c_str()),
|
|
d.code.size() + 1, sz,
|
|
ImGuiInputTextFlags_ReadOnly);
|
|
}
|
|
ImGui::EndTabItem();
|
|
}
|
|
if (ImGui::BeginTabItem("Documentation")) {
|
|
if (d.documentation.empty()) {
|
|
ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::text_dim);
|
|
ImGui::TextUnformatted("(no documentation)");
|
|
ImGui::PopStyleColor();
|
|
} else {
|
|
ImGui::BeginChild("##doc_scroll", ImVec2(0, 0));
|
|
fn_ui::selectable_text_wrapped(d.documentation.c_str());
|
|
ImGui::EndChild();
|
|
}
|
|
ImGui::EndTabItem();
|
|
}
|
|
// Tests: caja con dropdown si hay varios + visor del codigo del
|
|
// test seleccionado. Nombre del tab incluye el conteo para que se
|
|
// vea de un vistazo si la funcion tiene tests o no.
|
|
char tests_tab_label[32];
|
|
std::snprintf(tests_tab_label, sizeof(tests_tab_label),
|
|
"Tests (%zu)", g_explorer_tests.size());
|
|
if (ImGui::BeginTabItem(tests_tab_label)) {
|
|
if (g_explorer_tests.empty()) {
|
|
ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::text_dim);
|
|
ImGui::TextUnformatted("(no unit tests for this function)");
|
|
ImGui::PopStyleColor();
|
|
} else {
|
|
// Combo para elegir entre los varios tests (la mayoria
|
|
// de funciones solo tienen 1, pero algunas tienen varios
|
|
// ficheros de tests).
|
|
if (g_explorer_test_idx >= (int)g_explorer_tests.size())
|
|
g_explorer_test_idx = 0;
|
|
std::vector<std::string> names;
|
|
names.reserve(g_explorer_tests.size());
|
|
for (auto& t : g_explorer_tests) names.push_back(t.name);
|
|
std::vector<const char*> name_cstr;
|
|
for (auto& n : names) name_cstr.push_back(n.c_str());
|
|
|
|
ImGui::PushItemWidth(-FLT_MIN);
|
|
ImGui::Combo("##test_select", &g_explorer_test_idx,
|
|
name_cstr.data(),
|
|
static_cast<int>(name_cstr.size()));
|
|
ImGui::PopItemWidth();
|
|
|
|
const auto& t = g_explorer_tests[g_explorer_test_idx];
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::text_muted);
|
|
ImGui::Text("%s · %s", t.lang.c_str(),
|
|
t.file_path.empty() ? "(no path)" : t.file_path.c_str());
|
|
ImGui::PopStyleColor();
|
|
ImGui::Separator();
|
|
|
|
if (t.code.empty()) {
|
|
ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::text_dim);
|
|
ImGui::TextUnformatted("(no code stored for this test)");
|
|
ImGui::PopStyleColor();
|
|
} else {
|
|
ImVec2 sz = ImGui::GetContentRegionAvail();
|
|
ImGui::InputTextMultiline("##test_code",
|
|
const_cast<char*>(t.code.c_str()),
|
|
t.code.size() + 1, sz,
|
|
ImGuiInputTextFlags_ReadOnly);
|
|
}
|
|
}
|
|
ImGui::EndTabItem();
|
|
}
|
|
// Tab Dependencies: muestra uses_functions, uses_types, y reverse
|
|
// lookup "used by" (clientes-side sobre g_explorer_funcs). Caches
|
|
// (g_explorer_uses_*) se rellenan en explorer_select().
|
|
int total_deps = (int)(g_explorer_uses_funcs.size()
|
|
+ g_explorer_uses_types.size());
|
|
char deps_label[32];
|
|
std::snprintf(deps_label, sizeof(deps_label),
|
|
"Dependencies (%d)", total_deps);
|
|
if (ImGui::BeginTabItem(deps_label)) {
|
|
ImGui::BeginChild("##deps_scroll", ImVec2(0, 0));
|
|
|
|
// Section 1: Uses functions (clickables; color dim si la id
|
|
// no resuelve en el catalogo actual).
|
|
char hdr1[64];
|
|
std::snprintf(hdr1, sizeof(hdr1),
|
|
"Uses functions (%zu)",
|
|
g_explorer_uses_funcs.size());
|
|
if (ImGui::CollapsingHeader(hdr1,
|
|
ImGuiTreeNodeFlags_DefaultOpen)) {
|
|
if (g_explorer_uses_funcs.empty()) {
|
|
ImGui::PushStyleColor(ImGuiCol_Text,
|
|
fn_tokens::colors::text_dim);
|
|
ImGui::TextUnformatted("(none)");
|
|
ImGui::PopStyleColor();
|
|
} else {
|
|
for (const auto& fid : g_explorer_uses_funcs) {
|
|
bool resolves = false;
|
|
for (const auto& f : g_explorer_funcs) {
|
|
if (f.id == fid) { resolves = true; break; }
|
|
}
|
|
ImVec4 col = resolves
|
|
? fn_tokens::colors::text
|
|
: fn_tokens::colors::text_dim;
|
|
ImGui::PushStyleColor(ImGuiCol_Text, col);
|
|
ImGui::PushID(fid.c_str());
|
|
if (ImGui::Selectable(fid.c_str())) {
|
|
if (resolves) explorer_select(fid);
|
|
}
|
|
ImGui::PopID();
|
|
ImGui::PopStyleColor();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Section 2: Uses types — sin navegacion (no hay type explorer
|
|
// todavia); bullets para indicar listado pasivo.
|
|
char hdr2[64];
|
|
std::snprintf(hdr2, sizeof(hdr2),
|
|
"Uses types (%zu)",
|
|
g_explorer_uses_types.size());
|
|
if (ImGui::CollapsingHeader(hdr2,
|
|
ImGuiTreeNodeFlags_DefaultOpen)) {
|
|
if (g_explorer_uses_types.empty()) {
|
|
ImGui::PushStyleColor(ImGuiCol_Text,
|
|
fn_tokens::colors::text_dim);
|
|
ImGui::TextUnformatted("(none)");
|
|
ImGui::PopStyleColor();
|
|
} else {
|
|
for (const auto& tid : g_explorer_uses_types) {
|
|
ImGui::BulletText("%s", tid.c_str());
|
|
}
|
|
}
|
|
}
|
|
|
|
// Section 3: Used by (reverse) — clientes detectados al
|
|
// seleccionar la funcion.
|
|
char hdr3[64];
|
|
std::snprintf(hdr3, sizeof(hdr3),
|
|
"Used by (%zu)",
|
|
g_explorer_used_by.size());
|
|
if (ImGui::CollapsingHeader(hdr3,
|
|
ImGuiTreeNodeFlags_DefaultOpen)) {
|
|
if (g_explorer_used_by.empty()) {
|
|
ImGui::PushStyleColor(ImGuiCol_Text,
|
|
fn_tokens::colors::text_dim);
|
|
ImGui::TextUnformatted("(no consumers in catalog)");
|
|
ImGui::PopStyleColor();
|
|
} else {
|
|
for (const auto& fid : g_explorer_used_by) {
|
|
ImGui::PushID(fid.c_str());
|
|
if (ImGui::Selectable(fid.c_str())) {
|
|
explorer_select(fid);
|
|
}
|
|
ImGui::PopID();
|
|
}
|
|
}
|
|
}
|
|
|
|
ImGui::EndChild();
|
|
ImGui::EndTabItem();
|
|
}
|
|
if (ImGui::BeginTabItem("Metadata")) {
|
|
ImGui::BeginChild("##meta_scroll", ImVec2(0, 0));
|
|
auto kv = [](const char* k, const std::string& v) {
|
|
if (v.empty()) return;
|
|
ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::text_muted);
|
|
ImGui::TextUnformatted(k);
|
|
ImGui::PopStyleColor();
|
|
fn_ui::selectable_text_wrapped(v.c_str());
|
|
ImGui::Spacing();
|
|
};
|
|
kv("ID:", d.id);
|
|
kv("Version:", d.version);
|
|
kv("File path:", d.file_path);
|
|
kv("Created:", d.created_at);
|
|
kv("Returns:", d.returns);
|
|
kv("Error type:", d.error_type);
|
|
// Uses functions/types se muestran en el tab Dependencies.
|
|
kv("Params schema:", d.params_schema);
|
|
kv("Example:", d.example);
|
|
kv("Notes:", d.notes);
|
|
ImGui::EndChild();
|
|
ImGui::EndTabItem();
|
|
}
|
|
ImGui::EndTabBar();
|
|
}
|
|
}
|
|
ImGui::EndChild();
|
|
|
|
ImGui::EndTable();
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Main draw
|
|
// ---------------------------------------------------------------------------
|
|
|
|
void draw_dashboard(RegistryData& data) {
|
|
// Tema aplicado por fn::run_app() (app_base.h, ThemeMode::FnDark default).
|
|
// FPS overlay lo dibuja app_base.cpp segun settings().show_fps — no llamarlo aqui.
|
|
|
|
fullscreen_window_begin("##dashboard");
|
|
|
|
char subtitle[128];
|
|
std::snprintf(subtitle, sizeof(subtitle),
|
|
"%d functions · %d types · %d apps · %d analyses · %zu projects",
|
|
data.stats.total_functions, data.stats.total_types,
|
|
data.stats.total_apps, data.stats.total_analysis,
|
|
data.projects.size());
|
|
|
|
page_header_begin("fn_registry Dashboard", subtitle);
|
|
page_header_end();
|
|
|
|
draw_actions_bar();
|
|
ImGui::Dummy(ImVec2(0, fn_tokens::spacing::sm));
|
|
|
|
// Navegacion top-level: "Monitor" es la primera y por defecto (issue 0086).
|
|
// El bucle reactivo (construir / ejecutar / recopilar / analizar / mejorar)
|
|
// se vigila desde alli, asi que pega como landing. Las demas son vistas
|
|
// dedicadas a entidades del registry.
|
|
if (ImGui::BeginTabBar("##main_tabs", ImGuiTabBarFlags_FittingPolicyScroll)) {
|
|
if (ImGui::BeginTabItem("Monitor")) {
|
|
draw_monitor(data);
|
|
ImGui::EndTabItem();
|
|
}
|
|
if (ImGui::BeginTabItem("Work")) {
|
|
draw_work_tab();
|
|
ImGui::EndTabItem();
|
|
}
|
|
if (ImGui::BeginTabItem("Dashboard")) {
|
|
draw_kpi_row(data);
|
|
ImGui::Dummy(ImVec2(0, fn_tokens::spacing::md));
|
|
|
|
// Altura del bloque de charts proporcional al espacio disponible:
|
|
// ~32% del espacio restante despues de KPIs (clamped a [200, 360]).
|
|
const float remaining_h = ImGui::GetContentRegionAvail().y;
|
|
float chart_h = remaining_h * 0.40f;
|
|
if (chart_h < 200.0f) chart_h = 200.0f;
|
|
if (chart_h > 360.0f) chart_h = 360.0f;
|
|
draw_charts(data, chart_h);
|
|
ImGui::Dummy(ImVec2(0, fn_tokens::spacing::md));
|
|
|
|
// Tabla de funciones recientes para que el Dashboard tenga algo
|
|
// accionable abajo sin tener que cambiar de pestana.
|
|
ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::text_muted);
|
|
ImGui::TextUnformatted("Recent Functions");
|
|
ImGui::PopStyleColor();
|
|
draw_recent_functions(data.recent_funcs);
|
|
ImGui::EndTabItem();
|
|
}
|
|
if (ImGui::BeginTabItem("Explorer")) {
|
|
draw_functions_explorer();
|
|
ImGui::EndTabItem();
|
|
}
|
|
if (ImGui::BeginTabItem("Projects")) {
|
|
draw_projects_list(data);
|
|
ImGui::EndTabItem();
|
|
}
|
|
if (ImGui::BeginTabItem("Apps")) {
|
|
draw_apps_list(data.apps);
|
|
ImGui::EndTabItem();
|
|
}
|
|
if (ImGui::BeginTabItem("Analysis")) {
|
|
draw_analysis_list(data.analyses);
|
|
ImGui::EndTabItem();
|
|
}
|
|
if (ImGui::BeginTabItem("Types")) {
|
|
draw_types_list(data.types);
|
|
ImGui::EndTabItem();
|
|
}
|
|
ImGui::EndTabBar();
|
|
}
|
|
|
|
draw_add_modal(data);
|
|
|
|
fullscreen_window_end();
|
|
|
|
// Toasts encima de todo
|
|
fn_ui::toast_render();
|
|
}
|