Initial commit: registry_dashboard — Dashboard ImGui para fn_registry

This commit is contained in:
Egutierrez
2026-04-08 00:42:29 +02:00
commit 303b6d476d
9 changed files with 722 additions and 0 deletions
+4
View File
@@ -0,0 +1,4 @@
operations.db
operations.db-wal
operations.db-shm
*.exe
+36
View File
@@ -0,0 +1,36 @@
# SQLite3: use system library on Linux, vendored amalgamation on Windows cross-compile
find_package(SQLite3 QUIET)
if(NOT SQLite3_FOUND)
# Build from amalgamation
set(SQLITE3_AMALG_DIR ${CMAKE_SOURCE_DIR}/vendor/sqlite3)
add_library(sqlite3_vendored STATIC ${SQLITE3_AMALG_DIR}/sqlite3.c)
target_include_directories(sqlite3_vendored PUBLIC ${SQLITE3_AMALG_DIR})
target_compile_definitions(sqlite3_vendored PRIVATE
SQLITE_THREADSAFE=1
SQLITE_ENABLE_FTS5
SQLITE_ENABLE_JSON1
)
# Alias so we can use the same target name
add_library(SQLite::SQLite3 ALIAS sqlite3_vendored)
endif()
add_imgui_app(registry_dashboard
main.cpp
data.cpp
views.cpp
${CMAKE_SOURCE_DIR}/functions/viz/kpi_card.cpp
${CMAKE_SOURCE_DIR}/functions/viz/bar_chart.cpp
${CMAKE_SOURCE_DIR}/functions/viz/pie_chart.cpp
${CMAKE_SOURCE_DIR}/functions/viz/table_view.cpp
${CMAKE_SOURCE_DIR}/functions/viz/sparkline.cpp
${CMAKE_SOURCE_DIR}/functions/core/dashboard_panel.cpp
${CMAKE_SOURCE_DIR}/functions/core/dashboard_grid.cpp
${CMAKE_SOURCE_DIR}/functions/core/fps_overlay.cpp
${CMAKE_SOURCE_DIR}/functions/core/fullscreen_window.cpp
)
target_include_directories(registry_dashboard PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}
)
target_link_libraries(registry_dashboard PRIVATE SQLite::SQLite3)
+70
View File
@@ -0,0 +1,70 @@
---
name: registry_dashboard
lang: cpp
domain: tui
description: "Dashboard ImGui para visualizar el estado del fn_registry: KPIs, charts, tablas, desglose por lenguaje/dominio/pureza"
tags: [dashboard, imgui, visualization, registry]
uses_functions:
- kpi_card_cpp_viz
- bar_chart_cpp_viz
- pie_chart_cpp_viz
- table_view_cpp_viz
- sparkline_cpp_viz
- dashboard_panel_cpp_core
- dashboard_grid_cpp_core
- fps_overlay_cpp_core
- fullscreen_window_cpp_core
uses_types: []
framework: "imgui"
entry_point: "main.cpp"
dir_path: "apps/registry_dashboard"
repo_url: ""
---
## Arquitectura
Dashboard C++ que lee `registry.db` directamente via SQLite C API y visualiza el estado completo del fn_registry.
**Data layer** (`data.cpp`): Carga todas las stats, desgloses y listas en un solo paso desde SQLite. Soporte para reload en caliente.
**Views** (`views.cpp`): Compone funciones del registry C++ para renderizar:
- 6 KPI cards: total functions, types, apps, analysis, tested%, pure%
- Bar charts: funciones por lenguaje, por dominio, actividad ultimos 30 dias
- Pie charts: pureza (pure/impure), kind (function/pipeline/component), tested/untested
- Tablas: ultimas 20 funciones, apps, analysis, tipos
**Entrada**: Paths a `registry.db` como argumentos con fallback encadenado.
## Build
```bash
# Linux
cd cpp && cmake -B build/linux -S . && cmake --build build/linux --target registry_dashboard -j$(nproc)
# Windows (cross-compile)
cd cpp && cmake -B build/windows -S . -DCMAKE_TOOLCHAIN_FILE=toolchains/mingw-w64.cmake && cmake --build build/windows --target registry_dashboard -j$(nproc)
```
## Ejecucion
```bash
# Linux
./cpp/build/linux/apps/registry_dashboard/registry_dashboard /home/lucas/fn_registry/registry.db
# Windows (PowerShell) — intenta WSL mount, luego local
.\registry_dashboard.ps1
# Otra maquina — copiar .exe + .db al mismo dir
registry_dashboard.exe .\registry.db
```
## Roadmap
- [ ] Integrar Go runtime (CGo) para ejecutar comandos `fn` desde el GUI
- [ ] Filtros interactivos por lenguaje/dominio en sidebar
- [ ] Busqueda FTS5 integrada
- [ ] Detalles de funcion al hacer click en tabla
## Notas
SQLite compilado estaticamente en Windows via amalgamation vendoreada. En Linux usa libsqlite3 del sistema.
+188
View File
@@ -0,0 +1,188 @@
#include "data.h"
#include <sqlite3.h>
#include <cstdio>
// Helper: execute a query and call row_fn for each row
template<typename F>
static bool query(sqlite3* db, const char* sql, F row_fn) {
sqlite3_stmt* stmt = nullptr;
if (sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr) != SQLITE_OK) {
fprintf(stderr, "SQL error: %s\n query: %s\n", sqlite3_errmsg(db), sql);
return false;
}
while (sqlite3_step(stmt) == SQLITE_ROW) {
row_fn(stmt);
}
sqlite3_finalize(stmt);
return true;
}
static std::string col_str(sqlite3_stmt* s, int i) {
const char* t = reinterpret_cast<const char*>(sqlite3_column_text(s, i));
return t ? t : "";
}
static int col_int(sqlite3_stmt* s, int i) {
return sqlite3_column_int(s, i);
}
void RegistryData::prepare_chart_data() {
lang_labels.clear(); lang_values.clear();
for (auto& lc : by_lang) {
lang_labels.push_back(lc.lang);
lang_values.push_back(static_cast<float>(lc.count));
}
domain_labels.clear(); domain_values.clear();
for (auto& dc : by_domain) {
domain_labels.push_back(dc.domain);
domain_values.push_back(static_cast<float>(dc.count));
}
kind_labels.clear(); kind_values.clear();
for (auto& kc : by_kind) {
kind_labels.push_back(kc.kind);
kind_values.push_back(static_cast<float>(kc.count));
}
date_labels.clear(); date_values.clear();
for (auto& dc : by_date) {
date_labels.push_back(dc.date);
date_values.push_back(static_cast<float>(dc.count));
}
}
bool load_registry_data(const char* db_path, RegistryData& out) {
sqlite3* db = nullptr;
if (sqlite3_open_v2(db_path, &db, SQLITE_OPEN_READONLY, nullptr) != SQLITE_OK) {
fprintf(stderr, "Cannot open %s: %s\n", db_path, sqlite3_errmsg(db));
return false;
}
// --- Counts ---
query(db, "SELECT COUNT(*) FROM functions", [&](sqlite3_stmt* s) {
out.stats.total_functions = col_int(s, 0);
});
query(db, "SELECT COUNT(*) FROM types", [&](sqlite3_stmt* s) {
out.stats.total_types = col_int(s, 0);
});
query(db, "SELECT COUNT(*) FROM apps", [&](sqlite3_stmt* s) {
out.stats.total_apps = col_int(s, 0);
});
query(db, "SELECT COUNT(*) FROM analysis", [&](sqlite3_stmt* s) {
out.stats.total_analysis = col_int(s, 0);
});
query(db, "SELECT COUNT(*) FROM unit_tests", [&](sqlite3_stmt* s) {
out.stats.total_unit_tests = col_int(s, 0);
});
query(db, "SELECT COUNT(*) FROM proposals", [&](sqlite3_stmt* s) {
out.stats.total_proposals = col_int(s, 0);
});
query(db, "SELECT COUNT(*) FROM functions WHERE tested = 1", [&](sqlite3_stmt* s) {
out.stats.tested_functions = col_int(s, 0);
});
query(db, "SELECT COUNT(*) FROM functions WHERE purity = 'pure'", [&](sqlite3_stmt* s) {
out.stats.pure_functions = col_int(s, 0);
});
query(db, "SELECT COUNT(*) FROM functions WHERE purity = 'impure'", [&](sqlite3_stmt* s) {
out.stats.impure_functions = col_int(s, 0);
});
// --- By language ---
out.by_lang.clear();
query(db, "SELECT lang, COUNT(*) as cnt FROM functions GROUP BY lang ORDER BY cnt DESC",
[&](sqlite3_stmt* s) {
out.by_lang.push_back({col_str(s, 0), col_int(s, 1)});
});
// --- By domain ---
out.by_domain.clear();
query(db, "SELECT domain, COUNT(*) as cnt FROM functions GROUP BY domain ORDER BY cnt DESC",
[&](sqlite3_stmt* s) {
out.by_domain.push_back({col_str(s, 0), col_int(s, 1)});
});
// --- By kind ---
out.by_kind.clear();
query(db, "SELECT kind, COUNT(*) as cnt FROM functions GROUP BY kind ORDER BY cnt DESC",
[&](sqlite3_stmt* s) {
out.by_kind.push_back({col_str(s, 0), col_int(s, 1)});
});
// --- By date (last 30 days) ---
out.by_date.clear();
query(db,
"SELECT date(created_at) as d, COUNT(*) as cnt FROM functions "
"WHERE created_at >= date('now', '-30 days') "
"GROUP BY d ORDER BY d",
[&](sqlite3_stmt* s) {
out.by_date.push_back({col_str(s, 0), col_int(s, 1)});
});
// --- Recent functions (last 20) ---
out.recent_funcs.clear();
query(db,
"SELECT id, name, lang, domain, kind, purity, description, created_at, tested "
"FROM functions ORDER BY created_at DESC LIMIT 20",
[&](sqlite3_stmt* s) {
FunctionRow r;
r.id = col_str(s, 0);
r.name = col_str(s, 1);
r.lang = col_str(s, 2);
r.domain = col_str(s, 3);
r.kind = col_str(s, 4);
r.purity = col_str(s, 5);
r.description = col_str(s, 6);
r.created_at = col_str(s, 7);
r.tested = col_int(s, 8) != 0;
out.recent_funcs.push_back(std::move(r));
});
// --- Apps ---
out.apps.clear();
query(db,
"SELECT id, name, lang, domain, description, framework FROM apps ORDER BY name",
[&](sqlite3_stmt* s) {
AppRow r;
r.id = col_str(s, 0);
r.name = col_str(s, 1);
r.lang = col_str(s, 2);
r.domain = col_str(s, 3);
r.description = col_str(s, 4);
r.framework = col_str(s, 5);
out.apps.push_back(std::move(r));
});
// --- Analysis ---
out.analyses.clear();
query(db,
"SELECT id, name, domain, description FROM analysis ORDER BY name",
[&](sqlite3_stmt* s) {
AnalysisRow r;
r.id = col_str(s, 0);
r.name = col_str(s, 1);
r.domain = col_str(s, 2);
r.description = col_str(s, 3);
out.analyses.push_back(std::move(r));
});
// --- Types ---
out.types.clear();
query(db,
"SELECT id, name, lang, domain, algebraic, description FROM types ORDER BY name",
[&](sqlite3_stmt* s) {
TypeRow r;
r.id = col_str(s, 0);
r.name = col_str(s, 1);
r.lang = col_str(s, 2);
r.domain = col_str(s, 3);
r.algebraic = col_str(s, 4);
r.description = col_str(s, 5);
out.types.push_back(std::move(r));
});
sqlite3_close(db);
out.prepare_chart_data();
return true;
}
+102
View File
@@ -0,0 +1,102 @@
#pragma once
#include <string>
#include <vector>
#include <cstdint>
struct RegistryStats {
int total_functions = 0;
int total_types = 0;
int total_apps = 0;
int total_analysis = 0;
int total_unit_tests = 0;
int total_proposals = 0;
int tested_functions = 0;
int pure_functions = 0;
int impure_functions = 0;
};
struct LangCount {
std::string lang;
int count = 0;
};
struct DomainCount {
std::string domain;
int count = 0;
};
struct KindCount {
std::string kind;
int count = 0;
};
struct DateCount {
std::string date; // YYYY-MM-DD
int count = 0;
};
struct FunctionRow {
std::string id;
std::string name;
std::string lang;
std::string domain;
std::string kind;
std::string purity;
std::string description;
std::string created_at;
bool tested = false;
};
struct AppRow {
std::string id;
std::string name;
std::string lang;
std::string domain;
std::string description;
std::string framework;
};
struct AnalysisRow {
std::string id;
std::string name;
std::string domain;
std::string description;
};
struct TypeRow {
std::string id;
std::string name;
std::string lang;
std::string domain;
std::string algebraic;
std::string description;
};
// All data loaded from registry.db in one shot
struct RegistryData {
RegistryStats stats;
std::vector<LangCount> by_lang;
std::vector<DomainCount> by_domain;
std::vector<KindCount> by_kind;
std::vector<DateCount> by_date; // last 30 days
std::vector<FunctionRow> recent_funcs; // last 20
std::vector<AppRow> apps;
std::vector<AnalysisRow> analyses;
std::vector<TypeRow> types;
// For chart data (populated by prepare_chart_data)
std::vector<std::string> lang_labels;
std::vector<float> lang_values;
std::vector<std::string> domain_labels;
std::vector<float> domain_values;
std::vector<std::string> kind_labels;
std::vector<float> kind_values;
std::vector<std::string> date_labels;
std::vector<float> date_values;
void prepare_chart_data();
};
// Load all data from registry.db. Returns true on success.
bool load_registry_data(const char* db_path, RegistryData& out);
+78
View File
@@ -0,0 +1,78 @@
#include "app_base.h"
#include "imgui.h"
#include "core/fullscreen_window.h"
#include "data.h"
#include "views.h"
#include <cstdio>
#include <cstdlib>
#include <string>
#include <fstream>
static RegistryData g_data;
static std::string g_db_path;
static bool g_loaded = false;
static void reload_data() {
g_data = RegistryData{};
g_loaded = load_registry_data(g_db_path.c_str(), g_data);
if (!g_loaded) {
fprintf(stderr, "Failed to load registry data from: %s\n", g_db_path.c_str());
}
}
static void render() {
if (ImGui::GetIO().UserData != nullptr) {
ImGui::GetIO().UserData = nullptr;
reload_data();
}
if (!g_loaded) {
fullscreen_window_begin("##error");
ImGui::TextColored(ImVec4(1, 0.3f, 0.3f, 1),
"Could not open registry.db");
ImGui::Spacing();
ImGui::Text("Tried: %s", g_db_path.c_str());
ImGui::Spacing();
ImGui::TextWrapped("Usage: registry_dashboard <path1> [path2] [path3] ...");
ImGui::Spacing();
if (ImGui::Button("Retry")) {
reload_data();
}
fullscreen_window_end();
return;
}
draw_dashboard(g_data);
}
int main(int argc, char** argv) {
if (argc < 2) {
fprintf(stderr, "Usage: registry_dashboard <db_path> [fallback_path ...]\n");
fprintf(stderr, " Tries each path in order until one opens successfully.\n");
return 1;
}
// Try each argument in order
for (int i = 1; i < argc; i++) {
std::string candidate = argv[i];
if (std::ifstream(candidate).good()) {
g_db_path = candidate;
fprintf(stdout, "Using: %s\n", g_db_path.c_str());
break;
}
fprintf(stderr, "Not found: %s\n", candidate);
}
if (g_db_path.empty()) {
// None found, use last arg so error screen shows something useful
g_db_path = argv[argc - 1];
}
reload_data();
return fn::run_app(
{.title = "fn_registry Dashboard", .width = 1600, .height = 1000, .viewports = true},
render
);
}
+6
View File
@@ -0,0 +1,6 @@
$exe = Join-Path $PSScriptRoot "registry_dashboard.exe"
& $exe `
"\\wsl.localhost\Ubuntu\home\lucas\fn_registry\registry.db" `
"\\wsl$\Ubuntu\home\lucas\fn_registry\registry.db" `
"$PSScriptRoot\registry.db"
+224
View File
@@ -0,0 +1,224 @@
#include "views.h"
#include "imgui.h"
#include "implot.h"
#include "viz/kpi_card.h"
#include "viz/bar_chart.h"
#include "viz/pie_chart.h"
#include "viz/table_view.h"
#include "viz/sparkline.h"
#include "core/dashboard_panel.h"
#include "core/dashboard_grid.h"
#include "core/fps_overlay.h"
#include "core/fullscreen_window.h"
#include <vector>
static std::vector<const char*> to_cstr(const std::vector<std::string>& v) {
std::vector<const char*> out;
out.reserve(v.size());
for (auto& s : v) out.push_back(s.c_str());
return out;
}
void draw_kpi_row(const RegistryStats& stats) {
float tested_pct = stats.total_functions > 0
? 100.0f * stats.tested_functions / stats.total_functions
: 0.0f;
float pure_pct = stats.total_functions > 0
? 100.0f * stats.pure_functions / stats.total_functions
: 0.0f;
dashboard_grid_begin(6, 12.0f);
kpi_card("Functions", static_cast<float>(stats.total_functions), 0, nullptr, 0, "%.0f");
dashboard_grid_next();
kpi_card("Types", static_cast<float>(stats.total_types), 0, nullptr, 0, "%.0f");
dashboard_grid_next();
kpi_card("Apps", static_cast<float>(stats.total_apps), 0, nullptr, 0, "%.0f");
dashboard_grid_next();
kpi_card("Analysis", static_cast<float>(stats.total_analysis), 0, nullptr, 0, "%.0f");
dashboard_grid_next();
kpi_card("Tested %", tested_pct, 0, nullptr, 0, "%.1f%%");
dashboard_grid_next();
kpi_card("Pure %", pure_pct, 0, nullptr, 0, "%.1f%%");
dashboard_grid_end();
}
void draw_charts(RegistryData& data) {
dashboard_grid_begin(2, 12.0f);
if (dashboard_panel_begin("Functions by Language", 300, 280)) {
auto labels = to_cstr(data.lang_labels);
if (!labels.empty())
bar_chart("##lang", labels.data(), data.lang_values.data(), static_cast<int>(labels.size()));
}
dashboard_panel_end();
dashboard_grid_next();
if (dashboard_panel_begin("Functions by Domain", 300, 280)) {
auto labels = to_cstr(data.domain_labels);
if (!labels.empty())
bar_chart("##domain", labels.data(), data.domain_values.data(), static_cast<int>(labels.size()));
}
dashboard_panel_end();
dashboard_grid_next();
if (dashboard_panel_begin("Purity", 250, 280)) {
const char* labels[] = {"Pure", "Impure"};
float values[] = {static_cast<float>(data.stats.pure_functions), static_cast<float>(data.stats.impure_functions)};
pie_chart("##purity", labels, values, 2);
}
dashboard_panel_end();
dashboard_grid_next();
if (dashboard_panel_begin("Kind", 250, 280)) {
auto labels = to_cstr(data.kind_labels);
if (!labels.empty())
pie_chart("##kind", labels.data(), data.kind_values.data(), static_cast<int>(labels.size()));
}
dashboard_panel_end();
dashboard_grid_next();
if (dashboard_panel_begin("Activity (last 30 days)", 600, 280)) {
auto labels = to_cstr(data.date_labels);
if (!labels.empty())
bar_chart("##dates", labels.data(), data.date_values.data(), static_cast<int>(labels.size()));
else
ImGui::TextDisabled("No functions created in the last 30 days");
}
dashboard_panel_end();
dashboard_grid_next();
if (dashboard_panel_begin("Extras", 250, 280)) {
kpi_card("Unit Tests", static_cast<float>(data.stats.total_unit_tests), 0, nullptr, 0, "%.0f");
ImGui::Spacing();
kpi_card("Proposals", static_cast<float>(data.stats.total_proposals), 0, nullptr, 0, "%.0f");
ImGui::Spacing();
const char* t_labels[] = {"Tested", "Untested"};
float t_values[] = {static_cast<float>(data.stats.tested_functions),
static_cast<float>(data.stats.total_functions - data.stats.tested_functions)};
pie_chart("##tested", t_labels, t_values, 2);
}
dashboard_panel_end();
dashboard_grid_end();
}
void draw_recent_functions(const std::vector<FunctionRow>& funcs) {
const char* headers[] = {"Name", "Lang", "Domain", "Kind", "Purity", "Tested", "Created"};
constexpr int cols = 7;
std::vector<std::string> cell_strings;
cell_strings.reserve(funcs.size() * cols);
for (auto& f : funcs) {
cell_strings.push_back(f.name);
cell_strings.push_back(f.lang);
cell_strings.push_back(f.domain);
cell_strings.push_back(f.kind);
cell_strings.push_back(f.purity);
cell_strings.push_back(f.tested ? "yes" : "no");
cell_strings.push_back(f.created_at.substr(0, 10));
}
auto cells = to_cstr(cell_strings);
table_view("##recent", headers, cols, cells.data(), static_cast<int>(funcs.size()));
}
void draw_apps_list(const std::vector<AppRow>& apps) {
const char* headers[] = {"Name", "Lang", "Domain", "Framework", "Description"};
constexpr int cols = 5;
std::vector<std::string> cell_strings;
cell_strings.reserve(apps.size() * cols);
for (auto& a : apps) {
cell_strings.push_back(a.name);
cell_strings.push_back(a.lang);
cell_strings.push_back(a.domain);
cell_strings.push_back(a.framework);
cell_strings.push_back(a.description);
}
auto cells = to_cstr(cell_strings);
table_view("##apps", headers, cols, cells.data(), static_cast<int>(apps.size()));
}
void draw_analysis_list(const std::vector<AnalysisRow>& analyses) {
const char* headers[] = {"Name", "Domain", "Description"};
constexpr int cols = 3;
std::vector<std::string> cell_strings;
cell_strings.reserve(analyses.size() * cols);
for (auto& a : analyses) {
cell_strings.push_back(a.name);
cell_strings.push_back(a.domain);
cell_strings.push_back(a.description);
}
auto cells = to_cstr(cell_strings);
table_view("##analysis", headers, cols, cells.data(), static_cast<int>(analyses.size()));
}
void draw_types_list(const std::vector<TypeRow>& types) {
const char* headers[] = {"Name", "Lang", "Domain", "Algebraic", "Description"};
constexpr int cols = 5;
std::vector<std::string> cell_strings;
cell_strings.reserve(types.size() * cols);
for (auto& t : types) {
cell_strings.push_back(t.name);
cell_strings.push_back(t.lang);
cell_strings.push_back(t.domain);
cell_strings.push_back(t.algebraic);
cell_strings.push_back(t.description);
}
auto cells = to_cstr(cell_strings);
table_view("##types", headers, cols, cells.data(), static_cast<int>(types.size()));
}
void draw_dashboard(RegistryData& data) {
fps_overlay();
fullscreen_window_begin("##dashboard");
// Header
ImGui::Text("fn_registry Dashboard");
ImGui::SameLine(ImGui::GetWindowWidth() - 100);
if (ImGui::Button("Reload")) {
ImGui::GetIO().UserData = reinterpret_cast<void*>(1);
}
ImGui::Separator();
ImGui::Spacing();
// KPIs
draw_kpi_row(data.stats);
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
// Charts
draw_charts(data);
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
// Tables in tabs
if (ImGui::BeginTabBar("##tables")) {
if (ImGui::BeginTabItem("Recent Functions")) {
draw_recent_functions(data.recent_funcs);
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("Apps")) {
draw_apps_list(data.apps);
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("Analysis")) {
draw_analysis_list(data.analyses);
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("Types")) {
draw_types_list(data.types);
ImGui::EndTabItem();
}
ImGui::EndTabBar();
}
fullscreen_window_end();
}
+14
View File
@@ -0,0 +1,14 @@
#pragma once
#include "data.h"
// Draw the full dashboard. Call every frame.
void draw_dashboard(RegistryData& data);
// Individual views (called by draw_dashboard)
void draw_kpi_row(const RegistryStats& stats);
void draw_charts(RegistryData& data);
void draw_recent_functions(const std::vector<FunctionRow>& funcs);
void draw_apps_list(const std::vector<AppRow>& apps);
void draw_analysis_list(const std::vector<AnalysisRow>& analyses);
void draw_types_list(const std::vector<TypeRow>& types);