// panel_board.cpp — columns + cards Kanban panel. #include "panels.h" #include "core/icons_tabler.h" #include #include #include #include namespace kanban_cpp { void refresh_data(AppState& s) { std::string err; 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 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) { if (!ImGui::Begin(TI_LAYOUT_KANBAN " Board", p_open)) { ImGui::End(); return; } // Snapshot bajo lock — refresh corre en thread separado. std::vector cards_snap; std::vector cols_snap; bool backend_ok_snap; std::string err_snap; std::string sse_snap; { std::lock_guard 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 (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)"); } // 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: " 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", err_snap.c_str()); } ImGui::Separator(); // Empty state if (cols_snap.empty()) { ImGui::TextDisabled("No columns yet. Pulsa Refresh o lanza el backend en :8403."); ImGui::End(); return; } // Render columns left-to-right const float col_w = 280.0f; if (ImGui::BeginChild("##board_scroll", ImVec2(0, 0), false, ImGuiWindowFlags_HorizontalScrollbar)) { 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 : cards_snap) if (c.column_id == col.id) ++count; ImGui::TextDisabled("(%d)", count); ImGui::Separator(); 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, ImGuiWindowFlags_NoScrollbar); ImGui::TextUnformatted(card.title.c_str()); if (!card.priority.empty()) { ImVec4 col_p(0.6f, 0.6f, 0.6f, 1); if (card.priority == "high") col_p = {0.95f, 0.55f, 0.2f, 1}; else if (card.priority == "critical") col_p = {0.95f, 0.25f, 0.25f, 1}; else if (card.priority == "low") col_p = {0.45f, 0.7f, 0.95f, 1}; ImGui::TextColored(col_p, TI_FLAG " %s", card.priority.c_str()); } if (!card.assignee.empty()) { ImGui::SameLine(); ImGui::TextDisabled(TI_USER " %s", card.assignee.c_str()); } ImGui::EndChild(); if (ImGui::IsItemHovered() && ImGui::IsItemClicked(ImGuiMouseButton_Right)) { ImGui::OpenPopup("##card_ctx"); } if (ImGui::BeginPopup("##card_ctx")) { ImGui::TextDisabled("Move to:"); for (const auto& tgt : cols_snap) { if (tgt.id == card.column_id) continue; if (ImGui::MenuItem(tgt.name.c_str())) { 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 lock(s.mu); s.last_refresh_error = "move: " + err; return; } refresh_data(s); }).detach(); } } ImGui::Separator(); if (ImGui::MenuItem(TI_PLAYER_PLAY " Launch agent workflow")) { std::string run_id, err; if (!launch_workflow(s.cfg, card.id, run_id, err)) s.last_refresh_error = "launch: " + err; } ImGui::EndPopup(); } ImGui::PopID(); } ImGui::EndChild(); } } ImGui::EndChild(); ImGui::End(); } } // namespace kanban_cpp