Repository: syncthing/syncthing (language: go)

Issue:
Show if devices have not connected in a long time
Currently folders that are up to date are shown like this:
![image](https://user-images.githubusercontent.com/6650122/119070774-efb37980-b9e8-11eb-8a83-5e240fd1c6ff.png)

However, the second folder has not been syncing for at least a year!
A problem that is not directly apparent from the overview. Only after inspecting the list of devices synced with this folder and then opening each device to look at the "last seen" value might it become clear that this folder has not been synced in a long time.

To help get trustworthy status update at first glance (something that instills trust in Syncthing) I suggest the above to be changed to:
![image](https://user-images.githubusercontent.com/6650122/119071250-c0e9d300-b9e9-11eb-80cf-0ab818e789e9.png)

The threshold for when a folder is considered possibly stale could be some high value by default but editable in the advanced settings of the folder. Let's say a default of 14 days as not all use cases have at least 1 devices online always every second. However, if a folder has not synced (not seen a single device) for weeks; I'd consider that something most people would want to know.

Update: This warning would align with the other similar orange warning when Syncthing knows it can't ever be synced:
![image](https://user-images.githubusercontent.com/6650122/119072191-60f42c00-b9eb-11eb-9ebf-4f2ace3fa0b9.png)



Discussion / hints:
Not really a fan of this concept as the duration for something to be considered stale is highly subjective. This could be anything between minutes and months.

And it's not a useful information in a multi node cluster. The real source could have been long gone and the other nodes would happily keep it synced.

An additional timestamp detail like "last modified" would probably have more value.
I agree the timing is highly subjective. However I still think any information is better then nothing. To clarify in light of a multi node cluster scenario:

- The warming should not show if a folder has at least 1 peer that has been online (syncing or not).
- The warning should not appear on a folder if there is a problem with just one of it's peers.
- The warning should only appear on a folder if that folder **on that device** has not synced with any of its peers in weeks (adjustable).
- Maybe we can even extend this and show a stale warning on the peers gui if a peer has not been seen in weeks?

A real life example from one of my setups. For a folder I had the topology:
![image](https://user-images.githubusercontent.com/6650122/119152293-78b1cb80-ba50-11eb-960d-a85b38807515.png)

Then my computer had to be replaced and I went about reinstalling everything. Syncthing was not my only priority at the time so I made a mistake and forgot the link to the storage server for this folder:
![image](https://user-images.githubusercontent.com/6650122/119152590-bca4d080-ba50-11eb-9ca5-57be317b4f17.png)

I do check the health of my storage server by glancing oer the 30+ folders on the syncthing dashboard of my storage server once in a while. However, because I forgot to replace the link the storage server kept showing "Up to Date" for this folder even though it had not seen my old PC (and thus any peer) in almost a year. As a consequence I've not enjoyed file versioning for that year.

Now I do realize that on the storage server I could see my old pc be "offline". However, there are many peers (more so than folders) all sometimes online, offline or unused and with different naming schemes making it not really useful to look at and quickly get a feel of the state. And even when I do notice the peer it will just be offline. In practice one looks at the page scanning for warnings (in color) to determine the health of the system. Is there something red or something yellow; then I'll take a look at it. Only this would have draw attention.
Thus it would have been really helpful if Syncthing would have notified me with such warning if it hadn't seen any peer for a long time. It has the information readily available already.

Maybe something like this:
![image](https://user-images.githubusercontent.com/6650122/119155991-0d69f880-ba54-11eb-8e2b-6aeed40a109e.png)

Update: Maybe tombstoned is the wrong wording here. What about this:
![image](https://user-images.githubusercontent.com/6650122/119157820-da286900-ba55-11eb-9453-4e2c4485e387.png)


To be honest, I feel this is your observability issue due to nature of your setup (a lot of devices yada yada), and not syncthing Issue.

If you have a large setup, I'd expect you to invest time into monitoring it or building tools that help you monitor it.

As it stands I feel you to asking us to solve your observability problems.

I am not against the idea, but I feel it will be of no use or even an annoyance for most users, and seems implementing like that would have a non-trivial engineering.
You misunderstand. I don't expect anybody to solve my observability problems. And laying out this feature request in detail and in good faith is my attempt at investing time in better tools to help monitor Syncthing.

If my humble suggestion resonates with other power users and is accepted by the team I'd even put in the time to get acquainted with Go to try and make a good pull request.

If not, consider the issue closed.
A label for devices we haven't seen in ages seems reasonable. I think this could/should live entirely in the web UI code, as you have all the needed info there (last connected time), no need to expand the API. If configuration is necessary, that can also be stored in local storage on the web UI.

And it should clearly happen at device level: For folders there's no clear criterium, as any amount of devices are involved (and they might get up-to-date via a third device we don't know about, and possible other cases). I took the liberty to rename the issue.
But isn't this the same thing as the existing "last seen" row?

I think this needs a better proposal fleshed out, as an outright "I have not seen a device for 7 days lets start flashing warnings" is definitely just going to cause a ton of noise with zero value.
> But isn't this the same thing as the existing "last seen" row?

I basically interpret this as a way to make that more discoverable, respectively pull attention to it.

Changing the folder status to orange/warning and then maybe highlighting the `last seen` row somehow (or whatever else) doesn't seem that noisy to me. I do personally agree it doesn't add much value (at least it wont for me), but it seems some people want it enough to get their hands dirty, and I don't see it as detrimental/noisy. Just my stance, I don't have much investment in this question beyond liking (good) contributions :) 
What happens if I startup my device after 7 days? Everything is orange?
IMHO adding a `Last change` timestamp is less noisy and we don't need to come up with an arbitrary threshold for a warning. I know that we already have that information but it's hidden in the mouseover of the `Latest change` and suffers from bad discoverability.
> What happens if I startup my device after 7 days? Everything is orange?

Yep. Until the device connects. Doesn't seem so bad to me, but that's something better discussed between you and @SixFive7 - I really don't mind if it's this variant, no change at all or another variant (well here I might mind depending on what it turns out to be).

And I don't see how last change has anything to do with this.

Fix patch:
diff --git a/gui/default/assets/lang/lang-en.json b/gui/default/assets/lang/lang-en.json
index bd448ab47d5..ee135ce8cf9 100644
--- a/gui/default/assets/lang/lang-en.json
+++ b/gui/default/assets/lang/lang-en.json
@@ -101,6 +101,7 @@
    "Disables comparing and syncing file permissions. Useful on systems with nonexistent or custom permissions (e.g. FAT, exFAT, Synology, Android).": "Disables comparing and syncing file permissions. Useful on systems with nonexistent or custom permissions (e.g. FAT, exFAT, Synology, Android).",
    "Discard": "Discard",
    "Disconnected": "Disconnected",
+   "Disconnected (Inactive)": "Disconnected (Inactive)",
    "Disconnected (Unused)": "Disconnected (Unused)",
    "Discovered": "Discovered",
    "Discovery": "Discovery",
@@ -228,6 +229,9 @@
    "Minimum Free Disk Space": "Minimum Free Disk Space",
    "Mod. Device": "Mod. Device",
    "Mod. Time": "Mod. Time",
+   "More than a month ago": "More than a month ago",
+   "More than a week ago": "More than a week ago",
+   "More than a year ago": "More than a year ago",
    "Move to top of queue": "Move to top of queue",
    "Multi level wildcard (matches multiple directory levels)": "Multi level wildcard (matches multiple directory levels)",
    "Never": "Never",
diff --git a/gui/default/index.html b/gui/default/index.html
index d197b0e0921..b5b4cb63bb5 100644
--- a/gui/default/index.html
+++ b/gui/default/index.html
@@ -748,6 +748,7 @@ <h4 class="panel-title">
                       <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="disconnected-inactive"><span class="hidden-xs" translate>Disconnected (Inactive)</span><span class="visible-xs" aria-label="{{'Disconnected (Inactive)' | 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">
@@ -767,8 +768,21 @@ <h4 class="panel-title">
                     <tbody>
                       <tr ng-if="!connections[deviceCfg.deviceID].connected">
                         <th><span class="fas fa-fw fa-eye"></span>&nbsp;<span translate>Last seen</span></th>
-                        <td translate ng-if="!deviceStats[deviceCfg.deviceID].lastSeenDays || deviceStats[deviceCfg.deviceID].lastSeenDays >= 365" class="text-right">Never</td>
-                        <td ng-if="deviceStats[deviceCfg.deviceID].lastSeenDays < 365" class="text-right">{{deviceStats[deviceCfg.deviceID].lastSeen | date:"yyyy-MM-dd HH:mm:ss"}}</td>
+                        <td class="text-right">
+                          <div ng-if="!deviceStats[deviceCfg.deviceID].lastSeenDays" translate>
+                            Never
+                          </div>
+                          <div ng-if="deviceStats[deviceCfg.deviceID].lastSeenDays">
+                            <div>
+                              {{deviceStats[deviceCfg.deviceID].lastSeen | date:"yyyy-MM-dd HH:mm:ss"}}
+                            </div>
+                            <div ng-if="deviceStats[deviceCfg.deviceID].lastSeenDays >= 7">
+                              <i ng-if="deviceStats[deviceCfg.deviceID].lastSeenDays < 30" translate>More than a week ago</i>
+                              <i class="text-warning" ng-if="deviceStats[deviceCfg.deviceID].lastSeenDays >= 30 && deviceStats[deviceCfg.deviceID].lastSeenDays < 365" translate>More than a month ago</i>
+                              <i class="text-danger" ng-if="deviceStats[deviceCfg.deviceID].lastSeenDays >= 365" translate>More than a year ago</i>
+                            </div>
+                          </div>
+                        </td>
                       </tr>
                       <tr ng-if="!connections[deviceCfg.deviceID].connected && deviceFolders(deviceCfg).length > 0">
                         <th><span class="fas fa-fw fa-cloud"></span>&nbsp;<span translate>Sync Status</span></th>
diff --git a/gui/default/syncthing/core/syncthingController.js b/gui/default/syncthing/core/syncthingController.js
index 1280145f595..f0c8ecad4ac 100755
--- a/gui/default/syncthing/core/syncthingController.js
+++ b/gui/default/syncthing/core/syncthingController.js
@@ -865,7 +865,9 @@ angular.module('syncthing.core')
                 $scope.deviceStats = data;
                 for (var device in $scope.deviceStats) {
                     $scope.deviceStats[device].lastSeen = new Date($scope.deviceStats[device].lastSeen);
-                    $scope.deviceStats[device].lastSeenDays = (new Date() - $scope.deviceStats[device].lastSeen) / 1000 / 86400;
+                    if ($scope.deviceStats[device].lastSeen.toISOString() !== '1970-01-01T00:00:00.000Z') {
+                        $scope.deviceStats[device].lastSeenDays = (new Date() - $scope.deviceStats[device].lastSeen) / 1000 / 86400;
+                    }
                 }
                 console.log("refreshDeviceStats", data);
             }).error($scope.emitHTTPError);
@@ -1073,8 +1075,9 @@ angular.module('syncthing.core')
 
         $scope.deviceStatus = function (deviceCfg) {
             var status = '';
+            var unused = $scope.deviceFolders(deviceCfg).length === 0;
 
-            if ($scope.deviceFolders(deviceCfg).length === 0) {
+            if (unused) {
                 status = 'unused-';
             }
 
@@ -1095,7 +1098,11 @@ angular.module('syncthing.core')
             }
 
             // Disconnected
-            return status + 'disconnected';
+            if (!unused && $scope.deviceStats[deviceCfg.deviceID].lastSeenDays >= 7) {
+                return status + 'disconnected-inactive';
+            } else {
+                return status + 'disconnected';
+            }
         };
 
         $scope.deviceClass = function (deviceCfg) {


Test patch: