Repository: syncthing/syncthing (language: go)

Issue:
Show connection type in Remote Devices
### Problem

I've noticed that more often than I'd expect, I have devices connected over a (slow) relay when they can have a (fast) local connection. It's easy to miss, but when big syncs take a long time, you'll start looking. I've seen this multiple times, for a variety of reasons. One time I entered the wrong tcp://ip-address so dynamic was used. Another time I used a laptop at the office first and the 24/7 online home server never used the local ip when I was on the home wifi, until I restarted syncthing on the home server. And just now I noticed that a remote connection with a very fast optical fiber was using a slow relay for the past year.

### Feature

It would help to see **a label**, icon or indicator of sorts to show if a relay was used, or a direct remote connection, or a LAN connection, and some polite way of indicating that a direct connection is better than a relay.  This way you'd immediately see if it is what you expected or not. This illustrates what I mean (although emoji's are not the implementation I'd suggest):

> ### Remote Devices
>
> [[device1]       [LAN 🤩] [Up to Date]]
> [[device2]       [LAN 🤩] [Up to Date]]
> [[device3]       [direct 😄] [Up to Date]]
> [[device4]       [relay 🙂] [Up to Date]]

Encouraging users to check their connections this way also alleviates relays that provide an invaluable service from peers that unknowingly unnecessarily burden those relays when all they need to do is a restart, config change or port forward. This way they get a faster sync, and the relays are free to service users that really need it. I'd prefer short concise labels, but any indicator would be helpful.


Discussion / hints:
You can already see the address it used to infer the type of connection, so its already done as far as I am concerned. You also don't need to restart things if better connections are available, syncthing will switch itself.

I don't think we want to be a kindergarten to start using crayons and emojis to expose this information that is already exposed.
The emojis are probably a no-go, but I'd personally very much like to have a clear and visible indicator about what kind of connection is being currently used. Just the address isn't really enough, as non-technical users have absolutely no idea whether addresses like `192.168.x.x` or `10.0.0.x` are local or remote. At the moment, there is a pop-up shown on hover which does contain information like `tcp-server`, `relay-server`, etc. but these are still way too technical for "average" users to understand.
Wholeheartedly agree with @tomasz1986 

A clear indicator for non-technical users would be very appreciated.
I think understanding URLs is enough steps above kindergarden that it warrants a ui enhancement requst for a small, non-emoji status indication about what kind of connection is active besides the URL (which as explained does indicate that information already, so it's just a user aid).
I don't think non technical users will understand the difference between whatever indication we put, as they don't understand how the internet works. Lan/wan or the benefits of one or the other has no meaning to them.

This is only meaningful to technical users who can already infer it from the address.
Even more technically inclined users might not understand the implications of e.g QUIC vs TCP or direct vs relay

A simple good/better/best indicator would be enough IMHO.

Most people have no idea about NAT but most games will give you a NAT rating and people know if things are good or if they need to reach out for help if not.
> Lan/wan or the benefits of one or the other has no meaning to them.

Yeah, but from my experience, even non-technical users are aware of the implications when they are told that the current connection is a "direct connection through the local network, i.e. potentially the fastest possible" and an "indirect connection through a remote relay server, i.e. potentially slow", etc. Basically, as long as overly technical language is avoided and no fancy abbreviations are used, people do understand 🙂.
We could use something commonly understood like a signal strength indicator. One bar is a relay connection, two bars is a quic connection, three bars a tcp connection to a random address, four bars a tcp connection to a LAN-like address, five bars a tcp connection to a directly connected network, or something like that.
https://icons.getbootstrap.com/icons/reception-1/  up to https://icons.getbootstrap.com/icons/reception-4/ maybe?

Fix patch:
diff --git a/gui/black/assets/css/theme.css b/gui/black/assets/css/theme.css
index 622a22a5895..1e280a952a0 100644
--- a/gui/black/assets/css/theme.css
+++ b/gui/black/assets/css/theme.css
@@ -273,3 +273,8 @@ code.ng-binding{
 .fancytree-focused {
     background-color: #222;
 }
+
+/* Remote Devices 'connection type'-icon color set to #aaa */
+.reception {
+    filter: invert(77%) sepia(0%) saturate(724%) hue-rotate(146deg) brightness(91%) contrast(85%);
+}
diff --git a/gui/dark/assets/css/theme.css b/gui/dark/assets/css/theme.css
index f554bb8a8af..26ab0f89821 100644
--- a/gui/dark/assets/css/theme.css
+++ b/gui/dark/assets/css/theme.css
@@ -285,3 +285,8 @@ code.ng-binding{
 .fancytree-focused {
     background-color: #424242;
 }
+
+/* Remote Devices 'connection type'-icon color set to #aaa */
+.reception {
+    filter: invert(77%) sepia(0%) saturate(724%) hue-rotate(146deg) brightness(91%) contrast(85%);
+}
\ No newline at end of file
diff --git a/gui/default/assets/css/overrides.css b/gui/default/assets/css/overrides.css
index 7033d8e3690..5186ec5c266 100644
--- a/gui/default/assets/css/overrides.css
+++ b/gui/default/assets/css/overrides.css
@@ -144,6 +144,38 @@ table.table-auto td {
     max-width: 0px;
 }
 
+/* Remote Devices connection-quality indicator */
+.reception-0 {
+    background: url('../../vendor/bootstrap/fonts/reception-0.svg') no-repeat;
+}
+
+.reception-1 {
+    background: url('../../vendor/bootstrap/fonts/reception-1.svg') no-repeat;
+}
+
+.reception-2 {
+    background: url('../../vendor/bootstrap/fonts/reception-2.svg') no-repeat;
+}
+
+.reception-3 {
+    background: url('../../vendor/bootstrap/fonts/reception-3.svg') no-repeat;
+}
+
+.reception-4 {
+    background: url('../../vendor/bootstrap/fonts/reception-4.svg') no-repeat;
+}
+  
+.reception { 
+    width: 1em;
+    height: 1em;
+    display: inline-block;
+    vertical-align: -20%;
+}
+
+.remote-devices-panel {
+    display: inline-block;
+}
+
 /* Wrap long file paths to prevent text overflow. See issue #6268. */
 .file-path {
     word-break: break-all;
diff --git a/gui/default/index.html b/gui/default/index.html
index 664c8f37fa4..d197b0e0921 100644
--- a/gui/default/index.html
+++ b/gui/default/index.html
@@ -24,6 +24,7 @@
   <link href="assets/css/tree.css" rel="stylesheet"/>
   <link href="assets/css/overrides.css" rel="stylesheet"/>
   <link href="assets/css/theme.css" rel="stylesheet"/>
+  <link href="assets/css/customicons.css" rel="stylesheet"/>
 </head>
 
 <body>
@@ -737,16 +738,25 @@ <h3 translate>Remote Devices</h3>
                 <div class="panel-progress" ng-show="deviceStatus(deviceCfg) == 'syncing'" ng-attr-style="width: {{completion[deviceCfg.deviceID]._total | percent}}"></div>
                 <h4 class="panel-title">
                   <identicon class="panel-icon" data-value="deviceCfg.deviceID"></identicon>
-                  <span ng-switch="deviceStatus(deviceCfg)" class="pull-right text-{{deviceClass(deviceCfg)}}">
-                    <span ng-switch-when="insync"><span class="hidden-xs" translate>Up to Date</span><span class="visible-xs" aria-label="{{'Up to Date' | translate}}"><i class="fas fa-fw fa-check"></i></span></span>
-                    <span ng-switch-when="unused-insync"><span class="hidden-xs" translate>Connected (Unused)</span><span class="visible-xs" aria-label="{{'Connected (Unused)' | translate}}"><i class="fas fa-fw fa-unlink"></i></span></span>
-                    <span ng-switch-when="syncing">
-                      <span class="hidden-xs" translate>Syncing</span> ({{completion[deviceCfg.deviceID]._total | percent}}, {{completion[deviceCfg.deviceID]._needBytes | binary}}B)
+                  <span class="pull-right text-{{deviceClass(deviceCfg)}}">
+                    <span ng-switch="deviceStatus(deviceCfg)" class="remote-devices-panel"> 
+                      <span ng-switch-when="insync"><span class="hidden-xs" translate>Up to Date</span><span class="visible-xs" aria-label="{{'Up to Date' | translate}}"><i class="fas fa-fw fa-check"></i></span></span>
+                      <span ng-switch-when="unused-insync"><span class="hidden-xs" translate>Connected (Unused)</span><span class="visible-xs" aria-label="{{'Connected (Unused)' | translate}}"><i class="fas fa-fw fa-unlink"></i></span></span>
+                      <span ng-switch-when="syncing">
+                        <span class="hidden-xs" translate>Syncing</span> ({{completion[deviceCfg.deviceID]._total | percent}}, {{completion[deviceCfg.deviceID]._needBytes | binary}}B)
+                      </span>
+                      <span ng-switch-when="paused"><span class="hidden-xs" translate>Paused</span><span class="visible-xs" aria-label="{{'Paused' | translate}}"><i class="fas fa-fw fa-pause"></i></span></span>
+                      <span ng-switch-when="unused-paused"><span class="hidden-xs" translate>Paused (Unused)</span><span class="visible-xs" aria-label="{{'Paused (Unused)' | translate}}"><i class="fas fa-fw fa-unlink"></i></span></span>
+                      <span ng-switch-when="disconnected"><span class="hidden-xs" translate>Disconnected</span><span class="visible-xs" aria-label="{{'Disconnected' | translate}}"><i class="fas fa-fw fa-power-off"></i></span></span>
+                      <span ng-switch-when="unused-disconnected"><span class="hidden-xs" translate>Disconnected (Unused)</span><span class="visible-xs" aria-label="{{'Disconnected (Unused)' | translate}}"><i class="fas fa-fw fa-unlink"></i></span></span>
+                    </span>
+                    <span ng-switch="rdConnType(deviceCfg.deviceID)" class="remote-devices-panel">
+                      <span ng-switch-when="tcplan" class="reception reception-4 reception-theme"></span>
+                      <span ng-switch-when="tcpwan" class="reception reception-3 reception-theme"></span>
+                      <span ng-switch-when="quic" class="reception reception-2 reception-theme"></span> 
+                      <span ng-switch-when="relay" class="reception reception-1 reception-theme"></span>
+                      <span ng-switch-when="disconnected" class="reception reception-0 reception-theme"></span>
                     </span>
-                    <span ng-switch-when="paused"><span class="hidden-xs" translate>Paused</span><span class="visible-xs" aria-label="{{'Paused' | translate}}"><i class="fas fa-fw fa-pause"></i></span></span>
-                    <span ng-switch-when="unused-paused"><span class="hidden-xs" translate>Paused (Unused)</span><span class="visible-xs" aria-label="{{'Paused (Unused)' | translate}}"><i class="fas fa-fw fa-unlink"></i></span></span>
-                    <span ng-switch-when="disconnected"><span class="hidden-xs" translate>Disconnected</span><span class="visible-xs" aria-label="{{'Disconnected' | translate}}"><i class="fas fa-fw fa-power-off"></i></span></span>
-                    <span ng-switch-when="unused-disconnected"><span class="hidden-xs" translate>Disconnected (Unused)</span><span class="visible-xs" aria-label="{{'Disconnected (Unused)' | translate}}"><i class="fas fa-fw fa-unlink"></i></span></span>
                   </span>
                   <div class="panel-title-text">{{deviceName(deviceCfg)}}</div>
                 </h4>
@@ -823,9 +833,13 @@ <h4 class="panel-title">
                           </span>
                         </td>
                       </tr>
-                      <tr ng-if="connections[deviceCfg.deviceID].connected && connections[deviceCfg.deviceID].type.indexOf('Relay') > -1" tooltip data-original-title="Connections via relays might be rate limited by the relay">
-                        <th><span class="fas fa-fw fa-exclamation-triangle text-danger"></span>&nbsp;<span translate>Connection Type</span></th>
-                        <td class="text-right">{{connections[deviceCfg.deviceID].type}}</td>
+                      <tr ng-if="connections[deviceCfg.deviceID].connected">
+                        <th><span class="reception reception-4 reception-theme"></span>&nbsp;<span translate>Connection Type</span></th>
+                        <td ng-if="connections[deviceCfg.deviceID].connected" class="text-right">
+                          <span tooltip data-original-title="{{rdConnDetails(rdConnType(deviceCfg.deviceID))}}">
+                            {{rdConnTypeString(rdConnType(deviceCfg.deviceID))}}
+                          </span>
+                        </td>
                       </tr>
                       <tr ng-if="deviceCfg.allowedNetworks.length > 0">
                         <th><span class="fas fa-fw fa-filter"></span>&nbsp;<span translate>Allowed Networks</span></th>
diff --git a/gui/default/syncthing/core/syncthingController.js b/gui/default/syncthing/core/syncthingController.js
index de5d3265932..49d9d446e87 100755
--- a/gui/default/syncthing/core/syncthingController.js
+++ b/gui/default/syncthing/core/syncthingController.js
@@ -1192,6 +1192,61 @@ angular.module('syncthing.core')
             return '?';
         };
 
+        $scope.rdConnType = function(deviceID) {
+            var conn = $scope.connections[deviceID];
+            if(!conn)
+                return "-1";
+                
+            if (conn.type.indexOf('relay') === 0)
+                return "relay";
+            
+            if (conn.type.indexOf('quic') === 0)
+                return "quic";
+            
+            if(conn.type.indexOf('tcp') === 0)
+                return "tcp"+rdAddrType(conn.address);
+
+            return "disconnected";
+        }
+
+        function rdAddrType(address) {
+            var re = /(^(?:127\.|0?10\.|172\.0?1[6-9]\.|172\.0?2[0-9]\.|172\.0?3[01]\.|192\.168\.|169\.254\.|::1|[fF][cCdD][0-9a-fA-F]{2}:|[fF][eE][89aAbB][0-9a-fA-F]:))/
+            if(re.test(address)) 
+                return "lan";
+        
+            return "wan";
+        }
+
+        $scope.rdConnTypeString = function(type) {
+            switch (type) {
+                case "relay":
+                    return $translate.instant('Relay');
+                case "quic":
+                    return $translate.instant('QUIC');
+                case "tcpwan":
+                    return $translate.instant('TCP WAN');
+                case "tcplan":
+                    return $translate.instant('TCP LAN');
+                default:
+                    return $translate.instant('Disconnected');
+            }
+        }
+
+        $scope.rdConnDetails = function(type) {
+            switch (type) {
+                case "relay":
+                    return $translate.instant('Connections via relays might be rate limited by the relay');
+                case "quic":
+                    return $translate.instant('QUIC connections are in most cases considered suboptimal');
+                case "tcpwan":
+                    return $translate.instant('Using a direct TCP connection over WAN');
+                case "tcplan":
+                    return $translate.instant('Using a direct TCP connection over LAN');
+                default:
+                    return $translate.instant('Unknown');
+            }
+        }
+
         $scope.hasRemoteGUIAddress = function (deviceCfg) {
             if (!deviceCfg.remoteGUIPort)
                 return false;
diff --git a/gui/default/vendor/bootstrap/fonts/reception-0.svg b/gui/default/vendor/bootstrap/fonts/reception-0.svg
new file mode 100644
index 00000000000..885bf3bb34d
--- /dev/null
+++ b/gui/default/vendor/bootstrap/fonts/reception-0.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-reception-0" viewBox="0 0 16 16">
+  <path d="M0 13.5a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 0 1h-2a.5.5 0 0 1-.5-.5zm4 0a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 0 1h-2a.5.5 0 0 1-.5-.5zm4 0a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 0 1h-2a.5.5 0 0 1-.5-.5zm4 0a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 0 1h-2a.5.5 0 0 1-.5-.5z"/>
+</svg>
\ No newline at end of file
diff --git a/gui/default/vendor/bootstrap/fonts/reception-1.svg b/gui/default/vendor/bootstrap/fonts/reception-1.svg
new file mode 100644
index 00000000000..3deafb6220d
--- /dev/null
+++ b/gui/default/vendor/bootstrap/fonts/reception-1.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-reception-1" viewBox="0 0 16 16">
+  <path d="M0 11.5a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1-.5-.5v-2zm4 2a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 0 1h-2a.5.5 0 0 1-.5-.5zm4 0a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 0 1h-2a.5.5 0 0 1-.5-.5zm4 0a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 0 1h-2a.5.5 0 0 1-.5-.5z"/>
+</svg>
\ No newline at end of file
diff --git a/gui/default/vendor/bootstrap/fonts/reception-2.svg b/gui/default/vendor/bootstrap/fonts/reception-2.svg
new file mode 100644
index 00000000000..7dca57aca8b
--- /dev/null
+++ b/gui/default/vendor/bootstrap/fonts/reception-2.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-reception-2" viewBox="0 0 16 16">
+  <path d="M0 11.5a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1-.5-.5v-2zm4-3a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 .5.5v5a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1-.5-.5v-5zm4 5a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 0 1h-2a.5.5 0 0 1-.5-.5zm4 0a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 0 1h-2a.5.5 0 0 1-.5-.5z"/>
+</svg>
\ No newline at end of file
diff --git a/gui/default/vendor/bootstrap/fonts/reception-3.svg b/gui/default/vendor/bootstrap/fonts/reception-3.svg
new file mode 100644
index 00000000000..b30d5fb79de
--- /dev/null
+++ b/gui/default/vendor/bootstrap/fonts/reception-3.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-reception-3" viewBox="0 0 16 16">
+  <path d="M0 11.5a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1-.5-.5v-2zm4-3a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 .5.5v5a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1-.5-.5v-5zm4-3a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 .5.5v8a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1-.5-.5v-8zm4 8a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 0 1h-2a.5.5 0 0 1-.5-.5z"/>
+</svg>
\ No newline at end of file
diff --git a/gui/default/vendor/bootstrap/fonts/reception-4.svg b/gui/default/vendor/bootstrap/fonts/reception-4.svg
new file mode 100644
index 00000000000..611bdf1b989
--- /dev/null
+++ b/gui/default/vendor/bootstrap/fonts/reception-4.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-reception-4" viewBox="0 0 16 16">
+  <path d="M0 11.5a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1-.5-.5v-2zm4-3a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 .5.5v5a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1-.5-.5v-5zm4-3a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 .5.5v8a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1-.5-.5v-8zm4-3a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 .5.5v11a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1-.5-.5v-11z"/>
+</svg>
\ No newline at end of file
diff --git a/gui/light/assets/css/theme.css b/gui/light/assets/css/theme.css
index 7e7212d3249..8a6183d4695 100644
--- a/gui/light/assets/css/theme.css
+++ b/gui/light/assets/css/theme.css
@@ -36,3 +36,8 @@
 .fancytree-focused {
     background-color: #eeeeee;
 }
+
+/* Remote Devices 'connection type'-icon color set to #333 */
+.reception {
+    filter: invert(12%) sepia(11%) saturate(20%) hue-rotate(318deg) brightness(100%) contrast(80%);
+}


Test patch: