Commit Graph

21 Commits

Author SHA1 Message Date
egutierrez f0d8a5ad04 feat(0036d): promote kind-aware (Group → clear group_id)
NodeGroups window kind=Group ahora expone un boton SmallButton(TI_ARROW_UP)
por fila que saca la entidad del grupo (group_id = NULL) y dispara
reload del grafo. kind=Table mantiene el comportamiento de issue 0011.

- entity_ops: nueva op `entity_clear_group_id(db, id)` idempotente. Si
  la columna group_id no existe (BD pre-0035a) retorna true como no-op.
  Falla solo si la entidad no existe o SQLite revienta.
- views.cpp: extra columna "promote" en kind=Group, tooltip header
  diferenciado por kind, boton conectado a app.want_clear_group_id_entity.
- main.cpp: handler que ejecuta entity_clear_group_id, marca windows
  como dirty, llama reload_after_mutation y loguea
  `[node_groups] promoted X out of group`.
- gx-cli: flag `node update --clear-group-id` (booleano) y exposicion
  MCP en inputSchema + MCP_DISPATCH defaults para que el agente Echo
  pueda promover via tool calls.
- tests: 3 nuevos CLI (clear, idempotente, combinable con --name) y
  4 MCP (defaults, schema, dispatch end-to-end, idempotente).

WSL: 102 passed (95 base + 7).
Windows: 91 passed, 11 skipped (84 base + 7).

Refs: issues/0036d-promote-kind-aware.md
2026-05-04 01:03:11 +02:00
egutierrez 8f91b4ed23 feat(0036c): doble click en Group abre NodeGroups; cleanup Table panel
Cambia el dispatch del doble click sobre nodos del viewport: si el tipo
es Group o Table, ahora abre/enfoca la NodeGroups window correspondiente
via views_node_groups_open(...). El branch de Group ya no carga el
panel Table generico con un filtro group_id (logica heredada de 0035d
que provocaba el bug de "tabla vacia").

Limpieza correlativa en views_table:
  - Eliminado el breadcrumb "Group: <name> (N)" + boton Clear filter.
  - Eliminado el filtro r.group_id != table_filter_group_id en
    build_visible y la restriccion de types_present.
  - Eliminado el reset on-close de los campos de filtro.

Eliminados los campos AppState::table_filter_group_id y
table_filter_group_name (audit: git grep table_filter_group_id devuelve
vacio fuera de issues/).

Render de NodeGroups ahora consume focus_request: llama
SetNextWindowFocus() antes de Begin y SetWindowFocus() dentro, asi la
window queda al frente tanto al crearse como al re-enfocarse.

El right-click "Open NodeGroups" del context menu sigue intacto
(want_toggle_nodegroups + node_groups_set_expanded). El doble click es
flujo paralelo nuevo.

Refs: issues/0036c-double-click-group-opens-nodegroups.md
2026-05-04 00:56:44 +02:00
egutierrez d6e13fddc3 feat(0036b): NodeGroups admite kind=Group + loader entities
NodeGroupsWindowState gana un discriminador `kind` (Table | Group) y
un flag `focus_request` (lo consumira 0036c). Por defecto Table, asi
que el flujo historico (DuckDB rows tras expand de un nodo Table) no
cambia.

kind=Group lee directamente operations.db consultando
`entities WHERE group_id = container_id` con columnas fijas
(id, name, type_ref, status, updated_at) ordenadas por updated_at DESC.
Los nuevos loaders viven en node_groups.cpp:

  - node_groups_count_for_group  -> SELECT count(*) ...
  - node_groups_page_for_group   -> SELECT id,name,type_ref,status,
                                     updated_at ... LIMIT ? OFFSET ?

Para columnas, opcion (A) del issue: pre-popular meta.columns con la
lista fija al abrir kind=Group, asi el render se mantiene generico.
NodeGroupsRow.values guarda los 5 campos en ese orden y row.id es la
key natural (= entity_id de la fila — al ser ya entidad, no hace falta
promocionarla).

Render en views.cpp ramifica por kind:

  - Table: layout original [id_col + columns + promoted] con doble
    click -> promote/focus.
  - Group: layout [columns fijas] sin promoted. Doble click sobre la
    fila ya pone want_focus_entity = id (los flujos posteriores 0036c-e
    afinan UX). Right click ofrece "Focus in Inspector".

main.cpp dispatcha por kind al refrescar paginas y, al cerrar via X,
solo llama a node_groups_set_expanded para kind=Table (Group no usa
ese flag).

views_node_groups_windows_sync se hace kind-aware: solo reconcilia
entries kind=Table contra el set de Tables expandidas; no toca las
entries kind=Group (las gestiona views_node_groups_open).

Nueva API publica:

  views_node_groups_open(app, container_id, kind, ops_db)

Crea o reusa la entry, setea focus_request=true y para kind=Group
pre-popula meta.columns + intenta leer `name` del Group para el
titulo. Sin caller todavia — la consume 0036c.

Tests:
  - tests/test_node_groups_loader.py (6 tests) verifica el contrato
    SQL via gx-cli. Nuevo subcomando `gx-cli group page <id>` espejea
    el loader C++ exactamente (mismo SQL); tambien expuesto como tool
    MCP `group_page` para que Echo pueda inspeccionar Groups.

Resultado:
  - WSL: 89 -> 95 passed
  - Windows: 78+11 -> 84+11 passed
  - Build C++ Windows limpio, sin warnings nuevos.
  - Regresion kind=Table: comportamiento identico (mismo render,
    mismo loader DuckDB).

Refs: issues/0036b-kind-discriminator-and-group-loader.md
2026-05-04 00:52:25 +02:00
egutierrez 810b564127 refactor(0036a): rename Table-expanded -> NodeGroups (paperwork)
Rename masivo sin cambio de comportamiento. Habilita 0036b-f que ya
asumen la nueva convencion.

Archivos:
- tableview.{cpp,h} -> node_groups.{cpp,h} (git mv para preservar history)
- CMakeLists.txt: tableview.cpp -> node_groups.cpp

Tipos:
- TableWindowState  -> NodeGroupsWindowState  (views.h)
- TableMetadata     -> NodeGroupsMeta         (node_groups.h)
- TablePageRow      -> NodeGroupsRow          (node_groups.h)

Campos AppState:
- table_windows         -> node_groups_windows
- table_node_counts     -> node_groups_counts
- toggle_expanded_id    -> toggle_nodegroups_id
- want_toggle_expanded  -> want_toggle_nodegroups

Funciones (window por contenedor — NO el panel generico Table):
- tableview_create / count / page / smoke_test / resolve_path /
  refresh_counts / list_columns / get_metadata / set_expanded /
  set_columns / promote_row / demote_row / ingest_file
  -> prefijo node_groups_*
- views_table_window         -> views_node_groups_window
- views_table_windows_sync   -> views_node_groups_windows_sync
- views_table_overlay        -> views_node_groups_overlay

Strings de UI:
- "Expand table" / "Collapse table" -> "Open NodeGroups" / "Close NodeGroups"
- Window title "<icon> <name>" -> "<icon> NodeGroups: <name>"
- Tooltip "(no expanded tables)" -> "(no open NodeGroups)"
- Logs [tableview_*] -> [node_groups_*]

Preservados intencionalmente (no son cambio de identificadores C++):
- CLI flag --test-tableview (cambiarlo seria cambio de behavior publico)
- Valor 'tableview' en columna entities.source (cambiarlo afectaria
  datos persistidos en BD)

NO tocado:
- Panel generico Table (views_table, panel_table, table_rows,
  table_show_all, table_search_buf, table_filter_*, table_col_filters,
  table_active_tab, TableRow, table_filter_group_*, etc.)
- issues/completed/* (historia)

Verificacion:
- Build C++ Linux + Windows: green sin warnings nuevos.
- pytest WSL: 89 passed.
- pytest Windows: 78 passed + 11 skipped.
- git grep audit: solo residuos en issues/ (historia) + CLI flag y
  source DB value preservados.

Refs: issues/0036a-rename-nodegroups.md
2026-05-04 00:43:16 +02:00
egutierrez b67da92e18 feat(0035d): doble click en Group abre tableview filtrado por group_id
- entity_ops: EntityRowSnapshot.group_id + SQL con COALESCE(group_id,'')
  + deteccion via PRAGMA para BDs viejas sin la columna.
- views.h: TableRow.group_id + AppState.table_filter_group_id /
  table_filter_group_name (RAM-only).
- main.cpp: dispatch en want_open_note — si type_ref == "Group", setea
  filtro de grupo + abre panel Table en vez de Note. Reset de search
  buf y col_filters al entrar al drill-in para que el usuario vea todo
  el contenido del grupo.
- views.cpp: build_visible compone group_id con search/tabs/col_filters
  (AND). types_present se reduce a tipos presentes en el grupo cuando
  hay drill-in activo. Header pintado en amarillo con TI_FOLDER +
  contador + boton "Clear group filter". Al cerrarse el panel se
  limpia el filtro automaticamente.

Tests: pytest 35 passed (WSL) / 24 passed + 11 skipped (Windows).

Refs: issues/0035d-tableview-drill-in.md
2026-05-03 14:57:20 +02:00
egutierrez 7a94160fd2 feat: catch-up de decisiones previas (Webpage→Url, anti-bot, UI 2-col, tests cross-platform)
Bloque de cambios revisados y validados con el usuario en sesiones
previas que no habian aterrizado en commits propios. Lista por tema:

* enrichers: web_search ahora usa lite.duckduckgo.com como endpoint
  primario (mas tolerante con bot detection desde IP residencial),
  con fallback al endpoint html. Detecta pagina captcha y emite
  error claro si ambos fallan. Anyade _DDGLiteParser para el formato
  lite + auto-pick de parser por contenido.

* enrichers: tipo Webpage unificado en Url (campos de cuerpo
  cacheado viven en metadata del Url). Manifests actualizados
  (applies_to: [Url]). fetch_webpage ya no convierte Url->Webpage.

* enrichers/manifest: campo `params` parseado a EnricherSpec.params
  (name, type, default_value, description). UI puede renderizar
  dialog de configuracion.

* jobs: fix de path conversion para Python embebido nativo Windows
  (no convertir a /mnt/c/... cuando el subproceso es Windows-native;
  solo cuando es bash o python via WSL).

* main.cpp: ventana ImGui (no modal) "Run enricher" con layout
  2-col (label izq, input der). Inserta job con JSON tipado. Layout
  clustering apretado: hijos del mismo anchor en un solo anillo
  alrededor del padre, sin desperdigar por anillos crecientes.

* views: inspector con layout 2-col via BeginTable (Identity,
  Schema fields, Extras). Description full-width debajo de su label.

* tests: portable conftest (auto-detecta REGISTRY_ROOT, PYTHON_BIN,
  ENRICHERS_DIR para WSL y Windows portable). _runner.py trampoline
  inyecta stub via sys.path porque embedded Python ignora PYTHONPATH.
  Tests bash-only (vendor_script, freeze, dispatcher bash, resolver
  Linux-binary) skipean en Windows. Tests existentes adaptados a
  Webpage->Url.

Resultado actual: 32 passed WSL, 21 passed + 11 skipped Windows.
2026-05-03 14:41:28 +02:00
egutierrez e6719a5ae0 fix(tableview): paths normalizados (Windows) + error visible en ventana
Hipotesis del bug 'tras promover, la tabla expandida queda a 0 filas':
en Windows std::filesystem::path::string() devuelve la ruta con
backslashes ('C:\\Users\\...\\operations.db'). Al embebirla en
'ATTACH ''<path>'' AS ops' DuckDB la interpretaba con quirks segun
version, fallaba el ATTACH (silent), pero ademas el siguiente
duckdb_open con paths mixtos podria no abrir el .duckdb correcto.

Cambios:
- tableview_resolve_path normaliza '\\' -> '/' (DuckDB acepta ambos
  para duckdb_open, pero forzamos '/' para evitar ambiguedad en SQL).
- ATTACH normaliza ops_db tambien.
- TableWindowState.last_error: cuando count o page fallan, se setea
  con el path/tabla involucrada y se muestra en rojo en la cabecera
  de la ventana. Asi el bug es visible sin abrir consola.
- tableview_page log incluye la SQL completa cuando falla — facil
  diagnosticar via stderr en linux.
2026-05-01 17:02:22 +02:00
egutierrez 87a554da84 feat(toolbar): Layout dropdown + Physics toggle, default fixed/paused
Cambios de UX en la toolbar y arranque:

- Boton 'Layout: <name>' que abre popup con la lista de layouts (force,
  grid, circular, radial, hierarchical, fixed) + 'Reset positions
  (unpin + restart)' + 'Save current layout'. Reemplaza el combo
  pequeno + los botones Save/Reset que estaban dispersos.

- Boton 'Physics: ON/OFF' (Player Play/Pause) toggle visible que
  reemplaza el checkbox 'Run layout'. Variant Primary cuando ON,
  Subtle cuando OFF.

- Default: layout_mode = 5 (fixed) y layout_running = false. Asi al
  abrir un proyecto los nodos respetan posiciones guardadas y no se
  mueven solos. El usuario activa fisicas con el boton Physics y/o
  cambia el layout desde el dropdown si quiere.

Reset layout (boton dentro del popup Layout) sigue activando physics
para que el grafo se reasiente; es el flujo natural del 'Reset'.
2026-05-01 16:54:27 +02:00
egutierrez b798454f35 feat(table-node): edge CONTAINS_ROW al promover + tabla cuadrada real
Tres ajustes derivados de feedback en uso:

1. tableview_promote_row recibe ahora `table_entity_id` y, si no es
   nulo, inserta una relacion 'CONTAINS_ROW' (id estable, INSERT OR
   IGNORE) entre la tabla origen y la entidad promovida. El viewport
   pinta la arista de pertenencia automaticamente sin codigo extra.

2. apply_types_yaml fija default_size = 32 px (world) para tipos
   Table junto al SHAPE_SQUARE ya existente. La GPU pinta el cuadrado
   real; antes era invisible bajo el overlay rectangular.

3. views_table_overlay adelgaza al rol que le toca: solo dibuja un
   contador discreto "<N> rows" debajo del cuadrado (texto pequeno
   con bg semitransparente). El cuadrado en si lo pinta el GPU.

Defensiva: views_table_windows_sync marca page_dirty=true en TODAS las
windows live tras cada sync para que el flag promoted se refresque
inmediatamente despues de promote/demote/import.
2026-05-01 14:18:26 +02:00
egutierrez c9d958f1c0 feat(table-ux): selectable rows + tables dropdown + filtros por columna
Tres cambios pequenos relacionados con la UX de las tablas:

1. fix views_table_window: la fila usaba TextUnformatted en col 0 que
   no registra hover/double-click sobre toda la fila. Reemplazado por
   ImGui::Selectable con SpanAllColumns + AllowDoubleClick — ahora el
   doble-click sobre fila no promovida promueve, sobre promovida abre
   Inspector. El popup right-click tambien funciona ahora.

2. Toolbar 'Tables (N)' dropdown que lista las Table windows abiertas
   con checkbox. Desmarcar = colapsar (cerrar ventana + expanded=false).
   Tambien tiene 'Collapse all' al final.

3. views_table (issue 0004) — filtros por columna:
   - Right-click sobre header de columna abre popup con InputText.
   - Apply / Clear / Enter aceptan y guardan en table_col_filters.
   - Chips arriba de la tabla con cada filtro activo + X para quitar.
   - Boton 'Clear all'.
   - build_visible aplica los filtros con substring case-insensitive.
2026-05-01 02:16:14 +02:00
egutierrez 5a6de53559 fix(views): WHERE expanded en sync — comparar INTEGER 1, no json('true')
json_extract(metadata,'\$.expanded') devuelve INTEGER 1 cuando el valor JSON
es true; json('true') devuelve TEXT 'true', asi que la comparacion era
1 = 'true' = 0 (falso) y views_table_windows_sync nunca encontraba las
Tables expandidas.

El bug se manifestaba como context menu Expand table sin abrir la ventana,
aunque tableview_set_expanded persistia correctamente el flag en la BD.

Fix: comparar contra 1 directamente.
2026-05-01 02:04:06 +02:00
egutierrez cedfe3b616 feat(views): ventana Table expandida + import modal (issue 0011)
- AppState::TableWindowState: estado runtime por Table expandida (meta,
  total_rows, offset, page cache, dirty, open). Mapa por entity_id.
- views_table_windows_sync: lee operations.db buscando Tables con
  metadata.expanded=true y crea/refresca/borra TableWindowState. Llamar
  tras load + reload_after_mutation.
- views_table_window: ImGui::Begin dockeable por Table expandida con
  cabecera de columnas, BeginTable + filas paginadas (200/pagina) +
  indicador 'promoted'. Doble-click promueve fila no promovida; en
  promovida abre Inspector. Right-click context menu por fila con
  Promote/Demote/Focus.
- views_import_dataset_modal: formulario File path + DuckDB path +
  Dest table + Row type. Trigger want_import.
- Toolbar 'Import dataset...' button.
- Triggers en AppState: want_promote_row, want_demote_entity,
  want_focus_entity, want_toggle_expanded, want_import.
2026-05-01 01:53:02 +02:00
egutierrez 082008bc00 feat(table-node): DuckDB foundation + render colapsado (issue 0010)
- tableview.{h,cpp}: capa C sobre DuckDB v1.1.3.
  * tableview_smoke_test (SELECT 42).
  * tableview_count (con sql_filter opcional).
  * tableview_page (LEFT JOIN sobre ops.entities via ATTACH para flag promoted).
  * tableview_create (inserta entidad type_ref='Table' con metadata pointer).
  * tableview_refresh_counts (lee Table entities, count cada DuckDB y cachea
    por user_data hash).
  * tableview_resolve_path (rel a dirname(ops_db) o absoluto).
- AppState::table_node_counts cache, refrescado tras load_input y mutaciones.
- views_table_overlay: rectangulo redondeado overlay ("Table  N") encima
  de cada nodo type_ref='Table'. Sigue camara via cam_x/cam_y/zoom.
- main.cpp:
  * --test-duckdb <path> smoke (SELECT 42).
  * --test-tableview <path> bulk test (1M rows count + page offset).
  * Refresh de counts tras load + reload_after_mutation.
  * Llamada a views_table_overlay despues de graph_labels_draw.
- CMakeLists.txt: link DuckDB::DuckDB + duckdb_copy_runtime.

Smoke tests:
- 1M rows count + page(offset=500k, limit=10) en 0.65 s end-to-end.
- Operations.db con un nodo Table apuntando a duckdb 1M filas: refresh
  reporta correctamente "1 tables, 1000000 total rows".
2026-05-01 01:24:43 +02:00
egutierrez 84afa4ce70 feat(table): vista tabla por tipo de entidad (issue 0004)
- entity_ops: entity_list_rows (bulk pull id/name/type_ref/status/updated_at).
- AppState::TableRow + cache + filtros (search substring + show_all toggle).
- views_table: tabs por type_ref (alfabetico) o tabla unica con todos los
  tipos. ImGui::BeginTable con sort + clipper para >10k filas. Click en
  Selectable selecciona el nodo en el viewport (clear + add via
  graph_viewport_*).
- views_table_refresh_indices: degree + node_idx por user_data hash.
- main.cpp: panel "Table" en g_panels; cache build tras load_input y
  reload_after_mutation.
2026-05-01 01:05:03 +02:00
egutierrez f80348d604 feat(types): Type Editor panel — CRUD de tipos en vivo (issue 0007)
- views_type_editor: panel "Types" con tabs Entities/Relations.
  Entities: name, color picker, shape combo, icon (ti-* + cp preview),
  principal_field combo, tabla de Fields (string/int/float/bool/date/url/enum)
  con required y enum values CSV; up/down/X por fila.
  Relations: name, color, style.
  Footer Save / Reload from disk + indicador dirty + error inline.
- views_type_editor_delete_modal: confirm con conteo de entidades en uso.
- types_registry: shape_name() + shape: emit en types_save_yaml para
  round-trip estable de la cosmetica editada en UI.
- main.cpp: panel "Types" en g_panels; init types_draft tras load_input;
  want_types_save -> save + apply_types_yaml + rebuild atlas + bind +
  refresh inspector caches; want_types_reload simetrico; conteo de
  uso desde operations.db cuando se abre el modal de delete.
2026-05-01 00:42:30 +02:00
egutierrez 54aba71bf5 feat(filter): tag chips + FTS5 search en toolbar (issue 0009)
- entity_ops: entity_search_fts (bm25, prefix tokens) + entity_list_by_tags (AND).
- AppState: filter_query_buf, filter_tags, filter_mode (highlight/hide), hits cache.
- views_filter_apply: combina tipos visibles + match-set (FTS query AND tags),
  highlight = color_override con alpha=0x40, hide = clear NF_VISIBLE.
- toolbar: input search + dropdown (max 20, click centra y selecciona),
  chips de tags con boton X, input para anadir tag, combo Highlight/Hide,
  Clear filter.
- Inspector: right-click sobre tag chip lo anade al filtro.
- main.cpp: reapply en cada frame si filter_dirty; cam_x/y al focus_target.
2026-05-01 00:23:26 +02:00
egutierrez b2ae793727 feat(views): Inspector editable — identidad, fields tipados, extras, tags
Issue 0008 — refactor del panel Inspector de read-only a editable.

views.h:
- AppState gana ParsedTypes parsed_types (schema vivo del proyecto), draft
  del Inspector (insp_*: name/type/desc/status buffers, field_keys/values
  paralelas, is_extra mask, tags vector, dirty flag), y dos triggers
  (want_inspector_save, want_inspector_discard).
- Helpers expuestos: views_inspector_clear_draft, _refresh_caches,
  _load_draft, _build_record.

views.cpp:
- views_inspector_load_draft: entity_load_full → buffers; campos del
  schema primero (orden del EntitySpec), extras detras.
- views_inspector_build_record: reconstruye EntityRecord respetando el
  schema para decidir is_string de cada campo (FK_BOOL → 'true'/'false',
  FK_INT/FLOAT → literal, resto → string). Extras siempre string.
- views_inspector: render por bloques:
  * Identity: name, type combo (lista del proyecto + tipos del grafo),
    status combo, description multiline.
  * Fields del schema: render por kind (string→InputText con hint,
    int→InputInt, float→InputDouble, bool→Checkbox, date→InputText
    con hint YYYY-MM-DD, url→InputText + boton Open en navegador,
    enum→Combo con values). Required marcado con '*'.
  * Extras: lista key-value con boton trash por fila + 'Add' al final.
  * Tags: chips clickables (click = quitar) + input con autocomplete
    (lista compacta de tags distintas en BD).
  * Footer: Save/Discard/Open notes + label '(modified)' si dirty.
  * Neighbors read-only (igual que antes).
- Si el draft no esta sincronizado con la seleccion actual y NO hay
  cambios pendientes, el inspector muestra 'Cargando...' (main.cpp
  carga). Si hay dirty, banner 'Save/Discard primero' bloqueando.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 00:13:01 +02:00
egutierrez d6f7318c24 feat(projects): integrate project switcher in app shell + Inspector menu
main.cpp:
- Forward decl + switch_to_project: cierra layout_store, libera grafo,
  aplica nuevos paths, vuelve a cargar.
- apply_project_paths: deriva operations.db/types.yaml/graph_explorer.db
  del slug y los expone a g_app.active_project.
- main: arg --project <slug>; modo legacy si --input/positional dado;
  modo proyecto si no — migra layout legacy, decide target via
  arg/last_active/'default', crea si no existe, abre BDs y carga.
- render(): handler want_switch_project + monta views_new_project_modal.

views.h: AppState gana active_project, want_switch_project,
switch_project_target, show_new_project_modal, new_project_buf,
new_project_error, project_list_cache, project_recent_cache.

views.cpp:
- Toolbar: boton 'Project: <slug>' con popup (New/Recent/Open/Reveal).
  Refresca caches al abrir el menu.
- views_new_project_modal: input slug + validacion + creacion + switch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 23:46:47 +02:00
egutierrez adde3026ea fix: docking gaps + hover radius + node spread + markdown notes
Bug fixes
- ImGui ID conflict en menu Change type: dedup tipos del grafo +
  defaults; PushID/PopID por entrada.
- Dockspace ya no tapa la toolbar: se posiciona 44 px por debajo, asi
  las ventanas dockeadas al borde superior quedan bajo la barra de
  filtros, no detras.
- Hover radius proporcional al tamaño visual del nodo: query espacial
  amplio (24/zoom) + filtro fino por (radio_visual + 2 px) / zoom. El
  tooltip solo se dispara si el raton esta efectivamente sobre el nodo.

Layout
- Default layout = grid (en vez de force) para que los grafos cargados
  se distribuyan ordenadamente al abrir.
- Boton "Reset layout" en la toolbar: limpia NF_PINNED en todos los
  nodos, resetea velocidades y reaplica el layout activo.
- Nodos recien creados (add_node, duplicate) caen en un anillo poisson
  alrededor del centro de la vista, no en el origen. Posicion
  determinista por user_data para que el mismo nodo no salte entre
  reloads.

Notes (markdown)
- Panel "Note" (dockeable) abierto con doble click sobre un nodo.
- entity_get_notes / entity_set_notes en entity_ops sobre la columna
  `notes` de operations.db (ya existente en el schema).
- Ctrl+S guarda. Cabecera muestra entity, type, id.
2026-04-30 23:11:48 +02:00
egutierrez a36530bb6f feat: docking host + add-node toolbar + node context menu
- Dockspace host (PassthruCentralNode) bajo la toolbar para que las
  ventanas Viewport/Legend/Inspector/Stats puedan dockearse dentro de la
  app principal.
- Toolbar: input "Add node" con auto-deteccion de tipo (text/email/
  ip_address/url/domain/phone). Insert en operations.db + reload.
- Context menu (right-click sobre nodo): Change type, Duplicate, Delete,
  submenu "Run enricher" (placeholder hasta issues 0001-0003).
- Inspector: vecinos ahora muestran etiqueta de relacion ("-> employs",
  "<- owns") usando rel_types[].name como label de arista.
- Default relation label k_default_relation_name="RELATED_TO" para
  relaciones creadas sin nombre semantico explicito.
- Indice EntityIndex (FNV1a hash -> sql id) reconstruido tras cada load
  para resolver mutaciones desde el grafo en memoria.

Issues planteadas para iteraciones siguientes:
- 0001: chat con Claude sobre el grafo (HTTP + tool-use)
- 0002: enricher GLiNER+GLiREL desde nodo texto
- 0003: enricher web (fetch URL/dominio + extract text)
- 0004: vista tabla por tipo de entidad
2026-04-30 22:55:30 +02:00
egutierrez b767b5b85e feat: graph_explorer app — agnostic operations.db viewer (issue 0049k)
App C++ ImGui que abre cualquier operations.db del registry y lo visualiza
como grafo con shapes/iconos/layouts/filtros/labels.

Composicion del registry:
- viz/graph_renderer + graph_force_layout(_gpu) + graph_layouts +
  graph_viewport + graph_labels + graph_icons + graph_sources
- core: toolbar, modal_dialog, select, text_input, tree_view, page_header,
  fullscreen_window, button, badge, empty_state

Capas:
- data.{h,cpp}    — dispatcher GraphLoadFn (operations hoy; json/graphml manana).
- types_registry.{h,cpp} — parser YAML minimal + tabler_codepoint_by_name +
  apply_types_yaml + IconAtlas builder.
- views.{h,cpp}   — Toolbar, Legend, Inspector, Stats, modal Filters/Open.
- layout_store.{h,cpp} — graph_explorer.db SQLite con tabla layouts(graph_hash,
  node_id, x, y, pinned, updated_at). UPSERT por nodo.
- main.cpp        — CLI (--input/--types/--layout) + fn::run_app + bucle
  force layout (CPU/GPU toggle) + render con 3 columnas (Legend / Viewport /
  Inspector+Stats).

examples/types.yaml: 10 entidades OSINT (Person/Email/Domain/Phone/Org/IBAN/
Account/Document/Address/Url) + 5 relaciones (owns/knows/located_in/
transfers_to/member_of) con shapes Tabler reales.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 00:13:59 +02:00