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,539 +0,0 @@
1
- /**
2
- * Unit tests for RateLimiter class
3
- */
4
-
5
- import alerts from '../src/alerts.js';
6
-
7
- const { RateLimiter } = alerts;
8
-
9
- describe('RateLimiter', () => {
10
- beforeEach(() => {
11
- alerts.resetRateLimit();
12
- });
13
-
14
- afterEach(() => {
15
- alerts.resetRateLimit();
16
- });
17
-
18
- describe('constructor', () => {
19
- test('should create RateLimiter with default options', () => {
20
- const limiter = new RateLimiter();
21
-
22
- expect(limiter.enabled).toBe(true);
23
- expect(limiter.windowMs).toBe(60000);
24
- expect(limiter.maxAlerts).toBe(5);
25
- expect(limiter.alwaysAllowCritical).toBe(true);
26
- expect(limiter.timestamps).toEqual({});
27
- });
28
-
29
- test('should create RateLimiter with custom options', () => {
30
- const limiter = new RateLimiter({
31
- enabled: false,
32
- windowMs: 30000,
33
- maxAlerts: 3,
34
- alwaysAllowCritical: false
35
- });
36
-
37
- expect(limiter.enabled).toBe(false);
38
- expect(limiter.windowMs).toBe(30000);
39
- expect(limiter.maxAlerts).toBe(3);
40
- expect(limiter.alwaysAllowCritical).toBe(false);
41
- });
42
-
43
- test('should partially apply custom options with defaults for others', () => {
44
- const limiter = new RateLimiter({
45
- maxAlerts: 10
46
- });
47
-
48
- expect(limiter.enabled).toBe(true); // default
49
- expect(limiter.windowMs).toBe(60000); // default
50
- expect(limiter.maxAlerts).toBe(10); // custom
51
- expect(limiter.alwaysAllowCritical).toBe(true); // default
52
- });
53
- });
54
-
55
- describe('check', () => {
56
- test('should allow all alerts when disabled', () => {
57
- const limiter = new RateLimiter({ enabled: false });
58
-
59
- for (let i = 0; i < 10; i++) {
60
- const result = limiter.check('cpu', 'warning');
61
- expect(result.allowed).toBe(true);
62
- expect(result.reason).toBe('rate_limiting_disabled');
63
- }
64
- });
65
-
66
- test('should allow alert when under limit', () => {
67
- const limiter = new RateLimiter({ maxAlerts: 3 });
68
-
69
- const result = limiter.check('cpu', 'warning');
70
- expect(result.allowed).toBe(true);
71
- expect(result.reason).toBe('ok');
72
- });
73
-
74
- test('should block alert when over limit', () => {
75
- const limiter = new RateLimiter({ maxAlerts: 2 });
76
-
77
- // Record two alerts to reach limit
78
- limiter.record('cpu', 'warning');
79
- limiter.record('cpu', 'warning');
80
-
81
- const result = limiter.check('cpu', 'warning');
82
- expect(result.allowed).toBe(false);
83
- expect(result.reason).toBe('rate_limit_exceeded');
84
- });
85
-
86
- test('should always allow critical when alwaysAllowCritical is true', () => {
87
- const limiter = new RateLimiter({
88
- maxAlerts: 1,
89
- alwaysAllowCritical: true
90
- });
91
-
92
- // Record one alert to reach limit
93
- limiter.record('cpu', 'warning');
94
-
95
- // Warning should be blocked
96
- const warningResult = limiter.check('cpu', 'warning');
97
- expect(warningResult.allowed).toBe(false);
98
-
99
- // Critical should be allowed
100
- const criticalResult = limiter.check('cpu', 'critical');
101
- expect(criticalResult.allowed).toBe(true);
102
- expect(criticalResult.reason).toBe('critical_always_allowed');
103
- });
104
-
105
- test('should block critical when alwaysAllowCritical is false', () => {
106
- const limiter = new RateLimiter({
107
- maxAlerts: 1,
108
- alwaysAllowCritical: false
109
- });
110
-
111
- // Record one alert to reach limit
112
- limiter.record('cpu', 'critical');
113
-
114
- // Another critical should be blocked
115
- const result = limiter.check('cpu', 'critical');
116
- expect(result.allowed).toBe(false);
117
- expect(result.reason).toBe('rate_limit_exceeded');
118
- });
119
-
120
- test('should track different alert types separately', () => {
121
- const limiter = new RateLimiter({ maxAlerts: 1 });
122
-
123
- // Fill up cpu
124
- limiter.record('cpu', 'warning');
125
- expect(limiter.check('cpu', 'warning').allowed).toBe(false);
126
-
127
- // Memory should still be allowed
128
- expect(limiter.check('memory', 'warning').allowed).toBe(true);
129
- });
130
-
131
- test('should default to warning level when not specified', () => {
132
- const limiter = new RateLimiter({ maxAlerts: 1 });
133
-
134
- limiter.record('cpu', 'warning');
135
-
136
- // Check without level should use warning
137
- const result = limiter.check('cpu');
138
- expect(result.allowed).toBe(false);
139
- });
140
- });
141
-
142
- describe('checkAndRecord', () => {
143
- test('should check and record when allowed', () => {
144
- const limiter = new RateLimiter({ maxAlerts: 2 });
145
-
146
- const result = limiter.checkAndRecord('cpu', 'warning');
147
- expect(result.allowed).toBe(true);
148
- expect(result.reason).toBe('ok');
149
-
150
- // Verify it was recorded
151
- expect(limiter.getCount('cpu')).toBe(1);
152
- });
153
-
154
- test('should check but not record when blocked', () => {
155
- const limiter = new RateLimiter({ maxAlerts: 1 });
156
-
157
- // First alert
158
- limiter.checkAndRecord('cpu', 'warning');
159
- expect(limiter.getCount('cpu')).toBe(1);
160
-
161
- // Second should be blocked and NOT recorded
162
- const result = limiter.checkAndRecord('cpu', 'warning');
163
- expect(result.allowed).toBe(false);
164
- expect(result.reason).toBe('rate_limit_exceeded');
165
- expect(limiter.getCount('cpu')).toBe(1); // Still 1, not incremented
166
- });
167
-
168
- test('should allow critical even when rate limited and record it', () => {
169
- const limiter = new RateLimiter({ maxAlerts: 1 });
170
-
171
- // Fill up with warning
172
- limiter.checkAndRecord('cpu', 'warning');
173
-
174
- // Critical should pass and be recorded
175
- const result = limiter.checkAndRecord('cpu', 'critical');
176
- expect(result.allowed).toBe(true);
177
- expect(result.reason).toBe('critical_always_allowed');
178
- expect(limiter.getCount('cpu')).toBe(2); // Both recorded
179
- });
180
-
181
- test('should not record when rate limiting disabled', () => {
182
- const limiter = new RateLimiter({ enabled: false });
183
-
184
- limiter.checkAndRecord('cpu', 'warning');
185
- limiter.checkAndRecord('cpu', 'warning');
186
-
187
- // Should not have recorded anything
188
- expect(limiter.getCount('cpu')).toBe(0);
189
- });
190
- });
191
-
192
- describe('record', () => {
193
- test('should record alert timestamp', () => {
194
- const limiter = new RateLimiter();
195
-
196
- limiter.record('cpu', 'warning');
197
- expect(limiter.getCount('cpu')).toBe(1);
198
-
199
- limiter.record('cpu', 'warning');
200
- expect(limiter.getCount('cpu')).toBe(2);
201
- });
202
-
203
- test('should not record when disabled', () => {
204
- const limiter = new RateLimiter({ enabled: false });
205
-
206
- limiter.record('cpu', 'warning');
207
- expect(limiter.getCount('cpu')).toBe(0);
208
- });
209
-
210
- test('should record multiple types independently', () => {
211
- const limiter = new RateLimiter();
212
-
213
- limiter.record('cpu', 'warning');
214
- limiter.record('cpu', 'warning');
215
- limiter.record('memory', 'warning');
216
-
217
- expect(limiter.getCount('cpu')).toBe(2);
218
- expect(limiter.getCount('memory')).toBe(1);
219
- expect(limiter.getCount('disk')).toBe(0);
220
- });
221
- });
222
-
223
- describe('getCount', () => {
224
- test('should return 0 when no alerts recorded', () => {
225
- const limiter = new RateLimiter();
226
- expect(limiter.getCount('cpu')).toBe(0);
227
- });
228
-
229
- test('should return count of alerts in current window', () => {
230
- const limiter = new RateLimiter({ windowMs: 60000 });
231
-
232
- limiter.record('cpu', 'warning');
233
- limiter.record('cpu', 'warning');
234
- limiter.record('cpu', 'warning');
235
-
236
- expect(limiter.getCount('cpu')).toBe(3);
237
- });
238
-
239
- test('should exclude expired timestamps', async () => {
240
- const limiter = new RateLimiter({ windowMs: 50 });
241
-
242
- limiter.record('cpu', 'warning');
243
- limiter.record('cpu', 'warning');
244
- expect(limiter.getCount('cpu')).toBe(2);
245
-
246
- // Wait for window to expire
247
- await new Promise(resolve => setTimeout(resolve, 60));
248
-
249
- // Old timestamps should be expired
250
- expect(limiter.getCount('cpu')).toBe(0);
251
- });
252
- });
253
-
254
- describe('getRetryAfter', () => {
255
- test('should return 0 when no alerts recorded', () => {
256
- const limiter = new RateLimiter();
257
- expect(limiter.getRetryAfter('cpu')).toBe(0);
258
- });
259
-
260
- test('should return 0 when under limit', () => {
261
- const limiter = new RateLimiter({ maxAlerts: 3 });
262
-
263
- limiter.record('cpu', 'warning');
264
- limiter.record('cpu', 'warning');
265
-
266
- expect(limiter.getRetryAfter('cpu')).toBe(0);
267
- });
268
-
269
- test('should return time until oldest expires when at limit', async () => {
270
- const windowMs = 100;
271
- const limiter = new RateLimiter({ maxAlerts: 2, windowMs });
272
-
273
- // Record two alerts at limit
274
- limiter.record('cpu', 'warning');
275
- await new Promise(resolve => setTimeout(resolve, 20));
276
- limiter.record('cpu', 'warning');
277
-
278
- // At limit, should return time until first expires
279
- const retryAfter = limiter.getRetryAfter('cpu');
280
- expect(retryAfter).toBeGreaterThan(0);
281
- expect(retryAfter).toBeLessThanOrEqual(windowMs);
282
- });
283
-
284
- test('should return 0 after window expires', async () => {
285
- const limiter = new RateLimiter({ maxAlerts: 1, windowMs: 50 });
286
-
287
- limiter.record('cpu', 'warning');
288
- expect(limiter.getRetryAfter('cpu')).toBeGreaterThan(0);
289
-
290
- // Wait for window to expire
291
- await new Promise(resolve => setTimeout(resolve, 60));
292
-
293
- expect(limiter.getRetryAfter('cpu')).toBe(0);
294
- });
295
-
296
- test('should handle different types independently', () => {
297
- const limiter = new RateLimiter({ maxAlerts: 1 });
298
-
299
- limiter.record('cpu', 'warning');
300
-
301
- expect(limiter.getRetryAfter('cpu')).toBeGreaterThan(0);
302
- expect(limiter.getRetryAfter('memory')).toBe(0);
303
- });
304
- });
305
-
306
- describe('getStatus', () => {
307
- test('should return full status object', () => {
308
- const limiter = new RateLimiter({
309
- enabled: true,
310
- windowMs: 30000,
311
- maxAlerts: 3,
312
- alwaysAllowCritical: false
313
- });
314
-
315
- const status = limiter.getStatus();
316
-
317
- expect(status.enabled).toBe(true);
318
- expect(status.windowMs).toBe(30000);
319
- expect(status.maxAlerts).toBe(3);
320
- expect(status.alwaysAllowCritical).toBe(false);
321
- expect(status.types).toEqual({});
322
- });
323
-
324
- test('should include type status for recorded alerts', () => {
325
- const limiter = new RateLimiter({ maxAlerts: 5 });
326
-
327
- limiter.record('cpu', 'warning');
328
- limiter.record('cpu', 'warning');
329
- limiter.record('memory', 'warning');
330
-
331
- const status = limiter.getStatus();
332
-
333
- expect(status.types.cpu).toEqual({
334
- current: 2,
335
- max: 5,
336
- retryAfter: 0
337
- });
338
-
339
- expect(status.types.memory).toEqual({
340
- current: 1,
341
- max: 5,
342
- retryAfter: 0
343
- });
344
-
345
- expect(status.types.disk).toBeUndefined();
346
- });
347
-
348
- test('should include retryAfter when at limit', () => {
349
- const limiter = new RateLimiter({ maxAlerts: 2 });
350
-
351
- limiter.record('cpu', 'warning');
352
- limiter.record('cpu', 'warning');
353
-
354
- const status = limiter.getStatus();
355
-
356
- expect(status.types.cpu.current).toBe(2);
357
- expect(status.types.cpu.retryAfter).toBeGreaterThan(0);
358
- });
359
-
360
- test('should exclude expired timestamps from status', async () => {
361
- const limiter = new RateLimiter({ maxAlerts: 2, windowMs: 50 });
362
-
363
- limiter.record('cpu', 'warning');
364
- limiter.record('cpu', 'warning');
365
-
366
- await new Promise(resolve => setTimeout(resolve, 60));
367
-
368
- const status = limiter.getStatus();
369
- expect(status.types.cpu.current).toBe(0);
370
- expect(status.types.cpu.retryAfter).toBe(0);
371
- });
372
- });
373
-
374
- describe('configure', () => {
375
- test('should update configuration', () => {
376
- const limiter = new RateLimiter();
377
-
378
- limiter.configure({
379
- enabled: false,
380
- windowMs: 30000,
381
- maxAlerts: 10,
382
- alwaysAllowCritical: false
383
- });
384
-
385
- expect(limiter.enabled).toBe(false);
386
- expect(limiter.windowMs).toBe(30000);
387
- expect(limiter.maxAlerts).toBe(10);
388
- expect(limiter.alwaysAllowCritical).toBe(false);
389
- });
390
-
391
- test('should partially update configuration', () => {
392
- const limiter = new RateLimiter();
393
-
394
- limiter.configure({ maxAlerts: 20 });
395
-
396
- expect(limiter.enabled).toBe(true); // unchanged
397
- expect(limiter.windowMs).toBe(60000); // unchanged
398
- expect(limiter.maxAlerts).toBe(20); // updated
399
- });
400
-
401
- test('should affect subsequent checks', () => {
402
- const limiter = new RateLimiter({ maxAlerts: 1 });
403
-
404
- limiter.record('cpu', 'warning');
405
- expect(limiter.check('cpu', 'warning').allowed).toBe(false);
406
-
407
- // Increase limit
408
- limiter.configure({ maxAlerts: 3 });
409
-
410
- // Should now be allowed
411
- expect(limiter.check('cpu', 'warning').allowed).toBe(true);
412
- });
413
- });
414
-
415
- describe('reset', () => {
416
- test('should clear all timestamps', () => {
417
- const limiter = new RateLimiter();
418
-
419
- limiter.record('cpu', 'warning');
420
- limiter.record('memory', 'warning');
421
-
422
- expect(limiter.getCount('cpu')).toBe(1);
423
- expect(limiter.getCount('memory')).toBe(1);
424
-
425
- limiter.reset();
426
-
427
- expect(limiter.getCount('cpu')).toBe(0);
428
- expect(limiter.getCount('memory')).toBe(0);
429
- expect(limiter.timestamps).toEqual({});
430
- });
431
-
432
- test('should allow alerts after reset', () => {
433
- const limiter = new RateLimiter({ maxAlerts: 1 });
434
-
435
- limiter.record('cpu', 'warning');
436
- expect(limiter.check('cpu', 'warning').allowed).toBe(false);
437
-
438
- limiter.reset();
439
-
440
- expect(limiter.check('cpu', 'warning').allowed).toBe(true);
441
- });
442
-
443
- test('should preserve configuration after reset', () => {
444
- const limiter = new RateLimiter({ maxAlerts: 10, windowMs: 30000 });
445
-
446
- limiter.reset();
447
-
448
- expect(limiter.maxAlerts).toBe(10);
449
- expect(limiter.windowMs).toBe(30000);
450
- });
451
- });
452
-
453
- describe('alwaysAllowCritical behavior', () => {
454
- test('should allow critical alerts past limit when true', () => {
455
- const limiter = new RateLimiter({
456
- maxAlerts: 1,
457
- alwaysAllowCritical: true
458
- });
459
-
460
- limiter.record('cpu', 'warning');
461
-
462
- expect(limiter.check('cpu', 'critical').allowed).toBe(true);
463
- expect(limiter.check('cpu', 'warning').allowed).toBe(false);
464
- });
465
-
466
- test('should block critical alerts past limit when false', () => {
467
- const limiter = new RateLimiter({
468
- maxAlerts: 1,
469
- alwaysAllowCritical: false
470
- });
471
-
472
- limiter.record('cpu', 'critical');
473
-
474
- expect(limiter.check('cpu', 'critical').allowed).toBe(false);
475
- });
476
-
477
- test('should respect level parameter variations', () => {
478
- const limiter = new RateLimiter({
479
- maxAlerts: 1,
480
- alwaysAllowCritical: true
481
- });
482
-
483
- limiter.record('cpu', 'warning');
484
-
485
- // Various non-critical levels should be blocked
486
- expect(limiter.check('cpu', 'warning').allowed).toBe(false);
487
- expect(limiter.check('cpu', 'info').allowed).toBe(false);
488
-
489
- // Critical should pass
490
- expect(limiter.check('cpu', 'critical').allowed).toBe(true);
491
- });
492
-
493
- test('should be configurable at runtime', () => {
494
- const limiter = new RateLimiter({
495
- maxAlerts: 1,
496
- alwaysAllowCritical: true
497
- });
498
-
499
- limiter.record('cpu', 'warning');
500
-
501
- // Initially allowed
502
- expect(limiter.check('cpu', 'critical').allowed).toBe(true);
503
-
504
- // Disable critical bypass
505
- limiter.configure({ alwaysAllowCritical: false });
506
-
507
- // Fill up again
508
- limiter.record('cpu', 'critical');
509
-
510
- // Should now be blocked
511
- expect(limiter.check('cpu', 'critical').allowed).toBe(false);
512
- });
513
- });
514
-
515
- describe('integration with global rate limiting', () => {
516
- test('should sync with global config on creation', () => {
517
- // Create limiter should sync its config to global
518
- new RateLimiter({ maxAlerts: 7 });
519
-
520
- const globalConfig = alerts.getRateLimit();
521
- expect(globalConfig.maxAlerts).toBe(7);
522
- });
523
-
524
- test('should sync with global config on configure', () => {
525
- const limiter = new RateLimiter();
526
-
527
- limiter.configure({ maxAlerts: 15 });
528
-
529
- const globalConfig = alerts.getRateLimit();
530
- expect(globalConfig.maxAlerts).toBe(15);
531
- });
532
-
533
- test('defaultRateLimiter instance should exist', () => {
534
- // Verify the exported defaultRateLimiter exists
535
- expect(alerts.defaultRateLimiter).toBeDefined();
536
- expect(alerts.defaultRateLimiter).toBeInstanceOf(RateLimiter);
537
- });
538
- });
539
- });