claw-dashboard 2.1.0 → 2.2.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 (90) hide show
  1. package/CHANGELOG.md +29 -1
  2. package/docs/API.md +1 -2
  3. package/index.js +32 -42
  4. package/package.json +23 -12
  5. package/src/auto-save.js +11 -5
  6. package/src/cli/export-snapshot.js +3 -1
  7. package/src/cli/import-snapshot.js +3 -1
  8. package/src/config.js +9 -15
  9. package/src/errors.js +0 -9
  10. package/src/security.js +6 -2
  11. package/src/snapshot.js +73 -26
  12. package/src/web-server.js +7 -10
  13. package/src/widgets/builtin-widgets.js +0 -94
  14. package/src/widgets/index.js +0 -1
  15. package/.c8rc.json +0 -38
  16. package/.dockerignore +0 -68
  17. package/.github/dependabot.yml +0 -45
  18. package/.github/pull_request_template.md +0 -39
  19. package/.github/workflows/ci.yml +0 -147
  20. package/.github/workflows/release.yml +0 -40
  21. package/.github/workflows/security.yml +0 -84
  22. package/.husky/pre-commit +0 -1
  23. package/.planning/codebase/ARCHITECTURE.md +0 -206
  24. package/.planning/codebase/INTEGRATIONS.md +0 -150
  25. package/.planning/codebase/STACK.md +0 -122
  26. package/.planning/codebase/STRUCTURE.md +0 -201
  27. package/CONTRIBUTING.md +0 -378
  28. package/Dockerfile +0 -54
  29. package/FEATURES.md +0 -307
  30. package/TODO.md +0 -28
  31. package/ai.openclaw.dashboard.plist +0 -35
  32. package/build-cjs.js +0 -127
  33. package/cjs-shim.js +0 -13
  34. package/dist/clawdash +0 -1729
  35. package/dist/clawdash.meta.json +0 -2236
  36. package/dist/widgets.cjs +0 -6511
  37. package/docker-compose.yml +0 -67
  38. package/esbuild.config.js +0 -158
  39. package/eslint.config.js +0 -56
  40. package/examples/plugins/README.md +0 -122
  41. package/examples/plugins/api-status/index.js +0 -294
  42. package/examples/plugins/api-status/plugin.json +0 -19
  43. package/examples/plugins/hello-world/index.js +0 -104
  44. package/examples/plugins/hello-world/plugin.json +0 -15
  45. package/examples/plugins/system-metrics-chart/index.js +0 -339
  46. package/examples/plugins/system-metrics-chart/plugin.json +0 -18
  47. package/examples/plugins/weather-widget/index.js +0 -136
  48. package/examples/plugins/weather-widget/plugin.json +0 -19
  49. package/index.cjs +0 -23005
  50. package/jest.config.js +0 -11
  51. package/scripts/release.js +0 -595
  52. package/src/database.js +0 -734
  53. package/tests/alerts.test.js +0 -437
  54. package/tests/auto-save.test.js +0 -529
  55. package/tests/cache.test.js +0 -317
  56. package/tests/cli.test.js +0 -351
  57. package/tests/command-palette.test.js +0 -320
  58. package/tests/config-processor.test.js +0 -452
  59. package/tests/config-validator.test.js +0 -710
  60. package/tests/config-watcher.test.js +0 -594
  61. package/tests/config.test.js +0 -755
  62. package/tests/database.test.js +0 -438
  63. package/tests/errors.test.js +0 -189
  64. package/tests/example-plugins.test.js +0 -624
  65. package/tests/integration.test.js +0 -1321
  66. package/tests/loading-states.test.js +0 -300
  67. package/tests/manifest-validation-on-load.test.js +0 -671
  68. package/tests/performance-monitor.test.js +0 -190
  69. package/tests/plugin-api-rate-limit.test.js +0 -302
  70. package/tests/plugin-errors.test.js +0 -311
  71. package/tests/plugin-lifecycle-e2e.test.js +0 -1036
  72. package/tests/plugin-reload.test.js +0 -764
  73. package/tests/rate-limiter.test.js +0 -539
  74. package/tests/retry.test.js +0 -308
  75. package/tests/security.test.js +0 -411
  76. package/tests/settings-modal.test.js +0 -226
  77. package/tests/theme-selector.test.js +0 -57
  78. package/tests/utils.js +0 -242
  79. package/tests/utils.test.js +0 -317
  80. package/tests/validate-plugin-cli.test.js +0 -359
  81. package/tests/validation.test.js +0 -837
  82. package/tests/web-server.test.js +0 -646
  83. package/tests/widget-arrange-mode.test.js +0 -183
  84. package/tests/widget-config-hot-reload.test.js +0 -465
  85. package/tests/widget-dependency.test.js +0 -591
  86. package/tests/widget-error-boundary.test.js +0 -749
  87. package/tests/widget-error-isolation.test.js +0 -469
  88. package/tests/widget-integration.test.js +0 -1147
  89. package/tests/widget-refresh-intervals.test.js +0 -284
  90. package/tests/worker-pool.test.js +0 -522
@@ -1,469 +0,0 @@
1
- /**
2
- * Unit tests for widget error isolation
3
- */
4
-
5
- import {
6
- WidgetErrorIsolator,
7
- WidgetHealthTracker,
8
- WidgetIsolatedError,
9
- WidgetHealthStatus,
10
- WidgetErrorType,
11
- executeWithIsolation,
12
- getWidgetErrorIsolator,
13
- DEFAULT_ISOLATION_CONFIG,
14
- } from '../src/widgets/widget-error-isolation.js';
15
-
16
- // Helper to create a simple mock function
17
- function createMockFn(returnValue) {
18
- let callCount = 0;
19
- const fn = (...args) => {
20
- callCount++;
21
- fn.calls.push(args);
22
- return Promise.resolve(returnValue);
23
- };
24
- fn.calls = [];
25
- fn.mockClear = () => { fn.calls = []; callCount = 0; };
26
- fn.mockResolvedValue = (val) => {
27
- const newFn = createMockFn(val);
28
- return newFn;
29
- };
30
- fn.mockRejectedValue = (err) => {
31
- const newFn = (...args) => {
32
- callCount++;
33
- fn.calls.push(args);
34
- return Promise.reject(err);
35
- };
36
- newFn.calls = fn.calls;
37
- return newFn;
38
- };
39
- return fn;
40
- }
41
-
42
- describe('Widget Error Isolation', () => {
43
-
44
- describe('WidgetHealthTracker', () => {
45
- let tracker;
46
-
47
- beforeEach(() => {
48
- tracker = new WidgetHealthTracker();
49
- });
50
-
51
- describe('recordSuccess', () => {
52
- test('should mark widget as healthy on success', () => {
53
- tracker.recordSuccess('test-widget');
54
- const health = tracker.getHealth('test-widget');
55
-
56
- expect(health.status).toBe(WidgetHealthStatus.HEALTHY);
57
- expect(health.consecutiveErrors).toBe(0);
58
- expect(health.isHealthy).toBe(true);
59
- });
60
-
61
- test('should reset consecutive errors on success', () => {
62
- tracker.recordError('test-widget', new Error('Test error'));
63
- tracker.recordSuccess('test-widget');
64
- const health = tracker.getHealth('test-widget');
65
-
66
- expect(health.consecutiveErrors).toBe(0);
67
- });
68
- });
69
-
70
- describe('recordError', () => {
71
- test('should track consecutive errors', () => {
72
- tracker.recordError('test-widget', new Error('Error 1'));
73
- tracker.recordError('test-widget', new Error('Error 2'));
74
-
75
- const health = tracker.getHealth('test-widget');
76
- expect(health.consecutiveErrors).toBe(2);
77
- expect(health.totalErrors).toBe(2);
78
- });
79
-
80
- test('should mark widget as degraded after first error', () => {
81
- tracker.recordError('test-widget', new Error('Test error'));
82
- const health = tracker.getHealth('test-widget');
83
-
84
- expect(health.status).toBe(WidgetHealthStatus.DEGRADED);
85
- expect(health.isHealthy).toBe(false);
86
- expect(health.isOperational).toBe(true);
87
- });
88
-
89
- test('should mark widget as failed after max consecutive errors', () => {
90
- const maxErrors = DEFAULT_ISOLATION_CONFIG.maxConsecutiveErrors;
91
-
92
- for (let i = 0; i < maxErrors; i++) {
93
- tracker.recordError('test-widget', new Error(`Error ${i}`));
94
- }
95
-
96
- const health = tracker.getHealth('test-widget');
97
- expect(health.status).toBe(WidgetHealthStatus.FAILED);
98
- expect(health.isOperational).toBe(false);
99
- });
100
-
101
- test('should store error details', () => {
102
- const error = new Error('Test error message');
103
- error.stack = 'Test stack trace';
104
-
105
- tracker.recordError('test-widget', error, WidgetErrorType.RENDER_ERROR);
106
- const health = tracker.getHealth('test-widget');
107
-
108
- expect(health.lastError.message).toBe('Test error message');
109
- expect(health.lastError.type).toBe(WidgetErrorType.RENDER_ERROR);
110
- expect(health.lastError.stack).toBe('Test stack trace');
111
- expect(health.lastError.timestamp).toBeDefined();
112
- });
113
-
114
- test('should track error history within window', () => {
115
- tracker.recordError('test-widget', new Error('Error 1'));
116
- tracker.recordError('test-widget', new Error('Error 2'));
117
-
118
- const health = tracker.getHealth('test-widget');
119
- expect(health.recentErrorCount).toBe(2);
120
- });
121
- });
122
-
123
- describe('getHealth', () => {
124
- test('should return null for untracked widget', () => {
125
- const health = tracker.getHealth('unknown-widget');
126
- expect(health).toBeNull();
127
- });
128
-
129
- test('should include computed properties', () => {
130
- tracker.recordSuccess('test-widget');
131
- const health = tracker.getHealth('test-widget');
132
-
133
- expect(health).toHaveProperty('isHealthy');
134
- expect(health).toHaveProperty('isOperational');
135
- expect(health).toHaveProperty('recentErrorCount');
136
- });
137
- });
138
-
139
- describe('getAllHealth', () => {
140
- test('should return health for all tracked widgets', () => {
141
- tracker.recordSuccess('widget-1');
142
- tracker.recordSuccess('widget-2');
143
-
144
- const allHealth = tracker.getAllHealth();
145
- expect(Object.keys(allHealth)).toHaveLength(2);
146
- expect(allHealth['widget-1']).toBeDefined();
147
- expect(allHealth['widget-2']).toBeDefined();
148
- });
149
- });
150
-
151
- describe('canRecover', () => {
152
- test('should allow recovery for degraded widget', () => {
153
- tracker.recordError('test-widget', new Error('Error'));
154
- expect(tracker.canRecover('test-widget')).toBe(true);
155
- });
156
-
157
- test('should allow recovery for failed widget after delay', () => {
158
- tracker.config.recoveryDelayMs = 1000;
159
- tracker.recordError('test-widget', new Error('Error 1'));
160
- tracker.recordError('test-widget', new Error('Error 2'));
161
- tracker.recordError('test-widget', new Error('Error 3'));
162
-
163
- // Initially should not allow recovery
164
- expect(tracker.canRecover('test-widget')).toBe(false);
165
-
166
- // After waiting, should allow recovery
167
- tracker.healthStatus.get('test-widget').failedSince = Date.now() - 2000;
168
- expect(tracker.canRecover('test-widget')).toBe(true);
169
- });
170
-
171
- test('should not allow recovery when autoRecover is disabled', () => {
172
- tracker.config.autoRecover = false;
173
- tracker.recordError('test-widget', new Error('Error'));
174
- expect(tracker.canRecover('test-widget')).toBe(false);
175
- });
176
-
177
- test('should not allow recovery after max attempts', () => {
178
- tracker.config.maxRecoveryAttempts = 2;
179
-
180
- tracker.markRecovering('test-widget');
181
- tracker.markRecovering('test-widget');
182
- tracker.markRecovering('test-widget');
183
-
184
- expect(tracker.canRecover('test-widget')).toBe(false);
185
- });
186
- });
187
-
188
- describe('resetHealth', () => {
189
- test('should clear health record', () => {
190
- tracker.recordError('test-widget', new Error('Error'));
191
- tracker.resetHealth('test-widget');
192
-
193
- const health = tracker.getHealth('test-widget');
194
- expect(health).toBeNull();
195
- });
196
- });
197
-
198
- describe('getStats', () => {
199
- test('should return summary statistics', () => {
200
- tracker.recordSuccess('widget-1');
201
- tracker.recordSuccess('widget-2');
202
- tracker.recordError('widget-2', new Error('Error'));
203
- tracker.recordError('widget-3', new Error('Error 1'));
204
- tracker.recordError('widget-3', new Error('Error 2'));
205
- tracker.recordError('widget-3', new Error('Error 3'));
206
-
207
- const stats = tracker.getStats();
208
- expect(stats.total).toBe(3);
209
- expect(stats.healthy).toBe(1);
210
- expect(stats.degraded).toBe(1);
211
- expect(stats.failed).toBe(1);
212
- expect(stats.totalErrors).toBe(4);
213
- });
214
- });
215
- });
216
-
217
- describe('WidgetErrorIsolator', () => {
218
- let isolator;
219
-
220
- beforeEach(() => {
221
- isolator = new WidgetErrorIsolator({
222
- failSilently: true,
223
- logErrors: false,
224
- });
225
- });
226
-
227
- afterEach(() => {
228
- isolator.shutdown();
229
- });
230
-
231
- describe('wrapInit', () => {
232
- test('should wrap successful init', async () => {
233
- const initFn = () => Promise.resolve({ initialized: true });
234
- const result = await isolator.wrapInit('test-widget', initFn);
235
-
236
- expect(result).toEqual({ initialized: true });
237
- });
238
-
239
- test('should catch init errors when failSilently is true', async () => {
240
- const initFn = () => Promise.reject(new Error('Init failed'));
241
- const result = await isolator.wrapInit('test-widget', initFn);
242
-
243
- expect(result).toBeNull();
244
- });
245
-
246
- test('should throw when failSilently is false', async () => {
247
- isolator.config.failSilently = false;
248
- const initFn = () => Promise.reject(new Error('Init failed'));
249
-
250
- await expect(isolator.wrapInit('test-widget', initFn)).rejects.toThrow(WidgetIsolatedError);
251
- });
252
-
253
- test('should mark widget as healthy after successful init', async () => {
254
- const initFn = () => Promise.resolve({ initialized: true });
255
- await isolator.wrapInit('test-widget', initFn);
256
-
257
- const health = isolator.getHealth('test-widget');
258
- expect(health.status).toBe(WidgetHealthStatus.HEALTHY);
259
- });
260
- });
261
-
262
- describe('wrapCreate', () => {
263
- test('should wrap successful create', async () => {
264
- const createFn = () => Promise.resolve({ created: true });
265
- const result = await isolator.wrapCreate('test-widget', createFn);
266
-
267
- expect(result).toEqual({ created: true });
268
- });
269
-
270
- test('should catch create errors', async () => {
271
- const createFn = () => Promise.reject(new Error('Create failed'));
272
- const result = await isolator.wrapCreate('test-widget', createFn);
273
-
274
- expect(result).toBeNull();
275
- });
276
- });
277
-
278
- describe('wrapGetData', () => {
279
- test('should wrap successful data fetch', async () => {
280
- const dataFn = () => Promise.resolve({ data: 'test' });
281
- const result = await isolator.wrapGetData('test-widget', dataFn);
282
-
283
- expect(result).toEqual({ data: 'test' });
284
- });
285
-
286
- test('should catch data fetch errors', async () => {
287
- const dataFn = () => Promise.reject(new Error('Data fetch failed'));
288
- const result = await isolator.wrapGetData('test-widget', dataFn);
289
-
290
- expect(result).toBeNull();
291
- });
292
- });
293
-
294
- describe('wrapRender', () => {
295
- test('should wrap successful render', async () => {
296
- const renderFn = () => Promise.resolve(undefined);
297
- const result = await isolator.wrapRender('test-widget', renderFn);
298
-
299
- expect(result).toBeUndefined();
300
- });
301
-
302
- test('should catch render errors', async () => {
303
- const renderFn = () => Promise.reject(new Error('Render failed'));
304
- const result = await isolator.wrapRender('test-widget', renderFn);
305
-
306
- expect(result).toBeNull();
307
- });
308
- });
309
-
310
- describe('wrapDestroy', () => {
311
- test('should wrap successful destroy', async () => {
312
- const destroyFn = () => Promise.resolve(undefined);
313
- const result = await isolator.wrapDestroy('test-widget', destroyFn);
314
-
315
- expect(result).toBeUndefined();
316
- });
317
-
318
- test('should catch destroy errors', async () => {
319
- const destroyFn = () => Promise.reject(new Error('Destroy failed'));
320
- const result = await isolator.wrapDestroy('test-widget', destroyFn);
321
-
322
- expect(result).toBeNull();
323
- });
324
- });
325
-
326
- describe('timeout handling', () => {
327
- test('should timeout slow operations', async () => {
328
- isolator.config.initTimeoutMs = 50;
329
-
330
- const slowFn = () =>
331
- new Promise((resolve) => setTimeout(() => resolve('done'), 200));
332
-
333
- const startTime = Date.now();
334
- const result = await isolator.wrapInit('test-widget', slowFn);
335
- const elapsed = Date.now() - startTime;
336
-
337
- expect(result).toBeNull();
338
- expect(elapsed).toBeLessThan(150); // Should timeout before 200ms
339
- });
340
- });
341
-
342
- describe('isOperational', () => {
343
- test('should return true for new widgets', () => {
344
- expect(isolator.isOperational('new-widget')).toBe(true);
345
- });
346
-
347
- test('should return false for failed widgets', async () => {
348
- const errorFn = () => Promise.reject(new Error('Error'));
349
-
350
- // Trigger failures
351
- for (let i = 0; i < DEFAULT_ISOLATION_CONFIG.maxConsecutiveErrors; i++) {
352
- await isolator.wrapRender('test-widget', errorFn);
353
- }
354
-
355
- expect(isolator.isOperational('test-widget')).toBe(false);
356
- });
357
- });
358
-
359
- describe('resetWidget', () => {
360
- test('should reset widget health', async () => {
361
- const errorFn = () => Promise.reject(new Error('Error'));
362
- await isolator.wrapRender('test-widget', errorFn);
363
-
364
- isolator.resetWidget('test-widget');
365
-
366
- const health = isolator.getHealth('test-widget');
367
- expect(health).toBeNull();
368
- expect(isolator.isOperational('test-widget')).toBe(true);
369
- });
370
- });
371
-
372
- describe('getStats', () => {
373
- test('should return isolator statistics', () => {
374
- const stats = isolator.getStats();
375
-
376
- expect(stats).toHaveProperty('total');
377
- expect(stats).toHaveProperty('healthy');
378
- expect(stats).toHaveProperty('degraded');
379
- expect(stats).toHaveProperty('failed');
380
- expect(stats).toHaveProperty('failedWidgetCount');
381
- expect(stats).toHaveProperty('pendingRecoveries');
382
- });
383
- });
384
- });
385
-
386
- describe('WidgetIsolatedError', () => {
387
- test('should create error with correct properties', () => {
388
- const originalError = new Error('Original error');
389
- const error = new WidgetIsolatedError(
390
- 'test-widget',
391
- 'render',
392
- originalError,
393
- WidgetErrorType.RENDER_ERROR
394
- );
395
-
396
- expect(error.widgetId).toBe('test-widget');
397
- expect(error.operation).toBe('render');
398
- expect(error.errorType).toBe(WidgetErrorType.RENDER_ERROR);
399
- expect(error.originalError).toBe(originalError);
400
- expect(error.message).toContain('test-widget');
401
- expect(error.message).toContain('render');
402
- expect(error.code).toBe('WIDGET_ISOLATED_ERROR');
403
- });
404
-
405
- test('should handle null original error', () => {
406
- const error = new WidgetIsolatedError('test-widget', 'init', null);
407
-
408
- expect(error.originalError).toBeNull();
409
- expect(error.message).toContain('Unknown error');
410
- });
411
- });
412
-
413
- describe('executeWithIsolation', () => {
414
- test('should execute multiple operations with isolation', async () => {
415
- const operations = [
416
- {
417
- widgetId: 'widget-1',
418
- operation: () => Promise.resolve('result-1'),
419
- },
420
- {
421
- widgetId: 'widget-2',
422
- operation: () => Promise.reject(new Error('Failed')),
423
- },
424
- {
425
- widgetId: 'widget-3',
426
- operation: () => Promise.resolve('result-3'),
427
- },
428
- ];
429
-
430
- const results = await executeWithIsolation(operations, { failSilently: true });
431
-
432
- expect(results).toHaveLength(3);
433
- expect(results[0].success).toBe(true);
434
- expect(results[0].result).toBe('result-1');
435
- expect(results[1].success).toBe(false);
436
- expect(results[1].error).toBeDefined();
437
- expect(results[2].success).toBe(true);
438
- expect(results[2].result).toBe('result-3');
439
- });
440
-
441
- test('should allow partial success', async () => {
442
- const operations = [
443
- { widgetId: 'widget-1', operation: () => Promise.resolve('ok') },
444
- { widgetId: 'widget-2', operation: () => Promise.reject(new Error('fail')) },
445
- ];
446
-
447
- const results = await executeWithIsolation(operations);
448
-
449
- expect(results[0].success).toBe(true);
450
- expect(results[1].success).toBe(false);
451
- });
452
- });
453
-
454
- describe('getWidgetErrorIsolator', () => {
455
- test('should return singleton instance', () => {
456
- const isolator1 = getWidgetErrorIsolator();
457
- const isolator2 = getWidgetErrorIsolator();
458
-
459
- expect(isolator1).toBe(isolator2);
460
- });
461
-
462
- test('should create instance if not exists', () => {
463
- const isolator = getWidgetErrorIsolator({ failSilently: false });
464
-
465
- expect(isolator).toBeInstanceOf(WidgetErrorIsolator);
466
- // Note: singleton may retain previous config
467
- });
468
- });
469
- });