@xenon-device-management/xenon 2.13.1 → 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.
- package/README.md +11 -5
- package/lib/package.json +1 -1
- package/lib/public/assets/{AnnotationOverlay-Cvpkk4r9.js → AnnotationOverlay-B_VNtKBH.js} +1 -1
- package/lib/public/assets/{ApiKeyGate-Bhn5ZtVM.js → ApiKeyGate-BwuaMjkX.js} +1 -1
- package/lib/public/assets/{BugReportButton-BA6aEjTC.js → BugReportButton-BAt0_pzq.js} +1 -1
- package/lib/public/assets/DeviceMosaicView-BYaQHhTf.js +2 -0
- package/lib/public/assets/{EmptyState-CgzbuUvO.js → EmptyState-BSjhk_IV.js} +1 -1
- package/lib/public/assets/{FieldGroup-BHc_5qNc.js → FieldGroup-Cb5QZsE1.js} +1 -1
- package/lib/public/assets/{FilterMenu-D7OMGuRc.js → FilterMenu-BAaSlWbW.js} +1 -1
- package/lib/public/assets/{Menu-C173JJ0l.js → Menu-JGGYrSQQ.js} +1 -1
- package/lib/public/assets/{Modal-ChvVUI3B.js → Modal-CFCp3n9d.js} +1 -1
- package/lib/public/assets/{RecordingPage-Dhw4d_O6.js → RecordingPage-CbJj_EBT.js} +1 -1
- package/lib/public/assets/{RecordingsPage-z97ftGK-.js → RecordingsPage-BJeYnygR.js} +2 -2
- package/lib/public/assets/{SegmentedControl-DbXhBGZC.js → SegmentedControl-Bg42Atjy.js} +1 -1
- package/lib/public/assets/{SettingCard-C908GgiI.js → SettingCard-Njz1pUIJ.js} +1 -1
- package/lib/public/assets/{Table-B1DB_ZN-.js → Table-BcTm5xV2.js} +1 -1
- package/lib/public/assets/{activity-Css0xNvp.js → activity-DOVdsfMQ.js} +1 -1
- package/lib/public/assets/{ai-settings-BMQK3-yN.js → ai-settings-CsuJroRf.js} +2 -2
- package/lib/public/assets/{api-keys-CbJ42eDQ.js → api-keys-BKnNSTw9.js} +1 -1
- package/lib/public/assets/{apps-DW1pw2re.js → apps-D0SC6uw6.js} +2 -2
- package/lib/public/assets/{arrow-left-Dpog9L0F.js → arrow-left-MMbTmgoE.js} +1 -1
- package/lib/public/assets/{arrow-right-CsBIlSIw.js → arrow-right-BMfnGmxW.js} +1 -1
- package/lib/public/assets/{arrow-up-right-z1tRQE4R.js → arrow-up-right-DuAftMWB.js} +1 -1
- package/lib/public/assets/{auth-shell-mXiX5vNk.js → auth-shell-nZC2JNDO.js} +2 -2
- package/lib/public/assets/{builds-page-BcFn23bE.js → builds-page-hDCgTwkS.js} +1 -1
- package/lib/public/assets/{button-Cq_zHETv.js → button-Dda4irn_.js} +1 -1
- package/lib/public/assets/{calendar-CKxABX9q.js → calendar-BMr0Q62v.js} +1 -1
- package/lib/public/assets/{check-C7xu-hGU.js → check-CZ9iw9-e.js} +1 -1
- package/lib/public/assets/{chevron-right-BiqfdCGA.js → chevron-right-BRYJJVC9.js} +1 -1
- package/lib/public/assets/{circle-check-CyBjjPxR.js → circle-check-CqAJjQ5O.js} +1 -1
- package/lib/public/assets/{circle-x-6gMRcx6z.js → circle-x-DlDJtnVz.js} +1 -1
- package/lib/public/assets/{clock-U1rDTT1h.js → clock-CpuDKysU.js} +1 -1
- package/lib/public/assets/{copy-BIM0ZX0L.js → copy-CAQydNFe.js} +1 -1
- package/lib/public/assets/device-explorer-CdO6_nrn.js +282 -0
- package/lib/public/assets/{download-DDBRXnAZ.js → download-C9XG6lLv.js} +1 -1
- package/lib/public/assets/{forgot-password-Dpg7yxM8.js → forgot-password-CGnkRgE9.js} +1 -1
- package/lib/public/assets/h264Stream-VQBphYhg.js +26 -0
- package/lib/public/assets/{index-BGBqohfQ.js → index-BbKEXUFH.js} +5 -5
- package/lib/public/assets/{index-BTYMAqNW.css → index-Bxoxt9sV.css} +1 -1
- package/lib/public/assets/{index-BQ2luT7L.js → index-DJGc1M2f.js} +1 -1
- package/lib/public/assets/{input-BvB1cjHE.js → input-Cm0tmSoc.js} +1 -1
- package/lib/public/assets/{line-chart-m78VggnW.js → line-chart-D2DyJCml.js} +1 -1
- package/lib/public/assets/{list-checks-cY7o1QTL.js → list-checks-UWf1GY99.js} +1 -1
- package/lib/public/assets/{lock-loz8txQm.js → lock-CCJv9m_I.js} +1 -1
- package/lib/public/assets/{login-BYOvz4lx.js → login-f8vOVW-U.js} +1 -1
- package/lib/public/assets/maintenance-settings-C1PrkuAH.js +6 -0
- package/lib/public/assets/{monitor-C4HPHb6s.js → monitor-B2jDgwm7.js} +1 -1
- package/lib/public/assets/{mouse-pointer-2-BkdKtCzi.js → mouse-pointer-2-DLI1RtzU.js} +1 -1
- package/lib/public/assets/{network-C5e0MIWo.js → network-ebT86qoi.js} +1 -1
- package/lib/public/assets/{overview-tlu8ijGd.js → overview-C0aykVrd.js} +1 -1
- package/lib/public/assets/{page-header-FXPlo6az.js → page-header-BjzgWz-T.js} +1 -1
- package/lib/public/assets/{play-CWdk0_Sy.js → play-DLG9t8FU.js} +1 -1
- package/lib/public/assets/{plus-B2xCnntV.js → plus-nBFSMu_X.js} +1 -1
- package/lib/public/assets/{profile-page-D080qzr4.js → profile-page-CSl0UYlv.js} +1 -1
- package/lib/public/assets/{reset-password-D3u4Mbvn.js → reset-password-LeOuHqmB.js} +1 -1
- package/lib/public/assets/{runbook-page-hPRZMy7s.js → runbook-page-CMMeQml9.js} +1 -1
- package/lib/public/assets/{select-12RThAXc.js → select-88jHcl-Z.js} +1 -1
- package/lib/public/assets/{selector-detail-redirect-uQwvV7EQ.js → selector-detail-redirect-BbXKJjZM.js} +1 -1
- package/lib/public/assets/{selector-health-page-CThoxDFO.js → selector-health-page-B4rA1Xkv.js} +2 -2
- package/lib/public/assets/session-detail-page-COL_RZxw.js +4 -0
- package/lib/public/assets/settings-GF6ZMSQ6.js +6 -0
- package/lib/public/assets/{stat-tile-B02WEte8.js → stat-tile-BV98e_Sv.js} +1 -1
- package/lib/public/assets/{tablet-Dj30VGZl.js → tablet-BiAsnFDj.js} +1 -1
- package/lib/public/assets/{teams-uL4fjTDR.js → teams-B4-jrr-1.js} +1 -1
- package/lib/public/assets/{trash-2-B54e-TfY.js → trash-2-C5RSuX0e.js} +1 -1
- package/lib/public/assets/{upload-B3k8WCSP.js → upload-BG7_rMXK.js} +1 -1
- package/lib/public/assets/{use-builds-data-BFn90NH3.js → use-builds-data-J-riQYyC.js} +1 -1
- package/lib/public/assets/{use-password-reset-mode-DL2TmWZ0.js → use-password-reset-mode-C8-eH-UG.js} +1 -1
- package/lib/public/assets/{users-CqBuxMlX.js → users-CFba5TSy.js} +1 -1
- package/lib/public/assets/{users-DjQSucKb.js → users-Cp0beJm3.js} +1 -1
- package/lib/public/assets/{video-off-T0rlt0GA.js → video-off-DDsEISjV.js} +1 -1
- package/lib/public/assets/webhook-settings-BGu32vSB.js +1 -0
- package/lib/public/assets/{zap-gBHqutZV.js → zap-l4VaXzq8.js} +1 -1
- package/lib/public/index.html +2 -2
- package/lib/schema.json +19 -46
- package/lib/src/XenonCapabilityManager.js +22 -5
- package/lib/src/app/openapi/control.yaml +71 -20
- package/lib/src/app/openapi/grid.yaml +23 -5
- package/lib/src/app/openapi/platform.yaml +136 -28
- package/lib/src/app/routers/control.js +30 -5
- package/lib/src/app/routers/dashboard.js +29 -1
- package/lib/src/app/routers/grid.js +32 -10
- package/lib/src/app/routers/streamStartConflict.js +3 -0
- package/lib/src/app/routers/streamType.js +7 -2
- package/lib/src/app/routers/webhook.js +8 -4
- package/lib/src/config.js +50 -5
- package/lib/src/dashboard/commands.js +185 -141
- package/lib/src/dashboard/event-manager.js +39 -6
- package/lib/src/data-service/appleFamily.js +43 -0
- package/lib/src/data-service/db.js +11 -0
- package/lib/src/data-service/device-service.js +46 -1
- package/lib/src/data-service/device-store.js +66 -3
- package/lib/src/data-service/deviceClaims.js +37 -0
- package/lib/src/data-service/deviceFieldOwners.js +25 -11
- package/lib/src/data-service/deviceSettings.js +79 -0
- package/lib/src/data-service/prisma-store.js +152 -9
- package/lib/src/data-service/web-config-service.js +43 -92
- package/lib/src/device-managers/AndroidDeviceManager.js +9 -31
- package/lib/src/device-managers/ios/WDAClient.js +44 -43
- package/lib/src/device-managers/shellCommands.js +96 -0
- package/lib/src/device-utils.js +143 -37
- package/lib/src/generated/client/edge.js +17 -3
- package/lib/src/generated/client/index-browser.js +14 -0
- package/lib/src/generated/client/index.d.ts +2054 -379
- package/lib/src/generated/client/index.js +17 -3
- package/lib/src/generated/client/package.json +1 -1
- package/lib/src/generated/client/schema.prisma +35 -7
- package/lib/src/generated/client/wasm.js +14 -0
- package/lib/src/interceptors/CommandInterceptor.js +29 -8
- package/lib/src/interceptors/xenonScripts.js +55 -0
- package/lib/src/interfaces/IPluginArgs.js +1 -2
- package/lib/src/middleware/deviceAccessGuard.js +63 -9
- package/lib/src/plugin.js +15 -1
- package/lib/src/prisma.js +1 -1
- package/lib/src/scripts/database-check.js +124 -0
- package/lib/src/services/AIService.js +99 -0
- package/lib/src/services/CleanupService.js +6 -1
- package/lib/src/services/InterceptorService.js +70 -13
- package/lib/src/services/NetworkConditioningService.js +85 -88
- package/lib/src/services/NotificationService.js +36 -29
- package/lib/src/services/OrphanSweeper.js +5 -0
- package/lib/src/services/PortAllocator.js +5 -0
- package/lib/src/services/ServerManager.js +97 -8
- package/lib/src/services/SessionLifecycleService.js +63 -53
- package/lib/src/services/ShutdownCoordinator.js +33 -0
- package/lib/src/services/TeamService.js +20 -3
- package/lib/src/services/device-access/SessionOwnerResolver.js +26 -0
- package/lib/src/services/device-access/deviceAccessPolicy.js +38 -6
- package/lib/src/services/device-access/leaseHold.js +26 -0
- package/lib/src/services/device-access/ticketActorAccess.js +6 -0
- package/lib/src/services/healing/HealingOrchestrator.js +32 -1
- package/lib/src/services/interceptor/AndroidProxyAdapter.js +18 -0
- package/lib/src/services/interceptor/interceptorOptions.js +44 -0
- package/lib/src/services/lease/activeLeases.js +37 -0
- package/lib/src/services/network/AndroidRadios.js +71 -0
- package/lib/src/services/network/PhoneNetworkLedger.js +184 -0
- package/lib/src/services/network/PhoneNetworkRestore.js +330 -0
- package/lib/src/services/network/adbForPhone.js +60 -0
- package/lib/src/services/network/phoneNetworkLock.js +20 -0
- package/lib/src/services/network/xenonProxy.js +107 -0
- package/lib/src/services/omni-vision/OmniVisionService.js +53 -43
- package/lib/src/services/omni-vision/VisionAssertionService.js +28 -25
- package/lib/src/services/omni-vision/ocrTextMatch.js +105 -0
- package/lib/src/services/omni-vision/screenScale.js +68 -0
- package/lib/src/services/recording/busy-precheck.js +24 -3
- package/lib/src/services/recording/concurrency-gate.js +12 -3
- package/lib/src/services/settings/SelfHealingSwitch.js +70 -0
- package/lib/src/services/settings/labSettings.js +149 -0
- package/lib/src/services/webhookEvents.js +173 -0
- package/lib/test/helpers/fake-adb.js +58 -0
- package/lib/test/helpers/fake-device-table.js +63 -24
- package/lib/test/helpers/scratch-database.js +1 -0
- package/lib/test/unit/TeamService.test.js +3 -0
- package/lib/test/unit/XenonCapabilityManager.spec.js +4 -2
- package/lib/test/unit/apple-family.spec.js +36 -0
- package/lib/test/unit/cleanup-settings-db.spec.js +103 -0
- package/lib/test/unit/cleanup-settings.spec.js +244 -0
- package/lib/test/unit/dashboard-api-paths.spec.js +101 -0
- package/lib/test/unit/dashboard-commands.spec.js +249 -0
- package/lib/test/unit/dashboard-events-team-scope.spec.js +9 -6
- package/lib/test/unit/database-check.spec.js +108 -0
- package/lib/test/unit/default-plugin-args.spec.js +33 -0
- package/lib/test/unit/device-access-lease.spec.js +270 -0
- package/lib/test/unit/device-offline-webhook.spec.js +81 -0
- package/lib/test/unit/device-setting-columns.spec.js +66 -0
- package/lib/test/unit/device-settings-loki.spec.js +87 -0
- package/lib/test/unit/device-settings-survive.spec.js +555 -0
- package/lib/test/unit/device-sync-lost-update.spec.js +5 -0
- package/lib/test/unit/emulator-boot.spec.js +91 -0
- package/lib/test/unit/hub-node-gateway-appium-server.spec.js +48 -0
- package/lib/test/unit/hub-node-same-host.spec.js +5 -0
- package/lib/test/unit/iphone-ipad-only.spec.js +132 -0
- package/lib/test/unit/json-logging-option.spec.js +74 -0
- package/lib/test/unit/lab-settings.spec.js +218 -0
- package/lib/test/unit/lease/allocate-lease-branch.spec.js +22 -0
- package/lib/test/unit/lease/session-lease-proof.spec.js +59 -0
- package/lib/test/unit/lease-held-phone.spec.js +196 -0
- package/lib/test/unit/max-sessions.spec.js +240 -0
- package/lib/test/unit/ocr-text-match.spec.js +44 -0
- package/lib/test/unit/omni-vision-analyze-screen.spec.js +72 -0
- package/lib/test/unit/omni-vision-ios-points.spec.js +161 -0
- package/lib/test/unit/omni-vision-tap-text.spec.js +129 -0
- package/lib/test/unit/omni-vision.spec.js +1 -1
- package/lib/test/unit/partial-config-file.spec.js +114 -0
- package/lib/test/unit/phone-network/boot-clean-up.spec.js +64 -0
- package/lib/test/unit/phone-network/interceptor-options.spec.js +77 -0
- package/lib/test/unit/phone-network/interceptor-proxy-restore.spec.js +145 -0
- package/lib/test/unit/phone-network/network-conditioning.spec.js +145 -0
- package/lib/test/unit/phone-network/phone-network-restore.spec.js +246 -0
- package/lib/test/unit/phone-network/session-end-paths.spec.js +182 -0
- package/lib/test/unit/phone-network/session-start.spec.js +110 -0
- package/lib/test/unit/phone-network/xenon-proxy.spec.js +69 -0
- package/lib/test/unit/plugin.spec.js +3 -3
- package/lib/test/unit/recording-options.spec.js +181 -0
- package/lib/test/unit/schema-required-args.spec.js +21 -16
- package/lib/test/unit/self-healing-boot.spec.js +75 -0
- package/lib/test/unit/self-healing-setting.spec.js +272 -0
- package/lib/test/unit/session-failed-webhook.spec.js +170 -0
- package/lib/test/unit/shell-commands.spec.js +133 -0
- package/lib/test/unit/socketServer.scopedEmit.spec.js +2 -0
- package/lib/test/unit/stream-start-pipelines.spec.js +210 -0
- package/lib/test/unit/stream-status-type.spec.js +4 -0
- package/lib/test/unit/vision-assertion.spec.js +118 -0
- package/lib/test/unit/wda-client-shell.spec.js +133 -0
- package/lib/test/unit/web-config-service.spec.js +79 -0
- package/lib/test/unit/webhook-delivery.spec.js +24 -0
- package/lib/test/unit/webhook-payloads.spec.js +273 -0
- package/lib/test/unit/xenon-execute-scripts.spec.js +138 -0
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/lib/web/src/components/webhook-settings/webhookEventVariables.json +30 -0
- package/package.json +1 -1
- package/prisma/migrations/20261004140000_keep_device_settings/migration.sql +19 -0
- package/prisma/schema.prisma +35 -7
- package/schema.json +19 -46
- package/scripts/generate-types-from-schema.js +7 -2
- package/scripts/lib/always-present-plugin-args.js +42 -0
- package/lib/public/assets/DeviceMosaicView-ChhHnpFQ.js +0 -2
- package/lib/public/assets/device-explorer-DSpAA_t9.js +0 -279
- package/lib/public/assets/maintenance-settings-BxbA7SkB.js +0 -6
- package/lib/public/assets/recording-group-store-B75435xR.js +0 -26
- package/lib/public/assets/session-detail-page-Bc02YjMm.js +0 -4
- package/lib/public/assets/settings-C1VIluuT.js +0 -6
- package/lib/public/assets/webhook-settings-DAHgnJyZ.js +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
import{o as s}from"./index-
|
|
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,
|
|
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.
|
|
@@ -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};
|
package/lib/public/index.html
CHANGED
|
@@ -36,8 +36,8 @@
|
|
|
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-
|
|
40
|
-
<link rel="stylesheet" crossorigin href="/xenon/assets/index-
|
|
39
|
+
<script type="module" crossorigin src="/xenon/assets/index-BbKEXUFH.js"></script>
|
|
40
|
+
<link rel="stylesheet" crossorigin href="/xenon/assets/index-Bxoxt9sV.css">
|
|
41
41
|
</head>
|
|
42
42
|
|
|
43
43
|
<body>
|
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
|
|
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": "
|
|
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": "
|
|
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.
|
|
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": "
|
|
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
|
|
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
|
-
"
|
|
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
|
|
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
|
-
"
|
|
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": "
|
|
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",
|
|
@@ -288,31 +286,6 @@
|
|
|
288
286
|
"description": "Record CPU and memory every 2 s for each session on this server's own phones (Android: the app under test and the device; iPhone: device CPU), shown in the session page's Performance panel. Needs the dashboard."
|
|
289
287
|
}
|
|
290
288
|
},
|
|
291
|
-
"required": [
|
|
292
|
-
"platform",
|
|
293
|
-
"androidDeviceType",
|
|
294
|
-
"iosDeviceType",
|
|
295
|
-
"skipChromeDownload",
|
|
296
|
-
"maxSessions",
|
|
297
|
-
"deviceAvailabilityTimeoutMs",
|
|
298
|
-
"deviceAvailabilityQueryIntervalMs",
|
|
299
|
-
"sendNodeDevicesToHubIntervalMs",
|
|
300
|
-
"checkStaleDevicesIntervalMs",
|
|
301
|
-
"checkBlockedDevicesIntervalMs",
|
|
302
|
-
"newCommandTimeoutSec",
|
|
303
|
-
"bindHostOrIp",
|
|
304
|
-
"enableDashboard",
|
|
305
|
-
"bootedSimulators",
|
|
306
|
-
"removeDevicesFromDatabaseBeforeRunningThePlugin",
|
|
307
|
-
"healthCheckIntervalMs",
|
|
308
|
-
"enableSelfHealing",
|
|
309
|
-
"buildCleanupDays",
|
|
310
|
-
"buildCleanupMaxCount",
|
|
311
|
-
"buildCleanupSchedule",
|
|
312
|
-
"deleteBuildAssets",
|
|
313
|
-
"sessionHeartbeatIntervalMs",
|
|
314
|
-
"enableJsonLogging"
|
|
315
|
-
],
|
|
316
289
|
"title": "IPluginArgs",
|
|
317
290
|
"type": "object",
|
|
318
291
|
"definitions": {
|
|
@@ -474,17 +447,17 @@
|
|
|
474
447
|
"enabled": {
|
|
475
448
|
"type": "boolean",
|
|
476
449
|
"default": false,
|
|
477
|
-
"description": "
|
|
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."
|
|
478
451
|
},
|
|
479
452
|
"bufferSize": {
|
|
480
453
|
"type": "number",
|
|
481
454
|
"default": 1000,
|
|
482
|
-
"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."
|
|
483
456
|
},
|
|
484
457
|
"captureBodies": {
|
|
485
458
|
"type": "boolean",
|
|
486
459
|
"default": true,
|
|
487
|
-
"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."
|
|
488
461
|
}
|
|
489
462
|
}
|
|
490
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] =
|
|
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' :
|
|
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] =
|
|
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
|
+
}
|
|
@@ -1483,13 +1483,20 @@ paths:
|
|
|
1483
1483
|
refused, and so is a command that fails, **with `200`** and an `error` field instead of
|
|
1484
1484
|
`output` (the dashboard's terminal prints either).
|
|
1485
1485
|
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1486
|
+
A command must be one of the commands below, word for word, on one line. Where it takes
|
|
1487
|
+
arguments, they are plain words: letters, digits and `._/:=@%+,-`. Anything a shell would
|
|
1488
|
+
interpret (`;`, `|`, `&`, `$`, backticks, quotes, `<`, `>`) is refused.
|
|
1489
1489
|
|
|
1490
|
-
- **
|
|
1491
|
-
`
|
|
1492
|
-
|
|
1490
|
+
- **Android** (`adb shell`): `ls`, `ps`, `top`, `getprop`, `pm list packages`, `ip addr`,
|
|
1491
|
+
`date` and `netstat`, with arguments; `dumpsys battery`, `dumpsys wifi`, `dumpsys power`,
|
|
1492
|
+
`whoami`, `cat /proc/meminfo`, `cat /proc/cpuinfo` and `uptime`, without.
|
|
1493
|
+
|
|
1494
|
+
- **iOS simulator:** `listapps`, and `get_app_container` and `getenv` with arguments
|
|
1495
|
+
(through `simctl`); `ls`, `ps`, `whoami`, `date`, `uptime` and `id` without arguments
|
|
1496
|
+
(spawned in the simulator).
|
|
1497
|
+
|
|
1498
|
+
- **Real iPhone** (go-ios, through the phone's tunnel): `apps` with arguments and `info`
|
|
1499
|
+
without. Xenon names the phone; a command may not pass `--udid`.
|
|
1493
1500
|
|
|
1494
1501
|
Needs the `MEMBER` role and the `devices` scope. Refused with `409` while another user holds
|
|
1495
1502
|
the device (their live preview or recording) or runs an Appium session on it; admins are
|
|
@@ -1639,14 +1646,14 @@ paths:
|
|
|
1639
1646
|
operationId: scanDeviceScreen
|
|
1640
1647
|
summary: Analyse the screen with OCR and AI
|
|
1641
1648
|
description: >-
|
|
1642
|
-
Screenshots the device, runs OCR over it (every word with its confidence and box
|
|
1643
|
-
|
|
1644
|
-
needed. Slow: allow tens of seconds.
|
|
1649
|
+
Screenshots the device, runs OCR over it (every word with its confidence and box, in the
|
|
1650
|
+
screenshot's pixels) and sends the screenshot to the configured AI provider for a short
|
|
1651
|
+
description of the screen. No Appium session is needed. Slow: allow tens of seconds.
|
|
1645
1652
|
|
|
1646
1653
|
|
|
1647
1654
|
If the analysis itself fails (an empty screenshot, OCR failing), the answer is `500` with
|
|
1648
|
-
`{ status: 'error', message }`. If only the AI step fails,
|
|
1649
|
-
`ai_insights` is `null
|
|
1655
|
+
`{ status: 'error', message }`. If only the AI step fails, or no AI provider is
|
|
1656
|
+
configured, the answer is `200`, `ai_insights` is `null` and `ai_insights_error` says why.
|
|
1650
1657
|
|
|
1651
1658
|
|
|
1652
1659
|
Open to watchers. Needs the `MEMBER` role; any scope will do.
|
|
@@ -1683,10 +1690,27 @@ paths:
|
|
|
1683
1690
|
y0: 210
|
|
1684
1691
|
x1: 188
|
|
1685
1692
|
y1: 236
|
|
1686
|
-
ai_insights:
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1693
|
+
ai_insights: >-
|
|
1694
|
+
A sign-in screen with empty email and password fields and a disabled Sign
|
|
1695
|
+
in button. No error or dialog is shown.
|
|
1696
|
+
noProvider:
|
|
1697
|
+
summary: No AI provider configured
|
|
1698
|
+
value:
|
|
1699
|
+
status: success
|
|
1700
|
+
value:
|
|
1701
|
+
timestamp: '2026-10-04T14:05:22.118Z'
|
|
1702
|
+
ocr:
|
|
1703
|
+
text: Sign in
|
|
1704
|
+
words:
|
|
1705
|
+
- text: Sign
|
|
1706
|
+
confidence: 96.2
|
|
1707
|
+
bbox:
|
|
1708
|
+
x0: 142
|
|
1709
|
+
y0: 210
|
|
1710
|
+
x1: 188
|
|
1711
|
+
y1: 236
|
|
1712
|
+
ai_insights: null
|
|
1713
|
+
ai_insights_error: No AI provider is configured
|
|
1690
1714
|
'400':
|
|
1691
1715
|
description: This server has no device manager for the platform, or the udid is malformed.
|
|
1692
1716
|
content:
|
|
@@ -1745,8 +1769,9 @@ paths:
|
|
|
1745
1769
|
Runs one of Xenon's AI locator strategies against a fresh screenshot, as a test would with
|
|
1746
1770
|
`findElement`, without an Appium session.
|
|
1747
1771
|
|
|
1748
|
-
- `-custom:ai-text`: OCR; every
|
|
1749
|
-
|
|
1772
|
+
- `-custom:ai-text`: OCR; every place `selector` appears (case-insensitive), within a word
|
|
1773
|
+
or across neighbouring words on one line, so `Sign in` matches; each match read with
|
|
1774
|
+
confidence above 60%.
|
|
1750
1775
|
|
|
1751
1776
|
- `-custom:ai-icon`: the configured AI provider looks for what `selector` describes; at most
|
|
1752
1777
|
one match, a 40×40 box around the point it names.
|
|
@@ -1939,6 +1964,28 @@ paths:
|
|
|
1939
1964
|
On a hub, for a node's phone, the request is forwarded to the node, which holds the phone by
|
|
1940
1965
|
its own rules; on success the hub also marks the phone busy and held by you at once. A cloud
|
|
1941
1966
|
provider's phone answers `501`.
|
|
1967
|
+
|
|
1968
|
+
One capture runs per Android device. A page that shows MJPEG (a browser that cannot decode
|
|
1969
|
+
H.264, which on plain http it cannot, or a page whose H.264 player failed) sends
|
|
1970
|
+
`player: mjpeg`: the server then starts the MJPEG capture, and first ends an H.264 capture
|
|
1971
|
+
already running for the device, so the two never run together.
|
|
1972
|
+
requestBody:
|
|
1973
|
+
required: false
|
|
1974
|
+
content:
|
|
1975
|
+
application/json:
|
|
1976
|
+
schema:
|
|
1977
|
+
type: object
|
|
1978
|
+
properties:
|
|
1979
|
+
player:
|
|
1980
|
+
type: string
|
|
1981
|
+
enum:
|
|
1982
|
+
- mjpeg
|
|
1983
|
+
description: >-
|
|
1984
|
+
Set to `mjpeg` when the page shows MJPEG. Left out, or any other value, the
|
|
1985
|
+
server chooses: H.264 for an Android device when the setting is on and the
|
|
1986
|
+
device is not being recorded, MJPEG otherwise.
|
|
1987
|
+
example:
|
|
1988
|
+
player: mjpeg
|
|
1942
1989
|
responses:
|
|
1943
1990
|
'200':
|
|
1944
1991
|
description: The preview is running. `type` says which transport to use.
|
|
@@ -2968,10 +3015,14 @@ components:
|
|
|
2968
3015
|
y1:
|
|
2969
3016
|
type: number
|
|
2970
3017
|
ai_insights:
|
|
2971
|
-
type:
|
|
3018
|
+
type: string
|
|
2972
3019
|
nullable: true
|
|
2973
|
-
|
|
2974
|
-
|
|
3020
|
+
description: >-
|
|
3021
|
+
The AI provider's description of the screen, in plain text; null when there is no
|
|
3022
|
+
AI provider or its call failed.
|
|
3023
|
+
ai_insights_error:
|
|
3024
|
+
type: string
|
|
3025
|
+
description: Why `ai_insights` is null. Present only then.
|
|
2975
3026
|
ControlTestLocatorRequest:
|
|
2976
3027
|
type: object
|
|
2977
3028
|
required:
|
|
@@ -141,7 +141,8 @@ paths:
|
|
|
141
141
|
description: |
|
|
142
142
|
Sets `userBlocked` on a device, so no new session or lease is
|
|
143
143
|
allocated to it. A session already running on it is not affected,
|
|
144
|
-
and neither is `busy`.
|
|
144
|
+
and neither is `busy`. The device stays blocked when it disconnects
|
|
145
|
+
and comes back, through restarts too.
|
|
145
146
|
|
|
146
147
|
Send both `udid` and `host`, exactly as `GET /api/devices` shows
|
|
147
148
|
them: the device is the row of that udid on that host. Without
|
|
@@ -247,7 +248,9 @@ paths:
|
|
|
247
248
|
Replaces the device's tag list with `tags` (it doesn't append). Tags
|
|
248
249
|
are free-form labels, such as `flaky` or `lab-row-3`, that a lease
|
|
249
250
|
can filter on. The row is chosen by exact `udid` and `host`; a pair
|
|
250
|
-
that names no device
|
|
251
|
+
that names no device is `404` (through 2.13 it changed nothing and
|
|
252
|
+
answered `200`). The device keeps its tags when it disconnects and
|
|
253
|
+
comes back, through restarts too.
|
|
251
254
|
|
|
252
255
|
Needs role `ADMIN` and the `devices` scope.
|
|
253
256
|
tags:
|
|
@@ -298,6 +301,14 @@ paths:
|
|
|
298
301
|
$ref: '#/components/responses/Unauthorized'
|
|
299
302
|
'403':
|
|
300
303
|
$ref: '#/components/responses/Forbidden'
|
|
304
|
+
'404':
|
|
305
|
+
description: No device of that udid on that host.
|
|
306
|
+
content:
|
|
307
|
+
application/json:
|
|
308
|
+
schema:
|
|
309
|
+
$ref: '#/components/schemas/Error'
|
|
310
|
+
example:
|
|
311
|
+
error: Device not found
|
|
301
312
|
'429':
|
|
302
313
|
$ref: '#/components/responses/RateLimited'
|
|
303
314
|
/api/device/{udid}/team:
|
|
@@ -307,10 +318,14 @@ paths:
|
|
|
307
318
|
description: |
|
|
308
319
|
Gives the device to a team, or with `teamId: null` returns it to the
|
|
309
320
|
shared pool, which every member sees. A missing `teamId` counts as
|
|
310
|
-
null. Every
|
|
321
|
+
null. Every device with this udid changes (on a hub, each node that
|
|
311
322
|
reports it), and allocation and live dashboard events follow the new
|
|
312
323
|
team at once.
|
|
313
324
|
|
|
325
|
+
A device keeps its team when it disconnects and comes back, through
|
|
326
|
+
restarts too, and this also moves one of this udid that isn't
|
|
327
|
+
connected now: a team that one belongs to can't be deleted.
|
|
328
|
+
|
|
314
329
|
Needs role `ADMIN` and the `admin` scope.
|
|
315
330
|
tags:
|
|
316
331
|
- Devices
|
|
@@ -336,7 +351,7 @@ paths:
|
|
|
336
351
|
example: clx9k2v0a0000qz8r4h7m3n2p
|
|
337
352
|
responses:
|
|
338
353
|
'200':
|
|
339
|
-
description: The device's team changed. `updated` is how many
|
|
354
|
+
description: The device's team changed. `updated` is how many devices changed, connected or not.
|
|
340
355
|
content:
|
|
341
356
|
application/json:
|
|
342
357
|
schema:
|
|
@@ -432,7 +447,10 @@ paths:
|
|
|
432
447
|
(`reservedByUserId`), who alone, or an admin, may release or extend
|
|
433
448
|
it. A device already reserved by someone else, or running an Appium
|
|
434
449
|
session, is refused with `409`. The same user under the same
|
|
435
|
-
`reservedBy` may reserve it again, which restarts the clock.
|
|
450
|
+
`reservedBy` may reserve it again, which restarts the clock. The
|
|
451
|
+
reservation lasts until it ends even if the device disconnects or
|
|
452
|
+
the server restarts meanwhile; one that ended while the device was
|
|
453
|
+
away is not brought back.
|
|
436
454
|
|
|
437
455
|
The device is the row of `udid` on `host`, so send both exactly as
|
|
438
456
|
`GET /api/devices` shows them. No such row, or a device outside the
|