00ee6a93e3
- data.h - data_http.cpp - main.cpp - views.cpp Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
758 lines
31 KiB
C++
758 lines
31 KiB
C++
#include "data_http.h"
|
|
#include "http_client.h"
|
|
#include "vendor/nlohmann/json.hpp"
|
|
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
|
|
using json = nlohmann::json;
|
|
|
|
// Parse host and port from URL like "http://127.0.0.1:8484"
|
|
static bool parse_url(const std::string& url, std::string& host, int& port) {
|
|
auto pos = url.find("://");
|
|
std::string rest = (pos != std::string::npos) ? url.substr(pos + 3) : url;
|
|
auto colon = rest.find(':');
|
|
if (colon == std::string::npos) {
|
|
host = rest;
|
|
port = 80;
|
|
} else {
|
|
host = rest.substr(0, colon);
|
|
port = std::atoi(rest.substr(colon + 1).c_str());
|
|
}
|
|
return !host.empty() && port > 0;
|
|
}
|
|
|
|
// POST a SQL query to the API and return parsed JSON, or null on failure.
|
|
static json api_query(HttpClient& cli, const char* sql) {
|
|
json body;
|
|
body["sql"] = sql;
|
|
auto res = cli.post("/api/databases/registry/query", body.dump(), "application/json");
|
|
if (!res.ok()) {
|
|
if (res.status > 0) fprintf(stderr, "[http] query error %d: %s\n", res.status, res.body.c_str());
|
|
else fprintf(stderr, "[http] connection failed for query: %s\n", sql);
|
|
return nullptr;
|
|
}
|
|
return json::parse(res.body, nullptr, false);
|
|
}
|
|
|
|
static int extract_int(const json& j) {
|
|
if (j.is_null() || !j.contains("rows") || j["rows"].empty()) return 0;
|
|
auto& val = j["rows"][0][0];
|
|
if (val.is_number()) return val.get<int>();
|
|
if (val.is_string()) return std::atoi(val.get<std::string>().c_str());
|
|
return 0;
|
|
}
|
|
|
|
static std::string extract_str(const json& row, size_t idx) {
|
|
if (idx >= row.size() || row[idx].is_null()) return "";
|
|
if (row[idx].is_string()) return row[idx].get<std::string>();
|
|
return row[idx].dump();
|
|
}
|
|
|
|
static int extract_row_int(const json& row, size_t idx) {
|
|
if (idx >= row.size() || row[idx].is_null()) return 0;
|
|
if (row[idx].is_number()) return row[idx].get<int>();
|
|
if (row[idx].is_string()) return std::atoi(row[idx].get<std::string>().c_str());
|
|
return 0;
|
|
}
|
|
|
|
bool load_registry_data_http(const std::string& api_url, RegistryData& out) {
|
|
std::string host;
|
|
int port;
|
|
if (!parse_url(api_url, host, port)) {
|
|
fprintf(stderr, "[http] invalid URL: %s\n", api_url.c_str());
|
|
return false;
|
|
}
|
|
|
|
HttpClient cli(host, port);
|
|
|
|
// Health check
|
|
auto health = cli.get("/health");
|
|
if (!health.ok()) {
|
|
fprintf(stderr, "[http] sqlite_api not reachable at %s\n", api_url.c_str());
|
|
return false;
|
|
}
|
|
|
|
fprintf(stdout, "[http] Connected to sqlite_api at %s\n", api_url.c_str());
|
|
|
|
// --- Counts ---
|
|
out.stats.total_functions = extract_int(api_query(cli, "SELECT COUNT(*) FROM functions"));
|
|
out.stats.total_types = extract_int(api_query(cli, "SELECT COUNT(*) FROM types"));
|
|
out.stats.total_apps = extract_int(api_query(cli, "SELECT COUNT(*) FROM apps"));
|
|
out.stats.total_analysis = extract_int(api_query(cli, "SELECT COUNT(*) FROM analysis"));
|
|
out.stats.total_unit_tests = extract_int(api_query(cli, "SELECT COUNT(*) FROM unit_tests"));
|
|
out.stats.total_proposals = extract_int(api_query(cli, "SELECT COUNT(*) FROM proposals"));
|
|
out.stats.tested_functions = extract_int(api_query(cli, "SELECT COUNT(*) FROM functions WHERE tested = 1"));
|
|
out.stats.pure_functions = extract_int(api_query(cli, "SELECT COUNT(*) FROM functions WHERE purity = 'pure'"));
|
|
out.stats.impure_functions = extract_int(api_query(cli, "SELECT COUNT(*) FROM functions WHERE purity = 'impure'"));
|
|
|
|
// --- By language ---
|
|
out.by_lang.clear();
|
|
auto j = api_query(cli, "SELECT lang, COUNT(*) as cnt FROM functions GROUP BY lang ORDER BY cnt DESC");
|
|
if (!j.is_null() && j.contains("rows"))
|
|
for (auto& row : j["rows"])
|
|
out.by_lang.push_back({extract_str(row, 0), extract_row_int(row, 1)});
|
|
|
|
// --- By domain ---
|
|
out.by_domain.clear();
|
|
j = api_query(cli, "SELECT domain, COUNT(*) as cnt FROM functions GROUP BY domain ORDER BY cnt DESC");
|
|
if (!j.is_null() && j.contains("rows"))
|
|
for (auto& row : j["rows"])
|
|
out.by_domain.push_back({extract_str(row, 0), extract_row_int(row, 1)});
|
|
|
|
// --- By kind ---
|
|
out.by_kind.clear();
|
|
j = api_query(cli, "SELECT kind, COUNT(*) as cnt FROM functions GROUP BY kind ORDER BY cnt DESC");
|
|
if (!j.is_null() && j.contains("rows"))
|
|
for (auto& row : j["rows"])
|
|
out.by_kind.push_back({extract_str(row, 0), extract_row_int(row, 1)});
|
|
|
|
// --- By date (last 30 days) ---
|
|
out.by_date.clear();
|
|
j = api_query(cli,
|
|
"SELECT date(created_at) as d, COUNT(*) as cnt FROM functions "
|
|
"WHERE created_at >= date('now', '-30 days') GROUP BY d ORDER BY d");
|
|
if (!j.is_null() && j.contains("rows"))
|
|
for (auto& row : j["rows"])
|
|
out.by_date.push_back({extract_str(row, 0), extract_row_int(row, 1)});
|
|
|
|
// --- Recent functions ---
|
|
out.recent_funcs.clear();
|
|
j = api_query(cli,
|
|
"SELECT id, name, lang, domain, kind, purity, description, created_at, tested "
|
|
"FROM functions ORDER BY created_at DESC LIMIT 20");
|
|
if (!j.is_null() && j.contains("rows")) {
|
|
for (auto& row : j["rows"]) {
|
|
FunctionRow r;
|
|
r.id = extract_str(row, 0); r.name = extract_str(row, 1);
|
|
r.lang = extract_str(row, 2); r.domain = extract_str(row, 3);
|
|
r.kind = extract_str(row, 4); r.purity = extract_str(row, 5);
|
|
r.description = extract_str(row, 6); r.created_at = extract_str(row, 7);
|
|
r.tested = extract_row_int(row, 8) != 0;
|
|
out.recent_funcs.push_back(std::move(r));
|
|
}
|
|
}
|
|
|
|
// --- Apps ---
|
|
out.apps.clear();
|
|
j = api_query(cli, "SELECT id, name, lang, domain, description, framework, repo_url, dir_path FROM apps ORDER BY name");
|
|
if (!j.is_null() && j.contains("rows")) {
|
|
for (auto& row : j["rows"]) {
|
|
AppRow r;
|
|
r.id = extract_str(row, 0); r.name = extract_str(row, 1);
|
|
r.lang = extract_str(row, 2); r.domain = extract_str(row, 3);
|
|
r.description = extract_str(row, 4); r.framework = extract_str(row, 5);
|
|
r.repo_url = extract_str(row, 6); r.dir_path = extract_str(row, 7);
|
|
out.apps.push_back(std::move(r));
|
|
}
|
|
}
|
|
|
|
// --- Analysis ---
|
|
out.analyses.clear();
|
|
j = api_query(cli, "SELECT id, name, lang, domain, description FROM analysis ORDER BY name");
|
|
if (!j.is_null() && j.contains("rows")) {
|
|
for (auto& row : j["rows"]) {
|
|
AnalysisRow r;
|
|
r.id = extract_str(row, 0); r.name = extract_str(row, 1);
|
|
r.lang = extract_str(row, 2); r.domain = extract_str(row, 3);
|
|
r.description = extract_str(row, 4);
|
|
out.analyses.push_back(std::move(r));
|
|
}
|
|
}
|
|
|
|
// --- Types ---
|
|
out.types.clear();
|
|
j = api_query(cli, "SELECT id, name, lang, domain, algebraic, description FROM types ORDER BY name");
|
|
if (!j.is_null() && j.contains("rows")) {
|
|
for (auto& row : j["rows"]) {
|
|
TypeRow r;
|
|
r.id = extract_str(row, 0); r.name = extract_str(row, 1);
|
|
r.lang = extract_str(row, 2); r.domain = extract_str(row, 3);
|
|
r.algebraic = extract_str(row, 4); r.description = extract_str(row, 5);
|
|
out.types.push_back(std::move(r));
|
|
}
|
|
}
|
|
|
|
out.prepare_chart_data();
|
|
|
|
// Best-effort: projects (no fatal si falla)
|
|
load_projects_http(api_url, out);
|
|
|
|
return true;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Projects endpoints
|
|
// ---------------------------------------------------------------------------
|
|
|
|
bool load_projects_http(const std::string& api_url, RegistryData& out) {
|
|
std::string host; int port;
|
|
if (!parse_url(api_url, host, port)) return false;
|
|
|
|
HttpClient cli(host, port);
|
|
auto res = cli.get("/api/projects");
|
|
if (!res.ok()) {
|
|
fprintf(stderr, "[http] GET /api/projects failed: %d\n", res.status);
|
|
return false;
|
|
}
|
|
auto j = json::parse(res.body, nullptr, false);
|
|
if (j.is_null()) return false;
|
|
|
|
out.projects.clear();
|
|
if (j.contains("projects") && j["projects"].is_array()) {
|
|
for (auto& p : j["projects"]) {
|
|
ProjectRow r;
|
|
if (p.contains("id") && p["id"].is_string()) r.id = p["id"];
|
|
if (p.contains("name") && p["name"].is_string()) r.name = p["name"];
|
|
if (p.contains("description") && p["description"].is_string()) r.description = p["description"];
|
|
if (p.contains("apps_count") && p["apps_count"].is_number()) r.apps_count = p["apps_count"].get<int>();
|
|
if (p.contains("analyses_count") && p["analyses_count"].is_number()) r.analyses_count = p["analyses_count"].get<int>();
|
|
if (p.contains("vaults_count") && p["vaults_count"].is_number()) r.vaults_count = p["vaults_count"].get<int>();
|
|
out.projects.push_back(std::move(r));
|
|
}
|
|
}
|
|
if (j.contains("orphans") && j["orphans"].is_object()) {
|
|
auto& o = j["orphans"];
|
|
if (o.contains("apps") && o["apps"].is_number()) out.orphan_apps = o["apps"].get<int>();
|
|
if (o.contains("analyses") && o["analyses"].is_number()) out.orphan_analyses = o["analyses"].get<int>();
|
|
if (o.contains("vaults") && o["vaults"].is_number()) out.orphan_vaults = o["vaults"].get<int>();
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool load_project_detail_http(const std::string& api_url,
|
|
const std::string& id,
|
|
ProjectDetail& out) {
|
|
std::string host; int port;
|
|
if (!parse_url(api_url, host, port)) return false;
|
|
|
|
HttpClient cli(host, port);
|
|
auto res = cli.get("/api/projects/" + id);
|
|
if (!res.ok()) {
|
|
fprintf(stderr, "[http] GET /api/projects/%s failed: %d\n", id.c_str(), res.status);
|
|
return false;
|
|
}
|
|
auto j = json::parse(res.body, nullptr, false);
|
|
if (j.is_null()) return false;
|
|
|
|
out = ProjectDetail{};
|
|
out.id = id;
|
|
if (j.contains("project") && j["project"].is_object()) {
|
|
auto& p = j["project"];
|
|
if (p.contains("name") && p["name"].is_string()) out.name = p["name"];
|
|
if (p.contains("description") && p["description"].is_string()) out.description = p["description"];
|
|
}
|
|
|
|
auto read_rows = [](const json& section) -> std::pair<std::vector<std::string>, std::vector<std::vector<std::string>>> {
|
|
std::vector<std::string> cols;
|
|
std::vector<std::vector<std::string>> rows;
|
|
if (!section.is_object()) return {cols, rows};
|
|
if (section.contains("columns") && section["columns"].is_array())
|
|
for (auto& c : section["columns"])
|
|
cols.push_back(c.is_string() ? c.get<std::string>() : "");
|
|
if (section.contains("rows") && section["rows"].is_array()) {
|
|
for (auto& row : section["rows"]) {
|
|
std::vector<std::string> cells;
|
|
for (auto& v : row) {
|
|
if (v.is_string()) cells.push_back(v.get<std::string>());
|
|
else if (v.is_number_integer()) cells.push_back(std::to_string(v.get<long long>()));
|
|
else if (v.is_null()) cells.push_back("");
|
|
else cells.push_back(v.dump());
|
|
}
|
|
rows.push_back(std::move(cells));
|
|
}
|
|
}
|
|
return {cols, rows};
|
|
};
|
|
|
|
// apps: [id, name, lang, domain, framework, description, dir_path]
|
|
if (j.contains("apps")) {
|
|
auto [cols, rows] = read_rows(j["apps"]);
|
|
for (auto& r : rows) {
|
|
AppRow a;
|
|
if (r.size() > 0) a.id = r[0];
|
|
if (r.size() > 1) a.name = r[1];
|
|
if (r.size() > 2) a.lang = r[2];
|
|
if (r.size() > 3) a.domain = r[3];
|
|
if (r.size() > 4) a.framework = r[4];
|
|
if (r.size() > 5) a.description = r[5];
|
|
out.apps.push_back(std::move(a));
|
|
}
|
|
}
|
|
// analyses: [id, name, lang, domain, description, dir_path]
|
|
if (j.contains("analyses")) {
|
|
auto [cols, rows] = read_rows(j["analyses"]);
|
|
for (auto& r : rows) {
|
|
AnalysisRow a;
|
|
if (r.size() > 0) a.id = r[0];
|
|
if (r.size() > 1) a.name = r[1];
|
|
if (r.size() > 2) a.lang = r[2];
|
|
if (r.size() > 3) a.domain = r[3];
|
|
if (r.size() > 4) a.description = r[4];
|
|
out.analyses.push_back(std::move(a));
|
|
}
|
|
}
|
|
// vaults: [id, name, path, symlink, description, tags]
|
|
if (j.contains("vaults")) {
|
|
auto [cols, rows] = read_rows(j["vaults"]);
|
|
for (auto& r : rows) {
|
|
VaultRow v;
|
|
if (r.size() > 0) v.id = r[0];
|
|
if (r.size() > 1) v.name = r[1];
|
|
if (r.size() > 2) v.path = r[2];
|
|
if (r.size() > 3) v.symlink = (r[3] == "1");
|
|
if (r.size() > 4) v.description = r[4];
|
|
out.vaults.push_back(std::move(v));
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Function detail endpoints (Explorer)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
bool load_function_detail_http(const std::string& api_url,
|
|
const std::string& id,
|
|
FunctionDetail& out) {
|
|
std::string host; int port;
|
|
if (!parse_url(api_url, host, port)) return false;
|
|
|
|
HttpClient cli(host, port);
|
|
|
|
// /api/databases/<db>/query solo acepta { "sql": ... } sin args
|
|
// parametrizados (ver handleQuery en sqlite_api). Escapamos comilla
|
|
// simple para que un id con apostrofe no rompa la query — los IDs del
|
|
// registry son [a-z0-9_]+ asi que el escape es defensivo.
|
|
std::string escaped;
|
|
escaped.reserve(id.size());
|
|
for (char c : id) {
|
|
if (c == '\'') escaped += "''";
|
|
else escaped.push_back(c);
|
|
}
|
|
std::string sql =
|
|
"SELECT id, name, lang, domain, kind, purity, version, signature, "
|
|
"description, code, documentation, notes, example, params_schema, "
|
|
"uses_functions, uses_types, returns, error_type, file_path, "
|
|
"created_at, tested FROM functions WHERE id = '" + escaped + "'";
|
|
|
|
auto j = api_query(cli, sql.c_str());
|
|
if (j.is_null() || !j.contains("rows") || j["rows"].empty()) return false;
|
|
|
|
auto& row = j["rows"][0];
|
|
out = FunctionDetail{};
|
|
out.id = extract_str(row, 0);
|
|
out.name = extract_str(row, 1);
|
|
out.lang = extract_str(row, 2);
|
|
out.domain = extract_str(row, 3);
|
|
out.kind = extract_str(row, 4);
|
|
out.purity = extract_str(row, 5);
|
|
out.version = extract_str(row, 6);
|
|
out.signature = extract_str(row, 7);
|
|
out.description = extract_str(row, 8);
|
|
out.code = extract_str(row, 9);
|
|
out.documentation = extract_str(row, 10);
|
|
out.notes = extract_str(row, 11);
|
|
out.example = extract_str(row, 12);
|
|
out.params_schema = extract_str(row, 13);
|
|
out.uses_functions= extract_str(row, 14);
|
|
out.uses_types = extract_str(row, 15);
|
|
out.returns = extract_str(row, 16);
|
|
out.error_type = extract_str(row, 17);
|
|
out.file_path = extract_str(row, 18);
|
|
out.created_at = extract_str(row, 19);
|
|
out.tested = extract_row_int(row, 20) != 0;
|
|
return true;
|
|
}
|
|
|
|
bool load_all_functions_http(const std::string& api_url,
|
|
std::vector<FunctionRow>& out) {
|
|
std::string host; int port;
|
|
if (!parse_url(api_url, host, port)) return false;
|
|
|
|
HttpClient cli(host, port);
|
|
auto j = api_query(cli,
|
|
"SELECT id, name, lang, domain, kind, purity, description, "
|
|
"created_at, tested FROM functions ORDER BY name");
|
|
if (j.is_null() || !j.contains("rows")) return false;
|
|
|
|
out.clear();
|
|
out.reserve(j["rows"].size());
|
|
for (auto& row : j["rows"]) {
|
|
FunctionRow r;
|
|
r.id = extract_str(row, 0);
|
|
r.name = extract_str(row, 1);
|
|
r.lang = extract_str(row, 2);
|
|
r.domain = extract_str(row, 3);
|
|
r.kind = extract_str(row, 4);
|
|
r.purity = extract_str(row, 5);
|
|
r.description = extract_str(row, 6);
|
|
r.created_at = extract_str(row, 7);
|
|
r.tested = extract_row_int(row, 8) != 0;
|
|
out.push_back(std::move(r));
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool load_unit_tests_http(const std::string& api_url,
|
|
const std::string& function_id,
|
|
std::vector<UnitTestRow>& out) {
|
|
std::string host; int port;
|
|
if (!parse_url(api_url, host, port)) return false;
|
|
|
|
HttpClient cli(host, port);
|
|
|
|
// Mismo escape defensivo que en load_function_detail_http — los IDs son
|
|
// [a-z0-9_]+ pero por consistencia escapamos comilla simple.
|
|
std::string escaped;
|
|
escaped.reserve(function_id.size());
|
|
for (char c : function_id) {
|
|
if (c == '\'') escaped += "''";
|
|
else escaped.push_back(c);
|
|
}
|
|
std::string sql =
|
|
"SELECT id, function_id, name, lang, file_path, code, created_at "
|
|
"FROM unit_tests WHERE function_id = '" + escaped + "' ORDER BY name";
|
|
|
|
auto j = api_query(cli, sql.c_str());
|
|
if (j.is_null() || !j.contains("rows")) return false;
|
|
|
|
out.clear();
|
|
for (auto& row : j["rows"]) {
|
|
UnitTestRow r;
|
|
r.id = extract_str(row, 0);
|
|
r.function_id = extract_str(row, 1);
|
|
r.name = extract_str(row, 2);
|
|
r.lang = extract_str(row, 3);
|
|
r.file_path = extract_str(row, 4);
|
|
r.code = extract_str(row, 5);
|
|
r.created_at = extract_str(row, 6);
|
|
out.push_back(std::move(r));
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Mutation endpoints
|
|
// ---------------------------------------------------------------------------
|
|
|
|
static bool post_json(const std::string& api_url, const std::string& path,
|
|
const json& body, std::string& out_body) {
|
|
std::string host; int port;
|
|
if (!parse_url(api_url, host, port)) {
|
|
out_body = "invalid API URL: " + api_url;
|
|
return false;
|
|
}
|
|
HttpClient cli(host, port);
|
|
auto res = cli.post(path, body.dump(), "application/json");
|
|
|
|
// Mensaje util para el toast: si OK, intenta sacar "output" del JSON.
|
|
// Si error, incluye status + error del body si existe.
|
|
if (res.ok()) {
|
|
auto j = json::parse(res.body, nullptr, false);
|
|
if (!j.is_null()) {
|
|
if (j.contains("output") && j["output"].is_string())
|
|
out_body = j["output"].get<std::string>();
|
|
else if (j.contains("ok") && j["ok"].is_boolean())
|
|
out_body = j["ok"].get<bool>() ? "OK" : "failed";
|
|
else
|
|
out_body = res.body;
|
|
} else {
|
|
out_body = res.body.empty() ? "OK" : res.body;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// Error path con ASCII (la fuente puede no tener em dash). Para status=0
|
|
// el http_client ya ha escrito un diagnostico de connect() en res.body.
|
|
if (res.status == 0) {
|
|
out_body = res.body.empty()
|
|
? "connection failed (is sqlite_api running?)"
|
|
: res.body;
|
|
return false;
|
|
}
|
|
|
|
auto j = json::parse(res.body, nullptr, false);
|
|
std::string detail;
|
|
if (!j.is_null() && j.contains("error") && j["error"].is_string())
|
|
detail = j["error"].get<std::string>();
|
|
else if (!res.body.empty())
|
|
detail = res.body;
|
|
|
|
char buf[64];
|
|
std::snprintf(buf, sizeof(buf), "HTTP %d: ", res.status);
|
|
out_body = std::string(buf) + detail;
|
|
return false;
|
|
}
|
|
|
|
bool http_post_reindex(const std::string& api_url, std::string& out_body) {
|
|
return post_json(api_url, "/api/reindex", json::object(), out_body);
|
|
}
|
|
|
|
bool http_post_add_app(const std::string& api_url,
|
|
const std::string& name, const std::string& lang,
|
|
const std::string& domain, const std::string& project,
|
|
const std::string& description,
|
|
std::string& out_body) {
|
|
json b;
|
|
b["name"] = name;
|
|
b["lang"] = lang;
|
|
b["domain"] = domain;
|
|
b["project"] = project;
|
|
b["description"] = description;
|
|
return post_json(api_url, "/api/add/app", b, out_body);
|
|
}
|
|
|
|
bool http_post_add_analysis(const std::string& api_url,
|
|
const std::string& name, const std::string& project,
|
|
const std::string& packages_csv,
|
|
const std::string& description,
|
|
std::string& out_body) {
|
|
json b;
|
|
b["name"] = name;
|
|
b["project"] = project;
|
|
b["description"] = description;
|
|
// Packages como array. Split CSV.
|
|
json pkgs = json::array();
|
|
std::string cur;
|
|
for (char c : packages_csv) {
|
|
if (c == ',' || c == ' ') {
|
|
if (!cur.empty()) { pkgs.push_back(cur); cur.clear(); }
|
|
} else cur.push_back(c);
|
|
}
|
|
if (!cur.empty()) pkgs.push_back(cur);
|
|
b["packages"] = pkgs;
|
|
return post_json(api_url, "/api/add/analysis", b, out_body);
|
|
}
|
|
|
|
bool http_post_add_vault(const std::string& api_url,
|
|
const std::string& name, const std::string& project,
|
|
const std::string& path, const std::string& description,
|
|
std::string& out_body) {
|
|
json b;
|
|
b["name"] = name;
|
|
b["project"] = project;
|
|
b["path"] = path;
|
|
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()));
|
|
}
|
|
// MCP / fn run / heredoc — herramientas registry-aware. Cubre las
|
|
// variantes vistas en produccion: mcp, mcp_fn_search, mcp_fn_run,
|
|
// fn_cli_run, fn_run_cli, heredoc, heredoc_py.
|
|
static const char* kRegistryAwareToolCond =
|
|
"(tool_used LIKE 'mcp%' OR tool_used LIKE 'heredoc%' "
|
|
"OR tool_used IN ('fn_cli_run','fn_run_cli'))";
|
|
{
|
|
const std::string wf_and = wf_calls.empty()
|
|
? std::string(" WHERE ") + kRegistryAwareToolCond
|
|
: std::string(wf_calls + " AND " + kRegistryAwareToolCond);
|
|
const std::string sql = "SELECT COUNT(*) FROM calls" + wf_and;
|
|
out.claude.total_mcp = extract_int(call_monitor_query(cli, sql.c_str()));
|
|
}
|
|
// % calls que llamaron a una funcion del registry (function_id no vacio).
|
|
{
|
|
const std::string wf_and = wf_calls.empty()
|
|
? std::string(" WHERE function_id != '' ")
|
|
: std::string(wf_calls + " AND function_id != '' ");
|
|
const std::string sql = "SELECT COUNT(*) FROM calls" + wf_and;
|
|
int reg_hits = extract_int(call_monitor_query(cli, sql.c_str()));
|
|
out.claude.registry_pct = (out.claude.total_calls > 0)
|
|
? 100.0 * static_cast<double>(reg_hits) / static_cast<double>(out.claude.total_calls)
|
|
: 0.0;
|
|
}
|
|
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, "
|
|
"COALESCE(command_snippet,'') AS command_snippet, "
|
|
"COALESCE(error_snippet,'') AS error_snippet "
|
|
"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);
|
|
row.command_snippet = extract_str(r, 8);
|
|
row.error_snippet = extract_str(r, 9);
|
|
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;
|
|
}
|