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.
- package/.c8rc.json +38 -0
- package/.dockerignore +68 -0
- package/.github/dependabot.yml +45 -0
- package/.github/pull_request_template.md +39 -0
- package/.github/workflows/ci.yml +147 -0
- package/.github/workflows/release.yml +40 -0
- package/.github/workflows/security.yml +84 -0
- package/.husky/pre-commit +1 -0
- package/.planning/codebase/ARCHITECTURE.md +206 -0
- package/.planning/codebase/INTEGRATIONS.md +150 -0
- package/.planning/codebase/STACK.md +122 -0
- package/.planning/codebase/STRUCTURE.md +201 -0
- package/CHANGELOG.md +293 -0
- package/CONTRIBUTING.md +378 -0
- package/Dockerfile +54 -0
- package/FEATURES.md +195 -21
- package/README.md +83 -6
- package/TODO.md +28 -0
- package/build-cjs.js +127 -0
- package/cjs-shim.js +13 -0
- package/dist/clawdash +1729 -0
- package/dist/clawdash.meta.json +2236 -0
- package/dist/widgets.cjs +6511 -0
- package/docker-compose.yml +67 -0
- package/docs/API.md +1050 -0
- package/docs/PLUGINS.md +1504 -0
- package/esbuild.config.js +158 -0
- package/eslint.config.js +56 -0
- package/examples/plugins/README.md +122 -0
- package/examples/plugins/api-status/index.js +294 -0
- package/examples/plugins/api-status/plugin.json +19 -0
- package/examples/plugins/hello-world/index.js +104 -0
- package/examples/plugins/hello-world/plugin.json +15 -0
- package/examples/plugins/system-metrics-chart/index.js +339 -0
- package/examples/plugins/system-metrics-chart/plugin.json +18 -0
- package/examples/plugins/weather-widget/index.js +136 -0
- package/examples/plugins/weather-widget/plugin.json +19 -0
- package/index.cjs +23005 -0
- package/index.js +5236 -566
- package/jest.config.js +11 -0
- package/man/clawdash.1 +276 -0
- package/package.json +54 -5
- package/schemas/plugin-manifest.json +128 -0
- package/scripts/release.js +595 -0
- package/src/alerts.js +693 -0
- package/src/auto-save.js +584 -0
- package/src/cache.js +390 -0
- package/src/checksum.js +146 -0
- package/src/cli/args.js +110 -0
- package/src/cli/export-schedule.js +423 -0
- package/src/cli/export-snapshot.js +138 -0
- package/src/cli/help.js +69 -0
- package/src/cli/import-snapshot.js +230 -0
- package/src/cli/index.js +14 -0
- package/src/cli/list-templates.js +69 -0
- package/src/cli/validate-config.js +76 -0
- package/src/cli/validate-plugin.js +187 -0
- package/src/cli/version.js +15 -0
- package/src/config-validator.js +586 -0
- package/src/config-watcher.js +401 -0
- package/src/config.js +684 -0
- package/src/container-detector.js +499 -0
- package/src/database.js +734 -0
- package/src/differential-render.js +327 -0
- package/src/errors.js +169 -0
- package/src/export-scheduler.js +581 -0
- package/src/gateway-manager.js +685 -0
- package/src/hints.js +371 -0
- package/src/loading-states.js +509 -0
- package/src/logger.js +185 -0
- package/src/memory-pressure.js +467 -0
- package/src/performance-monitor.js +374 -0
- package/src/plugin-errors.js +586 -0
- package/src/plugin-manifest-validator.js +219 -0
- package/src/plugin-reload.js +481 -0
- package/src/plugin-scaffold.js +1941 -0
- package/src/plugin-validator.js +284 -0
- package/src/retry.js +218 -0
- package/src/security.js +860 -0
- package/src/snapshot.js +372 -0
- package/src/splash.js +115 -0
- package/src/theme-selector.js +411 -0
- package/src/themes.js +874 -0
- package/src/transitions.js +534 -0
- package/src/types.d.ts +174 -0
- package/src/validation.js +926 -0
- package/src/web-server.js +885 -0
- package/src/widgets/builtin-widgets.js +1057 -0
- package/src/widgets/config-processor.js +401 -0
- package/src/widgets/dependency-resolver.js +596 -0
- package/src/widgets/index.js +79 -0
- package/src/widgets/plugin-api.js +845 -0
- package/src/widgets/widget-error-boundary.js +773 -0
- package/src/widgets/widget-error-isolation.js +551 -0
- package/src/widgets/widget-loader.js +1437 -0
- package/src/workers/system-worker.js +130 -0
- package/src/workers/worker-pool.js +633 -0
- package/tests/alerts.test.js +437 -0
- package/tests/auto-save.test.js +529 -0
- package/tests/cache.test.js +317 -0
- package/tests/cli.test.js +351 -0
- package/tests/command-palette.test.js +320 -0
- package/tests/config-processor.test.js +452 -0
- package/tests/config-validator.test.js +710 -0
- package/tests/config-watcher.test.js +594 -0
- package/tests/config.test.js +755 -0
- package/tests/database.test.js +438 -0
- package/tests/errors.test.js +189 -0
- package/tests/example-plugins.test.js +624 -0
- package/tests/integration.test.js +1321 -0
- package/tests/loading-states.test.js +300 -0
- package/tests/manifest-validation-on-load.test.js +671 -0
- package/tests/performance-monitor.test.js +190 -0
- package/tests/plugin-api-rate-limit.test.js +302 -0
- package/tests/plugin-errors.test.js +311 -0
- package/tests/plugin-lifecycle-e2e.test.js +1036 -0
- package/tests/plugin-reload.test.js +764 -0
- package/tests/rate-limiter.test.js +539 -0
- package/tests/retry.test.js +308 -0
- package/tests/security.test.js +411 -0
- package/tests/settings-modal.test.js +226 -0
- package/tests/theme-selector.test.js +57 -0
- package/tests/utils.js +242 -0
- package/tests/utils.test.js +317 -0
- package/tests/validate-plugin-cli.test.js +359 -0
- package/tests/validation.test.js +837 -0
- package/tests/web-server.test.js +646 -0
- package/tests/widget-arrange-mode.test.js +183 -0
- package/tests/widget-config-hot-reload.test.js +465 -0
- package/tests/widget-dependency.test.js +591 -0
- package/tests/widget-error-boundary.test.js +749 -0
- package/tests/widget-error-isolation.test.js +469 -0
- package/tests/widget-integration.test.js +1147 -0
- package/tests/widget-refresh-intervals.test.js +284 -0
- 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 };
|