@mindexec/cli 0.2.11 → 0.2.12

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.11",
3
+ "version": "0.2.12",
4
4
  "description": "MindExec local runtime and bridge CLI",
5
5
  "main": "server.js",
6
6
  "type": "module",
@@ -20,9 +20,10 @@
20
20
  "scripts": {
21
21
  "start": "node launch-bridge.cjs",
22
22
  "dev": "node launch-bridge.cjs --watch",
23
- "test:syntax": "node --check server.js && node --check remote-hub.js && node --check codex-runtime.js && node --check launch-bridge.cjs && node --check port-guard.cjs && node --check scripts/setup-tree-sitter-grammars.mjs && node --check scripts/remote-hub-smoke.mjs && node --check scripts/remote-hub-scale-smoke.mjs",
23
+ "test:syntax": "node --check server.js && node --check remote-hub.js && node --check codex-runtime.js && node --check launch-bridge.cjs && node --check port-guard.cjs && node --check scripts/setup-tree-sitter-grammars.mjs && node --check scripts/remote-hub-smoke.mjs && node --check scripts/remote-hub-scale-smoke.mjs && node --check scripts/remote-fleet-render-smoke.mjs",
24
24
  "test:remote": "node scripts/remote-hub-smoke.mjs",
25
25
  "test:remote:scale": "node scripts/remote-hub-scale-smoke.mjs",
26
+ "test:remote:render": "node scripts/remote-fleet-render-smoke.mjs",
26
27
  "pack:dry": "npm pack --dry-run",
27
28
  "setup:grammars": "node scripts/setup-tree-sitter-grammars.mjs",
28
29
  "postinstall": "npm run setup:grammars"
@@ -0,0 +1,579 @@
1
+ #!/usr/bin/env node
2
+
3
+ import assert from 'node:assert/strict';
4
+ import fs from 'node:fs/promises';
5
+ import path from 'node:path';
6
+ import vm from 'node:vm';
7
+ import { fileURLToPath } from 'node:url';
8
+ import { createRemoteHub } from '../remote-hub.js';
9
+
10
+ const SYNTHETIC_COUNT = Number(process.env.REMOTE_FLEET_RENDER_SMOKE_COUNT || 250);
11
+
12
+ function dataAttributeToDatasetKey(name) {
13
+ return String(name || '').replace(/^data-/, '').replace(/-([a-z])/g, (_, char) => char.toUpperCase());
14
+ }
15
+
16
+ class MiniStyle {
17
+ constructor() {
18
+ this._cssText = '';
19
+ }
20
+
21
+ get cssText() {
22
+ return this._cssText;
23
+ }
24
+
25
+ set cssText(value) {
26
+ this._cssText = String(value || '');
27
+ const displayMatch = /(?:^|;)\s*display\s*:\s*([^;]+)/i.exec(this._cssText);
28
+ if (displayMatch) {
29
+ this.display = displayMatch[1].trim();
30
+ }
31
+ }
32
+
33
+ setProperty(name, value) {
34
+ this[String(name)] = String(value);
35
+ }
36
+ }
37
+
38
+ class MiniClassList {
39
+ constructor(owner) {
40
+ this.owner = owner;
41
+ this.tokens = new Set();
42
+ }
43
+
44
+ add(...tokens) {
45
+ tokens.forEach(token => {
46
+ const value = String(token || '').trim();
47
+ if (value) {
48
+ this.tokens.add(value);
49
+ }
50
+ });
51
+ this.sync();
52
+ }
53
+
54
+ remove(...tokens) {
55
+ tokens.forEach(token => this.tokens.delete(String(token || '').trim()));
56
+ this.sync();
57
+ }
58
+
59
+ contains(token) {
60
+ return this.tokens.has(String(token || '').trim());
61
+ }
62
+
63
+ toggle(token, force) {
64
+ const value = String(token || '').trim();
65
+ if (!value) return false;
66
+ const shouldAdd = force === undefined ? !this.tokens.has(value) : !!force;
67
+ if (shouldAdd) {
68
+ this.tokens.add(value);
69
+ } else {
70
+ this.tokens.delete(value);
71
+ }
72
+ this.sync();
73
+ return shouldAdd;
74
+ }
75
+
76
+ sync() {
77
+ this.owner.className = [...this.tokens].join(' ');
78
+ }
79
+ }
80
+
81
+ class MiniElement {
82
+ constructor(tagName, ownerDocument) {
83
+ this.tagName = String(tagName || '').toUpperCase();
84
+ this.nodeName = this.tagName;
85
+ this.ownerDocument = ownerDocument;
86
+ this.parentElement = null;
87
+ this.children = [];
88
+ this.attributes = {};
89
+ this.dataset = {};
90
+ this.style = new MiniStyle();
91
+ this.classList = new MiniClassList(this);
92
+ this.className = '';
93
+ this._textContent = '';
94
+ this._listeners = new Map();
95
+ this.value = '';
96
+ this.checked = false;
97
+ this.disabled = false;
98
+ }
99
+
100
+ get textContent() {
101
+ return `${this._textContent}${this.children.map(child => child.textContent).join('')}`;
102
+ }
103
+
104
+ set textContent(value) {
105
+ this.children.forEach(child => {
106
+ child.parentElement = null;
107
+ });
108
+ this.children = [];
109
+ this._textContent = String(value ?? '');
110
+ }
111
+
112
+ get innerHTML() {
113
+ return '';
114
+ }
115
+
116
+ set innerHTML(_value) {
117
+ this.children.forEach(child => {
118
+ child.parentElement = null;
119
+ });
120
+ this.children = [];
121
+ this._textContent = '';
122
+ }
123
+
124
+ appendChild(child) {
125
+ if (!child) return child;
126
+ if (child.parentElement) {
127
+ child.parentElement.children = child.parentElement.children.filter(item => item !== child);
128
+ }
129
+ child.parentElement = this;
130
+ this.children.push(child);
131
+ return child;
132
+ }
133
+
134
+ remove() {
135
+ if (!this.parentElement) return;
136
+ this.parentElement.children = this.parentElement.children.filter(child => child !== this);
137
+ this.parentElement = null;
138
+ }
139
+
140
+ setAttribute(name, value) {
141
+ const key = String(name || '');
142
+ const text = String(value ?? '');
143
+ this.attributes[key] = text;
144
+ if (key === 'id') {
145
+ this.id = text;
146
+ } else if (key === 'class') {
147
+ this.className = text;
148
+ this.classList.tokens = new Set(text.split(/\s+/).filter(Boolean));
149
+ } else if (key.startsWith('data-')) {
150
+ this.dataset[dataAttributeToDatasetKey(key)] = text;
151
+ } else if (key === 'aria-label') {
152
+ this.ariaLabel = text;
153
+ }
154
+ }
155
+
156
+ getAttribute(name) {
157
+ const key = String(name || '');
158
+ if (key.startsWith('data-')) {
159
+ return this.dataset[dataAttributeToDatasetKey(key)];
160
+ }
161
+ return this.attributes[key];
162
+ }
163
+
164
+ addEventListener(type, listener) {
165
+ const key = String(type || '');
166
+ if (!this._listeners.has(key)) {
167
+ this._listeners.set(key, []);
168
+ }
169
+ this._listeners.get(key).push(listener);
170
+ }
171
+
172
+ dispatchEvent(event) {
173
+ const payload = typeof event === 'string' ? { type: event } : { ...(event || {}) };
174
+ payload.type = payload.type || 'event';
175
+ payload.target = payload.target || this;
176
+ payload.currentTarget = this;
177
+ payload.preventDefault = payload.preventDefault || (() => {});
178
+ payload.stopPropagation = payload.stopPropagation || (() => {});
179
+ for (const listener of this._listeners.get(payload.type) || []) {
180
+ listener(payload);
181
+ }
182
+ return true;
183
+ }
184
+
185
+ querySelector(selector) {
186
+ return this.querySelectorAll(selector)[0] || null;
187
+ }
188
+
189
+ querySelectorAll(selector) {
190
+ const matches = [];
191
+ const visit = element => {
192
+ for (const child of element.children) {
193
+ if (matchesSelector(child, selector)) {
194
+ matches.push(child);
195
+ }
196
+ visit(child);
197
+ }
198
+ };
199
+ visit(this);
200
+ return matches;
201
+ }
202
+
203
+ closest(selector) {
204
+ let current = this;
205
+ while (current) {
206
+ if (matchesSelector(current, selector)) {
207
+ return current;
208
+ }
209
+ current = current.parentElement;
210
+ }
211
+ return null;
212
+ }
213
+
214
+ contains(node) {
215
+ let current = node;
216
+ while (current) {
217
+ if (current === this) return true;
218
+ current = current.parentElement;
219
+ }
220
+ return false;
221
+ }
222
+ }
223
+
224
+ class MiniDocument {
225
+ constructor() {
226
+ this.body = new MiniElement('body', this);
227
+ this.head = new MiniElement('head', this);
228
+ }
229
+
230
+ createElement(tagName) {
231
+ return new MiniElement(tagName, this);
232
+ }
233
+
234
+ getElementById(id) {
235
+ const target = String(id || '');
236
+ return [this.head, this.body]
237
+ .flatMap(root => root.querySelectorAll('*'))
238
+ .find(element => element.id === target) || null;
239
+ }
240
+
241
+ querySelector(selector) {
242
+ return this.body.querySelector(selector) || this.head.querySelector(selector);
243
+ }
244
+
245
+ querySelectorAll(selector) {
246
+ return [
247
+ ...this.head.querySelectorAll(selector),
248
+ ...this.body.querySelectorAll(selector)
249
+ ];
250
+ }
251
+ }
252
+
253
+ function matchesSelector(element, selector) {
254
+ const text = String(selector || '').trim();
255
+ if (!text || !element) return false;
256
+ if (text === '*') return true;
257
+
258
+ const tagMatch = /^[a-zA-Z][\w-]*/.exec(text);
259
+ if (tagMatch && element.tagName.toLowerCase() !== tagMatch[0].toLowerCase()) {
260
+ return false;
261
+ }
262
+
263
+ for (const classMatch of text.matchAll(/\.([\w-]+)/g)) {
264
+ if (!element.classList.contains(classMatch[1])) {
265
+ return false;
266
+ }
267
+ }
268
+
269
+ for (const attrMatch of text.matchAll(/\[([^\]=]+)(?:="([^"]*)")?\]/g)) {
270
+ const attrName = attrMatch[1];
271
+ const expected = attrMatch[2];
272
+ const actual = attrName.startsWith('data-')
273
+ ? element.dataset[dataAttributeToDatasetKey(attrName)]
274
+ : element.getAttribute(attrName);
275
+ if (actual === undefined || actual === null) {
276
+ return false;
277
+ }
278
+ if (expected !== undefined && String(actual) !== expected) {
279
+ return false;
280
+ }
281
+ }
282
+
283
+ return true;
284
+ }
285
+
286
+ function readStatusNumber(status, key) {
287
+ const value = status?.[key];
288
+ return Number.isFinite(Number(value)) ? Number(value) : null;
289
+ }
290
+
291
+ function readLoad1(status) {
292
+ const value = status?.loadavg;
293
+ return Array.isArray(value) && Number.isFinite(Number(value[0])) ? Number(value[0]) : null;
294
+ }
295
+
296
+ function projectDevice(device) {
297
+ const status = device.status || {};
298
+ const capabilities = device.capabilities || {};
299
+ const thumbnail = device.latestThumbnail || {};
300
+ const liveFrame = device.latestLiveFrame || {};
301
+ const liveStream = device.activeLiveStream || {};
302
+ const latestTask = device.latestTask || {};
303
+ const platform = String(status.platform || device.platform || 'unknown');
304
+
305
+ return {
306
+ DeviceId: String(device.deviceId || ''),
307
+ SessionId: String(device.sessionId || ''),
308
+ Name: String(device.deviceName || device.hostname || device.deviceId || 'device'),
309
+ Hostname: String(device.hostname || ''),
310
+ Platform: platform,
311
+ Release: String(status.release || ''),
312
+ Arch: String(device.arch || ''),
313
+ AgentVersion: String(device.agentVersion || ''),
314
+ Connected: device.connected === true,
315
+ ConnectedAt: String(device.connectedAt || ''),
316
+ DisconnectedAt: String(device.disconnectedAt || ''),
317
+ LastSeenAt: String(device.lastSeenAt || ''),
318
+ LastStatusAt: String(device.lastStatusAt || ''),
319
+ LastDisconnectReason: String(device.lastDisconnectReason || ''),
320
+ RemoteAddress: String(device.remoteAddress || ''),
321
+ RemotePort: Number(device.remotePort || 0),
322
+ UptimeSec: readStatusNumber(status, 'uptimeSec'),
323
+ UsedMemRatio: readStatusNumber(status, 'usedMemRatio'),
324
+ Load1: readLoad1(status),
325
+ StatusEnabled: capabilities.status === true,
326
+ ThumbnailEnabled: capabilities.thumbnail === true,
327
+ LiveStreamEnabled: capabilities.liveStream === true,
328
+ ControlEnabled: capabilities.control === true,
329
+ ComputerAgentEnabled: capabilities.computerAgent === true || capabilities.taskDispatch === true,
330
+ AiAssistEnabled: capabilities.aiAssist === true,
331
+ AiModel: String(capabilities.aiModel || ''),
332
+ AiProvider: String(capabilities.aiProvider || ''),
333
+ ThumbnailStreamId: String(thumbnail.streamId || ''),
334
+ ThumbnailFrameSeq: Number(thumbnail.frameSeq || 0),
335
+ ThumbnailWidth: Number(thumbnail.width || 0),
336
+ ThumbnailHeight: Number(thumbnail.height || 0),
337
+ ThumbnailMimeType: String(thumbnail.mimeType || thumbnail.format || ''),
338
+ ThumbnailCapturedAt: String(thumbnail.capturedAt || ''),
339
+ ThumbnailReceivedAt: String(thumbnail.receivedAt || ''),
340
+ ThumbnailDataUrl: String(thumbnail.dataUrl || ''),
341
+ LiveStreamActive: liveStream.active === true,
342
+ LiveStreamId: String(liveStream.streamId || ''),
343
+ LiveStreamMode: String(liveStream.mode || ''),
344
+ LiveStreamFps: Number(liveStream.fps || 0),
345
+ LiveStreamStartedAt: String(liveStream.startedAt || ''),
346
+ LiveStreamStoppedAt: String(liveStream.stoppedAt || ''),
347
+ LiveStreamLastFrameAt: String(liveStream.lastFrameAt || ''),
348
+ LiveStreamFrameSeq: Number(liveStream.lastFrameSeq || 0),
349
+ LiveFrameStreamId: String(liveFrame.streamId || ''),
350
+ LiveFrameSeq: Number(liveFrame.frameSeq || 0),
351
+ LiveFrameWidth: Number(liveFrame.width || 0),
352
+ LiveFrameHeight: Number(liveFrame.height || 0),
353
+ LiveFrameMimeType: String(liveFrame.mimeType || liveFrame.format || ''),
354
+ LiveFrameCapturedAt: String(liveFrame.capturedAt || ''),
355
+ LiveFrameReceivedAt: String(liveFrame.receivedAt || ''),
356
+ LiveFrameDataUrl: String(liveFrame.dataUrl || ''),
357
+ LatestTaskId: String(latestTask.taskId || ''),
358
+ LatestTaskCommandId: String(latestTask.commandId || ''),
359
+ LatestTaskTitle: String(latestTask.title || ''),
360
+ LatestTaskInstructionPreview: String(latestTask.instructionPreview || ''),
361
+ LatestTaskStatus: String(latestTask.status || ''),
362
+ LatestTaskApprovalLevel: String(latestTask.approvalLevel || ''),
363
+ LatestTaskUpdatedAt: String(latestTask.updatedAt || latestTask.completedAt || latestTask.sentAt || ''),
364
+ LatestTaskError: String(latestTask.error || ''),
365
+ LatestTaskResultSummary: String(latestTask.resultSummary || '')
366
+ };
367
+ }
368
+
369
+ async function loadCss3DManager() {
370
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
371
+ const managerPath = path.resolve(__dirname, '../../MindExecution.Shared/wwwroot/js/mind-map-css3d-manager.js');
372
+ const source = await fs.readFile(managerPath, 'utf8');
373
+ const document = new MiniDocument();
374
+ const context = {
375
+ console,
376
+ document,
377
+ navigator: {
378
+ clipboard: {
379
+ writeText: async () => {}
380
+ }
381
+ },
382
+ setInterval: () => 0,
383
+ clearInterval: () => {},
384
+ setTimeout,
385
+ clearTimeout,
386
+ Date,
387
+ Math,
388
+ Number,
389
+ String,
390
+ Boolean,
391
+ Array,
392
+ Object,
393
+ JSON,
394
+ RegExp,
395
+ Map,
396
+ Set,
397
+ WeakMap,
398
+ THREE: createThreeStub(),
399
+ URL,
400
+ Promise
401
+ };
402
+ context.window = context;
403
+ context.self = context;
404
+
405
+ vm.runInNewContext(source, context, { filename: managerPath });
406
+ return { manager: context.window.MindMapCss3DManager, document };
407
+ }
408
+
409
+ function buildMonitorNode(devices, hubStatus) {
410
+ const connected = devices.filter(device => device.Connected).length;
411
+ const endpoint = hubStatus.agentEndpoint || '127.0.0.1:5197';
412
+ const pairToken = hubStatus.pairToken || 'render-smoke-token';
413
+ return {
414
+ id: 'remote-fleet-render-smoke',
415
+ contentType: 'memo',
416
+ metadata: {
417
+ SemanticType: 'RemoteFleetMonitor',
418
+ RemoteFleetSchemaVersion: 'remote-fleet@1',
419
+ RemoteFleetViewMode: 'all-devices',
420
+ RemoteFleetLastRefreshAtUtc: new Date().toISOString(),
421
+ RemoteFleetHubStatus: 'online',
422
+ RemoteFleetHubEndpoint: endpoint,
423
+ RemoteFleetAgentPackage: '@mindexec/remote',
424
+ RemoteFleetPairTokenPreview: '<pair-token>',
425
+ RemoteFleetConnectCommand: `npx @mindexec/remote connect --manager ${endpoint} --pair ${pairToken}`,
426
+ RemoteFleetDeviceCount: String(devices.length),
427
+ RemoteFleetConnectedDeviceCount: String(connected),
428
+ RemoteFleetCanvasPagination: 'none',
429
+ RemoteFleetDevicesJson: JSON.stringify(devices),
430
+ RemoteFleetLastError: ''
431
+ }
432
+ };
433
+ }
434
+
435
+ function buildDeviceNode(device, hubStatus) {
436
+ const endpoint = hubStatus.agentEndpoint || '127.0.0.1:5197';
437
+ return {
438
+ id: 'remote-device-render-smoke',
439
+ contentType: 'memo',
440
+ metadata: {
441
+ SemanticType: 'RemoteFleetDevice',
442
+ RemoteFleetSchemaVersion: 'remote-device@1',
443
+ RemoteFleetViewMode: 'pinned-device',
444
+ RemoteFleetLastRefreshAtUtc: new Date().toISOString(),
445
+ RemoteFleetHubStatus: 'online',
446
+ RemoteFleetHubEndpoint: endpoint,
447
+ RemoteFleetAgentPackage: '@mindexec/remote',
448
+ RemoteFleetPinnedDeviceId: device.DeviceId,
449
+ RemoteFleetPinnedDeviceJson: JSON.stringify(device),
450
+ RemoteFleetLastError: ''
451
+ }
452
+ };
453
+ }
454
+
455
+ function createThreeStub() {
456
+ class Vector3 {
457
+ constructor(x = 0, y = 0, z = 0) {
458
+ this.x = x;
459
+ this.y = y;
460
+ this.z = z;
461
+ }
462
+
463
+ set(x = 0, y = 0, z = 0) {
464
+ this.x = x;
465
+ this.y = y;
466
+ this.z = z;
467
+ return this;
468
+ }
469
+ }
470
+
471
+ class Matrix4 {
472
+ multiplyMatrices() { return this; }
473
+ }
474
+
475
+ class Frustum {
476
+ setFromProjectionMatrix() { return this; }
477
+ intersectsBox() { return true; }
478
+ }
479
+
480
+ class Quaternion {}
481
+
482
+ class Box3 {
483
+ setFromCenterAndSize() { return this; }
484
+ }
485
+
486
+ return { Box3, Frustum, Matrix4, Quaternion, Vector3 };
487
+ }
488
+
489
+ const hub = createRemoteHub({
490
+ env: {
491
+ MINDEXEC_REMOTE_HUB: '1',
492
+ REMOTE_HUB_HOST: '127.0.0.1',
493
+ REMOTE_HUB_PORT: '0',
494
+ REMOTE_HUB_PAIR_TOKEN: 'render-smoke-token'
495
+ }
496
+ });
497
+
498
+ try {
499
+ await hub.start();
500
+ const seeded = hub.seedSyntheticFleet({
501
+ count: SYNTHETIC_COUNT,
502
+ connectedRatio: 0.84,
503
+ thumbnailRatio: 0.72,
504
+ aiAssistRatio: 0.42,
505
+ liveCount: 0
506
+ });
507
+ assert.equal(seeded.ok, true);
508
+ assert.equal(seeded.seeded, SYNTHETIC_COUNT);
509
+
510
+ const rawDevices = hub.listDevices();
511
+ const devices = rawDevices.map(projectDevice);
512
+ const connectedCount = devices.filter(device => device.Connected).length;
513
+ const offlineCount = devices.length - connectedCount;
514
+ const aiCount = devices.filter(device => device.Connected && device.AiAssistEnabled).length;
515
+ const focusedDevice = devices.find(device => device.Connected);
516
+ assert.ok(focusedDevice);
517
+
518
+ const { manager, document } = await loadCss3DManager();
519
+ assert.equal(typeof manager?.renderRemoteFleetMonitorForTest, 'function');
520
+ assert.equal(typeof manager?.renderRemoteFleetDeviceForTest, 'function');
521
+
522
+ const bodyView = document.createElement('div');
523
+ bodyView.dataset.remoteFleetAutoMonitor = 'false';
524
+ bodyView.dataset.remoteFleetFocusDeviceId = focusedDevice.DeviceId;
525
+ document.body.appendChild(bodyView);
526
+
527
+ manager.renderRemoteFleetMonitorForTest(bodyView, buildMonitorNode(devices, hub.getStatus({ includeSecrets: true })));
528
+
529
+ let cards = bodyView.querySelectorAll('article[data-device-id]');
530
+ assert.equal(cards.length, SYNTHETIC_COUNT);
531
+ assert.equal(bodyView.querySelector('[data-remote-fleet-match-count="true"]')?.textContent, `${SYNTHETIC_COUNT}/${SYNTHETIC_COUNT}`);
532
+ assert.equal(bodyView.querySelectorAll('[data-remote-fleet-action="pin-device"]').length, SYNTHETIC_COUNT);
533
+ assert.equal(bodyView.querySelectorAll('[data-remote-fleet-action="task-device"]').length, connectedCount);
534
+ assert.equal(bodyView.querySelectorAll('[data-remote-fleet-action="thumbnail-device"]').length, devices.filter(device => device.Connected && device.ThumbnailEnabled).length);
535
+ assert.equal(bodyView.querySelector('[data-remote-fleet-live-panel="true"]')?.dataset.deviceId, focusedDevice.DeviceId);
536
+ assert.equal(bodyView.querySelector('[data-remote-fleet-action="task-visible"]')?.disabled, false);
537
+ assert.ok(bodyView.textContent.includes('all devices, no paging'));
538
+ assert.ok(bodyView.querySelector('code')?.textContent.includes('npx @mindexec/remote connect'));
539
+
540
+ const searchInput = bodyView.querySelector('[data-remote-fleet-search="true"]');
541
+ searchInput.value = 'synthetic pc 0001';
542
+ searchInput.dispatchEvent({ type: 'input' });
543
+ cards = bodyView.querySelectorAll('article[data-device-id]');
544
+ assert.equal(cards.filter(card => card.style.display !== 'none').length, 1);
545
+ assert.equal(bodyView.querySelector('[data-remote-fleet-match-count="true"]')?.textContent, `1/${SYNTHETIC_COUNT}`);
546
+
547
+ const selects = bodyView.querySelectorAll('select');
548
+ assert.equal(selects.length, 4);
549
+ const [filterSelect, , groupSelect] = selects;
550
+ searchInput.value = '';
551
+ searchInput.dispatchEvent({ type: 'input' });
552
+ filterSelect.value = 'offline';
553
+ filterSelect.dispatchEvent({ type: 'change' });
554
+ assert.equal(bodyView.querySelector('[data-remote-fleet-match-count="true"]')?.textContent, `${offlineCount}/${SYNTHETIC_COUNT}`);
555
+
556
+ filterSelect.value = 'ai';
557
+ filterSelect.dispatchEvent({ type: 'change' });
558
+ assert.equal(bodyView.querySelector('[data-remote-fleet-match-count="true"]')?.textContent, `${aiCount}/${SYNTHETIC_COUNT}`);
559
+
560
+ filterSelect.value = 'all';
561
+ filterSelect.dispatchEvent({ type: 'change' });
562
+ groupSelect.value = 'status';
563
+ groupSelect.dispatchEvent({ type: 'change' });
564
+ assert.equal(bodyView.dataset.remoteFleetGroup, 'status');
565
+ assert.ok(bodyView.querySelectorAll('[data-remote-fleet-group-header="true"]').length >= 2);
566
+ assert.equal(bodyView.querySelectorAll('article[data-device-id]').length, SYNTHETIC_COUNT);
567
+
568
+ const deviceBody = document.createElement('div');
569
+ document.body.appendChild(deviceBody);
570
+ manager.renderRemoteFleetDeviceForTest(deviceBody, buildDeviceNode(focusedDevice, hub.getStatus()));
571
+ assert.ok(deviceBody.textContent.includes(focusedDevice.Name));
572
+ assert.ok(deviceBody.querySelector('[data-remote-fleet-action="refresh-device"]'));
573
+ assert.ok(deviceBody.querySelector('[data-remote-fleet-action="task-device"]'));
574
+ assert.ok(deviceBody.textContent.includes(focusedDevice.DeviceId));
575
+
576
+ console.log(`RemoteFleet render smoke OK (${SYNTHETIC_COUNT} synthetic devices, ${connectedCount} connected, ${aiCount} AI-ready)`);
577
+ } finally {
578
+ await hub.close();
579
+ }
@@ -18079,6 +18079,8 @@
18079
18079
  syncVideoNodeVisibilityPlayback: syncVideoNodeVisibilityPlayback,
18080
18080
  syncEditingOverlay: syncEditingOverlay,
18081
18081
  syncTextOverlays: syncTextOverlays,
18082
+ renderRemoteFleetMonitorForTest: renderRemoteFleetMonitor,
18083
+ renderRemoteFleetDeviceForTest: renderRemoteFleetDevice,
18082
18084
  renderBusinessAutomationEdges: renderBusinessAutomationEdges,
18083
18085
  scheduleBusinessAutomationEdgeRender: scheduleBusinessAutomationEdgeRender,
18084
18086
  hideBusinessAutomationFloatingTooltipForNode: hideBusinessAutomationFloatingTooltipForNode,
@@ -558,7 +558,7 @@
558
558
  }
559
559
 
560
560
  const base = '_content/MindExecution.Shared/js/';
561
- const scriptVersion = '20260612-remote-device-pin-v470';
561
+ const scriptVersion = '20260612-remote-fleet-render-smoke-v471';
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": "x1hfM46o",
2
+ "version": "SWs2Qkml",
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-LSO5/TRH4+1bNtJc7X09UZHrFfaIF1SkX012PLDVndg=",
89
+ "hash": "sha256-L+uOxULau2wsk0zuWPFd4Ifi5xJUKJ8RAu1JP3JfsL4=",
90
90
  "url": "_content/MindExecution.Shared/js/mind-map-css3d-manager.js"
91
91
  },
92
92
  {
@@ -834,7 +834,7 @@
834
834
  "url": "image-manifest.json"
835
835
  },
836
836
  {
837
- "hash": "sha256-JxMVTLs/BdFFsgJ34YMkq3lh37rBZLnvhbj3f+MxRYo=",
837
+ "hash": "sha256-KfejnUckOnF3WGDdio4k0wnUNz1fpsWmWkKBCXX9mXE=",
838
838
  "url": "index.html"
839
839
  },
840
840
  {
@@ -1,4 +1,4 @@
1
- /* Manifest version: x1hfM46o */
1
+ /* Manifest version: SWs2Qkml */
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