claw-dashboard 1.8.4 → 2.0.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 (135) hide show
  1. package/.c8rc.json +38 -0
  2. package/.dockerignore +68 -0
  3. package/.github/dependabot.yml +45 -0
  4. package/.github/pull_request_template.md +39 -0
  5. package/.github/workflows/ci.yml +147 -0
  6. package/.github/workflows/release.yml +40 -0
  7. package/.github/workflows/security.yml +84 -0
  8. package/.husky/pre-commit +1 -0
  9. package/.planning/codebase/ARCHITECTURE.md +206 -0
  10. package/.planning/codebase/INTEGRATIONS.md +150 -0
  11. package/.planning/codebase/STACK.md +122 -0
  12. package/.planning/codebase/STRUCTURE.md +201 -0
  13. package/CHANGELOG.md +293 -0
  14. package/CONTRIBUTING.md +378 -0
  15. package/Dockerfile +54 -0
  16. package/FEATURES.md +195 -21
  17. package/README.md +83 -6
  18. package/TODO.md +28 -0
  19. package/build-cjs.js +127 -0
  20. package/cjs-shim.js +13 -0
  21. package/dist/clawdash +1729 -0
  22. package/dist/clawdash.meta.json +2236 -0
  23. package/dist/widgets.cjs +6511 -0
  24. package/docker-compose.yml +67 -0
  25. package/docs/API.md +1050 -0
  26. package/docs/PLUGINS.md +1504 -0
  27. package/esbuild.config.js +158 -0
  28. package/eslint.config.js +56 -0
  29. package/examples/plugins/README.md +122 -0
  30. package/examples/plugins/api-status/index.js +294 -0
  31. package/examples/plugins/api-status/plugin.json +19 -0
  32. package/examples/plugins/hello-world/index.js +104 -0
  33. package/examples/plugins/hello-world/plugin.json +15 -0
  34. package/examples/plugins/system-metrics-chart/index.js +339 -0
  35. package/examples/plugins/system-metrics-chart/plugin.json +18 -0
  36. package/examples/plugins/weather-widget/index.js +136 -0
  37. package/examples/plugins/weather-widget/plugin.json +19 -0
  38. package/index.cjs +23005 -0
  39. package/index.js +5331 -512
  40. package/jest.config.js +11 -0
  41. package/man/clawdash.1 +276 -0
  42. package/package.json +54 -5
  43. package/schemas/plugin-manifest.json +128 -0
  44. package/scripts/release.js +595 -0
  45. package/src/alerts.js +693 -0
  46. package/src/auto-save.js +584 -0
  47. package/src/cache.js +390 -0
  48. package/src/checksum.js +146 -0
  49. package/src/cli/args.js +110 -0
  50. package/src/cli/export-schedule.js +423 -0
  51. package/src/cli/export-snapshot.js +138 -0
  52. package/src/cli/help.js +69 -0
  53. package/src/cli/import-snapshot.js +230 -0
  54. package/src/cli/index.js +14 -0
  55. package/src/cli/list-templates.js +69 -0
  56. package/src/cli/validate-config.js +76 -0
  57. package/src/cli/validate-plugin.js +187 -0
  58. package/src/cli/version.js +15 -0
  59. package/src/config-validator.js +586 -0
  60. package/src/config-watcher.js +401 -0
  61. package/src/config.js +684 -0
  62. package/src/container-detector.js +499 -0
  63. package/src/database.js +734 -0
  64. package/src/differential-render.js +327 -0
  65. package/src/errors.js +169 -0
  66. package/src/export-scheduler.js +581 -0
  67. package/src/gateway-manager.js +685 -0
  68. package/src/hints.js +371 -0
  69. package/src/loading-states.js +509 -0
  70. package/src/logger.js +185 -0
  71. package/src/memory-pressure.js +467 -0
  72. package/src/performance-monitor.js +374 -0
  73. package/src/plugin-errors.js +586 -0
  74. package/src/plugin-manifest-validator.js +219 -0
  75. package/src/plugin-reload.js +481 -0
  76. package/src/plugin-scaffold.js +1941 -0
  77. package/src/plugin-validator.js +284 -0
  78. package/src/retry.js +218 -0
  79. package/src/security.js +860 -0
  80. package/src/snapshot.js +372 -0
  81. package/src/splash.js +115 -0
  82. package/src/theme-selector.js +411 -0
  83. package/src/themes.js +874 -0
  84. package/src/transitions.js +534 -0
  85. package/src/types.d.ts +174 -0
  86. package/src/validation.js +926 -0
  87. package/src/web-server.js +885 -0
  88. package/src/widgets/builtin-widgets.js +1057 -0
  89. package/src/widgets/config-processor.js +401 -0
  90. package/src/widgets/dependency-resolver.js +596 -0
  91. package/src/widgets/index.js +79 -0
  92. package/src/widgets/plugin-api.js +845 -0
  93. package/src/widgets/widget-error-boundary.js +773 -0
  94. package/src/widgets/widget-error-isolation.js +551 -0
  95. package/src/widgets/widget-loader.js +1437 -0
  96. package/src/workers/system-worker.js +130 -0
  97. package/src/workers/worker-pool.js +633 -0
  98. package/tests/alerts.test.js +437 -0
  99. package/tests/auto-save.test.js +529 -0
  100. package/tests/cache.test.js +317 -0
  101. package/tests/cli.test.js +351 -0
  102. package/tests/command-palette.test.js +320 -0
  103. package/tests/config-processor.test.js +452 -0
  104. package/tests/config-validator.test.js +710 -0
  105. package/tests/config-watcher.test.js +594 -0
  106. package/tests/config.test.js +755 -0
  107. package/tests/database.test.js +438 -0
  108. package/tests/errors.test.js +189 -0
  109. package/tests/example-plugins.test.js +624 -0
  110. package/tests/integration.test.js +1321 -0
  111. package/tests/loading-states.test.js +300 -0
  112. package/tests/manifest-validation-on-load.test.js +671 -0
  113. package/tests/performance-monitor.test.js +190 -0
  114. package/tests/plugin-api-rate-limit.test.js +302 -0
  115. package/tests/plugin-errors.test.js +311 -0
  116. package/tests/plugin-lifecycle-e2e.test.js +1036 -0
  117. package/tests/plugin-reload.test.js +764 -0
  118. package/tests/rate-limiter.test.js +539 -0
  119. package/tests/retry.test.js +308 -0
  120. package/tests/security.test.js +411 -0
  121. package/tests/settings-modal.test.js +226 -0
  122. package/tests/theme-selector.test.js +57 -0
  123. package/tests/utils.js +242 -0
  124. package/tests/utils.test.js +317 -0
  125. package/tests/validate-plugin-cli.test.js +359 -0
  126. package/tests/validation.test.js +837 -0
  127. package/tests/web-server.test.js +646 -0
  128. package/tests/widget-arrange-mode.test.js +183 -0
  129. package/tests/widget-config-hot-reload.test.js +465 -0
  130. package/tests/widget-dependency.test.js +591 -0
  131. package/tests/widget-error-boundary.test.js +749 -0
  132. package/tests/widget-error-isolation.test.js +469 -0
  133. package/tests/widget-integration.test.js +1147 -0
  134. package/tests/widget-refresh-intervals.test.js +284 -0
  135. package/tests/worker-pool.test.js +522 -0
@@ -0,0 +1,284 @@
1
+ /**
2
+ * Tests for Widget Configurable Refresh Intervals and Graceful Degradation
3
+ */
4
+
5
+ import { jest } from '@jest/globals';
6
+ import { BaseWidget } from '../src/widgets/plugin-api.js';
7
+ import config from '../src/config.js';
8
+
9
+ // Mock logger
10
+ jest.unstable_mockModule('../src/logger.js', () => ({
11
+ default: {
12
+ info: jest.fn(),
13
+ warn: jest.fn(),
14
+ error: jest.fn(),
15
+ debug: jest.fn(),
16
+ },
17
+ }));
18
+
19
+ describe('Widget Refresh Intervals', () => {
20
+ describe('BaseWidget', () => {
21
+ test('should initialize with null refresh interval by default', () => {
22
+ const widget = new BaseWidget({ id: 'test-widget' });
23
+ expect(widget.refreshInterval).toBeNull();
24
+ });
25
+
26
+ test('should use refresh interval from config', () => {
27
+ const widget = new BaseWidget({ id: 'cpu' });
28
+ // CPU should use WIDGET_REFRESH_INTERVALS.CPU from config
29
+ expect(widget.refreshInterval).toBe(config.WIDGET_REFRESH_INTERVALS.CPU);
30
+ });
31
+
32
+ test('should allow custom refresh interval in constructor', () => {
33
+ const widget = new BaseWidget({
34
+ id: 'custom-widget',
35
+ config: { refreshInterval: 5000 }
36
+ });
37
+ expect(widget.refreshInterval).toBe(5000);
38
+ });
39
+
40
+ test('should validate refresh interval range', () => {
41
+ const widget = new BaseWidget({ id: 'test-widget' });
42
+
43
+ // Valid intervals should work
44
+ widget.updateRefreshInterval(1000);
45
+ expect(widget.refreshInterval).toBe(1000);
46
+
47
+ widget.updateRefreshInterval(30000);
48
+ expect(widget.refreshInterval).toBe(30000);
49
+
50
+ widget.updateRefreshInterval(null);
51
+ expect(widget.refreshInterval).toBeNull();
52
+ });
53
+
54
+ test('should throw on invalid refresh interval', () => {
55
+ const widget = new BaseWidget({ id: 'test-widget' });
56
+
57
+ // Too low
58
+ expect(() => widget.updateRefreshInterval(100)).toThrow(/Invalid refresh interval/);
59
+
60
+ // Too high
61
+ expect(() => widget.updateRefreshInterval(100000)).toThrow(/Invalid refresh interval/);
62
+ });
63
+ });
64
+
65
+ describe('shouldUpdate', () => {
66
+ test('should always return true when no refresh interval is set', () => {
67
+ const widget = new BaseWidget({ id: 'test-widget' });
68
+ widget.refreshInterval = null;
69
+
70
+ expect(widget.shouldUpdate(1000)).toBe(true);
71
+ expect(widget.shouldUpdate(0)).toBe(true);
72
+ expect(widget.shouldUpdate(999999)).toBe(true);
73
+ });
74
+
75
+ test('should return true when interval has elapsed', () => {
76
+ const widget = new BaseWidget({ id: 'test-widget' });
77
+ widget.refreshInterval = 1000;
78
+ widget.lastUpdateTime = 0;
79
+
80
+ expect(widget.shouldUpdate(1000)).toBe(true);
81
+ expect(widget.shouldUpdate(1500)).toBe(true);
82
+ expect(widget.shouldUpdate(2000)).toBe(true);
83
+ });
84
+
85
+ test('should return false when interval has not elapsed', () => {
86
+ const widget = new BaseWidget({ id: 'test-widget' });
87
+ widget.refreshInterval = 1000;
88
+ widget.lastUpdateTime = 1000;
89
+
90
+ expect(widget.shouldUpdate(1000)).toBe(false);
91
+ expect(widget.shouldUpdate(1500)).toBe(false);
92
+ expect(widget.shouldUpdate(1999)).toBe(false);
93
+ });
94
+
95
+ test('should use current time when not provided', () => {
96
+ const widget = new BaseWidget({ id: 'test-widget' });
97
+ widget.refreshInterval = 50000; // Very long interval
98
+ widget.lastUpdateTime = Date.now();
99
+
100
+ expect(widget.shouldUpdate()).toBe(false);
101
+ });
102
+ });
103
+
104
+ describe('recordUpdate', () => {
105
+ test('should update lastUpdateTime and increment count', () => {
106
+ const widget = new BaseWidget({ id: 'test-widget' });
107
+ widget.recordUpdate(1000);
108
+
109
+ expect(widget.lastUpdateTime).toBe(1000);
110
+ expect(widget.updateCount).toBe(1);
111
+
112
+ widget.recordUpdate(2000);
113
+ expect(widget.lastUpdateTime).toBe(2000);
114
+ expect(widget.updateCount).toBe(2);
115
+ });
116
+
117
+ test('should use current time by default', () => {
118
+ const widget = new BaseWidget({ id: 'test-widget' });
119
+ const before = Date.now();
120
+ widget.recordUpdate();
121
+ const after = Date.now();
122
+
123
+ expect(widget.lastUpdateTime).toBeGreaterThanOrEqual(before);
124
+ expect(widget.lastUpdateTime).toBeLessThanOrEqual(after);
125
+ });
126
+ });
127
+
128
+ describe('getRefreshStats', () => {
129
+ test('should return refresh statistics', () => {
130
+ const widget = new BaseWidget({ id: 'test-widget' });
131
+ widget.refreshInterval = 5000;
132
+ widget.lastUpdateTime = 1000;
133
+ widget.updateCount = 5;
134
+ widget.skipCount = 2;
135
+
136
+ const stats = widget.getRefreshStats();
137
+
138
+ expect(stats.refreshInterval).toBe(5000);
139
+ expect(stats.lastUpdateTime).toBe(1000);
140
+ expect(stats.updateCount).toBe(5);
141
+ expect(stats.skippedUpdates).toBe(2);
142
+ });
143
+ });
144
+ });
145
+
146
+ describe('Widget Graceful Degradation', () => {
147
+ describe('shouldUpdateUnderDegradation', () => {
148
+ test('should always allow critical widgets to update', () => {
149
+ const widget = new BaseWidget({ id: 'cpu' });
150
+ widget.refreshInterval = 1000;
151
+ widget.lastUpdateTime = 0;
152
+
153
+ // Critical widgets should update even in critical degradation
154
+ const result = widget.shouldUpdateUnderDegradation('critical', 1000);
155
+ expect(result.shouldUpdate).toBe(true);
156
+ expect(result.reason).toBe('critical_widget');
157
+ });
158
+
159
+ test('should skip non-critical widgets in critical degradation', () => {
160
+ const widget = new BaseWidget({ id: 'disk' });
161
+ widget.priority = 100; // Non-critical priority
162
+ widget.refreshInterval = 30000;
163
+ widget.lastUpdateTime = 0;
164
+
165
+ // Disk widget should be skipped in critical degradation
166
+ const result = widget.shouldUpdateUnderDegradation('critical', 1000);
167
+ expect(result.shouldUpdate).toBe(false);
168
+ expect(result.reason).toBe('degradation_critical_skip');
169
+ expect(widget.skipCount).toBe(1);
170
+ });
171
+
172
+ test('should extend intervals under warning degradation', () => {
173
+ const widget = new BaseWidget({ id: 'disk' });
174
+ widget.priority = 100;
175
+ widget.refreshInterval = 10000;
176
+ widget.lastUpdateTime = 0;
177
+
178
+ // At time 5000, with 1.5x multiplier, need 15000ms - should not update
179
+ const result1 = widget.shouldUpdateUnderDegradation('warning', 5000);
180
+ expect(result1.shouldUpdate).toBe(false);
181
+ expect(result1.reason).toBe('degradation_extended_interval');
182
+
183
+ // At time 16000, with 1.5x multiplier on 10000ms = 15000ms - should update
184
+ const result2 = widget.shouldUpdateUnderDegradation('warning', 16000);
185
+ expect(result2.shouldUpdate).toBe(true);
186
+ expect(result2.reason).toBe('ok');
187
+ });
188
+
189
+ test('should extend intervals more under critical degradation', () => {
190
+ const widget = new BaseWidget({ id: 'network' });
191
+ widget.priority = 10; // Low enough to not be skipped
192
+ widget.refreshInterval = 10000;
193
+ widget.lastUpdateTime = 0;
194
+
195
+ // At time 15000, with 2.0x multiplier, need 20000ms - should not update
196
+ const result1 = widget.shouldUpdateUnderDegradation('critical', 15000);
197
+ expect(result1.shouldUpdate).toBe(false);
198
+ expect(result1.reason).toBe('degradation_extended_interval');
199
+
200
+ // At time 25000, with 2.0x multiplier on 10000ms = 20000ms - should update
201
+ const result2 = widget.shouldUpdateUnderDegradation('critical', 25000);
202
+ expect(result2.shouldUpdate).toBe(true);
203
+ expect(result2.reason).toBe('ok');
204
+ });
205
+
206
+ test('should use standard intervals when no degradation', () => {
207
+ const widget = new BaseWidget({ id: 'disk' });
208
+ widget.refreshInterval = 30000;
209
+ widget.lastUpdateTime = 0;
210
+
211
+ // Should use normal interval when no degradation
212
+ const result1 = widget.shouldUpdateUnderDegradation('none', 1000);
213
+ expect(result1.shouldUpdate).toBe(false);
214
+ expect(result1.reason).toBe('interval_not_elapsed');
215
+
216
+ const result2 = widget.shouldUpdateUnderDegradation('none', 30000);
217
+ expect(result2.shouldUpdate).toBe(true);
218
+ expect(result2.reason).toBe('ok');
219
+ });
220
+ });
221
+
222
+ describe('setDegradationLevel', () => {
223
+ test('should update degradation state', () => {
224
+ const widget = new BaseWidget({ id: 'test-widget' });
225
+
226
+ widget.setDegradationLevel('warning');
227
+ expect(widget.degradationLevel).toBe('warning');
228
+ expect(widget.isDegraded).toBe(true);
229
+
230
+ widget.setDegradationLevel('critical');
231
+ expect(widget.degradationLevel).toBe('critical');
232
+ expect(widget.isDegraded).toBe(true);
233
+
234
+ widget.setDegradationLevel('none');
235
+ expect(widget.degradationLevel).toBe('none');
236
+ expect(widget.isDegraded).toBe(false);
237
+ });
238
+
239
+ test('should adjust currentRefreshInterval based on degradation', () => {
240
+ const widget = new BaseWidget({ id: 'test-widget' });
241
+ widget.refreshInterval = 10000;
242
+
243
+ // Normal operation
244
+ widget.setDegradationLevel('none');
245
+ expect(widget.currentRefreshInterval).toBe(10000);
246
+
247
+ // Warning - 1.5x multiplier
248
+ widget.setDegradationLevel('warning');
249
+ expect(widget.currentRefreshInterval).toBe(15000);
250
+
251
+ // Critical - 2.0x multiplier
252
+ widget.setDegradationLevel('critical');
253
+ expect(widget.currentRefreshInterval).toBe(20000);
254
+ });
255
+ });
256
+ });
257
+
258
+ describe('Widget Config Integration', () => {
259
+ test('WIDGET_REFRESH_INTERVALS should have expected values', () => {
260
+ // Check that config has the expected default values
261
+ expect(config.WIDGET_REFRESH_INTERVALS.CPU).toBe(1000);
262
+ expect(config.WIDGET_REFRESH_INTERVALS.MEMORY).toBe(1000);
263
+ expect(config.WIDGET_REFRESH_INTERVALS.GPU).toBe(5000);
264
+ expect(config.WIDGET_REFRESH_INTERVALS.NETWORK).toBe(1000);
265
+ expect(config.WIDGET_REFRESH_INTERVALS.DISK).toBe(30000);
266
+ expect(config.WIDGET_REFRESH_INTERVALS.SYSTEM).toBe(5000);
267
+ expect(config.WIDGET_REFRESH_INTERVALS.UPTIME).toBe(60000);
268
+ });
269
+
270
+ test('WIDGET_REFRESH_VALIDATION should have min/max values', () => {
271
+ expect(config.WIDGET_REFRESH_VALIDATION.MIN_INTERVAL).toBe(500);
272
+ expect(config.WIDGET_REFRESH_VALIDATION.MAX_INTERVAL).toBe(60000);
273
+ });
274
+
275
+ test('WIDGET_DEGRADATION should define critical widgets', () => {
276
+ expect(config.WIDGET_DEGRADATION.CRITICAL_WIDGETS).toContain('cpu');
277
+ expect(config.WIDGET_DEGRADATION.CRITICAL_WIDGETS).toContain('memory');
278
+ });
279
+
280
+ test('WIDGET_DEGRADATION should have multipliers', () => {
281
+ expect(config.WIDGET_DEGRADATION.WARNING.EXTEND_INTERVAL_MULTIPLIER).toBe(1.5);
282
+ expect(config.WIDGET_DEGRADATION.CRITICAL.EXTEND_INTERVAL_MULTIPLIER).toBe(2.0);
283
+ });
284
+ });