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: 5 additions & 2 deletions Lite/MainWindow.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -108,12 +108,15 @@
<Style TargetType="Ellipse">
<Setter Property="Fill" Value="{DynamicResource ForegroundMutedBrush}"/>
<Style.Triggers>
<DataTrigger Binding="{Binding IsOnline}" Value="True">
<DataTrigger Binding="{Binding DotStatus}" Value="Online">
<Setter Property="Fill" Value="{DynamicResource SuccessBrush}"/>
</DataTrigger>
<DataTrigger Binding="{Binding IsOnline}" Value="False">
<DataTrigger Binding="{Binding DotStatus}" Value="Offline">
<Setter Property="Fill" Value="{DynamicResource ErrorBrush}"/>
</DataTrigger>
<DataTrigger Binding="{Binding DotStatus}" Value="Warning">
<Setter Property="Fill" Value="{DynamicResource WarningBrush}"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Ellipse.Style>
Expand Down
13 changes: 13 additions & 0 deletions Lite/MainWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ public partial class MainWindow : Window
private SystemTrayService? _trayService;
private readonly Dictionary<string, TabItem> _openServerTabs = new();
private readonly Dictionary<string, bool> _previousConnectionStates = new();
private readonly Dictionary<string, bool> _previousCollectorErrorStates = new();
private readonly Dictionary<string, DateTime> _lastCpuAlert = new();
private readonly Dictionary<string, DateTime> _lastBlockingAlert = new();
private readonly Dictionary<string, DateTime> _lastDeadlockAlert = new();
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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);
}
}
Expand Down Expand Up @@ -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))
{
Expand Down Expand Up @@ -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)
Expand Down
24 changes: 24 additions & 0 deletions Lite/Models/ServerConnection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,30 @@ public bool UseWindowsAuth
[JsonIgnore]
public bool? IsOnline { get; set; }

/// <summary>
/// Whether one or more collectors are currently failing for this server.
/// null = not yet determined; true = some collectors have consecutive errors; false = all healthy.
/// </summary>
[JsonIgnore]
public bool? HasCollectorErrors { get; set; }

/// <summary>
/// Computed dot status for the sidebar indicator. One of: "Unknown", "Online", "Warning", "Offline".
/// Drives the Ellipse fill via DataTrigger in MainWindow.xaml.
/// </summary>
[JsonIgnore]
public string DotStatus
{
get
{
if (IsOnline == true)
return HasCollectorErrors == true ? "Warning" : "Online";
if (IsOnline == false)
return "Offline";
return "Unknown"; // null — not yet checked
}
}

/// <summary>
/// Display-only property for showing status in UI.
/// </summary>
Expand Down
19 changes: 17 additions & 2 deletions Lite/Services/LocalDataService.Overview.cs
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,8 @@ public class ServerSummaryItem
public string ServerName { get; set; } = "";
public int ServerId { get; set; }
public bool? IsOnline { get; set; }
/// <summary>True when the server is reachable but one or more collectors have consecutive errors.</summary>
public bool HasCollectorErrors { get; set; }
public double? CpuPercent { get; set; }
public double? MemoryMb { get; set; }
public int BlockingCount { get; set; }
Expand All @@ -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 */
Expand All @@ -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)
Expand Down
6 changes: 6 additions & 0 deletions Lite/Services/RemoteCollectorService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,12 @@ public RemoteCollectorService(
/// </summary>
public Task CheckpointAsync() => _duckDb.CheckpointAsync();

/// <summary>
/// Gets a summary of collector health for a specific server connection.
/// </summary>
public CollectorHealthSummary GetHealthSummary(ServerConnection server)
=> GetHealthSummary(GetServerId(server));

/// <summary>
/// Gets a summary of collector health. When serverId is provided, filters to that server only.
/// </summary>
Expand Down
82 changes: 42 additions & 40 deletions Lite/Services/ServerManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -330,50 +330,52 @@ public async Task<ServerConnectionStatus> 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);
}
Expand Down
Loading