claw-dashboard 1.9.0 → 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 +5236 -566
  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,308 @@
1
+ /**
2
+ * Integration tests for retry logic with mock failures
3
+ */
4
+
5
+ import retry from '../src/retry.js';
6
+
7
+ // Simple mock helper that properly tracks call counts
8
+ function createMockFn(successValue, errorSequence = []) {
9
+ let errorIndex = 0;
10
+ return async (...args) => {
11
+ if (errorIndex < errorSequence.length) {
12
+ const err = errorSequence[errorIndex++];
13
+ throw err;
14
+ }
15
+ return successValue;
16
+ };
17
+ }
18
+
19
+ describe('Retry Logic', () => {
20
+ describe('withRetry', () => {
21
+ test('should succeed on first attempt when function succeeds', async () => {
22
+ let callCount = 0;
23
+ const fn = async () => {
24
+ callCount++;
25
+ return 'success';
26
+ };
27
+
28
+ const wrapped = retry.withRetry(fn);
29
+ const result = await wrapped();
30
+
31
+ expect(result).toBe('success');
32
+ expect(callCount).toBe(1);
33
+ });
34
+
35
+ test('should retry on failure and eventually succeed', async () => {
36
+ const mockFn = createMockFn(
37
+ 'success',
38
+ [new Error('Network timeout'), new Error('Connection refused')]
39
+ );
40
+
41
+ const wrapped = retry.withRetry(mockFn);
42
+ const result = await wrapped();
43
+
44
+ expect(result).toBe('success');
45
+ });
46
+
47
+ test('should fail after max retries exceeded', async () => {
48
+ const error = new Error('Network timeout');
49
+ const mockFn = async () => {
50
+ throw error;
51
+ };
52
+
53
+ const wrapped = retry.withRetry(mockFn, { maxRetries: 2 });
54
+
55
+ await expect(wrapped()).rejects.toThrow('Network timeout');
56
+ });
57
+
58
+ test('should not retry on non-retryable errors', async () => {
59
+ const error = new Error('Bad Request');
60
+ error.status = 400;
61
+ let callCount = 0;
62
+ const mockFn = async () => {
63
+ callCount++;
64
+ throw error;
65
+ };
66
+
67
+ const wrapped = retry.withRetry(mockFn);
68
+
69
+ await expect(wrapped()).rejects.toThrow('Bad Request');
70
+ expect(callCount).toBe(1);
71
+ });
72
+
73
+ test('should retry on retryable HTTP status codes', async () => {
74
+ const mockFn = createMockFn(
75
+ 'success',
76
+ [{ message: 'Server Error', status: 500 }]
77
+ );
78
+
79
+ const wrapped = retry.withRetry(mockFn);
80
+ const result = await wrapped();
81
+
82
+ expect(result).toBe('success');
83
+ });
84
+
85
+ test('should retry on retryable error codes', async () => {
86
+ const mockFn = createMockFn(
87
+ 'success',
88
+ [{ message: 'Connection refused', code: 'ECONNREFUSED' }]
89
+ );
90
+
91
+ const wrapped = retry.withRetry(mockFn);
92
+ const result = await wrapped();
93
+
94
+ expect(result).toBe('success');
95
+ });
96
+
97
+ test('should respect custom maxRetries option', async () => {
98
+ const error = new Error('Network timeout');
99
+ let attempts = 0;
100
+ const mockFn = async () => {
101
+ attempts++;
102
+ throw error;
103
+ };
104
+
105
+ const wrapped = retry.withRetry(mockFn, { maxRetries: 5, initialDelay: 100 });
106
+
107
+ await expect(wrapped()).rejects.toThrow();
108
+ expect(attempts).toBe(6);
109
+ }, 30000);
110
+
111
+ test('should pass arguments to wrapped function', async () => {
112
+ let receivedArgs = null;
113
+ const mockFn = async (...args) => {
114
+ receivedArgs = args;
115
+ return 'result';
116
+ };
117
+
118
+ const wrapped = retry.withRetry(mockFn);
119
+ await wrapped('arg1', 'arg2');
120
+
121
+ expect(receivedArgs).toEqual(['arg1', 'arg2']);
122
+ });
123
+
124
+ test('should use custom retryable statuses', async () => {
125
+ const mockFn = createMockFn(
126
+ 'success',
127
+ [{ message: 'Bad Request', status: 400 }]
128
+ );
129
+
130
+ const wrapped = retry.withRetry(mockFn, {
131
+ retryableStatuses: [400, 500]
132
+ });
133
+
134
+ const result = await wrapped();
135
+ expect(result).toBe('success');
136
+ });
137
+ });
138
+
139
+ describe('calculateDelay', () => {
140
+ test('should calculate exponential backoff delay', () => {
141
+ const options = {
142
+ initialDelay: 1000,
143
+ maxDelay: 10000,
144
+ backoffMultiplier: 2
145
+ };
146
+
147
+ const delay0 = retry.calculateDelay(0, options);
148
+ expect(delay0).toBeGreaterThanOrEqual(900);
149
+ expect(delay0).toBeLessThanOrEqual(1100);
150
+
151
+ const delay1 = retry.calculateDelay(1, options);
152
+ expect(delay1).toBeGreaterThanOrEqual(1800);
153
+ expect(delay1).toBeLessThanOrEqual(2200);
154
+
155
+ const delay2 = retry.calculateDelay(2, options);
156
+ expect(delay2).toBeGreaterThanOrEqual(3600);
157
+ expect(delay2).toBeLessThanOrEqual(4400);
158
+ });
159
+
160
+ test('should not exceed maxDelay', () => {
161
+ const options = {
162
+ initialDelay: 1000,
163
+ maxDelay: 5000,
164
+ backoffMultiplier: 2
165
+ };
166
+
167
+ const delay = retry.calculateDelay(10, options);
168
+ expect(delay).toBeLessThanOrEqual(5500);
169
+ });
170
+ });
171
+
172
+ describe('isRetryableError', () => {
173
+ test('should return true for retryable error codes', () => {
174
+ const options = { retryableErrors: ['ECONNREFUSED', 'ETIMEDOUT'] };
175
+
176
+ expect(retry.isRetryableError({ code: 'ECONNREFUSED' }, options)).toBe(true);
177
+ expect(retry.isRetryableError({ code: 'ETIMEDOUT' }, options)).toBe(true);
178
+ });
179
+
180
+ test('should return true for retryable HTTP statuses', () => {
181
+ const options = { retryableStatuses: [500, 503] };
182
+
183
+ expect(retry.isRetryableError({ status: 500 }, options)).toBe(true);
184
+ expect(retry.isRetryableError({ status: 503 }, options)).toBe(true);
185
+ });
186
+
187
+ test('should return false for non-retryable errors', () => {
188
+ const options = {
189
+ retryableErrors: ['ECONNREFUSED'],
190
+ retryableStatuses: [500]
191
+ };
192
+
193
+ expect(retry.isRetryableError({ code: 'ENOMEM' }, options)).toBe(false);
194
+ expect(retry.isRetryableError({ status: 404 }, options)).toBe(false);
195
+ // Use truly non-retryable message
196
+ expect(retry.isRetryableError({ message: 'Invalid input provided' }, options)).toBe(false);
197
+ });
198
+ });
199
+
200
+ describe('retryUntil', () => {
201
+ test('should succeed when function eventually succeeds', async () => {
202
+ let attempts = 0;
203
+ const fn = async () => {
204
+ attempts++;
205
+ if (attempts < 3) {
206
+ throw new Error('Not ready yet');
207
+ }
208
+ return 'ready';
209
+ };
210
+
211
+ const result = await retry.retryUntil(fn, 5000, 50);
212
+
213
+ expect(result).toBe('ready');
214
+ expect(attempts).toBe(3);
215
+ });
216
+
217
+ test('should timeout when function never succeeds', async () => {
218
+ const fn = async () => {
219
+ throw new Error('Network error');
220
+ };
221
+
222
+ await expect(
223
+ retry.retryUntil(fn, 100, 20)
224
+ ).rejects.toThrow('Retry timeout');
225
+ });
226
+ });
227
+
228
+ describe('createRetryableFetch', () => {
229
+ test('should retry on 5xx errors', async () => {
230
+ let attempts = 0;
231
+ const mockFetch = async () => {
232
+ attempts++;
233
+ if (attempts === 1) {
234
+ throw { message: 'Server Error', status: 500 };
235
+ }
236
+ return { status: 200, data: 'ok' };
237
+ };
238
+
239
+ const wrappedFetch = retry.createRetryableFetch(mockFetch);
240
+ const response = await wrappedFetch('http://test.com');
241
+
242
+ expect(response.status).toBe(200);
243
+ expect(attempts).toBe(2);
244
+ });
245
+
246
+ test('should not retry on 4xx errors', async () => {
247
+ let attempts = 0;
248
+ const mockFetch = async () => {
249
+ attempts++;
250
+ return { status: 404, statusText: 'Not Found' };
251
+ };
252
+
253
+ const wrappedFetch = retry.createRetryableFetch(mockFetch);
254
+ const response = await wrappedFetch('http://test.com');
255
+
256
+ expect(response.status).toBe(404);
257
+ expect(attempts).toBe(1);
258
+ });
259
+ });
260
+
261
+ describe('retryBatch', () => {
262
+ test('should execute all functions with retries', async () => {
263
+ let callCount1 = 0;
264
+ let callCount3 = 0;
265
+ const fn1 = async () => {
266
+ callCount1++;
267
+ if (callCount1 === 1) throw new Error('Network timeout');
268
+ return 'result1';
269
+ };
270
+ const fn2 = async () => 'result2';
271
+ const fn3 = async () => {
272
+ callCount3++;
273
+ if (callCount3 < 3) throw new Error('Network timeout');
274
+ return 'result3';
275
+ };
276
+
277
+ const results = await retry.retryBatch([fn1, fn2, fn3], { maxRetries: 2 });
278
+
279
+ expect(results[0].status).toBe('fulfilled');
280
+ expect(results[0].value).toBe('result1');
281
+ expect(results[1].status).toBe('fulfilled');
282
+ expect(results[1].value).toBe('result2');
283
+ expect(results[2].status).toBe('fulfilled');
284
+ expect(results[2].value).toBe('result3');
285
+ });
286
+
287
+ test('should handle batch with all failures', async () => {
288
+ const fn1 = async () => { throw new Error('Network timeout'); };
289
+ const fn2 = async () => { throw new Error('Connection refused'); };
290
+
291
+ const results = await retry.retryBatch([fn1, fn2], { maxRetries: 1 });
292
+
293
+ expect(results[0].status).toBe('rejected');
294
+ expect(results[1].status).toBe('rejected');
295
+ });
296
+ });
297
+
298
+ describe('DEFAULT_OPTIONS', () => {
299
+ test('should have sensible defaults', () => {
300
+ expect(retry.DEFAULT_OPTIONS.maxRetries).toBe(3);
301
+ expect(retry.DEFAULT_OPTIONS.initialDelay).toBe(1000);
302
+ expect(retry.DEFAULT_OPTIONS.maxDelay).toBe(10000);
303
+ expect(retry.DEFAULT_OPTIONS.backoffMultiplier).toBe(2);
304
+ expect(retry.DEFAULT_OPTIONS.retryableStatuses).toContain(500);
305
+ expect(retry.DEFAULT_OPTIONS.retryableErrors).toContain('ECONNREFUSED');
306
+ });
307
+ });
308
+ });