feat: cliente WebSocket + panel Live (issue 0095 step 4)
- ws_client.{h,cpp}: copia de registry_dashboard (RFC 6455 manual sobre TCP, sin TLS).
Background thread, reconnect con backoff, drain por frame.
- main.cpp: arranca WsClient apuntando a /api/ws/dagruns. Drain por frame.
Parse JSON snapshot/delta -> upsert g_live_runs por id.
Panel "Live (WS)" muestra estado conexion, watermarks runs/steps, lista live runs.
- CMakeLists.txt: ws_client.cpp en sources.
Build verificado. Tabs DAG List/Detail/Run Detail con data_table::render() en commits siguientes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -4,19 +4,67 @@
|
||||
#include "core/icons_tabler.h"
|
||||
#include "core/logger.h"
|
||||
#include "data_http.h"
|
||||
#include "ws_client.h"
|
||||
#include "vendor/nlohmann/json.hpp"
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
// Config global del backend HTTP. Persistido por imgui.ini via Mantine-no-go.
|
||||
static std::string g_api_url = "http://127.0.0.1:8090";
|
||||
using json = nlohmann::json;
|
||||
|
||||
// Cache en memoria del primer fetch. Tabs proximos updates via WS.
|
||||
static std::vector<dag_ui::DagInfo> g_dags;
|
||||
static std::string g_last_error;
|
||||
// Config global del backend HTTP.
|
||||
static std::string g_api_url = "http://127.0.0.1:8090";
|
||||
static std::string g_ws_host = "127.0.0.1";
|
||||
static int g_ws_port = 8090;
|
||||
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 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;
|
||||
|
||||
static WsClient g_ws;
|
||||
|
||||
// Toggles de paneles (visibles desde el menu View del menubar canonico)
|
||||
static bool g_show_main = true;
|
||||
static bool g_show_live = true;
|
||||
|
||||
// Upsert por id en g_live_runs.
|
||||
static void upsert_live_run(const dag_ui::DagRunRow& r) {
|
||||
for (auto& existing : g_live_runs) {
|
||||
if (existing.id == r.id) {
|
||||
existing = r;
|
||||
return;
|
||||
}
|
||||
}
|
||||
g_live_runs.push_back(r);
|
||||
}
|
||||
|
||||
static void parse_ws_payload(const std::string& payload) {
|
||||
auto j = json::parse(payload, nullptr, false);
|
||||
if (!j.is_object()) return;
|
||||
g_ws_msg_count++;
|
||||
if (j.contains("watermark") && j["watermark"].is_object()) {
|
||||
if (j["watermark"].contains("runs")) g_ws_runs_wm = j["watermark"]["runs"].get<long long>();
|
||||
if (j["watermark"].contains("steps")) g_ws_steps_wm = j["watermark"]["steps"].get<long long>();
|
||||
}
|
||||
if (j.contains("runs") && j["runs"].is_array()) {
|
||||
for (auto& rj : j["runs"]) {
|
||||
dag_ui::DagRunRow r;
|
||||
r.id = rj.value("id", "");
|
||||
r.dag_name = rj.value("dag_name", "");
|
||||
r.status = rj.value("status", "");
|
||||
r.trigger = rj.value("trigger", "");
|
||||
r.started_at = rj.value("started_at", "");
|
||||
r.finished_at = rj.value("finished_at", "");
|
||||
r.error = rj.value("error", "");
|
||||
upsert_live_run(r);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void draw_main() {
|
||||
if (!ImGui::Begin(TI_HOME " Main", &g_show_main)) {
|
||||
@@ -45,21 +93,46 @@ static void draw_main() {
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
static void render() {
|
||||
// El framework dibuja menubar (View/Layouts/Settings/About) y un
|
||||
// DockSpaceOverViewport central (auto_dockspace=true por defecto).
|
||||
// Aqui solo se dibujan los paneles propios de la app.
|
||||
if (g_show_main) draw_main();
|
||||
static void draw_live() {
|
||||
if (!ImGui::Begin(TI_BOLT " Live (WS)", &g_show_live)) {
|
||||
ImGui::End();
|
||||
return;
|
||||
}
|
||||
bool connected = g_ws.is_connected();
|
||||
ImGui::TextColored(connected ? ImVec4(0.3f, 0.9f, 0.3f, 1) : ImVec4(0.9f, 0.4f, 0.4f, 1),
|
||||
"%s", connected ? "connected" : "disconnected");
|
||||
ImGui::SameLine();
|
||||
ImGui::Text("| msgs=%d | wm runs=%lld steps=%lld",
|
||||
g_ws_msg_count, g_ws_runs_wm, g_ws_steps_wm);
|
||||
ImGui::Separator();
|
||||
ImGui::Text("Live runs: %zu", g_live_runs.size());
|
||||
for (auto& r : g_live_runs) {
|
||||
ImGui::BulletText("%s [%s] %s @ %s",
|
||||
r.dag_name.c_str(), r.status.c_str(),
|
||||
r.id.c_str(), r.started_at.c_str());
|
||||
}
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
// === Data panel (uncomment to enable) ===
|
||||
// static data_table::State data_state;
|
||||
// static std::vector<data_table::TableInput> data_tables; // populate from your source
|
||||
// data_table::render("main_data", data_tables, data_state);
|
||||
static void render() {
|
||||
// Drain WS messages this frame (cheap, max 64).
|
||||
{
|
||||
std::vector<std::string> msgs;
|
||||
g_ws.drain(msgs, 64);
|
||||
for (auto& m : msgs) parse_ws_payload(m);
|
||||
}
|
||||
|
||||
if (g_show_main) draw_main();
|
||||
if (g_show_live) draw_live();
|
||||
}
|
||||
|
||||
int main(int /*argc*/, char** /*argv*/) {
|
||||
// Conecta WS al backend dag_engine. Reconnect con backoff lo gestiona WsClient.
|
||||
g_ws.start(g_ws_host, g_ws_port, g_ws_path);
|
||||
|
||||
static fn_ui::PanelToggle panels[] = {
|
||||
{ "Main", nullptr, &g_show_main },
|
||||
{ "Live (WS)", nullptr, &g_show_live },
|
||||
};
|
||||
|
||||
fn::AppConfig cfg;
|
||||
|
||||
Reference in New Issue
Block a user