6df04652d8
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>
200 lines
7.3 KiB
C++
200 lines
7.3 KiB
C++
#include "views.h"
|
|
#include "jobs.h"
|
|
|
|
#include "core/icons_tabler.h"
|
|
#include "core/tokens.h"
|
|
|
|
#include "imgui.h"
|
|
|
|
#include <cfloat>
|
|
#include <chrono>
|
|
#include <cstdio>
|
|
|
|
namespace ge {
|
|
|
|
namespace {
|
|
|
|
// Cache de la lista de jobs. Se refresca cada N frames para no abrir SQLite
|
|
// en cada frame. ~10 Hz es suficiente para una progress bar fluida.
|
|
struct JobsCache {
|
|
std::vector<JobRow> rows;
|
|
int last_frame_refresh = -100;
|
|
int filter_idx = 0; // 0=all 1=active 2=done 3=error
|
|
char buf[8] = {};
|
|
};
|
|
JobsCache g_jobs_cache;
|
|
|
|
const char* status_icon(const std::string& s) {
|
|
if (s == "queued") return TI_HOURGLASS;
|
|
if (s == "running") return TI_PLAYER_PLAY;
|
|
if (s == "done") return TI_CHECK;
|
|
if (s == "error") return TI_ALERT_CIRCLE;
|
|
if (s == "cancelled") return TI_X;
|
|
return TI_QUESTION_MARK;
|
|
}
|
|
|
|
ImVec4 status_color(const std::string& s) {
|
|
if (s == "running") return ImVec4(0.36f, 0.78f, 1.0f, 1.0f);
|
|
if (s == "done") return ImVec4(0.40f, 0.85f, 0.55f, 1.0f);
|
|
if (s == "error") return ImVec4(0.95f, 0.45f, 0.45f, 1.0f);
|
|
if (s == "cancelled") return ImVec4(0.65f, 0.65f, 0.65f, 1.0f);
|
|
if (s == "queued") return ImVec4(0.85f, 0.78f, 0.45f, 1.0f);
|
|
return ImVec4(0.7f, 0.7f, 0.7f, 1.0f);
|
|
}
|
|
|
|
std::string format_duration(long long started, long long finished) {
|
|
if (started <= 0) return "—";
|
|
long long end = finished > 0 ? finished
|
|
: std::chrono::duration_cast<std::chrono::milliseconds>(
|
|
std::chrono::system_clock::now().time_since_epoch())
|
|
.count();
|
|
long long ms = end - started;
|
|
if (ms < 0) ms = 0;
|
|
char b[32];
|
|
if (ms < 1000) std::snprintf(b, sizeof(b), "%lld ms", ms);
|
|
else if (ms < 60'000) std::snprintf(b, sizeof(b), "%.1f s", ms / 1000.0);
|
|
else std::snprintf(b, sizeof(b), "%.1f m", ms / 60'000.0);
|
|
return b;
|
|
}
|
|
|
|
bool filter_match(const std::string& status, int idx) {
|
|
switch (idx) {
|
|
case 0: return true; // all
|
|
case 1: return status == "queued" || status == "running"; // active
|
|
case 2: return status == "done";
|
|
case 3: return status == "error" || status == "cancelled";
|
|
default: return true;
|
|
}
|
|
}
|
|
|
|
} // namespace
|
|
|
|
void views_jobs(AppState& app) {
|
|
if (!app.panel_jobs) return;
|
|
|
|
if (!ImGui::Begin("Jobs", &app.panel_jobs)) {
|
|
ImGui::End();
|
|
return;
|
|
}
|
|
|
|
// Refresh cache cada ~10 frames (~6 Hz a 60fps).
|
|
int frame = ImGui::GetFrameCount();
|
|
if (frame - g_jobs_cache.last_frame_refresh > 10) {
|
|
jobs_list(&g_jobs_cache.rows, 200);
|
|
g_jobs_cache.last_frame_refresh = frame;
|
|
}
|
|
|
|
// Header: counters + filtro.
|
|
JobCounters c = jobs_counters();
|
|
ImGui::TextColored(status_color("running"), "%s", TI_PLAYER_PLAY);
|
|
ImGui::SameLine(); ImGui::Text("%d", c.running);
|
|
ImGui::SameLine(0, 16);
|
|
ImGui::TextColored(status_color("queued"), "%s", TI_HOURGLASS);
|
|
ImGui::SameLine(); ImGui::Text("%d", c.queued);
|
|
ImGui::SameLine(0, 16);
|
|
ImGui::TextColored(status_color("done"), "%s", TI_CHECK);
|
|
ImGui::SameLine(); ImGui::Text("%d", c.done);
|
|
ImGui::SameLine(0, 16);
|
|
ImGui::TextColored(status_color("error"), "%s", TI_ALERT_CIRCLE);
|
|
ImGui::SameLine(); ImGui::Text("%d", c.error + c.cancelled);
|
|
|
|
ImGui::SameLine(0, 24);
|
|
const char* filter_labels[] = { "All", "Active", "Done", "Errors" };
|
|
ImGui::SetNextItemWidth(100);
|
|
ImGui::Combo("##jobs_filter", &g_jobs_cache.filter_idx,
|
|
filter_labels, IM_ARRAYSIZE(filter_labels));
|
|
|
|
ImGui::Separator();
|
|
|
|
// Tabla.
|
|
ImGuiTableFlags tflags = ImGuiTableFlags_Borders |
|
|
ImGuiTableFlags_RowBg |
|
|
ImGuiTableFlags_SizingStretchProp |
|
|
ImGuiTableFlags_ScrollY;
|
|
if (ImGui::BeginTable("jobs_table", 6, tflags,
|
|
ImVec2(0, ImGui::GetContentRegionAvail().y - 4))) {
|
|
ImGui::TableSetupScrollFreeze(0, 1);
|
|
ImGui::TableSetupColumn("Status", ImGuiTableColumnFlags_WidthFixed, 90);
|
|
ImGui::TableSetupColumn("Enricher", ImGuiTableColumnFlags_WidthStretch, 1.5f);
|
|
ImGui::TableSetupColumn("Target", ImGuiTableColumnFlags_WidthStretch, 2.0f);
|
|
ImGui::TableSetupColumn("Progress", ImGuiTableColumnFlags_WidthStretch, 2.0f);
|
|
ImGui::TableSetupColumn("Time", ImGuiTableColumnFlags_WidthFixed, 70);
|
|
ImGui::TableSetupColumn("##actions",ImGuiTableColumnFlags_WidthFixed, 80);
|
|
ImGui::TableHeadersRow();
|
|
|
|
for (const auto& r : g_jobs_cache.rows) {
|
|
if (!filter_match(r.status, g_jobs_cache.filter_idx)) continue;
|
|
ImGui::PushID(r.id.c_str());
|
|
ImGui::TableNextRow();
|
|
|
|
// Status.
|
|
ImGui::TableSetColumnIndex(0);
|
|
ImGui::TextColored(status_color(r.status), "%s %s",
|
|
status_icon(r.status), r.status.c_str());
|
|
|
|
// Enricher.
|
|
ImGui::TableSetColumnIndex(1);
|
|
ImGui::TextUnformatted(r.enricher_id.c_str());
|
|
|
|
// Target.
|
|
ImGui::TableSetColumnIndex(2);
|
|
if (!r.node_name.empty()) {
|
|
ImGui::TextUnformatted(r.node_name.c_str());
|
|
} else if (!r.node_id.empty()) {
|
|
ImGui::TextDisabled("%s", r.node_id.c_str());
|
|
} else {
|
|
ImGui::TextDisabled("(global)");
|
|
}
|
|
|
|
// Progress.
|
|
ImGui::TableSetColumnIndex(3);
|
|
if (r.status == "running" || r.status == "queued") {
|
|
ImGui::ProgressBar((float)r.progress, ImVec2(-FLT_MIN, 0),
|
|
r.stage.empty() ? nullptr : r.stage.c_str());
|
|
} else if (r.status == "error" && !r.error.empty()) {
|
|
ImGui::TextColored(status_color("error"), "%s",
|
|
r.error.size() > 64
|
|
? (r.error.substr(0, 64) + "…").c_str()
|
|
: r.error.c_str());
|
|
if (ImGui::IsItemHovered()) {
|
|
ImGui::SetTooltip("%s", r.error.c_str());
|
|
}
|
|
} else if (r.status == "done" && !r.result_json.empty()) {
|
|
ImGui::TextDisabled("%s",
|
|
r.result_json.size() > 80
|
|
? (r.result_json.substr(0, 80) + "…").c_str()
|
|
: r.result_json.c_str());
|
|
if (ImGui::IsItemHovered() && r.result_json.size() > 80) {
|
|
ImGui::SetTooltip("%s", r.result_json.c_str());
|
|
}
|
|
} else {
|
|
ImGui::TextDisabled("—");
|
|
}
|
|
|
|
// Time.
|
|
ImGui::TableSetColumnIndex(4);
|
|
ImGui::TextDisabled("%s",
|
|
format_duration(r.started_at, r.finished_at).c_str());
|
|
|
|
// Actions.
|
|
ImGui::TableSetColumnIndex(5);
|
|
if (r.status == "queued" || r.status == "running") {
|
|
if (ImGui::SmallButton("Cancel")) {
|
|
jobs_cancel(r.id.c_str());
|
|
}
|
|
} else {
|
|
if (ImGui::SmallButton("Delete")) {
|
|
jobs_delete(r.id.c_str());
|
|
}
|
|
}
|
|
|
|
ImGui::PopID();
|
|
}
|
|
ImGui::EndTable();
|
|
}
|
|
|
|
ImGui::End();
|
|
}
|
|
|
|
} // namespace ge
|