feat: wire sse_client_cpp_core for live updates from /api/boards/issues/stream

This commit is contained in:
agent
2026-05-18 20:05:14 +02:00
parent 264c5939f3
commit 4f5e9f6fbe
16 changed files with 726 additions and 44 deletions
+67 -21
View File
@@ -4,17 +4,27 @@
#include <imgui.h>
#include <ctime>
#include <thread>
#include <mutex>
namespace kanban_cpp {
void refresh_data(AppState& s) {
std::string err;
s.cards = list_cards(s.cfg, err);
if (!err.empty()) s.last_refresh_error = "cards: " + err;
s.columns = list_columns(s.cfg, err);
if (!err.empty()) s.last_refresh_error += " columns: " + err;
s.backend_ok = health(s.cfg);
s.last_refresh_ts = std::time(nullptr);
auto cards = list_cards(s.cfg, err);
std::string err_cards = err; err.clear();
auto columns = list_columns(s.cfg, err);
std::string err_cols = err;
bool ok = health(s.cfg);
int64_t ts = std::time(nullptr);
std::lock_guard<std::mutex> lock(s.mu);
s.cards = std::move(cards);
s.columns = std::move(columns);
s.last_refresh_error.clear();
if (!err_cards.empty()) s.last_refresh_error = "cards: " + err_cards;
if (!err_cols.empty()) s.last_refresh_error += " columns: " + err_cols;
s.backend_ok = ok;
s.last_refresh_ts = ts;
}
void draw_board(AppState& s, bool* p_open) {
@@ -23,22 +33,53 @@ void draw_board(AppState& s, bool* p_open) {
return;
}
// Toolbar
if (ImGui::Button(TI_REFRESH " Refresh")) refresh_data(s);
// Snapshot bajo lock — refresh corre en thread separado.
std::vector<Card> cards_snap;
std::vector<Column> cols_snap;
bool backend_ok_snap;
std::string err_snap;
std::string sse_snap;
{
std::lock_guard<std::mutex> lock(s.mu);
cards_snap = s.cards;
cols_snap = s.columns;
backend_ok_snap = s.backend_ok;
err_snap = s.last_refresh_error;
sse_snap = s.sse_status;
}
// Toolbar — refresh corre en thread separado (no bloquea frame).
if (ImGui::Button(TI_REFRESH " Refresh")) {
std::thread([&s](){ refresh_data(s); }).detach();
}
ImGui::SameLine();
if (s.backend_ok) {
if (backend_ok_snap) {
ImGui::TextColored(ImVec4(0.4f, 0.85f, 0.4f, 1.0f), TI_CHECK " backend :8403");
} else {
ImGui::TextColored(ImVec4(0.85f, 0.4f, 0.4f, 1.0f), TI_ALERT_TRIANGLE " backend offline (:8403)");
}
if (!s.last_refresh_error.empty()) {
// SSE live badge — refleja el estado del stream push del backend.
ImGui::SameLine();
if (sse_snap == "connected") {
ImGui::TextColored(ImVec4(0.4f, 0.85f, 0.4f, 1.0f), TI_BROADCAST " live");
} else if (sse_snap == "connecting") {
ImGui::TextColored(ImVec4(0.85f, 0.85f, 0.4f, 1.0f), TI_LOADER " connecting");
} else if (sse_snap == "disconnected") {
ImGui::TextColored(ImVec4(0.85f, 0.4f, 0.4f, 1.0f), TI_PLUG_CONNECTED_X " disconnected");
} else {
// "error: <msg>" o cualquier otro string
ImGui::TextColored(ImVec4(0.85f, 0.4f, 0.4f, 1.0f), TI_PLUG_CONNECTED_X " %s", sse_snap.c_str());
}
if (!err_snap.empty()) {
ImGui::SameLine();
ImGui::TextColored(ImVec4(0.85f, 0.6f, 0.2f, 1.0f), "%s", s.last_refresh_error.c_str());
ImGui::TextColored(ImVec4(0.85f, 0.6f, 0.2f, 1.0f), "%s", err_snap.c_str());
}
ImGui::Separator();
// Empty state
if (s.columns.empty()) {
if (cols_snap.empty()) {
ImGui::TextDisabled("No columns yet. Pulsa Refresh o lanza el backend en :8403.");
ImGui::End();
return;
@@ -48,19 +89,19 @@ void draw_board(AppState& s, bool* p_open) {
const float col_w = 280.0f;
if (ImGui::BeginChild("##board_scroll", ImVec2(0, 0), false,
ImGuiWindowFlags_HorizontalScrollbar)) {
for (size_t ci = 0; ci < s.columns.size(); ++ci) {
const auto& col = s.columns[ci];
for (size_t ci = 0; ci < cols_snap.size(); ++ci) {
const auto& col = cols_snap[ci];
ImGui::SameLine();
ImGui::BeginChild((std::string("##col_") + col.id).c_str(),
ImVec2(col_w, 0), true);
ImGui::TextUnformatted(col.name.c_str());
ImGui::SameLine();
int count = 0;
for (const auto& c : s.cards) if (c.column_id == col.id) ++count;
for (const auto& c : cards_snap) if (c.column_id == col.id) ++count;
ImGui::TextDisabled("(%d)", count);
ImGui::Separator();
for (const auto& card : s.cards) {
for (const auto& card : cards_snap) {
if (card.column_id != col.id) continue;
ImGui::PushID(card.id.c_str());
ImGui::BeginChild("##card", ImVec2(0, 70), true,
@@ -83,13 +124,18 @@ void draw_board(AppState& s, bool* p_open) {
}
if (ImGui::BeginPopup("##card_ctx")) {
ImGui::TextDisabled("Move to:");
for (const auto& tgt : s.columns) {
for (const auto& tgt : cols_snap) {
if (tgt.id == card.column_id) continue;
if (ImGui::MenuItem(tgt.name.c_str())) {
std::string err;
if (!move_card(s.cfg, card.id, tgt.id, err))
s.last_refresh_error = "move: " + err;
else refresh_data(s);
std::thread([&s, card_id=card.id, tgt_id=tgt.id](){
std::string err;
if (!move_card(s.cfg, card_id, tgt_id, err)) {
std::lock_guard<std::mutex> lock(s.mu);
s.last_refresh_error = "move: " + err;
return;
}
refresh_data(s);
}).detach();
}
}
ImGui::Separator();