feat(jobs): sistema de jobs asincronos + panel UI (issue 0026)
Infra para correr enrichers en background mientras la app sigue interactiva.
C++:
- jobs.{h,cpp}: tabla jobs en graph_explorer.db, JobRunner con N=2 std::thread
workers, fork+exec POSIX con pipes, parser de PROGRESS:<float> <stage> en
stderr, captura de stdout JSON, persistencia + dirty_counter.
- enrichers.{h,cpp}: scanner de enrichers/<id>/manifest.yaml, parser YAML
minimo (id/name/description/applies_to), filtro por tipo de nodo.
- views_jobs.cpp: panel "Jobs" dockeable con tabla (status/enricher/target/
progress/time), filtro all/active/done/errors, cancelar/borrar inline.
Wiring:
- main.cpp: resolve_registry_root() (FN_REGISTRY_ROOT env o subir desde cwd
buscando registry.db), jobs_init/enrichers_load antes de fn::run_app,
jobs_shutdown al cerrar, dirty_counter -> want_reload, jobs_set_ops_db al
cambiar de proyecto.
- main.cpp:render_context_menu: menu "Run enricher" sustituye placeholder
con submenu filtrado por type_ref via enrichers_for_type. Submit abre
panel Jobs auto.
- views.h: AppState::panel_jobs flag + decl views_jobs().
- CMakeLists.txt: anade jobs.cpp + enrichers.cpp + views_jobs.cpp y enlaza
Threads::Threads.
Wire protocol enricher (subprocess Python):
- stdin: JSON con node_id, metadata, ops_db_path, app_dir, cache_dir,
registry_root, params.
- stderr: PROGRESS:<float> <stage> + LOG lineas libres.
- stdout: JSON resumen al final.
- exit 0 = ok, !=0 = error con stderr capturado en panel Jobs.
El run.py escribe directamente al operations.db (sqlite3 stdlib) — C++ solo
orquesta, no parsea entities/relations.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -25,6 +25,8 @@
|
||||
#include "layout_store.h"
|
||||
#include "entity_ops.h"
|
||||
#include "project_manager.h"
|
||||
#include "jobs.h"
|
||||
#include "enrichers.h"
|
||||
|
||||
#include "../../../../cpp/vendor/sqlite3/sqlite3.h"
|
||||
|
||||
@@ -39,6 +41,13 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#ifndef _WIN32
|
||||
#include <unistd.h>
|
||||
#else
|
||||
#include <direct.h>
|
||||
#define getcwd _getcwd
|
||||
#endif
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Estado global de la app
|
||||
// ----------------------------------------------------------------------------
|
||||
@@ -116,6 +125,32 @@ static void apply_static_layout(int mode) {
|
||||
// Forward decl — definido mas abajo, lo necesita switch_to_project.
|
||||
static bool load_input();
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Registry path resolution (issue 0026)
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
// Devuelve el path absoluto al root de fn_registry. Estrategia:
|
||||
// 1) FN_REGISTRY_ROOT env var
|
||||
// 2) Sube desde getcwd() buscando un dir con `registry.db`
|
||||
// 3) "" si no se encuentra
|
||||
static std::string resolve_registry_root() {
|
||||
if (const char* env = std::getenv("FN_REGISTRY_ROOT")) {
|
||||
if (env && *env) return env;
|
||||
}
|
||||
char cwd[4096];
|
||||
if (getcwd(cwd, sizeof(cwd)) == nullptr) return "";
|
||||
std::string p = cwd;
|
||||
for (int i = 0; i < 8; ++i) {
|
||||
std::string probe = p + "/registry.db";
|
||||
FILE* f = std::fopen(probe.c_str(), "rb");
|
||||
if (f) { std::fclose(f); return p; }
|
||||
size_t s = p.find_last_of('/');
|
||||
if (s == std::string::npos || s == 0) break;
|
||||
p = p.substr(0, s);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Project lifecycle
|
||||
// ----------------------------------------------------------------------------
|
||||
@@ -240,6 +275,9 @@ static bool load_input() {
|
||||
ge::entity_index_build(g_input.uri, &g_idx);
|
||||
g_app.input_db_path = g_input.uri ? g_input.uri : "";
|
||||
|
||||
// issue 0026 — apunta el JobRunner a la nueva operations.db.
|
||||
if (g_input.uri) ge::jobs_set_ops_db(g_input.uri);
|
||||
|
||||
// Cargar posiciones guardadas para este graph_hash
|
||||
g_graph_hash = ge::compute_graph_hash(g_input.uri);
|
||||
int restored = ge::layout_store_load(g_graph_hash, g_graph);
|
||||
@@ -483,7 +521,27 @@ static void render_context_menu() {
|
||||
|
||||
ImGui::Separator();
|
||||
if (ImGui::BeginMenu("Run enricher")) {
|
||||
ImGui::TextDisabled("(coming soon — issues 0001/0002/0003)");
|
||||
// issue 0026 — listamos enrichers cuyo applies_to incluye este type.
|
||||
const char* type_name = (n.type_id < (uint16_t)g_graph.type_count)
|
||||
? g_graph.types[n.type_id].name : "";
|
||||
auto specs = ge::enrichers_for_type(type_name);
|
||||
if (!sql_id) {
|
||||
ImGui::TextDisabled("(node has no entity id)");
|
||||
} else if (specs.empty()) {
|
||||
ImGui::TextDisabled("(no enrichers para tipo '%s')", type_name);
|
||||
} else {
|
||||
for (const auto& s : specs) {
|
||||
if (ImGui::MenuItem(s.name.c_str())) {
|
||||
char job_id[64];
|
||||
bool ok = ge::jobs_submit(s.id.c_str(), sql_id, lbl,
|
||||
"{}", job_id, sizeof(job_id));
|
||||
if (ok) g_app.panel_jobs = true; // abrir panel auto
|
||||
}
|
||||
if (!s.description.empty() && ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip("%s", s.description.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
|
||||
@@ -512,6 +570,7 @@ static fn_ui::PanelToggle g_panels[] = {
|
||||
{"Note", nullptr, &g_app.panel_note},
|
||||
{"Types", nullptr, &g_app.panel_type_editor},
|
||||
{"Table", nullptr, &g_app.panel_table},
|
||||
{"Jobs", nullptr, &g_app.panel_jobs},
|
||||
};
|
||||
|
||||
static void render() {
|
||||
@@ -596,6 +655,16 @@ static void render() {
|
||||
g_app.apply_layout_tick = 0;
|
||||
}
|
||||
|
||||
// issue 0026 — si un job termino con cambios, dispara reload del grafo.
|
||||
{
|
||||
static int s_last_dirty = 0;
|
||||
int d = ge::jobs_dirty_counter();
|
||||
if (d != s_last_dirty) {
|
||||
s_last_dirty = d;
|
||||
g_app.want_reload = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Triggers desde la toolbar
|
||||
if (g_app.want_fit) {
|
||||
graph_viewport_fit(g_graph, g_viewport);
|
||||
@@ -1194,6 +1263,12 @@ static void render() {
|
||||
ge::views_table_window(g_app);
|
||||
ge::views_import_dataset_modal(g_app);
|
||||
|
||||
// Jobs panel (issue 0026) — flotante, dockeable.
|
||||
ImGui::SetNextWindowPos (ImVec2(vp->WorkPos.x + W * 0.20f, top + 40.0f),
|
||||
ImGuiCond_FirstUseEver);
|
||||
ImGui::SetNextWindowSize(ImVec2(900.0f, 360.0f), ImGuiCond_FirstUseEver);
|
||||
ge::views_jobs(g_app);
|
||||
|
||||
g_first_render = false;
|
||||
}
|
||||
|
||||
@@ -1418,6 +1493,34 @@ int main(int argc, char** argv) {
|
||||
"cualquier app del registry y permite explorar entidades/relaciones con "
|
||||
"shapes/iconos/layouts/filtros.");
|
||||
|
||||
// issue 0026 — sistema de jobs + enrichers.
|
||||
{
|
||||
std::string registry_root = resolve_registry_root();
|
||||
std::string app_dir = registry_root.empty()
|
||||
? "."
|
||||
: registry_root + "/projects/osint_graph/apps/graph_explorer";
|
||||
std::string enrichers_dir = app_dir + "/enrichers";
|
||||
|
||||
// graph_explorer.db es el mismo SQLite usado por layout_store.
|
||||
const char* app_db = g_layout_db_path.empty()
|
||||
? "graph_explorer.db" : g_layout_db_path.c_str();
|
||||
|
||||
ge::enrichers_load(enrichers_dir.c_str());
|
||||
if (!ge::jobs_init(app_db,
|
||||
g_input.uri ? g_input.uri : "",
|
||||
enrichers_dir.c_str(),
|
||||
app_dir.c_str(),
|
||||
registry_root.c_str(),
|
||||
/*n_workers=*/2)) {
|
||||
std::fprintf(stderr, "[graph_explorer] jobs_init failed (panel disabled)\n");
|
||||
} else {
|
||||
std::fprintf(stdout,
|
||||
"[graph_explorer] jobs_init OK — enrichers_dir=%s, registry_root=%s, %d enrichers\n",
|
||||
enrichers_dir.c_str(), registry_root.c_str(),
|
||||
(int)ge::enrichers_all().size());
|
||||
}
|
||||
}
|
||||
|
||||
int rc = fn::run_app(
|
||||
{.title = "graph_explorer",
|
||||
.width = 1600,
|
||||
@@ -1429,6 +1532,7 @@ int main(int argc, char** argv) {
|
||||
render);
|
||||
|
||||
// Cleanup
|
||||
ge::jobs_shutdown();
|
||||
if (g_gpu_ctx) graph_force_layout_gpu_destroy(g_gpu_ctx);
|
||||
if (g_atlas) graph_icons_destroy(g_atlas);
|
||||
graph_viewport_destroy(g_viewport);
|
||||
|
||||
Reference in New Issue
Block a user