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
6 changes: 6 additions & 0 deletions Dashboard/AddServerDialog.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,12 @@
ToolTip="When checked, accepts any server certificate without validation.
Uncheck for stricter security (requires valid certificate)."
Margin="0,0,0,6"/>

<!-- Read-Only Intent -->
<CheckBox x:Name="ReadOnlyIntentCheckBox"
Content="Read-only intent (for AG listeners and readable replicas)"
Foreground="{DynamicResource ForegroundBrush}" Margin="0,0,0,6"
ToolTip="Sets ApplicationIntent=ReadOnly. Required when connecting via an AG listener or Azure failover group endpoint to route to a readable secondary."/>

<!-- Is Favorite -->
<CheckBox x:Name="IsFavoriteCheckBox" Content="Mark as favorite (appears at top of list)"
Foreground="{DynamicResource ForegroundBrush}" Margin="0,0,0,6"/>
Expand Down
8 changes: 7 additions & 1 deletion Dashboard/AddServerDialog.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ public AddServerDialog(ServerConnection existingServer)
_ => 0 // Optional
};
TrustServerCertificateCheckBox.IsChecked = existingServer.TrustServerCertificate;
ReadOnlyIntentCheckBox.IsChecked = existingServer.ReadOnlyIntent;

if (existingServer.AuthenticationType == AuthenticationTypes.EntraMFA)
{
Expand Down Expand Up @@ -125,7 +126,10 @@ private SqlConnectionStringBuilder BuildConnectionBuilder()
ApplicationName = "PerformanceMonitorDashboard",
ConnectTimeout = 10,
TrustServerCertificate = TrustServerCertificateCheckBox.IsChecked == true,
Encrypt = ParseEncryptOption(GetSelectedEncryptMode())
Encrypt = ParseEncryptOption(GetSelectedEncryptMode()),
ApplicationIntent = ReadOnlyIntentCheckBox.IsChecked == true
? ApplicationIntent.ReadOnly
: ApplicationIntent.ReadWrite
};

if (WindowsAuthRadio.IsChecked == true)
Expand Down Expand Up @@ -329,6 +333,7 @@ private async void Save_Click(object sender, RoutedEventArgs e)
ServerConnection.IsFavorite = IsFavoriteCheckBox.IsChecked == true;
ServerConnection.EncryptMode = GetSelectedEncryptMode();
ServerConnection.TrustServerCertificate = TrustServerCertificateCheckBox.IsChecked == true;
ServerConnection.ReadOnlyIntent = ReadOnlyIntentCheckBox.IsChecked == true;
if (decimal.TryParse(MonthlyCostTextBox.Text, System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture, out var editCost) && editCost >= 0)
ServerConnection.MonthlyCostUsd = editCost;
}
Expand All @@ -349,6 +354,7 @@ private async void Save_Click(object sender, RoutedEventArgs e)
LastConnected = DateTime.Now,
EncryptMode = GetSelectedEncryptMode(),
TrustServerCertificate = TrustServerCertificateCheckBox.IsChecked == true,
ReadOnlyIntent = ReadOnlyIntentCheckBox.IsChecked == true,
MonthlyCostUsd = monthlyCost
};
}
Expand Down
22 changes: 11 additions & 11 deletions Dashboard/MainWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -558,7 +558,7 @@ private async Task OpenServerTabAsync(ServerConnection server)
{
var inner = ex.InnerException?.Message ?? ex.Message;
System.Windows.MessageBox.Show(
$"Failed to open server tab for '{server.DisplayName}'.\n\n" +
$"Failed to open server tab for '{server.DisplayNameWithIntent}'.\n\n" +
$"This is usually caused by a missing Visual C++ Redistributable (x64) " +
$"or an OS compatibility issue with the SkiaSharp rendering library.\n\n" +
$"Download the latest VC++ Redistributable from:\n" +
Expand All @@ -571,15 +571,15 @@ private async Task OpenServerTabAsync(ServerConnection server)
}
serverTab.AlertAcknowledged += (_, _) =>
{
_emailAlertService.HideAllAlerts(8760, server.DisplayName);
_emailAlertService.HideAllAlerts(8760, server.DisplayNameWithIntent);
UpdateAlertBadge();
_alertsHistoryContent?.RefreshAlerts();
};

var headerPanel = new StackPanel { Orientation = Orientation.Horizontal };
var headerText = new TextBlock
{
Text = server.DisplayName,
Text = server.ReadOnlyIntent ? $"{server.DisplayName} (RO)" : server.DisplayName,
VerticalAlignment = VerticalAlignment.Center
};
var closeButton = new Button
Expand Down Expand Up @@ -912,7 +912,7 @@ private async void AddServer_Click(object sender, RoutedEventArgs e)
await LoadServerListAsync();

MessageBox.Show(
$"Server '{server.DisplayName}' added successfully!\n\n" +
$"Server '{server.DisplayNameWithIntent}' added successfully!\n\n" +
(server.AuthenticationType == Models.AuthenticationTypes.Windows ? "Using Windows Authentication" : $"Using {server.AuthenticationDisplay} — credentials saved securely to Windows Credential Manager"),
"Server Added",
MessageBoxButton.OK,
Expand Down Expand Up @@ -953,12 +953,12 @@ private async void EditServer_Click(object sender, RoutedEventArgs e)
if (tabItem.Header is StackPanel headerPanel &&
headerPanel.Children[0] is TextBlock headerText)
{
headerText.Text = updatedServer.DisplayName;
headerText.Text = updatedServer.ReadOnlyIntent ? $"{updatedServer.DisplayName} (RO)" : updatedServer.DisplayName;
}
}

MessageBox.Show(
$"Server '{updatedServer.DisplayName}' updated successfully!\n\n" +
$"Server '{updatedServer.DisplayNameWithIntent}' updated successfully!\n\n" +
(updatedServer.AuthenticationType == Models.AuthenticationTypes.Windows ? "Using Windows Authentication" : $"Using {updatedServer.AuthenticationDisplay} — credentials updated securely in Windows Credential Manager"),
"Server Updated",
MessageBoxButton.OK,
Expand All @@ -983,7 +983,7 @@ private async void RemoveServer_Click(object sender, RoutedEventArgs e)
if (ServerListView.SelectedItem is ServerListItem item)
{
var server = item.Server;
var dialog = new RemoveServerDialog(server.DisplayName);
var dialog = new RemoveServerDialog(server.DisplayNameWithIntent);
dialog.Owner = this;

if (dialog.ShowDialog() == true)
Expand All @@ -998,7 +998,7 @@ private async void RemoveServer_Click(object sender, RoutedEventArgs e)
catch (Exception ex)
{
MessageBox.Show(
$"Could not drop the PerformanceMonitor database on '{server.DisplayName}':\n\n{ex.Message}\n\nThe server will still be removed from the Dashboard.",
$"Could not drop the PerformanceMonitor database on '{server.DisplayNameWithIntent}':\n\n{ex.Message}\n\nThe server will still be removed from the Dashboard.",
"Database Drop Failed",
MessageBoxButton.OK,
MessageBoxImage.Warning
Expand All @@ -1020,7 +1020,7 @@ private async void RemoveServer_Click(object sender, RoutedEventArgs e)
await LoadServerListAsync();

MessageBox.Show(
$"Server '{server.DisplayName}' removed successfully!",
$"Server '{server.DisplayNameWithIntent}' removed successfully!",
"Server Removed",
MessageBoxButton.OK,
MessageBoxImage.Information
Expand Down Expand Up @@ -1251,7 +1251,7 @@ private async Task CheckAllServerAlertsAsync()
so the badge delta calculation sees the correct baseline. */
var prevDeadlockCount = _previousDeadlockCounts.TryGetValue(server.Id, out var pdc) ? pdc : 0;

await EvaluateAlertConditionsAsync(server.Id, server.DisplayName, health, databaseService);
await EvaluateAlertConditionsAsync(server.Id, server.DisplayNameWithIntent, health, databaseService);

/* Update tab badges from alert health data.
This ensures badges update even when the NOC view isn't active. */
Expand Down Expand Up @@ -1956,7 +1956,7 @@ private void AcknowledgeServerAlerts_Click(object sender, RoutedEventArgs e)
var server = _serverManager.GetAllServers().FirstOrDefault(s => s.Id == serverId);
if (server != null)
{
_emailAlertService.HideAllAlerts(8760, server.DisplayName);
_emailAlertService.HideAllAlerts(8760, server.DisplayNameWithIntent);
UpdateAlertBadge();
_alertsHistoryContent?.RefreshAlerts();
}
Expand Down
10 changes: 5 additions & 5 deletions Dashboard/ManageServersWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ private void AddServer_Click(object sender, RoutedEventArgs e)
ServersModified = true;

MessageBox.Show(
$"Server '{dialog.ServerConnection.DisplayName}' added successfully!",
$"Server '{dialog.ServerConnection.DisplayNameWithIntent}' added successfully!",
"Server Added",
MessageBoxButton.OK,
MessageBoxImage.Information
Expand Down Expand Up @@ -93,7 +93,7 @@ private void EditSelectedServer()
ServersModified = true;

MessageBox.Show(
$"Server '{dialog.ServerConnection.DisplayName}' updated successfully!",
$"Server '{dialog.ServerConnection.DisplayNameWithIntent}' updated successfully!",
"Server Updated",
MessageBoxButton.OK,
MessageBoxImage.Information
Expand Down Expand Up @@ -145,7 +145,7 @@ private async void RemoveServer_Click(object sender, RoutedEventArgs e)
{
if (ServersDataGrid.SelectedItem is ServerConnection server)
{
var dialog = new RemoveServerDialog(server.DisplayName);
var dialog = new RemoveServerDialog(server.DisplayNameWithIntent);
dialog.Owner = this;

if (dialog.ShowDialog() == true)
Expand All @@ -159,7 +159,7 @@ private async void RemoveServer_Click(object sender, RoutedEventArgs e)
catch (Exception ex)
{
MessageBox.Show(
$"Could not drop the PerformanceMonitor database on '{server.DisplayName}':\n\n{ex.Message}\n\nThe server will still be removed from the Dashboard.",
$"Could not drop the PerformanceMonitor database on '{server.DisplayNameWithIntent}':\n\n{ex.Message}\n\nThe server will still be removed from the Dashboard.",
"Database Drop Failed",
MessageBoxButton.OK,
MessageBoxImage.Warning
Expand All @@ -172,7 +172,7 @@ private async void RemoveServer_Click(object sender, RoutedEventArgs e)
ServersModified = true;

MessageBox.Show(
$"Server '{server.DisplayName}' removed successfully!",
$"Server '{server.DisplayNameWithIntent}' removed successfully!",
"Server Removed",
MessageBoxButton.OK,
MessageBoxImage.Information
Expand Down
5 changes: 3 additions & 2 deletions Dashboard/Mcp/McpDiscoveryTools.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,10 @@ public static string ListServers(ServerManager serverManager)
var lines = new List<string> { $"Monitored servers ({servers.Count}):\n" };
foreach (var s in servers)
{
var roTag = s.ReadOnlyIntent ? " [Read-Only]" : "";
var display = string.IsNullOrEmpty(s.DisplayName) || s.DisplayName == s.ServerName
? s.ServerName
: $"{s.DisplayName} ({s.ServerName})";
? $"{s.ServerName}{roTag}"
: $"{s.DisplayName} ({s.ServerName}){roTag}";

var status = serverManager.GetConnectionStatus(s.Id);
var statusText = status.IsOnline switch
Expand Down
9 changes: 6 additions & 3 deletions Dashboard/Mcp/ServerResolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,12 @@ public static string ListAvailableServers(ServerManager serverManager)
}

var lines = servers.Select(s =>
string.IsNullOrEmpty(s.DisplayName) || s.DisplayName == s.ServerName
? s.ServerName
: $"{s.DisplayName} ({s.ServerName})");
{
var roTag = s.ReadOnlyIntent ? " [Read-Only]" : "";
return string.IsNullOrEmpty(s.DisplayName) || s.DisplayName == s.ServerName
? $"{s.ServerName}{roTag}"
: $"{s.DisplayName} ({s.ServerName}){roTag}";
});

return string.Join("\n", lines);
}
Expand Down
18 changes: 17 additions & 1 deletion Dashboard/Models/ServerConnection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,12 +62,26 @@ public bool UseWindowsAuth
/// </summary>
public bool TrustServerCertificate { get; set; } = false;

/// <summary>
/// When true, sets ApplicationIntent=ReadOnly on the connection string.
/// Required for connecting to AG listener read-only replicas and
/// Azure SQL Business Critical / Managed Instance built-in read replicas.
/// </summary>
public bool ReadOnlyIntent { get; set; } = false;

/// <summary>
/// Monthly cost of this server in USD, used for FinOps cost attribution.
/// Set to 0 to hide cost columns. All FinOps costs are proportional to this budget.
/// </summary>
public decimal MonthlyCostUsd { get; set; } = 0m;

/// <summary>
/// Display name with "(Read-Only)" suffix when ReadOnlyIntent is enabled.
/// Used for alerts, tray notifications, tab headers, and dialog messages.
/// </summary>
[JsonIgnore]
public string DisplayNameWithIntent => ReadOnlyIntent ? $"{DisplayName} (Read-Only)" : DisplayName;

/// <summary>
/// Display-only property for showing authentication type in UI.
/// </summary>
Expand Down Expand Up @@ -105,6 +119,7 @@ public string GetConnectionString(ICredentialService credentialService)
"Strict" => SqlConnectionEncryptOption.Strict,
_ => SqlConnectionEncryptOption.Mandatory
},
ApplicationIntent = ReadOnlyIntent ? ApplicationIntent.ReadOnly : ApplicationIntent.ReadWrite,
Authentication = SqlAuthenticationMethod.ActiveDirectoryInteractive
};

Expand Down Expand Up @@ -135,7 +150,8 @@ public string GetConnectionString(ICredentialService credentialService)
username,
password,
EncryptMode,
TrustServerCertificate
TrustServerCertificate,
ReadOnlyIntent
).ConnectionString;
}

Expand Down
2 changes: 1 addition & 1 deletion Dashboard/Models/ServerHealthStatus.cs
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ public ServerHealthStatus(ServerConnection server)

public ServerConnection Server => _server;
public string ServerId => _server.Id;
public string DisplayName => _server.DisplayName;
public string DisplayName => _server.DisplayNameWithIntent;
public string ServerName => _server.ServerName;

public bool IsLoading
Expand Down
2 changes: 1 addition & 1 deletion Dashboard/Models/ServerListItem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ public ServerConnectionStatus Status

// Convenience properties for binding
public string Id => Server.Id;
public string DisplayName => Server.DisplayName;
public string DisplayName => Server.DisplayNameWithIntent;
public string ServerName => Server.ServerName;

/// <summary>
Expand Down
6 changes: 4 additions & 2 deletions Dashboard/Services/DatabaseService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -85,15 +85,17 @@ public static SqlConnectionStringBuilder BuildConnectionString(
string? username = null,
string? password = null,
string encryptMode = "Mandatory",
bool trustServerCertificate = false)
bool trustServerCertificate = false,
bool readOnlyIntent = false)
{
var builder = new SqlConnectionStringBuilder
{
DataSource = serverName,
InitialCatalog = "PerformanceMonitor",
TrustServerCertificate = trustServerCertificate,
IntegratedSecurity = useWindowsAuth,
MultipleActiveResultSets = true
MultipleActiveResultSets = true,
ApplicationIntent = readOnlyIntent ? ApplicationIntent.ReadOnly : ApplicationIntent.ReadWrite
};

// Set encryption mode
Expand Down
Loading