Files
registry_dashboard/views.cpp
egutierrez 87b7ef45ff feat(monitor): Monitor tab as primary landing + errors KPI + recent executions + date filter
Rebrand "Claude Usage" tab to "Monitor" and promote it to first/default tab in
the main TabBar. Monitor is the landing for the reactive loop (construir →
ejecutar → recopilar → analizar → mejorar).

UI additions:
- Local toolbar inside Monitor with date preset combo (1h / 24h / 7d / 30d /
  All), manual Refresh button, and live LED + last-event-ts indicator.
- 5 KPI cards (was 4): added "Errors" derived from COUNT(*) FROM calls WHERE
  success = 0 filtered by the active window.
- New sub-tab "Recent Executions" (first sub-tab) with columns: When,
  Function, Tool, ms, OK (check/X colored), Error class. Backed by calls
  table, sorted by ts DESC, limit 100, filtered by window.
- Violations sub-tab gains "When" column with formatted ts.

Data layer:
- data.h: RecentExecutionRow + window_secs + ws_connected/last_event_ts /
  last_seen_call_id watermark on ClaudeUsageData.
- data_http.{h,cpp}: load_claude_usage_http now takes window_secs and embeds
  ts_filter() in calls/errors/violations queries. total_errors populated.
  recent_executions populated up to 100 rows. New standalone
  load_recent_executions_http() for future WS-driven partial refetch.
- main.cpp: reload_data preserves window_secs across reloads; new
  reload_monitor() does a Monitor-only refetch when the user changes the
  window or clicks Refresh, without re-querying the full registry.

Wiring:
- views.h: draw_monitor + monitor_consume_reload_request() +
  monitor_set_ws_state() exported. draw_claude_usage removed.
- views.cpp: render() consumes monitor_consume_reload_request each frame
  and dispatches reload_monitor().

This is the UI half of issue 0086. The server-side WebSocket endpoint in
sqlite_api and the C++ WS client are next; the LED is wired to
monitor_set_ws_state but stays gray until those land.

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

1222 lines
51 KiB
C++

#include "views.h"
#include "data_http.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 "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 <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;
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;
// 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;
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());
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;
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;
}
const char* headers[] = {"Name", "Lang", "Domain", "Kind", "Purity", "Tested", "Created"};
constexpr int cols = 7;
std::vector<std::string> cell_strings;
cell_strings.reserve(funcs.size() * cols);
for (auto& f : funcs) {
cell_strings.push_back(f.name);
cell_strings.push_back(f.lang);
cell_strings.push_back(f.domain);
cell_strings.push_back(f.kind);
cell_strings.push_back(f.purity);
cell_strings.push_back(f.tested ? "yes" : "no");
cell_strings.push_back(f.created_at.substr(0, 10));
}
auto cells = to_cstr(cell_strings);
table_view("##recent", headers, cols, cells.data(), static_cast<int>(funcs.size()));
}
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;
}
const char* headers[] = {"Name", "Lang", "Domain", "Framework", "Git", "Description"};
constexpr int cols = 6;
std::vector<std::string> cell_strings;
cell_strings.reserve(apps.size() * cols);
for (auto& a : apps) {
cell_strings.push_back(a.name);
cell_strings.push_back(a.lang);
cell_strings.push_back(a.domain);
cell_strings.push_back(a.framework);
// Indicador de git: tiene repo remoto (gitea), solo local, o ninguno.
// - "remote": repo_url poblado en el frontmatter del app.md
// - "local": hay .git/ en dir_path pero sin repo_url
// - "-": ni .git ni repo_url
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 = "-";
}
cell_strings.push_back(git_status);
cell_strings.push_back(a.description);
}
auto cells = to_cstr(cell_strings);
table_view("##apps", headers, cols, cells.data(), static_cast<int>(apps.size()));
}
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;
}
const char* headers[] = {"Name", "Lang", "Domain", "Description"};
constexpr int cols = 4;
std::vector<std::string> cell_strings;
cell_strings.reserve(analyses.size() * cols);
for (auto& a : analyses) {
cell_strings.push_back(a.name);
cell_strings.push_back(a.lang);
cell_strings.push_back(a.domain);
cell_strings.push_back(a.description);
}
auto cells = to_cstr(cell_strings);
table_view("##analysis", headers, cols, cells.data(), static_cast<int>(analyses.size()));
}
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;
}
const char* headers[] = {"Name", "Lang", "Domain", "Algebraic", "Description"};
constexpr int cols = 5;
std::vector<std::string> cell_strings;
cell_strings.reserve(types.size() * cols);
for (auto& t : types) {
cell_strings.push_back(t.name);
cell_strings.push_back(t.lang);
cell_strings.push_back(t.domain);
cell_strings.push_back(t.algebraic);
cell_strings.push_back(t.description);
}
auto cells = to_cstr(cell_strings);
table_view("##types", headers, cols, cells.data(), static_cast<int>(types.size()));
}
// ---------------------------------------------------------------------------
// 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();
}
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;
}
// 5 KPI cards: Calls / Errors / Violations / Copies / Versions
const ImGuiTableFlags flags = ImGuiTableFlags_SizingStretchSame | ImGuiTableFlags_NoPadOuterX;
if (ImGui::BeginTable("##monitor_kpi", 5, flags)) {
struct KPI { const char* label; float value; const char* icon; };
const KPI cards[5] = {
{"Calls", static_cast<float>(cu.total_calls), TI_ACTIVITY},
{"Errors", static_cast<float>(cu.total_errors), TI_ALERT_TRIANGLE},
{"Violations", static_cast<float>(cu.total_violations), TI_ALERT_CIRCLE},
{"Copies", static_cast<float>(cu.total_copies), TI_COPY},
{"Versions", static_cast<float>(cu.total_versions), TI_HISTORY},
};
ImGui::TableNextRow();
for (int i = 0; i < 5; i++) {
ImGui::TableSetColumnIndex(i);
kpi_card(cards[i].label, cards[i].value, 0.0f, nullptr, 0, "%.0f", cards[i].icon);
}
ImGui::EndTable();
}
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")) {
if (cu.recent_executions.empty()) {
ImGui::TextDisabled("No executions in this window. Try widening (7d/30d/All) or wait for the next call.");
} else {
const ImGuiTableFlags tf = ImGuiTableFlags_RowBg | ImGuiTableFlags_Borders
| ImGuiTableFlags_Resizable | ImGuiTableFlags_ScrollY;
if (ImGui::BeginTable("##monitor_recent", 6, tf, ImVec2(0, 0))) {
ImGui::TableSetupColumn("When");
ImGui::TableSetupColumn("Function");
ImGui::TableSetupColumn("Tool");
ImGui::TableSetupColumn("ms");
ImGui::TableSetupColumn("OK");
ImGui::TableSetupColumn("Error");
ImGui::TableHeadersRow();
for (const auto& r : cu.recent_executions) {
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0);
ImGui::TextUnformatted(format_ts(r.ts).c_str());
ImGui::TableSetColumnIndex(1);
ImGui::TextUnformatted(r.function_id.empty() ? "-" : r.function_id.c_str());
ImGui::TableSetColumnIndex(2);
ImGui::TextUnformatted(r.tool_used.c_str());
ImGui::TableSetColumnIndex(3);
ImGui::Text("%d", r.duration_ms);
ImGui::TableSetColumnIndex(4);
if (r.success) {
ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::success);
ImGui::TextUnformatted(TI_CHECK);
ImGui::PopStyleColor();
} else {
ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::error);
ImGui::TextUnformatted(TI_X);
ImGui::PopStyleColor();
}
ImGui::TableSetColumnIndex(5);
ImGui::TextUnformatted(r.error_class.c_str());
}
ImGui::EndTable();
}
}
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 {
const ImGuiTableFlags tf = ImGuiTableFlags_RowBg | ImGuiTableFlags_Borders
| ImGuiTableFlags_Resizable | ImGuiTableFlags_ScrollY;
if (ImGui::BeginTable("##monitor_top_fn", 6, tf, ImVec2(0, 0))) {
ImGui::TableSetupColumn("Function ID");
ImGui::TableSetupColumn("Calls");
ImGui::TableSetupColumn("7d");
ImGui::TableSetupColumn("Errors");
ImGui::TableSetupColumn("Error %");
ImGui::TableSetupColumn("Mean ms");
ImGui::TableHeadersRow();
for (const auto& r : cu.top_functions) {
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0); ImGui::TextUnformatted(r.function_id.c_str());
ImGui::TableSetColumnIndex(1); ImGui::Text("%d", r.calls_total);
ImGui::TableSetColumnIndex(2); ImGui::Text("%d", r.calls_7d);
ImGui::TableSetColumnIndex(3); ImGui::Text("%d", r.errors_total);
ImGui::TableSetColumnIndex(4); ImGui::Text("%.1f%%", r.error_rate * 100.0);
ImGui::TableSetColumnIndex(5); ImGui::Text("%.0f", r.mean_duration_ms);
}
ImGui::EndTable();
}
}
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("Violations")) {
if (cu.recent_violations.empty()) {
ImGui::TextDisabled("No antipattern violations detected.");
} else {
const ImGuiTableFlags tf = ImGuiTableFlags_RowBg | ImGuiTableFlags_Borders
| ImGuiTableFlags_Resizable | ImGuiTableFlags_ScrollY;
if (ImGui::BeginTable("##monitor_viol", 5, tf, ImVec2(0, 0))) {
ImGui::TableSetupColumn("When");
ImGui::TableSetupColumn("Rule");
ImGui::TableSetupColumn("Severity");
ImGui::TableSetupColumn("Function");
ImGui::TableSetupColumn("Snippet");
ImGui::TableHeadersRow();
for (const auto& r : cu.recent_violations) {
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0); ImGui::TextUnformatted(format_ts(r.ts).c_str());
ImGui::TableSetColumnIndex(1); ImGui::TextUnformatted(r.rule_id.c_str());
ImGui::TableSetColumnIndex(2); ImGui::TextUnformatted(r.severity.c_str());
ImGui::TableSetColumnIndex(3); ImGui::TextUnformatted(r.function_id.c_str());
ImGui::TableSetColumnIndex(4); ImGui::TextUnformatted(r.command_snippet.c_str());
}
ImGui::EndTable();
}
}
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 {
const ImGuiTableFlags tf = ImGuiTableFlags_RowBg | ImGuiTableFlags_Borders
| ImGuiTableFlags_Resizable | ImGuiTableFlags_ScrollY;
if (ImGui::BeginTable("##monitor_copies", 5, tf, ImVec2(0, 0))) {
ImGui::TableSetupColumn("Kind");
ImGui::TableSetupColumn("Sim");
ImGui::TableSetupColumn("App File");
ImGui::TableSetupColumn("App Function");
ImGui::TableSetupColumn("Registry ID");
ImGui::TableHeadersRow();
for (const auto& r : cu.copies) {
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0); ImGui::TextUnformatted(r.kind.c_str());
ImGui::TableSetColumnIndex(1); ImGui::Text("%.2f", r.similarity);
ImGui::TableSetColumnIndex(2); ImGui::TextUnformatted(r.app_file.c_str());
ImGui::TableSetColumnIndex(3); ImGui::TextUnformatted(r.app_function.c_str());
ImGui::TableSetColumnIndex(4); ImGui::TextUnformatted(r.registry_id.c_str());
}
ImGui::EndTable();
}
}
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.
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 {
const char* headers[] = {"Name", "Path", "Symlink", "Description"};
std::vector<std::string> cells;
cells.reserve(d.vaults.size() * 4);
for (auto& v : d.vaults) {
cells.push_back(v.name);
cells.push_back(v.path);
cells.push_back(v.symlink ? "yes" : "no");
cells.push_back(v.description);
}
auto cp = to_cstr(cells);
table_view("##vaults", headers, 4, cp.data(),
static_cast<int>(d.vaults.size()));
}
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;
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);
}
}
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;
}
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();
}
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);
kv("Uses functions:", d.uses_functions);
kv("Uses types:", d.uses_types);
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("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();
}