diff --git a/Dashboard/Models/UserPreferences.cs b/Dashboard/Models/UserPreferences.cs index 9276408..bfea5df 100644 --- a/Dashboard/Models/UserPreferences.cs +++ b/Dashboard/Models/UserPreferences.cs @@ -144,6 +144,9 @@ private static string GetDefaultCsvSeparator() // Default mute rule expiration ("1 hour", "24 hours", "7 days", "Never") public string MuteRuleDefaultExpiration { get; set; } = "24 hours"; + // Log alert dismiss/mute actions to file + public bool LogAlertDismissals { get; set; } = true; + // Alert suppression (persisted) public List SilencedServers { get; set; } = new(); public List SilencedServerTabs { get; set; } = new(); diff --git a/Dashboard/Services/EmailAlertService.cs b/Dashboard/Services/EmailAlertService.cs index 2bb0ab3..5e6a108 100644 --- a/Dashboard/Services/EmailAlertService.cs +++ b/Dashboard/Services/EmailAlertService.cs @@ -203,15 +203,22 @@ public void HideAlerts(List<(DateTime AlertTime, string ServerName, string Metri if (keys.Count == 0) return; var keySet = new HashSet<(DateTime, string, string)>(keys); + int hidden = 0; lock (_alertLogLock) { foreach (var alert in _alertLog) { if (keySet.Contains((alert.AlertTime, alert.ServerName, alert.MetricName))) + { alert.Hidden = true; + hidden++; + } } } + + if (_preferencesService.GetPreferences().LogAlertDismissals) + Logger.Info($"[AlertDismiss] Dismissed {hidden} of {keys.Count} selected alert(s)"); } /// @@ -220,6 +227,7 @@ public void HideAlerts(List<(DateTime AlertTime, string ServerName, string Metri public void HideAllAlerts(int hoursBack, string? serverName = null) { var cutoff = DateTime.UtcNow.AddHours(-hoursBack); + int hidden = 0; lock (_alertLogLock) { @@ -230,9 +238,13 @@ public void HideAllAlerts(int hoursBack, string? serverName = null) (serverName == null || alert.ServerName == serverName)) { alert.Hidden = true; + hidden++; } } } + + if (_preferencesService.GetPreferences().LogAlertDismissals) + Logger.Info($"[AlertDismiss] Dismissed all: {hidden} alert(s) hidden (hoursBack={hoursBack}, server={serverName ?? "all"})"); } /// diff --git a/Dashboard/SettingsWindow.xaml b/Dashboard/SettingsWindow.xaml index e626d4e..ffe62d8 100644 --- a/Dashboard/SettingsWindow.xaml +++ b/Dashboard/SettingsWindow.xaml @@ -300,6 +300,8 @@ Padding="12,4" Margin="0,0,0,4"/> + diff --git a/Dashboard/SettingsWindow.xaml.cs b/Dashboard/SettingsWindow.xaml.cs index 0b4e962..0374340 100644 --- a/Dashboard/SettingsWindow.xaml.cs +++ b/Dashboard/SettingsWindow.xaml.cs @@ -186,6 +186,7 @@ private void LoadSettings() "7 days" => 2, _ => 3 }; + LogAlertDismissalsCheckBox.IsChecked = prefs.LogAlertDismissals; UpdateNotificationCheckboxStates(); @@ -670,6 +671,7 @@ private async void OkButton_Click(object sender, RoutedEventArgs e) prefs.MuteRuleDefaultExpiration = (MuteRuleDefaultExpirationCombo.SelectedItem as ComboBoxItem)?.Content?.ToString() ?? "24 hours"; MuteRuleDialog.DefaultExpiration = prefs.MuteRuleDefaultExpiration; + prefs.LogAlertDismissals = LogAlertDismissalsCheckBox.IsChecked == true; // Save SMTP email settings prefs.SmtpEnabled = SmtpEnabledCheckBox.IsChecked == true; diff --git a/Lite/App.xaml.cs b/Lite/App.xaml.cs index a9da026..5026b57 100644 --- a/Lite/App.xaml.cs +++ b/Lite/App.xaml.cs @@ -77,6 +77,7 @@ public partial class App : Application public static int AlertCooldownMinutes { get; set; } = 5; // Tray notification cooldown between repeated alerts public static int EmailCooldownMinutes { get; set; } = 15; // Email cooldown between repeated alerts public static string MuteRuleDefaultExpiration { get; set; } = "24 hours"; // Default expiration for new mute rules + public static bool LogAlertDismissals { get; set; } = true; // Log alert dismiss/mute actions to file /* Connection settings */ public static int ConnectionTimeoutSeconds { get; set; } = 5; @@ -284,6 +285,7 @@ public static void LoadAlertSettings() if (exp is "1 hour" or "24 hours" or "7 days" or "Never") MuteRuleDefaultExpiration = exp; } + if (root.TryGetProperty("log_alert_dismissals", out v)) LogAlertDismissals = v.GetBoolean(); /* Connection settings */ if (root.TryGetProperty("connection_timeout_seconds", out v)) diff --git a/Lite/Controls/AlertsHistoryTab.xaml.cs b/Lite/Controls/AlertsHistoryTab.xaml.cs index afd24e7..f8721c6 100644 --- a/Lite/Controls/AlertsHistoryTab.xaml.cs +++ b/Lite/Controls/AlertsHistoryTab.xaml.cs @@ -72,6 +72,7 @@ private async System.Threading.Tasks.Task LoadAlertsAsync() var displayCount = AlertsDataGrid.ItemsSource is ICollection coll ? coll.Count : alerts.Count; NoAlertsMessage.Visibility = displayCount == 0 ? Visibility.Visible : Visibility.Collapsed; AlertCountIndicator.Text = displayCount > 0 ? $"{displayCount} alert(s)" : ""; + AppLogger.Debug("AlertsHistory", $"Loaded {displayCount} alert(s) (query returned {alerts.Count}, hoursBack={hoursBack}, serverId={serverId?.ToString() ?? "all"})"); PopulateServerFilter(alerts); } @@ -232,7 +233,11 @@ private async void DismissSelected_Click(object sender, RoutedEventArgs e) try { - await _dataService.DismissAlertsAsync(selected); + var affected = await _dataService.DismissAlertsAsync(selected); + if (affected < selected.Count && App.LogAlertDismissals) + { + AppLogger.Warn("AlertsHistory", $"Dismiss selected: only {affected} of {selected.Count} alert(s) were updated — remaining alerts may have been archived to parquet"); + } await LoadAlertsAsync(); } catch (Exception ex) @@ -260,7 +265,11 @@ private async void DismissAll_Click(object sender, RoutedEventArgs e) { var hoursBack = GetSelectedHoursBack(); int? serverId = GetSelectedServerId(); - await _dataService.DismissAllVisibleAlertsAsync(hoursBack, serverId); + var affected = await _dataService.DismissAllVisibleAlertsAsync(hoursBack, serverId); + if (affected < displayCount && App.LogAlertDismissals) + { + AppLogger.Warn("AlertsHistory", $"Dismiss all: only {affected} of {displayCount} displayed alert(s) were updated — remaining alerts may have been archived to parquet"); + } await LoadAlertsAsync(); } catch (Exception ex) diff --git a/Lite/Services/LocalDataService.AlertHistory.cs b/Lite/Services/LocalDataService.AlertHistory.cs index 0d42cdc..9e29d21 100644 --- a/Lite/Services/LocalDataService.AlertHistory.cs +++ b/Lite/Services/LocalDataService.AlertHistory.cs @@ -101,11 +101,15 @@ ORDER BY alert_time DESC /// Dismisses specific alerts by marking them as dismissed in DuckDB. /// Identifies rows by (alert_time, server_id, metric_name) composite key. /// - public async Task DismissAlertsAsync(List alerts) + public async Task DismissAlertsAsync(List alerts) { - if (alerts.Count == 0) return; + if (alerts.Count == 0) return 0; + + if (App.LogAlertDismissals) + AppLogger.Info("AlertDismiss", $"Dismissing {alerts.Count} selected alert(s)"); using var connection = await OpenConnectionAsync(); + int totalAffected = 0; foreach (var alert in alerts) { @@ -115,19 +119,31 @@ UPDATE config_alert_log SET dismissed = TRUE WHERE alert_time = $1 AND server_id = $2 -AND metric_name = $3"; +AND metric_name = $3 +AND dismissed = FALSE"; command.Parameters.Add(new DuckDBParameter { Value = alert.AlertTime }); command.Parameters.Add(new DuckDBParameter { Value = alert.ServerId }); command.Parameters.Add(new DuckDBParameter { Value = alert.MetricName }); - await command.ExecuteNonQueryAsync(); + var affected = await command.ExecuteNonQueryAsync(); + totalAffected += affected; + + if (affected == 0 && App.LogAlertDismissals) + AppLogger.Warn("AlertDismiss", $"No rows updated for alert: time={alert.AlertTime:O}, server_id={alert.ServerId}, metric={alert.MetricName} — may be archived to parquet"); } + + if (App.LogAlertDismissals) + AppLogger.Info("AlertDismiss", $"Dismiss complete: {totalAffected} row(s) updated out of {alerts.Count} selected"); + return totalAffected; } /// /// Dismisses all visible (non-dismissed) alerts matching the current filter criteria. /// - public async Task DismissAllVisibleAlertsAsync(int hoursBack, int? serverId = null) + public async Task DismissAllVisibleAlertsAsync(int hoursBack, int? serverId = null) { + if (App.LogAlertDismissals) + AppLogger.Info("AlertDismiss", $"Dismissing all visible alerts: hoursBack={hoursBack}, serverId={serverId?.ToString() ?? "all"}"); + using var connection = await OpenConnectionAsync(); using var command = connection.CreateCommand(); @@ -154,7 +170,10 @@ UPDATE config_alert_log command.Parameters.Add(new DuckDBParameter { Value = cutoff }); } - await command.ExecuteNonQueryAsync(); + var affected = await command.ExecuteNonQueryAsync(); + if (App.LogAlertDismissals) + AppLogger.Info("AlertDismiss", $"Dismiss all complete: {affected} row(s) updated (cutoff={cutoff:O})"); + return affected; } } diff --git a/Lite/Windows/SettingsWindow.xaml b/Lite/Windows/SettingsWindow.xaml index b9beaee..18a5a9d 100644 --- a/Lite/Windows/SettingsWindow.xaml +++ b/Lite/Windows/SettingsWindow.xaml @@ -267,6 +267,8 @@ + diff --git a/Lite/Windows/SettingsWindow.xaml.cs b/Lite/Windows/SettingsWindow.xaml.cs index 64d5ef0..63f47ed 100644 --- a/Lite/Windows/SettingsWindow.xaml.cs +++ b/Lite/Windows/SettingsWindow.xaml.cs @@ -566,6 +566,7 @@ private void LoadAlertSettings() "7 days" => 2, _ => 3 }; + LogAlertDismissalsCheckBox.IsChecked = App.LogAlertDismissals; UpdateAlertControlStates(); } @@ -616,6 +617,7 @@ private bool SaveAlertSettings() else validationErrors.Add("Email alert cooldown must be between 1 and 120 minutes."); App.MuteRuleDefaultExpiration = (MuteRuleDefaultExpirationCombo.SelectedItem as ComboBoxItem)?.Content?.ToString() ?? "24 hours"; + App.LogAlertDismissals = LogAlertDismissalsCheckBox.IsChecked == true; var settingsPath = Path.Combine(App.ConfigDirectory, "settings.json"); try @@ -659,6 +661,7 @@ private bool SaveAlertSettings() root["alert_cooldown_minutes"] = App.AlertCooldownMinutes; root["email_cooldown_minutes"] = App.EmailCooldownMinutes; root["mute_rule_default_expiration"] = App.MuteRuleDefaultExpiration; + root["log_alert_dismissals"] = App.LogAlertDismissals; var options = new JsonSerializerOptions { WriteIndented = true }; File.WriteAllText(settingsPath, root.ToJsonString(options));