@mindexec/cli 0.2.2 → 0.2.3

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 (24) hide show
  1. package/README.md +38 -0
  2. package/package.json +6 -4
  3. package/remote-hub.js +571 -0
  4. package/scripts/remote-hub-smoke.mjs +117 -0
  5. package/server.js +108 -28
  6. package/wwwroot/_content/MindExecution.Shared/js/mind-map-core.js +11 -0
  7. package/wwwroot/_content/MindExecution.Shared/js/mind-map-css3d-manager.js +550 -2
  8. package/wwwroot/_content/MindExecution.Shared/js/mind-map-nodes.js +3 -1
  9. package/wwwroot/_framework/MindExecution.Core.5luow1xgjs.dll +0 -0
  10. package/wwwroot/_framework/{MindExecution.Kernel.gwwc40sc45.dll → MindExecution.Kernel.mot9nj6bzm.dll} +0 -0
  11. package/wwwroot/_framework/{MindExecution.Plugins.Admin.0jgrn1sckv.dll → MindExecution.Plugins.Admin.x9v2drg2f7.dll} +0 -0
  12. package/wwwroot/_framework/{MindExecution.Plugins.Business.13mme2qcag.dll → MindExecution.Plugins.Business.b0kjoyx31x.dll} +0 -0
  13. package/wwwroot/_framework/{MindExecution.Plugins.Concept.9al2g3v3f9.dll → MindExecution.Plugins.Concept.6tojojgh1a.dll} +0 -0
  14. package/wwwroot/_framework/{MindExecution.Plugins.Directory.3w4t6n3se0.dll → MindExecution.Plugins.Directory.fqtbuqadsx.dll} +0 -0
  15. package/wwwroot/_framework/{MindExecution.Plugins.PlanMaster.vfmfbygv5y.dll → MindExecution.Plugins.PlanMaster.j7llfeae6l.dll} +0 -0
  16. package/wwwroot/_framework/{MindExecution.Plugins.YouTube.32jyiqs383.dll → MindExecution.Plugins.YouTube.yo5fwdhugr.dll} +0 -0
  17. package/wwwroot/_framework/{MindExecution.Shared.7ttmykvopx.dll → MindExecution.Shared.0qi7vbn9a4.dll} +0 -0
  18. package/wwwroot/_framework/MindExecution.Web.6cv7ad7rik.dll +0 -0
  19. package/wwwroot/_framework/blazor.boot.json +21 -21
  20. package/wwwroot/index.html +1 -1
  21. package/wwwroot/service-worker-assets.js +26 -26
  22. package/wwwroot/service-worker.js +1 -1
  23. package/wwwroot/_framework/MindExecution.Core.1q1trifbuu.dll +0 -0
  24. package/wwwroot/_framework/MindExecution.Web.ozzcqp30uy.dll +0 -0
@@ -3400,6 +3400,7 @@
3400
3400
  const MEDIA_GENERATION_STATUS_METADATA_KEY = 'MediaGenerationStatus';
3401
3401
  const MEDIA_GENERATION_STATUS_MESSAGE_METADATA_KEY = 'MediaGenerationStatusMessage';
3402
3402
  const BUSINESS_AUTOMATION_SEMANTIC_TYPE = 'BusinessAutomationNode';
3403
+ const REMOTE_FLEET_SEMANTIC_TYPE = 'RemoteFleetMonitor';
3403
3404
  const AUTOMATION_NODE_KIND_METADATA_KEY = 'AutomationNodeKind';
3404
3405
  const AUTOMATION_NODE_LABEL_METADATA_KEY = 'AutomationNodeLabel';
3405
3406
  const AUTOMATION_NODE_DESCRIPTION_METADATA_KEY = 'AutomationNodeDescription';
@@ -3569,6 +3570,10 @@
3569
3570
  return getNodeSemanticType(nodeModel) === BUSINESS_AUTOMATION_SEMANTIC_TYPE;
3570
3571
  }
3571
3572
 
3573
+ function isRemoteFleetMonitorNode(nodeModel) {
3574
+ return getNodeSemanticType(nodeModel) === REMOTE_FLEET_SEMANTIC_TYPE;
3575
+ }
3576
+
3572
3577
  function isBusinessAutomationContextSourceNode(nodeModel) {
3573
3578
  if (!nodeModel || isBusinessAutomationNode(nodeModel)) {
3574
3579
  return false;
@@ -3582,7 +3587,8 @@
3582
3587
  const semanticType = getNodeSemanticType(nodeModel);
3583
3588
  return semanticType === 'MindCanvasAgent'
3584
3589
  || semanticType === 'AgentCommand'
3585
- || semanticType === BUSINESS_AUTOMATION_SEMANTIC_TYPE;
3590
+ || semanticType === BUSINESS_AUTOMATION_SEMANTIC_TYPE
3591
+ || semanticType === REMOTE_FLEET_SEMANTIC_TYPE;
3586
3592
  }
3587
3593
 
3588
3594
  function allowsMemoExternalChrome(nodeModel) {
@@ -11834,6 +11840,18 @@
11834
11840
  function renderMemoBodyView(bodyView, nodeModel, content) {
11835
11841
  if (!bodyView) return;
11836
11842
 
11843
+ if (isRemoteFleetMonitorNode(nodeModel)) {
11844
+ renderRemoteFleetMonitor(bodyView, nodeModel);
11845
+ return;
11846
+ }
11847
+
11848
+ bodyView.classList.remove('map-node-remote-fleet__body');
11849
+ bodyView.style.cssText = `
11850
+ flex: 1 1 auto;
11851
+ min-height: 0;
11852
+ pointer-events: auto;
11853
+ `;
11854
+
11837
11855
  const nextValue = String(content || '');
11838
11856
  bodyView.dataset.src = nextValue;
11839
11857
 
@@ -11916,6 +11934,533 @@
11916
11934
  }, 220);
11917
11935
  }
11918
11936
 
11937
+ function getRemoteFleetMetadataValue(nodeModel, key, fallback = '') {
11938
+ const metadata = getNodeMetadata(nodeModel) || {};
11939
+ const value = metadata[key];
11940
+ return value === undefined || value === null ? fallback : String(value);
11941
+ }
11942
+
11943
+ function parseRemoteFleetDevices(nodeModel) {
11944
+ const raw = getRemoteFleetMetadataValue(nodeModel, 'RemoteFleetDevicesJson', '[]');
11945
+ if (!raw.trim()) {
11946
+ return [];
11947
+ }
11948
+
11949
+ try {
11950
+ const parsed = JSON.parse(raw);
11951
+ return Array.isArray(parsed) ? parsed : [];
11952
+ } catch {
11953
+ return [];
11954
+ }
11955
+ }
11956
+
11957
+ function formatRemoteFleetNumber(value, digits = 0) {
11958
+ const number = Number(value);
11959
+ return Number.isFinite(number) ? number.toFixed(digits) : '-';
11960
+ }
11961
+
11962
+ function formatRemoteFleetPercent(value) {
11963
+ const number = Number(value);
11964
+ if (!Number.isFinite(number)) {
11965
+ return '-';
11966
+ }
11967
+
11968
+ return `${Math.round(Math.max(0, Math.min(1, number)) * 100)}%`;
11969
+ }
11970
+
11971
+ function formatRemoteFleetDuration(seconds) {
11972
+ const value = Number(seconds);
11973
+ if (!Number.isFinite(value) || value <= 0) {
11974
+ return '-';
11975
+ }
11976
+
11977
+ const days = Math.floor(value / 86400);
11978
+ const hours = Math.floor((value % 86400) / 3600);
11979
+ const minutes = Math.floor((value % 3600) / 60);
11980
+ if (days > 0) {
11981
+ return `${days}d ${hours}h`;
11982
+ }
11983
+ if (hours > 0) {
11984
+ return `${hours}h ${minutes}m`;
11985
+ }
11986
+ return `${minutes}m`;
11987
+ }
11988
+
11989
+ function formatRemoteFleetAge(value) {
11990
+ const timestamp = Date.parse(String(value || ''));
11991
+ if (!Number.isFinite(timestamp)) {
11992
+ return '-';
11993
+ }
11994
+
11995
+ const seconds = Math.max(0, Math.round((Date.now() - timestamp) / 1000));
11996
+ if (seconds < 60) {
11997
+ return 'now';
11998
+ }
11999
+ if (seconds < 3600) {
12000
+ return `${Math.floor(seconds / 60)}m ago`;
12001
+ }
12002
+ if (seconds < 86400) {
12003
+ return `${Math.floor(seconds / 3600)}h ago`;
12004
+ }
12005
+ return `${Math.floor(seconds / 86400)}d ago`;
12006
+ }
12007
+
12008
+ function createRemoteFleetStat(label, value, tone = 'default') {
12009
+ const item = document.createElement('div');
12010
+ item.style.cssText = `
12011
+ display: flex;
12012
+ flex-direction: column;
12013
+ gap: 2px;
12014
+ min-width: 0;
12015
+ padding: 8px 10px;
12016
+ border-radius: 8px;
12017
+ background: ${tone === 'online' ? 'rgba(16, 185, 129, 0.10)' : 'rgba(15, 23, 42, 0.05)'};
12018
+ border: 1px solid ${tone === 'online' ? 'rgba(16, 185, 129, 0.22)' : 'rgba(148, 163, 184, 0.22)'};
12019
+ `;
12020
+
12021
+ const labelEl = document.createElement('span');
12022
+ labelEl.textContent = label;
12023
+ labelEl.style.cssText = `
12024
+ color: #64748b;
12025
+ font-size: 10px;
12026
+ font-weight: 800;
12027
+ letter-spacing: 0;
12028
+ text-transform: uppercase;
12029
+ `;
12030
+
12031
+ const valueEl = document.createElement('strong');
12032
+ valueEl.textContent = value;
12033
+ valueEl.style.cssText = `
12034
+ color: ${tone === 'online' ? '#047857' : '#0f172a'};
12035
+ font-size: 18px;
12036
+ line-height: 1.1;
12037
+ font-weight: 900;
12038
+ letter-spacing: 0;
12039
+ overflow: hidden;
12040
+ text-overflow: ellipsis;
12041
+ white-space: nowrap;
12042
+ `;
12043
+
12044
+ item.appendChild(labelEl);
12045
+ item.appendChild(valueEl);
12046
+ return item;
12047
+ }
12048
+
12049
+ function createRemoteFleetButton(label, title, action) {
12050
+ const button = document.createElement('button');
12051
+ button.type = 'button';
12052
+ button.dataset.remoteFleetAction = action;
12053
+ button.textContent = label;
12054
+ button.title = title || label;
12055
+ button.setAttribute('aria-label', title || label);
12056
+ button.style.cssText = `
12057
+ display: inline-flex;
12058
+ align-items: center;
12059
+ justify-content: center;
12060
+ min-width: 0;
12061
+ height: 28px;
12062
+ padding: 0 10px;
12063
+ border-radius: 7px;
12064
+ border: 1px solid rgba(37, 99, 235, 0.32);
12065
+ background: #ffffff;
12066
+ color: #1d4ed8;
12067
+ font-size: 11px;
12068
+ font-weight: 800;
12069
+ letter-spacing: 0;
12070
+ cursor: pointer;
12071
+ pointer-events: auto;
12072
+ `;
12073
+ return button;
12074
+ }
12075
+
12076
+ async function syncRemoteFleetNodeStateFromResult(result) {
12077
+ const nodeState = result?.nodeState || result?.refresh?.nodeState;
12078
+ if (nodeState && window.mindMap?.syncNodeStates) {
12079
+ await window.mindMap.syncNodeStates([nodeState]);
12080
+ }
12081
+ }
12082
+
12083
+ function renderRemoteFleetMonitor(bodyView, nodeModel) {
12084
+ if (!bodyView) return;
12085
+
12086
+ const nodeId = String(nodeModel?.id ?? nodeModel?.Id ?? '');
12087
+ const devices = parseRemoteFleetDevices(nodeModel);
12088
+ const total = Number(getRemoteFleetMetadataValue(nodeModel, 'RemoteFleetDeviceCount', devices.length));
12089
+ const connected = Number(getRemoteFleetMetadataValue(
12090
+ nodeModel,
12091
+ 'RemoteFleetConnectedDeviceCount',
12092
+ devices.filter(device => device?.connected === true || device?.Connected === true).length));
12093
+ const endpoint = getRemoteFleetMetadataValue(nodeModel, 'RemoteFleetHubEndpoint', '127.0.0.1:5197');
12094
+ const command = getRemoteFleetMetadataValue(
12095
+ nodeModel,
12096
+ 'RemoteFleetConnectCommand',
12097
+ `npx @mindexec/remote connect --manager ${endpoint} --pair <pair-token>`);
12098
+ const hubStatus = getRemoteFleetMetadataValue(nodeModel, 'RemoteFleetHubStatus', 'offline');
12099
+ const refreshedAt = getRemoteFleetMetadataValue(nodeModel, 'RemoteFleetLastRefreshAtUtc', '');
12100
+ const lastError = getRemoteFleetMetadataValue(nodeModel, 'RemoteFleetLastError', '');
12101
+
12102
+ bodyView.dataset.src = `remote-fleet:${refreshedAt}:${devices.length}:${connected}`;
12103
+ bodyView.classList.add('map-node-remote-fleet__body');
12104
+ bodyView.innerHTML = '';
12105
+ bodyView.style.cssText = `
12106
+ flex: 1 1 auto;
12107
+ min-height: 0;
12108
+ pointer-events: auto;
12109
+ display: flex;
12110
+ flex-direction: column;
12111
+ gap: 10px;
12112
+ padding: 12px 14px 14px;
12113
+ overflow: hidden;
12114
+ background: linear-gradient(180deg, rgba(248, 250, 252, 0.96), rgba(241, 245, 249, 0.92));
12115
+ `;
12116
+
12117
+ const top = document.createElement('div');
12118
+ top.style.cssText = `
12119
+ display: grid;
12120
+ grid-template-columns: repeat(4, minmax(0, 1fr));
12121
+ gap: 8px;
12122
+ flex: 0 0 auto;
12123
+ `;
12124
+ top.appendChild(createRemoteFleetStat('Hub', hubStatus === 'online' ? 'Online' : 'Offline', hubStatus === 'online' ? 'online' : 'default'));
12125
+ 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'));
12128
+ bodyView.appendChild(top);
12129
+
12130
+ const commandRow = document.createElement('div');
12131
+ commandRow.style.cssText = `
12132
+ display: grid;
12133
+ grid-template-columns: minmax(0, 1fr) auto auto;
12134
+ gap: 8px;
12135
+ align-items: center;
12136
+ flex: 0 0 auto;
12137
+ `;
12138
+
12139
+ const commandText = document.createElement('code');
12140
+ commandText.textContent = command;
12141
+ commandText.style.cssText = `
12142
+ min-width: 0;
12143
+ overflow: hidden;
12144
+ text-overflow: ellipsis;
12145
+ white-space: nowrap;
12146
+ padding: 8px 10px;
12147
+ border-radius: 7px;
12148
+ background: rgba(15, 23, 42, 0.92);
12149
+ color: #e2e8f0;
12150
+ font-family: ui-monospace, SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace;
12151
+ font-size: 11px;
12152
+ line-height: 1.2;
12153
+ `;
12154
+ commandText.title = command;
12155
+
12156
+ const copyButton = createRemoteFleetButton('Copy', 'Copy agent command', 'copy-command');
12157
+ const refreshButton = createRemoteFleetButton('Refresh', 'Refresh remote devices', 'refresh');
12158
+ commandRow.appendChild(commandText);
12159
+ commandRow.appendChild(copyButton);
12160
+ commandRow.appendChild(refreshButton);
12161
+ bodyView.appendChild(commandRow);
12162
+
12163
+ if (lastError.trim()) {
12164
+ const errorEl = document.createElement('div');
12165
+ errorEl.textContent = lastError;
12166
+ errorEl.style.cssText = `
12167
+ flex: 0 0 auto;
12168
+ padding: 8px 10px;
12169
+ border-radius: 7px;
12170
+ background: rgba(248, 113, 113, 0.12);
12171
+ color: #991b1b;
12172
+ font-size: 12px;
12173
+ line-height: 1.35;
12174
+ font-weight: 700;
12175
+ overflow-wrap: break-word;
12176
+ `;
12177
+ bodyView.appendChild(errorEl);
12178
+ }
12179
+
12180
+ const grid = document.createElement('div');
12181
+ grid.style.cssText = `
12182
+ flex: 1 1 auto;
12183
+ min-height: 0;
12184
+ overflow-y: auto;
12185
+ overflow-x: hidden;
12186
+ display: grid;
12187
+ grid-template-columns: repeat(auto-fill, minmax(168px, 1fr));
12188
+ align-content: start;
12189
+ gap: 8px;
12190
+ padding-right: 4px;
12191
+ `;
12192
+
12193
+ if (devices.length === 0) {
12194
+ const empty = document.createElement('div');
12195
+ empty.textContent = 'No devices connected yet.';
12196
+ empty.style.cssText = `
12197
+ grid-column: 1 / -1;
12198
+ display: flex;
12199
+ align-items: center;
12200
+ min-height: 94px;
12201
+ padding: 14px;
12202
+ border-radius: 8px;
12203
+ border: 1px dashed rgba(100, 116, 139, 0.36);
12204
+ color: #475569;
12205
+ font-size: 13px;
12206
+ font-weight: 800;
12207
+ background: rgba(255, 255, 255, 0.74);
12208
+ `;
12209
+ grid.appendChild(empty);
12210
+ } else {
12211
+ devices.forEach(device => {
12212
+ const connectedDevice = device?.connected === true || device?.Connected === true;
12213
+ const name = String(device?.name || device?.Name || device?.hostname || device?.Hostname || device?.deviceId || device?.DeviceId || 'device');
12214
+ const platform = [device?.platform || device?.Platform, device?.arch || device?.Arch]
12215
+ .filter(Boolean)
12216
+ .join(' / ') || 'unknown';
12217
+ const release = String(device?.release || device?.Release || '');
12218
+ const deviceId = String(device?.deviceId || device?.DeviceId || '');
12219
+ const thumbnailEnabled = device?.thumbnailEnabled === true || device?.ThumbnailEnabled === true;
12220
+ const thumbnailDataUrl = String(device?.thumbnailDataUrl || device?.ThumbnailDataUrl || '');
12221
+ const thumbnailCapturedAt = String(device?.thumbnailCapturedAt || device?.ThumbnailCapturedAt || '');
12222
+ const hasThumbnail = /^data:image\/(png|jpe?g|webp|svg\+xml);base64,/i.test(thumbnailDataUrl);
12223
+ const card = document.createElement('article');
12224
+ card.dataset.deviceId = deviceId;
12225
+ card.style.cssText = `
12226
+ display: flex;
12227
+ flex-direction: column;
12228
+ gap: 8px;
12229
+ min-width: 0;
12230
+ min-height: 134px;
12231
+ padding: 10px;
12232
+ border-radius: 8px;
12233
+ background: #ffffff;
12234
+ border: 1px solid ${connectedDevice ? 'rgba(16, 185, 129, 0.34)' : 'rgba(148, 163, 184, 0.28)'};
12235
+ box-shadow: 0 8px 20px rgba(15, 23, 42, 0.06);
12236
+ `;
12237
+
12238
+ const preview = document.createElement('div');
12239
+ preview.style.cssText = `
12240
+ position: relative;
12241
+ width: 100%;
12242
+ aspect-ratio: 16 / 9;
12243
+ overflow: hidden;
12244
+ border-radius: 7px;
12245
+ background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%);
12246
+ border: 1px solid rgba(15, 23, 42, 0.12);
12247
+ `;
12248
+ if (hasThumbnail) {
12249
+ const image = document.createElement('img');
12250
+ image.src = thumbnailDataUrl;
12251
+ image.alt = `${name} thumbnail`;
12252
+ image.loading = 'lazy';
12253
+ image.decoding = 'async';
12254
+ image.style.cssText = `
12255
+ width: 100%;
12256
+ height: 100%;
12257
+ object-fit: cover;
12258
+ display: block;
12259
+ `;
12260
+ preview.appendChild(image);
12261
+ } else {
12262
+ const placeholder = document.createElement('div');
12263
+ placeholder.textContent = thumbnailEnabled ? 'No frame yet' : 'Status only';
12264
+ placeholder.style.cssText = `
12265
+ position: absolute;
12266
+ inset: 0;
12267
+ display: flex;
12268
+ align-items: center;
12269
+ justify-content: center;
12270
+ color: rgba(226, 232, 240, 0.78);
12271
+ font-size: 11px;
12272
+ font-weight: 900;
12273
+ letter-spacing: 0;
12274
+ `;
12275
+ preview.appendChild(placeholder);
12276
+ }
12277
+ if (thumbnailCapturedAt) {
12278
+ const badge = document.createElement('span');
12279
+ badge.textContent = formatRemoteFleetAge(thumbnailCapturedAt);
12280
+ badge.style.cssText = `
12281
+ position: absolute;
12282
+ right: 6px;
12283
+ bottom: 6px;
12284
+ max-width: calc(100% - 12px);
12285
+ padding: 3px 6px;
12286
+ border-radius: 999px;
12287
+ background: rgba(15, 23, 42, 0.74);
12288
+ color: #e2e8f0;
12289
+ font-size: 9px;
12290
+ font-weight: 900;
12291
+ line-height: 1;
12292
+ overflow: hidden;
12293
+ text-overflow: ellipsis;
12294
+ white-space: nowrap;
12295
+ `;
12296
+ preview.appendChild(badge);
12297
+ }
12298
+ card.appendChild(preview);
12299
+
12300
+ const cardHeader = document.createElement('div');
12301
+ cardHeader.style.cssText = 'display:flex;align-items:flex-start;gap:8px;min-width:0;';
12302
+ const dot = document.createElement('span');
12303
+ dot.style.cssText = `
12304
+ flex: 0 0 auto;
12305
+ width: 9px;
12306
+ height: 9px;
12307
+ margin-top: 4px;
12308
+ border-radius: 999px;
12309
+ background: ${connectedDevice ? '#10b981' : '#94a3b8'};
12310
+ box-shadow: 0 0 0 4px ${connectedDevice ? 'rgba(16,185,129,0.14)' : 'rgba(148,163,184,0.14)'};
12311
+ `;
12312
+
12313
+ const titleBox = document.createElement('div');
12314
+ titleBox.style.cssText = 'min-width:0;display:flex;flex-direction:column;gap:2px;';
12315
+ const title = document.createElement('strong');
12316
+ title.textContent = name;
12317
+ title.title = name;
12318
+ title.style.cssText = `
12319
+ color: #0f172a;
12320
+ font-size: 13px;
12321
+ font-weight: 900;
12322
+ line-height: 1.2;
12323
+ overflow: hidden;
12324
+ text-overflow: ellipsis;
12325
+ white-space: nowrap;
12326
+ letter-spacing: 0;
12327
+ `;
12328
+ const subtitle = document.createElement('span');
12329
+ subtitle.textContent = release ? `${platform} ${release}` : platform;
12330
+ subtitle.title = subtitle.textContent;
12331
+ subtitle.style.cssText = `
12332
+ color: #64748b;
12333
+ font-size: 11px;
12334
+ line-height: 1.2;
12335
+ overflow: hidden;
12336
+ text-overflow: ellipsis;
12337
+ white-space: nowrap;
12338
+ letter-spacing: 0;
12339
+ `;
12340
+ titleBox.appendChild(title);
12341
+ titleBox.appendChild(subtitle);
12342
+ cardHeader.appendChild(dot);
12343
+ cardHeader.appendChild(titleBox);
12344
+ card.appendChild(cardHeader);
12345
+
12346
+ const metrics = document.createElement('div');
12347
+ metrics.style.cssText = `
12348
+ display: grid;
12349
+ grid-template-columns: repeat(2, minmax(0, 1fr));
12350
+ gap: 6px;
12351
+ `;
12352
+ const addMetric = (label, value) => {
12353
+ const metric = document.createElement('div');
12354
+ metric.style.cssText = `
12355
+ min-width: 0;
12356
+ padding: 6px 7px;
12357
+ border-radius: 7px;
12358
+ background: rgba(241, 245, 249, 0.84);
12359
+ `;
12360
+ const labelEl = document.createElement('div');
12361
+ labelEl.textContent = label;
12362
+ labelEl.style.cssText = 'color:#64748b;font-size:9px;font-weight:800;letter-spacing:0;text-transform:uppercase;';
12363
+ const valueEl = document.createElement('div');
12364
+ valueEl.textContent = value;
12365
+ valueEl.title = value;
12366
+ valueEl.style.cssText = 'color:#0f172a;font-size:12px;font-weight:900;letter-spacing:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;';
12367
+ metric.appendChild(labelEl);
12368
+ metric.appendChild(valueEl);
12369
+ metrics.appendChild(metric);
12370
+ };
12371
+ addMetric('Seen', formatRemoteFleetAge(device?.lastSeenAt || device?.LastSeenAt));
12372
+ addMetric('Uptime', formatRemoteFleetDuration(device?.uptimeSec ?? device?.UptimeSec));
12373
+ addMetric('Mem', formatRemoteFleetPercent(device?.usedMemRatio ?? device?.UsedMemRatio));
12374
+ addMetric('Load', formatRemoteFleetNumber(device?.load1 ?? device?.Load1, 2));
12375
+ card.appendChild(metrics);
12376
+
12377
+ const actions = document.createElement('div');
12378
+ actions.style.cssText = 'display:flex;align-items:center;justify-content:space-between;gap:8px;margin-top:auto;';
12379
+ const status = document.createElement('span');
12380
+ status.textContent = connectedDevice ? 'Connected' : 'Offline';
12381
+ status.style.cssText = `
12382
+ min-width: 0;
12383
+ color: ${connectedDevice ? '#047857' : '#64748b'};
12384
+ font-size: 11px;
12385
+ font-weight: 900;
12386
+ overflow: hidden;
12387
+ text-overflow: ellipsis;
12388
+ white-space: nowrap;
12389
+ `;
12390
+ actions.appendChild(status);
12391
+ if (connectedDevice && deviceId) {
12392
+ if (thumbnailEnabled) {
12393
+ const thumbnailButton = createRemoteFleetButton('Shot', 'Request thumbnail', 'thumbnail-device');
12394
+ thumbnailButton.dataset.deviceId = deviceId;
12395
+ thumbnailButton.style.height = '24px';
12396
+ thumbnailButton.style.fontSize = '10px';
12397
+ actions.appendChild(thumbnailButton);
12398
+ }
12399
+ const pingButton = createRemoteFleetButton('Ping', 'Ping device', 'ping-device');
12400
+ pingButton.dataset.deviceId = deviceId;
12401
+ pingButton.style.height = '24px';
12402
+ pingButton.style.fontSize = '10px';
12403
+ actions.appendChild(pingButton);
12404
+ }
12405
+ card.appendChild(actions);
12406
+ grid.appendChild(card);
12407
+ });
12408
+ }
12409
+
12410
+ bodyView.appendChild(grid);
12411
+
12412
+ const footer = document.createElement('div');
12413
+ footer.textContent = `Endpoint ${endpoint} · refreshed ${formatRemoteFleetAge(refreshedAt)}`;
12414
+ footer.style.cssText = `
12415
+ flex: 0 0 auto;
12416
+ color: #64748b;
12417
+ font-size: 11px;
12418
+ font-weight: 700;
12419
+ line-height: 1.2;
12420
+ overflow: hidden;
12421
+ text-overflow: ellipsis;
12422
+ white-space: nowrap;
12423
+ `;
12424
+ bodyView.appendChild(footer);
12425
+
12426
+ copyButton.addEventListener('click', event => {
12427
+ event.preventDefault();
12428
+ event.stopPropagation();
12429
+ navigator.clipboard?.writeText?.(command).catch(() => { });
12430
+ });
12431
+
12432
+ refreshButton.addEventListener('click', async event => {
12433
+ event.preventDefault();
12434
+ event.stopPropagation();
12435
+ refreshButton.disabled = true;
12436
+ const result = await invokeDotNetAsync('RefreshRemoteFleetMonitorNodeFromJs', nodeId);
12437
+ await syncRemoteFleetNodeStateFromResult(result);
12438
+ refreshButton.disabled = false;
12439
+ });
12440
+
12441
+ grid.querySelectorAll('[data-remote-fleet-action="ping-device"]').forEach(button => {
12442
+ button.addEventListener('click', async event => {
12443
+ event.preventDefault();
12444
+ event.stopPropagation();
12445
+ button.disabled = true;
12446
+ const result = await invokeDotNetAsync('PingRemoteFleetDeviceFromJs', nodeId, button.dataset.deviceId || '');
12447
+ await syncRemoteFleetNodeStateFromResult(result);
12448
+ button.disabled = false;
12449
+ });
12450
+ });
12451
+
12452
+ grid.querySelectorAll('[data-remote-fleet-action="thumbnail-device"]').forEach(button => {
12453
+ button.addEventListener('click', async event => {
12454
+ event.preventDefault();
12455
+ event.stopPropagation();
12456
+ button.disabled = true;
12457
+ const result = await invokeDotNetAsync('RequestRemoteFleetThumbnailFromJs', nodeId, button.dataset.deviceId || '');
12458
+ await syncRemoteFleetNodeStateFromResult(result);
12459
+ button.disabled = false;
12460
+ });
12461
+ });
12462
+ }
12463
+
11919
12464
  function bindMemoNodeEvents(container, nodeModel) {
11920
12465
  if (!container || !nodeModel) return;
11921
12466
  ensureMindCanvasAgentConsoleResize(container, nodeModel);
@@ -12147,11 +12692,12 @@
12147
12692
  const agentStyledMemo = isAgentStyledMemoNode(nodeModel);
12148
12693
  const mindCanvasAgent = isMindCanvasAgentNode(nodeModel);
12149
12694
  const businessAutomation = isBusinessAutomationNode(nodeModel);
12695
+ const remoteFleet = isRemoteFleetMonitorNode(nodeModel);
12150
12696
  const externalChrome = allowsMemoExternalChrome(nodeModel);
12151
12697
 
12152
12698
  const container = document.createElement('div');
12153
12699
  container.id = `node-${nodeModel.id}`;
12154
- container.className = `map-node css3d-dynamic-node map-node-memo${agentStyledMemo ? ' map-node-agent' : ''}${businessAutomation ? ' map-node-automation' : ''}`;
12700
+ container.className = `map-node css3d-dynamic-node map-node-memo${agentStyledMemo ? ' map-node-agent' : ''}${businessAutomation ? ' map-node-automation' : ''}${remoteFleet ? ' map-node-remote-fleet' : ''}`;
12155
12701
  container.dataset.nodeId = nodeModel.id;
12156
12702
  container.dataset.semanticType = getNodeSemanticType(nodeModel);
12157
12703
  container.dataset.agentConsoleOpen = isMindCanvasAgentConsoleOpen(nodeModel) ? 'true' : 'false';
@@ -15290,10 +15836,12 @@
15290
15836
  const iconKey = getMemoIconKey(nodeModel);
15291
15837
  const agentStyledMemo = isAgentStyledMemoNode(nodeModel);
15292
15838
  const businessAutomation = isBusinessAutomationNode(nodeModel);
15839
+ const remoteFleet = isRemoteFleetMonitorNode(nodeModel);
15293
15840
  const externalChrome = allowsMemoExternalChrome(nodeModel);
15294
15841
 
15295
15842
  memoEl.classList.toggle('map-node-agent', agentStyledMemo);
15296
15843
  memoEl.classList.toggle('map-node-automation', businessAutomation);
15844
+ memoEl.classList.toggle('map-node-remote-fleet', remoteFleet);
15297
15845
  memoEl.dataset.semanticType = getNodeSemanticType(nodeModel);
15298
15846
  memoEl.dataset.agentPlanOpen = getRenderedMindCanvasAgentPlanOpen(memoEl, nodeModel) ? 'true' : 'false';
15299
15847
  memoEl.dataset.agentConsoleOpen = getRenderedMindCanvasAgentConsoleOpen(memoEl, nodeModel) ? 'true' : 'false';
@@ -56,6 +56,7 @@
56
56
  }
57
57
 
58
58
  const BUSINESS_AUTOMATION_SEMANTIC_TYPE = 'BusinessAutomationNode';
59
+ const REMOTE_FLEET_SEMANTIC_TYPE = 'RemoteFleetMonitor';
59
60
  const AUTOMATION_INPUT_PINS_METADATA_KEY = 'AutomationInputPins';
60
61
  const AUTOMATION_OUTPUT_PINS_METADATA_KEY = 'AutomationOutputPins';
61
62
  const AUTOMATION_PIN_RAIL_WIDTH = 118;
@@ -101,7 +102,8 @@
101
102
  const semanticType = getNodeSemanticType(nodeModel);
102
103
  return semanticType === 'MindCanvasAgent'
103
104
  || semanticType === 'AgentCommand'
104
- || semanticType === BUSINESS_AUTOMATION_SEMANTIC_TYPE;
105
+ || semanticType === BUSINESS_AUTOMATION_SEMANTIC_TYPE
106
+ || semanticType === REMOTE_FLEET_SEMANTIC_TYPE;
105
107
  }
106
108
 
107
109
  function isAgentNodeModel(nodeModel) {