chore: snapshot WIP previo + flow 0008 + 7 sub-issues (0112-0119)
Snapshot de WIP acumulado de sesiones previas antes de merge wave 1 del flow 0008 (kanban_cpp + agent_runner_api + DoD schema). Incluye: - dev/flows/0008-kanban-cpp-and-agent-workflows.md - dev/issues/0112-0119*.md (7 sub-issues) - WIP previo en cmd/fn/doctor.go, registry/*, modules/, cpp/, etc. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,704 @@
|
||||
# Modules API — contrato publico de modulos C++
|
||||
|
||||
Contrato canonico de cada modulo C++ del registry. Una app DEBE poder integrar un modulo leyendo SOLO esta pagina, sin abrir el `.cpp`. Si una app inventa un patron distinto al documentado aqui, es bug — abrir issue o anadir la capacidad al modulo, NO improvisar.
|
||||
|
||||
Issue 0107 (modules-standardization). Mantenido en sync con `modules/*/module.md::version` por el slash command `/version`.
|
||||
|
||||
---
|
||||
|
||||
## Indice
|
||||
|
||||
1. [framework_cpp](#framework_cpp) — shell ImGui obligatorio
|
||||
2. [data_table_cpp](#data_table_cpp) — tabla TQL completa
|
||||
3. [Capability matrix consolidada](#capability-matrix-consolidada)
|
||||
4. [Ciclo de vida de un modulo](#ciclo-de-vida-de-un-modulo)
|
||||
5. [Como NO conectarse a un modulo (anti-patrones)](#como-no-conectarse-a-un-modulo)
|
||||
|
||||
---
|
||||
|
||||
## framework_cpp
|
||||
|
||||
**Static lib target:** `fn_framework`
|
||||
**Header path:** `cpp/framework/app_base.h`
|
||||
**Namespace:** `fn` + `fn_ui`
|
||||
**Entry function:** `int fn::run_app(AppConfig cfg, std::function<void()> render_fn)`
|
||||
**State:** no requerido (singleton interno via `fn::run_app` lifecycle)
|
||||
**Linkage en apps:** transitivo via macro `add_imgui_app(<target> ...)` — NO se declara en `uses_modules`.
|
||||
**Version actual:** `1.1.0`
|
||||
|
||||
### Opt-in
|
||||
|
||||
Apps cpp del registry obtienen `fn_framework` automaticamente via `add_imgui_app(<target> ...)`. NO listar miembros del framework en `uses_functions` ni en `uses_modules`.
|
||||
|
||||
### Ejemplo minimo
|
||||
|
||||
```cpp
|
||||
#include "framework/app_base.h"
|
||||
|
||||
int main() {
|
||||
fn::AppConfig cfg;
|
||||
cfg.title = "My App";
|
||||
cfg.width = 1280;
|
||||
cfg.height = 720;
|
||||
cfg.about = { "my_app", "1.0.0", "Lo que hace mi app." };
|
||||
cfg.log = { "my_app.log", 1 }; // file relativo a local_files/, level Info
|
||||
return fn::run_app(cfg, []() {
|
||||
ImGui::Begin("Main");
|
||||
ImGui::Text("hello");
|
||||
ImGui::End();
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### API completa
|
||||
|
||||
| Campo `AppConfig` | Tipo | Default | Para que |
|
||||
|---|---|---|---|
|
||||
| `title` | `const char*` | `"fn_registry"` | Titulo de la ventana |
|
||||
| `width` / `height` | `int` | 1280 / 720 | Tamano inicial |
|
||||
| `vsync` | `bool` | `true` | VSync GL |
|
||||
| `viewports` | `bool` | `true` | Multi-viewport (paneles dragged-out fuera del main) |
|
||||
| `theme` | `ThemeMode` | `FnDark` | `FnDark` / `ImGuiDark` / `ImGuiLight` / `None` |
|
||||
| `bg_r/g/b` | `float` | `fn_tokens::colors::bg` | Color background main |
|
||||
| `about` | `AppAboutInfo` | `{}` | `name`, `version`, `description` para About window |
|
||||
| `panels` + `panel_count` | `PanelToggle*` | `nullptr` | Toggles del menubar (paneles abre/cierra) |
|
||||
| `layouts_cb` | `LayoutCallbacks*` | `nullptr` | Override del menu Layouts (default: auto-storage SQLite) |
|
||||
| `auto_layouts` | `bool` | `true` | Si crear `LayoutStorage` por defecto |
|
||||
| `auto_layouts_db` | `const char*` | `"layouts.db"` | Archivo SQLite relativo a `local_files/` |
|
||||
| `view_extras` | `std::function<bool()>` | `{}` | Items extra en menu View |
|
||||
| `init_gl_loader` | `bool` | `false` | Solo si la app llama `gl*` directo |
|
||||
| `auto_dockspace` | `bool` | `true` | DockSpaceOverViewport antes de `render_fn` cada frame |
|
||||
| `log` | `AppLogConfig` | `{}` | `file_path`, `level` 0-3 |
|
||||
| `pre_frame` | `std::function<void()>` | `{}` | Hook antes del menubar (LayoutStorage custom apply) |
|
||||
|
||||
### Capacidades automaticas (zero opt-in)
|
||||
|
||||
- Window dark titlebar (Windows DWMWA_USE_IMMERSIVE_DARK_MODE).
|
||||
- AltSnap-safe sizemove + Alt+RMB resize + Alt+LMB move (via Win32 WndProc subclass per HWND).
|
||||
- App icon attach a HWND main + viewports secundarios (si `<app_dir>/appicon.ico` existe).
|
||||
- Layouts persistentes (save/load/list/delete/reset) con restore-on-open + save-on-close.
|
||||
- About / Settings / Logs windows + menubar.
|
||||
- `imgui.ini` + `app_settings.ini` bajo `<exe_dir>/local_files/`.
|
||||
- Migration desde cwd/exe_dir a `local_files/` para instalaciones viejas.
|
||||
|
||||
### Helpers publicos
|
||||
|
||||
```cpp
|
||||
const char* fn::exe_dir(); // dir del exe
|
||||
const char* fn::local_dir(); // <exe_dir>/local_files/
|
||||
const char* fn::local_path(const char*); // <local_dir>/<name>
|
||||
const char* fn::asset_dir(); // <exe_dir>/assets/
|
||||
const char* fn::asset_path(const char*); // <asset_dir>/<name>
|
||||
void fn::migrate_to_local_files(const char* const* names, size_t n);
|
||||
const char* fn::framework_version();
|
||||
const char* fn::framework_description();
|
||||
```
|
||||
|
||||
### Gotchas
|
||||
|
||||
- `Restart=always` en systemd unit (NO `on-failure`) — `SIGTERM` limpio = exit success.
|
||||
- App write-paths SIEMPRE via `fn::local_path("X")`. NO `fopen("X.db", ...)` con path relativo.
|
||||
- `init_gl_loader = true` SOLO si la app llama GL directo (`gl_loader_init`). ImGui/ImPlot puro NO lo necesita.
|
||||
- Anti-jitter pausa render durante `WM_ENTERSIZEMOVE` — apps con telemetria live deben tolerar pausa transitoria.
|
||||
|
||||
---
|
||||
|
||||
## data_table_cpp
|
||||
|
||||
**Static lib target:** `fn_module_data_table`
|
||||
**Header path:** `data_table/data_table.h`
|
||||
**Namespace:** `data_table`
|
||||
**Entry function:** `void data_table::render(const char* id, const std::vector<TableInput>& tables, State& state, std::vector<TableEvent>* events_out, bool show_chrome)`
|
||||
**State:** `data_table::State` opaco. Debe persistir entre frames (`static` per panel, NO stack-local).
|
||||
**Linkage en apps:** explicito.
|
||||
**Version actual:** `1.4.0` (sera `2.0.0` post 0107c).
|
||||
|
||||
### Opt-in en una app
|
||||
|
||||
1. `app.md` frontmatter:
|
||||
```yaml
|
||||
uses_modules:
|
||||
- name: data_table_cpp
|
||||
min_version: "1.4" # o "2.0" post 0107c
|
||||
```
|
||||
2. `CMakeLists.txt`:
|
||||
```cmake
|
||||
target_link_libraries(<app> PRIVATE fn_module_data_table)
|
||||
```
|
||||
3. Header en el `.cpp`:
|
||||
```cpp
|
||||
#include "data_table/data_table.h"
|
||||
#include "core/data_table_types.h"
|
||||
```
|
||||
4. Reservar `data_table::State` persistente entre frames. NO ponerla en stack.
|
||||
|
||||
### Tipos publicos
|
||||
|
||||
```cpp
|
||||
struct TableInput {
|
||||
std::string name; // identificador estable (joins)
|
||||
std::vector<std::string> headers; // names de columna
|
||||
std::vector<ColumnType> types; // String/Int/Float/Bool/Date/Json/Auto
|
||||
const char* const* cells; // row-major, cells[r*cols + c]
|
||||
int rows;
|
||||
int cols;
|
||||
std::vector<ColumnSpec> column_specs; // renderer config per col
|
||||
};
|
||||
|
||||
enum class ColumnType : uint8_t {
|
||||
Auto = 0, String, Int, Float, Bool, Date, Json,
|
||||
};
|
||||
|
||||
struct ColumnSpec {
|
||||
std::string id; // estable; usado en TQL
|
||||
CellRenderer renderer = CellRenderer::Text;
|
||||
|
||||
// Renderer-specific (ver tabla mas abajo)
|
||||
std::vector<BadgeRule> badges;
|
||||
std::vector<ChipRule> chips;
|
||||
std::vector<IconMapEntry> icon_map;
|
||||
bool progress_scale_100 = false;
|
||||
std::string progress_color_hex;
|
||||
float duration_warn_ms = 1000.0f;
|
||||
float duration_error_ms = 5000.0f;
|
||||
std::string button_action;
|
||||
std::string button_label;
|
||||
std::string button_color_hex;
|
||||
char dots_separator = ',';
|
||||
float dots_glyph_size = 0.0f;
|
||||
int dots_max = 0;
|
||||
bool dots_show_count = false;
|
||||
double range_min = 0.0;
|
||||
double range_max = 1.0;
|
||||
float range_alpha = 0.25f;
|
||||
std::vector<ColorStop> color_stops;
|
||||
std::string tooltip; // "auto" -> show cell value
|
||||
bool tooltip_on_hover = false;
|
||||
};
|
||||
|
||||
enum class CellRenderer : uint8_t {
|
||||
Text=0, Badge=1, Progress=2, Duration=3, Icon=4, Button=5,
|
||||
Dots=8, CategoricalChip=9, ColorScale=10,
|
||||
};
|
||||
|
||||
struct BadgeRule { std::string value, color_hex, label; };
|
||||
struct ChipRule { std::string match, color; };
|
||||
struct IconMapEntry { std::string value, icon_name, color_hex; };
|
||||
struct ColorStop { float position; std::string color; };
|
||||
|
||||
enum class TableEventKind : uint8_t {
|
||||
ButtonClick=1, RowDoubleClick=2, RowRightClick=3, CellEdit=4,
|
||||
};
|
||||
|
||||
struct TableEvent {
|
||||
TableEventKind kind;
|
||||
int row; // index en TableInput, NO en stage output
|
||||
int col;
|
||||
std::string column_id; // ColumnSpec.id de la columna clicada
|
||||
std::string action_id; // para ButtonClick: ColumnSpec.button_action
|
||||
std::string value; // valor de la celda
|
||||
};
|
||||
|
||||
enum class JoinStrategy : uint8_t { Left, Inner, Right, Full };
|
||||
```
|
||||
|
||||
### Capacidad 1: render basico (Text)
|
||||
|
||||
Caso minimo — todas las columnas como texto plano.
|
||||
|
||||
```cpp
|
||||
static data_table::State st;
|
||||
data_table::TableInput tbl;
|
||||
tbl.name = "items";
|
||||
tbl.headers = {"id", "name", "price"};
|
||||
tbl.types = {data_table::ColumnType::String,
|
||||
data_table::ColumnType::String,
|
||||
data_table::ColumnType::Float};
|
||||
tbl.cells = &cells_flat[0]; // row-major
|
||||
tbl.rows = N;
|
||||
tbl.cols = 3;
|
||||
|
||||
data_table::render("##items_tbl", {tbl}, st);
|
||||
```
|
||||
|
||||
NO `column_specs` necesario — `CellRenderer::Text` es default. Tabla aparece con sort, filter chips, freeze cols, todo gratis.
|
||||
|
||||
### Capacidad 2: Badge — colored badge per value
|
||||
|
||||
Para columnas con valores categoricos (status, version, env).
|
||||
|
||||
```cpp
|
||||
tbl.column_specs.resize(tbl.cols);
|
||||
for (int i = 0; i < tbl.cols; i++) tbl.column_specs[i].id = tbl.headers[i];
|
||||
|
||||
data_table::ColumnSpec& cs = tbl.column_specs[1]; // col "status"
|
||||
cs.renderer = data_table::CellRenderer::Badge;
|
||||
cs.badges = {
|
||||
data_table::BadgeRule{"ok", "#22c55e", ""}, // verde
|
||||
data_table::BadgeRule{"error", "#ef4444", "ERR"}, // rojo + label custom
|
||||
data_table::BadgeRule{"running", "#f59e0b", ""}, // amber
|
||||
};
|
||||
```
|
||||
|
||||
- Match exacto, case-sensitive.
|
||||
- `label` vacio = usa el valor de la celda como texto.
|
||||
- Si ningun rule matchea, se renderiza como Text (sin badge).
|
||||
|
||||
### Capacidad 3: CategoricalChip — dot + texto
|
||||
|
||||
Igual que Badge pero solo dot de color a la izquierda del texto. Menos visual que Badge.
|
||||
|
||||
```cpp
|
||||
cs.renderer = data_table::CellRenderer::CategoricalChip;
|
||||
cs.chips = {
|
||||
{"go", "#3b82f6"},
|
||||
{"py", "#22c55e"},
|
||||
{"cpp", "#a855f7"},
|
||||
};
|
||||
```
|
||||
|
||||
Pattern canonico apps:
|
||||
- `app_gestion`: `status_col` + `enabled_col`.
|
||||
- `data_factory`: kind columns + status columns.
|
||||
- `registry_dashboard`: language column.
|
||||
|
||||
### Capacidad 4: ColorScale — gradient en background de celda
|
||||
|
||||
Numerico continuo. Mapea `range_min..range_max` a gradient (default green→amber→red).
|
||||
|
||||
```cpp
|
||||
cs.renderer = data_table::CellRenderer::ColorScale;
|
||||
cs.range_min = 0.0;
|
||||
cs.range_max = 5000.0; // ms
|
||||
cs.range_alpha = 0.30f; // bajo para legibilidad
|
||||
|
||||
// Opcional: custom stops
|
||||
cs.color_stops = {
|
||||
{0.0f, "#22c55e"},
|
||||
{0.5f, "#f59e0b"},
|
||||
{1.0f, "#ef4444"},
|
||||
};
|
||||
```
|
||||
|
||||
Pattern canonico: `duration_ms`, `latency_ms`, `bytes_transferred`. Apps: `data_factory` (node_runs), `services_monitor` (health latency).
|
||||
|
||||
### Capacidad 5: Duration — milisegundos con thresholds
|
||||
|
||||
Especifico para tiempos. Renderiza valor formateado (`123ms`, `1.2s`) + color por threshold.
|
||||
|
||||
```cpp
|
||||
cs.renderer = data_table::CellRenderer::Duration;
|
||||
cs.duration_warn_ms = 1000.0f;
|
||||
cs.duration_error_ms = 5000.0f;
|
||||
```
|
||||
|
||||
< warn = verde, warn..error = amber, > error = rojo.
|
||||
|
||||
### Capacidad 6: Progress — barra de progreso
|
||||
|
||||
```cpp
|
||||
cs.renderer = data_table::CellRenderer::Progress;
|
||||
cs.progress_scale_100 = false; // true -> valor en 0..100, false -> 0..1
|
||||
cs.progress_color_hex = "#3b82f6"; // override; "" -> ImPlot auto
|
||||
```
|
||||
|
||||
### Capacidad 7: Icon — Tabler icon lookup
|
||||
|
||||
```cpp
|
||||
cs.renderer = data_table::CellRenderer::Icon;
|
||||
cs.icon_map = {
|
||||
{"ok", "TI_CHECK", "#22c55e"},
|
||||
{"error", "TI_X", "#ef4444"},
|
||||
{"pending", "TI_CIRCLE", "#a3a3a3"},
|
||||
};
|
||||
```
|
||||
|
||||
Iconos disponibles: `cpp/functions/core/icons_tabler.h` (Tabler v3.41, 1500+ glyphs).
|
||||
|
||||
### Capacidad 8: Button — clickable + event sink
|
||||
|
||||
Genera click events que la app procesa.
|
||||
|
||||
```cpp
|
||||
cs.renderer = data_table::CellRenderer::Button;
|
||||
cs.button_action = "retry_run"; // semantic id que la app reconoce
|
||||
cs.button_label = "Retry"; // "" -> usa valor de la celda como label
|
||||
cs.button_color_hex = "#3b82f6"; // "" -> color default
|
||||
```
|
||||
|
||||
Consumir events:
|
||||
|
||||
```cpp
|
||||
std::vector<data_table::TableEvent> events;
|
||||
data_table::render("##runs", {tbl}, st, &events);
|
||||
|
||||
for (const auto& ev : events) {
|
||||
if (ev.kind == data_table::TableEventKind::ButtonClick) {
|
||||
if (ev.action_id == "retry_run") {
|
||||
retry_job(ev.value); // ev.value = celda donde se clico
|
||||
}
|
||||
else if (ev.action_id == "cancel_job") {
|
||||
cancel_job(ev.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`events_out` puede ser `nullptr` si no quieres consumir eventos (back-compat).
|
||||
|
||||
### Capacidad 9: Dots — inline status sparkline
|
||||
|
||||
Celda contiene tokens separados (`"ok,error,ok,ok"`). Renderiza dots de color por token.
|
||||
|
||||
```cpp
|
||||
cs.renderer = data_table::CellRenderer::Dots;
|
||||
cs.badges = { // reutiliza BadgeRule.color_hex (NO label)
|
||||
{"ok", "#22c55e", ""},
|
||||
{"error", "#ef4444", ""},
|
||||
};
|
||||
cs.dots_separator = ',';
|
||||
cs.dots_glyph_size = 0.0f; // 0 = default font size
|
||||
cs.dots_max = 10; // hard limit; 0 = unlimited
|
||||
cs.dots_show_count = true; // append " (N)" tras los dots
|
||||
```
|
||||
|
||||
Pattern canonico: timeline de status de runs recientes.
|
||||
|
||||
### Capacidad 10: Tooltip por celda
|
||||
|
||||
```cpp
|
||||
cs.tooltip = "auto"; // muestra el valor de la celda en hover
|
||||
// o:
|
||||
cs.tooltip = "Click to retry";
|
||||
cs.tooltip_on_hover = true;
|
||||
```
|
||||
|
||||
### Capacidad 11: RowDoubleClick — drill / open detail
|
||||
|
||||
Sin setup especial. Solo consumir el event.
|
||||
|
||||
```cpp
|
||||
for (const auto& ev : events) {
|
||||
if (ev.kind == data_table::TableEventKind::RowDoubleClick) {
|
||||
open_detail_modal(ev.row);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`ev.row` es el indice en `TableInput.cells`, NO en el output del stage activo si hay TQL aplicado.
|
||||
|
||||
### Capacidad 12: RowRightClick — context menu app-side
|
||||
|
||||
Igual que RowDoubleClick. App abre su propio popup ImGui cuando recibe el event.
|
||||
|
||||
```cpp
|
||||
if (ev.kind == data_table::TableEventKind::RowRightClick) {
|
||||
g_ctx_menu_row = ev.row;
|
||||
ImGui::OpenPopup("##row_ctx");
|
||||
}
|
||||
// En el siguiente frame:
|
||||
if (ImGui::BeginPopup("##row_ctx")) {
|
||||
if (ImGui::MenuItem("Inspect")) inspect(g_ctx_menu_row);
|
||||
if (ImGui::MenuItem("Delete")) delete_row(g_ctx_menu_row);
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
```
|
||||
|
||||
### Capacidad 13: Joins entre tablas
|
||||
|
||||
Pasar multiples `TableInput`. El usuario configura join via UI de chips (drag table → drop). El primer `tables[0]` es main, los demas son joinables.
|
||||
|
||||
```cpp
|
||||
data_table::TableInput main_tbl, lookup_tbl;
|
||||
// ... fill ambas ...
|
||||
data_table::render("##joined", {main_tbl, lookup_tbl}, st, &events);
|
||||
```
|
||||
|
||||
Strategies: `JoinStrategy::{Left, Inner, Right, Full}`. Default Left.
|
||||
|
||||
### Capacidad 14: TQL pipeline (filter / breakout / agg / sort)
|
||||
|
||||
Pipeline declarativo construido por usuario via chips bar. Estado en `State.stages[]`. App no toca esto — el modulo lo gestiona.
|
||||
|
||||
Stages adicionales (`State.stages.resize(N)`) si la app quiere PRE-cargar un pipeline:
|
||||
|
||||
```cpp
|
||||
data_table::Stage s;
|
||||
s.filters.push_back({0, data_table::Op::Eq, "go"}); // col 0 == "go"
|
||||
s.breakouts.push_back("lang"); // group by lang
|
||||
s.aggregations.push_back({data_table::AggFn::Count}); // count(*)
|
||||
st.stages.push_back(s);
|
||||
st.active_stage = (int)st.stages.size() - 1;
|
||||
```
|
||||
|
||||
Agg fns: `Count`, `Sum`, `Avg`, `Min`, `Max`, `Median`, `P95`, `Distinct`.
|
||||
|
||||
### Capacidad 15: Ask AI panel
|
||||
|
||||
Usuario abre via boton "Ask AI" en chips bar. Modal con prompt natural language → llama `llm_anthropic::ask` (si `FN_LLM_ANTHROPIC` definido) → devuelve TQL o SQL ejecutable → Apply mutates `State.stages`.
|
||||
|
||||
App NO necesita setup. Solo `ANTHROPIC_API_KEY` en env.
|
||||
|
||||
### Capacidad 16: Color rules condicionales
|
||||
|
||||
Reglas de color que el usuario configura via header menu → "Conditional color". Tres tipos:
|
||||
|
||||
- **CellBg**: color de fondo fijo si valor matchea.
|
||||
- **CategoricalDot**: dot de color izquierdo (igual que `chips` pero configurado en runtime por usuario).
|
||||
- **NumericRange**: gradient sobre rango numerico (igual que `ColorScale` pero por regla, no por spec).
|
||||
|
||||
Persistencia en `State.stages[k].color_rules` — la app puede preset reglas en su setup si quiere, pero normal es dejar al usuario.
|
||||
|
||||
### Capacidad 17: Drill-down between stages
|
||||
|
||||
Usuario click derecho en celda de stage > 0 → "Drill into N=valor" → anade filter al stage previo, activa ese stage. Stack de back/forward en `State.drill_back/forward`.
|
||||
|
||||
App no toca. Solo `data_table::render` lo provee.
|
||||
|
||||
### Capacidad 18: Save/Load TQL desde disco
|
||||
|
||||
Modal "Save TQL" / "Load TQL" — escribe/lee `<state>.tql` en `local_files/`. App no toca.
|
||||
|
||||
### Capacidad 19: Export CSV
|
||||
|
||||
Boton "Export CSV" en chips bar. Escribe el output del stage activo a CSV. App no toca.
|
||||
|
||||
### Capacidad 20: `show_chrome` toggle
|
||||
|
||||
Si `show_chrome=false`, oculta chips bar + breadcrumb. Util para dashboards densos donde la tabla es read-only. Usuario puede reactivar via boton "Show UI".
|
||||
|
||||
```cpp
|
||||
data_table::render("##compact", {tbl}, st, &events, /*show_chrome=*/false);
|
||||
```
|
||||
|
||||
### Capacidad 21: Module version metadata (post 0107c)
|
||||
|
||||
```cpp
|
||||
const char* data_table::module_version(); // "2.0.0"
|
||||
const char* data_table::module_description();
|
||||
```
|
||||
|
||||
Para que About panel de la app muestre version del modulo, no solo del app.
|
||||
|
||||
### Cuando usar `data_table::render` vs `ImGui::BeginTable` directo
|
||||
|
||||
| Caso | API correcta | Razon |
|
||||
|---|---|---|
|
||||
| **Tabla de datos** con N filas dinamicas, sort/filter/freeze/drill | `data_table::render` | Es lo que el modulo aporta |
|
||||
| **KPI grid** (4-8 cards en grid de columnas) | `ImGui::BeginTable` directo | No es tabla de datos — layout helper |
|
||||
| **Form key/value** (inspector, settings, schema metadata) | `ImGui::BeginTable` directo | Form fijo, columnas estaticas |
|
||||
| **2-col layout** (tree izda / detail dcha) | `ImGui::BeginTable` directo | Splitter pattern, no data table |
|
||||
| **Chart grid** (4 plots lado a lado) | `ImGui::BeginTable` directo | Layout multi-panel, no datos |
|
||||
| **Schema editor row-form** (1 fila por field, con inputs editables por columna) | discutible — `ImGui::BeginTable` si rows < 20, `data_table::render` con `CellRenderer::Button`/edit si rows >> 20 | Depende del scale |
|
||||
| **Recipes / Flows / Issues list** (data dinamica con sort/filter) | `data_table::render` (migrar si esta inline) | Es tabla de datos canonica |
|
||||
|
||||
Regla heuristica: si la tabla necesita SORT, FILTER, FREEZE o RENDERERS DECLARATIVOS → usar `data_table::render`. Si es solo layout → BeginTable directo OK.
|
||||
|
||||
Audit automatico: `mcp__registry__fn_run audit_data_table_usage` detecta apps con `inline_begintable` warnings + verifica `state_not_persistent`, `no_child_host`, `no_event_sink`, `cmake_missing_link`. Output formato `dev/data_table_integration_audit.md`.
|
||||
|
||||
### Host container — BeginChild recommended
|
||||
|
||||
`data_table::render` NO crea su propio `BeginChild` para el contenido principal (si para sub-paneles de viz). Si la app no envuelve la llamada en un host con bounds definidos:
|
||||
|
||||
- La tabla se renderiza inline con `ImGui::GetContentRegionAvail()` actual.
|
||||
- Si el contenedor padre es scrollable, la tabla NO scrollea independientemente — comparte el scroll del padre.
|
||||
- Si hay widgets DESPUES del `render()` en el mismo Begin/BeginChild, compiten por espacio.
|
||||
|
||||
Pattern recomendado:
|
||||
|
||||
```cpp
|
||||
ImGui::BeginChild("##tbl_host", ImVec2(-1, -1)); // o tamano fijo
|
||||
data_table::render("##my_tbl", {tbl}, st, &events);
|
||||
ImGui::EndChild();
|
||||
```
|
||||
|
||||
Excepcion: si la tabla es lo UNICO que renderiza dentro de su `ImGui::Begin(...)` window, BeginChild es opcional. El audit lo marca como `no_child_host [warn]`, no error.
|
||||
|
||||
### Gotchas
|
||||
|
||||
- **`State` debe persistir entre frames**: `static` per panel. Si la pones en stack, pierdes filter chips/sort/etc cada frame. Cada panel necesita SU `State` (no compartir entre tablas distintas).
|
||||
- **`cells` row-major**: `cells[row*cols + col]`. Apps que pasan column-major rompen sin warning.
|
||||
- **`TableInput.name` no vacio si hay joins**: si declaras varias tablas, cada una con `name` unico para que el join UI las identifique.
|
||||
- **`column_specs.size() == cols`**: si no resizeas, el modulo asume `CellRenderer::Text` para columnas faltantes.
|
||||
- **`ColumnSpec.id` debe ser estable** entre frames. Si cambia, las preferencias de sort/filter del usuario sobre esa col se pierden.
|
||||
- **`button_action` debe ser unico** dentro del scope semantic de la app. Apps mismas que reusan `"retry"` en multiples columnas/contextos deberian diferenciar (`"retry_run"`, `"retry_test"`).
|
||||
- **`badges` vs `chips`**: Badge = fondo coloreado completo + label; CategoricalChip = solo dot + texto plano. Eligir segun densidad visual deseada.
|
||||
- **`Dots` reusa `badges`** map pero IGNORA `BadgeRule.label`. Solo `color_hex` por token.
|
||||
- **`ColorScale` requiere ColumnType numerico** (`Float`, `Int`, `Date`). String no calcula gradient.
|
||||
- **`tooltip = "auto"`** = string magico literal. NO `tooltip.empty()`.
|
||||
- **events_out se LIMPIA cada frame** — la app debe leerlo INMEDIATAMENTE tras `render()`. No acumular entre frames.
|
||||
- **`events.row` NO indexa al output del stage** — siempre apunta a `TableInput.cells` original. Si tu app hizo TQL pipeline, mapea manualmente via `state.row_mapping` si necesitas el row del output.
|
||||
- **Multi-instancia**: cada `data_table::render` debe usar `id` unico (`"##tbl_a"`, `"##tbl_b"`). Si dos paneles comparten id, ImGui se confunde.
|
||||
|
||||
### Pattern canonico end-to-end (replicable)
|
||||
|
||||
```cpp
|
||||
// Setup BadgeRule sets reusables a nivel de app
|
||||
static std::vector<data_table::BadgeRule> run_status_badges() {
|
||||
return {
|
||||
{"ok", "#22c55e", ""},
|
||||
{"error", "#ef4444", "ERR"},
|
||||
{"running", "#f59e0b", ""},
|
||||
{"pending", "#a3a3a3", ""},
|
||||
};
|
||||
}
|
||||
|
||||
// Inside panel render
|
||||
static data_table::State st;
|
||||
data_table::TableInput tbl;
|
||||
tbl.name = "runs";
|
||||
tbl.headers = {"id", "status", "duration_ms", "started_at", ""};
|
||||
tbl.types = {
|
||||
data_table::ColumnType::String,
|
||||
data_table::ColumnType::String,
|
||||
data_table::ColumnType::Float,
|
||||
data_table::ColumnType::Date,
|
||||
data_table::ColumnType::String, // actions col (vacio, solo button)
|
||||
};
|
||||
tbl.cells = &flat_cells[0];
|
||||
tbl.rows = N;
|
||||
tbl.cols = 5;
|
||||
|
||||
tbl.column_specs.resize(tbl.cols);
|
||||
for (int i = 0; i < tbl.cols; i++) tbl.column_specs[i].id = tbl.headers[i].empty()
|
||||
? std::string("actions") : tbl.headers[i];
|
||||
|
||||
// Status → CategoricalChip
|
||||
tbl.column_specs[1].renderer = data_table::CellRenderer::CategoricalChip;
|
||||
tbl.column_specs[1].chips = {
|
||||
{"ok", "#22c55e"}, {"error", "#ef4444"}, {"running", "#f59e0b"},
|
||||
};
|
||||
|
||||
// duration_ms → ColorScale + Duration thresholds combinados
|
||||
tbl.column_specs[2].renderer = data_table::CellRenderer::Duration;
|
||||
tbl.column_specs[2].duration_warn_ms = 1000.0f;
|
||||
tbl.column_specs[2].duration_error_ms = 5000.0f;
|
||||
|
||||
// Actions col → Button retry
|
||||
tbl.column_specs[4].renderer = data_table::CellRenderer::Button;
|
||||
tbl.column_specs[4].button_action = "retry_run";
|
||||
tbl.column_specs[4].button_label = "Retry";
|
||||
tbl.column_specs[4].tooltip = "Re-run this job";
|
||||
tbl.column_specs[4].tooltip_on_hover = true;
|
||||
|
||||
// Render
|
||||
std::vector<data_table::TableEvent> events;
|
||||
data_table::render("##runs_tbl", {tbl}, st, &events);
|
||||
|
||||
// Consume events
|
||||
for (const auto& ev : events) {
|
||||
if (ev.kind == data_table::TableEventKind::ButtonClick && ev.action_id == "retry_run") {
|
||||
retry_run_by_id(ev.value); // ev.value = id (col 0 de la fila clicada... NO)
|
||||
// ^ CORRECCION: ev.value = valor de la celda del boton
|
||||
// = "Retry" si button_label fijo. Para obtener el ID
|
||||
// de la fila: mapear via ev.row y leer tbl.cells[ev.row*cols + 0]
|
||||
}
|
||||
if (ev.kind == data_table::TableEventKind::RowDoubleClick) {
|
||||
open_run_detail_modal(/*run_id=*/ tbl.cells[ev.row * tbl.cols + 0]);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Capability matrix consolidada
|
||||
|
||||
| Capacidad | Renderer / Event | ColumnSpec fields | Apps que la usan hoy |
|
||||
|---|---|---|---|
|
||||
| Plain text | `Text` (default) | — | todas |
|
||||
| Colored badge | `Badge` | `badges` | registry_dashboard, app_gestion (version pinning) |
|
||||
| Categorical dot | `CategoricalChip` | `chips` | services_monitor, dag_engine_ui, data_factory |
|
||||
| Numeric heatmap | `ColorScale` | `range_min/max/alpha`, `color_stops` | data_factory (duration), services_monitor (latency) |
|
||||
| Duration with thresholds | `Duration` | `duration_warn_ms`, `duration_error_ms` | data_factory (node_runs) |
|
||||
| Progress bar | `Progress` | `progress_scale_100`, `progress_color_hex` | (raro — candidato para nuevos consumers) |
|
||||
| Tabler icon | `Icon` | `icon_map` | (raro — candidato) |
|
||||
| Click button | `Button` + ButtonClick | `button_action`, `button_label`, `button_color_hex` | graph_explorer (cancel/delete_job), data_factory |
|
||||
| Status timeline | `Dots` | `dots_separator`, `dots_max`, `badges` map | registry_dashboard (recent runs) |
|
||||
| Hover tooltip | (cualquier renderer) | `tooltip`, `tooltip_on_hover` | varios |
|
||||
| Open detail | RowDoubleClick | — | app_gestion, data_factory, services_monitor |
|
||||
| Context menu | RowRightClick | — | (raro) |
|
||||
| Joins | (UI-driven) | usar varios `TableInput` | (raro — UI feature) |
|
||||
| TQL pipeline | (UI-driven) | preset via `State.stages` | data_factory (preload filters) |
|
||||
| Ask AI | (UI-driven) | `FN_LLM_ANTHROPIC` env | usuario lo dispara |
|
||||
| Color rules runtime | (UI-driven) | `State.stages[k].color_rules` | usuario lo configura |
|
||||
| Drill-down | (UI-driven) | `State.drill_back/forward` | usuario lo dispara |
|
||||
| Save/Load TQL | (UI-driven) | — | usuario lo dispara |
|
||||
| Export CSV | (UI-driven) | — | usuario lo dispara |
|
||||
| Hide chrome | `show_chrome=false` | parametro de render() | dashboards densos |
|
||||
|
||||
---
|
||||
|
||||
## Como NO conectarse a un modulo
|
||||
|
||||
Anti-patrones que generan "conexiones raras entre apps":
|
||||
|
||||
| Anti-patron | Consecuencia | Sustituir por |
|
||||
|---|---|---|
|
||||
| Llamar funciones internas del modulo (helpers `static` en `data_table.cpp`) via include de path interno | Acoplamiento fuerte; rompe en cualquier refactor | API publica `data_table::*` solo |
|
||||
| Reescribir logica que el modulo ya hace (parser TQL propio, color picker propio) | Duplicacion + drift | Usar API; si falta capacidad, abrir issue |
|
||||
| Acceder a `State` fields directamente sin pasar por la API | State es opaco — campos pueden cambiar versiones | Usar la UI (chips bar) o setear `State.stages` segun esta doc |
|
||||
| Inventar nuevos `action_id` cross-app esperando que otro modulo lo entienda | events son scoped a la app que los emite | Cada app maneja sus propios action_ids |
|
||||
| Asumir `events_out` acumula entre frames | Se limpia cada frame | Procesar inmediatamente despues de `render()` |
|
||||
| Pasar `State` por valor / hacer copia | Pierde sort/filter/drill cada frame | `static data_table::State st;` referencia |
|
||||
| Compartir `State` entre dos `render()` con distintos `id` | UI dual-rendering corrupta | UN State per panel |
|
||||
| Reuso de `column_specs` con `id` que cambia entre frames | Sort/filter preferences perdidas | `id` estable como `TableInput.headers[i]` |
|
||||
| Saltarse `column_specs.resize(cols)` y solo settear algunas | Modulo asume `Text` para faltantes — comportamiento confuso | resize SIEMPRE a `cols` |
|
||||
| Pasar TableInput de UNA tabla con `name=""` y esperar joins | Joins necesitan name unico | Setear `name` siempre |
|
||||
| Mezclar Badge + CategoricalChip + Dots en misma columna (no compatible) | Solo uno por columna | Eligir basado en densidad visual |
|
||||
| Acoplar app a header interno (`modules/data_table/data_table_internal.h`) | header NO PUBLICO — solo para .cpp del modulo | API publica `data_table/data_table.h` solo |
|
||||
|
||||
---
|
||||
|
||||
## Ciclo de vida de un modulo
|
||||
|
||||
Crear modulo nuevo (ej. `chat_ia_cpp` tras 0107 cerrar):
|
||||
|
||||
1. `mkdir modules/chat_ia/` con:
|
||||
- `module.md` (frontmatter: name, version, description, members, dir_path).
|
||||
- `CMakeLists.txt` definiendo `add_library(fn_module_chat_ia STATIC ...)`.
|
||||
- `chat_ia.cpp` + `chat_ia.h` (entry function).
|
||||
- `chat_ia_internal.h` (compartido entre sub-cpp del modulo si tiene >1 archivo).
|
||||
|
||||
2. Anadir `add_subdirectory` en `cpp/CMakeLists.txt`.
|
||||
|
||||
3. Crear seccion en este doc (`docs/MODULES_API.md`) siguiendo el template de `data_table_cpp`.
|
||||
|
||||
4. Anadir fila en `modules/README.md`.
|
||||
|
||||
5. `fn index` registra el modulo en `registry.db::modules`.
|
||||
|
||||
6. Cada bump de version: `/version modules/chat_ia <major|minor|patch> "<reason>"`. Edita `module.md::version` + `## Capability growth log`.
|
||||
|
||||
7. Apps consumidoras declaran:
|
||||
```yaml
|
||||
uses_modules:
|
||||
- name: chat_ia_cpp
|
||||
min_version: "0.1"
|
||||
```
|
||||
+ `target_link_libraries(<app> PRIVATE fn_module_chat_ia)`.
|
||||
|
||||
8. `fn doctor modules` audita drift uses_modules vs uses_functions (0107a).
|
||||
|
||||
---
|
||||
|
||||
## E2E testing por modulo
|
||||
|
||||
Cada modulo declara su test en `modules/<name>/e2e_run.sh` (o equivalente). Ver issue 0108 para el testbed agresivo de `data_table_cpp` que combina:
|
||||
|
||||
- Catch2 unit (`cpp/tests/`) para sub-funciones puras.
|
||||
- Golden PNG diff (`primitives_gallery --capture`).
|
||||
- Auto-driving worker (modelo `altsnap_jitter_test`).
|
||||
- Dear ImGui Test Engine (`FN_BUILD_TESTS=ON`).
|
||||
- `--self-test` flag headless.
|
||||
- Cross-platform run (`e2e_run_cpp_windows.sh`).
|
||||
|
||||
Modulos sin testbed dedicado → al menos build smoke + linkage de >=2 apps consumidoras debe verificarse en `e2e_run.sh`.
|
||||
|
||||
---
|
||||
|
||||
## Mantenimiento de este doc
|
||||
|
||||
- Actualizar tras cada bump de version de un modulo (referenciado por `/version`).
|
||||
- Si una app introduce un patron nuevo no listado en capability matrix → anadirlo aqui ANTES de commit (o la fila queda undocumentada).
|
||||
- `fn doctor modules` (0107a) verifica que cada modulo en `registry.db::modules` tiene seccion aqui. Si no, WARN.
|
||||
- Generado parcialmente desde `module.md` via codegen futuro (issue postponed). Hoy mantenido a mano.
|
||||
Reference in New Issue
Block a user