---
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<std::string> g_back_recipes;   // backing
static std::vector<const char*> 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<data_table::TableEvent> 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<std::string>` (estable) + `std::vector<const char*>` (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_<name>;`. 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<string> to a row-major vector<const char*>
    // pointing into the backing storage. Stable pointers — backing must not be
    // resized while ptrs are in use.
    void cells_to_ptrs(const std::vector<std::string>& back,
                       std::vector<const char*>& 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.
