#include "views.h" #include "jobs.h" #include "core/icons_tabler.h" #include "core/tokens.h" #include "imgui.h" #include #include #include 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 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::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