bee-ladybug 0.5.4 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.MD +17 -1
- package/package.json +1 -1
- package/src/adapters/AntAdapter.js +52 -1
- package/src/adapters/CSSAdapter.js +131 -3
- package/src/adapters/CanvasAdapter.js +87 -1
- package/src/adapters/SpiderAdapter.js +53 -2
- package/src/adapters/WebDOMAdapter.js +209 -1
- package/src/adapters/python/PythonBridge.js +74 -2
- package/src/core/BeeLadybugCore.js +48 -4
- package/src/core/ReportExporter.js +695 -0
- package/src/core/UIOverlay.js +3 -1
- package/src/index.d.ts +89 -0
- package/src/index.js +1 -0
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
* Reads box models, the node under the pointer, and mutation noise,
|
|
5
5
|
* then pushes packets into the core. Highlights are drawn on a stacked
|
|
6
6
|
* overlay canvas so the page stays untouched.
|
|
7
|
+
* `snapshot()` hands the report the box model, problem nodes, and
|
|
8
|
+
* the latest mutation counts.
|
|
7
9
|
*
|
|
8
10
|
* The core must never import this file.
|
|
9
11
|
*
|
|
@@ -12,6 +14,10 @@
|
|
|
12
14
|
* adapter.attach();
|
|
13
15
|
*/
|
|
14
16
|
|
|
17
|
+
import { isReportNoise } from '../core/ReportExporter.js';
|
|
18
|
+
|
|
19
|
+
const MAX_PROBLEM_ROWS = 40;
|
|
20
|
+
|
|
15
21
|
export const WEB_DOM_ADAPTER_DEFAULTS = Object.freeze({
|
|
16
22
|
overlay: true,
|
|
17
23
|
autoTick: true,
|
|
@@ -52,6 +58,7 @@ export class WebDOMAdapter {
|
|
|
52
58
|
this.hover = null;
|
|
53
59
|
|
|
54
60
|
this.#attached = false;
|
|
61
|
+
this.#reporting = true;
|
|
55
62
|
this.#overlay = null;
|
|
56
63
|
this.#unsubscribe = null;
|
|
57
64
|
this.#observer = null;
|
|
@@ -62,6 +69,9 @@ export class WebDOMAdapter {
|
|
|
62
69
|
this.#mutAdded = 0;
|
|
63
70
|
this.#mutRemoved = 0;
|
|
64
71
|
this.#mutAttr = 0;
|
|
72
|
+
this.#seenAdded = 0;
|
|
73
|
+
this.#seenRemoved = 0;
|
|
74
|
+
this.#seenAttr = 0;
|
|
65
75
|
this.#mutTimer = 0;
|
|
66
76
|
this.#lastHoverKey = '';
|
|
67
77
|
this.#onMove = (event) => {
|
|
@@ -78,6 +88,7 @@ export class WebDOMAdapter {
|
|
|
78
88
|
}
|
|
79
89
|
|
|
80
90
|
#attached;
|
|
91
|
+
#reporting;
|
|
81
92
|
#overlay;
|
|
82
93
|
#unsubscribe;
|
|
83
94
|
#observer;
|
|
@@ -88,6 +99,9 @@ export class WebDOMAdapter {
|
|
|
88
99
|
#mutAdded;
|
|
89
100
|
#mutRemoved;
|
|
90
101
|
#mutAttr;
|
|
102
|
+
#seenAdded;
|
|
103
|
+
#seenRemoved;
|
|
104
|
+
#seenAttr;
|
|
91
105
|
#mutTimer;
|
|
92
106
|
#lastHoverKey;
|
|
93
107
|
#onMove;
|
|
@@ -123,6 +137,15 @@ export class WebDOMAdapter {
|
|
|
123
137
|
this.#rafId = requestAnimationFrame(this.#onTick);
|
|
124
138
|
}
|
|
125
139
|
|
|
140
|
+
this.#reporting = true;
|
|
141
|
+
this.core?.registerAdapter('dom', {
|
|
142
|
+
label: 'DOM',
|
|
143
|
+
enabled: true,
|
|
144
|
+
enable: () => this.#enable(),
|
|
145
|
+
disable: () => this.#disable(),
|
|
146
|
+
snapshot: () => this.snapshot()
|
|
147
|
+
});
|
|
148
|
+
|
|
126
149
|
this.core?.sendData('log', {
|
|
127
150
|
source: this.source,
|
|
128
151
|
message: 'WebDOMAdapter attached. Hover a node to inspect the box model.'
|
|
@@ -148,6 +171,7 @@ export class WebDOMAdapter {
|
|
|
148
171
|
this.#rafId = 0;
|
|
149
172
|
if (this.#mutTimer && typeof clearTimeout === 'function') clearTimeout(this.#mutTimer);
|
|
150
173
|
this.#mutTimer = 0;
|
|
174
|
+
this.core?.unregisterAdapter('dom');
|
|
151
175
|
if (typeof window !== 'undefined') {
|
|
152
176
|
window.removeEventListener('mousemove', this.#onMove, true);
|
|
153
177
|
window.removeEventListener('resize', this.#onResize);
|
|
@@ -172,8 +196,36 @@ export class WebDOMAdapter {
|
|
|
172
196
|
return this;
|
|
173
197
|
}
|
|
174
198
|
|
|
199
|
+
/**
|
|
200
|
+
* Box model of the hover and watched nodes, layout problems under root,
|
|
201
|
+
* and the latest mutation counts. Returns null when detached.
|
|
202
|
+
* Hidden boxes, the WordPress admin bar, and BeeLadybug chrome are skipped.
|
|
203
|
+
* The core stores this function and does not read the rows.
|
|
204
|
+
* @returns {{ name: string, items: Array<{ severity: string, selector: string, rule: string, value: string, source: string }> } | null}
|
|
205
|
+
*/
|
|
206
|
+
snapshot() {
|
|
207
|
+
if (!this.#attached) return null;
|
|
208
|
+
const items = [];
|
|
209
|
+
const seen = new Set();
|
|
210
|
+
const targets = [];
|
|
211
|
+
if (this.hover) targets.push(this.hover);
|
|
212
|
+
const watched = this.#watchedNodes();
|
|
213
|
+
for (let i = 0; i < watched.length; i++) targets.push(watched[i]);
|
|
214
|
+
for (let i = 0; i < targets.length; i++) {
|
|
215
|
+
const node = targets[i];
|
|
216
|
+
if (!node || seen.has(node) || !reportable(node)) continue;
|
|
217
|
+
seen.add(node);
|
|
218
|
+
items.push(boxItem(node, this.source));
|
|
219
|
+
}
|
|
220
|
+
const problems = problemItems(this.root, this.source);
|
|
221
|
+
for (let i = 0; i < problems.length; i++) items.push(problems[i]);
|
|
222
|
+
items.push(mutationItem(this.#mutationCounts(), this.root, this.source));
|
|
223
|
+
return { name: 'dom', items };
|
|
224
|
+
}
|
|
225
|
+
|
|
175
226
|
pump() {
|
|
176
227
|
if (!this.#attached || !this.core) return this;
|
|
228
|
+
if (!this.#reporting) return this;
|
|
177
229
|
|
|
178
230
|
const now = typeof performance !== 'undefined' ? performance.now() : Date.now();
|
|
179
231
|
if (this.#lastPump) {
|
|
@@ -311,6 +363,11 @@ export class WebDOMAdapter {
|
|
|
311
363
|
const added = this.#mutAdded;
|
|
312
364
|
const removed = this.#mutRemoved;
|
|
313
365
|
const attr = this.#mutAttr;
|
|
366
|
+
if (added || removed || attr) {
|
|
367
|
+
this.#seenAdded = added;
|
|
368
|
+
this.#seenRemoved = removed;
|
|
369
|
+
this.#seenAttr = attr;
|
|
370
|
+
}
|
|
314
371
|
this.#mutAdded = 0;
|
|
315
372
|
this.#mutRemoved = 0;
|
|
316
373
|
this.#mutAttr = 0;
|
|
@@ -394,7 +451,26 @@ export class WebDOMAdapter {
|
|
|
394
451
|
canvas.style.height = `${h}px`;
|
|
395
452
|
canvas.width = Math.round(w * dpr);
|
|
396
453
|
canvas.height = Math.round(h * dpr);
|
|
397
|
-
|
|
454
|
+
const hidden = !this.#reporting || (this.core && this.core.visible === false);
|
|
455
|
+
canvas.style.display = hidden ? 'none' : 'block';
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
#enable() {
|
|
459
|
+
this.#reporting = true;
|
|
460
|
+
this.#syncOverlay();
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
#disable() {
|
|
464
|
+
this.#reporting = false;
|
|
465
|
+
this.#syncOverlay();
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
#mutationCounts() {
|
|
469
|
+
const pending = this.#mutAdded + this.#mutRemoved + this.#mutAttr;
|
|
470
|
+
if (pending) {
|
|
471
|
+
return { added: this.#mutAdded, removed: this.#mutRemoved, attr: this.#mutAttr };
|
|
472
|
+
}
|
|
473
|
+
return { added: this.#seenAdded, removed: this.#seenRemoved, attr: this.#seenAttr };
|
|
398
474
|
}
|
|
399
475
|
}
|
|
400
476
|
|
|
@@ -504,6 +580,138 @@ function drawLabel(ctx, el, viewW, viewH, color) {
|
|
|
504
580
|
ctx.fillText(text, x + 6, y + height / 2);
|
|
505
581
|
}
|
|
506
582
|
|
|
583
|
+
function reportable(node) {
|
|
584
|
+
if (!node || node.nodeType !== 1) return false;
|
|
585
|
+
if (typeof node.getBoundingClientRect !== 'function') return false;
|
|
586
|
+
if (isBeeChrome(node) || isReportNoise(node)) return false;
|
|
587
|
+
return true;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
function boxItem(node, source) {
|
|
591
|
+
const model = boxModel(node);
|
|
592
|
+
return {
|
|
593
|
+
severity: 'ok',
|
|
594
|
+
selector: cssPath(node),
|
|
595
|
+
rule: 'box',
|
|
596
|
+
value: boxValue(model, styleOf(node)),
|
|
597
|
+
source
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
function boxValue(model, style) {
|
|
602
|
+
const border = model.border;
|
|
603
|
+
const content = model.content;
|
|
604
|
+
return `border ${Math.round(border.w)}x${Math.round(border.h)} margin ${sideText(style, 'margin')} padding ${sideText(style, 'padding')} content ${Math.round(content.w)}x${Math.round(content.h)} ${model.display}`;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
function sideText(style, kind) {
|
|
608
|
+
const prefix = kind === 'margin' ? 'margin' : 'padding';
|
|
609
|
+
const top = style ? parsePx(style[`${prefix}Top`]) : 0;
|
|
610
|
+
const right = style ? parsePx(style[`${prefix}Right`]) : 0;
|
|
611
|
+
const bottom = style ? parsePx(style[`${prefix}Bottom`]) : 0;
|
|
612
|
+
const left = style ? parsePx(style[`${prefix}Left`]) : 0;
|
|
613
|
+
return `${top},${right},${bottom},${left}`;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
function problemItems(root, source) {
|
|
617
|
+
const nodes = collectElements(root);
|
|
618
|
+
const items = [];
|
|
619
|
+
const seen = new Set();
|
|
620
|
+
for (let i = 0; i < nodes.length && items.length < MAX_PROBLEM_ROWS; i++) {
|
|
621
|
+
const node = nodes[i];
|
|
622
|
+
if (!node || seen.has(node) || !reportable(node)) continue;
|
|
623
|
+
seen.add(node);
|
|
624
|
+
const style = styleOf(node);
|
|
625
|
+
const margin = sideText(style, 'margin');
|
|
626
|
+
if (hasNegative(margin)) {
|
|
627
|
+
items.push(problemItem(node, 'negative margin', margin, source));
|
|
628
|
+
if (items.length >= MAX_PROBLEM_ROWS) break;
|
|
629
|
+
}
|
|
630
|
+
const clip = clipValue(node);
|
|
631
|
+
if (clip) {
|
|
632
|
+
items.push(problemItem(node, 'clipped', clip, source));
|
|
633
|
+
if (items.length >= MAX_PROBLEM_ROWS) break;
|
|
634
|
+
}
|
|
635
|
+
const model = boxModel(node);
|
|
636
|
+
if (model.border.w > 0 && model.border.h > 0 && (model.content.w === 0 || model.content.h === 0)) {
|
|
637
|
+
items.push(problemItem(
|
|
638
|
+
node,
|
|
639
|
+
'collapsed content',
|
|
640
|
+
`content ${Math.round(model.content.w)}x${Math.round(model.content.h)}`,
|
|
641
|
+
source
|
|
642
|
+
));
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
return items;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
function problemItem(node, rule, value, source) {
|
|
649
|
+
return {
|
|
650
|
+
severity: 'warn',
|
|
651
|
+
selector: cssPath(node),
|
|
652
|
+
rule,
|
|
653
|
+
value,
|
|
654
|
+
source
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
function hasNegative(sides) {
|
|
659
|
+
return sides.split(',').some((part) => Number(part) < 0);
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
function clipValue(node) {
|
|
663
|
+
const sw = finite(node.scrollWidth);
|
|
664
|
+
const cw = finite(node.clientWidth);
|
|
665
|
+
const sh = finite(node.scrollHeight);
|
|
666
|
+
const ch = finite(node.clientHeight);
|
|
667
|
+
const wide = sw != null && cw != null && sw > cw + 2;
|
|
668
|
+
const tall = sh != null && ch != null && sh > ch + 2;
|
|
669
|
+
if (!wide && !tall) return '';
|
|
670
|
+
return `scroll ${sw ?? 0}x${sh ?? 0} client ${cw ?? 0}x${ch ?? 0}`;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
function mutationItem(counts, root, source) {
|
|
674
|
+
const total = counts.added + counts.removed + counts.attr;
|
|
675
|
+
return {
|
|
676
|
+
severity: total > 0 ? 'warn' : 'ok',
|
|
677
|
+
selector: root && root.nodeType === 1 ? cssPath(root) : 'root',
|
|
678
|
+
rule: 'mutations',
|
|
679
|
+
value: `+${counts.added} -${counts.removed} attr ${counts.attr}`,
|
|
680
|
+
source
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
function styleOf(node) {
|
|
685
|
+
if (typeof getComputedStyle !== 'function') return null;
|
|
686
|
+
try {
|
|
687
|
+
return getComputedStyle(node);
|
|
688
|
+
} catch {
|
|
689
|
+
return null;
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
function collectElements(root) {
|
|
694
|
+
if (!root) return [];
|
|
695
|
+
const out = [];
|
|
696
|
+
if (root.nodeType === 1) out.push(root);
|
|
697
|
+
let list = null;
|
|
698
|
+
if (typeof root.querySelectorAll === 'function') {
|
|
699
|
+
try {
|
|
700
|
+
list = root.querySelectorAll('*');
|
|
701
|
+
} catch {
|
|
702
|
+
list = null;
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
if (list) {
|
|
706
|
+
for (let i = 0; i < list.length; i++) out.push(list[i]);
|
|
707
|
+
}
|
|
708
|
+
return out;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
function finite(value) {
|
|
712
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : null;
|
|
713
|
+
}
|
|
714
|
+
|
|
507
715
|
function countElements(root) {
|
|
508
716
|
if (!root) return 0;
|
|
509
717
|
if (root.nodeType === 1) return 1 + (root.getElementsByTagName?.('*').length ?? 0);
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
*
|
|
11
11
|
* ingest() is the same parser used by the socket, so tests and the mock
|
|
12
12
|
* demo do not need a live Python process.
|
|
13
|
+
* `snapshot()` lists the latest metric received for each name.
|
|
13
14
|
*/
|
|
14
15
|
|
|
15
16
|
export const PYTHON_BRIDGE_DEFAULTS = Object.freeze({
|
|
@@ -35,16 +36,27 @@ export class PythonBridge {
|
|
|
35
36
|
|
|
36
37
|
this.#socket = null;
|
|
37
38
|
this.#wanted = false;
|
|
39
|
+
this.#live = true;
|
|
38
40
|
this.#timer = 0;
|
|
39
41
|
this.#packets = 0;
|
|
40
42
|
this.#retries = 0;
|
|
43
|
+
this.#metrics = new Map();
|
|
44
|
+
this.core?.registerAdapter('python', {
|
|
45
|
+
label: 'PYTHON',
|
|
46
|
+
enabled: true,
|
|
47
|
+
enable: () => this.#enable(),
|
|
48
|
+
disable: () => this.#disable(),
|
|
49
|
+
snapshot: () => this.snapshot()
|
|
50
|
+
});
|
|
41
51
|
}
|
|
42
52
|
|
|
43
53
|
#socket;
|
|
44
54
|
#wanted;
|
|
55
|
+
#live;
|
|
45
56
|
#timer;
|
|
46
57
|
#packets;
|
|
47
58
|
#retries;
|
|
59
|
+
#metrics;
|
|
48
60
|
|
|
49
61
|
get connected() {
|
|
50
62
|
return this.status === 'open';
|
|
@@ -53,6 +65,7 @@ export class PythonBridge {
|
|
|
53
65
|
connect(url) {
|
|
54
66
|
if (url) this.url = url;
|
|
55
67
|
this.#wanted = true;
|
|
68
|
+
this.#live = true;
|
|
56
69
|
this.#retries = 0;
|
|
57
70
|
this.#open();
|
|
58
71
|
return this;
|
|
@@ -77,6 +90,25 @@ export class PythonBridge {
|
|
|
77
90
|
return this;
|
|
78
91
|
}
|
|
79
92
|
|
|
93
|
+
/**
|
|
94
|
+
* Latest metric value for each name received through ingest().
|
|
95
|
+
* @returns {{ name: string, items: Array<{ severity: string, selector: string, rule: string, value: string, source: string }> }}
|
|
96
|
+
*/
|
|
97
|
+
snapshot() {
|
|
98
|
+
const items = [];
|
|
99
|
+
for (const [name, metric] of this.#metrics) {
|
|
100
|
+
const unit = metric.unit ? ` ${metric.unit}` : '';
|
|
101
|
+
items.push({
|
|
102
|
+
severity: 'ok',
|
|
103
|
+
selector: name,
|
|
104
|
+
rule: 'metric',
|
|
105
|
+
value: `${metricText(metric.value)}${unit}`,
|
|
106
|
+
source: this.source
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
return { name: 'python', items };
|
|
110
|
+
}
|
|
111
|
+
|
|
80
112
|
/**
|
|
81
113
|
* Parse one wire message (object, JSON string, or array) into sendData().
|
|
82
114
|
* @param {unknown} raw
|
|
@@ -117,6 +149,7 @@ export class PythonBridge {
|
|
|
117
149
|
}
|
|
118
150
|
|
|
119
151
|
const payload = unpackPayload(data, this.source);
|
|
152
|
+
if (type === 'metric') this.#rememberMetric(payload);
|
|
120
153
|
this.#packets += 1;
|
|
121
154
|
this.core.sendData(type, payload);
|
|
122
155
|
this.core.sendData('state', {
|
|
@@ -128,7 +161,7 @@ export class PythonBridge {
|
|
|
128
161
|
}
|
|
129
162
|
|
|
130
163
|
#open() {
|
|
131
|
-
if (!this.#wanted) return;
|
|
164
|
+
if (!this.#wanted || !this.#live) return;
|
|
132
165
|
if (typeof WebSocket === 'undefined') {
|
|
133
166
|
this.core?.sendData('error', {
|
|
134
167
|
source: this.source,
|
|
@@ -205,7 +238,7 @@ export class PythonBridge {
|
|
|
205
238
|
}
|
|
206
239
|
|
|
207
240
|
#scheduleReconnect() {
|
|
208
|
-
if (!this.#wanted || !this.autoReconnect) return;
|
|
241
|
+
if (!this.#wanted || !this.#live || !this.autoReconnect) return;
|
|
209
242
|
this.#clearTimer();
|
|
210
243
|
this.#timer = setTimeout(() => this.#open(), this.reconnectMs);
|
|
211
244
|
}
|
|
@@ -215,6 +248,39 @@ export class PythonBridge {
|
|
|
215
248
|
this.#timer = 0;
|
|
216
249
|
}
|
|
217
250
|
|
|
251
|
+
#enable() {
|
|
252
|
+
this.#live = true;
|
|
253
|
+
if (this.#wanted) this.#open();
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
#disable() {
|
|
257
|
+
this.#live = false;
|
|
258
|
+
this.#clearTimer();
|
|
259
|
+
const socket = this.#socket;
|
|
260
|
+
this.#socket = null;
|
|
261
|
+
if (socket) {
|
|
262
|
+
try {
|
|
263
|
+
socket.close();
|
|
264
|
+
} catch {
|
|
265
|
+
/* already closing */
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
if (this.status !== 'idle') this.#setStatus('closed');
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
#rememberMetric(payload) {
|
|
272
|
+
const name = typeof payload.name === 'string' && payload.name ? payload.name : 'metric';
|
|
273
|
+
if (this.#metrics.has(name)) this.#metrics.delete(name);
|
|
274
|
+
this.#metrics.set(name, {
|
|
275
|
+
value: payload.value,
|
|
276
|
+
unit: typeof payload.unit === 'string' ? payload.unit : ''
|
|
277
|
+
});
|
|
278
|
+
while (this.#metrics.size > 40) {
|
|
279
|
+
const oldest = this.#metrics.keys().next().value;
|
|
280
|
+
this.#metrics.delete(oldest);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
218
284
|
#setStatus(status) {
|
|
219
285
|
this.status = status;
|
|
220
286
|
this.core?.sendData('state', {
|
|
@@ -225,6 +291,12 @@ export class PythonBridge {
|
|
|
225
291
|
}
|
|
226
292
|
}
|
|
227
293
|
|
|
294
|
+
function metricText(value) {
|
|
295
|
+
if (value == null) return '';
|
|
296
|
+
if (typeof value === 'object') return JSON.stringify(value);
|
|
297
|
+
return String(value);
|
|
298
|
+
}
|
|
299
|
+
|
|
228
300
|
function unpackPayload(data, source) {
|
|
229
301
|
const body = data.payload != null && typeof data.payload === 'object' && !Array.isArray(data.payload)
|
|
230
302
|
? { ...data.payload }
|
|
@@ -8,13 +8,15 @@
|
|
|
8
8
|
|
|
9
9
|
import { UIOverlay } from './UIOverlay.js';
|
|
10
10
|
import { buildAssistantSnapshot, normalizeAssistant } from './AiAssistant.js';
|
|
11
|
+
import { exportReport as writeReport } from './ReportExporter.js';
|
|
11
12
|
|
|
12
|
-
export const BEE_LADYBUG_VERSION = '0.
|
|
13
|
+
export const BEE_LADYBUG_VERSION = '0.6.1';
|
|
13
14
|
|
|
14
15
|
export const CORE_DEFAULTS = Object.freeze({
|
|
15
16
|
toggleKey: 'F2',
|
|
16
17
|
slowKey: 'F3',
|
|
17
18
|
freezeKey: 'F4',
|
|
19
|
+
exportKey: 'F8',
|
|
18
20
|
slowScale: 0.25,
|
|
19
21
|
maxLogs: 250,
|
|
20
22
|
historySize: 120,
|
|
@@ -61,6 +63,7 @@ export class BeeLadybugCore {
|
|
|
61
63
|
this.toggleKey = cfg.toggleKey;
|
|
62
64
|
this.slowKey = cfg.slowKey;
|
|
63
65
|
this.freezeKey = cfg.freezeKey;
|
|
66
|
+
this.exportKey = cfg.exportKey;
|
|
64
67
|
this.slowScale = cfg.slowScale;
|
|
65
68
|
this.maxLogs = cfg.maxLogs;
|
|
66
69
|
this.historySize = cfg.historySize;
|
|
@@ -138,7 +141,7 @@ export class BeeLadybugCore {
|
|
|
138
141
|
#detail;
|
|
139
142
|
/** @type {Array<{ text: string, level?: string, onSelect?: () => void }>} */
|
|
140
143
|
#issues;
|
|
141
|
-
/** @type {Map<string, { name: string, label: string, enabled: boolean, enable: Function, disable: Function }>} */
|
|
144
|
+
/** @type {Map<string, { name: string, label: string, hotkey: string, enabled: boolean, enable: Function, disable: Function, snapshot: Function | null }>} */
|
|
142
145
|
#adapters;
|
|
143
146
|
|
|
144
147
|
/**
|
|
@@ -295,7 +298,7 @@ export class BeeLadybugCore {
|
|
|
295
298
|
* Register a toggleable diagnostic adapter. The core stores hooks only —
|
|
296
299
|
* it does not import or special-case adapter modules.
|
|
297
300
|
* @param {string} name
|
|
298
|
-
* @param {{ enable: () => void, disable: () => void, label?: string, hotkey?: string, enabled?: boolean }} hooks
|
|
301
|
+
* @param {{ enable: () => void, disable: () => void, label?: string, hotkey?: string, enabled?: boolean, snapshot?: () => { name?: string, items?: object[] } | null }} hooks
|
|
299
302
|
*/
|
|
300
303
|
registerAdapter(name, hooks = {}) {
|
|
301
304
|
const key = String(name || '').trim();
|
|
@@ -315,7 +318,8 @@ export class BeeLadybugCore {
|
|
|
315
318
|
hotkey,
|
|
316
319
|
enabled,
|
|
317
320
|
enable: hooks.enable,
|
|
318
|
-
disable: hooks.disable
|
|
321
|
+
disable: hooks.disable,
|
|
322
|
+
snapshot: typeof hooks.snapshot === 'function' ? hooks.snapshot : null
|
|
319
323
|
});
|
|
320
324
|
this.sendData('sys', {
|
|
321
325
|
op: 'adapter',
|
|
@@ -349,6 +353,30 @@ export class BeeLadybugCore {
|
|
|
349
353
|
}));
|
|
350
354
|
}
|
|
351
355
|
|
|
356
|
+
/**
|
|
357
|
+
* Ask every registered adapter for a report snapshot.
|
|
358
|
+
* Modules without a snapshot hook are skipped. The core does not read the rows.
|
|
359
|
+
* A thrown snapshot becomes `{ name, error }` so one module cannot abort the report.
|
|
360
|
+
* @returns {Array<{ name: string, result?: unknown, error?: string }>}
|
|
361
|
+
*/
|
|
362
|
+
collectSnapshots() {
|
|
363
|
+
const out = [];
|
|
364
|
+
for (const entry of this.#adapters.values()) {
|
|
365
|
+
if (typeof entry.snapshot !== 'function') continue;
|
|
366
|
+
try {
|
|
367
|
+
const result = entry.snapshot();
|
|
368
|
+
if (result == null) continue;
|
|
369
|
+
out.push({ name: entry.name, result });
|
|
370
|
+
} catch (err) {
|
|
371
|
+
out.push({
|
|
372
|
+
name: entry.name,
|
|
373
|
+
error: err instanceof Error ? err.message : String(err)
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
return out;
|
|
378
|
+
}
|
|
379
|
+
|
|
352
380
|
/**
|
|
353
381
|
* Clickable rows inside the overlay. The core does not interpret them.
|
|
354
382
|
* @param {Array<{ text: string, level?: string, onSelect?: () => void }>} items
|
|
@@ -535,6 +563,16 @@ export class BeeLadybugCore {
|
|
|
535
563
|
return this;
|
|
536
564
|
}
|
|
537
565
|
|
|
566
|
+
/**
|
|
567
|
+
* Build a telemetry report. HTML and CSV return a string; JSON returns
|
|
568
|
+
* the plain object. A mounted overlay downloads the file.
|
|
569
|
+
* @param {'html' | 'json' | 'csv'} [format]
|
|
570
|
+
* @returns {object | string}
|
|
571
|
+
*/
|
|
572
|
+
exportReport(format = 'html') {
|
|
573
|
+
return writeReport(this, { format });
|
|
574
|
+
}
|
|
575
|
+
|
|
538
576
|
applySlowMo() {
|
|
539
577
|
if (this.frozen) this.unfreeze();
|
|
540
578
|
const next = this.timeScale === this.slowScale ? 1 : this.slowScale;
|
|
@@ -749,6 +787,12 @@ export class BeeLadybugCore {
|
|
|
749
787
|
return;
|
|
750
788
|
}
|
|
751
789
|
|
|
790
|
+
if (code === this.exportKey) {
|
|
791
|
+
event.preventDefault();
|
|
792
|
+
this.exportReport('html');
|
|
793
|
+
return;
|
|
794
|
+
}
|
|
795
|
+
|
|
752
796
|
if (!this.visible) return;
|
|
753
797
|
|
|
754
798
|
if (code === this.slowKey) {
|