@mindexec/cli 0.2.5 → 0.2.7

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 +6 -6
  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 +722 -78
  7. package/wwwroot/_framework/MindExecution.Core.pnw79cgqjx.dll +0 -0
  8. package/wwwroot/_framework/{MindExecution.Kernel.9wfplilp5l.dll → MindExecution.Kernel.dt3w864bqn.dll} +0 -0
  9. package/wwwroot/_framework/{MindExecution.Plugins.Admin.sb1vkmct0w.dll → MindExecution.Plugins.Admin.z93cu32xru.dll} +0 -0
  10. package/wwwroot/_framework/{MindExecution.Plugins.Business.zr7rkofx44.dll → MindExecution.Plugins.Business.b6da8sg85t.dll} +0 -0
  11. package/wwwroot/_framework/{MindExecution.Plugins.Concept.g6wd36v92i.dll → MindExecution.Plugins.Concept.mjooiqft9j.dll} +0 -0
  12. package/wwwroot/_framework/{MindExecution.Plugins.Directory.bb5flwt0u7.dll → MindExecution.Plugins.Directory.rjod6rdmly.dll} +0 -0
  13. package/wwwroot/_framework/{MindExecution.Plugins.PlanMaster.me8v9fpgwc.dll → MindExecution.Plugins.PlanMaster.1dcrzhsegj.dll} +0 -0
  14. package/wwwroot/_framework/{MindExecution.Plugins.YouTube.l811fqx9e0.dll → MindExecution.Plugins.YouTube.k75qxhbpp8.dll} +0 -0
  15. package/wwwroot/_framework/{MindExecution.Shared.oseamdg577.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.27f2blpou6.dll +0 -0
  22. package/wwwroot/_framework/MindExecution.Web.96r3nnp9is.dll +0 -0
@@ -12005,8 +12005,11 @@
12005
12005
  return `${Math.floor(seconds / 86400)}d ago`;
12006
12006
  }
12007
12007
 
12008
- function createRemoteFleetStat(label, value, tone = 'default') {
12008
+ function createRemoteFleetStat(label, value, tone = 'default', key = '') {
12009
12009
  const item = document.createElement('div');
12010
+ if (key) {
12011
+ item.dataset.remoteFleetStat = key;
12012
+ }
12010
12013
  item.style.cssText = `
12011
12014
  display: flex;
12012
12015
  flex-direction: column;
@@ -12029,6 +12032,7 @@
12029
12032
  `;
12030
12033
 
12031
12034
  const valueEl = document.createElement('strong');
12035
+ valueEl.dataset.remoteFleetStatValue = 'true';
12032
12036
  valueEl.textContent = value;
12033
12037
  valueEl.style.cssText = `
12034
12038
  color: ${tone === 'online' ? '#047857' : '#0f172a'};
@@ -12073,6 +12077,141 @@
12073
12077
  return button;
12074
12078
  }
12075
12079
 
12080
+ function createRemoteFleetSelect(options, value, title) {
12081
+ const select = document.createElement('select');
12082
+ select.title = title || '';
12083
+ select.setAttribute('aria-label', title || 'Remote fleet option');
12084
+ select.style.cssText = `
12085
+ min-width: 0;
12086
+ height: 34px;
12087
+ border-radius: 7px;
12088
+ border: 1px solid rgba(148, 163, 184, 0.34);
12089
+ background: rgba(255, 255, 255, 0.94);
12090
+ color: #0f172a;
12091
+ padding: 0 8px;
12092
+ font-size: 11px;
12093
+ font-weight: 850;
12094
+ letter-spacing: 0;
12095
+ outline: none;
12096
+ pointer-events: auto;
12097
+ `;
12098
+ options.forEach(option => {
12099
+ const optionEl = document.createElement('option');
12100
+ optionEl.value = option.value;
12101
+ optionEl.textContent = option.label;
12102
+ select.appendChild(optionEl);
12103
+ });
12104
+ select.value = value;
12105
+ return select;
12106
+ }
12107
+
12108
+ function getRemoteFleetDeviceField(device, camelKey, pascalKey, fallback = '') {
12109
+ const value = device?.[camelKey] ?? device?.[pascalKey] ?? fallback;
12110
+ return value === undefined || value === null ? fallback : value;
12111
+ }
12112
+
12113
+ function getRemoteFleetDeviceId(device) {
12114
+ return String(getRemoteFleetDeviceField(device, 'deviceId', 'DeviceId', '')).trim();
12115
+ }
12116
+
12117
+ function getRemoteFleetDeviceName(device) {
12118
+ return String(
12119
+ getRemoteFleetDeviceField(device, 'name', 'Name')
12120
+ || getRemoteFleetDeviceField(device, 'hostname', 'Hostname')
12121
+ || getRemoteFleetDeviceId(device)
12122
+ || 'device');
12123
+ }
12124
+
12125
+ function isRemoteFleetDeviceConnected(device) {
12126
+ return getRemoteFleetDeviceField(device, 'connected', 'Connected', false) === true;
12127
+ }
12128
+
12129
+ function isRemoteFleetDeviceTaskCapable(device) {
12130
+ return getRemoteFleetDeviceField(device, 'computerAgentEnabled', 'ComputerAgentEnabled', false) === true;
12131
+ }
12132
+
12133
+ function isRemoteFleetDeviceAiCapable(device) {
12134
+ return getRemoteFleetDeviceField(device, 'aiAssistEnabled', 'AiAssistEnabled', false) === true;
12135
+ }
12136
+
12137
+ function hasRemoteFleetThumbnail(device) {
12138
+ const dataUrl = String(getRemoteFleetDeviceField(device, 'thumbnailDataUrl', 'ThumbnailDataUrl', ''));
12139
+ return /^data:image\/(png|jpe?g|webp|svg\+xml);base64,/i.test(dataUrl);
12140
+ }
12141
+
12142
+ function hasRemoteFleetLiveFrame(device) {
12143
+ const dataUrl = String(getRemoteFleetDeviceField(device, 'liveFrameDataUrl', 'LiveFrameDataUrl', ''));
12144
+ return /^data:image\/(png|jpe?g|webp|svg\+xml);base64,/i.test(dataUrl);
12145
+ }
12146
+
12147
+ function isRemoteFleetLiveActive(device) {
12148
+ return getRemoteFleetDeviceField(device, 'liveStreamActive', 'LiveStreamActive', false) === true;
12149
+ }
12150
+
12151
+ function getRemoteFleetTimestampMs(value) {
12152
+ const timestamp = Date.parse(String(value || ''));
12153
+ return Number.isFinite(timestamp) ? timestamp : 0;
12154
+ }
12155
+
12156
+ function getRemoteFleetDeviceLastSeenMs(device) {
12157
+ return Math.max(
12158
+ getRemoteFleetTimestampMs(getRemoteFleetDeviceField(device, 'lastSeenAt', 'LastSeenAt', '')),
12159
+ getRemoteFleetTimestampMs(getRemoteFleetDeviceField(device, 'lastStatusAt', 'LastStatusAt', '')),
12160
+ getRemoteFleetTimestampMs(getRemoteFleetDeviceField(device, 'thumbnailReceivedAt', 'ThumbnailReceivedAt', '')));
12161
+ }
12162
+
12163
+ function getRemoteFleetDeviceNumber(device, camelKey, pascalKey) {
12164
+ const number = Number(getRemoteFleetDeviceField(device, camelKey, pascalKey, Number.NaN));
12165
+ return Number.isFinite(number) ? number : Number.NaN;
12166
+ }
12167
+
12168
+ function buildRemoteFleetSearchText(device) {
12169
+ return [
12170
+ getRemoteFleetDeviceId(device),
12171
+ getRemoteFleetDeviceName(device),
12172
+ getRemoteFleetDeviceField(device, 'hostname', 'Hostname', ''),
12173
+ getRemoteFleetDeviceField(device, 'platform', 'Platform', ''),
12174
+ getRemoteFleetDeviceField(device, 'release', 'Release', ''),
12175
+ getRemoteFleetDeviceField(device, 'arch', 'Arch', ''),
12176
+ getRemoteFleetDeviceField(device, 'agentVersion', 'AgentVersion', ''),
12177
+ getRemoteFleetDeviceField(device, 'liveStreamMode', 'LiveStreamMode', ''),
12178
+ isRemoteFleetLiveActive(device) ? 'live active remote-fast' : '',
12179
+ getRemoteFleetDeviceField(device, 'latestTaskTitle', 'LatestTaskTitle', ''),
12180
+ getRemoteFleetDeviceField(device, 'latestTaskStatus', 'LatestTaskStatus', ''),
12181
+ getRemoteFleetDeviceField(device, 'latestTaskResultSummary', 'LatestTaskResultSummary', ''),
12182
+ getRemoteFleetDeviceField(device, 'latestTaskError', 'LatestTaskError', '')
12183
+ ].join(' ').toLowerCase();
12184
+ }
12185
+
12186
+ function compareRemoteFleetDevices(left, right, sortMode) {
12187
+ const connectedDelta = Number(isRemoteFleetDeviceConnected(right)) - Number(isRemoteFleetDeviceConnected(left));
12188
+ const byName = () => getRemoteFleetDeviceName(left).localeCompare(getRemoteFleetDeviceName(right))
12189
+ || getRemoteFleetDeviceId(left).localeCompare(getRemoteFleetDeviceId(right));
12190
+
12191
+ switch (sortMode) {
12192
+ case 'recent':
12193
+ return getRemoteFleetDeviceLastSeenMs(right) - getRemoteFleetDeviceLastSeenMs(left) || byName();
12194
+ case 'memory': {
12195
+ const leftMem = getRemoteFleetDeviceNumber(left, 'usedMemRatio', 'UsedMemRatio');
12196
+ const rightMem = getRemoteFleetDeviceNumber(right, 'usedMemRatio', 'UsedMemRatio');
12197
+ return (Number.isFinite(rightMem) ? rightMem : -1) - (Number.isFinite(leftMem) ? leftMem : -1) || byName();
12198
+ }
12199
+ case 'load': {
12200
+ const leftLoad = getRemoteFleetDeviceNumber(left, 'load1', 'Load1');
12201
+ const rightLoad = getRemoteFleetDeviceNumber(right, 'load1', 'Load1');
12202
+ return (Number.isFinite(rightLoad) ? rightLoad : -1) - (Number.isFinite(leftLoad) ? leftLoad : -1) || byName();
12203
+ }
12204
+ case 'task':
12205
+ return String(getRemoteFleetDeviceField(right, 'latestTaskUpdatedAt', 'LatestTaskUpdatedAt', ''))
12206
+ .localeCompare(String(getRemoteFleetDeviceField(left, 'latestTaskUpdatedAt', 'LatestTaskUpdatedAt', ''))) || byName();
12207
+ case 'status':
12208
+ return connectedDelta || byName();
12209
+ case 'name':
12210
+ default:
12211
+ return byName();
12212
+ }
12213
+ }
12214
+
12076
12215
  async function syncRemoteFleetNodeStateFromResult(result) {
12077
12216
  const nodeState = result?.nodeState || result?.refresh?.nodeState;
12078
12217
  if (nodeState && window.mindMap?.syncNodeStates) {
@@ -12082,14 +12221,34 @@
12082
12221
 
12083
12222
  function renderRemoteFleetMonitor(bodyView, nodeModel) {
12084
12223
  if (!bodyView) return;
12224
+ if (bodyView._remoteFleetLiveRefreshTimer) {
12225
+ clearInterval(bodyView._remoteFleetLiveRefreshTimer);
12226
+ bodyView._remoteFleetLiveRefreshTimer = null;
12227
+ }
12085
12228
 
12086
12229
  const nodeId = String(nodeModel?.id ?? nodeModel?.Id ?? '');
12087
- const devices = parseRemoteFleetDevices(nodeModel);
12230
+ const searchState = bodyView.dataset.remoteFleetSearch || '';
12231
+ const filterState = bodyView.dataset.remoteFleetFilter || 'all';
12232
+ const sortState = bodyView.dataset.remoteFleetSort || 'status';
12233
+ const densityState = bodyView.dataset.remoteFleetDensity || 'cards';
12234
+ const aiAssistState = bodyView.dataset.remoteFleetAiAssist === 'true';
12235
+ const focusState = bodyView.dataset.remoteFleetFocusDeviceId || '';
12236
+ const devices = [...parseRemoteFleetDevices(nodeModel)]
12237
+ .sort((left, right) => compareRemoteFleetDevices(left, right, sortState));
12238
+ const hasActiveLiveStream = devices.some(isRemoteFleetLiveActive);
12239
+ const focusedDevice = devices.find(device => getRemoteFleetDeviceId(device) === focusState)
12240
+ || devices.find(device => isRemoteFleetLiveActive(device))
12241
+ || null;
12242
+ if (focusedDevice) {
12243
+ bodyView.dataset.remoteFleetFocusDeviceId = getRemoteFleetDeviceId(focusedDevice);
12244
+ }
12088
12245
  const total = Number(getRemoteFleetMetadataValue(nodeModel, 'RemoteFleetDeviceCount', devices.length));
12089
12246
  const connected = Number(getRemoteFleetMetadataValue(
12090
12247
  nodeModel,
12091
12248
  'RemoteFleetConnectedDeviceCount',
12092
- devices.filter(device => device?.connected === true || device?.Connected === true).length));
12249
+ devices.filter(isRemoteFleetDeviceConnected).length));
12250
+ const taskCapableCount = devices.filter(device => isRemoteFleetDeviceConnected(device) && isRemoteFleetDeviceTaskCapable(device)).length;
12251
+ const aiCapableCount = devices.filter(device => isRemoteFleetDeviceConnected(device) && isRemoteFleetDeviceAiCapable(device)).length;
12093
12252
  const endpoint = getRemoteFleetMetadataValue(nodeModel, 'RemoteFleetHubEndpoint', '127.0.0.1:5197');
12094
12253
  const command = getRemoteFleetMetadataValue(
12095
12254
  nodeModel,
@@ -12123,8 +12282,8 @@
12123
12282
  `;
12124
12283
  top.appendChild(createRemoteFleetStat('Hub', hubStatus === 'online' ? 'Online' : 'Offline', hubStatus === 'online' ? 'online' : 'default'));
12125
12284
  top.appendChild(createRemoteFleetStat('Connected', `${Number.isFinite(connected) ? connected : 0}/${Number.isFinite(total) ? total : devices.length}`, connected > 0 ? 'online' : 'default'));
12126
- top.appendChild(createRemoteFleetStat('Mode', 'All', 'default'));
12127
- top.appendChild(createRemoteFleetStat('Paging', 'None', 'default'));
12285
+ top.appendChild(createRemoteFleetStat('Task', String(taskCapableCount), taskCapableCount > 0 ? 'online' : 'default'));
12286
+ top.appendChild(createRemoteFleetStat('AI', String(aiCapableCount), aiCapableCount > 0 ? 'online' : 'default'));
12128
12287
  bodyView.appendChild(top);
12129
12288
 
12130
12289
  const commandRow = document.createElement('div');
@@ -12160,10 +12319,87 @@
12160
12319
  commandRow.appendChild(refreshButton);
12161
12320
  bodyView.appendChild(commandRow);
12162
12321
 
12322
+ const filterRow = document.createElement('div');
12323
+ filterRow.style.cssText = `
12324
+ display: grid;
12325
+ grid-template-columns: minmax(150px, 1.2fr) minmax(96px, 0.7fr) minmax(96px, 0.7fr) minmax(84px, 0.55fr) auto;
12326
+ gap: 8px;
12327
+ align-items: center;
12328
+ flex: 0 0 auto;
12329
+ `;
12330
+
12331
+ const searchInput = document.createElement('input');
12332
+ searchInput.type = 'search';
12333
+ searchInput.value = searchState;
12334
+ searchInput.placeholder = 'Search devices';
12335
+ searchInput.dataset.remoteFleetSearch = 'true';
12336
+ searchInput.autocomplete = 'off';
12337
+ searchInput.spellcheck = false;
12338
+ searchInput.style.cssText = `
12339
+ min-width: 0;
12340
+ height: 34px;
12341
+ border-radius: 7px;
12342
+ border: 1px solid rgba(148, 163, 184, 0.34);
12343
+ background: rgba(255, 255, 255, 0.94);
12344
+ color: #0f172a;
12345
+ padding: 0 10px;
12346
+ font-size: 12px;
12347
+ font-weight: 750;
12348
+ letter-spacing: 0;
12349
+ outline: none;
12350
+ pointer-events: auto;
12351
+ `;
12352
+
12353
+ const filterSelect = createRemoteFleetSelect([
12354
+ { value: 'all', label: 'All' },
12355
+ { value: 'connected', label: 'Online' },
12356
+ { value: 'offline', label: 'Offline' },
12357
+ { value: 'task', label: 'Task' },
12358
+ { value: 'ai', label: 'AI' },
12359
+ { value: 'live', label: 'Live' },
12360
+ { value: 'frames', label: 'Frames' },
12361
+ { value: 'issues', label: 'Issues' }
12362
+ ], filterState, 'Device filter');
12363
+
12364
+ const sortSelect = createRemoteFleetSelect([
12365
+ { value: 'status', label: 'Status' },
12366
+ { value: 'name', label: 'Name' },
12367
+ { value: 'recent', label: 'Recent' },
12368
+ { value: 'memory', label: 'Memory' },
12369
+ { value: 'load', label: 'Load' },
12370
+ { value: 'task', label: 'Task' }
12371
+ ], sortState, 'Device sort');
12372
+
12373
+ const densitySelect = createRemoteFleetSelect([
12374
+ { value: 'cards', label: 'Cards' },
12375
+ { value: 'dense', label: 'Dense' }
12376
+ ], densityState, 'Device density');
12377
+
12378
+ const matchCount = document.createElement('div');
12379
+ matchCount.dataset.remoteFleetMatchCount = 'true';
12380
+ matchCount.style.cssText = `
12381
+ min-width: 72px;
12382
+ color: #334155;
12383
+ font-size: 11px;
12384
+ font-weight: 900;
12385
+ letter-spacing: 0;
12386
+ text-align: right;
12387
+ white-space: nowrap;
12388
+ overflow: hidden;
12389
+ text-overflow: ellipsis;
12390
+ `;
12391
+
12392
+ filterRow.appendChild(searchInput);
12393
+ filterRow.appendChild(filterSelect);
12394
+ filterRow.appendChild(sortSelect);
12395
+ filterRow.appendChild(densitySelect);
12396
+ filterRow.appendChild(matchCount);
12397
+ bodyView.appendChild(filterRow);
12398
+
12163
12399
  const taskRow = document.createElement('div');
12164
12400
  taskRow.style.cssText = `
12165
12401
  display: grid;
12166
- grid-template-columns: minmax(0, 1fr) auto auto;
12402
+ grid-template-columns: minmax(0, 1fr) auto auto auto;
12167
12403
  gap: 8px;
12168
12404
  align-items: stretch;
12169
12405
  flex: 0 0 auto;
@@ -12212,6 +12448,7 @@
12212
12448
  `;
12213
12449
  const aiToggle = document.createElement('input');
12214
12450
  aiToggle.type = 'checkbox';
12451
+ aiToggle.checked = aiAssistState;
12215
12452
  aiToggle.dataset.remoteFleetAiToggle = 'true';
12216
12453
  aiToggle.style.cssText = `
12217
12454
  width: 14px;
@@ -12223,10 +12460,13 @@
12223
12460
  aiToggleText.textContent = 'AI';
12224
12461
  aiToggleLabel.appendChild(aiToggle);
12225
12462
  aiToggleLabel.appendChild(aiToggleText);
12226
- const sendConnectedButton = createRemoteFleetButton('Send connected', 'Dispatch task to all connected task-capable devices', 'task-connected');
12463
+ const sendVisibleButton = createRemoteFleetButton('Send visible', 'Dispatch task to visible connected task-capable devices', 'task-visible');
12464
+ sendVisibleButton.style.height = '54px';
12465
+ const sendConnectedButton = createRemoteFleetButton('Send all', 'Dispatch task to all connected task-capable devices', 'task-connected');
12227
12466
  sendConnectedButton.style.height = '54px';
12228
12467
  taskRow.appendChild(taskInput);
12229
12468
  taskRow.appendChild(aiToggleLabel);
12469
+ taskRow.appendChild(sendVisibleButton);
12230
12470
  taskRow.appendChild(sendConnectedButton);
12231
12471
  bodyView.appendChild(taskRow);
12232
12472
 
@@ -12249,6 +12489,141 @@
12249
12489
  `;
12250
12490
  bodyView.appendChild(taskFeedback);
12251
12491
 
12492
+ if (focusedDevice) {
12493
+ const focusedId = getRemoteFleetDeviceId(focusedDevice);
12494
+ const focusedName = getRemoteFleetDeviceName(focusedDevice);
12495
+ const liveActive = isRemoteFleetLiveActive(focusedDevice);
12496
+ const liveFrameDataUrl = String(getRemoteFleetDeviceField(focusedDevice, 'liveFrameDataUrl', 'LiveFrameDataUrl', ''));
12497
+ const liveFrameAt = String(getRemoteFleetDeviceField(focusedDevice, 'liveFrameReceivedAt', 'LiveFrameReceivedAt', ''));
12498
+ const liveStreamId = String(getRemoteFleetDeviceField(focusedDevice, 'liveStreamId', 'LiveStreamId', ''));
12499
+ const hasLiveFrame = hasRemoteFleetLiveFrame(focusedDevice);
12500
+ const livePanel = document.createElement('section');
12501
+ livePanel.dataset.remoteFleetLivePanel = 'true';
12502
+ livePanel.dataset.deviceId = focusedId;
12503
+ livePanel.style.cssText = `
12504
+ flex: 0 0 auto;
12505
+ display: grid;
12506
+ grid-template-columns: minmax(220px, 0.95fr) minmax(0, 1.25fr);
12507
+ gap: 10px;
12508
+ min-height: 170px;
12509
+ padding: 10px;
12510
+ border-radius: 8px;
12511
+ border: 1px solid rgba(14, 165, 233, 0.24);
12512
+ background: rgba(255, 255, 255, 0.82);
12513
+ `;
12514
+
12515
+ const livePreview = document.createElement('div');
12516
+ livePreview.style.cssText = `
12517
+ position: relative;
12518
+ min-width: 0;
12519
+ aspect-ratio: 16 / 9;
12520
+ overflow: hidden;
12521
+ border-radius: 7px;
12522
+ background: linear-gradient(135deg, #111827 0%, #1f2937 100%);
12523
+ border: 1px solid rgba(15, 23, 42, 0.16);
12524
+ `;
12525
+ if (hasLiveFrame) {
12526
+ const image = document.createElement('img');
12527
+ image.src = liveFrameDataUrl;
12528
+ image.alt = `${focusedName} live screen`;
12529
+ image.decoding = 'async';
12530
+ image.style.cssText = `
12531
+ width: 100%;
12532
+ height: 100%;
12533
+ object-fit: cover;
12534
+ display: block;
12535
+ `;
12536
+ livePreview.appendChild(image);
12537
+ } else {
12538
+ const placeholder = document.createElement('div');
12539
+ placeholder.textContent = liveActive ? 'Waiting for live frame' : 'Live view idle';
12540
+ placeholder.style.cssText = `
12541
+ position: absolute;
12542
+ inset: 0;
12543
+ display: flex;
12544
+ align-items: center;
12545
+ justify-content: center;
12546
+ color: rgba(226, 232, 240, 0.82);
12547
+ font-size: 12px;
12548
+ font-weight: 900;
12549
+ letter-spacing: 0;
12550
+ `;
12551
+ livePreview.appendChild(placeholder);
12552
+ }
12553
+ const liveBadge = document.createElement('span');
12554
+ liveBadge.textContent = liveActive
12555
+ ? `LIVE ${liveFrameAt ? formatRemoteFleetAge(liveFrameAt) : ''}`.trim()
12556
+ : 'FOCUS';
12557
+ liveBadge.style.cssText = `
12558
+ position: absolute;
12559
+ left: 7px;
12560
+ top: 7px;
12561
+ max-width: calc(100% - 14px);
12562
+ padding: 4px 7px;
12563
+ border-radius: 999px;
12564
+ background: ${liveActive ? 'rgba(220, 38, 38, 0.86)' : 'rgba(15, 23, 42, 0.72)'};
12565
+ color: #ffffff;
12566
+ font-size: 9px;
12567
+ font-weight: 950;
12568
+ line-height: 1;
12569
+ overflow: hidden;
12570
+ text-overflow: ellipsis;
12571
+ white-space: nowrap;
12572
+ letter-spacing: 0;
12573
+ `;
12574
+ livePreview.appendChild(liveBadge);
12575
+
12576
+ const liveInfo = document.createElement('div');
12577
+ liveInfo.style.cssText = `
12578
+ min-width: 0;
12579
+ display: flex;
12580
+ flex-direction: column;
12581
+ justify-content: space-between;
12582
+ gap: 8px;
12583
+ `;
12584
+ const liveTitle = document.createElement('div');
12585
+ liveTitle.style.cssText = 'min-width:0;display:flex;flex-direction:column;gap:3px;';
12586
+ const liveName = document.createElement('strong');
12587
+ liveName.textContent = focusedName;
12588
+ liveName.title = focusedName;
12589
+ 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;';
12590
+ const liveMeta = document.createElement('span');
12591
+ liveMeta.textContent = liveActive
12592
+ ? `RemoteFast ${getRemoteFleetDeviceField(focusedDevice, 'liveStreamFps', 'LiveStreamFps', 0) || 12} fps`
12593
+ : 'View-only focused screen stream';
12594
+ 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;';
12595
+ liveTitle.appendChild(liveName);
12596
+ liveTitle.appendChild(liveMeta);
12597
+
12598
+ const liveActions = document.createElement('div');
12599
+ liveActions.style.cssText = 'display:flex;align-items:center;gap:8px;flex-wrap:wrap;';
12600
+ if (isRemoteFleetDeviceConnected(focusedDevice) && getRemoteFleetDeviceField(focusedDevice, 'liveStreamEnabled', 'LiveStreamEnabled', false) === true) {
12601
+ const startLiveButton = createRemoteFleetButton(liveActive ? 'Restart live' : 'Start live', 'Start focused view-only live stream', 'live-start');
12602
+ startLiveButton.dataset.deviceId = focusedId;
12603
+ startLiveButton.style.height = '30px';
12604
+ liveActions.appendChild(startLiveButton);
12605
+ if (liveActive) {
12606
+ const stopLiveButton = createRemoteFleetButton('Stop live', 'Stop focused live stream', 'live-stop');
12607
+ stopLiveButton.dataset.deviceId = focusedId;
12608
+ stopLiveButton.dataset.streamId = liveStreamId;
12609
+ stopLiveButton.style.height = '30px';
12610
+ stopLiveButton.style.borderColor = 'rgba(220, 38, 38, 0.32)';
12611
+ stopLiveButton.style.color = '#b91c1c';
12612
+ liveActions.appendChild(stopLiveButton);
12613
+ }
12614
+ } else {
12615
+ const disabled = document.createElement('span');
12616
+ disabled.textContent = 'Live unavailable';
12617
+ disabled.style.cssText = 'color:#64748b;font-size:11px;font-weight:900;';
12618
+ liveActions.appendChild(disabled);
12619
+ }
12620
+ liveInfo.appendChild(liveTitle);
12621
+ liveInfo.appendChild(liveActions);
12622
+ livePanel.appendChild(livePreview);
12623
+ livePanel.appendChild(liveInfo);
12624
+ bodyView.appendChild(livePanel);
12625
+ }
12626
+
12252
12627
  if (lastError.trim()) {
12253
12628
  const errorEl = document.createElement('div');
12254
12629
  errorEl.textContent = lastError;
@@ -12273,7 +12648,7 @@
12273
12648
  overflow-y: auto;
12274
12649
  overflow-x: hidden;
12275
12650
  display: grid;
12276
- grid-template-columns: repeat(auto-fill, minmax(168px, 1fr));
12651
+ grid-template-columns: ${densityState === 'dense' ? 'repeat(auto-fill, minmax(220px, 1fr))' : 'repeat(auto-fill, minmax(168px, 1fr))'};
12277
12652
  align-content: start;
12278
12653
  gap: 8px;
12279
12654
  padding-right: 4px;
@@ -12298,19 +12673,25 @@
12298
12673
  grid.appendChild(empty);
12299
12674
  } else {
12300
12675
  devices.forEach(device => {
12301
- const connectedDevice = device?.connected === true || device?.Connected === true;
12302
- const name = String(device?.name || device?.Name || device?.hostname || device?.Hostname || device?.deviceId || device?.DeviceId || 'device');
12676
+ const connectedDevice = isRemoteFleetDeviceConnected(device);
12677
+ const name = getRemoteFleetDeviceName(device);
12303
12678
  const platform = [device?.platform || device?.Platform, device?.arch || device?.Arch]
12304
12679
  .filter(Boolean)
12305
12680
  .join(' / ') || 'unknown';
12306
12681
  const release = String(device?.release || device?.Release || '');
12307
- const deviceId = String(device?.deviceId || device?.DeviceId || '');
12682
+ const deviceId = getRemoteFleetDeviceId(device);
12308
12683
  const thumbnailEnabled = device?.thumbnailEnabled === true || device?.ThumbnailEnabled === true;
12309
12684
  const thumbnailDataUrl = String(device?.thumbnailDataUrl || device?.ThumbnailDataUrl || '');
12310
12685
  const thumbnailCapturedAt = String(device?.thumbnailCapturedAt || device?.ThumbnailCapturedAt || '');
12311
- const hasThumbnail = /^data:image\/(png|jpe?g|webp|svg\+xml);base64,/i.test(thumbnailDataUrl);
12312
- const taskEnabled = device?.computerAgentEnabled === true || device?.ComputerAgentEnabled === true;
12313
- const aiAssistEnabled = device?.aiAssistEnabled === true || device?.AiAssistEnabled === true;
12686
+ const liveStreamEnabled = device?.liveStreamEnabled === true || device?.LiveStreamEnabled === true;
12687
+ const liveStreamActive = isRemoteFleetLiveActive(device);
12688
+ const liveStreamId = String(device?.liveStreamId || device?.LiveStreamId || '');
12689
+ const liveFrameDataUrl = String(device?.liveFrameDataUrl || device?.LiveFrameDataUrl || '');
12690
+ const liveFrameReceivedAt = String(device?.liveFrameReceivedAt || device?.LiveFrameReceivedAt || '');
12691
+ const hasThumbnail = hasRemoteFleetThumbnail(device);
12692
+ const hasLiveFrame = hasRemoteFleetLiveFrame(device);
12693
+ const taskEnabled = isRemoteFleetDeviceTaskCapable(device);
12694
+ const aiAssistEnabled = isRemoteFleetDeviceAiCapable(device);
12314
12695
  const aiModel = String(device?.aiModel || device?.AiModel || '');
12315
12696
  const latestTaskStatus = String(device?.latestTaskStatus || device?.LatestTaskStatus || '');
12316
12697
  const latestTaskTitle = String(device?.latestTaskTitle || device?.LatestTaskTitle || '');
@@ -12320,80 +12701,94 @@
12320
12701
  const latestTaskResult = String(device?.latestTaskResultSummary || device?.LatestTaskResultSummary || '');
12321
12702
  const card = document.createElement('article');
12322
12703
  card.dataset.deviceId = deviceId;
12704
+ card.dataset.remoteFleetSearchText = buildRemoteFleetSearchText(device);
12705
+ card.dataset.remoteFleetConnected = connectedDevice ? 'true' : 'false';
12706
+ card.dataset.remoteFleetTaskCapable = taskEnabled ? 'true' : 'false';
12707
+ card.dataset.remoteFleetAiCapable = aiAssistEnabled ? 'true' : 'false';
12708
+ card.dataset.remoteFleetHasFrame = (hasThumbnail || hasLiveFrame) ? 'true' : 'false';
12709
+ card.dataset.remoteFleetLiveCapable = liveStreamEnabled ? 'true' : 'false';
12710
+ card.dataset.remoteFleetLiveActive = liveStreamActive ? 'true' : 'false';
12711
+ card.dataset.remoteFleetIssue = (!connectedDevice || latestTaskStatus === 'failed' || !!latestTaskError) ? 'true' : 'false';
12323
12712
  card.style.cssText = `
12324
12713
  display: flex;
12325
12714
  flex-direction: column;
12326
- gap: 8px;
12715
+ gap: ${densityState === 'dense' ? '6px' : '8px'};
12327
12716
  min-width: 0;
12328
- min-height: 134px;
12329
- padding: 10px;
12717
+ min-height: ${densityState === 'dense' ? '92px' : '134px'};
12718
+ padding: ${densityState === 'dense' ? '8px' : '10px'};
12330
12719
  border-radius: 8px;
12331
12720
  background: #ffffff;
12332
12721
  border: 1px solid ${connectedDevice ? 'rgba(16, 185, 129, 0.34)' : 'rgba(148, 163, 184, 0.28)'};
12333
12722
  box-shadow: 0 8px 20px rgba(15, 23, 42, 0.06);
12334
12723
  `;
12335
12724
 
12336
- const preview = document.createElement('div');
12337
- preview.style.cssText = `
12338
- position: relative;
12339
- width: 100%;
12340
- aspect-ratio: 16 / 9;
12341
- overflow: hidden;
12342
- border-radius: 7px;
12343
- background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%);
12344
- border: 1px solid rgba(15, 23, 42, 0.12);
12345
- `;
12346
- if (hasThumbnail) {
12347
- const image = document.createElement('img');
12348
- image.src = thumbnailDataUrl;
12349
- image.alt = `${name} thumbnail`;
12350
- image.loading = 'lazy';
12351
- image.decoding = 'async';
12352
- image.style.cssText = `
12725
+ if (densityState !== 'dense') {
12726
+ const previewDataUrl = hasLiveFrame ? liveFrameDataUrl : thumbnailDataUrl;
12727
+ const previewAt = hasLiveFrame ? liveFrameReceivedAt : thumbnailCapturedAt;
12728
+ const preview = document.createElement('div');
12729
+ preview.style.cssText = `
12730
+ position: relative;
12353
12731
  width: 100%;
12354
- height: 100%;
12355
- object-fit: cover;
12356
- display: block;
12357
- `;
12358
- preview.appendChild(image);
12359
- } else {
12360
- const placeholder = document.createElement('div');
12361
- placeholder.textContent = thumbnailEnabled ? 'No frame yet' : 'Status only';
12362
- placeholder.style.cssText = `
12363
- position: absolute;
12364
- inset: 0;
12365
- display: flex;
12366
- align-items: center;
12367
- justify-content: center;
12368
- color: rgba(226, 232, 240, 0.78);
12369
- font-size: 11px;
12370
- font-weight: 900;
12371
- letter-spacing: 0;
12372
- `;
12373
- preview.appendChild(placeholder);
12374
- }
12375
- if (thumbnailCapturedAt) {
12376
- const badge = document.createElement('span');
12377
- badge.textContent = formatRemoteFleetAge(thumbnailCapturedAt);
12378
- badge.style.cssText = `
12379
- position: absolute;
12380
- right: 6px;
12381
- bottom: 6px;
12382
- max-width: calc(100% - 12px);
12383
- padding: 3px 6px;
12384
- border-radius: 999px;
12385
- background: rgba(15, 23, 42, 0.74);
12386
- color: #e2e8f0;
12387
- font-size: 9px;
12388
- font-weight: 900;
12389
- line-height: 1;
12732
+ aspect-ratio: 16 / 9;
12390
12733
  overflow: hidden;
12391
- text-overflow: ellipsis;
12392
- white-space: nowrap;
12734
+ border-radius: 7px;
12735
+ background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%);
12736
+ border: 1px solid rgba(15, 23, 42, 0.12);
12393
12737
  `;
12394
- preview.appendChild(badge);
12738
+ if (hasLiveFrame || hasThumbnail) {
12739
+ const image = document.createElement('img');
12740
+ image.src = previewDataUrl;
12741
+ image.alt = hasLiveFrame ? `${name} live frame` : `${name} thumbnail`;
12742
+ image.loading = 'lazy';
12743
+ image.decoding = 'async';
12744
+ image.style.cssText = `
12745
+ width: 100%;
12746
+ height: 100%;
12747
+ object-fit: cover;
12748
+ display: block;
12749
+ `;
12750
+ preview.appendChild(image);
12751
+ } else {
12752
+ const placeholder = document.createElement('div');
12753
+ placeholder.textContent = thumbnailEnabled ? 'No frame yet' : 'Status only';
12754
+ placeholder.style.cssText = `
12755
+ position: absolute;
12756
+ inset: 0;
12757
+ display: flex;
12758
+ align-items: center;
12759
+ justify-content: center;
12760
+ color: rgba(226, 232, 240, 0.78);
12761
+ font-size: 11px;
12762
+ font-weight: 900;
12763
+ letter-spacing: 0;
12764
+ `;
12765
+ preview.appendChild(placeholder);
12766
+ }
12767
+ if (previewAt) {
12768
+ const badge = document.createElement('span');
12769
+ badge.textContent = hasLiveFrame
12770
+ ? `LIVE ${formatRemoteFleetAge(previewAt)}`
12771
+ : formatRemoteFleetAge(previewAt);
12772
+ badge.style.cssText = `
12773
+ position: absolute;
12774
+ right: 6px;
12775
+ bottom: 6px;
12776
+ max-width: calc(100% - 12px);
12777
+ padding: 3px 6px;
12778
+ border-radius: 999px;
12779
+ background: ${hasLiveFrame ? 'rgba(220, 38, 38, 0.82)' : 'rgba(15, 23, 42, 0.74)'};
12780
+ color: #e2e8f0;
12781
+ font-size: 9px;
12782
+ font-weight: 900;
12783
+ line-height: 1;
12784
+ overflow: hidden;
12785
+ text-overflow: ellipsis;
12786
+ white-space: nowrap;
12787
+ `;
12788
+ preview.appendChild(badge);
12789
+ }
12790
+ card.appendChild(preview);
12395
12791
  }
12396
- card.appendChild(preview);
12397
12792
 
12398
12793
  const cardHeader = document.createElement('div');
12399
12794
  cardHeader.style.cssText = 'display:flex;align-items:flex-start;gap:8px;min-width:0;';
@@ -12470,7 +12865,23 @@
12470
12865
  addMetric('Uptime', formatRemoteFleetDuration(device?.uptimeSec ?? device?.UptimeSec));
12471
12866
  addMetric('Mem', formatRemoteFleetPercent(device?.usedMemRatio ?? device?.UsedMemRatio));
12472
12867
  addMetric('Load', formatRemoteFleetNumber(device?.load1 ?? device?.Load1, 2));
12473
- card.appendChild(metrics);
12868
+ if (densityState === 'dense') {
12869
+ const denseMeta = document.createElement('div');
12870
+ denseMeta.textContent = `Seen ${formatRemoteFleetAge(device?.lastSeenAt || device?.LastSeenAt)} - Mem ${formatRemoteFleetPercent(device?.usedMemRatio ?? device?.UsedMemRatio)} - Load ${formatRemoteFleetNumber(device?.load1 ?? device?.Load1, 2)}`;
12871
+ denseMeta.style.cssText = `
12872
+ color: #475569;
12873
+ font-size: 10px;
12874
+ font-weight: 750;
12875
+ line-height: 1.2;
12876
+ overflow: hidden;
12877
+ text-overflow: ellipsis;
12878
+ white-space: nowrap;
12879
+ letter-spacing: 0;
12880
+ `;
12881
+ card.appendChild(denseMeta);
12882
+ } else {
12883
+ card.appendChild(metrics);
12884
+ }
12474
12885
 
12475
12886
  if (latestTaskStatus || latestTaskTitle || latestTaskResult || latestTaskError) {
12476
12887
  const taskBox = document.createElement('div');
@@ -12523,11 +12934,11 @@
12523
12934
  actions.style.cssText = 'display:flex;align-items:center;justify-content:space-between;gap:8px;margin-top:auto;';
12524
12935
  const status = document.createElement('span');
12525
12936
  status.textContent = connectedDevice
12526
- ? (aiAssistEnabled ? `AI ${aiModel || 'ready'}` : 'Connected')
12937
+ ? (liveStreamActive ? 'Live' : (aiAssistEnabled ? `AI ${aiModel || 'ready'}` : 'Connected'))
12527
12938
  : 'Offline';
12528
12939
  status.style.cssText = `
12529
12940
  min-width: 0;
12530
- color: ${connectedDevice ? '#047857' : '#64748b'};
12941
+ color: ${liveStreamActive ? '#b91c1c' : (connectedDevice ? '#047857' : '#64748b')};
12531
12942
  font-size: 11px;
12532
12943
  font-weight: 900;
12533
12944
  overflow: hidden;
@@ -12536,6 +12947,23 @@
12536
12947
  `;
12537
12948
  actions.appendChild(status);
12538
12949
  if (connectedDevice && deviceId) {
12950
+ const focusButton = createRemoteFleetButton('Focus', 'Show this device in focused live panel', 'live-focus');
12951
+ focusButton.dataset.deviceId = deviceId;
12952
+ focusButton.style.height = '24px';
12953
+ focusButton.style.fontSize = '10px';
12954
+ actions.appendChild(focusButton);
12955
+ if (liveStreamEnabled) {
12956
+ const liveButton = createRemoteFleetButton(liveStreamActive ? 'Stop' : 'Live', liveStreamActive ? 'Stop live stream' : 'Start focused live stream', liveStreamActive ? 'live-stop' : 'live-start');
12957
+ liveButton.dataset.deviceId = deviceId;
12958
+ liveButton.dataset.streamId = liveStreamId;
12959
+ liveButton.style.height = '24px';
12960
+ liveButton.style.fontSize = '10px';
12961
+ if (liveStreamActive) {
12962
+ liveButton.style.borderColor = 'rgba(220, 38, 38, 0.32)';
12963
+ liveButton.style.color = '#b91c1c';
12964
+ }
12965
+ actions.appendChild(liveButton);
12966
+ }
12539
12967
  if (taskEnabled) {
12540
12968
  const taskButton = createRemoteFleetButton('Task', 'Dispatch task to this device', 'task-device');
12541
12969
  taskButton.dataset.deviceId = deviceId;
@@ -12559,12 +12987,31 @@
12559
12987
  card.appendChild(actions);
12560
12988
  grid.appendChild(card);
12561
12989
  });
12990
+
12991
+ const noMatch = document.createElement('div');
12992
+ noMatch.dataset.remoteFleetNoMatch = 'true';
12993
+ noMatch.textContent = 'No matching devices.';
12994
+ noMatch.style.cssText = `
12995
+ grid-column: 1 / -1;
12996
+ display: none;
12997
+ align-items: center;
12998
+ min-height: 74px;
12999
+ padding: 14px;
13000
+ border-radius: 8px;
13001
+ border: 1px dashed rgba(100, 116, 139, 0.36);
13002
+ color: #475569;
13003
+ font-size: 13px;
13004
+ font-weight: 850;
13005
+ background: rgba(255, 255, 255, 0.74);
13006
+ `;
13007
+ grid.appendChild(noMatch);
12562
13008
  }
12563
13009
 
12564
13010
  bodyView.appendChild(grid);
12565
13011
 
12566
13012
  const footer = document.createElement('div');
12567
13013
  footer.textContent = `Endpoint ${endpoint} · refreshed ${formatRemoteFleetAge(refreshedAt)}`;
13014
+ footer.textContent = `Endpoint ${endpoint} - all devices, no paging - refreshed ${formatRemoteFleetAge(refreshedAt)}`;
12568
13015
  footer.style.cssText = `
12569
13016
  flex: 0 0 auto;
12570
13017
  color: #64748b;
@@ -12600,6 +13047,128 @@
12600
13047
 
12601
13048
  const readTaskInstruction = () => String(taskInput.value || '').trim();
12602
13049
  const useAiAssist = () => aiToggle.checked === true;
13050
+ const getDeviceCards = () => Array.from(grid.querySelectorAll('article[data-device-id]'));
13051
+ const getVisibleEligibleDeviceIds = () => {
13052
+ const wantsAi = useAiAssist();
13053
+ return getDeviceCards()
13054
+ .filter(card => card.style.display !== 'none'
13055
+ && card.dataset.remoteFleetConnected === 'true'
13056
+ && card.dataset.remoteFleetTaskCapable === 'true'
13057
+ && (!wantsAi || card.dataset.remoteFleetAiCapable === 'true'))
13058
+ .map(card => String(card.dataset.deviceId || '').trim())
13059
+ .filter(Boolean);
13060
+ };
13061
+
13062
+ const applyRemoteFleetFilters = () => {
13063
+ const searchText = String(searchInput.value || '').trim().toLowerCase();
13064
+ const terms = searchText.split(/\s+/).filter(Boolean);
13065
+ const filterMode = String(filterSelect.value || 'all');
13066
+ let visible = 0;
13067
+ let eligible = 0;
13068
+ const wantsAi = useAiAssist();
13069
+
13070
+ getDeviceCards().forEach(card => {
13071
+ const matchesSearch = terms.length === 0
13072
+ || terms.every(term => String(card.dataset.remoteFleetSearchText || '').includes(term));
13073
+ const matchesMode = filterMode === 'all'
13074
+ || (filterMode === 'connected' && card.dataset.remoteFleetConnected === 'true')
13075
+ || (filterMode === 'offline' && card.dataset.remoteFleetConnected !== 'true')
13076
+ || (filterMode === 'task' && card.dataset.remoteFleetConnected === 'true' && card.dataset.remoteFleetTaskCapable === 'true')
13077
+ || (filterMode === 'ai' && card.dataset.remoteFleetConnected === 'true' && card.dataset.remoteFleetAiCapable === 'true')
13078
+ || (filterMode === 'live' && card.dataset.remoteFleetLiveActive === 'true')
13079
+ || (filterMode === 'frames' && card.dataset.remoteFleetHasFrame === 'true')
13080
+ || (filterMode === 'issues' && card.dataset.remoteFleetIssue === 'true');
13081
+ const show = matchesSearch && matchesMode;
13082
+ card.style.display = show ? 'flex' : 'none';
13083
+ if (show) {
13084
+ visible += 1;
13085
+ if (card.dataset.remoteFleetConnected === 'true'
13086
+ && card.dataset.remoteFleetTaskCapable === 'true'
13087
+ && (!wantsAi || card.dataset.remoteFleetAiCapable === 'true')) {
13088
+ eligible += 1;
13089
+ }
13090
+ }
13091
+ });
13092
+
13093
+ const noMatch = grid.querySelector('[data-remote-fleet-no-match="true"]');
13094
+ if (noMatch) {
13095
+ noMatch.style.display = devices.length > 0 && visible === 0 ? 'flex' : 'none';
13096
+ }
13097
+
13098
+ matchCount.textContent = `${visible}/${devices.length}`;
13099
+ matchCount.title = `${visible} visible, ${eligible} targetable`;
13100
+ sendVisibleButton.disabled = eligible <= 0;
13101
+ sendVisibleButton.title = eligible > 0
13102
+ ? `Dispatch to ${eligible} visible target(s)`
13103
+ : 'No visible targetable devices';
13104
+ const allTargetCount = wantsAi ? aiCapableCount : taskCapableCount;
13105
+ sendConnectedButton.disabled = allTargetCount <= 0;
13106
+ sendConnectedButton.title = allTargetCount > 0
13107
+ ? `Dispatch to ${allTargetCount} connected target(s)`
13108
+ : 'No connected targetable devices';
13109
+ grid.querySelectorAll('[data-remote-fleet-action="task-device"]').forEach(button => {
13110
+ const card = button.closest('article[data-device-id]');
13111
+ button.disabled = wantsAi && card?.dataset.remoteFleetAiCapable !== 'true';
13112
+ button.title = button.disabled ? 'AI assist is not enabled on this device' : 'Dispatch task to this device';
13113
+ });
13114
+
13115
+ bodyView.dataset.remoteFleetSearch = searchText;
13116
+ bodyView.dataset.remoteFleetFilter = filterMode;
13117
+ bodyView.dataset.remoteFleetSort = String(sortSelect.value || 'status');
13118
+ bodyView.dataset.remoteFleetDensity = String(densitySelect.value || 'cards');
13119
+ bodyView.dataset.remoteFleetAiAssist = wantsAi ? 'true' : 'false';
13120
+ };
13121
+
13122
+ [searchInput, filterSelect, sortSelect, densitySelect, aiToggle].forEach(control => {
13123
+ ['mousedown', 'mouseup', 'click', 'dblclick', 'keydown'].forEach(eventName => {
13124
+ control.addEventListener(eventName, event => event.stopPropagation());
13125
+ });
13126
+ });
13127
+
13128
+ searchInput.addEventListener('input', applyRemoteFleetFilters);
13129
+ filterSelect.addEventListener('change', applyRemoteFleetFilters);
13130
+ aiToggle.addEventListener('change', applyRemoteFleetFilters);
13131
+ sortSelect.addEventListener('change', () => {
13132
+ bodyView.dataset.remoteFleetSort = String(sortSelect.value || 'status');
13133
+ renderRemoteFleetMonitor(bodyView, nodeModel);
13134
+ });
13135
+ densitySelect.addEventListener('change', () => {
13136
+ bodyView.dataset.remoteFleetDensity = String(densitySelect.value || 'cards');
13137
+ renderRemoteFleetMonitor(bodyView, nodeModel);
13138
+ });
13139
+ applyRemoteFleetFilters();
13140
+
13141
+ sendVisibleButton.addEventListener('click', async event => {
13142
+ event.preventDefault();
13143
+ event.stopPropagation();
13144
+ const instruction = readTaskInstruction();
13145
+ if (!instruction) {
13146
+ setTaskFeedback('Write a task first.', 'error');
13147
+ taskInput.focus();
13148
+ return;
13149
+ }
13150
+
13151
+ const targetIds = getVisibleEligibleDeviceIds();
13152
+ if (targetIds.length === 0) {
13153
+ setTaskFeedback('No visible targetable devices.', 'error');
13154
+ return;
13155
+ }
13156
+
13157
+ sendVisibleButton.disabled = true;
13158
+ setTaskFeedback('Dispatching task to visible devices...');
13159
+ try {
13160
+ const result = await invokeDotNetAsync('DispatchRemoteFleetTaskBatchFromJs', nodeId, targetIds, instruction, useAiAssist());
13161
+ await syncRemoteFleetNodeStateFromResult(result);
13162
+ if (result?.success) {
13163
+ const mode = useAiAssist() ? 'AI task' : 'remote task';
13164
+ setTaskFeedback(`Queued ${result.queued || targetIds.length} visible ${mode}(s).`, 'success');
13165
+ } else {
13166
+ setTaskFeedback(result?.error || 'Task dispatch failed.', 'error');
13167
+ }
13168
+ } finally {
13169
+ applyRemoteFleetFilters();
13170
+ }
13171
+ });
12603
13172
 
12604
13173
  sendConnectedButton.addEventListener('click', async event => {
12605
13174
  event.preventDefault();
@@ -12623,7 +13192,7 @@
12623
13192
  setTaskFeedback(result?.error || 'Task dispatch failed.', 'error');
12624
13193
  }
12625
13194
  } finally {
12626
- sendConnectedButton.disabled = false;
13195
+ applyRemoteFleetFilters();
12627
13196
  }
12628
13197
  });
12629
13198
 
@@ -12636,6 +13205,62 @@
12636
13205
  refreshButton.disabled = false;
12637
13206
  });
12638
13207
 
13208
+ bodyView.querySelectorAll('[data-remote-fleet-action="live-focus"]').forEach(button => {
13209
+ button.addEventListener('click', event => {
13210
+ event.preventDefault();
13211
+ event.stopPropagation();
13212
+ const deviceId = String(button.dataset.deviceId || '').trim();
13213
+ if (!deviceId) return;
13214
+ bodyView.dataset.remoteFleetFocusDeviceId = deviceId;
13215
+ renderRemoteFleetMonitor(bodyView, nodeModel);
13216
+ });
13217
+ });
13218
+
13219
+ bodyView.querySelectorAll('[data-remote-fleet-action="live-start"]').forEach(button => {
13220
+ button.addEventListener('click', async event => {
13221
+ event.preventDefault();
13222
+ event.stopPropagation();
13223
+ const deviceId = String(button.dataset.deviceId || '').trim();
13224
+ if (!deviceId) return;
13225
+ bodyView.dataset.remoteFleetFocusDeviceId = deviceId;
13226
+ button.disabled = true;
13227
+ setTaskFeedback('Starting focused live stream...');
13228
+ try {
13229
+ const result = await invokeDotNetAsync('StartRemoteFleetLiveStreamFromJs', nodeId, deviceId);
13230
+ await syncRemoteFleetNodeStateFromResult(result);
13231
+ if (result?.success) {
13232
+ setTaskFeedback('Focused live stream started.', 'success');
13233
+ } else {
13234
+ setTaskFeedback(result?.error || 'Live stream start failed.', 'error');
13235
+ }
13236
+ } finally {
13237
+ button.disabled = false;
13238
+ }
13239
+ });
13240
+ });
13241
+
13242
+ bodyView.querySelectorAll('[data-remote-fleet-action="live-stop"]').forEach(button => {
13243
+ button.addEventListener('click', async event => {
13244
+ event.preventDefault();
13245
+ event.stopPropagation();
13246
+ const deviceId = String(button.dataset.deviceId || '').trim();
13247
+ if (!deviceId) return;
13248
+ button.disabled = true;
13249
+ setTaskFeedback('Stopping focused live stream...');
13250
+ try {
13251
+ const result = await invokeDotNetAsync('StopRemoteFleetLiveStreamFromJs', nodeId, deviceId, button.dataset.streamId || '');
13252
+ await syncRemoteFleetNodeStateFromResult(result);
13253
+ if (result?.success) {
13254
+ setTaskFeedback('Focused live stream stopped.', 'success');
13255
+ } else {
13256
+ setTaskFeedback(result?.error || 'Live stream stop failed.', 'error');
13257
+ }
13258
+ } finally {
13259
+ button.disabled = false;
13260
+ }
13261
+ });
13262
+ });
13263
+
12639
13264
  grid.querySelectorAll('[data-remote-fleet-action="ping-device"]').forEach(button => {
12640
13265
  button.addEventListener('click', async event => {
12641
13266
  event.preventDefault();
@@ -12670,6 +13295,7 @@
12670
13295
  }
12671
13296
  } finally {
12672
13297
  button.disabled = false;
13298
+ applyRemoteFleetFilters();
12673
13299
  }
12674
13300
  });
12675
13301
  });
@@ -12684,6 +13310,24 @@
12684
13310
  button.disabled = false;
12685
13311
  });
12686
13312
  });
13313
+
13314
+ if (hasActiveLiveStream) {
13315
+ bodyView._remoteFleetLiveRefreshTimer = setInterval(async () => {
13316
+ if (!document.body.contains(bodyView)) {
13317
+ clearInterval(bodyView._remoteFleetLiveRefreshTimer);
13318
+ bodyView._remoteFleetLiveRefreshTimer = null;
13319
+ return;
13320
+ }
13321
+
13322
+ try {
13323
+ const result = await invokeDotNetAsync('RefreshRemoteFleetMonitorNodeFromJs', nodeId);
13324
+ await syncRemoteFleetNodeStateFromResult(result);
13325
+ } catch {
13326
+ clearInterval(bodyView._remoteFleetLiveRefreshTimer);
13327
+ bodyView._remoteFleetLiveRefreshTimer = null;
13328
+ }
13329
+ }, 1000);
13330
+ }
12687
13331
  }
12688
13332
 
12689
13333
  function bindMemoNodeEvents(container, nodeModel) {