diff --git a/Lite.Tests/AlertHistorySourceTests.cs b/Lite.Tests/AlertHistorySourceTests.cs
new file mode 100644
index 0000000..4e57790
--- /dev/null
+++ b/Lite.Tests/AlertHistorySourceTests.cs
@@ -0,0 +1,265 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Threading.Tasks;
+using DuckDB.NET.Data;
+using PerformanceMonitorLite.Database;
+using PerformanceMonitorLite.Services;
+using PerformanceMonitorLite.Tests.Helpers;
+using Xunit;
+
+namespace PerformanceMonitorLite.Tests;
+
+///
+/// Tests that the archive view source column correctly distinguishes live vs archived alerts,
+/// and that dismiss operations only target live alerts.
+///
+public class AlertHistorySourceTests : IDisposable
+{
+ private readonly string _tempDir;
+ private readonly string _dbPath;
+ private readonly TestAlertDataHelper _helper;
+
+ public AlertHistorySourceTests()
+ {
+ _tempDir = Path.Combine(Path.GetTempPath(), "LiteTests_" + Guid.NewGuid().ToString("N")[..8]);
+ Directory.CreateDirectory(_tempDir);
+ _dbPath = Path.Combine(_tempDir, "test.duckdb");
+ _helper = new TestAlertDataHelper(_dbPath);
+ }
+
+ public void Dispose()
+ {
+ try
+ {
+ if (Directory.Exists(_tempDir))
+ Directory.Delete(_tempDir, recursive: true);
+ }
+ catch
+ {
+ /* Best-effort cleanup */
+ }
+ }
+
+ private async Task InitializeDatabaseAsync()
+ {
+ var initializer = new DuckDbInitializer(_dbPath);
+ await initializer.InitializeAsync();
+
+ var connection = new DuckDBConnection($"Data Source={_dbPath}");
+ await connection.OpenAsync(TestContext.Current.CancellationToken);
+ return connection;
+ }
+
+ [Fact]
+ public async Task LiveAlerts_HaveSourceLive()
+ {
+ using var connection = await InitializeDatabaseAsync();
+
+ var recentTime = DateTime.UtcNow.AddHours(-1);
+ await _helper.InsertLiveAlertAsync(connection, recentTime, 1, "Server1", "High CPU");
+
+ await _helper.RefreshArchiveViewsAsync();
+
+ using var cmd = connection.CreateCommand();
+ cmd.CommandText = "SELECT source FROM v_config_alert_log WHERE metric_name = 'High CPU'";
+ var source = (string)(await cmd.ExecuteScalarAsync(TestContext.Current.CancellationToken))!;
+
+ Assert.Equal("live", source);
+ }
+
+ [Fact]
+ public async Task ArchivedAlerts_HaveSourceArchive()
+ {
+ using var connection = await InitializeDatabaseAsync();
+
+ var oldAlerts = new List
+ {
+ TestAlertDataHelper.CreateAlert(
+ alertTime: DateTime.UtcNow.AddDays(-14),
+ metricName: "Blocking",
+ serverName: "Server1")
+ };
+
+ await _helper.CreateArchivedAlertsParquetAsync(connection, oldAlerts);
+ await _helper.RefreshArchiveViewsAsync();
+
+ using var cmd = connection.CreateCommand();
+ cmd.CommandText = "SELECT source FROM v_config_alert_log WHERE metric_name = 'Blocking'";
+ var source = (string)(await cmd.ExecuteScalarAsync(TestContext.Current.CancellationToken))!;
+
+ Assert.Equal("archive", source);
+ }
+
+ [Fact]
+ public async Task MixedAlerts_CorrectSourcePerRow()
+ {
+ using var connection = await InitializeDatabaseAsync();
+
+ // Insert a live alert (recent)
+ await _helper.InsertLiveAlertAsync(
+ connection,
+ DateTime.UtcNow.AddHours(-2),
+ 1, "Server1", "High CPU");
+
+ // Create an archived alert (old)
+ var archivedAlerts = new List
+ {
+ TestAlertDataHelper.CreateAlert(
+ alertTime: DateTime.UtcNow.AddDays(-14),
+ metricName: "Deadlock Detected",
+ serverName: "Server1")
+ };
+ await _helper.CreateArchivedAlertsParquetAsync(connection, archivedAlerts);
+ await _helper.RefreshArchiveViewsAsync();
+
+ // Query both and verify sources
+ using var cmd = connection.CreateCommand();
+ cmd.CommandText = @"
+SELECT metric_name, source
+FROM v_config_alert_log
+ORDER BY alert_time DESC";
+
+ var results = new List<(string MetricName, string Source)>();
+ using var reader = await cmd.ExecuteReaderAsync(TestContext.Current.CancellationToken);
+ while (await reader.ReadAsync(TestContext.Current.CancellationToken))
+ {
+ results.Add((reader.GetString(0), reader.GetString(1)));
+ }
+
+ Assert.Equal(2, results.Count);
+ Assert.Contains(results, r => r.MetricName == "High CPU" && r.Source == "live");
+ Assert.Contains(results, r => r.MetricName == "Deadlock Detected" && r.Source == "archive");
+ }
+
+ [Fact]
+ public async Task DismissUpdate_OnlyAffectsLiveTable()
+ {
+ using var connection = await InitializeDatabaseAsync();
+
+ // Insert a live alert
+ var liveTime = DateTime.UtcNow.AddHours(-1);
+ await _helper.InsertLiveAlertAsync(
+ connection, liveTime, 1, "Server1", "High CPU");
+
+ // Create an archived alert
+ var archiveTime = DateTime.UtcNow.AddDays(-14);
+ var archivedAlerts = new List
+ {
+ TestAlertDataHelper.CreateAlert(
+ alertTime: archiveTime,
+ metricName: "Blocking",
+ serverName: "Server1")
+ };
+ await _helper.CreateArchivedAlertsParquetAsync(connection, archivedAlerts);
+ await _helper.RefreshArchiveViewsAsync();
+
+ // Dismiss the live alert
+ using var dismissCmd = connection.CreateCommand();
+ dismissCmd.CommandText = @"
+UPDATE config_alert_log
+SET dismissed = TRUE
+WHERE metric_name = 'High CPU'
+AND dismissed = FALSE";
+ var affected = await dismissCmd.ExecuteNonQueryAsync(TestContext.Current.CancellationToken);
+ Assert.Equal(1, affected);
+
+ // Attempt to dismiss the archived alert — should affect 0 rows
+ using var dismissArchiveCmd = connection.CreateCommand();
+ dismissArchiveCmd.CommandText = @"
+UPDATE config_alert_log
+SET dismissed = TRUE
+WHERE metric_name = 'Blocking'
+AND dismissed = FALSE";
+ var archivedAffected = await dismissArchiveCmd.ExecuteNonQueryAsync(TestContext.Current.CancellationToken);
+ Assert.Equal(0, archivedAffected);
+
+ // The archived alert should still be visible in the view (undismissed)
+ using var checkCmd = connection.CreateCommand();
+ checkCmd.CommandText = @"
+SELECT COUNT(1)
+FROM v_config_alert_log
+WHERE metric_name = 'Blocking'
+AND dismissed = FALSE";
+ var stillVisible = Convert.ToInt64(await checkCmd.ExecuteScalarAsync(TestContext.Current.CancellationToken));
+ Assert.Equal(1, stillVisible);
+ }
+
+ [Fact]
+ public async Task ViewWithNoParquet_AllRowsAreLive()
+ {
+ using var connection = await InitializeDatabaseAsync();
+
+ // No parquet files — just live data
+ await _helper.InsertLiveAlertAsync(
+ connection, DateTime.UtcNow.AddHours(-1), 1, "Server1", "High CPU");
+ await _helper.InsertLiveAlertAsync(
+ connection, DateTime.UtcNow.AddHours(-2), 2, "Server2", "TempDB Space");
+
+ await _helper.RefreshArchiveViewsAsync();
+
+ using var cmd = connection.CreateCommand();
+ cmd.CommandText = "SELECT COUNT(1) FROM v_config_alert_log WHERE source = 'live'";
+ var liveCount = Convert.ToInt64(await cmd.ExecuteScalarAsync(TestContext.Current.CancellationToken));
+ Assert.Equal(2, liveCount);
+
+ using var archiveCmd = connection.CreateCommand();
+ archiveCmd.CommandText = "SELECT COUNT(1) FROM v_config_alert_log WHERE source = 'archive'";
+ var archiveCount = Convert.ToInt64(await archiveCmd.ExecuteScalarAsync(TestContext.Current.CancellationToken));
+ Assert.Equal(0, archiveCount);
+ }
+
+ [Fact]
+ public async Task AlertHistoryRow_IsArchived_ReflectsSource()
+ {
+ var liveRow = new AlertHistoryRow { Source = "live" };
+ var archiveRow = new AlertHistoryRow { Source = "archive" };
+ var defaultRow = new AlertHistoryRow();
+
+ Assert.False(liveRow.IsArchived);
+ Assert.True(archiveRow.IsArchived);
+ Assert.False(defaultRow.IsArchived);
+
+ await Task.CompletedTask;
+ }
+
+ [Fact]
+ public async Task MultipleParquetFiles_AllMarkedAsArchive()
+ {
+ using var connection = await InitializeDatabaseAsync();
+
+ // Create two separate parquet files (simulating multiple archive cycles)
+ var batch1 = new List
+ {
+ TestAlertDataHelper.CreateAlert(
+ alertTime: DateTime.UtcNow.AddDays(-14),
+ metricName: "High CPU",
+ serverName: "Server1"),
+ TestAlertDataHelper.CreateAlert(
+ alertTime: DateTime.UtcNow.AddDays(-13),
+ metricName: "Blocking",
+ serverName: "Server1")
+ };
+
+ var batch2 = new List
+ {
+ TestAlertDataHelper.CreateAlert(
+ alertTime: DateTime.UtcNow.AddDays(-21),
+ metricName: "Deadlock Detected",
+ serverName: "Server2")
+ };
+
+ await _helper.CreateArchivedAlertsParquetAsync(
+ connection, batch1, "20260301_0000_config_alert_log.parquet");
+ await _helper.CreateArchivedAlertsParquetAsync(
+ connection, batch2, "20260215_0000_config_alert_log.parquet");
+
+ await _helper.RefreshArchiveViewsAsync();
+
+ using var cmd = connection.CreateCommand();
+ cmd.CommandText = @"
+SELECT COUNT(1) FROM v_config_alert_log WHERE source = 'archive'";
+ var archiveCount = Convert.ToInt64(await cmd.ExecuteScalarAsync(TestContext.Current.CancellationToken));
+ Assert.Equal(3, archiveCount);
+ }
+}
diff --git a/Lite.Tests/Helpers/TestAlertDataHelper.cs b/Lite.Tests/Helpers/TestAlertDataHelper.cs
new file mode 100644
index 0000000..0a44ff5
--- /dev/null
+++ b/Lite.Tests/Helpers/TestAlertDataHelper.cs
@@ -0,0 +1,189 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Threading.Tasks;
+using DuckDB.NET.Data;
+using PerformanceMonitorLite.Database;
+
+namespace PerformanceMonitorLite.Tests.Helpers;
+
+///
+/// Helper for inserting test alert data into DuckDB live tables and archived parquet files.
+///
+public class TestAlertDataHelper
+{
+ private readonly string _dbPath;
+ private readonly string _archivePath;
+
+ public TestAlertDataHelper(string dbPath)
+ {
+ _dbPath = dbPath;
+ _archivePath = Path.Combine(Path.GetDirectoryName(dbPath) ?? ".", "archive");
+ Directory.CreateDirectory(_archivePath);
+ }
+
+ public string ArchivePath => _archivePath;
+
+ ///
+ /// Inserts a single alert row into the live config_alert_log table.
+ ///
+ public async Task InsertLiveAlertAsync(
+ DuckDBConnection connection,
+ DateTime alertTime,
+ int serverId,
+ string serverName,
+ string metricName,
+ double currentValue = 95.0,
+ double thresholdValue = 80.0,
+ bool alertSent = true,
+ string notificationType = "tray",
+ string? sendError = null,
+ bool dismissed = false,
+ bool muted = false,
+ string? detailText = null)
+ {
+ using var cmd = connection.CreateCommand();
+ cmd.CommandText = @"
+INSERT INTO config_alert_log
+ (alert_time, server_id, server_name, metric_name, current_value,
+ threshold_value, alert_sent, notification_type, send_error,
+ dismissed, muted, detail_text)
+VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)";
+ cmd.Parameters.Add(new DuckDBParameter { Value = alertTime });
+ cmd.Parameters.Add(new DuckDBParameter { Value = serverId });
+ cmd.Parameters.Add(new DuckDBParameter { Value = serverName });
+ cmd.Parameters.Add(new DuckDBParameter { Value = metricName });
+ cmd.Parameters.Add(new DuckDBParameter { Value = currentValue });
+ cmd.Parameters.Add(new DuckDBParameter { Value = thresholdValue });
+ cmd.Parameters.Add(new DuckDBParameter { Value = alertSent });
+ cmd.Parameters.Add(new DuckDBParameter { Value = notificationType });
+ cmd.Parameters.Add(new DuckDBParameter { Value = sendError ?? (object)DBNull.Value });
+ cmd.Parameters.Add(new DuckDBParameter { Value = dismissed });
+ cmd.Parameters.Add(new DuckDBParameter { Value = muted });
+ cmd.Parameters.Add(new DuckDBParameter { Value = detailText ?? (object)DBNull.Value });
+ await cmd.ExecuteNonQueryAsync();
+ }
+
+ ///
+ /// Creates a parquet file in the archive directory containing the specified alert rows.
+ /// Uses a staging table to avoid touching the live config_alert_log table.
+ /// The parquet file is named to match the ArchiveService naming convention.
+ ///
+ public async Task CreateArchivedAlertsParquetAsync(
+ DuckDBConnection connection,
+ List alerts,
+ string? parquetFileName = null)
+ {
+ parquetFileName ??= $"20260101_0000_config_alert_log.parquet";
+ var parquetPath = Path.Combine(_archivePath, parquetFileName).Replace("\\", "/");
+
+ using var createCmd = connection.CreateCommand();
+ createCmd.CommandText = @"
+CREATE TEMP TABLE _staging_alerts (
+ alert_time TIMESTAMP NOT NULL,
+ server_id INTEGER NOT NULL,
+ server_name VARCHAR NOT NULL,
+ metric_name VARCHAR NOT NULL,
+ current_value DOUBLE NOT NULL,
+ threshold_value DOUBLE NOT NULL,
+ alert_sent BOOLEAN NOT NULL DEFAULT false,
+ notification_type VARCHAR NOT NULL DEFAULT 'tray',
+ send_error VARCHAR,
+ dismissed BOOLEAN NOT NULL DEFAULT false,
+ muted BOOLEAN NOT NULL DEFAULT false,
+ detail_text VARCHAR
+)";
+ await createCmd.ExecuteNonQueryAsync();
+
+ foreach (var alert in alerts)
+ {
+ using var insertCmd = connection.CreateCommand();
+ insertCmd.CommandText = @"
+INSERT INTO _staging_alerts
+ (alert_time, server_id, server_name, metric_name, current_value,
+ threshold_value, alert_sent, notification_type, send_error,
+ dismissed, muted, detail_text)
+VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)";
+ insertCmd.Parameters.Add(new DuckDBParameter { Value = alert.AlertTime });
+ insertCmd.Parameters.Add(new DuckDBParameter { Value = alert.ServerId });
+ insertCmd.Parameters.Add(new DuckDBParameter { Value = alert.ServerName });
+ insertCmd.Parameters.Add(new DuckDBParameter { Value = alert.MetricName });
+ insertCmd.Parameters.Add(new DuckDBParameter { Value = alert.CurrentValue });
+ insertCmd.Parameters.Add(new DuckDBParameter { Value = alert.ThresholdValue });
+ insertCmd.Parameters.Add(new DuckDBParameter { Value = alert.AlertSent });
+ insertCmd.Parameters.Add(new DuckDBParameter { Value = alert.NotificationType });
+ insertCmd.Parameters.Add(new DuckDBParameter { Value = alert.SendError ?? (object)DBNull.Value });
+ insertCmd.Parameters.Add(new DuckDBParameter { Value = alert.Dismissed });
+ insertCmd.Parameters.Add(new DuckDBParameter { Value = alert.Muted });
+ insertCmd.Parameters.Add(new DuckDBParameter { Value = alert.DetailText ?? (object)DBNull.Value });
+ await insertCmd.ExecuteNonQueryAsync();
+ }
+
+ using var copyCmd = connection.CreateCommand();
+ copyCmd.CommandText = $"COPY _staging_alerts TO '{parquetPath}' (FORMAT PARQUET)";
+ await copyCmd.ExecuteNonQueryAsync();
+
+ using var dropCmd = connection.CreateCommand();
+ dropCmd.CommandText = "DROP TABLE _staging_alerts";
+ await dropCmd.ExecuteNonQueryAsync();
+ }
+
+ ///
+ /// Recreates archive views so they pick up newly created parquet files.
+ ///
+ public async Task RefreshArchiveViewsAsync()
+ {
+ var initializer = new DuckDbInitializer(_dbPath);
+ await initializer.CreateArchiveViewsAsync();
+ }
+
+ ///
+ /// Creates a test alert record with sensible defaults. Adjust properties as needed.
+ ///
+ public static TestAlertRecord CreateAlert(
+ DateTime? alertTime = null,
+ int serverId = 1,
+ string serverName = "TestServer",
+ string metricName = "High CPU",
+ double currentValue = 95.0,
+ double thresholdValue = 80.0,
+ bool dismissed = false,
+ bool muted = false,
+ string? detailText = null)
+ {
+ return new TestAlertRecord
+ {
+ AlertTime = alertTime ?? DateTime.UtcNow.AddDays(-10),
+ ServerId = serverId,
+ ServerName = serverName,
+ MetricName = metricName,
+ CurrentValue = currentValue,
+ ThresholdValue = thresholdValue,
+ AlertSent = true,
+ NotificationType = "tray",
+ SendError = null,
+ Dismissed = dismissed,
+ Muted = muted,
+ DetailText = detailText
+ };
+ }
+}
+
+///
+/// Plain record for constructing test alert data.
+///
+public class TestAlertRecord
+{
+ public DateTime AlertTime { get; set; }
+ public int ServerId { get; set; }
+ public string ServerName { get; set; } = "TestServer";
+ public string MetricName { get; set; } = "High CPU";
+ public double CurrentValue { get; set; } = 95.0;
+ public double ThresholdValue { get; set; } = 80.0;
+ public bool AlertSent { get; set; } = true;
+ public string NotificationType { get; set; } = "tray";
+ public string? SendError { get; set; }
+ public bool Dismissed { get; set; }
+ public bool Muted { get; set; }
+ public string? DetailText { get; set; }
+}
diff --git a/Lite/Controls/AlertsHistoryTab.xaml b/Lite/Controls/AlertsHistoryTab.xaml
index 1bdfda7..c27490f 100644
--- a/Lite/Controls/AlertsHistoryTab.xaml
+++ b/Lite/Controls/AlertsHistoryTab.xaml
@@ -53,6 +53,10 @@
+
+
+
+
diff --git a/Lite/Controls/AlertsHistoryTab.xaml.cs b/Lite/Controls/AlertsHistoryTab.xaml.cs
index f8721c6..2c55df3 100644
--- a/Lite/Controls/AlertsHistoryTab.xaml.cs
+++ b/Lite/Controls/AlertsHistoryTab.xaml.cs
@@ -218,7 +218,9 @@ private async void RefreshButton_Click(object sender, RoutedEventArgs e)
private void AlertsDataGrid_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
- DismissSelectedButton.IsEnabled = AlertsDataGrid.SelectedItems.Count > 0;
+ var selected = AlertsDataGrid.SelectedItems.OfType().ToList();
+ var liveCount = selected.Count(a => !a.IsArchived);
+ DismissSelectedButton.IsEnabled = liveCount > 0;
}
private async void DismissSelected_Click(object sender, RoutedEventArgs e)
@@ -231,12 +233,36 @@ private async void DismissSelected_Click(object sender, RoutedEventArgs e)
if (selected.Count == 0) return;
+ var liveAlerts = selected.Where(a => !a.IsArchived).ToList();
+ var archivedCount = selected.Count - liveAlerts.Count;
+
+ if (liveAlerts.Count == 0)
+ {
+ MessageBox.Show(
+ "The selected alert(s) have been archived and cannot be dismissed.",
+ "Cannot Dismiss",
+ MessageBoxButton.OK,
+ MessageBoxImage.Information);
+ return;
+ }
+
+ if (archivedCount > 0)
+ {
+ var confirmResult = MessageBox.Show(
+ $"{archivedCount} of {selected.Count} selected alert(s) have been archived and cannot be dismissed.\n\n" +
+ $"Dismiss the remaining {liveAlerts.Count} alert(s)?",
+ "Partial Dismiss",
+ MessageBoxButton.YesNo,
+ MessageBoxImage.Question);
+ if (confirmResult != MessageBoxResult.Yes) return;
+ }
+
try
{
- var affected = await _dataService.DismissAlertsAsync(selected);
- if (affected < selected.Count && App.LogAlertDismissals)
+ var affected = await _dataService.DismissAlertsAsync(liveAlerts);
+ if (affected < liveAlerts.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");
+ AppLogger.Warn("AlertsHistory", $"Dismiss selected: only {affected} of {liveAlerts.Count} live alert(s) were updated");
}
await LoadAlertsAsync();
}
@@ -250,11 +276,29 @@ private async void DismissAll_Click(object sender, RoutedEventArgs e)
{
if (_dataService == null) return;
- var displayCount = AlertsDataGrid.ItemsSource is System.Collections.ICollection coll ? coll.Count : 0;
- if (displayCount == 0) return;
+ var allAlerts = (AlertsDataGrid.ItemsSource as IEnumerable)?.ToList();
+ if (allAlerts == null || allAlerts.Count == 0) return;
+
+ var liveCount = allAlerts.Count(a => !a.IsArchived);
+ var archivedCount = allAlerts.Count - liveCount;
+
+ if (liveCount == 0)
+ {
+ MessageBox.Show(
+ "All visible alerts have been archived and cannot be dismissed.",
+ "Cannot Dismiss",
+ MessageBoxButton.OK,
+ MessageBoxImage.Information);
+ return;
+ }
+
+ var message = $"Dismiss {liveCount} alert(s)?";
+ if (archivedCount > 0)
+ message += $"\n\n{archivedCount} archived alert(s) will remain visible.";
+ message += "\n\nDismissed alerts are hidden from this view but remain in the database.";
var result = MessageBox.Show(
- $"Dismiss all {displayCount} visible alert(s)?\n\nDismissed alerts are hidden from this view but remain in the database.",
+ message,
"Dismiss All Alerts",
MessageBoxButton.YesNo,
MessageBoxImage.Question);
@@ -266,9 +310,9 @@ private async void DismissAll_Click(object sender, RoutedEventArgs e)
var hoursBack = GetSelectedHoursBack();
int? serverId = GetSelectedServerId();
var affected = await _dataService.DismissAllVisibleAlertsAsync(hoursBack, serverId);
- if (affected < displayCount && App.LogAlertDismissals)
+ if (affected < liveCount && App.LogAlertDismissals)
{
- AppLogger.Warn("AlertsHistory", $"Dismiss all: only {affected} of {displayCount} displayed alert(s) were updated — remaining alerts may have been archived to parquet");
+ AppLogger.Warn("AlertsHistory", $"Dismiss all: only {affected} of {liveCount} live alert(s) were updated");
}
await LoadAlertsAsync();
}
diff --git a/Lite/Database/DuckDbInitializer.cs b/Lite/Database/DuckDbInitializer.cs
index 1a2c987..5bbca8d 100644
--- a/Lite/Database/DuckDbInitializer.cs
+++ b/Lite/Database/DuckDbInitializer.cs
@@ -685,11 +685,17 @@ public async Task CreateArchiveViewsAsync()
if (hasParquetFiles)
{
var globPath = parquetGlob.Replace("\\", "/");
- viewSql = $"CREATE OR REPLACE VIEW v_{table} AS SELECT * FROM {table} UNION ALL BY NAME SELECT * FROM read_parquet('{globPath}', union_by_name=true)";
+ if (table == "config_alert_log")
+ viewSql = $"CREATE OR REPLACE VIEW v_{table} AS SELECT *, 'live' AS source FROM {table} UNION ALL BY NAME SELECT *, 'archive' AS source FROM read_parquet('{globPath}', union_by_name=true)";
+ else
+ viewSql = $"CREATE OR REPLACE VIEW v_{table} AS SELECT * FROM {table} UNION ALL BY NAME SELECT * FROM read_parquet('{globPath}', union_by_name=true)";
}
else
{
- viewSql = $"CREATE OR REPLACE VIEW v_{table} AS SELECT * FROM {table}";
+ if (table == "config_alert_log")
+ viewSql = $"CREATE OR REPLACE VIEW v_{table} AS SELECT *, 'live' AS source FROM {table}";
+ else
+ viewSql = $"CREATE OR REPLACE VIEW v_{table} AS SELECT * FROM {table}";
}
using var cmd = connection.CreateCommand();
@@ -703,7 +709,10 @@ public async Task CreateArchiveViewsAsync()
try
{
using var fallbackCmd = connection.CreateCommand();
- fallbackCmd.CommandText = $"CREATE OR REPLACE VIEW v_{table} AS SELECT * FROM {table}";
+ if (table == "config_alert_log")
+ fallbackCmd.CommandText = $"CREATE OR REPLACE VIEW v_{table} AS SELECT *, 'live' AS source FROM {table}";
+ else
+ fallbackCmd.CommandText = $"CREATE OR REPLACE VIEW v_{table} AS SELECT * FROM {table}";
await fallbackCmd.ExecuteNonQueryAsync();
}
catch (Exception fallbackEx)
diff --git a/Lite/Services/LocalDataService.AlertHistory.cs b/Lite/Services/LocalDataService.AlertHistory.cs
index 9e29d21..0e093bf 100644
--- a/Lite/Services/LocalDataService.AlertHistory.cs
+++ b/Lite/Services/LocalDataService.AlertHistory.cs
@@ -39,7 +39,8 @@ public async Task> GetAlertHistoryAsync(int hoursBack = 24
notification_type,
send_error,
muted,
- detail_text
+ detail_text,
+ source
FROM v_config_alert_log
WHERE alert_time >= $1
AND server_id = $2
@@ -64,7 +65,8 @@ ORDER BY alert_time DESC
notification_type,
send_error,
muted,
- detail_text
+ detail_text,
+ source
FROM v_config_alert_log
WHERE alert_time >= $1
AND dismissed = FALSE
@@ -90,7 +92,8 @@ ORDER BY alert_time DESC
NotificationType = reader.GetString(7),
SendError = reader.IsDBNull(8) ? null : reader.GetString(8),
Muted = !reader.IsDBNull(9) && reader.GetBoolean(9),
- DetailText = reader.IsDBNull(10) ? null : reader.GetString(10)
+ DetailText = reader.IsDBNull(10) ? null : reader.GetString(10),
+ Source = reader.IsDBNull(11) ? "live" : reader.GetString(11)
});
}
@@ -190,6 +193,9 @@ public class AlertHistoryRow
public string? SendError { get; set; }
public bool Muted { get; set; }
public string? DetailText { get; set; }
+ public string Source { get; set; } = "live";
+
+ public bool IsArchived => string.Equals(Source, "archive", StringComparison.OrdinalIgnoreCase);
public string TimeLocal => AlertTime.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss");
public string CurrentValueDisplay => FormatValue(MetricName, CurrentValue);