@mindexec/cli 0.2.24 → 0.2.25

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 (95) hide show
  1. package/package.json +1 -1
  2. package/remote-hub.js +130 -1
  3. package/scripts/remote-fleet-render-smoke.mjs +80 -53
  4. package/scripts/remote-http-smoke.mjs +218 -40
  5. package/scripts/remote-hub-smoke.mjs +139 -0
  6. package/server.js +17 -0
  7. package/wwwroot/_content/MindExecution.Shared/js/mind-map-core.js +18 -1
  8. package/wwwroot/_content/MindExecution.Shared/js/mind-map-css3d-manager.js +653 -398
  9. package/wwwroot/_framework/{Microsoft.CSharp.qrvp77qmhs.dll → Microsoft.CSharp.8bsm8vx6su.dll} +0 -0
  10. package/wwwroot/_framework/MindExecution.Core.6rfnfdndxq.dll +0 -0
  11. package/wwwroot/_framework/{MindExecution.Kernel.qt0p5apeu2.dll → MindExecution.Kernel.z56elxihok.dll} +0 -0
  12. package/wwwroot/_framework/{MindExecution.Plugins.Admin.i9eswmhltm.dll → MindExecution.Plugins.Admin.p5cs4ap87v.dll} +0 -0
  13. package/wwwroot/_framework/{MindExecution.Plugins.Business.1eayoj2kvc.dll → MindExecution.Plugins.Business.s35og5uz44.dll} +0 -0
  14. package/wwwroot/_framework/{MindExecution.Plugins.Concept.a3850nmm3d.dll → MindExecution.Plugins.Concept.zczca3fsxz.dll} +0 -0
  15. package/wwwroot/_framework/{MindExecution.Plugins.Directory.l3rmdlu7o7.dll → MindExecution.Plugins.Directory.y74f55e8x3.dll} +0 -0
  16. package/wwwroot/_framework/{MindExecution.Plugins.PlanMaster.2x5gsi74yi.dll → MindExecution.Plugins.PlanMaster.jpdwbefrh1.dll} +0 -0
  17. package/wwwroot/_framework/{MindExecution.Plugins.YouTube.vbl462eegw.dll → MindExecution.Plugins.YouTube.8nz4wv2nsj.dll} +0 -0
  18. package/wwwroot/_framework/{MindExecution.Shared.l2w05i7sd6.dll → MindExecution.Shared.v6ani8nfp8.dll} +0 -0
  19. package/wwwroot/_framework/MindExecution.Web.0cs29v57jl.dll +0 -0
  20. package/wwwroot/_framework/{System.Collections.x53e19vfsj.dll → System.Collections.23yxgetbju.dll} +0 -0
  21. package/wwwroot/_framework/{System.Collections.Concurrent.y1zmvuyipi.dll → System.Collections.Concurrent.vow3rm1dku.dll} +0 -0
  22. package/wwwroot/_framework/{System.Collections.Immutable.ug3j698qms.dll → System.Collections.Immutable.ap9596utv5.dll} +0 -0
  23. package/wwwroot/_framework/{System.Collections.NonGeneric.h66hj3863h.dll → System.Collections.NonGeneric.npjkaz40oc.dll} +0 -0
  24. package/wwwroot/_framework/{System.Collections.Specialized.umr3y27ntj.dll → System.Collections.Specialized.ugkjbs6p02.dll} +0 -0
  25. package/wwwroot/_framework/{System.ComponentModel.Annotations.tz6gnt4ebt.dll → System.ComponentModel.Annotations.clwo0z2oyu.dll} +0 -0
  26. package/wwwroot/_framework/{System.ComponentModel.Primitives.j7tiphu4rg.dll → System.ComponentModel.Primitives.end4v0xe2c.dll} +0 -0
  27. package/wwwroot/_framework/{System.ComponentModel.TypeConverter.ujlztox1gx.dll → System.ComponentModel.TypeConverter.tgtp5sm4iv.dll} +0 -0
  28. package/wwwroot/_framework/{System.ComponentModel.x9xz0ojfb6.dll → System.ComponentModel.wupoltkk1t.dll} +0 -0
  29. package/wwwroot/_framework/{System.Console.ijzpqmj7ne.dll → System.Console.9dik0wogo2.dll} +0 -0
  30. package/wwwroot/_framework/{System.Data.Common.1r0sqffq1p.dll → System.Data.Common.4v8jejsiu0.dll} +0 -0
  31. package/wwwroot/_framework/{System.Diagnostics.DiagnosticSource.9upoqwq09o.dll → System.Diagnostics.DiagnosticSource.e2q75ondtq.dll} +0 -0
  32. package/wwwroot/_framework/{System.Diagnostics.Process.m99azzntjm.dll → System.Diagnostics.Process.9736yjnxs8.dll} +0 -0
  33. package/wwwroot/_framework/{System.Diagnostics.TraceSource.pl7wv26myr.dll → System.Diagnostics.TraceSource.h9al53gbbw.dll} +0 -0
  34. package/wwwroot/_framework/{System.Diagnostics.Tracing.crlhfx6tut.dll → System.Diagnostics.Tracing.h4bcp2fo98.dll} +0 -0
  35. package/wwwroot/_framework/{System.Drawing.mi7d8hwowb.dll → System.Drawing.64oovy8qts.dll} +0 -0
  36. package/wwwroot/_framework/{System.Drawing.Primitives.22e4y9ikq9.dll → System.Drawing.Primitives.o6jiqpgbgl.dll} +0 -0
  37. package/wwwroot/_framework/{System.Formats.Asn1.jx23sjiqnn.dll → System.Formats.Asn1.rylx5ipd40.dll} +0 -0
  38. package/wwwroot/_framework/{System.IO.Compression.6fyoii3uej.dll → System.IO.Compression.iceabaupns.dll} +0 -0
  39. package/wwwroot/_framework/{System.IO.Pipelines.vg77t4cd4d.dll → System.IO.Pipelines.uw8csd3mlz.dll} +0 -0
  40. package/wwwroot/_framework/{System.Linq.Expressions.24xqiypwdt.dll → System.Linq.Expressions.ty95ava37f.dll} +0 -0
  41. package/wwwroot/_framework/{System.Linq.Queryable.hvd01d6rsa.dll → System.Linq.Queryable.hs2195jrwy.dll} +0 -0
  42. package/wwwroot/_framework/{System.Linq.1bkoxlqgmq.dll → System.Linq.hssodjwmlf.dll} +0 -0
  43. package/wwwroot/_framework/{System.Memory.8dx3lwgym4.dll → System.Memory.1k78n7wdxb.dll} +0 -0
  44. package/wwwroot/_framework/{System.Net.Http.eitrz660my.dll → System.Net.Http.3br8rfql4c.dll} +0 -0
  45. package/wwwroot/_framework/{System.Net.Http.Json.3mhdm9l1rf.dll → System.Net.Http.Json.860wbh17d8.dll} +0 -0
  46. package/wwwroot/_framework/{System.Net.NetworkInformation.3pkuofcv9r.dll → System.Net.NetworkInformation.e3wr00853o.dll} +0 -0
  47. package/wwwroot/_framework/{System.Net.Ping.8clj5pklrp.dll → System.Net.Ping.r5cw4mf1a4.dll} +0 -0
  48. package/wwwroot/_framework/{System.Net.Primitives.qrp4wcjz1p.dll → System.Net.Primitives.ksxwiwlvhu.dll} +0 -0
  49. package/wwwroot/_framework/{System.Net.WebSockets.qp6u31zvm5.dll → System.Net.WebSockets.6rt3n3gl2q.dll} +0 -0
  50. package/wwwroot/_framework/{System.Net.WebSockets.Client.2u6pv01g69.dll → System.Net.WebSockets.Client.z3usrzo7rz.dll} +0 -0
  51. package/wwwroot/_framework/{System.Numerics.Vectors.kc7ufp2j4l.dll → System.Numerics.Vectors.lbdzx8reja.dll} +0 -0
  52. package/wwwroot/_framework/{System.ObjectModel.qv82fot1ib.dll → System.ObjectModel.yct1gdirzf.dll} +0 -0
  53. package/wwwroot/_framework/{System.Private.CoreLib.rkafq04oma.dll → System.Private.CoreLib.ns29bor93l.dll} +0 -0
  54. package/wwwroot/_framework/{System.Private.Uri.t9542hmr6j.dll → System.Private.Uri.zp9kmg0z93.dll} +0 -0
  55. package/wwwroot/_framework/{System.Private.Xml.Linq.n8n3ptrbwu.dll → System.Private.Xml.Linq.lgv2n0akl4.dll} +0 -0
  56. package/wwwroot/_framework/{System.Private.Xml.rxd3tytisn.dll → System.Private.Xml.dpsk8g304y.dll} +0 -0
  57. package/wwwroot/_framework/{System.Reflection.Emit.ILGeneration.stxyk8zoo1.dll → System.Reflection.Emit.ILGeneration.1tcuz2cmbk.dll} +0 -0
  58. package/wwwroot/_framework/{System.Reflection.Emit.Lightweight.6xrd5v8vg0.dll → System.Reflection.Emit.Lightweight.ddt2wylovg.dll} +0 -0
  59. package/wwwroot/_framework/{System.Reflection.Emit.9tjhp6y0j3.dll → System.Reflection.Emit.d8vkadiwhg.dll} +0 -0
  60. package/wwwroot/_framework/{System.Reflection.Primitives.wgn8fpwwvv.dll → System.Reflection.Primitives.cpsl71xd1z.dll} +0 -0
  61. package/wwwroot/_framework/{System.Runtime.InteropServices.te07xr2we9.dll → System.Runtime.InteropServices.31vjgsfk1o.dll} +0 -0
  62. package/wwwroot/_framework/{System.Runtime.InteropServices.JavaScript.sliym526xh.dll → System.Runtime.InteropServices.JavaScript.pn2wvizzet.dll} +0 -0
  63. package/wwwroot/_framework/{System.Runtime.InteropServices.RuntimeInformation.oji7zut14z.dll → System.Runtime.InteropServices.RuntimeInformation.l5rk496q70.dll} +0 -0
  64. package/wwwroot/_framework/{System.Runtime.Intrinsics.507y4h8nzq.dll → System.Runtime.Intrinsics.0zee9qcqfy.dll} +0 -0
  65. package/wwwroot/_framework/{System.Runtime.Loader.v7gk4bse0k.dll → System.Runtime.Loader.xw2jr9wl92.dll} +0 -0
  66. package/wwwroot/_framework/{System.Runtime.Numerics.eqy5xjv3nd.dll → System.Runtime.Numerics.ezj1dfyvbj.dll} +0 -0
  67. package/wwwroot/_framework/{System.Runtime.Serialization.Formatters.zpkrub8lab.dll → System.Runtime.Serialization.Formatters.0y019e9zkr.dll} +0 -0
  68. package/wwwroot/_framework/{System.Runtime.Serialization.Primitives.vhkpnbxjip.dll → System.Runtime.Serialization.Primitives.bia5wb62c8.dll} +0 -0
  69. package/wwwroot/_framework/{System.Runtime.jn319d5nyg.dll → System.Runtime.h9cduidfkh.dll} +0 -0
  70. package/wwwroot/_framework/{System.Security.Claims.0ztig1q9vo.dll → System.Security.Claims.2r18wim2rl.dll} +0 -0
  71. package/wwwroot/_framework/{System.Security.Cryptography.vttizqc9ho.dll → System.Security.Cryptography.qqgybpoucx.dll} +0 -0
  72. package/wwwroot/_framework/{System.Text.Encoding.Extensions.utdd47ny8f.dll → System.Text.Encoding.Extensions.9t3hs6kyll.dll} +0 -0
  73. package/wwwroot/_framework/{System.Text.Encodings.Web.wah8r1zoe0.dll → System.Text.Encodings.Web.wftjfh2crk.dll} +0 -0
  74. package/wwwroot/_framework/{System.Text.Json.kxlfxj0wrs.dll → System.Text.Json.4s25e3op6i.dll} +0 -0
  75. package/wwwroot/_framework/{System.Text.RegularExpressions.dbqn58klox.dll → System.Text.RegularExpressions.cvkox11l6l.dll} +0 -0
  76. package/wwwroot/_framework/{System.Threading.Channels.hfa7j0uv2w.dll → System.Threading.Channels.419493szqu.dll} +0 -0
  77. package/wwwroot/_framework/{System.Threading.Thread.caul0pdqul.dll → System.Threading.Thread.xhmys87xh5.dll} +0 -0
  78. package/wwwroot/_framework/{System.Threading.42ao9vi047.dll → System.Threading.b66vzsz9g1.dll} +0 -0
  79. package/wwwroot/_framework/{System.Transactions.Local.fimi2hamzo.dll → System.Transactions.Local.7zfffdvnwf.dll} +0 -0
  80. package/wwwroot/_framework/{System.Web.HttpUtility.gq8yz50p2e.dll → System.Web.HttpUtility.9up6xfbtuq.dll} +0 -0
  81. package/wwwroot/_framework/{System.Xml.Linq.kitin4zjoj.dll → System.Xml.Linq.n5rzv9nbf7.dll} +0 -0
  82. package/wwwroot/_framework/{System.Xml.ReaderWriter.kzvw3qgxb0.dll → System.Xml.ReaderWriter.ag8pilllob.dll} +0 -0
  83. package/wwwroot/_framework/{System.Xml.XDocument.c539ki6cuq.dll → System.Xml.XDocument.sn51jas17n.dll} +0 -0
  84. package/wwwroot/_framework/{System.m05i39uvk9.dll → System.brmz7yk5qh.dll} +0 -0
  85. package/wwwroot/_framework/blazor.boot.json +161 -161
  86. package/wwwroot/_framework/dotnet.js +1 -1
  87. package/wwwroot/_framework/{dotnet.native.vz0adxojrz.wasm → dotnet.native.boem75ye5i.wasm} +0 -0
  88. package/wwwroot/_framework/{dotnet.native.xsn1d6x2kd.js → dotnet.native.qc8g39g30v.js} +1 -1
  89. package/wwwroot/_framework/{dotnet.runtime.dstopyvqzi.js → dotnet.runtime.opaiwunc3t.js} +1 -1
  90. package/wwwroot/_framework/{netstandard.0xet7jg7ky.dll → netstandard.yvr3prsx0x.dll} +0 -0
  91. package/wwwroot/index.html +1 -1
  92. package/wwwroot/service-worker-assets.js +166 -166
  93. package/wwwroot/service-worker.js +1 -1
  94. package/wwwroot/_framework/MindExecution.Core.ri6sjbi2qk.dll +0 -0
  95. package/wwwroot/_framework/MindExecution.Web.i4ojmz00kp.dll +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mindexec/cli",
3
- "version": "0.2.24",
3
+ "version": "0.2.25",
4
4
  "description": "MindExec local runtime and bridge CLI",
5
5
  "main": "server.js",
6
6
  "type": "module",
package/remote-hub.js CHANGED
@@ -15,6 +15,7 @@ const RECENT_TASK_LIMIT = 12;
15
15
  const RECENT_TASK_BATCH_LIMIT = 16;
16
16
  const REMOTE_PROTOCOL_VERSION = 1;
17
17
  const MAX_SYNTHETIC_DEVICES = 1000;
18
+ const DEFAULT_HOST_TARGET_LEASE_MS = 30000;
18
19
  const SYNTHETIC_FRAME_DATA_URL = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAABCAYAAAD0In+KAAAADElEQVR42mP8z8AAAAMBAQDJ/pLvAAAAAElFTkSuQmCC';
19
20
 
20
21
  function isEnabledValue(value, fallback = true) {
@@ -219,9 +220,129 @@ export function createRemoteHub(options = {}) {
219
220
  let started = false;
220
221
  let boundPort = requestedPort;
221
222
  let lastError = '';
223
+ let hostTarget = null;
224
+
225
+ function getAgentEndpoint() {
226
+ return `${host}:${boundPort || requestedPort}`;
227
+ }
228
+
229
+ function getActiveHostTarget() {
230
+ if (!hostTarget) {
231
+ return null;
232
+ }
233
+
234
+ const expiresMs = Date.parse(hostTarget.expiresAt || '');
235
+ if (Number.isFinite(expiresMs) && expiresMs <= Date.now()) {
236
+ const expiredTarget = hostTarget;
237
+ hostTarget = null;
238
+ emitRemoteEvent('RemoteHostTargetExpired', null, {
239
+ hostTarget: {
240
+ ...expiredTarget,
241
+ active: false
242
+ }
243
+ });
244
+ return null;
245
+ }
246
+
247
+ return hostTarget;
248
+ }
249
+
250
+ function serializeHostTarget(target = getActiveHostTarget()) {
251
+ if (!target) {
252
+ return {
253
+ active: false,
254
+ nodeId: '',
255
+ leaseId: '',
256
+ endpoint: '',
257
+ activatedAt: '',
258
+ updatedAt: '',
259
+ expiresAt: ''
260
+ };
261
+ }
262
+
263
+ return {
264
+ active: true,
265
+ nodeId: target.nodeId,
266
+ leaseId: target.leaseId,
267
+ endpoint: target.endpoint,
268
+ activatedAt: target.activatedAt,
269
+ updatedAt: target.updatedAt,
270
+ expiresAt: target.expiresAt
271
+ };
272
+ }
273
+
274
+ function setHostTarget(options = {}) {
275
+ const enabled = options.enabled !== false;
276
+ const nodeId = safeString(options.nodeId, 128);
277
+ if (!enabled) {
278
+ const previous = getActiveHostTarget();
279
+ if (!previous) {
280
+ return {
281
+ ok: true,
282
+ active: false,
283
+ hostTarget: serializeHostTarget(null)
284
+ };
285
+ }
286
+
287
+ if (nodeId && previous.nodeId && nodeId !== previous.nodeId) {
288
+ return {
289
+ ok: false,
290
+ error: 'host-target-node-mismatch',
291
+ active: true,
292
+ hostTarget: serializeHostTarget(previous)
293
+ };
294
+ }
295
+
296
+ hostTarget = null;
297
+ emitRemoteEvent('RemoteHostTargetCleared', null, {
298
+ hostTarget: {
299
+ ...serializeHostTarget(previous),
300
+ active: false
301
+ }
302
+ });
303
+ return {
304
+ ok: true,
305
+ active: false,
306
+ hostTarget: serializeHostTarget(null)
307
+ };
308
+ }
309
+
310
+ if (!nodeId) {
311
+ return {
312
+ ok: false,
313
+ error: 'missing-node-id',
314
+ active: false,
315
+ hostTarget: serializeHostTarget()
316
+ };
317
+ }
318
+
319
+ const now = new Date();
320
+ const leaseMs = clampNumber(options.leaseMs, 5000, 10 * 60 * 1000, DEFAULT_HOST_TARGET_LEASE_MS);
321
+ const previous = getActiveHostTarget();
322
+ const activeSameNode = previous?.nodeId === nodeId;
323
+ hostTarget = {
324
+ nodeId,
325
+ leaseId: activeSameNode && previous?.leaseId ? previous.leaseId : crypto.randomUUID(),
326
+ endpoint: getAgentEndpoint(),
327
+ activatedAt: activeSameNode && previous?.activatedAt ? previous.activatedAt : now.toISOString(),
328
+ updatedAt: now.toISOString(),
329
+ expiresAt: new Date(now.getTime() + leaseMs).toISOString()
330
+ };
331
+
332
+ emitRemoteEvent('RemoteHostTargetSet', null, {
333
+ hostTarget: serializeHostTarget(hostTarget),
334
+ replacedNodeId: previous && previous.nodeId !== nodeId ? previous.nodeId : ''
335
+ });
336
+ return {
337
+ ok: true,
338
+ active: true,
339
+ hostTarget: serializeHostTarget(hostTarget)
340
+ };
341
+ }
222
342
 
223
343
  function getStatus({ includeSecrets = false } = {}) {
224
344
  const connectedDevices = [...devices.values()].filter(device => device.connected).length;
345
+ const activeHostTarget = serializeHostTarget();
225
346
  return {
226
347
  enabled,
227
348
  started,
@@ -234,13 +355,20 @@ export function createRemoteHub(options = {}) {
234
355
  managerPackage,
235
356
  managerVersion,
236
357
  agentPackage: '@mindexec/remote',
237
- agentEndpoint: `${host}:${boundPort || requestedPort}`,
358
+ agentEndpoint: getAgentEndpoint(),
238
359
  pairToken: includeSecrets ? pairToken : undefined,
239
360
  pairTokenPreview: maskToken(pairToken),
240
361
  deviceCount: devices.size,
241
362
  connectedDeviceCount: connectedDevices,
242
363
  canvasDeviceListMode: 'all-devices',
243
364
  canvasPagination: 'none',
365
+ hostTargetActive: activeHostTarget.active,
366
+ hostTargetNodeId: activeHostTarget.nodeId,
367
+ hostTargetLeaseId: activeHostTarget.leaseId,
368
+ hostTargetEndpoint: activeHostTarget.endpoint,
369
+ hostTargetActivatedAt: activeHostTarget.activatedAt,
370
+ hostTargetUpdatedAt: activeHostTarget.updatedAt,
371
+ hostTargetExpiresAt: activeHostTarget.expiresAt,
244
372
  externalExposure: host === '0.0.0.0' || host === '::',
245
373
  lastError
246
374
  };
@@ -1690,6 +1818,7 @@ export function createRemoteHub(options = {}) {
1690
1818
  sendCommand,
1691
1819
  requestAgentTask,
1692
1820
  requestAgentTaskBatch,
1821
+ setHostTarget,
1693
1822
  requestThumbnail,
1694
1823
  startLiveStream,
1695
1824
  stopLiveStream,
@@ -416,6 +416,9 @@ function buildMonitorNode(devices, hubStatus, latestTaskBatch = null, recentTask
416
416
  const connected = devices.filter(device => device.Connected).length;
417
417
  const endpoint = hubStatus.agentEndpoint || '127.0.0.1:5197';
418
418
  const pairToken = hubStatus.pairToken || 'render-smoke-token';
419
+ const hostTargetState = hubStatus.hostTargetActive
420
+ ? (hubStatus.hostTargetNodeId === 'remote-fleet-render-smoke' ? 'hosting' : 'other-monitor')
421
+ : 'inactive';
419
422
  return {
420
423
  id: 'remote-fleet-render-smoke',
421
424
  contentType: 'memo',
@@ -434,6 +437,13 @@ function buildMonitorNode(devices, hubStatus, latestTaskBatch = null, recentTask
434
437
  RemoteFleetDeviceCount: String(devices.length),
435
438
  RemoteFleetConnectedDeviceCount: String(connected),
436
439
  RemoteFleetCanvasPagination: 'none',
440
+ RemoteFleetHostTargetState: hostTargetState,
441
+ RemoteFleetHostTargetNodeId: hubStatus.hostTargetNodeId || '',
442
+ RemoteFleetHostTargetLeaseId: hubStatus.hostTargetLeaseId || '',
443
+ RemoteFleetHostTargetEndpoint: hubStatus.hostTargetEndpoint || endpoint,
444
+ RemoteFleetHostTargetActivatedAtUtc: hubStatus.hostTargetActivatedAt || '',
445
+ RemoteFleetHostTargetUpdatedAtUtc: hubStatus.hostTargetUpdatedAt || '',
446
+ RemoteFleetHostTargetExpiresAtUtc: hubStatus.hostTargetExpiresAt || '',
437
447
  RemoteFleetDevicesJson: JSON.stringify(devices),
438
448
  RemoteFleetLatestTaskBatchJson: latestTaskBatch ? JSON.stringify(latestTaskBatch) : '',
439
449
  RemoteFleetRecentTaskBatchesJson: JSON.stringify(recentTaskBatches),
@@ -645,6 +655,13 @@ try {
645
655
  };
646
656
  }
647
657
 
658
+ if (methodName === 'SetRemoteFleetHostFromJs') {
659
+ return {
660
+ success: true,
661
+ active: args[1] === true
662
+ };
663
+ }
664
+
648
665
  return { success: true };
649
666
  }
650
667
  };
@@ -660,16 +677,69 @@ try {
660
677
  let cards = bodyView.querySelectorAll('article[data-device-id]');
661
678
  assert.equal(cards.length, SYNTHETIC_COUNT);
662
679
  assert.equal(bodyView.querySelector('[data-remote-fleet-match-count="true"]')?.textContent, `${SYNTHETIC_COUNT}/${SYNTHETIC_COUNT}`);
663
- assert.equal(bodyView.querySelectorAll('[data-remote-fleet-action="pin-device"]').length, SYNTHETIC_COUNT);
664
- assert.equal(bodyView.querySelectorAll('[data-remote-fleet-action="task-device"]').length, connectedCount);
665
- assert.equal(bodyView.querySelectorAll('[data-remote-fleet-action="thumbnail-device"]').length, devices.filter(device => device.Connected && device.ThumbnailEnabled).length);
680
+ assert.ok(bodyView.querySelector('[data-remote-fleet-device-grid="true"]'));
681
+ const initialDetailPanel = bodyView.querySelector('[data-remote-fleet-detail-panel="true"]');
682
+ assert.equal(initialDetailPanel?.dataset.deviceId, focusedDevice.DeviceId);
683
+ assert.equal(bodyView.querySelectorAll('[data-remote-fleet-action="pin-device"]').length, 1);
684
+ assert.equal(bodyView.querySelectorAll('[data-remote-fleet-action="task-device"]').length, 0);
685
+ assert.equal(bodyView.querySelectorAll('[data-remote-fleet-action="thumbnail-device"]').length, focusedDevice.Connected && focusedDevice.ThumbnailEnabled ? 1 : 0);
686
+ assert.ok(initialDetailPanel?.textContent.includes('Seen'));
687
+ assert.ok(initialDetailPanel?.textContent.includes('Uptime'));
688
+ assert.ok(initialDetailPanel?.textContent.includes('Mem'));
689
+ assert.ok(initialDetailPanel?.textContent.includes('Load'));
690
+ assert.ok(cards[0]?.querySelector('[data-remote-fleet-device-preview="tile"]'));
691
+ assert.equal(/\b(Seen|Uptime|Mem|Load)\b/.test(cards[0]?.textContent || ''), false);
692
+ const alternateDevice = devices.find(device => device.DeviceId !== focusedDevice.DeviceId);
693
+ assert.ok(alternateDevice);
694
+ const alternateCard = Array.from(cards).find(card => card.dataset.deviceId === alternateDevice.DeviceId);
695
+ assert.ok(alternateCard);
696
+ alternateCard.dispatchEvent({ type: 'click' });
697
+ assert.equal(bodyView.dataset.remoteFleetSelectedDeviceId, alternateDevice.DeviceId);
698
+ assert.equal(bodyView.querySelector('[data-remote-fleet-detail-panel="true"]')?.dataset.deviceId, alternateDevice.DeviceId);
666
699
  const livePanel = bodyView.querySelector('[data-remote-fleet-live-panel="true"]');
667
700
  assert.equal(livePanel?.dataset.deviceId, focusedDevice.DeviceId);
668
- assert.equal(bodyView.querySelector('[data-remote-fleet-action="task-visible"]')?.disabled, false);
701
+ assert.equal(bodyView.querySelector('[data-remote-fleet-action="task-visible"]'), null);
702
+ assert.equal(bodyView.querySelector('[data-remote-fleet-action="task-connected"]'), null);
703
+ assert.equal(bodyView.querySelector('[data-remote-fleet-task-input="true"]'), null);
704
+ assert.equal(bodyView.querySelector('[data-remote-fleet-ai-toggle="true"]'), null);
669
705
  assert.ok(bodyView.textContent.includes('all devices, no paging'));
706
+ assert.equal(bodyView.querySelector('[data-remote-fleet-host-state="true"]')?.textContent.includes('Host: Inactive'), true);
707
+ const initialHostButton = bodyView.querySelector('[data-remote-fleet-action="set-host"]');
708
+ assert.equal(initialHostButton?.textContent, 'Set Host');
709
+ initialHostButton.dispatchEvent({ type: 'click' });
710
+ await wait();
711
+ const setHostCall = dotNetCalls.find(call => call.methodName === 'SetRemoteFleetHostFromJs');
712
+ assert.ok(setHostCall);
713
+ assert.equal(setHostCall.args[0], 'remote-fleet-render-smoke');
714
+ assert.equal(setHostCall.args[1], true);
715
+
716
+ hub.setHostTarget({
717
+ nodeId: 'remote-fleet-render-smoke',
718
+ enabled: true,
719
+ leaseMs: 30000
720
+ });
721
+ manager.renderRemoteFleetMonitorForTest(bodyView, buildMonitorNode(devices, hub.getStatus({ includeSecrets: true }), latestTaskBatch, recentTaskBatches));
722
+ assert.equal(bodyView.querySelector('[data-remote-fleet-host-state="true"]')?.textContent.includes('Host: Active'), true);
723
+ assert.equal(bodyView.querySelector('[data-remote-fleet-action="set-host"]')?.textContent, 'Hosting');
724
+ const stopHostButton = bodyView.querySelector('[data-remote-fleet-action="stop-host"]');
725
+ assert.ok(stopHostButton);
726
+ const stopHostStart = dotNetCalls.length;
727
+ stopHostButton.dispatchEvent({ type: 'click' });
728
+ await wait();
729
+ const stopHostCall = dotNetCalls
730
+ .slice(stopHostStart)
731
+ .find(call => call.methodName === 'SetRemoteFleetHostFromJs');
732
+ assert.ok(stopHostCall);
733
+ assert.equal(stopHostCall.args[0], 'remote-fleet-render-smoke');
734
+ assert.equal(stopHostCall.args[1], false);
735
+
670
736
  assert.match(bodyView.querySelector('[data-remote-fleet-manager-version="true"]')?.textContent || '', /^@mindexec\/cli render-smoke-manager - 127\.0\.0\.1:\d+$/);
671
737
  assert.ok(livePanel?.textContent.includes('RemoteFast 20 fps'), livePanel?.textContent || bodyView.textContent);
672
- assert.ok(bodyView.querySelector('code')?.textContent.includes('npx @mindexec/remote connect'));
738
+ assert.equal(bodyView.querySelector('code'), null);
739
+ assert.equal(bodyView.textContent.includes('npx @mindexec/remote connect'), false);
740
+ assert.equal(bodyView.querySelector('[data-remote-fleet-action="copy-command"]'), null);
741
+ assert.equal(bodyView.querySelector('[data-remote-fleet-action="refresh"]'), null);
742
+ assert.equal(bodyView.querySelector('[data-remote-fleet-auto-toggle="true"]'), null);
673
743
  assert.ok(bodyView.textContent.includes('synthetic-render-ai'));
674
744
  assert.ok(bodyView.textContent.includes('Task timed out waiting for the agent response.'));
675
745
  const batchSummary = bodyView.querySelector('[data-remote-fleet-task-batch="true"]');
@@ -719,63 +789,20 @@ try {
719
789
  assert.ok(bodyView.querySelectorAll('[data-remote-fleet-group-header="true"]').length >= 2);
720
790
  assert.equal(bodyView.querySelectorAll('article[data-device-id]').length, SYNTHETIC_COUNT);
721
791
 
722
- const taskInput = bodyView.querySelector('[data-remote-fleet-task-input="true"]');
723
- const sendVisibleButton = bodyView.querySelector('[data-remote-fleet-action="task-visible"]');
724
- assert.ok(taskInput);
725
- assert.ok(sendVisibleButton);
726
- taskInput.value = 'Dispatch smoke partial failure';
727
- sendVisibleButton.dispatchEvent({ type: 'click' });
728
- await wait();
729
- const batchCall = dotNetCalls.find(call => call.methodName === 'DispatchRemoteFleetTaskBatchFromJs');
730
- assert.ok(batchCall);
731
- const batchTargetIds = Array.isArray(batchCall.args[1]) ? batchCall.args[1] : [];
732
- assert.equal(batchTargetIds.length, connectedCount);
733
792
  const feedback = bodyView.querySelector('[data-remote-fleet-task-feedback="true"]');
734
793
  assert.ok(feedback);
735
- assert.equal(feedback.style.display, 'block');
736
- assert.match(feedback.textContent, new RegExp(`Queued ${connectedCount - 1}/${connectedCount} visible remote task\\(s\\); 1 failed`));
737
- assert.match(feedback.textContent, /device-ai-assist-unavailable/);
794
+ assert.equal(feedback.style.display, 'none');
738
795
 
739
796
  const activeSelects = bodyView.querySelectorAll('select');
740
797
  assert.equal(activeSelects.length, 4);
741
798
  const [aiFilterSelect] = activeSelects;
742
- const aiToggle = bodyView.querySelector('[data-remote-fleet-ai-toggle="true"]');
743
- assert.ok(aiToggle);
799
+ assert.equal(bodyView.querySelector('[data-remote-fleet-ai-toggle="true"]'), null);
744
800
  aiFilterSelect.value = 'ai';
745
801
  aiFilterSelect.dispatchEvent({ type: 'change' });
746
- aiToggle.checked = true;
747
- aiToggle.dispatchEvent({ type: 'change' });
748
802
  assert.equal(bodyView.querySelector('[data-remote-fleet-match-count="true"]')?.textContent, `${aiCount}/${SYNTHETIC_COUNT}`);
749
- assert.equal(sendVisibleButton.disabled, false);
750
- taskInput.value = 'Dispatch visible AI smoke';
751
- const aiDispatchCallStart = dotNetCalls.length;
752
- sendVisibleButton.dispatchEvent({ type: 'click' });
753
- await wait();
754
- const aiBatchCall = dotNetCalls
755
- .slice(aiDispatchCallStart)
756
- .find(call => call.methodName === 'DispatchRemoteFleetTaskBatchFromJs');
757
- assert.ok(aiBatchCall);
758
- const aiTargetIds = Array.isArray(aiBatchCall.args[1]) ? aiBatchCall.args[1] : [];
759
- const expectedAiTargetIds = devices
760
- .filter(device => device.Connected && device.AiAssistEnabled)
761
- .map(device => device.DeviceId)
762
- .sort();
763
- assert.deepEqual([...aiTargetIds].sort(), expectedAiTargetIds);
764
- assert.equal(aiBatchCall.args[3], true);
765
-
766
- const sendConnectedButton = bodyView.querySelector('[data-remote-fleet-action="task-connected"]');
767
- assert.ok(sendConnectedButton);
768
- taskInput.value = 'Dispatch all AI smoke';
769
- const allAiDispatchCallStart = dotNetCalls.length;
770
- sendConnectedButton.dispatchEvent({ type: 'click' });
771
- await wait();
772
- const allAiCall = dotNetCalls
773
- .slice(allAiDispatchCallStart)
774
- .find(call => call.methodName === 'DispatchRemoteFleetTaskFromJs');
775
- assert.ok(allAiCall);
776
- assert.equal(allAiCall.args[1], '');
777
- assert.equal(allAiCall.args[3], true);
778
- assert.match(feedback.textContent, new RegExp(`Queued ${aiCount}/${aiCount} AI task\\(s\\)`));
803
+ assert.equal(dotNetCalls.some(call =>
804
+ call.methodName === 'DispatchRemoteFleetTaskBatchFromJs'
805
+ || call.methodName === 'DispatchRemoteFleetTaskFromJs'), false);
779
806
 
780
807
  const deviceBody = document.createElement('div');
781
808
  document.body.appendChild(deviceBody);
@@ -51,11 +51,11 @@ async function fetchJson(url, options = {}) {
51
51
  };
52
52
  }
53
53
 
54
- async function waitForBridge(baseUrl, getFailureDetails) {
54
+ async function waitForBridge(baseUrl, getFailureDetails, bridgeToken = BRIDGE_TOKEN) {
55
55
  const startedAt = Date.now();
56
56
  while (Date.now() - startedAt < 30000) {
57
57
  try {
58
- const result = await fetchJson(`${baseUrl}/api/remote/status`, { token: BRIDGE_TOKEN });
58
+ const result = await fetchJson(`${baseUrl}/api/remote/status`, { token: bridgeToken });
59
59
  if (result.ok && result.payload?.started === true) {
60
60
  return result.payload;
61
61
  }
@@ -70,49 +70,96 @@ async function waitForBridge(baseUrl, getFailureDetails) {
70
70
 
71
71
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
72
72
  const bridgeRoot = path.resolve(__dirname, '..');
73
- const bridgePort = await findFreePort();
74
- const remoteHubPort = await findFreePort();
75
- const baseUrl = `http://127.0.0.1:${bridgePort}`;
76
-
77
- const child = spawn(process.execPath, ['server.js'], {
78
- cwd: bridgeRoot,
79
- stdio: ['ignore', 'pipe', 'pipe'],
80
- windowsHide: true,
81
- env: {
73
+
74
+ function spawnBridge({
75
+ bridgePort,
76
+ remoteHubPort,
77
+ bridgeToken = BRIDGE_TOKEN,
78
+ pairToken = PAIR_TOKEN,
79
+ syntheticFleetEnabled = false
80
+ }) {
81
+ const env = {
82
82
  ...process.env,
83
83
  BRIDGE_PORT: String(bridgePort),
84
- BRIDGE_TOKEN,
84
+ BRIDGE_TOKEN: bridgeToken,
85
85
  BRIDGE_REQUIRE_TOKEN: '1',
86
86
  MINDEXEC_REMOTE_HUB: '1',
87
87
  REMOTE_HUB_HOST: '127.0.0.1',
88
88
  REMOTE_HUB_PORT: String(remoteHubPort),
89
- REMOTE_HUB_PAIR_TOKEN: PAIR_TOKEN,
90
- MINDEXEC_REMOTE_SYNTHETIC_FLEET: '1',
89
+ REMOTE_HUB_PAIR_TOKEN: pairToken,
91
90
  NO_COLOR: '1'
91
+ };
92
+
93
+ delete env.MINDEXEC_REMOTE_SYNTHETIC_FLEET;
94
+ delete env.REMOTE_HUB_SYNTHETIC_FLEET;
95
+ if (syntheticFleetEnabled) {
96
+ env.MINDEXEC_REMOTE_SYNTHETIC_FLEET = '1';
92
97
  }
93
- });
94
-
95
- let stdout = '';
96
- let stderr = '';
97
- child.stdout.on('data', chunk => {
98
- stdout += chunk.toString();
99
- });
100
- child.stderr.on('data', chunk => {
101
- stderr += chunk.toString();
102
- });
103
-
104
- const exitPromise = new Promise(resolve => child.once('exit', resolve));
105
- const details = () => `stdout=${stdout}\nstderr=${stderr}`;
106
-
107
- try {
108
- const status = await waitForBridge(baseUrl, details);
98
+
99
+ const child = spawn(process.execPath, ['server.js'], {
100
+ cwd: bridgeRoot,
101
+ stdio: ['ignore', 'pipe', 'pipe'],
102
+ windowsHide: true,
103
+ env
104
+ });
105
+
106
+ let stdout = '';
107
+ let stderr = '';
108
+ child.stdout.on('data', chunk => {
109
+ stdout += chunk.toString();
110
+ });
111
+ child.stderr.on('data', chunk => {
112
+ stderr += chunk.toString();
113
+ });
114
+
115
+ const exitPromise = new Promise(resolve => child.once('exit', resolve));
116
+ const details = () => `stdout=${stdout}\nstderr=${stderr}`;
117
+ const stop = async () => {
118
+ if (child.exitCode === null && !child.killed) {
119
+ child.kill('SIGTERM');
120
+ await Promise.race([
121
+ exitPromise,
122
+ wait(5000)
123
+ ]);
124
+ if (child.exitCode === null && !child.killed) {
125
+ child.kill('SIGKILL');
126
+ }
127
+ }
128
+ };
129
+
130
+ return {
131
+ baseUrl: `http://127.0.0.1:${bridgePort}`,
132
+ bridgePort,
133
+ remoteHubPort,
134
+ bridgeToken,
135
+ pairToken,
136
+ child,
137
+ details,
138
+ stop
139
+ };
140
+ }
141
+
142
+ async function runSyntheticEnabledSmoke() {
143
+ const bridgePort = await findFreePort();
144
+ const remoteHubPort = await findFreePort();
145
+ const bridge = spawnBridge({
146
+ bridgePort,
147
+ remoteHubPort,
148
+ syntheticFleetEnabled: true
149
+ });
150
+ const { baseUrl } = bridge;
151
+ const details = bridge.details;
152
+
153
+ try {
154
+ const status = await waitForBridge(baseUrl, details, bridge.bridgeToken);
109
155
  assert.equal(status.started, true);
110
156
  assert.equal(status.port, remoteHubPort);
111
157
  assert.equal(status.managerPackage, '@mindexec/cli');
112
- assert.equal(status.managerVersion, '0.2.24');
158
+ assert.equal(status.managerVersion, '0.2.25');
113
159
  assert.equal(status.agentPackage, '@mindexec/remote');
114
160
  assert.equal(status.canvasPagination, 'none');
115
161
  assert.equal(status.canvasDeviceListMode, 'all-devices');
162
+ assert.equal(status.hostTargetActive, false);
116
163
  assert.equal(status.pairToken, PAIR_TOKEN);
117
164
 
118
165
  const unauthorizedStatus = await fetchJson(`${baseUrl}/api/remote/status`);
@@ -124,6 +171,58 @@ try {
124
171
  });
125
172
  assert.equal(unauthorizedDelete.status, 401);
126
173
 
174
+ const unauthorizedHost = await fetchJson(`${baseUrl}/api/remote/host-target`, {
175
+ method: 'POST',
176
+ body: JSON.stringify({
177
+ nodeId: 'remote-fleet-host-unauthorized',
178
+ enabled: true
179
+ })
180
+ });
181
+ assert.equal(unauthorizedHost.status, 401);
182
+
183
+ const setHost = await fetchJson(`${baseUrl}/api/remote/host-target`, {
184
+ method: 'POST',
185
+ token: BRIDGE_TOKEN,
186
+ body: JSON.stringify({
187
+ nodeId: 'remote-fleet-monitor-a',
188
+ enabled: true,
189
+ leaseMs: 30000
190
+ })
191
+ });
192
+ assert.equal(setHost.ok, true, JSON.stringify(setHost.payload));
193
+ assert.equal(setHost.payload?.ok, true);
194
+ assert.equal(setHost.payload?.active, true);
195
+ assert.equal(setHost.payload?.hostTarget?.nodeId, 'remote-fleet-monitor-a');
196
+ assert.equal(setHost.payload?.hostTarget?.endpoint, `127.0.0.1:${remoteHubPort}`);
197
+
198
+ const hostStatus = await fetchJson(`${baseUrl}/api/remote/status`, { token: BRIDGE_TOKEN });
199
+ assert.equal(hostStatus.ok, true, JSON.stringify(hostStatus.payload));
200
+ assert.equal(hostStatus.payload?.hostTargetActive, true);
201
+ assert.equal(hostStatus.payload?.hostTargetNodeId, 'remote-fleet-monitor-a');
202
+ assert.equal(hostStatus.payload?.hostTargetEndpoint, `127.0.0.1:${remoteHubPort}`);
203
+
204
+ const clearHostMismatch = await fetchJson(`${baseUrl}/api/remote/host-target`, {
205
+ method: 'DELETE',
206
+ token: BRIDGE_TOKEN,
207
+ body: JSON.stringify({
208
+ nodeId: 'remote-fleet-monitor-b'
209
+ })
210
+ });
211
+ assert.equal(clearHostMismatch.ok, true, JSON.stringify(clearHostMismatch.payload));
212
+ assert.equal(clearHostMismatch.payload?.ok, false);
213
+ assert.equal(clearHostMismatch.payload?.error, 'host-target-node-mismatch');
214
+
215
+ const clearHost = await fetchJson(`${baseUrl}/api/remote/host-target`, {
216
+ method: 'DELETE',
217
+ token: BRIDGE_TOKEN,
218
+ body: JSON.stringify({
219
+ nodeId: 'remote-fleet-monitor-a'
220
+ })
221
+ });
222
+ assert.equal(clearHost.ok, true, JSON.stringify(clearHost.payload));
223
+ assert.equal(clearHost.payload?.ok, true);
224
+ assert.equal(clearHost.payload?.active, false);
225
+
127
226
  const seed = await fetchJson(`${baseUrl}/api/remote/synthetic/seed`, {
128
227
  method: 'POST',
129
228
  token: BRIDGE_TOKEN,
@@ -283,14 +382,93 @@ try {
283
382
 
284
383
  console.log(`RemoteHub HTTP smoke OK (${SYNTHETIC_COUNT} synthetic devices, bridge ${bridgePort}, remote ${remoteHubPort})`);
285
384
  } finally {
286
- if (child.exitCode === null && !child.killed) {
287
- child.kill('SIGTERM');
288
- await Promise.race([
289
- exitPromise,
290
- wait(5000)
291
- ]);
292
- if (child.exitCode === null && !child.killed) {
293
- child.kill('SIGKILL');
294
- }
385
+ await bridge.stop();
386
+ }
387
+ }
388
+
389
+ async function runSyntheticDisabledSmoke() {
390
+ const bridgePort = await findFreePort();
391
+ const remoteHubPort = await findFreePort();
392
+ const bridgeToken = `${BRIDGE_TOKEN}-disabled`;
393
+ const pairToken = `${PAIR_TOKEN}-disabled`;
394
+ const bridge = spawnBridge({
395
+ bridgePort,
396
+ remoteHubPort,
397
+ bridgeToken,
398
+ pairToken,
399
+ syntheticFleetEnabled: false
400
+ });
401
+ const { baseUrl } = bridge;
402
+
403
+ try {
404
+ const status = await waitForBridge(baseUrl, bridge.details, bridge.bridgeToken);
405
+ assert.equal(status.started, true);
406
+ assert.equal(status.port, remoteHubPort);
407
+ assert.equal(status.pairToken, pairToken);
408
+ assert.equal(status.canvasPagination, 'none');
409
+ assert.equal(status.canvasDeviceListMode, 'all-devices');
410
+ assert.equal(status.hostTargetActive, false);
411
+
412
+ const seedDenied = await fetchJson(`${baseUrl}/api/remote/synthetic/seed`, {
413
+ method: 'POST',
414
+ token: bridge.bridgeToken,
415
+ body: JSON.stringify({
416
+ count: 3,
417
+ replace: true
418
+ })
419
+ });
420
+ assert.equal(seedDenied.status, 403);
421
+ assert.equal(seedDenied.payload?.ok, false);
422
+ assert.equal(seedDenied.payload?.error, 'synthetic-fleet-disabled');
423
+
424
+ const clearDenied = await fetchJson(`${baseUrl}/api/remote/synthetic`, {
425
+ method: 'DELETE',
426
+ token: bridge.bridgeToken
427
+ });
428
+ assert.equal(clearDenied.status, 403);
429
+ assert.equal(clearDenied.payload?.ok, false);
430
+ assert.equal(clearDenied.payload?.error, 'synthetic-fleet-disabled');
431
+
432
+ const devicesResult = await fetchJson(`${baseUrl}/api/remote/devices`, { token: bridge.bridgeToken });
433
+ assert.equal(devicesResult.ok, true, JSON.stringify(devicesResult.payload));
434
+ assert.equal(devicesResult.payload?.total, 0);
435
+ assert.equal(devicesResult.payload?.pagination, 'none');
436
+ assert.equal(devicesResult.payload?.canvasDeviceListMode, 'all-devices');
437
+ assert.equal(devicesResult.payload?.devices?.length, 0);
438
+
439
+ const emptyBatch = await fetchJson(`${baseUrl}/api/remote/tasks`, {
440
+ method: 'POST',
441
+ token: bridge.bridgeToken,
442
+ body: JSON.stringify({
443
+ allConnected: true,
444
+ title: 'HTTP smoke no target batch',
445
+ instruction: 'This should not queue because no remote devices are connected.',
446
+ approvalLevel: 'task-only'
447
+ })
448
+ });
449
+ assert.equal(emptyBatch.ok, true, JSON.stringify(emptyBatch.payload));
450
+ assert.equal(emptyBatch.payload?.ok, false);
451
+ assert.equal(emptyBatch.payload?.total, 0);
452
+ assert.equal(emptyBatch.payload?.queued, 0);
453
+ assert.equal(emptyBatch.payload?.error, 'no-target-devices');
454
+
455
+ const hostTarget = await fetchJson(`${baseUrl}/api/remote/host-target`, {
456
+ method: 'POST',
457
+ token: bridge.bridgeToken,
458
+ body: JSON.stringify({
459
+ nodeId: 'disabled-synthetic-host',
460
+ enabled: true
461
+ })
462
+ });
463
+ assert.equal(hostTarget.ok, true, JSON.stringify(hostTarget.payload));
464
+ assert.equal(hostTarget.payload?.ok, true);
465
+ assert.equal(hostTarget.payload?.active, true);
466
+
467
+ console.log(`RemoteHub HTTP safety smoke OK (synthetic disabled, bridge ${bridgePort}, remote ${remoteHubPort})`);
468
+ } finally {
469
+ await bridge.stop();
295
470
  }
296
471
  }
472
+
473
+ await runSyntheticEnabledSmoke();
474
+ await runSyntheticDisabledSmoke();