capdebug 0.1.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,2786 @@
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
5
+ var core = require('@capacitor/core');
6
+
7
+ const DEFAULT_SENSITIVE_KEYS = new Set([
8
+ 'authorization',
9
+ 'cookie',
10
+ 'set-cookie',
11
+ 'password',
12
+ 'passwd',
13
+ 'pass',
14
+ 'token',
15
+ 'access_token',
16
+ 'accesstoken',
17
+ 'refresh_token',
18
+ 'refreshtoken',
19
+ 'api_key',
20
+ 'apikey',
21
+ 'secret',
22
+ 'client_secret',
23
+ 'clientsecret',
24
+ 'bearer',
25
+ 'private_key',
26
+ 'privatekey',
27
+ 'session_id',
28
+ 'sessionid',
29
+ 'jwt',
30
+ 'credit_card',
31
+ 'creditcard',
32
+ 'cvv',
33
+ 'ssn',
34
+ ]);
35
+ const MASK_VALUE = '********';
36
+ class DataSanitizer {
37
+ maskKeys;
38
+ maxDepth;
39
+ maxArrayLength;
40
+ constructor(options = {}) {
41
+ this.maskKeys = new Set([
42
+ ...DEFAULT_SENSITIVE_KEYS,
43
+ ...(options.customMaskKeys || []).map(k => k.toLowerCase().trim()),
44
+ ]);
45
+ this.maxDepth = options.maxDepth ?? 10;
46
+ this.maxArrayLength = options.maxArrayLength ?? 100;
47
+ }
48
+ isSensitiveKey(key) {
49
+ const normalized = key.toLowerCase().replace(/[-_]/g, '');
50
+ if (this.maskKeys.has(key.toLowerCase()) || this.maskKeys.has(normalized)) {
51
+ return true;
52
+ }
53
+ for (const sensitive of this.maskKeys) {
54
+ const cleanSensitive = sensitive.replace(/[-_]/g, '');
55
+ if (normalized.includes(cleanSensitive) && cleanSensitive.length >= 4) {
56
+ return true;
57
+ }
58
+ }
59
+ return false;
60
+ }
61
+ sanitize(value) {
62
+ const seen = new WeakSet();
63
+ return this.sanitizeRecursive(value, 0, seen);
64
+ }
65
+ sanitizeRecursive(value, depth, seen) {
66
+ if (value === null || value === undefined) {
67
+ return value;
68
+ }
69
+ if (depth > this.maxDepth) {
70
+ return '[Max Depth Reached]';
71
+ }
72
+ const type = typeof value;
73
+ if (type === 'string' || type === 'number' || type === 'boolean') {
74
+ return value;
75
+ }
76
+ if (type === 'bigint') {
77
+ return value.toString() + 'n';
78
+ }
79
+ if (type === 'symbol') {
80
+ return value.toString();
81
+ }
82
+ if (type === 'function') {
83
+ return `[Function: ${value.name || 'anonymous'}]`;
84
+ }
85
+ if (typeof value === 'object') {
86
+ // Check circular reference
87
+ if (seen.has(value)) {
88
+ return '[Circular Reference]';
89
+ }
90
+ seen.add(value);
91
+ // Error handling
92
+ if (value instanceof Error) {
93
+ return {
94
+ name: value.name,
95
+ message: value.message,
96
+ stack: value.stack,
97
+ ...value,
98
+ };
99
+ }
100
+ // Date handling
101
+ if (value instanceof Date) {
102
+ return value.toISOString();
103
+ }
104
+ // RegExp handling
105
+ if (value instanceof RegExp) {
106
+ return value.toString();
107
+ }
108
+ // DOM Node handling
109
+ if (typeof Element !== 'undefined' && value instanceof Element) {
110
+ const id = value.id ? `#${value.id}` : '';
111
+ const className = value.className ? `.${String(value.className).trim().replace(/\s+/g, '.')}` : '';
112
+ return `<${value.tagName.toLowerCase()}${id}${className}>`;
113
+ }
114
+ // Array handling
115
+ if (Array.isArray(value)) {
116
+ const arr = value.slice(0, this.maxArrayLength).map(item => this.sanitizeRecursive(item, depth + 1, seen));
117
+ if (value.length > this.maxArrayLength) {
118
+ arr.push(`[... ${value.length - this.maxArrayLength} more items]`);
119
+ }
120
+ return arr;
121
+ }
122
+ // Map handling
123
+ if (value instanceof Map) {
124
+ const entries = {};
125
+ for (const [k, v] of value.entries()) {
126
+ const keyStr = String(k);
127
+ entries[keyStr] = this.isSensitiveKey(keyStr)
128
+ ? MASK_VALUE
129
+ : this.sanitizeRecursive(v, depth + 1, seen);
130
+ }
131
+ return entries;
132
+ }
133
+ // Set handling
134
+ if (value instanceof Set) {
135
+ return Array.from(value).map(item => this.sanitizeRecursive(item, depth + 1, seen));
136
+ }
137
+ // Plain Object handling
138
+ const result = {};
139
+ const obj = value;
140
+ for (const [key, val] of Object.entries(obj)) {
141
+ if (this.isSensitiveKey(key)) {
142
+ result[key] = MASK_VALUE;
143
+ }
144
+ else {
145
+ result[key] = this.sanitizeRecursive(val, depth + 1, seen);
146
+ }
147
+ }
148
+ return result;
149
+ }
150
+ return String(value);
151
+ }
152
+ }
153
+ const defaultSanitizer = new DataSanitizer();
154
+ function sanitize(value, options) {
155
+ if (!options) {
156
+ return defaultSanitizer.sanitize(value);
157
+ }
158
+ return new DataSanitizer(options).sanitize(value);
159
+ }
160
+
161
+ class CapDebugStore {
162
+ buffer = [];
163
+ maxEvents;
164
+ paused = false;
165
+ listeners = new Set();
166
+ rafId = null;
167
+ customMaskKeys = [];
168
+ filterState = {
169
+ tab: 'all',
170
+ search: '',
171
+ level: 'all',
172
+ source: 'all',
173
+ };
174
+ constructor(maxEvents = 500, customMaskKeys = []) {
175
+ this.maxEvents = Math.max(1, maxEvents);
176
+ this.customMaskKeys = customMaskKeys;
177
+ }
178
+ setMaxEvents(max) {
179
+ this.maxEvents = Math.max(1, max);
180
+ if (this.buffer.length > this.maxEvents) {
181
+ this.buffer = this.buffer.slice(this.buffer.length - this.maxEvents);
182
+ this.notify();
183
+ }
184
+ }
185
+ setCustomMaskKeys(keys) {
186
+ this.customMaskKeys = keys;
187
+ }
188
+ add(event) {
189
+ if (this.paused) {
190
+ return null;
191
+ }
192
+ const sanitizedData = event.data !== undefined
193
+ ? sanitize(event.data, { customMaskKeys: this.customMaskKeys })
194
+ : undefined;
195
+ const fullEvent = {
196
+ id: event.id || this.generateId(),
197
+ timestamp: event.timestamp || Date.now(),
198
+ source: event.source || 'web',
199
+ type: event.type,
200
+ level: event.level || 'info',
201
+ message: event.message,
202
+ data: sanitizedData,
203
+ duration: event.duration,
204
+ platform: event.platform,
205
+ };
206
+ if (this.buffer.length >= this.maxEvents) {
207
+ this.buffer.shift(); // Evict oldest FIFO
208
+ }
209
+ this.buffer.push(fullEvent);
210
+ this.notify();
211
+ return fullEvent;
212
+ }
213
+ clear() {
214
+ this.buffer = [];
215
+ this.notify();
216
+ }
217
+ pause() {
218
+ this.paused = true;
219
+ this.notify();
220
+ }
221
+ resume() {
222
+ this.paused = false;
223
+ this.notify();
224
+ }
225
+ togglePause() {
226
+ this.paused = !this.paused;
227
+ this.notify();
228
+ return this.paused;
229
+ }
230
+ isPaused() {
231
+ return this.paused;
232
+ }
233
+ setFilter(partial) {
234
+ this.filterState = { ...this.filterState, ...partial };
235
+ this.notify();
236
+ }
237
+ getFilter() {
238
+ return this.filterState;
239
+ }
240
+ getAll() {
241
+ return [...this.buffer];
242
+ }
243
+ getCounts() {
244
+ const counts = {
245
+ all: this.buffer.length,
246
+ console: 0,
247
+ errors: 0,
248
+ events: 0,
249
+ native: 0,
250
+ device: 0,
251
+ network: 0,
252
+ bridge: 0,
253
+ };
254
+ for (const ev of this.buffer) {
255
+ if (ev.source === 'console')
256
+ counts.console++;
257
+ if (ev.level === 'error')
258
+ counts.errors++;
259
+ if (ev.source === 'web' || ev.source === 'system')
260
+ counts.events++;
261
+ if (ev.source === 'native')
262
+ counts.native++;
263
+ if (ev.source === 'network')
264
+ counts.network++;
265
+ if (ev.source === 'bridge')
266
+ counts.bridge++;
267
+ }
268
+ return counts;
269
+ }
270
+ getFiltered() {
271
+ const { tab, search, level, source } = this.filterState;
272
+ const query = search.trim().toLowerCase();
273
+ return this.buffer.filter(event => {
274
+ // 1. Tab filtering
275
+ if (tab === 'console' && event.source !== 'console')
276
+ return false;
277
+ if (tab === 'errors' && event.level !== 'error')
278
+ return false;
279
+ if (tab === 'events' && (event.source !== 'web' && event.source !== 'system'))
280
+ return false;
281
+ if (tab === 'native' && event.source !== 'native')
282
+ return false;
283
+ if (tab === 'network' && event.source !== 'network')
284
+ return false;
285
+ if (tab === 'bridge' && event.source !== 'bridge')
286
+ return false;
287
+ // 2. Level filter
288
+ if (level !== 'all' && event.level !== level)
289
+ return false;
290
+ // 3. Source filter
291
+ if (source !== 'all' && event.source !== source)
292
+ return false;
293
+ // 4. Search query
294
+ if (query) {
295
+ const typeMatch = event.type.toLowerCase().includes(query);
296
+ const msgMatch = event.message ? event.message.toLowerCase().includes(query) : false;
297
+ const sourceMatch = event.source.toLowerCase().includes(query);
298
+ if (typeMatch || msgMatch || sourceMatch)
299
+ return true;
300
+ if (event.data) {
301
+ try {
302
+ const dataStr = typeof event.data === 'string'
303
+ ? event.data.toLowerCase()
304
+ : JSON.stringify(event.data).toLowerCase();
305
+ return dataStr.includes(query);
306
+ }
307
+ catch {
308
+ return false;
309
+ }
310
+ }
311
+ return false;
312
+ }
313
+ return true;
314
+ });
315
+ }
316
+ exportJson() {
317
+ return JSON.stringify({
318
+ exportedAt: new Date().toISOString(),
319
+ totalEvents: this.buffer.length,
320
+ events: this.buffer,
321
+ }, null, 2);
322
+ }
323
+ subscribe(listener) {
324
+ this.listeners.add(listener);
325
+ listener(this.getFiltered(), this);
326
+ return () => {
327
+ this.listeners.delete(listener);
328
+ };
329
+ }
330
+ notify() {
331
+ if (typeof window !== 'undefined' && typeof window.requestAnimationFrame === 'function') {
332
+ if (this.rafId !== null) {
333
+ cancelAnimationFrame(this.rafId);
334
+ }
335
+ this.rafId = requestAnimationFrame(() => {
336
+ this.rafId = null;
337
+ this.dispatchToListeners();
338
+ });
339
+ }
340
+ else {
341
+ this.dispatchToListeners();
342
+ }
343
+ }
344
+ dispatchToListeners() {
345
+ const filtered = this.getFiltered();
346
+ for (const listener of this.listeners) {
347
+ try {
348
+ listener(filtered, this);
349
+ }
350
+ catch (err) {
351
+ // Prevent subscriber errors from halting store
352
+ }
353
+ }
354
+ }
355
+ generateId() {
356
+ return 'ev_' + Math.random().toString(36).substring(2, 9) + '_' + Date.now().toString(36);
357
+ }
358
+ }
359
+
360
+ class ConsoleCollector {
361
+ id = 'console';
362
+ running = false;
363
+ originalMethods = {};
364
+ start() {
365
+ if (this.running || typeof console === 'undefined')
366
+ return;
367
+ const methods = [
368
+ { method: 'log', level: 'info' },
369
+ { method: 'info', level: 'info' },
370
+ { method: 'warn', level: 'warn' },
371
+ { method: 'error', level: 'error' },
372
+ { method: 'debug', level: 'debug' },
373
+ ];
374
+ for (const { method, level } of methods) {
375
+ const original = console[method]?.bind(console);
376
+ this.originalMethods[method] = original;
377
+ console[method] = (...args) => {
378
+ // 1. Call original console method
379
+ if (original) {
380
+ try {
381
+ original(...args);
382
+ }
383
+ catch {
384
+ // ignore
385
+ }
386
+ }
387
+ // 2. Dispatch CapDebug event safely
388
+ try {
389
+ this.emitConsoleEvent(method, level, args);
390
+ }
391
+ catch {
392
+ // ignore to prevent crashing host application
393
+ }
394
+ };
395
+ }
396
+ this.running = true;
397
+ }
398
+ stop() {
399
+ if (!this.running || typeof console === 'undefined')
400
+ return;
401
+ for (const method of Object.keys(this.originalMethods)) {
402
+ const original = this.originalMethods[method];
403
+ if (original) {
404
+ console[method] = original;
405
+ }
406
+ }
407
+ this.originalMethods = {};
408
+ this.running = false;
409
+ }
410
+ emitConsoleEvent(method, level, args) {
411
+ if (typeof window === 'undefined')
412
+ return;
413
+ let message = '';
414
+ let data = undefined;
415
+ if (args.length === 1) {
416
+ const arg = args[0];
417
+ if (typeof arg === 'string' || typeof arg === 'number' || typeof arg === 'boolean') {
418
+ message = String(arg);
419
+ }
420
+ else if (arg instanceof Error) {
421
+ message = `${arg.name}: ${arg.message}`;
422
+ data = { name: arg.name, message: arg.message, stack: arg.stack };
423
+ }
424
+ else {
425
+ message = `[${typeof arg}]`;
426
+ data = arg;
427
+ }
428
+ }
429
+ else if (args.length > 1) {
430
+ // Multiple arguments
431
+ const first = args[0];
432
+ if (typeof first === 'string') {
433
+ message = first;
434
+ data = args.slice(1);
435
+ }
436
+ else {
437
+ message = args.map(a => (typeof a === 'object' && a !== null ? '[Object]' : String(a))).join(' ');
438
+ data = args;
439
+ }
440
+ }
441
+ window.dispatchEvent(new CustomEvent('capdebug', {
442
+ detail: {
443
+ source: 'console',
444
+ type: `console.${method}`,
445
+ level,
446
+ message,
447
+ data,
448
+ timestamp: Date.now(),
449
+ },
450
+ }));
451
+ }
452
+ }
453
+
454
+ class ErrorCollector {
455
+ id = 'error';
456
+ running = false;
457
+ errorHandler = null;
458
+ rejectionHandler = null;
459
+ start() {
460
+ if (this.running || typeof window === 'undefined')
461
+ return;
462
+ this.errorHandler = (event) => {
463
+ try {
464
+ const error = event.error;
465
+ const message = event.message || (error && error.message) || 'Uncaught Error';
466
+ const stack = error && error.stack ? error.stack : undefined;
467
+ window.dispatchEvent(new CustomEvent('capdebug', {
468
+ detail: {
469
+ source: 'web',
470
+ type: 'window.error',
471
+ level: 'error',
472
+ message,
473
+ data: {
474
+ filename: event.filename,
475
+ lineno: event.lineno,
476
+ colno: event.colno,
477
+ stack,
478
+ errorName: error?.name,
479
+ },
480
+ timestamp: Date.now(),
481
+ },
482
+ }));
483
+ }
484
+ catch {
485
+ // ignore
486
+ }
487
+ };
488
+ this.rejectionHandler = (event) => {
489
+ try {
490
+ const reason = event.reason;
491
+ let message = 'Unhandled Promise Rejection';
492
+ let stack = undefined;
493
+ let data = reason;
494
+ if (reason instanceof Error) {
495
+ message = `Unhandled Rejection: ${reason.message}`;
496
+ stack = reason.stack;
497
+ data = {
498
+ name: reason.name,
499
+ message: reason.message,
500
+ stack,
501
+ };
502
+ }
503
+ else if (typeof reason === 'string') {
504
+ message = `Unhandled Rejection: ${reason}`;
505
+ }
506
+ window.dispatchEvent(new CustomEvent('capdebug', {
507
+ detail: {
508
+ source: 'web',
509
+ type: 'unhandledrejection',
510
+ level: 'error',
511
+ message,
512
+ data: {
513
+ reason: data,
514
+ stack,
515
+ },
516
+ timestamp: Date.now(),
517
+ },
518
+ }));
519
+ }
520
+ catch {
521
+ // ignore
522
+ }
523
+ };
524
+ window.addEventListener('error', this.errorHandler);
525
+ window.addEventListener('unhandledrejection', this.rejectionHandler);
526
+ this.running = true;
527
+ }
528
+ stop() {
529
+ if (!this.running || typeof window === 'undefined')
530
+ return;
531
+ if (this.errorHandler) {
532
+ window.removeEventListener('error', this.errorHandler);
533
+ this.errorHandler = null;
534
+ }
535
+ if (this.rejectionHandler) {
536
+ window.removeEventListener('unhandledrejection', this.rejectionHandler);
537
+ this.rejectionHandler = null;
538
+ }
539
+ this.running = false;
540
+ }
541
+ }
542
+
543
+ class NetworkCollector {
544
+ id = 'network';
545
+ running = false;
546
+ originalFetch = null;
547
+ options;
548
+ constructor(options = {}) {
549
+ this.options = {
550
+ body: false,
551
+ ...options,
552
+ };
553
+ }
554
+ setOptions(options) {
555
+ this.options = { ...this.options, ...options };
556
+ }
557
+ start() {
558
+ if (this.running || typeof window === 'undefined' || !window.fetch)
559
+ return;
560
+ this.originalFetch = window.fetch;
561
+ const self = this;
562
+ window.fetch = async function (input, init) {
563
+ const startTime = performance.now();
564
+ const timestamp = Date.now();
565
+ let url = '';
566
+ let method = 'GET';
567
+ if (typeof input === 'string') {
568
+ url = input;
569
+ }
570
+ else if (input instanceof URL) {
571
+ url = input.toString();
572
+ }
573
+ else if (typeof Request !== 'undefined' && input instanceof Request) {
574
+ url = input.url;
575
+ method = input.method;
576
+ }
577
+ if (init && init.method) {
578
+ method = init.method.toUpperCase();
579
+ }
580
+ let requestHeaders = {};
581
+ if (init && init.headers) {
582
+ if (typeof Headers !== 'undefined' && init.headers instanceof Headers) {
583
+ init.headers.forEach((value, key) => {
584
+ requestHeaders[key] = value;
585
+ });
586
+ }
587
+ else if (Array.isArray(init.headers)) {
588
+ for (const [k, v] of init.headers) {
589
+ requestHeaders[k] = v;
590
+ }
591
+ }
592
+ else if (typeof init.headers === 'object') {
593
+ requestHeaders = { ...init.headers };
594
+ }
595
+ }
596
+ let requestBody = undefined;
597
+ if (self.options.body && init && init.body) {
598
+ try {
599
+ if (typeof init.body === 'string') {
600
+ try {
601
+ requestBody = JSON.parse(init.body);
602
+ }
603
+ catch {
604
+ requestBody = init.body;
605
+ }
606
+ }
607
+ else {
608
+ requestBody = '[Binary/Stream Body]';
609
+ }
610
+ }
611
+ catch {
612
+ requestBody = '[Unreadable Body]';
613
+ }
614
+ }
615
+ try {
616
+ const response = await self.originalFetch.call(this, input, init);
617
+ const duration = Math.round(performance.now() - startTime);
618
+ let level = 'info';
619
+ if (response.status >= 500) {
620
+ level = 'error';
621
+ }
622
+ else if (response.status >= 400) {
623
+ level = 'warn';
624
+ }
625
+ let responseBody = undefined;
626
+ if (self.options.body) {
627
+ try {
628
+ const clone = response.clone();
629
+ const contentType = clone.headers.get('content-type') || '';
630
+ if (contentType.includes('application/json')) {
631
+ responseBody = await clone.json();
632
+ }
633
+ else if (contentType.includes('text/')) {
634
+ responseBody = await clone.text();
635
+ }
636
+ }
637
+ catch {
638
+ responseBody = '[Could not clone response body]';
639
+ }
640
+ }
641
+ const responseHeaders = {};
642
+ response.headers.forEach((v, k) => {
643
+ responseHeaders[k] = v;
644
+ });
645
+ self.emitNetworkEvent({
646
+ type: `fetch ${method}`,
647
+ level,
648
+ message: `${method} ${url} → ${response.status} ${response.statusText} (${duration}ms)`,
649
+ duration,
650
+ timestamp,
651
+ data: {
652
+ method,
653
+ url,
654
+ status: response.status,
655
+ statusText: response.statusText,
656
+ ok: response.ok,
657
+ duration,
658
+ requestHeaders,
659
+ responseHeaders,
660
+ requestBody,
661
+ responseBody,
662
+ },
663
+ });
664
+ return response;
665
+ }
666
+ catch (err) {
667
+ const duration = Math.round(performance.now() - startTime);
668
+ self.emitNetworkEvent({
669
+ type: `fetch ${method}`,
670
+ level: 'error',
671
+ message: `${method} ${url} → Failed: ${err?.message || 'Network Error'} (${duration}ms)`,
672
+ duration,
673
+ timestamp,
674
+ data: {
675
+ method,
676
+ url,
677
+ error: err?.message || String(err),
678
+ duration,
679
+ requestHeaders,
680
+ requestBody,
681
+ },
682
+ });
683
+ throw err;
684
+ }
685
+ };
686
+ this.running = true;
687
+ }
688
+ stop() {
689
+ if (!this.running || typeof window === 'undefined')
690
+ return;
691
+ if (this.originalFetch) {
692
+ window.fetch = this.originalFetch;
693
+ this.originalFetch = null;
694
+ }
695
+ this.running = false;
696
+ }
697
+ emitNetworkEvent(detail) {
698
+ if (typeof window === 'undefined')
699
+ return;
700
+ try {
701
+ window.dispatchEvent(new CustomEvent('capdebug', {
702
+ detail: {
703
+ source: 'network',
704
+ ...detail,
705
+ },
706
+ }));
707
+ }
708
+ catch {
709
+ // ignore
710
+ }
711
+ }
712
+ }
713
+
714
+ class CustomEventCollector {
715
+ id = 'custom-event';
716
+ store;
717
+ running = false;
718
+ handler = null;
719
+ constructor(store) {
720
+ this.store = store;
721
+ }
722
+ start() {
723
+ if (this.running || typeof window === 'undefined')
724
+ return;
725
+ this.handler = (event) => {
726
+ const customEvent = event;
727
+ if (customEvent && customEvent.detail) {
728
+ const detail = customEvent.detail;
729
+ this.store.add({
730
+ id: detail.id,
731
+ timestamp: detail.timestamp || Date.now(),
732
+ source: detail.source || 'web',
733
+ type: detail.type || 'custom-event',
734
+ level: detail.level || 'info',
735
+ message: detail.message,
736
+ data: detail.data,
737
+ duration: detail.duration,
738
+ platform: detail.platform,
739
+ });
740
+ }
741
+ };
742
+ window.addEventListener('capdebug', this.handler);
743
+ this.running = true;
744
+ }
745
+ stop() {
746
+ if (!this.running || typeof window === 'undefined')
747
+ return;
748
+ if (this.handler) {
749
+ window.removeEventListener('capdebug', this.handler);
750
+ this.handler = null;
751
+ }
752
+ this.running = false;
753
+ }
754
+ }
755
+
756
+ class BrowserEventCollector {
757
+ id = 'browser-event';
758
+ running = false;
759
+ observedEvents = new Map();
760
+ start() {
761
+ this.running = true;
762
+ }
763
+ observe(eventName) {
764
+ if (!this.running || typeof window === 'undefined' || this.observedEvents.has(eventName))
765
+ return;
766
+ const listener = (event) => {
767
+ let data = {
768
+ type: event.type,
769
+ timeStamp: event.timeStamp,
770
+ };
771
+ if (event.type === 'visibilitychange' && typeof document !== 'undefined') {
772
+ data.visibilityState = document.visibilityState;
773
+ data.hidden = document.hidden;
774
+ }
775
+ else if (event.type === 'online' || event.type === 'offline') {
776
+ data.onLine = typeof navigator !== 'undefined' ? navigator.onLine : undefined;
777
+ }
778
+ window.dispatchEvent(new CustomEvent('capdebug', {
779
+ detail: {
780
+ source: 'system',
781
+ type: `event.${eventName}`,
782
+ level: 'info',
783
+ message: `Browser event fired: ${eventName}`,
784
+ data,
785
+ timestamp: Date.now(),
786
+ },
787
+ }));
788
+ };
789
+ const target = eventName === 'visibilitychange' && typeof document !== 'undefined'
790
+ ? document
791
+ : window;
792
+ target.addEventListener(eventName, listener);
793
+ this.observedEvents.set(eventName, listener);
794
+ }
795
+ unobserve(eventName) {
796
+ const listener = this.observedEvents.get(eventName);
797
+ if (!listener)
798
+ return;
799
+ const target = eventName === 'visibilitychange' && typeof document !== 'undefined'
800
+ ? document
801
+ : window;
802
+ target.removeEventListener(eventName, listener);
803
+ this.observedEvents.delete(eventName);
804
+ }
805
+ stop() {
806
+ for (const [eventName, listener] of this.observedEvents.entries()) {
807
+ const target = eventName === 'visibilitychange' && typeof document !== 'undefined'
808
+ ? document
809
+ : window;
810
+ target.removeEventListener(eventName, listener);
811
+ }
812
+ this.observedEvents.clear();
813
+ this.running = false;
814
+ }
815
+ }
816
+
817
+ class BridgeCollector {
818
+ id = 'bridge';
819
+ running = false;
820
+ start() {
821
+ if (this.running)
822
+ return;
823
+ // Reserved for non-intrusive Capacitor bridge telemetry
824
+ this.running = true;
825
+ }
826
+ stop() {
827
+ this.running = false;
828
+ }
829
+ emitBridgeCall(pluginName, methodName, options, duration) {
830
+ if (!this.running || typeof window === 'undefined')
831
+ return;
832
+ window.dispatchEvent(new CustomEvent('capdebug', {
833
+ detail: {
834
+ source: 'bridge',
835
+ type: `${pluginName}.${methodName}`,
836
+ level: 'debug',
837
+ message: `Bridge call: ${pluginName}.${methodName}`,
838
+ data: options,
839
+ duration,
840
+ timestamp: Date.now(),
841
+ },
842
+ }));
843
+ }
844
+ }
845
+
846
+ const VIEWER_STYLES = `
847
+ :host {
848
+ --cd-bg-base: #0d1117;
849
+ --cd-bg-surface: #161b22;
850
+ --cd-bg-surface-elevated: #21262d;
851
+ --cd-bg-hover: #30363d;
852
+ --cd-border: #30363d;
853
+ --cd-border-subtle: #21262d;
854
+
855
+ --cd-text-primary: #f0f6fc;
856
+ --cd-text-secondary: #8b949e;
857
+ --cd-text-muted: #6e7681;
858
+
859
+ --cd-color-brand: #58a6ff;
860
+ --cd-color-brand-rgb: 88, 166, 255;
861
+ --cd-color-info: #38bdf8;
862
+ --cd-color-warn: #fbbf24;
863
+ --cd-color-error: #f87171;
864
+ --cd-color-debug: #c084fc;
865
+ --cd-color-success: #34d399;
866
+
867
+ --cd-font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
868
+ --cd-font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Monaco, Consolas, monospace;
869
+
870
+ --cd-z-index: 2147483647;
871
+
872
+ all: initial;
873
+ font-family: var(--cd-font-sans);
874
+ font-size: 13px;
875
+ line-height: 1.4;
876
+ color: var(--cd-text-primary);
877
+ box-sizing: border-box;
878
+ }
879
+
880
+ *, *::before, *::after {
881
+ box-sizing: border-box;
882
+ }
883
+
884
+ /* Floating Trigger Button */
885
+ .cd-floating-btn {
886
+ position: fixed;
887
+ z-index: var(--cd-z-index);
888
+ display: flex;
889
+ align-items: center;
890
+ justify-content: center;
891
+ gap: 6px;
892
+ height: 38px;
893
+ padding: 0 12px;
894
+ background: rgba(22, 27, 34, 0.88);
895
+ backdrop-filter: blur(12px);
896
+ -webkit-backdrop-filter: blur(12px);
897
+ border: 1px solid rgba(240, 246, 252, 0.15);
898
+ border-radius: 9999px;
899
+ color: var(--cd-text-primary);
900
+ cursor: pointer;
901
+ user-select: none;
902
+ -webkit-user-select: none;
903
+ touch-action: none;
904
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4), 0 2px 6px rgba(0, 0, 0, 0.3);
905
+ transition: transform 0.15s ease, background-color 0.2s ease, border-color 0.2s ease;
906
+ }
907
+
908
+ .cd-floating-btn:active {
909
+ transform: scale(0.94);
910
+ }
911
+
912
+ .cd-floating-btn.dragging {
913
+ opacity: 0.85;
914
+ transform: scale(1.05);
915
+ box-shadow: 0 12px 32px rgba(0, 0, 0, 0.6);
916
+ }
917
+
918
+ .cd-btn-logo {
919
+ display: flex;
920
+ align-items: center;
921
+ justify-content: center;
922
+ font-weight: 700;
923
+ font-size: 11px;
924
+ letter-spacing: 0.5px;
925
+ color: #38bdf8;
926
+ font-family: var(--cd-font-mono);
927
+ }
928
+
929
+ .cd-btn-badge {
930
+ display: none;
931
+ align-items: center;
932
+ justify-content: center;
933
+ min-width: 18px;
934
+ height: 18px;
935
+ padding: 0 5px;
936
+ font-size: 10px;
937
+ font-weight: 700;
938
+ font-family: var(--cd-font-mono);
939
+ background: var(--cd-color-error);
940
+ color: #ffffff;
941
+ border-radius: 9999px;
942
+ }
943
+
944
+ .cd-btn-badge.visible {
945
+ display: flex;
946
+ }
947
+
948
+ /* Backdrop */
949
+ .cd-backdrop {
950
+ position: fixed;
951
+ inset: 0;
952
+ z-index: calc(var(--cd-z-index) - 1);
953
+ background: rgba(0, 0, 0, 0.45);
954
+ backdrop-filter: blur(2px);
955
+ opacity: 0;
956
+ pointer-events: none;
957
+ transition: opacity 0.25s cubic-bezier(0.16, 1, 0.3, 1);
958
+ }
959
+
960
+ .cd-backdrop.open {
961
+ opacity: 1;
962
+ pointer-events: auto;
963
+ }
964
+
965
+ /* Main Drawer Panel */
966
+ .cd-panel {
967
+ position: fixed;
968
+ bottom: 0;
969
+ left: 0;
970
+ right: 0;
971
+ z-index: var(--cd-z-index);
972
+ height: 80vh;
973
+ max-height: 85vh;
974
+ background: var(--cd-bg-base);
975
+ border-top: 1px solid var(--cd-border);
976
+ border-top-left-radius: 16px;
977
+ border-top-right-radius: 16px;
978
+ box-shadow: 0 -10px 40px rgba(0, 0, 0, 0.6);
979
+ display: flex;
980
+ flex-direction: column;
981
+ transform: translateY(100%);
982
+ transition: transform 0.28s cubic-bezier(0.32, 0.72, 0, 1);
983
+ padding-bottom: env(safe-area-inset-bottom, 12px);
984
+ }
985
+
986
+ @media (min-width: 768px) {
987
+ .cd-panel {
988
+ bottom: 12px;
989
+ right: 12px;
990
+ left: auto;
991
+ width: 480px;
992
+ height: calc(100vh - 24px);
993
+ max-height: 880px;
994
+ border-radius: 16px;
995
+ border: 1px solid var(--cd-border);
996
+ transform: translateX(calc(100% + 24px));
997
+ padding-bottom: 0;
998
+ }
999
+ }
1000
+
1001
+ .cd-panel.open {
1002
+ transform: translateY(0);
1003
+ }
1004
+
1005
+ @media (min-width: 768px) {
1006
+ .cd-panel.open {
1007
+ transform: translateX(0);
1008
+ }
1009
+ }
1010
+
1011
+ /* Panel Header */
1012
+ .cd-header {
1013
+ display: flex;
1014
+ align-items: center;
1015
+ justify-content: space-between;
1016
+ padding: 10px 14px;
1017
+ border-bottom: 1px solid var(--cd-border);
1018
+ background: var(--cd-bg-surface);
1019
+ border-top-left-radius: 16px;
1020
+ border-top-right-radius: 16px;
1021
+ user-select: none;
1022
+ }
1023
+
1024
+ .cd-header-left {
1025
+ display: flex;
1026
+ align-items: center;
1027
+ gap: 8px;
1028
+ }
1029
+
1030
+ .cd-header-title {
1031
+ font-weight: 700;
1032
+ font-size: 13px;
1033
+ font-family: var(--cd-font-mono);
1034
+ color: var(--cd-text-primary);
1035
+ display: flex;
1036
+ align-items: center;
1037
+ gap: 6px;
1038
+ }
1039
+
1040
+ .cd-status-indicator {
1041
+ width: 8px;
1042
+ height: 8px;
1043
+ border-radius: 50%;
1044
+ background: var(--cd-color-success);
1045
+ }
1046
+
1047
+ .cd-status-indicator.paused {
1048
+ background: var(--cd-color-warn);
1049
+ }
1050
+
1051
+ .cd-header-actions {
1052
+ display: flex;
1053
+ align-items: center;
1054
+ gap: 6px;
1055
+ }
1056
+
1057
+ .cd-btn-icon {
1058
+ display: flex;
1059
+ align-items: center;
1060
+ justify-content: center;
1061
+ width: 28px;
1062
+ height: 28px;
1063
+ background: transparent;
1064
+ border: 1px solid transparent;
1065
+ border-radius: 6px;
1066
+ color: var(--cd-text-secondary);
1067
+ cursor: pointer;
1068
+ transition: all 0.15s ease;
1069
+ padding: 0;
1070
+ }
1071
+
1072
+ .cd-btn-icon:hover {
1073
+ background: var(--cd-bg-hover);
1074
+ color: var(--cd-text-primary);
1075
+ border-color: var(--cd-border);
1076
+ }
1077
+
1078
+ .cd-btn-icon.active {
1079
+ background: rgba(251, 191, 36, 0.15);
1080
+ color: var(--cd-color-warn);
1081
+ border-color: rgba(251, 191, 36, 0.3);
1082
+ }
1083
+
1084
+ /* Tabs Bar */
1085
+ .cd-tabs-bar {
1086
+ display: flex;
1087
+ align-items: center;
1088
+ gap: 4px;
1089
+ padding: 6px 10px;
1090
+ background: var(--cd-bg-surface);
1091
+ border-bottom: 1px solid var(--cd-border);
1092
+ overflow-x: auto;
1093
+ scrollbar-width: none;
1094
+ -webkit-overflow-scrolling: touch;
1095
+ }
1096
+
1097
+ .cd-tabs-bar::-webkit-scrollbar {
1098
+ display: none;
1099
+ }
1100
+
1101
+ .cd-tab {
1102
+ display: flex;
1103
+ align-items: center;
1104
+ gap: 6px;
1105
+ padding: 5px 10px;
1106
+ background: transparent;
1107
+ border: 1px solid transparent;
1108
+ border-radius: 6px;
1109
+ color: var(--cd-text-secondary);
1110
+ font-size: 12px;
1111
+ font-weight: 500;
1112
+ cursor: pointer;
1113
+ white-space: nowrap;
1114
+ transition: all 0.15s ease;
1115
+ }
1116
+
1117
+ .cd-tab:hover {
1118
+ background: var(--cd-bg-hover);
1119
+ color: var(--cd-text-primary);
1120
+ }
1121
+
1122
+ .cd-tab.active {
1123
+ background: var(--cd-bg-surface-elevated);
1124
+ border-color: var(--cd-border);
1125
+ color: var(--cd-text-primary);
1126
+ font-weight: 600;
1127
+ }
1128
+
1129
+ .cd-tab-badge {
1130
+ font-size: 10px;
1131
+ font-family: var(--cd-font-mono);
1132
+ padding: 1px 5px;
1133
+ background: var(--cd-bg-hover);
1134
+ border-radius: 10px;
1135
+ color: var(--cd-text-muted);
1136
+ }
1137
+
1138
+ .cd-tab.active .cd-tab-badge {
1139
+ background: rgba(88, 166, 255, 0.2);
1140
+ color: var(--cd-color-brand);
1141
+ }
1142
+
1143
+ /* Toolbar */
1144
+ .cd-toolbar {
1145
+ display: flex;
1146
+ align-items: center;
1147
+ gap: 8px;
1148
+ padding: 8px 12px;
1149
+ background: var(--cd-bg-base);
1150
+ border-bottom: 1px solid var(--cd-border-subtle);
1151
+ }
1152
+
1153
+ .cd-search-input {
1154
+ flex: 1;
1155
+ height: 28px;
1156
+ padding: 0 8px;
1157
+ background: var(--cd-bg-surface);
1158
+ border: 1px solid var(--cd-border);
1159
+ border-radius: 6px;
1160
+ color: var(--cd-text-primary);
1161
+ font-size: 12px;
1162
+ outline: none;
1163
+ transition: border-color 0.15s ease;
1164
+ }
1165
+
1166
+ .cd-search-input:focus {
1167
+ border-color: var(--cd-color-brand);
1168
+ }
1169
+
1170
+ .cd-search-input::placeholder {
1171
+ color: var(--cd-text-muted);
1172
+ }
1173
+
1174
+ .cd-level-filters {
1175
+ display: flex;
1176
+ align-items: center;
1177
+ gap: 4px;
1178
+ }
1179
+
1180
+ .cd-level-pill {
1181
+ padding: 3px 7px;
1182
+ border-radius: 4px;
1183
+ font-size: 11px;
1184
+ font-weight: 600;
1185
+ background: transparent;
1186
+ border: 1px solid var(--cd-border-subtle);
1187
+ color: var(--cd-text-muted);
1188
+ cursor: pointer;
1189
+ transition: all 0.12s ease;
1190
+ }
1191
+
1192
+ .cd-level-pill:hover {
1193
+ border-color: var(--cd-border);
1194
+ color: var(--cd-text-secondary);
1195
+ }
1196
+
1197
+ .cd-level-pill.active {
1198
+ background: var(--cd-bg-surface-elevated);
1199
+ border-color: var(--cd-border);
1200
+ color: var(--cd-text-primary);
1201
+ }
1202
+
1203
+ .cd-level-pill[data-level="error"].active {
1204
+ background: rgba(248, 113, 113, 0.15);
1205
+ border-color: rgba(248, 113, 113, 0.3);
1206
+ color: var(--cd-color-error);
1207
+ }
1208
+
1209
+ .cd-level-pill[data-level="warn"].active {
1210
+ background: rgba(251, 191, 36, 0.15);
1211
+ border-color: rgba(251, 191, 36, 0.3);
1212
+ color: var(--cd-color-warn);
1213
+ }
1214
+
1215
+ /* Event List */
1216
+ .cd-content {
1217
+ flex: 1;
1218
+ overflow-y: auto;
1219
+ position: relative;
1220
+ -webkit-overflow-scrolling: touch;
1221
+ }
1222
+
1223
+ .cd-event-list {
1224
+ display: flex;
1225
+ flex-direction: column;
1226
+ }
1227
+
1228
+ .cd-event-row {
1229
+ display: flex;
1230
+ align-items: flex-start;
1231
+ gap: 8px;
1232
+ padding: 8px 12px;
1233
+ border-bottom: 1px solid var(--cd-border-subtle);
1234
+ cursor: pointer;
1235
+ transition: background-color 0.12s ease;
1236
+ font-family: var(--cd-font-mono);
1237
+ font-size: 11.5px;
1238
+ }
1239
+
1240
+ .cd-event-row:hover {
1241
+ background: var(--cd-bg-surface);
1242
+ }
1243
+
1244
+ .cd-event-level-dot {
1245
+ width: 6px;
1246
+ height: 6px;
1247
+ border-radius: 50%;
1248
+ margin-top: 5px;
1249
+ flex-shrink: 0;
1250
+ background: var(--cd-text-muted);
1251
+ }
1252
+
1253
+ .cd-event-row[data-level="info"] .cd-event-level-dot { background: var(--cd-color-info); }
1254
+ .cd-event-row[data-level="warn"] .cd-event-level-dot { background: var(--cd-color-warn); }
1255
+ .cd-event-row[data-level="error"] .cd-event-level-dot { background: var(--cd-color-error); }
1256
+ .cd-event-row[data-level="debug"] .cd-event-level-dot { background: var(--cd-color-debug); }
1257
+
1258
+ .cd-event-time {
1259
+ color: var(--cd-text-muted);
1260
+ font-size: 10.5px;
1261
+ flex-shrink: 0;
1262
+ margin-top: 1px;
1263
+ }
1264
+
1265
+ .cd-event-source {
1266
+ padding: 1px 5px;
1267
+ border-radius: 3px;
1268
+ background: var(--cd-bg-surface-elevated);
1269
+ color: var(--cd-text-secondary);
1270
+ font-size: 10px;
1271
+ font-weight: 600;
1272
+ text-transform: uppercase;
1273
+ flex-shrink: 0;
1274
+ }
1275
+
1276
+ .cd-event-main {
1277
+ flex: 1;
1278
+ min-width: 0;
1279
+ display: flex;
1280
+ flex-direction: column;
1281
+ gap: 2px;
1282
+ }
1283
+
1284
+ .cd-event-type {
1285
+ font-weight: 600;
1286
+ color: var(--cd-text-primary);
1287
+ overflow: hidden;
1288
+ text-overflow: ellipsis;
1289
+ white-space: nowrap;
1290
+ }
1291
+
1292
+ .cd-event-msg {
1293
+ color: var(--cd-text-secondary);
1294
+ overflow: hidden;
1295
+ text-overflow: ellipsis;
1296
+ white-space: nowrap;
1297
+ font-size: 11px;
1298
+ }
1299
+
1300
+ .cd-event-duration {
1301
+ font-size: 10px;
1302
+ padding: 1px 4px;
1303
+ border-radius: 3px;
1304
+ background: rgba(88, 166, 255, 0.1);
1305
+ color: var(--cd-color-brand);
1306
+ flex-shrink: 0;
1307
+ }
1308
+
1309
+ .cd-empty-state {
1310
+ display: flex;
1311
+ flex-direction: column;
1312
+ align-items: center;
1313
+ justify-content: center;
1314
+ height: 200px;
1315
+ color: var(--cd-text-muted);
1316
+ gap: 8px;
1317
+ font-size: 12px;
1318
+ }
1319
+
1320
+ /* Detail Modal Overlay */
1321
+ .cd-detail-overlay {
1322
+ position: absolute;
1323
+ inset: 0;
1324
+ background: var(--cd-bg-base);
1325
+ z-index: 10;
1326
+ display: flex;
1327
+ flex-direction: column;
1328
+ transform: translateX(100%);
1329
+ transition: transform 0.22s cubic-bezier(0.16, 1, 0.3, 1);
1330
+ }
1331
+
1332
+ .cd-detail-overlay.open {
1333
+ transform: translateX(0);
1334
+ }
1335
+
1336
+ .cd-detail-header {
1337
+ display: flex;
1338
+ align-items: center;
1339
+ justify-content: space-between;
1340
+ padding: 10px 14px;
1341
+ border-bottom: 1px solid var(--cd-border);
1342
+ background: var(--cd-bg-surface);
1343
+ }
1344
+
1345
+ .cd-detail-back {
1346
+ display: flex;
1347
+ align-items: center;
1348
+ gap: 6px;
1349
+ background: transparent;
1350
+ border: none;
1351
+ color: var(--cd-color-brand);
1352
+ font-size: 12px;
1353
+ font-weight: 600;
1354
+ cursor: pointer;
1355
+ padding: 4px 6px;
1356
+ border-radius: 4px;
1357
+ }
1358
+
1359
+ .cd-detail-back:hover {
1360
+ background: var(--cd-bg-hover);
1361
+ }
1362
+
1363
+ .cd-detail-body {
1364
+ flex: 1;
1365
+ overflow-y: auto;
1366
+ padding: 14px;
1367
+ display: flex;
1368
+ flex-direction: column;
1369
+ gap: 14px;
1370
+ font-family: var(--cd-font-mono);
1371
+ font-size: 12px;
1372
+ }
1373
+
1374
+ .cd-meta-grid {
1375
+ display: grid;
1376
+ grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
1377
+ gap: 8px;
1378
+ padding: 10px;
1379
+ background: var(--cd-bg-surface);
1380
+ border: 1px solid var(--cd-border-subtle);
1381
+ border-radius: 8px;
1382
+ }
1383
+
1384
+ .cd-meta-item {
1385
+ display: flex;
1386
+ flex-direction: column;
1387
+ gap: 2px;
1388
+ }
1389
+
1390
+ .cd-meta-label {
1391
+ font-size: 10px;
1392
+ text-transform: uppercase;
1393
+ color: var(--cd-text-muted);
1394
+ font-weight: 600;
1395
+ }
1396
+
1397
+ .cd-meta-val {
1398
+ font-size: 11.5px;
1399
+ color: var(--cd-text-primary);
1400
+ word-break: break-all;
1401
+ }
1402
+
1403
+ .cd-section-title {
1404
+ font-size: 11px;
1405
+ text-transform: uppercase;
1406
+ color: var(--cd-text-muted);
1407
+ font-weight: 700;
1408
+ margin-top: 6px;
1409
+ display: flex;
1410
+ align-items: center;
1411
+ justify-content: space-between;
1412
+ }
1413
+
1414
+ .cd-msg-block {
1415
+ padding: 10px;
1416
+ background: var(--cd-bg-surface);
1417
+ border: 1px solid var(--cd-border-subtle);
1418
+ border-radius: 8px;
1419
+ color: var(--cd-text-primary);
1420
+ white-space: pre-wrap;
1421
+ word-break: break-word;
1422
+ line-height: 1.5;
1423
+ }
1424
+
1425
+ /* Device Tab View */
1426
+ .cd-device-view {
1427
+ padding: 14px;
1428
+ display: flex;
1429
+ flex-direction: column;
1430
+ gap: 14px;
1431
+ }
1432
+
1433
+ .cd-device-card {
1434
+ background: var(--cd-bg-surface);
1435
+ border: 1px solid var(--cd-border);
1436
+ border-radius: 8px;
1437
+ padding: 12px;
1438
+ display: flex;
1439
+ flex-direction: column;
1440
+ gap: 8px;
1441
+ }
1442
+
1443
+ .cd-device-card-header {
1444
+ display: flex;
1445
+ align-items: center;
1446
+ justify-content: space-between;
1447
+ font-weight: 700;
1448
+ font-size: 12px;
1449
+ color: var(--cd-color-brand);
1450
+ border-bottom: 1px solid var(--cd-border-subtle);
1451
+ padding-bottom: 6px;
1452
+ }
1453
+
1454
+ .cd-device-grid {
1455
+ display: grid;
1456
+ grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
1457
+ gap: 8px;
1458
+ }
1459
+
1460
+ /* Collapsible JSON Viewer */
1461
+ .cd-json-tree {
1462
+ background: var(--cd-bg-surface);
1463
+ border: 1px solid var(--cd-border-subtle);
1464
+ border-radius: 8px;
1465
+ padding: 10px;
1466
+ font-family: var(--cd-font-mono);
1467
+ font-size: 11.5px;
1468
+ line-height: 1.5;
1469
+ overflow-x: auto;
1470
+ }
1471
+
1472
+ .cd-json-node {
1473
+ display: block;
1474
+ }
1475
+
1476
+ .cd-json-row {
1477
+ display: flex;
1478
+ align-items: flex-start;
1479
+ gap: 4px;
1480
+ padding: 1px 0;
1481
+ }
1482
+
1483
+ .cd-json-toggle {
1484
+ display: inline-flex;
1485
+ align-items: center;
1486
+ justify-content: center;
1487
+ width: 14px;
1488
+ height: 14px;
1489
+ cursor: pointer;
1490
+ user-select: none;
1491
+ color: var(--cd-text-muted);
1492
+ font-size: 9px;
1493
+ transition: transform 0.1s ease;
1494
+ }
1495
+
1496
+ .cd-json-toggle.collapsed {
1497
+ transform: rotate(-90deg);
1498
+ }
1499
+
1500
+ .cd-json-key {
1501
+ color: #7ee787;
1502
+ }
1503
+
1504
+ .cd-json-colon {
1505
+ color: var(--cd-text-muted);
1506
+ }
1507
+
1508
+ .cd-json-string {
1509
+ color: #a5d6ff;
1510
+ word-break: break-all;
1511
+ }
1512
+
1513
+ .cd-json-number {
1514
+ color: #79c0ff;
1515
+ }
1516
+
1517
+ .cd-json-boolean {
1518
+ color: #ff7b72;
1519
+ }
1520
+
1521
+ .cd-json-null {
1522
+ color: var(--cd-text-muted);
1523
+ font-style: italic;
1524
+ }
1525
+
1526
+ .cd-json-copy-btn {
1527
+ opacity: 0;
1528
+ padding: 1px 4px;
1529
+ font-size: 9px;
1530
+ background: var(--cd-bg-hover);
1531
+ border: 1px solid var(--cd-border);
1532
+ border-radius: 3px;
1533
+ color: var(--cd-text-secondary);
1534
+ cursor: pointer;
1535
+ margin-left: 6px;
1536
+ }
1537
+
1538
+ .cd-json-row:hover .cd-json-copy-btn {
1539
+ opacity: 1;
1540
+ }
1541
+
1542
+ .cd-json-children {
1543
+ padding-left: 14px;
1544
+ border-left: 1px solid var(--cd-border-subtle);
1545
+ margin-left: 6px;
1546
+ }
1547
+
1548
+ .cd-json-children.hidden {
1549
+ display: none;
1550
+ }
1551
+
1552
+ /* Small UI helpers */
1553
+ .cd-copy-btn-sm {
1554
+ background: var(--cd-bg-surface-elevated);
1555
+ border: 1px solid var(--cd-border);
1556
+ border-radius: 4px;
1557
+ color: var(--cd-text-secondary);
1558
+ font-size: 10px;
1559
+ padding: 3px 8px;
1560
+ cursor: pointer;
1561
+ transition: all 0.15s ease;
1562
+ }
1563
+
1564
+ .cd-copy-btn-sm:hover {
1565
+ background: var(--cd-bg-hover);
1566
+ color: var(--cd-text-primary);
1567
+ }
1568
+ `;
1569
+
1570
+ const STORAGE_KEY = 'capdebug_btn_pos';
1571
+ class FloatingButton {
1572
+ element;
1573
+ badgeElement;
1574
+ options;
1575
+ isDragging = false;
1576
+ hasMoved = false;
1577
+ startX = 0;
1578
+ startY = 0;
1579
+ initialLeft = 0;
1580
+ initialTop = 0;
1581
+ constructor(options = {}) {
1582
+ this.options = {
1583
+ position: 'right',
1584
+ persistPosition: true,
1585
+ ...options,
1586
+ };
1587
+ this.element = document.createElement('div');
1588
+ this.element.className = 'cd-floating-btn';
1589
+ this.element.setAttribute('role', 'button');
1590
+ this.element.setAttribute('aria-label', 'Open CapDebug');
1591
+ const logo = document.createElement('span');
1592
+ logo.className = 'cd-btn-logo';
1593
+ logo.innerHTML = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right: 4px"><path d="m8 2 1.88 1.88"/><path d="M14.12 3.88 16 2"/><path d="M9 7.13v-1a3.003 3.003 0 1 1 6 0v1"/><path d="M12 20c-3.3 0-6-2.7-6-6v-3a4 4 0 0 1 4-4h4a4 4 0 0 1 4 4v3c0 3.3-2.7 6-6 6"/><path d="M12 20v-9"/><path d="M6.53 9C4.6 8.8 3 7.1 3 5"/><path d="M6 13H2"/><path d="M3 21c0-2.1 1.7-3.9 3.8-4"/><path d="M20.97 5c0 2.1-1.6 3.8-3.5 4"/><path d="M22 13h-4"/><path d="M17.2 17c2.1.1 3.8 1.9 3.8 4"/></svg>CAP`;
1594
+ this.badgeElement = document.createElement('span');
1595
+ this.badgeElement.className = 'cd-btn-badge';
1596
+ this.badgeElement.textContent = '0';
1597
+ this.element.appendChild(logo);
1598
+ this.element.appendChild(this.badgeElement);
1599
+ this.initPosition();
1600
+ this.bindEvents();
1601
+ }
1602
+ getElement() {
1603
+ return this.element;
1604
+ }
1605
+ setErrorCount(count) {
1606
+ if (count > 0) {
1607
+ this.badgeElement.textContent = count > 99 ? '99+' : String(count);
1608
+ this.badgeElement.classList.add('visible');
1609
+ }
1610
+ else {
1611
+ this.badgeElement.classList.remove('visible');
1612
+ }
1613
+ }
1614
+ show() {
1615
+ this.element.style.display = 'flex';
1616
+ }
1617
+ hide() {
1618
+ this.element.style.display = 'none';
1619
+ }
1620
+ initPosition() {
1621
+ let savedPos = null;
1622
+ if (this.options.persistPosition && typeof localStorage !== 'undefined') {
1623
+ try {
1624
+ const stored = localStorage.getItem(STORAGE_KEY);
1625
+ if (stored) {
1626
+ savedPos = JSON.parse(stored);
1627
+ }
1628
+ }
1629
+ catch {
1630
+ // ignore
1631
+ }
1632
+ }
1633
+ const windowWidth = typeof window !== 'undefined' ? window.innerWidth : 400;
1634
+ const windowHeight = typeof window !== 'undefined' ? window.innerHeight : 800;
1635
+ let x = windowWidth - 76;
1636
+ let y = Math.max(80, windowHeight * 0.4);
1637
+ if (savedPos && typeof savedPos.x === 'number' && typeof savedPos.y === 'number') {
1638
+ x = Math.min(Math.max(10, savedPos.x), windowWidth - 76);
1639
+ y = Math.min(Math.max(60, savedPos.y), windowHeight - 60);
1640
+ }
1641
+ else if (typeof this.options.position === 'object') {
1642
+ x = this.options.position.x;
1643
+ y = this.options.position.y;
1644
+ }
1645
+ else if (this.options.position === 'left') {
1646
+ x = 12;
1647
+ }
1648
+ this.setPosition(x, y);
1649
+ }
1650
+ setPosition(x, y) {
1651
+ this.element.style.left = `${x}px`;
1652
+ this.element.style.top = `${y}px`;
1653
+ this.element.style.right = 'auto';
1654
+ this.element.style.bottom = 'auto';
1655
+ }
1656
+ bindEvents() {
1657
+ const onPointerDown = (e) => {
1658
+ this.isDragging = true;
1659
+ this.hasMoved = false;
1660
+ this.startX = e.clientX;
1661
+ this.startY = e.clientY;
1662
+ const rect = this.element.getBoundingClientRect();
1663
+ this.initialLeft = rect.left;
1664
+ this.initialTop = rect.top;
1665
+ this.element.classList.add('dragging');
1666
+ this.element.setPointerCapture(e.pointerId);
1667
+ };
1668
+ const onPointerMove = (e) => {
1669
+ if (!this.isDragging)
1670
+ return;
1671
+ const deltaX = e.clientX - this.startX;
1672
+ const deltaY = e.clientY - this.startY;
1673
+ if (Math.abs(deltaX) > 4 || Math.abs(deltaY) > 4) {
1674
+ this.hasMoved = true;
1675
+ }
1676
+ let newX = this.initialLeft + deltaX;
1677
+ let newY = this.initialTop + deltaY;
1678
+ const maxX = window.innerWidth - this.element.offsetWidth - 8;
1679
+ const maxY = window.innerHeight - this.element.offsetHeight - 8;
1680
+ newX = Math.max(8, Math.min(newX, maxX));
1681
+ newY = Math.max(40, Math.min(newY, maxY));
1682
+ this.setPosition(newX, newY);
1683
+ };
1684
+ const onPointerUp = (e) => {
1685
+ if (!this.isDragging)
1686
+ return;
1687
+ this.isDragging = false;
1688
+ this.element.classList.remove('dragging');
1689
+ try {
1690
+ this.element.releasePointerCapture(e.pointerId);
1691
+ }
1692
+ catch {
1693
+ // ignore
1694
+ }
1695
+ if (!this.hasMoved) {
1696
+ // Tap / Click action
1697
+ if (this.options.onClick) {
1698
+ this.options.onClick();
1699
+ }
1700
+ }
1701
+ else {
1702
+ // Snap to nearest edge (left or right)
1703
+ this.snapToEdge();
1704
+ }
1705
+ };
1706
+ this.element.addEventListener('pointerdown', onPointerDown);
1707
+ this.element.addEventListener('pointermove', onPointerMove);
1708
+ this.element.addEventListener('pointerup', onPointerUp);
1709
+ this.element.addEventListener('pointercancel', onPointerUp);
1710
+ }
1711
+ snapToEdge() {
1712
+ const rect = this.element.getBoundingClientRect();
1713
+ const windowWidth = window.innerWidth;
1714
+ const windowHeight = window.innerHeight;
1715
+ const snapLeft = 12;
1716
+ const snapRight = windowWidth - rect.width - 12;
1717
+ const targetX = rect.left + rect.width / 2 < windowWidth / 2 ? snapLeft : snapRight;
1718
+ const targetY = Math.max(50, Math.min(rect.top, windowHeight - rect.height - 50));
1719
+ this.element.style.transition = 'left 0.2s cubic-bezier(0.16, 1, 0.3, 1), top 0.2s cubic-bezier(0.16, 1, 0.3, 1)';
1720
+ this.setPosition(targetX, targetY);
1721
+ setTimeout(() => {
1722
+ this.element.style.transition = '';
1723
+ }, 200);
1724
+ if (this.options.persistPosition && typeof localStorage !== 'undefined') {
1725
+ try {
1726
+ localStorage.setItem(STORAGE_KEY, JSON.stringify({ x: targetX, y: targetY }));
1727
+ }
1728
+ catch {
1729
+ // ignore
1730
+ }
1731
+ }
1732
+ }
1733
+ }
1734
+
1735
+ class JsonViewer {
1736
+ static render(data, initialExpandedDepth = 2) {
1737
+ const container = document.createElement('div');
1738
+ container.className = 'cd-json-tree';
1739
+ if (data === undefined) {
1740
+ const undefinedSpan = document.createElement('span');
1741
+ undefinedSpan.className = 'cd-json-null';
1742
+ undefinedSpan.textContent = 'undefined';
1743
+ container.appendChild(undefinedSpan);
1744
+ return container;
1745
+ }
1746
+ const rootNode = JsonViewer.renderNode('', data, 0, initialExpandedDepth);
1747
+ container.appendChild(rootNode);
1748
+ return container;
1749
+ }
1750
+ static renderNode(key, value, depth, maxExpandedDepth) {
1751
+ const node = document.createElement('div');
1752
+ node.className = 'cd-json-node';
1753
+ const row = document.createElement('div');
1754
+ row.className = 'cd-json-row';
1755
+ const isObject = typeof value === 'object' && value !== null;
1756
+ const isArray = Array.isArray(value);
1757
+ // Toggle button for objects/arrays
1758
+ if (isObject) {
1759
+ const toggle = document.createElement('span');
1760
+ toggle.className = 'cd-json-toggle';
1761
+ toggle.innerHTML = '▼';
1762
+ row.appendChild(toggle);
1763
+ }
1764
+ else {
1765
+ const spacer = document.createElement('span');
1766
+ spacer.style.width = '14px';
1767
+ spacer.style.display = 'inline-block';
1768
+ row.appendChild(spacer);
1769
+ }
1770
+ // Key (if present)
1771
+ if (key !== '') {
1772
+ const keySpan = document.createElement('span');
1773
+ keySpan.className = 'cd-json-key';
1774
+ keySpan.textContent = typeof key === 'number' ? `[${key}]` : `"${key}"`;
1775
+ row.appendChild(keySpan);
1776
+ const colonSpan = document.createElement('span');
1777
+ colonSpan.className = 'cd-json-colon';
1778
+ colonSpan.textContent = ': ';
1779
+ row.appendChild(colonSpan);
1780
+ }
1781
+ // Value preview or primitive
1782
+ if (isObject) {
1783
+ const entries = isArray
1784
+ ? value.map((v, i) => [i, v])
1785
+ : Object.entries(value);
1786
+ const bracketOpen = isArray ? '[' : '{';
1787
+ const bracketClose = isArray ? ']' : '}';
1788
+ const previewSpan = document.createElement('span');
1789
+ previewSpan.className = 'cd-json-colon';
1790
+ previewSpan.textContent = `${bracketOpen} ${entries.length} ${isArray ? 'items' : 'keys'} ${bracketClose}`;
1791
+ row.appendChild(previewSpan);
1792
+ // Copy object JSON button
1793
+ const copyBtn = document.createElement('button');
1794
+ copyBtn.className = 'cd-json-copy-btn';
1795
+ copyBtn.textContent = 'copy';
1796
+ copyBtn.title = 'Copy branch JSON';
1797
+ copyBtn.addEventListener('click', e => {
1798
+ e.stopPropagation();
1799
+ JsonViewer.copyToClipboard(JSON.stringify(value, null, 2), copyBtn);
1800
+ });
1801
+ row.appendChild(copyBtn);
1802
+ node.appendChild(row);
1803
+ // Children container
1804
+ const childrenContainer = document.createElement('div');
1805
+ childrenContainer.className = 'cd-json-children';
1806
+ const isInitiallyExpanded = depth < maxExpandedDepth;
1807
+ if (!isInitiallyExpanded) {
1808
+ childrenContainer.classList.add('hidden');
1809
+ const toggle = row.querySelector('.cd-json-toggle');
1810
+ if (toggle)
1811
+ toggle.classList.add('collapsed');
1812
+ }
1813
+ for (const [childKey, childVal] of entries) {
1814
+ childrenContainer.appendChild(JsonViewer.renderNode(childKey, childVal, depth + 1, maxExpandedDepth));
1815
+ }
1816
+ node.appendChild(childrenContainer);
1817
+ // Toggle collapse on row or toggle click
1818
+ row.addEventListener('click', e => {
1819
+ if (e.target.tagName.toLowerCase() === 'button')
1820
+ return;
1821
+ const toggle = row.querySelector('.cd-json-toggle');
1822
+ const isHidden = childrenContainer.classList.toggle('hidden');
1823
+ if (toggle) {
1824
+ toggle.classList.toggle('collapsed', isHidden);
1825
+ }
1826
+ });
1827
+ }
1828
+ else {
1829
+ // Primitive value
1830
+ const valSpan = document.createElement('span');
1831
+ const valType = typeof value;
1832
+ if (value === null) {
1833
+ valSpan.className = 'cd-json-null';
1834
+ valSpan.textContent = 'null';
1835
+ }
1836
+ else if (valType === 'string') {
1837
+ valSpan.className = 'cd-json-string';
1838
+ valSpan.textContent = `"${value}"`;
1839
+ }
1840
+ else if (valType === 'number') {
1841
+ valSpan.className = 'cd-json-number';
1842
+ valSpan.textContent = String(value);
1843
+ }
1844
+ else if (valType === 'boolean') {
1845
+ valSpan.className = 'cd-json-boolean';
1846
+ valSpan.textContent = String(value);
1847
+ }
1848
+ else {
1849
+ valSpan.className = 'cd-json-null';
1850
+ valSpan.textContent = String(value);
1851
+ }
1852
+ row.appendChild(valSpan);
1853
+ // Copy primitive value button
1854
+ const copyBtn = document.createElement('button');
1855
+ copyBtn.className = 'cd-json-copy-btn';
1856
+ copyBtn.textContent = 'copy';
1857
+ copyBtn.title = 'Copy value';
1858
+ copyBtn.addEventListener('click', e => {
1859
+ e.stopPropagation();
1860
+ JsonViewer.copyToClipboard(String(value), copyBtn);
1861
+ });
1862
+ row.appendChild(copyBtn);
1863
+ node.appendChild(row);
1864
+ }
1865
+ return node;
1866
+ }
1867
+ static async copyToClipboard(text, triggerBtn) {
1868
+ try {
1869
+ if (navigator.clipboard && window.isSecureContext) {
1870
+ await navigator.clipboard.writeText(text);
1871
+ }
1872
+ else {
1873
+ const textarea = document.createElement('textarea');
1874
+ textarea.value = text;
1875
+ textarea.style.position = 'fixed';
1876
+ textarea.style.left = '-9999px';
1877
+ textarea.style.top = '-9999px';
1878
+ document.body.appendChild(textarea);
1879
+ textarea.focus();
1880
+ textarea.select();
1881
+ document.execCommand('copy');
1882
+ document.body.removeChild(textarea);
1883
+ }
1884
+ if (triggerBtn) {
1885
+ const originalText = triggerBtn.textContent;
1886
+ triggerBtn.textContent = 'copied!';
1887
+ triggerBtn.style.color = '#34d399';
1888
+ setTimeout(() => {
1889
+ triggerBtn.textContent = originalText;
1890
+ triggerBtn.style.color = '';
1891
+ }, 1500);
1892
+ }
1893
+ return true;
1894
+ }
1895
+ catch {
1896
+ return false;
1897
+ }
1898
+ }
1899
+ }
1900
+
1901
+ const CapDebugNative = core.registerPlugin('CapDebug', {
1902
+ web: () => Promise.resolve().then(function () { return web; }).then(m => new m.CapDebugWeb()),
1903
+ });
1904
+
1905
+ class DebugPanel {
1906
+ element;
1907
+ backdrop;
1908
+ store;
1909
+ options;
1910
+ isOpen = false;
1911
+ // Header UI
1912
+ statusIndicator;
1913
+ titleCountSpan;
1914
+ pauseBtn;
1915
+ // Tab elements
1916
+ tabElements = new Map();
1917
+ tabBadgeElements = new Map();
1918
+ // Search & Filter UI
1919
+ searchInput;
1920
+ levelPills = new Map();
1921
+ // Content
1922
+ contentContainer;
1923
+ eventListContainer;
1924
+ deviceViewContainer;
1925
+ // Detail Overlay
1926
+ detailOverlay;
1927
+ selectedEvent = null;
1928
+ // Device tab state
1929
+ isDeviceLoading = false;
1930
+ deviceData = null;
1931
+ constructor(store, options = {}) {
1932
+ this.store = store;
1933
+ this.options = {
1934
+ defaultTab: 'all',
1935
+ ...options,
1936
+ };
1937
+ this.backdrop = document.createElement('div');
1938
+ this.backdrop.className = 'cd-backdrop';
1939
+ this.element = document.createElement('div');
1940
+ this.element.className = 'cd-panel';
1941
+ this.buildUI();
1942
+ this.bindStore();
1943
+ if (this.options.defaultTab) {
1944
+ this.store.setFilter({ tab: this.options.defaultTab });
1945
+ }
1946
+ }
1947
+ getElement() {
1948
+ return this.element;
1949
+ }
1950
+ getBackdrop() {
1951
+ return this.backdrop;
1952
+ }
1953
+ open() {
1954
+ this.isOpen = true;
1955
+ this.element.classList.add('open');
1956
+ this.backdrop.classList.add('open');
1957
+ // If device tab is active, trigger lazy load
1958
+ if (this.store.getFilter().tab === 'device') {
1959
+ this.loadDeviceDiagnostics();
1960
+ }
1961
+ }
1962
+ close() {
1963
+ this.isOpen = false;
1964
+ this.element.classList.remove('open');
1965
+ this.backdrop.classList.remove('open');
1966
+ this.closeDetail();
1967
+ if (this.options.onClose) {
1968
+ this.options.onClose();
1969
+ }
1970
+ }
1971
+ toggle() {
1972
+ if (this.isOpen) {
1973
+ this.close();
1974
+ return false;
1975
+ }
1976
+ else {
1977
+ this.open();
1978
+ return true;
1979
+ }
1980
+ }
1981
+ isPanelOpen() {
1982
+ return this.isOpen;
1983
+ }
1984
+ buildUI() {
1985
+ this.backdrop.addEventListener('click', () => this.close());
1986
+ // 1. Header
1987
+ const header = document.createElement('div');
1988
+ header.className = 'cd-header';
1989
+ const headerLeft = document.createElement('div');
1990
+ headerLeft.className = 'cd-header-left';
1991
+ this.statusIndicator = document.createElement('div');
1992
+ this.statusIndicator.className = 'cd-status-indicator';
1993
+ this.statusIndicator.title = 'CapDebug Active';
1994
+ const headerTitle = document.createElement('div');
1995
+ headerTitle.className = 'cd-header-title';
1996
+ headerTitle.innerHTML = `<span>CapDebug</span>`;
1997
+ this.titleCountSpan = document.createElement('span');
1998
+ this.titleCountSpan.style.color = 'var(--cd-text-muted)';
1999
+ this.titleCountSpan.style.fontSize = '11px';
2000
+ this.titleCountSpan.textContent = '(0)';
2001
+ headerTitle.appendChild(this.titleCountSpan);
2002
+ headerLeft.appendChild(this.statusIndicator);
2003
+ headerLeft.appendChild(headerTitle);
2004
+ const headerActions = document.createElement('div');
2005
+ headerActions.className = 'cd-header-actions';
2006
+ // Pause/Resume Button
2007
+ this.pauseBtn = document.createElement('button');
2008
+ this.pauseBtn.className = 'cd-btn-icon';
2009
+ this.pauseBtn.title = 'Pause/Resume capture';
2010
+ this.pauseBtn.innerHTML = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="6" y="4" width="4" height="16"/><rect x="14" y="4" width="4" height="16"/></svg>`;
2011
+ this.pauseBtn.addEventListener('click', () => {
2012
+ const isPaused = this.store.togglePause();
2013
+ this.updatePauseState(isPaused);
2014
+ });
2015
+ // Clear Button
2016
+ const clearBtn = document.createElement('button');
2017
+ clearBtn.className = 'cd-btn-icon';
2018
+ clearBtn.title = 'Clear events';
2019
+ clearBtn.innerHTML = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="4.93" y1="4.93" x2="19.07" y2="19.07"/></svg>`;
2020
+ clearBtn.addEventListener('click', () => {
2021
+ this.store.clear();
2022
+ this.closeDetail();
2023
+ });
2024
+ // Export Button
2025
+ const exportBtn = document.createElement('button');
2026
+ exportBtn.className = 'cd-btn-icon';
2027
+ exportBtn.title = 'Export JSON to clipboard';
2028
+ exportBtn.innerHTML = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>`;
2029
+ exportBtn.addEventListener('click', async () => {
2030
+ const json = this.store.exportJson();
2031
+ await JsonViewer.copyToClipboard(json, exportBtn);
2032
+ });
2033
+ // Close Button
2034
+ const closeBtn = document.createElement('button');
2035
+ closeBtn.className = 'cd-btn-icon';
2036
+ closeBtn.title = 'Close viewer';
2037
+ closeBtn.innerHTML = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>`;
2038
+ closeBtn.addEventListener('click', () => this.close());
2039
+ headerActions.appendChild(this.pauseBtn);
2040
+ headerActions.appendChild(clearBtn);
2041
+ headerActions.appendChild(exportBtn);
2042
+ headerActions.appendChild(closeBtn);
2043
+ header.appendChild(headerLeft);
2044
+ header.appendChild(headerActions);
2045
+ this.element.appendChild(header);
2046
+ // 2. Tabs Bar
2047
+ const tabsBar = document.createElement('div');
2048
+ tabsBar.className = 'cd-tabs-bar';
2049
+ const tabs = [
2050
+ { id: 'all', label: 'All' },
2051
+ { id: 'console', label: 'Console' },
2052
+ { id: 'errors', label: 'Errors' },
2053
+ { id: 'events', label: 'Events' },
2054
+ { id: 'native', label: 'Native' },
2055
+ { id: 'device', label: 'Device' },
2056
+ { id: 'network', label: 'Network' },
2057
+ { id: 'bridge', label: 'Bridge' },
2058
+ ];
2059
+ for (const { id, label } of tabs) {
2060
+ const tabBtn = document.createElement('button');
2061
+ tabBtn.className = 'cd-tab';
2062
+ if (id === this.options.defaultTab) {
2063
+ tabBtn.classList.add('active');
2064
+ }
2065
+ const labelSpan = document.createElement('span');
2066
+ labelSpan.textContent = label;
2067
+ const badgeSpan = document.createElement('span');
2068
+ badgeSpan.className = 'cd-tab-badge';
2069
+ badgeSpan.textContent = '0';
2070
+ tabBtn.appendChild(labelSpan);
2071
+ tabBtn.appendChild(badgeSpan);
2072
+ tabBtn.addEventListener('click', () => {
2073
+ this.selectTab(id);
2074
+ });
2075
+ this.tabElements.set(id, tabBtn);
2076
+ this.tabBadgeElements.set(id, badgeSpan);
2077
+ tabsBar.appendChild(tabBtn);
2078
+ }
2079
+ this.element.appendChild(tabsBar);
2080
+ // 3. Toolbar (Search & Level Filters)
2081
+ const toolbar = document.createElement('div');
2082
+ toolbar.className = 'cd-toolbar';
2083
+ this.searchInput = document.createElement('input');
2084
+ this.searchInput.className = 'cd-search-input';
2085
+ this.searchInput.type = 'text';
2086
+ this.searchInput.placeholder = 'Filter logs, types, payloads...';
2087
+ this.searchInput.addEventListener('input', () => {
2088
+ this.store.setFilter({ search: this.searchInput.value });
2089
+ });
2090
+ const levelFilters = document.createElement('div');
2091
+ levelFilters.className = 'cd-level-filters';
2092
+ const levels = ['all', 'error', 'warn', 'info', 'debug'];
2093
+ for (const lvl of levels) {
2094
+ const pill = document.createElement('button');
2095
+ pill.className = 'cd-level-pill';
2096
+ pill.setAttribute('data-level', lvl);
2097
+ pill.textContent = lvl === 'all' ? 'All' : lvl.toUpperCase();
2098
+ if (lvl === 'all')
2099
+ pill.classList.add('active');
2100
+ pill.addEventListener('click', () => {
2101
+ this.selectLevel(lvl);
2102
+ });
2103
+ this.levelPills.set(lvl, pill);
2104
+ levelFilters.appendChild(pill);
2105
+ }
2106
+ toolbar.appendChild(this.searchInput);
2107
+ toolbar.appendChild(levelFilters);
2108
+ this.element.appendChild(toolbar);
2109
+ // 4. Content Area
2110
+ this.contentContainer = document.createElement('div');
2111
+ this.contentContainer.className = 'cd-content';
2112
+ this.eventListContainer = document.createElement('div');
2113
+ this.eventListContainer.className = 'cd-event-list';
2114
+ this.deviceViewContainer = document.createElement('div');
2115
+ this.deviceViewContainer.className = 'cd-device-view';
2116
+ this.deviceViewContainer.style.display = 'none';
2117
+ this.contentContainer.appendChild(this.eventListContainer);
2118
+ this.contentContainer.appendChild(this.deviceViewContainer);
2119
+ this.element.appendChild(this.contentContainer);
2120
+ // 5. Detail Overlay
2121
+ this.buildDetailOverlay();
2122
+ }
2123
+ selectTab(tab) {
2124
+ for (const [id, el] of this.tabElements.entries()) {
2125
+ el.classList.toggle('active', id === tab);
2126
+ }
2127
+ this.store.setFilter({ tab });
2128
+ this.closeDetail();
2129
+ if (tab === 'device') {
2130
+ this.eventListContainer.style.display = 'none';
2131
+ this.deviceViewContainer.style.display = 'flex';
2132
+ this.loadDeviceDiagnostics();
2133
+ }
2134
+ else {
2135
+ this.eventListContainer.style.display = 'flex';
2136
+ this.deviceViewContainer.style.display = 'none';
2137
+ }
2138
+ }
2139
+ selectLevel(level) {
2140
+ for (const [lvl, pill] of this.levelPills.entries()) {
2141
+ pill.classList.toggle('active', lvl === level);
2142
+ }
2143
+ this.store.setFilter({ level });
2144
+ }
2145
+ updatePauseState(isPaused) {
2146
+ this.statusIndicator.classList.toggle('paused', isPaused);
2147
+ this.pauseBtn.classList.toggle('active', isPaused);
2148
+ this.pauseBtn.title = isPaused ? 'Resume capture' : 'Pause capture';
2149
+ }
2150
+ bindStore() {
2151
+ this.store.subscribe((events, store) => {
2152
+ this.renderEventList(events);
2153
+ this.renderCounts(store.getCounts());
2154
+ this.updatePauseState(store.isPaused());
2155
+ });
2156
+ }
2157
+ renderCounts(counts) {
2158
+ this.titleCountSpan.textContent = `(${counts.all})`;
2159
+ for (const [tab, badge] of this.tabBadgeElements.entries()) {
2160
+ if (tab === 'device') {
2161
+ badge.style.display = 'none';
2162
+ }
2163
+ else {
2164
+ const count = counts[tab] || 0;
2165
+ badge.textContent = count > 999 ? '999+' : String(count);
2166
+ }
2167
+ }
2168
+ }
2169
+ renderEventList(events) {
2170
+ if (this.store.getFilter().tab === 'device') {
2171
+ return;
2172
+ }
2173
+ this.eventListContainer.innerHTML = '';
2174
+ if (events.length === 0) {
2175
+ const empty = document.createElement('div');
2176
+ empty.className = 'cd-empty-state';
2177
+ empty.innerHTML = `
2178
+ <svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
2179
+ <span>No events match current filter</span>
2180
+ `;
2181
+ this.eventListContainer.appendChild(empty);
2182
+ return;
2183
+ }
2184
+ // Render newest items at the top
2185
+ for (let i = events.length - 1; i >= 0; i--) {
2186
+ const event = events[i];
2187
+ const row = this.createEventRow(event);
2188
+ this.eventListContainer.appendChild(row);
2189
+ }
2190
+ }
2191
+ createEventRow(event) {
2192
+ const row = document.createElement('div');
2193
+ row.className = 'cd-event-row';
2194
+ row.setAttribute('data-level', event.level || 'info');
2195
+ const dot = document.createElement('div');
2196
+ dot.className = 'cd-event-level-dot';
2197
+ const time = document.createElement('div');
2198
+ time.className = 'cd-event-time';
2199
+ time.textContent = this.formatTime(event.timestamp);
2200
+ const source = document.createElement('div');
2201
+ source.className = 'cd-event-source';
2202
+ source.textContent = event.source;
2203
+ const main = document.createElement('div');
2204
+ main.className = 'cd-event-main';
2205
+ const type = document.createElement('div');
2206
+ type.className = 'cd-event-type';
2207
+ type.textContent = event.type;
2208
+ main.appendChild(type);
2209
+ if (event.message) {
2210
+ const msg = document.createElement('div');
2211
+ msg.className = 'cd-event-msg';
2212
+ msg.textContent = event.message;
2213
+ main.appendChild(msg);
2214
+ }
2215
+ row.appendChild(dot);
2216
+ row.appendChild(time);
2217
+ row.appendChild(source);
2218
+ row.appendChild(main);
2219
+ if (event.duration !== undefined) {
2220
+ const duration = document.createElement('div');
2221
+ duration.className = 'cd-event-duration';
2222
+ duration.textContent = `${event.duration}ms`;
2223
+ row.appendChild(duration);
2224
+ }
2225
+ row.addEventListener('click', () => {
2226
+ this.showDetail(event);
2227
+ });
2228
+ return row;
2229
+ }
2230
+ buildDetailOverlay() {
2231
+ this.detailOverlay = document.createElement('div');
2232
+ this.detailOverlay.className = 'cd-detail-overlay';
2233
+ const header = document.createElement('div');
2234
+ header.className = 'cd-detail-header';
2235
+ const backBtn = document.createElement('button');
2236
+ backBtn.className = 'cd-detail-back';
2237
+ backBtn.innerHTML = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="19" y1="12" x2="5" y2="12"/><polyline points="12 19 5 12 12 5"/></svg> Back`;
2238
+ backBtn.addEventListener('click', () => this.closeDetail());
2239
+ const copyAllBtn = document.createElement('button');
2240
+ copyAllBtn.className = 'cd-copy-btn-sm';
2241
+ copyAllBtn.textContent = 'Copy JSON';
2242
+ copyAllBtn.addEventListener('click', async () => {
2243
+ if (this.selectedEvent) {
2244
+ await JsonViewer.copyToClipboard(JSON.stringify(this.selectedEvent, null, 2), copyAllBtn);
2245
+ }
2246
+ });
2247
+ header.appendChild(backBtn);
2248
+ header.appendChild(copyAllBtn);
2249
+ this.detailOverlay.appendChild(header);
2250
+ const body = document.createElement('div');
2251
+ body.className = 'cd-detail-body';
2252
+ this.detailOverlay.appendChild(body);
2253
+ this.contentContainer.appendChild(this.detailOverlay);
2254
+ }
2255
+ showDetail(event) {
2256
+ this.selectedEvent = event;
2257
+ const body = this.detailOverlay.querySelector('.cd-detail-body');
2258
+ body.innerHTML = '';
2259
+ // 1. Meta Grid
2260
+ const metaGrid = document.createElement('div');
2261
+ metaGrid.className = 'cd-meta-grid';
2262
+ const addMeta = (label, value) => {
2263
+ if (value === undefined)
2264
+ return;
2265
+ const item = document.createElement('div');
2266
+ item.className = 'cd-meta-item';
2267
+ item.innerHTML = `
2268
+ <span class="cd-meta-label">${label}</span>
2269
+ <span class="cd-meta-val">${value}</span>
2270
+ `;
2271
+ metaGrid.appendChild(item);
2272
+ };
2273
+ addMeta('Type', event.type);
2274
+ addMeta('Source', event.source);
2275
+ addMeta('Level', event.level);
2276
+ addMeta('Time', `${this.formatTime(event.timestamp)} (${new Date(event.timestamp).toLocaleTimeString()})`);
2277
+ addMeta('ID', event.id);
2278
+ if (event.duration !== undefined)
2279
+ addMeta('Duration', `${event.duration}ms`);
2280
+ if (event.platform)
2281
+ addMeta('Platform', event.platform);
2282
+ body.appendChild(metaGrid);
2283
+ // 2. Message
2284
+ if (event.message) {
2285
+ const msgSection = document.createElement('div');
2286
+ msgSection.innerHTML = `<div class="cd-section-title">Message</div>`;
2287
+ const msgBlock = document.createElement('div');
2288
+ msgBlock.className = 'cd-msg-block';
2289
+ msgBlock.textContent = event.message;
2290
+ msgSection.appendChild(msgBlock);
2291
+ body.appendChild(msgSection);
2292
+ }
2293
+ // 3. Data Payload / JSON Viewer
2294
+ if (event.data !== undefined) {
2295
+ const dataSection = document.createElement('div');
2296
+ dataSection.innerHTML = `<div class="cd-section-title">Data Payload</div>`;
2297
+ const jsonTree = JsonViewer.render(event.data, 3);
2298
+ dataSection.appendChild(jsonTree);
2299
+ body.appendChild(dataSection);
2300
+ }
2301
+ this.detailOverlay.classList.add('open');
2302
+ }
2303
+ closeDetail() {
2304
+ this.selectedEvent = null;
2305
+ this.detailOverlay.classList.remove('open');
2306
+ }
2307
+ async loadDeviceDiagnostics() {
2308
+ if (this.isDeviceLoading)
2309
+ return;
2310
+ this.isDeviceLoading = true;
2311
+ this.deviceViewContainer.innerHTML = `
2312
+ <div class="cd-empty-state">
2313
+ <span>Querying native device diagnostics...</span>
2314
+ </div>
2315
+ `;
2316
+ try {
2317
+ const [device, memory, webView, app, network] = await Promise.allSettled([
2318
+ CapDebugNative.getDeviceInfo(),
2319
+ CapDebugNative.getMemoryInfo(),
2320
+ CapDebugNative.getWebViewInfo(),
2321
+ CapDebugNative.getAppInfo(),
2322
+ CapDebugNative.getNetworkInfo(),
2323
+ ]);
2324
+ this.deviceData = {
2325
+ device: device.status === 'fulfilled' ? device.value : { error: String(device.reason) },
2326
+ memory: memory.status === 'fulfilled' ? memory.value : { error: String(memory.reason) },
2327
+ webView: webView.status === 'fulfilled' ? webView.value : { error: String(webView.reason) },
2328
+ app: app.status === 'fulfilled' ? app.value : { error: String(app.reason) },
2329
+ network: network.status === 'fulfilled' ? network.value : { error: String(network.reason) },
2330
+ };
2331
+ // Emit native diagnostics event into the store as well
2332
+ this.store.add({
2333
+ source: 'native',
2334
+ type: 'device.diagnostics',
2335
+ level: 'info',
2336
+ message: 'Device diagnostics loaded',
2337
+ data: this.deviceData,
2338
+ });
2339
+ this.renderDeviceView();
2340
+ }
2341
+ catch (err) {
2342
+ this.deviceViewContainer.innerHTML = `
2343
+ <div class="cd-empty-state">
2344
+ <span style="color: var(--cd-color-error)">Failed to load diagnostics: ${err?.message || String(err)}</span>
2345
+ </div>
2346
+ `;
2347
+ }
2348
+ finally {
2349
+ this.isDeviceLoading = false;
2350
+ }
2351
+ }
2352
+ renderDeviceView() {
2353
+ if (!this.deviceData)
2354
+ return;
2355
+ this.deviceViewContainer.innerHTML = '';
2356
+ // Refresh action bar
2357
+ const topBar = document.createElement('div');
2358
+ topBar.style.display = 'flex';
2359
+ topBar.style.justifyContent = 'space-between';
2360
+ topBar.style.alignItems = 'center';
2361
+ const infoLabel = document.createElement('span');
2362
+ infoLabel.style.fontSize = '11px';
2363
+ infoLabel.style.color = 'var(--cd-text-muted)';
2364
+ infoLabel.textContent = `Diagnostic snapshot: ${new Date().toLocaleTimeString()}`;
2365
+ const refreshBtn = document.createElement('button');
2366
+ refreshBtn.className = 'cd-copy-btn-sm';
2367
+ refreshBtn.textContent = '↻ Refresh Live Data';
2368
+ refreshBtn.addEventListener('click', () => this.loadDeviceDiagnostics());
2369
+ topBar.appendChild(infoLabel);
2370
+ topBar.appendChild(refreshBtn);
2371
+ this.deviceViewContainer.appendChild(topBar);
2372
+ const sections = [
2373
+ { title: 'Device Information', data: this.deviceData.device },
2374
+ { title: 'Memory & Heap', data: this.deviceData.memory },
2375
+ { title: 'Application Info', data: this.deviceData.app },
2376
+ { title: 'WebView & User Agent', data: this.deviceData.webView },
2377
+ { title: 'Network State', data: this.deviceData.network },
2378
+ ];
2379
+ for (const { title, data } of sections) {
2380
+ const card = document.createElement('div');
2381
+ card.className = 'cd-device-card';
2382
+ const cardHeader = document.createElement('div');
2383
+ cardHeader.className = 'cd-device-card-header';
2384
+ cardHeader.textContent = title;
2385
+ card.appendChild(cardHeader);
2386
+ if (typeof data === 'object' && data !== null) {
2387
+ const grid = document.createElement('div');
2388
+ grid.className = 'cd-device-grid';
2389
+ for (const [k, v] of Object.entries(data)) {
2390
+ if (typeof v === 'object' && v !== null)
2391
+ continue;
2392
+ const item = document.createElement('div');
2393
+ item.className = 'cd-meta-item';
2394
+ item.innerHTML = `
2395
+ <span class="cd-meta-label">${k}</span>
2396
+ <span class="cd-meta-val">${v !== undefined && v !== null ? String(v) : '—'}</span>
2397
+ `;
2398
+ grid.appendChild(item);
2399
+ }
2400
+ card.appendChild(grid);
2401
+ // Render nested objects if present
2402
+ for (const [k, v] of Object.entries(data)) {
2403
+ if (typeof v === 'object' && v !== null) {
2404
+ const nestedSection = document.createElement('div');
2405
+ nestedSection.innerHTML = `<div style="font-size: 10px; color: var(--cd-text-muted); font-weight: 600; margin-top: 6px;">${k.toUpperCase()}</div>`;
2406
+ nestedSection.appendChild(JsonViewer.render(v, 2));
2407
+ card.appendChild(nestedSection);
2408
+ }
2409
+ }
2410
+ }
2411
+ else {
2412
+ card.textContent = String(data);
2413
+ }
2414
+ this.deviceViewContainer.appendChild(card);
2415
+ }
2416
+ }
2417
+ formatTime(timestamp) {
2418
+ const d = new Date(timestamp);
2419
+ const h = String(d.getHours()).padStart(2, '0');
2420
+ const m = String(d.getMinutes()).padStart(2, '0');
2421
+ const s = String(d.getSeconds()).padStart(2, '0');
2422
+ const ms = String(d.getMilliseconds()).padStart(3, '0');
2423
+ return `${h}:${m}:${s}.${ms}`;
2424
+ }
2425
+ }
2426
+
2427
+ class CapDebugViewerElement extends HTMLElement {
2428
+ static TAG = 'cap-debug-viewer';
2429
+ shadowRootNode;
2430
+ floatingButton = null;
2431
+ debugPanel = null;
2432
+ store = null;
2433
+ unsubscribeStore = null;
2434
+ constructor() {
2435
+ super();
2436
+ this.shadowRootNode = this.attachShadow({ mode: 'open' });
2437
+ }
2438
+ getStore() {
2439
+ return this.store;
2440
+ }
2441
+ init(store, options = {}) {
2442
+ this.store = store;
2443
+ // Inject isolated styles
2444
+ const styleEl = document.createElement('style');
2445
+ styleEl.textContent = VIEWER_STYLES;
2446
+ this.shadowRootNode.appendChild(styleEl);
2447
+ // Create Panel
2448
+ this.debugPanel = new DebugPanel(store, {
2449
+ defaultTab: options.defaultTab || 'all',
2450
+ onClose: () => {
2451
+ // Can handle panel close events if needed
2452
+ },
2453
+ });
2454
+ this.shadowRootNode.appendChild(this.debugPanel.getBackdrop());
2455
+ this.shadowRootNode.appendChild(this.debugPanel.getElement());
2456
+ // Create Floating Button (if enabled)
2457
+ if (options.button !== false) {
2458
+ this.floatingButton = new FloatingButton({
2459
+ position: options.position,
2460
+ persistPosition: options.persistPosition !== false,
2461
+ onClick: () => {
2462
+ this.toggle();
2463
+ },
2464
+ });
2465
+ this.shadowRootNode.appendChild(this.floatingButton.getElement());
2466
+ }
2467
+ // Subscribe to error counts for the button badge
2468
+ this.unsubscribeStore = store.subscribe((_events, s) => {
2469
+ const counts = s.getCounts();
2470
+ if (this.floatingButton) {
2471
+ this.floatingButton.setErrorCount(counts.errors);
2472
+ }
2473
+ });
2474
+ }
2475
+ show() {
2476
+ if (this.debugPanel) {
2477
+ this.debugPanel.open();
2478
+ }
2479
+ }
2480
+ hide() {
2481
+ if (this.debugPanel) {
2482
+ this.debugPanel.close();
2483
+ }
2484
+ }
2485
+ toggle() {
2486
+ if (this.debugPanel) {
2487
+ return this.debugPanel.toggle();
2488
+ }
2489
+ return false;
2490
+ }
2491
+ showButton() {
2492
+ if (this.floatingButton) {
2493
+ this.floatingButton.show();
2494
+ }
2495
+ }
2496
+ hideButton() {
2497
+ if (this.floatingButton) {
2498
+ this.floatingButton.hide();
2499
+ }
2500
+ }
2501
+ disconnectedCallback() {
2502
+ if (this.unsubscribeStore) {
2503
+ this.unsubscribeStore();
2504
+ this.unsubscribeStore = null;
2505
+ }
2506
+ }
2507
+ }
2508
+ function registerCustomElement() {
2509
+ if (typeof customElements !== 'undefined' && !customElements.get(CapDebugViewerElement.TAG)) {
2510
+ customElements.define(CapDebugViewerElement.TAG, CapDebugViewerElement);
2511
+ }
2512
+ }
2513
+
2514
+ class CapDebugManager {
2515
+ store;
2516
+ viewerElement = null;
2517
+ isMounted = false;
2518
+ customEventCollector;
2519
+ consoleCollector;
2520
+ errorCollector;
2521
+ networkCollector;
2522
+ browserEventCollector;
2523
+ bridgeCollector;
2524
+ constructor() {
2525
+ this.store = new CapDebugStore();
2526
+ this.customEventCollector = new CustomEventCollector(this.store);
2527
+ this.consoleCollector = new ConsoleCollector();
2528
+ this.errorCollector = new ErrorCollector();
2529
+ this.networkCollector = new NetworkCollector();
2530
+ this.browserEventCollector = new BrowserEventCollector();
2531
+ this.bridgeCollector = new BridgeCollector();
2532
+ }
2533
+ get native() {
2534
+ return CapDebugNative;
2535
+ }
2536
+ get currentStore() {
2537
+ return this.store;
2538
+ }
2539
+ /**
2540
+ * Mounts the CapDebug UI into the DOM and activates collectors.
2541
+ */
2542
+ mount(options = {}) {
2543
+ if (typeof window === 'undefined' || typeof document === 'undefined') {
2544
+ return;
2545
+ }
2546
+ if (this.isMounted) {
2547
+ this.unmount();
2548
+ }
2549
+ // Configure store
2550
+ if (options.maxEvents) {
2551
+ this.store.setMaxEvents(options.maxEvents);
2552
+ }
2553
+ if (options.customMaskKeys) {
2554
+ this.store.setCustomMaskKeys(options.customMaskKeys);
2555
+ }
2556
+ // 1. Start core CustomEvent bus listener
2557
+ this.customEventCollector.start();
2558
+ // 2. Start optional collectors
2559
+ if (options.console !== false) {
2560
+ this.consoleCollector.start();
2561
+ }
2562
+ if (options.errors !== false) {
2563
+ this.errorCollector.start();
2564
+ }
2565
+ if (options.network) {
2566
+ const netOpts = typeof options.network === 'object' ? options.network : {};
2567
+ this.networkCollector.setOptions(netOpts);
2568
+ this.networkCollector.start();
2569
+ }
2570
+ if (options.bridge) {
2571
+ this.bridgeCollector.start();
2572
+ }
2573
+ this.browserEventCollector.start();
2574
+ // 3. Register and create Web Component
2575
+ registerCustomElement();
2576
+ this.viewerElement = document.createElement(CapDebugViewerElement.TAG);
2577
+ this.viewerElement.init(this.store, options);
2578
+ document.body.appendChild(this.viewerElement);
2579
+ this.isMounted = true;
2580
+ }
2581
+ /**
2582
+ * Unmounts CapDebug UI and stops all collectors.
2583
+ */
2584
+ unmount() {
2585
+ if (!this.isMounted)
2586
+ return;
2587
+ if (this.viewerElement && this.viewerElement.parentNode) {
2588
+ this.viewerElement.parentNode.removeChild(this.viewerElement);
2589
+ this.viewerElement = null;
2590
+ }
2591
+ this.customEventCollector.stop();
2592
+ this.consoleCollector.stop();
2593
+ this.errorCollector.stop();
2594
+ this.networkCollector.stop();
2595
+ this.browserEventCollector.stop();
2596
+ this.bridgeCollector.stop();
2597
+ this.isMounted = false;
2598
+ }
2599
+ /**
2600
+ * Opens the debug viewer drawer.
2601
+ */
2602
+ show() {
2603
+ if (this.viewerElement) {
2604
+ this.viewerElement.show();
2605
+ }
2606
+ }
2607
+ /**
2608
+ * Closes the debug viewer drawer.
2609
+ */
2610
+ hide() {
2611
+ if (this.viewerElement) {
2612
+ this.viewerElement.hide();
2613
+ }
2614
+ }
2615
+ /**
2616
+ * Toggles the debug viewer drawer open/closed.
2617
+ */
2618
+ toggle() {
2619
+ if (this.viewerElement) {
2620
+ return this.viewerElement.toggle();
2621
+ }
2622
+ return false;
2623
+ }
2624
+ /**
2625
+ * Dispatches a debug event through the CustomEvent bus.
2626
+ */
2627
+ emit(event) {
2628
+ if (typeof window === 'undefined')
2629
+ return;
2630
+ window.dispatchEvent(new CustomEvent('capdebug', {
2631
+ detail: {
2632
+ id: event.id,
2633
+ timestamp: event.timestamp || Date.now(),
2634
+ source: event.source || 'web',
2635
+ type: event.type,
2636
+ level: event.level || 'info',
2637
+ message: event.message,
2638
+ data: event.data,
2639
+ duration: event.duration,
2640
+ platform: event.platform,
2641
+ },
2642
+ }));
2643
+ }
2644
+ /**
2645
+ * Clears all recorded events from the store.
2646
+ */
2647
+ clear() {
2648
+ this.store.clear();
2649
+ }
2650
+ /**
2651
+ * Pauses capturing new events.
2652
+ */
2653
+ pause() {
2654
+ this.store.pause();
2655
+ }
2656
+ /**
2657
+ * Resumes capturing events.
2658
+ */
2659
+ resume() {
2660
+ this.store.resume();
2661
+ }
2662
+ captureConsole() {
2663
+ this.consoleCollector.start();
2664
+ }
2665
+ releaseConsole() {
2666
+ this.consoleCollector.stop();
2667
+ }
2668
+ captureErrors() {
2669
+ this.errorCollector.start();
2670
+ }
2671
+ releaseErrors() {
2672
+ this.errorCollector.stop();
2673
+ }
2674
+ captureFetch(options) {
2675
+ if (options) {
2676
+ this.networkCollector.setOptions(options);
2677
+ }
2678
+ this.networkCollector.start();
2679
+ }
2680
+ releaseFetch() {
2681
+ this.networkCollector.stop();
2682
+ }
2683
+ observeEvent(eventName) {
2684
+ this.browserEventCollector.observe(eventName);
2685
+ }
2686
+ unobserveEvent(eventName) {
2687
+ this.browserEventCollector.unobserve(eventName);
2688
+ }
2689
+ }
2690
+ const CapDebug = new CapDebugManager();
2691
+
2692
+ function formatBytes(bytes) {
2693
+ if (bytes === 0)
2694
+ return '0 B';
2695
+ const k = 1024;
2696
+ const sizes = ['B', 'KB', 'MB', 'GB'];
2697
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
2698
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
2699
+ }
2700
+ class CapDebugWeb extends core.WebPlugin {
2701
+ async getDeviceInfo() {
2702
+ const nav = typeof navigator !== 'undefined' ? navigator : {};
2703
+ return {
2704
+ manufacturer: 'Web Browser',
2705
+ model: nav.userAgentData?.brands?.[0]?.brand || nav.appName || 'Browser',
2706
+ device: nav.platform || 'Web',
2707
+ systemVersion: nav.appVersion || 'Unknown',
2708
+ architecture: nav.userAgentData?.architecture || 'Unknown',
2709
+ availableProcessors: nav.hardwareConcurrency || 1,
2710
+ isEmulator: false,
2711
+ platform: 'web',
2712
+ };
2713
+ }
2714
+ async getMemoryInfo() {
2715
+ const perf = typeof window !== 'undefined' ? window.performance : null;
2716
+ const memory = perf && perf.memory ? perf.memory : null;
2717
+ if (memory) {
2718
+ return {
2719
+ appMemoryUsage: memory.usedJSHeapSize,
2720
+ appMemoryUsageFormatted: formatBytes(memory.usedJSHeapSize),
2721
+ totalHeap: memory.totalJSHeapSize,
2722
+ totalHeapFormatted: formatBytes(memory.totalJSHeapSize),
2723
+ maxHeap: memory.jsHeapSizeLimit,
2724
+ maxHeapFormatted: formatBytes(memory.jsHeapSizeLimit),
2725
+ freeHeap: memory.totalJSHeapSize - memory.usedJSHeapSize,
2726
+ freeHeapFormatted: formatBytes(memory.totalJSHeapSize - memory.usedJSHeapSize),
2727
+ lowMemory: false,
2728
+ };
2729
+ }
2730
+ return {
2731
+ appMemoryUsageFormatted: 'N/A (Browser non-Chrome)',
2732
+ lowMemory: false,
2733
+ };
2734
+ }
2735
+ async getWebViewInfo() {
2736
+ const nav = typeof navigator !== 'undefined' ? navigator : {};
2737
+ return {
2738
+ userAgent: nav.userAgent || 'Unknown',
2739
+ version: nav.appVersion || 'Unknown',
2740
+ packageName: typeof window !== 'undefined' ? window.location.hostname : 'localhost',
2741
+ debugEnabled: true,
2742
+ };
2743
+ }
2744
+ async getAppInfo() {
2745
+ return {
2746
+ applicationId: typeof window !== 'undefined' ? window.location.host : 'localhost',
2747
+ version: '1.0.0',
2748
+ versionName: '1.0.0 (Web)',
2749
+ versionCode: 1,
2750
+ buildType: typeof process !== 'undefined' && process.env?.NODE_ENV ? process.env.NODE_ENV : 'development',
2751
+ };
2752
+ }
2753
+ async getNetworkInfo() {
2754
+ const nav = typeof navigator !== 'undefined' ? navigator : {};
2755
+ const conn = nav.connection || nav.mozConnection || nav.webkitConnection;
2756
+ return {
2757
+ connected: nav.onLine ?? true,
2758
+ connectionType: conn ? conn.effectiveType || conn.type || 'unknown' : (nav.onLine ? 'online' : 'offline'),
2759
+ metered: conn ? conn.saveData ?? false : false,
2760
+ };
2761
+ }
2762
+ }
2763
+
2764
+ var web = /*#__PURE__*/Object.freeze({
2765
+ __proto__: null,
2766
+ CapDebugWeb: CapDebugWeb
2767
+ });
2768
+
2769
+ exports.BridgeCollector = BridgeCollector;
2770
+ exports.BrowserEventCollector = BrowserEventCollector;
2771
+ exports.CapDebug = CapDebug;
2772
+ exports.CapDebugManager = CapDebugManager;
2773
+ exports.CapDebugNative = CapDebugNative;
2774
+ exports.CapDebugStore = CapDebugStore;
2775
+ exports.CapDebugViewerElement = CapDebugViewerElement;
2776
+ exports.ConsoleCollector = ConsoleCollector;
2777
+ exports.CustomEventCollector = CustomEventCollector;
2778
+ exports.DataSanitizer = DataSanitizer;
2779
+ exports.ErrorCollector = ErrorCollector;
2780
+ exports.JsonViewer = JsonViewer;
2781
+ exports.NetworkCollector = NetworkCollector;
2782
+ exports.default = CapDebug;
2783
+ exports.defaultSanitizer = defaultSanitizer;
2784
+ exports.registerCustomElement = registerCustomElement;
2785
+ exports.sanitize = sanitize;
2786
+ //# sourceMappingURL=plugin.cjs.js.map