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