diff --git a/server/notificationService.js b/server/notificationService.js index 4a3a0b4..565fdbf 100644 --- a/server/notificationService.js +++ b/server/notificationService.js @@ -48,15 +48,17 @@ class NotificationService { } } - async sendAlert(resource, incident) { - const message = incident.type === 'started' - ? `🔴 ${resource.name} is DOWN!\n\nURL: ${resource.url}\nTime: ${new Date().toLocaleString()}` - : `🟢 ${resource.name} is back UP!\n\nURL: ${resource.url}\nTime: ${new Date().toLocaleString()}`; + async sendAlert(resource, incident, stats = null) { + 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 = []; if (this.emailEnabled) { - promises.push(this.sendEmail(resource, message, incident.type)); + promises.push(this.sendEmail(resource, message, incident.type, stats)); } if (this.webhookEnabled) { @@ -66,18 +68,51 @@ class NotificationService { await Promise.allSettled(promises); } - async sendEmail(resource, message, type) { + async sendEmail(resource, message, type, stats = null) { if (!this.transporter) { console.error('Email config incomplete; skipping email'); return; } try { + let htmlContent = ` +
+

${type === 'started' ? '🔴 Alert' : '🟢 Recovered'}

+

Resource: ${resource.name}

+

Status: ${type === 'started' ? 'DOWN' : 'UP'}

+

URL: ${resource.url}

+

Check Type: ${resource.type || 'http'}

+

Time: ${new Date().toLocaleString()}

+ `; + + if (stats) { + htmlContent += ` +
+

Last 24 Hours Performance

+

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 += `
`; + } + + htmlContent += `
`; + await this.transporter.sendMail({ from: this.config.email_from, to: this.config.email_to, subject: `Alert: ${resource.name} is ${type === 'started' ? 'DOWN' : 'UP'}`, text: message, + html: htmlContent, }); console.log(`Email sent for ${resource.name}`); } catch (error) { @@ -85,6 +120,37 @@ class NotificationService { } } + generateAsciiGraph(recentChecks) { + if (!recentChecks || recentChecks.length === 0) return ''; + + // Get last 12 checks and their response times + const checks = recentChecks.slice(-12); + const times = checks.map(c => c.response_time || 0); + const maxTime = Math.max(...times); + + if (maxTime === 0) return 'No data'; + + // Simple ASCII bar chart (8 rows) + const height = 8; + let graph = ''; + + for (let row = height; row > 0; row--) { + const threshold = (maxTime / height) * row; + for (const time of times) { + graph += time >= threshold ? '█' : ' '; + } + graph += '\n'; + } + + // Add baseline + graph += times.map(() => '─').join('') + '\n'; + + // Add labels + graph += `0ms${' '.repeat(Math.max(0, times.length - 5))}${(maxTime).toFixed(0)}ms`; + + return graph; + } + async sendWebhook(resource, message, type) { try { await axios.post(this.config.webhook_url || process.env.WEBHOOK_URL, { diff --git a/server/scheduler.js b/server/scheduler.js index c43fef7..d22cdee 100644 --- a/server/scheduler.js +++ b/server/scheduler.js @@ -41,7 +41,15 @@ class Scheduler { ); if (incident.type !== 'none') { - await notificationService.sendAlert(resource, incident); + const stats = monitorService.getResourceStats(resource.id, 24); + const recentChecks = db.prepare(` + SELECT response_time, status FROM checks + WHERE resource_id = ? + ORDER BY checked_at DESC + LIMIT 12 + `).all(resource.id); + stats.recentChecks = recentChecks.reverse(); + await notificationService.sendAlert(resource, incident, stats); } console.log(`Checked ${resource.name}: ${result.status} (${result.response_time}ms)`);