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>
This commit is contained in:
@@ -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<ClaudeUsageRow> top_functions; // top 20 by calls_total
|
||||
std::vector<ClaudeViolationRow> recent_violations; // last 20
|
||||
std::vector<ClaudeCopiedRow> copies;
|
||||
std::vector<RecentExecutionRow> 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<LangCount> by_lang;
|
||||
@@ -151,6 +211,7 @@ struct RegistryData {
|
||||
std::vector<AnalysisRow> analyses;
|
||||
std::vector<TypeRow> types;
|
||||
std::vector<ProjectRow> projects;
|
||||
ClaudeUsageData claude;
|
||||
int orphan_apps = 0;
|
||||
int orphan_analyses = 0;
|
||||
int orphan_vaults = 0;
|
||||
|
||||
+191
@@ -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<double>();
|
||||
if (row[idx].is_string()) return std::atof(row[idx].get<std::string>().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<RecentExecutionRow>& 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;
|
||||
}
|
||||
|
||||
+22
@@ -35,6 +35,28 @@ bool load_unit_tests_http(const std::string& api_url,
|
||||
const std::string& function_id,
|
||||
std::vector<UnitTestRow>& 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<RecentExecutionRow>& 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);
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <cctype>
|
||||
#include <ctime>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
@@ -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<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
|
||||
@@ -311,6 +363,226 @@ void draw_types_list(const std::vector<TypeRow>& types) {
|
||||
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
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -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));
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user