@xenon-device-management/xenon 1.7.3 → 1.7.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/lib/package.json +1 -1
  2. package/lib/public/assets/{ApiKeyGate-BousCIQZ.js → ApiKeyGate-BVaB1Eg_.js} +1 -1
  3. package/lib/public/assets/{BugReportButton-Gw4lnGnV.js → BugReportButton-CioDV7QP.js} +1 -1
  4. package/lib/public/assets/DeviceMosaicView-BtRxZTXc.js +2 -0
  5. package/lib/public/assets/{EmptyState-CPOfuV3O.js → EmptyState-UcJej2vi.js} +1 -1
  6. package/lib/public/assets/{FieldGroup-C5UkCHbe.js → FieldGroup-CYlyr34Q.js} +1 -1
  7. package/lib/public/assets/{Modal-MMGhL8eW.js → Modal-CjmzIR4Y.js} +1 -1
  8. package/lib/public/assets/{SegmentedControl-CZfAicws.js → SegmentedControl-wRDvkYAQ.js} +1 -1
  9. package/lib/public/assets/{SettingCard-Dp7XIdcl.js → SettingCard-BzjYlAYS.js} +1 -1
  10. package/lib/public/assets/{Table-DsN_IX6J.js → Table-ta1UYEeC.js} +1 -1
  11. package/lib/public/assets/{activity-DCBAvxuo.js → activity-jltyhcrF.js} +1 -1
  12. package/lib/public/assets/{ai-settings-yNS_bU1m.js → ai-settings-Dfjxb1SZ.js} +1 -1
  13. package/lib/public/assets/{api-keys-CQ9g1g1b.js → api-keys-cFOese97.js} +1 -1
  14. package/lib/public/assets/{apps-Bqh1eav9.js → apps-sHtZU8T_.js} +1 -1
  15. package/lib/public/assets/{arrow-left-GX0vzYtn.js → arrow-left-DGZzN6Sl.js} +1 -1
  16. package/lib/public/assets/{arrow-up-right-DePHHOby.js → arrow-up-right-DtZUe0YH.js} +1 -1
  17. package/lib/public/assets/{auth-shell-BBGlC6i3.js → auth-shell-D0eF5eyS.js} +1 -1
  18. package/lib/public/assets/{builds-page-Czf7RsmD.js → builds-page-DxDwaq_t.js} +1 -1
  19. package/lib/public/assets/{button-ybNY3GYx.js → button-3RkAu6xE.js} +1 -1
  20. package/lib/public/assets/{check-BZMx8AhR.js → check-D6DDcFwM.js} +1 -1
  21. package/lib/public/assets/{chevron-right-WL_bcB02.js → chevron-right-BtA7DR8A.js} +1 -1
  22. package/lib/public/assets/{circle-check-CI2xUlDo.js → circle-check-Bjgvpq5F.js} +1 -1
  23. package/lib/public/assets/{circle-x-De3thbcr.js → circle-x-_TOawobA.js} +1 -1
  24. package/lib/public/assets/{clock-BFVLE7wW.js → clock-pm4LDp97.js} +1 -1
  25. package/lib/public/assets/{copy-ve3lNhva.js → copy-x-UuUQkc.js} +1 -1
  26. package/lib/public/assets/{device-explorer-vzZxT4-Y.js → device-explorer-BDih8ZO9.js} +1 -1
  27. package/lib/public/assets/{download-BDZj44ke.js → download-CuDjBGgs.js} +1 -1
  28. package/lib/public/assets/{forgot-password-BSYQX2oR.js → forgot-password-lgHuL3gW.js} +1 -1
  29. package/lib/public/assets/{funnel-CptlpFZ9.js → funnel-DQuWVevA.js} +1 -1
  30. package/lib/public/assets/{index-PtnKslHU.js → index--TKsSVZp.js} +2 -2
  31. package/lib/public/assets/{layers-DRpwoKhI.js → layers-CdE4q4fN.js} +1 -1
  32. package/lib/public/assets/{lock-CrQhC34I.js → lock-D2Vg6I6G.js} +1 -1
  33. package/lib/public/assets/{login-DaJOt8Qz.js → login-C2dtnYcH.js} +1 -1
  34. package/lib/public/assets/{maintenance-settings-D629C2IC.js → maintenance-settings-Ck0suBTt.js} +1 -1
  35. package/lib/public/assets/{mouse-pointer-2-Dpcc76zN.js → mouse-pointer-2-JuM7rDhC.js} +1 -1
  36. package/lib/public/assets/{overview-CGEKYX62.js → overview-BBzmQBEl.js} +1 -1
  37. package/lib/public/assets/{page-header-CSvuGbSO.js → page-header-D5tpZwkI.js} +1 -1
  38. package/lib/public/assets/{plus-vJJF9g79.js → plus-tcA3_CAo.js} +1 -1
  39. package/lib/public/assets/{profile-page-CF7ZQNQz.js → profile-page-BuuLTEtS.js} +1 -1
  40. package/lib/public/assets/{reset-password-BgpjbCac.js → reset-password-ERq0WAyA.js} +1 -1
  41. package/lib/public/assets/{runbook-page-Dxuzftyv.js → runbook-page-BubL06xm.js} +1 -1
  42. package/lib/public/assets/{selector-detail-page-B_puCd9g.js → selector-detail-page-DgL7bNdy.js} +1 -1
  43. package/lib/public/assets/{selector-health-BjMHf106.js → selector-health-B-V7vomq.js} +1 -1
  44. package/lib/public/assets/{selector-health-page-Du_bGD2p.js → selector-health-page-DWy7bTdU.js} +1 -1
  45. package/lib/public/assets/{session-detail-page-lQTe5_B1.js → session-detail-page-COgS5oAy.js} +1 -1
  46. package/lib/public/assets/{settings-DhxWGUzy.js → settings-D0fQ4JNR.js} +1 -1
  47. package/lib/public/assets/{shield-JGzamJo5.js → shield-DINNxWHp.js} +1 -1
  48. package/lib/public/assets/{shield-alert-DzE36jcb.js → shield-alert-DDDbE5ER.js} +1 -1
  49. package/lib/public/assets/{status-pill-outline-50aN0wRU.js → status-pill-outline-CFz5xaoF.js} +1 -1
  50. package/lib/public/assets/{tablet-DHY88mxw.js → tablet-DoE7LIzI.js} +1 -1
  51. package/lib/public/assets/{teams-EOAIPbZC.js → teams-DpW-IUPC.js} +1 -1
  52. package/lib/public/assets/{trash-2-BrTSfFh5.js → trash-2-Dvr08Dpq.js} +1 -1
  53. package/lib/public/assets/useSocket-COssbQIj.js +1 -0
  54. package/lib/public/assets/{users-Dd44jLAB.js → users-DKmLh2WK.js} +1 -1
  55. package/lib/public/assets/{webhook-settings-DBYmqim_.js → webhook-settings-nwtLvp2Q.js} +1 -1
  56. package/lib/public/assets/{zap-JUbOoK3o.js → zap-CM2cWetx.js} +1 -1
  57. package/lib/public/index.html +1 -1
  58. package/lib/src/device-managers/IOSDeviceManager.js +24 -3
  59. package/lib/src/device-managers/ios/IOSStreamService.js +94 -8
  60. package/lib/src/device-managers/ios/iosBattery.js +24 -0
  61. package/lib/src/device-managers/ios/iosStreamDiagnostics.js +81 -0
  62. package/lib/src/services/PortAllocator.js +14 -0
  63. package/lib/src/services/VideoPipelineService.js +51 -43
  64. package/lib/test/unit/PortAllocator.test.js +21 -0
  65. package/lib/test/unit/iosBattery.spec.js +29 -0
  66. package/lib/test/unit/iosStreamDiagnostics.spec.js +82 -0
  67. package/lib/test/unit/video-pipeline-args.spec.js +46 -0
  68. package/lib/tsconfig.tsbuildinfo +1 -1
  69. package/package.json +1 -1
  70. package/lib/public/assets/DeviceMosaicView-BX2hfhBf.js +0 -2
  71. package/lib/public/assets/useSocket-BRto9a5m.js +0 -1
@@ -56,6 +56,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
56
56
  Object.defineProperty(exports, "__esModule", { value: true });
57
57
  exports.VideoPipelineService = void 0;
58
58
  exports.resolveOutputPath = resolveOutputPath;
59
+ exports.buildRecordArgs = buildRecordArgs;
59
60
  const typedi_1 = require("typedi");
60
61
  const ProcessRegistry_1 = require("./ProcessRegistry");
61
62
  const child_process_1 = require("child_process");
@@ -73,6 +74,48 @@ const ResourceIsolationService_1 = require("./ResourceIsolationService");
73
74
  function resolveOutputPath(sessionId, override) {
74
75
  return (override !== null && override !== void 0 ? override : path_1.default.join(config_1.config.sessionAssetsPath, sessionId, 'video', `${sessionId}.mp4`));
75
76
  }
77
+ /**
78
+ * Build the ffmpeg argv for a single-device MJPEG → mp4 recording.
79
+ *
80
+ * Extracted as a pure function so it can be unit-tested without spawning ffmpeg.
81
+ *
82
+ * Timing correctness: an HTTP MJPEG stream carries no real timestamps, so the
83
+ * mjpeg demuxer assumes a fixed 25 fps and stamps frames 1/25 s apart. The
84
+ * recorded duration then becomes (frames received)/25, which drifts from real
85
+ * time whenever the source's actual rate differs from 25 fps — a ~12.5 fps
86
+ * stream records at 2× speed, a >25 fps stream at slow motion. Stamping each
87
+ * frame with its wall-clock arrival time (`-use_wallclock_as_timestamps`) and
88
+ * keeping variable frame timing on output (`-vsync vfr`) makes the mp4 duration
89
+ * track real elapsed time regardless of the source frame rate.
90
+ */
91
+ function buildRecordArgs(opts) {
92
+ const args = [
93
+ '-y',
94
+ '-loglevel', 'error',
95
+ '-reconnect', '1',
96
+ '-reconnect_at_eof', '1',
97
+ '-reconnect_streamed', '1',
98
+ '-reconnect_delay_max', '5',
99
+ '-probesize', '32',
100
+ '-analyzeduration', '0',
101
+ // Stamp frames with wall-clock arrival time (must precede -i).
102
+ '-use_wallclock_as_timestamps', '1',
103
+ '-f', 'mjpeg',
104
+ '-i', opts.mjpegUrl,
105
+ '-pix_fmt', 'yuv420p',
106
+ ];
107
+ if (opts.isMac) {
108
+ args.push('-c:v', 'h264_videotoolbox', '-realtime', '1', '-q:v', '50');
109
+ }
110
+ else {
111
+ args.push('-c:v', 'libx264', '-preset', 'veryfast', '-crf', '25');
112
+ }
113
+ // Preserve the (variable) wall-clock timing rather than forcing constant fps.
114
+ args.push('-vsync', 'vfr');
115
+ args.push('-movflags', 'frag_keyframe+empty_moov+default_base_moof');
116
+ args.push(opts.outputPath);
117
+ return args;
118
+ }
76
119
  /**
77
120
  * Intelligent Video Pipeline Service
78
121
  *
@@ -121,47 +164,8 @@ let VideoPipelineService = class VideoPipelineService {
121
164
  logger_1.default.info(`[VideoPipeline] Starting HW-accelerated recording for ${sessionId} from ${mjpegUrl}`);
122
165
  // Small settlement delay to allow the source stream to prime
123
166
  yield new Promise((r) => setTimeout(r, 500));
124
- // 2. Construct FFMPEG Args
125
- // -f mjpeg: Input format
126
- // -i: Input source
127
- // -c:v: Hardware accelerated encoder based on platform
128
- // -movflags: fMP4 for instant playback and crash resiliency
129
- const args = [
130
- '-y',
131
- '-loglevel',
132
- 'error', // Only log errors to keep console clean
133
- '-reconnect',
134
- '1',
135
- '-reconnect_at_eof',
136
- '1',
137
- '-reconnect_streamed',
138
- '1',
139
- '-reconnect_delay_max',
140
- '5',
141
- '-probesize',
142
- '32', // Fast startup for MJPEG
143
- '-analyzeduration',
144
- '0',
145
- '-f',
146
- 'mjpeg',
147
- '-i',
148
- mjpegUrl,
149
- '-pix_fmt',
150
- 'yuv420p',
151
- ];
152
- if (this.isMac) {
153
- args.push('-c:v', 'h264_videotoolbox');
154
- args.push('-realtime', '1'); // VideoToolbox optimization
155
- args.push('-q:v', '50'); // High quality/efficiency balance
156
- }
157
- else {
158
- args.push('-c:v', 'libx264');
159
- args.push('-preset', 'veryfast');
160
- args.push('-crf', '25');
161
- }
162
- // Instant Playback Flags (fMP4)
163
- args.push('-movflags', 'frag_keyframe+empty_moov+default_base_moof');
164
- args.push(outputPath);
167
+ // 2. Construct FFMPEG Args (see buildRecordArgs for the timing rationale).
168
+ const args = buildRecordArgs({ mjpegUrl, outputPath, isMac: this.isMac });
165
169
  // 3. Wrap with Resource Isolation (Economy)
166
170
  const isolationService = typedi_1.Container.get(ResourceIsolationService_1.ResourceIsolationService);
167
171
  const { command, args: wrappedArgs } = isolationService.wrapSpawn('ffmpeg', args, 'Economy');
@@ -248,9 +252,11 @@ let VideoPipelineService = class VideoPipelineService {
248
252
  '-loglevel',
249
253
  'error',
250
254
  ];
251
- // Per-input args: mjpeg + reconnect.
255
+ // Per-input args: mjpeg + reconnect. `-use_wallclock_as_timestamps` stamps
256
+ // each input's frames with real arrival time so the composite duration
257
+ // tracks wall-clock (see buildRecordArgs for the full rationale).
252
258
  for (const inp of inputs) {
253
- args.push('-reconnect', '1', '-reconnect_at_eof', '1', '-reconnect_streamed', '1', '-reconnect_delay_max', '5', '-probesize', '32', '-analyzeduration', '0', '-f', 'mjpeg', '-i', `http://127.0.0.1:${inp.mjpegPort}`);
259
+ args.push('-reconnect', '1', '-reconnect_at_eof', '1', '-reconnect_streamed', '1', '-reconnect_delay_max', '5', '-probesize', '32', '-analyzeduration', '0', '-use_wallclock_as_timestamps', '1', '-f', 'mjpeg', '-i', `http://127.0.0.1:${inp.mjpegPort}`);
254
260
  }
255
261
  // Build the filtergraph: scale+pad each input to a uniform cell, then stack.
256
262
  const filterParts = [];
@@ -267,6 +273,8 @@ let VideoPipelineService = class VideoPipelineService {
267
273
  else {
268
274
  args.push('-c:v', 'libx264', '-preset', 'veryfast', '-crf', '25');
269
275
  }
276
+ // Preserve wall-clock frame timing rather than forcing constant fps.
277
+ args.push('-vsync', 'vfr');
270
278
  args.push('-movflags', 'frag_keyframe+empty_moov+default_base_moof');
271
279
  args.push(outputPath);
272
280
  logger_1.default.info(`[VideoPipeline] Composite [${groupId}] starting: ${inputs.length} inputs, layout=${layout}, ${cellW}×${cellH}/cell → ${outputPath}`);
@@ -87,6 +87,27 @@ describe('PortAllocator', () => {
87
87
  const call = deleteManyStub.getCalls().find((c) => { var _a, _b; return ((_b = (_a = c.args[0]) === null || _a === void 0 ? void 0 : _a.where) === null || _b === void 0 ? void 0 : _b.leasedToUdid) === 'udid-1'; });
88
88
  (0, chai_1.expect)(call, 'expected deleteMany with leasedToUdid filter').to.exist;
89
89
  }));
90
+ it('never hands out a port already leased to another device (the 9100 collision)', () => __awaiter(void 0, void 0, void 0, function* () {
91
+ // Regression: an iOS device must not be allocated port 9100 while the
92
+ // Android stream holds a live lease on it. The lease table's `taken` set
93
+ // must exclude it, so allocation moves to the next free port.
94
+ findManyStub.resolves([{ port: 9100 }]); // 9100 leased to someone else
95
+ createStub.callsFake(({ data }) => Promise.resolve({ port: data.port }));
96
+ const allocator = makeAllocator({ mjpeg: [9100, 9101] });
97
+ allocator.isOsFree = () => __awaiter(void 0, void 0, void 0, function* () { return true; });
98
+ const port = yield allocator.acquire('mjpeg', 'ios-udid');
99
+ (0, chai_1.expect)(port).to.equal(9101);
100
+ // It must never have attempted to create a lease on the taken port.
101
+ const attempted9100 = createStub.getCalls().some((c) => { var _a, _b; return ((_b = (_a = c.args[0]) === null || _a === void 0 ? void 0 : _a.data) === null || _b === void 0 ? void 0 : _b.port) === 9100; });
102
+ (0, chai_1.expect)(attempted9100, 'must not try to lease the already-taken 9100').to.be.false;
103
+ }));
104
+ it('touch extends the lease expiry on a specific port', () => __awaiter(void 0, void 0, void 0, function* () {
105
+ const allocator = makeAllocator({ mjpeg: [9100, 9101] });
106
+ yield allocator.touch(9101, 90 * 60 * 1000);
107
+ (0, chai_1.expect)(updateStub.calledOnce).to.be.true;
108
+ (0, chai_1.expect)(updateStub.firstCall.args[0].where.port).to.equal(9101);
109
+ (0, chai_1.expect)(updateStub.firstCall.args[0].data.expiresAt).to.be.a('number');
110
+ }));
90
111
  it('skips ports that fail OS-level availability check', () => __awaiter(void 0, void 0, void 0, function* () {
91
112
  let attempt = 0;
92
113
  createStub.callsFake(({ data }) => {
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const chai_1 = require("chai");
4
+ const iosBattery_1 = require("../../src/device-managers/ios/iosBattery");
5
+ describe('parseBatteryCapacity', () => {
6
+ it('reads BatteryCurrentCapacity from ideviceinfo battery-domain output', () => {
7
+ const out = [
8
+ 'BatteryCurrentCapacity: 87',
9
+ 'BatteryIsCharging: false',
10
+ 'ExternalConnected: true',
11
+ 'FullyCharged: false',
12
+ 'HasBattery: true',
13
+ ].join('\n');
14
+ (0, chai_1.expect)((0, iosBattery_1.parseBatteryCapacity)(out)).to.equal(87);
15
+ });
16
+ it('handles a full charge', () => {
17
+ (0, chai_1.expect)((0, iosBattery_1.parseBatteryCapacity)('BatteryCurrentCapacity: 100\nFullyCharged: true')).to.equal(100);
18
+ });
19
+ it('clamps out-of-range values into 0–100', () => {
20
+ (0, chai_1.expect)((0, iosBattery_1.parseBatteryCapacity)('BatteryCurrentCapacity: 250')).to.equal(100);
21
+ });
22
+ it('returns undefined when the field is absent', () => {
23
+ (0, chai_1.expect)((0, iosBattery_1.parseBatteryCapacity)('HasBattery: true\nExternalConnected: false')).to.equal(undefined);
24
+ });
25
+ it('is safe on empty / undefined input', () => {
26
+ (0, chai_1.expect)((0, iosBattery_1.parseBatteryCapacity)('')).to.equal(undefined);
27
+ (0, chai_1.expect)((0, iosBattery_1.parseBatteryCapacity)(undefined)).to.equal(undefined);
28
+ });
29
+ });
@@ -0,0 +1,82 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const chai_1 = require("chai");
4
+ const iosStreamDiagnostics_1 = require("../../src/device-managers/ios/iosStreamDiagnostics");
5
+ describe('iosStreamDiagnostics', () => {
6
+ describe('isMissingWdaError', () => {
7
+ it('detects the go-ios "Did not find test app" WDA crash', () => {
8
+ const log = `setup test config: Did not find test app for ` +
9
+ `'com.qasecret.WebDriverAgentRunner.xctrunner' on device. Is it installed?`;
10
+ (0, chai_1.expect)((0, iosStreamDiagnostics_1.isMissingWdaError)(log)).to.equal(true);
11
+ });
12
+ it('detects a "WebDriverAgentRunner ... not installed" phrasing', () => {
13
+ (0, chai_1.expect)((0, iosStreamDiagnostics_1.isMissingWdaError)('WebDriverAgentRunner app is not installed on the device')).to.equal(true);
14
+ });
15
+ it('does not flag unrelated WDA crash logs', () => {
16
+ (0, chai_1.expect)((0, iosStreamDiagnostics_1.isMissingWdaError)('WDA crashed: signal 11 (SIGSEGV)')).to.equal(false);
17
+ });
18
+ it('is safe on empty input', () => {
19
+ (0, chai_1.expect)((0, iosStreamDiagnostics_1.isMissingWdaError)('')).to.equal(false);
20
+ });
21
+ it('produces an actionable message naming the udid', () => {
22
+ const msg = (0, iosStreamDiagnostics_1.missingWdaMessage)('c008789a75');
23
+ (0, chai_1.expect)(msg).to.contain('c008789a75');
24
+ (0, chai_1.expect)(msg).to.contain('WebDriverAgent');
25
+ (0, chai_1.expect)(msg.toLowerCase()).to.contain('install');
26
+ });
27
+ });
28
+ describe('classifyTunnelStderr', () => {
29
+ it('extracts the real target udid from the go-ios JSON payload', () => {
30
+ const line = JSON.stringify({
31
+ error: 'manualPairingTunnelStart: unsupported iOS version 15.8.8',
32
+ level: 'warning',
33
+ msg: 'failed to start tunnel',
34
+ udid: 'c008789a75aac6b5cfb1c014e306beeb5d45b85d',
35
+ });
36
+ (0, chai_1.expect)((0, iosStreamDiagnostics_1.classifyTunnelStderr)(line)).to.deep.equal({
37
+ unsupported: true,
38
+ udid: 'c008789a75aac6b5cfb1c014e306beeb5d45b85d',
39
+ });
40
+ });
41
+ it('recovers the udid via regex when the chunk is not clean JSON', () => {
42
+ const noisy = 'time=... level=warning "error":"unsupported iOS version 15.8.8" "udid":"abc123" trailing';
43
+ (0, chai_1.expect)((0, iosStreamDiagnostics_1.classifyTunnelStderr)(noisy)).to.deep.equal({ unsupported: true, udid: 'abc123' });
44
+ });
45
+ it('reports unsupported with a null udid when none can be parsed', () => {
46
+ (0, chai_1.expect)((0, iosStreamDiagnostics_1.classifyTunnelStderr)('go-ios: unsupported iOS version 15.8.8')).to.deep.equal({
47
+ unsupported: true,
48
+ udid: null,
49
+ });
50
+ });
51
+ it('classifies ordinary tunnel stderr as not-unsupported', () => {
52
+ (0, chai_1.expect)((0, iosStreamDiagnostics_1.classifyTunnelStderr)('tunnel established on port 60105')).to.deep.equal({
53
+ unsupported: false,
54
+ udid: null,
55
+ });
56
+ });
57
+ it('is safe on empty input', () => {
58
+ (0, chai_1.expect)((0, iosStreamDiagnostics_1.classifyTunnelStderr)('')).to.deep.equal({ unsupported: false, udid: null });
59
+ });
60
+ });
61
+ describe('isOwnStreamProcess', () => {
62
+ const udid = '00008110-00084CE80E51401E';
63
+ it('claims an iproxy forward for this udid', () => {
64
+ (0, chai_1.expect)((0, iosStreamDiagnostics_1.isOwnStreamProcess)(`iproxy -u ${udid} 9101:9100`, udid)).to.equal(true);
65
+ });
66
+ it('claims a go-ios/WDA process for this udid', () => {
67
+ (0, chai_1.expect)((0, iosStreamDiagnostics_1.isOwnStreamProcess)(`/root/.cache/xenon/goIOS/ios runwda --udid ${udid}`, udid)).to.equal(true);
68
+ });
69
+ it('does NOT claim a neighbour process bound to the same port under a different udid', () => {
70
+ // The Android stream leased port 9100; an iOS device defaulting to 9100
71
+ // must not kill it during cleanup.
72
+ (0, chai_1.expect)((0, iosStreamDiagnostics_1.isOwnStreamProcess)('iproxy -u 381103b720057ece 9100:9100', udid)).to.equal(false);
73
+ });
74
+ it('does NOT claim a process with no udid in its command', () => {
75
+ (0, chai_1.expect)((0, iosStreamDiagnostics_1.isOwnStreamProcess)('node /opt/app/android-stream-capture.js', udid)).to.equal(false);
76
+ });
77
+ it('is safe on empty command or udid', () => {
78
+ (0, chai_1.expect)((0, iosStreamDiagnostics_1.isOwnStreamProcess)('', udid)).to.equal(false);
79
+ (0, chai_1.expect)((0, iosStreamDiagnostics_1.isOwnStreamProcess)(`iproxy -u ${udid}`, '')).to.equal(false);
80
+ });
81
+ });
82
+ });
@@ -0,0 +1,46 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ require("reflect-metadata");
4
+ const chai_1 = require("chai");
5
+ const VideoPipelineService_1 = require("../../src/services/VideoPipelineService");
6
+ /**
7
+ * Guards the F7 fix: the single-device recorder must stamp MJPEG frames with
8
+ * wall-clock arrival time so the mp4 duration tracks real elapsed time, rather
9
+ * than assuming a fixed 25fps (verified live: a 15s capture produced 7.48s
10
+ * before this flag and 14.84s after).
11
+ */
12
+ describe('VideoPipelineService.buildRecordArgs — F7 wall-clock timing', () => {
13
+ const base = { mjpegUrl: 'http://127.0.0.1:9101', outputPath: '/tmp/rec/out.mp4' };
14
+ it('stamps frames with wall-clock time, positioned before the input', () => {
15
+ const args = (0, VideoPipelineService_1.buildRecordArgs)(Object.assign(Object.assign({}, base), { isMac: true }));
16
+ const wallIdx = args.indexOf('-use_wallclock_as_timestamps');
17
+ const inputIdx = args.indexOf('-i');
18
+ (0, chai_1.expect)(wallIdx).to.be.greaterThan(-1);
19
+ (0, chai_1.expect)(args[wallIdx + 1]).to.equal('1');
20
+ // Must precede -i to apply to that input.
21
+ (0, chai_1.expect)(wallIdx).to.be.lessThan(inputIdx);
22
+ });
23
+ it('keeps variable frame timing on output (-vsync vfr)', () => {
24
+ const args = (0, VideoPipelineService_1.buildRecordArgs)(Object.assign(Object.assign({}, base), { isMac: true }));
25
+ const vsyncIdx = args.indexOf('-vsync');
26
+ (0, chai_1.expect)(vsyncIdx).to.be.greaterThan(-1);
27
+ (0, chai_1.expect)(args[vsyncIdx + 1]).to.equal('vfr');
28
+ });
29
+ it('feeds the mjpeg source and writes the output path last', () => {
30
+ const args = (0, VideoPipelineService_1.buildRecordArgs)(Object.assign(Object.assign({}, base), { isMac: false }));
31
+ (0, chai_1.expect)(args.indexOf('-f')).to.be.greaterThan(-1);
32
+ (0, chai_1.expect)(args[args.indexOf('-f') + 1]).to.equal('mjpeg');
33
+ (0, chai_1.expect)(args[args.indexOf('-i') + 1]).to.equal(base.mjpegUrl);
34
+ (0, chai_1.expect)(args[args.length - 1]).to.equal(base.outputPath);
35
+ });
36
+ it('uses VideoToolbox on mac and libx264 elsewhere', () => {
37
+ (0, chai_1.expect)((0, VideoPipelineService_1.buildRecordArgs)(Object.assign(Object.assign({}, base), { isMac: true }))).to.include('h264_videotoolbox');
38
+ (0, chai_1.expect)((0, VideoPipelineService_1.buildRecordArgs)(Object.assign(Object.assign({}, base), { isMac: false }))).to.include('libx264');
39
+ });
40
+ it('still emits fragmented-mp4 flags for instant playback / crash resiliency', () => {
41
+ const args = (0, VideoPipelineService_1.buildRecordArgs)(Object.assign(Object.assign({}, base), { isMac: true }));
42
+ const mv = args.indexOf('-movflags');
43
+ (0, chai_1.expect)(mv).to.be.greaterThan(-1);
44
+ (0, chai_1.expect)(args[mv + 1]).to.equal('frag_keyframe+empty_moov+default_base_moof');
45
+ });
46
+ });
@@ -1 +1 @@
1
- {"root":["../src/InternalHttpClient.ts","../src/PluginContext.ts","../src/XenonCapabilityManager.ts","../src/chromeUtils.ts","../src/config.ts","../src/device-utils.ts","../src/index.ts","../src/logger.ts","../src/plugin.ts","../src/prisma.ts","../src/app/index.ts","../src/app/swagger-docs.ts","../src/app/swagger.ts","../src/app/routers/apikeys.ts","../src/app/routers/apps.ts","../src/app/routers/auth.ts","../src/app/routers/bug-report.ts","../src/app/routers/build-export.ts","../src/app/routers/capabilities.ts","../src/app/routers/config.ts","../src/app/routers/control.ts","../src/app/routers/dashboard.ts","../src/app/routers/grid.ts","../src/app/routers/interceptor.ts","../src/app/routers/ports.ts","../src/app/routers/processes.ts","../src/app/routers/profile.ts","../src/app/routers/projects.ts","../src/app/routers/recordings.ts","../src/app/routers/reservation.ts","../src/app/routers/sdk-leases.ts","../src/app/routers/sdk-version.ts","../src/app/routers/teams.ts","../src/app/routers/users.ts","../src/app/routers/webhook.ts","../src/commands/handle.ts","../src/commands/index.ts","../src/dashboard/asset-manager.ts","../src/dashboard/commands.ts","../src/dashboard/event-manager.ts","../src/dashboard/services/app-service.ts","../src/dashboard/services/failure-analysis-service.ts","../src/dashboard/services/session-service.ts","../src/data-service/CircuitBreaker.ts","../src/data-service/config-service.ts","../src/data-service/db.ts","../src/data-service/device-service.ts","../src/data-service/device-store.interface.ts","../src/data-service/device-store.ts","../src/data-service/pending-sessions-service.ts","../src/data-service/pluginArgs.ts","../src/data-service/prisma-service.ts","../src/data-service/prisma-store.ts","../src/data-service/queue-service.ts","../src/data-service/web-config-service.ts","../src/device-managers/AndroidDeviceManager.ts","../src/device-managers/ChromeDriverManager.ts","../src/device-managers/HealthMonitorService.ts","../src/device-managers/IOSDeviceManager.ts","../src/device-managers/NodeDevices.ts","../src/device-managers/iOSTracker.ts","../src/device-managers/index.ts","../src/device-managers/android/AndroidStreamService.ts","../src/device-managers/android/DeviceLockManager.ts","../src/device-managers/cloud/CapabilityManager.ts","../src/device-managers/cloud/Devices.ts","../src/device-managers/ios/IOSDiscoveryService.ts","../src/device-managers/ios/IOSStreamService.ts","../src/device-managers/ios/WDAClient.ts","../src/enums/Capabilities.ts","../src/enums/Cloud.ts","../src/enums/Platform.ts","../src/enums/SessionType.ts","../src/enums/SocketEvents.ts","../src/generated/client/default.d.ts","../src/generated/client/default.js","../src/generated/client/edge.d.ts","../src/generated/client/edge.js","../src/generated/client/index-browser.js","../src/generated/client/index.d.ts","../src/generated/client/index.js","../src/generated/client/wasm.d.ts","../src/generated/client/wasm.js","../src/generated/client/runtime/edge-esm.js","../src/generated/client/runtime/edge.js","../src/generated/client/runtime/index-browser.d.ts","../src/generated/client/runtime/index-browser.js","../src/generated/client/runtime/library.d.ts","../src/generated/client/runtime/library.js","../src/generated/client/runtime/react-native.js","../src/generated/client/runtime/wasm.js","../src/helpers/UniversalMjpegProxy.ts","../src/helpers/index.ts","../src/interceptors/CommandInterceptor.ts","../src/interfaces/ICloudManager.ts","../src/interfaces/IDevice.ts","../src/interfaces/IDeviceFilterOptions.ts","../src/interfaces/IDeviceManager.ts","../src/interfaces/IOptions.ts","../src/interfaces/IPluginArgs.ts","../src/interfaces/ISessionCapability.ts","../src/logging/sessionContext.ts","../src/middleware/authMiddleware.ts","../src/middleware/csrfMiddleware.ts","../src/middleware/leaseTokenMiddleware.ts","../src/middleware/loginRateLimiter.ts","../src/middleware/rateLimitMiddleware.ts","../src/middleware/roleGuard.ts","../src/middleware/scopeGuard.ts","../src/profiling/AndroidAppProfiler.ts","../src/proxy/wd-command-proxy.ts","../src/scripts/generate-database-migration.ts","../src/scripts/initialize-database.ts","../src/scripts/install-go-ios.ts","../src/scripts/prepare-prisma.ts","../src/scripts/run-migrations.ts","../src/services/AICommandService.ts","../src/services/AIService.ts","../src/services/ApiKeyService.ts","../src/services/CircuitBreaker.ts","../src/services/CleanupService.ts","../src/services/DeviceReconciler.ts","../src/services/EmailService.ts","../src/services/EventBus.ts","../src/services/EventLogService.ts","../src/services/InspectorService.ts","../src/services/InterceptorService.ts","../src/services/MetricsService.ts","../src/services/NetworkConditioningService.ts","../src/services/NotificationService.ts","../src/services/OrphanSweeper.ts","../src/services/PasswordResetService.ts","../src/services/PortAllocator.ts","../src/services/ProcessMetricsService.ts","../src/services/ProcessRegistry.ts","../src/services/RequestLogService.ts","../src/services/ResourceIsolationService.ts","../src/services/SecurityService.ts","../src/services/SelectorStateService.ts","../src/services/SelectorVerificationJob.ts","../src/services/ServerManager.ts","../src/services/SessionHeartbeatService.ts","../src/services/SessionLifecycleService.ts","../src/services/ShutdownCoordinator.ts","../src/services/SocketClient.ts","../src/services/SocketServer.ts","../src/services/TeamService.ts","../src/services/TracingService.ts","../src/services/UserService.ts","../src/services/UserSessionService.ts","../src/services/VideoPipelineService.ts","../src/services/artifacts/ArtifactStore.ts","../src/services/autowait/AutowaitService.ts","../src/services/autowait/waitFor.ts","../src/services/bug-report/BugReportService.ts","../src/services/bug-report/archive.ts","../src/services/bug-report/har-collector.ts","../src/services/bug-report/manifest.ts","../src/services/bug-report/readme.ts","../src/services/bug-report/types.ts","../src/services/bug-report/video-slice.ts","../src/services/bug-report/window.ts","../src/services/healing/FuzzyXmlHealingProvider.ts","../src/services/healing/HealEtalonService.ts","../src/services/healing/HealedLocatorGenerator.ts","../src/services/healing/HealingMetrics.ts","../src/services/healing/HealingOrchestrator.ts","../src/services/healing/LlmHealingProvider.ts","../src/services/healing/OcrHealingProvider.ts","../src/services/healing/ResilioTreeHealingProvider.ts","../src/services/healing/VisualAiHealingProvider.ts","../src/services/healing/types.ts","../src/services/identity/bootstrap.ts","../src/services/identity/sessionCleanupCron.ts","../src/services/identity/userAuthorization.ts","../src/services/interceptor/AndroidProxyAdapter.ts","../src/services/interceptor/CertManager.ts","../src/services/interceptor/HarBuilder.ts","../src/services/interceptor/HostFilter.ts","../src/services/interceptor/MitmProxyHost.ts","../src/services/interceptor/MockEngine.ts","../src/services/interceptor/RequestBuffer.ts","../src/services/interceptor/SessionArchive.ts","../src/services/interceptor/types.ts","../src/services/lease/LeaseOrphanSweeper.ts","../src/services/lease/LeaseService.ts","../src/services/lease/buildCapabilityBag.ts","../src/services/lease/leaseToken.ts","../src/services/omni-vision/OmniVisionService.ts","../src/services/omni-vision/VisionAssertionService.ts","../src/services/ports/PortAllocatorClient.ts","../src/services/ports/PortAllocatorService.ts","../src/services/recording/RecordingOrchestrator.ts","../src/services/recording/annotation-render.ts","../src/services/recording/busy-precheck.ts","../src/services/recording/concurrency-gate.ts","../src/services/recording/manualLock.ts","../src/services/recording/proof-bundle.ts","../src/services/recording/recording-store.ts","../src/services/telemetry/attributes.ts","../src/services/token/JwtKeyService.ts","../src/services/token/StreamTicketService.ts","../src/sessions/CloudSession.ts","../src/sessions/LocalSession.ts","../src/sessions/RemoteSession.ts","../src/sessions/SessionManager.ts","../src/sessions/XenonSession.ts","../src/types/CLIArgs.ts","../src/types/CloudArgs.ts","../src/types/CloudSchema.ts","../src/types/DeviceType.ts","../src/types/DeviceUpdate.ts","../src/types/IOSDevice.ts","../src/types/Platform.ts","../src/types/SessionStatus.ts","../src/types/express.d.ts","../src/types/healing.d.ts","../src/types/identity.ts","../src/validators/CapabilityValidator.ts","../test/e2e/e2ehelper.ts","../test/e2e/plugin-harness.ts","../test/e2e/plugin.spec.ts","../test/e2e/startup-verification.spec.ts","../test/e2e/telemetry_verification.spec.ts","../test/e2e/video-recording-test.spec.js","../test/e2e/android/conf.spec.js","../test/e2e/android/conf2.spec.js","../test/e2e/android/conf3.spec.js","../test/e2e/hubnode/forward-request.spec.ts","../test/e2e/hubnode/hubnode.spec.ts","../test/e2e/ios/conf1.spec.js","../test/e2e/ios/conf2.spec.js","../test/helpers/seedUser.ts","../test/helpers/test-container.ts","../test/integration/androidDevices.spec.ts","../test/integration/auth-flow.spec.ts","../test/integration/auth-rate-limit.spec.ts","../test/integration/bug-report-route.spec.ts","../test/integration/cliArgs.js","../test/integration/cross-workflow.spec.ts","../test/integration/forgot-password-rate-limit.spec.ts","../test/integration/forgot-password.spec.ts","../test/integration/legacy-deprecation-headers.spec.ts","../test/integration/migration.spec.ts","../test/integration/node-pair-auth-rest.spec.ts","../test/integration/ports-router.spec.ts","../test/integration/profile-tokens.spec.ts","../test/integration/recordings-router.spec.ts","../test/integration/reset-revokes-sessions.spec.ts","../test/integration/role-matrix.spec.ts","../test/integration/sdk-leases-router.spec.ts","../test/integration/sdk-version.spec.ts","../test/integration/selector-state-flow.spec.ts","../test/integration/team-membership-migration.spec.ts","../test/integration/team-visibility-dashboard.spec.ts","../test/integration/team-visibility-grid.spec.ts","../test/integration/team-visibility-recordings.spec.ts","../test/integration/users-flow.spec.ts","../test/integration/users-lockout.spec.ts","../test/integration/ios/01iOSSimulator.spec.ts","../test/integration/ios/02iOSDevices.spec.ts","../test/unit/AndroidDeviceManager.spec.ts","../test/unit/ApiKeyService.test.ts","../test/unit/ArtifactStore.spec.ts","../test/unit/AutowaitService.spec.ts","../test/unit/CapabilitiesRouter.spec.ts","../test/unit/ChromeDriverManager.spec.js","../test/unit/CleanupService.spec.ts","../test/unit/CommandInterceptor.autowait.spec.ts","../test/unit/DeviceModel.spec.ts","../test/unit/EmailService.test.ts","../test/unit/EventLogService.spec.ts","../test/unit/FuzzyXmlHealingProvider.test.ts","../test/unit/GetAdbOriginal.js","../test/unit/HealingCascade.test.ts","../test/unit/IOSDeviceManager.spec.js","../test/unit/JwtKeyService.spec.ts","../test/unit/OrphanSweeper.test.ts","../test/unit/PasswordResetService.test.ts","../test/unit/PortAllocator.test.ts","../test/unit/ProcessRegistry.test.ts","../test/unit/ProjectsRouter.spec.ts","../test/unit/RemoteIOs.spec.js","../test/unit/ResilioTreeHealingProvider.test.ts","../test/unit/SessionHeartbeatWrite.test.ts","../test/unit/StreamTicketService.spec.ts","../test/unit/TeamService.test.ts","../test/unit/TokenEndpoint.spec.ts","../test/unit/UserService.test.ts","../test/unit/UserSessionService.test.ts","../test/unit/XenonCapabilityManager.spec.ts","../test/unit/aggregate-hotspots-tuple-key.spec.ts","../test/unit/auth-me-auth-disabled.test.ts","../test/unit/authMiddleware.bearer.spec.ts","../test/unit/authMiddleware.streamTicket.spec.ts","../test/unit/authMiddleware.test.ts","../test/unit/bootstrap-identity.test.ts","../test/unit/build-export.spec.ts","../test/unit/busy-precheck.spec.ts","../test/unit/commands.spec.js","../test/unit/concurrency-gate.spec.ts","../test/unit/config.spec.js","../test/unit/device-service.spec.ts","../test/unit/device-utils.spec.ts","../test/unit/event-manager-regression-hook.spec.ts","../test/unit/extract-access-key-token-pair.test.ts","../test/unit/findAndLockDevice-leasemerge.spec.ts","../test/unit/healing-instrumentation.spec.ts","../test/unit/healing-state-endpoints.spec.ts","../test/unit/helpers.spec.ts","../test/unit/loginRateLimiter.test.ts","../test/unit/omni-vision.spec.ts","../test/unit/plugin.spec.ts","../test/unit/prisma.spec.ts","../test/unit/profile-router.test.ts","../test/unit/proof-bundle.spec.ts","../test/unit/rateLimitMiddleware.test.ts","../test/unit/recording-instrumentation.spec.ts","../test/unit/recording-orchestrator.spec.ts","../test/unit/recording-store.spec.ts","../test/unit/roleGuard.test.ts","../test/unit/run-migrations-fail-fast.test.ts","../test/unit/run-migrations.test.ts","../test/unit/selector-state-service.spec.ts","../test/unit/selector-verification-job.spec.ts","../test/unit/team-filter.test.ts","../test/unit/tracing-service-init.spec.ts","../test/unit/userAuthorization.test.ts","../test/unit/users-router.test.ts","../test/unit/video-pipeline-outputpath.spec.ts","../test/unit/bug-report/archive.spec.ts","../test/unit/bug-report/har-collector.spec.ts","../test/unit/bug-report/manifest.spec.ts","../test/unit/bug-report/readme.spec.ts","../test/unit/bug-report/route.spec.ts","../test/unit/bug-report/service.spec.ts","../test/unit/bug-report/video-slice.spec.ts","../test/unit/bug-report/window.spec.ts","../test/unit/fixtures/device.config.js","../test/unit/fixtures/devices.js","../test/unit/interceptor/AndroidProxyAdapter.spec.ts","../test/unit/interceptor/CertManager.spec.ts","../test/unit/interceptor/HarBuilder.spec.ts","../test/unit/interceptor/HostFilter.spec.ts","../test/unit/interceptor/MitmProxyHost.spec.ts","../test/unit/interceptor/MockEngine.spec.ts","../test/unit/interceptor/RequestBuffer.spec.ts","../test/unit/interceptor/SessionArchive.spec.ts","../test/unit/lease/LeaseOrphanSweeper.spec.ts","../test/unit/lease/LeaseService.spec.ts","../test/unit/lease/buildCapabilityBag.spec.ts","../test/unit/lease/leaseToken.spec.ts","../test/unit/ports/PortAllocatorService.spec.ts"],"version":"5.9.3"}
1
+ {"root":["../src/InternalHttpClient.ts","../src/PluginContext.ts","../src/XenonCapabilityManager.ts","../src/chromeUtils.ts","../src/config.ts","../src/device-utils.ts","../src/index.ts","../src/logger.ts","../src/plugin.ts","../src/prisma.ts","../src/app/index.ts","../src/app/swagger-docs.ts","../src/app/swagger.ts","../src/app/routers/apikeys.ts","../src/app/routers/apps.ts","../src/app/routers/auth.ts","../src/app/routers/bug-report.ts","../src/app/routers/build-export.ts","../src/app/routers/capabilities.ts","../src/app/routers/config.ts","../src/app/routers/control.ts","../src/app/routers/dashboard.ts","../src/app/routers/grid.ts","../src/app/routers/interceptor.ts","../src/app/routers/ports.ts","../src/app/routers/processes.ts","../src/app/routers/profile.ts","../src/app/routers/projects.ts","../src/app/routers/recordings.ts","../src/app/routers/reservation.ts","../src/app/routers/sdk-leases.ts","../src/app/routers/sdk-version.ts","../src/app/routers/teams.ts","../src/app/routers/users.ts","../src/app/routers/webhook.ts","../src/commands/handle.ts","../src/commands/index.ts","../src/dashboard/asset-manager.ts","../src/dashboard/commands.ts","../src/dashboard/event-manager.ts","../src/dashboard/services/app-service.ts","../src/dashboard/services/failure-analysis-service.ts","../src/dashboard/services/session-service.ts","../src/data-service/CircuitBreaker.ts","../src/data-service/config-service.ts","../src/data-service/db.ts","../src/data-service/device-service.ts","../src/data-service/device-store.interface.ts","../src/data-service/device-store.ts","../src/data-service/pending-sessions-service.ts","../src/data-service/pluginArgs.ts","../src/data-service/prisma-service.ts","../src/data-service/prisma-store.ts","../src/data-service/queue-service.ts","../src/data-service/web-config-service.ts","../src/device-managers/AndroidDeviceManager.ts","../src/device-managers/ChromeDriverManager.ts","../src/device-managers/HealthMonitorService.ts","../src/device-managers/IOSDeviceManager.ts","../src/device-managers/NodeDevices.ts","../src/device-managers/iOSTracker.ts","../src/device-managers/index.ts","../src/device-managers/android/AndroidStreamService.ts","../src/device-managers/android/DeviceLockManager.ts","../src/device-managers/cloud/CapabilityManager.ts","../src/device-managers/cloud/Devices.ts","../src/device-managers/ios/IOSDiscoveryService.ts","../src/device-managers/ios/IOSStreamService.ts","../src/device-managers/ios/WDAClient.ts","../src/device-managers/ios/iosBattery.ts","../src/device-managers/ios/iosStreamDiagnostics.ts","../src/enums/Capabilities.ts","../src/enums/Cloud.ts","../src/enums/Platform.ts","../src/enums/SessionType.ts","../src/enums/SocketEvents.ts","../src/generated/client/default.d.ts","../src/generated/client/default.js","../src/generated/client/edge.d.ts","../src/generated/client/edge.js","../src/generated/client/index-browser.js","../src/generated/client/index.d.ts","../src/generated/client/index.js","../src/generated/client/wasm.d.ts","../src/generated/client/wasm.js","../src/generated/client/runtime/edge-esm.js","../src/generated/client/runtime/edge.js","../src/generated/client/runtime/index-browser.d.ts","../src/generated/client/runtime/index-browser.js","../src/generated/client/runtime/library.d.ts","../src/generated/client/runtime/library.js","../src/generated/client/runtime/react-native.js","../src/generated/client/runtime/wasm.js","../src/helpers/UniversalMjpegProxy.ts","../src/helpers/index.ts","../src/interceptors/CommandInterceptor.ts","../src/interfaces/ICloudManager.ts","../src/interfaces/IDevice.ts","../src/interfaces/IDeviceFilterOptions.ts","../src/interfaces/IDeviceManager.ts","../src/interfaces/IOptions.ts","../src/interfaces/IPluginArgs.ts","../src/interfaces/ISessionCapability.ts","../src/logging/sessionContext.ts","../src/middleware/authMiddleware.ts","../src/middleware/csrfMiddleware.ts","../src/middleware/leaseTokenMiddleware.ts","../src/middleware/loginRateLimiter.ts","../src/middleware/rateLimitMiddleware.ts","../src/middleware/roleGuard.ts","../src/middleware/scopeGuard.ts","../src/profiling/AndroidAppProfiler.ts","../src/proxy/wd-command-proxy.ts","../src/scripts/generate-database-migration.ts","../src/scripts/initialize-database.ts","../src/scripts/install-go-ios.ts","../src/scripts/prepare-prisma.ts","../src/scripts/run-migrations.ts","../src/services/AICommandService.ts","../src/services/AIService.ts","../src/services/ApiKeyService.ts","../src/services/CircuitBreaker.ts","../src/services/CleanupService.ts","../src/services/DeviceReconciler.ts","../src/services/EmailService.ts","../src/services/EventBus.ts","../src/services/EventLogService.ts","../src/services/InspectorService.ts","../src/services/InterceptorService.ts","../src/services/MetricsService.ts","../src/services/NetworkConditioningService.ts","../src/services/NotificationService.ts","../src/services/OrphanSweeper.ts","../src/services/PasswordResetService.ts","../src/services/PortAllocator.ts","../src/services/ProcessMetricsService.ts","../src/services/ProcessRegistry.ts","../src/services/RequestLogService.ts","../src/services/ResourceIsolationService.ts","../src/services/SecurityService.ts","../src/services/SelectorStateService.ts","../src/services/SelectorVerificationJob.ts","../src/services/ServerManager.ts","../src/services/SessionHeartbeatService.ts","../src/services/SessionLifecycleService.ts","../src/services/ShutdownCoordinator.ts","../src/services/SocketClient.ts","../src/services/SocketServer.ts","../src/services/TeamService.ts","../src/services/TracingService.ts","../src/services/UserService.ts","../src/services/UserSessionService.ts","../src/services/VideoPipelineService.ts","../src/services/artifacts/ArtifactStore.ts","../src/services/autowait/AutowaitService.ts","../src/services/autowait/waitFor.ts","../src/services/bug-report/BugReportService.ts","../src/services/bug-report/archive.ts","../src/services/bug-report/har-collector.ts","../src/services/bug-report/manifest.ts","../src/services/bug-report/readme.ts","../src/services/bug-report/types.ts","../src/services/bug-report/video-slice.ts","../src/services/bug-report/window.ts","../src/services/healing/FuzzyXmlHealingProvider.ts","../src/services/healing/HealEtalonService.ts","../src/services/healing/HealedLocatorGenerator.ts","../src/services/healing/HealingMetrics.ts","../src/services/healing/HealingOrchestrator.ts","../src/services/healing/LlmHealingProvider.ts","../src/services/healing/OcrHealingProvider.ts","../src/services/healing/ResilioTreeHealingProvider.ts","../src/services/healing/VisualAiHealingProvider.ts","../src/services/healing/types.ts","../src/services/identity/bootstrap.ts","../src/services/identity/sessionCleanupCron.ts","../src/services/identity/userAuthorization.ts","../src/services/interceptor/AndroidProxyAdapter.ts","../src/services/interceptor/CertManager.ts","../src/services/interceptor/HarBuilder.ts","../src/services/interceptor/HostFilter.ts","../src/services/interceptor/MitmProxyHost.ts","../src/services/interceptor/MockEngine.ts","../src/services/interceptor/RequestBuffer.ts","../src/services/interceptor/SessionArchive.ts","../src/services/interceptor/types.ts","../src/services/lease/LeaseOrphanSweeper.ts","../src/services/lease/LeaseService.ts","../src/services/lease/buildCapabilityBag.ts","../src/services/lease/leaseToken.ts","../src/services/omni-vision/OmniVisionService.ts","../src/services/omni-vision/VisionAssertionService.ts","../src/services/ports/PortAllocatorClient.ts","../src/services/ports/PortAllocatorService.ts","../src/services/recording/RecordingOrchestrator.ts","../src/services/recording/annotation-render.ts","../src/services/recording/busy-precheck.ts","../src/services/recording/concurrency-gate.ts","../src/services/recording/manualLock.ts","../src/services/recording/proof-bundle.ts","../src/services/recording/recording-store.ts","../src/services/telemetry/attributes.ts","../src/services/token/JwtKeyService.ts","../src/services/token/StreamTicketService.ts","../src/sessions/CloudSession.ts","../src/sessions/LocalSession.ts","../src/sessions/RemoteSession.ts","../src/sessions/SessionManager.ts","../src/sessions/XenonSession.ts","../src/types/CLIArgs.ts","../src/types/CloudArgs.ts","../src/types/CloudSchema.ts","../src/types/DeviceType.ts","../src/types/DeviceUpdate.ts","../src/types/IOSDevice.ts","../src/types/Platform.ts","../src/types/SessionStatus.ts","../src/types/express.d.ts","../src/types/healing.d.ts","../src/types/identity.ts","../src/validators/CapabilityValidator.ts","../test/e2e/e2ehelper.ts","../test/e2e/plugin-harness.ts","../test/e2e/plugin.spec.ts","../test/e2e/startup-verification.spec.ts","../test/e2e/telemetry_verification.spec.ts","../test/e2e/video-recording-test.spec.js","../test/e2e/android/conf.spec.js","../test/e2e/android/conf2.spec.js","../test/e2e/android/conf3.spec.js","../test/e2e/hubnode/forward-request.spec.ts","../test/e2e/hubnode/hubnode.spec.ts","../test/e2e/ios/conf1.spec.js","../test/e2e/ios/conf2.spec.js","../test/helpers/seedUser.ts","../test/helpers/test-container.ts","../test/integration/androidDevices.spec.ts","../test/integration/auth-flow.spec.ts","../test/integration/auth-rate-limit.spec.ts","../test/integration/bug-report-route.spec.ts","../test/integration/cliArgs.js","../test/integration/cross-workflow.spec.ts","../test/integration/forgot-password-rate-limit.spec.ts","../test/integration/forgot-password.spec.ts","../test/integration/legacy-deprecation-headers.spec.ts","../test/integration/migration.spec.ts","../test/integration/node-pair-auth-rest.spec.ts","../test/integration/ports-router.spec.ts","../test/integration/profile-tokens.spec.ts","../test/integration/recordings-router.spec.ts","../test/integration/reset-revokes-sessions.spec.ts","../test/integration/role-matrix.spec.ts","../test/integration/sdk-leases-router.spec.ts","../test/integration/sdk-version.spec.ts","../test/integration/selector-state-flow.spec.ts","../test/integration/team-membership-migration.spec.ts","../test/integration/team-visibility-dashboard.spec.ts","../test/integration/team-visibility-grid.spec.ts","../test/integration/team-visibility-recordings.spec.ts","../test/integration/users-flow.spec.ts","../test/integration/users-lockout.spec.ts","../test/integration/ios/01iOSSimulator.spec.ts","../test/integration/ios/02iOSDevices.spec.ts","../test/unit/AndroidDeviceManager.spec.ts","../test/unit/ApiKeyService.test.ts","../test/unit/ArtifactStore.spec.ts","../test/unit/AutowaitService.spec.ts","../test/unit/CapabilitiesRouter.spec.ts","../test/unit/ChromeDriverManager.spec.js","../test/unit/CleanupService.spec.ts","../test/unit/CommandInterceptor.autowait.spec.ts","../test/unit/DeviceModel.spec.ts","../test/unit/EmailService.test.ts","../test/unit/EventLogService.spec.ts","../test/unit/FuzzyXmlHealingProvider.test.ts","../test/unit/GetAdbOriginal.js","../test/unit/HealingCascade.test.ts","../test/unit/IOSDeviceManager.spec.js","../test/unit/JwtKeyService.spec.ts","../test/unit/OrphanSweeper.test.ts","../test/unit/PasswordResetService.test.ts","../test/unit/PortAllocator.test.ts","../test/unit/ProcessRegistry.test.ts","../test/unit/ProjectsRouter.spec.ts","../test/unit/RemoteIOs.spec.js","../test/unit/ResilioTreeHealingProvider.test.ts","../test/unit/SessionHeartbeatWrite.test.ts","../test/unit/StreamTicketService.spec.ts","../test/unit/TeamService.test.ts","../test/unit/TokenEndpoint.spec.ts","../test/unit/UserService.test.ts","../test/unit/UserSessionService.test.ts","../test/unit/XenonCapabilityManager.spec.ts","../test/unit/aggregate-hotspots-tuple-key.spec.ts","../test/unit/auth-me-auth-disabled.test.ts","../test/unit/authMiddleware.bearer.spec.ts","../test/unit/authMiddleware.streamTicket.spec.ts","../test/unit/authMiddleware.test.ts","../test/unit/bootstrap-identity.test.ts","../test/unit/build-export.spec.ts","../test/unit/busy-precheck.spec.ts","../test/unit/commands.spec.js","../test/unit/concurrency-gate.spec.ts","../test/unit/config.spec.js","../test/unit/device-service.spec.ts","../test/unit/device-utils.spec.ts","../test/unit/event-manager-regression-hook.spec.ts","../test/unit/extract-access-key-token-pair.test.ts","../test/unit/findAndLockDevice-leasemerge.spec.ts","../test/unit/healing-instrumentation.spec.ts","../test/unit/healing-state-endpoints.spec.ts","../test/unit/helpers.spec.ts","../test/unit/iosBattery.spec.ts","../test/unit/iosStreamDiagnostics.spec.ts","../test/unit/loginRateLimiter.test.ts","../test/unit/omni-vision.spec.ts","../test/unit/plugin.spec.ts","../test/unit/prisma.spec.ts","../test/unit/profile-router.test.ts","../test/unit/proof-bundle.spec.ts","../test/unit/rateLimitMiddleware.test.ts","../test/unit/recording-instrumentation.spec.ts","../test/unit/recording-orchestrator.spec.ts","../test/unit/recording-store.spec.ts","../test/unit/roleGuard.test.ts","../test/unit/run-migrations-fail-fast.test.ts","../test/unit/run-migrations.test.ts","../test/unit/selector-state-service.spec.ts","../test/unit/selector-verification-job.spec.ts","../test/unit/team-filter.test.ts","../test/unit/tracing-service-init.spec.ts","../test/unit/userAuthorization.test.ts","../test/unit/users-router.test.ts","../test/unit/video-pipeline-args.spec.ts","../test/unit/video-pipeline-outputpath.spec.ts","../test/unit/bug-report/archive.spec.ts","../test/unit/bug-report/har-collector.spec.ts","../test/unit/bug-report/manifest.spec.ts","../test/unit/bug-report/readme.spec.ts","../test/unit/bug-report/route.spec.ts","../test/unit/bug-report/service.spec.ts","../test/unit/bug-report/video-slice.spec.ts","../test/unit/bug-report/window.spec.ts","../test/unit/fixtures/device.config.js","../test/unit/fixtures/devices.js","../test/unit/interceptor/AndroidProxyAdapter.spec.ts","../test/unit/interceptor/CertManager.spec.ts","../test/unit/interceptor/HarBuilder.spec.ts","../test/unit/interceptor/HostFilter.spec.ts","../test/unit/interceptor/MitmProxyHost.spec.ts","../test/unit/interceptor/MockEngine.spec.ts","../test/unit/interceptor/RequestBuffer.spec.ts","../test/unit/interceptor/SessionArchive.spec.ts","../test/unit/lease/LeaseOrphanSweeper.spec.ts","../test/unit/lease/LeaseService.spec.ts","../test/unit/lease/buildCapabilityBag.spec.ts","../test/unit/lease/leaseToken.spec.ts","../test/unit/ports/PortAllocatorService.spec.ts"],"version":"5.9.3"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xenon-device-management/xenon",
3
- "version": "1.7.3",
3
+ "version": "1.7.5",
4
4
  "description": "Xenon - Intelligent Mobile Infrastructure. A self-healing device orchestration platform for Appium.",
5
5
  "main": "./lib/src/index.js",
6
6
  "exports": {
@@ -1,2 +0,0 @@
1
- import{r as m,j as t,X as T}from"./index-PtnKslHU.js";import{A as U,I as le}from"./keycodes-bIEJfQzr.js";function de(e,r){return e!=="auto"?e:r<=1?"1":r===2?"2x1":r<=4?"2x2":"3x2"}const ue={selected:new Set,layout:"2x2",tiles:[],groupId:null,recording:!1,startedAt:null,annotateMode:!1,shape:"RECT",color:"#ff3333",errorBanner:null};function fe(e,r){switch(r.type){case"SET_LAYOUT":return{...e,layout:r.layout};case"TOGGLE_SELECTED":{const n=new Set(e.selected);return n.has(r.udid)?n.delete(r.udid):n.add(r.udid),{...e,selected:n}}case"CLEAR_SELECTED":return{...e,selected:new Set};case"SET_TILES":return{...e,tiles:r.tiles};case"ADD_TILE":return e.tiles.some(n=>n.udid===r.tile.udid)?e:{...e,tiles:[...e.tiles,r.tile]};case"REMOVE_TILE":return{...e,tiles:e.tiles.filter(n=>n.udid!==r.udid)};case"BIND_RECORDING_IDS":{const n=e.tiles.map(l=>({...l,recordingId:r.map[l.udid]??l.recordingId}));return{...e,tiles:n}}case"START_RECORDING":{const n=e.tiles.map(l=>({...l,recordingId:r.tileIds[l.udid]??l.recordingId}));return{...e,groupId:r.groupId,recording:!0,startedAt:r.startedAt,tiles:n}}case"STOP_RECORDING":return{...e,recording:!1};case"SET_ANNOTATE_MODE":return{...e,annotateMode:r.enabled};case"SET_SHAPE":return{...e,shape:r.shape};case"SET_COLOR":return{...e,color:r.color};case"SET_ERROR_BANNER":return{...e,errorBanner:r.message};default:return e}}const V=m.createContext(null);function me(){const e=m.useContext(V);if(!e)throw new Error("useMosaic must be used inside <MosaicContext.Provider>");return e}function xe(){return m.useReducer(fe,ue)}function he(e){switch(e){case"automation":return"In automation";case"manual_other":return"Manual control by another user";case"manual_self":return"In your mosaic";case"recording_other_group":return"Recording in another group";default:return e?"Busy":null}}const G=[{id:"ios",label:"iOS",match:e=>e==="ios"},{id:"android",label:"Android",match:e=>e==="android"},{id:"tvos",label:"tvOS",match:e=>e==="tvos"},{id:"androidtv",label:"Android TV",match:e=>e==="androidtv"||e==="android-tv"},{id:"other",label:"Other",match:()=>!0}];function pe(e){return e?e==="androidtv"||e==="android-tv"?"ANDROID TV":e.toUpperCase():""}function ge({devices:e,inMosaic:r,onToggle:n}){const[l,b]=m.useState(""),[f,w]=m.useState({}),N=m.useMemo(()=>{const u=l.trim().toLowerCase();return u?e.filter(g=>g.udid.toLowerCase().includes(u)||(g.name??"").toLowerCase().includes(u)||(g.platform??"").toLowerCase().includes(u)):e},[e,l]),v=m.useMemo(()=>{const u=G.map(o=>({id:o.id,label:o.label,rows:[]})),g=o=>u.find((c,s)=>G[s].match(o))??u[u.length-1];for(const o of N)g((o.platform??"").toLowerCase()).rows.push(o);return u.filter(o=>o.rows.length>0)},[N]);return e.length===0?t.jsx("div",{className:"text-xs text-[var(--text-dim)] p-3",children:"No devices online."}):t.jsxs("div",{className:"flex flex-col gap-2",children:[t.jsxs("div",{className:"relative",children:[t.jsx("span",{"aria-hidden":!0,className:"absolute left-2 top-1/2 -translate-y-1/2 text-[var(--text-dim)] text-xs",children:"⌕"}),t.jsx("input",{type:"text",value:l,placeholder:"Filter devices...",onChange:u=>b(u.target.value),className:"w-full pl-7 pr-2 py-1.5 text-xs rounded border border-[var(--border)] bg-transparent placeholder:text-[var(--text-dim)] focus:outline-none focus:border-[var(--accent,#3b82f6)]"})]}),v.map(u=>{const g=f[u.id];return t.jsxs("section",{className:"flex flex-col gap-1",children:[t.jsxs("button",{type:"button",onClick:()=>w(o=>({...o,[u.id]:!o[u.id]})),className:"flex items-center justify-between w-full text-[10px] uppercase tracking-wider text-[var(--text-dim)] hover:text-white px-1 py-0.5",children:[t.jsxs("span",{className:"flex items-center gap-1.5",children:[t.jsx("span",{"aria-hidden":!0,className:`inline-block transition-transform ${g?"-rotate-90":"rotate-0"}`,children:"▾"}),u.label]}),t.jsx("span",{children:u.rows.length})]}),!g&&t.jsx("ul",{className:"flex flex-col gap-1 text-sm",children:u.rows.map(o=>{const c=o.busyReason==="manual_self",s=r.has(o.udid),x=!!o.busy&&!c&&!s||!!o.offline,R=o.offline?"Offline":he(o.busyReason),i=!x;return t.jsx("li",{children:t.jsxs("button",{type:"button",disabled:x,draggable:!x,onDragStart:d=>{d.dataTransfer.effectAllowed="copy",d.dataTransfer.setData("application/x-xenon-udid",o.udid),d.dataTransfer.setData("text/plain",o.udid)},onClick:()=>n(o.udid),className:`w-full flex items-center gap-2 px-2 py-1.5 rounded text-left border transition-colors ${x?"opacity-60 cursor-not-allowed border-transparent":s?"border-emerald-500/40 bg-emerald-500/5 hover:bg-emerald-500/10":"border-transparent hover:border-[var(--border)] hover:bg-[var(--surface-2,rgba(255,255,255,0.04))]"}`,title:x?`${R} — release first`:s?"Click to remove from mosaic (or drag to a cell)":"Click to add to mosaic (or drag to a cell)",children:[t.jsx("span",{"aria-hidden":!0,className:`inline-flex items-center justify-center w-4 h-4 text-xs ${s?"text-emerald-400":"text-[var(--text-dim)]"}`,children:s?"●":"○"}),t.jsx("span",{"aria-hidden":!0,className:`inline-block w-1.5 h-1.5 rounded-full ${o.offline?"bg-zinc-600":i?"bg-emerald-400 shadow-[0_0_4px_rgba(52,211,153,0.7)]":"bg-yellow-500"}`}),t.jsx("span",{className:"font-medium flex-1 truncate text-[13px]",children:o.name??o.udid}),o.platform&&t.jsx("span",{className:"text-[9px] uppercase tracking-wider px-1.5 py-0.5 rounded bg-[var(--surface-2,rgba(255,255,255,0.06))] text-[var(--text-dim)]",children:pe(o.platform)})]})},o.udid)})})]},u.id)}),l&&v.length===0&&t.jsxs("div",{className:"text-xs text-[var(--text-dim)] px-1 py-2",children:['No devices match "',l,'".']}),t.jsxs("div",{className:"text-[10px] text-[var(--text-dim)] px-1 pt-2 flex items-center gap-3",children:[t.jsxs("span",{className:"inline-flex items-center gap-1",children:[t.jsx("span",{className:"w-1.5 h-1.5 rounded-full bg-emerald-400 inline-block"})," available"]}),t.jsxs("span",{className:"inline-flex items-center gap-1",children:[t.jsx("span",{className:"w-1.5 h-1.5 rounded-full bg-yellow-500 inline-block"})," in use"]}),t.jsxs("span",{className:"inline-flex items-center gap-1",children:[t.jsx("span",{className:"w-1.5 h-1.5 rounded-full bg-zinc-600 inline-block"})," offline"]})]})]})}const be=[{id:"1",label:"1",icon:t.jsx(ye,{})},{id:"2x1",label:"2×1",icon:t.jsx(we,{})},{id:"2x2",label:"2×2",icon:t.jsx(Ne,{})},{id:"3x2",label:"3×2",icon:t.jsx(je,{})},{id:"auto",label:"Auto",icon:t.jsx(Re,{})}];function ve({value:e,onChange:r}){return t.jsx("div",{role:"tablist",className:"inline-flex border border-[var(--border)] rounded-md overflow-hidden text-xs bg-[var(--surface-1,rgba(255,255,255,0.02))]",children:be.map(n=>{const l=e===n.id;return t.jsxs("button",{role:"tab","aria-selected":l,onClick:()=>r(n.id),className:`flex items-center gap-1.5 px-2.5 py-1.5 border-r border-[var(--border)] last:border-r-0 transition-colors ${l?"bg-[var(--surface-2,#2a2a2a)] text-white":"text-[var(--text-dim)] hover:bg-[var(--surface-2,rgba(255,255,255,0.04))] hover:text-white"}`,title:`Layout: ${n.label}`,children:[t.jsx("span",{className:"w-3.5 h-3.5 inline-flex items-center justify-center",children:n.icon}),t.jsx("span",{children:n.label})]},n.id)})})}function C(e){return t.jsx("rect",{rx:1.2,ry:1.2,fill:"currentColor",...e})}function ye(){return t.jsx("svg",{viewBox:"0 0 14 14",className:"w-3.5 h-3.5",children:C({x:2,y:2,width:10,height:10})})}function we(){return t.jsxs("svg",{viewBox:"0 0 14 14",className:"w-3.5 h-3.5",children:[C({x:1.5,y:3,width:5,height:8}),C({x:7.5,y:3,width:5,height:8})]})}function Ne(){return t.jsxs("svg",{viewBox:"0 0 14 14",className:"w-3.5 h-3.5",children:[C({x:1.5,y:1.5,width:5,height:5}),C({x:7.5,y:1.5,width:5,height:5}),C({x:1.5,y:7.5,width:5,height:5}),C({x:7.5,y:7.5,width:5,height:5})]})}function je(){return t.jsxs("svg",{viewBox:"0 0 14 14",className:"w-3.5 h-3.5",children:[C({x:1,y:1.5,width:3.3,height:5}),C({x:5.35,y:1.5,width:3.3,height:5}),C({x:9.7,y:1.5,width:3.3,height:5}),C({x:1,y:7.5,width:3.3,height:5}),C({x:5.35,y:7.5,width:3.3,height:5}),C({x:9.7,y:7.5,width:3.3,height:5})]})}function Re(){return t.jsxs("svg",{viewBox:"0 0 14 14",className:"w-3.5 h-3.5",fill:"none",stroke:"currentColor",strokeWidth:"1.4",children:[t.jsx("rect",{x:"2",y:"2",width:"10",height:"10",rx:"1.5"}),t.jsx("circle",{cx:"7",cy:"7",r:"1.5",fill:"currentColor",stroke:"none"})]})}function Ee({enabled:e,shape:r,color:n,onCommit:l}){const b=m.useRef(null),[f,w]=m.useState(null);m.useEffect(()=>{const c=b.current;if(!c)return;const s=()=>{c.width=c.clientWidth,c.height=c.clientHeight,N()};s();const x=new ResizeObserver(s);return x.observe(c),()=>x.disconnect()},[]);const N=m.useCallback(()=>{const c=b.current;if(!c)return;const s=c.getContext("2d");if(s&&(s.clearRect(0,0,c.width,c.height),!!f)){if(s.strokeStyle=n,s.lineWidth=3,r==="RECT"||r==="ARROW"||r==="FREEHAND")s.strokeRect(Math.min(f.startX,f.curX),Math.min(f.startY,f.curY),Math.abs(f.curX-f.startX),Math.abs(f.curY-f.startY));else if(r==="CIRCLE"){const x=Math.hypot(f.curX-f.startX,f.curY-f.startY);s.beginPath(),s.arc(f.startX,f.startY,x,0,Math.PI*2),s.stroke()}}},[f,n,r]);m.useEffect(()=>{N()},[f,N]);const v=c=>{const x=b.current.getBoundingClientRect();return{px:c.clientX-x.left,py:c.clientY-x.top,w:x.width,h:x.height}},u=c=>{if(!e)return;const s=v(c);w({startX:s.px,startY:s.py,curX:s.px,curY:s.py})},g=c=>{if(!f)return;const s=v(c);w({...f,curX:s.px,curY:s.py})},o=c=>{if(!f||!e)return;const s=v(c),x=Math.min(f.startX,s.px)/s.w,R=Math.min(f.startY,s.py)/s.h,i=Math.abs(s.px-f.startX)/s.w,d=Math.abs(s.py-f.startY)/s.h;i>.005&&d>.005&&l({shape:r,color:n,geometry:{x,y:R,w:i,h:d}}),w(null)};return t.jsx("canvas",{ref:b,style:{position:"absolute",inset:0,pointerEvents:e?"auto":"none",cursor:e?"crosshair":"default"},onMouseDown:u,onMouseMove:g,onMouseUp:o,onMouseLeave:()=>w(null)})}const Te=10,Se=500;function ke({udid:e,name:r,recordingId:n,annotateMode:l,shape:b,color:f,aspect:w="9 / 16",screenWidth:N,screenHeight:v,platform:u,onAnnotation:g,onRemove:o}){const[c,s]=m.useState("connecting"),[x,R]=m.useState(0),[i,d]=m.useState([]),[p,j]=m.useState(!1),O=m.useRef(null),_=m.useRef(null),y=u==="android"||u==="androidtv"||u==="android-tv",S=u==="ios"||u==="tvos",E=!l&&!!N&&!!v,X=(a,h,k)=>{const A=N,D=v,M=Math.min(1,Math.max(0,(a-k.left)/k.width)),I=Math.min(1,Math.max(0,(h-k.top)/k.height));return{x:Math.round(M*A),y:Math.round(I*D)}},Z=(a,h,k)=>{const A=Date.now()+Math.random(),D=a-k.left,M=h-k.top;d(I=>[...I,{id:A,x:D,y:M}]),window.setTimeout(()=>{d(I=>I.filter(B=>B.id!==A))},500)},Q=a=>{if(!E||c!=="live"||a.button!==0)return;const h=a.currentTarget;h.setPointerCapture(a.pointerId),_.current={startX:a.clientX,startY:a.clientY,startTime:Date.now(),rect:h.getBoundingClientRect()},Z(a.clientX,a.clientY,_.current.rect),h.focus()},ee=async a=>{var H,K;const h=_.current;if(_.current=null,!h||!E)return;try{a.currentTarget.releasePointerCapture(a.pointerId)}catch{}const k=a.clientX-h.startX,A=a.clientY-h.startY,D=Math.hypot(k,A),M=Date.now()-h.startTime,I=X(h.startX,h.startY,h.rect),B=D<Te;try{if(B&&M<Se)await T.tap(e,I.x,I.y);else if(B)await((K=(H=T).touchAndHold)==null?void 0:K.call(H,e,I.x,I.y,M));else{const P=X(a.clientX,a.clientY,h.rect);await T.swipe(e,I.x,I.y,P.x,P.y)}}catch(P){console.error(`[DeviceTile] gesture failed for ${e}`,P)}},te=()=>{_.current=null},re=async a=>{if(!E||c!=="live"||a.metaKey||a.ctrlKey||a.altKey)return;const h=a.key;try{if(h.length===1){a.preventDefault(),await T.typeText(e,h);return}if(h==="Backspace"){a.preventDefault(),y?await T.pressKey(e,67):await T.typeText(e,"\b");return}if(h==="Enter"){a.preventDefault(),y?await T.pressKey(e,66):await T.typeText(e,`
2
- `);return}if(h==="Tab"){a.preventDefault(),await T.typeText(e," ");return}h==="Escape"&&y&&(a.preventDefault(),await T.pressKey(e,U.BACK))}catch(k){console.error(`[DeviceTile] keystroke "${h}" failed for ${e}`,k)}},ne=()=>T.pressKey(e,y?U.HOME:le.HOME).catch(a=>console.error(`[DeviceTile] home failed for ${e}`,a)),se=()=>T.pressKey(e,U.BACK).catch(a=>console.error(`[DeviceTile] back failed for ${e}`,a)),oe=()=>T.pressKey(e,U.APP_SWITCH).catch(a=>console.error(`[DeviceTile] app-switch failed for ${e}`,a)),ae=async()=>{try{const a=await T.getScreenshot(e),h=(a==null?void 0:a.screenshot)??(a==null?void 0:a.value)??a;if(!h)return;const k=await fetch(`data:image/png;base64,${h}`).then(M=>M.blob()),A=URL.createObjectURL(k),D=document.createElement("a");D.href=A,D.download=`${r||e}-${Date.now()}.png`,document.body.appendChild(D),D.click(),D.remove(),setTimeout(()=>URL.revokeObjectURL(A),1e3)}catch(a){console.error(`[DeviceTile] screenshot failed for ${e}`,a)}};m.useEffect(()=>{console.log(`[DeviceTile] Rendering ${e} in state: ${c}`)},[e,c]);const ie=`/xenon/api/control/${encodeURIComponent(e)}/stream?t=${x}`;m.useEffect(()=>{if(c!=="connecting")return;const a=setTimeout(()=>{s(h=>h==="connecting"?"unavailable":h)},9e4);return()=>clearTimeout(a)},[c,x]);const ce=()=>{s("connecting"),R(Date.now())},L=!!n,W=r||e.slice(0,16)+"…";return t.jsxs("div",{className:"flex items-stretch justify-center w-full h-full min-h-0 min-w-0 gap-2",children:[t.jsxs("div",{className:"relative bg-[#000] rounded-lg overflow-hidden border border-[var(--border)] group max-h-full max-w-full self-center",style:{aspectRatio:w,height:"100%"},children:[c==="connecting"&&t.jsxs("div",{className:"flex flex-col items-center justify-center bg-neutral-900 text-neutral-400 absolute inset-0 z-20",children:[t.jsxs("div",{className:"relative flex items-center justify-center",children:[t.jsx("div",{className:"w-12 h-12 border-4 border-neutral-800 border-t-red-500 rounded-full animate-spin"}),t.jsx("div",{className:"absolute text-[10px] text-red-500 font-bold",children:"WDA"})]}),t.jsx("div",{className:"mt-4 font-bold text-neutral-200",children:"Starting Stream…"}),t.jsx("div",{className:"text-[10px] mt-1 text-neutral-500 opacity-60 font-mono",children:e}),t.jsx("div",{className:"text-[10px] mt-4 text-neutral-400 bg-white/5 px-2 py-1 rounded",children:"Waiting for device connection..."})]}),c==="unavailable"&&t.jsx("div",{className:"flex items-center justify-center bg-black text-neutral-400 absolute inset-0 z-20",children:t.jsxs("div",{className:"text-center p-6",children:[t.jsx("div",{className:"text-5xl mb-4 opacity-20",children:"📵"}),t.jsx("div",{className:"font-bold text-white text-lg",children:"Connection Failed"}),t.jsx("p",{className:"text-xs mt-2 text-neutral-500 leading-relaxed max-w-[240px] mx-auto",children:"We couldn't connect to the WDA MJPEG stream on port 9100. If automation is running, ensure it hasn't blocked the MJPEG port."}),t.jsx("button",{className:"mt-6 text-sm font-semibold px-6 py-2.5 rounded-full bg-red-600 hover:bg-red-500 text-white transition-all shadow-[0_0_20px_rgba(220,38,38,0.3)] active:scale-95",onClick:ce,children:"Retry Connection"})]})}),t.jsx("img",{src:ie,alt:W,className:"absolute inset-0 w-full h-full object-contain bg-black select-none pointer-events-none",draggable:!1,style:{opacity:c==="live"?1:0,transition:"opacity 0.3s ease-in-out"},onLoad:()=>{console.log(`[DeviceTile] Image LOADED for ${e}`),s("live")},onError:a=>{console.error(`[DeviceTile] Image ERROR for ${e}`,a),s("unavailable")}},x),E&&c==="live"&&t.jsx("div",{ref:O,className:`absolute inset-0 z-10 touch-none outline-none ${p?"ring-2 ring-emerald-400/60 ring-inset rounded-lg":""}`,tabIndex:0,onPointerDown:Q,onPointerUp:ee,onPointerCancel:te,onKeyDown:re,onFocus:()=>j(!0),onBlur:()=>j(!1)}),t.jsx("div",{className:"absolute inset-0 z-20 pointer-events-none overflow-hidden",children:i.map(a=>t.jsx("span",{className:"absolute block w-10 h-10 rounded-full border-2 border-emerald-300/80 animate-ping",style:{left:a.x-20,top:a.y-20}},a.id))}),L&&l&&t.jsx(Ee,{enabled:!0,shape:b,color:f,onCommit:a=>g(n,a)}),L&&t.jsx("div",{className:"absolute top-3 left-3 z-30 pointer-events-none",children:t.jsx("span",{className:"px-2 py-0.5 rounded-sm bg-red-600 text-white text-[9px] font-black shadow-lg",children:"REC"})}),t.jsxs("div",{className:"absolute top-3 left-3 z-30 flex flex-col gap-1.5 pointer-events-none opacity-0 group-hover:opacity-100 transition-opacity duration-150",children:[c==="live"&&!L&&t.jsx("span",{className:"px-2 py-0.5 rounded-sm bg-emerald-500 text-black text-[9px] font-black shadow-lg w-fit",children:"LIVE"}),t.jsx("span",{className:"px-2 py-1 rounded bg-black/80 text-white text-[11px] font-medium backdrop-blur-md border border-white/10 shadow-xl truncate max-w-[180px]",children:W})]})]}),t.jsx(Ce,{streamState:c,recording:L,isIOS:S,isAndroid:y,platform:u,onClose:!L&&o?()=>o(e):void 0,sendHome:ne,sendBack:se,sendAppSwitch:oe,captureScreenshot:ae,displayName:W})]})}function Ce({streamState:e,recording:r,isIOS:n,isAndroid:l,platform:b,onClose:f,sendHome:w,sendBack:N,sendAppSwitch:v,captureScreenshot:u,displayName:g}){const o=e==="live";return!f&&!o?null:t.jsxs("div",{className:"self-stretch w-9 flex flex-col items-center justify-between gap-1 py-2 rounded-lg bg-black/40 border border-white/10 backdrop-blur-md text-white",children:[t.jsx("div",{className:"flex flex-col items-center gap-1",children:f&&t.jsx("button",{type:"button","aria-label":`Remove ${g} from mosaic`,title:"Remove from mosaic",onClick:f,className:"w-7 h-7 flex items-center justify-center rounded-md hover:bg-red-600/70 transition-colors text-base leading-none",children:"×"})}),o&&t.jsxs("div",{className:"flex flex-col items-center gap-1.5 my-auto",children:[(l||n)&&t.jsx("button",{type:"button",title:`Home${b?` (${b})`:""}`,onClick:w,className:"w-7 h-7 flex items-center justify-center rounded-md hover:bg-white/15 transition-colors text-sm",children:"⌂"}),l&&t.jsx("button",{type:"button",title:"Back",onClick:N,className:"w-7 h-7 flex items-center justify-center rounded-md hover:bg-white/15 transition-colors text-sm",children:"‹"}),l&&t.jsx("button",{type:"button",title:"Recent apps",onClick:v,className:"w-7 h-7 flex items-center justify-center rounded-md hover:bg-white/15 transition-colors text-sm",children:"▢"})]}),o&&t.jsx("div",{className:"flex flex-col items-center gap-1",children:t.jsx("button",{type:"button",title:"Screenshot",onClick:u,className:"w-7 h-7 flex items-center justify-center rounded-md hover:bg-white/15 transition-colors text-sm",children:"⎙"})})]})}const Ie={1:"1fr","2x1":"1fr 1fr","2x2":"1fr 1fr","3x2":"1fr 1fr 1fr"},De={1:"1fr","2x1":"1fr","2x2":"1fr 1fr","3x2":"1fr 1fr"},_e={1:1,"2x1":2,"2x2":4,"3x2":6};function Ae({layout:e,tiles:r,annotateMode:n,shape:l,color:b,onAnnotation:f,onRemove:w,onDropDevice:N}){const v=de(e,r.length),u=_e[v];r.length;const g=r.length,o=e==="auto"?Math.max(g,1):u,c=Math.max(0,o-g);return t.jsxs("div",{style:{display:"grid",gap:8,gridTemplateColumns:Ie[v],gridTemplateRows:De[v]},className:"h-full",children:[r.map(s=>t.jsx(ke,{udid:s.udid,name:s.name,mjpegPort:s.mjpegPort,recordingId:s.recordingId,aspect:s.aspect,screenWidth:s.screenWidth,screenHeight:s.screenHeight,platform:s.platform,annotateMode:n,shape:l,color:b,onAnnotation:f,onRemove:w},s.udid)),Array.from({length:c},(s,x)=>t.jsx(Me,{onDropDevice:N},`empty-${x}`))]})}function Me({onDropDevice:e}){const[r,n]=m.useState(!1);return t.jsxs("div",{onDragOver:l=>{e&&(l.preventDefault(),l.dataTransfer.dropEffect="copy",r||n(!0))},onDragLeave:()=>n(!1),onDrop:l=>{if(n(!1),!e)return;l.preventDefault();const b=l.dataTransfer.getData("application/x-xenon-udid")||l.dataTransfer.getData("text/plain");b&&e(b)},className:`flex flex-col items-center justify-center gap-2 rounded-lg border border-dashed transition-colors ${r?"border-emerald-500/60 bg-emerald-500/5":"border-[var(--border)] bg-[var(--surface-1,rgba(255,255,255,0.02))]"} text-[var(--text-dim)] min-h-[120px]`,children:[t.jsx("div",{className:`w-8 h-8 rounded-full flex items-center justify-center text-xl ${r?"bg-emerald-500/15 text-emerald-300":"bg-white/5 text-white/60"}`,children:"+"}),t.jsx("div",{className:"text-xs font-medium",children:"Drag device here"}),t.jsx("div",{className:"text-[10px] opacity-70",children:"or click one in the panel"})]})}const $="/xenon/api/recordings";class J extends Error{constructor(r){super(r.message??r.error),this.body=r}}async function Y(e,r){const n=await fetch(e,{method:"POST",headers:{"Content-Type":"application/json"},body:r?JSON.stringify(r):void 0});if(n.status===409)throw new J(await n.json());if(!n.ok)throw new Error(`HTTP ${n.status}`);return n.json()}function Oe(e,r){return Y($,{udids:e,...r})}function $e(e){return Y(`${$}/${encodeURIComponent(e)}/stop`)}function q(e,r,n,l,b){return Y(`${$}/${encodeURIComponent(e)}/bookmark`,{recordingId:r,timecodeMs:n,label:l,note:b})}function Le(e,r){return Y(`${$}/${encodeURIComponent(e)}/annotation`,r)}function Be(e){return`${$}/${encodeURIComponent(e)}/bundle.zip`}function Pe(e){return`${$}/${encodeURIComponent(e)}/composite.mp4`}function Ue({selectedUdids:e}){const{state:r,dispatch:n}=me(),l=async()=>{if(!(r.recording||e.length===0))try{const o=await Oe(e),c={};for(const s of o.recordings)c[s.udid]=s.id;n({type:"START_RECORDING",groupId:o.groupId,startedAt:Date.now(),tileIds:c}),n({type:"SET_ERROR_BANNER",message:null})}catch(o){if(o instanceof J)if(o.body.error==="device_busy"){const c=(o.body.busyDevices??[]).map(s=>`${s.udid} (${s.reason})`).join(", ");n({type:"SET_ERROR_BANNER",message:`Cannot start — these devices are busy: ${c}`})}else n({type:"SET_ERROR_BANNER",message:`Server-wide recording cap reached (${o.body.active}/${o.body.limit}).`});else n({type:"SET_ERROR_BANNER",message:`Recording failed: ${o.message}`})}},b=async()=>{if(r.groupId)try{await $e(r.groupId)}catch(o){n({type:"SET_ERROR_BANNER",message:`Stop failed: ${o.message}`})}finally{n({type:"STOP_RECORDING"})}},f=async()=>{var x;if(!r.groupId||!r.recording)return;const o=window.prompt("Bookmark label?");if(!o)return;const c=r.startedAt?Date.now()-r.startedAt:0,s=(x=r.tiles.find(R=>R.recordingId))==null?void 0:x.recordingId;if(s)try{await q(r.groupId,s,c,o)}catch(R){n({type:"SET_ERROR_BANNER",message:`Bookmark failed: ${R.message}`})}},w=()=>n({type:"SET_ANNOTATE_MODE",enabled:!r.annotateMode}),N=!r.recording&&e.length>0,v=r.recording,u=r.groupId&&!r.recording,g=u&&r.tiles.length>=2;return t.jsxs("div",{className:"flex items-center gap-2",children:[t.jsxs("button",{onClick:l,disabled:!N,className:"px-3 py-1.5 text-sm rounded bg-red-600 text-white disabled:opacity-40 disabled:cursor-not-allowed",children:["● ",e.length>1?"Record All":"Record"]}),t.jsx("button",{onClick:b,disabled:!v,className:"px-3 py-1.5 text-sm rounded border border-[var(--border)] disabled:opacity-40 disabled:cursor-not-allowed",children:"⏹ Stop"}),t.jsx("button",{onClick:f,disabled:!v,className:"px-3 py-1.5 text-sm rounded border border-[var(--border)] disabled:opacity-40 disabled:cursor-not-allowed",children:"🔖 Bookmark"}),t.jsx("button",{onClick:w,disabled:!r.recording,className:`px-3 py-1.5 text-sm rounded border ${r.annotateMode?"bg-[var(--accent,#3b82f6)] text-white border-transparent":"border-[var(--border)]"} disabled:opacity-40 disabled:cursor-not-allowed`,children:"✎ Annotate"}),g&&t.jsx("a",{href:Pe(r.groupId),className:"ml-2 px-3 py-1.5 text-sm rounded border border-[var(--border)] hover:bg-[var(--surface-2,#1a1a1a)]",download:`mosaic-${r.groupId}.mp4`,children:"⤓ Composite mp4"}),u&&t.jsx("a",{href:Be(r.groupId),className:`${g?"ml-1":"ml-2"} px-3 py-1.5 text-sm rounded border border-[var(--border)] hover:bg-[var(--surface-2,#1a1a1a)]`,download:!0,children:"⤓ Proof bundle"})]})}function Ye({remainingSec:e,onContinue:r,onReleaseNow:n}){return t.jsx("div",{role:"dialog","aria-modal":"true","aria-labelledby":"idle-warning-title",className:"fixed inset-0 z-40 bg-black/60 flex items-center justify-center",children:t.jsxs("div",{className:"bg-[var(--bg)] border border-[var(--border)] rounded-lg w-full max-w-md p-6",children:[t.jsx("h3",{id:"idle-warning-title",className:"text-base font-semibold mb-2",children:"Are you still there?"}),t.jsxs("p",{className:"text-sm text-[var(--text-dim)] mb-4",children:["Your devices will be released in"," ",t.jsxs("span",{className:"text-[var(--text)] font-mono font-semibold",children:[e,"s"]})," due to inactivity. Click ",t.jsx("strong",{children:"Continue Session"})," to keep them locked."]}),t.jsxs("div",{className:"flex justify-end gap-2",children:[t.jsx("button",{type:"button",onClick:n,className:"h-9 px-3 rounded-md border border-[var(--border)] text-sm text-[var(--text-dim)] hover:text-[var(--text)]",children:"Release now"}),t.jsx("button",{type:"button",autoFocus:!0,onClick:r,className:"h-9 px-4 rounded-md bg-[var(--green)] text-black text-sm font-medium",children:"Continue Session"})]})]})})}const F=["mousemove","mousedown","keydown","touchstart","wheel"];function We({idleAfterMs:e,warningSec:r,onWarning:n,onTimeout:l,enabled:b}){const[f,w]=m.useState(!1),[N,v]=m.useState(null),u=m.useRef(null),g=m.useRef(null),o=m.useRef(!1),c=m.useRef(n),s=m.useRef(l);c.current=n,s.current=l;function x(){u.current&&(clearTimeout(u.current),u.current=null),g.current&&(clearInterval(g.current),g.current=null)}function R(){o.current=!0,w(!0),v(r),c.current(r);let p=r;g.current=setInterval(()=>{p-=1,p<=0?(x(),o.current=!1,w(!1),v(null),s.current()):v(p)},1e3)}function i(){x(),o.current=!1,w(!1),v(null);const p=e-r*1e3;u.current=setTimeout(R,Math.max(0,p))}function d(){b&&i()}return m.useEffect(()=>{if(!b){x(),o.current=!1,w(!1),v(null);return}function p(){o.current||i()}return F.forEach(j=>window.addEventListener(j,p,{passive:!0})),i(),()=>{F.forEach(j=>window.removeEventListener(j,p)),x()}},[b,e,r]),{warning:f,remainingSec:N,reset:d}}const Xe=5*60*1e3,He=30;function z(e){if(!e)return"9 / 16";const r=Number(e.screenWidth),n=Number(e.screenHeight);if(Number.isFinite(r)&&Number.isFinite(n)&&r>0&&n>0)return`${r} / ${n}`;const l=(e.platform||"").toLowerCase();return l==="tvos"||l==="androidtv"||l==="android-tv"?"16 / 9":"9 / 16"}function Ke(e,r,n){return n?e===`manual_${n}_${r}`:!1}function Ge(e,r){if(e.busy)return e.session_id?e.session_id.startsWith("manual_")?Ke(e.session_id,e.udid,r)?"manual_self":"manual_other":"automation":"unknown"}function Fe(e,r){return{udid:e.udid,name:e.name,platform:e.platform,busy:e.busy,busyReason:Ge(e,r),mjpegServerPort:e.mjpegServerPort,offline:!!e.offline}}function Je(){const[e,r]=xe(),[n,l]=m.useState([]),[b,f]=m.useState(0),[w,N]=m.useState(null);m.useEffect(()=>{let i=!1;return fetch("/xenon/api/auth/me",{credentials:"include"}).then(d=>d.ok?d.json():null).then(d=>{!i&&(d!=null&&d.userId)&&N(d.userId)}).catch(()=>{}),()=>{i=!0}},[]),m.useEffect(()=>{let i=!1;const d=async()=>{try{const j=await T.getDevices();i||l(Array.isArray(j)?j:[])}catch{}};d();const p=window.setInterval(d,5e3);return()=>{i=!0,window.clearInterval(p)}},[b]),m.useEffect(()=>{let i=!1;return(async()=>{try{const d=await T.getDevices(),p=(Array.isArray(d)?d:[]).filter(y=>{var S;return y.busy&&((S=y.session_id)==null?void 0:S.startsWith("manual_"))});if(p.length===0)return;const O=(await Promise.all(p.map(async y=>{try{const S=await fetch(`/xenon/api/control/${encodeURIComponent(y.udid)}/stream/status`);if(!S.ok)return null;const E=await S.json();return(E==null?void 0:E.status)==="running"?y:null}catch{return null}}))).filter(y=>y!==null);if(i||O.length===0)return;const _=O.map(y=>{const S=Number(y.screenWidth),E=Number(y.screenHeight);return{udid:y.udid,name:y.name,mjpegPort:0,aspect:z(y),screenWidth:Number.isFinite(S)&&S>0?S:void 0,screenHeight:Number.isFinite(E)&&E>0?E:void 0,platform:y.platform}});r({type:"SET_TILES",tiles:_})}catch{}})(),()=>{i=!0}},[]),m.useEffect(()=>{const i=async d=>{var y,S;if(d.key!=="b"&&d.key!=="B")return;const p=(y=d.target)==null?void 0:y.tagName;if(p==="INPUT"||p==="TEXTAREA"||p==="SELECT"||!e.recording||!e.groupId)return;const j=window.prompt("Bookmark label?");if(!j)return;const O=e.startedAt?Date.now()-e.startedAt:0,_=(S=e.tiles.find(E=>E.recordingId))==null?void 0:S.recordingId;if(_)try{await q(e.groupId,_,O,j)}catch(E){r({type:"SET_ERROR_BANNER",message:`Bookmark failed: ${E.message}`})}};return window.addEventListener("keydown",i),()=>window.removeEventListener("keydown",i)},[e.recording,e.groupId,e.startedAt,e.tiles,r]);const v=m.useMemo(()=>new Set(e.tiles.map(i=>i.udid)),[e.tiles]),u=m.useMemo(()=>n.map(i=>Fe(i,w)),[n,w]),g=i=>{const d=Number(i.screenWidth),p=Number(i.screenHeight);return{udid:i.udid,name:i.name,mjpegPort:0,aspect:z(i),screenWidth:Number.isFinite(d)&&d>0?d:void 0,screenHeight:Number.isFinite(p)&&p>0?p:void 0,platform:i.platform}},o=async i=>{if(e.tiles.some(j=>j.udid===i)){await c(i);return}if(e.recording){r({type:"SET_ERROR_BANNER",message:"Stop recording before changing the mosaic layout."});return}const p=n.find(j=>j.udid===i);p&&(r({type:"ADD_TILE",tile:g(p)}),fetch(`/xenon/api/control/${encodeURIComponent(i)}/stream/start`,{method:"POST"}).catch(()=>{}))},c=async i=>{r({type:"REMOVE_TILE",udid:i});try{await fetch(`/xenon/api/control/${encodeURIComponent(i)}/stream/stop`,{method:"POST"})}catch{}f(d=>d+1)},s=async(i,d)=>{if(e.groupId)try{await Le(e.groupId,{recordingId:i,timecodeMs:e.startedAt?Date.now()-e.startedAt:0,shape:d.shape,geometry:JSON.stringify(d.geometry),color:d.color,text:d.text})}catch(p){r({type:"SET_ERROR_BANNER",message:`Annotation failed: ${p.message}`})}},x=()=>{for(const i of e.tiles){const d=`/xenon/api/control/${encodeURIComponent(i.udid)}/stream/stop`;try{navigator.sendBeacon?navigator.sendBeacon(d):fetch(d,{method:"POST",keepalive:!0}).catch(()=>{})}catch{}}e.tiles.forEach(i=>r({type:"REMOVE_TILE",udid:i.udid}))},R=We({idleAfterMs:Xe,warningSec:He,enabled:e.tiles.length>0,onWarning:()=>{},onTimeout:()=>{x()}});return t.jsxs(V.Provider,{value:{state:e,dispatch:r},children:[t.jsxs("div",{className:"flex flex-col h-full p-4 gap-3",children:[t.jsxs("header",{className:"flex items-center justify-between gap-3",children:[t.jsxs("div",{className:"flex items-center gap-3",children:[t.jsx("h1",{className:"text-base font-semibold",children:"Live Devices"}),t.jsx(ve,{value:e.layout,onChange:i=>r({type:"SET_LAYOUT",layout:i})})]}),t.jsx(Ue,{selectedUdids:e.tiles.map(i=>i.udid)})]}),e.errorBanner&&t.jsxs("div",{role:"alert",className:"text-sm rounded border border-red-500/40 bg-red-500/10 text-red-100 px-3 py-2 flex items-center gap-2",children:[t.jsx("span",{className:"flex-1",children:e.errorBanner}),t.jsx("button",{className:"text-xs opacity-80 hover:opacity-100",onClick:()=>r({type:"SET_ERROR_BANNER",message:null}),children:"Dismiss"})]}),t.jsxs("div",{className:"grid grid-cols-[260px_1fr] gap-3 flex-1 min-h-0",children:[t.jsxs("aside",{className:"flex flex-col gap-2 border border-[var(--border)] rounded p-3 overflow-y-auto min-h-0",children:[t.jsxs("div",{className:"flex items-center justify-between",children:[t.jsx("span",{className:"text-xs uppercase tracking-wide text-[var(--text-dim)]",children:"Devices"}),t.jsx("button",{className:"text-xs underline opacity-70 hover:opacity-100",onClick:()=>f(i=>i+1),children:"Refresh"})]}),t.jsx(ge,{devices:u,inMosaic:v,onToggle:o}),t.jsx("p",{className:"mt-2 text-[11px] text-[var(--text-dim)] leading-relaxed",children:"Click a device to add it to the mosaic. Click again (or use the × on the tile) to remove it."})]}),t.jsx("main",{className:"border border-[var(--border)] rounded p-2 overflow-y-auto h-full min-h-0",children:t.jsx(Ae,{layout:e.layout,tiles:e.tiles,annotateMode:e.annotateMode,shape:e.shape,color:e.color,onAnnotation:s,onRemove:c,onDropDevice:o})})]})]}),R.warning&&R.remainingSec!==null&&t.jsx(Ye,{remainingSec:R.remainingSec,onContinue:R.reset,onReleaseNow:x})]})}export{Je as default};