claw-dashboard 2.1.1 → 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 (89) hide show
  1. package/CHANGELOG.md +29 -1
  2. package/docs/API.md +1 -2
  3. package/index.js +31 -41
  4. package/package.json +22 -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/.c8rc.json +0 -38
  14. package/.dockerignore +0 -68
  15. package/.github/dependabot.yml +0 -45
  16. package/.github/pull_request_template.md +0 -39
  17. package/.github/workflows/ci.yml +0 -147
  18. package/.github/workflows/release.yml +0 -40
  19. package/.github/workflows/security.yml +0 -84
  20. package/.husky/pre-commit +0 -1
  21. package/.planning/codebase/ARCHITECTURE.md +0 -206
  22. package/.planning/codebase/INTEGRATIONS.md +0 -150
  23. package/.planning/codebase/STACK.md +0 -122
  24. package/.planning/codebase/STRUCTURE.md +0 -201
  25. package/CONTRIBUTING.md +0 -378
  26. package/Dockerfile +0 -54
  27. package/FEATURES.md +0 -307
  28. package/TODO.md +0 -28
  29. package/ai.openclaw.dashboard.plist +0 -35
  30. package/build-cjs.js +0 -127
  31. package/cjs-shim.js +0 -13
  32. package/dist/clawdash +0 -1729
  33. package/dist/clawdash.meta.json +0 -2236
  34. package/dist/widgets.cjs +0 -6511
  35. package/docker-compose.yml +0 -67
  36. package/esbuild.config.js +0 -158
  37. package/eslint.config.js +0 -56
  38. package/examples/plugins/README.md +0 -122
  39. package/examples/plugins/api-status/index.js +0 -294
  40. package/examples/plugins/api-status/plugin.json +0 -19
  41. package/examples/plugins/hello-world/index.js +0 -104
  42. package/examples/plugins/hello-world/plugin.json +0 -15
  43. package/examples/plugins/system-metrics-chart/index.js +0 -339
  44. package/examples/plugins/system-metrics-chart/plugin.json +0 -18
  45. package/examples/plugins/weather-widget/index.js +0 -136
  46. package/examples/plugins/weather-widget/plugin.json +0 -19
  47. package/index.cjs +0 -23005
  48. package/jest.config.js +0 -11
  49. package/scripts/release.js +0 -595
  50. package/src/database.js +0 -734
  51. package/tests/alerts.test.js +0 -437
  52. package/tests/auto-save.test.js +0 -529
  53. package/tests/cache.test.js +0 -317
  54. package/tests/cli.test.js +0 -351
  55. package/tests/command-palette.test.js +0 -320
  56. package/tests/config-processor.test.js +0 -452
  57. package/tests/config-validator.test.js +0 -710
  58. package/tests/config-watcher.test.js +0 -594
  59. package/tests/config.test.js +0 -755
  60. package/tests/database.test.js +0 -438
  61. package/tests/errors.test.js +0 -189
  62. package/tests/example-plugins.test.js +0 -624
  63. package/tests/integration.test.js +0 -1321
  64. package/tests/loading-states.test.js +0 -300
  65. package/tests/manifest-validation-on-load.test.js +0 -671
  66. package/tests/performance-monitor.test.js +0 -190
  67. package/tests/plugin-api-rate-limit.test.js +0 -302
  68. package/tests/plugin-errors.test.js +0 -311
  69. package/tests/plugin-lifecycle-e2e.test.js +0 -1036
  70. package/tests/plugin-reload.test.js +0 -764
  71. package/tests/rate-limiter.test.js +0 -539
  72. package/tests/removed-dependencies.test.js +0 -74
  73. package/tests/retry.test.js +0 -308
  74. package/tests/security.test.js +0 -411
  75. package/tests/settings-modal.test.js +0 -226
  76. package/tests/theme-selector.test.js +0 -57
  77. package/tests/utils.js +0 -242
  78. package/tests/utils.test.js +0 -317
  79. package/tests/validate-plugin-cli.test.js +0 -359
  80. package/tests/validation.test.js +0 -837
  81. package/tests/web-server.test.js +0 -646
  82. package/tests/widget-arrange-mode.test.js +0 -183
  83. package/tests/widget-config-hot-reload.test.js +0 -465
  84. package/tests/widget-dependency.test.js +0 -591
  85. package/tests/widget-error-boundary.test.js +0 -749
  86. package/tests/widget-error-isolation.test.js +0 -469
  87. package/tests/widget-integration.test.js +0 -1147
  88. package/tests/widget-refresh-intervals.test.js +0 -284
  89. 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
- });