@mindexec/cli 0.2.153 → 0.2.154

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mindexec/cli",
3
- "version": "0.2.153",
3
+ "version": "0.2.154",
4
4
  "description": "MindExec local runtime and bridge CLI",
5
5
  "main": "server.js",
6
6
  "type": "module",
@@ -865,6 +865,18 @@ try {
865
865
  };
866
866
  }
867
867
 
868
+ if (methodName === 'AddRemoteFleetDeviceNodeFromJs') {
869
+ const deviceId = String(args[1] || '');
870
+ return {
871
+ success: true,
872
+ nodeId: 'remote-device-render-smoke',
873
+ deviceId,
874
+ nodeState: buildDeviceNode(
875
+ devices.find(device => device.DeviceId === deviceId) || focusedDevice,
876
+ hub.getStatus())
877
+ };
878
+ }
879
+
868
880
  return { success: true };
869
881
  }
870
882
  };
@@ -1095,133 +1107,16 @@ try {
1095
1107
  assert.equal(rerenderedPatchPreview.dataset.remoteFleetFrameSeq, String(websocketFrameSeq));
1096
1108
  assert.equal(rerenderedPatchPreview.querySelector('canvas[data-remote-fleet-frame-canvas="true"]')?.style.display, 'block');
1097
1109
 
1098
- const liveStartCallsBeforeControl = dotNetCalls.filter(call => call.methodName === 'StartRemoteFleetLiveStreamFromJs').length;
1099
- const sendCountBeforeControl = diagnostics.webSocketSends.length;
1100
- const drawCountBeforeControl = document.canvasDrawCalls.length;
1101
1110
  const controlCard = bodyView.querySelector(`article[data-device-id="${patchTarget.DeviceId}"]`);
1102
1111
  assert.ok(controlCard);
1103
1112
  controlCard.dispatchEvent({ type: 'dblclick', button: 0 });
1104
1113
  await wait(30);
1105
- const controlPopup = document.body.querySelector('[data-remote-fleet-control-popup="true"]');
1106
- assert.ok(controlPopup, 'double-clicking an MDM tile should open the control popup');
1107
- const controlShell = controlPopup.querySelector('[data-remote-fleet-control-shell="true"]');
1108
- const controlStage = controlPopup.querySelector('[data-remote-fleet-control-stage="true"]');
1109
- const controlCanvas = controlPopup.querySelector('[data-remote-fleet-control-canvas="true"]');
1110
- assert.ok(controlShell);
1111
- assert.ok(controlStage);
1112
- assert.ok(controlCanvas);
1113
- assert.equal(document.activeElement, controlShell);
1114
- const controlStartCalls = dotNetCalls
1115
- .filter(call => call.methodName === 'StartRemoteFleetLiveStreamFromJs')
1116
- .slice(liveStartCallsBeforeControl);
1117
- assert.equal(controlStartCalls.length, 1);
1118
- assert.equal(controlStartCalls[0].args[0], 'remote-fleet-render-smoke');
1119
- assert.equal(controlStartCalls[0].args[1], patchTarget.DeviceId);
1120
- assert.equal(controlStartCalls[0].args[2], 20);
1121
- const controlSubscribe = diagnostics.webSocketSends
1122
- .slice(sendCountBeforeControl)
1123
- .map(send => {
1124
- try {
1125
- return JSON.parse(send.payload);
1126
- } catch {
1127
- return null;
1128
- }
1129
- })
1130
- .find(message =>
1131
- message
1132
- && message.type === 'subscribe'
1133
- && message.nodeId === 'remote-fleet-render-smoke'
1134
- && message.autoStartLive === true
1135
- && message.fps === 20
1136
- && Array.isArray(message.deviceIds)
1137
- && message.deviceIds.includes(patchTarget.DeviceId));
1138
- assert.ok(controlSubscribe, 'control popup should reuse the binary frame WebSocket with a 20fps subscription');
1139
-
1140
- const controlFrameSeq = 20001;
1141
- liveWs.onmessage?.({
1142
- data: new Blob([encodeRemoteBinaryFrame({
1143
- type: 'remote.frame.binary',
1144
- deviceId: patchTarget.DeviceId,
1145
- kind: 'live',
1146
- frameSeq: controlFrameSeq,
1147
- streamId: 'render-smoke-control-live',
1148
- contentHash: `render-smoke-control-live-${controlFrameSeq}`,
1149
- mimeType: 'image/jpeg',
1150
- width: 64,
1151
- height: 36,
1152
- capturedAt: new Date().toISOString(),
1153
- receivedAt: new Date().toISOString()
1154
- })])
1155
- });
1156
- await wait(40);
1157
- assert.equal(controlCanvas.style.display, 'block');
1158
- assert.ok(
1159
- document.canvasDrawCalls
1160
- .slice(drawCountBeforeControl)
1161
- .some(call => call.canvas === controlCanvas && call.op === 'drawImage'),
1162
- 'control popup should paint binary WebSocket frames directly to its canvas');
1163
-
1164
- controlStage.dispatchEvent({
1165
- type: 'pointerdown',
1166
- button: 0,
1167
- pointerId: 7,
1168
- clientX: 160,
1169
- clientY: 90
1170
- });
1171
- controlStage.dispatchEvent({
1172
- type: 'pointerup',
1173
- button: 0,
1174
- pointerId: 7,
1175
- clientX: 160,
1176
- clientY: 90
1177
- });
1178
- controlShell.dispatchEvent({
1179
- type: 'keydown',
1180
- key: 'A',
1181
- code: 'KeyA',
1182
- repeat: false
1183
- });
1184
- controlShell.dispatchEvent({
1185
- type: 'keyup',
1186
- key: 'A',
1187
- code: 'KeyA',
1188
- repeat: false
1189
- });
1190
- await wait(50);
1191
- const inputWsSends = diagnostics.webSocketSends
1192
- .filter(send => send.url.includes('/api/remote/input/ws'))
1193
- .map(send => {
1194
- try {
1195
- return JSON.parse(send.payload);
1196
- } catch {
1197
- return null;
1198
- }
1199
- })
1200
- .filter(Boolean);
1201
- assert.ok(inputWsSends.length >= 3, 'control popup should send pointer and keyboard input over the input WebSocket');
1202
- const pointerDown = inputWsSends.find(message => message.input?.type === 'pointerDown');
1203
- const keyDown = inputWsSends.find(message => message.input?.type === 'keyDown');
1204
- const keyUp = inputWsSends.find(message => message.input?.type === 'keyUp');
1205
- assert.ok(pointerDown);
1206
- assert.ok(keyDown);
1207
- assert.ok(keyUp);
1208
- assert.equal(pointerDown.nodeId, 'remote-fleet-render-smoke');
1209
- assert.equal(pointerDown.deviceId, patchTarget.DeviceId);
1210
- assert.equal(pointerDown.input.button, 'left');
1211
- assert.ok(pointerDown.input.controlLeaseId);
1212
- assert.ok(pointerDown.input.normalizedX >= 0 && pointerDown.input.normalizedX <= 1);
1213
- assert.ok(pointerDown.input.normalizedY >= 0 && pointerDown.input.normalizedY <= 1);
1214
- assert.equal(keyDown.input.key, 'A');
1215
- assert.equal(keyDown.input.code, 'KeyA');
1216
- assert.equal(keyUp.input.key, 'A');
1217
- assert.equal(keyUp.input.code, 'KeyA');
1218
- assert.equal(
1219
- dotNetCalls.filter(call => call.methodName === 'SendRemoteFleetInputFromJs').length,
1220
- 0,
1221
- 'WebSocket input path should not use the slower DotNet input fallback in the happy path');
1222
- manager.closeRemoteFleetControlPopupForTest('render-smoke');
1223
- await wait(20);
1224
1114
  assert.equal(document.body.querySelector('[data-remote-fleet-control-popup="true"]'), null);
1115
+ const addControlNodeCall = dotNetCalls.find(call =>
1116
+ call.methodName === 'AddRemoteFleetDeviceNodeFromJs'
1117
+ && call.args[0] === 'remote-fleet-render-smoke'
1118
+ && call.args[1] === patchTarget.DeviceId);
1119
+ assert.ok(addControlNodeCall, 'double-clicking an MDM tile should create a remote-control canvas node');
1225
1120
 
1226
1121
  const alternateDevice = devices.find(device =>
1227
1122
  device.Connected === true
@@ -1305,8 +1200,7 @@ try {
1305
1200
  assert.ok(devices.some(device => /^synthetic-response-/.test(device.LatestTaskResultResponseId)));
1306
1201
  await wait(20);
1307
1202
  const liveStartCalls = dotNetCalls
1308
- .filter(call => call.methodName === 'StartRemoteFleetLiveStreamFromJs')
1309
- .slice(0, liveStartCallsBeforeControl);
1203
+ .filter(call => call.methodName === 'StartRemoteFleetLiveStreamFromJs');
1310
1204
  const subscribeMessages = diagnostics.webSocketSends
1311
1205
  .map(send => {
1312
1206
  try {
@@ -1392,9 +1286,120 @@ try {
1392
1286
  child !== errorFooter && (child.textContent || '').includes('RemoteHub is unavailable')), false);
1393
1287
 
1394
1288
  const deviceBody = document.createElement('div');
1289
+ deviceBody.dataset.nodeId = 'remote-device-render-smoke';
1395
1290
  document.body.appendChild(deviceBody);
1291
+ const sendCountBeforeControlNode = diagnostics.webSocketSends.length;
1292
+ const drawCountBeforeControlNode = document.canvasDrawCalls.length;
1396
1293
  manager.renderRemoteFleetDeviceForTest(deviceBody, buildDeviceNode(focusedDevice, hub.getStatus()));
1397
1294
  assert.ok(deviceBody.textContent.includes(focusedDevice.Name));
1295
+ const deviceControlStage = deviceBody.querySelector('[data-remote-fleet-control-node-stage="true"]');
1296
+ const deviceControlCanvas = deviceBody.querySelector('[data-remote-fleet-control-node-canvas="true"]');
1297
+ assert.ok(deviceControlStage);
1298
+ assert.ok(deviceControlCanvas);
1299
+ assert.equal(document.activeElement, deviceControlStage);
1300
+ await wait(30);
1301
+ const deviceControlSubscribe = diagnostics.webSocketSends
1302
+ .slice(sendCountBeforeControlNode)
1303
+ .map(send => {
1304
+ try {
1305
+ return JSON.parse(send.payload);
1306
+ } catch {
1307
+ return null;
1308
+ }
1309
+ })
1310
+ .find(message =>
1311
+ message
1312
+ && message.type === 'subscribe'
1313
+ && message.nodeId === 'remote-device-render-smoke'
1314
+ && message.autoStartLive === true
1315
+ && message.fps === 20
1316
+ && Array.isArray(message.deviceIds)
1317
+ && message.deviceIds.includes(focusedDevice.DeviceId));
1318
+ assert.ok(deviceControlSubscribe, 'RemoteFleetDevice node should subscribe to 20fps binary frames');
1319
+ const deviceControlWs = diagnostics.webSocketConnections
1320
+ .filter(ws => ws.url.includes('/api/remote/frames/ws'))
1321
+ .at(-1);
1322
+ assert.ok(deviceControlWs, 'RemoteFleetDevice node should open a binary frame WebSocket');
1323
+ const controlNodeFrameSeq = 30001;
1324
+ deviceControlWs.onmessage?.({
1325
+ data: new Blob([encodeRemoteBinaryFrame({
1326
+ type: 'remote.frame.binary',
1327
+ deviceId: focusedDevice.DeviceId,
1328
+ kind: 'live',
1329
+ frameSeq: controlNodeFrameSeq,
1330
+ streamId: 'render-smoke-control-node-live',
1331
+ contentHash: `render-smoke-control-node-live-${controlNodeFrameSeq}`,
1332
+ mimeType: 'image/jpeg',
1333
+ width: 64,
1334
+ height: 36,
1335
+ capturedAt: new Date().toISOString(),
1336
+ receivedAt: new Date().toISOString()
1337
+ })])
1338
+ });
1339
+ await wait(40);
1340
+ assert.ok(
1341
+ document.canvasDrawCalls
1342
+ .slice(drawCountBeforeControlNode)
1343
+ .some(call => call.canvas === deviceControlCanvas && call.op === 'drawImage'),
1344
+ 'RemoteFleetDevice node should paint binary WebSocket frames directly to its canvas');
1345
+ deviceControlStage.dispatchEvent({
1346
+ type: 'pointerdown',
1347
+ button: 0,
1348
+ pointerId: 11,
1349
+ clientX: 160,
1350
+ clientY: 90
1351
+ });
1352
+ deviceControlStage.dispatchEvent({
1353
+ type: 'pointerup',
1354
+ button: 0,
1355
+ pointerId: 11,
1356
+ clientX: 160,
1357
+ clientY: 90
1358
+ });
1359
+ deviceControlStage.dispatchEvent({
1360
+ type: 'keydown',
1361
+ key: 'A',
1362
+ code: 'KeyA',
1363
+ repeat: false
1364
+ });
1365
+ deviceControlStage.dispatchEvent({
1366
+ type: 'keyup',
1367
+ key: 'A',
1368
+ code: 'KeyA',
1369
+ repeat: false
1370
+ });
1371
+ await wait(50);
1372
+ const inputWsSends = diagnostics.webSocketSends
1373
+ .filter(send => send.url.includes('/api/remote/input/ws'))
1374
+ .map(send => {
1375
+ try {
1376
+ return JSON.parse(send.payload);
1377
+ } catch {
1378
+ return null;
1379
+ }
1380
+ })
1381
+ .filter(Boolean);
1382
+ assert.ok(inputWsSends.length >= 3, 'RemoteFleetDevice node should send pointer and keyboard input over the input WebSocket');
1383
+ const pointerDown = inputWsSends.find(message => message.input?.type === 'pointerDown');
1384
+ const keyDown = inputWsSends.find(message => message.input?.type === 'keyDown');
1385
+ const keyUp = inputWsSends.find(message => message.input?.type === 'keyUp');
1386
+ assert.ok(pointerDown);
1387
+ assert.ok(keyDown);
1388
+ assert.ok(keyUp);
1389
+ assert.equal(pointerDown.nodeId, 'remote-device-render-smoke');
1390
+ assert.equal(pointerDown.deviceId, focusedDevice.DeviceId);
1391
+ assert.equal(pointerDown.input.button, 'left');
1392
+ assert.ok(pointerDown.input.controlLeaseId);
1393
+ assert.ok(pointerDown.input.normalizedX >= 0 && pointerDown.input.normalizedX <= 1);
1394
+ assert.ok(pointerDown.input.normalizedY >= 0 && pointerDown.input.normalizedY <= 1);
1395
+ assert.equal(keyDown.input.key, 'A');
1396
+ assert.equal(keyDown.input.code, 'KeyA');
1397
+ assert.equal(keyUp.input.key, 'A');
1398
+ assert.equal(keyUp.input.code, 'KeyA');
1399
+ assert.equal(
1400
+ dotNetCalls.filter(call => call.methodName === 'SendRemoteFleetInputFromJs').length,
1401
+ 0,
1402
+ 'WebSocket input path should not use the slower DotNet input fallback in the happy path');
1398
1403
  assert.ok(deviceBody.querySelector('[data-remote-fleet-action="refresh-device"]'));
1399
1404
  assert.ok(deviceBody.querySelector('[data-remote-fleet-action="task-device"]'));
1400
1405
  assert.ok(deviceBody.textContent.includes('Task timed out waiting for the agent response.'));
@@ -13730,6 +13730,7 @@
13730
13730
 
13731
13731
  function clearRemoteFleetTimers(bodyView, options = {}) {
13732
13732
  if (!bodyView) return;
13733
+ closeRemoteFleetControlNodeSession(bodyView, 'clear');
13733
13734
  if (options.keepBinaryFrameSocket !== true) {
13734
13735
  releaseRemoteFleetBinaryFrameSocket(bodyView);
13735
13736
  }
@@ -15867,6 +15868,8 @@
15867
15868
 
15868
15869
  drawRemoteFleetFrameToCanvas(session.canvas, bitmap, 'contain');
15869
15870
  session.canvas.style.display = 'block';
15871
+ session.placeholder?.remove?.();
15872
+ session.placeholder = null;
15870
15873
  if (typeof bitmap.close === 'function') {
15871
15874
  bitmap.close();
15872
15875
  }
@@ -15917,6 +15920,8 @@
15917
15920
  session.canvas.style.display = 'block';
15918
15921
  session.lastControlFrameAt = Date.now();
15919
15922
  session.status.textContent = `Control ${REMOTE_FLEET_CONTROL_LIVE_FPS}fps`;
15923
+ session.placeholder?.remove?.();
15924
+ session.placeholder = null;
15920
15925
  if (typeof bitmap.close === 'function') {
15921
15926
  bitmap.close();
15922
15927
  }
@@ -16211,16 +16216,45 @@
16211
16216
  }, Math.max(1, REMOTE_FLEET_CONTROL_INPUT_MOVE_MS - elapsed));
16212
16217
  }
16213
16218
 
16214
- function bindRemoteFleetControlInput(session) {
16219
+ function isRemoteFleetControlBypassTarget(target, session) {
16220
+ let current = target || null;
16221
+ while (current) {
16222
+ if (current === session?.stage || current === session?.shell) {
16223
+ return false;
16224
+ }
16225
+
16226
+ const tag = String(current.tagName || '').toLowerCase();
16227
+ if (current?.dataset?.remoteFleetControlClose === 'true'
16228
+ || tag === 'button'
16229
+ || tag === 'input'
16230
+ || tag === 'textarea'
16231
+ || tag === 'select'
16232
+ || String(current.getAttribute?.('contenteditable') || '').toLowerCase() === 'true') {
16233
+ return true;
16234
+ }
16235
+
16236
+ current = current.parentElement || null;
16237
+ }
16238
+
16239
+ return false;
16240
+ }
16241
+
16242
+ function bindRemoteFleetControlInput(session, options = {}) {
16215
16243
  const { overlay, shell, stage, canvas } = session;
16216
16244
  let pointerActive = false;
16217
16245
 
16218
- ['mousedown', 'mouseup', 'click', 'dblclick', 'keydown', 'keyup', 'wheel', 'contextmenu'].forEach(eventName => {
16219
- overlay.addEventListener(eventName, event => event.stopPropagation(), true);
16220
- });
16246
+ if (options.captureOverlayEvents !== false) {
16247
+ ['mousedown', 'mouseup', 'click', 'dblclick', 'keydown', 'keyup', 'wheel', 'contextmenu'].forEach(eventName => {
16248
+ overlay.addEventListener(eventName, event => {
16249
+ if (!isRemoteFleetControlBypassTarget(event.target, session)) {
16250
+ event.stopPropagation();
16251
+ }
16252
+ }, true);
16253
+ });
16254
+ }
16221
16255
 
16222
16256
  const shouldIgnoreKeyboardEvent = event =>
16223
- event?.target?.closest?.('[data-remote-fleet-control-close="true"]');
16257
+ isRemoteFleetControlBypassTarget(event?.target, session);
16224
16258
 
16225
16259
  const handleKeyboardEvent = (event, type) => {
16226
16260
  if (!session.active || shouldIgnoreKeyboardEvent(event) || event.__remoteFleetControlHandled === true) {
@@ -16241,12 +16275,13 @@
16241
16275
 
16242
16276
  const handleWindowKeyDown = event => handleKeyboardEvent(event, 'keyDown');
16243
16277
  const handleWindowKeyUp = event => handleKeyboardEvent(event, 'keyUp');
16244
- if (typeof window.addEventListener === 'function') {
16278
+ const captureWindowKeyboard = options.windowKeyboardCapture !== false;
16279
+ if (captureWindowKeyboard && typeof window.addEventListener === 'function') {
16245
16280
  window.addEventListener('keydown', handleWindowKeyDown, true);
16246
16281
  window.addEventListener('keyup', handleWindowKeyUp, true);
16247
16282
  }
16248
16283
  session.inputCleanup = () => {
16249
- if (typeof window.removeEventListener === 'function') {
16284
+ if (captureWindowKeyboard && typeof window.removeEventListener === 'function') {
16250
16285
  window.removeEventListener('keydown', handleWindowKeyDown, true);
16251
16286
  window.removeEventListener('keyup', handleWindowKeyUp, true);
16252
16287
  }
@@ -16584,6 +16619,93 @@
16584
16619
  });
16585
16620
  }
16586
16621
 
16622
+ function closeRemoteFleetControlNodeSession(bodyView, reason = 'close') {
16623
+ const session = bodyView?._remoteFleetControlNodeSession || null;
16624
+ if (!session) {
16625
+ return;
16626
+ }
16627
+
16628
+ bodyView._remoteFleetControlNodeSession = null;
16629
+ session.active = false;
16630
+ detachRemoteFleetControlBinaryFrameSession(session);
16631
+ closeRemoteFleetControlInputSocket(session, 'close');
16632
+ session.inputCleanup?.();
16633
+ if (session.moveTimer) {
16634
+ clearTimeout(session.moveTimer);
16635
+ session.moveTimer = null;
16636
+ }
16637
+ session.pendingMove = null;
16638
+ window.RuntimeTrace?.emit?.('remote.control.nodeClosed', {
16639
+ nodeId: session.nodeId,
16640
+ deviceId: session.deviceId,
16641
+ reason
16642
+ });
16643
+ }
16644
+
16645
+ function startRemoteFleetControlNodeSession(bodyView, nodeId, deviceId, stage, canvas, status, placeholder = null) {
16646
+ const normalizedNodeId = String(nodeId || '').trim();
16647
+ const normalizedDeviceId = String(deviceId || '').trim();
16648
+ if (!bodyView || !normalizedNodeId || !normalizedDeviceId || !stage || !canvas) {
16649
+ return null;
16650
+ }
16651
+
16652
+ closeRemoteFleetControlNodeSession(bodyView, 'replace');
16653
+ const session = {
16654
+ active: true,
16655
+ nodeId: normalizedNodeId,
16656
+ deviceId: normalizedDeviceId,
16657
+ overlay: bodyView,
16658
+ shell: stage,
16659
+ stage,
16660
+ canvas,
16661
+ status: status || { textContent: '' },
16662
+ placeholder,
16663
+ wasLiveActive: false,
16664
+ startedStream: false,
16665
+ streamId: '',
16666
+ controlLeaseId: `control-node-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`,
16667
+ timer: null,
16668
+ moveTimer: null,
16669
+ pendingMove: null,
16670
+ lastMoveSentAt: 0,
16671
+ lastControlFrameAt: 0,
16672
+ lastControlFrameSeq: 0,
16673
+ binaryFramePreferred: false,
16674
+ inputWs: null,
16675
+ inputWsConnecting: false,
16676
+ inputWsConnectToken: '',
16677
+ inputWsOpenTimer: null,
16678
+ inputWsUnavailableUntil: 0,
16679
+ inputQueue: [],
16680
+ inputCleanup: null
16681
+ };
16682
+
16683
+ bodyView._remoteFleetControlNodeSession = session;
16684
+ bindRemoteFleetControlInput(session, {
16685
+ captureOverlayEvents: false,
16686
+ windowKeyboardCapture: false
16687
+ });
16688
+ ensureRemoteFleetControlInputSocket(session);
16689
+ startRemoteFleetBinaryFrameSocket(bodyView, () => [normalizedDeviceId], {
16690
+ autoStartLive: true,
16691
+ fps: REMOTE_FLEET_CONTROL_LIVE_FPS,
16692
+ maxWidth: 1280,
16693
+ maxHeight: 720,
16694
+ quality: 65,
16695
+ mode: 'remote-fast-control-node'
16696
+ });
16697
+ attachRemoteFleetControlBinaryFrameSession(session, bodyView);
16698
+ stage.focus?.({ preventScroll: true });
16699
+ if (status) {
16700
+ status.textContent = `Control ${REMOTE_FLEET_CONTROL_LIVE_FPS}fps`;
16701
+ }
16702
+ window.RuntimeTrace?.emit?.('remote.control.nodeOpened', {
16703
+ nodeId: normalizedNodeId,
16704
+ deviceId: normalizedDeviceId
16705
+ });
16706
+ return session;
16707
+ }
16708
+
16587
16709
  function isRemoteFleetLiveActive(device) {
16588
16710
  return getRemoteFleetDeviceField(device, 'liveStreamActive', 'LiveStreamActive', false) === true;
16589
16711
  }
@@ -16828,7 +16950,7 @@
16828
16950
 
16829
16951
  if (!device || !deviceId) {
16830
16952
  const empty = document.createElement('div');
16831
- empty.textContent = lastError || 'Pinned remote device snapshot is unavailable.';
16953
+ empty.textContent = lastError || 'Remote control device snapshot is unavailable.';
16832
16954
  empty.style.cssText = `
16833
16955
  flex: 1 1 auto;
16834
16956
  display: flex;
@@ -16844,7 +16966,7 @@
16844
16966
  text-align: center;
16845
16967
  background: rgba(255, 255, 255, 0.74);
16846
16968
  `;
16847
- const refreshButton = createRemoteFleetButton('Refresh', 'Refresh this pinned device', 'refresh-device');
16969
+ const refreshButton = createRemoteFleetButton('Refresh', 'Refresh this remote control device', 'refresh-device');
16848
16970
  refreshButton.style.height = '32px';
16849
16971
  refreshButton.addEventListener('click', async event => {
16850
16972
  event.preventDefault();
@@ -16923,52 +17045,109 @@
16923
17045
  header.appendChild(statusBadge);
16924
17046
  bodyView.appendChild(header);
16925
17047
 
16926
- const preview = document.createElement('div');
16927
- preview.dataset.remoteFleetDevicePreview = 'pinned';
16928
- preview.dataset.deviceId = deviceId;
16929
- preview.dataset.remoteFleetFrameKind = hasLiveFrame ? 'live' : (hasThumbnail ? 'thumbnail' : '');
16930
- preview.dataset.remoteFleetFrameSeq = String(hasLiveFrame
16931
- ? getRemoteFleetDeviceField(device, 'liveFrameSeq', 'LiveFrameSeq', 0)
16932
- : getRemoteFleetDeviceField(device, 'thumbnailFrameSeq', 'ThumbnailFrameSeq', 0));
16933
- preview.style.cssText = `
17048
+ const controlStage = document.createElement('div');
17049
+ controlStage.dataset.remoteFleetControlNodeStage = 'true';
17050
+ controlStage.dataset.deviceId = deviceId;
17051
+ controlStage.tabIndex = 0;
17052
+ controlStage.title = `${name} remote control`;
17053
+ controlStage.style.cssText = `
16934
17054
  position: relative;
16935
- flex: 0 0 auto;
17055
+ flex: 1 1 auto;
17056
+ min-height: 260px;
16936
17057
  width: 100%;
16937
- aspect-ratio: 16 / 9;
16938
17058
  overflow: hidden;
16939
- border-radius: 8px;
16940
- background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%);
16941
- border: 1px solid rgba(15, 23, 42, 0.16);
17059
+ border-radius: 0;
17060
+ background: #020617;
17061
+ border: 1px solid rgba(15, 23, 42, 0.18);
17062
+ cursor: crosshair;
17063
+ touch-action: none;
17064
+ outline: none;
16942
17065
  `;
16943
- if (hasLiveFrame || hasThumbnail) {
16944
- const image = document.createElement('img');
16945
- image.dataset.remoteFleetFrameImage = 'true';
16946
- image.dataset.remoteFleetFrameKind = hasLiveFrame ? 'live' : 'thumbnail';
16947
- image.dataset.remoteFleetFrameSeq = preview.dataset.remoteFleetFrameSeq;
16948
- image.src = previewSource;
16949
- image.alt = `${name} screen`;
16950
- image.loading = 'lazy';
16951
- image.decoding = 'async';
16952
- image.style.cssText = 'width:100%;height:100%;object-fit:cover;display:block;';
16953
- preview.appendChild(image);
16954
- } else {
16955
- const placeholder = document.createElement('div');
16956
- placeholder.dataset.remoteFleetScreenPlaceholder = 'true';
16957
- placeholder.textContent = thumbnailEnabled ? 'No frame yet' : 'Status only';
16958
- placeholder.style.cssText = `
16959
- position: absolute;
16960
- inset: 0;
16961
- display: flex;
16962
- align-items: center;
16963
- justify-content: center;
16964
- color: rgba(226, 232, 240, 0.78);
16965
- font-size: 12px;
16966
- font-weight: 900;
16967
- letter-spacing: 0;
16968
- `;
16969
- preview.appendChild(placeholder);
17066
+ const controlCanvas = document.createElement('canvas');
17067
+ controlCanvas.dataset.remoteFleetControlNodeCanvas = 'true';
17068
+ controlCanvas.style.cssText = `
17069
+ position: absolute;
17070
+ inset: 0;
17071
+ width: 100%;
17072
+ height: 100%;
17073
+ display: block;
17074
+ background: #020617;
17075
+ `;
17076
+ const controlOverlay = document.createElement('div');
17077
+ controlOverlay.style.cssText = `
17078
+ position: absolute;
17079
+ left: 8px;
17080
+ top: 8px;
17081
+ display: flex;
17082
+ align-items: center;
17083
+ gap: 6px;
17084
+ pointer-events: none;
17085
+ z-index: 2;
17086
+ `;
17087
+ const controlDot = document.createElement('span');
17088
+ controlDot.style.cssText = `
17089
+ width: 7px;
17090
+ height: 7px;
17091
+ border-radius: 50%;
17092
+ background: ${connected ? '#10b981' : '#94a3b8'};
17093
+ box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.72);
17094
+ `;
17095
+ const controlStatus = document.createElement('span');
17096
+ controlStatus.dataset.remoteFleetControlNodeStatus = 'true';
17097
+ controlStatus.textContent = connected ? 'Connecting' : 'Offline';
17098
+ controlStatus.style.cssText = `
17099
+ color: rgba(248, 250, 252, 0.94);
17100
+ font-size: 10px;
17101
+ font-weight: 900;
17102
+ line-height: 1;
17103
+ padding: 4px 6px;
17104
+ border-radius: 999px;
17105
+ background: rgba(15, 23, 42, 0.72);
17106
+ letter-spacing: 0;
17107
+ `;
17108
+ const controlPlaceholder = document.createElement('div');
17109
+ controlPlaceholder.dataset.remoteFleetControlNodePlaceholder = 'true';
17110
+ controlPlaceholder.textContent = connected ? 'Waiting for screen' : 'Offline';
17111
+ controlPlaceholder.style.cssText = `
17112
+ position: absolute;
17113
+ inset: 0;
17114
+ display: flex;
17115
+ align-items: center;
17116
+ justify-content: center;
17117
+ color: rgba(226, 232, 240, 0.74);
17118
+ font-size: 12px;
17119
+ font-weight: 900;
17120
+ letter-spacing: 0;
17121
+ pointer-events: none;
17122
+ `;
17123
+ controlOverlay.appendChild(controlDot);
17124
+ controlOverlay.appendChild(controlStatus);
17125
+ controlStage.appendChild(controlCanvas);
17126
+ controlStage.appendChild(controlPlaceholder);
17127
+ controlStage.appendChild(controlOverlay);
17128
+ bodyView.appendChild(controlStage);
17129
+
17130
+ if (connected) {
17131
+ const session = startRemoteFleetControlNodeSession(
17132
+ bodyView,
17133
+ nodeId,
17134
+ deviceId,
17135
+ controlStage,
17136
+ controlCanvas,
17137
+ controlStatus,
17138
+ controlPlaceholder);
17139
+ if (session && isRemoteFleetFrameSource(previewSource)) {
17140
+ paintRemoteFleetControlFrame(session, {
17141
+ deviceId,
17142
+ kind: hasLiveFrame ? 'live' : 'thumbnail',
17143
+ frameSeq: hasLiveFrame
17144
+ ? getRemoteFleetDeviceField(device, 'liveFrameSeq', 'LiveFrameSeq', 0)
17145
+ : getRemoteFleetDeviceField(device, 'thumbnailFrameSeq', 'ThumbnailFrameSeq', 0),
17146
+ frameUrl: previewSource,
17147
+ receivedAt: previewAt
17148
+ }).catch(() => undefined);
17149
+ }
16970
17150
  }
16971
- bodyView.appendChild(preview);
16972
17151
 
16973
17152
  const stats = document.createElement('div');
16974
17153
  stats.style.cssText = 'display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:7px;flex:0 0 auto;';
@@ -17035,7 +17214,7 @@
17035
17214
 
17036
17215
  const actions = document.createElement('div');
17037
17216
  actions.style.cssText = 'display:flex;align-items:center;gap:7px;flex-wrap:wrap;flex:0 0 auto;';
17038
- const refreshButton = createRemoteFleetButton('Refresh', 'Refresh this pinned device', 'refresh-device');
17217
+ const refreshButton = createRemoteFleetButton('Refresh', 'Refresh this remote control device', 'refresh-device');
17039
17218
  refreshButton.style.height = '30px';
17040
17219
  actions.appendChild(refreshButton);
17041
17220
  if (connected && thumbnailEnabled) {
@@ -18380,7 +18559,32 @@
18380
18559
  const deviceId = String(card.dataset.deviceId || '').trim();
18381
18560
  if (!deviceId) return;
18382
18561
  bodyView.dataset.remoteFleetSelectedDeviceId = deviceId;
18383
- openRemoteFleetControlPopup(bodyView, nodeModel, deviceId);
18562
+ if (card.dataset.remoteFleetOpeningControlNode === 'true') {
18563
+ return;
18564
+ }
18565
+
18566
+ card.dataset.remoteFleetOpeningControlNode = 'true';
18567
+ invokeDotNetAsync('AddRemoteFleetDeviceNodeFromJs', nodeId, deviceId)
18568
+ .then(async result => {
18569
+ await syncRemoteFleetNodeStateFromResult(result);
18570
+ if (result?.success) {
18571
+ const controlNodeId = String(result.nodeId || result.NodeId || '').trim();
18572
+ if (controlNodeId) {
18573
+ window.mindMap?.focusNode?.(controlNodeId, { selectNode: true });
18574
+ window.mindMap?.selectNode?.(controlNodeId);
18575
+ invokeDotNetAsync('SelectNodeInBlazor', controlNodeId).catch(() => undefined);
18576
+ }
18577
+ setTaskFeedback('');
18578
+ } else {
18579
+ setTaskFeedback(result?.error || result?.Error || 'Remote control node failed.', 'error');
18580
+ }
18581
+ })
18582
+ .catch(error => {
18583
+ setTaskFeedback(error?.message || 'Remote control node failed.', 'error');
18584
+ })
18585
+ .finally(() => {
18586
+ delete card.dataset.remoteFleetOpeningControlNode;
18587
+ });
18384
18588
  });
18385
18589
  const selectCard = event => {
18386
18590
  event.preventDefault();
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "mainAssemblyName": "MindExecution.Web",
3
3
  "resources": {
4
- "hash": "sha256-exyWukcppwSmHQvoV122RgnUj4KZOvdS3LHrc3Z6BTY=",
4
+ "hash": "sha256-H62hFd8/JAFraA8jNVN3s3GNyvSJa5o8b6zHgtuC/es=",
5
5
  "fingerprinting": {
6
6
  "Google.Protobuf.9h59ukbel7.dll": "Google.Protobuf.dll",
7
7
  "Markdig.d1j7v41cl1.dll": "Markdig.dll",
@@ -127,12 +127,12 @@
127
127
  "MindExecution.Kernel.3zom09ir6j.dll": "MindExecution.Kernel.dll",
128
128
  "MindExecution.Plugins.Admin.bk23zzy6wc.dll": "MindExecution.Plugins.Admin.dll",
129
129
  "MindExecution.Plugins.Business.zwcvc8wufe.dll": "MindExecution.Plugins.Business.dll",
130
- "MindExecution.Plugins.Concept.5gltgdqyo3.dll": "MindExecution.Plugins.Concept.dll",
130
+ "MindExecution.Plugins.Concept.4ncya2e794.dll": "MindExecution.Plugins.Concept.dll",
131
131
  "MindExecution.Plugins.Directory.jc0g7fvyzv.dll": "MindExecution.Plugins.Directory.dll",
132
- "MindExecution.Plugins.PlanMaster.cn6ppe6qoz.dll": "MindExecution.Plugins.PlanMaster.dll",
133
- "MindExecution.Plugins.YouTube.d7nnsxrftv.dll": "MindExecution.Plugins.YouTube.dll",
134
- "MindExecution.Shared.mnpwk5h86c.dll": "MindExecution.Shared.dll",
135
- "MindExecution.Web.ygql0sg6b3.dll": "MindExecution.Web.dll",
132
+ "MindExecution.Plugins.PlanMaster.0yfii1nh3p.dll": "MindExecution.Plugins.PlanMaster.dll",
133
+ "MindExecution.Plugins.YouTube.y4vxx807fo.dll": "MindExecution.Plugins.YouTube.dll",
134
+ "MindExecution.Shared.50ptsgk9eg.dll": "MindExecution.Shared.dll",
135
+ "MindExecution.Web.hztii75ar1.dll": "MindExecution.Web.dll",
136
136
  "dotnet.js": "dotnet.js",
137
137
  "dotnet.native.qc8g39g30v.js": "dotnet.native.js",
138
138
  "dotnet.native.boem75ye5i.wasm": "dotnet.native.wasm",
@@ -280,16 +280,16 @@
280
280
  "netstandard.yvr3prsx0x.dll": "sha256-EksNn8Luo4bOWqJ6X7dIe9qG9oOqwOVzjH2xYyMNi+E=",
281
281
  "MindExecution.Core.asayb4hw1o.dll": "sha256-5gla4JTR/zbvkGpneSt6S7ItE51+MjWgjW6L+B6Yevk=",
282
282
  "MindExecution.Kernel.3zom09ir6j.dll": "sha256-78vR2SNkF9FwdQv0fEItSyz4HzgyzwGUROJAnpripPA=",
283
- "MindExecution.Plugins.Concept.5gltgdqyo3.dll": "sha256-1ZVpFoyPoidSyJz38jH40AFealONt+qQpToJKgqoWW8=",
284
- "MindExecution.Plugins.PlanMaster.cn6ppe6qoz.dll": "sha256-GIFH/f4ilVTFDJVEDLLiWkkI7yGeeQl/T5/iuivI87Q=",
285
- "MindExecution.Shared.mnpwk5h86c.dll": "sha256-Ht4Kl9nQePXwyDEwL4jBxo2K3F9R4B2OGnSuhHmZ+eU=",
286
- "MindExecution.Web.ygql0sg6b3.dll": "sha256-vhb5mjLTaEStzJ9ggN4jBqtd1Z7FAkKHp7bx6Pz33Mc="
283
+ "MindExecution.Plugins.Concept.4ncya2e794.dll": "sha256-gKad12tQh1p7kInUiSIECobEZiD4VIuxmmskGE3ambo=",
284
+ "MindExecution.Plugins.PlanMaster.0yfii1nh3p.dll": "sha256-OMcIYPKBnnJCW6QRN8nGykgCloUl1FRnFx6lma85ZGs=",
285
+ "MindExecution.Shared.50ptsgk9eg.dll": "sha256-1X+o7c7jCm4MVorCrq52beiyMe+C6qFw6EyIGfZGQdo=",
286
+ "MindExecution.Web.hztii75ar1.dll": "sha256-s/mh0EOyp5uxPxzn6b4blKD/zAAA5oKCjFDe0tCgc9g="
287
287
  },
288
288
  "lazyAssembly": {
289
289
  "MindExecution.Plugins.Admin.bk23zzy6wc.dll": "sha256-uz7Nz0OZ8BBESp2Nr9YocyzV0dUrn7szPuD4/m/RWL8=",
290
290
  "MindExecution.Plugins.Business.zwcvc8wufe.dll": "sha256-3YDsB2/uNI+feWqJk+cesLHgNt8s2/8hXxGPO6CRZZE=",
291
291
  "MindExecution.Plugins.Directory.jc0g7fvyzv.dll": "sha256-PSe7135JPpTBmu4j+rGLF0Dc/6w/wDJNPi8zlfTmSMk=",
292
- "MindExecution.Plugins.YouTube.d7nnsxrftv.dll": "sha256-OQVjt1f240FGxRV8b2FpxDv4i3U4a7GGA+dBBiZBih8="
292
+ "MindExecution.Plugins.YouTube.y4vxx807fo.dll": "sha256-HkMC8BltV070e0Uaskpmyu6+TIs04y0G2t4E62RJdcc="
293
293
  }
294
294
  },
295
295
  "cacheBootResources": true,
@@ -1,5 +1,5 @@
1
1
  self.assetsManifest = {
2
- "version": "uSz5Xt0z",
2
+ "version": "QafYmh64",
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-4BdT/RnBOnh3Mm/hTZilg3zo9az6W9oYf8h3br2aDko=",
89
+ "hash": "sha256-7XM3q7xvvWVxDjSBniKorjQgNjyhOcAz2WNqg8OHOa0=",
90
90
  "url": "_content/MindExecution.Shared/js/mind-map-css3d-manager.js"
91
91
  },
92
92
  {
@@ -426,28 +426,28 @@
426
426
  "url": "_framework/MindExecution.Plugins.Business.zwcvc8wufe.dll"
427
427
  },
428
428
  {
429
- "hash": "sha256-1ZVpFoyPoidSyJz38jH40AFealONt+qQpToJKgqoWW8=",
430
- "url": "_framework/MindExecution.Plugins.Concept.5gltgdqyo3.dll"
429
+ "hash": "sha256-gKad12tQh1p7kInUiSIECobEZiD4VIuxmmskGE3ambo=",
430
+ "url": "_framework/MindExecution.Plugins.Concept.4ncya2e794.dll"
431
431
  },
432
432
  {
433
433
  "hash": "sha256-PSe7135JPpTBmu4j+rGLF0Dc/6w/wDJNPi8zlfTmSMk=",
434
434
  "url": "_framework/MindExecution.Plugins.Directory.jc0g7fvyzv.dll"
435
435
  },
436
436
  {
437
- "hash": "sha256-GIFH/f4ilVTFDJVEDLLiWkkI7yGeeQl/T5/iuivI87Q=",
438
- "url": "_framework/MindExecution.Plugins.PlanMaster.cn6ppe6qoz.dll"
437
+ "hash": "sha256-OMcIYPKBnnJCW6QRN8nGykgCloUl1FRnFx6lma85ZGs=",
438
+ "url": "_framework/MindExecution.Plugins.PlanMaster.0yfii1nh3p.dll"
439
439
  },
440
440
  {
441
- "hash": "sha256-OQVjt1f240FGxRV8b2FpxDv4i3U4a7GGA+dBBiZBih8=",
442
- "url": "_framework/MindExecution.Plugins.YouTube.d7nnsxrftv.dll"
441
+ "hash": "sha256-HkMC8BltV070e0Uaskpmyu6+TIs04y0G2t4E62RJdcc=",
442
+ "url": "_framework/MindExecution.Plugins.YouTube.y4vxx807fo.dll"
443
443
  },
444
444
  {
445
- "hash": "sha256-Ht4Kl9nQePXwyDEwL4jBxo2K3F9R4B2OGnSuhHmZ+eU=",
446
- "url": "_framework/MindExecution.Shared.mnpwk5h86c.dll"
445
+ "hash": "sha256-1X+o7c7jCm4MVorCrq52beiyMe+C6qFw6EyIGfZGQdo=",
446
+ "url": "_framework/MindExecution.Shared.50ptsgk9eg.dll"
447
447
  },
448
448
  {
449
- "hash": "sha256-vhb5mjLTaEStzJ9ggN4jBqtd1Z7FAkKHp7bx6Pz33Mc=",
450
- "url": "_framework/MindExecution.Web.ygql0sg6b3.dll"
449
+ "hash": "sha256-s/mh0EOyp5uxPxzn6b4blKD/zAAA5oKCjFDe0tCgc9g=",
450
+ "url": "_framework/MindExecution.Web.hztii75ar1.dll"
451
451
  },
452
452
  {
453
453
  "hash": "sha256-IsZJ91/OW+fHzNqIgEc7Y072ns8z9dGritiSyvR9Wgc=",
@@ -770,7 +770,7 @@
770
770
  "url": "_framework/Websocket.Client.vapounvmnl.dll"
771
771
  },
772
772
  {
773
- "hash": "sha256-FgsKNE/RFoMI9OioRZ4cgImxpXXkbc2D4LpHp7W2L7A=",
773
+ "hash": "sha256-pG5Qn7IywPsHJ02D/12EcGeCG65zRYsIE4kfzFBZv3I=",
774
774
  "url": "_framework/blazor.boot.json"
775
775
  },
776
776
  {
@@ -1,4 +1,4 @@
1
- /* Manifest version: uSz5Xt0z */
1
+ /* Manifest version: QafYmh64 */
2
2
  // Hosted deployments should prefer the network over stale offline caches.
3
3
  // This service worker immediately clears old Blazor offline caches and unregisters itself.
4
4