diff --git a/CHANGELOG.md b/CHANGELOG.md
index e0ebd159..eee63d09 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,48 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+## [2.7.0] - 2026-04-13
+
+### Added
+
+- **Host OS column** in Server Inventory for both Dashboard and Lite ([#748], [#823])
+- **Offline community script support** via `community/` directory for user-contributed scripts ([#814], [#822])
+- **MultiSubnetFailover connection option** in Dashboard and Lite for Always On availability groups ([#813], [#821])
+
+### Changed
+
+- **PlanAnalyzer and ShowPlanParser** synced from PerformanceStudio with latest improvements ([#816])
+- **MCP query tools** optimized for large databases ([#826])
+- **Add Server dialog UX** improved with inline connection status and full-height window
+- **"CPUs" renamed to "Logical CPUs"** for clarity in Lite ([#825])
+
+### Fixed
+
+- **Dashboard auto-refresh stalling under load** — replaced DispatcherTimer with async Task.Delay loop to prevent priority starvation during heavy chart rendering ([#833], [#834])
+- **Lite auto-refresh silently skipping** every tick ([#824])
+- **Deadlock count not resetting** between collections ([#803], [#820])
+- **Upgrade filter skipping patch versions** during version comparison ([#817], [#819])
+- **Upgrade script executing against master** instead of PerformanceMonitor database ([#828])
+- **Duplicate release builds** triggering on both created and published events
+
+[#748]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/748
+[#803]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/803
+[#813]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/813
+[#814]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/814
+[#816]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/816
+[#817]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/817
+[#819]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/819
+[#820]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/820
+[#821]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/821
+[#822]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/822
+[#823]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/823
+[#824]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/824
+[#825]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/825
+[#826]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/826
+[#828]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/828
+[#833]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/833
+[#834]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/834
+
## [2.6.0] - 2026-04-08
### Added
diff --git a/Dashboard/Dashboard.csproj b/Dashboard/Dashboard.csproj
index 8fc70396..b7a0103f 100644
--- a/Dashboard/Dashboard.csproj
+++ b/Dashboard/Dashboard.csproj
@@ -7,10 +7,10 @@
PerformanceMonitorDashboard.Program
PerformanceMonitorDashboard
SQL Server Performance Monitor Dashboard
- 2.6.0
- 2.6.0.0
- 2.6.0.0
- 2.6.0
+ 2.7.0
+ 2.7.0.0
+ 2.7.0.0
+ 2.7.0
Darling Data, LLC
Copyright © 2026 Darling Data, LLC
EDD.ico
diff --git a/Dashboard/ServerTab.xaml.cs b/Dashboard/ServerTab.xaml.cs
index 42ab6a1d..a67daf89 100644
--- a/Dashboard/ServerTab.xaml.cs
+++ b/Dashboard/ServerTab.xaml.cs
@@ -5,6 +5,7 @@
using System.Windows.Data;
using System.Text;
using System.Collections.Generic;
+using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
@@ -46,6 +47,7 @@ public partial class ServerTab : UserControl
private readonly UserPreferencesService _preferencesService;
private DispatcherTimer? _autoRefreshTimer;
+ private CancellationTokenSource? _autoRefreshCts;
private bool _isRefreshing;
private DateTime _refreshStartedUtc;
private bool _suppressPickerUpdates;
@@ -127,7 +129,6 @@ public ServerTab(ServerConnection serverConnection, int utcOffsetMinutes = 0)
InitializeDefaultTimeRanges();
SetupChartContextMenus();
- SetupAutoRefresh();
SetupSubTabContextMenus();
BlockingSlicer.RangeChanged += OnBlockingSlicerChanged;
@@ -343,42 +344,62 @@ private void SetupAutoRefresh()
if (prefs.AutoRefreshEnabled)
{
- _autoRefreshTimer = new DispatcherTimer
- {
- Interval = TimeSpan.FromSeconds(prefs.AutoRefreshIntervalSeconds)
- };
- _autoRefreshTimer.Tick += async (s, e) =>
+ StartAutoRefreshLoop(prefs.AutoRefreshIntervalSeconds);
+ AutoRefreshToggle.IsChecked = true;
+ AutoRefreshToggle.Content = $"Auto-Refresh: {prefs.AutoRefreshIntervalSeconds}s";
+ }
+ else
+ {
+ AutoRefreshToggle.IsChecked = false;
+ AutoRefreshToggle.Content = "Auto-Refresh: Off";
+ }
+ }
+
+ ///
+ /// Async loop that replaces DispatcherTimer for auto-refresh. Task.Delay is not
+ /// subject to Dispatcher priority starvation under heavy UI load (chart rendering,
+ /// data binding) that can indefinitely defer Background-priority DispatcherTimer ticks.
+ ///
+ private async void StartAutoRefreshLoop(int intervalSeconds)
+ {
+ if (_autoRefreshCts != null && !_autoRefreshCts.IsCancellationRequested)
+ return;
+
+ _autoRefreshCts?.Cancel();
+ var cts = new CancellationTokenSource();
+ _autoRefreshCts = cts;
+
+ try
+ {
+ while (!cts.Token.IsCancellationRequested)
{
- _autoRefreshTimer?.Stop();
+ await Task.Delay(TimeSpan.FromSeconds(intervalSeconds), cts.Token);
+ if (cts.Token.IsCancellationRequested) break;
+
try
{
+ var sw = System.Diagnostics.Stopwatch.StartNew();
await RefreshVisibleTabAsync();
StatusText.Text = "Ready";
FooterText.Text = $"Last refresh: {DateTime.Now:yyyy-MM-dd HH:mm:ss} | Server: {_serverConnection.DisplayName}";
+ Logger.Info($"Auto-refresh completed in {sw.ElapsedMilliseconds}ms for {_serverConnection.DisplayName}");
}
- catch (Exception ex)
+ catch (Exception ex) when (ex is not OperationCanceledException)
{
Logger.Error($"Auto-refresh error: {ex.Message}", ex);
StatusText.Text = "Auto-refresh error";
}
- finally
- {
- _autoRefreshTimer?.Start();
- }
- };
- _autoRefreshTimer.Start();
- AutoRefreshToggle.IsChecked = true;
- AutoRefreshToggle.Content = $"Auto-Refresh: {prefs.AutoRefreshIntervalSeconds}s";
+ }
}
- else
+ catch (OperationCanceledException)
{
- AutoRefreshToggle.IsChecked = false;
- AutoRefreshToggle.Content = "Auto-Refresh: Off";
+ // Normal shutdown
}
}
private void ServerTab_Unloaded(object sender, RoutedEventArgs e)
{
+ _autoRefreshCts?.Cancel();
_autoRefreshTimer?.Stop();
_autoRefreshTimer = null;
@@ -448,7 +469,9 @@ private void OnThemeChanged(string _)
public void RefreshAutoRefreshSettings()
{
- // Stop existing timer
+ // Stop existing loop and timer
+ _autoRefreshCts?.Cancel();
+ _autoRefreshCts = null;
_autoRefreshTimer?.Stop();
_autoRefreshTimer = null;
@@ -457,30 +480,7 @@ public void RefreshAutoRefreshSettings()
if (prefs.AutoRefreshEnabled)
{
- _autoRefreshTimer = new DispatcherTimer
- {
- Interval = TimeSpan.FromSeconds(prefs.AutoRefreshIntervalSeconds)
- };
- _autoRefreshTimer.Tick += async (s, e) =>
- {
- _autoRefreshTimer?.Stop();
- try
- {
- await RefreshVisibleTabAsync();
- StatusText.Text = "Ready";
- FooterText.Text = $"Last refresh: {DateTime.Now:yyyy-MM-dd HH:mm:ss} | Server: {_serverConnection.DisplayName}";
- }
- catch (Exception ex)
- {
- Logger.Error($"Auto-refresh error: {ex.Message}", ex);
- StatusText.Text = "Auto-refresh error";
- }
- finally
- {
- _autoRefreshTimer?.Start();
- }
- };
- _autoRefreshTimer.Start();
+ StartAutoRefreshLoop(prefs.AutoRefreshIntervalSeconds);
AutoRefreshToggle.IsChecked = true;
AutoRefreshToggle.Content = $"Auto-Refresh: {prefs.AutoRefreshIntervalSeconds}s";
}
@@ -506,30 +506,7 @@ private void AutoRefreshToggle_Click(object sender, RoutedEventArgs e)
prefs.AutoRefreshEnabled = true;
_preferencesService.SavePreferences(prefs);
- _autoRefreshTimer = new DispatcherTimer
- {
- Interval = TimeSpan.FromSeconds(prefs.AutoRefreshIntervalSeconds)
- };
- _autoRefreshTimer.Tick += async (s, args) =>
- {
- _autoRefreshTimer?.Stop();
- try
- {
- await RefreshVisibleTabAsync();
- StatusText.Text = "Ready";
- FooterText.Text = $"Last refresh: {DateTime.Now:yyyy-MM-dd HH:mm:ss} | Server: {_serverConnection.DisplayName}";
- }
- catch (Exception ex)
- {
- Logger.Error($"Auto-refresh error: {ex.Message}", ex);
- StatusText.Text = "Auto-refresh error";
- }
- finally
- {
- _autoRefreshTimer?.Start();
- }
- };
- _autoRefreshTimer.Start();
+ StartAutoRefreshLoop(prefs.AutoRefreshIntervalSeconds);
AutoRefreshToggle.Content = $"Auto-Refresh: {prefs.AutoRefreshIntervalSeconds}s";
}
else
@@ -538,8 +515,7 @@ private void AutoRefreshToggle_Click(object sender, RoutedEventArgs e)
prefs.AutoRefreshEnabled = false;
_preferencesService.SavePreferences(prefs);
- _autoRefreshTimer?.Stop();
- _autoRefreshTimer = null;
+ _autoRefreshCts?.Cancel();
AutoRefreshToggle.Content = "Auto-Refresh: Off";
}
}
@@ -643,6 +619,7 @@ private async void ServerTab_Loaded(object sender, RoutedEventArgs e)
DefaultTraceTab.SetTimeRange(_globalHoursBack, _globalFromDate, _globalToDate);
await LoadDataAsync();
+ SetupAutoRefresh();
}
catch (Exception ex)
{
diff --git a/Installer.Core/Installer.Core.csproj b/Installer.Core/Installer.Core.csproj
index fbf0387b..bb5f1f32 100644
--- a/Installer.Core/Installer.Core.csproj
+++ b/Installer.Core/Installer.Core.csproj
@@ -7,10 +7,10 @@
Installer.Core
Installer.Core
SQL Server Performance Monitor Installer Core
- 2.6.0
- 2.6.0.0
- 2.6.0.0
- 2.6.0
+ 2.7.0
+ 2.7.0.0
+ 2.7.0.0
+ 2.7.0
Darling Data, LLC
Copyright (c) 2026 Darling Data, LLC
true
diff --git a/Installer/PerformanceMonitorInstaller.csproj b/Installer/PerformanceMonitorInstaller.csproj
index 7c3fa1a9..a8293d15 100644
--- a/Installer/PerformanceMonitorInstaller.csproj
+++ b/Installer/PerformanceMonitorInstaller.csproj
@@ -20,10 +20,10 @@
PerformanceMonitorInstaller
SQL Server Performance Monitor Installer
- 2.6.0
- 2.6.0.0
- 2.6.0.0
- 2.6.0
+ 2.7.0
+ 2.7.0.0
+ 2.7.0.0
+ 2.7.0
Darling Data, LLC
Copyright © 2026 Darling Data, LLC
Installation utility for SQL Server Performance Monitor - Supports SQL Server 2016-2025
diff --git a/Lite/PerformanceMonitorLite.csproj b/Lite/PerformanceMonitorLite.csproj
index d87dde0a..784cd81b 100644
--- a/Lite/PerformanceMonitorLite.csproj
+++ b/Lite/PerformanceMonitorLite.csproj
@@ -8,10 +8,10 @@
PerformanceMonitorLite
PerformanceMonitorLite
SQL Server Performance Monitor Lite
- 2.6.0
- 2.6.0.0
- 2.6.0.0
- 2.6.0
+ 2.7.0
+ 2.7.0.0
+ 2.7.0.0
+ 2.7.0
Darling Data, LLC
Copyright © 2026 Darling Data, LLC
Lightweight SQL Server performance monitoring - no installation required on target servers
diff --git a/README.md b/README.md
index 9390c014..19402312 100644
--- a/README.md
+++ b/README.md
@@ -99,7 +99,7 @@ Data starts flowing within 1–5 minutes. That's it. No installation on your ser
**Upgrading from zip?** Click **Import Settings** then **Import Data** in the sidebar and point both at your old Lite folder. Settings imports server connections, alert thresholds, SMTP config, and schedules. Data imports historical DuckDB + Parquet archives. **Auto-update users** (installed via Setup.exe) get updates automatically — no manual import needed.
-**Always On AG?** Enable **ReadOnlyIntent** in the connection settings to route Lite's monitoring queries to a readable secondary, keeping the primary clear.
+**Always On AG?** Enable **ReadOnlyIntent** in the connection settings to route Lite's monitoring queries to a readable secondary, keeping the primary clear. Enable **MultiSubnetFailover** for multi-subnet failover scenarios.
### Lite Collectors
@@ -191,6 +191,8 @@ PerformanceMonitorInstaller.exe YourServerName sa YourPassword --uninstall
The installer automatically tests the connection, checks the SQL Server version (2016+ required), executes SQL scripts, downloads community dependencies, creates SQL Agent jobs, and runs initial data collection. You can also install directly from the Dashboard's Add Server dialog.
+**Air-gapped environments?** Place pre-downloaded community scripts (`sp_WhoIsActive.sql`, `DarlingData.sql`, `Install-All-Scripts.sql`) in a `community/` directory next to the installer. The installer uses local files when present and falls back to GitHub downloads otherwise.
+
### CLI Installer Options
| Option | Description |