@mindexec/cli 0.2.39 → 0.2.41

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.39",
3
+ "version": "0.2.41",
4
4
  "description": "MindExec local runtime and bridge CLI",
5
5
  "main": "server.js",
6
6
  "type": "module",
package/remote-hub.js CHANGED
@@ -9,6 +9,8 @@ const DEFAULT_AGENT_TASK_TIMEOUT_MS = 120000;
9
9
  const MAX_LINE_CHARS = 4 * 1024 * 1024;
10
10
  const MAX_THUMBNAIL_BASE64_CHARS = 3 * 1024 * 1024;
11
11
  const MAX_STREAM_BASE64_CHARS = 3 * 1024 * 1024;
12
+ const MAX_THUMBNAIL_BINARY_BYTES = Math.floor(MAX_THUMBNAIL_BASE64_CHARS * 3 / 4);
13
+ const MAX_STREAM_BINARY_BYTES = Math.floor(MAX_STREAM_BASE64_CHARS * 3 / 4);
12
14
  const MAX_AGENT_TASK_CHARS = 4000;
13
15
  const MAX_AGENT_TASK_RESULT_CHARS = 3000;
14
16
  const RECENT_TASK_LIMIT = 12;
@@ -1075,6 +1077,161 @@ export function createRemoteHub(options = {}) {
1075
1077
  return task;
1076
1078
  }
1077
1079
 
1080
+ function normalizeFrameByteLength(framePayload, frameData = '') {
1081
+ if (Buffer.isBuffer(framePayload)) {
1082
+ return framePayload.length;
1083
+ }
1084
+
1085
+ return Math.floor(String(frameData || '').length * 3 / 4);
1086
+ }
1087
+
1088
+ function buildFrameDataUrl(framePayload, frameData, mimeType) {
1089
+ if (Buffer.isBuffer(framePayload)) {
1090
+ return `data:${mimeType};base64,${framePayload.toString('base64')}`;
1091
+ }
1092
+
1093
+ return String(frameData || '').startsWith('data:')
1094
+ ? String(frameData || '')
1095
+ : `data:${mimeType};base64,${frameData}`;
1096
+ }
1097
+
1098
+ function applyThumbnailFrame(device, message, framePayload, transport = 'json-base64') {
1099
+ const frameData = Buffer.isBuffer(framePayload)
1100
+ ? ''
1101
+ : safeString(framePayload, MAX_THUMBNAIL_BASE64_CHARS + 1);
1102
+ const frameSeq = Number(message.frameSeq);
1103
+ const byteLength = normalizeFrameByteLength(framePayload, frameData);
1104
+ if ((!Buffer.isBuffer(framePayload) && !frameData)
1105
+ || byteLength > MAX_THUMBNAIL_BINARY_BYTES
1106
+ || !Number.isFinite(frameSeq)) {
1107
+ device.counters.thumbnailFramesDropped += 1;
1108
+ emitRemoteEvent('RemoteFrameDropped', device, {
1109
+ reason: 'invalid-thumbnail-frame',
1110
+ frameSeq: Number.isFinite(frameSeq) ? frameSeq : null,
1111
+ transport
1112
+ });
1113
+ return false;
1114
+ }
1115
+
1116
+ const mimeType = safeString(message.mimeType || message.format || 'image/jpeg', 80) || 'image/jpeg';
1117
+ const capturedAt = safeString(message.capturedAt, 80) || device.lastSeenAt;
1118
+ device.latestThumbnail = {
1119
+ streamId: safeString(message.streamId, 128) || 'thumbnail',
1120
+ frameSeq,
1121
+ commandId: safeString(message.commandId, 128),
1122
+ width: Number.isFinite(Number(message.width)) ? Number(message.width) : 0,
1123
+ height: Number.isFinite(Number(message.height)) ? Number(message.height) : 0,
1124
+ mimeType,
1125
+ format: mimeType,
1126
+ capturedAt,
1127
+ receivedAt: device.lastSeenAt,
1128
+ byteLength,
1129
+ transport,
1130
+ dataUrl: buildFrameDataUrl(framePayload, frameData, mimeType)
1131
+ };
1132
+ device.counters.thumbnailFramesReceived += 1;
1133
+ emitRemoteEvent('RemoteFrameReceived', device, {
1134
+ streamId: device.latestThumbnail.streamId,
1135
+ frameSeq,
1136
+ width: device.latestThumbnail.width,
1137
+ height: device.latestThumbnail.height,
1138
+ transport
1139
+ });
1140
+ return true;
1141
+ }
1142
+
1143
+ function applyLiveFrame(device, message, framePayload, transport = 'json-base64') {
1144
+ const frameData = Buffer.isBuffer(framePayload)
1145
+ ? ''
1146
+ : safeString(framePayload, MAX_STREAM_BASE64_CHARS + 1);
1147
+ const frameSeq = Number(message.frameSeq);
1148
+ const streamId = safeString(message.streamId, 128) || 'live';
1149
+ if (!device.activeLiveStream?.active || device.activeLiveStream.streamId !== streamId) {
1150
+ device.counters.liveFramesDropped += 1;
1151
+ emitRemoteEvent('RemoteFrameDropped', device, {
1152
+ reason: 'stale-live-stream-frame',
1153
+ streamId,
1154
+ frameSeq: Number.isFinite(frameSeq) ? frameSeq : null,
1155
+ transport
1156
+ });
1157
+ return false;
1158
+ }
1159
+
1160
+ const byteLength = normalizeFrameByteLength(framePayload, frameData);
1161
+ if ((!Buffer.isBuffer(framePayload) && !frameData)
1162
+ || byteLength > MAX_STREAM_BINARY_BYTES
1163
+ || !Number.isFinite(frameSeq)) {
1164
+ device.counters.liveFramesDropped += 1;
1165
+ emitRemoteEvent('RemoteFrameDropped', device, {
1166
+ reason: 'invalid-live-frame',
1167
+ streamId,
1168
+ frameSeq: Number.isFinite(frameSeq) ? frameSeq : null,
1169
+ transport
1170
+ });
1171
+ return false;
1172
+ }
1173
+
1174
+ const mimeType = safeString(message.mimeType || message.format || 'image/jpeg', 80) || 'image/jpeg';
1175
+ const capturedAt = safeString(message.capturedAt, 80) || device.lastSeenAt;
1176
+ device.latestLiveFrame = {
1177
+ streamId,
1178
+ frameSeq,
1179
+ commandId: safeString(message.commandId, 128),
1180
+ width: Number.isFinite(Number(message.width)) ? Number(message.width) : 0,
1181
+ height: Number.isFinite(Number(message.height)) ? Number(message.height) : 0,
1182
+ mimeType,
1183
+ format: mimeType,
1184
+ mode: safeString(message.mode || device.activeLiveStream.mode || 'remote-fast', 80),
1185
+ fps: Number.isFinite(Number(message.fps)) ? Number(message.fps) : device.activeLiveStream.fps,
1186
+ capturedAt,
1187
+ receivedAt: device.lastSeenAt,
1188
+ byteLength,
1189
+ transport,
1190
+ dataUrl: buildFrameDataUrl(framePayload, frameData, mimeType)
1191
+ };
1192
+ device.activeLiveStream.lastFrameAt = device.lastSeenAt;
1193
+ device.activeLiveStream.lastFrameSeq = frameSeq;
1194
+ device.activeLiveStream.framesReceived = (device.activeLiveStream.framesReceived || 0) + 1;
1195
+ device.counters.liveFramesReceived += 1;
1196
+ emitRemoteEvent('RemoteFrameReceived', device, {
1197
+ streamId,
1198
+ frameSeq,
1199
+ width: device.latestLiveFrame.width,
1200
+ height: device.latestLiveFrame.height,
1201
+ mode: device.latestLiveFrame.mode,
1202
+ transport
1203
+ });
1204
+ return true;
1205
+ }
1206
+
1207
+ function handleAgentBinaryFrame(socket, state, header, framePayload) {
1208
+ if (!state.authenticated || !state.device) {
1209
+ writeJsonLine(socket, { type: 'error', error: 'hello-required' });
1210
+ socket.destroy();
1211
+ return;
1212
+ }
1213
+
1214
+ const device = state.device;
1215
+ device.counters.messagesReceived += 1;
1216
+ device.lastSeenAt = new Date().toISOString();
1217
+
1218
+ const frameKind = safeString(header.frameKind || header.kind || header.frameType || '', 40).toLowerCase();
1219
+ if (frameKind === 'thumbnail' || header.type === 'thumbnail.binary') {
1220
+ applyThumbnailFrame(device, header, framePayload, 'binary');
1221
+ return;
1222
+ }
1223
+
1224
+ if (frameKind === 'stream' || frameKind === 'live' || header.type === 'stream.binary') {
1225
+ applyLiveFrame(device, header, framePayload, 'binary');
1226
+ return;
1227
+ }
1228
+
1229
+ emitRemoteEvent('RemoteAgentMessageIgnored', device, {
1230
+ messageType: safeString(header.type, 80),
1231
+ frameKind
1232
+ });
1233
+ }
1234
+
1078
1235
  function handleAgentMessage(socket, state, message) {
1079
1236
  if (!message || typeof message !== 'object') {
1080
1237
  return;
@@ -1139,97 +1296,11 @@ export function createRemoteHub(options = {}) {
1139
1296
  });
1140
1297
  break;
1141
1298
  case 'thumbnail.frame': {
1142
- const frameData = safeString(message.data, MAX_THUMBNAIL_BASE64_CHARS + 1);
1143
- const frameSeq = Number(message.frameSeq);
1144
- if (!frameData || frameData.length > MAX_THUMBNAIL_BASE64_CHARS || !Number.isFinite(frameSeq)) {
1145
- device.counters.thumbnailFramesDropped += 1;
1146
- emitRemoteEvent('RemoteFrameDropped', device, {
1147
- reason: 'invalid-thumbnail-frame',
1148
- frameSeq: Number.isFinite(frameSeq) ? frameSeq : null
1149
- });
1150
- break;
1151
- }
1152
-
1153
- const mimeType = safeString(message.mimeType || message.format || 'image/jpeg', 80) || 'image/jpeg';
1154
- const capturedAt = safeString(message.capturedAt, 80) || device.lastSeenAt;
1155
- device.latestThumbnail = {
1156
- streamId: safeString(message.streamId, 128) || 'thumbnail',
1157
- frameSeq,
1158
- commandId: safeString(message.commandId, 128),
1159
- width: Number.isFinite(Number(message.width)) ? Number(message.width) : 0,
1160
- height: Number.isFinite(Number(message.height)) ? Number(message.height) : 0,
1161
- mimeType,
1162
- format: mimeType,
1163
- capturedAt,
1164
- receivedAt: device.lastSeenAt,
1165
- byteLength: Math.floor(frameData.length * 3 / 4),
1166
- dataUrl: frameData.startsWith('data:')
1167
- ? frameData
1168
- : `data:${mimeType};base64,${frameData}`
1169
- };
1170
- device.counters.thumbnailFramesReceived += 1;
1171
- emitRemoteEvent('RemoteFrameReceived', device, {
1172
- streamId: device.latestThumbnail.streamId,
1173
- frameSeq,
1174
- width: device.latestThumbnail.width,
1175
- height: device.latestThumbnail.height
1176
- });
1299
+ applyThumbnailFrame(device, message, message.data, 'json-base64');
1177
1300
  break;
1178
1301
  }
1179
1302
  case 'stream.frame': {
1180
- const frameData = safeString(message.data, MAX_STREAM_BASE64_CHARS + 1);
1181
- const frameSeq = Number(message.frameSeq);
1182
- const streamId = safeString(message.streamId, 128) || 'live';
1183
- if (!device.activeLiveStream?.active || device.activeLiveStream.streamId !== streamId) {
1184
- device.counters.liveFramesDropped += 1;
1185
- emitRemoteEvent('RemoteFrameDropped', device, {
1186
- reason: 'stale-live-stream-frame',
1187
- streamId,
1188
- frameSeq: Number.isFinite(frameSeq) ? frameSeq : null
1189
- });
1190
- break;
1191
- }
1192
-
1193
- if (!frameData || frameData.length > MAX_STREAM_BASE64_CHARS || !Number.isFinite(frameSeq)) {
1194
- device.counters.liveFramesDropped += 1;
1195
- emitRemoteEvent('RemoteFrameDropped', device, {
1196
- reason: 'invalid-live-frame',
1197
- streamId,
1198
- frameSeq: Number.isFinite(frameSeq) ? frameSeq : null
1199
- });
1200
- break;
1201
- }
1202
-
1203
- const mimeType = safeString(message.mimeType || message.format || 'image/jpeg', 80) || 'image/jpeg';
1204
- const capturedAt = safeString(message.capturedAt, 80) || device.lastSeenAt;
1205
- device.latestLiveFrame = {
1206
- streamId,
1207
- frameSeq,
1208
- commandId: safeString(message.commandId, 128),
1209
- width: Number.isFinite(Number(message.width)) ? Number(message.width) : 0,
1210
- height: Number.isFinite(Number(message.height)) ? Number(message.height) : 0,
1211
- mimeType,
1212
- format: mimeType,
1213
- mode: safeString(message.mode || device.activeLiveStream.mode || 'remote-fast', 80),
1214
- fps: Number.isFinite(Number(message.fps)) ? Number(message.fps) : device.activeLiveStream.fps,
1215
- capturedAt,
1216
- receivedAt: device.lastSeenAt,
1217
- byteLength: Math.floor(frameData.length * 3 / 4),
1218
- dataUrl: frameData.startsWith('data:')
1219
- ? frameData
1220
- : `data:${mimeType};base64,${frameData}`
1221
- };
1222
- device.activeLiveStream.lastFrameAt = device.lastSeenAt;
1223
- device.activeLiveStream.lastFrameSeq = frameSeq;
1224
- device.activeLiveStream.framesReceived = (device.activeLiveStream.framesReceived || 0) + 1;
1225
- device.counters.liveFramesReceived += 1;
1226
- emitRemoteEvent('RemoteFrameReceived', device, {
1227
- streamId,
1228
- frameSeq,
1229
- width: device.latestLiveFrame.width,
1230
- height: device.latestLiveFrame.height,
1231
- mode: device.latestLiveFrame.mode
1232
- });
1303
+ applyLiveFrame(device, message, message.data, 'json-base64');
1233
1304
  break;
1234
1305
  }
1235
1306
  default:
@@ -1242,14 +1313,14 @@ export function createRemoteHub(options = {}) {
1242
1313
 
1243
1314
  function handleSocket(socket) {
1244
1315
  allSockets.add(socket);
1245
- socket.setEncoding('utf8');
1246
1316
  socket.setNoDelay(true);
1247
1317
  socket.setKeepAlive(true, heartbeatMs);
1248
1318
 
1249
1319
  const state = {
1250
1320
  authenticated: false,
1251
1321
  device: null,
1252
- buffer: ''
1322
+ buffer: Buffer.alloc(0),
1323
+ pendingBinaryFrame: null
1253
1324
  };
1254
1325
 
1255
1326
  const helloTimer = setTimeout(() => {
@@ -1260,27 +1331,63 @@ export function createRemoteHub(options = {}) {
1260
1331
  }, 10000);
1261
1332
 
1262
1333
  socket.on('data', chunk => {
1263
- state.buffer += chunk;
1264
- if (state.buffer.length > MAX_LINE_CHARS) {
1265
- writeJsonLine(socket, { type: 'error', error: 'message-too-large' });
1266
- socket.destroy();
1267
- return;
1268
- }
1334
+ const incoming = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
1335
+ state.buffer = state.buffer.length > 0
1336
+ ? Buffer.concat([state.buffer, incoming])
1337
+ : incoming;
1338
+
1339
+ while (!socket.destroyed) {
1340
+ if (state.pendingBinaryFrame) {
1341
+ const { header, byteLength } = state.pendingBinaryFrame;
1342
+ if (state.buffer.length < byteLength) {
1343
+ return;
1344
+ }
1345
+
1346
+ const framePayload = state.buffer.subarray(0, byteLength);
1347
+ state.buffer = state.buffer.subarray(byteLength);
1348
+ state.pendingBinaryFrame = null;
1349
+ handleAgentBinaryFrame(socket, state, header, framePayload);
1350
+ continue;
1351
+ }
1352
+
1353
+ const newlineIndex = state.buffer.indexOf(0x0a);
1354
+ if (newlineIndex < 0) {
1355
+ if (state.buffer.length > MAX_LINE_CHARS) {
1356
+ writeJsonLine(socket, { type: 'error', error: 'message-too-large' });
1357
+ socket.destroy();
1358
+ }
1359
+ return;
1360
+ }
1269
1361
 
1270
- let newlineIndex = state.buffer.indexOf('\n');
1271
- while (newlineIndex >= 0) {
1272
- const line = state.buffer.slice(0, newlineIndex);
1273
- state.buffer = state.buffer.slice(newlineIndex + 1);
1362
+ const lineBuffer = state.buffer.subarray(0, newlineIndex);
1363
+ state.buffer = state.buffer.subarray(newlineIndex + 1);
1274
1364
 
1275
1365
  try {
1276
- const message = parseJsonLine(line);
1366
+ const message = parseJsonLine(lineBuffer.toString('utf8'));
1367
+ if (message?.type === 'frame.binary') {
1368
+ const frameKind = safeString(message.frameKind || message.kind || message.frameType, 40).toLowerCase();
1369
+ const maxBytes = frameKind === 'thumbnail'
1370
+ ? MAX_THUMBNAIL_BINARY_BYTES
1371
+ : MAX_STREAM_BINARY_BYTES;
1372
+ const byteLength = Number(message.byteLength ?? message.payloadBytes ?? message.dataLength);
1373
+ if (!Number.isFinite(byteLength) || byteLength < 1 || byteLength > maxBytes) {
1374
+ writeJsonLine(socket, { type: 'error', error: 'invalid-binary-frame' });
1375
+ logWarn('remote', 'invalid binary frame header from agent.');
1376
+ continue;
1377
+ }
1378
+
1379
+ state.pendingBinaryFrame = {
1380
+ header: message,
1381
+ byteLength: Math.floor(byteLength)
1382
+ };
1383
+ continue;
1384
+ }
1385
+
1277
1386
  handleAgentMessage(socket, state, message);
1278
1387
  } catch (err) {
1279
1388
  writeJsonLine(socket, { type: 'error', error: 'invalid-json' });
1280
1389
  logWarn('remote', `invalid agent message: ${err?.message || err}`);
1281
1390
  }
1282
-
1283
- newlineIndex = state.buffer.indexOf('\n');
1284
1391
  }
1285
1392
  });
1286
1393
 
@@ -5,10 +5,13 @@ import net from 'node:net';
5
5
  import { spawn } from 'node:child_process';
6
6
  import path from 'node:path';
7
7
  import { fileURLToPath } from 'node:url';
8
+ import { readFileSync } from 'node:fs';
8
9
 
9
10
  const BRIDGE_TOKEN = 'remote-http-smoke-token';
10
11
  const PAIR_TOKEN = 'remote-http-pair-token';
11
12
  const SYNTHETIC_COUNT = Number(process.env.REMOTE_HTTP_SMOKE_COUNT || 500);
13
+ const LOCAL_BRIDGE_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
14
+ const LOCAL_BRIDGE_PACKAGE = JSON.parse(readFileSync(path.join(LOCAL_BRIDGE_DIR, 'package.json'), 'utf8'));
12
15
 
13
16
  function wait(ms) {
14
17
  return new Promise(resolve => setTimeout(resolve, ms));
@@ -155,7 +158,7 @@ async function runSyntheticEnabledSmoke() {
155
158
  assert.equal(status.started, true);
156
159
  assert.equal(status.port, remoteHubPort);
157
160
  assert.equal(status.managerPackage, '@mindexec/cli');
158
- assert.equal(status.managerVersion, '0.2.25');
161
+ assert.equal(status.managerVersion, LOCAL_BRIDGE_PACKAGE.version);
159
162
  assert.equal(status.agentPackage, '@mindexec/remote');
160
163
  assert.equal(status.canvasPagination, 'none');
161
164
  assert.equal(status.canvasDeviceListMode, 'all-devices');
@@ -8,6 +8,16 @@ function writeJsonLine(socket, payload) {
8
8
  socket.write(`${JSON.stringify(payload)}\n`);
9
9
  }
10
10
 
11
+ function writeBinaryFrame(socket, header, payload) {
12
+ const framePayload = Buffer.isBuffer(payload) ? payload : Buffer.from(payload);
13
+ socket.write(`${JSON.stringify({
14
+ ...header,
15
+ type: 'frame.binary',
16
+ byteLength: framePayload.length
17
+ })}\n`);
18
+ socket.write(framePayload);
19
+ }
20
+
11
21
  function wait(ms) {
12
22
  return new Promise(resolve => setTimeout(resolve, ms));
13
23
  }
@@ -34,6 +44,7 @@ const hub = createRemoteHub({
34
44
  REMOTE_HUB_TASK_TIMEOUT_MS: '120'
35
45
  }
36
46
  });
47
+ const smokePngFrame = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAIAAAABCAYAAAD0In+KAAAADElEQVR42mP8z8AAAAMBAQDJ/pLvAAAAAElFTkSuQmCC', 'base64');
37
48
 
38
49
  try {
39
50
  await hub.start();
@@ -115,6 +126,33 @@ try {
115
126
  assert.equal(thumbnailDevice.latestThumbnail.streamId, 'smoke-thumb');
116
127
  assert.equal(thumbnailDevice.counters.thumbnailFramesReceived, 1);
117
128
 
129
+ const binaryThumbnailCommand = hub.requestThumbnail('smoke-device', {
130
+ streamId: 'smoke-thumb-binary',
131
+ maxWidth: 320,
132
+ maxHeight: 180,
133
+ quality: 50
134
+ });
135
+ assert.equal(binaryThumbnailCommand.ok, true);
136
+ writeBinaryFrame(socket, {
137
+ frameKind: 'thumbnail',
138
+ commandId: binaryThumbnailCommand.commandId,
139
+ streamId: 'smoke-thumb-binary',
140
+ frameSeq: 4,
141
+ width: 2,
142
+ height: 1,
143
+ mimeType: 'image/png',
144
+ capturedAt: new Date().toISOString()
145
+ }, smokePngFrame);
146
+
147
+ const binaryThumbnailDevice = await waitFor(() => {
148
+ const current = hub.listDevices();
149
+ return current[0]?.latestThumbnail?.frameSeq === 4 ? current[0] : null;
150
+ });
151
+ assert.equal(binaryThumbnailDevice.latestThumbnail.streamId, 'smoke-thumb-binary');
152
+ assert.equal(binaryThumbnailDevice.latestThumbnail.transport, 'binary');
153
+ assert.equal(binaryThumbnailDevice.latestThumbnail.byteLength, smokePngFrame.length);
154
+ assert.equal(binaryThumbnailDevice.counters.thumbnailFramesReceived, 2);
155
+
118
156
  const liveCommand = hub.startLiveStream('smoke-device', {
119
157
  streamId: 'smoke-live',
120
158
  fps: 5,
@@ -145,6 +183,28 @@ try {
145
183
  assert.equal(liveDevice.latestLiveFrame.mode, 'remote-fast');
146
184
  assert.equal(liveDevice.counters.liveFramesReceived, 1);
147
185
 
186
+ writeBinaryFrame(socket, {
187
+ frameKind: 'stream',
188
+ commandId: liveCommand.commandId,
189
+ streamId: 'smoke-live',
190
+ frameSeq: 5,
191
+ width: 2,
192
+ height: 1,
193
+ mimeType: 'image/png',
194
+ mode: 'remote-fast',
195
+ fps: 5,
196
+ capturedAt: new Date().toISOString()
197
+ }, smokePngFrame);
198
+
199
+ const binaryLiveDevice = await waitFor(() => {
200
+ const current = hub.listDevices();
201
+ return current[0]?.latestLiveFrame?.frameSeq === 5 ? current[0] : null;
202
+ });
203
+ assert.equal(binaryLiveDevice.latestLiveFrame.streamId, 'smoke-live');
204
+ assert.equal(binaryLiveDevice.latestLiveFrame.transport, 'binary');
205
+ assert.equal(binaryLiveDevice.latestLiveFrame.byteLength, smokePngFrame.length);
206
+ assert.equal(binaryLiveDevice.counters.liveFramesReceived, 2);
207
+
148
208
  const staleFrameBefore = liveDevice.counters.liveFramesDropped;
149
209
  writeJsonLine(socket, {
150
210
  type: 'stream.frame',
@@ -5,7 +5,7 @@
5
5
  const DEBUG = false;
6
6
  const FPS_DEBUG = false;
7
7
  const FRAME_PERF_DEBUG = false;
8
- const MINDMAP_CORE_BUILD_ID = '20260613-template-current-canvas-v213';
8
+ const MINDMAP_CORE_BUILD_ID = '20260613-remote-monitor-gap-v214';
9
9
  const CanvasPhase = Object.freeze({
10
10
  Booting: 'booting',
11
11
  BoardFileLoading: 'board-file-loading',
@@ -3405,6 +3405,9 @@
3405
3405
  const REMOTE_FLEET_SEMANTIC_TYPE = 'RemoteFleetMonitor';
3406
3406
  const REMOTE_FLEET_DEVICE_SEMANTIC_TYPE = 'RemoteFleetDevice';
3407
3407
  const REMOTE_FLEET_CENTER_NUDGE_PX = 4;
3408
+ const REMOTE_FLEET_MONITOR_TILE_GAP_PX = 8;
3409
+ const REMOTE_FLEET_EMPTY_SCREEN_MIN_WIDTH = 132;
3410
+ const REMOTE_FLEET_EMPTY_SCREEN_MIN_HEIGHT = 72;
3408
3411
  const AUTOMATION_NODE_KIND_METADATA_KEY = 'AutomationNodeKind';
3409
3412
  const AUTOMATION_NODE_LABEL_METADATA_KEY = 'AutomationNodeLabel';
3410
3413
  const AUTOMATION_NODE_DESCRIPTION_METADATA_KEY = 'AutomationNodeDescription';
@@ -12475,8 +12478,8 @@
12475
12478
  const height = Number(nodeModel?.height ?? nodeModel?.Height ?? 620);
12476
12479
  const usableWidth = Number.isFinite(width) ? Math.max(360, width - 44) : 916;
12477
12480
  const usableHeight = Number.isFinite(height) ? Math.max(260, height - 128) : 492;
12478
- const columns = Math.max(3, Math.floor(usableWidth / 142));
12479
- const rows = Math.max(3, Math.ceil(usableHeight / 88));
12481
+ const columns = Math.max(3, Math.floor((usableWidth + REMOTE_FLEET_MONITOR_TILE_GAP_PX) / (REMOTE_FLEET_EMPTY_SCREEN_MIN_WIDTH + REMOTE_FLEET_MONITOR_TILE_GAP_PX)));
12482
+ const rows = Math.max(3, Math.ceil((usableHeight + REMOTE_FLEET_MONITOR_TILE_GAP_PX) / (REMOTE_FLEET_EMPTY_SCREEN_MIN_HEIGHT + REMOTE_FLEET_MONITOR_TILE_GAP_PX)));
12480
12483
  return Math.max(12, Math.min(80, columns * rows));
12481
12484
  }
12482
12485
 
@@ -12486,19 +12489,20 @@
12486
12489
  shell.style.cssText = `
12487
12490
  flex: 1 1 auto;
12488
12491
  min-height: 0;
12489
- overflow: hidden;
12492
+ overflow-y: auto;
12493
+ overflow-x: hidden;
12490
12494
  display: grid;
12491
- grid-template-columns: repeat(auto-fill, minmax(132px, 1fr));
12492
- grid-auto-rows: minmax(72px, auto);
12495
+ grid-template-columns: repeat(auto-fill, minmax(${REMOTE_FLEET_EMPTY_SCREEN_MIN_WIDTH}px, 1fr));
12496
+ grid-auto-rows: max-content;
12493
12497
  align-content: start;
12498
+ align-items: start;
12494
12499
  justify-content: center;
12495
12500
  justify-items: stretch;
12496
12501
  align-self: center;
12497
12502
  width: calc(100% - ${REMOTE_FLEET_CENTER_NUDGE_PX * 2}px);
12498
12503
  max-width: 100%;
12499
12504
  transform: translateX(${REMOTE_FLEET_CENTER_NUDGE_PX}px);
12500
- column-gap: 8px;
12501
- row-gap: 8px;
12505
+ gap: ${REMOTE_FLEET_MONITOR_TILE_GAP_PX}px;
12502
12506
  box-sizing: border-box;
12503
12507
  padding: 2px 4px 6px 4px;
12504
12508
  `;
@@ -12508,8 +12512,10 @@
12508
12512
  const screen = document.createElement('div');
12509
12513
  screen.dataset.remoteFleetEmptyScreen = 'true';
12510
12514
  screen.style.cssText = `
12515
+ width: 100%;
12516
+ height: auto;
12511
12517
  aspect-ratio: 16 / 9;
12512
- min-height: 72px;
12518
+ min-height: ${REMOTE_FLEET_EMPTY_SCREEN_MIN_HEIGHT}px;
12513
12519
  box-sizing: border-box;
12514
12520
  border-radius: 8px;
12515
12521
  border: 1px solid rgba(203, 213, 225, 0.72);
@@ -14287,7 +14293,7 @@
14287
14293
 
14288
14294
  const tileMinWidth = densityState === 'dense' ? 104 : 132;
14289
14295
  const tileMinHeight = densityState === 'dense' ? 66 : 84;
14290
- const tileGap = 8;
14296
+ const tileGap = REMOTE_FLEET_MONITOR_TILE_GAP_PX;
14291
14297
 
14292
14298
  const monitorWorkspace = document.createElement('div');
14293
14299
  monitorWorkspace.dataset.remoteFleetMonitorWorkspace = 'true';
@@ -14310,17 +14316,17 @@
14310
14316
  overflow-x: hidden;
14311
14317
  display: grid;
14312
14318
  grid-template-columns: repeat(auto-fill, minmax(${tileMinWidth}px, 1fr));
14313
- grid-auto-rows: minmax(${tileMinHeight}px, auto);
14319
+ grid-auto-rows: max-content;
14314
14320
  grid-auto-flow: row;
14315
14321
  align-content: start;
14322
+ align-items: start;
14316
14323
  justify-content: center;
14317
14324
  justify-items: stretch;
14318
14325
  justify-self: center;
14319
14326
  width: calc(100% - ${REMOTE_FLEET_CENTER_NUDGE_PX * 2}px);
14320
14327
  max-width: 100%;
14321
14328
  transform: translateX(${REMOTE_FLEET_CENTER_NUDGE_PX}px);
14322
- column-gap: ${tileGap}px;
14323
- row-gap: ${tileGap}px;
14329
+ gap: ${tileGap}px;
14324
14330
  box-sizing: border-box;
14325
14331
  padding: 0 4px;
14326
14332
  `;
@@ -14415,6 +14421,8 @@
14415
14421
  display: flex;
14416
14422
  align-items: stretch;
14417
14423
  min-width: 0;
14424
+ width: 100%;
14425
+ height: auto;
14418
14426
  min-height: ${tileMinHeight}px;
14419
14427
  aspect-ratio: 16 / 9;
14420
14428
  box-sizing: border-box;
@@ -558,7 +558,7 @@
558
558
  }
559
559
 
560
560
  const base = '_content/MindExecution.Shared/js/';
561
- const scriptVersion = '20260613-template-current-canvas-v507';
561
+ const scriptVersion = '20260613-remote-monitor-gap-v508';
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": "yVKOGKeq",
2
+ "version": "P5rJ7sPB",
3
3
  "assets": [
4
4
  {
5
5
  "hash": "sha256-+CSYMcqLNTsq3VnH11jgYyOCCdxvHzL74CBmo4sCmMU=",
@@ -78,7 +78,7 @@
78
78
  "url": "_content/MindExecution.Shared/js/marked.min.js"
79
79
  },
80
80
  {
81
- "hash": "sha256-XaxBBTGHhZrW3oi5I1lPG/e1TW5seriQfInR9zw4AsI=",
81
+ "hash": "sha256-tS2NkdGFK7jWKDH5NTbXqmJ+vgtdIuHB7akcpMxvC84=",
82
82
  "url": "_content/MindExecution.Shared/js/mind-map-core.js"
83
83
  },
84
84
  {
@@ -86,7 +86,7 @@
86
86
  "url": "_content/MindExecution.Shared/js/mind-map-core.js.backup"
87
87
  },
88
88
  {
89
- "hash": "sha256-qup69YZeiW9fSkNG+pViWzQ8eT5NNxuzX7ofwnc0vvI=",
89
+ "hash": "sha256-jrzXaQSlY2BHJW062PzZuylbAEYe02+uQrMQyJt7FJA=",
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-lpSXakW1b+qoRoSLyzdSZY5hQCckBnnUmCJGa5EDWdQ=",
837
+ "hash": "sha256-bjvQNTq7IFWd435tpUl5xUScla/vHBE+JHSWSDH464c=",
838
838
  "url": "index.html"
839
839
  },
840
840
  {
@@ -1,4 +1,4 @@
1
- /* Manifest version: yVKOGKeq */
1
+ /* Manifest version: P5rJ7sPB */
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