@dynamicu/chromedebug-mcp 2.7.1 → 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
@@ -132,9 +132,20 @@ class ChromeDebugDatabase {
132
132
  // Migrate from old location if needed
133
133
  this.migrateIfNeeded();
134
134
 
135
+ // Check if database exists and is corrupted
136
+ if (fs.existsSync(this.dbPath)) {
137
+ const integrityStatus = this.checkDatabaseIntegrity();
138
+ if (!integrityStatus.ok) {
139
+ logger.warn(`[Database] Database corruption detected: ${integrityStatus.error}`);
140
+ this.handleCorruptedDatabase();
141
+ }
142
+ }
143
+
135
144
  // Open database connection
136
145
  this.db = new Database(this.dbPath);
137
146
  this.db.pragma('journal_mode = WAL'); // Enable WAL mode for better concurrent access
147
+ this.db.pragma('synchronous = NORMAL'); // Balance between safety and performance
148
+ this.db.pragma('foreign_keys = ON'); // Enforce foreign key constraints
138
149
 
139
150
  // Create tables
140
151
  this.createTables();
@@ -142,6 +153,64 @@ class ChromeDebugDatabase {
142
153
  logger.debug(`ChromeDebug MCP database initialized at: ${this.dbPath}`);
143
154
  }
144
155
 
156
+ /**
157
+ * Check database integrity before opening
158
+ * @returns {{ ok: boolean, error?: string }}
159
+ */
160
+ checkDatabaseIntegrity() {
161
+ try {
162
+ const tempDb = new Database(this.dbPath, { readonly: true });
163
+ const result = tempDb.pragma('integrity_check');
164
+ tempDb.close();
165
+
166
+ if (result && result[0] && result[0].integrity_check === 'ok') {
167
+ return { ok: true };
168
+ } else {
169
+ const errorMsg = result?.[0]?.integrity_check || 'Unknown integrity error';
170
+ return { ok: false, error: errorMsg };
171
+ }
172
+ } catch (error) {
173
+ return { ok: false, error: error.message };
174
+ }
175
+ }
176
+
177
+ /**
178
+ * Handle corrupted database by backing it up and creating a fresh one
179
+ */
180
+ handleCorruptedDatabase() {
181
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
182
+ const backupPath = `${this.dbPath}.corrupted_${timestamp}`;
183
+
184
+ logger.warn(`[Database] Backing up corrupted database to: ${backupPath}`);
185
+
186
+ try {
187
+ // Move corrupted database and associated files
188
+ if (fs.existsSync(this.dbPath)) {
189
+ fs.renameSync(this.dbPath, backupPath);
190
+ }
191
+ if (fs.existsSync(`${this.dbPath}-shm`)) {
192
+ fs.renameSync(`${this.dbPath}-shm`, `${backupPath}-shm`);
193
+ }
194
+ if (fs.existsSync(`${this.dbPath}-wal`)) {
195
+ fs.renameSync(`${this.dbPath}-wal`, `${backupPath}-wal`);
196
+ }
197
+
198
+ logger.info(`[Database] Corrupted database backed up. A fresh database will be created.`);
199
+ } catch (error) {
200
+ logger.error(`[Database] Failed to backup corrupted database: ${error.message}`);
201
+ // Try to delete instead of rename
202
+ try {
203
+ fs.unlinkSync(this.dbPath);
204
+ if (fs.existsSync(`${this.dbPath}-shm`)) fs.unlinkSync(`${this.dbPath}-shm`);
205
+ if (fs.existsSync(`${this.dbPath}-wal`)) fs.unlinkSync(`${this.dbPath}-wal`);
206
+ logger.info(`[Database] Corrupted database removed. A fresh database will be created.`);
207
+ } catch (deleteError) {
208
+ logger.error(`[Database] Failed to remove corrupted database: ${deleteError.message}`);
209
+ throw new Error(`Cannot recover from database corruption: ${deleteError.message}`);
210
+ }
211
+ }
212
+ }
213
+
145
214
  createTables() {
146
215
  // Recordings table for frame capture sessions
147
216
  this.db.exec(`
@@ -223,6 +292,8 @@ class ChromeDebugDatabase {
223
292
  xpath TEXT,
224
293
  x INTEGER,
225
294
  y INTEGER,
295
+ viewport_width INTEGER,
296
+ viewport_height INTEGER,
226
297
  value TEXT,
227
298
  text TEXT,
228
299
  key TEXT,
@@ -234,6 +305,24 @@ class ChromeDebugDatabase {
234
305
  )
235
306
  `);
236
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
+
237
326
  // Workflow recordings table
238
327
  this.db.exec(`
239
328
  CREATE TABLE IF NOT EXISTS workflow_recordings (
@@ -556,7 +645,14 @@ class ChromeDebugDatabase {
556
645
  logger.debug('[Database] Migrating workflow_recordings table to add screenshot_settings column');
557
646
  this.db.exec(`ALTER TABLE workflow_recordings ADD COLUMN screenshot_settings TEXT`);
558
647
  }
559
-
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
+
560
656
  // Check if workflow_actions table has screenshot_data column
561
657
  const actionColumns = this.db.pragma(`table_info(workflow_actions)`);
562
658
  const hasScreenshotData = actionColumns.some(col => col.name === 'screenshot_data');
@@ -595,23 +691,30 @@ class ChromeDebugDatabase {
595
691
  logger.debug('[Database] Migrating recordings table to add name column');
596
692
  this.db.exec(`ALTER TABLE recordings ADD COLUMN name TEXT`);
597
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
+ }
598
701
  }
599
702
 
600
703
  // Store a recording session
601
- 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) {
602
705
  this.init();
603
706
 
604
707
  const stmt = this.db.prepare(`
605
- INSERT OR REPLACE INTO recordings (id, session_id, type, timestamp, updated_at, recording_type, user_note, name)
606
- 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, ?, ?, ?, ?)
607
710
  `);
608
711
 
609
712
  // Use base ID instead of prefixed ID
610
713
  const recordingId = sessionId;
611
714
  const timestamp = Date.now();
612
715
 
613
- logger.debug(`[Database] Storing recording: ${recordingId} for session: ${sessionId}, type: ${recordingType}, name: ${name || 'none'}`);
614
- 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);
615
718
  logger.debug(`[Database] Recording stored with changes: ${result.changes}, lastInsertRowid: ${result.lastInsertRowid}`);
616
719
 
617
720
  return recordingId;
@@ -671,18 +774,18 @@ class ChromeDebugDatabase {
671
774
  */
672
775
 
673
776
  // Store frame batch
674
- storeFrameBatch(sessionId, frames, name = null) {
777
+ storeFrameBatch(sessionId, frames, name = null, isVideoMode = false) {
675
778
  this.init();
676
779
 
677
780
  // Use base ID instead of prefixed ID
678
781
  const recordingId = sessionId;
679
782
 
680
- 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}`);
681
784
 
682
785
  // Ensure recording exists (but don't overwrite existing)
683
786
  const existingRecording = this.getRecording(sessionId);
684
787
  if (!existingRecording) {
685
- this.storeRecording(sessionId, 'frame_capture', 'continuous', null, name);
788
+ this.storeRecording(sessionId, 'frame_capture', 'continuous', null, name, isVideoMode);
686
789
  } else {
687
790
  logger.debug(`[storeFrameBatch] Using existing recording: ${existingRecording.id}`);
688
791
  // Update name if provided and different
@@ -691,6 +794,12 @@ class ChromeDebugDatabase {
691
794
  updateStmt.run(name, sessionId);
692
795
  logger.debug(`[storeFrameBatch] Updated recording name to: ${name}`);
693
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
+ }
694
803
  }
695
804
 
696
805
  // Check existing frame count BEFORE insertion
@@ -1282,12 +1391,31 @@ class ChromeDebugDatabase {
1282
1391
 
1283
1392
  // Get all interactions for this recording
1284
1393
  const interactionsStmt = this.db.prepare(`
1285
- SELECT * FROM screen_interactions
1394
+ SELECT * FROM screen_interactions
1286
1395
  WHERE recording_id = ?
1287
1396
  ORDER BY timestamp ASC
1288
1397
  `);
1289
- const interactions = interactionsStmt.all(recording.id);
1290
-
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
+
1291
1419
  return {
1292
1420
  type: 'frame_capture',
1293
1421
  sessionId: recording.session_id,
@@ -1302,7 +1430,7 @@ class ChromeDebugDatabase {
1302
1430
  this.init();
1303
1431
 
1304
1432
  const stmt = this.db.prepare(`
1305
- SELECT session_id, total_frames, timestamp, name
1433
+ SELECT session_id, total_frames, timestamp, name, is_video_mode
1306
1434
  FROM recordings
1307
1435
  WHERE type = 'frame_capture'
1308
1436
  ORDER BY timestamp DESC
@@ -1313,7 +1441,8 @@ class ChromeDebugDatabase {
1313
1441
  sessionId: r.session_id,
1314
1442
  totalFrames: r.total_frames,
1315
1443
  timestamp: r.timestamp,
1316
- name: r.name
1444
+ name: r.name,
1445
+ isVideoMode: r.is_video_mode === 1
1317
1446
  }));
1318
1447
  }
1319
1448
 
@@ -1364,6 +1493,38 @@ class ChromeDebugDatabase {
1364
1493
  return result.changes > 0;
1365
1494
  }
1366
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
+
1367
1528
  // Delete workflow recording
1368
1529
  deleteWorkflowRecording(recordingId) {
1369
1530
  this.init();
@@ -1400,29 +1561,30 @@ class ChromeDebugDatabase {
1400
1561
  }
1401
1562
 
1402
1563
  // Store workflow recording
1403
- storeWorkflowRecording(sessionId, url, title, includeLogs = false, name = null, screenshotSettings = null) {
1564
+ storeWorkflowRecording(sessionId, url, title, includeLogs = false, name = null, screenshotSettings = null, recordingMode = 'workflow') {
1404
1565
  this.init();
1405
-
1406
- logger.debug('[Database] Storing workflow recording with name:', name, 'sessionId:', sessionId);
1407
-
1566
+
1567
+ logger.debug('[Database] Storing workflow recording with name:', name, 'sessionId:', sessionId, 'recordingMode:', recordingMode);
1568
+
1408
1569
  const stmt = this.db.prepare(`
1409
- INSERT OR REPLACE INTO workflow_recordings (id, session_id, name, url, title, timestamp, include_logs, screenshot_settings, updated_at)
1410
- 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)
1411
1572
  `);
1412
-
1573
+
1413
1574
  // sessionId already contains workflow_ prefix from extension, use it directly
1414
1575
  const recordingId = sessionId;
1415
1576
  const timestamp = Date.now();
1416
1577
 
1417
1578
  stmt.run(
1418
1579
  recordingId,
1419
- sessionId,
1580
+ sessionId,
1420
1581
  name,
1421
- url,
1422
- title,
1423
- timestamp,
1582
+ url,
1583
+ title,
1584
+ timestamp,
1424
1585
  includeLogs ? 1 : 0,
1425
- screenshotSettings ? JSON.stringify(screenshotSettings) : null
1586
+ screenshotSettings ? JSON.stringify(screenshotSettings) : null,
1587
+ recordingMode
1426
1588
  );
1427
1589
  return recordingId;
1428
1590
  }
@@ -1726,16 +1888,32 @@ class ChromeDebugDatabase {
1726
1888
  }
1727
1889
 
1728
1890
  // List all workflow recordings
1729
- listWorkflowRecordings() {
1891
+ listWorkflowRecordings(recordingModeFilter = null) {
1730
1892
  this.init();
1731
-
1732
- const stmt = this.db.prepare(`
1733
- SELECT id, session_id, name, url, title, total_actions, timestamp, screenshot_settings
1734
- FROM workflow_recordings
1735
- ORDER BY timestamp DESC
1736
- `);
1737
-
1738
- 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 => ({
1739
1917
  id: r.id,
1740
1918
  session_id: r.session_id,
1741
1919
  name: r.name,
@@ -1743,7 +1921,8 @@ class ChromeDebugDatabase {
1743
1921
  title: r.title,
1744
1922
  total_actions: r.total_actions,
1745
1923
  timestamp: r.timestamp,
1746
- 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'
1747
1926
  }));
1748
1927
  }
1749
1928
 
@@ -1951,11 +2130,11 @@ class ChromeDebugDatabase {
1951
2130
 
1952
2131
  const stmt = this.db.prepare(`
1953
2132
  INSERT OR REPLACE INTO screen_interactions
1954
- (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,
1955
2134
  element_html, component_data, event_handlers, element_state, performance_metrics)
1956
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
2135
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1957
2136
  `);
1958
-
2137
+
1959
2138
  const transaction = this.db.transaction(() => {
1960
2139
  interactions.forEach((interaction, index) => {
1961
2140
  stmt.run(
@@ -1966,6 +2145,9 @@ class ChromeDebugDatabase {
1966
2145
  interaction.xpath || null,
1967
2146
  interaction.x || null,
1968
2147
  interaction.y || null,
2148
+ interaction.viewportWidth || null,
2149
+ interaction.viewportHeight || null,
2150
+ interaction.devicePixelRatio || null, // Critical for Retina/HiDPI display scaling
1969
2151
  interaction.value || null,
1970
2152
  interaction.text || null,
1971
2153
  interaction.key || null,
@@ -2048,6 +2230,8 @@ class ChromeDebugDatabase {
2048
2230
  xpath,
2049
2231
  x,
2050
2232
  y,
2233
+ viewport_width,
2234
+ viewport_height,
2051
2235
  value,
2052
2236
  text,
2053
2237
  key,
@@ -2080,6 +2264,9 @@ class ChromeDebugDatabase {
2080
2264
  xpath: interaction.xpath,
2081
2265
  x: interaction.x,
2082
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
2083
2270
  value: interaction.value,
2084
2271
  text: interaction.text,
2085
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,
@@ -1,5 +1,10 @@
1
1
  import Joi from 'joi';
2
2
 
3
+ // Valid console log levels - must match what Chrome extension captures
4
+ // Chrome extension captures: log, error, warn, info, debug, trace, table, dir, group, groupEnd, time, timeEnd, count
5
+ // See: chrome-extension/console-interception-library.js line 228
6
+ const VALID_LOG_LEVELS = ['log', 'info', 'warn', 'error', 'debug', 'trace', 'table', 'dir', 'group', 'groupEnd', 'time', 'timeEnd', 'count'];
7
+
3
8
  // Common validation patterns
4
9
  const patterns = {
5
10
  sessionId: Joi.string().pattern(/^[a-zA-Z0-9_-]+$/).min(1).max(100),
@@ -53,7 +58,7 @@ export const workflowRecordingSchema = Joi.object({
53
58
  ).required(),
54
59
  logs: Joi.array().items(
55
60
  Joi.object({
56
- level: Joi.string().valid('log', 'info', 'warn', 'error', 'debug').required(),
61
+ level: Joi.string().valid(...VALID_LOG_LEVELS).required(),
57
62
  message: Joi.string().required(),
58
63
  timestamp: Joi.number().required(),
59
64
  args: Joi.array().optional()
@@ -76,13 +81,15 @@ export const workflowRecordingSchema = Joi.object({
76
81
  format: Joi.string().valid('png', 'jpeg').optional().allow(null),
77
82
  enabled: Joi.boolean().optional().allow(null),
78
83
  maxWidth: Joi.number().integer().optional().allow(null)
79
- }).optional().allow(null)
84
+ }).optional().allow(null),
85
+ recordingMode: Joi.string().valid('workflow', 'screenshot').optional().default('workflow')
80
86
  });
81
87
 
82
88
  // Frame batch schema
83
89
  export const frameBatchSchema = Joi.object({
84
90
  sessionId: patterns.sessionId.required(),
85
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
86
93
  frames: Joi.array().items(
87
94
  Joi.object({
88
95
  timestamp: Joi.number().required(),
@@ -91,9 +98,11 @@ export const frameBatchSchema = Joi.object({
91
98
  consoleLog: Joi.string().optional(),
92
99
  frameIndex: Joi.number().integer().min(0).optional(),
93
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
94
103
  logs: Joi.array().items(
95
104
  Joi.object({
96
- level: Joi.string().valid('log', 'info', 'warn', 'error', 'debug').required(),
105
+ level: Joi.string().valid(...VALID_LOG_LEVELS).required(),
97
106
  message: Joi.string().required(),
98
107
  timestamp: Joi.number().required(),
99
108
  args: Joi.array().optional()
@@ -105,6 +114,8 @@ export const frameBatchSchema = Joi.object({
105
114
  timestamp: Joi.number().required(),
106
115
  x: Joi.number().when('type', { is: 'click', then: Joi.required(), otherwise: Joi.optional() }),
107
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
108
119
  target: Joi.string().optional(),
109
120
  targetId: Joi.string().optional(),
110
121
  scrollX: Joi.number().when('type', { is: 'scroll', then: Joi.required(), otherwise: Joi.optional() }),
@@ -121,7 +132,7 @@ export const associateLogsSchema = Joi.object({
121
132
  sessionId: patterns.sessionId.required(),
122
133
  logs: Joi.array().items(
123
134
  Joi.object({
124
- level: Joi.string().valid('log', 'info', 'warn', 'error', 'debug').required(),
135
+ level: Joi.string().valid(...VALID_LOG_LEVELS).required(),
125
136
  message: Joi.string().required(),
126
137
  timestamp: Joi.number().required(),
127
138
  args: Joi.array().optional()
@@ -134,7 +145,7 @@ export const streamLogsSchema = Joi.object({
134
145
  sessionId: patterns.sessionId.required(),
135
146
  logs: Joi.array().items(
136
147
  Joi.object({
137
- level: Joi.string().valid('log', 'info', 'warn', 'error', 'debug', 'trace', 'table', 'dir', 'group', 'groupEnd', 'time', 'timeEnd', 'count').required(),
148
+ level: Joi.string().valid(...VALID_LOG_LEVELS).required(),
138
149
  message: Joi.string().required(),
139
150
  timestamp: Joi.number().required(),
140
151
  sequence: Joi.number().integer().min(0).required(),
@@ -173,6 +184,10 @@ export const screenInteractionsSchema = Joi.object({
173
184
  timestamp: Joi.number().required(),
174
185
  x: Joi.number().optional(),
175
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
176
191
  key: Joi.string().optional(),
177
192
  target: Joi.string().optional(),
178
193
  // Basic element fields that were previously blocked