claw-dashboard 1.8.4 → 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.
Files changed (135) hide show
  1. package/.c8rc.json +38 -0
  2. package/.dockerignore +68 -0
  3. package/.github/dependabot.yml +45 -0
  4. package/.github/pull_request_template.md +39 -0
  5. package/.github/workflows/ci.yml +147 -0
  6. package/.github/workflows/release.yml +40 -0
  7. package/.github/workflows/security.yml +84 -0
  8. package/.husky/pre-commit +1 -0
  9. package/.planning/codebase/ARCHITECTURE.md +206 -0
  10. package/.planning/codebase/INTEGRATIONS.md +150 -0
  11. package/.planning/codebase/STACK.md +122 -0
  12. package/.planning/codebase/STRUCTURE.md +201 -0
  13. package/CHANGELOG.md +293 -0
  14. package/CONTRIBUTING.md +378 -0
  15. package/Dockerfile +54 -0
  16. package/FEATURES.md +195 -21
  17. package/README.md +83 -6
  18. package/TODO.md +28 -0
  19. package/build-cjs.js +127 -0
  20. package/cjs-shim.js +13 -0
  21. package/dist/clawdash +1729 -0
  22. package/dist/clawdash.meta.json +2236 -0
  23. package/dist/widgets.cjs +6511 -0
  24. package/docker-compose.yml +67 -0
  25. package/docs/API.md +1050 -0
  26. package/docs/PLUGINS.md +1504 -0
  27. package/esbuild.config.js +158 -0
  28. package/eslint.config.js +56 -0
  29. package/examples/plugins/README.md +122 -0
  30. package/examples/plugins/api-status/index.js +294 -0
  31. package/examples/plugins/api-status/plugin.json +19 -0
  32. package/examples/plugins/hello-world/index.js +104 -0
  33. package/examples/plugins/hello-world/plugin.json +15 -0
  34. package/examples/plugins/system-metrics-chart/index.js +339 -0
  35. package/examples/plugins/system-metrics-chart/plugin.json +18 -0
  36. package/examples/plugins/weather-widget/index.js +136 -0
  37. package/examples/plugins/weather-widget/plugin.json +19 -0
  38. package/index.cjs +23005 -0
  39. package/index.js +5331 -512
  40. package/jest.config.js +11 -0
  41. package/man/clawdash.1 +276 -0
  42. package/package.json +54 -5
  43. package/schemas/plugin-manifest.json +128 -0
  44. package/scripts/release.js +595 -0
  45. package/src/alerts.js +693 -0
  46. package/src/auto-save.js +584 -0
  47. package/src/cache.js +390 -0
  48. package/src/checksum.js +146 -0
  49. package/src/cli/args.js +110 -0
  50. package/src/cli/export-schedule.js +423 -0
  51. package/src/cli/export-snapshot.js +138 -0
  52. package/src/cli/help.js +69 -0
  53. package/src/cli/import-snapshot.js +230 -0
  54. package/src/cli/index.js +14 -0
  55. package/src/cli/list-templates.js +69 -0
  56. package/src/cli/validate-config.js +76 -0
  57. package/src/cli/validate-plugin.js +187 -0
  58. package/src/cli/version.js +15 -0
  59. package/src/config-validator.js +586 -0
  60. package/src/config-watcher.js +401 -0
  61. package/src/config.js +684 -0
  62. package/src/container-detector.js +499 -0
  63. package/src/database.js +734 -0
  64. package/src/differential-render.js +327 -0
  65. package/src/errors.js +169 -0
  66. package/src/export-scheduler.js +581 -0
  67. package/src/gateway-manager.js +685 -0
  68. package/src/hints.js +371 -0
  69. package/src/loading-states.js +509 -0
  70. package/src/logger.js +185 -0
  71. package/src/memory-pressure.js +467 -0
  72. package/src/performance-monitor.js +374 -0
  73. package/src/plugin-errors.js +586 -0
  74. package/src/plugin-manifest-validator.js +219 -0
  75. package/src/plugin-reload.js +481 -0
  76. package/src/plugin-scaffold.js +1941 -0
  77. package/src/plugin-validator.js +284 -0
  78. package/src/retry.js +218 -0
  79. package/src/security.js +860 -0
  80. package/src/snapshot.js +372 -0
  81. package/src/splash.js +115 -0
  82. package/src/theme-selector.js +411 -0
  83. package/src/themes.js +874 -0
  84. package/src/transitions.js +534 -0
  85. package/src/types.d.ts +174 -0
  86. package/src/validation.js +926 -0
  87. package/src/web-server.js +885 -0
  88. package/src/widgets/builtin-widgets.js +1057 -0
  89. package/src/widgets/config-processor.js +401 -0
  90. package/src/widgets/dependency-resolver.js +596 -0
  91. package/src/widgets/index.js +79 -0
  92. package/src/widgets/plugin-api.js +845 -0
  93. package/src/widgets/widget-error-boundary.js +773 -0
  94. package/src/widgets/widget-error-isolation.js +551 -0
  95. package/src/widgets/widget-loader.js +1437 -0
  96. package/src/workers/system-worker.js +130 -0
  97. package/src/workers/worker-pool.js +633 -0
  98. package/tests/alerts.test.js +437 -0
  99. package/tests/auto-save.test.js +529 -0
  100. package/tests/cache.test.js +317 -0
  101. package/tests/cli.test.js +351 -0
  102. package/tests/command-palette.test.js +320 -0
  103. package/tests/config-processor.test.js +452 -0
  104. package/tests/config-validator.test.js +710 -0
  105. package/tests/config-watcher.test.js +594 -0
  106. package/tests/config.test.js +755 -0
  107. package/tests/database.test.js +438 -0
  108. package/tests/errors.test.js +189 -0
  109. package/tests/example-plugins.test.js +624 -0
  110. package/tests/integration.test.js +1321 -0
  111. package/tests/loading-states.test.js +300 -0
  112. package/tests/manifest-validation-on-load.test.js +671 -0
  113. package/tests/performance-monitor.test.js +190 -0
  114. package/tests/plugin-api-rate-limit.test.js +302 -0
  115. package/tests/plugin-errors.test.js +311 -0
  116. package/tests/plugin-lifecycle-e2e.test.js +1036 -0
  117. package/tests/plugin-reload.test.js +764 -0
  118. package/tests/rate-limiter.test.js +539 -0
  119. package/tests/retry.test.js +308 -0
  120. package/tests/security.test.js +411 -0
  121. package/tests/settings-modal.test.js +226 -0
  122. package/tests/theme-selector.test.js +57 -0
  123. package/tests/utils.js +242 -0
  124. package/tests/utils.test.js +317 -0
  125. package/tests/validate-plugin-cli.test.js +359 -0
  126. package/tests/validation.test.js +837 -0
  127. package/tests/web-server.test.js +646 -0
  128. package/tests/widget-arrange-mode.test.js +183 -0
  129. package/tests/widget-config-hot-reload.test.js +465 -0
  130. package/tests/widget-dependency.test.js +591 -0
  131. package/tests/widget-error-boundary.test.js +749 -0
  132. package/tests/widget-error-isolation.test.js +469 -0
  133. package/tests/widget-integration.test.js +1147 -0
  134. package/tests/widget-refresh-intervals.test.js +284 -0
  135. package/tests/worker-pool.test.js +522 -0
@@ -0,0 +1,1504 @@
1
+ # Claw Dashboard Widget Plugin System
2
+
3
+ The Claw Dashboard now supports **lazy loading** of widgets and a **plugin API** for third-party widgets.
4
+
5
+ ## Features
6
+
7
+ - **Lazy Loading**: Widgets are loaded on-demand, improving startup performance
8
+ - **Plugin API**: Stable API for developing third-party widgets
9
+ - **Built-in Widgets**: All built-in widgets use the lazy loading system
10
+ - **Auto-Discovery**: Plugins are automatically discovered from `~/.openclaw/plugins/`
11
+
12
+ ## Quick Start
13
+
14
+ ### Creating a Custom Widget Plugin
15
+
16
+ 1. Create a directory in `~/.openclaw/plugins/my-widget/`
17
+ 2. Create `plugin.json` manifest
18
+ 3. Create `index.js` with your widget code
19
+
20
+ ### Plugin Structure
21
+
22
+ ```
23
+ ~/.openclaw/plugins/
24
+ └── my-widget/
25
+ ├── plugin.json
26
+ └── index.js
27
+ ```
28
+
29
+ ### Plugin Manifest (plugin.json)
30
+
31
+ ```json
32
+ {
33
+ "id": "my-custom-widget",
34
+ "name": "My Custom Widget",
35
+ "description": "A custom widget for the dashboard",
36
+ "version": "1.0.0",
37
+ "author": "Your Name",
38
+ "category": "custom",
39
+ "type": "widget",
40
+ "lazyLoad": true,
41
+ "config": {
42
+ "title": "Custom Widget",
43
+ "refreshInterval": 5000
44
+ }
45
+ }
46
+ ```
47
+
48
+ ### Widget Code (index.js)
49
+
50
+ ```javascript
51
+ import { BaseWidget } from 'claw-dashboard/widgets';
52
+
53
+ export default class MyCustomWidget extends BaseWidget {
54
+ constructor(options) {
55
+ super(options);
56
+ this.name = options.name || 'Custom Widget';
57
+ this.data = null;
58
+ }
59
+
60
+ async create(screen, theme) {
61
+ // Create blessed elements
62
+ const blessed = require('blessed');
63
+
64
+ this.box = blessed.box({
65
+ parent: screen,
66
+ height: 5,
67
+ border: { type: 'line' },
68
+ label: ` ${this.name} `,
69
+ style: { border: { fg: 'cyan' } },
70
+ });
71
+
72
+ this.content = blessed.text({
73
+ parent: this.box,
74
+ top: 1,
75
+ left: 'center',
76
+ content: 'Loading...',
77
+ });
78
+
79
+ return this;
80
+ }
81
+
82
+ async getData(dataProvider) {
83
+ // Fetch data from built-in providers or custom sources
84
+ return dataProvider ? await dataProvider('cpu') : null;
85
+ }
86
+
87
+ render(data) {
88
+ if (!this.box || !data) return;
89
+ this.content.setContent(`CPU: ${data.avg}%`);
90
+ }
91
+ }
92
+ ```
93
+
94
+ ## Plugin API Reference
95
+
96
+ ### BaseWidget Class
97
+
98
+ All custom widgets should extend `BaseWidget`:
99
+
100
+ ```javascript
101
+ class MyWidget extends BaseWidget {
102
+ // Required methods
103
+ async create(screen, theme) // Create UI elements
104
+ async getData(provider) // Fetch widget data
105
+ async render(data) // Update display
106
+
107
+ // Optional methods
108
+ async init() // Initialize (called once)
109
+ async destroy() // Cleanup (called on unload)
110
+ show() // Show widget
111
+ hide() // Hide widget
112
+ }
113
+ ```
114
+
115
+ ### PluginAPI
116
+
117
+ The PluginAPI provides access to dashboard functionality:
118
+
119
+ ```javascript
120
+ // Register extension points
121
+ api.registerExtensionPoint('header', {
122
+ description: 'Add items to header',
123
+ multiple: true,
124
+ });
125
+
126
+ // Extend functionality
127
+ api.extend('header', handler, { priority: 100 });
128
+
129
+ // Register data providers
130
+ api.registerDataProvider('custom', async () => {
131
+ return await fetchCustomData();
132
+ });
133
+
134
+ // Access system metrics
135
+ const cpu = await api.getMetrics('cpu');
136
+ const memory = await api.getMetrics('memory');
137
+
138
+ // Create UI components
139
+ const box = api.createComponent('box', {
140
+ top: 0,
141
+ left: 0,
142
+ width: 20,
143
+ height: 5,
144
+ });
145
+
146
+ // Logging
147
+ api.log('debug', 'Debug message');
148
+ api.log('info', 'Info message');
149
+ api.log('warn', 'Warning');
150
+ api.log('error', 'Error!');
151
+
152
+ // Configuration
153
+ const config = api.getConfig('my-widget', { default: 'value' });
154
+ await api.saveConfig('my-widget', { key: 'value' });
155
+ ```
156
+
157
+ ## Plugin Manifest Schema
158
+
159
+ The `plugin.json` file defines your widget's metadata and configuration. All fields are validated on load.
160
+
161
+ ### Required Fields
162
+
163
+ | Field | Type | Description |
164
+ |-------|------|-------------|
165
+ | `id` | `string` | Unique identifier for the plugin (kebab-case recommended) |
166
+ | `name` | `string` | Display name for the widget |
167
+ | `version` | `string` | Semantic version (e.g., "1.0.0") |
168
+ | `type` | `string` | Plugin type: `"widget"` |
169
+
170
+ ### Optional Fields
171
+
172
+ | Field | Type | Default | Description |
173
+ |-------|------|---------|-------------|
174
+ | `description` | `string` | `""` | Brief description of the widget |
175
+ | `author` | `string` | `""` | Author name or email |
176
+ | `category` | `string` | `"custom"` | Category for organization: `"system"`, `"monitoring"`, `"custom"`, `"example"` |
177
+ | `lazyLoad` | `boolean` | `true` | Whether to defer loading until needed |
178
+ | `priority` | `number` | `100` | Loading priority (lower = earlier) |
179
+ | `config` | `object` | `{}` | Default configuration values |
180
+ | `entryPoint` | `string` | `"index.js"` | Main JavaScript file (deprecated, use standard `index.js`) |
181
+ | `dependencies` | `array` | `[]` | Widget dependencies (see [Widget Dependencies](#widget-dependencies)) |
182
+
183
+ ### Complete Manifest Example
184
+
185
+ ```json
186
+ {
187
+ "id": "my-custom-widget",
188
+ "name": "My Custom Widget",
189
+ "description": "A widget that displays custom data with visualization",
190
+ "version": "1.2.0",
191
+ "author": "Your Name <you@example.com>",
192
+ "category": "monitoring",
193
+ "type": "widget",
194
+ "lazyLoad": true,
195
+ "priority": 50,
196
+ "config": {
197
+ "refreshInterval": 5000,
198
+ "maxDataPoints": 30,
199
+ "theme": "auto",
200
+ "enabled": true
201
+ }
202
+ }
203
+ ```
204
+
205
+ ### Field Constraints
206
+
207
+ - **id**: Must be unique across all plugins. Valid characters: alphanumeric, hyphens, underscores
208
+ - **version**: Must follow semantic versioning (major.minor.patch)
209
+ - **category**: Standard categories are `"system"`, `"monitoring"`, `"custom"`, `"example"`
210
+ - **priority**: Range 0-1000, where lower numbers load earlier
211
+ - **config**: JSON-serializable object with primitive values, arrays, and nested objects
212
+ - **dependencies**: Array of widget IDs (strings) or dependency objects (see [Widget Dependency System](#widget-dependency-system))
213
+
214
+ ### Widget Dependencies
215
+
216
+ Widgets can declare dependencies on other widgets to ensure correct initialization order:
217
+
218
+ ```json
219
+ {
220
+ "id": "my-widget",
221
+ "name": "My Widget",
222
+ "version": "1.0.0",
223
+ "type": "widget",
224
+ "dependencies": [
225
+ "cpu",
226
+ "memory",
227
+ { "id": "network", "optional": true, "version": "^1.0.0" }
228
+ ]
229
+ }
230
+ ```
231
+
232
+ **Dependency formats:**
233
+ - **Simple string**: `"cpu"` - Required dependency on widget "cpu"
234
+ - **Dependency object**: `{ "id": "network", "optional": true, "version": "^1.0.0" }`
235
+ - `id` (required): Widget ID to depend on
236
+ - `optional` (optional): If `true`, widget loads even if dependency is missing
237
+ - `version` (optional): Semver constraint for dependency version (e.g., `"^1.0.0"`, `">=2.0.0"`)
238
+
239
+ ### Manifest Validation Errors
240
+
241
+ The widget loader validates manifests and reports specific errors:
242
+
243
+ - `Missing required fields: id, name, version`
244
+ - `Invalid version format: expected semver (e.g., 1.0.0)`
245
+ - `Duplicate plugin id: my-widget`
246
+ - `Invalid category: must be one of system, monitoring, custom, example`
247
+
248
+ ## Widget Dependencies
249
+
250
+ Widgets can declare dependencies on other widgets to ensure proper initialization order. The dependency system provides:
251
+
252
+ - **Automatic ordering**: Widgets are loaded in the correct order based on dependencies
253
+ - **Circular dependency detection**: Prevents infinite loops from circular references
254
+ - **Optional dependencies**: Support for optional dependencies that don't block loading
255
+ - **Version constraints**: Specify required versions of dependencies
256
+
257
+ ### Declaring Dependencies
258
+
259
+ Add a `dependencies` array to your `plugin.json`:
260
+
261
+ ```json
262
+ {
263
+ "id": "my-widget",
264
+ "name": "My Widget",
265
+ "version": "1.0.0",
266
+ "type": "widget",
267
+ "dependencies": [
268
+ "cpu",
269
+ "memory",
270
+ { "id": "network", "optional": true },
271
+ { "id": "disk", "version": ">=1.0.0" }
272
+ ]
273
+ }
274
+ ```
275
+
276
+ ### Dependency Formats
277
+
278
+ Dependencies can be specified in three formats:
279
+
280
+ 1. **Simple string**: Just the widget ID (required dependency)
281
+ ```json
282
+ "dependencies": ["cpu", "memory"]
283
+ ```
284
+
285
+ 2. **Object with optional flag**: Mark a dependency as optional
286
+ ```json
287
+ "dependencies": [
288
+ { "id": "network", "optional": true }
289
+ ]
290
+ ```
291
+
292
+ 3. **Object with version constraint**: Require a specific version
293
+ ```json
294
+ "dependencies": [
295
+ { "id": "disk", "version": ">=1.0.0" },
296
+ { "id": "gpu", "version": "^2.0.0" }
297
+ ]
298
+ ```
299
+
300
+ ### Version Constraints
301
+
302
+ The dependency system supports semantic versioning constraints:
303
+
304
+ | Format | Meaning | Example |
305
+ |--------|---------|---------|
306
+ | `1.0.0` | Exact version | `"1.2.3"` matches only 1.2.3 |
307
+ | `>=1.0.0` | Greater than or equal | `"1.0.0"` and higher |
308
+ | `^1.0.0` | Compatible with (same major) | `1.x.x` but not `2.0.0` |
309
+ | `~1.0.0` | Approximately (same major.minor) | `1.0.x` but not `1.1.0` |
310
+
311
+ ### Dependency Loading
312
+
313
+ When using `loadAllPluginsWithFallback()`, widgets are automatically loaded in dependency order:
314
+
315
+ ```javascript
316
+ const loader = getWidgetLoader();
317
+
318
+ // Dependencies are resolved and widgets loaded in correct order
319
+ const results = await loader.loadAllPluginsWithFallback({
320
+ resolveDependencies: true, // Enable dependency resolution (default)
321
+ allowPartial: false, // Fail if dependencies missing
322
+ });
323
+
324
+ console.log(results.dependencyOrder); // ['cpu', 'memory', 'my-widget']
325
+ ```
326
+
327
+ ### Loading with Dependencies
328
+
329
+ Load a widget and its dependencies together:
330
+
331
+ ```javascript
332
+ // Load 'my-widget' and all its dependencies in order
333
+ const loaded = await loader.loadWithDependencies(['my-widget']);
334
+
335
+ // Access the loaded widgets
336
+ const myWidget = loaded.get('my-widget');
337
+ const cpuWidget = loaded.get('cpu');
338
+ ```
339
+
340
+ ### Checking Dependencies
341
+
342
+ Validate dependencies before loading:
343
+
344
+ ```javascript
345
+ const info = loader.getDependencyInfo('my-widget');
346
+
347
+ console.log(info);
348
+ // {
349
+ // id: 'my-widget',
350
+ // dependencies: ['cpu', 'memory'],
351
+ // allDependencies: ['cpu', 'memory', 'system'], // includes transitive
352
+ // dependents: ['dashboard-header'], // widgets that depend on this
353
+ // validation: { valid: true, dependencies: ['cpu', 'memory'] }
354
+ // }
355
+ ```
356
+
357
+ ### Error Handling
358
+
359
+ The dependency system detects and reports common issues:
360
+
361
+ ```javascript
362
+ const results = await loader.loadAllPluginsWithFallback({
363
+ allowPartial: true, // Skip widgets with missing deps
364
+ });
365
+
366
+ // Check for errors
367
+ if (results.dependencyErrors.length > 0) {
368
+ for (const error of results.dependencyErrors) {
369
+ console.error('Circular:', error.circularPath);
370
+ console.error('Missing:', error.missingDeps);
371
+ console.error('Version issues:', error.constraintViolations);
372
+ }
373
+ }
374
+ ```
375
+
376
+ Common errors:
377
+
378
+ | Error | Description | Solution |
379
+ |-------|-------------|----------|
380
+ | `Circular dependency detected: a -> b -> a` | Widgets depend on each other in a cycle | Restructure widgets to break the cycle |
381
+ | `Missing required dependencies: widget requires: other-widget` | A required dependency is not registered | Install the missing widget or make it optional |
382
+ | `Version X does not satisfy constraint >=Y` | Dependency version mismatch | Update the dependency or relax the constraint |
383
+
384
+ ### Visualizing Dependencies
385
+
386
+ Get the full dependency graph:
387
+
388
+ ```javascript
389
+ const graph = loader.getDependencyGraph();
390
+
391
+ console.log(graph);
392
+ // {
393
+ // 'my-widget': {
394
+ // id: 'my-widget',
395
+ // dependencies: [
396
+ // { id: 'cpu', optional: false },
397
+ // { id: 'network', optional: true }
398
+ // ],
399
+ // dependents: ['dashboard-header']
400
+ // },
401
+ // 'cpu': {
402
+ // id: 'cpu',
403
+ // dependencies: [],
404
+ // dependents: ['my-widget', 'system']
405
+ // }
406
+ // }
407
+ ```
408
+
409
+ Widgets follow a well-defined lifecycle. Each hook is called at specific points during the widget's existence.
410
+
411
+ ### Lifecycle Flow
412
+
413
+ ```
414
+ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
415
+ │ Load │────▶│ Init │────▶│ Create │────▶│ getData │────▶│ Render │
416
+ └─────────┘ └─────────┘ └─────────┘ └─────────┘ └─────────┘
417
+
418
+ ┌───────────────────────────────────────────────────────────────────────┘
419
+
420
+
421
+ ┌─────────┐ ┌─────────┐
422
+ │ Show │────▶│ Destroy │
423
+ └─────────┘ └─────────┘
424
+ ```
425
+
426
+ ### Hook Reference
427
+
428
+ #### `init()`
429
+
430
+ Called once when the widget is first loaded. Use this for one-time initialization.
431
+
432
+ **Timing:** After the widget class is instantiated, before `create()`
433
+ **Returns:** `Promise<boolean>` - Return `true` to proceed, `false` to prevent loading
434
+
435
+ ```javascript
436
+ async init() {
437
+ // Initialize state
438
+ this.cache = new Map();
439
+ this.requestCount = 0;
440
+
441
+ // Validate configuration
442
+ if (!this.config.apiKey) {
443
+ this.log('error', 'API key required');
444
+ return false;
445
+ }
446
+
447
+ // Setup external connections
448
+ this.client = new ApiClient(this.config.apiKey);
449
+ await this.client.connect();
450
+
451
+ this.log('info', 'Widget initialized');
452
+ return true;
453
+ }
454
+ ```
455
+
456
+ **Best Practices:**
457
+ - Validate configuration and return `false` if required settings are missing
458
+ - Initialize internal state and caches
459
+ - Set up external connections (API clients, databases)
460
+ - Avoid creating UI elements here (do that in `create()`)
461
+
462
+ ---
463
+
464
+ #### `create(screen, theme)`
465
+
466
+ Create the widget's UI elements using blessed. This is where you define the visual appearance.
467
+
468
+ **Parameters:**
469
+ - `screen` (`blessed.Screen`) - The blessed screen instance
470
+ - `theme` (`Object`) - Theme colors and styling
471
+
472
+ **Returns:** `Promise<Object>` - Returns `this` for chaining
473
+
474
+ ```javascript
475
+ async create(screen, theme = {}) {
476
+ const C = theme.colors || {};
477
+ const blessed = await import('blessed');
478
+
479
+ // Main container
480
+ this.box = blessed.default.box({
481
+ parent: screen,
482
+ width: '50%',
483
+ height: 10,
484
+ border: { type: 'line' },
485
+ label: ' MY WIDGET ',
486
+ style: {
487
+ border: { fg: C.cyan || 'cyan' },
488
+ },
489
+ });
490
+
491
+ // Child elements
492
+ this.titleText = blessed.default.text({
493
+ parent: this.box,
494
+ top: 0,
495
+ left: 'center',
496
+ style: { fg: C.brightCyan || 'bright-cyan', bold: true },
497
+ });
498
+
499
+ this.contentText = blessed.default.text({
500
+ parent: this.box,
501
+ top: 2,
502
+ left: 1,
503
+ wrap: true,
504
+ style: { fg: C.white || 'white' },
505
+ });
506
+
507
+ this.loaded = true;
508
+ return this;
509
+ }
510
+ ```
511
+
512
+ **Best Practices:**
513
+ - Always set `parent: screen` or `parent: this.box` for proper rendering
514
+ - Use theme colors for consistent styling
515
+ - Store element references (e.g., `this.titleText`) for later updates
516
+ - Set `this.loaded = true` when complete
517
+
518
+ ---
519
+
520
+ #### `getData()`
521
+
522
+ Fetch and return data for the widget. Called before each render cycle.
523
+
524
+ **Returns:** `Promise<Object>` - Data to pass to `render()`
525
+
526
+ ```javascript
527
+ async getData() {
528
+ const startTime = Date.now();
529
+
530
+ try {
531
+ // Fetch from API
532
+ const response = await fetch(this.config.apiUrl);
533
+ const data = await response.json();
534
+
535
+ // Process data
536
+ return {
537
+ items: data.results,
538
+ count: data.total,
539
+ latency: Date.now() - startTime,
540
+ timestamp: new Date().toISOString(),
541
+ };
542
+ } catch (err) {
543
+ this.log('error', `Data fetch failed: ${err.message}`);
544
+
545
+ // Return error state
546
+ return {
547
+ error: err.message,
548
+ timestamp: new Date().toISOString(),
549
+ };
550
+ }
551
+ }
552
+ ```
553
+
554
+ **Best Practices:**
555
+ - Always return an object, even on error
556
+ - Include timestamps for cache validation
557
+ - Handle errors gracefully - don't throw
558
+ - Use `this.log()` for debugging
559
+ - Respect rate limits with `RateLimiter`
560
+
561
+ ---
562
+
563
+ #### `render(data)`
564
+
565
+ Update the widget display with new data. This should be fast and synchronous where possible.
566
+
567
+ **Parameters:**
568
+ - `data` (`Object`) - The data returned from `getData()`
569
+
570
+ ```javascript
571
+ render(data) {
572
+ if (!this.box) return;
573
+
574
+ if (data.error) {
575
+ this.titleText.setContent('Error');
576
+ this.contentText.setContent(data.error);
577
+ this.contentText.style.fg = 'red';
578
+ return;
579
+ }
580
+
581
+ // Update display
582
+ this.titleText.setContent(`${data.count} Items`);
583
+ this.contentText.setContent(
584
+ data.items.slice(0, 5).map(item => item.name).join('\n')
585
+ );
586
+
587
+ // Update footer
588
+ if (this.footerText) {
589
+ this.footerText.setContent(`Updated: ${data.timestamp}`);
590
+ }
591
+ }
592
+ ```
593
+
594
+ **Best Practices:**
595
+ - Check if elements exist before updating (`if (!this.box) return`)
596
+ - Handle error states gracefully
597
+ - Keep updates minimal - only change what changed
598
+ - Use `setContent()` rather than recreating elements
599
+
600
+ ---
601
+
602
+ #### `destroy()`
603
+
604
+ Clean up resources when the widget is unloaded. Always called when the dashboard exits or the widget is disabled.
605
+
606
+ **Returns:** `Promise<void>`
607
+
608
+ ```javascript
609
+ async destroy() {
610
+ // Stop timers
611
+ if (this.refreshTimer) {
612
+ clearInterval(this.refreshTimer);
613
+ this.refreshTimer = null;
614
+ }
615
+
616
+ // Close connections
617
+ if (this.client) {
618
+ await this.client.disconnect();
619
+ this.client = null;
620
+ }
621
+
622
+ // Clear caches
623
+ this.cache.clear();
624
+
625
+ // Destroy blessed elements
626
+ if (this.box) {
627
+ this.box.destroy();
628
+ this.box = null;
629
+ }
630
+
631
+ this.loaded = false;
632
+ this.log('info', 'Widget destroyed');
633
+ }
634
+ ```
635
+
636
+ **Best Practices:**
637
+ - Stop all timers and intervals
638
+ - Close external connections
639
+ - Clear caches and large data structures
640
+ - Destroy blessed elements
641
+ - Set `this.loaded = false`
642
+
643
+ ---
644
+
645
+ ### Optional Hooks
646
+
647
+ #### `show()` / `hide()`
648
+
649
+ Control widget visibility. Built into `BaseWidget`, but can be overridden.
650
+
651
+ ```javascript
652
+ show() {
653
+ if (this.box) {
654
+ this.box.show();
655
+ this.visible = true;
656
+ this.emit('show');
657
+ }
658
+ }
659
+
660
+ hide() {
661
+ if (this.box) {
662
+ this.box.hide();
663
+ this.visible = false;
664
+ this.emit('hide');
665
+ }
666
+ }
667
+ ```
668
+
669
+ ---
670
+
671
+ ### Hook Execution Order
672
+
673
+ ```javascript
674
+ // When dashboard starts with your widget enabled:
675
+ const widget = new MyWidget(options);
676
+ await widget.init(); // 1. Initialize
677
+ await widget.create(screen, theme); // 2. Create UI
678
+ const data = await widget.getData(); // 3. Fetch data
679
+ widget.render(data); // 4. Render display
680
+
681
+ // During normal operation (on refresh):
682
+ const data = await widget.getData(); // 1. Fetch data
683
+ widget.render(data); // 2. Render display
684
+
685
+ // When widget is disabled or dashboard exits:
686
+ await widget.destroy(); // 1. Cleanup
687
+ ```
688
+
689
+ ---
690
+
691
+ ### Error Handling in Hooks
692
+
693
+ Each hook should handle its own errors. Unhandled exceptions may crash the widget loader.
694
+
695
+ ```javascript
696
+ async getData() {
697
+ try {
698
+ return await fetchData();
699
+ } catch (err) {
700
+ this.log('error', err.message);
701
+ return { error: err.message }; // Return error state
702
+ }
703
+ }
704
+
705
+ async create(screen, theme) {
706
+ try {
707
+ // ... setup code
708
+ return this;
709
+ } catch (err) {
710
+ this.log('error', `Create failed: ${err.message}`);
711
+ throw err; // Re-throw to prevent partial initialization
712
+ }
713
+ }
714
+ ```
715
+
716
+ ### Lifecycle Events
717
+
718
+ Widgets can emit and listen to lifecycle events:
719
+
720
+ ```javascript
721
+ async init() {
722
+ // Listen to dashboard events
723
+ this.api.on('theme:changed', () => {
724
+ this.updateTheme();
725
+ });
726
+
727
+ // Emit custom events
728
+ this.emit('initialized', { timestamp: Date.now() });
729
+ }
730
+ ```
731
+
732
+ ## Configuration
733
+
734
+ Add to `~/.openclaw/dashboard-settings.json`:
735
+
736
+ ```json
737
+ {
738
+ "widgetLoading": {
739
+ "enabled": true,
740
+ "preloadPriority": ["cpu", "memory", "gpu"],
741
+ "lazyLoadDelay": 500,
742
+ "maxConcurrent": 3,
743
+ "autoDiscover": true
744
+ },
745
+ "plugins": {
746
+ "my-widget": {
747
+ "enabled": true,
748
+ "customSetting": "value"
749
+ }
750
+ }
751
+ }
752
+ ```
753
+
754
+ ### Widget Configuration Versioning (`__version`)
755
+
756
+ Widget configurations support versioning through the special `__version` field. This enables automatic migration when your widget's config schema changes.
757
+
758
+ #### How It Works
759
+
760
+ 1. **Declare version in `plugin.json`**:
761
+ ```json
762
+ {
763
+ "id": "my-widget",
764
+ "name": "My Widget",
765
+ "version": "2.0.0",
766
+ "config": {
767
+ "__version": "2.0.0",
768
+ "refreshInterval": 5000,
769
+ "theme": "dark"
770
+ }
771
+ }
772
+ ```
773
+
774
+ 2. **Create migration functions** for schema changes:
775
+ ```javascript
776
+ import { registerMigration } from 'claw-dashboard/widgets';
777
+
778
+ // Migrate from 1.x to 2.0.0
779
+ registerMigration('my-widget', '1.0.0', '2.0.0', (oldConfig) => {
780
+ return {
781
+ ...oldConfig,
782
+ theme: oldConfig.darkMode ? 'dark' : 'light',
783
+ __version: '2.0.0',
784
+ };
785
+ });
786
+ ```
787
+
788
+ 3. **Automatic migration** happens on load:
789
+ - The system compares stored config version with current version
790
+ - Runs registered migrations in sequence
791
+ - Preserves user customizations
792
+
793
+ #### Version Field Rules
794
+
795
+ - **`__version`** is reserved for internal use
796
+ - Stored as a string (semver format recommended)
797
+ - Automatically added if missing (defaults to `"1.0.0"`)
798
+ - Migrations run only when stored version < target version
799
+
800
+ #### Environment Variable Interpolation
801
+
802
+ Configuration values support environment variable substitution:
803
+
804
+ ```json
805
+ {
806
+ "config": {
807
+ "apiKey": "${API_KEY}",
808
+ "endpoint": "${API_ENDPOINT:-https://api.example.com}",
809
+ "timeout": "${TIMEOUT:-5000}"
810
+ }
811
+ }
812
+ ```
813
+
814
+ Syntax:
815
+ - `${VAR}` - Replace with environment variable value
816
+ - `${VAR:-default}` - Use default if variable not set
817
+ - Values are automatically coerced to appropriate types
818
+
819
+ #### Migration Example
820
+
821
+ ```javascript
822
+ import { registerMigration } from 'claw-dashboard/widgets';
823
+
824
+ // 1.0.0 -> 1.1.0: Add new field with default
825
+ registerMigration('my-widget', '1.0.0', '1.1.0', (config) => ({
826
+ ...config,
827
+ newField: 'default-value',
828
+ __version: '1.1.0',
829
+ }));
830
+
831
+ // 1.1.0 -> 2.0.0: Rename field
832
+ registerMigration('my-widget', '1.1.0', '2.0.0', (config) => {
833
+ const { oldField, ...rest } = config;
834
+ return {
835
+ ...rest,
836
+ renamedField: oldField,
837
+ __version: '2.0.0',
838
+ };
839
+ });
840
+ ```
841
+
842
+ ## Built-in Widgets
843
+
844
+ The following built-in widgets are available:
845
+
846
+ | Widget | ID | Priority | Lazy Load |
847
+ |--------|-----|----------|-----------|
848
+ | CPU | `cpu` | 10 | No |
849
+ | Memory | `memory` | 20 | No |
850
+ | GPU | `gpu` | 30 | No |
851
+ | Network | `network` | 40 | Yes |
852
+ | Disk | `disk` | 50 | Yes |
853
+ | System | `system` | 60 | Yes |
854
+ | Uptime | `uptime` | 70 | Yes |
855
+ | Data Health | `dataHealth` | 80 | Yes |
856
+
857
+ ## Widget Loader
858
+
859
+ For programmatic control:
860
+
861
+ ```javascript
862
+ import { getWidgetLoader } from 'claw-dashboard/widgets';
863
+
864
+ const loader = getWidgetLoader();
865
+
866
+ // Register a widget
867
+ loader.register('my-widget', metadata, async () => {
868
+ const { MyWidget } = await import('./my-widget.js');
869
+ return new MyWidget();
870
+ });
871
+
872
+ // Load on demand
873
+ const widget = await loader.load('my-widget');
874
+
875
+ // Get loading stats
876
+ const stats = loader.getStats();
877
+ console.log(stats);
878
+ // { total: 10, loaded: 3, failed: 0, loading: 0, averageLoadTime: 45.2 }
879
+ ```
880
+
881
+ ## Hooks
882
+
883
+ The widget loader supports lifecycle hooks:
884
+
885
+ ```javascript
886
+ loader.addHook('beforeLoad', (widget) => {
887
+ console.log(`Loading widget: ${widget.id}`);
888
+ });
889
+
890
+ loader.addHook('afterLoad', (widget) => {
891
+ console.log(`Widget ${widget.id} loaded in ${widget.loadTime}ms`);
892
+ });
893
+
894
+ loader.addHook('beforeUnload', (widget) => {
895
+ console.log(`Unloading widget: ${widget.id}`);
896
+ });
897
+ ```
898
+
899
+ ## Best Practices
900
+
901
+ 1. **Lazy Load**: Set `lazyLoad: true` for widgets that aren't immediately needed
902
+ 2. **Error Handling**: Always wrap async operations in try/catch
903
+ 3. **Cleanup**: Implement `destroy()` to clean up resources
904
+ 4. **Themes**: Respect the theme colors passed to `create()`
905
+ 5. **Testing**: Test widgets with the plugin system enabled and disabled
906
+
907
+ ## RateLimiter API
908
+
909
+ The `RateLimiter` class provides rate limiting functionality for plugins that need to control the frequency of operations like API calls, notifications, or alerts.
910
+
911
+ ### Importing
912
+
913
+ ```javascript
914
+ import { RateLimiter } from 'claw-dashboard/widgets';
915
+ ```
916
+
917
+ ### Constructor
918
+
919
+ ```javascript
920
+ const limiter = new RateLimiter(options);
921
+ ```
922
+
923
+ **Options:**
924
+ - `enabled` (boolean): Enable rate limiting (default: `true`)
925
+ - `windowMs` (number): Time window in milliseconds (default: `60000`)
926
+ - `maxAlerts` (number): Maximum operations allowed per window (default: `5`)
927
+ - `alwaysAllowCritical` (boolean): Always allow critical-level operations (default: `true`)
928
+
929
+ ### Methods
930
+
931
+ #### `check(type, level?)`
932
+
933
+ Check if an operation should be allowed without recording it.
934
+
935
+ **Parameters:**
936
+ - `type` (string): A category identifier (e.g., 'api', 'notification')
937
+ - `level` (string, optional): Severity level - 'warning', 'critical', 'info' (default: 'warning')
938
+
939
+ **Returns:** `{ allowed: boolean, reason: string }`
940
+
941
+ ```javascript
942
+ const result = limiter.check('api', 'warning');
943
+ if (result.allowed) {
944
+ // Proceed with operation
945
+ } else {
946
+ console.log(`Rate limited: ${result.reason}`);
947
+ }
948
+ ```
949
+
950
+ #### `record(type, level?)`
951
+
952
+ Record an operation occurrence (use after `check` if you need separate check/record).
953
+
954
+ ```javascript
955
+ limiter.record('api', 'warning');
956
+ ```
957
+
958
+ #### `checkAndRecord(type, level?)`
959
+
960
+ Atomic check-and-record operation (recommended for most use cases).
961
+
962
+ ```javascript
963
+ const result = limiter.checkAndRecord('notification', 'warning');
964
+ if (result.allowed) {
965
+ sendNotification();
966
+ }
967
+ ```
968
+
969
+ #### `getCount(type)`
970
+
971
+ Get the current count of operations in the window for a type.
972
+
973
+ ```javascript
974
+ const count = limiter.getCount('api');
975
+ console.log(`${count} API calls in current window`);
976
+ ```
977
+
978
+ #### `getRetryAfter(type)`
979
+
980
+ Get milliseconds until the next operation is allowed for a type.
981
+
982
+ ```javascript
983
+ const waitMs = limiter.getRetryAfter('api');
984
+ if (waitMs > 0) {
985
+ console.log(`Try again in ${waitMs}ms`);
986
+ }
987
+ ```
988
+
989
+ #### `getStatus()`
990
+
991
+ Get complete rate limiter status.
992
+
993
+ ```javascript
994
+ const status = limiter.getStatus();
995
+ // Returns:
996
+ // {
997
+ // enabled: boolean,
998
+ // windowMs: number,
999
+ // maxAlerts: number,
1000
+ // alwaysAllowCritical: boolean,
1001
+ // types: {
1002
+ // [type]: { current: number, max: number, retryAfter: number }
1003
+ // }
1004
+ // }
1005
+ ```
1006
+
1007
+ #### `configure(options)`
1008
+
1009
+ Update rate limiter configuration at runtime.
1010
+
1011
+ ```javascript
1012
+ limiter.configure({
1013
+ maxAlerts: 10,
1014
+ windowMs: 30000
1015
+ });
1016
+ ```
1017
+
1018
+ #### `reset()`
1019
+
1020
+ Clear all recorded timestamps and reset state.
1021
+
1022
+ ```javascript
1023
+ limiter.reset();
1024
+ ```
1025
+
1026
+ ### Example: Rate-Limited API Client
1027
+
1028
+ ```javascript
1029
+ import { RateLimiter } from 'claw-dashboard/widgets';
1030
+
1031
+ class RateLimitedApiClient {
1032
+ constructor() {
1033
+ this.limiter = new RateLimiter({
1034
+ windowMs: 60000, // 1 minute window
1035
+ maxAlerts: 10, // Max 10 calls per minute
1036
+ alwaysAllowCritical: false
1037
+ });
1038
+ }
1039
+
1040
+ async fetch(url, options = {}) {
1041
+ const level = options.critical ? 'critical' : 'warning';
1042
+ const result = this.limiter.checkAndRecord('api', level);
1043
+
1044
+ if (!result.allowed) {
1045
+ const waitMs = this.limiter.getRetryAfter('api');
1046
+ throw new Error(`Rate limited. Try again in ${waitMs}ms`);
1047
+ }
1048
+
1049
+ return fetch(url);
1050
+ }
1051
+
1052
+ getStatus() {
1053
+ return this.limiter.getStatus();
1054
+ }
1055
+ }
1056
+ ```
1057
+
1058
+ ### Example: Notification Throttling
1059
+
1060
+ ```javascript
1061
+ import { RateLimiter } from 'claw-dashboard/widgets';
1062
+
1063
+ const notificationLimiter = new RateLimiter({
1064
+ windowMs: 300000, // 5 minutes
1065
+ maxAlerts: 3, // Max 3 notifications per 5 minutes
1066
+ alwaysAllowCritical: true // Always allow critical notifications
1067
+ });
1068
+
1069
+ function notifyUser(message, level = 'warning') {
1070
+ const result = notificationLimiter.checkAndRecord('notification', level);
1071
+
1072
+ if (!result.allowed) {
1073
+ console.log('Notification throttled:', message);
1074
+ return false;
1075
+ }
1076
+
1077
+ // Send notification
1078
+ showNotification(message);
1079
+ return true;
1080
+ }
1081
+ ```
1082
+
1083
+ ## Example: System Metrics Chart
1084
+
1085
+ A complete example showing data visualization with blessed-contrib line charts:
1086
+
1087
+ ```javascript
1088
+ // ~/.openclaw/plugins/system-metrics-chart/index.js
1089
+ import { BaseWidget } from 'claw-dashboard/widgets';
1090
+
1091
+ export default class SystemMetricsChartWidget extends BaseWidget {
1092
+ constructor(options = {}) {
1093
+ super(options);
1094
+ this.name = 'System Metrics Chart';
1095
+ this.metricType = this.config.metricType || 'cpu';
1096
+ this.maxDataPoints = this.config.maxDataPoints || 30;
1097
+ this.dataHistory = { labels: [], values: [] };
1098
+ }
1099
+
1100
+ async create(screen, theme = {}) {
1101
+ const C = theme.colors || {};
1102
+ const blessed = await import('blessed');
1103
+ const contrib = await import('blessed-contrib');
1104
+
1105
+ // Main container
1106
+ this.box = blessed.default.box({
1107
+ parent: screen,
1108
+ width: '70%',
1109
+ height: 15,
1110
+ border: { type: 'line' },
1111
+ label: ` METRICS (${this.metricType.toUpperCase()}) `,
1112
+ style: { border: { fg: C.cyan || 'cyan' } },
1113
+ });
1114
+
1115
+ // Create blessed-contrib line chart
1116
+ this.chart = contrib.default.line({
1117
+ parent: this.box,
1118
+ width: '95%',
1119
+ height: 12,
1120
+ style: {
1121
+ line: C.green || 'green',
1122
+ text: C.white || 'white',
1123
+ baseline: C.gray || 'gray',
1124
+ },
1125
+ numYLabels: 5,
1126
+ showLegend: true,
1127
+ minY: 0,
1128
+ maxY: 100,
1129
+ });
1130
+
1131
+ this.loaded = true;
1132
+ return this;
1133
+ }
1134
+
1135
+ async getData() {
1136
+ const now = new Date();
1137
+ const label = now.toLocaleTimeString();
1138
+
1139
+ // Simulate metric value
1140
+ const value = Math.floor(Math.random() * 60) + 20;
1141
+
1142
+ // Update history
1143
+ this.dataHistory.labels.push(label);
1144
+ this.dataHistory.values.push(value);
1145
+
1146
+ // Trim to max data points
1147
+ if (this.dataHistory.labels.length > this.maxDataPoints) {
1148
+ this.dataHistory.labels.shift();
1149
+ this.dataHistory.values.shift();
1150
+ }
1151
+
1152
+ return {
1153
+ title: this.metricType.toUpperCase(),
1154
+ x: [...this.dataHistory.labels],
1155
+ y: [...this.dataHistory.values],
1156
+ };
1157
+ }
1158
+
1159
+ render(data) {
1160
+ if (!this.chart) return;
1161
+
1162
+ // Update chart with new data
1163
+ this.chart.setData([{
1164
+ title: data.title,
1165
+ x: data.x,
1166
+ y: data.y,
1167
+ style: { line: 'green' },
1168
+ }]);
1169
+ }
1170
+
1171
+ async destroy() {
1172
+ if (this.chart) this.chart = null;
1173
+ if (this.box) {
1174
+ this.box.destroy();
1175
+ this.box = null;
1176
+ }
1177
+ this.loaded = false;
1178
+ }
1179
+ }
1180
+ ```
1181
+
1182
+ ### Chart Configuration
1183
+
1184
+ The `blessed-contrib.line` chart accepts these options:
1185
+
1186
+ | Option | Type | Default | Description |
1187
+ |--------|------|---------|-------------|
1188
+ | `style.line` | `string` | `'yellow'` | Line color |
1189
+ | `style.text` | `string` | `'green'` | Text color |
1190
+ | `style.baseline` | `string` | `'black'` | Baseline color |
1191
+ | `numYLabels` | `number` | `5` | Number of Y-axis labels |
1192
+ | `showNthLabel` | `number` | `1` | Show every Nth X label |
1193
+ | `showLegend` | `boolean` | `false` | Show legend box |
1194
+ | `minY` | `number` | `0` | Minimum Y value |
1195
+ | `maxY` | `number` | auto | Maximum Y value |
1196
+ | `wholeNumbersOnly` | `boolean` | `false` | Round Y labels to integers |
1197
+
1198
+ ### Data Format for Line Charts
1199
+
1200
+ ```javascript
1201
+ // Single series
1202
+ const chartData = {
1203
+ title: 'CPU Usage',
1204
+ x: ['10:00', '10:01', '10:02', '10:03', '10:04'],
1205
+ y: [20, 35, 45, 30, 25],
1206
+ style: { line: 'green' },
1207
+ };
1208
+
1209
+ this.chart.setData([chartData]);
1210
+
1211
+ // Multiple series (for comparison)
1212
+ const multiSeriesData = [
1213
+ {
1214
+ title: 'CPU',
1215
+ x: timestamps,
1216
+ y: cpuValues,
1217
+ style: { line: 'green' },
1218
+ },
1219
+ {
1220
+ title: 'Memory',
1221
+ x: timestamps,
1222
+ y: memoryValues,
1223
+ style: { line: 'yellow' },
1224
+ },
1225
+ ];
1226
+
1227
+ this.chart.setData(multiSeriesData);
1228
+ ```
1229
+
1230
+ ## Example: Custom Weather Widget
1231
+
1232
+ ```javascript
1233
+ // ~/.openclaw/plugins/weather/plugin.json
1234
+ {
1235
+ "id": "weather",
1236
+ "name": "Weather",
1237
+ "description": "Display current weather",
1238
+ "version": "1.0.0",
1239
+ "author": "You",
1240
+ "category": "custom",
1241
+ "type": "widget",
1242
+ "lazyLoad": true,
1243
+ "config": {
1244
+ "location": "auto",
1245
+ "unit": "celsius"
1246
+ }
1247
+ }
1248
+
1249
+ // ~/.openclaw/plugins/weather/index.js
1250
+ import { BaseWidget } from 'claw-dashboard/widgets';
1251
+
1252
+ export default class WeatherWidget extends BaseWidget {
1253
+ async create(screen, theme) {
1254
+ const blessed = await import('blessed');
1255
+
1256
+ this.box = blessed.default.box({
1257
+ parent: screen,
1258
+ height: 5,
1259
+ border: { type: 'line' },
1260
+ label: ' WEATHER ',
1261
+ });
1262
+
1263
+ this.text = blessed.default.text({
1264
+ parent: this.box,
1265
+ top: 1,
1266
+ left: 'center',
1267
+ });
1268
+ }
1269
+
1270
+ async getData() {
1271
+ const config = this.api.getConfig('weather');
1272
+ // Fetch weather data
1273
+ return { temp: 22, condition: 'sunny' };
1274
+ }
1275
+
1276
+ render(data) {
1277
+ if (this.text) {
1278
+ this.text.setContent(`${data.temp}°C - ${data.condition}`);
1279
+ }
1280
+ }
1281
+ }
1282
+ ```
1283
+
1284
+ ## Migration Guide
1285
+
1286
+ ### From Built-in Widgets
1287
+
1288
+ If you have custom widgets built into the main codebase:
1289
+
1290
+ 1. Move widget code to a separate file in `src/widgets/`
1291
+ 2. Extend `BaseWidget` instead of creating blessed elements directly
1292
+ 3. Register the widget in the widget loader
1293
+ 4. Remove inline widget code from `index.js`
1294
+
1295
+ ### Breaking Changes
1296
+
1297
+ - Widgets must now extend `BaseWidget`
1298
+ - `create()` is now async
1299
+ - `getData()` receives a dataProvider function
1300
+ - `render()` receives data instead of accessing `this.data`
1301
+
1302
+ ## Troubleshooting
1303
+
1304
+ ### Widget not loading
1305
+ - Check plugin.json is valid JSON
1306
+ - Verify entry point (index.js) exists
1307
+ - Check for syntax errors in widget code
1308
+ - Enable debug logging: `clawdash --debug`
1309
+
1310
+ ### Widget renders blank
1311
+ - Ensure `create()` properly sets up blessed elements
1312
+ - Check that `render()` is being called
1313
+ - Verify data is being returned from `getData()`
1314
+
1315
+ ### Performance issues
1316
+ - Enable lazy loading: `"lazyLoad": true`
1317
+ - Reduce refresh intervals
1318
+ - Cache expensive calculations
1319
+
1320
+ ### Common Error Patterns
1321
+
1322
+ #### Error: "Cannot read property X of undefined"
1323
+ This usually means a blessed element hasn't been initialized. Always check if elements exist before accessing them:
1324
+
1325
+ ```javascript
1326
+ render(data) {
1327
+ // Safe access pattern
1328
+ if (!this.box || !this.content) return;
1329
+ this.content.setContent(data.value);
1330
+ }
1331
+ ```
1332
+
1333
+ #### Error: "Rate limit exceeded"
1334
+ You're making too many API calls. Use the built-in RateLimiter:
1335
+
1336
+ ```javascript
1337
+ async getData() {
1338
+ const limiter = this.api.getRateLimiter();
1339
+ const result = limiter.checkAndRecord('myWidget');
1340
+
1341
+ if (!result.allowed) {
1342
+ // Return cached data instead of making API call
1343
+ return this.cachedData;
1344
+ }
1345
+
1346
+ // Fetch new data
1347
+ const data = await this.fetchData();
1348
+ this.cachedData = data;
1349
+ return data;
1350
+ }
1351
+ ```
1352
+
1353
+ #### Widget crashes the dashboard
1354
+ Wrap your widget code in error boundaries:
1355
+
1356
+ ```javascript
1357
+ async getData() {
1358
+ try {
1359
+ // Your data fetching logic
1360
+ return await this.fetchData();
1361
+ } catch (err) {
1362
+ this.log('error', `Data fetch failed: ${err.message}`);
1363
+ return { error: err.message, _isError: true };
1364
+ }
1365
+ }
1366
+
1367
+ render(data) {
1368
+ // Always check for error state
1369
+ if (data?._isError) {
1370
+ this.errorText.setContent(`Error: ${data.error}`);
1371
+ this.errorText.show();
1372
+ return;
1373
+ }
1374
+
1375
+ // Normal rendering
1376
+ this.errorText.hide();
1377
+ this.content.setContent(data.value);
1378
+ }
1379
+ ```
1380
+
1381
+ ### Debug Mode
1382
+
1383
+ Enable debug logging to diagnose issues:
1384
+
1385
+ ```bash
1386
+ clawdash --debug
1387
+ ```
1388
+
1389
+ Debug output includes:
1390
+ - Widget loading/unloading events
1391
+ - Plugin registration
1392
+ - Data provider calls
1393
+ - Rate limit status
1394
+ - Configuration loading
1395
+
1396
+ ### Validation Errors
1397
+
1398
+ If you see validation errors in plugin.json:
1399
+
1400
+ | Error | Solution |
1401
+ |-------|----------|
1402
+ | `Missing required fields: id, name, version` | Add all required fields to manifest |
1403
+ | `Invalid version format` | Use semver format: "1.0.0" |
1404
+ | `Duplicate plugin id` | Choose a unique ID for your plugin |
1405
+ | `Invalid category` | Must be one of: system, monitoring, custom, example |
1406
+
1407
+ ### Common Error Solutions
1408
+
1409
+ #### Module not found errors
1410
+ If you see `Cannot find module 'some-module'`:
1411
+ ```javascript
1412
+ // Use dynamic import instead of static import for external modules
1413
+ async getData() {
1414
+ const axios = await import('axios');
1415
+ const response = await axios.default.get(this.config.apiUrl);
1416
+ return response.data;
1417
+ }
1418
+ ```
1419
+
1420
+ #### Rate limit exceeded
1421
+ If API calls are being throttled:
1422
+ ```javascript
1423
+ async getData() {
1424
+ const status = this.api.getRateLimitStatus();
1425
+ if (status.types.getData?.current >= status.types.getData?.max * 0.8) {
1426
+ // Use cached data or reduce refresh rate
1427
+ return this.cachedData;
1428
+ }
1429
+ // Proceed with API call
1430
+ }
1431
+ ```
1432
+
1433
+ #### Plugin crashes the dashboard
1434
+ Add error boundaries to your widget:
1435
+
1436
+ ```javascript
1437
+ import { BaseWidget } from 'claw-dashboard/widgets';
1438
+
1439
+ export default class SafeWidget extends BaseWidget {
1440
+ async getData() {
1441
+ try {
1442
+ // Risky operation
1443
+ return await fetchData();
1444
+ } catch (err) {
1445
+ this.log('error', `Data fetch failed: ${err.message}`);
1446
+ return { error: err.message, fallback: true };
1447
+ }
1448
+ }
1449
+
1450
+ render(data) {
1451
+ try {
1452
+ if (data.error) {
1453
+ this.showError(data.error);
1454
+ return;
1455
+ }
1456
+ // Normal rendering
1457
+ this.updateDisplay(data);
1458
+ } catch (err) {
1459
+ this.log('error', `Render failed: ${err.message}`);
1460
+ // Render error state safely
1461
+ this.showErrorState();
1462
+ }
1463
+ }
1464
+ }
1465
+ ```
1466
+
1467
+ ### Debug Mode
1468
+
1469
+ Enable detailed plugin logging:
1470
+ ```javascript
1471
+ // In your widget
1472
+ log(level, message) {
1473
+ // This uses the dashboard's logging system
1474
+ if (this.api) {
1475
+ this.api.log(this.id, level, message);
1476
+ }
1477
+ }
1478
+
1479
+ // Usage
1480
+ this.log('debug', 'Widget initializing');
1481
+ this.log('info', 'Data fetched successfully');
1482
+ this.log('warn', 'Using cached data');
1483
+ this.log('error', 'Failed to connect to API');
1484
+ ```
1485
+
1486
+ Run with debug mode:
1487
+ ```bash
1488
+ clawdash --debug
1489
+ ```
1490
+
1491
+ Debug output includes:
1492
+ - Widget loading/unloading events
1493
+ - Plugin API calls and rate limiting
1494
+ - Data provider invocations
1495
+ - Extension point executions
1496
+
1497
+ ## API Version
1498
+
1499
+ Current Plugin API Version: **1.0.0**
1500
+
1501
+ The API follows semantic versioning:
1502
+ - Major: Breaking changes
1503
+ - Minor: New features (backward compatible)
1504
+ - Patch: Bug fixes