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)