feat(0131): data_table_cpp_viz migration + botones acción + 7 nuevos tests e2e

Issue 0131 (agents v0.2) — frontend agents_dashboard:

* Migra tabla Agents de ImGui::BeginTable a render_grid_stage0 (data_table_cpp_viz).
  11 columnas: Status(Badge), ID, Name, Uptime, Msg/24h,
  Start/Stop/Restart/Clear Memory/Del Cache/Logs (Button renderers).
* Lee campos reales uptime_seconds + messages_24h del backend (antes leía
  'instances' como proxy para msg_24h; ahora lee el campo correcto).
* Confirmation modal (ImGui::BeginPopupModal) para acciones destructivas
  clear_memory y delete_cache.
* Link fn_module_data_table en CMakeLists (HAS_DATA_TABLE activado).
* Actualiza app.md: data_table_cpp_viz en uses_functions, version 0.2.0.
* Añade 7 tests pytest nuevos (total 24):
  test_uptime_field_present, test_msg_24h_field_present,
  test_clear_memory_requires_apikey, test_delete_cache_requires_apikey,
  test_control_roundtrip, test_unified_stop_does_not_kill_launcher,
  test_clear_memory_response_shape.
  SEGURIDAD: solo test-bot se para/arranca en e2e, nunca agentes reales.

Build verificado: Linux + Windows cross-compile (cmake --build cpp/build/windows).

Co-Authored-By: fn-orquestador (issue 0131) <noreply@fn-registry>
This commit is contained in:
2026-05-22 23:07:01 +02:00
parent af83e571c6
commit c8b7adf81d
4 changed files with 433 additions and 64 deletions
+229 -62
View File
@@ -2,11 +2,11 @@
//
// Panels:
// Connection — base_url + apikey input, Test button, SSE status LED
// Agents — data table (id, status, uptime, msg_24h, actions)
// Agents — data table (id, status, uptime, msg_24h, actions) via data_table_cpp_viz
// Logs — SSE tail buffer for /sse/agents/{id}/logs
// Status Feed — SSE events from /sse/status (collapsible)
//
// Issue 0129. Registry functions used:
// Issue 0129 + 0131. Registry functions used:
// http_request_cpp_core, http_get_json_cpp_core,
// sse_client_cpp_core, data_table_cpp_viz, logger_cpp_core,
// secret_store_cpp_infra
@@ -21,6 +21,12 @@
#include "infra/secret_store.h"
#include "nlohmann/json.hpp"
// data_table_cpp_viz: render_grid_stage0 + declarative column specs (issue 0131)
#if __has_include("viz/data_table_grid.h")
# include "viz/data_table_grid.h"
# define HAS_DATA_TABLE 1
#endif
// SQLite (vendored via fn_framework)
#include <sqlite3.h>
@@ -115,6 +121,16 @@ struct AppState {
std::mutex action_mu;
std::string action_feedback;
long long action_feedback_ts = 0;
// Confirmation modals for destructive actions (clear_memory, delete_cache)
std::string confirm_agent_id; // agent being confirmed
std::string confirm_action; // "clear_memory" | "delete_cache"
bool confirm_open = false;
#ifdef HAS_DATA_TABLE
// Per-panel data_table State for the agents grid
data_table::State agents_tbl_state;
#endif
};
static AppState g_state;
@@ -243,10 +259,9 @@ static std::string make_url(const AppState& s, const std::string& path) {
}
// Parse agents JSON array from /agents endpoint.
// Backend shape (agents_and_robots/internal/api/handlers.go):
// Backend shape (agents_and_robots/internal/api/handlers.go v0.2):
// { id, name, version, desc, enabled: bool, running: bool, pid: int,
// instances: int, config_path }
// status is derived; uptime/msg_24h not provided yet (v0.2 backend work).
// instances: int, config_path, uptime_seconds: int64, messages_24h: int }
static std::vector<AgentRow> parse_agents(const std::string& body) {
std::vector<AgentRow> rows;
auto j = json::parse(body, nullptr, false);
@@ -260,8 +275,9 @@ static std::vector<AgentRow> parse_agents(const std::string& body) {
if (running) r.status = "running";
else if (!enabled) r.status = "disabled";
else r.status = "stopped";
r.uptime_s = 0;
r.msg_24h = a.value("instances", 0); // hack: show instances until backend exposes msg_24h
// v0.2: real uptime_seconds and messages_24h from backend
r.uptime_s = a.value("uptime_seconds", (long long)0);
r.msg_24h = a.value("messages_24h", 0);
rows.push_back(std::move(r));
}
return rows;
@@ -560,6 +576,15 @@ static const char* status_icon(const std::string& s) {
return TI_CIRCLE_DOTTED;
}
// ---------------------------------------------------------------------------
// Agents panel — rendered via data_table_cpp_viz (render_grid_stage0).
// Columns: Status | ID | Name | Uptime | Msg/24h | Start | Stop | Restart |
// ClearMem | DelCache | Logs
// Actions are Button renderer columns; events handled after render_grid_stage0.
//
// Fallback (compile without fn_module_data_table): plain text list.
// ---------------------------------------------------------------------------
static void draw_agents_panel(AppState& s) {
if (!ImGui::Begin(TI_ROBOT " Agents", &g_show_agents)) {
ImGui::End();
@@ -585,87 +610,229 @@ static void draw_agents_panel(AppState& s) {
ImGui::Separator();
// Table
ImGuiTableFlags flags = ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg |
ImGuiTableFlags_Resizable | ImGuiTableFlags_ScrollY |
ImGuiTableFlags_SizingStretchProp;
float available_h = ImGui::GetContentRegionAvail().y;
if (ImGui::BeginTable("##agents_tbl", 6, flags, ImVec2(0, available_h))) {
ImGui::TableSetupScrollFreeze(0, 1);
ImGui::TableSetupColumn("Status", ImGuiTableColumnFlags_WidthFixed, 70);
ImGui::TableSetupColumn("ID", ImGuiTableColumnFlags_WidthStretch);
ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_WidthStretch);
ImGui::TableSetupColumn("Uptime", ImGuiTableColumnFlags_WidthFixed, 65);
ImGui::TableSetupColumn("Msg/24h",ImGuiTableColumnFlags_WidthFixed, 65);
ImGui::TableSetupColumn("Actions",ImGuiTableColumnFlags_WidthFixed, 120);
ImGui::TableHeadersRow();
// Confirmation modal for destructive actions
if (s.confirm_open) {
ImGui::OpenPopup("##confirm_action");
s.confirm_open = false;
}
if (ImGui::BeginPopupModal("##confirm_action", nullptr,
ImGuiWindowFlags_AlwaysAutoResize)) {
if (s.confirm_action == "clear_memory") {
ImGui::Text("Clear memory for agent '%s'?", s.confirm_agent_id.c_str());
ImGui::TextDisabled("This will delete all messages and facts.");
} else {
ImGui::Text("Delete cache for agent '%s'?", s.confirm_agent_id.c_str());
ImGui::TextDisabled("This will remove crypto/ and cache/ directories.");
}
ImGui::Separator();
if (ImGui::Button("Confirm", ImVec2(120, 0))) {
agent_action(s, s.confirm_agent_id, s.confirm_action);
ImGui::CloseCurrentPopup();
}
ImGui::SameLine();
if (ImGui::Button("Cancel", ImVec2(120, 0))) {
ImGui::CloseCurrentPopup();
}
ImGui::EndPopup();
}
#ifdef HAS_DATA_TABLE
// ----- data_table_cpp_viz path (issue 0131 migration) -----
// Column layout:
// 0=Status 1=ID 2=Name 3=Uptime 4=Msg24h
// 5=Start 6=Stop 7=Restart 8=ClearMem 9=DelCache 10=Logs
static constexpr int N_COLS = 11;
static const char* kHeaders[N_COLS] = {
"Status", "ID", "Name", "Uptime", "Msg/24h",
"Start", "Stop", "Restart", "Clear Memory", "Del Cache", "Logs"
};
static const data_table::ColumnType kTypes[N_COLS] = {
data_table::ColumnType::String, data_table::ColumnType::String,
data_table::ColumnType::String, data_table::ColumnType::String,
data_table::ColumnType::Int,
data_table::ColumnType::String, data_table::ColumnType::String,
data_table::ColumnType::String, data_table::ColumnType::String,
data_table::ColumnType::String, data_table::ColumnType::String
};
static const int kSrcForEff[N_COLS] = { 0,1,2,3,4, 5,6,7,8,9,10 };
// Build column specs (Badge for Status, Button for action columns)
static data_table::TableInput main_t;
if (main_t.column_specs.empty()) {
main_t.column_specs.resize(N_COLS);
// Status: Badge renderer
auto& cs_status = main_t.column_specs[0];
cs_status.id = "status";
cs_status.renderer = data_table::CellRenderer::Badge;
cs_status.badges = {
{ "running", "#22c55e", "" },
{ "stopped", "#6b7280", "" },
{ "disabled", "#374151", "" },
{ "crashed", "#ef4444", "" },
};
// Text columns
for (int c : {1,2,3,4}) {
main_t.column_specs[c].id = kHeaders[c];
main_t.column_specs[c].renderer = data_table::CellRenderer::Text;
}
// Button columns (indices 5..10)
const char* btn_actions[] = { "start","stop","restart","clear_memory","delete_cache","logs" };
const char* btn_labels[] = { TI_PLAYER_PLAY " Start",
TI_PLAYER_STOP " Stop",
TI_REFRESH " Restart",
TI_TRASH " Clear Mem",
TI_FOLDER_MINUS " Del Cache",
TI_TERMINAL " Logs" };
for (int i = 0; i < 6; ++i) {
int c = 5 + i;
main_t.column_specs[c].id = btn_actions[i];
main_t.column_specs[c].renderer = data_table::CellRenderer::Button;
main_t.column_specs[c].button_action = btn_actions[i];
main_t.column_specs[c].button_label = btn_labels[i];
}
// Tooltip for destructive buttons
main_t.column_specs[8].tooltip = "Delete all messages and facts for this agent";
main_t.column_specs[8].tooltip_on_hover = true;
main_t.column_specs[9].tooltip = "Remove crypto/ and cache/ directories";
main_t.column_specs[9].tooltip_on_hover = true;
}
// Snapshot agents under lock
std::vector<AgentRow> snapshot;
std::string agents_err;
{
std::lock_guard<std::mutex> lk(s.agents_mu);
std::string filter = s.filter_buf;
for (auto& row : s.agents) {
// Filter
if (!filter.empty() &&
row.id.find(filter) == std::string::npos &&
row.id.find(filter) == std::string::npos &&
row.display_name.find(filter) == std::string::npos) {
continue;
}
snapshot.push_back(row);
}
agents_err = s.agents_error;
}
ImGui::TableNextRow();
// Build flat cell array (row-major, N_COLS per row)
int n_rows = (int)snapshot.size();
std::vector<std::string> cell_strs;
cell_strs.reserve((size_t)n_rows * N_COLS);
for (auto& row : snapshot) {
bool is_running = (row.status == "running");
cell_strs.push_back(row.status); // 0 Status
cell_strs.push_back(row.id); // 1 ID
cell_strs.push_back(row.display_name); // 2 Name
cell_strs.push_back(format_uptime(row.uptime_s)); // 3 Uptime
cell_strs.push_back(std::to_string(row.msg_24h)); // 4 Msg/24h
// Button cells: show label only if action is applicable
cell_strs.push_back(!is_running ? "start" : ""); // 5 Start (disabled when running)
cell_strs.push_back(is_running ? "stop" : ""); // 6 Stop (disabled when stopped)
cell_strs.push_back("restart"); // 7 Restart always available
cell_strs.push_back("clear_memory"); // 8 ClearMem
cell_strs.push_back("delete_cache"); // 9 DelCache
cell_strs.push_back("logs"); // 10 Logs
}
// Build pointer array
std::vector<const char*> cells_ptr;
cells_ptr.reserve(cell_strs.size());
for (auto& s_str : cell_strs) cells_ptr.push_back(s_str.c_str());
// Status column
ImGui::TableSetColumnIndex(0);
ImGui::PushStyleColor(ImGuiCol_Text, status_color(row.status));
ImGui::TextUnformatted(status_icon(row.status));
ImGui::SameLine();
ImGui::TextUnformatted(row.status.c_str());
ImGui::PopStyleColor();
// All rows visible (filtering already applied above)
std::vector<int> visible_rows(n_rows);
for (int i = 0; i < n_rows; ++i) visible_rows[i] = i;
// ID
ImGui::TableSetColumnIndex(1);
ImGui::TextUnformatted(row.id.c_str());
// Collect events from render_grid_stage0
std::vector<data_table::TableEvent> events;
float available_h = ImGui::GetContentRegionAvail().y;
// Constrain height so the scroll region doesn't eat the whole panel
ImVec2 tbl_size(0, available_h);
// Name
ImGui::TableSetColumnIndex(2);
ImGui::TextUnformatted(row.display_name.c_str());
// Pass height hint via State display — we force Table view mode
s.agents_tbl_state.display = data_table::ViewMode::Table;
// Uptime
ImGui::TableSetColumnIndex(3);
ImGui::TextUnformatted(format_uptime(row.uptime_s).c_str());
// Need at least 1 row for the API to be happy
if (n_rows > 0) {
render_grid_stage0("##agents_tbl",
s.agents_tbl_state,
cells_ptr.empty() ? nullptr : cells_ptr.data(),
n_rows, N_COLS, N_COLS,
kHeaders, kTypes, kSrcForEff,
visible_rows, main_t, &events);
} else {
ImGui::TextDisabled("(no agents match filter)");
}
// Msg 24h
ImGui::TableSetColumnIndex(4);
ImGui::Text("%d", row.msg_24h);
// Handle button events
for (auto& ev : events) {
if (ev.kind != data_table::TableEventKind::ButtonClick) continue;
if (ev.row < 0 || ev.row >= n_rows) continue;
const std::string& agent_id = snapshot[ev.row].id;
const std::string& act = ev.action_id;
// Actions
ImGui::TableSetColumnIndex(5);
if (act == "start") {
agent_action(s, agent_id, "start");
} else if (act == "stop") {
agent_action(s, agent_id, "stop");
} else if (act == "restart") {
agent_action(s, agent_id, "restart");
} else if (act == "clear_memory") {
// Require confirmation
s.confirm_agent_id = agent_id;
s.confirm_action = "clear_memory";
s.confirm_open = true;
} else if (act == "delete_cache") {
// Require confirmation
s.confirm_agent_id = agent_id;
s.confirm_action = "delete_cache";
s.confirm_open = true;
} else if (act == "logs") {
snprintf(s.log_agent_id, sizeof(s.log_agent_id), "%s", agent_id.c_str());
db_save_state(s, "log_agent_id", agent_id.c_str());
start_log_sse(s, agent_id);
g_show_logs = true;
}
}
#else
// ----- Fallback: plain text list (no fn_module_data_table) -----
{
std::lock_guard<std::mutex> lk(s.agents_mu);
std::string filter = s.filter_buf;
for (auto& row : s.agents) {
if (!filter.empty() &&
row.id.find(filter) == std::string::npos &&
row.display_name.find(filter) == std::string::npos) {
continue;
}
ImGui::PushID(row.id.c_str());
ImGui::Text("[%s] %s uptime=%s msg24h=%d",
row.status.c_str(), row.id.c_str(),
format_uptime(row.uptime_s).c_str(), row.msg_24h);
ImGui::SameLine();
bool is_running = (row.status == "running");
if (!is_running) {
if (ImGui::SmallButton(TI_PLAYER_PLAY " Start")) {
agent_action(s, row.id, "start");
}
} else {
if (ImGui::SmallButton(TI_PLAYER_STOP " Stop")) {
agent_action(s, row.id, "stop");
}
}
if (!is_running && ImGui::SmallButton("Start")) agent_action(s, row.id, "start");
if (is_running && ImGui::SmallButton("Stop")) agent_action(s, row.id, "stop");
ImGui::SameLine();
if (ImGui::SmallButton(TI_REFRESH " Restart")) {
agent_action(s, row.id, "restart");
}
if (ImGui::SmallButton("Restart")) agent_action(s, row.id, "restart");
ImGui::SameLine();
if (ImGui::SmallButton(TI_TERMINAL " Logs")) {
if (ImGui::SmallButton("Logs")) {
snprintf(s.log_agent_id, sizeof(s.log_agent_id), "%s", row.id.c_str());
db_save_state(s, "log_agent_id", row.id.c_str());
start_log_sse(s, row.id);
g_show_logs = true;
}
ImGui::SameLine();
if (ImGui::SmallButton("ClearMem")) {
s.confirm_agent_id = row.id; s.confirm_action = "clear_memory"; s.confirm_open = true;
}
ImGui::SameLine();
if (ImGui::SmallButton("DelCache")) {
s.confirm_agent_id = row.id; s.confirm_action = "delete_cache"; s.confirm_open = true;
}
ImGui::PopID();
}
ImGui::EndTable();
}
#endif // HAS_DATA_TABLE
ImGui::End();
}