@mindexec/cli 0.2.10 → 0.2.12
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/package.json +4 -2
- package/remote-hub.js +429 -15
- package/scripts/remote-fleet-render-smoke.mjs +579 -0
- package/scripts/remote-hub-scale-smoke.mjs +109 -0
- package/server.js +32 -0
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-css3d-manager.js +2 -0
- package/wwwroot/index.html +1 -1
- package/wwwroot/service-worker-assets.js +3 -3
- package/wwwroot/service-worker.js +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mindexec/cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.12",
|
|
4
4
|
"description": "MindExec local runtime and bridge CLI",
|
|
5
5
|
"main": "server.js",
|
|
6
6
|
"type": "module",
|
|
@@ -20,8 +20,10 @@
|
|
|
20
20
|
"scripts": {
|
|
21
21
|
"start": "node launch-bridge.cjs",
|
|
22
22
|
"dev": "node launch-bridge.cjs --watch",
|
|
23
|
-
"test:syntax": "node --check server.js && node --check remote-hub.js && node --check codex-runtime.js && node --check launch-bridge.cjs && node --check port-guard.cjs && node --check scripts/setup-tree-sitter-grammars.mjs && node --check scripts/remote-hub-smoke.mjs",
|
|
23
|
+
"test:syntax": "node --check server.js && node --check remote-hub.js && node --check codex-runtime.js && node --check launch-bridge.cjs && node --check port-guard.cjs && node --check scripts/setup-tree-sitter-grammars.mjs && node --check scripts/remote-hub-smoke.mjs && node --check scripts/remote-hub-scale-smoke.mjs && node --check scripts/remote-fleet-render-smoke.mjs",
|
|
24
24
|
"test:remote": "node scripts/remote-hub-smoke.mjs",
|
|
25
|
+
"test:remote:scale": "node scripts/remote-hub-scale-smoke.mjs",
|
|
26
|
+
"test:remote:render": "node scripts/remote-fleet-render-smoke.mjs",
|
|
25
27
|
"pack:dry": "npm pack --dry-run",
|
|
26
28
|
"setup:grammars": "node scripts/setup-tree-sitter-grammars.mjs",
|
|
27
29
|
"postinstall": "npm run setup:grammars"
|
package/remote-hub.js
CHANGED
|
@@ -12,6 +12,8 @@ const MAX_AGENT_TASK_CHARS = 4000;
|
|
|
12
12
|
const MAX_AGENT_TASK_RESULT_CHARS = 3000;
|
|
13
13
|
const RECENT_TASK_LIMIT = 12;
|
|
14
14
|
const REMOTE_PROTOCOL_VERSION = 1;
|
|
15
|
+
const MAX_SYNTHETIC_DEVICES = 1000;
|
|
16
|
+
const SYNTHETIC_FRAME_DATA_URL = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAABCAYAAAD0In+KAAAADElEQVR42mP8z8AAAAMBAQDJ/pLvAAAAAElFTkSuQmCC';
|
|
15
17
|
|
|
16
18
|
function isEnabledValue(value, fallback = true) {
|
|
17
19
|
if (value === undefined || value === null || value === '') {
|
|
@@ -62,6 +64,25 @@ function readCapabilityFlag(capabilities, key) {
|
|
|
62
64
|
return /^(1|true|yes|on)$/i.test(String(value || '').trim());
|
|
63
65
|
}
|
|
64
66
|
|
|
67
|
+
function createDeviceCounters(overrides = {}) {
|
|
68
|
+
return {
|
|
69
|
+
messagesReceived: 0,
|
|
70
|
+
statusReceived: 0,
|
|
71
|
+
commandsSent: 0,
|
|
72
|
+
commandResultsReceived: 0,
|
|
73
|
+
thumbnailFramesReceived: 0,
|
|
74
|
+
thumbnailFramesDropped: 0,
|
|
75
|
+
liveFramesReceived: 0,
|
|
76
|
+
liveFramesDropped: 0,
|
|
77
|
+
liveStreamsStarted: 0,
|
|
78
|
+
liveStreamsStopped: 0,
|
|
79
|
+
tasksQueued: 0,
|
|
80
|
+
taskResultsReceived: 0,
|
|
81
|
+
taskResultsFailed: 0,
|
|
82
|
+
...overrides
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
65
86
|
function normalizeDeviceId(value) {
|
|
66
87
|
const id = safeString(value, 128).replace(/[^a-zA-Z0-9_.:-]/g, '-');
|
|
67
88
|
return id || crypto.randomUUID();
|
|
@@ -122,6 +143,7 @@ function serializeDevice(device) {
|
|
|
122
143
|
latestLiveFrame: device.latestLiveFrame ? { ...device.latestLiveFrame } : null,
|
|
123
144
|
activeLiveStream: device.activeLiveStream ? { ...device.activeLiveStream } : null,
|
|
124
145
|
latestTask: device.latestTask ? { ...device.latestTask } : null,
|
|
146
|
+
synthetic: device.synthetic === true,
|
|
125
147
|
recentTasks: Array.isArray(device.recentTasks)
|
|
126
148
|
? device.recentTasks.map(task => ({ ...task }))
|
|
127
149
|
: [],
|
|
@@ -207,6 +229,77 @@ export function createRemoteHub(options = {}) {
|
|
|
207
229
|
});
|
|
208
230
|
}
|
|
209
231
|
|
|
232
|
+
function makeSyntheticFrame(device, streamId, mode = 'thumbnail', options = {}) {
|
|
233
|
+
const now = new Date().toISOString();
|
|
234
|
+
const frameSeq = Number(device?.counters?.thumbnailFramesReceived || 0)
|
|
235
|
+
+ Number(device?.counters?.liveFramesReceived || 0)
|
|
236
|
+
+ 1;
|
|
237
|
+
return {
|
|
238
|
+
streamId: safeString(streamId, 128) || `${mode}-${Date.now()}`,
|
|
239
|
+
frameSeq,
|
|
240
|
+
commandId: safeString(options.commandId, 128),
|
|
241
|
+
width: clampNumber(options.width, 2, 2560, mode === 'remote-fast' ? 960 : 360),
|
|
242
|
+
height: clampNumber(options.height, 1, 1440, mode === 'remote-fast' ? 540 : 220),
|
|
243
|
+
mimeType: 'image/png',
|
|
244
|
+
format: 'image/png',
|
|
245
|
+
mode,
|
|
246
|
+
fps: clampNumber(options.fps, 1, 24, mode === 'remote-fast' ? 12 : 1),
|
|
247
|
+
capturedAt: now,
|
|
248
|
+
receivedAt: now,
|
|
249
|
+
byteLength: 68,
|
|
250
|
+
dataUrl: SYNTHETIC_FRAME_DATA_URL
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function applySyntheticThumbnail(device, command) {
|
|
255
|
+
if (!device) {
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const payload = command?.payload || {};
|
|
260
|
+
const frame = makeSyntheticFrame(device, payload.streamId || 'synthetic-thumb', 'thumbnail', {
|
|
261
|
+
commandId: command?.commandId,
|
|
262
|
+
width: payload.maxWidth || 360,
|
|
263
|
+
height: payload.maxHeight || 220
|
|
264
|
+
});
|
|
265
|
+
delete frame.mode;
|
|
266
|
+
delete frame.fps;
|
|
267
|
+
device.latestThumbnail = frame;
|
|
268
|
+
device.lastSeenAt = frame.receivedAt;
|
|
269
|
+
device.counters.thumbnailFramesReceived += 1;
|
|
270
|
+
emitRemoteEvent('RemoteFrameReceived', device, {
|
|
271
|
+
streamId: frame.streamId,
|
|
272
|
+
frameSeq: frame.frameSeq,
|
|
273
|
+
synthetic: true
|
|
274
|
+
});
|
|
275
|
+
return frame;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function applySyntheticLiveFrame(device, streamId, options = {}) {
|
|
279
|
+
if (!device?.activeLiveStream?.active) {
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const frame = makeSyntheticFrame(device, streamId || device.activeLiveStream.streamId, 'remote-fast', {
|
|
284
|
+
commandId: options.commandId || device.activeLiveStream.commandId,
|
|
285
|
+
width: options.maxWidth || 960,
|
|
286
|
+
height: options.maxHeight || 540,
|
|
287
|
+
fps: options.fps || device.activeLiveStream.fps
|
|
288
|
+
});
|
|
289
|
+
device.latestLiveFrame = frame;
|
|
290
|
+
device.activeLiveStream.lastFrameAt = frame.receivedAt;
|
|
291
|
+
device.activeLiveStream.lastFrameSeq = frame.frameSeq;
|
|
292
|
+
device.activeLiveStream.framesReceived = (device.activeLiveStream.framesReceived || 0) + 1;
|
|
293
|
+
device.lastSeenAt = frame.receivedAt;
|
|
294
|
+
device.counters.liveFramesReceived += 1;
|
|
295
|
+
emitRemoteEvent('RemoteFrameReceived', device, {
|
|
296
|
+
streamId: frame.streamId,
|
|
297
|
+
frameSeq: frame.frameSeq,
|
|
298
|
+
synthetic: true
|
|
299
|
+
});
|
|
300
|
+
return frame;
|
|
301
|
+
}
|
|
302
|
+
|
|
210
303
|
function closeExistingDeviceSocket(deviceId, nextSessionId) {
|
|
211
304
|
const existing = devices.get(deviceId);
|
|
212
305
|
if (!existing?.socket || existing.socket.destroyed) {
|
|
@@ -222,6 +315,186 @@ export function createRemoteHub(options = {}) {
|
|
|
222
315
|
existing.socket.destroy();
|
|
223
316
|
}
|
|
224
317
|
|
|
318
|
+
function clearSyntheticFleet() {
|
|
319
|
+
let removed = 0;
|
|
320
|
+
for (const [deviceId, device] of devices.entries()) {
|
|
321
|
+
if (device.synthetic === true) {
|
|
322
|
+
devices.delete(deviceId);
|
|
323
|
+
removed += 1;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return { ok: true, removed, total: devices.size };
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function seedSyntheticFleet(options = {}) {
|
|
331
|
+
const count = clampNumber(options.count, 1, MAX_SYNTHETIC_DEVICES, 250);
|
|
332
|
+
const connectedRatio = Math.max(0, Math.min(1, Number(options.connectedRatio ?? 0.88)));
|
|
333
|
+
const thumbnailRatio = Math.max(0, Math.min(1, Number(options.thumbnailRatio ?? 0.7)));
|
|
334
|
+
const aiAssistRatio = Math.max(0, Math.min(1, Number(options.aiAssistRatio ?? 0.35)));
|
|
335
|
+
const liveCount = clampNumber(options.liveCount, 0, Math.min(count, 24), Math.min(6, count));
|
|
336
|
+
const replace = options.replace !== false;
|
|
337
|
+
const now = new Date();
|
|
338
|
+
const platforms = ['win32', 'linux', 'darwin'];
|
|
339
|
+
|
|
340
|
+
if (replace) {
|
|
341
|
+
clearSyntheticFleet();
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
let connected = 0;
|
|
345
|
+
let aiAssist = 0;
|
|
346
|
+
let live = 0;
|
|
347
|
+
for (let index = 0; index < count; index += 1) {
|
|
348
|
+
const ordinal = index + 1;
|
|
349
|
+
const deviceId = `synthetic-${String(ordinal).padStart(4, '0')}`;
|
|
350
|
+
const isConnected = index / count < connectedRatio;
|
|
351
|
+
const hasThumbnail = index / count < thumbnailRatio;
|
|
352
|
+
const hasAiAssist = index / count < aiAssistRatio;
|
|
353
|
+
const isLive = isConnected && index < liveCount;
|
|
354
|
+
const seenAt = new Date(now.getTime() - index * 1350).toISOString();
|
|
355
|
+
const connectedAt = new Date(now.getTime() - (index + 8) * 60000).toISOString();
|
|
356
|
+
const platform = platforms[index % platforms.length];
|
|
357
|
+
const usedMemRatio = Number((0.18 + (index % 71) / 100).toFixed(2));
|
|
358
|
+
const load1 = Number(((index % 19) / 3).toFixed(2));
|
|
359
|
+
const taskStatus = index % 17 === 0
|
|
360
|
+
? 'failed'
|
|
361
|
+
: index % 5 === 0
|
|
362
|
+
? 'completed'
|
|
363
|
+
: index % 7 === 0
|
|
364
|
+
? 'queued'
|
|
365
|
+
: '';
|
|
366
|
+
const latestTask = taskStatus
|
|
367
|
+
? {
|
|
368
|
+
taskId: `synthetic-task-${ordinal}`,
|
|
369
|
+
commandId: `synthetic-command-${ordinal}`,
|
|
370
|
+
title: taskStatus === 'failed' ? 'Synthetic issue check' : 'Synthetic status task',
|
|
371
|
+
instructionPreview: 'Synthetic fleet scale validation task.',
|
|
372
|
+
status: taskStatus,
|
|
373
|
+
approvalLevel: hasAiAssist ? 'ai-assist' : 'task-only',
|
|
374
|
+
requestedAt: seenAt,
|
|
375
|
+
sentAt: seenAt,
|
|
376
|
+
updatedAt: seenAt,
|
|
377
|
+
completedAt: taskStatus === 'completed' || taskStatus === 'failed' ? seenAt : '',
|
|
378
|
+
error: taskStatus === 'failed' ? 'Synthetic task failure sample.' : '',
|
|
379
|
+
resultKind: 'synthetic-agent-task',
|
|
380
|
+
resultSummary: taskStatus === 'failed'
|
|
381
|
+
? 'Synthetic task reported a sample issue.'
|
|
382
|
+
: 'Synthetic task completed for scale validation.'
|
|
383
|
+
}
|
|
384
|
+
: null;
|
|
385
|
+
const device = {
|
|
386
|
+
socket: null,
|
|
387
|
+
synthetic: true,
|
|
388
|
+
deviceId,
|
|
389
|
+
sessionId: `synthetic-session-${ordinal}`,
|
|
390
|
+
deviceName: `Synthetic PC ${String(ordinal).padStart(4, '0')}`,
|
|
391
|
+
hostname: `synthetic-host-${String(ordinal).padStart(4, '0')}`,
|
|
392
|
+
platform,
|
|
393
|
+
arch: index % 4 === 0 ? 'arm64' : 'x64',
|
|
394
|
+
pid: 0,
|
|
395
|
+
agentVersion: 'synthetic-scale',
|
|
396
|
+
capabilities: {
|
|
397
|
+
status: true,
|
|
398
|
+
thumbnail: hasThumbnail,
|
|
399
|
+
control: false,
|
|
400
|
+
liveStream: true,
|
|
401
|
+
computerAgent: true,
|
|
402
|
+
taskDispatch: true,
|
|
403
|
+
aiAssist: hasAiAssist,
|
|
404
|
+
aiModel: hasAiAssist ? 'synthetic-ai' : '',
|
|
405
|
+
aiProvider: hasAiAssist ? 'synthetic' : ''
|
|
406
|
+
},
|
|
407
|
+
connected: isConnected,
|
|
408
|
+
connectedAt: isConnected ? connectedAt : '',
|
|
409
|
+
disconnectedAt: isConnected ? '' : seenAt,
|
|
410
|
+
lastSeenAt: seenAt,
|
|
411
|
+
lastStatusAt: seenAt,
|
|
412
|
+
lastDisconnectReason: isConnected ? '' : 'synthetic-offline',
|
|
413
|
+
remoteAddress: 'synthetic.local',
|
|
414
|
+
remotePort: 0,
|
|
415
|
+
status: {
|
|
416
|
+
platform,
|
|
417
|
+
release: platform === 'win32' ? 'Windows 11' : platform === 'darwin' ? 'macOS' : 'Linux',
|
|
418
|
+
uptimeSec: (ordinal * 791) % 1209600,
|
|
419
|
+
usedMemRatio,
|
|
420
|
+
loadavg: [load1, Math.max(0, load1 - 0.2), Math.max(0, load1 - 0.4)]
|
|
421
|
+
},
|
|
422
|
+
latestThumbnail: null,
|
|
423
|
+
latestLiveFrame: null,
|
|
424
|
+
activeLiveStream: null,
|
|
425
|
+
latestTask,
|
|
426
|
+
recentTasks: latestTask ? [latestTask] : [],
|
|
427
|
+
pendingTaskCommands: new Map(),
|
|
428
|
+
counters: createDeviceCounters({
|
|
429
|
+
messagesReceived: 1,
|
|
430
|
+
statusReceived: 1,
|
|
431
|
+
tasksQueued: latestTask ? 1 : 0,
|
|
432
|
+
taskResultsReceived: latestTask && ['completed', 'failed'].includes(latestTask.status) ? 1 : 0,
|
|
433
|
+
taskResultsFailed: latestTask?.status === 'failed' ? 1 : 0
|
|
434
|
+
})
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
if (hasThumbnail) {
|
|
438
|
+
const frame = makeSyntheticFrame(device, `synthetic-thumb-${ordinal}`, 'thumbnail', {
|
|
439
|
+
width: 360,
|
|
440
|
+
height: 220
|
|
441
|
+
});
|
|
442
|
+
delete frame.mode;
|
|
443
|
+
delete frame.fps;
|
|
444
|
+
frame.receivedAt = seenAt;
|
|
445
|
+
frame.capturedAt = seenAt;
|
|
446
|
+
device.latestThumbnail = frame;
|
|
447
|
+
device.counters.thumbnailFramesReceived = 1;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
if (isLive) {
|
|
451
|
+
device.activeLiveStream = {
|
|
452
|
+
streamId: `synthetic-live-${ordinal}`,
|
|
453
|
+
commandId: `synthetic-live-command-${ordinal}`,
|
|
454
|
+
active: true,
|
|
455
|
+
mode: 'remote-fast',
|
|
456
|
+
fps: 12,
|
|
457
|
+
startedAt: seenAt,
|
|
458
|
+
stoppedAt: '',
|
|
459
|
+
stopReason: '',
|
|
460
|
+
lastFrameAt: '',
|
|
461
|
+
lastFrameSeq: 0,
|
|
462
|
+
framesReceived: 0
|
|
463
|
+
};
|
|
464
|
+
applySyntheticLiveFrame(device, device.activeLiveStream.streamId, {
|
|
465
|
+
commandId: device.activeLiveStream.commandId,
|
|
466
|
+
fps: 12
|
|
467
|
+
});
|
|
468
|
+
live += 1;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
if (isConnected) {
|
|
472
|
+
connected += 1;
|
|
473
|
+
}
|
|
474
|
+
if (hasAiAssist) {
|
|
475
|
+
aiAssist += 1;
|
|
476
|
+
}
|
|
477
|
+
devices.set(deviceId, device);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
emitRemoteEvent('RemoteSyntheticFleetSeeded', null, {
|
|
481
|
+
count,
|
|
482
|
+
connected,
|
|
483
|
+
aiAssist,
|
|
484
|
+
live
|
|
485
|
+
});
|
|
486
|
+
return {
|
|
487
|
+
ok: true,
|
|
488
|
+
synthetic: true,
|
|
489
|
+
seeded: count,
|
|
490
|
+
connected,
|
|
491
|
+
aiAssist,
|
|
492
|
+
live,
|
|
493
|
+
total: devices.size,
|
|
494
|
+
max: MAX_SYNTHETIC_DEVICES
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
|
|
225
498
|
function attachDevice(socket, hello) {
|
|
226
499
|
const now = new Date().toISOString();
|
|
227
500
|
const sessionId = crypto.randomUUID();
|
|
@@ -257,21 +530,8 @@ export function createRemoteHub(options = {}) {
|
|
|
257
530
|
latestTask: null,
|
|
258
531
|
recentTasks: [],
|
|
259
532
|
pendingTaskCommands: new Map(),
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
statusReceived: 0,
|
|
263
|
-
commandsSent: 0,
|
|
264
|
-
commandResultsReceived: 0,
|
|
265
|
-
thumbnailFramesReceived: 0,
|
|
266
|
-
thumbnailFramesDropped: 0,
|
|
267
|
-
liveFramesReceived: 0,
|
|
268
|
-
liveFramesDropped: 0,
|
|
269
|
-
liveStreamsStarted: 0,
|
|
270
|
-
liveStreamsStopped: 0,
|
|
271
|
-
tasksQueued: 0,
|
|
272
|
-
taskResultsReceived: 0,
|
|
273
|
-
taskResultsFailed: 0
|
|
274
|
-
}
|
|
533
|
+
synthetic: false,
|
|
534
|
+
counters: createDeviceCounters({ messagesReceived: 1 })
|
|
275
535
|
};
|
|
276
536
|
|
|
277
537
|
devices.set(deviceId, device);
|
|
@@ -685,6 +945,19 @@ export function createRemoteHub(options = {}) {
|
|
|
685
945
|
|
|
686
946
|
function disconnectDevice(deviceId, reason = 'manager-disconnect') {
|
|
687
947
|
const device = devices.get(String(deviceId || ''));
|
|
948
|
+
if (device?.synthetic === true) {
|
|
949
|
+
device.connected = false;
|
|
950
|
+
device.disconnectedAt = new Date().toISOString();
|
|
951
|
+
device.lastDisconnectReason = reason;
|
|
952
|
+
if (device.activeLiveStream) {
|
|
953
|
+
device.activeLiveStream.active = false;
|
|
954
|
+
device.activeLiveStream.stoppedAt = device.disconnectedAt;
|
|
955
|
+
device.activeLiveStream.stopReason = reason;
|
|
956
|
+
}
|
|
957
|
+
emitRemoteEvent('RemoteDeviceDisconnected', device, { reason, synthetic: true });
|
|
958
|
+
return true;
|
|
959
|
+
}
|
|
960
|
+
|
|
688
961
|
if (!device?.socket || device.socket.destroyed) {
|
|
689
962
|
return false;
|
|
690
963
|
}
|
|
@@ -696,6 +969,29 @@ export function createRemoteHub(options = {}) {
|
|
|
696
969
|
|
|
697
970
|
function sendCommand(deviceId, command) {
|
|
698
971
|
const device = devices.get(String(deviceId || ''));
|
|
972
|
+
if (device?.synthetic === true && device.connected) {
|
|
973
|
+
const commandId = safeString(command?.commandId, 128) || crypto.randomUUID();
|
|
974
|
+
device.counters.commandsSent += 1;
|
|
975
|
+
device.lastSeenAt = new Date().toISOString();
|
|
976
|
+
if (command?.command === 'thumbnail.capture') {
|
|
977
|
+
applySyntheticThumbnail(device, {
|
|
978
|
+
commandId,
|
|
979
|
+
payload: command?.payload || {}
|
|
980
|
+
});
|
|
981
|
+
}
|
|
982
|
+
emitRemoteEvent('RemoteCommandQueued', device, {
|
|
983
|
+
commandId,
|
|
984
|
+
command: safeString(command?.command || 'ping', 80),
|
|
985
|
+
synthetic: true
|
|
986
|
+
});
|
|
987
|
+
emitRemoteEvent('RemoteCommandResult', device, {
|
|
988
|
+
commandId,
|
|
989
|
+
result: { ok: true, synthetic: true },
|
|
990
|
+
error: ''
|
|
991
|
+
});
|
|
992
|
+
return { ok: true, commandId, synthetic: true };
|
|
993
|
+
}
|
|
994
|
+
|
|
699
995
|
if (!device?.socket || device.socket.destroyed || !device.connected) {
|
|
700
996
|
return { ok: false, error: 'device-not-connected' };
|
|
701
997
|
}
|
|
@@ -720,6 +1016,65 @@ export function createRemoteHub(options = {}) {
|
|
|
720
1016
|
|
|
721
1017
|
function requestAgentTask(deviceId, options = {}) {
|
|
722
1018
|
const device = devices.get(String(deviceId || ''));
|
|
1019
|
+
if (device?.synthetic === true && device.connected) {
|
|
1020
|
+
const instruction = safeText(options.instruction, MAX_AGENT_TASK_CHARS);
|
|
1021
|
+
if (!instruction) {
|
|
1022
|
+
return { ok: false, error: 'missing-instruction' };
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
const now = new Date().toISOString();
|
|
1026
|
+
const approvalLevel = normalizeApprovalLevel(options.approvalLevel);
|
|
1027
|
+
if (approvalLevel === 'ai-assist' && !readCapabilityFlag(device.capabilities, 'aiAssist')) {
|
|
1028
|
+
return { ok: false, error: 'device-ai-assist-unavailable' };
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
const commandId = safeString(options.commandId, 128) || crypto.randomUUID();
|
|
1032
|
+
const taskId = safeString(options.taskId, 128) || crypto.randomUUID();
|
|
1033
|
+
const title = safeString(options.title, 120)
|
|
1034
|
+
|| safeString(instruction.split(/\r?\n/)[0], 120)
|
|
1035
|
+
|| 'Remote task';
|
|
1036
|
+
const task = {
|
|
1037
|
+
taskId,
|
|
1038
|
+
commandId,
|
|
1039
|
+
title,
|
|
1040
|
+
instructionPreview: safeText(instruction, 320),
|
|
1041
|
+
status: 'completed',
|
|
1042
|
+
approvalLevel,
|
|
1043
|
+
requestedAt: now,
|
|
1044
|
+
sentAt: now,
|
|
1045
|
+
updatedAt: now,
|
|
1046
|
+
completedAt: now,
|
|
1047
|
+
error: '',
|
|
1048
|
+
resultKind: approvalLevel === 'ai-assist' ? 'synthetic-ai-assist' : 'synthetic-agent-task',
|
|
1049
|
+
resultSummary: approvalLevel === 'ai-assist'
|
|
1050
|
+
? `Synthetic AI assist completed for ${device.deviceName}.`
|
|
1051
|
+
: `Synthetic task accepted by ${device.deviceName}.`
|
|
1052
|
+
};
|
|
1053
|
+
|
|
1054
|
+
device.counters.commandsSent += 1;
|
|
1055
|
+
device.counters.commandResultsReceived += 1;
|
|
1056
|
+
device.counters.tasksQueued += 1;
|
|
1057
|
+
device.counters.taskResultsReceived += 1;
|
|
1058
|
+
device.lastSeenAt = now;
|
|
1059
|
+
rememberDeviceTask(device, task);
|
|
1060
|
+
device.pendingTaskCommands.delete(commandId);
|
|
1061
|
+
emitRemoteEvent('RemoteTaskQueued', device, {
|
|
1062
|
+
commandId,
|
|
1063
|
+
taskId,
|
|
1064
|
+
title,
|
|
1065
|
+
approvalLevel,
|
|
1066
|
+
synthetic: true
|
|
1067
|
+
});
|
|
1068
|
+
emitRemoteEvent('RemoteTaskResult', device, {
|
|
1069
|
+
commandId,
|
|
1070
|
+
taskId,
|
|
1071
|
+
status: task.status,
|
|
1072
|
+
error: '',
|
|
1073
|
+
synthetic: true
|
|
1074
|
+
});
|
|
1075
|
+
return { ok: true, commandId, taskId, approvalLevel, synthetic: true };
|
|
1076
|
+
}
|
|
1077
|
+
|
|
723
1078
|
if (!device?.socket || device.socket.destroyed || !device.connected) {
|
|
724
1079
|
return { ok: false, error: 'device-not-connected' };
|
|
725
1080
|
}
|
|
@@ -803,6 +1158,45 @@ export function createRemoteHub(options = {}) {
|
|
|
803
1158
|
|
|
804
1159
|
function startLiveStream(deviceId, options = {}) {
|
|
805
1160
|
const device = devices.get(String(deviceId || ''));
|
|
1161
|
+
if (device?.synthetic === true && device.connected) {
|
|
1162
|
+
if (!readCapabilityFlag(device.capabilities, 'liveStream')) {
|
|
1163
|
+
return { ok: false, error: 'device-live-stream-unavailable' };
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
const now = new Date().toISOString();
|
|
1167
|
+
const streamId = safeString(options.streamId, 128) || `live-${Date.now()}`;
|
|
1168
|
+
const fps = clampNumber(options.fps, 1, 24, 12);
|
|
1169
|
+
const commandId = safeString(options.commandId, 128) || crypto.randomUUID();
|
|
1170
|
+
device.counters.commandsSent += 1;
|
|
1171
|
+
device.activeLiveStream = {
|
|
1172
|
+
streamId,
|
|
1173
|
+
commandId,
|
|
1174
|
+
active: true,
|
|
1175
|
+
mode: 'remote-fast',
|
|
1176
|
+
fps,
|
|
1177
|
+
startedAt: now,
|
|
1178
|
+
stoppedAt: '',
|
|
1179
|
+
stopReason: '',
|
|
1180
|
+
lastFrameAt: '',
|
|
1181
|
+
lastFrameSeq: 0,
|
|
1182
|
+
framesReceived: 0
|
|
1183
|
+
};
|
|
1184
|
+
device.counters.liveStreamsStarted += 1;
|
|
1185
|
+
applySyntheticLiveFrame(device, streamId, {
|
|
1186
|
+
commandId,
|
|
1187
|
+
maxWidth: options.maxWidth,
|
|
1188
|
+
maxHeight: options.maxHeight,
|
|
1189
|
+
fps
|
|
1190
|
+
});
|
|
1191
|
+
emitRemoteEvent('RemoteLiveStreamStarted', device, {
|
|
1192
|
+
streamId,
|
|
1193
|
+
commandId,
|
|
1194
|
+
fps,
|
|
1195
|
+
synthetic: true
|
|
1196
|
+
});
|
|
1197
|
+
return { ok: true, commandId, streamId, fps, synthetic: true };
|
|
1198
|
+
}
|
|
1199
|
+
|
|
806
1200
|
if (!device?.socket || device.socket.destroyed || !device.connected) {
|
|
807
1201
|
return { ok: false, error: 'device-not-connected' };
|
|
808
1202
|
}
|
|
@@ -857,6 +1251,24 @@ export function createRemoteHub(options = {}) {
|
|
|
857
1251
|
|
|
858
1252
|
function stopLiveStream(deviceId, options = {}) {
|
|
859
1253
|
const device = devices.get(String(deviceId || ''));
|
|
1254
|
+
if (device?.synthetic === true && device.connected) {
|
|
1255
|
+
const streamId = safeString(options.streamId, 128) || device.activeLiveStream?.streamId || '';
|
|
1256
|
+
const commandId = safeString(options.commandId, 128) || crypto.randomUUID();
|
|
1257
|
+
device.counters.commandsSent += 1;
|
|
1258
|
+
if (device.activeLiveStream && (!streamId || device.activeLiveStream.streamId === streamId)) {
|
|
1259
|
+
device.activeLiveStream.active = false;
|
|
1260
|
+
device.activeLiveStream.stoppedAt = new Date().toISOString();
|
|
1261
|
+
device.activeLiveStream.stopReason = 'manager-request';
|
|
1262
|
+
}
|
|
1263
|
+
device.counters.liveStreamsStopped += 1;
|
|
1264
|
+
emitRemoteEvent('RemoteLiveStreamStopped', device, {
|
|
1265
|
+
streamId,
|
|
1266
|
+
commandId,
|
|
1267
|
+
synthetic: true
|
|
1268
|
+
});
|
|
1269
|
+
return { ok: true, commandId, streamId, synthetic: true };
|
|
1270
|
+
}
|
|
1271
|
+
|
|
860
1272
|
if (!device?.socket || device.socket.destroyed || !device.connected) {
|
|
861
1273
|
return { ok: false, error: 'device-not-connected' };
|
|
862
1274
|
}
|
|
@@ -913,6 +1325,8 @@ export function createRemoteHub(options = {}) {
|
|
|
913
1325
|
stopLiveStream,
|
|
914
1326
|
getDeviceLiveFrame,
|
|
915
1327
|
getDeviceThumbnail,
|
|
1328
|
+
seedSyntheticFleet,
|
|
1329
|
+
clearSyntheticFleet,
|
|
916
1330
|
getPairToken: () => pairToken
|
|
917
1331
|
};
|
|
918
1332
|
}
|
|
@@ -0,0 +1,579 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import assert from 'node:assert/strict';
|
|
4
|
+
import fs from 'node:fs/promises';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import vm from 'node:vm';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import { createRemoteHub } from '../remote-hub.js';
|
|
9
|
+
|
|
10
|
+
const SYNTHETIC_COUNT = Number(process.env.REMOTE_FLEET_RENDER_SMOKE_COUNT || 250);
|
|
11
|
+
|
|
12
|
+
function dataAttributeToDatasetKey(name) {
|
|
13
|
+
return String(name || '').replace(/^data-/, '').replace(/-([a-z])/g, (_, char) => char.toUpperCase());
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
class MiniStyle {
|
|
17
|
+
constructor() {
|
|
18
|
+
this._cssText = '';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
get cssText() {
|
|
22
|
+
return this._cssText;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
set cssText(value) {
|
|
26
|
+
this._cssText = String(value || '');
|
|
27
|
+
const displayMatch = /(?:^|;)\s*display\s*:\s*([^;]+)/i.exec(this._cssText);
|
|
28
|
+
if (displayMatch) {
|
|
29
|
+
this.display = displayMatch[1].trim();
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
setProperty(name, value) {
|
|
34
|
+
this[String(name)] = String(value);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
class MiniClassList {
|
|
39
|
+
constructor(owner) {
|
|
40
|
+
this.owner = owner;
|
|
41
|
+
this.tokens = new Set();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
add(...tokens) {
|
|
45
|
+
tokens.forEach(token => {
|
|
46
|
+
const value = String(token || '').trim();
|
|
47
|
+
if (value) {
|
|
48
|
+
this.tokens.add(value);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
this.sync();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
remove(...tokens) {
|
|
55
|
+
tokens.forEach(token => this.tokens.delete(String(token || '').trim()));
|
|
56
|
+
this.sync();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
contains(token) {
|
|
60
|
+
return this.tokens.has(String(token || '').trim());
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
toggle(token, force) {
|
|
64
|
+
const value = String(token || '').trim();
|
|
65
|
+
if (!value) return false;
|
|
66
|
+
const shouldAdd = force === undefined ? !this.tokens.has(value) : !!force;
|
|
67
|
+
if (shouldAdd) {
|
|
68
|
+
this.tokens.add(value);
|
|
69
|
+
} else {
|
|
70
|
+
this.tokens.delete(value);
|
|
71
|
+
}
|
|
72
|
+
this.sync();
|
|
73
|
+
return shouldAdd;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
sync() {
|
|
77
|
+
this.owner.className = [...this.tokens].join(' ');
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
class MiniElement {
|
|
82
|
+
constructor(tagName, ownerDocument) {
|
|
83
|
+
this.tagName = String(tagName || '').toUpperCase();
|
|
84
|
+
this.nodeName = this.tagName;
|
|
85
|
+
this.ownerDocument = ownerDocument;
|
|
86
|
+
this.parentElement = null;
|
|
87
|
+
this.children = [];
|
|
88
|
+
this.attributes = {};
|
|
89
|
+
this.dataset = {};
|
|
90
|
+
this.style = new MiniStyle();
|
|
91
|
+
this.classList = new MiniClassList(this);
|
|
92
|
+
this.className = '';
|
|
93
|
+
this._textContent = '';
|
|
94
|
+
this._listeners = new Map();
|
|
95
|
+
this.value = '';
|
|
96
|
+
this.checked = false;
|
|
97
|
+
this.disabled = false;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
get textContent() {
|
|
101
|
+
return `${this._textContent}${this.children.map(child => child.textContent).join('')}`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
set textContent(value) {
|
|
105
|
+
this.children.forEach(child => {
|
|
106
|
+
child.parentElement = null;
|
|
107
|
+
});
|
|
108
|
+
this.children = [];
|
|
109
|
+
this._textContent = String(value ?? '');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
get innerHTML() {
|
|
113
|
+
return '';
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
set innerHTML(_value) {
|
|
117
|
+
this.children.forEach(child => {
|
|
118
|
+
child.parentElement = null;
|
|
119
|
+
});
|
|
120
|
+
this.children = [];
|
|
121
|
+
this._textContent = '';
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
appendChild(child) {
|
|
125
|
+
if (!child) return child;
|
|
126
|
+
if (child.parentElement) {
|
|
127
|
+
child.parentElement.children = child.parentElement.children.filter(item => item !== child);
|
|
128
|
+
}
|
|
129
|
+
child.parentElement = this;
|
|
130
|
+
this.children.push(child);
|
|
131
|
+
return child;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
remove() {
|
|
135
|
+
if (!this.parentElement) return;
|
|
136
|
+
this.parentElement.children = this.parentElement.children.filter(child => child !== this);
|
|
137
|
+
this.parentElement = null;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
setAttribute(name, value) {
|
|
141
|
+
const key = String(name || '');
|
|
142
|
+
const text = String(value ?? '');
|
|
143
|
+
this.attributes[key] = text;
|
|
144
|
+
if (key === 'id') {
|
|
145
|
+
this.id = text;
|
|
146
|
+
} else if (key === 'class') {
|
|
147
|
+
this.className = text;
|
|
148
|
+
this.classList.tokens = new Set(text.split(/\s+/).filter(Boolean));
|
|
149
|
+
} else if (key.startsWith('data-')) {
|
|
150
|
+
this.dataset[dataAttributeToDatasetKey(key)] = text;
|
|
151
|
+
} else if (key === 'aria-label') {
|
|
152
|
+
this.ariaLabel = text;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
getAttribute(name) {
|
|
157
|
+
const key = String(name || '');
|
|
158
|
+
if (key.startsWith('data-')) {
|
|
159
|
+
return this.dataset[dataAttributeToDatasetKey(key)];
|
|
160
|
+
}
|
|
161
|
+
return this.attributes[key];
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
addEventListener(type, listener) {
|
|
165
|
+
const key = String(type || '');
|
|
166
|
+
if (!this._listeners.has(key)) {
|
|
167
|
+
this._listeners.set(key, []);
|
|
168
|
+
}
|
|
169
|
+
this._listeners.get(key).push(listener);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
dispatchEvent(event) {
|
|
173
|
+
const payload = typeof event === 'string' ? { type: event } : { ...(event || {}) };
|
|
174
|
+
payload.type = payload.type || 'event';
|
|
175
|
+
payload.target = payload.target || this;
|
|
176
|
+
payload.currentTarget = this;
|
|
177
|
+
payload.preventDefault = payload.preventDefault || (() => {});
|
|
178
|
+
payload.stopPropagation = payload.stopPropagation || (() => {});
|
|
179
|
+
for (const listener of this._listeners.get(payload.type) || []) {
|
|
180
|
+
listener(payload);
|
|
181
|
+
}
|
|
182
|
+
return true;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
querySelector(selector) {
|
|
186
|
+
return this.querySelectorAll(selector)[0] || null;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
querySelectorAll(selector) {
|
|
190
|
+
const matches = [];
|
|
191
|
+
const visit = element => {
|
|
192
|
+
for (const child of element.children) {
|
|
193
|
+
if (matchesSelector(child, selector)) {
|
|
194
|
+
matches.push(child);
|
|
195
|
+
}
|
|
196
|
+
visit(child);
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
visit(this);
|
|
200
|
+
return matches;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
closest(selector) {
|
|
204
|
+
let current = this;
|
|
205
|
+
while (current) {
|
|
206
|
+
if (matchesSelector(current, selector)) {
|
|
207
|
+
return current;
|
|
208
|
+
}
|
|
209
|
+
current = current.parentElement;
|
|
210
|
+
}
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
contains(node) {
|
|
215
|
+
let current = node;
|
|
216
|
+
while (current) {
|
|
217
|
+
if (current === this) return true;
|
|
218
|
+
current = current.parentElement;
|
|
219
|
+
}
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
class MiniDocument {
|
|
225
|
+
constructor() {
|
|
226
|
+
this.body = new MiniElement('body', this);
|
|
227
|
+
this.head = new MiniElement('head', this);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
createElement(tagName) {
|
|
231
|
+
return new MiniElement(tagName, this);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
getElementById(id) {
|
|
235
|
+
const target = String(id || '');
|
|
236
|
+
return [this.head, this.body]
|
|
237
|
+
.flatMap(root => root.querySelectorAll('*'))
|
|
238
|
+
.find(element => element.id === target) || null;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
querySelector(selector) {
|
|
242
|
+
return this.body.querySelector(selector) || this.head.querySelector(selector);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
querySelectorAll(selector) {
|
|
246
|
+
return [
|
|
247
|
+
...this.head.querySelectorAll(selector),
|
|
248
|
+
...this.body.querySelectorAll(selector)
|
|
249
|
+
];
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function matchesSelector(element, selector) {
|
|
254
|
+
const text = String(selector || '').trim();
|
|
255
|
+
if (!text || !element) return false;
|
|
256
|
+
if (text === '*') return true;
|
|
257
|
+
|
|
258
|
+
const tagMatch = /^[a-zA-Z][\w-]*/.exec(text);
|
|
259
|
+
if (tagMatch && element.tagName.toLowerCase() !== tagMatch[0].toLowerCase()) {
|
|
260
|
+
return false;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
for (const classMatch of text.matchAll(/\.([\w-]+)/g)) {
|
|
264
|
+
if (!element.classList.contains(classMatch[1])) {
|
|
265
|
+
return false;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
for (const attrMatch of text.matchAll(/\[([^\]=]+)(?:="([^"]*)")?\]/g)) {
|
|
270
|
+
const attrName = attrMatch[1];
|
|
271
|
+
const expected = attrMatch[2];
|
|
272
|
+
const actual = attrName.startsWith('data-')
|
|
273
|
+
? element.dataset[dataAttributeToDatasetKey(attrName)]
|
|
274
|
+
: element.getAttribute(attrName);
|
|
275
|
+
if (actual === undefined || actual === null) {
|
|
276
|
+
return false;
|
|
277
|
+
}
|
|
278
|
+
if (expected !== undefined && String(actual) !== expected) {
|
|
279
|
+
return false;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return true;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function readStatusNumber(status, key) {
|
|
287
|
+
const value = status?.[key];
|
|
288
|
+
return Number.isFinite(Number(value)) ? Number(value) : null;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function readLoad1(status) {
|
|
292
|
+
const value = status?.loadavg;
|
|
293
|
+
return Array.isArray(value) && Number.isFinite(Number(value[0])) ? Number(value[0]) : null;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function projectDevice(device) {
|
|
297
|
+
const status = device.status || {};
|
|
298
|
+
const capabilities = device.capabilities || {};
|
|
299
|
+
const thumbnail = device.latestThumbnail || {};
|
|
300
|
+
const liveFrame = device.latestLiveFrame || {};
|
|
301
|
+
const liveStream = device.activeLiveStream || {};
|
|
302
|
+
const latestTask = device.latestTask || {};
|
|
303
|
+
const platform = String(status.platform || device.platform || 'unknown');
|
|
304
|
+
|
|
305
|
+
return {
|
|
306
|
+
DeviceId: String(device.deviceId || ''),
|
|
307
|
+
SessionId: String(device.sessionId || ''),
|
|
308
|
+
Name: String(device.deviceName || device.hostname || device.deviceId || 'device'),
|
|
309
|
+
Hostname: String(device.hostname || ''),
|
|
310
|
+
Platform: platform,
|
|
311
|
+
Release: String(status.release || ''),
|
|
312
|
+
Arch: String(device.arch || ''),
|
|
313
|
+
AgentVersion: String(device.agentVersion || ''),
|
|
314
|
+
Connected: device.connected === true,
|
|
315
|
+
ConnectedAt: String(device.connectedAt || ''),
|
|
316
|
+
DisconnectedAt: String(device.disconnectedAt || ''),
|
|
317
|
+
LastSeenAt: String(device.lastSeenAt || ''),
|
|
318
|
+
LastStatusAt: String(device.lastStatusAt || ''),
|
|
319
|
+
LastDisconnectReason: String(device.lastDisconnectReason || ''),
|
|
320
|
+
RemoteAddress: String(device.remoteAddress || ''),
|
|
321
|
+
RemotePort: Number(device.remotePort || 0),
|
|
322
|
+
UptimeSec: readStatusNumber(status, 'uptimeSec'),
|
|
323
|
+
UsedMemRatio: readStatusNumber(status, 'usedMemRatio'),
|
|
324
|
+
Load1: readLoad1(status),
|
|
325
|
+
StatusEnabled: capabilities.status === true,
|
|
326
|
+
ThumbnailEnabled: capabilities.thumbnail === true,
|
|
327
|
+
LiveStreamEnabled: capabilities.liveStream === true,
|
|
328
|
+
ControlEnabled: capabilities.control === true,
|
|
329
|
+
ComputerAgentEnabled: capabilities.computerAgent === true || capabilities.taskDispatch === true,
|
|
330
|
+
AiAssistEnabled: capabilities.aiAssist === true,
|
|
331
|
+
AiModel: String(capabilities.aiModel || ''),
|
|
332
|
+
AiProvider: String(capabilities.aiProvider || ''),
|
|
333
|
+
ThumbnailStreamId: String(thumbnail.streamId || ''),
|
|
334
|
+
ThumbnailFrameSeq: Number(thumbnail.frameSeq || 0),
|
|
335
|
+
ThumbnailWidth: Number(thumbnail.width || 0),
|
|
336
|
+
ThumbnailHeight: Number(thumbnail.height || 0),
|
|
337
|
+
ThumbnailMimeType: String(thumbnail.mimeType || thumbnail.format || ''),
|
|
338
|
+
ThumbnailCapturedAt: String(thumbnail.capturedAt || ''),
|
|
339
|
+
ThumbnailReceivedAt: String(thumbnail.receivedAt || ''),
|
|
340
|
+
ThumbnailDataUrl: String(thumbnail.dataUrl || ''),
|
|
341
|
+
LiveStreamActive: liveStream.active === true,
|
|
342
|
+
LiveStreamId: String(liveStream.streamId || ''),
|
|
343
|
+
LiveStreamMode: String(liveStream.mode || ''),
|
|
344
|
+
LiveStreamFps: Number(liveStream.fps || 0),
|
|
345
|
+
LiveStreamStartedAt: String(liveStream.startedAt || ''),
|
|
346
|
+
LiveStreamStoppedAt: String(liveStream.stoppedAt || ''),
|
|
347
|
+
LiveStreamLastFrameAt: String(liveStream.lastFrameAt || ''),
|
|
348
|
+
LiveStreamFrameSeq: Number(liveStream.lastFrameSeq || 0),
|
|
349
|
+
LiveFrameStreamId: String(liveFrame.streamId || ''),
|
|
350
|
+
LiveFrameSeq: Number(liveFrame.frameSeq || 0),
|
|
351
|
+
LiveFrameWidth: Number(liveFrame.width || 0),
|
|
352
|
+
LiveFrameHeight: Number(liveFrame.height || 0),
|
|
353
|
+
LiveFrameMimeType: String(liveFrame.mimeType || liveFrame.format || ''),
|
|
354
|
+
LiveFrameCapturedAt: String(liveFrame.capturedAt || ''),
|
|
355
|
+
LiveFrameReceivedAt: String(liveFrame.receivedAt || ''),
|
|
356
|
+
LiveFrameDataUrl: String(liveFrame.dataUrl || ''),
|
|
357
|
+
LatestTaskId: String(latestTask.taskId || ''),
|
|
358
|
+
LatestTaskCommandId: String(latestTask.commandId || ''),
|
|
359
|
+
LatestTaskTitle: String(latestTask.title || ''),
|
|
360
|
+
LatestTaskInstructionPreview: String(latestTask.instructionPreview || ''),
|
|
361
|
+
LatestTaskStatus: String(latestTask.status || ''),
|
|
362
|
+
LatestTaskApprovalLevel: String(latestTask.approvalLevel || ''),
|
|
363
|
+
LatestTaskUpdatedAt: String(latestTask.updatedAt || latestTask.completedAt || latestTask.sentAt || ''),
|
|
364
|
+
LatestTaskError: String(latestTask.error || ''),
|
|
365
|
+
LatestTaskResultSummary: String(latestTask.resultSummary || '')
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
async function loadCss3DManager() {
|
|
370
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
371
|
+
const managerPath = path.resolve(__dirname, '../../MindExecution.Shared/wwwroot/js/mind-map-css3d-manager.js');
|
|
372
|
+
const source = await fs.readFile(managerPath, 'utf8');
|
|
373
|
+
const document = new MiniDocument();
|
|
374
|
+
const context = {
|
|
375
|
+
console,
|
|
376
|
+
document,
|
|
377
|
+
navigator: {
|
|
378
|
+
clipboard: {
|
|
379
|
+
writeText: async () => {}
|
|
380
|
+
}
|
|
381
|
+
},
|
|
382
|
+
setInterval: () => 0,
|
|
383
|
+
clearInterval: () => {},
|
|
384
|
+
setTimeout,
|
|
385
|
+
clearTimeout,
|
|
386
|
+
Date,
|
|
387
|
+
Math,
|
|
388
|
+
Number,
|
|
389
|
+
String,
|
|
390
|
+
Boolean,
|
|
391
|
+
Array,
|
|
392
|
+
Object,
|
|
393
|
+
JSON,
|
|
394
|
+
RegExp,
|
|
395
|
+
Map,
|
|
396
|
+
Set,
|
|
397
|
+
WeakMap,
|
|
398
|
+
THREE: createThreeStub(),
|
|
399
|
+
URL,
|
|
400
|
+
Promise
|
|
401
|
+
};
|
|
402
|
+
context.window = context;
|
|
403
|
+
context.self = context;
|
|
404
|
+
|
|
405
|
+
vm.runInNewContext(source, context, { filename: managerPath });
|
|
406
|
+
return { manager: context.window.MindMapCss3DManager, document };
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function buildMonitorNode(devices, hubStatus) {
|
|
410
|
+
const connected = devices.filter(device => device.Connected).length;
|
|
411
|
+
const endpoint = hubStatus.agentEndpoint || '127.0.0.1:5197';
|
|
412
|
+
const pairToken = hubStatus.pairToken || 'render-smoke-token';
|
|
413
|
+
return {
|
|
414
|
+
id: 'remote-fleet-render-smoke',
|
|
415
|
+
contentType: 'memo',
|
|
416
|
+
metadata: {
|
|
417
|
+
SemanticType: 'RemoteFleetMonitor',
|
|
418
|
+
RemoteFleetSchemaVersion: 'remote-fleet@1',
|
|
419
|
+
RemoteFleetViewMode: 'all-devices',
|
|
420
|
+
RemoteFleetLastRefreshAtUtc: new Date().toISOString(),
|
|
421
|
+
RemoteFleetHubStatus: 'online',
|
|
422
|
+
RemoteFleetHubEndpoint: endpoint,
|
|
423
|
+
RemoteFleetAgentPackage: '@mindexec/remote',
|
|
424
|
+
RemoteFleetPairTokenPreview: '<pair-token>',
|
|
425
|
+
RemoteFleetConnectCommand: `npx @mindexec/remote connect --manager ${endpoint} --pair ${pairToken}`,
|
|
426
|
+
RemoteFleetDeviceCount: String(devices.length),
|
|
427
|
+
RemoteFleetConnectedDeviceCount: String(connected),
|
|
428
|
+
RemoteFleetCanvasPagination: 'none',
|
|
429
|
+
RemoteFleetDevicesJson: JSON.stringify(devices),
|
|
430
|
+
RemoteFleetLastError: ''
|
|
431
|
+
}
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function buildDeviceNode(device, hubStatus) {
|
|
436
|
+
const endpoint = hubStatus.agentEndpoint || '127.0.0.1:5197';
|
|
437
|
+
return {
|
|
438
|
+
id: 'remote-device-render-smoke',
|
|
439
|
+
contentType: 'memo',
|
|
440
|
+
metadata: {
|
|
441
|
+
SemanticType: 'RemoteFleetDevice',
|
|
442
|
+
RemoteFleetSchemaVersion: 'remote-device@1',
|
|
443
|
+
RemoteFleetViewMode: 'pinned-device',
|
|
444
|
+
RemoteFleetLastRefreshAtUtc: new Date().toISOString(),
|
|
445
|
+
RemoteFleetHubStatus: 'online',
|
|
446
|
+
RemoteFleetHubEndpoint: endpoint,
|
|
447
|
+
RemoteFleetAgentPackage: '@mindexec/remote',
|
|
448
|
+
RemoteFleetPinnedDeviceId: device.DeviceId,
|
|
449
|
+
RemoteFleetPinnedDeviceJson: JSON.stringify(device),
|
|
450
|
+
RemoteFleetLastError: ''
|
|
451
|
+
}
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function createThreeStub() {
|
|
456
|
+
class Vector3 {
|
|
457
|
+
constructor(x = 0, y = 0, z = 0) {
|
|
458
|
+
this.x = x;
|
|
459
|
+
this.y = y;
|
|
460
|
+
this.z = z;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
set(x = 0, y = 0, z = 0) {
|
|
464
|
+
this.x = x;
|
|
465
|
+
this.y = y;
|
|
466
|
+
this.z = z;
|
|
467
|
+
return this;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
class Matrix4 {
|
|
472
|
+
multiplyMatrices() { return this; }
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
class Frustum {
|
|
476
|
+
setFromProjectionMatrix() { return this; }
|
|
477
|
+
intersectsBox() { return true; }
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
class Quaternion {}
|
|
481
|
+
|
|
482
|
+
class Box3 {
|
|
483
|
+
setFromCenterAndSize() { return this; }
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
return { Box3, Frustum, Matrix4, Quaternion, Vector3 };
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
const hub = createRemoteHub({
|
|
490
|
+
env: {
|
|
491
|
+
MINDEXEC_REMOTE_HUB: '1',
|
|
492
|
+
REMOTE_HUB_HOST: '127.0.0.1',
|
|
493
|
+
REMOTE_HUB_PORT: '0',
|
|
494
|
+
REMOTE_HUB_PAIR_TOKEN: 'render-smoke-token'
|
|
495
|
+
}
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
try {
|
|
499
|
+
await hub.start();
|
|
500
|
+
const seeded = hub.seedSyntheticFleet({
|
|
501
|
+
count: SYNTHETIC_COUNT,
|
|
502
|
+
connectedRatio: 0.84,
|
|
503
|
+
thumbnailRatio: 0.72,
|
|
504
|
+
aiAssistRatio: 0.42,
|
|
505
|
+
liveCount: 0
|
|
506
|
+
});
|
|
507
|
+
assert.equal(seeded.ok, true);
|
|
508
|
+
assert.equal(seeded.seeded, SYNTHETIC_COUNT);
|
|
509
|
+
|
|
510
|
+
const rawDevices = hub.listDevices();
|
|
511
|
+
const devices = rawDevices.map(projectDevice);
|
|
512
|
+
const connectedCount = devices.filter(device => device.Connected).length;
|
|
513
|
+
const offlineCount = devices.length - connectedCount;
|
|
514
|
+
const aiCount = devices.filter(device => device.Connected && device.AiAssistEnabled).length;
|
|
515
|
+
const focusedDevice = devices.find(device => device.Connected);
|
|
516
|
+
assert.ok(focusedDevice);
|
|
517
|
+
|
|
518
|
+
const { manager, document } = await loadCss3DManager();
|
|
519
|
+
assert.equal(typeof manager?.renderRemoteFleetMonitorForTest, 'function');
|
|
520
|
+
assert.equal(typeof manager?.renderRemoteFleetDeviceForTest, 'function');
|
|
521
|
+
|
|
522
|
+
const bodyView = document.createElement('div');
|
|
523
|
+
bodyView.dataset.remoteFleetAutoMonitor = 'false';
|
|
524
|
+
bodyView.dataset.remoteFleetFocusDeviceId = focusedDevice.DeviceId;
|
|
525
|
+
document.body.appendChild(bodyView);
|
|
526
|
+
|
|
527
|
+
manager.renderRemoteFleetMonitorForTest(bodyView, buildMonitorNode(devices, hub.getStatus({ includeSecrets: true })));
|
|
528
|
+
|
|
529
|
+
let cards = bodyView.querySelectorAll('article[data-device-id]');
|
|
530
|
+
assert.equal(cards.length, SYNTHETIC_COUNT);
|
|
531
|
+
assert.equal(bodyView.querySelector('[data-remote-fleet-match-count="true"]')?.textContent, `${SYNTHETIC_COUNT}/${SYNTHETIC_COUNT}`);
|
|
532
|
+
assert.equal(bodyView.querySelectorAll('[data-remote-fleet-action="pin-device"]').length, SYNTHETIC_COUNT);
|
|
533
|
+
assert.equal(bodyView.querySelectorAll('[data-remote-fleet-action="task-device"]').length, connectedCount);
|
|
534
|
+
assert.equal(bodyView.querySelectorAll('[data-remote-fleet-action="thumbnail-device"]').length, devices.filter(device => device.Connected && device.ThumbnailEnabled).length);
|
|
535
|
+
assert.equal(bodyView.querySelector('[data-remote-fleet-live-panel="true"]')?.dataset.deviceId, focusedDevice.DeviceId);
|
|
536
|
+
assert.equal(bodyView.querySelector('[data-remote-fleet-action="task-visible"]')?.disabled, false);
|
|
537
|
+
assert.ok(bodyView.textContent.includes('all devices, no paging'));
|
|
538
|
+
assert.ok(bodyView.querySelector('code')?.textContent.includes('npx @mindexec/remote connect'));
|
|
539
|
+
|
|
540
|
+
const searchInput = bodyView.querySelector('[data-remote-fleet-search="true"]');
|
|
541
|
+
searchInput.value = 'synthetic pc 0001';
|
|
542
|
+
searchInput.dispatchEvent({ type: 'input' });
|
|
543
|
+
cards = bodyView.querySelectorAll('article[data-device-id]');
|
|
544
|
+
assert.equal(cards.filter(card => card.style.display !== 'none').length, 1);
|
|
545
|
+
assert.equal(bodyView.querySelector('[data-remote-fleet-match-count="true"]')?.textContent, `1/${SYNTHETIC_COUNT}`);
|
|
546
|
+
|
|
547
|
+
const selects = bodyView.querySelectorAll('select');
|
|
548
|
+
assert.equal(selects.length, 4);
|
|
549
|
+
const [filterSelect, , groupSelect] = selects;
|
|
550
|
+
searchInput.value = '';
|
|
551
|
+
searchInput.dispatchEvent({ type: 'input' });
|
|
552
|
+
filterSelect.value = 'offline';
|
|
553
|
+
filterSelect.dispatchEvent({ type: 'change' });
|
|
554
|
+
assert.equal(bodyView.querySelector('[data-remote-fleet-match-count="true"]')?.textContent, `${offlineCount}/${SYNTHETIC_COUNT}`);
|
|
555
|
+
|
|
556
|
+
filterSelect.value = 'ai';
|
|
557
|
+
filterSelect.dispatchEvent({ type: 'change' });
|
|
558
|
+
assert.equal(bodyView.querySelector('[data-remote-fleet-match-count="true"]')?.textContent, `${aiCount}/${SYNTHETIC_COUNT}`);
|
|
559
|
+
|
|
560
|
+
filterSelect.value = 'all';
|
|
561
|
+
filterSelect.dispatchEvent({ type: 'change' });
|
|
562
|
+
groupSelect.value = 'status';
|
|
563
|
+
groupSelect.dispatchEvent({ type: 'change' });
|
|
564
|
+
assert.equal(bodyView.dataset.remoteFleetGroup, 'status');
|
|
565
|
+
assert.ok(bodyView.querySelectorAll('[data-remote-fleet-group-header="true"]').length >= 2);
|
|
566
|
+
assert.equal(bodyView.querySelectorAll('article[data-device-id]').length, SYNTHETIC_COUNT);
|
|
567
|
+
|
|
568
|
+
const deviceBody = document.createElement('div');
|
|
569
|
+
document.body.appendChild(deviceBody);
|
|
570
|
+
manager.renderRemoteFleetDeviceForTest(deviceBody, buildDeviceNode(focusedDevice, hub.getStatus()));
|
|
571
|
+
assert.ok(deviceBody.textContent.includes(focusedDevice.Name));
|
|
572
|
+
assert.ok(deviceBody.querySelector('[data-remote-fleet-action="refresh-device"]'));
|
|
573
|
+
assert.ok(deviceBody.querySelector('[data-remote-fleet-action="task-device"]'));
|
|
574
|
+
assert.ok(deviceBody.textContent.includes(focusedDevice.DeviceId));
|
|
575
|
+
|
|
576
|
+
console.log(`RemoteFleet render smoke OK (${SYNTHETIC_COUNT} synthetic devices, ${connectedCount} connected, ${aiCount} AI-ready)`);
|
|
577
|
+
} finally {
|
|
578
|
+
await hub.close();
|
|
579
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import assert from 'assert/strict';
|
|
4
|
+
import { createRemoteHub } from '../remote-hub.js';
|
|
5
|
+
|
|
6
|
+
const SYNTHETIC_COUNT = Number(process.env.REMOTE_HUB_SCALE_SMOKE_COUNT || 250);
|
|
7
|
+
|
|
8
|
+
const hub = createRemoteHub({
|
|
9
|
+
env: {
|
|
10
|
+
MINDEXEC_REMOTE_HUB: '1',
|
|
11
|
+
REMOTE_HUB_HOST: '127.0.0.1',
|
|
12
|
+
REMOTE_HUB_PORT: '0',
|
|
13
|
+
REMOTE_HUB_PAIR_TOKEN: 'scale-smoke-token'
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
await hub.start();
|
|
19
|
+
|
|
20
|
+
const seeded = hub.seedSyntheticFleet({
|
|
21
|
+
count: SYNTHETIC_COUNT,
|
|
22
|
+
connectedRatio: 0.84,
|
|
23
|
+
thumbnailRatio: 0.72,
|
|
24
|
+
aiAssistRatio: 0.42,
|
|
25
|
+
liveCount: 8
|
|
26
|
+
});
|
|
27
|
+
assert.equal(seeded.ok, true);
|
|
28
|
+
assert.equal(seeded.seeded, SYNTHETIC_COUNT);
|
|
29
|
+
|
|
30
|
+
const status = hub.getStatus();
|
|
31
|
+
assert.equal(status.started, true);
|
|
32
|
+
assert.equal(status.canvasPagination, 'none');
|
|
33
|
+
assert.equal(status.deviceCount, SYNTHETIC_COUNT);
|
|
34
|
+
assert.equal(status.connectedDeviceCount, seeded.connected);
|
|
35
|
+
|
|
36
|
+
const devices = hub.listDevices();
|
|
37
|
+
assert.equal(devices.length, SYNTHETIC_COUNT);
|
|
38
|
+
assert.equal(devices.filter(device => device.synthetic === true).length, SYNTHETIC_COUNT);
|
|
39
|
+
assert.equal(devices.some(device => device.connected === false), true);
|
|
40
|
+
assert.equal(devices.some(device => device.latestThumbnail?.dataUrl), true);
|
|
41
|
+
assert.equal(devices.some(device => device.activeLiveStream?.active === true), true);
|
|
42
|
+
|
|
43
|
+
const deviceListPayload = {
|
|
44
|
+
total: devices.length,
|
|
45
|
+
pagination: 'none',
|
|
46
|
+
canvasDeviceListMode: 'all-devices',
|
|
47
|
+
devices
|
|
48
|
+
};
|
|
49
|
+
assert.ok(JSON.stringify(deviceListPayload).length < 8 * 1024 * 1024);
|
|
50
|
+
|
|
51
|
+
const thumbnailTarget = devices.find(device => device.connected && device.capabilities?.thumbnail);
|
|
52
|
+
assert.ok(thumbnailTarget);
|
|
53
|
+
const thumbnailResult = hub.requestThumbnail(thumbnailTarget.deviceId, {
|
|
54
|
+
streamId: 'scale-thumb',
|
|
55
|
+
maxWidth: 360,
|
|
56
|
+
maxHeight: 220,
|
|
57
|
+
quality: 50
|
|
58
|
+
});
|
|
59
|
+
assert.equal(thumbnailResult.ok, true);
|
|
60
|
+
assert.equal(hub.getDeviceThumbnail(thumbnailTarget.deviceId).streamId, 'scale-thumb');
|
|
61
|
+
|
|
62
|
+
const liveTarget = devices.find(device => device.connected && device.capabilities?.liveStream);
|
|
63
|
+
assert.ok(liveTarget);
|
|
64
|
+
const liveResult = hub.startLiveStream(liveTarget.deviceId, {
|
|
65
|
+
streamId: 'scale-live',
|
|
66
|
+
fps: 12,
|
|
67
|
+
maxWidth: 960,
|
|
68
|
+
maxHeight: 540,
|
|
69
|
+
quality: 60
|
|
70
|
+
});
|
|
71
|
+
assert.equal(liveResult.ok, true);
|
|
72
|
+
assert.equal(hub.getDeviceLiveFrame(liveTarget.deviceId).streamId, 'scale-live');
|
|
73
|
+
const stopResult = hub.stopLiveStream(liveTarget.deviceId, { streamId: 'scale-live' });
|
|
74
|
+
assert.equal(stopResult.ok, true);
|
|
75
|
+
|
|
76
|
+
const connectedTargets = hub.listDevices()
|
|
77
|
+
.filter(device => device.connected && device.capabilities?.taskDispatch)
|
|
78
|
+
.slice(0, 200);
|
|
79
|
+
assert.ok(connectedTargets.length >= 100);
|
|
80
|
+
const taskResults = connectedTargets.map(device =>
|
|
81
|
+
hub.requestAgentTask(device.deviceId, {
|
|
82
|
+
instruction: 'Synthetic scale task: report current status to the manager.',
|
|
83
|
+
title: 'Scale task'
|
|
84
|
+
}));
|
|
85
|
+
assert.equal(taskResults.every(result => result.ok === true), true);
|
|
86
|
+
assert.equal(taskResults.length, connectedTargets.length);
|
|
87
|
+
|
|
88
|
+
const aiTarget = hub.listDevices().find(device =>
|
|
89
|
+
device.connected && device.capabilities?.taskDispatch && device.capabilities?.aiAssist);
|
|
90
|
+
assert.ok(aiTarget);
|
|
91
|
+
const aiResult = hub.requestAgentTask(aiTarget.deviceId, {
|
|
92
|
+
instruction: 'Synthetic AI assist: summarize fleet condition.',
|
|
93
|
+
title: 'Scale AI task',
|
|
94
|
+
approvalLevel: 'ai-assist'
|
|
95
|
+
});
|
|
96
|
+
assert.equal(aiResult.ok, true);
|
|
97
|
+
assert.equal(aiResult.approvalLevel, 'ai-assist');
|
|
98
|
+
assert.equal(hub.listDevices().find(device => device.deviceId === aiTarget.deviceId)
|
|
99
|
+
?.latestTask?.approvalLevel, 'ai-assist');
|
|
100
|
+
|
|
101
|
+
const cleared = hub.clearSyntheticFleet();
|
|
102
|
+
assert.equal(cleared.ok, true);
|
|
103
|
+
assert.equal(cleared.removed, SYNTHETIC_COUNT);
|
|
104
|
+
assert.equal(hub.listDevices().length, 0);
|
|
105
|
+
|
|
106
|
+
console.log(`RemoteHub scale smoke OK (${SYNTHETIC_COUNT} synthetic devices)`);
|
|
107
|
+
} finally {
|
|
108
|
+
await hub.close();
|
|
109
|
+
}
|
package/server.js
CHANGED
|
@@ -6973,6 +6973,38 @@ app.get('/api/remote/devices', (req, res) => {
|
|
|
6973
6973
|
});
|
|
6974
6974
|
});
|
|
6975
6975
|
|
|
6976
|
+
function isSyntheticRemoteFleetEnabled() {
|
|
6977
|
+
return /^(1|true|yes|on)$/i.test(String(
|
|
6978
|
+
process.env.MINDEXEC_REMOTE_SYNTHETIC_FLEET
|
|
6979
|
+
|| process.env.REMOTE_HUB_SYNTHETIC_FLEET
|
|
6980
|
+
|| ''));
|
|
6981
|
+
}
|
|
6982
|
+
|
|
6983
|
+
app.post('/api/remote/synthetic/seed', (req, res) => {
|
|
6984
|
+
if (!isSyntheticRemoteFleetEnabled()) {
|
|
6985
|
+
res.status(403).json({ ok: false, error: 'synthetic-fleet-disabled' });
|
|
6986
|
+
return;
|
|
6987
|
+
}
|
|
6988
|
+
|
|
6989
|
+
res.json(remoteHub.seedSyntheticFleet({
|
|
6990
|
+
count: req.body?.count,
|
|
6991
|
+
connectedRatio: req.body?.connectedRatio,
|
|
6992
|
+
thumbnailRatio: req.body?.thumbnailRatio,
|
|
6993
|
+
aiAssistRatio: req.body?.aiAssistRatio,
|
|
6994
|
+
liveCount: req.body?.liveCount,
|
|
6995
|
+
replace: req.body?.replace
|
|
6996
|
+
}));
|
|
6997
|
+
});
|
|
6998
|
+
|
|
6999
|
+
app.delete('/api/remote/synthetic', (req, res) => {
|
|
7000
|
+
if (!isSyntheticRemoteFleetEnabled()) {
|
|
7001
|
+
res.status(403).json({ ok: false, error: 'synthetic-fleet-disabled' });
|
|
7002
|
+
return;
|
|
7003
|
+
}
|
|
7004
|
+
|
|
7005
|
+
res.json(remoteHub.clearSyntheticFleet());
|
|
7006
|
+
});
|
|
7007
|
+
|
|
6976
7008
|
app.post('/api/remote/devices/:deviceId/disconnect', (req, res) => {
|
|
6977
7009
|
const disconnected = remoteHub.disconnectDevice(req.params.deviceId, 'manager-request');
|
|
6978
7010
|
res.json({ ok: disconnected });
|
|
@@ -18079,6 +18079,8 @@
|
|
|
18079
18079
|
syncVideoNodeVisibilityPlayback: syncVideoNodeVisibilityPlayback,
|
|
18080
18080
|
syncEditingOverlay: syncEditingOverlay,
|
|
18081
18081
|
syncTextOverlays: syncTextOverlays,
|
|
18082
|
+
renderRemoteFleetMonitorForTest: renderRemoteFleetMonitor,
|
|
18083
|
+
renderRemoteFleetDeviceForTest: renderRemoteFleetDevice,
|
|
18082
18084
|
renderBusinessAutomationEdges: renderBusinessAutomationEdges,
|
|
18083
18085
|
scheduleBusinessAutomationEdgeRender: scheduleBusinessAutomationEdgeRender,
|
|
18084
18086
|
hideBusinessAutomationFloatingTooltipForNode: hideBusinessAutomationFloatingTooltipForNode,
|
package/wwwroot/index.html
CHANGED
|
@@ -558,7 +558,7 @@
|
|
|
558
558
|
}
|
|
559
559
|
|
|
560
560
|
const base = '_content/MindExecution.Shared/js/';
|
|
561
|
-
const scriptVersion = '20260612-remote-
|
|
561
|
+
const scriptVersion = '20260612-remote-fleet-render-smoke-v471';
|
|
562
562
|
const scriptUrl = (script) => `${base}${script}?v=${scriptVersion}`;
|
|
563
563
|
console.log(`[Script Loader] Shared JS version: ${scriptVersion}`);
|
|
564
564
|
const criticalScripts = [
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
self.assetsManifest = {
|
|
2
|
-
"version": "
|
|
2
|
+
"version": "SWs2Qkml",
|
|
3
3
|
"assets": [
|
|
4
4
|
{
|
|
5
5
|
"hash": "sha256-+CSYMcqLNTsq3VnH11jgYyOCCdxvHzL74CBmo4sCmMU=",
|
|
@@ -86,7 +86,7 @@
|
|
|
86
86
|
"url": "_content/MindExecution.Shared/js/mind-map-core.js.backup"
|
|
87
87
|
},
|
|
88
88
|
{
|
|
89
|
-
"hash": "sha256-
|
|
89
|
+
"hash": "sha256-L+uOxULau2wsk0zuWPFd4Ifi5xJUKJ8RAu1JP3JfsL4=",
|
|
90
90
|
"url": "_content/MindExecution.Shared/js/mind-map-css3d-manager.js"
|
|
91
91
|
},
|
|
92
92
|
{
|
|
@@ -834,7 +834,7 @@
|
|
|
834
834
|
"url": "image-manifest.json"
|
|
835
835
|
},
|
|
836
836
|
{
|
|
837
|
-
"hash": "sha256-
|
|
837
|
+
"hash": "sha256-KfejnUckOnF3WGDdio4k0wnUNz1fpsWmWkKBCXX9mXE=",
|
|
838
838
|
"url": "index.html"
|
|
839
839
|
},
|
|
840
840
|
{
|