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,529 +0,0 @@
1
- /**
2
- * Tests for auto-save module
3
- * Dashboard state persistence and auto-save functionality
4
- */
5
-
6
- import { jest } from '@jest/globals';
7
- import fs from 'fs';
8
- import path from 'path';
9
- import os from 'os';
10
- import { fileURLToPath } from 'url';
11
-
12
- const __filename = fileURLToPath(import.meta.url);
13
- const __dirname = path.dirname(__filename);
14
-
15
- // Import the modules to test - using dynamic import to handle ESM mocks
16
- let autoSaveModule;
17
-
18
- describe('auto-save', () => {
19
- let tempDir;
20
- let statePath;
21
-
22
- beforeAll(async () => {
23
- // Create temporary directory for test files
24
- tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'autosave-test-'));
25
- statePath = path.join(tempDir, 'dashboard-state.json');
26
-
27
- // Import the module fresh for each test
28
- autoSaveModule = await import('../src/auto-save.js');
29
- });
30
-
31
- afterAll(() => {
32
- // Clean up temp directory
33
- if (fs.existsSync(tempDir)) {
34
- fs.rmSync(tempDir, { recursive: true, force: true });
35
- }
36
- });
37
-
38
- beforeEach(() => {
39
- // Clear all mocks
40
- jest.clearAllMocks();
41
-
42
- // Clean up any files in temp directory
43
- if (fs.existsSync(tempDir)) {
44
- fs.readdirSync(tempDir).forEach(f => {
45
- try {
46
- fs.unlinkSync(path.join(tempDir, f));
47
- } catch {
48
- // Ignore cleanup errors
49
- }
50
- });
51
- }
52
- });
53
-
54
- describe('AutoSaveManager', () => {
55
- test('should create instance with default options', () => {
56
- const { AutoSaveManager } = autoSaveModule;
57
- const manager = new AutoSaveManager({});
58
-
59
- expect(manager.enabled).toBe(true);
60
- expect(manager.intervalMs).toBe(30000);
61
- expect(manager.isDirty).toBe(false);
62
- expect(manager.saveCount).toBe(0);
63
- });
64
-
65
- test('should create instance with custom options', () => {
66
- const { AutoSaveManager } = autoSaveModule;
67
- const getState = () => ({ test: 'state' });
68
- const getSettings = () => ({ test: 'settings' });
69
- const saveSettings = jest.fn();
70
-
71
- const manager = new AutoSaveManager({
72
- enabled: false,
73
- intervalMs: 5000,
74
- statePath: '/custom/path/state.json',
75
- getState,
76
- getSettings,
77
- saveSettings,
78
- });
79
-
80
- expect(manager.enabled).toBe(false);
81
- expect(manager.intervalMs).toBe(5000);
82
- expect(manager.statePath).toBe('/custom/path/state.json');
83
- expect(manager.getState).toBe(getState);
84
- expect(manager.getSettings).toBe(getSettings);
85
- expect(manager.saveSettings).toBe(saveSettings);
86
- });
87
-
88
- test('should start and stop timer', () => {
89
- jest.useFakeTimers();
90
-
91
- const { AutoSaveManager } = autoSaveModule;
92
- const manager = new AutoSaveManager({
93
- statePath,
94
- intervalMs: 1000,
95
- });
96
-
97
- expect(manager.timer).toBeNull();
98
-
99
- manager.start();
100
- expect(manager.timer).not.toBeNull();
101
-
102
- manager.stop();
103
- expect(manager.timer).toBeNull();
104
-
105
- jest.useRealTimers();
106
- });
107
-
108
- test('should not start when disabled', () => {
109
- const { AutoSaveManager } = autoSaveModule;
110
- const manager = new AutoSaveManager({
111
- enabled: false,
112
- statePath,
113
- });
114
-
115
- manager.start();
116
- expect(manager.timer).toBeNull();
117
- });
118
-
119
- test('should mark dirty', () => {
120
- const { AutoSaveManager } = autoSaveModule;
121
- const manager = new AutoSaveManager({ statePath });
122
-
123
- expect(manager.isDirty).toBe(false);
124
- manager.markDirty();
125
- expect(manager.isDirty).toBe(true);
126
- });
127
-
128
- test('should perform save and write file', async () => {
129
- const { AutoSaveManager } = autoSaveModule;
130
- const manager = new AutoSaveManager({
131
- statePath,
132
- getState: () => ({
133
- selectedSessionIndex: 1,
134
- paginationOffset: 2,
135
- sessionSearchQuery: 'query',
136
- isSearchMode: true,
137
- showFavoritesOnly: true,
138
- focusedWidgetIndex: 0,
139
- currentRefreshInterval: 5000,
140
- }),
141
- getSettings: () => ({ theme: 'dark' }),
142
- });
143
-
144
- // Trigger save
145
- const result = manager.saveNow();
146
-
147
- expect(result).toBe(true);
148
- expect(manager.saveCount).toBe(1);
149
- expect(fs.existsSync(statePath)).toBe(true);
150
-
151
- // Verify file content
152
- const savedData = JSON.parse(fs.readFileSync(statePath, 'utf8'));
153
- expect(savedData.settings).toEqual({ theme: 'dark' });
154
- expect(savedData.ui.selectedSessionIndex).toBe(1);
155
- expect(savedData.ui.sessionSearchQuery).toBe('query');
156
- expect(savedData.ui.isSearchMode).toBe(true);
157
- });
158
-
159
- test('should skip save if state unchanged', () => {
160
- const { AutoSaveManager } = autoSaveModule;
161
- const manager = new AutoSaveManager({
162
- statePath,
163
- getState: () => ({ test: 'state' }),
164
- getSettings: () => ({ test: 'settings' }),
165
- });
166
-
167
- // First save
168
- manager.performAutoSave();
169
- expect(manager.saveCount).toBe(1);
170
-
171
- // Second save with same state should be skipped
172
- manager.performAutoSave();
173
- expect(manager.saveCount).toBe(1);
174
- });
175
-
176
- test('should save on state change', () => {
177
- let state = { version: 1 };
178
- const { AutoSaveManager } = autoSaveModule;
179
- const manager = new AutoSaveManager({
180
- statePath,
181
- getState: () => state,
182
- getSettings: () => ({}),
183
- });
184
-
185
- // First save
186
- manager.performAutoSave();
187
- expect(manager.saveCount).toBe(1);
188
-
189
- // Change state and save again
190
- state = { version: 2 };
191
- manager.isDirty = true;
192
- manager.performAutoSave();
193
- expect(manager.saveCount).toBe(2);
194
- });
195
-
196
- test('should return false when disabled', () => {
197
- const { AutoSaveManager } = autoSaveModule;
198
- const manager = new AutoSaveManager({
199
- enabled: false,
200
- statePath,
201
- getState: () => ({ test: 'state' }),
202
- getSettings: () => ({}),
203
- });
204
-
205
- const result = manager.performAutoSave();
206
- expect(result).toBe(false);
207
- });
208
-
209
- test('should return stats', () => {
210
- const { AutoSaveManager } = autoSaveModule;
211
- const manager = new AutoSaveManager({
212
- statePath: '/test/state.json',
213
- intervalMs: 5000,
214
- });
215
-
216
- const stats = manager.getStats();
217
-
218
- expect(stats.enabled).toBe(true);
219
- expect(stats.intervalMs).toBe(5000);
220
- expect(stats.saveCount).toBe(0);
221
- expect(stats.statePath).toBe('/test/state.json');
222
- });
223
-
224
- test('should update config', () => {
225
- const { AutoSaveManager } = autoSaveModule;
226
- const manager = new AutoSaveManager({
227
- statePath,
228
- intervalMs: 1000,
229
- });
230
-
231
- manager.start();
232
- expect(manager.intervalMs).toBe(1000);
233
-
234
- manager.updateConfig({ intervalMs: 2000 });
235
- expect(manager.intervalMs).toBe(2000);
236
-
237
- manager.stop();
238
- });
239
-
240
- test('should save state file with correct structure', () => {
241
- const { AutoSaveManager } = autoSaveModule;
242
- const manager = new AutoSaveManager({
243
- statePath,
244
- getState: () => ({
245
- selectedSessionIndex: 5,
246
- paginationOffset: 10,
247
- sessionSearchQuery: 'test',
248
- isSearchMode: true,
249
- showFavoritesOnly: false,
250
- focusedWidgetIndex: 2,
251
- currentRefreshInterval: 5000,
252
- }),
253
- getSettings: () => ({ theme: 'dark' }),
254
- });
255
-
256
- manager.performAutoSave();
257
-
258
- const savedData = JSON.parse(fs.readFileSync(statePath, 'utf8'));
259
-
260
- expect(savedData.timestamp).toBeDefined();
261
- expect(savedData.settings).toEqual({ theme: 'dark' });
262
- expect(savedData.ui.selectedSessionIndex).toBe(5);
263
- expect(savedData.ui.paginationOffset).toBe(10);
264
- expect(savedData.ui.sessionSearchQuery).toBe('test');
265
- expect(savedData.ui.isSearchMode).toBe(true);
266
- });
267
-
268
- test('should create backup before overwriting existing state', () => {
269
- const { AutoSaveManager } = autoSaveModule;
270
- const manager = new AutoSaveManager({
271
- statePath,
272
- getState: () => ({ selectedSessionIndex: 0 }),
273
- getSettings: () => ({ theme: 'dark' }),
274
- });
275
-
276
- // First save - creates initial state
277
- manager.performAutoSave();
278
- expect(fs.existsSync(statePath)).toBe(true);
279
-
280
- // Second save - should create backup
281
- manager.getState = () => ({ selectedSessionIndex: 1 });
282
- manager.isDirty = true;
283
- manager.performAutoSave();
284
-
285
- // Check backup was created
286
- const dir = path.dirname(statePath);
287
- const backups = fs.readdirSync(dir).filter(f => f.includes('dashboard-state.json') && f.endsWith('.backup'));
288
- expect(backups.length).toBeGreaterThan(0);
289
-
290
- // Verify backup contains first save data
291
- const backupPath = path.join(dir, backups[0]);
292
- const backupData = JSON.parse(fs.readFileSync(backupPath, 'utf8'));
293
- expect(backupData.ui.selectedSessionIndex).toBe(0);
294
- });
295
-
296
- test('should not create backup when backupEnabled is false', () => {
297
- const { AutoSaveManager } = autoSaveModule;
298
- const manager = new AutoSaveManager({
299
- statePath,
300
- backupEnabled: false,
301
- getState: () => ({ selectedSessionIndex: 0 }),
302
- getSettings: () => ({ theme: 'dark' }),
303
- });
304
-
305
- // First save
306
- manager.performAutoSave();
307
-
308
- // Second save - should not create backup
309
- manager.getState = () => ({ selectedSessionIndex: 1 });
310
- manager.isDirty = true;
311
- manager.performAutoSave();
312
-
313
- // Check no backups were created
314
- const dir = path.dirname(statePath);
315
- const backups = fs.readdirSync(dir).filter(f => f.endsWith('.backup'));
316
- expect(backups.length).toBe(0);
317
- });
318
-
319
- test('should clean up old backups keeping only backupCount', () => {
320
- // Use isolated temp directory for this test to avoid interference
321
- const isolatedDir = fs.mkdtempSync(path.join(os.tmpdir(), 'autosave-backup-test-'));
322
- const isolatedStatePath = path.join(isolatedDir, 'dashboard-state.json');
323
-
324
- const { AutoSaveManager } = autoSaveModule;
325
- const manager = new AutoSaveManager({
326
- statePath: isolatedStatePath,
327
- backupCount: 3,
328
- getState: () => ({ selectedSessionIndex: 0 }),
329
- getSettings: () => ({ theme: 'dark' }),
330
- });
331
-
332
- // Create multiple saves to generate backups
333
- // First save creates initial state (no backup)
334
- // Subsequent saves create backups (i=1..5 creates 5 backups, cleanup removes oldest 2, keeps 3)
335
- for (let i = 0; i < 6; i++) {
336
- manager.getState = () => ({ selectedSessionIndex: i });
337
- manager.isDirty = true;
338
- manager.performAutoSave();
339
- }
340
-
341
- // Check only 3 backups remain
342
- const backups = fs.readdirSync(isolatedDir)
343
- .filter(f => f.endsWith('.backup'));
344
- expect(backups.length).toBe(3);
345
-
346
- // Cleanup
347
- fs.rmSync(isolatedDir, { recursive: true, force: true });
348
- });
349
-
350
- test('should track statistics on saves', () => {
351
- const { AutoSaveManager } = autoSaveModule;
352
- const manager = new AutoSaveManager({
353
- statePath,
354
- getState: () => ({ selectedSessionIndex: 5 }),
355
- getSettings: () => ({ theme: 'dark' }),
356
- });
357
-
358
- manager.performAutoSave();
359
-
360
- expect(manager.saveCount).toBe(1);
361
- expect(manager.stats.totalBytesWritten).toBeGreaterThan(0);
362
- expect(manager.stats.totalSaveTimeMs).toBeGreaterThanOrEqual(0);
363
- expect(manager.stats.averageSaveTimeMs).toBeGreaterThanOrEqual(0);
364
- });
365
-
366
- test('should return extended stats including backup info', () => {
367
- const { AutoSaveManager } = autoSaveModule;
368
- const manager = new AutoSaveManager({
369
- statePath: '/test/state.json',
370
- backupCount: 3,
371
- backupEnabled: true,
372
- });
373
-
374
- const stats = manager.getStats();
375
-
376
- expect(stats.backupEnabled).toBe(true);
377
- expect(stats.backupCount).toBe(3);
378
- expect(stats.totalBytesWritten).toBe(0);
379
- expect(stats.totalBackupsCreated).toBe(0);
380
- expect(stats.totalBackupsCleaned).toBe(0);
381
- expect(stats.lastBackupPath).toBeNull();
382
- expect(stats.averageSaveTimeMs).toBe(0);
383
- expect(stats.totalSaveTimeMs).toBe(0);
384
- });
385
-
386
- test('should log stats periodically', () => {
387
- jest.useFakeTimers();
388
- const { AutoSaveManager } = autoSaveModule;
389
- const manager = new AutoSaveManager({
390
- statePath,
391
- statsLogIntervalMs: 1000,
392
- getState: () => ({ selectedSessionIndex: 0 }),
393
- getSettings: () => ({ theme: 'dark' }),
394
- });
395
-
396
- // First save should log initial stats
397
- manager.performAutoSave();
398
- expect(manager.lastStatsLogTime).toBeGreaterThan(0);
399
-
400
- // Fast-forward time
401
- jest.advanceTimersByTime(500);
402
-
403
- // Second save should not log (interval not passed)
404
- const prevLogTime = manager.lastStatsLogTime;
405
- manager.isDirty = true;
406
- manager.performAutoSave();
407
- expect(manager.lastStatsLogTime).toBe(prevLogTime);
408
-
409
- // Fast-forward past interval
410
- jest.advanceTimersByTime(600);
411
-
412
- // Third save should log again
413
- manager.isDirty = true;
414
- manager.performAutoSave();
415
- expect(manager.lastStatsLogTime).toBeGreaterThan(prevLogTime);
416
-
417
- jest.useRealTimers();
418
- });
419
- });
420
-
421
- describe('loadDashboardState', () => {
422
- test('should load existing state file', () => {
423
- const { loadDashboardState } = autoSaveModule;
424
- const stateData = {
425
- timestamp: Date.now(),
426
- settings: { theme: 'dark' },
427
- ui: { selectedSessionIndex: 3 },
428
- };
429
-
430
- fs.writeFileSync(statePath, JSON.stringify(stateData, null, 2));
431
-
432
- const loaded = loadDashboardState(statePath);
433
-
434
- expect(loaded.settings.theme).toBe('dark');
435
- expect(loaded.ui.selectedSessionIndex).toBe(3);
436
- });
437
-
438
- test('should return null for non-existent file', () => {
439
- const { loadDashboardState } = autoSaveModule;
440
- const loaded = loadDashboardState(path.join(tempDir, 'nonexistent-state.json'));
441
- expect(loaded).toBeNull();
442
- });
443
-
444
- test('should return null for invalid JSON', () => {
445
- const { loadDashboardState } = autoSaveModule;
446
- const badStatePath = path.join(tempDir, 'bad-state.json');
447
- fs.writeFileSync(badStatePath, 'invalid json');
448
- const loaded = loadDashboardState(badStatePath);
449
- expect(loaded).toBeNull();
450
- });
451
- });
452
-
453
- describe('restoreDashboardState', () => {
454
- test('should restore UI state to dashboard', () => {
455
- const { restoreDashboardState } = autoSaveModule;
456
- const dashboard = {
457
- selectedSessionIndex: 0,
458
- paginationOffset: 0,
459
- sessionSearchQuery: '',
460
- isSearchMode: false,
461
- showFavoritesOnly: false,
462
- focusedWidgetIndex: -1,
463
- currentRefreshInterval: 2000,
464
- };
465
-
466
- const savedState = {
467
- ui: {
468
- selectedSessionIndex: 5,
469
- paginationOffset: 10,
470
- sessionSearchQuery: 'test query',
471
- isSearchMode: true,
472
- showFavoritesOnly: true,
473
- focusedWidgetIndex: 2,
474
- currentRefreshInterval: 5000,
475
- },
476
- };
477
-
478
- const result = restoreDashboardState(savedState, dashboard);
479
-
480
- expect(result).toBe(true);
481
- expect(dashboard.selectedSessionIndex).toBe(5);
482
- expect(dashboard.paginationOffset).toBe(10);
483
- expect(dashboard.sessionSearchQuery).toBe('test query');
484
- expect(dashboard.isSearchMode).toBe(true);
485
- expect(dashboard.showFavoritesOnly).toBe(true);
486
- expect(dashboard.focusedWidgetIndex).toBe(2);
487
- expect(dashboard.currentRefreshInterval).toBe(5000);
488
- });
489
-
490
- test('should return false for null savedState', () => {
491
- const { restoreDashboardState } = autoSaveModule;
492
- const dashboard = {};
493
- const result = restoreDashboardState(null, dashboard);
494
- expect(result).toBe(false);
495
- });
496
-
497
- test('should return false for missing ui data', () => {
498
- const { restoreDashboardState } = autoSaveModule;
499
- const dashboard = {};
500
- const result = restoreDashboardState({}, dashboard);
501
- expect(result).toBe(false);
502
- });
503
- });
504
-
505
- describe('cleanupOldStateFiles', () => {
506
- test('should clean up old state files', () => {
507
- const { cleanupOldStateFiles } = autoSaveModule;
508
-
509
- // Create test files
510
- for (let i = 0; i < 5; i++) {
511
- const filePath = path.join(tempDir, `dashboard-state-${i}.json`);
512
- fs.writeFileSync(filePath, '{}');
513
- }
514
-
515
- cleanupOldStateFiles(tempDir, 2);
516
-
517
- const files = fs.readdirSync(tempDir).filter(f => f.startsWith('dashboard-state'));
518
- expect(files.length).toBe(2);
519
- });
520
-
521
- test('should handle non-existent directory', () => {
522
- const { cleanupOldStateFiles } = autoSaveModule;
523
- // Should not throw
524
- expect(() => {
525
- cleanupOldStateFiles('/nonexistent/directory', 3);
526
- }).not.toThrow();
527
- });
528
- });
529
- });