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:
+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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user