@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.
Files changed (335) hide show
  1. package/README.md +44 -29
  2. package/lib/package.json +6 -5
  3. package/lib/public/assets/{AnnotationOverlay-B_VNtKBH.js → AnnotationOverlay-2W4JvEyZ.js} +1 -1
  4. package/lib/public/assets/{ApiKeyGate-BwuaMjkX.js → ApiKeyGate-GmckX_7r.js} +1 -1
  5. package/lib/public/assets/DeviceMosaicView-DqODzkyo.js +2 -0
  6. package/lib/public/assets/{EmptyState-BSjhk_IV.js → EmptyState-7hETKEWb.js} +1 -1
  7. package/lib/public/assets/{FieldGroup-Cb5QZsE1.js → FieldGroup-GOgssFEP.js} +1 -1
  8. package/lib/public/assets/{FilterMenu-BAaSlWbW.js → FilterMenu-Yvf6RGMd.js} +1 -1
  9. package/lib/public/assets/{Menu-JGGYrSQQ.js → Menu-KrPnfdsf.js} +1 -1
  10. package/lib/public/assets/{Modal-CFCp3n9d.js → Modal-C6WjZFeI.js} +1 -1
  11. package/lib/public/assets/{RecordingPage-CbJj_EBT.js → RecordingPage-CSRIhZEh.js} +1 -1
  12. package/lib/public/assets/{RecordingsPage-BJeYnygR.js → RecordingsPage-CB8ST3nE.js} +1 -1
  13. package/lib/public/assets/{SegmentedControl-Bg42Atjy.js → SegmentedControl-C9o90KId.js} +1 -1
  14. package/lib/public/assets/{SettingCard-Njz1pUIJ.js → SettingCard-CPm3w9l2.js} +1 -1
  15. package/lib/public/assets/{Table-BcTm5xV2.js → Table-DwIgERRt.js} +1 -1
  16. package/lib/public/assets/{activity-DOVdsfMQ.js → activity-DrLbHifn.js} +1 -1
  17. package/lib/public/assets/ai-settings-DU_Ls06P.js +16 -0
  18. package/lib/public/assets/{api-keys-BKnNSTw9.js → api-keys-CWYdEzJ_.js} +1 -1
  19. package/lib/public/assets/{apps-D0SC6uw6.js → apps-BqNVdln7.js} +1 -1
  20. package/lib/public/assets/{arrow-left-MMbTmgoE.js → arrow-left-BtB0_Jg_.js} +1 -1
  21. package/lib/public/assets/{arrow-right-BMfnGmxW.js → arrow-right-C8NhdtyN.js} +1 -1
  22. package/lib/public/assets/{arrow-up-right-DuAftMWB.js → arrow-up-right-gMg0ffvv.js} +1 -1
  23. package/lib/public/assets/{auth-shell-nZC2JNDO.js → auth-shell-CiYZ3N4M.js} +2 -2
  24. package/lib/public/assets/{builds-page-hDCgTwkS.js → builds-page-EaaqDmLN.js} +1 -1
  25. package/lib/public/assets/{button-Dda4irn_.js → button-DDau4Axv.js} +1 -1
  26. package/lib/public/assets/{calendar-BMr0Q62v.js → calendar-DQ83B1ed.js} +1 -1
  27. package/lib/public/assets/{check-CZ9iw9-e.js → check-K1ovTQam.js} +1 -1
  28. package/lib/public/assets/{chevron-right-BRYJJVC9.js → chevron-right-Biq00pMt.js} +1 -1
  29. package/lib/public/assets/{circle-check-CqAJjQ5O.js → circle-check-CBv88cH7.js} +1 -1
  30. package/lib/public/assets/{circle-x-DlDJtnVz.js → circle-x-Bmltailn.js} +1 -1
  31. package/lib/public/assets/{clock-CpuDKysU.js → clock-BPrld-3h.js} +1 -1
  32. package/lib/public/assets/{copy-CAQydNFe.js → copy-t0QB14JI.js} +1 -1
  33. package/lib/public/assets/derive-UnLRp4r3.js +1 -0
  34. package/lib/public/assets/device-explorer-BgGZ8AvV.js +282 -0
  35. package/lib/public/assets/{download-C9XG6lLv.js → download-BroHxNOh.js} +1 -1
  36. package/lib/public/assets/forgot-password-Bjmf49i6.js +1 -0
  37. package/lib/public/assets/{h264Stream-VQBphYhg.js → h264Stream-DeD__6wa.js} +6 -6
  38. package/lib/public/assets/{index-DJGc1M2f.js → index-72oF89LJ.js} +1 -1
  39. package/lib/public/assets/index-B0JeuZbg.js +15 -0
  40. package/lib/public/assets/index-B0gQkEg0.js +219 -0
  41. package/lib/public/assets/{input-Cm0tmSoc.js → input-DIc4MSC7.js} +1 -1
  42. package/lib/public/assets/{line-chart-D2DyJCml.js → line-chart-BcWm2vW8.js} +1 -1
  43. package/lib/public/assets/{list-checks-UWf1GY99.js → list-checks-B06IJJad.js} +1 -1
  44. package/lib/public/assets/{lock-CCJv9m_I.js → lock-pVLvnN30.js} +1 -1
  45. package/lib/public/assets/{login-f8vOVW-U.js → login-CN77vy--.js} +1 -1
  46. package/lib/public/assets/{maintenance-settings-C1PrkuAH.js → maintenance-settings-CSdzQSkY.js} +1 -1
  47. package/lib/public/assets/{monitor-B2jDgwm7.js → monitor-BtrN5IkI.js} +1 -1
  48. package/lib/public/assets/{mouse-pointer-2-DLI1RtzU.js → mouse-pointer-2-CNsQfo6w.js} +1 -1
  49. package/lib/public/assets/{network-ebT86qoi.js → network-BSfA8ZxG.js} +1 -1
  50. package/lib/public/assets/{overview-C0aykVrd.js → overview-BfTebCK5.js} +1 -1
  51. package/lib/public/assets/{page-header-BjzgWz-T.js → page-header-D8KUX6Qg.js} +1 -1
  52. package/lib/public/assets/{play-DLG9t8FU.js → play-Cvc8xzic.js} +1 -1
  53. package/lib/public/assets/{plus-nBFSMu_X.js → plus-C9Zj84UF.js} +1 -1
  54. package/lib/public/assets/{profile-page-CSl0UYlv.js → profile-page-Bi27STgs.js} +1 -1
  55. package/lib/public/assets/{reset-password-LeOuHqmB.js → reset-password-CPsOuwSu.js} +1 -1
  56. package/lib/public/assets/runbook-page-DEQ-DBOX.js +304 -0
  57. package/lib/public/assets/{select-88jHcl-Z.js → select-BWpnhWXj.js} +1 -1
  58. package/lib/public/assets/{selector-detail-redirect-BbXKJjZM.js → selector-detail-redirect-BefSCSpe.js} +1 -1
  59. package/lib/public/assets/{selector-health-page-B4rA1Xkv.js → selector-health-page-MSk96Zz7.js} +1 -1
  60. package/lib/public/assets/session-detail-page-DPyBNef_.js +4 -0
  61. package/lib/public/assets/{settings-Dep_3kbA.css → settings-K8UHWMdI.css} +1 -1
  62. package/lib/public/assets/{settings-GF6ZMSQ6.js → settings-V6x-cXwy.js} +1 -1
  63. package/lib/public/assets/{stat-tile-BV98e_Sv.js → stat-tile-Duex-GZD.js} +1 -1
  64. package/lib/public/assets/{tablet-BiAsnFDj.js → tablet-BBcoqBmL.js} +1 -1
  65. package/lib/public/assets/{teams-B4-jrr-1.js → teams-ts8p5rEh.js} +1 -1
  66. package/lib/public/assets/{trash-2-C5RSuX0e.js → trash-2-CZ2ew6AZ.js} +1 -1
  67. package/lib/public/assets/{upload-BG7_rMXK.js → upload-Dv99y_le.js} +1 -1
  68. package/lib/public/assets/{use-builds-data-J-riQYyC.js → use-builds-data-DrYC8JCF.js} +1 -1
  69. package/lib/public/assets/{use-password-reset-mode-C8-eH-UG.js → use-password-reset-mode-C7QkBNkP.js} +1 -1
  70. package/lib/public/assets/{users-CFba5TSy.js → users-D7tKMESH.js} +1 -1
  71. package/lib/public/assets/{users-Cp0beJm3.js → users-gJ5Y01yo.js} +1 -1
  72. package/lib/public/assets/{video-off-DDsEISjV.js → video-off-CqZVuZGs.js} +1 -1
  73. package/lib/public/assets/{webhook-settings-BGu32vSB.js → webhook-settings-DI5DSW99.js} +1 -1
  74. package/lib/public/assets/{zap-l4VaXzq8.js → zap-ClcHFxfo.js} +1 -1
  75. package/lib/public/index.html +1 -1
  76. package/lib/schema.json +16 -12
  77. package/lib/src/InternalHttpClient.js +27 -5
  78. package/lib/src/XenonCapabilityManager.js +10 -3
  79. package/lib/src/app/dashboardPluginLink.js +117 -0
  80. package/lib/src/app/index.js +13 -34
  81. package/lib/src/app/openapi/control.yaml +6 -4
  82. package/lib/src/app/openapi/grid.yaml +8 -2
  83. package/lib/src/app/openapi/identity.yaml +103 -31
  84. package/lib/src/app/openapi/platform.yaml +33 -15
  85. package/lib/src/app/openapi/sessions.yaml +12 -3
  86. package/lib/src/app/routers/apikeys.js +14 -2
  87. package/lib/src/app/routers/auth.js +31 -6
  88. package/lib/src/app/routers/control.js +13 -5
  89. package/lib/src/app/routers/dashboard.js +42 -27
  90. package/lib/src/app/routers/grid.js +5 -3
  91. package/lib/src/app/routers/profile.js +21 -0
  92. package/lib/src/app/routers/users.js +12 -6
  93. package/lib/src/app/swagger.js +2 -0
  94. package/lib/src/app/ws/h264StreamWs.js +10 -1
  95. package/lib/src/app/ws/nodeSocketRelay.js +77 -11
  96. package/lib/src/config.js +1 -0
  97. package/lib/src/dashboard/commandLogFields.js +42 -0
  98. package/lib/src/dashboard/commands.js +5 -5
  99. package/lib/src/dashboard/event-manager.js +64 -204
  100. package/lib/src/dashboard/services/failure-analysis-service.js +118 -105
  101. package/lib/src/dashboard/services/failureCategories.js +264 -0
  102. package/lib/src/dashboard/sessionCommandSummary.js +39 -0
  103. package/lib/src/data-service/device-store.js +14 -1
  104. package/lib/src/data-service/prisma-store.js +19 -7
  105. package/lib/src/data-service/web-config-service.js +7 -0
  106. package/lib/src/device-managers/AndroidDeviceManager.js +61 -23
  107. package/lib/src/device-managers/IOSDeviceManager.js +10 -0
  108. package/lib/src/device-managers/android/AndroidH264StreamService.js +69 -20
  109. package/lib/src/device-managers/android/H264Multiplexer.js +38 -4
  110. package/lib/src/device-utils.js +29 -3
  111. package/lib/src/gateway/defaultGateway.js +1 -1
  112. package/lib/src/gateway/forwardToNode.js +6 -11
  113. package/lib/src/gateway/healReport.js +87 -0
  114. package/lib/src/gateway/hubSessionToken.js +27 -4
  115. package/lib/src/gateway/nodeWebDriverUrl.js +3 -5
  116. package/lib/src/gateway/sessionGateway.js +12 -2
  117. package/lib/src/generated/client/edge.js +4 -3
  118. package/lib/src/generated/client/index-browser.js +1 -0
  119. package/lib/src/generated/client/index.d.ts +32 -0
  120. package/lib/src/generated/client/index.js +4 -3
  121. package/lib/src/generated/client/package.json +1 -1
  122. package/lib/src/generated/client/schema.prisma +3 -0
  123. package/lib/src/generated/client/wasm.js +1 -0
  124. package/lib/src/helpers/outboundProxy.js +219 -0
  125. package/lib/src/index.js +21 -0
  126. package/lib/src/interceptors/CommandInterceptor.js +256 -196
  127. package/lib/src/interceptors/xenonScripts.js +28 -7
  128. package/lib/src/logger.js +3 -1
  129. package/lib/src/middleware/authMiddleware.js +15 -3
  130. package/lib/src/middleware/commandCaller.js +3 -3
  131. package/lib/src/middleware/verifyCredential.js +58 -1
  132. package/lib/src/plugin.js +16 -6
  133. package/lib/src/scripts/database-check.js +3 -3
  134. package/lib/src/scripts/initialize-database.js +35 -33
  135. package/lib/src/scripts/run-migrations.js +1079 -27
  136. package/lib/src/services/AICommandService.js +9 -2
  137. package/lib/src/services/AIService.js +192 -42
  138. package/lib/src/services/ApiKeyService.js +16 -5
  139. package/lib/src/services/EmailService.js +29 -0
  140. package/lib/src/services/InterceptorService.js +6 -2
  141. package/lib/src/services/OrphanSweeper.js +8 -0
  142. package/lib/src/services/SelectorStateService.js +11 -7
  143. package/lib/src/services/SelectorVerificationJob.js +21 -6
  144. package/lib/src/services/ServerManager.js +11 -1
  145. package/lib/src/services/SessionLifecycleService.js +118 -44
  146. package/lib/src/services/SocketClient.js +11 -0
  147. package/lib/src/services/SocketServer.js +380 -45
  148. package/lib/src/services/TeamService.js +5 -1
  149. package/lib/src/services/TracingService.js +171 -60
  150. package/lib/src/services/UserService.js +14 -3
  151. package/lib/src/services/UserSessionService.js +16 -4
  152. package/lib/src/services/bug-report/video-slice.js +4 -1
  153. package/lib/src/services/device-access/leaseOverride.js +7 -5
  154. package/lib/src/services/healing/FuzzyXmlHealingProvider.js +51 -0
  155. package/lib/src/services/healing/HealEtalonService.js +3 -2
  156. package/lib/src/services/healing/HealingOrchestrator.js +57 -72
  157. package/lib/src/services/healing/OcrHealingProvider.js +41 -64
  158. package/lib/src/services/healing/ResilioTreeHealingProvider.js +41 -50
  159. package/lib/src/services/healing/VisualAiHealingProvider.js +1 -1
  160. package/lib/src/services/healing/fingerprintIdentity.js +126 -0
  161. package/lib/src/services/healing/resilioPath.js +274 -0
  162. package/lib/src/services/identity/identityChanges.js +41 -0
  163. package/lib/src/services/logcat/SessionDeviceLogs.js +358 -0
  164. package/lib/src/services/logcat/deviceLogBook.js +164 -0
  165. package/lib/src/services/logcat/iosDriverLog.js +178 -0
  166. package/lib/src/services/ocr/ocrData.js +118 -0
  167. package/lib/src/services/ocr/vendor/LICENSE-tessdata.txt +202 -0
  168. package/lib/src/services/ocr/vendor/eng.traineddata.gz +0 -0
  169. package/lib/src/services/omni-vision/OmniVisionService.js +83 -56
  170. package/lib/src/services/passwordResetLink.js +50 -9
  171. package/lib/src/services/passwordResetMode.js +3 -1
  172. package/lib/src/services/publicUrl.js +45 -0
  173. package/lib/src/services/selector-health/SelectorVisibilityResolver.js +121 -0
  174. package/lib/src/services/session/sessionIdentity.js +5 -3
  175. package/lib/src/services/session/shutdownReason.js +26 -0
  176. package/lib/src/services/settings/aiEngineSettings.js +219 -0
  177. package/lib/src/services/token/JwtKeyService.js +8 -2
  178. package/lib/src/services/token/mcpTokenTtl.js +27 -0
  179. package/lib/src/services/token/mintedLifetime.js +51 -0
  180. package/lib/src/sessions/LocalSession.js +29 -1
  181. package/lib/src/sessions/RemoteSession.js +12 -2
  182. package/lib/src/sessions/SessionManager.js +4 -1
  183. package/lib/src/sessions/sessionMemory.js +25 -0
  184. package/lib/test/helpers/driver-log.js +38 -0
  185. package/lib/test/helpers/fake-ai-provider.js +188 -0
  186. package/lib/test/helpers/fake-http-proxy.js +106 -0
  187. package/lib/test/helpers/loki-stores.js +33 -0
  188. package/lib/test/helpers/otlp-probe.js +107 -0
  189. package/lib/test/helpers/page-sources.js +164 -0
  190. package/lib/test/helpers/scratch-database.js +57 -13
  191. package/lib/test/integration/androidDevices.spec.js +1 -1
  192. package/lib/test/integration/audit-ingest.spec.js +10 -10
  193. package/lib/test/integration/auth-flow.spec.js +9 -14
  194. package/lib/test/integration/auth-rate-limit.spec.js +6 -3
  195. package/lib/test/integration/bug-report-route.spec.js +22 -2
  196. package/lib/test/integration/forgot-password-rate-limit.spec.js +6 -3
  197. package/lib/test/integration/forgot-password.spec.js +17 -18
  198. package/lib/test/integration/ios/01iOSSimulator.spec.js +1 -1
  199. package/lib/test/integration/ios/02iOSDevices.spec.js +1 -1
  200. package/lib/test/integration/legacy-deprecation-headers.spec.js +5 -2
  201. package/lib/test/integration/migration.spec.js +6 -1
  202. package/lib/test/integration/node-pair-auth-rest.spec.js +5 -9
  203. package/lib/test/integration/ports-router.spec.js +4 -4
  204. package/lib/test/integration/prisma-device-store-add-devices.spec.js +2 -3
  205. package/lib/test/integration/prisma-device-store-team-filter.spec.js +2 -5
  206. package/lib/test/integration/profile-tokens.spec.js +10 -16
  207. package/lib/test/integration/recordings-router.spec.js +16 -16
  208. package/lib/test/integration/reset-revokes-sessions.spec.js +4 -9
  209. package/lib/test/integration/role-matrix.spec.js +4 -9
  210. package/lib/test/integration/sdk-leases-router.spec.js +9 -9
  211. package/lib/test/integration/sdk-version.spec.js +2 -2
  212. package/lib/test/integration/selector-health-endpoint.spec.js +5 -5
  213. package/lib/test/integration/selector-state-flow.spec.js +5 -5
  214. package/lib/test/integration/team-membership-migration.spec.js +73 -12
  215. package/lib/test/integration/team-visibility-apps.spec.js +15 -22
  216. package/lib/test/integration/team-visibility-control.spec.js +96 -69
  217. package/lib/test/integration/team-visibility-dashboard.spec.js +7 -23
  218. package/lib/test/integration/team-visibility-grid.spec.js +16 -15
  219. package/lib/test/integration/team-visibility-leases.spec.js +8 -11
  220. package/lib/test/integration/team-visibility-recordings.spec.js +6 -19
  221. package/lib/test/integration/team-visibility-sessions.spec.js +6 -19
  222. package/lib/test/integration/users-flow.spec.js +7 -12
  223. package/lib/test/integration/users-lockout.spec.js +5 -3
  224. package/lib/test/unit/EmailService.test.js +66 -0
  225. package/lib/test/unit/ResilioTreeHealingProvider.test.js +46 -19
  226. package/lib/test/unit/XenonCapabilityManager.spec.js +23 -0
  227. package/lib/test/unit/ai-call-time-limit.spec.js +130 -0
  228. package/lib/test/unit/ai-engine-settings.spec.js +338 -0
  229. package/lib/test/unit/ai-provider-switch.spec.js +130 -0
  230. package/lib/test/unit/ai-rate-limit.spec.js +126 -0
  231. package/lib/test/unit/android-adb-retry.spec.js +112 -0
  232. package/lib/test/unit/android-h264-viewers.spec.js +272 -0
  233. package/lib/test/unit/android-preview-hold.spec.js +3 -2
  234. package/lib/test/unit/apikeys-router.spec.js +84 -0
  235. package/lib/test/unit/authMiddleware.bearer.spec.js +9 -0
  236. package/lib/test/unit/authMiddleware.credentialLifetime.spec.js +149 -0
  237. package/lib/test/unit/bug-report/video-slice.spec.js +19 -0
  238. package/lib/test/unit/command-log-network-capture.spec.js +191 -0
  239. package/lib/test/unit/dashboard-events-device-logs.spec.js +255 -0
  240. package/lib/test/unit/dashboard-events-team-scope.spec.js +6 -3
  241. package/lib/test/unit/dashboard-plugin-link.spec.js +213 -0
  242. package/lib/test/unit/database-check.spec.js +1 -0
  243. package/lib/test/unit/device-log-book.spec.js +180 -0
  244. package/lib/test/unit/device-utils.spec.js +13 -4
  245. package/lib/test/unit/enable-dashboard-option.spec.js +57 -0
  246. package/lib/test/unit/event-log-session-data.spec.js +251 -0
  247. package/lib/test/unit/failure-analysis.spec.js +196 -0
  248. package/lib/test/unit/failure-categories.spec.js +130 -0
  249. package/lib/test/unit/failure-categorization.spec.js +393 -0
  250. package/lib/test/unit/fingerprint-identity.spec.js +433 -0
  251. package/lib/test/unit/h264-multiplexer.spec.js +46 -0
  252. package/lib/test/unit/heal-report.spec.js +71 -0
  253. package/lib/test/unit/healing-instrumentation.spec.js +6 -1
  254. package/lib/test/unit/healing-tier-gate.spec.js +74 -29
  255. package/lib/test/unit/healing-tiers-every-session.spec.js +219 -0
  256. package/lib/test/unit/hub-node-gateway-appium-server.spec.js +243 -1
  257. package/lib/test/unit/interceptor-visual-elements.spec.js +192 -0
  258. package/lib/test/unit/ios-driver-log.spec.js +168 -0
  259. package/lib/test/unit/ios-simulator-session-readiness.spec.js +187 -0
  260. package/lib/test/unit/ios-simulator-session-teardown.spec.js +158 -0
  261. package/lib/test/unit/lease/lease-override.spec.js +23 -6
  262. package/lib/test/unit/lease/session-lease-proof.spec.js +16 -4
  263. package/lib/test/unit/logcat-ws.spec.js +78 -37
  264. package/lib/test/unit/logger-json-levels.spec.js +44 -0
  265. package/lib/test/unit/mcp-token-ttl.spec.js +38 -0
  266. package/lib/test/unit/observability-example-loki.spec.js +25 -0
  267. package/lib/test/unit/ocr-healing-tier-real-ocr.spec.js +113 -0
  268. package/lib/test/unit/ocr-healing-tier.spec.js +122 -0
  269. package/lib/test/unit/ocr-offline.spec.js +213 -0
  270. package/lib/test/unit/omni-virtual-element-store.spec.js +301 -0
  271. package/lib/test/unit/omni-vision-ios-points.spec.js +0 -2
  272. package/lib/test/unit/omni-vision.spec.js +5 -7
  273. package/lib/test/unit/openapi-for-site.spec.js +80 -0
  274. package/lib/test/unit/outbound-proxy.spec.js +320 -0
  275. package/lib/test/unit/package-engines.spec.js +27 -0
  276. package/lib/test/unit/passwordResetMode.test.js +10 -2
  277. package/lib/test/unit/port-exhaustion.spec.js +2 -1
  278. package/lib/test/unit/profile-router.test.js +122 -1
  279. package/lib/test/unit/proxy-option.spec.js +358 -0
  280. package/lib/test/unit/queue-requester.spec.js +1 -1
  281. package/lib/test/unit/reset-link-address.spec.js +266 -0
  282. package/lib/test/unit/reset-link.test.js +10 -4
  283. package/lib/test/unit/resilio-path-healing.spec.js +270 -0
  284. package/lib/test/unit/run-migrations-database.spec.js +807 -0
  285. package/lib/test/unit/run-migrations-fail-fast.test.js +41 -27
  286. package/lib/test/unit/run-migrations.test.js +864 -0
  287. package/lib/test/unit/selector-health-actions.spec.js +3 -1
  288. package/lib/test/unit/selector-learning-every-session.spec.js +179 -0
  289. package/lib/test/unit/selector-state-service.spec.js +72 -23
  290. package/lib/test/unit/selector-verification-clean-build.spec.js +130 -0
  291. package/lib/test/unit/selector-verification-job.spec.js +28 -19
  292. package/lib/test/unit/selector-verification-missed-regression.spec.js +2 -2
  293. package/lib/test/unit/selector-visibility-resolver.spec.js +209 -0
  294. package/lib/test/unit/self-healing-boot.spec.js +10 -5
  295. package/lib/test/unit/self-healing-setting.spec.js +7 -6
  296. package/lib/test/unit/session-app-resolution.spec.js +5 -1
  297. package/lib/test/unit/session-attribution-wiring.spec.js +41 -13
  298. package/lib/test/unit/session-create-credential-owner.spec.js +242 -0
  299. package/lib/test/unit/session-credential-strip.spec.js +1 -1
  300. package/lib/test/unit/session-device-logs-ios.spec.js +180 -0
  301. package/lib/test/unit/session-device-logs.spec.js +356 -0
  302. package/lib/test/unit/session-end-failure-analysis.spec.js +133 -0
  303. package/lib/test/unit/session-log-indexes.spec.js +64 -3
  304. package/lib/test/unit/socket-team-scope.integration.spec.js +749 -0
  305. package/lib/test/unit/socketServer.scopedEmit.spec.js +237 -5
  306. package/lib/test/unit/socketServer.sessionCookie.spec.js +2 -1
  307. package/lib/test/unit/stream-start-pipelines.spec.js +72 -18
  308. package/lib/test/unit/token-mint-limits.spec.js +165 -0
  309. package/lib/test/unit/tracing-export-routing.spec.js +137 -0
  310. package/lib/test/unit/tracing-session-spans.spec.js +337 -0
  311. package/lib/test/unit/unexpected-shutdown-reason.spec.js +163 -0
  312. package/lib/test/unit/verify-credential.spec.js +33 -0
  313. package/lib/test/unit/vision-assertion.spec.js +3 -2
  314. package/lib/test/unit/web-config-service.spec.js +19 -0
  315. package/lib/tsconfig.tsbuildinfo +1 -1
  316. package/package.json +6 -5
  317. package/prisma/migrations/20261004174413_locator_etalon_path/migration.sql +2 -0
  318. package/prisma/schema.prisma +3 -0
  319. package/schema.json +16 -12
  320. package/scripts/dev/readme-screenshots.js +276 -23
  321. package/scripts/dev/screenshot-fixtures/logcat.js +157 -0
  322. package/scripts/dev/screenshot-fixtures/phone-screen.html +469 -0
  323. package/scripts/dev/screenshot-fixtures/session.js +242 -0
  324. package/scripts/export-openapi.js +32 -0
  325. package/scripts/lib/openapi-for-site.js +79 -0
  326. package/lib/public/assets/BugReportButton-BAt0_pzq.js +0 -15
  327. package/lib/public/assets/DeviceMosaicView-BYaQHhTf.js +0 -2
  328. package/lib/public/assets/ai-settings-CsuJroRf.js +0 -21
  329. package/lib/public/assets/device-explorer-CdO6_nrn.js +0 -282
  330. package/lib/public/assets/forgot-password-CGnkRgE9.js +0 -1
  331. package/lib/public/assets/index-BbKEXUFH.js +0 -219
  332. package/lib/public/assets/runbook-page-CMMeQml9.js +0 -124
  333. package/lib/public/assets/session-detail-page-COL_RZxw.js +0 -4
  334. package/lib/src/scripts/prepare-prisma.js +0 -89
  335. /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 (go-ios) and iOS simulators by itself.
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 (what Appium 3 needs) |
84
- | **Appium** | 3.x (`npm i -g appium`) |
85
- | **Android** | Android SDK platform tools (`adb`) and the UiAutomator2 driver |
86
- | **iOS** | macOS with Xcode, [go-ios](https://github.com/danielpaulus/go-ios) and the XCUITest driver |
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
- | **Optional** | `ffmpeg` for recordings; an AI provider key (Gemini, OpenAI, Anthropic or a local Ollama) for the AI healing tiers |
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 and its dashboard.**
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
- [Node provisioning](docs/node-provisioning.md) covers creating the node's user, its token, and recovering a lost one.
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 [Server arguments](docs/server-args.md), and [Data retention](docs/retention.md) 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 or cleanup value saved there replaces the option the server was started with, and applies without a restart.
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 hub's first start. |
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) applies database migrations at startup. Set `false` if your pipeline applies them. |
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 screenshots when a command fails, or after every command. |
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 whose dashboard is off; they never fail the test. A `xenon:` command Xenon doesn't have fails with `unknown command`.
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 the [API reference](#api).
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** | Fingerprints of the element stored from earlier successful runs |
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=false`, 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`.
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.md) explains setting them up.
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 emits OpenTelemetry traces and logs for every session and command. [`examples/observability`](examples/observability) has a ready Docker Compose stack (Grafana, Tempo and Loki) to view them.
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 dev # migrate the database, build, install the plugin and start Appium
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
- Migrations are applied when Xenon starts. If you set `XENON_AUTO_MIGRATE=false`, apply them yourself first; from a source checkout, that is `npm run db:migrate`.
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.14.0",
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": "^14.17.0 || ^16.13.0 || >=18.0.0",
211
- "npm": ">=8"
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-BbKEXUFH.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
+ 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-BbKEXUFH.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};
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-BbKEXUFH.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
+ 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-BbKEXUFH.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
+ 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-BbKEXUFH.js";import{P as b,M as C,a as g}from"./Menu-JGGYrSQQ.js";import{P as k}from"./plus-nBFSMu_X.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};
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};