claw-dashboard 1.9.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +5285 -566
- package/jest.config.js +11 -0
- package/man/clawdash.1 +276 -0
- package/package.json +54 -6
- 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 +1934 -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 +1056 -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
package/docs/PLUGINS.md
ADDED
|
@@ -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
|