@xenon-device-management/xenon 2.13.2 → 2.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -8
- package/lib/package.json +5 -5
- package/lib/public/assets/{AnnotationOverlay-BXruClDk.js → AnnotationOverlay-Btq3dZRI.js} +1 -1
- package/lib/public/assets/{ApiKeyGate-qTS4LPLC.js → ApiKeyGate-C9OoAgg_.js} +1 -1
- package/lib/public/assets/DeviceMosaicView-DBC_xczB.js +2 -0
- package/lib/public/assets/{EmptyState-Cr17PdDv.js → EmptyState-DqWU2-vM.js} +1 -1
- package/lib/public/assets/{FieldGroup-Bvn08svv.js → FieldGroup-CveQ3xrA.js} +1 -1
- package/lib/public/assets/{FilterMenu-DDSO8aDf.js → FilterMenu-BfKDSfHz.js} +1 -1
- package/lib/public/assets/{Menu-B-Qn2X3T.js → Menu-CtdCybzA.js} +1 -1
- package/lib/public/assets/{Modal-Cg9V5JYU.js → Modal-Bsvj1tm2.js} +1 -1
- package/lib/public/assets/{RecordingPage-BTUs9VTW.js → RecordingPage-CRofOiyx.js} +1 -1
- package/lib/public/assets/{RecordingsPage-DYcfxITK.js → RecordingsPage-jNQHb2IR.js} +2 -2
- package/lib/public/assets/{SegmentedControl-Zy7suhT5.js → SegmentedControl-C8HtTha-.js} +1 -1
- package/lib/public/assets/{SettingCard-CWYmycoe.js → SettingCard-D8K1LZNo.js} +1 -1
- package/lib/public/assets/{Table-io9fwa98.js → Table-zNTX92Pn.js} +1 -1
- package/lib/public/assets/{activity-Bdf_uIWl.js → activity-CJ8LwZq9.js} +1 -1
- package/lib/public/assets/ai-settings-CgxQZqrc.js +16 -0
- package/lib/public/assets/{api-keys-CPqtVpm3.js → api-keys-ECpo-zTo.js} +1 -1
- package/lib/public/assets/{apps-BgZan0H9.js → apps-BjGymyf3.js} +2 -2
- package/lib/public/assets/{arrow-left-B27ckB31.js → arrow-left-BWfCbeHP.js} +1 -1
- package/lib/public/assets/{arrow-right-DOF8ITCz.js → arrow-right-DC7UGf_n.js} +1 -1
- package/lib/public/assets/{arrow-up-right-eFG5KTFS.js → arrow-up-right-BOjwybDk.js} +1 -1
- package/lib/public/assets/{auth-shell-DlC3_27j.js → auth-shell-CxMTo9aH.js} +2 -2
- package/lib/public/assets/{builds-page-BuXpPfKo.js → builds-page-DOCm4HkA.js} +1 -1
- package/lib/public/assets/{button-fIW8owMI.js → button-Cg08-EMT.js} +1 -1
- package/lib/public/assets/{calendar-WNjmrF-a.js → calendar-CA1ikLJs.js} +1 -1
- package/lib/public/assets/{check-BdTh1Sq7.js → check-BMTaH72W.js} +1 -1
- package/lib/public/assets/{chevron-right-B4BKlsqk.js → chevron-right-B2QOv9Nx.js} +1 -1
- package/lib/public/assets/{circle-check-zJU80_EF.js → circle-check-CYXBoUqT.js} +1 -1
- package/lib/public/assets/{circle-x-5Uwquo0h.js → circle-x-DTwnSAnl.js} +1 -1
- package/lib/public/assets/{clock-BtUprDsZ.js → clock-C8_HViQP.js} +1 -1
- package/lib/public/assets/{copy-DGSbZI3u.js → copy-Be7LNMXV.js} +1 -1
- package/lib/public/assets/derive-UnLRp4r3.js +1 -0
- package/lib/public/assets/device-explorer-BOqtfGXz.js +282 -0
- package/lib/public/assets/{download-WG6l7BgD.js → download-B56lwxWP.js} +1 -1
- package/lib/public/assets/forgot-password-CCU6bzLn.js +1 -0
- package/lib/public/assets/h264Stream-C-OtmIuk.js +26 -0
- package/lib/public/assets/index-B9KQ6LsY.js +15 -0
- package/lib/public/assets/{index-DI1fGIyj.js → index-BOxe43p1.js} +5 -5
- package/lib/public/assets/{index-HDQVe1gi.js → index-CfxsUslp.js} +1 -1
- package/lib/public/assets/{input-BjDRKCea.js → input-CZHFjbSg.js} +1 -1
- package/lib/public/assets/{line-chart-C0zdgd0v.js → line-chart-Gl_Dz3X_.js} +1 -1
- package/lib/public/assets/{list-checks-BdW5CjM8.js → list-checks-CCgjeyGY.js} +1 -1
- package/lib/public/assets/{lock-Dy3L4I8E.js → lock-CkOwgDcx.js} +1 -1
- package/lib/public/assets/{login-BHbY9CLG.js → login-D5cc4w7M.js} +1 -1
- package/lib/public/assets/maintenance-settings-t7qr3fvX.js +6 -0
- package/lib/public/assets/{monitor-8xjkeHHi.js → monitor-DEJixKQO.js} +1 -1
- package/lib/public/assets/{mouse-pointer-2-CXdKMXQ8.js → mouse-pointer-2-CtOrk4M8.js} +1 -1
- package/lib/public/assets/{network-Cj8JOKON.js → network-Dii9qdvk.js} +1 -1
- package/lib/public/assets/{overview-CTg7F0im.js → overview-rolAACrO.js} +1 -1
- package/lib/public/assets/{page-header--aTyJXq6.js → page-header-COQZxJfR.js} +1 -1
- package/lib/public/assets/{play-B-FodgHV.js → play-DrN1Se_R.js} +1 -1
- package/lib/public/assets/{plus-CsIk10Cq.js → plus-5aq-9uRx.js} +1 -1
- package/lib/public/assets/{profile-page-DkFKkMfl.js → profile-page-6i5kY6kF.js} +1 -1
- package/lib/public/assets/{reset-password-I2-zqLlI.js → reset-password-sp2IBrc7.js} +1 -1
- package/lib/public/assets/runbook-page-DJDjeopU.js +253 -0
- package/lib/public/assets/{select-DvCSv25z.js → select-Bpyn7rWO.js} +1 -1
- package/lib/public/assets/{selector-detail-redirect-BLVfMJs_.js → selector-detail-redirect-BH74vmKv.js} +1 -1
- package/lib/public/assets/{selector-health-page-oqadRokn.js → selector-health-page-Cw_g218H.js} +2 -2
- package/lib/public/assets/session-detail-page-AZJE6XXE.js +4 -0
- package/lib/public/assets/settings-C3uyGhyc.js +6 -0
- package/lib/public/assets/{settings-Dep_3kbA.css → settings-K8UHWMdI.css} +1 -1
- package/lib/public/assets/{stat-tile-DV9Ix-4O.js → stat-tile-xVqAKIlI.js} +1 -1
- package/lib/public/assets/{tablet-6b_SEN2X.js → tablet-BxUScAeO.js} +1 -1
- package/lib/public/assets/{teams-BjZyfuHh.js → teams-B5ljGkY2.js} +1 -1
- package/lib/public/assets/{trash-2-CTZpPSdd.js → trash-2-B701i8sy.js} +1 -1
- package/lib/public/assets/{upload-P3XzwQUb.js → upload-C2gr44RS.js} +1 -1
- package/lib/public/assets/{use-builds-data-1cOCMf-z.js → use-builds-data-NeQOFEoy.js} +1 -1
- package/lib/public/assets/{use-password-reset-mode-DgF9w088.js → use-password-reset-mode-DEzJq5ef.js} +1 -1
- package/lib/public/assets/{users-DMF9Mk5H.js → users-D_vv_yz4.js} +1 -1
- package/lib/public/assets/{users-CiNTKUiz.js → users-MY6eNe6s.js} +1 -1
- package/lib/public/assets/{video-off-CHp1KU_T.js → video-off-OK1lNZQn.js} +1 -1
- package/lib/public/assets/webhook-settings-BU-C9lBf.js +1 -0
- package/lib/public/assets/{zap-DRz0tDq5.js → zap-DO9ry1iz.js} +1 -1
- package/lib/public/index.html +1 -1
- package/lib/schema.json +23 -25
- package/lib/src/XenonCapabilityManager.js +22 -5
- package/lib/src/app/dashboardPluginLink.js +117 -0
- package/lib/src/app/index.js +13 -34
- package/lib/src/app/openapi/control.yaml +73 -20
- package/lib/src/app/openapi/grid.yaml +31 -7
- package/lib/src/app/openapi/identity.yaml +103 -31
- package/lib/src/app/openapi/platform.yaml +165 -39
- package/lib/src/app/openapi/sessions.yaml +10 -3
- package/lib/src/app/routers/apikeys.js +14 -2
- package/lib/src/app/routers/auth.js +29 -5
- package/lib/src/app/routers/control.js +20 -2
- package/lib/src/app/routers/dashboard.js +69 -26
- package/lib/src/app/routers/grid.js +37 -13
- package/lib/src/app/routers/profile.js +21 -0
- package/lib/src/app/routers/streamType.js +7 -2
- package/lib/src/app/routers/users.js +10 -5
- package/lib/src/app/routers/webhook.js +8 -4
- package/lib/src/app/swagger.js +2 -0
- package/lib/src/app/ws/h264StreamWs.js +10 -1
- package/lib/src/config.js +51 -5
- package/lib/src/dashboard/commands.js +185 -141
- package/lib/src/dashboard/event-manager.js +88 -67
- package/lib/src/dashboard/services/failure-analysis-service.js +136 -45
- package/lib/src/dashboard/sessionCommandSummary.js +39 -0
- package/lib/src/data-service/appleFamily.js +43 -0
- package/lib/src/data-service/db.js +11 -0
- package/lib/src/data-service/device-service.js +6 -1
- package/lib/src/data-service/device-store.js +76 -3
- package/lib/src/data-service/deviceClaims.js +37 -0
- package/lib/src/data-service/deviceFieldOwners.js +25 -11
- package/lib/src/data-service/deviceSettings.js +79 -0
- package/lib/src/data-service/prisma-store.js +169 -15
- package/lib/src/data-service/web-config-service.js +50 -92
- package/lib/src/device-managers/AndroidDeviceManager.js +9 -31
- package/lib/src/device-managers/android/AndroidH264StreamService.js +69 -20
- package/lib/src/device-managers/android/H264Multiplexer.js +38 -4
- package/lib/src/device-managers/ios/WDAClient.js +44 -43
- package/lib/src/device-managers/shellCommands.js +96 -0
- package/lib/src/device-utils.js +114 -40
- package/lib/src/gateway/defaultGateway.js +1 -1
- package/lib/src/gateway/forwardToNode.js +2 -0
- package/lib/src/gateway/healReport.js +87 -0
- package/lib/src/gateway/sessionGateway.js +12 -2
- package/lib/src/generated/client/edge.js +18 -3
- package/lib/src/generated/client/index-browser.js +15 -0
- package/lib/src/generated/client/index.d.ts +2086 -379
- package/lib/src/generated/client/index.js +18 -3
- package/lib/src/generated/client/package.json +1 -1
- package/lib/src/generated/client/schema.prisma +38 -7
- package/lib/src/generated/client/wasm.js +15 -0
- package/lib/src/interceptors/CommandInterceptor.js +271 -188
- package/lib/src/interceptors/xenonScripts.js +55 -0
- package/lib/src/interfaces/IPluginArgs.js +1 -2
- package/lib/src/middleware/authMiddleware.js +15 -3
- package/lib/src/middleware/commandCaller.js +3 -3
- package/lib/src/middleware/verifyCredential.js +58 -1
- package/lib/src/plugin.js +23 -4
- package/lib/src/prisma.js +1 -1
- package/lib/src/scripts/database-check.js +124 -0
- package/lib/src/services/AICommandService.js +9 -2
- package/lib/src/services/AIService.js +281 -32
- package/lib/src/services/CleanupService.js +6 -1
- package/lib/src/services/EmailService.js +29 -0
- package/lib/src/services/InterceptorService.js +70 -13
- package/lib/src/services/NetworkConditioningService.js +85 -88
- package/lib/src/services/NotificationService.js +36 -29
- package/lib/src/services/OrphanSweeper.js +7 -0
- package/lib/src/services/PortAllocator.js +5 -0
- package/lib/src/services/SelectorVerificationJob.js +15 -4
- package/lib/src/services/ServerManager.js +105 -9
- package/lib/src/services/SessionLifecycleService.js +133 -75
- package/lib/src/services/ShutdownCoordinator.js +33 -0
- package/lib/src/services/SocketServer.js +19 -3
- package/lib/src/services/TeamService.js +20 -3
- package/lib/src/services/bug-report/video-slice.js +4 -1
- package/lib/src/services/device-access/leaseOverride.js +7 -5
- package/lib/src/services/healing/FuzzyXmlHealingProvider.js +51 -0
- package/lib/src/services/healing/HealEtalonService.js +3 -2
- package/lib/src/services/healing/HealingOrchestrator.js +85 -71
- package/lib/src/services/healing/OcrHealingProvider.js +41 -64
- package/lib/src/services/healing/ResilioTreeHealingProvider.js +41 -50
- package/lib/src/services/healing/VisualAiHealingProvider.js +1 -1
- package/lib/src/services/healing/fingerprintIdentity.js +126 -0
- package/lib/src/services/healing/resilioPath.js +274 -0
- package/lib/src/services/interceptor/AndroidProxyAdapter.js +18 -0
- package/lib/src/services/interceptor/interceptorOptions.js +44 -0
- package/lib/src/services/logcat/SessionDeviceLogs.js +271 -0
- package/lib/src/services/logcat/deviceLogBook.js +163 -0
- package/lib/src/services/network/AndroidRadios.js +71 -0
- package/lib/src/services/network/PhoneNetworkLedger.js +184 -0
- package/lib/src/services/network/PhoneNetworkRestore.js +330 -0
- package/lib/src/services/network/adbForPhone.js +60 -0
- package/lib/src/services/network/phoneNetworkLock.js +20 -0
- package/lib/src/services/network/xenonProxy.js +107 -0
- package/lib/src/services/ocr/ocrData.js +118 -0
- package/lib/src/services/ocr/vendor/LICENSE-tessdata.txt +202 -0
- package/lib/src/services/ocr/vendor/eng.traineddata.gz +0 -0
- package/lib/src/services/omni-vision/OmniVisionService.js +132 -95
- package/lib/src/services/omni-vision/VisionAssertionService.js +28 -25
- package/lib/src/services/omni-vision/ocrTextMatch.js +105 -0
- package/lib/src/services/omni-vision/screenScale.js +68 -0
- package/lib/src/services/passwordResetLink.js +50 -9
- package/lib/src/services/passwordResetMode.js +3 -1
- package/lib/src/services/publicUrl.js +45 -0
- package/lib/src/services/recording/concurrency-gate.js +12 -3
- package/lib/src/services/session/sessionIdentity.js +5 -3
- package/lib/src/services/session/shutdownReason.js +26 -0
- package/lib/src/services/settings/SelfHealingSwitch.js +70 -0
- package/lib/src/services/settings/aiEngineSettings.js +219 -0
- package/lib/src/services/settings/labSettings.js +149 -0
- package/lib/src/services/token/mintedLifetime.js +51 -0
- package/lib/src/services/webhookEvents.js +173 -0
- package/lib/src/sessions/SessionManager.js +8 -2
- package/lib/src/sessions/sessionMemory.js +25 -0
- package/lib/test/helpers/fake-adb.js +58 -0
- package/lib/test/helpers/fake-ai-provider.js +188 -0
- package/lib/test/helpers/fake-device-table.js +63 -24
- package/lib/test/helpers/loki-stores.js +33 -0
- package/lib/test/helpers/page-sources.js +164 -0
- package/lib/test/helpers/scratch-database.js +58 -13
- package/lib/test/integration/audit-ingest.spec.js +10 -10
- package/lib/test/integration/auth-flow.spec.js +9 -14
- package/lib/test/integration/auth-rate-limit.spec.js +6 -3
- package/lib/test/integration/bug-report-route.spec.js +22 -2
- package/lib/test/integration/forgot-password-rate-limit.spec.js +6 -3
- package/lib/test/integration/forgot-password.spec.js +17 -18
- package/lib/test/integration/legacy-deprecation-headers.spec.js +5 -2
- package/lib/test/integration/migration.spec.js +6 -1
- package/lib/test/integration/node-pair-auth-rest.spec.js +5 -9
- package/lib/test/integration/ports-router.spec.js +4 -4
- package/lib/test/integration/prisma-device-store-add-devices.spec.js +2 -3
- package/lib/test/integration/prisma-device-store-team-filter.spec.js +2 -5
- package/lib/test/integration/profile-tokens.spec.js +10 -16
- package/lib/test/integration/recordings-router.spec.js +16 -16
- package/lib/test/integration/reset-revokes-sessions.spec.js +4 -9
- package/lib/test/integration/role-matrix.spec.js +4 -9
- package/lib/test/integration/sdk-leases-router.spec.js +9 -9
- package/lib/test/integration/sdk-version.spec.js +2 -2
- package/lib/test/integration/selector-health-endpoint.spec.js +5 -5
- package/lib/test/integration/team-membership-migration.spec.js +73 -12
- package/lib/test/integration/team-visibility-apps.spec.js +15 -22
- package/lib/test/integration/team-visibility-control.spec.js +96 -69
- package/lib/test/integration/team-visibility-dashboard.spec.js +7 -23
- package/lib/test/integration/team-visibility-grid.spec.js +16 -15
- package/lib/test/integration/team-visibility-leases.spec.js +8 -11
- package/lib/test/integration/team-visibility-recordings.spec.js +6 -19
- package/lib/test/integration/team-visibility-sessions.spec.js +6 -19
- package/lib/test/integration/users-flow.spec.js +7 -12
- package/lib/test/integration/users-lockout.spec.js +5 -3
- package/lib/test/unit/EmailService.test.js +66 -0
- package/lib/test/unit/ResilioTreeHealingProvider.test.js +46 -19
- package/lib/test/unit/TeamService.test.js +3 -0
- package/lib/test/unit/XenonCapabilityManager.spec.js +4 -2
- package/lib/test/unit/ai-call-time-limit.spec.js +130 -0
- package/lib/test/unit/ai-engine-settings.spec.js +338 -0
- package/lib/test/unit/ai-provider-switch.spec.js +130 -0
- package/lib/test/unit/ai-rate-limit.spec.js +126 -0
- package/lib/test/unit/android-h264-viewers.spec.js +272 -0
- package/lib/test/unit/android-preview-hold.spec.js +3 -2
- package/lib/test/unit/apikeys-router.spec.js +84 -0
- package/lib/test/unit/apple-family.spec.js +36 -0
- package/lib/test/unit/authMiddleware.bearer.spec.js +9 -0
- package/lib/test/unit/authMiddleware.credentialLifetime.spec.js +149 -0
- package/lib/test/unit/bug-report/video-slice.spec.js +19 -0
- package/lib/test/unit/cleanup-settings-db.spec.js +103 -0
- package/lib/test/unit/cleanup-settings.spec.js +244 -0
- package/lib/test/unit/dashboard-api-paths.spec.js +101 -0
- package/lib/test/unit/dashboard-commands.spec.js +249 -0
- package/lib/test/unit/dashboard-events-device-logs.spec.js +207 -0
- package/lib/test/unit/dashboard-events-team-scope.spec.js +9 -6
- package/lib/test/unit/dashboard-plugin-link.spec.js +213 -0
- package/lib/test/unit/database-check.spec.js +108 -0
- package/lib/test/unit/default-plugin-args.spec.js +33 -0
- package/lib/test/unit/device-log-book.spec.js +180 -0
- package/lib/test/unit/device-offline-webhook.spec.js +81 -0
- package/lib/test/unit/device-setting-columns.spec.js +66 -0
- package/lib/test/unit/device-settings-loki.spec.js +87 -0
- package/lib/test/unit/device-settings-survive.spec.js +555 -0
- package/lib/test/unit/device-sync-lost-update.spec.js +5 -0
- package/lib/test/unit/emulator-boot.spec.js +91 -0
- package/lib/test/unit/enable-dashboard-option.spec.js +57 -0
- package/lib/test/unit/event-log-session-data.spec.js +244 -0
- package/lib/test/unit/failure-analysis.spec.js +182 -0
- package/lib/test/unit/failure-categories.spec.js +128 -0
- package/lib/test/unit/fingerprint-identity.spec.js +433 -0
- package/lib/test/unit/h264-multiplexer.spec.js +46 -0
- package/lib/test/unit/heal-report.spec.js +71 -0
- package/lib/test/unit/healing-instrumentation.spec.js +6 -1
- package/lib/test/unit/healing-tier-gate.spec.js +73 -29
- package/lib/test/unit/healing-tiers-every-session.spec.js +219 -0
- package/lib/test/unit/hub-node-gateway-appium-server.spec.js +219 -0
- package/lib/test/unit/hub-node-same-host.spec.js +5 -0
- package/lib/test/unit/interceptor-visual-elements.spec.js +192 -0
- package/lib/test/unit/iphone-ipad-only.spec.js +132 -0
- package/lib/test/unit/json-logging-option.spec.js +74 -0
- package/lib/test/unit/lab-settings.spec.js +218 -0
- package/lib/test/unit/lease/lease-override.spec.js +23 -6
- package/lib/test/unit/lease/session-lease-proof.spec.js +16 -4
- package/lib/test/unit/max-sessions.spec.js +240 -0
- package/lib/test/unit/ocr-healing-tier-real-ocr.spec.js +113 -0
- package/lib/test/unit/ocr-healing-tier.spec.js +122 -0
- package/lib/test/unit/ocr-offline.spec.js +213 -0
- package/lib/test/unit/ocr-text-match.spec.js +44 -0
- package/lib/test/unit/omni-virtual-element-store.spec.js +301 -0
- package/lib/test/unit/omni-vision-analyze-screen.spec.js +72 -0
- package/lib/test/unit/omni-vision-ios-points.spec.js +159 -0
- package/lib/test/unit/omni-vision-tap-text.spec.js +129 -0
- package/lib/test/unit/omni-vision.spec.js +6 -8
- package/lib/test/unit/package-engines.spec.js +27 -0
- package/lib/test/unit/partial-config-file.spec.js +14 -0
- package/lib/test/unit/passwordResetMode.test.js +10 -2
- package/lib/test/unit/phone-network/boot-clean-up.spec.js +64 -0
- package/lib/test/unit/phone-network/interceptor-options.spec.js +77 -0
- package/lib/test/unit/phone-network/interceptor-proxy-restore.spec.js +145 -0
- package/lib/test/unit/phone-network/network-conditioning.spec.js +145 -0
- package/lib/test/unit/phone-network/phone-network-restore.spec.js +246 -0
- package/lib/test/unit/phone-network/session-end-paths.spec.js +182 -0
- package/lib/test/unit/phone-network/session-start.spec.js +110 -0
- package/lib/test/unit/phone-network/xenon-proxy.spec.js +69 -0
- package/lib/test/unit/plugin.spec.js +3 -3
- package/lib/test/unit/profile-router.test.js +122 -1
- package/lib/test/unit/queue-requester.spec.js +1 -1
- package/lib/test/unit/recording-options.spec.js +181 -0
- package/lib/test/unit/reset-link-address.spec.js +266 -0
- package/lib/test/unit/reset-link.test.js +10 -4
- package/lib/test/unit/resilio-path-healing.spec.js +270 -0
- package/lib/test/unit/selector-learning-every-session.spec.js +179 -0
- package/lib/test/unit/selector-verification-clean-build.spec.js +130 -0
- package/lib/test/unit/selector-verification-job.spec.js +13 -13
- package/lib/test/unit/self-healing-boot.spec.js +80 -0
- package/lib/test/unit/self-healing-setting.spec.js +273 -0
- package/lib/test/unit/session-app-resolution.spec.js +5 -1
- package/lib/test/unit/session-attribution-wiring.spec.js +41 -13
- package/lib/test/unit/session-create-credential-owner.spec.js +242 -0
- package/lib/test/unit/session-credential-strip.spec.js +1 -1
- package/lib/test/unit/session-device-logs.spec.js +347 -0
- package/lib/test/unit/session-end-failure-analysis.spec.js +133 -0
- package/lib/test/unit/session-failed-webhook.spec.js +170 -0
- package/lib/test/unit/session-log-indexes.spec.js +64 -3
- package/lib/test/unit/shell-commands.spec.js +133 -0
- package/lib/test/unit/socketServer.scopedEmit.spec.js +2 -0
- package/lib/test/unit/socketServer.sessionCookie.spec.js +2 -1
- package/lib/test/unit/stream-start-pipelines.spec.js +264 -0
- package/lib/test/unit/stream-status-type.spec.js +4 -0
- package/lib/test/unit/token-mint-limits.spec.js +165 -0
- package/lib/test/unit/unexpected-shutdown-reason.spec.js +163 -0
- package/lib/test/unit/verify-credential.spec.js +33 -0
- package/lib/test/unit/vision-assertion.spec.js +119 -0
- package/lib/test/unit/wda-client-shell.spec.js +133 -0
- package/lib/test/unit/web-config-service.spec.js +98 -0
- package/lib/test/unit/webhook-delivery.spec.js +24 -0
- package/lib/test/unit/webhook-payloads.spec.js +273 -0
- package/lib/test/unit/xenon-execute-scripts.spec.js +138 -0
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/lib/web/src/components/webhook-settings/webhookEventVariables.json +30 -0
- package/package.json +5 -5
- package/prisma/migrations/20261004140000_keep_device_settings/migration.sql +19 -0
- package/prisma/migrations/20261004174413_locator_etalon_path/migration.sql +2 -0
- package/prisma/schema.prisma +38 -7
- package/schema.json +23 -25
- package/scripts/generate-types-from-schema.js +1 -2
- package/scripts/lib/always-present-plugin-args.js +4 -1
- package/lib/public/assets/BugReportButton-BoGhlI7F.js +0 -15
- package/lib/public/assets/DeviceMosaicView-8J0oYqMM.js +0 -2
- package/lib/public/assets/ai-settings-Bce5SJob.js +0 -21
- package/lib/public/assets/device-explorer-C-KnhMHI.js +0 -279
- package/lib/public/assets/forgot-password-DnudqFFo.js +0 -1
- package/lib/public/assets/maintenance-settings-zDVlOMVK.js +0 -6
- package/lib/public/assets/recording-group-store-WVM3hoXn.js +0 -26
- package/lib/public/assets/runbook-page-DJTVCJX7.js +0 -124
- package/lib/public/assets/session-detail-page-CGn55Pvl.js +0 -4
- package/lib/public/assets/settings-MsVU2ukE.js +0 -6
- package/lib/public/assets/webhook-settings-DiS8geIm.js +0 -1
- /package/lib/public/assets/{BugReportButton-CPBjfiv4.css → index-CPBjfiv4.css} +0 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
import{t as A,r as n,j as e,A as I,Z as f,q as $}from"./index-DI1fGIyj.js";/* empty css */import{a as F}from"./time-D-gjmE1o.js";import{T as E,a as M,b as T,c as u,d as U,e as p}from"./Table-io9fwa98.js";import{F as Z}from"./FieldGroup-Bvn08svv.js";import{P as R}from"./page-header--aTyJXq6.js";import{M as K}from"./Modal-Cg9V5JYU.js";import{B as D}from"./button-fIW8owMI.js";import{I as Q}from"./input-BjDRKCea.js";import{S as P}from"./select-DvCSv25z.js";import{l as V}from"./users-DMF9Mk5H.js";import{P as N}from"./plus-CsIk10Cq.js";import{A as W}from"./arrow-left-B27ckB31.js";import{T as L}from"./trash-2-CTZpPSdd.js";import"./index-HDQVe1gi.js";import"./utils-CDN07tui.js";const B={SUPER_ADMIN:"Super admin",ADMIN:"Admin",MEMBER:"Member"};async function j(i,r){const t=await fetch(i,{credentials:"include",...r});if(!t.ok){const c=await t.json().catch(()=>({}));throw Object.assign(new Error(c.error||`HTTP ${t.status}`),{status:t.status})}return t.json()}const ve=()=>{const{toast:i}=A(),[r,t]=n.useState([]),[c,m]=n.useState(!0),[l,h]=n.useState(!1),[x,d]=n.useState(!1),[_,C]=n.useState(null),v=async()=>{m(!0);try{const a=await j("/xenon/api/teams");t(a),h(!1)}catch(a){a.status===403?h(!0):i(`Failed to load teams: ${a.message}`,"error")}finally{m(!1)}};if(n.useEffect(()=>{v()},[]),c)return e.jsxs("div",{className:"settings-loading",children:[e.jsx(I,{className:"animate-spin",size:32}),e.jsx("span",{children:"Loading teams…"})]});if(l)return e.jsx("div",{className:"settings-container",children:e.jsx(R,{icon:f,title:"Teams",subtitle:e.jsxs(e.Fragment,{children:["Your key lacks the ",e.jsx("code",{children:"admin"})," scope. Ask an administrator to promote your key before you can manage teams."]})})});if(_)return e.jsx(se,{team:_,onBack:()=>{C(null),v()}});const S=r.reduce((a,o)=>a+o.deviceCount,0),y=r.reduce((a,o)=>a+o.memberCount,0);return e.jsxs("div",{className:"settings-container",children:[e.jsx(R,{icon:f,title:"Teams",subtitle:"Group devices and members by team. Devices with no team assignment stay in the shared pool — visible to every authenticated user.",action:r.length>0?e.jsxs("button",{type:"button",className:"page-header-action",onClick:()=>d(!0),children:[e.jsx(N,{size:16}),e.jsx("span",{children:"New team"})]}):void 0}),e.jsxs("div",{className:"settings-content",children:[r.length>0&&e.jsxs("div",{className:"stat-strip",children:[e.jsxs("div",{className:"stat-strip__cell",children:[e.jsx("div",{className:"stat-strip__value stat-strip__value--accent",children:r.length}),e.jsx("div",{className:"stat-strip__label",children:"Teams"})]}),e.jsxs("div",{className:"stat-strip__cell",children:[e.jsx("div",{className:"stat-strip__value",children:y}),e.jsx("div",{className:"stat-strip__label",children:"Members"})]}),e.jsxs("div",{className:"stat-strip__cell",children:[e.jsx("div",{className:"stat-strip__value",children:S}),e.jsx("div",{className:"stat-strip__label",children:"Devices assigned"})]})]}),r.length===0?e.jsxs("div",{className:"empty-state",children:[e.jsx("div",{className:"empty-state__icon",children:e.jsx(f,{size:28})}),e.jsx("h3",{className:"empty-state__title",children:"No teams yet"}),e.jsx("p",{className:"empty-state__copy",children:"Every device is in the shared pool until you create a team and assign it. Teams let you scope an API key to a subset of devices — useful for separating CI runners, staging fleets, or per-team device pools."}),e.jsxs("button",{type:"button",className:"page-header-action empty-state__action",onClick:()=>d(!0),children:[e.jsx(N,{size:16}),e.jsx("span",{children:"Create your first team"})]})]}):e.jsx("div",{className:"team-grid",children:r.map(a=>e.jsxs("button",{type:"button",className:"team-card",onClick:()=>C(a),children:[e.jsxs("div",{className:"team-card__header",children:[e.jsx("div",{className:"team-card__icon",children:e.jsx(f,{size:16})}),e.jsxs("div",{className:"team-card__title",children:[e.jsx("div",{className:"team-card__name",children:a.name}),e.jsx("div",{className:"team-card__id",children:a.id.slice(0,8)})]}),e.jsx("span",{className:"team-card__delete",role:"button",tabIndex:0,onClick:o=>o.stopPropagation(),children:e.jsx(ee,{team:a,onDeleted:v})})]}),e.jsxs("div",{className:"team-card__metrics",children:[e.jsxs("div",{className:"team-card__metric",children:[e.jsx($,{size:13}),e.jsx("span",{className:"team-card__metric-value",children:a.deviceCount}),e.jsx("span",{className:"team-card__metric-label",children:"devices"})]}),e.jsxs("div",{className:"team-card__metric",children:[e.jsx(f,{size:13}),e.jsx("span",{className:"team-card__metric-value",children:a.memberCount}),e.jsx("span",{className:"team-card__metric-label",children:"members"})]})]}),e.jsxs("div",{className:"team-card__footer",children:["Created ",F(a.createdAt)]})]},a.id))})]}),x&&e.jsx(X,{onClose:()=>d(!1),onCreated:()=>{d(!1),v()}})]})},X=({onClose:i,onCreated:r})=>{const{toast:t}=A(),[c,m]=n.useState(""),[l,h]=n.useState(!1),x=async()=>{if(!c.trim())return t("Name required","error");h(!0);try{await j("/xenon/api/teams",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:c.trim()})}),t("Team created","success"),r()}catch(d){t(d.message,"error")}finally{h(!1)}};return e.jsx(K,{open:!0,onClose:i,title:"New team",footer:e.jsxs(e.Fragment,{children:[e.jsx(D,{variant:"secondary",onClick:i,disabled:l,children:"Cancel"}),e.jsxs(D,{variant:"primary",onClick:x,disabled:l,children:[l?e.jsx(I,{className:"animate-spin",size:18}):e.jsx(N,{size:18}),l?"Creating…":"Create"]})]}),children:e.jsx("div",{style:{display:"flex",flexDirection:"column"},children:e.jsx(Z,{label:"Name",htmlFor:"team-name",children:e.jsx(Q,{id:"team-name",type:"text",value:c,onChange:d=>m(d.target.value),placeholder:"android-team, qa-ios, …",className:"w-full",autoFocus:!0})})})})},ee=({team:i,onDeleted:r})=>{const{toast:t}=A(),[c,m]=n.useState(!1),l=async h=>{if(h.stopPropagation(),!!window.confirm(`Delete team "${i.name}"? It must have no devices, members or apps.`)){m(!0);try{await j(`/xenon/api/teams/${i.id}`,{method:"DELETE"}),t("Team deleted","success"),r()}catch(x){t(x.message,"error")}finally{m(!1)}}};return e.jsxs("button",{className:"reset-btn",disabled:c,onClick:l,style:{color:"var(--status-error-fg)"},children:[e.jsx(L,{size:14}),c?"Deleting…":"Delete"]})},se=({team:i,onBack:r})=>{const{toast:t}=A(),[c,m]=n.useState([]),[l,h]=n.useState([]),[x,d]=n.useState([]),[_,C]=n.useState([]),[v,S]=n.useState(!0),[y,a]=n.useState(""),[o,k]=n.useState(""),b=async()=>{S(!0);try{const[s,g,Y]=await Promise.all([j(`/xenon/api/teams/${i.id}/members`),V(),j("/xenon/api/device")]);m(s),h(g);const z=Y;d(z.filter(w=>w.teamId===i.id)),C(z.filter(w=>!w.teamId))}catch(s){t(`Failed to load team detail: ${s.message}`,"error")}finally{S(!1)}};n.useEffect(()=>{b()},[i.id]);const O=async()=>{if(y)try{await j(`/xenon/api/teams/${i.id}/members`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({userId:y})}),t("Member added","success"),a(""),b()}catch(s){t(s.message,"error")}},H=async s=>{if(window.confirm("Remove this member from the team?"))try{await j(`/xenon/api/teams/${i.id}/members/${encodeURIComponent(s)}`,{method:"DELETE"}),t("Member removed","success"),b()}catch(g){t(g.message,"error")}},J=async()=>{if(o)try{await j(`/xenon/api/device/${encodeURIComponent(o)}/team`,{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({teamId:i.id})}),t("Device assigned","success"),k(""),b()}catch(s){t(s.message,"error")}},q=async s=>{if(window.confirm("Return this device to the shared pool?"))try{await j(`/xenon/api/device/${encodeURIComponent(s)}/team`,{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({teamId:null})}),t("Device unassigned","success"),b()}catch(g){t(g.message,"error")}},G=l.filter(s=>!c.some(g=>g.userId===s.id));return e.jsxs("div",{className:"settings-container mesh-gradient-infra",children:[e.jsxs("div",{className:"settings-header",children:[e.jsxs("div",{style:{display:"flex",alignItems:"center",gap:12},children:[e.jsx("button",{onClick:r,className:"reset-btn",style:{padding:"4px 8px"},"aria-label":"Back",children:e.jsx(W,{size:18})}),e.jsxs("div",{className:"settings-title-group",children:[e.jsx(f,{className:"settings-icon infra-icon",size:28}),e.jsx("h2",{children:i.name})]})]}),e.jsx("p",{className:"settings-subtitle",children:'Manage members and devices assigned to this team. To add a device, go to Devices and use "Assign to team."'})]}),v?e.jsx("div",{className:"settings-loading",children:e.jsx(I,{className:"animate-spin",size:32})}):e.jsxs("div",{className:"settings-content",style:{display:"flex",flexDirection:"column",gap:24},children:[e.jsxs("section",{children:[e.jsxs("h3",{style:{marginBottom:12},children:["Members (",c.length,")"]}),e.jsxs("div",{className:"surface-card",style:{padding:12},children:[e.jsxs("div",{style:{display:"flex",gap:8,marginBottom:12},children:[e.jsxs(P,{value:y,onChange:s=>a(s.target.value),className:"flex-1",children:[e.jsx("option",{value:"",children:"Select a user to add…"}),G.map(s=>e.jsxs("option",{value:s.id,children:[s.name," (",s.email,") — ",B[s.role]]},s.id))]}),e.jsxs(D,{variant:"primary",onClick:O,disabled:!y,children:[e.jsx(N,{size:16})," Add"]})]}),c.length===0?e.jsx("p",{style:{opacity:.6,textAlign:"center",padding:24},children:"No members yet. Add a user above."}):e.jsxs(E,{children:[e.jsx(M,{children:e.jsxs(T,{children:[e.jsx(u,{children:"Name"}),e.jsx(u,{children:"Email"}),e.jsx(u,{children:"Role"}),e.jsx(u,{children:"Added"}),e.jsx(u,{style:{textAlign:"right"},children:"Action"})]})}),e.jsx(U,{children:c.map(s=>e.jsxs(T,{children:[e.jsx(p,{children:s.name}),e.jsx(p,{children:e.jsx("code",{style:{fontSize:"11px",color:"var(--text-muted)"},children:s.email})}),e.jsx(p,{children:B[s.role]}),e.jsx(p,{children:F(s.addedAt)}),e.jsx(p,{style:{textAlign:"right"},children:e.jsx("button",{className:"reset-btn",onClick:()=>H(s.userId),style:{color:"var(--status-error-fg)"},"aria-label":"Remove member",title:"Remove member",children:e.jsx(L,{size:14})})})]},s.userId))})]})]})]}),e.jsxs("section",{children:[e.jsxs("h3",{style:{marginBottom:12},children:["Devices (",x.length,")"]}),e.jsxs("div",{className:"surface-card",style:{padding:12},children:[e.jsxs("div",{style:{display:"flex",gap:8,marginBottom:12},children:[e.jsxs(P,{value:o,onChange:s=>k(s.target.value),className:"flex-1",children:[e.jsx("option",{value:"",children:"Select a shared-pool device to assign…"}),_.map(s=>e.jsxs("option",{value:s.udid,children:[s.name," (",s.platform,") — ",s.udid.slice(0,12),"…"]},s.udid))]}),e.jsxs(D,{variant:"primary",onClick:J,disabled:!o,children:[e.jsx(N,{size:16})," Assign"]})]}),x.length===0?e.jsx("p",{style:{opacity:.6,textAlign:"center",padding:24},children:"No devices assigned. Use the Devices page to move devices into this team."}):e.jsxs(E,{children:[e.jsx(M,{children:e.jsxs(T,{children:[e.jsx(u,{children:"Name"}),e.jsx(u,{children:"Platform"}),e.jsx(u,{children:"UDID"}),e.jsx(u,{style:{textAlign:"right"},children:"Actions"})]})}),e.jsx(U,{children:x.map(s=>e.jsxs(T,{children:[e.jsxs(p,{children:[e.jsx($,{size:14,style:{marginRight:6,verticalAlign:"middle"}}),s.name]}),e.jsx(p,{children:s.platform}),e.jsx(p,{children:e.jsxs("code",{style:{fontSize:"11px"},children:[s.udid.slice(0,14),"…"]})}),e.jsx(p,{style:{textAlign:"right"},children:e.jsx("button",{className:"reset-btn",onClick:()=>q(s.udid),children:"Return to shared pool"})})]},s.udid))})]})]})]})]})]})};export{ve as Teams};
|
|
1
|
+
import{t as D,r as n,j as e,A as I,_ as f,q as $}from"./index-BOxe43p1.js";/* empty css */import{a as F}from"./time-D-gjmE1o.js";import{T as E,a as M,b as T,c as u,d as P,e as p}from"./Table-zNTX92Pn.js";import{F as K}from"./FieldGroup-CveQ3xrA.js";import{P as R}from"./page-header-COQZxJfR.js";import{M as Q}from"./Modal-Bsvj1tm2.js";import{B as A}from"./button-Cg08-EMT.js";import{I as V}from"./input-CZHFjbSg.js";import{S as U}from"./select-Bpyn7rWO.js";import{l as W}from"./users-D_vv_yz4.js";import{P as N}from"./plus-5aq-9uRx.js";import{A as X}from"./arrow-left-BWfCbeHP.js";import{T as L}from"./trash-2-B701i8sy.js";import"./index-CfxsUslp.js";import"./utils-CDN07tui.js";const B={SUPER_ADMIN:"Super admin",ADMIN:"Admin",MEMBER:"Member"};async function j(i,r){const t=await fetch(i,{credentials:"include",...r});if(!t.ok){const c=await t.json().catch(()=>({}));throw Object.assign(new Error(c.error||`HTTP ${t.status}`),{status:t.status})}return t.json()}const ve=()=>{const{toast:i}=D(),[r,t]=n.useState([]),[c,m]=n.useState(!0),[l,h]=n.useState(!1),[x,d]=n.useState(!1),[_,C]=n.useState(null),v=async()=>{m(!0);try{const a=await j("/xenon/api/teams");t(a),h(!1)}catch(a){a.status===403?h(!0):i(`Failed to load teams: ${a.message}`,"error")}finally{m(!1)}};if(n.useEffect(()=>{v()},[]),c)return e.jsxs("div",{className:"settings-loading",children:[e.jsx(I,{className:"animate-spin",size:32}),e.jsx("span",{children:"Loading teams…"})]});if(l)return e.jsx("div",{className:"settings-container",children:e.jsx(R,{icon:f,title:"Teams",subtitle:e.jsxs(e.Fragment,{children:["Your key lacks the ",e.jsx("code",{children:"admin"})," scope. Ask an administrator to promote your key before you can manage teams."]})})});if(_)return e.jsx(se,{team:_,onBack:()=>{C(null),v()}});const S=r.reduce((a,o)=>a+o.deviceCount,0),y=r.reduce((a,o)=>a+o.memberCount,0);return e.jsxs("div",{className:"settings-container",children:[e.jsx(R,{icon:f,title:"Teams",subtitle:"Group devices and members by team. Devices with no team assignment stay in the shared pool — visible to every authenticated user.",action:r.length>0?e.jsxs("button",{type:"button",className:"page-header-action",onClick:()=>d(!0),children:[e.jsx(N,{size:16}),e.jsx("span",{children:"New team"})]}):void 0}),e.jsxs("div",{className:"settings-content",children:[r.length>0&&e.jsxs("div",{className:"stat-strip",children:[e.jsxs("div",{className:"stat-strip__cell",children:[e.jsx("div",{className:"stat-strip__value stat-strip__value--accent",children:r.length}),e.jsx("div",{className:"stat-strip__label",children:"Teams"})]}),e.jsxs("div",{className:"stat-strip__cell",children:[e.jsx("div",{className:"stat-strip__value",children:y}),e.jsx("div",{className:"stat-strip__label",children:"Members"})]}),e.jsxs("div",{className:"stat-strip__cell",children:[e.jsx("div",{className:"stat-strip__value",children:S}),e.jsx("div",{className:"stat-strip__label",children:"Devices assigned"})]})]}),r.length===0?e.jsxs("div",{className:"empty-state",children:[e.jsx("div",{className:"empty-state__icon",children:e.jsx(f,{size:28})}),e.jsx("h3",{className:"empty-state__title",children:"No teams yet"}),e.jsx("p",{className:"empty-state__copy",children:"Every device is in the shared pool until you create a team and assign it. Teams let you scope an API key to a subset of devices — useful for separating CI runners, staging fleets, or per-team device pools."}),e.jsxs("button",{type:"button",className:"page-header-action empty-state__action",onClick:()=>d(!0),children:[e.jsx(N,{size:16}),e.jsx("span",{children:"Create your first team"})]})]}):e.jsx("div",{className:"team-grid",children:r.map(a=>e.jsxs("button",{type:"button",className:"team-card",onClick:()=>C(a),children:[e.jsxs("div",{className:"team-card__header",children:[e.jsx("div",{className:"team-card__icon",children:e.jsx(f,{size:16})}),e.jsxs("div",{className:"team-card__title",children:[e.jsx("div",{className:"team-card__name",children:a.name}),e.jsx("div",{className:"team-card__id",children:a.id.slice(0,8)})]}),e.jsx("span",{className:"team-card__delete",role:"button",tabIndex:0,onClick:o=>o.stopPropagation(),children:e.jsx(ee,{team:a,onDeleted:v})})]}),e.jsxs("div",{className:"team-card__metrics",children:[e.jsxs("div",{className:"team-card__metric",children:[e.jsx($,{size:13}),e.jsx("span",{className:"team-card__metric-value",children:a.deviceCount}),e.jsx("span",{className:"team-card__metric-label",children:"devices"})]}),e.jsxs("div",{className:"team-card__metric",children:[e.jsx(f,{size:13}),e.jsx("span",{className:"team-card__metric-value",children:a.memberCount}),e.jsx("span",{className:"team-card__metric-label",children:"members"})]})]}),e.jsxs("div",{className:"team-card__footer",children:["Created ",F(a.createdAt)]})]},a.id))})]}),x&&e.jsx(Z,{onClose:()=>d(!1),onCreated:()=>{d(!1),v()}})]})},Z=({onClose:i,onCreated:r})=>{const{toast:t}=D(),[c,m]=n.useState(""),[l,h]=n.useState(!1),x=async()=>{if(!c.trim())return t("Name required","error");h(!0);try{await j("/xenon/api/teams",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:c.trim()})}),t("Team created","success"),r()}catch(d){t(d.message,"error")}finally{h(!1)}};return e.jsx(Q,{open:!0,onClose:i,title:"New team",footer:e.jsxs(e.Fragment,{children:[e.jsx(A,{variant:"secondary",onClick:i,disabled:l,children:"Cancel"}),e.jsxs(A,{variant:"primary",onClick:x,disabled:l,children:[l?e.jsx(I,{className:"animate-spin",size:18}):e.jsx(N,{size:18}),l?"Creating…":"Create"]})]}),children:e.jsx("div",{style:{display:"flex",flexDirection:"column"},children:e.jsx(K,{label:"Name",htmlFor:"team-name",children:e.jsx(V,{id:"team-name",type:"text",value:c,onChange:d=>m(d.target.value),placeholder:"android-team, qa-ios, …",className:"w-full",autoFocus:!0})})})})},ee=({team:i,onDeleted:r})=>{const{toast:t}=D(),[c,m]=n.useState(!1),l=async h=>{if(h.stopPropagation(),!!window.confirm(`Delete team "${i.name}"? It must have no devices, members or apps.`)){m(!0);try{await j(`/xenon/api/teams/${i.id}`,{method:"DELETE"}),t("Team deleted","success"),r()}catch(x){t(x.message,"error")}finally{m(!1)}}};return e.jsxs("button",{className:"reset-btn",disabled:c,onClick:l,style:{color:"var(--status-error-fg)"},children:[e.jsx(L,{size:14}),c?"Deleting…":"Delete"]})},se=({team:i,onBack:r})=>{const{toast:t}=D(),[c,m]=n.useState([]),[l,h]=n.useState([]),[x,d]=n.useState([]),[_,C]=n.useState([]),[v,S]=n.useState(!0),[y,a]=n.useState(""),[o,k]=n.useState(""),b=async()=>{S(!0);try{const[s,g,Y]=await Promise.all([j(`/xenon/api/teams/${i.id}/members`),W(),j("/xenon/api/device")]);m(s),h(g);const z=Y;d(z.filter(w=>w.teamId===i.id)),C(z.filter(w=>!w.teamId))}catch(s){t(`Failed to load team detail: ${s.message}`,"error")}finally{S(!1)}};n.useEffect(()=>{b()},[i.id]);const O=async()=>{if(y)try{await j(`/xenon/api/teams/${i.id}/members`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({userId:y})}),t("Member added","success"),a(""),b()}catch(s){t(s.message,"error")}},H=async s=>{if(window.confirm("Remove this member from the team?"))try{await j(`/xenon/api/teams/${i.id}/members/${encodeURIComponent(s)}`,{method:"DELETE"}),t("Member removed","success"),b()}catch(g){t(g.message,"error")}},J=async()=>{if(o)try{await j(`/xenon/api/device/${encodeURIComponent(o)}/team`,{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({teamId:i.id})}),t("Device assigned","success"),k(""),b()}catch(s){t(s.message,"error")}},q=async s=>{if(window.confirm("Return this device to the shared pool?"))try{await j(`/xenon/api/device/${encodeURIComponent(s)}/team`,{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({teamId:null})}),t("Device unassigned","success"),b()}catch(g){t(g.message,"error")}},G=l.filter(s=>!c.some(g=>g.userId===s.id));return e.jsxs("div",{className:"settings-container mesh-gradient-infra",children:[e.jsxs("div",{className:"settings-header",children:[e.jsxs("div",{style:{display:"flex",alignItems:"center",gap:12},children:[e.jsx("button",{onClick:r,className:"reset-btn",style:{padding:"4px 8px"},"aria-label":"Back",children:e.jsx(X,{size:18})}),e.jsxs("div",{className:"settings-title-group",children:[e.jsx(f,{className:"settings-icon infra-icon",size:28}),e.jsx("h2",{children:i.name})]})]}),e.jsx("p",{className:"settings-subtitle",children:`Manage members and devices assigned to this team. Add a shared device below, or choose "Assign team…" in a device's ⋯ menu on the Devices page.`})]}),v?e.jsx("div",{className:"settings-loading",children:e.jsx(I,{className:"animate-spin",size:32})}):e.jsxs("div",{className:"settings-content",style:{display:"flex",flexDirection:"column",gap:24},children:[e.jsxs("section",{children:[e.jsxs("h3",{style:{marginBottom:12},children:["Members (",c.length,")"]}),e.jsxs("div",{className:"surface-card",style:{padding:12},children:[e.jsxs("div",{style:{display:"flex",gap:8,marginBottom:12},children:[e.jsxs(U,{value:y,onChange:s=>a(s.target.value),className:"flex-1",children:[e.jsx("option",{value:"",children:"Select a user to add…"}),G.map(s=>e.jsxs("option",{value:s.id,children:[s.name," (",s.email,") — ",B[s.role]]},s.id))]}),e.jsxs(A,{variant:"primary",onClick:O,disabled:!y,children:[e.jsx(N,{size:16})," Add"]})]}),c.length===0?e.jsx("p",{style:{opacity:.6,textAlign:"center",padding:24},children:"No members yet. Add a user above."}):e.jsxs(E,{children:[e.jsx(M,{children:e.jsxs(T,{children:[e.jsx(u,{children:"Name"}),e.jsx(u,{children:"Email"}),e.jsx(u,{children:"Role"}),e.jsx(u,{children:"Added"}),e.jsx(u,{style:{textAlign:"right"},children:"Action"})]})}),e.jsx(P,{children:c.map(s=>e.jsxs(T,{children:[e.jsx(p,{children:s.name}),e.jsx(p,{children:e.jsx("code",{style:{fontSize:"11px",color:"var(--text-muted)"},children:s.email})}),e.jsx(p,{children:B[s.role]}),e.jsx(p,{children:F(s.addedAt)}),e.jsx(p,{style:{textAlign:"right"},children:e.jsx("button",{className:"reset-btn",onClick:()=>H(s.userId),style:{color:"var(--status-error-fg)"},"aria-label":"Remove member",title:"Remove member",children:e.jsx(L,{size:14})})})]},s.userId))})]})]})]}),e.jsxs("section",{children:[e.jsxs("h3",{style:{marginBottom:12},children:["Devices (",x.length,")"]}),e.jsxs("div",{className:"surface-card",style:{padding:12},children:[e.jsxs("div",{style:{display:"flex",gap:8,marginBottom:12},children:[e.jsxs(U,{value:o,onChange:s=>k(s.target.value),className:"flex-1",children:[e.jsx("option",{value:"",children:"Select a shared-pool device to assign…"}),_.map(s=>e.jsxs("option",{value:s.udid,children:[s.name," (",s.platform,") — ",s.udid.slice(0,12),"…"]},s.udid))]}),e.jsxs(A,{variant:"primary",onClick:J,disabled:!o,children:[e.jsx(N,{size:16})," Assign"]})]}),x.length===0?e.jsx("p",{style:{opacity:.6,textAlign:"center",padding:24},children:"No devices assigned. Pick a shared device above to add one."}):e.jsxs(E,{children:[e.jsx(M,{children:e.jsxs(T,{children:[e.jsx(u,{children:"Name"}),e.jsx(u,{children:"Platform"}),e.jsx(u,{children:"UDID"}),e.jsx(u,{style:{textAlign:"right"},children:"Actions"})]})}),e.jsx(P,{children:x.map(s=>e.jsxs(T,{children:[e.jsxs(p,{children:[e.jsx($,{size:14,style:{marginRight:6,verticalAlign:"middle"}}),s.name]}),e.jsx(p,{children:s.platform}),e.jsx(p,{children:e.jsxs("code",{style:{fontSize:"11px"},children:[s.udid.slice(0,14),"…"]})}),e.jsx(p,{style:{textAlign:"right"},children:e.jsx("button",{className:"reset-btn",onClick:()=>q(s.udid),children:"Return to shared pool"})})]},s.udid))})]})]})]})]})]})};export{ve as Teams};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{c as G,r as o,p as V,X as v}from"./index-
|
|
1
|
+
import{c as G,r as o,p as V,X as v}from"./index-BOxe43p1.js";import{f as Z}from"./time-D-gjmE1o.js";/**
|
|
2
2
|
* @license lucide-react v0.555.0 - ISC
|
|
3
3
|
*
|
|
4
4
|
* This source code is licensed under the ISC license.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{r as t,h as u}from"./index-
|
|
1
|
+
import{r as t,h as u}from"./index-BOxe43p1.js";function n(){const[s,r]=t.useState(null);return t.useEffect(()=>{let e=!0;return u().then(o=>e&&r(o.passwordReset)),()=>{e=!1}},[]),s}export{n as u};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{o as s}from"./index-
|
|
1
|
+
import{o as s}from"./index-BOxe43p1.js";const o="/xenon/api/users";async function c(){const t=await fetch(`${o}/`,{credentials:"include"});if(!t.ok)throw new Error(`listUsers failed (${t.status})`);return t.json()}async function i(t){const e=await fetch(`${o}/`,{method:"POST",credentials:"include",headers:{"Content-Type":"application/json",...s},body:JSON.stringify(t)});if(!e.ok){const n=await e.json().catch(()=>({}));throw new Error(n.error||`createUser failed (${e.status})`)}return e.json()}async function d(t,e){const n=await fetch(`${o}/${encodeURIComponent(t)}`,{method:"PATCH",credentials:"include",headers:{"Content-Type":"application/json"},body:JSON.stringify(e)});if(!n.ok){const r=await n.json().catch(()=>({}));throw new Error(r.error||`updateUser failed (${n.status})`)}return n.json()}async function l(t){const e=await fetch(`${o}/${encodeURIComponent(t)}`,{method:"DELETE",credentials:"include"});if(!e.ok&&e.status!==204){const n=await e.json().catch(()=>({}));throw new Error(n.error||`deleteUser failed (${e.status})`)}}async function u(t){const e=await fetch(`${o}/${encodeURIComponent(t)}/reset-link`,{method:"POST",credentials:"include"}),n=await e.json().catch(()=>({}));if(!e.ok)throw new Error(n.error||`Reset link failed (${e.status})`);return n}export{i as a,u as c,l as d,c as l,d as u};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{c as w,b as k,r as a,j as e,
|
|
1
|
+
import{c as w,b as k,r as a,j as e,a2 as C}from"./index-BOxe43p1.js";import{l as M,d as R,c as I,a as A,u as D}from"./users-D_vv_yz4.js";import{f as U}from"./time-D-gjmE1o.js";import{P}from"./page-header-COQZxJfR.js";import{B as y}from"./button-Cg08-EMT.js";import{I as N}from"./input-CZHFjbSg.js";import{S as g}from"./select-Bpyn7rWO.js";import{P as B}from"./plus-5aq-9uRx.js";import{T as L}from"./trash-2-B701i8sy.js";import"./utils-CDN07tui.js";/**
|
|
2
2
|
* @license lucide-react v0.555.0 - ISC
|
|
3
3
|
*
|
|
4
4
|
* This source code is licensed under the ISC license.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{t as P,r as l,X as p,j as e,P as w,A as x,C as V,Q as I}from"./index-BOxe43p1.js";/* empty css */import{F as k}from"./FieldGroup-CveQ3xrA.js";import{S as U}from"./select-Bpyn7rWO.js";import{P as B}from"./page-header-COQZxJfR.js";import{T as D}from"./trash-2-B701i8sy.js";import{P as _}from"./plus-5aq-9uRx.js";import{C as J}from"./circle-x-DTwnSAnl.js";import{C as K}from"./circle-check-CYXBoUqT.js";import{Z as M}from"./zap-DO9ry1iz.js";import{A as X}from"./activity-CJ8LwZq9.js";import"./utils-CDN07tui.js";const Z=["udid","name","host","platform"],q=["udid","name","host","platform"],G=["sessionId","sessionName","failureReason","udid","deviceName","platform","osVersion","startTime","endTime"],Q=["windowDays","totalHeals","distinctSelectors"],Y={device_offline:Z,device_new:q,session_failed:G,selector_health_digest:Q},ee=Y,se={eventType:"The event: device_offline, session_failed, ...",udid:"The device's id",name:"The device's name",host:"The machine it is plugged into",platform:"android or ios",sessionId:"The session's id",sessionName:"The session's name, if the test gave it one",failureReason:"Why it failed",deviceName:"The device's name",osVersion:"The device's OS version",startTime:"When it started (ISO 8601)",endTime:"When it ended (ISO 8601)",windowDays:"How many days the digest covers",totalHeals:"Heals in that time",distinctSelectors:"Selectors that were healed"};function te(o){const c=["eventType"];for(const u of o)for(const h of ee[u]??[])c.includes(h)||c.push(h);return c}const ae=[{id:"slack",label:"Slack message"},{id:"webhook",label:"JSON (event and payload)"}];function oe(o){return o.payloadTemplate?"CUSTOM":o.type==="slack"?"SLACK":"JSON"}const j=[{id:"device_offline",label:"Device offline",icon:V,tone:"red"},{id:"device_new",label:"New device",icon:_,tone:"green"},{id:"session_failed",label:"Session failed",icon:J,tone:"amber"},{id:"selector_health_digest",label:"Selector health digest",icon:I,tone:"green"}],be=()=>{const{toast:o}=P(),[c,u]=l.useState([]),[h,N]=l.useState(!0),[r,y]=l.useState(""),[i,S]=l.useState(["device_offline","session_failed"]),[f,T]=l.useState(!1),[C,A]=l.useState(!1),[d,E]=l.useState(!1),[m,g]=l.useState(""),[b,O]=l.useState("slack");l.useEffect(()=>{v()},[]);const v=async()=>{N(!0);try{const s=await p.getWebhookConfigs();u(s||[])}catch(s){console.error("Failed to load webhook configs",s),o("Failed to load webhooks.","error")}finally{N(!1)}},W=async()=>{if(!r.trim()){o("Webhook URL is required.","error");return}if(i.length===0){o("Select at least one trigger event.","error");return}T(!0);try{await p.addWebhookConfig(r.trim(),i,b,m||void 0),y(""),g(""),E(!1),O("slack"),S(["device_offline","session_failed"]),await v(),o("Webhook saved.","success")}catch(s){console.error("Failed to add webhook",s),o(`Failed to add webhook: ${s.message||"unknown error"}`,"error")}finally{T(!1)}},$=async(s,t)=>{if(window.confirm(`Remove webhook for "${t}"?`))try{await p.deleteWebhookConfig(s),await v(),o("Webhook removed.","success")}catch(a){console.error("Failed to delete webhook",a),o(`Failed to delete: ${a.message||"unknown error"}`,"error")}},L=async()=>{if(!r.trim()||i.length===0)return;A(!0);const s=t=>{var a;return((a=j.find(n=>n.id===t))==null?void 0:a.label)??t};try{for(const t of i)try{await p.testWebhook(r.trim(),b,m||void 0,t)}catch(a){o(`Test failed for ${s(t)}: ${a.message||"check the URL"}`,"error");return}o(i.length===1?`Test message delivered: ${s(i[0])}.`:`Test messages delivered: ${i.map(s).join(", ")}.`,"success")}finally{A(!1)}},R=s=>{S(t=>t.includes(s)?t.filter(a=>a!==s):[...t,s])},z=s=>{g(t=>t+`{{${s}}} `)};return e.jsxs("div",{className:"settings-container",children:[e.jsx(B,{icon:w,title:"Notifications",subtitle:"Configure Slack or generic webhooks to receive alerts for critical infrastructure events."}),e.jsxs("div",{className:"settings-content",children:[h?e.jsxs("div",{className:"settings-loading",style:{height:200},children:[e.jsx(x,{className:"animate-spin",size:28}),e.jsx("span",{children:"Loading webhooks…"})]}):c.length===0?e.jsxs("div",{className:"empty-state",children:[e.jsx("div",{className:"empty-state__icon",children:e.jsx(w,{size:28})}),e.jsx("h3",{className:"empty-state__title",children:"No webhooks configured"}),e.jsx("p",{className:"empty-state__copy",children:"Wire up a Slack incoming webhook or any HTTPS endpoint to receive alerts when devices go offline, new devices register, or sessions fail. Configure your first webhook below."})]}):e.jsx("div",{className:"webhook-list-grid",children:c.map(s=>{const t=(()=>{try{return JSON.parse(s.events)}catch{return[]}})();return e.jsxs("div",{className:"webhook-row-card",children:[e.jsxs("div",{className:"webhook-row-card__header",children:[e.jsx("span",{className:`pill-chip ${s.payloadTemplate?"pill-chip--admin":"pill-chip--scope"}`,children:oe(s)}),e.jsx("span",{className:"webhook-row-card__url",title:s.url,children:s.url}),e.jsxs("button",{type:"button",className:"row-action row-action--danger",onClick:()=>$(s.id,s.url),"aria-label":`Remove webhook for ${s.url}`,children:[e.jsx(D,{size:13}),e.jsx("span",{children:"Remove"})]})]}),e.jsx("div",{className:"webhook-row-card__events",children:t.length===0?e.jsx("span",{className:"webhook-row-card__no-events",children:"No triggers"}):t.map(a=>{const n=j.find(H=>H.id===a),F=(n==null?void 0:n.icon)??w;return e.jsxs("span",{className:`event-pill event-pill--${(n==null?void 0:n.tone)||"gray"}`,children:[e.jsx(F,{size:11}),(n==null?void 0:n.label)||a]},a)})})]},s.id)})}),e.jsxs("div",{className:"add-webhook-card",children:[e.jsxs("div",{className:"add-webhook-card__header",children:[e.jsx(_,{size:14}),e.jsx("span",{children:"Add a new webhook"})]}),e.jsxs("div",{className:"add-webhook-card__body",children:[e.jsx(k,{label:"Webhook URL",description:"The endpoint we POST webhook events to.",htmlFor:"webhook-url",children:e.jsx("div",{className:"setting-input-wrapper",children:e.jsx("input",{id:"webhook-url",type:"url",placeholder:"https://hooks.slack.com/services/...",value:r,onChange:s=>y(s.target.value)})})}),e.jsx(k,{label:"Message format",description:m.trim()?"Your custom payload is sent as written, whatever the format.":"Slack message for a Slack incoming webhook; JSON for anything else.",htmlFor:"webhook-format",children:e.jsx(U,{id:"webhook-format",value:b,onChange:s=>O(s.target.value),children:ae.map(s=>e.jsx("option",{value:s.id,children:s.label},s.id))})}),e.jsx(k,{label:"Trigger events",children:e.jsx("div",{className:"event-toggle-grid",children:j.map(s=>{const t=i.includes(s.id),a=s.icon;return e.jsxs("button",{type:"button",role:"checkbox","aria-checked":t,className:`event-toggle event-toggle--${s.tone} ${t?"is-selected":""}`,onClick:()=>R(s.id),children:[e.jsx(a,{size:14,className:"event-toggle__lead"}),e.jsx("span",{children:s.label}),t&&e.jsx(K,{size:14,className:"event-toggle__check"})]},s.id)})})}),e.jsxs("div",{className:"template-section",children:[e.jsxs("button",{type:"button",className:"template-section__toggle",onClick:()=>E(!d),"aria-expanded":d,children:[e.jsx(M,{size:14,className:d?"tone-amber":"tone-dim"}),e.jsx("span",{children:"Use custom payload (optional)"}),e.jsx("span",{className:"template-section__indicator",children:d?"−":"+"})]}),d&&e.jsxs("div",{className:"template-editor",children:[e.jsx("p",{className:"template-editor__hint",children:"Define a JSON or text template. Click a name below to insert it; the names are the ones your selected events carry."}),e.jsx("div",{className:"template-editor__chips",children:te(i).map(s=>e.jsx("button",{type:"button",className:"template-editor__chip","aria-label":`{{${s}}}`,title:se[s],onClick:()=>z(s),children:`{{${s}}}`},s))}),e.jsx("textarea",{className:"template-editor__textarea",placeholder:'Example: { "text": "Alert: Device {{udid}} is offline!" }',value:m,onChange:s=>g(s.target.value),rows:4})]})]})]}),e.jsxs("div",{className:"add-webhook-card__footer",children:[e.jsxs("button",{type:"button",className:"page-header-action page-header-action--ghost",onClick:L,disabled:!r.trim()||i.length===0||C,"aria-label":"Send test",title:i.length===0?"Select a trigger event to send a sample of":"Sends a sample message for each selected event",children:[C?e.jsx(x,{size:14,className:"animate-spin"}):e.jsx(X,{size:14}),e.jsx("span",{children:"Send test"})]}),e.jsxs("button",{type:"button",className:"page-header-action",onClick:W,disabled:f||!r.trim(),children:[f?e.jsx(x,{size:14,className:"animate-spin"}):e.jsx(_,{size:14}),e.jsx("span",{children:f?"Saving…":"Save webhook"})]})]})]})]})]})};export{be as WebhookSettings};
|
package/lib/public/index.html
CHANGED
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
Learn how to configure a non-root public URL by running `npm run build`.
|
|
37
37
|
-->
|
|
38
38
|
<title>Xenon</title>
|
|
39
|
-
<script type="module" crossorigin src="/xenon/assets/index-
|
|
39
|
+
<script type="module" crossorigin src="/xenon/assets/index-BOxe43p1.js"></script>
|
|
40
40
|
<link rel="stylesheet" crossorigin href="/xenon/assets/index-Bxoxt9sV.css">
|
|
41
41
|
</head>
|
|
42
42
|
|
package/lib/schema.json
CHANGED
|
@@ -67,7 +67,7 @@
|
|
|
67
67
|
"maxSessions": {
|
|
68
68
|
"type": "number",
|
|
69
69
|
"default": 8,
|
|
70
|
-
"description": "Maximum number of Appium sessions this
|
|
70
|
+
"description": "Maximum number of Appium sessions this server runs at once. A new session waits until fewer are running or being started. A live preview, a recording, or an SDK lease that has no session on it does not use a slot; a session started on a leased phone does, but is never held back itself. On a hub the count includes its nodes' phones. A value below 1 means no limit."
|
|
71
71
|
},
|
|
72
72
|
"cloud": {
|
|
73
73
|
"type": "object",
|
|
@@ -87,7 +87,7 @@
|
|
|
87
87
|
"$ref": "#/definitions/EmulatorConfig"
|
|
88
88
|
},
|
|
89
89
|
"default": [],
|
|
90
|
-
"description": "
|
|
90
|
+
"description": "Android emulators (AVDs) to boot when the server starts, each as `{ \"avdName\": \"Pixel_7\", ... }`. Any other field is a launch option passed to the emulator: `args`, `env`, `language`, `country`, `launchTimeout`, `readyTimeout`, `retryTimes`. Nothing is booted when `platform` is `ios` or `androidDeviceType` is `real`. It does not limit which emulators are discovered: every emulator that is running is found, as `bootedEmulators` allows."
|
|
91
91
|
},
|
|
92
92
|
"proxy": {
|
|
93
93
|
"type": "object",
|
|
@@ -131,7 +131,7 @@
|
|
|
131
131
|
"enableDashboard": {
|
|
132
132
|
"type": "boolean",
|
|
133
133
|
"default": false,
|
|
134
|
-
"description": "
|
|
134
|
+
"description": "Keep a full record of each Appium session for the dashboard: its commands, screenshots, logs and performance on the Sessions page, and its heals in Selector Health. Self-healing learns selectors either way, while self-healing is on. Without it, a session on this server's own phones isn't listed at all, while a hub still lists the sessions it sends to its nodes' phones, with their result, why they failed and their video. A node's own setting records nothing for its hub's sessions. Video is recorded either way unless the session turns it off. The dashboard itself is always served at /xenon/, whatever this is set to."
|
|
135
135
|
},
|
|
136
136
|
"bootedSimulators": {
|
|
137
137
|
"type": "boolean",
|
|
@@ -146,16 +146,16 @@
|
|
|
146
146
|
"removeDevicesFromDatabaseBeforeRunningThePlugin": {
|
|
147
147
|
"type": "boolean",
|
|
148
148
|
"default": false,
|
|
149
|
-
"description": "
|
|
149
|
+
"description": "At startup, also forget what was set for this server's own phones (team, tags, maintenance, reservations), so each comes back as a new phone. Without it a phone keeps those whenever it reconnects, through restarts. A node forgets them for every phone it has; a hub keeps its nodes' phones and theirs either way."
|
|
150
150
|
},
|
|
151
151
|
"healthCheckIntervalMs": {
|
|
152
152
|
"type": "number",
|
|
153
153
|
"default": 300000,
|
|
154
|
-
"description": "Default interval (ms) between background device health checks. Overridden when `healthCheckSchedule` is set. Default 5 minutes — frequent enough to keep battery/thermal badges fresh without hammering devices."
|
|
154
|
+
"description": "Default interval (ms) between background device health checks. Overridden when `healthCheckSchedule` is set. Default 5 minutes — frequent enough to keep battery/thermal badges fresh without hammering devices. A value saved on the dashboard's Settings page replaces this one."
|
|
155
155
|
},
|
|
156
156
|
"healthCheckSchedule": {
|
|
157
157
|
"type": "string",
|
|
158
|
-
"description": "Cron expression for the device health-check job (e.g. '0 * * * *' for hourly). When set, takes precedence over `healthCheckIntervalMs`."
|
|
158
|
+
"description": "Cron expression for the device health-check job (e.g. '0 * * * *' for hourly). When set, takes precedence over `healthCheckIntervalMs`. A schedule saved on the dashboard's Settings page replaces this one."
|
|
159
159
|
},
|
|
160
160
|
"databaseProvider": {
|
|
161
161
|
"type": "string",
|
|
@@ -163,11 +163,11 @@
|
|
|
163
163
|
"sqlite",
|
|
164
164
|
"postgresql"
|
|
165
165
|
],
|
|
166
|
-
"description": "Database backend.
|
|
166
|
+
"description": "Database backend. The published plugin stores its data in SQLite only, and each server, hub or node, has its own database. postgresql is accepted so older configs still start, and has no effect: the database URL decides."
|
|
167
167
|
},
|
|
168
168
|
"databaseUrl": {
|
|
169
169
|
"type": "string",
|
|
170
|
-
"description": "
|
|
170
|
+
"description": "Where the SQLite database lives, as `file:/path/to/xenon.db`. Falls back to the DATABASE_URL env var, then to a file under ~/.cache/xenon. A PostgreSQL URL stops the server at startup."
|
|
171
171
|
},
|
|
172
172
|
"aiProvider": {
|
|
173
173
|
"type": "string",
|
|
@@ -177,15 +177,15 @@
|
|
|
177
177
|
"anthropic",
|
|
178
178
|
"ollama"
|
|
179
179
|
],
|
|
180
|
-
"description": "AI provider for the LLM healing tier and visual analysis. Also controlled by XENON_AI_PROVIDER."
|
|
180
|
+
"description": "AI provider for the LLM healing tier and visual analysis. Also controlled by XENON_AI_PROVIDER. A provider chosen on the dashboard's AI engine page replaces this one, and applies from the next AI call without a restart."
|
|
181
181
|
},
|
|
182
182
|
"aiModel": {
|
|
183
183
|
"type": "string",
|
|
184
|
-
"description": "Override the default model for the selected `aiProvider` (e.g. 'gemini-1.5-pro', 'gpt-4o', 'claude-sonnet-4-6'). Falls back to XENON_AI_MODEL."
|
|
184
|
+
"description": "Override the default model for the selected `aiProvider` (e.g. 'gemini-1.5-pro', 'gpt-4o', 'claude-sonnet-4-6'). Falls back to XENON_AI_MODEL. A model saved through the dashboard's settings (`POST /xenon/api/config`) replaces this one."
|
|
185
185
|
},
|
|
186
186
|
"aiBaseUrl": {
|
|
187
187
|
"type": "string",
|
|
188
|
-
"description": "Custom base URL for the AI provider (e.g. a local Ollama server or an OpenAI-compatible gateway). Falls back to XENON_AI_BASE_URL."
|
|
188
|
+
"description": "Custom base URL for the AI provider (e.g. a local Ollama server or an OpenAI-compatible gateway). Falls back to XENON_AI_BASE_URL. A base URL saved through the dashboard's settings (`POST /xenon/api/config`) replaces this one."
|
|
189
189
|
},
|
|
190
190
|
"geminiApiKey": {
|
|
191
191
|
"type": "string",
|
|
@@ -202,27 +202,27 @@
|
|
|
202
202
|
"enableSelfHealing": {
|
|
203
203
|
"type": "boolean",
|
|
204
204
|
"default": true,
|
|
205
|
-
"description": "Enable the
|
|
205
|
+
"description": "Enable the self-healing pipeline (etalon recovery → Native → Fuzzy XML → OCR → Visual AI → LLM) for failed findElement calls. A value saved with the AI self-healing switch on the dashboard's Settings page replaces this one, and applies from the next command without a restart. A session can limit the tiers it uses with `xe:options.healingTiers` (`[]` turns healing off for that session); it cannot turn healing on where this is off."
|
|
206
206
|
},
|
|
207
207
|
"buildCleanupDays": {
|
|
208
208
|
"type": "number",
|
|
209
209
|
"default": 30,
|
|
210
|
-
"description": "Builds/sessions older than this many days are purged by the cleanup job."
|
|
210
|
+
"description": "Builds/sessions older than this many days are purged by the cleanup job. A value saved on the dashboard's Maintenance page replaces this one, and applies at the next cleanup run without a restart."
|
|
211
211
|
},
|
|
212
212
|
"buildCleanupMaxCount": {
|
|
213
213
|
"type": "number",
|
|
214
214
|
"default": 100,
|
|
215
|
-
"description": "Maximum number of builds to retain. Oldest-first eviction beyond this cap regardless of `buildCleanupDays`."
|
|
215
|
+
"description": "Maximum number of builds to retain. Oldest-first eviction beyond this cap regardless of `buildCleanupDays`. A value saved on the dashboard's Maintenance page replaces this one."
|
|
216
216
|
},
|
|
217
217
|
"buildCleanupSchedule": {
|
|
218
218
|
"type": "string",
|
|
219
219
|
"default": "0 0 * * *",
|
|
220
|
-
"description": "Cron expression for the retention job. Default '0 0 * * *' runs at midnight."
|
|
220
|
+
"description": "Cron expression for the retention job. Default '0 0 * * *' runs at midnight. A schedule saved on the dashboard's Maintenance page replaces this one, and takes effect at once."
|
|
221
221
|
},
|
|
222
222
|
"deleteBuildAssets": {
|
|
223
223
|
"type": "boolean",
|
|
224
224
|
"default": true,
|
|
225
|
-
"description": "When true, the cleanup job also deletes session video recordings and screenshots from disk (not just DB rows)."
|
|
225
|
+
"description": "When true, the cleanup job also deletes session video recordings and screenshots from disk (not just DB rows). A value saved on the dashboard's Maintenance page replaces this one."
|
|
226
226
|
},
|
|
227
227
|
"recordingCleanupDays": {
|
|
228
228
|
"type": "number",
|
|
@@ -246,8 +246,7 @@
|
|
|
246
246
|
},
|
|
247
247
|
"enableJsonLogging": {
|
|
248
248
|
"type": "boolean",
|
|
249
|
-
"
|
|
250
|
-
"description": "Emit structured JSON log lines instead of human-readable text. Recommended for shipping logs to a log aggregator."
|
|
249
|
+
"description": "Emit structured JSON log lines instead of human-readable text. Recommended for shipping logs to a log aggregator. Off by default. When unset, the XENON_JSON_LOGGING environment variable decides (`true` turns it on); setting this to true or false overrides the variable."
|
|
251
250
|
},
|
|
252
251
|
"tlsRejectUnauthorized": {
|
|
253
252
|
"type": "boolean",
|
|
@@ -261,18 +260,17 @@
|
|
|
261
260
|
},
|
|
262
261
|
"interceptor": {
|
|
263
262
|
"type": "object",
|
|
264
|
-
"description": "Network
|
|
263
|
+
"description": "Network capture (the interceptor) for sessions on this server's own Android phones: the default each session gets when its own interceptor capability doesn't say. A session's capability wins field by field. Fields: enabled, bufferSize, captureBodies. Android only."
|
|
265
264
|
},
|
|
266
265
|
"maxConcurrentRecordings": {
|
|
267
266
|
"type": "integer",
|
|
268
267
|
"minimum": 1,
|
|
269
268
|
"maximum": 16,
|
|
270
|
-
"
|
|
271
|
-
"description": "Server-wide hard cap on simultaneous free-form (non-session) screen recordings across all users. Automation session recording is exempt and not counted against this cap."
|
|
269
|
+
"description": "Server-wide hard cap on simultaneous free-form (non-session) screen recordings across all users. Automation session recording is exempt and not counted against this cap. Default 4. When unset, the XENON_MAX_CONCURRENT_RECORDINGS environment variable is used if it is a whole number of at least 1."
|
|
272
270
|
},
|
|
273
271
|
"recordingsAssetsPath": {
|
|
274
272
|
"type": "string",
|
|
275
|
-
"description": "
|
|
273
|
+
"description": "Directory for free-form recording artifacts. When unset, the XENON_RECORDINGS_ASSETS_PATH environment variable is used if it is set, else ~/.cache/xenon/assets/sessions/recordings."
|
|
276
274
|
},
|
|
277
275
|
"autowait": {
|
|
278
276
|
"type": "object",
|
|
@@ -449,17 +447,17 @@
|
|
|
449
447
|
"enabled": {
|
|
450
448
|
"type": "boolean",
|
|
451
449
|
"default": false,
|
|
452
|
-
"description": "
|
|
450
|
+
"description": "Capture the network traffic of every Android session on this server that doesn't say otherwise. A session turns capture on or off for itself with its interceptor capability (xe:interceptor.enabled, xe:options.interceptor.enabled, the flat interceptorEnabled, ...), which wins over this. While capture runs, the phone's global HTTP proxy points at Xenon; it is put back when the session ends, however it ends."
|
|
453
451
|
},
|
|
454
452
|
"bufferSize": {
|
|
455
453
|
"type": "number",
|
|
456
454
|
"default": 1000,
|
|
457
|
-
"description": "Maximum number of captured requests to retain in-memory per session before evicting oldest."
|
|
455
|
+
"description": "Maximum number of captured requests to retain in-memory per session before evicting oldest. The default for sessions whose interceptor capability doesn't set bufferSize."
|
|
458
456
|
},
|
|
459
457
|
"captureBodies": {
|
|
460
458
|
"type": "boolean",
|
|
461
459
|
"default": true,
|
|
462
|
-
"description": "Whether to capture request/response bodies. Disable for privacy or to reduce memory usage."
|
|
460
|
+
"description": "Whether to capture request/response bodies. Disable for privacy or to reduce memory usage. The default for sessions whose interceptor capability doesn't set captureBodies."
|
|
463
461
|
}
|
|
464
462
|
}
|
|
465
463
|
},
|
|
@@ -303,11 +303,13 @@ function getXenonCapabilities(caps) {
|
|
|
303
303
|
// Accept either a structured object (`xe:interceptor: { enabled, mocks, ... }`)
|
|
304
304
|
// or individual flat keys for users who prefer them.
|
|
305
305
|
const interceptorObj = getAnyCap(XENON_CAPABILITIES.INTERCEPTOR, 'interceptor');
|
|
306
|
+
// `enabled` and `captureBodies` stay undefined when the session doesn't set
|
|
307
|
+
// them: the server's `interceptor` option decides those
|
|
308
|
+
// (resolveInterceptorOptions).
|
|
306
309
|
if (interceptorObj && typeof interceptorObj === 'object') {
|
|
307
|
-
capabilities[XENON_CAPABILITIES.INTERCEPTOR_ENABLED] =
|
|
310
|
+
capabilities[XENON_CAPABILITIES.INTERCEPTOR_ENABLED] = flagOrUnset(interceptorObj.enabled);
|
|
308
311
|
capabilities[XENON_CAPABILITIES.INTERCEPTOR_BUFFER_SIZE] = interceptorObj.bufferSize;
|
|
309
|
-
capabilities[XENON_CAPABILITIES.INTERCEPTOR_CAPTURE_BODIES] =
|
|
310
|
-
interceptorObj.captureBodies !== false;
|
|
312
|
+
capabilities[XENON_CAPABILITIES.INTERCEPTOR_CAPTURE_BODIES] = flagOrUnset(interceptorObj.captureBodies);
|
|
311
313
|
capabilities[XENON_CAPABILITIES.INTERCEPTOR_MOCKS] = Array.isArray(interceptorObj.mocks)
|
|
312
314
|
? interceptorObj.mocks
|
|
313
315
|
: [];
|
|
@@ -321,9 +323,9 @@ function getXenonCapabilities(caps) {
|
|
|
321
323
|
else {
|
|
322
324
|
const enabledFlat = getAnyCap(XENON_CAPABILITIES.INTERCEPTOR_ENABLED, 'interceptorEnabled');
|
|
323
325
|
capabilities[XENON_CAPABILITIES.INTERCEPTOR_ENABLED] =
|
|
324
|
-
enabledFlat !== undefined ? String(enabledFlat) === 'true' :
|
|
326
|
+
enabledFlat !== undefined ? String(enabledFlat) === 'true' : undefined;
|
|
325
327
|
capabilities[XENON_CAPABILITIES.INTERCEPTOR_BUFFER_SIZE] = getAnyCap(XENON_CAPABILITIES.INTERCEPTOR_BUFFER_SIZE, 'interceptorBufferSize');
|
|
326
|
-
capabilities[XENON_CAPABILITIES.INTERCEPTOR_CAPTURE_BODIES] =
|
|
328
|
+
capabilities[XENON_CAPABILITIES.INTERCEPTOR_CAPTURE_BODIES] = undefined;
|
|
327
329
|
capabilities[XENON_CAPABILITIES.INTERCEPTOR_MOCKS] = [];
|
|
328
330
|
const includeFlat = getAnyCap(XENON_CAPABILITIES.INTERCEPTOR_INCLUDE_HOSTS, 'interceptorIncludeHosts');
|
|
329
331
|
const excludeFlat = getAnyCap(XENON_CAPABILITIES.INTERCEPTOR_EXCLUDE_HOSTS, 'interceptorExcludeHosts');
|
|
@@ -341,3 +343,18 @@ function getXenonCapabilities(caps) {
|
|
|
341
343
|
`SaveLogs=${capabilities[XENON_CAPABILITIES.SAVE_DEVICE_LOGS]}`);
|
|
342
344
|
return capabilities;
|
|
343
345
|
}
|
|
346
|
+
/** true/false (or 'true'/'false'), undefined when not given; anything else by truthiness. */
|
|
347
|
+
function flagOrUnset(value) {
|
|
348
|
+
if (value === undefined || value === null)
|
|
349
|
+
return undefined;
|
|
350
|
+
if (typeof value === 'boolean')
|
|
351
|
+
return value;
|
|
352
|
+
if (typeof value === 'string') {
|
|
353
|
+
const v = value.trim().toLowerCase();
|
|
354
|
+
if (v === 'true')
|
|
355
|
+
return true;
|
|
356
|
+
if (v === 'false')
|
|
357
|
+
return false;
|
|
358
|
+
}
|
|
359
|
+
return !!value;
|
|
360
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.DASHBOARD_PING_TIMEOUT_MS = exports.DASHBOARD_RETRY_MAX_MS = void 0;
|
|
16
|
+
exports.ownServerOrigin = ownServerOrigin;
|
|
17
|
+
exports.dashboardPluginMiddlewareFor = dashboardPluginMiddlewareFor;
|
|
18
|
+
exports.dashboardPluginMiddleware = dashboardPluginMiddleware;
|
|
19
|
+
const InternalHttpClient_1 = require("../InternalHttpClient");
|
|
20
|
+
const logger_1 = __importDefault(require("../logger"));
|
|
21
|
+
const publicUrl_1 = require("../services/publicUrl");
|
|
22
|
+
/**
|
|
23
|
+
* Where the appium-dashboard-plugin is, when it runs on this Appium server
|
|
24
|
+
* (at `/dashboard`), for the device list: `request['dashboard-plugin-url']`
|
|
25
|
+
* is where Xenon itself fetches the plugin's sessions, and
|
|
26
|
+
* `request['dashboard-plugin-link']` the base of each device's
|
|
27
|
+
* `dashboard_link`, for people to open.
|
|
28
|
+
*
|
|
29
|
+
* Both come from configuration, never from a request. Through 2.14 they came
|
|
30
|
+
* from the Host header of the first /xenon/api request after startup, before
|
|
31
|
+
* the login: Xenon sent a GET to `<that host>/dashboard/api/ping`, kept
|
|
32
|
+
* `<that host>/dashboard` for the life of the process if the answer had
|
|
33
|
+
* `pong`, fetched `<that host>/dashboard/api/sessions` on every device list,
|
|
34
|
+
* and gave every user a link to that host. A forged Host made Xenon call a
|
|
35
|
+
* server of the sender's choosing and point everyone at it.
|
|
36
|
+
*
|
|
37
|
+
* - Xenon reaches the plugin on this server's own address and port
|
|
38
|
+
* (`ownServerOrigin`, a wildcard bind address on 127.0.0.1), over HTTPS when
|
|
39
|
+
* Appium serves it (`--ssl-certificate-path` and `--ssl-key-path`).
|
|
40
|
+
* - People get `<XENON_PUBLIC_URL>/dashboard`, or `/dashboard` on the server
|
|
41
|
+
* their browser already has open when XENON_PUBLIC_URL isn't set.
|
|
42
|
+
*
|
|
43
|
+
* The ping is sent once, with a short time limit, and no request waits for
|
|
44
|
+
* it: a request that arrives while it is out gets no link. A plain-HTTP ping
|
|
45
|
+
* to an HTTPS server got ECONNRESET and was retried three times, about 4 s,
|
|
46
|
+
* while every signed-in request waited on it. Once it succeeds it isn't sent
|
|
47
|
+
* again; after a failure it is retried with an exponential back-off (1 s
|
|
48
|
+
* doubling to 30 s), so a server without the plugin doesn't ping on every
|
|
49
|
+
* request.
|
|
50
|
+
*/
|
|
51
|
+
exports.DASHBOARD_RETRY_MAX_MS = 30000;
|
|
52
|
+
/** How long the ping may take. It goes to this same server. */
|
|
53
|
+
exports.DASHBOARD_PING_TIMEOUT_MS = 2000;
|
|
54
|
+
/**
|
|
55
|
+
* A server's own origin from its bind address and port; a wildcard is reached
|
|
56
|
+
* on 127.0.0.1. `tls` when Appium serves HTTPS.
|
|
57
|
+
*/
|
|
58
|
+
function ownServerOrigin(address, port, tls = false) {
|
|
59
|
+
const wildcard = !address || address === '0.0.0.0' || address === '::';
|
|
60
|
+
const host = wildcard ? '127.0.0.1' : address.includes(':') ? `[${address}]` : address;
|
|
61
|
+
return `${tls ? 'https' : 'http'}://${host}:${port}`;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* The middleware for this server: its bind address, port and whether Appium
|
|
65
|
+
* serves HTTPS, from Appium's CLI arguments. The ping is sent once
|
|
66
|
+
* (`retry: false`) with DASHBOARD_PING_TIMEOUT_MS.
|
|
67
|
+
*/
|
|
68
|
+
function dashboardPluginMiddlewareFor(ownServer) {
|
|
69
|
+
const config = {
|
|
70
|
+
silent: true,
|
|
71
|
+
retry: false,
|
|
72
|
+
timeout: exports.DASHBOARD_PING_TIMEOUT_MS,
|
|
73
|
+
};
|
|
74
|
+
return dashboardPluginMiddleware({
|
|
75
|
+
ownServerOrigin: ownServerOrigin(ownServer.address, ownServer.port, ownServer.tls),
|
|
76
|
+
get: (url) => InternalHttpClient_1.InternalHttpClient.get(url, config),
|
|
77
|
+
warn: (message) => logger_1.default.warn(message),
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
function dashboardPluginMiddleware(deps) {
|
|
81
|
+
var _a, _b;
|
|
82
|
+
const now = (_a = deps.now) !== null && _a !== void 0 ? _a : Date.now;
|
|
83
|
+
const publicBase = (_b = deps.publicBase) !== null && _b !== void 0 ? _b : (() => (0, publicUrl_1.publicServerBase)());
|
|
84
|
+
const pluginUrl = `${deps.ownServerOrigin}/dashboard`;
|
|
85
|
+
let present = false;
|
|
86
|
+
let pinging = false;
|
|
87
|
+
let nextRetryAt = 0;
|
|
88
|
+
let retryDelayMs = 1000;
|
|
89
|
+
const ping = () => __awaiter(this, void 0, void 0, function* () {
|
|
90
|
+
var _a;
|
|
91
|
+
try {
|
|
92
|
+
const response = yield deps.get(`${pluginUrl}/api/ping`);
|
|
93
|
+
if (response && response['pong'])
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
catch (err) {
|
|
97
|
+
(_a = deps.warn) === null || _a === void 0 ? void 0 : _a.call(deps, `[Xenon] Dashboard ping failed, retrying in ${retryDelayMs}ms: ${(err === null || err === void 0 ? void 0 : err.message) || err}`);
|
|
98
|
+
}
|
|
99
|
+
nextRetryAt = now() + retryDelayMs;
|
|
100
|
+
retryDelayMs = Math.min(retryDelayMs * 2, exports.DASHBOARD_RETRY_MAX_MS);
|
|
101
|
+
return false;
|
|
102
|
+
});
|
|
103
|
+
return (req, _res, next) => {
|
|
104
|
+
var _a;
|
|
105
|
+
if (!present && !pinging && now() >= nextRetryAt) {
|
|
106
|
+
// One ping at a time, and nobody waits for it.
|
|
107
|
+
pinging = true;
|
|
108
|
+
void ping().then((ok) => {
|
|
109
|
+
present = ok;
|
|
110
|
+
pinging = false;
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
req['dashboard-plugin-url'] = present ? pluginUrl : '';
|
|
114
|
+
req['dashboard-plugin-link'] = present ? `${(_a = publicBase()) !== null && _a !== void 0 ? _a : ''}/dashboard` : '';
|
|
115
|
+
next();
|
|
116
|
+
};
|
|
117
|
+
}
|
package/lib/src/app/index.js
CHANGED
|
@@ -54,7 +54,6 @@ const pluginArgs_1 = require("../data-service/pluginArgs");
|
|
|
54
54
|
const cors_1 = __importDefault(require("cors"));
|
|
55
55
|
const async_lock_1 = __importDefault(require("async-lock"));
|
|
56
56
|
const crypto_1 = __importDefault(require("crypto"));
|
|
57
|
-
const InternalHttpClient_1 = require("../InternalHttpClient");
|
|
58
57
|
const logger_1 = __importStar(require("../logger"));
|
|
59
58
|
const sessionContext_1 = require("../logging/sessionContext");
|
|
60
59
|
const dashboard_1 = __importDefault(require("./routers/dashboard"));
|
|
@@ -91,7 +90,7 @@ const PluginContext_1 = require("../PluginContext");
|
|
|
91
90
|
const nodeWebDriverUrl_1 = require("../gateway/nodeWebDriverUrl");
|
|
92
91
|
const nodeSessionStatus_1 = require("../gateway/nodeSessionStatus");
|
|
93
92
|
const nodeSessionMetrics_1 = require("../gateway/nodeSessionMetrics");
|
|
94
|
-
const
|
|
93
|
+
const dashboardPluginLink_1 = require("./dashboardPluginLink");
|
|
95
94
|
const ASYNC_LOCK = new async_lock_1.default();
|
|
96
95
|
// An async handler's error reaches the error handlers, as on Express 5.
|
|
97
96
|
(0, apiErrors_1.forwardAsyncErrors)();
|
|
@@ -148,37 +147,6 @@ apiRouter.use((req, res, next) => {
|
|
|
148
147
|
res.setHeader('Expires', '0');
|
|
149
148
|
next();
|
|
150
149
|
});
|
|
151
|
-
// Dashboard state cache - runs once and persists on success.
|
|
152
|
-
// On failure we back off exponentially (1s → 2s → 4s → … capped at 30s)
|
|
153
|
-
// so a down dashboard doesn't turn every API request into a fresh ping.
|
|
154
|
-
let dashboardPluginPromise = null;
|
|
155
|
-
let dashboardNextRetryAt = 0;
|
|
156
|
-
let dashboardRetryDelayMs = 1000;
|
|
157
|
-
const DASHBOARD_RETRY_MAX_MS = 30000;
|
|
158
|
-
apiRouter.use((req, res, next) => __awaiter(void 0, void 0, void 0, function* () {
|
|
159
|
-
if (dashboardPluginPromise === null && Date.now() >= dashboardNextRetryAt) {
|
|
160
|
-
dashboardPluginPromise = (() => __awaiter(void 0, void 0, void 0, function* () {
|
|
161
|
-
const pingurl = `${req.protocol}://${req.get('host')}/dashboard/api/ping`;
|
|
162
|
-
try {
|
|
163
|
-
const response = yield InternalHttpClient_1.InternalHttpClient.get(pingurl, { silent: true });
|
|
164
|
-
if (response && response['pong']) {
|
|
165
|
-
dashboardRetryDelayMs = 1000;
|
|
166
|
-
dashboardNextRetryAt = 0;
|
|
167
|
-
return `${req.protocol}://${req.get('host')}/dashboard`;
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
catch (err) {
|
|
171
|
-
logger_1.default.warn(`[Xenon] Dashboard ping failed, retrying in ${dashboardRetryDelayMs}ms: ${(err === null || err === void 0 ? void 0 : err.message) || err}`);
|
|
172
|
-
}
|
|
173
|
-
dashboardNextRetryAt = Date.now() + dashboardRetryDelayMs;
|
|
174
|
-
dashboardRetryDelayMs = Math.min(dashboardRetryDelayMs * 2, DASHBOARD_RETRY_MAX_MS);
|
|
175
|
-
dashboardPluginPromise = null;
|
|
176
|
-
return '';
|
|
177
|
-
}))();
|
|
178
|
-
}
|
|
179
|
-
req['dashboard-plugin-url'] = dashboardPluginPromise ? yield dashboardPluginPromise : '';
|
|
180
|
-
return next();
|
|
181
|
-
}));
|
|
182
150
|
const findPublicPath = () => {
|
|
183
151
|
const rootDir = path_1.default.resolve(__dirname, '..', '..');
|
|
184
152
|
const searchPaths = [
|
|
@@ -227,7 +195,13 @@ router.use('/api', apiRouter);
|
|
|
227
195
|
// express.static mount here, outside the login, which also served every
|
|
228
196
|
// recording and interceptor capture stored under the same folder.
|
|
229
197
|
router.use(staticFilesRouter);
|
|
230
|
-
|
|
198
|
+
/**
|
|
199
|
+
* `ownServer` is this Appium server's bind address and port, and whether it
|
|
200
|
+
* serves HTTPS (from its CLI arguments). With it, the device list links to an appium-dashboard-plugin on
|
|
201
|
+
* this server (dashboardPluginLink.ts); without it, as in specs that build a
|
|
202
|
+
* router alone, there is none.
|
|
203
|
+
*/
|
|
204
|
+
function createRouter(pluginArgs, ownServer) {
|
|
231
205
|
// CSRF defense (runs before /health so even a hostile GET->POST confused-
|
|
232
206
|
// deputy has no soft target). Pass-through for GETs, for header-authed
|
|
233
207
|
// callers, and when authDisabled=true. Returns 403 for cookie-authed
|
|
@@ -254,6 +228,11 @@ function createRouter(pluginArgs) {
|
|
|
254
228
|
// provisioned.
|
|
255
229
|
apiRouter.use(authMiddleware_1.authMiddleware);
|
|
256
230
|
apiRouter.use((0, rateLimitMiddleware_1.rateLimitMiddleware)());
|
|
231
|
+
// The appium-dashboard-plugin's address, for the device list, from this
|
|
232
|
+
// server's own address and XENON_PUBLIC_URL. Never from a request: it was
|
|
233
|
+
// built from the Host header of the first request, before the login.
|
|
234
|
+
if (ownServer)
|
|
235
|
+
apiRouter.use((0, dashboardPluginLink_1.dashboardPluginMiddlewareFor)(ownServer));
|
|
257
236
|
// Authenticated auth endpoints: /me, /change-password, /dashboard-session
|
|
258
237
|
apiRouter.use('/auth', (0, auth_1.authAuthedRouter)());
|
|
259
238
|
// Admin: API key management
|