@xenon-device-management/xenon 2.14.0 → 2.16.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 +44 -29
- package/lib/package.json +6 -5
- package/lib/public/assets/{AnnotationOverlay-B_VNtKBH.js → AnnotationOverlay-2W4JvEyZ.js} +1 -1
- package/lib/public/assets/{ApiKeyGate-BwuaMjkX.js → ApiKeyGate-GmckX_7r.js} +1 -1
- package/lib/public/assets/DeviceMosaicView-DqODzkyo.js +2 -0
- package/lib/public/assets/{EmptyState-BSjhk_IV.js → EmptyState-7hETKEWb.js} +1 -1
- package/lib/public/assets/{FieldGroup-Cb5QZsE1.js → FieldGroup-GOgssFEP.js} +1 -1
- package/lib/public/assets/{FilterMenu-BAaSlWbW.js → FilterMenu-Yvf6RGMd.js} +1 -1
- package/lib/public/assets/{Menu-JGGYrSQQ.js → Menu-KrPnfdsf.js} +1 -1
- package/lib/public/assets/{Modal-CFCp3n9d.js → Modal-C6WjZFeI.js} +1 -1
- package/lib/public/assets/{RecordingPage-CbJj_EBT.js → RecordingPage-CSRIhZEh.js} +1 -1
- package/lib/public/assets/{RecordingsPage-BJeYnygR.js → RecordingsPage-CB8ST3nE.js} +1 -1
- package/lib/public/assets/{SegmentedControl-Bg42Atjy.js → SegmentedControl-C9o90KId.js} +1 -1
- package/lib/public/assets/{SettingCard-Njz1pUIJ.js → SettingCard-CPm3w9l2.js} +1 -1
- package/lib/public/assets/{Table-BcTm5xV2.js → Table-DwIgERRt.js} +1 -1
- package/lib/public/assets/{activity-DOVdsfMQ.js → activity-DrLbHifn.js} +1 -1
- package/lib/public/assets/ai-settings-DU_Ls06P.js +16 -0
- package/lib/public/assets/{api-keys-BKnNSTw9.js → api-keys-CWYdEzJ_.js} +1 -1
- package/lib/public/assets/{apps-D0SC6uw6.js → apps-BqNVdln7.js} +1 -1
- package/lib/public/assets/{arrow-left-MMbTmgoE.js → arrow-left-BtB0_Jg_.js} +1 -1
- package/lib/public/assets/{arrow-right-BMfnGmxW.js → arrow-right-C8NhdtyN.js} +1 -1
- package/lib/public/assets/{arrow-up-right-DuAftMWB.js → arrow-up-right-gMg0ffvv.js} +1 -1
- package/lib/public/assets/{auth-shell-nZC2JNDO.js → auth-shell-CiYZ3N4M.js} +2 -2
- package/lib/public/assets/{builds-page-hDCgTwkS.js → builds-page-EaaqDmLN.js} +1 -1
- package/lib/public/assets/{button-Dda4irn_.js → button-DDau4Axv.js} +1 -1
- package/lib/public/assets/{calendar-BMr0Q62v.js → calendar-DQ83B1ed.js} +1 -1
- package/lib/public/assets/{check-CZ9iw9-e.js → check-K1ovTQam.js} +1 -1
- package/lib/public/assets/{chevron-right-BRYJJVC9.js → chevron-right-Biq00pMt.js} +1 -1
- package/lib/public/assets/{circle-check-CqAJjQ5O.js → circle-check-CBv88cH7.js} +1 -1
- package/lib/public/assets/{circle-x-DlDJtnVz.js → circle-x-Bmltailn.js} +1 -1
- package/lib/public/assets/{clock-CpuDKysU.js → clock-BPrld-3h.js} +1 -1
- package/lib/public/assets/{copy-CAQydNFe.js → copy-t0QB14JI.js} +1 -1
- package/lib/public/assets/derive-UnLRp4r3.js +1 -0
- package/lib/public/assets/device-explorer-BgGZ8AvV.js +282 -0
- package/lib/public/assets/{download-C9XG6lLv.js → download-BroHxNOh.js} +1 -1
- package/lib/public/assets/forgot-password-Bjmf49i6.js +1 -0
- package/lib/public/assets/{h264Stream-VQBphYhg.js → h264Stream-DeD__6wa.js} +6 -6
- package/lib/public/assets/{index-DJGc1M2f.js → index-72oF89LJ.js} +1 -1
- package/lib/public/assets/index-B0JeuZbg.js +15 -0
- package/lib/public/assets/index-B0gQkEg0.js +219 -0
- package/lib/public/assets/{input-Cm0tmSoc.js → input-DIc4MSC7.js} +1 -1
- package/lib/public/assets/{line-chart-D2DyJCml.js → line-chart-BcWm2vW8.js} +1 -1
- package/lib/public/assets/{list-checks-UWf1GY99.js → list-checks-B06IJJad.js} +1 -1
- package/lib/public/assets/{lock-CCJv9m_I.js → lock-pVLvnN30.js} +1 -1
- package/lib/public/assets/{login-f8vOVW-U.js → login-CN77vy--.js} +1 -1
- package/lib/public/assets/{maintenance-settings-C1PrkuAH.js → maintenance-settings-CSdzQSkY.js} +1 -1
- package/lib/public/assets/{monitor-B2jDgwm7.js → monitor-BtrN5IkI.js} +1 -1
- package/lib/public/assets/{mouse-pointer-2-DLI1RtzU.js → mouse-pointer-2-CNsQfo6w.js} +1 -1
- package/lib/public/assets/{network-ebT86qoi.js → network-BSfA8ZxG.js} +1 -1
- package/lib/public/assets/{overview-C0aykVrd.js → overview-BfTebCK5.js} +1 -1
- package/lib/public/assets/{page-header-BjzgWz-T.js → page-header-D8KUX6Qg.js} +1 -1
- package/lib/public/assets/{play-DLG9t8FU.js → play-Cvc8xzic.js} +1 -1
- package/lib/public/assets/{plus-nBFSMu_X.js → plus-C9Zj84UF.js} +1 -1
- package/lib/public/assets/{profile-page-CSl0UYlv.js → profile-page-Bi27STgs.js} +1 -1
- package/lib/public/assets/{reset-password-LeOuHqmB.js → reset-password-CPsOuwSu.js} +1 -1
- package/lib/public/assets/runbook-page-DEQ-DBOX.js +304 -0
- package/lib/public/assets/{select-88jHcl-Z.js → select-BWpnhWXj.js} +1 -1
- package/lib/public/assets/{selector-detail-redirect-BbXKJjZM.js → selector-detail-redirect-BefSCSpe.js} +1 -1
- package/lib/public/assets/{selector-health-page-B4rA1Xkv.js → selector-health-page-MSk96Zz7.js} +1 -1
- package/lib/public/assets/session-detail-page-DPyBNef_.js +4 -0
- package/lib/public/assets/{settings-Dep_3kbA.css → settings-K8UHWMdI.css} +1 -1
- package/lib/public/assets/{settings-GF6ZMSQ6.js → settings-V6x-cXwy.js} +1 -1
- package/lib/public/assets/{stat-tile-BV98e_Sv.js → stat-tile-Duex-GZD.js} +1 -1
- package/lib/public/assets/{tablet-BiAsnFDj.js → tablet-BBcoqBmL.js} +1 -1
- package/lib/public/assets/{teams-B4-jrr-1.js → teams-ts8p5rEh.js} +1 -1
- package/lib/public/assets/{trash-2-C5RSuX0e.js → trash-2-CZ2ew6AZ.js} +1 -1
- package/lib/public/assets/{upload-BG7_rMXK.js → upload-Dv99y_le.js} +1 -1
- package/lib/public/assets/{use-builds-data-J-riQYyC.js → use-builds-data-DrYC8JCF.js} +1 -1
- package/lib/public/assets/{use-password-reset-mode-C8-eH-UG.js → use-password-reset-mode-C7QkBNkP.js} +1 -1
- package/lib/public/assets/{users-CFba5TSy.js → users-D7tKMESH.js} +1 -1
- package/lib/public/assets/{users-Cp0beJm3.js → users-gJ5Y01yo.js} +1 -1
- package/lib/public/assets/{video-off-DDsEISjV.js → video-off-CqZVuZGs.js} +1 -1
- package/lib/public/assets/{webhook-settings-BGu32vSB.js → webhook-settings-DI5DSW99.js} +1 -1
- package/lib/public/assets/{zap-l4VaXzq8.js → zap-ClcHFxfo.js} +1 -1
- package/lib/public/index.html +1 -1
- package/lib/schema.json +16 -12
- package/lib/src/InternalHttpClient.js +27 -5
- package/lib/src/XenonCapabilityManager.js +10 -3
- package/lib/src/app/dashboardPluginLink.js +117 -0
- package/lib/src/app/index.js +13 -34
- package/lib/src/app/openapi/control.yaml +6 -4
- package/lib/src/app/openapi/grid.yaml +8 -2
- package/lib/src/app/openapi/identity.yaml +103 -31
- package/lib/src/app/openapi/platform.yaml +33 -15
- package/lib/src/app/openapi/sessions.yaml +12 -3
- package/lib/src/app/routers/apikeys.js +14 -2
- package/lib/src/app/routers/auth.js +31 -6
- package/lib/src/app/routers/control.js +13 -5
- package/lib/src/app/routers/dashboard.js +42 -27
- package/lib/src/app/routers/grid.js +5 -3
- package/lib/src/app/routers/profile.js +21 -0
- package/lib/src/app/routers/users.js +12 -6
- package/lib/src/app/swagger.js +2 -0
- package/lib/src/app/ws/h264StreamWs.js +10 -1
- package/lib/src/app/ws/nodeSocketRelay.js +77 -11
- package/lib/src/config.js +1 -0
- package/lib/src/dashboard/commandLogFields.js +42 -0
- package/lib/src/dashboard/commands.js +5 -5
- package/lib/src/dashboard/event-manager.js +64 -204
- package/lib/src/dashboard/services/failure-analysis-service.js +118 -105
- package/lib/src/dashboard/services/failureCategories.js +264 -0
- package/lib/src/dashboard/sessionCommandSummary.js +39 -0
- package/lib/src/data-service/device-store.js +14 -1
- package/lib/src/data-service/prisma-store.js +19 -7
- package/lib/src/data-service/web-config-service.js +7 -0
- package/lib/src/device-managers/AndroidDeviceManager.js +61 -23
- package/lib/src/device-managers/IOSDeviceManager.js +10 -0
- package/lib/src/device-managers/android/AndroidH264StreamService.js +69 -20
- package/lib/src/device-managers/android/H264Multiplexer.js +38 -4
- package/lib/src/device-utils.js +29 -3
- package/lib/src/gateway/defaultGateway.js +1 -1
- package/lib/src/gateway/forwardToNode.js +6 -11
- package/lib/src/gateway/healReport.js +87 -0
- package/lib/src/gateway/hubSessionToken.js +27 -4
- package/lib/src/gateway/nodeWebDriverUrl.js +3 -5
- package/lib/src/gateway/sessionGateway.js +12 -2
- package/lib/src/generated/client/edge.js +4 -3
- package/lib/src/generated/client/index-browser.js +1 -0
- package/lib/src/generated/client/index.d.ts +32 -0
- package/lib/src/generated/client/index.js +4 -3
- package/lib/src/generated/client/package.json +1 -1
- package/lib/src/generated/client/schema.prisma +3 -0
- package/lib/src/generated/client/wasm.js +1 -0
- package/lib/src/helpers/outboundProxy.js +219 -0
- package/lib/src/index.js +21 -0
- package/lib/src/interceptors/CommandInterceptor.js +256 -196
- package/lib/src/interceptors/xenonScripts.js +28 -7
- package/lib/src/logger.js +3 -1
- 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 +16 -6
- package/lib/src/scripts/database-check.js +3 -3
- package/lib/src/scripts/initialize-database.js +35 -33
- package/lib/src/scripts/run-migrations.js +1079 -27
- package/lib/src/services/AICommandService.js +9 -2
- package/lib/src/services/AIService.js +192 -42
- package/lib/src/services/ApiKeyService.js +16 -5
- package/lib/src/services/EmailService.js +29 -0
- package/lib/src/services/InterceptorService.js +6 -2
- package/lib/src/services/OrphanSweeper.js +8 -0
- package/lib/src/services/SelectorStateService.js +11 -7
- package/lib/src/services/SelectorVerificationJob.js +21 -6
- package/lib/src/services/ServerManager.js +11 -1
- package/lib/src/services/SessionLifecycleService.js +118 -44
- package/lib/src/services/SocketClient.js +11 -0
- package/lib/src/services/SocketServer.js +380 -45
- package/lib/src/services/TeamService.js +5 -1
- package/lib/src/services/TracingService.js +171 -60
- package/lib/src/services/UserService.js +14 -3
- package/lib/src/services/UserSessionService.js +16 -4
- 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 +57 -72
- 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/identity/identityChanges.js +41 -0
- package/lib/src/services/logcat/SessionDeviceLogs.js +358 -0
- package/lib/src/services/logcat/deviceLogBook.js +164 -0
- package/lib/src/services/logcat/iosDriverLog.js +178 -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 +83 -56
- 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/selector-health/SelectorVisibilityResolver.js +121 -0
- package/lib/src/services/session/sessionIdentity.js +5 -3
- package/lib/src/services/session/shutdownReason.js +26 -0
- package/lib/src/services/settings/aiEngineSettings.js +219 -0
- package/lib/src/services/token/JwtKeyService.js +8 -2
- package/lib/src/services/token/mcpTokenTtl.js +27 -0
- package/lib/src/services/token/mintedLifetime.js +51 -0
- package/lib/src/sessions/LocalSession.js +29 -1
- package/lib/src/sessions/RemoteSession.js +12 -2
- package/lib/src/sessions/SessionManager.js +4 -1
- package/lib/src/sessions/sessionMemory.js +25 -0
- package/lib/test/helpers/driver-log.js +38 -0
- package/lib/test/helpers/fake-ai-provider.js +188 -0
- package/lib/test/helpers/fake-http-proxy.js +106 -0
- package/lib/test/helpers/loki-stores.js +33 -0
- package/lib/test/helpers/otlp-probe.js +107 -0
- package/lib/test/helpers/page-sources.js +164 -0
- package/lib/test/helpers/scratch-database.js +57 -13
- package/lib/test/integration/androidDevices.spec.js +1 -1
- 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/ios/01iOSSimulator.spec.js +1 -1
- package/lib/test/integration/ios/02iOSDevices.spec.js +1 -1
- 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/selector-state-flow.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/XenonCapabilityManager.spec.js +23 -0
- 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-adb-retry.spec.js +112 -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/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/command-log-network-capture.spec.js +191 -0
- package/lib/test/unit/dashboard-events-device-logs.spec.js +255 -0
- package/lib/test/unit/dashboard-events-team-scope.spec.js +6 -3
- package/lib/test/unit/dashboard-plugin-link.spec.js +213 -0
- package/lib/test/unit/database-check.spec.js +1 -0
- package/lib/test/unit/device-log-book.spec.js +180 -0
- package/lib/test/unit/device-utils.spec.js +13 -4
- package/lib/test/unit/enable-dashboard-option.spec.js +57 -0
- package/lib/test/unit/event-log-session-data.spec.js +251 -0
- package/lib/test/unit/failure-analysis.spec.js +196 -0
- package/lib/test/unit/failure-categories.spec.js +130 -0
- package/lib/test/unit/failure-categorization.spec.js +393 -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 +74 -29
- package/lib/test/unit/healing-tiers-every-session.spec.js +219 -0
- package/lib/test/unit/hub-node-gateway-appium-server.spec.js +243 -1
- package/lib/test/unit/interceptor-visual-elements.spec.js +192 -0
- package/lib/test/unit/ios-driver-log.spec.js +168 -0
- package/lib/test/unit/ios-simulator-session-readiness.spec.js +187 -0
- package/lib/test/unit/ios-simulator-session-teardown.spec.js +158 -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/logcat-ws.spec.js +78 -37
- package/lib/test/unit/logger-json-levels.spec.js +44 -0
- package/lib/test/unit/mcp-token-ttl.spec.js +38 -0
- package/lib/test/unit/observability-example-loki.spec.js +25 -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/omni-virtual-element-store.spec.js +301 -0
- package/lib/test/unit/omni-vision-ios-points.spec.js +0 -2
- package/lib/test/unit/omni-vision.spec.js +5 -7
- package/lib/test/unit/openapi-for-site.spec.js +80 -0
- package/lib/test/unit/outbound-proxy.spec.js +320 -0
- package/lib/test/unit/package-engines.spec.js +27 -0
- package/lib/test/unit/passwordResetMode.test.js +10 -2
- package/lib/test/unit/port-exhaustion.spec.js +2 -1
- package/lib/test/unit/profile-router.test.js +122 -1
- package/lib/test/unit/proxy-option.spec.js +358 -0
- package/lib/test/unit/queue-requester.spec.js +1 -1
- 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/run-migrations-database.spec.js +807 -0
- package/lib/test/unit/run-migrations-fail-fast.test.js +41 -27
- package/lib/test/unit/run-migrations.test.js +864 -0
- package/lib/test/unit/selector-health-actions.spec.js +3 -1
- package/lib/test/unit/selector-learning-every-session.spec.js +179 -0
- package/lib/test/unit/selector-state-service.spec.js +72 -23
- package/lib/test/unit/selector-verification-clean-build.spec.js +130 -0
- package/lib/test/unit/selector-verification-job.spec.js +28 -19
- package/lib/test/unit/selector-verification-missed-regression.spec.js +2 -2
- package/lib/test/unit/selector-visibility-resolver.spec.js +209 -0
- package/lib/test/unit/self-healing-boot.spec.js +10 -5
- package/lib/test/unit/self-healing-setting.spec.js +7 -6
- 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-ios.spec.js +180 -0
- package/lib/test/unit/session-device-logs.spec.js +356 -0
- package/lib/test/unit/session-end-failure-analysis.spec.js +133 -0
- package/lib/test/unit/session-log-indexes.spec.js +64 -3
- package/lib/test/unit/socket-team-scope.integration.spec.js +749 -0
- package/lib/test/unit/socketServer.scopedEmit.spec.js +237 -5
- package/lib/test/unit/socketServer.sessionCookie.spec.js +2 -1
- package/lib/test/unit/stream-start-pipelines.spec.js +72 -18
- package/lib/test/unit/token-mint-limits.spec.js +165 -0
- package/lib/test/unit/tracing-export-routing.spec.js +137 -0
- package/lib/test/unit/tracing-session-spans.spec.js +337 -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 +3 -2
- package/lib/test/unit/web-config-service.spec.js +19 -0
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +6 -5
- package/prisma/migrations/20261004174413_locator_etalon_path/migration.sql +2 -0
- package/prisma/schema.prisma +3 -0
- package/schema.json +16 -12
- package/scripts/dev/readme-screenshots.js +276 -23
- package/scripts/dev/screenshot-fixtures/logcat.js +157 -0
- package/scripts/dev/screenshot-fixtures/phone-screen.html +469 -0
- package/scripts/dev/screenshot-fixtures/session.js +242 -0
- package/scripts/export-openapi.js +32 -0
- package/scripts/lib/openapi-for-site.js +79 -0
- package/lib/public/assets/BugReportButton-BAt0_pzq.js +0 -15
- package/lib/public/assets/DeviceMosaicView-BYaQHhTf.js +0 -2
- package/lib/public/assets/ai-settings-CsuJroRf.js +0 -21
- package/lib/public/assets/device-explorer-CdO6_nrn.js +0 -282
- package/lib/public/assets/forgot-password-CGnkRgE9.js +0 -1
- package/lib/public/assets/index-BbKEXUFH.js +0 -219
- package/lib/public/assets/runbook-page-CMMeQml9.js +0 -124
- package/lib/public/assets/session-detail-page-COL_RZxw.js +0 -4
- package/lib/src/scripts/prepare-prisma.js +0 -89
- /package/lib/public/assets/{BugReportButton-CPBjfiv4.css → index-CPBjfiv4.css} +0 -0
package/README.md
CHANGED
|
@@ -53,7 +53,7 @@ Xenon sits inside Appium and turns a set of Android and iOS devices, real or vir
|
|
|
53
53
|
## Highlights
|
|
54
54
|
|
|
55
55
|
**Device lab**
|
|
56
|
-
- Finds Android devices and emulators (adb), iPhones
|
|
56
|
+
- Finds Android devices and emulators (adb), and iPhones and iOS simulators on a Mac, by itself.
|
|
57
57
|
- Allocates a free, healthy device per session, and queues requests when none is free.
|
|
58
58
|
- One hub, many nodes: every machine's devices form one pool, behind one URL and one set of rules.
|
|
59
59
|
- Teams decide who may use which device; reservations and programmatic leases hold one for a person or a pipeline.
|
|
@@ -80,12 +80,15 @@ Xenon sits inside Appium and turns a set of Android and iOS devices, real or vir
|
|
|
80
80
|
|
|
81
81
|
| Component | Needed |
|
|
82
82
|
|---|---|
|
|
83
|
-
| **Node.js** | 20.19 or later
|
|
84
|
-
| **Appium** | 3.
|
|
85
|
-
| **Android** | Android SDK platform tools (`adb`) and the UiAutomator2 driver |
|
|
86
|
-
| **iOS** |
|
|
83
|
+
| **Node.js** | 20.19 or later in the 20 line, 22.12 or later in the 22 line, or 24 and later, with npm 10 or later: Appium 3's own range |
|
|
84
|
+
| **Appium** | 3.1.1 or later in the 3 line (`npm i -g appium`) |
|
|
85
|
+
| **Android** | The Android SDK platform tools (`adb`), with `ANDROID_HOME` set to the SDK's folder, and the UiAutomator2 driver |
|
|
86
|
+
| **iOS** | A Mac with Xcode and the XCUITest driver. Real iPhones also need Xenon's own copy of go-ios in `~/.cache/xenon/goIOS`, which you download once, and again after upgrading Xenon ([how](https://xenon-6e6.pages.dev/docs/installation#iphones-and-go-ios)) |
|
|
87
87
|
| **Database** | SQLite, built in: a file under `~/.cache/xenon`. Each server, hub or node, keeps its own. |
|
|
88
|
-
| **
|
|
88
|
+
| **Recording** | ffmpeg, which comes with Xenon |
|
|
89
|
+
| **Optional** | An AI provider key (Gemini, OpenAI, Anthropic or a local Ollama) for the AI healing tiers |
|
|
90
|
+
|
|
91
|
+
[Installation and requirements](https://xenon-6e6.pages.dev/docs/installation) has the details, and where Xenon keeps its data.
|
|
89
92
|
|
|
90
93
|
## Quick start
|
|
91
94
|
|
|
@@ -97,7 +100,7 @@ appium driver install uiautomator2 # Android
|
|
|
97
100
|
appium driver install xcuitest # iOS (macOS only)
|
|
98
101
|
```
|
|
99
102
|
|
|
100
|
-
**2. Start Appium with Xenon
|
|
103
|
+
**2. Start Appium with Xenon.** The dashboard is always served; `--plugin-xenon-enable-dashboard` records your sessions for its **Sessions** and **Selector Health** pages.
|
|
101
104
|
|
|
102
105
|
```bash
|
|
103
106
|
appium server --use-plugins=xenon \
|
|
@@ -105,6 +108,8 @@ appium server --use-plugins=xenon \
|
|
|
105
108
|
--plugin-xenon-enable-dashboard
|
|
106
109
|
```
|
|
107
110
|
|
|
111
|
+
For Android, export `ANDROID_HOME` (the Android SDK's folder) in that shell first: Xenon finds `adb` through it.
|
|
112
|
+
|
|
108
113
|
**3. Open the dashboard** at [http://localhost:4723/xenon/](http://localhost:4723/xenon/) and sign in as the first super admin. Unless you set `XENON_BOOTSTRAP_ADMIN_EMAIL` and `XENON_BOOTSTRAP_ADMIN_PASSWORD` before the first start, that is `admin@xenon.local` / `Admin@123`. **Change it at once** on any machine others can reach.
|
|
109
114
|
|
|
110
115
|
**4. Point a test at it.** Use your access key and an API token; both are under **Profile** in the dashboard, where you create the token:
|
|
@@ -124,7 +129,7 @@ const capabilities = {
|
|
|
124
129
|
// connect to http://localhost:4723 with these capabilities.
|
|
125
130
|
```
|
|
126
131
|
|
|
127
|
-
The session appears on the dashboard under **Sessions**, with its video and logs once it ends.
|
|
132
|
+
The session appears on the dashboard under **Sessions**, with its video and logs once it ends. The site's [Quick start](https://xenon-6e6.pages.dev/docs/quick-start) goes through the same steps in more detail.
|
|
128
133
|
|
|
129
134
|
## Hub and nodes
|
|
130
135
|
|
|
@@ -139,7 +144,7 @@ appium server --use-plugins=xenon \
|
|
|
139
144
|
--plugin-xenon-hub=http://hub.example.com:4723
|
|
140
145
|
```
|
|
141
146
|
|
|
142
|
-
[
|
|
147
|
+
[Hub and nodes](https://xenon-6e6.pages.dev/docs/hub-and-nodes) covers creating the node's user, its token, and recovering a lost one.
|
|
143
148
|
|
|
144
149
|
## Configuration
|
|
145
150
|
|
|
@@ -158,7 +163,7 @@ server:
|
|
|
158
163
|
buildCleanupDays: 30 # how long builds, videos and screenshots are kept
|
|
159
164
|
```
|
|
160
165
|
|
|
161
|
-
Every option, with its default, is in [
|
|
166
|
+
Every option, with its default, is in [Configuration](https://xenon-6e6.pages.dev/docs/configuration), and [Data retention](https://xenon-6e6.pages.dev/docs/retention) explains the cleanup job. Lab-wide settings such as health checks, cleanup and the AI provider can also be changed in the dashboard's **Settings**, **AI engine** and **Maintenance** pages; changing them needs a super admin. A health-check, cleanup or AI engine value saved there replaces the option or environment variable the server was started with, and applies without a restart.
|
|
162
167
|
|
|
163
168
|
### Environment variables
|
|
164
169
|
|
|
@@ -166,20 +171,25 @@ Keep credentials in the environment, not in config files or shell history.
|
|
|
166
171
|
|
|
167
172
|
| Variable | What it does |
|
|
168
173
|
|---|---|
|
|
169
|
-
| `XENON_BOOTSTRAP_ADMIN_EMAIL`, `XENON_BOOTSTRAP_ADMIN_PASSWORD` | The first super admin, created on the
|
|
174
|
+
| `XENON_BOOTSTRAP_ADMIN_EMAIL`, `XENON_BOOTSTRAP_ADMIN_PASSWORD` | The first super admin, created on the server's first start. |
|
|
170
175
|
| `XENON_AI_PROVIDER` | `gemini`, `openai`, `anthropic` or `ollama`, for the AI healing tiers. |
|
|
171
176
|
| `XENON_GEMINI_API_KEY`, `XENON_OPENAI_API_KEY`, `XENON_ANTHROPIC_API_KEY` | The provider's key. The dashboard never stores or shows keys. |
|
|
172
177
|
| `XENON_AI_MODEL`, `XENON_AI_BASE_URL` | A different model, or a custom endpoint such as a local Ollama. |
|
|
173
178
|
| `DATABASE_URL` | Where the SQLite database lives, as `file:/path/to/xenon.db`. Defaults to a file under `~/.cache/xenon`. The published plugin stores its data in SQLite only and won't start on a PostgreSQL URL. |
|
|
174
|
-
| `XENON_AUTO_MIGRATE` | `true` (default)
|
|
179
|
+
| `XENON_AUTO_MIGRATE` | `true` (default) brings the database up to date at startup: `prisma migrate deploy` if it keeps a migration history that matches its tables (or can be made to), `prisma db push` if not. Set `false` if your pipeline does it. |
|
|
175
180
|
| `XENON_HUB_ACCESS_KEY`, `XENON_HUB_TOKEN` | On a node: the credentials it uses to talk to its hub. |
|
|
176
181
|
| `XENON_REQUIRE_SESSION_TOKEN` | Refuse sessions created without valid credentials. |
|
|
177
182
|
| `XENON_REQUIRE_COMMAND_AUTH` | Check credentials on every Appium command, not only when the session is created. |
|
|
178
183
|
| `XENON_ALLOWED_ORIGINS` | Extra origins the dashboard may be served from, for a reverse proxy on another host. |
|
|
184
|
+
| `XENON_PUBLIC_URL` | The address people reach this server at, such as `https://xenon.example.com` or `http://lab-mac:4723` (the dashboard's address, ending in `/xenon/`, works too). Password reset links and device dashboard links point here; without it Xenon emails no reset links. |
|
|
185
|
+
| `XENON_SMTP_URL`, `XENON_SMTP_FROM` | The mail server for password reset links, and the sender. Needs `XENON_PUBLIC_URL`. |
|
|
186
|
+
| `XENON_USER_SESSION_TTL_MS` | How long a dashboard sign-in lasts without use (default 24 hours). |
|
|
179
187
|
| `XENON_AUTH_DISABLED` | `true` turns sign-in off. For local development only. |
|
|
180
188
|
| `XENON_JSON_LOGGING` | `true` writes JSON log lines. Used only when the `enableJsonLogging` option isn't set; the option, true or false, wins. |
|
|
181
189
|
| `XENON_MAX_CONCURRENT_RECORDINGS`, `XENON_RECORDINGS_ASSETS_PATH` | The cap on simultaneous Live Devices recordings (default 4) and where they are stored. Used only when the `maxConcurrentRecordings` and `recordingsAssetsPath` options aren't set. |
|
|
182
190
|
|
|
191
|
+
[Environment variables](https://xenon-6e6.pages.dev/docs/environment-variables) lists every variable Xenon reads.
|
|
192
|
+
|
|
183
193
|
## Capabilities for your tests
|
|
184
194
|
|
|
185
195
|
Xenon's own capabilities use the `xe:` prefix. Credentials and other options go in `xe:options`.
|
|
@@ -188,13 +198,14 @@ Xenon's own capabilities use the `xe:` prefix. Credentials and other options go
|
|
|
188
198
|
|---|---|
|
|
189
199
|
| `xe:options` | `accessKey` and `token`, or a `sessionToken`; a lease's `leaseId` and `leaseToken`; optionally a `team`. Xenon removes the credentials before the driver or any record sees them. |
|
|
190
200
|
| `xe:build`, `xe:name` | Group sessions into a build and name them on the dashboard. |
|
|
191
|
-
| `xe:record_video` | Record the session's video. |
|
|
192
|
-
| `xe:screenshot_on_failure`, `xe:screenshot_on_every_command` | Take
|
|
193
|
-
| `xe:save_device_logs` | Keep the device's logs with the session. |
|
|
201
|
+
| `xe:record_video` | Record the session's video. On by default; `false` turns it off. |
|
|
202
|
+
| `xe:screenshot_on_failure`, `xe:screenshot_on_every_command` | Take a screenshot when a command fails (on by default), or after every command. |
|
|
194
203
|
| `appium:udids`, `appium:minSDK`, `appium:maxSDK`, `appium:tags` | Narrow which devices the session may get. |
|
|
195
204
|
| `appium:iPhoneOnly`, `appium:iPadOnly`, `appium:filterByHost` | Limit to iPhones (simulators and real devices), to iPads, or to one node. If both are true, you get an iPad. |
|
|
196
205
|
| `appium:deviceAvailabilityTimeout`, `appium:deviceRetryInterval` | How long to wait for a free device, and how often to look (ms). |
|
|
197
206
|
|
|
207
|
+
[Capabilities](https://xenon-6e6.pages.dev/docs/capabilities) has every capability Xenon reads.
|
|
208
|
+
|
|
198
209
|
From inside a test, the `xenon:` execute commands report to the dashboard:
|
|
199
210
|
|
|
200
211
|
```js
|
|
@@ -204,11 +215,11 @@ await driver.execute('xenon: captureEvidence', { reason: 'Payment confirmed' });
|
|
|
204
215
|
|
|
205
216
|
Also available: `setSessionName`, `addTag` and `debug`, and on Android with network capture on, `addMock`, `getRequests` and `exportHar`.
|
|
206
217
|
|
|
207
|
-
The five that write to the dashboard (`setSessionStatus`, `captureEvidence`, `setSessionName`, `addTag`, `debug`) answer `{ recorded: true }`, or `{ recorded: false, message }` when nothing was saved, for example on a server
|
|
218
|
+
The five that write to the dashboard (`setSessionStatus`, `captureEvidence`, `setSessionName`, `addTag`, `debug`) answer `{ recorded: true }`, or `{ recorded: false, message }` when nothing was saved, for example on a server started without `enableDashboard`; they never fail the test. A `xenon:` command Xenon doesn't have fails with `unknown command`.
|
|
208
219
|
|
|
209
|
-
With an AI provider configured, `assertVisualState` answers `{ result, message }` with the provider's verdict on a screenshot, and fails when it couldn't check. `smartTap` finds text of several words, such as `Sign in`, and taps the right spot on iPhones too.
|
|
220
|
+
With an AI provider configured, `assertVisualState` answers `{ result, message }` with the provider's verdict on a screenshot, and fails when it couldn't check. `smartTap` finds text of several words, such as `Sign in`, and taps the right spot on iPhones too. [Execute commands](https://xenon-6e6.pages.dev/docs/execute-commands) lists them all.
|
|
210
221
|
|
|
211
|
-
For CI, a **lease** reserves a device before the test starts and hands back ready-made capabilities: `POST /xenon/api/sdk/leases`. See
|
|
222
|
+
For CI, a **lease** reserves a device before the test starts and hands back ready-made capabilities: `POST /xenon/api/sdk/leases`. See [Leases for CI](https://xenon-6e6.pages.dev/docs/leases).
|
|
212
223
|
|
|
213
224
|
## Self-healing
|
|
214
225
|
|
|
@@ -216,16 +227,16 @@ When `findElement` can't find an element, Xenon tries six strategies in turn, ch
|
|
|
216
227
|
|
|
217
228
|
| Tier | Strategy | How it finds the element |
|
|
218
229
|
|---|---|---|
|
|
219
|
-
| 0 | **Resilio** |
|
|
220
|
-
| 1 | **Native** | The original selector, retried |
|
|
230
|
+
| 0 | **Resilio** | The element's path through the screen's element tree, stored from an earlier run, compared with the tree on screen now |
|
|
231
|
+
| 1 | **Native** | The original selector: the find that just failed, retried until autowait's timeout when autowait is on |
|
|
221
232
|
| 2 | **Fuzzy XML** | The page source compared with the stored fingerprint |
|
|
222
233
|
| 3 | **OCR** | The element's text read from a screenshot |
|
|
223
234
|
| 4 | **Visual AI** | A screenshot analysed by the configured AI provider |
|
|
224
235
|
| 5 | **LLM** | The page source and the failed selector reasoned about by an LLM |
|
|
225
236
|
|
|
226
|
-
Before healing, an optional **autowait** retries `findElement` for a while, since most "broken" selectors are slow screens. Turn healing off with `--plugin-xenon-enable-self-healing
|
|
237
|
+
Before healing, an optional **autowait** retries `findElement` for a while, since most "broken" selectors are slow screens. Turn healing off with `enableSelfHealing: false` in the config file (a true-or-false flag such as `--plugin-xenon-enable-self-healing` can only turn an option on), or with the AI self-healing switch on the dashboard's **Settings** page, which applies from the next command and wins over the option. A session can limit which tiers it uses with `xe:options.healingTiers`, numbered from `1` (Resilio) to `5` (LLM); `[]` turns healing off for that session. Native has no number: your own selector always runs first.
|
|
227
238
|
|
|
228
|
-
The dashboard's **Selector health** page lists every selector that needed healing in a period, how often and in which sessions, with a suggested fix to copy in JavaScript, Java, Python, C# or Ruby. Mark one as fixed and Xenon watches later runs to confirm it: it moves from **To fix** to **Being verified** to **Fixed**, and back to **To fix** if it breaks again. **Muted** hides a selector you've decided to leave.
|
|
239
|
+
The dashboard's **Selector health** page lists every selector that needed healing in a period, how often and in which sessions, with a suggested fix to copy in JavaScript, Java, Python, C# or Ruby. Mark one as fixed and Xenon watches later runs to confirm it: it moves from **To fix** to **Being verified** to **Fixed**, and back to **To fix** if it breaks again. **Muted** hides a selector you've decided to leave. [How healing works](https://xenon-6e6.pages.dev/docs/self-healing) and [Selector health](https://xenon-6e6.pages.dev/docs/selector-health) have the details.
|
|
229
240
|
|
|
230
241
|
## Security and access
|
|
231
242
|
|
|
@@ -246,19 +257,22 @@ Every `/xenon/api` request needs a credential:
|
|
|
246
257
|
| `devices` | Controlling devices, previews, recordings, reservations and leases |
|
|
247
258
|
| `admin` | Users, teams, API keys, webhooks and lab settings (with the matching role) |
|
|
248
259
|
|
|
249
|
-
**Teams** decide which devices someone can reach: a member sees their teams' devices and the shared pool, and everything else answers as if it didn't exist. [Teams](docs/teams
|
|
260
|
+
**Teams** decide which devices someone can reach: a member sees their teams' devices and the shared pool, and everything else answers as if it didn't exist. [Teams](https://xenon-6e6.pages.dev/docs/teams) explains setting them up.
|
|
250
261
|
|
|
251
262
|
For a lab others can reach, we recommend:
|
|
252
263
|
- set your own bootstrap admin password before the first start;
|
|
253
|
-
- turn on `XENON_REQUIRE_SESSION_TOKEN`, so every session has an owner, and `XENON_REQUIRE_COMMAND_AUTH` on the hub;
|
|
264
|
+
- turn on `XENON_REQUIRE_SESSION_TOKEN`, so every session has an owner, and `XENON_REQUIRE_COMMAND_AUTH` on the hub when every client sends its credentials with each command (the Kotlin SDK doesn't);
|
|
254
265
|
- serve Xenon over HTTPS, and keep nodes on a trusted network.
|
|
255
266
|
|
|
267
|
+
[Authentication](https://xenon-6e6.pages.dev/docs/authentication), [Roles and scopes](https://xenon-6e6.pages.dev/docs/roles-and-scopes) and the [Hardening checklist](https://xenon-6e6.pages.dev/docs/hardening) have the details.
|
|
268
|
+
|
|
256
269
|
## API
|
|
257
270
|
|
|
258
271
|
Every endpoint is documented in the OpenAPI reference that each server serves:
|
|
259
272
|
|
|
260
273
|
- **Interactive reference:** `http://<your-host>:4723/xenon/api-docs`
|
|
261
274
|
- **Raw OpenAPI document:** `http://<your-host>:4723/xenon/api-docs.json`
|
|
275
|
+
- **On the site:** [xenon-6e6.pages.dev/api](https://xenon-6e6.pages.dev/api), for the current release
|
|
262
276
|
|
|
263
277
|
```bash
|
|
264
278
|
# List the devices you can see
|
|
@@ -276,7 +290,7 @@ Errors are JSON with an `error` field. Requests are rate limited per API key or
|
|
|
276
290
|
|
|
277
291
|
## Observability
|
|
278
292
|
|
|
279
|
-
Xenon
|
|
293
|
+
Xenon can send traces, logs and metrics over OpenTelemetry, write its log as JSON, and serve metrics for Prometheus. [Observability](https://xenon-6e6.pages.dev/docs/observability) explains each, and [`examples/observability`](examples/observability) has a ready Docker Compose stack (Grafana, Tempo and Loki) to view them.
|
|
280
294
|
|
|
281
295
|
## Development
|
|
282
296
|
|
|
@@ -284,17 +298,18 @@ Xenon emits OpenTelemetry traces and logs for every session and command. [`examp
|
|
|
284
298
|
git clone https://github.com/Rabindra184/xenon.git
|
|
285
299
|
cd xenon
|
|
286
300
|
npm install
|
|
287
|
-
npm run
|
|
301
|
+
npm run build:all # build the plugin and the dashboard, once
|
|
302
|
+
npm run dev # migrate the database, build the plugin, install it and start Appium
|
|
288
303
|
```
|
|
289
304
|
|
|
290
305
|
| Command | Does |
|
|
291
306
|
|---|---|
|
|
292
307
|
| `npm run build:all` | Build the plugin and the dashboard |
|
|
293
|
-
| `npm run test:all` | Run the unit tests |
|
|
308
|
+
| `npm run test:all` | Run the unit and integration tests (no devices needed) |
|
|
294
309
|
| `npm run test:android`, `npm run test:ios` | Run the integration tests on real devices |
|
|
295
310
|
| `npm run db:generate -- --name <change>` | Add a database migration after editing `prisma/schema.prisma` |
|
|
296
311
|
|
|
297
|
-
The dashboard is a React app in [`web/`](web), and the documentation site is in [`website/`](website).
|
|
312
|
+
The dashboard is a React app in [`web/`](web), and the documentation site is in [`website/`](website); [its README](website/README.md) says how to build it.
|
|
298
313
|
|
|
299
314
|
## Upgrading
|
|
300
315
|
|
|
@@ -304,7 +319,7 @@ Read the [changelog](CHANGELOG.md) before upgrading: each release says whether i
|
|
|
304
319
|
appium plugin update xenon
|
|
305
320
|
```
|
|
306
321
|
|
|
307
|
-
|
|
322
|
+
Xenon brings its database up to date when it starts. A database that keeps Prisma's migration history (a `_prisma_migrations` table) matching its tables gets the new migrations with `prisma migrate deploy`. A history that has fallen behind its tables, or records a failed migration, is corrected first when the tables are exactly what its first migrations make, and only with what the tables show: a migration that changes rows is never recorded as applied, and one that failed on the rows stays failed until they are changed (when Xenon can't tell how much of it ran, the start stops and says how to finish it). When something was added to the tables by hand, the migrations are tried on a copy next to the file first, and run on the file only if the copy then has the whole schema. Any other database, including every one made with the default settings, is matched to the new schema with `prisma db push`, which stops rather than delete a table or column that holds data from a database with a history. If you set `XENON_AUTO_MIGRATE=false`, do this yourself first; from a source checkout, `npm run db:migrate` does it by the same rule. [Upgrading](https://xenon-6e6.pages.dev/docs/upgrading) has the steps, for hubs and nodes too.
|
|
308
323
|
|
|
309
324
|
## Getting help
|
|
310
325
|
|
package/lib/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xenon-device-management/xenon",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.16.0",
|
|
4
4
|
"description": "Xenon - Intelligent Mobile Infrastructure. A self-healing device orchestration platform for Appium.",
|
|
5
5
|
"main": "./lib/src/index.js",
|
|
6
6
|
"exports": {
|
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
"postinstall": "node scripts/generate-prisma.js",
|
|
13
13
|
"build": "node scripts/generate-prisma.js && tsc -b && npm run build:copy",
|
|
14
14
|
"build:all": "npm run build:xenon && npm run build",
|
|
15
|
-
"build:copy": "rm -rf lib/public && mkdir -p lib/src && cp -R src/public lib/ && cp -R src/generated lib/src/ && cp schema.json lib/ && mkdir -p lib/src/app/openapi && cp src/app/openapi/*.yaml lib/src/app/openapi/ && mkdir -p lib/src/device-managers/android/vendor && cp src/device-managers/android/vendor/*.jar lib/src/device-managers/android/vendor/ && mkdir -p lib/src/services/recording/vendor && cp src/services/recording/vendor/*.ttf src/services/recording/vendor/*.txt lib/src/services/recording/vendor/",
|
|
15
|
+
"build:copy": "rm -rf lib/public && mkdir -p lib/src && cp -R src/public lib/ && cp -R src/generated lib/src/ && cp schema.json lib/ && mkdir -p lib/src/app/openapi && cp src/app/openapi/*.yaml lib/src/app/openapi/ && mkdir -p lib/src/device-managers/android/vendor && cp src/device-managers/android/vendor/*.jar lib/src/device-managers/android/vendor/ && mkdir -p lib/src/services/recording/vendor && cp src/services/recording/vendor/*.ttf src/services/recording/vendor/*.txt lib/src/services/recording/vendor/ && mkdir -p lib/src/services/ocr/vendor && cp src/services/ocr/vendor/*.gz src/services/ocr/vendor/*.txt lib/src/services/ocr/vendor/",
|
|
16
|
+
"build:openapi": "node scripts/export-openapi.js",
|
|
16
17
|
"build:schema": "node scripts/generate-types-from-schema.js",
|
|
17
18
|
"build:xenon": "sh scripts/build-xenon.sh",
|
|
18
19
|
"dev": "npm run db:migrate && npm run build && npm run plugin:reinstall && npm run server",
|
|
@@ -20,7 +21,7 @@
|
|
|
20
21
|
"plugin:install": "appium plugin install --source=local $(pwd)",
|
|
21
22
|
"plugin:reinstall": "export APPIUM_HOME=/tmp/xenon-home && (appium plugin uninstall xenon || exit 0) && npm run plugin:install",
|
|
22
23
|
"test": "mocha",
|
|
23
|
-
"test:all": "NODE_ENV=test mocha --require test/setup/scratch-default-database.js \"test/unit/**/*.{spec,test}.{ts,js}\" --plugin-xenon-platform=both",
|
|
24
|
+
"test:all": "NODE_ENV=test mocha --require test/setup/scratch-default-database.js \"test/unit/**/*.{spec,test}.{ts,js}\" \"test/integration/*.spec.ts\" --ignore test/integration/androidDevices.spec.ts --plugin-xenon-platform=both",
|
|
24
25
|
"test:e2e": "mocha \"test/e2e/plugin.spec.*\" --timeout 300000",
|
|
25
26
|
"test:e2e:hub": "mocha \"test/e2e/hubnode/*.spec.ts\" --plugin-xenon-platform=both --timeout 300000",
|
|
26
27
|
"test:android": "mocha \"test/integration/androidDevices.spec.ts\"",
|
|
@@ -207,8 +208,8 @@
|
|
|
207
208
|
"schema": "schema.json"
|
|
208
209
|
},
|
|
209
210
|
"engines": {
|
|
210
|
-
"node": "^
|
|
211
|
-
"npm": ">=
|
|
211
|
+
"node": "^20.19.0 || ^22.12.0 || >=24.0.0",
|
|
212
|
+
"npm": ">=10"
|
|
212
213
|
},
|
|
213
214
|
"lint-staged": {
|
|
214
215
|
"src/*.{js,jsx,ts,tsx,json,css,scss,md}": [
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{r as w,j as _}from"./index-
|
|
1
|
+
import{r as w,j as _}from"./index-B0gQkEg0.js";const F=1600;function j(t,r){const e=F/Math.max(t,r);return{w:Math.round(t*e),h:Math.round(r*e)}}function B(t,r,e,s=document){if(r<=0||e<=0)return null;const{w:o,h:f}=j(r,e),l=s.createElement("canvas");l.width=o,l.height=f;const y=l.getContext("2d");if(!y)return null;y.scale(o/r,f/e),Y(y,r,e,t);try{return l.toDataURL("image/png")}catch{return null}}function U(t,r,e){const s={left:0,top:0,width:t,height:r};if(!e||!(e>0)||t<=0||r<=0)return s;const o=t/r;if(Math.abs(o-e)<1e-6)return s;if(e>o){const l=t/e;return{left:0,top:(r-l)/2,width:t,height:l}}const f=r*e;return{left:(t-f)/2,top:0,width:f,height:r}}const G=(t,r)=>t===r||!!t&&!!r&&t.left===r.left&&t.top===r.top&&t.width===r.width&&t.height===r.height,J=2,q=2e3;function D(t,r,e){if(t.length>=q)return t;const s=t[t.length-1];return s&&Math.hypot(r-s[0],e-s[1])<J?t:[...t,[r,e]]}function $(t,r){const e=(t||"#ff3333").trim();if(/^#[0-9a-fA-F]{6}$/.test(e)){const s=parseInt(e.slice(1,3),16),o=parseInt(e.slice(3,5),16),f=parseInt(e.slice(5,7),16);return`rgba(${s},${o},${f},${r})`}return e}function Y(t,r,e,s){var S;const o=s.geometry,f=s.color||"#ff3333",l=Math.max(4,Math.round(Math.min(r,e)*.008));if(t.lineCap="round",t.lineJoin="round",s.shape==="CIRCLE"){const h=(o.x??0)*r,M=(o.y??0)*e,p=Math.max(2,(o.w??0)*r),a=Math.max(2,(o.h??0)*e);t.beginPath(),t.ellipse(h,M,p,a,0,0,Math.PI*2),t.fillStyle=$(f,.22),t.fill(),t.lineWidth=l+2,t.strokeStyle="rgba(0,0,0,0.55)",t.stroke(),t.beginPath(),t.ellipse(h,M,p,a,0,0,Math.PI*2),t.lineWidth=l,t.strokeStyle=f,t.stroke();return}if(s.shape==="ARROW"){const h=(o.x??0)*r,M=(o.y??0)*e,p=h+(o.w??0)*r,a=M+(o.h??0)*e;t.lineWidth=l+2,t.strokeStyle="rgba(0,0,0,0.55)",t.beginPath(),t.moveTo(h,M),t.lineTo(p,a),t.stroke(),t.lineWidth=l,t.strokeStyle=f,t.beginPath(),t.moveTo(h,M),t.lineTo(p,a),t.stroke();const P=Math.atan2(a-M,p-h),k=Math.max(14,l*3);t.fillStyle=f,t.beginPath(),t.moveTo(p,a),t.lineTo(p-k*Math.cos(P-.4),a-k*Math.sin(P-.4)),t.lineTo(p-k*Math.cos(P+.4),a-k*Math.sin(P+.4)),t.closePath(),t.fill();return}if(s.shape==="TEXT"&&s.text){const h=Math.max(16,Math.round(Math.min(r,e)*.035));t.font=`bold ${h}px sans-serif`,t.lineWidth=3,t.strokeStyle="rgba(0,0,0,0.65)",t.strokeText(s.text,(o.x??0)*r,(o.y??0)*e),t.fillStyle=f,t.fillText(s.text,(o.x??0)*r,(o.y??0)*e);return}if(s.shape==="FREEHAND"&&(((S=o.points)==null?void 0:S.length)??0)>=2){const h=o.points,M=()=>{t.beginPath(),t.moveTo(h[0][0]*r,h[0][1]*e);for(let p=1;p<h.length;p++)t.lineTo(h[p][0]*r,h[p][1]*e)};t.lineWidth=l+2,t.strokeStyle="rgba(0,0,0,0.55)",M(),t.stroke(),t.lineWidth=l,t.strokeStyle=f,M(),t.stroke();return}const y=(o.x??0)*r,R=(o.y??0)*e,c=Math.max(2,(o.w??0)*r),g=Math.max(2,(o.h??0)*e);t.fillStyle=$(f,.22),t.fillRect(y,R,c,g),t.lineWidth=l+2,t.strokeStyle="rgba(0,0,0,0.55)",t.strokeRect(y,R,c,g),t.lineWidth=l,t.strokeStyle=f,t.strokeRect(y,R,c,g)}function O(t,r,e,s,o){if(s<=0||o<=0)return null;if(t==="CIRCLE"){const c=Math.hypot(e.curX-e.startX,e.curY-e.startY);return c<=4?null:{shape:t,color:r,geometry:{x:e.startX/s,y:e.startY/o,w:c/s,h:c/o}}}if(t==="ARROW"){const c=e.curX-e.startX,g=e.curY-e.startY;return Math.hypot(c,g)<=6?null:{shape:t,color:r,geometry:{x:e.startX/s,y:e.startY/o,w:c/s,h:g/o}}}if(t==="FREEHAND"){if(e.points.length<2)return null;const c=e.points.map(M=>M[0]),g=e.points.map(M=>M[1]),S=Math.min(...c),h=Math.min(...g);return{shape:t,color:r,geometry:{x:S/s,y:h/o,w:Math.max(.005,(Math.max(...c)-S)/s),h:Math.max(.005,(Math.max(...g)-h)/o),points:e.points.map(([M,p])=>[M/s,p/o])}}}const f=Math.min(e.startX,e.curX)/s,l=Math.min(e.startY,e.curY)/o,y=Math.abs(e.curX-e.startX)/s,R=Math.abs(e.curY-e.startY)/o;return y<=.005||R<=.005?null:{shape:t,color:r,geometry:{x:f,y:l,w:y,h:R}}}function K({enabled:t,shape:r,color:e,onCommit:s,committed:o,onCommittedChange:f,mediaAspect:l}){const y=w.useRef(null),[R,c]=w.useState(null),[g,S]=w.useState([]),[h,M]=w.useState(null),p=o??g,a=w.useRef(p),P=w.useRef(R),k=w.useRef(r),X=w.useRef(e),I=w.useRef(t);a.current=p,P.current=R,k.current=r,X.current=e,I.current=t;const b=w.useCallback(n=>{const i=n(a.current);a.current=i,f?f(i):S(i)},[f]),T=w.useCallback(()=>{const n=y.current;if(!n)return;const i=n.getContext("2d");if(!i)return;i.clearRect(0,0,n.width,n.height);for(const m of a.current)Y(i,n.width,n.height,m);const u=P.current;if(u){const m=O(k.current,X.current,u,n.width,n.height);m&&Y(i,n.width,n.height,m)}},[]);w.useEffect(()=>{const n=y.current;if(!n)return;const i=()=>{const m=Math.max(1,Math.round(n.clientWidth)),d=Math.max(1,Math.round(n.clientHeight));(n.width!==m||n.height!==d)&&(n.width=m,n.height=d),T()};i();const u=new ResizeObserver(i);return u.observe(n),()=>u.disconnect()},[T]),w.useEffect(()=>{T()},[R,p,T]),w.useEffect(()=>{var m;const n=(m=y.current)==null?void 0:m.parentElement;if(!n)return;const i=()=>{const d=l?U(n.clientWidth,n.clientHeight,l):null;M(E=>G(E,d)?E:d)};i();const u=new ResizeObserver(i);return u.observe(n),()=>u.disconnect()},[l]),w.useEffect(()=>{var i;if(t)return;c(null);const n=y.current;if(n&&n.hasPointerCapture)try{for(let u=0;u<8;u++)(i=n.hasPointerCapture)!=null&&i.call(n,u)&&n.releasePointerCapture(u)}catch{}},[t]);const C=n=>{const u=y.current.getBoundingClientRect();return{px:n.clientX-u.left,py:n.clientY-u.top,w:u.width,h:u.height}},z=n=>{const i=P.current;if(!i||!I.current){c(null);return}const u=y.current;if(!u||u.width<=0){c(null);return}const m=C(n),d=O(k.current,X.current,{...i,curX:m.px,curY:m.py,points:D(i.points,m.px,m.py)},m.w,m.h);d&&(b(E=>[...E,d]),s(d,B(d,m.w,m.h))),c(null)},N=n=>{if(!t||n.button!==0)return;n.preventDefault(),n.currentTarget.setPointerCapture(n.pointerId);const i=C(n);c({startX:i.px,startY:i.py,curX:i.px,curY:i.py,points:[[i.px,i.py]]})},L=n=>{if(!P.current)return;const i=C(n);c(u=>u&&{...u,curX:i.px,curY:i.py,points:D(u.points,i.px,i.py)})},A=n=>{try{n.currentTarget.releasePointerCapture(n.pointerId)}catch{}z(n)};return _.jsx("canvas",{ref:y,style:{position:"absolute",...h?{left:`${h.left}px`,top:`${h.top}px`,width:`${h.width}px`,height:`${h.height}px`}:{inset:0,width:"100%",height:"100%"},pointerEvents:t?"auto":"none",cursor:t?"crosshair":"default",zIndex:t?40:25},onPointerDown:N,onPointerMove:L,onPointerUp:A,onPointerCancel:A})}export{K as A,U as f,Y as p};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{r as n,j as e,o as h}from"./index-
|
|
1
|
+
import{r as n,j as e,o as h}from"./index-B0gQkEg0.js";function y({children:c}){const[i,s]=n.useState(null),[o,l]=n.useState(""),[a,r]=n.useState(null);if(n.useEffect(()=>{fetch("/xenon/api/health").then(()=>fetch("/xenon/api/sessions",{credentials:"include"})).then(t=>s(t.status!==401)).catch(()=>s(!1))},[]),i===null)return e.jsx("div",{style:{padding:40},children:"Loading…"});if(i)return e.jsx(e.Fragment,{children:c});async function u(t){t.preventDefault(),r(null);const d=await fetch("/xenon/api/auth/dashboard-session",{method:"POST",headers:{"Content-Type":"application/json",...h},credentials:"include",body:JSON.stringify({apiKey:o.trim()})});if(d.ok)s(!0);else{const p=await d.json().catch(()=>({}));r(p.error||"Invalid key")}}return e.jsxs("div",{style:{maxWidth:420,margin:"120px auto",padding:24},children:[e.jsx("h2",{children:"Sign in to Xenon"}),e.jsxs("p",{children:["Paste an API key. First-time setup: the bootstrap key is in"," ",e.jsx("code",{children:"~/.cache/xenon/bootstrap-key.txt"})," on the server."]}),e.jsxs("form",{onSubmit:u,children:[e.jsx("input",{type:"password",value:o,onChange:t=>l(t.target.value),placeholder:"API key",style:{width:"100%",padding:8,boxSizing:"border-box"},autoFocus:!0}),e.jsx("button",{type:"submit",style:{marginTop:12,padding:"8px 16px"},children:"Continue"})]}),a&&e.jsx("p",{style:{color:"crimson",marginTop:12},children:a})]})}export{y as ApiKeyGate};
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{r as c,j as t,z as He,w as ze,F as pe,X as D,v as We,e as Ue,$ as Fe,T as Ke,a0 as Ge}from"./index-B0gQkEg0.js";import{C as Ne,p as Ve,c as Ye,W as Xe,M as Je,A as re,H as qe,b as Ze,S as je,a as Qe,I as et,e as tt,u as rt,f as nt,h as Ee,d as st,g as ot}from"./h264Stream-DeD__6wa.js";import{p as at}from"./labels-C21SA4JG.js";import{C as it}from"./circle-check-CBv88cH7.js";import{f as ct,A as lt}from"./AnnotationOverlay-2W4JvEyZ.js";import{V as dt}from"./video-off-CqZVuZGs.js";import{P as ut}from"./plus-C9Zj84UF.js";import{a as ft}from"./SegmentedControl-C9o90KId.js";import{v as pt,a as ht,c as mt,s as gt,R as xt,b as bt,g as vt,d as he,e as yt}from"./recordings-BWe1ydXm.js";import{D as ae}from"./download-BroHxNOh.js";import{B as me}from"./button-DDau4Axv.js";import{C as wt}from"./clock-BPrld-3h.js";import{a as Nt}from"./page-header-D8KUX6Qg.js";import"./utils-CDN07tui.js";function jt(e){switch(e){case"automation":return"In automation";case"manual_other":return"Manual control by another user";case"manual_self":return"On your grid";case"recording_other_group":return"Recording in another group";default:return e?"Busy":null}}const ge=[{id:"ios",label:"iOS",match:e=>e==="ios"},{id:"android",label:"Android",match:e=>e==="android"},{id:"tvos",label:"tvOS",match:e=>e==="tvos"},{id:"androidtv",label:"Android TV",match:e=>e==="androidtv"||e==="android-tv"},{id:"other",label:"Other",match:()=>!0}],X={available:"bg-[var(--color-success)]",inUse:"bg-[var(--color-warning)]",offline:"bg-[rgb(var(--rgb-offline))]"},Et=[["available",X.available],["in use",X.inUse],["offline",X.offline]];function xe(e){return at(e)}function Rt({devices:e,inMosaic:n,onToggle:r,status:a="ready"}){const[g,j]=c.useState(""),[x,w]=c.useState({}),m=c.useMemo(()=>{const l=g.trim().toLowerCase();return l?e.filter(b=>b.udid.toLowerCase().includes(l)||(b.name??"").toLowerCase().includes(l)||(b.altName??"").toLowerCase().includes(l)||(b.platform??"").toLowerCase().includes(l)):e},[e,g]),y=c.useMemo(()=>{const l=ge.map(d=>({id:d.id,label:d.label,rows:[]})),b=d=>l.find((S,v)=>ge[v].match(d))??l[l.length-1];for(const d of m)b((d.platform??"").toLowerCase()).rows.push(d);return l.filter(d=>d.rows.length>0)},[m]);if(e.length===0){const l=a==="loading"?"Loading devices…":a==="error"?"Couldn’t load devices. Retrying…":"No devices online.";return t.jsx("div",{role:"status",className:"text-xs text-[var(--text-dim)] p-3",children:l})}return t.jsxs("div",{className:"flex flex-col gap-2",children:[t.jsxs("div",{className:"relative",children:[t.jsx(He,{"aria-hidden":!0,size:12,className:"absolute left-2 top-1/2 -translate-y-1/2 text-[var(--text-dim)]"}),t.jsx("input",{type:"text",value:g,placeholder:"Filter devices...",onChange:l=>j(l.target.value),className:"w-full pl-7 pr-2 py-1.5 text-xs rounded border border-[var(--border)] bg-transparent placeholder:text-[var(--text-dim)] focus:outline-none focus:border-[var(--color-info)]"})]}),y.map(l=>{const b=x[l.id];return t.jsxs("section",{className:"flex flex-col gap-1",children:[t.jsxs("button",{type:"button","aria-expanded":!b,onClick:()=>w(d=>({...d,[l.id]:!d[l.id]})),className:"flex items-center justify-between w-full text-[11px] text-[var(--text-dim)] hover:text-[rgb(var(--rgb-fg))] px-1 py-0.5",children:[t.jsxs("span",{className:"flex items-center gap-1.5",children:[t.jsx(ze,{"aria-hidden":!0,size:12,className:`transition-transform ${b?"-rotate-90":"rotate-0"}`}),l.label]}),t.jsx("span",{children:l.rows.length})]}),!b&&t.jsx("ul",{className:"flex flex-col gap-1 text-sm",children:l.rows.map(d=>{const S=d.busyReason==="manual_self",v=n.has(d.udid),h=!!d.busy&&!S&&!v||!!d.offline,R=d.offline?"Offline":jt(d.busyReason),W=!h,A=[d.name??d.udid,d.platform&&xe(d.platform)].concat(h&&R?[R]:[]).filter(Boolean).join(", ");return t.jsx("li",{children:t.jsxs("button",{type:"button",disabled:h,draggable:!h,onDragStart:I=>{I.dataTransfer.effectAllowed="copy",I.dataTransfer.setData("application/x-xenon-udid",d.udid),I.dataTransfer.setData("text/plain",d.udid)},onClick:()=>r(d.udid),"aria-label":A,"aria-pressed":v,className:`w-full flex items-center gap-2 px-2 py-1.5 rounded text-left border transition-colors ${h?"opacity-60 cursor-not-allowed border-transparent":v?"border-[rgb(var(--rgb-accent)/0.4)] bg-[rgb(var(--rgb-accent)/0.05)] hover:bg-[rgb(var(--rgb-accent)/0.1)]":"border-transparent hover:border-[var(--border)] hover:bg-[var(--surface-2)]"}`,title:h?`${R} — release first`:v?"Click to remove from the grid":"Click to add to the grid (or drag to a cell)",children:[v?t.jsx(it,{"aria-hidden":!0,size:14,className:"shrink-0 text-[var(--color-accent-soft)]"}):t.jsx(Ne,{"aria-hidden":!0,size:14,className:"shrink-0 text-[var(--text-dim)]"}),t.jsx("span",{"aria-hidden":!0,className:`inline-block w-1.5 h-1.5 rounded-full ${d.offline?X.offline:W?X.available:X.inUse}`}),t.jsx("span",{className:"font-medium flex-1 truncate text-[13px]",children:d.name??d.udid}),d.platform&&t.jsx("span",{className:"text-[11px] px-1.5 py-0.5 rounded bg-[var(--surface-2)] text-[var(--text-dim)]",children:xe(d.platform)})]})},d.udid)})})]},l.id)}),g&&y.length===0&&t.jsxs("div",{className:"text-xs text-[var(--text-dim)] px-1 py-2",children:['No devices match "',g,'".']}),t.jsx("div",{className:"text-[10px] text-[var(--text-dim)] px-1 pt-2 flex items-center gap-3",children:Et.map(([l,b])=>t.jsxs("span",{className:"inline-flex items-center gap-1",children:[t.jsx("span",{className:`w-1.5 h-1.5 rounded-full inline-block ${b}`}),l]},l))})]})}const St=[{id:"1",label:"1",icon:t.jsx(Ct,{})},{id:"2x1",label:"2×1",icon:t.jsx(Dt,{})},{id:"2x2",label:"2×2",icon:t.jsx(kt,{})},{id:"3x2",label:"3×2",icon:t.jsx(At,{})},{id:"auto",label:"Auto",icon:t.jsx(It,{})}];function Tt({value:e,onChange:n}){return t.jsx("div",{role:"tablist",className:"inline-flex border border-[var(--border)] rounded-md overflow-hidden text-xs bg-[rgb(var(--rgb-white)/0.02)]",children:St.map(r=>{const a=e===r.id;return t.jsxs("button",{role:"tab","aria-selected":a,onClick:()=>n(r.id),className:`flex items-center gap-1.5 px-2.5 py-1.5 border-r border-[var(--border)] last:border-r-0 transition-colors ${a?"bg-[var(--surface-2)] text-[rgb(var(--rgb-fg))]":"text-[var(--text-dim)] hover:bg-[var(--surface-2)] hover:text-[rgb(var(--rgb-fg))]"}`,title:`Layout: ${r.label}`,children:[t.jsx("span",{className:"w-3.5 h-3.5 inline-flex items-center justify-center",children:r.icon}),t.jsx("span",{children:r.label})]},r.id)})})}function L(e){return t.jsx("rect",{rx:1.2,ry:1.2,fill:"currentColor",...e})}function Ct(){return t.jsx("svg",{viewBox:"0 0 14 14",className:"w-3.5 h-3.5",children:L({x:2,y:2,width:10,height:10})})}function Dt(){return t.jsxs("svg",{viewBox:"0 0 14 14",className:"w-3.5 h-3.5",children:[L({x:1.5,y:3,width:5,height:8}),L({x:7.5,y:3,width:5,height:8})]})}function kt(){return t.jsxs("svg",{viewBox:"0 0 14 14",className:"w-3.5 h-3.5",children:[L({x:1.5,y:1.5,width:5,height:5}),L({x:7.5,y:1.5,width:5,height:5}),L({x:1.5,y:7.5,width:5,height:5}),L({x:7.5,y:7.5,width:5,height:5})]})}function At(){return t.jsxs("svg",{viewBox:"0 0 14 14",className:"w-3.5 h-3.5",children:[L({x:1,y:1.5,width:3.3,height:5}),L({x:5.35,y:1.5,width:3.3,height:5}),L({x:9.7,y:1.5,width:3.3,height:5}),L({x:1,y:7.5,width:3.3,height:5}),L({x:5.35,y:7.5,width:3.3,height:5}),L({x:9.7,y:7.5,width:3.3,height:5})]})}function It(){return t.jsxs("svg",{viewBox:"0 0 14 14",className:"w-3.5 h-3.5",fill:"none",stroke:"currentColor",strokeWidth:"1.4",children:[t.jsx("rect",{x:"2",y:"2",width:"10",height:"10",rx:"1.5"}),t.jsx("circle",{cx:"7",cy:"7",r:"1.5",fill:"currentColor",stroke:"none"})]})}function _t(e,n,r,a,g,j,x=!1){const w=ct(r.width,r.height,j);let m=(e-r.left-w.left)/w.width,y=(n-r.top-w.top)/w.height;return!(m>=0&&m<=1&&y>=0&&y<=1)&&!x?null:(m=Math.min(1,Math.max(0,m)),y=Math.min(1,Math.max(0,y)),{x:Math.round(m*a),y:Math.round(y*g)})}const Ot=5e3;function $t(e,n){if(e.httpStatus===404)return{ended:!0,startedAt:n};if(e.httpStatus!==200)return{ended:!1,startedAt:n};const r=e.body??{};return r.status!=="running"?{ended:!0,startedAt:n}:n&&r.startedAt&&r.startedAt!==n?{ended:!0,startedAt:n}:{ended:!1,startedAt:n??r.startedAt}}function Mt(e,n,r,a=Ot){const g=c.useRef(r);g.current=r,c.useEffect(()=>{if(!n)return;let j=!1,x=!1,w;const m=async()=>{if(!(x||j)){x=!0;try{const l=await fetch(`/xenon/api/control/${encodeURIComponent(e)}/stream/status`),b=l.ok?await l.json().catch(()=>{}):void 0;if(j)return;const d=$t({httpStatus:l.status,body:b},w);w=d.startedAt,d.ended&&(j=!0,g.current())}catch{}finally{x=!1}}};m();const y=setInterval(m,a);return()=>{j=!0,clearInterval(y)}},[e,n,a])}const Pt=3,Lt=45e3,be=[2e3,5e3,1e4];function Bt(e){const n=Math.max(0,Math.min(e,be.length-1));return be[n]}function Ht(e){return e<Pt}function ve(e){var r;const n=(e.lastError||"").toLowerCase();if(/did not find test app|not installed|webdriveragentrunner|applicationverification/.test(n))return"WebDriverAgent is not installed on this device. Install and trust WDA, then retry.";if(/unsupported ios version|manualpairingtunnelstart/.test(n))return"This device’s iOS version is not supported for live streaming on this host.";if(/port range for purpose 'tunnel' is exhausted/.test(n))return"No free tunnel ports on this host (12100–12199 are in use). Stop another iPhone’s preview, then retry.";if(/go-ios tunnel .* exited before it was ready/.test(n)){const a=(r=/exited before it was ready \([^)]*\): (.+)$/.exec(e.lastError||""))==null?void 0:r[1];return`The device tunnel stopped while starting${a?` (go-ios: ${a.trim()})`:""}. Retry; if it keeps failing, unlock the iPhone and check it trusts this Mac.`}return/go-ios tunnel .* came up on port/.test(n)?"The device tunnel started on an unexpected port and was stopped. Retry.":/econnrefused|econnreset|connection reset|tunnel/.test(n)?"Lost the connection to the device tunnel. Reconnect the device, then retry.":e.lastError&&e.lastError.trim()?e.lastError.trim():"We couldn’t start the live stream for this device."}const zt=10,Wt=500;function Ut({udid:e,name:n,recordingId:r,annotateMode:a,shape:g,color:j,aspect:x="9 / 16",screenWidth:w,screenHeight:m,platform:y,onAnnotation:l,overlayAnnotations:b,onOverlayAnnotationsChange:d,onRemove:S}){const[v,h]=c.useState("connecting"),[R,W]=c.useState(),A=c.useCallback((o,u)=>{if(!(o>0)||!(u>0))return;const k=o/u;W(_=>_!==void 0&&Math.abs(_-k)<.001?_:k)},[]),[I,O]=c.useState(0),$=y==="android"||y==="androidtv"||y==="android-tv",K=y==="ios"||y==="tvos",[p,s]=c.useState(null),[i,f]=c.useState(()=>!($&&pe())),N=c.useRef(0),B=c.useRef(!1),M=c.useRef(0),E=c.useRef(null),[H,U]=c.useState(""),[z,T]=c.useState([]),[G,le]=c.useState(!1),Ce=c.useRef(null),J=c.useRef(null),V=c.useCallback(async o=>{if(B.current)return;B.current=!0;const u=N.current;if(f(!1),s(null),h("connecting"),!(o!=null&&o.streamEnded))try{await D.startStream(e,{player:"mjpeg"})}catch(k){console.warn(`[DeviceTile] Could not ask for MJPEG for ${e}`,k)}N.current===u&&(M.current=0,E.current&&clearTimeout(E.current),O(Date.now()),h("connecting"),f(!0))},[e]);c.useEffect(()=>{let o=!1;B.current=!1;const u=()=>{o=!0,N.current+=1};if(r)return s(null),f(!0),h("connecting"),O(Date.now()),D.startStream(e).catch(()=>{}),u;const k=pe(),_=()=>{o||f(!0)};return(async()=>{let C=null;try{const P=await fetch(`/xenon/api/control/${encodeURIComponent(e)}/stream/status`);P.ok&&(C=await P.json())}catch{}if(o)return;if(Ve(y||"",C==null?void 0:C.type,k)!=="h264"){C||!$||!k?_():V();return}const F=C!=null&&C.h264Path?await Ye(e,C.h264Path).catch(()=>null):null;o||(F?s(F):V())})(),u},[e,y,r,$,V]);const Z=!a&&!!w&&!!m,se=(o,u,k,_=!1)=>_t(o,u,k,w,m,R,_),De=(o,u,k)=>{const _=Date.now()+Math.random(),C=o-k.left,F=u-k.top;T(P=>[...P,{id:_,x:C,y:F}]),window.setTimeout(()=>{T(P=>P.filter(ee=>ee.id!==_))},500)},ke=o=>{if(!Z||v==="unavailable"||o.button!==0)return;const u=o.currentTarget;u.focus(),se(o.clientX,o.clientY,u.getBoundingClientRect())&&(u.setPointerCapture(o.pointerId),J.current={startX:o.clientX,startY:o.clientY,startTime:Date.now(),rect:u.getBoundingClientRect()},De(o.clientX,o.clientY,J.current.rect))},Ae=async o=>{var ue,fe;const u=J.current;if(J.current=null,!u||!Z)return;try{o.currentTarget.releasePointerCapture(o.pointerId)}catch{}const k=o.clientX-u.startX,_=o.clientY-u.startY,C=Math.hypot(k,_),F=Date.now()-u.startTime,P=se(u.startX,u.startY,u.rect,!0),ee=C<zt;try{if(ee&&F<Wt)await D.tap(e,P.x,P.y);else if(ee)await((fe=(ue=D).touchAndHold)==null?void 0:fe.call(ue,e,P.x,P.y,F));else{const te=se(o.clientX,o.clientY,u.rect,!0);await D.swipe(e,P.x,P.y,te.x,te.y)}}catch(te){console.error(`[DeviceTile] gesture failed for ${e}`,te)}},Ie=()=>{J.current=null},_e=async o=>{if(!Z||v==="unavailable"||o.metaKey||o.ctrlKey||o.altKey)return;const u=o.key;try{if(u.length===1){o.preventDefault(),await D.typeText(e,u);return}if(u==="Backspace"){o.preventDefault(),$?await D.pressKey(e,67):await D.typeText(e,"\b");return}if(u==="Enter"){o.preventDefault(),$?await D.pressKey(e,66):await D.typeText(e,`
|
|
2
|
+
`);return}if(u==="Tab"){o.preventDefault(),await D.typeText(e," ");return}u==="Escape"&&$&&(o.preventDefault(),await D.pressKey(e,re.BACK))}catch(k){console.error(`[DeviceTile] keystroke "${u}" failed for ${e}`,k)}},Oe=()=>D.pressKey(e,$?re.HOME:et.HOME).catch(o=>console.error(`[DeviceTile] home failed for ${e}`,o)),$e=()=>D.pressKey(e,re.BACK).catch(o=>console.error(`[DeviceTile] back failed for ${e}`,o)),Me=()=>D.pressKey(e,re.APP_SWITCH).catch(o=>console.error(`[DeviceTile] app-switch failed for ${e}`,o)),Pe=async()=>{try{const o=await D.getScreenshot(e),u=(o==null?void 0:o.screenshot)??(o==null?void 0:o.value)??o;if(!u)return;const k=await fetch(`data:image/png;base64,${u}`).then(F=>F.blob()),_=URL.createObjectURL(k),C=document.createElement("a");C.href=_,C.download=`${n||e}-${Date.now()}.png`,document.body.appendChild(C),C.click(),C.remove(),setTimeout(()=>URL.revokeObjectURL(_),1e3)}catch(o){console.error(`[DeviceTile] screenshot failed for ${e}`,o)}};c.useEffect(()=>{console.log(`[DeviceTile] Rendering ${e} in state: ${v}`)},[e,v]);const Le=`/xenon/api/control/${encodeURIComponent(e)}/stream?t=${I}`,de=c.useCallback(async()=>{try{const o=await fetch(`/xenon/api/control/${encodeURIComponent(e)}/stream/status`);if(!o.ok)throw new Error(String(o.status));const u=await o.json();U(ve({lastError:u==null?void 0:u.lastError,status:u==null?void 0:u.status}))}catch{U(ve({}))}},[e]),oe=c.useCallback(()=>{const o=M.current;Ht(o)?(M.current=o+1,E.current&&clearTimeout(E.current),E.current=setTimeout(()=>{h("connecting"),O(Date.now())},Bt(o))):(h("unavailable"),de())},[de]);c.useEffect(()=>{if(v!=="connecting")return;const o=setTimeout(p?()=>V():oe,Lt);return()=>clearTimeout(o)},[v,I,oe,p,V]),Mt(e,v==="live"&&!p,()=>{console.warn(`[DeviceTile] MJPEG stream ended for ${e}; reconnecting`),h("connecting"),O(Date.now())}),c.useEffect(()=>()=>{E.current&&clearTimeout(E.current)},[]);const Be=()=>{E.current&&clearTimeout(E.current),M.current=0,U(""),h("connecting"),O(Date.now()),f(!0)},q=!!r,Q=n||e.slice(0,16)+"…";return t.jsxs("div",{className:"flex items-stretch justify-center w-full h-full min-h-0 min-w-0 gap-2",children:[t.jsxs("div",{className:"theme-dark relative bg-[var(--black)] rounded-lg overflow-hidden border border-[var(--border)] group max-h-full max-w-full self-center",style:{aspectRatio:x,height:"100%"},children:[v==="connecting"&&t.jsxs("div",{role:"status","aria-live":"polite",className:"flex flex-col items-center justify-center gap-1 px-4 text-center bg-[rgb(var(--rgb-well)/0.8)] text-[var(--text-dim)] absolute inset-0 z-20 pointer-events-none",children:[t.jsx("div",{"aria-hidden":!0,className:"w-10 h-10 mb-3 border-4 border-[var(--border)] border-t-[var(--text-dim)] rounded-full animate-spin"}),t.jsx("div",{className:"font-semibold text-[var(--text)]",children:"Starting stream…"}),t.jsx("div",{className:"text-xs text-[var(--text-dim)] truncate max-w-full",children:Q}),t.jsx("div",{className:"text-[11px] mt-3 text-[var(--text-dim)] leading-snug",children:K?"iPhone streams can take up to 20 seconds to start.":"Connecting to the device…"})]}),v==="unavailable"&&t.jsx("div",{role:"alert",className:"flex items-center justify-center bg-[var(--black)] text-[var(--text-dim)] absolute inset-0 z-20",children:t.jsxs("div",{className:"text-center p-6",children:[t.jsx(dt,{"aria-hidden":!0,size:36,className:"mx-auto mb-3 text-[var(--text-dim)]"}),t.jsx("div",{className:"font-semibold text-[var(--text)]",children:"Connection failed"}),t.jsx("p",{className:"text-xs mt-2 text-[var(--text-dim)] leading-relaxed max-w-[240px] mx-auto",children:H||"We couldn’t start the live stream for this device."}),t.jsx("button",{type:"button",className:"mt-5 text-sm font-semibold px-4 py-2 rounded bg-[var(--color-accent)] hover:bg-[var(--color-accent-strong)] text-[var(--color-on-accent)] transition-colors",onClick:Be,children:"Retry"})]})}),p?t.jsx(Xe,{wsUrl:p,className:"absolute inset-0 w-full h-full object-contain bg-black select-none pointer-events-none",onReady:()=>h("live"),onFrameSize:A,onFatal:o=>{console.warn(`[DeviceTile] H.264 fatal for ${e}; falling back to MJPEG`),V(o)}}):i?t.jsx(Je,{src:Le,alt:Q,className:"absolute inset-0 w-full h-full object-contain bg-black select-none pointer-events-none",draggable:!1,style:{opacity:v==="live"?1:0,transition:"opacity 0.3s ease-in-out"},onLoad:o=>{console.log(`[DeviceTile] Image LOADED for ${e}`),A(o.currentTarget.naturalWidth,o.currentTarget.naturalHeight),M.current=0,E.current&&clearTimeout(E.current),h("live")},onError:o=>{console.error(`[DeviceTile] Image ERROR for ${e}`,o),oe()}},I):null,Z&&v!=="unavailable"&&t.jsx("div",{ref:Ce,className:`absolute inset-0 z-[35] touch-none outline-none ${G?"ring-2 ring-[rgb(var(--rgb-accent)/0.6)] ring-inset rounded-lg":""}`,tabIndex:0,onPointerDown:ke,onPointerUp:Ae,onPointerCancel:Ie,onKeyDown:_e,onFocus:()=>le(!0),onBlur:()=>le(!1)}),t.jsx("div",{className:"absolute inset-0 z-20 pointer-events-none overflow-hidden",children:z.map(o=>t.jsx("span",{className:"absolute block w-10 h-10 rounded-full border-2 border-[var(--color-accent-soft)] animate-ping",style:{left:o.x-20,top:o.y-20}},o.id))}),q&&t.jsx(lt,{enabled:a,shape:g,color:j,committed:b,mediaAspect:R,onCommittedChange:d,onCommit:(o,u)=>l(r,o,u)}),q&&a&&v==="live"&&t.jsx("div",{className:"absolute top-3 left-14 right-3 z-30 flex justify-end pointer-events-none",children:t.jsx("span",{className:"px-2 py-0.5 rounded-sm bg-[var(--color-info)] text-white text-[10px] font-semibold shadow-lg whitespace-nowrap overflow-hidden text-ellipsis",children:"Annotating · Esc to stop"})}),q&&t.jsx("div",{className:"absolute top-3 left-3 z-30 pointer-events-none",children:t.jsx("span",{className:"px-2 py-0.5 rounded-sm bg-[var(--red-600)] text-white text-[9px] font-black shadow-lg",children:"REC"})}),t.jsxs("div",{className:"absolute top-3 left-3 z-30 flex flex-col gap-1.5 pointer-events-none opacity-0 group-hover:opacity-100 transition-opacity duration-150",children:[v==="live"&&t.jsx("span",{className:"px-2 py-0.5 rounded-sm bg-[var(--color-success)] text-[var(--color-on-success)] text-[11px] font-semibold shadow-lg w-fit",children:"Live"}),t.jsx("span",{className:"px-2 py-1 rounded bg-[rgb(var(--rgb-well)/0.8)] text-[var(--text)] text-[11px] font-medium backdrop-blur-md border border-[rgb(var(--rgb-fg)/0.1)] shadow-xl truncate max-w-[180px]",children:Q})]})]}),t.jsx(Ft,{streamState:v,recording:q,isIOS:K,isAndroid:$,onClose:!q&&S?()=>S(e):void 0,sendHome:Oe,sendBack:$e,sendAppSwitch:Me,captureScreenshot:Pe,displayName:Q})]})}const ne="w-7 h-7 flex items-center justify-center rounded-md hover:bg-[var(--surface-2)] hover:text-[var(--text)] transition-colors";function Ft({streamState:e,recording:n,isIOS:r,isAndroid:a,onClose:g,sendHome:j,sendBack:x,sendAppSwitch:w,captureScreenshot:m,displayName:y}){const l=e==="live";return!g&&!l?null:t.jsxs("div",{className:"self-stretch w-9 flex flex-col items-center justify-between gap-1 py-2 rounded-lg bg-[var(--surface)] border border-[var(--border)] text-[var(--text-dim)]",children:[t.jsx("div",{className:"flex flex-col items-center gap-1",children:g&&t.jsx("button",{type:"button","aria-label":`Remove ${y}`,title:"Remove from the grid",onClick:g,className:"w-7 h-7 flex items-center justify-center rounded-md hover:bg-[rgb(var(--rgb-red)/0.15)] hover:text-[var(--color-danger)] transition-colors",children:t.jsx(We,{"aria-hidden":!0,size:16})})}),l&&t.jsxs("div",{className:"flex flex-col items-center gap-1.5 my-auto",children:[(a||r)&&t.jsx("button",{type:"button","aria-label":"Home",title:"Home",onClick:j,className:ne,children:t.jsx(qe,{"aria-hidden":!0,size:15})}),a&&t.jsx("button",{type:"button","aria-label":"Back",title:"Back",onClick:x,className:ne,children:t.jsx(Ze,{"aria-hidden":!0,size:16})}),a&&t.jsx("button",{type:"button","aria-label":"Recent apps",title:"Recent apps",onClick:w,className:ne,children:t.jsx(je,{"aria-hidden":!0,size:13})})]}),l&&t.jsx("div",{className:"flex flex-col items-center gap-1",children:t.jsx("button",{type:"button","aria-label":"Screenshot",title:"Screenshot",onClick:m,className:ne,children:t.jsx(Qe,{"aria-hidden":!0,size:15})})})]})}const Kt={1:"1fr","2x1":"1fr 1fr","2x2":"1fr 1fr","3x2":"1fr 1fr 1fr"},Gt={1:"1fr","2x1":"1fr","2x2":"1fr 1fr","3x2":"1fr 1fr"},Vt={1:1,"2x1":2,"2x2":4,"3x2":6};function Yt({layout:e,tiles:n,annotateMode:r,shape:a,color:g,onAnnotation:j,overlayAnnotationsByRecording:x,onOverlayAnnotationsChange:w,onRemove:m,onDropDevice:y}){const l=tt(e,n.length),b=Vt[l];n.length;const d=n.length,S=e==="auto"?Math.max(d,1):b,v=Math.max(0,S-d);return t.jsxs("div",{style:{display:"grid",gap:8,gridTemplateColumns:Kt[l],gridTemplateRows:Gt[l]},className:"h-full",children:[n.map(h=>t.jsx(Ut,{udid:h.udid,name:h.name,mjpegPort:h.mjpegPort,recordingId:h.recordingId,aspect:h.aspect,screenWidth:h.screenWidth,screenHeight:h.screenHeight,platform:h.platform,annotateMode:r,shape:a,color:g,overlayAnnotations:h.recordingId?x==null?void 0:x[h.recordingId]:void 0,onOverlayAnnotationsChange:h.recordingId&&w?R=>w(h.recordingId,R):void 0,onAnnotation:j,onRemove:m},h.udid)),Array.from({length:v},(h,R)=>t.jsx(Xt,{onDropDevice:y},`empty-${R}`))]})}function Xt({onDropDevice:e}){const[n,r]=c.useState(!1);return t.jsxs("div",{onDragOver:a=>{e&&(a.preventDefault(),a.dataTransfer.dropEffect="copy",n||r(!0))},onDragLeave:()=>r(!1),onDrop:a=>{if(r(!1),!e)return;a.preventDefault();const g=a.dataTransfer.getData("application/x-xenon-udid")||a.dataTransfer.getData("text/plain");g&&e(g)},className:`flex flex-col items-center justify-center gap-2 rounded-lg border border-dashed transition-colors ${n?"border-[rgb(var(--rgb-accent)/0.6)] bg-[rgb(var(--rgb-accent)/0.05)]":"border-[var(--border)] bg-[rgb(var(--rgb-fg)/0.02)]"} text-[var(--text-dim)] min-h-[120px]`,children:[t.jsx("div",{"aria-hidden":"true",className:`w-8 h-8 rounded-full flex items-center justify-center ${n?"bg-[rgb(var(--rgb-accent)/0.15)] text-[var(--color-accent-soft)]":"bg-[rgb(var(--rgb-fg)/0.05)] text-[rgb(var(--rgb-fg)/0.6)]"}`,children:t.jsx(ut,{size:16})}),t.jsx("div",{className:"text-xs font-medium",children:"Drag a device here"}),t.jsx("div",{className:"text-[10px] opacity-70",children:"or click one in the list"})]})}function Jt(e,n){return n==="starting"?"Starting…":e<=1?"Record":`Record ${e} devices`}const Y={"aria-hidden":!0,size:14,className:"shrink-0"};function qt({selectedUdids:e,onClearMarks:n}){const{state:r,dispatch:a}=rt(),[g,j]=c.useState(()=>Date.now());c.useEffect(()=>{if(r.recordingPhase!=="recording"&&r.recordingPhase!=="stopping")return;j(Date.now());const p=window.setInterval(()=>j(Date.now()),250);return()=>window.clearInterval(p)},[r.recordingPhase]);const x=r.startedAt&&(r.recordingPhase==="recording"||r.recordingPhase==="stopping")?g-r.startedAt:0,w=r.recordingPhase==="starting"||r.recordingPhase==="stopping",m=r.recordingPhase==="idle"&&e.length>0&&!w,y=r.recordingPhase==="recording"||r.recording&&r.tiles.some(p=>!!p.recordingId),l=y&&!w,b=l,d=b&&r.annotateMode,S=!!r.groupId&&r.recordingPhase==="idle",v=e.length>1||r.downloadableVideoCount>1,h=S&&r.compositeEnabled,R=S&&!r.compositeEnabled&&(r.downloadableVideoCount===1||r.downloadableVideoCount===0&&e.length<=1),W=async()=>{var p;if(m){a({type:"SET_RECORDING_PHASE",phase:"starting"}),a({type:"SET_BANNER",banner:null});try{const s=await gt(e);if(!((p=s.recordings)!=null&&p.length)){a({type:"SET_RECORDING_PHASE",phase:"idle"}),a({type:"SET_BANNER",banner:{tone:"error",message:"Recording did not start. Check that live preview is working, then try again."}});return}const i={};for(const N of s.recordings)i[N.udid]=N.id;const f=e.length-s.recordings.length;a({type:"START_RECORDING",groupId:s.groupId,startedAt:Date.now(),tileIds:i,compositeEnabled:s.compositeEnabled===!0}),f>0?a({type:"SET_BANNER",banner:{tone:"error",message:`Recording started on ${s.recordings.length} of ${e.length} devices (${f} failed).`}}):a({type:"SET_BANNER",banner:null})}catch(s){if(a({type:"SET_RECORDING_PHASE",phase:"idle"}),s instanceof xt)if(s.body.error==="device_busy"){const i=(s.body.busyDevices??[]).map(f=>`${f.udid} (${f.reason})`).join(", ");a({type:"SET_BANNER",banner:{tone:"error",message:`Cannot start — these devices are busy: ${i}`}})}else a({type:"SET_BANNER",banner:{tone:"error",message:`Server-wide recording cap reached (${s.body.active}/${s.body.limit}).`}});else a({type:"SET_BANNER",banner:{tone:"error",message:`Recording failed: ${s.message}`}})}}},A=async()=>{if(!(!r.groupId||r.recordingPhase!=="recording")){a({type:"SET_RECORDING_PHASE",phase:"stopping"});try{const p=await bt(r.groupId),s=(p.recordings??[]).filter(f=>f.status==="STOPPED"&&(f.sizeBytes??0)>=1024),i=(p.recordings??[]).filter(f=>f.status!=="STOPPED");a({type:"STOP_RECORDING",downloadableVideoCount:s.length,compositeEnabled:r.compositeEnabled}),s.length===0?a({type:"SET_BANNER",banner:{tone:"error",message:i.length>0?`Recording failed on ${i.length} device(s). Nothing to download.`:"Recording stopped but no playable video was produced."}}):i.length>0?a({type:"SET_BANNER",banner:{tone:"info",message:`${s.length} ready, ${i.length} failed. Download with the button above.`}}):a({type:"SET_BANNER",banner:{tone:"info",message:s.length===1?"Video ready. Download it with the button above.":`${s.length} videos ready. Download them with the button above.`}})}catch(p){a({type:"STOP_RECORDING"}),a({type:"SET_BANNER",banner:{tone:"error",message:`Stop failed: ${p.message}`}})}}},I=p=>{const s=p==="annotate";s&&!b||s!==r.annotateMode&&a({type:"SET_ANNOTATE_MODE",enabled:s})};c.useEffect(()=>{if(!r.annotateMode)return;const p=s=>{var f;if(s.key!=="Escape"||s.repeat)return;const i=s.target;i&&((f=i.closest)!=null&&f.call(i,"input, textarea, select")||i.isContentEditable)||a({type:"SET_ANNOTATE_MODE",enabled:!1})};return window.addEventListener("keydown",p),()=>window.removeEventListener("keydown",p)},[r.annotateMode,a]);const O=()=>{b&&n()},$=Object.values(r.overlayAnnotations).some(p=>((p==null?void 0:p.length)??0)>0),K=[{id:"RECT",label:"Rect"},{id:"CIRCLE",label:"Circle"},{id:"ARROW",label:"Arrow"},{id:"FREEHAND",label:"Draw"}];return t.jsxs("div",{className:"flex items-center gap-2 flex-wrap justify-end",children:[r.recordingPhase==="recording"||r.recordingPhase==="stopping"?t.jsxs("span",{className:"inline-flex items-center gap-1.5 px-2 py-1 rounded text-xs font-mono tabular-nums bg-[rgb(var(--rgb-red-600)/0.2)] text-[var(--red-200)] border border-[rgb(var(--rgb-red)/0.4)]",title:"Recording elapsed time",children:[t.jsx("span",{className:"inline-block w-1.5 h-1.5 rounded-full bg-[var(--red)] animate-pulse"}),"REC ",nt(x),r.recordingPhase==="stopping"?" · Stopping…":""]}):t.jsxs("button",{type:"button",onClick:W,disabled:!m,title:e.length===0?"Add a device to the grid first":"Record every device on the grid",className:"inline-flex items-center gap-1.5 px-3 py-1.5 text-sm rounded bg-[var(--red-600)] text-white disabled:opacity-40 disabled:cursor-not-allowed light:disabled:bg-transparent light:disabled:text-[var(--text)] light:disabled:ring-1 light:disabled:ring-inset light:disabled:ring-[color:var(--border)]",children:[t.jsx(Ne,{...Y,size:10,fill:"currentColor"}),Jt(e.length,r.recordingPhase)]}),t.jsxs("button",{type:"button",onClick:A,disabled:!l,className:"inline-flex items-center gap-1.5 px-3 py-1.5 text-sm rounded border border-[var(--border)] disabled:opacity-40 disabled:cursor-not-allowed",children:[t.jsx(je,{...Y,size:12}),r.recordingPhase==="stopping"?"Stopping…":"Stop"]}),t.jsx(ft,{size:"sm",value:r.annotateMode?"annotate":"interact",onChange:I,segments:[{value:"interact",label:"Interact",title:"Tap, swipe and type on the devices (Esc)",keyShortcuts:"Escape"},{value:"annotate",label:"Annotate",title:b?"Draw marks on the recording":"Start recording to annotate",disabled:!b}]}),y&&t.jsxs("div",{className:"flex items-center gap-1 ml-1 pl-2 border-l border-[var(--border)]",children:[K.map(p=>t.jsx("button",{type:"button",disabled:!d,"aria-pressed":r.shape===p.id,onClick:()=>a({type:"SET_SHAPE",shape:p.id}),className:`px-2 py-1 text-xs rounded border disabled:opacity-40 disabled:cursor-not-allowed ${r.shape===p.id?"bg-[var(--surface-2)] border-[var(--color-info)] text-[rgb(var(--rgb-fg))]":"border-[var(--border)] opacity-80 hover:opacity-100"}`,children:p.label},p.id)),t.jsx("input",{type:"color",value:r.color,disabled:!d,onChange:p=>a({type:"SET_COLOR",color:p.target.value}),title:"Annotation color","aria-label":"Annotation color",className:"w-7 h-7 rounded border border-[var(--border)] bg-transparent cursor-pointer disabled:opacity-40 disabled:cursor-not-allowed"}),t.jsx("button",{type:"button",onClick:O,disabled:!b||!$,title:"Clear marks from the preview and the recording",className:"px-2 py-1 text-xs rounded border border-[var(--border)] hover:bg-[var(--surface-2)] disabled:opacity-40 disabled:cursor-not-allowed",children:"Clear marks"})]}),S&&R&&t.jsxs("a",{href:pt(r.groupId),className:"ml-1 inline-flex items-center gap-1.5 px-3 py-1.5 text-sm rounded border border-[var(--border)] hover:bg-[var(--surface-2)]",download:!0,children:[t.jsx(ae,{...Y}),"Download video"]}),S&&!R&&t.jsxs("a",{href:ht(r.groupId),className:"ml-1 inline-flex items-center gap-1.5 px-3 py-1.5 text-sm rounded border border-[var(--border)] hover:bg-[var(--surface-2)]",download:`videos-${r.groupId}.zip`,title:"ZIP of mp4 files only (no JSON extras)",children:[t.jsx(ae,{...Y}),"Download ",v?"videos":"video"]}),h&&t.jsxs("a",{href:mt(r.groupId),className:"inline-flex items-center gap-1.5 px-3 py-1.5 text-sm rounded border border-[var(--border)] hover:bg-[var(--surface-2)]",download:`side-by-side-${r.groupId}.mp4`,title:"All devices in one side-by-side video",children:[t.jsx(ae,{...Y}),"Side-by-side"]}),S&&r.groupId&&t.jsxs(Ue,{to:`/recordings/${encodeURIComponent(r.groupId)}`,className:"inline-flex items-center gap-1.5 px-3 py-1.5 text-sm rounded border border-[var(--border)] hover:bg-[var(--surface-2)]",children:[t.jsx(Fe,{...Y}),"Open in Recordings"]})]})}function Zt({remainingSec:e,onContinue:n,onReleaseNow:r}){return t.jsx("div",{role:"dialog","aria-modal":"true","aria-labelledby":"idle-warning-title",className:"fixed inset-0 z-40 bg-black/60 flex items-center justify-center",children:t.jsxs("div",{className:"bg-[var(--bg)] border border-[var(--border)] rounded-lg w-full max-w-md p-6",children:[t.jsx("h3",{id:"idle-warning-title",className:"text-base font-semibold mb-2",children:"Are you still there?"}),t.jsxs("p",{className:"text-sm text-[var(--text-dim)] mb-4",children:["Your devices will be released in"," ",t.jsxs("span",{className:"text-[var(--text)] font-mono font-semibold",children:[e,"s"]})," due to inactivity. Click ",t.jsx("strong",{children:"Continue Session"})," to keep them locked."]}),t.jsxs("div",{className:"flex justify-end gap-2",children:[t.jsx("button",{type:"button",onClick:r,className:"h-9 px-3 rounded-md border border-[var(--border)] text-sm text-[var(--text-dim)] hover:text-[var(--text)]",children:"Release now"}),t.jsx("button",{type:"button",autoFocus:!0,onClick:n,className:"h-9 px-4 rounded-md bg-[var(--color-accent)] text-[var(--color-on-accent)] text-sm font-medium",children:"Continue session"})]})]})})}function Qt({message:e,onRestore:n,onDismiss:r}){return t.jsxs("div",{role:"status",className:"text-sm rounded border border-[var(--border)] bg-[var(--surface-2)] text-[var(--text)] px-3 py-2 flex items-center gap-2",children:[t.jsx(wt,{"aria-hidden":!0,size:14,className:"shrink-0 text-[var(--text-dim)]"}),t.jsx("span",{className:"flex-1",children:e}),n&&t.jsx(me,{variant:"tonal",size:"sm",onClick:n,children:"Restore devices"}),t.jsx(me,{variant:"ghost",size:"sm",onClick:r,children:"Dismiss"})]})}function Re(e){var n;return((n=e.marketingName)==null?void 0:n.trim())||e.name||e.udid}function Se(e){if(!e)return"9 / 16";const n=Number(e.screenWidth),r=Number(e.screenHeight);if(Number.isFinite(n)&&Number.isFinite(r)&&n>0&&r>0)return`${n} / ${r}`;const a=(e.platform||"").toLowerCase();return a==="tvos"||a==="androidtv"||a==="android-tv"?"16 / 9":"9 / 16"}function ie(e){const n=Number(e.screenWidth),r=Number(e.screenHeight);return{udid:e.udid,name:Re(e),mjpegPort:0,aspect:Se(e),screenWidth:Number.isFinite(n)&&n>0?n:void 0,screenHeight:Number.isFinite(r)&&r>0?r:void 0,platform:e.platform}}function er(e,n){return n?"blocked":e?"remove":"add"}function ce(e,n,r){return!r||!e?!1:e===`manual_${r}_${n}`}function tr(e,n){if(!e.busy)return;const r=Ee(e);return r?r.startsWith("manual_")?ce(r,e.udid,n)?"manual_self":"manual_other":"automation":"unknown"}function rr(e,n){return e.busy?ce(Ee(e),e.udid,n):!1}function nr(e,n){return e>0&&n==="idle"}function sr(e,n,r){const a=[],g=[];for(const j of e){const x=n.find(m=>m.udid===j.udid),w=(x==null?void 0:x.name)||j.name||j.udid;x?x.offline?g.push({name:w,reason:"offline"}):x.busy&&!ce(x.session_id,x.udid,r)?g.push({name:w,reason:"in_use"}):a.push(x):g.push({name:w,reason:"gone"})}return{restore:a,skipped:g}}const Te=(e,n)=>`${e} ${n}${e===1?"":"s"}`;function or(e,n){return`Released ${Te(e,"device")} after ${n} minutes without activity.`}const ar={in_use:"is now in use by someone else.",offline:"is offline.",gone:"is no longer connected."};function ir(e,n){return n.length===0?null:[e>0?`Restored ${Te(e,"device")}.`:"No devices restored.",...n.map(a=>`${a.name} ${ar[a.reason]}`)].join(" ")}function cr(){let e=Promise.resolve();return{enqueue(n){const r=e.then(n,n);return e=r.then(()=>{},()=>{}),r}}}const ye=["mousemove","mousedown","keydown","touchstart","wheel"];function lr({idleAfterMs:e,warningSec:n,onWarning:r,onTimeout:a,enabled:g}){const[j,x]=c.useState(!1),[w,m]=c.useState(null),y=c.useRef(null),l=c.useRef(null),b=c.useRef(!1),d=c.useRef(r),S=c.useRef(a);d.current=r,S.current=a;function v(){y.current&&(clearTimeout(y.current),y.current=null),l.current&&(clearInterval(l.current),l.current=null)}function h(){b.current=!0,x(!0),m(n),d.current(n);let A=n;l.current=setInterval(()=>{A-=1,A<=0?(v(),b.current=!1,x(!1),m(null),S.current()):m(A)},1e3)}function R(){v(),b.current=!1,x(!1),m(null);const A=e-n*1e3;y.current=setTimeout(h,Math.max(0,A))}function W(){g&&R()}return c.useEffect(()=>{if(!g){v(),b.current=!1,x(!1),m(null);return}function A(){b.current||R()}return ye.forEach(I=>window.addEventListener(I,A,{passive:!0})),R(),()=>{ye.forEach(I=>window.removeEventListener(I,A)),v()}},[g,e,n]),{warning:j,remainingSec:w,reset:W}}const we=5*60*1e3,dr=30;function ur(e,n){return{udid:e.udid,name:Re(e),altName:e.name,platform:e.platform,busy:e.busy,busyReason:tr(e,n),mjpegServerPort:e.mjpegServerPort,offline:!!e.offline}}function Sr(){const[e,n]=st(),[r,a]=c.useState([]),[g,j]=c.useState("loading"),[x,w]=c.useState(0),[m,y]=c.useState(null),l=c.useRef(!1),b=c.useRef(cr());c.useEffect(()=>{let s=!1;return fetch("/xenon/api/auth/me",{credentials:"include"}).then(i=>i.ok?i.json():null).then(i=>{!s&&(i!=null&&i.userId)&&y(i.userId)}).catch(()=>{}),()=>{s=!0}},[]),c.useEffect(()=>{let s=!1;const i=async()=>{try{const N=await D.getDevices();if(s)return;a(Array.isArray(N)?N:[]),j("ready")}catch{s||j("error")}};i();const f=window.setInterval(i,5e3);return()=>{s=!0,window.clearInterval(f)}},[x]),c.useEffect(()=>{let s=!1;if(!(!m||l.current))return l.current=!0,(async()=>{var N;let i=[],f=[];try{const B=await D.getDevices();i=Array.isArray(B)?B:[];const M=i.filter(H=>rr(H,m));if(f=(await Promise.all(M.map(async H=>{try{const U=await fetch(`/xenon/api/control/${encodeURIComponent(H.udid)}/stream/status`);if(!U.ok)return null;const z=await U.json();return(z==null?void 0:z.status)==="running"?H:null}catch{return null}}))).filter(H=>H!==null).map(ie),s)return;f.length>0&&n({type:"SET_TILES",tiles:f})}catch{}try{const{serverNow:B,groups:M}=await vt(),E=M[0];if(s||!E)return;const H=new Set(f.map(T=>T.udid)),U=E.recordings.filter(T=>!H.has(T.udid)).map(T=>i.find(G=>G.udid===T.udid)).filter(T=>!!T).map(ie);U.length>0&&n({type:"SET_TILES",tiles:[...f,...U]});const z={};for(const T of E.annotations){let G;try{G=JSON.parse(T.geometry)}catch{continue}(z[N=T.recordingId]??(z[N]=[])).push({shape:T.shape,color:T.color,geometry:G,text:T.text??void 0})}n({type:"REHYDRATE_RECORDING",groupId:E.groupId,startedAt:Date.now()-(B-Date.parse(E.startedAt)),tileIds:Object.fromEntries(E.recordings.map(T=>[T.udid,T.id])),compositeEnabled:E.compositeEnabled,overlayAnnotations:z})}catch{}})(),()=>{s=!0}},[m,n]),c.useEffect(()=>{for(const s of e.tiles){if(s.screenWidth&&s.screenHeight)continue;const i=r.find(B=>B.udid===s.udid);if(!i)continue;const f=Number(i.screenWidth),N=Number(i.screenHeight);Number.isFinite(f)&&f>0&&Number.isFinite(N)&&N>0&&n({type:"PATCH_TILE_DIMS",udid:s.udid,screenWidth:f,screenHeight:N,aspect:Se(i)})}},[r,e.tiles,n]);const d=c.useMemo(()=>new Set(e.tiles.map(s=>s.udid)),[e.tiles]),S=c.useMemo(()=>r.map(s=>ur(s,m)),[r,m]),v=async s=>{n({type:"ADD_TILE",tile:ie(s)});const i=await D.startStream(s.udid).catch(()=>null);Ge(i)&&n({type:"REMOVE_TILE",udid:s.udid})},h=async s=>{const i=er(e.tiles.some(N=>N.udid===s),e.recording);if(i==="blocked"){n({type:"SET_ERROR_BANNER",message:"Stop recording before changing the devices on the grid."});return}if(i==="remove"){await R(s);return}const f=r.find(N=>N.udid===s);f&&await v(f)},R=async s=>{n({type:"REMOVE_TILE",udid:s});try{await fetch(`/xenon/api/control/${encodeURIComponent(s)}/stream/leave`,{method:"POST"})}catch{}w(i=>i+1)},W=(s,i,f)=>{if(!e.groupId)return;const N=e.groupId,B=e.startedAt?Date.now()-e.startedAt:0,M={recordingId:s,timecodeMs:B,shape:i.shape,geometry:JSON.stringify(i.geometry),color:i.color,text:i.text};b.current.enqueue(async()=>{try{return await he(N,f?{...M,image:f}:M)}catch(E){if(!f)throw E;return he(N,M)}}).catch(E=>n({type:"SET_ERROR_BANNER",message:`Annotation failed: ${E.message}`}))},A=()=>{if(!e.groupId)return;const s=e.groupId,i=e.startedAt?Date.now()-e.startedAt:0;n({type:"CLEAR_OVERLAY_ANNOTATIONS"}),b.current.enqueue(()=>yt(s,i)).catch(()=>n({type:"SET_ERROR_BANNER",message:"Couldn't clear marks from the recording. They will still appear in the video."}))},I=()=>{for(const s of e.tiles){const i=`/xenon/api/control/${encodeURIComponent(s.udid)}/stream/leave`;try{navigator.sendBeacon?navigator.sendBeacon(i):fetch(i,{method:"POST",keepalive:!0}).catch(()=>{})}catch{}}e.tiles.forEach(s=>n({type:"REMOVE_TILE",udid:s.udid}))},[O,$]=c.useState(null),K=async()=>{const s=O==null?void 0:O.tiles;if(!s)return;const i=sr(s,r,m);for(const N of i.restore)await v(N);const f=ir(i.restore.length,i.skipped);$(f?{message:f,tiles:null}:null),w(N=>N+1)},p=lr({idleAfterMs:we,warningSec:dr,enabled:nr(e.tiles.length,e.recordingPhase),onWarning:()=>{},onTimeout:()=>{const s=e.tiles;I(),$({message:or(s.length,we/6e4),tiles:s})}});return t.jsxs(ot.Provider,{value:{state:e,dispatch:n},children:[t.jsxs("div",{className:"flex flex-col h-full p-4 gap-3",children:[t.jsxs("header",{className:"flex items-center justify-between gap-3",children:[t.jsxs("div",{className:"flex items-center gap-3",children:[t.jsx(Nt,{icon:Ke,children:"Live devices"}),t.jsx(Tt,{value:e.layout,onChange:s=>n({type:"SET_LAYOUT",layout:s})})]}),t.jsx(qt,{selectedUdids:e.tiles.map(s=>s.udid),onClearMarks:A})]}),e.banner&&t.jsxs("div",{role:"alert",className:`text-sm rounded border px-3 py-2 flex items-center gap-2 ${e.banner.tone==="error"?"border-[rgb(var(--rgb-red)/0.4)] bg-[rgb(var(--rgb-red)/0.1)] text-[var(--red-100)]":"border-[rgb(var(--rgb-success)/0.4)] bg-[rgb(var(--rgb-success)/0.1)] text-[var(--text)]"}`,children:[t.jsx("span",{className:"flex-1",children:e.banner.message}),t.jsx("button",{type:"button",className:"text-xs opacity-80 hover:opacity-100",onClick:()=>n({type:"SET_BANNER",banner:null}),children:"Dismiss"})]}),O&&t.jsx(Qt,{message:O.message,onRestore:O.tiles&&!e.recording?K:void 0,onDismiss:()=>$(null)}),t.jsxs("div",{className:"grid grid-cols-[260px_1fr] gap-3 flex-1 min-h-0",children:[t.jsxs("aside",{className:"flex flex-col gap-2 border border-[var(--border)] rounded p-3 overflow-y-auto min-h-0",children:[t.jsxs("div",{className:"flex items-center justify-between",children:[t.jsx("span",{className:"text-xs text-[var(--text-dim)]",children:"Devices"}),t.jsx("button",{className:"text-xs underline opacity-70 hover:opacity-100",onClick:()=>w(s=>s+1),children:"Refresh"})]}),t.jsx(Rt,{devices:S,inMosaic:d,onToggle:h,status:g}),t.jsx("p",{className:"mt-2 text-[11px] text-[var(--text-dim)] leading-relaxed",children:"Click a device to add it to the grid. Record captures every device on the grid; after Stop, download the video."})]}),t.jsx("main",{className:"border border-[var(--border)] rounded p-2 overflow-y-auto h-full min-h-0",children:t.jsx(Yt,{layout:e.layout,tiles:e.tiles,annotateMode:e.annotateMode,shape:e.shape,color:e.color,overlayAnnotationsByRecording:e.overlayAnnotations,onOverlayAnnotationsChange:(s,i)=>n({type:"SET_OVERLAY_ANNOTATIONS",recordingId:s,annotations:i}),onAnnotation:W,onRemove:R,onDropDevice:h})})]})]}),p.warning&&p.remainingSec!==null&&t.jsx(Zt,{remainingSec:p.remainingSec,onContinue:p.reset,onReleaseNow:I})]})}export{Sr as default};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{j as s}from"./index-
|
|
1
|
+
import{j as s}from"./index-B0gQkEg0.js";const i=({icon:e,title:m,description:t,action:a})=>s.jsxs("div",{className:"empty-state",children:[e&&s.jsx("div",{className:"empty-state-icon",children:e}),s.jsx("div",{className:"empty-state-title",children:m}),t&&s.jsx("div",{className:"empty-state-desc",children:t}),a&&s.jsx("div",{className:"empty-state-action",children:a})]});export{i as E};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{j as s}from"./index-
|
|
1
|
+
import{j as s}from"./index-B0gQkEg0.js";const r=({label:e,description:l,children:c,error:a,htmlFor:d})=>s.jsxs("div",{className:"fg",children:[s.jsx("label",{className:"fg-label",htmlFor:d,children:e}),l&&s.jsx("div",{className:"fg-desc",children:l}),s.jsx("div",{className:"fg-control",children:c}),a&&s.jsx("div",{className:"fg-error",children:a})]});export{r as F};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{r as t,j as s,v as d}from"./index-
|
|
1
|
+
import{r as t,j as s,v as d}from"./index-B0gQkEg0.js";import{P as b,M as C,a as g}from"./Menu-KrPnfdsf.js";import{P as k}from"./plus-C9Zj84UF.js";function F({name:a,value:r,anyValue:o,options:u,onChange:f}){const[m,p]=t.useState(!1),x=t.useRef(null),h=t.useRef(null),c=r!==o,l=u.find(e=>e.value===r),n=()=>{var e;p(!1),(e=h.current)==null||e.focus()},j=t.useCallback(e=>{const i=(e==null?void 0:e.querySelector('[aria-checked="true"]'))??(e==null?void 0:e.querySelector('[role="menuitemradio"]'));i==null||i.focus()},[]);return s.jsxs("span",{ref:x,className:`fm${c?" fm-set":""}`,children:[s.jsx("button",{ref:h,type:"button",className:"fm-trigger","aria-haspopup":"menu","aria-expanded":m,onClick:()=>p(e=>!e),children:c?s.jsxs(s.Fragment,{children:[s.jsxs("span",{className:"fm-name",children:[a,":"]})," ",(l==null?void 0:l.label)??r]}):s.jsxs(s.Fragment,{children:[s.jsx(k,{size:12,"aria-hidden":"true"}),a]})}),c&&s.jsx("button",{type:"button",className:"fm-clear","aria-label":`Clear ${a.toLowerCase()} filter`,onClick:()=>{f(o),n()},children:s.jsx(d,{size:12,"aria-hidden":"true"})}),s.jsx(b,{open:m,onClose:n,anchorRef:x,placement:"bottom-start",children:s.jsx("div",{ref:j,children:s.jsx(C,{children:u.map(e=>s.jsx(g,{checked:e.value===r,note:e.note,trailing:s.jsx("span",{className:"fm-count",children:e.count}),onClick:()=>{f(e.value),n()},children:e.label},e.value))})})})]})}export{F};
|