#include "tabs.h" #include "data_table/data_table.h" #include "core/data_table_types.h" #include "core/icons_tabler.h" #include "core/empty_state.h" #include "core/badge.h" #include #include #include #include #include #include #include namespace dag_ui_tabs { // --------------------------------------------------------------------------- // Globals // --------------------------------------------------------------------------- Selection& selection() { static Selection s; return s; } Caches& caches() { static Caches c; return c; } FunctionPanelState& function_panel() { static FunctionPanelState fps; return fps; } // data_table::State persistente por panel (issue 0081-J pattern). static data_table::State g_st_dag_list; static data_table::State g_st_dag_runs; static data_table::State g_st_run_steps; // Backing storage para cells de cada tabla. Owner del char* const* en TableInput. static std::vector g_back_dag_list; static std::vector g_ptrs_dag_list; static std::vector g_back_dag_runs; static std::vector g_ptrs_dag_runs; static std::vector g_back_run_steps; static std::vector g_ptrs_run_steps; // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- static void cells_to_ptrs(const std::vector& backing, std::vector& ptrs) { ptrs.resize(backing.size()); for (size_t i = 0; i < backing.size(); i++) ptrs[i] = backing[i].c_str(); } static std::string format_duration(long long ms) { if (ms <= 0) return "-"; if (ms < 1000) return std::to_string(ms) + "ms"; char buf[32]; std::snprintf(buf, sizeof(buf), "%.2fs", ms / 1000.0); return buf; } static std::string status_for_dag(const std::string& dag_name, const dag_ui::DagInfo& info, const std::vector& live_runs) { // Find most recent live run that matches this DAG. const dag_ui::DagRunRow* best = nullptr; for (auto& r : live_runs) { if (r.dag_name != dag_name) continue; if (!best || r.started_at > best->started_at) best = &r; } if (best) return best->status; if (info.has_last_run) return info.last_run_status; return "-"; } // --------------------------------------------------------------------------- // DAG List // --------------------------------------------------------------------------- // Forward decl — main.cpp owns the cache and the refresh trigger. extern "C" void dag_list_request_refresh(); void draw_dag_list(const std::string& api_url, const std::vector& dags, const std::vector& live_runs) { if (!ImGui::Begin(TI_LIST " DAGs")) { ImGui::End(); return; } if (ImGui::Button(TI_REFRESH " Refresh##dag_list")) { dag_list_request_refresh(); } ImGui::SameLine(); ImGui::TextDisabled("Double-click row -> inspect. Recent = last 5 runs inline."); if (dags.empty()) { empty_state("( no DAGs )", "Empty registry", "Place a YAML in apps/dag_engine/dags_migrated/ and reload the server."); ImGui::End(); return; } // Build TableInput. 6 columns (was 10 with R1..R5 as separate Badge cols — // that was an antipattern fixed in issue 0081-O.5). "Recent" uses CellRenderer::Dots // to show up to 5 inline colored dots from a comma-separated status string. data_table::TableInput ti; ti.name = "dags"; ti.headers = {"Name", "Schedule", "Status", "Recent", "Tags", "Valid"}; ti.types = { data_table::ColumnType::String, // Name data_table::ColumnType::String, // Schedule data_table::ColumnType::String, // Status (last run) data_table::ColumnType::String, // Recent (dots: up to 5 runs) data_table::ColumnType::String, // Tags data_table::ColumnType::String, // Valid }; ti.rows = static_cast(dags.size()); ti.cols = static_cast(ti.headers.size()); // BadgeRule set: shared by Recent (Dots). auto run_status_badges = [](){ std::vector rules; rules.push_back({"success", "#22c55e", ""}); // verde rules.push_back({"failed", "#ef4444", ""}); // rojo rules.push_back({"running", "#eab308", ""}); // amarillo rules.push_back({"pending", "#94a3b8", ""}); // gris azulado rules.push_back({"cancelled", "#6b7280", ""}); // gris return rules; }; // ChipRule set: Status (CategoricalChip — dot izquierda + texto, siempre visible). auto run_status_chips = [](){ std::vector rules; rules.push_back({"success", "#22c55e"}); rules.push_back({"failed", "#ef4444"}); rules.push_back({"running", "#eab308"}); rules.push_back({"pending", "#94a3b8"}); rules.push_back({"cancelled", "#6b7280"}); return rules; }; // ColumnSpec per column. ti.column_specs.resize(ti.cols); for (int i = 0; i < ti.cols; i++) ti.column_specs[i].id = ti.headers[i]; // idx 2 — "Status": CategoricalChip (dot izquierda + texto, always visible). ti.column_specs[2].renderer = data_table::CellRenderer::CategoricalChip; ti.column_specs[2].chips = run_status_chips(); // idx 3 — "Recent": Dots renderer — each dot = one of the last 5 runs. ti.column_specs[3].renderer = data_table::CellRenderer::Dots; ti.column_specs[3].badges = run_status_badges(); ti.column_specs[3].dots_max = 5; ti.column_specs[3].dots_show_count = false; ti.column_specs[3].tooltip_on_hover = true; // hover each dot -> show status string g_back_dag_list.clear(); g_back_dag_list.reserve(dags.size() * ti.cols); for (auto& d : dags) { g_back_dag_list.push_back(d.name); g_back_dag_list.push_back(d.schedule.empty() ? "-" : d.schedule[0]); g_back_dag_list.push_back(status_for_dag(d.name, d, live_runs)); // "Recent": join up to 5 last run statuses as comma-separated string. // Most-recent first. Only include runs that exist (no padding with "-"). { std::string recent; int n = std::min(5, static_cast(d.last_runs_status.size())); for (int i = 0; i < n; i++) { if (i) recent += ','; recent += d.last_runs_status[i]; } g_back_dag_list.push_back(recent); } std::string tags_csv; for (size_t i = 0; i < d.tags.size(); i++) { if (i) tags_csv += ","; tags_csv += d.tags[i]; } g_back_dag_list.push_back(tags_csv); g_back_dag_list.push_back(d.valid ? "yes" : "no"); } cells_to_ptrs(g_back_dag_list, g_ptrs_dag_list); ti.cells = g_ptrs_dag_list.data(); std::vector events; ImGui::BeginChild("##dag_list_wrap", ImVec2(-1, -1)); data_table::render("##dt_dag_list", {ti}, g_st_dag_list, &events); ImGui::EndChild(); // Handle row events -> select DAG. for (auto& ev : events) { if (ev.kind == data_table::TableEventKind::RowDoubleClick && ev.row >= 0 && ev.row < static_cast(dags.size())) { selection().dag_name = dags[ev.row].name; caches().dag_detail_loaded = false; // force reload } } ImGui::End(); } // --------------------------------------------------------------------------- // DAG Detail // --------------------------------------------------------------------------- static void load_dag_detail(const std::string& api_url) { auto& sel = selection(); auto& c = caches(); c.dag_detail = {}; if (sel.dag_name.empty()) { c.dag_detail_loaded = false; return; } if (dag_ui::get_dag_http(api_url, sel.dag_name, c.dag_detail)) { c.dag_detail_loaded = true; } } void draw_dag_detail(const std::string& api_url) { if (!ImGui::Begin(TI_INFO_CIRCLE " DAG Detail")) { ImGui::End(); return; } auto& sel = selection(); auto& c = caches(); if (sel.dag_name.empty()) { empty_state("( nothing selected )", "Pick a DAG", "Double-click a row in the DAG list to inspect it here."); ImGui::End(); return; } if (!c.dag_detail_loaded) load_dag_detail(api_url); auto& info = c.dag_detail.info; ImGui::Text("%s %s", TI_HASH, info.name.empty() ? sel.dag_name.c_str() : info.name.c_str()); if (!info.description.empty()) ImGui::TextWrapped("%s", info.description.c_str()); if (!info.schedule.empty()) ImGui::Text("Schedule: %s", info.schedule[0].c_str()); ImGui::Separator(); if (ImGui::Button(TI_PLAYER_PLAY " Run Now")) { std::string run_id, err; if (dag_ui::trigger_dag_http(api_url, sel.dag_name, run_id, err)) { sel.run_id = run_id; c.run_detail_loaded = false; } else { // Surface error via console; UI banner could be added later. fprintf(stderr, "[dag_detail] trigger failed: %s\n", err.c_str()); } } ImGui::SameLine(); if (ImGui::Button(TI_REFRESH " Refresh")) { c.dag_detail_loaded = false; } ImGui::Separator(); ImGui::TextUnformatted("Recent runs:"); auto& runs = c.dag_detail.recent_runs; if (runs.empty()) { ImGui::TextDisabled("( no runs yet )"); ImGui::End(); return; } data_table::TableInput ti; ti.name = "runs"; ti.headers = {"Run ID", "Status", "Trigger", "Started", "Finished", "Error"}; ti.types = { data_table::ColumnType::String, data_table::ColumnType::String, data_table::ColumnType::String, data_table::ColumnType::Date, data_table::ColumnType::Date, data_table::ColumnType::String, }; ti.rows = static_cast(runs.size()); ti.cols = static_cast(ti.headers.size()); g_back_dag_runs.clear(); g_back_dag_runs.reserve(runs.size() * ti.cols); for (auto& r : runs) { g_back_dag_runs.push_back(r.id); g_back_dag_runs.push_back(r.status); g_back_dag_runs.push_back(r.trigger); g_back_dag_runs.push_back(r.started_at); g_back_dag_runs.push_back(r.finished_at); g_back_dag_runs.push_back(r.error); } cells_to_ptrs(g_back_dag_runs, g_ptrs_dag_runs); ti.cells = g_ptrs_dag_runs.data(); std::vector events; ImGui::BeginChild("##dag_runs_wrap", ImVec2(-1, -1)); data_table::render("##dt_dag_runs", {ti}, g_st_dag_runs, &events); ImGui::EndChild(); for (auto& ev : events) { if (ev.kind == data_table::TableEventKind::RowDoubleClick && ev.row >= 0 && ev.row < static_cast(runs.size())) { sel.run_id = runs[ev.row].id; caches().run_detail_loaded = false; } } ImGui::End(); } // --------------------------------------------------------------------------- // Run Detail // --------------------------------------------------------------------------- static void load_run_detail(const std::string& api_url) { auto& sel = selection(); auto& c = caches(); c.run_detail = {}; if (sel.run_id.empty()) { c.run_detail_loaded = false; return; } if (dag_ui::get_run_http(api_url, sel.run_id, c.run_detail)) { c.run_detail_loaded = true; } } void draw_run_detail(const std::string& api_url) { if (!ImGui::Begin(TI_CLIPBOARD_LIST " Run Detail")) { ImGui::End(); return; } auto& sel = selection(); auto& c = caches(); if (sel.run_id.empty()) { empty_state("( nothing selected )", "Pick a run", "Double-click a row in DAG Detail recent runs."); ImGui::End(); return; } if (!c.run_detail_loaded) load_run_detail(api_url); auto& run = c.run_detail.run; ImGui::Text("%s %s", TI_HASH, run.id.empty() ? sel.run_id.c_str() : run.id.c_str()); ImGui::Text("Status: %s | Trigger: %s", run.status.c_str(), run.trigger.c_str()); ImGui::Text("Started: %s | Finished: %s", run.started_at.c_str(), run.finished_at.empty() ? "-" : run.finished_at.c_str()); if (!run.error.empty()) { ImGui::TextColored(ImVec4(1, 0.4f, 0.4f, 1), "Error: %s", run.error.c_str()); } ImGui::SameLine(); if (ImGui::Button(TI_REFRESH " Refresh##run")) { c.run_detail_loaded = false; } ImGui::Separator(); auto& steps = c.run_detail.steps; if (steps.empty()) { ImGui::TextDisabled("( no steps yet )"); ImGui::End(); return; } // Steps table — migrado a data_table::render (issue 0107g). // La columna Function usa CellRenderer::Button con action_id="open_fn". // Celdas con function_id="" muestran "(shell)" via Text (no button). static data_table::State g_st_run_steps; static std::vector g_back_run_steps; static std::vector g_ptrs_run_steps; g_back_run_steps.clear(); for (const auto& s : steps) { g_back_run_steps.push_back(s.step_name); g_back_run_steps.push_back(s.function_id.empty() ? "(shell)" : s.function_id); g_back_run_steps.push_back(s.status); g_back_run_steps.push_back(std::to_string(s.exit_code)); g_back_run_steps.push_back(format_duration(s.duration_ms)); g_back_run_steps.push_back(s.started_at); } g_ptrs_run_steps.clear(); for (const auto& sv : g_back_run_steps) g_ptrs_run_steps.push_back(sv.c_str()); data_table::TableInput tbl_steps; tbl_steps.name = "dt_run_steps"; tbl_steps.headers = {"Step", "Function", "Status", "Exit", "Duration", "Started"}; tbl_steps.types = { data_table::ColumnType::String, data_table::ColumnType::String, data_table::ColumnType::String, data_table::ColumnType::Int, data_table::ColumnType::String, data_table::ColumnType::String, }; tbl_steps.cells = g_ptrs_run_steps.empty() ? nullptr : g_ptrs_run_steps.data(); tbl_steps.rows = (int)steps.size(); tbl_steps.cols = 6; tbl_steps.column_specs.resize(tbl_steps.cols); for (int i = 0; i < tbl_steps.cols; i++) tbl_steps.column_specs[i].id = tbl_steps.headers[i]; // Function → Button (celdas "(shell)" no son function_ids — se ven como texto si label=value) tbl_steps.column_specs[1].renderer = data_table::CellRenderer::Button; tbl_steps.column_specs[1].button_action = "open_fn"; tbl_steps.column_specs[1].button_label = ""; // "" → usa valor de celda como label tbl_steps.column_specs[1].button_color_hex = "#21882b"; tbl_steps.column_specs[1].tooltip = "Open in Function panel"; tbl_steps.column_specs[1].tooltip_on_hover = true; // Status → CategoricalChip tbl_steps.column_specs[2].renderer = data_table::CellRenderer::CategoricalChip; tbl_steps.column_specs[2].chips = { {"success", "#22c55e"}, {"failed", "#ef4444"}, {"running", "#f59e0b"}, {"cancelled", "#a3a3a3"}, {"pending", "#3b82f6"}, }; // Duration → Duration renderer tbl_steps.column_specs[4].renderer = data_table::CellRenderer::Duration; tbl_steps.column_specs[4].duration_warn_ms = 5000.0f; tbl_steps.column_specs[4].duration_error_ms = 30000.0f; std::vector step_events; ImGui::BeginChild("##run_steps_wrap", ImVec2(-1, ImGui::GetContentRegionAvail().y * 0.5f)); data_table::render("##dt_run_steps", {tbl_steps}, g_st_run_steps, &step_events); ImGui::EndChild(); for (const auto& ev : step_events) { if (ev.kind == data_table::TableEventKind::ButtonClick && ev.action_id == "open_fn" && ev.value != "(shell)") { auto& fp = function_panel(); if (!fp.selected_id.empty() && fp.selected_id != ev.value) { fp.breadcrumb.push_back(fp.selected_id); } fp.selected_id = ev.value; fp.loaded = false; fp.load_error.clear(); } } // stdout/stderr expandible por step. ImGui::Separator(); ImGui::TextUnformatted("Step output:"); for (size_t i = 0; i < steps.size(); i++) { char hdr[256]; std::snprintf(hdr, sizeof(hdr), "%s##step_%zu", steps[i].step_name.c_str(), i); if (ImGui::CollapsingHeader(hdr)) { if (!steps[i].stdout_text.empty()) { ImGui::TextUnformatted("stdout:"); ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.08f, 0.08f, 0.08f, 1)); ImGui::BeginChild((std::string("##stdout_") + std::to_string(i)).c_str(), ImVec2(-1, 80), false); ImGui::TextUnformatted(steps[i].stdout_text.c_str()); ImGui::EndChild(); ImGui::PopStyleColor(); } if (!steps[i].stderr_text.empty()) { ImGui::TextColored(ImVec4(1, 0.4f, 0.4f, 1), "stderr:"); ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.12f, 0.06f, 0.06f, 1)); ImGui::BeginChild((std::string("##stderr_") + std::to_string(i)).c_str(), ImVec2(-1, 80), false); ImGui::TextUnformatted(steps[i].stderr_text.c_str()); ImGui::EndChild(); ImGui::PopStyleColor(); } } } ImGui::End(); } // --------------------------------------------------------------------------- // Timeline (ImPlot scatter X=tiempo, Y=DAG) // --------------------------------------------------------------------------- // Parse RFC3339 "2026-05-15T17:01:20+02:00" -> epoch seconds (local time). // Devuelve 0 si parse falla. static time_t parse_rfc3339(const std::string& s) { if (s.empty()) return 0; std::tm tm{}; // strptime no acepta timezone offsets en todos los libc. Hacemos manual: // "YYYY-MM-DDTHH:MM:SS" -> los primeros 19 chars. Ignoramos el offset y // tratamos como local time (coherente con ImPlot::UseLocalTime=true). if (s.size() < 19) return 0; tm.tm_year = std::atoi(s.substr(0, 4).c_str()) - 1900; tm.tm_mon = std::atoi(s.substr(5, 2).c_str()) - 1; tm.tm_mday = std::atoi(s.substr(8, 2).c_str()); tm.tm_hour = std::atoi(s.substr(11, 2).c_str()); tm.tm_min = std::atoi(s.substr(14, 2).c_str()); tm.tm_sec = std::atoi(s.substr(17, 2).c_str()); tm.tm_isdst = -1; return std::mktime(&tm); } static ImVec4 color_for_status(const std::string& st) { if (st == "success") return ImVec4(0.30f, 0.85f, 0.40f, 0.95f); // verde if (st == "failed") return ImVec4(0.95f, 0.35f, 0.30f, 0.95f); // rojo if (st == "running") return ImVec4(0.95f, 0.80f, 0.20f, 0.95f); // amarillo if (st == "pending") return ImVec4(0.60f, 0.65f, 0.75f, 0.85f); // gris azul if (st == "cancelled") return ImVec4(0.50f, 0.50f, 0.50f, 0.85f); // gris return ImVec4(0.70f, 0.70f, 0.70f, 0.70f); } // Ventana de tiempo seleccionable. static const char* kTLWindowLabels[] = {"15m", "1h", "6h", "24h", "7d"}; static const int kTLWindowSecs[] = {900, 3600, 21600, 86400, 604800}; static int g_tl_window_idx = 3; // 24h void draw_timeline(const std::string& api_url, const std::vector& runs_all) { if (!ImGui::Begin(TI_CHART_LINE " Timeline")) { ImGui::End(); return; } ImGui::SetNextItemWidth(120); if (ImGui::BeginCombo("Window", kTLWindowLabels[g_tl_window_idx])) { for (int i = 0; i < IM_ARRAYSIZE(kTLWindowLabels); i++) { bool sel = (i == g_tl_window_idx); if (ImGui::Selectable(kTLWindowLabels[i], sel)) g_tl_window_idx = i; if (sel) ImGui::SetItemDefaultFocus(); } ImGui::EndCombo(); } ImGui::SameLine(); ImGui::TextDisabled("%zu runs en cache", runs_all.size()); if (runs_all.empty()) { empty_state("( no data )", "No runs yet", "Trigger a DAG (DAG Detail -> Run Now) o espera al scheduler."); ImGui::End(); return; } // Calculate window bounds. const double now = static_cast(std::time(nullptr)); const int win_s = kTLWindowSecs[g_tl_window_idx]; const double left = now - static_cast(win_s); // Asignar Y index por DAG. Mantener orden alfabetico estable. std::map dag_y; { std::vector uniq; for (auto& r : runs_all) { if (!dag_y.count(r.dag_name)) { dag_y[r.dag_name] = -1; uniq.push_back(r.dag_name); } } std::sort(uniq.begin(), uniq.end()); for (size_t i = 0; i < uniq.size(); i++) dag_y[uniq[i]] = static_cast(i); } // Buffers por status -> {xs, ys} para PlotScatter. struct StatusBuf { std::vector xs, ys; }; std::map by_status; for (auto& r : runs_all) { if (r.dag_name.empty()) continue; time_t t = parse_rfc3339(r.started_at); if (t == 0) continue; double x = static_cast(t); if (x < left || x > now + 10.0) continue; double y = static_cast(dag_y[r.dag_name]); by_status[r.status].xs.push_back(x); by_status[r.status].ys.push_back(y); } ImPlot::GetStyle().UseLocalTime = true; // Y ticks = DAG names. std::vector ticks; std::vector labels; std::vector labels_owner; // backing storage for c_str() labels_owner.reserve(dag_y.size()); ticks.reserve(dag_y.size()); labels.reserve(dag_y.size()); // Build sorted-by-y view std::vector> pairs(dag_y.begin(), dag_y.end()); std::sort(pairs.begin(), pairs.end(), [](auto& a, auto& b){ return a.second < b.second; }); for (auto& p : pairs) { ticks.push_back(static_cast(p.second)); labels_owner.push_back(p.first); } for (auto& s : labels_owner) labels.push_back(s.c_str()); const float plot_h = ImGui::GetContentRegionAvail().y - 4.0f; if (ImPlot::BeginPlot("##dag_timeline", ImVec2(-1, plot_h), ImPlotFlags_NoTitle | ImPlotFlags_NoMouseText)) { ImPlot::SetupAxis(ImAxis_X1, "time", ImPlotAxisFlags_NoGridLines | ImPlotAxisFlags_NoHighlight); ImPlot::SetupAxisScale(ImAxis_X1, ImPlotScale_Time); ImPlot::SetupAxisLimits(ImAxis_X1, left, now + 10.0, ImPlotCond_Always); ImPlot::SetupAxis(ImAxis_Y1, "DAG", ImPlotAxisFlags_NoGridLines | ImPlotAxisFlags_NoHighlight); if (!ticks.empty()) { ImPlot::SetupAxisTicks(ImAxis_Y1, ticks.data(), static_cast(ticks.size()), labels.data()); ImPlot::SetupAxisLimits(ImAxis_Y1, -0.5, static_cast(ticks.size()) - 0.5, ImPlotCond_Always); } for (auto& kv : by_status) { if (kv.second.xs.empty()) continue; ImPlotSpec spec; spec.Marker = ImPlotMarker_Circle; spec.MarkerSize = 6.0f; ImVec4 col = color_for_status(kv.first); spec.MarkerFillColor = col; spec.MarkerLineColor = col; spec.LineColor = col; ImPlot::PlotScatter(kv.first.c_str(), kv.second.xs.data(), kv.second.ys.data(), static_cast(kv.second.xs.size()), spec); } // Hover tooltip: closest run. if (ImPlot::IsPlotHovered() && !runs_all.empty()) { const ImVec2 mp_px = ImGui::GetIO().MousePos; const double kHitRadiusPx = 12.0; double best_dist = kHitRadiusPx; const dag_ui::DagRunRow* best = nullptr; for (auto& r : runs_all) { if (!dag_y.count(r.dag_name)) continue; time_t t = parse_rfc3339(r.started_at); if (t == 0) continue; double x = static_cast(t); if (x < left || x > now + 10.0) continue; double y = static_cast(dag_y[r.dag_name]); ImVec2 px = ImPlot::PlotToPixels(x, y); double dx = px.x - mp_px.x; double dy = px.y - mp_px.y; double d = std::sqrt(dx*dx + dy*dy); if (d < best_dist) { best_dist = d; best = &r; } } if (best) { ImGui::BeginTooltip(); ImGui::TextColored(color_for_status(best->status), "%s", best->status.c_str()); ImGui::Text("%s", best->dag_name.c_str()); ImGui::Separator(); ImGui::Text("run id: %s", best->id.c_str()); ImGui::Text("started: %s", best->started_at.c_str()); if (!best->finished_at.empty()) ImGui::Text("finished: %s", best->finished_at.c_str()); ImGui::Text("trigger: %s", best->trigger.c_str()); if (!best->error.empty()) { ImGui::TextColored(ImVec4(1,0.4f,0.4f,1), "err: %s", best->error.c_str()); } ImGui::EndTooltip(); } } ImPlot::EndPlot(); } ImGui::End(); } // --------------------------------------------------------------------------- // Health panel // --------------------------------------------------------------------------- void draw_health(const std::string& /*api_url*/, const std::vector& runs_all) { if (!ImGui::Begin(TI_ACTIVITY " Health")) { ImGui::End(); return; } const time_t now = std::time(nullptr); const time_t cutoff_24h = now - 86400; int runs_24h = 0; int success_24h = 0; int failed_24h = 0; int cancelled_24h = 0; int pending_total = 0; int success_all = 0; int failed_all = 0; int cancelled_all = 0; for (auto& r : runs_all) { if (r.status == "pending" || r.status == "running") pending_total++; // success_rate computed across success+failed+cancelled (terminal states). if (r.status == "success") success_all++; if (r.status == "failed") failed_all++; if (r.status == "cancelled") cancelled_all++; time_t t = parse_rfc3339(r.started_at); if (t == 0) continue; if (t < cutoff_24h) continue; runs_24h++; if (r.status == "success") success_24h++; if (r.status == "failed") failed_24h++; if (r.status == "cancelled") cancelled_24h++; } int terminal_all = success_all + failed_all + cancelled_all; float success_rate = (terminal_all > 0) ? (100.0f * static_cast(success_all) / static_cast(terminal_all)) : 0.0f; if (runs_all.empty()) { empty_state(TI_ACTIVITY, "No runs yet", "Trigger a DAG to populate health metrics."); ImGui::End(); return; } // LAYOUT-TABLE — KPI/form/splitter, no data; keep BeginTable inline. if (ImGui::BeginTable("##health_kpis", 4, ImGuiTableFlags_Borders | ImGuiTableFlags_SizingStretchSame)) { ImGui::TableNextRow(); ImGui::TableNextColumn(); ImGui::Text("%s Runs (24h)", TI_ACTIVITY); ImGui::Text("%d", runs_24h); ImGui::TextDisabled("success: %d", success_24h); ImGui::TableNextColumn(); ImGui::Text("%s Success rate", TI_CHECK); ImGui::TextColored(ImVec4(0.30f, 0.85f, 0.40f, 1.0f), "%.1f%%", success_rate); ImGui::TextDisabled("%d / %d terminal", success_all, terminal_all); ImGui::TableNextColumn(); ImGui::Text("%s Failed (24h)", TI_ALERT_TRIANGLE); if (failed_24h > 0) { ImGui::TextColored(ImVec4(0.95f, 0.35f, 0.30f, 1.0f), "%d", failed_24h); } else { ImGui::Text("%d", failed_24h); } ImGui::TextDisabled("cancelled: %d", cancelled_24h); ImGui::TableNextColumn(); ImGui::Text("%s Pending/Running", TI_LOADER); if (pending_total > 0) { ImGui::TextColored(ImVec4(0.95f, 0.80f, 0.20f, 1.0f), "%d", pending_total); } else { ImGui::Text("%d", pending_total); } ImGui::TextDisabled("active now"); ImGui::EndTable(); } ImGui::Separator(); ImGui::TextDisabled("Computed client-side from %zu runs in cache.", runs_all.size()); ImGui::End(); } // --------------------------------------------------------------------------- // Function panel — sidebar con metadata del function_id seleccionado. // Lazy-load on click. Cada uses_functions[] es navegable (TreeNode click -> // recursive load). Boton Back consume el breadcrumb. // --------------------------------------------------------------------------- static BadgeVariant variant_for_purity(const std::string& p) { if (p == "pure") return BadgeVariant::Success; if (p == "impure") return BadgeVariant::Warning; return BadgeVariant::Default; } void draw_function_panel(const std::string& api_url, bool* p_open) { auto& fp = function_panel(); if (fp.selected_id.empty()) return; // panel oculto si no hay seleccion char title[512]; std::snprintf(title, sizeof(title), TI_FUNCTION " Function — %s###function_panel", fp.selected_id.c_str()); if (!ImGui::Begin(title, p_open)) { ImGui::End(); return; } // Lazy load. if (!fp.loaded && fp.load_error.empty()) { fp.cached = {}; if (dag_ui::get_function_http(api_url, fp.selected_id, fp.cached)) { fp.loaded = true; } else { fp.load_error = "Failed to fetch /api/functions/" + fp.selected_id; } } // Toolbar: Back + Close (clear) + Refresh bool has_history = !fp.breadcrumb.empty(); if (!has_history) ImGui::BeginDisabled(); if (ImGui::SmallButton(TI_ARROW_LEFT " Back")) { std::string prev = fp.breadcrumb.back(); fp.breadcrumb.pop_back(); fp.selected_id = prev; fp.loaded = false; fp.load_error.clear(); } if (!has_history) ImGui::EndDisabled(); ImGui::SameLine(); if (ImGui::SmallButton(TI_REFRESH " Reload##fn_panel")) { fp.loaded = false; fp.load_error.clear(); } ImGui::SameLine(); if (ImGui::SmallButton(TI_X " Close##fn_panel")) { fp.selected_id.clear(); fp.breadcrumb.clear(); fp.loaded = false; fp.load_error.clear(); ImGui::End(); return; } ImGui::Separator(); if (!fp.load_error.empty()) { ImGui::TextColored(ImVec4(1, 0.4f, 0.4f, 1), "%s", fp.load_error.c_str()); ImGui::End(); return; } if (!fp.loaded) { ImGui::TextDisabled("loading..."); ImGui::End(); return; } const auto& fn = fp.cached; // Header: id grande + 3 badges (domain / lang / purity) ImGui::TextUnformatted(fn.id.c_str()); if (!fn.domain.empty()) { badge(fn.domain.c_str(), BadgeVariant::Info); ImGui::SameLine(); } if (!fn.lang.empty()) { badge(fn.lang.c_str(), BadgeVariant::Default); ImGui::SameLine(); } if (!fn.purity.empty()) { badge(fn.purity.c_str(), variant_for_purity(fn.purity)); } ImGui::Spacing(); if (!fn.description.empty()) { ImGui::TextWrapped("%s", fn.description.c_str()); ImGui::Spacing(); } if (!fn.signature.empty()) { ImGui::TextDisabled("signature"); ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.08f, 0.08f, 0.08f, 1)); ImGui::BeginChild("##fn_sig", ImVec2(-1, ImGui::GetTextLineHeightWithSpacing() * 2.4f), false, ImGuiWindowFlags_HorizontalScrollbar); ImGui::TextUnformatted(fn.signature.c_str()); ImGui::EndChild(); ImGui::PopStyleColor(); } ImGui::Separator(); // uses_functions[] char hdr_fns[64]; std::snprintf(hdr_fns, sizeof(hdr_fns), TI_FUNCTION " Uses functions (%zu)###uses_fns", fn.uses_functions.size()); if (ImGui::CollapsingHeader(hdr_fns, ImGuiTreeNodeFlags_DefaultOpen)) { if (fn.uses_functions.empty()) { ImGui::TextDisabled(" (none)"); } else { for (size_t i = 0; i < fn.uses_functions.size(); i++) { const std::string& dep = fn.uses_functions[i]; ImGui::PushID(static_cast(i)); ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_Leaf | ImGuiTreeNodeFlags_NoTreePushOnOpen | ImGuiTreeNodeFlags_SpanAvailWidth; ImGui::TreeNodeEx(dep.c_str(), flags, "%s %s", TI_CODE, dep.c_str()); if (ImGui::IsItemClicked()) { if (!fp.selected_id.empty() && fp.selected_id != dep) { fp.breadcrumb.push_back(fp.selected_id); } fp.selected_id = dep; fp.loaded = false; fp.load_error.clear(); } ImGui::PopID(); } } } // uses_types[] char hdr_types[64]; std::snprintf(hdr_types, sizeof(hdr_types), TI_NETWORK " Uses types (%zu)###uses_types", fn.uses_types.size()); if (ImGui::CollapsingHeader(hdr_types)) { if (fn.uses_types.empty()) { ImGui::TextDisabled(" (none)"); } else { for (auto& t : fn.uses_types) ImGui::BulletText("%s", t.c_str()); } } ImGui::End(); } // --------------------------------------------------------------------------- // All Runs panel // --------------------------------------------------------------------------- static data_table::State g_st_all_runs; static std::vector g_back_all_runs; static std::vector g_ptrs_all_runs; void draw_all_runs(const std::string& /*api_url*/, const std::vector& runs_all) { if (!ImGui::Begin(TI_HISTORY " All Runs")) { ImGui::End(); return; } if (runs_all.empty()) { empty_state(TI_HISTORY, "No runs yet", "Lanza algun DAG desde DAG List para que aparezca aqui."); ImGui::End(); return; } // Sort by started_at desc (most recent first). Hacer copia para no mutar el cache. std::vector sorted; sorted.reserve(runs_all.size()); for (auto& r : runs_all) sorted.push_back(&r); std::sort(sorted.begin(), sorted.end(), [](const dag_ui::DagRunRow* a, const dag_ui::DagRunRow* b){ return a->started_at > b->started_at; }); data_table::TableInput ti; ti.name = "all_runs"; ti.headers = {"Run ID", "DAG", "Status", "Trigger", "Started", "Finished", "Duration"}; ti.types = { data_table::ColumnType::String, data_table::ColumnType::String, data_table::ColumnType::String, data_table::ColumnType::String, data_table::ColumnType::Date, data_table::ColumnType::Date, data_table::ColumnType::String, }; ti.rows = static_cast(sorted.size()); ti.cols = static_cast(ti.headers.size()); auto status_chips = [](){ std::vector rules; rules.push_back({"success", "#22c55e"}); rules.push_back({"failed", "#ef4444"}); rules.push_back({"running", "#eab308"}); rules.push_back({"pending", "#94a3b8"}); rules.push_back({"cancelled", "#6b7280"}); return rules; }; auto trigger_chips = [](){ std::vector rules; rules.push_back({"manual", "#3b82f6"}); rules.push_back({"cron", "#a855f7"}); rules.push_back({"api", "#06b6d4"}); return rules; }; ti.column_specs.resize(ti.cols); for (int i = 0; i < ti.cols; i++) ti.column_specs[i].id = ti.headers[i]; ti.column_specs[2].renderer = data_table::CellRenderer::CategoricalChip; ti.column_specs[2].chips = status_chips(); ti.column_specs[3].renderer = data_table::CellRenderer::CategoricalChip; ti.column_specs[3].chips = trigger_chips(); // Helper: duracion humana entre started_at y finished_at (best-effort). auto duration_str = [](const std::string& s, const std::string& f) -> std::string { if (s.empty() || f.empty()) return "-"; // Parse ISO 8601 minimalist (YYYY-MM-DDTHH:MM:SS). auto to_secs = [](const std::string& t) -> long long { int Y,M,D,h,mi,se; if (std::sscanf(t.c_str(), "%d-%d-%dT%d:%d:%d", &Y,&M,&D,&h,&mi,&se) != 6) return 0; std::tm tm = {}; tm.tm_year=Y-1900; tm.tm_mon=M-1; tm.tm_mday=D; tm.tm_hour=h; tm.tm_min=mi; tm.tm_sec=se; #ifdef _WIN32 return static_cast(_mkgmtime(&tm)); #else return static_cast(timegm(&tm)); #endif }; long long ss = to_secs(s), ff = to_secs(f); if (ss == 0 || ff == 0 || ff < ss) return "-"; long long dur = ff - ss; if (dur < 60) return std::to_string(dur) + "s"; if (dur < 3600) return std::to_string(dur/60) + "m " + std::to_string(dur%60) + "s"; return std::to_string(dur/3600) + "h " + std::to_string((dur%3600)/60) + "m"; }; g_back_all_runs.clear(); g_back_all_runs.reserve(sorted.size() * ti.cols); for (auto* r : sorted) { // Truncate run id for display (keep last 8 chars). std::string short_id = r->id; if (short_id.size() > 12) short_id = "..." + short_id.substr(short_id.size() - 8); g_back_all_runs.push_back(short_id); g_back_all_runs.push_back(r->dag_name); g_back_all_runs.push_back(r->status); g_back_all_runs.push_back(r->trigger); g_back_all_runs.push_back(r->started_at); g_back_all_runs.push_back(r->finished_at); g_back_all_runs.push_back(duration_str(r->started_at, r->finished_at)); } cells_to_ptrs(g_back_all_runs, g_ptrs_all_runs); ti.cells = g_ptrs_all_runs.data(); std::vector events; ImGui::BeginChild("##all_runs_wrap", ImVec2(-1, -1)); data_table::render("##dt_all_runs", {ti}, g_st_all_runs, &events); ImGui::EndChild(); for (auto& ev : events) { if (ev.kind == data_table::TableEventKind::RowDoubleClick && ev.row >= 0 && ev.row < static_cast(sorted.size())) { selection().run_id = sorted[ev.row]->id; selection().dag_name = sorted[ev.row]->dag_name; caches().run_detail_loaded = false; caches().dag_detail_loaded = false; } } ImGui::End(); } } // namespace dag_ui_tabs