diff --git a/data.h b/data.h index 90c0e48..1a2693b 100644 --- a/data.h +++ b/data.h @@ -140,6 +140,66 @@ struct ProjectDetail { }; // All data loaded from registry.db in one shot +// Issue 0085: Claude usage telemetry rows from call_monitor.operations.db. +struct ClaudeUsageRow { + std::string function_id; + int calls_total = 0; + int calls_7d = 0; + int errors_total = 0; + double error_rate = 0.0; + double mean_duration_ms = 0.0; +}; + +struct ClaudeViolationRow { + std::string rule_id; + std::string function_id; + std::string command_snippet; + std::string severity; + long long ts = 0; +}; + +struct ClaudeCopiedRow { + std::string app_file; + std::string app_function; + std::string registry_id; + std::string kind; + double similarity = 1.0; +}; + +// Una invocacion concreta del registro de telemetria. Lo que el agente lanzo, +// cuanto tardo, si fallo. Usado por la tabla "Recent Executions" del Monitor. +struct RecentExecutionRow { + long long id = 0; // calls.id (watermark para WS deltas) + long long ts = 0; // epoch seconds + std::string function_id; + std::string tool_used; // mcp / fn_cli_run / bash / heredoc / ... + int duration_ms = 0; + bool success = true; + std::string error_class; + std::string session_id; +}; + +struct ClaudeUsageData { + bool available = false; // true if call_monitor.operations.db is reachable + int total_calls = 0; + int total_errors = 0; + int total_violations = 0; + int total_copies = 0; + int total_versions = 0; + std::vector top_functions; // top 20 by calls_total + std::vector recent_violations; // last 20 + std::vector copies; + std::vector recent_executions; // last N within window + + // Filtro de fecha. 0 = All. Otros valores en segundos. + int window_secs = 86400; // default 24h + + // WS live state. true cuando hay conexion WS activa al hub de eventos. + bool ws_connected = false; + long long last_event_ts = 0; // ultimo ts recibido por WS + long long last_seen_call_id = 0; // watermark (max id procesado) +}; + struct RegistryData { RegistryStats stats; std::vector by_lang; @@ -151,6 +211,7 @@ struct RegistryData { std::vector analyses; std::vector types; std::vector projects; + ClaudeUsageData claude; int orphan_apps = 0; int orphan_analyses = 0; int orphan_vaults = 0; diff --git a/data_http.cpp b/data_http.cpp index bee7e13..4d6ed03 100644 --- a/data_http.cpp +++ b/data_http.cpp @@ -536,3 +536,194 @@ bool http_post_add_vault(const std::string& api_url, b["description"] = description; return post_json(api_url, "/api/add/vault", b, out_body); } + +// ---- Issue 0085d: Claude usage telemetry ---- + +// Query against ops:call_monitor instead of registry. +static json call_monitor_query(HttpClient& cli, const char* sql) { + json body; + body["sql"] = sql; + auto res = cli.post("/api/databases/ops:call_monitor/query", body.dump(), "application/json"); + if (!res.ok()) { + return nullptr; + } + return json::parse(res.body, nullptr, false); +} + +static double extract_row_double(const json& row, size_t idx) { + if (idx >= row.size() || row[idx].is_null()) return 0.0; + if (row[idx].is_number()) return row[idx].get(); + if (row[idx].is_string()) return std::atof(row[idx].get().c_str()); + return 0.0; +} + +// Construye un filtro temporal `WHERE ts >= ?` literal embebido (no prepared) +// reemplazando el placeholder. window_secs == 0 -> sin filtro. +static std::string ts_filter(int window_secs, const char* col = "ts", + const char* glue = "WHERE") { + if (window_secs <= 0) return ""; + char buf[128]; + std::snprintf(buf, sizeof(buf), " %s %s >= (strftime('%%s','now') - %d) ", + glue, col, window_secs); + return std::string(buf); +} + +bool load_claude_usage_http(const std::string& api_url, RegistryData& out, + int window_secs) { + // Preservar window y estado WS al recargar. + bool prev_ws = out.claude.ws_connected; + long long prev_last_ev = out.claude.last_event_ts; + long long prev_max_id = out.claude.last_seen_call_id; + out.claude = ClaudeUsageData{}; + out.claude.window_secs = window_secs; + out.claude.ws_connected = prev_ws; + out.claude.last_event_ts = prev_last_ev; + out.claude.last_seen_call_id = prev_max_id; + + std::string host; + int port; + if (!parse_url(api_url, host, port)) return false; + HttpClient cli(host, port); + + // Probe: is ops:call_monitor known? + auto probe = cli.get("/api/databases/ops:call_monitor/tables"); + if (!probe.ok()) { + out.claude.available = false; + return true; // not an error: monitor not yet deployed + } + out.claude.available = true; + + const std::string wf_calls = ts_filter(window_secs); // " WHERE ts >= ..." + const std::string wf_viol = ts_filter(window_secs); + const std::string wf_calls_and = wf_calls.empty() + ? std::string(" WHERE success = 0 ") + : std::string(wf_calls + " AND success = 0 "); + + // Totals (filtradas por ventana donde aplica) + { + const std::string sql_calls = "SELECT COUNT(*) FROM calls" + wf_calls; + out.claude.total_calls = extract_int(call_monitor_query(cli, sql_calls.c_str())); + } + { + const std::string sql_err = "SELECT COUNT(*) FROM calls" + wf_calls_and; + out.claude.total_errors = extract_int(call_monitor_query(cli, sql_err.c_str())); + } + { + const std::string sql_viol = "SELECT COUNT(*) FROM violations" + wf_viol; + out.claude.total_violations = extract_int(call_monitor_query(cli, sql_viol.c_str())); + } + out.claude.total_copies = extract_int(call_monitor_query(cli, "SELECT COUNT(*) FROM copied_code")); + out.claude.total_versions = extract_int(call_monitor_query(cli, "SELECT COUNT(*) FROM function_versions")); + + // Recent executions (calls table) ordenada por ts DESC + { + std::string sql = "SELECT id, ts, function_id, tool_used, duration_ms, success, error_class, session_id " + "FROM calls" + wf_calls + " ORDER BY ts DESC LIMIT 100"; + json rx = call_monitor_query(cli, sql.c_str()); + if (rx.is_object() && rx.contains("rows")) { + long long mx = out.claude.last_seen_call_id; + for (const auto& r : rx["rows"]) { + RecentExecutionRow row; + row.id = (long long)extract_row_int(r, 0); + row.ts = (long long)extract_row_int(r, 1); + row.function_id = extract_str(r, 2); + row.tool_used = extract_str(r, 3); + row.duration_ms = extract_row_int(r, 4); + row.success = extract_row_int(r, 5) != 0; + row.error_class = extract_str(r, 6); + row.session_id = extract_str(r, 7); + if (row.id > mx) mx = row.id; + out.claude.recent_executions.push_back(row); + } + out.claude.last_seen_call_id = mx; + } + } + + // Top functions by calls_total + json top = call_monitor_query(cli, + "SELECT function_id, calls_total, calls_7d, errors_total, error_rate, mean_duration_ms " + "FROM function_stats ORDER BY calls_total DESC LIMIT 20"); + if (top.is_object() && top.contains("rows")) { + for (const auto& r : top["rows"]) { + ClaudeUsageRow row; + row.function_id = extract_str(r, 0); + row.calls_total = extract_row_int(r, 1); + row.calls_7d = extract_row_int(r, 2); + row.errors_total = extract_row_int(r, 3); + row.error_rate = extract_row_double(r, 4); + row.mean_duration_ms = extract_row_double(r, 5); + out.claude.top_functions.push_back(row); + } + } + + // Recent violations (filtradas por ventana) + std::string sql_viol_list = "SELECT rule_id, function_id, command_snippet, severity, ts " + "FROM violations" + wf_viol + " ORDER BY ts DESC LIMIT 20"; + json viol = call_monitor_query(cli, sql_viol_list.c_str()); + if (viol.is_object() && viol.contains("rows")) { + for (const auto& r : viol["rows"]) { + ClaudeViolationRow row; + row.rule_id = extract_str(r, 0); + row.function_id = extract_str(r, 1); + row.command_snippet = extract_str(r, 2); + row.severity = extract_str(r, 3); + row.ts = (long long)extract_row_int(r, 4); + out.claude.recent_violations.push_back(row); + } + } + + // Copied code matches + json cp = call_monitor_query(cli, + "SELECT app_file, app_function, registry_id, kind, similarity " + "FROM copied_code ORDER BY detected_at DESC LIMIT 50"); + if (cp.is_object() && cp.contains("rows")) { + for (const auto& r : cp["rows"]) { + ClaudeCopiedRow row; + row.app_file = extract_str(r, 0); + row.app_function = extract_str(r, 1); + row.registry_id = extract_str(r, 2); + row.kind = extract_str(r, 3); + row.similarity = extract_row_double(r, 4); + out.claude.copies.push_back(row); + } + } + + return true; +} + +bool load_recent_executions_http(const std::string& api_url, + int window_secs, int limit, + std::vector& out, + long long& out_max_id) { + out.clear(); + out_max_id = 0; + + std::string host; + int port; + if (!parse_url(api_url, host, port)) return false; + HttpClient cli(host, port); + + const std::string wf = ts_filter(window_secs); + char lim_buf[32]; + std::snprintf(lim_buf, sizeof(lim_buf), " LIMIT %d", limit > 0 ? limit : 100); + + std::string sql = "SELECT id, ts, function_id, tool_used, duration_ms, success, error_class, session_id " + "FROM calls" + wf + " ORDER BY ts DESC" + lim_buf; + json rx = call_monitor_query(cli, sql.c_str()); + if (!rx.is_object() || !rx.contains("rows")) return false; + + for (const auto& r : rx["rows"]) { + RecentExecutionRow row; + row.id = (long long)extract_row_int(r, 0); + row.ts = (long long)extract_row_int(r, 1); + row.function_id = extract_str(r, 2); + row.tool_used = extract_str(r, 3); + row.duration_ms = extract_row_int(r, 4); + row.success = extract_row_int(r, 5) != 0; + row.error_class = extract_str(r, 6); + row.session_id = extract_str(r, 7); + if (row.id > out_max_id) out_max_id = row.id; + out.push_back(row); + } + return true; +} diff --git a/data_http.h b/data_http.h index 2ed0b8f..91f7898 100644 --- a/data_http.h +++ b/data_http.h @@ -35,6 +35,28 @@ bool load_unit_tests_http(const std::string& api_url, const std::string& function_id, std::vector& out); +// Issue 0085d: Load Claude usage telemetry from ops:call_monitor (top +// functions, recent violations, copied code). Si la BD no esta disponible +// setea out.claude.available = false sin error. +// +// `window_secs`: ventana hacia atras desde now. 0 = sin filtro (All). +// Aplica a total_calls, total_errors, top_functions, recent_executions, +// recent_violations. total_versions/total_copies son acumulados (no filtran). +bool load_claude_usage_http(const std::string& api_url, RegistryData& out, + int window_secs); + +// Variante legacy (window default 24h). Usar la version con window_secs. +inline bool load_claude_usage_http(const std::string& api_url, RegistryData& out) { + return load_claude_usage_http(api_url, out, 86400); +} + +// Carga la ventana de "Recent Executions" (calls table) ordenada por ts DESC. +// Filtrada por window_secs (0 = sin filtro). limit = max filas (default 100). +bool load_recent_executions_http(const std::string& api_url, + int window_secs, int limit, + std::vector& out, + long long& out_max_id); + // Operaciones de mutacion (thread-safe porque http_post ya lo es). // Devuelven el body de respuesta en `out_body`. true si HTTP status 2xx. bool http_post_reindex(const std::string& api_url, std::string& out_body); diff --git a/main.cpp b/main.cpp index fa0475e..d8b90fe 100644 --- a/main.cpp +++ b/main.cpp @@ -22,13 +22,21 @@ static bool g_loaded = false; static bool g_using_http = false; static void reload_data() { + // Conservar la ventana del Monitor entre recargas (no se pierde al refrescar). + int prev_window = g_data.claude.window_secs; + if (prev_window == 0 && g_data.claude.total_calls == 0) prev_window = 86400; g_data = RegistryData{}; + g_data.claude.window_secs = prev_window; // Try HTTP API first if (!g_api_url.empty()) { g_loaded = load_registry_data_http(g_api_url, g_data); if (g_loaded) { g_using_http = true; + // Issue 0085d: best-effort load of Claude telemetry from + // ops:call_monitor. Falla silenciosamente si no esta disponible + // (la tab Monitor mostrara placeholder). + load_claude_usage_http(g_api_url, g_data, g_data.claude.window_secs); return; } fprintf(stderr, "HTTP API failed, falling back to SQLite\n"); @@ -44,12 +52,27 @@ static void reload_data() { } } +// Refetch SOLO de telemetria del Monitor. Se dispara al cambiar la ventana +// temporal o al recibir un evento WS que invalide el snapshot. No toca el +// registry general. +static void reload_monitor() { + if (g_api_url.empty() || !g_loaded) return; + load_claude_usage_http(g_api_url, g_data, g_data.claude.window_secs); +} + static void render() { if (ImGui::GetIO().UserData != nullptr) { ImGui::GetIO().UserData = nullptr; reload_data(); } + // Issue 0086: el Monitor pide refetch parcial cuando el usuario cambia la + // ventana temporal o pulsa Refresh. No pasa por reload_data() para no + // tirar abajo todo el dataset del registry. + if (monitor_consume_reload_request()) { + reload_monitor(); + } + if (!g_loaded) { fullscreen_window_begin("##error"); ImGui::TextColored(ImVec4(1, 0.3f, 0.3f, 1), diff --git a/views.cpp b/views.cpp index 6eb307f..66e0fac 100644 --- a/views.cpp +++ b/views.cpp @@ -31,6 +31,7 @@ #include #include #include +#include #include #include @@ -40,6 +41,57 @@ 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(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(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 @@ -311,6 +363,226 @@ void draw_types_list(const std::vector& types) { table_view("##types", headers, cols, cells.data(), static_cast(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(cu.total_calls), TI_ACTIVITY}, + {"Errors", static_cast(cu.total_errors), TI_ALERT_TRIANGLE}, + {"Violations", static_cast(cu.total_violations), TI_ALERT_CIRCLE}, + {"Copies", static_cast(cu.total_copies), TI_COPY}, + {"Versions", static_cast(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 // --------------------------------------------------------------------------- @@ -887,10 +1159,15 @@ void draw_dashboard(RegistryData& data) { draw_actions_bar(); ImGui::Dummy(ImVec2(0, fn_tokens::spacing::sm)); - // Navegacion top-level: cada tab ocupa toda la zona de contenido. El - // primero ("Dashboard") incluye los KPIs + charts + tabla de recientes; - // los demas son vistas dedicadas a su entidad. + // 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)); diff --git a/views.h b/views.h index 7d12994..a4dd050 100644 --- a/views.h +++ b/views.h @@ -22,3 +22,18 @@ void draw_projects_list(RegistryData& data); // Explorer: lista navegable de funciones + visor de codigo y documentacion. // Carga la lista completa al primer render via /api/databases/registry/query. void draw_functions_explorer(); + +// Issue 0086: tab "Monitor" (antes "Claude Usage"). Pestana principal y por +// defecto. Muestra KPIs del bucle reactivo + Recent Executions con timestamps, +// top functions, violations, copied code. Filtro de fecha por presets y +// estado WS live (LED + ts ultimo evento). Si data.claude.available == false +// muestra placeholder con instrucciones para inicializar call_monitor. +void draw_monitor(RegistryData& data); + +// Flag global: cuando draw_monitor cambia la ventana o WS reconecta y necesita +// refetch parcial. Lo lee main.cpp y llama load_claude_usage_http sin tocar +// el resto del registry (rapido). +bool monitor_consume_reload_request(); + +// Setter expuesto a main.cpp: refleja estado WS en el LED. true = live. +void monitor_set_ws_state(bool connected, long long last_event_ts);