diff --git a/CMakeLists.txt b/CMakeLists.txt index 6ab60c8..da30be9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -17,6 +17,7 @@ endif() add_imgui_app(registry_dashboard main.cpp data.cpp + data_http.cpp views.cpp ${CMAKE_SOURCE_DIR}/functions/viz/kpi_card.cpp ${CMAKE_SOURCE_DIR}/functions/viz/bar_chart.cpp @@ -31,6 +32,18 @@ add_imgui_app(registry_dashboard target_include_directories(registry_dashboard PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_SOURCE_DIR}/vendor ) target_link_libraries(registry_dashboard PRIVATE SQLite::SQLite3) + +# cpp-httplib needs threads on Linux +if(UNIX) + find_package(Threads REQUIRED) + target_link_libraries(registry_dashboard PRIVATE Threads::Threads) +endif() + +# On Windows cross-compile, link ws2_32 for sockets +if(WIN32) + target_link_libraries(registry_dashboard PRIVATE ws2_32) +endif() diff --git a/data_http.cpp b/data_http.cpp new file mode 100644 index 0000000..6fccd97 --- /dev/null +++ b/data_http.cpp @@ -0,0 +1,202 @@ +#include "data_http.h" + +#define CPPHTTPLIB_OPENSSL_SUPPORT 0 +#include "vendor/httplib.h" +#include "vendor/nlohmann/json.hpp" + +#include + +using json = nlohmann::json; + +// POST a SQL query to the API and return parsed JSON, or null on failure. +static json api_query(httplib::Client& cli, const char* sql) { + json body; + body["sql"] = sql; + auto res = cli.Post("/api/databases/registry/query", + body.dump(), "application/json"); + if (!res || res->status != 200) { + if (res) 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); +} + +// Extract first int from a single-row, single-column result. +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(); + if (val.is_string()) return std::atoi(val.get().c_str()); + return 0; +} + +// Extract string from a row at given column index. +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(); + return row[idx].dump(); +} + +// Extract int from a row at given column index. +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(); + if (row[idx].is_string()) return std::atoi(row[idx].get().c_str()); + return 0; +} + +bool load_registry_data_http(const std::string& api_url, RegistryData& out) { + httplib::Client cli(api_url); + cli.set_connection_timeout(2); + cli.set_read_timeout(5); + + // Health check first + auto health = cli.Get("/health"); + if (!health || health->status != 200) { + 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 --- + auto j = api_query(cli, "SELECT COUNT(*) FROM functions"); + out.stats.total_functions = extract_int(j); + + j = api_query(cli, "SELECT COUNT(*) FROM types"); + out.stats.total_types = extract_int(j); + + j = api_query(cli, "SELECT COUNT(*) FROM apps"); + out.stats.total_apps = extract_int(j); + + j = api_query(cli, "SELECT COUNT(*) FROM analysis"); + out.stats.total_analysis = extract_int(j); + + j = api_query(cli, "SELECT COUNT(*) FROM unit_tests"); + out.stats.total_unit_tests = extract_int(j); + + j = api_query(cli, "SELECT COUNT(*) FROM proposals"); + out.stats.total_proposals = extract_int(j); + + j = api_query(cli, "SELECT COUNT(*) FROM functions WHERE tested = 1"); + out.stats.tested_functions = extract_int(j); + + j = api_query(cli, "SELECT COUNT(*) FROM functions WHERE purity = 'pure'"); + out.stats.pure_functions = extract_int(j); + + j = api_query(cli, "SELECT COUNT(*) FROM functions WHERE purity = 'impure'"); + out.stats.impure_functions = extract_int(j); + + // --- By language --- + out.by_lang.clear(); + 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 (last 20) --- + 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 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); + out.apps.push_back(std::move(r)); + } + } + + // --- Analysis --- + out.analyses.clear(); + j = api_query(cli, + "SELECT id, name, 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.domain = extract_str(row, 2); + r.description = extract_str(row, 3); + 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(); + return true; +} diff --git a/data_http.h b/data_http.h new file mode 100644 index 0000000..4f9d022 --- /dev/null +++ b/data_http.h @@ -0,0 +1,9 @@ +#pragma once + +#include "data.h" +#include + +// Load all registry data via sqlite_api HTTP endpoint. +// api_url should be like "http://127.0.0.1:8484". +// Returns true on success. +bool load_registry_data_http(const std::string& api_url, RegistryData& out);