diff --git a/DESIGN.md b/DESIGN.md index 1579f897..5cb4ca48 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -208,7 +208,7 @@ Fuente normativa: [patrón de diálogos](docs/patron-de-dialogos.md). Cada inter | `layout="picker"` | Selector de altura estable; búsqueda inicial en input `data-quick-search`; tabla `layout="fill"`, selección primera y compacta, confirmación/cancelación de borrador. | | `layout="content"` | Diálogos breves de contenido estable; es el default técnico, no la elección normativa para asistentes variables. | | FormBody | Scroll interno, `min-h-0`, margen de foco; `contentLayout="fill"` distribuye alto restante y permite que EmptyState crezca por toda la cadena flex. | -| FormHelp | Hermano de FormBody antes de acciones, junto al pie; scroll propio y límite actual del 40 %. Su densidad en ventana corta requiere diseño adicional, V04. ContextHelp dentro del cuerpo no garantiza ese anclaje. | +| FormHelp / ContextHelp | Dentro de FormDialog declaran ayuda bajo demanda desde ? en cabecera: panel o vista interna según capacidad. PickerHelp declara ayuda breve (Popover o vista). Véase el patrón de diálogos y la auditoría de ayuda. | | FormProgress | Paso actual, completados y anuncio con foco; falta ajustar etiquetas estrechas, V04. No simular navegación clicable si los pasos no la ofrecen. | Capacidad operativa de selectores: [L02.1](docs/selectores-accesibilidad-l02-1.md) corrige @@ -262,7 +262,7 @@ Vigente: distribución por contenedor para grupos de formulario, orden DOM conse - Escritorio amplio: mostrar datos comparables y paneles cuando quepan; evitar márgenes ornamentales y tarjetas sin función. - Portátil/tablet: aplicar la política del shell V01.2 y después la capacidad real del inventario de activos según V01.3. No deducir el modo contextual del viewport; recursos aplica el presupuesto propio de V01.4. - Móvil: contenido y acciones alcanzables sin desbordamiento global horizontal; la tabla puede tener scroll propio. V01.1 mantiene compañía y sección en una franja sticky, y sus destinos en Sheet; la navegación de portátil/tablet sigue V01.2 y el detalle de activos sigue V01.3. -- Ventanas bajas: altura dinámica, cuerpo desplazable y pie visible; no reducir campos para hacerlos caber. El presupuesto de ayuda y la versión compacta de progreso siguen pendientes, V04. +- Ventanas bajas: altura dinámica, cuerpo desplazable y pie visible; no reducir campos para hacerlos caber. La ayuda bajo demanda está implementada; quedan por revisar la versión compacta de progreso y los escenarios de altura baja, V04. - Formularios y tablas extensas: usar grupos/pestañas/componentes existentes; truncar solo con acceso al valor completo cuando sea necesario. La búsqueda del selector de columnas, V08, y el resumen de ficha, V10, requieren implementación o diseño adicional. Ninguna propuesta de nuevo ancho, breakpoint, colapso inicial o token se considera decisión vigente por aparecer en la auditoría. @@ -284,7 +284,7 @@ V02, V03, V06 y V09 están **resueltos en los escenarios verificados del sprint | V01 | Resuelto en los escenarios verificados de V01.1–V01.4: navegación y ambos inventarios por capacidad | 28 métodos visuales y once resoluciones; presupuestos/contratos documentados en [V01.4](docs/recursos-responsive-v01-4.md). Fallos globales previos identificados | | V02 | Resuelto por escenarios: inventario claro y selección de ambos inventarios en L04.4 | [L04.4](docs/cierre-programa-listados-l04-4.md) corrige el contraste seleccionado oscuro con una receta contextual; no equivale a validar todos los pares, temas o tablas | | V03 | Resuelto: adopción de patrón y pie estable | Tres pasos de activos/recursos verificados en los cuatro tamaños del sprint | -| V04 | Diseño de ayuda/progreso compacto + componentes | Acordar presupuesto de altura sin perder ayuda junto al pie; verificar 320×640 | +| V04 | Diseño de ayuda/progreso compacto + componentes | Ayuda bajo demanda implementada; pendiente revisar el presupuesto de progreso y verificar 320×640 | | V05 | Resuelto en los pilotos de L03.1 y los consumidores y escenarios de L04.1/L04.2/[L04.3](docs/adopcion-listados-l04-3.md); pendiente en otros consumidores | URL validada, preferencias existentes y retorno de scroll/foco; [L04.2](docs/adopcion-listados-l04-2.md) conserva estado local en configuraciones puramente modales | | V06 | Resuelto: integración de foco del inventario | Ratón/Enter, Escape/Cancelar, cabecera/fila, alternativa y selector anidado verificados | | V07 | Resuelto en consumidores locales y escenarios verificados de L03.2; cobertura restante y proveedores remotos pendientes | [L03.2](docs/normalizacion-busquedas-l03-2.md): vocales/Unicode/ñ, campos propios y consulta original; no confundir con ordenación | diff --git a/apps/web/package.json b/apps/web/package.json index 8275baff..0de42d9f 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -22,7 +22,8 @@ "@metis/identity": "workspace:*", "@metis/companies": "workspace:*", "@metis/assets": "workspace:*", - "@metis/resources": "workspace:*" + "@metis/resources": "workspace:*", + "@repo/rdx-ui": "workspace:*" }, "devDependencies": { "@repo/typescript-config": "workspace:*", diff --git a/apps/web/src/app.tsx b/apps/web/src/app.tsx index 38bf0afd..35068437 100644 --- a/apps/web/src/app.tsx +++ b/apps/web/src/app.tsx @@ -1,11 +1,14 @@ import { CompaniesWorkspace } from "@metis/companies/client"; import { IdentityWorkspace, identityApi, identityKeys } from "@metis/identity/client"; +import { DialogHelpLabelsContext } from "@repo/rdx-ui/lib/dialog-help-context"; import { useQuery } from "@tanstack/react-query"; +import { useTranslation } from "react-i18next"; import { LanguageSwitcher } from "./language-switcher"; import { useAppNavigation } from "./navigation/use-app-navigation"; export function App() { + const { t } = useTranslation("core"); const session = useQuery({ queryKey: identityKeys.session, queryFn: identityApi.session, @@ -15,20 +18,29 @@ export function App() { useAppNavigation(session.data); return ( -
- ( - } - userName={snapshot.user?.name ?? ""} - /> - )} - > - {null} - -
+ +
+ ( + } + userName={snapshot.user?.name ?? ""} + /> + )} + > + {null} + +
+
); } diff --git a/docs/architecture/auditoria-catalogo-global-sistemas-operativos.md b/docs/architecture/auditoria-catalogo-global-sistemas-operativos.md new file mode 100644 index 00000000..2fa6bd9b --- /dev/null +++ b/docs/architecture/auditoria-catalogo-global-sistemas-operativos.md @@ -0,0 +1,528 @@ +# Auditoría arquitectónica — Catálogo global de sistemas operativos + +Fecha: 25 de octubre de 2026. Estado: **auditoría y propuesta pendiente de aprobación**. No autoriza implementación, cambios de catálogo, activación de gates, consolidación ni migraciones. + +## 1. Resumen ejecutivo + +La plantilla real es `template:os`, denominada inicialmente «Sistema operativo», con uso explícito `os`. Sus características de serie son **proveedor, edición o plan y descripción**, todas opcionales. El nombre comercial es `Resource.name`, no una característica de esa plantilla. No hay campos propios de familia o versión principal ni un catálogo normalizado de fabricantes [E01–E04]. + +El catálogo global continúa limitado a aplicaciones de la plantilla stock `template:application`. La limitación aparece en captura de especificación, elegibilidad, DTO, lectores, administración y navegación. El agregado `Resource` y el esquema scoped son más generales que esa política: no basta con que una fila admita `scope: global` para que el sistema soporte un SO global [E05–E12]. + +`SoftwareInstallation` ya referencia el producto por UUID, mantiene compañía, activo, versión, estado y licencia particular y puede incorporar referencias globales mediante el servicio público de Resources. **Es razonable reutilizar su contrato persistido y DTO operativo sin añadir otro identificador de producto**, siempre que se amplíen las referencias públicas y todos los consumidores pertinentes. No puede hacerse hoy cambiando solo un gate o una allowlist [E13–E17]. + +Recomendación preliminar por ejes: + +1. **Identidad: A + E.** Un Resource por producto completo —familia, versión principal y edición cuando distingue producto—, con UUID como identidad y características declarativas estructuradas. E complementa la granularidad de A; no exige entidades jerárquicas. +2. **Alcance: opción 1 acotada.** Reutilizar Resources y su infraestructura global, admitiendo específicamente `template:os` además de aplicaciones, con consultas/contratos discriminados por tipo. Conservar los contratos de aplicaciones y la exclusión del SO en perfiles. No crear un catálogo persistido paralelo ni generalizar todos los recursos. +3. **Convivencia: preservar particulares.** Incorporar globales para selecciones nuevas, con procedencia visible; no reemplazar referencias ni deducir equivalencia por texto. Una posible preferencia visual es una decisión aparte. La adopción/sucesión D08.2 **sigue sin ratificar ni implementar** en las fuentes examinadas [E18–E20]. + +Confianza alta en el diagnóstico de código y en reutilizar Resource/SoftwareInstallation; media en las reglas semánticas de identidad, que necesitan aprobación funcional. No se conocen los datos ni los gates efectivos de una instalación real. + +## 2. Estado actual y método + +Base inspeccionada: HEAD `91f1d07e975e25723fe28358f6fa7b6b2dde07cc`, con cambios locales previos y trabajo concurrente principalmente de ayuda contextual y diálogos. La evidencia corresponde al **árbol de trabajo**. No se modifican sus archivos; la entrega propia es este informe. + +Se contrastaron dominio, casos de uso, validación, servicios públicos, DTO, HTTP, consultas, migraciones existentes, frontend y pruebas. Se consultaron los antecedentes de [SO en activos/puestos](auditoria-sistemas-operativos-activos-y-puestos.md), la [decisión del resumen actual](decision-so-actual-en-activos-y-puestos.md), [software del ordenador](../software-del-ordenador.md), normativa modular, documentación R2.0–R2.6 y el acta de D08.2. No se reabre la propiedad del SO instalado. + +Clasificación de afirmaciones: + +| Marca | Significado | +| --- | --- | +| **V** | Verificado estáticamente en el código o documentación vigente citados. | +| **T** | Probado en esta auditoría mediante los tests de §19. | +| **I** | Inferencia arquitectónica sustentada en los contratos; no funcionalidad instalada. | +| **P** | Propuesta pendiente de aprobación y desarrollo. | + +Salvo indicación P/I, las descripciones del estado actual son V. Las pruebas no convierten los modelos propuestos en funcionalidad disponible. Una migración leída no demuestra que una BD desplegada la tenga aplicada. + +Node `v24.20.0` y PNPM `11.25.0` concuerdan con `package.json`; herramientas ejecutadas en WSL/bash. No se leen `.env` ni credenciales, no se consulta BD real ni se crean datos maestros. Se revisan UX y contratos por código, DESIGN, patrón de diálogos y matriz de skills, sin mockups ni navegador. + +## 3. Auditoría de la plantilla SO + +### 3.1. Definición y significado real + +`template:os` es un **stockKey estable de definición**, expuesto como `builtinKey` en DTO. No es el UUID de un producto ni un UUID universal fijo de plantilla. Los registros y definiciones tienen identidades UUID propias. La plantilla tiene nombre editable, descripción inicial vacía, icono de serie y `usage: os`. Las copias y plantillas propias con uso explícito también pueden servir para SO particulares; ese uso no las convierte en globales [E01–E04]. + +La tabla siguiente describe la definición de serie del código; no certifica los bindings efectivos de una plantilla que haya sido editada en una instalación. El seed crea definiciones, no productos Windows, Ubuntu u otros. En esta auditoría no se leen inventarios persistidos: no se verifica cuántos SO particulares existen ni cómo están cumplimentados. Los nombres de pruebas son datos sintéticos. + +| Campo real | Tipo actual | Obligatorio | Utilidad | Problemas | Recomendación P | +| --- | --- | --- | --- | --- | --- | +| `Resource.name` | Texto, 1–100; Name normaliza su valor | Sí | Nombre comercial y selección | Puede contener fabricante, versión, edición o arquitectura sin estructura | Conservar nombre legible; proponer etiqueta coherente, sin renombrar registros automáticamente | +| `field:provider` | Característica `text`, hasta 10 000 | No | Proveedor/fabricante/distribuidor documental | Texto compartido con otras plantillas; no FK ni fabricante canónico | Reutilizar con semántica SO definida; no renombrar globalmente el campo | +| `field:software_edition` | Característica `text`, hasta 10 000 | No | Edición/plan | Ayuda orientada también a licencias; ausencia ambigua | Conservar; definir significado de edición e incompleto/no aplicable antes de publicar globales | +| `field:description` | Característica `text`, hasta 10 000 | No | Descripción del producto | No sustituye campos de identidad; no está en el índice de búsqueda actual | Mantener descriptiva, sin extraer automáticamente identidad | +| `Resource.notes` | Texto general, hasta 10 000 | Propiedad exigida por DTO; admite vacío | Notas administrativas | No es el campo description ni metadato público de identidad | Conservar notas; no usarlas para claves o atributos estructurados | +| `Resource.active` | Boolean | Sí | Disponibilidad para nuevas asociaciones | No significa soporte vigente, licencia válida o instalación efectiva | Conservar su semántica actual | +| `templateId` | UUID, fijo al editar | Sí | Referencia a definición/especificación | Una copia tiene identidad distinta | Global SO solo con plantilla stock real, salvo futura decisión expresa | + +No hay valores de producto predeterminados en la plantilla. Las descripciones de campos son ayuda, no datos que rellenar automáticamente. Las referencias de los tres campos de SO tienen `required: false`; no tienen listas de opciones ni relaciones de fabricante/familia [E01–E04]. + +### 3.2. Validación, persistencia, historia e idioma + +Los contratos Zod son estrictos; `characteristics` identifica campos mediante UUID y admite valores escalares. `ResourceValues.create` comprueba pertenencia a la especificación, tipo, opciones exactas de `choice`, fecha ISO y obligatoriedad; recorta texto y omite valores vacíos. `reconstruct` permite reconstruir registros anteriores incompletos. No hay validación semántica fabricante–familia–versión–edición [E02–E04]. + +Se guardan columnas tradicionales en `resources`, `resource_characteristics`, especificaciones y tablas de revisiones. `resource_characteristics` tiene clave por recurso/campo y columnas `textValue`, `numberValue`, `booleanValue`, `dateValue`. No se persiste una ficha SO como JSON. `ResourceRevision` conserva valores, especificación, actor y fecha; UUID, ámbito y plantilla no se reasignan al editar [E03, E11]. + +Añadir un campo al seed **no garantiza incorporarlo a una plantilla ya existente**. `ResourceCatalog.seed` conserva la definición previa mediante su enriquecimiento permitido; no sustituye sus bindings por los nuevos. La modificación explícita de plantilla tiene propagación transaccional que conserva estructura histórica y trata campos retirados con valor. Exigir nuevos datos globalmente puede dejar particulares incompletos o bloquear futuras ediciones; necesita plan específico, no modificación ciega del seed [E04]. + +Los formularios particulares y globales reutilizan React Hook Form, Zod y controles declarativos. Los tipos de catálogo disponibles son text/number/date/boolean/choice; no hay aquí una relación tipada entre valores de dos características ni listas dependientes. Las opciones choice son valores textuales compartidos; el tipo del campo y las opciones anteriores tienen restricciones de edición [E02, E04, E23]. + +Hay traducciones es/en de interfaz, uso `os`, acciones y estados. Sin embargo, `ResourceIdentityFields` y `ResourceCharacteristicControl` muestran nombres de plantilla/campo recibidos en datos: el seed los crea en castellano y esos componentes no traducen automáticamente `field.name`. La existencia de `usage.os = Operating system` no prueba traducción completa de los metadatos del catálogo. Es una limitación a resolver expresamente en nuevos campos, sin sobrescribir nombres personalizados [E23, E24]. + +## 4. Modelo de identidad del SO + +**V:** la identidad real es el UUID de Resource; no nombre, provider, edición ni combinación dinámica. El producto particular pertenece a compañía; el global tiene dueño NULL. Crear de nuevo con otro UUID puede representar otro producto con descripción equivalente. La idempotencia por `creationId` evita repetir una misma solicitud, no garantiza unicidad comercial [E03, E05–E08]. + +**P — A + E:** un Resource representa un producto seleccionable completo. Una tupla candidata para revisar identidad semántica es: + +`tipo SO + proveedor/fabricante canónico + familia + versión principal del producto + edición aplicable` + +No es una nueva PK ni un identificador compuesto que sustituya UUID. Arquitectura, canal o SKU solo entrarían en la identidad si el negocio confirma que distinguen productos que deben seleccionarse por separado. La composición no es una jerarquía universal ni obliga a crear entidades para cada nivel. + +Ejemplos conceptuales aportados por la solicitud: Windows 10 y Windows 11 serían recursos distintos; Windows 11 Home y Pro también. Windows Server 2022 Standard y Datacenter serían productos distintos. Para Ubuntu 24.04 LTS Desktop/Server debe aprobarse si esa distinción representa el producto que Metis necesita inventariar o solo la configuración de instalación. No se crea un catálogo maestro con estos ejemplos ni se presupone equivalencia comercial de sus etiquetas. + +La edición es **opcional en el modelo conceptual**, porque no todos los productos la tienen; una edición desconocida no equivale a «no aplica». La publicación de globales activos debe requerir identidad suficiente y revisión de esa diferencia. Antes de implementar se decide cómo expresarla: regla de admisión y revisión del catálogo, o característica declarativa de aplicabilidad si se necesita persistir la distinción. No inventar «Standard» o «Sin edición» como valor de relleno. + +## 5. Familia, versión, edición y campos candidatos + +La relación fabricante → familia → versión → edición ayuda a describir y buscar, pero no es obligatoriamente una cadena de entidades. Debe admitir productos sin edición, distribuciones/proveedores distintos, nombres no numéricos y productos con varias arquitecturas. No asumir que proveedor, fabricante y distribuidor son sinónimos universales. + +| Concepto evaluado | Equivalente actual | Propietario conceptual | Admisión recomendada P | Lista controlada | Declarativo existente / valor para esta fase | +| --- | --- | --- | --- | --- | --- | +| Fabricante/proveedor | `provider` texto | Producto; puede diferir del vendedor de la licencia | Exigir dato revisado para nuevos globales activos; no exigirlo retroactivamente a particulares | Vocabulario curado útil; no nueva entidad ahora | Sí, campo existente; definir rol y normalización | +| Familia | Sin campo específico | Producto | Añadir `os_family` como clave propuesta, obligatoria para nuevos globales activos | Texto curado inicialmente; choice solo con gobernanza/lista aprobada | Sí; aporta búsqueda, comparación y claridad | +| Nombre comercial | `Resource.name` | Producto | Obligatorio y legible; sugerencia/validación, edición consciente | No enum | Sí, raíz existente; no requiere otro campo nombre | +| Versión principal | Solo implícita en nombre/notas | Producto | Añadir `os_product_version` propuesto, texto obligatorio para nuevos globales activos | No numérica ni enum universal | Sí; distingue 10/11, XP o releases completos según producto | +| Edición | `software_edition` | Producto cuando diferencia producto | Completa cuando aplica; decidir no aplica/desconocida | Vocabularios por familia, sin imponer lista universal | Sí; conservar campo y revisar ayuda específica | +| Arquitectura | Sin campo SO de serie | Instalación efectiva; también puede restringir producto/SKU | Diferir salvo necesidad real; no parte de clave por defecto | Choice acotado solo si se aprueba | Un campo de producto documenta compatibilidad, no arquitectura efectiva de cada instalación | +| Canal de distribución | Sin equivalente SO | Producto/paquete o instalación, según significado | Diferir; separar canal de actualizaciones de adquisición | Solo tras definir semántica | Sí si es dato documental; no mezclar retail/OEM con canal técnico | +| Modalidad de licencia | `license_model` en plantilla licencia | Recurso licencia/contrato particular | Mantener allí; no duplicarla por cada SO | Ya hay Suscripción/Perpetua/OEM/Otra | No necesita ampliación del SO global | +| SKU | MPN/referencia de licencia no equivalen a SKU SO universal | Producto comercial/paquete | Opcional futuro con emisor definido | No enum | Texto posible; varios SKU no deben forzar una identidad por cadena sin criterio | +| Fecha de lanzamiento | Sin campo SO de serie | Release del producto al nivel que describa | Diferir salvo consumidor real; opcional con fuente | No | `date` posible; no necesaria para compartir productos | +| Fin de soporte | Sin campo SO propio | Producto/release/edición/canal según fuente | Diferir y definir alcance exacto antes de usarlo | No | `date` posible; una fecha de producto mayor puede ser engañosa para distintas versiones instaladas | +| Fin de soporte extendido | Sin campo propio | Política de soporte aplicable, no derecho adquirido | Diferir; fecha opcional si se conoce y se contextualiza | No | No acredita contratación ni licencia; no retirar instalaciones automáticamente | +| Descripción | `description` | Producto | Opcional | No | Reutilizar | +| URL oficial | `field:url` existe, pero no está enlazado a la plantilla SO stock | Producto | Ampliación opcional útil para documentación | No | Reutilizar definición si se aprueba el binding; no inventar otro campo URL | +| Identificadores externos | Sin contrato específico | Producto con emisor/namespace y tipo | Diferir hasta tener integración | No universal | Un ID textual puede bastar para un consumidor; múltiples IDs exigirían diseño relacional, no JSON por defecto | + +**Alcance mínimo recomendado:** reutilizar provider/edición/descripción/nombre, proponer familia y versión principal y valorar URL. No añadir automáticamente el resto. Las claves nuevas de esta tabla son propuestas, no definiciones instaladas ni datos maestros aprobados. + +El campo URL existente es texto; no hay validación semántica de URL en la validación genérica de características. Las policies propuestas deben identificar campos por sus identidades/stockKey reales, no por nombres visibles o traducidos. + +No reutilizar `field:expires` como «fin de soporte del SO»: hoy participa en vigencia documental de recursos/licencias. Caducidad contractual y fin de soporte tienen significados distintos. No derivar estado `active`, `needsReview` o derechos de uso a partir de fechas nuevas sin una decisión adicional [E01, E13]. + +## 6. Versión del producto frente a versión instalada + +**V:** `SoftwareInstallation.version` es un texto nullable de hasta 100 caracteres, sin gramática de fabricante, edición ni build separados. El dominio registra revisiones al cambiarlo y no añade revisión si los valores son semánticamente iguales. La pareja activo/producto es inmutable [E14–E16]. + +| Pregunta | Respuesta preliminar P y límite V | +| --- | --- | +| 1. ¿Windows 10 y 11 son recursos distintos? | Sí bajo A + E: cambia la versión principal del producto. No reclasificar automáticamente referencias actuales a partir del nombre. | +| 2. ¿Windows 11 Home y Pro son distintos? | Sí si representan productos/ediciones distintas que el técnico debe seleccionar. La licencia sigue siendo una relación independiente. | +| 3. ¿Actualización sin cambiar producto? | Actualizar `version` en la instalación actual; crea revisión conservando UUID, activo, producto y licencia. La disponibilidad se vuelve a comprobar cuando corresponda. | +| 4. ¿Cambio de edición? | Si cambia el producto según la identidad aprobada, elegir otro Resource. El contrato actual no permite cambiar `resourceId` de una instalación; requiere otra raíz y un cierre correcto de la anterior. | +| 5. ¿Actualización mayor: raíz o revisión? | Depende de si cambia el producto. Bajo A + E, cambiar Windows 10 por 11 implica producto y raíz nuevos; una actualización de mantenimiento del mismo producto implica revisión. Hoy no hay operación atómica de upgrade que cierre/cree ambas raíces. | +| 6. ¿Qué conserva `version`? | Texto de versión/release efectivamente declarado para esa instalación. Puede contener datos históricos más amplios: no se debe convertir silenciosamente a una nueva semántica restringida ni parsear valores automáticamente. | +| 7. ¿Hace falta build? | No para admitir productos SO globales. Diferir un campo `build` hasta tener consumidor y contrato. Si se aprueba, sería dato de instalación y de cada revisión; nullable para anteriores, con DTO/VO/mapper/migración y validación propia. | +| 8. ¿Nombre completo? | Nombre de producto identificable —ejemplo conceptual Windows 11 Pro— más `version` de instalación —por ejemplo 24H2—; build solo si posteriormente existe. No convertir 24H2 o 26100.x en edición ni sustituir la versión principal del producto. | + +Ubuntu, Debian, Fedora, macOS, Windows Server y otros productos pueden usar convenciones distintas. Conservar texto permite números, puntos, nombres y sufijos; no imponer un único orden ni una conversión numérica. Para cada familia se deberá confirmar qué release distingue producto y qué detalle pertenece a la instalación. Los ejemplos no fijan reglas comerciales universales ni fechas de soporte. + +El resumen actual de SO evita repetir una versión literal ya incluida en el nombre, con límites alfanuméricos; es una regla de presentación, no un normalizador ni una prueba de identidad. El producto/edición se resuelve del Resource actual, mientras el historial de instalación guarda su UUID, no una revisión fijada del producto. Congelar o reconstruir la descripción comercial histórica es una decisión distinta [E17]. + +## 7. Infraestructura global existente + +### 7.1. Modelo actual verificado + +```mermaid +flowchart LR + Definitions["Resources: definiciones globales\ntemplate:application y template:os"] --> App["Resource global application\nscope=global; companyId=NULL"] + Definitions --> LocalOS["Resource SO particular\nscope=company; companyId=C"] + App --> Resolver["Resolver por propósito\nallowlist global solo application"] + LocalOS --> Resolver + Resolver --> Installation["Companies: SoftwareInstallation\ncompanyId, assetId, resourceId, versión"] + License["Licencia particular de C"] --> Installation + Asset["Assets: activo de C\ncaracterísticas de fábrica"] --> Installation + Installation --> View["SO actual derivado\nficha del activo y puesto principal"] + App --> Profile["ApplicationConfiguration\nsolo application"] +``` + +La plantilla global define estructura reutilizable; un recurso basado en ella puede seguir siendo particular. El diagrama no afirma que exista un SO Resource global. Los atributos de fábrica son independientes de la instalación y no son entrada de esta propuesta. + +### 7.2. Dónde está restringido el SO global + +| Capa | Restricción V | Adaptación necesaria P | +| --- | --- | --- | +| Dominio Resource | Scope/owner estrictos, UUID v7 para global nuevo, plantilla fija e integración prohibida. `createGlobal` no decide por sí solo `template:application` | Conservar invariantes; aplicar elegibilidad coherente en todas las fronteras | +| Captura de especificación | `captureGlobalResourceSpecification` exige plantilla stock application, global y campos globales | Admitir exclusivamente stock os en su camino aprobado, sin elevar copias ni campos locales | +| Política de elegibilidad | `globalApplicationTemplateEligible` allowlist application exacta | Centralizar la decisión específica por tipo sin aflojar checks de aplicaciones | +| DTO de referencias | `SoftwareReference` admite application global/company y os **solo company** | Rama global os explícita; mantener el SO fuera de ApplicationConfiguration | +| DTO de listado/detalle | `GlobalApplicationSummary/Detail` y catálogo particular de aplicaciones fijan `kind: application` | Consultas/DTO por tipo; no devolver SO a un contrato cerrado de aplicaciones | +| Persistencia | Scoped permite global/NULL y prohíbe integración; FK UUID no impone familia application | Reutilizable en principio, sin DDL de raíz/instalación si scoped está instalado; validar sus writers y lectores | +| Lectura de metadatos | Carga globales, valida allowlist y parsea unión cerrada | Extender validación y proyección conjuntamente; un SO global insertado fuera del flujo hoy provoca inconsistencia | +| Administración | `repository.template()` busca application; create/update/propagación recapturan su especificación | Elección de tipo aprobada y plantilla os real; historial/capabilities y validación de identidad coherentes | +| HTTP | Schemas create/update genéricos sin integración, pero rutas, respuestas y permisos orientados a global applications | Extender contrato de lectura/admin y negociación; no confiar en un `kind` enviado por navegador para autorizar plantilla | +| Gates | Catálogo, selección mixta, asociación software y perfiles separados | Decidir habilitación específica del SO; no inferirla de los flags existentes | +| Frontend | Catálogo/rutas/formulario global de aplicaciones; candidatos sin edición/familia estructuradas | Reutilizar primitivas con contexto de tipo, enlaces y etiquetas correctos | + +La ampliación no es un ajuste de SQL aislado. Si se permite `os` en la policy pero no se amplía `SoftwareReferenceSchema`, fallará la lectura. Si se amplía esa unión pero el listado global sigue parseando todos los globales con `GlobalApplicationSummarySchema`, un SO puede romper una consulta de aplicaciones. Deben filtrarse tipos **antes** de construir sus DTO específicos [E05–E12]. + +### Superficies HTTP y de navegador verificadas + +| Superficie actual | Contrato y límite | +| --- | --- | +| `/api/resource-global-applications` | GET listado; POST alta administrada. `/capabilities`, `/template`, `/:id`, `/:id/admin`, `/:id/history`, POST `/:id/update` y `/:id/status`. No se localiza DELETE global público; desactivar conserva el registro. | +| `/api/resource-application-candidates/:companyId` | GET candidatos por propósito; `/capabilities` negocia capacidades; `/catalog` lista aplicaciones globales/particulares y continúa excluyendo os. | +| `/api/resource-application-references/:companyId/resolve` | POST de resolución por lote de UUID y propósito; lectura mínima de referencias, incluso inactivas. | +| `/api/resource-application-coincidences/:companyId/:id` | GET de coincidencias de aplicación particular; no SO ni comando de fusión. | +| `/catalogs/global-applications/:id` | Destino web global de aplicaciones; los enlaces de panel/mapa actuales se apoyan en él. No es una ficha SO global confirmada. | + +Estas rutas no se reinterpretan ni amplían en esta entrega. Su evolución por tipo debe mantener administración, proyección pública y operación particular separadas [E08, E10, E17, E18]. + +### 7.3. Gates y seguridad + +La capability de creación actual se calcula sobre la única plantilla application servida por `repository.template()`. Con dos tipos deberá distinguir disponibilidad/autorización de cada uno; una capability de aplicación no autoriza por omisión crear SO. + +Los defaults declarados son catálogo false, selección mixta false, asociación software false y perfil false; administration es una capacidad interna distinta y no habilita acceso por sí sola. Los valores de entorno efectivos no se leen. El comentario de configuración sobre perfiles cerrados no sustituye al código: R2.5 soporta su gate configurable. El arranque de catálogo exige esquema scoped y CHECK/FKs activos; la migración scope existente es explícita y no se ejecuta por esta auditoría [E09–E12]. + +La administración global pasa por Identity y su guard público, inicialmente ligado al administrador inicial. La lectura pública y la operación contextual por compañía tienen superficies distintas; no se exponen notas/actores administrativos en metadatos. El espacio de soporte compartido no es una ACL granular operador–compañía: validar pertenencia dentro de C no significa que ese operador nunca pueda abrir B. Esta extensión no autoriza cambiar ese modelo [E12, E25]. + +## 8. Integración con SoftwareInstallation + +### 8.1. Reutilización e impacto + +**I:** en una instalación scoped preparada, A y B pueden conservar instalaciones distintas con el mismo `resourceId` de SO global, sin alterar los campos actuales de SoftwareInstallation. Su `companyId` sigue siendo A/B; el activo y `licenseResourceId` se validan en su contexto. Compartir producto no comparte instalación, versión, licencia ni historial. + +El repositorio ya recibe lectores públicos de Resources/Assets ligados a la misma transacción; carga las referencias globales por propósito y las incorpora al contexto mínimo como `{ id, active, usage }`. Las políticas de software aceptan `application` u `os`. Las FKs a `resources.id` no necesitan otro `globalResourceId`; la licencia continúa local. Esta estructura es reutilizable, pero el resolver actual nunca devuelve un SO global válido [E13–E16]. + +| Componente | Contrato conservable | Extensión necesaria | +| --- | --- | --- | +| Root, revisiones, tablas y DTO de SoftwareInstallation | UUID, compañía, activo, resourceId, licencia, versión, estado, notas | Ninguna para el objetivo básico, bajo la inferencia anterior | +| Resolver y tipos comunes Resources | Sesión transaccional, scope y validación por propósito | Global os, su elegibilidad y DTO público, con pruebas de no ampliación de perfiles | +| Referencias particulares | Pertenencia y lectura local | Conservar templates/copies usage os locales; no promoverlas automáticamente | +| Selectores | Selección por UUID y badges de procedencia, recuperación y paginación | Identificación clara del tipo SO y de ediciones; contratos capaces de leer global os | +| `needsReview` | Producto/activo/licencia no disponibles o retirada pendiente | Debe seguir utilizando disponibilidad del producto global; sin añadir inferencia de soporte/licencia | +| Historia | Pareja fija y revisiones propias, referencias históricas conservadas | Leer global os inactivo también con gates de altas cerrados; no filtrar historia por elegibilidad de nueva asociación | +| Resumen de SO actual | Mismo selector por activo + configured + kind os | Transportar scope/destino resuelto: el enlace actual está fijado a recurso de compañía y sería incorrecto para global os | +| Panel, mapa y puesto | Consulta derivada del activo, gestión existente | Revisar rutas global-applications y nombre/origen; no copiar SO al puesto | + +**V:** el propósito vigente es `software-installation`; **no existe un propósito literal `os`**. El tipo os se determina a partir de plantilla/referencia dentro de ese propósito. `application-configuration` debe seguir aceptando solo application. No ampliar la policy de perfiles ni el inventario particular genérico para resolver esta necesidad [E10, E13–E17]. + +### 8.2. Compatibilidad y concurrencia + +El header de contrato de Resources actual es `2`; sus clientes validan una unión que excluye global os. **P:** aprobar negociación/capacidad versionada para lectores preparados antes de permitir escrituras. Añadir una rama a un schema del servidor no actualiza los navegadores antiguos. El número del contrato y los errores de upgrade requieren decisión explícita, no se dan por implementados. + +Conservar la coordinación catálogo compartido → compañía → referencias/raíces y la validación de disponibilidad dentro de la transacción. La creación/edición de catálogo mantiene su bloqueo exclusivo. Desactivar global os no debe dejar carreras con nuevas instalaciones, borrar datos ni impedir notas/retirada de las existentes. Los tests MariaDB actuales contienen esas carreras para aplicaciones, pero no se han ejecutado aquí ni prueban automáticamente el caso SO [E13–E16, E26]. + +Cerrar gates de escritura después de crear referencias no elimina los globales ni permite que lectores antiguos las ignoren. Rollout y rollback deben preservar la lectura de todo UUID ya usado. Tampoco se habilitan licencias globales como efecto colateral. + +## 9. Convivencia con SO particulares + +Resource utiliza el booleano `active`; «retirado» en los escenarios de recursos se interpreta aquí como inactivo, no como un estado `Resource.retired`. El estado retirado de un activo y el de una instalación son conceptos diferentes. + +| Escenario | Estado/riesgo V | Tratamiento recomendado P | +| --- | --- | --- | +| Windows 11 Pro global y particular | UUID distintos; el global SO aún no es elegible | Mantener ambos cuando se habilite; mostrar procedencia, no sustituir particular | +| Dos compañías con nombres iguales | La similitud no crea identidad compartida | Crear/curar un global para nuevas selecciones; conservar referencias anteriores | +| Particular con instalaciones abiertas | `assetId/resourceId` fijos y pareja abierta única | No UPDATE directo de target; conservar raíz y revisiones | +| Particular que mezcla ediciones | No hay semántica estructurada que lo separe | Revisión manual; no dividir instalaciones ni inventar su edición | +| Denominación incorrecta | Renombrar registra ResourceRevision; consumidores resuelven nombre actual | Corregir presentación si sigue siendo el mismo producto; si cambia identidad, crear otro producto | +| Particular inactivo/retirado | No es candidato de alta; historia y notas/retirada conservan sus caminos | Mantenerlo y sus referencias; no usar global para ocultar su historia | +| Falta edición | No implica ausencia de edición | Mantener incompleto hasta revisión; no equiparar con global sin edición | +| Versión solo en nombre | Texto no separable de forma segura por gramática universal | Conservar original y completar características por revisión consciente | +| Equivalente aparente a global | R2.6 no autoriza consolidar | Señal/revisión futura, sin reemplazar UUID; adopción requiere ratificación aparte | + +### Estrategia de transición + +1. **Coexistencia indefinida:** viable con el esquema conceptual actual; opción inicial de menor riesgo. +2. **Preferencia visual por globales:** puede facilitar nuevas selecciones si se aprueba, manteniendo visibles los particulares y el valor seleccionado. El orden actual es por nombre, sin una preferencia global semántica. +3. **Detección de equivalencias:** R2.6 aporta infraestructura de lectura y señales por nombre/proveedor, pero excluye explícitamente SO. Extenderla exige criterios de identidad de SO y contrato propio; no habilita fusión. +4. **Vinculación documental:** hoy no existe una relación SO particular → SO global de equivalencia. Una futura relación sería otro contrato, con autor, evidencia y semántica; no usar notas como FK encubierta. +5. **Sustitución controlada:** `resourceId` es inmutable. No reutilizar un update ordinario ni simular retirada física para cambiar solo la referencia documental. +6. **Adopción individual y migración administrativa:** D08.2 propone sucesión, planes y confirmación atómica, pero conserva acta pendiente; no hay `superseded`, `closeForSuccession`, permisos ni writers de adopción instalados en los módulos examinados. Aun ratificado para aplicaciones, su alcance SO necesitaría aprobación específica. +7. **Revisión de recursos heredados:** solo tras inventario autorizado y aislado; nunca convertir todos los textos que contengan «Windows» en un mismo producto [E18–E20]. + +## 10. Duplicados e identidad semántica + +### 10.1. Qué garantiza hoy el sistema + +- UUID distingue recursos; `creationId` y la comparación con el primer registro/actor permiten reintentar un alta global. No es una clave semántica comercial. +- `ResourceUseCases` particular impide ciertos nombres repetidos en la misma plantilla/contexto, además de la regla de buzón. Esa regla textual local no es una unicidad global fabricante/familia/versión/edición. +- El catálogo de **definiciones** también protege nombres dentro de su ámbito; no confundirlo con unicidad de productos Resource. +- No se localizó una restricción de unicidad semántica de productos globales por características. Las claves de `resource_characteristics` impiden dos valores para el mismo campo/recurso, no dos recursos con la misma combinación de atributos [E04, E06, E11, E21]. + +R2.6 compara NFC, trim, espacios y minúsculas, conservando tildes, puntuación, números y ediciones. Es una señal de revisión. La búsqueda usa otra normalización, que pliega ciertos acentos vocálicos pero conserva ñ y otras escrituras, y una coincidencia por substring. Ninguna convierte Professional/Pro/Profesional ni elimina «64-bit» para fusionar [E19, E22]. + +### 10.2. Qué puede conseguir E sin nuevas entidades + +**I/P:** características declarativas más una policy de aplicación pueden calcular/comparar una tupla normalizada y exigir consistencia de nuevos SO globales. Se pueden revisar duplicados bajo el lock de catálogo existente, siempre que todos los writers relevantes —alta, edición y propagación de campos/plantillas— apliquen la misma regla. No basta un check del formulario. + +**Límite:** una UNIQUE ordinaria sobre filas EAV no impone directamente una combinación repartida entre provider/familia/versión/edición. Si se exige garantía física por esa combinación, estudiar columnas relacionales o una proyección/tabla de identidad normalizada y su migración explícita. No se propone índice ni tabla en esta auditoría; no persistir una clave en JSON como sustituto del diseño. + +Nombre canónico, aliases, fabricante normalizado, identificadores externos y señales de duplicado tienen funciones distintas. La recomendación mínima es nombre legible coherente y metadatos curados, sin alias automáticos ni motor de equivalencias. Un identificador externo solo puede ser decisivo con namespace, unicidad y fuente fiables definidos. + +**P:** fijar los atributos que determinan producto al hacerlo utilizable; una corrección ortográfica puede conservar identidad si no cambia su significado, pero cambiar familia/release/edición comercial requiere otro UUID. Si se permite edición semántica de un producto ya usado, se corre el riesgo de reinterpretar instalaciones históricas. El nombre visible puede evolucionar con revisión sin renombrados masivos automáticos. + +## 11. Licencias + +**V:** `licenseResourceId` apunta opcionalmente a un Resource particular de la misma compañía, de uso license, con FK en instalación y revisiones. La policy valida actividad/vigencia en nuevas dependencias, permite desvincular y documentar retirada y conserva historia. No existe cálculo de derechos, plazas consumidas ni compatibilidad automática producto–licencia [E13–E16]. + +La plantilla licencia incluye producto licenciado, edición/plan, provider, modalidad, métrica, cantidad, referencia, caducidad y renovación. Modalidad de serie: Suscripción/Perpetua/OEM/Otra. **Retail y Volumen no son opciones propias de serie**. La métrica puede documentar usuario, dispositivo, servidor, concurrente, corporativa u otro. Esto no acredita un vínculo efectivo a dispositivo/persona, transferencia permitida o cobertura de una edición. + +OEM, Retail, Volumen o Suscripción deben evaluarse como condiciones/documentación del recurso licencia o paquete contratado; no convertirlas automáticamente en familias/ediciones del SO. Un producto global compartido puede tener instalaciones con licencias particulares diferentes o sin referencia de licencia. La identidad de edición facilita al técnico distinguir productos, pero no demuestra derechos de uso. + +**Cambio imprescindible para SO global:** conservar la separación de producto global y licencia local en políticas/resolvers/selectores. No hace falta cambiar la cardinalidad ni añadir campos de licencia por este objetivo. Añadir opciones de modalidad o reglas de cobertura sería otro alcance aprobado, no una condición implícita de habilitar SO global. + +## 12. UX de búsqueda, edición y selección + +### 12.1. Recorrido actual y limitaciones + +| Tarea | Comportamiento verificado por código | Adaptación propuesta, sin nuevo diseño | +| --- | --- | --- | +| Crear SO global | La administración ofrece la especificación de application; os se rechaza | Entrada de tipo explícita con plantilla os stock y permisos existentes | +| Buscar SO | Candidatos de software pueden incluir SO particulares; no SO globales | Consulta capaz por tipo y etiquetas de producto suficientes | +| Seleccionar para activo | `SoftwareApplicationSelect` usa propósito software-installation; alta rápida SO crea particular | Mantener destino particular del alta rápida salvo acción global autorizada explícita | +| Consultar características | Particular tiene ficha/especificación; global público tiene proyección application de nombre/provider/url/description | Definir proyección pública SO para identidad; no exponer notes, actores o usos de compañías | +| Distinguir procedencia | Picker muestra global/company y provider; deshabilita global sin gate de asociación | Conservar badges y selección original; no presentar un particular como copia global | +| Diferenciar versiones/ediciones | Etiqueta del picker es nombre; no columnas de familia/release/edición | Nombre canónico breve y metadatos secundarios; decidir filtros solo cuando el catálogo lo justifique | +| Evitar ambigüedad | Revisión de coincidencias por nombre en formulario global y detección R2.6 para aplicaciones | Comparar identidad SO propuesta; cambiar campos semánticos debe invalidar la revisión previa de duplicados | + +El selector remoto tiene búsqueda con debounce de 200 ms, cursor/paginación, carga, error/reintento y selección inicial. El listado global admite search, provider, active y orden name/id. **Los candidatos no admiten filtros de familia, edición, versión ni provider separado:** su schema solo permite search/purpose/paginación. El paginador compara substring en nombre **o** provider; no consulta cualquier característica dinámica. Las descripciones o campos añadidos no pasan a ser buscables por existir en EAV [E10, E22–E24]. + +Por tanto, «Windows 11» puede distinguir Home/Pro/Enterprise/Education si esos términos están en nombres de productos correctamente curados; «Windows Server 2022» puede distinguir Standard/Datacenter de igual modo. No se puede garantizar ese resultado con nombres genéricos y edición escondida en una característica que el resolver no proyecta. Son ejemplos de UX, no datos aprobados ni resultados observados en una BD. + +La nueva discriminación por tipo debe reflejarse en filtros, query keys y contexto de cursores; también se revisa la invalidación por UUID de referencias, fichas e instalaciones. El helper actual de invalidación ya identifica instalaciones/configuraciones que usan el producto: se reutiliza esa estrategia explícita, sin introducir otro estado remoto ni invalidar toda la caché indiscriminadamente. + +Una etiqueta canónica basada en metadatos puede bastar inicialmente para selección conocida. No sustituye una búsqueda estructurada si se necesitan filtros; esa extensión exigiría query/DTO, proyección, cursor y tests. La etiqueta debe respetar el límite actual de 100 del nombre; el fabricante puede ir en información secundaria. No forzar un nombre interminable ni introducir filtros, recientes o sugerencias sin una necesidad real. + +### 12.2. Edición e historia + +Conviene separar proveedor/familia/versión principal/edición para nuevos SO globales curados. El nombre puede proponerse y validarse respecto a esos atributos, pero hoy se introduce manualmente y crea historial. No sobrescribirlo cada vez que cambia un campo ni regenerar los existentes. Decidir si se permiten alias/localizaciones y cómo preservar la identidad al corregir texto. + +La característica description usa hoy el control genérico Input de tipo texto; las notas generales disponen de Textarea. El límite de 10 000 caracteres no acredita una experiencia cómoda de edición de descripciones largas. No se cambia ese control en esta auditoría. + +El formulario global actual exige revisar el nombre en altas y consulta posibles coincidencias por texto; la confirmación se vincula al nombre revisado, no a una huella completa de campos. Es insuficiente como prevención de duplicado de SO si cambian familia, versión o edición sin cambiar el nombre. No es una garantía de servidor de unicidad comercial [E23]. + +### 12.3. UI, accesibilidad y límites de esta revisión + +La UI usa shadcn/Base UI y componentes propios RemoteOptionPicker, CursorPagination, SelectWithAction, FormDialog, FieldSections y ayuda contextual. Reutilizarlos, manteniendo estados diferenciados, labels, foco, cancelación y es/en. Los campos dinámicos se muestran con su nombre almacenado y necesitan política de traducción explícita. Las fichas y resúmenes también requieren enlaces que distingan destinos globales y particulares [E17, E23, E24]. + +DESIGN y el patrón de diálogos son normativos; ayuda contextual está cambiando concurrentemente en el árbol. No se da por terminada su nueva implementación ni se convierte una desviación en estándar. No se han probado tema claro/oscuro, responsive, teclado, foco o contraste en navegador durante esta auditoría. Las capturas de la implementación previa del resumen no validan un selector global SO futuro. No se crean mockups ni componentes. + +## 13. Alternativas arquitectónicas y alcance de cambios + +### Identidad + +- **A — Producto completo:** comprensible en selección y consistente con un target de instalación fijo. Sin metadatos adicionales no permite revisar identidad ni filtrar bien. +- **B — Familia + versión, edición en instalación:** exige modificar instalación, revisiones, schemas, migración y presentación; debilita la distinción de ediciones en catálogo/licencia. +- **C — Familia, versión/edición en instalación:** reduce entradas, pero traslada gran parte de identidad a cada compañía; cambia el significado operativo y el coste de migración. +- **D — Entidades jerárquicas:** útil si hay consumidores reales de taxonomía, releases y edición, pero introduce relaciones, contratos y administración que hoy no se justifican. +- **E — Resources declarativo:** reutiliza UUID y EAV relacional con campos estructurados/policies. No obtiene unicidad física multicampo ni equivalencia comercial solo por añadir campos. Se recomienda combinado con A. + +### Globalidad + +- **O1 — Ampliación puntual por tipos admitidos:** incorpora os sobre Resource, con políticas específicas de tipo y propósito y contratos compatibles; menor impacto suficiente. +- **O2 — Catálogo global de productos software generalizado:** puede ser una evolución de vocabulario/API si aparecen más consumidores; no requiere por sí mismo otra entidad, pero hoy implicaría renombrar/generalizar mucho más de lo necesario. +- **O3 — Catálogo independiente SO:** duplicaría autoridad de producto, referencias, admin, seguridad e historial y necesitaría nuevas relaciones/migraciones; no recomendado con los requisitos actuales. + +### Clasificación del impacto P/I + +| Cambio potencial | Reutilización / extensión / migración | +| --- | --- | +| Global SO con metadatos actuales en una instalación scoped | Reutilizar tablas Resource y SoftwareInstallation; extender policies, DTO, resolver, administración, consumidores y negociación. No se identifica DDL nuevo imprescindible para la relación básica. | +| Familia/versión principal/URL declarativas | Evolución explícita de definiciones y bindings, con revisión de plantillas ya editadas, propagación y registros incompletos. Es cambio de datos/contrato, aunque no exija DDL de columnas. | +| Entorno legacy sin scope | Requiere el procedimiento existente R2.1b previamente autorizado; no se presume aplicado ni se ejecuta ahora. | +| Build/arquitectura efectiva en instalación | Extensión del agregado/DTO/revisiones y migración relacional; diferida. | +| Unicidad física semántica multicampo | Diseño de columnas/proyección relacional e índices, con migración expresa; pendiente de necesidad y aprobación. | +| Jerarquía de fabricantes/familias/releases/ediciones | Nuevas entidades y contratos; diferida por falta de consumidores que justifiquen el coste. | +| Adopción/sucesión | Requiere ratificación D08 y alcance SO, implementación coordinada y probablemente persistencia operativa nueva; fuera de esta entrega. | +| Licencias globales/compliance/perfiles con requisitos SO | No necesarias para el objetivo; pospuestas y no habilitadas por O1. | + +## 14. Matrices comparativas + +Valoraciones cualitativas de **adecuación al objetivo actual**: A=alta, M=media/condicionada, B=baja. En migración/mantenimiento, A significa menor coste/riesgo, no más migración. Son juicios arquitectónicos, no mediciones ni una decisión aprobada. + +### 14.1. Identidad + +| Criterio | A: producto completo | B: familia+versión | C: familia | D: jerarquía | E: declarativo con granularidad A | +| --- | --- | --- | --- | --- | --- | +| Coherencia de dominio | A: producto distinguible | M: edición dispersa | B: versión dispersa | A: niveles explícitos | A: producto y atributos | +| Simplicidad | A: una raíz | M: campos en I | M: menos recursos, I más compleja | B: varias entidades | A: Resource existente | +| Reutilización Resources | A | M | M | B | A | +| Integridad/duplicidad | M: texto insuficiente | M: clave parcial | B: producto ambiguo | A si reglas aprobadas | M: policy necesaria; EAV sin UNIQUE de tupla | +| Compatibilidad instalación | A: target UUID actual | B: mover edición | B: mover versión/edición | M: requiere adaptadores | A: mismo target | +| Flexibilidad entre fabricantes | M: curación | M: edición no universal | M: detalle disperso | M: jerarquía no universal | A: campos opcionales/policy | +| Migración heredada | A: conservar UUID | B: reinterpretar ediciones | B: reagrupar y mover valores | B: reconstruir relaciones | M: enriquecer sin inventar | +| Mantenimiento | A | M | M | B | M: gobernar atributos/policies | +| Evolución/inventario automático | M: nombre poco estructurado | M | M | A con coste alto | A: atributos estructurados | +| UX/claridad/búsqueda | A si nombre preciso | M: edición fuera del selector | B: selección demasiado genérica | M: navegación más compleja | A: etiqueta y metadatos | +| Versiones de producto | A: recursos distintos | A | B: solo en I | A | A: campo explícito | +| Ediciones | A: recursos distintos | B: en I | B: en I | A | A: atributo identitario | +| Compartir globalmente | A | M: global incompleto | M: global muy amplio | A | A | +| Licencias documentales | A: producto reconocible | M: depende de I | B: familia demasiado amplia | A | A: edición visible sin inferir derechos | +| Actualizaciones e historial | M: cambio producto exige nueva I | M: parte cambia en I | B: mezcla identidad y mantenimiento | M: muchos niveles | M: conserva I; semántica por aprobar | + +A + E reutiliza la separación vigente producto/instalación [E01–E17]. B/C exigen modificaciones y reinterpretaciones de instalación. D solo compensa su coste si surgen consumidores de esa taxonomía. E no justifica por sí sola crear un framework de schemas de identidad. + +### 14.2. Globalidad + +| Criterio | O1: ampliación puntual | O2: generalización software | O3: catálogo SO separado | +| --- | --- | --- | --- | +| Coherencia | A: producto Resource | A si conserva políticas por tipo | M: autoridad paralela | +| Simplicidad | A: extensión acotada | M: refactor de superficies | B: nueva infraestructura | +| Reutilización | A | A | B | +| Integridad | M: actualizar todas las barreras | M: evitar permisividad genérica | M: mantener referencias cruzadas nuevas | +| Compatibilidad | M: DTO/versionado explícitos | M/B: más consumidores | B: otro tipo de destino | +| Flexibilidad | M: dos tipos concretos | A | M | +| Migración | A: sin mover UUID | M: posible coste de contratos | B: traslado/vínculos nuevos | +| Mantenimiento | A | M: abstracción mayor | B: duplicación | +| Evolución | A para SO | A para necesidades adicionales | M: silo | +| UX | A: tipo y etiquetas claros | M: requiere reorganizar vocabulario | M: selección entre catálogos | + +O1 incluye contratos por tipo; no significa tocar una constante y aceptar SO en todos los endpoints. Se mantiene el propósito de perfiles cerrado a application [E05–E12]. + +### 14.3. Convivencia + +| Criterio | Coexistencia visible | Preferencia por globales en altas | Adopción/sucesión controlada | +| --- | --- | --- | --- | +| Coherencia | A: UUID y dueño conservados | A si no cambia referencias | A si se aprueba semántica | +| Simplicidad | A | M: criterios de orden/recomendación | B: coordinador/operaciones | +| Reutilización | A | A: candidatos existentes ampliados | M: D08 aún propuesta | +| Integridad | A: sin reescritura | A si elegido es explícito | M: atomicidad y evidencia nuevas | +| Compatibilidad | A tras ampliar lectura global os | M: clientes/capabilities | B: nuevos estados/lectores si se adopta D08 | +| Flexibilidad | A | A si particulares permanecen | A: selección explícita | +| Migración | A: no obligatoria | A: solo futuras altas | B: operación administrativa por diseñar | +| Mantenimiento | M: duplicados permanecen | M: duplicados previos permanecen | B: trazabilidad/compensación | +| Evolución | M: limpieza manual diferida | A: reduce nuevas copias si se usa | A con coste y ratificación | +| UX | M: procedencia evita confusión | A: facilitar selección, sin ocultar | M: flujo especializado y riesgos claros | + +Se recomienda coexistencia primero; preferencia visual sujeta a decisión y adopción diferida. No convertir ese orden en una migración automática [E18–E20]. + +## 15. Recomendación preliminar concreta + +**P, sin aprobación efectiva:** + +1. Cerrar la granularidad A + E: producto completo con UUID; provider/familia/versión principal y edición aplicable estructurados. Aprobar primero las excepciones de edición, arquitectura, canales y familias que no siguen la jerarquía. +2. Autorizar O1 como ampliación de la allowlist global exacta a stock os, manteniendo application y sus garantías. No promover plantillas particulares/copiadas ni campos locales a globales por declarar usage os. +3. Preparar todos los lectores, contratos versionados y destinos de navegación antes de permitir altas globales SO. Mantener aplicaciones globales y perfiles con sus contratos específicos. +4. Reutilizar SoftwareInstallation y sus licencias particulares sin nuevos campos para enlazar producto global. Conservar estados, historial, locks, validación y unicidad de pareja abierta. +5. Empezar con coexistencia y elecciones nuevas explícitas. Cualquier detección específica SO requiere revisión; adopción/sucesión queda pendiente de D08 y aprobación SO aparte. +6. Diferir build, compliance, integración automática, entidades jerárquicas y unicidad física hasta contar con requisitos/consumidores que justifiquen sus migraciones. + +No basta con cambiar el nombre del catálogo. La recomendación conserva Resource como autoridad y la instalación como fuente única del SO operativo. No implementa ninguna de estas fases. + +### Modelo propuesto, no implementado + +```mermaid +flowchart LR + Template["Resources: template:os stock\ncampos de identidad aprobados"] --> GlobalOS["Resource global SO\nUUID P; scope=global; owner=NULL"] + Identity["Características declarativas\nproveedor, familia, release, edición aplicable"] --> GlobalOS + Local["SO particulares existentes\nUUID y compañía conservados"] --> Resolver["Resources: referencias por tipo y propósito\ncontrato preparado para global os"] + GlobalOS --> Resolver + Resolver --> IA["SoftwareInstallation A\nactivo A, versión A, historia A"] + Resolver --> IB["SoftwareInstallation B\nactivo B, versión B, historia B"] + LA["Licencia particular A"] --> IA + LB["Licencia particular B"] --> IB + IA --> VA["Activo A / puesto principal\nvista derivada"] + IB --> VB["Activo B / puesto principal\nvista derivada"] + Pending["Adopción/sucesión\ndiferida y no ratificada"] -.-> Local +``` + +Los SO particulares se resuelven exclusivamente en su propia compañía; solo el producto global P se comparte entre A y B. Las cajas describen una propuesta de colaboración, no nuevas entidades del fabricante/familia ni tablas creadas. IA/IB siguen siendo el agregado existente y no heredan el dueño NULL del producto. + +## 16. Riesgos prioritarios + +| Riesgo | Consecuencia | Condición antes de desarrollar/desplegar | +| --- | --- | --- | +| Abrir solo la policy/flag | Lecturas fallan por DTO cerrados o plantilla no admitida | Revisar captura, metadata, listado/detalle, admin, consumers y compatibilidad como unidad | +| SO global contaminando listados application | Error de consulta o SO seleccionable en perfiles | Filtro por tipo antes del schema; negativos explícitos de perfiles/SMB/Access/licencias | +| Clientes v2 antiguos | Parseo fallido de referencias/candidatos nuevos | Negociación/capacidad y despliegue de lectores antes de writers | +| Identidad de producto mutable | Instalaciones pasadas cambian de significado | Regla de atributos identitarios y correcciones, con historia preservada | +| Particular desconocido equiparado a global | Pérdida semántica al consolidar | No fusionar por texto, versión ausente o alias supuesto | +| Seed sustituido o campos obligatorios masivos | Personalizaciones y ediciones bloqueadas | Evolución explícita, particular incompleto preservado, tests de propagación | +| Unicidad comercial supuesta en EAV | Duplicados concurrentes o falsa garantía SQL | Policy común y locks; decidir si hace falta soporte físico relacional | +| Fechas soporte mezcladas con caducidad/licencia | Retiradas o alertas incorrectas | Definir significado y no reutilizar expires silenciosamente | +| Licencia global o de otra compañía aceptada | Mezcla de ámbitos y pérdida de control | Mantener license local; negativos HTTP/SQL futuros | +| D08 tratado como aprobado | Cambio de targets/estados sin autorización | Ratificación expresa y alcance SO antes de sucesión | +| Navegación actual de resumen | Enlace global os apuntaría a ficha particular | Scope/destino resuelto en todas las vistas e historial | +| Activación real no conocida | Diagnóstico documental tomado como despliegue certificado | Verificar entorno aislado autorizado y aceptación global en una futura implementación | + +## 17. Decisiones pendientes de aprobación + +| ID | Decisión que debe cerrar el propietario funcional/arquitectónico | Recomendación preliminar | +| --- | --- | --- | +| D-SO-01 | Granularidad de producto, ejemplos por familia y qué cambio supone otro UUID | A + E; principal+edición cuando distingue producto | +| D-SO-02 | Rol de provider: fabricante/distribuidor y vocabulario canónico | Reutilizar campo, definir valores curados sin entidad nueva | +| D-SO-03 | Obligatoriedad de familia/release y tratamiento de locales incompletos | Exigir suficiencia a nuevos globales activos; no backfill inferido | +| D-SO-04 | Edición no aplica/desconocida | No equivaler ausencia; aprobar regla de publicación y, si se necesita, atributo de aplicabilidad | +| D-SO-05 | Arquitectura/canal/SKU como identidad o detalle | Fuera de clave inicial salvo caso funcional demostrado | +| D-SO-06 | Mutabilidad semántica de productos ya usados y revisión histórica | Nueva identidad para cambio comercial; corrección documentada sin reescritura silenciosa | +| D-SO-07 | Alcance global, plantillas admitidas y contratos/rutas por tipo | O1, solo stock os; aplicaciones/perfiles mantienen restricciones | +| D-SO-08 | Compatibilidad, negociación y gates específicos | Lectores preparados primero; no asumir header 2 compatible | +| D-SO-09 | Detección de duplicados frente a garantía física de unicidad | Curación/policy inicial; aprobar DDL aparte si se exige garantía física | +| D-SO-10 | Presentación/nombre canónico, alias y traducciones de campos | Nombre legible revisable; no generación obligatoria/renombrado masivo | +| D-SO-11 | Preferencia de globales y alcance de posibles coincidencias SO | Visibilidad de ambos; priorización explícita sin sustitución | +| D-SO-12 | Adopción/sucesión de particulares | Diferir; D08.2 sigue pendiente y no concede alcance SO | +| D-SO-13 | Necesidad de build, soporte y fuentes externas | Posponer hasta consumidor definido; conservar version actual | +| D-SO-14 | Aceptación, seguridad y plan de despliegue/rollback | Ensayos aislados por tipo, concurrencia, clientes antiguos y preservación histórica | + +## 18. Evidencias de código y documentación + +Los enlaces señalan líneas del árbol inspeccionado. Los símbolos y rutas son la referencia estable si el trabajo concurrente desplaza líneas. La lectura de DDL no se presenta como inspección de una base instalada. + +| ID | Hallazgo / símbolos | Rutas y líneas | +| --- | --- | --- | +| E01 | Plantilla SO stock, campos compartidos y licencia; seed no crea productos | [resource-catalog-seed.ts:149](../../modules/resources/src/api/domain/resource-catalog-seed.ts#L149); [resource-catalog-seed.ts:90](../../modules/resources/src/api/domain/resource-catalog-seed.ts#L90); [resource-catalog-seed.ts:96](../../modules/resources/src/api/domain/resource-catalog-seed.ts#L96); [resource-catalog-seed.ts:229](../../modules/resources/src/api/domain/resource-catalog-seed.ts#L229) | +| E02 | Tipos declarativos y contratos; uso os | [resource-definition.ts:9](../../modules/resources/src/api/domain/resource-definition.ts#L9); [resource-definition.ts:40](../../modules/resources/src/api/domain/resource-definition.ts#L40); [resource-values.ts:40](../../modules/resources/src/api/domain/resource-values.ts#L40) | +| E03 | UUID/scope/owner, createGlobal y plantilla fija; revisión de Resource | [resource.aggregate.ts:63](../../modules/resources/src/api/domain/resource.aggregate.ts#L63); [resource.aggregate.ts:219](../../modules/resources/src/api/domain/resource.aggregate.ts#L219); [resource-scope.ts:5](../../modules/resources/src/api/domain/resource-scope.ts#L5); [resource-revision.entity.ts:13](../../modules/resources/src/api/domain/resource-revision.entity.ts#L13) | +| E04 | Validación de valores; seed conservador, definiciones y propagación | [resource-values.vo.ts:212](../../modules/resources/src/api/domain/value-objects/resource-values.vo.ts#L212); [resource-catalog.aggregate.ts:174](../../modules/resources/src/api/domain/resource-catalog.aggregate.ts#L174); [resource-definition.entity.ts:86](../../modules/resources/src/api/domain/resource-definition.entity.ts#L86); [prepare-resource-template-updates.ts:128](../../modules/resources/src/api/infrastructure/persistence/prepare-resource-template-updates.ts#L128) | +| E05 | Dos barreras globales de plantilla y campos reales | [global-application-template-policy.ts:2](../../modules/resources/src/api/application/global-application-template-policy.ts#L2); [global-resource-specification.ts:8](../../modules/resources/src/api/application/global-resource-specification.ts#L8) | +| E06 | Idempotencia por UUID, alta/edición global y writer transaccional | [global-resource.use-cases.ts:22](../../modules/resources/src/api/application/global-resource.use-cases.ts#L22); [global-resource.use-cases.ts:69](../../modules/resources/src/api/application/global-resource.use-cases.ts#L69); [sequelize-global-resource.repository.ts:59](../../modules/resources/src/api/infrastructure/persistence/sequelize-global-resource.repository.ts#L59); [sequelize-global-resource.repository.ts:197](../../modules/resources/src/api/infrastructure/persistence/sequelize-global-resource.repository.ts#L197) | +| E07 | Unión de referencias cerrada: os solo particular; proyecciones application | [application-reference.dto.ts:29](../../modules/resources/src/common/dto/shared/application-reference.dto.ts#L29); [application-reference.dto.ts:64](../../modules/resources/src/common/dto/shared/application-reference.dto.ts#L64); [resource-application.request.dto.ts:46](../../modules/resources/src/common/dto/request/resource-application.request.dto.ts#L46) | +| E08 | Metadata, allowlist y lectores globales | [resource-reference-metadata.ts:29](../../modules/resources/src/api/infrastructure/persistence/resource-reference-metadata.ts#L29); [resource-reference-metadata.ts:89](../../modules/resources/src/api/infrastructure/persistence/resource-reference-metadata.ts#L89); [resource-reference-reader.ts:250](../../modules/resources/src/api/infrastructure/persistence/resource-reference-reader.ts#L250); [resource-reference-reader.ts:273](../../modules/resources/src/api/infrastructure/persistence/resource-reference-reader.ts#L273) | +| E09 | Scope relacional y migración explícita existente | [resource-scope-schema.ts:15](../../modules/resources/src/api/infrastructure/persistence/resource-scope-schema.ts#L15); [resource-scope-schema.ts:18](../../modules/resources/src/api/infrastructure/persistence/resource-scope-schema.ts#L18); [resource-scope-migration.ts:51](../../modules/resources/src/api/infrastructure/persistence/resource-scope-migration.ts#L51) | +| E10 | Propósitos, candidatos, administración y servicios públicos | [application-reference.dto.ts:5](../../modules/resources/src/common/dto/shared/application-reference.dto.ts#L5); [resource-reference-reader.ts:166](../../modules/resources/src/api/infrastructure/persistence/resource-reference-reader.ts#L166); [resource-reference-service.ts:33](../../modules/resources/src/api/application/resource-reference-service.ts#L33); [global-resource.routes.ts:71](../../modules/resources/src/api/infrastructure/http/global-resource.routes.ts#L71); [resource-application.routes.ts:26](../../modules/resources/src/api/infrastructure/http/resource-application.routes.ts#L26) | +| E11 | Persistencia relacional y mapper global | [resource-relational-schema.ts:107](../../modules/resources/src/api/infrastructure/persistence/resource-relational-schema.ts#L107); [resource.model.ts:19](../../modules/resources/src/api/infrastructure/persistence/resource.model.ts#L19); [resource-persistence.mapper.ts:155](../../modules/resources/src/api/infrastructure/persistence/resource-persistence.mapper.ts#L155) | +| E12 | Gates declarados, preflight y guard de administración | [resource-global-gates.ts:14](../../modules/resources/src/api/application/resource-global-gates.ts#L14); [database.ts:14](../../apps/server/src/config/database.ts#L14); [module.ts:75](../../modules/resources/src/api/module.ts#L75); [require-global-resource-manager.ts:6](../../modules/identity/src/api/infrastructure/http/require-global-resource-manager.ts#L6) | +| E13 | Producto, licencia particular, vigencia y proyección pública global | [software-installation-policy.ts:6](../../modules/companies/src/api/application/software-installation-policy.ts#L6); [software-installation-policy.ts:20](../../modules/companies/src/api/application/software-installation-policy.ts#L20); [resource-inventory.service.ts:22](../../modules/resources/src/api/application/resource-inventory.service.ts#L22); [configuration-resource-references.ts:6](../../modules/companies/src/api/infrastructure/persistence/configuration-resource-references.ts#L6) | +| E14 | Campos y estados de instalación; target inmutable y revisiones | [software-installation-values.ts:5](../../modules/companies/src/api/domain/software-installation-values.ts#L5); [software-installation.aggregate.ts:64](../../modules/companies/src/api/domain/software-installation.aggregate.ts#L64); [software-installation-values.vo.ts:56](../../modules/companies/src/api/domain/value-objects/software-installation-values.vo.ts#L56) | +| E15 | FK e índice de instalación actual; reconstrucción histórica | [software-installation-snapshot.ts:9](../../modules/companies/src/api/infrastructure/persistence/software-installation-snapshot.ts#L9); [software-installation-migration.ts:6](../../modules/companies/src/api/infrastructure/persistence/software-installation-migration.ts#L6); [software-installation-persistence.mapper.ts:43](../../modules/companies/src/api/infrastructure/persistence/software-installation-persistence.mapper.ts#L43) | +| E16 | Unidad transaccional y composición intermodular | [sequelize-software-installation.repository.ts:32](../../modules/companies/src/api/infrastructure/persistence/sequelize-software-installation.repository.ts#L32); [sequelize-software-installation.repository.ts:220](../../modules/companies/src/api/infrastructure/persistence/sequelize-software-installation.repository.ts#L220); [resource-configuration-dependencies.ts:13](../../modules/companies/src/api/infrastructure/persistence/resource-configuration-dependencies.ts#L13); [identity-runtime.ts:60](../../apps/server/src/infrastructure/http/identity-runtime.ts#L60) | +| E17 | SO derivado actual, pérdida de scope en salida y destinos de navegación | [current-operating-systems.ts:20](../../modules/companies/src/web/software-installations/shared/helpers/current-operating-systems.ts#L20); [current-operating-systems.tsx:64](../../modules/companies/src/web/software-installations/detail/ui/current-operating-systems.tsx#L64); [software-installations-panel.tsx:125](../../modules/companies/src/web/software-installations/list/ui/software-installations-panel.tsx#L125); [use-workplace-map-model.ts:36](../../modules/companies/src/web/workstations/detail/controllers/use-workplace-map-model.ts#L36); [use-asset-software.ts:11](../../modules/companies/src/web/software-installations/shared/queries/use-asset-software.ts#L11) | +| E18 | R2.6 excluye SO y no consolida recursos | [17-r2-6-coexistencia-duplicados.md:46](../../docs/architecture/resources-global-scope/17-r2-6-coexistencia-duplicados.md#L46); [application-coincidence-reader.ts:111](../../modules/resources/src/api/infrastructure/persistence/application-coincidence-reader.ts#L111) | +| E19 | Normalización de identidad y comparación como señal | [application-coincidence.ts:4](../../modules/resources/src/api/application/application-coincidence.ts#L4); [application-coincidence.ts:9](../../modules/resources/src/api/application/application-coincidence.ts#L9); [application-coincidence.test.ts:20](../../modules/resources/src/api/application/application-coincidence.test.ts#L20) | +| E20 | D08.2 sigue pendiente; estados reales de instalación sin superseded | [19-d08-2-resolucion-ratificacion.md:3](../../docs/architecture/resources-global-scope/19-d08-2-resolucion-ratificacion.md#L3); [19-d08-2-resolucion-ratificacion.md:716](../../docs/architecture/resources-global-scope/19-d08-2-resolucion-ratificacion.md#L716); [software-installation-values.ts:22](../../modules/companies/src/api/domain/software-installation-values.ts#L22) | +| E21 | Unicidad textual particular y de definiciones, no unicidad semántica global EAV | [resource.use-cases.ts:128](../../modules/resources/src/api/application/resource.use-cases.ts#L128); [resource-catalog.aggregate.ts:93](../../modules/resources/src/api/domain/resource-catalog.aggregate.ts#L93); [sequelize-global-resource.repository.ts:197](../../modules/resources/src/api/infrastructure/persistence/sequelize-global-resource.repository.ts#L197); [resource-relational-schema.ts:98](../../modules/resources/src/api/infrastructure/persistence/resource-relational-schema.ts#L98) | +| E22 | Búsqueda concreta: substring name/provider, provider opcional, sin características arbitrarias | [resource-reference-pagination.ts:13](../../modules/resources/src/api/application/resource-reference-pagination.ts#L13); [resource-application.request.dto.ts:16](../../modules/resources/src/common/dto/request/resource-application.request.dto.ts#L16); [resource-application.request.dto.ts:30](../../modules/resources/src/common/dto/request/resource-application.request.dto.ts#L30); [text-search.ts:2](../../packages/rdx-utils/src/text-search.ts#L2) | +| E23 | Formularios, selección y proyecciones públicas/administrativas | [global-application-form.tsx:64](../../modules/resources/src/web/global-applications/shared/ui/global-application-form.tsx#L64); [global-application-coincidences.tsx:12](../../modules/resources/src/web/global-applications/shared/ui/global-application-coincidences.tsx#L12); [resource-application-picker.tsx:44](../../modules/resources/src/web/application-references/select/ui/resource-application-picker.tsx#L44); [resource-characteristic-control.tsx:29](../../modules/resources/src/web/resources/shared/ui/resource-characteristic-control.tsx#L29); [global-application-detail.tsx:48](../../modules/resources/src/web/global-applications/detail/ui/global-application-detail.tsx#L48); [global-applications-keys.ts:35](../../modules/resources/src/web/global-applications/shared/queries/global-applications-keys.ts#L35) | +| E24 | Internacionalización de interfaz frente a nombres de campos; catálogo de compañía application-only | [en.json:96](../../modules/resources/src/common/locales/en.json#L96); [resource-identity-fields.tsx:36](../../modules/resources/src/web/resources/shared/ui/resource-identity-fields.tsx#L36); [resource-catalog-response.builder.ts:16](../../modules/resources/src/api/application/resource-catalog-response.builder.ts#L16); [resource-reference-reader.ts:218](../../modules/resources/src/api/infrastructure/persistence/resource-reference-reader.ts#L218); [use-company-applications-controller.ts:11](../../modules/resources/src/web/resources/list/controllers/use-company-applications-controller.ts#L11) | +| E25 | Normativa, seguridad y propiedad del SO instalado | [composicion-de-modulos.md:1](../../docs/arquitectura/composicion-de-modulos.md#L1); [03-contrato-seguridad.md:7](../../docs/architecture/resources-global-scope/03-contrato-seguridad.md#L7); [decision-so-actual-en-activos-y-puestos.md:5](../../docs/architecture/decision-so-actual-en-activos-y-puestos.md#L5); [DESIGN.md:23](../../DESIGN.md#L23); [patron-de-dialogos.md:1](../../docs/patron-de-dialogos.md#L1) | +| E26 | Pruebas existentes y límites de lo verificado | [resource-reference-contracts.test.ts:22](../../modules/resources/src/api/application/resource-reference-contracts.test.ts#L22); [global-resource.test.ts:14](../../modules/resources/src/api/domain/global-resource.test.ts#L14); [resource-catalog.test.ts:349](../../modules/resources/src/api/domain/resource-catalog.test.ts#L349); [resource-scope.test.ts:93](../../modules/resources/src/api/application/resource-scope.test.ts#L93); [global-software.integration.test.ts:363](../../apps/server/src/global-software.integration.test.ts#L363); [global-applications.integration.test.ts:71](../../apps/server/src/global-applications.integration.test.ts#L71); [application-coincidences.integration.test.ts:152](../../apps/server/src/application-coincidences.integration.test.ts#L152) | + +## 19. Validaciones ejecutadas y límites + +Se ejecutaron diez archivos existentes, con aislamiento desactivado para obtener casos individuales: **44 tests, 44 correctos, cero fallos/cancelados/omitidos**. Incluyen catálogo/plantillas SO, UUID/scope, creación global, allowlist exacta, DTO/propósitos, búsqueda/paginación/gates, coincidencias, ciclo de recursos, readers/mappers y software/licencias/perfiles. No prueban un camino de éxito de SO global; comprueban, entre otras cosas, su exclusión actual. + +```bash +node --import tsx --test --test-isolation=none \ + modules/resources/src/api/domain/resource-catalog.test.ts \ + modules/resources/src/api/domain/resource-scope.test.ts \ + modules/resources/src/api/domain/global-resource.test.ts \ + modules/resources/src/api/application/resource-reference-contracts.test.ts \ + modules/resources/src/api/application/resource-scope.test.ts \ + modules/resources/src/api/application/application-coincidence.test.ts \ + modules/resources/src/api/application/resource-lifecycle.test.ts \ + modules/resources/src/api/infrastructure/persistence/resource-scope-reader.test.ts \ + modules/companies/src/api/domain/software-installation.test.ts \ + modules/companies/src/api/domain/application-configuration.test.ts +``` + +El primer intento bajo sandbox obtuvo 43/44: el caso HTTP de `resource-scope.test.ts` falló con `listen EPERM` al abrir loopback. Se repitió el mismo comando con autorización de ejecución para ese servidor temporal y pasaron 44/44. No se corrigió código ni se desactivó el caso. Su fixture HTTP usa repositorios en memoria con 15 compañías y 50 recursos locales; **no es MariaDB** ni una instalación poblada con 50 personas/activos. Los tests de readers/persistencia usan dobles o mappers, no certifican FKs/locks reales. + +Se inspeccionaron además integraciones existentes de global applications, software, coincidencias y autorización, incluidas sus negativas para SO y carreras. **No se ejecutaron SQL/MariaDB, migraciones, fixtures persistentes ni navegador** en esta auditoría. Tampoco se accedió al catálogo de un entorno real ni se verificaron sus flags efectivos. Los resultados anteriores de R2.4–R2.6 o del resumen SO no se cuentan como pruebas nuevas. + +No se ejecutan typecheck/lint/build globales ni una suite general que pudiera usar configuración habitual: la única escritura propia es este documento Markdown. Se comprueban referencias locales, símbolos/líneas y whitespace del informe; los diagramas Mermaid se entregan como fuente, sin renderizado validado. El diff del trabajo concurrente se conserva y no se atribuye a esta auditoría. + +Skills consultadas: `rdx-solid` —referencias de arquitectura/decisiones ya consultadas en esta sesión— para separar dato, identidad, contratos e inferencias; `ux-search` para búsqueda de producto conocido, etiquetas, filtros y recuperación sin sobrecargar la UI. Se contrastan APIs locales de shadcn/RDX, DESIGN y patrón de diálogos; no se generan componentes ni se requiere CLI/descarga de vendor. Los auditores `ux-*-audit` citados por ux-search no están disponibles y no se invocan ni instalan; la revisión UX es estática y no se presenta como auditoría visual o de accesibilidad completa. + +**Entrega: este informe. No se modifica Resources, SoftwareInstallation, Assets, Workstation, esquema, gates ni datos maestros. La recomendación necesita aprobación antes de cualquier implementación.** diff --git a/docs/auditoria-ayuda-contextual-dialogos.md b/docs/auditoria-ayuda-contextual-dialogos.md new file mode 100644 index 00000000..8707e3b3 --- /dev/null +++ b/docs/auditoria-ayuda-contextual-dialogos.md @@ -0,0 +1,313 @@ +# Auditoría de ayuda contextual en diálogos + +Fecha: 25 de octubre de 2026. Alcance: código frontend activo de `modules/*/src/web` y componentes propios de `packages/rdx-ui/src`. Se excluyen vendor, archive y fixtures de navegador. + +## Resultado e infraestructura + +Se han auditado **95 componentes modales de módulos y 7 constructores/adaptadores compartidos** (102 unidades). El recuento es por componente, no por variantes de alta/edición ni por diálogos anidados. **59 componentes de módulo contienen ayuda: 58 adoptan el patrón bajo demanda y 1 conserva su consulta de ayuda dedicada**. Además, el constructor compartido `FieldPickerDialog` migra su ayuda breve y cubre los dos adaptadores de catálogo ya contabilizados. Las demás 36 unidades de módulos y 6 compartidas no tienen ayuda complementaria propia. + +La búsqueda incluye FormDialog, DialogContent, ConfirmDeleteDialog, AlertDialog, Sheet, Drawer, formularios, wizards, pickers, bloques de información y claves de traducción. Se contrastó el inventario previo de modales con las fuentes actuales; los nombres de fichero no determinaron el alcance. Se siguieron imports y reexports de componentes para localizar ayuda en formularios descendientes, deteniendo el recorrido en diálogos anidados. Las variantes suministradas mediante callbacks se revisaron en sus consumidores. + +Encontrados: `FormDialog`, `FormBody`, `FormActions`, `ContextHelp`, `FormHelp`, `PickerHelp`, `EntityHelp`, `CatalogHelp`, `SupportHelp` y `ProcessHelp`. Las primitivas instaladas son shadcn Base UI 1.8.0, estilo Vega. No existe un segundo sistema Radix que deba integrarse ni se ha modificado vendor. + +## Decisiones + +- Se amplía FormDialog mediante contexto y slots DOM. DialogHelp declara contenido traducido o React en cualquier descendiente. Los portales sitúan el botón en la cabecera y el contenido en el mismo diálogo, conservando sus contextos. No se registra contenido React mediante efectos ni se replica en estado. +- ContextHelp y FormHelp aprovechan el slot cuando están dentro de FormDialog; fuera conservan el contenido visible. Así se migran todos sus consumidores sin elevar traducciones ni duplicar configuraciones en cada editor/alta rápida. PickerHelp declara compact explícitamente. +- Auto significa ayuda estructurada (descripción, ámbito, ejemplos y orientación), equivalente a expanded. Compact se declara para una explicación breve; no se mide texto del DOM. +- ResizeObserver mide la superficie efectiva. Compact usa Popover desde 30rem; auto/expanded usan panel desde 60rem (40rem de formulario + 20rem de ayuda). En menor capacidad se usa vista interna. Las comparaciones usan el tamaño raíz de fuente; no dependen solo del viewport. Un diálogo pequeño en escritorio también utiliza vista interna. +- El formulario sigue montado en vista interna, con visibility:hidden, inert y aria-hidden; mantiene su geometría y sus datos. La ayuda ocupa esa misma región. Sus acciones no están disponibles mientras se consulta la vista interna. El panel no ensancha el diálogo ni crea otra capa modal. +- Escape se intercepta en el contrato onOpenChange de Base UI mediante reason=escape-key y cancel(); cierra la ayuda antes que el diálogo. X conserva el cierre de Base UI. El cierre de ayuda devuelve foco al botón; al abrir o cambiar de modalidad se enfoca el encabezado de ayuda. +- Las etiquetas genéricas se traducen en apps/web a través de core.dialogHelp y se suministran a rdx-ui, que permanece independiente de i18next y del dominio. + +## Criterios y excepciones + +Se migran exclusivamente bloques complementarios de ContextHelp/FormHelp y la ayuda general de PickerHelp. Se conserva íntegramente descripción, ámbito, listas plegables, ejemplos, orientación e iconos personalizados. No se modifica ninguna traducción de ayuda previa ni contratos HTTP, modelos o persistencia. + +| Categoría | Decisión y ejemplos | +| --- | --- | +| Ayuda general complementaria | Bajo demanda: ContextHelp/FormHelp, EntityHelp, CatalogHelp, ProcessHelp y SupportHelp. | +| Instrucciones de campo | Visibles: choicesHelp, addressHelp, instrucciones de plantilla/resolución, licencia, límites de nombre, fecha de alta y estado técnico, carga de imágenes y formatos. | +| Consecuencias y seguridad | Visibles: desactivación de personas/sedes/departamentos, retirada de identidad local, avisos de alta global/local y datos creados aunque se cancele la relación. | +| Validación y transporte | Visibles: InventoryFeedback, CompanyFeedback, errores de campo, concurrencia, permisos, reintento y fallos de guardado/refresco. | +| Confirmaciones destructivas | Se conserva descripción/confirmación visible en ConfirmDeleteDialog y editores de cierre. | +| Vacío y orientación operativa | Se conservan EmptyState, selección/destino, contadores y restricciones de selección. | +| Consulta dedicada de ayuda | AccessHelpDialog mantiene EntityHelp placement=inline visible: ocultarlo tras otro ? dejaría vacía su tarea principal. Es la única excepción a la migración de bloques generales. | + +Los títulos y descripciones de los diálogos de consulta, historiales, selección de compañía y software del puesto son contenido funcional. El texto requiredColumnHelp de configuración de columnas explica una restricción activa y continúa visible. No se añade ? por el mero hecho de contener texto informativo. + +## Inventario final + +“Migrado” acredita integración en el sistema compartido y revisión estática; la validación interactiva representativa se detalla aparte y no implica haber abierto todas las variantes. Las columnas de contenido apuntan a las declaraciones originales con sus claves conservadas. + +### assets + +| Componente / archivo | Composición | Ayuda y ubicación anterior | Longitud y propuesta | Particularidades / estado | +| --- | --- | --- | --- | --- | +| [EndRelationshipDialog](../modules/assets/src/web/asset-relationships/end/ui/end-relationship-dialog.tsx) | FormDialog | Sin ayuda complementaria propia | — | acciones de formulario · Auditado · conserva contenido funcional | +| [RelationshipEditorDialog](../modules/assets/src/web/asset-relationships/shared/ui/relationship-editor-dialog.tsx) | FormDialog | [ContextHelp](../modules/assets/src/web/asset-relationships/shared/ui/relationship-form.tsx) · bloque en cuerpo o antes de acciones | Extensa · panel / vista | acciones de formulario · Migrado · contenido conservado | +| [WorkstationAssetCreateDialog](../modules/assets/src/web/assets/create/ui/workstation-asset-create-dialog.tsx) | FormDialog | [ContextHelp](../modules/assets/src/web/assets/shared/ui/asset-form.tsx) · bloque en cuerpo o antes de acciones | Extensa · panel / vista | acciones de formulario, tamaño variable/amplio · Migrado · contenido conservado | +| [AssetDetailDialog](../modules/assets/src/web/assets/detail/ui/asset-detail-dialog.tsx) | FormDialog | Sin ayuda complementaria propia | — | altura estable, tamaño variable/amplio · Auditado · conserva contenido funcional | +| [AssetPickerDialog](../modules/assets/src/web/assets/select/ui/asset-picker-dialog.tsx) | FormDialog | Sin ayuda complementaria propia | — | altura estable, acciones de formulario, tamaño variable/amplio · Auditado · conserva contenido funcional | +| [AssetEditorDialog](../modules/assets/src/web/assets/shared/ui/asset-editor-dialog.tsx) | FormDialog | [ContextHelp](../modules/assets/src/web/assets/shared/ui/asset-form.tsx) · bloque en cuerpo o antes de acciones | Extensa · panel / vista | altura estable, acciones de formulario · Migrado · contenido conservado | +| [DeleteAssetDialog](../modules/assets/src/web/assets/shared/ui/delete-asset-dialog.tsx) | ConfirmDeleteDialog | Sin ayuda complementaria propia | — | Auditado · conserva contenido funcional | +| [DeleteBrandDialog](../modules/assets/src/web/brands/delete/ui/delete-brand-dialog.tsx) | FormDialog | Sin ayuda complementaria propia | — | acciones de formulario · Auditado · conserva contenido funcional | +| [BrandEditorDialog](../modules/assets/src/web/brands/shared/ui/brand-editor-dialog.tsx) | FormDialog | [ContextHelp](../modules/assets/src/web/brands/shared/ui/brand-form.tsx) · bloque en cuerpo o antes de acciones | Extensa · panel / vista | acciones de formulario · Migrado · contenido conservado | +| [DeleteCatalogDefinitionDialog](../modules/assets/src/web/catalog/delete/ui/delete-catalog-definition-dialog.tsx) | FormDialog | Sin ayuda complementaria propia | — | acciones de formulario · Auditado · conserva contenido funcional | +| [CatalogDefinitionDialog](../modules/assets/src/web/catalog/detail/ui/catalog-definition-dialog.tsx) | FormDialog | [ContextHelp](../modules/assets/src/web/catalog/shared/ui/catalog-help.tsx) · bloque en cuerpo o antes de acciones | Extensa · panel / vista | tamaño variable/amplio · Migrado · contenido conservado | +| [CatalogEditorDialog](../modules/assets/src/web/catalog/shared/ui/catalog-editor-dialog.tsx) | FormDialog | [ContextHelp](../modules/assets/src/web/catalog/shared/ui/catalog-help.tsx) · bloque en cuerpo o antes de acciones | Extensa · panel / vista | acciones de formulario, tamaño variable/amplio · Migrado · contenido conservado | +| [CatalogFieldCreateDialog](../modules/assets/src/web/catalog-fields/create/ui/catalog-field-create-dialog.tsx) | FormDialog | [ContextHelp](../modules/assets/src/web/catalog/shared/ui/catalog-help.tsx) · bloque en cuerpo o antes de acciones | Extensa · panel / vista | acciones de formulario · Migrado · contenido conservado | +| [CatalogFieldPickerDialog](../modules/assets/src/web/catalog-fields/select/ui/catalog-field-picker-dialog.tsx) | FieldPickerDialog | [PickerHelp](../packages/rdx-ui/src/components/field-picker-dialog.tsx) · bloque en cuerpo o antes de acciones | Breve · Popover / vista | Migrado · contenido conservado | +| [CatalogRelationCreateDialog](../modules/assets/src/web/catalog-relations/create/ui/catalog-relation-create-dialog.tsx) | FormDialog | [ContextHelp](../modules/assets/src/web/catalog/shared/ui/catalog-help.tsx) · bloque en cuerpo o antes de acciones | Extensa · panel / vista | acciones de formulario · Migrado · contenido conservado | +| [CatalogTemplateCreateDialog](../modules/assets/src/web/catalog-templates/create/ui/catalog-template-create-dialog.tsx) | FormDialog | [ContextHelp](../modules/assets/src/web/catalog/shared/ui/catalog-help.tsx) · bloque en cuerpo o antes de acciones | Extensa · panel / vista | acciones de formulario, tamaño variable/amplio · Migrado · contenido conservado | +| [DirectoryMembershipEditor](../modules/assets/src/web/directory-memberships/manage/ui/directory-membership-editor.tsx) | FormDialog | [FormHelp](../modules/assets/src/web/directory-memberships/manage/ui/directory-membership-editor.tsx) · bloque en cuerpo o antes de acciones | Extensa · panel / vista | altura estable, acciones de formulario · Migrado · contenido conservado | +| [EndDirectoryMembershipDialog](../modules/assets/src/web/directory-memberships/manage/ui/end-directory-membership-dialog.tsx) | ConfirmDeleteDialog | Sin ayuda complementaria propia | — | altura estable · Auditado · conserva contenido funcional | +| [DirectoryDomainPicker](../modules/assets/src/web/directory-memberships/select/ui/directory-domain-picker.tsx) | FormDialog | Sin ayuda complementaria propia | — | altura estable · Auditado · conserva contenido funcional | +| [InfrastructureEditorDialog](../modules/assets/src/web/infrastructure/shared/ui/infrastructure-editor-dialog.tsx) | FormDialog | [FormHelp](../modules/assets/src/web/infrastructure/shared/ui/infrastructure-editor-dialog.tsx) · bloque en cuerpo o antes de acciones | Extensa · panel / vista | altura estable, acciones de formulario, tamaño variable/amplio · Migrado · contenido conservado | +| [PublicationEditor](../modules/assets/src/web/network-share-publications/manage/ui/publication-editor.tsx) | FormDialog | [FormHelp](../modules/assets/src/web/network-share-publications/manage/ui/publication-editor.tsx) · bloque en cuerpo o antes de acciones | Extensa · panel / vista | altura estable, acciones de formulario · Migrado · contenido conservado | +| [DeleteRecordDialog](../modules/assets/src/web/shared/ui/delete-record-dialog.tsx) | ConfirmDeleteDialog | Sin ayuda complementaria propia | — | Auditado · conserva contenido funcional | +| [SupportHistoryDialog](../modules/assets/src/web/support-connections/history/ui/support-history-dialog.tsx) | FormDialog | Sin ayuda complementaria propia | — | Auditado · conserva contenido funcional | +| [AssetSupportManagementDialog](../modules/assets/src/web/support-connections/list/ui/asset-support-management-dialog.tsx) | FormDialog | Sin ayuda complementaria propia | — | altura estable · Auditado · conserva contenido funcional | +| [SupportConnectionEditorDialog](../modules/assets/src/web/support-connections/shared/ui/support-connection-editor-dialog.tsx) | FormDialog | [ContextHelp](../modules/assets/src/web/support-connections/shared/ui/support-help.tsx) · bloque en cuerpo o antes de acciones | Extensa · panel / vista | acciones de formulario · Migrado · contenido conservado | +| [SupportResourceCreateDialog](../modules/assets/src/web/support-connections/shared/ui/support-resource-create-dialog.tsx) | FormDialog | [ContextHelp](../modules/resources/src/web/resources/shared/ui/resource-form.tsx) · bloque en cuerpo o antes de acciones | Extensa · panel / vista | acciones de formulario, tamaño variable/amplio · Migrado · contenido conservado | +| [WorkstationAssetAddDialog](../modules/assets/src/web/workstations/associate-assets/ui/workstation-asset-add-dialog.tsx) | FormDialog | [PickerHelp](../modules/assets/src/web/workstations/associate-assets/ui/workstation-asset-add-dialog.tsx) · bloque en cuerpo o antes de acciones | Breve · Popover / vista | altura estable, acciones de formulario, tamaño variable/amplio · Migrado · contenido conservado | +| [WorkstationHistoryDialog](../modules/assets/src/web/workstations/history/ui/workstation-history-dialog.tsx) | FormDialog | Sin ayuda complementaria propia | — | altura estable, tamaño variable/amplio · Auditado · conserva contenido funcional | +| [WorkstationEditorDialog](../modules/assets/src/web/workstations/shared/ui/workstation-editor-dialog.tsx) | FormDialog | [FormHelp](../modules/assets/src/web/workstations/shared/ui/workstation-form.tsx) · bloque en cuerpo o antes de acciones | Extensa · panel / vista | altura estable, acciones de formulario · Migrado · contenido conservado | +| [WorkstationLifecycleDialog](../modules/assets/src/web/workstations/shared/ui/workstation-lifecycle-dialog.tsx) | ConfirmDeleteDialog | Sin ayuda complementaria propia | — | Auditado · conserva contenido funcional | + +### companies + +| Componente / archivo | Composición | Ayuda y ubicación anterior | Longitud y propuesta | Particularidades / estado | +| --- | --- | --- | --- | --- | +| [AccessQuickEditor](../modules/companies/src/web/access/create/ui/access-quick-editor.tsx) | FormDialog | [EntityHelp](../modules/companies/src/web/access/help/ui/entity-help.tsx) · bloque en cuerpo o antes de acciones | Extensa · panel / vista | acciones de formulario, tamaño variable/amplio · Migrado · contenido conservado | +| [QuickCreateDialog](../modules/companies/src/web/access/create/ui/quick-create-dialog.tsx) | FormDialog | [EntityHelp](../modules/companies/src/web/access/help/ui/entity-help.tsx) · bloque en cuerpo o antes de acciones | Extensa · panel / vista | acciones de formulario · Migrado · contenido conservado | +| [AccessHelpDialog](../modules/companies/src/web/access/help/ui/access-help-dialog.tsx) | FormDialog | [EntityHelp](../modules/companies/src/web/access/help/ui/entity-help.tsx) · bloque en cuerpo o antes de acciones | Contenido principal visible | Consulta dedicada visible · excepción justificada | +| [AccessHistory](../modules/companies/src/web/access/history/ui/access-history.tsx) | FormDialog | Sin ayuda complementaria propia | — | Auditado · conserva contenido funcional | +| [AccessEditorDialog](../modules/companies/src/web/access/shared/ui/access-editor-dialog.tsx) | FormDialog | [ContextHelp](../modules/resources/src/web/resources/shared/ui/resource-form.tsx); [EntityHelp](../modules/companies/src/web/access/help/ui/entity-help.tsx) · bloque en cuerpo o antes de acciones | Extensa · panel / vista | altura estable, acciones de formulario, tamaño variable/amplio · Migrado · contenido conservado | +| [ApplicationQuickEditor](../modules/companies/src/web/application-configurations/create/ui/application-quick-editor.tsx) | FormDialog | [FormHelp](../modules/companies/src/web/work-profiles/shared/ui/work-profile-form.tsx); [ContextHelp](../modules/resources/src/web/resources/shared/ui/resource-form.tsx) · bloque en cuerpo o antes de acciones | Extensa · panel / vista | acciones de formulario, tamaño variable/amplio · Migrado · contenido conservado | +| [ApplicationHistoryDialog](../modules/companies/src/web/application-configurations/history/ui/application-history-dialog.tsx) | FormDialog | Sin ayuda complementaria propia | — | tamaño variable/amplio · Auditado · conserva contenido funcional | +| [ApplicationConfigurationEditorDialog](../modules/companies/src/web/application-configurations/shared/ui/application-configuration-editor-dialog.tsx) | FormDialog | [ContextHelp](../modules/companies/src/web/application-configurations/shared/ui/application-configuration-form.tsx) · bloque en cuerpo o antes de acciones | Extensa · panel / vista | altura estable, acciones de formulario · Migrado · contenido conservado | +| [AssignmentHistoryDialog](../modules/companies/src/web/assignments/history/ui/assignment-history-dialog.tsx) | FormDialog | Sin ayuda complementaria propia | — | Auditado · conserva contenido funcional | +| [AssignmentEditorDialog](../modules/companies/src/web/assignments/shared/ui/assignment-editor-dialog.tsx) | FormDialog | [EntityHelp](../modules/companies/src/web/access/help/ui/entity-help.tsx) · bloque en cuerpo o antes de acciones | Extensa · panel / vista | acciones de formulario · Migrado · contenido conservado | +| [CertificateQuickEditor](../modules/companies/src/web/certificate-configurations/create/ui/certificate-quick-editor.tsx) | FormDialog | [FormHelp](../modules/companies/src/web/work-profiles/shared/ui/work-profile-form.tsx); [ContextHelp](../modules/resources/src/web/resources/shared/ui/resource-form.tsx) · bloque en cuerpo o antes de acciones | Extensa · panel / vista | acciones de formulario, tamaño variable/amplio · Migrado · contenido conservado | +| [CertificateHistoryDialog](../modules/companies/src/web/certificate-configurations/history/ui/certificate-history-dialog.tsx) | FormDialog | Sin ayuda complementaria propia | — | tamaño variable/amplio · Auditado · conserva contenido funcional | +| [CertificateConfigurationEditorDialog](../modules/companies/src/web/certificate-configurations/shared/ui/certificate-configuration-editor-dialog.tsx) | FormDialog | [ContextHelp](../modules/companies/src/web/certificate-configurations/shared/ui/certificate-configuration-form.tsx) · bloque en cuerpo o antes de acciones | Extensa · panel / vista | acciones de formulario, tamaño variable/amplio · Migrado · contenido conservado | +| [InventoryQuickCreateDialog](../modules/companies/src/web/companies/detail/ui/inventory-quick-create-dialog.tsx) | FormDialog | [EntityHelp](../modules/companies/src/web/access/help/ui/entity-help.tsx) · bloque en cuerpo o antes de acciones | Extensa · panel / vista | acciones de formulario · Migrado · contenido conservado | +| [CompanyEditorDialog](../modules/companies/src/web/companies/shared/ui/company-editor-dialog.tsx) | FormDialog | [EntityHelp](../modules/companies/src/web/access/help/ui/entity-help.tsx) · bloque en cuerpo o antes de acciones | Extensa · panel / vista | acciones de formulario · Migrado · contenido conservado | +| [DashboardQuickCreate](../modules/companies/src/web/dashboard/create/ui/dashboard-quick-create.tsx) | FormDialog | [EntityHelp](../modules/companies/src/web/access/help/ui/entity-help.tsx) · bloque en cuerpo o antes de acciones | Extensa · panel / vista | acciones de formulario · Migrado · contenido conservado | +| [DashboardDetailsDialog](../modules/companies/src/web/dashboard/detail/ui/dashboard-details-dialog.tsx) | FormDialog | Sin ayuda complementaria propia | — | altura estable, tamaño variable/amplio · Auditado · conserva contenido funcional | +| [DepartmentEditorDialog](../modules/companies/src/web/departments/shared/ui/department-editor-dialog.tsx) | FormDialog | [EntityHelp](../modules/companies/src/web/access/help/ui/entity-help.tsx) · bloque en cuerpo o antes de acciones | Extensa · panel / vista | acciones de formulario · Migrado · contenido conservado | +| [FolderQuickEditor](../modules/companies/src/web/folder-mappings/create/ui/folder-quick-editor.tsx) | FormDialog | [FormHelp](../modules/companies/src/web/work-profiles/shared/ui/work-profile-form.tsx); [ContextHelp](../modules/resources/src/web/resources/shared/ui/resource-form.tsx); [EntityHelp](../modules/companies/src/web/access/help/ui/entity-help.tsx) · bloque en cuerpo o antes de acciones | Extensa · panel / vista | acciones de formulario, tamaño variable/amplio · Migrado · contenido conservado | +| [FolderMappingHistoryDialog](../modules/companies/src/web/folder-mappings/history/ui/folder-mapping-history-dialog.tsx) | FormDialog | Sin ayuda complementaria propia | — | altura estable, tamaño variable/amplio · Auditado · conserva contenido funcional | +| [FolderMappingEditorDialog](../modules/companies/src/web/folder-mappings/shared/ui/folder-mapping-editor-dialog.tsx) | FormDialog | [FormHelp](../modules/companies/src/web/folder-mappings/shared/ui/folder-mapping-form.tsx) · bloque en cuerpo o antes de acciones | Extensa · panel / vista | altura estable, acciones de formulario, tamaño variable/amplio · Migrado · contenido conservado | +| [LocalIdentityEditor](../modules/companies/src/web/local-identity-environments/manage/ui/local-identity-editor.tsx) | FormDialog | [FormHelp](../modules/companies/src/web/local-identity-environments/manage/ui/local-identity-editor.tsx) · bloque en cuerpo o antes de acciones | Extensa · panel / vista | altura estable, acciones de formulario · Migrado · contenido conservado | +| [MailQuickEditor](../modules/companies/src/web/mail-configurations/create/ui/mail-quick-editor.tsx) | FormDialog | [ContextHelp](../modules/resources/src/web/resources/shared/ui/resource-form.tsx); [FormHelp](../modules/companies/src/web/work-profiles/shared/ui/work-profile-form.tsx); [ContextHelp](../modules/companies/src/web/mailbox-uses/shared/ui/mailbox-use-form.tsx) · bloque en cuerpo o antes de acciones | Extensa · panel / vista | acciones de formulario, tamaño variable/amplio · Migrado · contenido conservado | +| [MailConfigurationHistoryDialog](../modules/companies/src/web/mail-configurations/history/ui/mail-configuration-history-dialog.tsx) | FormDialog | Sin ayuda complementaria propia | — | tamaño variable/amplio · Auditado · conserva contenido funcional | +| [MailConfigurationEditorDialog](../modules/companies/src/web/mail-configurations/shared/ui/mail-configuration-editor-dialog.tsx) | FormDialog | [ContextHelp](../modules/companies/src/web/mail-configurations/shared/ui/mail-configuration-form.tsx) · bloque en cuerpo o antes de acciones | Extensa · panel / vista | acciones de formulario, tamaño variable/amplio · Migrado · contenido conservado | +| [MailboxUseEditorDialog](../modules/companies/src/web/mailbox-uses/shared/ui/mailbox-use-editor-dialog.tsx) | FormDialog | [ContextHelp](../modules/companies/src/web/mailbox-uses/shared/ui/mailbox-use-form.tsx) · bloque en cuerpo o antes de acciones | Extensa · panel / vista | acciones de formulario · Migrado · contenido conservado | +| [MailboxCreateDialog](../modules/companies/src/web/mailboxes/create/ui/mailbox-create-dialog.tsx) | FormDialog | [ContextHelp](../modules/resources/src/web/resources/shared/ui/resource-form.tsx) · bloque en cuerpo o antes de acciones | Extensa · panel / vista | acciones de formulario · Migrado · contenido conservado | +| [PersonEditorDialog](../modules/companies/src/web/people/shared/ui/person-editor-dialog.tsx) | FormDialog | [EntityHelp](../modules/companies/src/web/access/help/ui/entity-help.tsx) · bloque en cuerpo o antes de acciones | Extensa · panel / vista | acciones de formulario · Migrado · contenido conservado | +| [PrinterProfileCreateDialog](../modules/companies/src/web/printer-configurations/create/ui/printer-profile-create-dialog.tsx) | FormDialog | [FormHelp](../modules/companies/src/web/work-profiles/shared/ui/work-profile-form.tsx) · bloque en cuerpo o antes de acciones | Extensa · panel / vista | acciones de formulario, tamaño variable/amplio · Migrado · contenido conservado | +| [PrinterHistoryDialog](../modules/companies/src/web/printer-configurations/history/ui/printer-history-dialog.tsx) | FormDialog | Sin ayuda complementaria propia | — | tamaño variable/amplio · Auditado · conserva contenido funcional | +| [PrinterConfigurationEditorDialog](../modules/companies/src/web/printer-configurations/shared/ui/printer-configuration-editor-dialog.tsx) | FormDialog | [ContextHelp](../modules/companies/src/web/printer-configurations/shared/ui/printer-configuration-form.tsx) · bloque en cuerpo o antes de acciones | Extensa · panel / vista | acciones de formulario, tamaño variable/amplio · Migrado · contenido conservado | +| [ProcessTemplateEditorDialog](../modules/companies/src/web/process-templates/shared/ui/process-template-editor-dialog.tsx) | FormDialog | [ContextHelp](../modules/companies/src/web/processes/shared/ui/process-help.tsx) · bloque en cuerpo o antes de acciones | Extensa · panel / vista | acciones de formulario, tamaño variable/amplio · Migrado · contenido conservado | +| [ProcessQuickCreateDialog](../modules/companies/src/web/processes/create/ui/process-quick-create-dialog.tsx) | FormDialog | [EntityHelp](../modules/companies/src/web/access/help/ui/entity-help.tsx); [ContextHelp](../modules/companies/src/web/processes/shared/ui/process-help.tsx) · bloque en cuerpo o antes de acciones | Extensa · panel / vista | acciones de formulario, tamaño variable/amplio · Migrado · contenido conservado | +| [ProcessStartDialog](../modules/companies/src/web/processes/start/ui/process-start-dialog.tsx) | FormDialog | [ContextHelp](../modules/companies/src/web/processes/shared/ui/process-help.tsx) · bloque en cuerpo o antes de acciones | Extensa · panel / vista | acciones de formulario, tamaño variable/amplio · Migrado · contenido conservado | +| [ProcessEditDialog](../modules/companies/src/web/processes/update/ui/process-edit-dialog.tsx) | FormDialog | [ContextHelp](../modules/companies/src/web/processes/shared/ui/process-help.tsx) · bloque en cuerpo o antes de acciones | Extensa · panel / vista | acciones de formulario, tamaño variable/amplio · Migrado · contenido conservado | +| [RemoteQuickEditor](../modules/companies/src/web/remote-connections/create/ui/remote-quick-editor.tsx) | FormDialog | [EntityHelp](../modules/companies/src/web/access/help/ui/entity-help.tsx); [ContextHelp](../modules/resources/src/web/resources/shared/ui/resource-form.tsx) · bloque en cuerpo o antes de acciones | Extensa · panel / vista | acciones de formulario, tamaño variable/amplio · Migrado · contenido conservado | +| [RemoteConnectionHistoryDialog](../modules/companies/src/web/remote-connections/history/ui/remote-connection-history-dialog.tsx) | FormDialog | Sin ayuda complementaria propia | — | tamaño variable/amplio · Auditado · conserva contenido funcional | +| [RemoteConnectionEditorDialog](../modules/companies/src/web/remote-connections/shared/ui/remote-connection-editor-dialog.tsx) | FormDialog | [ContextHelp](../modules/companies/src/web/remote-connections/shared/ui/remote-connection-form.tsx) · bloque en cuerpo o antes de acciones | Extensa · panel / vista | acciones de formulario, tamaño variable/amplio · Migrado · contenido conservado | +| [ConfigurationQuickEditor](../modules/companies/src/web/shared/ui/configuration-quick-editor.tsx) | FormDialog | [EntityHelp](../modules/companies/src/web/access/help/ui/entity-help.tsx); [ContextHelp](../modules/resources/src/web/resources/shared/ui/resource-form.tsx); [ContextHelp](../modules/assets/src/web/assets/shared/ui/asset-form.tsx) · bloque en cuerpo o antes de acciones | Extensa · panel / vista | acciones de formulario, tamaño variable/amplio · Migrado · contenido conservado | +| [DeleteRecordDialog](../modules/companies/src/web/shared/ui/delete-record-dialog.tsx) | ConfirmDeleteDialog | Sin ayuda complementaria propia | — | Auditado · conserva contenido funcional | +| [SiteDeleteDialog](../modules/companies/src/web/sites/delete/ui/site-delete-dialog.tsx) | ConfirmDeleteDialog | Sin ayuda complementaria propia | — | Auditado · conserva contenido funcional | +| [SiteEditorDialog](../modules/companies/src/web/sites/shared/ui/site-editor-dialog.tsx) | FormDialog | [EntityHelp](../modules/companies/src/web/access/help/ui/entity-help.tsx) · bloque en cuerpo o antes de acciones | Extensa · panel / vista | acciones de formulario · Migrado · contenido conservado | +| [SoftwareResourceCreateDialog](../modules/companies/src/web/software-installations/create/ui/software-resource-create-dialog.tsx) | FormDialog | [ContextHelp](../modules/resources/src/web/resources/shared/ui/resource-form.tsx) · bloque en cuerpo o antes de acciones | Extensa · panel / vista | acciones de formulario, tamaño variable/amplio · Migrado · contenido conservado | +| [SoftwareHistoryDialog](../modules/companies/src/web/software-installations/history/ui/software-history-dialog.tsx) | FormDialog | Sin ayuda complementaria propia | — | tamaño variable/amplio · Auditado · conserva contenido funcional | +| [SoftwareInstallationEditorDialog](../modules/companies/src/web/software-installations/shared/ui/software-installation-editor-dialog.tsx) | FormDialog | [ContextHelp](../modules/companies/src/web/software-installations/shared/ui/software-installation-form.tsx) · bloque en cuerpo o antes de acciones | Extensa · panel / vista | altura estable, acciones de formulario · Migrado · contenido conservado | +| [TeleworkAuthorizationHistoryDialog](../modules/companies/src/web/telework-authorizations/history/ui/telework-authorization-history-dialog.tsx) | FormDialog | Sin ayuda complementaria propia | — | Auditado · conserva contenido funcional | +| [TeleworkAuthorizationEditorDialog](../modules/companies/src/web/telework-authorizations/shared/ui/telework-authorization-editor-dialog.tsx) | FormDialog | [ContextHelp](../modules/companies/src/web/telework-authorizations/shared/ui/telework-authorization-form.tsx) · bloque en cuerpo o antes de acciones | Extensa · panel / vista | acciones de formulario · Migrado · contenido conservado | +| [WorkProfileHistoryDialog](../modules/companies/src/web/work-profiles/history/ui/work-profile-history-dialog.tsx) | FormDialog | Sin ayuda complementaria propia | — | tamaño variable/amplio · Auditado · conserva contenido funcional | +| [WorkProfileEditorDialog](../modules/companies/src/web/work-profiles/shared/ui/work-profile-editor-dialog.tsx) | FormDialog | [FormHelp](../modules/companies/src/web/work-profiles/shared/ui/work-profile-form.tsx) · bloque en cuerpo o antes de acciones | Extensa · panel / vista | altura estable, acciones de formulario · Migrado · contenido conservado | +| [WorkProfileLifecycleDialog](../modules/companies/src/web/work-profiles/shared/ui/work-profile-lifecycle-dialog.tsx) | ConfirmDeleteDialog | Sin ayuda complementaria propia | — | Auditado · conserva contenido funcional | +| [WorkspaceSettingsDialog](../modules/companies/src/web/workspace/settings/ui/workspace-settings-dialog.tsx) | FormDialog | Sin ayuda complementaria propia | — | Auditado · conserva contenido funcional | +| [WorkspaceMobileSheet](../modules/companies/src/web/workspace/shared/ui/workspace-mobile-sheet.tsx) | SheetContent | Sin ayuda complementaria propia | — | Auditado · conserva contenido funcional | +| [WorkstationSoftwareDialog](../modules/companies/src/web/workstations/detail/ui/workstation-software-dialog.tsx) | FormDialog | Sin ayuda complementaria propia | — | altura estable, tamaño variable/amplio · Auditado · conserva contenido funcional | + +### resources + +| Componente / archivo | Composición | Ayuda y ubicación anterior | Longitud y propuesta | Particularidades / estado | +| --- | --- | --- | --- | --- | +| [ApplicationCoincidencesDialog](../modules/resources/src/web/application-coincidences/review/ui/application-coincidences-dialog.tsx) | FormDialog | Sin ayuda complementaria propia | — | altura estable · Auditado · conserva contenido funcional | +| [CatalogDefinitionDialog](../modules/resources/src/web/catalog/detail/ui/catalog-definition-dialog.tsx) | FormDialog | [ContextHelp](../modules/resources/src/web/catalog/shared/ui/catalog-help.tsx) · bloque en cuerpo o antes de acciones | Extensa · panel / vista | tamaño variable/amplio · Migrado · contenido conservado | +| [CatalogEditorDialog](../modules/resources/src/web/catalog/shared/ui/catalog-editor-dialog.tsx) | FormDialog | [ContextHelp](../modules/resources/src/web/catalog/shared/ui/catalog-help.tsx) · bloque en cuerpo o antes de acciones | Extensa · panel / vista | acciones de formulario, tamaño variable/amplio · Migrado · contenido conservado | +| [CatalogFieldCreateDialog](../modules/resources/src/web/catalog-fields/create/ui/catalog-field-create-dialog.tsx) | FormDialog | [ContextHelp](../modules/resources/src/web/catalog/shared/ui/catalog-help.tsx) · bloque en cuerpo o antes de acciones | Extensa · panel / vista | acciones de formulario · Migrado · contenido conservado | +| [CatalogFieldPickerDialog](../modules/resources/src/web/catalog-fields/select/ui/catalog-field-picker-dialog.tsx) | FieldPickerDialog | [PickerHelp](../packages/rdx-ui/src/components/field-picker-dialog.tsx) · bloque en cuerpo o antes de acciones | Breve · Popover / vista | Migrado · contenido conservado | +| [CatalogTemplateCreateDialog](../modules/resources/src/web/catalog-templates/create/ui/catalog-template-create-dialog.tsx) | FormDialog | [ContextHelp](../modules/resources/src/web/catalog/shared/ui/catalog-help.tsx) · bloque en cuerpo o antes de acciones | Extensa · panel / vista | acciones de formulario, tamaño variable/amplio · Migrado · contenido conservado | +| [GlobalApplicationEditorDialog](../modules/resources/src/web/global-applications/shared/ui/global-application-editor-dialog.tsx) | FormDialog | [ContextHelp](../modules/resources/src/web/global-applications/shared/ui/global-application-form.tsx) · bloque en cuerpo o antes de acciones | Extensa · panel / vista | altura estable, acciones de formulario · Migrado · contenido conservado | +| [ResourceDetailDialog](../modules/resources/src/web/resources/detail/ui/resource-detail-dialog.tsx) | FormDialog | Sin ayuda complementaria propia | — | altura estable, tamaño variable/amplio · Auditado · conserva contenido funcional | +| [DeleteResourceDialog](../modules/resources/src/web/resources/lifecycle/ui/delete-resource-dialog.tsx) | ConfirmDeleteDialog | Sin ayuda complementaria propia | — | Auditado · conserva contenido funcional | +| [ResourceEditorDialog](../modules/resources/src/web/resources/shared/ui/resource-editor-dialog.tsx) | FormDialog | [ContextHelp](../modules/resources/src/web/resources/shared/ui/resource-form.tsx) · bloque en cuerpo o antes de acciones | Extensa · panel / vista | altura estable, acciones de formulario · Migrado · contenido conservado | + +### identity + +| Componente / archivo | Composición | Ayuda y ubicación anterior | Longitud y propuesta | Particularidades / estado | +| --- | --- | --- | --- | --- | +| [AccountPasswordDialog](../modules/identity/src/web/passwords/change/ui/account-password-dialog.tsx) | FormDialog | Sin ayuda complementaria propia | — | acciones de formulario · Auditado · conserva contenido funcional | +| [UserEditorDialog](../modules/identity/src/web/users/shared/ui/user-editor-dialog.tsx) | FormDialog | Sin ayuda complementaria propia | — | acciones de formulario · Auditado · conserva contenido funcional | + +### rdx-ui + +| Componente / archivo | Composición | Ayuda y ubicación anterior | Longitud y propuesta | Particularidades / estado | +| --- | --- | --- | --- | --- | +| [ConfirmDeleteDialog](../packages/rdx-ui/src/components/confirm-delete-dialog.tsx) | FormDialog | Sin ayuda complementaria propia | — | acciones de formulario · Auditado · conserva contenido funcional | +| [EntityTableColumnsDialog](../packages/rdx-ui/src/components/entity-table-columns-dialog.tsx) | FormDialog | Sin ayuda complementaria propia | — | altura estable · Auditado · conserva contenido funcional | +| [FieldPickerDialog](../packages/rdx-ui/src/components/field-picker-dialog.tsx) | FormDialog | [PickerHelp](../packages/rdx-ui/src/components/field-picker-dialog.tsx) · bloque en cuerpo o antes de acciones | Breve · Popover / vista | altura estable, acciones de formulario, tamaño variable/amplio · Migrado · contenido conservado | +| [FormDialog](../packages/rdx-ui/src/components/form-dialog.tsx) | DialogContent | Sin ayuda complementaria propia | — | acciones de formulario, tamaño variable/amplio · Auditado · conserva contenido funcional | +| [OptionPickerDialog](../packages/rdx-ui/src/components/option-picker-dialog.tsx) | FormDialog | Sin ayuda complementaria propia | — | altura estable, acciones de formulario · Auditado · conserva contenido funcional | +| [OptionSelectionField](../packages/rdx-ui/src/components/option-selection-field.tsx) | OptionPickerDialog | Sin ayuda complementaria propia | — | Auditado · conserva contenido funcional | +| [RemoteOptionPicker](../packages/rdx-ui/src/components/remote-option-picker.tsx) | FormDialog | Sin ayuda complementaria propia | — | altura estable, acciones de formulario · Auditado · conserva contenido funcional | + +En AccessQuickEditor, el callback NestedAccessForm lo aporta AccessForm: las variantes de +cuenta/acceso utilizan EntityHelp y la variante recurso utiliza ResourceEditor/ContextHelp. +Comparten el contexto del diálogo activo; sus altas anidadas reciben otro FormDialog. + +## Declaraciones y claves conservadas + +Las traducciones existentes se siguen resolviendo en sus namespaces originales (assets, companies, access, resources). Los títulos, descripciones, ámbitos, ejemplos y orientación se conservan en las mismas expresiones JSX. + +| Archivo de declaración | Expresiones de contenido | +| --- | --- | +| [modules/assets/src/web/asset-relationships/shared/ui/relationship-form.tsx](../modules/assets/src/web/asset-relationships/shared/ui/relationship-form.tsx) | ``t("relationshipHelp")``; ``t("relationshipExample")``; ``t("examplesLabel")``; ``t("relationshipGuidance")``; ``t("helpTitle")`` | +| [modules/assets/src/web/assets/shared/ui/asset-form.tsx](../modules/assets/src/web/assets/shared/ui/asset-form.tsx) | ``t("assetDescription")``; ``t("assetExamples")``; ``t("examplesLabel")``; ``t("assetGuidance")``; ``t("assetScope")``; ``t("helpTitle")`` | +| [modules/assets/src/web/brands/shared/ui/brand-form.tsx](../modules/assets/src/web/brands/shared/ui/brand-form.tsx) | ``t("brands.help")``; ``t("brands.examples")``; ``t("examplesLabel")``; ``t("brands.guidance")``; ``t("globalHelp")``; ``t("helpTitle")`` | +| [modules/assets/src/web/catalog/shared/ui/catalog-help.tsx](../modules/assets/src/web/catalog/shared/ui/catalog-help.tsx) | ``t(`${kind}Description`)``; ``t(`${kind}Examples`)``; ``t("examplesLabel")``; ``t(`${kind}Guidance`)``; ``t(scope === "global" ? "globalHelp" : "companyHelp")``; ``t("helpTitle")`` | +| [modules/assets/src/web/directory-memberships/manage/ui/directory-membership-editor.tsx](../modules/assets/src/web/directory-memberships/manage/ui/directory-membership-editor.tsx) | ``t("directoryUi.help")``; ``t("directoryUi.example")``; ``t("directoryUi.examples")``; ``t("directoryUi.guidance")``; ``t("directoryUi.helpTitle")`` | +| [modules/assets/src/web/infrastructure/shared/ui/infrastructure-editor-dialog.tsx](../modules/assets/src/web/infrastructure/shared/ui/infrastructure-editor-dialog.tsx) | ``t("infrastructure.help")``; ``t("infrastructure.examples")``; ``t("infrastructure.examplesLabel")``; ``t("infrastructure.guidance")``; ``t("infrastructure.helpTitle")`` | +| [modules/assets/src/web/network-share-publications/manage/ui/publication-editor.tsx](../modules/assets/src/web/network-share-publications/manage/ui/publication-editor.tsx) | ``t("shareUi.help")``; ``t("shareUi.example")``; ``t("shareUi.examples")``; ``t( operation.action === "transfer" ? "shareUi.transferHelp" : operation.action === "end" ? "shareUi.endHelp" : "shareUi.publishHelp" )``; ``t("shareUi.documentary")``; ``t("shareUi.helpTitle")`` | +| [modules/assets/src/web/support-connections/shared/ui/support-help.tsx](../modules/assets/src/web/support-connections/shared/ui/support-help.tsx) | ``t("support.description")``; ``t("support.examples")``; ``t("support.examplesLabel")``; ``t("support.guidance")``; ``t("support.scope")``; ``t("support.help")`` | +| [modules/assets/src/web/workstations/associate-assets/ui/workstation-asset-add-dialog.tsx](../modules/assets/src/web/workstations/associate-assets/ui/workstation-asset-add-dialog.tsx) | ``t("workstationAdd.help")``; ``t("workstationAdd.helpLabel")``; ``t("workstationAdd.title")`` | +| [modules/assets/src/web/workstations/shared/ui/workstation-form.tsx](../modules/assets/src/web/workstations/shared/ui/workstation-form.tsx) | ``t("workstationDescription")``; ``t("workstationExamples")``; ``t("examplesLabel")``; ``t("workstationGuidance")``; ``t("workstationScope")``; ``t("helpTitle")`` | +| [modules/companies/src/web/access/help/ui/entity-help.tsx](../modules/companies/src/web/access/help/ui/entity-help.tsx) | ``t(`helpContent.${kind}.description`)``; ``t(`helpContent.${kind}.examples`)``; ``t("helpExamples")``; ``t(`helpContent.${kind}.guidance`)``; ``catalogue && scope ? t(scope === "global" ? "helpGlobalScope" : "helpCompanyScope") : undefined``; ``t("helpTitle")`` | +| [modules/companies/src/web/application-configurations/shared/ui/application-configuration-form.tsx](../modules/companies/src/web/application-configurations/shared/ui/application-configuration-form.tsx) | ``t("applications.description")``; ``t("applications.examples")``; ``t("mail.examplesLabel")``; ``t("applications.guidance")``; ``t("applications.scope")``; ``t("applications.help")`` | +| [modules/companies/src/web/certificate-configurations/shared/ui/certificate-configuration-form.tsx](../modules/companies/src/web/certificate-configurations/shared/ui/certificate-configuration-form.tsx) | ``t("certificates.description")``; ``t("certificates.examples")``; ``t("mail.examplesLabel")``; ``t("certificates.guidance")``; ``t("certificates.scope")``; ``t("certificates.help")`` | +| [modules/companies/src/web/folder-mappings/shared/ui/folder-mapping-form.tsx](../modules/companies/src/web/folder-mappings/shared/ui/folder-mapping-form.tsx) | ``t("folders.description")``; ``t("folders.examples")``; ``t("mail.examplesLabel")``; ``t("folders.guidance")``; ```${t("folders.scope")``; ``t("folders.help")`` | +| [modules/companies/src/web/local-identity-environments/manage/ui/local-identity-editor.tsx](../modules/companies/src/web/local-identity-environments/manage/ui/local-identity-editor.tsx) | ``t("localIdentity.description")``; ``t("localIdentity.examples")``; ``t("mail.examplesLabel")``; ``t("localIdentity.immutable")``; ``t("localIdentity.scope")``; ``t("localIdentity.help")`` | +| [modules/companies/src/web/mail-configurations/shared/ui/mail-configuration-form.tsx](../modules/companies/src/web/mail-configurations/shared/ui/mail-configuration-form.tsx) | ``t("mail.description")``; ``t("mail.examples")``; ``t("mail.examplesLabel")``; ``t("mail.guidance")``; ``t("mail.scope")``; ``t("mail.help")`` | +| [modules/companies/src/web/mailbox-uses/shared/ui/mailbox-use-form.tsx](../modules/companies/src/web/mailbox-uses/shared/ui/mailbox-use-form.tsx) | ``t("mailbox.useDescription")``; ``t("mailbox.useExamples")``; ``t("mail.examplesLabel")``; ``t("mailbox.useGuidance")``; ``t("mailbox.useHelp")`` | +| [modules/companies/src/web/printer-configurations/shared/ui/printer-configuration-form.tsx](../modules/companies/src/web/printer-configurations/shared/ui/printer-configuration-form.tsx) | ``t("printers.description")``; ``t("printers.examples")``; ``t("mail.examplesLabel")``; ``t("printers.guidance")``; ``t("printers.scope")``; ``t("printers.help")`` | +| [modules/companies/src/web/processes/shared/ui/process-help.tsx](../modules/companies/src/web/processes/shared/ui/process-help.tsx) | ``t(`${prefix}.description`)``; ``t(`${prefix}.examples`)``; ``t("mail.examplesLabel")``; ``t(`${prefix}.guidance`)``; ``t(`${prefix}.scope`)``; ``t(`${prefix}.title`)`` | +| [modules/companies/src/web/remote-connections/shared/ui/remote-connection-form.tsx](../modules/companies/src/web/remote-connections/shared/ui/remote-connection-form.tsx) | ``t("remote.description")``; ``t("remote.examples")``; ``t("mail.examplesLabel")``; ``t("remote.guidance")``; ``t("remote.scope")``; ``t("remote.help")`` | +| [modules/companies/src/web/software-installations/shared/ui/software-installation-form.tsx](../modules/companies/src/web/software-installations/shared/ui/software-installation-form.tsx) | ``t("software.description")``; ``t("software.examples")``; ``t("mail.examplesLabel")``; ``t("software.help")``; ``t("applications.help")`` | +| [modules/companies/src/web/telework-authorizations/shared/ui/telework-authorization-form.tsx](../modules/companies/src/web/telework-authorizations/shared/ui/telework-authorization-form.tsx) | ``t("telework.description")``; ``t("telework.examples")``; ``t("mail.examplesLabel")``; ``t("telework.guidance")``; ``t("telework.scope")``; ``t("telework.help")`` | +| [modules/companies/src/web/work-profiles/shared/ui/work-profile-form.tsx](../modules/companies/src/web/work-profiles/shared/ui/work-profile-form.tsx) | ``t("profiles.description")``; ``t("profiles.examples")``; ``t("profiles.examplesLabel")``; ``t("profiles.guidance")``; ``t("profiles.scope")``; ``t("profiles.help")`` | +| [modules/resources/src/web/catalog/shared/ui/catalog-help.tsx](../modules/resources/src/web/catalog/shared/ui/catalog-help.tsx) | ``t(`${kind}Description`)``; ``t(`${kind}Examples`)``; ``t("examplesLabel")``; ``t(`${kind}Guidance`)``; ``t(scope === "global" ? "globalHelp" : "companyHelp")``; ``t("helpTitle")`` | +| [modules/resources/src/web/global-applications/shared/ui/global-application-form.tsx](../modules/resources/src/web/global-applications/shared/ui/global-application-form.tsx) | ``t("global.formDescription")``; ``t("global.formExamples")``; ``t("examplesLabel")``; ``t("global.formGuidance")``; ``t("global.formScope")``; ``t("helpTitle")`` | +| [modules/resources/src/web/resources/shared/ui/resource-form.tsx](../modules/resources/src/web/resources/shared/ui/resource-form.tsx) | ``t(`${helpPrefix}Description`)``; ``t(`${helpPrefix}Examples`)``; ``t("examplesLabel")``; ``t(`${helpPrefix}Guidance`)``; ``t(`${helpPrefix}Scope`)``; ``t("helpTitle")`` | +| [packages/rdx-ui/src/components/field-picker-dialog.tsx](../packages/rdx-ui/src/components/field-picker-dialog.tsx) | ``props.labels.help``; ``props.labels.helpLabel``; ``props.labels.destination`` | + +## Skills consultadas + +Principal: ui-ux-pro-max (búsqueda local sobre foco, teclado y responsive). Complementarias: shadcn (Base UI, composición y estilos), vercel-composition-patterns (slots y contexto con consumidores reales), vercel-react-best-practices (estado mínimo y formularios montados), rdx-solid (contratos y pruebas). Se consultaron sus referencias aplicables junto a DESIGN, patrón de diálogos y procedimiento MCP. La CLI shadcn docs falló por DNS; se consultaron las páginas oficiales de [Dialog](https://ui.shadcn.com/docs/components/base/dialog) y [Popover](https://ui.shadcn.com/docs/components/base/popover) y las APIs instaladas. No se instalaron dependencias externas ni auditores. Solo se añadió el enlace workspace de apps/web a rdx-ui. + +## Pruebas técnicas ejecutadas + +| Comprobación | Resultado | +| --- | --- | +| pnpm typecheck | Falla en packages/shadcn-ui/src/components/calendar.tsx:165, TS2375 por locale opcional con exactOptionalPropertyTypes. Reproducido ejecutando el typecheck aislado del vendor; no hay diferencias en vendor respecto a HEAD. No se modifica por instrucción expresa del repositorio. | +| Typecheck dirigido de @repo/rdx-ui, @metis/web, @metis/assets, @metis/resources y @metis/companies | Correctos. | +| pnpm test (con sockets locales disponibles) | 671 pruebas correctas y 2 fallos, ambos en companies-page.test.tsx: expectativas antiguas de «Personas (1)» y «Esta persona no tiene acceso a Metis Desk». Se reprodujeron los mismos dos fallos en una copia temporal de git archive del commit 91f1d07, sin los cambios de ayuda. | +| Suites de recursos, activos y rdx-ui en la ejecución global | 105, 149 y 82 pruebas correctas, respectivamente. | +| pnpm --filter @metis/web test | 3 archivos correctos: aplicación, traducciones y redirección. Incluye paridad completa es/en. Turbo no alcanzaba esta suite tras los fallos de compañías; se ejecutó expresamente. | +| pnpm --filter @repo/rdx-ui test | 14 archivos correctos, incluidas las nuevas pruebas de política y conservación de contenido. | +| python3 scripts/visual-audit.test.py | 10 pruebas correctas del preparador, aislamiento y diagnóstico. | +| pnpm lint / pnpm check | Fallan fuera del diff: 15 errores de lint y 166 diagnósticos de error en check (incluye formato). Entre las fuentes: implementación previa de fotos de personas, migraciones y archivos locales de skills/diagnóstico. No se reformatean ni corrigen en esta tarea. | +| Biome check y ESLint de espaciado sobre los 19 archivos TS/TSX/JSON modificados o nuevos | Correctos, sin diagnósticos. | +| pnpm build | Correcto; conserva el aviso de tamaño de chunks de la aplicación. | +| git diff --check | Correcto. | + +La primera ejecución de pnpm test dentro del sandbox falló al probar HTTP en resource-scope.test.ts. Con sockets locales autorizados, esa suite pasó. No se contabiliza ese fallo de entorno como regresión del código. La prueba de perfiles que exigía la antigua clase max-h-[40%] se actualizó: comprueba pasos y conservación de ejemplos/orientación, sin exigir el pie retirado. + +## Validación interactiva y evidencias + +Procedimiento reproducible: `bash scripts/setup-playwright-wsl.sh` y `python3 scripts/visual-audit.py --dialog-help --check`. Usa la imagen MCP fijada por digest, Chromium 155, build de producción en Vite preview y HTTP real con usuario ordinario. El runner confirma el origen y puerto libres antes de navegar. No se utiliza la base local: crea y retira un contenedor exclusivo con metis_desk_test. + +Volumen preparado en cada ejecución: 15 compañías; en la principal, 50 personas, 51 activos, 66 recursos (16 fixtures y 50 aplicaciones), 50 puestos y 50 perfiles. Las escrituras de prueba se limitan a esa instalación desechable. Las sesiones y credenciales se retiran al finalizar, también tras fallos. + +La matriz representativa principal terminó correctamente en `.artifacts/playwright/authenticated-u53obiy7/`: edición real de activo con valores modificados, validación y selector; ayuda extensa y breve en selector real de activos del puesto. Se comprobaron siete anchos (320, 375, 390, 768, 1024, 1280 y 1440), ausencia de desbordamiento del diálogo, alineación ?/X, foco al abrir y cerrar, Escape, mismo nodo de input tras regresar, datos y validaciones conservados, cambio panel→vista y Popover→vista, retorno explícito y cierre X. La consola de ese recorrido registró cero errores y cero warnings. + +Se abrieron e inspeccionaron las capturas de 320 y 1440, Popover y tema oscuro. El manifiesto dialog-help-report.json conserva medidas y SHA-256 de cada captura. Son artefactos locales excluidos de Git. + +## Accesibilidad, correcciones y límites + +- La comprobación real de Escape detectó el cierre del modal completo; se corrigió utilizando el contrato de cancelación de Base UI. No se añadió un listener global ni un focus trap propio. +- Se mantiene la misma instancia del formulario, sus validaciones y selector. La vista interna desactiva su interacción con inert y lo oculta del árbol accesible. Los botones de ayuda son type=button. +- El título del panel/vista recibe foco y etiqueta la región. Cerrar devuelve foco al trigger incluso si el cambio de modalidad sustituye su composición con Popover. Se conserva la navegación de teclado de Base UI. +- La auditoría adicional de movimiento reducido detectó precedencia de animaciones vendor sobre la clase inicial. Se corrigió mediante utilidades important limitadas a prefers-reduced-motion en los wrappers propios. Vendor permanece intacto. +- Se midió el color calculado de texto y fondos compuestos con canvas. El desplegable de ejemplos daba 4,28:1 en claro; dentro de los diálogos se cambió a text-foreground, conservando íntegro el texto. La verificación posterior se detalla en el resultado ampliado. +- No se han probado lectores de pantalla reales ni se certifica conformidad WCAG 2.2 AA completa. La inspección de capturas y las medidas programáticas no sustituyen esas pruebas. +- La matriz no implica abrir cada variante de los 102 componentes ni volver a probar todos sus estados de red, permisos o confirmaciones destructivas. Esas descripciones y controles no se migraron. +- El zoom nativo del navegador al 200 % y 400 % no se considera verificado mediante cambios de viewport o tamaño de fuente: se documentan como pruebas de reflow y ampliación de texto, por separado. + +### Recorrido ampliado completado + +Ejecución completa correcta en `.artifacts/playwright/authenticated-q920cqcu/` mediante +`python3 scripts/visual-audit.py --dialog-help --check`: + +- 28 comprobaciones: aislamiento del inventario de otra compañía, usuario ordinario, + cabecera única, ayuda inicialmente oculta, formulario inválido real, siete anchos, + panel/vista, Enter/Escape, foco de retorno, identidad del input, datos/validación/selector, + ausencia de overflow, redimensionamiento, oscuro, cierre X, movimiento reducido y Tab + en Popover, cierre Popover sin cerrar el selector, conservación de búsqueda y radio, + diálogo de columnas sin ?, contraste, reflow/texto ampliado, título largo sintético, + guardado tras consultar ayuda, cancelación sin escritura y etiquetas en inglés. +- Se cambió el estado técnico a Averiado y se conservó la pestaña Hardware durante las + aperturas/cierres; no se comprueba solo el valor inicial de un selector. +- Contrastes calculados después de la corrección: texto secundario **4,83:1 en claro** + y **6,85:1 en oscuro**; el desplegable de ejemplos **17,42:1 / 16,68:1**. +- Reflow a 720 y 360 CSS px, equivalente a reducir el espacio de una ventana de 1440 al + 200 % y 400 %. Por separado, ampliación de fuente raíz al 200 % y 400 % en 1440 px. + Son comprobaciones de capacidad y texto; no una prueba de zoom nativo del navegador. +- El título largo es una modificación DOM sintética y reversible de la cabecera del + formulario real; no se guardó un nombre de negocio ni se inventó una pantalla. +- La consola no registró errores ni warnings React. Aparecieron dos avisos de rendimiento + de Canvas2D generados por el propio medidor de contraste, sin relación con el producto. + El comprobador posterior utiliza willReadFrequently para evitarlos. + +La inspección de la captura con fuente al 400 % motivó además un fallback de scroll en la +región de ayuda, con un mínimo de tres líneas para su contenido; conserva acceso al botón +de retorno cuando el espacio vertical resulta insuficiente. + +Comprobación final completa: **28 verificaciones correctas, 14 capturas y consola sin errores +ni warnings** en `.artifacts/playwright/authenticated-a0gc2ghb/`. Se repitió el recorrido +con la región legible y el retorno mediante scroll. Se abrieron e inspeccionaron las +capturas finales de panel a 1440, vista a 320, Popover, título largo y texto al 400 %. +El [informe local de navegador](../.artifacts/playwright/authenticated-a0gc2ghb/dialog-help-report.json) +registra todas las mediciones y las huellas SHA-256; dialog-help-console.json conserva la +salida de consola. Los archivos son locales y están excluidos de Git. El contenedor y su +sesión se retiraron al finalizar. La compilación y las comprobaciones dirigidas se +repitieron correctamente después del último ajuste. + +### Ampliación: animación CSS del panel lateral + +El panel compartido incorpora entrada con fundido y desplazamiento corto desde la derecha, +200 ms y ease-out, reutilizando las utilidades CSS existentes. Se anima únicamente +opacity/transform, también al reabrir; prefers-reduced-motion desactiva la animación. +Criterios consultados: UI/UX Pro Max (movimiento reducido, movimiento limitado y duración +adecuada a la distancia) y las convenciones shadcn del proyecto. + +Verificación correcta mediante `python3 scripts/visual-audit.py --dialog-help --check`: +30 comprobaciones, 14 capturas y consola sin errores ni warnings. El comprobador captura +la animación en la misma operación que abre el panel y controla su timeline con Web +Animations API, sin esperas arbitrarias: opacidad 0 al inicio, intermedia entre 0 y 1 a los +100 ms y 1 al finalizar; duración calculada 0,2 s. Se verificó también animationName=none +con movimiento reducido y se repitió el recorrido de foco, formulario y responsive. +Evidencia: `.artifacts/playwright/authenticated-ft29wong/dialog-help-report.json`. +El entorno desechable y su sesión se retiraron al terminar. Typecheck de rdx-ui, Biome, +ESLint de espaciado y git diff --check correctos; la auditoría ejecutó además el build. + +## Pendientes reales + +Pruebas con lectores de pantalla, zoom nativo 200/400 %, revisión manual completa de todos +los diálogos y escenarios de altura baja como 320×640. No se instalan auditores ausentes. +Los fallos globales de tipos, lint/formato y las dos expectativas previas de compañías +permanecen fuera de esta migración y se han detallado con sus comprobaciones de aislamiento. diff --git a/docs/componentes-de-interfaz.md b/docs/componentes-de-interfaz.md index 475e15ee..eac769fa 100644 --- a/docs/componentes-de-interfaz.md +++ b/docs/componentes-de-interfaz.md @@ -149,13 +149,13 @@ Guías oficiales: [monorepos](https://ui.shadcn.com/docs/monorepo) e [instalaci ## Ayuda contextual -ContextHelp muestra una explicación breve y permite desplegar ejemplos y recomendaciones mediante teclado. EntityHelp aporta contenido específico en español e inglés para compañías, personas, departamentos, sedes, asignaciones y todas las secciones de cuentas y accesos. Los catálogos explican si la definición es global o exclusiva de la compañía. Los ejemplos son orientativos: nunca se convierten en valores del formulario. +ContextHelp declara una explicación estructurada con ejemplos y recomendaciones desplegables mediante teclado. Dentro de FormDialog se consulta bajo demanda desde ? en la cabecera; en páginas conserva su presentación visible. EntityHelp aporta contenido específico en español e inglés para compañías, personas, departamentos, sedes, asignaciones y todas las secciones de cuentas y accesos. Los catálogos explican si la definición es global o exclusiva de la compañía. Los ejemplos son orientativos: nunca se convierten en valores del formulario. La ayuda se presenta en una tarjeta con icono circular, título y explicación; debajo, una banda desplegable contiene los ejemplos y recomendaciones. ContextHelp admite `icon?: ReactNode` para sustituir el icono informativo predeterminado, por ejemplo `icon={}`. El icono es decorativo y hereda el tamaño y color del contenedor. Se conservan los tokens del tema, los textos aportados por cada módulo y el despliegue nativo por teclado. El botón Ayuda de Cuentas y accesos y Catálogos informáticos permite consultar la explicación de la sección sin abrir un alta. La misma ayuda aparece dentro de los modales de alta y edición. FormDialog utiliza una variante amplia para personas y formularios con más campos; el número de campos no cambia la navegación a una página propia. -La ayuda de los formularios se coloca en su parte inferior, inmediatamente antes de FormActions. Este pie compartido reúne Guardar y Cancelar dentro del formulario, conservando el envío y el cierre accesible del diálogo. Los diálogos de consulta mantienen únicamente Cerrar. +La ayuda complementaria de formularios se presenta mediante el slot compartido DialogHelp; ContextHelp y FormHelp lo integran automáticamente en FormDialog. `FormActions` reúne Guardar y Cancelar dentro del formulario, conservando el envío y el cierre accesible del diálogo. Los diálogos de consulta mantienen únicamente Cerrar. ## SelectWithAction: desplegable con acción integrada @@ -186,10 +186,10 @@ Sistema permite crear un sistema propio de la compañía mediante un modal adici QuickCreateDialog conserva los valores del formulario principal, actualiza el catálogo y selecciona el nuevo UUID después de guardar. Usa las rutas, protección CSRF y reglas de negocio existentes. Un nombre activo ya existente permite usar ese registro sin duplicarlo. Cancelar el modal adicional conserva la cuenta sin cambios. El elemento creado permanece en el catálogo aunque se cancele después la cuenta. -La ayuda aparece antes del pie con Guardar y seleccionar / Cancelar. Solo hay un nivel de alta adicional; el nuevo formulario no vuelve a ofrecer altas rápidas. +La ayuda se consulta desde ? en cabecera; Guardar y seleccionar / Cancelar permanecen en el pie. Solo hay un nivel de alta adicional; el nuevo formulario no vuelve a ofrecer altas rápidas. -La revisión de los formularios extiende este patrón a personas, departamentos, cuentas, grupos, recursos, tipos de recurso, permisos y plantillas. Las asignaciones de departamento permiten añadir el primero sin abandonar el formulario. Se reutilizan los formularios y rutas existentes, manteniendo su validación y ayuda inferior. +La revisión de los formularios extiende este patrón a personas, departamentos, cuentas, grupos, recursos, tipos de recurso, permisos y plantillas. Las asignaciones de departamento permiten añadir el primero sin abandonar el formulario. Se reutilizan los formularios y rutas existentes, manteniendo su validación y ayuda contextual bajo demanda. Los entornos necesitan un sistema activo seleccionado. Los permisos nuevos heredan el tipo del recurso elegido. Al crear un grupo desde una pertenencia, sistema y entorno quedan fijados a los de la cuenta. Las plantillas siguen siendo globales; el modal lo explica y carga únicamente sus opciones globales. Los elementos creados desde un selector están activos para poder seleccionarlos. @@ -376,7 +376,7 @@ tabla. Cambiar de paso, añadir o retirar activos y desplegar ayuda no redimensi modal. La cabecera y las acciones permanecen visibles. `layout="content"` queda para contenido breve que no cambia de tamaño. Véase la [regla normativa de estabilidad](patron-de-dialogos.md#estabilidad-de-altura-del-modal). -La cabecera permanece visible. Un formulario modal utiliza una raíz flex con altura mínima cero, `FormBody` para campos y errores, y `FormActions` como hermano final fuera del cuerpo desplazable. Los botones permanecen dentro del formulario HTML original: se conservan envío por teclado, validación nativa, estados pendientes y cancelación. La ayuda queda inmediatamente antes de las acciones; en diálogos de altura estable ocupa un bloque anclado abajo fuera del scroll de campos, con altura limitada y desplazamiento propio si se despliega contenido extenso. Los diálogos de consulta conservan el cuerpo desplazable y su pie externo. +La cabecera permanece visible. Un formulario modal utiliza una raíz flex con altura mínima cero, `FormBody` para campos y errores, y `FormActions` como hermano final fuera del cuerpo desplazable. Los botones permanecen dentro del formulario HTML original: se conservan envío por teclado, validación nativa, estados pendientes y cancelación. La ayuda complementaria se declara con ContextHelp/FormHelp y se consulta desde ? en cabecera mediante el sistema compartido; usa panel o vista interna según capacidad. Los diálogos de consulta conservan el cuerpo desplazable y su pie externo. `FormSection` agrupa datos relacionados mediante título accesible; la variante `card` añade un borde discreto y `plain` conserva el bloque sin marco. Ambas utilizan `FormSectionTitle`. Activos, recursos y plantillas lo utilizan para sus datos principales. Las características se distribuyen en dos bloques por fila cuando el contenedor dispone de al menos 900 px; los controles dentro de cada bloque mantienen una o dos columnas según el espacio. Una plantilla sin secciones puede aprovechar hasta tres columnas de características. Observaciones y ayuda ocupan toda la fila. La disposición responde al espacio del modal, no solo al ancho del navegador. @@ -431,7 +431,7 @@ El alta y la edición de activos usan `FormSection variant="plain"` para separar Para formularios que muestran todos los grupos juntos, `FieldSections layout="form"` presenta cada grupo plegable de ancho completo, abierto inicialmente, con separador tenue bajo el título. Los controles se alinean con el texto del título y pasan de una a dos, tres o cuatro columnas según el ancho real del contenedor, conservando el orden de los campos. No se emparejan grupos de distinta altura, evitando huecos entre secciones. Los campos sin grupo permanecen visibles; los grupos con controles inválidos se abren para permitir su enfoque. -La ayuda inferior utiliza `ContextHelp variant="plain"`. Guardar y Cancelar permanecen en el pie fijo del diálogo; el cuerpo conserva desplazamiento cuando sea necesario. Las variantes anteriores continúan disponibles para los demás formularios. Esta distribución no modifica validación, instantáneas, valores ni operaciones de alta relacionada. +La ayuda utiliza `ContextHelp`, que dentro de FormDialog se presenta bajo demanda desde cabecera. Guardar y Cancelar permanecen en el pie fijo del diálogo; el cuerpo conserva desplazamiento cuando sea necesario. Las variantes anteriores continúan disponibles para los demás formularios. Esta distribución no modifica validación, instantáneas, valores ni operaciones de alta relacionada. ### Navegación entre grupos de características @@ -439,7 +439,7 @@ El formulario de activos mantiene nombre, plantilla, estado y sede siempre visib En escritorio, los grupos se navegan en una columna vertical a la izquierda de los controles. En móvil, la lista pasa a horizontal con desplazamiento; la orientación de teclado también se adapta mediante las primitivas Tabs de shadcn/ui. Los paneles se mantienen montados para conservar los valores; al cambiar de plantilla, se inicia la navegación en su primer grupo. -Antes del envío, el formulario localiza el primer control inválido y solicita la validación nativa. El panel correspondiente se activa de forma síncrona antes de que el navegador enfoque el control. Se mantienen la validación de esquema, los errores, la ayuda inferior y el pie fijo de operaciones. +Antes del envío, el formulario localiza el primer control inválido y solicita la validación nativa. El panel correspondiente se activa de forma síncrona antes de que el navegador enfoque el control. Se mantienen la validación de esquema, los errores, la ayuda contextual bajo demanda y el pie fijo de operaciones. ## Edición de plantillas globales @@ -473,7 +473,7 @@ El diálogo «Nuevo recurso» reutiliza `FormDialog`, `FormBody`, `FormActions`, los controles actuales. Su indicador muestra los tres pasos, identifica el actual mediante `aria-current="step"` y anuncia el avance con un texto traducido al que se lleva el foco. Las acciones conservan Cancelar y añaden Anterior/Siguiente; Guardar aparece en el último -paso. La ayuda permanece al final del cuerpo, inmediatamente antes del pie. +paso. La ayuda se declara en el formulario y se presenta bajo demanda desde la cabecera. `ResourceIdentityFields` y `ResourceFields` comparten identificación, integración y características entre alta y edición. `ResourceFormActions` encapsula las acciones de ambos @@ -548,7 +548,7 @@ operativo propio; `layout="content"` conserva la presentación de listados. Los internos `EntityTableConfiguration` y `EntityListPagination` reciben la presentación `picker` o `full`, con `full` como default. No conocen entidades ni queries. -`PickerHelp` recibe label/title/description y compone Popover de Base UI junto al pie. +`PickerHelp` recibe label/title/description y declara ayuda compact mediante DialogHelp: Popover de Base UI en cabecera o vista interna cuando falta espacio. Permite consultar sección e instrucciones completas sin consumir el alto de las filas. `OptionPickerList` protege selection/name; sus etiquetas incluyen `entity(count)`, separado del título de acción. `EntityTable.entityLabel` acepta ese callback o la cadena anterior. @@ -790,7 +790,7 @@ cuerpo desplazable distribuye mediante flex el espacio disponible, sin un conten intermedio con altura porcentual. En el paso 2, el fieldset crece y distribuye ese alto al estado vacío, que ocupa también todo el ancho. El contenido permanece centrado, con acciones que se adaptan a espacios estrechos y scroll cuando -se necesita; no altera la altura exterior del modal ni la ayuda anclada al pie. +se necesita; no altera la altura exterior del modal ni la ayuda bajo demanda. ## Estado vacío con acciones @@ -845,7 +845,7 @@ edición manual se conserva al retroceder y volver al resumen. Véase Componentes genéricos de `@repo/rdx-ui`, independientes de DTO, consultas y traducciones: -- `FormHelp` encapsula `ContextHelp` y su área inferior con altura limitada y scroll propio. +- `FormHelp` encapsula `ContextHelp` y declara el slot de ayuda estructurada de FormDialog, sin área inferior permanente. Perfiles y puestos lo sitúan como hermano de `FormBody`, inmediatamente antes del pie. - `FormStepActions` comparte Anterior, Siguiente y Guardar: `canContinue` bloquea el envío en cualquier paso; durante el guardado bloquea también Cancelar y Anterior. El consumidor @@ -1627,3 +1627,15 @@ Assets publica `WorkstationAssetAddDialog`, con `AssetPickerList`, el asistente unifica la recuperación de revisión obsoleta con el editor tradicional. El módulo consumidor aporta autorización, compañía, puesto y lookups; los nodos solo reciben callbacks. Contratos, pruebas y limitaciones: [WD04.4](mapa-visual-del-puesto.md#wd044--asociación-de-activos-desde-el-mapa-visual). + + +## Ayuda contextual global de diálogos + +FormDialog, DialogHelp, ContextHelp, FormHelp y PickerHelp comparten una única infraestructura. +Los módulos declaran contenido y, excepcionalmente, su política semántica. No gestionan +portales, breakpoints ni foco. La API, ejemplos, reserva mínima de espacio, traducciones, +excepciones y restricciones se definen en el [patrón de diálogos](patron-de-dialogos.md#ayuda-contextual-bajo-demanda). +El [inventario y resultados](auditoria-ayuda-contextual-dialogos.md) distingue revisión estática +y validación interactiva. Las descripciones previas de ayuda permanente al pie quedan +sustituidas por este patrón para la ayuda complementaria; las instrucciones de campo, +validaciones, advertencias críticas y confirmaciones destructivas conservan visibilidad. diff --git a/docs/patron-de-dialogos.md b/docs/patron-de-dialogos.md index c9d570f6..2f3ba2a3 100644 --- a/docs/patron-de-dialogos.md +++ b/docs/patron-de-dialogos.md @@ -26,7 +26,7 @@ y puede crecer en un contenedor flex para ocupar el alto restante. Para distribu espacio desde el cuerpo del diálogo, utilizar `FormBody contentLayout="fill"` y mantener la cadena de crecimiento hasta el estado vacío. No depender de `min-height: 100%` en contenedores intermedios con altura automática. Conservar el scroll, la altura exterior -estable y la ayuda inmediatamente encima del pie del modal. +estable. La ayuda complementaria se consulta bajo demanda desde la cabecera. ## Grupos, títulos y separación @@ -155,7 +155,7 @@ operativo con herramientas y paginación compactas; no sumar mínimos independie RadioGroup/Tabs. Verificar `clientHeight` y clipping de filas, no solo el alto del modal. El objetivo en 320×640 es mostrar dos opciones ordinarias completas; si el contenido extenso lo impide, documentar el presupuesto y demostrar acceso íntegro mediante scroll. -La ayuda extensa puede utilizar `PickerHelp` junto al pie: conserva instrucciones y +La ayuda breve utiliza `PickerHelp` bajo demanda desde la cabecera: conserva instrucciones y destino en un Popover accesible sin reducir las filas al desplegarlo. No ocultar información obligatoria ni crear focus traps o listeners Escape manuales. En ventanas muy cortas, el scroll de contingencia del cuerpo conserva acceso a controles; no encadenar su gesto @@ -214,15 +214,77 @@ quedar en las pestañas, filtros, controles de selección o botones del pie. - Los formularios con contenido variable mantienen altura exterior estable según la regla de estabilidad; sus grupos interiores conservan altura según contenido. No introducir pestañas ni pasos para resolver solo espacio. -- Reutilizar `FormHelp` para la ayuda contextual general inmediatamente antes de `FormActions`. En los - diálogos de altura estable debe quedar anclada abajo, fuera del scroll principal: - campos, progreso y errores se desplazan en `FormBody`, mientras la ayuda permanece - pegada al pie incluso en pasos cortos. Eliminar márgenes superiores innecesarios y - conservar el padding normal junto al footer. Limitar su altura y permitir scroll propio al - desplegar ejemplos, para no ocultar los campos ni las acciones en ventanas pequeñas. - Mantener errores junto al contexto que los origina y las acciones dentro del formulario. +- Declarar la ayuda contextual complementaria mediante `ContextHelp`, `FormHelp`, `PickerHelp` + o `DialogHelp` dentro de `FormDialog`. El sistema compartido la sitúa bajo demanda desde + el botón `?` de cabecera, inmediatamente antes de `X`. No añadir bloques permanentes al pie + salvo una excepción funcional justificada. La [API y política de ayuda](#ayuda-contextual-bajo-demanda) + definen contenido, modos y accesibilidad. Mantener errores, advertencias críticas e + instrucciones imprescindibles visibles junto al contexto que los origina. - Conservar foco, cancelación, cierre por teclado, estados de carga, error, vacío y envío. No cerrar ni limpiar el formulario antes de confirmar una operación correcta. - Validar los cambios de interacción con pruebas dirigidas y revisar visualmente los anchos estrecho y amplio, nombres largos, secciones de distinta altura, plegado y resultados vacíos cuando correspondan. Informar de las comprobaciones que no se hayan podido ejecutar. + +## Ayuda contextual bajo demanda + +El patrón reduce la información secundaria persistente sin eliminar contenido. Se aplica a +formularios, pickers, wizards y consultas con ayuda complementaria. La ayuda que constituye +la tarea principal de un diálogo se mantiene visible (`presentation="inline"` en ContextHelp). + +`FormDialog` conserva el contrato de apertura, cierre, foco, tamaño y layout. Proporciona +los destinos de cabecera y ayuda; `DialogHelp` declara `title: string`, `children: ReactNode` +y `presentation?: "auto" | "compact" | "expanded"` (auto por defecto). ContextHelp mantiene +sus props de descripción, ejemplos, ámbito, orientación e icono; FormHelp reutiliza ese +contrato. Dentro de FormDialog adoptan el nuevo slot automáticamente. Fuera de él siguen +mostrando contenido visible. No es necesario elevar traducciones desde los formularios. + +```tsx + + + + + + +``` + +Declarar un único bloque de ayuda general por diálogo. Si AssetForm ya declara ContextHelp +o FormHelp, no añadir otro DialogHelp al padre. Los diálogos anidados tienen su propio contexto. + +- `compact`: explicación breve de una tarea. Popover desde 30rem de ancho efectivo del + diálogo; vista interna cuando falta espacio. PickerHelp declara esta política. +- `auto`: contenido estructurado, como descripción + ámbito + ejemplos + orientación. + Equivale a expanded; nunca mide caracteres ni texto del DOM. +- `expanded`: panel integrado de 20rem a la derecha desde 60rem de superficie, reservando + al menos 40rem al formulario; por debajo, vista de ayuda dentro del mismo modal. +- Se miden el diálogo real y la fuente raíz mediante ResizeObserver. Un modal estrecho + en un monitor amplio utiliza vista interna. El panel no ensancha la superficie exterior. + Al aparecer, el panel utiliza una animación CSS de entrada de 200 ms con fundido y + desplazamiento corto desde la derecha. Solo anima opacity/transform y se desactiva con + prefers-reduced-motion. La animación se aplica al contenedor compartido, también al reabrirlo. +- En vista interna el formulario permanece montado y conserva geometría, scroll y estado, + con visibility:hidden, inert y aria-hidden. Sus acciones no se pueden activar. La ayuda + ofrece «Volver al formulario» y mantiene disponible X. En panel, el formulario y su pie + siguen siendo utilizables; la ayuda dispone de desplazamiento y cierre independientes. + El contenido reserva al menos tres líneas de altura; con texto muy ampliado, la región + completa de ayuda también permite scroll para conservar legibilidad y acceso al retorno. +- El botón de ayuda usa icono CircleHelp del catálogo Tabler, Button icon-sm/ghost, + nombre accesible traducido, aria-expanded y aria-controls cuando está abierto. El título + dispone de espacio flexible; los controles no se solapan ni reservan un hueco de ayuda ausente. +- Al abrir panel/vista o cambiar de modo se enfoca su encabezado. Al cerrar se devuelve foco + a ?. Escape cierra primero la ayuda mediante el contrato de cierre de Base UI; otra + pulsación cierra el diálogo. Los controles anidados conservan las reglas de sus primitivas. +- Popover utiliza las primitivas instaladas, su posicionamiento adaptativo, portal y gestión + de foco; no se crea otro modal, Sheet superpuesto ni focus trap propio. +- Las etiquetas genéricas se suministran con DialogHelpLabelsContext. La aplicación las + resuelve en core.dialogHelp (es/en); rdx-ui no conoce namespaces ni entidades. +- Conservar sin reformular textos, enlaces, listas, ejemplos y traducciones. Los campos, + validaciones, consecuencias de operaciones, seguridad y estados vacíos siguen visibles. + +No duplicar triggers, cierres, detectores responsive ni paneles en módulos. No clasificar +por longitud del DOM, usar hover como único acceso, desmontar el formulario ni introducir +otra capa modal para la ayuda. Comprobar teclado, foco, responsive, scroll y estados en +navegador; no atribuir a pruebas automatizadas validación con lectores de pantalla reales. + +Véase [auditoría e inventario final](auditoria-ayuda-contextual-dialogos.md) para migraciones, +excepciones, evidencias y límites de las comprobaciones. diff --git a/modules/companies/src/web/access/help/ui/access-help-dialog.tsx b/modules/companies/src/web/access/help/ui/access-help-dialog.tsx index 8eb236bf..1b0968e4 100644 --- a/modules/companies/src/web/access/help/ui/access-help-dialog.tsx +++ b/modules/companies/src/web/access/help/ui/access-help-dialog.tsx @@ -25,7 +25,7 @@ export function AccessHelpDialog({ open={open} title={t(`kinds.${kind}`)} > - + ); } diff --git a/modules/companies/src/web/access/help/ui/entity-help.tsx b/modules/companies/src/web/access/help/ui/entity-help.tsx index f69e5dbc..d7fb9677 100644 --- a/modules/companies/src/web/access/help/ui/entity-help.tsx +++ b/modules/companies/src/web/access/help/ui/entity-help.tsx @@ -11,7 +11,7 @@ export function EntityHelp({ }: { kind: HelpKind; scope?: string; - placement?: "body" | "footer"; + placement?: "body" | "footer" | "inline"; }) { const { t } = useTranslation("access"); const catalogue = ["system", "resource_type", "permission", "template"].includes(kind); @@ -24,6 +24,7 @@ export function EntityHelp({ examples={t(`helpContent.${kind}.examples`)} examplesLabel={t("helpExamples")} guidance={t(`helpContent.${kind}.guidance`)} + presentation={placement === "inline" ? "inline" : "auto"} scopeNote={ catalogue && scope ? t(scope === "global" ? "helpGlobalScope" : "helpCompanyScope") diff --git a/modules/companies/src/web/work-profiles/shared/profile.test.tsx b/modules/companies/src/web/work-profiles/shared/profile.test.tsx index 3c7f44a8..3d455808 100644 --- a/modules/companies/src/web/work-profiles/shared/profile.test.tsx +++ b/modules/companies/src/web/work-profiles/shared/profile.test.tsx @@ -149,7 +149,7 @@ test("perfiles: la cabecera breve conserva el motivo completo en la celda de rev } }); -test("perfiles: alta en cuatro pasos, sin guardado intermedio y ayuda anclada al pie", async () => { +test("perfiles: alta en cuatro pasos, sin guardado intermedio y contenido de ayuda conservado", async () => { for (const language of ["es", "en"]) { const html = await render( language, @@ -168,7 +168,8 @@ test("perfiles: alta en cuatro pasos, sin guardado intermedio y ayuda anclada al assert.equal(html.includes('type="password"'), false); assert.ok(html.indexOf(" html.indexOf('data-slot="form-body"')); assert.ok(html.indexOf(text.profiles.help) < html.indexOf(text.profiles.steps.next)); - assert.ok(html.includes("max-h-[40%]")); + assert.ok(html.includes(text.profiles.examples)); + assert.ok(html.includes(text.profiles.guidance)); } }); diff --git a/modules/core/src/common/locales/en.json b/modules/core/src/common/locales/en.json index f10608b5..07a8884e 100644 --- a/modules/core/src/common/locales/en.json +++ b/modules/core/src/common/locales/en.json @@ -30,5 +30,11 @@ "supportWorkspace": "Support workspace", "collapseSidebar": "Collapse menu", "expandSidebar": "Expand menu" + }, + "dialogHelp": { + "help": "Help", + "closeHelp": "Close help", + "backToForm": "Back to form", + "closeDialog": "Close" } } diff --git a/modules/core/src/common/locales/es.json b/modules/core/src/common/locales/es.json index fa6a18ef..9288fb57 100644 --- a/modules/core/src/common/locales/es.json +++ b/modules/core/src/common/locales/es.json @@ -30,5 +30,11 @@ "supportWorkspace": "Espacio de soporte", "collapseSidebar": "Contraer menú", "expandSidebar": "Expandir menú" + }, + "dialogHelp": { + "help": "Ayuda", + "closeHelp": "Cerrar ayuda", + "backToForm": "Volver al formulario", + "closeDialog": "Cerrar" } } diff --git a/packages/rdx-ui/package.json b/packages/rdx-ui/package.json index d985d091..a6df8d8e 100644 --- a/packages/rdx-ui/package.json +++ b/packages/rdx-ui/package.json @@ -11,7 +11,7 @@ }, "scripts": { "typecheck": "tsc -p tsconfig.json", - "test": "TSX_TSCONFIG_PATH=../../tsconfig.json node --import tsx --test src/select-with-action.test.tsx src/selection-components.test.tsx src/entity-list.test.tsx src/compact-collections.test.tsx src/entity-column-visibility.test.tsx src/time-line.test.tsx src/count-badge.test.tsx src/scrollable-tabs.test.tsx src/field-sections.test.tsx src/phone-link.test.tsx src/lib/reorder-items.test.ts src/lib/text-search.test.ts src/remote-option-list.test.tsx" + "test": "TSX_TSCONFIG_PATH=../../tsconfig.json node --import tsx --test src/select-with-action.test.tsx src/selection-components.test.tsx src/entity-list.test.tsx src/compact-collections.test.tsx src/entity-column-visibility.test.tsx src/time-line.test.tsx src/count-badge.test.tsx src/scrollable-tabs.test.tsx src/field-sections.test.tsx src/phone-link.test.tsx src/lib/reorder-items.test.ts src/lib/text-search.test.ts src/remote-option-list.test.tsx src/dialog-help.test.tsx" }, "dependencies": { "@base-ui/react": "1.8.0", diff --git a/packages/rdx-ui/src/components/context-help-content.tsx b/packages/rdx-ui/src/components/context-help-content.tsx new file mode 100644 index 00000000..38d9642b --- /dev/null +++ b/packages/rdx-ui/src/components/context-help-content.tsx @@ -0,0 +1,77 @@ +import type { ReactNode } from "react"; + +import { ChevronDownIcon, InfoIcon } from "../lib/icons"; + +export function ContextHelpContent({ + title, + description, + examples, + guidance, + examplesLabel, + scopeNote, + icon = , + variant = "card", +}: { + title: string; + description: string; + examples: string; + guidance: string; + examplesLabel: string; + scopeNote?: string | undefined; + icon?: ReactNode; + variant?: "card" | "plain" | "dialog"; +}) { + return ( + + ); +} diff --git a/packages/rdx-ui/src/components/context-help.tsx b/packages/rdx-ui/src/components/context-help.tsx index c067a000..9439037b 100644 --- a/packages/rdx-ui/src/components/context-help.tsx +++ b/packages/rdx-ui/src/components/context-help.tsx @@ -1,73 +1,29 @@ -import type { ReactNode } from "react"; +import type { ComponentProps } from "react"; +import { useContext } from "react"; -import { ChevronDownIcon, InfoIcon } from "../lib/icons"; +import { DialogHelpContext } from "../lib/dialog-help-context"; +import type { DialogHelpPresentation } from "../lib/dialog-help-presentation"; +import { ContextHelpContent } from "./context-help-content"; +import { DialogHelp } from "./dialog-help"; + +/** Declara ayuda complementaria; fuera de un FormDialog conserva su presentación en página. */ export function ContextHelp({ - title, - description, - examples, - guidance, - examplesLabel, - scopeNote, - icon = , - variant = "card", -}: { - title: string; - description: string; - examples: string; - guidance: string; - examplesLabel: string; - scopeNote?: string | undefined; - icon?: ReactNode; + presentation = "auto", + ...props +}: Omit, "variant"> & { variant?: "card" | "plain"; + presentation?: DialogHelpPresentation | "inline"; }) { + const dialog = useContext(DialogHelpContext); + + if (!dialog || presentation === "inline") { + return ; + } + return ( - + + + ); } diff --git a/packages/rdx-ui/src/components/dialog-help.tsx b/packages/rdx-ui/src/components/dialog-help.tsx new file mode 100644 index 00000000..c4b987e1 --- /dev/null +++ b/packages/rdx-ui/src/components/dialog-help.tsx @@ -0,0 +1,126 @@ +import { Button } from "@repo/shadcn-ui/components/button"; +import { + Popover, + PopoverContent, + PopoverTitle, + PopoverTrigger, +} from "@repo/shadcn-ui/components/popover"; +import type { ReactNode } from "react"; +import { useContext, useEffect, useId, useRef } from "react"; +import { createPortal } from "react-dom"; + +import { DialogHelpContext, DialogHelpLabelsContext } from "../lib/dialog-help-context"; +import type { DialogHelpPresentation } from "../lib/dialog-help-presentation"; +import { CircleHelpIcon } from "../lib/icons"; + +/** Slot declarativo: los portales conservan contexto, traducciones y una única copia del contenido. */ +export function DialogHelp({ + title, + children, + presentation = "auto", +}: { + title: string; + children: ReactNode; + presentation?: DialogHelpPresentation; +}) { + const dialog = useContext(DialogHelpContext); + const labels = useContext(DialogHelpLabelsContext); + const contentId = useId(); + const titleId = useId(); + const headingRef = useRef(null); + const wasOpen = useRef(false); + const triggerRef = dialog?.triggerRef; + const open = dialog?.open ?? false; + const mode = dialog?.mode; + + // El destino cambia al redimensionar. El foco debe salir del formulario antes de ocultarlo. + useEffect(() => { + if (open && mode !== "popover") { + headingRef.current?.focus(); + } else if (!open && wasOpen.current) { + triggerRef?.current?.focus(); + } + wasOpen.current = open; + }, [open, mode, triggerRef]); + + if (!(dialog?.actionsHost && dialog.contentHost)) { + return null; + } + + const close = () => { + dialog.setPresentation(null); + dialog.triggerRef.current?.focus(); + }; + + const trigger = ( + + ); + + return ( + <> + {createPortal( + mode === "popover" || (!open && presentation === "compact") ? ( + dialog.setPresentation(next ? presentation : null)} + open={open && mode === "popover"} + > + + + {title} + {children} + + + + ) : ( + trigger + ), + dialog.actionsHost + )} + {createPortal( + open && mode !== "popover" ? ( +
+

+ {title} +

+
+ {children} +
+ +
+ ) : null, + dialog.contentHost + )} + + ); +} diff --git a/packages/rdx-ui/src/components/form-dialog.tsx b/packages/rdx-ui/src/components/form-dialog.tsx index 4abd58fd..5cc8763b 100644 --- a/packages/rdx-ui/src/components/form-dialog.tsx +++ b/packages/rdx-ui/src/components/form-dialog.tsx @@ -8,7 +8,11 @@ import { DialogTitle, } from "@repo/shadcn-ui/components/dialog"; import type { ComponentProps, ReactNode } from "react"; -import { createContext, useContext, useRef } from "react"; +import { createContext, useContext } from "react"; + +import { useDialogHelpLayout } from "../hooks/use-dialog-help-layout"; +import { DialogHelpContext, DialogHelpLabelsContext } from "../lib/dialog-help-context"; +import { XIcon } from "../lib/icons"; type FormDialogLayout = "content" | "stable" | "picker"; @@ -41,7 +45,21 @@ export function FormDialog({ finalFocus?: ComponentProps["finalFocus"]; initialFocus?: "title" | ComponentProps["initialFocus"]; }) { - const popupRef = useRef(null); + const help = useDialogHelpLayout(open, onOpenChange); + const { + popupRef, + triggerRef, + setPopupRef, + actionsHost, + setActionsHost, + contentHost, + setContentHost, + setPresentation, + helpOpen, + helpMode, + helpView, + } = help; + const labels = useContext(DialogHelpLabelsContext); const width = size ?? (wide ? "large" : "small"); const widths = { small: "sm:max-w-xl", @@ -53,9 +71,13 @@ export function FormDialog({ const height = layout === "content" ? "" : "h-[calc(100dvh-2rem)] sm:h-[min(85dvh,900px)]"; return ( - + - - - {title} - - -
- - {children} - -
- {!formActions && ( - - }> - {cancelLabel} - - - )} + +
+ + {title} + +
+
+
+ } + > + +
+ +
+
+ + {children} + +
+
+
+ {!(formActions || helpView) && ( + + }> + {cancelLabel} + + + )} +
); diff --git a/packages/rdx-ui/src/components/form-help.tsx b/packages/rdx-ui/src/components/form-help.tsx index 20ac8208..fc404de7 100644 --- a/packages/rdx-ui/src/components/form-help.tsx +++ b/packages/rdx-ui/src/components/form-help.tsx @@ -2,11 +2,7 @@ import type { ComponentProps } from "react"; import { ContextHelp } from "./context-help"; -/** Mantiene la ayuda general junto al pie, fuera del scroll principal del formulario. */ +/** Ayuda estructurada del formulario, presentada bajo demanda por FormDialog. */ export function FormHelp(props: Omit, "variant">) { - return ( -
- -
- ); + return ; } diff --git a/packages/rdx-ui/src/components/picker-help.tsx b/packages/rdx-ui/src/components/picker-help.tsx index fa879218..24acb746 100644 --- a/packages/rdx-ui/src/components/picker-help.tsx +++ b/packages/rdx-ui/src/components/picker-help.tsx @@ -1,15 +1,10 @@ -import { Button } from "@repo/shadcn-ui/components/button"; -import { - Popover, - PopoverContent, - PopoverDescription, - PopoverTitle, - PopoverTrigger, -} from "@repo/shadcn-ui/components/popover"; +import { useContext } from "react"; -import { InfoIcon } from "../lib/icons"; +import { DialogHelpContext } from "../lib/dialog-help-context"; -/** La ayuda completa se consulta junto al pie sin competir con la superficie de selección. */ +import { DialogHelp } from "./dialog-help"; + +/** Ayuda breve del selector; la cabecera y el modo responsive pertenecen a FormDialog. */ export function PickerHelp({ label, title, @@ -19,22 +14,21 @@ export function PickerHelp({ title: string; description: string; }) { + const dialog = useContext(DialogHelpContext); + + if (!dialog) { + return ( + + ); + } + return ( -
- - }> - - - {title} - {description} - - -
+ +

{label}

+

{description}

+
); } diff --git a/packages/rdx-ui/src/dialog-help.test.tsx b/packages/rdx-ui/src/dialog-help.test.tsx new file mode 100644 index 00000000..6ecc93f2 --- /dev/null +++ b/packages/rdx-ui/src/dialog-help.test.tsx @@ -0,0 +1,46 @@ +import assert from "node:assert/strict"; +import { test } from "node:test"; + +import { renderToStaticMarkup } from "react-dom/server"; + +import { ContextHelp } from "./components/context-help"; +import { getDialogHelpMode } from "./lib/dialog-help-presentation"; + +test("la ayuda breve respeta la capacidad efectiva, también con fuentes ampliadas", () => { + for (const width of [288, 343, 358, 479]) { + assert.equal(getDialogHelpMode("compact", width), "view"); + } + assert.equal(getDialogHelpMode("compact", 480), "popover"); + assert.equal(getDialogHelpMode("compact", 1200), "popover"); + assert.equal(getDialogHelpMode("compact", 768, 32), "view"); +}); + +test("auto y expanded reservan 40rem al formulario y 20rem al panel", () => { + for (const policy of ["auto", "expanded"] as const) { + for (const width of [288, 343, 358, 736, 768, 959]) { + assert.equal(getDialogHelpMode(policy, width), "view"); + } + assert.equal(getDialogHelpMode(policy, 960), "panel"); + assert.equal(getDialogHelpMode(policy, 1200), "panel"); + assert.equal(getDialogHelpMode(policy, 1200, 32), "view"); + } +}); + +test("el contenido inline conserva descripción, ámbito, ejemplos, orientación e icono", () => { + const props = { + title: "Ayuda", + description: "Descripción con tildes", + scopeNote: "Ámbito de compañía", + examplesLabel: "Ver ejemplos", + examples: "Ejemplo original", + guidance: "Instrucciones originales", + icon: Icono propio, + }; + const html = renderToStaticMarkup(); + + for (const text of Object.values(props).filter((value) => typeof value === "string")) { + assert.ok(html.includes(text)); + } + assert.ok(html.includes(" void) { + const popupRef = useRef(null); + const triggerRef = useRef(null); + const [popup, setPopup] = useState(null); + const [actionsHost, setActionsHost] = useState(null); + const [contentHost, setContentHost] = useState(null); + const [presentation, setPresentation] = useState(null); + const [dimensions, setDimensions] = useState({ width: 0, fontSize: 16 }); + const setPopupRef = useCallback((element: HTMLDivElement | null) => { + popupRef.current = element; + setPopup(element); + }, []); + const helpOpen = open && presentation !== null; + const helpMode = getDialogHelpMode(presentation ?? "auto", dimensions.width, dimensions.fontSize); + const helpView = helpOpen && helpMode === "view"; + const helpPanel = helpOpen && helpMode === "panel"; + + useEffect(() => { + if (!popup) { + return; + } + + const measure = () => + setDimensions({ + width: popup.getBoundingClientRect().width, + fontSize: Number.parseFloat(getComputedStyle(document.documentElement).fontSize), + }); + + measure(); + const observer = new ResizeObserver(measure); + + observer.observe(popup); + observer.observe(document.documentElement); + + return () => observer.disconnect(); + }, [popup]); + + const contentHostClassName = helpView + ? "absolute inset-0 overflow-hidden" + : helpPanel + ? "w-80 shrink-0 overflow-hidden border-l animate-in fade-in-0 slide-in-from-right-4 duration-200 ease-out motion-reduce:animate-none!" + : "hidden"; + + const changeOpen: NonNullable["onOpenChange"]> = ( + next, + details + ) => { + if (!next && helpOpen && details.reason === "escape-key") { + details.cancel(); + setPresentation(null); + triggerRef.current?.focus(); + + return; + } + + if (!next) { + setPresentation(null); + } + onOpenChange(next); + }; + + const completeOpenChange = (next: boolean) => { + if (!next) { + setPresentation(null); + } + }; + + return { + contentHostClassName, + popupRef, + triggerRef, + setPopupRef, + actionsHost, + setActionsHost, + contentHost, + setContentHost, + setPresentation, + helpOpen, + helpMode, + helpView, + helpPanel, + changeOpen, + completeOpenChange, + }; +} diff --git a/packages/rdx-ui/src/lib/dialog-help-context.ts b/packages/rdx-ui/src/lib/dialog-help-context.ts new file mode 100644 index 00000000..64343814 --- /dev/null +++ b/packages/rdx-ui/src/lib/dialog-help-context.ts @@ -0,0 +1,27 @@ +import type { RefObject } from "react"; +import { createContext } from "react"; + +import type { DialogHelpMode, DialogHelpPresentation } from "./dialog-help-presentation"; + +export type DialogHelpLabels = { + help: string; + closeHelp: string; + backToForm: string; + closeDialog: string; +}; + +export const DialogHelpLabelsContext = createContext({ + help: "Help", + closeHelp: "Close help", + backToForm: "Back to form", + closeDialog: "Close", +}); + +export const DialogHelpContext = createContext<{ + actionsHost: HTMLDivElement | null; + contentHost: HTMLDivElement | null; + mode: DialogHelpMode; + open: boolean; + setPresentation: (presentation: DialogHelpPresentation | null) => void; + triggerRef: RefObject; +} | null>(null); diff --git a/packages/rdx-ui/src/lib/dialog-help-presentation.ts b/packages/rdx-ui/src/lib/dialog-help-presentation.ts new file mode 100644 index 00000000..969015cb --- /dev/null +++ b/packages/rdx-ui/src/lib/dialog-help-presentation.ts @@ -0,0 +1,20 @@ +export type DialogHelpPresentation = "compact" | "expanded" | "auto"; + +export type DialogHelpMode = "popover" | "panel" | "view"; + +/** Auto representa contenido estructurado: reserva 40rem al formulario y 20rem a la ayuda. */ +export function getDialogHelpMode( + presentation: DialogHelpPresentation, + dialogWidth: number, + rootFontSize = 16 +): DialogHelpMode { + if (presentation === "compact" && dialogWidth >= 30 * rootFontSize) { + return "popover"; + } + + if (presentation !== "compact" && dialogWidth >= 60 * rootFontSize) { + return "panel"; + } + + return "view"; +} diff --git a/packages/rdx-ui/src/lib/icons.ts b/packages/rdx-ui/src/lib/icons.ts index b33ab5ef..ed49e17b 100644 --- a/packages/rdx-ui/src/lib/icons.ts +++ b/packages/rdx-ui/src/lib/icons.ts @@ -25,6 +25,7 @@ export { IconFolder as FolderIcon, IconGripVertical as GripVerticalIcon, IconHeadphones as HeadphonesIcon, + IconHelpCircle as CircleHelpIcon, IconHome as HomeIcon, IconIdBadge2 as WorkProfileIcon, IconInfoCircle as InfoIcon, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8661ee1f..9b1e7969 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -105,6 +105,9 @@ importers: '@metis/resources': specifier: workspace:* version: link:../../modules/resources + '@repo/rdx-ui': + specifier: workspace:* + version: link:../../packages/rdx-ui '@repo/shadcn-ui': specifier: workspace:* version: link:../../packages/shadcn-ui diff --git a/scripts/dialog-help-ui-audit.py b/scripts/dialog-help-ui-audit.py new file mode 100644 index 00000000..e3cdec4c --- /dev/null +++ b/scripts/dialog-help-ui-audit.py @@ -0,0 +1,133 @@ +"""Ayuda contextual: formularios reales, volumen sintético y navegador MCP fijado.""" +import json + + +def verify(context, fixture, password): + tool, browser, capture = context['tool'], context['browser'], context['capture'] + origin, directory = context['origin'], context['directory'] + checks, screenshots, measurements = [], [], [] + + def code(client, source): + print('Ayuda MCP: ' + source[:95], flush=True) + try: + result = context['result_json'](tool(client, 'browser_run_code_unsafe', { + 'code': 'async (page) => { page.setDefaultTimeout(15000); try { ' + source + ' return true; } catch(error) { return {helpCheckFailure: String(error.message).split(String.fromCharCode(10))[0]}; } }'})) + if isinstance(result, dict) and 'helpCheckFailure' in result: + raise RuntimeError('Comprobación de ayuda: ' + result['helpCheckFailure']) + return result + except RuntimeError: + capture(client, directory, 'dialog-help-failure') + raise + + with browser(context['config']) as client: + tool(client, 'browser_resize', {'width': 1440, 'height': 1000}) + tool(client, 'browser_navigate', {'url': origin + '/login'}) + code(client, "await page.getByRole('button',{name:'Entrar',exact:true}).waitFor();") + tool(client, 'browser_fill_form', {'fields': [ + {'name': 'Correo electrónico', 'type': 'textbox', 'target': 'input[type=email]', 'value': fixture['email']}, + {'name': 'Contraseña', 'type': 'textbox', 'target': 'input[type=password]', 'value': fixture['password']}]}) + tool(client, 'browser_click', {'target': 'button[type=submit]'}) + code(client, "await page.locator('[data-slot=workspace]').waitFor({timeout:30000});") + code(client, "const response=await page.request.get(" + json.dumps(origin + '/api/assets/' + fixture['otherCompanies'][0]) + "); if(!response.ok()||(await response.json()).assets.length!==0)throw Error('Aislamiento de compañía');") + checks.append('other-company-inventory-isolated') + tool(client, 'browser_navigate', {'url': origin + f'/companies/{fixture["company"]}/assets/{fixture["asset"]}'}) + code(client, "await page.getByRole('button',{name:'Editar activo',exact:true}).click(); await page.getByRole('dialog',{name:'Editar activo',exact:true}).waitFor();") + code(client, "const d=page.getByRole('dialog'); if(await d.getByRole('button',{name:'Ayuda',exact:true}).count()!==1 || await d.getByRole('button',{name:'Cerrar',exact:true}).count()!==1)throw Error('Cabecera duplicada'); if(await d.locator('aside').count())throw Error('Ayuda permanente'); await d.getByRole('combobox',{name:'Estado técnico',exact:true}).click(); await page.getByRole('option',{name:'Averiado',exact:true}).click(); await d.getByRole('textbox',{name:'Nombre',exact:true}).fill(' '); await d.getByRole('button',{name:'Guardar',exact:true}).click(); await d.getByRole('alert').first().waitFor(); await d.getByRole('textbox',{name:'Notas',exact:true}).fill('Borrador Álvarez conservado'); page.helpError=await d.getByRole('alert').first().textContent(); page.helpStatus=await d.getByRole('combobox',{name:'Estado técnico',exact:true}).textContent(); page.helpInput=await d.getByRole('textbox',{name:'Notas',exact:true}).elementHandle(); await d.getByRole('tab',{name:'Hardware',exact:true}).click();") + checks.extend(['ordinary-user', 'one-help-one-close', 'help-hidden-initially', 'real-invalid-form']) + for width in [320, 375, 390, 768, 1024, 1280, 1440]: + tool(client, 'browser_resize', {'width': width, 'height': 900}) + code(client, "const b=page.getByRole('button',{name:'Ayuda',exact:true}); await b.focus(); await b.press('Enter'); await page.locator('[data-slot=dialog-help]').waitFor();") + value = code(client, "return await page.evaluate(()=>{const d=document.querySelector('[data-slot=dialog-content]');const h=d.querySelector('button[aria-label=Ayuda]');const x=d.querySelector('button[aria-label=Cerrar]');const main=d.querySelector('[data-slot=form-dialog-content]');const help=d.querySelector('[data-slot=dialog-help]');const r=d.getBoundingClientRect();return {width:innerWidth,dialogWidth:r.width,mode:d.querySelector('[data-help-mode]').dataset.helpMode,hidden:main.inert,focus:document.activeElement===help.querySelector('h2'),overflow:d.scrollWidth>d.clientWidth+1||document.documentElement.scrollWidth>innerWidth+1,aligned:h.getBoundingClientRect().right<=x.getBoundingClientRect().left,formWidth:main.getBoundingClientRect().width}});") + if value['overflow'] or not value['aligned'] or not value['focus']: + raise RuntimeError('Geometría o foco incorrectos: ' + json.dumps(value)) + expected = 'panel' if value['dialogWidth'] >= 960 else 'view' + if value['mode'] != expected or (expected == 'view') != value['hidden']: + raise RuntimeError('Modo incorrecto: ' + json.dumps(value)) + if expected == 'panel' and value['formWidth'] < 640: + raise RuntimeError('Formulario excesivamente estrecho') + measurements.append(value) + screenshots.append(capture(client, directory, 'dialog-help-' + str(width))) + code(client, "await page.keyboard.press('Escape'); const d=page.getByRole('dialog'); await d.getByRole('textbox',{name:'Notas',exact:true}).waitFor(); if(await d.getByRole('textbox',{name:'Notas',exact:true}).inputValue()!=='Borrador Álvarez conservado'||await d.getByRole('textbox',{name:'Nombre',exact:true}).inputValue()!==' ')throw Error('Campos perdidos'); if(await d.getByRole('alert').first().textContent()!==page.helpError||await d.getByRole('combobox',{name:'Estado técnico',exact:true}).textContent()!==page.helpStatus)throw Error('Validación o selector perdidos'); if(!await d.getByRole('button',{name:'Ayuda',exact:true}).evaluate(e=>e===document.activeElement))throw Error('Foco no restaurado'); if(!await d.getByRole('textbox',{name:'Notas',exact:true}).evaluate((e,previous)=>e===previous,page.helpInput))throw Error('Formulario remontado'); if(await d.getByRole('tab',{name:'Hardware',exact:true}).getAttribute('aria-selected')!=='true')throw Error('Estado local de pestaña perdido');") + checks.extend(['seven-widths', 'panel-and-view', 'keyboard-enter-escape', 'focus-return', 'same-input-node', 'draft-errors-select-preserved', 'no-horizontal-overflow']) + # Cambiar de modo con ayuda abierta; comprobar retorno explícito y reflow equivalente al zoom. + motion = code(client, "await page.getByRole('button',{name:'Ayuda',exact:true}).click(); await page.locator('[data-help-mode=panel]').waitFor(); return await page.locator('[data-slot=dialog-help]').evaluate(help=>{const host=help.parentElement;const animation=host.getAnimations().find(a=>a.animationName==='enter');if(!animation)throw Error('Panel sin animación CSS');const duration=getComputedStyle(host).animationDuration;animation.pause();animation.currentTime=0;const start=Number(getComputedStyle(host).opacity);animation.currentTime=100;const middle=Number(getComputedStyle(host).opacity);animation.finish();const end=Number(getComputedStyle(host).opacity);if(duration!=='0.2s'||start!==0||middle<=0||middle>=1||end!==1)throw Error('Progreso de animación incorrecto');return {duration,start,middle,end}});") + measurements.append({'panelAnimation': motion}) + code(client, "await page.emulateMedia({reducedMotion:'reduce'}); if(await page.locator('[data-slot=dialog-help]').evaluate(e=>getComputedStyle(e.parentElement).animationName)!=='none')throw Error('Panel animado con movimiento reducido'); await page.emulateMedia({reducedMotion:'no-preference'});") + checks.extend(['panel-css-animation', 'panel-reduced-motion']) + tool(client, 'browser_resize', {'width': 390, 'height': 844}) + code(client, "await page.locator('[data-help-mode=view]').waitFor(); await page.getByRole('button',{name:'Volver al formulario',exact:true}).click(); await page.getByRole('textbox',{name:'Notas',exact:true}).waitFor();") + checks.append('resize-panel-to-view-back-action') + tool(client, 'browser_resize', {'width': 1440, 'height': 1000}) + code(client, "await page.getByRole('button',{name:'Ayuda',exact:true}).click(); await page.locator('[data-help-mode=panel]').waitFor(); await page.evaluate(()=>document.documentElement.classList.add('dark'));") + screenshots.append(capture(client, directory, 'dialog-help-dark')) + code(client, "await page.evaluate(()=>document.documentElement.classList.remove('dark')); await page.getByRole('button',{name:'Cerrar ayuda',exact:true}).click();") + checks.append('dark-mode-capture') + code(client, "await page.getByRole('button',{name:'Cerrar',exact:true}).click(); await page.getByRole('dialog').waitFor({state:'hidden'});") + checks.append('close-x') + # Selector real: la ayuda breve usa el Popover de Base UI dentro del diálogo modal. + tool(client, 'browser_navigate', {'url': origin + f'/companies/{fixture["company"]}/workstations/{fixture["softwareWorkstations"][0]}'}) + code(client, "await page.getByRole('button',{name:'Añadir activo al puesto',exact:true}).click(); await page.getByRole('dialog',{name:'Añadir activo al puesto',exact:true}).waitFor(); await page.locator('[data-quick-search]').fill('AUDIT-PORT-01'); await page.getByRole('radio',{name:'Seleccionar AUDIT-PORT-01',exact:true}).check(); await page.getByRole('button',{name:'Ayuda',exact:true}).click(); await page.locator('[data-slot=popover-content]').waitFor();") + code(client, "await page.emulateMedia({reducedMotion:'reduce'}); if(await page.locator('[data-slot=popover-content]').evaluate(e=>getComputedStyle(e).animationName)!=='none')throw Error('Animación con movimiento reducido'); await page.keyboard.press('Tab'); if(!await page.locator('[data-slot=dialog-content]').evaluate(e=>e.contains(document.activeElement))&&!await page.locator('[data-slot=popover-content]').evaluate(e=>e.contains(document.activeElement)))throw Error('Foco fuera de modal'); await page.emulateMedia({reducedMotion:'no-preference'}); if(!await page.locator('[data-slot=popover-content]').isVisible()){await page.getByRole('button',{name:'Ayuda',exact:true}).click(); await page.locator('[data-slot=popover-content]').waitFor();}") + checks.append('reduced-motion-and-popover-tab') + screenshots.append(capture(client, directory, 'dialog-help-popover')) + code(client, "await page.keyboard.press('Escape'); await page.locator('[data-slot=popover-content]').waitFor({state:'hidden'}); if(await page.getByRole('dialog').count()!==1)throw Error('Escape cerró selector'); if(await page.locator('[data-quick-search]').inputValue()!=='AUDIT-PORT-01')throw Error('Búsqueda perdida'); await page.getByRole('button',{name:'Ayuda',exact:true}).click(); await page.locator('[data-slot=popover-content]').waitFor();") + tool(client, 'browser_resize', {'width': 375, 'height': 844}) + code(client, "await page.locator('[data-help-mode=view]').waitFor(); await page.getByRole('button',{name:'Volver al formulario',exact:true}).click(); if(await page.locator('[data-quick-search]').inputValue()!=='AUDIT-PORT-01')throw Error('Búsqueda móvil perdida'); if(!await page.getByRole('button',{name:'Ayuda',exact:true}).evaluate(e=>e===document.activeElement))throw Error('Foco del selector móvil perdido'); if(!await page.getByRole('radio',{name:'Seleccionar AUDIT-PORT-01',exact:true}).isChecked())throw Error('Selección perdida'); await page.keyboard.press('Escape'); await page.getByRole('dialog').waitFor({state:'hidden'});") + checks.extend(['compact-popover', 'popover-escape-keeps-dialog', 'resize-popover-to-view', 'picker-search-preserved', 'picker-selection-preserved']) + tool(client, 'browser_resize', {'width': 1440, 'height': 1000}) + tool(client, 'browser_navigate', {'url': origin + f'/companies/{fixture["company"]}/assets'}) + code(client, "await page.getByRole('button',{name:'Columnas',exact:true}).click(); const d=page.getByRole('dialog'); await d.waitFor(); if(await d.getByRole('button',{name:'Ayuda',exact:true}).count())throw Error('Ayuda en diálogo sin ayuda'); if(await d.locator('[data-slot=dialog-header]').getByRole('button',{name:'Cerrar',exact:true}).count()!==1)throw Error('Cierre duplicado sin ayuda'); await page.keyboard.press('Escape'); await d.waitFor({state:'hidden'});") + checks.append('dialog-without-help') + tool(client, 'browser_navigate', {'url': origin + f'/companies/{fixture["company"]}/assets/{fixture["asset"]}'}) + code(client, "await page.getByRole('button',{name:'Editar activo',exact:true}).click(); await page.getByRole('dialog').waitFor(); await page.getByRole('button',{name:'Ayuda',exact:true}).click(); await page.locator('[data-help-mode=panel]').waitFor(); await page.locator('[data-slot=dialog-help] summary').click(); await page.locator('[data-slot=dialog-help] details[open]').waitFor();") + # Colores realmente calculados, convertidos mediante canvas; no se deduce contraste de tokens. + contrast_source = """return await page.evaluate(()=>{ + const surface=document.querySelector('[data-slot=dialog-content]'); + const canvas=document.createElement('canvas');canvas.width=canvas.height=1;const ctx=canvas.getContext('2d',{willReadFrequently:true}); + const paint=color=>{ctx.fillStyle=color;ctx.fillRect(0,0,1,1)}; + const lum=channels=>channels.slice(0,3).map(v=>{v/=255;return v<=.04045?v/12.92:((v+.055)/1.055)**2.4}).reduce((v,c,i)=>v+c*[.2126,.7152,.0722][i],0); + return [...surface.querySelectorAll('[data-slot=dialog-help] p,[data-slot=dialog-help] h2,[data-slot=dialog-help] summary')].map(e=>{ + ctx.clearRect(0,0,1,1);paint(getComputedStyle(surface).backgroundColor); + const ancestors=[];let current=e;while(current!==surface){ancestors.push(current);current=current.parentElement} + for(const parent of ancestors.reverse())paint(getComputedStyle(parent).backgroundColor); + const b=lum([...ctx.getImageData(0,0,1,1).data]);paint(getComputedStyle(e).color); + const a=lum([...ctx.getImageData(0,0,1,1).data]);return {tag:e.tagName,ratio:(Math.max(a,b)+.05)/(Math.min(a,b)+.05)}; + }); + });""" + contrast = {'light': code(client, contrast_source)} + code(client, "await page.evaluate(()=>document.documentElement.classList.add('dark'));") + contrast['dark'] = code(client, contrast_source) + for theme, items in contrast.items(): + if any(item['ratio'] < 4.5 for item in items): + raise RuntimeError('Contraste insuficiente ' + theme + ': ' + json.dumps(items)) + measurements.append({'contrast': contrast}) + code(client, "await page.evaluate(()=>document.documentElement.classList.remove('dark')); await page.getByRole('button',{name:'Cerrar ayuda',exact:true}).click();") + checks.append('computed-text-contrast-light-dark') + for scale, width in [(2, 720), (4, 360)]: + tool(client, 'browser_resize', {'width': width, 'height': 900}) + code(client, "await page.getByRole('button',{name:'Ayuda',exact:true}).click(); await page.locator('[data-help-mode=view]').waitFor(); if(await page.evaluate(()=>{const d=document.querySelector('[data-slot=dialog-content]');return d.scrollWidth>d.clientWidth+1}))throw Error('Reflow de zoom equivalente');") + screenshots.append(capture(client, directory, 'dialog-help-reflow-' + str(scale * 100))) + code(client, "await page.getByRole('button',{name:'Volver al formulario',exact:true}).click();") + tool(client, 'browser_resize', {'width': 1440, 'height': 1000}) + for scale in [2, 4]: + code(client, "await page.evaluate(scale=>document.documentElement.style.fontSize=(scale*100)+'%'," + str(scale) + "); await page.getByRole('button',{name:'Ayuda',exact:true}).click(); await page.locator('[data-help-mode=view]').waitFor(); if(await page.evaluate(()=>{const d=document.querySelector('[data-slot=dialog-content]');return d.scrollWidth>d.clientWidth+1}))throw Error('Texto ampliado'); if(await page.locator('[data-slot=dialog-help]>div').evaluate(e=>e.clientHeightdocument.documentElement.style.fontSize='');") + checks.append('reflow-and-text-200-400-not-browser-zoom') + tool(client, 'browser_resize', {'width': 390, 'height': 844}) + code(client, "await page.getByRole('dialog').locator('[data-slot=dialog-title]').evaluate(e=>e.textContent='Edición de un activo con un título muy largo, tildes y una descripción contextual para comprobar la cabecera'); await page.getByRole('button',{name:'Ayuda',exact:true}).click(); await page.locator('[data-slot=dialog-help]').waitFor(); if(await page.evaluate(()=>{const d=document.querySelector('[data-slot=dialog-content]');return d.scrollWidth>d.clientWidth+1}))throw Error('Título largo desborda');") + screenshots.append(capture(client, directory, 'dialog-help-long-title')) + code(client, "await page.keyboard.press('Escape'); await page.keyboard.press('Escape'); await page.getByRole('dialog').waitFor({state:'hidden'});") + checks.append('synthetic-long-title-stress') + tool(client, 'browser_resize', {'width': 1440, 'height': 1000}) + code(client, "await page.getByRole('button',{name:'Editar activo',exact:true}).click(); await page.getByRole('dialog').waitFor(); await page.getByRole('textbox',{name:'Notas',exact:true}).fill('Nota guardada tras consultar ayuda'); await page.getByRole('button',{name:'Ayuda',exact:true}).click(); await page.locator('[data-slot=dialog-help]').waitFor(); await page.getByRole('button',{name:'Cerrar ayuda',exact:true}).click(); await page.getByRole('button',{name:'Guardar',exact:true}).click(); await page.getByRole('dialog').waitFor({state:'hidden'}); await page.getByRole('button',{name:'Editar activo',exact:true}).click(); if(await page.getByRole('textbox',{name:'Notas',exact:true}).inputValue()!=='Nota guardada tras consultar ayuda')throw Error('Guardado tras ayuda'); await page.getByRole('textbox',{name:'Notas',exact:true}).fill('Cambio cancelado'); await page.getByRole('button',{name:'Cancelar',exact:true}).click(); await page.getByRole('dialog').waitFor({state:'hidden'}); await page.getByRole('button',{name:'Editar activo',exact:true}).click(); if(await page.getByRole('textbox',{name:'Notas',exact:true}).inputValue()!=='Nota guardada tras consultar ayuda')throw Error('Cancelar persistió'); await page.keyboard.press('Escape');") + checks.extend(['save-after-help', 'cancel-does-not-save']) + code(client, "await page.getByRole('button',{name:'Configuración',exact:true}).click(); await page.getByRole('combobox',{name:'Idioma',exact:true}).click(); await page.getByRole('option',{name:'English',exact:true}).click(); await page.getByRole('dialog').getByRole('button',{name:'Cancel',exact:true}).click(); await page.getByRole('button',{name:'Edit asset',exact:true}).click(); await page.getByRole('button',{name:'Help',exact:true}).click(); await page.getByRole('button',{name:'Close help',exact:true}).click(); await page.getByRole('dialog').getByRole('button',{name:'Close',exact:true}).click();") + checks.append('english-help-labels') + console = tool(client, 'browser_console_messages', {'level': 'warning'}) + (directory / 'dialog-help-console.json').write_text(json.dumps(console, ensure_ascii=False, indent=2) + '\n') + report = {'checks': checks, 'screenshots': screenshots, 'measurements': measurements, + 'database': 'metis_desk_test', 'companies': 15, + 'principal': {'people': 50, 'assets': 51, 'resources': 66, 'profiles': 50, 'workstations': 50}} + (directory / 'dialog-help-report.json').write_text(json.dumps(report, ensure_ascii=False, indent=2) + '\n') + print('Ayuda contextual verificada. Evidencias: ' + str(directory.relative_to(context['ROOT'])), flush=True) diff --git a/scripts/visual-audit.py b/scripts/visual-audit.py index 5a418e56..c2b9aca1 100644 --- a/scripts/visual-audit.py +++ b/scripts/visual-audit.py @@ -301,6 +301,7 @@ def verify_browser(config, origin, fixture, directory, state, logout): def main(): parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--dialog-help", action="store_true", help="Ayuda contextual en diálogos reales con datos sintéticos y MCP") parser.add_argument("--check", action="store_true", help="Verifica y retira el entorno; sin esta opción espera hasta Ctrl+C") parser.add_argument("--global-applications", action="store_true", help="Recorrido R2.3 con catálogo habilitado solo tras migración desechable") parser.add_argument("--company-applications", action="store_true", help="Listado y selectores globales/locales de compañía en una instalación desechable") @@ -347,6 +348,11 @@ def main(): "DB_NAME": "metis_desk_test", "DB_USER": "visual_audit", "DB_PASSWORD": database_password, "SESSION_SECRET": secrets.token_urlsafe(48), "NODE_ENV": "test", "APP_ORIGIN": origin, "PORT": str(api_port), "API_PROXY_TARGET": api.base} + if options.dialog_help: + runner = runpy.run_path(str(ROOT / "scripts/global-applications-ui-audit.py"))["run"] + verifier = runpy.run_path(str(ROOT / "scripts/dialog-help-ui-audit.py"))["verify"] + runner({**globals(), **locals()}, verifier=verifier) + return if options.company_applications: runner = runpy.run_path(str(ROOT / "scripts/global-applications-ui-audit.py"))["run"] verifier = runpy.run_path(str(ROOT / "scripts/company-applications-ui-audit.py"))["verify"]