@mindexec/cli 0.2.1 → 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 (26) 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/css/mind-map-overrides.css +11 -0
  7. package/wwwroot/_content/MindExecution.Shared/js/mind-map-core.js +76 -1
  8. package/wwwroot/_content/MindExecution.Shared/js/mind-map-css3d-manager.js +629 -5
  9. package/wwwroot/_content/MindExecution.Shared/js/mind-map-lod-renderer.js +16 -0
  10. package/wwwroot/_content/MindExecution.Shared/js/mind-map-nodes.js +16 -5
  11. package/wwwroot/_framework/MindExecution.Core.5luow1xgjs.dll +0 -0
  12. package/wwwroot/_framework/{MindExecution.Kernel.gwwc40sc45.dll → MindExecution.Kernel.mot9nj6bzm.dll} +0 -0
  13. package/wwwroot/_framework/{MindExecution.Plugins.Admin.0jgrn1sckv.dll → MindExecution.Plugins.Admin.x9v2drg2f7.dll} +0 -0
  14. package/wwwroot/_framework/{MindExecution.Plugins.Business.13mme2qcag.dll → MindExecution.Plugins.Business.b0kjoyx31x.dll} +0 -0
  15. package/wwwroot/_framework/{MindExecution.Plugins.Concept.dfp2mdt45q.dll → MindExecution.Plugins.Concept.6tojojgh1a.dll} +0 -0
  16. package/wwwroot/_framework/{MindExecution.Plugins.Directory.3w4t6n3se0.dll → MindExecution.Plugins.Directory.fqtbuqadsx.dll} +0 -0
  17. package/wwwroot/_framework/{MindExecution.Plugins.PlanMaster.s0qpntz420.dll → MindExecution.Plugins.PlanMaster.j7llfeae6l.dll} +0 -0
  18. package/wwwroot/_framework/{MindExecution.Plugins.YouTube.iu11fq8d16.dll → MindExecution.Plugins.YouTube.yo5fwdhugr.dll} +0 -0
  19. package/wwwroot/_framework/{MindExecution.Shared.7j27dcqnrc.dll → MindExecution.Shared.0qi7vbn9a4.dll} +0 -0
  20. package/wwwroot/_framework/MindExecution.Web.6cv7ad7rik.dll +0 -0
  21. package/wwwroot/_framework/blazor.boot.json +21 -21
  22. package/wwwroot/index.html +3 -3
  23. package/wwwroot/service-worker-assets.js +28 -28
  24. package/wwwroot/service-worker.js +1 -1
  25. package/wwwroot/_framework/MindExecution.Core.1q1trifbuu.dll +0 -0
  26. package/wwwroot/_framework/MindExecution.Web.pq1ty8ov2v.dll +0 -0
@@ -297,6 +297,17 @@
297
297
  overflow: hidden !important;
298
298
  }
299
299
 
300
+ .css3d-resolution-wrapper.node-type-templatelauncher,
301
+ .css3d-resolution-wrapper.node-type-templatelauncher > .map-node,
302
+ .css3d-resolution-wrapper.node-type-templatelauncher > .map-node-template-card {
303
+ contain: layout style !important;
304
+ content-visibility: visible !important;
305
+ contain-intrinsic-size: auto !important;
306
+ isolation: isolate !important;
307
+ backface-visibility: visible !important;
308
+ -webkit-backface-visibility: visible !important;
309
+ }
310
+
300
311
  .css3d-resolution-wrapper.node-type-image .node-content-wrapper,
301
312
  .css3d-resolution-wrapper.node-type-video .node-content-wrapper,
302
313
  .css3d-resolution-wrapper.node-type-embed .node-content-wrapper,
@@ -3196,6 +3207,8 @@
3196
3207
 
3197
3208
  nodeModel.response = normalized;
3198
3209
  nodeModel.Response = normalized;
3210
+ mediaEl.dataset.mediaSourceUrl = normalized;
3211
+ mediaEl.dataset.resolvedMediaSourceUrl = normalized;
3199
3212
  mediaEl.setAttribute('src', normalized);
3200
3213
 
3201
3214
  if (contentTypeLower === 'video' && typeof mediaEl.load === 'function') {
@@ -3266,11 +3279,25 @@
3266
3279
  return;
3267
3280
  }
3268
3281
 
3282
+ const clearVideoPlaybackError = () => {
3283
+ if (contentTypeLower !== 'video') {
3284
+ return;
3285
+ }
3286
+
3287
+ const metadata = ensureCssMediaMetadata(nodeModel);
3288
+ delete metadata.VideoPlaybackError;
3289
+ delete metadata.videoPlaybackError;
3290
+ delete metadata.VideoPlaybackErrorCode;
3291
+ delete metadata.videoPlaybackErrorCode;
3292
+ mediaEl.closest?.('.video-content')?.classList?.remove?.('mind-map-video-error');
3293
+ };
3294
+
3269
3295
  const handleReady = () => {
3270
3296
  if (contentTypeLower === 'image') {
3271
3297
  mediaEl.dataset.mediaReady = '1';
3272
3298
  mediaEl.style.opacity = '1';
3273
3299
  }
3300
+ clearVideoPlaybackError();
3274
3301
  setAssetRefreshState(nodeModel, '', '');
3275
3302
  syncMediaLoadingRowForElement(mediaEl, nodeModel, contentTypeLower);
3276
3303
  mediaEl.dataset.assetRefreshAttempted = '';
@@ -3282,12 +3309,61 @@
3282
3309
  }
3283
3310
  };
3284
3311
 
3285
- mediaEl.onerror = () => {
3312
+ const describeVideoElementError = () => {
3313
+ const code = Number(mediaEl?.error?.code || 0);
3314
+ const reason = {
3315
+ 1: 'loading was aborted',
3316
+ 2: 'a network error stopped loading',
3317
+ 3: 'the browser could not decode the media',
3318
+ 4: 'the browser does not support this codec or container'
3319
+ }[code] || 'the browser could not load the media';
3320
+ const source = getCssMediaCanonicalSource(mediaEl, nodeModel?.response || nodeModel?.Response || '');
3321
+ let hint = 'Try H.264/AAC MP4 or WebM.';
3322
+ try {
3323
+ const parsed = new URL(source, window.location?.href || undefined);
3324
+ const extension = parsed.pathname.split('.').pop()?.toLowerCase() || '';
3325
+ if (extension === 'mov' || extension === 'm4v') {
3326
+ hint = 'MOV/M4V often fails in browsers unless encoded as H.264/AAC.';
3327
+ }
3328
+ } catch { }
3329
+
3330
+ return `Video playback failed: ${reason}. ${hint}`;
3331
+ };
3332
+
3333
+ const markVideoElementError = () => {
3334
+ if (contentTypeLower !== 'video') {
3335
+ return;
3336
+ }
3337
+
3338
+ const metadata = ensureCssMediaMetadata(nodeModel);
3339
+ const message = describeVideoElementError();
3340
+ metadata.VideoPlaybackError = message;
3341
+ metadata.videoPlaybackError = message;
3342
+ metadata.VideoPlaybackErrorCode = String(Number(mediaEl?.error?.code || 0));
3343
+ metadata.videoPlaybackErrorCode = metadata.VideoPlaybackErrorCode;
3344
+ mediaEl.closest?.('.video-content')?.classList?.add?.('mind-map-video-error');
3345
+ setAssetRefreshState(nodeModel, 'failed', message);
3346
+ syncMediaLoadingRowForElement(mediaEl, nodeModel, contentTypeLower);
3347
+ };
3348
+
3349
+ const handleMediaError = async () => {
3286
3350
  if (contentTypeLower === 'image') {
3287
3351
  mediaEl.dataset.mediaReady = '0';
3288
3352
  mediaEl.style.opacity = '0';
3289
3353
  }
3290
- void tryRefreshCssMediaSource(mediaEl, nodeModel, contentTypeLower);
3354
+ const beforeSource = getCssMediaCanonicalSource(mediaEl, nodeModel?.response || nodeModel?.Response || '');
3355
+ await tryRefreshCssMediaSource(mediaEl, nodeModel, contentTypeLower);
3356
+ if (contentTypeLower === 'video') {
3357
+ const afterSource = getCssMediaCanonicalSource(mediaEl, nodeModel?.response || nodeModel?.Response || '');
3358
+ const refreshState = getAssetRefreshState(nodeModel);
3359
+ if (afterSource === beforeSource && refreshState !== 'refreshing') {
3360
+ markVideoElementError();
3361
+ }
3362
+ }
3363
+ };
3364
+
3365
+ mediaEl.onerror = () => {
3366
+ void handleMediaError();
3291
3367
  };
3292
3368
 
3293
3369
  if (contentTypeLower === 'video') {
@@ -3324,6 +3400,7 @@
3324
3400
  const MEDIA_GENERATION_STATUS_METADATA_KEY = 'MediaGenerationStatus';
3325
3401
  const MEDIA_GENERATION_STATUS_MESSAGE_METADATA_KEY = 'MediaGenerationStatusMessage';
3326
3402
  const BUSINESS_AUTOMATION_SEMANTIC_TYPE = 'BusinessAutomationNode';
3403
+ const REMOTE_FLEET_SEMANTIC_TYPE = 'RemoteFleetMonitor';
3327
3404
  const AUTOMATION_NODE_KIND_METADATA_KEY = 'AutomationNodeKind';
3328
3405
  const AUTOMATION_NODE_LABEL_METADATA_KEY = 'AutomationNodeLabel';
3329
3406
  const AUTOMATION_NODE_DESCRIPTION_METADATA_KEY = 'AutomationNodeDescription';
@@ -3493,6 +3570,10 @@
3493
3570
  return getNodeSemanticType(nodeModel) === BUSINESS_AUTOMATION_SEMANTIC_TYPE;
3494
3571
  }
3495
3572
 
3573
+ function isRemoteFleetMonitorNode(nodeModel) {
3574
+ return getNodeSemanticType(nodeModel) === REMOTE_FLEET_SEMANTIC_TYPE;
3575
+ }
3576
+
3496
3577
  function isBusinessAutomationContextSourceNode(nodeModel) {
3497
3578
  if (!nodeModel || isBusinessAutomationNode(nodeModel)) {
3498
3579
  return false;
@@ -3506,7 +3587,8 @@
3506
3587
  const semanticType = getNodeSemanticType(nodeModel);
3507
3588
  return semanticType === 'MindCanvasAgent'
3508
3589
  || semanticType === 'AgentCommand'
3509
- || semanticType === BUSINESS_AUTOMATION_SEMANTIC_TYPE;
3590
+ || semanticType === BUSINESS_AUTOMATION_SEMANTIC_TYPE
3591
+ || semanticType === REMOTE_FLEET_SEMANTIC_TYPE;
3510
3592
  }
3511
3593
 
3512
3594
  function allowsMemoExternalChrome(nodeModel) {
@@ -5740,7 +5822,7 @@
5740
5822
  tone = 'refresh';
5741
5823
  } else if ((contentTypeLower === 'image' || contentTypeLower === 'video') && assetRefreshState === 'failed') {
5742
5824
  showRow = true;
5743
- label = 'Asset refresh failed';
5825
+ label = contentTypeLower === 'video' ? 'Video playback failed' : 'Asset refresh failed';
5744
5826
  message = assetRefreshMessage || 'Reload the board or reopen this file.';
5745
5827
  tone = 'failed';
5746
5828
  } else if (contentTypeLower === 'video') {
@@ -11758,6 +11840,18 @@
11758
11840
  function renderMemoBodyView(bodyView, nodeModel, content) {
11759
11841
  if (!bodyView) return;
11760
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
+
11761
11855
  const nextValue = String(content || '');
11762
11856
  bodyView.dataset.src = nextValue;
11763
11857
 
@@ -11840,6 +11934,533 @@
11840
11934
  }, 220);
11841
11935
  }
11842
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
+
11843
12464
  function bindMemoNodeEvents(container, nodeModel) {
11844
12465
  if (!container || !nodeModel) return;
11845
12466
  ensureMindCanvasAgentConsoleResize(container, nodeModel);
@@ -12071,11 +12692,12 @@
12071
12692
  const agentStyledMemo = isAgentStyledMemoNode(nodeModel);
12072
12693
  const mindCanvasAgent = isMindCanvasAgentNode(nodeModel);
12073
12694
  const businessAutomation = isBusinessAutomationNode(nodeModel);
12695
+ const remoteFleet = isRemoteFleetMonitorNode(nodeModel);
12074
12696
  const externalChrome = allowsMemoExternalChrome(nodeModel);
12075
12697
 
12076
12698
  const container = document.createElement('div');
12077
12699
  container.id = `node-${nodeModel.id}`;
12078
- 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' : ''}`;
12079
12701
  container.dataset.nodeId = nodeModel.id;
12080
12702
  container.dataset.semanticType = getNodeSemanticType(nodeModel);
12081
12703
  container.dataset.agentConsoleOpen = isMindCanvasAgentConsoleOpen(nodeModel) ? 'true' : 'false';
@@ -15214,10 +15836,12 @@
15214
15836
  const iconKey = getMemoIconKey(nodeModel);
15215
15837
  const agentStyledMemo = isAgentStyledMemoNode(nodeModel);
15216
15838
  const businessAutomation = isBusinessAutomationNode(nodeModel);
15839
+ const remoteFleet = isRemoteFleetMonitorNode(nodeModel);
15217
15840
  const externalChrome = allowsMemoExternalChrome(nodeModel);
15218
15841
 
15219
15842
  memoEl.classList.toggle('map-node-agent', agentStyledMemo);
15220
15843
  memoEl.classList.toggle('map-node-automation', businessAutomation);
15844
+ memoEl.classList.toggle('map-node-remote-fleet', remoteFleet);
15221
15845
  memoEl.dataset.semanticType = getNodeSemanticType(nodeModel);
15222
15846
  memoEl.dataset.agentPlanOpen = getRenderedMindCanvasAgentPlanOpen(memoEl, nodeModel) ? 'true' : 'false';
15223
15847
  memoEl.dataset.agentConsoleOpen = getRenderedMindCanvasAgentConsoleOpen(memoEl, nodeModel) ? 'true' : 'false';