bee-ladybug 0.5.3 → 0.6.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.
- package/README.MD +18 -1
- package/package.json +1 -1
- package/src/adapters/AntAdapter.js +52 -1
- package/src/adapters/CSSAdapter.js +257 -14
- 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 +615 -0
- package/src/core/UIOverlay.js +4 -1
- package/src/index.d.ts +89 -0
- package/src/index.js +1 -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.0';
|
|
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) {
|