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