feat(dashboard): functions explorer + top-level tabs + pie chart fit

- Pie charts (Purity, Kind) ahora reciben plot_h explicito para que respeten
  el panel contenedor y no desborden cuando la ventana se reduce.
- Nueva pestana "Explorer": split layout con lista filtrable a la izquierda
  (search por nombre/descripcion + combos lang/domain) y panel de detalle
  a la derecha con tabs Code (read-only multiline), Documentation (selectable
  text), Tests (con dropdown si la funcion tiene varios tests, muestra lang +
  file_path + codigo) y Metadata (id, version, file_path, returns,
  uses_functions, uses_types, params_schema, example, notes).
- Reorganizacion de navegacion: TabBar movido arriba, justo debajo de la
  toolbar. Tabs: Dashboard | Explorer | Projects | Apps | Analysis | Types.
  Cada tab ocupa toda la zona de contenido. Dashboard incluye KPIs + charts
  + tabla de Recent Functions.
- Cache del Explorer (lista de funciones) invalidado en trigger_reload.
- Nuevas funciones HTTP: load_function_detail_http, load_all_functions_http,
  load_unit_tests_http (todas usan /api/databases/registry/query con escape
  defensivo de comilla simple en IDs).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-05 23:13:02 +02:00
parent 641723bdf1
commit ddf8e76ad6
5 changed files with 485 additions and 18 deletions
+124
View File
@@ -308,6 +308,130 @@ bool load_project_detail_http(const std::string& api_url,
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
// ---------------------------------------------------------------------------