diff --git a/Lite/Controls/ServerTab.xaml b/Lite/Controls/ServerTab.xaml
index db802ed1..127df301 100644
--- a/Lite/Controls/ServerTab.xaml
+++ b/Lite/Controls/ServerTab.xaml
@@ -1130,6 +1130,63 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -1138,7 +1195,8 @@
+ HeadersVisibility="Column" GridLinesVisibility="Horizontal"
+ MouseDoubleClick="CollectionHealthGrid_MouseDoubleClick">
diff --git a/Lite/Controls/ServerTab.xaml.cs b/Lite/Controls/ServerTab.xaml.cs
index 4b4f669d..16ba442c 100644
--- a/Lite/Controls/ServerTab.xaml.cs
+++ b/Lite/Controls/ServerTab.xaml.cs
@@ -78,6 +78,7 @@ public partial class ServerTab : UserControl
private DataGridFilterManager? _traceFlagsFilterMgr;
private DataGridFilterManager? _collectionHealthFilterMgr;
private DataGridFilterManager? _collectionLogFilterMgr;
+ private DateTime? _dailySummaryDate; // null = today
private static readonly HashSet _defaultPerfmonCounters = new(StringComparer.OrdinalIgnoreCase)
{
@@ -488,6 +489,7 @@ private async System.Threading.Tasks.Task RefreshAllDataAsync()
var runningJobsTask = SafeQueryAsync(() => _dataService.GetRunningJobsAsync(_serverId));
var collectionHealthTask = SafeQueryAsync(() => _dataService.GetCollectionHealthAsync(_serverId));
var collectionLogTask = SafeQueryAsync(() => _dataService.GetRecentCollectionLogAsync(_serverId, hoursBack));
+ var dailySummaryTask = _dataService.GetDailySummaryAsync(_serverId, _dailySummaryDate);
/* Core data tasks */
await System.Threading.Tasks.Task.WhenAll(
snapshotsTask, cpuTask, memoryTask, memoryTrendTask,
@@ -495,7 +497,7 @@ await System.Threading.Tasks.Task.WhenAll(
deadlockTask, blockedProcessTask, waitTypesTask, memoryClerkTypesTask, perfmonCountersTask,
queryStoreTask, memoryGrantTrendTask,
serverConfigTask, databaseConfigTask, databaseScopedConfigTask, traceFlagsTask,
- runningJobsTask, collectionHealthTask, collectionLogTask);
+ runningJobsTask, collectionHealthTask, collectionLogTask, dailySummaryTask);
/* Trend chart tasks - run separately so failures don't kill the whole refresh */
var lockWaitTrendTask = SafeQueryAsync(() => _dataService.GetLockWaitTrendAsync(_serverId, hoursBack, fromDate, toDate));
@@ -538,6 +540,11 @@ await System.Threading.Tasks.Task.WhenAll(
_runningJobsFilterMgr!.UpdateData(runningJobsTask.Result);
_collectionHealthFilterMgr!.UpdateData(collectionHealthTask.Result);
_collectionLogFilterMgr!.UpdateData(collectionLogTask.Result);
+ var dailySummary = await dailySummaryTask;
+ DailySummaryGrid.ItemsSource = dailySummary != null
+ ? new List { dailySummary } : null;
+ DailySummaryNoData.Visibility = dailySummary == null
+ ? System.Windows.Visibility.Visible : System.Windows.Visibility.Collapsed;
UpdateCollectorDurationChart(collectionLogTask.Result);
/* Update memory summary */
@@ -2063,6 +2070,50 @@ private void QueryStoreGrid_MouseDoubleClick(object sender, System.Windows.Input
}
+ private void CollectionHealthGrid_MouseDoubleClick(object sender, System.Windows.Input.MouseButtonEventArgs e)
+ {
+ if (CollectionHealthGrid.SelectedItem is not CollectorHealthRow item) return;
+
+ var window = new Windows.CollectionLogWindow(_dataService, _serverId, item.CollectorName);
+ window.Owner = Window.GetWindow(this);
+ window.ShowDialog();
+ }
+
+ private void DailySummaryToday_Click(object sender, RoutedEventArgs e)
+ {
+ _dailySummaryDate = null;
+ DailySummaryDatePicker.SelectedDate = null;
+ DailySummaryTodayButton.FontWeight = FontWeights.Bold;
+ DailySummaryIndicator.Text = "Showing: Today (UTC)";
+ DailySummaryRefresh_Click(sender, e);
+ }
+
+ private void DailySummaryDate_Changed(object sender, SelectionChangedEventArgs e)
+ {
+ if (DailySummaryDatePicker.SelectedDate.HasValue)
+ {
+ _dailySummaryDate = DailySummaryDatePicker.SelectedDate.Value.Date;
+ DailySummaryTodayButton.FontWeight = FontWeights.Normal;
+ DailySummaryIndicator.Text = $"Showing: {_dailySummaryDate.Value:MMM d, yyyy}";
+ }
+ }
+
+ private async void DailySummaryRefresh_Click(object sender, RoutedEventArgs e)
+ {
+ try
+ {
+ var result = await _dataService.GetDailySummaryAsync(_serverId, _dailySummaryDate);
+ DailySummaryGrid.ItemsSource = result != null
+ ? new List { result } : null;
+ DailySummaryNoData.Visibility = result == null
+ ? Visibility.Visible : Visibility.Collapsed;
+ }
+ catch (Exception ex)
+ {
+ AppLogger.Error("DailySummary", $"Error refreshing: {ex.Message}");
+ }
+ }
+
private async void DownloadQueryStatsPlan_Click(object sender, RoutedEventArgs e)
{
if (sender is not Button btn || btn.DataContext is not QueryStatsRow row) return;
diff --git a/Lite/Services/LocalDataService.CollectionHealth.cs b/Lite/Services/LocalDataService.CollectionHealth.cs
index b93bfb76..19c47eb5 100644
--- a/Lite/Services/LocalDataService.CollectionHealth.cs
+++ b/Lite/Services/LocalDataService.CollectionHealth.cs
@@ -113,6 +113,55 @@ ORDER BY collection_time DESC
return items;
}
+
+ ///
+ /// Gets collection log entries for a specific collector on a server.
+ ///
+ public async Task> GetCollectionLogByCollectorAsync(int serverId, string collectorName, int hoursBack = 168)
+ {
+ using var connection = await OpenConnectionAsync();
+ using var command = connection.CreateCommand();
+ command.CommandText = @"
+SELECT
+ collector_name,
+ collection_time,
+ duration_ms,
+ sql_duration_ms,
+ duckdb_duration_ms,
+ rows_collected,
+ status,
+ error_message,
+ server_name
+FROM v_collection_log
+WHERE server_id = $1
+AND collector_name = $2
+AND collection_time >= $3
+ORDER BY collection_time DESC";
+
+ command.Parameters.Add(new DuckDBParameter { Value = serverId });
+ command.Parameters.Add(new DuckDBParameter { Value = collectorName });
+ command.Parameters.Add(new DuckDBParameter { Value = DateTime.UtcNow.AddHours(-hoursBack) });
+
+ var items = new List();
+ using var reader = await command.ExecuteReaderAsync();
+ while (await reader.ReadAsync())
+ {
+ items.Add(new CollectionLogRow
+ {
+ CollectorName = reader.GetString(0),
+ CollectionTime = reader.GetDateTime(1),
+ DurationMs = reader.IsDBNull(2) ? null : (int?)Convert.ToInt32(reader.GetValue(2)),
+ SqlDurationMs = reader.IsDBNull(3) ? null : (int?)Convert.ToInt32(reader.GetValue(3)),
+ DuckDbDurationMs = reader.IsDBNull(4) ? null : (int?)Convert.ToInt32(reader.GetValue(4)),
+ RowsCollected = reader.IsDBNull(5) ? null : (int?)Convert.ToInt32(reader.GetValue(5)),
+ Status = reader.GetString(6),
+ ErrorMessage = reader.IsDBNull(7) ? null : reader.GetString(7),
+ ServerName = reader.IsDBNull(8) ? null : reader.GetString(8)
+ });
+ }
+
+ return items;
+ }
}
public class CollectionLogRow
diff --git a/Lite/Services/LocalDataService.DailySummary.cs b/Lite/Services/LocalDataService.DailySummary.cs
new file mode 100644
index 00000000..fdd2dd0e
--- /dev/null
+++ b/Lite/Services/LocalDataService.DailySummary.cs
@@ -0,0 +1,127 @@
+/*
+ * Copyright (c) 2026 Erik Darling, Darling Data LLC
+ *
+ * This file is part of the SQL Server Performance Monitor Lite.
+ *
+ * Licensed under the MIT License. See LICENSE file in the project root for full license information.
+ */
+
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using DuckDB.NET.Data;
+
+namespace PerformanceMonitorLite.Services;
+
+public partial class LocalDataService
+{
+ ///
+ /// Gets daily summary for a specific date (or today if null).
+ ///
+ public async Task GetDailySummaryAsync(int serverId, DateTime? summaryDate = null)
+ {
+ using var connection = await OpenConnectionAsync();
+ using var command = connection.CreateCommand();
+
+ var targetDate = summaryDate?.Date ?? DateTime.UtcNow.Date;
+ var dayStart = targetDate;
+ var dayEnd = targetDate.AddDays(1);
+
+ command.CommandText = @"
+SELECT
+ COALESCE(
+ (SELECT SUM(delta_wait_time_ms) / 1000.0
+ FROM v_wait_stats
+ WHERE server_id = $1
+ AND collection_time >= $2 AND collection_time < $3
+ AND delta_wait_time_ms > 0), 0
+ ) AS total_wait_sec,
+ (SELECT wait_type
+ FROM v_wait_stats
+ WHERE server_id = $1
+ AND collection_time >= $2 AND collection_time < $3
+ AND delta_wait_time_ms > 0
+ ORDER BY delta_wait_time_ms DESC
+ LIMIT 1
+ ) AS top_wait_type,
+ COALESCE(
+ (SELECT COUNT(DISTINCT query_hash)
+ FROM v_query_stats
+ WHERE server_id = $1
+ AND collection_time >= $2 AND collection_time < $3), 0
+ ) AS unique_queries,
+ COALESCE(
+ (SELECT COUNT(*)
+ FROM v_deadlocks
+ WHERE server_id = $1
+ AND collection_time >= $2 AND collection_time < $3), 0
+ ) AS deadlock_count,
+ COALESCE(
+ (SELECT COUNT(*)
+ FROM v_blocked_process_reports
+ WHERE server_id = $1
+ AND collection_time >= $2 AND collection_time < $3), 0
+ ) AS blocking_events,
+ COALESCE(
+ (SELECT COUNT(*)
+ FROM v_cpu_utilization_stats
+ WHERE server_id = $1
+ AND sqlserver_cpu_utilization >= 80
+ AND collection_time >= $2 AND collection_time < $3), 0
+ ) AS high_cpu_events,
+ COALESCE(
+ (SELECT COUNT(*)
+ FROM v_collection_log
+ WHERE server_id = $1
+ AND status = 'ERROR'
+ AND collection_time >= $2 AND collection_time < $3), 0
+ ) AS collection_errors";
+
+ command.Parameters.Add(new DuckDBParameter { Value = serverId });
+ command.Parameters.Add(new DuckDBParameter { Value = dayStart });
+ command.Parameters.Add(new DuckDBParameter { Value = dayEnd });
+
+ using var reader = await command.ExecuteReaderAsync();
+ if (!await reader.ReadAsync()) return null;
+
+ var deadlocks = reader.IsDBNull(3) ? 0L : Convert.ToInt64(reader.GetValue(3));
+ var blocking = reader.IsDBNull(4) ? 0L : Convert.ToInt64(reader.GetValue(4));
+ var highCpu = reader.IsDBNull(5) ? 0L : Convert.ToInt64(reader.GetValue(5));
+
+ var health = "NORMAL";
+ if (deadlocks > 0) health = "DEADLOCKS";
+ else if (highCpu > 5) health = "CPU_CRITICAL";
+ else if (blocking > 10) health = "BLOCKING";
+
+ return new DailySummaryRow
+ {
+ SummaryDate = targetDate,
+ TotalWaitTimeSec = reader.IsDBNull(0) ? 0m : Convert.ToDecimal(reader.GetValue(0)),
+ TopWaitType = reader.IsDBNull(1) ? "" : reader.GetString(1),
+ UniqueQueries = reader.IsDBNull(2) ? 0L : Convert.ToInt64(reader.GetValue(2)),
+ DeadlockCount = deadlocks,
+ BlockingEvents = blocking,
+ HighCpuEvents = highCpu,
+ CollectionErrors = reader.IsDBNull(6) ? 0L : Convert.ToInt64(reader.GetValue(6)),
+ OverallHealth = health
+ };
+ }
+}
+
+public class DailySummaryRow
+{
+ public DateTime SummaryDate { get; set; }
+ public decimal TotalWaitTimeSec { get; set; }
+ public string TopWaitType { get; set; } = "";
+ public long UniqueQueries { get; set; }
+ public long DeadlockCount { get; set; }
+ public long BlockingEvents { get; set; }
+ public long HighCpuEvents { get; set; }
+ public long CollectionErrors { get; set; }
+ public string OverallHealth { get; set; } = "";
+
+ public string SummaryDateFormatted => SummaryDate.ToString("yyyy-MM-dd");
+ public string TotalWaitFormatted => TotalWaitTimeSec < 1000
+ ? $"{TotalWaitTimeSec:N1} s"
+ : $"{TotalWaitTimeSec / 60:N1} min";
+}
diff --git a/Lite/Windows/CollectionLogWindow.xaml b/Lite/Windows/CollectionLogWindow.xaml
new file mode 100644
index 00000000..549221a2
--- /dev/null
+++ b/Lite/Windows/CollectionLogWindow.xaml
@@ -0,0 +1,81 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Lite/Windows/CollectionLogWindow.xaml.cs b/Lite/Windows/CollectionLogWindow.xaml.cs
new file mode 100644
index 00000000..ddd9590d
--- /dev/null
+++ b/Lite/Windows/CollectionLogWindow.xaml.cs
@@ -0,0 +1,80 @@
+/*
+ * Copyright (c) 2026 Erik Darling, Darling Data LLC
+ *
+ * This file is part of the SQL Server Performance Monitor Lite.
+ *
+ * Licensed under the MIT License. See LICENSE file in the project root for full license information.
+ */
+
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+using System.Windows;
+using PerformanceMonitorLite.Services;
+
+namespace PerformanceMonitorLite.Windows
+{
+ public partial class CollectionLogWindow : Window
+ {
+ private readonly string _collectorName;
+ private readonly LocalDataService _dataService;
+ private readonly int _serverId;
+
+ public CollectionLogWindow(LocalDataService dataService, int serverId, string collectorName)
+ {
+ InitializeComponent();
+
+ _dataService = dataService;
+ _serverId = serverId;
+ _collectorName = collectorName;
+
+ CollectorNameText.Text = $"Collection History: {collectorName}";
+
+ Loaded += CollectionLogWindow_Loaded;
+ }
+
+ private async void CollectionLogWindow_Loaded(object sender, RoutedEventArgs e)
+ {
+ await LoadCollectionLogAsync();
+ }
+
+ private async Task LoadCollectionLogAsync()
+ {
+ try
+ {
+ var logs = await _dataService.GetCollectionLogByCollectorAsync(_serverId, _collectorName);
+ LogDataGrid.ItemsSource = logs;
+
+ if (logs.Count > 0)
+ {
+ var successCount = logs.Count(l => l.Status == "SUCCESS");
+ var errorCount = logs.Count(l => l.Status == "ERROR");
+ var avgDuration = logs.Where(l => l.Status == "SUCCESS" && l.DurationMs.HasValue)
+ .Select(l => (double)l.DurationMs!.Value)
+ .DefaultIfEmpty(0)
+ .Average();
+
+ SummaryText.Text = $"Total Runs: {logs.Count} | Success: {successCount} | Errors: {errorCount} | Avg Duration: {avgDuration:F0} ms";
+ }
+ else
+ {
+ SummaryText.Text = "No collection history found for this collector.";
+ }
+ }
+ catch (Exception ex)
+ {
+ MessageBox.Show(
+ $"Failed to load collection history:\n\n{ex.Message}",
+ "Error",
+ MessageBoxButton.OK,
+ MessageBoxImage.Error
+ );
+ }
+ }
+
+ private void Close_Click(object sender, RoutedEventArgs e)
+ {
+ Close();
+ }
+ }
+}