From f24cec73e4b2d9a187c38ce749a4aaf16ff1c83b Mon Sep 17 00:00:00 2001 From: Erik Darling <2136037+erikdarlingdata@users.noreply.github.com> Date: Thu, 12 Feb 2026 08:05:15 -0500 Subject: [PATCH 01/29] Add check for updates feature (#1) Both Dashboard and Lite now check the GitHub releases API for new versions. On startup (once per 24h, configurable), a notification appears if an update is available. The About dialog's "Check for Updates" link now performs a live check and shows the result inline with a clickable download link. Also simplified the Dashboard About dialog credits section to match Lite. Co-Authored-By: Claude Opus 4.6 --- Dashboard/AboutWindow.xaml | 7 +- Dashboard/AboutWindow.xaml.cs | 35 +++++++- Dashboard/MainWindow.xaml.cs | 24 ++++++ Dashboard/Models/UserPreferences.cs | 3 + Dashboard/Services/UpdateCheckService.cs | 101 ++++++++++++++++++++++ Lite/App.xaml.cs | 6 ++ Lite/MainWindow.xaml.cs | 33 ++++++++ Lite/Services/UpdateCheckService.cs | 102 +++++++++++++++++++++++ Lite/Windows/AboutWindow.xaml | 1 + Lite/Windows/AboutWindow.xaml.cs | 35 +++++++- 10 files changed, 339 insertions(+), 8 deletions(-) create mode 100644 Dashboard/Services/UpdateCheckService.cs create mode 100644 Lite/Services/UpdateCheckService.cs diff --git a/Dashboard/AboutWindow.xaml b/Dashboard/AboutWindow.xaml index 432be912..b8760fa4 100644 --- a/Dashboard/AboutWindow.xaml +++ b/Dashboard/AboutWindow.xaml @@ -43,13 +43,12 @@ Check for Updates + - + - - - + www.erikdarling.com diff --git a/Dashboard/AboutWindow.xaml.cs b/Dashboard/AboutWindow.xaml.cs index e661dfd5..0e61086e 100644 --- a/Dashboard/AboutWindow.xaml.cs +++ b/Dashboard/AboutWindow.xaml.cs @@ -7,6 +7,7 @@ using System.Diagnostics; using System.Reflection; using System.Windows; +using PerformanceMonitorDashboard.Services; namespace PerformanceMonitorDashboard { @@ -17,6 +18,8 @@ public partial class AboutWindow : Window private const string ReleasesUrl = "https://github.com/erikdarlingdata/PerformanceMonitor/releases"; private const string DarlingDataUrl = "https://www.erikdarling.com"; + private string? _updateReleaseUrl; + public AboutWindow() { InitializeComponent(); @@ -39,9 +42,37 @@ private void ReportIssueLink_Click(object sender, RoutedEventArgs e) OpenUrl(IssuesUrl); } - private void CheckUpdatesLink_Click(object sender, RoutedEventArgs e) + private async void CheckUpdatesLink_Click(object sender, RoutedEventArgs e) + { + UpdateStatusText.Text = "Checking for updates..."; + UpdateStatusText.Visibility = Visibility.Visible; + + var result = await UpdateCheckService.CheckForUpdateAsync(bypassCache: true); + + if (result == null) + { + UpdateStatusText.Text = "Unable to check for updates. Please try again later."; + } + else if (result.IsUpdateAvailable) + { + _updateReleaseUrl = result.ReleaseUrl; + UpdateStatusText.Text = $"Update available: {result.LatestVersion} (you have {result.CurrentVersion})"; + UpdateStatusText.Cursor = System.Windows.Input.Cursors.Hand; + UpdateStatusText.MouseLeftButtonUp += UpdateStatusText_Click; + UpdateStatusText.TextDecorations = System.Windows.TextDecorations.Underline; + UpdateStatusText.Foreground = FindResource("AccentBrush") as System.Windows.Media.Brush + ?? System.Windows.Media.Brushes.DodgerBlue; + } + else + { + UpdateStatusText.Text = $"You're up to date ({result.CurrentVersion})"; + } + } + + private void UpdateStatusText_Click(object sender, System.Windows.Input.MouseButtonEventArgs e) { - OpenUrl(ReleasesUrl); + if (!string.IsNullOrEmpty(_updateReleaseUrl)) + OpenUrl(_updateReleaseUrl); } private void DarlingDataLink_Click(object sender, RoutedEventArgs e) diff --git a/Dashboard/MainWindow.xaml.cs b/Dashboard/MainWindow.xaml.cs index 0a031764..f665f82c 100644 --- a/Dashboard/MainWindow.xaml.cs +++ b/Dashboard/MainWindow.xaml.cs @@ -137,6 +137,30 @@ private async void MainWindow_Loaded(object sender, RoutedEventArgs e) _displayRefreshTimer.Start(); await CheckAllConnectionsAsync(); + + _ = CheckForUpdatesOnStartupAsync(); + } + + private async Task CheckForUpdatesOnStartupAsync() + { + try + { + var prefs = _preferencesService.GetPreferences(); + if (!prefs.CheckForUpdatesOnStartup) return; + + var result = await UpdateCheckService.CheckForUpdateAsync(); + if (result?.IsUpdateAvailable == true) + { + _notificationService?.ShowNotification( + "Update Available", + $"Performance Monitor {result.LatestVersion} is available (you have {result.CurrentVersion}). Check About for details.", + NotificationType.Info); + } + } + catch + { + // Never crash on update check failure + } } private void StartMcpServerIfEnabled() diff --git a/Dashboard/Models/UserPreferences.cs b/Dashboard/Models/UserPreferences.cs index a915900e..58f8723d 100644 --- a/Dashboard/Models/UserPreferences.cs +++ b/Dashboard/Models/UserPreferences.cs @@ -92,6 +92,9 @@ public class UserPreferences public bool McpEnabled { get; set; } = false; public int McpPort { get; set; } = 5150; + // Update check settings + public bool CheckForUpdatesOnStartup { get; set; } = true; + // Alert suppression (persisted) public List SilencedServers { get; set; } = new(); public List SilencedServerTabs { get; set; } = new(); diff --git a/Dashboard/Services/UpdateCheckService.cs b/Dashboard/Services/UpdateCheckService.cs new file mode 100644 index 00000000..13c5edc2 --- /dev/null +++ b/Dashboard/Services/UpdateCheckService.cs @@ -0,0 +1,101 @@ +/* + * Performance Monitor Dashboard + * Copyright (c) 2026 Darling Data, LLC + * Licensed under the MIT License - see LICENSE file for details + */ + +using System; +using System.Net.Http; +using System.Reflection; +using System.Text.Json; +using System.Threading.Tasks; + +namespace PerformanceMonitorDashboard.Services +{ + public record UpdateInfo( + bool IsUpdateAvailable, + string CurrentVersion, + string LatestVersion, + string ReleaseUrl, + string ReleaseNotes); + + public static class UpdateCheckService + { + private const string ReleasesApiUrl = + "https://api.github.com/repos/erikdarlingdata/PerformanceMonitor/releases/latest"; + + private static readonly HttpClient HttpClient = CreateHttpClient(); + private static UpdateInfo? _cachedResult; + private static DateTime _cacheExpiry = DateTime.MinValue; + + private static HttpClient CreateHttpClient() + { + var client = new HttpClient { Timeout = TimeSpan.FromSeconds(10) }; + client.DefaultRequestHeaders.Add("User-Agent", "PerformanceMonitor"); + client.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json"); + return client; + } + + public static async Task CheckForUpdateAsync(bool bypassCache = false) + { + try + { + if (!bypassCache && _cachedResult != null && DateTime.UtcNow < _cacheExpiry) + return _cachedResult; + + var response = await HttpClient.GetAsync(ReleasesApiUrl).ConfigureAwait(false); + if (!response.IsSuccessStatusCode) + return null; + + var json = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + using var doc = JsonDocument.Parse(json); + var root = doc.RootElement; + + var tagName = root.GetProperty("tag_name").GetString() ?? ""; + var releaseUrl = root.GetProperty("html_url").GetString() ?? ""; + var releaseNotes = root.TryGetProperty("body", out var bodyProp) + ? bodyProp.GetString() ?? "" + : ""; + + var currentVersion = GetCurrentVersion(); + var latestVersion = ParseVersion(tagName); + var isUpdateAvailable = latestVersion != null + && currentVersion != null + && latestVersion > currentVersion; + + var result = new UpdateInfo( + isUpdateAvailable, + FormatVersion(currentVersion), + tagName, + releaseUrl, + releaseNotes); + + _cachedResult = result; + _cacheExpiry = DateTime.UtcNow.AddHours(24); + + return result; + } + catch + { + return null; + } + } + + private static Version? GetCurrentVersion() + { + return Assembly.GetExecutingAssembly().GetName().Version; + } + + private static Version? ParseVersion(string tagName) + { + var versionString = tagName.TrimStart('v', 'V'); + return Version.TryParse(versionString, out var version) ? version : null; + } + + private static string FormatVersion(Version? version) + { + if (version == null) return "unknown"; + return $"{version.Major}.{version.Minor}.{version.Build}"; + } + } +} diff --git a/Lite/App.xaml.cs b/Lite/App.xaml.cs index 21dc418c..cd9dba13 100644 --- a/Lite/App.xaml.cs +++ b/Lite/App.xaml.cs @@ -56,6 +56,9 @@ public partial class App : Application public static bool AlertDeadlockEnabled { get; set; } = true; public static int AlertDeadlockThreshold { get; set; } = 1; + /* Update check settings */ + public static bool CheckForUpdatesOnStartup { get; set; } = true; + /* SMTP email alert settings */ public static bool SmtpEnabled { get; set; } = false; public static string SmtpServer { get; set; } = ""; @@ -195,6 +198,9 @@ public static void LoadAlertSettings() if (root.TryGetProperty("alert_deadlock_enabled", out v)) AlertDeadlockEnabled = v.GetBoolean(); if (root.TryGetProperty("alert_deadlock_threshold", out v)) AlertDeadlockThreshold = v.GetInt32(); + /* Update check settings */ + if (root.TryGetProperty("check_for_updates_on_startup", out v)) CheckForUpdatesOnStartup = v.GetBoolean(); + /* SMTP settings */ if (root.TryGetProperty("smtp_enabled", out v)) SmtpEnabled = v.GetBoolean(); if (root.TryGetProperty("smtp_server", out v)) SmtpServer = v.GetString() ?? ""; diff --git a/Lite/MainWindow.xaml.cs b/Lite/MainWindow.xaml.cs index cfae29a2..fb6589ef 100644 --- a/Lite/MainWindow.xaml.cs +++ b/Lite/MainWindow.xaml.cs @@ -121,6 +121,8 @@ private async void MainWindow_Loaded(object sender, RoutedEventArgs e) await RefreshOverviewAsync(); StatusText.Text = "Ready - Collection active"; + + _ = CheckForUpdatesOnStartupAsync(); } catch (Exception ex) { @@ -133,6 +135,37 @@ private async void MainWindow_Loaded(object sender, RoutedEventArgs e) } } + private async Task CheckForUpdatesOnStartupAsync() + { + try + { + if (!App.CheckForUpdatesOnStartup) return; + + var result = await UpdateCheckService.CheckForUpdateAsync(); + if (result?.IsUpdateAvailable == true) + { + var answer = MessageBox.Show( + $"Performance Monitor {result.LatestVersion} is available (you have {result.CurrentVersion}).\n\nWould you like to open the download page?", + "Update Available", + MessageBoxButton.YesNo, + MessageBoxImage.Information); + + if (answer == MessageBoxResult.Yes) + { + System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo + { + FileName = result.ReleaseUrl, + UseShellExecute = true + }); + } + } + } + catch + { + // Never crash on update check failure + } + } + private async void MainWindow_Closing(object? sender, System.ComponentModel.CancelEventArgs e) { // Dispose system tray diff --git a/Lite/Services/UpdateCheckService.cs b/Lite/Services/UpdateCheckService.cs new file mode 100644 index 00000000..fb598b4e --- /dev/null +++ b/Lite/Services/UpdateCheckService.cs @@ -0,0 +1,102 @@ +/* + * 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.Net.Http; +using System.Reflection; +using System.Text.Json; +using System.Threading.Tasks; + +namespace PerformanceMonitorLite.Services; + +public record UpdateInfo( + bool IsUpdateAvailable, + string CurrentVersion, + string LatestVersion, + string ReleaseUrl, + string ReleaseNotes); + +public static class UpdateCheckService +{ + private const string ReleasesApiUrl = + "https://api.github.com/repos/erikdarlingdata/PerformanceMonitor/releases/latest"; + + private static readonly HttpClient HttpClient = CreateHttpClient(); + private static UpdateInfo? _cachedResult; + private static DateTime _cacheExpiry = DateTime.MinValue; + + private static HttpClient CreateHttpClient() + { + var client = new HttpClient { Timeout = TimeSpan.FromSeconds(10) }; + client.DefaultRequestHeaders.Add("User-Agent", "PerformanceMonitor"); + client.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json"); + return client; + } + + public static async Task CheckForUpdateAsync(bool bypassCache = false) + { + try + { + if (!bypassCache && _cachedResult != null && DateTime.UtcNow < _cacheExpiry) + return _cachedResult; + + var response = await HttpClient.GetAsync(ReleasesApiUrl).ConfigureAwait(false); + if (!response.IsSuccessStatusCode) + return null; + + var json = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + using var doc = JsonDocument.Parse(json); + var root = doc.RootElement; + + var tagName = root.GetProperty("tag_name").GetString() ?? ""; + var releaseUrl = root.GetProperty("html_url").GetString() ?? ""; + var releaseNotes = root.TryGetProperty("body", out var bodyProp) + ? bodyProp.GetString() ?? "" + : ""; + + var currentVersion = GetCurrentVersion(); + var latestVersion = ParseVersion(tagName); + var isUpdateAvailable = latestVersion != null + && currentVersion != null + && latestVersion > currentVersion; + + var result = new UpdateInfo( + isUpdateAvailable, + FormatVersion(currentVersion), + tagName, + releaseUrl, + releaseNotes); + + _cachedResult = result; + _cacheExpiry = DateTime.UtcNow.AddHours(24); + + return result; + } + catch + { + return null; + } + } + + private static Version? GetCurrentVersion() + { + return Assembly.GetExecutingAssembly().GetName().Version; + } + + private static Version? ParseVersion(string tagName) + { + var versionString = tagName.TrimStart('v', 'V'); + return Version.TryParse(versionString, out var version) ? version : null; + } + + private static string FormatVersion(Version? version) + { + if (version == null) return "unknown"; + return $"{version.Major}.{version.Minor}.{version.Build}"; + } +} diff --git a/Lite/Windows/AboutWindow.xaml b/Lite/Windows/AboutWindow.xaml index 404739d1..4b55197f 100644 --- a/Lite/Windows/AboutWindow.xaml +++ b/Lite/Windows/AboutWindow.xaml @@ -51,6 +51,7 @@ Check for Updates + www.erikdarling.com diff --git a/Lite/Windows/AboutWindow.xaml.cs b/Lite/Windows/AboutWindow.xaml.cs index 496b5a79..05a388a4 100644 --- a/Lite/Windows/AboutWindow.xaml.cs +++ b/Lite/Windows/AboutWindow.xaml.cs @@ -9,6 +9,7 @@ using System.Diagnostics; using System.Reflection; using System.Windows; +using PerformanceMonitorLite.Services; namespace PerformanceMonitorLite.Windows; @@ -19,6 +20,8 @@ public partial class AboutWindow : Window private const string ReleasesUrl = "https://github.com/erikdarlingdata/PerformanceMonitor/releases"; private const string DarlingDataUrl = "https://www.erikdarling.com"; + private string? _updateReleaseUrl; + public AboutWindow() { InitializeComponent(); @@ -37,9 +40,37 @@ private void ReportIssueLink_Click(object sender, RoutedEventArgs e) OpenUrl(IssuesUrl); } - private void CheckUpdatesLink_Click(object sender, RoutedEventArgs e) + private async void CheckUpdatesLink_Click(object sender, RoutedEventArgs e) + { + UpdateStatusText.Text = "Checking for updates..."; + UpdateStatusText.Visibility = Visibility.Visible; + + var result = await UpdateCheckService.CheckForUpdateAsync(bypassCache: true); + + if (result == null) + { + UpdateStatusText.Text = "Unable to check for updates. Please try again later."; + } + else if (result.IsUpdateAvailable) + { + _updateReleaseUrl = result.ReleaseUrl; + UpdateStatusText.Text = $"Update available: {result.LatestVersion} (you have {result.CurrentVersion})"; + UpdateStatusText.Cursor = System.Windows.Input.Cursors.Hand; + UpdateStatusText.MouseLeftButtonUp += UpdateStatusText_Click; + UpdateStatusText.TextDecorations = System.Windows.TextDecorations.Underline; + UpdateStatusText.Foreground = FindResource("AccentBrush") as System.Windows.Media.Brush + ?? System.Windows.Media.Brushes.DodgerBlue; + } + else + { + UpdateStatusText.Text = $"You're up to date ({result.CurrentVersion})"; + } + } + + private void UpdateStatusText_Click(object sender, System.Windows.Input.MouseButtonEventArgs e) { - OpenUrl(ReleasesUrl); + if (!string.IsNullOrEmpty(_updateReleaseUrl)) + OpenUrl(_updateReleaseUrl); } private void DarlingDataLink_Click(object sender, RoutedEventArgs e) From 78ff66fe79f117badf0405cd21d4754db39e8025 Mon Sep 17 00:00:00 2001 From: Erik Darling <2136037+erikdarlingdata@users.noreply.github.com> Date: Thu, 12 Feb 2026 10:21:41 -0500 Subject: [PATCH 02/29] Lite overview: show Online/Offline status for servers (#2) Overview cards now display connection status with a colored dot (green/red), status label, and a semi-transparent offline overlay with red X for unreachable servers. Offline servers also get a red card border. Connection status is sourced from the existing ServerManager connection tracking. Closes #2 Co-Authored-By: Claude Opus 4.6 --- Lite/MainWindow.xaml | 75 ++++++++++++++-------- Lite/MainWindow.xaml.cs | 2 + Lite/Services/LocalDataService.Overview.cs | 13 +++- 3 files changed, 61 insertions(+), 29 deletions(-) diff --git a/Lite/MainWindow.xaml b/Lite/MainWindow.xaml index adea8549..aa6996b0 100644 --- a/Lite/MainWindow.xaml +++ b/Lite/MainWindow.xaml @@ -12,6 +12,7 @@ + @@ -190,37 +191,55 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Lite/MainWindow.xaml.cs b/Lite/MainWindow.xaml.cs index fb6589ef..fb231638 100644 --- a/Lite/MainWindow.xaml.cs +++ b/Lite/MainWindow.xaml.cs @@ -340,6 +340,8 @@ private async Task RefreshOverviewAsync() if (summary != null) { summary.ServerName = server.ServerName; + var connStatus = _serverManager.GetConnectionStatus(server.Id); + summary.IsOnline = connStatus.IsOnline; summaries.Add(summary); } } diff --git a/Lite/Services/LocalDataService.Overview.cs b/Lite/Services/LocalDataService.Overview.cs index 8258323a..2e45a864 100644 --- a/Lite/Services/LocalDataService.Overview.cs +++ b/Lite/Services/LocalDataService.Overview.cs @@ -125,6 +125,7 @@ public class ServerSummaryItem public string DisplayName { get; set; } = ""; public string ServerName { get; set; } = ""; public int ServerId { get; set; } + public bool? IsOnline { get; set; } public double? CpuPercent { get; set; } public double? MemoryMb { get; set; } public int BlockingCount { get; set; } @@ -137,11 +138,21 @@ public class ServerSummaryItem public string DeadlockDisplay => DeadlockCount > 0 ? DeadlockCount.ToString() : "0"; public string LastCollectionDisplay => LastCollectionTime.HasValue ? ServerTimeHelper.FormatServerTime(LastCollectionTime, "HH:mm:ss") : "Never"; + /* Connection status */ + public string StatusDisplay => IsOnline switch { true => "Online", false => "Offline", _ => "Unknown" }; + public SolidColorBrush StatusBrush => MakeBrush(IsOnline switch { true => "#4CAF50", false => "#EF4444", _ => "#888888" }); + public bool IsOffline => IsOnline == false; + /* Color coding */ public SolidColorBrush CpuBrush => MakeBrush(CpuPercent >= 80 ? "#F44336" : CpuPercent >= 50 ? "#FF9800" : "#4CAF50"); public SolidColorBrush BlockingBrush => MakeBrush(BlockingCount > 0 ? "#FF9800" : "#4CAF50"); public SolidColorBrush DeadlockBrush => MakeBrush(DeadlockCount > 0 ? "#F44336" : "#4CAF50"); - public SolidColorBrush CardBorderBrush => MakeBrush(DeadlockCount > 0 ? "#F44336" : BlockingCount > 0 ? "#FF9800" : CpuPercent >= 80 ? "#FF9800" : "#555555"); + public SolidColorBrush CardBorderBrush => MakeBrush( + IsOnline == false ? "#EF4444" : + DeadlockCount > 0 ? "#F44336" : + BlockingCount > 0 ? "#FF9800" : + CpuPercent >= 80 ? "#FF9800" : + "#555555"); private static SolidColorBrush MakeBrush(string hex) { From a5cab0464401dc3688192e5e1ec426d3c5244a04 Mon Sep 17 00:00:00 2001 From: Erik Darling <2136037+erikdarlingdata@users.noreply.github.com> Date: Thu, 12 Feb 2026 11:05:46 -0500 Subject: [PATCH 03/29] Lite: show per-server collector health in status bar (#5) Status bar now filters collector health by the selected server tab instead of showing global aggregate. Overview tab still shows all-server health. Co-Authored-By: Claude Opus 4.6 --- Lite/Controls/ServerTab.xaml.cs | 1 + Lite/MainWindow.xaml.cs | 10 +++++++++- Lite/Services/RemoteCollectorService.cs | 26 +++++++++++++++---------- 3 files changed, 26 insertions(+), 11 deletions(-) diff --git a/Lite/Controls/ServerTab.xaml.cs b/Lite/Controls/ServerTab.xaml.cs index b09a3661..c7001e88 100644 --- a/Lite/Controls/ServerTab.xaml.cs +++ b/Lite/Controls/ServerTab.xaml.cs @@ -30,6 +30,7 @@ public partial class ServerTab : UserControl private readonly ServerConnection _server; private readonly LocalDataService _dataService; private readonly int _serverId; + public int ServerId => _serverId; private readonly CredentialService _credentialService; private readonly DispatcherTimer _refreshTimer; private readonly Dictionary _legendPanels = new(); diff --git a/Lite/MainWindow.xaml.cs b/Lite/MainWindow.xaml.cs index fb231638..69e63df0 100644 --- a/Lite/MainWindow.xaml.cs +++ b/Lite/MainWindow.xaml.cs @@ -219,6 +219,8 @@ private void ServerTabControl_SelectionChanged(object sender, SelectionChangedEv { ServerTimeHelper.UtcOffsetMinutes = serverTab.UtcOffsetMinutes; } + + UpdateCollectorHealth(); } private void RefreshServerList() @@ -290,7 +292,13 @@ private void UpdateCollectorHealth() return; } - var health = _collectorService.GetHealthSummary(); + int? selectedServerId = null; + if (ServerTabControl.SelectedItem is TabItem { Content: ServerTab serverTab }) + { + selectedServerId = serverTab.ServerId; + } + + var health = _collectorService.GetHealthSummary(selectedServerId); if (health.TotalCollectors == 0) { diff --git a/Lite/Services/RemoteCollectorService.cs b/Lite/Services/RemoteCollectorService.cs index 6dfb5353..0973c20d 100644 --- a/Lite/Services/RemoteCollectorService.cs +++ b/Lite/Services/RemoteCollectorService.cs @@ -30,6 +30,7 @@ namespace PerformanceMonitorLite.Services; /// public class CollectorHealthEntry { + public int ServerId { get; set; } public string CollectorName { get; set; } = ""; public DateTime? LastSuccessTime { get; set; } public DateTime? LastErrorTime { get; set; } @@ -82,9 +83,9 @@ public partial class RemoteCollectorService private long _lastDuckDbMs; /// - /// Tracks health state per collector (keyed by collector name). + /// Tracks health state per collector per server. /// - private readonly Dictionary _collectorHealth = new(StringComparer.OrdinalIgnoreCase); + private readonly Dictionary<(int ServerId, string CollectorName), CollectorHealthEntry> _collectorHealth = new(); private readonly object _healthLock = new(); /// @@ -113,20 +114,24 @@ public RemoteCollectorService( public Task SeedDeltaCacheAsync() => _deltaCalculator.SeedFromDatabaseAsync(_duckDb); /// - /// Gets a summary of collector health across all tracked collectors. + /// Gets a summary of collector health. When serverId is provided, filters to that server only. /// - public CollectorHealthSummary GetHealthSummary() + public CollectorHealthSummary GetHealthSummary(int? serverId = null) { lock (_healthLock) { var summary = new CollectorHealthSummary { - TotalCollectors = _collectorHealth.Count, LoggingFailures = _logInsertFailures }; foreach (var entry in _collectorHealth.Values) { + if (serverId.HasValue && entry.ServerId != serverId.Value) + continue; + + summary.TotalCollectors++; + if (entry.ConsecutiveErrors > 0) { summary.ErroringCollectors++; @@ -141,14 +146,15 @@ public CollectorHealthSummary GetHealthSummary() /// /// Records a collector execution result for health tracking. /// - private void RecordCollectorResult(string collectorName, bool success, string? errorMessage = null) + private void RecordCollectorResult(int serverId, string collectorName, bool success, string? errorMessage = null) { lock (_healthLock) { - if (!_collectorHealth.TryGetValue(collectorName, out var entry)) + var key = (serverId, collectorName); + if (!_collectorHealth.TryGetValue(key, out var entry)) { - entry = new CollectorHealthEntry { CollectorName = collectorName }; - _collectorHealth[collectorName] = entry; + entry = new CollectorHealthEntry { ServerId = serverId, CollectorName = collectorName }; + _collectorHealth[key] = entry; } if (success) @@ -330,7 +336,7 @@ public async Task RunCollectorAsync(ServerConnection server, string collectorNam } // Track collector health - RecordCollectorResult(collectorName, status == "SUCCESS", errorMessage); + RecordCollectorResult(GetServerId(server), collectorName, status == "SUCCESS", errorMessage); // Log the collection attempt await LogCollectionAsync(GetServerId(server), collectorName, startTime, status, errorMessage, rowsCollected, _lastSqlMs, _lastDuckDbMs); From 2e943a855a848db14e9ff6c95417afa6c3056b50 Mon Sep 17 00:00:00 2001 From: ClaudioESSilva Date: Thu, 12 Feb 2026 17:33:19 +0000 Subject: [PATCH 04/29] Add support for High DPI to the Dashboard --- Dashboard/App.xaml.cs | 60 ++++++++++++++++++++++++++++++++++++++ Dashboard/Dashboard.csproj | 1 + Dashboard/app.manifest | 49 +++++++++++++++++++++++++++++++ 3 files changed, 110 insertions(+) create mode 100644 Dashboard/app.manifest diff --git a/Dashboard/App.xaml.cs b/Dashboard/App.xaml.cs index 3e257435..4f21236f 100644 --- a/Dashboard/App.xaml.cs +++ b/Dashboard/App.xaml.cs @@ -8,6 +8,7 @@ using System; using System.IO; +using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; using System.Windows; @@ -21,8 +22,33 @@ public partial class App : Application private const string MutexName = "PerformanceMonitorDashboard_SingleInstance"; private Mutex? _singleInstanceMutex; + // DPI awareness for proper scaling on high DPI displays + private enum PROCESS_DPI_AWARENESS + { + Process_DPI_Unaware = 0, + Process_System_DPI_Aware = 1, + Process_Per_Monitor_DPI_Aware = 2 + } + + private enum DPI_AWARENESS_CONTEXT + { + Unaware = -1, + SystemAware = -2, + PerMonitorAware = -3, + PerMonitorAwareV2 = -4 + } + + [DllImport("SHCore.dll", SetLastError = true)] + private static extern bool SetProcessDpiAwareness(PROCESS_DPI_AWARENESS awareness); + + [DllImport("user32.dll", SetLastError = true)] + private static extern bool SetProcessDpiAwarenessContext(int dpiFlag); + protected override void OnStartup(StartupEventArgs e) { + // Enable per-monitor DPI awareness for proper scaling on high DPI displays + EnableDpiAwareness(); + // Check for existing instance _singleInstanceMutex = new Mutex(true, MutexName, out bool isNewInstance); @@ -65,6 +91,40 @@ protected override void OnExit(ExitEventArgs e) base.OnExit(e); } + private static void EnableDpiAwareness() + { + try + { + // Try PerMonitorV2 first (Windows 10 1703+) - best scaling quality + if (Environment.OSVersion.Version.Major >= 10) + { + try + { + SetProcessDpiAwarenessContext((int)DPI_AWARENESS_CONTEXT.PerMonitorAwareV2); + return; + } + catch + { + // Fall through to try other methods + } + } + + // Try PerMonitor awareness (Windows 8.1+) + try + { + SetProcessDpiAwareness(PROCESS_DPI_AWARENESS.Process_Per_Monitor_DPI_Aware); + } + catch + { + // If all else fails, WPF will use system DPI awareness + } + } + catch + { + // Silently fail - WPF will handle DPI at a basic level + } + } + private void OnUnhandledException(object sender, UnhandledExceptionEventArgs e) { var exception = e.ExceptionObject as Exception; diff --git a/Dashboard/Dashboard.csproj b/Dashboard/Dashboard.csproj index e1ecd936..9c3d2a7f 100644 --- a/Dashboard/Dashboard.csproj +++ b/Dashboard/Dashboard.csproj @@ -10,6 +10,7 @@ Darling Data, LLC Copyright © 2026 Darling Data, LLC EDD.ico + app.manifest true latest-recommended diff --git a/Dashboard/app.manifest b/Dashboard/app.manifest new file mode 100644 index 00000000..c70b1558 --- /dev/null +++ b/Dashboard/app.manifest @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + true + PerMonitorV2 + true + + + From 1e96f31e7a530be4ab348ddcec561e48294f79ae Mon Sep 17 00:00:00 2001 From: ClaudioESSilva Date: Thu, 12 Feb 2026 17:33:32 +0000 Subject: [PATCH 05/29] Add support for High DPI to the Lite --- Lite/App.xaml.cs | 60 ++++++++++++++++++++++++++++++ Lite/PerformanceMonitorLite.csproj | 1 + Lite/app.manifest | 49 ++++++++++++++++++++++++ 3 files changed, 110 insertions(+) create mode 100644 Lite/app.manifest diff --git a/Lite/App.xaml.cs b/Lite/App.xaml.cs index 21dc418c..55388deb 100644 --- a/Lite/App.xaml.cs +++ b/Lite/App.xaml.cs @@ -8,6 +8,7 @@ using System; using System.IO; +using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; using System.Windows; @@ -21,6 +22,28 @@ public partial class App : Application private const string MutexName = "PerformanceMonitorLite_SingleInstance"; private Mutex? _singleInstanceMutex; + // DPI awareness for proper scaling on high DPI displays + private enum PROCESS_DPI_AWARENESS + { + Process_DPI_Unaware = 0, + Process_System_DPI_Aware = 1, + Process_Per_Monitor_DPI_Aware = 2 + } + + private enum DPI_AWARENESS_CONTEXT + { + Unaware = -1, + SystemAware = -2, + PerMonitorAware = -3, + PerMonitorAwareV2 = -4 + } + + [DllImport("SHCore.dll", SetLastError = true)] + private static extern bool SetProcessDpiAwareness(PROCESS_DPI_AWARENESS awareness); + + [DllImport("user32.dll", SetLastError = true)] + private static extern bool SetProcessDpiAwarenessContext(int dpiFlag); + /// /// Gets the application data directory where config and data files are stored. /// @@ -103,6 +126,9 @@ public static void SaveSmtpPassword(string password) protected override void OnStartup(StartupEventArgs e) { + // Enable per-monitor DPI awareness for proper scaling on high DPI displays + EnableDpiAwareness(); + // Check for existing instance _singleInstanceMutex = new Mutex(true, MutexName, out bool isNewInstance); @@ -160,6 +186,40 @@ protected override void OnExit(ExitEventArgs e) base.OnExit(e); } + private static void EnableDpiAwareness() + { + try + { + // Try PerMonitorV2 first (Windows 10 1703+) - best scaling quality + if (Environment.OSVersion.Version.Major >= 10) + { + try + { + SetProcessDpiAwarenessContext((int)DPI_AWARENESS_CONTEXT.PerMonitorAwareV2); + return; + } + catch + { + // Fall through to try other methods + } + } + + // Try PerMonitor awareness (Windows 8.1+) + try + { + SetProcessDpiAwareness(PROCESS_DPI_AWARENESS.Process_Per_Monitor_DPI_Aware); + } + catch + { + // If all else fails, WPF will use system DPI awareness + } + } + catch + { + // Silently fail - WPF will handle DPI at a basic level + } + } + private static void LoadDefaultTimeRange() { try diff --git a/Lite/PerformanceMonitorLite.csproj b/Lite/PerformanceMonitorLite.csproj index e91aa06f..a058adb3 100644 --- a/Lite/PerformanceMonitorLite.csproj +++ b/Lite/PerformanceMonitorLite.csproj @@ -12,6 +12,7 @@ Copyright © 2026 Darling Data, LLC Lightweight SQL Server performance monitoring - no installation required on target servers EDD.ico + app.manifest true latest-recommended diff --git a/Lite/app.manifest b/Lite/app.manifest new file mode 100644 index 00000000..a3dc0922 --- /dev/null +++ b/Lite/app.manifest @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + true + PerMonitorV2 + true + + + From cff77bfcb5ba1e63f68aa0358f869a103f4d66cf Mon Sep 17 00:00:00 2001 From: Erik Darling <2136037+erikdarlingdata@users.noreply.github.com> Date: Thu, 12 Feb 2026 19:52:00 -0500 Subject: [PATCH 06/29] Add workflow to enforce PRs to main must come from dev Automatically fails PRs targeting main from any branch other than dev, so contributors don't need manual correction. Co-Authored-By: Claude Opus 4.6 --- .github/workflows/check-pr-branch.yml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 .github/workflows/check-pr-branch.yml diff --git a/.github/workflows/check-pr-branch.yml b/.github/workflows/check-pr-branch.yml new file mode 100644 index 00000000..88fa6fae --- /dev/null +++ b/.github/workflows/check-pr-branch.yml @@ -0,0 +1,21 @@ +name: Check pull request target branch +on: + pull_request_target: + types: + - opened + - reopened + - synchronize + - edited +jobs: + check-branches: + runs-on: ubuntu-latest + steps: + - name: Check branches + env: + HEAD_REF: ${{ github.head_ref }} + BASE_REF: ${{ github.base_ref }} + run: | + if [ "$HEAD_REF" != "dev" ] && [ "$BASE_REF" == "main" ]; then + echo "::error::Pull requests to main are only allowed from dev. Please target the dev branch instead." + exit 1 + fi From 49f05cc50d772aad93e48391d3e2ef1377f66edd Mon Sep 17 00:00:00 2001 From: Erik Darling <2136037+erikdarlingdata@users.noreply.github.com> Date: Thu, 12 Feb 2026 20:24:04 -0500 Subject: [PATCH 07/29] Exclude WAITFOR queries from top queries views (#4) WAITFOR-based queries (e.g., Service Broker queue listeners) dominate duration rankings but aren't actionable. Filter them at the display layer in both Dashboard and Lite query stats + Query Store queries. Co-Authored-By: Claude Opus 4.6 --- Dashboard/Services/DatabaseService.QueryPerformance.cs | 2 ++ Lite/Services/LocalDataService.QueryStats.cs | 1 + Lite/Services/LocalDataService.QueryStore.cs | 1 + 3 files changed, 4 insertions(+) diff --git a/Dashboard/Services/DatabaseService.QueryPerformance.cs b/Dashboard/Services/DatabaseService.QueryPerformance.cs index c1b3f6e6..b13913d6 100644 --- a/Dashboard/Services/DatabaseService.QueryPerformance.cs +++ b/Dashboard/Services/DatabaseService.QueryPerformance.cs @@ -748,6 +748,7 @@ FROM report.query_stats_summary AS qs OR (qs.last_execution_time >= @fromDate AND qs.last_execution_time <= @toDate) OR (qs.first_execution_time <= @fromDate AND qs.last_execution_time >= @toDate))) ) + AND qs.query_text NOT LIKE N'WAITFOR%' ORDER BY qs.avg_worker_time_ms DESC;"; @@ -989,6 +990,7 @@ FROM report.query_store_summary AS qss OR (qss.last_execution_time >= @fromDate AND qss.last_execution_time <= @toDate) OR (qss.first_execution_time <= @fromDate AND qss.last_execution_time >= @toDate))) ) + AND qss.query_sql_text NOT LIKE N'WAITFOR%' ORDER BY qss.avg_cpu_time_ms DESC OPTION diff --git a/Lite/Services/LocalDataService.QueryStats.cs b/Lite/Services/LocalDataService.QueryStats.cs index dabb04e9..78b4ab51 100644 --- a/Lite/Services/LocalDataService.QueryStats.cs +++ b/Lite/Services/LocalDataService.QueryStats.cs @@ -69,6 +69,7 @@ FROM query_stats WHERE server_id = $1 AND collection_time >= $2 AND collection_time <= $3 +AND query_text NOT LIKE 'WAITFOR%' GROUP BY database_name, query_hash ORDER BY SUM(delta_elapsed_time) DESC LIMIT $4"; diff --git a/Lite/Services/LocalDataService.QueryStore.cs b/Lite/Services/LocalDataService.QueryStore.cs index 65a52f70..f02fe78f 100644 --- a/Lite/Services/LocalDataService.QueryStore.cs +++ b/Lite/Services/LocalDataService.QueryStore.cs @@ -47,6 +47,7 @@ FROM query_store_stats WHERE server_id = $1 AND collection_time >= $2 AND collection_time <= $3 +AND query_text NOT LIKE 'WAITFOR%' GROUP BY database_name, query_id, plan_id, query_hash ORDER BY SUM(execution_count) * AVG(avg_duration_ms) DESC LIMIT $4"; From 60b6b687513c2a9b2cccc0a51336b8b6651693ae Mon Sep 17 00:00:00 2001 From: Erik Darling <2136037+erikdarlingdata@users.noreply.github.com> Date: Thu, 12 Feb 2026 20:53:20 -0500 Subject: [PATCH 08/29] Dashboard: always show legends on Performance Trends charts (#11) Scatter series and legend are now created unconditionally so the legend displays the series name even when there's no data for the time range. Co-Authored-By: Claude Opus 4.6 --- .../Controls/QueryPerformanceContent.xaml.cs | 42 +++++++++---------- 1 file changed, 19 insertions(+), 23 deletions(-) diff --git a/Dashboard/Controls/QueryPerformanceContent.xaml.cs b/Dashboard/Controls/QueryPerformanceContent.xaml.cs index c4396979..e3dd1e8e 100644 --- a/Dashboard/Controls/QueryPerformanceContent.xaml.cs +++ b/Dashboard/Controls/QueryPerformanceContent.xaml.cs @@ -915,18 +915,13 @@ private void LoadDurationChart(WpfPlot chart, IEnumerable tre dataList.Select(d => d.CollectionTime), dataList.Select(d => d.AvgDurationMs)); - if (xs.Length > 0) - { - var scatter = chart.Plot.Add.Scatter(xs, ys); - scatter.LineWidth = 2; - scatter.MarkerSize = 5; - scatter.Color = color; - scatter.LegendText = legendText; - - _legendPanels[chart] = chart.Plot.ShowLegend(ScottPlot.Edge.Bottom); - chart.Plot.Legend.FontSize = 12; - } - else + var scatter = chart.Plot.Add.Scatter(xs, ys); + scatter.LineWidth = 2; + scatter.MarkerSize = 5; + scatter.Color = color; + scatter.LegendText = legendText; + + if (xs.Length == 0) { double xCenter = xMin + (xMax - xMin) / 2; var noDataText = chart.Plot.Add.Text("No data for selected time range", xCenter, 0.5); @@ -935,6 +930,9 @@ private void LoadDurationChart(WpfPlot chart, IEnumerable tre noDataText.LabelAlignment = ScottPlot.Alignment.MiddleCenter; } + _legendPanels[chart] = chart.Plot.ShowLegend(ScottPlot.Edge.Bottom); + chart.Plot.Legend.FontSize = 12; + chart.Plot.Axes.DateTimeTicksBottom(); chart.Plot.Axes.SetLimitsX(xMin, xMax); chart.Plot.YLabel("Duration (ms/sec)"); @@ -971,18 +969,13 @@ private void LoadExecChart(IEnumerable execTrends, int hours dataList.Select(d => d.CollectionTime), dataList.Select(d => (double)d.ExecutionsPerSecond)); - if (xs.Length > 0) - { - var scatter = QueryPerfTrendsExecChart.Plot.Add.Scatter(xs, ys); - scatter.LineWidth = 2; - scatter.MarkerSize = 5; - scatter.Color = ScottPlot.Colors.Blue; - scatter.LegendText = "Executions/sec"; + var scatter = QueryPerfTrendsExecChart.Plot.Add.Scatter(xs, ys); + scatter.LineWidth = 2; + scatter.MarkerSize = 5; + scatter.Color = ScottPlot.Colors.Blue; + scatter.LegendText = "Executions/sec"; - _legendPanels[QueryPerfTrendsExecChart] = QueryPerfTrendsExecChart.Plot.ShowLegend(ScottPlot.Edge.Bottom); - QueryPerfTrendsExecChart.Plot.Legend.FontSize = 12; - } - else + if (xs.Length == 0) { double xCenter = xMin + (xMax - xMin) / 2; var noDataText = QueryPerfTrendsExecChart.Plot.Add.Text("No data for selected time range", xCenter, 0.5); @@ -991,6 +984,9 @@ private void LoadExecChart(IEnumerable execTrends, int hours noDataText.LabelAlignment = ScottPlot.Alignment.MiddleCenter; } + _legendPanels[QueryPerfTrendsExecChart] = QueryPerfTrendsExecChart.Plot.ShowLegend(ScottPlot.Edge.Bottom); + QueryPerfTrendsExecChart.Plot.Legend.FontSize = 12; + QueryPerfTrendsExecChart.Plot.Axes.DateTimeTicksBottom(); QueryPerfTrendsExecChart.Plot.Axes.SetLimitsX(xMin, xMax); QueryPerfTrendsExecChart.Plot.YLabel("Executions/sec"); From 28db7cc60556d8d8d94dd322e5fc07f021fb8b1a Mon Sep 17 00:00:00 2001 From: ClaudioESSilva Date: Fri, 13 Feb 2026 09:10:30 +0000 Subject: [PATCH 09/29] Clean unneeded code --- Dashboard/App.xaml.cs | 59 ------------------------------------------- Lite/App.xaml.cs | 59 ------------------------------------------- 2 files changed, 118 deletions(-) diff --git a/Dashboard/App.xaml.cs b/Dashboard/App.xaml.cs index 4f21236f..1a17661f 100644 --- a/Dashboard/App.xaml.cs +++ b/Dashboard/App.xaml.cs @@ -8,7 +8,6 @@ using System; using System.IO; -using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; using System.Windows; @@ -22,33 +21,8 @@ public partial class App : Application private const string MutexName = "PerformanceMonitorDashboard_SingleInstance"; private Mutex? _singleInstanceMutex; - // DPI awareness for proper scaling on high DPI displays - private enum PROCESS_DPI_AWARENESS - { - Process_DPI_Unaware = 0, - Process_System_DPI_Aware = 1, - Process_Per_Monitor_DPI_Aware = 2 - } - - private enum DPI_AWARENESS_CONTEXT - { - Unaware = -1, - SystemAware = -2, - PerMonitorAware = -3, - PerMonitorAwareV2 = -4 - } - - [DllImport("SHCore.dll", SetLastError = true)] - private static extern bool SetProcessDpiAwareness(PROCESS_DPI_AWARENESS awareness); - - [DllImport("user32.dll", SetLastError = true)] - private static extern bool SetProcessDpiAwarenessContext(int dpiFlag); - protected override void OnStartup(StartupEventArgs e) { - // Enable per-monitor DPI awareness for proper scaling on high DPI displays - EnableDpiAwareness(); - // Check for existing instance _singleInstanceMutex = new Mutex(true, MutexName, out bool isNewInstance); @@ -91,39 +65,6 @@ protected override void OnExit(ExitEventArgs e) base.OnExit(e); } - private static void EnableDpiAwareness() - { - try - { - // Try PerMonitorV2 first (Windows 10 1703+) - best scaling quality - if (Environment.OSVersion.Version.Major >= 10) - { - try - { - SetProcessDpiAwarenessContext((int)DPI_AWARENESS_CONTEXT.PerMonitorAwareV2); - return; - } - catch - { - // Fall through to try other methods - } - } - - // Try PerMonitor awareness (Windows 8.1+) - try - { - SetProcessDpiAwareness(PROCESS_DPI_AWARENESS.Process_Per_Monitor_DPI_Aware); - } - catch - { - // If all else fails, WPF will use system DPI awareness - } - } - catch - { - // Silently fail - WPF will handle DPI at a basic level - } - } private void OnUnhandledException(object sender, UnhandledExceptionEventArgs e) { diff --git a/Lite/App.xaml.cs b/Lite/App.xaml.cs index 55388deb..7171a8bf 100644 --- a/Lite/App.xaml.cs +++ b/Lite/App.xaml.cs @@ -8,7 +8,6 @@ using System; using System.IO; -using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; using System.Windows; @@ -22,28 +21,6 @@ public partial class App : Application private const string MutexName = "PerformanceMonitorLite_SingleInstance"; private Mutex? _singleInstanceMutex; - // DPI awareness for proper scaling on high DPI displays - private enum PROCESS_DPI_AWARENESS - { - Process_DPI_Unaware = 0, - Process_System_DPI_Aware = 1, - Process_Per_Monitor_DPI_Aware = 2 - } - - private enum DPI_AWARENESS_CONTEXT - { - Unaware = -1, - SystemAware = -2, - PerMonitorAware = -3, - PerMonitorAwareV2 = -4 - } - - [DllImport("SHCore.dll", SetLastError = true)] - private static extern bool SetProcessDpiAwareness(PROCESS_DPI_AWARENESS awareness); - - [DllImport("user32.dll", SetLastError = true)] - private static extern bool SetProcessDpiAwarenessContext(int dpiFlag); - /// /// Gets the application data directory where config and data files are stored. /// @@ -126,8 +103,6 @@ public static void SaveSmtpPassword(string password) protected override void OnStartup(StartupEventArgs e) { - // Enable per-monitor DPI awareness for proper scaling on high DPI displays - EnableDpiAwareness(); // Check for existing instance _singleInstanceMutex = new Mutex(true, MutexName, out bool isNewInstance); @@ -186,40 +161,6 @@ protected override void OnExit(ExitEventArgs e) base.OnExit(e); } - private static void EnableDpiAwareness() - { - try - { - // Try PerMonitorV2 first (Windows 10 1703+) - best scaling quality - if (Environment.OSVersion.Version.Major >= 10) - { - try - { - SetProcessDpiAwarenessContext((int)DPI_AWARENESS_CONTEXT.PerMonitorAwareV2); - return; - } - catch - { - // Fall through to try other methods - } - } - - // Try PerMonitor awareness (Windows 8.1+) - try - { - SetProcessDpiAwareness(PROCESS_DPI_AWARENESS.Process_Per_Monitor_DPI_Aware); - } - catch - { - // If all else fails, WPF will use system DPI awareness - } - } - catch - { - // Silently fail - WPF will handle DPI at a basic level - } - } - private static void LoadDefaultTimeRange() { try From c939be2cbe76eadee4913cab303f1fb1b1423329 Mon Sep 17 00:00:00 2001 From: Erik Darling <2136037+erikdarlingdata@users.noreply.github.com> Date: Fri, 13 Feb 2026 06:19:30 -0500 Subject: [PATCH 10/29] Grafana-inspired dark theme + tray icon polish (#16) Apply consistent blue-gray tinted color palette across both Dashboard and Lite: - Backgrounds: #111217, #181b1f, #22252b, #2a2d35 (Grafana-style) - Text: #E4E6EB (primary), #9DA5B4 (dim), #6B7280 (muted) - Borders: #2a2d35 / #33363e - Chart backgrounds, grid lines, axis text, legends updated to match - Calendar pickers styled consistently Fix system tray visual issues in both apps: - Replace native ToolTipText (Windows light theme) with custom dark TrayToolTip - Add full ContextMenu ControlTemplate to eliminate WPF default light gutter - Add emoji icons to Lite tray context menu (matching Dashboard) - Transparent backgrounds on all tray menu emoji icons Also includes WAITFOR exclusion filters from #14 in query snapshots. Co-Authored-By: Claude Opus 4.6 --- Dashboard/Helpers/TabHelpers.cs | 26 +++---- Dashboard/ServerTab.xaml.cs | 4 +- .../DatabaseService.QueryPerformance.cs | 2 + Dashboard/Services/NotificationService.cs | 24 +++++-- Dashboard/Themes/DarkTheme.xaml | 69 +++++++++++-------- Lite/Controls/ServerTab.xaml.cs | 22 +++--- Lite/Services/LocalDataService.Blocking.cs | 1 + Lite/Services/LocalDataService.Overview.cs | 2 +- Lite/Services/SystemTrayService.cs | 31 +++++++-- Lite/Themes/DarkTheme.xaml | 69 +++++++++++-------- Lite/Windows/ProcedureHistoryWindow.xaml.cs | 8 +-- Lite/Windows/QueryStatsHistoryWindow.xaml.cs | 8 +-- Lite/Windows/QueryStoreHistoryWindow.xaml.cs | 8 +-- 13 files changed, 167 insertions(+), 107 deletions(-) diff --git a/Dashboard/Helpers/TabHelpers.cs b/Dashboard/Helpers/TabHelpers.cs index e73a438b..bf01f70b 100644 --- a/Dashboard/Helpers/TabHelpers.cs +++ b/Dashboard/Helpers/TabHelpers.cs @@ -33,19 +33,19 @@ public static class TabHelpers /// public static void ApplyDarkModeToChart(WpfPlot chart) { - // Dark theme colors matching Darling Data brand - var darkBackground = ScottPlot.Color.FromHex("#333333"); - var darkerBackground = ScottPlot.Color.FromHex("#252525"); - var textColor = ScottPlot.Color.FromHex("#E0E0E0"); - var gridColor = ScottPlot.Color.FromHex("#444444"); + // Grafana-inspired dark theme colors + var darkBackground = ScottPlot.Color.FromHex("#22252b"); + var darkerBackground = ScottPlot.Color.FromHex("#111217"); + var textColor = ScottPlot.Color.FromHex("#9DA5B4"); + var gridColor = ScottPlot.Colors.White.WithAlpha(20); chart.Plot.FigureBackground.Color = darkBackground; chart.Plot.DataBackground.Color = darkerBackground; chart.Plot.Axes.Color(textColor); chart.Plot.Grid.MajorLineColor = gridColor; chart.Plot.Legend.BackgroundColor = darkBackground; - chart.Plot.Legend.FontColor = textColor; - chart.Plot.Legend.OutlineColor = gridColor; + chart.Plot.Legend.FontColor = ScottPlot.Color.FromHex("#E4E6EB"); + chart.Plot.Legend.OutlineColor = ScottPlot.Color.FromHex("#2a2d35"); chart.Plot.Legend.Alignment = ScottPlot.Alignment.LowerCenter; chart.Plot.Legend.Orientation = ScottPlot.Orientation.Horizontal; chart.Plot.Axes.Margins(bottom: 0); // No bottom margin - SetChartYLimitsWithLegendPadding handles Y-axis @@ -63,7 +63,7 @@ public static void ApplyDarkModeToChart(WpfPlot chart) /// public static void ReapplyAxisColors(WpfPlot chart) { - var textColor = ScottPlot.Color.FromHex("#E0E0E0"); + var textColor = ScottPlot.Color.FromHex("#9DA5B4"); chart.Plot.Axes.Bottom.TickLabelStyle.ForeColor = textColor; chart.Plot.Axes.Left.TickLabelStyle.ForeColor = textColor; chart.Plot.Axes.Bottom.Label.ForeColor = textColor; @@ -124,15 +124,15 @@ public static void SetChartYLimitsWithLegendPadding(WpfPlot chart, double dataYM /// public static void ApplyDarkThemeToCalendar(System.Windows.Controls.Calendar calendar) { - var darkBg = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#252525")); - var lightBg = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#404040")); - var whiteFg = new SolidColorBrush(Colors.White); - var mutedFg = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#858585")); + var darkBg = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#111217")); + var lightBg = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#22252b")); + var whiteFg = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#E4E6EB")); + var mutedFg = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#6B7280")); var accentBg = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#2eaef1")); calendar.Background = darkBg; calendar.Foreground = whiteFg; - calendar.BorderBrush = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#555555")); + calendar.BorderBrush = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#2a2d35")); // Apply to all child controls recursively ApplyDarkThemeRecursively(calendar, darkBg, lightBg, whiteFg, mutedFg); diff --git a/Dashboard/ServerTab.xaml.cs b/Dashboard/ServerTab.xaml.cs index 0a00c6b1..10ee9201 100644 --- a/Dashboard/ServerTab.xaml.cs +++ b/Dashboard/ServerTab.xaml.cs @@ -246,7 +246,7 @@ private void SetupAutoRefresh() { AutoRefreshToggle.IsChecked = false; AutoRefreshToggle.Content = "Auto-Refresh: Off"; - AutoRefreshToggle.Background = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#555555")); // Gray when inactive + AutoRefreshToggle.Background = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#2a2d35")); // Gray when inactive } } @@ -358,7 +358,7 @@ private void AutoRefreshToggle_Click(object sender, RoutedEventArgs e) _autoRefreshTimer?.Stop(); _autoRefreshTimer = null; AutoRefreshToggle.Content = "Auto-Refresh: Off"; - AutoRefreshToggle.Background = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#555555")); // Gray when inactive + AutoRefreshToggle.Background = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#2a2d35")); // Gray when inactive } } diff --git a/Dashboard/Services/DatabaseService.QueryPerformance.cs b/Dashboard/Services/DatabaseService.QueryPerformance.cs index b13913d6..f4dc4b72 100644 --- a/Dashboard/Services/DatabaseService.QueryPerformance.cs +++ b/Dashboard/Services/DatabaseService.QueryPerformance.cs @@ -568,6 +568,7 @@ SELECT TOP (500) FROM report.query_snapshots AS qs WHERE qs.collection_time >= @from_date AND qs.collection_time <= @to_date + AND qs.sql_text NOT LIKE N'WAITFOR%' ORDER BY qs.collection_time DESC, qs.session_id;"; @@ -609,6 +610,7 @@ SELECT TOP (500) /* query_plan fetched on-demand via GetQuerySnapshotPlanAsync */ FROM report.query_snapshots AS qs WHERE qs.collection_time >= DATEADD(HOUR, @hours_back, SYSDATETIME()) + AND qs.sql_text NOT LIKE N'WAITFOR%' ORDER BY qs.collection_time DESC, qs.session_id;"; diff --git a/Dashboard/Services/NotificationService.cs b/Dashboard/Services/NotificationService.cs index 5dcc55f9..e04199a2 100644 --- a/Dashboard/Services/NotificationService.cs +++ b/Dashboard/Services/NotificationService.cs @@ -9,6 +9,7 @@ using System; using System.Windows; using System.Windows.Controls; +using System.Windows.Media; using System.Windows.Media.Imaging; using Hardcodet.Wpf.TaskbarNotification; using PerformanceMonitorDashboard.Interfaces; @@ -33,9 +34,22 @@ public void Initialize() // Dispose any existing icon first _trayIcon?.Dispose(); - _trayIcon = new TaskbarIcon + _trayIcon = new TaskbarIcon(); + + /* Custom dark tooltip (native ToolTipText uses Windows light theme) */ + _trayIcon.TrayToolTip = new Border { - ToolTipText = "SQL Server Performance Monitor" + Background = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#22252b")), + BorderBrush = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#33363e")), + BorderThickness = new Thickness(1), + Padding = new Thickness(10, 8, 10, 8), + CornerRadius = new CornerRadius(4), + Child = new TextBlock + { + Text = "SQL Server Performance Monitor", + Foreground = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#E4E6EB")), + FontSize = 12 + } }; // Load dark theme for context menu styling @@ -62,14 +76,14 @@ public void Initialize() var showItem = new MenuItem { Header = "Show Dashboard", - Icon = new TextBlock { Text = "📊" } + Icon = new TextBlock { Text = "📊", Background = Brushes.Transparent } }; showItem.Click += (s, e) => ShowMainWindow(); var settingsItem = new MenuItem { Header = "Settings...", - Icon = new TextBlock { Text = "⚙" } + Icon = new TextBlock { Text = "⚙", Background = Brushes.Transparent } }; settingsItem.Click += (s, e) => OpenSettings(); @@ -78,7 +92,7 @@ public void Initialize() var exitItem = new MenuItem { Header = "Exit", - Icon = new TextBlock { Text = "✕" } + Icon = new TextBlock { Text = "✕", Background = Brushes.Transparent } }; exitItem.Click += (s, e) => ExitApplication(); diff --git a/Dashboard/Themes/DarkTheme.xaml b/Dashboard/Themes/DarkTheme.xaml index 60f791b8..3e873343 100644 --- a/Dashboard/Themes/DarkTheme.xaml +++ b/Dashboard/Themes/DarkTheme.xaml @@ -11,21 +11,21 @@ #5bc4f5 #1a9ae0 - - #252525 - #2d2d2d - #333333 - #404040 - #4a4a4a + + #111217 + #1c1f25 + #181b1f + #22252b + #2a2d35 - - #FFFFFF - #D0D0D0 - #B0B0B0 + + #E4E6EB + #9DA5B4 + #6B7280 - - #555555 - #666666 + + #2a2d35 + #33363e #4CAF50 @@ -83,7 +83,7 @@ @@ -514,7 +514,7 @@ - + @@ -587,7 +587,7 @@ - + @@ -999,9 +999,22 @@ diff --git a/Lite/Controls/ServerTab.xaml.cs b/Lite/Controls/ServerTab.xaml.cs index c7001e88..882d6aad 100644 --- a/Lite/Controls/ServerTab.xaml.cs +++ b/Lite/Controls/ServerTab.xaml.cs @@ -296,13 +296,13 @@ private void DatePicker_CalendarOpened(object sender, RoutedEventArgs e) private void ApplyDarkThemeToCalendar(System.Windows.Controls.Calendar calendar) { - var darkBg = new SolidColorBrush((System.Windows.Media.Color)ColorConverter.ConvertFromString("#252525")!); - var whiteFg = new SolidColorBrush(System.Windows.Media.Colors.White); - var mutedFg = new SolidColorBrush((System.Windows.Media.Color)ColorConverter.ConvertFromString("#858585")!); + var darkBg = new SolidColorBrush((System.Windows.Media.Color)ColorConverter.ConvertFromString("#111217")!); + var whiteFg = new SolidColorBrush((System.Windows.Media.Color)ColorConverter.ConvertFromString("#E4E6EB")!); + var mutedFg = new SolidColorBrush((System.Windows.Media.Color)ColorConverter.ConvertFromString("#6B7280")!); calendar.Background = darkBg; calendar.Foreground = whiteFg; - calendar.BorderBrush = new SolidColorBrush((System.Windows.Media.Color)ColorConverter.ConvertFromString("#555555")!); + calendar.BorderBrush = new SolidColorBrush((System.Windows.Media.Color)ColorConverter.ConvertFromString("#2a2d35")!); ApplyDarkThemeRecursively(calendar, darkBg, whiteFg, mutedFg); } @@ -1331,18 +1331,18 @@ private void ShowChartLegend(ScottPlot.WPF.WpfPlot chart) /// private static void ApplyDarkTheme(ScottPlot.WPF.WpfPlot chart) { - var darkBackground = ScottPlot.Color.FromHex("#333333"); - var darkerBackground = ScottPlot.Color.FromHex("#252525"); - var textColor = ScottPlot.Color.FromHex("#E0E0E0"); - var gridColor = ScottPlot.Color.FromHex("#444444"); + var darkBackground = ScottPlot.Color.FromHex("#22252b"); + var darkerBackground = ScottPlot.Color.FromHex("#111217"); + var textColor = ScottPlot.Color.FromHex("#9DA5B4"); + var gridColor = ScottPlot.Colors.White.WithAlpha(20); chart.Plot.FigureBackground.Color = darkBackground; chart.Plot.DataBackground.Color = darkerBackground; chart.Plot.Axes.Color(textColor); chart.Plot.Grid.MajorLineColor = gridColor; chart.Plot.Legend.BackgroundColor = darkBackground; - chart.Plot.Legend.FontColor = textColor; - chart.Plot.Legend.OutlineColor = gridColor; + chart.Plot.Legend.FontColor = ScottPlot.Color.FromHex("#E4E6EB"); + chart.Plot.Legend.OutlineColor = ScottPlot.Color.FromHex("#2a2d35"); chart.Plot.Legend.Alignment = ScottPlot.Alignment.LowerCenter; chart.Plot.Legend.Orientation = ScottPlot.Orientation.Horizontal; chart.Plot.Axes.Margins(bottom: 0); /* No bottom margin - SetChartYLimitsWithLegendPadding handles Y-axis */ @@ -1358,7 +1358,7 @@ private static void ApplyDarkTheme(ScottPlot.WPF.WpfPlot chart) /// private static void ReapplyAxisColors(ScottPlot.WPF.WpfPlot chart) { - var textColor = ScottPlot.Color.FromHex("#E0E0E0"); + var textColor = ScottPlot.Color.FromHex("#9DA5B4"); chart.Plot.Axes.Bottom.TickLabelStyle.ForeColor = textColor; chart.Plot.Axes.Left.TickLabelStyle.ForeColor = textColor; chart.Plot.Axes.Bottom.Label.ForeColor = textColor; diff --git a/Lite/Services/LocalDataService.Blocking.cs b/Lite/Services/LocalDataService.Blocking.cs index c52b0c9d..11616d79 100644 --- a/Lite/Services/LocalDataService.Blocking.cs +++ b/Lite/Services/LocalDataService.Blocking.cs @@ -143,6 +143,7 @@ FROM query_snapshots WHERE server_id = $1 AND collection_time >= $2 AND collection_time <= $3 +AND query_text NOT LIKE 'WAITFOR%' ORDER BY collection_time DESC, cpu_time_ms DESC"; command.Parameters.Add(new DuckDBParameter { Value = serverId }); diff --git a/Lite/Services/LocalDataService.Overview.cs b/Lite/Services/LocalDataService.Overview.cs index 2e45a864..f93cdf05 100644 --- a/Lite/Services/LocalDataService.Overview.cs +++ b/Lite/Services/LocalDataService.Overview.cs @@ -152,7 +152,7 @@ public class ServerSummaryItem DeadlockCount > 0 ? "#F44336" : BlockingCount > 0 ? "#FF9800" : CpuPercent >= 80 ? "#FF9800" : - "#555555"); + "#2a2d35"); private static SolidColorBrush MakeBrush(string hex) { diff --git a/Lite/Services/SystemTrayService.cs b/Lite/Services/SystemTrayService.cs index 820426ea..209b1d97 100644 --- a/Lite/Services/SystemTrayService.cs +++ b/Lite/Services/SystemTrayService.cs @@ -9,6 +9,7 @@ using System; using System.Windows; using System.Windows.Controls; +using System.Windows.Media; using System.Windows.Media.Imaging; using Hardcodet.Wpf.TaskbarNotification; @@ -24,6 +25,7 @@ public class SystemTrayService : IDisposable private readonly CollectionBackgroundService? _backgroundService; private bool _disposed; private MenuItem? _pauseResumeItem; + private TextBlock? _tooltipText; public SystemTrayService(Window mainWindow, CollectionBackgroundService? backgroundService = null) { @@ -38,9 +40,23 @@ public void Initialize() { _trayIcon?.Dispose(); - _trayIcon = new TaskbarIcon + _trayIcon = new TaskbarIcon(); + + /* Custom dark tooltip (native ToolTipText uses Windows light theme) */ + _tooltipText = new TextBlock + { + Text = "Performance Monitor Lite", + Foreground = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#E4E6EB")), + FontSize = 12 + }; + _trayIcon.TrayToolTip = new Border { - ToolTipText = "Performance Monitor Lite" + Background = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#22252b")), + BorderBrush = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#33363e")), + BorderThickness = new Thickness(1), + Padding = new Thickness(10, 8, 10, 8), + CornerRadius = new CornerRadius(4), + Child = _tooltipText }; /* Load dark theme for context menu styling */ @@ -64,19 +80,19 @@ public void Initialize() var contextMenu = new ContextMenu(); contextMenu.Resources.MergedDictionaries.Add(darkTheme); - var showItem = new MenuItem { Header = "Show Window" }; + var showItem = new MenuItem { Header = "Show Window", Icon = new TextBlock { Text = "📊", Background = Brushes.Transparent } }; showItem.Click += (s, e) => ShowMainWindow(); contextMenu.Items.Add(showItem); contextMenu.Items.Add(new Separator()); - _pauseResumeItem = new MenuItem { Header = "Pause Collection" }; + _pauseResumeItem = new MenuItem { Header = "Pause Collection", Icon = new TextBlock { Text = "⏸", Background = Brushes.Transparent } }; _pauseResumeItem.Click += (s, e) => ToggleCollection(); contextMenu.Items.Add(_pauseResumeItem); contextMenu.Items.Add(new Separator()); - var exitItem = new MenuItem { Header = "Exit" }; + var exitItem = new MenuItem { Header = "Exit", Icon = new TextBlock { Text = "✕", Background = Brushes.Transparent } }; exitItem.Click += (s, e) => ExitApplication(); contextMenu.Items.Add(exitItem); @@ -114,11 +130,12 @@ private void ToggleCollection() if (_pauseResumeItem != null) { _pauseResumeItem.Header = _backgroundService.IsPaused ? "Resume Collection" : "Pause Collection"; + _pauseResumeItem.Icon = new TextBlock { Text = _backgroundService.IsPaused ? "▶" : "⏸", Background = Brushes.Transparent }; } - if (_trayIcon != null) + if (_tooltipText != null) { - _trayIcon.ToolTipText = _backgroundService.IsPaused + _tooltipText.Text = _backgroundService.IsPaused ? "Performance Monitor Lite (Paused)" : "Performance Monitor Lite"; } diff --git a/Lite/Themes/DarkTheme.xaml b/Lite/Themes/DarkTheme.xaml index 933d0a28..5af346d9 100644 --- a/Lite/Themes/DarkTheme.xaml +++ b/Lite/Themes/DarkTheme.xaml @@ -11,21 +11,21 @@ #5bc4f5 #1a9ae0 - - #252525 - #2d2d2d - #333333 - #404040 - #4a4a4a + + #111217 + #1c1f25 + #181b1f + #22252b + #2a2d35 - - #FFFFFF - #D0D0D0 - #B0B0B0 + + #E4E6EB + #9DA5B4 + #6B7280 - - #555555 - #666666 + + #2a2d35 + #33363e #4CAF50 @@ -83,7 +83,7 @@ @@ -514,7 +514,7 @@ - + @@ -587,7 +587,7 @@ - + @@ -999,9 +999,22 @@ diff --git a/Lite/Windows/ProcedureHistoryWindow.xaml.cs b/Lite/Windows/ProcedureHistoryWindow.xaml.cs index 4e059dea..1d9547b2 100644 --- a/Lite/Windows/ProcedureHistoryWindow.xaml.cs +++ b/Lite/Windows/ProcedureHistoryWindow.xaml.cs @@ -120,10 +120,10 @@ private void UpdateChart() private static void ApplyDarkTheme(ScottPlot.WPF.WpfPlot chart) { - var darkBg = ScottPlot.Color.FromHex("#333333"); - var darkerBg = ScottPlot.Color.FromHex("#252525"); - var text = ScottPlot.Color.FromHex("#E0E0E0"); - var grid = ScottPlot.Color.FromHex("#444444"); + var darkBg = ScottPlot.Color.FromHex("#22252b"); + var darkerBg = ScottPlot.Color.FromHex("#111217"); + var text = ScottPlot.Color.FromHex("#9DA5B4"); + var grid = ScottPlot.Colors.White.WithAlpha(20); chart.Plot.FigureBackground.Color = darkBg; chart.Plot.DataBackground.Color = darkerBg; diff --git a/Lite/Windows/QueryStatsHistoryWindow.xaml.cs b/Lite/Windows/QueryStatsHistoryWindow.xaml.cs index 5a9db62f..76a51bfd 100644 --- a/Lite/Windows/QueryStatsHistoryWindow.xaml.cs +++ b/Lite/Windows/QueryStatsHistoryWindow.xaml.cs @@ -155,10 +155,10 @@ private async void DownloadPlan_Click(object sender, RoutedEventArgs e) private static void ApplyDarkTheme(ScottPlot.WPF.WpfPlot chart) { - var darkBg = ScottPlot.Color.FromHex("#333333"); - var darkerBg = ScottPlot.Color.FromHex("#252525"); - var text = ScottPlot.Color.FromHex("#E0E0E0"); - var grid = ScottPlot.Color.FromHex("#444444"); + var darkBg = ScottPlot.Color.FromHex("#22252b"); + var darkerBg = ScottPlot.Color.FromHex("#111217"); + var text = ScottPlot.Color.FromHex("#9DA5B4"); + var grid = ScottPlot.Colors.White.WithAlpha(20); chart.Plot.FigureBackground.Color = darkBg; chart.Plot.DataBackground.Color = darkerBg; diff --git a/Lite/Windows/QueryStoreHistoryWindow.xaml.cs b/Lite/Windows/QueryStoreHistoryWindow.xaml.cs index df036ee6..8699d208 100644 --- a/Lite/Windows/QueryStoreHistoryWindow.xaml.cs +++ b/Lite/Windows/QueryStoreHistoryWindow.xaml.cs @@ -158,10 +158,10 @@ private async void DownloadPlan_Click(object sender, RoutedEventArgs e) private static void ApplyDarkTheme(ScottPlot.WPF.WpfPlot chart) { - var darkBg = ScottPlot.Color.FromHex("#333333"); - var darkerBg = ScottPlot.Color.FromHex("#252525"); - var text = ScottPlot.Color.FromHex("#E0E0E0"); - var grid = ScottPlot.Color.FromHex("#444444"); + var darkBg = ScottPlot.Color.FromHex("#22252b"); + var darkerBg = ScottPlot.Color.FromHex("#111217"); + var text = ScottPlot.Color.FromHex("#9DA5B4"); + var grid = ScottPlot.Colors.White.WithAlpha(20); chart.Plot.FigureBackground.Color = darkBg; chart.Plot.DataBackground.Color = darkerBg; From c2d1841885fe97223004c1522a92f2da33680b57 Mon Sep 17 00:00:00 2001 From: Erik Darling <2136037+erikdarlingdata@users.noreply.github.com> Date: Fri, 13 Feb 2026 07:36:34 -0500 Subject: [PATCH 11/29] Centralized Material Design 300 color palette + visual polish (#16) Replace all stock ScottPlot colors and scattered inline hex colors with a centralized TabHelpers.ChartColors palette (12 Material Design 300-level pastels optimized for dark backgrounds). Standardize MarkerSize to 5 across all Dashboard charts for consistent dot visibility matching Lite. Desaturate status indicator colors from Material 500 to 300 in both apps (green #81C784, amber #FFD54F, red #E57373, orange #FFB74D). Increase DataGrid cell padding from 6,4 to 8,6 for better readability. Add ClipToBounds to DataGrid style for clean edge rendering. Co-Authored-By: Claude Opus 4.6 --- Dashboard/Controls/LandingPage.xaml | 6 +- Dashboard/Controls/MemoryContent.xaml.cs | 44 +++---- .../Controls/QueryPerformanceContent.xaml.cs | 8 +- .../Controls/ResourceMetricsContent.xaml.cs | 118 ++++++++---------- Dashboard/Controls/ServerHealthCard.xaml | 4 +- .../Controls/SystemEventsContent.xaml.cs | 98 +++++++-------- Dashboard/Helpers/DateFilterHelper.cs | 16 +-- Dashboard/Helpers/TabHelpers.cs | 21 ++++ Dashboard/Models/ServerListItem.cs | 2 +- Dashboard/ServerTab.xaml.cs | 46 ++++--- .../DatabaseService.QueryPerformance.cs | 10 +- Dashboard/Themes/DarkTheme.xaml | 12 +- Lite/Controls/ServerTab.xaml.cs | 34 ++--- Lite/MainWindow.xaml | 4 +- Lite/Services/LocalDataService.Overview.cs | 16 +-- Lite/Themes/DarkTheme.xaml | 12 +- 16 files changed, 233 insertions(+), 218 deletions(-) diff --git a/Dashboard/Controls/LandingPage.xaml b/Dashboard/Controls/LandingPage.xaml index 250f2a10..7e99b049 100644 --- a/Dashboard/Controls/LandingPage.xaml +++ b/Dashboard/Controls/LandingPage.xaml @@ -113,15 +113,15 @@ Padding="16,8"> - + - + - + diff --git a/Dashboard/Controls/MemoryContent.xaml.cs b/Dashboard/Controls/MemoryContent.xaml.cs index 0b7e662d..56ffdec8 100644 --- a/Dashboard/Controls/MemoryContent.xaml.cs +++ b/Dashboard/Controls/MemoryContent.xaml.cs @@ -213,26 +213,26 @@ private void LoadMemoryStatsOverviewChart(List memoryData, int var totalScatter = MemoryStatsOverviewChart.Plot.Add.Scatter(totalXs, totalYs); totalScatter.LineWidth = 2; - totalScatter.MarkerSize = 0; - totalScatter.Color = ScottPlot.Colors.Gray; + totalScatter.MarkerSize = 5; + totalScatter.Color = TabHelpers.ChartColors[9]; totalScatter.LegendText = "Total Memory"; var bufferScatter = MemoryStatsOverviewChart.Plot.Add.Scatter(bufferXs, bufferYs); bufferScatter.LineWidth = 2; - bufferScatter.MarkerSize = 0; - bufferScatter.Color = ScottPlot.Colors.Blue; + bufferScatter.MarkerSize = 5; + bufferScatter.Color = TabHelpers.ChartColors[0]; bufferScatter.LegendText = "Buffer Pool"; var cacheScatter = MemoryStatsOverviewChart.Plot.Add.Scatter(cacheXs, cacheYs); cacheScatter.LineWidth = 2; - cacheScatter.MarkerSize = 0; - cacheScatter.Color = ScottPlot.Colors.Green; + cacheScatter.MarkerSize = 5; + cacheScatter.Color = TabHelpers.ChartColors[1]; cacheScatter.LegendText = "Plan Cache"; var availScatter = MemoryStatsOverviewChart.Plot.Add.Scatter(availXs, availYs); availScatter.LineWidth = 2; - availScatter.MarkerSize = 0; - availScatter.Color = ScottPlot.Colors.Orange; + availScatter.MarkerSize = 5; + availScatter.Color = TabHelpers.ChartColors[2]; availScatter.LegendText = "Available Physical"; _legendPanels[MemoryStatsOverviewChart] = MemoryStatsOverviewChart.Plot.ShowLegend(ScottPlot.Edge.Bottom); @@ -282,7 +282,7 @@ private void AddPressureWarningSpans(List dataList) if (item.BufferPoolPressureWarning && item.PlanCachePressureWarning) { - vline.Color = ScottPlot.Colors.Red.WithAlpha(0.5); + vline.Color = TabHelpers.ChartColors[3].WithAlpha(0.5); // Add legend entry for BP pressure (covers "both" case too) if (!bpLegendAdded) { @@ -292,7 +292,7 @@ private void AddPressureWarningSpans(List dataList) } else if (item.BufferPoolPressureWarning) { - vline.Color = ScottPlot.Colors.Red.WithAlpha(0.3); + vline.Color = TabHelpers.ChartColors[3].WithAlpha(0.3); if (!bpLegendAdded) { vline.LegendText = "BP Pressure"; @@ -301,7 +301,7 @@ private void AddPressureWarningSpans(List dataList) } else { - vline.Color = ScottPlot.Colors.Orange.WithAlpha(0.3); + vline.Color = TabHelpers.ChartColors[2].WithAlpha(0.3); if (!pcLegendAdded) { vline.LegendText = "PC Pressure"; @@ -419,14 +419,14 @@ private void LoadMemoryGrantsChart(IEnumerable data, int h { var grantedScatter = MemoryGrantsChart.Plot.Add.Scatter(grantedXs, grantedYs); grantedScatter.LineWidth = 2; - grantedScatter.MarkerSize = 0; - grantedScatter.Color = ScottPlot.Colors.Blue; + grantedScatter.MarkerSize = 5; + grantedScatter.Color = TabHelpers.ChartColors[0]; grantedScatter.LegendText = "Granted MB"; var targetScatter = MemoryGrantsChart.Plot.Add.Scatter(targetXs, targetYs); targetScatter.LineWidth = 2; - targetScatter.MarkerSize = 0; - targetScatter.Color = ScottPlot.Colors.Orange; + targetScatter.MarkerSize = 5; + targetScatter.Color = TabHelpers.ChartColors[2]; targetScatter.LegendText = "Target MB"; _legendPanels[MemoryGrantsChart] = MemoryGrantsChart.Plot.ShowLegend(ScottPlot.Edge.Bottom); @@ -502,7 +502,7 @@ private void LoadMemoryClerksChart(List data, int hoursBack, D .Select(x => x.ClerkType) .ToList(); - var colors = new[] { ScottPlot.Colors.Blue, ScottPlot.Colors.Green, ScottPlot.Colors.Orange, ScottPlot.Colors.Red, ScottPlot.Colors.Purple }; + var colors = TabHelpers.ChartColors; int colorIndex = 0; foreach (var clerkType in topClerks) @@ -519,7 +519,7 @@ private void LoadMemoryClerksChart(List data, int hoursBack, D var scatter = MemoryClerksChart.Plot.Add.Scatter(xs, ys); scatter.LineWidth = 2; - scatter.MarkerSize = 0; + scatter.MarkerSize = 5; scatter.Color = colors[colorIndex % colors.Length]; scatter.LegendText = clerkType.Length > 20 ? clerkType.Substring(0, 20) + "..." : clerkType; colorIndex++; @@ -647,8 +647,8 @@ private void LoadPlanCacheChart(IEnumerable data, int hoursB var singleScatter = PlanCacheChart.Plot.Add.Scatter(singleXs, singleYs); singleScatter.LineWidth = 2; - singleScatter.MarkerSize = 0; - singleScatter.Color = ScottPlot.Colors.Red; + singleScatter.MarkerSize = 5; + singleScatter.Color = TabHelpers.ChartColors[3]; singleScatter.LegendText = "Single-Use"; // Multi-Use series with gap filling @@ -658,8 +658,8 @@ private void LoadPlanCacheChart(IEnumerable data, int hoursB var multiScatter = PlanCacheChart.Plot.Add.Scatter(multiXs, multiYs); multiScatter.LineWidth = 2; - multiScatter.MarkerSize = 0; - multiScatter.Color = ScottPlot.Colors.Green; + multiScatter.MarkerSize = 5; + multiScatter.Color = TabHelpers.ChartColors[1]; multiScatter.LegendText = "Multi-Use"; _legendPanels[PlanCacheChart] = PlanCacheChart.Plot.ShowLegend(ScottPlot.Edge.Bottom); @@ -786,7 +786,7 @@ private void LoadMemoryPressureEventsChart(IEnumerable var highScatter = MemoryPressureEventsChart.Plot.Add.Scatter(xs, ys); highScatter.LineWidth = 2; highScatter.MarkerSize = 5; - highScatter.Color = ScottPlot.Colors.Red; + highScatter.Color = TabHelpers.ChartColors[3]; highScatter.LegendText = "High Pressure Events"; _legendPanels[MemoryPressureEventsChart] = MemoryPressureEventsChart.Plot.ShowLegend(ScottPlot.Edge.Bottom); diff --git a/Dashboard/Controls/QueryPerformanceContent.xaml.cs b/Dashboard/Controls/QueryPerformanceContent.xaml.cs index e3dd1e8e..34509c4e 100644 --- a/Dashboard/Controls/QueryPerformanceContent.xaml.cs +++ b/Dashboard/Controls/QueryPerformanceContent.xaml.cs @@ -243,9 +243,9 @@ await Task.WhenAll( QueryStoreNoDataMessage.Visibility = queryStore.Count == 0 ? Visibility.Visible : Visibility.Collapsed; // Populate charts from time-series data - LoadDurationChart(QueryPerfTrendsQueryChart, await queryDurationTrendsTask, _perfTrendsHoursBack, _perfTrendsFromDate, _perfTrendsToDate, "Duration (ms/sec)", ScottPlot.Colors.Blue); - LoadDurationChart(QueryPerfTrendsProcChart, await procDurationTrendsTask, _perfTrendsHoursBack, _perfTrendsFromDate, _perfTrendsToDate, "Duration (ms/sec)", ScottPlot.Colors.Green); - LoadDurationChart(QueryPerfTrendsQsChart, await qsDurationTrendsTask, _perfTrendsHoursBack, _perfTrendsFromDate, _perfTrendsToDate, "Duration (ms/sec)", ScottPlot.Colors.Purple); + LoadDurationChart(QueryPerfTrendsQueryChart, await queryDurationTrendsTask, _perfTrendsHoursBack, _perfTrendsFromDate, _perfTrendsToDate, "Duration (ms/sec)", TabHelpers.ChartColors[0]); + LoadDurationChart(QueryPerfTrendsProcChart, await procDurationTrendsTask, _perfTrendsHoursBack, _perfTrendsFromDate, _perfTrendsToDate, "Duration (ms/sec)", TabHelpers.ChartColors[1]); + LoadDurationChart(QueryPerfTrendsQsChart, await qsDurationTrendsTask, _perfTrendsHoursBack, _perfTrendsFromDate, _perfTrendsToDate, "Duration (ms/sec)", TabHelpers.ChartColors[4]); LoadExecChart(await execTrendsTask, _perfTrendsHoursBack, _perfTrendsFromDate, _perfTrendsToDate); } catch (Exception ex) @@ -972,7 +972,7 @@ private void LoadExecChart(IEnumerable execTrends, int hours var scatter = QueryPerfTrendsExecChart.Plot.Add.Scatter(xs, ys); scatter.LineWidth = 2; scatter.MarkerSize = 5; - scatter.Color = ScottPlot.Colors.Blue; + scatter.Color = TabHelpers.ChartColors[0]; scatter.LegendText = "Executions/sec"; if (xs.Length == 0) diff --git a/Dashboard/Controls/ResourceMetricsContent.xaml.cs b/Dashboard/Controls/ResourceMetricsContent.xaml.cs index e755ded5..f680bf3b 100644 --- a/Dashboard/Controls/ResourceMetricsContent.xaml.cs +++ b/Dashboard/Controls/ResourceMetricsContent.xaml.cs @@ -289,7 +289,7 @@ private void LoadLatchStatsChart(IEnumerable data, int hoursBack .Select(x => x.LatchClass) .ToList(); - var colors = new[] { ScottPlot.Colors.Blue, ScottPlot.Colors.Green, ScottPlot.Colors.Orange, ScottPlot.Colors.Red, ScottPlot.Colors.Purple }; + var colors = TabHelpers.ChartColors; int colorIndex = 0; foreach (var latchClass in topLatches) @@ -306,7 +306,7 @@ private void LoadLatchStatsChart(IEnumerable data, int hoursBack var scatter = LatchStatsChart.Plot.Add.Scatter(xs, ys); scatter.LineWidth = 2; - scatter.MarkerSize = 0; + scatter.MarkerSize = 5; scatter.Color = colors[colorIndex % colors.Length]; scatter.LegendText = latchClass?.Length > 20 ? latchClass.Substring(0, 20) + "..." : latchClass ?? ""; colorIndex++; @@ -379,7 +379,7 @@ private void LoadSpinlockStatsChart(IEnumerable data, int hou .Select(x => x.SpinlockName) .ToList(); - var colors = new[] { ScottPlot.Colors.Blue, ScottPlot.Colors.Green, ScottPlot.Colors.Orange, ScottPlot.Colors.Red, ScottPlot.Colors.Purple }; + var colors = TabHelpers.ChartColors; int colorIndex = 0; foreach (var spinlock in topSpinlocks) @@ -396,7 +396,7 @@ private void LoadSpinlockStatsChart(IEnumerable data, int hou var scatter = SpinlockStatsChart.Plot.Add.Scatter(xs, ys); scatter.LineWidth = 2; - scatter.MarkerSize = 0; + scatter.MarkerSize = 5; scatter.Color = colors[colorIndex % colors.Length]; scatter.LegendText = spinlock?.Length > 20 ? spinlock.Substring(0, 20) + "..." : spinlock ?? ""; colorIndex++; @@ -494,8 +494,8 @@ private void LoadCombinedTempDbLatencyChart(List da aggregated.Select(d => d.AvgReadLatency)); var readScatter = TempDbLatencyChart.Plot.Add.Scatter(readXs, readYs); readScatter.LineWidth = 2; - readScatter.MarkerSize = 0; - readScatter.Color = ScottPlot.Colors.Blue; + readScatter.MarkerSize = 5; + readScatter.Color = TabHelpers.ChartColors[0]; readScatter.LegendText = "Read Latency"; // Write Latency series @@ -504,8 +504,8 @@ private void LoadCombinedTempDbLatencyChart(List da aggregated.Select(d => d.AvgWriteLatency)); var writeScatter = TempDbLatencyChart.Plot.Add.Scatter(writeXs, writeYs); writeScatter.LineWidth = 2; - writeScatter.MarkerSize = 0; - writeScatter.Color = ScottPlot.Colors.Orange; + writeScatter.MarkerSize = 5; + writeScatter.Color = TabHelpers.ChartColors[2]; writeScatter.LegendText = "Write Latency"; // Store legend panel reference for removal on refresh (ScottPlot issue #4717) @@ -554,8 +554,8 @@ private void LoadTempdbStatsChart(IEnumerable data, int hoursBa dataList.Select(d => (double)d.UserObjectReservedMb)); var userScatter = TempdbStatsChart.Plot.Add.Scatter(userXs, userYs); userScatter.LineWidth = 2; - userScatter.MarkerSize = 0; - userScatter.Color = ScottPlot.Colors.Blue; + userScatter.MarkerSize = 5; + userScatter.Color = TabHelpers.ChartColors[0]; userScatter.LegendText = "User Objects"; // Version Store series @@ -564,8 +564,8 @@ private void LoadTempdbStatsChart(IEnumerable data, int hoursBa dataList.Select(d => (double)d.VersionStoreReservedMb)); var versionScatter = TempdbStatsChart.Plot.Add.Scatter(versionXs, versionYs); versionScatter.LineWidth = 2; - versionScatter.MarkerSize = 0; - versionScatter.Color = ScottPlot.Colors.Green; + versionScatter.MarkerSize = 5; + versionScatter.Color = TabHelpers.ChartColors[1]; versionScatter.LegendText = "Version Store"; // Internal Objects series @@ -574,8 +574,8 @@ private void LoadTempdbStatsChart(IEnumerable data, int hoursBa dataList.Select(d => (double)d.InternalObjectReservedMb)); var internalScatter = TempdbStatsChart.Plot.Add.Scatter(internalXs, internalYs); internalScatter.LineWidth = 2; - internalScatter.MarkerSize = 0; - internalScatter.Color = ScottPlot.Colors.Orange; + internalScatter.MarkerSize = 5; + internalScatter.Color = TabHelpers.ChartColors[2]; internalScatter.LegendText = "Internal Objects"; // Unallocated (free space) series @@ -586,8 +586,8 @@ private void LoadTempdbStatsChart(IEnumerable data, int hoursBa { var unallocScatter = TempdbStatsChart.Plot.Add.Scatter(unallocXs, unallocYs); unallocScatter.LineWidth = 2; - unallocScatter.MarkerSize = 0; - unallocScatter.Color = ScottPlot.Colors.Gray; + unallocScatter.MarkerSize = 5; + unallocScatter.Color = TabHelpers.ChartColors[9]; unallocScatter.LegendText = "Unallocated"; } @@ -600,8 +600,8 @@ private void LoadTempdbStatsChart(IEnumerable data, int hoursBa topTaskValues); var topTaskScatter = TempdbStatsChart.Plot.Add.Scatter(topTaskXs, topTaskYs); topTaskScatter.LineWidth = 2; - topTaskScatter.MarkerSize = 0; - topTaskScatter.Color = ScottPlot.Colors.Red; + topTaskScatter.MarkerSize = 5; + topTaskScatter.Color = TabHelpers.ChartColors[3]; topTaskScatter.LegendText = "Top Task"; } @@ -700,8 +700,8 @@ private void LoadSessionStatsChart(IEnumerable data, int hours var (xs, ys) = TabHelpers.FillTimeSeriesGaps(timePoints, totalCounts.Select(c => c)); var totalScatter = SessionStatsChart.Plot.Add.Scatter(xs, ys); totalScatter.LineWidth = 2; - totalScatter.MarkerSize = 0; - totalScatter.Color = ScottPlot.Colors.Blue; + totalScatter.MarkerSize = 5; + totalScatter.Color = TabHelpers.ChartColors[0]; totalScatter.LegendText = "Total"; } @@ -710,8 +710,8 @@ private void LoadSessionStatsChart(IEnumerable data, int hours var (xs, ys) = TabHelpers.FillTimeSeriesGaps(timePoints, runningCounts.Select(c => c)); var runningScatter = SessionStatsChart.Plot.Add.Scatter(xs, ys); runningScatter.LineWidth = 2; - runningScatter.MarkerSize = 0; - runningScatter.Color = ScottPlot.Colors.Green; + runningScatter.MarkerSize = 5; + runningScatter.Color = TabHelpers.ChartColors[1]; runningScatter.LegendText = "Running"; } @@ -720,8 +720,8 @@ private void LoadSessionStatsChart(IEnumerable data, int hours var (xs, ys) = TabHelpers.FillTimeSeriesGaps(timePoints, sleepingCounts.Select(c => c)); var sleepingScatter = SessionStatsChart.Plot.Add.Scatter(xs, ys); sleepingScatter.LineWidth = 2; - sleepingScatter.MarkerSize = 0; - sleepingScatter.Color = ScottPlot.Colors.Orange; + sleepingScatter.MarkerSize = 5; + sleepingScatter.Color = TabHelpers.ChartColors[2]; sleepingScatter.LegendText = "Sleeping"; } @@ -731,8 +731,8 @@ private void LoadSessionStatsChart(IEnumerable data, int hours var (xs, ys) = TabHelpers.FillTimeSeriesGaps(timePoints, backgroundCounts.Select(c => c)); var backgroundScatter = SessionStatsChart.Plot.Add.Scatter(xs, ys); backgroundScatter.LineWidth = 2; - backgroundScatter.MarkerSize = 0; - backgroundScatter.Color = ScottPlot.Colors.Purple; + backgroundScatter.MarkerSize = 5; + backgroundScatter.Color = TabHelpers.ChartColors[4]; backgroundScatter.LegendText = "Background"; } @@ -742,8 +742,8 @@ private void LoadSessionStatsChart(IEnumerable data, int hours var (xs, ys) = TabHelpers.FillTimeSeriesGaps(timePoints, dormantCounts.Select(c => c)); var dormantScatter = SessionStatsChart.Plot.Add.Scatter(xs, ys); dormantScatter.LineWidth = 2; - dormantScatter.MarkerSize = 0; - dormantScatter.Color = ScottPlot.Colors.Cyan; + dormantScatter.MarkerSize = 5; + dormantScatter.Color = TabHelpers.ChartColors[5]; dormantScatter.LegendText = "Dormant"; } @@ -753,8 +753,8 @@ private void LoadSessionStatsChart(IEnumerable data, int hours var (xs, ys) = TabHelpers.FillTimeSeriesGaps(timePoints, idleOver30MinCounts.Select(c => c)); var idleScatter = SessionStatsChart.Plot.Add.Scatter(xs, ys); idleScatter.LineWidth = 2; - idleScatter.MarkerSize = 0; - idleScatter.Color = ScottPlot.Colors.Gray; + idleScatter.MarkerSize = 5; + idleScatter.Color = TabHelpers.ChartColors[9]; idleScatter.LegendText = "Idle >30m"; } @@ -764,8 +764,8 @@ private void LoadSessionStatsChart(IEnumerable data, int hours var (xs, ys) = TabHelpers.FillTimeSeriesGaps(timePoints, waitingForMemoryCounts.Select(c => c)); var waitingScatter = SessionStatsChart.Plot.Add.Scatter(xs, ys); waitingScatter.LineWidth = 2; - waitingScatter.MarkerSize = 0; - waitingScatter.Color = ScottPlot.Colors.Red; + waitingScatter.MarkerSize = 5; + waitingScatter.Color = TabHelpers.ChartColors[3]; waitingScatter.LegendText = "Waiting for Memory"; } @@ -828,7 +828,7 @@ private async Task LoadFileIoLatencyChartsAsync() double xMin = rangeStart.ToOADate(); double xMax = rangeEnd.ToOADate(); - var colors = new[] { ScottPlot.Colors.Blue, ScottPlot.Colors.Green, ScottPlot.Colors.Orange, ScottPlot.Colors.Red, ScottPlot.Colors.Purple, ScottPlot.Colors.Cyan, ScottPlot.Colors.Magenta, ScottPlot.Colors.DarkGreen, ScottPlot.Colors.Navy, ScottPlot.Colors.Brown }; + var colors = TabHelpers.ChartColors; // Load User DB data only - TempDB latency moved to TempDB Stats tab var userDbData = await _databaseService.GetFileIoLatencyTimeSeriesAsync(isTempDb: false, _fileIoHoursBack, _fileIoFromDate, _fileIoToDate); @@ -872,7 +872,7 @@ private void LoadFileIoChart(ScottPlot.WPF.WpfPlot chart, List cpuData, int hou dataList.Select(d => (double)d.SqlServerCpu)); var sqlScatter = ServerUtilTrendsCpuChart.Plot.Add.Scatter(sqlXs, sqlYs); sqlScatter.LineWidth = 2; - sqlScatter.MarkerSize = 0; - sqlScatter.Color = ScottPlot.Colors.Blue; + sqlScatter.MarkerSize = 5; + sqlScatter.Color = TabHelpers.ChartColors[0]; sqlScatter.LegendText = "SQL CPU"; // Other CPU series @@ -970,8 +970,8 @@ private void LoadServerTrendsCpuChart(IEnumerable cpuData, int hou dataList.Select(d => (double)d.OtherProcessCpu)); var otherScatter = ServerUtilTrendsCpuChart.Plot.Add.Scatter(otherXs, otherYs); otherScatter.LineWidth = 2; - otherScatter.MarkerSize = 0; - otherScatter.Color = ScottPlot.Colors.Orange; + otherScatter.MarkerSize = 5; + otherScatter.Color = TabHelpers.ChartColors[2]; otherScatter.LegendText = "Other CPU"; _legendPanels[ServerUtilTrendsCpuChart] = ServerUtilTrendsCpuChart.Plot.ShowLegend(ScottPlot.Edge.Bottom); @@ -1023,14 +1023,14 @@ private void LoadServerTrendsTempdbChart(IEnumerable tempdbData var userScatter = ServerUtilTrendsTempdbChart.Plot.Add.Scatter(userXs, userYs); userScatter.LineWidth = 2; - userScatter.MarkerSize = 0; - userScatter.Color = ScottPlot.Colors.Green; + userScatter.MarkerSize = 5; + userScatter.Color = TabHelpers.ChartColors[1]; userScatter.LegendText = "User Objects"; var versionScatter = ServerUtilTrendsTempdbChart.Plot.Add.Scatter(versionXs, versionYs); versionScatter.LineWidth = 2; - versionScatter.MarkerSize = 0; - versionScatter.Color = ScottPlot.Colors.Orange; + versionScatter.MarkerSize = 5; + versionScatter.Color = TabHelpers.ChartColors[2]; versionScatter.LegendText = "Version Store"; _legendPanels[ServerUtilTrendsTempdbChart] = ServerUtilTrendsTempdbChart.Plot.ShowLegend(ScottPlot.Edge.Bottom); @@ -1118,14 +1118,14 @@ private void LoadServerTrendsMemoryChart(IEnumerable memoryData var bufferScatter = ServerUtilTrendsMemoryChart.Plot.Add.Scatter(bufferXs, bufferYs); bufferScatter.LineWidth = 2; - bufferScatter.MarkerSize = 0; - bufferScatter.Color = ScottPlot.Colors.Purple; + bufferScatter.MarkerSize = 5; + bufferScatter.Color = TabHelpers.ChartColors[4]; bufferScatter.LegendText = "Buffer Pool"; var cacheScatter = ServerUtilTrendsMemoryChart.Plot.Add.Scatter(cacheXs, cacheYs); cacheScatter.LineWidth = 2; - cacheScatter.MarkerSize = 0; - cacheScatter.Color = ScottPlot.Colors.Cyan; + cacheScatter.MarkerSize = 5; + cacheScatter.Color = TabHelpers.ChartColors[5]; cacheScatter.LegendText = "Plan Cache"; _legendPanels[ServerUtilTrendsMemoryChart] = ServerUtilTrendsMemoryChart.Plot.ShowLegend(ScottPlot.Edge.Bottom); @@ -1176,10 +1176,10 @@ private void LoadServerTrendsPerfmonChart(IEnumerable perfmonD // Counters to display var countersToShow = new[] { - ("Batch Requests/sec", ScottPlot.Colors.Blue), - ("SQL Compilations/sec", ScottPlot.Colors.Orange), - ("SQL Re-Compilations/sec", ScottPlot.Colors.Red), - ("Optimizer Statistics", ScottPlot.Colors.Green) + ("Batch Requests/sec", TabHelpers.ChartColors[0]), + ("SQL Compilations/sec", TabHelpers.ChartColors[2]), + ("SQL Re-Compilations/sec", TabHelpers.ChartColors[3]), + ("Optimizer Statistics", TabHelpers.ChartColors[1]) }; // Get all time points across all counters for gap filling @@ -1201,7 +1201,7 @@ private void LoadServerTrendsPerfmonChart(IEnumerable perfmonD var scatter = ServerUtilTrendsPerfmonChart.Plot.Add.Scatter(xs, ys); scatter.LineWidth = 2; - scatter.MarkerSize = 0; + scatter.MarkerSize = 5; scatter.Color = color; scatter.LegendText = counterName.Replace("/sec", "", StringComparison.Ordinal); linesAdded++; @@ -1677,11 +1677,7 @@ private void LoadPerfmonCountersChart(List? data, int hoursBac return; } - var colors = new[] { - ScottPlot.Colors.Blue, ScottPlot.Colors.Green, ScottPlot.Colors.Orange, ScottPlot.Colors.Red, - ScottPlot.Colors.Purple, ScottPlot.Colors.Cyan, ScottPlot.Colors.Magenta, ScottPlot.Colors.DarkGreen, - ScottPlot.Colors.Navy, ScottPlot.Colors.Brown, ScottPlot.Colors.Teal, ScottPlot.Colors.Olive - }; + var colors = TabHelpers.ChartColors; // Get all time points across all counters for gap filling int colorIndex = 0; @@ -1706,7 +1702,7 @@ private void LoadPerfmonCountersChart(List? data, int hoursBac var scatter = PerfmonCountersChart.Plot.Add.Scatter(xs, ys); scatter.LineWidth = 2; - scatter.MarkerSize = 3; // Show small markers to ensure visibility + scatter.MarkerSize = 5; // Show small markers to ensure visibility scatter.Color = colors[colorIndex % colors.Length]; scatter.LegendText = counter.CounterName; @@ -2044,11 +2040,7 @@ private void LoadWaitStatsDetailChart(List? data, int hoursB return; } - var colors = new[] { - ScottPlot.Colors.Blue, ScottPlot.Colors.Green, ScottPlot.Colors.Orange, ScottPlot.Colors.Red, - ScottPlot.Colors.Purple, ScottPlot.Colors.Cyan, ScottPlot.Colors.Magenta, ScottPlot.Colors.DarkGreen, - ScottPlot.Colors.Navy, ScottPlot.Colors.Brown, ScottPlot.Colors.Teal, ScottPlot.Colors.Olive - }; + var colors = TabHelpers.ChartColors; // Get all time points across all wait types for gap filling int colorIndex = 0; @@ -2073,7 +2065,7 @@ private void LoadWaitStatsDetailChart(List? data, int hoursB var scatter = WaitStatsDetailChart.Plot.Add.Scatter(xs, ys); scatter.LineWidth = 2; - scatter.MarkerSize = 3; + scatter.MarkerSize = 5; scatter.Color = colors[colorIndex % colors.Length]; // Truncate legend text if too long diff --git a/Dashboard/Controls/ServerHealthCard.xaml b/Dashboard/Controls/ServerHealthCard.xaml index 3c07ea3c..b599da5a 100644 --- a/Dashboard/Controls/ServerHealthCard.xaml +++ b/Dashboard/Controls/ServerHealthCard.xaml @@ -214,11 +214,11 @@ diff --git a/Dashboard/Controls/SystemEventsContent.xaml.cs b/Dashboard/Controls/SystemEventsContent.xaml.cs index 4294939c..f9070bd9 100644 --- a/Dashboard/Controls/SystemEventsContent.xaml.cs +++ b/Dashboard/Controls/SystemEventsContent.xaml.cs @@ -417,8 +417,8 @@ private void LoadCorruptionEventsCharts(List data, orderedData.Select(d => (double)(d.BadPagesDetected ?? 0))); var scatter = BadPagesChart.Plot.Add.Scatter(xs, ys); scatter.LineWidth = 2; - scatter.MarkerSize = 4; - scatter.Color = ScottPlot.Colors.Red; + scatter.MarkerSize = 5; + scatter.Color = TabHelpers.ChartColors[3]; } else { @@ -445,8 +445,8 @@ private void LoadCorruptionEventsCharts(List data, orderedData.Select(d => (double)(d.IntervalDumpRequests ?? 0))); var scatter = DumpRequestsChart.Plot.Add.Scatter(xs, ys); scatter.LineWidth = 2; - scatter.MarkerSize = 4; - scatter.Color = ScottPlot.Colors.Orange; + scatter.MarkerSize = 5; + scatter.Color = TabHelpers.ChartColors[2]; } else { @@ -473,8 +473,8 @@ private void LoadCorruptionEventsCharts(List data, orderedData.Select(d => (double)(d.IsAccessViolationOccurred ?? 0))); var scatter = AccessViolationsChart.Plot.Add.Scatter(xs, ys); scatter.LineWidth = 2; - scatter.MarkerSize = 4; - scatter.Color = ScottPlot.Colors.Purple; + scatter.MarkerSize = 5; + scatter.Color = TabHelpers.ChartColors[4]; } else { @@ -501,8 +501,8 @@ private void LoadCorruptionEventsCharts(List data, orderedData.Select(d => (double)(d.WriteAccessViolationCount ?? 0))); var scatter = WriteAccessViolationsChart.Plot.Add.Scatter(xs, ys); scatter.LineWidth = 2; - scatter.MarkerSize = 4; - scatter.Color = ScottPlot.Colors.Blue; + scatter.MarkerSize = 5; + scatter.Color = TabHelpers.ChartColors[0]; } else { @@ -541,8 +541,8 @@ private void LoadContentionEventsCharts(List data, orderedData.Select(d => (double)(d.NonYieldingTasksReported ?? 0))); var scatter = NonYieldingTasksChart.Plot.Add.Scatter(xs, ys); scatter.LineWidth = 2; - scatter.MarkerSize = 4; - scatter.Color = ScottPlot.Colors.Red; + scatter.MarkerSize = 5; + scatter.Color = TabHelpers.ChartColors[3]; } else { @@ -569,8 +569,8 @@ private void LoadContentionEventsCharts(List data, orderedData.Select(d => (double)(d.LatchWarnings ?? 0))); var scatter = LatchWarningsChart.Plot.Add.Scatter(xs, ys); scatter.LineWidth = 2; - scatter.MarkerSize = 4; - scatter.Color = ScottPlot.Colors.Orange; + scatter.MarkerSize = 5; + scatter.Color = TabHelpers.ChartColors[2]; } else { @@ -605,7 +605,7 @@ private void LoadContentionEventsCharts(List data, .Take(5) // Limit to top 5 types to avoid chart clutter .ToList(); - var colors = new[] { ScottPlot.Colors.Purple, ScottPlot.Colors.Blue, ScottPlot.Colors.Green, ScottPlot.Colors.Cyan, ScottPlot.Colors.Magenta }; + var colors = TabHelpers.ChartColors; int colorIndex = 0; foreach (var spinlockType in spinlockTypes) @@ -621,7 +621,7 @@ private void LoadContentionEventsCharts(List data, typeData.Select(d => (double)(d.SpinlockBackoffs ?? 1))); // Use backoffs count or 1 if null var scatter = SickSpinlocksChart.Plot.Add.Scatter(xs, ys); scatter.LineWidth = 2; - scatter.MarkerSize = 4; + scatter.MarkerSize = 5; scatter.Color = colors[colorIndex % colors.Length]; scatter.LegendText = spinlockType ?? "Unknown"; colorIndex++; @@ -665,8 +665,8 @@ private void LoadContentionEventsCharts(List data, orderedData.Select(d => (double)(d.SystemCpuUtilization ?? 0))); var sysScatter = CpuComparisonChart.Plot.Add.Scatter(sysXs, sysYs); sysScatter.LineWidth = 2; - sysScatter.MarkerSize = 4; - sysScatter.Color = ScottPlot.Colors.Blue; + sysScatter.MarkerSize = 5; + sysScatter.Color = TabHelpers.ChartColors[0]; sysScatter.LegendText = "System CPU %"; // SQL CPU series @@ -675,8 +675,8 @@ private void LoadContentionEventsCharts(List data, orderedData.Select(d => (double)(d.SqlCpuUtilization ?? 0))); var sqlScatter = CpuComparisonChart.Plot.Add.Scatter(sqlXs, sqlYs); sqlScatter.LineWidth = 2; - sqlScatter.MarkerSize = 4; - sqlScatter.Color = ScottPlot.Colors.Green; + sqlScatter.MarkerSize = 5; + sqlScatter.Color = TabHelpers.ChartColors[1]; sqlScatter.LegendText = "SQL CPU %"; _legendPanels[CpuComparisonChart] = CpuComparisonChart.Plot.ShowLegend(ScottPlot.Edge.Bottom); @@ -773,7 +773,7 @@ private void LoadSevereErrorsChart(IEnumerable data var scatter = SevereErrorsChart.Plot.Add.Scatter(xs, ys); scatter.LineWidth = 2; scatter.MarkerSize = 5; - scatter.Color = ScottPlot.Colors.Red; + scatter.Color = TabHelpers.ChartColors[3]; scatter.LegendText = "Error Count"; _legendPanels[SevereErrorsChart] = SevereErrorsChart.Plot.ShowLegend(ScottPlot.Edge.Bottom); @@ -919,7 +919,7 @@ private void LoadIOIssuesChart(IEnumerable data, int ho var scatter = IOIssuesChart.Plot.Add.Scatter(xs, ys); scatter.LineWidth = 2; scatter.MarkerSize = 5; - scatter.Color = ScottPlot.Colors.Red; + scatter.Color = TabHelpers.ChartColors[3]; scatter.LegendText = "Latch Timeouts"; } @@ -929,7 +929,7 @@ private void LoadIOIssuesChart(IEnumerable data, int ho var scatter = IOIssuesChart.Plot.Add.Scatter(xs, ys); scatter.LineWidth = 2; scatter.MarkerSize = 5; - scatter.Color = ScottPlot.Colors.Orange; + scatter.Color = TabHelpers.ChartColors[2]; scatter.LegendText = "Long IOs"; } @@ -992,7 +992,7 @@ private void LoadLongestPendingIOChart(IEnumerable data if (filePathGroups.Count > 0) { hasData = true; - var colors = new[] { ScottPlot.Colors.Blue, ScottPlot.Colors.Green, ScottPlot.Colors.Purple, ScottPlot.Colors.Cyan, ScottPlot.Colors.Magenta }; + var colors = TabHelpers.ChartColors; int colorIndex = 0; foreach (var group in filePathGroups) @@ -1121,7 +1121,7 @@ long ParseNonYield(string? value) var scatter = SchedulerIssuesChart.Plot.Add.Scatter(xs, ys); scatter.LineWidth = 2; scatter.MarkerSize = 5; - scatter.Color = ScottPlot.Colors.Orange; + scatter.Color = TabHelpers.ChartColors[2]; scatter.LegendText = "Total Non-Yield Time"; _legendPanels[SchedulerIssuesChart] = SchedulerIssuesChart.Plot.ShowLegend(ScottPlot.Edge.Bottom); @@ -1268,7 +1268,7 @@ private void LoadMemoryConditionsChart(IEnumerable data, int h var scatter = CPUTasksChart.Plot.Add.Scatter(xs, ys); scatter.LineWidth = 2; scatter.MarkerSize = 5; - scatter.Color = ScottPlot.Colors.Blue; + scatter.Color = TabHelpers.ChartColors[0]; scatter.LegendText = "Workers Created"; // Max Workers threshold line (horizontal) @@ -1374,7 +1374,7 @@ private void LoadCPUTasksChart(IEnumerable data, int h if (maxWorkersValue > 0) { var hLine = CPUTasksChart.Plot.Add.HorizontalLine(maxWorkersValue); - hLine.Color = ScottPlot.Colors.Orange; + hLine.Color = TabHelpers.ChartColors[2]; hLine.LineWidth = 2; hLine.LinePattern = ScottPlot.LinePattern.Dashed; hLine.LegendText = $"Max Workers ({maxWorkersValue})"; @@ -1394,7 +1394,7 @@ private void LoadCPUTasksChart(IEnumerable data, int h var dlYs = unresolvableDLByHour.Select(b => 0.0).ToArray(); var dlScatter = CPUTasksChart.Plot.Add.Scatter(dlXs, dlYs); dlScatter.LineWidth = 0; - dlScatter.Color = ScottPlot.Colors.Red; + dlScatter.Color = TabHelpers.ChartColors[3]; dlScatter.LegendText = "Unresolvable DL"; dlScatter.MarkerSize = 10; dlScatter.MarkerShape = ScottPlot.MarkerShape.FilledCircle; @@ -1413,7 +1413,7 @@ private void LoadCPUTasksChart(IEnumerable data, int h var schedYs = schedDLByHour.Select(b => 0.0).ToArray(); var schedScatter = CPUTasksChart.Plot.Add.Scatter(schedXs, schedYs); schedScatter.LineWidth = 0; - schedScatter.Color = ScottPlot.Colors.Orange; + schedScatter.Color = TabHelpers.ChartColors[2]; schedScatter.LegendText = "Sched Deadlock"; schedScatter.MarkerSize = 10; schedScatter.MarkerShape = ScottPlot.MarkerShape.FilledCircle; @@ -1433,7 +1433,7 @@ private void LoadCPUTasksChart(IEnumerable data, int h var blockingYs = blockingByHour.Select(b => 0.0).ToArray(); // At bottom var blockingScatter = CPUTasksChart.Plot.Add.Scatter(blockingXs, blockingYs); blockingScatter.LineWidth = 0; // No connecting line - blockingScatter.Color = ScottPlot.Colors.Yellow; + blockingScatter.Color = TabHelpers.ChartColors[6]; blockingScatter.LegendText = "Blocking"; // Size points based on count - min 8, max 20, scaled by count var maxCount = blockingByHour.Max(b => b.Count); @@ -1566,7 +1566,7 @@ private void LoadMemoryBrokerChart(IEnumerable dat if (dataList.Count > 0) { - var colors = new[] { ScottPlot.Colors.Blue, ScottPlot.Colors.Green, ScottPlot.Colors.Orange, ScottPlot.Colors.Red, ScottPlot.Colors.Purple, ScottPlot.Colors.Cyan, ScottPlot.Colors.Magenta, ScottPlot.Colors.DarkGreen }; + var colors = TabHelpers.ChartColors; /* Chart 1: Currently Allocated by Broker */ var brokerGroups = dataList @@ -1587,7 +1587,7 @@ private void LoadMemoryBrokerChart(IEnumerable dat var scatter = MemoryBrokerChart.Plot.Add.Scatter(xs, ys); scatter.LineWidth = 2; - scatter.MarkerSize = 0; + scatter.MarkerSize = 5; scatter.Color = colors[colorIndex % colors.Length]; scatter.LegendText = brokerGroup.Key.Length > 25 ? brokerGroup.Key.Substring(0, 25) + "..." : brokerGroup.Key; colorIndex++; @@ -1613,8 +1613,8 @@ private void LoadMemoryBrokerChart(IEnumerable dat var scatter = MemoryBrokerRatioChart.Plot.Add.Scatter(xs, ys); scatter.LineWidth = 2; - scatter.MarkerSize = 0; - scatter.Color = ScottPlot.Colors.Blue; + scatter.MarkerSize = 5; + scatter.Color = TabHelpers.ChartColors[0]; scatter.LegendText = "Memory Ratio"; } @@ -1627,8 +1627,8 @@ private void LoadMemoryBrokerChart(IEnumerable dat var scatter = MemoryBrokerRatioChart.Plot.Add.Scatter(xs, ys); scatter.LineWidth = 2; - scatter.MarkerSize = 0; - scatter.Color = ScottPlot.Colors.Orange; + scatter.MarkerSize = 5; + scatter.Color = TabHelpers.ChartColors[2]; scatter.LegendText = "Overall"; } @@ -1795,7 +1795,7 @@ private void LoadMemoryNodeOOMChart(IEnumerable d var scatter = MemoryNodeOOMChart.Plot.Add.Scatter(xs, ys); scatter.LineWidth = 2; scatter.MarkerSize = 5; - scatter.Color = ScottPlot.Colors.Red; + scatter.Color = TabHelpers.ChartColors[3]; scatter.LegendText = "OOM Event Count"; _legendPanels[MemoryNodeOOMChart] = MemoryNodeOOMChart.Plot.ShowLegend(ScottPlot.Edge.Bottom); @@ -1847,8 +1847,8 @@ private void LoadMemoryNodeOOMUtilChart(IEnumerable (double)d.TargetKb!.Value / 1024.0).ToArray(); var scatter = MemoryNodeOOMMemoryChart.Plot.Add.Scatter(xs, ys); scatter.LineWidth = 2; - scatter.MarkerSize = 4; - scatter.Color = ScottPlot.Colors.Green; + scatter.MarkerSize = 5; + scatter.Color = TabHelpers.ChartColors[1]; scatter.LegendText = "Target"; } @@ -1911,8 +1911,8 @@ private void LoadMemoryNodeOOMMemoryChart(IEnumerable (double)d.CommittedKb!.Value / 1024.0).ToArray(); var scatter = MemoryNodeOOMMemoryChart.Plot.Add.Scatter(xs, ys); scatter.LineWidth = 2; - scatter.MarkerSize = 4; - scatter.Color = ScottPlot.Colors.Orange; + scatter.MarkerSize = 5; + scatter.Color = TabHelpers.ChartColors[2]; scatter.LegendText = "Committed"; } @@ -1925,8 +1925,8 @@ private void LoadMemoryNodeOOMMemoryChart(IEnumerable (double)d.TotalPageFileKb!.Value / 1024.0).ToArray(); var scatter = MemoryNodeOOMMemoryChart.Plot.Add.Scatter(xs, ys); scatter.LineWidth = 2; - scatter.MarkerSize = 4; - scatter.Color = ScottPlot.Colors.Purple; + scatter.MarkerSize = 5; + scatter.Color = TabHelpers.ChartColors[4]; scatter.LegendText = "Total Page File"; } @@ -1939,8 +1939,8 @@ private void LoadMemoryNodeOOMMemoryChart(IEnumerable (double)d.AvailablePageFileKb!.Value / 1024.0).ToArray(); var scatter = MemoryNodeOOMMemoryChart.Plot.Add.Scatter(xs, ys); scatter.LineWidth = 2; - scatter.MarkerSize = 4; - scatter.Color = ScottPlot.Colors.Cyan; + scatter.MarkerSize = 5; + scatter.Color = TabHelpers.ChartColors[5]; scatter.LegendText = "Avail Page File"; } @@ -1970,9 +1970,9 @@ private void LoadMemoryNodeOOMMemoryChart(IEnumerable data) { // Indicator colors - var healthyBrush = new System.Windows.Media.SolidColorBrush((System.Windows.Media.Color)System.Windows.Media.ColorConverter.ConvertFromString("#22C55E")); - var warningBrush = new System.Windows.Media.SolidColorBrush((System.Windows.Media.Color)System.Windows.Media.ColorConverter.ConvertFromString("#F59E0B")); - var criticalBrush = new System.Windows.Media.SolidColorBrush((System.Windows.Media.Color)System.Windows.Media.ColorConverter.ConvertFromString("#EF4444")); + var healthyBrush = new System.Windows.Media.SolidColorBrush((System.Windows.Media.Color)System.Windows.Media.ColorConverter.ConvertFromString("#81C784")); + var warningBrush = new System.Windows.Media.SolidColorBrush((System.Windows.Media.Color)System.Windows.Media.ColorConverter.ConvertFromString("#FFD54F")); + var criticalBrush = new System.Windows.Media.SolidColorBrush((System.Windows.Media.Color)System.Windows.Media.ColorConverter.ConvertFromString("#E57373")); var unknownBrush = new System.Windows.Media.SolidColorBrush((System.Windows.Media.Color)System.Windows.Media.ColorConverter.ConvertFromString("#888888")); var dataList = data?.ToList() ?? new List(); diff --git a/Dashboard/Helpers/DateFilterHelper.cs b/Dashboard/Helpers/DateFilterHelper.cs index a49873c5..b9b5e4f4 100644 --- a/Dashboard/Helpers/DateFilterHelper.cs +++ b/Dashboard/Helpers/DateFilterHelper.cs @@ -138,13 +138,13 @@ private static bool TryConvertToDateTime(object value, out DateTime result) switch (expressionLower) { case "today": - return DateTime.Today; + return ServerTimeHelper.ServerNow.Date; case "yesterday": - return DateTime.Today.AddDays(-1); + return ServerTimeHelper.ServerNow.Date.AddDays(-1); case "tomorrow": - return DateTime.Today.AddDays(1); + return ServerTimeHelper.ServerNow.Date.AddDays(1); case "now": - return DateTime.Now; + return ServerTimeHelper.ServerNow; } // "last N hours/days/weeks" expressions @@ -155,13 +155,13 @@ private static bool TryConvertToDateTime(object value, out DateTime result) string unit = lastMatch.Groups[2].Value; if (unit.StartsWith("hour", StringComparison.Ordinal)) - return DateTime.Now.AddHours(-count); + return ServerTimeHelper.ServerNow.AddHours(-count); else if (unit.StartsWith("day", StringComparison.Ordinal)) - return DateTime.Now.AddDays(-count); + return ServerTimeHelper.ServerNow.AddDays(-count); else if (unit.StartsWith("week", StringComparison.Ordinal)) - return DateTime.Now.AddDays(-count * 7); + return ServerTimeHelper.ServerNow.AddDays(-count * 7); else if (unit.StartsWith("month", StringComparison.Ordinal)) - return DateTime.Now.AddMonths(-count); + return ServerTimeHelper.ServerNow.AddMonths(-count); } // Try to parse as absolute date (use original case for proper parsing) diff --git a/Dashboard/Helpers/TabHelpers.cs b/Dashboard/Helpers/TabHelpers.cs index bf01f70b..5a1e3f95 100644 --- a/Dashboard/Helpers/TabHelpers.cs +++ b/Dashboard/Helpers/TabHelpers.cs @@ -28,6 +28,27 @@ namespace PerformanceMonitorDashboard.Helpers /// public static class TabHelpers { + /// + /// Material Design 300-level color palette for chart data series. + /// Soft pastels optimized for dark backgrounds, ordered to map 1:1 + /// with common ScottPlot stock colors (Blue→[0], Green→[1], etc.). + /// + public static readonly ScottPlot.Color[] ChartColors = new[] + { + ScottPlot.Color.FromHex("#4FC3F7"), // [0] Light Blue 300 + ScottPlot.Color.FromHex("#81C784"), // [1] Green 300 + ScottPlot.Color.FromHex("#FFB74D"), // [2] Orange 300 + ScottPlot.Color.FromHex("#E57373"), // [3] Red 300 + ScottPlot.Color.FromHex("#BA68C8"), // [4] Purple 300 + ScottPlot.Color.FromHex("#4DD0E1"), // [5] Cyan 300 + ScottPlot.Color.FromHex("#FFF176"), // [6] Yellow 300 + ScottPlot.Color.FromHex("#F06292"), // [7] Pink 300 + ScottPlot.Color.FromHex("#AED581"), // [8] Light Green 300 + ScottPlot.Color.FromHex("#90A4AE"), // [9] Blue Grey 300 + ScottPlot.Color.FromHex("#A1887F"), // [10] Brown 300 + ScottPlot.Color.FromHex("#7986CB"), // [11] Indigo 300 + }; + /// /// Applies the Darling Data dark theme to a ScottPlot chart. /// diff --git a/Dashboard/Models/ServerListItem.cs b/Dashboard/Models/ServerListItem.cs index aeb9e0fe..eb31fd1a 100644 --- a/Dashboard/Models/ServerListItem.cs +++ b/Dashboard/Models/ServerListItem.cs @@ -79,7 +79,7 @@ public string StatusColor if (!_status.LastChecked.HasValue) return "#888888"; // Gray for not checked - return _status.IsOnline == true ? "#22C55E" : "#EF4444"; // Green or Red + return _status.IsOnline == true ? "#81C784" : "#E57373"; // Green or Red } } diff --git a/Dashboard/ServerTab.xaml.cs b/Dashboard/ServerTab.xaml.cs index 10ee9201..1d54bc03 100644 --- a/Dashboard/ServerTab.xaml.cs +++ b/Dashboard/ServerTab.xaml.cs @@ -1650,8 +1650,8 @@ private void LoadBlockingStatsCharts(List data, int h { var scatter = BlockingStatsBlockingEventsChart.Plot.Add.Scatter(blockingXs, blockingYs); scatter.LineWidth = 2; - scatter.MarkerSize = 4; - scatter.Color = ScottPlot.Colors.Blue; + scatter.MarkerSize = 5; + scatter.Color = TabHelpers.ChartColors[0]; } else { @@ -1678,8 +1678,8 @@ private void LoadBlockingStatsCharts(List data, int h { var scatter = BlockingStatsDurationChart.Plot.Add.Scatter(durationXs, durationYs); scatter.LineWidth = 2; - scatter.MarkerSize = 4; - scatter.Color = ScottPlot.Colors.Orange; + scatter.MarkerSize = 5; + scatter.Color = TabHelpers.ChartColors[2]; } else { @@ -1706,8 +1706,8 @@ private void LoadBlockingStatsCharts(List data, int h { var scatter = BlockingStatsDeadlocksChart.Plot.Add.Scatter(deadlockXs, deadlockYs); scatter.LineWidth = 2; - scatter.MarkerSize = 4; - scatter.Color = ScottPlot.Colors.Red; + scatter.MarkerSize = 5; + scatter.Color = TabHelpers.ChartColors[3]; } else { @@ -1734,8 +1734,8 @@ private void LoadBlockingStatsCharts(List data, int h { var scatter = BlockingStatsDeadlockWaitTimeChart.Plot.Add.Scatter(deadlockWaitXs, deadlockWaitYs); scatter.LineWidth = 2; - scatter.MarkerSize = 4; - scatter.Color = ScottPlot.Colors.Purple; + scatter.MarkerSize = 5; + scatter.Color = TabHelpers.ChartColors[4]; } else { @@ -1772,9 +1772,7 @@ private void LoadLockWaitStatsChart(List data, int hoursBack, // Get all unique time points across all wait types for gap filling // Group by wait type and plot each as a separate series var waitTypes = data.Select(d => d.WaitType).Distinct().OrderBy(w => w).ToList(); - var colors = new[] { ScottPlot.Colors.Blue, ScottPlot.Colors.Green, ScottPlot.Colors.Orange, - ScottPlot.Colors.Red, ScottPlot.Colors.Purple, ScottPlot.Colors.Cyan, - ScottPlot.Colors.Magenta, ScottPlot.Colors.Yellow }; + var colors = TabHelpers.ChartColors; int colorIndex = 0; foreach (var waitType in waitTypes) @@ -1789,7 +1787,7 @@ private void LoadLockWaitStatsChart(List data, int hoursBack, var scatter = LockWaitStatsChart.Plot.Add.Scatter(xs, ys); scatter.LineWidth = 2; - scatter.MarkerSize = 0; + scatter.MarkerSize = 5; scatter.Color = colors[colorIndex % colors.Length]; scatter.LegendText = waitType.Replace("LCK_M_", "").Replace("LCK_", ""); colorIndex++; @@ -2216,8 +2214,8 @@ private void LoadResourceOverviewCpuChart(IEnumerable cpuData, int { var scatter = ResourceOverviewCpuChart.Plot.Add.Scatter(xs, ys); scatter.LineWidth = 2; - scatter.MarkerSize = 0; - scatter.Color = ScottPlot.Colors.Blue; + scatter.MarkerSize = 5; + scatter.Color = TabHelpers.ChartColors[0]; scatter.LegendText = "SQL CPU %"; _legendPanels[ResourceOverviewCpuChart] = ResourceOverviewCpuChart.Plot.ShowLegend(ScottPlot.Edge.Bottom); @@ -2271,14 +2269,14 @@ private void LoadResourceOverviewMemoryChart(IEnumerable memory { var bufferScatter = ResourceOverviewMemoryChart.Plot.Add.Scatter(bufferXs, bufferYs); bufferScatter.LineWidth = 2; - bufferScatter.MarkerSize = 0; - bufferScatter.Color = ScottPlot.Colors.Purple; + bufferScatter.MarkerSize = 5; + bufferScatter.Color = TabHelpers.ChartColors[4]; bufferScatter.LegendText = "Buffer Pool"; var grantsScatter = ResourceOverviewMemoryChart.Plot.Add.Scatter(grantsXs, grantsYs); grantsScatter.LineWidth = 2; - grantsScatter.MarkerSize = 0; - grantsScatter.Color = ScottPlot.Colors.Orange; + grantsScatter.MarkerSize = 5; + grantsScatter.Color = TabHelpers.ChartColors[2]; grantsScatter.LegendText = "Memory Grants"; _legendPanels[ResourceOverviewMemoryChart] = ResourceOverviewMemoryChart.Plot.ShowLegend(ScottPlot.Edge.Bottom); @@ -2347,14 +2345,14 @@ private void LoadResourceOverviewIoChart(IEnumerable ioData, in { var readScatter = ResourceOverviewIoChart.Plot.Add.Scatter(readXs, readYs); readScatter.LineWidth = 2; - readScatter.MarkerSize = 0; - readScatter.Color = ScottPlot.Colors.Green; + readScatter.MarkerSize = 5; + readScatter.Color = TabHelpers.ChartColors[1]; readScatter.LegendText = "Read ms"; var writeScatter = ResourceOverviewIoChart.Plot.Add.Scatter(writeXs, writeYs); writeScatter.LineWidth = 2; - writeScatter.MarkerSize = 0; - writeScatter.Color = ScottPlot.Colors.Orange; + writeScatter.MarkerSize = 5; + writeScatter.Color = TabHelpers.ChartColors[2]; writeScatter.LegendText = "Write ms"; _legendPanels[ResourceOverviewIoChart] = ResourceOverviewIoChart.Plot.ShowLegend(ScottPlot.Edge.Bottom); @@ -2407,7 +2405,7 @@ private void LoadResourceOverviewWaitChart(IEnumerable waitD .Select(x => x.WaitType) .ToList(); - var colors = new[] { ScottPlot.Colors.Red, ScottPlot.Colors.Blue, ScottPlot.Colors.Green, ScottPlot.Colors.Orange, ScottPlot.Colors.Purple }; + var colors = TabHelpers.ChartColors; int colorIndex = 0; foreach (var waitType in topWaitTypes) @@ -2422,7 +2420,7 @@ private void LoadResourceOverviewWaitChart(IEnumerable waitD var scatter = ResourceOverviewWaitChart.Plot.Add.Scatter(xs, ys); scatter.LineWidth = 2; - scatter.MarkerSize = 0; + scatter.MarkerSize = 5; scatter.Color = colors[colorIndex % colors.Length]; scatter.LegendText = waitType.Length > 15 ? waitType.Substring(0, 15) + "..." : waitType; colorIndex++; diff --git a/Dashboard/Services/DatabaseService.QueryPerformance.cs b/Dashboard/Services/DatabaseService.QueryPerformance.cs index f4dc4b72..26cd0d17 100644 --- a/Dashboard/Services/DatabaseService.QueryPerformance.cs +++ b/Dashboard/Services/DatabaseService.QueryPerformance.cs @@ -568,7 +568,7 @@ SELECT TOP (500) FROM report.query_snapshots AS qs WHERE qs.collection_time >= @from_date AND qs.collection_time <= @to_date - AND qs.sql_text NOT LIKE N'WAITFOR%' + AND CONVERT(nvarchar(max), qs.sql_text) NOT LIKE N'WAITFOR%' ORDER BY qs.collection_time DESC, qs.session_id;"; @@ -610,12 +610,12 @@ SELECT TOP (500) /* query_plan fetched on-demand via GetQuerySnapshotPlanAsync */ FROM report.query_snapshots AS qs WHERE qs.collection_time >= DATEADD(HOUR, @hours_back, SYSDATETIME()) - AND qs.sql_text NOT LIKE N'WAITFOR%' + AND CONVERT(nvarchar(max), qs.sql_text) NOT LIKE N'WAITFOR%' ORDER BY qs.collection_time DESC, qs.session_id;"; } - + using var command = new SqlCommand(query, connection); command.CommandTimeout = 120; command.Parameters.Add(new SqlParameter("@hours_back", SqlDbType.Int) { Value = -hoursBack }); @@ -1208,8 +1208,8 @@ ORDER BY } else { - startDate = DateTime.Now.AddHours(-hoursBack); - endDate = DateTime.Now; + startDate = Helpers.ServerTimeHelper.ServerNow.AddHours(-hoursBack); + endDate = Helpers.ServerTimeHelper.ServerNow; } command.Parameters.Add(new SqlParameter("@start_date", SqlDbType.DateTime2) { Value = startDate }); diff --git a/Dashboard/Themes/DarkTheme.xaml b/Dashboard/Themes/DarkTheme.xaml index 3e873343..686dcef6 100644 --- a/Dashboard/Themes/DarkTheme.xaml +++ b/Dashboard/Themes/DarkTheme.xaml @@ -27,10 +27,10 @@ #2a2d35 #33363e - - #4CAF50 - #FFC107 - #F44336 + + #81C784 + #FFD54F + #E57373 #2eaef1 @@ -540,6 +540,8 @@ + + @@ -571,7 +573,7 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -