const nodemailer = require('nodemailer'); const axios = require('axios'); require('dotenv').config(); class NotificationService { constructor() { this.applyConfigFromEnv(); } applyConfigFromEnv() { this.setConfig({ email_enabled: process.env.EMAIL_ENABLED === 'true', email_host: process.env.EMAIL_HOST, email_port: parseInt(process.env.EMAIL_PORT), email_user: process.env.EMAIL_USER, email_pass: process.env.EMAIL_PASS, email_from: process.env.EMAIL_FROM, email_to: process.env.EMAIL_TO, webhook_enabled: process.env.WEBHOOK_ENABLED === 'true', webhook_url: process.env.WEBHOOK_URL, }); } setConfig(config) { this.emailEnabled = !!config.email_enabled; this.webhookEnabled = !!config.webhook_enabled; this.config = config; // Recreate transporter when email config changes; skip if incomplete if ( this.emailEnabled && config.email_host && config.email_port && config.email_user && config.email_pass ) { this.transporter = nodemailer.createTransport({ host: config.email_host, port: parseInt(config.email_port), secure: false, auth: { user: config.email_user, pass: config.email_pass, }, }); } else { this.transporter = null; } } async sendAlert(resource, incident, stats = null) { // Check if incident is acknowledged if (incident.acknowledged) { console.log(`Incident ${incident.id} already acknowledged, skipping alert`); return; } // Check quiet hours if (this.isQuietHours(resource)) { console.log(`Resource ${resource.name} is in quiet hours, skipping alert`); return; } const isDown = incident.type === 'started'; const statusEmoji = isDown ? '🔴' : '🟢'; const statusText = isDown ? 'DOWN' : 'UP'; const message = `${statusEmoji} ${resource.name} is ${statusText}!\n\nURL: ${resource.url}\nCheck Type: ${resource.type || 'http'}\nTime: ${new Date().toLocaleString()}`; const promises = []; // Use resource-specific email if provided, otherwise fall back to global config if (this.emailEnabled && (resource.email_to || this.config.email_to)) { const chosenEmail = resource.email_to || this.config.email_to; console.log(`[Alert] Email target for ${resource.name}: ${chosenEmail || 'none'} (enabled=${this.emailEnabled})`); promises.push(this.sendEmail(resource, message, incident.type, stats)); } else { if (!this.emailEnabled) { console.log(`[Alert] Email disabled via config; skipping email for ${resource.name}`); } else { console.log(`[Alert] No email target for ${resource.name}; skipping email`); } } if (this.webhookEnabled) { promises.push(this.sendWebhook(resource, message, incident.type)); } await Promise.allSettled(promises); } isQuietHours(resource) { if (!resource.quiet_hours_start || !resource.quiet_hours_end) { return false; } const now = new Date(); const currentTime = now.getHours() * 60 + now.getMinutes(); const [startHour, startMin] = resource.quiet_hours_start.split(':').map(Number); const [endHour, endMin] = resource.quiet_hours_end.split(':').map(Number); const startTime = startHour * 60 + startMin; const endTime = endHour * 60 + endMin; // Handle overnight quiet hours if (startTime > endTime) { return currentTime >= startTime || currentTime < endTime; } return currentTime >= startTime && currentTime < endTime; } async sendEmail(resource, message, type, stats = null) { if (!this.transporter) { console.error(`[Email] Config incomplete; skipping email (enabled=${this.emailEnabled}, host=${this.config.email_host})`); return; } try { let htmlContent = `
Resource: ${resource.name}
Status: ${type === 'started' ? 'DOWN' : 'UP'}
URL: ${resource.url}
Check Type: ${resource.type || 'http'}
Time: ${new Date().toLocaleString()}
`; if (stats) { htmlContent += `Uptime: ${(stats.uptime * 100).toFixed(2)}%
Avg Response Time: ${stats.avgResponseTime.toFixed(0)}ms
Last Check: ${stats.lastCheck || 'Never'}
`; if (stats.recentChecks && stats.recentChecks.length > 0) { const graph = this.generateAsciiGraph(stats.recentChecks); htmlContent += `Recent Response Times (last 12 checks):
${graph}
`;
}
htmlContent += `