diff --git a/docs/componentes-de-interfaz.md b/docs/componentes-de-interfaz.md index 03f488f0..5800f6bc 100644 --- a/docs/componentes-de-interfaz.md +++ b/docs/componentes-de-interfaz.md @@ -506,6 +506,20 @@ se añade una primitiva genérica de wizard sin otros consumidores. Véase [el comportamiento del alta](recursos-campos-y-plantillas.md#alta-de-recursos-en-tres-pasos). +### Vinculación del publicador tras el alta SMB + +`ResourceForm` entrega al callback `onSaved` el UUID y el recurso confirmado por el +servidor. `ResourcesPage.onCreated` permite completar una operación posterior al alta +antes de abrir la ficha; el consumidor llama a `onComplete` al finalizarla o cancelarla. +`CompanyResources` utiliza este contrato para abrir el `PublicationEditor` de Assets +cuando la carpeta recién creada está activa y tiene capacidad `network:smb-share`. + +El editor conserva la selección explícita del activo, búsqueda, paginación, errores e +invalidación de la relación existente. Informa de que la carpeta ya está guardada y ofrece +«Vincular más tarde». Reintentar la publicación no repite el alta del recurso. Las +ediciones, los recursos ordinarios y las altas inactivas siguen abriendo directamente +su ficha. Resources no importa Assets; Companies compone ambos módulos. + ### Presentación compacta del alta por pasos El alta de recursos y el alta de activos desde el inventario de compañía usan diff --git a/docs/integracion-visual-de-recursos-compartidos-smb.md b/docs/integracion-visual-de-recursos-compartidos-smb.md index 208eaeda..19157120 100644 --- a/docs/integracion-visual-de-recursos-compartidos-smb.md +++ b/docs/integracion-visual-de-recursos-compartidos-smb.md @@ -129,3 +129,65 @@ Incidencias previas indicadas por el encargo: dos expectativas de texto de Compa | `git diff --check` | Correcto. | Los comandos PNPM se ejecutaron con `--config.verify-deps-before-run=false` para utilizar las dependencias instaladas. No se instalaron librerías ni se cambiaron lockfiles. La última adaptación de los editores de Access fija su altura y coloca `EntityHelp` mediante `FormHelp` junto al pie; conserva los mismos textos y la variante original de ayuda para los demás consumidores. + +## Continuación del alta hacia el activo publicador + +Al guardar una carpeta SMB nueva y activa desde Recursos de la compañía, se cierra +el alta y se abre el editor de publicación existente, con el UUID del recurso ya +confirmado. El operador selecciona expresamente un activo de esa compañía; no se +infiere a partir del host UNC ni del campo documental Servidor. + +La carpeta y su publicación son dos operaciones explícitas. El editor explica que la +carpeta ya está guardada, conserva la selección ante errores y permite reintentar solo +la publicación. «Vincular más tarde», Escape o el cierre conservan la carpeta sin +publicador; la ficha mantiene la acción de vincularlo después. Las carpetas inactivas, +los demás recursos y las ediciones no abren esta continuación. + +La composición pertenece a `CompanyResources`. Resources entrega el resultado del alta +mediante `onCreated` y abre la ficha cuando el consumidor completa el flujo; Assets +conserva el editor, la selección remota, los comandos y la invalidación de publicaciones. +No se añaden tablas, endpoints, migraciones ni conversiones de carpetas históricas. + +Regresión reproducible con datos sintéticos y el MCP Docker fijado: + +```bash +python3 scripts/visual-audit.py --network-share-publisher --check +``` + +El preparador utiliza exclusivamente una instalación desechable de `metis_desk_test`, +con 15 compañías y una principal con 50 personas, 51 activos, 50 perfiles, 50 puestos y +66 recursos antes del recorrido. La prueba verifica alta, selección, reintento sin +duplicados, aplazamiento, edición, recursos inactivos y ordinarios y aislamiento de +publicaciones; genera capturas a 1440, 390 y 320 px. Los resultados de cada ejecución +se guardan en `publisher-report.json` dentro del directorio local de evidencias. + +### Validación de la continuación del alta (26 de octubre de 2026) + +- `python3 scripts/visual-audit.py --network-share-publisher --check`: 18 comprobaciones + correctas; 15 compañías, 50 personas, 51 activos, 50 perfiles, 50 puestos y 70 recursos + finales en la principal. Teclado, retorno de foco, búsqueda, paginación, búsqueda vacía, + error simulado de publicación, reintento con persistencia real, aplazamiento, edición, + altas inactivas/ordinarias y aislamiento. Capturas inspeccionadas a 1440 y 320 px; + también se generó y comprobó ausencia de desbordamiento a 390 px. Entorno retirado. +- `pnpm --filter @metis/resources --filter @metis/assets --filter @metis/companies + --filter @metis/web typecheck`: correcto. `pnpm build`: correcto. +- Suites completas con `--test-isolation=none`: Resources 124/124; Assets 150/150; + Companies 310/312. Persisten las expectativas de «Personas (1)» y del texto de acceso + de personas en `companies-page.test.tsx`, ajenas a este cambio. +- `pnpm test`, repetido con acceso a loopback: termina con esos dos fallos de Companies. + La primera ejecución de Resources dentro del sandbox falló al abrir un puerto local; + su suite completa pasó al repetir con acceso a loopback. +- `pnpm typecheck`: falla en `packages/shadcn-ui/src/components/calendar.tsx:165` + (TS2375), fuera del cambio y sin modificar vendor. `pnpm lint` y `pnpm check` globales + fallan en archivos ajenos, incluidos ejemplos de skills y código de fotos de personas. +- Biome y ESLint dirigidos a todos los archivos TS/TSX/JSON del cambio: correctos; + sintaxis de los dos scripts Python y `git diff --check`: correctos. + `python3 scripts/visual-audit.test.py`: 10/10. + +Skills consultadas: `rdx-solid` para reutilizar ownership y contratos; `ux-inputs-and-forms` +para explicar el guardado y el aplazamiento; `shadcn` para componer el editor existente; +`vercel-react-best-practices` para coordinar el avance desde el callback sin efectos ni +copias de estado remoto. Los auditores especializados referidos por la skill de formularios +no están instalados; las comprobaciones citadas son directas mediante Playwright MCP, +no una certificación de accesibilidad. No se han ejecutado suites MariaDB adicionales: +los comandos existentes se han ejercitado a través de HTTP en el entorno desechable. diff --git a/modules/assets/src/common/locales/en.json b/modules/assets/src/common/locales/en.json index 2c568e1e..6ef315c0 100644 --- a/modules/assets/src/common/locales/en.json +++ b/modules/assets/src/common/locales/en.json @@ -628,6 +628,8 @@ "publisher": "Publishing asset" }, "shareUi": { + "linkLater": "Link later", + "resourceCreated": "The share “{{name}}” has been saved. Select its publishing asset. If you leave this for later, you can link it from the share details.", "published": "Published network shares", "publisher": "Published by", "publish": "Assign publication", diff --git a/modules/assets/src/common/locales/es.json b/modules/assets/src/common/locales/es.json index 96c7aff1..84a6a041 100644 --- a/modules/assets/src/common/locales/es.json +++ b/modules/assets/src/common/locales/es.json @@ -628,6 +628,8 @@ "publisher": "Activo publicador" }, "shareUi": { + "linkLater": "Vincular más tarde", + "resourceCreated": "La carpeta «{{name}}» está guardada. Selecciona el activo que la publica. Si lo dejas para más tarde, podrás vincularlo desde la ficha de la carpeta.", "published": "Recursos compartidos publicados", "publisher": "Publicado por", "publish": "Asignar publicación", diff --git a/modules/assets/src/web/index.ts b/modules/assets/src/web/index.ts index fe63915a..2df5ac2e 100644 --- a/modules/assets/src/web/index.ts +++ b/modules/assets/src/web/index.ts @@ -16,6 +16,7 @@ export { } from "./infrastructure/shared/api/infrastructure-api"; export { getInfrastructureItems } from "./infrastructure/shared/helpers/infrastructure-items"; export { NetworkSharePublications } from "./network-share-publications/detail/ui/network-share-publications"; +export { PublicationEditor } from "./network-share-publications/manage/ui/publication-editor"; export { PublicationPicker } from "./network-share-publications/select/ui/publication-picker"; export { assetsEnglish, assetsSpanish } from "./shared"; export { inventoryApi } from "./shared/api"; diff --git a/modules/assets/src/web/network-share-publications/manage/ui/publication-editor.tsx b/modules/assets/src/web/network-share-publications/manage/ui/publication-editor.tsx index 695742a9..3b830bbd 100644 --- a/modules/assets/src/web/network-share-publications/manage/ui/publication-editor.tsx +++ b/modules/assets/src/web/network-share-publications/manage/ui/publication-editor.tsx @@ -22,8 +22,10 @@ export function PublicationEditor({ companyId, operation, onClose, + createdResourceName, }: { companyId: string; + createdResourceName?: string; operation: PublicationOperation; onClose: () => void; }) { @@ -95,7 +97,7 @@ export function PublicationEditor({ return ( <> { @@ -107,6 +109,11 @@ export function PublicationEditor({ title={t(`shareUi.${operation.action}`)} > + {createdResourceName && ( +

+ {t("shareUi.resourceCreated", { name: createdResourceName })} +

+ )} {operation.action !== "end" && (
diff --git a/modules/companies/src/web/companies/detail/ui/company-section.tsx b/modules/companies/src/web/companies/detail/ui/company-section.tsx index 757e8e7f..7f46fb18 100644 --- a/modules/companies/src/web/companies/detail/ui/company-section.tsx +++ b/modules/companies/src/web/companies/detail/ui/company-section.tsx @@ -1,6 +1,6 @@ import { AssetCatalogPage, InfrastructurePage } from "@metis/assets/client"; import { useBrowserSearchParams, useCreateNavigation } from "@metis/core/client"; -import { ResourceCatalogPage, ResourcesPage } from "@metis/resources/client"; +import { ResourceCatalogPage } from "@metis/resources/client"; import type { ComponentProps } from "react"; import type { CompanyDTO } from "../../../../common"; @@ -14,14 +14,13 @@ import { PeopleSection } from "../../../people"; import { PrinterConfigurationsPage } from "../../../printer-configurations"; import { ProcessesPage } from "../../../processes"; import { RemoteConnectionsPage } from "../../../remote-connections"; -import { CompanyResourceRelatedContent } from "../../../resources/detail/ui/company-resource-related-content"; +import { CompanyResources } from "../../../resources/list/ui/company-resources"; import { SiteSection } from "../../../sites"; import { TeleworkAuthorizationsPage } from "../../../telework-authorizations"; import { WorkProfilesPage } from "../../../work-profiles"; import { CatalogOverview } from "../../../workspace/catalogs/ui/catalog-overview"; import type { CompanyTab } from "../../shared/types"; -import { CompanyEntityContext } from "./company-entity-context"; import { CompanyInventory } from "./company-inventory"; import { CompanyOverview } from "./company-overview"; @@ -156,20 +155,12 @@ export function CompanySection({ ); case "resources": return ( - ( - - - - )} + key={company.id} + layoutReady={layoutReady} /> ); case "resource-catalog": diff --git a/modules/companies/src/web/resources/list/ui/company-resources.tsx b/modules/companies/src/web/resources/list/ui/company-resources.tsx new file mode 100644 index 00000000..a97233dc --- /dev/null +++ b/modules/companies/src/web/resources/list/ui/company-resources.tsx @@ -0,0 +1,70 @@ +import { PublicationEditor } from "@metis/assets/client"; +import { ResourcesPage } from "@metis/resources/client"; +import { useState } from "react"; + +import type { CompanyDTO } from "../../../../common"; +import { CompanyEntityContext } from "../../../companies/detail/ui/company-entity-context"; +import { CompanyResourceRelatedContent } from "../../detail/ui/company-resource-related-content"; + +export function CompanyResources({ + company, + initialCreate, + initialId, + layoutReady, +}: { + company: CompanyDTO; + initialCreate?: boolean | undefined; + initialId?: string | undefined; + layoutReady: boolean; +}) { + const [publication, setPublication] = useState<{ + resourceId: string; + resourceName: string; + onComplete: () => void; + }>(); + + return ( + <> + { + if (resource.networkShare?.capability === "network:smb-share" && resource.values.active) { + setPublication({ + resourceId: resource.id, + resourceName: resource.values.name, + onComplete, + }); + } else { + onComplete(); + } + }} + relatedContent={(resource) => ( + + + + )} + /> + {publication && ( + { + setPublication(undefined); + publication.onComplete(); + }} + operation={{ + action: "publish", + anchor: { type: "resources:resource", id: publication.resourceId }, + }} + /> + )} + + ); +} diff --git a/modules/resources/src/web/resources/list/types/resources-page-props.ts b/modules/resources/src/web/resources/list/types/resources-page-props.ts index f94f7759..d8a19f19 100644 --- a/modules/resources/src/web/resources/list/types/resources-page-props.ts +++ b/modules/resources/src/web/resources/list/types/resources-page-props.ts @@ -4,6 +4,8 @@ import type { ResourceDTO } from "../../../../common"; export type ResourcesPageProps = { companyId: string; + /** Completa el flujo posterior al alta antes de abrir la ficha del recurso guardado. */ + onCreated?: (resource: ResourceDTO, onComplete: () => void) => void; relatedContent?: (resource: ResourceDTO) => ReactNode; initialId?: string | undefined; initialCreate?: boolean | undefined; diff --git a/modules/resources/src/web/resources/list/ui/resources-page.tsx b/modules/resources/src/web/resources/list/ui/resources-page.tsx index f18c95ed..e4fa315f 100644 --- a/modules/resources/src/web/resources/list/ui/resources-page.tsx +++ b/modules/resources/src/web/resources/list/ui/resources-page.tsx @@ -27,7 +27,7 @@ import { ResourceInventoryTable } from "./resource-inventory-table"; export function ResourcesPage(props: ResourcesPageProps) { const controller = useResourcesPageController(props); - const { companyId, relatedContent } = props; + const { companyId, relatedContent, onCreated } = props; const { editor, setEditor, @@ -213,9 +213,13 @@ export function ResourcesPage(props: ResourcesPageProps) { initialKind={creationKind} items={catalog.data.items} onClose={() => setEditor(undefined)} - onSaved={(id) => { + onSaved={(id, resource) => { setEditor(undefined); - selectResource(id); + if (editor === "new" && onCreated) { + onCreated(resource, () => selectResource(id)); + } else { + selectResource(id); + } }} resource={edited} /> diff --git a/modules/resources/src/web/resources/shared/controllers/use-resource-form-controller.ts b/modules/resources/src/web/resources/shared/controllers/use-resource-form-controller.ts index 4ce39498..553ca32b 100644 --- a/modules/resources/src/web/resources/shared/controllers/use-resource-form-controller.ts +++ b/modules/resources/src/web/resources/shared/controllers/use-resource-form-controller.ts @@ -89,7 +89,7 @@ export function useResourceFormController({ ) ); if (mounted.current) { - onSaved(response.resource.id); + onSaved(response.resource.id, response.resource); } }, }); diff --git a/modules/resources/src/web/resources/shared/types/resource-form-props.ts b/modules/resources/src/web/resources/shared/types/resource-form-props.ts index 8d576625..66ec01dc 100644 --- a/modules/resources/src/web/resources/shared/types/resource-form-props.ts +++ b/modules/resources/src/web/resources/shared/types/resource-form-props.ts @@ -5,7 +5,7 @@ export type ResourceFormProps = { companyId: string; resource?: ResourceDTO | undefined; items: readonly CatalogItemDTO[]; - onSaved: (id: string) => void; + onSaved: (id: string, resource: ResourceDTO) => void; allowQuickCreate?: boolean; creationLayout?: "single-page" | "steps"; initialKind?: "mailbox" | "access" | "application" | "certificate" | "support" | "os" | undefined; diff --git a/modules/resources/src/web/resources/shared/ui/resource-editor-dialog.tsx b/modules/resources/src/web/resources/shared/ui/resource-editor-dialog.tsx index 2b4c9a32..b29eac4f 100644 --- a/modules/resources/src/web/resources/shared/ui/resource-editor-dialog.tsx +++ b/modules/resources/src/web/resources/shared/ui/resource-editor-dialog.tsx @@ -19,7 +19,7 @@ export function ResourceEditorDialog({ items: ComponentProps["items"]; onClose: () => void; finalFocus?: ComponentProps["finalFocus"]; - onSaved: (id: string) => void; + onSaved: ComponentProps["onSaved"]; initialKind?: ComponentProps["initialKind"]; }) { const { t } = useTranslation("resources"); diff --git a/scripts/network-share-publisher-ui-audit.py b/scripts/network-share-publisher-ui-audit.py new file mode 100644 index 00000000..baacac74 --- /dev/null +++ b/scripts/network-share-publisher-ui-audit.py @@ -0,0 +1,78 @@ +"""Alta SMB y publicación: regresión con HTTP real en el entorno visual desechable.""" +import json + + +def verify(context, fixture, admin_password): + tool, browser, capture = context['tool'], context['browser'], context['capture'] + origin, directory = context['origin'], context['directory'] + company = fixture['company'] + base = origin + '/companies/' + company + '/resources' + screenshots, checks = [], [] + + def code(client, source): + result = context['result_json'](tool(client, 'browser_run_code_unsafe', { + 'code': 'async (page) => { page.setDefaultTimeout(15000); try { ' + source + ' return true; } catch(error) { return {failure: String(error.message)}; } }'})) + if isinstance(result, dict) and 'failure' in result: + raise RuntimeError(result['failure']) + return result + + def create(client, name, active=True, smb=True): + print('Publicador SMB: ' + name, flush=True) + tool(client, 'browser_navigate', {'url': base}) + code(client, "await page.getByRole('button',{name:'Nuevo recurso',exact:true}).click(); await page.getByRole('combobox',{name:'Plantilla de recurso',exact:true}).click(); await page.getByRole('option',{name:" + json.dumps('Carpeta compartida' if smb else 'Licencia de software') + ",exact:true}).click(); await page.getByRole('button',{name:'Siguiente',exact:true}).click();") + if smb: + unc = '\\\\alias-documental\\' + name.replace(' ', '-') + code(client, "await page.getByRole('textbox',{name:'Ruta UNC',exact:true}).fill(" + json.dumps(unc) + ");") + code(client, "await page.getByRole('button',{name:'Siguiente',exact:true}).click(); await page.getByRole('textbox',{name:'Nombre',exact:true}).fill(" + json.dumps(name) + ");") + if not active: + code(client, "await page.getByRole('combobox',{name:'Estado',exact:true}).click(); await page.getByRole('option',{name:'Inactivo',exact:true}).click();") + code(client, "await page.getByRole('button',{name:'Guardar',exact:true}).click(); await page.getByRole('dialog').filter({has:page.getByRole('heading',{name:'Nuevo recurso',exact:true})}).waitFor({state:'hidden'});") + + with browser(context['config']) as client: + try: + 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});") + # Las comprobaciones y escrituras siguientes apuntan solo al contenedor sintético. + create(client, 'AUDIT-SMB Álvarez vinculado') + code(client, "await page.getByRole('dialog').filter({has:page.getByRole('heading',{name:'Asignar publicación',exact:true})}).waitFor(); await page.getByRole('status').filter({hasText:'está guardada'}).waitFor(); if(!await page.getByRole('button',{name:'Confirmar',exact:true}).isDisabled())throw Error('Confirmación sin activo'); await page.getByRole('button',{name:'Seleccionar activo publicador',exact:true}).focus(); await page.keyboard.press('Enter'); await page.getByRole('dialog').filter({has:page.getByRole('heading',{name:'Seleccionar activo publicador',exact:true})}).waitFor();") + checks.extend(['ordinary-user', 'create-smb-opens-publication', 'saved-resource-explained', 'no-default-publisher', 'keyboard-open-picker']) + code(client, "await page.getByRole('button',{name:'Siguiente',exact:true}).click(); await page.getByRole('button',{name:'Anterior',exact:true}).click(); await page.getByRole('textbox',{name:'Buscar',exact:true}).fill('NO-EXISTE-SMB'); await page.getByText('No hay elementos disponibles para esta búsqueda.',{exact:true}).waitFor(); await page.getByRole('textbox',{name:'Buscar',exact:true}).fill('AUDIT-R23-PORT-50'); await page.getByRole('radio',{name:'Seleccionar: AUDIT-R23-PORT-50',exact:true}).check(); await page.getByRole('button',{name:'Seleccionar',exact:true}).click(); await page.getByRole('dialog').filter({has:page.getByRole('heading',{name:'Seleccionar activo publicador',exact:true})}).waitFor({state:'hidden'}); if(!await page.getByRole('button',{name:'Seleccionar activo publicador',exact:true}).evaluate(e=>e===document.activeElement))throw Error('Foco no restaurado');") + checks.extend(['asset-pagination', 'empty-search-recovery', 'asset-search', 'picker-focus-return']) + for width in [1440, 390, 320]: + tool(client, 'browser_resize', {'width': width, 'height': 900}) + code(client, "if(await page.getByRole('dialog').filter({has:page.getByRole('heading',{name:'Asignar publicación',exact:true})}).evaluate(e=>e.scrollWidth>e.clientWidth+1))throw Error('Desbordamiento horizontal');") + screenshots.append(capture(client, directory, 'publisher-create-' + str(width))) + tool(client, 'browser_resize', {'width': 1440, 'height': 1000}) + pattern = '**/api/asset-network-share-publications/' + company + code(client, "await page.route(" + json.dumps(pattern) + ",route=>route.request().method()==='POST'?route.fulfill({status:409,contentType:'application/json',json:{code:'ASSET_INACTIVE'}}):route.continue()); await page.getByRole('button',{name:'Confirmar',exact:true}).click(); await page.getByRole('alert').waitFor(); if(await page.getByRole('button',{name:'Seleccionar activo publicador',exact:true}).textContent()!=='AUDIT-R23-PORT-50')throw Error('Selección perdida'); await page.unroute(" + json.dumps(pattern) + "); await page.getByRole('button',{name:'Confirmar',exact:true}).click(); await page.getByRole('dialog').filter({has:page.getByRole('heading',{name:'Asignar publicación',exact:true})}).waitFor({state:'hidden'});") + outcome = code(client, "const resources=await (await page.request.get('" + origin + "/api/resources/" + company + "')).json(); const found=resources.resources.filter(r=>r.values.name==='AUDIT-SMB Álvarez vinculado'); if(found.length!==1)throw Error('Recurso duplicado'); const publications=await (await page.request.get('" + origin + "/api/asset-network-share-publications/" + company + "?resourceId='+found[0].id)).json(); if(publications.publications.length!==1||publications.publications[0].assetId!==" + json.dumps(fixture['softwareAssets'][-1]) + ")throw Error('Publicador incorrecto'); return {id:found[0].id};") + checks.extend(['publication-error-retains-selection', 'retry-no-duplicate-resource', 'real-publication-persisted', 'alias-does-not-infer-asset']) + tool(client, 'browser_navigate', {'url': base + '/' + outcome['id']}) + code(client, "await page.getByRole('complementary').getByRole('button',{name:'Editar',exact:true}).click(); await page.getByRole('dialog').filter({has:page.getByRole('heading',{name:'Editar recurso',exact:true})}).waitFor(); await page.getByRole('button',{name:'Guardar',exact:true}).click(); await page.getByRole('dialog').filter({has:page.getByRole('heading',{name:'Editar recurso',exact:true})}).waitFor({state:'hidden'}); if(await page.getByRole('dialog').filter({has:page.getByRole('heading',{name:'Asignar publicación',exact:true})}).count())throw Error('Edición repite alta');") + checks.append('editing-does-not-reopen-publication') + create(client, 'AUDIT-SMB pendiente') + code(client, "await page.getByRole('button',{name:'Vincular más tarde',exact:true}).click(); await page.getByRole('dialog').filter({has:page.getByRole('heading',{name:'Asignar publicación',exact:true})}).waitFor({state:'hidden'}); const resources=await (await page.request.get('" + origin + "/api/resources/" + company + "')).json(); const r=resources.resources.find(r=>r.values.name==='AUDIT-SMB pendiente'); if(!r)throw Error('Recurso perdido'); const p=await (await page.request.get('" + origin + "/api/asset-network-share-publications/" + company + "?resourceId='+r.id)).json(); if(p.publications.length)throw Error('Publicación no confirmada');") + checks.append('link-later-keeps-unpublished-resource') + create(client, 'AUDIT-SMB inactivo', active=False) + code(client, "if(await page.getByRole('dialog').filter({has:page.getByRole('heading',{name:'Asignar publicación',exact:true})}).count())throw Error('Alta inactiva ofrece publicación');") + create(client, 'AUDIT-LIC licencia', smb=False) + code(client, "if(await page.getByRole('dialog').filter({has:page.getByRole('heading',{name:'Asignar publicación',exact:true})}).count())throw Error('Recurso ordinario ofrece publicación');") + checks.extend(['inactive-share-skips-publication', 'ordinary-resource-skips-publication']) + code(client, "const response=await page.request.get('" + origin + "/api/asset-network-share-publications/" + fixture['otherCompanies'][0] + "'); if(!response.ok()||(await response.json()).publications.length)throw Error('Aislamiento de compañía');") + checks.append('other-company-has-no-publications') + except Exception: + capture(client, directory, 'publisher-failure') + snapshot = tool(client, 'browser_snapshot', {}) + (directory / 'publisher-failure.json').write_text(json.dumps(snapshot, ensure_ascii=False)) + raise + report = {'checks': checks, 'screenshots': screenshots, 'companies': 15, + 'principal': {'people': 50, 'assets': 51, 'resources': 70, 'profiles': 50, 'workstations': 50}, + 'database': 'metis_desk_test', 'mcp': 'Docker stdio fijado'} + (directory / 'publisher-report.json').write_text(json.dumps(report, ensure_ascii=False, indent=2) + '\n') + print('Publicador SMB verificado: ' + str(len(checks)) + ' comprobaciones. Evidencias: ' + str(directory), flush=True) diff --git a/scripts/visual-audit.py b/scripts/visual-audit.py index bf9dbd10..06080812 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("--network-share-publisher", action="store_true", help="Alta de carpeta SMB y vinculación del activo publicador con datos sintéticos") 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") @@ -349,6 +350,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.network_share_publisher: + runner = runpy.run_path(str(ROOT / "scripts/global-applications-ui-audit.py"))["run"] + verifier = runpy.run_path(str(ROOT / "scripts/network-share-publisher-ui-audit.py"))["verify"] + runner({**globals(), **locals()}, verifier=verifier) + return 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"]