--- id: "0107g" title: "Migrar inline ImGui::BeginTable a data_table::render en apps con tablas de datos reales" status: en-progreso type: refactor domain: - cpp-stack - meta scope: multi-app priority: media depends: - "0107c" blocks: [] related: - "0107" created: 2026-05-17 updated: 2026-05-17 tags: [modules, data-table, drift, audit, inline-begintable] --- # 0107g — Migrar inline BeginTable a `data_table::render` (data tables reales) Parte del issue principal [0107](0107-modules-standardization.md). Detectado por `audit_data_table_usage_go_infra` (output en `dev/data_table_integration_audit.md`). ## Problema Audit automatico identifica ~12 hits de `ImGui::BeginTable` inline en apps que YA declaran `uses_modules: [data_table_cpp]`. Mezcla legitimos + bugs: - **Legitimos** (NO migrar): KPI grids, schema forms k/v, layout 2-col splitters, chart grids. NO son tablas de datos. - **Bugs reales**: tablas de datos con filas dinamicas + sort/filter potencial que reinventan logica que el modulo provee. Resultado: codigo duplicado, comportamiento inconsistente, color/badge/sort/filter "casi-pero-no" igual entre apps. Conexiones raras: cada app personaliza su tabla a mano. ## Decision Migrar los hits identificados como bugs reales a `data_table::render`. Dejar los legitimos como excepciones documentadas en `docs/MODULES_API.md::Cuando usar data_table::render vs BeginTable directo`. ## Tabla de migracion | App | Path | Linea | Es bug? | Accion | |---|---|---|---|---| | dag_engine_ui | apps/dag_engine_ui/tabs.cpp | 382 | **BUG** (`##dt_run_steps`, 6 cols, scroll Y, runs dinamicas) | Migrar | | dag_engine_ui | apps/dag_engine_ui/tabs.cpp | 731 | LEGITIMO (`##health_kpis`, 4 cols stretch same, KPI grid) | Dejar + comentar | | navegator_dashboard | projects/navegator/apps/navegator_dashboard/autoextract_panel.cpp | 528 | **BUG** (`##ax_schema`, 5 cols, filas dinamicas schema) | Migrar | | navegator_dashboard | projects/navegator/apps/navegator_dashboard/recipes_panel.cpp | 238 | **BUG** (`##recipes_tbl`, 6 cols, filas dinamicas recipes) | Migrar | | graph_explorer | projects/osint_graph/apps/graph_explorer/extract_panel.cpp | 981 | **BUG** (`##ents`, 5 cols, filas dinamicas entities) | Migrar | | graph_explorer | projects/osint_graph/apps/graph_explorer/extract_panel.cpp | 1027 | **BUG** (`##rels`, 5 cols, filas dinamicas relations) | Migrar | | graph_explorer | projects/osint_graph/apps/graph_explorer/main.cpp | 1127 | LEGITIMO (`##enr_params`, form k/v) | Dejar + comentar | | graph_explorer | projects/osint_graph/apps/graph_explorer/views.cpp | 885 | LEGITIMO (`##insp_id`, inspector form k/v) | Dejar + comentar | | graph_explorer | projects/osint_graph/apps/graph_explorer/views.cpp | 958 | LEGITIMO (`##insp_fields`, inspector form) | Dejar + comentar | | graph_explorer | projects/osint_graph/apps/graph_explorer/views.cpp | 1546 | INFO comment, ya migrado a data_table::render | Ignorar (es comentario) | | graph_explorer | projects/osint_graph/apps/graph_explorer/views.cpp | 1854 | **BUG** (`##te_rows`, col_count dinamico, data table type explorer) | Migrar (segunda fase de la migration ya empezada) | | graph_explorer | projects/osint_graph/apps/graph_explorer/views.cpp | 2292 | DISCUTIBLE (`##te_fields`, 5 cols, fields de un tipo — semi-dinamico) | Evaluar; si rows >20 migrar, sino dejar | | registry_dashboard | projects/fn_monitoring/apps/registry_dashboard/views.cpp | 380 | LEGITIMO (`##kpi_grid`, KPI cards) | Dejar + comentar | | registry_dashboard | projects/fn_monitoring/apps/registry_dashboard/views.cpp | 436 | LEGITIMO (`##chart_grid`, plots grid) | Dejar + comentar | | registry_dashboard | projects/fn_monitoring/apps/registry_dashboard/views.cpp | 648 | LEGITIMO (`##monitor_kpi`, KPI cards) | Dejar + comentar | | registry_dashboard | projects/fn_monitoring/apps/registry_dashboard/views.cpp | 1110 | LEGITIMO (`##proj_layout`, 2-col splitter) | Dejar + comentar | | registry_dashboard | projects/fn_monitoring/apps/registry_dashboard/views.cpp | 1448 | LEGITIMO (`##explorer_layout`, 2-col splitter) | Dejar + comentar | | registry_dashboard | projects/fn_monitoring/apps/registry_dashboard/work_tab.cpp | 239 | **BUG** (`##flows_work`, 8 cols, filas dinamicas flows) | Migrar | | registry_dashboard | projects/fn_monitoring/apps/registry_dashboard/work_tab.cpp | 272 | **BUG** (`##top_issues_work`, 7 cols, filas dinamicas issues) | Migrar | | app_gestion | apps/app_gestion/main.cpp | 722 | DISCUTIBLE (`##linked_tbl`, 4 cols, lista de modulos linked — semi-dinamico, rows <20) | Evaluar; bias a dejar como esta | **Total a migrar (BUGs): 8 tablas en 4 apps.** **Total LEGITIMOS (dejar + comentar): 9.** **Total DISCUTIBLES: 2 — decision contextual.** **Total comentarios/already-migrated: 1.** ## Tareas - [x] **1.1** Migrar `dag_engine_ui/tabs.cpp:382` (`##dt_run_steps`) → `data_table::render`. HECHO. Function col → Button action="open_fn"; Status → CategoricalChip; Duration → Duration renderer. - [~] **1.2** Migrar `navegator_dashboard/autoextract_panel.cpp:528` (`##ax_schema`). ABORTADO: form editor con InputText/Checkbox editables inline en cada fila (field, selector, type, keep). data_table::render no soporta CellEdit como InputText inline. Comentado con LAYOUT-TABLE. - [x] **1.3** Migrar `navegator_dashboard/recipes_panel.cpp:238` (`##recipes_tbl`). HECHO. Patron B: 4 columnas Button (run/edit/delete/open_df) + ev.row para indexar yaml_path. last_status → CategoricalChip. - [~] **1.4** Migrar `graph_explorer/extract_panel.cpp:981` (`##ents`). ABORTADO: form editor con InputText editables por fila (type_buf, name_buf) + Checkbox "sel". Mutacion directa de structs entities[i]. No mapeable a data_table. Comentado con LAYOUT-TABLE. - [~] **1.5** Migrar `graph_explorer/extract_panel.cpp:1027` (`##rels`). ABORTADO: form editor con Checkbox "sel" + inmutabilidad necesaria (relations[i].selected se muta inline). Comentado con LAYOUT-TABLE. - [~] **1.6** Migrar `graph_explorer/views.cpp:1854` (`##te_rows`). ABORTADO: interactividad app-específica no mapeable — Selectable + single-click ramificado por estado de promocion (promoted/unpromoted), dblclick promote-flow, PopupContextItem con promote/demote/focus condicionales, SmallButton Promote-out-of-group, paginacion manual. Equivalente exact en data_table events no existe. Comentado explicando razon. - [x] **1.7** Migrar `registry_dashboard/work_tab.cpp:239` (`##flows_work`). HECHO. 8 cols. Status + Risk → CategoricalChip. BeginChild host 220px. - [x] **1.8** Migrar `registry_dashboard/work_tab.cpp:272` (`##top_issues_work`). HECHO. 7 cols. Status + Deps + Prio → CategoricalChip. Deps string "-"/"OK"/"blocked" preserva logica de color original. BeginChild host -1. - [x] **2.1** Anadir comentario `// LAYOUT-TABLE — KPI/form/splitter, no data; keep BeginTable inline.` encima de los 9 hits LEGITIMOS para que `audit_data_table_usage` los excluya en proximas pasadas. HECHO en: ##health_kpis, ##enr_params, ##insp_id, ##insp_fields, ##kpi_grid, ##chart_grid, ##monitor_kpi, ##proj_layout, ##explorer_layout. Los 3 ABORTADOS tambien comentados con razon tecnica. - [ ] **2.2** Actualizar `audit_data_table_usage_go_infra` para leer ese comentario y filtrar `[warn] -> [ignored:declared_layout_table]`. - [x] **3.1** Decidir los 2 DISCUTIBLES (`te_fields`, `linked_tbl`) con criterio "si rows pueden crecer > 50, migrar". Decision: DEJAR. `te_fields` max ~20 fields por tipo; `linked_tbl` max ~10 modules linked. Rows no escalan. - [x] **4.1** Envolver TODAS las llamadas a `data_table::render` en `ImGui::BeginChild` host. HECHO en las 3 tablas migradas: flows_work (220px), top_issues_work (-1), recipes_tbl (300px), dt_run_steps (usa el BeginChild preexistente ##run_steps_wrap). - [ ] **5.1** Re-ejecutar audit: ``` ./fn run audit_data_table_usage ``` Verificar: 0 BUG hits, 9 LEGITIMOS comentados, 11 `no_child_host` resueltos o documentados como excepcion. - [x] **5.2** Build de las 4 apps modificadas. HECHO: dag_engine_ui, registry_dashboard, graph_explorer compilan OK (Linux). navegator_dashboard es Windows-only (CMakeLists.txt retorna en non-WIN32); sintaxis verificada via g++ -fsyntax-only sin errores. ## Patrones de migracion canonicos ### Patron A: ImGui::BeginTable inline → data_table::render basico ```cpp // ANTES if (ImGui::BeginTable("##recipes_tbl", 6, flags)) { ImGui::TableSetupColumn("name"); ImGui::TableSetupColumn("url_pattern"); // ... for (const auto& r : recipes) { ImGui::TableNextRow(); ImGui::TableNextColumn(); ImGui::TextUnformatted(r.name.c_str()); ImGui::TableNextColumn(); ImGui::TextUnformatted(r.url_pattern.c_str()); // ... } ImGui::EndTable(); } // DESPUES static data_table::State g_st_recipes; static std::vector g_back_recipes; // backing static std::vector g_ptrs_recipes; // ptrs row-major g_back_recipes.clear(); for (const auto& r : recipes) { g_back_recipes.push_back(r.name); g_back_recipes.push_back(r.url_pattern); // ... resto cols } g_ptrs_recipes.clear(); for (auto& s : g_back_recipes) g_ptrs_recipes.push_back(s.c_str()); data_table::TableInput tbl; tbl.name = "recipes"; tbl.headers = {"name", "url_pattern", "last_status", "last_at", "tries", "ok"}; tbl.types = {data_table::ColumnType::String, data_table::ColumnType::String, data_table::ColumnType::String, data_table::ColumnType::Date, data_table::ColumnType::Int, data_table::ColumnType::Bool}; tbl.cells = g_ptrs_recipes.data(); tbl.rows = (int)recipes.size(); tbl.cols = 6; // Status como CategoricalChip (ganancia inmediata sobre BeginTable) tbl.column_specs.resize(tbl.cols); for (int i = 0; i < tbl.cols; i++) tbl.column_specs[i].id = tbl.headers[i]; tbl.column_specs[2].renderer = data_table::CellRenderer::CategoricalChip; tbl.column_specs[2].chips = {{"ok","#22c55e"},{"error","#ef4444"},{"pending","#a3a3a3"}}; std::vector events; ImGui::BeginChild("##recipes_host", ImVec2(-1, -1)); data_table::render("##recipes_dt", {tbl}, g_st_recipes, &events); ImGui::EndChild(); for (const auto& ev : events) { if (ev.kind == data_table::TableEventKind::RowDoubleClick) { open_recipe_detail(ev.row); } } ``` ### Patron B: BeginTable inline con interactividad (boton por fila) Si la BeginTable inline tiene un boton "Delete" / "Edit" por fila → migrar usando `CellRenderer::Button` + `action_id`: ```cpp // ANTES ImGui::TableNextColumn(); if (ImGui::SmallButton(("Delete##" + r.id).c_str())) { delete_recipe(r.id); } // DESPUES — anadir columna actions con button renderer tbl.headers.push_back("actions"); tbl.types.push_back(data_table::ColumnType::String); data_table::ColumnSpec actions_spec; actions_spec.id = "actions"; actions_spec.renderer = data_table::CellRenderer::Button; actions_spec.button_action = "delete_recipe"; actions_spec.button_label = "Delete"; actions_spec.button_color_hex = "#ef4444"; tbl.column_specs.push_back(actions_spec); tbl.cols++; // Backing extra for (const auto& r : recipes) { g_back_recipes.push_back(r.id); // celda actions = el id (consumido en ev.value) } // Handler for (const auto& ev : events) { if (ev.kind == data_table::TableEventKind::ButtonClick && ev.action_id == "delete_recipe") { delete_recipe(ev.value); // ev.value == r.id de la fila clicada } } ``` ## Riesgos - **Backing storage**: las apps deben mantener `std::vector` (estable) + `std::vector` (ptrs row-major). Helper `cells_to_ptrs()` ya esta usado en data_factory — generalizar como `cpp/functions/core/cells_to_ptrs.cpp` si patron se repite >2 veces (ya pasa). - **State persistente**: cada migracion requiere `static data_table::State g_st_;`. Si la app tiene N tablas, N states. - **Comportamiento sutil**: filter/sort/freeze ahora son user-toggle, no controlados por la app. La app pierde control fino, pero gana consistencia. ## Bonus: nueva funcion del registry `cells_to_ptrs_cpp_core` Patron `g_back + g_ptrs` aparece en data_factory + (post 0107g) en 4 apps mas. Promover a funcion del registry: ```cpp // cpp/functions/core/cells_to_ptrs.h namespace fn { // Converts a row-major flat vector to a row-major vector // pointing into the backing storage. Stable pointers — backing must not be // resized while ptrs are in use. void cells_to_ptrs(const std::vector& back, std::vector& ptrs); } ``` Issue separado o sub-task de 0107g segun apetito. ## Notas - El audit `audit_data_table_usage_go_infra` ya existe (FRESH 7d). Se referencia desde `fn doctor modules` (0107a) para mostrar drift en CI/dashboard.