# Integración visual de dominios Windows · Paso 2.4 ## Uso y responsabilidades Los dominios se consultan en **Recursos**, dentro de su plantilla existente. La identificación funcional es `resource.directory.capability === "directory:domain"`; renombrar una plantilla no cambia la clasificación. La ficha conserva nombre, características DNS/NetBIOS, estado, observaciones e historial propios de Resource. Companies compone `CompanyResourceRelatedContent` mediante `relatedContent`, tanto para el panel lateral como para el diálogo existente. El adaptador integra el componente público `DomainDirectoryMemberships` de Assets y los enlaces de accesos. Los recursos ordinarios mantienen su comportamiento y los buzones conservan `MailboxRelations`. Resources no depende de Assets ni de implementaciones privadas de Companies. En la ficha del dominio hay **una única tabla paginada**, con columnas de activo, plantilla, estado del activo, rol y vigencia de la pertenencia. Se adopta la alternativa A: miembros y controladores comparten el cursor del proveedor; el resumen de controladores indica expresamente que corresponde a la página visible. No hay paginaciones independientes, filtrados que oculten miembros de otras páginas ni contadores globales inferidos de veinte filas. La clasificación usa `roleCode`. Cada nombre disponible enlaza al activo por su UUID. Para asociar o administrar una pertenencia desde el dominio se abre el inventario o la ficha del activo. Los comandos y sus diálogos permanecen en Assets; no se duplican en Resources. La sección **Dominio Windows** del activo está integrada en `AssetDetail`, compartido por página, diálogo y panel lateral. En el panel adopta un AccordionItem de la raíz `multiple` existente. Permite: - Ver dominio, DNS, rol y estado, y abrir el recurso mediante su UUID. - Asociar un equipo compatible, eligiendo manualmente un dominio activo. - Cambiar rol: solo los servidores ofrecen controlador; también se respeta la clasificación técnica explícita de infraestructura del paso 2.2. - Trasladar mediante **un único comando atómico** existente; el diálogo explica y confirma el cierre de la pertenencia anterior al guardar. - Finalizar con confirmación, conservando los datos y el historial. - Consultar los periodos vigentes y finalizados mediante el proveedor tipado. La tabla histórica muestra el rol y estado de cada periodo, no un timeline de cada revisión intermedia del rol: el contrato tipado no proporciona fechas ni revisiones. Las revisiones completas siguen conservadas y accesibles por la API especializada del paso 2.2. No se inventan fechas, códigos de inventario ni metadatos ausentes. ## HTTP y servicios Se añaden tres GET acotados bajo el prefijo ya existente: | Ruta | Finalidad | | --- | --- | | `/api/asset-directory-memberships/:companyId/relationships` | Página del proveedor tipado y resúmenes del extremo relacionado por lote | | `/api/asset-directory-memberships/:companyId/domains` | Opciones activas de Resources, búsqueda por nombre/DNS y cursor | | `/api/asset-directory-memberships/:companyId/:assetId/eligibility` | Clasificación de un activo con la política existente | La primera exige `typeId=assets:directory-membership`, `anchorType=assets:asset` o `resources:resource`, `anchorId`, `direction`, `includeEnded=true|false` y `pageSize=1..100`, más cursor opcional. La respuesta es `{page: {items, nextCursor}, summaries}`; conserva `roleCode` y el orden de correspondencia entre filas y resúmenes. El adaptador llama a `runtime.relationships.memberships` y al resolutor propietario correspondiente. No reconstruye las consultas del proveedor ni hidrata inventarios completos. Las opciones se obtienen del servicio público `resources.services.directoryOptions`: una proyección de Recursos y su tabla de dominios, limitada por compañía y estado, con búsqueda parametrizada y `LIMIT 21` para devolver hasta veinte opciones y continuación. El cursor de este selector es una identidad existente tratada como opaca por la UI; no es el cursor HMAC de relaciones. Cambiar la búsqueda reinicia su página. No se descarga el catálogo completo para filtrar dominios en el navegador. La compatibilidad proyecta únicamente el activo solicitado, su plantilla y la clasificación explícita de servidor de la última revisión de infraestructura. Reutiliza `directoryMembershipEligibility`; la validación definitiva continúa en el comando, incluyendo dominio activo, estado del activo, concurrencia y compañía. Las escrituras siguen usando, sin alterar su contrato: ```text POST /api/asset-directory-memberships/:companyId/:assetId join | role | transfer | end ``` No hay un escritor genérico de relaciones, nueva tabla, migración o dependencia externa en este paso. Los contratos del paquete común y los cursores HMAC del paso 2.3 permanecen intactos. ## Sesión, errores y actualización Las rutas nuevas usan el middleware público de autenticación. Derivan usuario y `authVersion` de la sesión y obtienen un contexto con la fábrica existente, sin copiar su credencial. La compañía se valida antes de leer datos propietarios; cada servicio conserva la autorización del contexto. No se acepta `operatorId` en query o body de estas lecturas. Los tres GET se registran de forma que no intercepten los comandos anteriores. Errores tipados: `INVALID_QUERY` → 400, `FORBIDDEN` → 403 y `UNAVAILABLE` → 404; la ausencia de sesión produce 401 mediante el middleware existente. Los errores inesperados siguen el diagnóstico central del servidor. Las referencias ajenas no revelan nombres. Sigue existiendo acceso general de operadores autenticados a las compañías atendidas, **sin ACL granular por compañía**. El cliente valida las respuestas mediante schemas públicos y utiliza `csrfFetch` para las escrituras. Muestra carga, vacío real, error y datos disponibles de forma diferenciada, conserva errores funcionales y no presenta guardados optimistas. Tras éxito o conflicto invalida exclusivamente las consultas de pertenencias de esa compañía, que incluyen ambos extremos e historial; ante errores también marca como obsoletas sus opciones de dominio para no reutilizar un dominio retirado. El formulario solo se cierra tras confirmar el éxito y actualizar las consultas. El diálogo captura el UUID de la pertenencia al abrir la operación. Un refresco concurrente no lo sustituye por el de un periodo nuevo: el backend puede rechazar la operación obsoleta. No se realizan finalización y alta separadas para trasladar. Los listados de relaciones actualizan cada sesenta segundos en primer plano mediante `useListQuery`. El navegador conserva una pila de cursores opacos para anterior y siguiente. «Primera página» reinicia la consulta inicial y sus datos, recuperándose de cursores invalidados por un reinicio del servidor. No se fingen páginas totales ni se reordena localmente una sola página del servidor como si fuera el conjunto. ## Cuentas y grupos Los enlaces abren el `AccessWorkspace` existente con: ```text /companies/:companyId/access?kind=account&environmentId=:environmentId /companies/:companyId/access?kind=group&environmentId=:environmentId ``` `environmentId` procede de la asociación estructurada del dominio. Las rutas validan UUID, parámetros repetidos y ámbito permitido. El listado existente combina el filtro con búsqueda y tipo, indica el entorno y permite quitarlo. Cambiar a una sección incompatible retira el filtro; modificarlo reinicia la paginación. No se copian cuentas/grupos ni se añaden formularios AD, consultas SQL privadas o permisos del directorio como autorizaciones de Metis. ## Componentes y archivos Creados: - `packages/rdx-ui/src/components/cursor-pagination.tsx`: navegación genérica sin dependencia de entidades, DTO o consultas. - `modules/assets/src/common/directory-membership-read.ts`: contratos HTTP acotados. - `modules/resources/src/common/directory-domain-options.ts`: contrato del selector. - `modules/assets/src/api/infrastructure/persistence/directory-membership-eligibility-reader.ts`. - `modules/resources/src/api/infrastructure/persistence/directory-domain-options-reader.ts`. - `apps/server/src/infrastructure/http/directory-relationship.routes.ts` y `apps/server/src/windows-domains-ui.integration.test.ts`. - En `modules/assets/src/web/directory-memberships/`: API y query keys, hooks de consultas/mutación, secciones de activo y dominio, tabla de página, selector de dominios y diálogos de edición/finalización. Cada componente principal tiene su fichero; las operaciones están separadas en `detail`, `manage`, `select` y `shared`. - `modules/companies/src/web/resources/detail/ui/company-resource-related-content.tsx`. - Predicado y prueba de filtro de entorno, junto al listado de AccessWorkspace. Modificados principalmente: - Composition roots de Assets/Resources y runtime HTTP del servidor: inyección y rutas. - `AssetDetail`, `AssetSelectionPanel`, `ResourceDetailDialog` y `CompanySection`: composición contextual y altura estable del diálogo de recurso. - Ruta, controlador y listado de AccessWorkspace, con sus pruebas de navegación. - `ConfirmDeleteDialog`: opción `layout="stable"` utilizada por la confirmación nueva; los consumidores anteriores conservan su presentación. - Exports públicos, traducciones es/en, manifiestos de pruebas, fixture compartido y documentación. No se modifica código vendor ni se revierten los pasos anteriores. Reutilizados: `DetailPanel`, `DetailSection`, Accordion `multiple`, `EmptyState`, `EntityStatus`, Table/Button/Input de shadcn/ui, `FormDialog`, `FormBody`, `FormHelp`, `FormActions`, `FormLabel`, `RequiredIndicator`, `ConfirmDeleteDialog`, `BrowserLink`, TanStack Query y las operaciones públicas anteriores. No se usa AssetPickerDialog: la gestión se inicia desde el activo seleccionado y no necesita descargar otro inventario. El selector de dominios tiene búsqueda remota y selección mediante botones accesibles, sin añadir paginación local sobre su página remota. ## Verificación y límites Las suites usan Node y `tsx` con `--test-isolation=none`, conservando todos los ficheros de cada script. La integración solo utiliza `metis_desk_test`, datos con UUID propios y limpieza por compañía, sin tocar bases de desarrollo o producción. - Assets: 126/126; Resources: 53/53; RDX UI: 52/52. - Companies: 171/173; pasan las nuevas pruebas de ruta y filtrado. Persisten las dos expectativas previas de texto en `companies-page.test.tsx:180` y `:210`. - Integración conjunta de pasos 2.1–2.4: 15/15. Incluye consultas directas/inversas, roles, cursor ajeno/inválido, límites, sesión, compañía inexistente, datos ajenos, CSRF, asociación, cambio de rol, traslado, finalización repetida y conflictos. El selector se comprueba con más de veinte dominios, búsqueda y exclusión de dominios inactivos y de otra compañía. - Se comprueban renderizado es/en, enlaces UUID, estados, ausencia de filtraciones de nombres no disponibles y transporte de un único comando atómico con CSRF. - Typecheck correcto en Assets, Resources, Companies, servidor, web y RDX UI. - Biome y ESLint correctos en los 41 ficheros TypeScript del paso; Biome también comprueba tres manifiestos y cuatro ficheros de traducciones. - Builds de servidor y web correctos; `git diff --check` sin incidencias. - Grafo workspace verificado: 13 paquetes, sin ciclos. - Se ha reproducido TS2375 en el fichero vendor sin modificar `packages/shadcn-ui/src/components/calendar.tsx:165`. No se corrige aquí. - El build web conserva el aviso de chunks mayores de 500 kB. No se ha realizado una sesión interactiva de navegador: el entorno no dispone de herramienta de navegador ni ejecutable Chromium. Los diálogos usan el foco inicial de búsqueda y la restitución de foco de FormDialog/Base UI; los tests SSR no sustituyen una comprobación manual de teclado, viewport móvil y tema oscuro. Los cursores HMAC siguen siendo por instancia y sin instantánea entre peticiones. No hay sincronización LDAP, automatización AD ni gestión SMB. La siguiente fase puede reutilizar contratos, resolución por lotes y navegación por cursor, pero las reglas y comandos de SMB deberán seguir en su propietario; no están implementados. ### Comandos de verificación Desde la raíz, usando Node 24 y el PNPM fijado: ```bash pnpm --config.verify-deps-before-run=false --filter @metis/assets --filter @metis/resources --filter @metis/companies --filter @metis/server --filter @metis/web --filter @repo/rdx-ui typecheck pnpm --config.verify-deps-before-run=false --filter @metis/server test:windows-domains-ui pnpm --config.verify-deps-before-run=false --filter @metis/server build pnpm --config.verify-deps-before-run=false --filter @metis/web build git diff --check ``` La integración conjunta se ejecutó desde `apps/server`: ```bash DB_NAME=metis_desk_test NODE_ENV=test node --env-file=../../.env --import tsx \ --test --test-isolation=none --test-concurrency=1 \ src/directory-domains.integration.test.ts \ src/asset-directory-memberships.integration.test.ts \ src/typed-relationships.integration.test.ts \ src/windows-domains-ui.integration.test.ts ``` Las suites completas de Assets, Resources, Companies y RDX UI se ejecutaron con sus listas originales de ficheros más los casos nuevos, añadiendo `--test-isolation=none` al runner para enumerar los casos reales. No se eliminaron pruebas ni se cambiaron las expectativas previas fallidas. No se afirma que los checks globales del monorepo estén verdes.