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,1941 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Plugin scaffolding CLI for Claw Dashboard
|
|
5
|
+
* Provides `clawdash create-plugin <name>` functionality with multiple templates
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { mkdirSync, writeFileSync, existsSync, readFileSync } from 'fs';
|
|
9
|
+
import { join } from 'path';
|
|
10
|
+
import { homedir } from 'os';
|
|
11
|
+
import readline from 'readline';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Create a readline interface for interactive prompts
|
|
15
|
+
* @returns {readline.Interface} Readline interface
|
|
16
|
+
*/
|
|
17
|
+
function createReadlineInterface() {
|
|
18
|
+
return readline.createInterface({
|
|
19
|
+
input: process.stdin,
|
|
20
|
+
output: process.stdout,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Prompt for user input
|
|
26
|
+
* @param {string} question - Question to ask
|
|
27
|
+
* @returns {Promise<string>} User input
|
|
28
|
+
*/
|
|
29
|
+
function prompt(question) {
|
|
30
|
+
return new Promise((resolve) => {
|
|
31
|
+
const rl = createReadlineInterface();
|
|
32
|
+
rl.question(question, (answer) => {
|
|
33
|
+
rl.close();
|
|
34
|
+
resolve(answer);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Prompt with default value
|
|
41
|
+
* @param {string} question - Question to ask
|
|
42
|
+
* @param {string} defaultValue - Default value
|
|
43
|
+
* @returns {Promise<string>} User input or default
|
|
44
|
+
*/
|
|
45
|
+
async function promptWithDefault(question, defaultValue) {
|
|
46
|
+
const answer = await prompt(`${question} [${defaultValue}]: `);
|
|
47
|
+
return answer.trim() || defaultValue;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Prompt with multiple choice
|
|
52
|
+
* @param {string} question - Question to ask
|
|
53
|
+
* @param {string[]} choices - Available choices
|
|
54
|
+
* @param {number} defaultIndex - Default choice index
|
|
55
|
+
* @returns {Promise<string>} Selected choice
|
|
56
|
+
*/
|
|
57
|
+
async function promptChoice(question, choices, defaultIndex = 0) {
|
|
58
|
+
const options = choices.map((c, i) => ` ${i + 1}. ${c}`).join('\n');
|
|
59
|
+
|
|
60
|
+
while (true) {
|
|
61
|
+
const answer = await prompt(`${question}\n${options}\nSelect (1-${choices.length}) [${defaultIndex + 1}]: `);
|
|
62
|
+
|
|
63
|
+
const input = answer.trim() || String(defaultIndex + 1);
|
|
64
|
+
const num = parseInt(input, 10);
|
|
65
|
+
if (!isNaN(num) && num >= 1 && num <= choices.length) {
|
|
66
|
+
return choices[num - 1];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
console.log('Invalid selection. Please enter a number between 1 and ' + choices.length);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Run interactive mode to prompt for all options
|
|
75
|
+
* @returns {Promise<object>} Collected options
|
|
76
|
+
*/
|
|
77
|
+
async function runInteractiveMode() {
|
|
78
|
+
console.log('');
|
|
79
|
+
console.log('╔══════════════════════════════════════════════════════════════╗');
|
|
80
|
+
console.log('║ Claw Dashboard - Create New Widget Plugin ║');
|
|
81
|
+
console.log('╚══════════════════════════════════════════════════════════════╝');
|
|
82
|
+
console.log('');
|
|
83
|
+
|
|
84
|
+
// Get plugin ID
|
|
85
|
+
const id = await prompt('Plugin ID (kebab-case, e.g., "my-widget"): ');
|
|
86
|
+
if (!id.trim()) {
|
|
87
|
+
console.log('Error: Plugin ID is required');
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Validate ID format
|
|
92
|
+
const idValidation = validatePluginId(id.trim());
|
|
93
|
+
if (!idValidation.valid) {
|
|
94
|
+
console.log('Error: ' + idValidation.error);
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Get display name
|
|
99
|
+
const defaultName = id.trim().split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
|
|
100
|
+
const name = await promptWithDefault('Display name', defaultName);
|
|
101
|
+
|
|
102
|
+
// Get template
|
|
103
|
+
const templates = listTemplates();
|
|
104
|
+
const templateNames = templates.map(t => t.name + ' (' + t.id + ')');
|
|
105
|
+
const selectedTemplate = await promptChoice('Select template:', templateNames, 0);
|
|
106
|
+
const template = templates[templateNames.indexOf(selectedTemplate)].id;
|
|
107
|
+
|
|
108
|
+
// Get author
|
|
109
|
+
const author = await promptWithDefault('Author name/email', '');
|
|
110
|
+
|
|
111
|
+
// Get category
|
|
112
|
+
const category = await promptChoice('Select category:', ['Custom', 'System', 'Monitoring', 'Example'], 0);
|
|
113
|
+
|
|
114
|
+
// Get description
|
|
115
|
+
const description = await promptWithDefault('Description', 'A custom widget for Claw Dashboard');
|
|
116
|
+
|
|
117
|
+
console.log('');
|
|
118
|
+
console.log('══════════════════════════════════════════════════════════════');
|
|
119
|
+
console.log('Summary:');
|
|
120
|
+
console.log(' ID: ' + id.trim());
|
|
121
|
+
console.log(' Name: ' + name);
|
|
122
|
+
console.log(' Template: ' + template);
|
|
123
|
+
console.log(' Category: ' + category.toLowerCase());
|
|
124
|
+
console.log(' Author: ' + (author || '(none)'));
|
|
125
|
+
console.log(' Description: ' + description);
|
|
126
|
+
console.log('══════════════════════════════════════════════════════════════');
|
|
127
|
+
console.log('');
|
|
128
|
+
|
|
129
|
+
const confirm = await promptChoice('Create plugin?', ['Yes', 'No'], 0);
|
|
130
|
+
|
|
131
|
+
if (confirm !== 'Yes') {
|
|
132
|
+
console.log('Cancelled.');
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
id: id.trim(),
|
|
138
|
+
name,
|
|
139
|
+
template,
|
|
140
|
+
author,
|
|
141
|
+
category: category.toLowerCase(),
|
|
142
|
+
description,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Template definitions for different widget types
|
|
148
|
+
*/
|
|
149
|
+
const TEMPLATES = {
|
|
150
|
+
basic: {
|
|
151
|
+
name: 'Basic Widget',
|
|
152
|
+
description: 'Simple widget with minimal setup - displays static or simple data',
|
|
153
|
+
manifest: (id, name, author, options = {}) => ({
|
|
154
|
+
id,
|
|
155
|
+
name: name || id.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' '),
|
|
156
|
+
description: options.description || 'A custom widget plugin for Claw Dashboard',
|
|
157
|
+
version: '1.0.0',
|
|
158
|
+
author: author || '',
|
|
159
|
+
category: options.category || 'custom',
|
|
160
|
+
type: 'widget',
|
|
161
|
+
lazyLoad: true,
|
|
162
|
+
priority: 100,
|
|
163
|
+
config: {
|
|
164
|
+
message: 'Hello, World!',
|
|
165
|
+
showTimestamp: true,
|
|
166
|
+
},
|
|
167
|
+
__version: 1,
|
|
168
|
+
}),
|
|
169
|
+
|
|
170
|
+
widgetCode: (id, className) => `/**
|
|
171
|
+
* ${className} Widget Plugin
|
|
172
|
+
* Generated by clawdash create-plugin
|
|
173
|
+
*/
|
|
174
|
+
|
|
175
|
+
import { BaseWidget } from 'claw-dashboard/widgets';
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* ${className} - A custom widget for Claw Dashboard
|
|
179
|
+
*/
|
|
180
|
+
export default class ${className} extends BaseWidget {
|
|
181
|
+
constructor(options = {}) {
|
|
182
|
+
super(options);
|
|
183
|
+
this.name = options.name || '${className}';
|
|
184
|
+
this.description = options.description || 'A custom widget';
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Initialize the widget
|
|
189
|
+
* Called once when the widget is first loaded
|
|
190
|
+
*/
|
|
191
|
+
async init() {
|
|
192
|
+
this.log('info', '${className} widget initialized');
|
|
193
|
+
return true;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Create the widget UI
|
|
198
|
+
* @param {Object} screen - Blessed screen object
|
|
199
|
+
* @param {Object} theme - Theme colors
|
|
200
|
+
*/
|
|
201
|
+
async create(screen, theme = {}) {
|
|
202
|
+
const C = theme.colors || {};
|
|
203
|
+
const blessed = await import('blessed');
|
|
204
|
+
|
|
205
|
+
// Create main container
|
|
206
|
+
this.box = blessed.default.box({
|
|
207
|
+
parent: screen,
|
|
208
|
+
width: '50%',
|
|
209
|
+
height: 10,
|
|
210
|
+
border: { type: 'line' },
|
|
211
|
+
label: ' ${className.toUpperCase()} ',
|
|
212
|
+
style: {
|
|
213
|
+
border: { fg: C.cyan || 'cyan' },
|
|
214
|
+
},
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
// Create content text
|
|
218
|
+
this.contentText = blessed.default.text({
|
|
219
|
+
parent: this.box,
|
|
220
|
+
top: 2,
|
|
221
|
+
left: 1,
|
|
222
|
+
content: 'Loading...',
|
|
223
|
+
style: { fg: C.white || 'white' },
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
this.loaded = true;
|
|
227
|
+
this.log('debug', '${className} widget UI created');
|
|
228
|
+
|
|
229
|
+
return this;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Get data for the widget
|
|
234
|
+
* Fetch and return data for rendering
|
|
235
|
+
*/
|
|
236
|
+
async getData() {
|
|
237
|
+
const message = this.config.message || 'Hello, World!';
|
|
238
|
+
const showTimestamp = this.config.showTimestamp !== false;
|
|
239
|
+
|
|
240
|
+
return {
|
|
241
|
+
message,
|
|
242
|
+
timestamp: showTimestamp ? new Date().toISOString() : null,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Render the widget with data
|
|
248
|
+
* @param {Object} data - Data from getData()
|
|
249
|
+
*/
|
|
250
|
+
render(data) {
|
|
251
|
+
if (!this.box || !data) return;
|
|
252
|
+
|
|
253
|
+
let content = data.message;
|
|
254
|
+
if (data.timestamp) {
|
|
255
|
+
content += '\\n[' + data.timestamp + ']';
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
this.contentText.setContent(content);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Destroy the widget
|
|
263
|
+
* Clean up resources
|
|
264
|
+
*/
|
|
265
|
+
async destroy() {
|
|
266
|
+
if (this.box) {
|
|
267
|
+
this.box.destroy();
|
|
268
|
+
this.box = null;
|
|
269
|
+
}
|
|
270
|
+
this.loaded = false;
|
|
271
|
+
this.log('info', '${className} widget destroyed');
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Export named export for flexibility
|
|
276
|
+
export { ${className} };
|
|
277
|
+
`,
|
|
278
|
+
},
|
|
279
|
+
|
|
280
|
+
api: {
|
|
281
|
+
name: 'API Widget',
|
|
282
|
+
description: 'Widget with API fetching, error handling, and retry logic',
|
|
283
|
+
manifest: (id, name, author, options = {}) => ({
|
|
284
|
+
id,
|
|
285
|
+
name: name || id.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' '),
|
|
286
|
+
description: options.description || 'A widget that fetches data from an external API',
|
|
287
|
+
version: '1.0.0',
|
|
288
|
+
author: author || '',
|
|
289
|
+
category: options.category || 'monitoring',
|
|
290
|
+
type: 'widget',
|
|
291
|
+
lazyLoad: true,
|
|
292
|
+
priority: 100,
|
|
293
|
+
config: {
|
|
294
|
+
apiUrl: '${API_URL:-https://api.github.com/zen}',
|
|
295
|
+
apiKey: '${API_KEY:-}',
|
|
296
|
+
refreshInterval: 60000,
|
|
297
|
+
timeout: 5000,
|
|
298
|
+
retries: 3,
|
|
299
|
+
},
|
|
300
|
+
__version: 1,
|
|
301
|
+
}),
|
|
302
|
+
|
|
303
|
+
widgetCode: (id, className) => `/**
|
|
304
|
+
* ${className} Widget Plugin
|
|
305
|
+
* API-powered widget with error handling and retry logic
|
|
306
|
+
*/
|
|
307
|
+
|
|
308
|
+
import { BaseWidget } from 'claw-dashboard/widgets';
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* ${className} - API-powered widget for Claw Dashboard
|
|
312
|
+
*/
|
|
313
|
+
export default class ${className} extends BaseWidget {
|
|
314
|
+
constructor(options = {}) {
|
|
315
|
+
super(options);
|
|
316
|
+
this.name = options.name || '${className}';
|
|
317
|
+
this.description = options.description || 'API-powered widget';
|
|
318
|
+
|
|
319
|
+
// Internal state
|
|
320
|
+
this.loading = false;
|
|
321
|
+
this.error = null;
|
|
322
|
+
this.lastFetch = null;
|
|
323
|
+
this.refreshTimer = null;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Initialize the widget
|
|
328
|
+
*/
|
|
329
|
+
async init() {
|
|
330
|
+
this.log('info', '${className} widget initialized');
|
|
331
|
+
return true;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Create the widget UI
|
|
336
|
+
* @param {Object} screen - Blessed screen object
|
|
337
|
+
* @param {Object} theme - Theme colors
|
|
338
|
+
*/
|
|
339
|
+
async create(screen, theme = {}) {
|
|
340
|
+
const C = theme.colors || {};
|
|
341
|
+
const blessed = await import('blessed');
|
|
342
|
+
|
|
343
|
+
this.screen = screen;
|
|
344
|
+
this.theme = theme;
|
|
345
|
+
|
|
346
|
+
// Main container
|
|
347
|
+
this.box = blessed.default.box({
|
|
348
|
+
parent: screen,
|
|
349
|
+
width: '50%',
|
|
350
|
+
height: 7,
|
|
351
|
+
border: { type: 'line' },
|
|
352
|
+
label: ' ${className.toUpperCase()} ',
|
|
353
|
+
style: { border: { fg: C.cyan || 'cyan' } },
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
// Status line
|
|
357
|
+
this.statusText = blessed.default.text({
|
|
358
|
+
parent: this.box,
|
|
359
|
+
top: 0,
|
|
360
|
+
left: 1,
|
|
361
|
+
content: 'Initializing...',
|
|
362
|
+
style: { fg: C.gray || 'gray' },
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
// Data content
|
|
366
|
+
this.contentText = blessed.default.text({
|
|
367
|
+
parent: this.box,
|
|
368
|
+
top: 1,
|
|
369
|
+
left: 1,
|
|
370
|
+
content: '',
|
|
371
|
+
style: { fg: C.white || 'white' },
|
|
372
|
+
wrap: true,
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
// Last updated
|
|
376
|
+
this.updatedText = blessed.default.text({
|
|
377
|
+
parent: this.box,
|
|
378
|
+
top: 3,
|
|
379
|
+
left: 1,
|
|
380
|
+
content: 'Never updated',
|
|
381
|
+
style: { fg: C.gray || 'gray' },
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
// Stats
|
|
385
|
+
this.statsText = blessed.default.text({
|
|
386
|
+
parent: this.box,
|
|
387
|
+
top: 4,
|
|
388
|
+
left: 1,
|
|
389
|
+
content: 'Requests: 0 | Errors: 0',
|
|
390
|
+
style: { fg: C.gray || 'gray' },
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
this.loaded = true;
|
|
394
|
+
this.log('debug', '${className} widget UI created');
|
|
395
|
+
|
|
396
|
+
// Start auto-refresh
|
|
397
|
+
const refreshInterval = this.config.refreshInterval || 60000;
|
|
398
|
+
if (refreshInterval > 0) {
|
|
399
|
+
this.startAutoRefresh(refreshInterval);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
return this;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Fetch data from the configured API
|
|
407
|
+
*/
|
|
408
|
+
async getData() {
|
|
409
|
+
const apiUrl = this.config.apiUrl || 'https://api.github.com/zen';
|
|
410
|
+
const timeout = this.config.timeout || 5000;
|
|
411
|
+
const maxRetries = this.config.retries || 3;
|
|
412
|
+
|
|
413
|
+
let lastError = null;
|
|
414
|
+
|
|
415
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
416
|
+
try {
|
|
417
|
+
this.loading = true;
|
|
418
|
+
this.error = null;
|
|
419
|
+
this.updateStatus('loading', 'Fetching (attempt ' + attempt + '/' + maxRetries + ')...');
|
|
420
|
+
|
|
421
|
+
const controller = new AbortController();
|
|
422
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
423
|
+
|
|
424
|
+
const response = await fetch(apiUrl, {
|
|
425
|
+
signal: controller.signal,
|
|
426
|
+
headers: {
|
|
427
|
+
'User-Agent': 'Claw-Dashboard-Widget/1.0',
|
|
428
|
+
...(this.config.apiKey && { 'Authorization': 'Bearer ' + this.config.apiKey }),
|
|
429
|
+
},
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
clearTimeout(timeoutId);
|
|
433
|
+
|
|
434
|
+
if (!response.ok) {
|
|
435
|
+
throw new Error('HTTP ' + response.status + ': ' + response.statusText);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// Handle different response types
|
|
439
|
+
const contentType = response.headers.get('content-type') || '';
|
|
440
|
+
let data;
|
|
441
|
+
|
|
442
|
+
if (contentType.includes('application/json')) {
|
|
443
|
+
data = await response.json();
|
|
444
|
+
} else {
|
|
445
|
+
data = await response.text();
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
this.loading = false;
|
|
449
|
+
this.lastFetch = new Date();
|
|
450
|
+
|
|
451
|
+
return {
|
|
452
|
+
success: true,
|
|
453
|
+
data,
|
|
454
|
+
timestamp: this.lastFetch.toISOString(),
|
|
455
|
+
apiUrl,
|
|
456
|
+
};
|
|
457
|
+
} catch (err) {
|
|
458
|
+
lastError = err;
|
|
459
|
+
this.log('warn', 'API fetch attempt ' + attempt + ' failed: ' + err.message);
|
|
460
|
+
|
|
461
|
+
if (err.name === 'AbortError') {
|
|
462
|
+
break;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// Exponential backoff
|
|
466
|
+
if (attempt < maxRetries) {
|
|
467
|
+
await new Promise(resolve => setTimeout(resolve, 1000 * attempt));
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// All retries failed
|
|
473
|
+
this.loading = false;
|
|
474
|
+
this.error = lastError;
|
|
475
|
+
|
|
476
|
+
return {
|
|
477
|
+
success: false,
|
|
478
|
+
error: lastError?.message || 'Unknown error',
|
|
479
|
+
timestamp: new Date().toISOString(),
|
|
480
|
+
apiUrl,
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Render the widget with fetched data
|
|
486
|
+
*/
|
|
487
|
+
render(result) {
|
|
488
|
+
if (!this.box) return;
|
|
489
|
+
|
|
490
|
+
if (result.success) {
|
|
491
|
+
this.updateStatus('success', 'Connected');
|
|
492
|
+
|
|
493
|
+
// Format the data for display
|
|
494
|
+
let content;
|
|
495
|
+
if (typeof result.data === 'object') {
|
|
496
|
+
content = JSON.stringify(result.data, null, 0).slice(0, 100);
|
|
497
|
+
} else {
|
|
498
|
+
content = String(result.data).slice(0, 100);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
this.contentText.setContent(content);
|
|
502
|
+
this.contentText.style.fg = this.theme?.colors?.white || 'white';
|
|
503
|
+
} else {
|
|
504
|
+
this.updateStatus('error', 'Error: ' + result.error);
|
|
505
|
+
this.contentText.setContent('Unable to fetch data');
|
|
506
|
+
this.contentText.style.fg = this.theme?.colors?.red || 'red';
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
this.updatedText.setContent('Last: ' + (result.timestamp || 'Never'));
|
|
510
|
+
|
|
511
|
+
const stats = this.getStats();
|
|
512
|
+
this.statsText.setContent('Requests: ' + stats.requests + ' | Errors: ' + stats.errors);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* Update the status indicator
|
|
517
|
+
*/
|
|
518
|
+
updateStatus(status, message) {
|
|
519
|
+
const colors = {
|
|
520
|
+
loading: this.theme?.colors?.yellow || 'yellow',
|
|
521
|
+
success: this.theme?.colors?.green || 'green',
|
|
522
|
+
error: this.theme?.colors?.red || 'red',
|
|
523
|
+
};
|
|
524
|
+
|
|
525
|
+
this.statusText.setContent(message);
|
|
526
|
+
this.statusText.style.fg = colors[status] || 'white';
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* Track request statistics
|
|
531
|
+
*/
|
|
532
|
+
getStats() {
|
|
533
|
+
if (!this._stats) {
|
|
534
|
+
this._stats = { requests: 0, errors: 0 };
|
|
535
|
+
}
|
|
536
|
+
return this._stats;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* Start auto-refresh timer
|
|
541
|
+
*/
|
|
542
|
+
startAutoRefresh(intervalMs) {
|
|
543
|
+
this.stopAutoRefresh();
|
|
544
|
+
this.refreshTimer = setInterval(() => {
|
|
545
|
+
if (!this.loading) {
|
|
546
|
+
this.getData()
|
|
547
|
+
.then(data => this.render(data))
|
|
548
|
+
.catch(err => this.log('error', 'Auto-refresh failed: ' + err.message));
|
|
549
|
+
}
|
|
550
|
+
}, intervalMs);
|
|
551
|
+
|
|
552
|
+
this.log('debug', 'Auto-refresh started (' + intervalMs + 'ms interval)');
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* Stop auto-refresh timer
|
|
557
|
+
*/
|
|
558
|
+
stopAutoRefresh() {
|
|
559
|
+
if (this.refreshTimer) {
|
|
560
|
+
clearInterval(this.refreshTimer);
|
|
561
|
+
this.refreshTimer = null;
|
|
562
|
+
this.log('debug', 'Auto-refresh stopped');
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* Destroy the widget
|
|
568
|
+
*/
|
|
569
|
+
async destroy() {
|
|
570
|
+
this.stopAutoRefresh();
|
|
571
|
+
|
|
572
|
+
if (this.box) {
|
|
573
|
+
this.box.destroy();
|
|
574
|
+
this.box = null;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
this.loaded = false;
|
|
578
|
+
this.log('info', '${className} widget destroyed');
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
export { ${className} };
|
|
583
|
+
`,
|
|
584
|
+
},
|
|
585
|
+
|
|
586
|
+
chart: {
|
|
587
|
+
name: 'Chart Widget',
|
|
588
|
+
description: 'Widget with real-time line chart visualization using blessed-contrib',
|
|
589
|
+
manifest: (id, name, author, options = {}) => ({
|
|
590
|
+
id,
|
|
591
|
+
name: name || id.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' '),
|
|
592
|
+
description: options.description || 'A widget that displays real-time data with charts',
|
|
593
|
+
version: '1.0.0',
|
|
594
|
+
author: author || '',
|
|
595
|
+
category: options.category || 'monitoring',
|
|
596
|
+
type: 'widget',
|
|
597
|
+
lazyLoad: true,
|
|
598
|
+
priority: 100,
|
|
599
|
+
config: {
|
|
600
|
+
metricType: 'cpu',
|
|
601
|
+
maxDataPoints: 30,
|
|
602
|
+
refreshInterval: 2000,
|
|
603
|
+
showLegend: true,
|
|
604
|
+
},
|
|
605
|
+
__version: 1,
|
|
606
|
+
}),
|
|
607
|
+
|
|
608
|
+
widgetCode: (id, className) => `/**
|
|
609
|
+
* ${className} Widget Plugin
|
|
610
|
+
* Chart widget with real-time data visualization using blessed-contrib
|
|
611
|
+
*/
|
|
612
|
+
|
|
613
|
+
import { BaseWidget } from 'claw-dashboard/widgets';
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* ${className} - Chart widget for Claw Dashboard
|
|
617
|
+
*/
|
|
618
|
+
export default class ${className} extends BaseWidget {
|
|
619
|
+
constructor(options = {}) {
|
|
620
|
+
super(options);
|
|
621
|
+
this.name = options.name || '${className}';
|
|
622
|
+
this.description = options.description || 'Chart widget with real-time data';
|
|
623
|
+
|
|
624
|
+
// Chart configuration
|
|
625
|
+
this.metricType = this.config.metricType || 'cpu';
|
|
626
|
+
this.maxDataPoints = this.config.maxDataPoints || 30;
|
|
627
|
+
this.refreshInterval = this.config.refreshInterval || 2000;
|
|
628
|
+
this.showLegend = this.config.showLegend !== false;
|
|
629
|
+
|
|
630
|
+
// Data storage for time series
|
|
631
|
+
this.dataHistory = { labels: [], values: [] };
|
|
632
|
+
|
|
633
|
+
// Widget state
|
|
634
|
+
this.chart = null;
|
|
635
|
+
this.refreshTimer = null;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
/**
|
|
639
|
+
* Initialize the widget
|
|
640
|
+
*/
|
|
641
|
+
async init() {
|
|
642
|
+
this.log('info', '${className} widget initialized');
|
|
643
|
+
return true;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
/**
|
|
647
|
+
* Create the widget UI with blessed-contrib line chart
|
|
648
|
+
* @param {Object} screen - Blessed screen object
|
|
649
|
+
* @param {Object} theme - Theme colors
|
|
650
|
+
*/
|
|
651
|
+
async create(screen, theme = {}) {
|
|
652
|
+
const C = theme.colors || {};
|
|
653
|
+
const blessed = await import('blessed');
|
|
654
|
+
const contrib = await import('blessed-contrib');
|
|
655
|
+
|
|
656
|
+
this.screen = screen;
|
|
657
|
+
this.theme = theme;
|
|
658
|
+
|
|
659
|
+
// Create main container box
|
|
660
|
+
this.box = blessed.default.box({
|
|
661
|
+
parent: screen,
|
|
662
|
+
width: '70%',
|
|
663
|
+
height: 17,
|
|
664
|
+
border: { type: 'line' },
|
|
665
|
+
label: ' ${className.toUpperCase()} ',
|
|
666
|
+
style: {
|
|
667
|
+
border: { fg: C.cyan || 'cyan' },
|
|
668
|
+
},
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
// Create the line chart using blessed-contrib
|
|
672
|
+
this.chart = contrib.default.line({
|
|
673
|
+
parent: this.box,
|
|
674
|
+
top: 1,
|
|
675
|
+
left: 1,
|
|
676
|
+
width: '95%',
|
|
677
|
+
height: 14,
|
|
678
|
+
style: {
|
|
679
|
+
line: C.green || 'green',
|
|
680
|
+
text: C.white || 'white',
|
|
681
|
+
baseline: C.gray || 'gray',
|
|
682
|
+
},
|
|
683
|
+
xLabelPadding: 3,
|
|
684
|
+
xPadding: 5,
|
|
685
|
+
numYLabels: 5,
|
|
686
|
+
showNthLabel: Math.ceil(this.maxDataPoints / 6),
|
|
687
|
+
showLegend: this.showLegend,
|
|
688
|
+
legend: { width: 12 },
|
|
689
|
+
minY: 0,
|
|
690
|
+
maxY: 100,
|
|
691
|
+
wholeNumbersOnly: true,
|
|
692
|
+
});
|
|
693
|
+
|
|
694
|
+
// Add info text
|
|
695
|
+
this.infoText = blessed.default.text({
|
|
696
|
+
parent: this.box,
|
|
697
|
+
bottom: 0,
|
|
698
|
+
left: 1,
|
|
699
|
+
content: 'Initializing...',
|
|
700
|
+
style: { fg: C.gray || 'gray' },
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
this.loaded = true;
|
|
704
|
+
this.log('debug', '${className} widget UI created');
|
|
705
|
+
|
|
706
|
+
// Start auto-refresh
|
|
707
|
+
this.startAutoRefresh();
|
|
708
|
+
|
|
709
|
+
return this;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
/**
|
|
713
|
+
* Generate sample data - customize this for your data source
|
|
714
|
+
*/
|
|
715
|
+
async getData() {
|
|
716
|
+
const now = new Date();
|
|
717
|
+
const timeLabel = now.getHours().toString().padStart(2, '0') + ':' +
|
|
718
|
+
now.getMinutes().toString().padStart(2, '0') + ':' +
|
|
719
|
+
now.getSeconds().toString().padStart(2, '0');
|
|
720
|
+
|
|
721
|
+
// Generate sample data - replace with actual data fetching
|
|
722
|
+
const baseValue = 30;
|
|
723
|
+
const variance = Math.random() * 40;
|
|
724
|
+
const value = Math.min(100, Math.max(0, Math.floor(baseValue + variance)));
|
|
725
|
+
|
|
726
|
+
// Store in history
|
|
727
|
+
this.dataHistory.labels.push(timeLabel);
|
|
728
|
+
this.dataHistory.values.push(value);
|
|
729
|
+
|
|
730
|
+
// Trim to max data points
|
|
731
|
+
if (this.dataHistory.labels.length > this.maxDataPoints) {
|
|
732
|
+
this.dataHistory.labels.shift();
|
|
733
|
+
this.dataHistory.values.shift();
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
return {
|
|
737
|
+
currentValue: value,
|
|
738
|
+
timestamp: now.toISOString(),
|
|
739
|
+
labels: [...this.dataHistory.labels],
|
|
740
|
+
values: [...this.dataHistory.values],
|
|
741
|
+
dataPoints: this.dataHistory.values.length,
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
/**
|
|
746
|
+
* Render the chart with data
|
|
747
|
+
*/
|
|
748
|
+
render(data) {
|
|
749
|
+
if (!this.chart || !data) return;
|
|
750
|
+
|
|
751
|
+
// Prepare chart data
|
|
752
|
+
const chartData = {
|
|
753
|
+
title: '${className}',
|
|
754
|
+
x: data.labels,
|
|
755
|
+
y: data.values,
|
|
756
|
+
style: { line: 'green' },
|
|
757
|
+
};
|
|
758
|
+
|
|
759
|
+
// Update the chart
|
|
760
|
+
this.chart.setData([chartData]);
|
|
761
|
+
|
|
762
|
+
// Update info text
|
|
763
|
+
const avg = data.values.length > 0
|
|
764
|
+
? Math.floor(data.values.reduce((a, b) => a + b, 0) / data.values.length)
|
|
765
|
+
: 0;
|
|
766
|
+
const current = data.currentValue;
|
|
767
|
+
const max = data.values.length > 0 ? Math.max(...data.values) : 0;
|
|
768
|
+
|
|
769
|
+
this.infoText.setContent(
|
|
770
|
+
'Current: ' + current + ' | Avg: ' + avg + ' | Peak: ' + max +
|
|
771
|
+
' | Points: ' + data.dataPoints + '/' + this.maxDataPoints
|
|
772
|
+
);
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
/**
|
|
776
|
+
* Start auto-refresh timer
|
|
777
|
+
*/
|
|
778
|
+
startAutoRefresh() {
|
|
779
|
+
this.stopAutoRefresh();
|
|
780
|
+
|
|
781
|
+
if (this.refreshInterval > 0) {
|
|
782
|
+
this.refreshTimer = setInterval(async () => {
|
|
783
|
+
try {
|
|
784
|
+
const data = await this.getData();
|
|
785
|
+
this.render(data);
|
|
786
|
+
} catch (err) {
|
|
787
|
+
this.log('error', 'Auto-refresh failed: ' + err.message);
|
|
788
|
+
}
|
|
789
|
+
}, this.refreshInterval);
|
|
790
|
+
|
|
791
|
+
this.log('debug', 'Auto-refresh started (' + this.refreshInterval + 'ms)');
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
/**
|
|
796
|
+
* Stop auto-refresh timer
|
|
797
|
+
*/
|
|
798
|
+
stopAutoRefresh() {
|
|
799
|
+
if (this.refreshTimer) {
|
|
800
|
+
clearInterval(this.refreshTimer);
|
|
801
|
+
this.refreshTimer = null;
|
|
802
|
+
this.log('debug', 'Auto-refresh stopped');
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
/**
|
|
807
|
+
* Destroy the widget
|
|
808
|
+
*/
|
|
809
|
+
async destroy() {
|
|
810
|
+
this.stopAutoRefresh();
|
|
811
|
+
|
|
812
|
+
// Clear data history
|
|
813
|
+
this.dataHistory = { labels: [], values: [] };
|
|
814
|
+
|
|
815
|
+
if (this.chart) {
|
|
816
|
+
this.chart = null;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
if (this.box) {
|
|
820
|
+
this.box.destroy();
|
|
821
|
+
this.box = null;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
this.loaded = false;
|
|
825
|
+
this.log('info', '${className} widget destroyed');
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
export { ${className} };
|
|
830
|
+
`,
|
|
831
|
+
},
|
|
832
|
+
|
|
833
|
+
table: {
|
|
834
|
+
name: 'Table Widget',
|
|
835
|
+
description: 'Widget that displays data in a sortable table format',
|
|
836
|
+
manifest: (id, name, author, options = {}) => ({
|
|
837
|
+
id,
|
|
838
|
+
name: name || id.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' '),
|
|
839
|
+
description: options.description || 'A widget that displays tabular data',
|
|
840
|
+
version: '1.0.0',
|
|
841
|
+
author: author || '',
|
|
842
|
+
category: options.category || 'monitoring',
|
|
843
|
+
type: 'widget',
|
|
844
|
+
lazyLoad: true,
|
|
845
|
+
priority: 100,
|
|
846
|
+
config: {
|
|
847
|
+
columns: ['Name', 'Status', 'Value'],
|
|
848
|
+
refreshInterval: 5000,
|
|
849
|
+
maxRows: 10,
|
|
850
|
+
},
|
|
851
|
+
__version: 1,
|
|
852
|
+
}),
|
|
853
|
+
|
|
854
|
+
widgetCode: (id, className) => `/**
|
|
855
|
+
* ${className} Widget Plugin
|
|
856
|
+
* Table widget for displaying tabular data
|
|
857
|
+
*/
|
|
858
|
+
|
|
859
|
+
import { BaseWidget } from 'claw-dashboard/widgets';
|
|
860
|
+
|
|
861
|
+
/**
|
|
862
|
+
* ${className} - Table widget for Claw Dashboard
|
|
863
|
+
*/
|
|
864
|
+
export default class ${className} extends BaseWidget {
|
|
865
|
+
constructor(options = {}) {
|
|
866
|
+
super(options);
|
|
867
|
+
this.name = options.name || '${className}';
|
|
868
|
+
this.description = options.description || 'Table widget';
|
|
869
|
+
|
|
870
|
+
// Table configuration
|
|
871
|
+
this.columns = this.config.columns || ['Name', 'Status', 'Value'];
|
|
872
|
+
this.refreshInterval = this.config.refreshInterval || 5000;
|
|
873
|
+
this.maxRows = this.config.maxRows || 10;
|
|
874
|
+
|
|
875
|
+
// Widget state
|
|
876
|
+
this.table = null;
|
|
877
|
+
this.refreshTimer = null;
|
|
878
|
+
this.sortColumn = 0;
|
|
879
|
+
this.sortAsc = true;
|
|
880
|
+
this.data = [];
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
/**
|
|
884
|
+
* Initialize the widget
|
|
885
|
+
*/
|
|
886
|
+
async init() {
|
|
887
|
+
this.log('info', '${className} widget initialized');
|
|
888
|
+
return true;
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
/**
|
|
892
|
+
* Create the widget UI
|
|
893
|
+
* @param {Object} screen - Blessed screen object
|
|
894
|
+
* @param {Object} theme - Theme colors
|
|
895
|
+
*/
|
|
896
|
+
async create(screen, theme = {}) {
|
|
897
|
+
const C = theme.colors || {};
|
|
898
|
+
const blessed = await import('blessed');
|
|
899
|
+
|
|
900
|
+
this.screen = screen;
|
|
901
|
+
this.theme = theme;
|
|
902
|
+
|
|
903
|
+
// Main container
|
|
904
|
+
this.box = blessed.default.box({
|
|
905
|
+
parent: screen,
|
|
906
|
+
width: '60%',
|
|
907
|
+
height: 15,
|
|
908
|
+
border: { type: 'line' },
|
|
909
|
+
label: ' ${className.toUpperCase()} ',
|
|
910
|
+
style: {
|
|
911
|
+
border: { fg: C.cyan || 'cyan' },
|
|
912
|
+
},
|
|
913
|
+
});
|
|
914
|
+
|
|
915
|
+
// Create table
|
|
916
|
+
this.table = blessed.default.table({
|
|
917
|
+
parent: this.box,
|
|
918
|
+
top: 1,
|
|
919
|
+
left: 1,
|
|
920
|
+
width: '98%',
|
|
921
|
+
height: '90%',
|
|
922
|
+
border: { type: 'none' },
|
|
923
|
+
style: {
|
|
924
|
+
header: { fg: C.cyan || 'cyan', bold: true },
|
|
925
|
+
cell: { fg: C.white || 'white' },
|
|
926
|
+
},
|
|
927
|
+
columns: this.columns,
|
|
928
|
+
rows: [],
|
|
929
|
+
});
|
|
930
|
+
|
|
931
|
+
this.loaded = true;
|
|
932
|
+
this.log('debug', '${className} widget UI created');
|
|
933
|
+
|
|
934
|
+
// Start auto-refresh
|
|
935
|
+
this.startAutoRefresh();
|
|
936
|
+
|
|
937
|
+
return this;
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
/**
|
|
941
|
+
* Generate sample data - customize this for your data source
|
|
942
|
+
*/
|
|
943
|
+
async getData() {
|
|
944
|
+
// Sample data - replace with actual data fetching
|
|
945
|
+
const sampleData = [
|
|
946
|
+
['Server 1', 'Online', Math.floor(Math.random() * 100) + '%'],
|
|
947
|
+
['Server 2', 'Online', Math.floor(Math.random() * 100) + '%'],
|
|
948
|
+
['Server 3', 'Warning', Math.floor(Math.random() * 100) + '%'],
|
|
949
|
+
['Server 4', 'Online', Math.floor(Math.random() * 100) + '%'],
|
|
950
|
+
['Server 5', 'Offline', '0%'],
|
|
951
|
+
];
|
|
952
|
+
|
|
953
|
+
// Sort data
|
|
954
|
+
const sorted = [...sampleData].sort((a, b) => {
|
|
955
|
+
const aVal = a[this.sortColumn];
|
|
956
|
+
const bVal = b[this.sortColumn];
|
|
957
|
+
const cmp = aVal.localeCompare(bVal);
|
|
958
|
+
return this.sortAsc ? cmp : -cmp;
|
|
959
|
+
});
|
|
960
|
+
|
|
961
|
+
this.data = sorted.slice(0, this.maxRows);
|
|
962
|
+
|
|
963
|
+
return {
|
|
964
|
+
rows: this.data,
|
|
965
|
+
timestamp: new Date().toISOString(),
|
|
966
|
+
};
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
/**
|
|
970
|
+
* Render the table with data
|
|
971
|
+
*/
|
|
972
|
+
render(result) {
|
|
973
|
+
if (!this.table || !result) return;
|
|
974
|
+
|
|
975
|
+
this.table.setData({
|
|
976
|
+
headers: this.columns,
|
|
977
|
+
rows: result.rows,
|
|
978
|
+
});
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
/**
|
|
982
|
+
* Start auto-refresh timer
|
|
983
|
+
*/
|
|
984
|
+
startAutoRefresh() {
|
|
985
|
+
this.stopAutoRefresh();
|
|
986
|
+
|
|
987
|
+
if (this.refreshInterval > 0) {
|
|
988
|
+
this.refreshTimer = setInterval(async () => {
|
|
989
|
+
try {
|
|
990
|
+
const data = await this.getData();
|
|
991
|
+
this.render(data);
|
|
992
|
+
} catch (err) {
|
|
993
|
+
this.log('error', 'Auto-refresh failed: ' + err.message);
|
|
994
|
+
}
|
|
995
|
+
}, this.refreshInterval);
|
|
996
|
+
|
|
997
|
+
this.log('debug', 'Auto-refresh started (' + this.refreshInterval + 'ms)');
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
/**
|
|
1002
|
+
* Stop auto-refresh timer
|
|
1003
|
+
*/
|
|
1004
|
+
stopAutoRefresh() {
|
|
1005
|
+
if (this.refreshTimer) {
|
|
1006
|
+
clearInterval(this.refreshTimer);
|
|
1007
|
+
this.refreshTimer = null;
|
|
1008
|
+
this.log('debug', 'Auto-refresh stopped');
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
/**
|
|
1013
|
+
* Destroy the widget
|
|
1014
|
+
*/
|
|
1015
|
+
async destroy() {
|
|
1016
|
+
this.stopAutoRefresh();
|
|
1017
|
+
|
|
1018
|
+
if (this.table) {
|
|
1019
|
+
this.table.destroy();
|
|
1020
|
+
this.table = null;
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
if (this.box) {
|
|
1024
|
+
this.box.destroy();
|
|
1025
|
+
this.box = null;
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
this.loaded = false;
|
|
1029
|
+
this.log('info', '${className} widget destroyed');
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
export { ${className} };
|
|
1034
|
+
`,
|
|
1035
|
+
},
|
|
1036
|
+
|
|
1037
|
+
gauge: {
|
|
1038
|
+
name: 'Gauge Widget',
|
|
1039
|
+
description: 'Widget that displays a circular or linear gauge for single metrics',
|
|
1040
|
+
manifest: (id, name, author, options = {}) => ({
|
|
1041
|
+
id,
|
|
1042
|
+
name: name || id.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' '),
|
|
1043
|
+
description: options.description || 'A widget that displays metrics with a gauge',
|
|
1044
|
+
version: '1.0.0',
|
|
1045
|
+
author: author || '',
|
|
1046
|
+
category: options.category || 'monitoring',
|
|
1047
|
+
type: 'widget',
|
|
1048
|
+
lazyLoad: true,
|
|
1049
|
+
priority: 100,
|
|
1050
|
+
config: {
|
|
1051
|
+
gaugeType: 'circle',
|
|
1052
|
+
minValue: 0,
|
|
1053
|
+
maxValue: 100,
|
|
1054
|
+
refreshInterval: 2000,
|
|
1055
|
+
unit: '%',
|
|
1056
|
+
},
|
|
1057
|
+
__version: 1,
|
|
1058
|
+
}),
|
|
1059
|
+
|
|
1060
|
+
widgetCode: (id, className) => `/**
|
|
1061
|
+
* ${className} Widget Plugin
|
|
1062
|
+
* Gauge widget for displaying single metrics
|
|
1063
|
+
*/
|
|
1064
|
+
|
|
1065
|
+
import { BaseWidget } from 'claw-dashboard/widgets';
|
|
1066
|
+
|
|
1067
|
+
/**
|
|
1068
|
+
* ${className} - Gauge widget for Claw Dashboard
|
|
1069
|
+
*/
|
|
1070
|
+
export default class ${className} extends BaseWidget {
|
|
1071
|
+
constructor(options = {}) {
|
|
1072
|
+
super(options);
|
|
1073
|
+
this.name = options.name || '${className}';
|
|
1074
|
+
this.description = options.description || 'Gauge widget';
|
|
1075
|
+
|
|
1076
|
+
// Gauge configuration
|
|
1077
|
+
this.gaugeType = this.config.gaugeType || 'circle';
|
|
1078
|
+
this.minValue = this.config.minValue || 0;
|
|
1079
|
+
this.maxValue = this.config.maxValue || 100;
|
|
1080
|
+
this.refreshInterval = this.config.refreshInterval || 2000;
|
|
1081
|
+
this.unit = this.config.unit || '%';
|
|
1082
|
+
|
|
1083
|
+
// Widget state
|
|
1084
|
+
this.gauge = null;
|
|
1085
|
+
this.refreshTimer = null;
|
|
1086
|
+
this.currentValue = 0;
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
/**
|
|
1090
|
+
* Initialize the widget
|
|
1091
|
+
*/
|
|
1092
|
+
async init() {
|
|
1093
|
+
this.log('info', '${className} widget initialized');
|
|
1094
|
+
return true;
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
/**
|
|
1098
|
+
* Create the widget UI
|
|
1099
|
+
* @param {Object} screen - Blessed screen object
|
|
1100
|
+
* @param {Object} theme - Theme colors
|
|
1101
|
+
*/
|
|
1102
|
+
async create(screen, theme = {}) {
|
|
1103
|
+
const C = theme.colors || {};
|
|
1104
|
+
const blessed = await import('blessed');
|
|
1105
|
+
const contrib = await import('blessed-contrib');
|
|
1106
|
+
|
|
1107
|
+
this.screen = screen;
|
|
1108
|
+
this.theme = theme;
|
|
1109
|
+
|
|
1110
|
+
// Main container
|
|
1111
|
+
this.box = blessed.default.box({
|
|
1112
|
+
parent: screen,
|
|
1113
|
+
width: '30%',
|
|
1114
|
+
height: 10,
|
|
1115
|
+
border: { type: 'line' },
|
|
1116
|
+
label: ' ${className.toUpperCase()} ',
|
|
1117
|
+
style: {
|
|
1118
|
+
border: { fg: C.cyan || 'cyan' },
|
|
1119
|
+
},
|
|
1120
|
+
});
|
|
1121
|
+
|
|
1122
|
+
// Create gauge based on type
|
|
1123
|
+
if (this.gaugeType === 'circle') {
|
|
1124
|
+
this.gauge = contrib.default.gauge({
|
|
1125
|
+
parent: this.box,
|
|
1126
|
+
top: 1,
|
|
1127
|
+
left: 'center',
|
|
1128
|
+
width: '90%',
|
|
1129
|
+
height: 6,
|
|
1130
|
+
style: {
|
|
1131
|
+
label: { fg: C.white || 'white' },
|
|
1132
|
+
value: { fg: C.green || 'green' },
|
|
1133
|
+
},
|
|
1134
|
+
label: this.name,
|
|
1135
|
+
});
|
|
1136
|
+
} else {
|
|
1137
|
+
// Linear gauge
|
|
1138
|
+
this.gauge = contrib.default.gauge({
|
|
1139
|
+
parent: this.box,
|
|
1140
|
+
top: 2,
|
|
1141
|
+
left: 1,
|
|
1142
|
+
width: '96%',
|
|
1143
|
+
height: 4,
|
|
1144
|
+
style: {
|
|
1145
|
+
label: { fg: C.white || 'white' },
|
|
1146
|
+
value: { fg: C.green || 'green' },
|
|
1147
|
+
},
|
|
1148
|
+
label: this.name,
|
|
1149
|
+
});
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
// Value display
|
|
1153
|
+
this.valueText = blessed.default.text({
|
|
1154
|
+
parent: this.box,
|
|
1155
|
+
bottom: 0,
|
|
1156
|
+
left: 'center',
|
|
1157
|
+
content: '0' + this.unit,
|
|
1158
|
+
style: { fg: C.white || 'white', bold: true },
|
|
1159
|
+
});
|
|
1160
|
+
|
|
1161
|
+
this.loaded = true;
|
|
1162
|
+
this.log('debug', '${className} widget UI created');
|
|
1163
|
+
|
|
1164
|
+
// Start auto-refresh
|
|
1165
|
+
this.startAutoRefresh();
|
|
1166
|
+
|
|
1167
|
+
return this;
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
/**
|
|
1171
|
+
* Generate sample data - customize this for your data source
|
|
1172
|
+
*/
|
|
1173
|
+
async getData() {
|
|
1174
|
+
// Sample data - replace with actual data fetching
|
|
1175
|
+
const value = Math.floor(Math.random() * (this.maxValue - this.minValue) + this.minValue);
|
|
1176
|
+
this.currentValue = value;
|
|
1177
|
+
|
|
1178
|
+
return {
|
|
1179
|
+
value: value,
|
|
1180
|
+
percentage: ((value - this.minValue) / (this.maxValue - this.minValue)) * 100,
|
|
1181
|
+
timestamp: new Date().toISOString(),
|
|
1182
|
+
};
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
/**
|
|
1186
|
+
* Render the gauge with data
|
|
1187
|
+
*/
|
|
1188
|
+
render(result) {
|
|
1189
|
+
if (!this.gauge || !result) return;
|
|
1190
|
+
|
|
1191
|
+
// Set gauge percentage (0-100)
|
|
1192
|
+
this.gauge.setData(result.percentage);
|
|
1193
|
+
|
|
1194
|
+
// Update value text
|
|
1195
|
+
this.valueText.setContent(result.value + this.unit);
|
|
1196
|
+
|
|
1197
|
+
// Color based on value
|
|
1198
|
+
let color = 'green';
|
|
1199
|
+
if (result.percentage > 80) {
|
|
1200
|
+
color = 'red';
|
|
1201
|
+
} else if (result.percentage > 60) {
|
|
1202
|
+
color = 'yellow';
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
this.valueText.style.fg = this.theme?.colors?.[color] || color;
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
/**
|
|
1209
|
+
* Start auto-refresh timer
|
|
1210
|
+
*/
|
|
1211
|
+
startAutoRefresh() {
|
|
1212
|
+
this.stopAutoRefresh();
|
|
1213
|
+
|
|
1214
|
+
if (this.refreshInterval > 0) {
|
|
1215
|
+
this.refreshTimer = setInterval(async () => {
|
|
1216
|
+
try {
|
|
1217
|
+
const data = await this.getData();
|
|
1218
|
+
this.render(data);
|
|
1219
|
+
} catch (err) {
|
|
1220
|
+
this.log('error', 'Auto-refresh failed: ' + err.message);
|
|
1221
|
+
}
|
|
1222
|
+
}, this.refreshInterval);
|
|
1223
|
+
|
|
1224
|
+
this.log('debug', 'Auto-refresh started (' + this.refreshInterval + 'ms)');
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
/**
|
|
1229
|
+
* Stop auto-refresh timer
|
|
1230
|
+
*/
|
|
1231
|
+
stopAutoRefresh() {
|
|
1232
|
+
if (this.refreshTimer) {
|
|
1233
|
+
clearInterval(this.refreshTimer);
|
|
1234
|
+
this.refreshTimer = null;
|
|
1235
|
+
this.log('debug', 'Auto-refresh stopped');
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
/**
|
|
1240
|
+
* Destroy the widget
|
|
1241
|
+
*/
|
|
1242
|
+
async destroy() {
|
|
1243
|
+
this.stopAutoRefresh();
|
|
1244
|
+
|
|
1245
|
+
if (this.gauge) {
|
|
1246
|
+
this.gauge.destroy();
|
|
1247
|
+
this.gauge = null;
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
if (this.box) {
|
|
1251
|
+
this.box.destroy();
|
|
1252
|
+
this.box = null;
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
this.loaded = false;
|
|
1256
|
+
this.log('info', '${className} widget destroyed');
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
export { ${className} };
|
|
1261
|
+
`,
|
|
1262
|
+
},
|
|
1263
|
+
|
|
1264
|
+
logViewer: {
|
|
1265
|
+
name: 'Log Viewer Widget',
|
|
1266
|
+
description: 'Widget that displays scrolling log entries with filtering',
|
|
1267
|
+
manifest: (id, name, author, options = {}) => ({
|
|
1268
|
+
id,
|
|
1269
|
+
name: name || id.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' '),
|
|
1270
|
+
description: options.description || 'A widget that displays scrolling log entries',
|
|
1271
|
+
version: '1.0.0',
|
|
1272
|
+
author: author || '',
|
|
1273
|
+
category: options.category || 'monitoring',
|
|
1274
|
+
type: 'widget',
|
|
1275
|
+
lazyLoad: true,
|
|
1276
|
+
priority: 100,
|
|
1277
|
+
config: {
|
|
1278
|
+
maxLines: 50,
|
|
1279
|
+
showTimestamp: true,
|
|
1280
|
+
filterLevels: ['info', 'warn', 'error'],
|
|
1281
|
+
refreshInterval: 1000,
|
|
1282
|
+
},
|
|
1283
|
+
__version: 1,
|
|
1284
|
+
}),
|
|
1285
|
+
|
|
1286
|
+
widgetCode: (id, className) => `/**
|
|
1287
|
+
* ${className} Widget Plugin
|
|
1288
|
+
* Log viewer widget for displaying scrolling log entries
|
|
1289
|
+
*/
|
|
1290
|
+
|
|
1291
|
+
import { BaseWidget } from 'claw-dashboard/widgets';
|
|
1292
|
+
|
|
1293
|
+
/**
|
|
1294
|
+
* ${className} - Log viewer widget for Claw Dashboard
|
|
1295
|
+
*/
|
|
1296
|
+
export default class ${className} extends BaseWidget {
|
|
1297
|
+
constructor(options = {}) {
|
|
1298
|
+
super(options);
|
|
1299
|
+
this.name = options.name || '${className}';
|
|
1300
|
+
this.description = options.description || 'Log viewer widget';
|
|
1301
|
+
|
|
1302
|
+
// Log viewer configuration
|
|
1303
|
+
this.maxLines = this.config.maxLines || 50;
|
|
1304
|
+
this.showTimestamp = this.config.showTimestamp !== false;
|
|
1305
|
+
this.filterLevels = this.config.filterLevels || ['info', 'warn', 'error'];
|
|
1306
|
+
this.refreshInterval = this.config.refreshInterval || 1000;
|
|
1307
|
+
|
|
1308
|
+
// Widget state
|
|
1309
|
+
this.logBox = null;
|
|
1310
|
+
this.refreshTimer = null;
|
|
1311
|
+
this.logEntries = [];
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
/**
|
|
1315
|
+
* Initialize the widget
|
|
1316
|
+
*/
|
|
1317
|
+
async init() {
|
|
1318
|
+
this.log('info', '${className} widget initialized');
|
|
1319
|
+
|
|
1320
|
+
// Add initial log entries
|
|
1321
|
+
this.addLogEntry('info', 'Log viewer initialized');
|
|
1322
|
+
this.addLogEntry('info', 'Waiting for log data...');
|
|
1323
|
+
|
|
1324
|
+
return true;
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
/**
|
|
1328
|
+
* Add a log entry
|
|
1329
|
+
* @param {string} level - Log level (info, warn, error)
|
|
1330
|
+
* @param {string} message - Log message
|
|
1331
|
+
*/
|
|
1332
|
+
addLogEntry(level, message) {
|
|
1333
|
+
const entry = {
|
|
1334
|
+
level,
|
|
1335
|
+
message,
|
|
1336
|
+
timestamp: new Date(),
|
|
1337
|
+
};
|
|
1338
|
+
|
|
1339
|
+
this.logEntries.push(entry);
|
|
1340
|
+
|
|
1341
|
+
// Trim to max lines
|
|
1342
|
+
if (this.logEntries.length > this.maxLines) {
|
|
1343
|
+
this.logEntries.shift();
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
/**
|
|
1348
|
+
* Create the widget UI
|
|
1349
|
+
* @param {Object} screen - Blessed screen object
|
|
1350
|
+
* @param {Object} theme - Theme colors
|
|
1351
|
+
*/
|
|
1352
|
+
async create(screen, theme = {}) {
|
|
1353
|
+
const C = theme.colors || {};
|
|
1354
|
+
const blessed = await import('blessed');
|
|
1355
|
+
|
|
1356
|
+
this.screen = screen;
|
|
1357
|
+
this.theme = theme;
|
|
1358
|
+
|
|
1359
|
+
// Main container
|
|
1360
|
+
this.box = blessed.default.box({
|
|
1361
|
+
parent: screen,
|
|
1362
|
+
width: '70%',
|
|
1363
|
+
height: 15,
|
|
1364
|
+
border: { type: 'line' },
|
|
1365
|
+
label: ' ${className.toUpperCase()} ',
|
|
1366
|
+
style: {
|
|
1367
|
+
border: { fg: C.cyan || 'cyan' },
|
|
1368
|
+
},
|
|
1369
|
+
});
|
|
1370
|
+
|
|
1371
|
+
// Log entries box with scrolling
|
|
1372
|
+
this.logBox = blessed.default.log({
|
|
1373
|
+
parent: this.box,
|
|
1374
|
+
top: 1,
|
|
1375
|
+
left: 1,
|
|
1376
|
+
width: '98%',
|
|
1377
|
+
height: '90%',
|
|
1378
|
+
scrollable: true,
|
|
1379
|
+
scrollbar: {
|
|
1380
|
+
style: {
|
|
1381
|
+
bg: C.gray || 'gray',
|
|
1382
|
+
},
|
|
1383
|
+
},
|
|
1384
|
+
style: {
|
|
1385
|
+
fg: C.white || 'white',
|
|
1386
|
+
bg: C.black || 'black',
|
|
1387
|
+
},
|
|
1388
|
+
});
|
|
1389
|
+
|
|
1390
|
+
this.loaded = true;
|
|
1391
|
+
this.log('debug', '${className} widget UI created');
|
|
1392
|
+
|
|
1393
|
+
// Initial render
|
|
1394
|
+
this.renderLogs();
|
|
1395
|
+
|
|
1396
|
+
// Start auto-refresh
|
|
1397
|
+
this.startAutoRefresh();
|
|
1398
|
+
|
|
1399
|
+
return this;
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
/**
|
|
1403
|
+
* Get filtered log entries
|
|
1404
|
+
*/
|
|
1405
|
+
async getData() {
|
|
1406
|
+
// Sample log generation - replace with actual log fetching
|
|
1407
|
+
const levels = ['info', 'info', 'info', 'warn', 'error'];
|
|
1408
|
+
const messages = [
|
|
1409
|
+
'Request processed successfully',
|
|
1410
|
+
'Connection established',
|
|
1411
|
+
'Data synchronized',
|
|
1412
|
+
'High memory usage detected',
|
|
1413
|
+
'Failed to connect to service',
|
|
1414
|
+
];
|
|
1415
|
+
|
|
1416
|
+
// Randomly add new log entry
|
|
1417
|
+
if (Math.random() > 0.7) {
|
|
1418
|
+
const level = levels[Math.floor(Math.random() * levels.length)];
|
|
1419
|
+
const message = messages[Math.floor(Math.random() * messages.length)];
|
|
1420
|
+
this.addLogEntry(level, message);
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
// Filter by level
|
|
1424
|
+
const filtered = this.logEntries.filter(entry =>
|
|
1425
|
+
this.filterLevels.includes(entry.level)
|
|
1426
|
+
);
|
|
1427
|
+
|
|
1428
|
+
return {
|
|
1429
|
+
entries: filtered,
|
|
1430
|
+
timestamp: new Date().toISOString(),
|
|
1431
|
+
};
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
/**
|
|
1435
|
+
* Render the log entries
|
|
1436
|
+
*/
|
|
1437
|
+
renderLogs() {
|
|
1438
|
+
if (!this.logBox) return;
|
|
1439
|
+
|
|
1440
|
+
this.logBox.setContent('');
|
|
1441
|
+
|
|
1442
|
+
for (const entry of this.logEntries) {
|
|
1443
|
+
if (!this.filterLevels.includes(entry.level)) continue;
|
|
1444
|
+
|
|
1445
|
+
let line = '';
|
|
1446
|
+
|
|
1447
|
+
if (this.showTimestamp) {
|
|
1448
|
+
const time = entry.timestamp.toLocaleTimeString();
|
|
1449
|
+
line += '[' + time + '] ';
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
const levelStr = entry.level.toUpperCase().padEnd(5);
|
|
1453
|
+
line += '[' + levelStr + '] ' + entry.message;
|
|
1454
|
+
|
|
1455
|
+
// Set color based on level
|
|
1456
|
+
const colorMap = {
|
|
1457
|
+
info: this.theme?.colors?.white || 'white',
|
|
1458
|
+
warn: this.theme?.colors?.yellow || 'yellow',
|
|
1459
|
+
error: this.theme?.colors?.red || 'red',
|
|
1460
|
+
};
|
|
1461
|
+
|
|
1462
|
+
this.logBox.add(line, colorMap[entry.level] || 'white');
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
// Scroll to bottom
|
|
1466
|
+
this.logBox.setScrollPerc(100);
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
/**
|
|
1470
|
+
* Render the widget with data
|
|
1471
|
+
*/
|
|
1472
|
+
render(result) {
|
|
1473
|
+
if (!result) return;
|
|
1474
|
+
this.renderLogs();
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
/**
|
|
1478
|
+
* Start auto-refresh timer
|
|
1479
|
+
*/
|
|
1480
|
+
startAutoRefresh() {
|
|
1481
|
+
this.stopAutoRefresh();
|
|
1482
|
+
|
|
1483
|
+
if (this.refreshInterval > 0) {
|
|
1484
|
+
this.refreshTimer = setInterval(async () => {
|
|
1485
|
+
try {
|
|
1486
|
+
const data = await this.getData();
|
|
1487
|
+
this.render(data);
|
|
1488
|
+
} catch (err) {
|
|
1489
|
+
this.log('error', 'Auto-refresh failed: ' + err.message);
|
|
1490
|
+
}
|
|
1491
|
+
}, this.refreshInterval);
|
|
1492
|
+
|
|
1493
|
+
this.log('debug', 'Auto-refresh started (' + this.refreshInterval + 'ms)');
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
/**
|
|
1498
|
+
* Stop auto-refresh timer
|
|
1499
|
+
*/
|
|
1500
|
+
stopAutoRefresh() {
|
|
1501
|
+
if (this.refreshTimer) {
|
|
1502
|
+
clearInterval(this.refreshTimer);
|
|
1503
|
+
this.refreshTimer = null;
|
|
1504
|
+
this.log('debug', 'Auto-refresh stopped');
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
/**
|
|
1509
|
+
* Destroy the widget
|
|
1510
|
+
*/
|
|
1511
|
+
async destroy() {
|
|
1512
|
+
this.stopAutoRefresh();
|
|
1513
|
+
|
|
1514
|
+
if (this.logBox) {
|
|
1515
|
+
this.logBox.destroy();
|
|
1516
|
+
this.logBox = null;
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
if (this.box) {
|
|
1520
|
+
this.box.destroy();
|
|
1521
|
+
this.box = null;
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
this.logEntries = [];
|
|
1525
|
+
this.loaded = false;
|
|
1526
|
+
this.log('info', '${className} widget destroyed');
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
export { ${className} };
|
|
1531
|
+
`,
|
|
1532
|
+
},
|
|
1533
|
+
};
|
|
1534
|
+
|
|
1535
|
+
/**
|
|
1536
|
+
* Converts a plugin ID to a valid JavaScript class name
|
|
1537
|
+
* @param {string} id - Plugin ID (e.g., "my-custom-widget")
|
|
1538
|
+
* @returns {string} Class name (e.g., "MyCustomWidget")
|
|
1539
|
+
*/
|
|
1540
|
+
function toClassName(id) {
|
|
1541
|
+
return id
|
|
1542
|
+
.split(/[-_]/)
|
|
1543
|
+
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
|
1544
|
+
.join('');
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
/**
|
|
1548
|
+
* Validates a plugin ID
|
|
1549
|
+
* @param {string} id - Plugin ID
|
|
1550
|
+
* @returns {object} Validation result
|
|
1551
|
+
*/
|
|
1552
|
+
function validatePluginId(id) {
|
|
1553
|
+
if (!id || typeof id !== 'string') {
|
|
1554
|
+
return { valid: false, error: 'Plugin ID must be a non-empty string' };
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
if (id.length < 1 || id.length > 64) {
|
|
1558
|
+
return { valid: false, error: 'Plugin ID must be between 1 and 64 characters' };
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
// Check for valid characters
|
|
1562
|
+
const validPattern = /^[a-zA-Z0-9]([a-zA-Z0-9_-]*[a-zA-Z0-9])?$/;
|
|
1563
|
+
if (!validPattern.test(id)) {
|
|
1564
|
+
return {
|
|
1565
|
+
valid: false,
|
|
1566
|
+
error: 'Plugin ID must contain only alphanumeric characters, hyphens, and underscores, and cannot start or end with a hyphen/underscore',
|
|
1567
|
+
};
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
// Check for reserved names
|
|
1571
|
+
const reservedNames = ['claw', 'dashboard', 'admin', 'system', 'test'];
|
|
1572
|
+
if (reservedNames.includes(id.toLowerCase())) {
|
|
1573
|
+
return { valid: false, error: `'${id}' is a reserved name` };
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
return { valid: true };
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
/**
|
|
1580
|
+
* Generate README content
|
|
1581
|
+
* @param {string} id - Plugin ID
|
|
1582
|
+
* @param {string} name - Plugin name
|
|
1583
|
+
* @param {string} templateType - Template type
|
|
1584
|
+
* @returns {string} README content
|
|
1585
|
+
*/
|
|
1586
|
+
function generateReadme(id, name, templateType) {
|
|
1587
|
+
const template = TEMPLATES[templateType] || TEMPLATES.basic;
|
|
1588
|
+
return `# ${name}
|
|
1589
|
+
|
|
1590
|
+
${template.description} for Claw Dashboard.
|
|
1591
|
+
|
|
1592
|
+
## Installation
|
|
1593
|
+
|
|
1594
|
+
1. Copy this directory to your Claw Dashboard plugins folder:
|
|
1595
|
+
\`\`\`bash
|
|
1596
|
+
cp -r ${id} ~/.openclaw/plugins/
|
|
1597
|
+
\`\`\`
|
|
1598
|
+
|
|
1599
|
+
2. Restart Claw Dashboard or reload plugins
|
|
1600
|
+
|
|
1601
|
+
## Configuration
|
|
1602
|
+
|
|
1603
|
+
Edit \`plugin.json\` to customize the widget:
|
|
1604
|
+
|
|
1605
|
+
\`\`\`json
|
|
1606
|
+
{
|
|
1607
|
+
"config": {
|
|
1608
|
+
// See plugin.json for available options
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
\`\`\`
|
|
1612
|
+
|
|
1613
|
+
## Development
|
|
1614
|
+
|
|
1615
|
+
### File Structure
|
|
1616
|
+
|
|
1617
|
+
\`\`\`
|
|
1618
|
+
${id}/
|
|
1619
|
+
├── plugin.json # Plugin manifest
|
|
1620
|
+
├── index.js # Widget code
|
|
1621
|
+
└── README.md # This file
|
|
1622
|
+
\`\`\`
|
|
1623
|
+
|
|
1624
|
+
### Testing
|
|
1625
|
+
|
|
1626
|
+
Run your widget in Claw Dashboard:
|
|
1627
|
+
|
|
1628
|
+
\`\`\`bash
|
|
1629
|
+
clawdash --debug
|
|
1630
|
+
\`\`\`
|
|
1631
|
+
|
|
1632
|
+
## API Reference
|
|
1633
|
+
|
|
1634
|
+
See [Claw Dashboard Plugin Documentation](https://github.com/spleck/claw-dashboard/blob/main/docs/PLUGINS.md) for full API reference.
|
|
1635
|
+
|
|
1636
|
+
## License
|
|
1637
|
+
|
|
1638
|
+
MIT
|
|
1639
|
+
`;
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
/**
|
|
1643
|
+
* List available templates
|
|
1644
|
+
* @returns {object[]} Array of template info
|
|
1645
|
+
*/
|
|
1646
|
+
export function listTemplates() {
|
|
1647
|
+
return Object.entries(TEMPLATES).map(([key, template]) => ({
|
|
1648
|
+
id: key,
|
|
1649
|
+
name: template.name,
|
|
1650
|
+
description: template.description,
|
|
1651
|
+
}));
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
/**
|
|
1655
|
+
* Creates a plugin scaffold
|
|
1656
|
+
* @param {string} id - Plugin ID
|
|
1657
|
+
* @param {object} options - Options
|
|
1658
|
+
* @returns {object} Result
|
|
1659
|
+
*/
|
|
1660
|
+
export async function createPlugin(id, options = {}) {
|
|
1661
|
+
const {
|
|
1662
|
+
name,
|
|
1663
|
+
author,
|
|
1664
|
+
outputDir,
|
|
1665
|
+
template = 'basic',
|
|
1666
|
+
dryRun = false,
|
|
1667
|
+
force = false,
|
|
1668
|
+
} = options;
|
|
1669
|
+
|
|
1670
|
+
// Validate plugin ID
|
|
1671
|
+
const validation = validatePluginId(id);
|
|
1672
|
+
if (!validation.valid) {
|
|
1673
|
+
return {
|
|
1674
|
+
success: false,
|
|
1675
|
+
error: validation.error,
|
|
1676
|
+
code: 'INVALID_ID',
|
|
1677
|
+
};
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
// Validate template
|
|
1681
|
+
const selectedTemplate = TEMPLATES[template];
|
|
1682
|
+
if (!selectedTemplate) {
|
|
1683
|
+
return {
|
|
1684
|
+
success: false,
|
|
1685
|
+
error: `Unknown template: ${template}. Available: ${Object.keys(TEMPLATES).join(', ')}`,
|
|
1686
|
+
code: 'INVALID_TEMPLATE',
|
|
1687
|
+
};
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1690
|
+
// Determine output directory
|
|
1691
|
+
const pluginsDir = outputDir || join(homedir(), '.openclaw', 'plugins');
|
|
1692
|
+
const pluginDir = join(pluginsDir, id);
|
|
1693
|
+
|
|
1694
|
+
// Check if plugin already exists
|
|
1695
|
+
if (existsSync(pluginDir) && !force) {
|
|
1696
|
+
return {
|
|
1697
|
+
success: false,
|
|
1698
|
+
error: `Plugin directory already exists: ${pluginDir}`,
|
|
1699
|
+
code: 'ALREADY_EXISTS',
|
|
1700
|
+
path: pluginDir,
|
|
1701
|
+
};
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
// Generate class name from ID
|
|
1705
|
+
const className = toClassName(id);
|
|
1706
|
+
|
|
1707
|
+
// Generate files
|
|
1708
|
+
const files = {
|
|
1709
|
+
'plugin.json': JSON.stringify(
|
|
1710
|
+
selectedTemplate.manifest(id, name, author, options),
|
|
1711
|
+
null,
|
|
1712
|
+
2
|
|
1713
|
+
),
|
|
1714
|
+
'index.js': selectedTemplate.widgetCode(id, className),
|
|
1715
|
+
'README.md': generateReadme(id, name || id, template),
|
|
1716
|
+
};
|
|
1717
|
+
|
|
1718
|
+
// In dry-run mode, just return what would be created
|
|
1719
|
+
if (dryRun) {
|
|
1720
|
+
return {
|
|
1721
|
+
success: true,
|
|
1722
|
+
dryRun: true,
|
|
1723
|
+
path: pluginDir,
|
|
1724
|
+
files: Object.keys(files),
|
|
1725
|
+
template,
|
|
1726
|
+
};
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
// Create directory
|
|
1730
|
+
try {
|
|
1731
|
+
mkdirSync(pluginDir, { recursive: true });
|
|
1732
|
+
} catch (err) {
|
|
1733
|
+
return {
|
|
1734
|
+
success: false,
|
|
1735
|
+
error: `Failed to create directory: ${err.message}`,
|
|
1736
|
+
code: 'MKDIR_ERROR',
|
|
1737
|
+
};
|
|
1738
|
+
}
|
|
1739
|
+
|
|
1740
|
+
// Write files
|
|
1741
|
+
const createdFiles = [];
|
|
1742
|
+
for (const [filename, content] of Object.entries(files)) {
|
|
1743
|
+
const filePath = join(pluginDir, filename);
|
|
1744
|
+
try {
|
|
1745
|
+
writeFileSync(filePath, content);
|
|
1746
|
+
createdFiles.push(filename);
|
|
1747
|
+
} catch (err) {
|
|
1748
|
+
return {
|
|
1749
|
+
success: false,
|
|
1750
|
+
error: `Failed to write ${filename}: ${err.message}`,
|
|
1751
|
+
code: 'WRITE_ERROR',
|
|
1752
|
+
path: filePath,
|
|
1753
|
+
};
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
return {
|
|
1758
|
+
success: true,
|
|
1759
|
+
path: pluginDir,
|
|
1760
|
+
files: createdFiles,
|
|
1761
|
+
id,
|
|
1762
|
+
className,
|
|
1763
|
+
template,
|
|
1764
|
+
};
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
/**
|
|
1768
|
+
* Main CLI handler
|
|
1769
|
+
* @param {string[]} args - CLI arguments
|
|
1770
|
+
* @returns {number} Exit code
|
|
1771
|
+
*/
|
|
1772
|
+
export async function runScaffoldCli(args) {
|
|
1773
|
+
const command = args[0];
|
|
1774
|
+
|
|
1775
|
+
if (!command || command === '--help' || command === '-h') {
|
|
1776
|
+
console.log(`
|
|
1777
|
+
Plugin Scaffolding CLI for Claw Dashboard
|
|
1778
|
+
|
|
1779
|
+
Usage: clawdash create-plugin <id> [options]
|
|
1780
|
+
|
|
1781
|
+
Arguments:
|
|
1782
|
+
id Plugin ID (kebab-case, e.g., "my-custom-widget")
|
|
1783
|
+
|
|
1784
|
+
Options:
|
|
1785
|
+
-t, --template Template to use (basic, api, chart, table, gauge, logViewer)
|
|
1786
|
+
Default: basic
|
|
1787
|
+
-n, --name Display name for the widget
|
|
1788
|
+
-a, --author Author name or email
|
|
1789
|
+
-c, --category Widget category (system, monitoring, custom, example)
|
|
1790
|
+
Default: custom
|
|
1791
|
+
--desc Widget description
|
|
1792
|
+
-o, --output Output directory (default: ~/.openclaw/plugins/)
|
|
1793
|
+
-f, --force Overwrite existing plugin
|
|
1794
|
+
--dry-run Show what would be created without creating it
|
|
1795
|
+
--list-templates Show available templates
|
|
1796
|
+
-i, --interactive Start interactive mode (prompts for all options)
|
|
1797
|
+
-h, --help Show this help message
|
|
1798
|
+
|
|
1799
|
+
Examples:
|
|
1800
|
+
clawdash create-plugin my-widget
|
|
1801
|
+
clawdash create-plugin api-status --template api --author "John Doe"
|
|
1802
|
+
clawdash create-plugin metrics --template chart --category monitoring
|
|
1803
|
+
clawdash create-plugin my-widget --interactive
|
|
1804
|
+
clawdash create-plugin --list-templates
|
|
1805
|
+
`);
|
|
1806
|
+
return 0;
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1809
|
+
if (command === '--version' || command === '-v') {
|
|
1810
|
+
console.log('clawdash-create-plugin 1.1.0');
|
|
1811
|
+
return 0;
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1814
|
+
if (command === '--list-templates' || command === 'list-templates') {
|
|
1815
|
+
console.log('Available Templates:');
|
|
1816
|
+
console.log('');
|
|
1817
|
+
const templates = listTemplates();
|
|
1818
|
+
templates.forEach(t => {
|
|
1819
|
+
console.log(` ${t.id.padEnd(12)} ${t.name}`);
|
|
1820
|
+
console.log(` ${t.description}`);
|
|
1821
|
+
console.log('');
|
|
1822
|
+
});
|
|
1823
|
+
return 0;
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
// Parse arguments
|
|
1827
|
+
const options = {
|
|
1828
|
+
name: undefined,
|
|
1829
|
+
author: undefined,
|
|
1830
|
+
category: 'custom',
|
|
1831
|
+
description: undefined,
|
|
1832
|
+
template: 'basic',
|
|
1833
|
+
outputDir: undefined,
|
|
1834
|
+
force: false,
|
|
1835
|
+
dryRun: false,
|
|
1836
|
+
};
|
|
1837
|
+
|
|
1838
|
+
let pluginId = null;
|
|
1839
|
+
|
|
1840
|
+
for (let i = 0; i < args.length; i++) {
|
|
1841
|
+
const arg = args[i];
|
|
1842
|
+
|
|
1843
|
+
if (!arg.startsWith('-') && !pluginId) {
|
|
1844
|
+
pluginId = arg;
|
|
1845
|
+
continue;
|
|
1846
|
+
}
|
|
1847
|
+
|
|
1848
|
+
switch (arg) {
|
|
1849
|
+
case '-t':
|
|
1850
|
+
case '--template':
|
|
1851
|
+
options.template = args[++i];
|
|
1852
|
+
break;
|
|
1853
|
+
case '-n':
|
|
1854
|
+
case '--name':
|
|
1855
|
+
options.name = args[++i];
|
|
1856
|
+
break;
|
|
1857
|
+
case '-a':
|
|
1858
|
+
case '--author':
|
|
1859
|
+
options.author = args[++i];
|
|
1860
|
+
break;
|
|
1861
|
+
case '-c':
|
|
1862
|
+
case '--category':
|
|
1863
|
+
options.category = args[++i];
|
|
1864
|
+
break;
|
|
1865
|
+
case '--desc':
|
|
1866
|
+
options.description = args[++i];
|
|
1867
|
+
break;
|
|
1868
|
+
case '-o':
|
|
1869
|
+
case '--output':
|
|
1870
|
+
options.outputDir = args[++i];
|
|
1871
|
+
break;
|
|
1872
|
+
case '-f':
|
|
1873
|
+
case '--force':
|
|
1874
|
+
options.force = true;
|
|
1875
|
+
break;
|
|
1876
|
+
case '--dry-run':
|
|
1877
|
+
options.dryRun = true;
|
|
1878
|
+
break;
|
|
1879
|
+
case '-i':
|
|
1880
|
+
case '--interactive':
|
|
1881
|
+
options.interactive = true;
|
|
1882
|
+
break;
|
|
1883
|
+
case '-h':
|
|
1884
|
+
case '--help':
|
|
1885
|
+
// Already handled above
|
|
1886
|
+
break;
|
|
1887
|
+
}
|
|
1888
|
+
}
|
|
1889
|
+
|
|
1890
|
+
// Handle interactive mode
|
|
1891
|
+
if (options.interactive) {
|
|
1892
|
+
const interactiveOptions = await runInteractiveMode();
|
|
1893
|
+
if (!interactiveOptions) {
|
|
1894
|
+
return 0; // User cancelled
|
|
1895
|
+
}
|
|
1896
|
+
// Merge interactive options with any passed CLI options
|
|
1897
|
+
Object.assign(options, interactiveOptions);
|
|
1898
|
+
pluginId = interactiveOptions.id;
|
|
1899
|
+
}
|
|
1900
|
+
|
|
1901
|
+
if (!pluginId) {
|
|
1902
|
+
console.error('Error: Plugin ID is required');
|
|
1903
|
+
console.error('Run with --help for usage information');
|
|
1904
|
+
return 1;
|
|
1905
|
+
}
|
|
1906
|
+
|
|
1907
|
+
const result = await createPlugin(pluginId, options);
|
|
1908
|
+
|
|
1909
|
+
if (!result.success) {
|
|
1910
|
+
console.error(`Error: ${result.error}`);
|
|
1911
|
+
return 1;
|
|
1912
|
+
}
|
|
1913
|
+
|
|
1914
|
+
if (result.dryRun) {
|
|
1915
|
+
console.log('Dry run - would create:');
|
|
1916
|
+
console.log(` Directory: ${result.path}`);
|
|
1917
|
+
console.log(` Template: ${result.template}`);
|
|
1918
|
+
console.log(' Files:');
|
|
1919
|
+
result.files.forEach(f => console.log(` - ${f}`));
|
|
1920
|
+
} else {
|
|
1921
|
+
console.log(`✓ Created plugin: ${pluginId}`);
|
|
1922
|
+
console.log(` Path: ${result.path}`);
|
|
1923
|
+
console.log(` Template: ${result.template}`);
|
|
1924
|
+
console.log(` Files: ${result.files.join(', ')}`);
|
|
1925
|
+
console.log('');
|
|
1926
|
+
console.log('Next steps:');
|
|
1927
|
+
console.log(` 1. Edit ${result.path}/index.js to implement your widget`);
|
|
1928
|
+
console.log(` 2. Update ${result.path}/plugin.json with your configuration`);
|
|
1929
|
+
console.log(' 3. Run clawdash to see your widget in action');
|
|
1930
|
+
}
|
|
1931
|
+
|
|
1932
|
+
return 0;
|
|
1933
|
+
}
|
|
1934
|
+
|
|
1935
|
+
// Run if called directly
|
|
1936
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
1937
|
+
(async () => {
|
|
1938
|
+
const exitCode = await runScaffoldCli(process.argv.slice(2));
|
|
1939
|
+
process.exit(exitCode);
|
|
1940
|
+
})();
|
|
1941
|
+
}
|