@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
@@ -0,0 +1,4 @@
1
+ import{r as x,X as S,j as t,e as te,f as $e,t as H,y as Ae,R as Ee,p as De,i as Le,u as Me}from"./index-BbKEXUFH.js";import{C as Oe,d as Fe,p as Ne,e as Pe,r as ye,f as Be,j as we,k as G,c as He,h as re,u as Ie,b as Ue,q as Ve}from"./use-builds-data-J-riQYyC.js";import{A as Je}from"./arrow-left-MMbTmgoE.js";import{C as K}from"./chevron-right-BRYJJVC9.js";import{s as We}from"./labels-C21SA4JG.js";import{B as ze,S as ke,P as $}from"./BugReportButton-BAt0_pzq.js";import{C as Ke}from"./circle-x-DlDJtnVz.js";import{C as qe}from"./circle-check-CqAJjQ5O.js";import{S as F}from"./stat-tile-BV98e_Sv.js";import{C as X}from"./copy-CAQydNFe.js";import{A as Ge}from"./arrow-up-right-DuAftMWB.js";import{A as Xe}from"./arrow-right-BMfnGmxW.js";import{f as Ye,a as se,C as Se,b as Qe,s as Ze,L as et,n as ae}from"./line-chart-D2DyJCml.js";import{E as M}from"./EmptyState-BSjhk_IV.js";import{A as tt}from"./activity-DOVdsfMQ.js";import{V as rt}from"./video-off-DDsEISjV.js";import{D as _e}from"./download-C9XG6lLv.js";import{T as st,a as at,b as ne,c as E,d as nt,e as D}from"./Table-BcTm5xV2.js";import{M as it}from"./Modal-CFCp3n9d.js";import{N as ot}from"./network-ebT86qoi.js";import"./time-D-gjmE1o.js";import"./index-DJGc1M2f.js";const lt=4e3,ie=e=>!!e&&typeof e=="object"&&typeof e.id=="string"&&typeof e.status=="string";function ct(e){const[r,s]=x.useState(null),[a,n]=x.useState([]),[o,l]=x.useState([]),[i,c]=x.useState([]),[u,b]=x.useState([]),[h,v]=x.useState(!0),[y,g]=x.useState(null),[N,w]=x.useState(!1),[T,A]=x.useState(0),_=x.useRef(null);x.useEffect(()=>{if(!e){s(null),n([]),l([]),c([]),b([]),w(!1),v(!1);return}let p=!0;return _.current!==e&&v(!0),Promise.all([S.getSession(e),S.getSessionLogs(e),S.getDeviceLogs(e),S.getDebugLogs(e),S.getProfilingData(e).catch(()=>[])]).then(([f,C,d,j,L])=>{if(!p)return;const O=ie(f);s(O?f:null),O?(_.current=e,w(!1),g(null)):(w(!!f&&typeof f=="object"&&(f.error===!0||typeof f.message=="string")),g(typeof(f==null?void 0:f.message)=="string"?f.message:"Session not found")),n(Array.isArray(C)?C:[]),l(Array.isArray(d)?d:[]),c(Array.isArray(j)?j:[]),b(Array.isArray(L)?L:[])}).catch(f=>{p&&g(f instanceof Error?f.message:String(f))}).finally(()=>{p&&v(!1)}),()=>{p=!1}},[e,T]);const m=(r==null?void 0:r.status)==="running";return x.useEffect(()=>{if(!e||!m)return;let p=!0;const f=setInterval(async()=>{try{const[C,d]=await Promise.all([S.getSession(e),S.getSessionLogs(e)]);if(!p||!ie(C))return;s(C),Array.isArray(d)&&n(d),C.status!=="running"&&A(j=>j+1)}catch{}},lt);return()=>{p=!1,clearInterval(f)}},[e,m]),{session:r,sessionLogs:a,deviceLogs:o,debugLogs:i,profiling:u,loading:h,error:y,notFound:N,refresh:()=>A(p=>p+1)}}const oe=({buildId:e,buildName:r,sessionId:s})=>t.jsxs("nav",{"aria-label":"Breadcrumb",className:"flex items-center gap-2 px-4 py-2 border-b border-[var(--border)] bg-[var(--surface)]",children:[t.jsxs(te,{to:"/builds",className:"inline-flex items-center gap-1.5 text-xs text-[var(--text-muted)] hover:text-[var(--text)] transition-colors",children:[t.jsx(Je,{className:"h-3.5 w-3.5"}),"Sessions"]}),t.jsx(K,{className:"h-3 w-3 text-[var(--text-dim)]"}),r&&t.jsxs(t.Fragment,{children:[t.jsx(te,{to:`/builds/${e}`,className:"text-[11px] text-[var(--color-accent)] hover:underline",children:r}),t.jsx(K,{className:"h-3 w-3 text-[var(--text-dim)]"})]}),t.jsxs("span",{className:"font-mono text-[11px] text-[var(--text-muted)]",title:s,children:["#",s.slice(0,8)]})]}),dt={passed:{label:"Passed",Icon:qe,cls:"text-[var(--color-success)]"},failed:{label:"Failed",Icon:Ke,cls:"text-[var(--color-danger)]"},running:{label:"Running",Icon:$e,cls:"text-[var(--color-warning)]"},other:{Icon:Oe,cls:"text-[var(--text-dim)]"}};function Y(e){const r=He(e),s=dt[r];return{bucket:r,...s,label:s.label??We(e||"unknown")}}const xt=({session:e})=>{var o,l;const r=Y(e.status),s=Fe(e),a=[Ne(e),e.device_version].filter(i=>i&&i!=="—").join(" "),n=[[Pe(e),a].filter(Boolean).join(" · "),ye(e.ranOn),(o=e.owner)==null?void 0:o.name,Be(e.startTime),we(G(e))].filter(i=>i&&i!=="—");return t.jsxs("header",{className:"flex items-start justify-between gap-4 px-4 py-4 border-b border-[var(--border)] bg-[var(--surface)]",children:[t.jsxs("div",{className:"min-w-0",children:[t.jsxs("div",{className:"flex items-center gap-2 min-w-0",children:[t.jsxs("span",{className:`inline-flex items-center gap-1.5 text-xs font-medium shrink-0 ${r.cls}`,children:[t.jsx(r.Icon,{className:`h-3.5 w-3.5 ${r.bucket==="running"?"animate-spin":""}`,"aria-hidden":"true"}),r.label]}),t.jsx("h1",{className:`text-lg font-semibold truncate ${s.source==="name"?"text-[var(--text)]":"text-[var(--text-muted)]"}`,title:s.source==="name"?s.text:`${s.text}
2
+ Unnamed session. Set xe:options.name in its capabilities to name it.`,children:s.text})]}),t.jsx("p",{"data-testid":"outcome-meta",className:"mt-1 text-xs text-[var(--text-muted)] tabular-nums truncate",title:((l=e.owner)==null?void 0:l.email)??void 0,children:n.join(" · ")})]}),t.jsx("div",{className:"shrink-0",children:t.jsx(ze,{sessionId:e.id,mode:"full",variant:"inline"})})]})};function mt(e,r=8){if(!e)return[];const s=e.split(/\r?\n/),a=[];for(const n of s){const o=n.trim();if(/^at\s+/.test(o)&&(a.push(o),a.length>=r))break}return a}function Q(e){return e?e.split(/[_\s]+/).filter(Boolean).map(r=>r.charAt(0).toUpperCase()+r.slice(1).toLowerCase()).join(" "):""}function le(e){if(!e)return{};try{const r=JSON.parse(e);return r&&typeof r=="object"&&!Array.isArray(r)?r:{}}catch{return{}}}function ce(e,r=200){if(e==null)return"";if(typeof e=="string")return e;if(typeof e=="number"||typeof e=="boolean")return String(e);try{const s=JSON.stringify(e);return s.length>r?s.slice(0,r)+"…":s}catch{return String(e)}}const I=e=>e.command_name||e.title||"command",q=e=>{const r=e.createdAt?Date.parse(e.createdAt):NaN;return Number.isFinite(r)?r:0},Z=e=>[...e].sort((r,s)=>q(r)-q(s));function U(e){return e.is_success===!1||e.is_error===!0}function Ce(e){const r=[];let s=null;for(const a of Z(e))a.is_healed&&a.healing_tier&&!r.includes(a.healing_tier)&&r.push(a.healing_tier),typeof a.duration=="number"&&(!s||a.duration>s.ms)&&(s={name:I(a),ms:a.duration});return{total:e.length,failed:e.filter(U).length,healed:e.filter(a=>a.is_healed).length,healedTiers:r,slowest:s,screenshots:e.filter(a=>a.screenshot).length}}const de=(e,r)=>r?e?`${e}=${r}`:r:null;function ut(e){return Z(e.filter(r=>r.is_healed)).map(r=>({id:r.id,command:I(r),original:de(r.original_strategy,r.original_selector),healed:de(r.healed_strategy,r.healed_selector),tier:r.healing_tier??null,confidence:typeof r.healing_confidence=="number"?Math.round(r.healing_confidence*100):null,at:r.createdAt}))}function ft(e){var r;if(!e.response)return null;try{const s=(r=JSON.parse(e.response))==null?void 0:r.value;if(!s||typeof s!="object")return null;const a=typeof s.message=="string"?s.message.trim():"";return a||(typeof s.error=="string"&&s.error?s.error:null)}catch{return null}}function ht(e){if(e.length===0)return{spanMs:0,bars:[]};const r=e.map(n=>{const o=q(n),l=typeof n.duration=="number"&&n.duration>0?n.duration:0;return{l:n,start:o-l,end:o,durationMs:l}}),s=Math.min(...r.map(n=>n.start)),a=Math.max(...r.map(n=>n.end));return{spanMs:Math.max(1,a-s),bars:r.sort((n,o)=>n.start-o.start).map(({l:n,start:o,durationMs:l})=>({id:n.id,label:I(n),startMs:o-s,durationMs:l,failed:U(n),healed:!!n.is_healed}))}}function pt(e){return Z(e.filter(r=>r.screenshot)).map(r=>({id:r.id,path:r.screenshot,command:I(r),at:r.createdAt,failed:U(r)}))}function B(e){if(e==null||!Number.isFinite(e)||e<0)return"—";if(e<1e3)return`${Math.round(e)}ms`;if(e<6e4)return`${(e/1e3).toFixed(1)}s`;const r=Math.round(e/1e3);return`${Math.floor(r/60)}m ${r%60}s`}const vt=({session:e,commands:r})=>{var l,i;const s=Y(e.status),a=Ce(r),n=we(G(e)),o=s.bucket==="failed"?Q(e.failure_category)||"No category recorded":s.bucket==="running"?`Running for ${n}`:`Ran ${n}`;return t.jsxs("div",{className:"grid grid-cols-4 gap-3",children:[t.jsx(F,{label:"Result",value:s.label,valueCls:s.cls,note:o,noteTone:s.bucket==="failed"?"bad":"neutral"}),t.jsx(F,{label:"Commands",value:a.total,note:a.failed>0?`${a.failed} failed`:"None failed",noteTone:a.failed>0?"bad":"neutral"}),t.jsx(F,{label:"Self-healing",value:a.healed>0?`${a.healed} healed`:"None",note:a.healed>0?a.healedTiers.join(", ")||"Tier not recorded":"No selector needed healing"}),t.jsx(F,{label:"Slowest command",value:B((l=a.slowest)==null?void 0:l.ms),note:((i=a.slowest)==null?void 0:i.name)??"No timings recorded"})]})};function bt(e){const r=e.filter(U),s=r[r.length-1];return s?{command:s.command_name||s.title||"command",message:ft(s)}:null}function gt(e,r,s,a,n){var u;const o=Q(e.failure_category),l=((u=e.device_name)==null?void 0:u.trim())||"Unknown Device",i=e.device_version?` ${e.device_version}`:"",c=n?`${n.command}${n.message?`: ${n.message}`:""}`:"(no failed command recorded)";return[`**Session:** ${e.id}`,`**Build:** ${r} (#${s})`,`**Device:** ${l} (${e.node_id||"unknown-node"}) · ${e.device_platform||"unknown"}${i}`,`**Duration:** ${a}`,`**Failure category:** ${o||"(uncategorized)"}`,`**Failure reason:** ${e.failure_reason||"(not set)"}`,`**First failed command:** ${c}`,...e.ai_analysis?[`**AI analysis:** ${e.ai_analysis}`]:[]].join(`
3
+ `)}function jt({text:e}){return t.jsx(t.Fragment,{children:e.split(/(\*\*[^*]+\*\*|`[^`]+`)/g).map((r,s)=>/^\*\*[^*]+\*\*$/.test(r)?t.jsx("strong",{children:r.slice(2,-2)},s):/^`[^`]+`$/.test(r)?t.jsx("code",{className:"font-mono text-[11px] px-1 rounded bg-[var(--surface-2)]",children:r.slice(1,-1)},s):t.jsx(Ee.Fragment,{children:r},s))})}const P=({children:e})=>t.jsx("div",{className:"text-[11px] font-semibold text-[var(--text-dim)] mb-1",children:e}),Nt=({session:e,buildName:r,buildId:s,commands:a,durationText:n})=>{var N;const{toast:o}=H(),[l,i]=x.useState(!1),c=Q(e.failure_category),u=bt(a),b=mt(e.failure_reason),h=(N=e.ai_analysis)==null?void 0:N.trim(),v=!!h&&h.length>480,y=async()=>{const w=gt(e,r,s,n,u);try{await navigator.clipboard.writeText(w),o("Failure report copied","success")}catch{o("Clipboard unavailable","error")}},g=e.failure_category?`/xenon/runbooks/${encodeURIComponent(e.failure_category.toLowerCase())}`:void 0;return t.jsxs("section",{className:"rounded-lg border border-[var(--red)]/30 bg-[var(--surface)] overflow-hidden",children:[t.jsxs("header",{className:"flex items-center justify-between gap-4 px-4 py-3 border-b border-[var(--border)] bg-[var(--red)]/5",children:[t.jsxs("div",{className:"flex items-center gap-2",children:[t.jsx(Ae,{className:"h-4 w-4 text-[var(--red)]","aria-hidden":"true"}),t.jsx("h2",{className:"text-sm font-semibold text-[var(--red)]",children:"Why it failed"}),c&&t.jsx("span",{className:"text-[11px] text-[var(--text-dim)] ml-2",children:c})]}),t.jsxs("div",{className:"flex items-center gap-2",children:[t.jsxs("button",{type:"button",onClick:y,className:"inline-flex items-center gap-1.5 h-7 px-2.5 rounded-md border border-[var(--border)] text-xs text-[var(--text)] hover:border-[var(--border-strong)] hover:bg-[var(--surface-2)] transition-colors",children:[t.jsx(X,{className:"h-3 w-3"}),"Copy"]}),t.jsxs("a",{...g?{href:g,target:"_blank",rel:"noreferrer"}:{"aria-disabled":!0,onClick:w=>w.preventDefault()},className:`inline-flex items-center gap-1.5 h-7 px-2.5 rounded-md border border-[var(--border)] text-xs text-[var(--text)] hover:border-[var(--border-strong)] hover:bg-[var(--surface-2)] transition-colors ${g?"":"opacity-50 cursor-not-allowed"}`,title:g?"Open runbook in new tab":"No runbook — failure category not set",children:[t.jsx(Ge,{className:"h-3 w-3"}),"Open runbook"]})]})]}),t.jsxs("div",{className:"px-4 py-4 space-y-4",children:[t.jsxs("div",{children:[t.jsx(P,{children:"Reason"}),t.jsx("div",{className:"text-sm text-[var(--text)] break-words",children:e.failure_reason||t.jsx("span",{className:"text-[var(--text-dim)]",children:"(not set)"})})]}),t.jsxs("div",{children:[t.jsx(P,{children:"First failed command"}),u?t.jsxs("div",{className:"flex items-baseline gap-2 min-w-0 text-sm",children:[t.jsx("span",{className:"font-mono text-xs text-[var(--color-danger)] shrink-0",children:u.command}),u.message&&t.jsx("span",{className:"text-[var(--text-muted)] break-words min-w-0",children:u.message})]}):t.jsx("span",{className:"text-sm text-[var(--text-dim)]",children:"No failed command recorded."})]}),h&&t.jsxs("div",{"data-testid":"ai-analysis",children:[t.jsx(P,{children:t.jsxs("span",{className:"inline-flex items-center gap-1.5",children:[t.jsx(ke,{className:"h-3 w-3","aria-hidden":"true"}),"AI analysis"]})}),t.jsx("div",{className:`text-sm text-[var(--text)] whitespace-pre-wrap break-words ${v&&!l?"line-clamp-6":""}`,children:t.jsx(jt,{text:h})}),v&&t.jsx("button",{type:"button",onClick:()=>i(w=>!w),"aria-expanded":l,className:"mt-1 text-xs text-[var(--color-accent)] hover:underline",children:l?"Show less":"Show all"})]}),b.length>0&&t.jsxs("div",{children:[t.jsx(P,{children:"Stack trace"}),t.jsx("pre",{className:"rounded-md border border-[var(--border)] bg-[var(--bg)] p-3 font-mono text-[11px] leading-relaxed text-[var(--color-accent)] overflow-x-auto",children:b.join(`
4
+ `)})]})]})]})};function yt(e){return e>=1e3?`${(e/1e3).toFixed(1).replace(/\.0$/,"")}k`:String(e)}function V(e){const r=e;return typeof r.message=="string"&&!r.command_name&&!r.title}const wt=/^\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}\.\d+\s+\d+\s+\d+\s+([VDIWEF])\s/,kt=/^([VDIWEF])\/[^(:]*[(:]/,St=/<(Error|Fault|Warning)>/;function Te(e){var a,n,o;const r=((a=wt.exec(e))==null?void 0:a[1])??((n=kt.exec(e))==null?void 0:n[1]);if(r)return r==="E"||r==="F"?"error":r==="W"?"warn":null;const s=(o=St.exec(e))==null?void 0:o[1];return s?s==="Warning"?"warn":"error":null}function J(e){const r=e.timestamp;if(!r){const l=e.createdAt;return l?J({timestamp:l}):"--:--:--"}const s=typeof r=="number"?new Date(r):new Date(String(r));if(Number.isNaN(s.getTime()))return"--:--:--";const a=String(s.getHours()).padStart(2,"0"),n=String(s.getMinutes()).padStart(2,"0"),o=String(s.getSeconds()).padStart(2,"0");return`${a}:${n}:${o}`}function _t(e){if(V(e)){const s=Te(e.message);return s==="error"?{label:"error",tone:"red"}:s==="warn"?{label:"warn",tone:"amber"}:{label:"",tone:"neutral"}}if(e.is_healed===!0)return{label:String(e.command_name||"healed"),tone:"amber"};if(e.is_success===!1||e.is_error===!0)return{label:String(e.command_name||e.title||"error"),tone:"red"};if(e.is_success===!0)return{label:String(e.command_name||e.title||"ok"),tone:"green"};const r=e.title||e.event||e.command_name;return typeof r=="string"?/stop|fail|error/i.test(r)?{label:r,tone:"red"}:/start|begin|init/i.test(r)?{label:r,tone:"green"}:{label:r,tone:"blue"}:{label:"log",tone:"neutral"}}function Ct(e){if(e==null)return"";if(typeof e=="string")try{return JSON.stringify(JSON.parse(e),null,2)}catch{return e}try{return JSON.stringify(e,null,2)}catch{return String(e)}}function Tt(e){if(!e||typeof e!="string")return[{text:String(e??""),kind:"plain"}];const r=[],s=/("(?:\\.|[^"\\])*")(\s*:)|("(?:\\.|[^"\\])*")|(-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)|\b(true|false)\b|\b(null)\b|([{}[\],])/g;let a=0,n;for(;(n=s.exec(e))!==null;)n.index>a&&r.push({text:e.slice(a,n.index),kind:"plain"}),n[1]&&n[2]!==void 0?(r.push({text:n[1],kind:"key"}),r.push({text:n[2],kind:"punct"})):n[3]?r.push({text:n[3],kind:"string"}):n[4]?r.push({text:n[4],kind:"number"}):n[5]?r.push({text:n[5],kind:"boolean"}):n[6]?r.push({text:n[6],kind:"null"}):n[7]&&r.push({text:n[7],kind:"punct"}),a=s.lastIndex;return a<e.length&&r.push({text:e.slice(a),kind:"plain"}),r}function Rt(e){if(V(e))return e.message.split(/\r?\n/,1)[0].trimEnd();const r=e,s=typeof r.title=="string"&&r.title.trim()?r.title.trim():null,a=typeof r.command_name=="string"&&r.command_name.trim()?r.command_name.trim():null;return s??a??"Command"}function xe(e){const r=e.subtitle;return typeof r=="string"&&r.trim()?r.trim():null}function $t(e,r){return r?e.filter(s=>s.is_success===!1||s.is_error===!0||V(s)&&Te(s.message)==="error"):e}const At=({commands:e})=>{const r=ut(e);return r.length===0?null:t.jsxs("section",{className:"rounded-lg border border-[var(--border)] bg-[var(--surface)] overflow-hidden",children:[t.jsxs("header",{className:"flex items-center gap-2 px-4 py-3 border-b border-[var(--border)]",children:[t.jsx(ke,{className:"h-4 w-4 text-[var(--color-warning)]","aria-hidden":"true"}),t.jsx("h2",{className:"text-sm font-semibold text-[var(--text)]",children:"Self-healing"}),t.jsxs("span",{className:"text-[11px] text-[var(--text-muted)]",children:[r.length," selector",r.length===1?"":"s"," healed. Update the test so the next run doesn't need to."]})]}),t.jsxs("table",{className:"w-full table-fixed text-left",children:[t.jsx("thead",{children:t.jsxs("tr",{className:"border-b border-[var(--border)] text-[11px] text-[var(--text-dim)]",children:[t.jsx("th",{className:"px-4 py-2 font-medium w-[88px]",children:"Time"}),t.jsx("th",{className:"px-3 py-2 font-medium w-[128px]",children:"Command"}),t.jsx("th",{className:"px-3 py-2 font-medium",children:"Asked for"}),t.jsx("th",{className:"w-[28px]",children:t.jsx("span",{className:"sr-only",children:"healed to"})}),t.jsx("th",{className:"px-3 py-2 font-medium",children:"Healed to"}),t.jsx("th",{className:"px-3 py-2 font-medium w-[112px]",children:"Tier"}),t.jsx("th",{className:"px-4 py-2 font-medium w-[96px] text-right",children:"Confidence"})]})}),t.jsx("tbody",{children:r.map((s,a)=>t.jsxs("tr",{className:"border-b border-[var(--border)] last:border-b-0 align-top",children:[t.jsx("td",{className:"px-4 py-2 text-xs text-[var(--text-muted)] tabular-nums",children:J({timestamp:s.at??null})}),t.jsx("td",{className:"px-3 py-2 text-xs text-[var(--text)] truncate",children:s.command}),t.jsx("td",{className:"px-3 py-2 font-mono text-[11px] text-[var(--text-muted)] break-all",children:s.original??"—"}),t.jsx("td",{className:"py-2 text-[var(--text-dim)]",children:t.jsx(Xe,{className:"h-3.5 w-3.5","aria-hidden":"true"})}),t.jsx("td",{className:"px-3 py-2 font-mono text-[11px] text-[var(--text)] break-all",children:s.healed??"—"}),t.jsx("td",{className:"px-3 py-2 text-xs text-[var(--text)]",children:s.tier??"—"}),t.jsx("td",{className:"px-4 py-2 text-xs text-[var(--text)] tabular-nums text-right",children:s.confidence===null?"—":`${s.confidence}%`})]},s.id??a))})]})]})},Et=1e4,me="var(--color-info)",ue="var(--color-accent)",Dt="var(--border-strong)";function fe(e){return e>=1024?ae(e/1024)*1024:ae(e)}const he=e=>{let r=0;for(const s of e)for(const a of s.values)a!==null&&a>r&&(r=a);return r},z=({title:e,series:r,times:s,yMax:a,format:n,ariaLabel:o})=>t.jsxs("section",{"aria-label":e,children:[t.jsxs("div",{className:"mb-2 flex flex-wrap items-baseline gap-x-4 gap-y-1",children:[t.jsx("h3",{className:"text-xs font-semibold text-[var(--text)]",children:e}),r.map(l=>{const i=Ze(l.values);return t.jsxs("span",{className:"flex min-w-0 items-center gap-1.5 text-[11px] tabular-nums text-[var(--text-muted)]",children:[t.jsx("span",{className:"inline-block h-0.5 w-3 shrink-0",style:{background:l.color}}),t.jsx("span",{className:"max-w-[16rem] truncate",title:l.label,children:l.label}),i&&(l.dashed?t.jsx("span",{children:n(i.peak)}):t.jsxs("span",{children:["peak ",n(i.peak)," · avg ",n(i.average)]}))]},l.key)})]}),t.jsx(et,{times:s,series:r,yMax:a,formatValue:n,ariaLabel:o})]}),Lt=({sessionId:e,running:r,hasTrace:s})=>{const[a,n]=x.useState(null),[o,l]=x.useState(!1);x.useEffect(()=>{let m=!0;const p=()=>S.getSessionMetrics(e).then(C=>{m&&(n(Qe(C)),l(!0))}).catch(()=>{m&&l(!0)});if(p(),!r)return()=>{m=!1};const f=setInterval(()=>void p(),Et);return()=>{m=!1,clearInterval(f)}},[e,r]);const i=(a==null?void 0:a.samples)??[],c=x.useMemo(()=>i.map(m=>{var p;return m.t-(((p=i[0])==null?void 0:p.t)??0)}),[i]),u=new Set(i.map(m=>m.app).filter(m=>!!m)),b=u.size>1?"Foreground app":Array.from(u)[0]??(a==null?void 0:a.appId)??"App",h=i.map(m=>m.app??null),v=m=>m>0&&!!h[m]&&!!h[m-1]&&h[m]!==h[m-1],y=[],g=[],N=[];a&&(a.series.deviceCpu&&y.push({key:"deviceCpu",label:"Device",color:me,values:i.map(m=>m.deviceCpu)}),a.series.appCpu&&y.push({key:"appCpu",label:b,color:ue,values:i.map((m,p)=>v(p)?null:m.appCpu),pointLabels:h}),a.series.appMem&&g.push({key:"appMem",label:b,color:ue,values:i.map((m,p)=>v(p)?null:m.appMemMb),pointLabels:h}),a.series.deviceMem&&(N.push({key:"deviceMem",label:"Device used",color:me,values:i.map(m=>m.deviceMemMb)}),N.push({key:"deviceTotal",label:"Device total",color:Dt,values:i.map(m=>m.deviceMemTotalMb),dashed:!0})));const w=r&&(a==null?void 0:a.recording)==="off",T=(a==null?void 0:a.recording)==="stopped",A="Recording stopped: the device stopped responding.";let _;return o?w?_=t.jsx(M,{title:"Performance isn't recorded for this session",description:"CPU and memory are recorded for Android devices and iPhones. This session runs on an iOS simulator or on a machine that needs updating, or recording is turned off."}):i.length===0&&!r?_=t.jsx(M,{title:"No performance data for this session",description:"CPU and memory are recorded for Android devices and iPhones. This session ran on an iOS simulator or on a machine that needed updating, before recording was available, or with recording turned off."}):i.length<2?_=t.jsx("div",{className:"text-xs text-[var(--text-dim)]",children:T?A:r?"Collecting… The first figures appear within 10 seconds.":"Too few figures were recorded to draw a chart."}):_=t.jsxs("div",{className:"space-y-4",children:[T&&t.jsxs("p",{className:"text-xs text-[var(--color-warning)]",children:[A," The data ends there."]}),(a==null?void 0:a.platform)==="ios"&&t.jsxs("p",{className:"text-xs text-[var(--text-dim)]",children:["On iPhones, only the device's overall CPU is recorded.",s?" For more detail, download the Performance trace under Details.":""]}),y.length>0&&t.jsx(z,{title:"CPU",series:y,times:c,yMax:100,format:Ye,ariaLabel:"CPU over the session"}),g.length>0&&t.jsx(z,{title:"App memory",series:g,times:c,yMax:fe(he(g)),format:se,ariaLabel:"App memory over the session"}),N.length>0&&t.jsx(z,{title:"Device memory",series:N,times:c,yMax:fe(he(N)),format:se,ariaLabel:"Device memory over the session"})]}):_=t.jsx("div",{className:"text-xs text-[var(--text-dim)]",children:"Loading performance…"}),t.jsx(Se,{header:t.jsxs("span",{className:"flex items-center gap-2",children:[t.jsx(tt,{size:14})," Performance"]}),children:_})},Mt=({session:e})=>{const r=e.status==="running",s=!!e.video_recording;return t.jsxs("section",{className:"rounded-lg border border-[var(--border)] bg-[var(--surface)] overflow-hidden",children:[t.jsxs("header",{className:"px-3 py-2 border-b border-[var(--border)] flex items-center justify-between",children:[t.jsx("span",{className:"text-[11px] font-semibold text-[var(--text-dim)]",children:"Recording"}),r&&t.jsxs("span",{className:"inline-flex items-center gap-1.5 text-[11px] text-[var(--red)]",children:[t.jsx("span",{className:"h-1.5 w-1.5 rounded-full bg-[var(--red)] pulse-dot"}),"Live"]})]}),t.jsx("div",{className:"aspect-video bg-[var(--bg)] flex items-center justify-center",children:r&&e.has_live_video?t.jsx("img",{src:S.getSessionLiveVideoUrl(e.id),alt:`Live view of session ${e.id}`,className:"w-full h-full object-contain"}):s?t.jsx("video",{controls:!0,src:S.getAssetUrl(e.video_recording),className:"w-full h-full object-contain"}):t.jsxs("div",{className:"flex flex-col items-center text-center px-6 py-8 gap-2",children:[t.jsx("div",{className:"h-10 w-10 rounded-full bg-[var(--surface-2)] border border-[var(--border)] flex items-center justify-center",children:t.jsx(rt,{className:"h-5 w-5 text-[var(--text-dim)]"})}),t.jsx("div",{className:"text-sm text-[var(--text)] font-medium",children:"No video available"}),t.jsx("div",{className:"text-xs text-[var(--text-dim)] max-w-xs",children:e.status==="failed"?"Session failed before recording started.":e.video_recording_enabled===!1?"Recording was disabled for this session.":"No recording was captured."})]})})]})},R=({label:e,children:r,title:s,mono:a})=>t.jsxs("div",{className:"flex items-baseline justify-between gap-3 px-3 py-2",children:[t.jsx("dt",{className:"text-xs text-[var(--text-muted)] shrink-0",children:e}),t.jsx("dd",{className:`text-xs text-[var(--text)] text-right min-w-0 break-all ${a?"font-mono text-[11px]":""}`,title:s,children:r})]}),Ot=({session:e,buildName:r})=>{var l,i;const{toast:s}=H(),a=ye(e.ranOn),n=e.performance_trace?S.getAssetUrl(e.performance_trace):"",o=async()=>{try{await navigator.clipboard.writeText(e.id),s("Session ID copied","success")}catch{s("Clipboard unavailable","error")}};return t.jsxs("section",{className:"rounded-lg border border-[var(--border)] bg-[var(--surface)] overflow-hidden",children:[t.jsx("header",{className:"px-3 py-2 border-b border-[var(--border)]",children:t.jsx("h2",{className:"text-[11px] font-semibold text-[var(--text-dim)]",children:"Details"})}),t.jsxs("dl",{className:"divide-y divide-[var(--border)]",children:[t.jsx(R,{label:"Session ID",mono:!0,children:t.jsxs("span",{className:"inline-flex items-center gap-1",children:[t.jsx("span",{children:e.id}),t.jsx("button",{type:"button",onClick:o,"aria-label":"Copy session ID",className:"p-0.5 rounded text-[var(--text-dim)] hover:text-[var(--text)]",children:t.jsx(X,{className:"h-3 w-3"})})]})}),r&&t.jsx(R,{label:"Build",children:r}),t.jsx(R,{label:"Device UDID",mono:!0,children:e.device_udid||"—"}),t.jsx(R,{label:"Platform",children:[Ne(e),e.device_version].filter(c=>c&&c!=="—").join(" ")||"—"}),t.jsx(R,{label:"Ran on",title:e.node_id?`Node ${e.node_id}`:void 0,children:a??"Not known"}),t.jsx(R,{label:"Run by",title:(l=e.owner)==null?void 0:l.email,children:((i=e.owner)==null?void 0:i.name)??"Not recorded"}),t.jsx(R,{label:"Started",children:re(e.startTime)}),t.jsx(R,{label:"Ended",children:e.endTime?re(e.endTime):"—"}),e.tags&&t.jsx(R,{label:"Tags",children:e.tags}),n&&t.jsx(R,{label:"iOS trace",children:t.jsxs("a",{href:n,className:"inline-flex items-center gap-1 text-[var(--color-accent)] hover:underline",children:[t.jsx(_e,{className:"h-3 w-3","aria-hidden":"true"}),"Performance trace"]})})]})]})},pe={desired:"Requested",session:"Actual"},Ft=({session:e})=>{const[r,s]=x.useState("desired"),a=le(e.desired_capabilities),n=le(e.session_capabilities),l=Object.entries(r==="desired"?a:n);return t.jsxs("section",{className:"rounded-lg border border-[var(--border)] bg-[var(--surface)] overflow-hidden",children:[t.jsx("header",{className:"flex items-center border-b border-[var(--border)]",children:["desired","session"].map(i=>t.jsx("button",{type:"button",onClick:()=>s(i),"aria-pressed":r===i,className:`flex-1 h-9 text-[11px] font-semibold transition-colors border-b-2 ${r===i?"border-[var(--color-accent)] text-[var(--text)]":"border-transparent text-[var(--text-muted)] hover:text-[var(--text)]"}`,children:pe[i]},i))}),l.length===0?t.jsxs("div",{className:"px-3 py-6 text-center text-xs text-[var(--text-dim)]",children:["No ",pe[r].toLowerCase()," capabilities recorded for this session."]}):t.jsx("dl",{className:"divide-y divide-[var(--border)]",children:l.map(([i,c])=>t.jsxs("div",{className:"grid grid-cols-[minmax(0,1fr)_minmax(0,1fr)] items-baseline gap-3 px-3 py-2",children:[t.jsx("dt",{className:"text-xs text-[var(--text-muted)] break-all",children:i}),t.jsx("dd",{className:"font-mono text-xs text-[var(--text)] truncate text-right min-w-0",title:ce(c,500),children:ce(c)})]},i))})]})},Pt=({tabs:e,active:r,onChange:s,errorsOnly:a,onErrorsOnlyChange:n,showErrorsOnly:o=!0})=>{const l=e.filter(i=>!(i.hideWhenEmpty&&i.count===0));return t.jsxs("div",{className:"flex items-center justify-between gap-4 px-3 border-b border-[var(--border)] bg-[var(--surface)]",children:[t.jsx("div",{className:"flex items-center gap-1 overflow-x-auto",children:l.map(i=>{const c=i.key===r;return t.jsxs("button",{type:"button",onClick:()=>s(i.key),"aria-pressed":c,"data-empty":i.count===0?"true":"false",className:`relative inline-flex items-center gap-1.5 h-9 px-3 text-xs whitespace-nowrap transition-colors ${c?"text-[var(--text)]":i.count===0?"text-[var(--text-dim)] hover:text-[var(--text-muted)]":"text-[var(--text-muted)] hover:text-[var(--text)]"}`,children:[t.jsx("span",{children:i.label}),i.count>0&&t.jsx("span",{className:`text-[10px] tabular-nums ${c?"text-[var(--text-muted)]":"text-[var(--text-dim)]"}`,children:yt(i.count)}),c&&t.jsx("span",{className:"absolute left-2 right-2 bottom-0 h-[2px] bg-[var(--color-accent)] rounded-t"})]},i.key)})}),o&&t.jsxs("label",{className:"inline-flex items-center gap-2 text-[11px] text-[var(--text-muted)] cursor-pointer select-none whitespace-nowrap",children:[t.jsx("input",{type:"checkbox",checked:a,onChange:i=>n(i.target.checked),"aria-label":"Show only error rows"}),"Errors only"]})]})},Bt={key:"text-[var(--blue)]",string:"text-[var(--color-accent)]",number:"text-[var(--amber)]",boolean:"text-[var(--amber)]",null:"text-[var(--text-dim)] italic",punct:"text-[var(--text-muted)]",plain:"text-[var(--text)]"},Ht=({value:e,maxHeightPx:r})=>{const s=Ct(e);if(!s)return t.jsx("span",{className:"text-[var(--text-dim)] text-xs",children:"(empty)"});let a;try{a=Tt(s)}catch{a=[{text:s,kind:"plain"}]}return t.jsx("pre",{className:"font-mono text-[11px] leading-relaxed bg-[var(--bg)] border border-[var(--border)] rounded-md p-3 overflow-x-auto whitespace-pre",style:r?{maxHeight:r,overflowY:"auto"}:void 0,children:a.map((n,o)=>t.jsx("span",{className:Bt[n.kind],children:n.text},o))})},It={green:"bg-[var(--color-success)]",red:"bg-[var(--red)]",amber:"bg-[var(--amber)]",blue:"bg-[var(--blue)]",neutral:"bg-[var(--text-dim)]"},Ut={green:"text-[var(--color-success)]",red:"text-[var(--red)]",amber:"text-[var(--amber)]",blue:"text-[var(--blue)]",neutral:"text-[var(--text-muted)]"},Vt=({log:e})=>{const[r,s]=x.useState(!1),{toast:a}=H(),n=J(e),o=_t(e),l=V(e),i=l?String(e.message):"",c=async u=>{u.stopPropagation();try{const b=l?i:e.body??e,h=typeof b=="string"?b:JSON.stringify(b,null,2);await navigator.clipboard.writeText(h),a("Log entry copied","success")}catch{a("Clipboard unavailable","error")}};return t.jsxs("div",{className:"border-b border-[var(--border)] last:border-b-0",children:[t.jsxs("button",{type:"button",onClick:()=>s(u=>!u),"aria-expanded":r,className:"group w-full flex items-center gap-2 px-3 py-2 text-left hover:bg-[var(--surface-2)]/60 transition-colors",children:[t.jsx(K,{className:`h-3 w-3 text-[var(--text-dim)] transition-transform shrink-0 ${r?"rotate-90":""}`}),t.jsx("span",{className:"font-mono text-[11px] text-[var(--text-muted)] w-[64px] shrink-0",children:n}),t.jsx("span",{className:`h-1.5 w-1.5 rounded-full shrink-0 ${It[o.tone]}`}),o.label&&t.jsx("span",{className:`font-mono text-[11px] font-medium shrink-0 ${Ut[o.tone]}`,children:o.label}),t.jsxs("span",{className:"min-w-0 flex-1 flex items-baseline gap-2 truncate",children:[t.jsx("span",{className:l?"font-mono text-[11px] text-[var(--text)] truncate min-w-0":"text-[12px] text-[var(--text)] shrink-0",children:Rt(e)}),xe(e)&&t.jsx("span",{className:"font-mono text-[11px] text-[var(--text-dim)] truncate min-w-0",children:xe(e)})]}),typeof e.duration=="number"&&t.jsx("span",{className:"text-[11px] tabular-nums text-[var(--text-muted)] shrink-0",children:B(e.duration)}),t.jsx("span",{role:"button",tabIndex:0,onClick:c,onKeyDown:u=>{(u.key==="Enter"||u.key===" ")&&c(u)},"aria-label":"Copy log entry",className:"opacity-0 group-hover:opacity-100 p-1 rounded text-[var(--text-dim)] hover:text-[var(--text)] transition-opacity shrink-0",children:t.jsx(X,{className:"h-3 w-3"})})]}),r&&t.jsx("div",{className:"px-3 pb-3 pl-9",children:l?t.jsx("pre",{className:"font-mono text-[11px] leading-relaxed text-[var(--text)] bg-[var(--bg)] border border-[var(--border)] rounded-md p-3 max-h-[400px] overflow-auto whitespace-pre-wrap break-words",children:i}):t.jsx(Ht,{value:e.body??e,maxHeightPx:400})})]})},Jt=(e,r)=>e?"bg-[var(--color-danger)]":r?"bg-[var(--color-warning)]":"bg-[var(--color-success)]",Wt=({commands:e})=>{const{spanMs:r,bars:s}=ht(e);return s.length===0?t.jsx("div",{className:"px-4 py-10 text-center text-xs text-[var(--text-dim)]",children:"No commands recorded for this session."}):t.jsxs("div",{className:"px-3 py-3",children:[t.jsxs("div",{className:"flex items-center justify-between pb-2 text-[11px] text-[var(--text-dim)] tabular-nums",children:[t.jsxs("span",{children:[s.length," command",s.length===1?"":"s"," over ",B(r)]}),t.jsxs("span",{className:"inline-flex items-center gap-3",children:[t.jsxs("span",{className:"inline-flex items-center gap-1",children:[t.jsx("span",{className:"h-2 w-2 rounded-sm bg-[var(--color-danger)]"})," Failed"]}),t.jsxs("span",{className:"inline-flex items-center gap-1",children:[t.jsx("span",{className:"h-2 w-2 rounded-sm bg-[var(--color-warning)]"})," Healed"]})]})]}),t.jsx("ol",{className:"space-y-1",children:s.map((a,n)=>t.jsxs("li",{"data-testid":"timeline-bar","data-command":a.label,"data-failed":a.failed?"true":"false",className:"grid grid-cols-[128px_minmax(0,1fr)_64px] items-center gap-2",children:[t.jsx("span",{className:"truncate text-[11px] font-mono text-[var(--text-muted)]",title:a.label,children:a.label}),t.jsx("span",{className:"relative h-3 rounded-sm bg-[rgb(var(--rgb-fg)/0.05)]",children:t.jsx("span",{className:`absolute inset-y-0 rounded-sm ${Jt(a.failed,a.healed)}`,style:{left:`${a.startMs/r*100}%`,width:`max(2px, ${a.durationMs/r*100}%)`}})}),t.jsx("span",{className:"text-right text-[11px] tabular-nums text-[var(--text-muted)]",children:a.durationMs>0?B(a.durationMs):"—"})]},a.id??n))})]})},zt=({commands:e})=>{const r=pt(e);return r.length===0?t.jsxs("div",{className:"px-4 py-10 text-center text-xs text-[var(--text-dim)]",children:["No screenshots were kept. A failed command keeps one unless"," ",t.jsx("code",{className:"font-mono",children:"xe:options.screenshotOnFailure"})," is false; set"," ",t.jsx("code",{className:"font-mono",children:"xe:options.screenshotOnEveryCommand"})," to keep one for every command."]}):t.jsx("ul",{className:"grid grid-cols-[repeat(auto-fill,minmax(140px,1fr))] gap-3 p-3",children:r.map((s,a)=>{const n=S.getAssetUrl(s.path),o=J({timestamp:s.at??null});return t.jsx("li",{children:t.jsxs("a",{href:n,target:"_blank",rel:"noreferrer",className:`block rounded-md border overflow-hidden hover:border-[var(--border-strong)] ${s.failed?"border-[var(--color-danger)]":"border-[var(--border)]"}`,children:[t.jsx("img",{src:n,alt:`${s.command} at ${o}`,loading:"lazy",className:"block w-full aspect-[9/16] object-contain bg-[rgb(var(--rgb-fg)/0.03)]"}),t.jsxs("span",{className:"flex items-center justify-between gap-2 px-2 py-1 text-[11px]",children:[t.jsx("span",{className:`truncate font-mono ${s.failed?"text-[var(--color-danger)]":"text-[var(--text)]"}`,children:s.command}),t.jsx("span",{className:"tabular-nums text-[var(--text-dim)] shrink-0",children:o})]})]})},s.id??a)})})},ve=["commands","device","debug","profiling"],Kt=({sessionLogs:e,deviceLogs:r,debugLogs:s,profiling:a})=>{const[n,o]=x.useState("commands"),[l,i]=x.useState(!1),c=e,u=x.useMemo(()=>Ce(c).screenshots,[c]),b=x.useMemo(()=>[{key:"commands",label:"Commands",count:e.length},{key:"timeline",label:"Timeline",count:e.length},{key:"screenshots",label:"Screenshots",count:u},{key:"device",label:"Device logs",count:r.length},{key:"debug",label:"Debug logs",count:s.length},{key:"profiling",label:"System profiling",count:a.length,hideWhenEmpty:!0}],[e.length,u,r.length,s.length,a.length]),h=x.useMemo(()=>{let v=[];return n==="commands"?v=e:n==="device"?v=r:n==="debug"?v=s:n==="profiling"&&(v=a),$t(v,l)},[n,e,r,s,a,l]);return t.jsxs("section",{className:"rounded-lg border border-[var(--border)] bg-[var(--surface)] flex flex-col min-h-[280px] max-h-[640px]",children:[t.jsx(Pt,{tabs:b,active:n,onChange:o,errorsOnly:l,onErrorsOnlyChange:i,showErrorsOnly:ve.includes(n)}),t.jsxs("div",{className:"flex-1 overflow-y-auto",children:[n==="timeline"&&t.jsx(Wt,{commands:c}),n==="screenshots"&&t.jsx(zt,{commands:c}),ve.includes(n)&&(h.length===0?t.jsx("div",{className:"px-4 py-10 text-center text-xs text-[var(--text-dim)]",children:l?"No error rows in this tab.":"No log entries in this tab."}):h.map((v,y)=>t.jsx(Vt,{log:v},v.id??y)))]})]})};function Re(e){if(!e)return"failed";switch(e){case"HTTPS_CLIENT_ERROR":case"HTTPS_SERVER_ERROR":case"OPEN_HTTPS_SERVER_ERROR":case"ON_CONNECT_ERROR":return"tls";case"PROXY_TO_SERVER_REQUEST_ERROR":return"net";default:return"failed"}}const ee="/xenon/api/interceptor";async function be(e){const r=await fetch(`${ee}/sessions/${encodeURIComponent(e)}/requests`);if(r.status===403)return{active:!1,forbidden:!0};if(r.status===404){const a=await r.json().catch(()=>({}));return{active:!1,error:a==null?void 0:a.error}}return r.ok?{active:!0,requests:(await r.json()).requests||[]}:{active:!1,error:`request failed (${r.status})`}}async function qt(e,r){const s=await fetch(`${ee}/sessions/${encodeURIComponent(e)}/requests/${encodeURIComponent(r)}`);return s.ok?s.json():null}function Gt(e){return`${ee}/sessions/${encodeURIComponent(e)}/har`}function Xt(e){return e>=200&&e<300?"ready":e>=300&&e<400?"neutral":e>=400&&e<500?"busy":e>=500?"error":"neutral"}const ge=({headers:e})=>{const r=Object.entries(e||{});return r.length===0?t.jsx("div",{className:"text-xs text-[var(--text-dim)]",children:"No headers"}):t.jsx("div",{className:"space-y-0.5 font-mono text-xs",children:r.map(([s,a])=>t.jsxs("div",{className:"flex gap-2",children:[t.jsxs("span",{className:"text-[var(--text-dim)] shrink-0",children:[s,":"]}),t.jsx("span",{className:"break-all",children:a})]},s))})},je=({body:e})=>{if(e==null||e==="")return t.jsx("div",{className:"text-xs text-[var(--text-dim)]",children:"No body"});let r=e;try{r=JSON.stringify(JSON.parse(e),null,2)}catch{}return t.jsx("pre",{className:"font-mono text-xs whitespace-pre-wrap break-all bg-[var(--surface-2,var(--surface))] rounded p-2 max-h-64 overflow-auto",children:r})},Yt=({request:e,onClose:r})=>{if(!e)return null;const s=t.jsxs("div",{className:"flex items-center gap-2 text-sm",children:[t.jsx($,{tone:"accent",children:e.method}),e.failed?t.jsx($,{tone:"error",children:Re(e.failureKind)}):t.jsx($,{tone:Xt(e.resStatus),children:e.resStatus||"—"}),t.jsx("span",{className:"font-mono text-xs truncate",title:e.url,children:e.url})]});return t.jsx(it,{open:!!e,title:s,onClose:r,width:720,children:t.jsxs("div",{className:"space-y-4 text-xs",children:[e.failed&&t.jsxs("div",{className:"border border-[var(--red)] rounded p-2 bg-[rgb(var(--rgb-red-600)/0.08)]",children:[t.jsx("div",{className:"text-[var(--red)] text-[11px] mb-1",children:"Failure"}),t.jsx("div",{className:"text-xs",children:e.failureReason||"Request failed"}),e.failureKind&&t.jsx("div",{className:"font-mono text-[10px] text-[var(--text-dim)] mt-1",children:e.failureKind})]}),!e.failed&&t.jsxs("div",{className:"grid grid-cols-2 gap-3",children:[t.jsxs("div",{children:[t.jsx("div",{className:"text-[var(--text-dim)] text-[11px] mb-1",children:"Duration"}),t.jsxs("div",{className:"font-mono",children:[e.durationMs," ms"]})]}),t.jsxs("div",{children:[t.jsx("div",{className:"text-[var(--text-dim)] text-[11px] mb-1",children:"Flags"}),t.jsxs("div",{className:"flex gap-1",children:[e.mocked&&t.jsx($,{tone:"accent",children:"mocked"}),e.modified&&t.jsx($,{tone:"busy",children:"modified"}),!e.mocked&&!e.modified&&t.jsx("span",{className:"text-[var(--text-dim)]",children:"passthrough"})]})]})]}),e.commandHint&&t.jsxs("div",{children:[t.jsx("div",{className:"text-[var(--text-dim)] text-[11px] mb-1",children:"Triggered by"}),t.jsx("div",{className:"font-mono",children:e.commandHint.commandName})]}),!e.failed&&t.jsxs(t.Fragment,{children:[t.jsxs("div",{children:[t.jsx("div",{className:"text-[var(--text-dim)] text-[11px] mb-1",children:"Request Headers"}),t.jsx(ge,{headers:e.reqHeaders})]}),t.jsxs("div",{children:[t.jsx("div",{className:"text-[var(--text-dim)] text-[11px] mb-1",children:"Request Body"}),t.jsx(je,{body:e.reqBody})]}),t.jsxs("div",{children:[t.jsx("div",{className:"text-[var(--text-dim)] text-[11px] mb-1",children:"Response Headers"}),t.jsx(ge,{headers:e.resHeaders})]}),t.jsxs("div",{children:[t.jsx("div",{className:"text-[var(--text-dim)] text-[11px] mb-1",children:"Response Body"}),t.jsx(je,{body:e.resBody})]})]})]})})};function Qt(e){return e?new Date(e).toLocaleTimeString(void 0,{hour12:!1}):"—"}function Zt(e){return e>=200&&e<300?"ready":e>=300&&e<400?"neutral":e>=400&&e<500?"busy":e>=500?"error":"neutral"}const er=({sessionId:e,sessionEnded:r})=>{const[s,a]=x.useState(null),[n,o]=x.useState(!1),[l,i]=x.useState(null),[c,u]=x.useState([]),[b,h]=x.useState(!0),[v,y]=x.useState(null),g=x.useRef(new Set),{on:N,isConnected:w}=De(),T=x.useCallback(d=>{o(!!d.forbidden),a(d.active),i(d.error||null);const j=d.requests||[];u(j),g.current=new Set(j.map(L=>L.id))},[]);x.useEffect(()=>{let d=!0;return h(!0),be(e).then(j=>{d&&T(j)}).catch(()=>{d&&T({active:!1,error:"the server could not be reached"})}).finally(()=>{d&&h(!1)}),()=>{d=!1}},[e,T]),x.useEffect(()=>{if(r)return;let d=!0;const j=N("interceptor_request",k=>{!k||k.sessionId!==e||g.current.has(k.id)||(g.current.add(k.id),u(W=>[k,...W]),a(!0))}),L=N("interceptor_session_started",k=>{(k==null?void 0:k.sessionId)===e&&a(!0)}),O=N("interceptor_session_stopped",k=>{(k==null?void 0:k.sessionId)===e&&be(e).then(W=>{d&&T(W)}).catch(()=>{})});return()=>{d=!1,j(),L(),O()}},[N,e,r,w,T]);const A=x.useMemo(()=>[...c].sort((d,j)=>j.ts-d.ts),[c]),_=async d=>{if(y(d),s){const j=await qt(e,d.id);j&&y(j)}},m=s&&c.length>0?t.jsxs("a",{href:Gt(e),className:"inline-flex items-center gap-1 text-xs text-[var(--text-dim)] hover:text-[var(--text)]",title:"Download HAR",children:[t.jsx(_e,{className:"h-3 w-3"}),"HAR"]}):null,p=t.jsxs("div",{className:"flex items-center gap-2",children:[t.jsx(ot,{className:"h-3.5 w-3.5 text-[var(--text-dim)]"}),t.jsx("span",{children:"Network"}),s&&c.length>0&&t.jsxs("span",{className:"text-xs text-[var(--text-dim)]",children:["(",c.length,")"]})]});let f;const C=s===!1&&(!l||l==="interceptor inactive");return b?f=t.jsx("div",{className:"p-4 text-xs text-[var(--text-dim)]",children:"Loading…"}):n?f=t.jsx(M,{title:"Only admins can see network requests",description:"Captured requests can carry sign-in details and personal data, so only admins can open them. Ask an admin to look at this session's requests."}):C&&r?f=t.jsx(M,{title:"No network capture",description:"This session didn't capture its network requests."}):C?f=t.jsx(M,{title:"Network capture is off",description:"This session isn't capturing its network requests. Android sessions capture them when the test turns network capture on in its capabilities, or when it is on for every session in the server settings."}):l?f=t.jsxs("div",{className:"p-4 text-xs text-[var(--red)]",children:["Failed to load requests: ",l]}):A.length===0?f=t.jsx(M,{title:"Waiting for requests",description:r?"No HTTP traffic was captured during this session.":"Captured requests will appear here as the device makes calls."}):f=t.jsx("div",{className:"overflow-auto max-h-96",children:t.jsxs(st,{children:[t.jsx(at,{children:t.jsxs(ne,{children:[t.jsx(E,{style:{width:80},children:"Time"}),t.jsx(E,{style:{width:70},children:"Method"}),t.jsx(E,{style:{width:70},children:"Status"}),t.jsx(E,{children:"Host"}),t.jsx(E,{children:"Path"}),t.jsx(E,{style:{width:80},children:"Duration"}),t.jsx(E,{style:{width:90},children:"Flags"})]})}),t.jsx(nt,{children:A.map(d=>t.jsxs(ne,{onClick:()=>_(d),style:{cursor:"pointer"},title:d.failed?d.failureReason:void 0,children:[t.jsx(D,{className:"font-mono text-[11px]",children:Qt(d.ts)}),t.jsx(D,{children:t.jsx($,{tone:"accent",children:d.method})}),t.jsx(D,{children:d.failed?t.jsx($,{tone:"error",children:Re(d.failureKind)}):t.jsx($,{tone:Zt(d.resStatus),children:d.resStatus||"—"})}),t.jsx(D,{className:"font-mono text-[11px] truncate",title:d.host,children:d.host}),t.jsx(D,{className:"font-mono text-[11px] truncate",title:d.path,children:d.failed?"—":d.path}),t.jsx(D,{className:"font-mono text-[11px]",children:d.failed?"—":`${d.durationMs}ms`}),t.jsx(D,{children:t.jsxs("div",{className:"flex gap-1",children:[d.mocked&&t.jsx($,{tone:"accent",children:"mock"}),d.modified&&t.jsx($,{tone:"busy",children:"mod"})]})})]},d.id))})]})}),t.jsxs(t.Fragment,{children:[t.jsx(Se,{header:p,action:m,padded:!1,children:f}),t.jsx(Yt,{request:v,onClose:()=>y(null)})]})},wr=()=>{const{buildId:e="",sessionId:r=""}=Le(),{toast:s}=H(),a=Me(),n=Ie(),o=ct(r||null),l=x.useMemo(()=>n.builds.find(h=>h.id===e)??null,[n.builds,e]),i=l?Ue(l):"Build";if(x.useEffect(()=>{!o.loading&&o.notFound&&(s("Session not available — it may belong to a team you are not on.","info"),a("/builds",{replace:!0}))},[o.loading,o.notFound,a,s]),o.loading)return t.jsx("div",{className:"p-6 text-xs text-[var(--text-dim)]",children:"Loading session…"});if(o.error||!o.session)return t.jsxs("div",{className:"p-6 space-y-2",children:[t.jsx(oe,{buildId:e,buildName:l?i:null,sessionId:r}),t.jsx("div",{className:"mt-6 rounded-md border border-[var(--red)]/30 bg-[var(--surface)] p-4 text-xs text-[var(--red)]",children:o.error||"Session not found."})]});const c=o.session,u=o.sessionLogs,b=Y(c.status).bucket==="failed";return t.jsxs("div",{className:"flex flex-col h-full min-h-0",children:[t.jsx(oe,{buildId:e,buildName:l?i:null,sessionId:c.id}),t.jsx(xt,{session:c}),t.jsx("div",{className:"flex-1 overflow-y-auto overflow-x-hidden",children:t.jsxs("div",{className:"px-4 py-4 space-y-4",children:[t.jsx(vt,{session:c,commands:u}),b&&t.jsx(Nt,{session:c,buildName:i,buildId:e,commands:u,durationText:Ve(G(c))}),t.jsx(At,{commands:u}),t.jsx(Lt,{sessionId:c.id,running:c.status==="running",hasTrace:!!c.performance_trace}),t.jsxs("div",{className:"grid grid-cols-1 lg:grid-cols-[minmax(0,1fr)_340px] gap-4",children:[t.jsx("div",{className:"min-w-0",children:t.jsx(Kt,{sessionLogs:o.sessionLogs,deviceLogs:o.deviceLogs,debugLogs:o.debugLogs,profiling:o.profiling})}),t.jsxs("div",{className:"flex flex-col gap-4 min-w-0",children:[t.jsx(Mt,{session:c}),t.jsx(Ot,{session:c,buildName:l?i:null}),t.jsx(Ft,{session:c})]})]}),t.jsx(er,{sessionId:c.id,sessionEnded:c.status!=="running"})]})})]})};export{wr as SessionDetailPage,wr as default};
@@ -0,0 +1,6 @@
1
+ import{c as X,t as G,r as h,p as O,X as g,j as e,A as W,C as E,V as K,e as U,W as Y,Y as J}from"./index-BbKEXUFH.js";/* empty css */import{S as x,A as Q}from"./SettingCard-Njz1pUIJ.js";import{M as Z}from"./Modal-CFCp3n9d.js";import{B as j}from"./button-Dda4irn_.js";import{P as ee}from"./page-header-BjzgWz-T.js";import{C as w}from"./circle-check-CqAJjQ5O.js";import{C as ae}from"./clock-CpuDKysU.js";import{M as se}from"./mouse-pointer-2-DLI1RtzU.js";import{C as te}from"./check-CZ9iw9-e.js";import{A as le}from"./activity-DOVdsfMQ.js";import{C as ne}from"./calendar-BMr0Q62v.js";import{A as re}from"./arrow-up-right-DuAftMWB.js";import"./index-DJGc1M2f.js";import"./utils-CDN07tui.js";/**
2
+ * @license lucide-react v0.555.0 - ISC
3
+ *
4
+ * This source code is licensed under the ISC license.
5
+ * See the LICENSE file in the root directory of this source tree.
6
+ */const ie=[["path",{d:"M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z",key:"1oefj6"}],["path",{d:"M14 2v5a1 1 0 0 0 1 1h5",key:"wfsgrz"}],["path",{d:"M10 9H8",key:"b1mrlr"}],["path",{d:"M16 13H8",key:"t4e002"}],["path",{d:"M16 17H8",key:"z1uh3a"}]],ce=X("file-text",ie),he=t=>({healthCheckIntervalMs:t.healthCheckIntervalMs,healthCheckSchedule:"",enableSelfHealing:t.enableSelfHealing??!0}),S=5e3,oe=[{label:"Battery saver (2 AM)",value:"0 2 * * *"},{label:"Standard (hourly)",value:"0 * * * *"},{label:"Operational coverage (30m)",value:"*/30 * * * *"},{label:"High performance (10m)",value:"*/10 * * * *"}],C=5;function de(t){const a=Date.parse(t);if(!Number.isFinite(a))return"";const n=Math.max(0,Date.now()-a),l=Math.floor(n/1e3);if(l<60)return`${l}s ago`;const d=Math.floor(l/60);if(d<60)return`${d}m ago`;const i=Math.floor(d/60);return i<24?`${i}h ago`:`${Math.floor(i/24)}d ago`}function z(t){var i,m;const a=(i=t.originalSelector)==null?void 0:i.trim(),n=(m=t.healedSelector)==null?void 0:m.trim(),l=t.tier&&t.commandName?`${t.commandName}: `:"";if(a&&n&&a!==n)return`${l}${a} → ${n}`;if(n)return`${l}Recovered locator ${n}`;if(a)return`${l}Recovered locator ${a}`;const d=typeof t.confidence=="number"?` (${Math.round(t.confidence*100)}%)`:"";return`${l||""}Self-healed${d}`}function ue(t){return t.tier?t.tier:t.commandName?t.commandName:t.deviceName?t.deviceName:"Self-heal"}const me=(t,a)=>t.healthCheckIntervalMs===a.healthCheckIntervalMs&&t.healthCheckSchedule===a.healthCheckSchedule&&t.enableSelfHealing===a.enableSelfHealing,fe=(t,a)=>{const n={};return t.healthCheckIntervalMs!==a.healthCheckIntervalMs&&(n.healthCheckIntervalMs=t.healthCheckIntervalMs),t.healthCheckSchedule!==a.healthCheckSchedule&&(n.healthCheckSchedule=t.healthCheckSchedule),t.enableSelfHealing!==a.enableSelfHealing&&(n.enableSelfHealing=t.enableSelfHealing),n},Ae=()=>{const{toast:t}=G(),[a,n]=h.useState(null),[l,d]=h.useState(null),[i,m]=h.useState(null),[$,N]=h.useState(!0),[R,y]=h.useState(!1),[k,b]=h.useState(!1),[L,f]=h.useState(!1),[M,I]=h.useState([]),[D,F]=h.useState(!1),{on:_}=O();h.useEffect(()=>{H()},[]),h.useEffect(()=>{let s=!1;return g.getRecentHealingEvents(C).then(r=>{if(s||!r)return;const c=Array.isArray(r.events)?r.events.slice(0,C):[];I(c)}).catch(()=>{}).finally(()=>{s||F(!0)}),()=>{s=!0}},[]),h.useEffect(()=>{const s=_("healing_event",r=>{const c=r;!c||!c.id||I(u=>u.some(P=>P.id===c.id)?u:[c,...u].slice(0,C))});return()=>{s&&s()}},[_]);const H=async()=>{N(!0),y(!1);try{const s=await g.getGlobalConfig();if(!s||!s.defaults)throw new Error((s==null?void 0:s.message)||"No settings in the answer");const r=he(s.defaults),c={healthCheckIntervalMs:s.healthCheckIntervalMs??r.healthCheckIntervalMs,healthCheckSchedule:s.healthCheckSchedule||"",enableSelfHealing:s.enableSelfHealing??r.enableSelfHealing};m(r),n(c),d(c)}catch(s){console.error("Failed to load settings",s),y(!0),t("Failed to access infrastructure parameters.","error")}finally{N(!1)}},v=a!==null&&l!==null&&!me(a,l),o=a!==null&&(Number.isNaN(a.healthCheckIntervalMs)||a.healthCheckIntervalMs<S)?`Below minimum safe value of ${S}ms.`:null,T=v&&!o,p={interval:(a==null?void 0:a.healthCheckIntervalMs)!==(l==null?void 0:l.healthCheckIntervalMs),schedule:(a==null?void 0:a.healthCheckSchedule)!==(l==null?void 0:l.healthCheckSchedule),healing:(a==null?void 0:a.enableSelfHealing)!==(l==null?void 0:l.enableSelfHealing)},A=async s=>{if(!a||!l)return;if(!s&&o){t(o,"error");return}const r=s??fe(a,l),c=s??a;b(!0);try{await g.updateGlobalConfig(r),d(c),n(c),t("Infrastructure parameters synchronized across fleet.","success")}catch(u){console.error("Failed to save settings",u),J(t,u)}finally{b(!1)}},q=async()=>{if(i){n(i),await A(i);try{await g.resetMetrics()}catch(s){console.error("Failed to reset metrics",s)}}},B=()=>{n(l)};if($)return e.jsxs("div",{className:"settings-loading",children:[e.jsx(W,{className:"animate-spin",size:32}),e.jsx("span",{children:"Synchronizing Global Infrastructure..."})]});if(R||!a||!i)return e.jsxs("div",{className:"settings-loading",role:"alert",children:[e.jsx(E,{size:32}),e.jsx("span",{children:"Couldn't load the settings from the server."}),e.jsx(j,{variant:"secondary",onClick:()=>H(),children:"Try again"})]});const V=a.healthCheckSchedule!=="";return e.jsxs("div",{className:"settings-container",children:[e.jsx(ee,{icon:K,title:"Settings",subtitle:"Manage core farm parameters, heartbeat frequency, and maintenance orchestrations across the global registry."}),e.jsx("div",{className:"settings-content",children:e.jsxs("div",{className:"settings-grid settings-grid--three",children:[e.jsxs(x,{icon:e.jsx(ae,{size:16}),title:e.jsxs("span",{className:"card-title-row",children:["Idle health frequency",p.interval&&e.jsx("span",{className:"modified-dot","aria-label":"Modified"})]}),description:"Frequency of passive health pings when the system is in idle state.",hint:"Minimum safe value: 5000ms. Note: This frequency is overridden when a schedule is active.",children:[e.jsx("div",{className:"card-spacer"}),e.jsxs("div",{className:`input-group ${o?"has-error":""}`,children:[e.jsx("input",{type:"number",value:a.healthCheckIntervalMs,onChange:s=>n({...a,healthCheckIntervalMs:parseInt(s.target.value,10)}),min:S,step:5e3,"aria-invalid":!!o,"aria-describedby":o?"interval-error":"interval-status"}),e.jsx("span",{className:"code-font",children:"MS"})]}),o?e.jsxs("div",{id:"interval-error",className:"input-status input-status--error",role:"alert",children:[e.jsx(E,{size:12}),e.jsx("span",{children:o})]}):e.jsxs("div",{id:"interval-status",className:"input-status input-status--ok",children:[e.jsx(w,{size:12}),e.jsx("span",{children:"Valid frequency"})]})]}),e.jsxs(x,{icon:e.jsx(ne,{size:16}),title:e.jsxs("span",{className:"card-title-row",children:["Deep diagnostic schedule",p.schedule&&e.jsx("span",{className:"modified-dot","aria-label":"Modified"})]}),titleExtra:V?e.jsxs("span",{className:"active-pill",children:[e.jsx(le,{size:11}),e.jsx("span",{children:"Active"})]}):null,description:"Execute intensive reliability bursts (WDA restarts, Cache purges) using standardized Cron syntax.",children:[e.jsx("div",{className:"setting-input-wrapper",children:e.jsx("input",{type:"text",placeholder:"e.g. 0 * * * * (hourly, at minute 0)",value:a.healthCheckSchedule,onChange:s=>n({...a,healthCheckSchedule:s.target.value})})}),e.jsxs("div",{className:"cron-presets",children:[e.jsxs("div",{className:"presets-label",children:[e.jsx(se,{size:12}),e.jsx("span",{children:"Intent-based presets"})]}),e.jsx("div",{className:"presets-grid presets-grid--stacked",children:oe.map(s=>{const r=a.healthCheckSchedule===s.value;return e.jsxs("button",{type:"button",className:`preset-chip ${r?"active":""}`,onClick:()=>n({...a,healthCheckSchedule:s.value}),"aria-pressed":r,children:[r&&e.jsx(te,{size:11,className:"preset-chip__check"}),e.jsx("span",{children:s.label})]},s.label)})})]})]}),e.jsxs(x,{icon:e.jsx(Y,{size:16}),title:e.jsxs("span",{className:"card-title-row",children:["AI self-healing",p.healing&&e.jsx("span",{className:"modified-dot","aria-label":"Modified"})]}),titleExtra:e.jsxs("label",{className:"switch switch--header",children:[e.jsx("input",{type:"checkbox",checked:a.enableSelfHealing,onChange:s=>n({...a,enableSelfHealing:s.target.checked}),"aria-label":"Toggle AI self-healing"}),e.jsx("span",{className:"slider round"})]}),children:[e.jsx("div",{className:`healing-state ${a.enableSelfHealing?"is-on":"is-off"}`,children:a.enableSelfHealing?"Enabled":"Disabled"}),e.jsx("p",{className:"setting-card-description",children:"Automatically intercept and recover from failing locators using Xenon's 6-tier strategy: etalon recovery, native retry, fuzzy XML, OCR, visual AI, and LLM — before failing a test."}),a.enableSelfHealing&&e.jsxs("div",{className:"healing-events",children:[e.jsxs("div",{className:"healing-events__header",children:[e.jsx(ce,{size:11}),e.jsx("span",{children:"Recent healing events"}),e.jsxs(U,{to:"/selector-health",className:"healing-events__link",children:["Selector health ",e.jsx(re,{size:10})]})]}),M.length>0?e.jsx("ul",{className:"healing-events__list",children:M.map(s=>e.jsxs("li",{className:"healing-event",children:[e.jsx(w,{size:14,className:"healing-event__icon"}),e.jsxs("div",{className:"healing-event__body",children:[e.jsxs("div",{className:"healing-event__row",children:[e.jsx("span",{className:"healing-event__kind",children:ue(s)}),e.jsx("span",{className:"healing-event__when",children:de(s.createdAt)})]}),e.jsx("div",{className:"healing-event__message",title:z(s),children:z(s)})]})]},s.id))}):e.jsx("div",{className:"healing-events__empty",children:D?"No healing events recorded yet — the dashboard will populate as locators are recovered.":"Loading recent events…"})]})]})]})}),(v||k)&&e.jsx(Q,{onSave:()=>A(),onDiscard:B,onRestoreDefaults:()=>f(!0),isSaving:k,isValidating:!!o,isDirty:v,saveLabel:T?"Save Configuration":"Resolve errors to save"}),e.jsxs(Z,{open:L,onClose:()=>f(!1),title:"Restore default settings?",footer:e.jsxs(e.Fragment,{children:[e.jsx(j,{variant:"secondary",onClick:()=>f(!1),children:"Cancel"}),e.jsx(j,{variant:"danger",onClick:async()=>{f(!1),await q()},children:"Restore defaults"})]}),children:[e.jsx("p",{className:"text-sm text-[var(--text-muted)]",children:"This saves immediately and can't be undone:"}),e.jsxs("ul",{className:"mt-2 list-disc pl-5 text-sm text-[var(--text)] space-y-1",children:[e.jsxs("li",{children:["Idle health frequency goes back to ",i.healthCheckIntervalMs.toLocaleString()," ms"]}),e.jsx("li",{children:"The diagnostic schedule is cleared"}),e.jsx("li",{children:"AI self-healing is turned on"}),e.jsx("li",{children:"Every device's healed-selector count is reset to zero"})]})]})]})};export{Ae as Settings};
@@ -1 +1 @@
1
- import{j as t}from"./index-DI1fGIyj.js";const d={neutral:"text-[var(--text-muted)]",good:"text-[var(--color-success)]",bad:"text-[var(--color-danger)]"},o=({label:e,value:r,valueCls:a="text-[var(--text)]",note:s,noteTone:x="neutral"})=>t.jsxs("div",{className:"min-w-0 rounded-lg border border-[var(--border)] bg-[var(--surface)] px-4 py-3",children:[t.jsx("div",{className:"text-xs text-[var(--text-muted)]",children:e}),t.jsx("div",{className:`mt-1 text-2xl font-semibold tabular-nums leading-tight truncate ${a}`,children:r}),t.jsx("div",{className:`mt-0.5 text-[11px] truncate ${d[x]}`,children:s??" "})]});export{o as S};
1
+ import{j as t}from"./index-BbKEXUFH.js";const d={neutral:"text-[var(--text-muted)]",good:"text-[var(--color-success)]",bad:"text-[var(--color-danger)]"},o=({label:e,value:r,valueCls:a="text-[var(--text)]",note:s,noteTone:x="neutral"})=>t.jsxs("div",{className:"min-w-0 rounded-lg border border-[var(--border)] bg-[var(--surface)] px-4 py-3",children:[t.jsx("div",{className:"text-xs text-[var(--text-muted)]",children:e}),t.jsx("div",{className:`mt-1 text-2xl font-semibold tabular-nums leading-tight truncate ${a}`,children:r}),t.jsx("div",{className:`mt-0.5 text-[11px] truncate ${d[x]}`,children:s??" "})]});export{o as S};
@@ -1,4 +1,4 @@
1
- import{c as e}from"./index-DI1fGIyj.js";/**
1
+ import{c as e}from"./index-BbKEXUFH.js";/**
2
2
  * @license lucide-react v0.555.0 - ISC
3
3
  *
4
4
  * This source code is licensed under the ISC license.
@@ -1 +1 @@
1
- import{t as A,r as n,j as e,A as I,Z as f,q as $}from"./index-DI1fGIyj.js";/* empty css */import{a as F}from"./time-D-gjmE1o.js";import{T as E,a as M,b as T,c as u,d as U,e as p}from"./Table-io9fwa98.js";import{F as Z}from"./FieldGroup-Bvn08svv.js";import{P as R}from"./page-header--aTyJXq6.js";import{M as K}from"./Modal-Cg9V5JYU.js";import{B as D}from"./button-fIW8owMI.js";import{I as Q}from"./input-BjDRKCea.js";import{S as P}from"./select-DvCSv25z.js";import{l as V}from"./users-DMF9Mk5H.js";import{P as N}from"./plus-CsIk10Cq.js";import{A as W}from"./arrow-left-B27ckB31.js";import{T as L}from"./trash-2-CTZpPSdd.js";import"./index-HDQVe1gi.js";import"./utils-CDN07tui.js";const B={SUPER_ADMIN:"Super admin",ADMIN:"Admin",MEMBER:"Member"};async function j(i,r){const t=await fetch(i,{credentials:"include",...r});if(!t.ok){const c=await t.json().catch(()=>({}));throw Object.assign(new Error(c.error||`HTTP ${t.status}`),{status:t.status})}return t.json()}const ve=()=>{const{toast:i}=A(),[r,t]=n.useState([]),[c,m]=n.useState(!0),[l,h]=n.useState(!1),[x,d]=n.useState(!1),[_,C]=n.useState(null),v=async()=>{m(!0);try{const a=await j("/xenon/api/teams");t(a),h(!1)}catch(a){a.status===403?h(!0):i(`Failed to load teams: ${a.message}`,"error")}finally{m(!1)}};if(n.useEffect(()=>{v()},[]),c)return e.jsxs("div",{className:"settings-loading",children:[e.jsx(I,{className:"animate-spin",size:32}),e.jsx("span",{children:"Loading teams…"})]});if(l)return e.jsx("div",{className:"settings-container",children:e.jsx(R,{icon:f,title:"Teams",subtitle:e.jsxs(e.Fragment,{children:["Your key lacks the ",e.jsx("code",{children:"admin"})," scope. Ask an administrator to promote your key before you can manage teams."]})})});if(_)return e.jsx(se,{team:_,onBack:()=>{C(null),v()}});const S=r.reduce((a,o)=>a+o.deviceCount,0),y=r.reduce((a,o)=>a+o.memberCount,0);return e.jsxs("div",{className:"settings-container",children:[e.jsx(R,{icon:f,title:"Teams",subtitle:"Group devices and members by team. Devices with no team assignment stay in the shared pool — visible to every authenticated user.",action:r.length>0?e.jsxs("button",{type:"button",className:"page-header-action",onClick:()=>d(!0),children:[e.jsx(N,{size:16}),e.jsx("span",{children:"New team"})]}):void 0}),e.jsxs("div",{className:"settings-content",children:[r.length>0&&e.jsxs("div",{className:"stat-strip",children:[e.jsxs("div",{className:"stat-strip__cell",children:[e.jsx("div",{className:"stat-strip__value stat-strip__value--accent",children:r.length}),e.jsx("div",{className:"stat-strip__label",children:"Teams"})]}),e.jsxs("div",{className:"stat-strip__cell",children:[e.jsx("div",{className:"stat-strip__value",children:y}),e.jsx("div",{className:"stat-strip__label",children:"Members"})]}),e.jsxs("div",{className:"stat-strip__cell",children:[e.jsx("div",{className:"stat-strip__value",children:S}),e.jsx("div",{className:"stat-strip__label",children:"Devices assigned"})]})]}),r.length===0?e.jsxs("div",{className:"empty-state",children:[e.jsx("div",{className:"empty-state__icon",children:e.jsx(f,{size:28})}),e.jsx("h3",{className:"empty-state__title",children:"No teams yet"}),e.jsx("p",{className:"empty-state__copy",children:"Every device is in the shared pool until you create a team and assign it. Teams let you scope an API key to a subset of devices — useful for separating CI runners, staging fleets, or per-team device pools."}),e.jsxs("button",{type:"button",className:"page-header-action empty-state__action",onClick:()=>d(!0),children:[e.jsx(N,{size:16}),e.jsx("span",{children:"Create your first team"})]})]}):e.jsx("div",{className:"team-grid",children:r.map(a=>e.jsxs("button",{type:"button",className:"team-card",onClick:()=>C(a),children:[e.jsxs("div",{className:"team-card__header",children:[e.jsx("div",{className:"team-card__icon",children:e.jsx(f,{size:16})}),e.jsxs("div",{className:"team-card__title",children:[e.jsx("div",{className:"team-card__name",children:a.name}),e.jsx("div",{className:"team-card__id",children:a.id.slice(0,8)})]}),e.jsx("span",{className:"team-card__delete",role:"button",tabIndex:0,onClick:o=>o.stopPropagation(),children:e.jsx(ee,{team:a,onDeleted:v})})]}),e.jsxs("div",{className:"team-card__metrics",children:[e.jsxs("div",{className:"team-card__metric",children:[e.jsx($,{size:13}),e.jsx("span",{className:"team-card__metric-value",children:a.deviceCount}),e.jsx("span",{className:"team-card__metric-label",children:"devices"})]}),e.jsxs("div",{className:"team-card__metric",children:[e.jsx(f,{size:13}),e.jsx("span",{className:"team-card__metric-value",children:a.memberCount}),e.jsx("span",{className:"team-card__metric-label",children:"members"})]})]}),e.jsxs("div",{className:"team-card__footer",children:["Created ",F(a.createdAt)]})]},a.id))})]}),x&&e.jsx(X,{onClose:()=>d(!1),onCreated:()=>{d(!1),v()}})]})},X=({onClose:i,onCreated:r})=>{const{toast:t}=A(),[c,m]=n.useState(""),[l,h]=n.useState(!1),x=async()=>{if(!c.trim())return t("Name required","error");h(!0);try{await j("/xenon/api/teams",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:c.trim()})}),t("Team created","success"),r()}catch(d){t(d.message,"error")}finally{h(!1)}};return e.jsx(K,{open:!0,onClose:i,title:"New team",footer:e.jsxs(e.Fragment,{children:[e.jsx(D,{variant:"secondary",onClick:i,disabled:l,children:"Cancel"}),e.jsxs(D,{variant:"primary",onClick:x,disabled:l,children:[l?e.jsx(I,{className:"animate-spin",size:18}):e.jsx(N,{size:18}),l?"Creating…":"Create"]})]}),children:e.jsx("div",{style:{display:"flex",flexDirection:"column"},children:e.jsx(Z,{label:"Name",htmlFor:"team-name",children:e.jsx(Q,{id:"team-name",type:"text",value:c,onChange:d=>m(d.target.value),placeholder:"android-team, qa-ios, …",className:"w-full",autoFocus:!0})})})})},ee=({team:i,onDeleted:r})=>{const{toast:t}=A(),[c,m]=n.useState(!1),l=async h=>{if(h.stopPropagation(),!!window.confirm(`Delete team "${i.name}"? It must have no devices, members or apps.`)){m(!0);try{await j(`/xenon/api/teams/${i.id}`,{method:"DELETE"}),t("Team deleted","success"),r()}catch(x){t(x.message,"error")}finally{m(!1)}}};return e.jsxs("button",{className:"reset-btn",disabled:c,onClick:l,style:{color:"var(--status-error-fg)"},children:[e.jsx(L,{size:14}),c?"Deleting…":"Delete"]})},se=({team:i,onBack:r})=>{const{toast:t}=A(),[c,m]=n.useState([]),[l,h]=n.useState([]),[x,d]=n.useState([]),[_,C]=n.useState([]),[v,S]=n.useState(!0),[y,a]=n.useState(""),[o,k]=n.useState(""),b=async()=>{S(!0);try{const[s,g,Y]=await Promise.all([j(`/xenon/api/teams/${i.id}/members`),V(),j("/xenon/api/device")]);m(s),h(g);const z=Y;d(z.filter(w=>w.teamId===i.id)),C(z.filter(w=>!w.teamId))}catch(s){t(`Failed to load team detail: ${s.message}`,"error")}finally{S(!1)}};n.useEffect(()=>{b()},[i.id]);const O=async()=>{if(y)try{await j(`/xenon/api/teams/${i.id}/members`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({userId:y})}),t("Member added","success"),a(""),b()}catch(s){t(s.message,"error")}},H=async s=>{if(window.confirm("Remove this member from the team?"))try{await j(`/xenon/api/teams/${i.id}/members/${encodeURIComponent(s)}`,{method:"DELETE"}),t("Member removed","success"),b()}catch(g){t(g.message,"error")}},J=async()=>{if(o)try{await j(`/xenon/api/device/${encodeURIComponent(o)}/team`,{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({teamId:i.id})}),t("Device assigned","success"),k(""),b()}catch(s){t(s.message,"error")}},q=async s=>{if(window.confirm("Return this device to the shared pool?"))try{await j(`/xenon/api/device/${encodeURIComponent(s)}/team`,{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({teamId:null})}),t("Device unassigned","success"),b()}catch(g){t(g.message,"error")}},G=l.filter(s=>!c.some(g=>g.userId===s.id));return e.jsxs("div",{className:"settings-container mesh-gradient-infra",children:[e.jsxs("div",{className:"settings-header",children:[e.jsxs("div",{style:{display:"flex",alignItems:"center",gap:12},children:[e.jsx("button",{onClick:r,className:"reset-btn",style:{padding:"4px 8px"},"aria-label":"Back",children:e.jsx(W,{size:18})}),e.jsxs("div",{className:"settings-title-group",children:[e.jsx(f,{className:"settings-icon infra-icon",size:28}),e.jsx("h2",{children:i.name})]})]}),e.jsx("p",{className:"settings-subtitle",children:'Manage members and devices assigned to this team. To add a device, go to Devices and use "Assign to team."'})]}),v?e.jsx("div",{className:"settings-loading",children:e.jsx(I,{className:"animate-spin",size:32})}):e.jsxs("div",{className:"settings-content",style:{display:"flex",flexDirection:"column",gap:24},children:[e.jsxs("section",{children:[e.jsxs("h3",{style:{marginBottom:12},children:["Members (",c.length,")"]}),e.jsxs("div",{className:"surface-card",style:{padding:12},children:[e.jsxs("div",{style:{display:"flex",gap:8,marginBottom:12},children:[e.jsxs(P,{value:y,onChange:s=>a(s.target.value),className:"flex-1",children:[e.jsx("option",{value:"",children:"Select a user to add…"}),G.map(s=>e.jsxs("option",{value:s.id,children:[s.name," (",s.email,") — ",B[s.role]]},s.id))]}),e.jsxs(D,{variant:"primary",onClick:O,disabled:!y,children:[e.jsx(N,{size:16})," Add"]})]}),c.length===0?e.jsx("p",{style:{opacity:.6,textAlign:"center",padding:24},children:"No members yet. Add a user above."}):e.jsxs(E,{children:[e.jsx(M,{children:e.jsxs(T,{children:[e.jsx(u,{children:"Name"}),e.jsx(u,{children:"Email"}),e.jsx(u,{children:"Role"}),e.jsx(u,{children:"Added"}),e.jsx(u,{style:{textAlign:"right"},children:"Action"})]})}),e.jsx(U,{children:c.map(s=>e.jsxs(T,{children:[e.jsx(p,{children:s.name}),e.jsx(p,{children:e.jsx("code",{style:{fontSize:"11px",color:"var(--text-muted)"},children:s.email})}),e.jsx(p,{children:B[s.role]}),e.jsx(p,{children:F(s.addedAt)}),e.jsx(p,{style:{textAlign:"right"},children:e.jsx("button",{className:"reset-btn",onClick:()=>H(s.userId),style:{color:"var(--status-error-fg)"},"aria-label":"Remove member",title:"Remove member",children:e.jsx(L,{size:14})})})]},s.userId))})]})]})]}),e.jsxs("section",{children:[e.jsxs("h3",{style:{marginBottom:12},children:["Devices (",x.length,")"]}),e.jsxs("div",{className:"surface-card",style:{padding:12},children:[e.jsxs("div",{style:{display:"flex",gap:8,marginBottom:12},children:[e.jsxs(P,{value:o,onChange:s=>k(s.target.value),className:"flex-1",children:[e.jsx("option",{value:"",children:"Select a shared-pool device to assign…"}),_.map(s=>e.jsxs("option",{value:s.udid,children:[s.name," (",s.platform,") — ",s.udid.slice(0,12),"…"]},s.udid))]}),e.jsxs(D,{variant:"primary",onClick:J,disabled:!o,children:[e.jsx(N,{size:16})," Assign"]})]}),x.length===0?e.jsx("p",{style:{opacity:.6,textAlign:"center",padding:24},children:"No devices assigned. Use the Devices page to move devices into this team."}):e.jsxs(E,{children:[e.jsx(M,{children:e.jsxs(T,{children:[e.jsx(u,{children:"Name"}),e.jsx(u,{children:"Platform"}),e.jsx(u,{children:"UDID"}),e.jsx(u,{style:{textAlign:"right"},children:"Actions"})]})}),e.jsx(U,{children:x.map(s=>e.jsxs(T,{children:[e.jsxs(p,{children:[e.jsx($,{size:14,style:{marginRight:6,verticalAlign:"middle"}}),s.name]}),e.jsx(p,{children:s.platform}),e.jsx(p,{children:e.jsxs("code",{style:{fontSize:"11px"},children:[s.udid.slice(0,14),"…"]})}),e.jsx(p,{style:{textAlign:"right"},children:e.jsx("button",{className:"reset-btn",onClick:()=>q(s.udid),children:"Return to shared pool"})})]},s.udid))})]})]})]})]})]})};export{ve as Teams};
1
+ import{t as D,r as n,j as e,A as I,_ as f,q as $}from"./index-BbKEXUFH.js";/* empty css */import{a as F}from"./time-D-gjmE1o.js";import{T as E,a as M,b as T,c as u,d as P,e as p}from"./Table-BcTm5xV2.js";import{F as K}from"./FieldGroup-Cb5QZsE1.js";import{P as R}from"./page-header-BjzgWz-T.js";import{M as Q}from"./Modal-CFCp3n9d.js";import{B as A}from"./button-Dda4irn_.js";import{I as V}from"./input-Cm0tmSoc.js";import{S as U}from"./select-88jHcl-Z.js";import{l as W}from"./users-CFba5TSy.js";import{P as N}from"./plus-nBFSMu_X.js";import{A as X}from"./arrow-left-MMbTmgoE.js";import{T as L}from"./trash-2-C5RSuX0e.js";import"./index-DJGc1M2f.js";import"./utils-CDN07tui.js";const B={SUPER_ADMIN:"Super admin",ADMIN:"Admin",MEMBER:"Member"};async function j(i,r){const t=await fetch(i,{credentials:"include",...r});if(!t.ok){const c=await t.json().catch(()=>({}));throw Object.assign(new Error(c.error||`HTTP ${t.status}`),{status:t.status})}return t.json()}const ve=()=>{const{toast:i}=D(),[r,t]=n.useState([]),[c,m]=n.useState(!0),[l,h]=n.useState(!1),[x,d]=n.useState(!1),[_,C]=n.useState(null),v=async()=>{m(!0);try{const a=await j("/xenon/api/teams");t(a),h(!1)}catch(a){a.status===403?h(!0):i(`Failed to load teams: ${a.message}`,"error")}finally{m(!1)}};if(n.useEffect(()=>{v()},[]),c)return e.jsxs("div",{className:"settings-loading",children:[e.jsx(I,{className:"animate-spin",size:32}),e.jsx("span",{children:"Loading teams…"})]});if(l)return e.jsx("div",{className:"settings-container",children:e.jsx(R,{icon:f,title:"Teams",subtitle:e.jsxs(e.Fragment,{children:["Your key lacks the ",e.jsx("code",{children:"admin"})," scope. Ask an administrator to promote your key before you can manage teams."]})})});if(_)return e.jsx(se,{team:_,onBack:()=>{C(null),v()}});const S=r.reduce((a,o)=>a+o.deviceCount,0),y=r.reduce((a,o)=>a+o.memberCount,0);return e.jsxs("div",{className:"settings-container",children:[e.jsx(R,{icon:f,title:"Teams",subtitle:"Group devices and members by team. Devices with no team assignment stay in the shared pool — visible to every authenticated user.",action:r.length>0?e.jsxs("button",{type:"button",className:"page-header-action",onClick:()=>d(!0),children:[e.jsx(N,{size:16}),e.jsx("span",{children:"New team"})]}):void 0}),e.jsxs("div",{className:"settings-content",children:[r.length>0&&e.jsxs("div",{className:"stat-strip",children:[e.jsxs("div",{className:"stat-strip__cell",children:[e.jsx("div",{className:"stat-strip__value stat-strip__value--accent",children:r.length}),e.jsx("div",{className:"stat-strip__label",children:"Teams"})]}),e.jsxs("div",{className:"stat-strip__cell",children:[e.jsx("div",{className:"stat-strip__value",children:y}),e.jsx("div",{className:"stat-strip__label",children:"Members"})]}),e.jsxs("div",{className:"stat-strip__cell",children:[e.jsx("div",{className:"stat-strip__value",children:S}),e.jsx("div",{className:"stat-strip__label",children:"Devices assigned"})]})]}),r.length===0?e.jsxs("div",{className:"empty-state",children:[e.jsx("div",{className:"empty-state__icon",children:e.jsx(f,{size:28})}),e.jsx("h3",{className:"empty-state__title",children:"No teams yet"}),e.jsx("p",{className:"empty-state__copy",children:"Every device is in the shared pool until you create a team and assign it. Teams let you scope an API key to a subset of devices — useful for separating CI runners, staging fleets, or per-team device pools."}),e.jsxs("button",{type:"button",className:"page-header-action empty-state__action",onClick:()=>d(!0),children:[e.jsx(N,{size:16}),e.jsx("span",{children:"Create your first team"})]})]}):e.jsx("div",{className:"team-grid",children:r.map(a=>e.jsxs("button",{type:"button",className:"team-card",onClick:()=>C(a),children:[e.jsxs("div",{className:"team-card__header",children:[e.jsx("div",{className:"team-card__icon",children:e.jsx(f,{size:16})}),e.jsxs("div",{className:"team-card__title",children:[e.jsx("div",{className:"team-card__name",children:a.name}),e.jsx("div",{className:"team-card__id",children:a.id.slice(0,8)})]}),e.jsx("span",{className:"team-card__delete",role:"button",tabIndex:0,onClick:o=>o.stopPropagation(),children:e.jsx(ee,{team:a,onDeleted:v})})]}),e.jsxs("div",{className:"team-card__metrics",children:[e.jsxs("div",{className:"team-card__metric",children:[e.jsx($,{size:13}),e.jsx("span",{className:"team-card__metric-value",children:a.deviceCount}),e.jsx("span",{className:"team-card__metric-label",children:"devices"})]}),e.jsxs("div",{className:"team-card__metric",children:[e.jsx(f,{size:13}),e.jsx("span",{className:"team-card__metric-value",children:a.memberCount}),e.jsx("span",{className:"team-card__metric-label",children:"members"})]})]}),e.jsxs("div",{className:"team-card__footer",children:["Created ",F(a.createdAt)]})]},a.id))})]}),x&&e.jsx(Z,{onClose:()=>d(!1),onCreated:()=>{d(!1),v()}})]})},Z=({onClose:i,onCreated:r})=>{const{toast:t}=D(),[c,m]=n.useState(""),[l,h]=n.useState(!1),x=async()=>{if(!c.trim())return t("Name required","error");h(!0);try{await j("/xenon/api/teams",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:c.trim()})}),t("Team created","success"),r()}catch(d){t(d.message,"error")}finally{h(!1)}};return e.jsx(Q,{open:!0,onClose:i,title:"New team",footer:e.jsxs(e.Fragment,{children:[e.jsx(A,{variant:"secondary",onClick:i,disabled:l,children:"Cancel"}),e.jsxs(A,{variant:"primary",onClick:x,disabled:l,children:[l?e.jsx(I,{className:"animate-spin",size:18}):e.jsx(N,{size:18}),l?"Creating…":"Create"]})]}),children:e.jsx("div",{style:{display:"flex",flexDirection:"column"},children:e.jsx(K,{label:"Name",htmlFor:"team-name",children:e.jsx(V,{id:"team-name",type:"text",value:c,onChange:d=>m(d.target.value),placeholder:"android-team, qa-ios, …",className:"w-full",autoFocus:!0})})})})},ee=({team:i,onDeleted:r})=>{const{toast:t}=D(),[c,m]=n.useState(!1),l=async h=>{if(h.stopPropagation(),!!window.confirm(`Delete team "${i.name}"? It must have no devices, members or apps.`)){m(!0);try{await j(`/xenon/api/teams/${i.id}`,{method:"DELETE"}),t("Team deleted","success"),r()}catch(x){t(x.message,"error")}finally{m(!1)}}};return e.jsxs("button",{className:"reset-btn",disabled:c,onClick:l,style:{color:"var(--status-error-fg)"},children:[e.jsx(L,{size:14}),c?"Deleting…":"Delete"]})},se=({team:i,onBack:r})=>{const{toast:t}=D(),[c,m]=n.useState([]),[l,h]=n.useState([]),[x,d]=n.useState([]),[_,C]=n.useState([]),[v,S]=n.useState(!0),[y,a]=n.useState(""),[o,k]=n.useState(""),b=async()=>{S(!0);try{const[s,g,Y]=await Promise.all([j(`/xenon/api/teams/${i.id}/members`),W(),j("/xenon/api/device")]);m(s),h(g);const z=Y;d(z.filter(w=>w.teamId===i.id)),C(z.filter(w=>!w.teamId))}catch(s){t(`Failed to load team detail: ${s.message}`,"error")}finally{S(!1)}};n.useEffect(()=>{b()},[i.id]);const O=async()=>{if(y)try{await j(`/xenon/api/teams/${i.id}/members`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({userId:y})}),t("Member added","success"),a(""),b()}catch(s){t(s.message,"error")}},H=async s=>{if(window.confirm("Remove this member from the team?"))try{await j(`/xenon/api/teams/${i.id}/members/${encodeURIComponent(s)}`,{method:"DELETE"}),t("Member removed","success"),b()}catch(g){t(g.message,"error")}},J=async()=>{if(o)try{await j(`/xenon/api/device/${encodeURIComponent(o)}/team`,{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({teamId:i.id})}),t("Device assigned","success"),k(""),b()}catch(s){t(s.message,"error")}},q=async s=>{if(window.confirm("Return this device to the shared pool?"))try{await j(`/xenon/api/device/${encodeURIComponent(s)}/team`,{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({teamId:null})}),t("Device unassigned","success"),b()}catch(g){t(g.message,"error")}},G=l.filter(s=>!c.some(g=>g.userId===s.id));return e.jsxs("div",{className:"settings-container mesh-gradient-infra",children:[e.jsxs("div",{className:"settings-header",children:[e.jsxs("div",{style:{display:"flex",alignItems:"center",gap:12},children:[e.jsx("button",{onClick:r,className:"reset-btn",style:{padding:"4px 8px"},"aria-label":"Back",children:e.jsx(X,{size:18})}),e.jsxs("div",{className:"settings-title-group",children:[e.jsx(f,{className:"settings-icon infra-icon",size:28}),e.jsx("h2",{children:i.name})]})]}),e.jsx("p",{className:"settings-subtitle",children:`Manage members and devices assigned to this team. Add a shared device below, or choose "Assign team…" in a device's ⋯ menu on the Devices page.`})]}),v?e.jsx("div",{className:"settings-loading",children:e.jsx(I,{className:"animate-spin",size:32})}):e.jsxs("div",{className:"settings-content",style:{display:"flex",flexDirection:"column",gap:24},children:[e.jsxs("section",{children:[e.jsxs("h3",{style:{marginBottom:12},children:["Members (",c.length,")"]}),e.jsxs("div",{className:"surface-card",style:{padding:12},children:[e.jsxs("div",{style:{display:"flex",gap:8,marginBottom:12},children:[e.jsxs(U,{value:y,onChange:s=>a(s.target.value),className:"flex-1",children:[e.jsx("option",{value:"",children:"Select a user to add…"}),G.map(s=>e.jsxs("option",{value:s.id,children:[s.name," (",s.email,") — ",B[s.role]]},s.id))]}),e.jsxs(A,{variant:"primary",onClick:O,disabled:!y,children:[e.jsx(N,{size:16})," Add"]})]}),c.length===0?e.jsx("p",{style:{opacity:.6,textAlign:"center",padding:24},children:"No members yet. Add a user above."}):e.jsxs(E,{children:[e.jsx(M,{children:e.jsxs(T,{children:[e.jsx(u,{children:"Name"}),e.jsx(u,{children:"Email"}),e.jsx(u,{children:"Role"}),e.jsx(u,{children:"Added"}),e.jsx(u,{style:{textAlign:"right"},children:"Action"})]})}),e.jsx(P,{children:c.map(s=>e.jsxs(T,{children:[e.jsx(p,{children:s.name}),e.jsx(p,{children:e.jsx("code",{style:{fontSize:"11px",color:"var(--text-muted)"},children:s.email})}),e.jsx(p,{children:B[s.role]}),e.jsx(p,{children:F(s.addedAt)}),e.jsx(p,{style:{textAlign:"right"},children:e.jsx("button",{className:"reset-btn",onClick:()=>H(s.userId),style:{color:"var(--status-error-fg)"},"aria-label":"Remove member",title:"Remove member",children:e.jsx(L,{size:14})})})]},s.userId))})]})]})]}),e.jsxs("section",{children:[e.jsxs("h3",{style:{marginBottom:12},children:["Devices (",x.length,")"]}),e.jsxs("div",{className:"surface-card",style:{padding:12},children:[e.jsxs("div",{style:{display:"flex",gap:8,marginBottom:12},children:[e.jsxs(U,{value:o,onChange:s=>k(s.target.value),className:"flex-1",children:[e.jsx("option",{value:"",children:"Select a shared-pool device to assign…"}),_.map(s=>e.jsxs("option",{value:s.udid,children:[s.name," (",s.platform,") — ",s.udid.slice(0,12),"…"]},s.udid))]}),e.jsxs(A,{variant:"primary",onClick:J,disabled:!o,children:[e.jsx(N,{size:16})," Assign"]})]}),x.length===0?e.jsx("p",{style:{opacity:.6,textAlign:"center",padding:24},children:"No devices assigned. Pick a shared device above to add one."}):e.jsxs(E,{children:[e.jsx(M,{children:e.jsxs(T,{children:[e.jsx(u,{children:"Name"}),e.jsx(u,{children:"Platform"}),e.jsx(u,{children:"UDID"}),e.jsx(u,{style:{textAlign:"right"},children:"Actions"})]})}),e.jsx(P,{children:x.map(s=>e.jsxs(T,{children:[e.jsxs(p,{children:[e.jsx($,{size:14,style:{marginRight:6,verticalAlign:"middle"}}),s.name]}),e.jsx(p,{children:s.platform}),e.jsx(p,{children:e.jsxs("code",{style:{fontSize:"11px"},children:[s.udid.slice(0,14),"…"]})}),e.jsx(p,{style:{textAlign:"right"},children:e.jsx("button",{className:"reset-btn",onClick:()=>q(s.udid),children:"Return to shared pool"})})]},s.udid))})]})]})]})]})]})};export{ve as Teams};
@@ -1,4 +1,4 @@
1
- import{c as a}from"./index-DI1fGIyj.js";/**
1
+ import{c as a}from"./index-BbKEXUFH.js";/**
2
2
  * @license lucide-react v0.555.0 - ISC
3
3
  *
4
4
  * This source code is licensed under the ISC license.
@@ -1,4 +1,4 @@
1
- import{c as a}from"./index-DI1fGIyj.js";/**
1
+ import{c as a}from"./index-BbKEXUFH.js";/**
2
2
  * @license lucide-react v0.555.0 - ISC
3
3
  *
4
4
  * This source code is licensed under the ISC license.
@@ -1,4 +1,4 @@
1
- import{c as G,r as o,p as V,X as v}from"./index-DI1fGIyj.js";import{f as Z}from"./time-D-gjmE1o.js";/**
1
+ import{c as G,r as o,p as V,X as v}from"./index-BbKEXUFH.js";import{f as Z}from"./time-D-gjmE1o.js";/**
2
2
  * @license lucide-react v0.555.0 - ISC
3
3
  *
4
4
  * This source code is licensed under the ISC license.
@@ -1 +1 @@
1
- import{r as t,h as u}from"./index-DI1fGIyj.js";function n(){const[s,r]=t.useState(null);return t.useEffect(()=>{let e=!0;return u().then(o=>e&&r(o.passwordReset)),()=>{e=!1}},[]),s}export{n as u};
1
+ import{r as t,h as u}from"./index-BbKEXUFH.js";function n(){const[s,r]=t.useState(null);return t.useEffect(()=>{let e=!0;return u().then(o=>e&&r(o.passwordReset)),()=>{e=!1}},[]),s}export{n as u};
@@ -1 +1 @@
1
- import{o as s}from"./index-DI1fGIyj.js";const o="/xenon/api/users";async function c(){const t=await fetch(`${o}/`,{credentials:"include"});if(!t.ok)throw new Error(`listUsers failed (${t.status})`);return t.json()}async function i(t){const e=await fetch(`${o}/`,{method:"POST",credentials:"include",headers:{"Content-Type":"application/json",...s},body:JSON.stringify(t)});if(!e.ok){const n=await e.json().catch(()=>({}));throw new Error(n.error||`createUser failed (${e.status})`)}return e.json()}async function d(t,e){const n=await fetch(`${o}/${encodeURIComponent(t)}`,{method:"PATCH",credentials:"include",headers:{"Content-Type":"application/json"},body:JSON.stringify(e)});if(!n.ok){const r=await n.json().catch(()=>({}));throw new Error(r.error||`updateUser failed (${n.status})`)}return n.json()}async function l(t){const e=await fetch(`${o}/${encodeURIComponent(t)}`,{method:"DELETE",credentials:"include"});if(!e.ok&&e.status!==204){const n=await e.json().catch(()=>({}));throw new Error(n.error||`deleteUser failed (${e.status})`)}}async function u(t){const e=await fetch(`${o}/${encodeURIComponent(t)}/reset-link`,{method:"POST",credentials:"include"}),n=await e.json().catch(()=>({}));if(!e.ok)throw new Error(n.error||`Reset link failed (${e.status})`);return n}export{i as a,u as c,l as d,c as l,d as u};
1
+ import{o as s}from"./index-BbKEXUFH.js";const o="/xenon/api/users";async function c(){const t=await fetch(`${o}/`,{credentials:"include"});if(!t.ok)throw new Error(`listUsers failed (${t.status})`);return t.json()}async function i(t){const e=await fetch(`${o}/`,{method:"POST",credentials:"include",headers:{"Content-Type":"application/json",...s},body:JSON.stringify(t)});if(!e.ok){const n=await e.json().catch(()=>({}));throw new Error(n.error||`createUser failed (${e.status})`)}return e.json()}async function d(t,e){const n=await fetch(`${o}/${encodeURIComponent(t)}`,{method:"PATCH",credentials:"include",headers:{"Content-Type":"application/json"},body:JSON.stringify(e)});if(!n.ok){const r=await n.json().catch(()=>({}));throw new Error(r.error||`updateUser failed (${n.status})`)}return n.json()}async function l(t){const e=await fetch(`${o}/${encodeURIComponent(t)}`,{method:"DELETE",credentials:"include"});if(!e.ok&&e.status!==204){const n=await e.json().catch(()=>({}));throw new Error(n.error||`deleteUser failed (${e.status})`)}}async function u(t){const e=await fetch(`${o}/${encodeURIComponent(t)}/reset-link`,{method:"POST",credentials:"include"}),n=await e.json().catch(()=>({}));if(!e.ok)throw new Error(n.error||`Reset link failed (${e.status})`);return n}export{i as a,u as c,l as d,c as l,d as u};
@@ -1,4 +1,4 @@
1
- import{c as w,b as k,r as a,j as e,a1 as C}from"./index-DI1fGIyj.js";import{l as M,d as R,c as I,a as A,u as D}from"./users-DMF9Mk5H.js";import{f as U}from"./time-D-gjmE1o.js";import{P}from"./page-header--aTyJXq6.js";import{B as y}from"./button-fIW8owMI.js";import{I as N}from"./input-BjDRKCea.js";import{S as g}from"./select-DvCSv25z.js";import{P as B}from"./plus-CsIk10Cq.js";import{T as L}from"./trash-2-CTZpPSdd.js";import"./utils-CDN07tui.js";/**
1
+ import{c as w,b as k,r as a,j as e,a2 as C}from"./index-BbKEXUFH.js";import{l as M,d as R,c as I,a as A,u as D}from"./users-CFba5TSy.js";import{f as U}from"./time-D-gjmE1o.js";import{P}from"./page-header-BjzgWz-T.js";import{B as y}from"./button-Dda4irn_.js";import{I as N}from"./input-Cm0tmSoc.js";import{S as g}from"./select-88jHcl-Z.js";import{P as B}from"./plus-nBFSMu_X.js";import{T as L}from"./trash-2-C5RSuX0e.js";import"./utils-CDN07tui.js";/**
2
2
  * @license lucide-react v0.555.0 - ISC
3
3
  *
4
4
  * This source code is licensed under the ISC license.
@@ -1,4 +1,4 @@
1
- import{c as o}from"./index-DI1fGIyj.js";/**
1
+ import{c as o}from"./index-BbKEXUFH.js";/**
2
2
  * @license lucide-react v0.555.0 - ISC
3
3
  *
4
4
  * This source code is licensed under the ISC license.
@@ -0,0 +1 @@
1
+ import{t as P,r as l,X as p,j as e,P as w,A as x,C as V,Q as I}from"./index-BbKEXUFH.js";/* empty css */import{F as k}from"./FieldGroup-Cb5QZsE1.js";import{S as U}from"./select-88jHcl-Z.js";import{P as B}from"./page-header-BjzgWz-T.js";import{T as D}from"./trash-2-C5RSuX0e.js";import{P as _}from"./plus-nBFSMu_X.js";import{C as J}from"./circle-x-DlDJtnVz.js";import{C as K}from"./circle-check-CqAJjQ5O.js";import{Z as M}from"./zap-l4VaXzq8.js";import{A as X}from"./activity-DOVdsfMQ.js";import"./utils-CDN07tui.js";const Z=["udid","name","host","platform"],q=["udid","name","host","platform"],G=["sessionId","sessionName","failureReason","udid","deviceName","platform","osVersion","startTime","endTime"],Q=["windowDays","totalHeals","distinctSelectors"],Y={device_offline:Z,device_new:q,session_failed:G,selector_health_digest:Q},ee=Y,se={eventType:"The event: device_offline, session_failed, ...",udid:"The device's id",name:"The device's name",host:"The machine it is plugged into",platform:"android or ios",sessionId:"The session's id",sessionName:"The session's name, if the test gave it one",failureReason:"Why it failed",deviceName:"The device's name",osVersion:"The device's OS version",startTime:"When it started (ISO 8601)",endTime:"When it ended (ISO 8601)",windowDays:"How many days the digest covers",totalHeals:"Heals in that time",distinctSelectors:"Selectors that were healed"};function te(o){const c=["eventType"];for(const u of o)for(const h of ee[u]??[])c.includes(h)||c.push(h);return c}const ae=[{id:"slack",label:"Slack message"},{id:"webhook",label:"JSON (event and payload)"}];function oe(o){return o.payloadTemplate?"CUSTOM":o.type==="slack"?"SLACK":"JSON"}const j=[{id:"device_offline",label:"Device offline",icon:V,tone:"red"},{id:"device_new",label:"New device",icon:_,tone:"green"},{id:"session_failed",label:"Session failed",icon:J,tone:"amber"},{id:"selector_health_digest",label:"Selector health digest",icon:I,tone:"green"}],be=()=>{const{toast:o}=P(),[c,u]=l.useState([]),[h,N]=l.useState(!0),[r,y]=l.useState(""),[i,S]=l.useState(["device_offline","session_failed"]),[f,T]=l.useState(!1),[C,A]=l.useState(!1),[d,E]=l.useState(!1),[m,g]=l.useState(""),[b,O]=l.useState("slack");l.useEffect(()=>{v()},[]);const v=async()=>{N(!0);try{const s=await p.getWebhookConfigs();u(s||[])}catch(s){console.error("Failed to load webhook configs",s),o("Failed to load webhooks.","error")}finally{N(!1)}},W=async()=>{if(!r.trim()){o("Webhook URL is required.","error");return}if(i.length===0){o("Select at least one trigger event.","error");return}T(!0);try{await p.addWebhookConfig(r.trim(),i,b,m||void 0),y(""),g(""),E(!1),O("slack"),S(["device_offline","session_failed"]),await v(),o("Webhook saved.","success")}catch(s){console.error("Failed to add webhook",s),o(`Failed to add webhook: ${s.message||"unknown error"}`,"error")}finally{T(!1)}},$=async(s,t)=>{if(window.confirm(`Remove webhook for "${t}"?`))try{await p.deleteWebhookConfig(s),await v(),o("Webhook removed.","success")}catch(a){console.error("Failed to delete webhook",a),o(`Failed to delete: ${a.message||"unknown error"}`,"error")}},L=async()=>{if(!r.trim()||i.length===0)return;A(!0);const s=t=>{var a;return((a=j.find(n=>n.id===t))==null?void 0:a.label)??t};try{for(const t of i)try{await p.testWebhook(r.trim(),b,m||void 0,t)}catch(a){o(`Test failed for ${s(t)}: ${a.message||"check the URL"}`,"error");return}o(i.length===1?`Test message delivered: ${s(i[0])}.`:`Test messages delivered: ${i.map(s).join(", ")}.`,"success")}finally{A(!1)}},R=s=>{S(t=>t.includes(s)?t.filter(a=>a!==s):[...t,s])},z=s=>{g(t=>t+`{{${s}}} `)};return e.jsxs("div",{className:"settings-container",children:[e.jsx(B,{icon:w,title:"Notifications",subtitle:"Configure Slack or generic webhooks to receive alerts for critical infrastructure events."}),e.jsxs("div",{className:"settings-content",children:[h?e.jsxs("div",{className:"settings-loading",style:{height:200},children:[e.jsx(x,{className:"animate-spin",size:28}),e.jsx("span",{children:"Loading webhooks…"})]}):c.length===0?e.jsxs("div",{className:"empty-state",children:[e.jsx("div",{className:"empty-state__icon",children:e.jsx(w,{size:28})}),e.jsx("h3",{className:"empty-state__title",children:"No webhooks configured"}),e.jsx("p",{className:"empty-state__copy",children:"Wire up a Slack incoming webhook or any HTTPS endpoint to receive alerts when devices go offline, new devices register, or sessions fail. Configure your first webhook below."})]}):e.jsx("div",{className:"webhook-list-grid",children:c.map(s=>{const t=(()=>{try{return JSON.parse(s.events)}catch{return[]}})();return e.jsxs("div",{className:"webhook-row-card",children:[e.jsxs("div",{className:"webhook-row-card__header",children:[e.jsx("span",{className:`pill-chip ${s.payloadTemplate?"pill-chip--admin":"pill-chip--scope"}`,children:oe(s)}),e.jsx("span",{className:"webhook-row-card__url",title:s.url,children:s.url}),e.jsxs("button",{type:"button",className:"row-action row-action--danger",onClick:()=>$(s.id,s.url),"aria-label":`Remove webhook for ${s.url}`,children:[e.jsx(D,{size:13}),e.jsx("span",{children:"Remove"})]})]}),e.jsx("div",{className:"webhook-row-card__events",children:t.length===0?e.jsx("span",{className:"webhook-row-card__no-events",children:"No triggers"}):t.map(a=>{const n=j.find(H=>H.id===a),F=(n==null?void 0:n.icon)??w;return e.jsxs("span",{className:`event-pill event-pill--${(n==null?void 0:n.tone)||"gray"}`,children:[e.jsx(F,{size:11}),(n==null?void 0:n.label)||a]},a)})})]},s.id)})}),e.jsxs("div",{className:"add-webhook-card",children:[e.jsxs("div",{className:"add-webhook-card__header",children:[e.jsx(_,{size:14}),e.jsx("span",{children:"Add a new webhook"})]}),e.jsxs("div",{className:"add-webhook-card__body",children:[e.jsx(k,{label:"Webhook URL",description:"The endpoint we POST webhook events to.",htmlFor:"webhook-url",children:e.jsx("div",{className:"setting-input-wrapper",children:e.jsx("input",{id:"webhook-url",type:"url",placeholder:"https://hooks.slack.com/services/...",value:r,onChange:s=>y(s.target.value)})})}),e.jsx(k,{label:"Message format",description:m.trim()?"Your custom payload is sent as written, whatever the format.":"Slack message for a Slack incoming webhook; JSON for anything else.",htmlFor:"webhook-format",children:e.jsx(U,{id:"webhook-format",value:b,onChange:s=>O(s.target.value),children:ae.map(s=>e.jsx("option",{value:s.id,children:s.label},s.id))})}),e.jsx(k,{label:"Trigger events",children:e.jsx("div",{className:"event-toggle-grid",children:j.map(s=>{const t=i.includes(s.id),a=s.icon;return e.jsxs("button",{type:"button",role:"checkbox","aria-checked":t,className:`event-toggle event-toggle--${s.tone} ${t?"is-selected":""}`,onClick:()=>R(s.id),children:[e.jsx(a,{size:14,className:"event-toggle__lead"}),e.jsx("span",{children:s.label}),t&&e.jsx(K,{size:14,className:"event-toggle__check"})]},s.id)})})}),e.jsxs("div",{className:"template-section",children:[e.jsxs("button",{type:"button",className:"template-section__toggle",onClick:()=>E(!d),"aria-expanded":d,children:[e.jsx(M,{size:14,className:d?"tone-amber":"tone-dim"}),e.jsx("span",{children:"Use custom payload (optional)"}),e.jsx("span",{className:"template-section__indicator",children:d?"−":"+"})]}),d&&e.jsxs("div",{className:"template-editor",children:[e.jsx("p",{className:"template-editor__hint",children:"Define a JSON or text template. Click a name below to insert it; the names are the ones your selected events carry."}),e.jsx("div",{className:"template-editor__chips",children:te(i).map(s=>e.jsx("button",{type:"button",className:"template-editor__chip","aria-label":`{{${s}}}`,title:se[s],onClick:()=>z(s),children:`{{${s}}}`},s))}),e.jsx("textarea",{className:"template-editor__textarea",placeholder:'Example: { "text": "Alert: Device {{udid}} is offline!" }',value:m,onChange:s=>g(s.target.value),rows:4})]})]})]}),e.jsxs("div",{className:"add-webhook-card__footer",children:[e.jsxs("button",{type:"button",className:"page-header-action page-header-action--ghost",onClick:L,disabled:!r.trim()||i.length===0||C,"aria-label":"Send test",title:i.length===0?"Select a trigger event to send a sample of":"Sends a sample message for each selected event",children:[C?e.jsx(x,{size:14,className:"animate-spin"}):e.jsx(X,{size:14}),e.jsx("span",{children:"Send test"})]}),e.jsxs("button",{type:"button",className:"page-header-action",onClick:W,disabled:f||!r.trim(),children:[f?e.jsx(x,{size:14,className:"animate-spin"}):e.jsx(_,{size:14}),e.jsx("span",{children:f?"Saving…":"Save webhook"})]})]})]})]})]})};export{be as WebhookSettings};
@@ -1,4 +1,4 @@
1
- import{c as a}from"./index-DI1fGIyj.js";/**
1
+ import{c as a}from"./index-BbKEXUFH.js";/**
2
2
  * @license lucide-react v0.555.0 - ISC
3
3
  *
4
4
  * This source code is licensed under the ISC license.
@@ -36,7 +36,7 @@
36
36
  Learn how to configure a non-root public URL by running `npm run build`.
37
37
  -->
38
38
  <title>Xenon</title>
39
- <script type="module" crossorigin src="/xenon/assets/index-DI1fGIyj.js"></script>
39
+ <script type="module" crossorigin src="/xenon/assets/index-BbKEXUFH.js"></script>
40
40
  <link rel="stylesheet" crossorigin href="/xenon/assets/index-Bxoxt9sV.css">
41
41
  </head>
42
42
 
package/lib/schema.json CHANGED
@@ -67,7 +67,7 @@
67
67
  "maxSessions": {
68
68
  "type": "number",
69
69
  "default": 8,
70
- "description": "Maximum number of Appium sessions this node will run concurrently. Additional requests queue until a slot frees."
70
+ "description": "Maximum number of Appium sessions this server runs at once. A new session waits until fewer are running or being started. A live preview, a recording, or an SDK lease that has no session on it does not use a slot; a session started on a leased phone does, but is never held back itself. On a hub the count includes its nodes' phones. A value below 1 means no limit."
71
71
  },
72
72
  "cloud": {
73
73
  "type": "object",
@@ -87,7 +87,7 @@
87
87
  "$ref": "#/definitions/EmulatorConfig"
88
88
  },
89
89
  "default": [],
90
- "description": "Allow-list of Android emulator AVDs to expose. Empty array means expose all discoverable emulators."
90
+ "description": "Android emulators (AVDs) to boot when the server starts, each as `{ \"avdName\": \"Pixel_7\", ... }`. Any other field is a launch option passed to the emulator: `args`, `env`, `language`, `country`, `launchTimeout`, `readyTimeout`, `retryTimes`. Nothing is booted when `platform` is `ios` or `androidDeviceType` is `real`. It does not limit which emulators are discovered: every emulator that is running is found, as `bootedEmulators` allows."
91
91
  },
92
92
  "proxy": {
93
93
  "type": "object",
@@ -146,16 +146,16 @@
146
146
  "removeDevicesFromDatabaseBeforeRunningThePlugin": {
147
147
  "type": "boolean",
148
148
  "default": false,
149
- "description": "Wipe the persisted Device table at startup so discovery begins from a clean slate. Useful after hardware changes."
149
+ "description": "At startup, also forget what was set for this server's own phones (team, tags, maintenance, reservations), so each comes back as a new phone. Without it a phone keeps those whenever it reconnects, through restarts. A node forgets them for every phone it has; a hub keeps its nodes' phones and theirs either way."
150
150
  },
151
151
  "healthCheckIntervalMs": {
152
152
  "type": "number",
153
153
  "default": 300000,
154
- "description": "Default interval (ms) between background device health checks. Overridden when `healthCheckSchedule` is set. Default 5 minutes — frequent enough to keep battery/thermal badges fresh without hammering devices."
154
+ "description": "Default interval (ms) between background device health checks. Overridden when `healthCheckSchedule` is set. Default 5 minutes — frequent enough to keep battery/thermal badges fresh without hammering devices. A value saved on the dashboard's Settings page replaces this one."
155
155
  },
156
156
  "healthCheckSchedule": {
157
157
  "type": "string",
158
- "description": "Cron expression for the device health-check job (e.g. '0 * * * *' for hourly). When set, takes precedence over `healthCheckIntervalMs`."
158
+ "description": "Cron expression for the device health-check job (e.g. '0 * * * *' for hourly). When set, takes precedence over `healthCheckIntervalMs`. A schedule saved on the dashboard's Settings page replaces this one."
159
159
  },
160
160
  "databaseProvider": {
161
161
  "type": "string",
@@ -163,11 +163,11 @@
163
163
  "sqlite",
164
164
  "postgresql"
165
165
  ],
166
- "description": "Database backend. Defaults to sqlite (file under ~/.cache/xenon). Use postgresql for multi-node hub deployments."
166
+ "description": "Database backend. The published plugin stores its data in SQLite only, and each server, hub or node, has its own database. postgresql is accepted so older configs still start, and has no effect: the database URL decides."
167
167
  },
168
168
  "databaseUrl": {
169
169
  "type": "string",
170
- "description": "Prisma-style database URL. For sqlite: `file:/path/to/xenon.db`. For postgres: `postgresql://user:pass@host/db`. Falls back to the DATABASE_URL env var."
170
+ "description": "Where the SQLite database lives, as `file:/path/to/xenon.db`. Falls back to the DATABASE_URL env var, then to a file under ~/.cache/xenon. A PostgreSQL URL stops the server at startup."
171
171
  },
172
172
  "aiProvider": {
173
173
  "type": "string",
@@ -202,27 +202,27 @@
202
202
  "enableSelfHealing": {
203
203
  "type": "boolean",
204
204
  "default": true,
205
- "description": "Enable the 5-tier self-healing pipeline (Native → Fuzzy XML → OCR → Visual AI → LLM) for failed findElement calls. Can also be toggled at runtime from the dashboard."
205
+ "description": "Enable the self-healing pipeline (etalon recovery → Native → Fuzzy XML → OCR → Visual AI → LLM) for failed findElement calls. A value saved with the AI self-healing switch on the dashboard's Settings page replaces this one, and applies from the next command without a restart. A session can limit the tiers it uses with `xe:options.healingTiers`; that cannot turn healing off."
206
206
  },
207
207
  "buildCleanupDays": {
208
208
  "type": "number",
209
209
  "default": 30,
210
- "description": "Builds/sessions older than this many days are purged by the cleanup job."
210
+ "description": "Builds/sessions older than this many days are purged by the cleanup job. A value saved on the dashboard's Maintenance page replaces this one, and applies at the next cleanup run without a restart."
211
211
  },
212
212
  "buildCleanupMaxCount": {
213
213
  "type": "number",
214
214
  "default": 100,
215
- "description": "Maximum number of builds to retain. Oldest-first eviction beyond this cap regardless of `buildCleanupDays`."
215
+ "description": "Maximum number of builds to retain. Oldest-first eviction beyond this cap regardless of `buildCleanupDays`. A value saved on the dashboard's Maintenance page replaces this one."
216
216
  },
217
217
  "buildCleanupSchedule": {
218
218
  "type": "string",
219
219
  "default": "0 0 * * *",
220
- "description": "Cron expression for the retention job. Default '0 0 * * *' runs at midnight."
220
+ "description": "Cron expression for the retention job. Default '0 0 * * *' runs at midnight. A schedule saved on the dashboard's Maintenance page replaces this one, and takes effect at once."
221
221
  },
222
222
  "deleteBuildAssets": {
223
223
  "type": "boolean",
224
224
  "default": true,
225
- "description": "When true, the cleanup job also deletes session video recordings and screenshots from disk (not just DB rows)."
225
+ "description": "When true, the cleanup job also deletes session video recordings and screenshots from disk (not just DB rows). A value saved on the dashboard's Maintenance page replaces this one."
226
226
  },
227
227
  "recordingCleanupDays": {
228
228
  "type": "number",
@@ -246,8 +246,7 @@
246
246
  },
247
247
  "enableJsonLogging": {
248
248
  "type": "boolean",
249
- "default": false,
250
- "description": "Emit structured JSON log lines instead of human-readable text. Recommended for shipping logs to a log aggregator."
249
+ "description": "Emit structured JSON log lines instead of human-readable text. Recommended for shipping logs to a log aggregator. Off by default. When unset, the XENON_JSON_LOGGING environment variable decides (`true` turns it on); setting this to true or false overrides the variable."
251
250
  },
252
251
  "tlsRejectUnauthorized": {
253
252
  "type": "boolean",
@@ -261,18 +260,17 @@
261
260
  },
262
261
  "interceptor": {
263
262
  "type": "object",
264
- "description": "Network request interception configuration (capture + mock + modify). See InterceptorConfig interface for details. Android-only in v1."
263
+ "description": "Network capture (the interceptor) for sessions on this server's own Android phones: the default each session gets when its own interceptor capability doesn't say. A session's capability wins field by field. Fields: enabled, bufferSize, captureBodies. Android only."
265
264
  },
266
265
  "maxConcurrentRecordings": {
267
266
  "type": "integer",
268
267
  "minimum": 1,
269
268
  "maximum": 16,
270
- "default": 4,
271
- "description": "Server-wide hard cap on simultaneous free-form (non-session) screen recordings across all users. Automation session recording is exempt and not counted against this cap."
269
+ "description": "Server-wide hard cap on simultaneous free-form (non-session) screen recordings across all users. Automation session recording is exempt and not counted against this cap. Default 4. When unset, the XENON_MAX_CONCURRENT_RECORDINGS environment variable is used if it is a whole number of at least 1."
272
270
  },
273
271
  "recordingsAssetsPath": {
274
272
  "type": "string",
275
- "description": "Override directory for free-form recording artifacts. Defaults to <sessionAssetsPath>/recordings."
273
+ "description": "Directory for free-form recording artifacts. When unset, the XENON_RECORDINGS_ASSETS_PATH environment variable is used if it is set, else ~/.cache/xenon/assets/sessions/recordings."
276
274
  },
277
275
  "autowait": {
278
276
  "type": "object",
@@ -449,17 +447,17 @@
449
447
  "enabled": {
450
448
  "type": "boolean",
451
449
  "default": false,
452
- "description": "Enable the network interceptor. Sessions still need to opt in via any of: xe:options.interceptor.enabled=true (or the xenon:options alias), xe:interceptor.enabled=true, appium:interceptor.enabled=true, a bare interceptor.enabled=true cap, or the flat interceptorEnabled cap."
450
+ "description": "Capture the network traffic of every Android session on this server that doesn't say otherwise. A session turns capture on or off for itself with its interceptor capability (xe:interceptor.enabled, xe:options.interceptor.enabled, the flat interceptorEnabled, ...), which wins over this. While capture runs, the phone's global HTTP proxy points at Xenon; it is put back when the session ends, however it ends."
453
451
  },
454
452
  "bufferSize": {
455
453
  "type": "number",
456
454
  "default": 1000,
457
- "description": "Maximum number of captured requests to retain in-memory per session before evicting oldest."
455
+ "description": "Maximum number of captured requests to retain in-memory per session before evicting oldest. The default for sessions whose interceptor capability doesn't set bufferSize."
458
456
  },
459
457
  "captureBodies": {
460
458
  "type": "boolean",
461
459
  "default": true,
462
- "description": "Whether to capture request/response bodies. Disable for privacy or to reduce memory usage."
460
+ "description": "Whether to capture request/response bodies. Disable for privacy or to reduce memory usage. The default for sessions whose interceptor capability doesn't set captureBodies."
463
461
  }
464
462
  }
465
463
  },
@@ -303,11 +303,13 @@ function getXenonCapabilities(caps) {
303
303
  // Accept either a structured object (`xe:interceptor: { enabled, mocks, ... }`)
304
304
  // or individual flat keys for users who prefer them.
305
305
  const interceptorObj = getAnyCap(XENON_CAPABILITIES.INTERCEPTOR, 'interceptor');
306
+ // `enabled` and `captureBodies` stay undefined when the session doesn't set
307
+ // them: the server's `interceptor` option decides those
308
+ // (resolveInterceptorOptions).
306
309
  if (interceptorObj && typeof interceptorObj === 'object') {
307
- capabilities[XENON_CAPABILITIES.INTERCEPTOR_ENABLED] = !!interceptorObj.enabled;
310
+ capabilities[XENON_CAPABILITIES.INTERCEPTOR_ENABLED] = flagOrUnset(interceptorObj.enabled);
308
311
  capabilities[XENON_CAPABILITIES.INTERCEPTOR_BUFFER_SIZE] = interceptorObj.bufferSize;
309
- capabilities[XENON_CAPABILITIES.INTERCEPTOR_CAPTURE_BODIES] =
310
- interceptorObj.captureBodies !== false;
312
+ capabilities[XENON_CAPABILITIES.INTERCEPTOR_CAPTURE_BODIES] = flagOrUnset(interceptorObj.captureBodies);
311
313
  capabilities[XENON_CAPABILITIES.INTERCEPTOR_MOCKS] = Array.isArray(interceptorObj.mocks)
312
314
  ? interceptorObj.mocks
313
315
  : [];
@@ -321,9 +323,9 @@ function getXenonCapabilities(caps) {
321
323
  else {
322
324
  const enabledFlat = getAnyCap(XENON_CAPABILITIES.INTERCEPTOR_ENABLED, 'interceptorEnabled');
323
325
  capabilities[XENON_CAPABILITIES.INTERCEPTOR_ENABLED] =
324
- enabledFlat !== undefined ? String(enabledFlat) === 'true' : false;
326
+ enabledFlat !== undefined ? String(enabledFlat) === 'true' : undefined;
325
327
  capabilities[XENON_CAPABILITIES.INTERCEPTOR_BUFFER_SIZE] = getAnyCap(XENON_CAPABILITIES.INTERCEPTOR_BUFFER_SIZE, 'interceptorBufferSize');
326
- capabilities[XENON_CAPABILITIES.INTERCEPTOR_CAPTURE_BODIES] = true;
328
+ capabilities[XENON_CAPABILITIES.INTERCEPTOR_CAPTURE_BODIES] = undefined;
327
329
  capabilities[XENON_CAPABILITIES.INTERCEPTOR_MOCKS] = [];
328
330
  const includeFlat = getAnyCap(XENON_CAPABILITIES.INTERCEPTOR_INCLUDE_HOSTS, 'interceptorIncludeHosts');
329
331
  const excludeFlat = getAnyCap(XENON_CAPABILITIES.INTERCEPTOR_EXCLUDE_HOSTS, 'interceptorExcludeHosts');
@@ -341,3 +343,18 @@ function getXenonCapabilities(caps) {
341
343
  `SaveLogs=${capabilities[XENON_CAPABILITIES.SAVE_DEVICE_LOGS]}`);
342
344
  return capabilities;
343
345
  }
346
+ /** true/false (or 'true'/'false'), undefined when not given; anything else by truthiness. */
347
+ function flagOrUnset(value) {
348
+ if (value === undefined || value === null)
349
+ return undefined;
350
+ if (typeof value === 'boolean')
351
+ return value;
352
+ if (typeof value === 'string') {
353
+ const v = value.trim().toLowerCase();
354
+ if (v === 'true')
355
+ return true;
356
+ if (v === 'false')
357
+ return false;
358
+ }
359
+ return !!value;
360
+ }