@xenon-device-management/xenon 2.13.2 → 2.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -5
- package/lib/package.json +1 -1
- package/lib/public/assets/{AnnotationOverlay-BXruClDk.js → AnnotationOverlay-B_VNtKBH.js} +1 -1
- package/lib/public/assets/{ApiKeyGate-qTS4LPLC.js → ApiKeyGate-BwuaMjkX.js} +1 -1
- package/lib/public/assets/{BugReportButton-BoGhlI7F.js → BugReportButton-BAt0_pzq.js} +1 -1
- package/lib/public/assets/DeviceMosaicView-BYaQHhTf.js +2 -0
- package/lib/public/assets/{EmptyState-Cr17PdDv.js → EmptyState-BSjhk_IV.js} +1 -1
- package/lib/public/assets/{FieldGroup-Bvn08svv.js → FieldGroup-Cb5QZsE1.js} +1 -1
- package/lib/public/assets/{FilterMenu-DDSO8aDf.js → FilterMenu-BAaSlWbW.js} +1 -1
- package/lib/public/assets/{Menu-B-Qn2X3T.js → Menu-JGGYrSQQ.js} +1 -1
- package/lib/public/assets/{Modal-Cg9V5JYU.js → Modal-CFCp3n9d.js} +1 -1
- package/lib/public/assets/{RecordingPage-BTUs9VTW.js → RecordingPage-CbJj_EBT.js} +1 -1
- package/lib/public/assets/{RecordingsPage-DYcfxITK.js → RecordingsPage-BJeYnygR.js} +2 -2
- package/lib/public/assets/{SegmentedControl-Zy7suhT5.js → SegmentedControl-Bg42Atjy.js} +1 -1
- package/lib/public/assets/{SettingCard-CWYmycoe.js → SettingCard-Njz1pUIJ.js} +1 -1
- package/lib/public/assets/{Table-io9fwa98.js → Table-BcTm5xV2.js} +1 -1
- package/lib/public/assets/{activity-Bdf_uIWl.js → activity-DOVdsfMQ.js} +1 -1
- package/lib/public/assets/{ai-settings-Bce5SJob.js → ai-settings-CsuJroRf.js} +2 -2
- package/lib/public/assets/{api-keys-CPqtVpm3.js → api-keys-BKnNSTw9.js} +1 -1
- package/lib/public/assets/{apps-BgZan0H9.js → apps-D0SC6uw6.js} +2 -2
- package/lib/public/assets/{arrow-left-B27ckB31.js → arrow-left-MMbTmgoE.js} +1 -1
- package/lib/public/assets/{arrow-right-DOF8ITCz.js → arrow-right-BMfnGmxW.js} +1 -1
- package/lib/public/assets/{arrow-up-right-eFG5KTFS.js → arrow-up-right-DuAftMWB.js} +1 -1
- package/lib/public/assets/{auth-shell-DlC3_27j.js → auth-shell-nZC2JNDO.js} +2 -2
- package/lib/public/assets/{builds-page-BuXpPfKo.js → builds-page-hDCgTwkS.js} +1 -1
- package/lib/public/assets/{button-fIW8owMI.js → button-Dda4irn_.js} +1 -1
- package/lib/public/assets/{calendar-WNjmrF-a.js → calendar-BMr0Q62v.js} +1 -1
- package/lib/public/assets/{check-BdTh1Sq7.js → check-CZ9iw9-e.js} +1 -1
- package/lib/public/assets/{chevron-right-B4BKlsqk.js → chevron-right-BRYJJVC9.js} +1 -1
- package/lib/public/assets/{circle-check-zJU80_EF.js → circle-check-CqAJjQ5O.js} +1 -1
- package/lib/public/assets/{circle-x-5Uwquo0h.js → circle-x-DlDJtnVz.js} +1 -1
- package/lib/public/assets/{clock-BtUprDsZ.js → clock-CpuDKysU.js} +1 -1
- package/lib/public/assets/{copy-DGSbZI3u.js → copy-CAQydNFe.js} +1 -1
- package/lib/public/assets/device-explorer-CdO6_nrn.js +282 -0
- package/lib/public/assets/{download-WG6l7BgD.js → download-C9XG6lLv.js} +1 -1
- package/lib/public/assets/{forgot-password-DnudqFFo.js → forgot-password-CGnkRgE9.js} +1 -1
- package/lib/public/assets/h264Stream-VQBphYhg.js +26 -0
- package/lib/public/assets/{index-DI1fGIyj.js → index-BbKEXUFH.js} +5 -5
- package/lib/public/assets/{index-HDQVe1gi.js → index-DJGc1M2f.js} +1 -1
- package/lib/public/assets/{input-BjDRKCea.js → input-Cm0tmSoc.js} +1 -1
- package/lib/public/assets/{line-chart-C0zdgd0v.js → line-chart-D2DyJCml.js} +1 -1
- package/lib/public/assets/{list-checks-BdW5CjM8.js → list-checks-UWf1GY99.js} +1 -1
- package/lib/public/assets/{lock-Dy3L4I8E.js → lock-CCJv9m_I.js} +1 -1
- package/lib/public/assets/{login-BHbY9CLG.js → login-f8vOVW-U.js} +1 -1
- package/lib/public/assets/maintenance-settings-C1PrkuAH.js +6 -0
- package/lib/public/assets/{monitor-8xjkeHHi.js → monitor-B2jDgwm7.js} +1 -1
- package/lib/public/assets/{mouse-pointer-2-CXdKMXQ8.js → mouse-pointer-2-DLI1RtzU.js} +1 -1
- package/lib/public/assets/{network-Cj8JOKON.js → network-ebT86qoi.js} +1 -1
- package/lib/public/assets/{overview-CTg7F0im.js → overview-C0aykVrd.js} +1 -1
- package/lib/public/assets/{page-header--aTyJXq6.js → page-header-BjzgWz-T.js} +1 -1
- package/lib/public/assets/{play-B-FodgHV.js → play-DLG9t8FU.js} +1 -1
- package/lib/public/assets/{plus-CsIk10Cq.js → plus-nBFSMu_X.js} +1 -1
- package/lib/public/assets/{profile-page-DkFKkMfl.js → profile-page-CSl0UYlv.js} +1 -1
- package/lib/public/assets/{reset-password-I2-zqLlI.js → reset-password-LeOuHqmB.js} +1 -1
- package/lib/public/assets/{runbook-page-DJTVCJX7.js → runbook-page-CMMeQml9.js} +1 -1
- package/lib/public/assets/{select-DvCSv25z.js → select-88jHcl-Z.js} +1 -1
- package/lib/public/assets/{selector-detail-redirect-BLVfMJs_.js → selector-detail-redirect-BbXKJjZM.js} +1 -1
- package/lib/public/assets/{selector-health-page-oqadRokn.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-DV9Ix-4O.js → stat-tile-BV98e_Sv.js} +1 -1
- package/lib/public/assets/{tablet-6b_SEN2X.js → tablet-BiAsnFDj.js} +1 -1
- package/lib/public/assets/{teams-BjZyfuHh.js → teams-B4-jrr-1.js} +1 -1
- package/lib/public/assets/{trash-2-CTZpPSdd.js → trash-2-C5RSuX0e.js} +1 -1
- package/lib/public/assets/{upload-P3XzwQUb.js → upload-BG7_rMXK.js} +1 -1
- package/lib/public/assets/{use-builds-data-1cOCMf-z.js → use-builds-data-J-riQYyC.js} +1 -1
- package/lib/public/assets/{use-password-reset-mode-DgF9w088.js → use-password-reset-mode-C8-eH-UG.js} +1 -1
- package/lib/public/assets/{users-DMF9Mk5H.js → users-CFba5TSy.js} +1 -1
- package/lib/public/assets/{users-CiNTKUiz.js → users-Cp0beJm3.js} +1 -1
- package/lib/public/assets/{video-off-CHp1KU_T.js → video-off-DDsEISjV.js} +1 -1
- package/lib/public/assets/webhook-settings-BGu32vSB.js +1 -0
- package/lib/public/assets/{zap-DRz0tDq5.js → zap-l4VaXzq8.js} +1 -1
- package/lib/public/index.html +1 -1
- package/lib/schema.json +19 -21
- 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 +12 -2
- package/lib/src/app/routers/dashboard.js +29 -1
- package/lib/src/app/routers/grid.js +32 -10
- 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 +6 -1
- package/lib/src/data-service/device-store.js +62 -2
- 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 +150 -8
- 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 +96 -40
- 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/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 +94 -8
- package/lib/src/services/SessionLifecycleService.js +47 -49
- package/lib/src/services/ShutdownCoordinator.js +33 -0
- package/lib/src/services/TeamService.js +20 -3
- 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/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/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-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/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 +14 -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/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 -21
- package/scripts/generate-types-from-schema.js +1 -2
- package/scripts/lib/always-present-plugin-args.js +4 -1
- package/lib/public/assets/DeviceMosaicView-8J0oYqMM.js +0 -2
- package/lib/public/assets/device-explorer-C-KnhMHI.js +0 -279
- package/lib/public/assets/maintenance-settings-zDVlOMVK.js +0 -6
- package/lib/public/assets/recording-group-store-WVM3hoXn.js +0 -26
- package/lib/public/assets/session-detail-page-CGn55Pvl.js +0 -4
- package/lib/public/assets/settings-MsVU2ukE.js +0 -6
- package/lib/public/assets/webhook-settings-DiS8geIm.js +0 -1
|
@@ -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
|
|
@@ -187,11 +187,17 @@ paths:
|
|
|
187
187
|
operationId: getLabSettings
|
|
188
188
|
summary: Read the lab's settings and AI provider
|
|
189
189
|
description: |
|
|
190
|
-
The lab-wide settings
|
|
191
|
-
with the AI settings currently in effect.
|
|
192
|
-
|
|
193
|
-
is
|
|
194
|
-
|
|
190
|
+
The lab-wide settings in effect (health checks, build cleanup and the AI self-healing
|
|
191
|
+
switch), merged with the AI settings currently in effect. Each setting is the value saved
|
|
192
|
+
in the dashboard, else the plugin option the server was started with, else the default,
|
|
193
|
+
which is also what the server runs with. `defaults` holds the defaults themselves, which
|
|
194
|
+
is what "restore defaults" goes back to. Only `healthCheckSchedule` can be absent: no
|
|
195
|
+
schedule is set. Through 2.13 `enableSelfHealing` was neither stored nor sent, so the
|
|
196
|
+
Settings page always showed it on.
|
|
197
|
+
API keys are never returned: `geminiSet`, `openaiSet` and `anthropicSet` say only
|
|
198
|
+
whether one is configured. Needs the `ADMIN` role or above, as the dashboard's settings
|
|
199
|
+
pages do; through 2.12 any signed-in user could read it. Through 2.13 a setting never
|
|
200
|
+
saved was absent, and `defaults` was not sent.
|
|
195
201
|
tags:
|
|
196
202
|
- Configuration
|
|
197
203
|
responses:
|
|
@@ -202,8 +208,19 @@ paths:
|
|
|
202
208
|
schema:
|
|
203
209
|
$ref: '#/components/schemas/PlatformSettings'
|
|
204
210
|
example:
|
|
211
|
+
enableSelfHealing: true
|
|
205
212
|
healthCheckIntervalMs: 300000
|
|
206
213
|
buildCleanupDays: 30
|
|
214
|
+
buildCleanupMaxCount: 100
|
|
215
|
+
buildCleanupSchedule: 0 0 * * *
|
|
216
|
+
deleteBuildAssets: true
|
|
217
|
+
defaults:
|
|
218
|
+
enableSelfHealing: true
|
|
219
|
+
healthCheckIntervalMs: 300000
|
|
220
|
+
buildCleanupDays: 30
|
|
221
|
+
buildCleanupMaxCount: 100
|
|
222
|
+
buildCleanupSchedule: 0 0 * * *
|
|
223
|
+
deleteBuildAssets: true
|
|
207
224
|
aiProvider: gemini
|
|
208
225
|
aiModel: gemini-3-flash-preview
|
|
209
226
|
geminiModel: gemini-3-flash-preview
|
|
@@ -233,7 +250,22 @@ paths:
|
|
|
233
250
|
Saves the lab-wide settings and changes the AI provider settings. Every field is
|
|
234
251
|
optional; fields not sent are left as they are, and unknown fields are ignored.
|
|
235
252
|
|
|
236
|
-
- Health-check and build-cleanup fields are stored in the database.
|
|
253
|
+
- Health-check and build-cleanup fields are stored in the database. A stored value
|
|
254
|
+
replaces the plugin option the server was started with, and applies without a
|
|
255
|
+
restart: the health check within a minute, the retention values at the next cleanup
|
|
256
|
+
run, and a new `buildCleanupSchedule` at once. The cleanup fields are checked first
|
|
257
|
+
and refused with `400` when they could not work: `buildCleanupDays` and
|
|
258
|
+
`buildCleanupMaxCount` must be whole numbers of at least 1, `buildCleanupSchedule` a
|
|
259
|
+
cron expression of five or six fields, `deleteBuildAssets` true or false. Nothing is
|
|
260
|
+
saved then. Send only the fields to change: a field sent is saved as the lab's own.
|
|
261
|
+
Through 2.13 only one setting could be saved: saving a second failed with a `500`.
|
|
262
|
+
- `enableSelfHealing` (true or false; anything else is refused with `400`) is the AI
|
|
263
|
+
self-healing switch. A stored value replaces the plugin option the server was started
|
|
264
|
+
with, and applies to the next command this server runs, with no restart. A session's
|
|
265
|
+
own `xe:options.healingTiers` still limits the tiers it may use. Through 2.13 the
|
|
266
|
+
switch was accepted and ignored. The value belongs to the server it is saved on:
|
|
267
|
+
saving it on a hub changes the hub's own sessions, not those on a node's phones,
|
|
268
|
+
which the node's own setting decides.
|
|
237
269
|
- AI fields (`aiProvider`, `aiModel`, `aiBaseUrl` and the per-provider models) change the
|
|
238
270
|
running server only. They are not stored, so a restart returns to the values the
|
|
239
271
|
server was started with. API keys can't be set here; they come from the server's
|
|
@@ -254,6 +286,7 @@ paths:
|
|
|
254
286
|
healthCheckIntervalMs: 300000
|
|
255
287
|
buildCleanupDays: 14
|
|
256
288
|
deleteBuildAssets: true
|
|
289
|
+
enableSelfHealing: false
|
|
257
290
|
aiProvider: anthropic
|
|
258
291
|
anthropicModel: claude-sonnet-4-6
|
|
259
292
|
responses:
|
|
@@ -265,6 +298,16 @@ paths:
|
|
|
265
298
|
$ref: '#/components/schemas/Success'
|
|
266
299
|
example:
|
|
267
300
|
success: true
|
|
301
|
+
'400':
|
|
302
|
+
description: A build-cleanup field or the self-healing switch has a value that could not work. Nothing was saved.
|
|
303
|
+
content:
|
|
304
|
+
application/json:
|
|
305
|
+
schema:
|
|
306
|
+
$ref: '#/components/schemas/Error'
|
|
307
|
+
example:
|
|
308
|
+
error: invalid_setting
|
|
309
|
+
field: buildCleanupDays
|
|
310
|
+
message: buildCleanupDays must be a whole number of at least 1.
|
|
268
311
|
'401':
|
|
269
312
|
$ref: '#/components/responses/Unauthorized'
|
|
270
313
|
'403':
|
|
@@ -279,7 +322,7 @@ paths:
|
|
|
279
322
|
$ref: '#/components/schemas/Error'
|
|
280
323
|
example:
|
|
281
324
|
error: true
|
|
282
|
-
message:
|
|
325
|
+
message: Can't reach database server
|
|
283
326
|
/api/config/reset-metrics:
|
|
284
327
|
post:
|
|
285
328
|
operationId: resetDeviceHealCounts
|
|
@@ -415,21 +458,39 @@ paths:
|
|
|
415
458
|
description: |
|
|
416
459
|
Adds a webhook, active at once. When one of its `events` happens Xenon POSTs to `url`:
|
|
417
460
|
|
|
418
|
-
- with `payloadTemplate`: the template, with each `{{
|
|
419
|
-
the event (`eventType` plus the event's fields
|
|
420
|
-
JSON,
|
|
461
|
+
- with `payloadTemplate`: the template, with each `{{name}}` (or `{{a.b}}`, `{{list.0.b}}`)
|
|
462
|
+
replaced from the event (`eventType` plus the event's fields, below). A template that
|
|
463
|
+
is JSON as written is filled in string by string, so a value holding a quote or a line
|
|
464
|
+
break can't break it, and is sent as that JSON. Any other template is filled in as
|
|
465
|
+
text and sent as JSON if the result parses, otherwise as `{ "text": "<result>" }`. A
|
|
466
|
+
`{{name}}` the event doesn't have is left as written;
|
|
421
467
|
- otherwise, `type: slack`: a Slack message with an attachment;
|
|
422
468
|
- otherwise: `{ "event": "<event>", "payload": { ... } }`.
|
|
423
469
|
|
|
424
|
-
Each delivery is one POST, never retried. A
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
470
|
+
Each delivery is one POST, never retried. A webhook that refuses an event or can't be
|
|
471
|
+
reached doesn't fail the event: the failure is logged on the server and the other
|
|
472
|
+
webhooks still get it. Use `POST /api/webhook/test` to see whether a webhook accepts
|
|
473
|
+
deliveries.
|
|
428
474
|
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
475
|
+
What each event carries (a device event also has the device's other fields, which may
|
|
476
|
+
change; only the names listed are promised):
|
|
477
|
+
|
|
478
|
+
- `device_offline`, `device_new`: `udid`, `name`, `host`, `platform`.
|
|
479
|
+
Sent when a device stops being reported, or is first detected.
|
|
480
|
+
|
|
481
|
+
- `session_failed`: `sessionId`, `sessionName`, `failureReason`, `udid`, `deviceName`,
|
|
482
|
+
`platform`, `osVersion`, `startTime`, `endTime` (ISO 8601; unknown values are empty
|
|
483
|
+
text). Sent once per session when it ends as failed: the test marked it failed, a command
|
|
484
|
+
failed, it timed out for inactivity, its driver crashed, or its heartbeat stopped. Not
|
|
485
|
+
when the server itself shuts down, not for a session already failed when the server
|
|
486
|
+
started, and not when the dashboard is off, since the session's record is what it is
|
|
487
|
+
built from.
|
|
488
|
+
|
|
489
|
+
- `selector_health_digest`: `windowDays`, `totalHeals`, `distinctSelectors` and
|
|
490
|
+
`hotspots`, a list of `healCount`, `originalSelector` and `suggestedRewrite`.
|
|
491
|
+
|
|
492
|
+
Neither `url` nor the event names are checked. Needs the `ADMIN` role and the `admin`
|
|
493
|
+
scope. With the dashboard cookie the request must come from the same host.
|
|
433
494
|
tags:
|
|
434
495
|
- Webhooks
|
|
435
496
|
requestBody:
|
|
@@ -536,11 +597,12 @@ paths:
|
|
|
536
597
|
operationId: testWebhook
|
|
537
598
|
summary: Send a test message to a URL
|
|
538
599
|
description: |
|
|
539
|
-
Sends a sample `device_new`
|
|
540
|
-
`test-device-udid`, host `127.0.0.1
|
|
541
|
-
`payloadTemplate` gets its real events (see
|
|
542
|
-
|
|
543
|
-
|
|
600
|
+
Sends a sample of `event` (`device_new` unless named; a device named `Test Device`, udid
|
|
601
|
+
`test-device-udid`, host `127.0.0.1`, or for `session_failed` a failed session on it) to
|
|
602
|
+
`url` the way a webhook with this `type` and `payloadTemplate` gets its real events (see
|
|
603
|
+
`POST /api/webhook`, which lists what each event carries): the filled-in template if
|
|
604
|
+
there is one, else a Slack message for `type: slack` (the default), else
|
|
605
|
+
`{ "event": "<event>", "payload": { ... } }`. No stored webhook is involved.
|
|
544
606
|
|
|
545
607
|
Answers `200` when `url` accepted the delivery (a 2xx), and `502 delivery_failed`, with
|
|
546
608
|
the reason, when it refused it or couldn't be reached. Needs the `ADMIN` role and the
|
|
@@ -575,6 +637,16 @@ paths:
|
|
|
575
637
|
The webhook's template, filled in from the sample event as a real event's
|
|
576
638
|
would be.
|
|
577
639
|
example: '{"text": "{{eventType}}: {{name}} ({{udid}})"}'
|
|
640
|
+
event:
|
|
641
|
+
type: string
|
|
642
|
+
default: device_new
|
|
643
|
+
enum:
|
|
644
|
+
- device_offline
|
|
645
|
+
- session_failed
|
|
646
|
+
- device_new
|
|
647
|
+
- selector_health_digest
|
|
648
|
+
description: Which event to send a sample of.
|
|
649
|
+
example: session_failed
|
|
578
650
|
example:
|
|
579
651
|
url: https://hooks.slack.com/services/T000/B000/XXXXXXXX
|
|
580
652
|
type: slack
|
|
@@ -588,13 +660,18 @@ paths:
|
|
|
588
660
|
example:
|
|
589
661
|
success: true
|
|
590
662
|
'400':
|
|
591
|
-
description: '`url` is missing or not a string.'
|
|
663
|
+
description: '`url` is missing or not a string, or `event` is not one of the four.'
|
|
592
664
|
content:
|
|
593
665
|
application/json:
|
|
594
666
|
schema:
|
|
595
667
|
$ref: '#/components/schemas/Error'
|
|
596
|
-
|
|
597
|
-
|
|
668
|
+
examples:
|
|
669
|
+
noUrl:
|
|
670
|
+
value:
|
|
671
|
+
error: url is required
|
|
672
|
+
badEvent:
|
|
673
|
+
value:
|
|
674
|
+
error: 'event must be one of: device_offline, session_failed, device_new, selector_health_digest'
|
|
598
675
|
'401':
|
|
599
676
|
$ref: '#/components/responses/Unauthorized'
|
|
600
677
|
'403':
|
|
@@ -2264,8 +2341,12 @@ components:
|
|
|
2264
2341
|
example: 734120
|
|
2265
2342
|
PlatformSettings:
|
|
2266
2343
|
type: object
|
|
2267
|
-
description: The lab's
|
|
2344
|
+
description: The lab's settings in effect (saved, else the startup option, else the default), merged with the AI settings in effect. Only `healthCheckSchedule` can be absent.
|
|
2268
2345
|
properties:
|
|
2346
|
+
enableSelfHealing:
|
|
2347
|
+
type: boolean
|
|
2348
|
+
description: Whether a failed findElement goes on to the self-healing tiers.
|
|
2349
|
+
example: true
|
|
2269
2350
|
healthCheckIntervalMs:
|
|
2270
2351
|
type: integer
|
|
2271
2352
|
example: 300000
|
|
@@ -2283,6 +2364,27 @@ components:
|
|
|
2283
2364
|
example: 0 3 * * *
|
|
2284
2365
|
deleteBuildAssets:
|
|
2285
2366
|
type: boolean
|
|
2367
|
+
defaults:
|
|
2368
|
+
type: object
|
|
2369
|
+
description: The server's own defaults for the settings above, which is what "restore defaults" goes back to.
|
|
2370
|
+
properties:
|
|
2371
|
+
enableSelfHealing:
|
|
2372
|
+
type: boolean
|
|
2373
|
+
example: true
|
|
2374
|
+
healthCheckIntervalMs:
|
|
2375
|
+
type: integer
|
|
2376
|
+
example: 300000
|
|
2377
|
+
buildCleanupDays:
|
|
2378
|
+
type: integer
|
|
2379
|
+
example: 30
|
|
2380
|
+
buildCleanupMaxCount:
|
|
2381
|
+
type: integer
|
|
2382
|
+
example: 100
|
|
2383
|
+
buildCleanupSchedule:
|
|
2384
|
+
type: string
|
|
2385
|
+
example: 0 0 * * *
|
|
2386
|
+
deleteBuildAssets:
|
|
2387
|
+
type: boolean
|
|
2286
2388
|
aiProvider:
|
|
2287
2389
|
type: string
|
|
2288
2390
|
example: gemini
|
|
@@ -2311,6 +2413,10 @@ components:
|
|
|
2311
2413
|
type: object
|
|
2312
2414
|
description: Any subset. Unknown fields are ignored.
|
|
2313
2415
|
properties:
|
|
2416
|
+
enableSelfHealing:
|
|
2417
|
+
type: boolean
|
|
2418
|
+
description: The AI self-healing switch. Takes effect at the next command, with no restart.
|
|
2419
|
+
example: false
|
|
2314
2420
|
healthCheckIntervalMs:
|
|
2315
2421
|
type: integer
|
|
2316
2422
|
example: 300000
|
|
@@ -2448,7 +2554,9 @@ components:
|
|
|
2448
2554
|
example: slack
|
|
2449
2555
|
payloadTemplate:
|
|
2450
2556
|
type: string
|
|
2451
|
-
description:
|
|
2557
|
+
description: >-
|
|
2558
|
+
A body template with `{{name}}` placeholders, used in place of the default body (see
|
|
2559
|
+
`POST /api/webhook` for the names each event has).
|
|
2452
2560
|
example: '{"text":"{{eventType}} on {{udid}}"}'
|
|
2453
2561
|
PlatformApp:
|
|
2454
2562
|
type: object
|
|
@@ -625,7 +625,7 @@ const MJPEG_PROXY_CACHE = new Map();
|
|
|
625
625
|
* Start stream endpoint - Initiates WDA and MJPEG streaming for iOS devices
|
|
626
626
|
*/
|
|
627
627
|
router.post('/:udid/stream/start', (req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
628
|
-
var _a, _b, _c, _d, _e;
|
|
628
|
+
var _a, _b, _c, _d, _e, _f;
|
|
629
629
|
const { udid } = req.params;
|
|
630
630
|
// Someone is about to watch: a page that just left (a reload, or another tab
|
|
631
631
|
// closing) must not stop the stream this page is starting.
|
|
@@ -709,7 +709,11 @@ router.post('/:udid/stream/start', (req, res) => __awaiter(void 0, void 0, void
|
|
|
709
709
|
// A recording device keeps MJPEG preview so we never run screenrecord and
|
|
710
710
|
// the recording's screencap pipeline against the same device at once.
|
|
711
711
|
const recording = yield typedi_1.Container.get(recording_store_1.RecordingStore).isRecording(udid);
|
|
712
|
-
|
|
712
|
+
// A page that shows MJPEG says so (`player: 'mjpeg'`): device control, a
|
|
713
|
+
// browser with no WebCodecs, or a page whose H.264 player just failed.
|
|
714
|
+
// Anything else is the server's choice.
|
|
715
|
+
const clientShowsMjpeg = ((_f = req.body) === null || _f === void 0 ? void 0 : _f.player) === 'mjpeg';
|
|
716
|
+
const streamType = (0, streamType_1.resolveStreamType)(device.platform, flagOn, recording, !clientShowsMjpeg);
|
|
713
717
|
let mjpegPort;
|
|
714
718
|
if (device.platform === 'ios' || device.platform === 'tvos') {
|
|
715
719
|
mjpegPort = (yield typedi_1.Container.get(IOSStreamService_1.default).startStream(udid)).mjpegPort;
|
|
@@ -720,6 +724,12 @@ router.post('/:udid/stream/start', (req, res) => __awaiter(void 0, void 0, void
|
|
|
720
724
|
yield typedi_1.Container.get(AndroidH264StreamService_1.default).start(udid, { source: h264Cfg.source });
|
|
721
725
|
}
|
|
722
726
|
else {
|
|
727
|
+
// One capture pipeline per device: an H.264 capture still running for it
|
|
728
|
+
// (a tile's, or this page's own before its player failed) ends before the
|
|
729
|
+
// screencap loop starts.
|
|
730
|
+
const h264 = typedi_1.Container.get(AndroidH264StreamService_1.default);
|
|
731
|
+
if (h264.getMultiplexer(udid))
|
|
732
|
+
yield h264.stop(udid);
|
|
723
733
|
mjpegPort = (yield typedi_1.Container.get(AndroidStreamService_1.default).startStream(udid)).mjpegPort;
|
|
724
734
|
}
|
|
725
735
|
// Lazy-load the screen size if missing: tiles need it to accept taps.
|
|
@@ -56,6 +56,9 @@ const prisma_1 = require("../../prisma");
|
|
|
56
56
|
const SessionManager_1 = require("../../sessions/SessionManager");
|
|
57
57
|
const UniversalMjpegProxy_1 = require("../../helpers/UniversalMjpegProxy");
|
|
58
58
|
const web_config_service_1 = require("../../data-service/web-config-service");
|
|
59
|
+
const PluginContext_1 = require("../../PluginContext");
|
|
60
|
+
const labSettings_1 = require("../../services/settings/labSettings");
|
|
61
|
+
const SelfHealingSwitch_1 = require("../../services/settings/SelfHealingSwitch");
|
|
59
62
|
const typedi_1 = require("typedi");
|
|
60
63
|
const scopeGuard_1 = require("../../middleware/scopeGuard");
|
|
61
64
|
const roleGuard_1 = require("../../middleware/roleGuard");
|
|
@@ -1103,6 +1106,10 @@ function getGlobalConfig(request, response) {
|
|
|
1103
1106
|
return __awaiter(this, void 0, void 0, function* () {
|
|
1104
1107
|
try {
|
|
1105
1108
|
const dbConfig = yield typedi_1.Container.get(web_config_service_1.WebConfigService).getConfig();
|
|
1109
|
+
// What the server runs with: a saved value, else the startup option, else
|
|
1110
|
+
// the default. The pages show this (a saved value alone left them to guess
|
|
1111
|
+
// at what a setting never saved was), and `defaults` for "restore defaults".
|
|
1112
|
+
const effective = (0, labSettings_1.effectiveSettings)(typedi_1.Container.get(PluginContext_1.PluginContext).pluginArgs, dbConfig);
|
|
1106
1113
|
// Merge with Environment Config (AI Settings)
|
|
1107
1114
|
const { config } = yield Promise.resolve().then(() => __importStar(require('../../config')));
|
|
1108
1115
|
// Sanitize keys - return boolean existence only
|
|
@@ -1118,7 +1125,9 @@ function getGlobalConfig(request, response) {
|
|
|
1118
1125
|
openaiSet: !!config.openaiApiKey,
|
|
1119
1126
|
anthropicSet: !!config.anthropicApiKey,
|
|
1120
1127
|
};
|
|
1121
|
-
return response
|
|
1128
|
+
return response
|
|
1129
|
+
.status(200)
|
|
1130
|
+
.json(Object.assign(Object.assign(Object.assign(Object.assign({}, dbConfig), effective), { defaults: (0, labSettings_1.settingsDefaults)() }), aiConfig));
|
|
1122
1131
|
}
|
|
1123
1132
|
catch (err) {
|
|
1124
1133
|
return response.status(500).json({ error: true, message: err.message });
|
|
@@ -1129,6 +1138,14 @@ function updateGlobalConfig(request, response) {
|
|
|
1129
1138
|
return __awaiter(this, void 0, void 0, function* () {
|
|
1130
1139
|
try {
|
|
1131
1140
|
const payload = request.body;
|
|
1141
|
+
// Before anything is saved: a value the cleanup job would act on, such as a
|
|
1142
|
+
// retention window of 0, is refused rather than stored.
|
|
1143
|
+
const problem = (0, labSettings_1.validateSettingsUpdate)(payload !== null && payload !== void 0 ? payload : {});
|
|
1144
|
+
if (problem) {
|
|
1145
|
+
return response
|
|
1146
|
+
.status(400)
|
|
1147
|
+
.json({ error: 'invalid_setting', field: problem.field, message: problem.message });
|
|
1148
|
+
}
|
|
1132
1149
|
// Handle Runtime AI Config Overrides (Memory only)
|
|
1133
1150
|
// Only pass defined values to avoid overwriting env vars (e.g. aiBaseUrl, ollamaModel) with undefined
|
|
1134
1151
|
const runtimeOverrides = {};
|
|
@@ -1152,6 +1169,17 @@ function updateGlobalConfig(request, response) {
|
|
|
1152
1169
|
}
|
|
1153
1170
|
// Persist Web Configs to DB
|
|
1154
1171
|
yield typedi_1.Container.get(web_config_service_1.WebConfigService).setConfig(payload);
|
|
1172
|
+
// The command interceptor asks at every command and never reads the
|
|
1173
|
+
// database: hand it the saved value, in force from the next command.
|
|
1174
|
+
if (payload.enableSelfHealing !== undefined) {
|
|
1175
|
+
typedi_1.Container.get(SelfHealingSwitch_1.SelfHealingSwitch).set(payload.enableSelfHealing);
|
|
1176
|
+
}
|
|
1177
|
+
// The retention values are read at each cleanup run, so they apply by
|
|
1178
|
+
// themselves. A schedule is a timer: replace it now, not at the next restart.
|
|
1179
|
+
if (payload.buildCleanupSchedule !== undefined) {
|
|
1180
|
+
const { rescheduleCleanupBuilds } = yield Promise.resolve().then(() => __importStar(require('../../device-utils')));
|
|
1181
|
+
yield rescheduleCleanupBuilds(typedi_1.Container.get(PluginContext_1.PluginContext).pluginArgs);
|
|
1182
|
+
}
|
|
1155
1183
|
return response.status(200).json({ success: true });
|
|
1156
1184
|
}
|
|
1157
1185
|
catch (err) {
|
|
@@ -402,10 +402,33 @@ function updateTags(request, response) {
|
|
|
402
402
|
if (!udid || !host || !Array.isArray(tags)) {
|
|
403
403
|
return response.status(400).json({ error: 'Missing udid, host, or tags array' });
|
|
404
404
|
}
|
|
405
|
+
// The tags are saved for the phone (deviceSettings.ts): only for one that
|
|
406
|
+
// is here. Through 2.13 any udid and host answered 200 and wrote nothing.
|
|
407
|
+
if (!(yield device_store_1.DeviceStoreFactory.getStore().findDevice({ udid, host }))) {
|
|
408
|
+
return response.status(404).json({ error: 'Device not found' });
|
|
409
|
+
}
|
|
405
410
|
yield (0, device_service_1.updateDeviceTags)(udid, host, tags);
|
|
406
411
|
response.status(200).json({ success: true });
|
|
407
412
|
});
|
|
408
413
|
}
|
|
414
|
+
/**
|
|
415
|
+
* Every phone of this udid that this server knows: those connected now, and
|
|
416
|
+
* those whose settings are saved while they are away (deviceSettings.ts), so
|
|
417
|
+
* a phone that is not connected can still be moved, and its team deleted.
|
|
418
|
+
*/
|
|
419
|
+
function phonesOfUdid(udid) {
|
|
420
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
421
|
+
const store = device_store_1.DeviceStoreFactory.getStore();
|
|
422
|
+
const phones = new Map();
|
|
423
|
+
for (const phone of [
|
|
424
|
+
...(yield store.findDevices({ udid })),
|
|
425
|
+
...(yield store.findSavedPhones(udid)),
|
|
426
|
+
]) {
|
|
427
|
+
phones.set(`${phone.udid}\u0000${phone.host}`, { udid: phone.udid, host: phone.host });
|
|
428
|
+
}
|
|
429
|
+
return [...phones.values()];
|
|
430
|
+
});
|
|
431
|
+
}
|
|
409
432
|
function assignDeviceToTeam(request, response) {
|
|
410
433
|
return __awaiter(this, void 0, void 0, function* () {
|
|
411
434
|
const { udid } = request.params;
|
|
@@ -415,20 +438,19 @@ function assignDeviceToTeam(request, response) {
|
|
|
415
438
|
if (!team)
|
|
416
439
|
return response.status(404).json({ error: 'team not found' });
|
|
417
440
|
}
|
|
418
|
-
const
|
|
419
|
-
|
|
420
|
-
data: { teamId: teamId || null },
|
|
421
|
-
});
|
|
422
|
-
if (result.count === 0)
|
|
441
|
+
const phones = yield phonesOfUdid(udid);
|
|
442
|
+
if (phones.length === 0)
|
|
423
443
|
return response.status(404).json({ error: 'device not found' });
|
|
424
|
-
//
|
|
425
|
-
|
|
426
|
-
for (const
|
|
427
|
-
yield
|
|
444
|
+
// Through the store, which saves the team for the phone as it writes the
|
|
445
|
+
// row: a phone keeps it when it disconnects and comes back.
|
|
446
|
+
for (const phone of phones) {
|
|
447
|
+
yield device_store_1.DeviceStoreFactory.getStore().updateDevice(phone.udid, phone.host, {
|
|
448
|
+
teamId: teamId || null,
|
|
449
|
+
});
|
|
428
450
|
}
|
|
429
451
|
// The live dashboard events follow the new team now, not after the cache's TTL.
|
|
430
452
|
typedi_1.Container.get(DeviceTeamResolver_1.DeviceTeamResolver).note(udid, teamId || null);
|
|
431
|
-
response.json({ ok: true, updated:
|
|
453
|
+
response.json({ ok: true, updated: phones.length });
|
|
432
454
|
});
|
|
433
455
|
}
|
|
434
456
|
/**
|