@dynamicu/chromedebug-mcp 2.7.2 → 2.7.4

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/src/database.js CHANGED
@@ -292,6 +292,8 @@ class ChromeDebugDatabase {
292
292
  xpath TEXT,
293
293
  x INTEGER,
294
294
  y INTEGER,
295
+ viewport_width INTEGER,
296
+ viewport_height INTEGER,
295
297
  value TEXT,
296
298
  text TEXT,
297
299
  key TEXT,
@@ -303,6 +305,24 @@ class ChromeDebugDatabase {
303
305
  )
304
306
  `);
305
307
 
308
+ // Add viewport columns to screen_interactions if they don't exist (migration for existing DBs)
309
+ try {
310
+ this.db.exec(`ALTER TABLE screen_interactions ADD COLUMN viewport_width INTEGER`);
311
+ } catch (e) {
312
+ // Column already exists, ignore
313
+ }
314
+ try {
315
+ this.db.exec(`ALTER TABLE screen_interactions ADD COLUMN viewport_height INTEGER`);
316
+ } catch (e) {
317
+ // Column already exists, ignore
318
+ }
319
+ // Add devicePixelRatio column (critical for Retina/HiDPI display scaling)
320
+ try {
321
+ this.db.exec(`ALTER TABLE screen_interactions ADD COLUMN device_pixel_ratio REAL`);
322
+ } catch (e) {
323
+ // Column already exists, ignore
324
+ }
325
+
306
326
  // Workflow recordings table
307
327
  this.db.exec(`
308
328
  CREATE TABLE IF NOT EXISTS workflow_recordings (
@@ -625,7 +645,14 @@ class ChromeDebugDatabase {
625
645
  logger.debug('[Database] Migrating workflow_recordings table to add screenshot_settings column');
626
646
  this.db.exec(`ALTER TABLE workflow_recordings ADD COLUMN screenshot_settings TEXT`);
627
647
  }
628
-
648
+
649
+ // Migration for recording_mode support (workflow vs screenshot)
650
+ const hasRecordingMode = columns.some(col => col.name === 'recording_mode');
651
+ if (!hasRecordingMode) {
652
+ logger.debug('[Database] Migrating workflow_recordings table to add recording_mode column');
653
+ this.db.exec(`ALTER TABLE workflow_recordings ADD COLUMN recording_mode TEXT DEFAULT 'workflow'`);
654
+ }
655
+
629
656
  // Check if workflow_actions table has screenshot_data column
630
657
  const actionColumns = this.db.pragma(`table_info(workflow_actions)`);
631
658
  const hasScreenshotData = actionColumns.some(col => col.name === 'screenshot_data');
@@ -664,23 +691,30 @@ class ChromeDebugDatabase {
664
691
  logger.debug('[Database] Migrating recordings table to add name column');
665
692
  this.db.exec(`ALTER TABLE recordings ADD COLUMN name TEXT`);
666
693
  }
694
+
695
+ // Migration for video mode support
696
+ const hasIsVideoMode = recordingsColumns.some(col => col.name === 'is_video_mode');
697
+ if (!hasIsVideoMode) {
698
+ logger.debug('[Database] Migrating recordings table to add is_video_mode column');
699
+ this.db.exec(`ALTER TABLE recordings ADD COLUMN is_video_mode INTEGER DEFAULT 0`);
700
+ }
667
701
  }
668
702
 
669
703
  // Store a recording session
670
- storeRecording(sessionId, type = 'frame_capture', recordingType = 'continuous', userNote = null, name = null) {
704
+ storeRecording(sessionId, type = 'frame_capture', recordingType = 'continuous', userNote = null, name = null, isVideoMode = false) {
671
705
  this.init();
672
706
 
673
707
  const stmt = this.db.prepare(`
674
- INSERT OR REPLACE INTO recordings (id, session_id, type, timestamp, updated_at, recording_type, user_note, name)
675
- VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP, ?, ?, ?)
708
+ INSERT OR REPLACE INTO recordings (id, session_id, type, timestamp, updated_at, recording_type, user_note, name, is_video_mode)
709
+ VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP, ?, ?, ?, ?)
676
710
  `);
677
711
 
678
712
  // Use base ID instead of prefixed ID
679
713
  const recordingId = sessionId;
680
714
  const timestamp = Date.now();
681
715
 
682
- logger.debug(`[Database] Storing recording: ${recordingId} for session: ${sessionId}, type: ${recordingType}, name: ${name || 'none'}`);
683
- const result = stmt.run(recordingId, sessionId, type, timestamp, recordingType, userNote, name);
716
+ logger.debug(`[Database] Storing recording: ${recordingId} for session: ${sessionId}, type: ${recordingType}, name: ${name || 'none'}, isVideoMode: ${isVideoMode}`);
717
+ const result = stmt.run(recordingId, sessionId, type, timestamp, recordingType, userNote, name, isVideoMode ? 1 : 0);
684
718
  logger.debug(`[Database] Recording stored with changes: ${result.changes}, lastInsertRowid: ${result.lastInsertRowid}`);
685
719
 
686
720
  return recordingId;
@@ -740,18 +774,18 @@ class ChromeDebugDatabase {
740
774
  */
741
775
 
742
776
  // Store frame batch
743
- storeFrameBatch(sessionId, frames, name = null) {
777
+ storeFrameBatch(sessionId, frames, name = null, isVideoMode = false) {
744
778
  this.init();
745
779
 
746
780
  // Use base ID instead of prefixed ID
747
781
  const recordingId = sessionId;
748
782
 
749
- logger.debug(`[storeFrameBatch] DEATH SPIRAL FIX: Processing ${frames.length} frames for session ${sessionId}`);
783
+ logger.debug(`[storeFrameBatch] DEATH SPIRAL FIX: Processing ${frames.length} frames for session ${sessionId}, isVideoMode: ${isVideoMode}`);
750
784
 
751
785
  // Ensure recording exists (but don't overwrite existing)
752
786
  const existingRecording = this.getRecording(sessionId);
753
787
  if (!existingRecording) {
754
- this.storeRecording(sessionId, 'frame_capture', 'continuous', null, name);
788
+ this.storeRecording(sessionId, 'frame_capture', 'continuous', null, name, isVideoMode);
755
789
  } else {
756
790
  logger.debug(`[storeFrameBatch] Using existing recording: ${existingRecording.id}`);
757
791
  // Update name if provided and different
@@ -760,6 +794,12 @@ class ChromeDebugDatabase {
760
794
  updateStmt.run(name, sessionId);
761
795
  logger.debug(`[storeFrameBatch] Updated recording name to: ${name}`);
762
796
  }
797
+ // Update isVideoMode if provided and different (ensure video mode is properly tagged)
798
+ if (isVideoMode && existingRecording.is_video_mode !== 1) {
799
+ const updateModeStmt = this.db.prepare(`UPDATE recordings SET is_video_mode = ? WHERE id = ?`);
800
+ updateModeStmt.run(1, sessionId);
801
+ logger.debug(`[storeFrameBatch] Updated recording isVideoMode to: true`);
802
+ }
763
803
  }
764
804
 
765
805
  // Check existing frame count BEFORE insertion
@@ -1351,12 +1391,31 @@ class ChromeDebugDatabase {
1351
1391
 
1352
1392
  // Get all interactions for this recording
1353
1393
  const interactionsStmt = this.db.prepare(`
1354
- SELECT * FROM screen_interactions
1394
+ SELECT * FROM screen_interactions
1355
1395
  WHERE recording_id = ?
1356
1396
  ORDER BY timestamp ASC
1357
1397
  `);
1358
- const interactions = interactionsStmt.all(recording.id);
1359
-
1398
+ const rawInteractions = interactionsStmt.all(recording.id);
1399
+
1400
+ // Transform to camelCase for API consistency
1401
+ const interactions = rawInteractions.map(i => ({
1402
+ id: i.id,
1403
+ index: i.interaction_index,
1404
+ type: i.type,
1405
+ selector: i.selector,
1406
+ xpath: i.xpath,
1407
+ x: i.x,
1408
+ y: i.y,
1409
+ viewportWidth: i.viewport_width,
1410
+ viewportHeight: i.viewport_height,
1411
+ devicePixelRatio: i.device_pixel_ratio, // Critical for Retina/HiDPI display scaling
1412
+ value: i.value,
1413
+ text: i.text,
1414
+ key: i.key,
1415
+ timestamp: i.timestamp,
1416
+ frameIndex: i.frame_index
1417
+ }));
1418
+
1360
1419
  return {
1361
1420
  type: 'frame_capture',
1362
1421
  sessionId: recording.session_id,
@@ -1371,7 +1430,7 @@ class ChromeDebugDatabase {
1371
1430
  this.init();
1372
1431
 
1373
1432
  const stmt = this.db.prepare(`
1374
- SELECT session_id, total_frames, timestamp, name
1433
+ SELECT session_id, total_frames, timestamp, name, is_video_mode
1375
1434
  FROM recordings
1376
1435
  WHERE type = 'frame_capture'
1377
1436
  ORDER BY timestamp DESC
@@ -1382,7 +1441,8 @@ class ChromeDebugDatabase {
1382
1441
  sessionId: r.session_id,
1383
1442
  totalFrames: r.total_frames,
1384
1443
  timestamp: r.timestamp,
1385
- name: r.name
1444
+ name: r.name,
1445
+ isVideoMode: r.is_video_mode === 1
1386
1446
  }));
1387
1447
  }
1388
1448
 
@@ -1433,6 +1493,38 @@ class ChromeDebugDatabase {
1433
1493
  return result.changes > 0;
1434
1494
  }
1435
1495
 
1496
+ // Delete all screen recordings
1497
+ deleteAllScreenRecordings() {
1498
+ this.init();
1499
+
1500
+ const transaction = this.db.transaction(() => {
1501
+ // Delete all deferred logs
1502
+ const deleteDeferredLogsStmt = this.db.prepare(`DELETE FROM deferred_logs`);
1503
+ deleteDeferredLogsStmt.run();
1504
+
1505
+ // Delete all screen interactions
1506
+ const deleteInteractionsStmt = this.db.prepare(`DELETE FROM screen_interactions`);
1507
+ deleteInteractionsStmt.run();
1508
+
1509
+ // Delete all console logs (will cascade from frames deletion, but explicit for safety)
1510
+ const deleteLogsStmt = this.db.prepare(`DELETE FROM console_logs`);
1511
+ deleteLogsStmt.run();
1512
+
1513
+ // Delete all frames
1514
+ const deleteFramesStmt = this.db.prepare(`DELETE FROM frames`);
1515
+ deleteFramesStmt.run();
1516
+
1517
+ // Delete all recordings (screen recordings only, type = 'frame_capture')
1518
+ const deleteRecordingsStmt = this.db.prepare(`DELETE FROM recordings WHERE type = 'frame_capture'`);
1519
+ const result = deleteRecordingsStmt.run();
1520
+
1521
+ logger.debug(`[Database] Cleared screen recordings: ${result.changes} recordings deleted`);
1522
+ return result.changes;
1523
+ });
1524
+
1525
+ return transaction();
1526
+ }
1527
+
1436
1528
  // Delete workflow recording
1437
1529
  deleteWorkflowRecording(recordingId) {
1438
1530
  this.init();
@@ -1469,29 +1561,30 @@ class ChromeDebugDatabase {
1469
1561
  }
1470
1562
 
1471
1563
  // Store workflow recording
1472
- storeWorkflowRecording(sessionId, url, title, includeLogs = false, name = null, screenshotSettings = null) {
1564
+ storeWorkflowRecording(sessionId, url, title, includeLogs = false, name = null, screenshotSettings = null, recordingMode = 'workflow') {
1473
1565
  this.init();
1474
-
1475
- logger.debug('[Database] Storing workflow recording with name:', name, 'sessionId:', sessionId);
1476
-
1566
+
1567
+ logger.debug('[Database] Storing workflow recording with name:', name, 'sessionId:', sessionId, 'recordingMode:', recordingMode);
1568
+
1477
1569
  const stmt = this.db.prepare(`
1478
- INSERT OR REPLACE INTO workflow_recordings (id, session_id, name, url, title, timestamp, include_logs, screenshot_settings, updated_at)
1479
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
1570
+ INSERT OR REPLACE INTO workflow_recordings (id, session_id, name, url, title, timestamp, include_logs, screenshot_settings, recording_mode, updated_at)
1571
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
1480
1572
  `);
1481
-
1573
+
1482
1574
  // sessionId already contains workflow_ prefix from extension, use it directly
1483
1575
  const recordingId = sessionId;
1484
1576
  const timestamp = Date.now();
1485
1577
 
1486
1578
  stmt.run(
1487
1579
  recordingId,
1488
- sessionId,
1580
+ sessionId,
1489
1581
  name,
1490
- url,
1491
- title,
1492
- timestamp,
1582
+ url,
1583
+ title,
1584
+ timestamp,
1493
1585
  includeLogs ? 1 : 0,
1494
- screenshotSettings ? JSON.stringify(screenshotSettings) : null
1586
+ screenshotSettings ? JSON.stringify(screenshotSettings) : null,
1587
+ recordingMode
1495
1588
  );
1496
1589
  return recordingId;
1497
1590
  }
@@ -1795,16 +1888,32 @@ class ChromeDebugDatabase {
1795
1888
  }
1796
1889
 
1797
1890
  // List all workflow recordings
1798
- listWorkflowRecordings() {
1891
+ listWorkflowRecordings(recordingModeFilter = null) {
1799
1892
  this.init();
1800
-
1801
- const stmt = this.db.prepare(`
1802
- SELECT id, session_id, name, url, title, total_actions, timestamp, screenshot_settings
1803
- FROM workflow_recordings
1804
- ORDER BY timestamp DESC
1805
- `);
1806
-
1807
- return stmt.all().map(r => ({
1893
+
1894
+ let query = `
1895
+ SELECT id, session_id, name, url, title, total_actions, timestamp, screenshot_settings, recording_mode
1896
+ FROM workflow_recordings
1897
+ `;
1898
+
1899
+ // Add filter if specified
1900
+ // For 'workflow' filter, also match NULL values (old recordings before recording_mode was added)
1901
+ if (recordingModeFilter && recordingModeFilter !== 'all') {
1902
+ if (recordingModeFilter === 'workflow') {
1903
+ query += ` WHERE (recording_mode = ? OR recording_mode IS NULL)`;
1904
+ } else {
1905
+ query += ` WHERE recording_mode = ?`;
1906
+ }
1907
+ }
1908
+
1909
+ query += ` ORDER BY timestamp DESC`;
1910
+
1911
+ const stmt = this.db.prepare(query);
1912
+ const results = recordingModeFilter && recordingModeFilter !== 'all'
1913
+ ? stmt.all(recordingModeFilter)
1914
+ : stmt.all();
1915
+
1916
+ return results.map(r => ({
1808
1917
  id: r.id,
1809
1918
  session_id: r.session_id,
1810
1919
  name: r.name,
@@ -1812,7 +1921,8 @@ class ChromeDebugDatabase {
1812
1921
  title: r.title,
1813
1922
  total_actions: r.total_actions,
1814
1923
  timestamp: r.timestamp,
1815
- screenshot_settings: r.screenshot_settings ? JSON.parse(r.screenshot_settings) : null
1924
+ screenshot_settings: r.screenshot_settings ? JSON.parse(r.screenshot_settings) : null,
1925
+ recording_mode: r.recording_mode || 'workflow'
1816
1926
  }));
1817
1927
  }
1818
1928
 
@@ -2020,11 +2130,11 @@ class ChromeDebugDatabase {
2020
2130
 
2021
2131
  const stmt = this.db.prepare(`
2022
2132
  INSERT OR REPLACE INTO screen_interactions
2023
- (recording_id, interaction_index, type, selector, xpath, x, y, value, text, key, timestamp, frame_index,
2133
+ (recording_id, interaction_index, type, selector, xpath, x, y, viewport_width, viewport_height, device_pixel_ratio, value, text, key, timestamp, frame_index,
2024
2134
  element_html, component_data, event_handlers, element_state, performance_metrics)
2025
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
2135
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
2026
2136
  `);
2027
-
2137
+
2028
2138
  const transaction = this.db.transaction(() => {
2029
2139
  interactions.forEach((interaction, index) => {
2030
2140
  stmt.run(
@@ -2035,6 +2145,9 @@ class ChromeDebugDatabase {
2035
2145
  interaction.xpath || null,
2036
2146
  interaction.x || null,
2037
2147
  interaction.y || null,
2148
+ interaction.viewportWidth || null,
2149
+ interaction.viewportHeight || null,
2150
+ interaction.devicePixelRatio || null, // Critical for Retina/HiDPI display scaling
2038
2151
  interaction.value || null,
2039
2152
  interaction.text || null,
2040
2153
  interaction.key || null,
@@ -2117,6 +2230,8 @@ class ChromeDebugDatabase {
2117
2230
  xpath,
2118
2231
  x,
2119
2232
  y,
2233
+ viewport_width,
2234
+ viewport_height,
2120
2235
  value,
2121
2236
  text,
2122
2237
  key,
@@ -2149,6 +2264,9 @@ class ChromeDebugDatabase {
2149
2264
  xpath: interaction.xpath,
2150
2265
  x: interaction.x,
2151
2266
  y: interaction.y,
2267
+ viewportWidth: interaction.viewport_width,
2268
+ viewportHeight: interaction.viewport_height,
2269
+ devicePixelRatio: interaction.device_pixel_ratio, // Critical for Retina/HiDPI display scaling
2152
2270
  value: interaction.value,
2153
2271
  text: interaction.text,
2154
2272
  key: interaction.key,
@@ -291,16 +291,16 @@ async function startHttpServer(chromeController = null, targetPort = null) {
291
291
  authorize(PERMISSIONS.WORKFLOW_WRITE),
292
292
  createValidator(workflowRecordingSchema),
293
293
  async (req, res) => {
294
- const { sessionId, url, title, includeLogs, actions, logs, functionTraces, name, screenshotSettings } = req.body;
294
+ const { sessionId, url, title, includeLogs, actions, logs, functionTraces, name, screenshotSettings, recordingMode } = req.body;
295
295
 
296
- logger.info('[HTTP Server] Received workflow recording with name:', name);
296
+ logger.info('[HTTP Server] Received workflow recording with name:', name, 'recordingMode:', recordingMode || 'workflow');
297
297
  logger.info('[HTTP Server] Function traces count:', functionTraces ? functionTraces.length : 0);
298
298
  if (!sessionId || !actions) {
299
299
  return res.status(400).json({ error: 'sessionId and actions are required' });
300
300
  }
301
301
 
302
302
  try {
303
- const result = await activeController.storeWorkflowRecording(sessionId, url, title, includeLogs, actions, logs, name, screenshotSettings, functionTraces);
303
+ const result = await activeController.storeWorkflowRecording(sessionId, url, title, includeLogs, actions, logs, name, screenshotSettings, functionTraces, recordingMode || 'workflow');
304
304
  res.json(result);
305
305
  } catch (error) {
306
306
  logger.error('Error storing workflow recording:', error);
@@ -331,7 +331,9 @@ async function startHttpServer(chromeController = null, targetPort = null) {
331
331
  authorize(PERMISSIONS.WORKFLOW_READ),
332
332
  async (req, res) => {
333
333
  try {
334
- const result = await activeController.listWorkflowRecordings();
334
+ // Support filtering by recording_mode: 'all', 'workflow', 'screenshot'
335
+ const recordingModeFilter = req.query.mode || null;
336
+ const result = await activeController.listWorkflowRecordings(recordingModeFilter);
335
337
  res.json(result);
336
338
  } catch (error) {
337
339
  logger.error('Error listing workflow recordings:', error);
@@ -475,7 +477,7 @@ async function startHttpServer(chromeController = null, targetPort = null) {
475
477
  logger.debug(' Raw body:', req.body);
476
478
 
477
479
  // Handle both JSON and FormData
478
- let sessionId, frames, sessionName;
480
+ let sessionId, frames, sessionName, isVideoMode;
479
481
 
480
482
  if (req.headers['content-type']?.includes('application/json')) {
481
483
  logger.debug(' Processing as JSON request');
@@ -494,21 +496,25 @@ async function startHttpServer(chromeController = null, targetPort = null) {
494
496
  sessionId = value.sessionId;
495
497
  frames = value.frames;
496
498
  sessionName = value.sessionName || null;
499
+ isVideoMode = value.isVideoMode || false;
497
500
 
498
- logger.debug(' JSON parsed successfully:', { sessionId, frameCount: frames?.length, sessionName });
501
+ logger.debug(' JSON parsed successfully:', { sessionId, frameCount: frames?.length, sessionName, isVideoMode });
499
502
  } else {
500
503
  logger.debug(' Processing as FormData request');
501
-
504
+
502
505
  // FormData request - manual validation
503
506
  sessionId = req.body.sessionId;
504
507
  sessionName = req.body.sessionName || null;
508
+ // Parse isVideoMode from FormData (can be string 'true'/'false' or boolean)
509
+ isVideoMode = req.body.isVideoMode === 'true' || req.body.isVideoMode === true;
505
510
 
506
511
  logger.debug(' SessionId from FormData:', sessionId);
507
512
  logger.debug(' SessionName from FormData:', sessionName);
513
+ logger.debug(' IsVideoMode from FormData:', isVideoMode);
508
514
  logger.debug(' Frames field type:', typeof req.body.frames);
509
515
  logger.debug(' Frames field length:', req.body.frames?.length);
510
516
  logger.debug(' Frames field preview:', req.body.frames?.substring(0, 200));
511
-
517
+
512
518
  try {
513
519
  if (!req.body.frames) {
514
520
  logger.debug(' ❌ No frames field in FormData');
@@ -520,10 +526,10 @@ async function startHttpServer(chromeController = null, targetPort = null) {
520
526
  logger.debug(' Parsed frames count:', frames?.length);
521
527
  logger.debug(' First frame structure:', frames?.[0] ? Object.keys(frames[0]) : 'no frames');
522
528
  }
523
-
529
+
524
530
  logger.debug(' 🔄 Validating parsed FormData...');
525
531
  // Validate the parsed data
526
- const { error, value } = frameBatchSchema.validate({ sessionId, frames, sessionName });
532
+ const { error, value } = frameBatchSchema.validate({ sessionId, frames, sessionName, isVideoMode });
527
533
  if (error) {
528
534
  logger.debug(' ❌ FormData validation failed:', error.details);
529
535
  return res.status(400).json({
@@ -537,6 +543,7 @@ async function startHttpServer(chromeController = null, targetPort = null) {
537
543
  sessionId = value.sessionId;
538
544
  frames = value.frames;
539
545
  sessionName = value.sessionName;
546
+ isVideoMode = value.isVideoMode || false;
540
547
 
541
548
  logger.debug(' ✅ FormData validation successful');
542
549
  } catch (parseError) {
@@ -566,8 +573,9 @@ async function startHttpServer(chromeController = null, targetPort = null) {
566
573
  }
567
574
 
568
575
  logger.debug(' Session name:', sessionName || 'none');
576
+ logger.debug(' Is video mode:', isVideoMode);
569
577
 
570
- const result = await activeController.storeFrameBatch(sessionId, frames, sessionName);
578
+ const result = await activeController.storeFrameBatch(sessionId, frames, sessionName, isVideoMode);
571
579
  logger.debug(' ✅ Frame batch storage result:', result);
572
580
 
573
581
  logger.debug(' Result recording ID:', result?.id);
@@ -1090,6 +1098,42 @@ async function startHttpServer(chromeController = null, targetPort = null) {
1090
1098
  }
1091
1099
  });
1092
1100
 
1101
+ // Delete all screen recordings
1102
+ app.delete('/chromedebug/recordings/all',
1103
+ authenticate,
1104
+ authorize(PERMISSIONS.WORKFLOW_DELETE),
1105
+ async (req, res) => {
1106
+ try {
1107
+ const deletedCount = activeController.database.deleteAllScreenRecordings();
1108
+ res.json({
1109
+ success: true,
1110
+ deletedCount,
1111
+ message: `Deleted ${deletedCount} screen recording(s)`
1112
+ });
1113
+ } catch (error) {
1114
+ logger.error('Error deleting all screen recordings:', error);
1115
+ res.status(500).json({ error: 'Failed to delete all screen recordings', details: error.message });
1116
+ }
1117
+ });
1118
+
1119
+ // Delete all workflow recordings
1120
+ app.delete('/chromedebug/workflow-recordings/all',
1121
+ authenticate,
1122
+ authorize(PERMISSIONS.WORKFLOW_DELETE),
1123
+ async (req, res) => {
1124
+ try {
1125
+ const deletedCount = activeController.database.clearWorkflowRecordings();
1126
+ res.json({
1127
+ success: true,
1128
+ deletedCount,
1129
+ message: `Deleted ${deletedCount} workflow recording(s)`
1130
+ });
1131
+ } catch (error) {
1132
+ logger.error('Error deleting all workflow recordings:', error);
1133
+ res.status(500).json({ error: 'Failed to delete all workflow recordings', details: error.message });
1134
+ }
1135
+ });
1136
+
1093
1137
  // Save screen interactions for a recording
1094
1138
  app.post('/chromedebug/screen-interactions/:sessionId',
1095
1139
  authenticate,
@@ -81,13 +81,15 @@ export const workflowRecordingSchema = Joi.object({
81
81
  format: Joi.string().valid('png', 'jpeg').optional().allow(null),
82
82
  enabled: Joi.boolean().optional().allow(null),
83
83
  maxWidth: Joi.number().integer().optional().allow(null)
84
- }).optional().allow(null)
84
+ }).optional().allow(null),
85
+ recordingMode: Joi.string().valid('workflow', 'screenshot').optional().default('workflow')
85
86
  });
86
87
 
87
88
  // Frame batch schema
88
89
  export const frameBatchSchema = Joi.object({
89
90
  sessionId: patterns.sessionId.required(),
90
91
  sessionName: Joi.string().max(200).optional().allow(null), // Optional session name for recordings
92
+ isVideoMode: Joi.boolean().optional().default(false), // Whether this is a video mode recording
91
93
  frames: Joi.array().items(
92
94
  Joi.object({
93
95
  timestamp: Joi.number().required(),
@@ -96,6 +98,8 @@ export const frameBatchSchema = Joi.object({
96
98
  consoleLog: Joi.string().optional(),
97
99
  frameIndex: Joi.number().integer().min(0).optional(),
98
100
  index: Joi.number().integer().min(0).optional(), // Chrome extension sends this
101
+ isSynthetic: Joi.boolean().optional(), // Synthetic frames for early log capture
102
+ isManual: Joi.boolean().optional(), // Manual snapshot frames
99
103
  logs: Joi.array().items(
100
104
  Joi.object({
101
105
  level: Joi.string().valid(...VALID_LOG_LEVELS).required(),
@@ -110,6 +114,8 @@ export const frameBatchSchema = Joi.object({
110
114
  timestamp: Joi.number().required(),
111
115
  x: Joi.number().when('type', { is: 'click', then: Joi.required(), otherwise: Joi.optional() }),
112
116
  y: Joi.number().when('type', { is: 'click', then: Joi.required(), otherwise: Joi.optional() }),
117
+ viewportWidth: Joi.number().optional(), // For coordinate scaling during playback
118
+ viewportHeight: Joi.number().optional(), // For coordinate scaling during playback
113
119
  target: Joi.string().optional(),
114
120
  targetId: Joi.string().optional(),
115
121
  scrollX: Joi.number().when('type', { is: 'scroll', then: Joi.required(), otherwise: Joi.optional() }),
@@ -178,6 +184,10 @@ export const screenInteractionsSchema = Joi.object({
178
184
  timestamp: Joi.number().required(),
179
185
  x: Joi.number().optional(),
180
186
  y: Joi.number().optional(),
187
+ pageX: Joi.number().optional(), // Document-relative coordinates
188
+ pageY: Joi.number().optional(), // Document-relative coordinates
189
+ viewportWidth: Joi.number().optional(), // For coordinate scaling during playback
190
+ viewportHeight: Joi.number().optional(), // For coordinate scaling during playback
181
191
  key: Joi.string().optional(),
182
192
  target: Joi.string().optional(),
183
193
  // Basic element fields that were previously blocked