diff --git a/Lite/MainWindow.xaml b/Lite/MainWindow.xaml
index 4db4f1b8..01384324 100644
--- a/Lite/MainWindow.xaml
+++ b/Lite/MainWindow.xaml
@@ -108,12 +108,15 @@
diff --git a/Lite/MainWindow.xaml.cs b/Lite/MainWindow.xaml.cs
index 0c8427d6..b247b241 100644
--- a/Lite/MainWindow.xaml.cs
+++ b/Lite/MainWindow.xaml.cs
@@ -35,6 +35,7 @@ public partial class MainWindow : Window
private SystemTrayService? _trayService;
private readonly Dictionary _openServerTabs = new();
private readonly Dictionary _previousConnectionStates = new();
+ private readonly Dictionary _previousCollectorErrorStates = new();
private readonly Dictionary _lastCpuAlert = new();
private readonly Dictionary _lastBlockingAlert = new();
private readonly Dictionary _lastDeadlockAlert = new();
@@ -255,6 +256,9 @@ private void RefreshServerList()
foreach (var server in servers)
{
server.IsOnline = _serverManager.GetConnectionStatus(server.Id).IsOnline;
+ server.HasCollectorErrors = _collectorService != null
+ && server.IsOnline == true
+ && _collectorService.GetHealthSummary(server).ErroringCollectors > 0;
}
ServerListView.ItemsSource = servers;
@@ -380,6 +384,8 @@ private async Task RefreshOverviewAsync()
summary.ServerName = server.ServerName;
var connStatus = _serverManager.GetConnectionStatus(server.Id);
summary.IsOnline = connStatus.IsOnline;
+ if (_collectorService != null && connStatus.IsOnline == true)
+ summary.HasCollectorErrors = _collectorService.GetHealthSummary(server).ErroringCollectors > 0;
summaries.Add(summary);
}
}
@@ -898,6 +904,9 @@ private void CheckConnectionsAndNotify()
if (status?.IsOnline == null) continue;
bool isOnline = status.IsOnline == true;
+ bool hasErrors = _collectorService != null && isOnline
+ && _collectorService.GetHealthSummary(server).ErroringCollectors > 0;
+ server.HasCollectorErrors = hasErrors;
if (_previousConnectionStates.TryGetValue(server.Id, out var wasOnline))
{
@@ -930,7 +939,11 @@ private void CheckConnectionsAndNotify()
needsRefresh = true;
}
+ if (_previousCollectorErrorStates.TryGetValue(server.Id, out var prevHasErrors) && prevHasErrors != hasErrors)
+ needsRefresh = true;
+
_previousConnectionStates[server.Id] = isOnline;
+ _previousCollectorErrorStates[server.Id] = hasErrors;
}
if (needsRefresh)
diff --git a/Lite/Models/ServerConnection.cs b/Lite/Models/ServerConnection.cs
index 610b2b23..b9211dec 100644
--- a/Lite/Models/ServerConnection.cs
+++ b/Lite/Models/ServerConnection.cs
@@ -88,6 +88,30 @@ public bool UseWindowsAuth
[JsonIgnore]
public bool? IsOnline { get; set; }
+ ///
+ /// Whether one or more collectors are currently failing for this server.
+ /// null = not yet determined; true = some collectors have consecutive errors; false = all healthy.
+ ///
+ [JsonIgnore]
+ public bool? HasCollectorErrors { get; set; }
+
+ ///
+ /// Computed dot status for the sidebar indicator. One of: "Unknown", "Online", "Warning", "Offline".
+ /// Drives the Ellipse fill via DataTrigger in MainWindow.xaml.
+ ///
+ [JsonIgnore]
+ public string DotStatus
+ {
+ get
+ {
+ if (IsOnline == true)
+ return HasCollectorErrors == true ? "Warning" : "Online";
+ if (IsOnline == false)
+ return "Offline";
+ return "Unknown"; // null — not yet checked
+ }
+ }
+
///
/// Display-only property for showing status in UI.
///
diff --git a/Lite/Services/LocalDataService.Overview.cs b/Lite/Services/LocalDataService.Overview.cs
index b86f6c4a..eee8a893 100644
--- a/Lite/Services/LocalDataService.Overview.cs
+++ b/Lite/Services/LocalDataService.Overview.cs
@@ -126,6 +126,8 @@ public class ServerSummaryItem
public string ServerName { get; set; } = "";
public int ServerId { get; set; }
public bool? IsOnline { get; set; }
+ /// True when the server is reachable but one or more collectors have consecutive errors.
+ public bool HasCollectorErrors { get; set; }
public double? CpuPercent { get; set; }
public double? MemoryMb { get; set; }
public int BlockingCount { get; set; }
@@ -139,8 +141,20 @@ public class ServerSummaryItem
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 => "#81C784", false => "#E57373", _ => "#888888" });
+ public string StatusDisplay => IsOnline switch
+ {
+ true when HasCollectorErrors => "Warning",
+ true => "Online",
+ false => "Offline",
+ _ => "Unknown"
+ };
+ public SolidColorBrush StatusBrush => MakeBrush(IsOnline switch
+ {
+ true when HasCollectorErrors => "#FFD54F", // amber — connected but collectors failing
+ true => "#81C784",
+ false => "#E57373",
+ _ => "#888888"
+ });
public bool IsOffline => IsOnline == false;
/* Color coding */
@@ -152,6 +166,7 @@ public class ServerSummaryItem
DeadlockCount > 0 ? "#E57373" :
BlockingCount > 0 ? "#FFB74D" :
CpuPercent >= 80 ? "#FFB74D" :
+ HasCollectorErrors ? "#FFD54F" : // amber border when collectors are failing
"#2a2d35");
private static SolidColorBrush MakeBrush(string hex)
diff --git a/Lite/Services/RemoteCollectorService.cs b/Lite/Services/RemoteCollectorService.cs
index 10eb30c2..fc9fe77f 100644
--- a/Lite/Services/RemoteCollectorService.cs
+++ b/Lite/Services/RemoteCollectorService.cs
@@ -126,6 +126,12 @@ public RemoteCollectorService(
///
public Task CheckpointAsync() => _duckDb.CheckpointAsync();
+ ///
+ /// Gets a summary of collector health for a specific server connection.
+ ///
+ public CollectorHealthSummary GetHealthSummary(ServerConnection server)
+ => GetHealthSummary(GetServerId(server));
+
///
/// Gets a summary of collector health. When serverId is provided, filters to that server only.
///
diff --git a/Lite/Services/ServerManager.cs b/Lite/Services/ServerManager.cs
index a007c2bb..bc738a29 100644
--- a/Lite/Services/ServerManager.cs
+++ b/Lite/Services/ServerManager.cs
@@ -330,50 +330,52 @@ public async Task CheckConnectionAsync(string serverId,
using var connection = new SqlConnection(builder.ConnectionString);
await connection.OpenAsync();
- // Query server start time, version, and UTC offset to verify connectivity
- using var command = new SqlCommand(@"
- SELECT
- sqlserver_start_time,
- @@VERSION AS sql_version,
- CONVERT(integer, SERVERPROPERTY('ProductMajorVersion')) AS major_version,
- DATEDIFF(MINUTE, GETUTCDATE(), GETDATE()) AS utc_offset_minutes,
- CONVERT(integer, SERVERPROPERTY('EngineEdition')) AS engine_edition,
- CASE WHEN DB_ID('rdsadmin') IS NOT NULL THEN 1 ELSE 0 END AS is_aws_rds
- FROM sys.dm_os_sys_info", connection);
- command.CommandTimeout = ConnectionCheckTimeoutSeconds;
-
- using var reader = await command.ExecuteReaderAsync();
- if (await reader.ReadAsync())
+ // Connection succeeded — server is reachable regardless of DMV permissions below.
+ status.IsOnline = true;
+ status.ErrorMessage = null;
+ status.UserCancelledMfa = false; // Clear cancellation flag on successful connection
+
+ // Query server metadata (version, start time, UTC offset).
+ // Wrapped in its own try/catch: a permissions failure on sys.dm_os_sys_info
+ // must NOT flip IsOnline back to false — the login itself worked.
+ try
{
- status.IsOnline = true;
- status.ErrorMessage = null;
- status.UserCancelledMfa = false; // Clear cancellation flag on successful connection
-
- if (!reader.IsDBNull(0))
- {
- status.ServerStartTime = reader.GetDateTime(0);
- }
- if (!reader.IsDBNull(1))
- {
- status.SqlServerVersion = reader.GetString(1);
- }
- if (!reader.IsDBNull(2))
- {
- status.SqlMajorVersion = Convert.ToInt32(reader.GetValue(2));
- }
- if (!reader.IsDBNull(3))
- {
- status.UtcOffsetMinutes = Convert.ToInt32(reader.GetValue(3));
- }
- if (!reader.IsDBNull(4))
+ using var command = new SqlCommand(@"
+ SELECT
+ sqlserver_start_time,
+ @@VERSION AS sql_version,
+ CONVERT(integer, SERVERPROPERTY('ProductMajorVersion')) AS major_version,
+ DATEDIFF(MINUTE, GETUTCDATE(), GETDATE()) AS utc_offset_minutes,
+ CONVERT(integer, SERVERPROPERTY('EngineEdition')) AS engine_edition,
+ CASE WHEN DB_ID('rdsadmin') IS NOT NULL THEN 1 ELSE 0 END AS is_aws_rds
+ FROM sys.dm_os_sys_info", connection);
+ command.CommandTimeout = ConnectionCheckTimeoutSeconds;
+
+ using var reader = await command.ExecuteReaderAsync();
+ if (await reader.ReadAsync())
{
- status.SqlEngineEdition = Convert.ToInt32(reader.GetValue(4));
- }
- if (!reader.IsDBNull(5))
- {
- status.IsAwsRds = Convert.ToInt32(reader.GetValue(5)) == 1;
+ if (!reader.IsDBNull(0))
+ status.ServerStartTime = reader.GetDateTime(0);
+ if (!reader.IsDBNull(1))
+ status.SqlServerVersion = reader.GetString(1);
+ if (!reader.IsDBNull(2))
+ status.SqlMajorVersion = Convert.ToInt32(reader.GetValue(2));
+ if (!reader.IsDBNull(3))
+ status.UtcOffsetMinutes = Convert.ToInt32(reader.GetValue(3));
+ if (!reader.IsDBNull(4))
+ status.SqlEngineEdition = Convert.ToInt32(reader.GetValue(4));
+ if (!reader.IsDBNull(5))
+ status.IsAwsRds = Convert.ToInt32(reader.GetValue(5)) == 1;
}
}
+ catch (SqlException metaEx)
+ {
+ // Metadata query failed (e.g. no VIEW SERVER STATE permission) but the
+ // server IS reachable — keep IsOnline = true, just record the warning.
+ status.ErrorMessage = $"Connected, but metadata query failed: {metaEx.Message}";
+ _logger?.LogWarning("Metadata query failed for server '{DisplayName}' (server is still online): {Message}",
+ server.DisplayName, metaEx.Message);
+ }
_logger?.LogDebug("Connectivity check passed for server '{DisplayName}'", server.DisplayName);
}