feat: implement dynamic threshold alerts with unified rule system

- Add visual threshold-to-alert conversion feature in dashboard filtering UI
- Implement compound threshold rules that support multiple metrics with AND logic
- Enhance existing AlertManager to handle both single-metric and compound threshold rules
- Add dynamic alert rule creation modal with step-by-step configuration
- Implement comprehensive rule management (view, enable/disable, delete)
- Add real-time evaluation of compound threshold rules in main metrics processing loop
- Create compound rule API endpoints integrated with existing alerts system
- Add smart threshold summary display and button state management
- Support email/webhook notifications for compound threshold rule violations
- Maintain backward compatibility with existing single-metric alert rules

This bridges the gap between visual filtering and automated alerting, allowing users to
convert their threshold filtering criteria into persistent monitoring rules with a
seamless user experience.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
courtmanr@gmail.com
2025-06-04 20:37:36 +01:00
parent a8fc9e0d61
commit 6b0024f1ee
4 changed files with 625 additions and 6 deletions
+144 -5
View File
@@ -282,11 +282,19 @@ class AlertManager extends EventEmitter {
const timestamp = Date.now();
guests.forEach(guest => {
// Evaluate all alert rules (both single-metric and compound threshold rules)
this.alertRules.forEach(rule => {
if (this.isRuleSuppressed(rule.id, guest)) return;
const alertKey = `${rule.id}_${guest.endpointId}_${guest.node}_${guest.vmid}`;
this.evaluateRule(rule, guest, metrics, alertKey, timestamp);
if (rule.type === 'compound_threshold' && rule.thresholds) {
// Handle compound threshold rules
this.evaluateCompoundThresholdRule(rule, guest, metrics, alertKey, timestamp);
} else {
// Handle single-metric rules
this.evaluateRule(rule, guest, metrics, alertKey, timestamp);
}
});
});
}
@@ -904,26 +912,40 @@ class AlertManager extends EventEmitter {
}
addRule(rule) {
if (!rule.id || !rule.name || !rule.metric) {
throw new Error('Rule must have id, name, and metric');
// Support both single-metric rules and compound threshold rules
const isCompoundRule = rule.thresholds && Array.isArray(rule.thresholds) && rule.thresholds.length > 0;
if (!rule.name) {
throw new Error('Rule must have a name');
}
if (!isCompoundRule && !rule.metric) {
throw new Error('Single-metric rule must have a metric, or provide thresholds for compound rule');
}
const ruleId = rule.id || (isCompoundRule ?
`compound_${Date.now()}_${Math.random().toString(36).substr(2, 9)}` :
`rule_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`);
// Set defaults for new rules
const fullRule = {
id: ruleId,
condition: 'greater_than',
duration: 300000,
severity: 'warning',
enabled: true,
tags: [],
group: 'custom',
group: isCompoundRule ? 'compound_threshold' : 'custom',
escalationTime: 900000,
autoResolve: true,
suppressionTime: 300000,
notificationChannels: ['default'],
type: isCompoundRule ? 'compound_threshold' : 'single_metric',
...rule
};
this.alertRules.set(rule.id, fullRule);
this.alertRules.set(ruleId, fullRule);
console.log(`[AlertManager] Added ${isCompoundRule ? 'compound threshold' : 'single-metric'} rule: ${fullRule.name} (${ruleId})`);
this.emit('ruleAdded', fullRule);
return fullRule;
}
@@ -969,6 +991,123 @@ class AlertManager extends EventEmitter {
this.emit('rulesRefreshed', { activeRules: nowActiveRules.size, disabledRules });
}
evaluateCompoundThresholdRule(rule, guest, metrics, alertKey, timestamp) {
const guestMetrics = metrics[guest.vmid] || metrics[guest.name];
if (!guestMetrics || !guestMetrics.current) return;
// Check if ALL threshold conditions are met (AND logic)
const thresholdsMet = rule.thresholds.every(threshold => {
return this.evaluateThresholdCondition(threshold, guestMetrics.current, guest);
});
const existingAlert = this.activeAlerts.get(alertKey);
if (thresholdsMet) {
if (!existingAlert) {
// Create new alert
const alert = {
id: alertKey,
ruleId: rule.id,
rule: rule,
guest: guest,
severity: rule.severity,
message: this.formatCompoundThresholdMessage(rule, guestMetrics.current, guest),
timestamp: timestamp,
state: 'firing',
values: this.getCurrentThresholdValues(rule.thresholds, guestMetrics.current, guest)
};
this.fireAlert(alert);
}
} else if (existingAlert) {
// Resolve existing alert
this.resolveAlert(alertKey, timestamp, 'Thresholds no longer exceeded');
}
}
evaluateThresholdCondition(threshold, currentMetrics, guest) {
let metricValue;
switch (threshold.type) {
case 'cpu':
metricValue = currentMetrics.cpu;
break;
case 'memory':
metricValue = currentMetrics.memory;
break;
case 'disk':
metricValue = currentMetrics.disk;
break;
case 'diskread':
metricValue = currentMetrics.diskread;
break;
case 'diskwrite':
metricValue = currentMetrics.diskwrite;
break;
case 'netin':
metricValue = currentMetrics.netin;
break;
case 'netout':
metricValue = currentMetrics.netout;
break;
default:
return false;
}
if (metricValue === undefined || metricValue === null || isNaN(metricValue)) {
return false;
}
return metricValue >= threshold.value;
}
formatCompoundThresholdMessage(rule, currentMetrics, guest) {
const conditions = rule.thresholds.map(threshold => {
const value = this.getThresholdCurrentValue(threshold, currentMetrics);
const displayName = this.getThresholdDisplayName(threshold.type);
const unit = ['cpu', 'memory', 'disk'].includes(threshold.type) ? '%' : ' bytes/s';
return `${displayName}: ${value}${unit} (≥ ${threshold.value}${unit})`;
}).join(', ');
return `Dynamic threshold rule "${rule.name}" triggered for ${guest.name}: ${conditions}`;
}
getCurrentThresholdValues(thresholds, currentMetrics, guest) {
const values = {};
thresholds.forEach(threshold => {
values[threshold.type] = this.getThresholdCurrentValue(threshold, currentMetrics);
});
return values;
}
getThresholdCurrentValue(threshold, currentMetrics) {
switch (threshold.type) {
case 'cpu': return currentMetrics.cpu || 0;
case 'memory': return currentMetrics.memory || 0;
case 'disk': return currentMetrics.disk || 0;
case 'diskread': return currentMetrics.diskread || 0;
case 'diskwrite': return currentMetrics.diskwrite || 0;
case 'netin': return currentMetrics.netin || 0;
case 'netout': return currentMetrics.netout || 0;
default: return 0;
}
}
getThresholdDisplayName(type) {
const names = {
'cpu': 'CPU',
'memory': 'Memory',
'disk': 'Disk',
'diskread': 'Disk Read',
'diskwrite': 'Disk Write',
'netin': 'Network In',
'netout': 'Network Out'
};
return names[type] || type;
}
/**
* Clean up active alerts for a specific rule type
*/
+12
View File
@@ -507,6 +507,18 @@ app.delete('/api/alerts/rules/:id', (req, res) => {
}
});
// Enhanced alerts/rules endpoints to handle compound threshold rules
app.get('/api/alerts/compound-rules', (req, res) => {
try {
const allRules = stateManager.alertManager.getRules();
const compoundRules = allRules.filter(rule => rule.type === 'compound_threshold');
res.json({ success: true, rules: compoundRules });
} catch (error) {
console.error("Error fetching compound threshold rules:", error);
res.status(500).json({ error: "Failed to fetch compound threshold rules" });
}
});
// Version API endpoint
app.get('/api/version', async (req, res) => {
+27
View File
@@ -678,6 +678,33 @@
</select>
</th>
</tr>
<!-- Alert Rule Creation Row -->
<tr id="alert-rule-row" class="hidden">
<td colspan="11" class="py-2 px-4 bg-blue-50 dark:bg-blue-900/20 border-t border-blue-200 dark:border-blue-700">
<div class="flex items-center justify-between">
<div class="flex items-center gap-2">
<svg class="h-4 w-4 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 17h5l-5 5v-5zm-2-8V7a1 1 0 00-1-1H5a1 1 0 00-1 1v3a1 1 0 001 1h7a1 1 0 001-1z"></path>
</svg>
<span class="text-sm font-medium text-blue-700 dark:text-blue-300">
Create Alert Rule from Current Thresholds
</span>
<span id="active-thresholds-summary" class="text-xs text-blue-600 dark:text-blue-400"></span>
</div>
<div class="flex items-center gap-2">
<button id="create-alert-rule-btn"
class="px-3 py-1 bg-blue-600 hover:bg-blue-700 text-white text-xs font-medium rounded transition-colors"
disabled>
Create Alert Rule
</button>
<button id="view-alert-rules-btn"
class="px-3 py-1 bg-gray-500 hover:bg-gray-600 text-white text-xs font-medium rounded transition-colors">
View Rules
</button>
</div>
</div>
</td>
</tr>
</thead>
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
<tr>
+442 -1
View File
@@ -2,16 +2,24 @@ PulseApp.ui = PulseApp.ui || {};
PulseApp.ui.thresholds = (() => {
let thresholdRow = null;
let alertRuleRow = null;
let toggleThresholdsButton = null;
let thresholdBadge = null;
let createAlertRuleBtn = null;
let viewAlertRulesBtn = null;
let activeThresholdsSummary = null;
let sliders = {};
let thresholdSelects = {};
let isDraggingSlider = false;
function init() {
thresholdRow = document.getElementById('threshold-slider-row');
alertRuleRow = document.getElementById('alert-rule-row');
toggleThresholdsButton = document.getElementById('toggle-thresholds-checkbox');
thresholdBadge = document.getElementById('threshold-count-badge');
createAlertRuleBtn = document.getElementById('create-alert-rule-btn');
viewAlertRulesBtn = document.getElementById('view-alert-rules-btn');
activeThresholdsSummary = document.getElementById('active-thresholds-summary');
sliders = {
cpu: document.getElementById('threshold-slider-cpu'),
@@ -41,6 +49,7 @@ PulseApp.ui.thresholds = (() => {
_setupSliderListeners();
_setupSelectListeners();
_setupDragEndListeners();
_setupAlertRuleListeners();
}
function applyInitialThresholdUI() {
@@ -110,6 +119,355 @@ PulseApp.ui.thresholds = (() => {
document.addEventListener('touchend', _handleThresholdDragEnd);
}
function _setupAlertRuleListeners() {
if (createAlertRuleBtn) {
createAlertRuleBtn.addEventListener('click', _handleCreateAlertRule);
}
if (viewAlertRulesBtn) {
viewAlertRulesBtn.addEventListener('click', _handleViewAlertRules);
}
}
function _handleCreateAlertRule() {
const thresholdState = PulseApp.state.getThresholdState();
const activeThresholds = _getActiveThresholds(thresholdState);
if (activeThresholds.length === 0) {
alert('Please set at least one threshold to create an alert rule.');
return;
}
// Show modal for creating alert rule
_showCreateAlertRuleModal(activeThresholds);
}
function _handleViewAlertRules() {
// Show modal for viewing/managing existing alert rules
_showViewAlertRulesModal();
}
function _showCreateAlertRuleModal(activeThresholds) {
// Create a modal for alert rule creation
const modalHtml = `
<div id="alert-rule-modal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-2xl w-full mx-4 max-h-[90vh] overflow-y-auto">
<div class="p-6">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100">
Create Alert Rule from Thresholds
</h3>
<button id="close-alert-modal" class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300">
<svg class="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
</div>
<form id="alert-rule-form">
<div class="space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Rule Name
</label>
<input type="text" id="rule-name" required
placeholder="e.g., High Resource Usage Alert"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Description
</label>
<textarea id="rule-description" rows="2"
placeholder="Optional description of what this alert monitors"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"></textarea>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Alert Conditions
</label>
<div class="bg-gray-50 dark:bg-gray-700 rounded-md p-3">
<p class="text-sm text-gray-600 dark:text-gray-400 mb-2">
Alert will trigger when ANY guest meets ALL of these conditions:
</p>
<ul class="space-y-1">
${activeThresholds.map(threshold => `
<li class="text-sm text-gray-800 dark:text-gray-200">
${_getThresholdDisplayName(threshold.type)}${_formatThresholdValue(threshold)}
</li>
`).join('')}
</ul>
</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Alert Severity
</label>
<select id="rule-severity" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100">
<option value="warning">Warning</option>
<option value="critical">Critical</option>
</select>
</div>
</div>
<div class="flex justify-end gap-3 mt-6">
<button type="button" id="cancel-alert-rule"
class="px-4 py-2 text-gray-700 dark:text-gray-300 bg-gray-200 dark:bg-gray-600 rounded-md hover:bg-gray-300 dark:hover:bg-gray-500">
Cancel
</button>
<button type="submit"
class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-md">
Create Alert Rule
</button>
</div>
</form>
</div>
</div>
</div>
`;
document.body.insertAdjacentHTML('beforeend', modalHtml);
_setupAlertRuleModalListeners(activeThresholds);
}
function _formatThresholdValue(threshold) {
if (['cpu', 'memory', 'disk'].includes(threshold.type)) {
return `${threshold.value}%`;
} else {
return _formatBytesThreshold(threshold.value);
}
}
function _setupAlertRuleModalListeners(activeThresholds) {
const modal = document.getElementById('alert-rule-modal');
const closeBtn = document.getElementById('close-alert-modal');
const cancelBtn = document.getElementById('cancel-alert-rule');
const form = document.getElementById('alert-rule-form');
const closeModal = () => {
modal.remove();
};
closeBtn.addEventListener('click', closeModal);
cancelBtn.addEventListener('click', closeModal);
modal.addEventListener('click', (e) => {
if (e.target === modal) closeModal();
});
form.addEventListener('submit', (e) => {
e.preventDefault();
_handleAlertRuleSubmit(activeThresholds);
closeModal();
});
}
async function _handleAlertRuleSubmit(activeThresholds) {
const ruleName = document.getElementById('rule-name').value;
const ruleDescription = document.getElementById('rule-description').value;
const ruleSeverity = document.getElementById('rule-severity').value;
const alertRule = {
name: ruleName,
description: ruleDescription,
severity: ruleSeverity,
thresholds: activeThresholds,
enabled: true
};
try {
const response = await fetch('/api/alerts/rules', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(alertRule)
});
const result = await response.json();
if (response.ok && result.success) {
alert(`✅ Alert rule "${ruleName}" created successfully!\n\nIt will monitor for guests meeting ALL of these conditions:\n${activeThresholds.map(t => `${_getThresholdDisplayName(t.type)}${_formatThresholdValue(t)}`).join('\n')}\n\nYou'll receive email notifications when any guest meets these criteria.`);
} else {
throw new Error(result.error || 'Failed to create alert rule');
}
} catch (error) {
console.error('Error creating alert rule:', error);
alert(`❌ Failed to create alert rule: ${error.message}`);
}
}
async function _showViewAlertRulesModal() {
try {
const response = await fetch('/api/alerts/compound-rules');
const result = await response.json();
if (!response.ok || !result.success) {
throw new Error(result.error || 'Failed to fetch alert rules');
}
const rules = result.rules || [];
_displayAlertRulesModal(rules);
} catch (error) {
console.error('Error fetching alert rules:', error);
alert(`❌ Failed to fetch alert rules: ${error.message}`);
}
}
function _displayAlertRulesModal(rules) {
const modalHtml = `
<div id="view-rules-modal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-4xl w-full mx-4 max-h-[90vh] overflow-y-auto">
<div class="p-6">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100">
Dynamic Threshold Alert Rules
</h3>
<button id="close-rules-modal" class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300">
<svg class="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
</div>
${rules.length === 0 ? `
<div class="text-center py-8">
<svg class="h-12 w-12 text-gray-400 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path>
</svg>
<p class="text-gray-500 dark:text-gray-400 mb-2">No dynamic threshold rules created yet</p>
<p class="text-sm text-gray-400 dark:text-gray-500">Set some thresholds above and click "Create Alert Rule" to get started!</p>
</div>
` : `
<div class="space-y-4">
${rules.map(rule => _formatRuleCard(rule)).join('')}
</div>
`}
</div>
</div>
</div>
`;
document.body.insertAdjacentHTML('beforeend', modalHtml);
_setupViewRulesModalListeners();
}
function _formatRuleCard(rule) {
const thresholdsList = rule.thresholds.map(t =>
`<span class="inline-block bg-gray-100 dark:bg-gray-700 px-2 py-1 text-xs rounded mr-2 mb-1">
${_getThresholdDisplayName(t.type)}${_formatThresholdValue(t)}
</span>`
).join('');
const createdDate = new Date(rule.createdAt).toLocaleDateString();
const severityColor = rule.severity === 'critical' ? 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200' : 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200';
return `
<div class="border border-gray-200 dark:border-gray-600 rounded-lg p-4">
<div class="flex items-start justify-between mb-3">
<div class="flex-1">
<div class="flex items-center gap-2 mb-2">
<h4 class="font-medium text-gray-900 dark:text-gray-100">${rule.name}</h4>
<span class="px-2 py-1 text-xs font-medium rounded ${severityColor}">
${rule.severity.toUpperCase()}
</span>
<span class="px-2 py-1 text-xs font-medium rounded ${rule.enabled ? 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200' : 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-200'}">
${rule.enabled ? 'ENABLED' : 'DISABLED'}
</span>
</div>
${rule.description ? `<p class="text-sm text-gray-600 dark:text-gray-400 mb-2">${rule.description}</p>` : ''}
<div class="text-xs text-gray-500 dark:text-gray-400 mb-2">
Created: ${createdDate} • ID: ${rule.id}
</div>
</div>
<div class="flex gap-2">
<button onclick="PulseApp.ui.thresholds.toggleRule('${rule.id}', ${!rule.enabled})"
class="px-2 py-1 text-xs bg-blue-600 hover:bg-blue-700 text-white rounded">
${rule.enabled ? 'Disable' : 'Enable'}
</button>
<button onclick="PulseApp.ui.thresholds.deleteRule('${rule.id}')"
class="px-2 py-1 text-xs bg-red-600 hover:bg-red-700 text-white rounded">
Delete
</button>
</div>
</div>
<div>
<p class="text-sm text-gray-700 dark:text-gray-300 mb-2">Alert triggers when ANY guest meets ALL conditions:</p>
<div class="flex flex-wrap">
${thresholdsList}
</div>
</div>
</div>
`;
}
function _setupViewRulesModalListeners() {
const modal = document.getElementById('view-rules-modal');
const closeBtn = document.getElementById('close-rules-modal');
const closeModal = () => {
modal.remove();
};
closeBtn.addEventListener('click', closeModal);
modal.addEventListener('click', (e) => {
if (e.target === modal) closeModal();
});
}
async function toggleRule(ruleId, enabled) {
try {
const response = await fetch(`/api/alerts/rules/${ruleId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ enabled })
});
const result = await response.json();
if (response.ok && result.success) {
// Refresh the modal
document.getElementById('view-rules-modal').remove();
_showViewAlertRulesModal();
} else {
throw new Error(result.error || 'Failed to update rule');
}
} catch (error) {
console.error('Error updating rule:', error);
alert(`❌ Failed to update rule: ${error.message}`);
}
}
async function deleteRule(ruleId) {
if (!confirm('Are you sure you want to delete this alert rule? This action cannot be undone.')) {
return;
}
try {
const response = await fetch(`/api/alerts/rules/${ruleId}`, {
method: 'DELETE'
});
const result = await response.json();
if (response.ok && result.success) {
// Refresh the modal
document.getElementById('view-rules-modal').remove();
_showViewAlertRulesModal();
} else {
throw new Error(result.error || 'Failed to delete rule');
}
} catch (error) {
console.error('Error deleting rule:', error);
alert(`❌ Failed to delete rule: ${error.message}`);
}
}
function updateThreshold(type, value) {
PulseApp.state.setThresholdValue(type, value);
@@ -130,6 +488,11 @@ PulseApp.ui.thresholds = (() => {
toggleThresholdsButton.checked = isVisible;
}
}
// Show alert rule row only when threshold row is visible
if (alertRuleRow) {
alertRuleRow.classList.toggle('hidden', !isVisible);
}
}
function _updateThresholdHeaderStyles(thresholdState) {
@@ -163,6 +526,7 @@ PulseApp.ui.thresholds = (() => {
const thresholdState = PulseApp.state.getThresholdState();
const activeCount = _updateThresholdHeaderStyles(thresholdState);
const activeThresholds = _getActiveThresholds(thresholdState);
if (activeCount > 0) {
thresholdBadge.textContent = activeCount;
@@ -170,6 +534,81 @@ PulseApp.ui.thresholds = (() => {
} else {
thresholdBadge.classList.add('hidden');
}
// Update alert rule button state and summary
_updateAlertRuleUI(activeThresholds, activeCount);
}
function _getActiveThresholds(thresholdState) {
const active = [];
for (const type in thresholdState) {
if (thresholdState[type].value > 0) {
active.push({
type: type,
value: thresholdState[type].value
});
}
}
return active;
}
function _updateAlertRuleUI(activeThresholds, activeCount) {
// Update button state
if (createAlertRuleBtn) {
createAlertRuleBtn.disabled = activeCount === 0;
if (activeCount === 0) {
createAlertRuleBtn.classList.add('opacity-50', 'cursor-not-allowed');
} else {
createAlertRuleBtn.classList.remove('opacity-50', 'cursor-not-allowed');
}
}
// Update summary text
if (activeThresholdsSummary) {
if (activeCount > 0) {
const summaryText = _formatThresholdSummary(activeThresholds);
activeThresholdsSummary.textContent = `(${summaryText})`;
} else {
activeThresholdsSummary.textContent = '(Set thresholds above to enable)';
}
}
}
function _formatThresholdSummary(activeThresholds) {
const formatted = activeThresholds.map(threshold => {
const name = _getThresholdDisplayName(threshold.type);
let value;
if (['cpu', 'memory', 'disk'].includes(threshold.type)) {
value = `${threshold.value}%`;
} else {
value = _formatBytesThreshold(threshold.value);
}
return `${name} > ${value}`;
});
return formatted.join(', ');
}
function _getThresholdDisplayName(type) {
const names = {
'cpu': 'CPU',
'memory': 'Memory',
'disk': 'Disk',
'diskread': 'Disk Read',
'diskwrite': 'Disk Write',
'netin': 'Net In',
'netout': 'Net Out'
};
return names[type] || type;
}
function _formatBytesThreshold(bytes) {
const mb = bytes / (1024 * 1024);
if (mb >= 100) return `${Math.round(mb)}MB/s`;
if (mb >= 10) return `${Math.round(mb)}MB/s`;
return `${Math.round(mb * 10) / 10}MB/s`;
}
function resetThresholds() {
@@ -198,6 +637,8 @@ PulseApp.ui.thresholds = (() => {
return {
init,
resetThresholds,
isThresholdDragInProgress
isThresholdDragInProgress,
toggleRule,
deleteRule
};
})();