feat: panel Timeline scatter (X=tiempo, Y=DAG, color=status) via ImPlot

- tabs.cpp draw_timeline: scatter ImPlot con eje X tiempo (UseLocalTime),
  eje Y categorico DAG (SetupAxisTicks), 1 serie por status con color
  consistente (verde/rojo/amarillo/gris).
- Combo ventana: 15m/1h/6h/24h/7d. Default 24h.
- Hover tooltip: punto mas cercano en pixel-space -> muestra status,
  dag, run id, started/finished, trigger, error.
- main.cpp: g_runs_all cache. Snapshot inicial via list_runs_http(limit=200)
  + upserts desde WS deltas. Auto-refresh por g_refresh_pending.
- Panel toggle "Timeline" en el menu View.
- Helper parse_rfc3339 inline (ignora offset, asume hora local — coherente
  con ImPlot::UseLocalTime).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-15 17:14:11 +02:00
parent 49fc908fb4
commit 4cb36a92e8
3 changed files with 223 additions and 0 deletions
+21
View File
@@ -22,11 +22,23 @@ static std::string g_ws_path = "/api/ws/dagruns";
// Cache en memoria del primer fetch + ultimos eventos WS.
static std::vector<dag_ui::DagInfo> g_dags;
static std::vector<dag_ui::DagRunRow> g_live_runs; // upsert por id desde WS
static std::vector<dag_ui::DagRunRow> g_runs_all; // cache para Timeline (snapshot REST + WS upsert)
static long long g_ws_runs_wm = 0;
static long long g_ws_steps_wm = 0;
static int g_ws_msg_count = 0;
static std::string g_last_error;
// Upsert por id en g_runs_all. Mantiene mas reciente al frente.
static void upsert_run_in_all(const dag_ui::DagRunRow& r) {
for (auto& existing : g_runs_all) {
if (existing.id == r.id) {
existing = r;
return;
}
}
g_runs_all.push_back(r);
}
static WsClient g_ws;
// Toggles de paneles (visibles desde el menu View del menubar canonico)
@@ -35,6 +47,7 @@ static bool g_show_live = true;
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;
// Auto-fetch DAG list una vez al arrancar.
static bool g_initial_fetched = false;
@@ -75,6 +88,7 @@ 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);
upsert_run_in_all(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" ||
@@ -139,6 +153,11 @@ static void render() {
g_initial_fetched = true;
g_refresh_pending = false;
dag_ui::list_dags_http(g_api_url, g_dags);
// Tambien snapshot inicial / refresh de runs para Timeline.
std::vector<dag_ui::DagRunRow> tmp;
if (dag_ui::list_runs_http(g_api_url, "", 200, tmp)) {
for (auto& r : tmp) upsert_run_in_all(r);
}
}
// Drain WS messages this frame (cheap, max 64).
@@ -151,6 +170,7 @@ static void render() {
if (g_show_dag_list) dag_ui_tabs::draw_dag_list(g_api_url, g_dags, g_live_runs);
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_main) draw_main();
if (g_show_live) draw_live();
}
@@ -163,6 +183,7 @@ int main(int /*argc*/, char** /*argv*/) {
{ "DAGs", nullptr, &g_show_dag_list },
{ "DAG Detail", nullptr, &g_show_dag_detail },
{ "Run Detail", nullptr, &g_show_run_detail },
{ "Timeline", nullptr, &g_show_timeline },
{ "Live (WS)", nullptr, &g_show_live },
{ "Main (diag)", nullptr, &g_show_main },
};