From c77369bb8ad9adb74b4a0a9e63979d29eef340a4 Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Mon, 23 Jun 2025 20:15:21 +0200 Subject: [PATCH 1/4] chore: added latency tooltips on workspaces --- App/ViewModels/AgentViewModel.cs | 126 ++++++++++++++++++++---- App/ViewModels/TrayWindowViewModel.cs | 35 +++++-- App/Views/Pages/TrayWindowMainPage.xaml | 9 +- Vpn.Proto/vpn.proto | 24 ++++- 4 files changed, 160 insertions(+), 34 deletions(-) diff --git a/App/ViewModels/AgentViewModel.cs b/App/ViewModels/AgentViewModel.cs index cd5907b..0b41fc1 100644 --- a/App/ViewModels/AgentViewModel.cs +++ b/App/ViewModels/AgentViewModel.cs @@ -1,11 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.ComponentModel; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Windows.ApplicationModel.DataTransfer; using Coder.Desktop.App.Services; using Coder.Desktop.App.Utils; using Coder.Desktop.CoderSdk; @@ -18,15 +10,24 @@ using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Controls.Primitives; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using System.Xml.Linq; +using Windows.ApplicationModel.DataTransfer; namespace Coder.Desktop.App.ViewModels; public interface IAgentViewModelFactory { public AgentViewModel Create(IAgentExpanderHost expanderHost, Uuid id, string fullyQualifiedDomainName, - string hostnameSuffix, - AgentConnectionStatus connectionStatus, Uri dashboardBaseUrl, string? workspaceName); - + string hostnameSuffix, AgentConnectionStatus connectionStatus, Uri dashboardBaseUrl, + string? workspaceName, bool? didP2p, string? preferredDerp, TimeSpan? latency, TimeSpan? preferredDerpLatency, DateTime? lastHandshake); public AgentViewModel CreateDummy(IAgentExpanderHost expanderHost, Uuid id, string hostnameSuffix, AgentConnectionStatus connectionStatus, Uri dashboardBaseUrl, string workspaceName); @@ -40,8 +41,11 @@ public class AgentViewModelFactory( { public AgentViewModel Create(IAgentExpanderHost expanderHost, Uuid id, string fullyQualifiedDomainName, string hostnameSuffix, - AgentConnectionStatus connectionStatus, Uri dashboardBaseUrl, string? workspaceName) + AgentConnectionStatus connectionStatus, Uri dashboardBaseUrl, + string? workspaceName, bool? didP2p, string? preferredDerp, TimeSpan? latency, TimeSpan? preferredDerpLatency, + DateTime? lastHandshake) { + System.Diagnostics.Debug.WriteLine($"Creating agent: {didP2p} {preferredDerp} {latency} {lastHandshake}"); return new AgentViewModel(childLogger, coderApiClientFactory, credentialManager, agentAppViewModelFactory, expanderHost, id) { @@ -51,6 +55,11 @@ public AgentViewModel Create(IAgentExpanderHost expanderHost, Uuid id, string fu ConnectionStatus = connectionStatus, DashboardBaseUrl = dashboardBaseUrl, WorkspaceName = workspaceName, + DidP2p = didP2p, + PreferredDerp = preferredDerp, + Latency = latency, + PreferredDerpLatency = preferredDerpLatency, + LastHandshake = lastHandshake, }; } @@ -73,10 +82,10 @@ public AgentViewModel CreateDummy(IAgentExpanderHost expanderHost, Uuid id, public enum AgentConnectionStatus { - Green, - Yellow, - Red, - Gray, + Healthy, + Unhealthy, + NoRecentHandshake, + Offline, } public partial class AgentViewModel : ObservableObject, IModelUpdateable @@ -182,6 +191,75 @@ public string FullyQualifiedDomainName [NotifyPropertyChangedFor(nameof(ExpandAppsMessage))] public partial bool AppFetchErrored { get; set; } = false; + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(ConnectionTooltip))] + public partial bool? DidP2p { get; set; } = false; + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(ConnectionTooltip))] + public partial string? PreferredDerp { get; set; } = null; + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(ConnectionTooltip))] + public partial TimeSpan? Latency { get; set; } = null; + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(ConnectionTooltip))] + public partial TimeSpan? PreferredDerpLatency { get; set; } = null; + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(ConnectionTooltip))] + public partial DateTime? LastHandshake { get; set; } = null; + + public string ConnectionTooltip { get + { + var description = new StringBuilder(); + + if (DidP2p != null && DidP2p.Value && Latency != null) + { + description.Append($""" + You're connected peer-to-peer. + + You ↔ {Latency.Value.Milliseconds} ms ↔ {WorkspaceName} + """ + ); + } + else if (PreferredDerpLatency != null) + { + description.Append($""" + You're connected through a DERP relay. + We'll switch over to peer-to-peer when available. + + Total latency: {PreferredDerpLatency.Value.Milliseconds} ms + """ + ); + + if (PreferredDerp != null && Latency != null) + { + description.Append($"\nYou ↔ {PreferredDerp}: {PreferredDerpLatency.Value.Milliseconds} ms"); + + var derpToWorkspaceEstimatedLatency = Latency - PreferredDerpLatency; + + // Guard against negative values if the two readings were taken at different times + if (derpToWorkspaceEstimatedLatency > TimeSpan.Zero) + { + description.Append($"\n{PreferredDerp} ms ↔ {WorkspaceName}: {derpToWorkspaceEstimatedLatency.Value.Milliseconds} ms"); + } + } + } + if (LastHandshake != null) + description.Append($"\n\nLast handshake: {LastHandshake?.ToString() ?? "Unknown"}"); + + var tooltip = description.ToString().TrimEnd('\n', ' '); + + if (string.IsNullOrEmpty(tooltip)) + return "No connection information available."; + + return tooltip; + } + } + + // We only show 6 apps max, which fills the entire width of the tray // window. public IEnumerable VisibleApps => Apps.Count > MaxAppsPerRow ? Apps.Take(MaxAppsPerRow) : Apps; @@ -192,7 +270,7 @@ public string? ExpandAppsMessage { get { - if (ConnectionStatus == AgentConnectionStatus.Gray) + if (ConnectionStatus == AgentConnectionStatus.Offline) return "Your workspace is offline."; if (FetchingApps && Apps.Count == 0) // Don't show this message if we have any apps already. When @@ -285,6 +363,16 @@ public bool TryApplyChanges(AgentViewModel model) DashboardBaseUrl = model.DashboardBaseUrl; if (WorkspaceName != model.WorkspaceName) WorkspaceName = model.WorkspaceName; + if (DidP2p != model.DidP2p) + DidP2p = model.DidP2p; + if (PreferredDerp != model.PreferredDerp) + PreferredDerp = model.PreferredDerp; + if (Latency != model.Latency) + Latency = model.Latency; + if (PreferredDerpLatency != model.PreferredDerpLatency) + PreferredDerpLatency = model.PreferredDerpLatency; + if (LastHandshake != model.LastHandshake) + LastHandshake = model.LastHandshake; // Apps are not set externally. @@ -307,7 +395,7 @@ public void SetExpanded(bool expanded) partial void OnConnectionStatusChanged(AgentConnectionStatus oldValue, AgentConnectionStatus newValue) { - if (IsExpanded && newValue is not AgentConnectionStatus.Gray) FetchApps(); + if (IsExpanded && newValue is not AgentConnectionStatus.Offline) FetchApps(); } private void FetchApps() @@ -316,7 +404,7 @@ private void FetchApps() FetchingApps = true; // If the workspace is off, then there's no agent and there's no apps. - if (ConnectionStatus == AgentConnectionStatus.Gray) + if (ConnectionStatus == AgentConnectionStatus.Offline) { FetchingApps = false; Apps.Clear(); diff --git a/App/ViewModels/TrayWindowViewModel.cs b/App/ViewModels/TrayWindowViewModel.cs index 8540453..b43eaa8 100644 --- a/App/ViewModels/TrayWindowViewModel.cs +++ b/App/ViewModels/TrayWindowViewModel.cs @@ -3,6 +3,7 @@ using System.Collections.ObjectModel; using System.ComponentModel; using System.Linq; +using System.Security.Principal; using System.Threading.Tasks; using Coder.Desktop.App.Models; using Coder.Desktop.App.Services; @@ -29,6 +30,7 @@ public partial class TrayWindowViewModel : ObservableObject, IAgentExpanderHost { private const int MaxAgents = 5; private const string DefaultDashboardUrl = "https://coder.com"; + private readonly TimeSpan HealthyPingThreshold = TimeSpan.FromMilliseconds(150); private readonly IServiceProvider _services; private readonly IRpcController _rpcController; @@ -222,12 +224,26 @@ private void UpdateFromRpcModel(RpcModel rpcModel) if (string.IsNullOrWhiteSpace(fqdn)) continue; + var lastHandshakeAgo = DateTime.UtcNow.Subtract(agent.LastHandshake.ToDateTime()); - var connectionStatus = lastHandshakeAgo < TimeSpan.FromMinutes(5) - ? AgentConnectionStatus.Green - : AgentConnectionStatus.Yellow; + + // For compatibility with older deployments, we assume that if the + // last ping is null, the agent is healthy. + var isLatencyAcceptable = agent.LastPing != null ? agent.LastPing.Latency.ToTimeSpan() < HealthyPingThreshold : true; + var connectionStatus = AgentConnectionStatus.Healthy; + if (lastHandshakeAgo > TimeSpan.FromMinutes(5)) + { + connectionStatus = AgentConnectionStatus.NoRecentHandshake; + } + else if (!isLatencyAcceptable) + { + connectionStatus = AgentConnectionStatus.Unhealthy; + } + + workspacesWithAgents.Add(agent.WorkspaceId); var workspace = rpcModel.Workspaces.FirstOrDefault(w => w.Id == agent.WorkspaceId); + System.Diagnostics.Debug.WriteLine($"Agent {uuid} LastHandshakeAgo: {lastHandshakeAgo} ConnectionStatus: {connectionStatus} FQDN: {fqdn} Last ping: {agent.LastPing} Last handshake: {agent.LastHandshake}"); agents.Add(_agentViewModelFactory.Create( this, @@ -236,7 +252,12 @@ private void UpdateFromRpcModel(RpcModel rpcModel) _hostnameSuffixGetter.GetCachedSuffix(), connectionStatus, credentialModel.CoderUrl, - workspace?.Name)); + workspace?.Name, + agent.LastPing?.DidP2P, + agent.LastPing?.PreferredDerp, + agent.LastPing?.Latency?.ToTimeSpan(), + agent.LastPing?.PreferredDerpLatency?.ToTimeSpan(), + agent.LastHandshake?.ToDateTime())); } // For every stopped workspace that doesn't have any agents, add a @@ -253,7 +274,7 @@ private void UpdateFromRpcModel(RpcModel rpcModel) // conflict with any agent IDs. uuid, _hostnameSuffixGetter.GetCachedSuffix(), - AgentConnectionStatus.Gray, + AgentConnectionStatus.Offline, credentialModel.CoderUrl, workspace.Name)); } @@ -268,7 +289,7 @@ private void UpdateFromRpcModel(RpcModel rpcModel) if (Agents.Count < MaxAgents) ShowAllAgents = false; - var firstOnlineAgent = agents.FirstOrDefault(a => a.ConnectionStatus != AgentConnectionStatus.Gray); + var firstOnlineAgent = agents.FirstOrDefault(a => a.ConnectionStatus != AgentConnectionStatus.Offline); if (firstOnlineAgent is null) _hasExpandedAgent = false; if (!_hasExpandedAgent && firstOnlineAgent is not null) @@ -433,7 +454,7 @@ private static bool ShouldShowDummy(Workspace workspace) case Workspace.Types.Status.Stopping: case Workspace.Types.Status.Stopped: return true; - // TODO: should we include and show a different color than Gray for workspaces that are + // TODO: should we include and show a different color than Offline for workspaces that are // failed, canceled or deleting? default: return false; diff --git a/App/Views/Pages/TrayWindowMainPage.xaml b/App/Views/Pages/TrayWindowMainPage.xaml index 9f27fb1..896327e 100644 --- a/App/Views/Pages/TrayWindowMainPage.xaml +++ b/App/Views/Pages/TrayWindowMainPage.xaml @@ -137,22 +137,22 @@ x:Key="StatusColor" SelectedKey="{x:Bind Path=ConnectionStatus, Mode=OneWay}"> - + - + - + - + @@ -189,6 +189,7 @@ HorizontalAlignment="Right" VerticalAlignment="Center" Height="14" Width="14" + ToolTipService.ToolTip="{x:Bind ConnectionTooltip, Mode=OneWay}" Margin="0,1,0,0"> Date: Tue, 24 Jun 2025 14:17:57 +0200 Subject: [PATCH 2/4] PR fixes --- App/ViewModels/AgentViewModel.cs | 33 +++++++++++++++++++-------- App/ViewModels/TrayWindowViewModel.cs | 6 ++--- 2 files changed, 26 insertions(+), 13 deletions(-) diff --git a/App/ViewModels/AgentViewModel.cs b/App/ViewModels/AgentViewModel.cs index 0b41fc1..c1ea0fc 100644 --- a/App/ViewModels/AgentViewModel.cs +++ b/App/ViewModels/AgentViewModel.cs @@ -45,7 +45,6 @@ public AgentViewModel Create(IAgentExpanderHost expanderHost, Uuid id, string fu string? workspaceName, bool? didP2p, string? preferredDerp, TimeSpan? latency, TimeSpan? preferredDerpLatency, DateTime? lastHandshake) { - System.Diagnostics.Debug.WriteLine($"Creating agent: {didP2p} {preferredDerp} {latency} {lastHandshake}"); return new AgentViewModel(childLogger, coderApiClientFactory, credentialManager, agentAppViewModelFactory, expanderHost, id) { @@ -88,6 +87,19 @@ public enum AgentConnectionStatus Offline, } +public static class AgentConnectionStatusExtensions +{ + public static string ToDisplayString(this AgentConnectionStatus status) => + status switch + { + AgentConnectionStatus.Healthy => "Healthy", + AgentConnectionStatus.Unhealthy => "High latency", + AgentConnectionStatus.NoRecentHandshake => "No recent handshake", + AgentConnectionStatus.Offline => "Offline", + _ => status.ToString() + }; +} + public partial class AgentViewModel : ObservableObject, IModelUpdateable { private const string DefaultDashboardUrl = "https://coder.com"; @@ -169,6 +181,7 @@ public string FullyQualifiedDomainName [ObservableProperty] [NotifyPropertyChangedFor(nameof(ShowExpandAppsMessage))] [NotifyPropertyChangedFor(nameof(ExpandAppsMessage))] + [NotifyPropertyChangedFor(nameof(ConnectionTooltip))] public required partial AgentConnectionStatus ConnectionStatus { get; set; } [ObservableProperty] @@ -214,11 +227,12 @@ public string FullyQualifiedDomainName public string ConnectionTooltip { get { var description = new StringBuilder(); + var highLatencyWarning = ConnectionStatus == AgentConnectionStatus.Unhealthy ? $"({AgentConnectionStatus.Unhealthy.ToDisplayString()})" : ""; if (DidP2p != null && DidP2p.Value && Latency != null) { description.Append($""" - You're connected peer-to-peer. + You're connected peer-to-peer. {highLatencyWarning} You ↔ {Latency.Value.Milliseconds} ms ↔ {WorkspaceName} """ @@ -227,7 +241,7 @@ public string ConnectionTooltip { get else if (PreferredDerpLatency != null) { description.Append($""" - You're connected through a DERP relay. + You're connected through a DERP relay. {highLatencyWarning} We'll switch over to peer-to-peer when available. Total latency: {PreferredDerpLatency.Value.Milliseconds} ms @@ -247,15 +261,14 @@ public string ConnectionTooltip { get } } } + else + { + description.Append(ConnectionStatus.ToDisplayString()); + } if (LastHandshake != null) - description.Append($"\n\nLast handshake: {LastHandshake?.ToString() ?? "Unknown"}"); - - var tooltip = description.ToString().TrimEnd('\n', ' '); - - if (string.IsNullOrEmpty(tooltip)) - return "No connection information available."; + description.Append($"\n\nLast handshake: {LastHandshake?.ToString()}"); - return tooltip; + return description.ToString().TrimEnd('\n', ' '); ; } } diff --git a/App/ViewModels/TrayWindowViewModel.cs b/App/ViewModels/TrayWindowViewModel.cs index b43eaa8..3b1e0be 100644 --- a/App/ViewModels/TrayWindowViewModel.cs +++ b/App/ViewModels/TrayWindowViewModel.cs @@ -224,12 +224,13 @@ private void UpdateFromRpcModel(RpcModel rpcModel) if (string.IsNullOrWhiteSpace(fqdn)) continue; - +#pragma warning disable CS8602 // Protobuf will always set this value, so we can safely dereference them. var lastHandshakeAgo = DateTime.UtcNow.Subtract(agent.LastHandshake.ToDateTime()); +#pragma warning restore CS8602 // For compatibility with older deployments, we assume that if the // last ping is null, the agent is healthy. - var isLatencyAcceptable = agent.LastPing != null ? agent.LastPing.Latency.ToTimeSpan() < HealthyPingThreshold : true; + var isLatencyAcceptable = agent.LastPing == null || agent.LastPing.Latency.ToTimeSpan() < HealthyPingThreshold; var connectionStatus = AgentConnectionStatus.Healthy; if (lastHandshakeAgo > TimeSpan.FromMinutes(5)) { @@ -243,7 +244,6 @@ private void UpdateFromRpcModel(RpcModel rpcModel) workspacesWithAgents.Add(agent.WorkspaceId); var workspace = rpcModel.Workspaces.FirstOrDefault(w => w.Id == agent.WorkspaceId); - System.Diagnostics.Debug.WriteLine($"Agent {uuid} LastHandshakeAgo: {lastHandshakeAgo} ConnectionStatus: {connectionStatus} FQDN: {fqdn} Last ping: {agent.LastPing} Last handshake: {agent.LastHandshake}"); agents.Add(_agentViewModelFactory.Create( this, From d1b2c7d59b7e770d334f0ea449ad13bb8468980f Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Tue, 24 Jun 2025 14:46:36 +0200 Subject: [PATCH 3/4] added connecting state and improved the logic on tooltip generation --- App/ViewModels/AgentViewModel.cs | 8 +++++-- App/ViewModels/TrayWindowViewModel.cs | 30 ++++++++++++++----------- App/Views/Pages/TrayWindowMainPage.xaml | 5 +++++ 3 files changed, 28 insertions(+), 15 deletions(-) diff --git a/App/ViewModels/AgentViewModel.cs b/App/ViewModels/AgentViewModel.cs index c1ea0fc..0e50b06 100644 --- a/App/ViewModels/AgentViewModel.cs +++ b/App/ViewModels/AgentViewModel.cs @@ -82,9 +82,10 @@ public AgentViewModel CreateDummy(IAgentExpanderHost expanderHost, Uuid id, public enum AgentConnectionStatus { Healthy, + Connecting, Unhealthy, NoRecentHandshake, - Offline, + Offline } public static class AgentConnectionStatusExtensions @@ -93,6 +94,7 @@ public static string ToDisplayString(this AgentConnectionStatus status) => status switch { AgentConnectionStatus.Healthy => "Healthy", + AgentConnectionStatus.Connecting => "Connecting", AgentConnectionStatus.Unhealthy => "High latency", AgentConnectionStatus.NoRecentHandshake => "No recent handshake", AgentConnectionStatus.Offline => "Offline", @@ -224,7 +226,9 @@ public string FullyQualifiedDomainName [NotifyPropertyChangedFor(nameof(ConnectionTooltip))] public partial DateTime? LastHandshake { get; set; } = null; - public string ConnectionTooltip { get + public string ConnectionTooltip + { + get { var description = new StringBuilder(); var highLatencyWarning = ConnectionStatus == AgentConnectionStatus.Unhealthy ? $"({AgentConnectionStatus.Unhealthy.ToDisplayString()})" : ""; diff --git a/App/ViewModels/TrayWindowViewModel.cs b/App/ViewModels/TrayWindowViewModel.cs index 3b1e0be..f57947d 100644 --- a/App/ViewModels/TrayWindowViewModel.cs +++ b/App/ViewModels/TrayWindowViewModel.cs @@ -224,24 +224,28 @@ private void UpdateFromRpcModel(RpcModel rpcModel) if (string.IsNullOrWhiteSpace(fqdn)) continue; -#pragma warning disable CS8602 // Protobuf will always set this value, so we can safely dereference them. - var lastHandshakeAgo = DateTime.UtcNow.Subtract(agent.LastHandshake.ToDateTime()); -#pragma warning restore CS8602 - - // For compatibility with older deployments, we assume that if the - // last ping is null, the agent is healthy. - var isLatencyAcceptable = agent.LastPing == null || agent.LastPing.Latency.ToTimeSpan() < HealthyPingThreshold; var connectionStatus = AgentConnectionStatus.Healthy; - if (lastHandshakeAgo > TimeSpan.FromMinutes(5)) + + if (agent.LastHandshake != null && agent.LastHandshake.ToDateTime() != default && agent.LastHandshake.ToDateTime() < DateTime.UtcNow) { - connectionStatus = AgentConnectionStatus.NoRecentHandshake; + // For compatibility with older deployments, we assume that if the + // last ping is null, the agent is healthy. + var isLatencyAcceptable = agent.LastPing == null || agent.LastPing.Latency.ToTimeSpan() < HealthyPingThreshold; + + var lastHandshakeAgo = DateTime.UtcNow.Subtract(agent.LastHandshake.ToDateTime()); + + if (lastHandshakeAgo > TimeSpan.FromMinutes(5)) + connectionStatus = AgentConnectionStatus.NoRecentHandshake; + else if (!isLatencyAcceptable) + connectionStatus = AgentConnectionStatus.Unhealthy; } - else if (!isLatencyAcceptable) + else { - connectionStatus = AgentConnectionStatus.Unhealthy; + // If the last handshake is not correct (null, default or in the future), + // we assume the agent is connecting (yellow status icon). + connectionStatus = AgentConnectionStatus.Connecting; } - workspacesWithAgents.Add(agent.WorkspaceId); var workspace = rpcModel.Workspaces.FirstOrDefault(w => w.Id == agent.WorkspaceId); @@ -257,7 +261,7 @@ private void UpdateFromRpcModel(RpcModel rpcModel) agent.LastPing?.PreferredDerp, agent.LastPing?.Latency?.ToTimeSpan(), agent.LastPing?.PreferredDerpLatency?.ToTimeSpan(), - agent.LastHandshake?.ToDateTime())); + agent.LastHandshake != null && agent.LastHandshake.ToDateTime() != default ? agent.LastHandshake?.ToDateTime() : null)); } // For every stopped workspace that doesn't have any agents, add a diff --git a/App/Views/Pages/TrayWindowMainPage.xaml b/App/Views/Pages/TrayWindowMainPage.xaml index 17d2eef..3cc21de 100644 --- a/App/Views/Pages/TrayWindowMainPage.xaml +++ b/App/Views/Pages/TrayWindowMainPage.xaml @@ -152,6 +152,11 @@ + + + + + From 8fbbff0531065a2c469e323e953d4cac002a5668 Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Tue, 24 Jun 2025 14:50:23 +0200 Subject: [PATCH 4/4] PR review --- App/ViewModels/AgentViewModel.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/App/ViewModels/AgentViewModel.cs b/App/ViewModels/AgentViewModel.cs index 0e50b06..0cf2651 100644 --- a/App/ViewModels/AgentViewModel.cs +++ b/App/ViewModels/AgentViewModel.cs @@ -242,17 +242,17 @@ public string ConnectionTooltip """ ); } - else if (PreferredDerpLatency != null) + else if (Latency != null) { description.Append($""" You're connected through a DERP relay. {highLatencyWarning} We'll switch over to peer-to-peer when available. - Total latency: {PreferredDerpLatency.Value.Milliseconds} ms + Total latency: {Latency.Value.Milliseconds} ms """ ); - if (PreferredDerp != null && Latency != null) + if (PreferredDerpLatency != null) { description.Append($"\nYou ↔ {PreferredDerp}: {PreferredDerpLatency.Value.Milliseconds} ms");