# L02.2 — Configuración avanzada de columnas (V08) Fecha: 22 de octubre de 2026. Segunda entrega de [L01](auditoria-listados-l01.md), sobre la composición de [L02.1](selectores-accesibilidad-l02-1.md). ## 1. Resumen de implementación **V08 resuelto en los escenarios verificados**, con ocho pruebas MCP específicas y cuatro regresiones conjuntas correctas. EntityTable incorpora búsqueda local de columnas, agrupación semántica opcional, contador de visibilidad efectiva y restauración inmediata. Activos aporta cinco grupos; Recursos, Personas y los selectores sencillos conservan una presentación sin categorías artificiales. No cambia la ordenación real de columnas ni se añade un borrador Aplicar/Cancelar. La corrección se limita a V08. V05, V07 y V12 permanecen pendientes; la normalización del selector de columnas no cambia las búsquedas de entidades. No se modifican vendor, rutas, autenticación, contratos HTTP, persistencia del negocio, tokens ni dependencias. ## 2. Arquitectura y API [EntityColumn](../packages/rdx-ui/src/components/entity-list.tsx) conserva `id`, `label`, `render`, `value`, `defaultSort`, `hideable` y `defaultVisible`. Añade opcionalmente `groupId?: string` y `keywords?: readonly string[]`. `EntityTable.columnGroups?: readonly EntityColumnGroup[]` recibe grupos `{ id, label, description? }`; su orden en el array es el orden de presentación. El consumidor aporta etiquetas y descripciones traducidas. No se implementa `align` en esta entrega. ```tsx ``` [EntityTableConfiguration](../packages/rdx-ui/src/components/entity-table-configuration.tsx) transmite metadatos y grupos al [diálogo](../packages/rdx-ui/src/components/entity-table-columns-dialog.tsx). Los nuevos parámetros del diálogo/configurador son opcionales. `columnMetadata` es una colección de `{ id, groupId?, keywords? }`, sin DTO ni conceptos del catálogo. El diálogo sigue admitiendo una tabla TanStack y, sin callback específico, restaura mediante `table.resetColumnVisibility()`. La integración de EntityTable aporta `onResetColumns`, que vacía sus preferencias locales y permite que useEntityTable resuelva los defaults **actuales**, también cuando las columnas llegan después. No se duplica el estado de TanStack ni se usa un efecto para calcular resultados, grupos o contadores. ## 3. Búsqueda [normalizeColumnSearch/groupColumnOptions](../packages/rdx-ui/src/lib/entity-column-configuration.ts) filtran por etiqueta o keywords, mediante coincidencia parcial inmediata. Recortan espacios externos, ignoran mayúsculas y tildes y conservan la diferencia entre ñ/n, también con Unicode descompuesto. La consulta solo vive en el diálogo y se limpia al cerrar desde sus acciones. El botón Limpiar devuelve foco al buscador. Sin coincidencias, el estado explica cómo recuperarse; búsqueda, limpieza, restauración y cierre continúan disponibles. Filtrar no cambia visibilidad, IDs, sort, filtros del inventario ni preferencias. El contador de coincidencias describe opciones encontradas; el resumen superior sigue describiendo columnas visibles del conjunto completo. Esta utilidad podría reutilizarse, tras definir el contrato de entidades en L03; **V07 no queda resuelto**. ## 4. Agrupación Los grupos de Activos son: | ID | Castellano | Inglés | Contenido | | --- | --- | --- | --- | | `general` | General | General | Tipo, nombre, plantilla, puesto, notas y acciones | | `state-location` | Estado y ubicación | Status and location | Estado mostrado/técnico, sede y asignación | | `audit` | Auditoría | Audit | Fecha de alta y fechas/autores de revisiones | | `infrastructure` | Infraestructura | Infrastructure | Funciones, interfaces, direcciones IP/MAC y notas de interfaz | | `characteristics` | Características del catálogo | Catalog characteristics | Campos activos de catálogo e instantáneas conservadas | La primera declaración de un ID repetido de grupo o metadato prevalece. Las opciones se deduplican por ID; los grupos sin coincidencias desaparecen. Columnas sin grupo o con grupo desconocido siguen disponibles al final, bajo «Otras columnas» cuando se han proporcionado categorías. Sin categorías, hay una lista sencilla sin leyenda artificial. Los metadatos cambian la organización del selector, nunca la posición de las columnas en la tabla. ## 5. Columnas dinámicas [assetCharacteristicColumns](../modules/assets/src/web/assets/shared/helpers/asset-characteristic-columns.ts) conserva los IDs `characteristic-`, la unión de campos activos con instantáneas y los ordinales para etiquetas iguales de campos diferentes. Cada campo pertenece al grupo principal estable `characteristics`. No se elige arbitrariamente una plantilla o sección como propietario visual. Keywords reúnen los nombres de todas sus plantillas/secciones, etiquetas históricas, descripción y unidad del campo activo. Un campo compartido mantiene un único checkbox. Los contextos se ordenan y las instantáneas se recorren por ID de activo para mantener una etiqueta histórica determinista cuando no hay definición activa. El render de celdas continúa utilizando el tipo y la unidad de la instantánea; estos metadatos no cambian valores, sincronización de plantillas ni conservación de campos retirados. ## 6. Visibilidad y obligatoriedad `hideable:false` y las protecciones de useEntityTable siguen prevaleciendo sobre preferencias incompatibles. En Activos permanecen obligatorias Tipo de activo, Nombre, Puesto de trabajo y Acciones. Se muestran marcadas, deshabilitadas y con «Obligatoria»; una descripción accesible explica que siempre permanecen visibles. El resumen incluye todas las columnas expuestas, también las obligatorias y Acciones. La columna interna `selection` de pickers se excluye del configurador y su contador: sigue obligatoria en la tabla. Tipo de activo se incluye porque representa información de datos, aunque su cabecera visual sea un icono. Los totales del catálogo comprobado son **75 columnas: 20 base/adicionales/acciones y 55 características**. Ocho están visibles inicialmente. El contador no representa coincidencias de búsqueda ni filas del inventario. ## 7. Restauración Restaurar columnas aplica inmediatamente el conjunto inicial definido por `defaultVisible`, la visibilidad predeterminada y las protecciones de cada consumidor. No equivale a mostrar todas las columnas. Se conservan búsqueda y orden del listado y la consulta local del diálogo. El callback de EntityTable restablece las preferencias a `{}`: cada columna disponible recupera su default actual; las retiradas no reaparecen y las nuevas respetan su definición. Cerrar termina la interacción sin revertir cambios. No se añaden Aplicar/Cancelar ni escrituras de negocio. ## 8. Persistencia Se mantiene `asset-inventory-columns`, su alcance existente entre compañías del navegador y el formato plano `{ "column-id": boolean }`. La restauración persiste `{}` mediante el escritor existente, sin borrar otras claves ni migrar globalmente preferencias. Sin preferencia, con valores parciales o con IDs desconocidos, el hook mantiene los defaults y solo presenta columnas reales. Los valores protegidos incompatibles no afectan a la identidad. La prueba de restauración verifica recarga y conservación de una clave ajena. Datos inválidos y storage inaccesible siguen permitiendo operar localmente. Los listados sin `columnVisibilityStorageKey`, incluidos Recursos y Personas, conservan su configuración local durante su montaje; esta entrega no les añade persistencia. ## 9. Accesibilidad Se compone FormDialog con Dialog/Base UI, Checkbox, InputGroup y FieldSet/FieldLegend ya instalados. No hay listeners globales ni focus traps propios. El buscador tiene etiqueta accesible, foco inicial y limpieza con retorno de foco. Las categorías tienen leyendas semánticas y las descripciones opcionales están asociadas a su grupo. Las etiquetas de checkbox excluyen el indicador visual redundante «Obligatoria» de su nombre y lo complementan con una descripción accesible. Tab, Shift+Tab, Enter, Espacio, Escape, cierre y retorno al activador se verifican mediante MCP; el último control conserva foco visible al desplazar las opciones. El diálogo de columnas también se comprueba dentro de un picker sin cerrar el formulario padre. No se certifica WCAG ni se atribuye a estas pruebas una validación con lector de pantalla. ## 10. Responsive y reproducción inicial Las medidas previas se tomaron en esta instalación, **antes de cambiar el código funcional**, y se conservan en `before/measurements.json`: | Resolución | Antes: modal / área con scroll | Después: modal / área de opciones | | --- | --- | --- | | 320×640 | 288×608; 468 px de área, lista plana | 288×608; 336 px útiles para opciones, búsqueda y restauración fuera del scroll | | 1440×900 | 576×868; 728 px de área, lista plana | 576×765; altura exterior estable según el patrón picker | Antes había 75 checkbox, cero buscadores de texto, cero grupos y foco inicial en Plantilla. Se comprobó ocultar Sede y recargar: `{ "site": false }` se conserva. En móvil había entonces siete visibles por esa elección previa, no por pérdida de un default. Mediciones de la ejecución final, en píxeles CSS: | Resolución | Alto exterior | Alto útil de opciones | Filas completas iniciales | | --- | --- | --- | --- | | 320×640 | 608.0 | 336 | 6 | | 390×844 | 812.0 | 540 | 10 | | 768×1024 | 870.4 | 626 | 12 | | 1024×768 | 652.8 | 409 | 8 | | 1366×768 | 652.8 | 409 | 8 | | 1440×900 | 765.0 | 521 | 10 | | 1920×1080 | 900.0 | 656 | 13 | Después se verifican 320×640, 390×844, 768×1024, 1024×768, 1366×768, 1440×900 y 1920×1080. FormBody conserva la cadena flex/min-height y desplazamiento interno. Buscar o vaciar resultados no cambia la altura; la lista no colapsa, ni aparece overflow horizontal global. No se cambian tamaños globales de Dialog. ## 11. Pruebas automatizadas Se amplían las pruebas existentes de rdx-ui y columnas de activos. Cubren normalización, ñ/n, keywords, grupos opcionales/múltiples/vacíos/desconocidos/repetidos, unicidad y contextos compartidos, defaults, campos nuevos/retirados, storage inválido/bloqueado y aislamiento por clave. [scripts/columns-ui.test.py](../scripts/columns-ui.test.py) utiliza unittest y el cliente MCP stdio existente. [La fixture React](../packages/rdx-ui/src/column-browser-fixture.tsx) se importa exclusivamente desde Vite durante la prueba, sin rutas ni importación de producción: tres columnas, keywords, campo nuevo y campo retirado. La suite de navegador comprueba catálogo real, siete resoluciones, visibilidad inmediata, filtrado sin mutaciones, reapertura sin filtro, persistencia/recarga/restauración, inglés/español, teclado, scroll y consumidores sencillos/anidados. La fixture de storage bloqueado afecta solo a la clave de prueba. ## 12. Entorno, evidencias y reproducción Playwright Instancia única de `scripts/visual-audit.py`, MariaDB desechable `metis_desk_test`, usuario ordinario y login real. [visual-audit-volume.py](../scripts/visual-audit-volume.py) amplía exclusivamente la compañía sintética del informe, mediante API HTTP real con sesión y CSRF válidos. Resultado: **15 compañías; 50 personas, 51 activos y 66 recursos en la principal**. Repetir conserva esos recuentos. Incluye nombres extensos/con tildes, activos de distintas plantillas/estados y recursos activos/inactivos; no simula permisos ni identidades de dominio. ```bash python3 scripts/visual-audit.py # En otra terminal, utilizar la ruta privada que imprime el preparador: PYTHONDONTWRITEBYTECODE=1 METIS_PLAYWRIGHT_STORAGE_STATE=.artifacts/playwright/authenticated-ID/session.json \ python3 scripts/visual-audit-volume.py PYTHONDONTWRITEBYTECODE=1 METIS_PLAYWRIGHT_STORAGE_STATE=.artifacts/playwright/authenticated-ID/session.json \ python3 scripts/columns-ui.test.py ``` Mantener abierto el preparador; cerrarlo al terminar para retirar solo sus servicios y sesión. El guion de tests no inicia entornos ni crea entidades y bloquea escrituras HTTP de negocio. Las preferencias solo afectan al navegador aislado. No copiar el contenido de `session.json` a Git, logs ni mensajes. Evidencias ignoradas en [.artifacts/playwright/l02-2/](../.artifacts/playwright/l02-2/): capturas antes/después, snapshots, mediciones por ejecución, consola, comprobaciones técnicas e índice con dimensiones y SHA-256. El índice conserva 134 capturas, incluidas ejecuciones intermedias; `verification.json` identifica la ejecución completa final y las regresiones correctas. Se abren e inspeccionan capturas representativas de móvil/escritorio, inglés, búsqueda, campo compartido, foco y paneles antes de cerrar la entrega. MCP se ejecuta realmente por stdio dentro de la imagen Docker fijada del proyecto; no se instala Chromium en WSL. Capturas representativas inspeccionadas: - Antes: [escritorio](../.artifacts/playwright/l02-2/before/columns-1440.png) y [móvil](../.artifacts/playwright/l02-2/before/columns-320.png). - Después: [escritorio](../.artifacts/playwright/l02-2/after/assets-1440.png) y [320×640](../.artifacts/playwright/l02-2/after/assets-320.png). - [Inglés en móvil](../.artifacts/playwright/l02-2/after/assets-english-320.png), [campo compartido único](../.artifacts/playwright/l02-2/after/shared-field-one-option.png) y [foco al final del scroll](../.artifacts/playwright/l02-2/after/scroll-focus-last-320.png). - Regresión: [selección/contraste de activos](../.artifacts/playwright/l02-2/regression-verified/contrast-selected-1920.png) y [panel de recursos en W=984](../.artifacts/playwright/l02-2/regression-verified/resource-capacity-984.png). ## 13. Resultados técnicos y regresiones Resultados realmente ejecutados: | Comprobación | Resultado | | --- | --- | | Suite MCP L02.2 | **8/8 correctas**, 160,367 s en la ejecución completa final. | | Siete resoluciones | Correctas; entre 336 y 656 px útiles en el área de opciones. | | Campo compartido real | Marca, usado por doce plantillas: una opción por ID; localizable por varios contextos. | | Teclado y scroll | Último checkbox con foco visible, scrollTop 3064; recorrido de 85 pulsaciones Tab sin salir del diálogo tras asentarse Base UI. | | Persistencia y restauración | Aplicación inmediata, recarga, defaults, claves ajenas intactas, campos nuevos/retirados y storage bloqueado: correctos en navegador. | | `pnpm --filter @repo/rdx-ui test` | Correcto: 11 archivos. | | `pnpm --filter @metis/assets test` | Correcto: 27 archivos; repetido tras ampliar el caso de instantáneas históricas. | | `pnpm --filter @metis/resources test` | Correcto: 11 archivos. | | Typecheck dirigido rdx-ui, activos, recursos, compañías y web | Correcto en los cinco paquetes. | | Biome dirigido y ESLint de espaciado | Correctos sobre los 15 archivos propios TS/TSX/JSON modificados/nuevos (ESLint sobre los TS/TSX). | | `pnpm typecheck` | Falla en vendor `calendar.tsx:165`, incompatibilidad previa de `locale`/exactOptionalPropertyTypes. | | `pnpm lint` | Falla: 15 errores, un aviso y cuatro informaciones, ajenos al cambio y coincidentes con L02.1. | | `pnpm check` | Falla: 168 errores, un aviso y cuatro informaciones previos; comprobación dirigida correcta. | | `pnpm test`, MariaDB temporal independiente | Falla en dos expectativas antiguas de compañías; 7 tareas correctas de 8. No se presenta como suite global superada. | | `pnpm build` | Correcto, dos tareas; conserva aviso de chunk web superior a 500 kB. | | Preparación de volumen repetida | Correcta: 15 compañías, 50 personas, 51 activos y 66 recursos; sin duplicados. | | Regresión MCP V01.3/V01.4, V02 y asistentes | **4/4 correctas**, 158,944 s; contraste, ambos presupuestos/paneles y geometría de asistentes. | | Consola suite MCP final | Cero errores y advertencias; ninguna escritura HTTP de negocio intentada. | Las primeras ejecuciones del guion detectaron errores de instrumentación: nombre «Campo» en la fixture de selector, comillas en un localizador y comprobación de foco durante una transferencia transitoria de Base UI. Se corrigieron los tests y se repitió la suite completa. No son defectos funcionales de Metis ni se cuentan como pruebas correctas esos intentos. Las regresiones antiguas de contraste/capacidad buscaban AUDIT-PORT-01 en la primera página; al preparar 51 activos dejó de estar ahí. Se utiliza la búsqueda real antes de esos dos escenarios. La ejecución conjunta final obtiene **4/4 correctas**, 158,944 s (`browser-regression-verified.log`), con lecturas mediante Locator que esperan la fila/tabla real antes de medir. Las ejecuciones intermedias con errores quedan conservadas y no se presentan como suites correctas. La espera de elementos no modifica la aplicación ni resuelve V12. Las pruebas de asistente y recursos conservan sus operaciones de lectura/cancelación; no guardan cambios. Durante la revisión se corrigió también la sustitución de parámetros del nuevo preparador de volumen: los placeholders ya no pueden alterar el prefijo de los nombres de compañía. Se normalizaron mediante HTTP las catorce compañías **creadas por esta fixture en la instalación desechable**, sin borrar registros, y se repitió la preparación para verificar los recuentos e idempotencia. Los fallos globales se comparan con L02.1: `calendar.tsx:165` de vendor en typecheck, 15 errores de lint, 168 de check y las dos expectativas antiguas de compañías («Personas (1)» y aviso de persona sin acceso). No se corrige código ajeno para presentar una suite global verde. El test global usa otra MariaDB nueva y temporal, separada del entorno visual, con cleanup propio. ## 14. Limitaciones y skills No se introducen reordenación, densidad, alineación semántica, cambios de búsqueda de entidades, restauración completa de contexto ni skeletons. La persistencia sigue compartida entre compañías por el contrato previo; no se transforma en preferencia de servidor ni por usuario autenticado. Los datos voluminosos no constituyen una prueba de carga o rendimiento de producción. Skill principal: `ux-tables` para personalización y lectura de tablas. Complementarias leídas: `ux-search` para búsqueda secundaria y recuperación, `shadcn` para composición Base UI y componentes instalados, `ux-microcopy` para contadores/obligatoriedad/restauración, `vercel-composition-patterns` para metadatos genéricos sin flags de entidad y `vercel-react-best-practices` para estado derivado sin efectos. Se consultan sus referencias aplicables de estado derivado/eventos, composición y formularios; prevalecen las APIs locales y los patrones Metis. No se necesita `frontend-design` ni `ui-ux-pro-max` para cambiar jerarquía o resolver otra duda. Los auditores especializados `*-audit` y `docs/orchestration-policy.md` referenciados por las skills no están instalados; no se instalan herramientas ni se presentan esos auditores como ejecutados. No se ejecuta el CLI de shadcn ni se descarga otra versión; se utilizan las primitivas locales sin regenerar vendor. ## 15. Preparación de L02.3 El selector dispone de metadatos opcionales y restauración por consumidor; los listados sin categorías no necesitan migración. L02.3 requiere un encargo propio. **V05, V07 y V12 permanecen pendientes**; no se adelanta reordenación, densidad ni otra entrega del programa. ## Archivos principales | Área | Archivos modificados o creados | | --- | --- | | rdx-ui | `components/entity-list.tsx`, `entity-table-configuration.tsx`, `entity-table-columns-dialog.tsx`; nuevo `lib/entity-column-configuration.ts`; `lib/entity-list.ts`; traducciones `locales/es.json` y `en.json`. | | Activos | `assets/shared/ui/asset-list.tsx`; helpers `asset-additional-columns.tsx`, `asset-characteristic-columns.ts`; traducciones comunes es/en. | | Pruebas | `entity-column-visibility.test.tsx`, `asset-columns.test.tsx`; nueva fixture `column-browser-fixture.tsx`; nuevos `scripts/columns-ui.test.py`, `scripts/visual-audit-volume.py`; adaptación acotada de `scripts/visual-audit-ui.test.py` a volumen y lecturas de DOM mediante Locator. | | Documentación | Este informe, DESIGN.md, auditorías L01/visual, patrón de listados, componentes y Playwright MCP. | No se modifican `useEntityTable`, el contrato de almacenamiento ni EntityTableContent: sus protecciones, resolución de defaults, persistencia y render existentes permiten implementar la mejora desde la composición. No hay cambios en `packages/shadcn-ui`, package.json, lockfiles o AGENTS.md. Cierre: se detuvo el preparador propio; retiró sus servicios temporales y eliminó `session.json`. Se verificó la ausencia del archivo de sesión. Las capturas e informes sin credenciales permanecen en `.artifacts/playwright/l02-2/`, excluidos de Git.