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.
Files changed (39) hide show
  1. package/Parse-Dashboard/browser-control/BrowserControlAPI.js +468 -0
  2. package/Parse-Dashboard/browser-control/BrowserEventStream.js +294 -0
  3. package/Parse-Dashboard/browser-control/BrowserSessionManager.js +304 -0
  4. package/Parse-Dashboard/browser-control/README.md +852 -0
  5. package/Parse-Dashboard/browser-control/ServerOrchestrator.js +334 -0
  6. package/Parse-Dashboard/browser-control/index.js +27 -0
  7. package/Parse-Dashboard/browser-control/setup.js +405 -0
  8. package/Parse-Dashboard/public/bundles/120.bundle.js +1 -1
  9. package/Parse-Dashboard/public/bundles/183.bundle.js +1 -1
  10. package/Parse-Dashboard/public/bundles/19.bundle.js +1 -1
  11. package/Parse-Dashboard/public/bundles/221.bundle.js +1 -1
  12. package/Parse-Dashboard/public/bundles/372.bundle.js +1 -1
  13. package/Parse-Dashboard/public/bundles/448.bundle.js +1 -1
  14. package/Parse-Dashboard/public/bundles/573.bundle.js +1 -1
  15. package/Parse-Dashboard/public/bundles/578.bundle.js +1 -1
  16. package/Parse-Dashboard/public/bundles/584.bundle.js +1 -1
  17. package/Parse-Dashboard/public/bundles/629.bundle.js +1 -1
  18. package/Parse-Dashboard/public/bundles/643.bundle.js +1 -1
  19. package/Parse-Dashboard/public/bundles/647.bundle.js +1 -1
  20. package/Parse-Dashboard/public/bundles/701.bundle.js +1 -1
  21. package/Parse-Dashboard/public/bundles/729.bundle.js +1 -1
  22. package/Parse-Dashboard/public/bundles/75.bundle.js +1 -1
  23. package/Parse-Dashboard/public/bundles/753.bundle.js +1 -1
  24. package/Parse-Dashboard/public/bundles/817.bundle.js +1 -1
  25. package/Parse-Dashboard/public/bundles/844.bundle.js +1 -1
  26. package/Parse-Dashboard/public/bundles/862.bundle.js +1 -1
  27. package/Parse-Dashboard/public/bundles/866.bundle.js +1 -1
  28. package/Parse-Dashboard/public/bundles/874.bundle.js +1 -1
  29. package/Parse-Dashboard/public/bundles/881.bundle.js +1 -1
  30. package/Parse-Dashboard/public/bundles/929.bundle.js +1 -1
  31. package/Parse-Dashboard/public/bundles/950.bundle.js +1 -1
  32. package/Parse-Dashboard/public/bundles/97.bundle.js +1 -1
  33. package/Parse-Dashboard/public/bundles/976.bundle.js +1 -1
  34. package/Parse-Dashboard/public/bundles/dashboard.bundle.js +1 -1
  35. package/Parse-Dashboard/public/bundles/dashboard.bundle.js.LICENSE.txt +23 -0
  36. package/Parse-Dashboard/public/bundles/login.bundle.js +1 -1
  37. package/Parse-Dashboard/server.js +33 -2
  38. package/README.md +43 -0
  39. 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;