From 49fc908fb4d07361f63715de800a1a4044851b4e Mon Sep 17 00:00:00 2001 From: Egutierrez Date: Fri, 15 May 2026 17:07:09 +0200 Subject: [PATCH] feat: 5 puntitos de status en DAG List (R1..R5 columnas Badge) - data_http: parsea last_runs[] del /api/dags y guarda d.last_runs_status (max 5, mas reciente primero). - tabs.cpp DAG List: 5 columnas R1..R5 con CellRenderer::Badge + BadgeRule por status (success=verde, failed=rojo, running=amarillo, pending/cancelled=gris, "-"=tenue). - main.cpp: g_refresh_pending. WS auto-trigger refresh /api/dags cuando ve un run con status terminal -> last_runs se actualiza sin pulsar nada. - main + tabs: extern "C" dag_list_request_refresh() para el boton Refresh manual. Co-Authored-By: Claude Opus 4.7 (1M context) --- data_http.cpp | 5 +++++ data_http.h | 5 +++++ main.cpp | 17 ++++++++++++-- tabs.cpp | 61 +++++++++++++++++++++++++++++++++++++++++++-------- 4 files changed, 77 insertions(+), 11 deletions(-) diff --git a/data_http.cpp b/data_http.cpp index 9a9f3c0..b6a0e5b 100644 --- a/data_http.cpp +++ b/data_http.cpp @@ -94,6 +94,11 @@ static void parse_dag_info(const json& j, DagInfo& d) { d.last_run_started_at = lr.started_at; d.last_run_finished_at = lr.finished_at; } + if (j.contains("last_runs") && j["last_runs"].is_array()) { + for (auto& r : j["last_runs"]) { + d.last_runs_status.push_back(get_str(r, "status")); + } + } } bool list_dags_http(const std::string& api_url, std::vector& out) { diff --git a/data_http.h b/data_http.h index 8d8d8ff..f81e35f 100644 --- a/data_http.h +++ b/data_http.h @@ -10,6 +10,8 @@ namespace dag_ui { // --- Modelo de datos (mirror JSON shape de apps/dag_engine) --- +struct DagRunRow; // fwd + struct DagInfo { std::string name; std::string description; @@ -24,6 +26,9 @@ struct DagInfo { std::string last_run_status; std::string last_run_started_at; std::string last_run_finished_at; + // last_runs[] (max 5, most recent first). Empty if no runs yet. + // Populated from /api/dags `last_runs` array. + std::vector last_runs_status; // parallel: status of each (size <= 5) }; struct DagRunRow { diff --git a/main.cpp b/main.cpp index fe55807..1c7daf8 100644 --- a/main.cpp +++ b/main.cpp @@ -38,6 +38,12 @@ static bool g_show_run_detail = true; // Auto-fetch DAG list una vez al arrancar. static bool g_initial_fetched = false; +// Flag set by tabs::draw_dag_list cuando el usuario pulsa Refresh, o cuando +// WS notifica que un run termino (status != running) — re-fetch /api/dags +// para actualizar last_runs. +static bool g_refresh_pending = false; + +extern "C" void dag_list_request_refresh() { g_refresh_pending = true; } // Upsert por id en g_live_runs. static void upsert_live_run(const dag_ui::DagRunRow& r) { @@ -69,6 +75,12 @@ static void parse_ws_payload(const std::string& payload) { r.finished_at = rj.value("finished_at", ""); r.error = rj.value("error", ""); upsert_live_run(r); + // Cuando un run termina, refresca DAG List para que last_runs + // refleje la nueva ejecucion en R1..R5. + if (r.status == "success" || r.status == "failed" || + r.status == "cancelled") { + g_refresh_pending = true; + } } } } @@ -122,9 +134,10 @@ static void draw_live() { } static void render() { - // Auto-fetch DAGs on first frame. - if (!g_initial_fetched) { + // Auto-fetch DAGs on first frame or on explicit refresh. + if (!g_initial_fetched || g_refresh_pending) { g_initial_fetched = true; + g_refresh_pending = false; dag_ui::list_dags_http(g_api_url, g_dags); } diff --git a/tabs.cpp b/tabs.cpp index fcb8edf..4d3e302 100644 --- a/tabs.cpp +++ b/tabs.cpp @@ -75,6 +75,9 @@ static std::string status_for_dag(const std::string& dag_name, // 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) @@ -84,6 +87,12 @@ void draw_dag_list(const std::string& api_url, return; } + if (ImGui::Button(TI_REFRESH " Refresh##dag_list")) { + dag_list_request_refresh(); + } + ImGui::SameLine(); + ImGui::TextDisabled("Double-click row -> inspect. R1..R5 = last 5 runs."); + if (dags.empty()) { empty_state("( no DAGs )", "Empty registry", "Place a YAML in apps/dag_engine/dags_migrated/ and reload the server."); @@ -91,27 +100,62 @@ void draw_dag_list(const std::string& api_url, return; } - // Build TableInput + // Build TableInput. Columnas R1..R5 muestran status de las ultimas 5 runs + // como badges coloreadas (verde/rojo/amarillo/gris) — issue 0095. data_table::TableInput ti; ti.name = "dags"; - ti.headers = {"Name", "Schedule", "Last Status", "Tags", "Valid", "File"}; + ti.headers = {"Name", "Schedule", "Last Status", + "R1", "R2", "R3", "R4", "R5", + "Tags", "Valid"}; ti.types = { - data_table::ColumnType::String, - data_table::ColumnType::String, - data_table::ColumnType::String, - data_table::ColumnType::String, - data_table::ColumnType::String, - data_table::ColumnType::String, + data_table::ColumnType::String, // Name + data_table::ColumnType::String, // Schedule + data_table::ColumnType::String, // Last Status + data_table::ColumnType::String, // R1 + data_table::ColumnType::String, // R2 + data_table::ColumnType::String, // R3 + data_table::ColumnType::String, // R4 + data_table::ColumnType::String, // R5 + data_table::ColumnType::String, // Tags + data_table::ColumnType::String, // Valid }; ti.rows = static_cast(dags.size()); ti.cols = static_cast(ti.headers.size()); + // BadgeRule por status: misma config para R1..R5. + 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 + rules.push_back({"-", "#1f2937", "·"}); // dot tenue cuando no hay run + return rules; + }; + + // ColumnSpec por columna. Solo R1..R5 (indices 3..7) son Badge. + ti.column_specs.resize(ti.cols); + for (int i = 0; i < ti.cols; i++) ti.column_specs[i].id = ti.headers[i]; + for (int i = 3; i <= 7; i++) { + ti.column_specs[i].renderer = data_table::CellRenderer::Badge; + ti.column_specs[i].badges = run_status_badges(); + } + 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)); + // R1..R5 — most recent first; "-" si menos de 5 runs. + for (int i = 0; i < 5; i++) { + if (i < static_cast(d.last_runs_status.size())) { + g_back_dag_list.push_back(d.last_runs_status[i]); + } else { + g_back_dag_list.push_back("-"); + } + } std::string tags_csv; for (size_t i = 0; i < d.tags.size(); i++) { if (i) tags_csv += ","; @@ -119,7 +163,6 @@ void draw_dag_list(const std::string& api_url, } g_back_dag_list.push_back(tags_csv); g_back_dag_list.push_back(d.valid ? "yes" : "no"); - g_back_dag_list.push_back(d.file_path); } cells_to_ptrs(g_back_dag_list, g_ptrs_dag_list); ti.cells = g_ptrs_dag_list.data();