@xenon-device-management/xenon 2.16.0 → 2.17.1

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 (135) hide show
  1. package/README.md +2 -2
  2. package/lib/package.json +2 -3
  3. package/lib/public/assets/{AnnotationOverlay-2W4JvEyZ.js → AnnotationOverlay-CB8njQvS.js} +1 -1
  4. package/lib/public/assets/{ApiKeyGate-GmckX_7r.js → ApiKeyGate-B-804QBS.js} +1 -1
  5. package/lib/public/assets/DeviceMosaicView-BsDvfjR1.js +2 -0
  6. package/lib/public/assets/{EmptyState-7hETKEWb.js → EmptyState-WzLw1cEV.js} +1 -1
  7. package/lib/public/assets/{FieldGroup-GOgssFEP.js → FieldGroup-CaQNXgcE.js} +1 -1
  8. package/lib/public/assets/{FilterMenu-Yvf6RGMd.js → FilterMenu-C9gAiypL.js} +1 -1
  9. package/lib/public/assets/{Menu-KrPnfdsf.js → Menu-CfTti3rw.js} +1 -1
  10. package/lib/public/assets/{Modal-C6WjZFeI.js → Modal-BcZUFhDm.js} +1 -1
  11. package/lib/public/assets/{RecordingPage-CSRIhZEh.js → RecordingPage-B_pbl3UE.js} +1 -1
  12. package/lib/public/assets/{RecordingsPage-CB8ST3nE.js → RecordingsPage-BkVhplE3.js} +1 -1
  13. package/lib/public/assets/{SegmentedControl-C9o90KId.js → SegmentedControl-CDLMLNl5.js} +1 -1
  14. package/lib/public/assets/{SettingCard-CPm3w9l2.js → SettingCard-Lyw5CB1F.js} +1 -1
  15. package/lib/public/assets/{Table-DwIgERRt.js → Table-CK414bOU.js} +1 -1
  16. package/lib/public/assets/{activity-DrLbHifn.js → activity-pEyAdE-M.js} +1 -1
  17. package/lib/public/assets/{ai-settings-DU_Ls06P.js → ai-settings-CIhS7MMq.js} +1 -1
  18. package/lib/public/assets/{api-keys-CWYdEzJ_.js → api-keys-D2J44Ebv.js} +1 -1
  19. package/lib/public/assets/{apps-BqNVdln7.js → apps-CCV3RZY8.js} +1 -1
  20. package/lib/public/assets/{arrow-left-BtB0_Jg_.js → arrow-left-DaEUmERk.js} +1 -1
  21. package/lib/public/assets/{arrow-right-C8NhdtyN.js → arrow-right-CnRXHXit.js} +1 -1
  22. package/lib/public/assets/{arrow-up-right-gMg0ffvv.js → arrow-up-right-BGCS7zhi.js} +1 -1
  23. package/lib/public/assets/{auth-shell-CiYZ3N4M.js → auth-shell-CpW1zsxD.js} +2 -2
  24. package/lib/public/assets/{builds-page-EaaqDmLN.js → builds-page-B-xdzjMx.js} +1 -1
  25. package/lib/public/assets/{button-DDau4Axv.js → button-B5QkOVa4.js} +1 -1
  26. package/lib/public/assets/{calendar-DQ83B1ed.js → calendar-gX_1EKue.js} +1 -1
  27. package/lib/public/assets/{check-K1ovTQam.js → check-MIM4THvH.js} +1 -1
  28. package/lib/public/assets/{chevron-right-Biq00pMt.js → chevron-right-BZIp3-9t.js} +1 -1
  29. package/lib/public/assets/{circle-check-CBv88cH7.js → circle-check-Crt-G_uy.js} +1 -1
  30. package/lib/public/assets/{circle-x-Bmltailn.js → circle-x-C8GvvXVt.js} +1 -1
  31. package/lib/public/assets/{clock-BPrld-3h.js → clock-MFKV4qGa.js} +1 -1
  32. package/lib/public/assets/{copy-t0QB14JI.js → copy-BU-Z7mBK.js} +1 -1
  33. package/lib/public/assets/device-explorer-D-78Twa0.js +282 -0
  34. package/lib/public/assets/{download-BroHxNOh.js → download-DIoLCCYv.js} +1 -1
  35. package/lib/public/assets/{forgot-password-Bjmf49i6.js → forgot-password-vNYlPnia.js} +1 -1
  36. package/lib/public/assets/{h264Stream-DeD__6wa.js → h264Stream-BLcSplMU.js} +1 -1
  37. package/lib/public/assets/{index-B0gQkEg0.js → index-BpqyclNY.js} +2 -2
  38. package/lib/public/assets/{index-B0JeuZbg.js → index-Hl1OL-k_.js} +1 -1
  39. package/lib/public/assets/{index-72oF89LJ.js → index-QbukVVrl.js} +1 -1
  40. package/lib/public/assets/{input-DIc4MSC7.js → input-DHH3fHer.js} +1 -1
  41. package/lib/public/assets/{line-chart-BcWm2vW8.js → line-chart-PdViEphw.js} +1 -1
  42. package/lib/public/assets/{list-checks-B06IJJad.js → list-checks-DI_heIg9.js} +1 -1
  43. package/lib/public/assets/{lock-pVLvnN30.js → lock-DVP_dL6J.js} +1 -1
  44. package/lib/public/assets/{login-CN77vy--.js → login-B8vtTDcr.js} +1 -1
  45. package/lib/public/assets/{maintenance-settings-CSdzQSkY.js → maintenance-settings-BMa5EfgI.js} +1 -1
  46. package/lib/public/assets/{monitor-BtrN5IkI.js → monitor-CtzX_RkD.js} +1 -1
  47. package/lib/public/assets/{mouse-pointer-2-CNsQfo6w.js → mouse-pointer-2-BA09sdS9.js} +1 -1
  48. package/lib/public/assets/{network-BSfA8ZxG.js → network-B_F9Nox-.js} +1 -1
  49. package/lib/public/assets/{overview-BfTebCK5.js → overview-kWCYMHMV.js} +1 -1
  50. package/lib/public/assets/{page-header-D8KUX6Qg.js → page-header-CAbH1O1a.js} +1 -1
  51. package/lib/public/assets/{play-Cvc8xzic.js → play-sg95hLqA.js} +1 -1
  52. package/lib/public/assets/{plus-C9Zj84UF.js → plus-BT9mk2gj.js} +1 -1
  53. package/lib/public/assets/{profile-page-Bi27STgs.js → profile-page-DZOz5Ypc.js} +1 -1
  54. package/lib/public/assets/{reset-password-CPsOuwSu.js → reset-password-CqeImlEO.js} +1 -1
  55. package/lib/public/assets/{runbook-page-DEQ-DBOX.js → runbook-page-U_8TSysj.js} +1 -1
  56. package/lib/public/assets/{select-BWpnhWXj.js → select-Cx4JDzsR.js} +1 -1
  57. package/lib/public/assets/{selector-detail-redirect-BefSCSpe.js → selector-detail-redirect-DiMRe7DW.js} +1 -1
  58. package/lib/public/assets/{selector-health-page-MSk96Zz7.js → selector-health-page-CKR5URll.js} +1 -1
  59. package/lib/public/assets/{session-detail-page-DPyBNef_.js → session-detail-page-DuIVzTW_.js} +1 -1
  60. package/lib/public/assets/{settings-V6x-cXwy.js → settings-B5fuvgTP.js} +1 -1
  61. package/lib/public/assets/{stat-tile-Duex-GZD.js → stat-tile-CtHX1dkh.js} +1 -1
  62. package/lib/public/assets/{tablet-BBcoqBmL.js → tablet-DCGD0GQt.js} +1 -1
  63. package/lib/public/assets/{teams-ts8p5rEh.js → teams-MzlELlw9.js} +1 -1
  64. package/lib/public/assets/{trash-2-CZ2ew6AZ.js → trash-2-BIRm2KVs.js} +1 -1
  65. package/lib/public/assets/{upload-Dv99y_le.js → upload-C-lgyswH.js} +1 -1
  66. package/lib/public/assets/{use-builds-data-DrYC8JCF.js → use-builds-data-IOydXA8d.js} +1 -1
  67. package/lib/public/assets/{use-password-reset-mode-C7QkBNkP.js → use-password-reset-mode-CIVXG7Me.js} +1 -1
  68. package/lib/public/assets/{users-gJ5Y01yo.js → users-hwK5XGIy.js} +1 -1
  69. package/lib/public/assets/{users-D7tKMESH.js → users-ivNCjeus.js} +1 -1
  70. package/lib/public/assets/{video-off-CqZVuZGs.js → video-off-DvcIEsdZ.js} +1 -1
  71. package/lib/public/assets/{webhook-settings-DI5DSW99.js → webhook-settings-DukgCe1u.js} +1 -1
  72. package/lib/public/assets/{zap-ClcHFxfo.js → zap-BE9gacFj.js} +1 -1
  73. package/lib/public/index.html +1 -1
  74. package/lib/schema.json +1 -1
  75. package/lib/src/app/index.js +2 -0
  76. package/lib/src/app/openapi/control.yaml +69 -4
  77. package/lib/src/app/openapi/grid.yaml +102 -0
  78. package/lib/src/app/routers/control.js +24 -5
  79. package/lib/src/dashboard/event-manager.js +5 -8
  80. package/lib/src/device-managers/ios/IOSStreamService.js +147 -18
  81. package/lib/src/device-utils.js +61 -24
  82. package/lib/src/gateway/internalCall.js +33 -5
  83. package/lib/src/gateway/nodeAsk.js +57 -0
  84. package/lib/src/gateway/nodeSessionDeviceLogs.js +35 -0
  85. package/lib/src/gateway/sessionGateway.js +9 -4
  86. package/lib/src/helpers/index.js +9 -6
  87. package/lib/src/interceptors/CommandInterceptor.js +10 -0
  88. package/lib/src/plugin.js +4 -1
  89. package/lib/src/scripts/initialize-database.js +0 -6
  90. package/lib/src/scripts/run-migrations.js +123 -880
  91. package/lib/src/services/ServerManager.js +25 -4
  92. package/lib/src/services/SessionLifecycleService.js +20 -5
  93. package/lib/src/services/UserService.js +8 -3
  94. package/lib/src/services/interceptor/CertManager.js +50 -16
  95. package/lib/src/services/logcat/NodeDeviceLogStore.js +99 -0
  96. package/lib/src/services/logcat/NodeDeviceLogsCollector.js +164 -0
  97. package/lib/src/services/logcat/SessionDeviceLogs.js +199 -44
  98. package/lib/src/services/logcat/nodeDeviceLogs.js +91 -0
  99. package/lib/src/services/metrics/nodeMetrics.js +20 -49
  100. package/lib/src/sessions/RemoteSession.js +11 -2
  101. package/lib/src/sessions/SessionManager.js +28 -1
  102. package/lib/src/sessions/appiumUmbrella.js +11 -0
  103. package/lib/src/sessions/sessionMemory.js +11 -2
  104. package/lib/test/helpers/fake-logcat.js +80 -0
  105. package/lib/test/helpers/hub-node-pair.js +227 -0
  106. package/lib/test/unit/UserService.test.js +8 -0
  107. package/lib/test/unit/boot-device-sync.spec.js +195 -0
  108. package/lib/test/unit/dashboard-events-device-logs.spec.js +15 -0
  109. package/lib/test/unit/hub-node-gateway-appium-server.spec.js +21 -0
  110. package/lib/test/unit/hub-node-iphone-readiness.spec.js +134 -0
  111. package/lib/test/unit/hub-node-session-device-logs.spec.js +194 -0
  112. package/lib/test/unit/hub-node-session-metrics.spec.js +24 -192
  113. package/lib/test/unit/hub-remote-session-heartbeat.spec.js +8 -1
  114. package/lib/test/unit/install-scripts.spec.js +98 -0
  115. package/lib/test/unit/interceptor/CertManager.spec.js +89 -0
  116. package/lib/test/unit/internal-call.spec.js +99 -0
  117. package/lib/test/unit/internal-calls-session-record.spec.js +312 -0
  118. package/lib/test/unit/ios-simulator-preview.spec.js +315 -0
  119. package/lib/test/unit/is-xenon-running.spec.js +10 -0
  120. package/lib/test/unit/node-device-log-store.spec.js +102 -0
  121. package/lib/test/unit/node-device-logs-collector.spec.js +343 -0
  122. package/lib/test/unit/node-gateway-create-appium-server.spec.js +8 -1
  123. package/lib/test/unit/node-hub-session-owner.spec.js +8 -1
  124. package/lib/test/unit/node-session-device-logs-route.spec.js +103 -0
  125. package/lib/test/unit/run-migrations-database.spec.js +57 -424
  126. package/lib/test/unit/run-migrations.test.js +74 -674
  127. package/lib/test/unit/session-device-logs-hub-node.spec.js +340 -0
  128. package/lib/test/unit/session-device-logs-lifecycle.spec.js +137 -0
  129. package/lib/test/unit/session-device-logs.spec.js +32 -94
  130. package/lib/test/unit/session-log-indexes.spec.js +15 -0
  131. package/lib/tsconfig.tsbuildinfo +1 -1
  132. package/package.json +2 -3
  133. package/schema.json +1 -1
  134. package/lib/public/assets/DeviceMosaicView-DqODzkyo.js +0 -2
  135. package/lib/public/assets/device-explorer-BgGZ8AvV.js +0 -282
@@ -1 +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-B0gQkEg0.js";/* empty css */import{F as k}from"./FieldGroup-GOgssFEP.js";import{S as U}from"./select-BWpnhWXj.js";import{P as B}from"./page-header-D8KUX6Qg.js";import{T as D}from"./trash-2-CZ2ew6AZ.js";import{P as _}from"./plus-C9Zj84UF.js";import{C as J}from"./circle-x-Bmltailn.js";import{C as K}from"./circle-check-CBv88cH7.js";import{Z as M}from"./zap-ClcHFxfo.js";import{A as X}from"./activity-DrLbHifn.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
+ 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-BpqyclNY.js";/* empty css */import{F as k}from"./FieldGroup-CaQNXgcE.js";import{S as U}from"./select-Cx4JDzsR.js";import{P as B}from"./page-header-CAbH1O1a.js";import{T as D}from"./trash-2-BIRm2KVs.js";import{P as _}from"./plus-BT9mk2gj.js";import{C as J}from"./circle-x-C8GvvXVt.js";import{C as K}from"./circle-check-Crt-G_uy.js";import{Z as M}from"./zap-BE9gacFj.js";import{A as X}from"./activity-pEyAdE-M.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-B0gQkEg0.js";/**
1
+ import{c as a}from"./index-BpqyclNY.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-B0gQkEg0.js"></script>
39
+ <script type="module" crossorigin src="/xenon/assets/index-BpqyclNY.js"></script>
40
40
  <link rel="stylesheet" crossorigin href="/xenon/assets/index-Bxoxt9sV.css">
41
41
  </head>
42
42
 
package/lib/schema.json CHANGED
@@ -163,7 +163,7 @@
163
163
  "sqlite",
164
164
  "postgresql"
165
165
  ],
166
- "description": "Has no effect, and is accepted only so older configs still start. Xenon stores its data in SQLite, one database per server, hub or node (see databaseUrl). At startup the database itself decides how its tables are brought up to date: from its migration history if that history matches its tables (Xenon first corrects a history that has fallen behind them), otherwise by matching them to this version's schema."
166
+ "description": "Has no effect, and is accepted only so older configs still start. Xenon stores its data in SQLite, one database per server, hub or node (see databaseUrl). At startup the database itself decides how its tables are brought up to date: from its migration history if that history matches its tables, otherwise by matching them to this version's schema."
167
167
  },
168
168
  "databaseUrl": {
169
169
  "type": "string",
@@ -90,6 +90,7 @@ const PluginContext_1 = require("../PluginContext");
90
90
  const nodeWebDriverUrl_1 = require("../gateway/nodeWebDriverUrl");
91
91
  const nodeSessionStatus_1 = require("../gateway/nodeSessionStatus");
92
92
  const nodeSessionMetrics_1 = require("../gateway/nodeSessionMetrics");
93
+ const nodeSessionDeviceLogs_1 = require("../gateway/nodeSessionDeviceLogs");
93
94
  const dashboardPluginLink_1 = require("./dashboardPluginLink");
94
95
  const ASYNC_LOCK = new async_lock_1.default();
95
96
  // An async handler's error reaches the error handlers, as on Express 5.
@@ -219,6 +220,7 @@ function createRouter(pluginArgs, ownServer) {
219
220
  // credentials for the node); it takes the hub's session token instead.
220
221
  (0, nodeSessionStatus_1.registerNodeSessionStatus)(apiRouter, pluginArgs);
221
222
  (0, nodeSessionMetrics_1.registerNodeSessionMetrics)(apiRouter, pluginArgs);
223
+ (0, nodeSessionDeviceLogs_1.registerNodeSessionDeviceLogs)(apiRouter, pluginArgs);
222
224
  // Dashboard login: unauthenticated (rate-limited internally via separate IP logic)
223
225
  apiRouter.use('/auth', (0, auth_1.authPublicRouter)()); // login, logout — unauthenticated
224
226
  // All remaining /api/* requires authentication (cookie session for
@@ -1941,8 +1941,12 @@ paths:
1941
1941
  summary: Start the live preview and hold the device
1942
1942
  description: >-
1943
1943
  Starts the device's live preview and holds the device for you (`session_id` becomes
1944
- `manual_<yourUserId>_<udid>`), so automation is not given it while you watch. iOS starts
1945
- WebDriverAgent's MJPEG stream (and the go-ios tunnel on iOS 17+). Android starts an MJPEG
1944
+ `manual_<yourUserId>_<udid>`), so automation is not given it while you watch. An iPhone
1945
+ starts WebDriverAgent's MJPEG stream (and the go-ios tunnel on iOS 17+). An iOS simulator
1946
+ starts nothing and is not held (its test holds it): it shows the picture of the test
1947
+ running on it, from the WebDriverAgent the XCUITest driver runs for that test, and with no
1948
+ test running it answers `409` with `error: simulator_needs_test` and a `message` saying
1949
+ so. Android starts an MJPEG
1946
1950
  screen capture, or, when the server's `streaming.androidH264` setting is on and the device
1947
1951
  is not being recorded, an H.264 stream for the WebSocket in `h264Path`. Missing screen
1948
1952
  dimensions are filled in and saved.
@@ -2035,7 +2039,34 @@ paths:
2035
2039
  '404':
2036
2040
  $ref: '#/components/responses/ControlDeviceNotFound'
2037
2041
  '409':
2038
- $ref: '#/components/responses/ControlHeld'
2042
+ description: >-
2043
+ Another user holds the device, or it runs another user's Appium session (admins are
2044
+ never refused), as for every device action. Or the device is an iOS simulator no test
2045
+ runs on (`simulator_needs_test`): its screen shows only while a test runs on it.
2046
+ content:
2047
+ application/json:
2048
+ schema:
2049
+ oneOf:
2050
+ - $ref: '#/components/schemas/ControlDenied'
2051
+ - $ref: '#/components/schemas/ControlSimulatorNeedsTest'
2052
+ examples:
2053
+ held:
2054
+ summary: Another user's preview or recording
2055
+ value:
2056
+ success: false
2057
+ error: device_held_by_another_user
2058
+ message: >-
2059
+ Device is being controlled by Priya Raman. Ask them to release it, or use an
2060
+ admin key to force-release.
2061
+ holder:
2062
+ userId: c7a1e5d2-3f4b-4a9e-8c21-5b6d7e8f9a01
2063
+ name: Priya Raman
2064
+ simulatorNeedsTest:
2065
+ summary: An iOS simulator no test runs on
2066
+ value:
2067
+ success: false
2068
+ error: simulator_needs_test
2069
+ message: A simulator's screen shows here only while a test runs on it.
2039
2070
  '429':
2040
2071
  $ref: '#/components/responses/RateLimited'
2041
2072
  '500':
@@ -2286,7 +2317,9 @@ paths:
2286
2317
  Says whether the device's preview is running and which transport a player should use
2287
2318
  (`type`: `h264` on Android when the server's `streaming.androidH264` setting is on and the
2288
2319
  device isn't being recorded, else `mjpeg`). A device with no preview answers `200` with
2289
- `status: stopped`. iOS adds WebDriverAgent's port, the start time and the last error.
2320
+ `status: stopped`. iOS adds WebDriverAgent's port, the start time and the last error. A
2321
+ simulator no test runs on answers `status: stopped` with `lastError` saying why it shows
2322
+ nothing, and `reason: simulator_needs_test`.
2290
2323
 
2291
2324
 
2292
2325
  Open to watchers. Needs the `MEMBER` role; any scope will do.
@@ -2326,6 +2359,15 @@ paths:
2326
2359
  udid: R5CT32ABCDE
2327
2360
  status: stopped
2328
2361
  type: mjpeg
2362
+ simulatorWithoutTest:
2363
+ summary: An iOS simulator no test runs on
2364
+ value:
2365
+ udid: 35429994-B029-4AF4-9634-880B89DAC782
2366
+ status: stopped
2367
+ type: mjpeg
2368
+ mjpegPort: 9101
2369
+ lastError: A simulator's screen shows here only while a test runs on it.
2370
+ reason: simulator_needs_test
2329
2371
  '400':
2330
2372
  description: The udid is malformed.
2331
2373
  content:
@@ -3147,6 +3189,13 @@ components:
3147
3189
  lastError:
3148
3190
  type: string
3149
3191
  description: iOS only.
3192
+ reason:
3193
+ type: string
3194
+ enum:
3195
+ - simulator_needs_test
3196
+ description: >-
3197
+ Set when the preview is refused rather than failed: `simulator_needs_test`, an iOS
3198
+ simulator no test runs on.
3150
3199
  ControlStreamLeaveResult:
3151
3200
  type: object
3152
3201
  properties:
@@ -3156,6 +3205,22 @@ components:
3156
3205
  pending:
3157
3206
  type: boolean
3158
3207
  example: true
3208
+ ControlSimulatorNeedsTest:
3209
+ type: object
3210
+ required:
3211
+ - error
3212
+ - message
3213
+ properties:
3214
+ success:
3215
+ type: boolean
3216
+ example: false
3217
+ error:
3218
+ type: string
3219
+ enum:
3220
+ - simulator_needs_test
3221
+ message:
3222
+ type: string
3223
+ description: Why the simulator shows nothing, for testers.
3159
3224
  ControlDenied:
3160
3225
  type: object
3161
3226
  required:
@@ -1736,6 +1736,82 @@ paths:
1736
1736
  $ref: '#/components/responses/GridNodeUnknownSession'
1737
1737
  '503':
1738
1738
  $ref: '#/components/responses/GridNodeTokenUnavailable'
1739
+ /api/node/sessions/{sessionId}/device-logs:
1740
+ get:
1741
+ operationId: getGridNodeSessionDeviceLogs
1742
+ summary: Collect a session's device log lines from this node
1743
+ description: |
1744
+ Node only: a hub collects the device log lines a node has recorded
1745
+ for a session on one of its phones, as soon as the session is
1746
+ created, every 10 s after that, and once more when the session
1747
+ ends. The hub writes them as the session's Device logs. Only a
1748
+ server started with `hub` set has this route.
1749
+
1750
+ The node records each session its hub created, with the same limit
1751
+ as a session on the hub's own phone (10,000 lines, then up to 2,000
1752
+ more errors), and keeps the lines in memory until the hub collects
1753
+ them, an ended session's for 10 minutes. If no hub asks within 2
1754
+ minutes of the start, the node stops recording that session.
1755
+
1756
+ Each line has a number, `seq`, counting up from 1. `after` asks for
1757
+ the lines after that number, and the node drops the others, so a
1758
+ later call never returns them again. An answer holds at most 2,000
1759
+ lines; `more` says the node has others, to ask for at once. `state`
1760
+ is `recording`, `ended`, or `off` (the node doesn't record this
1761
+ session). `timestamp` is the line's moment by the node's clock.
1762
+
1763
+ It is answered before the login, with the same rule as
1764
+ `GET /api/node/sessions/{sessionId}`: no credential with
1765
+ per-command auth off or auth disabled, otherwise the hub's session
1766
+ token for that session. Every answer from a node carries
1767
+ `x-xenon-node-device-logs: 1`. It isn't rate-limited.
1768
+ tags:
1769
+ - Hub-Node
1770
+ security:
1771
+ - HubToken: []
1772
+ - {}
1773
+ parameters:
1774
+ - $ref: '#/components/parameters/GridNodeSessionId'
1775
+ - in: query
1776
+ name: after
1777
+ required: false
1778
+ description: A line number; only later lines are returned, and the others are dropped. Anything that isn't a plain number is ignored.
1779
+ schema:
1780
+ type: integer
1781
+ example: 2000
1782
+ responses:
1783
+ '200':
1784
+ description: The session's lines.
1785
+ headers:
1786
+ x-xenon-node-device-logs:
1787
+ description: Always `1` on a node.
1788
+ schema:
1789
+ type: string
1790
+ example: '1'
1791
+ content:
1792
+ application/json:
1793
+ schema:
1794
+ type: object
1795
+ properties:
1796
+ value:
1797
+ $ref: '#/components/schemas/GridNodeDeviceLogs'
1798
+ example:
1799
+ value:
1800
+ state: recording
1801
+ more: false
1802
+ lines:
1803
+ - seq: 2001
1804
+ message: '10-05 14:20:02.114 4127 4127 I ActivityManager: Start proc 4127:com.example.shop/u0a215'
1805
+ timestamp: 1791111602114
1806
+ - seq: 2002
1807
+ message: '10-05 14:20:02.391 4127 4127 E AndroidRuntime: FATAL EXCEPTION: main'
1808
+ timestamp: 1791111602391
1809
+ '401':
1810
+ $ref: '#/components/responses/GridNotANodeUnauthorized'
1811
+ '404':
1812
+ $ref: '#/components/responses/GridNodeUnknownSession'
1813
+ '503':
1814
+ $ref: '#/components/responses/GridNodeTokenUnavailable'
1739
1815
  /api/status:
1740
1816
  get:
1741
1817
  operationId: getGridServerStatus
@@ -2304,6 +2380,32 @@ components:
2304
2380
  type: string
2305
2381
  nullable: true
2306
2382
  description: The app the app figures are for (Android), else null.
2383
+ GridNodeDeviceLogs:
2384
+ type: object
2385
+ properties:
2386
+ state:
2387
+ type: string
2388
+ enum:
2389
+ - recording
2390
+ - ended
2391
+ - 'off'
2392
+ more:
2393
+ type: boolean
2394
+ description: The node holds more lines than this answer; ask again with the last `seq`.
2395
+ lines:
2396
+ type: array
2397
+ items:
2398
+ type: object
2399
+ properties:
2400
+ seq:
2401
+ type: integer
2402
+ description: The line's number in the session, from 1.
2403
+ message:
2404
+ type: string
2405
+ description: The line as the device printed it.
2406
+ timestamp:
2407
+ type: integer
2408
+ description: Epoch milliseconds, by the node's clock.
2307
2409
  GridWebDriverError:
2308
2410
  type: object
2309
2411
  description: A WebDriver-style error, as Appium answers one.
@@ -58,7 +58,7 @@ const InternalHttpClient_1 = require("../../InternalHttpClient");
58
58
  const device_service_1 = require("../../data-service/device-service");
59
59
  const UniversalMjpegProxy_1 = require("../../helpers/UniversalMjpegProxy");
60
60
  const DisplayStateService_1 = require("../../services/DisplayStateService");
61
- const IOSStreamService_1 = __importDefault(require("../../device-managers/ios/IOSStreamService"));
61
+ const IOSStreamService_1 = __importStar(require("../../device-managers/ios/IOSStreamService"));
62
62
  const AndroidStreamService_1 = __importDefault(require("../../device-managers/android/AndroidStreamService"));
63
63
  const AndroidH264StreamService_1 = __importDefault(require("../../device-managers/android/AndroidH264StreamService"));
64
64
  const path_1 = __importDefault(require("path"));
@@ -748,9 +748,14 @@ router.post('/:udid/stream/start', (req, res) => __awaiter(void 0, void 0, void
748
748
  // Mark device as "Busy" so automation sessions don't pick it up.
749
749
  // Lock is keyed on the user, not the credential — see
750
750
  // src/services/device-access/deviceAccessPolicy.ts.
751
- const manualSid = (0, manualLock_1.formatManualLock)(actorUserId, udid);
752
- yield (0, device_service_1.blockDevice)(udid, device.host, manualSid);
753
- logger_1.default.info(`Manual Control: Device ${udid} locked for active UI session (${manualSid}).`);
751
+ // A simulator is previewed only while a test holds it, so a preview hold
752
+ // adds nothing, and written after the test ended it would hold a free
753
+ // simulator for nobody's test.
754
+ if (!(0, IOSStreamService_1.isIosSimulator)(device)) {
755
+ const manualSid = (0, manualLock_1.formatManualLock)(actorUserId, udid);
756
+ yield (0, device_service_1.blockDevice)(udid, device.host, manualSid);
757
+ logger_1.default.info(`Manual Control: Device ${udid} locked for active UI session (${manualSid}).`);
758
+ }
754
759
  if (streamType === 'h264') {
755
760
  logger_1.default.info(`H.264 stream started for ${udid}`);
756
761
  return res.status(200).send({
@@ -774,6 +779,13 @@ router.post('/:udid/stream/start', (req, res) => __awaiter(void 0, void 0, void
774
779
  });
775
780
  }
776
781
  catch (err) {
782
+ // Not a failure: a simulator shows its running test's picture, and none
783
+ // runs. Said in plain words; the dashboard toasts it.
784
+ if (err instanceof IOSStreamService_1.SimulatorPreviewNeedsTest) {
785
+ return res
786
+ .status(409)
787
+ .send({ success: false, error: 'simulator_needs_test', message: err.message });
788
+ }
777
789
  logger_1.default.error(`Failed to start stream for ${udid}: ${err.message}`);
778
790
  return res.status(500).send({
779
791
  success: false,
@@ -1037,6 +1049,9 @@ router.get('/:udid/stream/status', (req, res) => __awaiter(void 0, void 0, void
1037
1049
  type,
1038
1050
  h264Path,
1039
1051
  mjpegPort: device.mjpegServerPort,
1052
+ // Why a simulator shows nothing: a Live devices tile says it once it stops retrying.
1053
+ lastError: (0, IOSStreamService_1.simulatorPreviewRefusal)(device),
1054
+ reason: (0, IOSStreamService_1.simulatorPreviewRefusal)(device) ? 'simulator_needs_test' : undefined,
1040
1055
  });
1041
1056
  }));
1042
1057
  /**
@@ -1083,7 +1098,11 @@ router.get('/:udid/stream', (req, res) => __awaiter(void 0, void 0, void 0, func
1083
1098
  });
1084
1099
  }
1085
1100
  catch (err) {
1086
- logger_1.default.error(`Failed to start stream for ${udid}: ${err.message}`);
1101
+ // A simulator no test runs on: expected, and asked again by each retry.
1102
+ if (err instanceof IOSStreamService_1.SimulatorPreviewNeedsTest)
1103
+ logger_1.default.debug(`${udid}: ${err.message}`);
1104
+ else
1105
+ logger_1.default.error(`Failed to start stream for ${udid}: ${err.message}`);
1087
1106
  return res.status(503).send({
1088
1107
  error: 'Stream not available',
1089
1108
  message: err.message,
@@ -79,6 +79,7 @@ const NotificationService_1 = require("../services/NotificationService");
79
79
  const SessionMetricsService_1 = require("../services/metrics/SessionMetricsService");
80
80
  const SessionDeviceLogs_1 = require("../services/logcat/SessionDeviceLogs");
81
81
  const nodeMetrics_1 = require("../services/metrics/nodeMetrics");
82
+ const nodeDeviceLogs_1 = require("../services/logcat/nodeDeviceLogs");
82
83
  const sessionCommandSummary_1 = require("./sessionCommandSummary");
83
84
  const commandLogFields_1 = require("./commandLogFields");
84
85
  const typedi_2 = require("typedi");
@@ -156,19 +157,15 @@ let DashboardEventManager = class DashboardEventManager {
156
157
  // once its row exists (the lines point at it), unless the session turned
157
158
  // it off: an Android phone's stream, or the log an iPhone's or
158
159
  // simulator's driver captures. Not waited for: an Android phone's clock
159
- // and log stream are read in the background.
160
+ // and log stream are read in the background. A node's phone's lines are
161
+ // collected from the node, through the session.
160
162
  const deviceLogs = typedi_1.Container.get(SessionDeviceLogs_1.SessionDeviceLogs);
161
163
  if (capabilities[XenonCapabilityManager_1.XENON_CAPABILITIES.SAVE_DEVICE_LOGS] === false) {
162
164
  void deviceLogs.noteOff(session.getId());
163
165
  }
164
166
  else {
165
- void deviceLogs.start({
166
- sessionId: session.getId(),
167
- device,
168
- since: session.allocatedAt,
169
- driverLog: (_d = (_c = session).deviceLog) === null || _d === void 0 ? void 0 : _d.call(_c),
170
- appUnderTest: (_f = (_e = session).appUnderTest) === null || _f === void 0 ? void 0 : _f.call(_e),
171
- });
167
+ const deviceLogSource = (0, nodeDeviceLogs_1.nodeDeviceLogsSourceOf)(session);
168
+ void deviceLogs.start(Object.assign({ sessionId: session.getId(), device, since: session.allocatedAt, driverLog: (_d = (_c = session).deviceLog) === null || _d === void 0 ? void 0 : _d.call(_c), appUnderTest: (_f = (_e = session).appUnderTest) === null || _f === void 0 ? void 0 : _f.call(_e) }, (deviceLogSource ? { source: deviceLogSource } : {})));
172
169
  }
173
170
  // Emit session started event
174
171
  void typedi_1.Container.get(SocketServer_1.SocketServer).emitToDashboardForDevices(SocketEvents_1.SocketEvents.SESSION_STARTED, Object.assign(Object.assign({}, createData), { status: 'running', build_name: buildName }), { udid: device.udid });