- Implemented asset and resource list query state helpers to manage loading, error, and availability states. - Created EntityListStatus component for displaying status messages and retry options based on entity list state. - Added EntityTableSkeleton component for rendering loading placeholders in tables. - Defined EntityListState type and entityListState function for consistent state management across components. - Developed comprehensive UI tests for list states using Playwright, covering various scenarios including loading, error handling, and responsiveness.
19 KiB
L02.3 — Estabilidad de listados y estados de consulta (V12)
Fecha: 22 de octubre de 2026. Implementación en Activos y adopción equivalente en Recursos. Este documento complementa L01, L02.1, L02.2 y el patrón de listados.
1. Resumen de la implementación
El inventario mantiene su instancia de AssetList/EntityTable desde la primera petición.
Búsqueda, cabeceras conocidas, configuración y espacio de paginación permanecen reconocibles.
La consulta principal, el catálogo y los datos de red tienen estados y reintentos independientes.
Las actualizaciones conservan la caché de TanStack Query y comunican cuándo fallan.
No se modifican backend, rutas, autenticación, permisos, dependencias ni vendor.
Archivos afectados, agrupados por responsabilidad:
| Área | Archivos principales |
|---|---|
| rdx-ui, componentes | entity-list.tsx, entity-list-controls.tsx, entity-table-configuration.tsx, entity-table-content.tsx; nuevos entity-list-status.tsx y entity-table-skeleton.tsx, en packages/rdx-ui/src/components/. |
| rdx-ui, contrato/pruebas/textos | Nuevo src/lib/entity-list-state.ts, src/entity-list.test.tsx y src/locales/{es,en}.json. |
| Activos | modules/assets/src/web/assets/list/controllers/use-inventory-page-controller.ts, nuevo list/helpers/asset-list-query-state.ts, list/ui/inventory-page.tsx, list/ui/asset-workstation-action.tsx, shared/ui/asset-list.tsx y modules/assets/src/common/locales/{es,en}.json. |
| Recursos | modules/resources/src/web/resources/list/controllers/use-resources-page-controller.ts, nuevo list/helpers/resource-list-query-state.ts, list/ui/resources-page.tsx y modules/resources/src/common/locales/{es,en}.json. |
| Pruebas MCP | Nuevo scripts/list-states-ui.test.py; actualización de scripts/picker-ui.test.py y scripts/visual-audit-ui.test.py para contratos vigentes y datos abundantes. |
| Documentación | Este informe, DESIGN.md, docs/patron-de-listados-de-entidades.md, docs/componentes-de-interfaz.md y los seguimientos de docs/auditoria-listados-l01.md / docs/auditoria-ui-ux-visual.md. |
2. Diagnóstico de consultas
| Consulta / clave real | Datos y necesidad | Disponibilidad, actualización y error |
|---|---|---|
inventoryKeys.company(companyId) → ["inventory", companyId] |
GET /api/assets/:companyId: activos, instantáneas, puestos y relaciones. Imprescindible para filas y selección. |
Inicialmente data indefinido; refetch conserva datos. Error inicial impide operar sobre filas; error posterior permite consultar la copia anterior. |
catalogKeys.scope(companyId) → ["asset-catalog", companyId] |
GET /api/asset-catalog/:companyId: plantillas y campos vigentes. Necesario para configurar el catálogo completo y editar/crear. |
Independiente del inventario. Las instantáneas permiten consultar filas mientras llega; sin catálogo se explica la limitación y se deshabilitan configuración y alta. Si falla refetch se conservan las definiciones anteriores y se indica su antigüedad. |
infrastructureKeys.company(companyId) → ["asset-infrastructure", companyId] |
GET /api/asset-infrastructure/:companyId: roles, interfaces, IP y MAC. Accesorio para el inventario. |
Pendiente/error no bloquea filas; aviso localizado y reintento propio. Refetch fallido mantiene los últimos datos de red. |
inventoryKeys.company(companyId) de Resources → ["resource-inventory", companyId] |
GET /api/resources/:companyId: recursos y referencias. |
Misma separación entre carga, disponibilidad, actualización y error; conserva contratos de selección URL y diálogo explícito. |
catalogKeys.scope(companyId) de Resources → ["resource-catalog", companyId] |
GET /api/resource-catalog/:companyId: plantillas de recursos. |
No bloquea lectura de instantáneas. Configuración y alta requieren referencias disponibles; el error tiene recuperación propia. |
Estas consultas no dependen unas de otras y se habilitan al montar la página con companyId.
Sedes/personas llegan mediante lookups desde la consulta de compañía del padre; no se añaden
consultas ni se cambia ese requisito de montaje. Los campos dinámicos se derivan del catálogo
y de las instantáneas reales, sin consulta adicional.
useListQuery mantiene su intervalo de 60 s en primer plano, refetch al recuperar foco/conexión
y deduplicación por claves. El cliente real configura retry: false: los reintentos añadidos
son explícitos y dirigidos a la consulta correspondiente. No cambia la política global.
Antes, AssetInventoryPage condicionaba AssetList a query.data y agrupaba el pending del
inventario, catálogo e infraestructura. El mensaje genérico «Cargando catálogo…» no identificaba
la consulta retenida. En Recursos, el pending/error combinado podía ocultar datos utilizables.
3. Modelo de estados adoptado
EntityListState es una unión discriminada presentacional: loading, ready, refreshing,
unavailable y stale. entityListState combina disponibilidad, actividad y fallo; no conoce
TanStack Query. Los adaptadores de módulo leen data, isFetching, isError y error.
| Datos disponibles | Consulta | Presentación |
|---|---|---|
| No | Pendiente | loading, mensaje específico y skeleton |
| Sí, incluidos cero registros | Correcta | ready; vacío solo si la respuesta válida está vacía |
| Sí | En curso | refreshing, filas anteriores y aviso discreto |
| No | Fallida | unavailable, error y reintento recuperable |
| Sí | Fallida | stale, copia anterior, aviso explícito y reintento |
Los errores conservan retrying para deshabilitar el reintento duplicado e indicar actividad.
No se guardan DTO, marcas temporales ni estados de consulta duplicados en React. Se opta por
explicar «últimos datos disponibles» sin introducir una hora ni duplicar dataUpdatedAt.
4. Cambios de composición
Activos monta siempre AssetList, sin keys dependientes de la consulta. Su controlador deriva
el conjunto legible de la caché; Resources aplica la misma regla. EntityTable conserva el hook
de tabla y acepta state, onRetry y columnsDisabled opcionales. Los consumidores legacy,
incluidos PickerList y selectores remotos, mantienen sus contratos existentes.
Las páginas deciden permisos, dependencias y operaciones; rdx-ui solo recibe textos y callbacks.
useContextualDetail, presupuestos de contenedor, navegación y EntityNotFound se conservan.
Un error transitorio no confirma que haya desaparecido una selección. Una respuesta correcta
que la retire sigue utilizando el contrato de desaparición existente.
5. Carga inicial
La búsqueda y ordenación de columnas conocidas funcionan durante la espera; sus valores se
mantienen cuando llegan los datos. Diez placeholders como máximo reservan una página plausible,
con las clases de las columnas reales. Son aria-hidden, sin nombres de entidades, selección,
links ni botones de acción. No se anuncian diez registros ni se muestra un total cero inventado.
Paginación/tamaño de página permanecen visibles pero deshabilitados hasta disponer de datos. El catálogo pendiente tiene un marcador decorativo y un mensaje contextual cuando hay filas. Las pestañas y columnas solo utilizan definiciones/instantáneas realmente conocidas. La alta espera a inventario y catálogo válidos; los datos de red pendientes no bloquean el inventario.
6. Refetch y datos anteriores
«Actualizando activos…»/«Updating assets…» sustituye el resumen en su banda habitual, sin spinner modal, skeleton completo ni toast por cada intervalo. Las filas y herramientas no se desmontan. Se conservan búsqueda, plantilla, orden, página válida, preferencias, selección, scroll y foco durante la petición. El refetch normal del catálogo/red no añade bandas que muevan el listado.
7. Error y recuperación
El error inicial y el posterior aparecen en la banda de estado, fuera del ancho desplazable de las columnas. Así, el mensaje y Reintentar no requieren scroll horizontal en móvil. La tabla conserva cabeceras y espacio corporal, sin simular registros.
Un 503 posterior comunica que se muestran datos anteriores; permite búsqueda y consulta. Alta/edición y operaciones sensibles se restringen cuando su consulta necesaria no es válida; el borrador ya abierto se conserva ante un error transitorio. Los reintentos de catálogo/red no invalidan indiscriminadamente otras consultas.
Una denegación InventoryApiError con FORBIDDEN oculta los datos en caché del inventario y
muestra falta de permiso sin prometer recuperación mediante Reintentar. Se conserva la
navegación a otros destinos. No se altera la autorización del servidor.
8. Estados vacíos
La respuesta válida sin activos conserva «Esta compañía todavía no tiene activos.» y su alta
de cabecera, cuando las referencias están listas. Cero coincidencias conserva la búsqueda y
«Limpiar filtros». Un error no utiliza ninguno de esos mensajes. — sigue reservado para
valores ausentes/controles sin valor conocido; no sustituye los estados de consulta.
9. Componentes compartidos
lib/entity-list-state.ts: unión y derivación pura, incluida compatibilidad legacy.EntityListStatus: estado, mensaje y recuperación con Alert/Button existentes; destino de foco opcionalretryFocus, sin DTO, query keys ni permisos.EntityTableSkeleton: placeholders decorativos con geometría de columnas conocidas.EntityTable: instancia estable y estado explícito;EntityTableConfigurationpermite deshabilitar la configuración dependiente;EntityListPaginationdistingue total desconocido.
Adopción nueva limitada a los inventarios de Activos/Recursos con layout content. No se
convierte ningún selector remoto en tabla local ni se generalizan estados de consultas ajenas.
10. Accesibilidad
La carga inicial tiene un único role=status; los skeletons no participan en el foco.
La tabla señala aria-busy durante carga/refetch normal. El refetch periódico no genera
anuncios live repetidos. Los errores utilizan el Alert existente y texto explícito, además
del color. Los controles pendientes tienen explicación contextual en es/en.
Reintentar funciona con Enter y enfoca la búsqueda persistente antes de iniciar la recuperación;
así su desaparición no deja el foco en body. El destino se aporta desde el consumidor.
Las animaciones nuevas respetan prefers-reduced-motion. Estas comprobaciones no acreditan
cumplimiento WCAG completo ni sustituyen pruebas con lector de pantalla.
11. Resultados responsive
La matriz de carga inicial comprende 1920×1080, 1440×900, 1366×768, 1024×768, 768×1024, 390×844 y 320×640. Se comprueba estructura, búsqueda habilitada, paginación pendiente, placeholders sin controles y ausencia de overflow horizontal global. La tabla conserva su scroll horizontal local. Paneles siguen los mínimos V01.3 (1144 px) y V01.4 (984 px).
La línea base retenida antes del cambio tenía cero tablas y cero controles de búsqueda y configuración, tanto a 1440×900 como a 320×640. La corrección conserva una tabla y sus herramientas. Las medidas completas, incluidas posiciones, se guardan junto a las capturas.
En la prueba de refetch a 1920×1080 se usan página 2, orden descendente, selección contextual, columnas opcionales y scroll real: horizontal 120 px y vertical 150 px. Durante la espera se conservan ambos scrolls, foco e identidad DOM; la cabecera de tabla permanece en y=230 px. El aviso de fallo posterior ocupa espacio adicional (26 px en ese escenario), sin desmontar filas ni reiniciar scroll/selección. No se promete layout idéntico ante mensajes de error.
12. Pruebas
| Comprobación ejecutada | Resultado real |
|---|---|
pnpm --filter @repo/rdx-ui typecheck, @metis/assets typecheck, @metis/resources typecheck |
Correctas las tres comprobaciones dirigidas. |
pnpm --filter @repo/rdx-ui test |
11/11 ficheros; el fichero de listado ejecutado directamente obtiene 27/27 casos. |
pnpm --filter @metis/assets test / @metis/resources test |
27/27 y 11/11 ficheros. |
pnpm exec biome check <archivos TS/TSX/JSON afectados> / pnpm exec eslint <TS/TSX afectados> |
Correctos, sin diagnósticos nuevos. |
pnpm typecheck |
Falla en packages/shadcn-ui/src/components/calendar.tsx:165, TS2375 de locale con exactOptionalPropertyTypes; mismo fallo vendor previo. La interrupción global no sustituye los typechecks dirigidos. |
pnpm lint |
Falla: 15 errores, un warning y cuatro infos preexistentes. |
pnpm check |
Falla: 168 errores, un warning y cuatro infos preexistentes. |
pnpm test |
Falla: 7/8 tareas correctas; dos expectativas SSR antiguas de Compañías (Personas (1) y el mensaje de acceso de persona). Ejecutada contra otra MariaDB temporal metis_desk_test, sin utilizar la BD del navegador. |
pnpm build |
Correcto, 2/2 tareas; persiste el aviso de chunk mayor de 500 kB. |
| MCP, L02.3 | 7/7 métodos principales + 1/1 adicional de plantilla/contraste/reintento móvil, ocho escenarios automatizados distintos correctos. |
| MCP, regresiones seleccionadas | 9/9: navegación móvil/teclado/rutas, capacidad del shell y Activos, contraste, asistentes, foco, capacidad de Recursos y edición anidada. |
| MCP, L02.2 | 8/8 métodos correctos. |
| MCP, L02.1 | 8/8 métodos correctos tras actualizar las expectativas del configurador L02.2. |
Se ejecutan 33 métodos MCP distintos; las repeticiones de diagnóstico no incrementan ese total. Las pruebas unitarias nuevas cubren ocho combinaciones de consulta, compatibilidad legacy, estructura inicial, ausencia de registros ficticios, datos anteriores y distinción entre error, vacío válido y filtros en es/en.
scripts/list-states-ui.test.py intercepta exclusivamente GET del entorno sintético: retención
y liberación inicial/refetch, 503 inicial/posterior, reintento real, catálogo/red independientes,
403, respuesta vacía válida, filtros, edición conservada, movimiento reducido y traducción.
No cambia opciones internas de QueryClient para simular los estados; la reconexión controlada
dispara el mecanismo normal del consumidor.
Ejecutar con la sesión privada activa del preparador, sin escribir su contenido ni guardarla en Git. El procedimiento de Playwright MCP explica cómo obtenerla:
METIS_PLAYWRIGHT_STORAGE_STATE=.artifacts/playwright/authenticated-ID/session.json \
python3 scripts/list-states-ui.test.py
La suite contiene ya los ocho métodos. La ejecución documentada separó el octavo, incorporado al final, de los siete primeros. Para las regresiones se seleccionaron los nueve métodos indicados, no los 28 del fichero entero. La consola final de estas regresiones y la de columnas no registra errores/warnings. En las simulaciones L02.3 aparecen los 503 deliberados del GET interceptado; no se clasifican como fallos espontáneos.
13. Evidencias
Directorio local ignorado: .artifacts/playwright/l02-3/. Contiene baseline before/, capturas,
snapshots, geometría, foco, consola y escrituras bloqueadas en after/, y logs de validación.
Los errores HTTP 503/403 se provocan deliberadamente y no representan incidencias espontáneas.
El índice distingue ejecuciones intermedias y finales. Las imágenes no se versionan ni se
incluyen sesiones privadas; su entrega fuera de esta instalación requiere compartir esos
PNG/mediciones de forma explícita.
Índice local: .artifacts/playwright/l02-3/indice-evidencias.md. Capturas representativas
abiertas: baseline retenido 1440, carga nueva 1440, error inicial 320, error posterior 1920,
catálogo fallido 1440 y reintento visible 320. El contraste real del texto obtiene 4,83:1 en
carga y 10,01:1 en error sobre sus fondos computados; son dos muestras concretas, no una
auditoría integral de contraste.
Se reutiliza scripts/visual-audit.py, login real y usuario ordinario: una única instalación
propia desechable, exclusivamente metis_desk_test, con 15 compañías, y una principal con
50 personas, 51 activos y 66 recursos mediante el preparador de volumen existente.
La sesión anterior encontrada devolvía 401 y no se reutilizó ni se eliminaron servicios ajenos.
Al terminar se cierra el preparador propio: salida correcta, servicios temporales retirados y
comprobación explícita de que su session.json ya no existe. Las capturas se conservan sin sesión.
14. Regresiones
Las suites de columnas L02.2, selectores L02.1 y los casos seleccionados de
visual-audit-ui.test.py comprueban navegación móvil/portátil, presupuestos de inventario,
detalle de Recursos, contraste de selección, asistentes y retorno de foco. Se preservan
visibilidad persistente, IDs de campos y acciones ancladas. Los tests anteriores deben esperar
datos disponibles explícitamente: la existencia de la tabla ya no implica que acabó el GET.
Las primeras ejecuciones detectaron supuestos obsoletos en pruebas: un único botón Editar,
etiquetas DOM sin la marca Obligatoria, selection configurable y todos los destinos visibles
sin scroll con quince compañías. Se actualizan las expectativas al contrato L02.2 y al volumen
real, usando nombre accesible para la identidad protegida y comprobando que el foco hace visible
Inicio en el menú desplazable. No se cambia el shell para hacer pasar esas expectativas.
Los instrumentos nuevos también se corrigieron antes de la ejecución final: esperar promesas de Locator, limitar Reintentar a su listado (hay otros observadores de la misma consulta) y esperar las compuertas en el contexto Node del MCP. Se conservan sus logs fallidos intermedios.
15. Limitaciones
No se acredita rendimiento de producción, otros motores, lectores de pantalla ni acceso a datos reales. La sesión y el navegador son efímeros. Las interceptaciones no prueban una caída real del backend. Compañía/lookups conserva su carga exterior; otros listados legacy no adoptan automáticamente el nuevo estado. Siguen vigentes los fallos globales preexistentes de calendario vendor, lint/check y dos expectativas SSR de Compañías; no se corrigen fuera de alcance.
Skills leídas: ux-tables principal; ux-empty-states, ux-error-recovery, ux-microcopy,
shadcn, vercel-composition-patterns y vercel-react-best-practices complementan estados,
mensajes, primitivas reales, composición y derivación sin efectos. Prevalecen DESIGN y contratos
de Metis. Los auditores *-audit y docs/orchestration-policy.md referidos por algunas skills
no están instalados/disponibles; no se instalan ni se afirma haberlos ejecutado.
16. Pendientes de L03 y L04
V05 (restauración entre rutas) y V07 (normalización general de búsqueda) permanecen pendientes. V08 sigue resuelto en los escenarios L02.2, sin reordenación ni densidades nuevas. L03 conserva su alcance futuro; L04 podrá valorar adopciones de estado en otros consumidores, sin refactor masivo de consultas ni paginación/virtualización nuevas en esta entrega.
V12 queda resuelto en los escenarios descritos y ejecutados. No se cambia el estado de otros hallazgos ni se declara que todos los listados del monorepo adopten este contrato.