@mindexec/cli 0.2.8 → 0.2.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mindexec/cli",
3
- "version": "0.2.8",
3
+ "version": "0.2.10",
4
4
  "description": "MindExec local runtime and bridge CLI",
5
5
  "main": "server.js",
6
6
  "type": "module",
@@ -3401,6 +3401,7 @@
3401
3401
  const MEDIA_GENERATION_STATUS_MESSAGE_METADATA_KEY = 'MediaGenerationStatusMessage';
3402
3402
  const BUSINESS_AUTOMATION_SEMANTIC_TYPE = 'BusinessAutomationNode';
3403
3403
  const REMOTE_FLEET_SEMANTIC_TYPE = 'RemoteFleetMonitor';
3404
+ const REMOTE_FLEET_DEVICE_SEMANTIC_TYPE = 'RemoteFleetDevice';
3404
3405
  const AUTOMATION_NODE_KIND_METADATA_KEY = 'AutomationNodeKind';
3405
3406
  const AUTOMATION_NODE_LABEL_METADATA_KEY = 'AutomationNodeLabel';
3406
3407
  const AUTOMATION_NODE_DESCRIPTION_METADATA_KEY = 'AutomationNodeDescription';
@@ -3574,6 +3575,16 @@
3574
3575
  return getNodeSemanticType(nodeModel) === REMOTE_FLEET_SEMANTIC_TYPE;
3575
3576
  }
3576
3577
 
3578
+ function isRemoteFleetDeviceNode(nodeModel) {
3579
+ return getNodeSemanticType(nodeModel) === REMOTE_FLEET_DEVICE_SEMANTIC_TYPE;
3580
+ }
3581
+
3582
+ function isRemoteFleetNode(nodeModel) {
3583
+ const semanticType = getNodeSemanticType(nodeModel);
3584
+ return semanticType === REMOTE_FLEET_SEMANTIC_TYPE
3585
+ || semanticType === REMOTE_FLEET_DEVICE_SEMANTIC_TYPE;
3586
+ }
3587
+
3577
3588
  function isBusinessAutomationContextSourceNode(nodeModel) {
3578
3589
  if (!nodeModel || isBusinessAutomationNode(nodeModel)) {
3579
3590
  return false;
@@ -3588,7 +3599,8 @@
3588
3599
  return semanticType === 'MindCanvasAgent'
3589
3600
  || semanticType === 'AgentCommand'
3590
3601
  || semanticType === BUSINESS_AUTOMATION_SEMANTIC_TYPE
3591
- || semanticType === REMOTE_FLEET_SEMANTIC_TYPE;
3602
+ || semanticType === REMOTE_FLEET_SEMANTIC_TYPE
3603
+ || semanticType === REMOTE_FLEET_DEVICE_SEMANTIC_TYPE;
3592
3604
  }
3593
3605
 
3594
3606
  function allowsMemoExternalChrome(nodeModel) {
@@ -11845,6 +11857,11 @@
11845
11857
  return;
11846
11858
  }
11847
11859
 
11860
+ if (isRemoteFleetDeviceNode(nodeModel)) {
11861
+ renderRemoteFleetDevice(bodyView, nodeModel);
11862
+ return;
11863
+ }
11864
+
11848
11865
  bodyView.classList.remove('map-node-remote-fleet__body');
11849
11866
  bodyView.style.cssText = `
11850
11867
  flex: 1 1 auto;
@@ -11954,6 +11971,22 @@
11954
11971
  }
11955
11972
  }
11956
11973
 
11974
+ function parseRemoteFleetPinnedDevice(nodeModel) {
11975
+ const raw = getRemoteFleetMetadataValue(nodeModel, 'RemoteFleetPinnedDeviceJson', '{}');
11976
+ if (!raw.trim()) {
11977
+ return null;
11978
+ }
11979
+
11980
+ try {
11981
+ const parsed = JSON.parse(raw);
11982
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
11983
+ ? parsed
11984
+ : null;
11985
+ } catch {
11986
+ return null;
11987
+ }
11988
+ }
11989
+
11957
11990
  function formatRemoteFleetNumber(value, digits = 0) {
11958
11991
  const number = Number(value);
11959
11992
  return Number.isFinite(number) ? number.toFixed(digits) : '-';
@@ -12167,6 +12200,68 @@
12167
12200
  return getRemoteFleetDeviceField(device, 'thumbnailEnabled', 'ThumbnailEnabled', false) === true;
12168
12201
  }
12169
12202
 
12203
+ function getRemoteFleetGroupInfo(device, groupMode) {
12204
+ const mode = String(groupMode || 'none');
12205
+ const connected = isRemoteFleetDeviceConnected(device);
12206
+ const latestTaskStatus = String(getRemoteFleetDeviceField(device, 'latestTaskStatus', 'LatestTaskStatus', '')).toLowerCase();
12207
+ const latestTaskError = String(getRemoteFleetDeviceField(device, 'latestTaskError', 'LatestTaskError', ''));
12208
+ const platform = String(getRemoteFleetDeviceField(device, 'platform', 'Platform', '') || 'unknown').toLowerCase();
12209
+
12210
+ switch (mode) {
12211
+ case 'status':
12212
+ return connected
12213
+ ? { key: 'status-online', label: 'Online', order: 0 }
12214
+ : { key: 'status-offline', label: 'Offline', order: 9 };
12215
+ case 'activity':
12216
+ if (isRemoteFleetLiveActive(device)) {
12217
+ return { key: 'activity-live', label: 'Live streams', order: 0 };
12218
+ }
12219
+ if (latestTaskError || latestTaskStatus === 'failed') {
12220
+ return { key: 'activity-issues', label: 'Needs attention', order: 1 };
12221
+ }
12222
+ if (latestTaskStatus && latestTaskStatus !== 'completed') {
12223
+ return { key: 'activity-working', label: 'Task in progress', order: 2 };
12224
+ }
12225
+ if (hasRemoteFleetThumbnail(device) || hasRemoteFleetLiveFrame(device)) {
12226
+ return { key: 'activity-frames', label: 'Recent frames', order: 3 };
12227
+ }
12228
+ return connected
12229
+ ? { key: 'activity-idle', label: 'Online idle', order: 4 }
12230
+ : { key: 'activity-offline', label: 'Offline', order: 9 };
12231
+ case 'platform':
12232
+ return {
12233
+ key: `platform-${platform || 'unknown'}`,
12234
+ label: platform ? platform.toUpperCase() : 'Unknown platform',
12235
+ order: connected ? 0 : 8
12236
+ };
12237
+ case 'capability':
12238
+ if (!connected) {
12239
+ return { key: 'capability-offline', label: 'Offline', order: 9 };
12240
+ }
12241
+ if (isRemoteFleetDeviceAiCapable(device)) {
12242
+ return { key: 'capability-ai', label: 'AI assist ready', order: 0 };
12243
+ }
12244
+ if (isRemoteFleetDeviceTaskCapable(device)) {
12245
+ return { key: 'capability-task', label: 'Task capable', order: 1 };
12246
+ }
12247
+ if (isRemoteFleetThumbnailCapable(device)) {
12248
+ return { key: 'capability-monitor', label: 'Monitor only', order: 2 };
12249
+ }
12250
+ return { key: 'capability-status', label: 'Status only', order: 3 };
12251
+ case 'none':
12252
+ default:
12253
+ return { key: '', label: '', order: 0 };
12254
+ }
12255
+ }
12256
+
12257
+ function compareRemoteFleetGroups(left, right, groupMode) {
12258
+ const leftGroup = getRemoteFleetGroupInfo(left, groupMode);
12259
+ const rightGroup = getRemoteFleetGroupInfo(right, groupMode);
12260
+ return (leftGroup.order - rightGroup.order)
12261
+ || leftGroup.label.localeCompare(rightGroup.label)
12262
+ || leftGroup.key.localeCompare(rightGroup.key);
12263
+ }
12264
+
12170
12265
  function getRemoteFleetTimestampMs(value) {
12171
12266
  const timestamp = Date.parse(String(value || ''));
12172
12267
  return Number.isFinite(timestamp) ? timestamp : 0;
@@ -12238,6 +12333,501 @@
12238
12333
  }
12239
12334
  }
12240
12335
 
12336
+ function renderRemoteFleetDevice(bodyView, nodeModel) {
12337
+ if (!bodyView) return;
12338
+ clearRemoteFleetTimers(bodyView);
12339
+
12340
+ const nodeId = String(nodeModel?.id ?? nodeModel?.Id ?? '');
12341
+ const device = parseRemoteFleetPinnedDevice(nodeModel);
12342
+ const endpoint = getRemoteFleetMetadataValue(nodeModel, 'RemoteFleetHubEndpoint', '127.0.0.1:5197');
12343
+ const hubStatus = getRemoteFleetMetadataValue(nodeModel, 'RemoteFleetHubStatus', 'offline');
12344
+ const refreshedAt = getRemoteFleetMetadataValue(nodeModel, 'RemoteFleetLastRefreshAtUtc', '');
12345
+ const lastError = getRemoteFleetMetadataValue(nodeModel, 'RemoteFleetLastError', '');
12346
+ const deviceId = getRemoteFleetMetadataValue(nodeModel, 'RemoteFleetPinnedDeviceId', device ? getRemoteFleetDeviceId(device) : '');
12347
+
12348
+ bodyView.dataset.src = `remote-device:${deviceId}:${refreshedAt}`;
12349
+ bodyView.classList.add('map-node-remote-fleet__body');
12350
+ bodyView.innerHTML = '';
12351
+ bodyView.style.cssText = `
12352
+ flex: 1 1 auto;
12353
+ min-height: 0;
12354
+ pointer-events: auto;
12355
+ display: flex;
12356
+ flex-direction: column;
12357
+ gap: 10px;
12358
+ padding: 12px 14px 14px;
12359
+ overflow: hidden;
12360
+ background: linear-gradient(180deg, rgba(248, 250, 252, 0.96), rgba(236, 253, 245, 0.78));
12361
+ `;
12362
+
12363
+ const feedback = document.createElement('div');
12364
+ feedback.dataset.remoteFleetDeviceFeedback = 'true';
12365
+ feedback.style.cssText = `
12366
+ display: none;
12367
+ flex: 0 0 auto;
12368
+ min-height: 22px;
12369
+ padding: 5px 8px;
12370
+ border-radius: 7px;
12371
+ background: rgba(37, 99, 235, 0.08);
12372
+ color: #1d4ed8;
12373
+ font-size: 11px;
12374
+ font-weight: 800;
12375
+ line-height: 1.25;
12376
+ overflow: hidden;
12377
+ text-overflow: ellipsis;
12378
+ white-space: nowrap;
12379
+ `;
12380
+ const setFeedback = (message, tone = 'info') => {
12381
+ feedback.textContent = message || '';
12382
+ feedback.style.display = message ? 'block' : 'none';
12383
+ feedback.style.background = tone === 'error'
12384
+ ? 'rgba(248, 113, 113, 0.12)'
12385
+ : tone === 'success'
12386
+ ? 'rgba(16, 185, 129, 0.10)'
12387
+ : 'rgba(37, 99, 235, 0.08)';
12388
+ feedback.style.color = tone === 'error'
12389
+ ? '#991b1b'
12390
+ : tone === 'success'
12391
+ ? '#047857'
12392
+ : '#1d4ed8';
12393
+ };
12394
+
12395
+ const refreshRemoteDeviceNode = async () => {
12396
+ if (bodyView._remoteFleetRefreshInFlight === true) {
12397
+ return null;
12398
+ }
12399
+
12400
+ bodyView._remoteFleetRefreshInFlight = true;
12401
+ try {
12402
+ const result = await invokeDotNetAsync('RefreshRemoteFleetDeviceNodeFromJs', nodeId);
12403
+ await syncRemoteFleetNodeStateFromResult(result);
12404
+ return result;
12405
+ } finally {
12406
+ bodyView._remoteFleetRefreshInFlight = false;
12407
+ }
12408
+ };
12409
+
12410
+ if (!device || !deviceId) {
12411
+ const empty = document.createElement('div');
12412
+ empty.textContent = lastError || 'Pinned remote device snapshot is unavailable.';
12413
+ empty.style.cssText = `
12414
+ flex: 1 1 auto;
12415
+ display: flex;
12416
+ align-items: center;
12417
+ justify-content: center;
12418
+ min-height: 120px;
12419
+ padding: 14px;
12420
+ border-radius: 8px;
12421
+ border: 1px dashed rgba(100, 116, 139, 0.36);
12422
+ color: #475569;
12423
+ font-size: 13px;
12424
+ font-weight: 850;
12425
+ text-align: center;
12426
+ background: rgba(255, 255, 255, 0.74);
12427
+ `;
12428
+ const refreshButton = createRemoteFleetButton('Refresh', 'Refresh this pinned device', 'refresh-device');
12429
+ refreshButton.style.height = '32px';
12430
+ refreshButton.addEventListener('click', async event => {
12431
+ event.preventDefault();
12432
+ event.stopPropagation();
12433
+ refreshButton.disabled = true;
12434
+ try {
12435
+ await refreshRemoteDeviceNode();
12436
+ } finally {
12437
+ refreshButton.disabled = false;
12438
+ }
12439
+ });
12440
+ bodyView.appendChild(empty);
12441
+ bodyView.appendChild(refreshButton);
12442
+ bodyView.appendChild(feedback);
12443
+ return;
12444
+ }
12445
+
12446
+ const name = getRemoteFleetDeviceName(device);
12447
+ const connected = isRemoteFleetDeviceConnected(device);
12448
+ const taskEnabled = isRemoteFleetDeviceTaskCapable(device);
12449
+ const aiAssistEnabled = isRemoteFleetDeviceAiCapable(device);
12450
+ const thumbnailEnabled = isRemoteFleetThumbnailCapable(device);
12451
+ const liveStreamEnabled = getRemoteFleetDeviceField(device, 'liveStreamEnabled', 'LiveStreamEnabled', false) === true;
12452
+ const liveActive = isRemoteFleetLiveActive(device);
12453
+ const liveStreamId = String(getRemoteFleetDeviceField(device, 'liveStreamId', 'LiveStreamId', ''));
12454
+ const hasLiveFrame = hasRemoteFleetLiveFrame(device);
12455
+ const hasThumbnail = hasRemoteFleetThumbnail(device);
12456
+ const previewDataUrl = hasLiveFrame
12457
+ ? String(getRemoteFleetDeviceField(device, 'liveFrameDataUrl', 'LiveFrameDataUrl', ''))
12458
+ : String(getRemoteFleetDeviceField(device, 'thumbnailDataUrl', 'ThumbnailDataUrl', ''));
12459
+ const previewAt = hasLiveFrame
12460
+ ? String(getRemoteFleetDeviceField(device, 'liveFrameReceivedAt', 'LiveFrameReceivedAt', ''))
12461
+ : String(getRemoteFleetDeviceField(device, 'thumbnailReceivedAt', 'ThumbnailReceivedAt',
12462
+ getRemoteFleetDeviceField(device, 'thumbnailCapturedAt', 'ThumbnailCapturedAt', '')));
12463
+ const platform = [
12464
+ getRemoteFleetDeviceField(device, 'platform', 'Platform', 'unknown'),
12465
+ getRemoteFleetDeviceField(device, 'arch', 'Arch', '')
12466
+ ].filter(Boolean).join(' / ');
12467
+ const release = String(getRemoteFleetDeviceField(device, 'release', 'Release', ''));
12468
+ const latestTaskStatus = String(getRemoteFleetDeviceField(device, 'latestTaskStatus', 'LatestTaskStatus', ''));
12469
+ const latestTaskTitle = String(getRemoteFleetDeviceField(device, 'latestTaskTitle', 'LatestTaskTitle', ''));
12470
+ const latestTaskApproval = String(getRemoteFleetDeviceField(device, 'latestTaskApprovalLevel', 'LatestTaskApprovalLevel', ''));
12471
+ const latestTaskUpdatedAt = String(getRemoteFleetDeviceField(device, 'latestTaskUpdatedAt', 'LatestTaskUpdatedAt', ''));
12472
+ const latestTaskError = String(getRemoteFleetDeviceField(device, 'latestTaskError', 'LatestTaskError', ''));
12473
+ const latestTaskResult = String(getRemoteFleetDeviceField(device, 'latestTaskResultSummary', 'LatestTaskResultSummary', ''));
12474
+
12475
+ const header = document.createElement('div');
12476
+ header.style.cssText = 'display:flex;align-items:flex-start;justify-content:space-between;gap:10px;flex:0 0 auto;';
12477
+ const titleBox = document.createElement('div');
12478
+ titleBox.style.cssText = 'min-width:0;display:flex;flex-direction:column;gap:3px;';
12479
+ const title = document.createElement('strong');
12480
+ title.textContent = name;
12481
+ title.title = `${name} (${deviceId})`;
12482
+ title.style.cssText = 'color:#0f172a;font-size:16px;font-weight:950;line-height:1.12;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;letter-spacing:0;';
12483
+ const subtitle = document.createElement('span');
12484
+ subtitle.textContent = `${connected ? 'Online' : 'Offline'} - ${release ? `${platform} ${release}` : platform}`;
12485
+ subtitle.title = subtitle.textContent;
12486
+ subtitle.style.cssText = `color:${connected ? '#047857' : '#64748b'};font-size:11px;font-weight:850;line-height:1.2;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;letter-spacing:0;`;
12487
+ titleBox.appendChild(title);
12488
+ titleBox.appendChild(subtitle);
12489
+ const statusBadge = document.createElement('span');
12490
+ statusBadge.textContent = liveActive ? 'LIVE' : (connected ? 'READY' : 'OFFLINE');
12491
+ statusBadge.style.cssText = `
12492
+ flex: 0 0 auto;
12493
+ padding: 5px 8px;
12494
+ border-radius: 999px;
12495
+ background: ${liveActive ? 'rgba(220, 38, 38, 0.12)' : connected ? 'rgba(16, 185, 129, 0.12)' : 'rgba(100, 116, 139, 0.12)'};
12496
+ color: ${liveActive ? '#b91c1c' : connected ? '#047857' : '#475569'};
12497
+ font-size: 10px;
12498
+ font-weight: 950;
12499
+ letter-spacing: 0;
12500
+ `;
12501
+ header.appendChild(titleBox);
12502
+ header.appendChild(statusBadge);
12503
+ bodyView.appendChild(header);
12504
+
12505
+ const preview = document.createElement('div');
12506
+ preview.style.cssText = `
12507
+ position: relative;
12508
+ flex: 0 0 auto;
12509
+ width: 100%;
12510
+ aspect-ratio: 16 / 9;
12511
+ overflow: hidden;
12512
+ border-radius: 8px;
12513
+ background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%);
12514
+ border: 1px solid rgba(15, 23, 42, 0.16);
12515
+ `;
12516
+ if (hasLiveFrame || hasThumbnail) {
12517
+ const image = document.createElement('img');
12518
+ image.src = previewDataUrl;
12519
+ image.alt = `${name} screen`;
12520
+ image.loading = 'lazy';
12521
+ image.decoding = 'async';
12522
+ image.style.cssText = 'width:100%;height:100%;object-fit:cover;display:block;';
12523
+ preview.appendChild(image);
12524
+ } else {
12525
+ const placeholder = document.createElement('div');
12526
+ placeholder.textContent = thumbnailEnabled ? 'No frame yet' : 'Status only';
12527
+ placeholder.style.cssText = `
12528
+ position: absolute;
12529
+ inset: 0;
12530
+ display: flex;
12531
+ align-items: center;
12532
+ justify-content: center;
12533
+ color: rgba(226, 232, 240, 0.78);
12534
+ font-size: 12px;
12535
+ font-weight: 900;
12536
+ letter-spacing: 0;
12537
+ `;
12538
+ preview.appendChild(placeholder);
12539
+ }
12540
+ const previewBadge = document.createElement('span');
12541
+ previewBadge.textContent = hasLiveFrame
12542
+ ? `LIVE ${previewAt ? formatRemoteFleetAge(previewAt) : ''}`.trim()
12543
+ : (previewAt ? formatRemoteFleetAge(previewAt) : (hubStatus === 'online' ? 'Pinned' : 'Hub offline'));
12544
+ previewBadge.style.cssText = `
12545
+ position: absolute;
12546
+ left: 7px;
12547
+ bottom: 7px;
12548
+ max-width: calc(100% - 14px);
12549
+ padding: 4px 7px;
12550
+ border-radius: 999px;
12551
+ background: ${hasLiveFrame ? 'rgba(220, 38, 38, 0.84)' : 'rgba(15, 23, 42, 0.74)'};
12552
+ color: #e2e8f0;
12553
+ font-size: 9px;
12554
+ font-weight: 950;
12555
+ line-height: 1;
12556
+ overflow: hidden;
12557
+ text-overflow: ellipsis;
12558
+ white-space: nowrap;
12559
+ letter-spacing: 0;
12560
+ `;
12561
+ preview.appendChild(previewBadge);
12562
+ bodyView.appendChild(preview);
12563
+
12564
+ const stats = document.createElement('div');
12565
+ stats.style.cssText = 'display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:7px;flex:0 0 auto;';
12566
+ stats.appendChild(createRemoteFleetStat('Seen', formatRemoteFleetAge(getRemoteFleetDeviceField(device, 'lastSeenAt', 'LastSeenAt', '')), connected ? 'online' : 'default'));
12567
+ stats.appendChild(createRemoteFleetStat('Uptime', formatRemoteFleetDuration(getRemoteFleetDeviceField(device, 'uptimeSec', 'UptimeSec', Number.NaN))));
12568
+ stats.appendChild(createRemoteFleetStat('Mem', formatRemoteFleetPercent(getRemoteFleetDeviceField(device, 'usedMemRatio', 'UsedMemRatio', Number.NaN))));
12569
+ stats.appendChild(createRemoteFleetStat('Load', formatRemoteFleetNumber(getRemoteFleetDeviceField(device, 'load1', 'Load1', Number.NaN), 2)));
12570
+ bodyView.appendChild(stats);
12571
+
12572
+ if (latestTaskStatus || latestTaskTitle || latestTaskResult || latestTaskError) {
12573
+ const taskBox = document.createElement('div');
12574
+ const taskTone = latestTaskError || latestTaskStatus === 'failed'
12575
+ ? 'error'
12576
+ : (latestTaskStatus === 'completed' ? 'done' : 'pending');
12577
+ taskBox.style.cssText = `
12578
+ display: flex;
12579
+ flex-direction: column;
12580
+ gap: 3px;
12581
+ min-width: 0;
12582
+ padding: 8px 9px;
12583
+ border-radius: 8px;
12584
+ background: ${taskTone === 'error' ? 'rgba(248, 113, 113, 0.12)' : taskTone === 'done' ? 'rgba(16, 185, 129, 0.10)' : 'rgba(37, 99, 235, 0.08)'};
12585
+ border: 1px solid ${taskTone === 'error' ? 'rgba(248, 113, 113, 0.24)' : taskTone === 'done' ? 'rgba(16, 185, 129, 0.20)' : 'rgba(37, 99, 235, 0.16)'};
12586
+ flex: 0 0 auto;
12587
+ `;
12588
+ const taskLine = document.createElement('div');
12589
+ const taskModeLabel = latestTaskApproval === 'ai-assist' ? 'AI' : 'Task';
12590
+ taskLine.textContent = `${taskModeLabel} ${latestTaskStatus || 'task'}${latestTaskUpdatedAt ? ` - ${formatRemoteFleetAge(latestTaskUpdatedAt)}` : ''}`;
12591
+ taskLine.style.cssText = `color:${taskTone === 'error' ? '#991b1b' : taskTone === 'done' ? '#047857' : '#1d4ed8'};font-size:10px;font-weight:900;line-height:1.2;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;letter-spacing:0;`;
12592
+ const taskSummary = document.createElement('div');
12593
+ taskSummary.textContent = latestTaskError || latestTaskResult || latestTaskTitle || 'Task queued';
12594
+ taskSummary.title = taskSummary.textContent;
12595
+ taskSummary.style.cssText = 'color:#334155;font-size:11px;font-weight:750;line-height:1.3;overflow:hidden;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;letter-spacing:0;';
12596
+ taskBox.appendChild(taskLine);
12597
+ taskBox.appendChild(taskSummary);
12598
+ bodyView.appendChild(taskBox);
12599
+ }
12600
+
12601
+ const taskInput = document.createElement('textarea');
12602
+ taskInput.dataset.remoteFleetDeviceTaskInput = 'true';
12603
+ taskInput.placeholder = 'Task for this computer';
12604
+ taskInput.rows = 2;
12605
+ taskInput.style.cssText = `
12606
+ width: 100%;
12607
+ min-width: 0;
12608
+ height: 50px;
12609
+ resize: none;
12610
+ border-radius: 8px;
12611
+ border: 1px solid rgba(148, 163, 184, 0.34);
12612
+ background: rgba(255, 255, 255, 0.92);
12613
+ color: #0f172a;
12614
+ padding: 8px 10px;
12615
+ font-size: 12px;
12616
+ line-height: 1.35;
12617
+ font-weight: 700;
12618
+ letter-spacing: 0;
12619
+ outline: none;
12620
+ pointer-events: auto;
12621
+ `;
12622
+ bodyView.appendChild(taskInput);
12623
+
12624
+ const actions = document.createElement('div');
12625
+ actions.style.cssText = 'display:flex;align-items:center;gap:7px;flex-wrap:wrap;flex:0 0 auto;';
12626
+ const refreshButton = createRemoteFleetButton('Refresh', 'Refresh this pinned device', 'refresh-device');
12627
+ refreshButton.style.height = '30px';
12628
+ actions.appendChild(refreshButton);
12629
+ if (connected && thumbnailEnabled) {
12630
+ const shotButton = createRemoteFleetButton('Shot', 'Request thumbnail', 'thumbnail-device');
12631
+ shotButton.dataset.deviceId = deviceId;
12632
+ shotButton.style.height = '30px';
12633
+ actions.appendChild(shotButton);
12634
+ }
12635
+ if (connected && liveStreamEnabled) {
12636
+ const liveButton = createRemoteFleetButton(liveActive ? 'Stop' : 'Live', liveActive ? 'Stop live stream' : 'Start live stream', liveActive ? 'live-stop' : 'live-start');
12637
+ liveButton.dataset.deviceId = deviceId;
12638
+ liveButton.dataset.streamId = liveStreamId;
12639
+ liveButton.style.height = '30px';
12640
+ if (liveActive) {
12641
+ liveButton.style.borderColor = 'rgba(220, 38, 38, 0.32)';
12642
+ liveButton.style.color = '#b91c1c';
12643
+ }
12644
+ actions.appendChild(liveButton);
12645
+ }
12646
+ if (connected && taskEnabled) {
12647
+ const taskButton = createRemoteFleetButton(aiAssistEnabled ? 'Task/AI' : 'Task', 'Dispatch task to this device', 'task-device');
12648
+ taskButton.dataset.deviceId = deviceId;
12649
+ taskButton.style.height = '30px';
12650
+ actions.appendChild(taskButton);
12651
+ }
12652
+ if (connected) {
12653
+ const pingButton = createRemoteFleetButton('Ping', 'Ping device', 'ping-device');
12654
+ pingButton.dataset.deviceId = deviceId;
12655
+ pingButton.style.height = '30px';
12656
+ actions.appendChild(pingButton);
12657
+ }
12658
+ const aiToggleLabel = document.createElement('label');
12659
+ aiToggleLabel.title = 'Use AI assist when this agent exposes it';
12660
+ aiToggleLabel.style.cssText = `
12661
+ display: ${aiAssistEnabled ? 'inline-flex' : 'none'};
12662
+ align-items: center;
12663
+ justify-content: center;
12664
+ gap: 6px;
12665
+ height: 30px;
12666
+ padding: 0 9px;
12667
+ border-radius: 7px;
12668
+ border: 1px solid rgba(14, 165, 233, 0.28);
12669
+ background: rgba(240, 249, 255, 0.92);
12670
+ color: #0369a1;
12671
+ font-size: 11px;
12672
+ font-weight: 900;
12673
+ letter-spacing: 0;
12674
+ cursor: pointer;
12675
+ pointer-events: auto;
12676
+ user-select: none;
12677
+ `;
12678
+ const aiToggle = document.createElement('input');
12679
+ aiToggle.type = 'checkbox';
12680
+ aiToggle.dataset.remoteFleetDeviceAiToggle = 'true';
12681
+ aiToggle.style.cssText = 'width:14px;height:14px;margin:0;accent-color:#0284c7;';
12682
+ const aiToggleText = document.createElement('span');
12683
+ aiToggleText.textContent = 'AI';
12684
+ aiToggleLabel.appendChild(aiToggle);
12685
+ aiToggleLabel.appendChild(aiToggleText);
12686
+ actions.appendChild(aiToggleLabel);
12687
+ bodyView.appendChild(actions);
12688
+ bodyView.appendChild(feedback);
12689
+
12690
+ const footer = document.createElement('div');
12691
+ footer.textContent = `Endpoint ${endpoint} - ${deviceId} - refreshed ${formatRemoteFleetAge(refreshedAt)}`;
12692
+ footer.title = footer.textContent;
12693
+ footer.style.cssText = `
12694
+ margin-top: auto;
12695
+ flex: 0 0 auto;
12696
+ color: #64748b;
12697
+ font-size: 10px;
12698
+ font-weight: 750;
12699
+ line-height: 1.2;
12700
+ overflow: hidden;
12701
+ text-overflow: ellipsis;
12702
+ white-space: nowrap;
12703
+ `;
12704
+ bodyView.appendChild(footer);
12705
+
12706
+ if (lastError.trim()) {
12707
+ setFeedback(lastError, 'error');
12708
+ }
12709
+
12710
+ [taskInput, aiToggleLabel, aiToggle, ...Array.from(actions.querySelectorAll('button'))].forEach(control => {
12711
+ if (!control) return;
12712
+ ['mousedown', 'mouseup', 'click', 'dblclick', 'keydown'].forEach(eventName => {
12713
+ control.addEventListener(eventName, event => event.stopPropagation());
12714
+ });
12715
+ });
12716
+
12717
+ refreshButton.addEventListener('click', async event => {
12718
+ event.preventDefault();
12719
+ event.stopPropagation();
12720
+ refreshButton.disabled = true;
12721
+ try {
12722
+ await refreshRemoteDeviceNode();
12723
+ } finally {
12724
+ refreshButton.disabled = false;
12725
+ }
12726
+ });
12727
+
12728
+ actions.querySelectorAll('[data-remote-fleet-action="thumbnail-device"]').forEach(button => {
12729
+ button.addEventListener('click', async event => {
12730
+ event.preventDefault();
12731
+ event.stopPropagation();
12732
+ button.disabled = true;
12733
+ setFeedback('Requesting thumbnail...');
12734
+ try {
12735
+ const result = await invokeDotNetAsync('RequestRemoteFleetThumbnailFromJs', nodeId, deviceId);
12736
+ await syncRemoteFleetNodeStateFromResult(result);
12737
+ setFeedback(result?.success ? 'Thumbnail refreshed.' : (result?.error || 'Thumbnail failed.'), result?.success ? 'success' : 'error');
12738
+ } finally {
12739
+ button.disabled = false;
12740
+ }
12741
+ });
12742
+ });
12743
+
12744
+ actions.querySelectorAll('[data-remote-fleet-action="live-start"]').forEach(button => {
12745
+ button.addEventListener('click', async event => {
12746
+ event.preventDefault();
12747
+ event.stopPropagation();
12748
+ button.disabled = true;
12749
+ setFeedback('Starting live stream...');
12750
+ try {
12751
+ const result = await invokeDotNetAsync('StartRemoteFleetLiveStreamFromJs', nodeId, deviceId);
12752
+ await syncRemoteFleetNodeStateFromResult(result);
12753
+ setFeedback(result?.success ? 'Live stream started.' : (result?.error || 'Live stream failed.'), result?.success ? 'success' : 'error');
12754
+ } finally {
12755
+ button.disabled = false;
12756
+ }
12757
+ });
12758
+ });
12759
+
12760
+ actions.querySelectorAll('[data-remote-fleet-action="live-stop"]').forEach(button => {
12761
+ button.addEventListener('click', async event => {
12762
+ event.preventDefault();
12763
+ event.stopPropagation();
12764
+ button.disabled = true;
12765
+ setFeedback('Stopping live stream...');
12766
+ try {
12767
+ const result = await invokeDotNetAsync('StopRemoteFleetLiveStreamFromJs', nodeId, deviceId, button.dataset.streamId || '');
12768
+ await syncRemoteFleetNodeStateFromResult(result);
12769
+ setFeedback(result?.success ? 'Live stream stopped.' : (result?.error || 'Live stop failed.'), result?.success ? 'success' : 'error');
12770
+ } finally {
12771
+ button.disabled = false;
12772
+ }
12773
+ });
12774
+ });
12775
+
12776
+ actions.querySelectorAll('[data-remote-fleet-action="ping-device"]').forEach(button => {
12777
+ button.addEventListener('click', async event => {
12778
+ event.preventDefault();
12779
+ event.stopPropagation();
12780
+ button.disabled = true;
12781
+ setFeedback('Pinging device...');
12782
+ try {
12783
+ const result = await invokeDotNetAsync('PingRemoteFleetDeviceFromJs', nodeId, deviceId);
12784
+ await syncRemoteFleetNodeStateFromResult(result);
12785
+ setFeedback(result?.success ? 'Ping queued.' : (result?.error || 'Ping failed.'), result?.success ? 'success' : 'error');
12786
+ } finally {
12787
+ button.disabled = false;
12788
+ }
12789
+ });
12790
+ });
12791
+
12792
+ actions.querySelectorAll('[data-remote-fleet-action="task-device"]').forEach(button => {
12793
+ button.addEventListener('click', async event => {
12794
+ event.preventDefault();
12795
+ event.stopPropagation();
12796
+ const instruction = String(taskInput.value || '').trim();
12797
+ if (!instruction) {
12798
+ setFeedback('Write a task first.', 'error');
12799
+ taskInput.focus();
12800
+ return;
12801
+ }
12802
+
12803
+ button.disabled = true;
12804
+ setFeedback('Dispatching task...');
12805
+ try {
12806
+ const result = await invokeDotNetAsync('DispatchRemoteFleetTaskFromJs', nodeId, deviceId, instruction, aiToggle.checked === true);
12807
+ await syncRemoteFleetNodeStateFromResult(result);
12808
+ setFeedback(result?.success ? 'Task queued.' : (result?.error || 'Task dispatch failed.'), result?.success ? 'success' : 'error');
12809
+ } finally {
12810
+ button.disabled = false;
12811
+ }
12812
+ });
12813
+ });
12814
+
12815
+ if (liveActive) {
12816
+ bodyView._remoteFleetLiveRefreshTimer = setInterval(async () => {
12817
+ if (!document.body.contains(bodyView)) {
12818
+ clearRemoteFleetTimers(bodyView);
12819
+ return;
12820
+ }
12821
+
12822
+ try {
12823
+ await refreshRemoteDeviceNode();
12824
+ } catch {
12825
+ clearRemoteFleetTimers(bodyView);
12826
+ }
12827
+ }, REMOTE_FLEET_LIVE_REFRESH_MS);
12828
+ }
12829
+ }
12830
+
12241
12831
  function renderRemoteFleetMonitor(bodyView, nodeModel) {
12242
12832
  if (!bodyView) return;
12243
12833
  clearRemoteFleetTimers(bodyView);
@@ -12246,12 +12836,18 @@
12246
12836
  const searchState = bodyView.dataset.remoteFleetSearch || '';
12247
12837
  const filterState = bodyView.dataset.remoteFleetFilter || 'all';
12248
12838
  const sortState = bodyView.dataset.remoteFleetSort || 'status';
12839
+ const groupState = bodyView.dataset.remoteFleetGroup || 'none';
12249
12840
  const densityState = bodyView.dataset.remoteFleetDensity || 'cards';
12250
12841
  const aiAssistState = bodyView.dataset.remoteFleetAiAssist === 'true';
12251
12842
  const autoMonitorState = bodyView.dataset.remoteFleetAutoMonitor !== 'false';
12252
12843
  const focusState = bodyView.dataset.remoteFleetFocusDeviceId || '';
12253
- const devices = [...parseRemoteFleetDevices(nodeModel)]
12844
+ const sortedDevices = [...parseRemoteFleetDevices(nodeModel)]
12254
12845
  .sort((left, right) => compareRemoteFleetDevices(left, right, sortState));
12846
+ const devices = groupState === 'none'
12847
+ ? sortedDevices
12848
+ : [...sortedDevices].sort((left, right) =>
12849
+ compareRemoteFleetGroups(left, right, groupState)
12850
+ || compareRemoteFleetDevices(left, right, sortState));
12255
12851
  const hasActiveLiveStream = devices.some(isRemoteFleetLiveActive);
12256
12852
  const hasMonitorTargets = devices.some(device =>
12257
12853
  isRemoteFleetDeviceConnected(device) && isRemoteFleetThumbnailCapable(device));
@@ -12376,7 +12972,7 @@
12376
12972
  const filterRow = document.createElement('div');
12377
12973
  filterRow.style.cssText = `
12378
12974
  display: grid;
12379
- grid-template-columns: minmax(150px, 1.2fr) minmax(96px, 0.7fr) minmax(96px, 0.7fr) minmax(84px, 0.55fr) auto;
12975
+ grid-template-columns: minmax(140px, 1.25fr) minmax(86px, 0.62fr) minmax(86px, 0.62fr) minmax(98px, 0.7fr) minmax(78px, 0.5fr) auto;
12380
12976
  gap: 8px;
12381
12977
  align-items: center;
12382
12978
  flex: 0 0 auto;
@@ -12424,6 +13020,14 @@
12424
13020
  { value: 'task', label: 'Task' }
12425
13021
  ], sortState, 'Device sort');
12426
13022
 
13023
+ const groupSelect = createRemoteFleetSelect([
13024
+ { value: 'none', label: 'No group' },
13025
+ { value: 'status', label: 'Status' },
13026
+ { value: 'activity', label: 'Activity' },
13027
+ { value: 'platform', label: 'Platform' },
13028
+ { value: 'capability', label: 'Capability' }
13029
+ ], groupState, 'Device grouping');
13030
+
12427
13031
  const densitySelect = createRemoteFleetSelect([
12428
13032
  { value: 'cards', label: 'Cards' },
12429
13033
  { value: 'dense', label: 'Dense' }
@@ -12446,6 +13050,7 @@
12446
13050
  filterRow.appendChild(searchInput);
12447
13051
  filterRow.appendChild(filterSelect);
12448
13052
  filterRow.appendChild(sortSelect);
13053
+ filterRow.appendChild(groupSelect);
12449
13054
  filterRow.appendChild(densitySelect);
12450
13055
  filterRow.appendChild(matchCount);
12451
13056
  bodyView.appendChild(filterRow);
@@ -12726,6 +13331,25 @@
12726
13331
  `;
12727
13332
  grid.appendChild(empty);
12728
13333
  } else {
13334
+ const groupStats = new Map();
13335
+ if (groupState !== 'none') {
13336
+ devices.forEach(device => {
13337
+ const group = getRemoteFleetGroupInfo(device, groupState);
13338
+ if (!group.key) return;
13339
+ const stat = groupStats.get(group.key) || {
13340
+ label: group.label,
13341
+ total: 0,
13342
+ connected: 0
13343
+ };
13344
+ stat.total += 1;
13345
+ if (isRemoteFleetDeviceConnected(device)) {
13346
+ stat.connected += 1;
13347
+ }
13348
+ groupStats.set(group.key, stat);
13349
+ });
13350
+ }
13351
+
13352
+ let lastGroupKey = '';
12729
13353
  devices.forEach(device => {
12730
13354
  const connectedDevice = isRemoteFleetDeviceConnected(device);
12731
13355
  const name = getRemoteFleetDeviceName(device);
@@ -12753,9 +13377,44 @@
12753
13377
  const latestTaskUpdatedAt = String(device?.latestTaskUpdatedAt || device?.LatestTaskUpdatedAt || '');
12754
13378
  const latestTaskError = String(device?.latestTaskError || device?.LatestTaskError || '');
12755
13379
  const latestTaskResult = String(device?.latestTaskResultSummary || device?.LatestTaskResultSummary || '');
13380
+ const groupInfo = getRemoteFleetGroupInfo(device, groupState);
13381
+ if (groupState !== 'none' && groupInfo.key && groupInfo.key !== lastGroupKey) {
13382
+ const stat = groupStats.get(groupInfo.key) || { label: groupInfo.label, total: 0, connected: 0 };
13383
+ const groupHeader = document.createElement('div');
13384
+ groupHeader.dataset.remoteFleetGroupHeader = 'true';
13385
+ groupHeader.dataset.remoteFleetGroupKey = groupInfo.key;
13386
+ groupHeader.style.cssText = `
13387
+ grid-column: 1 / -1;
13388
+ display: flex;
13389
+ align-items: center;
13390
+ justify-content: space-between;
13391
+ gap: 10px;
13392
+ min-height: 30px;
13393
+ padding: 6px 9px;
13394
+ border-radius: 7px;
13395
+ border: 1px solid rgba(148, 163, 184, 0.22);
13396
+ background: rgba(226, 232, 240, 0.58);
13397
+ color: #334155;
13398
+ font-size: 11px;
13399
+ font-weight: 950;
13400
+ letter-spacing: 0;
13401
+ `;
13402
+ const groupLabel = document.createElement('span');
13403
+ groupLabel.textContent = stat.label || groupInfo.label;
13404
+ groupLabel.style.cssText = 'min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;';
13405
+ const groupCount = document.createElement('span');
13406
+ groupCount.dataset.remoteFleetGroupCount = 'true';
13407
+ groupCount.textContent = `${stat.total} device${stat.total === 1 ? '' : 's'}${stat.connected !== stat.total ? ` - ${stat.connected} online` : ''}`;
13408
+ groupCount.style.cssText = 'flex:0 0 auto;color:#64748b;font-size:10px;font-weight:900;white-space:nowrap;';
13409
+ groupHeader.appendChild(groupLabel);
13410
+ groupHeader.appendChild(groupCount);
13411
+ grid.appendChild(groupHeader);
13412
+ lastGroupKey = groupInfo.key;
13413
+ }
12756
13414
  const card = document.createElement('article');
12757
13415
  card.dataset.deviceId = deviceId;
12758
13416
  card.dataset.remoteFleetSearchText = buildRemoteFleetSearchText(device);
13417
+ card.dataset.remoteFleetGroupKey = groupInfo.key || '';
12759
13418
  card.dataset.remoteFleetConnected = connectedDevice ? 'true' : 'false';
12760
13419
  card.dataset.remoteFleetTaskCapable = taskEnabled ? 'true' : 'false';
12761
13420
  card.dataset.remoteFleetAiCapable = aiAssistEnabled ? 'true' : 'false';
@@ -12985,7 +13644,7 @@
12985
13644
  }
12986
13645
 
12987
13646
  const actions = document.createElement('div');
12988
- actions.style.cssText = 'display:flex;align-items:center;justify-content:space-between;gap:8px;margin-top:auto;';
13647
+ actions.style.cssText = 'display:flex;align-items:center;gap:6px;flex-wrap:wrap;margin-top:auto;';
12989
13648
  const status = document.createElement('span');
12990
13649
  status.textContent = connectedDevice
12991
13650
  ? (liveStreamActive ? 'Live' : (aiAssistEnabled ? `AI ${aiModel || 'ready'}` : 'Connected'))
@@ -13000,6 +13659,13 @@
13000
13659
  white-space: nowrap;
13001
13660
  `;
13002
13661
  actions.appendChild(status);
13662
+ if (deviceId) {
13663
+ const pinButton = createRemoteFleetButton('Pin', 'Pin this device as a canvas node', 'pin-device');
13664
+ pinButton.dataset.deviceId = deviceId;
13665
+ pinButton.style.height = '24px';
13666
+ pinButton.style.fontSize = '10px';
13667
+ actions.appendChild(pinButton);
13668
+ }
13003
13669
  if (connectedDevice && deviceId) {
13004
13670
  const focusButton = createRemoteFleetButton('Focus', 'Show this device in focused live panel', 'live-focus');
13005
13671
  focusButton.dataset.deviceId = deviceId;
@@ -13064,7 +13730,7 @@
13064
13730
  bodyView.appendChild(grid);
13065
13731
 
13066
13732
  const footer = document.createElement('div');
13067
- footer.textContent = `Endpoint ${endpoint} - all devices, no paging - ${autoMonitorState ? 'auto monitor' : 'manual'} - refreshed ${formatRemoteFleetAge(refreshedAt)}`;
13733
+ footer.textContent = `Endpoint ${endpoint} - all devices, no paging - group ${groupState} - ${autoMonitorState ? 'auto monitor' : 'manual'} - refreshed ${formatRemoteFleetAge(refreshedAt)}`;
13068
13734
  footer.style.cssText = `
13069
13735
  flex: 0 0 auto;
13070
13736
  color: #64748b;
@@ -13157,6 +13823,18 @@
13157
13823
  }
13158
13824
  });
13159
13825
 
13826
+ grid.querySelectorAll('[data-remote-fleet-group-header="true"]').forEach(header => {
13827
+ const key = String(header.dataset.remoteFleetGroupKey || '');
13828
+ const groupCards = getDeviceCards().filter(card => String(card.dataset.remoteFleetGroupKey || '') === key);
13829
+ const visibleCards = groupCards.filter(card => card.style.display !== 'none').length;
13830
+ header.style.display = visibleCards > 0 ? 'flex' : 'none';
13831
+ const countEl = header.querySelector('[data-remote-fleet-group-count="true"]');
13832
+ if (countEl) {
13833
+ countEl.textContent = `${visibleCards}/${groupCards.length}`;
13834
+ countEl.title = `${visibleCards} visible in this group, ${groupCards.length} total`;
13835
+ }
13836
+ });
13837
+
13160
13838
  const noMatch = grid.querySelector('[data-remote-fleet-no-match="true"]');
13161
13839
  if (noMatch) {
13162
13840
  noMatch.style.display = devices.length > 0 && visible === 0 ? 'flex' : 'none';
@@ -13182,11 +13860,12 @@
13182
13860
  bodyView.dataset.remoteFleetSearch = searchText;
13183
13861
  bodyView.dataset.remoteFleetFilter = filterMode;
13184
13862
  bodyView.dataset.remoteFleetSort = String(sortSelect.value || 'status');
13863
+ bodyView.dataset.remoteFleetGroup = String(groupSelect.value || 'none');
13185
13864
  bodyView.dataset.remoteFleetDensity = String(densitySelect.value || 'cards');
13186
13865
  bodyView.dataset.remoteFleetAiAssist = wantsAi ? 'true' : 'false';
13187
13866
  };
13188
13867
 
13189
- [searchInput, filterSelect, sortSelect, densitySelect, aiToggle].forEach(control => {
13868
+ [searchInput, filterSelect, sortSelect, groupSelect, densitySelect, aiToggle].forEach(control => {
13190
13869
  ['mousedown', 'mouseup', 'click', 'dblclick', 'keydown'].forEach(eventName => {
13191
13870
  control.addEventListener(eventName, event => event.stopPropagation());
13192
13871
  });
@@ -13208,6 +13887,10 @@
13208
13887
  bodyView.dataset.remoteFleetSort = String(sortSelect.value || 'status');
13209
13888
  renderRemoteFleetMonitor(bodyView, nodeModel);
13210
13889
  });
13890
+ groupSelect.addEventListener('change', () => {
13891
+ bodyView.dataset.remoteFleetGroup = String(groupSelect.value || 'none');
13892
+ renderRemoteFleetMonitor(bodyView, nodeModel);
13893
+ });
13211
13894
  densitySelect.addEventListener('change', () => {
13212
13895
  bodyView.dataset.remoteFleetDensity = String(densitySelect.value || 'cards');
13213
13896
  renderRemoteFleetMonitor(bodyView, nodeModel);
@@ -13283,6 +13966,28 @@
13283
13966
  }
13284
13967
  });
13285
13968
 
13969
+ grid.querySelectorAll('[data-remote-fleet-action="pin-device"]').forEach(button => {
13970
+ button.addEventListener('click', async event => {
13971
+ event.preventDefault();
13972
+ event.stopPropagation();
13973
+ const deviceId = String(button.dataset.deviceId || '').trim();
13974
+ if (!deviceId) return;
13975
+ button.disabled = true;
13976
+ setTaskFeedback('Pinning device to canvas...');
13977
+ try {
13978
+ const result = await invokeDotNetAsync('AddRemoteFleetDeviceNodeFromJs', nodeId, deviceId);
13979
+ await syncRemoteFleetNodeStateFromResult(result);
13980
+ if (result?.success) {
13981
+ setTaskFeedback('Pinned device node.', 'success');
13982
+ } else {
13983
+ setTaskFeedback(result?.error || 'Pin failed.', 'error');
13984
+ }
13985
+ } finally {
13986
+ button.disabled = false;
13987
+ }
13988
+ });
13989
+ });
13990
+
13286
13991
  bodyView.querySelectorAll('[data-remote-fleet-action="live-focus"]').forEach(button => {
13287
13992
  button.addEventListener('click', event => {
13288
13993
  event.preventDefault();
@@ -13649,7 +14354,7 @@
13649
14354
  const agentStyledMemo = isAgentStyledMemoNode(nodeModel);
13650
14355
  const mindCanvasAgent = isMindCanvasAgentNode(nodeModel);
13651
14356
  const businessAutomation = isBusinessAutomationNode(nodeModel);
13652
- const remoteFleet = isRemoteFleetMonitorNode(nodeModel);
14357
+ const remoteFleet = isRemoteFleetNode(nodeModel);
13653
14358
  const externalChrome = allowsMemoExternalChrome(nodeModel);
13654
14359
 
13655
14360
  const container = document.createElement('div');
@@ -13726,7 +14431,7 @@
13726
14431
 
13727
14432
  header.appendChild(iconWrap);
13728
14433
  header.appendChild(titleWrap);
13729
- if (agentStyledMemo && !businessAutomation) {
14434
+ if (agentStyledMemo && !businessAutomation && !remoteFleet) {
13730
14435
  const roleSelect = createMindCanvasAgentRoleSelect(nodeModel);
13731
14436
  header.appendChild(roleSelect);
13732
14437
  }
@@ -16793,7 +17498,7 @@
16793
17498
  const iconKey = getMemoIconKey(nodeModel);
16794
17499
  const agentStyledMemo = isAgentStyledMemoNode(nodeModel);
16795
17500
  const businessAutomation = isBusinessAutomationNode(nodeModel);
16796
- const remoteFleet = isRemoteFleetMonitorNode(nodeModel);
17501
+ const remoteFleet = isRemoteFleetNode(nodeModel);
16797
17502
  const externalChrome = allowsMemoExternalChrome(nodeModel);
16798
17503
 
16799
17504
  memoEl.classList.toggle('map-node-agent', agentStyledMemo);
@@ -16817,7 +17522,7 @@
16817
17522
  titleInput.value = nextTitle;
16818
17523
  }
16819
17524
  disableEditorAssist(titleInput);
16820
- if (businessAutomation) {
17525
+ if (businessAutomation || remoteFleet) {
16821
17526
  memoEl.querySelector('.map-node-memo__agent-role-select')?.remove();
16822
17527
  } else {
16823
17528
  updateMindCanvasAgentRoleSelect(memoEl, nodeModel);
@@ -57,6 +57,7 @@
57
57
 
58
58
  const BUSINESS_AUTOMATION_SEMANTIC_TYPE = 'BusinessAutomationNode';
59
59
  const REMOTE_FLEET_SEMANTIC_TYPE = 'RemoteFleetMonitor';
60
+ const REMOTE_FLEET_DEVICE_SEMANTIC_TYPE = 'RemoteFleetDevice';
60
61
  const AUTOMATION_INPUT_PINS_METADATA_KEY = 'AutomationInputPins';
61
62
  const AUTOMATION_OUTPUT_PINS_METADATA_KEY = 'AutomationOutputPins';
62
63
  const AUTOMATION_PIN_RAIL_WIDTH = 118;
@@ -103,7 +104,8 @@
103
104
  return semanticType === 'MindCanvasAgent'
104
105
  || semanticType === 'AgentCommand'
105
106
  || semanticType === BUSINESS_AUTOMATION_SEMANTIC_TYPE
106
- || semanticType === REMOTE_FLEET_SEMANTIC_TYPE;
107
+ || semanticType === REMOTE_FLEET_SEMANTIC_TYPE
108
+ || semanticType === REMOTE_FLEET_DEVICE_SEMANTIC_TYPE;
107
109
  }
108
110
 
109
111
  function isAgentNodeModel(nodeModel) {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "mainAssemblyName": "MindExecution.Web",
3
3
  "resources": {
4
- "hash": "sha256-XfKFSqnMBnbJmPnM+j3zriBOOdujMLDI/U8Usvww2jQ=",
4
+ "hash": "sha256-AfctgIeogWni4PMiafMrW/TiX1otsT+MSNhvCPr7EjA=",
5
5
  "fingerprinting": {
6
6
  "Google.Protobuf.9h59ukbel7.dll": "Google.Protobuf.dll",
7
7
  "Markdig.d1j7v41cl1.dll": "Markdig.dll",
@@ -127,12 +127,12 @@
127
127
  "MindExecution.Kernel.dt3w864bqn.dll": "MindExecution.Kernel.dll",
128
128
  "MindExecution.Plugins.Admin.z93cu32xru.dll": "MindExecution.Plugins.Admin.dll",
129
129
  "MindExecution.Plugins.Business.b6da8sg85t.dll": "MindExecution.Plugins.Business.dll",
130
- "MindExecution.Plugins.Concept.mjooiqft9j.dll": "MindExecution.Plugins.Concept.dll",
130
+ "MindExecution.Plugins.Concept.sj3ujw4e72.dll": "MindExecution.Plugins.Concept.dll",
131
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",
132
+ "MindExecution.Plugins.PlanMaster.ec8ccog2o3.dll": "MindExecution.Plugins.PlanMaster.dll",
133
+ "MindExecution.Plugins.YouTube.mk7a07org5.dll": "MindExecution.Plugins.YouTube.dll",
134
+ "MindExecution.Shared.mc9gi9kvqg.dll": "MindExecution.Shared.dll",
135
+ "MindExecution.Web.n70z8m41l3.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",
@@ -280,16 +280,16 @@
280
280
  "netstandard.0xet7jg7ky.dll": "sha256-xENDv620uJ8fHwLJ2bdhrTHz4QPjvqXOztnk2a4wr0c=",
281
281
  "MindExecution.Core.pnw79cgqjx.dll": "sha256-64VJ+VCnq+r7lgfYzm99hPX3yo5u2QCC4guGqsrVTus=",
282
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="
283
+ "MindExecution.Plugins.Concept.sj3ujw4e72.dll": "sha256-eKBzfvFolc9FJhorB30L48rhIU4Ue/8vt1QO2wvOT0w=",
284
+ "MindExecution.Plugins.PlanMaster.ec8ccog2o3.dll": "sha256-PiE9Rs3TkNAWvj38n/uaLil6xi2eRYeFUTQhDsdiuA4=",
285
+ "MindExecution.Shared.mc9gi9kvqg.dll": "sha256-qlYWbNOfw5WCQm9YXBQj3DT2pPJcHtDxdeQ/99jrpTs=",
286
+ "MindExecution.Web.n70z8m41l3.dll": "sha256-04vr/dzHUHxf++L+m7zQsKAtV3khH9yNrcl4CouSppw="
287
287
  },
288
288
  "lazyAssembly": {
289
289
  "MindExecution.Plugins.Admin.z93cu32xru.dll": "sha256-aToPZeovhBXkB0URHkzYdYDAmg1+HxsvojgiefL4pkY=",
290
290
  "MindExecution.Plugins.Business.b6da8sg85t.dll": "sha256-M1macJ1ZqfBCrj/1WPw2JQxnFtFX31HxydBxQKa3Cgw=",
291
291
  "MindExecution.Plugins.Directory.rjod6rdmly.dll": "sha256-hx8N04/7sIYB8PdHSyVTLhPfPb7ZkmqM1RPBB1J0iuw=",
292
- "MindExecution.Plugins.YouTube.k75qxhbpp8.dll": "sha256-IM43bEjZvj6SqvBrKl8JmAN5NfN6IW8VPi3Z/HarwlQ="
292
+ "MindExecution.Plugins.YouTube.mk7a07org5.dll": "sha256-1gBQNQ4XkMp71HDUpy2X4q/IG5lrrMhuhwPdASIp0TA="
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-auto-monitor-v468';
561
+ const scriptVersion = '20260612-remote-device-pin-v470';
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": "ok6mf/2a",
2
+ "version": "x1hfM46o",
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-9f7DLlG6ShPyYhu/Db97YKVoFr+RhJC2Ko9E4NsL9Lc=",
89
+ "hash": "sha256-LSO5/TRH4+1bNtJc7X09UZHrFfaIF1SkX012PLDVndg=",
90
90
  "url": "_content/MindExecution.Shared/js/mind-map-css3d-manager.js"
91
91
  },
92
92
  {
@@ -134,7 +134,7 @@
134
134
  "url": "_content/MindExecution.Shared/js/mind-map-node-search-worker.js"
135
135
  },
136
136
  {
137
- "hash": "sha256-UNYwysmH7qa2H0uMg1M0+FbzNvkxVnOUsOTtDVCc4b4=",
137
+ "hash": "sha256-VTAgwmg02CIzBkl/jETeQ45ZTIx/QEfxdeeMnklK2xI=",
138
138
  "url": "_content/MindExecution.Shared/js/mind-map-nodes.js"
139
139
  },
140
140
  {
@@ -426,28 +426,28 @@
426
426
  "url": "_framework/MindExecution.Plugins.Business.b6da8sg85t.dll"
427
427
  },
428
428
  {
429
- "hash": "sha256-Qrz35qwSLDAaugzQMJ6YLfWI+jld56cCo+9wOwVobgw=",
430
- "url": "_framework/MindExecution.Plugins.Concept.mjooiqft9j.dll"
429
+ "hash": "sha256-eKBzfvFolc9FJhorB30L48rhIU4Ue/8vt1QO2wvOT0w=",
430
+ "url": "_framework/MindExecution.Plugins.Concept.sj3ujw4e72.dll"
431
431
  },
432
432
  {
433
433
  "hash": "sha256-hx8N04/7sIYB8PdHSyVTLhPfPb7ZkmqM1RPBB1J0iuw=",
434
434
  "url": "_framework/MindExecution.Plugins.Directory.rjod6rdmly.dll"
435
435
  },
436
436
  {
437
- "hash": "sha256-VZ7SUoDTHQ9MdwII/dBqeaVFpDt6UEFDV5xJHAlskgM=",
438
- "url": "_framework/MindExecution.Plugins.PlanMaster.1dcrzhsegj.dll"
437
+ "hash": "sha256-PiE9Rs3TkNAWvj38n/uaLil6xi2eRYeFUTQhDsdiuA4=",
438
+ "url": "_framework/MindExecution.Plugins.PlanMaster.ec8ccog2o3.dll"
439
439
  },
440
440
  {
441
- "hash": "sha256-IM43bEjZvj6SqvBrKl8JmAN5NfN6IW8VPi3Z/HarwlQ=",
442
- "url": "_framework/MindExecution.Plugins.YouTube.k75qxhbpp8.dll"
441
+ "hash": "sha256-1gBQNQ4XkMp71HDUpy2X4q/IG5lrrMhuhwPdASIp0TA=",
442
+ "url": "_framework/MindExecution.Plugins.YouTube.mk7a07org5.dll"
443
443
  },
444
444
  {
445
- "hash": "sha256-EnE/FEgfwb2Trt9cyLvVEeHE8iSRWYhaftVJUOKLAZI=",
446
- "url": "_framework/MindExecution.Shared.y3eqxd3mvo.dll"
445
+ "hash": "sha256-qlYWbNOfw5WCQm9YXBQj3DT2pPJcHtDxdeQ/99jrpTs=",
446
+ "url": "_framework/MindExecution.Shared.mc9gi9kvqg.dll"
447
447
  },
448
448
  {
449
- "hash": "sha256-YENKxQvH07LxcUCqbFITQtVD2sCcdYNAF9aX0YVtVGs=",
450
- "url": "_framework/MindExecution.Web.wou9x6mn2f.dll"
449
+ "hash": "sha256-04vr/dzHUHxf++L+m7zQsKAtV3khH9yNrcl4CouSppw=",
450
+ "url": "_framework/MindExecution.Web.n70z8m41l3.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-10lI3q/nXAGFdelS8ribQw72sOgD3PYExvQX6srRqWA=",
773
+ "hash": "sha256-T6ZFGjsJ+b+J0Je7Wr8VCbBYyPPOsuCXwkoNQny1pak=",
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-e/vzCKU0rJNUBs7+YbA+jxOZd53T9CbUxhBAJ0yi6oM=",
837
+ "hash": "sha256-JxMVTLs/BdFFsgJ34YMkq3lh37rBZLnvhbj3f+MxRYo=",
838
838
  "url": "index.html"
839
839
  },
840
840
  {
@@ -1,4 +1,4 @@
1
- /* Manifest version: ok6mf/2a */
1
+ /* Manifest version: x1hfM46o */
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