// Load environment variables from .env file // Check for persistent config directory (Docker) or use project root const fs = require('fs'); const path = require('path'); const configDir = path.join(__dirname, '../config'); const configEnvPath = path.join(configDir, '.env'); const projectEnvPath = path.join(__dirname, '../.env'); if (fs.existsSync(configEnvPath)) { require('dotenv').config({ path: configEnvPath }); } else { require('dotenv').config({ path: projectEnvPath }); } // Import the state manager FIRST const stateManager = require('./state'); // Import metrics history system const metricsHistory = require('./metricsHistory'); // Import diagnostic tool const DiagnosticTool = require('./diagnostics'); // --- BEGIN Configuration Loading using configLoader --- const { loadConfiguration, ConfigurationError } = require('./configLoader'); let endpoints; let pbsConfigs; let configIsPlaceholder = false; // Define placeholder flag variable here try { const { endpoints: loadedEndpoints, pbsConfigs: loadedPbsConfigs, isConfigPlaceholder: loadedPlaceholderFlag } = loadConfiguration(); endpoints = loadedEndpoints; pbsConfigs = loadedPbsConfigs; configIsPlaceholder = loadedPlaceholderFlag; // Store flag temporarily } catch (error) { if (error instanceof ConfigurationError) { console.error(error.message); process.exit(1); // Exit if configuration loading failed } else { console.error('An unexpected error occurred during configuration loading:', error); process.exit(1); // Exit on other unexpected errors during load } } // --- END Configuration Loading --- // Set the placeholder status in stateManager *after* config loading is complete stateManager.setConfigPlaceholderStatus(configIsPlaceholder); // Store globally for config reload global.pulseConfigStatus = { isPlaceholder: configIsPlaceholder }; // Set endpoint configurations for client use stateManager.setEndpointConfigurations(endpoints, pbsConfigs); const express = require('express'); const http = require('http'); const cors = require('cors'); const compression = require('compression'); const { Server } = require('socket.io'); const axios = require('axios'); const axiosRetry = require('axios-retry').default; // Import axios-retry // Hot reload dependencies (always try to load for development convenience) let chokidar; try { chokidar = require('chokidar'); } catch (e) { console.warn('chokidar is not installed. Hot reload requires chokidar: npm install chokidar'); } // --- API Client Initialization --- const { initializeApiClients } = require('./apiClients'); let apiClients = {}; // Initialize as empty objects let pbsApiClients = {}; // Note: Client initialization is now async and happens in startServer() // --- END API Client Initialization --- // Configuration API const ConfigApi = require('./configApi'); const configApi = new ConfigApi(); // --- Data Fetching (Imported) --- const { fetchDiscoveryData, fetchMetricsData } = require('./dataFetcher'); // --- END Data Fetching --- // Server configuration const PORT = 7655; // Using a different port from the main server // --- Define Update Intervals (Configurable via Env Vars) --- const METRIC_UPDATE_INTERVAL = parseInt(process.env.PULSE_METRIC_INTERVAL_MS, 10) || 2000; // Default: 2 seconds const DISCOVERY_UPDATE_INTERVAL = parseInt(process.env.PULSE_DISCOVERY_INTERVAL_MS, 10) || 30000; // Default: 30 seconds console.log(`INFO: Using Metric Update Interval: ${METRIC_UPDATE_INTERVAL}ms`); console.log(`INFO: Using Discovery Update Interval: ${DISCOVERY_UPDATE_INTERVAL}ms`); // Initialize enhanced state management stateManager.init(); // Create Express app const app = express(); const server = http.createServer(app); // Create HTTP server instance // Middleware app.use(compression({ filter: (req, res) => { // Don't compress responses with this request header if (req.headers['x-no-compression']) { return false; } // Fallback to standard filter function return compression.filter(req, res); }, threshold: 1024, // Only compress if response is over 1KB level: 6 // Compression level (1-9, 6 is good balance of speed vs compression) })); app.use(cors()); app.use(express.json()); // Define the public directory path const publicDir = path.join(__dirname, '../src/public'); // Serve static files (CSS, JS, images) from the public directory app.use(express.static(publicDir, { index: false })); // Route to serve the main HTML file for the root path app.get('/', (req, res) => { // Always serve the main application with settings modal for initial configuration const indexPath = path.join(publicDir, 'index.html'); res.sendFile(indexPath, (err) => { if (err) { console.error(`Error sending index.html: ${err.message}`); // Avoid sending error details to the client for security res.status(err.status || 500).send('Internal Server Error loading page.'); } }); }); // Route to explicitly handle setup page app.get('/setup.html', (req, res) => { const setupPath = path.join(publicDir, 'setup.html'); res.sendFile(setupPath, (err) => { if (err) { console.error(`Error sending setup.html: ${err.message}`); res.status(err.status || 500).send('Internal Server Error loading setup page.'); } }); }); // --- API Routes --- // Set up configuration API routes configApi.setupRoutes(app); // Set up threshold API routes const { setupThresholdRoutes } = require('./thresholdRoutes'); setupThresholdRoutes(app); // Set up update API routes const UpdateManager = require('./updateManager'); const updateManager = new UpdateManager(); // Check for updates endpoint app.get('/api/updates/check', async (req, res) => { try { // Check if we're in test mode if (process.env.UPDATE_TEST_MODE === 'true') { const testVersion = process.env.UPDATE_TEST_VERSION || '99.99.99'; const currentVersion = updateManager.currentVersion; // Create mock update data const mockUpdateInfo = { currentVersion: currentVersion, latestVersion: testVersion, updateAvailable: true, isDocker: updateManager.isDockerEnvironment(), releaseNotes: 'Test release for update mechanism testing\n\n- Testing download functionality\n- Testing backup process\n- Testing installation process', releaseUrl: 'https://github.com/rcourtman/Pulse/releases/test', publishedAt: new Date().toISOString(), assets: [{ name: 'pulse-v' + testVersion + '.tar.gz', size: 1024000, downloadUrl: 'http://localhost:3000/api/test/mock-update.tar.gz' }] }; console.log('[UpdateManager] Test mode enabled, returning mock update info'); return res.json(mockUpdateInfo); } // Allow override of update channel via query parameter for preview const channelOverride = req.query.channel; const updateInfo = await updateManager.checkForUpdates(channelOverride); res.json(updateInfo); } catch (error) { console.error('Error checking for updates:', error); res.status(500).json({ error: error.message }); } }); // Download and apply update endpoint app.post('/api/updates/apply', async (req, res) => { try { const { downloadUrl } = req.body; if (!downloadUrl) { return res.status(400).json({ error: 'Download URL is required' }); } // Send immediate response res.json({ message: 'Update started. The application will restart automatically when complete.', status: 'in_progress' }); // Apply update in background setTimeout(async () => { try { // Download update const updateFile = await updateManager.downloadUpdate(downloadUrl, (progress) => { io.emit('updateProgress', progress); }); // Apply update (pass download URL for version extraction) await updateManager.applyUpdate(updateFile, (progress) => { io.emit('updateProgress', progress); }, downloadUrl); io.emit('updateComplete', { success: true }); } catch (error) { console.error('Error applying update:', error); io.emit('updateError', { error: error.message }); } }, 100); } catch (error) { console.error('Error initiating update:', error); res.status(500).json({ error: error.message }); } }); // Update status endpoint app.get('/api/updates/status', (req, res) => { try { const status = updateManager.getUpdateStatus(); res.json(status); } catch (error) { console.error('Error getting update status:', error); res.status(500).json({ error: error.message }); } }); // Mock update tarball endpoint for testing (not actually used in test mode) app.get('/api/test/mock-update.tar.gz', (req, res) => { if (process.env.UPDATE_TEST_MODE !== 'true') { return res.status(404).json({ error: 'Test mode not enabled' }); } // This endpoint exists just to make the URL valid // The actual download is handled differently in test mode res.status(200).json({ message: 'Test mode active - download handled internally', testMode: true }); }); // Health check endpoint app.get('/healthz', (req, res) => { res.status(200).send('OK'); }); // Enhanced health endpoint with detailed monitoring info app.get('/api/health', (req, res) => { try { const healthSummary = stateManager.getHealthSummary(); // Add system info including placeholder status const state = stateManager.getState(); healthSummary.system = { configPlaceholder: state.isConfigPlaceholder || false, hasData: stateManager.hasData(), clientsInitialized: Object.keys(global.pulseApiClients?.apiClients || {}).length > 0 }; res.json(healthSummary); } catch (error) { console.error("Error in /api/health:", error); res.status(500).json({ error: "Failed to fetch health information" }); } }); // Performance metrics endpoint app.get('/api/performance', (req, res) => { try { const limit = parseInt(req.query.limit) || 50; const performanceHistory = stateManager.getPerformanceHistory(limit); const connectionHealth = stateManager.getConnectionHealth(); res.json({ history: performanceHistory, connections: connectionHealth, timestamp: Date.now() }); } catch (error) { console.error("Error in /api/performance:", error); res.status(500).json({ error: "Failed to fetch performance data" }); } }); // Enhanced alerts endpoint with filtering app.get('/api/alerts', (req, res) => { try { const filters = { severity: req.query.severity, group: req.query.group, node: req.query.node, acknowledged: req.query.acknowledged === 'true' ? true : req.query.acknowledged === 'false' ? false : undefined }; // Get alert data with safe serialization let alertInfo; try { const activeAlerts = stateManager.alertManager.getActiveAlerts(filters); const stats = stateManager.alertManager.getEnhancedAlertStats(); const rules = stateManager.alertManager.getRules(); alertInfo = { active: activeAlerts, stats: stats, rules: rules }; // Test serialization of each part to identify the issue JSON.stringify(activeAlerts); JSON.stringify(stats); JSON.stringify(rules); } catch (serializationError) { console.error('[API] Serialization error in /api/alerts:', serializationError.message); // Return empty data if serialization fails alertInfo = { active: [], stats: { active: 0, acknowledged: 0, last24Hours: 0, lastHour: 0, totalRules: 0, suppressedRules: 0, metrics: { totalFired: 0, totalResolved: 0, totalAcknowledged: 0, averageResolutionTime: 0, falsePositiveRate: 0 }, groups: [] }, rules: [] }; } res.json(alertInfo); } catch (error) { console.error("Error in /api/alerts:", error); res.status(500).json({ error: "Failed to fetch alert information" }); } }); // Alert history endpoint with pagination and filtering app.get('/api/alerts/history', (req, res) => { try { const limit = parseInt(req.query.limit) || 100; const filters = { severity: req.query.severity, group: req.query.group, node: req.query.node }; const history = stateManager.alertManager.getAlertHistory(limit, filters); res.json({ history, timestamp: Date.now() }); } catch (error) { console.error("Error in /api/alerts/history:", error); res.status(500).json({ error: "Failed to fetch alert history" }); } }); // Alert acknowledgment endpoint app.post('/api/alerts/:alertId/acknowledge', (req, res) => { try { const alertId = req.params.alertId; const { userId = 'api-user', note = '' } = req.body; const success = stateManager.alertManager.acknowledgeAlert(alertId, userId, note); if (success) { res.json({ success: true, message: "Alert acknowledged successfully" }); } else { res.status(404).json({ error: "Alert not found" }); } } catch (error) { console.error("Error acknowledging alert:", error); res.status(400).json({ error: error.message }); } }); // Alert suppression endpoint app.post('/api/alerts/suppress', (req, res) => { try { const { ruleId, guestFilter = {}, duration = 3600000, reason = '' } = req.body; if (!ruleId) { return res.status(400).json({ error: "ruleId is required" }); } const success = stateManager.alertManager.suppressAlert(ruleId, guestFilter, duration, reason); if (success) { res.json({ success: true, message: "Alert rule suppressed successfully" }); } else { res.status(400).json({ error: "Failed to suppress alert rule" }); } } catch (error) { console.error("Error suppressing alert:", error); res.status(400).json({ error: error.message }); } }); // Alert groups endpoint app.get('/api/alerts/groups', (req, res) => { try { const stats = stateManager.alertManager.getEnhancedAlertStats(); res.json({ groups: stats.groups }); } catch (error) { console.error("Error in /api/alerts/groups:", error); res.status(500).json({ error: "Failed to fetch alert groups" }); } }); // Enhanced alert metrics endpoint app.get('/api/alerts/metrics', (req, res) => { try { const stats = stateManager.alertManager.getEnhancedAlertStats(); res.json({ metrics: stats.metrics, summary: { active: stats.active, acknowledged: stats.acknowledged, suppressed: stats.suppressedRules }, trends: { last24Hours: stats.last24Hours, lastHour: stats.lastHour }, timestamp: Date.now() }); } catch (error) { console.error("Error in /api/alerts/metrics:", error); res.status(500).json({ error: "Failed to fetch alert metrics" }); } }); // Test email configuration app.post('/api/alerts/test-email', async (req, res) => { try { console.log('[Test Email] Sending test email...'); // Get email configuration from process.env (loaded by configLoader) const config = { ALERT_TO_EMAIL: process.env.ALERT_TO_EMAIL, ALERT_FROM_EMAIL: process.env.ALERT_FROM_EMAIL, SMTP_HOST: process.env.SMTP_HOST, SMTP_PORT: process.env.SMTP_PORT, SMTP_USER: process.env.SMTP_USER, SMTP_SECURE: process.env.SMTP_SECURE }; console.log('[Test Email] Config loaded, ALERT_TO_EMAIL:', config.ALERT_TO_EMAIL); if (!config.ALERT_TO_EMAIL) { return res.status(400).json({ success: false, error: 'No recipient email address configured' }); } if (!config.SMTP_HOST) { return res.status(400).json({ success: false, error: 'SMTP server not configured' }); } // Use the alert manager to send a test email with the config const testResult = await stateManager.alertManager.sendTestEmailWithConfig(config); if (testResult.success) { console.log('[Test Email] Test email sent successfully'); res.json({ success: true, message: 'Test email sent successfully' }); } else { console.error('[Test Email] Failed to send test email:', testResult.error); res.status(400).json({ success: false, error: testResult.error || 'Failed to send test email' }); } } catch (error) { console.error('[Test Email] Error sending test email:', error); res.status(500).json({ success: false, error: 'Internal server error while sending test email' }); } }); // Alert rules management with filtering app.get('/api/alerts/rules', (req, res) => { try { const filters = { group: req.query.group, severity: req.query.severity }; const rules = stateManager.alertManager.getRules(filters); res.json({ rules }); } catch (error) { console.error("Error in /api/alerts/rules:", error); res.status(500).json({ error: "Failed to fetch alert rules" }); } }); // Create new alert rule app.post('/api/alerts/rules', (req, res) => { try { const rule = req.body; const newRule = stateManager.alertManager.addRule(rule); res.json({ success: true, message: "Rule added successfully", rule: newRule }); } catch (error) { console.error("Error adding alert rule:", error); res.status(400).json({ error: error.message }); } }); // Update alert rule app.put('/api/alerts/rules/:id', (req, res) => { try { const ruleId = req.params.id; const updates = req.body; const success = stateManager.alertManager.updateRule(ruleId, updates); if (success) { res.json({ success: true, message: "Rule updated successfully" }); } else { res.status(404).json({ error: "Rule not found" }); } } catch (error) { console.error("Error updating alert rule:", error); res.status(400).json({ error: error.message }); } }); // Delete alert rule app.delete('/api/alerts/rules/:id', (req, res) => { try { const ruleId = req.params.id; const success = stateManager.alertManager.removeRule(ruleId); if (success) { res.json({ success: true, message: "Rule removed successfully" }); } else { res.status(404).json({ error: "Rule not found" }); } } catch (error) { console.error("Error removing alert rule:", error); res.status(400).json({ error: error.message }); } }); // 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" }); } }); // Debug endpoint to manually reload alert rules app.post('/api/alerts/rules/reload', async (req, res) => { try { await stateManager.alertManager.loadAlertRules(); const allRules = stateManager.alertManager.getRules(); res.json({ success: true, message: "Alert rules reloaded", rulesCount: allRules.length }); } catch (error) { console.error("Error reloading alert rules:", error); res.status(500).json({ error: "Failed to reload alert rules" }); } }); // Endpoint to trigger immediate alert evaluation app.post('/api/alerts/evaluate', async (req, res) => { try { stateManager.alertManager.evaluateCurrentState(); res.json({ success: true, message: "Alert evaluation triggered" }); } catch (error) { console.error("Error triggering alert evaluation:", error); res.status(500).json({ error: "Failed to trigger alert evaluation" }); } }); // Debug endpoint for alert threshold evaluation (without rule ID) app.get('/api/alerts/debug', (req, res) => { try { const ruleId = req.query.ruleId; const currentState = stateManager.getState(); const allGuests = [...(currentState.vms || []), ...(currentState.containers || [])]; const metrics = currentState.metrics || []; const debugInfo = { timestamp: new Date().toISOString(), totalGuests: allGuests.length, totalMetrics: metrics.length, debugMode: process.env.ALERT_DEBUG === 'true', guests: [] }; // If specific rule requested, filter to that rule let rulesToCheck = stateManager.alertManager.getRules(); if (ruleId) { rulesToCheck = rulesToCheck.filter(r => r.id === ruleId); if (rulesToCheck.length === 0) { return res.status(404).json({ error: `Rule '${ruleId}' not found` }); } } // Evaluate each guest against each rule allGuests.forEach(guest => { const guestMetrics = metrics.find(m => m.endpointId === guest.endpointId && m.node === guest.node && m.id === guest.vmid ); const guestDebug = { name: guest.name, vmid: guest.vmid, node: guest.node, type: guest.type, hasMetrics: !!guestMetrics, rules: [] }; if (guestMetrics && guestMetrics.current) { // Calculate disk percentage if available if (guest.maxdisk && guestMetrics.current.disk) { const diskPercentage = (guestMetrics.current.disk / guest.maxdisk) * 100; guestDebug.diskUsage = { raw: guestMetrics.current.disk, max: guest.maxdisk, percentage: Math.round(diskPercentage * 100) / 100 }; } guestDebug.currentMetrics = { cpu: guestMetrics.current.cpu, memory: guestMetrics.current.memory, disk: guestMetrics.current.disk }; } // Check each rule rulesToCheck.forEach(rule => { if (rule.type === 'compound_threshold' && rule.thresholds) { const ruleDebug = { ruleId: rule.id, ruleName: rule.name, thresholds: [], allThresholdsMet: true }; rule.thresholds.forEach(threshold => { let metricValue = null; let thresholdMet = false; if (guestMetrics && guestMetrics.current) { metricValue = stateManager.alertManager.evaluateThresholdCondition(threshold, guestMetrics.current, guest) ? stateManager.alertManager.getThresholdCurrentValue(threshold, guestMetrics.current, guest) : null; thresholdMet = stateManager.alertManager.evaluateThresholdCondition(threshold, guestMetrics.current, guest); } ruleDebug.thresholds.push({ metric: threshold.metric, condition: threshold.condition, threshold: threshold.threshold, currentValue: metricValue, met: thresholdMet }); if (!thresholdMet) { ruleDebug.allThresholdsMet = false; } }); guestDebug.rules.push(ruleDebug); } }); debugInfo.guests.push(guestDebug); }); res.json(debugInfo); } catch (error) { console.error("Error in alert debug endpoint:", error); res.status(500).json({ error: "Failed to generate debug information" }); } }); // Simple endpoint to get just the alert enabled/disabled status app.get('/api/alerts/status', (req, res) => { try { const { loadConfiguration } = require('./configLoader'); const { endpoints, pbsConfigs, isConfigPlaceholder } = loadConfiguration(); // Read the environment variables directly const alertStatus = { cpu: process.env.ALERT_CPU_ENABLED !== 'false', memory: process.env.ALERT_MEMORY_ENABLED !== 'false', disk: process.env.ALERT_DISK_ENABLED !== 'false', down: process.env.ALERT_DOWN_ENABLED === 'true' }; res.json({ success: true, alerts: alertStatus }); } catch (error) { console.error("Error getting alert status:", error); res.status(500).json({ error: "Failed to get alert status" }); } }); // Version API endpoint app.get('/api/version', async (req, res) => { try { const { getCurrentVersionInfo } = require('./versionUtils'); // Get version info using centralized logic const versionInfo = getCurrentVersionInfo(); const currentVersion = versionInfo.version; const gitBranch = versionInfo.gitBranch; const isDevelopment = versionInfo.isDevelopment; let latestVersion = currentVersion; let updateAvailable = false; try { // Try to check for updates, but don't fail if it doesn't work const updateInfo = await updateManager.checkForUpdates(); latestVersion = updateInfo.latestVersion || currentVersion; updateAvailable = updateInfo.hasUpdate || false; } catch (updateError) { // Log the error but continue with current version info console.error("[Version API] Error checking for updates:", updateError.message); } res.json({ version: currentVersion, latestVersion: latestVersion, updateAvailable: updateAvailable, gitBranch: gitBranch, isDevelopment: isDevelopment }); } catch (error) { console.error("[Version API] Error in version endpoint:", error); // Still try to return current version if possible try { const packageJson = require('../package.json'); res.json({ version: packageJson.version || 'N/A', latestVersion: packageJson.version || 'N/A', updateAvailable: false }); } catch (fallbackError) { res.status(500).json({ error: "Could not retrieve version" }); } } }); app.get('/api/storage', async (req, res) => { try { // Get current nodes from state manager const { nodes: currentNodes } = stateManager.getState(); const storageInfoByNode = {}; (currentNodes || []).forEach(node => { storageInfoByNode[node.node] = node.storage || []; }); res.json(storageInfoByNode); } catch (error) { console.error("Error in /api/storage:", error); res.status(500).json({ globalError: error.message || "Failed to fetch storage details." }); } }); // Chart data API endpoint app.get('/api/charts', async (req, res) => { try { // Get current guest info for context const currentState = stateManager.getState(); const guestInfoMap = {}; // Build guest info map [...(currentState.vms || []), ...(currentState.containers || [])].forEach(guest => { const guestId = `${guest.endpointId}-${guest.node}-${guest.vmid}`; guestInfoMap[guestId] = { maxmem: guest.maxmem, maxdisk: guest.maxdisk, type: guest.type }; }); const chartData = metricsHistory.getAllGuestChartData(guestInfoMap); const stats = metricsHistory.getStats(); res.json({ data: chartData, stats: stats, timestamp: Date.now() }); } catch (error) { console.error("Error in /api/charts:", error); res.status(500).json({ error: error.message || "Failed to fetch chart data." }); } }); // Direct state inspection endpoint app.get('/api/diagnostics-state', (req, res) => { try { const state = stateManager.getState(); const summary = { timestamp: new Date().toISOString(), last_update: state.lastUpdate, update_age_seconds: state.lastUpdate ? Math.floor((Date.now() - new Date(state.lastUpdate).getTime()) / 1000) : null, guests_count: state.guests?.length || 0, nodes_count: state.nodes?.length || 0, pbs_count: state.pbs?.length || 0, sample_guests: state.guests?.slice(0, 5).map(g => ({ vmid: g.vmid, name: g.name, type: g.type, status: g.status })) || [], sample_backups: [], errors: state.errors || [] }; // Get sample backups if (state.pbs && Array.isArray(state.pbs)) { state.pbs.forEach(pbsInstance => { if (pbsInstance.datastores) { pbsInstance.datastores.forEach(ds => { if (ds.snapshots && ds.snapshots.length > 0) { ds.snapshots.slice(0, 5).forEach(snap => { summary.sample_backups.push({ store: ds.store, backup_id: snap['backup-id'], backup_type: snap['backup-type'], backup_time: new Date(snap['backup-time'] * 1000).toISOString() }); }); } }); } }); } res.json(summary); } catch (error) { console.error("State inspection error:", error); res.status(500).json({ error: error.message }); } }); // Quick diagnostic check endpoint app.get('/api/diagnostics/check', async (req, res) => { try { // Use cached result if available and recent const cacheKey = 'diagnosticCheck'; const cached = global.diagnosticCache?.[cacheKey]; if (cached && (Date.now() - cached.timestamp) < 60000) { // Cache for 1 minute return res.json(cached.result); } // Run a quick check delete require.cache[require.resolve('./diagnostics')]; const DiagnosticTool = require('./diagnostics'); const diagnosticTool = new DiagnosticTool(stateManager, metricsHistory, apiClients, pbsApiClients); const report = await diagnosticTool.runDiagnostics(); const hasIssues = report.recommendations && report.recommendations.some(r => r.severity === 'critical' || r.severity === 'warning'); const result = { hasIssues, criticalCount: report.recommendations?.filter(r => r.severity === 'critical').length || 0, warningCount: report.recommendations?.filter(r => r.severity === 'warning').length || 0 }; // Cache the result if (!global.diagnosticCache) global.diagnosticCache = {}; global.diagnosticCache[cacheKey] = { timestamp: Date.now(), result }; res.json(result); } catch (error) { console.error("Error in diagnostic check:", error); res.json({ hasIssues: false }); // Don't show icon on error } }); // Raw state endpoint - shows everything app.get('/api/raw-state', (req, res) => { const state = stateManager.getState(); const rawState = stateManager.state || {}; res.json({ lastUpdate: state.lastUpdate, statsLastUpdated: state.stats?.lastUpdated, rawStateLastUpdated: rawState.stats?.lastUpdated, guestsLength: state.guests?.length, rawGuestsLength: rawState.guests?.length, guestsType: Array.isArray(state.guests) ? 'array' : typeof state.guests, allKeys: Object.keys(state), rawKeys: Object.keys(rawState), serverUptime: process.uptime(), // Sample guest to see structure firstGuest: state.guests?.[0], rawFirstGuest: rawState.guests?.[0] }); }); // --- Diagnostic Endpoint --- app.get('/api/diagnostics', async (req, res) => { try { console.log('Running diagnostics...'); // Force reload the diagnostic module to get latest changes delete require.cache[require.resolve('./diagnostics')]; const DiagnosticTool = require('./diagnostics'); const diagnosticTool = new DiagnosticTool(stateManager, metricsHistory, apiClients, pbsApiClients); const report = await diagnosticTool.runDiagnostics(); // Format the report for easy reading const formattedReport = { ...report, summary: { hasIssues: report.recommendations && report.recommendations.some(r => r.severity === 'critical'), criticalIssues: report.recommendations ? report.recommendations.filter(r => r.severity === 'critical').length : 0, warnings: report.recommendations ? report.recommendations.filter(r => r.severity === 'warning').length : 0, info: report.recommendations ? report.recommendations.filter(r => r.severity === 'info').length : 0, isTimingIssue: report.state && report.state.dataAge === null && report.state.serverUptime < 90 } }; res.json(formattedReport); } catch (error) { console.error("Error running diagnostics:", error); console.error("Stack trace:", error.stack); res.status(500).json({ error: "Failed to run diagnostics", details: error.message, stack: error.stack }); } }); // Test email endpoint app.post('/api/test-email', async (req, res) => { try { const { host, port, user, pass, from, to, secure } = req.body; if (!host || !port || !user || !pass || !from || !to) { return res.status(400).json({ success: false, error: 'All email fields are required for testing' }); } // Create a temporary transporter for testing const nodemailer = require('nodemailer'); const testTransporter = nodemailer.createTransport({ host: host, port: parseInt(port), secure: secure === true, // true for 465, false for other ports requireTLS: true, // Force TLS encryption auth: { user: user, pass: pass }, tls: { // Do not fail on invalid certs for testing rejectUnauthorized: false } }); // Send test email const testMailOptions = { from: from, to: to, subject: '🧪 Pulse Email Test - Configuration Successful', text: ` This is a test email from your Pulse monitoring system. If you received this email, your SMTP configuration is working correctly! Configuration used: - SMTP Host: ${host} - SMTP Port: ${port} - Secure: ${secure ? 'Yes' : 'No'} - From: ${from} - To: ${to} You will now receive alert notifications when VMs/LXCs exceed their configured thresholds. Best regards, Pulse Monitoring System `, html: `
Configuration Successful!
Congratulations! If you received this email, your SMTP configuration is working correctly.
You will now receive alert notifications when VMs/LXCs exceed their configured thresholds.
This test email was sent by your Pulse monitoring system.