Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Dashboard/MainWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ public MainWindow()

_credentialService = new CredentialService();
_emailAlertService = new EmailAlertService(_preferencesService);
_ = new WebhookAlertService(_preferencesService);

_alertCheckTimer = new DispatcherTimer();
_alertCheckTimer.Tick += AlertCheckTimer_Tick;
Expand Down
10 changes: 10 additions & 0 deletions Dashboard/Models/UserPreferences.cs
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,16 @@ public int EmailCooldownMinutes
public string SmtpFromAddress { get; set; } = "";
public string SmtpRecipients { get; set; } = "";

// Teams webhook settings
public bool TeamsWebhookEnabled { get; set; } = false;
public string TeamsWebhookUrl { get; set; } = "";
public string TeamsProxyAddress { get; set; } = "";

// Slack webhook settings
public bool SlackWebhookEnabled { get; set; } = false;
public string SlackWebhookUrl { get; set; } = "";
public string SlackProxyAddress { get; set; } = "";

// MCP server settings
public bool McpEnabled { get; set; } = false;
public int McpPort { get; set; } = 5150;
Expand Down
105 changes: 56 additions & 49 deletions Dashboard/Services/EmailAlertService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,8 @@ public EmailAlertService(UserPreferencesService preferencesService)
}

/// <summary>
/// Attempts to send an alert email if SMTP is enabled and cooldown has elapsed.
/// Attempts to send alert notifications (email, Teams, Slack) based on enabled channels.
/// Each channel operates independently — disabling email does not affect webhooks.
/// Never throws.
/// </summary>
public async Task TrySendAlertEmailAsync(
Expand All @@ -78,64 +79,70 @@ public async Task TrySendAlertEmailAsync(
try
{
var prefs = _preferencesService.GetPreferences();
if (!prefs.SmtpEnabled) return;

if (string.IsNullOrWhiteSpace(prefs.SmtpServer) ||
string.IsNullOrWhiteSpace(prefs.SmtpFromAddress) ||
string.IsNullOrWhiteSpace(prefs.SmtpRecipients))
{
return;
}

var cooldownKey = $"{serverId}:{metricName}";
if (_cooldowns.TryGetValue(cooldownKey, out var lastSent) &&
DateTime.UtcNow - lastSent < TimeSpan.FromMinutes(prefs.EmailCooldownMinutes))
{
return;
}

var subject = $"[SQL Monitor Alert] {metricName} on {serverName}";
var (htmlBody, plainTextBody) = EmailTemplateBuilder.BuildAlertEmail(
metricName, serverName, currentValue, thresholdValue, prefs.EmailCooldownMinutes, context);

string? sendError = null;
bool sent = false;
string notificationType = "tray";

try
/* Attempt email delivery if SMTP is fully configured */
if (prefs.SmtpEnabled &&
!string.IsNullOrWhiteSpace(prefs.SmtpServer) &&
!string.IsNullOrWhiteSpace(prefs.SmtpFromAddress) &&
!string.IsNullOrWhiteSpace(prefs.SmtpRecipients))
{
await SendEmailAsync(prefs, subject, htmlBody, plainTextBody, context);
sent = true;
_cooldowns[cooldownKey] = DateTime.UtcNow;
var cooldownKey = $"{serverId}:{metricName}";
var withinCooldown = _cooldowns.TryGetValue(cooldownKey, out var lastSent) &&
DateTime.UtcNow - lastSent < TimeSpan.FromMinutes(prefs.EmailCooldownMinutes);

/* Log recovery if we had previous failures */
if (_consecutiveFailures > 0)
if (!withinCooldown)
{
Logger.Info($"Alert email delivery recovered after {_consecutiveFailures} failure(s)");
}
_consecutiveFailures = 0;
_lastFailureError = null;

Logger.Info($"Alert email sent for {metricName} on {serverName}");
}
catch (Exception ex)
{
sendError = ex.Message;
_consecutiveFailures++;
_lastFailureError = ex.Message;

/* Loud on first 3 failures, then periodic reminders */
if (_consecutiveFailures <= 3)
{
Logger.Error($"ALERT EMAIL FAILED ({_consecutiveFailures}x): {ex.GetType().Name}: {ex.Message}");
}
else if (_consecutiveFailures % 50 == 0)
{
Logger.Error($"ALERT EMAIL STILL FAILING: {_consecutiveFailures} consecutive failures. Last error: {ex.Message}");
notificationType = "email";
var subject = $"[SQL Monitor Alert] {metricName} on {serverName}";
var (htmlBody, plainTextBody) = EmailTemplateBuilder.BuildAlertEmail(
metricName, serverName, currentValue, thresholdValue, prefs.EmailCooldownMinutes, context);

try
{
await SendEmailAsync(prefs, subject, htmlBody, plainTextBody, context);
sent = true;
_cooldowns[cooldownKey] = DateTime.UtcNow;

if (_consecutiveFailures > 0)
{
Logger.Info($"Alert email delivery recovered after {_consecutiveFailures} failure(s)");
}
_consecutiveFailures = 0;
_lastFailureError = null;

Logger.Info($"Alert email sent for {metricName} on {serverName}");
}
catch (Exception ex)
{
sendError = ex.Message;
_consecutiveFailures++;
_lastFailureError = ex.Message;

if (_consecutiveFailures <= 3)
{
Logger.Error($"ALERT EMAIL FAILED ({_consecutiveFailures}x): {ex.GetType().Name}: {ex.Message}");
}
else if (_consecutiveFailures % 50 == 0)
{
Logger.Error($"ALERT EMAIL STILL FAILING: {_consecutiveFailures} consecutive failures. Last error: {ex.Message}");
}
}
}
}

/* Log the alert attempt */
RecordAlert(serverId, serverName, metricName, currentValue, thresholdValue, sent, "email", sendError);
RecordAlert(serverId, serverName, metricName, currentValue, thresholdValue, sent, notificationType, sendError);

/* Send webhook notifications (Teams / Slack) — independent of email */
var webhookService = WebhookAlertService.Current;
if (webhookService != null)
{
await webhookService.TrySendWebhookAlertsAsync(
metricName, serverName, currentValue, thresholdValue, serverId, context);
}
}
catch (Exception ex)
{
Expand Down
Loading