parse-dashboard 8.2.0-alpha.8 → 8.2.0
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/Parse-Dashboard/browser-control/BrowserControlAPI.js +468 -0
- package/Parse-Dashboard/browser-control/BrowserEventStream.js +294 -0
- package/Parse-Dashboard/browser-control/BrowserSessionManager.js +304 -0
- package/Parse-Dashboard/browser-control/README.md +852 -0
- package/Parse-Dashboard/browser-control/ServerOrchestrator.js +334 -0
- package/Parse-Dashboard/browser-control/index.js +27 -0
- package/Parse-Dashboard/browser-control/setup.js +405 -0
- package/Parse-Dashboard/public/bundles/120.bundle.js +1 -1
- package/Parse-Dashboard/public/bundles/183.bundle.js +1 -1
- package/Parse-Dashboard/public/bundles/19.bundle.js +1 -1
- package/Parse-Dashboard/public/bundles/221.bundle.js +1 -1
- package/Parse-Dashboard/public/bundles/372.bundle.js +1 -1
- package/Parse-Dashboard/public/bundles/448.bundle.js +1 -1
- package/Parse-Dashboard/public/bundles/573.bundle.js +1 -1
- package/Parse-Dashboard/public/bundles/578.bundle.js +1 -1
- package/Parse-Dashboard/public/bundles/584.bundle.js +1 -1
- package/Parse-Dashboard/public/bundles/629.bundle.js +1 -1
- package/Parse-Dashboard/public/bundles/643.bundle.js +1 -1
- package/Parse-Dashboard/public/bundles/647.bundle.js +1 -1
- package/Parse-Dashboard/public/bundles/701.bundle.js +1 -1
- package/Parse-Dashboard/public/bundles/729.bundle.js +1 -1
- package/Parse-Dashboard/public/bundles/75.bundle.js +1 -1
- package/Parse-Dashboard/public/bundles/753.bundle.js +1 -1
- package/Parse-Dashboard/public/bundles/817.bundle.js +1 -1
- package/Parse-Dashboard/public/bundles/844.bundle.js +1 -1
- package/Parse-Dashboard/public/bundles/862.bundle.js +1 -1
- package/Parse-Dashboard/public/bundles/866.bundle.js +1 -1
- package/Parse-Dashboard/public/bundles/874.bundle.js +1 -1
- package/Parse-Dashboard/public/bundles/881.bundle.js +1 -1
- package/Parse-Dashboard/public/bundles/929.bundle.js +1 -1
- package/Parse-Dashboard/public/bundles/950.bundle.js +1 -1
- package/Parse-Dashboard/public/bundles/97.bundle.js +1 -1
- package/Parse-Dashboard/public/bundles/976.bundle.js +1 -1
- package/Parse-Dashboard/public/bundles/dashboard.bundle.js +1 -1
- package/Parse-Dashboard/public/bundles/dashboard.bundle.js.LICENSE.txt +23 -0
- package/Parse-Dashboard/public/bundles/login.bundle.js +1 -1
- package/Parse-Dashboard/server.js +33 -2
- package/README.md +43 -0
- package/package.json +18 -9
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const BrowserSessionManager = require('./BrowserSessionManager');
|
|
3
|
+
const ServerOrchestrator = require('./ServerOrchestrator');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Create Browser Control API
|
|
7
|
+
*
|
|
8
|
+
* Provides HTTP endpoints for AI agents to control browser sessions.
|
|
9
|
+
* Exposes session management, navigation, interaction, and debugging capabilities.
|
|
10
|
+
*
|
|
11
|
+
* @param {Function} getWebpackState - Function to get current webpack compilation state
|
|
12
|
+
* @returns {express.Router} Express router with browser control endpoints
|
|
13
|
+
*/
|
|
14
|
+
function createBrowserControlAPI(getWebpackState) {
|
|
15
|
+
const router = express.Router();
|
|
16
|
+
const sessionManager = new BrowserSessionManager();
|
|
17
|
+
const orchestrator = new ServerOrchestrator();
|
|
18
|
+
|
|
19
|
+
// Parse JSON bodies
|
|
20
|
+
router.use(express.json());
|
|
21
|
+
|
|
22
|
+
// Expose sessionManager for BrowserEventStream to access
|
|
23
|
+
router.sessionManager = sessionManager;
|
|
24
|
+
router.orchestrator = orchestrator;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* POST /session/start
|
|
28
|
+
* Create a new browser session, optionally starting Parse Server and Dashboard
|
|
29
|
+
*/
|
|
30
|
+
router.post('/session/start', async (req, res) => {
|
|
31
|
+
try {
|
|
32
|
+
const {
|
|
33
|
+
headless = true,
|
|
34
|
+
width = 1280,
|
|
35
|
+
height = 720,
|
|
36
|
+
slowMo = 0,
|
|
37
|
+
startServers = false,
|
|
38
|
+
parseServerOptions = {},
|
|
39
|
+
dashboardOptions = {}
|
|
40
|
+
} = req.body;
|
|
41
|
+
|
|
42
|
+
let parseServerInfo, dashboardInfo;
|
|
43
|
+
|
|
44
|
+
// Start servers if requested
|
|
45
|
+
if (startServers) {
|
|
46
|
+
try {
|
|
47
|
+
parseServerInfo = await orchestrator.startParseServer(parseServerOptions);
|
|
48
|
+
dashboardInfo = await orchestrator.startDashboard(parseServerInfo, dashboardOptions);
|
|
49
|
+
} catch (error) {
|
|
50
|
+
return res.status(500).json({
|
|
51
|
+
error: `Failed to start servers: ${error.message}`
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Create browser session
|
|
57
|
+
const { sessionId } = await sessionManager.createSession({
|
|
58
|
+
headless,
|
|
59
|
+
width,
|
|
60
|
+
height,
|
|
61
|
+
slowMo
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
res.json({
|
|
65
|
+
sessionId,
|
|
66
|
+
dashboardUrl: dashboardInfo?.url,
|
|
67
|
+
parseServerUrl: parseServerInfo?.serverURL,
|
|
68
|
+
servers: startServers ? {
|
|
69
|
+
parseServer: {
|
|
70
|
+
port: parseServerInfo.port,
|
|
71
|
+
appId: parseServerInfo.appId
|
|
72
|
+
},
|
|
73
|
+
dashboard: {
|
|
74
|
+
port: dashboardInfo.port
|
|
75
|
+
}
|
|
76
|
+
} : null
|
|
77
|
+
});
|
|
78
|
+
} catch (error) {
|
|
79
|
+
res.status(500).json({ error: error.message });
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* DELETE /session/:sessionId
|
|
85
|
+
* Clean up and destroy a browser session
|
|
86
|
+
*/
|
|
87
|
+
router.delete('/session/:sessionId', async (req, res) => {
|
|
88
|
+
try {
|
|
89
|
+
const { sessionId } = req.params;
|
|
90
|
+
const cleaned = await sessionManager.cleanup(sessionId);
|
|
91
|
+
|
|
92
|
+
if (!cleaned) {
|
|
93
|
+
return res.status(404).json({ error: `Session ${sessionId} not found` });
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
res.json({ success: true, sessionId });
|
|
97
|
+
} catch (error) {
|
|
98
|
+
res.status(500).json({ error: error.message });
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* GET /session/:sessionId/status
|
|
104
|
+
* Get session status and metadata
|
|
105
|
+
*/
|
|
106
|
+
router.get('/session/:sessionId/status', (req, res) => {
|
|
107
|
+
try {
|
|
108
|
+
const { sessionId } = req.params;
|
|
109
|
+
const session = sessionManager.getSession(sessionId);
|
|
110
|
+
|
|
111
|
+
res.json({
|
|
112
|
+
sessionId,
|
|
113
|
+
active: true,
|
|
114
|
+
pageUrl: session.page.url(),
|
|
115
|
+
createdAt: session.createdAt,
|
|
116
|
+
lastActivity: session.lastActivity,
|
|
117
|
+
uptime: Date.now() - session.createdAt
|
|
118
|
+
});
|
|
119
|
+
} catch (error) {
|
|
120
|
+
res.status(404).json({ error: error.message });
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* GET /sessions
|
|
126
|
+
* Get all active sessions
|
|
127
|
+
*/
|
|
128
|
+
router.get('/sessions', (req, res) => {
|
|
129
|
+
const sessions = sessionManager.getAllSessions();
|
|
130
|
+
res.json({ sessions, count: sessions.length });
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* POST /session/:sessionId/navigate
|
|
135
|
+
* Navigate to a URL
|
|
136
|
+
*/
|
|
137
|
+
router.post('/session/:sessionId/navigate', async (req, res) => {
|
|
138
|
+
try {
|
|
139
|
+
const { sessionId } = req.params;
|
|
140
|
+
const { url, waitUntil = 'networkidle2', timeout = 30000 } = req.body;
|
|
141
|
+
|
|
142
|
+
if (!url) {
|
|
143
|
+
return res.status(400).json({ error: 'url is required' });
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const { page } = sessionManager.getSession(sessionId);
|
|
147
|
+
await page.goto(url, { waitUntil, timeout });
|
|
148
|
+
|
|
149
|
+
res.json({
|
|
150
|
+
success: true,
|
|
151
|
+
currentUrl: page.url()
|
|
152
|
+
});
|
|
153
|
+
} catch (error) {
|
|
154
|
+
res.status(500).json({ error: error.message });
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* POST /session/:sessionId/click
|
|
160
|
+
* Click an element
|
|
161
|
+
*/
|
|
162
|
+
router.post('/session/:sessionId/click', async (req, res) => {
|
|
163
|
+
try {
|
|
164
|
+
const { sessionId } = req.params;
|
|
165
|
+
const { selector, timeout = 5000 } = req.body;
|
|
166
|
+
|
|
167
|
+
if (!selector) {
|
|
168
|
+
return res.status(400).json({ error: 'selector is required' });
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const { page } = sessionManager.getSession(sessionId);
|
|
172
|
+
await page.waitForSelector(selector, { timeout });
|
|
173
|
+
await page.click(selector);
|
|
174
|
+
|
|
175
|
+
res.json({ success: true, selector });
|
|
176
|
+
} catch (error) {
|
|
177
|
+
res.status(500).json({ error: error.message });
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* POST /session/:sessionId/type
|
|
183
|
+
* Type text into an input
|
|
184
|
+
*/
|
|
185
|
+
router.post('/session/:sessionId/type', async (req, res) => {
|
|
186
|
+
try {
|
|
187
|
+
const { sessionId } = req.params;
|
|
188
|
+
const { selector, text, timeout = 5000, delay = 0 } = req.body;
|
|
189
|
+
|
|
190
|
+
if (!selector || text === undefined) {
|
|
191
|
+
return res.status(400).json({ error: 'selector and text are required' });
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const { page } = sessionManager.getSession(sessionId);
|
|
195
|
+
await page.waitForSelector(selector, { timeout });
|
|
196
|
+
|
|
197
|
+
// Clear existing text first
|
|
198
|
+
await page.click(selector, { clickCount: 3 });
|
|
199
|
+
await page.keyboard.press('Backspace');
|
|
200
|
+
|
|
201
|
+
// Type new text
|
|
202
|
+
await page.type(selector, text, { delay });
|
|
203
|
+
|
|
204
|
+
res.json({ success: true, selector, text });
|
|
205
|
+
} catch (error) {
|
|
206
|
+
res.status(500).json({ error: error.message });
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* POST /session/:sessionId/wait
|
|
212
|
+
* Wait for a selector to appear
|
|
213
|
+
*/
|
|
214
|
+
router.post('/session/:sessionId/wait', async (req, res) => {
|
|
215
|
+
try {
|
|
216
|
+
const { sessionId } = req.params;
|
|
217
|
+
const { selector, timeout = 10000, visible = true } = req.body;
|
|
218
|
+
|
|
219
|
+
if (!selector) {
|
|
220
|
+
return res.status(400).json({ error: 'selector is required' });
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const { page } = sessionManager.getSession(sessionId);
|
|
224
|
+
const startTime = Date.now();
|
|
225
|
+
|
|
226
|
+
await page.waitForSelector(selector, { timeout, visible });
|
|
227
|
+
const duration = Date.now() - startTime;
|
|
228
|
+
|
|
229
|
+
res.json({ success: true, selector, found: true, duration });
|
|
230
|
+
} catch (error) {
|
|
231
|
+
res.status(500).json({ error: error.message, found: false });
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* GET /session/:sessionId/screenshot
|
|
237
|
+
* Take a screenshot
|
|
238
|
+
*/
|
|
239
|
+
router.get('/session/:sessionId/screenshot', async (req, res) => {
|
|
240
|
+
try {
|
|
241
|
+
const { sessionId } = req.params;
|
|
242
|
+
const { fullPage = 'true', encoding = 'base64' } = req.query;
|
|
243
|
+
|
|
244
|
+
const { page } = sessionManager.getSession(sessionId);
|
|
245
|
+
|
|
246
|
+
const screenshot = await page.screenshot({
|
|
247
|
+
fullPage: fullPage === 'true',
|
|
248
|
+
encoding
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
if (encoding === 'base64') {
|
|
252
|
+
res.json({ base64: `data:image/png;base64,${screenshot}` });
|
|
253
|
+
} else {
|
|
254
|
+
res.type('image/png').send(screenshot);
|
|
255
|
+
}
|
|
256
|
+
} catch (error) {
|
|
257
|
+
res.status(500).json({ error: error.message });
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* POST /session/:sessionId/evaluate
|
|
263
|
+
* Execute JavaScript in the page context
|
|
264
|
+
*/
|
|
265
|
+
router.post('/session/:sessionId/evaluate', async (req, res) => {
|
|
266
|
+
try {
|
|
267
|
+
const { sessionId } = req.params;
|
|
268
|
+
const { script } = req.body;
|
|
269
|
+
|
|
270
|
+
if (!script) {
|
|
271
|
+
return res.status(400).json({ error: 'script is required' });
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const { page } = sessionManager.getSession(sessionId);
|
|
275
|
+
|
|
276
|
+
// Wrap script in function if it's not already
|
|
277
|
+
const scriptToEval = script.startsWith('function') || script.includes('=>')
|
|
278
|
+
? `(${script})()`
|
|
279
|
+
: script;
|
|
280
|
+
|
|
281
|
+
const result = await page.evaluate(scriptToEval);
|
|
282
|
+
|
|
283
|
+
res.json({ result });
|
|
284
|
+
} catch (error) {
|
|
285
|
+
res.status(500).json({ error: error.message });
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* POST /session/:sessionId/query
|
|
291
|
+
* Query elements on the page
|
|
292
|
+
*/
|
|
293
|
+
router.post('/session/:sessionId/query', async (req, res) => {
|
|
294
|
+
try {
|
|
295
|
+
const { sessionId } = req.params;
|
|
296
|
+
const { selector, multiple = false } = req.body;
|
|
297
|
+
|
|
298
|
+
if (!selector) {
|
|
299
|
+
return res.status(400).json({ error: 'selector is required' });
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const { page } = sessionManager.getSession(sessionId);
|
|
303
|
+
|
|
304
|
+
if (multiple) {
|
|
305
|
+
// Query multiple elements
|
|
306
|
+
const elements = await page.$$eval(selector, els =>
|
|
307
|
+
els.map(el => ({
|
|
308
|
+
text: el.textContent?.trim(),
|
|
309
|
+
visible: el.offsetParent !== null,
|
|
310
|
+
tagName: el.tagName,
|
|
311
|
+
className: el.className
|
|
312
|
+
}))
|
|
313
|
+
);
|
|
314
|
+
res.json({ elements, count: elements.length });
|
|
315
|
+
} else {
|
|
316
|
+
// Query single element
|
|
317
|
+
const element = await page.$eval(selector, el => ({
|
|
318
|
+
text: el.textContent?.trim(),
|
|
319
|
+
visible: el.offsetParent !== null,
|
|
320
|
+
tagName: el.tagName,
|
|
321
|
+
className: el.className
|
|
322
|
+
})).catch(() => null);
|
|
323
|
+
|
|
324
|
+
if (!element) {
|
|
325
|
+
return res.status(404).json({ error: `Element not found: ${selector}` });
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
res.json({ element });
|
|
329
|
+
}
|
|
330
|
+
} catch (error) {
|
|
331
|
+
res.status(500).json({ error: error.message });
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* POST /session/:sessionId/reload
|
|
337
|
+
* Reload the current page
|
|
338
|
+
*/
|
|
339
|
+
router.post('/session/:sessionId/reload', async (req, res) => {
|
|
340
|
+
try {
|
|
341
|
+
const { sessionId } = req.params;
|
|
342
|
+
const { waitUntil = 'networkidle2' } = req.body;
|
|
343
|
+
|
|
344
|
+
const { page } = sessionManager.getSession(sessionId);
|
|
345
|
+
await page.reload({ waitUntil });
|
|
346
|
+
|
|
347
|
+
res.json({ success: true, currentUrl: page.url() });
|
|
348
|
+
} catch (error) {
|
|
349
|
+
res.status(500).json({ error: error.message });
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* GET /servers/status
|
|
355
|
+
* Get status of Parse Server and Dashboard
|
|
356
|
+
*/
|
|
357
|
+
router.get('/servers/status', (req, res) => {
|
|
358
|
+
const status = orchestrator.getStatus();
|
|
359
|
+
res.json(status);
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* GET /ready
|
|
364
|
+
* Check if the dashboard is ready (webpack compiled, assets available)
|
|
365
|
+
* Returns ready: true when webpack is not compiling, false otherwise
|
|
366
|
+
*/
|
|
367
|
+
router.get('/ready', (req, res) => {
|
|
368
|
+
const webpackState = getWebpackState ? getWebpackState() : { isCompiling: false };
|
|
369
|
+
const ready = !webpackState.isCompiling;
|
|
370
|
+
|
|
371
|
+
res.json({
|
|
372
|
+
ready,
|
|
373
|
+
webpack: {
|
|
374
|
+
compiling: webpackState.isCompiling,
|
|
375
|
+
lastCompilationTime: webpackState.lastCompilationTime,
|
|
376
|
+
lastCompilationDuration: webpackState.lastCompilationDuration,
|
|
377
|
+
compileCount: webpackState.compileCount,
|
|
378
|
+
hasErrors: webpackState.errors && webpackState.errors.length > 0,
|
|
379
|
+
}
|
|
380
|
+
});
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* GET /ready/wait
|
|
385
|
+
* Wait for the dashboard to be ready (webpack compiled)
|
|
386
|
+
* Polls until webpack is done compiling or timeout is reached
|
|
387
|
+
*/
|
|
388
|
+
router.get('/ready/wait', async (req, res) => {
|
|
389
|
+
const timeout = parseInt(req.query.timeout, 10) || 30000;
|
|
390
|
+
const pollInterval = 100;
|
|
391
|
+
const startTime = Date.now();
|
|
392
|
+
|
|
393
|
+
const checkReady = () => {
|
|
394
|
+
const webpackState = getWebpackState ? getWebpackState() : { isCompiling: false };
|
|
395
|
+
return !webpackState.isCompiling;
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
// Poll until ready or timeout
|
|
399
|
+
while (!checkReady()) {
|
|
400
|
+
if (Date.now() - startTime > timeout) {
|
|
401
|
+
const webpackState = getWebpackState ? getWebpackState() : {};
|
|
402
|
+
return res.status(408).json({
|
|
403
|
+
ready: false,
|
|
404
|
+
error: 'Timeout waiting for webpack to finish compiling',
|
|
405
|
+
waited: Date.now() - startTime,
|
|
406
|
+
webpack: {
|
|
407
|
+
compiling: webpackState.isCompiling,
|
|
408
|
+
compileCount: webpackState.compileCount,
|
|
409
|
+
}
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
await new Promise(resolve => setTimeout(resolve, pollInterval));
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const webpackState = getWebpackState ? getWebpackState() : {};
|
|
416
|
+
res.json({
|
|
417
|
+
ready: true,
|
|
418
|
+
waited: Date.now() - startTime,
|
|
419
|
+
webpack: {
|
|
420
|
+
compiling: false,
|
|
421
|
+
lastCompilationTime: webpackState.lastCompilationTime,
|
|
422
|
+
lastCompilationDuration: webpackState.lastCompilationDuration,
|
|
423
|
+
compileCount: webpackState.compileCount,
|
|
424
|
+
}
|
|
425
|
+
});
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* POST /servers/stop
|
|
430
|
+
* Stop Parse Server and Dashboard
|
|
431
|
+
*/
|
|
432
|
+
router.post('/servers/stop', async (req, res) => {
|
|
433
|
+
try {
|
|
434
|
+
await orchestrator.stopAll();
|
|
435
|
+
res.json({ success: true });
|
|
436
|
+
} catch (error) {
|
|
437
|
+
res.status(500).json({ error: error.message });
|
|
438
|
+
}
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* POST /cleanup
|
|
443
|
+
* Clean up all sessions and servers
|
|
444
|
+
*/
|
|
445
|
+
router.post('/cleanup', async (req, res) => {
|
|
446
|
+
try {
|
|
447
|
+
await sessionManager.cleanupAll();
|
|
448
|
+
await orchestrator.stopAll();
|
|
449
|
+
res.json({ success: true });
|
|
450
|
+
} catch (error) {
|
|
451
|
+
res.status(500).json({ error: error.message });
|
|
452
|
+
}
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
// Cleanup on process exit
|
|
456
|
+
const cleanup = async () => {
|
|
457
|
+
console.log('Browser Control API shutting down...');
|
|
458
|
+
await sessionManager.shutdown();
|
|
459
|
+
await orchestrator.stopAll();
|
|
460
|
+
};
|
|
461
|
+
|
|
462
|
+
process.on('SIGTERM', cleanup);
|
|
463
|
+
process.on('SIGINT', cleanup);
|
|
464
|
+
|
|
465
|
+
return router;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
module.exports = createBrowserControlAPI;
|