@dynamicu/chromedebug-mcp 2.6.6 → 2.7.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 (46) hide show
  1. package/CLAUDE.md +1 -1
  2. package/README.md +1 -1
  3. package/chrome-extension/activation-manager.js +18 -4
  4. package/chrome-extension/background.js +1044 -552
  5. package/chrome-extension/browser-recording-manager.js +256 -0
  6. package/chrome-extension/chrome-debug-logger.js +168 -0
  7. package/chrome-extension/console-interception-library.js +430 -0
  8. package/chrome-extension/content.css +16 -16
  9. package/chrome-extension/content.js +617 -215
  10. package/chrome-extension/data-buffer.js +206 -17
  11. package/chrome-extension/extension-config.js +1 -1
  12. package/chrome-extension/frame-capture.js +52 -15
  13. package/chrome-extension/license-helper.js +26 -0
  14. package/chrome-extension/manifest.free.json +3 -6
  15. package/chrome-extension/options.js +1 -1
  16. package/chrome-extension/popup.html +315 -181
  17. package/chrome-extension/popup.js +673 -526
  18. package/chrome-extension/pro/enhanced-capture.js +406 -0
  19. package/chrome-extension/pro/frame-editor.html +410 -0
  20. package/chrome-extension/pro/frame-editor.js +1496 -0
  21. package/chrome-extension/pro/function-tracker.js +843 -0
  22. package/chrome-extension/pro/jszip.min.js +13 -0
  23. package/config/chromedebug-config.json +101 -0
  24. package/dist/chromedebug-extension-free.zip +0 -0
  25. package/package.json +3 -1
  26. package/scripts/package-pro-extension.js +1 -1
  27. package/scripts/webpack.config.free.cjs +11 -8
  28. package/scripts/webpack.config.pro.cjs +5 -0
  29. package/src/chrome-controller.js +7 -7
  30. package/src/cli.js +2 -2
  31. package/src/database.js +61 -9
  32. package/src/http-server.js +3 -2
  33. package/src/index.js +9 -6
  34. package/src/mcp/server.js +2 -2
  35. package/src/services/process-manager.js +10 -6
  36. package/src/services/process-tracker.js +10 -5
  37. package/src/services/profile-manager.js +17 -2
  38. package/src/validation/schemas.js +36 -6
  39. package/src/index-direct.js +0 -157
  40. package/src/index-modular.js +0 -219
  41. package/src/index-monolithic-backup.js +0 -2230
  42. package/src/legacy/chrome-controller-old.js +0 -1406
  43. package/src/legacy/index-express.js +0 -625
  44. package/src/legacy/index-old.js +0 -977
  45. package/src/legacy/routes.js +0 -260
  46. package/src/legacy/shared-storage.js +0 -101
@@ -8,12 +8,129 @@ importScripts('pako.min.js');
8
8
  importScripts('data-buffer.js');
9
9
  importScripts('upload-manager.js');
10
10
  importScripts('chrome-session-manager.js');
11
- importScripts('firebase-config.js');
12
- importScripts('license-helper.js');
11
+ importScripts('console-interception-library.js'); // Shared console interception library
12
+ importScripts('browser-recording-manager.js');
13
13
 
14
14
  const CONFIG_PORTS = CHROMEDEBUG_CONFIG.ports;
15
15
  const DISCOVERY_TIMEOUT = CHROMEDEBUG_CONFIG.discoveryTimeout;
16
16
 
17
+ // ============================================================
18
+ // FREE TIER USAGE TRACKING (Local Storage - No Firebase)
19
+ // ============================================================
20
+ // Daily limit: 5 recordings per day
21
+ // Resets at midnight local time
22
+ // Tracks both workflow and screen recordings
23
+ // ============================================================
24
+
25
+ const FREE_TIER_DAILY_LIMIT = 5;
26
+ const USAGE_STORAGE_KEY = 'chromedebug_daily_usage';
27
+
28
+ /**
29
+ * Check if this is the PRO version
30
+ */
31
+ function isProVersion() {
32
+ const manifest = chrome.runtime.getManifest();
33
+ return manifest.name.includes('PRO');
34
+ }
35
+
36
+ /**
37
+ * Get today's date string in local timezone (YYYY-MM-DD)
38
+ */
39
+ function getTodayDateString() {
40
+ const now = new Date();
41
+ return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`;
42
+ }
43
+
44
+ /**
45
+ * Get current usage for today
46
+ * @returns {Promise<{count: number, date: string, limit: number}>}
47
+ */
48
+ async function getUsageToday() {
49
+ // PRO version: unlimited
50
+ if (isProVersion()) {
51
+ return { count: 0, date: getTodayDateString(), limit: Infinity, isPro: true };
52
+ }
53
+
54
+ const result = await chrome.storage.local.get(USAGE_STORAGE_KEY);
55
+ const usage = result[USAGE_STORAGE_KEY] || { count: 0, date: '' };
56
+ const today = getTodayDateString();
57
+
58
+ // Reset if new day
59
+ if (usage.date !== today) {
60
+ return { count: 0, date: today, limit: FREE_TIER_DAILY_LIMIT };
61
+ }
62
+
63
+ return { count: usage.count, date: today, limit: FREE_TIER_DAILY_LIMIT };
64
+ }
65
+
66
+ /**
67
+ * Check if user can start a new recording
68
+ * @returns {Promise<{allowed: boolean, count: number, limit: number, message?: string}>}
69
+ */
70
+ async function checkUsageBeforeRecording() {
71
+ const usage = await getUsageToday();
72
+
73
+ if (usage.isPro) {
74
+ return { allowed: true, count: 0, limit: Infinity, isPro: true };
75
+ }
76
+
77
+ if (usage.count >= usage.limit) {
78
+ return {
79
+ allowed: false,
80
+ count: usage.count,
81
+ limit: usage.limit,
82
+ message: `Daily limit reached (${usage.count}/${usage.limit}). Upgrade to Pro for unlimited recordings.`
83
+ };
84
+ }
85
+
86
+ return { allowed: true, count: usage.count, limit: usage.limit };
87
+ }
88
+
89
+ /**
90
+ * Increment usage count after successful recording
91
+ * @returns {Promise<{success: boolean, count: number, limit: number}>}
92
+ */
93
+ async function incrementUsageAfterRecording() {
94
+ // PRO version: skip tracking
95
+ if (isProVersion()) {
96
+ console.log('[Usage] PRO version - skipping usage tracking');
97
+ return { success: true, count: 0, limit: Infinity, isPro: true };
98
+ }
99
+
100
+ const today = getTodayDateString();
101
+ const result = await chrome.storage.local.get(USAGE_STORAGE_KEY);
102
+ const usage = result[USAGE_STORAGE_KEY] || { count: 0, date: '' };
103
+
104
+ // Reset if new day, then increment
105
+ const newCount = (usage.date === today) ? usage.count + 1 : 1;
106
+
107
+ await chrome.storage.local.set({
108
+ [USAGE_STORAGE_KEY]: { count: newCount, date: today }
109
+ });
110
+
111
+ console.log(`[Usage] Recording tracked: ${newCount}/${FREE_TIER_DAILY_LIMIT} today`);
112
+
113
+ return { success: true, count: newCount, limit: FREE_TIER_DAILY_LIMIT };
114
+ }
115
+
116
+ // Console interception configuration for screen recording
117
+ const SCREEN_RECORDING_CONSOLE_CONFIG = {
118
+ overrideFlagName: '__chromePilotConsoleOverridden',
119
+ originalConsoleName: '__chromePilotOriginalConsole',
120
+ relayFlagName: '__chromePilotConsoleRelay',
121
+ messageType: 'chrome-debug-console-log',
122
+ backgroundAction: 'consoleLog'
123
+ };
124
+
125
+ // Console interception configuration for workflow recording
126
+ const WORKFLOW_RECORDING_CONSOLE_CONFIG = {
127
+ overrideFlagName: '__chromePilotWorkflowConsoleOverridden',
128
+ originalConsoleName: '__chromePilotWorkflowOriginalConsole',
129
+ relayFlagName: '__chromePilotWorkflowConsoleRelay',
130
+ messageType: 'chrome-debug-workflow-console-log',
131
+ backgroundAction: 'workflowConsoleLog'
132
+ };
133
+
17
134
  // Frame queue for handling validation race conditions
18
135
  // v2.1.2: Prevents frame loss when lease renewal hasn't propagated yet
19
136
  const pendingFrameQueue = new Map(); // sessionId -> array of frame batches
@@ -35,12 +152,18 @@ class LogStreamer {
35
152
  }
36
153
 
37
154
  async init() {
155
+ // Skip initialization in browser-only mode
156
+ if (serverMode === 'browser-only') {
157
+ console.log('[LogStreamer] Skipping initialization - browser-only mode');
158
+ return;
159
+ }
160
+
38
161
  // Test server availability
39
162
  await this.checkServerAvailability();
40
-
163
+
41
164
  // Start periodic streaming
42
165
  this.startPeriodicStreaming();
43
-
166
+
44
167
  console.log('[LogStreamer] Initialized with server availability:', this.serverAvailable);
45
168
  }
46
169
 
@@ -78,10 +201,16 @@ class LogStreamer {
78
201
  async addLog(log, sessionId) {
79
202
  log.sequence = ++this.sequenceNumber;
80
203
  log.timestamp = Date.now();
81
-
204
+
82
205
  this.currentSessionId = sessionId;
206
+
207
+ // In browser-only mode, skip server streaming (logs stored in LogBuffer)
208
+ if (serverMode === 'browser-only') {
209
+ return;
210
+ }
211
+
83
212
  this.buffer.push(log);
84
-
213
+
85
214
  // If buffer is getting full, force immediate stream
86
215
  if (this.buffer.length >= this.maxBufferSize) {
87
216
  await this.streamLogs();
@@ -89,6 +218,9 @@ class LogStreamer {
89
218
  }
90
219
 
91
220
  async streamLogs() {
221
+ // Skip streaming in browser-only mode
222
+ if (serverMode === 'browser-only') return;
223
+
92
224
  if (this.buffer.length === 0 || !this.currentSessionId) return;
93
225
 
94
226
  // Check if we have waited at least 100ms since last frame capture
@@ -465,11 +597,169 @@ class LogTabBuffer {
465
597
  }
466
598
  }
467
599
 
600
+ /**
601
+ * WorkflowLogBuffer - Race-safe log buffer for workflow recording
602
+ *
603
+ * Mirrors LogTabBuffer but uses workflow-specific storage keys (`workflow_${tabId}`).
604
+ * This prevents race conditions when multiple logs arrive rapidly during workflow recording.
605
+ *
606
+ * Technical Debt Note: This class duplicates LogTabBuffer logic. Future refactoring
607
+ * could make LogTabBuffer accept a configurable storage key prefix to eliminate duplication.
608
+ */
609
+ class WorkflowLogBuffer {
610
+ constructor(tabId) {
611
+ this.tabId = tabId;
612
+ this.buffer = [];
613
+ this.processing = false;
614
+ this.processingQueue = [];
615
+ this.totalLogsAdded = 0;
616
+ this.totalLogsFlushed = 0;
617
+ this.lastFlushTime = 0;
618
+ this.storageKey = `workflow_${tabId}`;
619
+ }
620
+
621
+ // Add log to buffer with queuing to prevent race conditions
622
+ async addLog(logEntry) {
623
+ return new Promise((resolve, reject) => {
624
+ this.processingQueue.push({
625
+ type: 'add',
626
+ logEntry,
627
+ resolve,
628
+ reject
629
+ });
630
+
631
+ // Process queue if not already processing
632
+ if (!this.processing) {
633
+ this.processQueue();
634
+ }
635
+ });
636
+ }
637
+
638
+ // Flush buffer to Chrome storage
639
+ async flush() {
640
+ return new Promise((resolve, reject) => {
641
+ this.processingQueue.push({
642
+ type: 'flush',
643
+ resolve,
644
+ reject
645
+ });
646
+
647
+ // Process queue if not already processing
648
+ if (!this.processing) {
649
+ this.processQueue();
650
+ }
651
+ });
652
+ }
653
+
654
+ // Sequential processing of queue to prevent race conditions
655
+ async processQueue() {
656
+ if (this.processing) {
657
+ return; // Already processing
658
+ }
659
+
660
+ this.processing = true;
661
+
662
+ try {
663
+ while (this.processingQueue.length > 0) {
664
+ const operation = this.processingQueue.shift();
665
+
666
+ try {
667
+ if (operation.type === 'add') {
668
+ this.buffer.push(operation.logEntry);
669
+ this.totalLogsAdded++;
670
+
671
+ // Auto-flush if buffer gets too large
672
+ if (this.buffer.length >= 100) {
673
+ await this.performFlush();
674
+ }
675
+
676
+ operation.resolve();
677
+ } else if (operation.type === 'flush') {
678
+ await this.performFlush();
679
+ operation.resolve();
680
+ }
681
+ } catch (error) {
682
+ operation.reject(error);
683
+ }
684
+ }
685
+ } finally {
686
+ this.processing = false;
687
+ }
688
+ }
689
+
690
+ // Actual flush operation to Chrome storage
691
+ async performFlush() {
692
+ if (this.buffer.length === 0) {
693
+ return; // Nothing to flush
694
+ }
695
+
696
+ try {
697
+ // Read current logs from storage
698
+ const result = await chrome.storage.session.get(this.storageKey);
699
+ const existingLogs = result[this.storageKey] || [];
700
+
701
+ // Append new logs
702
+ const updatedLogs = [...existingLogs, ...this.buffer];
703
+
704
+ // Check storage size limit
705
+ const estimatedSize = JSON.stringify(updatedLogs).length;
706
+ if (estimatedSize > 500000) { // ~500KB limit
707
+ const beforeCount = updatedLogs.length;
708
+ const retentionPercentage = 0.95;
709
+ const afterCount = Math.floor(updatedLogs.length * retentionPercentage);
710
+ console.warn(`[WorkflowLogBuffer] Storage size limit approached for tab ${this.tabId}, truncating from ${beforeCount} to ${afterCount} logs`);
711
+ const recentLogs = updatedLogs.slice(-afterCount);
712
+ await chrome.storage.session.set({ [this.storageKey]: recentLogs });
713
+ } else {
714
+ await chrome.storage.session.set({ [this.storageKey]: updatedLogs });
715
+ }
716
+
717
+ this.totalLogsFlushed += this.buffer.length;
718
+ console.log(`[WorkflowLogBuffer] Flushed ${this.buffer.length} logs for tab ${this.tabId} (total: ${this.totalLogsFlushed})`);
719
+
720
+ // Clear the buffer
721
+ this.buffer = [];
722
+ this.lastFlushTime = Date.now();
723
+
724
+ } catch (error) {
725
+ console.error(`[WorkflowLogBuffer] Failed to flush logs for tab ${this.tabId}:`, error);
726
+ throw error;
727
+ }
728
+ }
729
+
730
+ // Get buffer statistics
731
+ getStats() {
732
+ return {
733
+ bufferedLogs: this.buffer.length,
734
+ totalAdded: this.totalLogsAdded,
735
+ totalFlushed: this.totalLogsFlushed,
736
+ processing: this.processing,
737
+ queueLength: this.processingQueue.length,
738
+ lastFlushTime: this.lastFlushTime
739
+ };
740
+ }
741
+
742
+ // Clean up buffer when recording stops
743
+ async cleanup() {
744
+ try {
745
+ // Flush any remaining logs
746
+ if (this.buffer.length > 0) {
747
+ await this.performFlush();
748
+ }
749
+ console.log(`[WorkflowLogBuffer] Cleaned up buffer for tab ${this.tabId}`);
750
+ } catch (error) {
751
+ console.error(`[WorkflowLogBuffer] Error during cleanup for tab ${this.tabId}:`, error);
752
+ }
753
+ }
754
+ }
755
+
468
756
  // Initialize data buffer and upload manager
469
757
  let dataBuffer = null;
470
758
  let uploadManager = null;
471
759
  let logStreamer = null;
472
760
  let logBuffer = null;
761
+ let browserRecordingManager = null;
762
+ let serverMode = 'unknown'; // 'server', 'browser-only', or 'unknown'
473
763
 
474
764
  // Error handling state and configuration
475
765
  // IMPORTANT: These must be declared before initializeServices()
@@ -566,6 +856,15 @@ async function initializeServices() {
566
856
  logBuffer = null;
567
857
  }
568
858
 
859
+ // Initialize browser recording manager for browser-only mode
860
+ try {
861
+ browserRecordingManager = new BrowserRecordingManager(dataBuffer);
862
+ console.log('[Background] Browser recording manager initialized');
863
+ } catch (recordingErr) {
864
+ console.error('[Background] Failed to initialize browser recording manager:', recordingErr);
865
+ browserRecordingManager = null;
866
+ }
867
+
569
868
  // Initialize session manager with recovery
570
869
  try {
571
870
  if (!sessionManager) {
@@ -628,10 +927,14 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
628
927
  'restoreFromPoint',
629
928
  'getWebSocketPort',
630
929
  'startRecording',
930
+ 'stopRecording',
931
+ 'openFrameEditor',
631
932
  'checkConnection',
632
933
  'workflowConsoleLog', // v2.0.8: handle workflow console logs
633
934
  'workflowAction', // v2.0.8: handle workflow actions
634
- 'renewLease' // v2.1.2: session manager lease renewal for frame capture
935
+ 'renewLease', // v2.1.2: session manager lease renewal for frame capture
936
+ 'frameSessionComplete', // Frame recording completion notification
937
+ 'recordingStopped' // Recording stopped notification for popup refresh
635
938
  ];
636
939
 
637
940
  if (handledByOtherListener.includes(messageType)) {
@@ -677,7 +980,27 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
677
980
  sendResponse({ status: 'error', message: error.message });
678
981
  }
679
982
  break;
680
-
983
+
984
+ case 'getUsage':
985
+ // Get current usage count for popup display
986
+ getUsageToday().then(usage => {
987
+ sendResponse({ success: true, ...usage });
988
+ }).catch(error => {
989
+ console.error('[Usage] Failed to get usage:', error);
990
+ sendResponse({ success: false, error: error.message });
991
+ });
992
+ return true; // Async response
993
+
994
+ case 'checkUsageLimit':
995
+ // Check if user can start a new recording
996
+ checkUsageBeforeRecording().then(result => {
997
+ sendResponse(result);
998
+ }).catch(error => {
999
+ console.error('[Usage] Failed to check usage limit:', error);
1000
+ sendResponse({ allowed: true, error: error.message }); // Fail-open
1001
+ });
1002
+ return true; // Async response
1003
+
681
1004
  case 'UPLOAD_BATCH':
682
1005
  // Add events to the background's DataBuffer for upload
683
1006
  if (message.events && message.recordingId) {
@@ -909,105 +1232,127 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
909
1232
  }
910
1233
  break;
911
1234
 
912
- default:
913
- // Don't warn or respond for messages that should be handled by other listener (v2.0.6)
914
- // This prevents intercepting workflow recording messages
915
- console.log('[Background v2.0.6] Message not handled by this listener:', messageType);
916
- // Don't send response - let other listener handle it
917
- return;
918
- }
919
- });
920
-
921
- // Auto-activate PRO license on startup if activation file exists
922
- async function checkAndActivateLicense() {
923
- try {
924
- console.log('[License Auto-Activation] Checking for activation file...');
925
-
926
- // Try to fetch the activation file from the extension directory
927
- const activationUrl = chrome.runtime.getURL('license-activation.json');
928
- const response = await fetch(activationUrl);
929
-
930
- if (!response.ok) {
931
- console.log('[License Auto-Activation] No activation file found - using normal license flow');
932
- return;
933
- }
934
-
935
- const activationData = await response.json();
936
- console.log('[License Auto-Activation] Found activation file:', {
937
- tier: activationData.tier,
938
- activated_at: activationData.activated_at
939
- });
940
-
941
- // Check if license is already cached and valid
942
- const cached = await chrome.storage.local.get('chromedebug_license_cache');
943
- if (cached.chromedebug_license_cache?.valid && cached.chromedebug_license_cache?.tier === 'pro') {
944
- console.log('[License Auto-Activation] License already activated and cached');
945
- return;
946
- }
1235
+ case 'getBrowserRecordings':
1236
+ // Get all browser-only recordings from IndexedDB
1237
+ console.log('[Background] getBrowserRecordings - Manager exists:', !!browserRecordingManager);
1238
+ if (browserRecordingManager) {
1239
+ browserRecordingManager.listRecordings().then(recordings => {
1240
+ console.log('[Background] Recordings retrieved from manager:', recordings);
1241
+ sendResponse(recordings);
1242
+ }).catch(error => {
1243
+ console.error('[Background] Failed to get browser recordings:', error);
1244
+ sendResponse([]);
1245
+ });
1246
+ return true; // Async response
1247
+ } else {
1248
+ console.warn('[Background] browserRecordingManager not initialized');
1249
+ sendResponse([]);
1250
+ }
1251
+ break;
947
1252
 
948
- // Activate the license
949
- const licenseKey = activationData.license_key;
950
- console.log('[License Auto-Activation] Activating license...');
1253
+ case 'exportBrowserRecording':
1254
+ // Export a browser-only recording
1255
+ if (browserRecordingManager && message.sessionId) {
1256
+ browserRecordingManager.exportRecording(message.sessionId).then(result => {
1257
+ sendResponse({ success: true, data: result });
1258
+ }).catch(error => {
1259
+ console.error('[Background] Failed to export recording:', error);
1260
+ sendResponse({ success: false, error: error.message });
1261
+ });
1262
+ return true; // Async response
1263
+ } else {
1264
+ sendResponse({ success: false, error: 'Manager not available or missing sessionId' });
1265
+ }
1266
+ break;
951
1267
 
952
- // Call LemonSqueezy activation via Firebase Cloud Function
953
- const instanceId = crypto.randomUUID();
954
- await chrome.storage.local.set({
955
- 'chromedebug_instance_id': instanceId,
956
- 'ls_instance_id': instanceId,
957
- 'ls_license_key': licenseKey
958
- });
1268
+ case 'deleteBrowserRecording':
1269
+ // Delete a browser-only recording
1270
+ if (browserRecordingManager && message.sessionId) {
1271
+ browserRecordingManager.deleteRecording(message.sessionId).then(() => {
1272
+ sendResponse({ success: true });
1273
+ }).catch(error => {
1274
+ console.error('[Background] Failed to delete recording:', error);
1275
+ sendResponse({ success: false, error: error.message });
1276
+ });
1277
+ return true; // Async response
1278
+ } else {
1279
+ sendResponse({ success: false, error: 'Manager not available or missing sessionId' });
1280
+ }
1281
+ break;
959
1282
 
960
- const activationResponse = await fetch(`${FUNCTIONS_URL}/activateLicense`, {
961
- method: 'POST',
962
- headers: {'Content-Type': 'application/json'},
963
- body: JSON.stringify({
964
- licenseKey,
965
- instanceId,
966
- instanceName: `ChromeDebug-${navigator.userAgent.match(/Chrome\/(\d+)/)?.[1] || 'Unknown'}`
967
- })
968
- });
1283
+ case 'getBrowserRecordingFrames':
1284
+ // Get frames for a browser-only recording
1285
+ if (dataBuffer && message.sessionId) {
1286
+ dataBuffer.getBrowserFrames(message.sessionId).then(frames => {
1287
+ console.log(`[DEBUG] getBrowserRecordingFrames: Retrieved ${frames.length} frames from IndexedDB`);
1288
+
1289
+ // Debug first frame to see structure
1290
+ if (frames.length > 0) {
1291
+ const firstFrame = frames[0];
1292
+ const debugInfo = {
1293
+ hasScreenshot: !!firstFrame.screenshot,
1294
+ screenshotPrefix: firstFrame.screenshot ? firstFrame.screenshot.substring(0, 30) : 'N/A',
1295
+ hasLogs: !!firstFrame.logs,
1296
+ logsCount: firstFrame.logs ? firstFrame.logs.length : 0,
1297
+ timestamp: firstFrame.timestamp,
1298
+ frameIndex: firstFrame.frameIndex,
1299
+ allKeys: Object.keys(firstFrame)
1300
+ };
1301
+ console.log('[DEBUG] First frame structure:', JSON.stringify(debugInfo, null, 2));
1302
+ }
969
1303
 
970
- if (!activationResponse.ok) {
971
- throw new Error(`Activation failed: HTTP ${activationResponse.status}`);
972
- }
1304
+ // Transform frames to match frame editor's expected structure
1305
+ // Frame editor expects 'imageData' property, but we store as 'screenshot'
1306
+ const transformedFrames = frames.map(frame => ({
1307
+ ...frame,
1308
+ imageData: frame.screenshot // Rename screenshot to imageData
1309
+ }));
973
1310
 
974
- const activationResult = await activationResponse.json();
975
- console.log('[License Auto-Activation] Activation result:', activationResult);
1311
+ const transformDebug = transformedFrames.length > 0 ? {
1312
+ hasImageData: !!transformedFrames[0].imageData,
1313
+ imageDataPrefix: transformedFrames[0].imageData ? transformedFrames[0].imageData.substring(0, 30) : 'N/A',
1314
+ hasLogs: !!transformedFrames[0].logs,
1315
+ logsCount: transformedFrames[0].logs ? transformedFrames[0].logs.length : 0,
1316
+ allKeys: Object.keys(transformedFrames[0])
1317
+ } : 'No frames';
1318
+ console.log('[DEBUG] Transformed first frame:', JSON.stringify(transformDebug, null, 2));
976
1319
 
977
- if (activationResult.activated || activationResult.valid) {
978
- // Cache the license status
979
- const licenseCache = {
980
- valid: true,
981
- tier: 'pro',
982
- licenseKey: licenseKey.substring(0, 8) + '...',
983
- cachedAt: Date.now(),
984
- graceUntil: Date.now() + (30 * 24 * 60 * 60 * 1000) // 30 days
985
- };
1320
+ sendResponse({ success: true, frames: transformedFrames });
1321
+ }).catch(error => {
1322
+ console.error('[Background] Failed to get browser recording frames:', error);
1323
+ sendResponse({ success: false, error: error.message });
1324
+ });
1325
+ return true; // Async response
1326
+ } else {
1327
+ sendResponse({ success: false, error: 'DataBuffer not available or missing sessionId' });
1328
+ }
1329
+ break;
986
1330
 
987
- await chrome.storage.local.set({
988
- 'chromedebug_license_cache': licenseCache
1331
+ case 'getQuotaInfo':
1332
+ // Get storage quota information
1333
+ getQuotaInfo().then(info => {
1334
+ sendResponse(info);
1335
+ }).catch(error => {
1336
+ console.error('[Background] Failed to get quota info:', error);
1337
+ sendResponse({ supported: false });
989
1338
  });
1339
+ return true; // Async response
990
1340
 
991
- console.log('[License Auto-Activation] ✓ PRO license activated successfully!');
992
- console.log('[License Auto-Activation] Cached license status:', licenseCache);
993
- } else {
994
- console.error('[License Auto-Activation] Activation returned invalid status:', activationResult);
995
- }
996
-
997
- } catch (error) {
998
- // Don't block extension startup on activation errors
999
- console.error('[License Auto-Activation] Failed to auto-activate license:', error);
1000
- console.log('[License Auto-Activation] Extension will continue with normal license flow');
1341
+ default:
1342
+ // Don't warn or respond for messages that should be handled by other listener (v2.0.6)
1343
+ // This prevents intercepting workflow recording messages
1344
+ console.log('[Background v2.0.6] Message not handled by this listener:', messageType);
1345
+ // Don't send response - let other listener handle it
1346
+ return;
1001
1347
  }
1002
- }
1348
+ });
1349
+
1350
+ // License checking removed - using separate Chrome Web Store listings for FREE and PRO
1003
1351
 
1004
1352
  // Session recovery on startup - handle any stuck recordings from previous session
1005
1353
  chrome.runtime.onStartup.addListener(async () => {
1006
1354
  console.log('[Background] Extension startup detected - checking for stuck sessions');
1007
1355
 
1008
- // Check for PRO license auto-activation first
1009
- await checkAndActivateLicense();
1010
-
1011
1356
  if (sessionManager) {
1012
1357
  try {
1013
1358
  // Use existing recovery method from session manager
@@ -1021,12 +1366,9 @@ chrome.runtime.onStartup.addListener(async () => {
1021
1366
  }
1022
1367
  });
1023
1368
 
1024
- // Extension install/update - also check for license activation
1369
+ // Extension install/update
1025
1370
  chrome.runtime.onInstalled.addListener(async (details) => {
1026
1371
  console.log('[Background] Extension installed/updated:', details.reason);
1027
-
1028
- // Check for PRO license auto-activation on install/update
1029
- await checkAndActivateLicense();
1030
1372
  });
1031
1373
 
1032
1374
  // Listen for tab updates to handle restore points
@@ -1124,6 +1466,43 @@ chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
1124
1466
  }
1125
1467
  });
1126
1468
 
1469
+ // Defensive cleanup when tabs are closed
1470
+ chrome.tabs.onRemoved.addListener((tabId, removeInfo) => {
1471
+ // Clean up any workflow recording state for closed tabs
1472
+ if (workflowRecordingTabs.has(tabId)) {
1473
+ console.log(`[Workflow] Tab ${tabId} closed during recording, cleaning up`);
1474
+ workflowRecordingTabs.delete(tabId);
1475
+ workflowIncludeLogs.delete(tabId);
1476
+ workflowScreenshotSettings.delete(tabId);
1477
+ workflowSessionNames.delete(tabId);
1478
+
1479
+ // Clean up WorkflowLogBuffer
1480
+ const buffer = workflowLogBuffers.get(tabId);
1481
+ if (buffer) {
1482
+ buffer.cleanup().then(() => {
1483
+ workflowLogBuffers.delete(tabId);
1484
+ console.log(`[Workflow] Cleaned up WorkflowLogBuffer for closed tab ${tabId}`);
1485
+ }).catch(err => {
1486
+ console.error(`[Workflow] Error cleaning up buffer for closed tab ${tabId}:`, err);
1487
+ workflowLogBuffers.delete(tabId);
1488
+ });
1489
+ }
1490
+
1491
+ // Remove session storage
1492
+ chrome.storage.session.remove(`workflow_${tabId}`).catch(err => {
1493
+ console.error(`[Workflow] Error removing session storage for closed tab ${tabId}:`, err);
1494
+ });
1495
+
1496
+ // Notify popup that recording has stopped (tab was closed)
1497
+ chrome.storage.local.set({
1498
+ workflowRecording: false,
1499
+ workflowStartTime: null
1500
+ }).catch(err => {
1501
+ console.error(`[Workflow] Error updating storage for closed tab ${tabId}:`, err);
1502
+ });
1503
+ }
1504
+ });
1505
+
1127
1506
  // Session Manager for robust recording state management
1128
1507
  let sessionManager = null;
1129
1508
  let currentSession = null; // Cache current session state
@@ -1266,7 +1645,7 @@ let workflowRecordingTabs = new Map(); // Map of tabId to recording state
1266
1645
  let workflowIncludeLogs = new Map(); // Map of tabId to includeLogsInExport setting
1267
1646
  let workflowScreenshotSettings = new Map(); // Map of tabId to screenshot settings
1268
1647
  let workflowSessionNames = new Map(); // Map of tabId to session name
1269
- let workflowUserIds = new Map(); // Map of tabId to userId for license tracking
1648
+ let workflowLogBuffers = new Map(); // Map of tabId to WorkflowLogBuffer instance
1270
1649
 
1271
1650
  //=============================================================================
1272
1651
  // COMPREHENSIVE ERROR HANDLING SYSTEM
@@ -1742,7 +2121,7 @@ async function downloadBackupData() {
1742
2121
 
1743
2122
  chrome.downloads.download({
1744
2123
  url: url,
1745
- filename: `chrome-pilot-backup-${Date.now()}.json`
2124
+ filename: `chrome-debug-backup-${Date.now()}.json`
1746
2125
  });
1747
2126
 
1748
2127
  console.log('[ErrorHandling] Backup data download initiated');
@@ -1882,8 +2261,13 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
1882
2261
  }
1883
2262
 
1884
2263
  if (request.action === 'stopRecording') {
1885
- stopRecording().then(() => {
1886
- sendResponse({ success: true });
2264
+ stopRecording().then(async (result) => {
2265
+ // Track usage for FREE tier after successful screen recording
2266
+ if (result.success) {
2267
+ const usageResult = await incrementUsageAfterRecording();
2268
+ result.usage = usageResult; // Include updated usage in response
2269
+ }
2270
+ sendResponse(result); // Pass through { success: true, sessionId: ..., usage: ... }
1887
2271
  }).catch((error) => {
1888
2272
  console.error('Error stopping recording:', error);
1889
2273
  sendResponse({ success: false, error: error.message });
@@ -1891,6 +2275,34 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
1891
2275
  return true;
1892
2276
  }
1893
2277
 
2278
+ if (request.action === 'openFrameEditor') {
2279
+ // Content scripts can't create tabs, so handle it here
2280
+ const sessionId = request.sessionId;
2281
+ const type = request.type; // 'workflow' or undefined for screen recordings
2282
+ const editorUrl = type === 'workflow'
2283
+ ? chrome.runtime.getURL(`pro/frame-editor.html?sessionId=${sessionId}&type=workflow`)
2284
+ : chrome.runtime.getURL(`pro/frame-editor.html?sessionId=${sessionId}`);
2285
+
2286
+ chrome.tabs.create({ url: editorUrl }).then(() => {
2287
+ sendResponse({ success: true });
2288
+ }).catch((error) => {
2289
+ console.error('Error opening frame editor:', error);
2290
+ sendResponse({ success: false, error: error.message });
2291
+ });
2292
+ return true;
2293
+ }
2294
+
2295
+ // Get quota information for browser-only mode
2296
+ if (request.action === 'getQuotaInfo') {
2297
+ getQuotaInfo().then((quotaInfo) => {
2298
+ sendResponse({ quotaInfo });
2299
+ }).catch((error) => {
2300
+ console.error('Error getting quota info:', error);
2301
+ sendResponse({ quotaInfo: null, error: error.message });
2302
+ });
2303
+ return true;
2304
+ }
2305
+
1894
2306
  // Session manager lease renewal handler
1895
2307
  if (request.action === 'renewLease') {
1896
2308
  console.log('[Background] RENEW LEASE - Session:', request.sessionId, 'Manager exists:', !!sessionManager);
@@ -2073,20 +2485,6 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
2073
2485
  return true;
2074
2486
  }
2075
2487
 
2076
- // License check handler for workflow recording
2077
- if (request.action === 'checkLicenseForWorkflow') {
2078
- console.log('[License] popup.js requesting license check for workflow recording');
2079
- LicenseHelper.checkLicenseBeforeRecording().then((licenseCheck) => {
2080
- console.log('[License] License check result for popup:', licenseCheck);
2081
- sendResponse(licenseCheck);
2082
- }).catch((error) => {
2083
- console.error('[License] Error checking license:', error);
2084
- // Fail-open: allow recording on error
2085
- sendResponse({ allowed: true, error: error.message });
2086
- });
2087
- return true;
2088
- }
2089
-
2090
2488
  // Workflow recording handlers
2091
2489
  if (request.action === 'startWorkflowRecording') {
2092
2490
  console.log('Starting workflow recording for tab:', request.tabId);
@@ -2105,9 +2503,17 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
2105
2503
  }
2106
2504
 
2107
2505
  if (request.action === 'stopWorkflowRecording') {
2108
- console.log('Stopping workflow recording for tab:', request.tabId);
2109
- stopWorkflowRecording(request.tabId).then((workflow) => {
2110
- sendResponse({ success: true, workflow: workflow });
2506
+ const tabId = request.tabId || sender.tab?.id;
2507
+ console.log('Stopping workflow recording for tab:', tabId);
2508
+ // stopWorkflowRecording already returns { success: true, workflow: {...}, savedToServer: bool }
2509
+ // so we pass it through directly - don't double-wrap!
2510
+ stopWorkflowRecording(tabId).then(async (result) => {
2511
+ // Track usage for FREE tier after successful recording
2512
+ if (result.success) {
2513
+ const usageResult = await incrementUsageAfterRecording();
2514
+ result.usage = usageResult; // Include updated usage in response
2515
+ }
2516
+ sendResponse(result);
2111
2517
  }).catch((error) => {
2112
2518
  console.error('Error stopping workflow recording:', error);
2113
2519
  sendResponse({ success: false, error: error.message });
@@ -2212,18 +2618,19 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
2212
2618
  }
2213
2619
  }
2214
2620
 
2215
- // Buffer workflow console logs
2621
+ // Buffer workflow console logs using race-safe WorkflowLogBuffer
2216
2622
  if (request.action === 'workflowConsoleLog' && sender.tab) {
2217
2623
  const tabId = sender.tab.id;
2218
2624
  if (workflowRecordingTabs.has(tabId) && workflowIncludeLogs.get(tabId)) {
2219
- // Store logs in session storage for workflow recording
2220
- chrome.storage.session.get(`workflow_${tabId}`).then(result => {
2221
- const logs = result[`workflow_${tabId}`] || [];
2222
- logs.push(request.log);
2223
- chrome.storage.session.set({ [`workflow_${tabId}`]: logs });
2224
- }).catch(err => {
2225
- console.error('Error buffering workflow log:', err);
2226
- });
2625
+ const buffer = workflowLogBuffers.get(tabId);
2626
+ if (buffer) {
2627
+ // Use race-safe buffer to prevent read-modify-write race conditions
2628
+ buffer.addLog(request.log).catch(err => {
2629
+ console.error('[WorkflowLogBuffer] Error adding log:', err);
2630
+ });
2631
+ } else {
2632
+ console.warn(`[Workflow] No buffer found for tab ${tabId}, log dropped`);
2633
+ }
2227
2634
  }
2228
2635
  }
2229
2636
 
@@ -2322,13 +2729,24 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
2322
2729
 
2323
2730
  // Handle workflow screenshot capture
2324
2731
  if (request.action === 'captureWorkflowScreenshot' && sender.tab) {
2732
+ console.log('[SCREENSHOT-DEBUG] background.js - Received captureWorkflowScreenshot message');
2325
2733
  const tabId = sender.tab.id;
2326
2734
  const settings = request.settings || {};
2327
-
2735
+ console.log('[SCREENSHOT-DEBUG] background.js - tabId:', tabId);
2736
+ console.log('[SCREENSHOT-DEBUG] background.js - settings:', JSON.stringify(settings));
2737
+
2328
2738
  captureTabScreenshot(tabId, settings).then((screenshotData) => {
2739
+ console.log('[SCREENSHOT-DEBUG] background.js - captureTabScreenshot succeeded');
2740
+ console.log('[SCREENSHOT-DEBUG] background.js - screenshotData type:', typeof screenshotData);
2741
+ console.log('[SCREENSHOT-DEBUG] background.js - screenshotData length:', screenshotData?.length);
2742
+ console.log('[SCREENSHOT-DEBUG] background.js - screenshotData preview:', screenshotData?.substring(0, 100));
2743
+ console.log('[SCREENSHOT-DEBUG] background.js - Sending response with success: true');
2329
2744
  sendResponse({ success: true, screenshotData });
2330
2745
  }).catch((error) => {
2331
- console.error('Error capturing screenshot:', error);
2746
+ console.error('[SCREENSHOT-DEBUG] background.js - captureTabScreenshot FAILED');
2747
+ console.error('[SCREENSHOT-DEBUG] background.js - Error:', error);
2748
+ console.error('[SCREENSHOT-DEBUG] background.js - Error message:', error.message);
2749
+ console.error('[SCREENSHOT-DEBUG] background.js - Error stack:', error.stack);
2332
2750
  sendResponse({ success: false, error: error.message });
2333
2751
  });
2334
2752
  return true;
@@ -2456,14 +2874,34 @@ async function takeStandaloneSnapshot(tabId, note = '') {
2456
2874
 
2457
2875
  // Capture screenshot for workflow recording
2458
2876
  async function captureTabScreenshot(tabId, settings) {
2877
+ console.log('[SCREENSHOT-DEBUG] captureTabScreenshot - ENTRY');
2878
+ console.log('[SCREENSHOT-DEBUG] captureTabScreenshot - tabId:', tabId);
2879
+ console.log('[SCREENSHOT-DEBUG] captureTabScreenshot - settings:', JSON.stringify(settings));
2880
+
2459
2881
  try {
2460
- const dataUrl = await chrome.tabs.captureVisibleTab(null, {
2882
+ // Get the tab to find its window
2883
+ console.log('[SCREENSHOT-DEBUG] captureTabScreenshot - Getting tab info...');
2884
+ const tab = await chrome.tabs.get(tabId);
2885
+ const windowId = tab.windowId;
2886
+ console.log('[SCREENSHOT-DEBUG] captureTabScreenshot - windowId:', windowId);
2887
+ console.log('[SCREENSHOT-DEBUG] captureTabScreenshot - tab.url:', tab.url);
2888
+
2889
+ const captureOptions = {
2461
2890
  format: settings.format || 'jpeg',
2462
2891
  quality: settings.quality || 30
2463
- });
2464
-
2892
+ };
2893
+ console.log('[SCREENSHOT-DEBUG] captureTabScreenshot - captureOptions:', JSON.stringify(captureOptions));
2894
+ console.log('[SCREENSHOT-DEBUG] captureTabScreenshot - Calling chrome.tabs.captureVisibleTab...');
2895
+
2896
+ const dataUrl = await chrome.tabs.captureVisibleTab(windowId, captureOptions);
2897
+
2898
+ console.log('[SCREENSHOT-DEBUG] captureTabScreenshot - captureVisibleTab SUCCESS');
2899
+ console.log('[SCREENSHOT-DEBUG] captureTabScreenshot - dataUrl length:', dataUrl?.length);
2900
+ console.log('[SCREENSHOT-DEBUG] captureTabScreenshot - dataUrl prefix:', dataUrl?.substring(0, 100));
2901
+
2465
2902
  // If resolution is specified, resize the image
2466
2903
  if (settings.maxWidth || settings.maxHeight) {
2904
+ console.log('[SCREENSHOT-DEBUG] captureTabScreenshot - Resizing required, maxWidth:', settings.maxWidth, 'maxHeight:', settings.maxHeight);
2467
2905
  // Create an image element to get dimensions
2468
2906
  const img = new Image();
2469
2907
  const canvas = new OffscreenCanvas(1, 1);
@@ -2503,36 +2941,73 @@ async function captureTabScreenshot(tabId, settings) {
2503
2941
  });
2504
2942
  }
2505
2943
 
2944
+ console.log('[SCREENSHOT-DEBUG] captureTabScreenshot - No resize needed, returning dataUrl');
2945
+ console.log('[SCREENSHOT-DEBUG] captureTabScreenshot - Final dataUrl length:', dataUrl?.length);
2506
2946
  return dataUrl;
2507
2947
  } catch (error) {
2508
- console.error('Error capturing tab screenshot:', error);
2948
+ console.error('[SCREENSHOT-DEBUG] captureTabScreenshot - EXCEPTION CAUGHT');
2949
+ console.error('[SCREENSHOT-DEBUG] captureTabScreenshot - Error:', error);
2950
+ console.error('[SCREENSHOT-DEBUG] captureTabScreenshot - Error message:', error.message);
2951
+ console.error('[SCREENSHOT-DEBUG] captureTabScreenshot - Error stack:', error.stack);
2509
2952
  throw error;
2510
2953
  }
2511
2954
  }
2512
2955
 
2513
2956
  // Start recording
2514
- async function startRecording(tabId, settings = {}) {
2515
- try {
2516
- // STEP 1: Check license and usage limits BEFORE recording
2517
- console.log('[License] Checking license before recording...');
2518
- const licenseCheck = await LicenseHelper.checkLicenseBeforeRecording();
2519
- console.log('[License] License check result:', licenseCheck);
2957
+ // Detect if server is available (server mode vs browser-only mode)
2958
+ async function detectServerMode() {
2959
+ console.log('[ServerDetection] Checking server availability...');
2520
2960
 
2521
- if (!licenseCheck.allowed) {
2522
- // Show notification to user
2523
- chrome.notifications.create({
2524
- type: 'basic',
2525
- iconUrl: chrome.runtime.getURL('icon48.png'),
2526
- title: 'Recording Limit Reached',
2527
- message: licenseCheck.message || 'Upgrade to Pro for unlimited recordings.',
2528
- priority: 2
2529
- });
2961
+ for (const port of CONFIG_PORTS) {
2962
+ try {
2963
+ const response = await Promise.race([
2964
+ fetch(`http://localhost:${port}/health`, {
2965
+ method: 'GET'
2966
+ }),
2967
+ new Promise((_, reject) =>
2968
+ setTimeout(() => reject(new Error('Timeout')), 1000)
2969
+ )
2970
+ ]);
2530
2971
 
2531
- throw new Error(licenseCheck.message || 'Recording not allowed');
2972
+ if (response.ok) {
2973
+ console.log(`[ServerDetection] Server detected on port ${port}`);
2974
+ serverMode = 'server';
2975
+ return 'server';
2976
+ }
2977
+ } catch (error) {
2978
+ // Continue checking other ports
2532
2979
  }
2980
+ }
2981
+
2982
+ console.log('[ServerDetection] No server detected - using browser-only mode');
2983
+ serverMode = 'browser-only';
2984
+ return 'browser-only';
2985
+ }
2986
+
2987
+ // Simple quota info utility (replaces over-engineered QuotaMonitor class)
2988
+ async function getQuotaInfo() {
2989
+ if (!navigator.storage?.estimate) {
2990
+ return { supported: false };
2991
+ }
2992
+
2993
+ const { usage = 0, quota = 0 } = await navigator.storage.estimate();
2994
+ const percentage = quota > 0 ? usage / quota : 0;
2995
+ const availableMB = Math.round((quota - usage) / (1024 * 1024));
2996
+
2997
+ return {
2998
+ supported: true,
2999
+ percentage,
3000
+ availableMB,
3001
+ usageMB: Math.round(usage / (1024 * 1024)),
3002
+ quotaMB: Math.round(quota / (1024 * 1024))
3003
+ };
3004
+ }
2533
3005
 
2534
- // Store userId for later usage tracking
2535
- const userId = licenseCheck.userId;
3006
+ async function startRecording(tabId, settings = {}) {
3007
+ try {
3008
+ // Detect server mode before starting
3009
+ await detectServerMode();
3010
+ console.log(`[Recording] Mode: ${serverMode}`);
2536
3011
 
2537
3012
  // Check if session manager is available
2538
3013
  if (!sessionManager) {
@@ -2602,6 +3077,18 @@ async function startRecording(tabId, settings = {}) {
2602
3077
  const sessionId = currentSession.sessionId;
2603
3078
  const scheduledStartTime = Date.now() + 3000; // Schedule start 3 seconds in future for countdown synchronization
2604
3079
 
3080
+ // STEP 4: Initialize browser-only recording if in browser-only mode
3081
+ if (serverMode === 'browser-only' && browserRecordingManager) {
3082
+ console.log('[Recording] Initializing browser-only recording in IndexedDB');
3083
+ const tab = await chrome.tabs.get(tabId);
3084
+ await browserRecordingManager.startRecording(sessionId, {
3085
+ tabId: tabId,
3086
+ url: tab.url,
3087
+ title: tab.title || settings.sessionName || 'Browser Recording',
3088
+ startTime: scheduledStartTime
3089
+ });
3090
+ }
3091
+
2605
3092
  // Update legacy state for backward compatibility (will be removed)
2606
3093
  recordingTabId = tabId;
2607
3094
  isCurrentlyRecording = true;
@@ -2679,6 +3166,17 @@ async function startRecording(tabId, settings = {}) {
2679
3166
  }
2680
3167
  });
2681
3168
 
3169
+ // STEP 4: Initialize browser recording if in browser-only mode
3170
+ if (serverMode === 'browser-only' && browserRecordingManager) {
3171
+ const tab = await chrome.tabs.get(tabId);
3172
+ await browserRecordingManager.startRecording(sessionId, {
3173
+ tabId,
3174
+ url: tab.url,
3175
+ title: tab.title
3176
+ });
3177
+ console.log('[Recording] Browser recording initialized for session:', sessionId);
3178
+ }
3179
+
2682
3180
  // Send stream ID, session info, and settings to offscreen document for frame capture
2683
3181
  await chrome.runtime.sendMessage({
2684
3182
  type: 'start-frame-capture',
@@ -2688,14 +3186,15 @@ async function startRecording(tabId, settings = {}) {
2688
3186
  sessionId: sessionId,
2689
3187
  ownerId: currentOwnerId,
2690
3188
  scheduledStartTime: scheduledStartTime,
3189
+ mode: serverMode, // Pass mode to offscreen document
2691
3190
  settings: {
2692
3191
  frameRate: settings.frameRate || 1,
2693
- imageQuality: settings.imageQuality || 30,
3192
+ imageQuality: settings.imageQuality || (serverMode === 'browser-only' ? 15 : 30),
2694
3193
  frameFlash: settings.frameFlash !== false
2695
3194
  }
2696
3195
  });
2697
-
2698
- console.log('Recording started');
3196
+
3197
+ console.log('Recording started in', serverMode, 'mode');
2699
3198
  } catch (error) {
2700
3199
  console.error('Error in startRecording:', error);
2701
3200
  throw error;
@@ -2708,16 +3207,23 @@ async function stopRecording() {
2708
3207
  // Check if session manager is available
2709
3208
  if (!sessionManager) {
2710
3209
  console.warn('Session manager not available, stopping recording anyway');
2711
- return;
3210
+ return { success: false, error: 'Session manager not available' };
2712
3211
  }
2713
3212
 
2714
3213
  // Check if currently recording via session manager
2715
3214
  const currentlyRecording = await isCurrentlyRecordingAsync();
2716
3215
  if (!currentlyRecording) {
2717
3216
  console.log('No active recording session to stop');
2718
- return;
3217
+ return { success: false, error: 'No active recording' };
2719
3218
  }
2720
3219
 
3220
+ // Store sessionId before clearing it
3221
+ const stoppedSessionId = currentSession?.sessionId;
3222
+
3223
+ // Capture frame count and duration for the response
3224
+ let recordedFrameCount = 0;
3225
+ let recordedDuration = 0;
3226
+
2721
3227
  // Stop recording with session manager
2722
3228
  if (currentSession?.sessionId && currentOwnerId) {
2723
3229
  const stopResult = await sessionManager.stopRecording(currentSession.sessionId, currentOwnerId);
@@ -2725,12 +3231,15 @@ async function stopRecording() {
2725
3231
  console.error('Failed to stop session:', stopResult.error?.message);
2726
3232
  // Continue with cleanup anyway
2727
3233
  } else {
2728
- console.log(`Recording stopped. Duration: ${stopResult.sessionDuration}ms, Frames: ${stopResult.frameCount}`);
2729
-
2730
- // STEP 2: Track usage AFTER successful recording completion
2731
- console.log('[License] Tracking usage after recording completion...');
2732
- const usageResult = await LicenseHelper.trackUsageAfterRecording();
2733
- console.log('[License] Usage tracking result:', usageResult);
3234
+ recordedFrameCount = stopResult.frameCount || 0;
3235
+ recordedDuration = stopResult.sessionDuration || 0;
3236
+ console.log(`Recording stopped. Duration: ${recordedDuration}ms, Frames: ${recordedFrameCount}`);
3237
+
3238
+ // Finalize browser-only recording if in browser-only mode
3239
+ if (serverMode === 'browser-only' && browserRecordingManager) {
3240
+ console.log('[Browser-Only] Finalizing browser recording in IndexedDB');
3241
+ await browserRecordingManager.stopRecording(currentSession.sessionId);
3242
+ }
2734
3243
  }
2735
3244
  }
2736
3245
 
@@ -2760,6 +3269,29 @@ async function stopRecording() {
2760
3269
  target: 'offscreen'
2761
3270
  });
2762
3271
 
3272
+ // CRITICAL: Store recordingTabId BEFORE clearing it (for cleanup)
3273
+ const previousRecordingTabId = recordingTabId;
3274
+ console.log('[Background] previousRecordingTabId:', previousRecordingTabId, 'stoppedSessionId:', stoppedSessionId);
3275
+
3276
+ // CRITICAL FIX: Send stop-screen-capture-tracking message to recording tab WITH sessionId
3277
+ // so it can show the completion UI
3278
+ if (previousRecordingTabId && stoppedSessionId) {
3279
+ try {
3280
+ console.log('[Background] Sending recording-complete-show-ui message to tab:', previousRecordingTabId, 'with sessionId:', stoppedSessionId);
3281
+ await chrome.tabs.sendMessage(previousRecordingTabId, {
3282
+ type: 'recording-complete-show-ui',
3283
+ sessionId: stoppedSessionId
3284
+ }).catch((err) => {
3285
+ console.log('[Background] Could not send completion UI message to recording tab:', err);
3286
+ });
3287
+ console.log('[Background] Successfully sent recording-complete-show-ui message');
3288
+ } catch (error) {
3289
+ console.error('[Background] Failed to send completion UI message:', error);
3290
+ }
3291
+ } else {
3292
+ console.log('[Background] NOT sending recording-complete-show-ui - missing previousRecordingTabId or stoppedSessionId');
3293
+ }
3294
+
2763
3295
  // CRITICAL FIX: Send stop-screen-capture-tracking message to ALL content scripts
2764
3296
  try {
2765
3297
  const tabs = await chrome.tabs.query({});
@@ -2780,12 +3312,22 @@ async function stopRecording() {
2780
3312
  console.error('[Background] Failed to send cleanup messages to content scripts:', error);
2781
3313
  }
2782
3314
 
3315
+ // CRITICAL FIX: Restore original console methods and remove event listeners
3316
+ if (previousRecordingTabId) {
3317
+ try {
3318
+ console.log('[Background] Stopping console log capture for tab:', previousRecordingTabId);
3319
+ await stopCapturingLogs(previousRecordingTabId);
3320
+ } catch (cleanupError) {
3321
+ console.error('[Background] Failed to cleanup console interceptor:', cleanupError);
3322
+ // Continue with stop process even if cleanup fails
3323
+ }
3324
+ }
3325
+
2783
3326
  // Clear session cache
2784
3327
  currentSession = null;
2785
3328
  currentOwnerId = null;
2786
3329
 
2787
3330
  // Update legacy state for backward compatibility (will be removed)
2788
- const previousRecordingTabId = recordingTabId; // Store for logBuffer cleanup
2789
3331
  isCurrentlyRecording = false;
2790
3332
  recordingTabId = null;
2791
3333
  currentRecordingSessionId = null;
@@ -2795,11 +3337,56 @@ async function stopRecording() {
2795
3337
  logBuffer.clearTab(previousRecordingTabId);
2796
3338
  }
2797
3339
 
3340
+ // CRITICAL: Clear pending frame queue to prevent infinite retry loop
3341
+ if (stoppedSessionId && pendingFrameQueue.has(stoppedSessionId)) {
3342
+ console.log(`[FrameQueue] Clearing ${pendingFrameQueue.get(stoppedSessionId).length} pending frames for stopped session ${stoppedSessionId}`);
3343
+ pendingFrameQueue.delete(stoppedSessionId);
3344
+ }
3345
+
2798
3346
  console.log('Stop message sent to offscreen document');
3347
+
3348
+ // Notify popup to refresh recordings list
3349
+ chrome.runtime.sendMessage({
3350
+ action: 'recordingStopped',
3351
+ sessionId: stoppedSessionId
3352
+ }).catch(() => {
3353
+ // Popup might not be open, that's okay
3354
+ console.log('[Background] Popup not available to notify of recording stop');
3355
+ });
3356
+
3357
+ // Format duration as human-readable string (e.g., "1m 30s" or "45s")
3358
+ const formatDurationForDisplay = (ms) => {
3359
+ const seconds = Math.floor(ms / 1000);
3360
+ if (seconds < 60) {
3361
+ return `${seconds}s`;
3362
+ }
3363
+ const minutes = Math.floor(seconds / 60);
3364
+ const remainingSeconds = seconds % 60;
3365
+ return remainingSeconds > 0 ? `${minutes}m ${remainingSeconds}s` : `${minutes}m`;
3366
+ };
3367
+
3368
+ // Return success with sessionId, frameCount and duration for popup display
3369
+ return {
3370
+ success: true,
3371
+ sessionId: stoppedSessionId,
3372
+ frameCount: recordedFrameCount,
3373
+ duration: formatDurationForDisplay(recordedDuration)
3374
+ };
2799
3375
  } catch (error) {
2800
3376
  console.error('Error in stopRecording:', error);
2801
3377
  // Clear session cache even on error
2802
3378
  const errorRecordingTabId = recordingTabId; // Store for cleanup
3379
+
3380
+ // CRITICAL FIX: Clean up console interceptor even on error
3381
+ if (errorRecordingTabId) {
3382
+ try {
3383
+ console.log('[Background] Emergency cleanup of console interceptor for tab:', errorRecordingTabId);
3384
+ await stopCapturingLogs(errorRecordingTabId);
3385
+ } catch (cleanupError) {
3386
+ console.error('[Background] Failed to cleanup console interceptor during error handling:', cleanupError);
3387
+ }
3388
+ }
3389
+
2803
3390
  currentSession = null;
2804
3391
  currentOwnerId = null;
2805
3392
  isCurrentlyRecording = false;
@@ -2818,204 +3405,16 @@ async function stopRecording() {
2818
3405
 
2819
3406
 
2820
3407
  // Capture console logs from the recording tab
3408
+ // REFACTORED: Now uses shared console-interception-library.js
2821
3409
  async function startCapturingLogs(tabId) {
2822
- // Check if this tab allows content script injection
2823
- const tab = await chrome.tabs.get(tabId);
2824
- if (!tab.url || tab.url.startsWith('chrome://') || tab.url.startsWith('chrome-extension://') || tab.url.startsWith('moz-extension://')) {
2825
- console.log('Cannot inject console logger into restricted URL:', tab.url);
2826
- console.warn('WARNING: Console logs cannot be captured on restricted pages (chrome://, chrome-extension://, etc.)');
2827
- console.warn('To capture console logs, please navigate to a regular web page before recording.');
2828
- return false;
2829
- }
2830
-
2831
- // First inject the main world console interceptor
2832
- try {
2833
- const results = await chrome.scripting.executeScript({
2834
- target: { tabId: tabId },
2835
- world: 'MAIN',
2836
- func: () => {
2837
- // Check if we've already overridden console methods
2838
- if (window.__chromePilotConsoleOverridden) {
2839
- console.log('[Chrome Debug] Console already overridden, skipping');
2840
- return 'already_installed';
2841
- }
2842
- window.__chromePilotConsoleOverridden = true;
2843
- console.log('[Chrome Debug] Installing console interceptor');
2844
-
2845
- // Override console methods to capture logs - complete coverage
2846
- const originalLog = console.log;
2847
- const originalError = console.error;
2848
- const originalWarn = console.warn;
2849
- const originalInfo = console.info;
2850
- const originalDebug = console.debug;
2851
- const originalTrace = console.trace;
2852
- const originalTable = console.table;
2853
- const originalDir = console.dir;
2854
- const originalGroup = console.group;
2855
- const originalGroupEnd = console.groupEnd;
2856
- const originalTime = console.time;
2857
- const originalTimeEnd = console.timeEnd;
2858
- const originalCount = console.count;
2859
-
2860
- const sendLog = (level, args) => {
2861
- try {
2862
- // Pre-serialize arguments to strings to avoid structured clone errors
2863
- const serializedArgs = args.map(arg => {
2864
- try {
2865
- if (arg === null) return 'null';
2866
- if (arg === undefined) return 'undefined';
2867
- if (typeof arg === 'function') return '[Function: ' + (arg.name || 'anonymous') + ']';
2868
- if (arg instanceof Element) return '[DOM Element: ' + arg.tagName + ']';
2869
- if (typeof arg === 'object') {
2870
- // Try to stringify, but limit depth to avoid circular references
2871
- let stringified = JSON.stringify(arg, null, 2);
2872
-
2873
- // Check if this looks like a base64 image and truncate it
2874
- if (stringified.includes('data:image/') && stringified.length > 1000) {
2875
- const match = stringified.match(/data:image\/([^;]+);base64,(.{0,100})/);
2876
- if (match) {
2877
- return `[Base64 Image: ${match[1]}, ${stringified.length} bytes total, truncated...]`;
2878
- }
2879
- }
2880
-
2881
- // Truncate any extremely large strings
2882
- const maxLength = 5000;
2883
- if (stringified.length > maxLength) {
2884
- return stringified.substring(0, maxLength) + `... [TRUNCATED: ${stringified.length} total bytes]`;
2885
- }
2886
-
2887
- return stringified;
2888
- }
2889
-
2890
- // Also check for base64 strings directly
2891
- const strValue = String(arg);
2892
- if (strValue.includes('data:image/') && strValue.length > 1000) {
2893
- const match = strValue.match(/data:image\/([^;]+);base64,(.{0,100})/);
2894
- if (match) {
2895
- return `[Base64 Image: ${match[1]}, ${strValue.length} bytes total, truncated...]`;
2896
- }
2897
- }
2898
-
2899
- // Truncate any extremely large strings
2900
- if (strValue.length > 5000) {
2901
- return strValue.substring(0, 5000) + `... [TRUNCATED: ${strValue.length} total bytes]`;
2902
- }
2903
-
2904
- return strValue;
2905
- } catch (e) {
2906
- return '[Object: could not serialize]';
2907
- }
2908
- });
2909
-
2910
- // Post message to content script
2911
- window.postMessage({
2912
- type: 'chrome-pilot-console-log',
2913
- log: {
2914
- level,
2915
- message: serializedArgs.join(' '),
2916
- timestamp: Date.now()
2917
- }
2918
- }, '*');
2919
- } catch (e) {
2920
- // Ignore errors when sending logs
2921
- }
2922
- };
2923
-
2924
- console.log = (...args) => {
2925
- sendLog('log', args);
2926
- originalLog.apply(console, args);
2927
- };
2928
-
2929
- console.error = (...args) => {
2930
- sendLog('error', args);
2931
- originalError.apply(console, args);
2932
- };
2933
-
2934
- console.warn = (...args) => {
2935
- sendLog('warn', args);
2936
- originalWarn.apply(console, args);
2937
- };
2938
-
2939
- console.info = (...args) => {
2940
- sendLog('info', args);
2941
- originalInfo.apply(console, args);
2942
- };
2943
-
2944
- console.debug = (...args) => {
2945
- sendLog('debug', args);
2946
- originalDebug.apply(console, args);
2947
- };
2948
-
2949
- console.trace = (...args) => {
2950
- sendLog('trace', args);
2951
- originalTrace.apply(console, args);
2952
- };
2953
-
2954
- console.table = (...args) => {
2955
- sendLog('table', args);
2956
- originalTable.apply(console, args);
2957
- };
2958
-
2959
- console.dir = (...args) => {
2960
- sendLog('dir', args);
2961
- originalDir.apply(console, args);
2962
- };
2963
-
2964
- console.group = (...args) => {
2965
- sendLog('group', args);
2966
- originalGroup.apply(console, args);
2967
- };
2968
-
2969
- console.groupEnd = (...args) => {
2970
- sendLog('groupEnd', args);
2971
- originalGroupEnd.apply(console, args);
2972
- };
2973
-
2974
- console.time = (...args) => {
2975
- sendLog('time', args);
2976
- originalTime.apply(console, args);
2977
- };
2978
-
2979
- console.timeEnd = (...args) => {
2980
- sendLog('timeEnd', args);
2981
- originalTimeEnd.apply(console, args);
2982
- };
2983
-
2984
- console.count = (...args) => {
2985
- sendLog('count', args);
2986
- originalCount.apply(console, args);
2987
- };
2988
-
2989
- return 'console_installed';
2990
- }
2991
- });
2992
-
2993
- console.log('[Console Injection] MAIN world script injected successfully:', results);
2994
-
2995
- // Then inject a content script to relay messages from main world to background
2996
- await chrome.scripting.executeScript({
2997
- target: { tabId: tabId },
2998
- func: () => {
2999
- // Listen for messages from main world
3000
- window.addEventListener('message', (event) => {
3001
- if (event.data && event.data.type === 'chrome-pilot-console-log') {
3002
- // Forward to background script
3003
- chrome.runtime.sendMessage({
3004
- action: 'consoleLog',
3005
- log: event.data.log
3006
- });
3007
- }
3008
- });
3009
- }
3010
- });
3410
+ console.log('[Screen Recording] Starting console interception using shared library');
3411
+ return await self.ConsoleInterceptionLibrary.startConsoleInterception(tabId, SCREEN_RECORDING_CONSOLE_CONFIG);
3412
+ }
3011
3413
 
3012
- console.log('[Console Injection] Content script relay injected successfully');
3013
- return true; // Successfully started capturing logs
3014
- } catch (error) {
3015
- console.error('[Console Injection] Failed to inject console interceptor:', error);
3016
- console.error('[Console Injection] Error details:', error.message, error.stack);
3017
- return false; // Failed to inject console interceptor
3018
- }
3414
+ // REFACTORED: Now uses shared console-interception-library.js via proper namespace
3415
+ async function stopCapturingLogs(tabId) {
3416
+ console.log('[Screen Recording] Stopping console interception using shared library');
3417
+ return await self.ConsoleInterceptionLibrary.stopConsoleInterception(tabId, SCREEN_RECORDING_CONSOLE_CONFIG);
3019
3418
  }
3020
3419
 
3021
3420
  // Workflow Recording Functions
@@ -3023,29 +3422,6 @@ async function startWorkflowRecording(tabId, includeLogsInExport, sessionName =
3023
3422
  try {
3024
3423
  console.log('Starting workflow recording for tab:', tabId);
3025
3424
 
3026
- // STEP 1: Check license and usage limits BEFORE recording (same pattern as startRecording)
3027
- console.log('[License] Checking license before workflow recording...');
3028
- const licenseCheck = await LicenseHelper.checkLicenseBeforeRecording();
3029
- console.log('[License] License check result:', licenseCheck);
3030
-
3031
- if (!licenseCheck.allowed) {
3032
- // Show notification to user (same pattern as screen recording)
3033
- chrome.notifications.create({
3034
- type: 'basic',
3035
- iconUrl: chrome.runtime.getURL('icon128.png'),
3036
- title: 'Recording Limit Reached',
3037
- message: licenseCheck.message || 'Daily limit reached. Upgrade to Pro for unlimited workflow recordings.',
3038
- buttons: [{ title: 'Upgrade to Pro' }],
3039
- priority: 2
3040
- });
3041
-
3042
- throw new Error(licenseCheck.message || 'Workflow recording not allowed');
3043
- }
3044
-
3045
- // Store userId for later usage tracking
3046
- const userId = licenseCheck.userId;
3047
- workflowUserIds.set(tabId, userId);
3048
-
3049
3425
  // Check if this tab allows content script injection
3050
3426
  const tab = await chrome.tabs.get(tabId);
3051
3427
  if (!tab.url || tab.url.startsWith('chrome://') || tab.url.startsWith('chrome-extension://') || tab.url.startsWith('moz-extension://')) {
@@ -3063,107 +3439,18 @@ async function startWorkflowRecording(tabId, includeLogsInExport, sessionName =
3063
3439
  const workflowId = `workflow_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
3064
3440
  await chrome.storage.local.set({ currentWorkflowId: workflowId });
3065
3441
 
3066
- // Clear any existing logs for this tab
3442
+ // Clear any existing logs for this tab and initialize race-safe buffer
3067
3443
  await chrome.storage.session.set({ [`workflow_${tabId}`]: [] });
3068
-
3069
- // Inject main world console interceptor (similar to frame recording)
3070
- await chrome.scripting.executeScript({
3071
- target: { tabId: tabId },
3072
- world: 'MAIN',
3073
- func: () => {
3074
- // Check if already injected
3075
- if (window.__chromePilotWorkflowConsoleOverridden) {
3076
- return;
3077
- }
3078
- window.__chromePilotWorkflowConsoleOverridden = true;
3079
-
3080
- // Override console methods to capture logs
3081
- const originalLog = console.log;
3082
- const originalError = console.error;
3083
- const originalWarn = console.warn;
3084
- const originalInfo = console.info;
3085
-
3086
- const sendLog = (level, args) => {
3087
- try {
3088
- // Pre-serialize arguments to strings to avoid structured clone errors
3089
- const serializedArgs = args.map(arg => {
3090
- try {
3091
- if (arg === null) return 'null';
3092
- if (arg === undefined) return 'undefined';
3093
- if (typeof arg === 'function') return '[Function: ' + (arg.name || 'anonymous') + ']';
3094
- if (arg instanceof Element) return '[DOM Element: ' + arg.tagName + ']';
3095
- if (typeof arg === 'object') {
3096
- // Try to stringify, but limit depth to avoid circular references
3097
- return JSON.stringify(arg, null, 2);
3098
- }
3099
- return String(arg);
3100
- } catch (e) {
3101
- return '[Object: could not serialize]';
3102
- }
3103
- });
3104
-
3105
- // Post message to content script
3106
- window.postMessage({
3107
- type: 'chrome-pilot-workflow-console-log',
3108
- log: {
3109
- level,
3110
- message: serializedArgs.join(' '),
3111
- timestamp: Date.now()
3112
- }
3113
- }, '*');
3114
- } catch (e) {
3115
- // Ignore errors when sending logs
3116
- }
3117
- };
3118
-
3119
- console.log = (...args) => {
3120
- sendLog('log', args);
3121
- originalLog.apply(console, args);
3122
- };
3123
-
3124
- console.error = (...args) => {
3125
- sendLog('error', args);
3126
- originalError.apply(console, args);
3127
- };
3128
-
3129
- console.warn = (...args) => {
3130
- sendLog('warn', args);
3131
- originalWarn.apply(console, args);
3132
- };
3133
-
3134
- console.info = (...args) => {
3135
- sendLog('info', args);
3136
- originalInfo.apply(console, args);
3137
- };
3138
-
3139
- // Also capture window errors
3140
- window.addEventListener('error', (event) => {
3141
- sendLog('exception', [`${event.message} at ${event.filename}:${event.lineno}:${event.colno}`]);
3142
- });
3143
-
3144
- window.addEventListener('unhandledrejection', (event) => {
3145
- sendLog('exception', [`Unhandled Promise Rejection: ${event.reason}`]);
3146
- });
3147
- }
3148
- });
3149
-
3150
- // Inject content script to relay messages and handle workflow recording
3151
- await chrome.scripting.executeScript({
3152
- target: { tabId: tabId },
3153
- func: (includeLogsInExport) => {
3154
- // Listen for console log messages from main world
3155
- window.addEventListener('message', (event) => {
3156
- if (event.data && event.data.type === 'chrome-pilot-workflow-console-log') {
3157
- // Forward to background script
3158
- chrome.runtime.sendMessage({
3159
- action: 'workflowConsoleLog',
3160
- log: event.data.log
3161
- });
3162
- }
3163
- });
3164
- },
3165
- args: [includeLogsInExport]
3166
- });
3444
+
3445
+ // Initialize WorkflowLogBuffer for race-safe log handling
3446
+ if (includeLogsInExport) {
3447
+ const buffer = new WorkflowLogBuffer(tabId);
3448
+ workflowLogBuffers.set(tabId, buffer);
3449
+ console.log(`[Workflow] Initialized WorkflowLogBuffer for tab ${tabId}`);
3450
+ }
3451
+
3452
+ // REFACTORED: Use shared console interception library via proper namespace (replaces 150+ lines of duplicate code)
3453
+ await self.ConsoleInterceptionLibrary.startConsoleInterception(tabId, WORKFLOW_RECORDING_CONSOLE_CONFIG);
3167
3454
 
3168
3455
  // Ensure content script is injected
3169
3456
  try {
@@ -3181,9 +3468,11 @@ async function startWorkflowRecording(tabId, includeLogsInExport, sessionName =
3181
3468
  }
3182
3469
 
3183
3470
  // Tell the content script to start recording
3471
+ // Include tabId so content script can use it when stopping recording
3184
3472
  await chrome.tabs.sendMessage(tabId, {
3185
3473
  action: 'startWorkflowRecording',
3186
- screenshotSettings: screenshotSettings
3474
+ screenshotSettings: screenshotSettings,
3475
+ tabId: tabId
3187
3476
  });
3188
3477
 
3189
3478
  console.log('Workflow recording started successfully');
@@ -3202,38 +3491,77 @@ async function stopWorkflowRecording(tabId) {
3202
3491
  throw new Error('No workflow recording active for this tab');
3203
3492
  }
3204
3493
 
3205
- // Get workflow from content script
3494
+ // Get workflow from content script using a different action to avoid circular dependency
3495
+ // (mini menu also sends 'stopWorkflowRecording' to background, creating confusion)
3206
3496
  const response = await chrome.tabs.sendMessage(tabId, {
3207
- action: 'stopWorkflowRecording'
3497
+ action: 'getWorkflowData'
3208
3498
  });
3209
-
3499
+
3210
3500
  if (!response || !response.success) {
3211
3501
  throw new Error('Failed to get workflow from content script');
3212
3502
  }
3213
-
3503
+
3504
+ // CRITICAL FIX: Restore original console methods and remove event listeners
3505
+ try {
3506
+ console.log('[Workflow] Stopping console log capture for tab:', tabId);
3507
+ await stopCapturingWorkflowLogs(tabId);
3508
+ } catch (cleanupError) {
3509
+ console.error('[Workflow] Failed to cleanup console interceptor:', cleanupError);
3510
+ // Continue with stop process even if cleanup fails
3511
+ }
3512
+
3214
3513
  // Handle both old format (array) and new format (object with actions and functionTraces)
3215
3514
  let workflowData = response.workflow || [];
3216
3515
  let workflow = Array.isArray(workflowData) ? workflowData : (workflowData.actions || []);
3217
3516
  let functionTraces = Array.isArray(workflowData) ? [] : (workflowData.functionTraces || []);
3218
-
3517
+
3219
3518
  console.log(`[Background] Received ${workflow.length} actions and ${functionTraces.length} function traces`);
3519
+ console.log('[SCREENSHOT-DEBUG] background.stopWorkflowRecording - Checking actions for screenshot_data');
3520
+
3521
+ const actionsWithScreenshots = workflow.filter(a => a.screenshot_data);
3522
+ console.log('[SCREENSHOT-DEBUG] background.stopWorkflowRecording - Actions WITH screenshot_data:', actionsWithScreenshots.length);
3523
+
3524
+ workflow.forEach((action, index) => {
3525
+ console.log(`[SCREENSHOT-DEBUG] background.stopWorkflowRecording - Action ${index}:`, {
3526
+ type: action.type,
3527
+ hasScreenshotData: !!action.screenshot_data,
3528
+ screenshotDataLength: action.screenshot_data?.length,
3529
+ screenshotPreview: action.screenshot_data?.substring(0, 50)
3530
+ });
3531
+ });
3220
3532
 
3221
- // If logs should be included, get them from session storage
3533
+ // If logs should be included, flush buffer and get them from session storage
3222
3534
  if (workflowIncludeLogs.get(tabId)) {
3535
+ // CRITICAL: Flush any buffered logs before retrieval to ensure all logs are captured
3536
+ const buffer = workflowLogBuffers.get(tabId);
3537
+ if (buffer) {
3538
+ await buffer.flush();
3539
+ console.log(`[Workflow] Flushed buffer before retrieval, stats:`, buffer.getStats());
3540
+ }
3541
+
3223
3542
  const result = await chrome.storage.session.get(`workflow_${tabId}`);
3224
3543
  const logs = result[`workflow_${tabId}`] || [];
3225
3544
 
3226
- // Associate logs with workflow actions
3545
+ // Associate logs with workflow actions using NON-OVERLAPPING windows
3546
+ // Each log appears in exactly ONE action to avoid duplicates
3227
3547
  workflow = workflow.map((action, index) => {
3228
- // Find logs that occurred around this action
3548
+ // Calculate non-overlapping time window:
3549
+ // - Each action owns logs from ITS timestamp until the NEXT action's timestamp
3550
+ // - First action also includes 500ms before it
3551
+ // - Last action includes 5000ms after it
3552
+ const windowStart = index === 0
3553
+ ? action.timestamp - 500 // First action: include 500ms before
3554
+ : action.timestamp; // Other actions: start at this action's timestamp
3555
+
3556
+ const windowEnd = index < workflow.length - 1
3557
+ ? workflow[index + 1].timestamp // Until next action's timestamp
3558
+ : action.timestamp + 5000; // Last action: 5000ms after
3559
+
3560
+ // Use >= for start and < for end to ensure non-overlapping windows
3229
3561
  const actionLogs = logs.filter(log => {
3230
- // Get time window
3231
- const prevActionTime = index > 0 ? workflow[index - 1].timestamp : action.timestamp - 500;
3232
- const nextActionTime = index < workflow.length - 1 ? workflow[index + 1].timestamp : action.timestamp + 5000;
3233
-
3234
- return log.timestamp > prevActionTime && log.timestamp < nextActionTime;
3562
+ return log.timestamp >= windowStart && log.timestamp < windowEnd;
3235
3563
  });
3236
-
3564
+
3237
3565
  if (actionLogs.length > 0) {
3238
3566
  return { ...action, logs: actionLogs };
3239
3567
  }
@@ -3245,59 +3573,87 @@ async function stopWorkflowRecording(tabId) {
3245
3573
  const sessionName = workflowSessionNames.get(tabId);
3246
3574
  const screenshotSettings = workflowScreenshotSettings.get(tabId);
3247
3575
  const includeLogs = workflowIncludeLogs.get(tabId) || false;
3248
- const userId = workflowUserIds.get(tabId); // Get userId for usage tracking
3249
3576
 
3250
3577
  // Clean up
3251
3578
  workflowRecordingTabs.delete(tabId);
3252
3579
  workflowIncludeLogs.delete(tabId);
3253
3580
  workflowScreenshotSettings.delete(tabId);
3254
3581
  workflowSessionNames.delete(tabId);
3255
- workflowUserIds.delete(tabId); // Clean up userId tracking
3582
+
3583
+ // Clean up WorkflowLogBuffer
3584
+ const bufferToCleanup = workflowLogBuffers.get(tabId);
3585
+ if (bufferToCleanup) {
3586
+ await bufferToCleanup.cleanup();
3587
+ workflowLogBuffers.delete(tabId);
3588
+ console.log(`[Workflow] Cleaned up WorkflowLogBuffer for tab ${tabId}`);
3589
+ }
3590
+
3256
3591
  await chrome.storage.session.remove(`workflow_${tabId}`);
3257
-
3592
+
3593
+ // Notify popup and other extension components that recording has stopped
3594
+ // This is critical for popup UI sync when stopping from mini-menu
3595
+ await chrome.storage.local.set({
3596
+ workflowRecording: false,
3597
+ workflowStartTime: null
3598
+ });
3599
+
3258
3600
  console.log('Workflow recording stopped, returning workflow:', workflow);
3259
3601
 
3260
3602
  // Try to save to server
3261
3603
  try {
3262
3604
  const serverPorts = CONFIG_PORTS.slice(0, 5); // Use first 5 configured ports for workflow recording
3263
3605
  let serverResult = null;
3264
-
3606
+
3265
3607
  // Get current URL and title
3266
3608
  const tab = await chrome.tabs.get(tabId);
3267
- const sessionId = `workflow_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
3609
+
3610
+ // Retrieve the workflow ID that was generated at start
3611
+ const storedData = await chrome.storage.local.get(['currentWorkflowId']);
3612
+ const sessionId = storedData.currentWorkflowId || `workflow_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
3613
+ console.log('[Workflow] Using sessionId for save:', sessionId);
3268
3614
 
3269
3615
  for (const port of serverPorts) {
3270
3616
  try {
3271
3617
  console.log(`[Workflow] Attempting to save to server on port ${port}...`);
3618
+
3619
+ const payloadData = {
3620
+ sessionId: sessionId,
3621
+ name: sessionName,
3622
+ url: tab.url,
3623
+ title: tab.title,
3624
+ includeLogs: includeLogs,
3625
+ screenshotSettings: screenshotSettings || {}, // v2.0.8 fix: ensure it's an object, not null
3626
+ actions: workflow,
3627
+ logs: includeLogs ? workflow.flatMap(action => action.logs || []) : [],
3628
+ functionTraces: functionTraces // Include function execution traces
3629
+ };
3630
+
3631
+ console.log('[SCREENSHOT-DEBUG] background.stopWorkflowRecording - HTTP POST payload being prepared');
3632
+ console.log('[SCREENSHOT-DEBUG] background.stopWorkflowRecording - Payload actions count:', payloadData.actions.length);
3633
+
3634
+ const payloadActionsWithScreenshots = payloadData.actions.filter(a => a.screenshot_data);
3635
+ console.log('[SCREENSHOT-DEBUG] background.stopWorkflowRecording - Payload actions WITH screenshot_data:', payloadActionsWithScreenshots.length);
3636
+
3637
+ payloadData.actions.forEach((action, index) => {
3638
+ console.log(`[SCREENSHOT-DEBUG] background.stopWorkflowRecording - Payload Action ${index}:`, {
3639
+ type: action.type,
3640
+ hasScreenshotData: !!action.screenshot_data,
3641
+ screenshotDataLength: action.screenshot_data?.length,
3642
+ screenshotPreview: action.screenshot_data?.substring(0, 50)
3643
+ });
3644
+ });
3645
+
3272
3646
  const response = await fetch(`http://localhost:${port}/chromedebug/workflow-recording`, {
3273
3647
  method: 'POST',
3274
3648
  headers: {
3275
3649
  'Content-Type': 'application/json',
3276
3650
  },
3277
- body: JSON.stringify({
3278
- sessionId: sessionId,
3279
- name: sessionName,
3280
- url: tab.url,
3281
- title: tab.title,
3282
- includeLogs: includeLogs,
3283
- screenshotSettings: screenshotSettings || {}, // v2.0.8 fix: ensure it's an object, not null
3284
- actions: workflow,
3285
- logs: includeLogs ? workflow.flatMap(action => action.logs || []) : [],
3286
- functionTraces: functionTraces // Include function execution traces
3287
- })
3651
+ body: JSON.stringify(payloadData)
3288
3652
  });
3289
3653
 
3290
3654
  if (response.ok) {
3291
3655
  serverResult = await response.json();
3292
3656
  console.log(`[Workflow] Successfully saved to server on port ${port}`, serverResult);
3293
-
3294
- // STEP 2: Track usage AFTER successful workflow recording completion (same pattern as stopRecording)
3295
- if (userId) {
3296
- console.log('[License] Tracking usage after workflow recording completion...');
3297
- const usageResult = await LicenseHelper.trackUsageAfterRecording(userId);
3298
- console.log('[License] Usage tracking result:', usageResult);
3299
- }
3300
-
3301
3657
  break;
3302
3658
  } else {
3303
3659
  console.log(`[Workflow] Failed on port ${port}: ${response.status} ${response.statusText}`);
@@ -3310,10 +3666,13 @@ async function stopWorkflowRecording(tabId) {
3310
3666
  }
3311
3667
 
3312
3668
  if (serverResult) {
3669
+ // Clean up currentWorkflowId after successful save
3670
+ await chrome.storage.local.remove(['currentWorkflowId']);
3671
+
3313
3672
  return {
3314
3673
  success: true,
3315
3674
  workflow: {
3316
- sessionId: sessionId,
3675
+ sessionId: serverResult.workflowId || serverResult.sessionId || sessionId,
3317
3676
  url: tab.url,
3318
3677
  title: tab.title,
3319
3678
  actions: workflow,
@@ -3323,17 +3682,36 @@ async function stopWorkflowRecording(tabId) {
3323
3682
  serverResult: serverResult
3324
3683
  };
3325
3684
  } else {
3685
+ // Server not available - clean up but return local sessionId
3686
+ await chrome.storage.local.remove(['currentWorkflowId']);
3687
+
3326
3688
  return {
3327
3689
  success: true,
3328
- workflow: workflow,
3690
+ workflow: {
3691
+ sessionId: sessionId,
3692
+ url: tab.url,
3693
+ title: tab.title,
3694
+ actions: workflow,
3695
+ logs: []
3696
+ },
3329
3697
  savedToServer: false
3330
3698
  };
3331
3699
  }
3332
3700
  } catch (error) {
3333
3701
  console.error('Error saving workflow:', error);
3702
+ // Clean up on error too
3703
+ await chrome.storage.local.remove(['currentWorkflowId']);
3704
+
3705
+ // Return structured response even on error
3334
3706
  return {
3335
3707
  success: true,
3336
- workflow: workflow,
3708
+ workflow: {
3709
+ sessionId: sessionId,
3710
+ url: tab?.url || 'unknown',
3711
+ title: tab?.title || 'Untitled',
3712
+ actions: workflow,
3713
+ logs: []
3714
+ },
3337
3715
  savedToServer: false,
3338
3716
  error: error.message
3339
3717
  };
@@ -3344,6 +3722,13 @@ async function stopWorkflowRecording(tabId) {
3344
3722
  }
3345
3723
  }
3346
3724
 
3725
+ // CRITICAL FIX: Stop workflow console log capture and restore original methods
3726
+ // REFACTORED: Now uses shared console-interception-library.js via proper namespace
3727
+ async function stopCapturingWorkflowLogs(tabId) {
3728
+ console.log('[Workflow Recording] Stopping console interception using shared library');
3729
+ return await self.ConsoleInterceptionLibrary.stopConsoleInterception(tabId, WORKFLOW_RECORDING_CONSOLE_CONFIG);
3730
+ }
3731
+
3347
3732
  // Delete recording from server
3348
3733
  async function deleteRecordingFromServer(recordingId, sendResponse) {
3349
3734
  try {
@@ -3403,13 +3788,28 @@ async function retryPendingFrames(sessionId) {
3403
3788
 
3404
3789
  console.log(`[FrameQueue] Retrying ${pending.length} pending frame batches for session ${sessionId}`);
3405
3790
 
3791
+ // Check if this is still the active recording session
3792
+ if (currentSession?.sessionId !== sessionId) {
3793
+ console.log(`[FrameQueue] Session ${sessionId} is no longer active, clearing pending frames`);
3794
+ pendingFrameQueue.delete(sessionId);
3795
+ return;
3796
+ }
3797
+
3406
3798
  // Validate session now
3407
3799
  if (sessionManager) {
3408
3800
  const validationResult = await sessionManager.isSessionValid(sessionId);
3409
3801
  if (!validationResult.valid) {
3410
3802
  console.warn(`[FrameQueue] Session still invalid during retry: ${validationResult.reason}`);
3411
- // Try again after another delay
3412
- setTimeout(() => retryPendingFrames(sessionId), 200);
3803
+ // Try again after another delay (max 3 retries)
3804
+ const retryCount = pending[0]?.retryCount || 0;
3805
+ if (retryCount < 3) {
3806
+ // Mark all batches with retry count
3807
+ pending.forEach(batch => batch.retryCount = retryCount + 1);
3808
+ setTimeout(() => retryPendingFrames(sessionId), 200);
3809
+ } else {
3810
+ console.error(`[FrameQueue] Max retries (3) exceeded for session ${sessionId}, clearing queue`);
3811
+ pendingFrameQueue.delete(sessionId);
3812
+ }
3413
3813
  return;
3414
3814
  }
3415
3815
  }
@@ -3432,8 +3832,9 @@ async function handleFrameBatch(batchData, skipValidation = false) {
3432
3832
  try {
3433
3833
  const sessionId = batchData.sessionId;
3434
3834
 
3435
- // Validate session if session manager is available (unless skipping for retry)
3436
- if (sessionManager && !skipValidation) {
3835
+ // Skip session validation entirely in browser-only mode
3836
+ // Validate session if session manager is available (unless skipping for retry or in browser-only mode)
3837
+ if (sessionManager && !skipValidation && serverMode !== 'browser-only') {
3437
3838
  const validationResult = await sessionManager.isSessionValid(sessionId);
3438
3839
  if (!validationResult.valid) {
3439
3840
  // DON'T LOSE FRAMES - Queue for retry instead of rejecting
@@ -3537,18 +3938,43 @@ async function handleFrameBatch(batchData, skipValidation = false) {
3537
3938
  }
3538
3939
 
3539
3940
  if (!uploadSuccess) {
3540
- console.error(`CRITICAL: Failed to upload frame batch to any Chrome Debug server port. Last error: ${lastError}`);
3541
- console.error(`Session ${batchData.sessionId} frames are only stored in Chrome extension storage and not accessible to Chrome Debug.`);
3542
-
3543
- // Store error in chrome storage for user visibility
3544
- await chrome.storage.local.set({
3545
- [`upload_error_${batchData.sessionId}`]: {
3546
- error: lastError,
3547
- timestamp: Date.now(),
3548
- sessionId: batchData.sessionId,
3549
- frameCount: indexedFrames.length
3941
+ console.log(`No server available - checking if browser-only mode`);
3942
+
3943
+ // If in browser-only mode, store frames in IndexedDB
3944
+ if (serverMode === 'browser-only' && browserRecordingManager) {
3945
+ console.log(`[Browser-Only] Storing ${indexedFrames.length} frames in IndexedDB`);
3946
+ // Use the browser recording session ID, not the frame capture session ID
3947
+ const recordingSessionId = currentRecordingSessionId || batchData.sessionId;
3948
+ for (const frame of indexedFrames) {
3949
+ // Validate that critical timestamp data exists
3950
+ if (!frame.absoluteTimestamp) {
3951
+ console.warn(`[Browser-Only] WARNING: Frame ${frame.index} missing absoluteTimestamp - log association will fail`);
3952
+ }
3953
+
3954
+ await browserRecordingManager.addFrame(recordingSessionId, {
3955
+ frameIndex: frame.index,
3956
+ screenshot: frame.imageData || frame.screenshot, // Use imageData from frame capture
3957
+ logs: frame.logs || [],
3958
+ timestamp: frame.timestamp || Date.now(),
3959
+ absoluteTimestamp: frame.absoluteTimestamp // Don't use Date.now() fallback - preserve actual capture time
3960
+ });
3550
3961
  }
3551
- });
3962
+ console.log(`[Browser-Only] Frames stored successfully in IndexedDB for session ${recordingSessionId}`);
3963
+ } else {
3964
+ // Server mode but no server available - show error
3965
+ console.error(`CRITICAL: Failed to upload frame batch to any Chrome Debug server port. Last error: ${lastError}`);
3966
+ console.error(`Session ${batchData.sessionId} frames are only stored in Chrome extension storage and not accessible to Chrome Debug.`);
3967
+
3968
+ // Store error in chrome storage for user visibility
3969
+ await chrome.storage.local.set({
3970
+ [`upload_error_${batchData.sessionId}`]: {
3971
+ error: lastError,
3972
+ timestamp: Date.now(),
3973
+ sessionId: batchData.sessionId,
3974
+ frameCount: indexedFrames.length
3975
+ }
3976
+ });
3977
+ }
3552
3978
  }
3553
3979
 
3554
3980
  // Notify LogStreamer that frames have been captured and processed
@@ -3671,7 +4097,9 @@ async function handleFrameCaptureComplete(sessionData) {
3671
4097
  await chrome.storage.session.remove(String(tabIdToNotify));
3672
4098
 
3673
4099
  // Send logs to server for association with frames with retry logic
3674
- if (bufferedLogs.length > 0) {
4100
+ // Skip server communication in browser-only mode (logs already in IndexedDB)
4101
+ if (bufferedLogs.length > 0 && serverMode !== 'browser-only') {
4102
+ console.log(`[Server Mode] Associating ${bufferedLogs.length} logs with server...`);
3675
4103
 
3676
4104
  // Retry logic to handle race conditions
3677
4105
  const maxRetries = 3;
@@ -3849,6 +4277,62 @@ async function handleFrameCaptureComplete(sessionData) {
3849
4277
  priority: 1
3850
4278
  });
3851
4279
  }
4280
+ } else if (bufferedLogs.length > 0 && serverMode === 'browser-only') {
4281
+ // Browser-only mode: Associate logs with frames in IndexedDB
4282
+ console.log(`[Browser-Only] Associating ${bufferedLogs.length} logs with frames in IndexedDB...`);
4283
+
4284
+ try {
4285
+ // Get all frames for this session from IndexedDB
4286
+ const frames = await dataBuffer.getBrowserFrames(sessionIdToUse);
4287
+ console.log(`[Browser-Only] Retrieved ${frames.length} frames from IndexedDB for log association`);
4288
+
4289
+ if (frames.length === 0) {
4290
+ console.warn('[Browser-Only] No frames found - logs cannot be associated');
4291
+ } else {
4292
+ // Associate each log with the appropriate frame based on timestamp
4293
+ // Frames have absoluteTimestamp, logs have timestamp
4294
+
4295
+ for (const log of bufferedLogs) {
4296
+ // Find the frame that this log belongs to (closest frame before the log timestamp)
4297
+ let targetFrame = null;
4298
+ for (let i = 0; i < frames.length; i++) {
4299
+ const frame = frames[i];
4300
+ const frameTime = frame.absoluteTimestamp || frame.timestamp;
4301
+ if (frameTime <= log.timestamp) {
4302
+ targetFrame = frame;
4303
+ } else {
4304
+ break; // Frames are in chronological order
4305
+ }
4306
+ }
4307
+
4308
+ // If no frame found before this log, associate with first frame
4309
+ if (!targetFrame && frames.length > 0) {
4310
+ targetFrame = frames[0];
4311
+ }
4312
+
4313
+ if (targetFrame) {
4314
+ if (!targetFrame.logs) {
4315
+ targetFrame.logs = [];
4316
+ }
4317
+ targetFrame.logs.push(log);
4318
+ }
4319
+ }
4320
+
4321
+ // Update frames in IndexedDB with associated logs
4322
+ let updatedCount = 0;
4323
+ for (const frame of frames) {
4324
+ if (frame.logs && frame.logs.length > 0) {
4325
+ // Update this frame in IndexedDB
4326
+ await dataBuffer.updateBrowserFrame(frame.id, { logs: frame.logs });
4327
+ updatedCount++;
4328
+ }
4329
+ }
4330
+
4331
+ console.log(`[Browser-Only] ✓ Associated logs with ${updatedCount} frames in IndexedDB`);
4332
+ }
4333
+ } catch (error) {
4334
+ console.error('[Browser-Only] Failed to associate logs with frames:', error);
4335
+ }
3852
4336
  } else {
3853
4337
  // No logs were captured during the session
3854
4338
  console.warn('[Recording] No console logs were captured during this session');
@@ -3866,10 +4350,11 @@ async function handleFrameCaptureComplete(sessionData) {
3866
4350
  priority: 0
3867
4351
  });
3868
4352
  }
3869
-
4353
+
3870
4354
  // Save screen interactions to database
3871
- if (screenInteractions.length > 0) {
3872
- console.log('Saving', screenInteractions.length, 'screen interactions to database');
4355
+ // Skip server communication in browser-only mode
4356
+ if (screenInteractions.length > 0 && serverMode !== 'browser-only') {
4357
+ console.log(`[Server Mode] Saving ${screenInteractions.length} screen interactions to database`);
3873
4358
 
3874
4359
  // Use comprehensive error handling for screen interactions save
3875
4360
  const saveScreenInteractions = async (data, context) => {
@@ -3935,8 +4420,11 @@ async function handleFrameCaptureComplete(sessionData) {
3935
4420
  } else {
3936
4421
  console.error('[ErrorHandling] Failed to save screen interactions after comprehensive error handling');
3937
4422
  }
4423
+ } else if (screenInteractions.length > 0 && serverMode === 'browser-only') {
4424
+ // Browser-only mode: screen interactions not needed
4425
+ console.log(`[Browser-Only] ✓ Skipping screen interactions save (browser-only mode)`);
3938
4426
  }
3939
-
4427
+
3940
4428
  // Stop session via session manager if available
3941
4429
  if (sessionManager && currentSession?.sessionId === sessionData.sessionId && currentOwnerId) {
3942
4430
  try {
@@ -3983,15 +4471,19 @@ async function handleFrameCaptureComplete(sessionData) {
3983
4471
  priority: 2
3984
4472
  });
3985
4473
 
3986
- // Notify popup if open
3987
- chrome.runtime.sendMessage({
3988
- action: 'frameSessionComplete',
3989
- sessionId: sessionData.sessionId,
3990
- sessionName: currentSession?.settings?.sessionName || null,
3991
- totalFrames: sessionData.totalFrames,
3992
- duration: sessionData.duration,
3993
- serverPort: recordingServerPort
3994
- }).catch(() => {});
4474
+ // Notify popup if open (but NOT for browser-only recordings - they're in IndexedDB)
4475
+ if (serverMode !== 'browser-only') {
4476
+ chrome.runtime.sendMessage({
4477
+ action: 'frameSessionComplete',
4478
+ sessionId: sessionData.sessionId,
4479
+ sessionName: currentSession?.settings?.sessionName || null,
4480
+ totalFrames: sessionData.totalFrames,
4481
+ duration: sessionData.duration,
4482
+ serverPort: recordingServerPort
4483
+ }).catch(() => {});
4484
+ } else {
4485
+ console.log('[Browser-Only] Skipping frameSessionComplete message - recording is in IndexedDB');
4486
+ }
3995
4487
 
3996
4488
  // Notify content script to hide recording indicator
3997
4489
  if (tabIdToNotify) {