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,374 @@
1
+ /**
2
+ * Performance Monitor Module
3
+ * Tracks dashboard performance metrics including refresh rates, memory usage, and worker pool stats
4
+ */
5
+
6
+ import os from 'os';
7
+ import si from 'systeminformation';
8
+ import logger from './logger.js';
9
+ import memoryPressure, { MemoryPressureDetector } from './memory-pressure.js';
10
+
11
+ // Worker pool reference (set via setWorkerPool)
12
+ let workerPoolRef = null;
13
+
14
+ /**
15
+ * Set the worker pool reference for metrics tracking
16
+ * @param {Object} pool - Worker pool instance
17
+ */
18
+ export function setWorkerPool(pool) {
19
+ workerPoolRef = pool;
20
+ }
21
+
22
+ /**
23
+ * Get worker pool metrics
24
+ * @returns {Object|null} Worker pool status or null if not available
25
+ */
26
+ export function getWorkerPoolMetrics() {
27
+ if (!workerPoolRef) {
28
+ return null;
29
+ }
30
+ try {
31
+ return workerPoolRef.getStatus();
32
+ } catch (error) {
33
+ logger.debug('Failed to get worker pool metrics:', error.message);
34
+ return null;
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Performance metrics snapshot
40
+ * @typedef {Object} PerformanceSnapshot
41
+ * @property {number} timestamp - Unix timestamp
42
+ * @property {number} refreshRate - Current refresh rate in ms
43
+ * @property {number} memoryUsed - Heap used in MB
44
+ * @property {number} memoryTotal - Heap total in MB
45
+ * @property {number} memoryPercent - Memory usage percentage
46
+ * @property {number} cpuPercent - CPU usage percentage (process-specific)
47
+ * @property {number} eventLoopLag - Event loop lag in ms
48
+ * @property {number} uptime - Process uptime in seconds
49
+ * @property {Object} operationTimings - Timing details for each operation
50
+ * @property {string|null} slowOperations - Summary of slow operations
51
+ * @property {number} totalRefreshTime - Total refresh cycle time in ms
52
+ */
53
+
54
+ class PerformanceMonitor {
55
+ constructor() {
56
+ /** @type {PerformanceSnapshot[]} */
57
+ this.history = [];
58
+ this.maxHistory = 60; // 2 minutes at 2s refresh
59
+ this.lastCheck = Date.now();
60
+ this.lastCPUUsage = process.cpuUsage();
61
+ this.isTracking = false;
62
+
63
+ // Memory pressure detector
64
+ this.memoryPressure = memoryPressure;
65
+ this.enableMemoryPressure = true;
66
+
67
+ // Metrics
68
+ this.metrics = {
69
+ avgRefreshRate: 0,
70
+ avgMemoryUsed: 0,
71
+ peakMemoryUsed: 0,
72
+ avgHeapUsed: 0,
73
+ peakHeapUsed: 0,
74
+ avgCpuPercent: 0,
75
+ avgEventLoopLag: 0,
76
+ slowOperations: null,
77
+ totalRefreshTime: 0,
78
+ };
79
+ }
80
+
81
+ /**
82
+ * Start performance tracking
83
+ */
84
+ start() {
85
+ this.isTracking = true;
86
+ this.lastCheck = Date.now();
87
+ this.lastCPUUsage = process.cpuUsage();
88
+ memoryPressure.start();
89
+ logger.debug('Performance monitoring started');
90
+ }
91
+
92
+ /**
93
+ * Stop performance tracking
94
+ */
95
+ stop() {
96
+ this.isTracking = false;
97
+ memoryPressure.stop();
98
+ logger.debug('Performance monitoring stopped');
99
+ }
100
+
101
+ /**
102
+ * Record a performance snapshot
103
+ * @param {number} refreshRate - Current refresh interval in ms
104
+ * @param {Object} timingDetails - Optional timing details from refresh operations
105
+ * @returns {PerformanceSnapshot}
106
+ */
107
+ async record(refreshRate = 2000, timingDetails = {}) {
108
+ if (!this.isTracking) {
109
+ return null;
110
+ }
111
+
112
+ const now = Date.now();
113
+ const memoryUsage = process.memoryUsage();
114
+ const cpuUsage = process.cpuUsage(this.lastCPUUsage);
115
+
116
+ // Get system memory for comparison
117
+ let systemMem = null;
118
+ try {
119
+ systemMem = await si.mem();
120
+ } catch (e) {
121
+ // Fallback to process memory only
122
+ }
123
+
124
+ // Calculate CPU percentage (user + system time / elapsed time)
125
+ const elapsedMs = now - this.lastCheck;
126
+ const cpuPercent = elapsedMs > 0
127
+ ? Math.min(100, ((cpuUsage.user + cpuUsage.system) / 1000) / elapsedMs * 100)
128
+ : 0;
129
+
130
+ // Calculate event loop lag (how much behind schedule we are)
131
+ const eventLoopLag = Math.max(0, now - this.lastCheck - refreshRate);
132
+
133
+ // Calculate system memory (excluding cache like Activity Monitor)
134
+ const systemUsed = systemMem
135
+ ? (systemMem.available ? systemMem.total - systemMem.available : systemMem.used)
136
+ : memoryUsage.heapUsed;
137
+ const systemTotal = systemMem ? systemMem.total : memoryUsage.heapTotal;
138
+ const systemPercent = systemMem && systemMem.total > 0
139
+ ? Math.round((systemUsed / systemMem.total) * 100)
140
+ : 0;
141
+
142
+ const snapshot = {
143
+ timestamp: now,
144
+ refreshRate,
145
+ // Node.js process memory (heap)
146
+ heapUsed: Math.round(memoryUsage.heapUsed / 1024 / 1024),
147
+ heapTotal: Math.round(memoryUsage.heapTotal / 1024 / 1024),
148
+ heapPercent: memoryUsage.heapTotal > 0
149
+ ? Math.round((memoryUsage.heapUsed / memoryUsage.heapTotal) * 100)
150
+ : 0,
151
+ // System memory (what Activity Monitor shows)
152
+ memoryUsed: Math.round(systemUsed / 1024 / 1024),
153
+ memoryTotal: Math.round(systemTotal / 1024 / 1024),
154
+ memoryPercent: systemPercent,
155
+ cpuPercent: Math.round(cpuPercent * 10) / 10,
156
+ eventLoopLag: Math.round(eventLoopLag),
157
+ uptime: Math.floor(process.uptime()),
158
+ operationTimings: timingDetails.operationTimings || {},
159
+ slowOperations: timingDetails.slowOperations || null,
160
+ totalRefreshTime: timingDetails.totalRefreshTime || 0,
161
+ };
162
+
163
+ // Add to history
164
+ this.history.push(snapshot);
165
+ if (this.history.length > this.maxHistory) {
166
+ this.history.shift();
167
+ }
168
+
169
+ // Update cached metrics
170
+ this._updateMetrics();
171
+
172
+ // Update for next iteration
173
+ this.lastCheck = now;
174
+ this.lastCPUUsage = process.cpuUsage();
175
+
176
+ return snapshot;
177
+ }
178
+
179
+ /**
180
+ * Update cached aggregate metrics
181
+ * @private
182
+ */
183
+ _updateMetrics() {
184
+ if (this.history.length === 0) {
185
+ return;
186
+ }
187
+
188
+ const count = this.history.length;
189
+ const sum = (arr, key) => arr.reduce((a, b) => a + (b[key] || 0), 0);
190
+
191
+ // Get latest for slow operations
192
+ const latest = this.history[this.history.length - 1];
193
+
194
+ this.metrics = {
195
+ avgRefreshRate: Math.round(sum(this.history, 'refreshRate') / count),
196
+ // System memory (Activity Monitor style)
197
+ avgMemoryUsed: Math.round(sum(this.history, 'memoryUsed') / count),
198
+ peakMemoryUsed: Math.max(...this.history.map(h => h.memoryUsed)),
199
+ // Heap memory (Node.js process)
200
+ avgHeapUsed: Math.round(sum(this.history, 'heapUsed') / count),
201
+ peakHeapUsed: Math.max(...this.history.map(h => h.heapUsed)),
202
+ avgCpuPercent: Math.round((sum(this.history, 'cpuPercent') / count) * 10) / 10,
203
+ avgEventLoopLag: Math.round(sum(this.history, 'eventLoopLag') / count),
204
+ slowOperations: latest.slowOperations || null,
205
+ totalRefreshTime: latest.totalRefreshTime || 0,
206
+ };
207
+ }
208
+
209
+ /**
210
+ * Get current metrics summary
211
+ * @returns {Object}
212
+ */
213
+ getMetrics() {
214
+ const latest = this.history[this.history.length - 1] || null;
215
+ return {
216
+ current: latest,
217
+ history: [...this.history],
218
+ aggregates: { ...this.metrics },
219
+ isTracking: this.isTracking,
220
+ };
221
+ }
222
+
223
+ /**
224
+ * Get formatted status string for display
225
+ * @param {boolean} detailed - Include detailed metrics
226
+ * @returns {string}
227
+ */
228
+ getStatusString(detailed = false) {
229
+ const latest = this.history[this.history.length - 1];
230
+ if (!latest) {
231
+ return 'Performance monitoring inactive';
232
+ }
233
+
234
+ const memoryColor = latest.memoryPercent >= 80 ? 'red-fg' :
235
+ latest.memoryPercent >= 60 ? 'yellow-fg' : 'green-fg';
236
+ const cpuColor = latest.cpuPercent >= 80 ? 'red-fg' :
237
+ latest.cpuPercent >= 50 ? 'yellow-fg' : 'green-fg';
238
+
239
+ // Show system memory (not heap) in footer
240
+ let status = `{${memoryColor}}MEM: ${latest.memoryUsed}MB (${latest.memoryPercent}%){/${memoryColor}`;
241
+
242
+ // Add memory pressure indicator if elevated
243
+ if (this.enableMemoryPressure) {
244
+ const pressureStatus = memoryPressure.getStatusString();
245
+ if (memoryPressure.isElevated()) {
246
+ status += ` | ${pressureStatus}`;
247
+ }
248
+ }
249
+
250
+ status += ` | {${cpuColor}}CPU: ${latest.cpuPercent}%{/${cpuColor}}`;
251
+ status += ` | Refresh: ${latest.refreshRate}ms`;
252
+
253
+ if (detailed && this.metrics.avgEventLoopLag > 0) {
254
+ const lagColor = this.metrics.avgEventLoopLag > 100 ? 'red-fg' :
255
+ this.metrics.avgEventLoopLag > 50 ? 'yellow-fg' : 'gray-fg';
256
+ status += ` | {${lagColor}}Lag: ${this.metrics.avgEventLoopLag}ms{/${lagColor}}`;
257
+ }
258
+
259
+ // Add worker pool metrics when available
260
+ const workerMetrics = getWorkerPoolMetrics();
261
+ if (workerMetrics) {
262
+ const workerColor = workerMetrics.pendingTasks > 0 ? 'yellow-fg' : 'green-fg';
263
+ const busyCount = workerMetrics.busyWorkers || 0;
264
+ const totalCount = workerMetrics.totalWorkers || 0;
265
+ const pendingCount = workerMetrics.pendingTasks || 0;
266
+ status += ` | {${workerColor}}Workers: ${busyCount}/${totalCount}{/${workerColor}}`;
267
+ if (pendingCount > 0) {
268
+ status += ` ({yellow-fg}${pendingCount} pending{/${yellow-fg}})`;
269
+ }
270
+ }
271
+
272
+ // Add memory pressure info when detailed and pressure is elevated
273
+ if (detailed && this.enableMemoryPressure) {
274
+ const pressureStatus = memoryPressure.getStatus();
275
+ if (pressureStatus.currentLevel !== 'none') {
276
+ const pressureColor = pressureStatus.currentLevel === 'emergency' ? 'red-fg' :
277
+ pressureStatus.currentLevel === 'critical' ? 'red-fg' :
278
+ pressureStatus.currentLevel === 'warning' ? 'yellow-fg' : 'cyan-fg';
279
+ status += ` | {${pressureColor}}Pressure: ${pressureStatus.currentLevel}{/${pressureColor}}`;
280
+ if (pressureStatus.trend?.direction === 'growing') {
281
+ status += ` {yellow-fg}↑${pressureStatus.trend.rateMBPerMin.toFixed(0)}MB/min{/}`;
282
+ }
283
+ }
284
+ }
285
+
286
+ return status;
287
+ }
288
+
289
+ /**
290
+ * Get memory usage sparkline data
291
+ * @param {number} points - Number of data points
292
+ * @returns {number[]}
293
+ */
294
+ getMemorySparkline(points = 30) {
295
+ const data = this.history.slice(-points).map(h => h.memoryUsed);
296
+ return data.length > 0 ? data : [0];
297
+ }
298
+
299
+ /**
300
+ * Get CPU usage sparkline data
301
+ * @param {number} points - Number of data points
302
+ * @returns {number[]}
303
+ */
304
+ getCpuSparkline(points = 30) {
305
+ const data = this.history.slice(-points).map(h => h.cpuPercent);
306
+ return data.length > 0 ? data : [0];
307
+ }
308
+
309
+ /**
310
+ * Check if performance is degraded
311
+ * @returns {{degraded: boolean, reasons: string[]}}
312
+ */
313
+ checkHealth() {
314
+ const reasons = [];
315
+ const latest = this.history[this.history.length - 1];
316
+
317
+ if (!latest) {
318
+ return { degraded: false, reasons: [] };
319
+ }
320
+
321
+ if (latest.memoryPercent >= 85) {
322
+ reasons.push(`High memory usage: ${latest.memoryPercent}%`);
323
+ }
324
+
325
+ if (latest.cpuPercent >= 80) {
326
+ reasons.push(`High CPU usage: ${latest.cpuPercent}%`);
327
+ }
328
+
329
+ if (this.metrics.avgEventLoopLag > 100) {
330
+ reasons.push(`Event loop lag: ${this.metrics.avgEventLoopLag}ms`);
331
+ }
332
+
333
+ // Check memory pressure
334
+ const pressureState = memoryPressure.check();
335
+ if (pressureState.level !== 'none' && pressureState.level !== 'elevated') {
336
+ reasons.push(`Memory pressure: ${pressureState.level} (${pressureState.heapUsedMB}MB)`);
337
+ }
338
+
339
+ return {
340
+ degraded: reasons.length > 0,
341
+ reasons,
342
+ };
343
+ }
344
+
345
+ /**
346
+ * Check memory pressure state
347
+ * @returns {import('./memory-pressure.js').PressureState}
348
+ */
349
+ checkMemoryPressure() {
350
+ return memoryPressure.check();
351
+ }
352
+
353
+ /**
354
+ * Reset all metrics and history
355
+ */
356
+ reset() {
357
+ this.history = [];
358
+ this.lastCheck = Date.now();
359
+ this.lastCPUUsage = process.cpuUsage();
360
+ this.metrics = {
361
+ avgRefreshRate: 0,
362
+ avgMemoryUsed: 0,
363
+ peakMemoryUsed: 0,
364
+ avgCpuPercent: 0,
365
+ avgEventLoopLag: 0,
366
+ };
367
+ memoryPressure.reset();
368
+ logger.debug('Performance metrics reset');
369
+ }
370
+ }
371
+
372
+ // Export singleton instance
373
+ export default new PerformanceMonitor();
374
+ export { PerformanceMonitor };