@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/CLAUDE.md +18 -0
- package/README.md +226 -16
- package/chrome-extension/background.js +547 -56
- package/chrome-extension/browser-recording-manager.js +34 -0
- package/chrome-extension/content.js +438 -32
- package/chrome-extension/firebase-config.public-sw.js +1 -1
- package/chrome-extension/firebase-config.public.js +1 -1
- package/chrome-extension/frame-capture.js +31 -10
- package/chrome-extension/image-processor.js +193 -0
- package/chrome-extension/manifest.free.json +1 -1
- package/chrome-extension/popup.html +82 -4
- package/chrome-extension/popup.js +1106 -38
- package/chrome-extension/pro/frame-editor.html +259 -6
- package/chrome-extension/pro/frame-editor.js +959 -10
- package/chrome-extension/pro/video-exporter.js +917 -0
- package/chrome-extension/pro/video-player.js +545 -0
- package/dist/chromedebug-extension-free.zip +0 -0
- package/package.json +1 -1
- package/scripts/postinstall.js +1 -1
- package/scripts/webpack.config.free.cjs +6 -0
- package/scripts/webpack.config.pro.cjs +6 -0
- package/src/chrome-controller.js +6 -6
- package/src/database.js +157 -39
- package/src/http-server.js +55 -11
- package/src/validation/schemas.js +11 -1
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
|
|
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
|
-
|
|
1802
|
-
SELECT id, session_id, name, url, title, total_actions, timestamp, screenshot_settings
|
|
1803
|
-
FROM workflow_recordings
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
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,
|
package/src/http-server.js
CHANGED
|
@@ -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
|
-
|
|
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
|