@xenon-device-management/xenon 2.2.0 → 2.4.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 (105) hide show
  1. package/lib/package.json +1 -1
  2. package/lib/public/assets/{AnnotationOverlay-XpewX5nn.js → AnnotationOverlay-CpoWcXKP.js} +1 -1
  3. package/lib/public/assets/{ApiKeyGate-BQDv3ZCC.js → ApiKeyGate-DAYuoJDl.js} +1 -1
  4. package/lib/public/assets/{BugReportButton-C7OmS61B.js → BugReportButton-DEXyENUv.js} +1 -1
  5. package/lib/public/assets/DeviceMosaicView-C-sJRz63.js +2 -0
  6. package/lib/public/assets/{EmptyState-fPxujqqE.js → EmptyState-B_JRYedM.js} +1 -1
  7. package/lib/public/assets/{FieldGroup-5bCutWrs.js → FieldGroup-CorXlSLU.js} +1 -1
  8. package/lib/public/assets/{FilterMenu-owsBiDT1.js → FilterMenu-BKv95DEc.js} +1 -1
  9. package/lib/public/assets/{Menu-DFhqzPSt.js → Menu-Cpy5yMwg.js} +1 -1
  10. package/lib/public/assets/{Modal-Bw-a-E-e.js → Modal-Djp4NvyZ.js} +1 -1
  11. package/lib/public/assets/{RecordingPage-WtByf5bC.js → RecordingPage-CP57OKw9.js} +1 -1
  12. package/lib/public/assets/{RecordingsPage-FpWWFNgT.js → RecordingsPage-BjrTit38.js} +1 -1
  13. package/lib/public/assets/{SegmentedControl-Jzn9c7o8.js → SegmentedControl-MGTzJhBf.js} +1 -1
  14. package/lib/public/assets/{SettingCard-BR7nOhpT.js → SettingCard-CYBPhKPe.js} +1 -1
  15. package/lib/public/assets/{Table-B_Va3V05.js → Table-CdKQ9hnU.js} +1 -1
  16. package/lib/public/assets/{activity-DxRFWvkN.js → activity-QsRK21QF.js} +1 -1
  17. package/lib/public/assets/{ai-settings-CQf-9_V6.js → ai-settings-Dn6DtRn7.js} +1 -1
  18. package/lib/public/assets/{api-keys-SoE1Kd7N.js → api-keys-DYyf17eU.js} +1 -1
  19. package/lib/public/assets/{apps-CgGXTlgn.js → apps-HKFKTe1J.js} +1 -1
  20. package/lib/public/assets/{arrow-left-uQ6WIwiC.js → arrow-left-BkA7oMRe.js} +1 -1
  21. package/lib/public/assets/{arrow-right-C9RyfzXS.js → arrow-right-BsSr58EU.js} +1 -1
  22. package/lib/public/assets/{arrow-up-right-DFHi_8yG.js → arrow-up-right-B7TEV_Gr.js} +1 -1
  23. package/lib/public/assets/{auth-shell-Bii1t4qc.js → auth-shell-BlmNHGcG.js} +2 -2
  24. package/lib/public/assets/{builds-page-Bnv8Z9hi.js → builds-page-E4WIKtl8.js} +1 -1
  25. package/lib/public/assets/{button-_Y78wXiP.js → button-BIwCF2bR.js} +1 -1
  26. package/lib/public/assets/{calendar-D4MqSOc2.js → calendar-CMLnXC7N.js} +1 -1
  27. package/lib/public/assets/{check-uGHCXe-L.js → check-zugO3p5U.js} +1 -1
  28. package/lib/public/assets/{chevron-left-Dy1OqpzB.js → chevron-left-sCqxpTY6.js} +1 -1
  29. package/lib/public/assets/{chevron-right-BUsldEli.js → chevron-right-Bb59xwZc.js} +1 -1
  30. package/lib/public/assets/{circle-check-D3Qq0tKp.js → circle-check-DsvxbnTg.js} +1 -1
  31. package/lib/public/assets/{circle-x-B4IOD1DE.js → circle-x-w69AbmbW.js} +1 -1
  32. package/lib/public/assets/{clock-BfYfzvSg.js → clock-C6XOs4bc.js} +1 -1
  33. package/lib/public/assets/{copy-DZwYE5Va.js → copy-pHk-SWBM.js} +1 -1
  34. package/lib/public/assets/{device-explorer-CRG4Utt5.js → device-explorer-BDq38IcO.js} +52 -52
  35. package/lib/public/assets/{download-CCDYo_TN.js → download-CAHtPZmY.js} +1 -1
  36. package/lib/public/assets/{forgot-password-DuLCmVFb.js → forgot-password-cmvhGOUu.js} +1 -1
  37. package/lib/public/assets/{funnel-CtKNPqzb.js → funnel-BrDuhOtM.js} +1 -1
  38. package/lib/public/assets/{index-3uxl3o7K.js → index-BQCXPOXt.js} +2 -2
  39. package/lib/public/assets/{index-Zy1Op5C1.js → index-C-Q4QkXb.js} +1 -1
  40. package/lib/public/assets/{input-CzTWvUXZ.js → input-DnUVHYax.js} +1 -1
  41. package/lib/public/assets/{keycodes-CI-nYDFM.js → keycodes-CmOptFVa.js} +4 -4
  42. package/lib/public/assets/{layers-YZZGFyTX.js → layers-DRtz_bzF.js} +1 -1
  43. package/lib/public/assets/{lock-L8tzWgDo.js → lock-DF2VnPFm.js} +1 -1
  44. package/lib/public/assets/{login-DBbX-fu8.js → login-DHYffYa4.js} +1 -1
  45. package/lib/public/assets/{maintenance-settings-AJwkOIsE.js → maintenance-settings-r0uPsJ6n.js} +1 -1
  46. package/lib/public/assets/{monitor-Di_66F2m.js → monitor-7nt9oGNx.js} +1 -1
  47. package/lib/public/assets/{mouse-pointer-2-Cnab2zdg.js → mouse-pointer-2-ys1MKVr2.js} +1 -1
  48. package/lib/public/assets/{network-DiSumY-1.js → network-B8m-F79k.js} +1 -1
  49. package/lib/public/assets/{overview-CwE4gXl5.js → overview-BHEntlXz.js} +1 -1
  50. package/lib/public/assets/{page-header-CfR4Sm9V.js → page-header-CuFXVUop.js} +1 -1
  51. package/lib/public/assets/{plus-DOS8P9CQ.js → plus-DUSlMac2.js} +1 -1
  52. package/lib/public/assets/{profile-page-BYpHwQIH.js → profile-page-CBamPy_h.js} +1 -1
  53. package/lib/public/assets/{reset-password-BRVJU_qV.js → reset-password-DG0vZl-o.js} +1 -1
  54. package/lib/public/assets/{runbook-page-Bi3JvAFZ.js → runbook-page-5S3T-I4g.js} +1 -1
  55. package/lib/public/assets/{select-Cqx7pEou.js → select-f0ci1QC7.js} +1 -1
  56. package/lib/public/assets/{selector-detail-page-C0DsncKj.js → selector-detail-page-BXA9LUf9.js} +1 -1
  57. package/lib/public/assets/{selector-health-CAhpdilF.js → selector-health-TyMPgAWo.js} +1 -1
  58. package/lib/public/assets/{selector-health-page-CrCVeW2m.js → selector-health-page-uBlXy33G.js} +1 -1
  59. package/lib/public/assets/{session-detail-page-F_hwYqEt.js → session-detail-page-DECim5lh.js} +1 -1
  60. package/lib/public/assets/{settings-C-s5pypL.js → settings-IfOFbqtv.js} +1 -1
  61. package/lib/public/assets/{status-pill-outline-DY5uzv6A.js → status-pill-outline-grXLf8I8.js} +1 -1
  62. package/lib/public/assets/{tablet-Dli7gwyg.js → tablet-lmFk4qxW.js} +1 -1
  63. package/lib/public/assets/{teams-ui4IhHNC.js → teams-CwMSjVVW.js} +1 -1
  64. package/lib/public/assets/{trash-2-BV4hSmXJ.js → trash-2-BKsgmBS6.js} +1 -1
  65. package/lib/public/assets/{upload-DkGy_J-L.js → upload-DXH47sQS.js} +1 -1
  66. package/lib/public/assets/{use-password-reset-mode-BFtMqQOh.js → use-password-reset-mode-DU98dxpc.js} +1 -1
  67. package/lib/public/assets/{users-Nb_rzRmF.js → users-V5O64rVx.js} +1 -1
  68. package/lib/public/assets/{users-BU0nXIVe.js → users-eXWwQZjX.js} +1 -1
  69. package/lib/public/assets/{video-off-CUNzD2kH.js → video-off-BuoIHEYy.js} +1 -1
  70. package/lib/public/assets/{webhook-settings-Ca3UFXZe.js → webhook-settings-ClsxsKnC.js} +1 -1
  71. package/lib/public/assets/{zap-B2bH2vKX.js → zap-I10vdQXd.js} +1 -1
  72. package/lib/public/index.html +1 -1
  73. package/lib/src/app/routers/control.js +114 -61
  74. package/lib/src/app/routers/nodePhoneControl.js +288 -46
  75. package/lib/src/data-service/device-store.js +7 -3
  76. package/lib/src/data-service/deviceClaims.js +14 -0
  77. package/lib/src/data-service/deviceFieldOwners.js +3 -1
  78. package/lib/src/data-service/prisma-store.js +3 -3
  79. package/lib/src/device-managers/AndroidDeviceManager.js +11 -1
  80. package/lib/src/gateway/forwardToNode.js +10 -3
  81. package/lib/src/generated/client/edge.js +5 -4
  82. package/lib/src/generated/client/index-browser.js +2 -1
  83. package/lib/src/generated/client/index.d.ts +36 -1
  84. package/lib/src/generated/client/index.js +5 -4
  85. package/lib/src/generated/client/package.json +1 -1
  86. package/lib/src/generated/client/schema.prisma +4 -0
  87. package/lib/src/generated/client/wasm.js +2 -1
  88. package/lib/src/services/recording/RecordingOrchestrator.js +39 -5
  89. package/lib/src/services/recording/busy-precheck.js +5 -2
  90. package/lib/src/services/recording/nodeRecordingSource.js +156 -0
  91. package/lib/test/unit/AndroidDeviceManager.spec.js +12 -0
  92. package/lib/test/unit/busy-precheck.spec.js +32 -0
  93. package/lib/test/unit/hub-node-control-forward.spec.js +66 -10
  94. package/lib/test/unit/hub-node-race.spec.js +29 -3
  95. package/lib/test/unit/mark-node-busy-loki.spec.js +17 -4
  96. package/lib/test/unit/node-phone-control.spec.js +124 -4
  97. package/lib/test/unit/node-recording-relay.spec.js +165 -0
  98. package/lib/test/unit/recording-orchestrator.spec.js +86 -2
  99. package/lib/test/unit/screenshot-route.spec.js +104 -0
  100. package/lib/test/unit/upload-install-route.spec.js +205 -0
  101. package/lib/tsconfig.tsbuildinfo +1 -1
  102. package/package.json +1 -1
  103. package/prisma/migrations/20261001120000_device_node_hold/migration.sql +2 -0
  104. package/prisma/schema.prisma +4 -0
  105. package/lib/public/assets/DeviceMosaicView-wl3l2Zlt.js +0 -2
@@ -1 +1 @@
1
- import{t as P,r,X as c,j as e,J as u,A as b,C as U,K as D}from"./index-3uxl3o7K.js";/* empty css */import{F as S}from"./FieldGroup-5bCutWrs.js";import{P as O}from"./page-header-CfR4Sm9V.js";import{T as V}from"./trash-2-BV4hSmXJ.js";import{P as x}from"./plus-DOS8P9CQ.js";import{C as B}from"./circle-x-B4IOD1DE.js";import{C as H}from"./circle-check-D3Qq0tKp.js";import{Z as I}from"./zap-B2bH2vKX.js";import{A as J}from"./activity-DxRFWvkN.js";const C=[{id:"device_offline",label:"Device offline",icon:U,tone:"red"},{id:"device_new",label:"New device",icon:x,tone:"green"},{id:"session_failed",label:"Session failed",icon:B,tone:"amber"},{id:"selector_health_digest",label:"Selector health digest",icon:D,tone:"green"}],X=["udid","host","name","sessionId","failureReason","eventType","platform"],ae=()=>{const{toast:o}=P(),[g,T]=r.useState([]),[A,w]=r.useState(!0),[l,j]=r.useState(""),[d,k]=r.useState(["device_offline","session_failed"]),[h,f]=r.useState(!1),[v,N]=r.useState(!1),[n,_]=r.useState(!1),[y,m]=r.useState("");r.useEffect(()=>{p()},[]);const p=async()=>{w(!0);try{const s=await c.getWebhookConfigs();T(s||[])}catch(s){console.error("Failed to load webhook configs",s),o("Failed to load webhooks.","error")}finally{w(!1)}},z=async()=>{if(!l.trim()){o("Webhook URL is required.","error");return}if(d.length===0){o("Select at least one trigger event.","error");return}f(!0);try{await c.addWebhookConfig(l.trim(),d,"slack",y||void 0),j(""),m(""),_(!1),k(["device_offline","session_failed"]),await p(),o("Webhook saved.","success")}catch(s){console.error("Failed to add webhook",s),o(`Failed to add webhook: ${s.message||"unknown error"}`,"error")}finally{f(!1)}},E=async(s,t)=>{if(window.confirm(`Remove webhook for "${t}"?`))try{await c.deleteWebhookConfig(s),await p(),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(l.trim()){N(!0);try{await c.testWebhook(l.trim(),"slack"),o("Test payload delivered.","success")}catch(s){o(`Test failed: ${s.message||"check the URL"}`,"error")}finally{N(!1)}}},$=s=>{k(t=>t.includes(s)?t.filter(a=>a!==s):[...t,s])},R=s=>{m(t=>t+`{{${s}}} `)};return e.jsxs("div",{className:"settings-container",children:[e.jsx(O,{icon:u,title:"Notifications",subtitle:"Configure Slack or generic webhooks to receive alerts for critical infrastructure events."}),e.jsxs("div",{className:"settings-content",children:[A?e.jsxs("div",{className:"settings-loading",style:{height:200},children:[e.jsx(b,{className:"animate-spin",size:28}),e.jsx("span",{children:"Loading webhooks…"})]}):g.length===0?e.jsxs("div",{className:"empty-state",children:[e.jsx("div",{className:"empty-state__icon",children:e.jsx(u,{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:g.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:s.payloadTemplate?"CUSTOM":"SLACK"}),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:()=>E(s.id,s.url),"aria-label":`Remove webhook for ${s.url}`,children:[e.jsx(V,{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 i=C.find(F=>F.id===a),W=(i==null?void 0:i.icon)??u;return e.jsxs("span",{className:`event-pill event-pill--${(i==null?void 0:i.tone)||"gray"}`,children:[e.jsx(W,{size:11}),(i==null?void 0:i.label)||a]},a)})})]},s.id)})}),e.jsxs("div",{className:"add-webhook-card",children:[e.jsxs("div",{className:"add-webhook-card__header",children:[e.jsx(x,{size:14}),e.jsx("span",{children:"Add a new webhook"})]}),e.jsxs("div",{className:"add-webhook-card__body",children:[e.jsx(S,{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:l,onChange:s=>j(s.target.value)})})}),e.jsx(S,{label:"Trigger events",children:e.jsx("div",{className:"event-toggle-grid",children:C.map(s=>{const t=d.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:()=>$(s.id),children:[e.jsx(a,{size:14,className:"event-toggle__lead"}),e.jsx("span",{children:s.label}),t&&e.jsx(H,{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:()=>_(!n),"aria-expanded":n,children:[e.jsx(I,{size:14,className:n?"tone-amber":"tone-dim"}),e.jsx("span",{children:"Use custom payload (optional)"}),e.jsx("span",{className:"template-section__indicator",children:n?"−":"+"})]}),n&&e.jsxs("div",{className:"template-editor",children:[e.jsx("p",{className:"template-editor__hint",children:"Define a JSON or text template. Click a variable below to insert it."}),e.jsx("div",{className:"template-editor__chips",children:X.map(s=>e.jsx("button",{type:"button",className:"template-editor__chip",onClick:()=>R(s),children:`{{${s}}}`},s))}),e.jsx("textarea",{className:"template-editor__textarea",placeholder:'Example: { "text": "Alert: Device {{udid}} is offline!" }',value:y,onChange:s=>m(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:!l.trim()||v,children:[v?e.jsx(b,{size:14,className:"animate-spin"}):e.jsx(J,{size:14}),e.jsx("span",{children:"Test payload"})]}),e.jsxs("button",{type:"button",className:"page-header-action",onClick:z,disabled:h||!l.trim(),children:[h?e.jsx(b,{size:14,className:"animate-spin"}):e.jsx(x,{size:14}),e.jsx("span",{children:h?"Saving…":"Save webhook"})]})]})]})]})]})};export{ae as WebhookSettings};
1
+ import{t as P,r,X as c,j as e,J as u,A as b,C as U,K as D}from"./index-BQCXPOXt.js";/* empty css */import{F as S}from"./FieldGroup-CorXlSLU.js";import{P as O}from"./page-header-CuFXVUop.js";import{T as V}from"./trash-2-BKsgmBS6.js";import{P as x}from"./plus-DUSlMac2.js";import{C as B}from"./circle-x-w69AbmbW.js";import{C as H}from"./circle-check-DsvxbnTg.js";import{Z as I}from"./zap-I10vdQXd.js";import{A as J}from"./activity-QsRK21QF.js";const C=[{id:"device_offline",label:"Device offline",icon:U,tone:"red"},{id:"device_new",label:"New device",icon:x,tone:"green"},{id:"session_failed",label:"Session failed",icon:B,tone:"amber"},{id:"selector_health_digest",label:"Selector health digest",icon:D,tone:"green"}],X=["udid","host","name","sessionId","failureReason","eventType","platform"],ae=()=>{const{toast:o}=P(),[g,T]=r.useState([]),[A,w]=r.useState(!0),[l,j]=r.useState(""),[d,k]=r.useState(["device_offline","session_failed"]),[h,f]=r.useState(!1),[v,N]=r.useState(!1),[n,_]=r.useState(!1),[y,m]=r.useState("");r.useEffect(()=>{p()},[]);const p=async()=>{w(!0);try{const s=await c.getWebhookConfigs();T(s||[])}catch(s){console.error("Failed to load webhook configs",s),o("Failed to load webhooks.","error")}finally{w(!1)}},z=async()=>{if(!l.trim()){o("Webhook URL is required.","error");return}if(d.length===0){o("Select at least one trigger event.","error");return}f(!0);try{await c.addWebhookConfig(l.trim(),d,"slack",y||void 0),j(""),m(""),_(!1),k(["device_offline","session_failed"]),await p(),o("Webhook saved.","success")}catch(s){console.error("Failed to add webhook",s),o(`Failed to add webhook: ${s.message||"unknown error"}`,"error")}finally{f(!1)}},E=async(s,t)=>{if(window.confirm(`Remove webhook for "${t}"?`))try{await c.deleteWebhookConfig(s),await p(),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(l.trim()){N(!0);try{await c.testWebhook(l.trim(),"slack"),o("Test payload delivered.","success")}catch(s){o(`Test failed: ${s.message||"check the URL"}`,"error")}finally{N(!1)}}},$=s=>{k(t=>t.includes(s)?t.filter(a=>a!==s):[...t,s])},R=s=>{m(t=>t+`{{${s}}} `)};return e.jsxs("div",{className:"settings-container",children:[e.jsx(O,{icon:u,title:"Notifications",subtitle:"Configure Slack or generic webhooks to receive alerts for critical infrastructure events."}),e.jsxs("div",{className:"settings-content",children:[A?e.jsxs("div",{className:"settings-loading",style:{height:200},children:[e.jsx(b,{className:"animate-spin",size:28}),e.jsx("span",{children:"Loading webhooks…"})]}):g.length===0?e.jsxs("div",{className:"empty-state",children:[e.jsx("div",{className:"empty-state__icon",children:e.jsx(u,{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:g.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:s.payloadTemplate?"CUSTOM":"SLACK"}),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:()=>E(s.id,s.url),"aria-label":`Remove webhook for ${s.url}`,children:[e.jsx(V,{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 i=C.find(F=>F.id===a),W=(i==null?void 0:i.icon)??u;return e.jsxs("span",{className:`event-pill event-pill--${(i==null?void 0:i.tone)||"gray"}`,children:[e.jsx(W,{size:11}),(i==null?void 0:i.label)||a]},a)})})]},s.id)})}),e.jsxs("div",{className:"add-webhook-card",children:[e.jsxs("div",{className:"add-webhook-card__header",children:[e.jsx(x,{size:14}),e.jsx("span",{children:"Add a new webhook"})]}),e.jsxs("div",{className:"add-webhook-card__body",children:[e.jsx(S,{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:l,onChange:s=>j(s.target.value)})})}),e.jsx(S,{label:"Trigger events",children:e.jsx("div",{className:"event-toggle-grid",children:C.map(s=>{const t=d.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:()=>$(s.id),children:[e.jsx(a,{size:14,className:"event-toggle__lead"}),e.jsx("span",{children:s.label}),t&&e.jsx(H,{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:()=>_(!n),"aria-expanded":n,children:[e.jsx(I,{size:14,className:n?"tone-amber":"tone-dim"}),e.jsx("span",{children:"Use custom payload (optional)"}),e.jsx("span",{className:"template-section__indicator",children:n?"−":"+"})]}),n&&e.jsxs("div",{className:"template-editor",children:[e.jsx("p",{className:"template-editor__hint",children:"Define a JSON or text template. Click a variable below to insert it."}),e.jsx("div",{className:"template-editor__chips",children:X.map(s=>e.jsx("button",{type:"button",className:"template-editor__chip",onClick:()=>R(s),children:`{{${s}}}`},s))}),e.jsx("textarea",{className:"template-editor__textarea",placeholder:'Example: { "text": "Alert: Device {{udid}} is offline!" }',value:y,onChange:s=>m(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:!l.trim()||v,children:[v?e.jsx(b,{size:14,className:"animate-spin"}):e.jsx(J,{size:14}),e.jsx("span",{children:"Test payload"})]}),e.jsxs("button",{type:"button",className:"page-header-action",onClick:z,disabled:h||!l.trim(),children:[h?e.jsx(b,{size:14,className:"animate-spin"}):e.jsx(x,{size:14}),e.jsx("span",{children:h?"Saving…":"Save webhook"})]})]})]})]})]})};export{ae as WebhookSettings};
@@ -1,4 +1,4 @@
1
- import{c as a}from"./index-3uxl3o7K.js";/**
1
+ import{c as a}from"./index-BQCXPOXt.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-3uxl3o7K.js"></script>
39
+ <script type="module" crossorigin src="/xenon/assets/index-BQCXPOXt.js"></script>
40
40
  <link rel="stylesheet" crossorigin href="/xenon/assets/index-hymrhlEZ.css">
41
41
  </head>
42
42
 
@@ -45,7 +45,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
45
45
  return (mod && mod.__esModule) ? mod : { "default": mod };
46
46
  };
47
47
  Object.defineProperty(exports, "__esModule", { value: true });
48
- exports.previewLeaves = exports.previewLeaveDeps = void 0;
48
+ exports.previewLeaves = exports.previewLeaveDeps = exports.UPLOAD_INSTALL_MAX_BYTES = void 0;
49
+ exports.uploadName = uploadName;
49
50
  const plugin_1 = require("../../plugin");
50
51
  const express_1 = require("express");
51
52
  const device_store_1 = require("../../data-service/device-store");
@@ -61,6 +62,7 @@ const IOSStreamService_1 = __importDefault(require("../../device-managers/ios/IO
61
62
  const AndroidStreamService_1 = __importDefault(require("../../device-managers/android/AndroidStreamService"));
62
63
  const AndroidH264StreamService_1 = __importDefault(require("../../device-managers/android/AndroidH264StreamService"));
63
64
  const path_1 = __importDefault(require("path"));
65
+ const express_fileupload_1 = __importDefault(require("express-fileupload"));
64
66
  const os_1 = __importDefault(require("os"));
65
67
  const fs_extra_1 = __importDefault(require("fs-extra"));
66
68
  const OmniVisionService_1 = require("../../services/omni-vision/OmniVisionService");
@@ -233,6 +235,7 @@ router.post('/:udid/keyevent', (req, res) => __awaiter(void 0, void 0, void 0, f
233
235
  res.status(400).send('Manager not found or pressKey not supported');
234
236
  }));
235
237
  router.get('/:udid/screenshot', (req, res) => __awaiter(void 0, void 0, void 0, function* () {
238
+ var _a, _b;
236
239
  const { udid } = req.params;
237
240
  const device = yield getDeviceInfo(udid);
238
241
  if (!device)
@@ -248,7 +251,16 @@ router.get('/:udid/screenshot', (req, res) => __awaiter(void 0, void 0, void 0,
248
251
  }
249
252
  const manager = yield getDeviceManagerForPlatform(device.platform);
250
253
  if (manager && manager.getScreenshot) {
251
- const base64 = yield manager.getScreenshot(udid);
254
+ let base64;
255
+ try {
256
+ base64 = yield manager.getScreenshot(udid);
257
+ }
258
+ catch (err) {
259
+ // Answered: Express 4 doesn't catch a rejected async handler, and an
260
+ // iPhone with no live preview (no WDA, no tunnel) left this unanswered.
261
+ logger_1.default.error(`Screenshot capture failed for ${udid}: ${(_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : err}`);
262
+ return res.status(502).send({ error: `Screenshot capture failed: ${(_b = err === null || err === void 0 ? void 0 : err.message) !== null && _b !== void 0 ? _b : err}` });
263
+ }
252
264
  // CRITICAL: Validate screenshot is not empty before returning success
253
265
  if (base64 && base64.length > 100) {
254
266
  return res.status(200).send({ screenshot: base64 });
@@ -415,11 +427,16 @@ router.post('/:udid/install-repository-app', (req, res) => __awaiter(void 0, voi
415
427
  if (!(0, appVisibility_1.canSeeApp)(app, (_a = req.auth) === null || _a === void 0 ? void 0 : _a.teamIds)) {
416
428
  return res.status(404).send('App not found in repository');
417
429
  }
430
+ // A node's phone: the app is in this server's library, not the node's,
431
+ // so the file goes to the node's own upload-install.
432
+ if ((0, nodePhoneControl_1.isOtherServersPhone)(device)) {
433
+ return yield (0, nodePhoneControl_1.installFileOnNode)(req, res, udid, device, {
434
+ path: app.filepath,
435
+ name: app.filename || path_1.default.basename(app.filepath),
436
+ });
437
+ }
418
438
  const manager = yield getDeviceManagerForPlatform(device.platform);
419
439
  if (manager && manager.installApp) {
420
- // If it's a local file, pass the path.
421
- // In a distributed setup, the node would ideally download it.
422
- // For now, we assume hub-node shared storage or hub-local execution.
423
440
  yield manager.installApp(udid, app.filepath);
424
441
  return res.status(200).send({ success: true, message: `Installed ${app.name}` });
425
442
  }
@@ -430,38 +447,71 @@ router.post('/:udid/install-repository-app', (req, res) => __awaiter(void 0, voi
430
447
  res.status(500).send({ error: err.message });
431
448
  }
432
449
  }));
433
- router.post('/:udid/upload-install', (req, res) => __awaiter(void 0, void 0, void 0, function* () {
450
+ /** Where upload-install writes an uploaded app while it is installed. */
451
+ const UPLOAD_DIR = path_1.default.join(os_1.default.tmpdir(), 'xenon-uploads');
452
+ /** The largest app upload-install takes: room for a large iOS app. */
453
+ exports.UPLOAD_INSTALL_MAX_BYTES = 4 * 1024 ** 3;
454
+ /**
455
+ * upload-install's own multipart parser. /control had none (only /apps
456
+ * mounts one), so every upload was answered "No files were uploaded." The
457
+ * file goes to a temporary file rather than memory, and one past the limit
458
+ * is answered 413.
459
+ */
460
+ const uploadParser = (0, express_fileupload_1.default)({
461
+ useTempFiles: true,
462
+ tempFileDir: UPLOAD_DIR,
463
+ limits: { fileSize: exports.UPLOAD_INSTALL_MAX_BYTES },
464
+ abortOnLimit: true,
465
+ });
466
+ /**
467
+ * The name an upload is saved under: the client's name, cut to its last
468
+ * path segment and to safe characters, keeping its extension, which the
469
+ * installers read. busboy already cuts the name to its last segment (its
470
+ * `preservePath` is off); this holds if a parser ever keeps paths.
471
+ */
472
+ function uploadName(clientName) {
473
+ const base = path_1.default
474
+ .basename(String(clientName !== null && clientName !== void 0 ? clientName : ''))
475
+ .replace(/[^\w.-]+/g, '_')
476
+ .replace(/^\.+/, '');
477
+ return `${Date.now()}-${base || 'app'}`;
478
+ }
479
+ router.post('/:udid/upload-install', uploadParser, (req, res) => __awaiter(void 0, void 0, void 0, function* () {
480
+ var _a;
434
481
  const { udid } = req.params;
435
- const device = yield getDeviceInfo(udid);
436
- if (!device)
437
- return res.status(404).send('Device not found');
438
- if (!req.files || Object.keys(req.files).length === 0) {
439
- return res.status(400).send('No files were uploaded.');
440
- }
441
- const appFile = req.files.app;
442
- if (!appFile)
443
- return res.status(400).send('File "app" is required');
444
- const tmpDir = path_1.default.join(os_1.default.tmpdir(), 'xenon-uploads');
445
- if (!fs_extra_1.default.existsSync(tmpDir))
446
- fs_extra_1.default.mkdirSync(tmpDir, { recursive: true });
447
- const appPath = path_1.default.join(tmpDir, `${Date.now()}-${appFile.name}`);
448
- try {
482
+ const appFile = (_a = req.files) === null || _a === void 0 ? void 0 : _a.app;
483
+ const appPath = appFile ? path_1.default.join(UPLOAD_DIR, uploadName(appFile.name)) : undefined;
484
+ const answer = yield (() => __awaiter(void 0, void 0, void 0, function* () {
485
+ const device = yield getDeviceInfo(udid);
486
+ if (!device)
487
+ return { status: 404, body: 'Device not found' };
488
+ if (!req.files || Object.keys(req.files).length === 0) {
489
+ return { status: 400, body: 'No files were uploaded.' };
490
+ }
491
+ if (!appFile || !appPath)
492
+ return { status: 400, body: 'File "app" is required' };
449
493
  yield appFile.mv(appPath);
450
494
  const manager = yield getDeviceManagerForPlatform(device.platform);
451
- if (manager && manager.installApp) {
452
- yield manager.installApp(udid, appPath);
453
- // Clean up after installation
454
- setTimeout(() => fs_extra_1.default.remove(appPath).catch(() => { }), 10000);
455
- return res
456
- .status(200)
457
- .send({ success: true, message: `App ${appFile.name} installed successfully` });
495
+ if (!(manager === null || manager === void 0 ? void 0 : manager.installApp)) {
496
+ return { status: 400, body: 'Manager not found or installApp not supported' };
458
497
  }
459
- res.status(400).send('Manager not found or installApp not supported');
460
- }
461
- catch (err) {
498
+ yield manager.installApp(udid, appPath);
499
+ return {
500
+ status: 200,
501
+ body: { success: true, message: `App ${appFile.name} installed successfully` },
502
+ };
503
+ }))().catch((err) => {
462
504
  logger_1.default.error(`Installation failed for ${udid}: ${err.message}`);
463
- res.status(500).send({ error: err.message });
505
+ return { status: 500, body: { error: err.message } };
506
+ });
507
+ // The install has finished with the file, whether it worked or not (both
508
+ // installers wait for their process). Removed before answering, so nothing
509
+ // is left behind once the caller has the answer.
510
+ for (const file of [appPath, appFile === null || appFile === void 0 ? void 0 : appFile.tempFilePath]) {
511
+ if (file)
512
+ yield fs_extra_1.default.remove(file).catch(() => undefined);
464
513
  }
514
+ res.status(answer.status).send(answer.body);
465
515
  }));
466
516
  router.post('/:udid/uninstall', (req, res) => __awaiter(void 0, void 0, void 0, function* () {
467
517
  const { udid } = req.params;
@@ -1032,6 +1082,33 @@ router.post('/:udid/shell', (req, res) => __awaiter(void 0, void 0, void 0, func
1032
1082
  }
1033
1083
  res.status(400).send('Manager not found or executeShell not supported');
1034
1084
  }));
1085
+ /**
1086
+ * Where Omni-Vision reads the screen from, for the stand-in driver it is
1087
+ * given: this server's device manager, or, for a node's phone, the node
1088
+ * (screenshotFromNode). Omni itself runs here either way, with this
1089
+ * server's AI settings. Null when this server has no manager for the phone.
1090
+ */
1091
+ function omniScreen(req, udid, device) {
1092
+ return __awaiter(this, void 0, void 0, function* () {
1093
+ if ((0, nodePhoneControl_1.isOtherServersPhone)(device)) {
1094
+ return {
1095
+ getScreenshot: () => (0, nodePhoneControl_1.screenshotFromNode)(req, udid, device),
1096
+ getPageSource: () => __awaiter(this, void 0, void 0, function* () { return ''; }),
1097
+ };
1098
+ }
1099
+ const manager = yield getDeviceManagerForPlatform(device.platform);
1100
+ if (!manager)
1101
+ return null;
1102
+ return {
1103
+ getScreenshot: () => __awaiter(this, void 0, void 0, function* () {
1104
+ if (manager.getScreenshot)
1105
+ return yield manager.getScreenshot(udid);
1106
+ throw new Error('Screenshot not supported for this device');
1107
+ }),
1108
+ getPageSource: () => __awaiter(this, void 0, void 0, function* () { return (manager.getPageSource ? yield manager.getPageSource(udid) : ''); }),
1109
+ };
1110
+ });
1111
+ }
1035
1112
  /**
1036
1113
  * Omni-Scan for manual control (No Appium Session)
1037
1114
  */
@@ -1040,28 +1117,12 @@ router.get('/:udid/omni-scan', (req, res) => __awaiter(void 0, void 0, void 0, f
1040
1117
  const device = yield getDeviceInfo(udid);
1041
1118
  if (!device)
1042
1119
  return res.status(404).send('Device not found');
1043
- const manager = yield getDeviceManagerForPlatform(device.platform);
1044
- if (!manager)
1120
+ const screen = yield omniScreen(req, udid, device);
1121
+ if (!screen)
1045
1122
  return res.status(400).send('Manager not found');
1046
1123
  try {
1047
1124
  const omniService = typedi_1.Container.get(OmniVisionService_1.OmniVisionService);
1048
- // Create a Mock Driver that OmniVisionService can use
1049
- const mockDriver = {
1050
- sessionId: `manual_${udid}`,
1051
- getScreenshot: () => __awaiter(void 0, void 0, void 0, function* () {
1052
- if (manager.getScreenshot) {
1053
- return yield manager.getScreenshot(udid);
1054
- }
1055
- throw new Error('Screenshot not supported for this device');
1056
- }),
1057
- // OmniVision might need page source for some analysis later
1058
- getPageSource: () => __awaiter(void 0, void 0, void 0, function* () {
1059
- if (manager.getPageSource) {
1060
- return yield manager.getPageSource(udid);
1061
- }
1062
- return '';
1063
- }),
1064
- };
1125
+ const mockDriver = Object.assign({ sessionId: `manual_${udid}` }, screen);
1065
1126
  const result = yield omniService.analyzeScreen(mockDriver);
1066
1127
  return res.status(200).send({ status: 'success', value: result });
1067
1128
  }
@@ -1140,20 +1201,12 @@ router.post('/:udid/test-locator', (req, res) => __awaiter(void 0, void 0, void
1140
1201
  const device = yield getDeviceInfo(udid);
1141
1202
  if (!device)
1142
1203
  return res.status(404).send('Device not found');
1143
- const manager = yield getDeviceManagerForPlatform(device.platform);
1144
- if (!manager)
1204
+ const screen = yield omniScreen(req, udid, device);
1205
+ if (!screen)
1145
1206
  return res.status(400).send('Manager not found');
1146
1207
  try {
1147
1208
  const omniService = typedi_1.Container.get(OmniVisionService_1.OmniVisionService);
1148
- const mockDriver = {
1149
- sessionId: `manual_${udid}`,
1150
- getScreenshot: () => __awaiter(void 0, void 0, void 0, function* () {
1151
- if (manager.getScreenshot) {
1152
- return yield manager.getScreenshot(udid);
1153
- }
1154
- throw new Error('Screenshot not supported for this device');
1155
- }),
1156
- };
1209
+ const mockDriver = { sessionId: `manual_${udid}`, getScreenshot: screen.getScreenshot };
1157
1210
  let value = [];
1158
1211
  if (strategy === '-custom:ai-text') {
1159
1212
  value = yield omniService.findByText(mockDriver, selector);