@xenon-device-management/xenon 2.12.0 → 2.13.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/lib/package.json +2 -2
- package/lib/public/assets/{AnnotationOverlay-Crkn72_3.js → AnnotationOverlay-CzbFYtPI.js} +1 -1
- package/lib/public/assets/{ApiKeyGate-BThVlj_j.js → ApiKeyGate-CA5KFHHJ.js} +1 -1
- package/lib/public/assets/{BugReportButton-Dwa0GnBh.js → BugReportButton-D-DSGg8C.js} +1 -1
- package/lib/public/assets/{DeviceMosaicView-Ccc-pOls.js → DeviceMosaicView-paDEFU4B.js} +1 -1
- package/lib/public/assets/{EmptyState-BeuTrlN_.js → EmptyState-BE_5f8Vw.js} +1 -1
- package/lib/public/assets/{FieldGroup-DBIaNxl1.js → FieldGroup-DqMf7Rf3.js} +1 -1
- package/lib/public/assets/{FilterMenu-BgSAS6iT.js → FilterMenu-BQY75Su3.js} +1 -1
- package/lib/public/assets/{Menu-GCbP6gYN.js → Menu-BiW3os9u.js} +1 -1
- package/lib/public/assets/{Modal-ByESeBRC.js → Modal-DfawTxih.js} +1 -1
- package/lib/public/assets/{RecordingPage-Bwyf3WMl.js → RecordingPage-BvemyRiy.js} +1 -1
- package/lib/public/assets/{RecordingsPage-7kM1rB3i.js → RecordingsPage-CXfw7wpd.js} +1 -1
- package/lib/public/assets/{SegmentedControl-B8tTU8kD.js → SegmentedControl-B7m9kpom.js} +1 -1
- package/lib/public/assets/{SettingCard-Clg_Ijbc.js → SettingCard-Cw6lDD0l.js} +1 -1
- package/lib/public/assets/{Table-CIaR0QWV.js → Table-DlEuFgv-.js} +1 -1
- package/lib/public/assets/{activity-Bengh9SD.js → activity-CnLvNwae.js} +1 -1
- package/lib/public/assets/{ai-settings-G9SymiPH.js → ai-settings-D7Te8o-a.js} +1 -1
- package/lib/public/assets/{api-keys-CrGlbfn-.js → api-keys-DphSVzBr.js} +1 -1
- package/lib/public/assets/{apps-DIbcGVlU.js → apps-vUfu15lX.js} +1 -1
- package/lib/public/assets/{arrow-left-BtJIXXMY.js → arrow-left-CLQWAiCY.js} +1 -1
- package/lib/public/assets/{arrow-right-zm-NagQh.js → arrow-right-K6A5e_Ee.js} +1 -1
- package/lib/public/assets/{arrow-up-right-DdU8YEYp.js → arrow-up-right-DUA_lq6f.js} +1 -1
- package/lib/public/assets/{auth-shell-CHmBFmOz.js → auth-shell-BYRHcZd6.js} +2 -2
- package/lib/public/assets/{builds-page-JkzWKRpU.js → builds-page-C0fTvIG8.js} +1 -1
- package/lib/public/assets/{button-CqhPBGRj.js → button-BxnHCCsX.js} +1 -1
- package/lib/public/assets/{calendar-C7eYYJTo.js → calendar-CqVsn28A.js} +1 -1
- package/lib/public/assets/{check-DpbIM4E0.js → check-R6V3bot0.js} +1 -1
- package/lib/public/assets/{chevron-right-Dl4X1PRz.js → chevron-right-BA2Fntu-.js} +1 -1
- package/lib/public/assets/{circle-check-Du3sjbfV.js → circle-check-D4EzyGzs.js} +1 -1
- package/lib/public/assets/{circle-x-BSSqhpXi.js → circle-x-Cf6AF2B2.js} +1 -1
- package/lib/public/assets/{clock-DMT60v1C.js → clock-EZfet0sS.js} +1 -1
- package/lib/public/assets/{copy-BKNyOehd.js → copy-Bc6P8v9V.js} +1 -1
- package/lib/public/assets/{device-explorer-CoPR8DW3.js → device-explorer-BFXpRq6d.js} +1 -1
- package/lib/public/assets/{download-BM6Xn22t.js → download-Bd8OGLzi.js} +1 -1
- package/lib/public/assets/{forgot-password-B8WqMqBT.js → forgot-password-CoE7Ugtx.js} +1 -1
- package/lib/public/assets/{index-DauQh6ie.js → index-BiL0Enl_.js} +1 -1
- package/lib/public/assets/{index-ClrpAMAT.js → index-CHOr4JCs.js} +2 -2
- package/lib/public/assets/{input-CyKdLnEx.js → input-CJ8z3IO5.js} +1 -1
- package/lib/public/assets/{line-chart-EIBXwYGo.js → line-chart-CPK0ObjV.js} +1 -1
- package/lib/public/assets/{list-checks-fWsgD9bI.js → list-checks-mXGHUVbB.js} +1 -1
- package/lib/public/assets/{lock-CVCe56TH.js → lock-BSXM1xvq.js} +1 -1
- package/lib/public/assets/{login-BJ8a7yVD.js → login-Ct2iW066.js} +1 -1
- package/lib/public/assets/{maintenance-settings-DcRmTLS6.js → maintenance-settings-DaOs00P6.js} +1 -1
- package/lib/public/assets/{monitor-Bw1YQZnL.js → monitor-D8WQK8md.js} +1 -1
- package/lib/public/assets/{mouse-pointer-2-C01jbqkO.js → mouse-pointer-2-Cw2tWzDX.js} +1 -1
- package/lib/public/assets/{network-CBGUjJDJ.js → network-Ba_uFxWo.js} +1 -1
- package/lib/public/assets/{overview-Cxe8aQ7C.js → overview-CxrZ7_6k.js} +1 -1
- package/lib/public/assets/{page-header-B92DKLiq.js → page-header-Cm1XL49f.js} +1 -1
- package/lib/public/assets/{play-Ck0L-0_m.js → play-C0CuKLXj.js} +1 -1
- package/lib/public/assets/{plus-Dq3tCy2N.js → plus-CIIvR_6Z.js} +1 -1
- package/lib/public/assets/{profile-page--dkDiKbr.js → profile-page-BZ_e-Bgw.js} +1 -1
- package/lib/public/assets/{recording-group-store-5BYIFFN9.js → recording-group-store-BWeeNs2_.js} +1 -1
- package/lib/public/assets/{reset-password-KYWlfjia.js → reset-password-lfNz8Rm1.js} +1 -1
- package/lib/public/assets/{runbook-page-mnsxgS1d.js → runbook-page-Cnec3VSx.js} +1 -1
- package/lib/public/assets/{select-BBOIZTYm.js → select-CZuCnold.js} +1 -1
- package/lib/public/assets/{selector-detail-redirect-CcI2j_Su.js → selector-detail-redirect-BG3pxFLw.js} +1 -1
- package/lib/public/assets/{selector-health-page-BdJb_x5L.js → selector-health-page-DsyL58tb.js} +1 -1
- package/lib/public/assets/{session-detail-page-BjLaJy8z.js → session-detail-page-ZZyGM413.js} +1 -1
- package/lib/public/assets/{settings-B6cpMhsx.js → settings-DGZ7B_AR.js} +1 -1
- package/lib/public/assets/{stat-tile-BU9e4s36.js → stat-tile-DFhv8JoI.js} +1 -1
- package/lib/public/assets/{tablet-hgbEwrWq.js → tablet-68J5f6zh.js} +1 -1
- package/lib/public/assets/{teams-uiiG4hZM.js → teams-eHSyEpbD.js} +1 -1
- package/lib/public/assets/{trash-2-NK_Iazmg.js → trash-2-CcSsv4fS.js} +1 -1
- package/lib/public/assets/{upload-BUX8TNFi.js → upload-BYf26K71.js} +1 -1
- package/lib/public/assets/{use-builds-data-DZszmFyl.js → use-builds-data-CHW-MhaP.js} +1 -1
- package/lib/public/assets/{use-password-reset-mode-BK4B9kRC.js → use-password-reset-mode-DozShXkk.js} +1 -1
- package/lib/public/assets/{users-BU4XBbMT.js → users-BpYnpOl2.js} +1 -1
- package/lib/public/assets/{users-BDh1xjad.js → users-CcSQRoK9.js} +1 -1
- package/lib/public/assets/{video-off-BOjNwT4R.js → video-off-dFKgps73.js} +1 -1
- package/lib/public/assets/{webhook-settings-BMzyUcx5.js → webhook-settings-DD1XjiOD.js} +1 -1
- package/lib/public/assets/{zap-DTWwVMUg.js → zap-COC2tZaH.js} +1 -1
- package/lib/public/index.html +1 -1
- package/lib/src/app/apiErrors.js +118 -0
- package/lib/src/app/index.js +6 -1
- package/lib/src/app/openapi/control.yaml +3129 -0
- package/lib/src/app/openapi/grid.yaml +2295 -0
- package/lib/src/app/openapi/identity.yaml +2168 -0
- package/lib/src/app/openapi/platform.yaml +2885 -0
- package/lib/src/app/openapi/sessions.yaml +3784 -0
- package/lib/src/app/routers/bug-report.js +4 -1
- package/lib/src/app/routers/config.js +6 -107
- package/lib/src/app/routers/control.js +117 -59
- package/lib/src/app/routers/dashboard.js +13 -7
- package/lib/src/app/routers/grid.js +54 -14
- package/lib/src/app/routers/profile.js +27 -13
- package/lib/src/app/routers/recordings.js +11 -5
- package/lib/src/app/routers/reservation.js +63 -15
- package/lib/src/app/routers/users.js +4 -0
- package/lib/src/app/routers/webhook.js +17 -8
- package/lib/src/app/swagger.js +259 -177
- package/lib/src/data-service/device-service.js +4 -1
- package/lib/src/data-service/deviceFieldOwners.js +1 -0
- package/lib/src/device-managers/AndroidDeviceManager.js +5 -2
- package/lib/src/device-managers/ios/WDAClient.js +32 -32
- package/lib/src/generated/client/edge.js +4 -3
- package/lib/src/generated/client/index-browser.js +1 -0
- package/lib/src/generated/client/index.d.ts +38 -0
- package/lib/src/generated/client/index.js +4 -3
- package/lib/src/generated/client/package.json +1 -1
- package/lib/src/generated/client/schema.prisma +2 -0
- package/lib/src/generated/client/wasm.js +1 -0
- package/lib/src/middleware/csrfMiddleware.js +13 -5
- package/lib/src/middleware/rateLimitMiddleware.js +19 -5
- package/lib/src/middleware/roleGuard.js +20 -0
- package/lib/src/services/AIService.js +13 -3
- package/lib/src/services/NotificationService.js +28 -28
- package/lib/src/services/bug-report/BugReportService.js +8 -2
- package/lib/src/services/lease/LeaseService.js +71 -20
- package/lib/src/services/omni-vision/OmniVisionService.js +14 -5
- package/lib/src/services/recording/RecordingOrchestrator.js +16 -2
- package/lib/test/helpers/expressRoutes.js +41 -0
- package/lib/test/integration/team-visibility-control.spec.js +2 -2
- package/lib/test/unit/access-scopes.spec.js +227 -0
- package/lib/test/unit/api-error-handling.spec.js +179 -0
- package/lib/test/unit/bug-report/route.spec.js +29 -0
- package/lib/test/unit/bug-report/service.spec.js +30 -0
- package/lib/test/unit/control-honest-answers.spec.js +134 -0
- package/lib/test/unit/device-allocation-routes.spec.js +232 -0
- package/lib/test/unit/healing-state-endpoints.spec.js +8 -5
- package/lib/test/unit/install-repository-app-team.spec.js +4 -1
- package/lib/test/unit/lease/LeaseService.spec.js +5 -4
- package/lib/test/unit/lease/lease-device-match.spec.js +161 -0
- package/lib/test/unit/lease/lease-session-ownership.spec.js +10 -9
- package/lib/test/unit/omni-vision-failures.spec.js +81 -0
- package/lib/test/unit/openapi-coverage.spec.js +114 -0
- package/lib/test/unit/profile-router.test.js +42 -0
- package/lib/test/unit/rateLimitMiddleware.test.js +49 -0
- package/lib/test/unit/recording-orchestrator.spec.js +80 -0
- package/lib/test/unit/recordings-library-routes.spec.js +37 -0
- package/lib/test/unit/reservation-team-visibility.spec.js +4 -3
- package/lib/test/unit/reset-link.test.js +2 -1
- package/lib/test/unit/stream-ticket-identity.spec.js +1 -1
- package/lib/test/unit/users-router.test.js +14 -1
- package/lib/test/unit/wda-client-failures.spec.js +90 -0
- package/lib/test/unit/webhook-delivery.spec.js +142 -0
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/prisma/migrations/20261004120000_reservation_holder/migration.sql +2 -0
- package/prisma/schema.prisma +2 -0
- package/lib/src/app/swagger-docs.js +0 -1701
|
@@ -1,1701 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* @swagger
|
|
4
|
-
* /api/devices:
|
|
5
|
-
* get:
|
|
6
|
-
* summary: Get all connected devices
|
|
7
|
-
* description: Retrieve a list of all connected devices with their current status
|
|
8
|
-
* tags: [Devices]
|
|
9
|
-
* responses:
|
|
10
|
-
* 200:
|
|
11
|
-
* description: List of devices
|
|
12
|
-
* content:
|
|
13
|
-
* application/json:
|
|
14
|
-
* schema:
|
|
15
|
-
* type: array
|
|
16
|
-
* items:
|
|
17
|
-
* $ref: '#/components/schemas/Device'
|
|
18
|
-
* 401: { $ref: '#/components/responses/Unauthorized' }
|
|
19
|
-
* 429: { $ref: '#/components/responses/RateLimited' }
|
|
20
|
-
*/
|
|
21
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
22
|
-
/**
|
|
23
|
-
* @swagger
|
|
24
|
-
* /api/config:
|
|
25
|
-
* get:
|
|
26
|
-
* summary: Get current configuration
|
|
27
|
-
* description: Retrieve the current runtime configuration of the Xenon plugin
|
|
28
|
-
* tags: [Configuration]
|
|
29
|
-
* responses:
|
|
30
|
-
* 200:
|
|
31
|
-
* description: Current configuration object
|
|
32
|
-
* content:
|
|
33
|
-
* application/json:
|
|
34
|
-
* schema:
|
|
35
|
-
* type: object
|
|
36
|
-
* 401: { $ref: '#/components/responses/Unauthorized' }
|
|
37
|
-
* 429: { $ref: '#/components/responses/RateLimited' }
|
|
38
|
-
* put:
|
|
39
|
-
* summary: Update configuration
|
|
40
|
-
* description: Update plugin configuration properties. Changes are persisted. Some changes may require a server restart.
|
|
41
|
-
* tags: [Configuration]
|
|
42
|
-
* requestBody:
|
|
43
|
-
* required: true
|
|
44
|
-
* content:
|
|
45
|
-
* application/json:
|
|
46
|
-
* schema:
|
|
47
|
-
* type: object
|
|
48
|
-
* description: Partial configuration object
|
|
49
|
-
* example:
|
|
50
|
-
* deviceAvailabilityTimeoutMs: 60000
|
|
51
|
-
* maxSessions: 5
|
|
52
|
-
* responses:
|
|
53
|
-
* 200:
|
|
54
|
-
* description: Configuration updated
|
|
55
|
-
* content:
|
|
56
|
-
* application/json:
|
|
57
|
-
* schema:
|
|
58
|
-
* type: object
|
|
59
|
-
* properties:
|
|
60
|
-
* success:
|
|
61
|
-
* type: boolean
|
|
62
|
-
* restartRequired:
|
|
63
|
-
* type: boolean
|
|
64
|
-
* description: True if the updated properties require a server restart
|
|
65
|
-
* message:
|
|
66
|
-
* type: string
|
|
67
|
-
* 401: { $ref: '#/components/responses/Unauthorized' }
|
|
68
|
-
* 403: { $ref: '#/components/responses/Forbidden' }
|
|
69
|
-
* 429: { $ref: '#/components/responses/RateLimited' }
|
|
70
|
-
*/
|
|
71
|
-
/**
|
|
72
|
-
* @swagger
|
|
73
|
-
* /api/sessions/active:
|
|
74
|
-
* get:
|
|
75
|
-
* summary: Get active sessions
|
|
76
|
-
* description: Get statistics and list of currently active sessions in memory
|
|
77
|
-
* tags: [Sessions]
|
|
78
|
-
* responses:
|
|
79
|
-
* 200:
|
|
80
|
-
* description: Active session information
|
|
81
|
-
* content:
|
|
82
|
-
* application/json:
|
|
83
|
-
* schema:
|
|
84
|
-
* type: object
|
|
85
|
-
* properties:
|
|
86
|
-
* stats:
|
|
87
|
-
* type: object
|
|
88
|
-
* properties:
|
|
89
|
-
* total:
|
|
90
|
-
* type: number
|
|
91
|
-
* description: Total number of active sessions
|
|
92
|
-
* byType:
|
|
93
|
-
* type: object
|
|
94
|
-
* properties:
|
|
95
|
-
* local:
|
|
96
|
-
* type: number
|
|
97
|
-
* remote:
|
|
98
|
-
* type: number
|
|
99
|
-
* cloud:
|
|
100
|
-
* type: number
|
|
101
|
-
* sessions:
|
|
102
|
-
* type: array
|
|
103
|
-
* items:
|
|
104
|
-
* type: object
|
|
105
|
-
* properties:
|
|
106
|
-
* id:
|
|
107
|
-
* type: string
|
|
108
|
-
* type:
|
|
109
|
-
* type: string
|
|
110
|
-
* enum: [local, remote, cloud]
|
|
111
|
-
* deviceUdid:
|
|
112
|
-
* type: string
|
|
113
|
-
* deviceName:
|
|
114
|
-
* type: string
|
|
115
|
-
* platform:
|
|
116
|
-
* type: string
|
|
117
|
-
* 401: { $ref: '#/components/responses/Unauthorized' }
|
|
118
|
-
* 429: { $ref: '#/components/responses/RateLimited' }
|
|
119
|
-
*/
|
|
120
|
-
/**
|
|
121
|
-
* @swagger
|
|
122
|
-
* /api/logs/requests:
|
|
123
|
-
* get:
|
|
124
|
-
* summary: Get HTTP request logs
|
|
125
|
-
* description: Retrieve recent HTTP request logs for debugging hub-node communication
|
|
126
|
-
* tags: [Observability]
|
|
127
|
-
* parameters:
|
|
128
|
-
* - in: query
|
|
129
|
-
* name: limit
|
|
130
|
-
* schema:
|
|
131
|
-
* type: integer
|
|
132
|
-
* default: 50
|
|
133
|
-
* description: Maximum number of logs to return
|
|
134
|
-
* - in: query
|
|
135
|
-
* name: method
|
|
136
|
-
* schema:
|
|
137
|
-
* type: string
|
|
138
|
-
* enum: [GET, POST, PUT, DELETE]
|
|
139
|
-
* description: Filter by HTTP method
|
|
140
|
-
* - in: query
|
|
141
|
-
* name: url
|
|
142
|
-
* schema:
|
|
143
|
-
* type: string
|
|
144
|
-
* description: Filter by URL pattern (substring match)
|
|
145
|
-
* - in: query
|
|
146
|
-
* name: hasError
|
|
147
|
-
* schema:
|
|
148
|
-
* type: boolean
|
|
149
|
-
* description: Filter by error status (true = only errors, false = only success)
|
|
150
|
-
* responses:
|
|
151
|
-
* 200:
|
|
152
|
-
* description: Request logs and statistics
|
|
153
|
-
* content:
|
|
154
|
-
* application/json:
|
|
155
|
-
* schema:
|
|
156
|
-
* type: object
|
|
157
|
-
* properties:
|
|
158
|
-
* stats:
|
|
159
|
-
* type: object
|
|
160
|
-
* properties:
|
|
161
|
-
* totalLogged:
|
|
162
|
-
* type: number
|
|
163
|
-
* description: Total requests in buffer
|
|
164
|
-
* errorCount:
|
|
165
|
-
* type: number
|
|
166
|
-
* description: Number of failed requests
|
|
167
|
-
* avgDurationMs:
|
|
168
|
-
* type: number
|
|
169
|
-
* description: Average request duration in milliseconds
|
|
170
|
-
* byMethod:
|
|
171
|
-
* type: object
|
|
172
|
-
* additionalProperties:
|
|
173
|
-
* type: number
|
|
174
|
-
* byStatusCode:
|
|
175
|
-
* type: object
|
|
176
|
-
* additionalProperties:
|
|
177
|
-
* type: number
|
|
178
|
-
* logs:
|
|
179
|
-
* type: array
|
|
180
|
-
* items:
|
|
181
|
-
* type: object
|
|
182
|
-
* properties:
|
|
183
|
-
* timestamp:
|
|
184
|
-
* type: string
|
|
185
|
-
* format: date-time
|
|
186
|
-
* direction:
|
|
187
|
-
* type: string
|
|
188
|
-
* enum: [outgoing, incoming]
|
|
189
|
-
* method:
|
|
190
|
-
* type: string
|
|
191
|
-
* url:
|
|
192
|
-
* type: string
|
|
193
|
-
* statusCode:
|
|
194
|
-
* type: number
|
|
195
|
-
* durationMs:
|
|
196
|
-
* type: number
|
|
197
|
-
* error:
|
|
198
|
-
* type: string
|
|
199
|
-
* nullable: true
|
|
200
|
-
* requestBody:
|
|
201
|
-
* type: string
|
|
202
|
-
* description: Sanitized request body (sensitive data redacted)
|
|
203
|
-
* responseBody:
|
|
204
|
-
* type: string
|
|
205
|
-
* description: Truncated response body
|
|
206
|
-
* 401: { $ref: '#/components/responses/Unauthorized' }
|
|
207
|
-
* 429: { $ref: '#/components/responses/RateLimited' }
|
|
208
|
-
*/
|
|
209
|
-
/**
|
|
210
|
-
* @swagger
|
|
211
|
-
* tags:
|
|
212
|
-
* - name: Observability
|
|
213
|
-
* description: Debugging and monitoring endpoints
|
|
214
|
-
*/
|
|
215
|
-
/**
|
|
216
|
-
* @swagger
|
|
217
|
-
* /api/healing/selector/state:
|
|
218
|
-
* post:
|
|
219
|
-
* summary: Apply a lifecycle action to a selector
|
|
220
|
-
* description: |
|
|
221
|
-
* Mutates the SelectorState row for a (strategy, selector) tuple and
|
|
222
|
-
* records the change, with the person, in SelectorEvent. Actions are
|
|
223
|
-
* bounded — `mark_fixed`, `mute`, `unmute`, `cancel_verification`.
|
|
224
|
-
* Members and admins may act on a selector healed in a session they
|
|
225
|
-
* can see; any other answers 404, as an unknown one. Needs the
|
|
226
|
-
* `sessions` scope (`admin` implies it). `original_strategy` may be
|
|
227
|
-
* empty, for heals recorded with no strategy. Returns 409 with
|
|
228
|
-
* `currentStatus` if the action is incompatible with the row's
|
|
229
|
-
* current state (e.g. mark_fixed on a muted selector).
|
|
230
|
-
* tags: [Selector Health]
|
|
231
|
-
* security:
|
|
232
|
-
* - apiKey: []
|
|
233
|
-
* requestBody:
|
|
234
|
-
* required: true
|
|
235
|
-
* content:
|
|
236
|
-
* application/json:
|
|
237
|
-
* schema:
|
|
238
|
-
* type: object
|
|
239
|
-
* required: [original_strategy, original_selector, action]
|
|
240
|
-
* properties:
|
|
241
|
-
* original_strategy:
|
|
242
|
-
* type: string
|
|
243
|
-
* example: accessibility id
|
|
244
|
-
* original_selector:
|
|
245
|
-
* type: string
|
|
246
|
-
* example: login-btn
|
|
247
|
-
* action:
|
|
248
|
-
* type: string
|
|
249
|
-
* enum: [mark_fixed, mute, unmute, cancel_verification]
|
|
250
|
-
* reason: { type: string, maxLength: 500, description: 'Why the selector is muted; mute only' }
|
|
251
|
-
* responses:
|
|
252
|
-
* 200:
|
|
253
|
-
* description: Updated SelectorState row
|
|
254
|
-
* 400:
|
|
255
|
-
* description: Missing fields, unknown action, or a reason over 500 characters
|
|
256
|
-
* 403:
|
|
257
|
-
* description: The caller lacks the `sessions` scope
|
|
258
|
-
* 404:
|
|
259
|
-
* description: '`{ error: "not_found", message: "Selector not found" }`'
|
|
260
|
-
* 409:
|
|
261
|
-
* description: Action conflicts with current state (returns currentStatus)
|
|
262
|
-
* 401: { $ref: '#/components/responses/Unauthorized' }
|
|
263
|
-
* 429: { $ref: '#/components/responses/RateLimited' }
|
|
264
|
-
*/
|
|
265
|
-
/**
|
|
266
|
-
* @swagger
|
|
267
|
-
* /api/healing/state/muted:
|
|
268
|
-
* get:
|
|
269
|
-
* summary: List muted selectors
|
|
270
|
-
* description: |
|
|
271
|
-
* Returns muted selectors sourced directly from SelectorState
|
|
272
|
-
* (not the hotspot aggregator), so silenced-but-not-recently-healed
|
|
273
|
-
* rows still surface. Each row is enriched with the most recent
|
|
274
|
-
* `is_healed=true` SessionLog timestamp for the tuple.
|
|
275
|
-
* tags: [Selector Health]
|
|
276
|
-
* parameters:
|
|
277
|
-
* - in: query
|
|
278
|
-
* name: limit
|
|
279
|
-
* schema: { type: integer, default: 50, maximum: 200 }
|
|
280
|
-
* - in: query
|
|
281
|
-
* name: offset
|
|
282
|
-
* schema: { type: integer, default: 0 }
|
|
283
|
-
* responses:
|
|
284
|
-
* 200:
|
|
285
|
-
* description: Paginated muted selectors
|
|
286
|
-
* content:
|
|
287
|
-
* application/json:
|
|
288
|
-
* schema:
|
|
289
|
-
* type: object
|
|
290
|
-
* properties:
|
|
291
|
-
* muted: { type: array }
|
|
292
|
-
* total: { type: integer }
|
|
293
|
-
* limit: { type: integer }
|
|
294
|
-
* offset: { type: integer }
|
|
295
|
-
* 401: { $ref: '#/components/responses/Unauthorized' }
|
|
296
|
-
* 429: { $ref: '#/components/responses/RateLimited' }
|
|
297
|
-
*/
|
|
298
|
-
/**
|
|
299
|
-
* @swagger
|
|
300
|
-
* /api/healing/state/{strategy}/{value}:
|
|
301
|
-
* get:
|
|
302
|
-
* summary: Look up SelectorState for a single tuple
|
|
303
|
-
* description: |
|
|
304
|
-
* URL-encoded strategy + value. Returns `{ state: null }` when no
|
|
305
|
-
* row exists — null is meaningful (the selector is implicitly Active).
|
|
306
|
-
* tags: [Selector Health]
|
|
307
|
-
* parameters:
|
|
308
|
-
* - in: path
|
|
309
|
-
* name: strategy
|
|
310
|
-
* required: true
|
|
311
|
-
* schema: { type: string }
|
|
312
|
-
* - in: path
|
|
313
|
-
* name: value
|
|
314
|
-
* required: true
|
|
315
|
-
* schema: { type: string }
|
|
316
|
-
* responses:
|
|
317
|
-
* 200:
|
|
318
|
-
* description: SelectorState row, or null
|
|
319
|
-
* 401: { $ref: '#/components/responses/Unauthorized' }
|
|
320
|
-
* 429: { $ref: '#/components/responses/RateLimited' }
|
|
321
|
-
*/
|
|
322
|
-
/**
|
|
323
|
-
* @swagger
|
|
324
|
-
* /api/healing/hotspots:
|
|
325
|
-
* get:
|
|
326
|
-
* summary: List the most-healed selectors (with lifecycle filter)
|
|
327
|
-
* description: |
|
|
328
|
-
* Status filter is `active` (default), `pending`, `resolved`, `muted`,
|
|
329
|
-
* or `all`. The default value transparently hides muted/pending/
|
|
330
|
-
* resolved selectors from the live list, the CI gate, and the digest.
|
|
331
|
-
* tags: [Selector Health]
|
|
332
|
-
* parameters:
|
|
333
|
-
* - in: query
|
|
334
|
-
* name: windowDays
|
|
335
|
-
* schema: { type: integer, default: 30 }
|
|
336
|
-
* - in: query
|
|
337
|
-
* name: limit
|
|
338
|
-
* schema: { type: integer, default: 10 }
|
|
339
|
-
* - in: query
|
|
340
|
-
* name: status
|
|
341
|
-
* schema: { type: string, enum: [active, pending, resolved, muted, all], default: active }
|
|
342
|
-
* - in: query
|
|
343
|
-
* name: tier
|
|
344
|
-
* schema: { type: string }
|
|
345
|
-
* - in: query
|
|
346
|
-
* name: platform
|
|
347
|
-
* schema: { type: string }
|
|
348
|
-
* responses:
|
|
349
|
-
* 200:
|
|
350
|
-
* description: Hotspots with state overlay
|
|
351
|
-
* 401: { $ref: '#/components/responses/Unauthorized' }
|
|
352
|
-
* 429: { $ref: '#/components/responses/RateLimited' }
|
|
353
|
-
*/
|
|
354
|
-
// =============================================================================
|
|
355
|
-
// Health & Operations
|
|
356
|
-
// =============================================================================
|
|
357
|
-
/**
|
|
358
|
-
* @swagger
|
|
359
|
-
* /api/health:
|
|
360
|
-
* get:
|
|
361
|
-
* summary: Liveness probe
|
|
362
|
-
* description: |
|
|
363
|
-
* Public, unauthenticated, not rate-limited. Returns `{ ok: true }` as soon
|
|
364
|
-
* as the Express stack is up — does not check database or device state.
|
|
365
|
-
* Suitable for Kubernetes/load-balancer liveness probes.
|
|
366
|
-
* tags: [Health & Ops]
|
|
367
|
-
* security: []
|
|
368
|
-
* responses:
|
|
369
|
-
* 200:
|
|
370
|
-
* description: Server is up
|
|
371
|
-
* content:
|
|
372
|
-
* application/json:
|
|
373
|
-
* schema:
|
|
374
|
-
* type: object
|
|
375
|
-
* properties:
|
|
376
|
-
* ok: { type: boolean, example: true }
|
|
377
|
-
*/
|
|
378
|
-
/**
|
|
379
|
-
* @swagger
|
|
380
|
-
* /api/ping:
|
|
381
|
-
* get:
|
|
382
|
-
* summary: Authenticated ping with build version
|
|
383
|
-
* description: Returns `{ pong, version }`. Use to confirm an API key is valid and read the running plugin version.
|
|
384
|
-
* tags: [Health & Ops]
|
|
385
|
-
* responses:
|
|
386
|
-
* 200:
|
|
387
|
-
* description: Pong with version
|
|
388
|
-
* content:
|
|
389
|
-
* application/json:
|
|
390
|
-
* schema:
|
|
391
|
-
* type: object
|
|
392
|
-
* properties:
|
|
393
|
-
* pong: { type: boolean, example: true }
|
|
394
|
-
* version: { type: string, example: '1.5.0' }
|
|
395
|
-
* 401: { $ref: '#/components/responses/Unauthorized' }
|
|
396
|
-
* 429: { $ref: '#/components/responses/RateLimited' }
|
|
397
|
-
*/
|
|
398
|
-
/**
|
|
399
|
-
* @swagger
|
|
400
|
-
* /api/metrics:
|
|
401
|
-
* get:
|
|
402
|
-
* summary: Prometheus-format metrics
|
|
403
|
-
* description: |
|
|
404
|
-
* Returns scrape-friendly Prometheus exposition format (`text/plain; version=0.0.4`).
|
|
405
|
-
* Auth-gated to avoid operational reconnaissance — point your scraper at this
|
|
406
|
-
* endpoint with a `read`-scope token in the `(x-xenon-access-key, x-xenon-token)` pair.
|
|
407
|
-
* tags: [Health & Ops]
|
|
408
|
-
* responses:
|
|
409
|
-
* 200:
|
|
410
|
-
* description: Prometheus metrics
|
|
411
|
-
* content:
|
|
412
|
-
* text/plain:
|
|
413
|
-
* schema: { type: string }
|
|
414
|
-
* 401: { $ref: '#/components/responses/Unauthorized' }
|
|
415
|
-
* 429: { $ref: '#/components/responses/RateLimited' }
|
|
416
|
-
*/
|
|
417
|
-
/**
|
|
418
|
-
* @swagger
|
|
419
|
-
* /api/cliArgs:
|
|
420
|
-
* get:
|
|
421
|
-
* summary: Read the plugin CLI arguments in effect
|
|
422
|
-
* description: |
|
|
423
|
-
* Returns the resolved plugin arguments (host, hub URL, platform, intervals, etc.)
|
|
424
|
-
* this process was started with. Requires the ADMIN role and `admin` scope.
|
|
425
|
-
* Values are redacted the way the logger redacts them: any key that looks
|
|
426
|
-
* secret (database URL, API key, token, password, auth…) reads
|
|
427
|
-
* `***REDACTED***`, and so do provider-key-shaped strings.
|
|
428
|
-
* tags: [Health & Ops]
|
|
429
|
-
* responses:
|
|
430
|
-
* 200:
|
|
431
|
-
* description: Resolved plugin arguments
|
|
432
|
-
* content:
|
|
433
|
-
* application/json:
|
|
434
|
-
* schema: { type: object, additionalProperties: true }
|
|
435
|
-
* 401: { $ref: '#/components/responses/Unauthorized' }
|
|
436
|
-
* 403: { $ref: '#/components/responses/Forbidden' }
|
|
437
|
-
* 429: { $ref: '#/components/responses/RateLimited' }
|
|
438
|
-
*/
|
|
439
|
-
// =============================================================================
|
|
440
|
-
// Authentication
|
|
441
|
-
// =============================================================================
|
|
442
|
-
/**
|
|
443
|
-
* @swagger
|
|
444
|
-
* /api/auth/dashboard-session:
|
|
445
|
-
* post:
|
|
446
|
-
* summary: Exchange an API key for a browser dashboard session cookie
|
|
447
|
-
* description: |
|
|
448
|
-
* Used by the dashboard UI to convert a one-time API key paste into a
|
|
449
|
-
* short-lived session cookie. Public endpoint — no API key header is
|
|
450
|
-
* required because the body itself carries the credential. On success,
|
|
451
|
-
* sets `xenon_dashboard_session` (HttpOnly, Secure, SameSite=Strict, 24 h
|
|
452
|
-
* max-age). Subsequent dashboard requests use the cookie instead of the
|
|
453
|
-
* header. CSRF protection: cookie-authed callers must send a matching
|
|
454
|
-
* `Origin`/`Referer`.
|
|
455
|
-
* tags: [Authentication]
|
|
456
|
-
* security: []
|
|
457
|
-
* requestBody:
|
|
458
|
-
* required: true
|
|
459
|
-
* content:
|
|
460
|
-
* application/json:
|
|
461
|
-
* schema:
|
|
462
|
-
* type: object
|
|
463
|
-
* required: [apiKey]
|
|
464
|
-
* properties:
|
|
465
|
-
* apiKey: { type: string, description: 'Plain-text API key' }
|
|
466
|
-
* responses:
|
|
467
|
-
* 200:
|
|
468
|
-
* description: Session cookie set
|
|
469
|
-
* content:
|
|
470
|
-
* application/json:
|
|
471
|
-
* schema:
|
|
472
|
-
* type: object
|
|
473
|
-
* properties:
|
|
474
|
-
* ok: { type: boolean, example: true }
|
|
475
|
-
* scopes:
|
|
476
|
-
* type: array
|
|
477
|
-
* items: { type: string, enum: [read, sessions, devices, admin] }
|
|
478
|
-
* 400: { description: 'Missing apiKey field' }
|
|
479
|
-
* 401: { description: 'Invalid API key' }
|
|
480
|
-
*/
|
|
481
|
-
// =============================================================================
|
|
482
|
-
// Admin — API Keys
|
|
483
|
-
// =============================================================================
|
|
484
|
-
/**
|
|
485
|
-
* @swagger
|
|
486
|
-
* /api/apikeys:
|
|
487
|
-
* get:
|
|
488
|
-
* summary: List all API keys
|
|
489
|
-
* description: Returns metadata only — the plain-text key value is never returned after creation. Requires `admin` scope.
|
|
490
|
-
* tags: [Admin]
|
|
491
|
-
* responses:
|
|
492
|
-
* 200:
|
|
493
|
-
* description: List of API key records
|
|
494
|
-
* content:
|
|
495
|
-
* application/json:
|
|
496
|
-
* schema:
|
|
497
|
-
* type: array
|
|
498
|
-
* items:
|
|
499
|
-
* type: object
|
|
500
|
-
* properties:
|
|
501
|
-
* id: { type: string }
|
|
502
|
-
* name: { type: string }
|
|
503
|
-
* scopes:
|
|
504
|
-
* type: array
|
|
505
|
-
* items: { type: string, enum: [read, sessions, devices, admin] }
|
|
506
|
-
* rateLimit: { type: integer, nullable: true }
|
|
507
|
-
* teamId: { type: string, nullable: true }
|
|
508
|
-
* createdAt: { type: string, format: date-time }
|
|
509
|
-
* lastUsedAt: { type: string, format: date-time, nullable: true }
|
|
510
|
-
* 401: { $ref: '#/components/responses/Unauthorized' }
|
|
511
|
-
* 403: { $ref: '#/components/responses/Forbidden' }
|
|
512
|
-
* 429: { $ref: '#/components/responses/RateLimited' }
|
|
513
|
-
* post:
|
|
514
|
-
* summary: Mint a new API key
|
|
515
|
-
* description: |
|
|
516
|
-
* Returns the plain-text key **once** in the response. Store it immediately —
|
|
517
|
-
* it cannot be retrieved later. Requires `admin` scope.
|
|
518
|
-
* tags: [Admin]
|
|
519
|
-
* requestBody:
|
|
520
|
-
* required: true
|
|
521
|
-
* content:
|
|
522
|
-
* application/json:
|
|
523
|
-
* schema:
|
|
524
|
-
* type: object
|
|
525
|
-
* required: [name, scopes]
|
|
526
|
-
* properties:
|
|
527
|
-
* name: { type: string, example: 'CI runner' }
|
|
528
|
-
* scopes:
|
|
529
|
-
* type: array
|
|
530
|
-
* minItems: 1
|
|
531
|
-
* items: { type: string, enum: [read, sessions, devices, admin] }
|
|
532
|
-
* rateLimit:
|
|
533
|
-
* type: integer
|
|
534
|
-
* description: 'Per-window quota override; null inherits the plugin default'
|
|
535
|
-
* teamId:
|
|
536
|
-
* type: string
|
|
537
|
-
* nullable: true
|
|
538
|
-
* description: 'Bind this key to a team (so it sees team-owned devices only)'
|
|
539
|
-
* responses:
|
|
540
|
-
* 200:
|
|
541
|
-
* description: Key created
|
|
542
|
-
* content:
|
|
543
|
-
* application/json:
|
|
544
|
-
* schema:
|
|
545
|
-
* type: object
|
|
546
|
-
* properties:
|
|
547
|
-
* id: { type: string }
|
|
548
|
-
* key: { type: string, description: 'Plain-text key — shown once' }
|
|
549
|
-
* 400: { description: 'Missing name or empty scopes array' }
|
|
550
|
-
* 401: { $ref: '#/components/responses/Unauthorized' }
|
|
551
|
-
* 403: { $ref: '#/components/responses/Forbidden' }
|
|
552
|
-
* 429: { $ref: '#/components/responses/RateLimited' }
|
|
553
|
-
*/
|
|
554
|
-
/**
|
|
555
|
-
* @swagger
|
|
556
|
-
* /api/apikeys/{id}:
|
|
557
|
-
* delete:
|
|
558
|
-
* summary: Revoke an API key
|
|
559
|
-
* description: Hard-deletes the key. In-flight requests already authenticated continue until completion; subsequent requests with the revoked key get 401. Requires `admin` scope.
|
|
560
|
-
* tags: [Admin]
|
|
561
|
-
* parameters:
|
|
562
|
-
* - in: path
|
|
563
|
-
* name: id
|
|
564
|
-
* required: true
|
|
565
|
-
* schema: { type: string }
|
|
566
|
-
* responses:
|
|
567
|
-
* 200:
|
|
568
|
-
* description: Key revoked
|
|
569
|
-
* content:
|
|
570
|
-
* application/json:
|
|
571
|
-
* schema: { $ref: '#/components/schemas/Success' }
|
|
572
|
-
* 401: { $ref: '#/components/responses/Unauthorized' }
|
|
573
|
-
* 403: { $ref: '#/components/responses/Forbidden' }
|
|
574
|
-
* 429: { $ref: '#/components/responses/RateLimited' }
|
|
575
|
-
*/
|
|
576
|
-
// =============================================================================
|
|
577
|
-
// Admin — Teams
|
|
578
|
-
// =============================================================================
|
|
579
|
-
/**
|
|
580
|
-
* @swagger
|
|
581
|
-
* /api/teams:
|
|
582
|
-
* get:
|
|
583
|
-
* summary: List all teams
|
|
584
|
-
* description: Requires `admin` scope.
|
|
585
|
-
* tags: [Admin]
|
|
586
|
-
* responses:
|
|
587
|
-
* 200:
|
|
588
|
-
* description: List of teams
|
|
589
|
-
* content:
|
|
590
|
-
* application/json:
|
|
591
|
-
* schema:
|
|
592
|
-
* type: array
|
|
593
|
-
* items:
|
|
594
|
-
* type: object
|
|
595
|
-
* properties:
|
|
596
|
-
* id: { type: string }
|
|
597
|
-
* name: { type: string }
|
|
598
|
-
* createdAt: { type: string, format: date-time }
|
|
599
|
-
* 401: { $ref: '#/components/responses/Unauthorized' }
|
|
600
|
-
* 403: { $ref: '#/components/responses/Forbidden' }
|
|
601
|
-
* 429: { $ref: '#/components/responses/RateLimited' }
|
|
602
|
-
* post:
|
|
603
|
-
* summary: Create a team
|
|
604
|
-
* description: Team names must be unique. Requires `admin` scope.
|
|
605
|
-
* tags: [Admin]
|
|
606
|
-
* requestBody:
|
|
607
|
-
* required: true
|
|
608
|
-
* content:
|
|
609
|
-
* application/json:
|
|
610
|
-
* schema:
|
|
611
|
-
* type: object
|
|
612
|
-
* required: [name]
|
|
613
|
-
* properties:
|
|
614
|
-
* name: { type: string, example: 'Mobile Platform' }
|
|
615
|
-
* responses:
|
|
616
|
-
* 200:
|
|
617
|
-
* description: Team created
|
|
618
|
-
* 400: { description: 'Missing name' }
|
|
619
|
-
* 409: { description: 'Team name already exists' }
|
|
620
|
-
* 401: { $ref: '#/components/responses/Unauthorized' }
|
|
621
|
-
* 403: { $ref: '#/components/responses/Forbidden' }
|
|
622
|
-
* 429: { $ref: '#/components/responses/RateLimited' }
|
|
623
|
-
*/
|
|
624
|
-
/**
|
|
625
|
-
* @swagger
|
|
626
|
-
* /api/teams/{id}:
|
|
627
|
-
* delete:
|
|
628
|
-
* summary: Delete a team
|
|
629
|
-
* description: Cascades to membership rows. Devices owned by the team revert to shared (null) ownership. Requires `admin` scope.
|
|
630
|
-
* tags: [Admin]
|
|
631
|
-
* parameters:
|
|
632
|
-
* - in: path
|
|
633
|
-
* name: id
|
|
634
|
-
* required: true
|
|
635
|
-
* schema: { type: string }
|
|
636
|
-
* responses:
|
|
637
|
-
* 200:
|
|
638
|
-
* description: Team deleted
|
|
639
|
-
* content:
|
|
640
|
-
* application/json:
|
|
641
|
-
* schema: { $ref: '#/components/schemas/Success' }
|
|
642
|
-
* 401: { $ref: '#/components/responses/Unauthorized' }
|
|
643
|
-
* 403: { $ref: '#/components/responses/Forbidden' }
|
|
644
|
-
* 429: { $ref: '#/components/responses/RateLimited' }
|
|
645
|
-
*/
|
|
646
|
-
/**
|
|
647
|
-
* @swagger
|
|
648
|
-
* /api/teams/{id}/members:
|
|
649
|
-
* get:
|
|
650
|
-
* summary: List team members
|
|
651
|
-
* description: Returns the API keys bound to this team. Requires `admin` scope.
|
|
652
|
-
* tags: [Admin]
|
|
653
|
-
* parameters:
|
|
654
|
-
* - in: path
|
|
655
|
-
* name: id
|
|
656
|
-
* required: true
|
|
657
|
-
* schema: { type: string }
|
|
658
|
-
* responses:
|
|
659
|
-
* 200:
|
|
660
|
-
* description: List of members
|
|
661
|
-
* content:
|
|
662
|
-
* application/json:
|
|
663
|
-
* schema:
|
|
664
|
-
* type: array
|
|
665
|
-
* items:
|
|
666
|
-
* type: object
|
|
667
|
-
* properties:
|
|
668
|
-
* apiKeyId: { type: string }
|
|
669
|
-
* role: { type: string, enum: [member, owner] }
|
|
670
|
-
* 401: { $ref: '#/components/responses/Unauthorized' }
|
|
671
|
-
* 429: { $ref: '#/components/responses/RateLimited' }
|
|
672
|
-
* post:
|
|
673
|
-
* summary: Add a member to a team
|
|
674
|
-
* description: Adds an existing API key as a team member. Defaults to `member` role. Requires `admin` scope.
|
|
675
|
-
* tags: [Admin]
|
|
676
|
-
* parameters:
|
|
677
|
-
* - in: path
|
|
678
|
-
* name: id
|
|
679
|
-
* required: true
|
|
680
|
-
* schema: { type: string }
|
|
681
|
-
* requestBody:
|
|
682
|
-
* required: true
|
|
683
|
-
* content:
|
|
684
|
-
* application/json:
|
|
685
|
-
* schema:
|
|
686
|
-
* type: object
|
|
687
|
-
* required: [apiKeyId]
|
|
688
|
-
* properties:
|
|
689
|
-
* apiKeyId: { type: string }
|
|
690
|
-
* role: { type: string, enum: [member, owner], default: member }
|
|
691
|
-
* responses:
|
|
692
|
-
* 200: { description: 'Member added' }
|
|
693
|
-
* 400: { description: 'Missing apiKeyId' }
|
|
694
|
-
* 401: { $ref: '#/components/responses/Unauthorized' }
|
|
695
|
-
* 403: { $ref: '#/components/responses/Forbidden' }
|
|
696
|
-
* 429: { $ref: '#/components/responses/RateLimited' }
|
|
697
|
-
*/
|
|
698
|
-
/**
|
|
699
|
-
* @swagger
|
|
700
|
-
* /api/teams/{id}/members/{apiKeyId}:
|
|
701
|
-
* delete:
|
|
702
|
-
* summary: Remove a member from a team
|
|
703
|
-
* description: Does not revoke the API key itself — only unbinds it from the team. Requires `admin` scope.
|
|
704
|
-
* tags: [Admin]
|
|
705
|
-
* parameters:
|
|
706
|
-
* - in: path
|
|
707
|
-
* name: id
|
|
708
|
-
* required: true
|
|
709
|
-
* schema: { type: string }
|
|
710
|
-
* - in: path
|
|
711
|
-
* name: apiKeyId
|
|
712
|
-
* required: true
|
|
713
|
-
* schema: { type: string }
|
|
714
|
-
* responses:
|
|
715
|
-
* 200:
|
|
716
|
-
* description: Member removed
|
|
717
|
-
* content:
|
|
718
|
-
* application/json:
|
|
719
|
-
* schema: { $ref: '#/components/schemas/Success' }
|
|
720
|
-
* 401: { $ref: '#/components/responses/Unauthorized' }
|
|
721
|
-
* 403: { $ref: '#/components/responses/Forbidden' }
|
|
722
|
-
* 429: { $ref: '#/components/responses/RateLimited' }
|
|
723
|
-
*/
|
|
724
|
-
// =============================================================================
|
|
725
|
-
// Admin — Process snapshot
|
|
726
|
-
// =============================================================================
|
|
727
|
-
/**
|
|
728
|
-
* @swagger
|
|
729
|
-
* /api/processes:
|
|
730
|
-
* get:
|
|
731
|
-
* summary: Snapshot of long-running child processes
|
|
732
|
-
* description: |
|
|
733
|
-
* Lists the long-lived workers Xenon is currently supervising — Appium-driven
|
|
734
|
-
* sessions, ADB log tails, MJPEG stream encoders, etc. Useful when triaging
|
|
735
|
-
* why a host is hot or a device is wedged. Requires `admin` scope.
|
|
736
|
-
* tags: [Admin]
|
|
737
|
-
* responses:
|
|
738
|
-
* 200:
|
|
739
|
-
* description: List of supervised processes
|
|
740
|
-
* content:
|
|
741
|
-
* application/json:
|
|
742
|
-
* schema:
|
|
743
|
-
* type: array
|
|
744
|
-
* items:
|
|
745
|
-
* type: object
|
|
746
|
-
* properties:
|
|
747
|
-
* id: { type: string }
|
|
748
|
-
* sessionId: { type: string, nullable: true }
|
|
749
|
-
* udid: { type: string, nullable: true }
|
|
750
|
-
* kind: { type: string, example: 'mjpeg-stream' }
|
|
751
|
-
* pid: { type: integer }
|
|
752
|
-
* uptimeMs: { type: integer }
|
|
753
|
-
* 401: { $ref: '#/components/responses/Unauthorized' }
|
|
754
|
-
* 403: { $ref: '#/components/responses/Forbidden' }
|
|
755
|
-
* 429: { $ref: '#/components/responses/RateLimited' }
|
|
756
|
-
*/
|
|
757
|
-
// =============================================================================
|
|
758
|
-
// Network Interceptor
|
|
759
|
-
// =============================================================================
|
|
760
|
-
/**
|
|
761
|
-
* @swagger
|
|
762
|
-
* /api/interceptor/sessions/{sessionId}/requests:
|
|
763
|
-
* get:
|
|
764
|
-
* summary: List intercepted HTTP requests for a session
|
|
765
|
-
* description: |
|
|
766
|
-
* Returns the full list of requests captured by the per-session interceptor.
|
|
767
|
-
* If the session is still active, results are read live; otherwise the
|
|
768
|
-
* archived `requests.json` from the session asset directory is replayed.
|
|
769
|
-
* Returns 404 when neither a live interceptor nor an archive exists.
|
|
770
|
-
* tags: [Network Interceptor]
|
|
771
|
-
* parameters:
|
|
772
|
-
* - in: path
|
|
773
|
-
* name: sessionId
|
|
774
|
-
* required: true
|
|
775
|
-
* schema: { type: string }
|
|
776
|
-
* responses:
|
|
777
|
-
* 200:
|
|
778
|
-
* description: Request list
|
|
779
|
-
* content:
|
|
780
|
-
* application/json:
|
|
781
|
-
* schema:
|
|
782
|
-
* type: object
|
|
783
|
-
* properties:
|
|
784
|
-
* requests:
|
|
785
|
-
* type: array
|
|
786
|
-
* items: { type: object, additionalProperties: true }
|
|
787
|
-
* 404: { description: 'Interceptor inactive and no archive on disk' }
|
|
788
|
-
* 401: { $ref: '#/components/responses/Unauthorized' }
|
|
789
|
-
* 429: { $ref: '#/components/responses/RateLimited' }
|
|
790
|
-
*/
|
|
791
|
-
/**
|
|
792
|
-
* @swagger
|
|
793
|
-
* /api/interceptor/sessions/{sessionId}/requests/{requestId}:
|
|
794
|
-
* get:
|
|
795
|
-
* summary: Get a single intercepted request with full body
|
|
796
|
-
* description: |
|
|
797
|
-
* Hydrates the request detail, including any response body that was
|
|
798
|
-
* offloaded to disk (`bodyPath`). Use this from the dashboard's request
|
|
799
|
-
* drawer or for HAR-equivalent single-record export.
|
|
800
|
-
* tags: [Network Interceptor]
|
|
801
|
-
* parameters:
|
|
802
|
-
* - in: path
|
|
803
|
-
* name: sessionId
|
|
804
|
-
* required: true
|
|
805
|
-
* schema: { type: string }
|
|
806
|
-
* - in: path
|
|
807
|
-
* name: requestId
|
|
808
|
-
* required: true
|
|
809
|
-
* schema: { type: string }
|
|
810
|
-
* responses:
|
|
811
|
-
* 200:
|
|
812
|
-
* description: Single request entry
|
|
813
|
-
* content:
|
|
814
|
-
* application/json:
|
|
815
|
-
* schema: { type: object, additionalProperties: true }
|
|
816
|
-
* 404: { description: 'Request not found in live or archived state' }
|
|
817
|
-
* 401: { $ref: '#/components/responses/Unauthorized' }
|
|
818
|
-
* 429: { $ref: '#/components/responses/RateLimited' }
|
|
819
|
-
*/
|
|
820
|
-
/**
|
|
821
|
-
* @swagger
|
|
822
|
-
* /api/interceptor/sessions/{sessionId}/har:
|
|
823
|
-
* get:
|
|
824
|
-
* summary: Export session traffic as a HAR file
|
|
825
|
-
* description: |
|
|
826
|
-
* Returns a HAR 1.2 archive (`application/json`, with `Content-Disposition:
|
|
827
|
-
* attachment`). Falls back to the on-disk archive if the session has ended.
|
|
828
|
-
* tags: [Network Interceptor]
|
|
829
|
-
* parameters:
|
|
830
|
-
* - in: path
|
|
831
|
-
* name: sessionId
|
|
832
|
-
* required: true
|
|
833
|
-
* schema: { type: string }
|
|
834
|
-
* responses:
|
|
835
|
-
* 200:
|
|
836
|
-
* description: HAR file
|
|
837
|
-
* content:
|
|
838
|
-
* application/json:
|
|
839
|
-
* schema: { type: object, additionalProperties: true }
|
|
840
|
-
* 404: { description: 'No live interceptor and no archived HAR' }
|
|
841
|
-
* 401: { $ref: '#/components/responses/Unauthorized' }
|
|
842
|
-
* 429: { $ref: '#/components/responses/RateLimited' }
|
|
843
|
-
*/
|
|
844
|
-
/**
|
|
845
|
-
* @swagger
|
|
846
|
-
* /api/interceptor/sessions/{sessionId}/mocks:
|
|
847
|
-
* get:
|
|
848
|
-
* summary: List mock rules for a session
|
|
849
|
-
* description: Mock rules apply only while the session is live — there is no archive fallback.
|
|
850
|
-
* tags: [Network Interceptor]
|
|
851
|
-
* parameters:
|
|
852
|
-
* - in: path
|
|
853
|
-
* name: sessionId
|
|
854
|
-
* required: true
|
|
855
|
-
* schema: { type: string }
|
|
856
|
-
* responses:
|
|
857
|
-
* 200:
|
|
858
|
-
* description: Mock list
|
|
859
|
-
* content:
|
|
860
|
-
* application/json:
|
|
861
|
-
* schema:
|
|
862
|
-
* type: object
|
|
863
|
-
* properties:
|
|
864
|
-
* mocks:
|
|
865
|
-
* type: array
|
|
866
|
-
* items: { type: object, additionalProperties: true }
|
|
867
|
-
* 404: { description: 'Interceptor not active for this session' }
|
|
868
|
-
* 401: { $ref: '#/components/responses/Unauthorized' }
|
|
869
|
-
* 429: { $ref: '#/components/responses/RateLimited' }
|
|
870
|
-
* post:
|
|
871
|
-
* summary: Add a mock rule
|
|
872
|
-
* description: |
|
|
873
|
-
* Mock rules match by URL pattern and method, then either return a static
|
|
874
|
-
* response, rewrite the upstream response, or delay it. Only valid while
|
|
875
|
-
* the target session is live.
|
|
876
|
-
* tags: [Network Interceptor]
|
|
877
|
-
* parameters:
|
|
878
|
-
* - in: path
|
|
879
|
-
* name: sessionId
|
|
880
|
-
* required: true
|
|
881
|
-
* schema: { type: string }
|
|
882
|
-
* requestBody:
|
|
883
|
-
* required: true
|
|
884
|
-
* content:
|
|
885
|
-
* application/json:
|
|
886
|
-
* schema: { type: object, additionalProperties: true }
|
|
887
|
-
* responses:
|
|
888
|
-
* 201:
|
|
889
|
-
* description: Mock created
|
|
890
|
-
* content:
|
|
891
|
-
* application/json:
|
|
892
|
-
* schema:
|
|
893
|
-
* type: object
|
|
894
|
-
* properties:
|
|
895
|
-
* id: { type: string }
|
|
896
|
-
* 400: { description: 'Invalid mock rule' }
|
|
897
|
-
* 404: { description: 'Interceptor not active' }
|
|
898
|
-
* 401: { $ref: '#/components/responses/Unauthorized' }
|
|
899
|
-
* 403: { $ref: '#/components/responses/Forbidden' }
|
|
900
|
-
* 429: { $ref: '#/components/responses/RateLimited' }
|
|
901
|
-
* delete:
|
|
902
|
-
* summary: Clear all mock rules for a session
|
|
903
|
-
* tags: [Network Interceptor]
|
|
904
|
-
* parameters:
|
|
905
|
-
* - in: path
|
|
906
|
-
* name: sessionId
|
|
907
|
-
* required: true
|
|
908
|
-
* schema: { type: string }
|
|
909
|
-
* responses:
|
|
910
|
-
* 200:
|
|
911
|
-
* description: Mocks cleared
|
|
912
|
-
* content:
|
|
913
|
-
* application/json:
|
|
914
|
-
* schema: { $ref: '#/components/schemas/Success' }
|
|
915
|
-
* 404: { description: 'Interceptor not active' }
|
|
916
|
-
* 401: { $ref: '#/components/responses/Unauthorized' }
|
|
917
|
-
* 403: { $ref: '#/components/responses/Forbidden' }
|
|
918
|
-
* 429: { $ref: '#/components/responses/RateLimited' }
|
|
919
|
-
*/
|
|
920
|
-
/**
|
|
921
|
-
* @swagger
|
|
922
|
-
* /api/interceptor/sessions/{sessionId}/mocks/{mockId}:
|
|
923
|
-
* delete:
|
|
924
|
-
* summary: Remove a single mock rule
|
|
925
|
-
* tags: [Network Interceptor]
|
|
926
|
-
* parameters:
|
|
927
|
-
* - in: path
|
|
928
|
-
* name: sessionId
|
|
929
|
-
* required: true
|
|
930
|
-
* schema: { type: string }
|
|
931
|
-
* - in: path
|
|
932
|
-
* name: mockId
|
|
933
|
-
* required: true
|
|
934
|
-
* schema: { type: string }
|
|
935
|
-
* responses:
|
|
936
|
-
* 200:
|
|
937
|
-
* description: Mock removed
|
|
938
|
-
* content:
|
|
939
|
-
* application/json:
|
|
940
|
-
* schema:
|
|
941
|
-
* type: object
|
|
942
|
-
* properties:
|
|
943
|
-
* removed: { type: boolean }
|
|
944
|
-
* 404: { description: 'Interceptor not active or mock not found' }
|
|
945
|
-
* 401: { $ref: '#/components/responses/Unauthorized' }
|
|
946
|
-
* 403: { $ref: '#/components/responses/Forbidden' }
|
|
947
|
-
* 429: { $ref: '#/components/responses/RateLimited' }
|
|
948
|
-
*/
|
|
949
|
-
// =============================================================================
|
|
950
|
-
// Hub-Node channel (pair auth)
|
|
951
|
-
// =============================================================================
|
|
952
|
-
/**
|
|
953
|
-
* @swagger
|
|
954
|
-
* /api/register:
|
|
955
|
-
* post:
|
|
956
|
-
* summary: Node→Hub device inventory push
|
|
957
|
-
* description: |
|
|
958
|
-
* Hub-node only. Authenticates with the per-node
|
|
959
|
-
* `(x-xenon-access-key, x-xenon-token)` pair the node was provisioned
|
|
960
|
-
* with on the hub. Nodes call this on startup and on a recurring
|
|
961
|
-
* interval (`sendNodeDevicesToHubIntervalMs`, default 30 s) to publish
|
|
962
|
-
* their device list. The `type` query param selects the operation:
|
|
963
|
-
*
|
|
964
|
-
* | type | effect |
|
|
965
|
-
* |---------------|---------------------------------------------------------------|
|
|
966
|
-
* | `add` | Insert / refresh the supplied devices |
|
|
967
|
-
* | `remove` | Drop the supplied devices (e.g. USB unplug) |
|
|
968
|
-
* | `unregister` | Drop every device for the calling host (used on graceful exit) |
|
|
969
|
-
*
|
|
970
|
-
* Not intended for end-user automation. Document here for transparency.
|
|
971
|
-
* tags: [Hub-Node]
|
|
972
|
-
* security:
|
|
973
|
-
* - AccessKeyAuth: []
|
|
974
|
-
* TokenAuth: []
|
|
975
|
-
* parameters:
|
|
976
|
-
* - in: query
|
|
977
|
-
* name: type
|
|
978
|
-
* required: true
|
|
979
|
-
* schema: { type: string, enum: [add, remove, unregister] }
|
|
980
|
-
* - in: query
|
|
981
|
-
* name: host
|
|
982
|
-
* schema: { type: string }
|
|
983
|
-
* description: 'Required for `unregister`'
|
|
984
|
-
* requestBody:
|
|
985
|
-
* content:
|
|
986
|
-
* application/json:
|
|
987
|
-
* schema:
|
|
988
|
-
* oneOf:
|
|
989
|
-
* - type: array
|
|
990
|
-
* items: { $ref: '#/components/schemas/Device' }
|
|
991
|
-
* - type: object
|
|
992
|
-
* responses:
|
|
993
|
-
* 200:
|
|
994
|
-
* description: Inventory accepted
|
|
995
|
-
* content:
|
|
996
|
-
* application/json:
|
|
997
|
-
* schema: { $ref: '#/components/schemas/Success' }
|
|
998
|
-
* 401: { description: 'Missing or invalid (accessKey, token) pair' }
|
|
999
|
-
* 403: { $ref: '#/components/responses/Forbidden' }
|
|
1000
|
-
* 429: { $ref: '#/components/responses/RateLimited' }
|
|
1001
|
-
*/
|
|
1002
|
-
/**
|
|
1003
|
-
* @swagger
|
|
1004
|
-
* /api/unblock:
|
|
1005
|
-
* post:
|
|
1006
|
-
* summary: Node→Hub release of a manually-blocked device
|
|
1007
|
-
* description: |
|
|
1008
|
-
* Hub-node only. Authenticates with the per-node
|
|
1009
|
-
* `(x-xenon-access-key, x-xenon-token)` pair. Nodes call this to
|
|
1010
|
-
* re-enter a device into the pool after a maintenance lock or a
|
|
1011
|
-
* blocked state lifted on the node side. Returns 404 if the
|
|
1012
|
-
* (udid, host) tuple is not in the registry.
|
|
1013
|
-
* tags: [Hub-Node]
|
|
1014
|
-
* security:
|
|
1015
|
-
* - AccessKeyAuth: []
|
|
1016
|
-
* TokenAuth: []
|
|
1017
|
-
* requestBody:
|
|
1018
|
-
* required: true
|
|
1019
|
-
* content:
|
|
1020
|
-
* application/json:
|
|
1021
|
-
* schema:
|
|
1022
|
-
* type: object
|
|
1023
|
-
* required: [udid, host]
|
|
1024
|
-
* properties:
|
|
1025
|
-
* udid: { type: string }
|
|
1026
|
-
* host: { type: string }
|
|
1027
|
-
* responses:
|
|
1028
|
-
* 200:
|
|
1029
|
-
* description: Device unblocked
|
|
1030
|
-
* content:
|
|
1031
|
-
* application/json:
|
|
1032
|
-
* schema: { $ref: '#/components/schemas/Success' }
|
|
1033
|
-
* 404: { description: 'Device not found in registry' }
|
|
1034
|
-
* 401: { description: 'Missing or invalid (accessKey, token) pair' }
|
|
1035
|
-
* 403: { $ref: '#/components/responses/Forbidden' }
|
|
1036
|
-
* 429: { $ref: '#/components/responses/RateLimited' }
|
|
1037
|
-
*/
|
|
1038
|
-
// =============================================================================
|
|
1039
|
-
// Grid — previously-undocumented routes
|
|
1040
|
-
// =============================================================================
|
|
1041
|
-
/**
|
|
1042
|
-
* @swagger
|
|
1043
|
-
* /api/device/tags:
|
|
1044
|
-
* post:
|
|
1045
|
-
* summary: Replace the tag list on a device
|
|
1046
|
-
* description: Tags are user-defined free-form labels (e.g. `flaky`, `lab-row-3`). Replaces — does not append. Requires `devices` scope.
|
|
1047
|
-
* tags: [Devices]
|
|
1048
|
-
* requestBody:
|
|
1049
|
-
* required: true
|
|
1050
|
-
* content:
|
|
1051
|
-
* application/json:
|
|
1052
|
-
* schema:
|
|
1053
|
-
* type: object
|
|
1054
|
-
* required: [udid, host, tags]
|
|
1055
|
-
* properties:
|
|
1056
|
-
* udid: { type: string }
|
|
1057
|
-
* host: { type: string }
|
|
1058
|
-
* tags:
|
|
1059
|
-
* type: array
|
|
1060
|
-
* items: { type: string }
|
|
1061
|
-
* responses:
|
|
1062
|
-
* 200:
|
|
1063
|
-
* description: Tags updated
|
|
1064
|
-
* content:
|
|
1065
|
-
* application/json:
|
|
1066
|
-
* schema: { $ref: '#/components/schemas/Success' }
|
|
1067
|
-
* 400: { description: 'Missing udid, host, or tags array' }
|
|
1068
|
-
* 401: { $ref: '#/components/responses/Unauthorized' }
|
|
1069
|
-
* 403: { $ref: '#/components/responses/Forbidden' }
|
|
1070
|
-
* 429: { $ref: '#/components/responses/RateLimited' }
|
|
1071
|
-
*/
|
|
1072
|
-
/**
|
|
1073
|
-
* @swagger
|
|
1074
|
-
* /api/device/{udid}/team:
|
|
1075
|
-
* put:
|
|
1076
|
-
* summary: Assign (or clear) a device's team ownership
|
|
1077
|
-
* description: |
|
|
1078
|
-
* Pass `teamId: null` to unassign the device (it returns to the shared
|
|
1079
|
-
* pool, visible to all keys). Pass a string id to bind it. The team must
|
|
1080
|
-
* already exist. Requires `admin` scope.
|
|
1081
|
-
* tags: [Devices]
|
|
1082
|
-
* parameters:
|
|
1083
|
-
* - in: path
|
|
1084
|
-
* name: udid
|
|
1085
|
-
* required: true
|
|
1086
|
-
* schema: { type: string }
|
|
1087
|
-
* requestBody:
|
|
1088
|
-
* required: true
|
|
1089
|
-
* content:
|
|
1090
|
-
* application/json:
|
|
1091
|
-
* schema:
|
|
1092
|
-
* type: object
|
|
1093
|
-
* required: [teamId]
|
|
1094
|
-
* properties:
|
|
1095
|
-
* teamId: { type: string, nullable: true }
|
|
1096
|
-
* responses:
|
|
1097
|
-
* 200:
|
|
1098
|
-
* description: Team ownership updated
|
|
1099
|
-
* content:
|
|
1100
|
-
* application/json:
|
|
1101
|
-
* schema:
|
|
1102
|
-
* type: object
|
|
1103
|
-
* properties:
|
|
1104
|
-
* ok: { type: boolean }
|
|
1105
|
-
* updated: { type: integer }
|
|
1106
|
-
* 404: { description: 'Team or device not found' }
|
|
1107
|
-
* 401: { $ref: '#/components/responses/Unauthorized' }
|
|
1108
|
-
* 403: { $ref: '#/components/responses/Forbidden' }
|
|
1109
|
-
* 429: { $ref: '#/components/responses/RateLimited' }
|
|
1110
|
-
*/
|
|
1111
|
-
/**
|
|
1112
|
-
* @swagger
|
|
1113
|
-
* /api/queue/status/{capability_id}:
|
|
1114
|
-
* get:
|
|
1115
|
-
* summary: Status of a single queued session request
|
|
1116
|
-
* description: |
|
|
1117
|
-
* Returns the queue position and ETA for a pending session keyed by the
|
|
1118
|
-
* capability id you submitted. 404 if the request has already been
|
|
1119
|
-
* allocated or expired, or is one the caller can't see in
|
|
1120
|
-
* `GET /api/queue`: the same answer as for an unknown id.
|
|
1121
|
-
* tags: [Grid]
|
|
1122
|
-
* parameters:
|
|
1123
|
-
* - in: path
|
|
1124
|
-
* name: capability_id
|
|
1125
|
-
* required: true
|
|
1126
|
-
* schema: { type: string }
|
|
1127
|
-
* responses:
|
|
1128
|
-
* 200:
|
|
1129
|
-
* description: Queue status
|
|
1130
|
-
* content:
|
|
1131
|
-
* application/json:
|
|
1132
|
-
* schema: { type: object, additionalProperties: true }
|
|
1133
|
-
* 404: { description: 'Pending session not found' }
|
|
1134
|
-
* 401: { $ref: '#/components/responses/Unauthorized' }
|
|
1135
|
-
* 429: { $ref: '#/components/responses/RateLimited' }
|
|
1136
|
-
*/
|
|
1137
|
-
/**
|
|
1138
|
-
* @swagger
|
|
1139
|
-
* /api/node/{host}/status:
|
|
1140
|
-
* get:
|
|
1141
|
-
* summary: ADB / device status for a specific node
|
|
1142
|
-
* description: |
|
|
1143
|
-
* If `host` matches the node serving the request, the response is built
|
|
1144
|
-
* locally. Otherwise the hub proxies the call to that node's
|
|
1145
|
-
* `/api/node/status` endpoint and returns the result. Returns 404 if no
|
|
1146
|
-
* devices are registered for the requested host.
|
|
1147
|
-
* tags: [Grid]
|
|
1148
|
-
* parameters:
|
|
1149
|
-
* - in: path
|
|
1150
|
-
* name: host
|
|
1151
|
-
* required: true
|
|
1152
|
-
* schema: { type: string }
|
|
1153
|
-
* description: 'Node host (URL or hostname as registered with the hub)'
|
|
1154
|
-
* responses:
|
|
1155
|
-
* 200:
|
|
1156
|
-
* description: Per-device status
|
|
1157
|
-
* content:
|
|
1158
|
-
* application/json:
|
|
1159
|
-
* schema:
|
|
1160
|
-
* type: array
|
|
1161
|
-
* items:
|
|
1162
|
-
* type: object
|
|
1163
|
-
* properties:
|
|
1164
|
-
* udid: { type: string }
|
|
1165
|
-
* host: { type: string }
|
|
1166
|
-
* state: { type: string }
|
|
1167
|
-
* platform: { type: string, enum: [ios, android] }
|
|
1168
|
-
* 404: { description: 'No devices registered for that host' }
|
|
1169
|
-
* 401: { $ref: '#/components/responses/Unauthorized' }
|
|
1170
|
-
* 429: { $ref: '#/components/responses/RateLimited' }
|
|
1171
|
-
*/
|
|
1172
|
-
// =============================================================================
|
|
1173
|
-
// Selector Health — previously-undocumented routes
|
|
1174
|
-
// =============================================================================
|
|
1175
|
-
/**
|
|
1176
|
-
* @swagger
|
|
1177
|
-
* /api/healing/events:
|
|
1178
|
-
* get:
|
|
1179
|
-
* summary: Recent healing events
|
|
1180
|
-
* description: |
|
|
1181
|
-
* Returns the most recent self-healing events along with a `todayCount`
|
|
1182
|
-
* used by the dashboard "today" badge. Newest first; clamped to 1–200.
|
|
1183
|
-
* tags: [Selector Health]
|
|
1184
|
-
* parameters:
|
|
1185
|
-
* - in: query
|
|
1186
|
-
* name: limit
|
|
1187
|
-
* schema: { type: integer, minimum: 1, maximum: 200, default: 50 }
|
|
1188
|
-
* responses:
|
|
1189
|
-
* 200:
|
|
1190
|
-
* description: Recent events
|
|
1191
|
-
* content:
|
|
1192
|
-
* application/json:
|
|
1193
|
-
* schema:
|
|
1194
|
-
* type: object
|
|
1195
|
-
* properties:
|
|
1196
|
-
* events:
|
|
1197
|
-
* type: array
|
|
1198
|
-
* items:
|
|
1199
|
-
* type: object
|
|
1200
|
-
* properties:
|
|
1201
|
-
* id: { type: string }
|
|
1202
|
-
* sessionId: { type: string }
|
|
1203
|
-
* deviceUdid: { type: string }
|
|
1204
|
-
* deviceName: { type: string }
|
|
1205
|
-
* devicePlatform: { type: string, enum: [ios, android] }
|
|
1206
|
-
* commandName: { type: string }
|
|
1207
|
-
* originalSelector: { type: string }
|
|
1208
|
-
* healedSelector: { type: string }
|
|
1209
|
-
* confidence: { type: number }
|
|
1210
|
-
* tier: { type: string }
|
|
1211
|
-
* isSuccess: { type: boolean }
|
|
1212
|
-
* createdAt: { type: string, format: date-time }
|
|
1213
|
-
* todayCount: { type: integer }
|
|
1214
|
-
* 401: { $ref: '#/components/responses/Unauthorized' }
|
|
1215
|
-
* 429: { $ref: '#/components/responses/RateLimited' }
|
|
1216
|
-
*/
|
|
1217
|
-
/**
|
|
1218
|
-
* @swagger
|
|
1219
|
-
* /api/healing/summary:
|
|
1220
|
-
* get:
|
|
1221
|
-
* summary: Selector-health KPI summary with prior-period comparison
|
|
1222
|
-
* description: |
|
|
1223
|
-
* Aggregates over `windowDays` (clamped 1–365, default 30) and emits a
|
|
1224
|
-
* parallel "prior" object covering the equivalent immediately-preceding
|
|
1225
|
-
* window — use the diff for week-over-week / month-over-month deltas.
|
|
1226
|
-
* tags: [Selector Health]
|
|
1227
|
-
* parameters:
|
|
1228
|
-
* - in: query
|
|
1229
|
-
* name: windowDays
|
|
1230
|
-
* schema: { type: integer, minimum: 1, maximum: 365, default: 30 }
|
|
1231
|
-
* - in: query
|
|
1232
|
-
* name: tz
|
|
1233
|
-
* schema: { type: integer, minimum: -840, maximum: 840, default: 0 }
|
|
1234
|
-
* description: "The caller's offset from UTC in minutes (east positive), so `trend` days are theirs"
|
|
1235
|
-
* responses:
|
|
1236
|
-
* 200:
|
|
1237
|
-
* description: KPI summary
|
|
1238
|
-
* content:
|
|
1239
|
-
* application/json:
|
|
1240
|
-
* schema:
|
|
1241
|
-
* type: object
|
|
1242
|
-
* properties:
|
|
1243
|
-
* windowDays: { type: integer }
|
|
1244
|
-
* current:
|
|
1245
|
-
* type: object
|
|
1246
|
-
* properties:
|
|
1247
|
-
* totalHeals: { type: integer }
|
|
1248
|
-
* distinctSelectors: { type: integer }
|
|
1249
|
-
* sessionsTouched: { type: integer }
|
|
1250
|
-
* byTier: { type: object, additionalProperties: { type: integer } }
|
|
1251
|
-
* timeSpentMs: { type: integer, description: 'Total duration of the commands that needed healing' }
|
|
1252
|
-
* prior:
|
|
1253
|
-
* type: object
|
|
1254
|
-
* description: 'Same shape as `current` for the immediately preceding window'
|
|
1255
|
-
* resolvedCount: { type: integer }
|
|
1256
|
-
* pendingCount: { type: integer }
|
|
1257
|
-
* trend:
|
|
1258
|
-
* type: array
|
|
1259
|
-
* description: 'Heals per day of the period, in the caller''s time zone, days with none included'
|
|
1260
|
-
* items:
|
|
1261
|
-
* type: object
|
|
1262
|
-
* properties:
|
|
1263
|
-
* t: { type: integer, description: 'When the day began, epoch ms' }
|
|
1264
|
-
* heals: { type: integer }
|
|
1265
|
-
* aiHeals: { type: integer, description: 'Heals by Visual AI or an LLM' }
|
|
1266
|
-
* 401: { $ref: '#/components/responses/Unauthorized' }
|
|
1267
|
-
* 429: { $ref: '#/components/responses/RateLimited' }
|
|
1268
|
-
*/
|
|
1269
|
-
/**
|
|
1270
|
-
* @swagger
|
|
1271
|
-
* /api/healing/selectors:
|
|
1272
|
-
* get:
|
|
1273
|
-
* summary: One tab of the Selector Health list
|
|
1274
|
-
* description: |
|
|
1275
|
-
* `fix` groups the period's heals by selector and leaves out selectors
|
|
1276
|
-
* being verified, fixed or muted; the other tabs list the selectors with
|
|
1277
|
-
* that status (`fixed`: verified within the period). Only selectors
|
|
1278
|
-
* healed, at any time, in sessions the caller can see. `counts` follow
|
|
1279
|
-
* the period, not the search or filters. A page past the end answers the
|
|
1280
|
-
* last page.
|
|
1281
|
-
* tags: [Selector Health]
|
|
1282
|
-
* parameters:
|
|
1283
|
-
* - { in: query, name: tab, schema: { type: string, enum: [fix, verifying, fixed, muted], default: fix } }
|
|
1284
|
-
* - { in: query, name: days, schema: { type: integer, minimum: 1, maximum: 365, default: 30 } }
|
|
1285
|
-
* - { in: query, name: q, schema: { type: string }, description: 'Text in the selector, or (fix) in its suggested fix' }
|
|
1286
|
-
* - { in: query, name: platform, schema: { type: string }, description: 'fix only' }
|
|
1287
|
-
* - { in: query, name: method, schema: { type: string }, description: 'Healing method, e.g. LLM; fix only' }
|
|
1288
|
-
* - { in: query, name: sort, schema: { type: string, enum: [heals, recent, time], default: heals }, description: 'fix only' }
|
|
1289
|
-
* - { in: query, name: page, schema: { type: integer, minimum: 1, default: 1 } }
|
|
1290
|
-
* - { in: query, name: pageSize, schema: { type: integer, minimum: 1, maximum: 100, default: 50 } }
|
|
1291
|
-
* responses:
|
|
1292
|
-
* 200: { description: '`{ tab, days, page, pageSize, total, counts, canAct, items }`' }
|
|
1293
|
-
* 401: { $ref: '#/components/responses/Unauthorized' }
|
|
1294
|
-
*/
|
|
1295
|
-
/**
|
|
1296
|
-
* @swagger
|
|
1297
|
-
* /api/healing/selectors/detail:
|
|
1298
|
-
* get:
|
|
1299
|
-
* summary: Everything the Selector Health side panel shows for one selector
|
|
1300
|
-
* description: |
|
|
1301
|
-
* Its heals in the period (suggested fixes with their share, methods and
|
|
1302
|
-
* confidence; heals per day; platforms, builds and devices; the latest 20
|
|
1303
|
-
* heals), its status, the latest 50 status changes with who made them,
|
|
1304
|
-
* and whether the caller may act. A selector no session the caller can
|
|
1305
|
-
* see has healed answers 404, exactly as an unknown one.
|
|
1306
|
-
* tags: [Selector Health]
|
|
1307
|
-
* parameters:
|
|
1308
|
-
* - { in: query, name: selector, required: true, schema: { type: string } }
|
|
1309
|
-
* - { in: query, name: strategy, schema: { type: string }, description: "Empty for heals recorded with no strategy" }
|
|
1310
|
-
* - { in: query, name: days, schema: { type: integer, minimum: 1, maximum: 365, default: 30 } }
|
|
1311
|
-
* - { in: query, name: tz, schema: { type: integer, minimum: -840, maximum: 840, default: 0 } }
|
|
1312
|
-
* responses:
|
|
1313
|
-
* 200: { description: 'The panel for one selector' }
|
|
1314
|
-
* 400: { description: 'No selector given' }
|
|
1315
|
-
* 404: { description: '`{ error: "not_found", message: "Selector not found" }`' }
|
|
1316
|
-
* 401: { $ref: '#/components/responses/Unauthorized' }
|
|
1317
|
-
*/
|
|
1318
|
-
/**
|
|
1319
|
-
* @swagger
|
|
1320
|
-
* /api/healing/digest/send:
|
|
1321
|
-
* post:
|
|
1322
|
-
* summary: On-demand selector-health digest webhook
|
|
1323
|
-
* description: |
|
|
1324
|
-
* Manually fires the digest that the scheduler normally sends on a fixed
|
|
1325
|
-
* cadence. Useful for pre-release "is the selector landscape healthy
|
|
1326
|
-
* enough to ship?" gates. Requires `admin` scope.
|
|
1327
|
-
* tags: [Selector Health]
|
|
1328
|
-
* requestBody:
|
|
1329
|
-
* content:
|
|
1330
|
-
* application/json:
|
|
1331
|
-
* schema:
|
|
1332
|
-
* type: object
|
|
1333
|
-
* properties:
|
|
1334
|
-
* windowDays: { type: integer }
|
|
1335
|
-
* limit: { type: integer }
|
|
1336
|
-
* minHealCount: { type: integer }
|
|
1337
|
-
* responses:
|
|
1338
|
-
* 200:
|
|
1339
|
-
* description: Digest dispatched
|
|
1340
|
-
* content:
|
|
1341
|
-
* application/json:
|
|
1342
|
-
* schema:
|
|
1343
|
-
* type: object
|
|
1344
|
-
* properties:
|
|
1345
|
-
* sent: { type: integer, description: 'Number of webhook subscribers fired' }
|
|
1346
|
-
* windowDays: { type: integer }
|
|
1347
|
-
* hotspotsIncluded: { type: integer }
|
|
1348
|
-
* 401: { $ref: '#/components/responses/Unauthorized' }
|
|
1349
|
-
* 403: { $ref: '#/components/responses/Forbidden' }
|
|
1350
|
-
* 429: { $ref: '#/components/responses/RateLimited' }
|
|
1351
|
-
*/
|
|
1352
|
-
// =============================================================================
|
|
1353
|
-
// Control — previously-undocumented routes
|
|
1354
|
-
// =============================================================================
|
|
1355
|
-
/**
|
|
1356
|
-
* @swagger
|
|
1357
|
-
* /api/control/{udid}/logs:
|
|
1358
|
-
* get:
|
|
1359
|
-
* summary: Pull recent device logs
|
|
1360
|
-
* description: |
|
|
1361
|
-
* Android: `adb logcat -d` style snapshot. iOS: recent syslog buffer.
|
|
1362
|
-
* Returns plain text wrapped in a JSON `logs` field.
|
|
1363
|
-
* tags: [Control]
|
|
1364
|
-
* parameters:
|
|
1365
|
-
* - in: path
|
|
1366
|
-
* name: udid
|
|
1367
|
-
* required: true
|
|
1368
|
-
* schema: { type: string }
|
|
1369
|
-
* responses:
|
|
1370
|
-
* 200:
|
|
1371
|
-
* description: Device logs
|
|
1372
|
-
* content:
|
|
1373
|
-
* application/json:
|
|
1374
|
-
* schema:
|
|
1375
|
-
* type: object
|
|
1376
|
-
* properties:
|
|
1377
|
-
* logs: { type: string }
|
|
1378
|
-
* 404: { description: 'Device not found' }
|
|
1379
|
-
* 401: { $ref: '#/components/responses/Unauthorized' }
|
|
1380
|
-
* 429: { $ref: '#/components/responses/RateLimited' }
|
|
1381
|
-
*/
|
|
1382
|
-
// =============================================================================
|
|
1383
|
-
// Bug Report — one-click bundling of session artifacts
|
|
1384
|
-
// =============================================================================
|
|
1385
|
-
/**
|
|
1386
|
-
* @swagger
|
|
1387
|
-
* /api/sessions/{sessionId}/bug-report:
|
|
1388
|
-
* post:
|
|
1389
|
-
* summary: Generate a bug-report bundle for a session
|
|
1390
|
-
* description: |
|
|
1391
|
-
* Streams a zip archive containing the session's video, logs, HAR, AI
|
|
1392
|
-
* summary, and a manifest.json. In `slice` mode the video is trimmed to
|
|
1393
|
-
* the last `windowSec` seconds (default 60). In `full` mode the entire
|
|
1394
|
-
* session is included verbatim.
|
|
1395
|
-
* tags: [Sessions]
|
|
1396
|
-
* parameters:
|
|
1397
|
-
* - in: path
|
|
1398
|
-
* name: sessionId
|
|
1399
|
-
* required: true
|
|
1400
|
-
* schema: { type: string }
|
|
1401
|
-
* - in: query
|
|
1402
|
-
* name: mode
|
|
1403
|
-
* required: true
|
|
1404
|
-
* schema: { type: string, enum: [slice, full] }
|
|
1405
|
-
* - in: query
|
|
1406
|
-
* name: windowSec
|
|
1407
|
-
* required: false
|
|
1408
|
-
* schema: { type: integer, minimum: 5, maximum: 600, default: 60 }
|
|
1409
|
-
* description: Slice window length in seconds. Ignored when mode=full.
|
|
1410
|
-
* responses:
|
|
1411
|
-
* 200:
|
|
1412
|
-
* description: zip archive
|
|
1413
|
-
* content:
|
|
1414
|
-
* application/zip:
|
|
1415
|
-
* schema: { type: string, format: binary }
|
|
1416
|
-
* 400: { $ref: '#/components/responses/BadRequest' }
|
|
1417
|
-
* 401: { $ref: '#/components/responses/Unauthorized' }
|
|
1418
|
-
* 403: { $ref: '#/components/responses/Forbidden' }
|
|
1419
|
-
* 404: { $ref: '#/components/responses/NotFound' }
|
|
1420
|
-
* 429: { $ref: '#/components/responses/RateLimited' }
|
|
1421
|
-
*/
|
|
1422
|
-
/**
|
|
1423
|
-
* @swagger
|
|
1424
|
-
* /api/recordings:
|
|
1425
|
-
* post:
|
|
1426
|
-
* summary: Start a multi-device recording group
|
|
1427
|
-
* description: Start free-form recording on one or more devices. Atomic — if any UDID is busy or the concurrency cap is exceeded, nothing is started.
|
|
1428
|
-
* tags: [Recordings]
|
|
1429
|
-
* requestBody:
|
|
1430
|
-
* required: true
|
|
1431
|
-
* content:
|
|
1432
|
-
* application/json:
|
|
1433
|
-
* schema:
|
|
1434
|
-
* type: object
|
|
1435
|
-
* required: [udids]
|
|
1436
|
-
* properties:
|
|
1437
|
-
* udids:
|
|
1438
|
-
* type: array
|
|
1439
|
-
* items: { type: string }
|
|
1440
|
-
* description: Device UDIDs to record
|
|
1441
|
-
* sessionId:
|
|
1442
|
-
* type: string
|
|
1443
|
-
* description: Optional session ID to associate
|
|
1444
|
-
* note:
|
|
1445
|
-
* type: string
|
|
1446
|
-
* description: Optional label for the recording group
|
|
1447
|
-
* responses:
|
|
1448
|
-
* 202:
|
|
1449
|
-
* description: Recording started
|
|
1450
|
-
* content:
|
|
1451
|
-
* application/json:
|
|
1452
|
-
* schema:
|
|
1453
|
-
* type: object
|
|
1454
|
-
* properties:
|
|
1455
|
-
* groupId: { type: string }
|
|
1456
|
-
* recordings:
|
|
1457
|
-
* type: array
|
|
1458
|
-
* items:
|
|
1459
|
-
* type: object
|
|
1460
|
-
* properties:
|
|
1461
|
-
* id: { type: string }
|
|
1462
|
-
* udid: { type: string }
|
|
1463
|
-
* status: { type: string }
|
|
1464
|
-
* startedAt: { type: string, format: date-time }
|
|
1465
|
-
* 409:
|
|
1466
|
-
* description: Device busy or concurrency cap reached
|
|
1467
|
-
* content:
|
|
1468
|
-
* application/json:
|
|
1469
|
-
* schema:
|
|
1470
|
-
* type: object
|
|
1471
|
-
* properties:
|
|
1472
|
-
* error: { type: string, enum: [device_busy, concurrency_cap] }
|
|
1473
|
-
* busyDevices:
|
|
1474
|
-
* type: array
|
|
1475
|
-
* items:
|
|
1476
|
-
* type: object
|
|
1477
|
-
* properties:
|
|
1478
|
-
* udid: { type: string }
|
|
1479
|
-
* reason: { type: string }
|
|
1480
|
-
* sessionId: { type: string }
|
|
1481
|
-
* blockId: { type: string }
|
|
1482
|
-
* limit: { type: integer }
|
|
1483
|
-
* active: { type: integer }
|
|
1484
|
-
* message: { type: string }
|
|
1485
|
-
* 401: { $ref: '#/components/responses/Unauthorized' }
|
|
1486
|
-
* 429: { $ref: '#/components/responses/RateLimited' }
|
|
1487
|
-
*/
|
|
1488
|
-
/**
|
|
1489
|
-
* @swagger
|
|
1490
|
-
* /api/recordings/{groupId}/add-device:
|
|
1491
|
-
* post:
|
|
1492
|
-
* summary: Add a device to a running recording group
|
|
1493
|
-
* description: Add a single device to an existing, active recording group. Atomic — if the device is busy or the cap is exceeded, nothing is started.
|
|
1494
|
-
* tags: [Recordings]
|
|
1495
|
-
* parameters:
|
|
1496
|
-
* - in: path
|
|
1497
|
-
* name: groupId
|
|
1498
|
-
* required: true
|
|
1499
|
-
* schema: { type: string }
|
|
1500
|
-
* description: Recording group ID
|
|
1501
|
-
* requestBody:
|
|
1502
|
-
* required: true
|
|
1503
|
-
* content:
|
|
1504
|
-
* application/json:
|
|
1505
|
-
* schema:
|
|
1506
|
-
* type: object
|
|
1507
|
-
* required: [udid]
|
|
1508
|
-
* properties:
|
|
1509
|
-
* udid:
|
|
1510
|
-
* type: string
|
|
1511
|
-
* description: Device UDID to add
|
|
1512
|
-
* responses:
|
|
1513
|
-
* 201:
|
|
1514
|
-
* description: Device added to recording group
|
|
1515
|
-
* content:
|
|
1516
|
-
* application/json:
|
|
1517
|
-
* schema:
|
|
1518
|
-
* type: object
|
|
1519
|
-
* properties:
|
|
1520
|
-
* recording:
|
|
1521
|
-
* type: object
|
|
1522
|
-
* properties:
|
|
1523
|
-
* id: { type: string }
|
|
1524
|
-
* udid: { type: string }
|
|
1525
|
-
* status: { type: string }
|
|
1526
|
-
* 409:
|
|
1527
|
-
* description: Device busy or concurrency cap reached
|
|
1528
|
-
* 401: { $ref: '#/components/responses/Unauthorized' }
|
|
1529
|
-
* 429: { $ref: '#/components/responses/RateLimited' }
|
|
1530
|
-
*/
|
|
1531
|
-
/**
|
|
1532
|
-
* @swagger
|
|
1533
|
-
* /api/recordings/{groupId}/stop:
|
|
1534
|
-
* post:
|
|
1535
|
-
* summary: Stop all recordings in a group
|
|
1536
|
-
* description: Stop all active recordings in the specified group. Each recording is finalized independently.
|
|
1537
|
-
* tags: [Recordings]
|
|
1538
|
-
* parameters:
|
|
1539
|
-
* - in: path
|
|
1540
|
-
* name: groupId
|
|
1541
|
-
* required: true
|
|
1542
|
-
* schema: { type: string }
|
|
1543
|
-
* responses:
|
|
1544
|
-
* 200:
|
|
1545
|
-
* description: Recordings stopped
|
|
1546
|
-
* content:
|
|
1547
|
-
* application/json:
|
|
1548
|
-
* schema:
|
|
1549
|
-
* type: object
|
|
1550
|
-
* properties:
|
|
1551
|
-
* groupId: { type: string }
|
|
1552
|
-
* recordings:
|
|
1553
|
-
* type: array
|
|
1554
|
-
* items:
|
|
1555
|
-
* type: object
|
|
1556
|
-
* properties:
|
|
1557
|
-
* id: { type: string }
|
|
1558
|
-
* udid: { type: string }
|
|
1559
|
-
* status: { type: string }
|
|
1560
|
-
* durationMs: { type: integer }
|
|
1561
|
-
* sizeBytes: { type: integer }
|
|
1562
|
-
* 401: { $ref: '#/components/responses/Unauthorized' }
|
|
1563
|
-
* 429: { $ref: '#/components/responses/RateLimited' }
|
|
1564
|
-
*/
|
|
1565
|
-
/**
|
|
1566
|
-
* @swagger
|
|
1567
|
-
* /api/recordings/{groupId}/bookmark:
|
|
1568
|
-
* post:
|
|
1569
|
-
* summary: Add a bookmark to a recording
|
|
1570
|
-
* description: Drop a time-stamped bookmark on a recording, optionally with a note.
|
|
1571
|
-
* tags: [Recordings]
|
|
1572
|
-
* parameters:
|
|
1573
|
-
* - in: path
|
|
1574
|
-
* name: groupId
|
|
1575
|
-
* required: true
|
|
1576
|
-
* schema: { type: string }
|
|
1577
|
-
* requestBody:
|
|
1578
|
-
* required: true
|
|
1579
|
-
* content:
|
|
1580
|
-
* application/json:
|
|
1581
|
-
* schema:
|
|
1582
|
-
* type: object
|
|
1583
|
-
* required: [recordingId, timecodeMs, label]
|
|
1584
|
-
* properties:
|
|
1585
|
-
* recordingId: { type: string }
|
|
1586
|
-
* timecodeMs: { type: integer }
|
|
1587
|
-
* label: { type: string }
|
|
1588
|
-
* note: { type: string }
|
|
1589
|
-
* responses:
|
|
1590
|
-
* 201:
|
|
1591
|
-
* description: Bookmark created
|
|
1592
|
-
* 401: { $ref: '#/components/responses/Unauthorized' }
|
|
1593
|
-
* 429: { $ref: '#/components/responses/RateLimited' }
|
|
1594
|
-
*/
|
|
1595
|
-
/**
|
|
1596
|
-
* @swagger
|
|
1597
|
-
* /api/recordings/{groupId}/annotation:
|
|
1598
|
-
* post:
|
|
1599
|
-
* summary: Add an annotation to a recording
|
|
1600
|
-
* description: Persist a normalized annotation (shape, geometry, color) on a recording frame.
|
|
1601
|
-
* tags: [Recordings]
|
|
1602
|
-
* parameters:
|
|
1603
|
-
* - in: path
|
|
1604
|
-
* name: groupId
|
|
1605
|
-
* required: true
|
|
1606
|
-
* schema: { type: string }
|
|
1607
|
-
* requestBody:
|
|
1608
|
-
* required: true
|
|
1609
|
-
* content:
|
|
1610
|
-
* application/json:
|
|
1611
|
-
* schema:
|
|
1612
|
-
* type: object
|
|
1613
|
-
* required: [recordingId, timecodeMs, shape, geometry, color]
|
|
1614
|
-
* properties:
|
|
1615
|
-
* recordingId: { type: string }
|
|
1616
|
-
* timecodeMs: { type: integer }
|
|
1617
|
-
* shape: { type: string, enum: [RECT, CIRCLE, ARROW, TEXT, FREEHAND] }
|
|
1618
|
-
* geometry: { type: string, description: JSON string with normalized coordinates }
|
|
1619
|
-
* color: { type: string }
|
|
1620
|
-
* text: { type: string }
|
|
1621
|
-
* author: { type: string }
|
|
1622
|
-
* responses:
|
|
1623
|
-
* 201:
|
|
1624
|
-
* description: Annotation created
|
|
1625
|
-
* 401: { $ref: '#/components/responses/Unauthorized' }
|
|
1626
|
-
* 429: { $ref: '#/components/responses/RateLimited' }
|
|
1627
|
-
*/
|
|
1628
|
-
/**
|
|
1629
|
-
* @swagger
|
|
1630
|
-
* /api/recordings/{groupId}:
|
|
1631
|
-
* get:
|
|
1632
|
-
* summary: Get recording group details
|
|
1633
|
-
* description: Retrieve all recordings, bookmarks, and annotations for a recording group.
|
|
1634
|
-
* tags: [Recordings]
|
|
1635
|
-
* parameters:
|
|
1636
|
-
* - in: path
|
|
1637
|
-
* name: groupId
|
|
1638
|
-
* required: true
|
|
1639
|
-
* schema: { type: string }
|
|
1640
|
-
* responses:
|
|
1641
|
-
* 200:
|
|
1642
|
-
* description: Recording group details
|
|
1643
|
-
* content:
|
|
1644
|
-
* application/json:
|
|
1645
|
-
* schema:
|
|
1646
|
-
* type: object
|
|
1647
|
-
* properties:
|
|
1648
|
-
* groupId: { type: string }
|
|
1649
|
-
* recordings: { type: array }
|
|
1650
|
-
* 401: { $ref: '#/components/responses/Unauthorized' }
|
|
1651
|
-
* 429: { $ref: '#/components/responses/RateLimited' }
|
|
1652
|
-
*/
|
|
1653
|
-
/**
|
|
1654
|
-
* @swagger
|
|
1655
|
-
* /api/recordings/{groupId}/bundle.zip:
|
|
1656
|
-
* get:
|
|
1657
|
-
* summary: Download proof bundle
|
|
1658
|
-
* description: Download a self-contained zip containing videos, bookmarks, annotations, and device metadata.
|
|
1659
|
-
* tags: [Recordings]
|
|
1660
|
-
* parameters:
|
|
1661
|
-
* - in: path
|
|
1662
|
-
* name: groupId
|
|
1663
|
-
* required: true
|
|
1664
|
-
* schema: { type: string }
|
|
1665
|
-
* responses:
|
|
1666
|
-
* 200:
|
|
1667
|
-
* description: Zip archive stream
|
|
1668
|
-
* content:
|
|
1669
|
-
* application/zip:
|
|
1670
|
-
* schema: { type: string, format: binary }
|
|
1671
|
-
* 401: { $ref: '#/components/responses/Unauthorized' }
|
|
1672
|
-
* 429: { $ref: '#/components/responses/RateLimited' }
|
|
1673
|
-
*/
|
|
1674
|
-
/**
|
|
1675
|
-
* @swagger
|
|
1676
|
-
* /api/recordings/{groupId}/exports/annotated.mp4:
|
|
1677
|
-
* get:
|
|
1678
|
-
* summary: Download annotated MP4
|
|
1679
|
-
* description: Generate and stream an MP4 with annotations burned into the video pixels. Lazy — only rendered on demand.
|
|
1680
|
-
* tags: [Recordings]
|
|
1681
|
-
* parameters:
|
|
1682
|
-
* - in: path
|
|
1683
|
-
* name: groupId
|
|
1684
|
-
* required: true
|
|
1685
|
-
* schema: { type: string }
|
|
1686
|
-
* - in: query
|
|
1687
|
-
* name: recordingId
|
|
1688
|
-
* required: true
|
|
1689
|
-
* schema: { type: string }
|
|
1690
|
-
* description: The specific recording to render
|
|
1691
|
-
* responses:
|
|
1692
|
-
* 200:
|
|
1693
|
-
* description: Annotated MP4 stream
|
|
1694
|
-
* content:
|
|
1695
|
-
* video/mp4:
|
|
1696
|
-
* schema: { type: string, format: binary }
|
|
1697
|
-
* 400:
|
|
1698
|
-
* description: Missing recordingId query param
|
|
1699
|
-
* 401: { $ref: '#/components/responses/Unauthorized' }
|
|
1700
|
-
* 429: { $ref: '#/components/responses/RateLimited' }
|
|
1701
|
-
*/
|