diff --git a/Dashboard/MainWindow.xaml.cs b/Dashboard/MainWindow.xaml.cs index 94aa06d..dfb032d 100644 --- a/Dashboard/MainWindow.xaml.cs +++ b/Dashboard/MainWindow.xaml.cs @@ -886,6 +886,7 @@ private void CloseTab_Click(object sender, RoutedEventArgs e) serverTab.AlertAcknowledged -= handler; _alertAcknowledgedHandlers.Remove(tabId); } + serverTab.CleanupOnClose(); } _openTabs.Remove(tabId); _tabBadges.Remove(tabId); @@ -1092,6 +1093,7 @@ private async void RemoveServer_Click(object sender, RoutedEventArgs e) if (_openTabs.TryGetValue(server.Id, out var tabItem)) { + if (tabItem.Content is ServerTab st) st.CleanupOnClose(); _openTabs.Remove(server.Id); ServerTabControl.Items.Remove(tabItem); } diff --git a/Dashboard/ServerTab.xaml.cs b/Dashboard/ServerTab.xaml.cs index 4f990bf..4ba7629 100644 --- a/Dashboard/ServerTab.xaml.cs +++ b/Dashboard/ServerTab.xaml.cs @@ -377,6 +377,8 @@ private async void StartAutoRefreshLoop(int intervalSeconds) if (cts.Token.IsCancellationRequested) break; if (_isRefreshing) continue; + _isRefreshing = true; + _refreshStartedUtc = DateTime.UtcNow; try { var sw = System.Diagnostics.Stopwatch.StartNew(); @@ -387,7 +389,6 @@ private async void StartAutoRefreshLoop(int intervalSeconds) } catch (OperationCanceledException) when (!cts.Token.IsCancellationRequested) { - // SQL query cancelled or timed out, but our loop CTS is still alive — keep going Logger.Error($"Auto-refresh query cancelled for {_serverConnection.DisplayName}, continuing loop"); } catch (Exception ex) when (ex is not OperationCanceledException) @@ -395,6 +396,10 @@ private async void StartAutoRefreshLoop(int intervalSeconds) Logger.Error($"Auto-refresh error: {ex.Message}", ex); StatusText.Text = "Auto-refresh error"; } + finally + { + _isRefreshing = false; + } } } catch (OperationCanceledException) @@ -405,9 +410,18 @@ private async void StartAutoRefreshLoop(int intervalSeconds) private void ServerTab_Unloaded(object sender, RoutedEventArgs e) { - // Don't cancel auto-refresh on tab switch — WPF fires Unloaded when - // a TabItem is deselected, not just when the control is destroyed. - // The loop is lightweight and should keep ticking in the background. + // WPF fires Unloaded on tab switch, not just destruction. + // Don't tear down state here — the auto-refresh loop and chart + // state must survive tab switches. Cleanup happens when the tab + // is actually removed from the TabControl (via CleanupOnClose). + } + + /// + /// Full cleanup — call when the server tab is permanently removed, not on tab switch. + /// + public void CleanupOnClose() + { + _autoRefreshCts?.Cancel(); _autoRefreshTimer?.Stop(); _autoRefreshTimer = null;