@mindexec/cli 0.2.152 → 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.
@@ -13494,6 +13494,8 @@
13494
13494
  const REMOTE_FLEET_LIVE_FRAME_REFRESH_MS = Math.round(1000 / REMOTE_FLEET_MONITOR_LIVE_FPS);
13495
13495
  const REMOTE_FLEET_CONTROL_LIVE_FPS = 20;
13496
13496
  const REMOTE_FLEET_CONTROL_FRAME_REFRESH_MS = Math.round(1000 / REMOTE_FLEET_CONTROL_LIVE_FPS);
13497
+ const REMOTE_FLEET_CONTROL_INPUT_MOVE_MS = 16;
13498
+ const REMOTE_FLEET_CONTROL_INPUT_QUEUE_LIMIT = 64;
13497
13499
  const REMOTE_FLEET_THUMBNAIL_FRAME_REFRESH_MS = 2000;
13498
13500
  const REMOTE_FLEET_FRAME_CANVAS_MAX_DPR = 2;
13499
13501
  const REMOTE_FLEET_FRAME_BLOB_CACHE_MS = 10000;
@@ -13728,6 +13730,7 @@
13728
13730
 
13729
13731
  function clearRemoteFleetTimers(bodyView, options = {}) {
13730
13732
  if (!bodyView) return;
13733
+ closeRemoteFleetControlNodeSession(bodyView, 'clear');
13731
13734
  if (options.keepBinaryFrameSocket !== true) {
13732
13735
  releaseRemoteFleetBinaryFrameSocket(bodyView);
13733
13736
  }
@@ -13829,6 +13832,22 @@
13829
13832
  return url.toString();
13830
13833
  }
13831
13834
 
13835
+ function buildRemoteFleetInputWsUrl(status) {
13836
+ const baseUrl = String(status?.__remoteFleetBridgeBaseUrl || window.location?.origin || '').trim();
13837
+ const path = String(status?.remoteInputWsPath || '/api/remote/input/ws').trim() || '/api/remote/input/ws';
13838
+ if (!baseUrl) {
13839
+ return '';
13840
+ }
13841
+
13842
+ const url = new URL(path, baseUrl);
13843
+ url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:';
13844
+ const bridgeToken = String(status?.bridgeToken || '').trim();
13845
+ if (bridgeToken) {
13846
+ url.searchParams.set('token', bridgeToken);
13847
+ }
13848
+ return url.toString();
13849
+ }
13850
+
13832
13851
  async function parseRemoteFleetBinaryFrameMessage(data) {
13833
13852
  let buffer = null;
13834
13853
  if (data instanceof ArrayBuffer) {
@@ -15729,6 +15748,8 @@
15729
15748
  activeRemoteFleetControlPopup = null;
15730
15749
  session.active = false;
15731
15750
  detachRemoteFleetControlBinaryFrameSession(session);
15751
+ closeRemoteFleetControlInputSocket(session, 'close');
15752
+ session.inputCleanup?.();
15732
15753
  if (session.timer) {
15733
15754
  clearTimeout(session.timer);
15734
15755
  session.timer = null;
@@ -15847,6 +15868,8 @@
15847
15868
 
15848
15869
  drawRemoteFleetFrameToCanvas(session.canvas, bitmap, 'contain');
15849
15870
  session.canvas.style.display = 'block';
15871
+ session.placeholder?.remove?.();
15872
+ session.placeholder = null;
15850
15873
  if (typeof bitmap.close === 'function') {
15851
15874
  bitmap.close();
15852
15875
  }
@@ -15897,6 +15920,8 @@
15897
15920
  session.canvas.style.display = 'block';
15898
15921
  session.lastControlFrameAt = Date.now();
15899
15922
  session.status.textContent = `Control ${REMOTE_FLEET_CONTROL_LIVE_FPS}fps`;
15923
+ session.placeholder?.remove?.();
15924
+ session.placeholder = null;
15900
15925
  if (typeof bitmap.close === 'function') {
15901
15926
  bitmap.close();
15902
15927
  }
@@ -15910,34 +15935,268 @@
15910
15935
  }
15911
15936
  }
15912
15937
 
15913
- function sendRemoteFleetControlInput(session, payload, throttleMove = false) {
15914
- if (!session?.active || !payload?.type) {
15938
+ function closeRemoteFleetControlInputSocket(session, reason = 'close') {
15939
+ if (!session) {
15915
15940
  return;
15916
15941
  }
15917
15942
 
15918
- const send = next => {
15919
- if (!session.active) {
15920
- return;
15943
+ if (session.inputWsOpenTimer) {
15944
+ clearTimeout(session.inputWsOpenTimer);
15945
+ session.inputWsOpenTimer = null;
15946
+ }
15947
+
15948
+ const ws = session.inputWs;
15949
+ session.inputWs = null;
15950
+ session.inputWsConnecting = false;
15951
+ if (ws && ws.readyState !== 3) {
15952
+ try {
15953
+ ws.close();
15954
+ } catch {
15955
+ // Best-effort input socket cleanup.
15921
15956
  }
15922
- invokeDotNetAsync('SendRemoteFleetInputFromJs', session.nodeId, session.deviceId, {
15923
- ...next,
15924
- controlLeaseId: session.controlLeaseId,
15925
- issuedAt: new Date().toISOString()
15926
- }).catch(error => {
15957
+ }
15958
+
15959
+ if (reason === 'close') {
15960
+ session.inputQueue = [];
15961
+ }
15962
+ }
15963
+
15964
+ function createRemoteFleetControlInput(session, payload) {
15965
+ return {
15966
+ ...payload,
15967
+ controlLeaseId: session.controlLeaseId,
15968
+ issuedAt: new Date().toISOString()
15969
+ };
15970
+ }
15971
+
15972
+ function sendRemoteFleetControlInputFallback(session, input) {
15973
+ if (!session?.active || !input?.type) {
15974
+ return;
15975
+ }
15976
+
15977
+ invokeDotNetAsync('SendRemoteFleetInputFromJs', session.nodeId, session.deviceId, {
15978
+ ...input,
15979
+ controlLeaseId: input.controlLeaseId || session.controlLeaseId,
15980
+ issuedAt: input.issuedAt || new Date().toISOString()
15981
+ }).catch(error => {
15982
+ if (session.active) {
15927
15983
  session.status.textContent = error?.message || 'Input failed';
15984
+ }
15985
+ });
15986
+ }
15987
+
15988
+ function drainRemoteFleetControlInputQueueToFallback(session) {
15989
+ if (!session?.inputQueue?.length) {
15990
+ return;
15991
+ }
15992
+
15993
+ const queued = session.inputQueue.splice(0);
15994
+ queued.forEach(envelope => sendRemoteFleetControlInputFallback(session, envelope.input));
15995
+ }
15996
+
15997
+ function enqueueRemoteFleetControlInput(session, envelope) {
15998
+ if (!session) {
15999
+ return;
16000
+ }
16001
+
16002
+ if (!Array.isArray(session.inputQueue)) {
16003
+ session.inputQueue = [];
16004
+ }
16005
+
16006
+ const inputType = String(envelope?.input?.type || '').toLowerCase();
16007
+ if (inputType === 'pointermove') {
16008
+ for (let i = session.inputQueue.length - 1; i >= 0; i -= 1) {
16009
+ if (String(session.inputQueue[i]?.input?.type || '').toLowerCase() === 'pointermove') {
16010
+ session.inputQueue.splice(i, 1);
16011
+ }
16012
+ }
16013
+ }
16014
+
16015
+ session.inputQueue.push(envelope);
16016
+ while (session.inputQueue.length > REMOTE_FLEET_CONTROL_INPUT_QUEUE_LIMIT) {
16017
+ const moveIndex = session.inputQueue.findIndex(item =>
16018
+ String(item?.input?.type || '').toLowerCase() === 'pointermove');
16019
+ session.inputQueue.splice(moveIndex >= 0 ? moveIndex : 0, 1);
16020
+ }
16021
+ }
16022
+
16023
+ function flushRemoteFleetControlInputQueue(session) {
16024
+ const ws = session?.inputWs;
16025
+ if (!session?.active || !ws || ws.readyState !== 1 || !session.inputQueue?.length) {
16026
+ return;
16027
+ }
16028
+
16029
+ const queued = session.inputQueue.splice(0);
16030
+ for (const envelope of queued) {
16031
+ if (!sendRemoteFleetControlInputEnvelope(session, envelope)) {
16032
+ enqueueRemoteFleetControlInput(session, envelope);
16033
+ break;
16034
+ }
16035
+ }
16036
+ }
16037
+
16038
+ function sendRemoteFleetControlInputEnvelope(session, envelope) {
16039
+ const ws = session?.inputWs;
16040
+ if (!session?.active || !ws || ws.readyState !== 1) {
16041
+ return false;
16042
+ }
16043
+
16044
+ const inputType = String(envelope?.input?.type || '').toLowerCase();
16045
+ if (inputType === 'pointermove' && Number(ws.bufferedAmount || 0) > 256 * 1024) {
16046
+ window.RuntimeTrace?.emit?.('remote.control.input.drop', {
16047
+ reason: 'ws-buffered',
16048
+ deviceId: session.deviceId,
16049
+ bufferedAmount: Number(ws.bufferedAmount || 0)
15928
16050
  });
16051
+ return true;
16052
+ }
16053
+
16054
+ try {
16055
+ ws.send(JSON.stringify(envelope));
16056
+ return true;
16057
+ } catch {
16058
+ return false;
16059
+ }
16060
+ }
16061
+
16062
+ function handleRemoteFleetControlInputSocketFailure(session) {
16063
+ if (!session?.active) {
16064
+ return;
16065
+ }
16066
+
16067
+ closeRemoteFleetControlInputSocket(session, 'failed');
16068
+ session.inputWsUnavailableUntil = Date.now() + 1000;
16069
+ drainRemoteFleetControlInputQueueToFallback(session);
16070
+ }
16071
+
16072
+ function ensureRemoteFleetControlInputSocket(session) {
16073
+ if (!session?.active || typeof WebSocket !== 'function') {
16074
+ return false;
16075
+ }
16076
+
16077
+ const ws = session.inputWs;
16078
+ if (ws && (ws.readyState === 0 || ws.readyState === 1)) {
16079
+ return true;
16080
+ }
16081
+
16082
+ if (session.inputWsConnecting === true) {
16083
+ return true;
16084
+ }
16085
+
16086
+ if ((session.inputWsUnavailableUntil || 0) > Date.now()) {
16087
+ return false;
16088
+ }
16089
+
16090
+ const connectToken = `input-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
16091
+ session.inputWsConnecting = true;
16092
+ session.inputWsConnectToken = connectToken;
16093
+ getRemoteFleetBridgeStatusForFrames()
16094
+ .then(status => {
16095
+ if (!session.active || session.inputWsConnectToken !== connectToken) {
16096
+ return;
16097
+ }
16098
+
16099
+ const wsUrl = buildRemoteFleetInputWsUrl(status);
16100
+ if (!wsUrl) {
16101
+ throw new Error('input-ws-url-unavailable');
16102
+ }
16103
+
16104
+ const inputWs = new WebSocket(wsUrl);
16105
+ session.inputWs = inputWs;
16106
+ inputWs.onopen = () => {
16107
+ if (!session.active || session.inputWs !== inputWs) {
16108
+ return;
16109
+ }
16110
+
16111
+ session.inputWsConnecting = false;
16112
+ session.inputWsUnavailableUntil = 0;
16113
+ if (session.inputWsOpenTimer) {
16114
+ clearTimeout(session.inputWsOpenTimer);
16115
+ session.inputWsOpenTimer = null;
16116
+ }
16117
+ flushRemoteFleetControlInputQueue(session);
16118
+ };
16119
+ inputWs.onmessage = event => {
16120
+ try {
16121
+ const message = JSON.parse(String(event?.data || ''));
16122
+ if (message?.ok === false && session.active) {
16123
+ session.status.textContent = message.error || 'Input failed';
16124
+ }
16125
+ } catch {
16126
+ // Ignore input ack parse errors.
16127
+ }
16128
+ };
16129
+ inputWs.onerror = () => {
16130
+ if (session.inputWs === inputWs) {
16131
+ handleRemoteFleetControlInputSocketFailure(session);
16132
+ }
16133
+ };
16134
+ inputWs.onclose = () => {
16135
+ if (session.inputWs === inputWs) {
16136
+ handleRemoteFleetControlInputSocketFailure(session);
16137
+ }
16138
+ };
16139
+ session.inputWsOpenTimer = setTimeout(() => {
16140
+ if (session.active && session.inputWs === inputWs && inputWs.readyState === 0) {
16141
+ handleRemoteFleetControlInputSocketFailure(session);
16142
+ }
16143
+ }, 900);
16144
+ })
16145
+ .catch(() => {
16146
+ if (!session.active || session.inputWsConnectToken !== connectToken) {
16147
+ return;
16148
+ }
16149
+
16150
+ session.inputWsConnecting = false;
16151
+ session.inputWsUnavailableUntil = Date.now() + 1000;
16152
+ drainRemoteFleetControlInputQueueToFallback(session);
16153
+ });
16154
+
16155
+ return true;
16156
+ }
16157
+
16158
+ function sendRemoteFleetControlInputNow(session, payload) {
16159
+ if (!session?.active || !payload?.type) {
16160
+ return;
16161
+ }
16162
+
16163
+ const input = createRemoteFleetControlInput(session, payload);
16164
+ const envelope = {
16165
+ type: 'input',
16166
+ nodeId: session.nodeId,
16167
+ deviceId: session.deviceId,
16168
+ requestId: `input-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`,
16169
+ input
15929
16170
  };
15930
16171
 
16172
+ const wsReady = ensureRemoteFleetControlInputSocket(session);
16173
+ if (wsReady && sendRemoteFleetControlInputEnvelope(session, envelope)) {
16174
+ return;
16175
+ }
16176
+
16177
+ if (wsReady) {
16178
+ enqueueRemoteFleetControlInput(session, envelope);
16179
+ return;
16180
+ }
16181
+
16182
+ sendRemoteFleetControlInputFallback(session, input);
16183
+ }
16184
+
16185
+ function sendRemoteFleetControlInput(session, payload, throttleMove = false) {
16186
+ if (!session?.active || !payload?.type) {
16187
+ return;
16188
+ }
16189
+
15931
16190
  if (!throttleMove) {
15932
- send(payload);
16191
+ sendRemoteFleetControlInputNow(session, payload);
15933
16192
  return;
15934
16193
  }
15935
16194
 
15936
16195
  const now = performance.now();
15937
16196
  const elapsed = now - (session.lastMoveSentAt || 0);
15938
- if (elapsed >= 32) {
16197
+ if (elapsed >= REMOTE_FLEET_CONTROL_INPUT_MOVE_MS) {
15939
16198
  session.lastMoveSentAt = now;
15940
- send(payload);
16199
+ sendRemoteFleetControlInputNow(session, payload);
15941
16200
  return;
15942
16201
  }
15943
16202
 
@@ -15952,18 +16211,82 @@
15952
16211
  session.pendingMove = null;
15953
16212
  if (next) {
15954
16213
  session.lastMoveSentAt = performance.now();
15955
- send(next);
16214
+ sendRemoteFleetControlInputNow(session, next);
15956
16215
  }
15957
- }, Math.max(1, 32 - elapsed));
16216
+ }, Math.max(1, REMOTE_FLEET_CONTROL_INPUT_MOVE_MS - elapsed));
15958
16217
  }
15959
16218
 
15960
- 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 = {}) {
15961
16243
  const { overlay, shell, stage, canvas } = session;
15962
16244
  let pointerActive = false;
15963
16245
 
15964
- ['mousedown', 'mouseup', 'click', 'dblclick', 'keydown', 'keyup', 'wheel', 'contextmenu'].forEach(eventName => {
15965
- overlay.addEventListener(eventName, event => event.stopPropagation(), true);
15966
- });
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
+ }
16255
+
16256
+ const shouldIgnoreKeyboardEvent = event =>
16257
+ isRemoteFleetControlBypassTarget(event?.target, session);
16258
+
16259
+ const handleKeyboardEvent = (event, type) => {
16260
+ if (!session.active || shouldIgnoreKeyboardEvent(event) || event.__remoteFleetControlHandled === true) {
16261
+ return;
16262
+ }
16263
+
16264
+ event.__remoteFleetControlHandled = true;
16265
+ event.preventDefault();
16266
+ event.stopPropagation();
16267
+ shell.focus({ preventScroll: true });
16268
+ sendRemoteFleetControlInput(session, {
16269
+ type,
16270
+ key: event.key,
16271
+ code: event.code,
16272
+ repeat: event.repeat === true
16273
+ });
16274
+ };
16275
+
16276
+ const handleWindowKeyDown = event => handleKeyboardEvent(event, 'keyDown');
16277
+ const handleWindowKeyUp = event => handleKeyboardEvent(event, 'keyUp');
16278
+ const captureWindowKeyboard = options.windowKeyboardCapture !== false;
16279
+ if (captureWindowKeyboard && typeof window.addEventListener === 'function') {
16280
+ window.addEventListener('keydown', handleWindowKeyDown, true);
16281
+ window.addEventListener('keyup', handleWindowKeyUp, true);
16282
+ }
16283
+ session.inputCleanup = () => {
16284
+ if (captureWindowKeyboard && typeof window.removeEventListener === 'function') {
16285
+ window.removeEventListener('keydown', handleWindowKeyDown, true);
16286
+ window.removeEventListener('keyup', handleWindowKeyUp, true);
16287
+ }
16288
+ session.inputCleanup = null;
16289
+ };
15967
16290
 
15968
16291
  stage.addEventListener('pointerdown', event => {
15969
16292
  const point = getRemoteFleetControlPoint(event, canvas, false);
@@ -16041,35 +16364,8 @@
16041
16364
  event.stopPropagation();
16042
16365
  });
16043
16366
 
16044
- shell.addEventListener('keydown', event => {
16045
- if (event.target?.closest?.('[data-remote-fleet-control-close="true"]')) {
16046
- return;
16047
- }
16048
-
16049
- event.preventDefault();
16050
- event.stopPropagation();
16051
- sendRemoteFleetControlInput(session, {
16052
- type: 'keyDown',
16053
- key: event.key,
16054
- code: event.code,
16055
- repeat: event.repeat === true
16056
- });
16057
- });
16058
-
16059
- shell.addEventListener('keyup', event => {
16060
- if (event.target?.closest?.('[data-remote-fleet-control-close="true"]')) {
16061
- return;
16062
- }
16063
-
16064
- event.preventDefault();
16065
- event.stopPropagation();
16066
- sendRemoteFleetControlInput(session, {
16067
- type: 'keyUp',
16068
- key: event.key,
16069
- code: event.code,
16070
- repeat: event.repeat === true
16071
- });
16072
- });
16367
+ shell.addEventListener('keydown', event => handleKeyboardEvent(event, 'keyDown'));
16368
+ shell.addEventListener('keyup', event => handleKeyboardEvent(event, 'keyUp'));
16073
16369
  }
16074
16370
 
16075
16371
  function openRemoteFleetControlPopup(bodyView, nodeModel, deviceId) {
@@ -16233,10 +16529,18 @@
16233
16529
  lastMoveSentAt: 0,
16234
16530
  lastControlFrameAt: 0,
16235
16531
  lastControlFrameSeq: 0,
16236
- binaryFramePreferred: false
16532
+ binaryFramePreferred: false,
16533
+ inputWs: null,
16534
+ inputWsConnecting: false,
16535
+ inputWsConnectToken: '',
16536
+ inputWsOpenTimer: null,
16537
+ inputWsUnavailableUntil: 0,
16538
+ inputQueue: [],
16539
+ inputCleanup: null
16237
16540
  };
16238
16541
  activeRemoteFleetControlPopup = session;
16239
16542
  bindRemoteFleetControlInput(session);
16543
+ ensureRemoteFleetControlInputSocket(session);
16240
16544
  attachRemoteFleetControlBinaryFrameSession(session, bodyView);
16241
16545
 
16242
16546
  const refresh = async () => {
@@ -16315,6 +16619,93 @@
16315
16619
  });
16316
16620
  }
16317
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
+
16318
16709
  function isRemoteFleetLiveActive(device) {
16319
16710
  return getRemoteFleetDeviceField(device, 'liveStreamActive', 'LiveStreamActive', false) === true;
16320
16711
  }
@@ -16559,7 +16950,7 @@
16559
16950
 
16560
16951
  if (!device || !deviceId) {
16561
16952
  const empty = document.createElement('div');
16562
- empty.textContent = lastError || 'Pinned remote device snapshot is unavailable.';
16953
+ empty.textContent = lastError || 'Remote control device snapshot is unavailable.';
16563
16954
  empty.style.cssText = `
16564
16955
  flex: 1 1 auto;
16565
16956
  display: flex;
@@ -16575,7 +16966,7 @@
16575
16966
  text-align: center;
16576
16967
  background: rgba(255, 255, 255, 0.74);
16577
16968
  `;
16578
- const refreshButton = createRemoteFleetButton('Refresh', 'Refresh this pinned device', 'refresh-device');
16969
+ const refreshButton = createRemoteFleetButton('Refresh', 'Refresh this remote control device', 'refresh-device');
16579
16970
  refreshButton.style.height = '32px';
16580
16971
  refreshButton.addEventListener('click', async event => {
16581
16972
  event.preventDefault();
@@ -16654,52 +17045,109 @@
16654
17045
  header.appendChild(statusBadge);
16655
17046
  bodyView.appendChild(header);
16656
17047
 
16657
- const preview = document.createElement('div');
16658
- preview.dataset.remoteFleetDevicePreview = 'pinned';
16659
- preview.dataset.deviceId = deviceId;
16660
- preview.dataset.remoteFleetFrameKind = hasLiveFrame ? 'live' : (hasThumbnail ? 'thumbnail' : '');
16661
- preview.dataset.remoteFleetFrameSeq = String(hasLiveFrame
16662
- ? getRemoteFleetDeviceField(device, 'liveFrameSeq', 'LiveFrameSeq', 0)
16663
- : getRemoteFleetDeviceField(device, 'thumbnailFrameSeq', 'ThumbnailFrameSeq', 0));
16664
- 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 = `
16665
17054
  position: relative;
16666
- flex: 0 0 auto;
17055
+ flex: 1 1 auto;
17056
+ min-height: 260px;
16667
17057
  width: 100%;
16668
- aspect-ratio: 16 / 9;
16669
17058
  overflow: hidden;
16670
- border-radius: 8px;
16671
- background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%);
16672
- 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;
16673
17065
  `;
16674
- if (hasLiveFrame || hasThumbnail) {
16675
- const image = document.createElement('img');
16676
- image.dataset.remoteFleetFrameImage = 'true';
16677
- image.dataset.remoteFleetFrameKind = hasLiveFrame ? 'live' : 'thumbnail';
16678
- image.dataset.remoteFleetFrameSeq = preview.dataset.remoteFleetFrameSeq;
16679
- image.src = previewSource;
16680
- image.alt = `${name} screen`;
16681
- image.loading = 'lazy';
16682
- image.decoding = 'async';
16683
- image.style.cssText = 'width:100%;height:100%;object-fit:cover;display:block;';
16684
- preview.appendChild(image);
16685
- } else {
16686
- const placeholder = document.createElement('div');
16687
- placeholder.dataset.remoteFleetScreenPlaceholder = 'true';
16688
- placeholder.textContent = thumbnailEnabled ? 'No frame yet' : 'Status only';
16689
- placeholder.style.cssText = `
16690
- position: absolute;
16691
- inset: 0;
16692
- display: flex;
16693
- align-items: center;
16694
- justify-content: center;
16695
- color: rgba(226, 232, 240, 0.78);
16696
- font-size: 12px;
16697
- font-weight: 900;
16698
- letter-spacing: 0;
16699
- `;
16700
- 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
+ }
16701
17150
  }
16702
- bodyView.appendChild(preview);
16703
17151
 
16704
17152
  const stats = document.createElement('div');
16705
17153
  stats.style.cssText = 'display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:7px;flex:0 0 auto;';
@@ -16766,7 +17214,7 @@
16766
17214
 
16767
17215
  const actions = document.createElement('div');
16768
17216
  actions.style.cssText = 'display:flex;align-items:center;gap:7px;flex-wrap:wrap;flex:0 0 auto;';
16769
- const refreshButton = createRemoteFleetButton('Refresh', 'Refresh this pinned device', 'refresh-device');
17217
+ const refreshButton = createRemoteFleetButton('Refresh', 'Refresh this remote control device', 'refresh-device');
16770
17218
  refreshButton.style.height = '30px';
16771
17219
  actions.appendChild(refreshButton);
16772
17220
  if (connected && thumbnailEnabled) {
@@ -18111,7 +18559,32 @@
18111
18559
  const deviceId = String(card.dataset.deviceId || '').trim();
18112
18560
  if (!deviceId) return;
18113
18561
  bodyView.dataset.remoteFleetSelectedDeviceId = deviceId;
18114
- 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
+ });
18115
18588
  });
18116
18589
  const selectCard = event => {
18117
18590
  event.preventDefault();