feat: Settings submenu (Settings.../About...), git column, projects tab
- main.cpp: registrar info About via fn_ui::about_window_set_info - views.cpp: nueva columna "Git" en la tabla Apps (remote/local/-) - data.h/cpp + data_http.cpp: AppRow gana repo_url + dir_path - views.cpp: actions bar (Reindex / + Add / Reload / inbox) y modal Add - views.cpp: tab Projects con tree + detalle anidado - data_http.cpp: load_projects_http, load_project_detail_http, http_post_* - http_client.cpp: SO_RCVTIMEO en Windows como DWORD ms (timeout 5 ms bug) - CMakeLists: limpieza de srcs duplicados con fn_framework - app.md: notas operativas y estado actual Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+13
-5
@@ -1,4 +1,5 @@
|
|||||||
# SQLite3: use system library on Linux, vendored amalgamation on Windows cross-compile
|
# SQLite3: prefer the target created by the top-level CMakeLists. If running
|
||||||
|
# stand-alone (without the parent project), fall back to building the amalgamation.
|
||||||
find_package(SQLite3 QUIET)
|
find_package(SQLite3 QUIET)
|
||||||
if(NOT SQLite3_FOUND AND NOT TARGET sqlite3_vendored)
|
if(NOT SQLite3_FOUND AND NOT TARGET sqlite3_vendored)
|
||||||
# Build from amalgamation
|
# Build from amalgamation
|
||||||
@@ -10,7 +11,6 @@ if(NOT SQLite3_FOUND AND NOT TARGET sqlite3_vendored)
|
|||||||
SQLITE_ENABLE_FTS5
|
SQLITE_ENABLE_FTS5
|
||||||
SQLITE_ENABLE_JSON1
|
SQLITE_ENABLE_JSON1
|
||||||
)
|
)
|
||||||
# Alias so we can use the same target name
|
|
||||||
add_library(SQLite::SQLite3 ALIAS sqlite3_vendored)
|
add_library(SQLite::SQLite3 ALIAS sqlite3_vendored)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
@@ -27,13 +27,21 @@ add_imgui_app(registry_dashboard
|
|||||||
${CMAKE_SOURCE_DIR}/functions/viz/sparkline.cpp
|
${CMAKE_SOURCE_DIR}/functions/viz/sparkline.cpp
|
||||||
${CMAKE_SOURCE_DIR}/functions/core/dashboard_panel.cpp
|
${CMAKE_SOURCE_DIR}/functions/core/dashboard_panel.cpp
|
||||||
${CMAKE_SOURCE_DIR}/functions/core/dashboard_grid.cpp
|
${CMAKE_SOURCE_DIR}/functions/core/dashboard_grid.cpp
|
||||||
${CMAKE_SOURCE_DIR}/functions/core/fps_overlay.cpp
|
# fps_overlay y tokens viven en fn_framework
|
||||||
${CMAKE_SOURCE_DIR}/functions/core/fullscreen_window.cpp
|
${CMAKE_SOURCE_DIR}/functions/core/fullscreen_window.cpp
|
||||||
# Design tokens + primitives (fase 1 y 2 del plan del dashboard)
|
|
||||||
${CMAKE_SOURCE_DIR}/functions/core/tokens.cpp
|
|
||||||
${CMAKE_SOURCE_DIR}/functions/core/badge.cpp
|
${CMAKE_SOURCE_DIR}/functions/core/badge.cpp
|
||||||
${CMAKE_SOURCE_DIR}/functions/core/empty_state.cpp
|
${CMAKE_SOURCE_DIR}/functions/core/empty_state.cpp
|
||||||
${CMAKE_SOURCE_DIR}/functions/core/page_header.cpp
|
${CMAKE_SOURCE_DIR}/functions/core/page_header.cpp
|
||||||
|
# Fase 3: UI primitives (projects + reindex + add modals)
|
||||||
|
${CMAKE_SOURCE_DIR}/functions/core/button.cpp
|
||||||
|
${CMAKE_SOURCE_DIR}/functions/core/icon_button.cpp
|
||||||
|
${CMAKE_SOURCE_DIR}/functions/core/toolbar.cpp
|
||||||
|
${CMAKE_SOURCE_DIR}/functions/core/modal_dialog.cpp
|
||||||
|
${CMAKE_SOURCE_DIR}/functions/core/text_input.cpp
|
||||||
|
${CMAKE_SOURCE_DIR}/functions/core/select.cpp
|
||||||
|
${CMAKE_SOURCE_DIR}/functions/core/toast.cpp
|
||||||
|
${CMAKE_SOURCE_DIR}/functions/core/process_runner.cpp
|
||||||
|
${CMAKE_SOURCE_DIR}/functions/core/tree_view.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
target_include_directories(registry_dashboard PRIVATE
|
target_include_directories(registry_dashboard PRIVATE
|
||||||
|
|||||||
@@ -85,3 +85,43 @@ cd cpp && cmake -B build/windows -S . -DCMAKE_TOOLCHAIN_FILE=toolchains/mingw-w6
|
|||||||
- Por defecto intenta conectar a sqlite_api en `http://127.0.0.1:8484`. Si falla, usa SQLite directo.
|
- Por defecto intenta conectar a sqlite_api en `http://127.0.0.1:8484`. Si falla, usa SQLite directo.
|
||||||
- SQLite compilado estaticamente en Windows via amalgamation vendoreada. En Linux usa libsqlite3 del sistema.
|
- SQLite compilado estaticamente en Windows via amalgamation vendoreada. En Linux usa libsqlite3 del sistema.
|
||||||
- cpp-httplib usa sockets nativos (no OpenSSL) — solo HTTP, no HTTPS.
|
- cpp-httplib usa sockets nativos (no OpenSSL) — solo HTTP, no HTTPS.
|
||||||
|
|
||||||
|
## Estado actual
|
||||||
|
|
||||||
|
### Fase — actions bar + projects tab + Add modal `[done 2026-04-25]`
|
||||||
|
|
||||||
|
Cambios estructurales (requieren `sqlite_api` v0.2 con endpoints de mutacion):
|
||||||
|
|
||||||
|
- **Actions bar** en el page header (`fn_ui::toolbar`): `Reindex` (Primary) → `http_post_reindex` via `process_runner`; `+ Add` (Secondary) → abre modal; `Reload` (Subtle) → re-fetch via `UserData` flag; `toast_inbox_button` con badge.
|
||||||
|
- **Modal Add** (`modal_dialog`): `select` para Kind (App/Analysis/Vault), `select` de proyecto (obligatorio para Vault), `text_input` Name + Description + campos especificos. `process_runner` para el POST. Toast de exito/error + reload al completar.
|
||||||
|
- **Tab Projects** (`tree_view` + tabs): columna izquierda con proyectos + entrada "(orphans)"; columna derecha con detalle nested (Apps/Analysis/Vaults). Click dispara `load_project_detail_http`.
|
||||||
|
|
||||||
|
`RegistryData` gana `projects[]`, `orphan_apps`, `orphan_analyses`, `orphan_vaults`. Tipos nuevos `ProjectRow`, `VaultRow`, `ProjectDetail`. Layer `data_http.cpp` gana `load_projects_http`, `load_project_detail_http`, `http_post_{reindex,add_app,add_analysis,add_vault}`.
|
||||||
|
|
||||||
|
`AnalysisRow` gana campo `lang` para coherencia con la tabla `analysis` (la query SQL pasa de `SELECT id,name,domain,description` a `SELECT id,name,lang,domain,description`).
|
||||||
|
|
||||||
|
`views_set_api_url(url)` invocado desde `main.cpp` para que las vistas puedan disparar mutaciones.
|
||||||
|
|
||||||
|
### Bug fixes operativos `[done 2026-04-25]`
|
||||||
|
|
||||||
|
- **Vibracion al redimensionar**: `fullscreen_window` v0.2 (NoScrollbar), altura de charts fija 260 px, `kpi_card` v1.2 (78 px + scale 1.4 + NoScrollbar). Ver `project.md` para detalle.
|
||||||
|
- **HTTP POST timeout 5 ms en Windows**: `http_client.cpp::request()` usaba `struct timeval` en `setsockopt(SO_RCVTIMEO)`, que Windows interpreta como `DWORD` ms → 5 ms efectivos. Fix: rama `_WIN32` con `DWORD timeout_ms = timeout_sec * 1000`. `wsa_init` envuelto en `std::call_once`.
|
||||||
|
- **Mensajes de toast vacios**: `post_json` ahora siempre escribe en `out_body` (extrae `output` del JSON en exito; sintetiza `"connect() failed to host:port (err=N)"` con codigo Winsock en error de conexion). Em dash sustituido por ASCII `:` para evitar render como `?` en fuentes sin ese codepoint.
|
||||||
|
- **Inbox popup en otra pantalla**: `toast_inbox_button` antes usaba posicion calculada con `btn_pos.x - 332` que podia caer fuera del `WorkRect` del viewport principal; con `viewports = true` ImGui lo movia a otra ventana del OS. Fix: clamp al `WorkRect`, anclar con `ImGuiCond_Appearing` (no `Always`), `SetNextWindowViewport(vp->ID)`.
|
||||||
|
|
||||||
|
### Toolchain MinGW para Windows `[importante]`
|
||||||
|
|
||||||
|
El cross-compile pasa de thread model `win32` a `posix` (`x86_64-w64-mingw32-g++-posix`) para que `std::mutex`/`std::thread` funcionen — necesario para `process_runner` y `toast`. Linker: `-static-libgcc -static-libstdc++ -static -lwinpthread`. Configurado en `cpp/toolchains/mingw-w64.cmake`.
|
||||||
|
|
||||||
|
### Lo siguiente que pega
|
||||||
|
|
||||||
|
- Filtros del Roadmap: el `select` de proyecto del modal Add ya prueba que filtrar por proyecto es trivial; aplicar mismo patron a las tablas Apps/Analysis/Types.
|
||||||
|
- Detalles al click: cuando un row de la tabla Apps (o Functions) se selecciona, abrir un panel lateral con metadata + boton "Open in editor" (segun OS, `xdg-open`/`explorer`).
|
||||||
|
- Integracion FTS5: el endpoint `/api/databases/registry/fts` ya existe; falta cablearlo desde la actions bar como `text_input` con resultados live.
|
||||||
|
|
||||||
|
## Notas — Settings menubar (sesion 2026-04-25)
|
||||||
|
|
||||||
|
- `render()` ahora llama `fn_ui::app_menubar(nullptr, 0, nullptr)` al inicio para exponer el item `Settings...` en la MainMenuBar. La app no tiene paneles toggleables ni layouts propios, asi que solo aparece Settings.
|
||||||
|
- El usuario puede cambiar fuente (DroidSans/Karla/Roboto/Cousine) y tamaño (10..32 px) en runtime, y togglear el FPS overlay. Persistencia en `app_settings.ini` junto al `registry_dashboard.exe`.
|
||||||
|
- `CMakeLists.txt` limpiado: `fps_overlay.cpp` y `tokens.cpp` ya viven en `fn_framework` — no listarlos explicitamente o el linker da multiple-definition.
|
||||||
|
- 5 TTFs (Karla/Roboto/DroidSans/Cousine/Tabler) copiadas junto al exe via `add_imgui_app` post-build.
|
||||||
|
|||||||
@@ -192,7 +192,7 @@ bool load_registry_data(const char* db_path, RegistryData& out) {
|
|||||||
// --- Apps ---
|
// --- Apps ---
|
||||||
out.apps.clear();
|
out.apps.clear();
|
||||||
query(db,
|
query(db,
|
||||||
"SELECT id, name, lang, domain, description, framework FROM apps ORDER BY name",
|
"SELECT id, name, lang, domain, description, framework, repo_url, dir_path FROM apps ORDER BY name",
|
||||||
[&](sqlite3_stmt* s) {
|
[&](sqlite3_stmt* s) {
|
||||||
AppRow r;
|
AppRow r;
|
||||||
r.id = col_str(s, 0);
|
r.id = col_str(s, 0);
|
||||||
@@ -201,19 +201,22 @@ bool load_registry_data(const char* db_path, RegistryData& out) {
|
|||||||
r.domain = col_str(s, 3);
|
r.domain = col_str(s, 3);
|
||||||
r.description = col_str(s, 4);
|
r.description = col_str(s, 4);
|
||||||
r.framework = col_str(s, 5);
|
r.framework = col_str(s, 5);
|
||||||
|
r.repo_url = col_str(s, 6);
|
||||||
|
r.dir_path = col_str(s, 7);
|
||||||
out.apps.push_back(std::move(r));
|
out.apps.push_back(std::move(r));
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- Analysis ---
|
// --- Analysis ---
|
||||||
out.analyses.clear();
|
out.analyses.clear();
|
||||||
query(db,
|
query(db,
|
||||||
"SELECT id, name, domain, description FROM analysis ORDER BY name",
|
"SELECT id, name, lang, domain, description FROM analysis ORDER BY name",
|
||||||
[&](sqlite3_stmt* s) {
|
[&](sqlite3_stmt* s) {
|
||||||
AnalysisRow r;
|
AnalysisRow r;
|
||||||
r.id = col_str(s, 0);
|
r.id = col_str(s, 0);
|
||||||
r.name = col_str(s, 1);
|
r.name = col_str(s, 1);
|
||||||
r.domain = col_str(s, 2);
|
r.lang = col_str(s, 2);
|
||||||
r.description = col_str(s, 3);
|
r.domain = col_str(s, 3);
|
||||||
|
r.description = col_str(s, 4);
|
||||||
out.analyses.push_back(std::move(r));
|
out.analyses.push_back(std::move(r));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -55,11 +55,14 @@ struct AppRow {
|
|||||||
std::string domain;
|
std::string domain;
|
||||||
std::string description;
|
std::string description;
|
||||||
std::string framework;
|
std::string framework;
|
||||||
|
std::string repo_url;
|
||||||
|
std::string dir_path;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct AnalysisRow {
|
struct AnalysisRow {
|
||||||
std::string id;
|
std::string id;
|
||||||
std::string name;
|
std::string name;
|
||||||
|
std::string lang;
|
||||||
std::string domain;
|
std::string domain;
|
||||||
std::string description;
|
std::string description;
|
||||||
};
|
};
|
||||||
@@ -73,6 +76,32 @@ struct TypeRow {
|
|||||||
std::string description;
|
std::string description;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct VaultRow {
|
||||||
|
std::string id;
|
||||||
|
std::string name;
|
||||||
|
std::string path;
|
||||||
|
std::string description;
|
||||||
|
bool symlink = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ProjectRow {
|
||||||
|
std::string id;
|
||||||
|
std::string name;
|
||||||
|
std::string description;
|
||||||
|
int apps_count = 0;
|
||||||
|
int analyses_count = 0;
|
||||||
|
int vaults_count = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ProjectDetail {
|
||||||
|
std::string id; // "" si no hay seleccion; "orphans" para huerfanas
|
||||||
|
std::string name;
|
||||||
|
std::string description;
|
||||||
|
std::vector<AppRow> apps;
|
||||||
|
std::vector<AnalysisRow> analyses;
|
||||||
|
std::vector<VaultRow> vaults;
|
||||||
|
};
|
||||||
|
|
||||||
// All data loaded from registry.db in one shot
|
// All data loaded from registry.db in one shot
|
||||||
struct RegistryData {
|
struct RegistryData {
|
||||||
RegistryStats stats;
|
RegistryStats stats;
|
||||||
@@ -84,6 +113,10 @@ struct RegistryData {
|
|||||||
std::vector<AppRow> apps;
|
std::vector<AppRow> apps;
|
||||||
std::vector<AnalysisRow> analyses;
|
std::vector<AnalysisRow> analyses;
|
||||||
std::vector<TypeRow> types;
|
std::vector<TypeRow> types;
|
||||||
|
std::vector<ProjectRow> projects;
|
||||||
|
int orphan_apps = 0;
|
||||||
|
int orphan_analyses = 0;
|
||||||
|
int orphan_vaults = 0;
|
||||||
|
|
||||||
// For chart data (populated by prepare_chart_data)
|
// For chart data (populated by prepare_chart_data)
|
||||||
std::vector<std::string> lang_labels;
|
std::vector<std::string> lang_labels;
|
||||||
|
|||||||
+241
-3
@@ -135,25 +135,27 @@ bool load_registry_data_http(const std::string& api_url, RegistryData& out) {
|
|||||||
|
|
||||||
// --- Apps ---
|
// --- Apps ---
|
||||||
out.apps.clear();
|
out.apps.clear();
|
||||||
j = api_query(cli, "SELECT id, name, lang, domain, description, framework FROM apps ORDER BY name");
|
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")) {
|
if (!j.is_null() && j.contains("rows")) {
|
||||||
for (auto& row : j["rows"]) {
|
for (auto& row : j["rows"]) {
|
||||||
AppRow r;
|
AppRow r;
|
||||||
r.id = extract_str(row, 0); r.name = extract_str(row, 1);
|
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.lang = extract_str(row, 2); r.domain = extract_str(row, 3);
|
||||||
r.description = extract_str(row, 4); r.framework = extract_str(row, 5);
|
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));
|
out.apps.push_back(std::move(r));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Analysis ---
|
// --- Analysis ---
|
||||||
out.analyses.clear();
|
out.analyses.clear();
|
||||||
j = api_query(cli, "SELECT id, name, domain, description FROM analysis ORDER BY name");
|
j = api_query(cli, "SELECT id, name, lang, domain, description FROM analysis ORDER BY name");
|
||||||
if (!j.is_null() && j.contains("rows")) {
|
if (!j.is_null() && j.contains("rows")) {
|
||||||
for (auto& row : j["rows"]) {
|
for (auto& row : j["rows"]) {
|
||||||
AnalysisRow r;
|
AnalysisRow r;
|
||||||
r.id = extract_str(row, 0); r.name = extract_str(row, 1);
|
r.id = extract_str(row, 0); r.name = extract_str(row, 1);
|
||||||
r.domain = extract_str(row, 2); r.description = extract_str(row, 3);
|
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));
|
out.analyses.push_back(std::move(r));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -172,5 +174,241 @@ bool load_registry_data_http(const std::string& api_url, RegistryData& out) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
out.prepare_chart_data();
|
out.prepare_chart_data();
|
||||||
|
|
||||||
|
// Best-effort: projects (no fatal si falla)
|
||||||
|
load_projects_http(api_url, out);
|
||||||
|
|
||||||
return true;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
|||||||
+28
@@ -7,3 +7,31 @@
|
|||||||
// api_url should be like "http://127.0.0.1:8484".
|
// api_url should be like "http://127.0.0.1:8484".
|
||||||
// Returns true on success.
|
// Returns true on success.
|
||||||
bool load_registry_data_http(const std::string& api_url, RegistryData& out);
|
bool load_registry_data_http(const std::string& api_url, RegistryData& out);
|
||||||
|
|
||||||
|
// Load projects list (con conteos) y huerfanas. No destruye el resto de
|
||||||
|
// campos de out, solo setea projects + orphan_*.
|
||||||
|
bool load_projects_http(const std::string& api_url, RegistryData& out);
|
||||||
|
|
||||||
|
// Load detalle de un proyecto (apps/analyses/vaults). Si id=="orphans"
|
||||||
|
// devuelve las entidades sin project_id.
|
||||||
|
bool load_project_detail_http(const std::string& api_url,
|
||||||
|
const std::string& id,
|
||||||
|
ProjectDetail& out);
|
||||||
|
|
||||||
|
// Operaciones de mutacion (thread-safe porque http_post ya lo es).
|
||||||
|
// Devuelven el body de respuesta en `out_body`. true si HTTP status 2xx.
|
||||||
|
bool http_post_reindex(const std::string& api_url, std::string& 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);
|
||||||
|
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);
|
||||||
|
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);
|
||||||
|
|||||||
+24
-6
@@ -8,15 +8,19 @@
|
|||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
#include <winsock2.h>
|
#include <winsock2.h>
|
||||||
#include <ws2tcpip.h>
|
#include <ws2tcpip.h>
|
||||||
|
#include <mutex>
|
||||||
#pragma comment(lib, "ws2_32.lib")
|
#pragma comment(lib, "ws2_32.lib")
|
||||||
|
|
||||||
|
// std::call_once para evitar race condition si hay peticiones simultaneas
|
||||||
|
// desde multiples threads (main + runners).
|
||||||
|
static std::once_flag g_wsa_once;
|
||||||
|
static bool g_wsa_ok = false;
|
||||||
static bool wsa_init() {
|
static bool wsa_init() {
|
||||||
static bool done = false;
|
std::call_once(g_wsa_once, []() {
|
||||||
if (!done) {
|
|
||||||
WSADATA wsa;
|
WSADATA wsa;
|
||||||
done = (WSAStartup(MAKEWORD(2, 2), &wsa) == 0);
|
g_wsa_ok = (WSAStartup(MAKEWORD(2, 2), &wsa) == 0);
|
||||||
}
|
});
|
||||||
return done;
|
return g_wsa_ok;
|
||||||
}
|
}
|
||||||
typedef SOCKET sock_t;
|
typedef SOCKET sock_t;
|
||||||
#define SOCK_INVALID INVALID_SOCKET
|
#define SOCK_INVALID INVALID_SOCKET
|
||||||
@@ -64,12 +68,19 @@ HttpResponse HttpClient::request(const std::string& method, const std::string& p
|
|||||||
return resp;
|
return resp;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set timeout
|
// Timeout — Windows y POSIX usan formatos distintos para SO_{RCV,SND}TIMEO.
|
||||||
|
// Windows: DWORD milisegundos. POSIX: struct timeval.
|
||||||
|
#ifdef _WIN32
|
||||||
|
DWORD timeout_ms = static_cast<DWORD>(timeout_sec_ * 1000);
|
||||||
|
setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, (const char*)&timeout_ms, sizeof(timeout_ms));
|
||||||
|
setsockopt(sock, SOL_SOCKET, SO_SNDTIMEO, (const char*)&timeout_ms, sizeof(timeout_ms));
|
||||||
|
#else
|
||||||
struct timeval tv;
|
struct timeval tv;
|
||||||
tv.tv_sec = timeout_sec_;
|
tv.tv_sec = timeout_sec_;
|
||||||
tv.tv_usec = 0;
|
tv.tv_usec = 0;
|
||||||
setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, (const char*)&tv, sizeof(tv));
|
setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, (const char*)&tv, sizeof(tv));
|
||||||
setsockopt(sock, SOL_SOCKET, SO_SNDTIMEO, (const char*)&tv, sizeof(tv));
|
setsockopt(sock, SOL_SOCKET, SO_SNDTIMEO, (const char*)&tv, sizeof(tv));
|
||||||
|
#endif
|
||||||
|
|
||||||
// Connect
|
// Connect
|
||||||
struct sockaddr_in addr;
|
struct sockaddr_in addr;
|
||||||
@@ -79,7 +90,14 @@ HttpResponse HttpClient::request(const std::string& method, const std::string& p
|
|||||||
addr.sin_addr.s_addr = inet_addr(host_.c_str());
|
addr.sin_addr.s_addr = inet_addr(host_.c_str());
|
||||||
|
|
||||||
if (connect(sock, (struct sockaddr*)&addr, sizeof(addr)) != 0) {
|
if (connect(sock, (struct sockaddr*)&addr, sizeof(addr)) != 0) {
|
||||||
|
int err = SOCK_ERR;
|
||||||
SOCK_CLOSE(sock);
|
SOCK_CLOSE(sock);
|
||||||
|
// Reportamos el errno/WSAError en el body para que el toast sea util.
|
||||||
|
char buf[128];
|
||||||
|
std::snprintf(buf, sizeof(buf),
|
||||||
|
"connect() failed to %s:%d (err=%d)",
|
||||||
|
host_.c_str(), port_, err);
|
||||||
|
resp.body = buf;
|
||||||
return resp;
|
return resp;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
#include "app_base.h"
|
#include "app_base.h"
|
||||||
#include "imgui.h"
|
#include "imgui.h"
|
||||||
#include "core/fullscreen_window.h"
|
#include "core/fullscreen_window.h"
|
||||||
|
#include "core/app_menubar.h"
|
||||||
|
#include "core/app_about.h"
|
||||||
#include "data.h"
|
#include "data.h"
|
||||||
#include "data_http.h"
|
#include "data_http.h"
|
||||||
#include "views.h"
|
#include "views.h"
|
||||||
@@ -41,6 +43,9 @@ static void reload_data() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static void render() {
|
static void render() {
|
||||||
|
// MainMenuBar (solo Settings — el dashboard no expone paneles toggleables)
|
||||||
|
fn_ui::app_menubar(nullptr, 0, nullptr);
|
||||||
|
|
||||||
if (ImGui::GetIO().UserData != nullptr) {
|
if (ImGui::GetIO().UserData != nullptr) {
|
||||||
ImGui::GetIO().UserData = nullptr;
|
ImGui::GetIO().UserData = nullptr;
|
||||||
reload_data();
|
reload_data();
|
||||||
@@ -107,6 +112,18 @@ int main(int argc, char** argv) {
|
|||||||
if (g_db_path.empty()) g_db_path = db_candidates.back();
|
if (g_db_path.empty()) g_db_path = db_candidates.back();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Compartir el API URL con las vistas (para reindex/add desde la toolbar)
|
||||||
|
views_set_api_url(g_api_url);
|
||||||
|
|
||||||
|
// Info de la ventana About (submenu Settings → About...)
|
||||||
|
fn_ui::about_window_set_info(
|
||||||
|
"fn_registry Dashboard",
|
||||||
|
"0.2.0",
|
||||||
|
"Dashboard ImGui para visualizar el estado del fn_registry. "
|
||||||
|
"Consume datos via sqlite_api HTTP (fallback a SQLite directo). "
|
||||||
|
"KPIs, charts, tablas, desglose por lenguaje/dominio/pureza."
|
||||||
|
);
|
||||||
|
|
||||||
reload_data();
|
reload_data();
|
||||||
|
|
||||||
return fn::run_app(
|
return fn::run_app(
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
#include "views.h"
|
#include "views.h"
|
||||||
|
#include "data_http.h"
|
||||||
|
#include <filesystem>
|
||||||
#include "imgui.h"
|
#include "imgui.h"
|
||||||
#include "implot.h"
|
#include "implot.h"
|
||||||
|
|
||||||
@@ -15,10 +17,42 @@
|
|||||||
#include "core/page_header.h"
|
#include "core/page_header.h"
|
||||||
#include "core/empty_state.h"
|
#include "core/empty_state.h"
|
||||||
#include "core/badge.h"
|
#include "core/badge.h"
|
||||||
|
#include "core/button.h"
|
||||||
|
#include "core/icon_button.h"
|
||||||
|
#include "core/toolbar.h"
|
||||||
|
#include "core/modal_dialog.h"
|
||||||
|
#include "core/text_input.h"
|
||||||
|
#include "core/select.h"
|
||||||
|
#include "core/toast.h"
|
||||||
|
#include "core/process_runner.h"
|
||||||
|
#include "core/tree_view.h"
|
||||||
|
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Shared state
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
static std::string g_api_url;
|
||||||
|
static fn_ui::ProcessRunner g_reindex_runner;
|
||||||
|
static fn_ui::ProcessRunner g_add_runner;
|
||||||
|
|
||||||
|
// Add modal state
|
||||||
|
enum class AddKind : int { App = 0, Analysis = 1, Vault = 2 };
|
||||||
|
static bool g_show_add = false;
|
||||||
|
static int g_add_kind_idx = static_cast<int>(AddKind::App);
|
||||||
|
static int g_add_project_idx = -1; // -1 = (none / sin project)
|
||||||
|
static int g_add_lang_idx = 0;
|
||||||
|
static int g_add_domain_idx = 0;
|
||||||
|
static char g_add_name[128] = {};
|
||||||
|
static char g_add_desc[256] = {};
|
||||||
|
static char g_add_packages[256] = {};
|
||||||
|
static char g_add_vault_path[512] = {};
|
||||||
|
|
||||||
|
void views_set_api_url(const std::string& url) { g_api_url = url; }
|
||||||
|
|
||||||
static std::vector<const char*> to_cstr(const std::vector<std::string>& v) {
|
static std::vector<const char*> to_cstr(const std::vector<std::string>& v) {
|
||||||
std::vector<const char*> out;
|
std::vector<const char*> out;
|
||||||
out.reserve(v.size());
|
out.reserve(v.size());
|
||||||
@@ -26,15 +60,20 @@ static std::vector<const char*> to_cstr(const std::vector<std::string>& v) {
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void trigger_reload() {
|
||||||
|
ImGui::GetIO().UserData = reinterpret_cast<void*>(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// KPI row
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
void draw_kpi_row(const RegistryStats& stats) {
|
void draw_kpi_row(const RegistryStats& stats) {
|
||||||
float tested_pct = stats.total_functions > 0
|
float tested_pct = stats.total_functions > 0
|
||||||
? 100.0f * stats.tested_functions / stats.total_functions : 0.0f;
|
? 100.0f * stats.tested_functions / stats.total_functions : 0.0f;
|
||||||
float pure_pct = stats.total_functions > 0
|
float pure_pct = stats.total_functions > 0
|
||||||
? 100.0f * stats.pure_functions / stats.total_functions : 0.0f;
|
? 100.0f * stats.pure_functions / stats.total_functions : 0.0f;
|
||||||
|
|
||||||
// ImGui::BeginTable da celdas con ancho constrained, algo que BeginGroup
|
|
||||||
// (dashboard_grid) no hace — necesario para que el BeginChild dentro de
|
|
||||||
// kpi_card ocupe exactamente la celda y no desborde.
|
|
||||||
const ImGuiTableFlags flags = ImGuiTableFlags_SizingStretchSame
|
const ImGuiTableFlags flags = ImGuiTableFlags_SizingStretchSame
|
||||||
| ImGuiTableFlags_NoPadOuterX;
|
| ImGuiTableFlags_NoPadOuterX;
|
||||||
|
|
||||||
@@ -59,10 +98,10 @@ void draw_kpi_row(const RegistryStats& stats) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Chart panel con tamano FIJO (no AutoResizeY) para evitar el feedback loop
|
// ---------------------------------------------------------------------------
|
||||||
// con ImPlot que provocaba deslizamiento lateral de las barras y scrollbar
|
// Charts
|
||||||
// intermitente. Usa los mismos tokens que dashboard_panel para consistencia
|
// ---------------------------------------------------------------------------
|
||||||
// visual, pero con tamano determinista.
|
|
||||||
static bool chart_panel_begin(const char* title, const ImVec2& size) {
|
static bool chart_panel_begin(const char* title, const ImVec2& size) {
|
||||||
using namespace fn_tokens;
|
using namespace fn_tokens;
|
||||||
ImGui::PushStyleColor(ImGuiCol_ChildBg, colors::surface);
|
ImGui::PushStyleColor(ImGuiCol_ChildBg, colors::surface);
|
||||||
@@ -88,13 +127,8 @@ static void chart_panel_end() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void draw_charts(RegistryData& data, float height) {
|
void draw_charts(RegistryData& data, float height) {
|
||||||
// ImGui::BeginTable para reparto equitativo de ancho en 4 columnas y que
|
|
||||||
// cada chart_panel tenga ancho constrained via GetContentRegionAvail.
|
|
||||||
const ImGuiTableFlags flags = ImGuiTableFlags_SizingStretchSame
|
const ImGuiTableFlags flags = ImGuiTableFlags_SizingStretchSame
|
||||||
| ImGuiTableFlags_NoPadOuterX;
|
| ImGuiTableFlags_NoPadOuterX;
|
||||||
|
|
||||||
// Altura util dentro del panel (height total - title row - separator - padding).
|
|
||||||
// El plot recibe exactamente esto, asi que no hay redimensionado recursivo.
|
|
||||||
const float plot_h = height - 48.0f;
|
const float plot_h = height - 48.0f;
|
||||||
|
|
||||||
if (ImGui::BeginTable("##chart_grid", 4, flags)) {
|
if (ImGui::BeginTable("##chart_grid", 4, flags)) {
|
||||||
@@ -110,7 +144,6 @@ void draw_charts(RegistryData& data, float height) {
|
|||||||
static_cast<int>(labels.size()), 0.67f, plot_h);
|
static_cast<int>(labels.size()), 0.67f, plot_h);
|
||||||
chart_panel_end();
|
chart_panel_end();
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui::TableSetColumnIndex(1);
|
ImGui::TableSetColumnIndex(1);
|
||||||
{
|
{
|
||||||
ImVec2 sz(ImGui::GetContentRegionAvail().x, height);
|
ImVec2 sz(ImGui::GetContentRegionAvail().x, height);
|
||||||
@@ -121,7 +154,6 @@ void draw_charts(RegistryData& data, float height) {
|
|||||||
static_cast<int>(labels.size()), 0.67f, plot_h);
|
static_cast<int>(labels.size()), 0.67f, plot_h);
|
||||||
chart_panel_end();
|
chart_panel_end();
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui::TableSetColumnIndex(2);
|
ImGui::TableSetColumnIndex(2);
|
||||||
{
|
{
|
||||||
ImVec2 sz(ImGui::GetContentRegionAvail().x, height);
|
ImVec2 sz(ImGui::GetContentRegionAvail().x, height);
|
||||||
@@ -132,7 +164,6 @@ void draw_charts(RegistryData& data, float height) {
|
|||||||
pie_chart("##purity", labels, values, 2);
|
pie_chart("##purity", labels, values, 2);
|
||||||
chart_panel_end();
|
chart_panel_end();
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui::TableSetColumnIndex(3);
|
ImGui::TableSetColumnIndex(3);
|
||||||
{
|
{
|
||||||
ImVec2 sz(ImGui::GetContentRegionAvail().x, height);
|
ImVec2 sz(ImGui::GetContentRegionAvail().x, height);
|
||||||
@@ -143,11 +174,14 @@ void draw_charts(RegistryData& data, float height) {
|
|||||||
static_cast<int>(labels.size()));
|
static_cast<int>(labels.size()));
|
||||||
chart_panel_end();
|
chart_panel_end();
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui::EndTable();
|
ImGui::EndTable();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Tables
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
void draw_recent_functions(const std::vector<FunctionRow>& funcs) {
|
void draw_recent_functions(const std::vector<FunctionRow>& funcs) {
|
||||||
if (funcs.empty()) {
|
if (funcs.empty()) {
|
||||||
empty_state("( no data )", "No functions yet",
|
empty_state("( no data )", "No functions yet",
|
||||||
@@ -174,11 +208,11 @@ void draw_recent_functions(const std::vector<FunctionRow>& funcs) {
|
|||||||
void draw_apps_list(const std::vector<AppRow>& apps) {
|
void draw_apps_list(const std::vector<AppRow>& apps) {
|
||||||
if (apps.empty()) {
|
if (apps.empty()) {
|
||||||
empty_state("( no data )", "No apps registered",
|
empty_state("( no data )", "No apps registered",
|
||||||
"Clone apps with 'fn app clone <id>' or run 'fn sync'");
|
"Use the + Add button above or run 'fn sync'");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const char* headers[] = {"Name", "Lang", "Domain", "Framework", "Description"};
|
const char* headers[] = {"Name", "Lang", "Domain", "Framework", "Git", "Description"};
|
||||||
constexpr int cols = 5;
|
constexpr int cols = 6;
|
||||||
std::vector<std::string> cell_strings;
|
std::vector<std::string> cell_strings;
|
||||||
cell_strings.reserve(apps.size() * cols);
|
cell_strings.reserve(apps.size() * cols);
|
||||||
for (auto& a : apps) {
|
for (auto& a : apps) {
|
||||||
@@ -186,6 +220,24 @@ void draw_apps_list(const std::vector<AppRow>& apps) {
|
|||||||
cell_strings.push_back(a.lang);
|
cell_strings.push_back(a.lang);
|
||||||
cell_strings.push_back(a.domain);
|
cell_strings.push_back(a.domain);
|
||||||
cell_strings.push_back(a.framework);
|
cell_strings.push_back(a.framework);
|
||||||
|
// Indicador de git: tiene repo remoto (gitea), solo local, o ninguno.
|
||||||
|
// - "remote": repo_url poblado en el frontmatter del app.md
|
||||||
|
// - "local": hay .git/ en dir_path pero sin repo_url
|
||||||
|
// - "-": ni .git ni repo_url
|
||||||
|
std::string git_status;
|
||||||
|
if (!a.repo_url.empty()) {
|
||||||
|
git_status = "remote";
|
||||||
|
} else if (!a.dir_path.empty()) {
|
||||||
|
std::error_code ec;
|
||||||
|
if (std::filesystem::exists(std::filesystem::path(a.dir_path) / ".git", ec)) {
|
||||||
|
git_status = "local";
|
||||||
|
} else {
|
||||||
|
git_status = "-";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
git_status = "-";
|
||||||
|
}
|
||||||
|
cell_strings.push_back(git_status);
|
||||||
cell_strings.push_back(a.description);
|
cell_strings.push_back(a.description);
|
||||||
}
|
}
|
||||||
auto cells = to_cstr(cell_strings);
|
auto cells = to_cstr(cell_strings);
|
||||||
@@ -195,15 +247,16 @@ void draw_apps_list(const std::vector<AppRow>& apps) {
|
|||||||
void draw_analysis_list(const std::vector<AnalysisRow>& analyses) {
|
void draw_analysis_list(const std::vector<AnalysisRow>& analyses) {
|
||||||
if (analyses.empty()) {
|
if (analyses.empty()) {
|
||||||
empty_state("( no data )", "No analysis yet",
|
empty_state("( no data )", "No analysis yet",
|
||||||
"Create one with 'fn run init_jupyter_analysis <name>'");
|
"Use the + Add button above with kind = Analysis");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const char* headers[] = {"Name", "Domain", "Description"};
|
const char* headers[] = {"Name", "Lang", "Domain", "Description"};
|
||||||
constexpr int cols = 3;
|
constexpr int cols = 4;
|
||||||
std::vector<std::string> cell_strings;
|
std::vector<std::string> cell_strings;
|
||||||
cell_strings.reserve(analyses.size() * cols);
|
cell_strings.reserve(analyses.size() * cols);
|
||||||
for (auto& a : analyses) {
|
for (auto& a : analyses) {
|
||||||
cell_strings.push_back(a.name);
|
cell_strings.push_back(a.name);
|
||||||
|
cell_strings.push_back(a.lang);
|
||||||
cell_strings.push_back(a.domain);
|
cell_strings.push_back(a.domain);
|
||||||
cell_strings.push_back(a.description);
|
cell_strings.push_back(a.description);
|
||||||
}
|
}
|
||||||
@@ -232,45 +285,337 @@ void draw_types_list(const std::vector<TypeRow>& types) {
|
|||||||
table_view("##types", headers, cols, cells.data(), static_cast<int>(types.size()));
|
table_view("##types", headers, cols, cells.data(), static_cast<int>(types.size()));
|
||||||
}
|
}
|
||||||
|
|
||||||
void draw_dashboard(RegistryData& data) {
|
// ---------------------------------------------------------------------------
|
||||||
// Aplicar tema una sola vez por vida de la app.
|
// Projects view
|
||||||
static bool theme_applied = false;
|
// ---------------------------------------------------------------------------
|
||||||
if (!theme_applied) {
|
|
||||||
fn_tokens::apply_dark_theme();
|
static std::string g_selected_project_id = "";
|
||||||
theme_applied = true;
|
static ProjectDetail g_project_detail;
|
||||||
|
|
||||||
|
static void refresh_project_detail() {
|
||||||
|
g_project_detail = ProjectDetail{};
|
||||||
|
if (g_selected_project_id.empty() || g_api_url.empty()) return;
|
||||||
|
load_project_detail_http(g_api_url, g_selected_project_id, g_project_detail);
|
||||||
|
}
|
||||||
|
|
||||||
|
void draw_projects_list(RegistryData& data) {
|
||||||
|
if (data.projects.empty() && data.orphan_apps == 0
|
||||||
|
&& data.orphan_analyses == 0 && data.orphan_vaults == 0) {
|
||||||
|
empty_state("( no data )", "No projects yet",
|
||||||
|
"Create a project under projects/{name}/ with project.md and reindex");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Dos columnas: izquierda arbol, derecha detalle.
|
||||||
|
const ImGuiTableFlags flags = ImGuiTableFlags_Resizable
|
||||||
|
| ImGuiTableFlags_SizingStretchProp;
|
||||||
|
if (!ImGui::BeginTable("##proj_layout", 2, flags)) return;
|
||||||
|
|
||||||
|
ImGui::TableSetupColumn("tree", ImGuiTableColumnFlags_WidthStretch, 1.0f);
|
||||||
|
ImGui::TableSetupColumn("detail", ImGuiTableColumnFlags_WidthStretch, 2.5f);
|
||||||
|
ImGui::TableNextRow();
|
||||||
|
|
||||||
|
// --- Left column: tree ---
|
||||||
|
ImGui::TableSetColumnIndex(0);
|
||||||
|
ImGui::BeginChild("##proj_tree", ImVec2(0, 0), ImGuiChildFlags_Borders);
|
||||||
|
for (const auto& p : data.projects) {
|
||||||
|
bool sel = (g_selected_project_id == p.id);
|
||||||
|
char label[256];
|
||||||
|
std::snprintf(label, sizeof(label), "%s [%d/%d/%d]",
|
||||||
|
p.name.c_str(), p.apps_count, p.analyses_count, p.vaults_count);
|
||||||
|
fn_ui::tree_leaf(p.id.c_str(), label, sel);
|
||||||
|
if (fn_ui::tree_node_clicked()) {
|
||||||
|
g_selected_project_id = p.id;
|
||||||
|
refresh_project_detail();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Orphans
|
||||||
|
if (data.orphan_apps + data.orphan_analyses + data.orphan_vaults > 0) {
|
||||||
|
bool sel = (g_selected_project_id == "orphans");
|
||||||
|
char label[128];
|
||||||
|
std::snprintf(label, sizeof(label), "(orphans) [%d/%d/%d]",
|
||||||
|
data.orphan_apps, data.orphan_analyses, data.orphan_vaults);
|
||||||
|
fn_ui::tree_leaf("orphans", label, sel);
|
||||||
|
if (fn_ui::tree_node_clicked()) {
|
||||||
|
g_selected_project_id = "orphans";
|
||||||
|
refresh_project_detail();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImGui::EndChild();
|
||||||
|
|
||||||
|
// --- Right column: detail ---
|
||||||
|
ImGui::TableSetColumnIndex(1);
|
||||||
|
ImGui::BeginChild("##proj_detail", ImVec2(0, 0), ImGuiChildFlags_Borders);
|
||||||
|
if (g_selected_project_id.empty()) {
|
||||||
|
empty_state("\xe2\x86\x90", "Select a project",
|
||||||
|
"Click a project on the left to see its apps, analyses and vaults");
|
||||||
|
} else {
|
||||||
|
const auto& d = g_project_detail;
|
||||||
|
ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::text);
|
||||||
|
if (!d.name.empty())
|
||||||
|
ImGui::TextUnformatted(d.name.c_str());
|
||||||
|
else
|
||||||
|
ImGui::TextUnformatted(g_selected_project_id.c_str());
|
||||||
|
ImGui::PopStyleColor();
|
||||||
|
if (!d.description.empty()) {
|
||||||
|
ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::text_muted);
|
||||||
|
ImGui::TextWrapped("%s", d.description.c_str());
|
||||||
|
ImGui::PopStyleColor();
|
||||||
|
}
|
||||||
|
ImGui::Separator();
|
||||||
|
|
||||||
|
if (ImGui::BeginTabBar("##proj_tabs")) {
|
||||||
|
char tab_apps[64]; std::snprintf(tab_apps, sizeof(tab_apps), "Apps (%zu)", d.apps.size());
|
||||||
|
char tab_analyses[64]; std::snprintf(tab_analyses, sizeof(tab_analyses), "Analysis (%zu)", d.analyses.size());
|
||||||
|
char tab_vaults[64]; std::snprintf(tab_vaults, sizeof(tab_vaults), "Vaults (%zu)", d.vaults.size());
|
||||||
|
|
||||||
|
if (ImGui::BeginTabItem(tab_apps)) {
|
||||||
|
draw_apps_list(d.apps);
|
||||||
|
ImGui::EndTabItem();
|
||||||
|
}
|
||||||
|
if (ImGui::BeginTabItem(tab_analyses)) {
|
||||||
|
draw_analysis_list(d.analyses);
|
||||||
|
ImGui::EndTabItem();
|
||||||
|
}
|
||||||
|
if (ImGui::BeginTabItem(tab_vaults)) {
|
||||||
|
if (d.vaults.empty()) {
|
||||||
|
empty_state("( no data )", "No vaults in this project",
|
||||||
|
"Use the + Add button with kind = Vault (requires a project)");
|
||||||
|
} else {
|
||||||
|
const char* headers[] = {"Name", "Path", "Symlink", "Description"};
|
||||||
|
std::vector<std::string> cells;
|
||||||
|
cells.reserve(d.vaults.size() * 4);
|
||||||
|
for (auto& v : d.vaults) {
|
||||||
|
cells.push_back(v.name);
|
||||||
|
cells.push_back(v.path);
|
||||||
|
cells.push_back(v.symlink ? "yes" : "no");
|
||||||
|
cells.push_back(v.description);
|
||||||
|
}
|
||||||
|
auto cp = to_cstr(cells);
|
||||||
|
table_view("##vaults", headers, 4, cp.data(),
|
||||||
|
static_cast<int>(d.vaults.size()));
|
||||||
|
}
|
||||||
|
ImGui::EndTabItem();
|
||||||
|
}
|
||||||
|
ImGui::EndTabBar();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImGui::EndChild();
|
||||||
|
|
||||||
|
ImGui::EndTable();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Actions bar (Reindex + Add button) + Add modal
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
static const char* kLangs[] = {"go", "py", "ts", "sh", "cpp"};
|
||||||
|
static const char* kDomains[] = {"core", "infra", "finance", "datascience",
|
||||||
|
"cybersecurity", "shell", "tui", "pipelines",
|
||||||
|
"browser", "viz", "gfx", "notebook"};
|
||||||
|
|
||||||
|
static void submit_add() {
|
||||||
|
if (g_api_url.empty()) {
|
||||||
|
fn_ui::toast_push(fn_ui::ToastKind::Error, "API URL not set");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
std::string name = g_add_name;
|
||||||
|
std::string desc = g_add_desc;
|
||||||
|
std::string project;
|
||||||
|
if (g_add_project_idx >= 0) {
|
||||||
|
// g_add_project_idx indexa la lista live de projects (resolved en el modal)
|
||||||
|
// guardada en g_project_ids_cache — ver draw_add_modal
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolvemos project en draw_add_modal y lo pasamos via g_add_project_resolved
|
||||||
|
extern std::string g_add_project_resolved;
|
||||||
|
project = g_add_project_resolved;
|
||||||
|
|
||||||
|
AddKind kind = static_cast<AddKind>(g_add_kind_idx);
|
||||||
|
std::string url = g_api_url;
|
||||||
|
|
||||||
|
fn_ui::runner_trigger(g_add_runner,
|
||||||
|
[kind, url, name, desc, project,
|
||||||
|
lang_idx = g_add_lang_idx, domain_idx = g_add_domain_idx,
|
||||||
|
packages = std::string(g_add_packages),
|
||||||
|
vault_path = std::string(g_add_vault_path)
|
||||||
|
](std::string& out) -> bool {
|
||||||
|
std::string body;
|
||||||
|
bool ok = false;
|
||||||
|
switch (kind) {
|
||||||
|
case AddKind::App:
|
||||||
|
ok = http_post_add_app(url, name, kLangs[lang_idx],
|
||||||
|
kDomains[domain_idx], project, desc, body);
|
||||||
|
break;
|
||||||
|
case AddKind::Analysis:
|
||||||
|
ok = http_post_add_analysis(url, name, project, packages, desc, body);
|
||||||
|
break;
|
||||||
|
case AddKind::Vault:
|
||||||
|
ok = http_post_add_vault(url, name, project, vault_path, desc, body);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
out = body;
|
||||||
|
return ok;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string g_add_project_resolved = "";
|
||||||
|
|
||||||
|
static void draw_add_modal(RegistryData& data) {
|
||||||
|
if (!fn_ui::modal_dialog_begin("Add...", &g_show_add, ImVec2(460, 0))) {
|
||||||
|
fn_ui::modal_dialog_end();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kind selector
|
||||||
|
const char* kinds[] = {"App", "Analysis", "Vault"};
|
||||||
|
fn_ui::select("Kind", &g_add_kind_idx, kinds, 3);
|
||||||
|
|
||||||
|
// Project selector (del registro vivo)
|
||||||
|
std::vector<std::string> proj_labels;
|
||||||
|
std::vector<std::string> proj_ids;
|
||||||
|
for (auto& p : data.projects) {
|
||||||
|
proj_labels.push_back(p.name);
|
||||||
|
proj_ids.push_back(p.id);
|
||||||
|
}
|
||||||
|
std::vector<const char*> proj_cstr;
|
||||||
|
for (auto& s : proj_labels) proj_cstr.push_back(s.c_str());
|
||||||
|
|
||||||
|
AddKind kind = static_cast<AddKind>(g_add_kind_idx);
|
||||||
|
fn_ui::select("Project", &g_add_project_idx, proj_cstr.data(),
|
||||||
|
static_cast<int>(proj_cstr.size()),
|
||||||
|
kind != AddKind::Vault /* vault obliga a project */);
|
||||||
|
g_add_project_resolved = (g_add_project_idx >= 0 && g_add_project_idx < (int)proj_ids.size())
|
||||||
|
? proj_ids[g_add_project_idx] : "";
|
||||||
|
|
||||||
|
fn_ui::text_input("Name", g_add_name, sizeof(g_add_name),
|
||||||
|
"snake_case, a-z0-9_");
|
||||||
|
fn_ui::text_input("Description", g_add_desc, sizeof(g_add_desc));
|
||||||
|
|
||||||
|
// Campos especificos segun kind
|
||||||
|
if (kind == AddKind::App) {
|
||||||
|
fn_ui::select("Lang", &g_add_lang_idx, kLangs, 5);
|
||||||
|
fn_ui::select("Domain", &g_add_domain_idx, kDomains, 12);
|
||||||
|
} else if (kind == AddKind::Analysis) {
|
||||||
|
fn_ui::text_input("Packages (CSV)", g_add_packages, sizeof(g_add_packages),
|
||||||
|
"polars,scikit-learn,torch");
|
||||||
|
} else if (kind == AddKind::Vault) {
|
||||||
|
fn_ui::text_input("Path (abs, opcional)", g_add_vault_path, sizeof(g_add_vault_path),
|
||||||
|
"/home/lucas/vaults/my_data");
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::Dummy(ImVec2(0, fn_tokens::spacing::sm));
|
||||||
|
|
||||||
|
// Status del runner en curso
|
||||||
|
fn_ui::runner_status(g_add_runner, "Creating...");
|
||||||
|
|
||||||
|
// Detectar transicion running->done para cerrar y notificar
|
||||||
|
static fn_ui::RunnerState last_state = fn_ui::RunnerState::Idle;
|
||||||
|
fn_ui::RunnerState now = g_add_runner.state();
|
||||||
|
if (last_state == fn_ui::RunnerState::Running
|
||||||
|
&& (now == fn_ui::RunnerState::Success || now == fn_ui::RunnerState::Error)) {
|
||||||
|
const bool ok = (now == fn_ui::RunnerState::Success);
|
||||||
|
fn_ui::toast_push(ok ? fn_ui::ToastKind::Success : fn_ui::ToastKind::Error,
|
||||||
|
ok ? "Created OK — reloading" : g_add_runner.message().c_str());
|
||||||
|
if (ok) {
|
||||||
|
g_show_add = false;
|
||||||
|
g_add_name[0] = '\0';
|
||||||
|
g_add_desc[0] = '\0';
|
||||||
|
g_add_packages[0] = '\0';
|
||||||
|
g_add_vault_path[0] = '\0';
|
||||||
|
trigger_reload();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
last_state = now;
|
||||||
|
|
||||||
|
ImGui::Separator();
|
||||||
|
if (fn_ui::button("Cancel", fn_ui::ButtonVariant::Subtle)) g_show_add = false;
|
||||||
|
ImGui::SameLine();
|
||||||
|
const bool disabled = g_add_runner.is_busy() || g_add_name[0] == '\0';
|
||||||
|
if (disabled) ImGui::BeginDisabled();
|
||||||
|
if (fn_ui::button("Create", fn_ui::ButtonVariant::Primary)) {
|
||||||
|
submit_add();
|
||||||
|
}
|
||||||
|
if (disabled) ImGui::EndDisabled();
|
||||||
|
|
||||||
|
fn_ui::modal_dialog_end();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void draw_actions_bar() {
|
||||||
|
if (g_api_url.empty()) return;
|
||||||
|
|
||||||
|
fn_ui::toolbar_begin();
|
||||||
|
if (fn_ui::button("Reindex", fn_ui::ButtonVariant::Primary)
|
||||||
|
&& !g_reindex_runner.is_busy()) {
|
||||||
|
const std::string url = g_api_url;
|
||||||
|
fn_ui::runner_trigger(g_reindex_runner, [url](std::string& out) -> bool {
|
||||||
|
return http_post_reindex(url, out);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
ImGui::SameLine();
|
||||||
|
if (fn_ui::button("+ Add", fn_ui::ButtonVariant::Secondary)) {
|
||||||
|
g_show_add = true;
|
||||||
|
g_add_runner.reset();
|
||||||
|
}
|
||||||
|
ImGui::SameLine();
|
||||||
|
if (fn_ui::button("Reload", fn_ui::ButtonVariant::Subtle)) {
|
||||||
|
trigger_reload();
|
||||||
|
}
|
||||||
|
ImGui::SameLine();
|
||||||
|
fn_ui::toast_inbox_button("##inbox");
|
||||||
|
fn_ui::toolbar_end();
|
||||||
|
|
||||||
|
// Status del reindex debajo del toolbar
|
||||||
|
static fn_ui::RunnerState last_reindex_state = fn_ui::RunnerState::Idle;
|
||||||
|
fn_ui::RunnerState now = g_reindex_runner.state();
|
||||||
|
if (now != fn_ui::RunnerState::Idle) {
|
||||||
|
fn_ui::runner_status(g_reindex_runner, "Reindexing...");
|
||||||
|
}
|
||||||
|
if (last_reindex_state == fn_ui::RunnerState::Running
|
||||||
|
&& (now == fn_ui::RunnerState::Success || now == fn_ui::RunnerState::Error)) {
|
||||||
|
const bool ok = (now == fn_ui::RunnerState::Success);
|
||||||
|
fn_ui::toast_push(ok ? fn_ui::ToastKind::Success : fn_ui::ToastKind::Error,
|
||||||
|
g_reindex_runner.message().c_str());
|
||||||
|
if (ok) trigger_reload();
|
||||||
|
}
|
||||||
|
last_reindex_state = now;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Main draw
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
void draw_dashboard(RegistryData& data) {
|
||||||
|
// Tema aplicado por fn::run_app() (app_base.h, ThemeMode::FnDark default).
|
||||||
|
|
||||||
fps_overlay();
|
fps_overlay();
|
||||||
fullscreen_window_begin("##dashboard");
|
fullscreen_window_begin("##dashboard");
|
||||||
|
|
||||||
// Subtitle con conteos — contexto rápido para el usuario
|
|
||||||
char subtitle[128];
|
char subtitle[128];
|
||||||
std::snprintf(subtitle, sizeof(subtitle),
|
std::snprintf(subtitle, sizeof(subtitle),
|
||||||
"%d functions · %d types · %d apps · %d analyses",
|
"%d functions · %d types · %d apps · %d analyses · %zu projects",
|
||||||
data.stats.total_functions, data.stats.total_types,
|
data.stats.total_functions, data.stats.total_types,
|
||||||
data.stats.total_apps, data.stats.total_analysis);
|
data.stats.total_apps, data.stats.total_analysis,
|
||||||
|
data.projects.size());
|
||||||
|
|
||||||
// Header con acción Reload a la derecha
|
|
||||||
page_header_begin("fn_registry Dashboard", subtitle);
|
page_header_begin("fn_registry Dashboard", subtitle);
|
||||||
ImGui::SameLine(ImGui::GetWindowWidth() - 120.0f);
|
|
||||||
if (ImGui::Button("Reload")) {
|
|
||||||
ImGui::GetIO().UserData = reinterpret_cast<void*>(1);
|
|
||||||
}
|
|
||||||
page_header_end();
|
page_header_end();
|
||||||
|
|
||||||
// KPIs
|
draw_actions_bar();
|
||||||
|
ImGui::Dummy(ImVec2(0, fn_tokens::spacing::sm));
|
||||||
|
|
||||||
draw_kpi_row(data.stats);
|
draw_kpi_row(data.stats);
|
||||||
ImGui::Dummy(ImVec2(0, fn_tokens::spacing::md));
|
ImGui::Dummy(ImVec2(0, fn_tokens::spacing::md));
|
||||||
|
|
||||||
// Charts — altura FIJA en pixeles (no depende del resize de la ventana).
|
|
||||||
// Antes usabamos remaining*0.35, pero eso recalculaba todo el layout al
|
|
||||||
// redimensionar, provocando vibracion visible en los plots.
|
|
||||||
constexpr float chart_h = 260.0f;
|
constexpr float chart_h = 260.0f;
|
||||||
draw_charts(data, chart_h);
|
draw_charts(data, chart_h);
|
||||||
ImGui::Dummy(ImVec2(0, fn_tokens::spacing::md));
|
ImGui::Dummy(ImVec2(0, fn_tokens::spacing::md));
|
||||||
|
|
||||||
// Tables
|
|
||||||
if (ImGui::BeginTabBar("##tables")) {
|
if (ImGui::BeginTabBar("##tables")) {
|
||||||
|
if (ImGui::BeginTabItem("Projects")) {
|
||||||
|
draw_projects_list(data);
|
||||||
|
ImGui::EndTabItem();
|
||||||
|
}
|
||||||
if (ImGui::BeginTabItem("Recent Functions")) {
|
if (ImGui::BeginTabItem("Recent Functions")) {
|
||||||
draw_recent_functions(data.recent_funcs);
|
draw_recent_functions(data.recent_funcs);
|
||||||
ImGui::EndTabItem();
|
ImGui::EndTabItem();
|
||||||
@@ -290,5 +635,10 @@ void draw_dashboard(RegistryData& data) {
|
|||||||
ImGui::EndTabBar();
|
ImGui::EndTabBar();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
draw_add_modal(data);
|
||||||
|
|
||||||
fullscreen_window_end();
|
fullscreen_window_end();
|
||||||
|
|
||||||
|
// Toasts encima de todo
|
||||||
|
fn_ui::toast_render();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "data.h"
|
#include "data.h"
|
||||||
|
#include <string>
|
||||||
|
|
||||||
// Draw the full dashboard. Call every frame.
|
// Draw the full dashboard. Call every frame.
|
||||||
void draw_dashboard(RegistryData& data);
|
void draw_dashboard(RegistryData& data);
|
||||||
|
|
||||||
|
// Called once from main.cpp after parsing --api to share the URL with the
|
||||||
|
// views (for triggering reindex/add mutations).
|
||||||
|
void views_set_api_url(const std::string& url);
|
||||||
|
|
||||||
// Individual views (called by draw_dashboard)
|
// Individual views (called by draw_dashboard)
|
||||||
void draw_kpi_row(const RegistryStats& stats);
|
void draw_kpi_row(const RegistryStats& stats);
|
||||||
void draw_charts(RegistryData& data, float height = 250.0f);
|
void draw_charts(RegistryData& data, float height = 250.0f);
|
||||||
@@ -12,3 +17,4 @@ void draw_recent_functions(const std::vector<FunctionRow>& funcs);
|
|||||||
void draw_apps_list(const std::vector<AppRow>& apps);
|
void draw_apps_list(const std::vector<AppRow>& apps);
|
||||||
void draw_analysis_list(const std::vector<AnalysisRow>& analyses);
|
void draw_analysis_list(const std::vector<AnalysisRow>& analyses);
|
||||||
void draw_types_list(const std::vector<TypeRow>& types);
|
void draw_types_list(const std::vector<TypeRow>& types);
|
||||||
|
void draw_projects_list(RegistryData& data);
|
||||||
|
|||||||
Reference in New Issue
Block a user