From 4abc3f97ec4264edb049dc28cc4c29e105667670 Mon Sep 17 00:00:00 2001 From: Egutierrez Date: Sat, 16 May 2026 16:33:23 +0200 Subject: [PATCH] chore: auto-commit (8 archivos) - CMakeLists.txt - app.md - data_http.cpp - data_http.h - main.cpp - tabs.cpp - tabs.h - appicon.ico Co-Authored-By: Claude Opus 4.7 (1M context) --- CMakeLists.txt | 1 + app.md | 20 ++- appicon.ico | Bin 0 -> 8841 bytes data_http.cpp | 30 ++++ data_http.h | 20 +++ main.cpp | 36 +++- tabs.cpp | 480 ++++++++++++++++++++++++++++++++++++++++++++++--- tabs.h | 27 +++ 8 files changed, 586 insertions(+), 28 deletions(-) create mode 100644 appicon.ico diff --git a/CMakeLists.txt b/CMakeLists.txt index f2ede28..839e594 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -5,6 +5,7 @@ add_imgui_app(dag_engine_ui ws_client.cpp tabs.cpp ${CMAKE_SOURCE_DIR}/functions/core/empty_state.cpp + ${CMAKE_SOURCE_DIR}/functions/core/badge.cpp ) target_include_directories(dag_engine_ui PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) diff --git a/app.md b/app.md index a778cb4..0be1774 100644 --- a/app.md +++ b/app.md @@ -21,8 +21,26 @@ uses_functions: uses_types: [] framework: "imgui" entry_point: "main.cpp" -dir_path: "cpp/apps/dag_engine_ui" +dir_path: "apps/dag_engine_ui" repo_url: "https://gitea.organic-machine.com/dataforge/dag_engine_ui" +e2e_checks: + - id: build_cmake + cmd: "cmake --build cpp/build -j --target dag_engine_ui" + timeout_s: 300 + severity: critical + - id: binary_exists + cmd: "test -x cpp/build/linux/apps/dag_engine_ui/dag_engine_ui || test -x cpp/build/apps/dag_engine_ui/dag_engine_ui" + timeout_s: 5 + severity: critical + - id: self_test + cmd: "(cpp/build/linux/apps/dag_engine_ui/dag_engine_ui --self-test 2>&1 || cpp/build/apps/dag_engine_ui/dag_engine_ui --self-test 2>&1) | head -20" + timeout_s: 10 + expect_stdout_contains: "self-test" + severity: warning + - id: cpp_apps_conformance + cmd: "./fn doctor cpp-apps 2>&1 | grep -A1 dag_engine_ui || echo 'no issues'" + expect_stdout_contains: "no issues" + severity: critical --- # dag_engine_ui diff --git a/appicon.ico b/appicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..05cc7f1598e7ea249512e51cbd118fcdc73dc628 GIT binary patch literal 8841 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 literal 0 HcmV?d00001 diff --git a/data_http.cpp b/data_http.cpp index b6a0e5b..4e8ea21 100644 --- a/data_http.cpp +++ b/data_http.cpp @@ -73,6 +73,7 @@ static void parse_step(const json& j, DagStepRow& s) { s.finished_at = get_str(j, "finished_at"); s.duration_ms = get_int64(j, "duration_ms"); s.error = get_str(j, "error"); + s.function_id = get_str(j, "function_id"); } static void parse_dag_info(const json& j, DagInfo& d) { @@ -245,4 +246,33 @@ bool trigger_dag_http(const std::string& api_url, const std::string& name, return true; } +bool get_function_http(const std::string& api_url, + const std::string& function_id, + FnInfo& out) { + std::string host; + int port; + if (!parse_url(api_url, host, port)) return false; + if (function_id.empty()) return false; + HttpClient cli(host, port); + auto res = cli.get("/api/functions/" + function_id); + if (!res.ok()) { + fprintf(stderr, "[dag_http] get_function(%s) failed: status=%d\n", + function_id.c_str(), res.status); + return false; + } + auto j = json::parse(res.body, nullptr, false); + if (!j.is_object()) return false; + + out.id = get_str(j, "id"); + out.name = get_str(j, "name"); + out.description = get_str(j, "description"); + out.signature = get_str(j, "signature"); + out.purity = get_str(j, "purity"); + out.domain = get_str(j, "domain"); + out.lang = get_str(j, "lang"); + out.uses_functions = get_str_array(j, "uses_functions"); + out.uses_types = get_str_array(j, "uses_types"); + return true; +} + } // namespace dag_ui diff --git a/data_http.h b/data_http.h index f81e35f..43520c0 100644 --- a/data_http.h +++ b/data_http.h @@ -54,6 +54,20 @@ struct DagStepRow { std::string finished_at; long long duration_ms = 0; std::string error; + std::string function_id; // "" if not a function step +}; + +// Metadata de una funcion del registry (response shape de GET /api/functions/{id}). +struct FnInfo { + std::string id; + std::string name; + std::string description; + std::string signature; + std::string purity; // "pure" | "impure" + std::string domain; + std::string lang; + std::vector uses_functions; + std::vector uses_types; }; struct DagRunDetail { @@ -86,4 +100,10 @@ bool get_run_http(const std::string& api_url, const std::string& run_id, bool trigger_dag_http(const std::string& api_url, const std::string& name, std::string& out_run_id, std::string& out_error); +// GET /api/functions/{function_id} -> rellena out con metadata del registry. +// Devuelve false si red falla, status != 2xx, o JSON no parseable. +bool get_function_http(const std::string& api_url, + const std::string& function_id, + FnInfo& out); + } // namespace dag_ui diff --git a/main.cpp b/main.cpp index 57f15e3..7c9edf0 100644 --- a/main.cpp +++ b/main.cpp @@ -4,10 +4,13 @@ #include "core/icons_tabler.h" #include "core/logger.h" #include "data_http.h" +#include "http_client.h" #include "ws_client.h" #include "tabs.h" #include "vendor/nlohmann/json.hpp" +#include +#include #include #include @@ -48,6 +51,9 @@ static bool g_show_dag_list = true; static bool g_show_dag_detail = true; static bool g_show_run_detail = true; static bool g_show_timeline = true; +static bool g_show_all_runs = true; +static bool g_show_health = true; +static bool g_show_function_panel = true; // Auto-fetch DAG list una vez al arrancar. static bool g_initial_fetched = false; @@ -171,11 +177,36 @@ static void render() { if (g_show_dag_detail) dag_ui_tabs::draw_dag_detail(g_api_url); if (g_show_run_detail) dag_ui_tabs::draw_run_detail(g_api_url); if (g_show_timeline) dag_ui_tabs::draw_timeline(g_api_url, g_runs_all); + if (g_show_all_runs) dag_ui_tabs::draw_all_runs(g_api_url, g_runs_all); + if (g_show_health) dag_ui_tabs::draw_health(g_api_url, g_runs_all); if (g_show_main) draw_main(); if (g_show_live) draw_live(); + if (g_show_function_panel) dag_ui_tabs::draw_function_panel(g_api_url, &g_show_function_panel); } -int main(int /*argc*/, char** /*argv*/) { +// Self-test: blocking HTTP GET to the dag_engine backend, no GUI. +// Returns 0 if reachable (any 2xx), 1 otherwise. +static int run_self_test() { + HttpClient client(g_ws_host, g_ws_port); + // Probe /api/dags as a sucedaneo de /health (no dedicated /health helper). + HttpResponse resp = client.get("/api/dags"); + if (resp.ok()) { + std::printf("self-test ok: dag_engine reachable at %s\n", g_api_url.c_str()); + return 0; + } + std::printf("self-test fail: dag_engine unreachable at %s (status=%d)\n", + g_api_url.c_str(), resp.status); + return 1; +} + +int main(int argc, char** argv) { + // CLI flag --self-test: probe backend and exit without opening GUI. + for (int i = 1; i < argc; i++) { + if (argv[i] && std::strcmp(argv[i], "--self-test") == 0) { + return run_self_test(); + } + } + // Conecta WS al backend dag_engine. Reconnect con backoff lo gestiona WsClient. g_ws.start(g_ws_host, g_ws_port, g_ws_path); @@ -184,6 +215,9 @@ int main(int /*argc*/, char** /*argv*/) { { "DAG Detail", nullptr, &g_show_dag_detail }, { "Run Detail", nullptr, &g_show_run_detail }, { "Timeline", nullptr, &g_show_timeline }, + { "All Runs", nullptr, &g_show_all_runs }, + { "Health", nullptr, &g_show_health }, + { "Function", nullptr, &g_show_function_panel }, { "Live (WS)", nullptr, &g_show_live }, { "Main (diag)", nullptr, &g_show_main }, }; diff --git a/tabs.cpp b/tabs.cpp index 1008263..245a077 100644 --- a/tabs.cpp +++ b/tabs.cpp @@ -3,6 +3,7 @@ #include "core/data_table_types.h" #include "core/icons_tabler.h" #include "core/empty_state.h" +#include "core/badge.h" #include #include @@ -28,6 +29,11 @@ Caches& caches() { 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; @@ -355,33 +361,84 @@ void draw_run_detail(const std::string& api_url) { return; } - data_table::TableInput ti; - ti.name = "steps"; - ti.headers = {"Step", "Status", "Exit", "Duration", "Started"}; - ti.types = { - data_table::ColumnType::String, - data_table::ColumnType::String, - data_table::ColumnType::Int, - data_table::ColumnType::String, - data_table::ColumnType::Date, - }; - ti.rows = static_cast(steps.size()); - ti.cols = static_cast(ti.headers.size()); - - g_back_run_steps.clear(); - g_back_run_steps.reserve(steps.size() * ti.cols); - for (auto& s : steps) { - g_back_run_steps.push_back(s.step_name); - 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); - } - cells_to_ptrs(g_back_run_steps, g_ptrs_run_steps); - ti.cells = g_ptrs_run_steps.data(); - + // Steps table — render nativo (ImGui::BeginTable) en vez de data_table::render + // para soportar la columna "Function" clickable (badge -> abre Function panel). + // Status sigue mostrando badge coloreado por tipo. ImGui::BeginChild("##run_steps_wrap", ImVec2(-1, ImGui::GetContentRegionAvail().y * 0.5f)); - data_table::render("##dt_run_steps", {ti}, g_st_run_steps); + const ImGuiTableFlags steps_flags = + ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | + ImGuiTableFlags_Resizable | ImGuiTableFlags_SizingStretchProp | + ImGuiTableFlags_ScrollY; + if (ImGui::BeginTable("##dt_run_steps", 6, steps_flags)) { + ImGui::TableSetupScrollFreeze(0, 1); + ImGui::TableSetupColumn("Step", ImGuiTableColumnFlags_WidthStretch, 1.6f); + ImGui::TableSetupColumn("Function", ImGuiTableColumnFlags_WidthStretch, 2.2f); + ImGui::TableSetupColumn("Status", ImGuiTableColumnFlags_WidthStretch, 0.8f); + ImGui::TableSetupColumn("Exit", ImGuiTableColumnFlags_WidthStretch, 0.4f); + ImGui::TableSetupColumn("Duration", ImGuiTableColumnFlags_WidthStretch, 0.7f); + ImGui::TableSetupColumn("Started", ImGuiTableColumnFlags_WidthStretch, 1.2f); + ImGui::TableHeadersRow(); + + for (size_t i = 0; i < steps.size(); i++) { + auto& s = steps[i]; + ImGui::TableNextRow(); + + // Step name + ImGui::TableSetColumnIndex(0); + ImGui::TextUnformatted(s.step_name.c_str()); + + // Function — badge clickable o "(shell)" + ImGui::TableSetColumnIndex(1); + if (!s.function_id.empty()) { + ImGui::PushID(static_cast(i)); + // Small button styled like a badge (registry green). + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.13f, 0.55f, 0.30f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.18f, 0.65f, 0.38f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.10f, 0.45f, 0.25f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1, 1, 1, 1)); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(6, 1)); + char btn[512]; + std::snprintf(btn, sizeof(btn), "%s %s", TI_FUNCTION, s.function_id.c_str()); + if (ImGui::SmallButton(btn)) { + auto& fp = function_panel(); + if (!fp.selected_id.empty() && fp.selected_id != s.function_id) { + fp.breadcrumb.push_back(fp.selected_id); + } + fp.selected_id = s.function_id; + fp.loaded = false; + fp.load_error.clear(); + } + ImGui::PopStyleVar(); + ImGui::PopStyleColor(4); + ImGui::PopID(); + } else { + ImGui::TextDisabled("(shell)"); + } + + // Status badge + ImGui::TableSetColumnIndex(2); + BadgeVariant v = BadgeVariant::Default; + if (s.status == "success") v = BadgeVariant::Success; + else if (s.status == "failed") v = BadgeVariant::Error; + else if (s.status == "running") v = BadgeVariant::Warning; + else if (s.status == "cancelled") v = BadgeVariant::Default; + else if (s.status == "pending") v = BadgeVariant::Info; + badge(s.status.c_str(), v); + + // Exit + ImGui::TableSetColumnIndex(3); + ImGui::Text("%d", s.exit_code); + + // Duration + ImGui::TableSetColumnIndex(4); + ImGui::TextUnformatted(format_duration(s.duration_ms).c_str()); + + // Started + ImGui::TableSetColumnIndex(5); + ImGui::TextUnformatted(s.started_at.c_str()); + } + ImGui::EndTable(); + } ImGui::EndChild(); // stdout/stderr expandible por step. @@ -607,4 +664,375 @@ void draw_timeline(const std::string& api_url, 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; + } + + 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_badges = [](){ + 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_badges = [](){ + 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::Badge; + ti.column_specs[2].badges = status_badges(); + ti.column_specs[3].renderer = data_table::CellRenderer::Badge; + ti.column_specs[3].badges = trigger_badges(); + + // 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 diff --git a/tabs.h b/tabs.h index ec4c6a1..ba193ae 100644 --- a/tabs.h +++ b/tabs.h @@ -32,6 +32,19 @@ struct Caches { Caches& caches(); +// Estado del panel lateral "Function" — registry metadata para el function_id +// seleccionado actualmente. selected_id == "" -> panel oculto. breadcrumb mantiene +// el historial de navegacion para soportar el boton Back. +struct FunctionPanelState { + std::string selected_id; // "" = panel oculto + dag_ui::FnInfo cached; + bool loaded = false; + std::string load_error; + std::vector breadcrumb; // ids visitados antes del actual +}; + +FunctionPanelState& function_panel(); + // Render cada tab. api_url es el endpoint dag_engine. // `live_runs` es el cache global mantenido por WS (sirve para DAG List status). void draw_dag_list(const std::string& api_url, @@ -47,4 +60,18 @@ void draw_run_detail(const std::string& api_url); void draw_timeline(const std::string& api_url, const std::vector& runs_all); +// Health panel: KPIs derivados de runs_all (client-side). +// runs_24h, success_rate, failed_runs_24h, pending_runs. +void draw_health(const std::string& api_url, + const std::vector& runs_all); + +// All Runs panel: historico completo de runs (todas las DAGs). Tabla +// ordenada por started_at desc. Click row -> set selection().run_id. +void draw_all_runs(const std::string& api_url, + const std::vector& runs_all); + +// Function panel: detalle de la funcion del registry seleccionada (id, domain, +// purity, signature, uses_functions[], uses_types[]). Lazy-load por click. +void draw_function_panel(const std::string& api_url, bool* p_open); + } // namespace dag_ui_tabs