@xenon-device-management/xenon 2.13.2 → 2.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (350) hide show
  1. package/README.md +17 -8
  2. package/lib/package.json +5 -5
  3. package/lib/public/assets/{AnnotationOverlay-BXruClDk.js → AnnotationOverlay-Btq3dZRI.js} +1 -1
  4. package/lib/public/assets/{ApiKeyGate-qTS4LPLC.js → ApiKeyGate-C9OoAgg_.js} +1 -1
  5. package/lib/public/assets/DeviceMosaicView-DBC_xczB.js +2 -0
  6. package/lib/public/assets/{EmptyState-Cr17PdDv.js → EmptyState-DqWU2-vM.js} +1 -1
  7. package/lib/public/assets/{FieldGroup-Bvn08svv.js → FieldGroup-CveQ3xrA.js} +1 -1
  8. package/lib/public/assets/{FilterMenu-DDSO8aDf.js → FilterMenu-BfKDSfHz.js} +1 -1
  9. package/lib/public/assets/{Menu-B-Qn2X3T.js → Menu-CtdCybzA.js} +1 -1
  10. package/lib/public/assets/{Modal-Cg9V5JYU.js → Modal-Bsvj1tm2.js} +1 -1
  11. package/lib/public/assets/{RecordingPage-BTUs9VTW.js → RecordingPage-CRofOiyx.js} +1 -1
  12. package/lib/public/assets/{RecordingsPage-DYcfxITK.js → RecordingsPage-jNQHb2IR.js} +2 -2
  13. package/lib/public/assets/{SegmentedControl-Zy7suhT5.js → SegmentedControl-C8HtTha-.js} +1 -1
  14. package/lib/public/assets/{SettingCard-CWYmycoe.js → SettingCard-D8K1LZNo.js} +1 -1
  15. package/lib/public/assets/{Table-io9fwa98.js → Table-zNTX92Pn.js} +1 -1
  16. package/lib/public/assets/{activity-Bdf_uIWl.js → activity-CJ8LwZq9.js} +1 -1
  17. package/lib/public/assets/ai-settings-CgxQZqrc.js +16 -0
  18. package/lib/public/assets/{api-keys-CPqtVpm3.js → api-keys-ECpo-zTo.js} +1 -1
  19. package/lib/public/assets/{apps-BgZan0H9.js → apps-BjGymyf3.js} +2 -2
  20. package/lib/public/assets/{arrow-left-B27ckB31.js → arrow-left-BWfCbeHP.js} +1 -1
  21. package/lib/public/assets/{arrow-right-DOF8ITCz.js → arrow-right-DC7UGf_n.js} +1 -1
  22. package/lib/public/assets/{arrow-up-right-eFG5KTFS.js → arrow-up-right-BOjwybDk.js} +1 -1
  23. package/lib/public/assets/{auth-shell-DlC3_27j.js → auth-shell-CxMTo9aH.js} +2 -2
  24. package/lib/public/assets/{builds-page-BuXpPfKo.js → builds-page-DOCm4HkA.js} +1 -1
  25. package/lib/public/assets/{button-fIW8owMI.js → button-Cg08-EMT.js} +1 -1
  26. package/lib/public/assets/{calendar-WNjmrF-a.js → calendar-CA1ikLJs.js} +1 -1
  27. package/lib/public/assets/{check-BdTh1Sq7.js → check-BMTaH72W.js} +1 -1
  28. package/lib/public/assets/{chevron-right-B4BKlsqk.js → chevron-right-B2QOv9Nx.js} +1 -1
  29. package/lib/public/assets/{circle-check-zJU80_EF.js → circle-check-CYXBoUqT.js} +1 -1
  30. package/lib/public/assets/{circle-x-5Uwquo0h.js → circle-x-DTwnSAnl.js} +1 -1
  31. package/lib/public/assets/{clock-BtUprDsZ.js → clock-C8_HViQP.js} +1 -1
  32. package/lib/public/assets/{copy-DGSbZI3u.js → copy-Be7LNMXV.js} +1 -1
  33. package/lib/public/assets/derive-UnLRp4r3.js +1 -0
  34. package/lib/public/assets/device-explorer-BOqtfGXz.js +282 -0
  35. package/lib/public/assets/{download-WG6l7BgD.js → download-B56lwxWP.js} +1 -1
  36. package/lib/public/assets/forgot-password-CCU6bzLn.js +1 -0
  37. package/lib/public/assets/h264Stream-C-OtmIuk.js +26 -0
  38. package/lib/public/assets/index-B9KQ6LsY.js +15 -0
  39. package/lib/public/assets/{index-DI1fGIyj.js → index-BOxe43p1.js} +5 -5
  40. package/lib/public/assets/{index-HDQVe1gi.js → index-CfxsUslp.js} +1 -1
  41. package/lib/public/assets/{input-BjDRKCea.js → input-CZHFjbSg.js} +1 -1
  42. package/lib/public/assets/{line-chart-C0zdgd0v.js → line-chart-Gl_Dz3X_.js} +1 -1
  43. package/lib/public/assets/{list-checks-BdW5CjM8.js → list-checks-CCgjeyGY.js} +1 -1
  44. package/lib/public/assets/{lock-Dy3L4I8E.js → lock-CkOwgDcx.js} +1 -1
  45. package/lib/public/assets/{login-BHbY9CLG.js → login-D5cc4w7M.js} +1 -1
  46. package/lib/public/assets/maintenance-settings-t7qr3fvX.js +6 -0
  47. package/lib/public/assets/{monitor-8xjkeHHi.js → monitor-DEJixKQO.js} +1 -1
  48. package/lib/public/assets/{mouse-pointer-2-CXdKMXQ8.js → mouse-pointer-2-CtOrk4M8.js} +1 -1
  49. package/lib/public/assets/{network-Cj8JOKON.js → network-Dii9qdvk.js} +1 -1
  50. package/lib/public/assets/{overview-CTg7F0im.js → overview-rolAACrO.js} +1 -1
  51. package/lib/public/assets/{page-header--aTyJXq6.js → page-header-COQZxJfR.js} +1 -1
  52. package/lib/public/assets/{play-B-FodgHV.js → play-DrN1Se_R.js} +1 -1
  53. package/lib/public/assets/{plus-CsIk10Cq.js → plus-5aq-9uRx.js} +1 -1
  54. package/lib/public/assets/{profile-page-DkFKkMfl.js → profile-page-6i5kY6kF.js} +1 -1
  55. package/lib/public/assets/{reset-password-I2-zqLlI.js → reset-password-sp2IBrc7.js} +1 -1
  56. package/lib/public/assets/runbook-page-DJDjeopU.js +253 -0
  57. package/lib/public/assets/{select-DvCSv25z.js → select-Bpyn7rWO.js} +1 -1
  58. package/lib/public/assets/{selector-detail-redirect-BLVfMJs_.js → selector-detail-redirect-BH74vmKv.js} +1 -1
  59. package/lib/public/assets/{selector-health-page-oqadRokn.js → selector-health-page-Cw_g218H.js} +2 -2
  60. package/lib/public/assets/session-detail-page-AZJE6XXE.js +4 -0
  61. package/lib/public/assets/settings-C3uyGhyc.js +6 -0
  62. package/lib/public/assets/{settings-Dep_3kbA.css → settings-K8UHWMdI.css} +1 -1
  63. package/lib/public/assets/{stat-tile-DV9Ix-4O.js → stat-tile-xVqAKIlI.js} +1 -1
  64. package/lib/public/assets/{tablet-6b_SEN2X.js → tablet-BxUScAeO.js} +1 -1
  65. package/lib/public/assets/{teams-BjZyfuHh.js → teams-B5ljGkY2.js} +1 -1
  66. package/lib/public/assets/{trash-2-CTZpPSdd.js → trash-2-B701i8sy.js} +1 -1
  67. package/lib/public/assets/{upload-P3XzwQUb.js → upload-C2gr44RS.js} +1 -1
  68. package/lib/public/assets/{use-builds-data-1cOCMf-z.js → use-builds-data-NeQOFEoy.js} +1 -1
  69. package/lib/public/assets/{use-password-reset-mode-DgF9w088.js → use-password-reset-mode-DEzJq5ef.js} +1 -1
  70. package/lib/public/assets/{users-DMF9Mk5H.js → users-D_vv_yz4.js} +1 -1
  71. package/lib/public/assets/{users-CiNTKUiz.js → users-MY6eNe6s.js} +1 -1
  72. package/lib/public/assets/{video-off-CHp1KU_T.js → video-off-OK1lNZQn.js} +1 -1
  73. package/lib/public/assets/webhook-settings-BU-C9lBf.js +1 -0
  74. package/lib/public/assets/{zap-DRz0tDq5.js → zap-DO9ry1iz.js} +1 -1
  75. package/lib/public/index.html +1 -1
  76. package/lib/schema.json +23 -25
  77. package/lib/src/XenonCapabilityManager.js +22 -5
  78. package/lib/src/app/dashboardPluginLink.js +117 -0
  79. package/lib/src/app/index.js +13 -34
  80. package/lib/src/app/openapi/control.yaml +73 -20
  81. package/lib/src/app/openapi/grid.yaml +31 -7
  82. package/lib/src/app/openapi/identity.yaml +103 -31
  83. package/lib/src/app/openapi/platform.yaml +165 -39
  84. package/lib/src/app/openapi/sessions.yaml +10 -3
  85. package/lib/src/app/routers/apikeys.js +14 -2
  86. package/lib/src/app/routers/auth.js +29 -5
  87. package/lib/src/app/routers/control.js +20 -2
  88. package/lib/src/app/routers/dashboard.js +69 -26
  89. package/lib/src/app/routers/grid.js +37 -13
  90. package/lib/src/app/routers/profile.js +21 -0
  91. package/lib/src/app/routers/streamType.js +7 -2
  92. package/lib/src/app/routers/users.js +10 -5
  93. package/lib/src/app/routers/webhook.js +8 -4
  94. package/lib/src/app/swagger.js +2 -0
  95. package/lib/src/app/ws/h264StreamWs.js +10 -1
  96. package/lib/src/config.js +51 -5
  97. package/lib/src/dashboard/commands.js +185 -141
  98. package/lib/src/dashboard/event-manager.js +88 -67
  99. package/lib/src/dashboard/services/failure-analysis-service.js +136 -45
  100. package/lib/src/dashboard/sessionCommandSummary.js +39 -0
  101. package/lib/src/data-service/appleFamily.js +43 -0
  102. package/lib/src/data-service/db.js +11 -0
  103. package/lib/src/data-service/device-service.js +6 -1
  104. package/lib/src/data-service/device-store.js +76 -3
  105. package/lib/src/data-service/deviceClaims.js +37 -0
  106. package/lib/src/data-service/deviceFieldOwners.js +25 -11
  107. package/lib/src/data-service/deviceSettings.js +79 -0
  108. package/lib/src/data-service/prisma-store.js +169 -15
  109. package/lib/src/data-service/web-config-service.js +50 -92
  110. package/lib/src/device-managers/AndroidDeviceManager.js +9 -31
  111. package/lib/src/device-managers/android/AndroidH264StreamService.js +69 -20
  112. package/lib/src/device-managers/android/H264Multiplexer.js +38 -4
  113. package/lib/src/device-managers/ios/WDAClient.js +44 -43
  114. package/lib/src/device-managers/shellCommands.js +96 -0
  115. package/lib/src/device-utils.js +114 -40
  116. package/lib/src/gateway/defaultGateway.js +1 -1
  117. package/lib/src/gateway/forwardToNode.js +2 -0
  118. package/lib/src/gateway/healReport.js +87 -0
  119. package/lib/src/gateway/sessionGateway.js +12 -2
  120. package/lib/src/generated/client/edge.js +18 -3
  121. package/lib/src/generated/client/index-browser.js +15 -0
  122. package/lib/src/generated/client/index.d.ts +2086 -379
  123. package/lib/src/generated/client/index.js +18 -3
  124. package/lib/src/generated/client/package.json +1 -1
  125. package/lib/src/generated/client/schema.prisma +38 -7
  126. package/lib/src/generated/client/wasm.js +15 -0
  127. package/lib/src/interceptors/CommandInterceptor.js +271 -188
  128. package/lib/src/interceptors/xenonScripts.js +55 -0
  129. package/lib/src/interfaces/IPluginArgs.js +1 -2
  130. package/lib/src/middleware/authMiddleware.js +15 -3
  131. package/lib/src/middleware/commandCaller.js +3 -3
  132. package/lib/src/middleware/verifyCredential.js +58 -1
  133. package/lib/src/plugin.js +23 -4
  134. package/lib/src/prisma.js +1 -1
  135. package/lib/src/scripts/database-check.js +124 -0
  136. package/lib/src/services/AICommandService.js +9 -2
  137. package/lib/src/services/AIService.js +281 -32
  138. package/lib/src/services/CleanupService.js +6 -1
  139. package/lib/src/services/EmailService.js +29 -0
  140. package/lib/src/services/InterceptorService.js +70 -13
  141. package/lib/src/services/NetworkConditioningService.js +85 -88
  142. package/lib/src/services/NotificationService.js +36 -29
  143. package/lib/src/services/OrphanSweeper.js +7 -0
  144. package/lib/src/services/PortAllocator.js +5 -0
  145. package/lib/src/services/SelectorVerificationJob.js +15 -4
  146. package/lib/src/services/ServerManager.js +105 -9
  147. package/lib/src/services/SessionLifecycleService.js +133 -75
  148. package/lib/src/services/ShutdownCoordinator.js +33 -0
  149. package/lib/src/services/SocketServer.js +19 -3
  150. package/lib/src/services/TeamService.js +20 -3
  151. package/lib/src/services/bug-report/video-slice.js +4 -1
  152. package/lib/src/services/device-access/leaseOverride.js +7 -5
  153. package/lib/src/services/healing/FuzzyXmlHealingProvider.js +51 -0
  154. package/lib/src/services/healing/HealEtalonService.js +3 -2
  155. package/lib/src/services/healing/HealingOrchestrator.js +85 -71
  156. package/lib/src/services/healing/OcrHealingProvider.js +41 -64
  157. package/lib/src/services/healing/ResilioTreeHealingProvider.js +41 -50
  158. package/lib/src/services/healing/VisualAiHealingProvider.js +1 -1
  159. package/lib/src/services/healing/fingerprintIdentity.js +126 -0
  160. package/lib/src/services/healing/resilioPath.js +274 -0
  161. package/lib/src/services/interceptor/AndroidProxyAdapter.js +18 -0
  162. package/lib/src/services/interceptor/interceptorOptions.js +44 -0
  163. package/lib/src/services/logcat/SessionDeviceLogs.js +271 -0
  164. package/lib/src/services/logcat/deviceLogBook.js +163 -0
  165. package/lib/src/services/network/AndroidRadios.js +71 -0
  166. package/lib/src/services/network/PhoneNetworkLedger.js +184 -0
  167. package/lib/src/services/network/PhoneNetworkRestore.js +330 -0
  168. package/lib/src/services/network/adbForPhone.js +60 -0
  169. package/lib/src/services/network/phoneNetworkLock.js +20 -0
  170. package/lib/src/services/network/xenonProxy.js +107 -0
  171. package/lib/src/services/ocr/ocrData.js +118 -0
  172. package/lib/src/services/ocr/vendor/LICENSE-tessdata.txt +202 -0
  173. package/lib/src/services/ocr/vendor/eng.traineddata.gz +0 -0
  174. package/lib/src/services/omni-vision/OmniVisionService.js +132 -95
  175. package/lib/src/services/omni-vision/VisionAssertionService.js +28 -25
  176. package/lib/src/services/omni-vision/ocrTextMatch.js +105 -0
  177. package/lib/src/services/omni-vision/screenScale.js +68 -0
  178. package/lib/src/services/passwordResetLink.js +50 -9
  179. package/lib/src/services/passwordResetMode.js +3 -1
  180. package/lib/src/services/publicUrl.js +45 -0
  181. package/lib/src/services/recording/concurrency-gate.js +12 -3
  182. package/lib/src/services/session/sessionIdentity.js +5 -3
  183. package/lib/src/services/session/shutdownReason.js +26 -0
  184. package/lib/src/services/settings/SelfHealingSwitch.js +70 -0
  185. package/lib/src/services/settings/aiEngineSettings.js +219 -0
  186. package/lib/src/services/settings/labSettings.js +149 -0
  187. package/lib/src/services/token/mintedLifetime.js +51 -0
  188. package/lib/src/services/webhookEvents.js +173 -0
  189. package/lib/src/sessions/SessionManager.js +8 -2
  190. package/lib/src/sessions/sessionMemory.js +25 -0
  191. package/lib/test/helpers/fake-adb.js +58 -0
  192. package/lib/test/helpers/fake-ai-provider.js +188 -0
  193. package/lib/test/helpers/fake-device-table.js +63 -24
  194. package/lib/test/helpers/loki-stores.js +33 -0
  195. package/lib/test/helpers/page-sources.js +164 -0
  196. package/lib/test/helpers/scratch-database.js +58 -13
  197. package/lib/test/integration/audit-ingest.spec.js +10 -10
  198. package/lib/test/integration/auth-flow.spec.js +9 -14
  199. package/lib/test/integration/auth-rate-limit.spec.js +6 -3
  200. package/lib/test/integration/bug-report-route.spec.js +22 -2
  201. package/lib/test/integration/forgot-password-rate-limit.spec.js +6 -3
  202. package/lib/test/integration/forgot-password.spec.js +17 -18
  203. package/lib/test/integration/legacy-deprecation-headers.spec.js +5 -2
  204. package/lib/test/integration/migration.spec.js +6 -1
  205. package/lib/test/integration/node-pair-auth-rest.spec.js +5 -9
  206. package/lib/test/integration/ports-router.spec.js +4 -4
  207. package/lib/test/integration/prisma-device-store-add-devices.spec.js +2 -3
  208. package/lib/test/integration/prisma-device-store-team-filter.spec.js +2 -5
  209. package/lib/test/integration/profile-tokens.spec.js +10 -16
  210. package/lib/test/integration/recordings-router.spec.js +16 -16
  211. package/lib/test/integration/reset-revokes-sessions.spec.js +4 -9
  212. package/lib/test/integration/role-matrix.spec.js +4 -9
  213. package/lib/test/integration/sdk-leases-router.spec.js +9 -9
  214. package/lib/test/integration/sdk-version.spec.js +2 -2
  215. package/lib/test/integration/selector-health-endpoint.spec.js +5 -5
  216. package/lib/test/integration/team-membership-migration.spec.js +73 -12
  217. package/lib/test/integration/team-visibility-apps.spec.js +15 -22
  218. package/lib/test/integration/team-visibility-control.spec.js +96 -69
  219. package/lib/test/integration/team-visibility-dashboard.spec.js +7 -23
  220. package/lib/test/integration/team-visibility-grid.spec.js +16 -15
  221. package/lib/test/integration/team-visibility-leases.spec.js +8 -11
  222. package/lib/test/integration/team-visibility-recordings.spec.js +6 -19
  223. package/lib/test/integration/team-visibility-sessions.spec.js +6 -19
  224. package/lib/test/integration/users-flow.spec.js +7 -12
  225. package/lib/test/integration/users-lockout.spec.js +5 -3
  226. package/lib/test/unit/EmailService.test.js +66 -0
  227. package/lib/test/unit/ResilioTreeHealingProvider.test.js +46 -19
  228. package/lib/test/unit/TeamService.test.js +3 -0
  229. package/lib/test/unit/XenonCapabilityManager.spec.js +4 -2
  230. package/lib/test/unit/ai-call-time-limit.spec.js +130 -0
  231. package/lib/test/unit/ai-engine-settings.spec.js +338 -0
  232. package/lib/test/unit/ai-provider-switch.spec.js +130 -0
  233. package/lib/test/unit/ai-rate-limit.spec.js +126 -0
  234. package/lib/test/unit/android-h264-viewers.spec.js +272 -0
  235. package/lib/test/unit/android-preview-hold.spec.js +3 -2
  236. package/lib/test/unit/apikeys-router.spec.js +84 -0
  237. package/lib/test/unit/apple-family.spec.js +36 -0
  238. package/lib/test/unit/authMiddleware.bearer.spec.js +9 -0
  239. package/lib/test/unit/authMiddleware.credentialLifetime.spec.js +149 -0
  240. package/lib/test/unit/bug-report/video-slice.spec.js +19 -0
  241. package/lib/test/unit/cleanup-settings-db.spec.js +103 -0
  242. package/lib/test/unit/cleanup-settings.spec.js +244 -0
  243. package/lib/test/unit/dashboard-api-paths.spec.js +101 -0
  244. package/lib/test/unit/dashboard-commands.spec.js +249 -0
  245. package/lib/test/unit/dashboard-events-device-logs.spec.js +207 -0
  246. package/lib/test/unit/dashboard-events-team-scope.spec.js +9 -6
  247. package/lib/test/unit/dashboard-plugin-link.spec.js +213 -0
  248. package/lib/test/unit/database-check.spec.js +108 -0
  249. package/lib/test/unit/default-plugin-args.spec.js +33 -0
  250. package/lib/test/unit/device-log-book.spec.js +180 -0
  251. package/lib/test/unit/device-offline-webhook.spec.js +81 -0
  252. package/lib/test/unit/device-setting-columns.spec.js +66 -0
  253. package/lib/test/unit/device-settings-loki.spec.js +87 -0
  254. package/lib/test/unit/device-settings-survive.spec.js +555 -0
  255. package/lib/test/unit/device-sync-lost-update.spec.js +5 -0
  256. package/lib/test/unit/emulator-boot.spec.js +91 -0
  257. package/lib/test/unit/enable-dashboard-option.spec.js +57 -0
  258. package/lib/test/unit/event-log-session-data.spec.js +244 -0
  259. package/lib/test/unit/failure-analysis.spec.js +182 -0
  260. package/lib/test/unit/failure-categories.spec.js +128 -0
  261. package/lib/test/unit/fingerprint-identity.spec.js +433 -0
  262. package/lib/test/unit/h264-multiplexer.spec.js +46 -0
  263. package/lib/test/unit/heal-report.spec.js +71 -0
  264. package/lib/test/unit/healing-instrumentation.spec.js +6 -1
  265. package/lib/test/unit/healing-tier-gate.spec.js +73 -29
  266. package/lib/test/unit/healing-tiers-every-session.spec.js +219 -0
  267. package/lib/test/unit/hub-node-gateway-appium-server.spec.js +219 -0
  268. package/lib/test/unit/hub-node-same-host.spec.js +5 -0
  269. package/lib/test/unit/interceptor-visual-elements.spec.js +192 -0
  270. package/lib/test/unit/iphone-ipad-only.spec.js +132 -0
  271. package/lib/test/unit/json-logging-option.spec.js +74 -0
  272. package/lib/test/unit/lab-settings.spec.js +218 -0
  273. package/lib/test/unit/lease/lease-override.spec.js +23 -6
  274. package/lib/test/unit/lease/session-lease-proof.spec.js +16 -4
  275. package/lib/test/unit/max-sessions.spec.js +240 -0
  276. package/lib/test/unit/ocr-healing-tier-real-ocr.spec.js +113 -0
  277. package/lib/test/unit/ocr-healing-tier.spec.js +122 -0
  278. package/lib/test/unit/ocr-offline.spec.js +213 -0
  279. package/lib/test/unit/ocr-text-match.spec.js +44 -0
  280. package/lib/test/unit/omni-virtual-element-store.spec.js +301 -0
  281. package/lib/test/unit/omni-vision-analyze-screen.spec.js +72 -0
  282. package/lib/test/unit/omni-vision-ios-points.spec.js +159 -0
  283. package/lib/test/unit/omni-vision-tap-text.spec.js +129 -0
  284. package/lib/test/unit/omni-vision.spec.js +6 -8
  285. package/lib/test/unit/package-engines.spec.js +27 -0
  286. package/lib/test/unit/partial-config-file.spec.js +14 -0
  287. package/lib/test/unit/passwordResetMode.test.js +10 -2
  288. package/lib/test/unit/phone-network/boot-clean-up.spec.js +64 -0
  289. package/lib/test/unit/phone-network/interceptor-options.spec.js +77 -0
  290. package/lib/test/unit/phone-network/interceptor-proxy-restore.spec.js +145 -0
  291. package/lib/test/unit/phone-network/network-conditioning.spec.js +145 -0
  292. package/lib/test/unit/phone-network/phone-network-restore.spec.js +246 -0
  293. package/lib/test/unit/phone-network/session-end-paths.spec.js +182 -0
  294. package/lib/test/unit/phone-network/session-start.spec.js +110 -0
  295. package/lib/test/unit/phone-network/xenon-proxy.spec.js +69 -0
  296. package/lib/test/unit/plugin.spec.js +3 -3
  297. package/lib/test/unit/profile-router.test.js +122 -1
  298. package/lib/test/unit/queue-requester.spec.js +1 -1
  299. package/lib/test/unit/recording-options.spec.js +181 -0
  300. package/lib/test/unit/reset-link-address.spec.js +266 -0
  301. package/lib/test/unit/reset-link.test.js +10 -4
  302. package/lib/test/unit/resilio-path-healing.spec.js +270 -0
  303. package/lib/test/unit/selector-learning-every-session.spec.js +179 -0
  304. package/lib/test/unit/selector-verification-clean-build.spec.js +130 -0
  305. package/lib/test/unit/selector-verification-job.spec.js +13 -13
  306. package/lib/test/unit/self-healing-boot.spec.js +80 -0
  307. package/lib/test/unit/self-healing-setting.spec.js +273 -0
  308. package/lib/test/unit/session-app-resolution.spec.js +5 -1
  309. package/lib/test/unit/session-attribution-wiring.spec.js +41 -13
  310. package/lib/test/unit/session-create-credential-owner.spec.js +242 -0
  311. package/lib/test/unit/session-credential-strip.spec.js +1 -1
  312. package/lib/test/unit/session-device-logs.spec.js +347 -0
  313. package/lib/test/unit/session-end-failure-analysis.spec.js +133 -0
  314. package/lib/test/unit/session-failed-webhook.spec.js +170 -0
  315. package/lib/test/unit/session-log-indexes.spec.js +64 -3
  316. package/lib/test/unit/shell-commands.spec.js +133 -0
  317. package/lib/test/unit/socketServer.scopedEmit.spec.js +2 -0
  318. package/lib/test/unit/socketServer.sessionCookie.spec.js +2 -1
  319. package/lib/test/unit/stream-start-pipelines.spec.js +264 -0
  320. package/lib/test/unit/stream-status-type.spec.js +4 -0
  321. package/lib/test/unit/token-mint-limits.spec.js +165 -0
  322. package/lib/test/unit/unexpected-shutdown-reason.spec.js +163 -0
  323. package/lib/test/unit/verify-credential.spec.js +33 -0
  324. package/lib/test/unit/vision-assertion.spec.js +119 -0
  325. package/lib/test/unit/wda-client-shell.spec.js +133 -0
  326. package/lib/test/unit/web-config-service.spec.js +98 -0
  327. package/lib/test/unit/webhook-delivery.spec.js +24 -0
  328. package/lib/test/unit/webhook-payloads.spec.js +273 -0
  329. package/lib/test/unit/xenon-execute-scripts.spec.js +138 -0
  330. package/lib/tsconfig.tsbuildinfo +1 -1
  331. package/lib/web/src/components/webhook-settings/webhookEventVariables.json +30 -0
  332. package/package.json +5 -5
  333. package/prisma/migrations/20261004140000_keep_device_settings/migration.sql +19 -0
  334. package/prisma/migrations/20261004174413_locator_etalon_path/migration.sql +2 -0
  335. package/prisma/schema.prisma +38 -7
  336. package/schema.json +23 -25
  337. package/scripts/generate-types-from-schema.js +1 -2
  338. package/scripts/lib/always-present-plugin-args.js +4 -1
  339. package/lib/public/assets/BugReportButton-BoGhlI7F.js +0 -15
  340. package/lib/public/assets/DeviceMosaicView-8J0oYqMM.js +0 -2
  341. package/lib/public/assets/ai-settings-Bce5SJob.js +0 -21
  342. package/lib/public/assets/device-explorer-C-KnhMHI.js +0 -279
  343. package/lib/public/assets/forgot-password-DnudqFFo.js +0 -1
  344. package/lib/public/assets/maintenance-settings-zDVlOMVK.js +0 -6
  345. package/lib/public/assets/recording-group-store-WVM3hoXn.js +0 -26
  346. package/lib/public/assets/runbook-page-DJTVCJX7.js +0 -124
  347. package/lib/public/assets/session-detail-page-CGn55Pvl.js +0 -4
  348. package/lib/public/assets/settings-MsVU2ukE.js +0 -6
  349. package/lib/public/assets/webhook-settings-DiS8geIm.js +0 -1
  350. /package/lib/public/assets/{BugReportButton-CPBjfiv4.css → index-CPBjfiv4.css} +0 -0
@@ -67,8 +67,6 @@ const appium_ios_device_1 = require("appium-ios-device");
67
67
  const config_1 = require("../config");
68
68
  const helpers_2 = require("../helpers");
69
69
  const typedi_1 = require("typedi");
70
- const device_managers_1 = require("../device-managers");
71
- const AndroidDeviceManager_1 = __importDefault(require("../device-managers/AndroidDeviceManager"));
72
70
  const IOSStreamService_1 = __importDefault(require("../device-managers/ios/IOSStreamService"));
73
71
  const AndroidStreamService_1 = __importDefault(require("../device-managers/android/AndroidStreamService"));
74
72
  const SocketServer_1 = require("../services/SocketServer");
@@ -78,8 +76,11 @@ const SocketEvents_1 = require("../enums/SocketEvents");
78
76
  const types_1 = require("../services/healing/types");
79
77
  const SelectorStateService_1 = require("../services/SelectorStateService");
80
78
  const recording_store_1 = require("../services/recording/recording-store");
79
+ const NotificationService_1 = require("../services/NotificationService");
81
80
  const SessionMetricsService_1 = require("../services/metrics/SessionMetricsService");
81
+ const SessionDeviceLogs_1 = require("../services/logcat/SessionDeviceLogs");
82
82
  const nodeMetrics_1 = require("../services/metrics/nodeMetrics");
83
+ const sessionCommandSummary_1 = require("./sessionCommandSummary");
83
84
  const typedi_2 = require("typedi");
84
85
  /**
85
86
  * What a Session row keeps of the capabilities the driver returned. The row
@@ -105,7 +106,7 @@ let DashboardEventManager = class DashboardEventManager {
105
106
  this.sessionToUdid = new Map();
106
107
  // Map session ID to device info
107
108
  this.sessionToDevice = new Map();
108
- // Track last log line for each session (for device logs)
109
+ // Track last log line for each session (an iPhone's device logs)
109
110
  this.lastLogLine = new Map();
110
111
  // Track start time for each command to calculate duration
111
112
  this.commandStartTime = new Map();
@@ -157,6 +158,14 @@ let DashboardEventManager = class DashboardEventManager {
157
158
  if (sampled) {
158
159
  metrics.start(Object.assign({ sessionId: session.getId(), device, capabilities: session.getCapabilities() }, (source ? { source } : {})));
159
160
  }
161
+ // An Android phone's device log, from about when the phone was given to
162
+ // the session, once its row exists (the lines point at it). Not waited
163
+ // for: the phone's clock and log stream are read in the background.
164
+ void typedi_1.Container.get(SessionDeviceLogs_1.SessionDeviceLogs).start({
165
+ sessionId: session.getId(),
166
+ device,
167
+ since: session.allocatedAt,
168
+ });
160
169
  // Emit session started event
161
170
  void typedi_1.Container.get(SocketServer_1.SocketServer).emitToDashboardForDevices(SocketEvents_1.SocketEvents.SESSION_STARTED, Object.assign(Object.assign({}, createData), { status: 'running', build_name: buildName }), { udid: device.udid });
162
171
  // Increment Metrics
@@ -190,8 +199,16 @@ let DashboardEventManager = class DashboardEventManager {
190
199
  }
191
200
  });
192
201
  }
193
- onSessionStopped(sessionId, status, failureReason) {
194
- return __awaiter(this, void 0, void 0, function* () {
202
+ /**
203
+ * Closes a session's record: its end time, its final status, the device's
204
+ * release. Every way a session ends comes here (the client's delete, an
205
+ * inactivity timeout, a driver crash, a heartbeat timeout, a shutdown), so
206
+ * this is where a failed one is announced to the `session_failed` webhooks.
207
+ * `notify: false` records the failure without announcing it: a server
208
+ * shutting down fails the sessions it drains, which no test caused.
209
+ */
210
+ onSessionStopped(sessionId_1, status_1, failureReason_1) {
211
+ return __awaiter(this, arguments, void 0, function* (sessionId, status, failureReason, options = {}) {
195
212
  var _a, _b, _c, _d;
196
213
  // Idempotency Guard: If this session is already being stopped by another actor
197
214
  // (heartbeat, stream watchdog, plugin.deleteSession), skip to avoid double-cleanup.
@@ -202,8 +219,10 @@ let DashboardEventManager = class DashboardEventManager {
202
219
  this.stoppingSessionIds.add(sessionId);
203
220
  try {
204
221
  logger_1.default.info(`🟢 onSessionStopped called for session ${sessionId}`);
205
- // However the session ended, in memory or not, its sampler stops here.
222
+ // However the session ended, in memory or not, its sampler stops here,
223
+ // and its device log is written to the end.
206
224
  yield typedi_1.Container.get(SessionMetricsService_1.SessionMetricsService).stop(sessionId);
225
+ yield typedi_1.Container.get(SessionDeviceLogs_1.SessionDeviceLogs).stop(sessionId);
207
226
  // Video recording is now handled in plugin.ts deleteSession() before the session is deleted
208
227
  // This ensures we can call stop_recording_screen while the session is still active
209
228
  // Here we just handle the session status update
@@ -341,11 +360,21 @@ let DashboardEventManager = class DashboardEventManager {
341
360
  else if (updateData.status === SessionStatus_1.SessionStatus.FAILED) {
342
361
  typedi_1.Container.get(MetricsService_1.MetricsService).incrementSessionFailure();
343
362
  }
344
- // Principal Triage: If session failed, perform intelligent failure analysis
363
+ // Webhooks: not awaited, since a slow or dead webhook must not hold up
364
+ // the end of the session. The service sends once per session even
365
+ // though a session can end twice (a crash, then the client's delete).
366
+ if (updateData.status === SessionStatus_1.SessionStatus.FAILED && options.notify !== false) {
367
+ this.announceFailure(Object.assign(Object.assign({}, sessionEntry), updateData));
368
+ }
369
+ // Principal Triage: If session failed, perform intelligent failure analysis.
370
+ // The category now; the AI's analysis is not awaited, since every way
371
+ // a session ends waits for this method (the client's quit, a hub's
372
+ // DELETE) and an AI call can take minutes. It is saved when it comes.
345
373
  if (updateData.status === SessionStatus_1.SessionStatus.FAILED) {
346
374
  try {
347
- const { analyzeSessionFailure } = yield Promise.resolve().then(() => __importStar(require('./services/failure-analysis-service')));
348
- yield analyzeSessionFailure(sessionId);
375
+ const { categorizeSessionFailure, explainSessionFailure } = yield Promise.resolve().then(() => __importStar(require('./services/failure-analysis-service')));
376
+ yield categorizeSessionFailure(sessionId);
377
+ void explainSessionFailure(sessionId);
349
378
  }
350
379
  catch (analysisErr) {
351
380
  logger_1.default.warn(`⚠️ Failure analysis skipped for ${sessionId}: ${analysisErr.message}`);
@@ -363,6 +392,18 @@ let DashboardEventManager = class DashboardEventManager {
363
392
  }
364
393
  });
365
394
  }
395
+ /** Starts the `session_failed` webhooks; never throws and never waits for them. */
396
+ announceFailure(session) {
397
+ var _a;
398
+ try {
399
+ void typedi_1.Container.get(NotificationService_1.NotificationService)
400
+ .notifySessionFailed(session)
401
+ .catch((err) => { var _a; return logger_1.default.warn(`session_failed webhook for ${session.id} not sent: ${(_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : err}`); });
402
+ }
403
+ catch (err) {
404
+ logger_1.default.warn(`session_failed webhook for ${session.id} not sent: ${(_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : err}`);
405
+ }
406
+ }
366
407
  beforeSessionCommand(sessionId, commandName, request, response) {
367
408
  return __awaiter(this, void 0, void 0, function* () {
368
409
  var _a;
@@ -370,16 +411,22 @@ let DashboardEventManager = class DashboardEventManager {
370
411
  this.commandStartTime.set(`${sessionId}:${commandName}`, Date.now());
371
412
  logger_1.default.debug(`[EventManager] beforeSessionCommand: sessionId=${sessionId}, commandName=${commandName}`);
372
413
  }
373
- // Principal Interception: Handle Xenon-specific commands regardless of session state in memory
414
+ // The session-details commands (`xenon: setSessionName`, ...) are answered
415
+ // here, from this server's record of the session, whether or not the
416
+ // session is in memory. Every other `xenon:` script goes on: to the
417
+ // plugin's CommandInterceptor on the server that drives the phone, which
418
+ // a hub reaches by forwarding it. Taking them all here answered a node
419
+ // phone's autowait, Omni-Vision and network-capture scripts with null on
420
+ // the hub, and they never reached the node.
374
421
  if (commandName === 'execute') {
375
422
  const script = ((_a = request.body) === null || _a === void 0 ? void 0 : _a.script) || (Array.isArray(request.body) ? request.body[0] : undefined);
376
- if (script && commands_1.dashboardCommands.isDashboardCommand(script)) {
423
+ if ((0, commands_1.sessionDetailsCommandOf)(script)) {
377
424
  logger_1.default.info(`[EventManager] Intercepting Xenon command: ${script} for session ${sessionId}`);
378
425
  yield commands_1.dashboardCommands.process(sessionId, request, response);
379
426
  return false;
380
427
  }
381
- else if (script && script.includes(':')) {
382
- logger_1.default.debug(`[EventManager] Custom command ${script} not handled by Xenon. Passing to driver.`);
428
+ else if (typeof script === 'string' && script.includes(':')) {
429
+ logger_1.default.debug(`[EventManager] Script ${script} is not a session-details command; passing it on.`);
383
430
  }
384
431
  }
385
432
  const session = SessionManager_1.SESSION_MANAGER.getSession(sessionId);
@@ -396,7 +443,7 @@ let DashboardEventManager = class DashboardEventManager {
396
443
  }
397
444
  afterSessionCommand(sessionId, commandName, driver, request, response, responseBody, healingInfo) {
398
445
  return __awaiter(this, void 0, void 0, function* () {
399
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s;
446
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q;
400
447
  const session = SessionManager_1.SESSION_MANAGER.getSession(sessionId);
401
448
  if (session) {
402
449
  try {
@@ -437,15 +484,20 @@ let DashboardEventManager = class DashboardEventManager {
437
484
  };
438
485
  // Smart Passive: capture strategy + selector on every findElement,
439
486
  // not just heals. CommandInterceptor synthesizes request.body = args
440
- // (the array [strategy, value]). This gives the verification job
441
- // exact evidence of "selector ran and didn't heal" per build.
442
- if ((commandName === 'findElement' || commandName === 'findElements') &&
443
- Array.isArray(request.body)) {
444
- if (logEntry.original_strategy === null) {
445
- logEntry.original_strategy = (_g = request.body[0]) !== null && _g !== void 0 ? _g : null;
487
+ // (the array [strategy, value]); a command a hub forwards to a node
488
+ // has the client's own W3C body ({ using, value }). This gives the
489
+ // verification job exact evidence of "selector ran and didn't heal"
490
+ // per build.
491
+ if (commandName === 'findElement' || commandName === 'findElements') {
492
+ const body = request.body;
493
+ const [strategy, selector] = Array.isArray(body)
494
+ ? [body[0], body[1]]
495
+ : [body === null || body === void 0 ? void 0 : body.using, body === null || body === void 0 ? void 0 : body.value];
496
+ if (logEntry.original_strategy === null && typeof strategy === 'string') {
497
+ logEntry.original_strategy = strategy;
446
498
  }
447
- if (logEntry.original_selector === null) {
448
- logEntry.original_selector = (_h = request.body[1]) !== null && _h !== void 0 ? _h : null;
499
+ if (logEntry.original_selector === null && typeof selector === 'string') {
500
+ logEntry.original_selector = selector;
449
501
  }
450
502
  }
451
503
  // Increment Healing Metrics
@@ -497,8 +549,10 @@ let DashboardEventManager = class DashboardEventManager {
497
549
  });
498
550
  // Emit command log event to dashboard. One per command: the phone's
499
551
  // team comes from DeviceTeamResolver's cache, not a query each time.
552
+ // Its summary only: what the command typed and answered stays in the
553
+ // SessionLog row above, which goes with the session.
500
554
  const device = session.getDevice();
501
- void typedi_1.Container.get(SocketServer_1.SocketServer).emitToDashboardForDevices(SocketEvents_1.SocketEvents.SESSION_COMMAND, Object.assign({ session_id: session.getId() }, logEntry), { udid: device === null || device === void 0 ? void 0 : device.udid });
555
+ void typedi_1.Container.get(SocketServer_1.SocketServer).emitToDashboardForDevices(SocketEvents_1.SocketEvents.SESSION_COMMAND, (0, sessionCommandSummary_1.sessionCommandSummary)(Object.assign(Object.assign({}, logEntry), { session_id: session.getId() })), { udid: device === null || device === void 0 ? void 0 : device.udid });
502
556
  // Heal event broadcast — match the shape returned by GET /healing/events
503
557
  // so the Overview activity feed and Settings list can consume both
504
558
  // sources interchangeably without per-source field translation.
@@ -506,15 +560,15 @@ let DashboardEventManager = class DashboardEventManager {
506
560
  void typedi_1.Container.get(SocketServer_1.SocketServer).emitToDashboardForDevices(SocketEvents_1.SocketEvents.HEALING_EVENT, {
507
561
  id: persistedLog.id,
508
562
  sessionId: session.getId(),
509
- deviceUdid: (_j = device === null || device === void 0 ? void 0 : device.udid) !== null && _j !== void 0 ? _j : null,
510
- deviceName: (_k = device === null || device === void 0 ? void 0 : device.name) !== null && _k !== void 0 ? _k : null,
511
- devicePlatform: (_l = device === null || device === void 0 ? void 0 : device.platform) !== null && _l !== void 0 ? _l : null,
512
- commandName: (_m = logEntry.command_name) !== null && _m !== void 0 ? _m : null,
513
- originalSelector: (_o = logEntry.original_selector) !== null && _o !== void 0 ? _o : null,
514
- healedSelector: (_p = logEntry.healed_selector) !== null && _p !== void 0 ? _p : null,
515
- confidence: (_q = logEntry.healing_confidence) !== null && _q !== void 0 ? _q : null,
516
- tier: (_r = logEntry.healing_tier) !== null && _r !== void 0 ? _r : null,
517
- isSuccess: (_s = logEntry.is_success) !== null && _s !== void 0 ? _s : null,
563
+ deviceUdid: (_g = device === null || device === void 0 ? void 0 : device.udid) !== null && _g !== void 0 ? _g : null,
564
+ deviceName: (_h = device === null || device === void 0 ? void 0 : device.name) !== null && _h !== void 0 ? _h : null,
565
+ devicePlatform: (_j = device === null || device === void 0 ? void 0 : device.platform) !== null && _j !== void 0 ? _j : null,
566
+ commandName: (_k = logEntry.command_name) !== null && _k !== void 0 ? _k : null,
567
+ originalSelector: (_l = logEntry.original_selector) !== null && _l !== void 0 ? _l : null,
568
+ healedSelector: (_m = logEntry.healed_selector) !== null && _m !== void 0 ? _m : null,
569
+ confidence: (_o = logEntry.healing_confidence) !== null && _o !== void 0 ? _o : null,
570
+ tier: (_p = logEntry.healing_tier) !== null && _p !== void 0 ? _p : null,
571
+ isSuccess: (_q = logEntry.is_success) !== null && _q !== void 0 ? _q : null,
518
572
  createdAt: persistedLog.createdAt.toISOString(),
519
573
  }, { udid: device === null || device === void 0 ? void 0 : device.udid });
520
574
  }
@@ -617,41 +671,8 @@ let DashboardEventManager = class DashboardEventManager {
617
671
  return [];
618
672
  }
619
673
  }
620
- else if (automationName === 'uiautomator2' || (device === null || device === void 0 ? void 0 : device.platform) === 'android') {
621
- try {
622
- if (typeof driver.extractLogs === 'function') {
623
- const logs = yield driver.extractLogs('logcat');
624
- if (Array.isArray(logs) && logs.length > 0) {
625
- return logs;
626
- }
627
- }
628
- }
629
- catch (err) {
630
- logger_1.default.debug(`Could not extract logcat via driver: ${err}. Trying direct ADB...`);
631
- }
632
- // Principal Intelligence: Fallback to direct ADB logs if driver fails or returns nothing
633
- if (device === null || device === void 0 ? void 0 : device.udid) {
634
- try {
635
- const deviceManager = typedi_1.Container.get(device_managers_1.XenonManager);
636
- const androidManager = (yield deviceManager.deviceInstances()).find((m) => m instanceof AndroidDeviceManager_1.default);
637
- if (androidManager) {
638
- const rawLogs = yield androidManager.getLogs(device.udid);
639
- if (rawLogs && typeof rawLogs === 'string') {
640
- const logLines = rawLogs.split('\n').filter((l) => l.trim().length > 0);
641
- // Convert string lines to expected format (last 100 lines to avoid DB bloat)
642
- return logLines.slice(-100).map((line) => ({
643
- message: line,
644
- timestamp: Date.now(),
645
- level: 'INFO',
646
- }));
647
- }
648
- }
649
- }
650
- catch (adbErr) {
651
- logger_1.default.debug(`Direct ADB log fetch failed for ${device.udid}: ${adbErr.message}`);
652
- }
653
- }
654
- }
674
+ // An Android phone's lines are recorded for the whole session by
675
+ // SessionDeviceLogs, not per command.
655
676
  return [];
656
677
  }
657
678
  catch (error) {
@@ -45,10 +45,18 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
45
45
  return (mod && mod.__esModule) ? mod : { "default": mod };
46
46
  };
47
47
  Object.defineProperty(exports, "__esModule", { value: true });
48
- exports.analyzeSessionFailure = analyzeSessionFailure;
48
+ exports.MAX_CONCURRENT_FAILURE_ANALYSES = exports.ANALYSIS_CATEGORIES = exports.ERROR_PATTERNS = void 0;
49
+ exports.categorizeSessionFailure = categorizeSessionFailure;
50
+ exports.explainSessionFailure = explainSessionFailure;
49
51
  const prisma_1 = require("../../prisma");
50
52
  const logger_1 = __importDefault(require("../../logger"));
51
- const ERROR_PATTERNS = [
53
+ /**
54
+ * Each category and the texts that file a failure under it, tried in this
55
+ * order against the failure reason and the last five failed commands. The
56
+ * dashboard's runbooks describe a category by these, not by its name
57
+ * (failure-categories.spec.ts).
58
+ */
59
+ exports.ERROR_PATTERNS = [
52
60
  {
53
61
  category: 'ELEMENT_NOT_FOUND',
54
62
  patterns: [
@@ -98,7 +106,54 @@ const ERROR_PATTERNS = [
98
106
  patterns: ['OutOfMemory', 'MemoryLimit', 'thermal throttling', 'too many open files'],
99
107
  },
100
108
  ];
101
- function analyzeSessionFailure(sessionId) {
109
+ /** What the analysis writes when no pattern matches. */
110
+ const UNMATCHED = 'UNKNOWN';
111
+ /**
112
+ * Every category the analysis writes to a failed session's
113
+ * `failure_category`, upper case as stored. The only other value the column
114
+ * holds is `HUB_RESTART_CATEGORY` (SessionManager). The API reference's
115
+ * examples and the dashboard's runbooks are held to these
116
+ * (failure-categories.spec.ts).
117
+ */
118
+ exports.ANALYSIS_CATEGORIES = [
119
+ ...exports.ERROR_PATTERNS.map((p) => p.category),
120
+ UNMATCHED,
121
+ ];
122
+ /**
123
+ * How many failed sessions' AI analyses run at once (explainSessionFailure);
124
+ * the others wait their turn. They run after their sessions have ended, so
125
+ * nothing else limits them: a broken build on a big lab ends hundreds of
126
+ * failed sessions in minutes, and each analysis holds a screenshot while it
127
+ * runs. Few at once also spares the provider's rate limit.
128
+ */
129
+ exports.MAX_CONCURRENT_FAILURE_ANALYSES = 4;
130
+ /** Each session's analysis, waiting or running, so one session gets one. */
131
+ const analyses = new Map();
132
+ let analysesRunning = 0;
133
+ const analysesWaiting = [];
134
+ function analysisTurn() {
135
+ return __awaiter(this, void 0, void 0, function* () {
136
+ if (analysesRunning < exports.MAX_CONCURRENT_FAILURE_ANALYSES) {
137
+ analysesRunning++;
138
+ return;
139
+ }
140
+ // The turn is handed over by endAnalysisTurn, still counted as running.
141
+ yield new Promise((resolve) => analysesWaiting.push(resolve));
142
+ });
143
+ }
144
+ function endAnalysisTurn() {
145
+ const next = analysesWaiting.shift();
146
+ if (next)
147
+ next();
148
+ else
149
+ analysesRunning--;
150
+ }
151
+ /**
152
+ * Files a failed session under a category (`failure_category`) by its failure
153
+ * reason and its failed commands. Rules only, so it is quick: a session's end
154
+ * waits for it. Never throws.
155
+ */
156
+ function categorizeSessionFailure(sessionId) {
102
157
  return __awaiter(this, void 0, void 0, function* () {
103
158
  try {
104
159
  const session = yield prisma_1.prisma.session.findUnique({
@@ -112,8 +167,8 @@ function analyzeSessionFailure(sessionId) {
112
167
  const reason = session.failure_reason || '';
113
168
  const logs_text = session.SessionLog.map((l) => `${l.title} ${l.response}`).join(' ');
114
169
  const combined_text = (reason + ' ' + logs_text).toLowerCase();
115
- let identifiedCategory = 'UNKNOWN';
116
- for (const item of ERROR_PATTERNS) {
170
+ let identifiedCategory = UNMATCHED;
171
+ for (const item of exports.ERROR_PATTERNS) {
117
172
  if (item.patterns.some((p) => new RegExp(p, 'i').test(combined_text))) {
118
173
  identifiedCategory = item.category;
119
174
  break;
@@ -122,48 +177,9 @@ function analyzeSessionFailure(sessionId) {
122
177
  // Special case for App Crash - check Logcat/Syslog if available
123
178
  // For now we rely on the error response text which usually mentions "process has died"
124
179
  logger_1.default.info(`[FailureAnalysis] Session ${sessionId} identified as ${identifiedCategory}`);
125
- // AI Root-Cause Analysis
126
- let aiAnalysis = null;
127
- try {
128
- const { AI_SERVICE } = yield Promise.resolve().then(() => __importStar(require('../../services/AIService')));
129
- if (AI_SERVICE.isEnabled()) {
130
- const lastLogs = yield prisma_1.prisma.log.findMany({
131
- where: { session_id: sessionId, log_type: 'DEVICE' },
132
- take: 50,
133
- orderBy: { timestamp: 'desc' },
134
- });
135
- const lastCommands = yield prisma_1.prisma.sessionLog.findMany({
136
- where: { session_id: sessionId },
137
- take: 10,
138
- orderBy: { createdAt: 'desc' },
139
- });
140
- // Find the last screenshot in logs
141
- const lastScreenshotLog = lastCommands.find((l) => l.screenshot !== null);
142
- aiAnalysis = yield AI_SERVICE.analyzeFailure({
143
- sessionId,
144
- failureReason: reason,
145
- commandLogs: lastCommands.map((c) => {
146
- var _a;
147
- return ({
148
- command: c.command_name,
149
- success: c.is_success,
150
- response: (_a = c.response) === null || _a === void 0 ? void 0 : _a.slice(0, 500), // Truncate long responses
151
- });
152
- }),
153
- deviceLogs: lastLogs.map((l) => l.message),
154
- screenshotPath: (lastScreenshotLog === null || lastScreenshotLog === void 0 ? void 0 : lastScreenshotLog.screenshot) || undefined,
155
- });
156
- }
157
- }
158
- catch (aiErr) {
159
- logger_1.default.warn(`[FailureAnalysis] AI Analysis failed for ${sessionId}: ${aiErr.message}`);
160
- }
161
180
  yield prisma_1.prisma.session.update({
162
181
  where: { id: sessionId },
163
- data: {
164
- failure_category: identifiedCategory,
165
- ai_analysis: aiAnalysis,
166
- },
182
+ data: { failure_category: identifiedCategory },
167
183
  });
168
184
  }
169
185
  catch (err) {
@@ -171,3 +187,78 @@ function analyzeSessionFailure(sessionId) {
171
187
  }
172
188
  });
173
189
  }
190
+ /**
191
+ * Asks the AI provider why a failed session failed, and saves the answer
192
+ * (`ai_analysis`). Only an answer is saved: with no provider, a rate limit, a
193
+ * time-out (FAILURE_ANALYSIS_TIMEOUT_MS) or a failed call nothing is written,
194
+ * and an analysis saved earlier stays. Never rejects.
195
+ *
196
+ * A session's end doesn't wait for it (onSessionStopped): an AI call can take
197
+ * minutes, and the client's quit, or a hub's DELETE, waits for the end. At
198
+ * most MAX_CONCURRENT_FAILURE_ANALYSES run at once, and a session whose
199
+ * analysis is waiting or running gets that one (a session can end twice: a
200
+ * crash, then the client's delete).
201
+ */
202
+ function explainSessionFailure(sessionId) {
203
+ const pending = analyses.get(sessionId);
204
+ if (pending)
205
+ return pending;
206
+ const analysis = (() => __awaiter(this, void 0, void 0, function* () {
207
+ yield analysisTurn();
208
+ try {
209
+ yield explain(sessionId);
210
+ }
211
+ finally {
212
+ endAnalysisTurn();
213
+ }
214
+ }))().finally(() => analyses.delete(sessionId));
215
+ analyses.set(sessionId, analysis);
216
+ return analysis;
217
+ }
218
+ function explain(sessionId) {
219
+ return __awaiter(this, void 0, void 0, function* () {
220
+ try {
221
+ const { AI_SERVICE } = yield Promise.resolve().then(() => __importStar(require('../../services/AIService')));
222
+ if (!AI_SERVICE.isEnabled())
223
+ return;
224
+ const session = yield prisma_1.prisma.session.findUnique({ where: { id: sessionId } });
225
+ if (!session)
226
+ return;
227
+ const lastLogs = yield prisma_1.prisma.log.findMany({
228
+ where: { session_id: sessionId, log_type: 'DEVICE' },
229
+ take: 50,
230
+ orderBy: { timestamp: 'desc' },
231
+ });
232
+ const lastCommands = yield prisma_1.prisma.sessionLog.findMany({
233
+ where: { session_id: sessionId },
234
+ take: 10,
235
+ orderBy: { createdAt: 'desc' },
236
+ });
237
+ // Find the last screenshot in logs
238
+ const lastScreenshotLog = lastCommands.find((l) => l.screenshot !== null);
239
+ const aiAnalysis = yield AI_SERVICE.analyzeFailure({
240
+ sessionId,
241
+ failureReason: session.failure_reason || '',
242
+ commandLogs: lastCommands.map((c) => {
243
+ var _a;
244
+ return ({
245
+ command: c.command_name,
246
+ success: c.is_success,
247
+ response: (_a = c.response) === null || _a === void 0 ? void 0 : _a.slice(0, 500), // Truncate long responses
248
+ });
249
+ }),
250
+ deviceLogs: lastLogs.map((l) => l.message),
251
+ screenshotPath: (lastScreenshotLog === null || lastScreenshotLog === void 0 ? void 0 : lastScreenshotLog.screenshot) || undefined,
252
+ });
253
+ if (!aiAnalysis)
254
+ return;
255
+ yield prisma_1.prisma.session.update({
256
+ where: { id: sessionId },
257
+ data: { ai_analysis: aiAnalysis },
258
+ });
259
+ }
260
+ catch (aiErr) {
261
+ logger_1.default.warn(`[FailureAnalysis] AI Analysis failed for ${sessionId}: ${aiErr.message}`);
262
+ }
263
+ });
264
+ }
@@ -0,0 +1,39 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SESSION_COMMAND_SUMMARY_FIELDS = void 0;
4
+ exports.sessionCommandSummary = sessionCommandSummary;
5
+ /**
6
+ * The fields of a command's record that leave its session: the live
7
+ * `session_command` event and the event log carry these and nothing else.
8
+ * Which command ran in which session, how it went and how it healed. The rest
9
+ * stays in the session's SessionLog, which goes with the session: the
10
+ * record's `body` is the command's arguments (the text `setValue` typed) and
11
+ * its `response` the command's answer (a page source, a screenshot's base64).
12
+ * Named one by one, so a field added to the record later goes nowhere until
13
+ * it is listed here.
14
+ */
15
+ exports.SESSION_COMMAND_SUMMARY_FIELDS = [
16
+ 'session_id',
17
+ 'command_name',
18
+ 'method',
19
+ 'is_success',
20
+ 'is_error',
21
+ 'is_healed',
22
+ 'original_strategy',
23
+ 'original_selector',
24
+ 'healed_strategy',
25
+ 'healed_selector',
26
+ 'healing_confidence',
27
+ 'healing_tier',
28
+ 'duration',
29
+ 'span_id',
30
+ 'trace_id',
31
+ ];
32
+ function sessionCommandSummary(record) {
33
+ const summary = {};
34
+ for (const field of exports.SESSION_COMMAND_SUMMARY_FIELDS) {
35
+ if ((record === null || record === void 0 ? void 0 : record[field]) !== undefined)
36
+ summary[field] = record[field];
37
+ }
38
+ return summary;
39
+ }
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.appleFamilyOf = appleFamilyOf;
4
+ /** Whichever of the two words a name carries; iPad first, as "iPad" holds no "iPhone". */
5
+ function familyInName(name) {
6
+ const lower = (name !== null && name !== void 0 ? name : '').toLowerCase();
7
+ if (lower.includes('ipad'))
8
+ return 'ipad';
9
+ if (lower.includes('iphone'))
10
+ return 'iphone';
11
+ return undefined;
12
+ }
13
+ /**
14
+ * Whether a phone is an iPhone or an iPad, or neither (an Android device, an
15
+ * Apple TV). Read from what the phone reports, in order of how far it can be
16
+ * trusted:
17
+ *
18
+ * 1. its model, lockdown's ProductType ("iPhone17,3", "iPad13,4");
19
+ * 2. its form factor, which a simulator gets from its own name and a real
20
+ * phone from its DeviceClass;
21
+ * 3. its marketing name, then its name, for a row written before the
22
+ * identity columns existed.
23
+ *
24
+ * A real phone's own name is whatever its owner typed ("QA-Phone-3", or
25
+ * "iPhone spare" for an iPad), so it is the last resort, not the first.
26
+ */
27
+ function appleFamilyOf(device) {
28
+ var _a, _b, _c;
29
+ if (((_a = device.platform) !== null && _a !== void 0 ? _a : '').toLowerCase() !== 'ios')
30
+ return undefined;
31
+ const model = (_b = device.model) !== null && _b !== void 0 ? _b : '';
32
+ if (/^iPhone/i.test(model))
33
+ return 'iphone';
34
+ if (/^iPad/i.test(model))
35
+ return 'ipad';
36
+ if (device.formFactor === 'phone')
37
+ return 'iphone';
38
+ if (device.formFactor === 'tablet')
39
+ return 'ipad';
40
+ if (device.formFactor === 'tv')
41
+ return undefined;
42
+ return (_c = familyInName(device.marketingName)) !== null && _c !== void 0 ? _c : familyInName(device.name);
43
+ }
@@ -20,6 +20,10 @@ class XenonDatabase {
20
20
  static get DeviceModel() {
21
21
  return XenonDatabase.getDeviceModel();
22
22
  }
23
+ /** Each phone's saved settings (deviceSettings.ts), by udid and host. */
24
+ static get DeviceSettingsModel() {
25
+ return XenonDatabase.getDeviceSettingsModel();
26
+ }
23
27
  static get PendingSessionsModel() {
24
28
  return XenonDatabase.getPendingSessionsModel();
25
29
  }
@@ -49,6 +53,12 @@ class XenonDatabase {
49
53
  return db.getCollection('devices') || db.addCollection('devices');
50
54
  });
51
55
  }
56
+ static getDeviceSettingsModel() {
57
+ return __awaiter(this, void 0, void 0, function* () {
58
+ const db = yield XenonDatabase.getDB();
59
+ return db.getCollection('device-settings') || db.addCollection('device-settings');
60
+ });
61
+ }
52
62
  static getPendingSessionsModel() {
53
63
  return __awaiter(this, void 0, void 0, function* () {
54
64
  const db = yield XenonDatabase.getDB();
@@ -63,6 +73,7 @@ class XenonDatabase {
63
73
  }
64
74
  static initCollections(db) {
65
75
  db.addCollection('devices');
76
+ db.addCollection('device-settings');
66
77
  db.addCollection('pending-sessions');
67
78
  db.addCollection('cliArgs');
68
79
  }
@@ -81,8 +81,13 @@ function removeDevice(devices_1) {
81
81
  const team = socket.hasScopedDashboard()
82
82
  ? yield typedi_1.Container.get(DeviceTeamResolver_1.DeviceTeamResolver).resolve(device.udid)
83
83
  : undefined;
84
+ // The callers hold a udid and a host. The device_offline webhook promises
85
+ // its name and platform too, so read them while the row still exists.
86
+ const known = yield store
87
+ .findDevice({ udid: device.udid, host: device.host })
88
+ .catch(() => null);
84
89
  yield store.removeDevices({ udid: device.udid, host: device.host }, options);
85
- typedi_1.Container.get(NotificationService_1.NotificationService).dispatchEvent('device_offline', device);
90
+ typedi_1.Container.get(NotificationService_1.NotificationService).dispatchEvent('device_offline', Object.assign(Object.assign({}, known), device));
86
91
  void socket.emitToDashboardForDevices('device_removed', device, deviceScope(device.udid, (team === null || team === void 0 ? void 0 : team.known) ? team.teamId : undefined));
87
92
  }
88
93
  });