claw-dashboard 1.9.0 → 2.1.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 +5285 -566
  40. package/jest.config.js +11 -0
  41. package/man/clawdash.1 +276 -0
  42. package/package.json +54 -6
  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 +1934 -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 +1056 -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,327 @@
1
+ /**
2
+ * Differential Rendering Module
3
+ * Optimizes blessed screen rendering by tracking widget state and only
4
+ * updating components that have actually changed.
5
+ */
6
+
7
+ import logger from './logger.js';
8
+
9
+ /**
10
+ * Widget state tracker for differential rendering
11
+ * Tracks content, styles, and labels to minimize unnecessary re-renders
12
+ */
13
+ export class WidgetStateTracker {
14
+ constructor() {
15
+ this.states = new Map();
16
+ this.stats = {
17
+ totalUpdates: 0,
18
+ skippedUpdates: 0,
19
+ actualUpdates: 0,
20
+ screenRenders: 0,
21
+ skippedRenders: 0
22
+ };
23
+ }
24
+
25
+ /**
26
+ * Generate a hash for comparing complex objects
27
+ * @param {*} value - Value to hash
28
+ * @returns {string} Hash string
29
+ */
30
+ _hash(value) {
31
+ if (value === null || value === undefined) return String(value);
32
+ if (typeof value === 'object') return JSON.stringify(value);
33
+ return String(value);
34
+ }
35
+
36
+ /**
37
+ * Track widget content update
38
+ * @param {string} widgetId - Unique widget identifier
39
+ * @param {string} newContent - New content to set
40
+ * @param {Function} updateFn - Function to call if update is needed
41
+ * @returns {boolean} True if update was performed
42
+ */
43
+ trackContent(widgetId, newContent, updateFn) {
44
+ const key = `${widgetId}:content`;
45
+ const current = this.states.get(key);
46
+ const newValue = String(newContent ?? '');
47
+
48
+ this.stats.totalUpdates++;
49
+
50
+ if (current === newValue) {
51
+ this.stats.skippedUpdates++;
52
+ return false;
53
+ }
54
+
55
+ this.states.set(key, newValue);
56
+ this.stats.actualUpdates++;
57
+ updateFn(newValue);
58
+ return true;
59
+ }
60
+
61
+ /**
62
+ * Track style update for a widget
63
+ * @param {string} widgetId - Unique widget identifier
64
+ * @param {string} styleProp - Style property name
65
+ * @param {*} newValue - New style value
66
+ * @param {Function} updateFn - Function to call if update is needed
67
+ * @returns {boolean} True if update was performed
68
+ */
69
+ trackStyle(widgetId, styleProp, newValue, updateFn) {
70
+ const key = `${widgetId}:style:${styleProp}`;
71
+ const current = this.states.get(key);
72
+ const hashedNew = this._hash(newValue);
73
+
74
+ if (current === hashedNew) {
75
+ return false;
76
+ }
77
+
78
+ this.states.set(key, hashedNew);
79
+ updateFn(newValue);
80
+ return true;
81
+ }
82
+
83
+ /**
84
+ * Track label update for a widget
85
+ * @param {string} widgetId - Unique widget identifier
86
+ * @param {string} newLabel - New label value
87
+ * @param {Function} updateFn - Function to call if update is needed
88
+ * @returns {boolean} True if update was performed
89
+ */
90
+ trackLabel(widgetId, newLabel, updateFn) {
91
+ const key = `${widgetId}:label`;
92
+ const current = this.states.get(key);
93
+ const newValue = String(newLabel ?? '');
94
+
95
+ if (current === newValue) {
96
+ return false;
97
+ }
98
+
99
+ this.states.set(key, newValue);
100
+ updateFn(newValue);
101
+ return true;
102
+ }
103
+
104
+ /**
105
+ * Reset state for a specific widget (e.g., after resize)
106
+ * @param {string} widgetId - Widget identifier
107
+ */
108
+ resetWidget(widgetId) {
109
+ for (const key of this.states.keys()) {
110
+ if (key.startsWith(`${widgetId}:`)) {
111
+ this.states.delete(key);
112
+ }
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Reset all tracked state
118
+ */
119
+ resetAll() {
120
+ this.states.clear();
121
+ logger.debug('Differential render state reset');
122
+ }
123
+
124
+ /**
125
+ * Get current statistics
126
+ * @returns {Object} Statistics object
127
+ */
128
+ getStats() {
129
+ const efficiency = this.stats.totalUpdates > 0
130
+ ? ((this.stats.skippedUpdates / this.stats.totalUpdates) * 100).toFixed(1)
131
+ : 0;
132
+ const renderEfficiency = this.stats.screenRenders > 0
133
+ ? ((this.stats.skippedRenders / (this.stats.screenRenders + this.stats.skippedRenders)) * 100).toFixed(1)
134
+ : 0;
135
+
136
+ return {
137
+ ...this.stats,
138
+ efficiency: `${efficiency}%`,
139
+ renderEfficiency: `${renderEfficiency}%`,
140
+ trackedWidgets: this.states.size
141
+ };
142
+ }
143
+
144
+ /**
145
+ * Reset statistics
146
+ */
147
+ resetStats() {
148
+ this.stats = {
149
+ totalUpdates: 0,
150
+ skippedUpdates: 0,
151
+ actualUpdates: 0,
152
+ screenRenders: 0,
153
+ skippedRenders: 0
154
+ };
155
+ }
156
+ }
157
+
158
+ /**
159
+ * Differential renderer that batches updates and minimizes screen.render() calls
160
+ */
161
+ export class DifferentialRenderer {
162
+ constructor(screen) {
163
+ this.screen = screen;
164
+ this.tracker = new WidgetStateTracker();
165
+ this.pendingChanges = new Set();
166
+ this.batchMode = false;
167
+ this.deferredRender = null;
168
+ }
169
+
170
+ /**
171
+ * Start batching mode - screen.render() will be deferred
172
+ */
173
+ beginBatch() {
174
+ this.batchMode = true;
175
+ this.pendingChanges.clear();
176
+ }
177
+
178
+ /**
179
+ * End batching and render if changes occurred
180
+ */
181
+ endBatch() {
182
+ this.batchMode = false;
183
+ const hasChanges = this.pendingChanges.size > 0;
184
+ this.pendingChanges.clear();
185
+
186
+ if (hasChanges) {
187
+ this.tracker.stats.screenRenders++;
188
+ this.screen.render();
189
+ return true;
190
+ } else {
191
+ this.tracker.stats.skippedRenders++;
192
+ return false;
193
+ }
194
+ }
195
+
196
+ /**
197
+ * Request a screen render, may be deferred in batch mode
198
+ */
199
+ requestRender() {
200
+ if (this.batchMode) {
201
+ this.pendingChanges.add('render');
202
+ } else {
203
+ this.tracker.stats.screenRenders++;
204
+ this.screen.render();
205
+ }
206
+ }
207
+
208
+ /**
209
+ * Set widget content with differential tracking
210
+ * @param {string} widgetId - Widget identifier
211
+ * @param {Object} widget - Blessed widget
212
+ * @param {string} content - New content
213
+ * @returns {boolean} True if content was updated
214
+ */
215
+ setContent(widgetId, widget, content) {
216
+ if (!widget || widget.destroyed) return false;
217
+
218
+ const changed = this.tracker.trackContent(widgetId, content, (newContent) => {
219
+ widget.setContent(newContent);
220
+ this.pendingChanges.add(widgetId);
221
+ });
222
+
223
+ return changed;
224
+ }
225
+
226
+ /**
227
+ * Set widget style property with differential tracking
228
+ * @param {string} widgetId - Widget identifier
229
+ * @param {Object} widget - Blessed widget
230
+ * @param {string} prop - Style property
231
+ * @param {*} value - New value
232
+ * @returns {boolean} True if style was updated
233
+ */
234
+ setStyle(widgetId, widget, prop, value) {
235
+ if (!widget || widget.destroyed) return false;
236
+
237
+ const changed = this.tracker.trackStyle(widgetId, prop, value, (newValue) => {
238
+ widget.style[prop] = newValue;
239
+ this.pendingChanges.add(`${widgetId}:style`);
240
+ });
241
+
242
+ return changed;
243
+ }
244
+
245
+ /**
246
+ * Set widget foreground color with differential tracking
247
+ * @param {string} widgetId - Widget identifier
248
+ * @param {Object} widget - Blessed widget
249
+ * @param {string} color - New color
250
+ * @returns {boolean} True if color was updated
251
+ */
252
+ setFg(widgetId, widget, color) {
253
+ return this.setStyle(widgetId, widget, 'fg', color);
254
+ }
255
+
256
+ /**
257
+ * Set border color with differential tracking
258
+ * @param {string} widgetId - Widget identifier
259
+ * @param {Object} widget - Blessed widget with border
260
+ * @param {string} color - New color
261
+ * @returns {boolean} True if border was updated
262
+ */
263
+ setBorderFg(widgetId, widget, color) {
264
+ if (!widget || widget.destroyed || !widget.style.border) return false;
265
+
266
+ const changed = this.tracker.trackStyle(widgetId, 'border.fg', color, (newValue) => {
267
+ widget.style.border.fg = newValue;
268
+ this.pendingChanges.add(`${widgetId}:border`);
269
+ });
270
+
271
+ return changed;
272
+ }
273
+
274
+ /**
275
+ * Set widget label with differential tracking
276
+ * @param {string} widgetId - Widget identifier
277
+ * @param {Object} widget - Blessed widget
278
+ * @param {string} label - New label
279
+ * @returns {boolean} True if label was updated
280
+ */
281
+ setLabel(widgetId, widget, label) {
282
+ if (!widget || widget.destroyed) return false;
283
+
284
+ const changed = this.tracker.trackLabel(widgetId, label, (newLabel) => {
285
+ widget.setLabel(newLabel);
286
+ this.pendingChanges.add(`${widgetId}:label`);
287
+ });
288
+
289
+ return changed;
290
+ }
291
+
292
+ /**
293
+ * Reset state for a widget (useful after resize events)
294
+ * @param {string} widgetId - Widget identifier
295
+ */
296
+ resetWidget(widgetId) {
297
+ this.tracker.resetWidget(widgetId);
298
+ }
299
+
300
+ /**
301
+ * Reset all tracked state
302
+ */
303
+ resetAll() {
304
+ this.tracker.resetAll();
305
+ this.pendingChanges.clear();
306
+ }
307
+
308
+ /**
309
+ * Get rendering statistics
310
+ * @returns {Object} Statistics
311
+ */
312
+ getStats() {
313
+ return this.tracker.getStats();
314
+ }
315
+
316
+ /**
317
+ * Reset statistics
318
+ */
319
+ resetStats() {
320
+ this.tracker.resetStats();
321
+ }
322
+ }
323
+
324
+ export default {
325
+ WidgetStateTracker,
326
+ DifferentialRenderer
327
+ };
package/src/errors.js ADDED
@@ -0,0 +1,169 @@
1
+ /**
2
+ * Custom error classes for Claw Dashboard
3
+ * Provides specific error types for better error handling and debugging
4
+ * @module errors
5
+ */
6
+
7
+ /**
8
+ * Base error class for all dashboard-specific errors
9
+ * @extends Error
10
+ */
11
+ export class DashboardError extends Error {
12
+ constructor(message, code = 'DASHBOARD_ERROR', details = {}) {
13
+ super(message);
14
+ this.name = 'DashboardError';
15
+ this.code = code;
16
+ this.details = details;
17
+ this.timestamp = new Date().toISOString();
18
+ Error.captureStackTrace(this, this.constructor);
19
+ }
20
+
21
+ toJSON() {
22
+ return {
23
+ name: this.name,
24
+ message: this.message,
25
+ code: this.code,
26
+ details: this.details,
27
+ timestamp: this.timestamp,
28
+ stack: this.stack
29
+ };
30
+ }
31
+ }
32
+
33
+ // Configuration related errors
34
+ export class ConfigError extends DashboardError {
35
+ constructor(message, details = {}) {
36
+ super(message, 'CONFIG_ERROR', details);
37
+ this.name = 'ConfigError';
38
+ }
39
+ }
40
+
41
+ // Settings validation or load/save errors
42
+ export class SettingsError extends DashboardError {
43
+ constructor(message, details = {}) {
44
+ super(message, 'SETTINGS_ERROR', details);
45
+ this.name = 'SettingsError';
46
+ }
47
+ }
48
+
49
+ // Gateway/OpenClaw communication errors
50
+ export class GatewayError extends DashboardError {
51
+ constructor(message, details = {}) {
52
+ super(message, 'GATEWAY_ERROR', details);
53
+ this.name = 'GatewayError';
54
+ }
55
+ }
56
+
57
+ // Session-related errors
58
+ export class SessionError extends DashboardError {
59
+ constructor(message, details = {}) {
60
+ super(message, 'SESSION_ERROR', details);
61
+ this.name = 'SessionError';
62
+ }
63
+ }
64
+
65
+ // Data fetch errors (systeminformation, CLI commands)
66
+ export class DataFetchError extends DashboardError {
67
+ constructor(message, details = {}) {
68
+ super(message, 'DATA_FETCH_ERROR', details);
69
+ this.name = 'DataFetchError';
70
+ }
71
+ }
72
+
73
+ // Authentication/authorization errors
74
+ export class AuthError extends DashboardError {
75
+ constructor(message, details = {}) {
76
+ super(message, 'AUTH_ERROR', details);
77
+ this.name = 'AuthError';
78
+ }
79
+ }
80
+
81
+ // Network-related errors
82
+ export class NetworkError extends DashboardError {
83
+ constructor(message, details = {}) {
84
+ super(message, 'NETWORK_ERROR', details);
85
+ this.name = 'NetworkError';
86
+ }
87
+ }
88
+
89
+ // UI/Rendering errors
90
+ export class UIError extends DashboardError {
91
+ constructor(message, details = {}) {
92
+ super(message, 'UI_ERROR', details);
93
+ this.name = 'UIError';
94
+ }
95
+ }
96
+
97
+ // Database errors
98
+ export class DatabaseError extends DashboardError {
99
+ constructor(message, details = {}) {
100
+ super(message, 'DATABASE_ERROR', details);
101
+ this.name = 'DatabaseError';
102
+ }
103
+ }
104
+
105
+ // Validation errors
106
+ export class ValidationError extends DashboardError {
107
+ constructor(message, details = {}) {
108
+ super(message, 'VALIDATION_ERROR', details);
109
+ this.name = 'ValidationError';
110
+ }
111
+ }
112
+
113
+ // Timeout errors
114
+ export class TimeoutError extends DashboardError {
115
+ constructor(message, details = {}) {
116
+ super(message, 'TIMEOUT_ERROR', details);
117
+ this.name = 'TimeoutError';
118
+ }
119
+ }
120
+
121
+ // Worker pool overload errors
122
+ export class WorkerPoolOverloadError extends DashboardError {
123
+ constructor(message, details = {}) {
124
+ super(message, 'WORKER_POOL_OVERLOAD', details);
125
+ this.name = 'WorkerPoolOverloadError';
126
+ this.degradationLevel = details.degradationLevel || 'none';
127
+ this.queueSize = details.queueSize || 0;
128
+ this.utilizationPercent = details.utilizationPercent || 0;
129
+ }
130
+ }
131
+
132
+ // Checksum verification errors
133
+ export class ChecksumError extends DashboardError {
134
+ constructor(message, details = {}) {
135
+ super(message, 'CHECKSUM_ERROR', details);
136
+ this.name = 'ChecksumError';
137
+ }
138
+ }
139
+
140
+ // Error code constants for programmatic handling
141
+ export const ERROR_CODES = {
142
+ CONFIG_ERROR: 'CONFIG_ERROR',
143
+ SETTINGS_ERROR: 'SETTINGS_ERROR',
144
+ GATEWAY_ERROR: 'GATEWAY_ERROR',
145
+ SESSION_ERROR: 'SESSION_ERROR',
146
+ DATA_FETCH_ERROR: 'DATA_FETCH_ERROR',
147
+ AUTH_ERROR: 'AUTH_ERROR',
148
+ NETWORK_ERROR: 'NETWORK_ERROR',
149
+ UI_ERROR: 'UI_ERROR',
150
+ DATABASE_ERROR: 'DATABASE_ERROR',
151
+ VALIDATION_ERROR: 'VALIDATION_ERROR',
152
+ TIMEOUT_ERROR: 'TIMEOUT_ERROR',
153
+ CHECKSUM_ERROR: 'CHECKSUM_ERROR',
154
+ WORKER_POOL_OVERLOAD: 'WORKER_POOL_OVERLOAD',
155
+ DASHBOARD_ERROR: 'DASHBOARD_ERROR'
156
+ };
157
+
158
+ // Helper to check if error is a dashboard error
159
+ export function isDashboardError(error) {
160
+ return error instanceof DashboardError;
161
+ }
162
+
163
+ // Helper to get error code or default
164
+ export function getErrorCode(error) {
165
+ if (error instanceof DashboardError) {
166
+ return error.code;
167
+ }
168
+ return 'UNKNOWN_ERROR';
169
+ }