Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions Dashboard/AboutWindow.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,12 @@
Check for Updates
</Hyperlink>
</TextBlock>
<TextBlock x:Name="UpdateStatusText" FontSize="11" Foreground="{DynamicResource ForegroundMutedBrush}" Margin="0,3,0,0" Visibility="Collapsed"/>
</StackPanel>

<!-- Credits -->
<!-- Links -->
<StackPanel Grid.Row="4">
<TextBlock Text="Credits" FontWeight="Bold" Margin="0,0,0,5"/>
<TextBlock Text="Created by Erik Darling" FontSize="12"/>
<TextBlock FontSize="12" Margin="0,5,0,0">
<TextBlock FontSize="12">
<Hyperlink x:Name="DarlingDataLink" Click="DarlingDataLink_Click">
www.erikdarling.com
</Hyperlink>
Expand Down
35 changes: 33 additions & 2 deletions Dashboard/AboutWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using System.Diagnostics;
using System.Reflection;
using System.Windows;
using PerformanceMonitorDashboard.Services;

namespace PerformanceMonitorDashboard
{
Expand All @@ -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();
Expand All @@ -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)
Expand Down
24 changes: 24 additions & 0 deletions Dashboard/MainWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
3 changes: 3 additions & 0 deletions Dashboard/Models/UserPreferences.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> SilencedServers { get; set; } = new();
public List<string> SilencedServerTabs { get; set; } = new();
Expand Down
101 changes: 101 additions & 0 deletions Dashboard/Services/UpdateCheckService.cs
Original file line number Diff line number Diff line change
@@ -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<UpdateInfo?> 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}";
}
}
}
6 changes: 6 additions & 0 deletions Lite/App.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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; } = "";
Expand Down Expand Up @@ -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() ?? "";
Expand Down
33 changes: 33 additions & 0 deletions Lite/MainWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,8 @@ private async void MainWindow_Loaded(object sender, RoutedEventArgs e)

await RefreshOverviewAsync();
StatusText.Text = "Ready - Collection active";

_ = CheckForUpdatesOnStartupAsync();
}
catch (Exception ex)
{
Expand All @@ -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
Expand Down
102 changes: 102 additions & 0 deletions Lite/Services/UpdateCheckService.cs
Original file line number Diff line number Diff line change
@@ -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<UpdateInfo?> 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}";
}
}
1 change: 1 addition & 0 deletions Lite/Windows/AboutWindow.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
Check for Updates
</Hyperlink>
</TextBlock>
<TextBlock x:Name="UpdateStatusText" FontSize="11" Foreground="{DynamicResource ForegroundMutedBrush}" Margin="0,0,0,5" Visibility="Collapsed"/>
<TextBlock FontSize="12">
<Hyperlink x:Name="DarlingDataLink" Click="DarlingDataLink_Click">
www.erikdarling.com
Expand Down
Loading