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.
@@ -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.5.3';
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) {