@xenon-device-management/xenon 2.13.2 → 2.14.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 (209) hide show
  1. package/README.md +11 -5
  2. package/lib/package.json +1 -1
  3. package/lib/public/assets/{AnnotationOverlay-BXruClDk.js → AnnotationOverlay-B_VNtKBH.js} +1 -1
  4. package/lib/public/assets/{ApiKeyGate-qTS4LPLC.js → ApiKeyGate-BwuaMjkX.js} +1 -1
  5. package/lib/public/assets/{BugReportButton-BoGhlI7F.js → BugReportButton-BAt0_pzq.js} +1 -1
  6. package/lib/public/assets/DeviceMosaicView-BYaQHhTf.js +2 -0
  7. package/lib/public/assets/{EmptyState-Cr17PdDv.js → EmptyState-BSjhk_IV.js} +1 -1
  8. package/lib/public/assets/{FieldGroup-Bvn08svv.js → FieldGroup-Cb5QZsE1.js} +1 -1
  9. package/lib/public/assets/{FilterMenu-DDSO8aDf.js → FilterMenu-BAaSlWbW.js} +1 -1
  10. package/lib/public/assets/{Menu-B-Qn2X3T.js → Menu-JGGYrSQQ.js} +1 -1
  11. package/lib/public/assets/{Modal-Cg9V5JYU.js → Modal-CFCp3n9d.js} +1 -1
  12. package/lib/public/assets/{RecordingPage-BTUs9VTW.js → RecordingPage-CbJj_EBT.js} +1 -1
  13. package/lib/public/assets/{RecordingsPage-DYcfxITK.js → RecordingsPage-BJeYnygR.js} +2 -2
  14. package/lib/public/assets/{SegmentedControl-Zy7suhT5.js → SegmentedControl-Bg42Atjy.js} +1 -1
  15. package/lib/public/assets/{SettingCard-CWYmycoe.js → SettingCard-Njz1pUIJ.js} +1 -1
  16. package/lib/public/assets/{Table-io9fwa98.js → Table-BcTm5xV2.js} +1 -1
  17. package/lib/public/assets/{activity-Bdf_uIWl.js → activity-DOVdsfMQ.js} +1 -1
  18. package/lib/public/assets/{ai-settings-Bce5SJob.js → ai-settings-CsuJroRf.js} +2 -2
  19. package/lib/public/assets/{api-keys-CPqtVpm3.js → api-keys-BKnNSTw9.js} +1 -1
  20. package/lib/public/assets/{apps-BgZan0H9.js → apps-D0SC6uw6.js} +2 -2
  21. package/lib/public/assets/{arrow-left-B27ckB31.js → arrow-left-MMbTmgoE.js} +1 -1
  22. package/lib/public/assets/{arrow-right-DOF8ITCz.js → arrow-right-BMfnGmxW.js} +1 -1
  23. package/lib/public/assets/{arrow-up-right-eFG5KTFS.js → arrow-up-right-DuAftMWB.js} +1 -1
  24. package/lib/public/assets/{auth-shell-DlC3_27j.js → auth-shell-nZC2JNDO.js} +2 -2
  25. package/lib/public/assets/{builds-page-BuXpPfKo.js → builds-page-hDCgTwkS.js} +1 -1
  26. package/lib/public/assets/{button-fIW8owMI.js → button-Dda4irn_.js} +1 -1
  27. package/lib/public/assets/{calendar-WNjmrF-a.js → calendar-BMr0Q62v.js} +1 -1
  28. package/lib/public/assets/{check-BdTh1Sq7.js → check-CZ9iw9-e.js} +1 -1
  29. package/lib/public/assets/{chevron-right-B4BKlsqk.js → chevron-right-BRYJJVC9.js} +1 -1
  30. package/lib/public/assets/{circle-check-zJU80_EF.js → circle-check-CqAJjQ5O.js} +1 -1
  31. package/lib/public/assets/{circle-x-5Uwquo0h.js → circle-x-DlDJtnVz.js} +1 -1
  32. package/lib/public/assets/{clock-BtUprDsZ.js → clock-CpuDKysU.js} +1 -1
  33. package/lib/public/assets/{copy-DGSbZI3u.js → copy-CAQydNFe.js} +1 -1
  34. package/lib/public/assets/device-explorer-CdO6_nrn.js +282 -0
  35. package/lib/public/assets/{download-WG6l7BgD.js → download-C9XG6lLv.js} +1 -1
  36. package/lib/public/assets/{forgot-password-DnudqFFo.js → forgot-password-CGnkRgE9.js} +1 -1
  37. package/lib/public/assets/h264Stream-VQBphYhg.js +26 -0
  38. package/lib/public/assets/{index-DI1fGIyj.js → index-BbKEXUFH.js} +5 -5
  39. package/lib/public/assets/{index-HDQVe1gi.js → index-DJGc1M2f.js} +1 -1
  40. package/lib/public/assets/{input-BjDRKCea.js → input-Cm0tmSoc.js} +1 -1
  41. package/lib/public/assets/{line-chart-C0zdgd0v.js → line-chart-D2DyJCml.js} +1 -1
  42. package/lib/public/assets/{list-checks-BdW5CjM8.js → list-checks-UWf1GY99.js} +1 -1
  43. package/lib/public/assets/{lock-Dy3L4I8E.js → lock-CCJv9m_I.js} +1 -1
  44. package/lib/public/assets/{login-BHbY9CLG.js → login-f8vOVW-U.js} +1 -1
  45. package/lib/public/assets/maintenance-settings-C1PrkuAH.js +6 -0
  46. package/lib/public/assets/{monitor-8xjkeHHi.js → monitor-B2jDgwm7.js} +1 -1
  47. package/lib/public/assets/{mouse-pointer-2-CXdKMXQ8.js → mouse-pointer-2-DLI1RtzU.js} +1 -1
  48. package/lib/public/assets/{network-Cj8JOKON.js → network-ebT86qoi.js} +1 -1
  49. package/lib/public/assets/{overview-CTg7F0im.js → overview-C0aykVrd.js} +1 -1
  50. package/lib/public/assets/{page-header--aTyJXq6.js → page-header-BjzgWz-T.js} +1 -1
  51. package/lib/public/assets/{play-B-FodgHV.js → play-DLG9t8FU.js} +1 -1
  52. package/lib/public/assets/{plus-CsIk10Cq.js → plus-nBFSMu_X.js} +1 -1
  53. package/lib/public/assets/{profile-page-DkFKkMfl.js → profile-page-CSl0UYlv.js} +1 -1
  54. package/lib/public/assets/{reset-password-I2-zqLlI.js → reset-password-LeOuHqmB.js} +1 -1
  55. package/lib/public/assets/{runbook-page-DJTVCJX7.js → runbook-page-CMMeQml9.js} +1 -1
  56. package/lib/public/assets/{select-DvCSv25z.js → select-88jHcl-Z.js} +1 -1
  57. package/lib/public/assets/{selector-detail-redirect-BLVfMJs_.js → selector-detail-redirect-BbXKJjZM.js} +1 -1
  58. package/lib/public/assets/{selector-health-page-oqadRokn.js → selector-health-page-B4rA1Xkv.js} +2 -2
  59. package/lib/public/assets/session-detail-page-COL_RZxw.js +4 -0
  60. package/lib/public/assets/settings-GF6ZMSQ6.js +6 -0
  61. package/lib/public/assets/{stat-tile-DV9Ix-4O.js → stat-tile-BV98e_Sv.js} +1 -1
  62. package/lib/public/assets/{tablet-6b_SEN2X.js → tablet-BiAsnFDj.js} +1 -1
  63. package/lib/public/assets/{teams-BjZyfuHh.js → teams-B4-jrr-1.js} +1 -1
  64. package/lib/public/assets/{trash-2-CTZpPSdd.js → trash-2-C5RSuX0e.js} +1 -1
  65. package/lib/public/assets/{upload-P3XzwQUb.js → upload-BG7_rMXK.js} +1 -1
  66. package/lib/public/assets/{use-builds-data-1cOCMf-z.js → use-builds-data-J-riQYyC.js} +1 -1
  67. package/lib/public/assets/{use-password-reset-mode-DgF9w088.js → use-password-reset-mode-C8-eH-UG.js} +1 -1
  68. package/lib/public/assets/{users-DMF9Mk5H.js → users-CFba5TSy.js} +1 -1
  69. package/lib/public/assets/{users-CiNTKUiz.js → users-Cp0beJm3.js} +1 -1
  70. package/lib/public/assets/{video-off-CHp1KU_T.js → video-off-DDsEISjV.js} +1 -1
  71. package/lib/public/assets/webhook-settings-BGu32vSB.js +1 -0
  72. package/lib/public/assets/{zap-DRz0tDq5.js → zap-l4VaXzq8.js} +1 -1
  73. package/lib/public/index.html +1 -1
  74. package/lib/schema.json +19 -21
  75. package/lib/src/XenonCapabilityManager.js +22 -5
  76. package/lib/src/app/openapi/control.yaml +71 -20
  77. package/lib/src/app/openapi/grid.yaml +23 -5
  78. package/lib/src/app/openapi/platform.yaml +136 -28
  79. package/lib/src/app/routers/control.js +12 -2
  80. package/lib/src/app/routers/dashboard.js +29 -1
  81. package/lib/src/app/routers/grid.js +32 -10
  82. package/lib/src/app/routers/streamType.js +7 -2
  83. package/lib/src/app/routers/webhook.js +8 -4
  84. package/lib/src/config.js +50 -5
  85. package/lib/src/dashboard/commands.js +185 -141
  86. package/lib/src/dashboard/event-manager.js +39 -6
  87. package/lib/src/data-service/appleFamily.js +43 -0
  88. package/lib/src/data-service/db.js +11 -0
  89. package/lib/src/data-service/device-service.js +6 -1
  90. package/lib/src/data-service/device-store.js +62 -2
  91. package/lib/src/data-service/deviceClaims.js +37 -0
  92. package/lib/src/data-service/deviceFieldOwners.js +25 -11
  93. package/lib/src/data-service/deviceSettings.js +79 -0
  94. package/lib/src/data-service/prisma-store.js +150 -8
  95. package/lib/src/data-service/web-config-service.js +43 -92
  96. package/lib/src/device-managers/AndroidDeviceManager.js +9 -31
  97. package/lib/src/device-managers/ios/WDAClient.js +44 -43
  98. package/lib/src/device-managers/shellCommands.js +96 -0
  99. package/lib/src/device-utils.js +96 -40
  100. package/lib/src/generated/client/edge.js +17 -3
  101. package/lib/src/generated/client/index-browser.js +14 -0
  102. package/lib/src/generated/client/index.d.ts +2054 -379
  103. package/lib/src/generated/client/index.js +17 -3
  104. package/lib/src/generated/client/package.json +1 -1
  105. package/lib/src/generated/client/schema.prisma +35 -7
  106. package/lib/src/generated/client/wasm.js +14 -0
  107. package/lib/src/interceptors/CommandInterceptor.js +29 -8
  108. package/lib/src/interceptors/xenonScripts.js +55 -0
  109. package/lib/src/interfaces/IPluginArgs.js +1 -2
  110. package/lib/src/plugin.js +15 -1
  111. package/lib/src/prisma.js +1 -1
  112. package/lib/src/scripts/database-check.js +124 -0
  113. package/lib/src/services/AIService.js +99 -0
  114. package/lib/src/services/CleanupService.js +6 -1
  115. package/lib/src/services/InterceptorService.js +70 -13
  116. package/lib/src/services/NetworkConditioningService.js +85 -88
  117. package/lib/src/services/NotificationService.js +36 -29
  118. package/lib/src/services/OrphanSweeper.js +5 -0
  119. package/lib/src/services/PortAllocator.js +5 -0
  120. package/lib/src/services/ServerManager.js +94 -8
  121. package/lib/src/services/SessionLifecycleService.js +47 -49
  122. package/lib/src/services/ShutdownCoordinator.js +33 -0
  123. package/lib/src/services/TeamService.js +20 -3
  124. package/lib/src/services/healing/HealingOrchestrator.js +32 -1
  125. package/lib/src/services/interceptor/AndroidProxyAdapter.js +18 -0
  126. package/lib/src/services/interceptor/interceptorOptions.js +44 -0
  127. package/lib/src/services/network/AndroidRadios.js +71 -0
  128. package/lib/src/services/network/PhoneNetworkLedger.js +184 -0
  129. package/lib/src/services/network/PhoneNetworkRestore.js +330 -0
  130. package/lib/src/services/network/adbForPhone.js +60 -0
  131. package/lib/src/services/network/phoneNetworkLock.js +20 -0
  132. package/lib/src/services/network/xenonProxy.js +107 -0
  133. package/lib/src/services/omni-vision/OmniVisionService.js +53 -43
  134. package/lib/src/services/omni-vision/VisionAssertionService.js +28 -25
  135. package/lib/src/services/omni-vision/ocrTextMatch.js +105 -0
  136. package/lib/src/services/omni-vision/screenScale.js +68 -0
  137. package/lib/src/services/recording/concurrency-gate.js +12 -3
  138. package/lib/src/services/settings/SelfHealingSwitch.js +70 -0
  139. package/lib/src/services/settings/labSettings.js +149 -0
  140. package/lib/src/services/webhookEvents.js +173 -0
  141. package/lib/test/helpers/fake-adb.js +58 -0
  142. package/lib/test/helpers/fake-device-table.js +63 -24
  143. package/lib/test/helpers/scratch-database.js +1 -0
  144. package/lib/test/unit/TeamService.test.js +3 -0
  145. package/lib/test/unit/XenonCapabilityManager.spec.js +4 -2
  146. package/lib/test/unit/apple-family.spec.js +36 -0
  147. package/lib/test/unit/cleanup-settings-db.spec.js +103 -0
  148. package/lib/test/unit/cleanup-settings.spec.js +244 -0
  149. package/lib/test/unit/dashboard-api-paths.spec.js +101 -0
  150. package/lib/test/unit/dashboard-commands.spec.js +249 -0
  151. package/lib/test/unit/dashboard-events-team-scope.spec.js +9 -6
  152. package/lib/test/unit/database-check.spec.js +108 -0
  153. package/lib/test/unit/default-plugin-args.spec.js +33 -0
  154. package/lib/test/unit/device-offline-webhook.spec.js +81 -0
  155. package/lib/test/unit/device-setting-columns.spec.js +66 -0
  156. package/lib/test/unit/device-settings-loki.spec.js +87 -0
  157. package/lib/test/unit/device-settings-survive.spec.js +555 -0
  158. package/lib/test/unit/device-sync-lost-update.spec.js +5 -0
  159. package/lib/test/unit/emulator-boot.spec.js +91 -0
  160. package/lib/test/unit/hub-node-gateway-appium-server.spec.js +48 -0
  161. package/lib/test/unit/hub-node-same-host.spec.js +5 -0
  162. package/lib/test/unit/iphone-ipad-only.spec.js +132 -0
  163. package/lib/test/unit/json-logging-option.spec.js +74 -0
  164. package/lib/test/unit/lab-settings.spec.js +218 -0
  165. package/lib/test/unit/max-sessions.spec.js +240 -0
  166. package/lib/test/unit/ocr-text-match.spec.js +44 -0
  167. package/lib/test/unit/omni-vision-analyze-screen.spec.js +72 -0
  168. package/lib/test/unit/omni-vision-ios-points.spec.js +161 -0
  169. package/lib/test/unit/omni-vision-tap-text.spec.js +129 -0
  170. package/lib/test/unit/omni-vision.spec.js +1 -1
  171. package/lib/test/unit/partial-config-file.spec.js +14 -0
  172. package/lib/test/unit/phone-network/boot-clean-up.spec.js +64 -0
  173. package/lib/test/unit/phone-network/interceptor-options.spec.js +77 -0
  174. package/lib/test/unit/phone-network/interceptor-proxy-restore.spec.js +145 -0
  175. package/lib/test/unit/phone-network/network-conditioning.spec.js +145 -0
  176. package/lib/test/unit/phone-network/phone-network-restore.spec.js +246 -0
  177. package/lib/test/unit/phone-network/session-end-paths.spec.js +182 -0
  178. package/lib/test/unit/phone-network/session-start.spec.js +110 -0
  179. package/lib/test/unit/phone-network/xenon-proxy.spec.js +69 -0
  180. package/lib/test/unit/plugin.spec.js +3 -3
  181. package/lib/test/unit/recording-options.spec.js +181 -0
  182. package/lib/test/unit/self-healing-boot.spec.js +75 -0
  183. package/lib/test/unit/self-healing-setting.spec.js +272 -0
  184. package/lib/test/unit/session-failed-webhook.spec.js +170 -0
  185. package/lib/test/unit/shell-commands.spec.js +133 -0
  186. package/lib/test/unit/socketServer.scopedEmit.spec.js +2 -0
  187. package/lib/test/unit/stream-start-pipelines.spec.js +210 -0
  188. package/lib/test/unit/stream-status-type.spec.js +4 -0
  189. package/lib/test/unit/vision-assertion.spec.js +118 -0
  190. package/lib/test/unit/wda-client-shell.spec.js +133 -0
  191. package/lib/test/unit/web-config-service.spec.js +79 -0
  192. package/lib/test/unit/webhook-delivery.spec.js +24 -0
  193. package/lib/test/unit/webhook-payloads.spec.js +273 -0
  194. package/lib/test/unit/xenon-execute-scripts.spec.js +138 -0
  195. package/lib/tsconfig.tsbuildinfo +1 -1
  196. package/lib/web/src/components/webhook-settings/webhookEventVariables.json +30 -0
  197. package/package.json +1 -1
  198. package/prisma/migrations/20261004140000_keep_device_settings/migration.sql +19 -0
  199. package/prisma/schema.prisma +35 -7
  200. package/schema.json +19 -21
  201. package/scripts/generate-types-from-schema.js +1 -2
  202. package/scripts/lib/always-present-plugin-args.js +4 -1
  203. package/lib/public/assets/DeviceMosaicView-8J0oYqMM.js +0 -2
  204. package/lib/public/assets/device-explorer-C-KnhMHI.js +0 -279
  205. package/lib/public/assets/maintenance-settings-zDVlOMVK.js +0 -6
  206. package/lib/public/assets/recording-group-store-WVM3hoXn.js +0 -26
  207. package/lib/public/assets/session-detail-page-CGn55Pvl.js +0 -4
  208. package/lib/public/assets/settings-MsVU2ukE.js +0 -6
  209. package/lib/public/assets/webhook-settings-DiS8geIm.js +0 -1
@@ -9,10 +9,15 @@ exports.resolveStreamType = resolveStreamType;
9
9
  * MJPEG path so the recording pipeline (which reads the MJPEG server) is
10
10
  * unaffected (spec phase-1 rule). iOS/tvOS always use MJPEG (WDA emits it
11
11
  * directly).
12
+ *
13
+ * `clientCanPlayH264` is false when the page that starts the stream shows
14
+ * MJPEG: device control, or a browser without WebCodecs (exposed only on
15
+ * https and localhost). Starting an H.264 capture for it would run a second
16
+ * capture beside the screencap loop its `<img>` starts.
12
17
  */
13
- function resolveStreamType(platform, flagOn, recording) {
18
+ function resolveStreamType(platform, flagOn, recording, clientCanPlayH264 = true) {
14
19
  const isAndroid = platform === 'android' || platform === 'androidtv' || platform === 'android-tv';
15
- if (!isAndroid || !flagOn || recording)
20
+ if (!isAndroid || !flagOn || recording || !clientCanPlayH264)
16
21
  return 'mjpeg';
17
22
  return 'h264';
18
23
  }
@@ -14,6 +14,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
15
  const express_1 = require("express");
16
16
  const NotificationService_1 = require("../../services/NotificationService");
17
+ const webhookEvents_1 = require("../../services/webhookEvents");
17
18
  const typedi_1 = require("typedi");
18
19
  const logger_1 = __importDefault(require("../../logger"));
19
20
  const scopeGuard_1 = require("../../middleware/scopeGuard");
@@ -63,17 +64,20 @@ function deleteConfig(req, res) {
63
64
  }
64
65
  });
65
66
  }
66
- // Sends a sample event the way the webhook's real events go out (its type
67
- // and template), and says whether it was delivered.
67
+ // Sends a sample of an event the way the webhook's real events go out (its
68
+ // type and template), and says whether it was delivered.
68
69
  function testWebhook(req, res) {
69
70
  return __awaiter(this, void 0, void 0, function* () {
70
71
  var _a, _b;
71
- const { url, type, payloadTemplate } = (_a = req.body) !== null && _a !== void 0 ? _a : {};
72
+ const { url, type, payloadTemplate, event } = (_a = req.body) !== null && _a !== void 0 ? _a : {};
72
73
  if (!url || typeof url !== 'string') {
73
74
  return res.status(400).json({ error: 'url is required' });
74
75
  }
76
+ if (event !== undefined && !(0, webhookEvents_1.isEventType)(event)) {
77
+ return res.status(400).json({ error: `event must be one of: ${webhookEvents_1.EVENT_TYPES.join(', ')}` });
78
+ }
75
79
  try {
76
- yield typedi_1.Container.get(NotificationService_1.NotificationService).sendTest(url, type || 'slack', payloadTemplate);
80
+ yield typedi_1.Container.get(NotificationService_1.NotificationService).sendTest(url, type || 'slack', payloadTemplate, event);
77
81
  res.json({ success: true });
78
82
  }
79
83
  catch (err) {
package/lib/src/config.js CHANGED
@@ -32,14 +32,20 @@ var __importStar = (this && this.__importStar) || (function () {
32
32
  return result;
33
33
  };
34
34
  })();
35
- var _a;
36
35
  Object.defineProperty(exports, "__esModule", { value: true });
37
- exports.config = void 0;
36
+ exports.config = exports.DEFAULT_MAX_CONCURRENT_RECORDINGS = void 0;
38
37
  exports.updateConfig = updateConfig;
39
38
  exports.resolveAuthDisabled = resolveAuthDisabled;
39
+ exports.resolveMaxConcurrentRecordings = resolveMaxConcurrentRecordings;
40
+ exports.resolveRecordingsAssetsPath = resolveRecordingsAssetsPath;
41
+ exports.recordingConfigFrom = recordingConfigFrom;
40
42
  const os = __importStar(require("os"));
41
43
  const path = __importStar(require("path"));
42
44
  const basePath = path.join(os.homedir(), '.cache', 'xenon');
45
+ /** Where free-form recordings go unless the option or the environment says otherwise. */
46
+ const DEFAULT_RECORDINGS_ASSETS_PATH = path.join(basePath, 'assets', 'sessions', 'recordings');
47
+ /** The server-wide cap on simultaneous free-form recordings, unless set. */
48
+ exports.DEFAULT_MAX_CONCURRENT_RECORDINGS = 4;
43
49
  exports.config = {
44
50
  cacheDir: basePath,
45
51
  databaseProvider: process.env.XENON_DB_PROVIDER || 'sqlite',
@@ -87,9 +93,8 @@ exports.config = {
87
93
  passwordResetLogFallback: process.env.XENON_PASSWORD_RESET_LOG_FALLBACK === 'true',
88
94
  resetRateLimitAttempts: Number(process.env.XENON_RESET_RATE_LIMIT_ATTEMPTS) || 3,
89
95
  resetRateLimitWindowMs: Number(process.env.XENON_RESET_RATE_LIMIT_WINDOW_MS) || 15 * 60 * 1000,
90
- recordingsAssetsPath: process.env.XENON_RECORDINGS_ASSETS_PATH ||
91
- path.join(basePath, 'assets', 'sessions', 'recordings'),
92
- maxConcurrentRecordings: Number((_a = process.env.XENON_MAX_CONCURRENT_RECORDINGS) !== null && _a !== void 0 ? _a : 4),
96
+ recordingsAssetsPath: resolveRecordingsAssetsPath(undefined, process.env.XENON_RECORDINGS_ASSETS_PATH),
97
+ maxConcurrentRecordings: resolveMaxConcurrentRecordings(undefined, process.env.XENON_MAX_CONCURRENT_RECORDINGS),
93
98
  };
94
99
  function updateConfig(newConfig) {
95
100
  Object.assign(exports.config, newConfig);
@@ -113,3 +118,43 @@ function updateConfig(newConfig) {
113
118
  function resolveAuthDisabled(pluginArgValue, envDisabled) {
114
119
  return pluginArgValue === true || envDisabled === true;
115
120
  }
121
+ function wholeNumberAtLeastOne(value) {
122
+ return typeof value === 'number' && Number.isInteger(value) && value >= 1 ? value : undefined;
123
+ }
124
+ function nonBlankString(value) {
125
+ return typeof value === 'string' && value.trim() !== '' ? value.trim() : undefined;
126
+ }
127
+ /**
128
+ * The cap on simultaneous free-form recordings: the `maxConcurrentRecordings`
129
+ * plugin option when it is set, else XENON_MAX_CONCURRENT_RECORDINGS, else 4.
130
+ * Both options are `schema.json` options Appium accepts, but only the variable
131
+ * was ever read. A value that is not a whole number of at least 1 is ignored,
132
+ * so the next source answers: the variable used to be taken as `Number(env)`,
133
+ * and `many` made that NaN, which no recording count exceeds (no cap at all).
134
+ * Pure, so the precedence is testable without booting a server.
135
+ */
136
+ function resolveMaxConcurrentRecordings(optionValue, envValue) {
137
+ var _a, _b;
138
+ const fromEnv = (envValue === null || envValue === void 0 ? void 0 : envValue.trim()) ? Number(envValue) : undefined;
139
+ return ((_b = (_a = wholeNumberAtLeastOne(optionValue)) !== null && _a !== void 0 ? _a : wholeNumberAtLeastOne(fromEnv)) !== null && _b !== void 0 ? _b : exports.DEFAULT_MAX_CONCURRENT_RECORDINGS);
140
+ }
141
+ /**
142
+ * Where free-form recordings are stored: the `recordingsAssetsPath` plugin
143
+ * option when it is set, else XENON_RECORDINGS_ASSETS_PATH, else
144
+ * `~/.cache/xenon/assets/sessions/recordings`. A blank value counts as unset.
145
+ */
146
+ function resolveRecordingsAssetsPath(optionValue, envValue) {
147
+ var _a, _b;
148
+ return (_b = (_a = nonBlankString(optionValue)) !== null && _a !== void 0 ? _a : nonBlankString(envValue)) !== null && _b !== void 0 ? _b : DEFAULT_RECORDINGS_ASSETS_PATH;
149
+ }
150
+ /**
151
+ * The part of `config` the recording plugin options set, from the options the
152
+ * server started with and the environment as it is now. Applied at boot,
153
+ * before the artifact store and the concurrency gate read them.
154
+ */
155
+ function recordingConfigFrom(pluginArgs, env = process.env) {
156
+ return {
157
+ maxConcurrentRecordings: resolveMaxConcurrentRecordings(pluginArgs.maxConcurrentRecordings, env.XENON_MAX_CONCURRENT_RECORDINGS),
158
+ recordingsAssetsPath: resolveRecordingsAssetsPath(pluginArgs.recordingsAssetsPath, env.XENON_RECORDINGS_ASSETS_PATH),
159
+ };
160
+ }
@@ -45,56 +45,123 @@ 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.dashboardCommands = exports.DashboardCommands = void 0;
48
+ exports.dashboardCommands = exports.DashboardCommands = exports.SESSION_DETAILS_COMMANDS = void 0;
49
+ exports.sessionDetailsCommandOf = sessionDetailsCommandOf;
49
50
  const session_service_1 = require("./services/session-service");
50
51
  const prisma_1 = require("../prisma");
51
52
  const logger_1 = __importDefault(require("../logger"));
52
- const lodash_1 = __importDefault(require("lodash"));
53
53
  const SessionManager_1 = require("../sessions/SessionManager");
54
54
  const asset_manager_1 = require("./asset-manager");
55
+ const xenonScripts_1 = require("../interceptors/xenonScripts");
56
+ /**
57
+ * The execute commands that write to a session's record (its Session row):
58
+ * `driver.execute('xenon: setSessionName', 'Checkout')` and the like.
59
+ *
60
+ * Each answers `{ recorded: true }` when it wrote, or `{ recorded: false,
61
+ * message }` saying why nothing was written: no record of the session on this
62
+ * server (its dashboard is off, or it is a node, which keeps none for the
63
+ * hub's sessions), an argument it can't use, or a failed write. None of them
64
+ * fails the test's command. They are bookkeeping, and a test shouldn't die
65
+ * because the server it ran on keeps no dashboard. Through 2.13.2
66
+ * setSessionName, setSessionStatus and debug failed the command there
67
+ * (Prisma's P2025, a foreign-key error), and addTag and captureEvidence
68
+ * answered `null` as if they had worked.
69
+ */
70
+ exports.SESSION_DETAILS_COMMANDS = [
71
+ 'setSessionName',
72
+ 'setSessionStatus',
73
+ 'debug',
74
+ 'addTag',
75
+ 'captureEvidence',
76
+ ];
77
+ const RECORDED = { recorded: true };
78
+ const notRecorded = (message) => ({ recorded: false, message });
79
+ /** The session-details command a script names (`xenon: addTag`), or null for any other script. */
80
+ function sessionDetailsCommandOf(script) {
81
+ const name = (0, xenonScripts_1.xenonScriptName)(script);
82
+ return name !== null && exports.SESSION_DETAILS_COMMANDS.includes(name)
83
+ ? name
84
+ : null;
85
+ }
86
+ /** The script's arguments as a list, as both the W3C body and Appium's executeScript give them. */
87
+ function argumentList(args) {
88
+ if (Array.isArray(args))
89
+ return args;
90
+ return args === undefined || args === null ? [] : [args];
91
+ }
92
+ function isObject(value) {
93
+ return typeof value === 'object' && value !== null;
94
+ }
95
+ /** A string argument, or the named field of an object argument. */
96
+ function stringArg(value, field) {
97
+ if (typeof value === 'string')
98
+ return value;
99
+ if (isObject(value) && typeof value[field] === 'string')
100
+ return value[field];
101
+ return undefined;
102
+ }
55
103
  class DashboardCommands {
56
- isDashboardCommand(commandName) {
57
- return commandName.startsWith('xenon') || commandName.startsWith('xe');
58
- }
104
+ /**
105
+ * Answers a session-details command on an Express-style response, as
106
+ * `{ value: <answer> }`, and returns the answer. The hub's gateway passes
107
+ * the client's response; CommandInterceptor passes one that keeps it.
108
+ */
59
109
  process(sessionId, request, response) {
60
110
  return __awaiter(this, void 0, void 0, function* () {
61
- const { script } = request.body;
62
- logger_1.default.info(`[DashboardCommands] Processing command: ${script}`);
63
- if (!script) {
64
- return this.sendSuccessResponse(response);
65
- }
66
- const commandName = script.split(':')[1];
67
- logger_1.default.info(`[DashboardCommands] Parsed command name: ${commandName}`);
68
- if (commandName && commandName.trim()) {
69
- const trimmedCommand = commandName.trim();
70
- logger_1.default.info(`[DashboardCommands] Trimmed command: ${trimmedCommand}`);
71
- switch (trimmedCommand) {
72
- case 'setSessionName':
73
- logger_1.default.info('[DashboardCommands] Executing setSessionName');
74
- return yield this.setSessionName(sessionId, request, response);
75
- case 'setSessionStatus':
76
- logger_1.default.info('[DashboardCommands] Executing setSessionStatus');
77
- return yield this.setSessionStatus(sessionId, request, response);
78
- case 'debug':
79
- logger_1.default.info('[DashboardCommands] Executing debug command');
80
- return yield this.debug(sessionId, request, response);
81
- case 'captureEvidence':
82
- logger_1.default.info('[DashboardCommands] Executing captureEvidence');
83
- return yield this.captureEvidence(sessionId, request, response);
84
- case 'addTag':
85
- logger_1.default.info('[DashboardCommands] Executing addTag');
86
- return yield this.addTag(sessionId, request, response);
87
- default:
88
- logger_1.default.warn(`[DashboardCommands] Unknown command: ${trimmedCommand}`);
89
- return this.sendSuccessResponse(response);
111
+ var _a;
112
+ const { script, args } = (_a = request.body) !== null && _a !== void 0 ? _a : {};
113
+ const command = sessionDetailsCommandOf(script);
114
+ const answer = command
115
+ ? yield this.run(sessionId, command, args)
116
+ : notRecorded(`"${script}" is not a command that records session details.`);
117
+ response.status(200).json({ value: answer });
118
+ return answer;
119
+ });
120
+ }
121
+ /** Runs one command. Never throws: a failure is `{ recorded: false, message }`. */
122
+ run(sessionId, command, args) {
123
+ return __awaiter(this, void 0, void 0, function* () {
124
+ var _a;
125
+ logger_1.default.info(`[DashboardCommands] ${command} for session ${sessionId}`);
126
+ const list = argumentList(args);
127
+ let answer;
128
+ try {
129
+ const row = yield prisma_1.prisma.session.findUnique({
130
+ where: { id: sessionId },
131
+ select: { id: true, tags: true },
132
+ });
133
+ if (!row) {
134
+ answer = notRecorded(`This server keeps no record of session ${sessionId}, so nothing was saved. ` +
135
+ 'Sessions are recorded on a hub with its dashboard turned on.');
136
+ }
137
+ else {
138
+ switch (command) {
139
+ case 'setSessionName':
140
+ answer = yield this.setSessionName(sessionId, list);
141
+ break;
142
+ case 'setSessionStatus':
143
+ answer = yield this.setSessionStatus(sessionId, list);
144
+ break;
145
+ case 'debug':
146
+ answer = yield this.debug(sessionId, list);
147
+ break;
148
+ case 'captureEvidence':
149
+ answer = yield this.captureEvidence(sessionId, list);
150
+ break;
151
+ case 'addTag':
152
+ answer = yield this.addTag(sessionId, list, row.tags);
153
+ break;
154
+ }
90
155
  }
91
156
  }
92
- return this.sendSuccessResponse(response);
157
+ catch (err) {
158
+ answer = notRecorded(`Xenon could not save it: ${(_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : err}`);
159
+ }
160
+ if (!answer.recorded)
161
+ logger_1.default.warn(`[DashboardCommands] ${command}: ${answer.message}`);
162
+ return answer;
93
163
  });
94
164
  }
95
- sendSuccessResponse(response) {
96
- return response.status(200).json({ value: null });
97
- }
98
165
  /* Commands */
99
166
  /**
100
167
  * Set the name of current test(session)
@@ -103,63 +170,50 @@ class DashboardCommands {
103
170
  * or
104
171
  * driver.executeScript("xenon: setSessionName", {"name": "MyTestName"})
105
172
  */
106
- setSessionName(sessionId, request, response) {
173
+ setSessionName(sessionId, args) {
107
174
  return __awaiter(this, void 0, void 0, function* () {
108
- const { args } = request.body;
109
- yield (0, session_service_1.updateSessionDetails)(sessionId, {
110
- name: typeof args[0] === 'object' && args[0].name ? args[0].name : args[0],
111
- });
112
- return this.sendSuccessResponse(response);
175
+ const name = stringArg(args[0], 'name');
176
+ if (!name || !name.trim()) {
177
+ return notRecorded('setSessionName needs a name, as a string or { name }.');
178
+ }
179
+ yield (0, session_service_1.updateSessionDetails)(sessionId, { name });
180
+ return RECORDED;
113
181
  });
114
182
  }
115
183
  /**
116
184
  * Update the status of the session
117
185
  *
118
186
  * driver.executeScript("xenon: setSessionStatus", {"status": "passed/failed", "reason": "optional reason"})
187
+ * or, positionally, ("xe:setSessionStatus", "failed", "reason")
119
188
  */
120
- setSessionStatus(sessionId, request, response) {
189
+ setSessionStatus(sessionId, args) {
121
190
  return __awaiter(this, void 0, void 0, function* () {
122
- const { args } = request.body;
123
- let statusArg = {};
124
- if (lodash_1.default.isArray(args)) {
125
- if (typeof args[0] === 'object') {
126
- statusArg = args[0];
127
- }
128
- else {
129
- // Handle positional: executeScript("xe:setSessionStatus", "failed", "reason")
130
- statusArg = {
131
- status: args[0],
132
- reason: args[1],
133
- };
134
- }
135
- }
136
- else if (typeof args === 'object') {
137
- statusArg = args;
138
- }
139
- // Normalize input
140
- const inputStatus = (statusArg.status || '').toLowerCase();
191
+ const statusArg = isObject(args[0]) ? args[0] : { status: args[0], reason: args[1] };
192
+ const given = statusArg.status;
193
+ const inputStatus = typeof given === 'string' ? given.toLowerCase() : '';
141
194
  const validStatuses = ['success', 'failed', 'passed'];
142
195
  if (!validStatuses.includes(inputStatus)) {
143
- logger_1.default.warn(`[DashboardCommands] Invalid status: ${statusArg.status}. Valid: ${validStatuses.join(', ')}`);
144
- return this.sendSuccessResponse(response);
196
+ return notRecorded('setSessionStatus takes "passed" or "failed" ("success" also means passed); ' +
197
+ `it was given ${JSON.stringify(given !== null && given !== void 0 ? given : null)}.`);
145
198
  }
146
199
  // Normalize 'passed' to 'success' for consistency with SessionStatus enum
147
200
  const normalizedStatus = inputStatus === 'passed' ? 'success' : inputStatus;
201
+ const reason = typeof statusArg.reason === 'string' ? statusArg.reason : undefined;
148
202
  const updated = yield (0, session_service_1.updateSessionDetails)(sessionId, {
149
203
  status: normalizedStatus,
150
- failure_reason: statusArg.reason || undefined,
204
+ failure_reason: reason || undefined,
151
205
  });
152
206
  // --- REAL-TIME UI UPDATE ---
153
- // Principal Speed: Emit a socket event so the dashboard row updates immediately.
207
+ // Emit a socket event so the dashboard row updates immediately.
154
208
  const { SocketServer } = yield Promise.resolve().then(() => __importStar(require('../services/SocketServer')));
155
209
  const { Container } = yield Promise.resolve().then(() => __importStar(require('typedi')));
156
210
  const { SocketEvents } = yield Promise.resolve().then(() => __importStar(require('../enums/SocketEvents')));
157
211
  void Container.get(SocketServer).emitToDashboardForDevices(SocketEvents.SESSION_STOPPED, {
158
212
  id: sessionId,
159
213
  status: normalizedStatus,
160
- failure_reason: args.reason || undefined,
214
+ failure_reason: reason || undefined,
161
215
  }, { udid: updated === null || updated === void 0 ? void 0 : updated.device_udid });
162
- return this.sendSuccessResponse(response);
216
+ return RECORDED;
163
217
  });
164
218
  }
165
219
  /**
@@ -169,12 +223,18 @@ class DashboardCommands {
169
223
  * or
170
224
  * driver.executeScript("xenon: debug", "Debug message")
171
225
  */
172
- debug(sessionId, request, response) {
226
+ debug(sessionId, args) {
173
227
  return __awaiter(this, void 0, void 0, function* () {
174
- const { args } = request.body;
175
- const logData = lodash_1.default.isArray(args) ? args[0] : args;
176
- // Support both string and object format
177
- const message = typeof logData === 'string' ? logData : logData.message || JSON.stringify(logData);
228
+ const logData = args[0];
229
+ let message;
230
+ if (typeof logData === 'string')
231
+ message = logData;
232
+ else if (isObject(logData)) {
233
+ message = typeof logData.message === 'string' ? logData.message : JSON.stringify(logData);
234
+ }
235
+ if (!message) {
236
+ return notRecorded('debug needs a message, as a string or { message }.');
237
+ }
178
238
  yield prisma_1.prisma.log.create({
179
239
  data: {
180
240
  session_id: sessionId,
@@ -182,7 +242,7 @@ class DashboardCommands {
182
242
  message: message,
183
243
  },
184
244
  });
185
- return this.sendSuccessResponse(response);
245
+ return RECORDED;
186
246
  });
187
247
  }
188
248
  /**
@@ -192,44 +252,43 @@ class DashboardCommands {
192
252
  * or
193
253
  * driver.executeScript("xenon: captureEvidence", "Checkpoint reached")
194
254
  */
195
- captureEvidence(sessionId, request, response) {
255
+ captureEvidence(sessionId, args) {
196
256
  return __awaiter(this, void 0, void 0, function* () {
197
- const { args } = request.body;
198
- const evidenceData = lodash_1.default.isArray(args) ? args[0] : args;
199
- const reason = typeof evidenceData === 'string' ? evidenceData : evidenceData.reason || 'Manual capture';
200
- const label = typeof evidenceData === 'object' ? evidenceData.label : undefined;
257
+ var _a;
258
+ const evidenceData = args[0];
259
+ const reason = stringArg(evidenceData, 'reason') || 'Manual capture';
260
+ const label = isObject(evidenceData) ? evidenceData.label : undefined;
261
+ const session = SessionManager_1.SESSION_MANAGER.getSession(sessionId);
262
+ if (!session) {
263
+ return notRecorded(`Session ${sessionId} isn't running here, so no screenshot was taken.`);
264
+ }
265
+ let screenshotBase64;
201
266
  try {
202
- const session = SessionManager_1.SESSION_MANAGER.getSession(sessionId);
203
- if (!session) {
204
- logger_1.default.warn(`[DashboardCommands] captureEvidence: Session ${sessionId} not found`);
205
- return this.sendSuccessResponse(response);
206
- }
207
- const screenshotBase64 = yield session.getScreenShot();
208
- if (screenshotBase64) {
209
- const screenshotPath = (0, asset_manager_1.saveScreenShot)(sessionId, screenshotBase64);
210
- // Log the evidence capture
211
- yield prisma_1.prisma.sessionLog.create({
212
- data: {
213
- session_id: sessionId,
214
- command_name: 'captureEvidence',
215
- body: JSON.stringify({ reason, label }),
216
- response: JSON.stringify({ captured: true }),
217
- is_success: true,
218
- is_error: false,
219
- method: 'POST',
220
- title: 'Evidence Captured',
221
- subtitle: reason,
222
- screenshot: screenshotPath,
223
- url: '/execute/sync',
224
- },
225
- });
226
- logger_1.default.info(`[DashboardCommands] Evidence captured for ${sessionId}: ${reason}`);
227
- }
267
+ screenshotBase64 = yield session.getScreenShot();
228
268
  }
229
269
  catch (err) {
230
- logger_1.default.error(`[DashboardCommands] captureEvidence failed: ${err.message}`);
270
+ return notRecorded(`No screenshot could be taken: ${(_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : err}`);
231
271
  }
232
- return this.sendSuccessResponse(response);
272
+ if (!screenshotBase64)
273
+ return notRecorded('No screenshot could be taken.');
274
+ const screenshotPath = (0, asset_manager_1.saveScreenShot)(sessionId, screenshotBase64);
275
+ yield prisma_1.prisma.sessionLog.create({
276
+ data: {
277
+ session_id: sessionId,
278
+ command_name: 'captureEvidence',
279
+ body: JSON.stringify({ reason, label }),
280
+ response: JSON.stringify({ captured: true }),
281
+ is_success: true,
282
+ is_error: false,
283
+ method: 'POST',
284
+ title: 'Evidence Captured',
285
+ subtitle: reason,
286
+ screenshot: screenshotPath,
287
+ url: '/execute/sync',
288
+ },
289
+ });
290
+ logger_1.default.info(`[DashboardCommands] Evidence captured for ${sessionId}: ${reason}`);
291
+ return RECORDED;
233
292
  });
234
293
  }
235
294
  /**
@@ -239,44 +298,29 @@ class DashboardCommands {
239
298
  * or
240
299
  * driver.executeScript("xenon: addTag", "smoke-test")
241
300
  */
242
- addTag(sessionId, request, response) {
301
+ addTag(sessionId, args, tags) {
243
302
  return __awaiter(this, void 0, void 0, function* () {
244
- const { args } = request.body;
245
- const tagData = lodash_1.default.isArray(args) ? args[0] : args;
246
- const newTag = typeof tagData === 'string' ? tagData : tagData.tag;
247
- if (!newTag) {
248
- logger_1.default.warn('[DashboardCommands] addTag: No tag provided');
249
- return this.sendSuccessResponse(response);
303
+ const newTag = stringArg(args[0], 'tag');
304
+ if (!newTag || !newTag.trim()) {
305
+ return notRecorded('addTag needs a tag, as a string or { tag }.');
250
306
  }
251
- try {
252
- // Get current session to append tag
253
- const session = yield prisma_1.prisma.session.findUnique({
254
- where: { id: sessionId },
255
- select: { tags: true },
256
- });
257
- // Parse existing tags or initialize empty array
258
- let existingTags = [];
259
- if (session === null || session === void 0 ? void 0 : session.tags) {
260
- try {
261
- existingTags = JSON.parse(session.tags);
262
- }
263
- catch (_a) {
264
- existingTags = [];
265
- }
307
+ let existingTags = [];
308
+ if (tags) {
309
+ try {
310
+ const parsed = JSON.parse(tags);
311
+ if (Array.isArray(parsed))
312
+ existingTags = parsed;
266
313
  }
267
- // Add new tag if not already present
268
- if (!existingTags.includes(newTag)) {
269
- existingTags.push(newTag);
270
- yield (0, session_service_1.updateSessionDetails)(sessionId, {
271
- tags: JSON.stringify(existingTags),
272
- });
273
- logger_1.default.info(`[DashboardCommands] Tag added to ${sessionId}: ${newTag}`);
314
+ catch (_a) {
315
+ existingTags = [];
274
316
  }
275
317
  }
276
- catch (err) {
277
- logger_1.default.error(`[DashboardCommands] addTag failed: ${err.message}`);
318
+ if (!existingTags.includes(newTag)) {
319
+ existingTags.push(newTag);
320
+ yield (0, session_service_1.updateSessionDetails)(sessionId, { tags: JSON.stringify(existingTags) });
321
+ logger_1.default.info(`[DashboardCommands] Tag added to ${sessionId}: ${newTag}`);
278
322
  }
279
- return this.sendSuccessResponse(response);
323
+ return RECORDED;
280
324
  });
281
325
  }
282
326
  }
@@ -78,6 +78,7 @@ const SocketEvents_1 = require("../enums/SocketEvents");
78
78
  const types_1 = require("../services/healing/types");
79
79
  const SelectorStateService_1 = require("../services/SelectorStateService");
80
80
  const recording_store_1 = require("../services/recording/recording-store");
81
+ const NotificationService_1 = require("../services/NotificationService");
81
82
  const SessionMetricsService_1 = require("../services/metrics/SessionMetricsService");
82
83
  const nodeMetrics_1 = require("../services/metrics/nodeMetrics");
83
84
  const typedi_2 = require("typedi");
@@ -190,8 +191,16 @@ let DashboardEventManager = class DashboardEventManager {
190
191
  }
191
192
  });
192
193
  }
193
- onSessionStopped(sessionId, status, failureReason) {
194
- return __awaiter(this, void 0, void 0, function* () {
194
+ /**
195
+ * Closes a session's record: its end time, its final status, the device's
196
+ * release. Every way a session ends comes here (the client's delete, an
197
+ * inactivity timeout, a driver crash, a heartbeat timeout, a shutdown), so
198
+ * this is where a failed one is announced to the `session_failed` webhooks.
199
+ * `notify: false` records the failure without announcing it: a server
200
+ * shutting down fails the sessions it drains, which no test caused.
201
+ */
202
+ onSessionStopped(sessionId_1, status_1, failureReason_1) {
203
+ return __awaiter(this, arguments, void 0, function* (sessionId, status, failureReason, options = {}) {
195
204
  var _a, _b, _c, _d;
196
205
  // Idempotency Guard: If this session is already being stopped by another actor
197
206
  // (heartbeat, stream watchdog, plugin.deleteSession), skip to avoid double-cleanup.
@@ -341,6 +350,12 @@ let DashboardEventManager = class DashboardEventManager {
341
350
  else if (updateData.status === SessionStatus_1.SessionStatus.FAILED) {
342
351
  typedi_1.Container.get(MetricsService_1.MetricsService).incrementSessionFailure();
343
352
  }
353
+ // Webhooks: not awaited, since a slow or dead webhook must not hold up
354
+ // the end of the session. The service sends once per session even
355
+ // though a session can end twice (a crash, then the client's delete).
356
+ if (updateData.status === SessionStatus_1.SessionStatus.FAILED && options.notify !== false) {
357
+ this.announceFailure(Object.assign(Object.assign({}, sessionEntry), updateData));
358
+ }
344
359
  // Principal Triage: If session failed, perform intelligent failure analysis
345
360
  if (updateData.status === SessionStatus_1.SessionStatus.FAILED) {
346
361
  try {
@@ -363,6 +378,18 @@ let DashboardEventManager = class DashboardEventManager {
363
378
  }
364
379
  });
365
380
  }
381
+ /** Starts the `session_failed` webhooks; never throws and never waits for them. */
382
+ announceFailure(session) {
383
+ var _a;
384
+ try {
385
+ void typedi_1.Container.get(NotificationService_1.NotificationService)
386
+ .notifySessionFailed(session)
387
+ .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}`); });
388
+ }
389
+ catch (err) {
390
+ 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}`);
391
+ }
392
+ }
366
393
  beforeSessionCommand(sessionId, commandName, request, response) {
367
394
  return __awaiter(this, void 0, void 0, function* () {
368
395
  var _a;
@@ -370,16 +397,22 @@ let DashboardEventManager = class DashboardEventManager {
370
397
  this.commandStartTime.set(`${sessionId}:${commandName}`, Date.now());
371
398
  logger_1.default.debug(`[EventManager] beforeSessionCommand: sessionId=${sessionId}, commandName=${commandName}`);
372
399
  }
373
- // Principal Interception: Handle Xenon-specific commands regardless of session state in memory
400
+ // The session-details commands (`xenon: setSessionName`, ...) are answered
401
+ // here, from this server's record of the session, whether or not the
402
+ // session is in memory. Every other `xenon:` script goes on: to the
403
+ // plugin's CommandInterceptor on the server that drives the phone, which
404
+ // a hub reaches by forwarding it. Taking them all here answered a node
405
+ // phone's autowait, Omni-Vision and network-capture scripts with null on
406
+ // the hub, and they never reached the node.
374
407
  if (commandName === 'execute') {
375
408
  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)) {
409
+ if ((0, commands_1.sessionDetailsCommandOf)(script)) {
377
410
  logger_1.default.info(`[EventManager] Intercepting Xenon command: ${script} for session ${sessionId}`);
378
411
  yield commands_1.dashboardCommands.process(sessionId, request, response);
379
412
  return false;
380
413
  }
381
- else if (script && script.includes(':')) {
382
- logger_1.default.debug(`[EventManager] Custom command ${script} not handled by Xenon. Passing to driver.`);
414
+ else if (typeof script === 'string' && script.includes(':')) {
415
+ logger_1.default.debug(`[EventManager] Script ${script} is not a session-details command; passing it on.`);
383
416
  }
384
417
  }
385
418
  const session = SessionManager_1.SESSION_MANAGER.getSession(sessionId);