tinytsdk 0.2.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,1217 @@
1
+ 'use strict';
2
+
3
+ var jsxRuntime = require('react/jsx-runtime');
4
+ var react = require('react');
5
+
6
+ /**
7
+ * TinyTrack binary protocol parser (v1 + v2).
8
+ *
9
+ * Wire format (big-endian):
10
+ * [magic:1][version:1][type:1][length:2][timestamp:4][checksum:1] + payload
11
+ *
12
+ * tt_metrics payload (52 bytes, little-endian packed struct):
13
+ * timestamp:8 cpu:2 mem:2 net_rx:4 net_tx:4
14
+ * load1:2 load5:2 load15:2 nr_running:4 nr_total:4
15
+ * du_usage:2 du_total:8 du_free:8
16
+ */
17
+ const PROTO_MAGIC = 0xaa;
18
+ const HEADER_SIZE = 10;
19
+ // Packet types
20
+ const PKT_METRICS = 0x01;
21
+ const PKT_CONFIG = 0x02;
22
+ const PKT_CMD = 0x04;
23
+ const PKT_ACK = 0x05;
24
+ const PKT_HISTORY_REQ = 0x10;
25
+ const PKT_HISTORY_RESP = 0x11;
26
+ const PKT_SUBSCRIBE = 0x12;
27
+ const PKT_RING_STATS = 0x13;
28
+ const PKT_SYS_INFO = 0x14;
29
+ // Ring levels
30
+ const RING_L1 = 0x01;
31
+ const RING_L2 = 0x02;
32
+ const RING_L3 = 0x03;
33
+ // Commands
34
+ const CMD_SET_INTERVAL = 0x01;
35
+ const CMD_GET_SNAPSHOT = 0x03;
36
+ const CMD_GET_RING_STATS = 0x10;
37
+ const CMD_GET_SYS_INFO = 0x11;
38
+ const CMD_START = 0x12;
39
+ const CMD_STOP = 0x13;
40
+ /** Parse the 10-byte header. Returns null if buffer is too short or magic is wrong. */
41
+ function parseHeader(buf, offset = 0) {
42
+ if (buf.byteLength - offset < HEADER_SIZE)
43
+ return null;
44
+ const v = new DataView(buf, offset);
45
+ if (v.getUint8(0) !== PROTO_MAGIC)
46
+ return null;
47
+ const version = v.getUint8(1);
48
+ const type = v.getUint8(2);
49
+ const length = v.getUint16(3, false); // big-endian
50
+ const timestamp = v.getUint32(5, false);
51
+ if (buf.byteLength - offset < HEADER_SIZE + length)
52
+ return null;
53
+ return {
54
+ type,
55
+ version,
56
+ timestamp,
57
+ payload: new DataView(buf, offset + HEADER_SIZE, length),
58
+ };
59
+ }
60
+ /** Parse PKT_METRICS payload (52 bytes, little-endian). */
61
+ function parseMetrics(p) {
62
+ // uint64 LE: lo word at offset 0, hi word at offset 4
63
+ const tsLo = p.getUint32(0, true);
64
+ const tsHi = p.getUint32(4, true);
65
+ const timestamp = tsHi * 0x100000000 + tsLo;
66
+ return {
67
+ timestamp,
68
+ cpu: p.getUint16(8, true),
69
+ mem: p.getUint16(10, true),
70
+ netRx: p.getUint32(12, true),
71
+ netTx: p.getUint32(16, true),
72
+ load1: p.getUint16(20, true),
73
+ load5: p.getUint16(22, true),
74
+ load15: p.getUint16(24, true),
75
+ nrRunning: p.getUint32(26, true),
76
+ nrTotal: p.getUint32(30, true),
77
+ duUsage: p.getUint16(34, true),
78
+ duTotal: readUint64LE(p, 36),
79
+ duFree: readUint64LE(p, 44),
80
+ };
81
+ }
82
+ /** Parse PKT_CONFIG payload (5 bytes). */
83
+ function parseConfig(p) {
84
+ return {
85
+ intervalMs: p.getUint32(0, false),
86
+ alertsEnabled: p.getUint8(4) !== 0,
87
+ };
88
+ }
89
+ /** Parse PKT_ACK payload (2 bytes). */
90
+ function parseAck(p) {
91
+ return { cmdType: p.getUint8(0), status: p.getUint8(1) };
92
+ }
93
+ /** Parse PKT_STATS payload (75 bytes = 3 × 25). */
94
+ function parseStats(p) {
95
+ return {
96
+ l1: parseRingStat(p, 0),
97
+ l2: parseRingStat(p, 29),
98
+ l3: parseRingStat(p, 58),
99
+ };
100
+ }
101
+ /** Parse PKT_HISTORY_RESP payload. */
102
+ function parseHistoryResp(p) {
103
+ const level = p.getUint8(0);
104
+ const count = p.getUint16(1, false);
105
+ const flags = p.getUint8(3);
106
+ const samples = [];
107
+ for (let i = 0; i < count; i++) {
108
+ const off = 4 + i * 52;
109
+ samples.push(parseMetrics(new DataView(p.buffer, p.byteOffset + off, 52)));
110
+ }
111
+ return { level, count, last: (flags & 0x01) !== 0, samples };
112
+ }
113
+ /** Parse PKT_SYS_INFO payload (168 bytes). */
114
+ function parseSysInfo(p) {
115
+ const dec = new TextDecoder();
116
+ const hostname = dec.decode(new Uint8Array(p.buffer, p.byteOffset, 64)).replace(/\0.*/, '');
117
+ const osType = dec.decode(new Uint8Array(p.buffer, p.byteOffset + 64, 64)).replace(/\0.*/, '');
118
+ return {
119
+ hostname,
120
+ osType,
121
+ uptimeSec: readUint64BE(p, 128), // server: htobe64
122
+ slotsL1: p.getUint32(136, false), // server: htonl (BE)
123
+ slotsL2: p.getUint32(140, false),
124
+ slotsL3: p.getUint32(144, false),
125
+ intervalMs: p.getUint32(148, false),
126
+ aggL2Ms: p.getUint32(152, false),
127
+ aggL3Ms: p.getUint32(156, false),
128
+ };
129
+ }
130
+ // ---------------------------------------------------------------------------
131
+ // Command builders
132
+ // ---------------------------------------------------------------------------
133
+ function buildCmd(cmdType, arg = 0) {
134
+ const buf = new ArrayBuffer(HEADER_SIZE + 9);
135
+ const h = new DataView(buf);
136
+ const ts = Math.floor(Date.now() / 1000);
137
+ h.setUint8(0, PROTO_MAGIC);
138
+ h.setUint8(1, 0x01); // v1
139
+ h.setUint8(2, PKT_CMD);
140
+ h.setUint16(3, 9, false);
141
+ h.setUint32(5, ts, false);
142
+ h.setUint8(9, calcChecksum(h));
143
+ // payload
144
+ h.setUint8(10, cmdType);
145
+ h.setUint32(11, arg, false);
146
+ return buf;
147
+ }
148
+ function buildHistoryReq(level, maxCount = 0, fromTs = 0, toTs = 0) {
149
+ const buf = new ArrayBuffer(HEADER_SIZE + 19);
150
+ const h = new DataView(buf);
151
+ const ts = Math.floor(Date.now() / 1000);
152
+ h.setUint8(0, PROTO_MAGIC);
153
+ h.setUint8(1, 0x02); // v2
154
+ h.setUint8(2, PKT_HISTORY_REQ);
155
+ h.setUint16(3, 19, false);
156
+ h.setUint32(5, ts, false);
157
+ h.setUint8(9, calcChecksum(h));
158
+ // payload: level(1) from_ts(8) to_ts(8) max_count(2)
159
+ h.setUint8(10, level);
160
+ writeUint64BE(h, 11, fromTs);
161
+ writeUint64BE(h, 19, toTs);
162
+ h.setUint16(27, maxCount, false);
163
+ return buf;
164
+ }
165
+ function buildSubscribe(level, intervalMs = 0) {
166
+ const buf = new ArrayBuffer(HEADER_SIZE + 6);
167
+ const h = new DataView(buf);
168
+ const ts = Math.floor(Date.now() / 1000);
169
+ h.setUint8(0, PROTO_MAGIC);
170
+ h.setUint8(1, 0x02);
171
+ h.setUint8(2, PKT_SUBSCRIBE);
172
+ h.setUint16(3, 6, false);
173
+ h.setUint32(5, ts, false);
174
+ h.setUint8(9, calcChecksum(h));
175
+ h.setUint8(10, level);
176
+ h.setUint32(11, intervalMs, false);
177
+ h.setUint8(15, 0);
178
+ return buf;
179
+ }
180
+ // ---------------------------------------------------------------------------
181
+ // Helpers
182
+ // ---------------------------------------------------------------------------
183
+ function calcChecksum(h) {
184
+ let cs = 0;
185
+ for (let i = 0; i < 9; i++)
186
+ cs ^= h.getUint8(i);
187
+ return cs;
188
+ }
189
+ function parseRingStat(p, off) {
190
+ return {
191
+ level: p.getUint8(off),
192
+ capacity: p.getUint32(off + 1, false), // htonl (BE)
193
+ head: p.getUint32(off + 5, false), // htonl (BE)
194
+ filled: p.getUint32(off + 9, false), // htonl (BE)
195
+ firstTs: readUint64LE(p, off + 13), // no hton, native LE
196
+ lastTs: readUint64LE(p, off + 21), // no hton, native LE
197
+ };
198
+ }
199
+ function readUint64LE(v, off) {
200
+ const lo = v.getUint32(off, true);
201
+ const hi = v.getUint32(off + 4, true);
202
+ return hi * 0x100000000 + lo;
203
+ }
204
+ function readUint64BE(v, off) {
205
+ const hi = v.getUint32(off, false);
206
+ const lo = v.getUint32(off + 4, false);
207
+ return hi * 0x100000000 + lo;
208
+ }
209
+ function writeUint64BE(v, off, val) {
210
+ const hi = Math.floor(val / 0x100000000);
211
+ const lo = val >>> 0;
212
+ v.setUint32(off, hi, false);
213
+ v.setUint32(off + 4, lo, false);
214
+ }
215
+
216
+ /**
217
+ * TinyTrackClient — WebSocket client for the tinytrack gateway.
218
+ *
219
+ * Usage:
220
+ * const client = new TinyTrackClient('ws://localhost:4026');
221
+ * client.on('metrics', m => console.log(m.cpu / 100, '%'));
222
+ * client.connect();
223
+ */
224
+ class TinyTrackClient {
225
+ constructor(url, opts = {}) {
226
+ var _a, _b, _c, _d;
227
+ this.ws = null;
228
+ this.listeners = new Map();
229
+ this.retries = 0;
230
+ this._closed = false;
231
+ // Normalise: strip trailing path, we'll append opts.path
232
+ this.url = url.replace(/\/websocket\/?$/, '');
233
+ this.opts = {
234
+ reconnect: (_a = opts.reconnect) !== null && _a !== void 0 ? _a : true,
235
+ reconnectDelay: (_b = opts.reconnectDelay) !== null && _b !== void 0 ? _b : 2000,
236
+ maxRetries: (_c = opts.maxRetries) !== null && _c !== void 0 ? _c : 0,
237
+ path: (_d = opts.path) !== null && _d !== void 0 ? _d : '/websocket',
238
+ };
239
+ }
240
+ // ---------------------------------------------------------------------------
241
+ // Lifecycle
242
+ // ---------------------------------------------------------------------------
243
+ connect() {
244
+ this._closed = false;
245
+ this._open();
246
+ return this;
247
+ }
248
+ disconnect() {
249
+ var _a;
250
+ this._closed = true;
251
+ (_a = this.ws) === null || _a === void 0 ? void 0 : _a.close();
252
+ this.ws = null;
253
+ }
254
+ get connected() {
255
+ var _a;
256
+ return ((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) === WebSocket.OPEN;
257
+ }
258
+ // ---------------------------------------------------------------------------
259
+ // Commands
260
+ // ---------------------------------------------------------------------------
261
+ getSnapshot() {
262
+ this._send(buildCmd(CMD_GET_SNAPSHOT));
263
+ }
264
+ getStats() {
265
+ this._send(buildCmd(CMD_GET_RING_STATS));
266
+ }
267
+ getSysInfo() {
268
+ this._send(buildCmd(CMD_GET_SYS_INFO));
269
+ }
270
+ setInterval(ms) {
271
+ this._send(buildCmd(CMD_SET_INTERVAL, ms));
272
+ }
273
+ start() {
274
+ this._send(buildCmd(CMD_START));
275
+ }
276
+ stop() {
277
+ this._send(buildCmd(CMD_STOP));
278
+ }
279
+ getHistory(level, maxCount = 60, fromTs = 0, toTs = 0) {
280
+ this._send(buildHistoryReq(level, maxCount, fromTs, toTs));
281
+ }
282
+ subscribe(level, intervalMs = 0) {
283
+ this._send(buildSubscribe(level, intervalMs));
284
+ }
285
+ // ---------------------------------------------------------------------------
286
+ // Events
287
+ // ---------------------------------------------------------------------------
288
+ on(event, fn) {
289
+ if (!this.listeners.has(event))
290
+ this.listeners.set(event, new Set());
291
+ this.listeners.get(event).add(fn);
292
+ return this;
293
+ }
294
+ off(event, fn) {
295
+ var _a;
296
+ (_a = this.listeners.get(event)) === null || _a === void 0 ? void 0 : _a.delete(fn);
297
+ return this;
298
+ }
299
+ // ---------------------------------------------------------------------------
300
+ // Private
301
+ // ---------------------------------------------------------------------------
302
+ _open() {
303
+ const wsUrl = this.url + this.opts.path;
304
+ const ws = new WebSocket(wsUrl);
305
+ ws.binaryType = 'arraybuffer';
306
+ this.ws = ws;
307
+ ws.onopen = () => {
308
+ this.retries = 0;
309
+ this._emit('open');
310
+ };
311
+ ws.onclose = (e) => {
312
+ this._emit('close', e.code, e.reason);
313
+ if (!this._closed && this.opts.reconnect) {
314
+ const max = this.opts.maxRetries;
315
+ if (max === 0 || this.retries < max) {
316
+ this.retries++;
317
+ setTimeout(() => this._open(), this.opts.reconnectDelay);
318
+ }
319
+ }
320
+ };
321
+ ws.onerror = (e) => this._emit('error', e);
322
+ ws.onmessage = (e) => {
323
+ const frame = parseHeader(e.data);
324
+ if (!frame)
325
+ return;
326
+ switch (frame.type) {
327
+ case PKT_METRICS:
328
+ this._emit('metrics', parseMetrics(frame.payload));
329
+ break;
330
+ case PKT_CONFIG:
331
+ this._emit('config', parseConfig(frame.payload));
332
+ break;
333
+ case PKT_ACK:
334
+ this._emit('ack', parseAck(frame.payload));
335
+ break;
336
+ case PKT_RING_STATS:
337
+ this._emit('stats', parseStats(frame.payload));
338
+ break;
339
+ case PKT_HISTORY_RESP:
340
+ this._emit('history', parseHistoryResp(frame.payload));
341
+ break;
342
+ case PKT_SYS_INFO:
343
+ this._emit('sysinfo', parseSysInfo(frame.payload));
344
+ break;
345
+ }
346
+ };
347
+ }
348
+ _send(buf) {
349
+ var _a;
350
+ if (((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) === WebSocket.OPEN) {
351
+ this.ws.send(buf);
352
+ }
353
+ }
354
+ _emit(event, ...args) {
355
+ var _a;
356
+ (_a = this.listeners.get(event)) === null || _a === void 0 ? void 0 : _a.forEach((fn) => fn(...args));
357
+ }
358
+ }
359
+
360
+ const TinyTrackContext = react.createContext({
361
+ client: null,
362
+ connected: false,
363
+ sysinfo: null,
364
+ streaming: true,
365
+ setStreaming: () => { },
366
+ });
367
+ function TinyTrackProvider({ url, children, reconnect, reconnectDelay }) {
368
+ // Client is created once and never recreated
369
+ const [client] = react.useState(() => new TinyTrackClient(url, { reconnect, reconnectDelay }));
370
+ const [connected, setConnected] = react.useState(false);
371
+ const [sysinfo, setSysinfo] = react.useState(null);
372
+ const [streaming, setStreamingState] = react.useState(true);
373
+ // Use ref so setStreaming closure always has the latest client
374
+ const clientRef = react.useRef(client);
375
+ const setStreaming = react.useCallback((v) => {
376
+ setStreamingState(v);
377
+ if (v)
378
+ clientRef.current.start();
379
+ else
380
+ clientRef.current.stop();
381
+ }, []);
382
+ react.useEffect(() => {
383
+ const onOpen = () => {
384
+ setConnected(true);
385
+ setStreamingState(true); // new session always starts streaming
386
+ client.getSysInfo();
387
+ client.getSnapshot();
388
+ };
389
+ const onClose = () => {
390
+ setConnected(false);
391
+ setSysinfo(null);
392
+ };
393
+ client.on('open', onOpen);
394
+ client.on('close', onClose);
395
+ client.on('sysinfo', setSysinfo);
396
+ client.connect();
397
+ return () => client.disconnect();
398
+ }, [client]);
399
+ return (jsxRuntime.jsx(TinyTrackContext.Provider, { value: { client, connected, sysinfo, streaming, setStreaming }, children: children }));
400
+ }
401
+ function useTinyTrack() {
402
+ return react.useContext(TinyTrackContext);
403
+ }
404
+ function useMetrics() {
405
+ const { client, connected, sysinfo, streaming, setStreaming } = useTinyTrack();
406
+ const [metrics, setMetrics] = react.useState(null);
407
+ const [config, setConfig] = react.useState(null);
408
+ const [stats, setStats] = react.useState(null);
409
+ react.useEffect(() => {
410
+ if (!client)
411
+ return;
412
+ client.on('metrics', setMetrics);
413
+ client.on('config', setConfig);
414
+ client.on('stats', setStats);
415
+ return () => {
416
+ client.off('metrics', setMetrics);
417
+ client.off('config', setConfig);
418
+ client.off('stats', setStats);
419
+ };
420
+ }, [client]);
421
+ return { client, connected, metrics, config, stats, sysinfo, streaming, setStreaming };
422
+ }
423
+ function useHistory(maxSamples = 3600) {
424
+ const { client, connected } = useTinyTrack();
425
+ const [samples, setSamples] = react.useState([]);
426
+ const buf = react.useRef([]);
427
+ react.useEffect(() => {
428
+ if (!client)
429
+ return;
430
+ const onHistory = (r) => {
431
+ buf.current = [...buf.current, ...r.samples].slice(-maxSamples);
432
+ setSamples([...buf.current]);
433
+ };
434
+ client.on('history', onHistory);
435
+ return () => {
436
+ client.off('history', onHistory);
437
+ };
438
+ }, [client, maxSamples]);
439
+ react.useEffect(() => {
440
+ if (connected) {
441
+ buf.current = [];
442
+ setSamples([]);
443
+ }
444
+ }, [connected]);
445
+ return samples;
446
+ }
447
+
448
+ const THEMES = {
449
+ /** Classic TUI — monospace, green-on-dark */
450
+ terminal: {
451
+ bg: '#111827',
452
+ surface: '#0d1117',
453
+ border: '#374151',
454
+ divider: '#1f2937',
455
+ text: '#f3f4f6',
456
+ muted: '#9ca3af',
457
+ faint: '#4b5563',
458
+ cpu: '#4ade80',
459
+ mem: '#60a5fa',
460
+ net: '#f59e0b',
461
+ disk: '#fb923c',
462
+ load: '#a78bfa',
463
+ ok: '#22c55e',
464
+ warn: '#f59e0b',
465
+ crit: '#ef4444',
466
+ btnBg: '#1f2937',
467
+ btnText: '#9ca3af',
468
+ font: '"JetBrains Mono", "Fira Code", monospace',
469
+ radius: 4,
470
+ },
471
+ /** Modern dark — VS Code / GitHub Dark style */
472
+ dark: {
473
+ bg: '#1e1e2e',
474
+ surface: '#181825',
475
+ border: '#313244',
476
+ divider: '#2a2a3d',
477
+ text: '#cdd6f4',
478
+ muted: '#a6adc8',
479
+ faint: '#585b70',
480
+ cpu: '#a6e3a1',
481
+ mem: '#89b4fa',
482
+ net: '#fab387',
483
+ disk: '#f9e2af',
484
+ load: '#cba6f7',
485
+ ok: '#a6e3a1',
486
+ warn: '#f9e2af',
487
+ crit: '#f38ba8',
488
+ btnBg: '#313244',
489
+ btnText: '#a6adc8',
490
+ font: '"Inter", "Segoe UI", system-ui, sans-serif',
491
+ radius: 6,
492
+ },
493
+ /** Light — clean minimal */
494
+ light: {
495
+ bg: '#ffffff',
496
+ surface: '#f8fafc',
497
+ border: '#e2e8f0',
498
+ divider: '#f1f5f9',
499
+ text: '#0f172a',
500
+ muted: '#64748b',
501
+ faint: '#cbd5e1',
502
+ cpu: '#16a34a',
503
+ mem: '#2563eb',
504
+ net: '#d97706',
505
+ disk: '#ea580c',
506
+ load: '#7c3aed',
507
+ ok: '#16a34a',
508
+ warn: '#d97706',
509
+ crit: '#dc2626',
510
+ btnBg: '#f1f5f9',
511
+ btnText: '#475569',
512
+ font: '"Inter", "Segoe UI", system-ui, sans-serif',
513
+ radius: 6,
514
+ },
515
+ /** Material — Google Material Design 3 tones */
516
+ material: {
517
+ bg: '#1c1b1f',
518
+ surface: '#141218',
519
+ border: '#49454f',
520
+ divider: '#2b2930',
521
+ text: '#e6e1e5',
522
+ muted: '#cac4d0',
523
+ faint: '#49454f',
524
+ cpu: '#79dd72',
525
+ mem: '#7fcfff',
526
+ net: '#ffb77c',
527
+ disk: '#ffb4ab',
528
+ load: '#d0bcff',
529
+ ok: '#79dd72',
530
+ warn: '#ffb77c',
531
+ crit: '#ffb4ab',
532
+ btnBg: '#2b2930',
533
+ btnText: '#cac4d0',
534
+ font: '"Roboto", "Google Sans", system-ui, sans-serif',
535
+ radius: 12,
536
+ },
537
+ /** Dracula — classic Dracula color scheme */
538
+ dracula: {
539
+ bg: '#282a36',
540
+ surface: '#21222c',
541
+ border: '#44475a',
542
+ divider: '#343746',
543
+ text: '#f8f8f2',
544
+ muted: '#6272a4',
545
+ faint: '#44475a',
546
+ cpu: '#50fa7b',
547
+ mem: '#8be9fd',
548
+ net: '#ffb86c',
549
+ disk: '#ff79c6',
550
+ load: '#bd93f9',
551
+ ok: '#50fa7b',
552
+ warn: '#ffb86c',
553
+ crit: '#ff5555',
554
+ btnBg: '#44475a',
555
+ btnText: '#f8f8f2',
556
+ font: '"Fira Code", "JetBrains Mono", monospace',
557
+ radius: 4,
558
+ },
559
+ /**
560
+ * HeroUI — inspired by NextUI/HeroUI design system.
561
+ * Deep navy background, violet/indigo accents, smooth rounded corners,
562
+ * glassmorphism-style surfaces with subtle borders.
563
+ */
564
+ heroui: {
565
+ bg: '#0f0f1a',
566
+ surface: '#16162a',
567
+ border: '#2d2d52',
568
+ divider: '#1e1e38',
569
+ text: '#e2e8f0',
570
+ muted: '#94a3b8',
571
+ faint: '#334155',
572
+ cpu: '#7c3aed',
573
+ mem: '#06b6d4',
574
+ net: '#10b981',
575
+ disk: '#f59e0b',
576
+ load: '#8b5cf6',
577
+ ok: '#10b981',
578
+ warn: '#f59e0b',
579
+ crit: '#f43f5e',
580
+ btnBg: '#1e1e38',
581
+ btnText: '#c4b5fd',
582
+ font: '"Inter", "SF Pro Display", system-ui, sans-serif',
583
+ radius: 12,
584
+ glow: '0 0 12px rgba(124,58,237,0.25)',
585
+ transition: 'all 0.2s ease',
586
+ },
587
+ };
588
+ // ---------------------------------------------------------------------------
589
+ // Context
590
+ // ---------------------------------------------------------------------------
591
+ const ThemeContext = react.createContext(THEMES.terminal);
592
+ function useTheme() {
593
+ return react.useContext(ThemeContext);
594
+ }
595
+ function ThemeProvider({ preset = 'terminal', theme, children }) {
596
+ const resolved = theme ? Object.assign(Object.assign({}, THEMES[preset]), theme) : THEMES[preset];
597
+ return jsxRuntime.jsx(ThemeContext.Provider, { value: resolved, children: children });
598
+ }
599
+ // ---------------------------------------------------------------------------
600
+ // Helper: build common inline style objects from theme tokens
601
+ // ---------------------------------------------------------------------------
602
+ function themeStyles(t) {
603
+ return {
604
+ root: {
605
+ fontFamily: t.font,
606
+ fontSize: 12,
607
+ background: t.bg,
608
+ color: t.text,
609
+ border: `1px solid ${t.border}`,
610
+ borderRadius: t.radius,
611
+ padding: '6px 10px',
612
+ display: 'flex',
613
+ flexDirection: 'column',
614
+ gap: 4,
615
+ boxSizing: 'border-box',
616
+ boxShadow: t.glow,
617
+ transition: t.transition,
618
+ },
619
+ divider: { height: 1, background: t.divider, margin: '2px 0' },
620
+ label: { color: t.muted, whiteSpace: 'nowrap' },
621
+ value: {
622
+ color: t.text,
623
+ whiteSpace: 'nowrap',
624
+ minWidth: 48,
625
+ display: 'inline-block',
626
+ },
627
+ numval: {
628
+ color: t.text,
629
+ whiteSpace: 'nowrap',
630
+ minWidth: 52,
631
+ display: 'inline-block',
632
+ fontVariantNumeric: 'tabular-nums',
633
+ },
634
+ btn: {
635
+ fontSize: 10,
636
+ padding: '2px 8px',
637
+ background: t.btnBg,
638
+ border: `1px solid ${t.border}`,
639
+ borderRadius: t.radius,
640
+ color: t.btnText,
641
+ cursor: 'pointer',
642
+ transition: t.transition,
643
+ fontFamily: t.font,
644
+ },
645
+ select: {
646
+ fontSize: 11,
647
+ background: t.btnBg,
648
+ border: `1px solid ${t.border}`,
649
+ borderRadius: t.radius,
650
+ color: t.text,
651
+ padding: '2px 6px',
652
+ cursor: 'pointer',
653
+ fontFamily: t.font,
654
+ },
655
+ badge: (color) => ({ fontSize: 10, color, fontWeight: 600, minWidth: 48 }),
656
+ alert: (level) => ({
657
+ fontSize: 10,
658
+ padding: '1px 6px',
659
+ borderRadius: t.radius,
660
+ background: level === 'crit' ? t.crit + '33' : level === 'ok' ? t.ok + '33' : t.warn + '33',
661
+ color: level === 'crit' ? t.crit : level === 'ok' ? t.ok : t.warn,
662
+ whiteSpace: 'nowrap',
663
+ }),
664
+ };
665
+ }
666
+
667
+ function fmtPct(val) {
668
+ return (val / 100).toFixed(1) + '%';
669
+ }
670
+ function fmtBytes(bytes) {
671
+ if (bytes < 1024)
672
+ return bytes + 'B';
673
+ if (bytes < 1024 * 1024)
674
+ return (bytes / 1024).toFixed(0) + 'KB';
675
+ if (bytes < 1024 * 1024 * 1024)
676
+ return (bytes / (1024 * 1024)).toFixed(1) + 'MB';
677
+ return (bytes / (1024 * 1024 * 1024)).toFixed(1) + 'GB';
678
+ }
679
+ function fmtLoad(val) {
680
+ return (val / 100).toFixed(2);
681
+ }
682
+ /** Build a simple ASCII bar: ▓▓▓░░░ */
683
+ function bar(pct, width = 8) {
684
+ const filled = Math.round((pct / 10000) * width);
685
+ return '▓'.repeat(filled) + '░'.repeat(width - filled);
686
+ }
687
+ function loadTrend(m) {
688
+ const diff = m.load1 - m.load15;
689
+ if (diff > 20)
690
+ return 'rising'; // >0.2 difference
691
+ if (diff < -20)
692
+ return 'falling';
693
+ return 'stable';
694
+ }
695
+ function detectAlerts(m, prev) {
696
+ const alerts = [];
697
+ // CPU
698
+ if (m.cpu > 8000)
699
+ alerts.push({ id: 'cpu-crit', label: 'CPU >80%', level: 'crit' });
700
+ else if (m.cpu > 6000)
701
+ alerts.push({ id: 'cpu-warn', label: 'CPU >60%', level: 'warn' });
702
+ // Memory
703
+ if (m.mem > 9000)
704
+ alerts.push({ id: 'mem-crit', label: 'Mem >90%', level: 'crit' });
705
+ else if (m.mem > 7500)
706
+ alerts.push({ id: 'mem-warn', label: 'Mem >75%', level: 'warn' });
707
+ // Disk
708
+ if (m.duUsage > 8000)
709
+ alerts.push({ id: 'disk-crit', label: 'Disk >80%', level: 'crit' });
710
+ else if (m.duUsage > 6000)
711
+ alerts.push({ id: 'disk-warn', label: 'Disk >60%', level: 'warn' });
712
+ // Load trend (1m vs 15m)
713
+ const trend = loadTrend(m);
714
+ if (trend === 'rising')
715
+ alerts.push({ id: 'load-rise', label: '↑ Load rising', level: 'warn' });
716
+ if (trend === 'falling')
717
+ alerts.push({ id: 'load-fall', label: '↓ Load falling', level: 'ok' });
718
+ // Sudden load spike vs previous sample
719
+ if (prev && m.load1 - prev.load1 > 50) {
720
+ alerts.push({ id: 'load-spike', label: '⚡ Load spike', level: 'crit' });
721
+ }
722
+ return alerts;
723
+ }
724
+
725
+ function fmtUptimeSec$1(sec) {
726
+ const d = Math.floor(sec / 86400);
727
+ const h = Math.floor((sec % 86400) / 3600);
728
+ const m = Math.floor((sec % 3600) / 60);
729
+ if (d > 0)
730
+ return `${d}d ${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`;
731
+ return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`;
732
+ }
733
+ function MetricsPanel({ className, style, theme: themeProp }) {
734
+ const base = useTheme();
735
+ const t = themeProp ? Object.assign(Object.assign({}, base), themeProp) : base;
736
+ const s = themeStyles(t);
737
+ const { metrics: m, connected, sysinfo } = useMetrics();
738
+ return (jsxRuntime.jsxs("div", { className: className, style: Object.assign(Object.assign(Object.assign({}, s.root), { width: 'fit-content', gap: 4 }), style), children: [jsxRuntime.jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 8 }, children: [jsxRuntime.jsx("span", { style: s.badge(connected ? t.ok : t.crit), children: connected ? '● live' : '○ off' }), sysinfo && jsxRuntime.jsx("span", { style: { color: t.muted, fontSize: 11 }, children: sysinfo.hostname })] }), sysinfo && (jsxRuntime.jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 6 }, children: [jsxRuntime.jsx("span", { style: s.label, children: "Uptime" }), jsxRuntime.jsx("span", { style: Object.assign(Object.assign({}, s.numval), { color: t.ok }), children: fmtUptimeSec$1(sysinfo.uptimeSec) })] })), jsxRuntime.jsx("div", { style: s.divider }), jsxRuntime.jsx(Row, { label: "CPU", value: m ? fmtPct(m.cpu) : '—', bar: m ? bar(m.cpu) : null, color: t.cpu, s: s }), jsxRuntime.jsx(Row, { label: "Mem", value: m ? fmtPct(m.mem) : '—', bar: m ? bar(m.mem) : null, color: t.mem, s: s }), jsxRuntime.jsx(Row, { label: "Disk", value: m ? fmtPct(m.duUsage) : '—', bar: m ? bar(m.duUsage) : null, color: t.disk, s: s }), jsxRuntime.jsx("div", { style: s.divider }), jsxRuntime.jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 6 }, children: [jsxRuntime.jsx("span", { style: Object.assign(Object.assign({}, s.label), { minWidth: 36 }), children: "Load" }), jsxRuntime.jsx("span", { style: Object.assign(Object.assign({}, s.numval), { color: t.load }), children: m ? fmtLoad(m.load1) : '—' }), jsxRuntime.jsx("span", { style: { color: t.faint }, children: "/" }), jsxRuntime.jsx("span", { style: Object.assign(Object.assign({}, s.numval), { color: t.muted }), children: m ? fmtLoad(m.load5) : '—' }), jsxRuntime.jsx("span", { style: { color: t.faint }, children: "/" }), jsxRuntime.jsx("span", { style: Object.assign(Object.assign({}, s.numval), { color: t.muted }), children: m ? fmtLoad(m.load15) : '—' })] }), jsxRuntime.jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 6 }, children: [jsxRuntime.jsx("span", { style: Object.assign(Object.assign({}, s.label), { minWidth: 36 }), children: "Proc" }), jsxRuntime.jsx("span", { style: s.numval, children: m ? `${m.nrRunning} / ${m.nrTotal}` : '—' })] }), jsxRuntime.jsx("div", { style: s.divider }), jsxRuntime.jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 6 }, children: [jsxRuntime.jsx("span", { style: Object.assign(Object.assign({}, s.label), { minWidth: 36 }), children: "Net \u2191" }), jsxRuntime.jsx("span", { style: Object.assign(Object.assign({}, s.numval), { color: t.net }), children: m ? fmtBytes(m.netTx) + '/s' : '—' }), jsxRuntime.jsx("span", { style: { color: t.faint }, children: "\u2193" }), jsxRuntime.jsx("span", { style: Object.assign(Object.assign({}, s.numval), { color: t.mem }), children: m ? fmtBytes(m.netRx) + '/s' : '—' })] }), m && (jsxRuntime.jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 6 }, children: [jsxRuntime.jsx("span", { style: Object.assign(Object.assign({}, s.label), { minWidth: 36 }), children: "Disk" }), jsxRuntime.jsxs("span", { style: Object.assign(Object.assign({}, s.numval), { minWidth: 120 }), children: [fmtBytes(m.duTotal - m.duFree), " / ", fmtBytes(m.duTotal)] })] }))] }));
739
+ }
740
+ function Row({ label, value, bar: barStr, color, s, }) {
741
+ return (jsxRuntime.jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 6 }, children: [jsxRuntime.jsx("span", { style: Object.assign(Object.assign({}, s.label), { minWidth: 36 }), children: label }), jsxRuntime.jsx("span", { style: { color, letterSpacing: 1, minWidth: 72, fontFamily: '"JetBrains Mono","Fira Code",monospace' }, children: barStr !== null && barStr !== void 0 ? barStr : ' ' }), jsxRuntime.jsx("span", { style: Object.assign(Object.assign({}, s.numval), { color }), children: value })] }));
742
+ }
743
+
744
+ function MetricsBar({ className, style, showDisk = true, showNet = true, theme: themeProp, compact, }) {
745
+ const base = useTheme();
746
+ const t = themeProp ? Object.assign(Object.assign({}, base), themeProp) : base;
747
+ const s = themeStyles(t);
748
+ const { metrics: m, connected } = useMetrics();
749
+ const prevRef = react.useRef(m);
750
+ const alerts = react.useMemo(() => {
751
+ const a = m ? detectAlerts(m, prevRef.current) : [];
752
+ prevRef.current = m;
753
+ return a;
754
+ // eslint-disable-next-line react-hooks/exhaustive-deps
755
+ }, [m]);
756
+ // Auto-detect mobile if compact not forced
757
+ const isMobile = compact !== null && compact !== void 0 ? compact : (typeof window !== 'undefined' && window.innerWidth < 640);
758
+ return (jsxRuntime.jsxs("div", { className: className, style: Object.assign({ display: 'inline-flex', alignItems: 'center', gap: isMobile ? 4 : 6, fontFamily: t.font, fontSize: isMobile ? 10 : 11, background: t.bg, color: t.text, border: `1px solid ${t.border}`, borderRadius: t.radius, padding: isMobile ? '2px 8px' : '2px 10px', whiteSpace: 'nowrap', boxSizing: 'border-box', height: isMobile ? 22 : 24 }, style), children: [jsxRuntime.jsx("span", { style: { color: connected ? t.ok : t.muted, fontSize: 8 }, title: connected ? 'live' : 'offline', children: "\u25CF" }), jsxRuntime.jsx(Sep, { t: t }), jsxRuntime.jsx(Metric, { label: "CPU", value: m ? fmtPct(m.cpu) : '—', color: t.cpu, t: t }), jsxRuntime.jsx(Metric, { label: "MEM", value: m ? fmtPct(m.mem) : '—', color: t.mem, t: t }), showDisk && !isMobile && jsxRuntime.jsx(Metric, { label: "DSK", value: m ? fmtPct(m.duUsage) : '—', color: t.disk, t: t }), !isMobile && (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(Sep, { t: t }), jsxRuntime.jsx("span", { style: { color: t.muted, fontSize: 10 }, children: "Load" }), jsxRuntime.jsx("span", { style: { color: t.text, minWidth: 120, fontVariantNumeric: 'tabular-nums' }, children: m ? `${fmtLoad(m.load1)} / ${fmtLoad(m.load5)} / ${fmtLoad(m.load15)}` : '— / — / —' })] })), showNet && !isMobile && (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(Sep, { t: t }), jsxRuntime.jsx("span", { style: { color: t.net, minWidth: 20 }, children: "\u2191" }), jsxRuntime.jsxs("span", { style: { color: t.net, minWidth: 72, fontVariantNumeric: 'tabular-nums' }, children: [m ? fmtBytes(m.netTx) : '—', "/s"] }), jsxRuntime.jsx("span", { style: { color: t.mem, minWidth: 20 }, children: "\u2193" }), jsxRuntime.jsxs("span", { style: { color: t.mem, minWidth: 72, fontVariantNumeric: 'tabular-nums' }, children: [m ? fmtBytes(m.netRx) : '—', "/s"] })] })), !isMobile && (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(Sep, { t: t }), jsxRuntime.jsx("span", { style: { color: t.muted, fontSize: 10 }, children: "Proc" }), jsxRuntime.jsx("span", { style: { color: t.text, minWidth: 56, fontVariantNumeric: 'tabular-nums' }, children: m ? `${m.nrRunning}/${m.nrTotal}` : '—' })] })), alerts.length > 0 && (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(Sep, { t: t }), alerts.map((a) => (jsxRuntime.jsx("span", { style: s.alert(a.level), children: a.label }, a.id)))] }))] }));
759
+ }
760
+ function Sep({ t }) {
761
+ return jsxRuntime.jsx("span", { style: { color: t.faint, userSelect: 'none' }, children: "\u2502" });
762
+ }
763
+ function Metric({ label, value, color, t }) {
764
+ return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("span", { style: { color: t.muted, fontSize: 10 }, children: label }), jsxRuntime.jsx("span", { style: { color, minWidth: 44, fontVariantNumeric: 'tabular-nums' }, children: value })] }));
765
+ }
766
+
767
+ function Sparkline({ data, max, width = 120, height = 32, color = '#4ade80', fill = 'rgba(74,222,128,0.15)', }) {
768
+ if (data.length < 2)
769
+ return null;
770
+ const m = max !== null && max !== void 0 ? max : Math.max(...data, 1);
771
+ const pts = data.map((v, i) => {
772
+ const x = (i / (data.length - 1)) * width;
773
+ const y = height - (v / m) * height;
774
+ return `${x.toFixed(1)},${y.toFixed(1)}`;
775
+ });
776
+ const polyline = pts.join(' ');
777
+ const area = `${pts[0]} ${pts.slice(1).join(' ')} ${width},${height} 0,${height}`;
778
+ return (jsxRuntime.jsxs("svg", { width: width, height: height, style: { display: 'block', overflow: 'visible' }, children: [jsxRuntime.jsx("polygon", { points: area, fill: fill }), jsxRuntime.jsx("polyline", { points: polyline, fill: "none", stroke: color, strokeWidth: "1.5", strokeLinejoin: "round" })] }));
779
+ }
780
+
781
+ const INTERVALS = [1000, 2000, 5000, 10000, 30000];
782
+ const INTERVAL_LABELS = ['1s', '2s', '5s', '10s', '30s'];
783
+ const MAX_LOG = 120;
784
+ function fmtUptimeSec(sec) {
785
+ const d = Math.floor(sec / 86400);
786
+ const h = Math.floor((sec % 86400) / 3600);
787
+ const m = Math.floor((sec % 3600) / 60);
788
+ if (d > 0)
789
+ return `${d}d ${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`;
790
+ return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`;
791
+ }
792
+ function Dashboard({ mode: modeProp, historySize = 60, className, style, theme: themeProp }) {
793
+ var _a, _b, _c, _d, _e;
794
+ const base = useTheme();
795
+ const t = themeProp ? Object.assign(Object.assign({}, base), themeProp) : base;
796
+ const s = themeStyles(t);
797
+ const { client, metrics, stats, connected, sysinfo, streaming, setStreaming } = useMetrics();
798
+ const [mode, setMode] = react.useState(modeProp !== null && modeProp !== void 0 ? modeProp : 'compact');
799
+ const [intervalIdx, setIntervalIdx] = react.useState(0);
800
+ const [consoleOpen, setConsoleOpen] = react.useState(false);
801
+ const [wsLog, setWsLog] = react.useState([]);
802
+ const logRef = react.useRef(null);
803
+ const history = react.useRef([]);
804
+ const prevMetrics = react.useRef(null);
805
+ // Push a line to the WS console
806
+ const pushLog = (dir, msg) => {
807
+ const ts = new Date().toLocaleTimeString('en', { hour12: false });
808
+ setWsLog((prev) => {
809
+ const next = [...prev, `[${ts}] ${dir} ${msg}`];
810
+ return next.length > MAX_LOG ? next.slice(-MAX_LOG) : next;
811
+ });
812
+ };
813
+ react.useEffect(() => {
814
+ if (!metrics)
815
+ return;
816
+ history.current = [...history.current.slice(-(historySize - 1)), metrics];
817
+ prevMetrics.current = metrics;
818
+ }, [metrics, historySize]);
819
+ // Request history on connect
820
+ react.useEffect(() => {
821
+ if (connected && client) {
822
+ client.getHistory(RING_L1, historySize);
823
+ pushLog('→', `CMD_GET_HISTORY L1 max=${historySize}`);
824
+ }
825
+ }, [connected, client, historySize]);
826
+ // Log all incoming packets
827
+ react.useEffect(() => {
828
+ if (!client)
829
+ return;
830
+ const lastTs = { current: 0 };
831
+ const onMetrics = (m) => {
832
+ if (m.timestamp === lastTs.current)
833
+ return;
834
+ lastTs.current = m.timestamp;
835
+ pushLog('←', `PKT_METRICS cpu=${fmtPct(m.cpu)} mem=${fmtPct(m.mem)} rx=${fmtBytes(m.netRx)}/s tx=${fmtBytes(m.netTx)}/s`);
836
+ };
837
+ const onAck = (a) => {
838
+ var _a;
839
+ const cmdNames = {
840
+ 0x01: 'SET_INTERVAL',
841
+ 0x02: 'SET_ALERTS',
842
+ 0x03: 'GET_SNAPSHOT',
843
+ 0x10: 'GET_RING_STATS',
844
+ 0x11: 'GET_SYS_INFO',
845
+ 0x12: 'START',
846
+ 0x13: 'STOP',
847
+ };
848
+ const name = (_a = cmdNames[a.cmdType]) !== null && _a !== void 0 ? _a : `0x${a.cmdType.toString(16)}`;
849
+ pushLog('←', `PKT_ACK cmd=${name} status=${a.status === 0 ? 'OK' : 'ERR'}`);
850
+ };
851
+ const onConfig = (c) => {
852
+ pushLog('←', `PKT_CONFIG interval=${c.intervalMs}ms`);
853
+ };
854
+ const onStats = () => pushLog('←', 'PKT_RING_STATS');
855
+ const onSysInfo = (si) => {
856
+ pushLog('←', `PKT_SYS_INFO host=${si.hostname} uptime=${fmtUptimeSec(si.uptimeSec)}`);
857
+ };
858
+ const onOpen = () => {
859
+ pushLog('✓', 'connected');
860
+ pushLog('→', 'CMD_GET_SYS_INFO');
861
+ pushLog('→', 'CMD_GET_SNAPSHOT');
862
+ };
863
+ const onClose = (code) => pushLog('✗', `closed (${code})`);
864
+ client.on('metrics', onMetrics);
865
+ client.on('ack', onAck);
866
+ client.on('config', onConfig);
867
+ client.on('stats', onStats);
868
+ client.on('sysinfo', onSysInfo);
869
+ client.on('open', onOpen);
870
+ client.on('close', onClose);
871
+ return () => {
872
+ client.off('metrics', onMetrics);
873
+ client.off('ack', onAck);
874
+ client.off('config', onConfig);
875
+ client.off('stats', onStats);
876
+ client.off('sysinfo', onSysInfo);
877
+ client.off('open', onOpen);
878
+ client.off('close', onClose);
879
+ };
880
+ }, [client]);
881
+ react.useEffect(() => {
882
+ if (consoleOpen && logRef.current)
883
+ logRef.current.scrollTop = logRef.current.scrollHeight;
884
+ }, [wsLog, consoleOpen]);
885
+ const m = metrics;
886
+ const alerts = react.useMemo(() => (m ? detectAlerts(m, prevMetrics.current) : []), [m]);
887
+ const cpuHistory = history.current.map((x) => x.cpu);
888
+ const memHistory = history.current.map((x) => x.mem);
889
+ const netHistory = history.current.map((x) => x.netRx + x.netTx);
890
+ const duUsed = m ? m.duTotal - m.duFree : 0;
891
+ // Uptime: prefer sysinfo (system uptime), fallback to ring stats
892
+ const uptimeStr = sysinfo
893
+ ? fmtUptimeSec(sysinfo.uptimeSec)
894
+ : stats
895
+ ? fmtUptimeSec(Math.floor((Date.now() - stats.l1.firstTs) / 1000))
896
+ : '—';
897
+ return (jsxRuntime.jsxs("div", { className: className, style: Object.assign(Object.assign(Object.assign({}, s.root), { width: '100%' }), style), children: [jsxRuntime.jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }, children: [jsxRuntime.jsx("span", { style: s.badge(connected ? t.ok : t.crit), children: connected ? '● live' : '○ off' }), sysinfo && jsxRuntime.jsx("span", { style: { color: t.muted, fontSize: 11, minWidth: 80 }, children: sysinfo.hostname }), jsxRuntime.jsx("span", { style: { color: t.muted, fontSize: 10 }, children: "up" }), jsxRuntime.jsx("span", { style: Object.assign(Object.assign({}, s.numval), { fontSize: 11 }), children: uptimeStr }), jsxRuntime.jsx("span", { style: { flex: 1 } }), connected && (jsxRuntime.jsx("button", { style: s.btn, onClick: () => {
898
+ const next = !streaming;
899
+ setStreaming(next);
900
+ pushLog('→', next ? 'CMD_START' : 'CMD_STOP');
901
+ }, children: streaming ? '⏸ stop' : '▶ start' })), jsxRuntime.jsx("button", { style: s.btn, onClick: () => setConsoleOpen((o) => !o), children: consoleOpen ? '⊟ log' : '⊞ log' }), jsxRuntime.jsx("button", { style: s.btn, onClick: () => setMode(mode === 'compact' ? 'expanded' : 'compact'), children: mode === 'compact' ? '⊞' : '⊟' })] }), jsxRuntime.jsx("div", { style: s.divider }), jsxRuntime.jsxs("div", { style: { display: 'flex', gap: 16, flexWrap: 'wrap' }, children: [jsxRuntime.jsx(MetricBar, { label: "CPU", value: (_a = m === null || m === void 0 ? void 0 : m.cpu) !== null && _a !== void 0 ? _a : 0, color: t.cpu, t: t }), jsxRuntime.jsx(MetricBar, { label: "Mem", value: (_b = m === null || m === void 0 ? void 0 : m.mem) !== null && _b !== void 0 ? _b : 0, color: t.mem, t: t })] }), mode === 'expanded' && (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("div", { style: s.divider }), jsxRuntime.jsxs("div", { style: { display: 'flex', gap: 12, padding: '4px 0' }, children: [jsxRuntime.jsx(SparkBlock, { label: "CPU", data: cpuHistory, max: 10000, color: t.cpu }), jsxRuntime.jsx(SparkBlock, { label: "Mem", data: memHistory, max: 10000, color: t.mem }), jsxRuntime.jsx(SparkBlock, { label: "Net", data: netHistory, color: t.net })] })] })), jsxRuntime.jsx("div", { style: s.divider }), jsxRuntime.jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 6, flexWrap: 'wrap' }, children: [jsxRuntime.jsx("span", { style: s.label, children: "Load" }), jsxRuntime.jsx("span", { style: Object.assign(Object.assign({}, s.numval), { color: t.load }), children: m ? fmtLoad(m.load1) : '—' }), jsxRuntime.jsx("span", { style: { color: t.faint }, children: "/" }), jsxRuntime.jsx("span", { style: Object.assign(Object.assign({}, s.numval), { color: t.muted, fontSize: 11 }), children: m ? fmtLoad(m.load5) : '—' }), jsxRuntime.jsx("span", { style: { color: t.faint }, children: "/" }), jsxRuntime.jsx("span", { style: Object.assign(Object.assign({}, s.numval), { color: t.muted, fontSize: 11 }), children: m ? fmtLoad(m.load15) : '—' }), jsxRuntime.jsx("span", { style: { flex: 1 } }), jsxRuntime.jsx("span", { style: s.label, children: "Proc" }), jsxRuntime.jsx("span", { style: Object.assign({}, s.numval), children: m ? `${m.nrRunning}/${m.nrTotal}` : '—' })] }), jsxRuntime.jsx("div", { style: s.divider }), jsxRuntime.jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 6 }, children: [jsxRuntime.jsx("span", { style: s.label, children: "Disk" }), mode === 'expanded' ? (jsxRuntime.jsx("span", { style: {
902
+ display: 'inline-block',
903
+ width: 80,
904
+ height: 6,
905
+ background: t.surface,
906
+ borderRadius: 99,
907
+ overflow: 'hidden',
908
+ }, children: jsxRuntime.jsx("span", { style: {
909
+ display: 'block',
910
+ height: '100%',
911
+ background: t.disk,
912
+ borderRadius: 99,
913
+ width: `${((_c = m === null || m === void 0 ? void 0 : m.duUsage) !== null && _c !== void 0 ? _c : 0) / 100}%`,
914
+ transition: (_d = t.transition) !== null && _d !== void 0 ? _d : 'width 0.4s ease',
915
+ } }) })) : (jsxRuntime.jsx("span", { style: { color: t.disk, letterSpacing: 1, minWidth: 72 }, children: bar((_e = m === null || m === void 0 ? void 0 : m.duUsage) !== null && _e !== void 0 ? _e : 0) })), jsxRuntime.jsx("span", { style: Object.assign(Object.assign({}, s.numval), { color: t.disk }), children: m ? fmtPct(m.duUsage) : '—' }), jsxRuntime.jsx("span", { style: { color: t.faint, fontSize: 10, minWidth: 120 }, children: m ? `${fmtBytes(duUsed)} / ${fmtBytes(m.duTotal)}` : '—' })] }), jsxRuntime.jsx("div", { style: s.divider }), jsxRuntime.jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 6 }, children: [jsxRuntime.jsx("span", { style: s.label, children: "Net" }), jsxRuntime.jsx("span", { style: { color: t.net, minWidth: 20 }, children: "\u2191" }), jsxRuntime.jsx("span", { style: Object.assign(Object.assign({}, s.numval), { color: t.net }), children: m ? fmtBytes(m.netTx) + '/s' : '—' }), jsxRuntime.jsx("span", { style: { color: t.mem, minWidth: 20 }, children: "\u2193" }), jsxRuntime.jsx("span", { style: Object.assign(Object.assign({}, s.numval), { color: t.mem }), children: m ? fmtBytes(m.netRx) + '/s' : '—' })] }), alerts.length > 0 && (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("div", { style: s.divider }), jsxRuntime.jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 6, flexWrap: 'wrap' }, children: [jsxRuntime.jsx("span", { style: s.label, children: "Alerts" }), alerts.map((a) => (jsxRuntime.jsx("span", { style: s.alert(a.level), children: a.label }, a.id)))] })] })), jsxRuntime.jsx("div", { style: s.divider }), jsxRuntime.jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }, children: [jsxRuntime.jsx("span", { style: s.label, children: "Refresh" }), jsxRuntime.jsx("select", { style: s.select, value: intervalIdx, onChange: (e) => {
916
+ const idx = Number(e.target.value);
917
+ setIntervalIdx(idx);
918
+ if (client) {
919
+ const ms = INTERVALS[idx];
920
+ client.setInterval(ms);
921
+ pushLog('→', `CMD_SET_INTERVAL: ${ms}ms`);
922
+ }
923
+ }, children: INTERVAL_LABELS.map((l, i) => (jsxRuntime.jsx("option", { value: i, children: l }, i))) }), sysinfo && (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("span", { style: { color: t.faint, fontSize: 10 }, children: "\u2502" }), jsxRuntime.jsx("span", { style: { color: t.faint, fontSize: 10 }, children: sysinfo.osType }), jsxRuntime.jsx("span", { style: { color: t.faint, fontSize: 10 }, children: "\u2502" }), jsxRuntime.jsxs("span", { style: { color: t.faint, fontSize: 10 }, children: ["L1:", sysinfo.slotsL1, " L2:", sysinfo.slotsL2, " L3:", sysinfo.slotsL3] })] }))] }), consoleOpen && (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("div", { style: s.divider }), jsxRuntime.jsx("div", { ref: logRef, style: {
924
+ fontFamily: '"JetBrains Mono","Fira Code",monospace',
925
+ fontSize: 10,
926
+ background: t.surface,
927
+ border: `1px solid ${t.divider}`,
928
+ borderRadius: t.radius,
929
+ padding: '6px 8px',
930
+ maxHeight: 180,
931
+ overflowY: 'auto',
932
+ display: 'flex',
933
+ flexDirection: 'column',
934
+ gap: 1,
935
+ }, children: wsLog.length === 0 ? (jsxRuntime.jsx("span", { style: { color: t.faint }, children: "No packets yet\u2026" })) : (wsLog.map((line, i) => (jsxRuntime.jsx("div", { style: {
936
+ color: line.includes('→')
937
+ ? t.muted
938
+ : line.includes('←')
939
+ ? t.ok
940
+ : line.includes('✗')
941
+ ? t.crit
942
+ : t.muted,
943
+ }, children: line }, i)))) }), jsxRuntime.jsx("div", { style: { display: 'flex', justifyContent: 'flex-end' }, children: jsxRuntime.jsx("button", { style: s.btn, onClick: () => setWsLog([]), children: "clear" }) })] }))] }));
944
+ }
945
+ function MetricBar({ label, value, color, t }) {
946
+ const s = themeStyles(t);
947
+ return (jsxRuntime.jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 6, minWidth: 180 }, children: [jsxRuntime.jsx("span", { style: Object.assign(Object.assign({}, s.label), { minWidth: 28 }), children: label }), jsxRuntime.jsx("span", { style: { color, letterSpacing: 1, minWidth: 72, fontFamily: '"JetBrains Mono","Fira Code",monospace' }, children: bar(value) }), jsxRuntime.jsx("span", { style: Object.assign(Object.assign({}, s.numval), { color, minWidth: 48 }), children: fmtPct(value) })] }));
948
+ }
949
+ function SparkBlock({ label, data, max, color }) {
950
+ return (jsxRuntime.jsxs("div", { style: { display: 'flex', flexDirection: 'column', gap: 2, flex: 1 }, children: [jsxRuntime.jsx("span", { style: { fontSize: 10, color: '#9ca3af' }, children: label }), jsxRuntime.jsx(Sparkline, { data: data, max: max, color: color, fill: color + '22', width: 100, height: 28 })] }));
951
+ }
952
+
953
+ const LEVEL_LABELS = {
954
+ [RING_L1]: 'L1 (1h)',
955
+ [RING_L2]: 'L2 (24h)',
956
+ [RING_L3]: 'L3 (7d)',
957
+ };
958
+ function TimeSeriesChart({ metric, level = RING_L1, maxSamples = 60, height = 180, className, style, theme: themeProp, }) {
959
+ const base = useTheme();
960
+ const t = themeProp ? Object.assign(Object.assign({}, base), themeProp) : base;
961
+ const s = themeStyles(t);
962
+ const { client, connected } = useTinyTrack();
963
+ const [data, setData] = react.useState([]);
964
+ const addSamples = react.useCallback((samples) => {
965
+ setData((prev) => [...prev, ...samples].slice(-maxSamples));
966
+ }, [maxSamples]);
967
+ react.useEffect(() => {
968
+ if (!connected || !client)
969
+ return;
970
+ client.subscribe(level, 0);
971
+ client.getHistory(level, maxSamples);
972
+ const onHistory = (r) => {
973
+ if (r.level === level)
974
+ addSamples(r.samples);
975
+ };
976
+ const onMetrics = (m) => {
977
+ addSamples([m]);
978
+ };
979
+ client.on('history', onHistory);
980
+ client.on('metrics', onMetrics);
981
+ return () => {
982
+ client.off('history', onHistory);
983
+ client.off('metrics', onMetrics);
984
+ };
985
+ }, [connected, client, level, maxSamples, addSamples]);
986
+ const color = metricColor(metric, t);
987
+ const values = data.map((m) => extractValue$1(m, metric));
988
+ const maxVal = metric === 'cpu' || metric === 'mem' || metric === 'disk' ? 10000 : Math.max(...values, 1);
989
+ const latest = data[data.length - 1];
990
+ const latestVal = latest ? extractValue$1(latest, metric) : null;
991
+ return (jsxRuntime.jsxs("div", { className: className, style: Object.assign(Object.assign(Object.assign({}, s.root), { gap: 6 }), style), children: [jsxRuntime.jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 8 }, children: [jsxRuntime.jsx("span", { style: { fontSize: 13, fontWeight: 600, color: t.text }, children: metricLabel(metric) }), jsxRuntime.jsx("span", { style: { fontSize: 9, padding: '1px 4px', background: t.surface, borderRadius: t.radius, color: t.muted }, children: LEVEL_LABELS[level] }), jsxRuntime.jsx("span", { style: { flex: 1 } }), jsxRuntime.jsx("span", { style: Object.assign(Object.assign({}, s.value), { color, fontWeight: 600 }), children: latestVal !== null ? formatValue$1(latestVal, metric) : '—' })] }), jsxRuntime.jsxs("svg", { width: "100%", height: height, viewBox: `0 0 400 ${height}`, preserveAspectRatio: "none", style: { display: 'block' }, children: [values.length > 1 && jsxRuntime.jsx(ChartPath, { values: values, maxVal: maxVal, height: height, color: color }), values.length === 0 && (jsxRuntime.jsx("text", { x: "200", y: height / 2, textAnchor: "middle", fill: t.faint, fontSize: "12", children: "waiting for data\u2026" }))] }), jsxRuntime.jsxs("div", { style: { display: 'flex', justifyContent: 'space-between', fontSize: 10 }, children: [jsxRuntime.jsxs("span", { style: { color: t.muted }, children: [data.length, " samples"] }), latest && jsxRuntime.jsx("span", { style: { color: t.muted }, children: new Date(latest.timestamp).toLocaleTimeString() })] })] }));
992
+ }
993
+ function ChartPath({ values, maxVal, height, color, }) {
994
+ const W = 400;
995
+ const PAD = 4;
996
+ const n = values.length;
997
+ const pts = values.map((v, i) => {
998
+ const x = (i / (n - 1)) * W;
999
+ const y = PAD + (1 - v / maxVal) * (height - PAD * 2);
1000
+ return [x, y];
1001
+ });
1002
+ const line = pts.map(([x, y], i) => `${i === 0 ? 'M' : 'L'}${x.toFixed(1)},${y.toFixed(1)}`).join(' ');
1003
+ return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("path", { d: `${line} L${W},${height} L0,${height} Z`, fill: color + '20' }), jsxRuntime.jsx("path", { d: line, fill: "none", stroke: color, strokeWidth: "1.5", strokeLinejoin: "round" })] }));
1004
+ }
1005
+ function extractValue$1(m, metric) {
1006
+ switch (metric) {
1007
+ case 'cpu':
1008
+ return m.cpu;
1009
+ case 'mem':
1010
+ return m.mem;
1011
+ case 'load':
1012
+ return m.load1;
1013
+ case 'net':
1014
+ return m.netRx + m.netTx;
1015
+ case 'disk':
1016
+ return m.duUsage;
1017
+ default:
1018
+ return 0;
1019
+ }
1020
+ }
1021
+ function formatValue$1(val, metric) {
1022
+ if (metric === 'cpu' || metric === 'mem' || metric === 'disk')
1023
+ return fmtPct(val);
1024
+ if (metric === 'load')
1025
+ return fmtLoad(val);
1026
+ if (metric === 'net')
1027
+ return fmtBytes(val) + '/s';
1028
+ return String(val);
1029
+ }
1030
+ function metricLabel(m) {
1031
+ return ({ cpu: 'CPU', mem: 'Memory', load: 'Load avg', net: 'Network', disk: 'Disk' }[m] || m);
1032
+ }
1033
+ function metricColor(m, t) {
1034
+ return { cpu: t.cpu, mem: t.mem, load: t.load, net: t.net, disk: t.disk }[m] || t.muted;
1035
+ }
1036
+
1037
+ const RINGS = [
1038
+ { level: RING_L1, label: 'L1 · 1s', span: 3600, fmt: (ts) => fmtTime(ts) },
1039
+ { level: RING_L2, label: 'L2 · 1m', span: 86400, fmt: (ts) => fmtHour(ts) },
1040
+ { level: RING_L3, label: 'L3 · 1h', span: 7 * 86400, fmt: (ts) => fmtDay(ts) },
1041
+ ];
1042
+ const METRIC_COLOR = {
1043
+ cpu: '#4ade80',
1044
+ mem: '#60a5fa',
1045
+ load: '#a78bfa',
1046
+ net: '#f59e0b',
1047
+ disk: '#fb923c',
1048
+ };
1049
+ const METRIC_LABEL = {
1050
+ cpu: 'CPU',
1051
+ mem: 'Mem',
1052
+ load: 'Load',
1053
+ net: 'Net',
1054
+ disk: 'Disk',
1055
+ };
1056
+ // ---------------------------------------------------------------------------
1057
+ // Main component
1058
+ // ---------------------------------------------------------------------------
1059
+ function Timeline({ metric = 'cpu', rowHeight = 40, className, style, theme: themeProp }) {
1060
+ const base = useTheme();
1061
+ const t = themeProp ? Object.assign(Object.assign({}, base), themeProp) : base;
1062
+ const s = themeStyles(t);
1063
+ const { client, connected } = useTinyTrack();
1064
+ const [samples, setSamples] = react.useState({
1065
+ [RING_L1]: [],
1066
+ [RING_L2]: [],
1067
+ [RING_L3]: [],
1068
+ });
1069
+ const color = METRIC_COLOR[metric];
1070
+ // Accumulate history per ring level
1071
+ const addSamples = react.useCallback((level, incoming) => {
1072
+ setSamples((prev) => {
1073
+ const merged = [...prev[level], ...incoming];
1074
+ const map = new Map(merged.map((m) => [m.timestamp, m]));
1075
+ return Object.assign(Object.assign({}, prev), { [level]: [...map.values()].sort((a, b) => a.timestamp - b.timestamp) });
1076
+ });
1077
+ }, []);
1078
+ react.useEffect(() => {
1079
+ if (!connected || !client)
1080
+ return;
1081
+ client.getHistory(RING_L1, 3600);
1082
+ client.getHistory(RING_L2, 1440);
1083
+ client.getHistory(RING_L3, 168);
1084
+ const onHistory = (r) => addSamples(r.level, r.samples);
1085
+ const onMetrics = (m) => addSamples(RING_L1, [m]);
1086
+ client.on('history', onHistory);
1087
+ client.on('metrics', onMetrics);
1088
+ return () => {
1089
+ client.off('history', onHistory);
1090
+ client.off('metrics', onMetrics);
1091
+ };
1092
+ }, [connected, client, addSamples]);
1093
+ return (jsxRuntime.jsxs("div", { className: className, style: Object.assign(Object.assign({}, s.root), style), children: [jsxRuntime.jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 8 }, children: [jsxRuntime.jsxs("span", { style: { fontSize: 13, fontWeight: 600, color: t.text }, children: ["Timeline \u00B7 ", METRIC_LABEL[metric]] }), jsxRuntime.jsx("span", { style: { fontSize: 9, padding: '1px 5px', background: t.surface, borderRadius: t.radius, color }, children: METRIC_LABEL[metric] }), jsxRuntime.jsx("span", { style: { flex: 1 } }), jsxRuntime.jsx("span", { style: { fontSize: 10, color: t.faint }, children: "scroll \u2192" })] }), jsxRuntime.jsx("div", { style: s.divider }), RINGS.map((ring) => (jsxRuntime.jsx(TimelineRow, { label: ring.label, data: samples[ring.level], metric: metric, color: color, rowHeight: rowHeight, spanSec: ring.span, fmtTick: ring.fmt, t: t }, ring.level)))] }));
1094
+ }
1095
+ function TimelineRow({ label, data, metric, color, rowHeight, spanSec, fmtTick, t }) {
1096
+ const scrollRef = react.useRef(null);
1097
+ const [tooltip, setTooltip] = react.useState(null);
1098
+ // Auto-scroll to right (latest) when new data arrives
1099
+ react.useEffect(() => {
1100
+ const el = scrollRef.current;
1101
+ if (el)
1102
+ el.scrollLeft = el.scrollWidth;
1103
+ }, [data.length]);
1104
+ const values = react.useMemo(() => data.map((m) => extractValue(m, metric)), [data, metric]);
1105
+ const maxVal = metric === 'cpu' || metric === 'mem' || metric === 'disk' ? 10000 : Math.max(...values, 1);
1106
+ // Canvas-like: each sample = 4px wide bar
1107
+ const BAR_W = 4;
1108
+ const totalW = Math.max(data.length * BAR_W, 600);
1109
+ const H = rowHeight;
1110
+ const latest = data[data.length - 1];
1111
+ const latestVal = latest ? extractValue(latest, metric) : null;
1112
+ // Tick marks: every ~80px
1113
+ const tickInterval = Math.max(1, Math.floor(80 / BAR_W));
1114
+ const ticks = [];
1115
+ data.forEach((m, i) => {
1116
+ if (i % tickInterval === 0)
1117
+ ticks.push({ x: i * BAR_W, label: fmtTick(m.timestamp) });
1118
+ });
1119
+ const handleMouseMove = react.useCallback((e) => {
1120
+ const rect = e.currentTarget.getBoundingClientRect();
1121
+ const x = e.clientX - rect.left;
1122
+ const idx = Math.floor(x / BAR_W);
1123
+ if (idx >= 0 && idx < data.length) {
1124
+ const m = data[idx];
1125
+ const v = extractValue(m, metric);
1126
+ setTooltip({ x, text: `${fmtTick(m.timestamp)} ${formatValue(v, metric)}` });
1127
+ }
1128
+ }, [data, metric, fmtTick]);
1129
+ return (jsxRuntime.jsxs("div", { style: { display: 'flex', flexDirection: 'column', gap: 2 }, children: [jsxRuntime.jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 6 }, children: [jsxRuntime.jsx("span", { style: { color: t.muted, fontSize: 10, minWidth: 72 }, children: label }), jsxRuntime.jsx("span", { style: { color, fontSize: 11, fontWeight: 600 }, children: latestVal !== null ? formatValue(latestVal, metric) : '—' }), jsxRuntime.jsxs("span", { style: { color: t.faint, fontSize: 9, marginLeft: 'auto' }, children: [data.length, "pts"] })] }), jsxRuntime.jsx("div", { ref: scrollRef, style: {
1130
+ overflowX: 'auto',
1131
+ overflowY: 'hidden',
1132
+ background: t.surface,
1133
+ borderRadius: t.radius,
1134
+ border: `1px solid ${t.divider}`,
1135
+ height: H + 16,
1136
+ position: 'relative',
1137
+ }, children: jsxRuntime.jsxs("div", { style: { position: 'relative', width: totalW, height: H + 16 }, children: [jsxRuntime.jsxs("svg", { width: totalW, height: H, style: { display: 'block', cursor: 'crosshair' }, onMouseMove: handleMouseMove, onMouseLeave: () => setTooltip(null), children: [values.map((v, i) => {
1138
+ const barH = Math.max(1, (v / maxVal) * H);
1139
+ return (jsxRuntime.jsx("rect", { x: i * BAR_W, y: H - barH, width: BAR_W - 1, height: barH, fill: color, opacity: 0.3 + (v / maxVal) * 0.7 }, i));
1140
+ }), tooltip && (jsxRuntime.jsx("line", { x1: tooltip.x, y1: 0, x2: tooltip.x, y2: H, stroke: t.text, strokeWidth: 1, opacity: 0.3 }))] }), jsxRuntime.jsx("div", { style: { position: 'relative', height: 16, overflow: 'hidden' }, children: ticks.map((tk, i) => (jsxRuntime.jsx("span", { style: {
1141
+ position: 'absolute',
1142
+ top: 0,
1143
+ fontSize: 9,
1144
+ color: t.faint,
1145
+ whiteSpace: 'nowrap',
1146
+ transform: 'translateX(-50%)',
1147
+ left: tk.x,
1148
+ }, children: tk.label }, i))) }), tooltip && (jsxRuntime.jsx("div", { style: {
1149
+ position: 'absolute',
1150
+ top: 2,
1151
+ left: tooltip.x + 6,
1152
+ fontSize: 10,
1153
+ background: t.surface,
1154
+ border: `1px solid ${t.border}`,
1155
+ borderRadius: t.radius,
1156
+ padding: '2px 6px',
1157
+ color: t.text,
1158
+ whiteSpace: 'nowrap',
1159
+ pointerEvents: 'none',
1160
+ zIndex: 10,
1161
+ }, children: tooltip.text }))] }) })] }));
1162
+ }
1163
+ // ---------------------------------------------------------------------------
1164
+ // Helpers
1165
+ // ---------------------------------------------------------------------------
1166
+ function extractValue(m, metric) {
1167
+ switch (metric) {
1168
+ case 'cpu':
1169
+ return m.cpu;
1170
+ case 'mem':
1171
+ return m.mem;
1172
+ case 'load':
1173
+ return m.load1;
1174
+ case 'net':
1175
+ return m.netRx + m.netTx;
1176
+ case 'disk':
1177
+ return m.duUsage;
1178
+ }
1179
+ }
1180
+ function formatValue(val, metric) {
1181
+ if (metric === 'cpu' || metric === 'mem' || metric === 'disk')
1182
+ return fmtPct(val);
1183
+ if (metric === 'load')
1184
+ return fmtLoad(val);
1185
+ if (metric === 'net')
1186
+ return fmtBytes(val) + '/s';
1187
+ return String(val);
1188
+ }
1189
+ function fmtTime(ts) {
1190
+ return new Date(ts).toLocaleTimeString('en', {
1191
+ hour12: false,
1192
+ hour: '2-digit',
1193
+ minute: '2-digit',
1194
+ second: '2-digit',
1195
+ });
1196
+ }
1197
+ function fmtHour(ts) {
1198
+ return new Date(ts).toLocaleTimeString('en', { hour12: false, hour: '2-digit', minute: '2-digit' });
1199
+ }
1200
+ function fmtDay(ts) {
1201
+ const d = new Date(ts);
1202
+ return `${d.getMonth() + 1}/${d.getDate()} ${String(d.getHours()).padStart(2, '0')}h`;
1203
+ }
1204
+
1205
+ exports.Dashboard = Dashboard;
1206
+ exports.MetricsBar = MetricsBar;
1207
+ exports.MetricsPanel = MetricsPanel;
1208
+ exports.THEMES = THEMES;
1209
+ exports.ThemeProvider = ThemeProvider;
1210
+ exports.TimeSeriesChart = TimeSeriesChart;
1211
+ exports.Timeline = Timeline;
1212
+ exports.TinyTrackContext = TinyTrackContext;
1213
+ exports.TinyTrackProvider = TinyTrackProvider;
1214
+ exports.useHistory = useHistory;
1215
+ exports.useMetrics = useMetrics;
1216
+ exports.useTheme = useTheme;
1217
+ exports.useTinyTrack = useTinyTrack;