fix: improve reliability and user experience across components

- Update PBS connectivity check endpoint for better compatibility
- Make alert checking asynchronous to prevent blocking
- Update test expectations for webhook error messages with retry counts
- Enhance alerts handler with better event listeners and error handling
- Improve PBS UI by replacing symbols with clearer text labels

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
root
2025-06-07 18:38:27 +01:00
parent 33412d4f8e
commit 944a197fef
5 changed files with 131 additions and 42 deletions
+1 -1
View File
@@ -1422,7 +1422,7 @@ async function fetchPbsData(currentPbsApiClients) {
// Quick connectivity check for PBS to fail fast
try {
await Promise.race([
pbsClient.client.get('/api2/json/version'),
pbsClient.client.get('/version'),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('PBS connectivity check timeout')), 3000)
)
+2 -2
View File
@@ -313,10 +313,10 @@ function addPerformanceSnapshot(type, duration, errorCount) {
}
}
function checkAlertsForMetrics() {
async function checkAlertsForMetrics() {
try {
const allGuests = [...state.vms, ...state.containers];
alertManager.checkMetrics(allGuests, state.metrics);
await alertManager.checkMetrics(allGuests, state.metrics);
} catch (error) {
console.error('[State Manager] Error checking alerts:', error);
}
+3 -3
View File
@@ -225,7 +225,7 @@ describe('AlertManager Webhook Functionality', () => {
await expect(
alertManager.sendWebhookNotification(mockWebhookChannel, mockAlert)
).rejects.toThrow('Webhook failed: 404 Not Found');
).rejects.toThrow('Webhook failed after 3 attempts: 404 Not Found');
});
test('should handle network errors gracefully', async () => {
@@ -235,7 +235,7 @@ describe('AlertManager Webhook Functionality', () => {
await expect(
alertManager.sendWebhookNotification(mockWebhookChannel, mockAlert)
).rejects.toThrow(`Webhook failed: No response from ${mockWebhookChannel.config.url}`);
).rejects.toThrow(`Webhook failed after 3 attempts: No response from ${mockWebhookChannel.config.url}`);
});
test('should handle other errors gracefully', async () => {
@@ -244,7 +244,7 @@ describe('AlertManager Webhook Functionality', () => {
await expect(
alertManager.sendWebhookNotification(mockWebhookChannel, mockAlert)
).rejects.toThrow(`Webhook failed: ${errorMessage}`);
).rejects.toThrow(`Webhook failed after 3 attempts: ${errorMessage}`);
});
});
+102 -9
View File
@@ -142,11 +142,29 @@ PulseApp.alerts = (() => {
}
function setupEventListeners() {
let socketListenersSetup = false;
// Wait for socket to be available and set up event listeners
const setupSocketListeners = () => {
if (window.socket && window.socket.connected) {
if (window.socket && !socketListenersSetup) {
// Set up alert event listeners
window.socket.on('alert', handleNewAlert);
window.socket.on('alertResolved', handleResolvedAlert);
window.socket.on('alertEscalated', handleEscalatedAlert);
window.socket.on('alertAcknowledged', handleAcknowledgedAlert);
// Handle reconnection - reload alert data when reconnected
window.socket.on('connect', () => {
console.log('[Alerts] Socket reconnected, reloading alert data');
loadInitialData();
});
window.socket.on('disconnect', () => {
console.log('[Alerts] Socket disconnected');
});
socketListenersSetup = true;
console.log('[Alerts] Socket event listeners configured');
return true;
}
return false;
@@ -250,7 +268,7 @@ PulseApp.alerts = (() => {
${SEVERITY_ICONS.info}
</svg>
<p class="text-xs mb-3">No active alerts</p>
<button onclick="PulseApp.alerts.hideAlertsDropdown(); PulseApp.ui.alertManagementModal.openModal();"
<button onclick="PulseApp.alerts.hideAlertsDropdown(); if (PulseApp.ui && PulseApp.ui.alertManagementModal) { PulseApp.ui.alertManagementModal.openModal(); } else { console.error('Alert management modal not available'); }"
class="w-full px-3 py-2 bg-blue-500 hover:bg-blue-600 text-white text-xs font-medium rounded transition-colors">
Manage Alerts
</button>
@@ -321,7 +339,7 @@ PulseApp.alerts = (() => {
// Add Manage Alerts button to the bottom
content += `
<div class="border-t border-gray-200 dark:border-gray-700 p-2">
<button onclick="PulseApp.alerts.hideAlertsDropdown(); PulseApp.ui.alertManagementModal.openModal();"
<button onclick="PulseApp.alerts.hideAlertsDropdown(); if (PulseApp.ui && PulseApp.ui.alertManagementModal) { PulseApp.ui.alertManagementModal.openModal(); } else { console.error('Alert management modal not available'); }"
class="w-full px-3 py-2 bg-blue-500 hover:bg-blue-600 text-white text-xs font-medium rounded transition-colors">
<svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3 inline mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
@@ -414,7 +432,8 @@ PulseApp.alerts = (() => {
<div class="flex-shrink-0 space-x-1">
${!acknowledged ? `
<button onclick="PulseApp.alerts.acknowledgeAlert('${alert.id}', '${alert.ruleId}');"
class="text-xs px-1 py-0.5 bg-green-500 text-white rounded hover:bg-green-600 focus:outline-none">
class="text-xs px-1 py-0.5 bg-green-500 text-white rounded hover:bg-green-600 focus:outline-none"
title="Acknowledge alert">
</button>
` : ''}
@@ -626,19 +645,30 @@ PulseApp.alerts = (() => {
}
} catch (error) {
console.error('[Alerts] Failed to acknowledge alert:', error);
// Only show notification for actual errors
// showNotification({ message: `Failed to acknowledge alert: ${error.message}` }, 'warning');
// Show user feedback for acknowledgment failures
showToastNotification(`Failed to acknowledge alert: ${error.message}`, 'error');
}
}
// Track cleanup timeouts to prevent memory leaks
const cleanupTimeouts = new Map();
function scheduleAcknowledgedCleanup(alertId) {
setTimeout(() => {
// Clear any existing timeout for this alert
if (cleanupTimeouts.has(alertId)) {
clearTimeout(cleanupTimeouts.get(alertId));
}
const timeoutId = setTimeout(() => {
activeAlerts = activeAlerts.filter(a => a.id !== alertId);
updateHeaderIndicator();
if (alertDropdown && !alertDropdown.classList.contains('hidden')) {
updateDropdownContent();
}
cleanupTimeouts.delete(alertId);
}, ACKNOWLEDGED_CLEANUP_DELAY);
cleanupTimeouts.set(alertId, timeoutId);
}
function toggleAcknowledgedSection() {
@@ -689,7 +719,7 @@ PulseApp.alerts = (() => {
}
} catch (error) {
console.error('[Alerts] Failed to suppress alert:', error);
// showNotification({ message: `Failed to suppress alert: ${error.message}` }, 'warning');
showToastNotification(`Failed to suppress alert: ${error.message}`, 'error');
}
}
@@ -771,6 +801,68 @@ PulseApp.alerts = (() => {
}
}
}
// Additional socket event handlers
function handleEscalatedAlert(alert) {
console.log('[Alerts] Alert escalated:', alert);
// Update existing alert or add if not found
const existingIndex = activeAlerts.findIndex(a => a.id === alert.id);
if (existingIndex >= 0) {
activeAlerts[existingIndex] = { ...activeAlerts[existingIndex], ...alert };
} else {
activeAlerts.unshift(alert);
}
updateHeaderIndicator();
if (alertDropdown && !alertDropdown.classList.contains('hidden')) {
updateDropdownContent();
}
// Show escalation notification
showToastNotification(`Alert escalated: ${alert.rule.name} for ${alert.guest.name}`, 'critical');
}
function handleAcknowledgedAlert(alert) {
console.log('[Alerts] Alert acknowledged:', alert);
// Update existing alert
const existingIndex = activeAlerts.findIndex(a => a.id === alert.id);
if (existingIndex >= 0) {
activeAlerts[existingIndex] = { ...activeAlerts[existingIndex], acknowledged: true, acknowledgedAt: Date.now() };
updateHeaderIndicator();
if (alertDropdown && !alertDropdown.classList.contains('hidden')) {
updateDropdownContent();
}
}
}
// Cleanup function to prevent memory leaks
function cleanup() {
// Clear all cleanup timeouts
for (const timeoutId of cleanupTimeouts.values()) {
clearTimeout(timeoutId);
}
cleanupTimeouts.clear();
// Remove socket listeners if needed
if (window.socket) {
window.socket.off('alert', handleNewAlert);
window.socket.off('alertResolved', handleResolvedAlert);
window.socket.off('alertEscalated', handleEscalatedAlert);
window.socket.off('alertAcknowledged', handleAcknowledgedAlert);
}
}
// Helper function for toast notifications
function showToastNotification(message, type = 'info') {
if (window.PulseApp && window.PulseApp.ui && window.PulseApp.ui.toastNotifications) {
window.PulseApp.ui.toastNotifications.show(message, type);
} else {
// Fallback to basic notification
showNotification({ message }, type);
}
}
// Public API
return {
@@ -784,7 +876,8 @@ PulseApp.alerts = (() => {
markAllAsAcknowledged,
toggleAcknowledgedSection,
getActiveAlerts: () => activeAlerts,
getAlertHistory: () => alertHistory
getAlertHistory: () => alertHistory,
cleanup
};
})();
+23 -27
View File
@@ -179,11 +179,11 @@ PulseApp.ui.pbs = (() => {
const getPbsStatusIcon = (status) => {
if (status === 'OK') {
return `<span class="${CSS_CLASSES.TEXT_GREEN_500_DARK_GREEN_400}" title="OK"></span>`;
return `<span class="${CSS_CLASSES.TEXT_GREEN_500_DARK_GREEN_400}" title="OK">OK</span>`;
} else if (status === 'running') {
return `<span class="${CSS_CLASSES.INLINE_BLOCK} ${CSS_CLASSES.ANIMATE_SPIN} ${CSS_CLASSES.ROUNDED_FULL} ${CSS_CLASSES.H_3} ${CSS_CLASSES.W_3} ${CSS_CLASSES.BORDER_T_2} ${CSS_CLASSES.BORDER_B_2} ${CSS_CLASSES.BORDER_BLUE_500}" title="Running"></span>`;
} else if (status) {
return `<span class="${CSS_CLASSES.TEXT_RED_500_DARK_RED_400} ${CSS_CLASSES.FONT_BOLD}" title="${status}"></span>`;
return `<span class="${CSS_CLASSES.TEXT_RED_500_DARK_RED_400} ${CSS_CLASSES.FONT_BOLD}" title="${status}">ERROR</span>`;
} else {
return `<span class="${CSS_CLASSES.TEXT_GRAY_400}" title="Unknown">?</span>`;
}
@@ -191,13 +191,13 @@ PulseApp.ui.pbs = (() => {
const getPbsStatusDisplay = (status) => {
if (status === 'OK') {
return `<span class="${CSS_CLASSES.TEXT_GREEN_500_DARK_GREEN_400}">OK</span>`;
return `<span class="${CSS_CLASSES.TEXT_GREEN_500_DARK_GREEN_400}">OK</span>`;
} else if (status === 'running') {
return `<span class="${CSS_CLASSES.INLINE_BLOCK} ${CSS_CLASSES.ANIMATE_SPIN} ${CSS_CLASSES.ROUNDED_FULL} ${CSS_CLASSES.H_3} ${CSS_CLASSES.W_3} ${CSS_CLASSES.BORDER_T_2} ${CSS_CLASSES.BORDER_B_2} ${CSS_CLASSES.BORDER_BLUE_500}" title="Running"></span> <span class="${CSS_CLASSES.TEXT_BLUE_600_DARK_BLUE_400}">Running</span>`;
} else if (status) {
// For failed tasks, show the full error message
const shortStatus = status.length > 50 ? `${status.substring(0, 47)}...` : status;
return `<span class="${CSS_CLASSES.TEXT_RED_500_DARK_RED_400} ${CSS_CLASSES.FONT_BOLD}" title="${status}"></span> <span class="${CSS_CLASSES.TEXT_RED_600_DARK_RED_400} ${CSS_CLASSES.TEXT_XS}" title="${status}">${shortStatus}</span>`;
return `<span class="${CSS_CLASSES.TEXT_RED_500_DARK_RED_400} ${CSS_CLASSES.FONT_BOLD}" title="${status}">ERROR</span> <span class="${CSS_CLASSES.TEXT_RED_600_DARK_RED_400} ${CSS_CLASSES.TEXT_XS}" title="${status}">${shortStatus}</span>`;
} else {
return `<span class="${CSS_CLASSES.TEXT_GRAY_400}">? Unknown</span>`;
}
@@ -288,7 +288,7 @@ PulseApp.ui.pbs = (() => {
const targetElement = document.createElement('div');
targetElement.className = 'font-medium text-sm truncate pr-2 flex-1';
if (isFailed) {
targetElement.innerHTML = `<span class="text-xs text-gray-400 mr-1">▶</span>${target}`;
targetElement.innerHTML = `${target}`;
} else {
targetElement.textContent = target;
}
@@ -331,7 +331,7 @@ PulseApp.ui.pbs = (() => {
if (isFailed) {
const expandButton = document.createElement('button');
expandButton.className = 'mt-3 w-full py-2 px-3 text-xs text-blue-600 dark:text-blue-400 border border-blue-200 dark:border-blue-600 rounded bg-blue-50 dark:bg-blue-900/20 hover:bg-blue-100 dark:hover:bg-blue-900/30 transition-colors tap-target';
expandButton.textContent = 'Show Error Details';
expandButton.textContent = 'Show Error Details';
expandButton.addEventListener('click', (event) => {
event.stopPropagation();
@@ -343,15 +343,15 @@ PulseApp.ui.pbs = (() => {
if (existingDetailCard && existingDetailCard.classList.contains('mobile-task-detail-card')) {
// Toggle existing detail card - collapse
existingDetailCard.remove();
expandButton.textContent = 'Show Error Details';
targetElement.innerHTML = `<span class="text-xs text-gray-400 mr-1">▶</span>${target}`;
expandButton.textContent = 'Show Error Details';
targetElement.innerHTML = `${target}`;
expandedTaskState.delete(upid);
} else {
// Create and show detail card - expand
const detailCard = _createMobileTaskDetailCard(task);
card.insertAdjacentElement('afterend', detailCard);
expandButton.textContent = 'Hide Error Details';
targetElement.innerHTML = `<span class="text-xs text-gray-400 mr-1">▼</span>${target}`;
expandButton.textContent = 'Hide Error Details';
targetElement.innerHTML = `${target}`;
expandedTaskState.add(upid);
}
});
@@ -449,7 +449,7 @@ PulseApp.ui.pbs = (() => {
// Add expand indicator for failed tasks
if (isFailed) {
targetCell.innerHTML = `<span class="text-xs text-gray-400 mr-1">▶</span>${target}`;
targetCell.innerHTML = `${target}`;
} else {
targetCell.textContent = target;
}
@@ -488,13 +488,13 @@ PulseApp.ui.pbs = (() => {
if (existingDetailRow && existingDetailRow.classList.contains('task-detail-row')) {
// Toggle existing detail row - collapse
existingDetailRow.remove();
targetCell.innerHTML = `<span class="text-xs text-gray-400 mr-1">▶</span>${target}`;
targetCell.innerHTML = `${target}`;
expandedTaskState.delete(upid); // Remove from global state
} else {
// Create and show detail row - expand
const detailRow = _createTaskDetailRow(task);
row.insertAdjacentElement('afterend', detailRow);
targetCell.innerHTML = `<span class="text-xs text-gray-400 mr-1">▼</span>${target}`;
targetCell.innerHTML = `${target}`;
expandedTaskState.add(upid); // Add to global state
}
});
@@ -658,7 +658,7 @@ PulseApp.ui.pbs = (() => {
const target = parsePbsTaskTarget(task);
const detailRow = _createTaskDetailRow(task);
taskRow.insertAdjacentElement('afterend', detailRow);
targetCell.innerHTML = `<span class="text-xs text-gray-400 mr-1">▼</span>${target}`;
targetCell.innerHTML = `${target}`;
}
});
@@ -739,9 +739,9 @@ PulseApp.ui.pbs = (() => {
let nameContent = ds.name || 'N/A';
if (usagePercent >= 95) {
nameElement.innerHTML = `<span class="text-red-700 dark:text-red-300">${nameContent}</span><div class="text-xs text-red-600 dark:text-red-400 font-normal mt-1">CRITICAL: ${usagePercent}% full</div>`;
nameElement.innerHTML = `<span class="text-red-700 dark:text-red-300">${nameContent}</span><div class="text-xs text-red-600 dark:text-red-400 font-normal mt-1">CRITICAL: ${usagePercent}% full</div>`;
} else if (usagePercent >= 85) {
nameElement.innerHTML = `<span class="text-yellow-700 dark:text-yellow-300">${nameContent}</span><div class="text-xs text-yellow-600 dark:text-yellow-400 font-normal mt-1">WARNING: ${usagePercent}% full</div>`;
nameElement.innerHTML = `<span class="text-yellow-700 dark:text-yellow-300">${nameContent}</span><div class="text-xs text-yellow-600 dark:text-yellow-400 font-normal mt-1">WARNING: ${usagePercent}% full</div>`;
} else {
nameElement.textContent = nameContent;
}
@@ -913,10 +913,10 @@ PulseApp.ui.pbs = (() => {
// Add usage alert to name if critical
let nameContent = ds.name || 'N/A';
if (usagePercent >= 95) {
nameContent = `${nameContent} [CRITICAL: ${usagePercent}% full]`;
nameContent = `${nameContent} [CRITICAL: ${usagePercent}% full]`;
createCell(nameContent, ['text-red-700', 'dark:text-red-300', 'font-semibold', 'sticky', 'left-0', 'bg-white', 'dark:bg-gray-800', 'z-10']);
} else if (usagePercent >= 85) {
nameContent = `${nameContent} [WARNING: ${usagePercent}% full]`;
nameContent = `${nameContent} [WARNING: ${usagePercent}% full]`;
createCell(nameContent, ['text-yellow-700', 'dark:text-yellow-300', 'font-semibold', 'sticky', 'left-0', 'bg-white', 'dark:bg-gray-800', 'z-10']);
} else {
createCell(nameContent, ['sticky', 'left-0', 'bg-white', 'dark:bg-gray-800', 'z-10']);
@@ -1124,12 +1124,12 @@ PulseApp.ui.pbs = (() => {
// More descriptive status text
let statusHtml = '';
if (failed > 0) {
statusHtml = `<span class="${CSS_CLASSES.TEXT_RED_600_DARK_RED_400} ${CSS_CLASSES.FONT_BOLD}">${failed} FAILED</span>`;
statusHtml = `<span class="${CSS_CLASSES.TEXT_RED_600_DARK_RED_400} ${CSS_CLASSES.FONT_BOLD}">${failed} FAILED</span>`;
if (ok > 0) {
statusHtml += ` / <span class="${CSS_CLASSES.TEXT_GREEN_600_DARK_GREEN_400}">${ok} OK</span>`;
}
} else if (ok > 0) {
statusHtml = `<span class="${CSS_CLASSES.TEXT_GREEN_600_DARK_GREEN_400}">All OK (${ok})</span>`;
statusHtml = `<span class="${CSS_CLASSES.TEXT_GREEN_600_DARK_GREEN_400}">All OK (${ok})</span>`;
} else {
statusHtml = `<span class="${CSS_CLASSES.TEXT_GRAY_600_DARK_GRAY_400}">No recent tasks</span>`;
}
@@ -1160,7 +1160,7 @@ PulseApp.ui.pbs = (() => {
const heading = document.createElement('h4');
heading.className = `${CSS_CLASSES.TEXT_MD} ${CSS_CLASSES.FONT_SEMIBOLD} ${CSS_CLASSES.MB2} ${CSS_CLASSES.TEXT_GRAY_700_DARK_GRAY_300}`;
heading.innerHTML = `Recent ${title} Tasks <span id="${tableId}-status" class="text-xs font-normal text-gray-500"></span><span id="${tableId}-priority" class="text-xs text-red-600 dark:text-red-400 ml-2 hidden">Failed tasks shown first</span>`;
heading.innerHTML = `Recent ${title} Tasks <span id="${tableId}-status" class="text-xs font-normal text-gray-500"></span><span id="${tableId}-priority" class="text-xs text-red-600 dark:text-red-400 ml-2 hidden">Failed tasks shown first</span>`;
fragment.appendChild(heading);
const tableContainer = document.createElement('div');
@@ -1434,7 +1434,7 @@ PulseApp.ui.pbs = (() => {
</div>
<div class="summary-card p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg text-center border border-gray-200 dark:border-gray-600">
<div class="text-lg font-semibold ${isServerHealthy ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400'}">
${isServerHealthy ? '' : ''}
${isServerHealthy ? 'OK' : 'ERROR'}
</div>
<div class="text-xs text-gray-600 dark:text-gray-400">Server</div>
<div class="text-xs ${isServerHealthy ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400'}">${isServerHealthy ? 'Online' : 'Error'}</div>
@@ -1482,14 +1482,11 @@ PulseApp.ui.pbs = (() => {
const overallHealthAndTitle = _calculateOverallHealth(pbsInstance);
const statusInfo = _getInstanceStatusInfo(pbsInstance);
let statusIcon = '✓';
let statusClass = 'text-green-600 dark:text-green-400';
if (pbsInstance.status === 'error') {
statusIcon = '✗';
statusClass = 'text-red-600 dark:text-red-400';
} else if (pbsInstance.status !== 'ok') {
statusIcon = '⚪';
statusClass = 'text-yellow-600 dark:text-yellow-400';
}
@@ -1503,7 +1500,6 @@ PulseApp.ui.pbs = (() => {
headerContent.innerHTML = `
<div class="flex items-center gap-2 mb-1">
<span class="${statusClass} text-sm">${statusIcon}</span>
${instanceNameHtml}
</div>
<div class="text-xs text-gray-500 dark:text-gray-400 truncate">${statusInfo.statusText}</div>
@@ -1511,7 +1507,7 @@ PulseApp.ui.pbs = (() => {
const chevron = document.createElement('span');
chevron.className = 'text-gray-400 transition-transform duration-200';
chevron.innerHTML = '';
chevron.innerHTML = '';
header.appendChild(headerContent);
header.appendChild(chevron);