@capillarytech/cap-ui-dev-tools 1.0.0 → 1.3.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.
@@ -0,0 +1,755 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ /**
5
+ * Report Enhancer Configuration
6
+ * @typedef {Object} ReportEnhancerConfig
7
+ * @property {boolean} [ENABLE_FEATURE] - Master feature flag - if false, replay enhancement is completely disabled (default: true)
8
+ * @property {string} [recordingsDir] - Directory where recordings are stored
9
+ * @property {string} [reportsDir] - Directory where HTML reports are located
10
+ * @property {string} [playerCSSPath] - Path to CapVision player CSS file
11
+ * @property {string} [playerScriptPath] - Path to CapVision player script file
12
+ * @property {string} [consoleReplayPluginPath] - Path to console replay plugin
13
+ * @property {string} [networkReplayPluginPath] - Path to network replay plugin
14
+ * @property {number} [playerWidth] - Player width in pixels
15
+ * @property {number} [playerHeight] - Player height in pixels
16
+ * @property {boolean} [autoPlay] - Auto-play recordings on load
17
+ * @property {string[]} [enabledClusters] - Only enhance for these clusters (empty = all)
18
+ * @property {string[]} [enabledModules] - Only enhance for these modules (empty = all)
19
+ */
20
+
21
+ /**
22
+ * Recording Data Interface
23
+ * @typedef {Object} RecordingData
24
+ * @property {string} filename - Recording filename
25
+ * @property {Object} data - Recording data object
26
+ * @property {string} data.testName - Test name
27
+ * @property {number} data.recordingNumber - Recording number/sequence
28
+ * @property {string} data.timestamp - ISO timestamp
29
+ * @property {number} data.totalEvents - Total events in recording
30
+ * @property {Array} data.events - Array of CapVision events
31
+ */
32
+
33
+ /**
34
+ * Default Configuration
35
+ * @type {ReportEnhancerConfig}
36
+ */
37
+ const DEFAULT_ENHANCER_CONFIG = {
38
+ ENABLE_FEATURE: true,
39
+ recordingsDir: path.join(process.cwd(), 'reports', 'recordings'),
40
+ reportsDir: path.join(process.cwd(), 'reports', 'html-reports'),
41
+ playerCSSPath: path.join(__dirname, '../assets/capvision-player.css'),
42
+ playerScriptPath: path.join(__dirname, '../assets/capvision-player.min.js'),
43
+ consoleReplayPluginPath: path.join(__dirname, '../assets/capvision-plugins/console-replay.min.js'),
44
+ networkReplayPluginPath: path.join(__dirname, '../assets/capvision-plugins/network-replay.min.js'),
45
+ playerWidth: 800,
46
+ playerHeight: 460,
47
+ autoPlay: false,
48
+ enabledClusters: [],
49
+ enabledModules: []
50
+ };
51
+
52
+ /**
53
+ * Report Enhancer Class
54
+ * Enhances HTML reports with CapVision player functionality
55
+ * @class
56
+ */
57
+ class ReportEnhancer {
58
+ /**
59
+ * Create a new Report Enhancer instance
60
+ * @param {ReportEnhancerConfig} [config={}] - Enhancer configuration
61
+ */
62
+ constructor(config = {}) {
63
+ /** @type {ReportEnhancerConfig} */
64
+ this.config = { ...DEFAULT_ENHANCER_CONFIG, ...config };
65
+ }
66
+
67
+ /**
68
+ * Update configuration
69
+ * @param {Partial<ReportEnhancerConfig>} config - Configuration updates
70
+ */
71
+ updateConfig(config) {
72
+ this.config = { ...this.config, ...config };
73
+ }
74
+
75
+ /**
76
+ * Get current configuration
77
+ * @returns {ReportEnhancerConfig} Current configuration object
78
+ */
79
+ getConfig() {
80
+ return { ...this.config };
81
+ }
82
+
83
+ /**
84
+ * Check if enhancement should be enabled for current cluster
85
+ * @private
86
+ * @param {string} [cluster] - Override cluster (defaults to process.env.cluster)
87
+ * @returns {boolean} True if enabled for cluster
88
+ */
89
+ isEnabledForCluster(cluster) {
90
+ if (this.config.enabledClusters.length === 0) {
91
+ return true;
92
+ }
93
+
94
+ const currentCluster = cluster || process.env.cluster;
95
+ return currentCluster ? this.config.enabledClusters.includes(currentCluster) : false;
96
+ }
97
+
98
+ /**
99
+ * Check if enhancement should be enabled for current module
100
+ * @private
101
+ * @param {string} [module] - Override module (defaults to process.env.module)
102
+ * @returns {boolean} True if enabled for module
103
+ */
104
+ isEnabledForModule(module) {
105
+ if (this.config.enabledModules.length === 0) {
106
+ return true;
107
+ }
108
+
109
+ const currentModule = module || process.env.module;
110
+ return currentModule ? this.config.enabledModules.includes(currentModule) : false;
111
+ }
112
+
113
+ /**
114
+ * Check if enhancement is enabled
115
+ * @param {string} [cluster] - Override cluster check
116
+ * @param {string} [module] - Override module check
117
+ * @returns {boolean} True if enhancement enabled
118
+ */
119
+ isEnhancementEnabled(cluster, module) {
120
+ return this.isEnabledForCluster(cluster) && this.isEnabledForModule(module);
121
+ }
122
+
123
+ /**
124
+ * Get available recordings
125
+ * @private
126
+ * @returns {RecordingData[]} Array of recording data objects
127
+ */
128
+ getAvailableRecordings() {
129
+ try {
130
+ if (!fs.existsSync(this.config.recordingsDir)) {
131
+ return [];
132
+ }
133
+
134
+ const files = fs.readdirSync(this.config.recordingsDir);
135
+ const recordingFiles = files.filter(file => file.endsWith('.json'));
136
+
137
+ const recordings = [];
138
+ for (const file of recordingFiles) {
139
+ try {
140
+ const filePath = path.join(this.config.recordingsDir, file);
141
+ const data = JSON.parse(fs.readFileSync(filePath, 'utf8'));
142
+ recordings.push({
143
+ filename: file,
144
+ data: data
145
+ });
146
+ } catch (error) {
147
+ console.error(`🔴 Failed to read recording file ${file}:`, error.message);
148
+ }
149
+ }
150
+
151
+ return recordings;
152
+ } catch (error) {
153
+ console.error('🔴 Failed to get recordings:', error.message);
154
+ return [];
155
+ }
156
+ }
157
+
158
+ /**
159
+ * Generate logo HTML header with toggle button
160
+ * @private
161
+ * @returns {string} Logo HTML
162
+ */
163
+ generateLogoHTML() {
164
+ // Inline SVG logo - CapVision branded logo (icon size)
165
+ const logoSVG = `
166
+ <svg width="120" height="80" viewBox="0 0 1600 800" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="display: inline-block; vertical-align: middle;">
167
+ <defs>
168
+ <!-- Neon gradient used for strokes/fills -->
169
+ <linearGradient id="neonGradient" x1="0" x2="1" y1="0" y2="1">
170
+ <stop offset="0%" stop-color="#00D4FF"/>
171
+ <stop offset="25%" stop-color="#3A7CFF"/>
172
+ <stop offset="50%" stop-color="#9A4BFF"/>
173
+ <stop offset="70%" stop-color="#FF4CA3"/>
174
+ <stop offset="85%" stop-color="#FF8E3C"/>
175
+ <stop offset="100%" stop-color="#FFD800"/>
176
+ </linearGradient>
177
+
178
+ <!-- Wordmark gradient (cool blue) -->
179
+ <linearGradient id="wordGradient" x1="0" x2="1">
180
+ <stop offset="0%" stop-color="#19C6FF"/>
181
+ <stop offset="100%" stop-color="#2EA6FF"/>
182
+ </linearGradient>
183
+
184
+ <!-- Soft inner shadow for icon fill -->
185
+ <linearGradient id="iconFill" x1="0" x2="1">
186
+ <stop offset="0%" stop-color="#061022" stop-opacity="0.0"/>
187
+ <stop offset="100%" stop-color="#061022" stop-opacity="0.08"/>
188
+ </linearGradient>
189
+
190
+ <!-- Stroke style for neon lines -->
191
+ <style type="text/css"><![CDATA[
192
+ .neon { stroke: url(#neonGradient); stroke-width:14; stroke-linecap:square; stroke-linejoin:miter; fill: none; }
193
+ .neon-thin { stroke: url(#neonGradient); stroke-width:8; stroke-linecap:square; stroke-linejoin:miter; fill: none; }
194
+ .eye-fill { fill: none; stroke: url(#neonGradient); stroke-width:12; stroke-linecap:square; stroke-linejoin:miter; }
195
+ .circuit { stroke: url(#neonGradient); stroke-width:10; stroke-linecap:square; fill: none; }
196
+ .accent-dot { fill: url(#neonGradient); }
197
+ .word { font-family: "Inter", "Montserrat", sans-serif; font-weight: 700; font-size:120px; fill: url(#wordGradient); letter-spacing: -2px; }
198
+ .bg-texture { opacity: 0.02; }
199
+ ]]></style>
200
+ </defs>
201
+
202
+ <!-- Dark background (keeps high contrast for presentation and dashboards) -->
203
+ <rect id="bg" x="0" y="0" width="1600" height="800" fill="#060719" />
204
+
205
+ <!-- subtle grid/texture (very faint) -->
206
+ <g class="bg-texture" transform="translate(0,0)">
207
+ <rect x="0" y="0" width="1600" height="800" fill="url(#iconFill)" />
208
+ </g>
209
+
210
+ <!-- LEFT ICON GROUP -->
211
+ <g id="icon" transform="translate(160,210) scale(1.4)">
212
+ <!-- robot-head-ish rounded rectangle -->
213
+ <rect x="0" y="0" rx="8" ry="8" width="220" height="150" class="neon" />
214
+
215
+ <!-- antenna -->
216
+ <line x1="110" y1="-30" x2="110" y2="0" class="neon-thin" />
217
+ <circle cx="110" cy="-36" r="12" class="accent-dot" />
218
+
219
+ <!-- small scan lines / UI fragments on head (to suggest vision/processing) -->
220
+ <path d="M170,30 h40" class="neon-thin" stroke-linecap="square" />
221
+ <path d="M162,54 h30" class="neon-thin" stroke-linecap="square" />
222
+ <path d="M28,28 h-12" class="neon-thin" />
223
+ <rect x="20" y="20" width="6" height="28" rx="1" class="neon-thin" />
224
+
225
+ <!-- Abstract eye -->
226
+ <g transform="translate(10,50)">
227
+ <!-- eye outline -->
228
+ <path d="M40,60 C20,60 0,44 0,30 C0,16 20,0 40,0 C60,0 80,16 80,30 C80,44 60,60 40,60 Z" class="eye-fill"/>
229
+ <!-- iris concentric rings -->
230
+ <circle cx="40" cy="30" r="16" stroke="url(#neonGradient)" stroke-width="8" fill="none" />
231
+ <circle cx="40" cy="30" r="8" stroke="url(#neonGradient)" stroke-width="6" fill="none" />
232
+ <circle cx="40" cy="30" r="3" fill="#04102A"/>
233
+
234
+ <!-- subtle scan ticks inside eye -->
235
+ <path d="M20,30 h6" class="neon-thin" />
236
+ <path d="M66,30 h6" class="neon-thin" />
237
+ <path d="M40,8 v6" class="neon-thin" />
238
+ <path d="M40,52 v6" class="neon-thin" />
239
+ </g>
240
+
241
+ <!-- Circuit-like legs below eye (AI & automation) -->
242
+ <g transform="translate(15,125)">
243
+ <!-- crossing trace lines -->
244
+ <path d="M20,0 L75,60" class="circuit" />
245
+ <path d="M110,0 L58,60" class="circuit" />
246
+
247
+ <!-- branch nodes -->
248
+ <circle cx="20" cy="0" r="7" class="accent-dot" />
249
+ <circle cx="110" cy="0" r="7" class="accent-dot" />
250
+
251
+ <circle cx="75" cy="60" r="8" class="accent-dot" />
252
+ <circle cx="58" cy="60" r="8" class="accent-dot" />
253
+
254
+ <!-- side traces -->
255
+ <path d="M10,18 L35,38" class="circuit" />
256
+ <path d="M120,18 L95,38" class="circuit" />
257
+
258
+ <circle cx="8" cy="18" r="6" class="accent-dot" />
259
+ <circle cx="122" cy="18" r="6" class="accent-dot" />
260
+ </g>
261
+ </g>
262
+
263
+ <!-- WORDMARK (Right side) -->
264
+ <g id="wordmark" transform="translate(530,360)">
265
+ <!-- Main text -->
266
+ <text class="word" x="0" y="30">CapVision</text>
267
+
268
+ <!-- subtle tech underline / focus ring hint under 'Vision' -->
269
+ <path d="M210,52 h160" stroke="url(#wordGradient)" stroke-width="6" stroke-linecap="square" opacity="0.35" />
270
+ <circle cx="360" cy="28" r="10" fill="none" stroke="url(#wordGradient)" stroke-width="4" opacity="0.45" />
271
+ </g>
272
+
273
+ <!-- Accessibility: invisible title/desc -->
274
+ <title>CapVision — cyberpunk AI vision logo</title>
275
+ <desc>Futuristic neon logo composed of an abstract robotic-eye icon and circuit traces left, and a bold CapVision wordmark right. Gradient neon colors and clean strokes create a cyberpunk high-tech look.</desc>
276
+ </svg>
277
+ `.trim();
278
+
279
+ return `
280
+ <tr class="recording-logo-header">
281
+ <td colspan="2">
282
+ <div style="padding: 20px 15px; background: #fff; border-bottom: 2px solid #e9ecef; display: flex; justify-content: space-between; align-items: center;">
283
+ <span id="capvision-toggle-wrapper"
284
+ onclick="toggleCapVisionRecordings()"
285
+ style="cursor: pointer; padding: 10px; user-select: none; background-color: #C8E6C9; border-radius: 3px; display: inline-block; flex: 1; max-width: calc(100% - 150px); margin-right: 20px;">
286
+ <span id="capvision-toggle-icon" class="collapsed" style="font-size: 12px; display: inline-block;"></span>
287
+ </span>
288
+ <div style="text-align: right;">
289
+ ${logoSVG}
290
+ </div>
291
+ </div>
292
+ </td>
293
+ </tr>`;
294
+ }
295
+
296
+ /**
297
+ * Generate wrapper HTML for recordings (table structure)
298
+ * @private
299
+ * @param {string} logoHTML - Logo HTML
300
+ * @param {string} recordingPlayersHTML - Recording players HTML
301
+ * @returns {string} Wrapped HTML
302
+ */
303
+ generateRecordingsWrapper(logoHTML, recordingPlayersHTML) {
304
+ return `
305
+ <table class="table table-bordered" style="margin-top: 20px;">
306
+ <thead>
307
+ ${logoHTML}
308
+ </thead>
309
+ <tbody id="capvision-recordings-container" class="collapsed" style="display: table-row-group;">
310
+ ${recordingPlayersHTML}
311
+ </tbody>
312
+ </table>
313
+ <style>
314
+ #capvision-recordings-container {
315
+ transition: opacity 0.3s ease-in-out;
316
+ }
317
+ #capvision-recordings-container.collapsed {
318
+ display: none !important;
319
+ }
320
+ #capvision-toggle-wrapper {
321
+ transition: background-color 0.2s ease;
322
+ }
323
+ #capvision-toggle-wrapper:hover {
324
+ background-color: #C8E6C9;
325
+ }
326
+ #capvision-toggle-wrapper:hover #capvision-toggle-icon {
327
+ color: #333;
328
+ }
329
+ #capvision-toggle-icon {
330
+ display: inline-block;
331
+ }
332
+ #capvision-toggle-icon::before {
333
+ font-family: "Glyphicons Halflings", "Font Awesome", Arial, sans-serif;
334
+ color: #333;
335
+ }
336
+ #capvision-toggle-icon.collapsed::before {
337
+ content: "\\e113";
338
+ }
339
+ #capvision-toggle-icon:not(.collapsed)::before {
340
+ content: "\\e114";
341
+ }
342
+ </style>
343
+ <script>
344
+ function toggleCapVisionRecordings() {
345
+ const container = document.getElementById('capvision-recordings-container');
346
+ const toggleIcon = document.getElementById('capvision-toggle-icon');
347
+
348
+ if (container.classList.contains('collapsed')) {
349
+ container.classList.remove('collapsed');
350
+ toggleIcon.classList.remove('collapsed');
351
+ } else {
352
+ container.classList.add('collapsed');
353
+ toggleIcon.classList.add('collapsed');
354
+ }
355
+ }
356
+ </script>`;
357
+ }
358
+
359
+ /**
360
+ * Add inline recording players to HTML content
361
+ * @private
362
+ * @param {string} htmlContent - Original HTML content
363
+ * @param {RecordingData[]} recordings - Array of recordings to add
364
+ * @returns {string} Modified HTML content with players
365
+ */
366
+ addInlineRecordingPlayers(htmlContent, recordings) {
367
+ if (recordings.length === 0) {
368
+ return htmlContent;
369
+ }
370
+
371
+ // Find table with class "table table-bordered table-suite"
372
+ const tableSuiteRegex = /<table[^>]*class=["'][^"']*table[^"']*table-bordered[^"']*table-suite[^"']*["'][^>]*>/i;
373
+ const match = htmlContent.match(tableSuiteRegex);
374
+
375
+ if (!match) {
376
+ // Table not found, return original content
377
+ return htmlContent;
378
+ }
379
+
380
+ const tableStartIndex = match.index;
381
+ const tableStartTag = match[0];
382
+
383
+ // Find the matching closing </table> tag for this table
384
+ // We need to account for nested tables
385
+ let depth = 1;
386
+ let searchIndex = tableStartIndex + tableStartTag.length;
387
+ let closingTableIndex = -1;
388
+
389
+ while (searchIndex < htmlContent.length && depth > 0) {
390
+ const nextOpen = htmlContent.indexOf('<table', searchIndex);
391
+ const nextClose = htmlContent.indexOf('</table>', searchIndex);
392
+
393
+ if (nextClose === -1) {
394
+ break; // No closing tag found
395
+ }
396
+
397
+ if (nextOpen !== -1 && nextOpen < nextClose) {
398
+ depth++;
399
+ searchIndex = nextOpen + 6;
400
+ } else {
401
+ depth--;
402
+ if (depth === 0) {
403
+ closingTableIndex = nextClose;
404
+ break;
405
+ }
406
+ searchIndex = nextClose + 8;
407
+ }
408
+ }
409
+
410
+ if (closingTableIndex === -1) {
411
+ // If we can't find matching closing tag, return original content
412
+ return htmlContent;
413
+ }
414
+
415
+ // Generate logo and players HTML
416
+ const logoHTML = this.generateLogoHTML();
417
+ const recordingPlayersHTML = recordings.map((recording, index) => {
418
+ const testName = recording.data.testName || 'Unknown Test';
419
+ const recordingNumber = recording.data.recordingNumber || (index + 1);
420
+ const displayName = recording.data.testName
421
+ ? `${testName} (Recording #${recordingNumber})`
422
+ : recording.filename;
423
+
424
+ return `
425
+ <tr class="test-row recording">
426
+ <td colspan="2">
427
+ <div class="recordingWrapper" style="padding: 15px; background: #f8f9fa; margin: 10px 0;">
428
+ <h4 style="margin: 0 0 10px 0; color: #007bff; font-size: 16px;">
429
+ 🎬 Session Recording: ${displayName}
430
+ </h4>
431
+ <div class="recording-info" style="margin-bottom: 10px; font-size: 12px; color: #666;">
432
+ Events: ${recording.data.totalEvents} | Timestamp: ${recording.data.timestamp} | File: ${recording.filename}
433
+ </div>
434
+ <div id="capvision-player-inline-${index}" class="capvision-player-inline" style="width: 100%; max-width: ${this.config.playerWidth + 200}px; height: ${this.config.playerHeight + 100}px; border: 1px solid #ddd; border-radius: 8px; background: #fff;">
435
+ <div style="display: flex; align-items: center; justify-content: center; height: 100%; color: #666;">
436
+ Loading recording player...
437
+ </div>
438
+ </div>
439
+ </div>
440
+ </td>
441
+ </tr>`;
442
+ }).join('');
443
+
444
+ // Wrap logo and players in a new table and inject after the closing </table> tag
445
+ const recordingsWrapper = this.generateRecordingsWrapper(logoHTML, recordingPlayersHTML);
446
+ const insertPosition = closingTableIndex + 8; // 8 = length of '</table>'
447
+
448
+ return htmlContent.substring(0, insertPosition) +
449
+ recordingsWrapper +
450
+ htmlContent.substring(insertPosition);
451
+ }
452
+
453
+ /**
454
+ * Generate CapVision player HTML and JavaScript
455
+ * @private
456
+ * @param {RecordingData[]} recordings - Array of recordings to embed
457
+ * @returns {string} HTML/JavaScript string to inject
458
+ */
459
+ generateCapVisionPlayerHTML(recordings) {
460
+ // Read bundled assets
461
+ let playerCSS = '';
462
+ try {
463
+ playerCSS = fs.readFileSync(this.config.playerCSSPath, 'utf8');
464
+ } catch (error) {
465
+ console.warn('⚠️ Failed to load CapVision player CSS:', error.message);
466
+ }
467
+
468
+ let playerScript = '';
469
+ try {
470
+ playerScript = fs.readFileSync(this.config.playerScriptPath, 'utf8');
471
+ } catch (error) {
472
+ console.warn('⚠️ Failed to load CapVision player script:', error.message);
473
+ }
474
+
475
+ let consoleReplayPlugin = '';
476
+ try {
477
+ consoleReplayPlugin = fs.readFileSync(this.config.consoleReplayPluginPath, 'utf8');
478
+ } catch (error) {
479
+ console.warn('⚠️ Failed to load console replay plugin:', error.message);
480
+ }
481
+
482
+ // Read bundled network replay plugin
483
+ let networkReplayPlugin = '';
484
+ try {
485
+ networkReplayPlugin = fs.readFileSync(this.config.networkReplayPluginPath, 'utf8');
486
+ } catch (error) {
487
+ console.warn('⚠️ Failed to load network replay plugin:', error.message);
488
+ }
489
+
490
+ return `
491
+ <!-- CapVision Player CSS (Bundled) -->
492
+ <style>
493
+ ${playerCSS}
494
+ </style>
495
+
496
+ <!-- CapVision Player Script (Bundled) -->
497
+ <script>
498
+ ${playerScript}
499
+ </script>
500
+
501
+ <!-- Console Replay Plugin (Bundled) -->
502
+ <script>
503
+ ${consoleReplayPlugin}
504
+ </script>
505
+
506
+ <!-- Network Replay Plugin (Bundled) -->
507
+ <script>
508
+ ${networkReplayPlugin}
509
+ </script>
510
+
511
+ <script>
512
+ // CapVision Player Integration Script for Inline Players
513
+ (function() {
514
+ const recordings = ${JSON.stringify(recordings)};
515
+ let players = [];
516
+
517
+ // Initialize all inline players
518
+ function initializeInlinePlayers() {
519
+ recordings.forEach((recording, index) => {
520
+ const playerContainer = document.getElementById(\`capvision-player-inline-\${index}\`);
521
+ if (playerContainer) {
522
+ try {
523
+ console.log('🟡 Initializing inline player for recording:', recording.filename);
524
+
525
+ // Clear loading message
526
+ playerContainer.innerHTML = '';
527
+
528
+ // Build plugins array for replay
529
+ const plugins = [];
530
+
531
+ // Add console replay plugin if available
532
+ if (window.rrwebPluginConsoleReplay) {
533
+ try {
534
+ const replayOptions = { level: ['log', 'info', 'warn', 'error'] };
535
+ let consolePlugin;
536
+
537
+ // Handle different export patterns
538
+ if (typeof window.rrwebPluginConsoleReplay === 'function') {
539
+ consolePlugin = window.rrwebPluginConsoleReplay(replayOptions);
540
+ } else if (window.rrwebPluginConsoleReplay.getReplayConsolePlugin) {
541
+ consolePlugin = window.rrwebPluginConsoleReplay.getReplayConsolePlugin(replayOptions);
542
+ } else if (window.rrwebPluginConsoleReplay.default) {
543
+ consolePlugin = window.rrwebPluginConsoleReplay.default(replayOptions);
544
+ }
545
+
546
+ if (consolePlugin) {
547
+ plugins.push(consolePlugin);
548
+ }
549
+ } catch (error) {
550
+ console.warn('⚠️ Failed to initialize console replay plugin:', error.message);
551
+ }
552
+ }
553
+
554
+ // Add network replay plugin if available
555
+ if (window.rrwebPluginNetworkReplay) {
556
+ try {
557
+ console.log('🟡 Initializing network replay plugin...');
558
+ const networkReplayOptions = {
559
+ showPanel: true,
560
+ maxEntries: 100,
561
+ position: 'bottom-right'
562
+ };
563
+ let networkPlugin;
564
+
565
+ // Handle different export patterns
566
+ if (typeof window.rrwebPluginNetworkReplay === 'function') {
567
+ networkPlugin = window.rrwebPluginNetworkReplay(networkReplayOptions);
568
+ console.log('🟢 Network replay plugin initialized (function pattern)');
569
+ } else if (window.rrwebPluginNetworkReplay.getReplayNetworkPlugin) {
570
+ networkPlugin = window.rrwebPluginNetworkReplay.getReplayNetworkPlugin(networkReplayOptions);
571
+ console.log('🟢 Network replay plugin initialized (getReplayNetworkPlugin pattern)');
572
+ } else if (window.rrwebPluginNetworkReplay.default) {
573
+ networkPlugin = window.rrwebPluginNetworkReplay.default(networkReplayOptions);
574
+ console.log('🟢 Network replay plugin initialized (default pattern)');
575
+ }
576
+
577
+ if (networkPlugin) {
578
+ console.log('🟡 Network plugin structure:', {
579
+ hasHandler: typeof networkPlugin.handler === 'function',
580
+ hasOnBuild: typeof networkPlugin.onBuild === 'function',
581
+ keys: Object.keys(networkPlugin)
582
+ });
583
+ plugins.push(networkPlugin);
584
+ console.log('🟢 Network replay plugin added to plugins array');
585
+ } else {
586
+ console.warn('⚠️ Network replay plugin is null or undefined');
587
+ }
588
+ } catch (error) {
589
+ console.error('🔴 Failed to initialize network replay plugin:', error);
590
+ console.error('🔴 Error stack:', error.stack);
591
+ }
592
+ } else {
593
+ console.warn('⚠️ window.rrwebPluginNetworkReplay not found');
594
+ }
595
+
596
+ // Create RRWeb player
597
+ const player = new rrwebPlayer({
598
+ target: playerContainer,
599
+ props: {
600
+ events: recording.data.events,
601
+ width: ${this.config.playerWidth},
602
+ height: ${this.config.playerHeight},
603
+ autoPlay: ${this.config.autoPlay},
604
+ speed: 1,
605
+ showController: true,
606
+ mouseTail: true,
607
+ plugins: plugins.length > 0 ? plugins : undefined,
608
+ insertStyleRules: [
609
+ '.replayer-wrapper { border-radius: 8px; width: 100%; height: 100%; }',
610
+ '.replayer-mouse { z-index: 1000; }',
611
+ '.replayer-controller { background: rgba(0,0,0,0.8); border-radius: 4px; }',
612
+ '.replayer-controller button { color: white; }',
613
+ '.replayer-wrapper { background: white; }'
614
+ ]
615
+ }
616
+ });
617
+
618
+ players[index] = player;
619
+
620
+ console.log('🟢 Successfully initialized inline player for recording:', recording.filename);
621
+
622
+ } catch (error) {
623
+ console.error('🔴 Failed to initialize inline player for recording:', recording.filename, error);
624
+ playerContainer.innerHTML = \`
625
+ <div style="display: flex; align-items: center; justify-content: center; height: 100%; color: #dc3545; font-size: 16px; flex-direction: column;">
626
+ <div>❌ Failed to load recording</div>
627
+ <div style="font-size: 12px; margin-top: 5px;">\${error.message}</div>
628
+ </div>
629
+ \`;
630
+ }
631
+ }
632
+ });
633
+ }
634
+
635
+ // Initialize players when DOM is ready
636
+ if (document.readyState === 'loading') {
637
+ document.addEventListener('DOMContentLoaded', initializeInlinePlayers);
638
+ } else {
639
+ initializeInlinePlayers();
640
+ }
641
+
642
+ // Cleanup function
643
+ window.addEventListener('beforeunload', function() {
644
+ players.forEach(player => {
645
+ if (player && player.destroy) {
646
+ player.destroy();
647
+ }
648
+ });
649
+ });
650
+
651
+ console.log('🟢 CapVision Player integration loaded with', recordings.length, 'recording(s)');
652
+
653
+ })();
654
+ </script>
655
+ `;
656
+ }
657
+
658
+ /**
659
+ * Enhance a single HTML report with CapVision player
660
+ * @async
661
+ * @param {string} reportPath - Path to HTML report file
662
+ * @returns {Promise<void>}
663
+ */
664
+ async enhanceReport(reportPath) {
665
+ try {
666
+ // Check master feature flag first
667
+ if (this.config.ENABLE_FEATURE === false) {
668
+ console.log('🟡 CapVision feature disabled (ENABLE_FEATURE=false), skipping report enhancement');
669
+ return;
670
+ }
671
+
672
+ console.log('🟡 Enhancing HTML report with CapVision player:', reportPath);
673
+
674
+ if (!fs.existsSync(reportPath)) {
675
+ console.log('🟡 HTML report not found, skipping enhancement');
676
+ return;
677
+ }
678
+
679
+ const recordings = this.getAvailableRecordings();
680
+ if (recordings.length === 0) {
681
+ console.log('🟡 No CapVision recordings found, skipping enhancement');
682
+ return;
683
+ }
684
+
685
+ let htmlContent = fs.readFileSync(reportPath, 'utf8');
686
+
687
+ // Add inline players
688
+ htmlContent = this.addInlineRecordingPlayers(htmlContent, recordings);
689
+
690
+ // Add player scripts and CSS
691
+ const capVisionPlayerHTML = this.generateCapVisionPlayerHTML(recordings);
692
+ const bodyEndIndex = htmlContent.lastIndexOf('</body>');
693
+
694
+ if (bodyEndIndex === -1) {
695
+ console.log('🟡 Could not find </body> tag, appending to end');
696
+ htmlContent += capVisionPlayerHTML;
697
+ } else {
698
+ htmlContent = htmlContent.slice(0, bodyEndIndex) + capVisionPlayerHTML + htmlContent.slice(bodyEndIndex);
699
+ }
700
+
701
+ fs.writeFileSync(reportPath, htmlContent);
702
+
703
+ console.log('🟢 Successfully enhanced HTML report with CapVision player');
704
+ console.log(`🟢 Found ${recordings.length} recording(s) to integrate`);
705
+
706
+ } catch (error) {
707
+ console.error('🔴 Failed to enhance HTML report:', error.message);
708
+ }
709
+ }
710
+
711
+ /**
712
+ * Enhance all HTML reports in the reports directory
713
+ * @async
714
+ * @returns {Promise<void>}
715
+ */
716
+ async enhanceAllReports() {
717
+ try {
718
+ // Check master feature flag first
719
+ if (this.config.ENABLE_FEATURE === false) {
720
+ console.log('🟡 CapVision feature disabled (ENABLE_FEATURE=false), skipping report enhancement');
721
+ return;
722
+ }
723
+
724
+ if (!this.isEnhancementEnabled()) {
725
+ console.log('🟡 Skipping report enhancement (disabled for current cluster/module)');
726
+ return;
727
+ }
728
+
729
+ if (!fs.existsSync(this.config.reportsDir)) {
730
+ console.log('🟡 Reports directory not found');
731
+ return;
732
+ }
733
+
734
+ const files = fs.readdirSync(this.config.reportsDir);
735
+ const htmlFiles = files.filter(file => file.endsWith('.html'));
736
+
737
+ for (const htmlFile of htmlFiles) {
738
+ const reportPath = path.join(this.config.reportsDir, htmlFile);
739
+ await this.enhanceReport(reportPath);
740
+ }
741
+
742
+ console.log('🟢 Enhanced all HTML reports with CapVision player');
743
+
744
+ } catch (error) {
745
+ console.error('🔴 Failed to enhance reports:', error.message);
746
+ }
747
+ }
748
+ }
749
+
750
+ // Export class and default config
751
+ module.exports = {
752
+ ReportEnhancer,
753
+ DEFAULT_ENHANCER_CONFIG
754
+ };
755
+