@xenon-device-management/xenon 2.14.0 → 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 (259) hide show
  1. package/README.md +7 -4
  2. package/lib/package.json +5 -5
  3. package/lib/public/assets/{AnnotationOverlay-B_VNtKBH.js → AnnotationOverlay-Btq3dZRI.js} +1 -1
  4. package/lib/public/assets/{ApiKeyGate-BwuaMjkX.js → ApiKeyGate-C9OoAgg_.js} +1 -1
  5. package/lib/public/assets/DeviceMosaicView-DBC_xczB.js +2 -0
  6. package/lib/public/assets/{EmptyState-BSjhk_IV.js → EmptyState-DqWU2-vM.js} +1 -1
  7. package/lib/public/assets/{FieldGroup-Cb5QZsE1.js → FieldGroup-CveQ3xrA.js} +1 -1
  8. package/lib/public/assets/{FilterMenu-BAaSlWbW.js → FilterMenu-BfKDSfHz.js} +1 -1
  9. package/lib/public/assets/{Menu-JGGYrSQQ.js → Menu-CtdCybzA.js} +1 -1
  10. package/lib/public/assets/{Modal-CFCp3n9d.js → Modal-Bsvj1tm2.js} +1 -1
  11. package/lib/public/assets/{RecordingPage-CbJj_EBT.js → RecordingPage-CRofOiyx.js} +1 -1
  12. package/lib/public/assets/{RecordingsPage-BJeYnygR.js → RecordingsPage-jNQHb2IR.js} +1 -1
  13. package/lib/public/assets/{SegmentedControl-Bg42Atjy.js → SegmentedControl-C8HtTha-.js} +1 -1
  14. package/lib/public/assets/{SettingCard-Njz1pUIJ.js → SettingCard-D8K1LZNo.js} +1 -1
  15. package/lib/public/assets/{Table-BcTm5xV2.js → Table-zNTX92Pn.js} +1 -1
  16. package/lib/public/assets/{activity-DOVdsfMQ.js → activity-CJ8LwZq9.js} +1 -1
  17. package/lib/public/assets/ai-settings-CgxQZqrc.js +16 -0
  18. package/lib/public/assets/{api-keys-BKnNSTw9.js → api-keys-ECpo-zTo.js} +1 -1
  19. package/lib/public/assets/{apps-D0SC6uw6.js → apps-BjGymyf3.js} +1 -1
  20. package/lib/public/assets/{arrow-left-MMbTmgoE.js → arrow-left-BWfCbeHP.js} +1 -1
  21. package/lib/public/assets/{arrow-right-BMfnGmxW.js → arrow-right-DC7UGf_n.js} +1 -1
  22. package/lib/public/assets/{arrow-up-right-DuAftMWB.js → arrow-up-right-BOjwybDk.js} +1 -1
  23. package/lib/public/assets/{auth-shell-nZC2JNDO.js → auth-shell-CxMTo9aH.js} +2 -2
  24. package/lib/public/assets/{builds-page-hDCgTwkS.js → builds-page-DOCm4HkA.js} +1 -1
  25. package/lib/public/assets/{button-Dda4irn_.js → button-Cg08-EMT.js} +1 -1
  26. package/lib/public/assets/{calendar-BMr0Q62v.js → calendar-CA1ikLJs.js} +1 -1
  27. package/lib/public/assets/{check-CZ9iw9-e.js → check-BMTaH72W.js} +1 -1
  28. package/lib/public/assets/{chevron-right-BRYJJVC9.js → chevron-right-B2QOv9Nx.js} +1 -1
  29. package/lib/public/assets/{circle-check-CqAJjQ5O.js → circle-check-CYXBoUqT.js} +1 -1
  30. package/lib/public/assets/{circle-x-DlDJtnVz.js → circle-x-DTwnSAnl.js} +1 -1
  31. package/lib/public/assets/{clock-CpuDKysU.js → clock-C8_HViQP.js} +1 -1
  32. package/lib/public/assets/{copy-CAQydNFe.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-C9XG6lLv.js → download-B56lwxWP.js} +1 -1
  36. package/lib/public/assets/forgot-password-CCU6bzLn.js +1 -0
  37. package/lib/public/assets/{h264Stream-VQBphYhg.js → h264Stream-C-OtmIuk.js} +6 -6
  38. package/lib/public/assets/index-B9KQ6LsY.js +15 -0
  39. package/lib/public/assets/{index-BbKEXUFH.js → index-BOxe43p1.js} +2 -2
  40. package/lib/public/assets/{index-DJGc1M2f.js → index-CfxsUslp.js} +1 -1
  41. package/lib/public/assets/{input-Cm0tmSoc.js → input-CZHFjbSg.js} +1 -1
  42. package/lib/public/assets/{line-chart-D2DyJCml.js → line-chart-Gl_Dz3X_.js} +1 -1
  43. package/lib/public/assets/{list-checks-UWf1GY99.js → list-checks-CCgjeyGY.js} +1 -1
  44. package/lib/public/assets/{lock-CCJv9m_I.js → lock-CkOwgDcx.js} +1 -1
  45. package/lib/public/assets/{login-f8vOVW-U.js → login-D5cc4w7M.js} +1 -1
  46. package/lib/public/assets/{maintenance-settings-C1PrkuAH.js → maintenance-settings-t7qr3fvX.js} +1 -1
  47. package/lib/public/assets/{monitor-B2jDgwm7.js → monitor-DEJixKQO.js} +1 -1
  48. package/lib/public/assets/{mouse-pointer-2-DLI1RtzU.js → mouse-pointer-2-CtOrk4M8.js} +1 -1
  49. package/lib/public/assets/{network-ebT86qoi.js → network-Dii9qdvk.js} +1 -1
  50. package/lib/public/assets/{overview-C0aykVrd.js → overview-rolAACrO.js} +1 -1
  51. package/lib/public/assets/{page-header-BjzgWz-T.js → page-header-COQZxJfR.js} +1 -1
  52. package/lib/public/assets/{play-DLG9t8FU.js → play-DrN1Se_R.js} +1 -1
  53. package/lib/public/assets/{plus-nBFSMu_X.js → plus-5aq-9uRx.js} +1 -1
  54. package/lib/public/assets/{profile-page-CSl0UYlv.js → profile-page-6i5kY6kF.js} +1 -1
  55. package/lib/public/assets/{reset-password-LeOuHqmB.js → reset-password-sp2IBrc7.js} +1 -1
  56. package/lib/public/assets/runbook-page-DJDjeopU.js +253 -0
  57. package/lib/public/assets/{select-88jHcl-Z.js → select-Bpyn7rWO.js} +1 -1
  58. package/lib/public/assets/{selector-detail-redirect-BbXKJjZM.js → selector-detail-redirect-BH74vmKv.js} +1 -1
  59. package/lib/public/assets/{selector-health-page-B4rA1Xkv.js → selector-health-page-Cw_g218H.js} +1 -1
  60. package/lib/public/assets/session-detail-page-AZJE6XXE.js +4 -0
  61. package/lib/public/assets/{settings-GF6ZMSQ6.js → settings-C3uyGhyc.js} +1 -1
  62. package/lib/public/assets/{settings-Dep_3kbA.css → settings-K8UHWMdI.css} +1 -1
  63. package/lib/public/assets/{stat-tile-BV98e_Sv.js → stat-tile-xVqAKIlI.js} +1 -1
  64. package/lib/public/assets/{tablet-BiAsnFDj.js → tablet-BxUScAeO.js} +1 -1
  65. package/lib/public/assets/{teams-B4-jrr-1.js → teams-B5ljGkY2.js} +1 -1
  66. package/lib/public/assets/{trash-2-C5RSuX0e.js → trash-2-B701i8sy.js} +1 -1
  67. package/lib/public/assets/{upload-BG7_rMXK.js → upload-C2gr44RS.js} +1 -1
  68. package/lib/public/assets/{use-builds-data-J-riQYyC.js → use-builds-data-NeQOFEoy.js} +1 -1
  69. package/lib/public/assets/{use-password-reset-mode-C8-eH-UG.js → use-password-reset-mode-DEzJq5ef.js} +1 -1
  70. package/lib/public/assets/{users-CFba5TSy.js → users-D_vv_yz4.js} +1 -1
  71. package/lib/public/assets/{users-Cp0beJm3.js → users-MY6eNe6s.js} +1 -1
  72. package/lib/public/assets/{video-off-DDsEISjV.js → video-off-OK1lNZQn.js} +1 -1
  73. package/lib/public/assets/{webhook-settings-BGu32vSB.js → webhook-settings-BU-C9lBf.js} +1 -1
  74. package/lib/public/assets/{zap-l4VaXzq8.js → zap-DO9ry1iz.js} +1 -1
  75. package/lib/public/index.html +1 -1
  76. package/lib/schema.json +5 -5
  77. package/lib/src/app/dashboardPluginLink.js +117 -0
  78. package/lib/src/app/index.js +13 -34
  79. package/lib/src/app/openapi/control.yaml +6 -4
  80. package/lib/src/app/openapi/grid.yaml +8 -2
  81. package/lib/src/app/openapi/identity.yaml +103 -31
  82. package/lib/src/app/openapi/platform.yaml +33 -15
  83. package/lib/src/app/openapi/sessions.yaml +10 -3
  84. package/lib/src/app/routers/apikeys.js +14 -2
  85. package/lib/src/app/routers/auth.js +29 -5
  86. package/lib/src/app/routers/control.js +13 -5
  87. package/lib/src/app/routers/dashboard.js +42 -27
  88. package/lib/src/app/routers/grid.js +5 -3
  89. package/lib/src/app/routers/profile.js +21 -0
  90. package/lib/src/app/routers/users.js +10 -5
  91. package/lib/src/app/swagger.js +2 -0
  92. package/lib/src/app/ws/h264StreamWs.js +10 -1
  93. package/lib/src/config.js +1 -0
  94. package/lib/src/dashboard/commands.js +5 -5
  95. package/lib/src/dashboard/event-manager.js +49 -61
  96. package/lib/src/dashboard/services/failure-analysis-service.js +136 -45
  97. package/lib/src/dashboard/sessionCommandSummary.js +39 -0
  98. package/lib/src/data-service/device-store.js +14 -1
  99. package/lib/src/data-service/prisma-store.js +19 -7
  100. package/lib/src/data-service/web-config-service.js +7 -0
  101. package/lib/src/device-managers/android/AndroidH264StreamService.js +69 -20
  102. package/lib/src/device-managers/android/H264Multiplexer.js +38 -4
  103. package/lib/src/device-utils.js +20 -2
  104. package/lib/src/gateway/defaultGateway.js +1 -1
  105. package/lib/src/gateway/forwardToNode.js +2 -0
  106. package/lib/src/gateway/healReport.js +87 -0
  107. package/lib/src/gateway/sessionGateway.js +12 -2
  108. package/lib/src/generated/client/edge.js +4 -3
  109. package/lib/src/generated/client/index-browser.js +1 -0
  110. package/lib/src/generated/client/index.d.ts +32 -0
  111. package/lib/src/generated/client/index.js +4 -3
  112. package/lib/src/generated/client/package.json +1 -1
  113. package/lib/src/generated/client/schema.prisma +3 -0
  114. package/lib/src/generated/client/wasm.js +1 -0
  115. package/lib/src/interceptors/CommandInterceptor.js +243 -181
  116. package/lib/src/middleware/authMiddleware.js +15 -3
  117. package/lib/src/middleware/commandCaller.js +3 -3
  118. package/lib/src/middleware/verifyCredential.js +58 -1
  119. package/lib/src/plugin.js +8 -3
  120. package/lib/src/services/AICommandService.js +9 -2
  121. package/lib/src/services/AIService.js +192 -42
  122. package/lib/src/services/EmailService.js +29 -0
  123. package/lib/src/services/OrphanSweeper.js +2 -0
  124. package/lib/src/services/SelectorVerificationJob.js +15 -4
  125. package/lib/src/services/ServerManager.js +11 -1
  126. package/lib/src/services/SessionLifecycleService.js +86 -26
  127. package/lib/src/services/SocketServer.js +19 -3
  128. package/lib/src/services/bug-report/video-slice.js +4 -1
  129. package/lib/src/services/device-access/leaseOverride.js +7 -5
  130. package/lib/src/services/healing/FuzzyXmlHealingProvider.js +51 -0
  131. package/lib/src/services/healing/HealEtalonService.js +3 -2
  132. package/lib/src/services/healing/HealingOrchestrator.js +55 -72
  133. package/lib/src/services/healing/OcrHealingProvider.js +41 -64
  134. package/lib/src/services/healing/ResilioTreeHealingProvider.js +41 -50
  135. package/lib/src/services/healing/VisualAiHealingProvider.js +1 -1
  136. package/lib/src/services/healing/fingerprintIdentity.js +126 -0
  137. package/lib/src/services/healing/resilioPath.js +274 -0
  138. package/lib/src/services/logcat/SessionDeviceLogs.js +271 -0
  139. package/lib/src/services/logcat/deviceLogBook.js +163 -0
  140. package/lib/src/services/ocr/ocrData.js +118 -0
  141. package/lib/src/services/ocr/vendor/LICENSE-tessdata.txt +202 -0
  142. package/lib/src/services/ocr/vendor/eng.traineddata.gz +0 -0
  143. package/lib/src/services/omni-vision/OmniVisionService.js +83 -56
  144. package/lib/src/services/passwordResetLink.js +50 -9
  145. package/lib/src/services/passwordResetMode.js +3 -1
  146. package/lib/src/services/publicUrl.js +45 -0
  147. package/lib/src/services/session/sessionIdentity.js +5 -3
  148. package/lib/src/services/session/shutdownReason.js +26 -0
  149. package/lib/src/services/settings/aiEngineSettings.js +219 -0
  150. package/lib/src/services/token/mintedLifetime.js +51 -0
  151. package/lib/src/sessions/SessionManager.js +8 -2
  152. package/lib/src/sessions/sessionMemory.js +25 -0
  153. package/lib/test/helpers/fake-ai-provider.js +188 -0
  154. package/lib/test/helpers/loki-stores.js +33 -0
  155. package/lib/test/helpers/page-sources.js +164 -0
  156. package/lib/test/helpers/scratch-database.js +57 -13
  157. package/lib/test/integration/audit-ingest.spec.js +10 -10
  158. package/lib/test/integration/auth-flow.spec.js +9 -14
  159. package/lib/test/integration/auth-rate-limit.spec.js +6 -3
  160. package/lib/test/integration/bug-report-route.spec.js +22 -2
  161. package/lib/test/integration/forgot-password-rate-limit.spec.js +6 -3
  162. package/lib/test/integration/forgot-password.spec.js +17 -18
  163. package/lib/test/integration/legacy-deprecation-headers.spec.js +5 -2
  164. package/lib/test/integration/migration.spec.js +6 -1
  165. package/lib/test/integration/node-pair-auth-rest.spec.js +5 -9
  166. package/lib/test/integration/ports-router.spec.js +4 -4
  167. package/lib/test/integration/prisma-device-store-add-devices.spec.js +2 -3
  168. package/lib/test/integration/prisma-device-store-team-filter.spec.js +2 -5
  169. package/lib/test/integration/profile-tokens.spec.js +10 -16
  170. package/lib/test/integration/recordings-router.spec.js +16 -16
  171. package/lib/test/integration/reset-revokes-sessions.spec.js +4 -9
  172. package/lib/test/integration/role-matrix.spec.js +4 -9
  173. package/lib/test/integration/sdk-leases-router.spec.js +9 -9
  174. package/lib/test/integration/sdk-version.spec.js +2 -2
  175. package/lib/test/integration/selector-health-endpoint.spec.js +5 -5
  176. package/lib/test/integration/team-membership-migration.spec.js +73 -12
  177. package/lib/test/integration/team-visibility-apps.spec.js +15 -22
  178. package/lib/test/integration/team-visibility-control.spec.js +96 -69
  179. package/lib/test/integration/team-visibility-dashboard.spec.js +7 -23
  180. package/lib/test/integration/team-visibility-grid.spec.js +16 -15
  181. package/lib/test/integration/team-visibility-leases.spec.js +8 -11
  182. package/lib/test/integration/team-visibility-recordings.spec.js +6 -19
  183. package/lib/test/integration/team-visibility-sessions.spec.js +6 -19
  184. package/lib/test/integration/users-flow.spec.js +7 -12
  185. package/lib/test/integration/users-lockout.spec.js +5 -3
  186. package/lib/test/unit/EmailService.test.js +66 -0
  187. package/lib/test/unit/ResilioTreeHealingProvider.test.js +46 -19
  188. package/lib/test/unit/ai-call-time-limit.spec.js +130 -0
  189. package/lib/test/unit/ai-engine-settings.spec.js +338 -0
  190. package/lib/test/unit/ai-provider-switch.spec.js +130 -0
  191. package/lib/test/unit/ai-rate-limit.spec.js +126 -0
  192. package/lib/test/unit/android-h264-viewers.spec.js +272 -0
  193. package/lib/test/unit/android-preview-hold.spec.js +3 -2
  194. package/lib/test/unit/apikeys-router.spec.js +84 -0
  195. package/lib/test/unit/authMiddleware.bearer.spec.js +9 -0
  196. package/lib/test/unit/authMiddleware.credentialLifetime.spec.js +149 -0
  197. package/lib/test/unit/bug-report/video-slice.spec.js +19 -0
  198. package/lib/test/unit/dashboard-events-device-logs.spec.js +207 -0
  199. package/lib/test/unit/dashboard-plugin-link.spec.js +213 -0
  200. package/lib/test/unit/device-log-book.spec.js +180 -0
  201. package/lib/test/unit/enable-dashboard-option.spec.js +57 -0
  202. package/lib/test/unit/event-log-session-data.spec.js +244 -0
  203. package/lib/test/unit/failure-analysis.spec.js +182 -0
  204. package/lib/test/unit/failure-categories.spec.js +128 -0
  205. package/lib/test/unit/fingerprint-identity.spec.js +433 -0
  206. package/lib/test/unit/h264-multiplexer.spec.js +46 -0
  207. package/lib/test/unit/heal-report.spec.js +71 -0
  208. package/lib/test/unit/healing-instrumentation.spec.js +6 -1
  209. package/lib/test/unit/healing-tier-gate.spec.js +73 -29
  210. package/lib/test/unit/healing-tiers-every-session.spec.js +219 -0
  211. package/lib/test/unit/hub-node-gateway-appium-server.spec.js +171 -0
  212. package/lib/test/unit/interceptor-visual-elements.spec.js +192 -0
  213. package/lib/test/unit/lease/lease-override.spec.js +23 -6
  214. package/lib/test/unit/lease/session-lease-proof.spec.js +16 -4
  215. package/lib/test/unit/ocr-healing-tier-real-ocr.spec.js +113 -0
  216. package/lib/test/unit/ocr-healing-tier.spec.js +122 -0
  217. package/lib/test/unit/ocr-offline.spec.js +213 -0
  218. package/lib/test/unit/omni-virtual-element-store.spec.js +301 -0
  219. package/lib/test/unit/omni-vision-ios-points.spec.js +0 -2
  220. package/lib/test/unit/omni-vision.spec.js +5 -7
  221. package/lib/test/unit/package-engines.spec.js +27 -0
  222. package/lib/test/unit/passwordResetMode.test.js +10 -2
  223. package/lib/test/unit/profile-router.test.js +122 -1
  224. package/lib/test/unit/queue-requester.spec.js +1 -1
  225. package/lib/test/unit/reset-link-address.spec.js +266 -0
  226. package/lib/test/unit/reset-link.test.js +10 -4
  227. package/lib/test/unit/resilio-path-healing.spec.js +270 -0
  228. package/lib/test/unit/selector-learning-every-session.spec.js +179 -0
  229. package/lib/test/unit/selector-verification-clean-build.spec.js +130 -0
  230. package/lib/test/unit/selector-verification-job.spec.js +13 -13
  231. package/lib/test/unit/self-healing-boot.spec.js +10 -5
  232. package/lib/test/unit/self-healing-setting.spec.js +7 -6
  233. package/lib/test/unit/session-app-resolution.spec.js +5 -1
  234. package/lib/test/unit/session-attribution-wiring.spec.js +41 -13
  235. package/lib/test/unit/session-create-credential-owner.spec.js +242 -0
  236. package/lib/test/unit/session-credential-strip.spec.js +1 -1
  237. package/lib/test/unit/session-device-logs.spec.js +347 -0
  238. package/lib/test/unit/session-end-failure-analysis.spec.js +133 -0
  239. package/lib/test/unit/session-log-indexes.spec.js +64 -3
  240. package/lib/test/unit/socketServer.sessionCookie.spec.js +2 -1
  241. package/lib/test/unit/stream-start-pipelines.spec.js +72 -18
  242. package/lib/test/unit/token-mint-limits.spec.js +165 -0
  243. package/lib/test/unit/unexpected-shutdown-reason.spec.js +163 -0
  244. package/lib/test/unit/verify-credential.spec.js +33 -0
  245. package/lib/test/unit/vision-assertion.spec.js +3 -2
  246. package/lib/test/unit/web-config-service.spec.js +19 -0
  247. package/lib/tsconfig.tsbuildinfo +1 -1
  248. package/package.json +5 -5
  249. package/prisma/migrations/20261004174413_locator_etalon_path/migration.sql +2 -0
  250. package/prisma/schema.prisma +3 -0
  251. package/schema.json +5 -5
  252. package/lib/public/assets/BugReportButton-BAt0_pzq.js +0 -15
  253. package/lib/public/assets/DeviceMosaicView-BYaQHhTf.js +0 -2
  254. package/lib/public/assets/ai-settings-CsuJroRf.js +0 -21
  255. package/lib/public/assets/device-explorer-CdO6_nrn.js +0 -282
  256. package/lib/public/assets/forgot-password-CGnkRgE9.js +0 -1
  257. package/lib/public/assets/runbook-page-CMMeQml9.js +0 -124
  258. package/lib/public/assets/session-detail-page-COL_RZxw.js +0 -4
  259. /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");
@@ -80,7 +78,9 @@ const SelectorStateService_1 = require("../services/SelectorStateService");
80
78
  const recording_store_1 = require("../services/recording/recording-store");
81
79
  const NotificationService_1 = require("../services/NotificationService");
82
80
  const SessionMetricsService_1 = require("../services/metrics/SessionMetricsService");
81
+ const SessionDeviceLogs_1 = require("../services/logcat/SessionDeviceLogs");
83
82
  const nodeMetrics_1 = require("../services/metrics/nodeMetrics");
83
+ const sessionCommandSummary_1 = require("./sessionCommandSummary");
84
84
  const typedi_2 = require("typedi");
85
85
  /**
86
86
  * What a Session row keeps of the capabilities the driver returned. The row
@@ -106,7 +106,7 @@ let DashboardEventManager = class DashboardEventManager {
106
106
  this.sessionToUdid = new Map();
107
107
  // Map session ID to device info
108
108
  this.sessionToDevice = new Map();
109
- // Track last log line for each session (for device logs)
109
+ // Track last log line for each session (an iPhone's device logs)
110
110
  this.lastLogLine = new Map();
111
111
  // Track start time for each command to calculate duration
112
112
  this.commandStartTime = new Map();
@@ -158,6 +158,14 @@ let DashboardEventManager = class DashboardEventManager {
158
158
  if (sampled) {
159
159
  metrics.start(Object.assign({ sessionId: session.getId(), device, capabilities: session.getCapabilities() }, (source ? { source } : {})));
160
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
+ });
161
169
  // Emit session started event
162
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 });
163
171
  // Increment Metrics
@@ -211,8 +219,10 @@ let DashboardEventManager = class DashboardEventManager {
211
219
  this.stoppingSessionIds.add(sessionId);
212
220
  try {
213
221
  logger_1.default.info(`🟢 onSessionStopped called for session ${sessionId}`);
214
- // 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.
215
224
  yield typedi_1.Container.get(SessionMetricsService_1.SessionMetricsService).stop(sessionId);
225
+ yield typedi_1.Container.get(SessionDeviceLogs_1.SessionDeviceLogs).stop(sessionId);
216
226
  // Video recording is now handled in plugin.ts deleteSession() before the session is deleted
217
227
  // This ensures we can call stop_recording_screen while the session is still active
218
228
  // Here we just handle the session status update
@@ -356,11 +366,15 @@ let DashboardEventManager = class DashboardEventManager {
356
366
  if (updateData.status === SessionStatus_1.SessionStatus.FAILED && options.notify !== false) {
357
367
  this.announceFailure(Object.assign(Object.assign({}, sessionEntry), updateData));
358
368
  }
359
- // Principal Triage: If session failed, perform intelligent failure analysis
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.
360
373
  if (updateData.status === SessionStatus_1.SessionStatus.FAILED) {
361
374
  try {
362
- const { analyzeSessionFailure } = yield Promise.resolve().then(() => __importStar(require('./services/failure-analysis-service')));
363
- 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);
364
378
  }
365
379
  catch (analysisErr) {
366
380
  logger_1.default.warn(`⚠️ Failure analysis skipped for ${sessionId}: ${analysisErr.message}`);
@@ -429,7 +443,7 @@ let DashboardEventManager = class DashboardEventManager {
429
443
  }
430
444
  afterSessionCommand(sessionId, commandName, driver, request, response, responseBody, healingInfo) {
431
445
  return __awaiter(this, void 0, void 0, function* () {
432
- 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;
433
447
  const session = SessionManager_1.SESSION_MANAGER.getSession(sessionId);
434
448
  if (session) {
435
449
  try {
@@ -470,15 +484,20 @@ let DashboardEventManager = class DashboardEventManager {
470
484
  };
471
485
  // Smart Passive: capture strategy + selector on every findElement,
472
486
  // not just heals. CommandInterceptor synthesizes request.body = args
473
- // (the array [strategy, value]). This gives the verification job
474
- // exact evidence of "selector ran and didn't heal" per build.
475
- if ((commandName === 'findElement' || commandName === 'findElements') &&
476
- Array.isArray(request.body)) {
477
- if (logEntry.original_strategy === null) {
478
- 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;
479
498
  }
480
- if (logEntry.original_selector === null) {
481
- 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;
482
501
  }
483
502
  }
484
503
  // Increment Healing Metrics
@@ -530,8 +549,10 @@ let DashboardEventManager = class DashboardEventManager {
530
549
  });
531
550
  // Emit command log event to dashboard. One per command: the phone's
532
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.
533
554
  const device = session.getDevice();
534
- 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 });
535
556
  // Heal event broadcast — match the shape returned by GET /healing/events
536
557
  // so the Overview activity feed and Settings list can consume both
537
558
  // sources interchangeably without per-source field translation.
@@ -539,15 +560,15 @@ let DashboardEventManager = class DashboardEventManager {
539
560
  void typedi_1.Container.get(SocketServer_1.SocketServer).emitToDashboardForDevices(SocketEvents_1.SocketEvents.HEALING_EVENT, {
540
561
  id: persistedLog.id,
541
562
  sessionId: session.getId(),
542
- deviceUdid: (_j = device === null || device === void 0 ? void 0 : device.udid) !== null && _j !== void 0 ? _j : null,
543
- deviceName: (_k = device === null || device === void 0 ? void 0 : device.name) !== null && _k !== void 0 ? _k : null,
544
- devicePlatform: (_l = device === null || device === void 0 ? void 0 : device.platform) !== null && _l !== void 0 ? _l : null,
545
- commandName: (_m = logEntry.command_name) !== null && _m !== void 0 ? _m : null,
546
- originalSelector: (_o = logEntry.original_selector) !== null && _o !== void 0 ? _o : null,
547
- healedSelector: (_p = logEntry.healed_selector) !== null && _p !== void 0 ? _p : null,
548
- confidence: (_q = logEntry.healing_confidence) !== null && _q !== void 0 ? _q : null,
549
- tier: (_r = logEntry.healing_tier) !== null && _r !== void 0 ? _r : null,
550
- 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,
551
572
  createdAt: persistedLog.createdAt.toISOString(),
552
573
  }, { udid: device === null || device === void 0 ? void 0 : device.udid });
553
574
  }
@@ -650,41 +671,8 @@ let DashboardEventManager = class DashboardEventManager {
650
671
  return [];
651
672
  }
652
673
  }
653
- else if (automationName === 'uiautomator2' || (device === null || device === void 0 ? void 0 : device.platform) === 'android') {
654
- try {
655
- if (typeof driver.extractLogs === 'function') {
656
- const logs = yield driver.extractLogs('logcat');
657
- if (Array.isArray(logs) && logs.length > 0) {
658
- return logs;
659
- }
660
- }
661
- }
662
- catch (err) {
663
- logger_1.default.debug(`Could not extract logcat via driver: ${err}. Trying direct ADB...`);
664
- }
665
- // Principal Intelligence: Fallback to direct ADB logs if driver fails or returns nothing
666
- if (device === null || device === void 0 ? void 0 : device.udid) {
667
- try {
668
- const deviceManager = typedi_1.Container.get(device_managers_1.XenonManager);
669
- const androidManager = (yield deviceManager.deviceInstances()).find((m) => m instanceof AndroidDeviceManager_1.default);
670
- if (androidManager) {
671
- const rawLogs = yield androidManager.getLogs(device.udid);
672
- if (rawLogs && typeof rawLogs === 'string') {
673
- const logLines = rawLogs.split('\n').filter((l) => l.trim().length > 0);
674
- // Convert string lines to expected format (last 100 lines to avoid DB bloat)
675
- return logLines.slice(-100).map((line) => ({
676
- message: line,
677
- timestamp: Date.now(),
678
- level: 'INFO',
679
- }));
680
- }
681
- }
682
- }
683
- catch (adbErr) {
684
- logger_1.default.debug(`Direct ADB log fetch failed for ${device.udid}: ${adbErr.message}`);
685
- }
686
- }
687
- }
674
+ // An Android phone's lines are recorded for the whole session by
675
+ // SessionDeviceLogs, not per command.
688
676
  return [];
689
677
  }
690
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
+ }
@@ -8,6 +8,17 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
8
8
  step((generator = generator.apply(thisArg, _arguments || [])).next());
9
9
  });
10
10
  };
11
+ var __rest = (this && this.__rest) || function (s, e) {
12
+ var t = {};
13
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
14
+ t[p] = s[p];
15
+ if (s != null && typeof Object.getOwnPropertySymbols === "function")
16
+ for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
17
+ if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
18
+ t[p[i]] = s[p[i]];
19
+ }
20
+ return t;
21
+ };
11
22
  var __importDefault = (this && this.__importDefault) || function (mod) {
12
23
  return (mod && mod.__esModule) ? mod : { "default": mod };
13
24
  };
@@ -438,7 +449,9 @@ class LokiHealEtalonStore {
438
449
  const coll = yield this.getCollection();
439
450
  const existing = coll.findOne({ selector: etalon.selector });
440
451
  if (existing) {
441
- Object.assign(existing, etalon);
452
+ // A fingerprint saved without a path keeps the one stored, as in Prisma.
453
+ const { path } = etalon, rest = __rest(etalon, ["path"]);
454
+ Object.assign(existing, rest, path == null ? {} : { path });
442
455
  coll.update(existing);
443
456
  }
444
457
  else {
@@ -625,21 +625,24 @@ class PrismaHealEtalonStore {
625
625
  get prisma() {
626
626
  return typedi_1.Container.get(prisma_service_1.PrismaService).client;
627
627
  }
628
+ /**
629
+ * The fingerprint, and its path when it has one. A fingerprint saved without
630
+ * a path (a heal whose element's path couldn't be worked out) keeps the one
631
+ * stored. Through 2.14 the path was never stored, so the Resilio tier,
632
+ * which needs it, never found anything.
633
+ */
628
634
  saveSignature(etalon) {
629
635
  return __awaiter(this, void 0, void 0, function* () {
636
+ const path = etalon.path == null ? undefined : JSON.stringify(etalon.path);
630
637
  yield this.prisma.locatorEtalon.upsert({
631
638
  where: { selector: etalon.selector },
632
- update: {
633
- strategy: etalon.strategy,
634
- attributes: JSON.stringify(etalon.attributes),
635
- nodeName: etalon.nodeName,
636
- lastSeen: new Date(),
637
- },
639
+ update: Object.assign(Object.assign({ strategy: etalon.strategy, attributes: JSON.stringify(etalon.attributes), nodeName: etalon.nodeName }, (path !== undefined ? { path } : {})), { lastSeen: new Date() }),
638
640
  create: {
639
641
  selector: etalon.selector,
640
642
  strategy: etalon.strategy,
641
643
  attributes: JSON.stringify(etalon.attributes),
642
644
  nodeName: etalon.nodeName,
645
+ path,
643
646
  lastSeen: new Date(),
644
647
  },
645
648
  });
@@ -652,7 +655,16 @@ class PrismaHealEtalonStore {
652
655
  });
653
656
  if (!etalon)
654
657
  return null;
655
- return Object.assign(Object.assign({}, etalon), { attributes: JSON.parse(etalon.attributes), lastSeen: etalon.lastSeen.getTime() });
658
+ let path = null;
659
+ if (etalon.path) {
660
+ try {
661
+ path = JSON.parse(etalon.path);
662
+ }
663
+ catch (_a) {
664
+ path = null;
665
+ }
666
+ }
667
+ return Object.assign(Object.assign({}, etalon), { attributes: JSON.parse(etalon.attributes), path, lastSeen: etalon.lastSeen.getTime() });
656
668
  });
657
669
  }
658
670
  }
@@ -27,6 +27,13 @@ const SETTINGS = {
27
27
  buildCleanupMaxCount: 'number',
28
28
  buildCleanupSchedule: 'text',
29
29
  deleteBuildAssets: 'boolean',
30
+ aiProvider: 'text',
31
+ aiModel: 'text',
32
+ aiBaseUrl: 'text',
33
+ geminiModel: 'text',
34
+ openaiModel: 'text',
35
+ anthropicModel: 'text',
36
+ ollamaModel: 'text',
30
37
  };
31
38
  /**
32
39
  * The lab settings the dashboard saves, one `WebConfig` row each, keyed by the