infinicode 2.8.38 → 2.8.40
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.
|
@@ -53,6 +53,8 @@ export declare class MeshServer {
|
|
|
53
53
|
start(): Promise<void>;
|
|
54
54
|
/** Proxy a dashboard request to the RoboPark scheduler (product data). */
|
|
55
55
|
private proxyToScheduler;
|
|
56
|
+
/** Proxy the scheduler's full Control Center and its same-origin assets. */
|
|
57
|
+
private proxySchedulerPath;
|
|
56
58
|
/** Protected local runtime controls for the RoboPark operator dashboard. */
|
|
57
59
|
private runtimeDocker;
|
|
58
60
|
/** Push a frame to every connected stream subscriber. */
|
|
@@ -119,8 +119,51 @@ export class MeshServer {
|
|
|
119
119
|
res.end(text);
|
|
120
120
|
})
|
|
121
121
|
.catch(err => {
|
|
122
|
-
res.
|
|
123
|
-
|
|
122
|
+
if (!res.headersSent) {
|
|
123
|
+
res.writeHead(502, { 'content-type': 'application/json' });
|
|
124
|
+
res.end(JSON.stringify({ ok: false, error: `scheduler unreachable: ${err instanceof Error ? err.message : String(err)}` }));
|
|
125
|
+
}
|
|
126
|
+
else if (!res.writableEnded) {
|
|
127
|
+
res.end();
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
/** Proxy the scheduler's full Control Center and its same-origin assets. */
|
|
132
|
+
proxySchedulerPath(req, res, path) {
|
|
133
|
+
const base = this.opts.schedulerUrl.replace(/\/+$/, '');
|
|
134
|
+
const target = `${base}${path}${req.url?.includes('?') ? req.url.slice(req.url.indexOf('?')) : ''}`;
|
|
135
|
+
this.readBody(req)
|
|
136
|
+
.then(async (body) => {
|
|
137
|
+
const headers = {};
|
|
138
|
+
if (req.headers['content-type'])
|
|
139
|
+
headers['content-type'] = req.headers['content-type'];
|
|
140
|
+
if (this.opts.schedulerToken)
|
|
141
|
+
headers.authorization = `Bearer ${this.opts.schedulerToken}`;
|
|
142
|
+
const upstream = await fetch(target, {
|
|
143
|
+
method: req.method,
|
|
144
|
+
headers,
|
|
145
|
+
body: req.method === 'GET' || req.method === 'HEAD' ? undefined : body,
|
|
146
|
+
signal: AbortSignal.timeout(15_000),
|
|
147
|
+
});
|
|
148
|
+
const responseHeaders = {
|
|
149
|
+
'content-type': upstream.headers.get('content-type') ?? 'application/octet-stream',
|
|
150
|
+
'cache-control': upstream.headers.get('cache-control') ?? 'no-store',
|
|
151
|
+
};
|
|
152
|
+
const presented = this.presentedToken(req);
|
|
153
|
+
if (this.opts.token && presented) {
|
|
154
|
+
responseHeaders['set-cookie'] = `robopark_auth=${encodeURIComponent(presented)}; Path=/; SameSite=Lax`;
|
|
155
|
+
}
|
|
156
|
+
res.writeHead(upstream.status, responseHeaders);
|
|
157
|
+
res.end(Buffer.from(await upstream.arrayBuffer()));
|
|
158
|
+
})
|
|
159
|
+
.catch(err => {
|
|
160
|
+
if (!res.headersSent) {
|
|
161
|
+
res.writeHead(502, { 'content-type': 'text/plain; charset=utf-8' });
|
|
162
|
+
res.end(`scheduler unreachable: ${err instanceof Error ? err.message : String(err)}`);
|
|
163
|
+
}
|
|
164
|
+
else if (!res.writableEnded) {
|
|
165
|
+
res.end();
|
|
166
|
+
}
|
|
124
167
|
});
|
|
125
168
|
}
|
|
126
169
|
/** Protected local runtime controls for the RoboPark operator dashboard. */
|
|
@@ -195,6 +238,10 @@ export class MeshServer {
|
|
|
195
238
|
if (t)
|
|
196
239
|
return t;
|
|
197
240
|
}
|
|
241
|
+
const cookie = req.headers.cookie ?? '';
|
|
242
|
+
const match = cookie.match(/(?:^|;\s*)robopark_auth=([^;]+)/);
|
|
243
|
+
if (match)
|
|
244
|
+
return decodeURIComponent(match[1]);
|
|
198
245
|
return undefined;
|
|
199
246
|
}
|
|
200
247
|
authOk(req) {
|
|
@@ -215,17 +262,41 @@ export class MeshServer {
|
|
|
215
262
|
return;
|
|
216
263
|
}
|
|
217
264
|
const remote = req.socket.remoteAddress ?? 'unknown';
|
|
218
|
-
//
|
|
219
|
-
//
|
|
265
|
+
// The scheduler owns the complete RoboPark Control Center. The mesh page is
|
|
266
|
+
// retained at /mesh as a diagnostics view, not as the primary operator UI.
|
|
220
267
|
if (req.method === 'GET' && (path === '/' || path === '/ui' || path === '/dashboard')) {
|
|
268
|
+
if (this.opts.schedulerUrl) {
|
|
269
|
+
this.proxySchedulerPath(req, res, '/');
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
221
272
|
if (!this.opts.dashboardHtml) {
|
|
222
273
|
res.writeHead(404).end('dashboard not enabled (start with --dashboard)');
|
|
223
274
|
return;
|
|
224
275
|
}
|
|
276
|
+
res.writeHead(200, {
|
|
277
|
+
'content-type': 'text/html; charset=utf-8',
|
|
278
|
+
'cache-control': 'no-store',
|
|
279
|
+
...(this.opts.token && this.presentedToken(req)
|
|
280
|
+
? { 'set-cookie': `robopark_auth=${encodeURIComponent(this.presentedToken(req))}; Path=/; SameSite=Lax` }
|
|
281
|
+
: {}),
|
|
282
|
+
});
|
|
283
|
+
res.end(this.opts.dashboardHtml);
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
if (req.method === 'GET' && path === '/mesh') {
|
|
287
|
+
if (!this.opts.dashboardHtml) {
|
|
288
|
+
res.writeHead(404).end('mesh dashboard not enabled (start with --dashboard)');
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
225
291
|
res.writeHead(200, { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-store' });
|
|
226
292
|
res.end(this.opts.dashboardHtml);
|
|
227
293
|
return;
|
|
228
294
|
}
|
|
295
|
+
// Full scheduler dashboard requests are same-origin after the root proxy.
|
|
296
|
+
if (this.opts.schedulerUrl && (path.startsWith('/api/') || path.startsWith('/static/') || path.startsWith('/vendor/'))) {
|
|
297
|
+
this.proxySchedulerPath(req, res, path);
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
229
300
|
// Static assets bundled with the dashboard (robot concept avatars, etc.).
|
|
230
301
|
if (req.method === 'GET' && path.startsWith('/static/')) {
|
|
231
302
|
const asset = loadStaticAsset(path.slice(8));
|
package/dist/robopark/serve.d.ts
CHANGED
package/dist/robopark/serve.js
CHANGED
|
@@ -181,14 +181,19 @@ export async function roboparkServe(opts) {
|
|
|
181
181
|
catch { /* ignore */ }
|
|
182
182
|
});
|
|
183
183
|
proc.on('exit', () => process.exit(0));
|
|
184
|
+
if (opts.open !== false) {
|
|
185
|
+
await openDashboard(host, port);
|
|
186
|
+
}
|
|
184
187
|
}
|
|
185
188
|
else {
|
|
186
189
|
// Detached mode: let the user walk away. The process keeps running.
|
|
187
190
|
proc.unref();
|
|
191
|
+
if (opts.open !== false) {
|
|
192
|
+
await openDashboard(host, port);
|
|
193
|
+
}
|
|
188
194
|
printUrl(port);
|
|
189
195
|
console.log(chalk.dim(' running in background. Use Task Manager / systemctl / launchctl to stop.'));
|
|
190
|
-
|
|
191
|
-
setTimeout(() => process.exit(0), 1200);
|
|
196
|
+
setTimeout(() => process.exit(0), 100);
|
|
192
197
|
}
|
|
193
198
|
}
|
|
194
199
|
function printUrl(port) {
|
|
@@ -205,3 +210,43 @@ export async function schedulerHealthy(url, timeoutMs = 3000) {
|
|
|
205
210
|
return false;
|
|
206
211
|
}
|
|
207
212
|
}
|
|
213
|
+
function dashboardUrl(host, port) {
|
|
214
|
+
// 0.0.0.0/:: are bind addresses, not useful browser destinations.
|
|
215
|
+
const browserHost = host === '0.0.0.0' || host === '::' ? 'localhost' : host;
|
|
216
|
+
const formattedHost = browserHost.includes(':') && !browserHost.startsWith('[')
|
|
217
|
+
? `[${browserHost}]`
|
|
218
|
+
: browserHost;
|
|
219
|
+
return `http://${formattedHost}:${port}/`;
|
|
220
|
+
}
|
|
221
|
+
function openBrowser(url) {
|
|
222
|
+
// Use detached platform-native launchers so robopark serve remains usable
|
|
223
|
+
// from terminals and does not inherit browser stdio or lifecycle.
|
|
224
|
+
const command = process.platform === 'win32'
|
|
225
|
+
? 'cmd.exe'
|
|
226
|
+
: process.platform === 'darwin'
|
|
227
|
+
? 'open'
|
|
228
|
+
: 'xdg-open';
|
|
229
|
+
const args = process.platform === 'win32' ? ['/c', 'start', '', url] : [url];
|
|
230
|
+
try {
|
|
231
|
+
spawn(command, args, { detached: true, stdio: 'ignore', windowsHide: true }).unref();
|
|
232
|
+
}
|
|
233
|
+
catch (err) {
|
|
234
|
+
console.log(chalk.yellow(` ⚠ could not open browser: ${err instanceof Error ? err.message : String(err)}`));
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
async function openDashboard(host, port) {
|
|
238
|
+
const url = dashboardUrl(host, port);
|
|
239
|
+
const deadline = Date.now() + 10_000;
|
|
240
|
+
let healthy = false;
|
|
241
|
+
while (Date.now() < deadline) {
|
|
242
|
+
if (await schedulerHealthy(url, 500)) {
|
|
243
|
+
healthy = true;
|
|
244
|
+
break;
|
|
245
|
+
}
|
|
246
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
247
|
+
}
|
|
248
|
+
if (!healthy) {
|
|
249
|
+
console.log(chalk.yellow(' ⚠ scheduler did not answer within 10s; opening the dashboard anyway.'));
|
|
250
|
+
}
|
|
251
|
+
openBrowser(url);
|
|
252
|
+
}
|
package/dist/robopark-cli.js
CHANGED
|
@@ -94,6 +94,7 @@ program
|
|
|
94
94
|
.option('--gateway <url>', 'InfiniBot gateway ws:// URL')
|
|
95
95
|
.option('--gateway-token <token>', 'InfiniBot gateway token')
|
|
96
96
|
.option('--gateway-password <pw>', 'InfiniBot gateway password')
|
|
97
|
+
.option('--no-open', 'do not open the RoboPark Control Center in a browser')
|
|
97
98
|
.option('--foreground', 'stay in foreground; do not detach')
|
|
98
99
|
.action(async (opts) => {
|
|
99
100
|
await roboparkServe(opts);
|
package/package.json
CHANGED