claw-dashboard 1.9.0 → 2.1.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.
Files changed (135) hide show
  1. package/.c8rc.json +38 -0
  2. package/.dockerignore +68 -0
  3. package/.github/dependabot.yml +45 -0
  4. package/.github/pull_request_template.md +39 -0
  5. package/.github/workflows/ci.yml +147 -0
  6. package/.github/workflows/release.yml +40 -0
  7. package/.github/workflows/security.yml +84 -0
  8. package/.husky/pre-commit +1 -0
  9. package/.planning/codebase/ARCHITECTURE.md +206 -0
  10. package/.planning/codebase/INTEGRATIONS.md +150 -0
  11. package/.planning/codebase/STACK.md +122 -0
  12. package/.planning/codebase/STRUCTURE.md +201 -0
  13. package/CHANGELOG.md +293 -0
  14. package/CONTRIBUTING.md +378 -0
  15. package/Dockerfile +54 -0
  16. package/FEATURES.md +195 -21
  17. package/README.md +83 -6
  18. package/TODO.md +28 -0
  19. package/build-cjs.js +127 -0
  20. package/cjs-shim.js +13 -0
  21. package/dist/clawdash +1729 -0
  22. package/dist/clawdash.meta.json +2236 -0
  23. package/dist/widgets.cjs +6511 -0
  24. package/docker-compose.yml +67 -0
  25. package/docs/API.md +1050 -0
  26. package/docs/PLUGINS.md +1504 -0
  27. package/esbuild.config.js +158 -0
  28. package/eslint.config.js +56 -0
  29. package/examples/plugins/README.md +122 -0
  30. package/examples/plugins/api-status/index.js +294 -0
  31. package/examples/plugins/api-status/plugin.json +19 -0
  32. package/examples/plugins/hello-world/index.js +104 -0
  33. package/examples/plugins/hello-world/plugin.json +15 -0
  34. package/examples/plugins/system-metrics-chart/index.js +339 -0
  35. package/examples/plugins/system-metrics-chart/plugin.json +18 -0
  36. package/examples/plugins/weather-widget/index.js +136 -0
  37. package/examples/plugins/weather-widget/plugin.json +19 -0
  38. package/index.cjs +23005 -0
  39. package/index.js +5285 -566
  40. package/jest.config.js +11 -0
  41. package/man/clawdash.1 +276 -0
  42. package/package.json +54 -6
  43. package/schemas/plugin-manifest.json +128 -0
  44. package/scripts/release.js +595 -0
  45. package/src/alerts.js +693 -0
  46. package/src/auto-save.js +584 -0
  47. package/src/cache.js +390 -0
  48. package/src/checksum.js +146 -0
  49. package/src/cli/args.js +110 -0
  50. package/src/cli/export-schedule.js +423 -0
  51. package/src/cli/export-snapshot.js +138 -0
  52. package/src/cli/help.js +69 -0
  53. package/src/cli/import-snapshot.js +230 -0
  54. package/src/cli/index.js +14 -0
  55. package/src/cli/list-templates.js +69 -0
  56. package/src/cli/validate-config.js +76 -0
  57. package/src/cli/validate-plugin.js +187 -0
  58. package/src/cli/version.js +15 -0
  59. package/src/config-validator.js +586 -0
  60. package/src/config-watcher.js +401 -0
  61. package/src/config.js +684 -0
  62. package/src/container-detector.js +499 -0
  63. package/src/database.js +734 -0
  64. package/src/differential-render.js +327 -0
  65. package/src/errors.js +169 -0
  66. package/src/export-scheduler.js +581 -0
  67. package/src/gateway-manager.js +685 -0
  68. package/src/hints.js +371 -0
  69. package/src/loading-states.js +509 -0
  70. package/src/logger.js +185 -0
  71. package/src/memory-pressure.js +467 -0
  72. package/src/performance-monitor.js +374 -0
  73. package/src/plugin-errors.js +586 -0
  74. package/src/plugin-manifest-validator.js +219 -0
  75. package/src/plugin-reload.js +481 -0
  76. package/src/plugin-scaffold.js +1934 -0
  77. package/src/plugin-validator.js +284 -0
  78. package/src/retry.js +218 -0
  79. package/src/security.js +860 -0
  80. package/src/snapshot.js +372 -0
  81. package/src/splash.js +115 -0
  82. package/src/theme-selector.js +411 -0
  83. package/src/themes.js +874 -0
  84. package/src/transitions.js +534 -0
  85. package/src/types.d.ts +174 -0
  86. package/src/validation.js +926 -0
  87. package/src/web-server.js +885 -0
  88. package/src/widgets/builtin-widgets.js +1056 -0
  89. package/src/widgets/config-processor.js +401 -0
  90. package/src/widgets/dependency-resolver.js +596 -0
  91. package/src/widgets/index.js +79 -0
  92. package/src/widgets/plugin-api.js +845 -0
  93. package/src/widgets/widget-error-boundary.js +773 -0
  94. package/src/widgets/widget-error-isolation.js +551 -0
  95. package/src/widgets/widget-loader.js +1437 -0
  96. package/src/workers/system-worker.js +130 -0
  97. package/src/workers/worker-pool.js +633 -0
  98. package/tests/alerts.test.js +437 -0
  99. package/tests/auto-save.test.js +529 -0
  100. package/tests/cache.test.js +317 -0
  101. package/tests/cli.test.js +351 -0
  102. package/tests/command-palette.test.js +320 -0
  103. package/tests/config-processor.test.js +452 -0
  104. package/tests/config-validator.test.js +710 -0
  105. package/tests/config-watcher.test.js +594 -0
  106. package/tests/config.test.js +755 -0
  107. package/tests/database.test.js +438 -0
  108. package/tests/errors.test.js +189 -0
  109. package/tests/example-plugins.test.js +624 -0
  110. package/tests/integration.test.js +1321 -0
  111. package/tests/loading-states.test.js +300 -0
  112. package/tests/manifest-validation-on-load.test.js +671 -0
  113. package/tests/performance-monitor.test.js +190 -0
  114. package/tests/plugin-api-rate-limit.test.js +302 -0
  115. package/tests/plugin-errors.test.js +311 -0
  116. package/tests/plugin-lifecycle-e2e.test.js +1036 -0
  117. package/tests/plugin-reload.test.js +764 -0
  118. package/tests/rate-limiter.test.js +539 -0
  119. package/tests/retry.test.js +308 -0
  120. package/tests/security.test.js +411 -0
  121. package/tests/settings-modal.test.js +226 -0
  122. package/tests/theme-selector.test.js +57 -0
  123. package/tests/utils.js +242 -0
  124. package/tests/utils.test.js +317 -0
  125. package/tests/validate-plugin-cli.test.js +359 -0
  126. package/tests/validation.test.js +837 -0
  127. package/tests/web-server.test.js +646 -0
  128. package/tests/widget-arrange-mode.test.js +183 -0
  129. package/tests/widget-config-hot-reload.test.js +465 -0
  130. package/tests/widget-dependency.test.js +591 -0
  131. package/tests/widget-error-boundary.test.js +749 -0
  132. package/tests/widget-error-isolation.test.js +469 -0
  133. package/tests/widget-integration.test.js +1147 -0
  134. package/tests/widget-refresh-intervals.test.js +284 -0
  135. package/tests/worker-pool.test.js +522 -0
@@ -0,0 +1,1056 @@
1
+ /**
2
+ * Built-in System Widgets for Claw Dashboard
3
+ * Each widget module can be lazy loaded on demand
4
+ */
5
+
6
+ import blessed from 'blessed';
7
+ import { BaseWidget } from './plugin-api.js';
8
+
9
+ /**
10
+ * CPU Widget - Displays CPU usage with gauge and sparkline
11
+ */
12
+ export class CpuWidget extends BaseWidget {
13
+ constructor(options = {}) {
14
+ super(options);
15
+ this.name = 'CPU';
16
+ this.description = 'CPU usage and history';
17
+ this.history = [];
18
+ this.maxHistory = 60;
19
+ }
20
+
21
+ async create(screen, theme = {}) {
22
+ const C = theme.colors || {};
23
+
24
+ this.box = blessed.box({
25
+ parent: screen,
26
+ height: 5,
27
+ border: { type: 'line' },
28
+ label: ' CPU ',
29
+ style: { border: { fg: C.cyan || 'cyan' } },
30
+ });
31
+
32
+ this.valueText = blessed.text({
33
+ parent: this.box,
34
+ top: 0,
35
+ left: 'center',
36
+ content: '0%',
37
+ style: { fg: C.brightGreen || 'bright-green', bold: true },
38
+ });
39
+
40
+ this.detailText = blessed.text({
41
+ parent: this.box,
42
+ top: 1,
43
+ left: 'center',
44
+ content: '',
45
+ style: { fg: C.gray || 'gray' },
46
+ });
47
+
48
+ return this;
49
+ }
50
+
51
+ async getData(dataProvider) {
52
+ if (dataProvider) {
53
+ return dataProvider('cpu');
54
+ }
55
+ return null;
56
+ }
57
+
58
+ update(data) {
59
+ if (!data || !this.box) return;
60
+
61
+ const percent = data.avg || 0;
62
+ const cores = data.cores || 1;
63
+
64
+ // Update history
65
+ this.history.push(percent);
66
+ if (this.history.length > this.maxHistory) {
67
+ this.history.shift();
68
+ }
69
+
70
+ // Update display
71
+ const color = percent > 90 ? 'red' : percent > 70 ? 'yellow' : 'green';
72
+ const gaugeWidth = 15;
73
+ const filled = Math.round((percent / 100) * gaugeWidth);
74
+ const gauge = '█'.repeat(filled) + '░'.repeat(gaugeWidth - filled);
75
+
76
+ this.valueText.setContent(`{${color}-fg}${percent}%{/${color}-fg} ${gauge}`);
77
+ this.detailText.setContent(`${cores} cores`);
78
+ }
79
+
80
+ render(data) {
81
+ this.update(data);
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Memory Widget - Displays memory usage with gauge and sparkline
87
+ */
88
+ export class MemoryWidget extends BaseWidget {
89
+ constructor(options = {}) {
90
+ super(options);
91
+ this.name = 'Memory';
92
+ this.description = 'Memory usage and history';
93
+ this.history = [];
94
+ this.maxHistory = 60;
95
+ }
96
+
97
+ async create(screen, theme = {}) {
98
+ const C = theme.colors || {};
99
+
100
+ this.box = blessed.box({
101
+ parent: screen,
102
+ height: 5,
103
+ border: { type: 'line' },
104
+ label: ' MEMORY ',
105
+ style: { border: { fg: C.magenta || 'magenta' } },
106
+ });
107
+
108
+ this.valueText = blessed.text({
109
+ parent: this.box,
110
+ top: 0,
111
+ left: 'center',
112
+ content: '0%',
113
+ style: { fg: C.brightMagenta || 'bright-magenta', bold: true },
114
+ });
115
+
116
+ this.detailText = blessed.text({
117
+ parent: this.box,
118
+ top: 1,
119
+ left: 'center',
120
+ content: '',
121
+ style: { fg: C.gray || 'gray' },
122
+ });
123
+
124
+ return this;
125
+ }
126
+
127
+ async getData(dataProvider) {
128
+ if (dataProvider) {
129
+ return dataProvider('memory');
130
+ }
131
+ return null;
132
+ }
133
+
134
+ update(data) {
135
+ if (!data || !this.box) return;
136
+
137
+ const percent = data.percent || 0;
138
+ const used = data.used || 0;
139
+ const total = data.total || 0;
140
+
141
+ // Update history
142
+ this.history.push(percent);
143
+ if (this.history.length > this.maxHistory) {
144
+ this.history.shift();
145
+ }
146
+
147
+ // Update display
148
+ const color = percent > 90 ? 'red' : percent > 75 ? 'yellow' : 'green';
149
+ const gaugeWidth = 15;
150
+ const filled = Math.round((percent / 100) * gaugeWidth);
151
+ const gauge = '█'.repeat(filled) + '░'.repeat(gaugeWidth - filled);
152
+
153
+ this.valueText.setContent(`{${color}-fg}${percent}%{/${color}-fg} ${gauge}`);
154
+ this.detailText.setContent(`${used}/${total} GB`);
155
+ }
156
+
157
+ render(data) {
158
+ this.update(data);
159
+ }
160
+ }
161
+
162
+ /**
163
+ * GPU Widget - Displays GPU information
164
+ */
165
+ export class GpuWidget extends BaseWidget {
166
+ constructor(options = {}) {
167
+ super(options);
168
+ this.name = 'GPU';
169
+ this.description = 'GPU usage and temperature';
170
+ }
171
+
172
+ async create(screen, theme = {}) {
173
+ const C = theme.colors || {};
174
+
175
+ this.box = blessed.box({
176
+ parent: screen,
177
+ height: 5,
178
+ border: { type: 'line' },
179
+ label: ' GPU ',
180
+ style: { border: { fg: C.yellow || 'yellow' } },
181
+ });
182
+
183
+ this.valueText = blessed.text({
184
+ parent: this.box,
185
+ top: 0,
186
+ left: 'center',
187
+ content: 'Detecting...',
188
+ style: { fg: C.brightYellow || 'bright-yellow', bold: true },
189
+ });
190
+
191
+ this.detailText = blessed.text({
192
+ parent: this.box,
193
+ top: 1,
194
+ left: 'center',
195
+ content: '',
196
+ style: { fg: C.gray || 'gray' },
197
+ });
198
+
199
+ return this;
200
+ }
201
+
202
+ async getData(dataProvider) {
203
+ if (dataProvider) {
204
+ return dataProvider('gpu');
205
+ }
206
+ return null;
207
+ }
208
+
209
+ update(data) {
210
+ if (!this.box) return;
211
+
212
+ if (!data) {
213
+ this.valueText.setContent('Not detected');
214
+ this.detailText.setContent('');
215
+ return;
216
+ }
217
+
218
+ const utilization = data.utilization;
219
+ const temp = data.temperature;
220
+ const short = data.short || 'GPU';
221
+
222
+ let value = short;
223
+ if (utilization !== null) {
224
+ value += ` ${utilization}%`;
225
+ }
226
+ if (temp !== null) {
227
+ value += ` ${temp}°C`;
228
+ }
229
+
230
+ this.valueText.setContent(value);
231
+ this.detailText.setContent(data.memoryUsed && data.memoryTotal
232
+ ? `${data.memoryUsed}/${data.memoryTotal} GB`
233
+ : '');
234
+ }
235
+
236
+ render(data) {
237
+ this.update(data);
238
+ }
239
+ }
240
+
241
+ /**
242
+ * Network Widget - Displays network activity
243
+ */
244
+ export class NetworkWidget extends BaseWidget {
245
+ constructor(options = {}) {
246
+ super(options);
247
+ this.name = 'Network';
248
+ this.description = 'Network activity';
249
+ this.history = [];
250
+ this.maxHistory = 30;
251
+ }
252
+
253
+ async create(screen, theme = {}) {
254
+ const C = theme.colors || {};
255
+
256
+ this.box = blessed.box({
257
+ parent: screen,
258
+ height: 5,
259
+ border: { type: 'line' },
260
+ label: ' NETWORK ',
261
+ style: { border: { fg: C.brightCyan || 'bright-cyan' } },
262
+ });
263
+
264
+ this.valueText = blessed.text({
265
+ parent: this.box,
266
+ top: 0,
267
+ left: 'center',
268
+ content: 'Loading...',
269
+ style: { fg: C.brightCyan || 'bright-cyan', bold: true },
270
+ });
271
+
272
+ this.detailText = blessed.text({
273
+ parent: this.box,
274
+ top: 1,
275
+ left: 'center',
276
+ content: '',
277
+ style: { fg: C.gray || 'gray' },
278
+ });
279
+
280
+ this.sparkline = contrib.sparkline({
281
+ parent: this.box,
282
+ top: 2,
283
+ left: 'center',
284
+ width: 20,
285
+ height: 1,
286
+ style: { fg: C.cyan || 'cyan' },
287
+ });
288
+
289
+ return this;
290
+ }
291
+
292
+ async getData(dataProvider) {
293
+ if (dataProvider) {
294
+ return dataProvider('network');
295
+ }
296
+ return null;
297
+ }
298
+
299
+ formatBytes(bytes) {
300
+ if (bytes === 0) return '0 B';
301
+ const k = 1024;
302
+ const sizes = ['B', 'KB', 'MB', 'GB'];
303
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
304
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
305
+ }
306
+
307
+ update(data) {
308
+ if (!data || !this.box) return;
309
+
310
+ const iface = data.interface || 'unknown';
311
+ const rx = data.rxSec || 0;
312
+ const tx = data.txSec || 0;
313
+ const total = rx + tx;
314
+
315
+ // Update history
316
+ this.history.push(total);
317
+ if (this.history.length > this.maxHistory) {
318
+ this.history.shift();
319
+ }
320
+
321
+ this.valueText.setContent(iface);
322
+ this.detailText.setContent(`↓${this.formatBytes(rx)} ↑${this.formatBytes(tx)}`);
323
+
324
+ if (this.sparkline && this.history.length > 1) {
325
+ this.sparkline.setData([this.history]);
326
+ }
327
+ }
328
+
329
+ render(data) {
330
+ this.update(data);
331
+ }
332
+ }
333
+
334
+ /**
335
+ * Disk Widget - Displays disk usage
336
+ */
337
+ export class DiskWidget extends BaseWidget {
338
+ constructor(options = {}) {
339
+ super(options);
340
+ this.name = 'Disk';
341
+ this.description = 'Disk usage';
342
+ }
343
+
344
+ async create(screen, theme = {}) {
345
+ const C = theme.colors || {};
346
+
347
+ this.box = blessed.box({
348
+ parent: screen,
349
+ height: 5,
350
+ border: { type: 'line' },
351
+ label: ' DISK ',
352
+ style: { border: { fg: C.green || 'green' } },
353
+ });
354
+
355
+ this.valueText = blessed.text({
356
+ parent: this.box,
357
+ top: 0,
358
+ left: 'center',
359
+ content: '0%',
360
+ style: { fg: C.brightGreen || 'bright-green', bold: true },
361
+ });
362
+
363
+ this.detailText = blessed.text({
364
+ parent: this.box,
365
+ top: 1,
366
+ left: 'center',
367
+ content: '',
368
+ style: { fg: C.gray || 'gray' },
369
+ });
370
+
371
+ return this;
372
+ }
373
+
374
+ async getData(dataProvider) {
375
+ if (dataProvider) {
376
+ return dataProvider('disk');
377
+ }
378
+ return null;
379
+ }
380
+
381
+ update(data) {
382
+ if (!data || !this.box) return;
383
+
384
+ const percent = data.percent || 0;
385
+ const used = data.used || 0;
386
+ const size = data.size || 0;
387
+
388
+ const color = percent > 90 ? 'red' : percent > 80 ? 'yellow' : 'green';
389
+ const gaugeWidth = 15;
390
+ const filled = Math.round((percent / 100) * gaugeWidth);
391
+ const gauge = '█'.repeat(filled) + '░'.repeat(gaugeWidth - filled);
392
+
393
+ this.valueText.setContent(`{${color}-fg}${percent}%{/${color}-fg} ${gauge}`);
394
+ this.detailText.setContent(`${used}/${size} GB`);
395
+ }
396
+
397
+ render(data) {
398
+ this.update(data);
399
+ }
400
+ }
401
+
402
+ /**
403
+ * System Widget - Displays system information
404
+ */
405
+ export class SystemWidget extends BaseWidget {
406
+ constructor(options = {}) {
407
+ super(options);
408
+ this.name = 'System';
409
+ this.description = 'System information';
410
+ }
411
+
412
+ async create(screen, theme = {}) {
413
+ const C = theme.colors || {};
414
+
415
+ this.box = blessed.box({
416
+ parent: screen,
417
+ height: 5,
418
+ border: { type: 'line' },
419
+ label: ' SYSTEM ',
420
+ style: { border: { fg: C.gray || 'gray' } },
421
+ });
422
+
423
+ this.line1 = blessed.text({
424
+ parent: this.box,
425
+ top: 0,
426
+ left: 'center',
427
+ content: '...',
428
+ style: { fg: C.gray || 'gray' },
429
+ });
430
+
431
+ this.line2 = blessed.text({
432
+ parent: this.box,
433
+ top: 1,
434
+ left: 'center',
435
+ content: '',
436
+ style: { fg: C.gray || 'gray' },
437
+ });
438
+
439
+ return this;
440
+ }
441
+
442
+ async getData(dataProvider) {
443
+ if (dataProvider) {
444
+ return dataProvider('system');
445
+ }
446
+ return null;
447
+ }
448
+
449
+ update(data) {
450
+ if (!data || !this.box) return;
451
+
452
+ const platform = data.platform || '';
453
+ const release = data.release || '';
454
+ const arch = data.arch || '';
455
+ const container = data.isContainer ? ' [container]' : '';
456
+
457
+ this.line1.setContent(`${platform} ${release}`);
458
+ this.line2.setContent(`${arch}${container}`);
459
+ }
460
+
461
+ render(data) {
462
+ this.update(data);
463
+ }
464
+ }
465
+
466
+ /**
467
+ * Uptime Widget - Displays system and claw uptime
468
+ */
469
+ export class UptimeWidget extends BaseWidget {
470
+ constructor(options = {}) {
471
+ super(options);
472
+ this.name = 'Uptime';
473
+ this.description = 'System and OpenClaw uptime';
474
+ }
475
+
476
+ async create(screen, theme = {}) {
477
+ const C = theme.colors || {};
478
+
479
+ this.box = blessed.box({
480
+ parent: screen,
481
+ height: 5,
482
+ border: { type: 'line' },
483
+ label: ' UPTIME ',
484
+ style: { border: { fg: C.brightMagenta || 'bright-magenta' } },
485
+ });
486
+
487
+ this.sysText = blessed.text({
488
+ parent: this.box,
489
+ top: 0,
490
+ left: 'center',
491
+ content: 'Sys: --',
492
+ style: { fg: C.brightMagenta || 'bright-magenta', bold: true },
493
+ });
494
+
495
+ this.clawText = blessed.text({
496
+ parent: this.box,
497
+ top: 1,
498
+ left: 'center',
499
+ content: 'Claw: --',
500
+ style: { fg: C.brightMagenta || 'bright-magenta', bold: true },
501
+ });
502
+
503
+ return this;
504
+ }
505
+
506
+ async getData(dataProvider) {
507
+ if (dataProvider) {
508
+ const [sysUptime, clawUptime] = await Promise.all([
509
+ dataProvider('uptime'),
510
+ dataProvider('clawUptime'),
511
+ ]);
512
+ return { sysUptime, clawUptime };
513
+ }
514
+ return null;
515
+ }
516
+
517
+ formatUptime(seconds) {
518
+ const days = Math.floor(seconds / 86400);
519
+ const hours = Math.floor((seconds % 86400) / 3600);
520
+ const mins = Math.floor((seconds % 3600) / 60);
521
+
522
+ if (days > 0) return `${days}d ${hours}h ${mins}m`;
523
+ if (hours > 0) return `${hours}h ${mins}m`;
524
+ return `${mins}m`;
525
+ }
526
+
527
+ update(data) {
528
+ if (!data || !this.box) return;
529
+
530
+ const sysUptime = data.sysUptime || 0;
531
+ const clawUptime = data.clawUptime || 0;
532
+
533
+ this.sysText.setContent(`Sys: ${this.formatUptime(sysUptime)}`);
534
+ this.clawText.setContent(`Claw: ${this.formatUptime(clawUptime)}`);
535
+ }
536
+
537
+ render(data) {
538
+ this.update(data);
539
+ }
540
+ }
541
+
542
+ /**
543
+ * Data Health Widget - Shows data freshness
544
+ */
545
+ export class DataHealthWidget extends BaseWidget {
546
+ constructor(options = {}) {
547
+ super(options);
548
+ this.name = 'Data Health';
549
+ this.description = 'Data freshness indicators';
550
+ this.lastUpdate = null;
551
+ }
552
+
553
+ async create(screen, theme = {}) {
554
+ const C = theme.colors || {};
555
+
556
+ this.box = blessed.box({
557
+ parent: screen,
558
+ height: 5,
559
+ border: { type: 'line' },
560
+ label: ' DATA HEALTH ',
561
+ style: { border: { fg: C.green || 'green' } },
562
+ });
563
+
564
+ this.statusText = blessed.text({
565
+ parent: this.box,
566
+ top: 0,
567
+ left: 'center',
568
+ content: 'All Fresh',
569
+ style: { fg: C.brightGreen || 'bright-green', bold: true },
570
+ });
571
+
572
+ this.detailText = blessed.text({
573
+ parent: this.box,
574
+ top: 1,
575
+ left: 'center',
576
+ content: '',
577
+ style: { fg: C.gray || 'gray' },
578
+ });
579
+
580
+ return this;
581
+ }
582
+
583
+ async getData(dataProvider) {
584
+ // This widget tracks its own update time
585
+ return { timestamp: Date.now() };
586
+ }
587
+
588
+ update(data) {
589
+ if (!this.box) return;
590
+
591
+ const now = Date.now();
592
+ if (data?.timestamp) {
593
+ this.lastUpdate = data.timestamp;
594
+ }
595
+
596
+ if (!this.lastUpdate) {
597
+ this.statusText.setContent('Waiting...');
598
+ return;
599
+ }
600
+
601
+ const age = now - this.lastUpdate;
602
+ const ageSec = Math.floor(age / 1000);
603
+
604
+ let status = 'All Fresh';
605
+ let color = 'green';
606
+ let detail = `${ageSec}s ago`;
607
+
608
+ if (age > 30000) {
609
+ status = 'Stale';
610
+ color = 'red';
611
+ detail = `Last update: ${ageSec}s ago`;
612
+ } else if (age > 10000) {
613
+ status = 'Aging';
614
+ color = 'yellow';
615
+ detail = `${ageSec}s since last refresh`;
616
+ }
617
+
618
+ this.statusText.setContent(`{${color}-fg}${status}{/${color}-fg}`);
619
+ this.detailText.setContent(detail);
620
+ }
621
+
622
+ render(data) {
623
+ this.update(data);
624
+ }
625
+ }
626
+
627
+ /**
628
+ * Settings Widget - Interactive settings panel for user preferences
629
+ * Allows users to modify theme, refresh rate, and other settings
630
+ */
631
+ export class SettingsWidget extends BaseWidget {
632
+ constructor(options = {}) {
633
+ super(options);
634
+ this.name = 'Settings';
635
+ this.description = 'User preferences configuration';
636
+ this.settings = options.settings || {};
637
+ this.onSettingsChange = options.onSettingsChange || null;
638
+ this.onSave = options.onSave || null;
639
+ this.currentIndex = 0;
640
+ this.isEditing = false;
641
+ }
642
+
643
+ async create(screen, theme = {}) {
644
+ const C = theme.colors || {};
645
+
646
+ this.box = blessed.box({
647
+ parent: screen,
648
+ height: 12,
649
+ border: { type: 'line' },
650
+ label: ' SETTINGS ',
651
+ style: { border: { fg: C.cyan || 'cyan' } },
652
+ });
653
+
654
+ this.instructionsText = blessed.text({
655
+ parent: this.box,
656
+ top: 0,
657
+ left: 0,
658
+ right: 0,
659
+ content: ' {cyan-fg}j/k{/cyan-fg} navigate {cyan-fg}enter{/cyan-fg} edit {cyan-fg}s{/cyan-fg} save {cyan-fg}q{/cyan-fg} close',
660
+ style: { fg: C.gray || 'gray' },
661
+ });
662
+
663
+ this.settingsList = blessed.list({
664
+ parent: this.box,
665
+ top: 1,
666
+ left: 0,
667
+ right: 0,
668
+ bottom: 0,
669
+ keys: true,
670
+ interactive: false,
671
+ style: {
672
+ item: { fg: C.white || 'white' },
673
+ selected: { fg: C.black || 'black', bg: C.cyan || 'cyan', bold: true },
674
+ focus: { fg: C.black || 'black', bg: C.cyan || 'cyan' },
675
+ },
676
+ });
677
+
678
+ // Set up keyboard navigation
679
+ this.setupKeys();
680
+
681
+ return this;
682
+ }
683
+
684
+ setupKeys() {
685
+ this.settingsList.key(['j', 'down'], () => {
686
+ if (!this.isEditing) {
687
+ this.currentIndex = Math.min(this.currentIndex + 1, this.getSettingsCount() - 1);
688
+ this.updateSelection();
689
+ }
690
+ });
691
+
692
+ this.settingsList.key(['k', 'up'], () => {
693
+ if (!this.isEditing) {
694
+ this.currentIndex = Math.max(this.currentIndex - 1, 0);
695
+ this.updateSelection();
696
+ }
697
+ });
698
+
699
+ this.settingsList.key(['g', 'home'], () => {
700
+ if (!this.isEditing) {
701
+ this.currentIndex = 0;
702
+ this.updateSelection();
703
+ }
704
+ });
705
+
706
+ this.settingsList.key(['G', 'end'], () => {
707
+ if (!this.isEditing) {
708
+ this.currentIndex = this.getSettingsCount() - 1;
709
+ this.updateSelection();
710
+ }
711
+ });
712
+
713
+ this.settingsList.key(['enter', 'space'], () => {
714
+ this.editCurrentSetting();
715
+ });
716
+
717
+ this.settingsList.key('s', () => {
718
+ this.saveSettings();
719
+ });
720
+
721
+ this.settingsList.key(['q', 'escape'], () => {
722
+ if (this.onClose) this.onClose();
723
+ });
724
+
725
+ // Focus the list
726
+ this.settingsList.focus();
727
+ }
728
+
729
+ getSettingsCount() {
730
+ // Theme, Refresh Rate, Log Level, Show/Hide Widgets (9), Export Format
731
+ return 13;
732
+ }
733
+
734
+ getSettingsOptions() {
735
+ return [
736
+ { key: 'theme', label: 'Theme', options: ['auto', 'default', 'dark', 'high-contrast', 'ocean'] },
737
+ { key: 'refreshInterval', label: 'Refresh Rate', options: ['1000ms', '2000ms', '5000ms', '10000ms'] },
738
+ { key: 'logLevelFilter', label: 'Log Level', options: ['all', 'error', 'warn', 'info', 'debug'] },
739
+ { key: 'showWidget1', label: 'Show CPU Widget', options: ['ON', 'OFF'] },
740
+ { key: 'showWidget2', label: 'Show Memory Widget', options: ['ON', 'OFF'] },
741
+ { key: 'showWidget3', label: 'Show GPU Widget', options: ['ON', 'OFF'] },
742
+ { key: 'showWidget4', label: 'Show Network Widget', options: ['ON', 'OFF'] },
743
+ { key: 'showWidget5', label: 'Show Disk Widget', options: ['ON', 'OFF'] },
744
+ { key: 'showWidget6', label: 'Show System Widget', options: ['ON', 'OFF'] },
745
+ { key: 'showWidget7', label: 'Show Uptime Widget', options: ['ON', 'OFF'] },
746
+ { key: 'showWidget8', label: 'Show Data Health Widget', options: ['ON', 'OFF'] },
747
+ { key: 'showWidget9', label: 'Show Gateway Widget', options: ['ON', 'OFF'] },
748
+ { key: 'exportFormat', label: 'Export Format', options: ['json', 'csv'] },
749
+ ];
750
+ }
751
+
752
+ formatSettingRow(option, index) {
753
+ const currentValue = this.settings[option.key];
754
+ let displayValue;
755
+
756
+ if (option.key === 'refreshInterval') {
757
+ displayValue = `${currentValue}ms`;
758
+ } else if (option.key.startsWith('showWidget')) {
759
+ displayValue = currentValue !== false ? 'ON' : 'OFF';
760
+ } else {
761
+ displayValue = currentValue || 'auto';
762
+ }
763
+
764
+ const label = option.label.padEnd(25, ' ');
765
+ const isSelected = index === this.currentIndex;
766
+ const prefix = isSelected ? '> ' : ' ';
767
+
768
+ return `${prefix}${label} ${displayValue}`;
769
+ }
770
+
771
+ updateDisplay() {
772
+ if (!this.settingsList) return;
773
+
774
+ const options = this.getSettingsOptions();
775
+ const items = options.map((opt, idx) => this.formatSettingRow(opt, idx));
776
+ this.settingsList.setItems(items);
777
+ this.updateSelection();
778
+ }
779
+
780
+ updateSelection() {
781
+ if (this.settingsList) {
782
+ this.settingsList.select(this.currentIndex);
783
+ this.box.screen.render();
784
+ }
785
+ }
786
+
787
+ editCurrentSetting() {
788
+ const options = this.getSettingsOptions();
789
+ const option = options[this.currentIndex];
790
+ if (!option) return;
791
+
792
+ const currentValue = this.settings[option.key];
793
+
794
+ if (option.key === 'refreshInterval') {
795
+ // Cycle through refresh intervals
796
+ const intervals = [1000, 2000, 5000, 10000];
797
+ const currentIdx = intervals.indexOf(currentValue);
798
+ const nextIdx = (currentIdx + 1) % intervals.length;
799
+ this.settings[option.key] = intervals[nextIdx];
800
+ } else if (option.key.startsWith('showWidget')) {
801
+ // Toggle boolean
802
+ this.settings[option.key] = currentValue === false;
803
+ } else if (option.options) {
804
+ // Cycle through options
805
+ const currentIdx = option.options.indexOf(currentValue);
806
+ const nextIdx = (currentIdx + 1) % option.options.length;
807
+ this.settings[option.key] = option.options[nextIdx];
808
+ }
809
+
810
+ this.updateDisplay();
811
+
812
+ // Notify of immediate change
813
+ if (this.onSettingsChange) {
814
+ this.onSettingsChange({ [option.key]: this.settings[option.key] });
815
+ }
816
+ }
817
+
818
+ saveSettings() {
819
+ if (this.onSave) {
820
+ this.onSave(this.settings);
821
+ }
822
+ // Show brief feedback
823
+ this.instructionsText.setContent(' {green-fg}Settings saved!{/green-fg}');
824
+ setTimeout(() => {
825
+ this.instructionsText.setContent(' {cyan-fg}j/k{/cyan-fg} navigate {cyan-fg}enter{/cyan-fg} edit {cyan-fg}s{/cyan-fg} save {cyan-fg}q{/cyan-fg} close');
826
+ this.box.screen.render();
827
+ }, 1000);
828
+ }
829
+
830
+ async getData(dataProvider) {
831
+ // Settings widget shows current settings
832
+ return { settings: this.settings };
833
+ }
834
+
835
+ update(data) {
836
+ if (data?.settings) {
837
+ this.settings = data.settings;
838
+ }
839
+ this.updateDisplay();
840
+ }
841
+
842
+ render(data) {
843
+ this.update(data);
844
+ }
845
+
846
+ focus() {
847
+ if (this.settingsList) {
848
+ this.settingsList.focus();
849
+ }
850
+ }
851
+ }
852
+
853
+ /**
854
+ * Gateway Status Widget - Shows gateway connection status with offline indicator
855
+ * Displays connected/reachable status for all configured endpoints with retry UI
856
+ */
857
+ export class GatewayStatusWidget extends BaseWidget {
858
+ constructor(options = {}) {
859
+ super(options);
860
+ this.name = 'Gateway Status';
861
+ this.description = 'Gateway connection status and health';
862
+ this.onRetry = options.onRetry || null;
863
+ this.selectedEndpoint = 0;
864
+ }
865
+
866
+ async create(screen, theme = {}) {
867
+ const C = theme.colors || {};
868
+
869
+ this.box = blessed.box({
870
+ parent: screen,
871
+ height: 6,
872
+ border: { type: 'line' },
873
+ label: ' GATEWAY STATUS ',
874
+ style: { border: { fg: C.cyan || 'cyan' } },
875
+ });
876
+
877
+ this.statusText = blessed.text({
878
+ parent: this.box,
879
+ top: 0,
880
+ left: 'center',
881
+ content: 'Checking...',
882
+ style: { fg: C.brightCyan || 'bright-cyan', bold: true },
883
+ });
884
+
885
+ this.endpointsText = blessed.text({
886
+ parent: this.box,
887
+ top: 1,
888
+ left: 1,
889
+ right: 1,
890
+ content: '',
891
+ style: { fg: C.white || 'white' },
892
+ });
893
+
894
+ this.detailText = blessed.text({
895
+ parent: this.box,
896
+ top: 4,
897
+ left: 'center',
898
+ content: '',
899
+ style: { fg: C.gray || 'gray' },
900
+ });
901
+
902
+ // Setup keyboard shortcuts for this widget
903
+ this.setupKeys();
904
+
905
+ return this;
906
+ }
907
+
908
+ setupKeys() {
909
+ this.box.key(['r'], () => {
910
+ this.triggerRetry();
911
+ });
912
+
913
+ this.box.key(['j', 'down'], () => {
914
+ const endpointHealth = this.lastHealthData || [];
915
+ if (endpointHealth.length > 0) {
916
+ this.selectedEndpoint = Math.min(this.selectedEndpoint + 1, endpointHealth.length - 1);
917
+ this.updateDisplay();
918
+ }
919
+ });
920
+
921
+ this.box.key(['k', 'up'], () => {
922
+ this.selectedEndpoint = Math.max(this.selectedEndpoint - 1, 0);
923
+ this.updateDisplay();
924
+ });
925
+ }
926
+
927
+ async getData(dataProvider) {
928
+ if (dataProvider) {
929
+ return dataProvider('gatewayHealth');
930
+ }
931
+ return null;
932
+ }
933
+
934
+ triggerRetry() {
935
+ if (this.onRetry) {
936
+ const endpointHealth = this.lastHealthData || [];
937
+ const selected = endpointHealth[this.selectedEndpoint];
938
+ this.onRetry(selected?.name || null);
939
+
940
+ // Show retry feedback
941
+ this.detailText.setContent('{yellow-fg}⟳ Retrying...{/yellow-fg}');
942
+ this.box.screen.render();
943
+ }
944
+ }
945
+
946
+ update(data) {
947
+ if (!this.box) return;
948
+
949
+ this.lastHealthData = data?.endpoints || [];
950
+ this.updateDisplay();
951
+ }
952
+
953
+ updateDisplay() {
954
+ const endpointHealth = this.lastHealthData || [];
955
+
956
+ if (endpointHealth.length === 0) {
957
+ this.statusText.setContent('{red-fg}No Endpoints{/red-fg}');
958
+ this.endpointsText.setContent('No gateway endpoints configured');
959
+ this.detailText.setContent('');
960
+ return;
961
+ }
962
+
963
+ const total = endpointHealth.length;
964
+ const reachable = endpointHealth.filter(ep => ep.reachable).length;
965
+ const unreachable = total - reachable;
966
+
967
+ // Update overall status
968
+ if (unreachable === 0) {
969
+ this.statusText.setContent(`{green-fg}✓ All Connected (${reachable}/${total}){/green-fg}`);
970
+ } else if (reachable === 0) {
971
+ this.statusText.setContent(`{red-fg}✗ All Offline (${unreachable}/${total}){/red-fg}`);
972
+ } else {
973
+ this.statusText.setContent(`{yellow-fg}⚠ Partial (${reachable}/${total}){/yellow-fg}`);
974
+ }
975
+
976
+ // Build endpoint list display
977
+ const lines = [];
978
+ endpointHealth.forEach((ep, idx) => {
979
+ const isSelected = idx === this.selectedEndpoint;
980
+ const prefix = isSelected ? '> ' : ' ';
981
+ const statusIcon = ep.reachable ? '{green-fg}●{/green-fg}' : '{red-fg}●{/red-fg}';
982
+ const latency = ep.latency ? ` ${ep.latency}ms` : '';
983
+ const failInfo = ep.failCount > 0 ? ` (${ep.failCount} fails)` : '';
984
+ const line = `${prefix}${statusIcon} ${ep.name}${latency}${failInfo}`;
985
+ lines.push(line);
986
+ });
987
+
988
+ // Show up to 2 endpoints (fits in widget height)
989
+ this.endpointsText.setContent(lines.slice(0, 3).join('\n'));
990
+
991
+ // Show instructions
992
+ const selected = endpointHealth[this.selectedEndpoint];
993
+ if (selected && !selected.reachable) {
994
+ this.detailText.setContent('{yellow-fg}Press [r] to retry{/yellow-fg}');
995
+ } else {
996
+ this.detailText.setContent('{gray-fg}j/k navigate, [r] retry{/gray-fg}');
997
+ }
998
+ }
999
+
1000
+ render(data) {
1001
+ this.update(data);
1002
+ }
1003
+ }
1004
+
1005
+ /**
1006
+ * Widget registry - maps widget types to classes
1007
+ */
1008
+ export const WIDGET_REGISTRY = {
1009
+ cpu: CpuWidget,
1010
+ memory: MemoryWidget,
1011
+ gpu: GpuWidget,
1012
+ network: NetworkWidget,
1013
+ disk: DiskWidget,
1014
+ system: SystemWidget,
1015
+ uptime: UptimeWidget,
1016
+ dataHealth: DataHealthWidget,
1017
+ settings: SettingsWidget,
1018
+ gatewayStatus: GatewayStatusWidget,
1019
+ };
1020
+
1021
+ /**
1022
+ * Create a widget instance
1023
+ * @param {string} type - Widget type
1024
+ * @param {Object} options - Widget options
1025
+ */
1026
+ export function createWidget(type, options = {}) {
1027
+ const WidgetClass = WIDGET_REGISTRY[type];
1028
+ if (!WidgetClass) {
1029
+ throw new Error(`Unknown widget type: ${type}`);
1030
+ }
1031
+ // Pass the widget type as the ID for proper config matching
1032
+ return new WidgetClass({ ...options, id: options.id || type });
1033
+ }
1034
+
1035
+ /**
1036
+ * Get all available widget types
1037
+ */
1038
+ export function getWidgetTypes() {
1039
+ return Object.keys(WIDGET_REGISTRY);
1040
+ }
1041
+
1042
+ export default {
1043
+ CpuWidget,
1044
+ MemoryWidget,
1045
+ GpuWidget,
1046
+ NetworkWidget,
1047
+ DiskWidget,
1048
+ SystemWidget,
1049
+ UptimeWidget,
1050
+ DataHealthWidget,
1051
+ SettingsWidget,
1052
+ GatewayStatusWidget,
1053
+ createWidget,
1054
+ getWidgetTypes,
1055
+ WIDGET_REGISTRY,
1056
+ };