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
package/src/alerts.js ADDED
@@ -0,0 +1,693 @@
1
+ /**
2
+ * Alert notifications module for Claw Dashboard
3
+ * Monitors system metrics (CPU, memory, disk) against configurable thresholds
4
+ * and provides visual notifications in the Dashboard UI
5
+ */
6
+
7
+ import logger from './logger.js';
8
+ import config from './config.js';
9
+ import { getCurrentTheme } from './themes.js';
10
+ import process from 'process';
11
+
12
+ // Default threshold configurations (normalized to lowercase keys)
13
+ const DEFAULT_THRESHOLDS = {
14
+ cpu: { warning: config.ALERT_THRESHOLDS.CPU.warning, critical: config.ALERT_THRESHOLDS.CPU.critical },
15
+ memory: { warning: config.ALERT_THRESHOLDS.MEMORY.warning, critical: config.ALERT_THRESHOLDS.MEMORY.critical },
16
+ disk: { warning: config.ALERT_THRESHOLDS.DISK.warning, critical: config.ALERT_THRESHOLDS.DISK.critical }
17
+ };
18
+
19
+ // Alert levels
20
+ const AlertLevel = {
21
+ INFO: 'info',
22
+ WARNING: 'warning',
23
+ CRITICAL: 'critical',
24
+ CLEARED: 'cleared'
25
+ };
26
+
27
+ // Sound notification configuration
28
+ let soundConfig = {
29
+ enabled: false,
30
+ soundType: 'bell', // 'bell' = terminal bell, 'beep' = system beep
31
+ warningEnabled: true,
32
+ criticalEnabled: true
33
+ };
34
+
35
+ // Alert state storage
36
+ let alerts = [];
37
+ let thresholds = { ...DEFAULT_THRESHOLDS };
38
+ let alertHistory = [];
39
+ const MAX_HISTORY = config.MAX_ALERT_HISTORY;
40
+
41
+ // Rate limiting configuration
42
+ const DEFAULT_RATE_LIMIT = config.ALERT_RATE_LIMIT;
43
+
44
+ // Rate limiting state (normalized to lowercase keys matching tests)
45
+ let rateLimit = {
46
+ enabled: config.ALERT_RATE_LIMIT.ENABLED,
47
+ windowMs: config.ALERT_RATE_LIMIT.WINDOW_MS,
48
+ maxAlerts: config.ALERT_RATE_LIMIT.MAX_ALERTS
49
+ };
50
+ let alertTimestamps = {}; // Track timestamps per alert type: { cpu: [ts1, ts2, ...] }
51
+
52
+ /**
53
+ * Check if alert should be rate-limited
54
+ * @param {string} type - Alert type (cpu, memory, disk)
55
+ * @returns {boolean} True if alert should be suppressed
56
+ */
57
+ function shouldRateLimitAlert(type) {
58
+ if (!rateLimit.enabled) {
59
+ return false;
60
+ }
61
+
62
+ const now = Date.now();
63
+ const timestamps = alertTimestamps[type] || [];
64
+
65
+ // Clean up old timestamps outside the window
66
+ const validTimestamps = timestamps.filter(ts => now - ts < rateLimit.windowMs);
67
+
68
+ // Check if we've exceeded the max alerts in this window
69
+ if (validTimestamps.length >= rateLimit.maxAlerts) {
70
+ logger.debug(`[RATE LIMIT] Alert for ${type} suppressed - rate limit exceeded (${validTimestamps.length}/${rateLimit.maxAlerts} in ${rateLimit.windowMs}ms)`);
71
+ return true;
72
+ }
73
+
74
+ // Update timestamps array (cleanup only, actual recording happens in recordAlertTimestamp)
75
+ alertTimestamps[type] = validTimestamps;
76
+
77
+ return false;
78
+ }
79
+
80
+ /**
81
+ * Record an alert timestamp for rate limiting
82
+ * @param {string} type - Alert type
83
+ */
84
+ function recordAlertTimestamp(type) {
85
+ if (!rateLimit.enabled) {
86
+ return;
87
+ }
88
+
89
+ if (!alertTimestamps[type]) {
90
+ alertTimestamps[type] = [];
91
+ }
92
+ alertTimestamps[type].push(Date.now());
93
+ }
94
+
95
+ /**
96
+ * Set rate limiting configuration
97
+ * @param {object} config - Rate limit configuration
98
+ */
99
+ function setRateLimit(config) {
100
+ rateLimit = { ...rateLimit, ...config };
101
+ logger.info(`Rate limiting updated: enabled=${rateLimit.enabled}, window=${rateLimit.windowMs}ms, max=${rateLimit.maxAlerts}`);
102
+ }
103
+
104
+ /**
105
+ * Get current rate limiting configuration
106
+ * @returns {object} Current rate limit config
107
+ */
108
+ function getRateLimit() {
109
+ return { ...rateLimit };
110
+ }
111
+
112
+ /**
113
+ * Reset rate limiting state (useful for testing)
114
+ */
115
+ function resetRateLimit() {
116
+ alertTimestamps = {};
117
+ rateLimit = {
118
+ enabled: config.ALERT_RATE_LIMIT.ENABLED,
119
+ windowMs: config.ALERT_RATE_LIMIT.WINDOW_MS,
120
+ maxAlerts: config.ALERT_RATE_LIMIT.MAX_ALERTS
121
+ };
122
+ }
123
+
124
+ /**
125
+ * Create a new alert object
126
+ * @param {string} type - Alert type (cpu, memory, disk)
127
+ * @param {string} level - Alert level (info, warning, critical)
128
+ * @param {number} value - Current value
129
+ * @param {number} threshold - Threshold that was exceeded
130
+ * @returns {object} Alert object
131
+ */
132
+ function createAlert(type, level, value, threshold) {
133
+ return {
134
+ id: `${type}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
135
+ type,
136
+ level,
137
+ value,
138
+ threshold,
139
+ timestamp: new Date().toISOString(),
140
+ message: getAlertMessage(type, level, value, threshold),
141
+ dismissed: false
142
+ };
143
+ }
144
+
145
+ /**
146
+ * Generate alert message based on type and level
147
+ */
148
+ function getAlertMessage(type, level, value, threshold) {
149
+ const typeNames = {
150
+ cpu: 'CPU usage',
151
+ memory: 'Memory usage',
152
+ disk: 'Disk usage'
153
+ };
154
+
155
+ const levelNames = {
156
+ info: 'Information',
157
+ warning: 'Warning',
158
+ critical: 'Critical',
159
+ cleared: 'Resolved'
160
+ };
161
+
162
+ if (level === AlertLevel.CLEARED) {
163
+ return `${typeNames[type]} normalized (${value}% - was above ${threshold}%)`;
164
+ }
165
+
166
+ return `${levelNames[level]}: ${typeNames[type]} at ${value}% (threshold: ${threshold}%)`;
167
+ }
168
+
169
+ /**
170
+ * Check a metric against thresholds and create alert if needed
171
+ * @param {string} type - Metric type (cpu, memory, disk)
172
+ * @param {number} value - Current value (0-100)
173
+ * @returns {object|null} New alert if created, null otherwise
174
+ */
175
+ function checkThreshold(type, value) {
176
+ if (!thresholds[type]) {
177
+ logger.warn(`Unknown alert type: ${type}`);
178
+ return null;
179
+ }
180
+
181
+ const { warning, critical } = thresholds[type];
182
+ const existingAlert = alerts.find(a => a.type === type && !a.dismissed);
183
+
184
+ // Check for critical level
185
+ if (value >= critical) {
186
+ if (!existingAlert || existingAlert.level !== AlertLevel.CRITICAL) {
187
+ // Use RateLimiter.checkAndRecord for atomic check and record
188
+ const rateLimitResult = thresholdRateLimiter.checkAndRecord(type, AlertLevel.CRITICAL);
189
+ if (rateLimitResult.allowed) {
190
+ const alert = createAlert(type, AlertLevel.CRITICAL, value, critical);
191
+ addAlert(alert);
192
+ return alert;
193
+ }
194
+ }
195
+ // Update existing alert if it exists and is already critical
196
+ if (existingAlert) {
197
+ existingAlert.value = value;
198
+ existingAlert.timestamp = new Date().toISOString();
199
+ }
200
+ return null;
201
+ }
202
+
203
+ // Check for warning level
204
+ if (value >= warning) {
205
+ if (!existingAlert || existingAlert.level === AlertLevel.CLEARED) {
206
+ // Use RateLimiter.checkAndRecord for atomic check and record
207
+ const rateLimitResult = thresholdRateLimiter.checkAndRecord(type, AlertLevel.WARNING);
208
+ if (rateLimitResult.allowed) {
209
+ const alert = createAlert(type, AlertLevel.WARNING, value, warning);
210
+ addAlert(alert);
211
+ return alert;
212
+ }
213
+ }
214
+ // Update existing alert if it exists
215
+ if (existingAlert) {
216
+ existingAlert.value = value;
217
+ existingAlert.timestamp = new Date().toISOString();
218
+ }
219
+ return null;
220
+ }
221
+
222
+ // Value below threshold - clear existing alert
223
+ if (existingAlert) {
224
+ // Always allow cleared alerts through
225
+ const clearedAlert = createAlert(type, AlertLevel.CLEARED, value, existingAlert.threshold);
226
+ dismissAlert(existingAlert.id);
227
+ addAlert(clearedAlert);
228
+ return clearedAlert;
229
+ }
230
+
231
+ return null;
232
+ }
233
+
234
+ /**
235
+ * Play sound notification for alert
236
+ * @param {string} level - Alert level
237
+ */
238
+ function playAlertSound(level) {
239
+ if (!soundConfig.enabled) {
240
+ return;
241
+ }
242
+
243
+ // Check if sound is enabled for this level
244
+ if (level === AlertLevel.WARNING && !soundConfig.warningEnabled) {
245
+ return;
246
+ }
247
+ if (level === AlertLevel.CRITICAL && !soundConfig.criticalEnabled) {
248
+ return;
249
+ }
250
+
251
+ try {
252
+ if (soundConfig.soundType === 'bell') {
253
+ // Terminal bell character
254
+ process.stdout.write('\x07');
255
+ } else if (soundConfig.soundType === 'beep') {
256
+ // Multiple bells for critical
257
+ const count = level === AlertLevel.CRITICAL ? 3 : 1;
258
+ process.stdout.write('\x07'.repeat(count));
259
+ }
260
+ } catch (err) {
261
+ logger.debug(`Failed to play sound: ${err.message}`);
262
+ }
263
+ }
264
+
265
+ /**
266
+ * Set sound notification configuration
267
+ * @param {object} config - Sound configuration
268
+ */
269
+ function setSoundConfig(config) {
270
+ soundConfig = { ...soundConfig, ...config };
271
+ logger.info(`Sound notifications: enabled=${soundConfig.enabled}, type=${soundConfig.soundType}`);
272
+ }
273
+
274
+ /**
275
+ * Get current sound configuration
276
+ * @returns {object} Current sound config
277
+ */
278
+ function getSoundConfig() {
279
+ return { ...soundConfig };
280
+ }
281
+
282
+ /**
283
+ * Toggle sound notifications
284
+ * @param {boolean} enabled - Enable or disable
285
+ */
286
+ function toggleSound(enabled) {
287
+ soundConfig.enabled = enabled;
288
+ logger.info(`Sound notifications ${enabled ? 'enabled' : 'disabled'}`);
289
+ }
290
+
291
+ /**
292
+ * Add an alert to the active alerts list and history
293
+ */
294
+ function addAlert(alert) {
295
+ // Remove duplicates of same type
296
+ alerts = alerts.filter(a => a.type !== alert.type || a.dismissed);
297
+ alerts.push(alert);
298
+
299
+ // Add to history
300
+ alertHistory.push(alert);
301
+ if (alertHistory.length > MAX_HISTORY) {
302
+ alertHistory = alertHistory.slice(-MAX_HISTORY);
303
+ }
304
+
305
+ // Play sound for warning and critical alerts (not cleared)
306
+ if (alert.level === AlertLevel.WARNING || alert.level === AlertLevel.CRITICAL) {
307
+ playAlertSound(alert.level);
308
+ }
309
+
310
+ logger.info(`[ALERT] ${alert.message}`);
311
+ }
312
+
313
+ /**
314
+ * Dismiss an active alert
315
+ * @param {string} id - Alert ID to dismiss
316
+ */
317
+ function dismissAlert(id) {
318
+ const alert = alerts.find(a => a.id === id);
319
+ if (alert) {
320
+ alert.dismissed = true;
321
+ alert.timestamp = new Date().toISOString();
322
+ }
323
+ }
324
+
325
+ /**
326
+ * Get all active (non-dismissed) alerts
327
+ * @returns {object[]} Active alerts
328
+ */
329
+ function getActiveAlerts() {
330
+ return alerts.filter(a => !a.dismissed);
331
+ }
332
+
333
+ /**
334
+ * Get all alerts of a specific level
335
+ * @param {string} level - Alert level to filter
336
+ * @returns {object[]} Filtered alerts
337
+ */
338
+ function getAlertsByLevel(level) {
339
+ return alerts.filter(a => a.level === level && !a.dismissed);
340
+ }
341
+
342
+ /**
343
+ * Get alert history
344
+ * @returns {object[]} Alert history
345
+ */
346
+ function getAlertHistory() {
347
+ return [...alertHistory];
348
+ }
349
+
350
+ /**
351
+ * Clear all alerts
352
+ */
353
+ function clearAllAlerts() {
354
+ alerts.forEach(a => a.dismissed = true);
355
+ }
356
+
357
+ /**
358
+ * Set custom thresholds
359
+ * @param {object} newThresholds - New threshold configuration
360
+ */
361
+ function setThresholds(newThresholds) {
362
+ if (newThresholds.cpu) {
363
+ thresholds.cpu = { ...thresholds.cpu, ...newThresholds.cpu };
364
+ }
365
+ if (newThresholds.memory) {
366
+ thresholds.memory = { ...thresholds.memory, ...newThresholds.memory };
367
+ }
368
+ if (newThresholds.disk) {
369
+ thresholds.disk = { ...thresholds.disk, ...newThresholds.disk };
370
+ }
371
+ logger.info(`Alert thresholds updated: CPU ${thresholds.cpu.warning}/${thresholds.cpu.critical}%, Memory ${thresholds.memory.warning}/${thresholds.memory.critical}%, Disk ${thresholds.disk.warning}/${thresholds.disk.critical}%`);
372
+ }
373
+
374
+ /**
375
+ * Get current thresholds
376
+ * @returns {object} Current thresholds
377
+ */
378
+ function getThresholds() {
379
+ return { ...thresholds };
380
+ }
381
+
382
+ /**
383
+ * Reset thresholds to defaults
384
+ */
385
+ function resetThresholds() {
386
+ thresholds = { ...DEFAULT_THRESHOLDS };
387
+ logger.info('Alert thresholds reset to defaults');
388
+ }
389
+
390
+ /**
391
+ * Check all system metrics and generate alerts
392
+ * @param {object} metrics - Object with cpu, memory, disk values
393
+ * @returns {object[]} Array of new alerts generated
394
+ */
395
+ function checkAllMetrics(metrics) {
396
+ const newAlerts = [];
397
+
398
+ if (metrics && metrics.cpu !== undefined) {
399
+ const alert = checkThreshold('cpu', metrics.cpu);
400
+ if (alert) newAlerts.push(alert);
401
+ }
402
+
403
+ if (metrics && metrics.memory !== undefined) {
404
+ const alert = checkThreshold('memory', metrics.memory);
405
+ if (alert) newAlerts.push(alert);
406
+ }
407
+
408
+ if (metrics && metrics.disk !== undefined) {
409
+ const alert = checkThreshold('disk', metrics.disk);
410
+ if (alert) newAlerts.push(alert);
411
+ }
412
+
413
+ return newAlerts;
414
+ }
415
+
416
+ /**
417
+ * Get alert color based on level and current theme
418
+ * @param {string} level - Alert level
419
+ * @returns {string} Color name
420
+ */
421
+ function getAlertColor(level) {
422
+ const theme = getCurrentTheme();
423
+ const colorMap = {
424
+ [AlertLevel.INFO]: theme.colors.alert?.info || 'cyan',
425
+ [AlertLevel.WARNING]: theme.colors.alert?.warning || 'yellow',
426
+ [AlertLevel.CRITICAL]: theme.colors.alert?.error || 'red',
427
+ [AlertLevel.CLEARED]: theme.colors.alert?.success || 'green'
428
+ };
429
+ return colorMap[level] || 'white';
430
+ }
431
+
432
+ /**
433
+ * Format alert for display in UI
434
+ * @param {object} alert - Alert object
435
+ * @returns {string} Formatted string
436
+ */
437
+ function formatAlert(alert) {
438
+ const color = getAlertColor(alert.level);
439
+ const icon = getAlertIcon(alert.level);
440
+ return `{${color}-fg}${icon} ${alert.message}{/}`;
441
+ }
442
+
443
+ /**
444
+ * Get icon for alert level
445
+ */
446
+ function getAlertIcon(level) {
447
+ const icons = {
448
+ [AlertLevel.INFO]: 'ℹ',
449
+ [AlertLevel.WARNING]: '⚠',
450
+ [AlertLevel.CRITICAL]: '✖',
451
+ [AlertLevel.CLEARED]: '✓'
452
+ };
453
+ return icons[level] || '•';
454
+ }
455
+
456
+ /**
457
+ * Get count of active alerts by level
458
+ * @returns {object} Counts by level
459
+ */
460
+ function getAlertCounts() {
461
+ const active = getActiveAlerts();
462
+ return {
463
+ info: active.filter(a => a.level === AlertLevel.INFO).length,
464
+ warning: active.filter(a => a.level === AlertLevel.WARNING).length,
465
+ critical: active.filter(a => a.level === AlertLevel.CRITICAL).length,
466
+ total: active.length
467
+ };
468
+ }
469
+
470
+ /**
471
+ * RateLimiter - Higher-level API for alert rate limiting
472
+ * Provides a cleaner interface for managing rate-limited alerts
473
+ */
474
+ class RateLimiter {
475
+ /**
476
+ * Create a RateLimiter instance
477
+ * @param {object} options - Configuration options
478
+ * @param {boolean} options.enabled - Enable rate limiting (default: true)
479
+ * @param {number} options.windowMs - Time window in milliseconds (default: 60000)
480
+ * @param {number} options.maxAlerts - Max alerts per window (default: 5)
481
+ * @param {boolean} options.alwaysAllowCritical - Always allow critical alerts (default: true)
482
+ */
483
+ constructor(options = {}) {
484
+ this.enabled = options.enabled ?? rateLimit.enabled;
485
+ this.windowMs = options.windowMs ?? rateLimit.windowMs;
486
+ this.maxAlerts = options.maxAlerts ?? rateLimit.maxAlerts;
487
+ this.alwaysAllowCritical = options.alwaysAllowCritical ?? true;
488
+ this.timestamps = {}; // { type: [ts1, ts2, ...] }
489
+
490
+ // Sync with global rate limit config
491
+ setRateLimit({
492
+ enabled: this.enabled,
493
+ windowMs: this.windowMs,
494
+ maxAlerts: this.maxAlerts
495
+ });
496
+ }
497
+
498
+ /**
499
+ * Check if an alert should be allowed or rate-limited
500
+ * @param {string} type - Alert type (cpu, memory, disk)
501
+ * @param {string} [level] - Alert level (critical, warning, info)
502
+ * @returns {object} Result with allowed boolean and reason
503
+ */
504
+ check(type, level = 'warning') {
505
+ // If rate limiting is disabled, allow all
506
+ if (!this.enabled) {
507
+ return { allowed: true, reason: 'rate_limiting_disabled' };
508
+ }
509
+
510
+ // Always allow critical alerts if configured
511
+ if (this.alwaysAllowCritical && level === 'critical') {
512
+ return { allowed: true, reason: 'critical_always_allowed' };
513
+ }
514
+
515
+ // Use the underlying function for the check
516
+ const shouldLimit = shouldRateLimitAlert(type);
517
+ if (shouldLimit) {
518
+ return { allowed: false, reason: 'rate_limit_exceeded' };
519
+ }
520
+
521
+ return { allowed: true, reason: 'ok' };
522
+ }
523
+
524
+ /**
525
+ * Record an alert occurrence after it's been allowed
526
+ * @param {string} type - Alert type
527
+ * @param {string} [level] - Alert level
528
+ */
529
+ record(type, level = 'warning') {
530
+ // Don't record if disabled
531
+ if (!this.enabled) {
532
+ return;
533
+ }
534
+
535
+ this._addTimestamp(type);
536
+ recordAlertTimestamp(type);
537
+ }
538
+
539
+ /**
540
+ * Internal method to add timestamp
541
+ * @private
542
+ */
543
+ _addTimestamp(type) {
544
+ if (!this.timestamps[type]) {
545
+ this.timestamps[type] = [];
546
+ }
547
+ this.timestamps[type].push(Date.now());
548
+ }
549
+
550
+ /**
551
+ * Get the count of alerts in current window for a type
552
+ * @param {string} type - Alert type
553
+ * @returns {number} Current count
554
+ */
555
+ getCount(type) {
556
+ const timestamps = this.timestamps[type] || [];
557
+ const now = Date.now();
558
+ const valid = timestamps.filter(ts => now - ts < this.windowMs);
559
+ return valid.length;
560
+ }
561
+
562
+ /**
563
+ * Get time until next alert is allowed for a type
564
+ * @param {string} type - Alert type
565
+ * @returns {number} Milliseconds until next alert, or 0 if allowed now
566
+ */
567
+ getRetryAfter(type) {
568
+ const timestamps = this.timestamps[type] || [];
569
+ if (timestamps.length === 0) {
570
+ return 0;
571
+ }
572
+
573
+ const now = Date.now();
574
+ const valid = timestamps.filter(ts => now - ts < this.windowMs);
575
+
576
+ if (valid.length < this.maxAlerts) {
577
+ return 0;
578
+ }
579
+
580
+ // Return oldest timestamp + window - now
581
+ const oldest = Math.min(...valid);
582
+ return Math.max(0, (oldest + this.windowMs) - now);
583
+ }
584
+
585
+ /**
586
+ * Get status of rate limiter
587
+ * @returns {object} Status object
588
+ */
589
+ getStatus() {
590
+ const now = Date.now();
591
+ const types = Object.keys(this.timestamps);
592
+
593
+ const typeStatus = {};
594
+ for (const type of types) {
595
+ const valid = this.timestamps[type].filter(ts => now - ts < this.windowMs);
596
+ typeStatus[type] = {
597
+ current: valid.length,
598
+ max: this.maxAlerts,
599
+ retryAfter: valid.length >= this.maxAlerts ? this.getRetryAfter(type) : 0
600
+ };
601
+ }
602
+
603
+ return {
604
+ enabled: this.enabled,
605
+ windowMs: this.windowMs,
606
+ maxAlerts: this.maxAlerts,
607
+ alwaysAllowCritical: this.alwaysAllowCritical,
608
+ types: typeStatus
609
+ };
610
+ }
611
+
612
+ /**
613
+ * Update configuration
614
+ * @param {object} options - New options
615
+ */
616
+ configure(options) {
617
+ if (options.enabled !== undefined) this.enabled = options.enabled;
618
+ if (options.windowMs !== undefined) this.windowMs = options.windowMs;
619
+ if (options.maxAlerts !== undefined) this.maxAlerts = options.maxAlerts;
620
+ if (options.alwaysAllowCritical !== undefined) this.alwaysAllowCritical = options.alwaysAllowCritical;
621
+
622
+ // Sync with global config
623
+ setRateLimit({
624
+ enabled: this.enabled,
625
+ windowMs: this.windowMs,
626
+ maxAlerts: this.maxAlerts
627
+ });
628
+ }
629
+
630
+ /**
631
+ * Reset all timestamps (clear rate limit state)
632
+ */
633
+ reset() {
634
+ this.timestamps = {};
635
+ resetRateLimit();
636
+ }
637
+
638
+ /**
639
+ * Create a combined check-and-record operation
640
+ * Use this for the common pattern of checking then recording
641
+ * @param {string} type - Alert type
642
+ * @param {string} [level] - Alert level
643
+ * @returns {object} Result with allowed boolean and reason; if allowed, also records the alert
644
+ */
645
+ checkAndRecord(type, level = 'warning') {
646
+ const result = this.check(type, level);
647
+ if (result.allowed) {
648
+ this.record(type, level);
649
+ }
650
+ return result;
651
+ }
652
+ }
653
+
654
+ // Rate limiter instance for alert throttling
655
+ // Uses the cleaner checkAndRecord API for atomic check-and-record operations
656
+ const thresholdRateLimiter = new RateLimiter({ alwaysAllowCritical: true });
657
+
658
+ // Default rate limiter instance
659
+ const defaultRateLimiter = new RateLimiter();
660
+
661
+ export default {
662
+ AlertLevel,
663
+ checkThreshold,
664
+ checkAllMetrics,
665
+ dismissAlert,
666
+ getActiveAlerts,
667
+ getAlertsByLevel,
668
+ getAlertHistory,
669
+ getAlertCounts,
670
+ getAlertColor,
671
+ formatAlert,
672
+ setThresholds,
673
+ getThresholds,
674
+ resetThresholds,
675
+ clearAllAlerts,
676
+ // Rate limiting exports
677
+ setRateLimit,
678
+ getRateLimit,
679
+ resetRateLimit,
680
+ shouldRateLimitAlert,
681
+ recordAlertTimestamp,
682
+ // Higher-level RateLimiter API
683
+ RateLimiter,
684
+ defaultRateLimiter,
685
+ // Sound notification exports
686
+ setSoundConfig,
687
+ getSoundConfig,
688
+ toggleSound,
689
+ playAlertSound
690
+ };
691
+
692
+ // Named export for plugin API use
693
+ export { RateLimiter };