@mindexec/cli 0.2.6 → 0.2.8

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 (22) hide show
  1. package/README.md +12 -0
  2. package/package.json +1 -1
  3. package/remote-hub.js +167 -0
  4. package/scripts/remote-hub-smoke.mjs +54 -0
  5. package/server.js +28 -0
  6. package/wwwroot/_content/MindExecution.Shared/js/mind-map-css3d-manager.js +366 -15
  7. package/wwwroot/_framework/MindExecution.Core.pnw79cgqjx.dll +0 -0
  8. package/wwwroot/_framework/{MindExecution.Kernel.79mgmkpsy1.dll → MindExecution.Kernel.dt3w864bqn.dll} +0 -0
  9. package/wwwroot/_framework/{MindExecution.Plugins.Admin.8ey1m70q79.dll → MindExecution.Plugins.Admin.z93cu32xru.dll} +0 -0
  10. package/wwwroot/_framework/{MindExecution.Plugins.Business.8rvwgqzxaz.dll → MindExecution.Plugins.Business.b6da8sg85t.dll} +0 -0
  11. package/wwwroot/_framework/{MindExecution.Plugins.Concept.ynnk3bqf03.dll → MindExecution.Plugins.Concept.mjooiqft9j.dll} +0 -0
  12. package/wwwroot/_framework/{MindExecution.Plugins.Directory.vi24rmnuyq.dll → MindExecution.Plugins.Directory.rjod6rdmly.dll} +0 -0
  13. package/wwwroot/_framework/{MindExecution.Plugins.PlanMaster.9r08m5atjr.dll → MindExecution.Plugins.PlanMaster.1dcrzhsegj.dll} +0 -0
  14. package/wwwroot/_framework/{MindExecution.Plugins.YouTube.fjkbniwa4o.dll → MindExecution.Plugins.YouTube.k75qxhbpp8.dll} +0 -0
  15. package/wwwroot/_framework/{MindExecution.Shared.e0qnm5vtax.dll → MindExecution.Shared.y3eqxd3mvo.dll} +0 -0
  16. package/wwwroot/_framework/MindExecution.Web.wou9x6mn2f.dll +0 -0
  17. package/wwwroot/_framework/blazor.boot.json +21 -21
  18. package/wwwroot/index.html +1 -1
  19. package/wwwroot/service-worker-assets.js +24 -24
  20. package/wwwroot/service-worker.js +1 -1
  21. package/wwwroot/_framework/MindExecution.Core.kf4752v5yl.dll +0 -0
  22. package/wwwroot/_framework/MindExecution.Web.wk9yv02eva.dll +0 -0
package/README.md CHANGED
@@ -83,6 +83,18 @@ curl -X POST -H "X-Bridge-Token: <bridge-token>" http://127.0.0.1:5147/api/remot
83
83
  curl -H "X-Bridge-Token: <bridge-token>" http://127.0.0.1:5147/api/remote/devices/<device-id>/thumbnail
84
84
  ```
85
85
 
86
+ Start, read, and stop the focused view-only RemoteFast live stream:
87
+
88
+ ```bash
89
+ curl -X POST -H "X-Bridge-Token: <bridge-token>" -H "Content-Type: application/json" \
90
+ -d "{\"fps\":12,\"maxWidth\":960,\"maxHeight\":540,\"quality\":60}" \
91
+ http://127.0.0.1:5147/api/remote/devices/<device-id>/live/start
92
+
93
+ curl -H "X-Bridge-Token: <bridge-token>" http://127.0.0.1:5147/api/remote/devices/<device-id>/live/frame
94
+
95
+ curl -X POST -H "X-Bridge-Token: <bridge-token>" http://127.0.0.1:5147/api/remote/devices/<device-id>/live/stop
96
+ ```
97
+
86
98
  Queue a safe task-only instruction for one device or all connected devices:
87
99
 
88
100
  ```bash
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mindexec/cli",
3
- "version": "0.2.6",
3
+ "version": "0.2.8",
4
4
  "description": "MindExec local runtime and bridge CLI",
5
5
  "main": "server.js",
6
6
  "type": "module",
package/remote-hub.js CHANGED
@@ -7,6 +7,7 @@ const DEFAULT_REMOTE_HUB_HOST = '127.0.0.1';
7
7
  const DEFAULT_HEARTBEAT_MS = 5000;
8
8
  const MAX_LINE_CHARS = 4 * 1024 * 1024;
9
9
  const MAX_THUMBNAIL_BASE64_CHARS = 3 * 1024 * 1024;
10
+ const MAX_STREAM_BASE64_CHARS = 3 * 1024 * 1024;
10
11
  const MAX_AGENT_TASK_CHARS = 4000;
11
12
  const MAX_AGENT_TASK_RESULT_CHARS = 3000;
12
13
  const RECENT_TASK_LIMIT = 12;
@@ -118,6 +119,8 @@ function serializeDevice(device) {
118
119
  remoteAddress: device.remoteAddress,
119
120
  remotePort: device.remotePort,
120
121
  latestThumbnail: device.latestThumbnail ? { ...device.latestThumbnail } : null,
122
+ latestLiveFrame: device.latestLiveFrame ? { ...device.latestLiveFrame } : null,
123
+ activeLiveStream: device.activeLiveStream ? { ...device.activeLiveStream } : null,
121
124
  latestTask: device.latestTask ? { ...device.latestTask } : null,
122
125
  recentTasks: Array.isArray(device.recentTasks)
123
126
  ? device.recentTasks.map(task => ({ ...task }))
@@ -249,6 +252,8 @@ export function createRemoteHub(options = {}) {
249
252
  remotePort: socket.remotePort || 0,
250
253
  status: {},
251
254
  latestThumbnail: null,
255
+ latestLiveFrame: null,
256
+ activeLiveStream: null,
252
257
  latestTask: null,
253
258
  recentTasks: [],
254
259
  pendingTaskCommands: new Map(),
@@ -259,6 +264,10 @@ export function createRemoteHub(options = {}) {
259
264
  commandResultsReceived: 0,
260
265
  thumbnailFramesReceived: 0,
261
266
  thumbnailFramesDropped: 0,
267
+ liveFramesReceived: 0,
268
+ liveFramesDropped: 0,
269
+ liveStreamsStarted: 0,
270
+ liveStreamsStopped: 0,
262
271
  tasksQueued: 0,
263
272
  taskResultsReceived: 0,
264
273
  taskResultsFailed: 0
@@ -297,6 +306,11 @@ export function createRemoteHub(options = {}) {
297
306
  device.socket = null;
298
307
  device.disconnectedAt = new Date().toISOString();
299
308
  device.lastDisconnectReason = reason;
309
+ if (device.activeLiveStream) {
310
+ device.activeLiveStream.active = false;
311
+ device.activeLiveStream.stoppedAt = device.disconnectedAt;
312
+ device.activeLiveStream.stopReason = reason;
313
+ }
300
314
  logWarn('remote', `device disconnected ${device.deviceName} (${deviceId}): ${reason}`);
301
315
  emitRemoteEvent('RemoteDeviceDisconnected', device, { reason });
302
316
  }
@@ -475,6 +489,62 @@ export function createRemoteHub(options = {}) {
475
489
  });
476
490
  break;
477
491
  }
492
+ case 'stream.frame': {
493
+ const frameData = safeString(message.data, MAX_STREAM_BASE64_CHARS + 1);
494
+ const frameSeq = Number(message.frameSeq);
495
+ const streamId = safeString(message.streamId, 128) || 'live';
496
+ if (!device.activeLiveStream?.active || device.activeLiveStream.streamId !== streamId) {
497
+ device.counters.liveFramesDropped += 1;
498
+ emitRemoteEvent('RemoteFrameDropped', device, {
499
+ reason: 'stale-live-stream-frame',
500
+ streamId,
501
+ frameSeq: Number.isFinite(frameSeq) ? frameSeq : null
502
+ });
503
+ break;
504
+ }
505
+
506
+ if (!frameData || frameData.length > MAX_STREAM_BASE64_CHARS || !Number.isFinite(frameSeq)) {
507
+ device.counters.liveFramesDropped += 1;
508
+ emitRemoteEvent('RemoteFrameDropped', device, {
509
+ reason: 'invalid-live-frame',
510
+ streamId,
511
+ frameSeq: Number.isFinite(frameSeq) ? frameSeq : null
512
+ });
513
+ break;
514
+ }
515
+
516
+ const mimeType = safeString(message.mimeType || message.format || 'image/jpeg', 80) || 'image/jpeg';
517
+ const capturedAt = safeString(message.capturedAt, 80) || device.lastSeenAt;
518
+ device.latestLiveFrame = {
519
+ streamId,
520
+ frameSeq,
521
+ commandId: safeString(message.commandId, 128),
522
+ width: Number.isFinite(Number(message.width)) ? Number(message.width) : 0,
523
+ height: Number.isFinite(Number(message.height)) ? Number(message.height) : 0,
524
+ mimeType,
525
+ format: mimeType,
526
+ mode: safeString(message.mode || device.activeLiveStream.mode || 'remote-fast', 80),
527
+ fps: Number.isFinite(Number(message.fps)) ? Number(message.fps) : device.activeLiveStream.fps,
528
+ capturedAt,
529
+ receivedAt: device.lastSeenAt,
530
+ byteLength: Math.floor(frameData.length * 3 / 4),
531
+ dataUrl: frameData.startsWith('data:')
532
+ ? frameData
533
+ : `data:${mimeType};base64,${frameData}`
534
+ };
535
+ device.activeLiveStream.lastFrameAt = device.lastSeenAt;
536
+ device.activeLiveStream.lastFrameSeq = frameSeq;
537
+ device.activeLiveStream.framesReceived = (device.activeLiveStream.framesReceived || 0) + 1;
538
+ device.counters.liveFramesReceived += 1;
539
+ emitRemoteEvent('RemoteFrameReceived', device, {
540
+ streamId,
541
+ frameSeq,
542
+ width: device.latestLiveFrame.width,
543
+ height: device.latestLiveFrame.height,
544
+ mode: device.latestLiveFrame.mode
545
+ });
546
+ break;
547
+ }
478
548
  default:
479
549
  emitRemoteEvent('RemoteAgentMessageIgnored', device, {
480
550
  messageType: safeString(message.type, 80)
@@ -731,6 +801,100 @@ export function createRemoteHub(options = {}) {
731
801
  });
732
802
  }
733
803
 
804
+ function startLiveStream(deviceId, options = {}) {
805
+ const device = devices.get(String(deviceId || ''));
806
+ if (!device?.socket || device.socket.destroyed || !device.connected) {
807
+ return { ok: false, error: 'device-not-connected' };
808
+ }
809
+ if (!readCapabilityFlag(device.capabilities, 'liveStream')) {
810
+ return { ok: false, error: 'device-live-stream-unavailable' };
811
+ }
812
+
813
+ const now = new Date().toISOString();
814
+ const streamId = safeString(options.streamId, 128) || `live-${Date.now()}`;
815
+ const fps = clampNumber(options.fps, 1, 24, 12);
816
+ const commandId = safeString(options.commandId, 128) || crypto.randomUUID();
817
+ const result = sendCommand(deviceId, {
818
+ command: 'stream.start',
819
+ commandId,
820
+ payload: {
821
+ streamId,
822
+ mode: safeString(options.mode, 80) || 'remote-fast',
823
+ fps,
824
+ maxWidth: clampNumber(options.maxWidth, 320, 2560, 960),
825
+ maxHeight: clampNumber(options.maxHeight, 180, 1440, 540),
826
+ quality: clampNumber(options.quality, 20, 95, 60),
827
+ requestedAt: now
828
+ }
829
+ });
830
+
831
+ if (!result.ok) {
832
+ return result;
833
+ }
834
+
835
+ device.activeLiveStream = {
836
+ streamId,
837
+ commandId,
838
+ active: true,
839
+ mode: 'remote-fast',
840
+ fps,
841
+ startedAt: now,
842
+ stoppedAt: '',
843
+ stopReason: '',
844
+ lastFrameAt: '',
845
+ lastFrameSeq: 0,
846
+ framesReceived: 0
847
+ };
848
+ device.counters.liveStreamsStarted += 1;
849
+ emitRemoteEvent('RemoteLiveStreamStarted', device, {
850
+ streamId,
851
+ commandId,
852
+ fps
853
+ });
854
+
855
+ return { ok: true, commandId, streamId, fps };
856
+ }
857
+
858
+ function stopLiveStream(deviceId, options = {}) {
859
+ const device = devices.get(String(deviceId || ''));
860
+ if (!device?.socket || device.socket.destroyed || !device.connected) {
861
+ return { ok: false, error: 'device-not-connected' };
862
+ }
863
+
864
+ const streamId = safeString(options.streamId, 128) || device.activeLiveStream?.streamId || '';
865
+ const commandId = safeString(options.commandId, 128) || crypto.randomUUID();
866
+ const result = sendCommand(deviceId, {
867
+ command: 'stream.stop',
868
+ commandId,
869
+ payload: {
870
+ streamId,
871
+ requestedAt: new Date().toISOString()
872
+ }
873
+ });
874
+
875
+ if (!result.ok) {
876
+ return result;
877
+ }
878
+
879
+ if (device.activeLiveStream && (!streamId || device.activeLiveStream.streamId === streamId)) {
880
+ device.activeLiveStream.active = false;
881
+ device.activeLiveStream.stoppedAt = new Date().toISOString();
882
+ device.activeLiveStream.stopReason = 'manager-request';
883
+ }
884
+ device.counters.liveStreamsStopped += 1;
885
+ emitRemoteEvent('RemoteLiveStreamStopped', device, {
886
+ streamId,
887
+ commandId
888
+ });
889
+
890
+ return { ok: true, commandId, streamId };
891
+ }
892
+
893
+ function getDeviceLiveFrame(deviceId) {
894
+ const device = devices.get(String(deviceId || ''));
895
+ return device?.latestLiveFrame ? { ...device.latestLiveFrame } : null;
896
+ }
897
+
734
898
  function getDeviceThumbnail(deviceId) {
735
899
  const device = devices.get(String(deviceId || ''));
736
900
  return device?.latestThumbnail ? { ...device.latestThumbnail } : null;
@@ -745,6 +909,9 @@ export function createRemoteHub(options = {}) {
745
909
  sendCommand,
746
910
  requestAgentTask,
747
911
  requestThumbnail,
912
+ startLiveStream,
913
+ stopLiveStream,
914
+ getDeviceLiveFrame,
748
915
  getDeviceThumbnail,
749
916
  getPairToken: () => pairToken
750
917
  };
@@ -61,6 +61,7 @@ try {
61
61
  status: true,
62
62
  thumbnail: false,
63
63
  control: false,
64
+ liveStream: true,
64
65
  computerAgent: true,
65
66
  taskDispatch: true,
66
67
  aiAssist: true,
@@ -112,6 +113,59 @@ try {
112
113
  assert.equal(thumbnailDevice.latestThumbnail.streamId, 'smoke-thumb');
113
114
  assert.equal(thumbnailDevice.counters.thumbnailFramesReceived, 1);
114
115
 
116
+ const liveCommand = hub.startLiveStream('smoke-device', {
117
+ streamId: 'smoke-live',
118
+ fps: 5,
119
+ maxWidth: 320,
120
+ maxHeight: 180,
121
+ quality: 50
122
+ });
123
+ assert.equal(liveCommand.ok, true);
124
+ writeJsonLine(socket, {
125
+ type: 'stream.frame',
126
+ commandId: liveCommand.commandId,
127
+ streamId: 'smoke-live',
128
+ frameSeq: 2,
129
+ width: 2,
130
+ height: 1,
131
+ mimeType: 'image/png',
132
+ mode: 'remote-fast',
133
+ fps: 5,
134
+ capturedAt: new Date().toISOString(),
135
+ data: 'iVBORw0KGgoAAAANSUhEUgAAAAIAAAABCAYAAAD0In+KAAAADElEQVR42mP8z8AAAAMBAQDJ/pLvAAAAAElFTkSuQmCC'
136
+ });
137
+
138
+ const liveDevice = await waitFor(() => {
139
+ const current = hub.listDevices();
140
+ return current[0]?.latestLiveFrame?.streamId === 'smoke-live' ? current[0] : null;
141
+ });
142
+ assert.equal(liveDevice.activeLiveStream.active, true);
143
+ assert.equal(liveDevice.latestLiveFrame.mode, 'remote-fast');
144
+ assert.equal(liveDevice.counters.liveFramesReceived, 1);
145
+
146
+ const staleFrameBefore = liveDevice.counters.liveFramesDropped;
147
+ writeJsonLine(socket, {
148
+ type: 'stream.frame',
149
+ streamId: 'stale-live',
150
+ frameSeq: 3,
151
+ width: 2,
152
+ height: 1,
153
+ mimeType: 'image/png',
154
+ capturedAt: new Date().toISOString(),
155
+ data: 'iVBORw0KGgoAAAANSUhEUgAAAAIAAAABCAYAAAD0In+KAAAADElEQVR42mP8z8AAAAMBAQDJ/pLvAAAAAElFTkSuQmCC'
156
+ });
157
+ const staleDropDevice = await waitFor(() => {
158
+ const current = hub.listDevices();
159
+ return current[0]?.counters?.liveFramesDropped > staleFrameBefore ? current[0] : null;
160
+ });
161
+ assert.equal(staleDropDevice.latestLiveFrame.streamId, 'smoke-live');
162
+
163
+ const stopLiveCommand = hub.stopLiveStream('smoke-device', {
164
+ streamId: 'smoke-live'
165
+ });
166
+ assert.equal(stopLiveCommand.ok, true);
167
+ assert.equal(hub.listDevices()[0].activeLiveStream.active, false);
168
+
115
169
  const taskCommand = hub.requestAgentTask('smoke-device', {
116
170
  instruction: 'Summarize current desktop status for the manager.',
117
171
  title: 'Smoke task'
package/server.js CHANGED
@@ -7048,6 +7048,34 @@ app.post('/api/remote/devices/:deviceId/thumbnail/request', (req, res) => {
7048
7048
  }));
7049
7049
  });
7050
7050
 
7051
+ app.get('/api/remote/devices/:deviceId/live/frame', (req, res) => {
7052
+ res.setHeader('Cache-Control', 'no-store');
7053
+ const frame = remoteHub.getDeviceLiveFrame(req.params.deviceId);
7054
+ if (!frame) {
7055
+ res.status(404).json({ ok: false, error: 'live-frame-not-available' });
7056
+ return;
7057
+ }
7058
+
7059
+ res.json({ ok: true, frame });
7060
+ });
7061
+
7062
+ app.post('/api/remote/devices/:deviceId/live/start', (req, res) => {
7063
+ res.json(remoteHub.startLiveStream(req.params.deviceId, {
7064
+ streamId: req.body?.streamId,
7065
+ mode: req.body?.mode,
7066
+ fps: req.body?.fps,
7067
+ maxWidth: req.body?.maxWidth,
7068
+ maxHeight: req.body?.maxHeight,
7069
+ quality: req.body?.quality
7070
+ }));
7071
+ });
7072
+
7073
+ app.post('/api/remote/devices/:deviceId/live/stop', (req, res) => {
7074
+ res.json(remoteHub.stopLiveStream(req.params.deviceId, {
7075
+ streamId: req.body?.streamId
7076
+ }));
7077
+ });
7078
+
7051
7079
  app.get('/api/codex/capabilities', async (req, res) => {
7052
7080
  try {
7053
7081
  res.json(await getCodexRuntime().getCapabilities());
@@ -12105,6 +12105,21 @@
12105
12105
  return select;
12106
12106
  }
12107
12107
 
12108
+ const REMOTE_FLEET_MONITOR_REFRESH_MS = 5000;
12109
+ const REMOTE_FLEET_LIVE_REFRESH_MS = 1000;
12110
+
12111
+ function clearRemoteFleetTimers(bodyView) {
12112
+ if (!bodyView) return;
12113
+ if (bodyView._remoteFleetLiveRefreshTimer) {
12114
+ clearInterval(bodyView._remoteFleetLiveRefreshTimer);
12115
+ bodyView._remoteFleetLiveRefreshTimer = null;
12116
+ }
12117
+ if (bodyView._remoteFleetMonitorRefreshTimer) {
12118
+ clearInterval(bodyView._remoteFleetMonitorRefreshTimer);
12119
+ bodyView._remoteFleetMonitorRefreshTimer = null;
12120
+ }
12121
+ }
12122
+
12108
12123
  function getRemoteFleetDeviceField(device, camelKey, pascalKey, fallback = '') {
12109
12124
  const value = device?.[camelKey] ?? device?.[pascalKey] ?? fallback;
12110
12125
  return value === undefined || value === null ? fallback : value;
@@ -12139,6 +12154,19 @@
12139
12154
  return /^data:image\/(png|jpe?g|webp|svg\+xml);base64,/i.test(dataUrl);
12140
12155
  }
12141
12156
 
12157
+ function hasRemoteFleetLiveFrame(device) {
12158
+ const dataUrl = String(getRemoteFleetDeviceField(device, 'liveFrameDataUrl', 'LiveFrameDataUrl', ''));
12159
+ return /^data:image\/(png|jpe?g|webp|svg\+xml);base64,/i.test(dataUrl);
12160
+ }
12161
+
12162
+ function isRemoteFleetLiveActive(device) {
12163
+ return getRemoteFleetDeviceField(device, 'liveStreamActive', 'LiveStreamActive', false) === true;
12164
+ }
12165
+
12166
+ function isRemoteFleetThumbnailCapable(device) {
12167
+ return getRemoteFleetDeviceField(device, 'thumbnailEnabled', 'ThumbnailEnabled', false) === true;
12168
+ }
12169
+
12142
12170
  function getRemoteFleetTimestampMs(value) {
12143
12171
  const timestamp = Date.parse(String(value || ''));
12144
12172
  return Number.isFinite(timestamp) ? timestamp : 0;
@@ -12165,6 +12193,8 @@
12165
12193
  getRemoteFleetDeviceField(device, 'release', 'Release', ''),
12166
12194
  getRemoteFleetDeviceField(device, 'arch', 'Arch', ''),
12167
12195
  getRemoteFleetDeviceField(device, 'agentVersion', 'AgentVersion', ''),
12196
+ getRemoteFleetDeviceField(device, 'liveStreamMode', 'LiveStreamMode', ''),
12197
+ isRemoteFleetLiveActive(device) ? 'live active remote-fast' : '',
12168
12198
  getRemoteFleetDeviceField(device, 'latestTaskTitle', 'LatestTaskTitle', ''),
12169
12199
  getRemoteFleetDeviceField(device, 'latestTaskStatus', 'LatestTaskStatus', ''),
12170
12200
  getRemoteFleetDeviceField(device, 'latestTaskResultSummary', 'LatestTaskResultSummary', ''),
@@ -12210,6 +12240,7 @@
12210
12240
 
12211
12241
  function renderRemoteFleetMonitor(bodyView, nodeModel) {
12212
12242
  if (!bodyView) return;
12243
+ clearRemoteFleetTimers(bodyView);
12213
12244
 
12214
12245
  const nodeId = String(nodeModel?.id ?? nodeModel?.Id ?? '');
12215
12246
  const searchState = bodyView.dataset.remoteFleetSearch || '';
@@ -12217,8 +12248,19 @@
12217
12248
  const sortState = bodyView.dataset.remoteFleetSort || 'status';
12218
12249
  const densityState = bodyView.dataset.remoteFleetDensity || 'cards';
12219
12250
  const aiAssistState = bodyView.dataset.remoteFleetAiAssist === 'true';
12251
+ const autoMonitorState = bodyView.dataset.remoteFleetAutoMonitor !== 'false';
12252
+ const focusState = bodyView.dataset.remoteFleetFocusDeviceId || '';
12220
12253
  const devices = [...parseRemoteFleetDevices(nodeModel)]
12221
12254
  .sort((left, right) => compareRemoteFleetDevices(left, right, sortState));
12255
+ const hasActiveLiveStream = devices.some(isRemoteFleetLiveActive);
12256
+ const hasMonitorTargets = devices.some(device =>
12257
+ isRemoteFleetDeviceConnected(device) && isRemoteFleetThumbnailCapable(device));
12258
+ const focusedDevice = devices.find(device => getRemoteFleetDeviceId(device) === focusState)
12259
+ || devices.find(device => isRemoteFleetLiveActive(device))
12260
+ || null;
12261
+ if (focusedDevice) {
12262
+ bodyView.dataset.remoteFleetFocusDeviceId = getRemoteFleetDeviceId(focusedDevice);
12263
+ }
12222
12264
  const total = Number(getRemoteFleetMetadataValue(nodeModel, 'RemoteFleetDeviceCount', devices.length));
12223
12265
  const connected = Number(getRemoteFleetMetadataValue(
12224
12266
  nodeModel,
@@ -12266,7 +12308,7 @@
12266
12308
  const commandRow = document.createElement('div');
12267
12309
  commandRow.style.cssText = `
12268
12310
  display: grid;
12269
- grid-template-columns: minmax(0, 1fr) auto auto;
12311
+ grid-template-columns: minmax(0, 1fr) auto auto auto;
12270
12312
  gap: 8px;
12271
12313
  align-items: center;
12272
12314
  flex: 0 0 auto;
@@ -12291,7 +12333,42 @@
12291
12333
 
12292
12334
  const copyButton = createRemoteFleetButton('Copy', 'Copy agent command', 'copy-command');
12293
12335
  const refreshButton = createRemoteFleetButton('Refresh', 'Refresh remote devices', 'refresh');
12336
+ const autoMonitorLabel = document.createElement('label');
12337
+ autoMonitorLabel.title = 'Refresh stale thumbnails on a bounded timer';
12338
+ autoMonitorLabel.style.cssText = `
12339
+ display: inline-flex;
12340
+ align-items: center;
12341
+ justify-content: center;
12342
+ gap: 6px;
12343
+ height: 34px;
12344
+ padding: 0 9px;
12345
+ border-radius: 7px;
12346
+ border: 1px solid rgba(14, 165, 233, 0.28);
12347
+ background: ${autoMonitorState ? 'rgba(240, 249, 255, 0.92)' : 'rgba(248, 250, 252, 0.92)'};
12348
+ color: ${autoMonitorState ? '#0369a1' : '#475569'};
12349
+ font-size: 11px;
12350
+ font-weight: 900;
12351
+ letter-spacing: 0;
12352
+ cursor: pointer;
12353
+ pointer-events: auto;
12354
+ user-select: none;
12355
+ `;
12356
+ const autoMonitorToggle = document.createElement('input');
12357
+ autoMonitorToggle.type = 'checkbox';
12358
+ autoMonitorToggle.checked = autoMonitorState;
12359
+ autoMonitorToggle.dataset.remoteFleetAutoToggle = 'true';
12360
+ autoMonitorToggle.style.cssText = `
12361
+ width: 14px;
12362
+ height: 14px;
12363
+ margin: 0;
12364
+ accent-color: #0284c7;
12365
+ `;
12366
+ const autoMonitorText = document.createElement('span');
12367
+ autoMonitorText.textContent = 'Auto';
12368
+ autoMonitorLabel.appendChild(autoMonitorToggle);
12369
+ autoMonitorLabel.appendChild(autoMonitorText);
12294
12370
  commandRow.appendChild(commandText);
12371
+ commandRow.appendChild(autoMonitorLabel);
12295
12372
  commandRow.appendChild(copyButton);
12296
12373
  commandRow.appendChild(refreshButton);
12297
12374
  bodyView.appendChild(commandRow);
@@ -12333,6 +12410,7 @@
12333
12410
  { value: 'offline', label: 'Offline' },
12334
12411
  { value: 'task', label: 'Task' },
12335
12412
  { value: 'ai', label: 'AI' },
12413
+ { value: 'live', label: 'Live' },
12336
12414
  { value: 'frames', label: 'Frames' },
12337
12415
  { value: 'issues', label: 'Issues' }
12338
12416
  ], filterState, 'Device filter');
@@ -12465,6 +12543,141 @@
12465
12543
  `;
12466
12544
  bodyView.appendChild(taskFeedback);
12467
12545
 
12546
+ if (focusedDevice) {
12547
+ const focusedId = getRemoteFleetDeviceId(focusedDevice);
12548
+ const focusedName = getRemoteFleetDeviceName(focusedDevice);
12549
+ const liveActive = isRemoteFleetLiveActive(focusedDevice);
12550
+ const liveFrameDataUrl = String(getRemoteFleetDeviceField(focusedDevice, 'liveFrameDataUrl', 'LiveFrameDataUrl', ''));
12551
+ const liveFrameAt = String(getRemoteFleetDeviceField(focusedDevice, 'liveFrameReceivedAt', 'LiveFrameReceivedAt', ''));
12552
+ const liveStreamId = String(getRemoteFleetDeviceField(focusedDevice, 'liveStreamId', 'LiveStreamId', ''));
12553
+ const hasLiveFrame = hasRemoteFleetLiveFrame(focusedDevice);
12554
+ const livePanel = document.createElement('section');
12555
+ livePanel.dataset.remoteFleetLivePanel = 'true';
12556
+ livePanel.dataset.deviceId = focusedId;
12557
+ livePanel.style.cssText = `
12558
+ flex: 0 0 auto;
12559
+ display: grid;
12560
+ grid-template-columns: minmax(220px, 0.95fr) minmax(0, 1.25fr);
12561
+ gap: 10px;
12562
+ min-height: 170px;
12563
+ padding: 10px;
12564
+ border-radius: 8px;
12565
+ border: 1px solid rgba(14, 165, 233, 0.24);
12566
+ background: rgba(255, 255, 255, 0.82);
12567
+ `;
12568
+
12569
+ const livePreview = document.createElement('div');
12570
+ livePreview.style.cssText = `
12571
+ position: relative;
12572
+ min-width: 0;
12573
+ aspect-ratio: 16 / 9;
12574
+ overflow: hidden;
12575
+ border-radius: 7px;
12576
+ background: linear-gradient(135deg, #111827 0%, #1f2937 100%);
12577
+ border: 1px solid rgba(15, 23, 42, 0.16);
12578
+ `;
12579
+ if (hasLiveFrame) {
12580
+ const image = document.createElement('img');
12581
+ image.src = liveFrameDataUrl;
12582
+ image.alt = `${focusedName} live screen`;
12583
+ image.decoding = 'async';
12584
+ image.style.cssText = `
12585
+ width: 100%;
12586
+ height: 100%;
12587
+ object-fit: cover;
12588
+ display: block;
12589
+ `;
12590
+ livePreview.appendChild(image);
12591
+ } else {
12592
+ const placeholder = document.createElement('div');
12593
+ placeholder.textContent = liveActive ? 'Waiting for live frame' : 'Live view idle';
12594
+ placeholder.style.cssText = `
12595
+ position: absolute;
12596
+ inset: 0;
12597
+ display: flex;
12598
+ align-items: center;
12599
+ justify-content: center;
12600
+ color: rgba(226, 232, 240, 0.82);
12601
+ font-size: 12px;
12602
+ font-weight: 900;
12603
+ letter-spacing: 0;
12604
+ `;
12605
+ livePreview.appendChild(placeholder);
12606
+ }
12607
+ const liveBadge = document.createElement('span');
12608
+ liveBadge.textContent = liveActive
12609
+ ? `LIVE ${liveFrameAt ? formatRemoteFleetAge(liveFrameAt) : ''}`.trim()
12610
+ : 'FOCUS';
12611
+ liveBadge.style.cssText = `
12612
+ position: absolute;
12613
+ left: 7px;
12614
+ top: 7px;
12615
+ max-width: calc(100% - 14px);
12616
+ padding: 4px 7px;
12617
+ border-radius: 999px;
12618
+ background: ${liveActive ? 'rgba(220, 38, 38, 0.86)' : 'rgba(15, 23, 42, 0.72)'};
12619
+ color: #ffffff;
12620
+ font-size: 9px;
12621
+ font-weight: 950;
12622
+ line-height: 1;
12623
+ overflow: hidden;
12624
+ text-overflow: ellipsis;
12625
+ white-space: nowrap;
12626
+ letter-spacing: 0;
12627
+ `;
12628
+ livePreview.appendChild(liveBadge);
12629
+
12630
+ const liveInfo = document.createElement('div');
12631
+ liveInfo.style.cssText = `
12632
+ min-width: 0;
12633
+ display: flex;
12634
+ flex-direction: column;
12635
+ justify-content: space-between;
12636
+ gap: 8px;
12637
+ `;
12638
+ const liveTitle = document.createElement('div');
12639
+ liveTitle.style.cssText = 'min-width:0;display:flex;flex-direction:column;gap:3px;';
12640
+ const liveName = document.createElement('strong');
12641
+ liveName.textContent = focusedName;
12642
+ liveName.title = focusedName;
12643
+ liveName.style.cssText = 'color:#0f172a;font-size:15px;font-weight:950;line-height:1.15;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;letter-spacing:0;';
12644
+ const liveMeta = document.createElement('span');
12645
+ liveMeta.textContent = liveActive
12646
+ ? `RemoteFast ${getRemoteFleetDeviceField(focusedDevice, 'liveStreamFps', 'LiveStreamFps', 0) || 12} fps`
12647
+ : 'View-only focused screen stream';
12648
+ liveMeta.style.cssText = 'color:#475569;font-size:11px;font-weight:800;line-height:1.25;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;letter-spacing:0;';
12649
+ liveTitle.appendChild(liveName);
12650
+ liveTitle.appendChild(liveMeta);
12651
+
12652
+ const liveActions = document.createElement('div');
12653
+ liveActions.style.cssText = 'display:flex;align-items:center;gap:8px;flex-wrap:wrap;';
12654
+ if (isRemoteFleetDeviceConnected(focusedDevice) && getRemoteFleetDeviceField(focusedDevice, 'liveStreamEnabled', 'LiveStreamEnabled', false) === true) {
12655
+ const startLiveButton = createRemoteFleetButton(liveActive ? 'Restart live' : 'Start live', 'Start focused view-only live stream', 'live-start');
12656
+ startLiveButton.dataset.deviceId = focusedId;
12657
+ startLiveButton.style.height = '30px';
12658
+ liveActions.appendChild(startLiveButton);
12659
+ if (liveActive) {
12660
+ const stopLiveButton = createRemoteFleetButton('Stop live', 'Stop focused live stream', 'live-stop');
12661
+ stopLiveButton.dataset.deviceId = focusedId;
12662
+ stopLiveButton.dataset.streamId = liveStreamId;
12663
+ stopLiveButton.style.height = '30px';
12664
+ stopLiveButton.style.borderColor = 'rgba(220, 38, 38, 0.32)';
12665
+ stopLiveButton.style.color = '#b91c1c';
12666
+ liveActions.appendChild(stopLiveButton);
12667
+ }
12668
+ } else {
12669
+ const disabled = document.createElement('span');
12670
+ disabled.textContent = 'Live unavailable';
12671
+ disabled.style.cssText = 'color:#64748b;font-size:11px;font-weight:900;';
12672
+ liveActions.appendChild(disabled);
12673
+ }
12674
+ liveInfo.appendChild(liveTitle);
12675
+ liveInfo.appendChild(liveActions);
12676
+ livePanel.appendChild(livePreview);
12677
+ livePanel.appendChild(liveInfo);
12678
+ bodyView.appendChild(livePanel);
12679
+ }
12680
+
12468
12681
  if (lastError.trim()) {
12469
12682
  const errorEl = document.createElement('div');
12470
12683
  errorEl.textContent = lastError;
@@ -12524,7 +12737,13 @@
12524
12737
  const thumbnailEnabled = device?.thumbnailEnabled === true || device?.ThumbnailEnabled === true;
12525
12738
  const thumbnailDataUrl = String(device?.thumbnailDataUrl || device?.ThumbnailDataUrl || '');
12526
12739
  const thumbnailCapturedAt = String(device?.thumbnailCapturedAt || device?.ThumbnailCapturedAt || '');
12740
+ const liveStreamEnabled = device?.liveStreamEnabled === true || device?.LiveStreamEnabled === true;
12741
+ const liveStreamActive = isRemoteFleetLiveActive(device);
12742
+ const liveStreamId = String(device?.liveStreamId || device?.LiveStreamId || '');
12743
+ const liveFrameDataUrl = String(device?.liveFrameDataUrl || device?.LiveFrameDataUrl || '');
12744
+ const liveFrameReceivedAt = String(device?.liveFrameReceivedAt || device?.LiveFrameReceivedAt || '');
12527
12745
  const hasThumbnail = hasRemoteFleetThumbnail(device);
12746
+ const hasLiveFrame = hasRemoteFleetLiveFrame(device);
12528
12747
  const taskEnabled = isRemoteFleetDeviceTaskCapable(device);
12529
12748
  const aiAssistEnabled = isRemoteFleetDeviceAiCapable(device);
12530
12749
  const aiModel = String(device?.aiModel || device?.AiModel || '');
@@ -12540,7 +12759,9 @@
12540
12759
  card.dataset.remoteFleetConnected = connectedDevice ? 'true' : 'false';
12541
12760
  card.dataset.remoteFleetTaskCapable = taskEnabled ? 'true' : 'false';
12542
12761
  card.dataset.remoteFleetAiCapable = aiAssistEnabled ? 'true' : 'false';
12543
- card.dataset.remoteFleetHasFrame = hasThumbnail ? 'true' : 'false';
12762
+ card.dataset.remoteFleetHasFrame = (hasThumbnail || hasLiveFrame) ? 'true' : 'false';
12763
+ card.dataset.remoteFleetLiveCapable = liveStreamEnabled ? 'true' : 'false';
12764
+ card.dataset.remoteFleetLiveActive = liveStreamActive ? 'true' : 'false';
12544
12765
  card.dataset.remoteFleetIssue = (!connectedDevice || latestTaskStatus === 'failed' || !!latestTaskError) ? 'true' : 'false';
12545
12766
  card.style.cssText = `
12546
12767
  display: flex;
@@ -12556,6 +12777,8 @@
12556
12777
  `;
12557
12778
 
12558
12779
  if (densityState !== 'dense') {
12780
+ const previewDataUrl = hasLiveFrame ? liveFrameDataUrl : thumbnailDataUrl;
12781
+ const previewAt = hasLiveFrame ? liveFrameReceivedAt : thumbnailCapturedAt;
12559
12782
  const preview = document.createElement('div');
12560
12783
  preview.style.cssText = `
12561
12784
  position: relative;
@@ -12566,10 +12789,10 @@
12566
12789
  background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%);
12567
12790
  border: 1px solid rgba(15, 23, 42, 0.12);
12568
12791
  `;
12569
- if (hasThumbnail) {
12792
+ if (hasLiveFrame || hasThumbnail) {
12570
12793
  const image = document.createElement('img');
12571
- image.src = thumbnailDataUrl;
12572
- image.alt = `${name} thumbnail`;
12794
+ image.src = previewDataUrl;
12795
+ image.alt = hasLiveFrame ? `${name} live frame` : `${name} thumbnail`;
12573
12796
  image.loading = 'lazy';
12574
12797
  image.decoding = 'async';
12575
12798
  image.style.cssText = `
@@ -12595,9 +12818,11 @@
12595
12818
  `;
12596
12819
  preview.appendChild(placeholder);
12597
12820
  }
12598
- if (thumbnailCapturedAt) {
12821
+ if (previewAt) {
12599
12822
  const badge = document.createElement('span');
12600
- badge.textContent = formatRemoteFleetAge(thumbnailCapturedAt);
12823
+ badge.textContent = hasLiveFrame
12824
+ ? `LIVE ${formatRemoteFleetAge(previewAt)}`
12825
+ : formatRemoteFleetAge(previewAt);
12601
12826
  badge.style.cssText = `
12602
12827
  position: absolute;
12603
12828
  right: 6px;
@@ -12605,7 +12830,7 @@
12605
12830
  max-width: calc(100% - 12px);
12606
12831
  padding: 3px 6px;
12607
12832
  border-radius: 999px;
12608
- background: rgba(15, 23, 42, 0.74);
12833
+ background: ${hasLiveFrame ? 'rgba(220, 38, 38, 0.82)' : 'rgba(15, 23, 42, 0.74)'};
12609
12834
  color: #e2e8f0;
12610
12835
  font-size: 9px;
12611
12836
  font-weight: 900;
@@ -12763,11 +12988,11 @@
12763
12988
  actions.style.cssText = 'display:flex;align-items:center;justify-content:space-between;gap:8px;margin-top:auto;';
12764
12989
  const status = document.createElement('span');
12765
12990
  status.textContent = connectedDevice
12766
- ? (aiAssistEnabled ? `AI ${aiModel || 'ready'}` : 'Connected')
12991
+ ? (liveStreamActive ? 'Live' : (aiAssistEnabled ? `AI ${aiModel || 'ready'}` : 'Connected'))
12767
12992
  : 'Offline';
12768
12993
  status.style.cssText = `
12769
12994
  min-width: 0;
12770
- color: ${connectedDevice ? '#047857' : '#64748b'};
12995
+ color: ${liveStreamActive ? '#b91c1c' : (connectedDevice ? '#047857' : '#64748b')};
12771
12996
  font-size: 11px;
12772
12997
  font-weight: 900;
12773
12998
  overflow: hidden;
@@ -12776,6 +13001,23 @@
12776
13001
  `;
12777
13002
  actions.appendChild(status);
12778
13003
  if (connectedDevice && deviceId) {
13004
+ const focusButton = createRemoteFleetButton('Focus', 'Show this device in focused live panel', 'live-focus');
13005
+ focusButton.dataset.deviceId = deviceId;
13006
+ focusButton.style.height = '24px';
13007
+ focusButton.style.fontSize = '10px';
13008
+ actions.appendChild(focusButton);
13009
+ if (liveStreamEnabled) {
13010
+ const liveButton = createRemoteFleetButton(liveStreamActive ? 'Stop' : 'Live', liveStreamActive ? 'Stop live stream' : 'Start focused live stream', liveStreamActive ? 'live-stop' : 'live-start');
13011
+ liveButton.dataset.deviceId = deviceId;
13012
+ liveButton.dataset.streamId = liveStreamId;
13013
+ liveButton.style.height = '24px';
13014
+ liveButton.style.fontSize = '10px';
13015
+ if (liveStreamActive) {
13016
+ liveButton.style.borderColor = 'rgba(220, 38, 38, 0.32)';
13017
+ liveButton.style.color = '#b91c1c';
13018
+ }
13019
+ actions.appendChild(liveButton);
13020
+ }
12779
13021
  if (taskEnabled) {
12780
13022
  const taskButton = createRemoteFleetButton('Task', 'Dispatch task to this device', 'task-device');
12781
13023
  taskButton.dataset.deviceId = deviceId;
@@ -12822,8 +13064,7 @@
12822
13064
  bodyView.appendChild(grid);
12823
13065
 
12824
13066
  const footer = document.createElement('div');
12825
- footer.textContent = `Endpoint ${endpoint} · refreshed ${formatRemoteFleetAge(refreshedAt)}`;
12826
- footer.textContent = `Endpoint ${endpoint} - all devices, no paging - refreshed ${formatRemoteFleetAge(refreshedAt)}`;
13067
+ footer.textContent = `Endpoint ${endpoint} - all devices, no paging - ${autoMonitorState ? 'auto monitor' : 'manual'} - refreshed ${formatRemoteFleetAge(refreshedAt)}`;
12827
13068
  footer.style.cssText = `
12828
13069
  flex: 0 0 auto;
12829
13070
  color: #64748b;
@@ -12860,6 +13101,20 @@
12860
13101
  const readTaskInstruction = () => String(taskInput.value || '').trim();
12861
13102
  const useAiAssist = () => aiToggle.checked === true;
12862
13103
  const getDeviceCards = () => Array.from(grid.querySelectorAll('article[data-device-id]'));
13104
+ const refreshRemoteFleetNode = async () => {
13105
+ if (bodyView._remoteFleetRefreshInFlight === true) {
13106
+ return null;
13107
+ }
13108
+
13109
+ bodyView._remoteFleetRefreshInFlight = true;
13110
+ try {
13111
+ const result = await invokeDotNetAsync('RefreshRemoteFleetMonitorNodeFromJs', nodeId);
13112
+ await syncRemoteFleetNodeStateFromResult(result);
13113
+ return result;
13114
+ } finally {
13115
+ bodyView._remoteFleetRefreshInFlight = false;
13116
+ }
13117
+ };
12863
13118
  const getVisibleEligibleDeviceIds = () => {
12864
13119
  const wantsAi = useAiAssist();
12865
13120
  return getDeviceCards()
@@ -12887,6 +13142,7 @@
12887
13142
  || (filterMode === 'offline' && card.dataset.remoteFleetConnected !== 'true')
12888
13143
  || (filterMode === 'task' && card.dataset.remoteFleetConnected === 'true' && card.dataset.remoteFleetTaskCapable === 'true')
12889
13144
  || (filterMode === 'ai' && card.dataset.remoteFleetConnected === 'true' && card.dataset.remoteFleetAiCapable === 'true')
13145
+ || (filterMode === 'live' && card.dataset.remoteFleetLiveActive === 'true')
12890
13146
  || (filterMode === 'frames' && card.dataset.remoteFleetHasFrame === 'true')
12891
13147
  || (filterMode === 'issues' && card.dataset.remoteFleetIssue === 'true');
12892
13148
  const show = matchesSearch && matchesMode;
@@ -12935,10 +13191,19 @@
12935
13191
  control.addEventListener(eventName, event => event.stopPropagation());
12936
13192
  });
12937
13193
  });
13194
+ [autoMonitorLabel, autoMonitorToggle].forEach(control => {
13195
+ ['mousedown', 'mouseup', 'click', 'dblclick', 'keydown'].forEach(eventName => {
13196
+ control.addEventListener(eventName, event => event.stopPropagation());
13197
+ });
13198
+ });
12938
13199
 
12939
13200
  searchInput.addEventListener('input', applyRemoteFleetFilters);
12940
13201
  filterSelect.addEventListener('change', applyRemoteFleetFilters);
12941
13202
  aiToggle.addEventListener('change', applyRemoteFleetFilters);
13203
+ autoMonitorToggle.addEventListener('change', () => {
13204
+ bodyView.dataset.remoteFleetAutoMonitor = autoMonitorToggle.checked ? 'true' : 'false';
13205
+ renderRemoteFleetMonitor(bodyView, nodeModel);
13206
+ });
12942
13207
  sortSelect.addEventListener('change', () => {
12943
13208
  bodyView.dataset.remoteFleetSort = String(sortSelect.value || 'status');
12944
13209
  renderRemoteFleetMonitor(bodyView, nodeModel);
@@ -13011,9 +13276,67 @@
13011
13276
  event.preventDefault();
13012
13277
  event.stopPropagation();
13013
13278
  refreshButton.disabled = true;
13014
- const result = await invokeDotNetAsync('RefreshRemoteFleetMonitorNodeFromJs', nodeId);
13015
- await syncRemoteFleetNodeStateFromResult(result);
13016
- refreshButton.disabled = false;
13279
+ try {
13280
+ await refreshRemoteFleetNode();
13281
+ } finally {
13282
+ refreshButton.disabled = false;
13283
+ }
13284
+ });
13285
+
13286
+ bodyView.querySelectorAll('[data-remote-fleet-action="live-focus"]').forEach(button => {
13287
+ button.addEventListener('click', event => {
13288
+ event.preventDefault();
13289
+ event.stopPropagation();
13290
+ const deviceId = String(button.dataset.deviceId || '').trim();
13291
+ if (!deviceId) return;
13292
+ bodyView.dataset.remoteFleetFocusDeviceId = deviceId;
13293
+ renderRemoteFleetMonitor(bodyView, nodeModel);
13294
+ });
13295
+ });
13296
+
13297
+ bodyView.querySelectorAll('[data-remote-fleet-action="live-start"]').forEach(button => {
13298
+ button.addEventListener('click', async event => {
13299
+ event.preventDefault();
13300
+ event.stopPropagation();
13301
+ const deviceId = String(button.dataset.deviceId || '').trim();
13302
+ if (!deviceId) return;
13303
+ bodyView.dataset.remoteFleetFocusDeviceId = deviceId;
13304
+ button.disabled = true;
13305
+ setTaskFeedback('Starting focused live stream...');
13306
+ try {
13307
+ const result = await invokeDotNetAsync('StartRemoteFleetLiveStreamFromJs', nodeId, deviceId);
13308
+ await syncRemoteFleetNodeStateFromResult(result);
13309
+ if (result?.success) {
13310
+ setTaskFeedback('Focused live stream started.', 'success');
13311
+ } else {
13312
+ setTaskFeedback(result?.error || 'Live stream start failed.', 'error');
13313
+ }
13314
+ } finally {
13315
+ button.disabled = false;
13316
+ }
13317
+ });
13318
+ });
13319
+
13320
+ bodyView.querySelectorAll('[data-remote-fleet-action="live-stop"]').forEach(button => {
13321
+ button.addEventListener('click', async event => {
13322
+ event.preventDefault();
13323
+ event.stopPropagation();
13324
+ const deviceId = String(button.dataset.deviceId || '').trim();
13325
+ if (!deviceId) return;
13326
+ button.disabled = true;
13327
+ setTaskFeedback('Stopping focused live stream...');
13328
+ try {
13329
+ const result = await invokeDotNetAsync('StopRemoteFleetLiveStreamFromJs', nodeId, deviceId, button.dataset.streamId || '');
13330
+ await syncRemoteFleetNodeStateFromResult(result);
13331
+ if (result?.success) {
13332
+ setTaskFeedback('Focused live stream stopped.', 'success');
13333
+ } else {
13334
+ setTaskFeedback(result?.error || 'Live stream stop failed.', 'error');
13335
+ }
13336
+ } finally {
13337
+ button.disabled = false;
13338
+ }
13339
+ });
13017
13340
  });
13018
13341
 
13019
13342
  grid.querySelectorAll('[data-remote-fleet-action="ping-device"]').forEach(button => {
@@ -13065,6 +13388,34 @@
13065
13388
  button.disabled = false;
13066
13389
  });
13067
13390
  });
13391
+
13392
+ if (hasActiveLiveStream) {
13393
+ bodyView._remoteFleetLiveRefreshTimer = setInterval(async () => {
13394
+ if (!document.body.contains(bodyView)) {
13395
+ clearRemoteFleetTimers(bodyView);
13396
+ return;
13397
+ }
13398
+
13399
+ try {
13400
+ await refreshRemoteFleetNode();
13401
+ } catch {
13402
+ clearRemoteFleetTimers(bodyView);
13403
+ }
13404
+ }, REMOTE_FLEET_LIVE_REFRESH_MS);
13405
+ } else if (autoMonitorState && hasMonitorTargets) {
13406
+ bodyView._remoteFleetMonitorRefreshTimer = setInterval(async () => {
13407
+ if (!document.body.contains(bodyView)) {
13408
+ clearRemoteFleetTimers(bodyView);
13409
+ return;
13410
+ }
13411
+
13412
+ try {
13413
+ await refreshRemoteFleetNode();
13414
+ } catch {
13415
+ clearRemoteFleetTimers(bodyView);
13416
+ }
13417
+ }, REMOTE_FLEET_MONITOR_REFRESH_MS);
13418
+ }
13068
13419
  }
13069
13420
 
13070
13421
  function bindMemoNodeEvents(container, nodeModel) {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "mainAssemblyName": "MindExecution.Web",
3
3
  "resources": {
4
- "hash": "sha256-/odScRNovgEc8hBO1R9VAaBYER2ZEkgcGNL8jCOVsoU=",
4
+ "hash": "sha256-XfKFSqnMBnbJmPnM+j3zriBOOdujMLDI/U8Usvww2jQ=",
5
5
  "fingerprinting": {
6
6
  "Google.Protobuf.9h59ukbel7.dll": "Google.Protobuf.dll",
7
7
  "Markdig.d1j7v41cl1.dll": "Markdig.dll",
@@ -123,16 +123,16 @@
123
123
  "System.m05i39uvk9.dll": "System.dll",
124
124
  "netstandard.0xet7jg7ky.dll": "netstandard.dll",
125
125
  "System.Private.CoreLib.rkafq04oma.dll": "System.Private.CoreLib.dll",
126
- "MindExecution.Core.kf4752v5yl.dll": "MindExecution.Core.dll",
127
- "MindExecution.Kernel.79mgmkpsy1.dll": "MindExecution.Kernel.dll",
128
- "MindExecution.Plugins.Admin.8ey1m70q79.dll": "MindExecution.Plugins.Admin.dll",
129
- "MindExecution.Plugins.Business.8rvwgqzxaz.dll": "MindExecution.Plugins.Business.dll",
130
- "MindExecution.Plugins.Concept.ynnk3bqf03.dll": "MindExecution.Plugins.Concept.dll",
131
- "MindExecution.Plugins.Directory.vi24rmnuyq.dll": "MindExecution.Plugins.Directory.dll",
132
- "MindExecution.Plugins.PlanMaster.9r08m5atjr.dll": "MindExecution.Plugins.PlanMaster.dll",
133
- "MindExecution.Plugins.YouTube.fjkbniwa4o.dll": "MindExecution.Plugins.YouTube.dll",
134
- "MindExecution.Shared.e0qnm5vtax.dll": "MindExecution.Shared.dll",
135
- "MindExecution.Web.wk9yv02eva.dll": "MindExecution.Web.dll",
126
+ "MindExecution.Core.pnw79cgqjx.dll": "MindExecution.Core.dll",
127
+ "MindExecution.Kernel.dt3w864bqn.dll": "MindExecution.Kernel.dll",
128
+ "MindExecution.Plugins.Admin.z93cu32xru.dll": "MindExecution.Plugins.Admin.dll",
129
+ "MindExecution.Plugins.Business.b6da8sg85t.dll": "MindExecution.Plugins.Business.dll",
130
+ "MindExecution.Plugins.Concept.mjooiqft9j.dll": "MindExecution.Plugins.Concept.dll",
131
+ "MindExecution.Plugins.Directory.rjod6rdmly.dll": "MindExecution.Plugins.Directory.dll",
132
+ "MindExecution.Plugins.PlanMaster.1dcrzhsegj.dll": "MindExecution.Plugins.PlanMaster.dll",
133
+ "MindExecution.Plugins.YouTube.k75qxhbpp8.dll": "MindExecution.Plugins.YouTube.dll",
134
+ "MindExecution.Shared.y3eqxd3mvo.dll": "MindExecution.Shared.dll",
135
+ "MindExecution.Web.wou9x6mn2f.dll": "MindExecution.Web.dll",
136
136
  "dotnet.js": "dotnet.js",
137
137
  "dotnet.native.xsn1d6x2kd.js": "dotnet.native.js",
138
138
  "dotnet.native.vz0adxojrz.wasm": "dotnet.native.wasm",
@@ -278,18 +278,18 @@
278
278
  "System.Xml.XDocument.c539ki6cuq.dll": "sha256-MPTRJkptrL9nGa2tl4kF46+wErNUYRPCGblX3ANoKoY=",
279
279
  "System.m05i39uvk9.dll": "sha256-5jDfIdbYAigw7/Q/lMzt5W/+cayGbW9ko9FvuaN1GsQ=",
280
280
  "netstandard.0xet7jg7ky.dll": "sha256-xENDv620uJ8fHwLJ2bdhrTHz4QPjvqXOztnk2a4wr0c=",
281
- "MindExecution.Core.kf4752v5yl.dll": "sha256-MK+6sYNYxso5bmurLtzuQOy6PiIr6jHmV0DMzHSg+sc=",
282
- "MindExecution.Kernel.79mgmkpsy1.dll": "sha256-qwoCzn5eUut0Qz0kvhUJmtlPAtoMtYtMTv9c4ekX84o=",
283
- "MindExecution.Plugins.Concept.ynnk3bqf03.dll": "sha256-Mvw1k0NexULKDOElumjmqjK4jl2MS5NNvFrTnBDZ2qc=",
284
- "MindExecution.Plugins.PlanMaster.9r08m5atjr.dll": "sha256-K2De6QooRNGCQL7zfYyZ4aeMZZE2etfT4Aj52u7y+RQ=",
285
- "MindExecution.Shared.e0qnm5vtax.dll": "sha256-bh3YoyPl9H4U9q8TO3u6hoJLMoWW0sYxJyJwj8heek0=",
286
- "MindExecution.Web.wk9yv02eva.dll": "sha256-AHwwn1rOC7uDlpCJJLndmyf97G7nM018K4WpzRuPwTg="
281
+ "MindExecution.Core.pnw79cgqjx.dll": "sha256-64VJ+VCnq+r7lgfYzm99hPX3yo5u2QCC4guGqsrVTus=",
282
+ "MindExecution.Kernel.dt3w864bqn.dll": "sha256-r0Q9mY7CB+L/3j7KlKlNiyLBlXcwBkPRtjGh0RBZOFs=",
283
+ "MindExecution.Plugins.Concept.mjooiqft9j.dll": "sha256-Qrz35qwSLDAaugzQMJ6YLfWI+jld56cCo+9wOwVobgw=",
284
+ "MindExecution.Plugins.PlanMaster.1dcrzhsegj.dll": "sha256-VZ7SUoDTHQ9MdwII/dBqeaVFpDt6UEFDV5xJHAlskgM=",
285
+ "MindExecution.Shared.y3eqxd3mvo.dll": "sha256-EnE/FEgfwb2Trt9cyLvVEeHE8iSRWYhaftVJUOKLAZI=",
286
+ "MindExecution.Web.wou9x6mn2f.dll": "sha256-YENKxQvH07LxcUCqbFITQtVD2sCcdYNAF9aX0YVtVGs="
287
287
  },
288
288
  "lazyAssembly": {
289
- "MindExecution.Plugins.Admin.8ey1m70q79.dll": "sha256-oMlSl7sNr3GfLsqVskx6UNQMyoGJANENnZIL62rfG+c=",
290
- "MindExecution.Plugins.Business.8rvwgqzxaz.dll": "sha256-xJBBAfZAEw5jQcNThJNI3vKn2rLWuCRV7Oo8onCFwOo=",
291
- "MindExecution.Plugins.Directory.vi24rmnuyq.dll": "sha256-x+iwabNhSVcOgBw2RlEp7HSQ7Eep/nvCn+lZVNGxCK8=",
292
- "MindExecution.Plugins.YouTube.fjkbniwa4o.dll": "sha256-RUR2I22IaFbLmHY/Gcw7wfwiybsIs8m+kgMfo1i78+w="
289
+ "MindExecution.Plugins.Admin.z93cu32xru.dll": "sha256-aToPZeovhBXkB0URHkzYdYDAmg1+HxsvojgiefL4pkY=",
290
+ "MindExecution.Plugins.Business.b6da8sg85t.dll": "sha256-M1macJ1ZqfBCrj/1WPw2JQxnFtFX31HxydBxQKa3Cgw=",
291
+ "MindExecution.Plugins.Directory.rjod6rdmly.dll": "sha256-hx8N04/7sIYB8PdHSyVTLhPfPb7ZkmqM1RPBB1J0iuw=",
292
+ "MindExecution.Plugins.YouTube.k75qxhbpp8.dll": "sha256-IM43bEjZvj6SqvBrKl8JmAN5NfN6IW8VPi3Z/HarwlQ="
293
293
  }
294
294
  },
295
295
  "cacheBootResources": true,
@@ -558,7 +558,7 @@
558
558
  }
559
559
 
560
560
  const base = '_content/MindExecution.Shared/js/';
561
- const scriptVersion = '20260612-remote-fleet-scale-v466';
561
+ const scriptVersion = '20260612-remote-auto-monitor-v468';
562
562
  const scriptUrl = (script) => `${base}${script}?v=${scriptVersion}`;
563
563
  console.log(`[Script Loader] Shared JS version: ${scriptVersion}`);
564
564
  const criticalScripts = [
@@ -1,5 +1,5 @@
1
1
  self.assetsManifest = {
2
- "version": "tyUOiEjY",
2
+ "version": "ok6mf/2a",
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-UIJj28RUmbRzdxZB7yxJHs88G8t0QTV1mCQOJeHywK8=",
89
+ "hash": "sha256-9f7DLlG6ShPyYhu/Db97YKVoFr+RhJC2Ko9E4NsL9Lc=",
90
90
  "url": "_content/MindExecution.Shared/js/mind-map-css3d-manager.js"
91
91
  },
92
92
  {
@@ -410,44 +410,44 @@
410
410
  "url": "_framework/MimeMapping.og9ys58ylm.dll"
411
411
  },
412
412
  {
413
- "hash": "sha256-MK+6sYNYxso5bmurLtzuQOy6PiIr6jHmV0DMzHSg+sc=",
414
- "url": "_framework/MindExecution.Core.kf4752v5yl.dll"
413
+ "hash": "sha256-64VJ+VCnq+r7lgfYzm99hPX3yo5u2QCC4guGqsrVTus=",
414
+ "url": "_framework/MindExecution.Core.pnw79cgqjx.dll"
415
415
  },
416
416
  {
417
- "hash": "sha256-qwoCzn5eUut0Qz0kvhUJmtlPAtoMtYtMTv9c4ekX84o=",
418
- "url": "_framework/MindExecution.Kernel.79mgmkpsy1.dll"
417
+ "hash": "sha256-r0Q9mY7CB+L/3j7KlKlNiyLBlXcwBkPRtjGh0RBZOFs=",
418
+ "url": "_framework/MindExecution.Kernel.dt3w864bqn.dll"
419
419
  },
420
420
  {
421
- "hash": "sha256-oMlSl7sNr3GfLsqVskx6UNQMyoGJANENnZIL62rfG+c=",
422
- "url": "_framework/MindExecution.Plugins.Admin.8ey1m70q79.dll"
421
+ "hash": "sha256-aToPZeovhBXkB0URHkzYdYDAmg1+HxsvojgiefL4pkY=",
422
+ "url": "_framework/MindExecution.Plugins.Admin.z93cu32xru.dll"
423
423
  },
424
424
  {
425
- "hash": "sha256-xJBBAfZAEw5jQcNThJNI3vKn2rLWuCRV7Oo8onCFwOo=",
426
- "url": "_framework/MindExecution.Plugins.Business.8rvwgqzxaz.dll"
425
+ "hash": "sha256-M1macJ1ZqfBCrj/1WPw2JQxnFtFX31HxydBxQKa3Cgw=",
426
+ "url": "_framework/MindExecution.Plugins.Business.b6da8sg85t.dll"
427
427
  },
428
428
  {
429
- "hash": "sha256-Mvw1k0NexULKDOElumjmqjK4jl2MS5NNvFrTnBDZ2qc=",
430
- "url": "_framework/MindExecution.Plugins.Concept.ynnk3bqf03.dll"
429
+ "hash": "sha256-Qrz35qwSLDAaugzQMJ6YLfWI+jld56cCo+9wOwVobgw=",
430
+ "url": "_framework/MindExecution.Plugins.Concept.mjooiqft9j.dll"
431
431
  },
432
432
  {
433
- "hash": "sha256-x+iwabNhSVcOgBw2RlEp7HSQ7Eep/nvCn+lZVNGxCK8=",
434
- "url": "_framework/MindExecution.Plugins.Directory.vi24rmnuyq.dll"
433
+ "hash": "sha256-hx8N04/7sIYB8PdHSyVTLhPfPb7ZkmqM1RPBB1J0iuw=",
434
+ "url": "_framework/MindExecution.Plugins.Directory.rjod6rdmly.dll"
435
435
  },
436
436
  {
437
- "hash": "sha256-K2De6QooRNGCQL7zfYyZ4aeMZZE2etfT4Aj52u7y+RQ=",
438
- "url": "_framework/MindExecution.Plugins.PlanMaster.9r08m5atjr.dll"
437
+ "hash": "sha256-VZ7SUoDTHQ9MdwII/dBqeaVFpDt6UEFDV5xJHAlskgM=",
438
+ "url": "_framework/MindExecution.Plugins.PlanMaster.1dcrzhsegj.dll"
439
439
  },
440
440
  {
441
- "hash": "sha256-RUR2I22IaFbLmHY/Gcw7wfwiybsIs8m+kgMfo1i78+w=",
442
- "url": "_framework/MindExecution.Plugins.YouTube.fjkbniwa4o.dll"
441
+ "hash": "sha256-IM43bEjZvj6SqvBrKl8JmAN5NfN6IW8VPi3Z/HarwlQ=",
442
+ "url": "_framework/MindExecution.Plugins.YouTube.k75qxhbpp8.dll"
443
443
  },
444
444
  {
445
- "hash": "sha256-bh3YoyPl9H4U9q8TO3u6hoJLMoWW0sYxJyJwj8heek0=",
446
- "url": "_framework/MindExecution.Shared.e0qnm5vtax.dll"
445
+ "hash": "sha256-EnE/FEgfwb2Trt9cyLvVEeHE8iSRWYhaftVJUOKLAZI=",
446
+ "url": "_framework/MindExecution.Shared.y3eqxd3mvo.dll"
447
447
  },
448
448
  {
449
- "hash": "sha256-AHwwn1rOC7uDlpCJJLndmyf97G7nM018K4WpzRuPwTg=",
450
- "url": "_framework/MindExecution.Web.wk9yv02eva.dll"
449
+ "hash": "sha256-YENKxQvH07LxcUCqbFITQtVD2sCcdYNAF9aX0YVtVGs=",
450
+ "url": "_framework/MindExecution.Web.wou9x6mn2f.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-v2ZTmYhjrtyEfbHSvkcdnYwoI+IoWSf/opZgluPefHA=",
773
+ "hash": "sha256-10lI3q/nXAGFdelS8ribQw72sOgD3PYExvQX6srRqWA=",
774
774
  "url": "_framework/blazor.boot.json"
775
775
  },
776
776
  {
@@ -834,7 +834,7 @@
834
834
  "url": "image-manifest.json"
835
835
  },
836
836
  {
837
- "hash": "sha256-TU40FZ+Rzj9ZDbwOAysHnWs1F2588obb2+wwcp7Mcy4=",
837
+ "hash": "sha256-e/vzCKU0rJNUBs7+YbA+jxOZd53T9CbUxhBAJ0yi6oM=",
838
838
  "url": "index.html"
839
839
  },
840
840
  {
@@ -1,4 +1,4 @@
1
- /* Manifest version: tyUOiEjY */
1
+ /* Manifest version: ok6mf/2a */
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