@mindexec/cli 0.2.19 → 0.2.21
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 +6 -5
- package/scripts/remote-fleet-render-smoke.mjs +4 -0
- package/scripts/remote-http-smoke.mjs +267 -0
- package/server.js +1 -0
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-css3d-manager.js +165 -0
- package/wwwroot/index.html +1 -1
- package/wwwroot/service-worker-assets.js +3 -3
- package/wwwroot/service-worker.js +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mindexec/cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.21",
|
|
4
4
|
"description": "MindExec local runtime and bridge CLI",
|
|
5
5
|
"main": "server.js",
|
|
6
6
|
"type": "module",
|
|
@@ -20,10 +20,11 @@
|
|
|
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 && node --check scripts/remote-fleet-render-smoke.mjs",
|
|
24
|
-
"test:remote": "node scripts/remote-hub-smoke.mjs",
|
|
25
|
-
"test:remote:scale": "node scripts/remote-hub-scale-smoke.mjs",
|
|
26
|
-
"test:remote:render": "node scripts/remote-fleet-render-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 && node --check scripts/remote-http-smoke.mjs",
|
|
24
|
+
"test:remote": "node scripts/remote-hub-smoke.mjs",
|
|
25
|
+
"test:remote:scale": "node scripts/remote-hub-scale-smoke.mjs",
|
|
26
|
+
"test:remote:render": "node scripts/remote-fleet-render-smoke.mjs",
|
|
27
|
+
"test:remote:http": "node scripts/remote-http-smoke.mjs",
|
|
27
28
|
"pack:dry": "npm pack --dry-run",
|
|
28
29
|
"setup:grammars": "node scripts/setup-tree-sitter-grammars.mjs",
|
|
29
30
|
"postinstall": "npm run setup:grammars"
|
|
@@ -652,6 +652,10 @@ try {
|
|
|
652
652
|
assert.ok(resultPanel.textContent.includes('synthetic-render-ai'));
|
|
653
653
|
assert.ok(resultPanel.textContent.includes('Overview AI result visible'));
|
|
654
654
|
assert.ok(devices.some(device => /^synthetic-response-/.test(device.LatestTaskResultResponseId)));
|
|
655
|
+
await wait(320);
|
|
656
|
+
assert.ok(dotNetCalls.some(call => call.methodName === 'RefreshRemoteFleetMonitorNodeFromJs'));
|
|
657
|
+
assert.equal(bodyView.dataset.remoteFleetTaskFollowKey, 'render-smoke-batch');
|
|
658
|
+
assert.ok(Number(bodyView.dataset.remoteFleetTaskFollowTicks || '0') >= 1);
|
|
655
659
|
|
|
656
660
|
const searchInput = bodyView.querySelector('[data-remote-fleet-search="true"]');
|
|
657
661
|
searchInput.value = 'synthetic pc 0001';
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import assert from 'node:assert/strict';
|
|
4
|
+
import net from 'node:net';
|
|
5
|
+
import { spawn } from 'node:child_process';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
|
|
9
|
+
const BRIDGE_TOKEN = 'remote-http-smoke-token';
|
|
10
|
+
const PAIR_TOKEN = 'remote-http-pair-token';
|
|
11
|
+
const SYNTHETIC_COUNT = Number(process.env.REMOTE_HTTP_SMOKE_COUNT || 250);
|
|
12
|
+
|
|
13
|
+
function wait(ms) {
|
|
14
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function findFreePort() {
|
|
18
|
+
return await new Promise((resolve, reject) => {
|
|
19
|
+
const server = net.createServer();
|
|
20
|
+
server.unref();
|
|
21
|
+
server.once('error', reject);
|
|
22
|
+
server.listen(0, '127.0.0.1', () => {
|
|
23
|
+
const address = server.address();
|
|
24
|
+
const port = typeof address === 'object' && address ? address.port : 0;
|
|
25
|
+
server.close(() => resolve(port));
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function fetchJson(url, options = {}) {
|
|
31
|
+
const response = await fetch(url, {
|
|
32
|
+
...options,
|
|
33
|
+
headers: {
|
|
34
|
+
...(options.body ? { 'Content-Type': 'application/json' } : {}),
|
|
35
|
+
...(options.token ? { 'X-Bridge-Token': options.token } : {}),
|
|
36
|
+
...(options.headers || {})
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
let payload = null;
|
|
41
|
+
try {
|
|
42
|
+
payload = await response.json();
|
|
43
|
+
} catch {
|
|
44
|
+
// Some failure responses may be empty.
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
status: response.status,
|
|
49
|
+
ok: response.ok,
|
|
50
|
+
payload
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function waitForBridge(baseUrl, getFailureDetails) {
|
|
55
|
+
const startedAt = Date.now();
|
|
56
|
+
while (Date.now() - startedAt < 30000) {
|
|
57
|
+
try {
|
|
58
|
+
const result = await fetchJson(`${baseUrl}/api/remote/status`, { token: BRIDGE_TOKEN });
|
|
59
|
+
if (result.ok && result.payload?.started === true) {
|
|
60
|
+
return result.payload;
|
|
61
|
+
}
|
|
62
|
+
} catch {
|
|
63
|
+
// Server is still starting.
|
|
64
|
+
}
|
|
65
|
+
await wait(100);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
throw new Error(`Timed out waiting for LocalBridge RemoteHub HTTP API.\n${getFailureDetails()}`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
72
|
+
const bridgeRoot = path.resolve(__dirname, '..');
|
|
73
|
+
const bridgePort = await findFreePort();
|
|
74
|
+
const remoteHubPort = await findFreePort();
|
|
75
|
+
const baseUrl = `http://127.0.0.1:${bridgePort}`;
|
|
76
|
+
|
|
77
|
+
const child = spawn(process.execPath, ['server.js'], {
|
|
78
|
+
cwd: bridgeRoot,
|
|
79
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
80
|
+
windowsHide: true,
|
|
81
|
+
env: {
|
|
82
|
+
...process.env,
|
|
83
|
+
BRIDGE_PORT: String(bridgePort),
|
|
84
|
+
BRIDGE_TOKEN,
|
|
85
|
+
BRIDGE_REQUIRE_TOKEN: '1',
|
|
86
|
+
MINDEXEC_REMOTE_HUB: '1',
|
|
87
|
+
REMOTE_HUB_HOST: '127.0.0.1',
|
|
88
|
+
REMOTE_HUB_PORT: String(remoteHubPort),
|
|
89
|
+
REMOTE_HUB_PAIR_TOKEN: PAIR_TOKEN,
|
|
90
|
+
MINDEXEC_REMOTE_SYNTHETIC_FLEET: '1',
|
|
91
|
+
NO_COLOR: '1'
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
let stdout = '';
|
|
96
|
+
let stderr = '';
|
|
97
|
+
child.stdout.on('data', chunk => {
|
|
98
|
+
stdout += chunk.toString();
|
|
99
|
+
});
|
|
100
|
+
child.stderr.on('data', chunk => {
|
|
101
|
+
stderr += chunk.toString();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const exitPromise = new Promise(resolve => child.once('exit', resolve));
|
|
105
|
+
const details = () => `stdout=${stdout}\nstderr=${stderr}`;
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
const status = await waitForBridge(baseUrl, details);
|
|
109
|
+
assert.equal(status.started, true);
|
|
110
|
+
assert.equal(status.port, remoteHubPort);
|
|
111
|
+
assert.equal(status.agentPackage, '@mindexec/remote');
|
|
112
|
+
assert.equal(status.canvasPagination, 'none');
|
|
113
|
+
assert.equal(status.canvasDeviceListMode, 'all-devices');
|
|
114
|
+
assert.equal(status.pairToken, PAIR_TOKEN);
|
|
115
|
+
|
|
116
|
+
const unauthorizedStatus = await fetchJson(`${baseUrl}/api/remote/status`);
|
|
117
|
+
assert.equal(unauthorizedStatus.status, 401);
|
|
118
|
+
assert.equal(unauthorizedStatus.payload?.header, 'X-Bridge-Token');
|
|
119
|
+
|
|
120
|
+
const unauthorizedDelete = await fetchJson(`${baseUrl}/api/remote/synthetic`, {
|
|
121
|
+
method: 'DELETE'
|
|
122
|
+
});
|
|
123
|
+
assert.equal(unauthorizedDelete.status, 401);
|
|
124
|
+
|
|
125
|
+
const seed = await fetchJson(`${baseUrl}/api/remote/synthetic/seed`, {
|
|
126
|
+
method: 'POST',
|
|
127
|
+
token: BRIDGE_TOKEN,
|
|
128
|
+
body: JSON.stringify({
|
|
129
|
+
count: SYNTHETIC_COUNT,
|
|
130
|
+
connectedRatio: 0.84,
|
|
131
|
+
thumbnailRatio: 0.72,
|
|
132
|
+
aiAssistRatio: 0.42,
|
|
133
|
+
liveCount: 8,
|
|
134
|
+
replace: true
|
|
135
|
+
})
|
|
136
|
+
});
|
|
137
|
+
assert.equal(seed.ok, true, JSON.stringify(seed.payload));
|
|
138
|
+
assert.equal(seed.payload?.ok, true);
|
|
139
|
+
assert.equal(seed.payload?.seeded, SYNTHETIC_COUNT);
|
|
140
|
+
|
|
141
|
+
const devicesResult = await fetchJson(`${baseUrl}/api/remote/devices`, { token: BRIDGE_TOKEN });
|
|
142
|
+
assert.equal(devicesResult.ok, true, JSON.stringify(devicesResult.payload));
|
|
143
|
+
assert.equal(devicesResult.payload?.total, SYNTHETIC_COUNT);
|
|
144
|
+
assert.equal(devicesResult.payload?.pagination, 'none');
|
|
145
|
+
assert.equal(devicesResult.payload?.canvasDeviceListMode, 'all-devices');
|
|
146
|
+
assert.equal(devicesResult.payload?.devices?.length, SYNTHETIC_COUNT);
|
|
147
|
+
assert.equal(devicesResult.payload.devices.some(device => device.connected === false), true);
|
|
148
|
+
|
|
149
|
+
const connectedTargets = devicesResult.payload.devices
|
|
150
|
+
.filter(device => device.connected && device.capabilities?.taskDispatch)
|
|
151
|
+
.slice(0, 200)
|
|
152
|
+
.map(device => device.deviceId);
|
|
153
|
+
assert.ok(connectedTargets.length >= 100);
|
|
154
|
+
|
|
155
|
+
const batch = await fetchJson(`${baseUrl}/api/remote/tasks`, {
|
|
156
|
+
method: 'POST',
|
|
157
|
+
token: BRIDGE_TOKEN,
|
|
158
|
+
body: JSON.stringify({
|
|
159
|
+
deviceIds: connectedTargets,
|
|
160
|
+
allConnected: false,
|
|
161
|
+
title: 'HTTP smoke task batch',
|
|
162
|
+
instruction: 'HTTP smoke batch: report current status to the manager.',
|
|
163
|
+
approvalLevel: 'task-only'
|
|
164
|
+
})
|
|
165
|
+
});
|
|
166
|
+
assert.equal(batch.ok, true, JSON.stringify(batch.payload));
|
|
167
|
+
assert.equal(batch.payload?.ok, true);
|
|
168
|
+
assert.equal(batch.payload?.total, connectedTargets.length);
|
|
169
|
+
assert.equal(batch.payload?.queued, connectedTargets.length);
|
|
170
|
+
assert.equal(batch.payload?.batch?.completed, connectedTargets.length);
|
|
171
|
+
assert.equal(batch.payload?.batch?.failed, 0);
|
|
172
|
+
assert.equal(batch.payload?.batch?.status, 'completed');
|
|
173
|
+
|
|
174
|
+
const aiTarget = devicesResult.payload.devices.find(device =>
|
|
175
|
+
device.connected && device.capabilities?.taskDispatch && device.capabilities?.aiAssist);
|
|
176
|
+
assert.ok(aiTarget);
|
|
177
|
+
const aiTask = await fetchJson(`${baseUrl}/api/remote/devices/${encodeURIComponent(aiTarget.deviceId)}/tasks`, {
|
|
178
|
+
method: 'POST',
|
|
179
|
+
token: BRIDGE_TOKEN,
|
|
180
|
+
body: JSON.stringify({
|
|
181
|
+
title: 'HTTP smoke AI task',
|
|
182
|
+
instruction: 'HTTP smoke AI assist: summarize fleet condition.',
|
|
183
|
+
approvalLevel: 'ai-assist'
|
|
184
|
+
})
|
|
185
|
+
});
|
|
186
|
+
assert.equal(aiTask.ok, true, JSON.stringify(aiTask.payload));
|
|
187
|
+
assert.equal(aiTask.payload?.ok, true);
|
|
188
|
+
assert.equal(aiTask.payload?.approvalLevel, 'ai-assist');
|
|
189
|
+
|
|
190
|
+
const thumbnailTarget = devicesResult.payload.devices.find(device =>
|
|
191
|
+
device.connected && device.capabilities?.thumbnail);
|
|
192
|
+
assert.ok(thumbnailTarget);
|
|
193
|
+
const thumbnailRequest = await fetchJson(`${baseUrl}/api/remote/devices/${encodeURIComponent(thumbnailTarget.deviceId)}/thumbnail/request`, {
|
|
194
|
+
method: 'POST',
|
|
195
|
+
token: BRIDGE_TOKEN,
|
|
196
|
+
body: JSON.stringify({
|
|
197
|
+
streamId: 'http-smoke-thumb',
|
|
198
|
+
maxWidth: 360,
|
|
199
|
+
maxHeight: 220,
|
|
200
|
+
quality: 50
|
|
201
|
+
})
|
|
202
|
+
});
|
|
203
|
+
assert.equal(thumbnailRequest.ok, true, JSON.stringify(thumbnailRequest.payload));
|
|
204
|
+
assert.equal(thumbnailRequest.payload?.ok, true);
|
|
205
|
+
|
|
206
|
+
const thumbnail = await fetchJson(`${baseUrl}/api/remote/devices/${encodeURIComponent(thumbnailTarget.deviceId)}/thumbnail`, {
|
|
207
|
+
token: BRIDGE_TOKEN
|
|
208
|
+
});
|
|
209
|
+
assert.equal(thumbnail.ok, true, JSON.stringify(thumbnail.payload));
|
|
210
|
+
assert.equal(thumbnail.payload?.thumbnail?.streamId, 'http-smoke-thumb');
|
|
211
|
+
assert.ok(String(thumbnail.payload?.thumbnail?.dataUrl || '').startsWith('data:image/png;base64,'));
|
|
212
|
+
|
|
213
|
+
const liveTarget = devicesResult.payload.devices.find(device =>
|
|
214
|
+
device.connected && device.capabilities?.liveStream);
|
|
215
|
+
assert.ok(liveTarget);
|
|
216
|
+
const liveStart = await fetchJson(`${baseUrl}/api/remote/devices/${encodeURIComponent(liveTarget.deviceId)}/live/start`, {
|
|
217
|
+
method: 'POST',
|
|
218
|
+
token: BRIDGE_TOKEN,
|
|
219
|
+
body: JSON.stringify({
|
|
220
|
+
streamId: 'http-smoke-live',
|
|
221
|
+
fps: 12,
|
|
222
|
+
maxWidth: 960,
|
|
223
|
+
maxHeight: 540,
|
|
224
|
+
quality: 60
|
|
225
|
+
})
|
|
226
|
+
});
|
|
227
|
+
assert.equal(liveStart.ok, true, JSON.stringify(liveStart.payload));
|
|
228
|
+
assert.equal(liveStart.payload?.ok, true);
|
|
229
|
+
|
|
230
|
+
const liveFrame = await fetchJson(`${baseUrl}/api/remote/devices/${encodeURIComponent(liveTarget.deviceId)}/live/frame`, {
|
|
231
|
+
token: BRIDGE_TOKEN
|
|
232
|
+
});
|
|
233
|
+
assert.equal(liveFrame.ok, true, JSON.stringify(liveFrame.payload));
|
|
234
|
+
assert.equal(liveFrame.payload?.frame?.streamId, 'http-smoke-live');
|
|
235
|
+
assert.equal(liveFrame.payload?.frame?.mode, 'remote-fast');
|
|
236
|
+
|
|
237
|
+
const liveStop = await fetchJson(`${baseUrl}/api/remote/devices/${encodeURIComponent(liveTarget.deviceId)}/live/stop`, {
|
|
238
|
+
method: 'POST',
|
|
239
|
+
token: BRIDGE_TOKEN,
|
|
240
|
+
body: JSON.stringify({
|
|
241
|
+
streamId: 'http-smoke-live'
|
|
242
|
+
})
|
|
243
|
+
});
|
|
244
|
+
assert.equal(liveStop.ok, true, JSON.stringify(liveStop.payload));
|
|
245
|
+
assert.equal(liveStop.payload?.ok, true);
|
|
246
|
+
|
|
247
|
+
const clear = await fetchJson(`${baseUrl}/api/remote/synthetic`, {
|
|
248
|
+
method: 'DELETE',
|
|
249
|
+
token: BRIDGE_TOKEN
|
|
250
|
+
});
|
|
251
|
+
assert.equal(clear.ok, true, JSON.stringify(clear.payload));
|
|
252
|
+
assert.equal(clear.payload?.ok, true);
|
|
253
|
+
assert.equal(clear.payload?.removed, SYNTHETIC_COUNT);
|
|
254
|
+
|
|
255
|
+
console.log(`RemoteHub HTTP smoke OK (${SYNTHETIC_COUNT} synthetic devices, bridge ${bridgePort}, remote ${remoteHubPort})`);
|
|
256
|
+
} finally {
|
|
257
|
+
if (child.exitCode === null && !child.killed) {
|
|
258
|
+
child.kill('SIGTERM');
|
|
259
|
+
await Promise.race([
|
|
260
|
+
exitPromise,
|
|
261
|
+
wait(5000)
|
|
262
|
+
]);
|
|
263
|
+
if (child.exitCode === null && !child.killed) {
|
|
264
|
+
child.kill('SIGKILL');
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
package/server.js
CHANGED
|
@@ -1750,6 +1750,7 @@ const PROTECTED_BRIDGE_ROUTES = [
|
|
|
1750
1750
|
{ method: 'POST', prefix: '/api/shell/' },
|
|
1751
1751
|
{ method: 'GET', prefix: '/api/remote/' },
|
|
1752
1752
|
{ method: 'POST', prefix: '/api/remote/' },
|
|
1753
|
+
{ method: 'DELETE', prefix: '/api/remote/' },
|
|
1753
1754
|
{ method: 'POST', prefix: '/project/' },
|
|
1754
1755
|
{ method: 'POST', exact: '/agent/connect' },
|
|
1755
1756
|
{ method: 'POST', exact: '/api/tool/trace' }
|
|
@@ -12005,6 +12005,73 @@
|
|
|
12005
12005
|
}
|
|
12006
12006
|
}
|
|
12007
12007
|
|
|
12008
|
+
function getRemoteFleetTaskBatchValue(batch, camelKey, pascalKey, fallback = '') {
|
|
12009
|
+
const value = batch?.[camelKey] ?? batch?.[pascalKey] ?? fallback;
|
|
12010
|
+
return value === undefined || value === null ? fallback : value;
|
|
12011
|
+
}
|
|
12012
|
+
|
|
12013
|
+
function getRemoteFleetTaskBatchNumber(batch, camelKey, pascalKey) {
|
|
12014
|
+
const value = Number(getRemoteFleetTaskBatchValue(batch, camelKey, pascalKey, 0));
|
|
12015
|
+
return Number.isFinite(value) ? value : 0;
|
|
12016
|
+
}
|
|
12017
|
+
|
|
12018
|
+
function getRemoteFleetTaskBatchKey(batch) {
|
|
12019
|
+
if (!batch || typeof batch !== 'object') {
|
|
12020
|
+
return '';
|
|
12021
|
+
}
|
|
12022
|
+
|
|
12023
|
+
const batchId = String(getRemoteFleetTaskBatchValue(batch, 'batchId', 'BatchId', '')).trim();
|
|
12024
|
+
if (batchId) {
|
|
12025
|
+
return batchId;
|
|
12026
|
+
}
|
|
12027
|
+
|
|
12028
|
+
const requestedAt = String(getRemoteFleetTaskBatchValue(batch, 'requestedAt', 'RequestedAt', '')).trim();
|
|
12029
|
+
const updatedAt = String(getRemoteFleetTaskBatchValue(batch, 'updatedAt', 'UpdatedAt', '')).trim();
|
|
12030
|
+
const title = String(getRemoteFleetTaskBatchValue(batch, 'title', 'Title', '')).trim();
|
|
12031
|
+
const total = getRemoteFleetTaskBatchNumber(batch, 'total', 'Total');
|
|
12032
|
+
return [title, requestedAt, updatedAt, total].filter(Boolean).join(':');
|
|
12033
|
+
}
|
|
12034
|
+
|
|
12035
|
+
function isRemoteFleetTaskBatchActive(batch) {
|
|
12036
|
+
if (!batch || typeof batch !== 'object') {
|
|
12037
|
+
return false;
|
|
12038
|
+
}
|
|
12039
|
+
|
|
12040
|
+
const status = String(getRemoteFleetTaskBatchValue(batch, 'status', 'Status', '')).trim().toLowerCase();
|
|
12041
|
+
const pending = getRemoteFleetTaskBatchNumber(batch, 'pending', 'Pending');
|
|
12042
|
+
const total = getRemoteFleetTaskBatchNumber(batch, 'total', 'Total');
|
|
12043
|
+
const completed = getRemoteFleetTaskBatchNumber(batch, 'completed', 'Completed');
|
|
12044
|
+
const failed = getRemoteFleetTaskBatchNumber(batch, 'failed', 'Failed');
|
|
12045
|
+
const terminalStatuses = new Set(['completed', 'completed-with-failures', 'failed', 'cancelled', 'canceled', 'done']);
|
|
12046
|
+
if (terminalStatuses.has(status)) {
|
|
12047
|
+
return false;
|
|
12048
|
+
}
|
|
12049
|
+
|
|
12050
|
+
if (pending > 0) {
|
|
12051
|
+
return true;
|
|
12052
|
+
}
|
|
12053
|
+
|
|
12054
|
+
if (['running', 'queued', 'pending', 'targeted', 'dispatching', 'in-progress'].includes(status)) {
|
|
12055
|
+
return true;
|
|
12056
|
+
}
|
|
12057
|
+
|
|
12058
|
+
return total > 0 && completed + failed < total;
|
|
12059
|
+
}
|
|
12060
|
+
|
|
12061
|
+
function getRemoteFleetTaskBatchFromResult(result) {
|
|
12062
|
+
const direct = result?.batch
|
|
12063
|
+
?? result?.Batch
|
|
12064
|
+
?? result?.taskBatch
|
|
12065
|
+
?? result?.TaskBatch
|
|
12066
|
+
?? result?.latestTaskBatch
|
|
12067
|
+
?? result?.LatestTaskBatch
|
|
12068
|
+
?? result?.refresh?.batch
|
|
12069
|
+
?? result?.refresh?.Batch;
|
|
12070
|
+
return direct && typeof direct === 'object' && !Array.isArray(direct)
|
|
12071
|
+
? direct
|
|
12072
|
+
: null;
|
|
12073
|
+
}
|
|
12074
|
+
|
|
12008
12075
|
function parseRemoteFleetPinnedDevice(nodeModel) {
|
|
12009
12076
|
const raw = getRemoteFleetMetadataValue(nodeModel, 'RemoteFleetPinnedDeviceJson', '{}');
|
|
12010
12077
|
if (!raw.trim()) {
|
|
@@ -12174,6 +12241,9 @@
|
|
|
12174
12241
|
|
|
12175
12242
|
const REMOTE_FLEET_MONITOR_REFRESH_MS = 5000;
|
|
12176
12243
|
const REMOTE_FLEET_LIVE_REFRESH_MS = 1000;
|
|
12244
|
+
const REMOTE_FLEET_TASK_FOLLOW_INITIAL_MS = 250;
|
|
12245
|
+
const REMOTE_FLEET_TASK_FOLLOW_REFRESH_MS = 2000;
|
|
12246
|
+
const REMOTE_FLEET_TASK_FOLLOW_MAX_TICKS = 60;
|
|
12177
12247
|
|
|
12178
12248
|
function clearRemoteFleetTimers(bodyView) {
|
|
12179
12249
|
if (!bodyView) return;
|
|
@@ -12185,6 +12255,10 @@
|
|
|
12185
12255
|
clearInterval(bodyView._remoteFleetMonitorRefreshTimer);
|
|
12186
12256
|
bodyView._remoteFleetMonitorRefreshTimer = null;
|
|
12187
12257
|
}
|
|
12258
|
+
if (bodyView._remoteFleetTaskFollowTimer) {
|
|
12259
|
+
clearTimeout(bodyView._remoteFleetTaskFollowTimer);
|
|
12260
|
+
bodyView._remoteFleetTaskFollowTimer = null;
|
|
12261
|
+
}
|
|
12188
12262
|
}
|
|
12189
12263
|
|
|
12190
12264
|
function getRemoteFleetDeviceField(device, camelKey, pascalKey, fallback = '') {
|
|
@@ -12900,6 +12974,13 @@
|
|
|
12900
12974
|
const focusState = bodyView.dataset.remoteFleetFocusDeviceId || '';
|
|
12901
12975
|
const latestTaskBatch = parseRemoteFleetLatestTaskBatch(nodeModel);
|
|
12902
12976
|
const recentTaskBatches = parseRemoteFleetRecentTaskBatches(nodeModel);
|
|
12977
|
+
const latestTaskBatchKey = getRemoteFleetTaskBatchKey(latestTaskBatch);
|
|
12978
|
+
const latestTaskBatchActive = isRemoteFleetTaskBatchActive(latestTaskBatch);
|
|
12979
|
+
if (latestTaskBatchKey && bodyView.dataset.remoteFleetTaskFollowKey !== latestTaskBatchKey) {
|
|
12980
|
+
bodyView.dataset.remoteFleetTaskFollowKey = latestTaskBatchKey;
|
|
12981
|
+
bodyView.dataset.remoteFleetTaskFollowTicks = '0';
|
|
12982
|
+
}
|
|
12983
|
+
bodyView.dataset.remoteFleetTaskFollowActive = latestTaskBatchActive ? 'true' : 'false';
|
|
12903
12984
|
const sortedDevices = [...parseRemoteFleetDevices(nodeModel)]
|
|
12904
12985
|
.sort((left, right) => compareRemoteFleetDevices(left, right, sortState));
|
|
12905
12986
|
const devices = groupState === 'none'
|
|
@@ -14035,6 +14116,84 @@
|
|
|
14035
14116
|
bodyView._remoteFleetRefreshInFlight = false;
|
|
14036
14117
|
}
|
|
14037
14118
|
};
|
|
14119
|
+
const prepareRemoteFleetTaskFollow = batch => {
|
|
14120
|
+
if (!isRemoteFleetTaskBatchActive(batch)) {
|
|
14121
|
+
bodyView.dataset.remoteFleetTaskFollowActive = 'false';
|
|
14122
|
+
return false;
|
|
14123
|
+
}
|
|
14124
|
+
|
|
14125
|
+
const batchKey = getRemoteFleetTaskBatchKey(batch);
|
|
14126
|
+
if (batchKey && bodyView.dataset.remoteFleetTaskFollowKey !== batchKey) {
|
|
14127
|
+
bodyView.dataset.remoteFleetTaskFollowKey = batchKey;
|
|
14128
|
+
bodyView.dataset.remoteFleetTaskFollowTicks = '0';
|
|
14129
|
+
}
|
|
14130
|
+
|
|
14131
|
+
bodyView.dataset.remoteFleetTaskFollowActive = 'true';
|
|
14132
|
+
return true;
|
|
14133
|
+
};
|
|
14134
|
+
const scheduleRemoteFleetTaskFollow = (delayMs = REMOTE_FLEET_TASK_FOLLOW_INITIAL_MS) => {
|
|
14135
|
+
if (bodyView._remoteFleetTaskFollowTimer) {
|
|
14136
|
+
clearTimeout(bodyView._remoteFleetTaskFollowTimer);
|
|
14137
|
+
bodyView._remoteFleetTaskFollowTimer = null;
|
|
14138
|
+
}
|
|
14139
|
+
|
|
14140
|
+
if (bodyView.dataset.remoteFleetTaskFollowActive !== 'true') {
|
|
14141
|
+
return;
|
|
14142
|
+
}
|
|
14143
|
+
|
|
14144
|
+
const ticks = Number(bodyView.dataset.remoteFleetTaskFollowTicks || '0');
|
|
14145
|
+
if (Number.isFinite(ticks) && ticks >= REMOTE_FLEET_TASK_FOLLOW_MAX_TICKS) {
|
|
14146
|
+
bodyView.dataset.remoteFleetTaskFollowActive = 'false';
|
|
14147
|
+
window.RuntimeTrace?.emit?.('remote.task.followStopped', {
|
|
14148
|
+
nodeId,
|
|
14149
|
+
batchKey: bodyView.dataset.remoteFleetTaskFollowKey || '',
|
|
14150
|
+
reason: 'max-ticks'
|
|
14151
|
+
});
|
|
14152
|
+
return;
|
|
14153
|
+
}
|
|
14154
|
+
|
|
14155
|
+
const timer = setTimeout(async () => {
|
|
14156
|
+
bodyView._remoteFleetTaskFollowTimer = null;
|
|
14157
|
+
if (!document.body.contains(bodyView)) {
|
|
14158
|
+
clearRemoteFleetTimers(bodyView);
|
|
14159
|
+
return;
|
|
14160
|
+
}
|
|
14161
|
+
|
|
14162
|
+
const currentTicks = Number(bodyView.dataset.remoteFleetTaskFollowTicks || '0');
|
|
14163
|
+
bodyView.dataset.remoteFleetTaskFollowTicks = String((Number.isFinite(currentTicks) ? currentTicks : 0) + 1);
|
|
14164
|
+
window.RuntimeTrace?.emit?.('remote.task.followRefresh', {
|
|
14165
|
+
nodeId,
|
|
14166
|
+
batchKey: bodyView.dataset.remoteFleetTaskFollowKey || '',
|
|
14167
|
+
tick: Number(bodyView.dataset.remoteFleetTaskFollowTicks || '0')
|
|
14168
|
+
});
|
|
14169
|
+
|
|
14170
|
+
try {
|
|
14171
|
+
await refreshRemoteFleetNode();
|
|
14172
|
+
} catch {
|
|
14173
|
+
clearRemoteFleetTimers(bodyView);
|
|
14174
|
+
return;
|
|
14175
|
+
}
|
|
14176
|
+
|
|
14177
|
+
if (!document.body.contains(bodyView)) {
|
|
14178
|
+
clearRemoteFleetTimers(bodyView);
|
|
14179
|
+
return;
|
|
14180
|
+
}
|
|
14181
|
+
|
|
14182
|
+
const nextTicks = Number(bodyView.dataset.remoteFleetTaskFollowTicks || '0');
|
|
14183
|
+
if (bodyView.dataset.remoteFleetTaskFollowActive === 'true'
|
|
14184
|
+
&& (!Number.isFinite(nextTicks) || nextTicks < REMOTE_FLEET_TASK_FOLLOW_MAX_TICKS)) {
|
|
14185
|
+
scheduleRemoteFleetTaskFollow(REMOTE_FLEET_TASK_FOLLOW_REFRESH_MS);
|
|
14186
|
+
}
|
|
14187
|
+
}, Math.max(0, Number(delayMs) || 0));
|
|
14188
|
+
timer?.unref?.();
|
|
14189
|
+
bodyView._remoteFleetTaskFollowTimer = timer;
|
|
14190
|
+
};
|
|
14191
|
+
const startRemoteFleetTaskFollowFromResult = result => {
|
|
14192
|
+
const batch = getRemoteFleetTaskBatchFromResult(result) || latestTaskBatch;
|
|
14193
|
+
if (prepareRemoteFleetTaskFollow(batch)) {
|
|
14194
|
+
scheduleRemoteFleetTaskFollow();
|
|
14195
|
+
}
|
|
14196
|
+
};
|
|
14038
14197
|
const getVisibleEligibleDeviceIds = () => {
|
|
14039
14198
|
const wantsAi = useAiAssist();
|
|
14040
14199
|
return getDeviceCards()
|
|
@@ -14150,6 +14309,9 @@
|
|
|
14150
14309
|
renderRemoteFleetMonitor(bodyView, nodeModel);
|
|
14151
14310
|
});
|
|
14152
14311
|
applyRemoteFleetFilters();
|
|
14312
|
+
if (latestTaskBatchActive) {
|
|
14313
|
+
scheduleRemoteFleetTaskFollow();
|
|
14314
|
+
}
|
|
14153
14315
|
|
|
14154
14316
|
sendVisibleButton.addEventListener('click', async event => {
|
|
14155
14317
|
event.preventDefault();
|
|
@@ -14175,6 +14337,7 @@
|
|
|
14175
14337
|
const mode = useAiAssist() ? 'visible AI task' : 'visible remote task';
|
|
14176
14338
|
const feedback = formatRemoteFleetDispatchFeedback(result, targetIds.length, mode);
|
|
14177
14339
|
setTaskFeedback(feedback.text, feedback.tone);
|
|
14340
|
+
startRemoteFleetTaskFollowFromResult(result);
|
|
14178
14341
|
} finally {
|
|
14179
14342
|
applyRemoteFleetFilters();
|
|
14180
14343
|
}
|
|
@@ -14198,6 +14361,7 @@
|
|
|
14198
14361
|
const mode = useAiAssist() ? 'AI task' : 'remote task';
|
|
14199
14362
|
const feedback = formatRemoteFleetDispatchFeedback(result, result?.total || 0, mode);
|
|
14200
14363
|
setTaskFeedback(feedback.text, feedback.tone);
|
|
14364
|
+
startRemoteFleetTaskFollowFromResult(result);
|
|
14201
14365
|
} finally {
|
|
14202
14366
|
applyRemoteFleetFilters();
|
|
14203
14367
|
}
|
|
@@ -14321,6 +14485,7 @@
|
|
|
14321
14485
|
await syncRemoteFleetNodeStateFromResult(result);
|
|
14322
14486
|
if (result?.success) {
|
|
14323
14487
|
setTaskFeedback(useAiAssist() ? 'Queued AI task.' : 'Queued remote task.', 'success');
|
|
14488
|
+
startRemoteFleetTaskFollowFromResult(result);
|
|
14324
14489
|
} else {
|
|
14325
14490
|
setTaskFeedback(result?.error || 'Task dispatch failed.', 'error');
|
|
14326
14491
|
}
|
package/wwwroot/index.html
CHANGED
|
@@ -558,7 +558,7 @@
|
|
|
558
558
|
}
|
|
559
559
|
|
|
560
560
|
const base = '_content/MindExecution.Shared/js/';
|
|
561
|
-
const scriptVersion = '20260612-remote-task-
|
|
561
|
+
const scriptVersion = '20260612-remote-task-follow-v478';
|
|
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": "
|
|
2
|
+
"version": "dr5Y7e+X",
|
|
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-
|
|
89
|
+
"hash": "sha256-r/rKyK7pwOufIiynKaiu7pt8CgoBdLd7u+FmbZHVbko=",
|
|
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-
|
|
837
|
+
"hash": "sha256-0dZRFimqvytlX0CmLM1UGW//sqP9ru7W0usTp/5Z570=",
|
|
838
838
|
"url": "index.html"
|
|
839
839
|
},
|
|
840
840
|
{
|